mainnet-js 3.0.1 → 3.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (38) hide show
  1. package/dist/index.html +1 -1
  2. package/dist/{mainnet-3.0.1.js → mainnet-3.1.0.js} +5 -5
  3. package/dist/module/history/getHistory.js +10 -10
  4. package/dist/module/history/getHistory.js.map +1 -1
  5. package/dist/module/network/ElectrumNetworkProvider.d.ts +6 -3
  6. package/dist/module/network/ElectrumNetworkProvider.d.ts.map +1 -1
  7. package/dist/module/network/ElectrumNetworkProvider.js +18 -11
  8. package/dist/module/network/ElectrumNetworkProvider.js.map +1 -1
  9. package/dist/module/network/NetworkProvider.d.ts +14 -6
  10. package/dist/module/network/NetworkProvider.d.ts.map +1 -1
  11. package/dist/module/network/index.d.ts +1 -1
  12. package/dist/module/network/index.d.ts.map +1 -1
  13. package/dist/module/network/interface.d.ts +4 -3
  14. package/dist/module/network/interface.d.ts.map +1 -1
  15. package/dist/module/wallet/Base.d.ts.map +1 -1
  16. package/dist/module/wallet/Base.js +1 -2
  17. package/dist/module/wallet/Base.js.map +1 -1
  18. package/dist/module/wallet/HDWallet.d.ts +8 -0
  19. package/dist/module/wallet/HDWallet.d.ts.map +1 -1
  20. package/dist/module/wallet/HDWallet.js +28 -0
  21. package/dist/module/wallet/HDWallet.js.map +1 -1
  22. package/dist/module/wallet/Util.d.ts +5 -3
  23. package/dist/module/wallet/Util.d.ts.map +1 -1
  24. package/dist/module/wallet/Util.js +28 -22
  25. package/dist/module/wallet/Util.js.map +1 -1
  26. package/dist/tsconfig.tsbuildinfo +1 -1
  27. package/package.json +1 -1
  28. package/src/history/getHistory.ts +15 -15
  29. package/src/network/ElectrumNetworkProvider.ts +61 -26
  30. package/src/network/NetworkProvider.ts +44 -6
  31. package/src/network/index.ts +5 -1
  32. package/src/network/interface.ts +8 -3
  33. package/src/wallet/Base.ts +1 -4
  34. package/src/wallet/HDWallet.test.ts +239 -0
  35. package/src/wallet/HDWallet.ts +36 -0
  36. package/src/wallet/Util.test.ts +5 -6
  37. package/src/wallet/Util.ts +67 -46
  38. package/src/wallet/Wif.test.ts +3 -9
@@ -14,7 +14,12 @@ import {
14
14
  HeaderI,
15
15
  } from "../interface.js";
16
16
  import { Network } from "../interface.js";
17
- import { ElectrumRawTransaction, ElectrumUtxo } from "./interface.js";
17
+ import {
18
+ ElectrumRawTransaction,
19
+ ElectrumRawTransactionVinWithValues,
20
+ ElectrumRawTransactionWithInputValues,
21
+ ElectrumUtxo,
22
+ } from "./interface.js";
18
23
 
19
24
  import { CancelFn } from "../wallet/interface.js";
20
25
  import { getTransactionHash } from "../util/transaction.js";
@@ -251,11 +256,28 @@ export default class ElectrumNetworkProvider implements NetworkProvider {
251
256
  return this.currentHeight;
252
257
  }
253
258
 
259
+ async getRawTransaction(
260
+ txHash: string,
261
+ verbose: true,
262
+ loadInputValues: true
263
+ ): Promise<ElectrumRawTransactionWithInputValues>;
264
+ async getRawTransaction(
265
+ txHash: string,
266
+ verbose: true,
267
+ loadInputValues?: false
268
+ ): Promise<ElectrumRawTransaction>;
269
+ async getRawTransaction(
270
+ txHash: string,
271
+ verbose?: false,
272
+ loadInputValues?: false
273
+ ): Promise<string>;
254
274
  async getRawTransaction(
255
275
  txHash: string,
256
276
  verbose: boolean = false,
257
277
  loadInputValues: boolean = false
258
- ): Promise<string> {
278
+ ): Promise<
279
+ string | ElectrumRawTransaction | ElectrumRawTransactionWithInputValues
280
+ > {
259
281
  const key = `tx-${this.network}-${txHash}-${verbose}-${loadInputValues}`;
260
282
 
261
283
  if (this.cache) {
@@ -266,22 +288,27 @@ export default class ElectrumNetworkProvider implements NetworkProvider {
266
288
  }
267
289
 
268
290
  try {
269
- const transaction = (await this.performRequest(
291
+ const result = await this.performRequest(
270
292
  "blockchain.transaction.get",
271
293
  txHash,
272
294
  verbose
273
- )) as ElectrumRawTransaction;
295
+ );
296
+
297
+ if (!verbose) {
298
+ const hex = result as string;
299
+ if (this.cache) {
300
+ await this.cache.setItem(key, hex);
301
+ }
302
+ return hex;
303
+ }
304
+
305
+ const transaction = result as ElectrumRawTransaction;
274
306
 
275
307
  if (this.cache) {
276
- await this.cache.setItem(
277
- key,
278
- verbose
279
- ? JSON.stringify(transaction)
280
- : (transaction as unknown as string)
281
- );
308
+ await this.cache.setItem(key, JSON.stringify(transaction));
282
309
  }
283
310
 
284
- if (verbose && loadInputValues) {
311
+ if (loadInputValues) {
285
312
  // get unique transaction hashes
286
313
  const hashes = [...new Set(transaction.vin.map((val) => val.txid))];
287
314
  const transactions = await Promise.all(
@@ -290,17 +317,18 @@ export default class ElectrumNetworkProvider implements NetworkProvider {
290
317
  const transactionMap = new Map<string, ElectrumRawTransaction>();
291
318
  transactions.forEach((val) => transactionMap.set(val.hash, val));
292
319
 
293
- transaction.vin.forEach((input) => {
294
- const output = transactionMap
295
- .get(input.txid)!
296
- .vout.find((val) => val.n === input.vout)!;
297
- input.address = output.scriptPubKey.addresses[0];
298
- input.value = output.value;
299
- input.tokenData = output.tokenData;
300
- });
320
+ const enrichedVin: ElectrumRawTransactionVinWithValues[] =
321
+ transaction.vin.map((input) => {
322
+ const output = transactionMap
323
+ .get(input.txid)!
324
+ .vout.find((val) => val.n === input.vout)!;
325
+ return { ...input, ...output };
326
+ });
327
+
328
+ return { ...transaction, vin: enrichedVin };
301
329
  }
302
330
 
303
- return transaction as any;
331
+ return transaction;
304
332
  } catch (error: any) {
305
333
  if (
306
334
  (error.message as string).indexOf(
@@ -315,15 +343,22 @@ export default class ElectrumNetworkProvider implements NetworkProvider {
315
343
  }
316
344
 
317
345
  // gets the decoded transaction in human readable form
346
+ async getRawTransactionObject(
347
+ txHash: string,
348
+ loadInputValues: true
349
+ ): Promise<ElectrumRawTransactionWithInputValues>;
350
+ async getRawTransactionObject(
351
+ txHash: string,
352
+ loadInputValues?: false
353
+ ): Promise<ElectrumRawTransaction>;
318
354
  async getRawTransactionObject(
319
355
  txHash: string,
320
356
  loadInputValues: boolean = false
321
- ): Promise<ElectrumRawTransaction> {
322
- return (await this.getRawTransaction(
323
- txHash,
324
- true,
325
- loadInputValues
326
- )) as unknown as ElectrumRawTransaction;
357
+ ): Promise<ElectrumRawTransaction | ElectrumRawTransactionWithInputValues> {
358
+ if (loadInputValues) {
359
+ return this.getRawTransaction(txHash, true, true);
360
+ }
361
+ return this.getRawTransaction(txHash, true);
327
362
  }
328
363
 
329
364
  async sendRawTransaction(
@@ -1,4 +1,8 @@
1
1
  import { TxI, Utxo, Network, HexHeaderI, HeaderI } from "../interface.js";
2
+ import {
3
+ ElectrumRawTransaction,
4
+ ElectrumRawTransactionWithInputValues,
5
+ } from "./interface.js";
2
6
  import { CancelFn } from "../wallet/interface.js";
3
7
 
4
8
  export default interface NetworkProvider {
@@ -38,13 +42,35 @@ export default interface NetworkProvider {
38
42
  getRelayFee(): Promise<number>;
39
43
 
40
44
  /**
41
- * Retrieve the Hex transaction details for a given transaction ID.
45
+ * Retrieve transaction details for a given transaction ID.
42
46
  * @param txHash Hex of transaction hash.
43
- * @param verbose Whether a verbose coin-specific response is required.
47
+ * @param verbose When true, returns a decoded transaction object instead of hex.
48
+ * @param loadInputValues When true (requires verbose), each vin is enriched with value, address and tokenData from the parent output.
44
49
  * @throws {Error} If the transaction does not exist
45
- * @returns The full hex transaction for the provided transaction ID.
50
+ * @returns The hex string (non-verbose) or decoded transaction object (verbose).
46
51
  */
47
- getRawTransaction(txHash: string): Promise<string>;
52
+ getRawTransaction(
53
+ txHash: string,
54
+ verbose: true,
55
+ loadInputValues: true
56
+ ): Promise<ElectrumRawTransactionWithInputValues>;
57
+ getRawTransaction(
58
+ txHash: string,
59
+ verbose: true,
60
+ loadInputValues?: false
61
+ ): Promise<ElectrumRawTransaction>;
62
+ getRawTransaction(
63
+ txHash: string,
64
+ verbose?: false,
65
+ loadInputValues?: false
66
+ ): Promise<string>;
67
+ getRawTransaction(
68
+ txHash: string,
69
+ verbose?: boolean,
70
+ loadInputValues?: boolean
71
+ ): Promise<
72
+ string | ElectrumRawTransaction | ElectrumRawTransactionWithInputValues
73
+ >;
48
74
 
49
75
  /**
50
76
  * Batch retrieve raw transactions by their hashes.
@@ -63,10 +89,22 @@ export default interface NetworkProvider {
63
89
  /**
64
90
  * Retrieve a verbose coin-specific response transaction details for a given transaction ID.
65
91
  * @param txHash Hex of transaction hash.
92
+ * @param loadInputValues When true, each vin is enriched with value, address and tokenData from the parent output.
66
93
  * @throws {Error} If the transaction does not exist
67
- * @returns The full hex transaction for the provided transaction ID.
94
+ * @returns The decoded transaction object.
68
95
  */
69
- getRawTransactionObject(txHash: string): Promise<any>;
96
+ getRawTransactionObject(
97
+ txHash: string,
98
+ loadInputValues: true
99
+ ): Promise<ElectrumRawTransactionWithInputValues>;
100
+ getRawTransactionObject(
101
+ txHash: string,
102
+ loadInputValues?: false
103
+ ): Promise<ElectrumRawTransaction>;
104
+ getRawTransactionObject(
105
+ txHash: string,
106
+ loadInputValues?: boolean
107
+ ): Promise<ElectrumRawTransaction | ElectrumRawTransactionWithInputValues>;
70
108
 
71
109
  /**
72
110
  * Broadcast a raw hex transaction to the Bitcoin Cash network.
@@ -7,4 +7,8 @@ export {
7
7
  } from "./Connection.js";
8
8
  export { default as ElectrumNetworkProvider } from "./ElectrumNetworkProvider.js";
9
9
  export { default as NetworkProvider } from "./NetworkProvider.js";
10
- export { ElectrumRawTransaction } from "./interface.js";
10
+ export {
11
+ ElectrumRawTransaction,
12
+ ElectrumRawTransactionWithInputValues,
13
+ ElectrumRawTransactionVinWithValues,
14
+ } from "./interface.js";
@@ -54,9 +54,14 @@ export interface ElectrumRawTransactionVin {
54
54
  sequence: number;
55
55
  txid: string;
56
56
  vout: number;
57
- value?: number; // optional extention by mainnet.cash
58
- address?: string; // optional extension by mainnet.cash
59
- tokenData?: ElectrumTokenData; // optional extension by mainnet.cash
57
+ }
58
+
59
+ export type ElectrumRawTransactionVinWithValues = ElectrumRawTransactionVin &
60
+ ElectrumRawTransactionVout;
61
+
62
+ export interface ElectrumRawTransactionWithInputValues
63
+ extends Omit<ElectrumRawTransaction, "vin"> {
64
+ vin: ElectrumRawTransactionVinWithValues[];
60
65
  }
61
66
 
62
67
  export interface ElectrumRawTransactionVout {
@@ -571,10 +571,7 @@ export class BaseWallet implements WalletI {
571
571
  ): Promise<CancelFn> {
572
572
  return this.watchTransactions(
573
573
  async (transaction: ElectrumRawTransaction) => {
574
- if (
575
- transaction.vin.some((val) => val.tokenData) ||
576
- transaction.vout.some((val) => val.tokenData)
577
- ) {
574
+ if (transaction.vout.some((val) => val.tokenData)) {
578
575
  callback(transaction);
579
576
  }
580
577
  }
@@ -6,6 +6,9 @@ import { getNextUnusedIndex } from "../util/hd";
6
6
  import { NFTCapability } from "../interface";
7
7
  import { TokenMintRequest, TokenSendRequest } from "./model";
8
8
  import { stringify } from "../cache";
9
+ import { mine } from "../mine";
10
+ import { delay } from "../util/delay";
11
+ import { CancelFn } from "./interface";
9
12
 
10
13
  const expectedXpub =
11
14
  "xpub6CGqRCnS5qDfyxtzV3y3tj8CY7qf3z3GiB2qnCUTdNkhpNxbLtobrU5ZXBVPG3rzPcBUpJAoj3K1u1jyDwKuduL71gLPm27Tckc85apgQRr";
@@ -111,12 +114,16 @@ describe("HDWallet", () => {
111
114
  cashaddr: hdWallet.getDepositAddress(0),
112
115
  value: 100000n,
113
116
  });
117
+ while (hdWallet.depositIndex < 1)
118
+ await new Promise((r) => setTimeout(r, 50));
114
119
  expect(hdWallet.depositIndex).toBe(1);
115
120
 
116
121
  await fundingWallet.send({
117
122
  cashaddr: hdWallet.getDepositAddress(1),
118
123
  value: 100000n,
119
124
  });
125
+ while (hdWallet.depositIndex < 2)
126
+ await new Promise((r) => setTimeout(r, 50));
120
127
  expect(hdWallet.depositIndex).toBe(2);
121
128
 
122
129
  await fundingWallet.send({
@@ -155,6 +162,9 @@ describe("HDWallet", () => {
155
162
  { cashaddr: addr0, value: 10000n },
156
163
  { cashaddr: addr20, value: 10000n },
157
164
  ]);
165
+ // Wait for seedWallet to see both transactions via electrum
166
+ while (seedWallet.depositIndex < 21)
167
+ await new Promise((r) => setTimeout(r, 50));
158
168
 
159
169
  // Restore wallet from same seed, starting from index 0
160
170
  const restoredWallet = await RegTestHDWallet.fromSeed(
@@ -181,6 +191,8 @@ describe("HDWallet", () => {
181
191
  cashaddr: hdWallet.getDepositAddress(0),
182
192
  value: 100000n,
183
193
  });
194
+ while (hdWallet.depositIndex < 1)
195
+ await new Promise((r) => setTimeout(r, 50));
184
196
  expect(hdWallet.depositIndex).toBe(1);
185
197
  expect(hdWallet.changeIndex).toBe(0);
186
198
 
@@ -199,6 +211,8 @@ describe("HDWallet", () => {
199
211
  cashaddr: hdWallet.getDepositAddress(),
200
212
  value: 100000n,
201
213
  });
214
+ while (hdWallet.depositIndex < 2)
215
+ await new Promise((r) => setTimeout(r, 50));
202
216
  await hdWallet.send({
203
217
  cashaddr: bob.getDepositAddress(),
204
218
  value: 50000n,
@@ -247,6 +261,8 @@ describe("HDWallet", () => {
247
261
  cashaddr: depositAddress,
248
262
  value: 100000n,
249
263
  });
264
+ while (hdWallet.depositIndex < 1)
265
+ await new Promise((r) => setTimeout(r, 50));
250
266
 
251
267
  expect(await hdWallet.getBalance()).toBe(100000n);
252
268
 
@@ -258,6 +274,8 @@ describe("HDWallet", () => {
258
274
  cashaddr: depositAddress2,
259
275
  value: 100000n,
260
276
  });
277
+ while (hdWallet.depositIndex < 2)
278
+ await new Promise((r) => setTimeout(r, 50));
261
279
 
262
280
  expect(await hdWallet.getBalance()).toBe(200000n);
263
281
 
@@ -289,6 +307,8 @@ describe("HDWallet", () => {
289
307
  cashaddr: bob.getDepositAddress(),
290
308
  value: 150000n,
291
309
  });
310
+ while (hdWallet.changeIndex < 1)
311
+ await new Promise((r) => setTimeout(r, 50));
292
312
 
293
313
  expect(
294
314
  await (
@@ -326,6 +346,10 @@ describe("HDWallet", () => {
326
346
  const charlie = await RegTestWallet.newRandom();
327
347
  await hdWallet.sendMax(charlie.cashaddr);
328
348
 
349
+ // Wait for HD wallet to process the spent notification
350
+ while ((await hdWallet.getBalance()) > 0n)
351
+ await new Promise((r) => setTimeout(r, 50));
352
+
329
353
  expect(await charlie.getBalance()).toBe(49407n);
330
354
  expect(await hdWallet.getBalance()).toBe(0n);
331
355
  });
@@ -343,6 +367,8 @@ describe("HDWallet", () => {
343
367
  cashaddr: depositAddress,
344
368
  value: 100000n,
345
369
  });
370
+ while (hdWallet.depositIndex < 1)
371
+ await new Promise((r) => setTimeout(r, 50));
346
372
 
347
373
  expect(await hdWallet.getBalance()).toBe(100000n);
348
374
 
@@ -436,6 +462,8 @@ describe("HDWallet", () => {
436
462
  cashaddr: hdWallet.getDepositAddress(0),
437
463
  value: 100000n,
438
464
  });
465
+ while (hdWallet.depositIndex < 1)
466
+ await new Promise((r) => setTimeout(r, 50));
439
467
 
440
468
  expect(
441
469
  hdWallet.walletCache.get(hdWallet.getDepositAddress(0))?.status
@@ -486,6 +514,8 @@ describe("HDWallet", () => {
486
514
  cashaddr: hdWallet.getDepositAddress(0),
487
515
  value: 100000n,
488
516
  });
517
+ while (hdWallet.depositIndex < 1)
518
+ await new Promise((r) => setTimeout(r, 50));
489
519
 
490
520
  // rawHistory should now have one entry
491
521
  expect(
@@ -527,11 +557,15 @@ describe("HDWallet", () => {
527
557
  cashaddr: hdWallet.getDepositAddress(0),
528
558
  value: 100000n,
529
559
  });
560
+ while (hdWallet.depositIndex < 1)
561
+ await new Promise((r) => setTimeout(r, 50));
530
562
 
531
563
  await fundingWallet.send({
532
564
  cashaddr: hdWallet.getDepositAddress(1),
533
565
  value: 100000n,
534
566
  });
567
+ while (hdWallet.depositIndex < 2)
568
+ await new Promise((r) => setTimeout(r, 50));
535
569
 
536
570
  // Check depositRawHistory arrays are populated
537
571
  expect(hdWallet.depositRawHistory[0].length).toBe(1);
@@ -557,6 +591,8 @@ describe("HDWallet", () => {
557
591
  cashaddr: hdWallet.getDepositAddress(0),
558
592
  value: 100000n,
559
593
  });
594
+ while (hdWallet.depositIndex < 1)
595
+ await new Promise((r) => setTimeout(r, 50));
560
596
 
561
597
  const history = await hdWallet.getHistory({ unit: "sat" });
562
598
  expect(history.length).toBe(1);
@@ -578,6 +614,8 @@ describe("HDWallet", () => {
578
614
  cashaddr: hdWallet.getDepositAddress(0),
579
615
  value: 50000n,
580
616
  });
617
+ while (hdWallet.depositIndex < 1)
618
+ await new Promise((r) => setTimeout(r, 50));
581
619
 
582
620
  // Check rawHistory is populated
583
621
  const cacheEntry1 = hdWallet.walletCache.get(hdWallet.getDepositAddress(0));
@@ -590,6 +628,8 @@ describe("HDWallet", () => {
590
628
  cashaddr: hdWallet.getDepositAddress(0),
591
629
  value: 60000n,
592
630
  });
631
+ while (hdWallet.depositRawHistory[0].length < 2)
632
+ await new Promise((r) => setTimeout(r, 50));
593
633
 
594
634
  // Check history accumulated correctly
595
635
  const cacheEntry2 = hdWallet.walletCache.get(hdWallet.getDepositAddress(0));
@@ -680,6 +720,7 @@ describe("HDWallet", () => {
680
720
  cashaddr: alice.getDepositAddress(),
681
721
  value: 1000000n,
682
722
  });
723
+ while (alice.depositIndex < 1) await new Promise((r) => setTimeout(r, 50));
683
724
 
684
725
  const genesisResponse = await alice.tokenGenesis({
685
726
  cashaddr: alice.getDepositAddress(1),
@@ -764,6 +805,7 @@ describe("HDWallet", () => {
764
805
  cashaddr: alice.getDepositAddress(),
765
806
  value: 1000000n,
766
807
  });
808
+ while (alice.depositIndex < 1) await new Promise((r) => setTimeout(r, 50));
767
809
 
768
810
  const genesisResponse = await alice.tokenGenesis({
769
811
  amount: 100n,
@@ -839,4 +881,201 @@ describe("HDWallet", () => {
839
881
 
840
882
  Config.EnforceCashTokenReceiptAddresses = previousValue;
841
883
  });
884
+
885
+ it("watchTransactionHashes reports new transactions on HD wallet", async () => {
886
+ const fundingWallet = await RegTestWallet.fromId(
887
+ "wif:regtest:cNfsPtqN2bMRS7vH5qd8tR8GMvgXyL5BjnGAKgZ8DYEiCrCCQcP6"
888
+ );
889
+ const hdWallet = await RegTestHDWallet.newRandom();
890
+
891
+ // Fund the HD wallet at deposit address 0
892
+ await fundingWallet.send({
893
+ cashaddr: hdWallet.getDepositAddress(0),
894
+ value: 100000n,
895
+ });
896
+ await mine({ cashaddr: hdWallet.getDepositAddress(0), blocks: 1 });
897
+ await delay(2000);
898
+
899
+ // Set up watchTransactionHashes, collect reported tx hashes
900
+ const reportedHashes: string[] = [];
901
+ let cancelWatch: CancelFn;
902
+ cancelWatch = await hdWallet.watchTransactionHashes((txHash) => {
903
+ reportedHashes.push(txHash);
904
+ });
905
+
906
+ // Wait for initial callback to fire with existing tx
907
+ await delay(2000);
908
+
909
+ // Send a new transaction to the HD wallet
910
+ const sendResponse = await fundingWallet.send({
911
+ cashaddr: hdWallet.getDepositAddress(),
912
+ value: 50000n,
913
+ });
914
+ await mine({ cashaddr: hdWallet.getDepositAddress(0), blocks: 1 });
915
+ await delay(2000);
916
+
917
+ await cancelWatch();
918
+
919
+ // The new transaction's txId should appear in collected hashes
920
+ expect(reportedHashes).toContain(sendResponse.txId);
921
+ });
922
+
923
+ it("watchTransactionHashes does not re-report old transactions", async () => {
924
+ const fundingWallet = await RegTestWallet.fromId(
925
+ "wif:regtest:cNfsPtqN2bMRS7vH5qd8tR8GMvgXyL5BjnGAKgZ8DYEiCrCCQcP6"
926
+ );
927
+ const hdWallet = await RegTestHDWallet.newRandom();
928
+
929
+ // Start watching before any transactions so the first funding triggers a callback
930
+ const reportedHashes: string[] = [];
931
+ let cancelWatch: CancelFn;
932
+ cancelWatch = await hdWallet.watchTransactionHashes((txHash) => {
933
+ reportedHashes.push(txHash);
934
+ });
935
+
936
+ // Fund the HD wallet at deposit address 0
937
+ const fundResponse = await fundingWallet.send({
938
+ cashaddr: hdWallet.getDepositAddress(0),
939
+ value: 100000n,
940
+ });
941
+ await mine({ cashaddr: hdWallet.getDepositAddress(0), blocks: 1 });
942
+ await delay(2000);
943
+
944
+ // Record how many hashes reported so far (the initial set)
945
+ const initialCount = reportedHashes.length;
946
+ expect(reportedHashes).toContain(fundResponse.txId);
947
+ const initialHashes = [...reportedHashes];
948
+
949
+ // Send a second transaction to the HD wallet
950
+ const sendResponse2 = await fundingWallet.send({
951
+ cashaddr: hdWallet.getDepositAddress(),
952
+ value: 50000n,
953
+ });
954
+ await mine({ cashaddr: hdWallet.getDepositAddress(0), blocks: 1 });
955
+ await delay(2000);
956
+
957
+ await cancelWatch();
958
+
959
+ // Hashes reported after the initial set
960
+ const laterHashes = reportedHashes.slice(initialCount);
961
+
962
+ // The second tx hash was reported
963
+ expect(laterHashes).toContain(sendResponse2.txId);
964
+
965
+ // None of the initial tx hashes were reported again after the first callback
966
+ for (const hash of initialHashes) {
967
+ expect(laterHashes).not.toContain(hash);
968
+ }
969
+ });
970
+
971
+ it("watchTransactionHashes handles transactions across multiple deposit addresses", async () => {
972
+ const fundingWallet = await RegTestWallet.fromId(
973
+ "wif:regtest:cNfsPtqN2bMRS7vH5qd8tR8GMvgXyL5BjnGAKgZ8DYEiCrCCQcP6"
974
+ );
975
+ const hdWallet = await RegTestHDWallet.newRandom();
976
+
977
+ // Fund deposit address 0
978
+ const fund0Response = await fundingWallet.send({
979
+ cashaddr: hdWallet.getDepositAddress(0),
980
+ value: 100000n,
981
+ });
982
+ await mine({ cashaddr: hdWallet.getDepositAddress(0), blocks: 1 });
983
+ await delay(1000);
984
+
985
+ // Fund deposit address 1
986
+ const fund1Response = await fundingWallet.send({
987
+ cashaddr: hdWallet.getDepositAddress(1),
988
+ value: 100000n,
989
+ });
990
+ await mine({ cashaddr: hdWallet.getDepositAddress(0), blocks: 1 });
991
+ await delay(1000);
992
+
993
+ // depositIndex should be 2
994
+ expect(hdWallet.depositIndex).toBe(2);
995
+
996
+ // Set up watchTransactionHashes, collect all reported hashes
997
+ const reportedHashes: string[] = [];
998
+ let cancelWatch: CancelFn;
999
+ cancelWatch = await hdWallet.watchTransactionHashes((txHash) => {
1000
+ reportedHashes.push(txHash);
1001
+ });
1002
+
1003
+ // Wait for initial callback to fire
1004
+ await delay(2000);
1005
+
1006
+ // Send a new transaction to the wallet's next deposit address
1007
+ const fund2Response = await fundingWallet.send({
1008
+ cashaddr: hdWallet.getDepositAddress(),
1009
+ value: 50000n,
1010
+ });
1011
+ await mine({ cashaddr: hdWallet.getDepositAddress(0), blocks: 1 });
1012
+ await delay(2000);
1013
+
1014
+ await cancelWatch();
1015
+
1016
+ // All 3 tx hashes appear in the collected output
1017
+ expect(reportedHashes).toContain(fund0Response.txId);
1018
+ expect(reportedHashes).toContain(fund1Response.txId);
1019
+ expect(reportedHashes).toContain(fund2Response.txId);
1020
+
1021
+ // Each tx hash appears exactly once
1022
+ const uniqueHashes = new Set(reportedHashes);
1023
+ expect(uniqueHashes.size).toBe(reportedHashes.length);
1024
+ });
1025
+
1026
+ it("gap is maintained when addresses near the edge are used", async () => {
1027
+ const fundingWallet = await RegTestWallet.fromId(
1028
+ "wif:regtest:cNfsPtqN2bMRS7vH5qd8tR8GMvgXyL5BjnGAKgZ8DYEiCrCCQcP6"
1029
+ );
1030
+ const hdWallet = await RegTestHDWallet.newRandom();
1031
+ await hdWallet.watchPromise;
1032
+
1033
+ // Initially: depositIndex=0, watched addresses 0..(GAP_SIZE-1)
1034
+ expect(hdWallet.depositIndex).toBe(0);
1035
+ const initialWatched = (hdWallet as any).depositStatuses.length;
1036
+ expect(initialWatched).toBe(GAP_SIZE);
1037
+
1038
+ // Fund an address near the edge of the gap
1039
+ const edgeIndex = GAP_SIZE - 2;
1040
+ await fundingWallet.send({
1041
+ cashaddr: hdWallet.getDepositAddress(edgeIndex),
1042
+ value: 10000n,
1043
+ });
1044
+
1045
+ // Wait for the subscription callback to fire and gap extension to complete
1046
+ while (hdWallet.depositIndex < edgeIndex + 1)
1047
+ await new Promise((r) => setTimeout(r, 50));
1048
+ await delay(1000);
1049
+
1050
+ // depositIndex should have advanced
1051
+ expect(hdWallet.depositIndex).toBe(edgeIndex + 1);
1052
+
1053
+ // The watched range should have extended to maintain the gap
1054
+ const newWatched = (hdWallet as any).depositStatuses.length;
1055
+ expect(newWatched).toBeGreaterThanOrEqual(hdWallet.depositIndex + GAP_SIZE);
1056
+
1057
+ // Verify the new addresses are actually subscribed (watchCancels populated)
1058
+ const watchCancels = (hdWallet as any).depositWatchCancels;
1059
+ for (let i = initialWatched; i < newWatched; i++) {
1060
+ expect(watchCancels[i]).toBeDefined();
1061
+ }
1062
+
1063
+ // Fund an address in the newly extended range to prove it's being watched
1064
+ const newEdge = newWatched - 2;
1065
+ await fundingWallet.send({
1066
+ cashaddr: hdWallet.getDepositAddress(newEdge),
1067
+ value: 10000n,
1068
+ });
1069
+ while (hdWallet.depositIndex < newEdge + 1)
1070
+ await new Promise((r) => setTimeout(r, 50));
1071
+ await delay(1000);
1072
+
1073
+ expect(hdWallet.depositIndex).toBe(newEdge + 1);
1074
+
1075
+ // Gap should still be maintained after the second extension
1076
+ const finalWatched = (hdWallet as any).depositStatuses.length;
1077
+ expect(finalWatched).toBeGreaterThanOrEqual(
1078
+ hdWallet.depositIndex + GAP_SIZE
1079
+ );
1080
+ });
842
1081
  });
@@ -414,6 +414,12 @@ export class HDWallet extends BaseWallet {
414
414
  if (newIndex > getCurrentIndex()) {
415
415
  setCurrentIndex(newIndex);
416
416
  }
417
+
418
+ // Maintain the gap: extend watched range if it shrank
419
+ const gap = statuses.length - getCurrentIndex();
420
+ if (gap < gapSize) {
421
+ await this.watchAddressType(isChange, gapSize);
422
+ }
417
423
  }
418
424
 
419
425
  // Notify wallet watchers of the status change
@@ -493,6 +499,36 @@ export class HDWallet extends BaseWallet {
493
499
  };
494
500
  }
495
501
 
502
+ /**
503
+ * Watch wallet for new transactions (HD wallet override)
504
+ *
505
+ * Uses unfiltered history so that seenTxHashes always covers all known
506
+ * transactions, including those from newly discovered addresses when
507
+ * depositIndex/changeIndex extends and widens getRawHistory's scope.
508
+ */
509
+ public override async watchTransactionHashes(
510
+ callback: (txHash: string) => void
511
+ ): Promise<CancelFn> {
512
+ const seenTxHashes = new Set<string>();
513
+
514
+ return this.watchStatus(async () => {
515
+ const history = await this.getRawHistory();
516
+
517
+ const newTxHashes: string[] = [];
518
+
519
+ for (const tx of history) {
520
+ if (!seenTxHashes.has(tx.tx_hash)) {
521
+ seenTxHashes.add(tx.tx_hash);
522
+ newTxHashes.push(tx.tx_hash);
523
+ }
524
+ }
525
+
526
+ if (newTxHashes.length > 0) {
527
+ newTxHashes.forEach((txHash) => callback(txHash));
528
+ }
529
+ });
530
+ }
531
+
496
532
  /**
497
533
  * utxos Get unspent outputs for the wallet
498
534
  *