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.
- package/browser/_version.d.ts +1 -1
- package/browser/index.js +1 -1
- package/browser/transactions/interfaces/BroadcastedTransaction.d.ts +0 -1
- package/browser/utxos/UTXOsManager.d.ts +5 -8
- package/build/_version.d.ts +1 -1
- package/build/_version.js +1 -1
- package/build/contracts/CallResult.js +1 -1
- package/build/providers/AbstractRpcProvider.js +2 -16
- package/build/transactions/interfaces/BroadcastedTransaction.d.ts +0 -1
- package/build/utxos/UTXOsManager.d.ts +5 -8
- package/build/utxos/UTXOsManager.js +62 -41
- package/package.json +2 -2
- package/src/_version.ts +1 -1
- package/src/contracts/CallResult.ts +5 -1
- package/src/providers/AbstractRpcProvider.ts +2 -18
- package/src/transactions/interfaces/BroadcastedTransaction.ts +0 -5
- package/src/utxos/UTXOsManager.ts +120 -93
|
@@ -16,44 +16,50 @@ const FETCH_COOLDOWN = 10000; // 10 seconds
|
|
|
16
16
|
const MEMPOOL_CHAIN_LIMIT = 25;
|
|
17
17
|
|
|
18
18
|
/**
|
|
19
|
-
*
|
|
20
|
-
* @category Bitcoin
|
|
19
|
+
* A helper interface for per-address data tracking.
|
|
21
20
|
*/
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
21
|
+
interface AddressData {
|
|
22
|
+
spentUTXOs: UTXOs;
|
|
23
|
+
pendingUTXOs: UTXOs;
|
|
26
24
|
/**
|
|
27
|
-
*
|
|
28
|
-
*
|
|
29
|
-
* - Value: number (the unconfirmed chain depth).
|
|
25
|
+
* Key: `${transactionId}:${outputIndex}`
|
|
26
|
+
* Value: the unconfirmed chain depth
|
|
30
27
|
*/
|
|
31
|
-
|
|
28
|
+
pendingUtxoDepth: Record<string, number>;
|
|
29
|
+
lastCleanup: number;
|
|
32
30
|
|
|
33
|
-
|
|
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
|
-
*
|
|
41
|
+
* Holds all address-specific data so we don’t mix up UTXOs between addresses/wallets.
|
|
37
42
|
*/
|
|
38
|
-
private
|
|
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
|
|
56
|
-
|
|
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
|
|
73
|
+
delete addressData.pendingUtxoDepth[key];
|
|
68
74
|
}
|
|
69
75
|
|
|
70
|
-
// Add
|
|
71
|
-
|
|
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
|
-
|
|
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
|
-
//
|
|
97
|
+
// Push the new UTXOs into this address’s pending and set their depth
|
|
93
98
|
for (const nu of newUTXOs) {
|
|
94
|
-
|
|
95
|
-
|
|
99
|
+
addressData.pendingUTXOs.push(nu);
|
|
100
|
+
addressData.pendingUtxoDepth[utxoKey(nu)] = newDepth;
|
|
96
101
|
}
|
|
97
102
|
}
|
|
98
103
|
|
|
99
104
|
/**
|
|
100
|
-
*
|
|
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
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
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
|
|
117
|
-
*
|
|
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] -
|
|
123
|
-
* @param {boolean} [options.filterSpentUTXOs=true] -
|
|
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
|
-
|
|
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:
|
|
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
|
|
159
|
-
for (const utxo of
|
|
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
|
|
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,
|
|
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
|
|
192
|
-
* @param {string} options.address The address to fetch UTXOs
|
|
193
|
-
* @param {bigint} options.amount The amount
|
|
194
|
-
* @param {boolean} [options.optimize=true] Optimize the UTXOs
|
|
195
|
-
* @param {boolean} [options.mergePendingUTXOs=true]
|
|
196
|
-
* @param {boolean} [options.filterSpentUTXOs=true]
|
|
197
|
-
* @param {boolean} [options.throwErrors=false]
|
|
198
|
-
* @returns {Promise<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
|
-
*
|
|
238
|
-
|
|
239
|
-
|
|
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 -
|
|
274
|
+
const age = now - addressData.lastFetchTimestamp;
|
|
244
275
|
|
|
245
|
-
// Purge
|
|
246
|
-
if (now -
|
|
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 (
|
|
252
|
-
return
|
|
282
|
+
if (addressData.lastFetchedData && age < FETCH_COOLDOWN) {
|
|
283
|
+
return addressData.lastFetchedData;
|
|
253
284
|
}
|
|
254
285
|
|
|
255
286
|
// Otherwise, fetch from the RPC
|
|
256
|
-
|
|
257
|
-
|
|
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
|
|
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
|
-
[
|
|
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
|
-
*
|
|
295
|
-
* or become known-spent.
|
|
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
|
-
|
|
299
|
-
|
|
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
|
-
|
|
333
|
+
fetched.confirmed.map((u) => `${u.transactionId}:${u.outputIndex}`),
|
|
304
334
|
);
|
|
305
|
-
|
|
306
335
|
const spentKeys = new Set(
|
|
307
|
-
|
|
308
|
-
(u) => `${u.transactionId}:${u.outputIndex}`,
|
|
309
|
-
),
|
|
336
|
+
fetched.spentTransactions.map((u) => `${u.transactionId}:${u.outputIndex}`),
|
|
310
337
|
);
|
|
311
338
|
|
|
312
|
-
|
|
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
|
|
343
|
+
delete addressData.pendingUtxoDepth[key];
|
|
317
344
|
return false;
|
|
318
345
|
}
|
|
319
346
|
return true;
|