@arkade-os/sdk 0.4.8 → 0.4.10
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/cjs/contracts/contractManager.js +59 -11
- package/dist/cjs/contracts/contractWatcher.js +21 -2
- package/dist/cjs/index.js +4 -3
- package/dist/cjs/providers/expoIndexer.js +1 -0
- package/dist/cjs/providers/indexer.js +1 -0
- package/dist/cjs/utils/arkTransaction.js +17 -6
- package/dist/cjs/utils/transactionHistory.js +2 -1
- package/dist/cjs/wallet/serviceWorker/wallet-message-handler.js +109 -29
- package/dist/cjs/wallet/serviceWorker/wallet.js +25 -2
- package/dist/cjs/wallet/vtxo-manager.js +81 -50
- package/dist/cjs/wallet/wallet.js +46 -34
- package/dist/cjs/worker/errors.js +3 -4
- package/dist/cjs/worker/messageBus.js +7 -0
- package/dist/esm/contracts/contractManager.js +59 -11
- package/dist/esm/contracts/contractWatcher.js +21 -2
- package/dist/esm/index.js +2 -2
- package/dist/esm/providers/expoIndexer.js +1 -0
- package/dist/esm/providers/indexer.js +1 -0
- package/dist/esm/utils/arkTransaction.js +17 -6
- package/dist/esm/utils/transactionHistory.js +2 -1
- package/dist/esm/wallet/serviceWorker/wallet-message-handler.js +109 -29
- package/dist/esm/wallet/serviceWorker/wallet.js +26 -3
- package/dist/esm/wallet/vtxo-manager.js +81 -50
- package/dist/esm/wallet/wallet.js +46 -34
- package/dist/esm/worker/errors.js +2 -3
- package/dist/esm/worker/messageBus.js +7 -0
- package/dist/types/contracts/contractManager.d.ts +10 -0
- package/dist/types/index.d.ts +2 -2
- package/dist/types/repositories/serialization.d.ts +1 -0
- package/dist/types/utils/transactionHistory.d.ts +1 -1
- package/dist/types/wallet/index.d.ts +2 -0
- package/dist/types/wallet/serviceWorker/wallet-message-handler.d.ts +23 -6
- package/dist/types/wallet/serviceWorker/wallet.d.ts +9 -1
- package/dist/types/wallet/vtxo-manager.d.ts +5 -0
- package/dist/types/worker/errors.d.ts +1 -0
- package/dist/types/worker/messageBus.d.ts +6 -0
- package/package.json +1 -1
|
@@ -157,7 +157,12 @@ class VtxoManager {
|
|
|
157
157
|
this.knownBoardingUtxos = new Set();
|
|
158
158
|
this.sweptBoardingUtxos = new Set();
|
|
159
159
|
this.pollInProgress = false;
|
|
160
|
+
this.disposed = false;
|
|
160
161
|
this.consecutivePollFailures = 0;
|
|
162
|
+
// Guards against renewal feedback loop: when renewVtxos() settles, the
|
|
163
|
+
// server emits new VTXOs → vtxo_received → renewVtxos() again → infinite loop.
|
|
164
|
+
this.renewalInProgress = false;
|
|
165
|
+
this.lastRenewalTimestamp = 0;
|
|
161
166
|
// Normalize: prefer settlementConfig, fall back to renewalConfig, default to enabled
|
|
162
167
|
if (settlementConfig !== undefined) {
|
|
163
168
|
this.settlementConfig = settlementConfig;
|
|
@@ -341,32 +346,43 @@ class VtxoManager {
|
|
|
341
346
|
* ```
|
|
342
347
|
*/
|
|
343
348
|
async renewVtxos(eventCallback) {
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
349
|
+
if (this.renewalInProgress) {
|
|
350
|
+
throw new Error("Renewal already in progress");
|
|
351
|
+
}
|
|
352
|
+
this.renewalInProgress = true;
|
|
353
|
+
try {
|
|
354
|
+
// Get all VTXOs (including recoverable ones)
|
|
355
|
+
// Use default threshold to bypass settlementConfig gate (manual API should always work)
|
|
356
|
+
const vtxos = await this.getExpiringVtxos(this.settlementConfig !== false &&
|
|
357
|
+
this.settlementConfig?.vtxoThreshold !== undefined
|
|
358
|
+
? this.settlementConfig.vtxoThreshold * 1000
|
|
359
|
+
: exports.DEFAULT_RENEWAL_CONFIG.thresholdMs);
|
|
360
|
+
if (vtxos.length === 0) {
|
|
361
|
+
throw new Error("No VTXOs available to renew");
|
|
362
|
+
}
|
|
363
|
+
const totalAmount = vtxos.reduce((sum, vtxo) => sum + vtxo.value, 0);
|
|
364
|
+
// Get dust amount from wallet
|
|
365
|
+
const dustAmount = getDustAmount(this.wallet);
|
|
366
|
+
// Check if total amount is above dust threshold
|
|
367
|
+
if (BigInt(totalAmount) < dustAmount) {
|
|
368
|
+
throw new Error(`Total amount ${totalAmount} is below dust threshold ${dustAmount}`);
|
|
369
|
+
}
|
|
370
|
+
const arkAddress = await this.wallet.getAddress();
|
|
371
|
+
const txid = await this.wallet.settle({
|
|
372
|
+
inputs: vtxos,
|
|
373
|
+
outputs: [
|
|
374
|
+
{
|
|
375
|
+
address: arkAddress,
|
|
376
|
+
amount: BigInt(totalAmount),
|
|
377
|
+
},
|
|
378
|
+
],
|
|
379
|
+
}, eventCallback);
|
|
380
|
+
this.lastRenewalTimestamp = Date.now();
|
|
381
|
+
return txid;
|
|
382
|
+
}
|
|
383
|
+
finally {
|
|
384
|
+
this.renewalInProgress = false;
|
|
359
385
|
}
|
|
360
|
-
const arkAddress = await this.wallet.getAddress();
|
|
361
|
-
return this.wallet.settle({
|
|
362
|
-
inputs: vtxos,
|
|
363
|
-
outputs: [
|
|
364
|
-
{
|
|
365
|
-
address: arkAddress,
|
|
366
|
-
amount: BigInt(totalAmount),
|
|
367
|
-
},
|
|
368
|
-
],
|
|
369
|
-
}, eventCallback);
|
|
370
386
|
}
|
|
371
387
|
// ========== Boarding UTXO Sweep Methods ==========
|
|
372
388
|
/**
|
|
@@ -534,7 +550,11 @@ class VtxoManager {
|
|
|
534
550
|
}
|
|
535
551
|
// Start polling for boarding UTXOs independently of contract manager
|
|
536
552
|
// SSE setup. Use a short delay to let the wallet finish construction.
|
|
537
|
-
setTimeout(() =>
|
|
553
|
+
this.startupPollTimeoutId = setTimeout(() => {
|
|
554
|
+
if (this.disposed)
|
|
555
|
+
return;
|
|
556
|
+
this.startBoardingUtxoPoll();
|
|
557
|
+
}, 1000);
|
|
538
558
|
try {
|
|
539
559
|
const [delegatorManager, contractManager, destination] = await Promise.all([
|
|
540
560
|
this.wallet.getDelegatorManager(),
|
|
@@ -545,28 +565,33 @@ class VtxoManager {
|
|
|
545
565
|
if (event.type !== "vtxo_received") {
|
|
546
566
|
return;
|
|
547
567
|
}
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
568
|
+
const msSinceLastRenewal = Date.now() - this.lastRenewalTimestamp;
|
|
569
|
+
const shouldRenew = !this.renewalInProgress &&
|
|
570
|
+
msSinceLastRenewal >= VtxoManager.RENEWAL_COOLDOWN_MS;
|
|
571
|
+
if (shouldRenew) {
|
|
572
|
+
this.renewVtxos().catch((e) => {
|
|
573
|
+
if (e instanceof Error) {
|
|
574
|
+
if (e.message.includes("No VTXOs available to renew")) {
|
|
575
|
+
// Not an error, just no VTXO eligible for renewal.
|
|
576
|
+
return;
|
|
577
|
+
}
|
|
578
|
+
if (e.message.includes("is below dust threshold")) {
|
|
579
|
+
// Not an error, just below dust threshold.
|
|
580
|
+
// As more VTXOs are received, the threshold will be raised.
|
|
581
|
+
return;
|
|
582
|
+
}
|
|
583
|
+
if (e.message.includes("VTXO_ALREADY_REGISTERED") ||
|
|
584
|
+
e.message.includes("duplicated input")) {
|
|
585
|
+
// VTXO is already being used in a concurrent
|
|
586
|
+
// user-initiated operation. Skip silently — the
|
|
587
|
+
// wallet's tx lock serializes these, but the
|
|
588
|
+
// renewal will retry on the next cycle.
|
|
589
|
+
return;
|
|
590
|
+
}
|
|
553
591
|
}
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
return;
|
|
558
|
-
}
|
|
559
|
-
if (e.message.includes("VTXO_ALREADY_REGISTERED") ||
|
|
560
|
-
e.message.includes("duplicated input")) {
|
|
561
|
-
// VTXO is already being used in a concurrent
|
|
562
|
-
// user-initiated operation. Skip silently — the
|
|
563
|
-
// wallet's tx lock serializes these, but the
|
|
564
|
-
// renewal will retry on the next cycle.
|
|
565
|
-
return;
|
|
566
|
-
}
|
|
567
|
-
}
|
|
568
|
-
console.error("Error renewing VTXOs:", e);
|
|
569
|
-
});
|
|
592
|
+
console.error("Error renewing VTXOs:", e);
|
|
593
|
+
});
|
|
594
|
+
}
|
|
570
595
|
delegatorManager
|
|
571
596
|
?.delegate(event.vtxos, destination)
|
|
572
597
|
.catch((e) => {
|
|
@@ -606,7 +631,7 @@ class VtxoManager {
|
|
|
606
631
|
this.pollBoardingUtxos();
|
|
607
632
|
}
|
|
608
633
|
schedulePoll() {
|
|
609
|
-
if (this.settlementConfig === false)
|
|
634
|
+
if (this.disposed || this.settlementConfig === false)
|
|
610
635
|
return;
|
|
611
636
|
const delay = this.getNextPollDelay();
|
|
612
637
|
this.pollTimeoutId = setTimeout(() => this.pollBoardingUtxos(), delay);
|
|
@@ -680,8 +705,8 @@ class VtxoManager {
|
|
|
680
705
|
const expired = boardingUtxos.filter((utxo) => (0, arkTransaction_1.hasBoardingTxExpired)(utxo, boardingTimelock, chainTipHeight));
|
|
681
706
|
expiredSet = new Set(expired.map((u) => `${u.txid}:${u.vout}`));
|
|
682
707
|
}
|
|
683
|
-
catch {
|
|
684
|
-
|
|
708
|
+
catch (e) {
|
|
709
|
+
throw e instanceof Error ? e : new Error(String(e));
|
|
685
710
|
}
|
|
686
711
|
const unsettledUtxos = boardingUtxos.filter((u) => !this.knownBoardingUtxos.has(`${u.txid}:${u.vout}`) &&
|
|
687
712
|
!expiredSet.has(`${u.txid}:${u.vout}`));
|
|
@@ -703,6 +728,11 @@ class VtxoManager {
|
|
|
703
728
|
}
|
|
704
729
|
async dispose() {
|
|
705
730
|
this.disposePromise ?? (this.disposePromise = (async () => {
|
|
731
|
+
this.disposed = true;
|
|
732
|
+
if (this.startupPollTimeoutId) {
|
|
733
|
+
clearTimeout(this.startupPollTimeoutId);
|
|
734
|
+
this.startupPollTimeoutId = undefined;
|
|
735
|
+
}
|
|
706
736
|
if (this.pollTimeoutId) {
|
|
707
737
|
clearTimeout(this.pollTimeoutId);
|
|
708
738
|
this.pollTimeoutId = undefined;
|
|
@@ -719,3 +749,4 @@ class VtxoManager {
|
|
|
719
749
|
}
|
|
720
750
|
exports.VtxoManager = VtxoManager;
|
|
721
751
|
VtxoManager.MAX_BACKOFF_MS = 5 * 60 * 1000; // 5 minutes
|
|
752
|
+
VtxoManager.RENEWAL_COOLDOWN_MS = 30000; // 30 seconds
|
|
@@ -278,27 +278,33 @@ class ReadonlyWallet {
|
|
|
278
278
|
const scriptMap = await this.getScriptMap();
|
|
279
279
|
const f = filter ?? { withRecoverable: true, withUnrolled: false };
|
|
280
280
|
const allExtended = [];
|
|
281
|
-
//
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
if (
|
|
291
|
-
|
|
292
|
-
|
|
281
|
+
// Batch all scripts into a single indexer call
|
|
282
|
+
const allScripts = [...scriptMap.keys()];
|
|
283
|
+
const response = await this.indexerProvider.getVtxos({
|
|
284
|
+
scripts: allScripts,
|
|
285
|
+
});
|
|
286
|
+
for (const vtxo of response.vtxos) {
|
|
287
|
+
const vtxoScript = vtxo.script
|
|
288
|
+
? scriptMap.get(vtxo.script)
|
|
289
|
+
: undefined;
|
|
290
|
+
if (!vtxoScript)
|
|
291
|
+
continue;
|
|
292
|
+
if ((0, _1.isSpendable)(vtxo)) {
|
|
293
|
+
if (!f.withRecoverable &&
|
|
294
|
+
((0, _1.isRecoverable)(vtxo) || (0, _1.isExpired)(vtxo))) {
|
|
295
|
+
continue;
|
|
296
|
+
}
|
|
293
297
|
}
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
forfeitTapLeafScript: vtxoScript.forfeit(),
|
|
298
|
-
intentTapLeafScript: vtxoScript.forfeit(),
|
|
299
|
-
tapTree: vtxoScript.encode(),
|
|
300
|
-
});
|
|
298
|
+
else {
|
|
299
|
+
if (!f.withUnrolled || !vtxo.isUnrolled)
|
|
300
|
+
continue;
|
|
301
301
|
}
|
|
302
|
+
allExtended.push({
|
|
303
|
+
...vtxo,
|
|
304
|
+
forfeitTapLeafScript: vtxoScript.forfeit(),
|
|
305
|
+
intentTapLeafScript: vtxoScript.forfeit(),
|
|
306
|
+
tapTree: vtxoScript.encode(),
|
|
307
|
+
});
|
|
302
308
|
}
|
|
303
309
|
// Update cache with fresh data
|
|
304
310
|
await this.walletRepository.saveVtxos(address, allExtended);
|
|
@@ -310,7 +316,7 @@ class ReadonlyWallet {
|
|
|
310
316
|
const { boardingTxs, commitmentsToIgnore } = await this.getBoardingTxs();
|
|
311
317
|
const getTxCreatedAt = (txid) => this.indexerProvider
|
|
312
318
|
.getVtxos({ outpoints: [{ txid, vout: 0 }] })
|
|
313
|
-
.then((res) => res.vtxos[0]?.createdAt.getTime()
|
|
319
|
+
.then((res) => res.vtxos[0]?.createdAt.getTime());
|
|
314
320
|
return (0, transactionHistory_1.buildTransactionHistory)(response.vtxos, boardingTxs, commitmentsToIgnore, getTxCreatedAt);
|
|
315
321
|
}
|
|
316
322
|
async getBoardingTxs() {
|
|
@@ -1344,23 +1350,29 @@ class Wallet extends ReadonlyWallet {
|
|
|
1344
1350
|
async finalizePendingTxs(vtxos) {
|
|
1345
1351
|
const MAX_INPUTS_PER_INTENT = 20;
|
|
1346
1352
|
if (!vtxos || vtxos.length === 0) {
|
|
1347
|
-
//
|
|
1353
|
+
// Batch all scripts into a single indexer call
|
|
1348
1354
|
const scriptMap = await this.getScriptMap();
|
|
1349
1355
|
const allExtended = [];
|
|
1350
|
-
|
|
1351
|
-
|
|
1352
|
-
|
|
1353
|
-
|
|
1354
|
-
|
|
1355
|
-
|
|
1356
|
-
|
|
1357
|
-
|
|
1358
|
-
|
|
1359
|
-
|
|
1360
|
-
|
|
1361
|
-
|
|
1362
|
-
|
|
1356
|
+
const allScripts = [...scriptMap.keys()];
|
|
1357
|
+
const { vtxos: fetchedVtxos } = await this.indexerProvider.getVtxos({
|
|
1358
|
+
scripts: allScripts,
|
|
1359
|
+
});
|
|
1360
|
+
for (const vtxo of fetchedVtxos) {
|
|
1361
|
+
const vtxoScript = vtxo.script
|
|
1362
|
+
? scriptMap.get(vtxo.script)
|
|
1363
|
+
: undefined;
|
|
1364
|
+
if (!vtxoScript)
|
|
1365
|
+
continue;
|
|
1366
|
+
if (vtxo.virtualStatus.state === "swept" ||
|
|
1367
|
+
vtxo.virtualStatus.state === "settled") {
|
|
1368
|
+
continue;
|
|
1363
1369
|
}
|
|
1370
|
+
allExtended.push({
|
|
1371
|
+
...vtxo,
|
|
1372
|
+
forfeitTapLeafScript: vtxoScript.forfeit(),
|
|
1373
|
+
intentTapLeafScript: vtxoScript.forfeit(),
|
|
1374
|
+
tapTree: vtxoScript.encode(),
|
|
1375
|
+
});
|
|
1364
1376
|
}
|
|
1365
1377
|
if (allExtended.length === 0) {
|
|
1366
1378
|
return { finalized: [], pending: [] };
|
|
@@ -1,17 +1,16 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.ServiceWorkerTimeoutError = exports.MessageBusNotInitializedError = void 0;
|
|
3
|
+
exports.ServiceWorkerTimeoutError = exports.MessageBusNotInitializedError = exports.MESSAGE_BUS_NOT_INITIALIZED = void 0;
|
|
4
|
+
exports.MESSAGE_BUS_NOT_INITIALIZED = "MessageBus not initialized";
|
|
4
5
|
class MessageBusNotInitializedError extends Error {
|
|
5
6
|
constructor() {
|
|
6
|
-
super(
|
|
7
|
-
this.name = "MessageBusNotInitializedError";
|
|
7
|
+
super(exports.MESSAGE_BUS_NOT_INITIALIZED);
|
|
8
8
|
}
|
|
9
9
|
}
|
|
10
10
|
exports.MessageBusNotInitializedError = MessageBusNotInitializedError;
|
|
11
11
|
class ServiceWorkerTimeoutError extends Error {
|
|
12
12
|
constructor(detail) {
|
|
13
13
|
super(detail);
|
|
14
|
-
this.name = "ServiceWorkerTimeoutError";
|
|
15
14
|
}
|
|
16
15
|
}
|
|
17
16
|
exports.ServiceWorkerTimeoutError = ServiceWorkerTimeoutError;
|
|
@@ -152,8 +152,12 @@ class MessageBus {
|
|
|
152
152
|
identity,
|
|
153
153
|
arkServerUrl: config.arkServer.url,
|
|
154
154
|
arkServerPublicKey: config.arkServer.publicKey,
|
|
155
|
+
indexerUrl: config.indexerUrl,
|
|
156
|
+
esploraUrl: config.esploraUrl,
|
|
155
157
|
storage,
|
|
156
158
|
delegatorProvider,
|
|
159
|
+
settlementConfig: config.settlementConfig,
|
|
160
|
+
watcherConfig: config.watcherConfig,
|
|
157
161
|
});
|
|
158
162
|
return { wallet, arkProvider, readonlyWallet: wallet };
|
|
159
163
|
}
|
|
@@ -163,8 +167,11 @@ class MessageBus {
|
|
|
163
167
|
identity,
|
|
164
168
|
arkServerUrl: config.arkServer.url,
|
|
165
169
|
arkServerPublicKey: config.arkServer.publicKey,
|
|
170
|
+
indexerUrl: config.indexerUrl,
|
|
171
|
+
esploraUrl: config.esploraUrl,
|
|
166
172
|
storage,
|
|
167
173
|
delegatorProvider,
|
|
174
|
+
watcherConfig: config.watcherConfig,
|
|
168
175
|
});
|
|
169
176
|
return { readonlyWallet, arkProvider };
|
|
170
177
|
}
|
|
@@ -71,9 +71,10 @@ export class ContractManager {
|
|
|
71
71
|
}
|
|
72
72
|
// Load persisted contracts
|
|
73
73
|
const contracts = await this.config.contractRepository.getContracts();
|
|
74
|
-
// fetch
|
|
74
|
+
// fetch all VTXOs (including spent/swept) for all contracts,
|
|
75
|
+
// so the repository has full history for transaction history and balance
|
|
75
76
|
// TODO: what if the user has 1k contracts?
|
|
76
|
-
await this.
|
|
77
|
+
await this.fetchContractVxosFromIndexer(contracts, true);
|
|
77
78
|
// add all contracts to the watcher
|
|
78
79
|
const now = Date.now();
|
|
79
80
|
for (const contract of contracts) {
|
|
@@ -136,8 +137,8 @@ export class ContractManager {
|
|
|
136
137
|
};
|
|
137
138
|
// Persist
|
|
138
139
|
await this.config.contractRepository.saveContract(contract);
|
|
139
|
-
//
|
|
140
|
-
await this.
|
|
140
|
+
// fetch all VTXOs (including spent/swept) for this contract
|
|
141
|
+
await this.fetchContractVxosFromIndexer([contract], true);
|
|
141
142
|
// Add to watcher
|
|
142
143
|
await this.watcher.addContract(contract);
|
|
143
144
|
return contract;
|
|
@@ -302,6 +303,14 @@ export class ContractManager {
|
|
|
302
303
|
this.eventCallbacks.delete(callback);
|
|
303
304
|
};
|
|
304
305
|
}
|
|
306
|
+
/**
|
|
307
|
+
* Force a full VTXO refresh from the indexer for all contracts.
|
|
308
|
+
* Populates the wallet repository with complete VTXO history.
|
|
309
|
+
*/
|
|
310
|
+
async refreshVtxos() {
|
|
311
|
+
const contracts = await this.config.contractRepository.getContracts();
|
|
312
|
+
await this.fetchContractVxosFromIndexer(contracts, true);
|
|
313
|
+
}
|
|
305
314
|
/**
|
|
306
315
|
* Check if currently watching.
|
|
307
316
|
*/
|
|
@@ -331,11 +340,13 @@ export class ContractManager {
|
|
|
331
340
|
case "vtxo_spent":
|
|
332
341
|
await this.fetchContractVxosFromIndexer([event.contract], true);
|
|
333
342
|
break;
|
|
334
|
-
case "connection_reset":
|
|
335
|
-
// Refetch all VTXOs for all active
|
|
343
|
+
case "connection_reset": {
|
|
344
|
+
// Refetch all VTXOs (including spent/swept) for all active
|
|
345
|
+
// contracts so the repo stays consistent with bootstrap state
|
|
336
346
|
const activeWatchedContracts = this.watcher.getActiveContracts();
|
|
337
|
-
await this.fetchContractVxosFromIndexer(activeWatchedContracts,
|
|
347
|
+
await this.fetchContractVxosFromIndexer(activeWatchedContracts, true);
|
|
338
348
|
break;
|
|
349
|
+
}
|
|
339
350
|
case "contract_expired":
|
|
340
351
|
// just update DB
|
|
341
352
|
await this.config.contractRepository.saveContract(event.contract);
|
|
@@ -362,11 +373,48 @@ export class ContractManager {
|
|
|
362
373
|
return result;
|
|
363
374
|
}
|
|
364
375
|
async fetchContractVtxosBulk(contracts, includeSpent) {
|
|
365
|
-
|
|
366
|
-
|
|
376
|
+
if (contracts.length === 0) {
|
|
377
|
+
return new Map();
|
|
378
|
+
}
|
|
379
|
+
// For a single contract, use the paginated path directly.
|
|
380
|
+
if (contracts.length === 1) {
|
|
381
|
+
const contract = contracts[0];
|
|
367
382
|
const vtxos = await this.fetchContractVtxosPaginated(contract, includeSpent);
|
|
368
|
-
|
|
369
|
-
}
|
|
383
|
+
return new Map([[contract.script, vtxos]]);
|
|
384
|
+
}
|
|
385
|
+
// For multiple contracts, batch all scripts into a single indexer call
|
|
386
|
+
// per page to minimise round-trips. Results are keyed by script so we
|
|
387
|
+
// can distribute them back to the correct contract afterwards.
|
|
388
|
+
const scriptToContract = new Map(contracts.map((c) => [c.script, c]));
|
|
389
|
+
const result = new Map(contracts.map((c) => [c.script, []]));
|
|
390
|
+
const scripts = contracts.map((c) => c.script);
|
|
391
|
+
const pageSize = 100;
|
|
392
|
+
const opts = includeSpent ? {} : { spendableOnly: true };
|
|
393
|
+
let pageIndex = 0;
|
|
394
|
+
let hasMore = true;
|
|
395
|
+
while (hasMore) {
|
|
396
|
+
const { vtxos, page } = await this.config.indexerProvider.getVtxos({
|
|
397
|
+
scripts,
|
|
398
|
+
...opts,
|
|
399
|
+
pageIndex,
|
|
400
|
+
pageSize,
|
|
401
|
+
});
|
|
402
|
+
for (const vtxo of vtxos) {
|
|
403
|
+
// Match the VTXO back to its contract via the script field
|
|
404
|
+
// populated by the indexer.
|
|
405
|
+
if (!vtxo.script)
|
|
406
|
+
continue;
|
|
407
|
+
const contract = scriptToContract.get(vtxo.script);
|
|
408
|
+
if (!contract)
|
|
409
|
+
continue;
|
|
410
|
+
result.get(contract.script).push({
|
|
411
|
+
...extendVtxoFromContract(vtxo, contract),
|
|
412
|
+
contractScript: contract.script,
|
|
413
|
+
});
|
|
414
|
+
}
|
|
415
|
+
hasMore = page ? vtxos.length === pageSize : false;
|
|
416
|
+
pageIndex++;
|
|
417
|
+
}
|
|
370
418
|
return result;
|
|
371
419
|
}
|
|
372
420
|
async fetchContractVtxosPaginated(contract, includeSpent) {
|
|
@@ -410,8 +410,27 @@ export class ContractWatcher {
|
|
|
410
410
|
}
|
|
411
411
|
return;
|
|
412
412
|
}
|
|
413
|
-
|
|
414
|
-
|
|
413
|
+
try {
|
|
414
|
+
this.subscriptionId =
|
|
415
|
+
await this.config.indexerProvider.subscribeForScripts(scriptsToWatch, this.subscriptionId);
|
|
416
|
+
}
|
|
417
|
+
catch (error) {
|
|
418
|
+
// If we sent a stale subscription ID that the server no longer
|
|
419
|
+
// recognises, clear it and retry to create a fresh subscription.
|
|
420
|
+
// The server currently returns HTTP 500 with a JSON body whose
|
|
421
|
+
// message field looks like "subscription <uuid> not found".
|
|
422
|
+
// All other errors (network failures, parse errors, etc.) are rethrown.
|
|
423
|
+
const isStale = error instanceof Error &&
|
|
424
|
+
/subscription\s+\S+\s+not\s+found/i.test(error.message);
|
|
425
|
+
if (this.subscriptionId && isStale) {
|
|
426
|
+
this.subscriptionId = undefined;
|
|
427
|
+
this.subscriptionId =
|
|
428
|
+
await this.config.indexerProvider.subscribeForScripts(scriptsToWatch);
|
|
429
|
+
}
|
|
430
|
+
else {
|
|
431
|
+
throw error;
|
|
432
|
+
}
|
|
433
|
+
}
|
|
415
434
|
}
|
|
416
435
|
/**
|
|
417
436
|
* Main listening loop for subscription events.
|
package/dist/esm/index.js
CHANGED
|
@@ -40,7 +40,7 @@ export * as asset from './extension/asset/index.js';
|
|
|
40
40
|
import { ContractManager, ContractWatcher, contractHandlers, DefaultContractHandler, DelegateContractHandler, VHTLCContractHandler, encodeArkContract, decodeArkContract, contractFromArkContract, contractFromArkContractWithAddress, isArkContract, } from './contracts/index.js';
|
|
41
41
|
import { closeDatabase, openDatabase } from './repositories/indexedDB/manager.js';
|
|
42
42
|
import { WalletMessageHandler, WalletNotInitializedError, ReadonlyWalletError, DelegatorNotConfiguredError, } from './wallet/serviceWorker/wallet-message-handler.js';
|
|
43
|
-
import { MessageBusNotInitializedError, ServiceWorkerTimeoutError, } from './worker/errors.js';
|
|
43
|
+
import { MESSAGE_BUS_NOT_INITIALIZED, MessageBusNotInitializedError, ServiceWorkerTimeoutError, } from './worker/errors.js';
|
|
44
44
|
export {
|
|
45
45
|
// Wallets
|
|
46
46
|
Wallet, ReadonlyWallet, SingleKey, ReadonlySingleKey, SeedIdentity, MnemonicIdentity, ReadonlyDescriptorIdentity, OnchainWallet, Ramps, VtxoManager, DelegatorManagerImpl, RestDelegatorProvider,
|
|
@@ -51,7 +51,7 @@ ArkAddress, DefaultVtxo, DelegateVtxo, VtxoScript, VHTLC,
|
|
|
51
51
|
// Enums
|
|
52
52
|
TxType, IndexerTxType, ChainTxType, SettlementEventType,
|
|
53
53
|
// Service Worker
|
|
54
|
-
setupServiceWorker, MessageBus, WalletMessageHandler, WalletNotInitializedError, ReadonlyWalletError, DelegatorNotConfiguredError, MessageBusNotInitializedError, ServiceWorkerTimeoutError, ServiceWorkerWallet, ServiceWorkerReadonlyWallet,
|
|
54
|
+
setupServiceWorker, MessageBus, WalletMessageHandler, WalletNotInitializedError, ReadonlyWalletError, DelegatorNotConfiguredError, MESSAGE_BUS_NOT_INITIALIZED, MessageBusNotInitializedError, ServiceWorkerTimeoutError, ServiceWorkerWallet, ServiceWorkerReadonlyWallet,
|
|
55
55
|
// Tapscript
|
|
56
56
|
decodeTapscript, MultisigTapscript, CSVMultisigTapscript, ConditionCSVMultisigTapscript, ConditionMultisigTapscript, CLTVMultisigTapscript, TapTreeCoder,
|
|
57
57
|
// Ark PSBT fields
|
|
@@ -402,6 +402,7 @@ function convertVtxo(vtxo) {
|
|
|
402
402
|
createdAt: new Date(Number(vtxo.createdAt) * 1000),
|
|
403
403
|
isUnrolled: vtxo.isUnrolled,
|
|
404
404
|
isSpent: vtxo.isSpent,
|
|
405
|
+
script: vtxo.script,
|
|
405
406
|
assets: vtxo.assets?.map((a) => ({
|
|
406
407
|
assetId: a.assetId,
|
|
407
408
|
amount: Number(a.amount),
|
|
@@ -8,6 +8,7 @@ import { P2A } from './anchor.js';
|
|
|
8
8
|
import { setArkPsbtField, VtxoTaprootTree } from './unknownFields.js';
|
|
9
9
|
import { Transaction } from './transaction.js';
|
|
10
10
|
import { ArkAddress } from '../script/address.js';
|
|
11
|
+
import { Extension } from '../extension/index.js';
|
|
11
12
|
/**
|
|
12
13
|
* Builds an offchain transaction with checkpoint transactions.
|
|
13
14
|
*
|
|
@@ -21,16 +22,26 @@ import { ArkAddress } from '../script/address.js';
|
|
|
21
22
|
* @returns Object containing the virtual transaction and checkpoint transactions
|
|
22
23
|
*/
|
|
23
24
|
export function buildOffchainTx(inputs, outputs, serverUnrollScript) {
|
|
24
|
-
|
|
25
|
+
// TODO: use arkd /info
|
|
26
|
+
const MAX_OP_RETURN = 2;
|
|
27
|
+
let countOpReturn = 0;
|
|
28
|
+
let hasExtensionOutput = false;
|
|
25
29
|
for (const [index, output] of outputs.entries()) {
|
|
26
30
|
if (!output.script)
|
|
27
31
|
throw new Error(`missing output script ${index}`);
|
|
28
|
-
const
|
|
29
|
-
|
|
32
|
+
const isExtension = Extension.isExtension(output.script);
|
|
33
|
+
const isOpReturn = isExtension || Script.decode(output.script)[0] === "RETURN";
|
|
34
|
+
if (isOpReturn) {
|
|
35
|
+
countOpReturn++;
|
|
36
|
+
}
|
|
37
|
+
if (!isExtension)
|
|
30
38
|
continue;
|
|
31
|
-
if (
|
|
32
|
-
throw new Error("multiple
|
|
33
|
-
|
|
39
|
+
if (hasExtensionOutput)
|
|
40
|
+
throw new Error("multiple extension outputs");
|
|
41
|
+
hasExtensionOutput = true;
|
|
42
|
+
}
|
|
43
|
+
if (countOpReturn > MAX_OP_RETURN) {
|
|
44
|
+
throw new Error(`too many OP_RETURN outputs: ${countOpReturn} > ${MAX_OP_RETURN}`);
|
|
34
45
|
}
|
|
35
46
|
const checkpoints = inputs.map((input) => buildCheckpointTx(input, serverUnrollScript));
|
|
36
47
|
const arkTx = buildVirtualTx(checkpoints.map((c) => c.input), outputs);
|
|
@@ -115,7 +115,8 @@ export async function buildTransactionHistory(vtxos, allBoardingTxs, commitments
|
|
|
115
115
|
txAmount = spentAmount;
|
|
116
116
|
// TODO: fetch the vtxo with /v1/indexer/vtxos?outpoints=<vtxo.arkTxid:0> to know when the tx was made
|
|
117
117
|
txTime = getTxCreatedAt
|
|
118
|
-
? await getTxCreatedAt(vtxo.arkTxId)
|
|
118
|
+
? ((await getTxCreatedAt(vtxo.arkTxId)) ??
|
|
119
|
+
vtxo.createdAt.getTime() + 1)
|
|
119
120
|
: vtxo.createdAt.getTime() + 1;
|
|
120
121
|
}
|
|
121
122
|
const assets = subtractAssets(allSpent, changes);
|