diff options
| author | Adam Soltys <[email protected]> | 2026-07-31 18:17:25 -0700 |
|---|---|---|
| committer | Adam Soltys <[email protected]> | 2026-07-31 18:17:25 -0700 |
| commit | 3b8f2b63e9fe4ecc83e54e7cc8c757924927f83d (patch) | |
| tree | 400438372c55b749b99bc5248bc485b331c2c342 /tools/ark | |
| parent | 37960a580acd2ab18d53e30e97dd1c46d12679f2 (diff) | |
| download | coinosv3-3b8f2b63e9fe4ecc83e54e7cc8c757924927f83d.tar.gz coinosv3-3b8f2b63e9fe4ecc83e54e7cc8c757924927f83d.zip | |
Add the live bridge demo against the coinos mutinynet ASP
Reference run for the upstream proposal: real deployed ASP, real deployed
bridge, real Lightning settlement — no regtest. Boards a fresh wallet, quotes
a 25 sat zap (1 sat fee vs second's 20 sat mainnet floor), locks the HTLC,
lets the bridge pay and claim, then reads the revealed preimage back out of
the ASP via GetHtlcPreimage.
Board funding goes through a helper on cs because a board output must be
vout 0 and sendtoaddress randomizes output order.
Co-Authored-By: Claude Fable 5 <[email protected]>
Diffstat (limited to 'tools/ark')
| -rw-r--r-- | tools/ark/live-bridge-demo.js | 103 |
1 files changed, 103 insertions, 0 deletions
diff --git a/tools/ark/live-bridge-demo.js b/tools/ark/live-bridge-demo.js new file mode 100644 index 0000000..a8e37a8 --- /dev/null +++ b/tools/ark/live-bridge-demo.js @@ -0,0 +1,103 @@ +// Live demo of the trustless Ark->Lightning bridge against the coinos +// mutinynet ASP. This is the reference run to point upstream at: everything +// here is public infrastructure, no regtest, no local shortcuts. +// +// wallet --HTLC--> arkswap-staging.coinos.io --pays bolt11--> cl-mut +// bridge claims with the preimage +// +// Usage: BRIDGE_TOKEN=... bun tools/ark/live-bridge-demo.js + +import { execSync } from 'node:child_process'; +import { generateMnemonic, mnemonicToSeedSync } from '@scure/bip39'; +import { wordlist } from '@scure/bip39/wordlists/english'; +import { HDKey } from '@scure/bip32'; +import { hex } from '@scure/base'; +import { sha256 } from '@noble/hashes/sha256'; + +import { ArkManager } from '../../src/ark/manager.js'; +import { getHtlcPreimage } from '../../src/ark/lightning.js'; + +const ARK = process.env.ARK_URL || 'https://ark-staging.coinos.io'; +const ESPLORA = process.env.ESPLORA_URL || 'https://mutinynet.com/api'; +const BRIDGE = process.env.BRIDGE_URL || 'https://arkswap-staging.coinos.io'; +const TOKEN = process.env.BRIDGE_TOKEN || ''; + +const sh = (c) => execSync(c, { shell: '/bin/bash' }).toString().trim(); +// invoices come from cl-mut, the same node the bridge pays from — on a demo +// network that's the only node we control; the payment is still a real +// bolt11 settlement observable in listinvoices. +const clmut = (c) => { + const out = sh(`ssh cs 'docker exec cl-mut lightning-cli --signet ${c}'`); + return JSON.parse(out.slice(out.indexOf('{'))); +}; +const mut = (c) => sh(`ssh cs 'docker exec mut bitcoin-cli -signet -rpcwallet=mutiny ${c}'`); +// A board's funding output MUST be vout 0 (captaind rejects it otherwise) and +// sendtoaddress randomizes output order, so funding goes through a helper on +// cs that pins change to position 1. +const fundBoard = (address, btc) => sh(`ssh cs '~/ark/fund-board.sh ${address} ${btc}'`); +const sleep = (ms) => new Promise((r) => setTimeout(r, ms)); + +let ok = true; +const check = (n, c, d = '') => { console.log(` ${c ? '✓' : '✗'} ${n}${d ? ' — ' + d : ''}`); if (!c) ok = false; }; + +const wallet = await new ArkManager({ + account: HDKey.fromMasterSeed(mnemonicToSeedSync(generateMnemonic(wordlist))).derive("m/86'/0'/9'"), + storage: { load: () => null, save: () => {} }, + arkUrl: ARK, esploraUrl: ESPLORA, network: 'mutinynet', +}).init(); + +console.log('\n[0] a fresh wallet boards onto the coinos ASP'); +const { actionId, fundingAddress, feeSat } = await wallet.startBoard(25000); +const txid = fundBoard(fundingAddress, '0.00025'); +console.log(" funding txid", txid.slice(0, 20)); +await wallet.completeBoard(actionId, txid); +for (let i = 0; i < 60 && wallet.pendingActions().length; i++) { + await sleep(5000); + await wallet.sync().catch((e) => console.log(' sync:', e.message)); + if (i % 6 === 5) console.log(` ...step=${wallet.pendingActions()[0]?.step} bal=${JSON.stringify(wallet.balance())}`); +} +check('boarded', wallet.balance().spendableSat === 25000 - feeSat, JSON.stringify(wallet.balance())); +if (!wallet.balance().spendableSat) { console.log('board did not complete; aborting'); process.exit(1); } + +console.log('\n[1] the bridge advertises itself'); +const info = await (await fetch(`${BRIDGE}/info`)).json(); +check('bridge is on our ASP', info.ark.includes('captaind-mut') || info.ark.includes('ark-staging'), info.ark); +check('has outbound liquidity', info.outboundSat > 0, `${info.outboundSat} sat`); + +console.log('\n[2] quote a 25-sat zap'); +const label = `demo-${Date.now()}`; +const inv = clmut(`invoice 25000 ${label} ark-bridge-demo`).bolt11; +const quote = await (await fetch(`${BRIDGE}/quote?invoice=${inv}`)).json(); +check('quoted', quote.amountSat === 25, JSON.stringify(quote).slice(0, 120)); +console.log(` bridge fee ${quote.feeSat} sat (second's mainnet ASP floor: 20 sat)`); +check('cheaper than the ASP floor', quote.feeSat < 20, `${quote.feeSat} sat`); + +console.log('\n[3] lock the HTLC and let the bridge pay'); +const before = wallet.balance().spendableSat; +const lock = await wallet.htlcLock({ + amountSat: quote.totalSat, claimPubkey: quote.claimPubkey, + paymentHash: quote.paymentHash, htlcExpiry: quote.htlcExpiry, +}); +const res = await (await fetch(`${BRIDGE}/swap`, { + method: 'POST', + headers: { 'content-type': 'application/json', ...(TOKEN ? { authorization: `Bearer ${TOKEN}` } : {}) }, + body: JSON.stringify({ invoice: inv, htlcVtxo: lock.htlcVtxos[0] }), +})).json(); +console.log(' ', JSON.stringify(res)); +check('swap completed', res.step === 'done', res.step + (res.error ? ' / ' + res.error : '')); +check('preimage proves payment', !!res.preimage + && hex.encode(sha256(hex.decode(res.preimage))) === quote.paymentHash); +check('invoice settled on the Lightning node', + clmut(`listinvoices ${label}`).invoices[0].status === 'paid'); +check('wallet debited exactly amount + fee', + wallet.balance().spendableSat === before - quote.totalSat, + `${before} -> ${wallet.balance().spendableSat}`); + +console.log('\n[4] the ASP publishes the revealed preimage'); +const revealed = await getHtlcPreimage(ARK, hex.decode(quote.paymentHash)); +check('GetHtlcPreimage returns it', revealed === res.preimage, `${revealed}`); + +console.log(ok + ? `\n✅ 25 sat zap paid over Lightning for a ${quote.feeSat} sat fee, trustlessly, on public infra` + : '\n❌ some checks failed'); +process.exit(ok ? 0 : 1); |
