SPB Git

Repository Public

smoke-utxo.mts 4.8 KB 59 lines typescript

1// Live, read-only check of the Dogecoin and Bitcoin Cash adapters against mainnet. Nothing is signed and nothing is broadcast.2//   npx tsx scripts/smoke-utxo.mts            (ONLY=dogecoin or ONLY=bitcoincash to run one)3// For each network: both indexers' tip and chain-identity block, health, the balance of a funded public address read from each4// indexer, a prepare() run (the two indexers must return the same spendable coins) and a rebuild of the unsigned transaction5// from the resulting intent — for Dogecoin this verifies the previous transactions fetched live against the approved inputs.6process.env.SPW_ORIGIN ??= 'https://wallet.invalid';7process.env.SPW_RP_ID ??= 'wallet.invalid';8process.env.DATABASE_URL ??= 'postgres://unused';9process.env.SPW_SIGNER_SECRET ??= 'unused-unused-unused-unused-unused-unused-unused';10const { buildBtcTx, formatUnits, IntentSchema, NETWORKS, pluginFor } = await import('@spw/core');11const { adapters } = await import('../services/api/src/chains/index.ts');12const { sourceOf } = await import('../services/api/src/chains/btc.ts');1314// Funded third-party addresses found on-chain (a single old unspent output each); `to` is the public test-mnemonic address.15const CASES: Record<string, { from: string; to: string; amount: bigint }> = {16  dogecoin: { from: 'DMhCxuxFe98xQhic6QEFdw7gWBCy6u9jMS', to: 'DBus3bamQjgJULBJtYXpEzDWQRwF5iwxgC', amount: 1_000_000_000n },17  bitcoincash: { from: 'bitcoincash:qzeer89eku4hkh3w7mrzra8ppwty27jq6vdpg7tpa0', to: 'bitcoincash:qqyx49mu0kkn9ftfj6hje6g2wfer34yfnq5tahq3q6', amount: 100_000n },18};19let failed = false;20for (const [id, c] of Object.entries(CASES)) {21  if (process.env.ONLY && !process.env.ONLY.split(',').includes(id)) continue;22  const n = NETWORKS[id];23  if (!n) { console.log(`${id}: NOT REGISTERED`); failed = true; continue; }24  const a = adapters[id]!, unit = (v: bigint) => `${formatUnits(v, n.native.decimals)} ${n.native.symbol}`;25  console.log(`\n== ${n.name} (${id}, family ${n.family}, coin type ${n.coinType})`);26  try {27    const refHeight = n.utxo?.refHeight ?? 0;28    for (const spec of n.rpc) {29      const s = sourceOf(spec), t = Date.now();30      const [tip, ref, bal] = [await s.tip(), await s.blockHash(refHeight), await s.balance(c.from)];31      console.log(`  source ${new URL(s.url).host.padEnd(26)} tip=${tip} block[${refHeight}]=${ref.slice(0, 20)}… ${ref === n.chainRef ? 'identity OK' : 'IDENTITY MISMATCH'} balance(${c.from.slice(-8)})=${unit(bal)} ${Date.now() - t}ms`);32      if (ref !== n.chainRef) failed = true;33    }34    const h = await a.health();35    console.log(`  health ok=${h.ok} identityVerified=${h.identityVerified} height=${h.height} endpoints=${h.endpoints.map((e) => `${e.url}:${e.ok ? e.ms + 'ms' : 'DOWN'}`).join(' ')}`);36    if (!h.ok) failed = true;37    console.log(`  balance ${c.from} = ${unit((await a.balances(c.from)).native!)}`);38    const d = await a.deposits(c.from, null);39    console.log(`  deposits ${d.items.length} (first: ${d.items[0] ? `${d.items[0].txid.slice(0, 16)}… ${unit(d.items[0].amount)} block ${d.items[0].block}` : '-'}) finalizedHeight=${d.finalizedHeight}`);40    const p = await a.prepare({ asset: 'native', from: c.from, to: c.to, amount: c.amount });41    const now = Math.floor(Date.now() / 1000);42    const intent = IntentSchema.parse({ v: 1, proposalId: '00000000-0000-4000-8000-000000000000', keyId: 'spw-smoke-read-only', network: id, chainRef: n.chainRef, asset: 'native',43      symbol: n.native.symbol, decimals: n.native.decimals, amount: c.amount.toString(), from: c.from, to: c.to, maxFee: p.maxFee.toString(), createdAt: now, expiresAt: now + p.ttlSec, policyVersion: 0, ...p.section });44    console.log(`  prepare: both sources agree on the spendable coins; send ${unit(c.amount)} fee=${unit(p.feeEstimate)} section=${JSON.stringify(p.section).replace(/([0-9a-f]{12})[0-9a-f]{52}/g, '$1…')}`);45    if (n.family === 'btc') {46      const tx = buildBtcTx(intent, p.aux);47      const prev = Object.entries((p.aux?.prevTx ?? {}) as Record<string, string>);48      console.log(`  rebuild: unsigned transaction ${tx.inputsLength} in / ${tx.outputsLength} out; previous transactions verified against the inputs: ${prev.map(([k, v]) => `${k.slice(0, 12)}… (${v.length / 2} bytes)`).join(', ')}`);49    } else {50      const b = pluginFor(n).build(intent, n, p.aux);51      console.log(`  rebuild: ${b.signDocs.length} signature digest(s); ${JSON.stringify(b.describe)}`);52    }53    const st = await a.track({ txid: 'ff'.repeat(32), intent: null, blockRef: null, broadcastAt: now });54    console.log(`  track(unknown txid) = ${JSON.stringify(st)}`);55  } catch (e) { failed = true; console.log(`  ERR ${(e as { code?: string }).code ?? ''} ${(e as Error).message}`); }56}57console.log(failed ? '\nSMOKE FAILED' : '\nSMOKE OK (read-only; nothing signed, nothing broadcast)');58process.exit(failed ? 1 : 0);59
Files main
Menu

Theme