Repository Public
1// Live READ-ONLY check of the Cosmos Hub adapter against mainnet: health + identity, balances, deposit scan, tracking of a2// committed and of an unknown transaction, and prepare() for funded public addresses (proves that the two sources agree at3// one block and that the chain's own simulation accepts the transfer). Nothing is signed with a real key and nothing is broadcast.4// Run: SPW_ORIGIN=https://smoke.invalid SPW_RP_ID=smoke.invalid DATABASE_URL=postgres://unused SPW_SIGNER_SECRET=unused npx tsx scripts/smoke-cosmos.mts5// (the API config module insists on those variables; none of them is used here)6import { NETWORKS, cosmosAddressFromPublicKey, formatUnits, pluginFor } from '@spw/core';7import { adapters } from '../services/api/src/chains/index.ts';89const n = NETWORKS.cosmos, a = adapters.cosmos!;10const TEST_MNEMONIC_ADDR = 'cosmos19rl4cm2hmr8afy4kldpxz3fka4jguq0auqdal4'; // m/44'/118'/0'/0/0 of the public test mnemonic (exists on chain, holds dust)11const FEE_COLLECTOR = 'cosmos13pxn9n3qw79e03844rdadagmg0nshmwf7qvuye'; // module account "feemarket-fee-collector"12const UNFUNDED = cosmosAddressFromPublicKey('cosmos', Uint8Array.from([2, ...new Uint8Array(31), 1])); // address of the curve point with x = 1: on chain nowhere, and nobody knows its private key13const atom = (v: bigint) => formatUnits(v, 6);14const json = (v: unknown) => JSON.stringify(v, (_k, x) => (typeof x === 'bigint' ? x.toString() : x));15const step = async (name: string, f: () => Promise<unknown>) => {16 try { const r = await f(); console.log(name.padEnd(40), typeof r === 'string' ? r : json(r)); }17 catch (e) { console.log(name.padEnd(40), 'REFUSED', (e as { code?: string }).code ?? '', '-', (e as Error).message); }18};19const get = async (u: string, path: string) => (await (await fetch(u + path, { signal: AbortSignal.timeout(15000) })).json()) as any;2021console.log('sources', n.rpc.join(' , '), '| chainRef', n.chainRef);22const h = await a.health();23console.log('health', json(h));24if (!h.ok || !h.identityVerified) { console.log('FAIL: health'); process.exit(1); }2526for (const u of n.rpc) {27 const [info, price, cfg] = await Promise.all([get(u, '/cosmos/base/tendermint/v1beta1/node_info'), get(u, '/feemarket/v1/gas_price/uatom'), get(u, '/cosmos/base/node/v1beta1/config')]);28 console.log(`node ${new URL(u).host}: network=${info.default_node_info.network} app=${info.application_version.name} ${info.application_version.version} sdk=${info.application_version.cosmos_sdk_version}` +29 ` | feemarket gas price=${price.price.amount}${price.price.denom} | node minimum=${cfg.minimum_gas_price}`);30}3132for (const addr of [TEST_MNEMONIC_ADDR, FEE_COLLECTOR, UNFUNDED]) await step(`balance ${addr}`, async () => atom((await a.balances(addr)).native!) + ' ATOM');3334// recent plain sends, to find funded public accounts and a committed transaction (read block by block from a source)35const sends: { from: string; to: string; amount: string; height: number }[] = [];36for (let hgt = h.height! - 1; hgt > h.height! - 40 && sends.length < 12; hgt--) {37 let txs: any[] = [];38 for (const u of n.rpc) { try { txs = (await get(u, `/cosmos/tx/v1beta1/txs/block/${hgt}`)).txs ?? []; break; } catch { /* next source */ } }39 for (const t of txs) {40 const m = t.body.messages;41 if (m.length === 1 && m[0]['@type'] === '/cosmos.bank.v1beta1.MsgSend' && m[0].amount.length === 1 && m[0].amount[0].denom === 'uatom') sends.push({ from: m[0].from_address, to: m[0].to_address, amount: m[0].amount[0].amount, height: hgt });42 }43}44console.log(`plain ATOM sends in the last blocks: ${sends.length}`);45const sample = sends[0];46if (!sample) { console.log('SMOKE INCOMPLETE: no recent send found'); process.exit(1); }47const recipient = sample.to;4849const first = await a.deposits(recipient, null);50console.log('deposits (first scan)'.padEnd(40), `${first.items.length} incoming ATOM transfers to ${recipient} in the latest page, cursor=${first.cursor}, finalized=${first.finalizedHeight}` +51 (first.items[0] ? `; latest: ${atom(first.items[0].amount)} ATOM from ${first.items[0].from} in block ${first.items[0].block} (${first.items[0].txid}, event ${first.items[0].idx})` : ''));52const found = first.items.find((d) => d.block === sample.height && d.from === sample.from);53console.log('deposit scan finds the sampled send'.padEnd(40), found ? `yes: ${atom(found.amount)} ATOM (the block says ${sample.amount} uatom)` : 'NO');54const inc = await a.deposits(recipient, sample.height - 1);55console.log('deposits (incremental)'.padEnd(40), `${inc.items.length} since block ${sample.height - 1}, cursor=${inc.cursor}`);56const none = await a.deposits(recipient, first.cursor);57console.log('deposits (nothing new)'.padEnd(40), `${none.items.length} since block ${first.cursor}`);58await step('deposits of the test-mnemonic address', async () => `${(await a.deposits(TEST_MNEMONIC_ADDR, null)).items.length} (its history is older than the nodes' index)`);5960// tracking61const tip = h.height!;62if (found) await step(`track committed tx ${found.txid.slice(0, 12)}…`, () => a.track({ txid: found.txid, intent: { from: sample.from, x: { sequence: '0', timeout_height: String(sample.height + 5) } }, blockRef: null, broadcastAt: 0 }));63await step('track unknown tx (window open)', () => a.track({ txid: 'AB'.repeat(32), intent: { from: TEST_MNEMONIC_ADDR, x: { sequence: '7', timeout_height: String(tip + 15) } }, blockRef: null, broadcastAt: 0 }));64await step('track unknown tx (expired, seq unused)', async () => {65 const seqNow = (await get(n.rpc[0]!, `/cosmos/auth/v1beta1/accounts/${TEST_MNEMONIC_ADDR}`)).account.sequence as string;66 return a.track({ txid: 'AB'.repeat(32), intent: { from: TEST_MNEMONIC_ADDR, x: { sequence: seqNow, timeout_height: String(tip - 50) } }, blockRef: null, broadcastAt: 0 });67});68await step('track unknown tx (expired, seq used)', () => a.track({ txid: 'AB'.repeat(32), intent: { from: TEST_MNEMONIC_ADDR, x: { sequence: '1', timeout_height: String(tip - 50) } }, blockRef: null, broadcastAt: 0 }));6970// prepare(): refusals that need both sources to agree on the accounts first71await step('prepare test-mnemonic → recipient 1 ATOM', () => a.prepare({ asset: 'native', from: TEST_MNEMONIC_ADDR, to: recipient, amount: 1_000_000n }));72await step('prepare from account not on chain', () => a.prepare({ asset: 'native', from: UNFUNDED, to: recipient, amount: 1_000_000n }));73await step('prepare with an IBC denom', () => a.prepare({ asset: 'ibc/27394FB092D2ECCD56123C74F36E4C1F926001CEADA9CA97EA622B25F41E5EB2', from: TEST_MNEMONIC_ADDR, to: recipient, amount: 1n }));7475// Full path for funded public accounts (senders of recent sends): two-source agreement at one block, fee market price,76// simulation on both sources; returns the section of the intent.77const senders = [...new Set(sends.map((t) => t.from))].slice(0, 8);78let done = 0;79for (const from of senders) {80 if (done >= 1) break;81 try {82 const bal = (await a.balances(from)).native!;83 const p = await a.prepare({ asset: 'native', from, to: TEST_MNEMONIC_ADDR, amount: 1000n, memo: 'smoke 104588310' });84 console.log(`prepare ${from} (${atom(bal)} ATOM) → ${TEST_MNEMONIC_ADDR} (0.001 ATOM, memo): OK`, json(p));85 // the plugin accepts exactly this section (unsigned build only; no key involved)86 const intent = { v: 1 as const, proposalId: '6f1c1f0e-8f5f-4b1e-9c56-0d6b5f0f1a11', keyId: 'spw-0123456789abcdef', createdAt: 1, expiresAt: 2, policyVersion: 2, network: n.id, chainRef: n.chainRef,87 asset: 'native', symbol: 'ATOM', decimals: 6, amount: '1000', from, to: TEST_MNEMONIC_ADDR, maxFee: p.maxFee.toString(), memo: 'smoke 104588310', ...(p.section as { x: Record<string, string> }) };88 console.log('plugin.build on that section:', json(pluginFor(n).build(intent, n).describe));89 console.log(`timeout_height − tip = ${Number(intent.x.timeout_height) - tip} blocks; fee ${atom(p.maxFee)} ATOM for gas_limit ${intent.x.gas_limit}`);90 await step('prepare same, no memo', () => a.prepare({ asset: 'native', from, to: TEST_MNEMONIC_ADDR, amount: 1000n }));91 await step('prepare same → account not on chain', () => a.prepare({ asset: 'native', from, to: UNFUNDED, amount: 1000n }));92 await step('prepare same → module account', () => a.prepare({ asset: 'native', from, to: FEE_COLLECTOR, amount: 1000n }));93 await step('prepare same, more than balance', () => a.prepare({ asset: 'native', from, to: TEST_MNEMONIC_ADDR, amount: bal + 1n }));94 await step('prepare same, balance minus 1 uatom', () => a.prepare({ asset: 'native', from, to: TEST_MNEMONIC_ADDR, amount: bal - 1n }));95 await step('prepare same, memo with a trailing space', () => a.prepare({ asset: 'native', from, to: TEST_MNEMONIC_ADDR, amount: 1000n, memo: '104588310 ' }));96 done += 1;97 } catch (e) { console.log(`prepare ${from} → …: REFUSED ${(e as { code?: string }).code ?? ''} - ${(e as Error).message}`); }98}99console.log(done ? 'SMOKE OK' : 'SMOKE INCOMPLETE: no candidate account passed prepare()');100process.exit(done ? 0 : 1);101