aboutsummaryrefslogtreecommitdiffstats
path: root/tools/gift-scope-test.js
blob: 71f36af778ce9cbff9c2122dec13be7bb9a05f37 (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
// Gift records must not follow you from one wallet into the next.
//
// The records live under a seed-derived localStorage key, so on disk they were
// always separate. The leak was in memory: giftRecords() caches the parsed blob
// on the wallet object, the app runs a single Wallet for its whole lifetime,
// and load() never dropped that cache. Switch wallets and the old wallet's
// gifts were still sitting there — every one of them rendering as "claimed",
// since none of their outpoints are live in the new wallet.
import { Wallet } from '../src/wallet.js';
import { installGiftWallet } from '../src/features/gifts-wallet.js';

const store = new Map();
globalThis.localStorage = {
  getItem: (k) => (store.has(k) ? store.get(k) : null),
  setItem: (k, v) => store.set(k, String(v)),
  removeItem: (k) => store.delete(k),
};

const A = 'abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about';
const B = 'legal winner thank year wave sausage worth useful legal winner thank yellow';

const wallet = new Wallet();
installGiftWallet(wallet);

let failures = 0;
const check = (name, ok, detail) => {
  console.log(`${ok ? 'ok  ' : 'FAIL'} ${name}${ok || !detail ? '' : ` — ${detail}`}`);
  if (!ok) failures++;
};

// Wallet A creates a gift and its record is cached in memory.
wallet.load({ mnemonic: A });
wallet.giftRecords()['g1'] = {
  id: 'g1', amount: 2100, created: Date.now() - 28 * 864e5, outpoints: ['deadbeef:0'],
};
wallet._saveGiftRecords();
check('wallet A sees its own gift', Object.keys(wallet.giftRecords()).length === 1);

// Switch to a brand new wallet B.
wallet.load({ mnemonic: B });
const leaked = Object.keys(wallet.giftRecords());
check('wallet B starts with no gift records', leaked.length === 0, `saw ${JSON.stringify(leaked)}`);

// And B must not write A's records under its own key.
wallet.giftRecords()['g2'] = { id: 'g2', amount: 500, created: Date.now(), outpoints: ['cafe:1'] };
wallet._saveGiftRecords();
wallet.load({ mnemonic: A });
const backInA = Object.keys(wallet.giftRecords());
check('wallet A is untouched by B', backInA.length === 1 && backInA[0] === 'g1', `saw ${JSON.stringify(backInA)}`);

wallet.load({ mnemonic: B });
const backInB = Object.keys(wallet.giftRecords());
check('wallet B kept only its own', backInB.length === 1 && backInB[0] === 'g2', `saw ${JSON.stringify(backInB)}`);

// Records an older build already wrote under the wrong key are forgotten on
// sight: their funding tx isn't in this wallet's history.
wallet.load({ mnemonic: B });
store.set(wallet._giftRecordsKey(), JSON.stringify({
  'aa:0': { id: 'aa:0', amount: 2100, created: Date.now() - 28 * 864e5, outpoints: ['aa:0'] },
  'bb:0': { id: 'bb:0', amount: 2100, created: Date.now() - 28 * 864e5, outpoints: ['bb:0'] },
}));
wallet.loaded = true;
wallet.txs = [];
check('foreign records are dropped from history', wallet.claimedGifts().length === 0);
check('and purged from storage', JSON.parse(store.get(wallet._giftRecordsKey())) &&
  !Object.keys(JSON.parse(store.get(wallet._giftRecordsKey()))).length);

// But a gift this wallet really did issue still shows.
wallet.load({ mnemonic: B });
store.set(wallet._giftRecordsKey(), JSON.stringify({
  'cc:0': { id: 'cc:0', amount: 2100, created: Date.now() - 864e5, outpoints: ['cc:0'] },
}));
wallet.loaded = true;
wallet.txs = [{ txid: 'cc' }];
wallet.utxos = [];
check('our own claimed gift survives', wallet.claimedGifts().length === 1);

console.log(failures ? `\n${failures} failing` : '\nall passing');
process.exit(failures ? 1 : 0);
lass="p">(keys.mailbox.pubkey, hex.decode(info.mailboxPubkey), keys.vtxo.privkey); const address = encodeAddress({ testnet: info.network !== 'bitcoin', serverPubkey: hex.decode(info.serverPubkey), userPubkey: keys.vtxo.pubkey, blindedMailboxId: blindedId, }); console.log('\nour ark address:', address); console.log('round-trip decode:', decodeAddress(address)); // --- 3. pay it from the bark CLI --- console.log(`\npaying ${AMOUNT_SAT} sat from alice (bark CLI)...`); execSync(`${ALICE} send ${address} "${AMOUNT_SAT} sat"`, { stdio: 'inherit' }); // --- 4. read mailbox, decode, validate --- let received = null; for (let attempt = 0; attempt < 15 && !received; attempt++) { const { messages } = await readMailbox(ARK, keys.mailbox); for (const m of messages) { if (m.kind === 'arkoor' && m.vtxos.length) received = m; } if (!received) await new Promise((res) => setTimeout(res, 2000)); } if (!received) { console.error('❌ no arkoor message in mailbox after 30s'); process.exit(1); } console.log(`\nmailbox message (checkpoint ${received.checkpoint}), ${received.vtxos.length} vtxo(s):`); const ourPub = hex.encode(keys.vtxo.pubkey); let ok = true; const check = (name, cond, detail = '') => { console.log(` ${cond ? '✓' : '✗'} ${name}${detail ? ' — ' + detail : ''}`); if (!cond) ok = false; }; for (const v of received.vtxos) { console.log(`\nvtxo ${v.id}`); console.log(` amount: ${v.amountSat} sat, expiry height: ${v.expiryHeight}, exit delta: ${v.exitDelta}`); console.log(` anchor: ${v.anchorPoint.txid}:${v.anchorPoint.vout}, genesis depth: ${v.genesis.length}`); console.log(` transitions: ${v.genesis.map((g) => g.transition.type).join(' -> ')}`); check('policy is pubkey', v.policy.type === 'pubkey', v.policy.type); check('paid to our key', v.policy.userPubkey === ourPub, v.policy.userPubkey); check('amount matches', v.amountSat === AMOUNT_SAT, String(v.amountSat)); check('server pubkey matches ark info', v.serverPubkey === info.serverPubkey); check('all genesis transitions signed', v.genesis.every((g) => g.transition.signature)); const st = await fetch(`${ESPLORA}/tx/${v.anchorPoint.txid}/status`).then((r) => r.json()); check('chain anchor tx confirmed onchain', st.confirmed === true, `esplora height ${st.block_height}`); const tip = await fetch(`${ESPLORA}/blocks/tip/height`).then((r) => r.text()); check('not expired', v.expiryHeight > Number(tip), `expires ${v.expiryHeight}, tip ${tip}`); } console.log(ok ? `\n✅ SUCCESS: native-JS client received + decoded + validated an off-chain Ark payment (balance: ${received.vtxos.reduce((n, v) => n + v.amountSat, 0)} sat)` : '\n❌ some checks failed'); process.exit(ok ? 0 : 1);