mainnet-js 3.1.7 → 4.0.0-next.2

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 (83) hide show
  1. package/dist/index.html +1 -1
  2. package/dist/{mainnet-3.1.7.js → mainnet-4.0.0-next.2.js} +66 -166
  3. package/dist/module/cache/walletCache.d.ts +16 -6
  4. package/dist/module/cache/walletCache.d.ts.map +1 -1
  5. package/dist/module/cache/walletCache.js +92 -34
  6. package/dist/module/cache/walletCache.js.map +1 -1
  7. package/dist/module/mine/mine.d.ts.map +1 -1
  8. package/dist/module/mine/mine.js +14 -19
  9. package/dist/module/mine/mine.js.map +1 -1
  10. package/dist/module/network/Connection.d.ts +1 -12
  11. package/dist/module/network/Connection.d.ts.map +1 -1
  12. package/dist/module/network/Connection.js +12 -33
  13. package/dist/module/network/Connection.js.map +1 -1
  14. package/dist/module/network/ElectrumNetworkProvider.d.ts +4 -7
  15. package/dist/module/network/ElectrumNetworkProvider.d.ts.map +1 -1
  16. package/dist/module/network/ElectrumNetworkProvider.js +43 -70
  17. package/dist/module/network/ElectrumNetworkProvider.js.map +1 -1
  18. package/dist/module/network/configuration.d.ts +2 -4
  19. package/dist/module/network/configuration.d.ts.map +1 -1
  20. package/dist/module/network/configuration.js +25 -50
  21. package/dist/module/network/configuration.js.map +1 -1
  22. package/dist/module/network/constant.d.ts +7 -7
  23. package/dist/module/network/constant.d.ts.map +1 -1
  24. package/dist/module/network/constant.js +20 -24
  25. package/dist/module/network/constant.js.map +1 -1
  26. package/dist/module/network/default.d.ts +3 -2
  27. package/dist/module/network/default.d.ts.map +1 -1
  28. package/dist/module/network/default.js +19 -51
  29. package/dist/module/network/default.js.map +1 -1
  30. package/dist/module/network/index.d.ts +2 -2
  31. package/dist/module/network/index.d.ts.map +1 -1
  32. package/dist/module/network/index.js +2 -2
  33. package/dist/module/network/index.js.map +1 -1
  34. package/dist/module/network/interface.d.ts +0 -6
  35. package/dist/module/network/interface.d.ts.map +1 -1
  36. package/dist/module/wallet/Base.d.ts.map +1 -1
  37. package/dist/module/wallet/Base.js +33 -12
  38. package/dist/module/wallet/Base.js.map +1 -1
  39. package/dist/module/wallet/HDWallet.d.ts.map +1 -1
  40. package/dist/module/wallet/HDWallet.js +16 -35
  41. package/dist/module/wallet/HDWallet.js.map +1 -1
  42. package/dist/module/wallet/Watch.d.ts +22 -1
  43. package/dist/module/wallet/Watch.d.ts.map +1 -1
  44. package/dist/module/wallet/Watch.js +137 -6
  45. package/dist/module/wallet/Watch.js.map +1 -1
  46. package/dist/module/wallet/Wif.d.ts +1 -1
  47. package/dist/module/wallet/Wif.d.ts.map +1 -1
  48. package/dist/module/wallet/Wif.js +0 -4
  49. package/dist/module/wallet/Wif.js.map +1 -1
  50. package/dist/tsconfig.tsbuildinfo +1 -1
  51. package/package.json +5 -3
  52. package/src/cache/walletCache.ts +122 -57
  53. package/src/mine/mine.ts +18 -22
  54. package/src/network/Connection.test.ts +8 -7
  55. package/src/network/Connection.ts +16 -42
  56. package/src/network/ElectrumNetworkProvider.ts +54 -88
  57. package/src/network/Rpc.test.ts +6 -5
  58. package/src/network/configuration.test.ts +23 -25
  59. package/src/network/configuration.ts +29 -55
  60. package/src/network/constant.ts +20 -25
  61. package/src/network/default.ts +25 -87
  62. package/src/network/electrum.test.ts +47 -11
  63. package/src/network/index.ts +7 -2
  64. package/src/network/interface.ts +0 -7
  65. package/src/network/subscription.test.ts +268 -0
  66. package/src/wallet/Base.ts +39 -17
  67. package/src/wallet/Cashtokens.test.headless.js +6 -0
  68. package/src/wallet/Cashtokens.test.ts +13 -0
  69. package/src/wallet/HDWallet.test.ts +4 -4
  70. package/src/wallet/HDWallet.ts +19 -35
  71. package/src/wallet/WalletCache.test.ts +6 -2
  72. package/src/wallet/Watch.ts +199 -7
  73. package/src/wallet/Wif.test.ts +6 -0
  74. package/src/wallet/Wif.ts +2 -9
  75. package/tsconfig.browser.json +10 -1
  76. package/tsconfig.json +12 -1
  77. package/webpack.config.cjs +1 -0
  78. package/dist/module/network/util.d.ts +0 -3
  79. package/dist/module/network/util.d.ts.map +0 -1
  80. package/dist/module/network/util.js +0 -27
  81. package/dist/module/network/util.js.map +0 -1
  82. package/src/network/util.test.ts +0 -24
  83. package/src/network/util.ts +0 -30
@@ -58,7 +58,7 @@ export interface WalletCache {
58
58
  // Full interface for wallet cache management
59
59
  export interface WalletCacheI extends WalletCache {
60
60
  init(): Promise<void>;
61
- persist(): Promise<void>;
61
+ persist(immediate?: boolean): Promise<void>;
62
62
  get(address: string): WalletCacheEntry | undefined;
63
63
  getByIndex(addressIndex: number, change: boolean): WalletCacheEntry;
64
64
  setStatusAndUtxos(
@@ -70,51 +70,114 @@ export interface WalletCacheI extends WalletCache {
70
70
  ): void;
71
71
  }
72
72
 
73
- export class PersistentWalletCache implements WalletCacheI {
73
+ function getStorage(): CacheProvider | undefined {
74
+ if (Config.UseMemoryCache) return new MemoryCache();
75
+ if (Config.UseLocalStorageCache) return new WebStorageCache();
76
+ if (Config.UseIndexedDBCache) return new IndexedDbCache("WalletCache");
77
+ return undefined;
78
+ }
79
+
80
+ /// Cache for single-address wallets (WatchWallet, Wif)
81
+ export class SingleAddressWalletCache implements WalletCacheI {
82
+ private entry: WalletCacheEntry;
74
83
  private _storage: CacheProvider | undefined;
75
- private walletCache: Record<string, WalletCacheEntry> = {};
76
- private indexCache: Record<
77
- string,
78
- {
79
- index: number;
80
- change: boolean;
81
- }
82
- > = {};
83
84
  private debounceTimer: ReturnType<typeof setTimeout> | undefined;
84
85
 
85
- get storage(): CacheProvider | undefined {
86
- if (
87
- !Config.UseMemoryCache &&
88
- !Config.UseLocalStorageCache &&
89
- !Config.UseIndexedDBCache
90
- ) {
91
- this._storage = undefined;
92
- return this._storage;
93
- }
86
+ constructor(
87
+ public walletId: string,
88
+ address: string,
89
+ tokenAddress: string,
90
+ public writeTimeout: number = 2000
91
+ ) {
92
+ this.entry = {
93
+ address,
94
+ tokenAddress,
95
+ privateKey: undefined,
96
+ publicKey: new Uint8Array(),
97
+ publicKeyHash: new Uint8Array(),
98
+ index: 0,
99
+ change: false,
100
+ status: null,
101
+ utxos: [],
102
+ rawHistory: [],
103
+ lastConfirmedHeight: 0,
104
+ };
105
+ }
94
106
 
95
- if (Config.UseMemoryCache && !(this._storage instanceof MemoryCache)) {
96
- this._storage = new MemoryCache();
97
- return this._storage;
107
+ public async init() {
108
+ this._storage = getStorage();
109
+ await this._storage?.init();
110
+ const data = await this._storage?.getItem(`walletCache-${this.walletId}`);
111
+ if (data) {
112
+ try {
113
+ const parsed = parse(data);
114
+ // Restore persisted fields, keep address identity from constructor
115
+ const addr = this.entry.address;
116
+ const tokenAddr = this.entry.tokenAddress;
117
+ Object.assign(this.entry, parsed, {
118
+ address: addr,
119
+ tokenAddress: tokenAddr,
120
+ });
121
+ } catch (_e) {
122
+ // ignore
123
+ }
98
124
  }
125
+ }
99
126
 
100
- if (
101
- Config.UseLocalStorageCache &&
102
- !(this._storage instanceof WebStorageCache)
103
- ) {
104
- this._storage = new WebStorageCache();
105
- return this._storage;
127
+ public async persist(immediate = false) {
128
+ if (this.debounceTimer) clearTimeout(this.debounceTimer);
129
+ if (immediate) {
130
+ this.debounceTimer = undefined;
131
+ await this._storage?.setItem(
132
+ `walletCache-${this.walletId}`,
133
+ stringify(this.entry)
134
+ );
135
+ } else {
136
+ this.debounceTimer = setTimeout(() => {
137
+ this.debounceTimer = undefined;
138
+ this._storage?.setItem(
139
+ `walletCache-${this.walletId}`,
140
+ stringify(this.entry)
141
+ );
142
+ }, this.writeTimeout);
106
143
  }
144
+ }
107
145
 
108
- if (
109
- Config.UseIndexedDBCache &&
110
- !(this._storage instanceof IndexedDbCache)
111
- ) {
112
- this._storage = new IndexedDbCache("WalletCache");
113
- return this._storage;
114
- }
146
+ public get(address: string) {
147
+ return address === this.entry.address ? this.entry : undefined;
148
+ }
149
+
150
+ public getByIndex(_index: number, _change: boolean) {
151
+ return this.entry;
152
+ }
115
153
 
116
- return this._storage;
154
+ public setStatusAndUtxos(
155
+ address: string,
156
+ status: string | null,
157
+ utxos: Utxo[],
158
+ rawHistory: TxI[],
159
+ lastConfirmedHeight: number
160
+ ) {
161
+ if (address !== this.entry.address) return;
162
+ this.entry.status = status;
163
+ this.entry.utxos = utxos;
164
+ this.entry.rawHistory = rawHistory;
165
+ this.entry.lastConfirmedHeight = lastConfirmedHeight;
166
+ this.persist();
117
167
  }
168
+ }
169
+
170
+ export class HDWalletCache implements WalletCacheI {
171
+ private _storage: CacheProvider | undefined;
172
+ private walletCache: Record<string, WalletCacheEntry> = {};
173
+ private indexCache: Record<
174
+ string,
175
+ {
176
+ index: number;
177
+ change: boolean;
178
+ }
179
+ > = {};
180
+ private debounceTimer: ReturnType<typeof setTimeout> | undefined;
118
181
 
119
182
  constructor(
120
183
  public walletId: string,
@@ -128,8 +191,9 @@ export class PersistentWalletCache implements WalletCacheI {
128
191
  }
129
192
 
130
193
  public async init() {
131
- await this.storage?.init();
132
- const data = await this.storage?.getItem(`walletCache-${this.walletId}`);
194
+ this._storage = getStorage();
195
+ await this._storage?.init();
196
+ const data = await this._storage?.getItem(`walletCache-${this.walletId}`);
133
197
  if (data) {
134
198
  try {
135
199
  const parsed = parse(data);
@@ -141,24 +205,25 @@ export class PersistentWalletCache implements WalletCacheI {
141
205
  }
142
206
  }
143
207
 
144
- private schedulePersist() {
208
+ public async persist(immediate = false) {
145
209
  if (this.debounceTimer) clearTimeout(this.debounceTimer);
146
- this.debounceTimer = setTimeout(() => {
147
- this.persist().catch(() => {});
148
- }, this.writeTimeout);
149
- }
150
-
151
- public async persist() {
152
- if (this.debounceTimer) clearTimeout(this.debounceTimer);
153
- this.debounceTimer = undefined;
154
-
155
- this.storage?.setItem(
156
- `walletCache-${this.walletId}`,
157
- stringify({
158
- walletCache: this.walletCache,
159
- indexCache: this.indexCache,
160
- })
161
- );
210
+ const write = () =>
211
+ this._storage?.setItem(
212
+ `walletCache-${this.walletId}`,
213
+ stringify({
214
+ walletCache: this.walletCache,
215
+ indexCache: this.indexCache,
216
+ })
217
+ );
218
+ if (immediate) {
219
+ this.debounceTimer = undefined;
220
+ await write();
221
+ } else {
222
+ this.debounceTimer = setTimeout(() => {
223
+ this.debounceTimer = undefined;
224
+ write();
225
+ }, this.writeTimeout);
226
+ }
162
227
  }
163
228
 
164
229
  public getByIndex(addressIndex: number, change: boolean) {
@@ -212,7 +277,7 @@ export class PersistentWalletCache implements WalletCacheI {
212
277
  change,
213
278
  };
214
279
 
215
- this.schedulePersist();
280
+ this.persist();
216
281
  }
217
282
 
218
283
  return this.walletCache[id];
@@ -249,6 +314,6 @@ export class PersistentWalletCache implements WalletCacheI {
249
314
  this.walletCache[key].rawHistory = rawHistory;
250
315
  this.walletCache[key].lastConfirmedHeight = lastConfirmedHeight;
251
316
 
252
- this.schedulePersist();
317
+ this.persist();
253
318
  }
254
319
  }
package/src/mine/mine.ts CHANGED
@@ -1,4 +1,7 @@
1
1
  import { binToBase64, utf8ToBin } from "@bitauth/libauth";
2
+ import { http } from "@rpckit/http";
3
+ import { fallback } from "@rpckit/fallback";
4
+ import type { Transport } from "@rpckit/core";
2
5
 
3
6
  /**
4
7
  * Mine blocks to a regtest address
@@ -16,27 +19,20 @@ export const mine = async ({
16
19
  cashaddr: string;
17
20
  blocks: number;
18
21
  }): Promise<any> => {
19
- const response = await fetch(`http://127.0.0.1:${process.env.RPC_PORT}/`, {
20
- method: "POST",
21
- headers: {
22
- "Content-Type": "application/json",
23
- Authorization:
24
- "Basic " +
25
- binToBase64(
26
- utf8ToBin(`${process.env.RPC_USER}:${process.env.RPC_PASS}`)
27
- ),
28
- },
29
- body: JSON.stringify({
30
- jsonrpc: "2.0",
31
- id: "0",
32
- method: "generatetoaddress",
33
- params: {
34
- nblocks: blocks,
35
- address: cashaddr,
36
- },
37
- }),
38
- });
39
- const json = await response.json();
22
+ const auth =
23
+ "Basic " +
24
+ binToBase64(utf8ToBin(`${process.env.RPC_USER}:${process.env.RPC_PASS}`));
40
25
 
41
- return json.result;
26
+ const transports = ["127.0.0.1", "host.docker.internal"].map((host) =>
27
+ http(`http://${host}:${process.env.RPC_PORT}/`, {
28
+ headers: { Authorization: auth },
29
+ })
30
+ ) as [Transport, Transport];
31
+
32
+ const transport = fallback(transports);
33
+ try {
34
+ return await transport.request("generatetoaddress", blocks, cashaddr);
35
+ } finally {
36
+ await transport.close();
37
+ }
42
38
  };
@@ -1,4 +1,5 @@
1
- import { initProviders, disconnectProviders, Connection } from "./Connection";
1
+ import { initProviders, disconnectProviders } from "./Connection";
2
+ import { createProvider } from "./default";
2
3
  import { RegTestWallet, TestNetWallet, Wallet } from "../wallet/Wif";
3
4
 
4
5
  beforeAll(async () => {
@@ -41,11 +42,11 @@ test.skip("Should lower overhead in creating wallets", async () => {
41
42
  expect(height).toBeGreaterThan(114);
42
43
  });
43
44
 
44
- test("Should create a new Connection", async () => {
45
- let conn = new Connection("mainnet", "wss://bch.imaginary.cash:50004");
46
- await conn.ready();
47
- expect(conn.networkProvider == globalThis.BCH).toBeFalsy();
48
- let blockheight = await conn.networkProvider.getBlockHeight();
45
+ test("Should create a provider with custom servers", async () => {
46
+ let provider = await createProvider("mainnet", "wss://fulcrum.pat.mn:50004");
47
+ await provider.connect();
48
+ expect(provider == globalThis.BCH).toBeFalsy();
49
+ let blockheight = await provider.getBlockHeight();
49
50
  expect(blockheight).toBeGreaterThan(10000);
50
- expect(10001).toBeGreaterThan(10000);
51
+ await provider.disconnect();
51
52
  });
@@ -1,19 +1,16 @@
1
- import { default as NetworkProvider } from "./NetworkProvider.js";
2
1
  import {
3
- getNetworkProvider,
2
+ createProvider,
4
3
  setGlobalProvider,
5
4
  getGlobalProvider,
6
5
  removeGlobalProvider,
7
6
  } from "./default.js";
8
7
  import { Network } from "../interface.js";
9
8
  import { networkTickerMap } from "./constant.js";
10
- import { prefixFromNetworkMap } from "../enum.js";
11
- import { CashAddressNetworkPrefix } from "@bitauth/libauth";
12
9
 
13
10
  export async function initProvider(network: Network) {
14
11
  if (!getGlobalProvider(network)) {
15
- const conn = new Connection(network);
16
- const provider = (await conn.ready()).networkProvider;
12
+ const provider = await createProvider(network);
13
+ await provider.connect();
17
14
  setGlobalProvider(network, provider);
18
15
  return provider;
19
16
  }
@@ -22,10 +19,18 @@ export async function initProvider(network: Network) {
22
19
 
23
20
  export async function initProviders(networks?: Network[]) {
24
21
  networks = networks ? networks : (Object.keys(networkTickerMap) as Network[]);
25
- let initPromises = networks.map((n) => initProvider(n));
26
- await Promise.all(initPromises).catch((e) => {
27
- console.warn(`Warning, couldn't establish a connection for ${e}`);
28
- });
22
+ const results = await Promise.allSettled(
23
+ networks.map((n) => initProvider(n))
24
+ );
25
+ for (let i = 0; i < results.length; i++) {
26
+ if (results[i].status === "rejected") {
27
+ const { reason } = results[i] as PromiseRejectedResult;
28
+ const message = reason instanceof Error ? reason.message : reason;
29
+ console.warn(
30
+ `Warning, couldn't establish a connection for ${networks[i]}: ${message}`
31
+ );
32
+ }
33
+ }
29
34
  }
30
35
 
31
36
  async function disconnectProvider(network: Network) {
@@ -33,41 +38,10 @@ async function disconnectProvider(network: Network) {
33
38
  if (provider) {
34
39
  await provider.disconnect();
35
40
  removeGlobalProvider(network);
36
- return;
37
- } else {
38
- // console.warn(
39
- // `Ignoring attempt to disconnect non-existent ${network} provider`
40
- // );
41
- return true;
42
41
  }
43
42
  }
44
43
 
45
44
  export async function disconnectProviders(networks?: Network[]) {
46
45
  networks = networks ? networks : (Object.keys(networkTickerMap) as Network[]);
47
- let disconnectPromises = networks.map((n) => disconnectProvider(n));
48
- await Promise.all(disconnectPromises);
49
- }
50
-
51
- export class Connection {
52
- public network: Network;
53
- public servers?: string[];
54
- public networkPrefix: CashAddressNetworkPrefix;
55
- public networkProvider: NetworkProvider;
56
-
57
- constructor(network?: Network, servers?: string[] | string) {
58
- this.network = network ? network : "mainnet";
59
- this.networkPrefix = prefixFromNetworkMap[this.network];
60
- this.networkProvider = getNetworkProvider(this.network, servers, true);
61
- }
62
-
63
- public async ready() {
64
- await this.networkProvider.connect();
65
- await this.networkProvider.ready();
66
- return this;
67
- }
68
-
69
- public async disconnect() {
70
- await this.networkProvider.disconnect();
71
- return this;
72
- }
46
+ await Promise.all(networks.map((n) => disconnectProvider(n)));
73
47
  }
@@ -1,10 +1,5 @@
1
- import {
2
- ElectrumClient,
3
- RequestResponse,
4
- ElectrumClientEvents,
5
- RPCNotification,
6
- ConnectionStatus,
7
- } from "@electrum-cash/network";
1
+ import type { Transport, Unsubscribe } from "@rpckit/core";
2
+ import type { ElectrumCashSchema } from "@rpckit/core/electrum-cash";
8
3
  import { default as NetworkProvider } from "./NetworkProvider.js";
9
4
  import {
10
5
  HexHeaderI,
@@ -34,9 +29,8 @@ import { MemoryCache } from "../cache/MemoryCache.js";
34
29
  type CachedRawTransaction = ElectrumRawTransaction & { fetchHeight: number };
35
30
 
36
31
  export default class ElectrumNetworkProvider implements NetworkProvider {
37
- public electrum: ElectrumClient<ElectrumClientEvents>;
32
+ public transport: Transport<ElectrumCashSchema>;
38
33
  public subscriptions: number = 0;
39
- private subscriptionMap: Record<string, number> = {};
40
34
  private currentHeight: number = 0;
41
35
  private headerCancelFn?: CancelFn;
42
36
 
@@ -74,14 +68,13 @@ export default class ElectrumNetworkProvider implements NetworkProvider {
74
68
  }
75
69
 
76
70
  constructor(
77
- electrum: ElectrumClient<ElectrumClientEvents>,
78
- public network: Network = Network.MAINNET,
79
- private manualConnectionManagement?: boolean
71
+ transport: Transport<ElectrumCashSchema>,
72
+ public network: Network = Network.MAINNET
80
73
  ) {
81
- if (electrum) {
82
- this.electrum = electrum;
74
+ if (transport) {
75
+ this.transport = transport;
83
76
  } else {
84
- throw new Error(`A electrum-cash client is required.`);
77
+ throw new Error(`A transport is required.`);
85
78
  }
86
79
  }
87
80
 
@@ -162,6 +155,7 @@ export default class ElectrumNetworkProvider implements NetworkProvider {
162
155
  }
163
156
 
164
157
  if (misses.length > 0) {
158
+ // rpckit automatically batches concurrent requests via BatchScheduler
165
159
  const fetched = await Promise.all(
166
160
  misses.map(async (hash) => {
167
161
  const tx = await this.performRequest<string>(
@@ -215,6 +209,7 @@ export default class ElectrumNetworkProvider implements NetworkProvider {
215
209
  }
216
210
 
217
211
  if (misses.length > 0) {
212
+ // rpckit automatically batches concurrent requests via BatchScheduler
218
213
  const fetched = await Promise.all(
219
214
  misses.map(async (height) => {
220
215
  const result = await this.performRequest<HexHeaderI>(
@@ -342,7 +337,7 @@ export default class ElectrumNetworkProvider implements NetworkProvider {
342
337
  } catch (error: any) {
343
338
  if (
344
339
  (error.message as string).indexOf(
345
- "No such mempool or blockchain transaction."
340
+ "No such mempool or blockchain transaction"
346
341
  ) > -1
347
342
  )
348
343
  throw Error(
@@ -490,8 +485,8 @@ export default class ElectrumNetworkProvider implements NetworkProvider {
490
485
  public async waitForBlock(height?: number): Promise<HexHeaderI> {
491
486
  return new Promise(async (resolve) => {
492
487
  let cancelWatch: CancelFn;
493
- if (this.electrum.chainHeight && !height) {
494
- height = this.electrum.chainHeight + 1;
488
+ if (this.currentHeight && !height) {
489
+ height = this.currentHeight + 1;
495
490
  }
496
491
 
497
492
  cancelWatch = await this.watchBlocks(async (header) => {
@@ -549,63 +544,38 @@ export default class ElectrumNetworkProvider implements NetworkProvider {
549
544
  ): Promise<T> {
550
545
  await this.ready();
551
546
 
552
- const requestTimeout = new Promise(function (_resolve, reject) {
553
- setTimeout(function () {
554
- reject("electrum-cash request timed out, retrying");
555
- }, 30000);
556
- }).catch(function (e) {
557
- throw e;
558
- });
559
-
560
- const request = this.electrum.request(name, ...parameters);
547
+ const TIMEOUT_MSG = "electrum-cash request timed out, retrying";
561
548
 
562
- return await Promise.race([request, requestTimeout])
563
- .then((value) => {
564
- if (value instanceof Error) throw value;
565
- let result = value as RequestResponse;
566
- return result as T;
567
- })
568
- .catch(async () => {
569
- return await Promise.race([request, requestTimeout])
570
- .then((value) => {
571
- if (value instanceof Error) throw value;
572
- let result = value as RequestResponse;
573
- return result as T;
574
- })
575
- .catch(function (e) {
576
- throw e;
577
- });
549
+ const makeTimeout = () =>
550
+ new Promise<never>(function (_resolve, reject) {
551
+ setTimeout(function () {
552
+ reject(TIMEOUT_MSG);
553
+ }, 30000);
578
554
  });
579
- }
580
555
 
581
- private async trackSubscription(
582
- methodName: string,
583
- ...parameters: (string | number | boolean)[]
584
- ): Promise<void> {
585
- const key = `${methodName}-${this.network}-${JSON.stringify(parameters)}`;
586
- if (this.subscriptionMap[key]) {
587
- this.subscriptionMap[key]++;
588
- } else {
589
- this.subscriptionMap[key] = 1;
590
- }
556
+ const ensureError = (e: unknown): Error => {
557
+ if (e instanceof Error) return e;
558
+ if (typeof e === "object" && e !== null && "message" in e)
559
+ return Object.assign(new Error((e as any).message), e);
560
+ return new Error(typeof e === "string" ? e : String(e));
561
+ };
591
562
 
592
- await this.electrum.subscribe(methodName, ...parameters);
593
- }
563
+ const request = this.transport.request(name as any, ...(parameters as any));
594
564
 
595
- private async untrackSubscription(
596
- methodName: string,
597
- ...parameters: (string | number | boolean)[]
598
- ): Promise<void> {
599
- const key = `${methodName}-${this.network}-${JSON.stringify(parameters)}`;
600
- if (this.subscriptionMap[key]) {
601
- this.subscriptionMap[key]--;
602
- if (this.subscriptionMap[key] <= 0) {
603
- // only really unsubscribe if there are no more subscriptions for this `key`
604
- delete this.subscriptionMap[key];
605
-
606
- try {
607
- await this.electrum.unsubscribe(methodName, ...parameters);
608
- } catch {}
565
+ try {
566
+ const value = await Promise.race([request, makeTimeout()]);
567
+ if (value instanceof Error) throw value;
568
+ return value as T;
569
+ } catch (e: unknown) {
570
+ const error = ensureError(e);
571
+ // Only retry on timeout, not on server errors
572
+ if (error.message !== TIMEOUT_MSG) throw error;
573
+ try {
574
+ const value = await Promise.race([request, makeTimeout()]);
575
+ if (value instanceof Error) throw value;
576
+ return value as T;
577
+ } catch (e2: unknown) {
578
+ throw ensureError(e2);
609
579
  }
610
580
  }
611
581
  }
@@ -617,19 +587,21 @@ export default class ElectrumNetworkProvider implements NetworkProvider {
617
587
  ): Promise<CancelFn> {
618
588
  await this.ready();
619
589
 
620
- const handler = (data: RPCNotification) => {
621
- if (data.method === methodName) {
622
- callback(data.params);
590
+ const subscribeFn = this.transport.subscribe.bind(this.transport) as (
591
+ method: string,
592
+ ...args: unknown[]
593
+ ) => Promise<Unsubscribe>;
594
+ const unsubscribe: Unsubscribe = await subscribeFn(
595
+ methodName,
596
+ ...parameters,
597
+ (data: unknown) => {
598
+ callback(data);
623
599
  }
624
- };
625
-
626
- this.electrum.on("notification", handler);
627
- await this.trackSubscription(methodName, ...parameters);
600
+ );
628
601
  this.subscriptions++;
629
602
 
630
603
  return async () => {
631
- this.electrum.off("notification", handler);
632
- await this.untrackSubscription(methodName, ...parameters);
604
+ await unsubscribe();
633
605
  this.subscriptions--;
634
606
  };
635
607
  }
@@ -640,18 +612,12 @@ export default class ElectrumNetworkProvider implements NetworkProvider {
640
612
 
641
613
  async connect(): Promise<void> {
642
614
  await this.cache?.init();
643
- if (this.electrum.status !== ConnectionStatus.CONNECTED) {
644
- await this.electrum.connect();
645
- }
615
+ await this.transport.connect();
646
616
  }
647
617
 
648
618
  async disconnect(): Promise<boolean> {
649
- if (this.subscriptions > 0) {
650
- // console.warn(
651
- // `Trying to disconnect a network provider with ${this.subscriptions} active subscriptions. This is in most cases a bad idea.`
652
- // );
653
- }
654
619
  await this.headerCancelFn?.();
655
- return this.electrum.disconnect(true, false);
620
+ await this.transport.close();
621
+ return true;
656
622
  }
657
623
  }
@@ -70,17 +70,18 @@ describe("Rpc tests", () => {
70
70
 
71
71
  test("Watch wallet balance", async () => {
72
72
  const aliceWallet = await RegTestWallet.fromId(aliceWif);
73
+ const bobWallet = await RegTestWallet.newRandom();
73
74
 
74
75
  let result = false;
75
- aliceWallet.watchBalance((balance) => {
76
+ const cancel = await aliceWallet.watchBalance((balance) => {
76
77
  expect(balance).toBeGreaterThan(0);
77
78
  result = true;
78
- // stop watching
79
- return true;
80
79
  });
81
- await new Promise((resolve) => setTimeout(resolve, 1000));
82
- // we do not trigger the callback upon subscription anymore
80
+ // Trigger a real status change on Alice's address
81
+ await aliceWallet.send([{ cashaddr: bobWallet.cashaddr!, value: 1000n }]);
82
+ await new Promise((resolve) => setTimeout(resolve, 2000));
83
83
  expect(result).toBe(true);
84
+ await cancel();
84
85
  });
85
86
 
86
87
  test("Wait for block timeout", async () => {