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.
- package/dist/index.html +1 -1
- package/dist/{mainnet-3.0.1.js → mainnet-3.1.0.js} +5 -5
- package/dist/module/history/getHistory.js +10 -10
- package/dist/module/history/getHistory.js.map +1 -1
- package/dist/module/network/ElectrumNetworkProvider.d.ts +6 -3
- package/dist/module/network/ElectrumNetworkProvider.d.ts.map +1 -1
- package/dist/module/network/ElectrumNetworkProvider.js +18 -11
- package/dist/module/network/ElectrumNetworkProvider.js.map +1 -1
- package/dist/module/network/NetworkProvider.d.ts +14 -6
- package/dist/module/network/NetworkProvider.d.ts.map +1 -1
- package/dist/module/network/index.d.ts +1 -1
- package/dist/module/network/index.d.ts.map +1 -1
- package/dist/module/network/interface.d.ts +4 -3
- package/dist/module/network/interface.d.ts.map +1 -1
- package/dist/module/wallet/Base.d.ts.map +1 -1
- package/dist/module/wallet/Base.js +1 -2
- package/dist/module/wallet/Base.js.map +1 -1
- package/dist/module/wallet/HDWallet.d.ts +8 -0
- package/dist/module/wallet/HDWallet.d.ts.map +1 -1
- package/dist/module/wallet/HDWallet.js +28 -0
- package/dist/module/wallet/HDWallet.js.map +1 -1
- package/dist/module/wallet/Util.d.ts +5 -3
- package/dist/module/wallet/Util.d.ts.map +1 -1
- package/dist/module/wallet/Util.js +28 -22
- package/dist/module/wallet/Util.js.map +1 -1
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/package.json +1 -1
- package/src/history/getHistory.ts +15 -15
- package/src/network/ElectrumNetworkProvider.ts +61 -26
- package/src/network/NetworkProvider.ts +44 -6
- package/src/network/index.ts +5 -1
- package/src/network/interface.ts +8 -3
- package/src/wallet/Base.ts +1 -4
- package/src/wallet/HDWallet.test.ts +239 -0
- package/src/wallet/HDWallet.ts +36 -0
- package/src/wallet/Util.test.ts +5 -6
- package/src/wallet/Util.ts +67 -46
- 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 {
|
|
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<
|
|
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
|
|
291
|
+
const result = await this.performRequest(
|
|
270
292
|
"blockchain.transaction.get",
|
|
271
293
|
txHash,
|
|
272
294
|
verbose
|
|
273
|
-
)
|
|
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 (
|
|
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
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
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
|
|
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
|
-
|
|
323
|
-
txHash,
|
|
324
|
-
|
|
325
|
-
|
|
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
|
|
45
|
+
* Retrieve transaction details for a given transaction ID.
|
|
42
46
|
* @param txHash Hex of transaction hash.
|
|
43
|
-
* @param verbose
|
|
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
|
|
50
|
+
* @returns The hex string (non-verbose) or decoded transaction object (verbose).
|
|
46
51
|
*/
|
|
47
|
-
getRawTransaction(
|
|
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
|
|
94
|
+
* @returns The decoded transaction object.
|
|
68
95
|
*/
|
|
69
|
-
getRawTransactionObject(
|
|
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.
|
package/src/network/index.ts
CHANGED
|
@@ -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 {
|
|
10
|
+
export {
|
|
11
|
+
ElectrumRawTransaction,
|
|
12
|
+
ElectrumRawTransactionWithInputValues,
|
|
13
|
+
ElectrumRawTransactionVinWithValues,
|
|
14
|
+
} from "./interface.js";
|
package/src/network/interface.ts
CHANGED
|
@@ -54,9 +54,14 @@ export interface ElectrumRawTransactionVin {
|
|
|
54
54
|
sequence: number;
|
|
55
55
|
txid: string;
|
|
56
56
|
vout: number;
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
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 {
|
package/src/wallet/Base.ts
CHANGED
|
@@ -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
|
});
|
package/src/wallet/HDWallet.ts
CHANGED
|
@@ -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
|
*
|