aboutsummaryrefslogtreecommitdiffstats
path: root/tools
diff options
context:
space:
mode:
Diffstat (limited to 'tools')
-rw-r--r--tools/gift-build-test.js89
1 files changed, 89 insertions, 0 deletions
diff --git a/tools/gift-build-test.js b/tools/gift-build-test.js
new file mode 100644
index 0000000..baf0696
--- /dev/null
+++ b/tools/gift-build-test.js
@@ -0,0 +1,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');