aboutsummaryrefslogtreecommitdiffstats
path: root/tools/ark/live-bridge-demo.js
blob: 2d3649c56f77871b6ee23e01cc63c481ad66fd97 (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
// 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
//
// Runs entirely over public HTTPS. NB the ASP hostname needs a Cloudflare
// managed-WAF skip for POSTs under /bark_server. and /mailbox_server. —
// binary protobuf trips signature rules above ~1.5KB.

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);