aboutsummaryrefslogtreecommitdiffstats
path: root/tools/ark
diff options
context:
space:
mode:
Diffstat (limited to 'tools/ark')
-rw-r--r--tools/ark/live-bridge-demo.js103
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);