SPB Git

Repository Public

smoke-stellar.mts 5.7 KB 50 lines typescript

1// Live READ-ONLY check of the Stellar adapter against mainnet: health + identity, balances, deposit scan, prepare() from a funded2// public address (proves both sources agree) and tracking of a real / an unknown transaction. Nothing is signed or broadcast.3// Run: npx tsx scripts/smoke-stellar.mts4import { createHash } from 'node:crypto';5for (const [k, v] of Object.entries({ SPW_ORIGIN: 'https://smoke.invalid', SPW_RP_ID: 'smoke.invalid', DATABASE_URL: 'postgres:///unused', SPW_SIGNER_SECRET: 'unused' })) process.env[k] ??= v;6const { NETWORKS, formatUnits, intentHash, pluginFor, stellarEncodeAccount, toHex } = await import('@spw/core');7const { stellarAdapter } = await import('../services/api/src/chains/stellar.ts');89const n = NETWORKS.stellar, a = stellarAdapter(n), xlm = (v: bigint) => `${formatUnits(v, 7)} XLM`;10const FUNDED = process.env.XLM_FROM ?? 'GA5XIGA5C7QTPTWXQHY6MCJRMTRZDOSHR6EFIBNDQTCQHG262N4GGKTM'; // long-lived exchange account11const EXISTING = process.env.XLM_TO ?? 'GBN7BKMAFTQXLW27B4WYFU7VS34V422S6ZCD3GUCC4MY4UBGUMNNKCJR';12const FRESH = stellarEncodeAccount(createHash('sha256').update(`spw-smoke-nonexistent-${Date.now()}`).digest());13let failed = 0;14const step = async (name: string, f: () => Promise<unknown>) => {15  try { console.log(name.padEnd(34), JSON.stringify(await f(), (_k, v) => (typeof v === 'bigint' ? v.toString() : v))); }16  catch (e) { failed++; console.log(name.padEnd(34), 'ERR', (e as { code?: string }).code ?? '', (e as Error).message); }17};18const expectErr = (code: string, f: () => Promise<unknown>) => async () => { try { await f(); } catch (e) { if ((e as { code?: string }).code === code) return `refused as expected: ${(e as Error).message}`; throw e; } throw new Error(`expected ${code}`); };19/** prepare, then rebuild the exact transaction with the plugin from the resulting intent (as the API does before approval) */20const prepareAndBuild = async (to: string, amount: bigint, memo?: string) => {21  const p = await a.prepare({ asset: 'native', from: FUNDED, to, amount, memo });22  const now = Math.floor(Date.now() / 1000);23  const intent = { v: 1 as const, proposalId: '6f1c1f0e-8f5f-4b1e-9c56-0d6b5f0f1a11', keyId: 'spw-smoke-0000000', network: n.id, chainRef: n.chainRef, asset: 'native', symbol: 'XLM', decimals: 7,24    amount: amount.toString(), from: FUNDED, to, maxFee: p.maxFee.toString(), createdAt: now, expiresAt: now + p.ttlSec, policyVersion: 3, ...(memo ? { memo } : {}), ...(p.section as { x: Record<string, string> }) };25  const built = pluginFor(n).build(intent, n);26  return { x: intent.x, maxFee: p.maxFee, warnings: p.warnings, ttlSec: p.ttlSec, intentHash: toHex(intentHash(intent)), txid: built.describe.txid };27};2829await step('health', async () => { const h = await a.health(); if (!h.ok || !h.identityVerified) throw new Error(JSON.stringify(h)); return h; });30await step('balance (funded public account)', async () => { const b = await a.balances(FUNDED); if (b.native! <= 0n) throw new Error('zero'); return xlm(b.native!); });31await step('balance (account that does not exist)', async () => { const b = await a.balances(FRESH); if (b.native !== 0n) throw new Error('non-zero'); return xlm(b.native); });32let lastTx = '';33await step('deposits (first scan)', async () => {34  const d = await a.deposits(FUNDED, null); lastTx = d.items[0]?.txid ?? '';35  if (d.items.length > 0) await step('deposits (incremental)', async () => { const e = await a.deposits(FUNDED, d.items[d.items.length - 1]!.block - 1); return { items: e.items.length, cursor: e.cursor }; });36  return { items: d.items.length, cursor: d.cursor, finalizedHeight: d.finalizedHeight, newest: d.items[0] && { ...d.items[0], amount: xlm(d.items[0].amount) } };37});38await step('prepare → existing account', async () => { const r = await prepareAndBuild(EXISTING, 12_500_000n); if (r.x.op !== 'payment') throw new Error('expected payment'); return r; });39await step('prepare → existing, numeric memo', async () => { const r = await prepareAndBuild(EXISTING, 12_500_000n, '123456'); if (r.x.memoType !== 'id') throw new Error('expected id'); return r; });40await step('prepare → new account (2 XLM)', async () => { const r = await prepareAndBuild(FRESH, 20_000_000n, 'hello'); if (r.x.op !== 'createAccount') throw new Error('expected createAccount'); return r; });41await step('prepare → new account (0.5 XLM)', expectErr('below_reserve', () => a.prepare({ asset: 'native', from: FUNDED, to: FRESH, amount: 5_000_000n })));42await step('prepare ← unfunded source', expectErr('insufficient_funds', () => a.prepare({ asset: 'native', from: FRESH, to: EXISTING, amount: 5_000_000n })));43await step('prepare, more than the balance', expectErr('insufficient_funds', () => a.prepare({ asset: 'native', from: FUNDED, to: EXISTING, amount: 9_000_000_000_000_000_000n })));44const nowSec = Math.floor(Date.now() / 1000);45if (lastTx) await step('track (real included transaction)', async () => { const t = await a.track({ txid: lastTx, intent: { x: { maxTime: String(nowSec) } }, blockRef: null, broadcastAt: nowSec }); if (t.state !== 'finalized') throw new Error(t.state); return t; });46await step('track (unknown, time bound open)', async () => { const t = await a.track({ txid: 'ab'.repeat(32), intent: { x: { maxTime: String(nowSec + 600) } }, blockRef: null, broadcastAt: nowSec }); if (t.state !== 'broadcast') throw new Error(t.state); return t; });47await step('track (unknown, time bound passed)', async () => { const t = await a.track({ txid: 'ab'.repeat(32), intent: { x: { maxTime: String(nowSec - 600) } }, blockRef: null, broadcastAt: nowSec - 800 }); if (t.state !== 'failed') throw new Error(t.state); return t; });48console.log(failed ? `FAILED: ${failed} step(s)` : 'OK: all steps passed (read-only, nothing signed or broadcast)');49process.exit(failed ? 1 : 0);50
Files main
Menu

Theme