opnet 1.4.0 → 1.4.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.
@@ -16,44 +16,50 @@ const FETCH_COOLDOWN = 10000; // 10 seconds
16
16
  const MEMPOOL_CHAIN_LIMIT = 25;
17
17
 
18
18
  /**
19
- * Manages unspent transaction outputs (UTXOs).
20
- * @category Bitcoin
19
+ * A helper interface for per-address data tracking.
21
20
  */
22
- export class UTXOsManager {
23
- private spentUTXOs: UTXOs = [];
24
- private pendingUTXOs: UTXOs = [];
25
-
21
+ interface AddressData {
22
+ spentUTXOs: UTXOs;
23
+ pendingUTXOs: UTXOs;
26
24
  /**
27
- * Tracks the current “depth” of each pending UTXO:
28
- * - Key: `${transactionId}:${outputIndex}`
29
- * - Value: number (the unconfirmed chain depth).
25
+ * Key: `${transactionId}:${outputIndex}`
26
+ * Value: the unconfirmed chain depth
30
27
  */
31
- private pendingUtxoDepth: Record<string, number> = {};
28
+ pendingUtxoDepth: Record<string, number>;
29
+ lastCleanup: number;
32
30
 
33
- private lastCleanup: number = Date.now();
31
+ lastFetchTimestamp: number;
32
+ lastFetchedData: IUTXOsData | null;
33
+ }
34
34
 
35
+ /**
36
+ * Manages unspent transaction outputs (UTXOs) by address/wallet.
37
+ * @category Bitcoin
38
+ */
39
+ export class UTXOsManager {
35
40
  /**
36
- * Cache for recently fetched data + timestamp
41
+ * Holds all address-specific data so we don’t mix up UTXOs between addresses/wallets.
37
42
  */
38
- private lastFetchTimestamp: number = 0;
39
- private lastFetchedData: IUTXOsData | null = null;
43
+ private dataByAddress: Record<string, AddressData> = {};
40
44
 
41
45
  public constructor(private readonly provider: AbstractRpcProvider) {}
42
46
 
43
47
  /**
44
- * Mark UTXOs as spent and track new UTXOs created by the transaction.
48
+ * Mark UTXOs as spent and track new UTXOs created by the transaction, _per address_.
45
49
  *
46
50
  * Enforces a mempool chain limit of 25 unconfirmed transaction descendants.
47
51
  *
52
+ * @param address - The address these spent/new UTXOs belong to
48
53
  * @param {UTXOs} spent - The UTXOs that were spent.
49
54
  * @param {UTXOs} newUTXOs - The new UTXOs created by the transaction.
50
55
  * @throws {Error} If adding the new unconfirmed outputs would exceed the mempool chain limit.
51
56
  */
52
- public spentUTXO(spent: UTXOs, newUTXOs: UTXOs): void {
57
+ public spentUTXO(address: string, spent: UTXOs, newUTXOs: UTXOs): void {
58
+ const addressData = this.getAddressData(address);
53
59
  const utxoKey = (u: UTXO) => `${u.transactionId}:${u.outputIndex}`;
54
60
 
55
- // Remove any spent UTXOs from pending
56
- this.pendingUTXOs = this.pendingUTXOs.filter((utxo) => {
61
+ // Remove spent UTXOs from that address’s pending
62
+ addressData.pendingUTXOs = addressData.pendingUTXOs.filter((utxo) => {
57
63
  return !spent.some(
58
64
  (spentUtxo) =>
59
65
  spentUtxo.transactionId === utxo.transactionId &&
@@ -61,22 +67,21 @@ export class UTXOsManager {
61
67
  );
62
68
  });
63
69
 
64
- // Also remove from the depth map if they were pending
70
+ // Also remove them from the depth map if they were pending
65
71
  for (const spentUtxo of spent) {
66
72
  const key = utxoKey(spentUtxo);
67
- delete this.pendingUtxoDepth[key];
73
+ delete addressData.pendingUtxoDepth[key];
68
74
  }
69
75
 
70
- // Add the spent UTXOs to the "spent" list
71
- this.spentUTXOs.push(...spent);
76
+ // Add spent UTXOs to the "spent" list
77
+ addressData.spentUTXOs.push(...spent);
72
78
 
73
79
  // Determine the parent depth for new UTXOs. If a spent UTXO was pending,
74
80
  // it contributes to the chain depth. If it was confirmed, depth = 0 for that.
75
81
  let maxParentDepth = 0;
76
82
  for (const spentUtxo of spent) {
77
83
  const key = utxoKey(spentUtxo);
78
- // If it was an unconfirmed (pending) parent, capture its depth
79
- const parentDepth = this.pendingUtxoDepth[key] ?? 0;
84
+ const parentDepth = addressData.pendingUtxoDepth[key] ?? 0;
80
85
  if (parentDepth > maxParentDepth) {
81
86
  maxParentDepth = parentDepth;
82
87
  }
@@ -89,38 +94,52 @@ export class UTXOsManager {
89
94
  );
90
95
  }
91
96
 
92
- // Now push the new UTXOs into pending; set their depth
97
+ // Push the new UTXOs into this address’s pending and set their depth
93
98
  for (const nu of newUTXOs) {
94
- this.pendingUTXOs.push(nu);
95
- this.pendingUtxoDepth[utxoKey(nu)] = newDepth;
99
+ addressData.pendingUTXOs.push(nu);
100
+ addressData.pendingUtxoDepth[utxoKey(nu)] = newDepth;
96
101
  }
97
102
  }
98
103
 
99
104
  /**
100
- * Clean the spent and pending UTXOs, allowing reset after transactions are built.
105
+ * Get the pending UTXOs for a specific address.
106
+ * @param address
107
+ */
108
+ public getPendingUTXOs(address: string): UTXOs {
109
+ const addressData = this.getAddressData(address);
110
+ return addressData.pendingUTXOs;
111
+ }
112
+
113
+ /**
114
+ * Clean (reset) the data for a particular address or for all addresses if none is passed.
101
115
  */
102
- public clean(): void {
103
- this.spentUTXOs = [];
104
- this.pendingUTXOs = [];
105
- this.pendingUtxoDepth = {};
106
- this.lastCleanup = Date.now();
107
-
108
- // Also reset the fetch cache.
109
- this.lastFetchTimestamp = 0;
110
- this.lastFetchedData = null;
116
+ public clean(address?: string): void {
117
+ if (address) {
118
+ // Reset a single address
119
+ const addressData = this.getAddressData(address);
120
+ addressData.spentUTXOs = [];
121
+ addressData.pendingUTXOs = [];
122
+ addressData.pendingUtxoDepth = {};
123
+ addressData.lastCleanup = Date.now();
124
+ addressData.lastFetchTimestamp = 0;
125
+ addressData.lastFetchedData = null;
126
+ } else {
127
+ // Reset everything
128
+ this.dataByAddress = {};
129
+ }
111
130
  }
112
131
 
113
132
  /**
114
- * Get UTXOs with configurable options.
133
+ * Get UTXOs with configurable options, specifically for an address.
115
134
  *
116
- * If the last UTXO fetch was less than 10 seconds ago, this returns cached data
117
- * rather than calling out to the provider again. Otherwise, it fetches fresh data.
135
+ * If the last UTXO fetch for that address was <10s ago, returns cached data.
136
+ * Otherwise, fetches fresh data from the provider.
118
137
  *
119
138
  * @param {object} options - The UTXO fetch options
120
139
  * @param {string} options.address - The address to get the UTXOs for
121
140
  * @param {boolean} [options.optimize=true] - Whether to optimize the UTXOs
122
- * @param {boolean} [options.mergePendingUTXOs=true] - Whether to merge pending UTXOs
123
- * @param {boolean} [options.filterSpentUTXOs=true] - Whether to filter out spent UTXOs
141
+ * @param {boolean} [options.mergePendingUTXOs=true] - Merge locally pending UTXOs
142
+ * @param {boolean} [options.filterSpentUTXOs=true] - Filter out known-spent UTXOs
124
143
  * @returns {Promise<UTXOs>} The UTXOs
125
144
  * @throws {Error} If something goes wrong
126
145
  */
@@ -130,21 +149,18 @@ export class UTXOsManager {
130
149
  mergePendingUTXOs = true,
131
150
  filterSpentUTXOs = true,
132
151
  }: RequestUTXOsParams): Promise<UTXOs> {
152
+ const addressData = this.getAddressData(address);
133
153
  const fetchedData = await this.maybeFetchUTXOs(address, optimize);
134
154
 
135
- // Helper function to create a unique key for UTXOs
136
155
  const utxoKey = (utxo: UTXO) => `${utxo.transactionId}:${utxo.outputIndex}`;
137
-
138
- // Prepare sets for quick lookups
139
- const pendingUTXOKeys = new Set(this.pendingUTXOs.map(utxoKey));
140
- const spentUTXOKeys = new Set(this.spentUTXOs.map(utxoKey));
156
+ const pendingUTXOKeys = new Set(addressData.pendingUTXOs.map(utxoKey));
157
+ const spentUTXOKeys = new Set(addressData.spentUTXOs.map(utxoKey));
141
158
  const fetchedSpentKeys = new Set(fetchedData.spentTransactions.map(utxoKey));
142
159
 
143
160
  // Start with confirmed UTXOs
144
- const combinedUTXOs: UTXO[] = [];
161
+ const combinedUTXOs: UTXOs = [];
145
162
  const combinedKeysSet = new Set<string>();
146
163
 
147
- // Add confirmed UTXOs without duplicates
148
164
  for (const utxo of fetchedData.confirmed) {
149
165
  const key = utxoKey(utxo);
150
166
  if (!combinedKeysSet.has(key)) {
@@ -155,16 +171,15 @@ export class UTXOsManager {
155
171
 
156
172
  // Merge pending UTXOs if requested
157
173
  if (mergePendingUTXOs) {
158
- // Add currently pending UTXOs without duplicates
159
- for (const utxo of this.pendingUTXOs) {
174
+ // Add currently pending
175
+ for (const utxo of addressData.pendingUTXOs) {
160
176
  const key = utxoKey(utxo);
161
177
  if (!combinedKeysSet.has(key)) {
162
178
  combinedUTXOs.push(utxo);
163
179
  combinedKeysSet.add(key);
164
180
  }
165
181
  }
166
-
167
- // Add fetched pending UTXOs that aren't already in pending or combined
182
+ // Add fetched pending UTXOs that aren't already known
168
183
  for (const utxo of fetchedData.pending) {
169
184
  const key = utxoKey(utxo);
170
185
  if (!pendingUTXOKeys.has(key) && !combinedKeysSet.has(key)) {
@@ -177,7 +192,7 @@ export class UTXOsManager {
177
192
  // Filter out UTXOs spent locally
178
193
  let finalUTXOs = combinedUTXOs.filter((utxo) => !spentUTXOKeys.has(utxoKey(utxo)));
179
194
 
180
- // Optionally filter out UTXOs that are known-spent in the fetch
195
+ // Optionally filter out UTXOs that are known spent in the fetch
181
196
  if (filterSpentUTXOs && fetchedSpentKeys.size > 0) {
182
197
  finalUTXOs = finalUTXOs.filter((utxo) => !fetchedSpentKeys.has(utxoKey(utxo)));
183
198
  }
@@ -186,17 +201,17 @@ export class UTXOsManager {
186
201
  }
187
202
 
188
203
  /**
189
- * Fetch UTXOs for a specific amount needed, merging from pending and confirmed UTXOs.
204
+ * Fetch UTXOs for a specific amount needed, from a single address,
205
+ * merging from pending and confirmed UTXOs.
190
206
  *
191
- * @param {object} options The UTXO fetch options
192
- * @param {string} options.address The address to fetch UTXOs from
193
- * @param {bigint} options.amount The amount of UTXOs to retrieve
194
- * @param {boolean} [options.optimize=true] Optimize the UTXOs (e.g., remove dust)
195
- * @param {boolean} [options.mergePendingUTXOs=true] - Whether to merge pending UTXOs
196
- * @param {boolean} [options.filterSpentUTXOs=true] - Whether to filter out spent UTXOs
197
- * @param {boolean} [options.throwErrors=false] - Whether to throw errors if UTXOs are insufficient
198
- * @returns {Promise<UTXOs>} The fetched UTXOs
199
- * @throws {Error} If something goes wrong or if not enough UTXOs are available (when throwErrors=true)
207
+ * @param {object} options
208
+ * @param {string} options.address The address to fetch UTXOs for
209
+ * @param {bigint} options.amount The needed amount
210
+ * @param {boolean} [options.optimize=true] Optimize the UTXOs
211
+ * @param {boolean} [options.mergePendingUTXOs=true] Merge pending
212
+ * @param {boolean} [options.filterSpentUTXOs=true] Filter out spent
213
+ * @param {boolean} [options.throwErrors=false] Throw error if insufficient
214
+ * @returns {Promise<UTXOs>}
200
215
  */
201
216
  public async getUTXOsForAmount({
202
217
  address,
@@ -234,42 +249,57 @@ export class UTXOsManager {
234
249
  }
235
250
 
236
251
  /**
237
- * Checks if we need to fetch fresh UTXOs or can return the cached data.
238
- * - If less than 10s since last fetch, return cached data
239
- * - Otherwise, fetch new data from the provider
252
+ * Return the AddressData object for a given address. Initializes it if nonexistent.
253
+ */
254
+ private getAddressData(address: string): AddressData {
255
+ if (!this.dataByAddress[address]) {
256
+ this.dataByAddress[address] = {
257
+ spentUTXOs: [],
258
+ pendingUTXOs: [],
259
+ pendingUtxoDepth: {},
260
+ lastCleanup: Date.now(),
261
+ lastFetchTimestamp: 0,
262
+ lastFetchedData: null,
263
+ };
264
+ }
265
+ return this.dataByAddress[address];
266
+ }
267
+
268
+ /**
269
+ * Checks if we need to fetch fresh UTXOs or can return the cached data (per address).
240
270
  */
241
271
  private async maybeFetchUTXOs(address: string, optimize: boolean): Promise<IUTXOsData> {
272
+ const addressData = this.getAddressData(address);
242
273
  const now = Date.now();
243
- const age = now - this.lastFetchTimestamp;
274
+ const age = now - addressData.lastFetchTimestamp;
244
275
 
245
- // Purge after certain time
246
- if (now - this.lastCleanup > AUTO_PURGE_AFTER) {
247
- this.clean();
276
+ // Purge if it's been too long for this address
277
+ if (now - addressData.lastCleanup > AUTO_PURGE_AFTER) {
278
+ this.clean(address); // Clean only this address data
248
279
  }
249
280
 
250
281
  // If it's been less than FETCH_COOLDOWN ms, return cached data if available
251
- if (this.lastFetchedData && age < FETCH_COOLDOWN) {
252
- return this.lastFetchedData;
282
+ if (addressData.lastFetchedData && age < FETCH_COOLDOWN) {
283
+ return addressData.lastFetchedData;
253
284
  }
254
285
 
255
286
  // Otherwise, fetch from the RPC
256
- this.lastFetchedData = await this.fetchUTXOs(address, optimize);
257
- this.lastFetchTimestamp = now;
287
+ addressData.lastFetchedData = await this.fetchUTXOs(address, optimize);
288
+ addressData.lastFetchTimestamp = now;
258
289
 
259
290
  // Remove any pending UTXOs that have become confirmed or known spent
260
- this.syncPendingDepthWithFetched();
291
+ this.syncPendingDepthWithFetched(address);
261
292
 
262
- return this.lastFetchedData;
293
+ return addressData.lastFetchedData;
263
294
  }
264
295
 
265
296
  /**
266
- * Generic method to fetch all UTXOs in one call (confirmed, pending, spent).
297
+ * Generic method to fetch all UTXOs in one call (confirmed, pending, spent) for a given address.
267
298
  */
268
299
  private async fetchUTXOs(address: string, optimize: boolean = false): Promise<IUTXOsData> {
269
- const addressStr: string = address.toString();
270
300
  const payload: JsonRpcPayload = this.provider.buildJsonRpcPayload(
271
301
  JSONRpcMethods.GET_UTXOS,
272
- [addressStr, optimize],
302
+ [address, optimize],
273
303
  );
274
304
 
275
305
  const rawUTXOs: JsonRpcResult = await this.provider.callPayloadSingle(payload);
@@ -291,29 +321,26 @@ export class UTXOsManager {
291
321
  }
292
322
 
293
323
  /**
294
- * Whenever we fetch new data, some pending UTXOs may have confirmed
295
- * or become known-spent. We remove them from pending state and depth map.
324
+ * After fetching new data for a single address, some pending UTXOs may have confirmed
325
+ * or become known-spent. Remove them from pending state/depth map.
296
326
  */
297
- private syncPendingDepthWithFetched(): void {
298
- if (!this.lastFetchedData) {
299
- return;
300
- }
327
+ private syncPendingDepthWithFetched(address: string): void {
328
+ const addressData = this.getAddressData(address);
329
+ const fetched = addressData.lastFetchedData;
330
+ if (!fetched) return;
301
331
 
302
332
  const confirmedKeys = new Set(
303
- this.lastFetchedData.confirmed.map((u) => `${u.transactionId}:${u.outputIndex}`),
333
+ fetched.confirmed.map((u) => `${u.transactionId}:${u.outputIndex}`),
304
334
  );
305
-
306
335
  const spentKeys = new Set(
307
- this.lastFetchedData.spentTransactions.map(
308
- (u) => `${u.transactionId}:${u.outputIndex}`,
309
- ),
336
+ fetched.spentTransactions.map((u) => `${u.transactionId}:${u.outputIndex}`),
310
337
  );
311
338
 
312
- this.pendingUTXOs = this.pendingUTXOs.filter((u) => {
339
+ addressData.pendingUTXOs = addressData.pendingUTXOs.filter((u) => {
313
340
  const key = `${u.transactionId}:${u.outputIndex}`;
314
341
  // If it’s now confirmed or known spent, remove it from pending
315
342
  if (confirmedKeys.has(key) || spentKeys.has(key)) {
316
- delete this.pendingUtxoDepth[key];
343
+ delete addressData.pendingUtxoDepth[key];
317
344
  return false;
318
345
  }
319
346
  return true;