{"version":3,"sources":["../src/registry.ts"],"sourcesContent":["// Private Pool V2 — public key registry client (privacy-wallet pattern).\n//\n// Maps `wallet_pubkey → (viewing_pk, spending_pk_field, payment_address)`\n// so a sender can do a private transfer using the recipient's REGULAR\n// Solana wallet pubkey instead of asking them for a long payment address.\n//\n// Registry stores only PUBLIC key material — no privacy leak. Per-payment\n// unlinkability stays intact via the ADR-008 stealth address mechanism.\n//\n// Usage:\n//   - On `/app/private` after `derivePrivatePoolKeys`, call\n//     `registerInRegistry({ ..., signature })` once. Idempotent — subsequent\n//     calls just refresh the `registered_at` timestamp.\n//   - In send flows (`/app/private` Send, `/app/send` instant), call\n//     `lookupInRegistry({ network, walletPubkey })` to resolve a wallet\n//     pubkey into the keys needed for encryption.\n//\n// Both functions throw on network errors; lookup returns `null` on 404\n// (= \"this wallet hasn't registered yet\").\n\nimport type { PrivatePoolKeyMaterial } from './keys'\n\nexport type RegistryEntry = {\n  network: string\n  wallet_pubkey: string\n  viewing_pk_hex: string\n  spending_pk_field: string\n  payment_address: string\n  registered_at: string\n}\n\nfunction buildUrl(apiBaseUrl: string, path: string): string {\n  return new URL(path, apiBaseUrl.endsWith('/') ? apiBaseUrl : apiBaseUrl + '/').toString()\n}\n\nfunction bytesToHex(bytes: Uint8Array): string {\n  let hex = ''\n  for (const b of bytes) hex += b.toString(16).padStart(2, '0')\n  return hex\n}\n\n/// Build the time-bounded UPDATE challenge. MUST match\n/// `server/src/routes/private-pool-v2.js` PATH B (audit C-5).\n/// Used when re-registering an existing wallet with NEW keys —\n/// without this, a leaked PATH-A signature could forever overwrite\n/// the victim's registry entry and redirect future payments to the\n/// attacker.\nexport function buildRegistryUpdateChallenge(params: {\n  walletPubkey: string\n  paymentAddress: string\n  timestampMs: number\n}): Uint8Array {\n  const message =\n    'RelAI Private Pool V2 — registry UPDATE\\n\\n'\n    + `wallet=${params.walletPubkey}\\n`\n    + `payment_address=${params.paymentAddress}\\n`\n    + `timestamp=${params.timestampMs}\\n`\n  return new TextEncoder().encode(message)\n}\n\n/// POST registry entry — must be called by the wallet owner with their\n/// signature over the canonical key challenge (the same `signature` that\n/// `derivePrivatePoolKeys` was given). Backend Ed25519-verifies against\n/// `walletPubkey`, so no impersonation is possible.\n///\n/// `signature` is the raw `Uint8Array` returned by `wallet.signMessage()`\n/// at onboarding time. Caller is responsible for passing it in.\n///\n/// For RE-registration with NEW keys (audit C-5), the backend requires\n/// a fresh time-bounded UPDATE signature. Pass `updateSignMessage` as\n/// well; the SDK will probe the existing entry and prompt the wallet\n/// for the second sig only when needed (so first-time onboarding stays\n/// at one popup). When `updateSignMessage` is omitted, the SDK falls\n/// back to PATH A and the backend will reject any update-with-different-keys\n/// request with `update_timestamp_invalid` — the caller has to retry\n/// with `updateSignMessage` provided.\nexport async function registerInRegistry(args: {\n  apiBaseUrl: string\n  network: string\n  walletPubkey: string\n  keys: PrivatePoolKeyMaterial\n  signature: Uint8Array\n  /// Optional. When provided, used to sign the UPDATE challenge IF the\n  /// existing registry entry has a different `payment_address` than\n  /// the one being submitted. Skipped on first-time registration.\n  updateSignMessage?: (message: Uint8Array) => Promise<Uint8Array>\n}): Promise<RegistryEntry> {\n  const url = buildUrl(args.apiBaseUrl, 'api/private-pool/v2/registry')\n\n  // Pre-flight: is there an existing entry, and does its payment_address\n  // differ from what we're about to submit? If yes → UPDATE path.\n  let needsUpdateSig = false\n  if (args.updateSignMessage) {\n    try {\n      const existing = await lookupInRegistry({\n        apiBaseUrl: args.apiBaseUrl,\n        network: args.network,\n        walletPubkey: args.walletPubkey,\n      })\n      if (existing && existing.payment_address !== args.keys.paymentAddress) {\n        needsUpdateSig = true\n      }\n    } catch {\n      // Lookup failure → assume first-time registration (matches\n      // backend's PATH A which is more permissive).\n    }\n  }\n\n  let signatureHex: string\n  let updateTimestamp: number | undefined\n  if (needsUpdateSig && args.updateSignMessage) {\n    updateTimestamp = Date.now()\n    const challenge = buildRegistryUpdateChallenge({\n      walletPubkey: args.walletPubkey,\n      paymentAddress: args.keys.paymentAddress,\n      timestampMs: updateTimestamp,\n    })\n    const sig = await args.updateSignMessage(challenge)\n    if (!sig || sig.length !== 64) {\n      throw new Error(`update signature: expected 64 bytes, got ${sig?.length ?? 0}`)\n    }\n    signatureHex = bytesToHex(sig)\n  } else {\n    signatureHex = bytesToHex(args.signature)\n  }\n\n  const r = await fetch(url, {\n    method: 'POST',\n    headers: { 'content-type': 'application/json' },\n    body: JSON.stringify({\n      network: args.network,\n      wallet_pubkey: args.walletPubkey,\n      viewing_pk_hex: bytesToHex(args.keys.viewingPk),\n      spending_pk_field: args.keys.spendingPkField.toString(10),\n      payment_address: args.keys.paymentAddress,\n      signature_hex: signatureHex,\n      ...(updateTimestamp !== undefined ? { update_timestamp: updateTimestamp } : {}),\n    }),\n  })\n  if (!r.ok) {\n    const err = await r.text()\n    throw new Error(`registry POST failed: ${r.status} ${err}`)\n  }\n  const j = (await r.json()) as { ok: boolean; entry: RegistryEntry }\n  return j.entry\n}\n\n/// GET registry entry by wallet pubkey. Returns `null` when the wallet\n/// hasn't registered yet (HTTP 404), throws on other network errors.\nexport async function lookupInRegistry(args: {\n  apiBaseUrl: string\n  network: string\n  walletPubkey: string\n}): Promise<RegistryEntry | null> {\n  const url = new URL(\n    `api/private-pool/v2/registry/${encodeURIComponent(args.walletPubkey)}`,\n    args.apiBaseUrl.endsWith('/') ? args.apiBaseUrl : args.apiBaseUrl + '/',\n  )\n  url.searchParams.set('network', args.network)\n  const r = await fetch(url.toString())\n  if (r.status === 404) return null\n  if (!r.ok) {\n    const err = await r.text()\n    throw new Error(`registry GET failed: ${r.status} ${err}`)\n  }\n  return (await r.json()) as RegistryEntry\n}\n\n/// Helper: convert a registry entry into the shape that\n/// `prepareJoinSplitTransaction` / `prepareDepositTransaction` expect for\n/// `recipient: { spendingPkField, viewingPk }`. Saves the caller a hex\n/// decode + bigint parse round-trip per send.\nexport function registryEntryToRecipient(entry: RegistryEntry): {\n  spendingPkField: bigint\n  viewingPk: Uint8Array\n} {\n  const cleanHex = entry.viewing_pk_hex.startsWith('0x')\n    ? entry.viewing_pk_hex.slice(2)\n    : entry.viewing_pk_hex\n  const viewingPk = new Uint8Array(cleanHex.length / 2)\n  for (let i = 0; i < viewingPk.length; i++) {\n    viewingPk[i] = parseInt(cleanHex.slice(i * 2, i * 2 + 2), 16)\n  }\n  return {\n    spendingPkField: BigInt(entry.spending_pk_field),\n    viewingPk,\n  }\n}\n"],"mappings":"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","names":[]}