Repository Public
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