aboutsummaryrefslogtreecommitdiffstats
path: root/tools/gift-build-test.js
blob: baf06968b812a5993be606d19d0ded27efb406f2 (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
// Exercises the on-chain (Savings) gift BUILDER — the path that had no
// coverage and shipped a "Cannot mix BigInt and other types" crash: the funding
// fee (a Number) was subtracted from BigInt coin sums in _buildGiftPsbt. Builds
// real gifts (specific amount, whole-balance, and the split carve-out) against a
// mock wallet and asserts they don't throw and produce claimable v2 codes.
import assert from 'node:assert';
import { HDKey } from '@scure/bip32';
import { p2wpkh } from '@scure/btc-signer/payment';
import * as btc from '@scure/btc-signer';
import { sha256 } from '@noble/hashes/sha256';

// localStorage shim (reserved/reclaimed sets persist through it)
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 { installGiftWallet, parseLazyGift, previewGift, giftClaimTxs } = await import('../src/features/gifts-wallet.js');

const master = HDKey.fromMasterSeed(sha256(new TextEncoder().encode('gift-build-test-seed')));
const child = (chain, index) => master.derive(`m/84'/0'/0'/${chain}/${index}`);
const NET = btc.NETWORK;

function mockWallet(utxos) {
  const w = {
    utxos,
    netCfg: { net: NET },
    feeRates: { halfHourFee: 5 },
    _cacheKey: () => 'giftbuildtest',
    _loadSet(key) { try { return new Set(JSON.parse(localStorage.getItem(key) || '[]')); } catch { return new Set(); } },
    derive: (chain, index) => {
      const n = child(chain, index);
      return { pubkey: n.publicKey, address: p2wpkh(n.publicKey, NET).address };
    },
    node: (chain, index) => ({ privateKey: child(chain, index).privateKey }),
    freshChange: () => ({ address: p2wpkh(child(1, 0).publicKey, NET).address }),
    isReserved(id) { return this.reservedSet().has(id); },
    saveCache() {},
    registerCoinLock() {},
    registerCacheExtension() {},
  };
  installGiftWallet(w);
  return w;
}

const coin = (value, i = 0) => ({ txid: '11'.repeat(32).slice(0, 64), vout: i, value, chain: 0, index: i, confirmed: true });

// 1) specific-amount gift from one big coin (the screenshot scenario: 99,847
//    coin, gift ~2,500) — the sum - gift - fundFee(2) path.
{
  const w = mockWallet([coin(99847, 0)]);
  const g = w.createGift(2500, 5);
  assert.ok(g.code && g.amount === 2500, 'specific-amount gift built');
  const parsed = parseLazyGift(g.code);
  assert.ok(parsed && parsed.tx.getOutput(0).amount === 2500n, 'v2 code parses, E holds the gift');
  assert.equal(previewGift(g.code).room, 2500, 'preview room = gift');
  const claim = giftClaimTxs(g.code, w.freshChange().address, 5, NET);
  assert.ok(claim.funding.hex && claim.sweep.hex && claim.amount > 0, 'claim funding+sweep build');
  store.clear();
  console.log('✓ specific-amount on-chain gift builds + claims (no BigInt crash)');
}

// 2) whole-balance gift (createGiftAll) — the sum - fundFee(1) path that crashed.
{
  const w = mockWallet([coin(30000, 0), coin(20000, 1)]);
  const g = w.createGiftAll(5);
  assert.ok(g.code && g.amount > 0 && g.amount < 50000, 'whole-balance gift built (amount = sum - fee)');
  assert.ok(parseLazyGift(g.code), 'whole-balance code parses');
  store.clear();
  console.log('✓ whole-balance on-chain gift builds (no BigInt crash)');
}

// 3) the split carve-out shape: _buildGiftPsbt on a coin sized gift + 1-output
//    fee, which lands in the dust-drop branch (sum - gift < fundFee(2)).
{
  const rate = 5;
  const w = mockWallet([]);
  const carveFee = Math.max(1, Math.ceil((11 + 68 + 31) * rate)); // _giftFundFee(1,1,rate)
  const gift = 2500;
  const carve = coin(gift + carveFee, 0);
  const g = w._buildGiftPsbt([carve], BigInt(gift), rate);
  assert.equal(g.amount, gift, 'split carve funds exactly the gift');
  assert.ok(parseLazyGift(g.code), 'split gift code parses');
  console.log('✓ split carve-out gift builds (dust-drop fee path, no BigInt crash)');
}

console.log('\n✅ on-chain gift builder passes');