@arkade-os/sdk 0.4.8 → 0.4.9
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/providers/expoIndexer.js +1 -0
- package/dist/cjs/providers/indexer.js +1 -0
- 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 +22 -0
- package/dist/cjs/wallet/vtxo-manager.js +81 -50
- package/dist/cjs/wallet/wallet.js +46 -34
- 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/providers/expoIndexer.js +1 -0
- package/dist/esm/providers/indexer.js +1 -0
- 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 +22 -0
- package/dist/esm/wallet/vtxo-manager.js +81 -50
- package/dist/esm/wallet/wallet.js +46 -34
- package/dist/esm/worker/messageBus.js +7 -0
- package/dist/types/contracts/contractManager.d.ts +10 -0
- 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/messageBus.d.ts +6 -0
- package/package.json +1 -1
|
@@ -74,9 +74,10 @@ class ContractManager {
|
|
|
74
74
|
}
|
|
75
75
|
// Load persisted contracts
|
|
76
76
|
const contracts = await this.config.contractRepository.getContracts();
|
|
77
|
-
// fetch
|
|
77
|
+
// fetch all VTXOs (including spent/swept) for all contracts,
|
|
78
|
+
// so the repository has full history for transaction history and balance
|
|
78
79
|
// TODO: what if the user has 1k contracts?
|
|
79
|
-
await this.
|
|
80
|
+
await this.fetchContractVxosFromIndexer(contracts, true);
|
|
80
81
|
// add all contracts to the watcher
|
|
81
82
|
const now = Date.now();
|
|
82
83
|
for (const contract of contracts) {
|
|
@@ -139,8 +140,8 @@ class ContractManager {
|
|
|
139
140
|
};
|
|
140
141
|
// Persist
|
|
141
142
|
await this.config.contractRepository.saveContract(contract);
|
|
142
|
-
//
|
|
143
|
-
await this.
|
|
143
|
+
// fetch all VTXOs (including spent/swept) for this contract
|
|
144
|
+
await this.fetchContractVxosFromIndexer([contract], true);
|
|
144
145
|
// Add to watcher
|
|
145
146
|
await this.watcher.addContract(contract);
|
|
146
147
|
return contract;
|
|
@@ -305,6 +306,14 @@ class ContractManager {
|
|
|
305
306
|
this.eventCallbacks.delete(callback);
|
|
306
307
|
};
|
|
307
308
|
}
|
|
309
|
+
/**
|
|
310
|
+
* Force a full VTXO refresh from the indexer for all contracts.
|
|
311
|
+
* Populates the wallet repository with complete VTXO history.
|
|
312
|
+
*/
|
|
313
|
+
async refreshVtxos() {
|
|
314
|
+
const contracts = await this.config.contractRepository.getContracts();
|
|
315
|
+
await this.fetchContractVxosFromIndexer(contracts, true);
|
|
316
|
+
}
|
|
308
317
|
/**
|
|
309
318
|
* Check if currently watching.
|
|
310
319
|
*/
|
|
@@ -334,11 +343,13 @@ class ContractManager {
|
|
|
334
343
|
case "vtxo_spent":
|
|
335
344
|
await this.fetchContractVxosFromIndexer([event.contract], true);
|
|
336
345
|
break;
|
|
337
|
-
case "connection_reset":
|
|
338
|
-
// Refetch all VTXOs for all active
|
|
346
|
+
case "connection_reset": {
|
|
347
|
+
// Refetch all VTXOs (including spent/swept) for all active
|
|
348
|
+
// contracts so the repo stays consistent with bootstrap state
|
|
339
349
|
const activeWatchedContracts = this.watcher.getActiveContracts();
|
|
340
|
-
await this.fetchContractVxosFromIndexer(activeWatchedContracts,
|
|
350
|
+
await this.fetchContractVxosFromIndexer(activeWatchedContracts, true);
|
|
341
351
|
break;
|
|
352
|
+
}
|
|
342
353
|
case "contract_expired":
|
|
343
354
|
// just update DB
|
|
344
355
|
await this.config.contractRepository.saveContract(event.contract);
|
|
@@ -365,11 +376,48 @@ class ContractManager {
|
|
|
365
376
|
return result;
|
|
366
377
|
}
|
|
367
378
|
async fetchContractVtxosBulk(contracts, includeSpent) {
|
|
368
|
-
|
|
369
|
-
|
|
379
|
+
if (contracts.length === 0) {
|
|
380
|
+
return new Map();
|
|
381
|
+
}
|
|
382
|
+
// For a single contract, use the paginated path directly.
|
|
383
|
+
if (contracts.length === 1) {
|
|
384
|
+
const contract = contracts[0];
|
|
370
385
|
const vtxos = await this.fetchContractVtxosPaginated(contract, includeSpent);
|
|
371
|
-
|
|
372
|
-
}
|
|
386
|
+
return new Map([[contract.script, vtxos]]);
|
|
387
|
+
}
|
|
388
|
+
// For multiple contracts, batch all scripts into a single indexer call
|
|
389
|
+
// per page to minimise round-trips. Results are keyed by script so we
|
|
390
|
+
// can distribute them back to the correct contract afterwards.
|
|
391
|
+
const scriptToContract = new Map(contracts.map((c) => [c.script, c]));
|
|
392
|
+
const result = new Map(contracts.map((c) => [c.script, []]));
|
|
393
|
+
const scripts = contracts.map((c) => c.script);
|
|
394
|
+
const pageSize = 100;
|
|
395
|
+
const opts = includeSpent ? {} : { spendableOnly: true };
|
|
396
|
+
let pageIndex = 0;
|
|
397
|
+
let hasMore = true;
|
|
398
|
+
while (hasMore) {
|
|
399
|
+
const { vtxos, page } = await this.config.indexerProvider.getVtxos({
|
|
400
|
+
scripts,
|
|
401
|
+
...opts,
|
|
402
|
+
pageIndex,
|
|
403
|
+
pageSize,
|
|
404
|
+
});
|
|
405
|
+
for (const vtxo of vtxos) {
|
|
406
|
+
// Match the VTXO back to its contract via the script field
|
|
407
|
+
// populated by the indexer.
|
|
408
|
+
if (!vtxo.script)
|
|
409
|
+
continue;
|
|
410
|
+
const contract = scriptToContract.get(vtxo.script);
|
|
411
|
+
if (!contract)
|
|
412
|
+
continue;
|
|
413
|
+
result.get(contract.script).push({
|
|
414
|
+
...(0, utils_1.extendVtxoFromContract)(vtxo, contract),
|
|
415
|
+
contractScript: contract.script,
|
|
416
|
+
});
|
|
417
|
+
}
|
|
418
|
+
hasMore = page ? vtxos.length === pageSize : false;
|
|
419
|
+
pageIndex++;
|
|
420
|
+
}
|
|
373
421
|
return result;
|
|
374
422
|
}
|
|
375
423
|
async fetchContractVtxosPaginated(contract, includeSpent) {
|
|
@@ -413,8 +413,27 @@ class ContractWatcher {
|
|
|
413
413
|
}
|
|
414
414
|
return;
|
|
415
415
|
}
|
|
416
|
-
|
|
417
|
-
|
|
416
|
+
try {
|
|
417
|
+
this.subscriptionId =
|
|
418
|
+
await this.config.indexerProvider.subscribeForScripts(scriptsToWatch, this.subscriptionId);
|
|
419
|
+
}
|
|
420
|
+
catch (error) {
|
|
421
|
+
// If we sent a stale subscription ID that the server no longer
|
|
422
|
+
// recognises, clear it and retry to create a fresh subscription.
|
|
423
|
+
// The server currently returns HTTP 500 with a JSON body whose
|
|
424
|
+
// message field looks like "subscription <uuid> not found".
|
|
425
|
+
// All other errors (network failures, parse errors, etc.) are rethrown.
|
|
426
|
+
const isStale = error instanceof Error &&
|
|
427
|
+
/subscription\s+\S+\s+not\s+found/i.test(error.message);
|
|
428
|
+
if (this.subscriptionId && isStale) {
|
|
429
|
+
this.subscriptionId = undefined;
|
|
430
|
+
this.subscriptionId =
|
|
431
|
+
await this.config.indexerProvider.subscribeForScripts(scriptsToWatch);
|
|
432
|
+
}
|
|
433
|
+
else {
|
|
434
|
+
throw error;
|
|
435
|
+
}
|
|
436
|
+
}
|
|
418
437
|
}
|
|
419
438
|
/**
|
|
420
439
|
* Main listening loop for subscription events.
|
|
@@ -406,6 +406,7 @@ function convertVtxo(vtxo) {
|
|
|
406
406
|
createdAt: new Date(Number(vtxo.createdAt) * 1000),
|
|
407
407
|
isUnrolled: vtxo.isUnrolled,
|
|
408
408
|
isSpent: vtxo.isSpent,
|
|
409
|
+
script: vtxo.script,
|
|
409
410
|
assets: vtxo.assets?.map((a) => ({
|
|
410
411
|
assetId: a.assetId,
|
|
411
412
|
amount: Number(a.amount),
|
|
@@ -118,7 +118,8 @@ async function buildTransactionHistory(vtxos, allBoardingTxs, commitmentsToIgnor
|
|
|
118
118
|
txAmount = spentAmount;
|
|
119
119
|
// TODO: fetch the vtxo with /v1/indexer/vtxos?outpoints=<vtxo.arkTxid:0> to know when the tx was made
|
|
120
120
|
txTime = getTxCreatedAt
|
|
121
|
-
? await getTxCreatedAt(vtxo.arkTxId)
|
|
121
|
+
? ((await getTxCreatedAt(vtxo.arkTxId)) ??
|
|
122
|
+
vtxo.createdAt.getTime() + 1)
|
|
122
123
|
: vtxo.createdAt.getTime() + 1;
|
|
123
124
|
}
|
|
124
125
|
const assets = subtractAssets(allSpent, changes);
|
|
@@ -4,6 +4,7 @@ exports.WalletMessageHandler = exports.DEFAULT_MESSAGE_TAG = exports.DelegatorNo
|
|
|
4
4
|
const indexer_1 = require("../../providers/indexer");
|
|
5
5
|
const index_1 = require("../index");
|
|
6
6
|
const utils_1 = require("../utils");
|
|
7
|
+
const transactionHistory_1 = require("../../utils/transactionHistory");
|
|
7
8
|
class WalletNotInitializedError extends Error {
|
|
8
9
|
constructor() {
|
|
9
10
|
super("Wallet handler not initialized");
|
|
@@ -175,7 +176,8 @@ class WalletMessageHandler {
|
|
|
175
176
|
});
|
|
176
177
|
}
|
|
177
178
|
case "GET_TRANSACTION_HISTORY": {
|
|
178
|
-
const
|
|
179
|
+
const allVtxos = await this.getVtxosFromRepo();
|
|
180
|
+
const transactions = (await this.buildTransactionHistoryFromCache(allVtxos)) ?? [];
|
|
179
181
|
return this.tagged({
|
|
180
182
|
id,
|
|
181
183
|
type: "TRANSACTION_HISTORY",
|
|
@@ -202,7 +204,7 @@ class WalletMessageHandler {
|
|
|
202
204
|
});
|
|
203
205
|
}
|
|
204
206
|
case "RELOAD_WALLET": {
|
|
205
|
-
await this.
|
|
207
|
+
await this.reloadWallet();
|
|
206
208
|
return this.tagged({
|
|
207
209
|
id,
|
|
208
210
|
type: "RELOAD_SUCCESS",
|
|
@@ -288,6 +290,14 @@ class WalletMessageHandler {
|
|
|
288
290
|
payload: { isWatching },
|
|
289
291
|
});
|
|
290
292
|
}
|
|
293
|
+
case "REFRESH_VTXOS": {
|
|
294
|
+
const manager = await this.readonlyWallet.getContractManager();
|
|
295
|
+
await manager.refreshVtxos();
|
|
296
|
+
return this.tagged({
|
|
297
|
+
id,
|
|
298
|
+
type: "REFRESH_VTXOS_SUCCESS",
|
|
299
|
+
});
|
|
300
|
+
}
|
|
291
301
|
case "SEND": {
|
|
292
302
|
const { recipients } = message.payload;
|
|
293
303
|
const txid = await this.wallet.send(...recipients);
|
|
@@ -444,10 +454,9 @@ class WalletMessageHandler {
|
|
|
444
454
|
await this.onWalletInitialized();
|
|
445
455
|
}
|
|
446
456
|
async handleGetBalance() {
|
|
447
|
-
const [boardingUtxos,
|
|
457
|
+
const [boardingUtxos, allVtxos] = await Promise.all([
|
|
448
458
|
this.getAllBoardingUtxos(),
|
|
449
|
-
this.
|
|
450
|
-
this.getSweptVtxos(),
|
|
459
|
+
this.getVtxosFromRepo(),
|
|
451
460
|
]);
|
|
452
461
|
// boarding
|
|
453
462
|
let confirmed = 0;
|
|
@@ -460,7 +469,9 @@ class WalletMessageHandler {
|
|
|
460
469
|
unconfirmed += utxo.value;
|
|
461
470
|
}
|
|
462
471
|
}
|
|
463
|
-
// offchain
|
|
472
|
+
// offchain — split spendable vs swept from single repo read
|
|
473
|
+
const spendableVtxos = allVtxos.filter(index_1.isSpendable);
|
|
474
|
+
const sweptVtxos = allVtxos.filter((vtxo) => vtxo.virtualStatus.state === "swept");
|
|
464
475
|
let settled = 0;
|
|
465
476
|
let preconfirmed = 0;
|
|
466
477
|
let recoverable = 0;
|
|
@@ -510,23 +521,12 @@ class WalletMessageHandler {
|
|
|
510
521
|
return this.readonlyWallet.getBoardingUtxos();
|
|
511
522
|
}
|
|
512
523
|
/**
|
|
513
|
-
* Get spendable vtxos
|
|
524
|
+
* Get spendable vtxos from the repository
|
|
514
525
|
*/
|
|
515
526
|
async getSpendableVtxos() {
|
|
516
|
-
|
|
517
|
-
return [];
|
|
518
|
-
const vtxos = await this.readonlyWallet.getVtxos();
|
|
527
|
+
const vtxos = await this.getVtxosFromRepo();
|
|
519
528
|
return vtxos.filter(index_1.isSpendable);
|
|
520
529
|
}
|
|
521
|
-
/**
|
|
522
|
-
* Get swept vtxos for the current wallet address
|
|
523
|
-
*/
|
|
524
|
-
async getSweptVtxos() {
|
|
525
|
-
if (!this.readonlyWallet)
|
|
526
|
-
return [];
|
|
527
|
-
const vtxos = await this.readonlyWallet.getVtxos();
|
|
528
|
-
return vtxos.filter((vtxo) => vtxo.virtualStatus.state === "swept");
|
|
529
|
-
}
|
|
530
530
|
async onWalletInitialized() {
|
|
531
531
|
if (!this.readonlyWallet ||
|
|
532
532
|
!this.arkProvider ||
|
|
@@ -534,10 +534,11 @@ class WalletMessageHandler {
|
|
|
534
534
|
!this.walletRepository) {
|
|
535
535
|
return;
|
|
536
536
|
}
|
|
537
|
-
//
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
537
|
+
// Initialize contract manager FIRST — this populates the repository
|
|
538
|
+
// with full VTXO history for all contracts (one indexer call per contract)
|
|
539
|
+
await this.ensureContractEventBroadcasting();
|
|
540
|
+
// Read VTXOs from repository (now populated by contract manager)
|
|
541
|
+
const vtxos = await this.getVtxosFromRepo();
|
|
541
542
|
if (this.wallet) {
|
|
542
543
|
try {
|
|
543
544
|
// recover pending transactions if possible
|
|
@@ -549,15 +550,13 @@ class WalletMessageHandler {
|
|
|
549
550
|
console.error("Error recovering pending transactions:", error);
|
|
550
551
|
}
|
|
551
552
|
}
|
|
552
|
-
// Get wallet address and save vtxos using unified repository
|
|
553
|
-
const address = await this.readonlyWallet.getAddress();
|
|
554
|
-
await this.walletRepository.saveVtxos(address, vtxos);
|
|
555
553
|
// Fetch boarding utxos and save using unified repository
|
|
556
554
|
const boardingAddress = await this.readonlyWallet.getBoardingAddress();
|
|
557
555
|
const coins = await this.readonlyWallet.onchainProvider.getCoins(boardingAddress);
|
|
558
556
|
await this.walletRepository.saveUtxos(boardingAddress, coins.map((utxo) => (0, utils_1.extendCoin)(this.readonlyWallet, utxo)));
|
|
559
|
-
//
|
|
560
|
-
const
|
|
557
|
+
// Build transaction history from cached VTXOs (no indexer call)
|
|
558
|
+
const address = await this.readonlyWallet.getAddress();
|
|
559
|
+
const txs = await this.buildTransactionHistoryFromCache(vtxos);
|
|
561
560
|
if (txs)
|
|
562
561
|
await this.walletRepository.saveTransactions(address, txs);
|
|
563
562
|
// unsubscribe previous subscription if any
|
|
@@ -602,7 +601,6 @@ class WalletMessageHandler {
|
|
|
602
601
|
}));
|
|
603
602
|
}
|
|
604
603
|
});
|
|
605
|
-
await this.ensureContractEventBroadcasting();
|
|
606
604
|
// Eagerly start the VtxoManager so its background tasks (auto-renewal,
|
|
607
605
|
// boarding UTXO polling/sweep) run inside the service worker without
|
|
608
606
|
// waiting for a client to send a vtxo-manager message first.
|
|
@@ -615,6 +613,17 @@ class WalletMessageHandler {
|
|
|
615
613
|
}
|
|
616
614
|
}
|
|
617
615
|
}
|
|
616
|
+
/**
|
|
617
|
+
* Force a full VTXO refresh from the indexer, then re-run bootstrap.
|
|
618
|
+
* Used by RELOAD_WALLET to ensure fresh data.
|
|
619
|
+
*/
|
|
620
|
+
async reloadWallet() {
|
|
621
|
+
if (!this.readonlyWallet)
|
|
622
|
+
return;
|
|
623
|
+
const manager = await this.readonlyWallet.getContractManager();
|
|
624
|
+
await manager.refreshVtxos();
|
|
625
|
+
await this.onWalletInitialized();
|
|
626
|
+
}
|
|
618
627
|
async handleSettle(message) {
|
|
619
628
|
const wallet = this.requireWallet();
|
|
620
629
|
const txid = await wallet.settle(message.payload.params, (e) => {
|
|
@@ -738,6 +747,77 @@ class WalletMessageHandler {
|
|
|
738
747
|
this.arkProvider = undefined;
|
|
739
748
|
this.indexerProvider = undefined;
|
|
740
749
|
}
|
|
750
|
+
/**
|
|
751
|
+
* Read all VTXOs from the repository, aggregated across all contract
|
|
752
|
+
* addresses and the wallet's primary address, with deduplication.
|
|
753
|
+
*/
|
|
754
|
+
async getVtxosFromRepo() {
|
|
755
|
+
if (!this.walletRepository || !this.readonlyWallet)
|
|
756
|
+
return [];
|
|
757
|
+
const seen = new Set();
|
|
758
|
+
const allVtxos = [];
|
|
759
|
+
const addVtxos = (vtxos) => {
|
|
760
|
+
for (const vtxo of vtxos) {
|
|
761
|
+
const key = `${vtxo.txid}:${vtxo.vout}`;
|
|
762
|
+
if (!seen.has(key)) {
|
|
763
|
+
seen.add(key);
|
|
764
|
+
allVtxos.push(vtxo);
|
|
765
|
+
}
|
|
766
|
+
}
|
|
767
|
+
};
|
|
768
|
+
// Aggregate VTXOs from all contract addresses
|
|
769
|
+
const manager = await this.readonlyWallet.getContractManager();
|
|
770
|
+
const contracts = await manager.getContracts();
|
|
771
|
+
for (const contract of contracts) {
|
|
772
|
+
const vtxos = await this.walletRepository.getVtxos(contract.address);
|
|
773
|
+
addVtxos(vtxos);
|
|
774
|
+
}
|
|
775
|
+
// Also check the wallet's primary address
|
|
776
|
+
const walletAddress = await this.readonlyWallet.getAddress();
|
|
777
|
+
const walletVtxos = await this.walletRepository.getVtxos(walletAddress);
|
|
778
|
+
addVtxos(walletVtxos);
|
|
779
|
+
return allVtxos;
|
|
780
|
+
}
|
|
781
|
+
/**
|
|
782
|
+
* Build transaction history from cached VTXOs without hitting the indexer.
|
|
783
|
+
* Falls back to indexer only for uncached transaction timestamps.
|
|
784
|
+
*/
|
|
785
|
+
async buildTransactionHistoryFromCache(vtxos) {
|
|
786
|
+
if (!this.readonlyWallet)
|
|
787
|
+
return null;
|
|
788
|
+
const { boardingTxs, commitmentsToIgnore } = await this.readonlyWallet.getBoardingTxs();
|
|
789
|
+
// Build a lookup for cached VTXO timestamps, keyed by txid.
|
|
790
|
+
// Multiple VTXOs can share a txid (different vouts) — we keep the
|
|
791
|
+
// earliest createdAt so the history ordering is stable.
|
|
792
|
+
const vtxoCreatedAt = new Map();
|
|
793
|
+
for (const vtxo of vtxos) {
|
|
794
|
+
const existing = vtxoCreatedAt.get(vtxo.txid);
|
|
795
|
+
const ts = vtxo.createdAt.getTime();
|
|
796
|
+
if (existing === undefined || ts < existing) {
|
|
797
|
+
vtxoCreatedAt.set(vtxo.txid, ts);
|
|
798
|
+
}
|
|
799
|
+
}
|
|
800
|
+
// getTxCreatedAt resolves the creation timestamp of a transaction.
|
|
801
|
+
// buildTransactionHistory calls this for spent-offchain VTXOs with
|
|
802
|
+
// no change outputs to determine the time of the sending tx.
|
|
803
|
+
// Returns undefined on miss so buildTransactionHistory uses its
|
|
804
|
+
// own fallback (vtxo.createdAt + 1) rather than epoch 0.
|
|
805
|
+
// The vout:0 lookup in the indexer fallback mirrors the pre-existing
|
|
806
|
+
// convention in ReadonlyWallet.getTransactionHistory().
|
|
807
|
+
const getTxCreatedAt = async (txid) => {
|
|
808
|
+
const cached = vtxoCreatedAt.get(txid);
|
|
809
|
+
if (cached !== undefined)
|
|
810
|
+
return cached;
|
|
811
|
+
if (this.indexerProvider) {
|
|
812
|
+
const res = await this.indexerProvider.getVtxos({
|
|
813
|
+
outpoints: [{ txid, vout: 0 }],
|
|
814
|
+
});
|
|
815
|
+
return res.vtxos[0]?.createdAt.getTime();
|
|
816
|
+
}
|
|
817
|
+
return undefined;
|
|
818
|
+
};
|
|
819
|
+
return (0, transactionHistory_1.buildTransactionHistory)(vtxos, boardingTxs, commitmentsToIgnore, getTxCreatedAt);
|
|
820
|
+
}
|
|
741
821
|
async ensureContractEventBroadcasting() {
|
|
742
822
|
if (!this.readonlyWallet)
|
|
743
823
|
return;
|
|
@@ -161,7 +161,10 @@ class ServiceWorkerReadonlyWallet {
|
|
|
161
161
|
publicKey: initConfig.arkServerPublicKey,
|
|
162
162
|
},
|
|
163
163
|
delegatorUrl: initConfig.delegatorUrl,
|
|
164
|
+
indexerUrl: options.indexerUrl,
|
|
165
|
+
esploraUrl: options.esploraUrl,
|
|
164
166
|
timeoutMs: options.messageBusTimeoutMs,
|
|
167
|
+
watcherConfig: options.watcherConfig,
|
|
165
168
|
}, options.messageBusTimeoutMs);
|
|
166
169
|
// Initialize the wallet handler
|
|
167
170
|
const initMessage = {
|
|
@@ -178,6 +181,9 @@ class ServiceWorkerReadonlyWallet {
|
|
|
178
181
|
publicKey: initConfig.arkServerPublicKey,
|
|
179
182
|
},
|
|
180
183
|
delegatorUrl: initConfig.delegatorUrl,
|
|
184
|
+
indexerUrl: options.indexerUrl,
|
|
185
|
+
esploraUrl: options.esploraUrl,
|
|
186
|
+
watcherConfig: options.watcherConfig,
|
|
181
187
|
};
|
|
182
188
|
wallet.initWalletPayload = initConfig;
|
|
183
189
|
wallet.messageBusTimeoutMs = options.messageBusTimeoutMs;
|
|
@@ -678,6 +684,14 @@ class ServiceWorkerReadonlyWallet {
|
|
|
678
684
|
navigator.serviceWorker.removeEventListener("message", messageHandler);
|
|
679
685
|
};
|
|
680
686
|
},
|
|
687
|
+
async refreshVtxos() {
|
|
688
|
+
const message = {
|
|
689
|
+
type: "REFRESH_VTXOS",
|
|
690
|
+
id: (0, utils_2.getRandomId)(),
|
|
691
|
+
tag: messageTag,
|
|
692
|
+
};
|
|
693
|
+
await sendContractMessage(message);
|
|
694
|
+
},
|
|
681
695
|
async isWatching() {
|
|
682
696
|
const message = {
|
|
683
697
|
type: "IS_CONTRACT_MANAGER_WATCHING",
|
|
@@ -748,7 +762,11 @@ class ServiceWorkerWallet extends ServiceWorkerReadonlyWallet {
|
|
|
748
762
|
publicKey: initConfig.arkServerPublicKey,
|
|
749
763
|
},
|
|
750
764
|
delegatorUrl: initConfig.delegatorUrl,
|
|
765
|
+
indexerUrl: options.indexerUrl,
|
|
766
|
+
esploraUrl: options.esploraUrl,
|
|
751
767
|
timeoutMs: options.messageBusTimeoutMs,
|
|
768
|
+
settlementConfig: options.settlementConfig,
|
|
769
|
+
watcherConfig: options.watcherConfig,
|
|
752
770
|
}, options.messageBusTimeoutMs);
|
|
753
771
|
// Initialize the service worker with the config
|
|
754
772
|
const initMessage = {
|
|
@@ -766,6 +784,10 @@ class ServiceWorkerWallet extends ServiceWorkerReadonlyWallet {
|
|
|
766
784
|
publicKey: initConfig.arkServerPublicKey,
|
|
767
785
|
},
|
|
768
786
|
delegatorUrl: initConfig.delegatorUrl,
|
|
787
|
+
indexerUrl: options.indexerUrl,
|
|
788
|
+
esploraUrl: options.esploraUrl,
|
|
789
|
+
settlementConfig: options.settlementConfig,
|
|
790
|
+
watcherConfig: options.watcherConfig,
|
|
769
791
|
};
|
|
770
792
|
wallet.initWalletPayload = initConfig;
|
|
771
793
|
wallet.messageBusTimeoutMs = options.messageBusTimeoutMs;
|
|
@@ -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
|