Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
e937ae6
feat(CBOR-Intergration- wallet-import-api): Create api route and vali…
Andre-Diamond Sep 30, 2025
f14c152
refactor(api): streamline multisig import handling and improve wallet…
Andre-Diamond Oct 1, 2025
a1fbb97
refactor(api): simplify signer address retrieval in ejection redirect…
Andre-Diamond Oct 1, 2025
5e24254
Merge remote-tracking branch 'origin/main' into feature/CBOR-Integrat…
Andre-Diamond Oct 2, 2025
4d828b9
refactor(api): update signer descriptions and enhance multisig import…
Andre-Diamond Oct 2, 2025
06e077b
Merge remote-tracking branch 'origin/main' into feature/CBOR-Integrat…
Andre-Diamond Oct 13, 2025
7795ad6
feat(wallet): integrate paymentCbor into wallet data structure
Andre-Diamond Oct 13, 2025
7771e5f
fix(wallet): make paymentCbor field optional in NewWallet model
Andre-Diamond Oct 13, 2025
d37e8f3
docs(ejection): update README to remove optional user and community f…
Andre-Diamond Oct 14, 2025
2cab4ec
feat(wallet): enhance NewWallet model and API to support additional C…
Andre-Diamond Oct 14, 2025
9976a24
feat(multisig): enhance validation to support payment and stake signa…
Andre-Diamond Oct 15, 2025
d656e85
feat(multisig): improve stake key ordering and address resolution in …
Andre-Diamond Oct 15, 2025
ea8cf40
feat(multisig): update validation to support asynchronous payload pro…
Andre-Diamond Oct 16, 2025
ec29fa3
docs(ejection): update README and redirect API for multisig wallet en…
Andre-Diamond Oct 16, 2025
484b2e3
feat(multisig): enhance validation and API to support user names and …
Andre-Diamond Oct 17, 2025
b2a033a
fix(ejection): standardize ownerAddress in redirect API to "all"
Andre-Diamond Oct 17, 2025
a7af71e
refactor(ejection): remove deprecated redirect API and documentation
Andre-Diamond Oct 20, 2025
e574333
feat(multisig): enhance validation to derive users from stakeCbor whe…
Andre-Diamond Oct 20, 2025
850cafd
feat(multisig): refine stake key ordering and address resolution in v…
Andre-Diamond Oct 23, 2025
811c6c1
fix(multisig): correct fallback logic in multisig import validation
Andre-Diamond Oct 23, 2025
a3812d8
Merge pull request #142 from MeshJS/feature/CBOR-Integration-wallet-i…
QSchlegel Oct 23, 2025
1e33793
refactor(api): enhance description sanitization in wallet import and …
Andre-Diamond Oct 23, 2025
f41f810
refactor(api): unify tag removal logic in description sanitization
Andre-Diamond Oct 23, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
-- DropIndex
DROP INDEX "BalanceSnapshot_snapshotDate_idx";

-- DropIndex
DROP INDEX "BalanceSnapshot_walletId_idx";

-- AlterTable
ALTER TABLE "NewWallet" ADD COLUMN "paymentCbor" TEXT;
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
-- AlterTable
ALTER TABLE "NewWallet" ADD COLUMN "stakeCbor" TEXT,
ADD COLUMN "usesStored" BOOLEAN NOT NULL DEFAULT false;
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
-- AlterTable
ALTER TABLE "NewWallet" ADD COLUMN "rawImportBodies" JSONB;
4 changes: 4 additions & 0 deletions prisma/schema.prisma
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,10 @@ model NewWallet {
ownerAddress String
stakeCredentialHash String?
scriptType String?
usesStored Boolean @default(false)
paymentCbor String?
stakeCbor String?
rawImportBodies Json?
}

model Nonce {
Expand Down
117 changes: 117 additions & 0 deletions src/pages/api/v1/import/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
### Import Summon API

Simple import endpoint to create/update a multisig wallet from an external data dump and return an invite URL.

### Endpoint

- POST `/api/v1/import/summon`

### What it does

- Validates an incoming `{ community, multisig, users }` payload for a multisig import.
- Upserts a `NewWallet` with signer data, `paymentCbor` (from `multisig.payment_script`), and `stakeCbor` (from `multisig.stake_script`).
- Returns the invite URL for the newly imported wallet.

### Request body

Send a single JSON object with these properties:

- `community` (object)
- `id` (string)
- `name` (string)
- `description` (string; HTML allowed, tags are stripped for storage)
- `profile_photo_url` (string)
- `verified` (boolean)
- `verified_name` (string)
- `multisig` (object)
- `id` (string)
- `name` (string)
- `address` (string; validated)
- `created_at` (string; ISO or timestamp-like)
- `payment_script` (string; CBOR hex of native script)
- `stake_script` (string; CBOR hex of native script)
- `users` (array of objects)
- `id` (string)
- `name` (string)
- `address_bech32` (string; optional)
- `stake_pubkey_hash_hex` (string; 56-char lowercase hex; required)
- `ada_handle` (string; optional)
- `profile_photo_url` (string; optional)

Minimal example payload:

```json
{
"community": {
"id": "28e2bee1-9b21-4393-bf7b-ec68c012a795",
"name": "Smart Contract Audit Token (SCATDAO)",
"description": "<p>A DAO for decentralized audits, research, and safety on Cardano. </p>",
"profile_photo_url": "https://scatdao.b-cdn.net/wp-content/uploads/2021/09/scatdao_graphic.png",
"verified": true,
"verified_name": "scatdao"
},
"multisig": {
"id": "40b18160-684c-42b2-8523-577165bba8ec",
"name": "Test Community Treasury 2024/25 (1)",
"address": "addr1x809f8t6jy...",
"created_at": "2024-12-06 10:12:39.606+00",
"payment_script": "82018183030386...",
"stake_script": "82018183030386..."
},
"users": [
{
"id": "1876cf5b-37c7-4785-8194-73751134ddbe",
"name": "Alice",
"address_bech32": "addr1q8772af8wuvzksqx5p679p5wqsq...",
"stake_pubkey_hash_hex": "40b39bac8ce1b8b527899f8ad19e51...",
"ada_handle": "",
"profile_photo_url": ""
},
{
"id": "af2b5725-accb-4de9-8fdc-5f439848a2cc",
"name": "Bob",
"address_bech32": "",
"stake_pubkey_hash_hex": "b7d2352a2a8a6661df9657f3fbe93a...",
"ada_handle": "",
"profile_photo_url": ""
}
]
}
```

### Curl example

```bash
curl -X POST \
-H "Content-Type: application/json" \
-d '{
"community": {"id": "c1", "name": "Team", "description": "<p>Our team treasury</p>", "verified": true, "verified_name": "team"},
"multisig": {"id": "msig_123", "name": "Team Treasury", "address": "addr1...", "payment_script": "4a50...c0", "stake_script": "4a50...c0"},
"users": [
{"id": "u1", "name": "Bob", "address_bech32": "", "stake_pubkey_hash_hex": "abcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcd"},
{"id": "u2", "name": "Carol", "address_bech32": "addr1...3zka", "stake_pubkey_hash_hex": "1234123412341234123412341234123412341234123412341234"}
]
}' \
https://multisig.meshjs.dev/api/v1/import/summon
```

### Response

```json
{
"ok": true,
"receivedAt": "2025-10-13T17:50:10.123Z",
"multisigAddress": "addr1...",
"dbUpdated": true,
"inviteUrl": "https://multisig.meshjs.dev/wallets/invite/<newWalletId>"
}
```

### Notes

- Only the`{ community, multisig, users }` shape is accepted.
- The wallet description prefers `community.description` (HTML tags are stripped);
- If a `multisig.id` is supplied, the wallet is upserted with that id; otherwise a new id is created.
- The raw request body is stored in `NewWallet.rawImportBodies`.
- CORS is enabled; `OPTIONS` requests return 200.
- If the database write fails, `dbUpdated` will be `false` and `inviteUrl` will be `null`.
183 changes: 183 additions & 0 deletions src/pages/api/v1/import/summon.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,183 @@
import type { NextApiRequest, NextApiResponse } from "next";
import { cors, addCorsCacheBustingHeaders } from "@/lib/cors";
import { validateMultisigImportPayload } from "@/utils/validateMultisigImport";
import { db } from "@/server/db";


// Draft endpoint: accepts POST request values and logs them
export default async function handler(
req: NextApiRequest,
res: NextApiResponse,
) {
// Add cache-busting headers for CORS
addCorsCacheBustingHeaders(res);

await cors(req, res);
if (req.method === "OPTIONS") {
return res.status(200).end();
}

if (req.method !== "POST") {
return res.status(405).json({ error: "Method Not Allowed" });
}

try {
const receivedAt = new Date().toISOString();

const result = await validateMultisigImportPayload(req.body);
if (!result.ok) {
return res.status(result.status).json(result.body);
}
const { summary, rows } = result;

// Normalize request body into an array for consistent handling of signersDescriptions
const bodyAsArray: unknown[] = req.body == null
? []
: Array.isArray(req.body)
? (req.body as unknown[])
: [req.body];
// Build wallet description from the first non-empty tagless community_description
function removeTagsLinear(input: string) {
let out = "";
let inTag = false;
for (let i = 0; i < input.length; i++) {
const ch = input[i]!;
if (ch === "<") { inTag = true; continue; }
if (ch === ">") { inTag = false; continue; }
if (!inTag) out += ch;
}
return out;
}
function sanitizeDescription(v: string) {
const noTags = removeTagsLinear(v);
return noTags
.replace(/&/g, "&amp;")
.replace(/</g, "&lt;")
.replace(/>/g, "&gt;")
.replace(/"/g, "&quot;")
.replace(/'/g, "&#39;")
.replace(/`/g, "&#96;")
.trim();
}
const walletDescription = (() => {
// Prefer new shape: req.body.community.description
const maybeDesc = (req.body as any)?.community?.description;
if (typeof maybeDesc === "string" && maybeDesc.trim().length > 0) {
return sanitizeDescription(maybeDesc);
}
for (const r of rows) {
const desc = (r as { community_description?: unknown }).community_description;
if (typeof desc === "string" && desc.trim().length > 0) {
return sanitizeDescription(desc);
}
}
return "";
})();

// Use descriptions computed from validation (user_name aligned with signerAddresses)
const signersDescriptions = Array.isArray(summary.signersDescriptions) ? summary.signersDescriptions : [];

// Use signer payment addresses as provided; leave empty string if missing
const paymentAddressesUsed = Array.isArray(summary.signerAddresses)
? summary.signerAddresses.map((addr: string) => (typeof addr === "string" ? addr.trim() : ""))
: [];

// Store the raw request body directly (new API only receives new shape)
const rawImportBodies = req.body;

// Persist to NewWallet using validated data
let dbUpdated = false;
let newWalletId: string | null = null;
try {
const specifiedId = typeof summary.multisigId === "string" && summary.multisigId.trim().length > 0
? summary.multisigId.trim()
: null;

if (specifiedId) {
const updateData: any = {
name: summary.multisigName ?? "Imported Multisig",
description: walletDescription,
signersAddresses: paymentAddressesUsed,
signersStakeKeys: summary.stakeAddressesUsed,
signersDRepKeys: [],
signersDescriptions,
numRequiredSigners: summary.numRequiredSigners,
ownerAddress: "all",
stakeCredentialHash: null,
scriptType: summary.scriptType ?? null,
paymentCbor: summary.paymentCbor ?? "",
stakeCbor: summary.stakeCbor ?? "",
usesStored: Boolean(summary.usesStored),
rawImportBodies,
};
const createDataWithId: any = {
id: specifiedId,
name: summary.multisigName ?? "Imported Multisig",
description: walletDescription,
signersAddresses: paymentAddressesUsed,
signersStakeKeys: summary.stakeAddressesUsed,
signersDRepKeys: [],
signersDescriptions,
numRequiredSigners: summary.numRequiredSigners,
ownerAddress: "all",
stakeCredentialHash: null,
scriptType: summary.scriptType ?? null,
paymentCbor: summary.paymentCbor ?? "",
stakeCbor: summary.stakeCbor ?? "",
usesStored: Boolean(summary.usesStored),
rawImportBodies,
};
const saved = await db.newWallet.upsert({
where: { id: specifiedId },
update: updateData,
create: createDataWithId,
});
console.log("[api/v1/import/summon] NewWallet upsert success:", { id: saved.id });
dbUpdated = true;
newWalletId = saved.id;
} else {
const createData: any = {
name: summary.multisigName ?? "Imported Multisig",
description: walletDescription,
signersAddresses: paymentAddressesUsed,
signersStakeKeys: summary.stakeAddressesUsed,
signersDRepKeys: [],
signersDescriptions,
numRequiredSigners: summary.numRequiredSigners,
ownerAddress: "all",
stakeCredentialHash: null,
scriptType: summary.scriptType ?? null,
paymentCbor: summary.paymentCbor ?? "",
stakeCbor: summary.stakeCbor ?? "",
usesStored: Boolean(summary.usesStored),
rawImportBodies,
};
const created = await db.newWallet.create({
data: createData,
});
console.log("[api/v1/import/summon] NewWallet create success:", { id: created.id });
dbUpdated = true;
newWalletId = created.id;
}
} catch (err) {
console.error("[api/v1/import/summon] NewWallet upsert failed:", err);
}

// Generate the URL for the multisig wallet invite page
const baseUrl = "https://multisig.meshjs.dev";
const inviteUrl = newWalletId ? `${baseUrl}/wallets/invite/${newWalletId}` : null;

return res.status(200).json({
ok: true,
receivedAt,
multisigAddress: summary.multisigAddress,
dbUpdated,
inviteUrl
});
} catch (error) {
console.error("[api/v1/import/summon] Error handling POST:", error);
return res.status(500).json({ error: "Internal Server Error" });
}
}


42 changes: 42 additions & 0 deletions src/pages/api/v1/og.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import type { NextApiRequest, NextApiResponse } from "next";

// Simple OG metadata extractor using fetch + regex fallbacks
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
const { url } = req.query;
if (typeof url !== "string") {
res.status(400).json({ error: "Missing url" });
return;
}

try {
const response = await fetch(url, { method: "GET" });
const html = await response.text();

const extract = (property: string, nameFallback?: string) => {
const ogRegex = new RegExp(`<meta[^>]+property=["']${property}["'][^>]+content=["']([^"']+)["']`, "i");
const ogMatch = ogRegex.exec(html);
if (ogMatch?.[1]) return ogMatch[1];
if (nameFallback) {
const nameRegex = new RegExp(`<meta[^>]+name=["']${nameFallback}["'][^>]+content=["']([^"']+)["']`, "i");
const nameMatch = nameRegex.exec(html);
if (nameMatch?.[1]) return nameMatch[1];
}
return undefined;
};

const title = extract("og:title", "title") ?? (() => {
const titleRegex = /<title>([^<]+)<\/title>/i;
const titleMatch = titleRegex.exec(html);
return titleMatch?.[1];
})();
const description = extract("og:description", "description");
const image = extract("og:image");
const siteName = extract("og:site_name");

res.status(200).json({ title, description, image, siteName, url });
} catch (e: unknown) {
const errorMessage = e instanceof Error ? e.message : "Failed to fetch OG";
res.status(500).json({ error: errorMessage });
}
}

Loading