mainnet-js 3.0.1 → 3.1.1

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.1.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 +7 -3
  6. package/dist/module/network/ElectrumNetworkProvider.d.ts.map +1 -1
  7. package/dist/module/network/ElectrumNetworkProvider.js +51 -25
  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 +105 -39
  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 +56 -6
  37. package/src/wallet/Util.ts +67 -46
  38. package/src/wallet/Wif.test.ts +3 -9
@@ -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
  *
@@ -1,6 +1,8 @@
1
1
  import { initProviders, disconnectProviders } from "../network";
2
- import { ElectrumRawTransaction } from "../network/interface";
3
2
  import { RegTestWallet, Wallet } from "./Wif";
3
+ import { mine } from "../mine";
4
+ import { Config } from "../config";
5
+ import { delay } from "../util/delay";
4
6
 
5
7
  beforeAll(async () => {
6
8
  await initProviders();
@@ -41,9 +43,9 @@ describe("Utility tests", () => {
41
43
  test("Should get raw transaction", async () => {
42
44
  let wallet = await RegTestWallet.fromId(process.env.ALICE_ID!);
43
45
  const utxo = (await wallet.getUtxos())[0];
44
- const transaction = (await wallet.provider!.getRawTransactionObject(
46
+ const transaction = await wallet.provider!.getRawTransactionObject(
45
47
  utxo.txid
46
- )) as ElectrumRawTransaction;
48
+ );
47
49
  expect((await wallet.util.decodeTransaction(transaction.hash)).hash).toBe(
48
50
  utxo.txid
49
51
  );
@@ -67,18 +69,66 @@ describe("Utility tests", () => {
67
69
  "0e3e2357e806b6cdb1f70b54c3a3a17b6714ee1f0e68bebb44a74b1efd512098"
68
70
  );
69
71
 
70
- expect(decoded.vin[0].address).toBeUndefined();
71
- expect(decoded.vin[0].value).toBeUndefined();
72
+ // non-enriched vin entries don't carry vout fields (enforced by types)
73
+ expect((decoded.vin[0] as any).address).toBeUndefined();
72
74
  });
73
75
 
74
76
  test("Should decode a transaction and fetch input values and addresses", async () => {
75
77
  const txHash =
76
78
  "dc8f059900807c36941313f10b43ec049e23dfede4e09f8fbccc3871ed359fbe";
77
79
  const decoded = await Wallet.util.decodeTransaction(txHash, true);
78
- expect(decoded.vin[0].address).toBeDefined();
80
+ expect(decoded.vin[0].scriptPubKey.addresses[0]).toBeDefined();
79
81
  expect(decoded.vin[0].value).toBeDefined();
80
82
 
81
83
  // uncomment next line
82
84
  // expect(await Wallet.util.decodeTransaction(txHash)).toBe(await new Wallet().provider!.getRawTransactionObject(txHash));
83
85
  });
84
86
  });
87
+
88
+ describe("Dynamic confirmations via fetchHeight", () => {
89
+ test("confirmations defaults to 0 for decoded mempool transactions", async () => {
90
+ const wallet = await RegTestWallet.newRandom();
91
+ const decoded = await wallet.util.decodeTransaction(
92
+ "01000000015bb9142c960a838329694d3fe9ba08c2a6421c5158d8f7044cb7c48006c1b484000000006a4730440220229ea5359a63c2b83a713fcc20d8c41b20d48fe639a639d2a8246a137f29d0fc02201de12de9c056912a4e581a62d12fb5f43ee6c08ed0238c32a1ee769213ca8b8b412103bcf9a004f1f7a9a8d8acce7b51c983233d107329ff7c4fb53e44c855dbe1f6a4feffffff02c6b68200000000001976a9141041fb024bd7a1338ef1959026bbba860064fe5f88ac50a8cf00000000001976a91445dac110239a7a3814535c15858b939211f8529888ac61ee0700"
93
+ );
94
+ expect(decoded.confirmations).toBe(0);
95
+ });
96
+
97
+ test("confirmations is present on verbose server response", async () => {
98
+ const alice = await RegTestWallet.fromId(process.env.ALICE_ID!);
99
+ const utxo = (await alice.getUtxos())[0];
100
+ const transaction = await alice.provider.getRawTransactionObject(utxo.txid);
101
+ expect(transaction.confirmations).toBeGreaterThan(0);
102
+ });
103
+
104
+ test("fetchHeight is not exposed in returned transaction", async () => {
105
+ const alice = await RegTestWallet.fromId(process.env.ALICE_ID!);
106
+ const utxo = (await alice.getUtxos())[0];
107
+ const transaction = await alice.provider.getRawTransactionObject(utxo.txid);
108
+ expect((transaction as any).fetchHeight).toBeUndefined();
109
+ });
110
+
111
+ test("cached transaction has up-to-date confirmations", async () => {
112
+ const memoryCacheValue = Config.UseMemoryCache;
113
+ Config.UseMemoryCache = true;
114
+ try {
115
+ const alice = await RegTestWallet.fromId(process.env.ALICE_ID!);
116
+ const utxo = (await alice.getUtxos())[0];
117
+
118
+ // first fetch primes the cache
119
+ const tx1 = await alice.provider.getRawTransactionObject(utxo.txid);
120
+ const confirmations1 = tx1.confirmations;
121
+
122
+ // mine a block and wait for header subscription to propagate
123
+ await mine({ cashaddr: alice.cashaddr!, blocks: 1 });
124
+ await delay(1000);
125
+
126
+ // second fetch should hit cache but with updated confirmations
127
+ const tx2 = await alice.provider.getRawTransactionObject(utxo.txid);
128
+ expect(tx2.confirmations).toBe(confirmations1 + 1);
129
+ expect((tx2 as any).fetchHeight).toBeUndefined();
130
+ } finally {
131
+ Config.UseMemoryCache = memoryCacheValue;
132
+ }
133
+ });
134
+ });
@@ -15,9 +15,9 @@ import { getNetworkProvider } from "../network/default.js";
15
15
  import {
16
16
  ElectrumRawTransaction,
17
17
  ElectrumRawTransactionVin,
18
- ElectrumRawTransactionVinScriptSig,
18
+ ElectrumRawTransactionVinWithValues,
19
19
  ElectrumRawTransactionVout,
20
- ElectrumRawTransactionVoutScriptPubKey,
20
+ ElectrumRawTransactionWithInputValues,
21
21
  } from "../network/interface.js";
22
22
  import NetworkProvider from "../network/NetworkProvider.js";
23
23
  import { getTransactionHash } from "../util/transaction.js";
@@ -49,10 +49,18 @@ export class Util {
49
49
  return getTransactionHash(rawTransactionHex);
50
50
  }
51
51
 
52
+ public async decodeTransaction(
53
+ transactionHashOrHex: string,
54
+ loadInputValues: true
55
+ ): Promise<ElectrumRawTransactionWithInputValues>;
56
+ public async decodeTransaction(
57
+ transactionHashOrHex: string,
58
+ loadInputValues?: false
59
+ ): Promise<ElectrumRawTransaction>;
52
60
  public async decodeTransaction(
53
61
  transactionHashOrHex: string,
54
62
  loadInputValues: boolean = false
55
- ): Promise<ElectrumRawTransaction> {
63
+ ): Promise<ElectrumRawTransaction | ElectrumRawTransactionWithInputValues> {
56
64
  let transactionHex: string;
57
65
  let transactionBin: Uint8Array;
58
66
  let txHash: string;
@@ -89,14 +97,15 @@ export class Util {
89
97
  const transactionMap = new Map<string, ElectrumRawTransaction>();
90
98
  transactions.forEach((val) => transactionMap.set(val.hash, val));
91
99
 
92
- transaction.vin.forEach((input) => {
93
- const output = transactionMap
94
- .get(input.txid)!
95
- .vout.find((val) => val.n === input.vout)!;
96
- input.address = output.scriptPubKey.addresses[0];
97
- input.value = output.value;
98
- input.tokenData = output.tokenData;
99
- });
100
+ const enrichedVin: ElectrumRawTransactionVinWithValues[] =
101
+ transaction.vin.map((input) => {
102
+ const output = transactionMap
103
+ .get(input.txid)!
104
+ .vout.find((val) => val.n === input.vout)!;
105
+ return { ...input, ...output };
106
+ });
107
+
108
+ return { ...transaction, vin: enrichedVin };
100
109
  }
101
110
 
102
111
  return transaction;
@@ -107,22 +116,30 @@ export class Util {
107
116
  txHash: string,
108
117
  txHex: string
109
118
  ): ElectrumRawTransaction {
110
- let result: ElectrumRawTransaction = {} as any;
111
-
112
- result.vin = transaction.inputs.map((input): ElectrumRawTransactionVin => {
113
- return {
114
- scriptSig: {
115
- hex: binToHex(input.unlockingBytecode),
116
- } as ElectrumRawTransactionVinScriptSig,
117
- sequence: input.sequenceNumber,
118
- txid: binToHex(input.outpointTransactionHash),
119
- vout: input.outpointIndex,
120
- };
121
- });
122
-
123
- result.vout = transaction.outputs.map(
124
- (output, index): ElectrumRawTransactionVout => {
125
- return {
119
+ return {
120
+ blockhash: "",
121
+ blocktime: 0,
122
+ confirmations: 0,
123
+ time: 0,
124
+ hash: txHash,
125
+ hex: txHex,
126
+ txid: txHash,
127
+ locktime: transaction.locktime,
128
+ version: transaction.version,
129
+ size: txHex.length / 2,
130
+ vin: transaction.inputs.map(
131
+ (input): ElectrumRawTransactionVin => ({
132
+ scriptSig: {
133
+ asm: "",
134
+ hex: binToHex(input.unlockingBytecode),
135
+ },
136
+ sequence: input.sequenceNumber,
137
+ txid: binToHex(input.outpointTransactionHash),
138
+ vout: input.outpointIndex,
139
+ })
140
+ ),
141
+ vout: transaction.outputs.map(
142
+ (output, index): ElectrumRawTransactionVout => ({
126
143
  n: index,
127
144
  scriptPubKey: {
128
145
  addresses: [
@@ -132,7 +149,7 @@ export class Util {
132
149
  output.lockingBytecode
133
150
  ).payload,
134
151
  prefix: prefixFromNetworkMap[this.network],
135
- })
152
+ }).address
136
153
  : assertSuccess(
137
154
  lockingBytecodeToCashAddress({
138
155
  bytecode: output.lockingBytecode,
@@ -140,31 +157,35 @@ export class Util {
140
157
  })
141
158
  ).address,
142
159
  ],
160
+ asm: "",
143
161
  hex: binToHex(output.lockingBytecode),
144
- } as ElectrumRawTransactionVoutScriptPubKey,
162
+ reqSigs: 1,
163
+ type: "",
164
+ },
145
165
  value: Number(output.valueSatoshis) / Number(bchParam.subUnits),
146
- };
147
- }
148
- );
149
-
150
- result.locktime = transaction.locktime;
151
- result.version = transaction.version;
152
- result.hash = txHash;
153
- result.hex = txHex;
154
- result.txid = txHash;
155
- result.size = txHex.length / 2;
156
-
157
- return result;
166
+ })
167
+ ),
168
+ };
158
169
  }
159
170
 
171
+ public static async decodeTransaction(
172
+ transactionHashOrHex: string,
173
+ loadInputValues: true,
174
+ network?: NetworkType
175
+ ): Promise<ElectrumRawTransactionWithInputValues>;
176
+ public static async decodeTransaction(
177
+ transactionHashOrHex: string,
178
+ loadInputValues?: false,
179
+ network?: NetworkType
180
+ ): Promise<ElectrumRawTransaction>;
160
181
  public static async decodeTransaction(
161
182
  transactionHashOrHex: string,
162
183
  loadInputValues: boolean = false,
163
184
  network?: NetworkType
164
- ): Promise<ElectrumRawTransaction> {
165
- return new this(network).decodeTransaction(
166
- transactionHashOrHex,
167
- loadInputValues
168
- );
185
+ ): Promise<ElectrumRawTransaction | ElectrumRawTransactionWithInputValues> {
186
+ if (loadInputValues) {
187
+ return new this(network).decodeTransaction(transactionHashOrHex, true);
188
+ }
189
+ return new this(network).decodeTransaction(transactionHashOrHex);
169
190
  }
170
191
  }