@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
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { RestIndexerProvider } from '../../providers/indexer.js';
|
|
2
2
|
import { isExpired, isRecoverable, isSpendable, isSubdust, } from '../index.js';
|
|
3
3
|
import { extendCoin, extendVirtualCoin } from '../utils.js';
|
|
4
|
+
import { buildTransactionHistory } from '../../utils/transactionHistory.js';
|
|
4
5
|
export class WalletNotInitializedError extends Error {
|
|
5
6
|
constructor() {
|
|
6
7
|
super("Wallet handler not initialized");
|
|
@@ -169,7 +170,8 @@ export class WalletMessageHandler {
|
|
|
169
170
|
});
|
|
170
171
|
}
|
|
171
172
|
case "GET_TRANSACTION_HISTORY": {
|
|
172
|
-
const
|
|
173
|
+
const allVtxos = await this.getVtxosFromRepo();
|
|
174
|
+
const transactions = (await this.buildTransactionHistoryFromCache(allVtxos)) ?? [];
|
|
173
175
|
return this.tagged({
|
|
174
176
|
id,
|
|
175
177
|
type: "TRANSACTION_HISTORY",
|
|
@@ -196,7 +198,7 @@ export class WalletMessageHandler {
|
|
|
196
198
|
});
|
|
197
199
|
}
|
|
198
200
|
case "RELOAD_WALLET": {
|
|
199
|
-
await this.
|
|
201
|
+
await this.reloadWallet();
|
|
200
202
|
return this.tagged({
|
|
201
203
|
id,
|
|
202
204
|
type: "RELOAD_SUCCESS",
|
|
@@ -282,6 +284,14 @@ export class WalletMessageHandler {
|
|
|
282
284
|
payload: { isWatching },
|
|
283
285
|
});
|
|
284
286
|
}
|
|
287
|
+
case "REFRESH_VTXOS": {
|
|
288
|
+
const manager = await this.readonlyWallet.getContractManager();
|
|
289
|
+
await manager.refreshVtxos();
|
|
290
|
+
return this.tagged({
|
|
291
|
+
id,
|
|
292
|
+
type: "REFRESH_VTXOS_SUCCESS",
|
|
293
|
+
});
|
|
294
|
+
}
|
|
285
295
|
case "SEND": {
|
|
286
296
|
const { recipients } = message.payload;
|
|
287
297
|
const txid = await this.wallet.send(...recipients);
|
|
@@ -438,10 +448,9 @@ export class WalletMessageHandler {
|
|
|
438
448
|
await this.onWalletInitialized();
|
|
439
449
|
}
|
|
440
450
|
async handleGetBalance() {
|
|
441
|
-
const [boardingUtxos,
|
|
451
|
+
const [boardingUtxos, allVtxos] = await Promise.all([
|
|
442
452
|
this.getAllBoardingUtxos(),
|
|
443
|
-
this.
|
|
444
|
-
this.getSweptVtxos(),
|
|
453
|
+
this.getVtxosFromRepo(),
|
|
445
454
|
]);
|
|
446
455
|
// boarding
|
|
447
456
|
let confirmed = 0;
|
|
@@ -454,7 +463,9 @@ export class WalletMessageHandler {
|
|
|
454
463
|
unconfirmed += utxo.value;
|
|
455
464
|
}
|
|
456
465
|
}
|
|
457
|
-
// offchain
|
|
466
|
+
// offchain — split spendable vs swept from single repo read
|
|
467
|
+
const spendableVtxos = allVtxos.filter(isSpendable);
|
|
468
|
+
const sweptVtxos = allVtxos.filter((vtxo) => vtxo.virtualStatus.state === "swept");
|
|
458
469
|
let settled = 0;
|
|
459
470
|
let preconfirmed = 0;
|
|
460
471
|
let recoverable = 0;
|
|
@@ -504,23 +515,12 @@ export class WalletMessageHandler {
|
|
|
504
515
|
return this.readonlyWallet.getBoardingUtxos();
|
|
505
516
|
}
|
|
506
517
|
/**
|
|
507
|
-
* Get spendable vtxos
|
|
518
|
+
* Get spendable vtxos from the repository
|
|
508
519
|
*/
|
|
509
520
|
async getSpendableVtxos() {
|
|
510
|
-
|
|
511
|
-
return [];
|
|
512
|
-
const vtxos = await this.readonlyWallet.getVtxos();
|
|
521
|
+
const vtxos = await this.getVtxosFromRepo();
|
|
513
522
|
return vtxos.filter(isSpendable);
|
|
514
523
|
}
|
|
515
|
-
/**
|
|
516
|
-
* Get swept vtxos for the current wallet address
|
|
517
|
-
*/
|
|
518
|
-
async getSweptVtxos() {
|
|
519
|
-
if (!this.readonlyWallet)
|
|
520
|
-
return [];
|
|
521
|
-
const vtxos = await this.readonlyWallet.getVtxos();
|
|
522
|
-
return vtxos.filter((vtxo) => vtxo.virtualStatus.state === "swept");
|
|
523
|
-
}
|
|
524
524
|
async onWalletInitialized() {
|
|
525
525
|
if (!this.readonlyWallet ||
|
|
526
526
|
!this.arkProvider ||
|
|
@@ -528,10 +528,11 @@ export class WalletMessageHandler {
|
|
|
528
528
|
!this.walletRepository) {
|
|
529
529
|
return;
|
|
530
530
|
}
|
|
531
|
-
//
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
531
|
+
// Initialize contract manager FIRST — this populates the repository
|
|
532
|
+
// with full VTXO history for all contracts (one indexer call per contract)
|
|
533
|
+
await this.ensureContractEventBroadcasting();
|
|
534
|
+
// Read VTXOs from repository (now populated by contract manager)
|
|
535
|
+
const vtxos = await this.getVtxosFromRepo();
|
|
535
536
|
if (this.wallet) {
|
|
536
537
|
try {
|
|
537
538
|
// recover pending transactions if possible
|
|
@@ -543,15 +544,13 @@ export class WalletMessageHandler {
|
|
|
543
544
|
console.error("Error recovering pending transactions:", error);
|
|
544
545
|
}
|
|
545
546
|
}
|
|
546
|
-
// Get wallet address and save vtxos using unified repository
|
|
547
|
-
const address = await this.readonlyWallet.getAddress();
|
|
548
|
-
await this.walletRepository.saveVtxos(address, vtxos);
|
|
549
547
|
// Fetch boarding utxos and save using unified repository
|
|
550
548
|
const boardingAddress = await this.readonlyWallet.getBoardingAddress();
|
|
551
549
|
const coins = await this.readonlyWallet.onchainProvider.getCoins(boardingAddress);
|
|
552
550
|
await this.walletRepository.saveUtxos(boardingAddress, coins.map((utxo) => extendCoin(this.readonlyWallet, utxo)));
|
|
553
|
-
//
|
|
554
|
-
const
|
|
551
|
+
// Build transaction history from cached VTXOs (no indexer call)
|
|
552
|
+
const address = await this.readonlyWallet.getAddress();
|
|
553
|
+
const txs = await this.buildTransactionHistoryFromCache(vtxos);
|
|
555
554
|
if (txs)
|
|
556
555
|
await this.walletRepository.saveTransactions(address, txs);
|
|
557
556
|
// unsubscribe previous subscription if any
|
|
@@ -596,7 +595,6 @@ export class WalletMessageHandler {
|
|
|
596
595
|
}));
|
|
597
596
|
}
|
|
598
597
|
});
|
|
599
|
-
await this.ensureContractEventBroadcasting();
|
|
600
598
|
// Eagerly start the VtxoManager so its background tasks (auto-renewal,
|
|
601
599
|
// boarding UTXO polling/sweep) run inside the service worker without
|
|
602
600
|
// waiting for a client to send a vtxo-manager message first.
|
|
@@ -609,6 +607,17 @@ export class WalletMessageHandler {
|
|
|
609
607
|
}
|
|
610
608
|
}
|
|
611
609
|
}
|
|
610
|
+
/**
|
|
611
|
+
* Force a full VTXO refresh from the indexer, then re-run bootstrap.
|
|
612
|
+
* Used by RELOAD_WALLET to ensure fresh data.
|
|
613
|
+
*/
|
|
614
|
+
async reloadWallet() {
|
|
615
|
+
if (!this.readonlyWallet)
|
|
616
|
+
return;
|
|
617
|
+
const manager = await this.readonlyWallet.getContractManager();
|
|
618
|
+
await manager.refreshVtxos();
|
|
619
|
+
await this.onWalletInitialized();
|
|
620
|
+
}
|
|
612
621
|
async handleSettle(message) {
|
|
613
622
|
const wallet = this.requireWallet();
|
|
614
623
|
const txid = await wallet.settle(message.payload.params, (e) => {
|
|
@@ -732,6 +741,77 @@ export class WalletMessageHandler {
|
|
|
732
741
|
this.arkProvider = undefined;
|
|
733
742
|
this.indexerProvider = undefined;
|
|
734
743
|
}
|
|
744
|
+
/**
|
|
745
|
+
* Read all VTXOs from the repository, aggregated across all contract
|
|
746
|
+
* addresses and the wallet's primary address, with deduplication.
|
|
747
|
+
*/
|
|
748
|
+
async getVtxosFromRepo() {
|
|
749
|
+
if (!this.walletRepository || !this.readonlyWallet)
|
|
750
|
+
return [];
|
|
751
|
+
const seen = new Set();
|
|
752
|
+
const allVtxos = [];
|
|
753
|
+
const addVtxos = (vtxos) => {
|
|
754
|
+
for (const vtxo of vtxos) {
|
|
755
|
+
const key = `${vtxo.txid}:${vtxo.vout}`;
|
|
756
|
+
if (!seen.has(key)) {
|
|
757
|
+
seen.add(key);
|
|
758
|
+
allVtxos.push(vtxo);
|
|
759
|
+
}
|
|
760
|
+
}
|
|
761
|
+
};
|
|
762
|
+
// Aggregate VTXOs from all contract addresses
|
|
763
|
+
const manager = await this.readonlyWallet.getContractManager();
|
|
764
|
+
const contracts = await manager.getContracts();
|
|
765
|
+
for (const contract of contracts) {
|
|
766
|
+
const vtxos = await this.walletRepository.getVtxos(contract.address);
|
|
767
|
+
addVtxos(vtxos);
|
|
768
|
+
}
|
|
769
|
+
// Also check the wallet's primary address
|
|
770
|
+
const walletAddress = await this.readonlyWallet.getAddress();
|
|
771
|
+
const walletVtxos = await this.walletRepository.getVtxos(walletAddress);
|
|
772
|
+
addVtxos(walletVtxos);
|
|
773
|
+
return allVtxos;
|
|
774
|
+
}
|
|
775
|
+
/**
|
|
776
|
+
* Build transaction history from cached VTXOs without hitting the indexer.
|
|
777
|
+
* Falls back to indexer only for uncached transaction timestamps.
|
|
778
|
+
*/
|
|
779
|
+
async buildTransactionHistoryFromCache(vtxos) {
|
|
780
|
+
if (!this.readonlyWallet)
|
|
781
|
+
return null;
|
|
782
|
+
const { boardingTxs, commitmentsToIgnore } = await this.readonlyWallet.getBoardingTxs();
|
|
783
|
+
// Build a lookup for cached VTXO timestamps, keyed by txid.
|
|
784
|
+
// Multiple VTXOs can share a txid (different vouts) — we keep the
|
|
785
|
+
// earliest createdAt so the history ordering is stable.
|
|
786
|
+
const vtxoCreatedAt = new Map();
|
|
787
|
+
for (const vtxo of vtxos) {
|
|
788
|
+
const existing = vtxoCreatedAt.get(vtxo.txid);
|
|
789
|
+
const ts = vtxo.createdAt.getTime();
|
|
790
|
+
if (existing === undefined || ts < existing) {
|
|
791
|
+
vtxoCreatedAt.set(vtxo.txid, ts);
|
|
792
|
+
}
|
|
793
|
+
}
|
|
794
|
+
// getTxCreatedAt resolves the creation timestamp of a transaction.
|
|
795
|
+
// buildTransactionHistory calls this for spent-offchain VTXOs with
|
|
796
|
+
// no change outputs to determine the time of the sending tx.
|
|
797
|
+
// Returns undefined on miss so buildTransactionHistory uses its
|
|
798
|
+
// own fallback (vtxo.createdAt + 1) rather than epoch 0.
|
|
799
|
+
// The vout:0 lookup in the indexer fallback mirrors the pre-existing
|
|
800
|
+
// convention in ReadonlyWallet.getTransactionHistory().
|
|
801
|
+
const getTxCreatedAt = async (txid) => {
|
|
802
|
+
const cached = vtxoCreatedAt.get(txid);
|
|
803
|
+
if (cached !== undefined)
|
|
804
|
+
return cached;
|
|
805
|
+
if (this.indexerProvider) {
|
|
806
|
+
const res = await this.indexerProvider.getVtxos({
|
|
807
|
+
outpoints: [{ txid, vout: 0 }],
|
|
808
|
+
});
|
|
809
|
+
return res.vtxos[0]?.createdAt.getTime();
|
|
810
|
+
}
|
|
811
|
+
return undefined;
|
|
812
|
+
};
|
|
813
|
+
return buildTransactionHistory(vtxos, boardingTxs, commitmentsToIgnore, getTxCreatedAt);
|
|
814
|
+
}
|
|
735
815
|
async ensureContractEventBroadcasting() {
|
|
736
816
|
if (!this.readonlyWallet)
|
|
737
817
|
return;
|
|
@@ -3,12 +3,13 @@ import { setupServiceWorker } from '../../worker/browser/utils.js';
|
|
|
3
3
|
import { IndexedDBContractRepository, IndexedDBWalletRepository, } from '../../repositories/index.js';
|
|
4
4
|
import { DEFAULT_MESSAGE_TAG, } from './wallet-message-handler.js';
|
|
5
5
|
import { getRandomId } from '../utils.js';
|
|
6
|
-
import { ServiceWorkerTimeoutError } from '../../worker/errors.js';
|
|
7
|
-
// Check by error
|
|
6
|
+
import { MESSAGE_BUS_NOT_INITIALIZED, ServiceWorkerTimeoutError, } from '../../worker/errors.js';
|
|
7
|
+
// Check by error message content instead of instanceof because postMessage uses the
|
|
8
8
|
// structured clone algorithm which strips the prototype chain — the page
|
|
9
9
|
// receives a plain Error, not the original MessageBusNotInitializedError.
|
|
10
10
|
function isMessageBusNotInitializedError(error) {
|
|
11
|
-
return (error instanceof Error &&
|
|
11
|
+
return (error instanceof Error &&
|
|
12
|
+
error.message.includes(MESSAGE_BUS_NOT_INITIALIZED));
|
|
12
13
|
}
|
|
13
14
|
const DEDUPABLE_REQUEST_TYPES = new Set([
|
|
14
15
|
"GET_ADDRESS",
|
|
@@ -158,7 +159,10 @@ export class ServiceWorkerReadonlyWallet {
|
|
|
158
159
|
publicKey: initConfig.arkServerPublicKey,
|
|
159
160
|
},
|
|
160
161
|
delegatorUrl: initConfig.delegatorUrl,
|
|
162
|
+
indexerUrl: options.indexerUrl,
|
|
163
|
+
esploraUrl: options.esploraUrl,
|
|
161
164
|
timeoutMs: options.messageBusTimeoutMs,
|
|
165
|
+
watcherConfig: options.watcherConfig,
|
|
162
166
|
}, options.messageBusTimeoutMs);
|
|
163
167
|
// Initialize the wallet handler
|
|
164
168
|
const initMessage = {
|
|
@@ -175,6 +179,9 @@ export class ServiceWorkerReadonlyWallet {
|
|
|
175
179
|
publicKey: initConfig.arkServerPublicKey,
|
|
176
180
|
},
|
|
177
181
|
delegatorUrl: initConfig.delegatorUrl,
|
|
182
|
+
indexerUrl: options.indexerUrl,
|
|
183
|
+
esploraUrl: options.esploraUrl,
|
|
184
|
+
watcherConfig: options.watcherConfig,
|
|
178
185
|
};
|
|
179
186
|
wallet.initWalletPayload = initConfig;
|
|
180
187
|
wallet.messageBusTimeoutMs = options.messageBusTimeoutMs;
|
|
@@ -675,6 +682,14 @@ export class ServiceWorkerReadonlyWallet {
|
|
|
675
682
|
navigator.serviceWorker.removeEventListener("message", messageHandler);
|
|
676
683
|
};
|
|
677
684
|
},
|
|
685
|
+
async refreshVtxos() {
|
|
686
|
+
const message = {
|
|
687
|
+
type: "REFRESH_VTXOS",
|
|
688
|
+
id: getRandomId(),
|
|
689
|
+
tag: messageTag,
|
|
690
|
+
};
|
|
691
|
+
await sendContractMessage(message);
|
|
692
|
+
},
|
|
678
693
|
async isWatching() {
|
|
679
694
|
const message = {
|
|
680
695
|
type: "IS_CONTRACT_MANAGER_WATCHING",
|
|
@@ -744,7 +759,11 @@ export class ServiceWorkerWallet extends ServiceWorkerReadonlyWallet {
|
|
|
744
759
|
publicKey: initConfig.arkServerPublicKey,
|
|
745
760
|
},
|
|
746
761
|
delegatorUrl: initConfig.delegatorUrl,
|
|
762
|
+
indexerUrl: options.indexerUrl,
|
|
763
|
+
esploraUrl: options.esploraUrl,
|
|
747
764
|
timeoutMs: options.messageBusTimeoutMs,
|
|
765
|
+
settlementConfig: options.settlementConfig,
|
|
766
|
+
watcherConfig: options.watcherConfig,
|
|
748
767
|
}, options.messageBusTimeoutMs);
|
|
749
768
|
// Initialize the service worker with the config
|
|
750
769
|
const initMessage = {
|
|
@@ -762,6 +781,10 @@ export class ServiceWorkerWallet extends ServiceWorkerReadonlyWallet {
|
|
|
762
781
|
publicKey: initConfig.arkServerPublicKey,
|
|
763
782
|
},
|
|
764
783
|
delegatorUrl: initConfig.delegatorUrl,
|
|
784
|
+
indexerUrl: options.indexerUrl,
|
|
785
|
+
esploraUrl: options.esploraUrl,
|
|
786
|
+
settlementConfig: options.settlementConfig,
|
|
787
|
+
watcherConfig: options.watcherConfig,
|
|
765
788
|
};
|
|
766
789
|
wallet.initWalletPayload = initConfig;
|
|
767
790
|
wallet.messageBusTimeoutMs = options.messageBusTimeoutMs;
|
|
@@ -152,7 +152,12 @@ export class VtxoManager {
|
|
|
152
152
|
this.knownBoardingUtxos = new Set();
|
|
153
153
|
this.sweptBoardingUtxos = new Set();
|
|
154
154
|
this.pollInProgress = false;
|
|
155
|
+
this.disposed = false;
|
|
155
156
|
this.consecutivePollFailures = 0;
|
|
157
|
+
// Guards against renewal feedback loop: when renewVtxos() settles, the
|
|
158
|
+
// server emits new VTXOs → vtxo_received → renewVtxos() again → infinite loop.
|
|
159
|
+
this.renewalInProgress = false;
|
|
160
|
+
this.lastRenewalTimestamp = 0;
|
|
156
161
|
// Normalize: prefer settlementConfig, fall back to renewalConfig, default to enabled
|
|
157
162
|
if (settlementConfig !== undefined) {
|
|
158
163
|
this.settlementConfig = settlementConfig;
|
|
@@ -336,32 +341,43 @@ export class VtxoManager {
|
|
|
336
341
|
* ```
|
|
337
342
|
*/
|
|
338
343
|
async renewVtxos(eventCallback) {
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
344
|
+
if (this.renewalInProgress) {
|
|
345
|
+
throw new Error("Renewal already in progress");
|
|
346
|
+
}
|
|
347
|
+
this.renewalInProgress = true;
|
|
348
|
+
try {
|
|
349
|
+
// Get all VTXOs (including recoverable ones)
|
|
350
|
+
// Use default threshold to bypass settlementConfig gate (manual API should always work)
|
|
351
|
+
const vtxos = await this.getExpiringVtxos(this.settlementConfig !== false &&
|
|
352
|
+
this.settlementConfig?.vtxoThreshold !== undefined
|
|
353
|
+
? this.settlementConfig.vtxoThreshold * 1000
|
|
354
|
+
: DEFAULT_RENEWAL_CONFIG.thresholdMs);
|
|
355
|
+
if (vtxos.length === 0) {
|
|
356
|
+
throw new Error("No VTXOs available to renew");
|
|
357
|
+
}
|
|
358
|
+
const totalAmount = vtxos.reduce((sum, vtxo) => sum + vtxo.value, 0);
|
|
359
|
+
// Get dust amount from wallet
|
|
360
|
+
const dustAmount = getDustAmount(this.wallet);
|
|
361
|
+
// Check if total amount is above dust threshold
|
|
362
|
+
if (BigInt(totalAmount) < dustAmount) {
|
|
363
|
+
throw new Error(`Total amount ${totalAmount} is below dust threshold ${dustAmount}`);
|
|
364
|
+
}
|
|
365
|
+
const arkAddress = await this.wallet.getAddress();
|
|
366
|
+
const txid = await this.wallet.settle({
|
|
367
|
+
inputs: vtxos,
|
|
368
|
+
outputs: [
|
|
369
|
+
{
|
|
370
|
+
address: arkAddress,
|
|
371
|
+
amount: BigInt(totalAmount),
|
|
372
|
+
},
|
|
373
|
+
],
|
|
374
|
+
}, eventCallback);
|
|
375
|
+
this.lastRenewalTimestamp = Date.now();
|
|
376
|
+
return txid;
|
|
377
|
+
}
|
|
378
|
+
finally {
|
|
379
|
+
this.renewalInProgress = false;
|
|
354
380
|
}
|
|
355
|
-
const arkAddress = await this.wallet.getAddress();
|
|
356
|
-
return this.wallet.settle({
|
|
357
|
-
inputs: vtxos,
|
|
358
|
-
outputs: [
|
|
359
|
-
{
|
|
360
|
-
address: arkAddress,
|
|
361
|
-
amount: BigInt(totalAmount),
|
|
362
|
-
},
|
|
363
|
-
],
|
|
364
|
-
}, eventCallback);
|
|
365
381
|
}
|
|
366
382
|
// ========== Boarding UTXO Sweep Methods ==========
|
|
367
383
|
/**
|
|
@@ -529,7 +545,11 @@ export class VtxoManager {
|
|
|
529
545
|
}
|
|
530
546
|
// Start polling for boarding UTXOs independently of contract manager
|
|
531
547
|
// SSE setup. Use a short delay to let the wallet finish construction.
|
|
532
|
-
setTimeout(() =>
|
|
548
|
+
this.startupPollTimeoutId = setTimeout(() => {
|
|
549
|
+
if (this.disposed)
|
|
550
|
+
return;
|
|
551
|
+
this.startBoardingUtxoPoll();
|
|
552
|
+
}, 1000);
|
|
533
553
|
try {
|
|
534
554
|
const [delegatorManager, contractManager, destination] = await Promise.all([
|
|
535
555
|
this.wallet.getDelegatorManager(),
|
|
@@ -540,28 +560,33 @@ export class VtxoManager {
|
|
|
540
560
|
if (event.type !== "vtxo_received") {
|
|
541
561
|
return;
|
|
542
562
|
}
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
563
|
+
const msSinceLastRenewal = Date.now() - this.lastRenewalTimestamp;
|
|
564
|
+
const shouldRenew = !this.renewalInProgress &&
|
|
565
|
+
msSinceLastRenewal >= VtxoManager.RENEWAL_COOLDOWN_MS;
|
|
566
|
+
if (shouldRenew) {
|
|
567
|
+
this.renewVtxos().catch((e) => {
|
|
568
|
+
if (e instanceof Error) {
|
|
569
|
+
if (e.message.includes("No VTXOs available to renew")) {
|
|
570
|
+
// Not an error, just no VTXO eligible for renewal.
|
|
571
|
+
return;
|
|
572
|
+
}
|
|
573
|
+
if (e.message.includes("is below dust threshold")) {
|
|
574
|
+
// Not an error, just below dust threshold.
|
|
575
|
+
// As more VTXOs are received, the threshold will be raised.
|
|
576
|
+
return;
|
|
577
|
+
}
|
|
578
|
+
if (e.message.includes("VTXO_ALREADY_REGISTERED") ||
|
|
579
|
+
e.message.includes("duplicated input")) {
|
|
580
|
+
// VTXO is already being used in a concurrent
|
|
581
|
+
// user-initiated operation. Skip silently — the
|
|
582
|
+
// wallet's tx lock serializes these, but the
|
|
583
|
+
// renewal will retry on the next cycle.
|
|
584
|
+
return;
|
|
585
|
+
}
|
|
548
586
|
}
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
return;
|
|
553
|
-
}
|
|
554
|
-
if (e.message.includes("VTXO_ALREADY_REGISTERED") ||
|
|
555
|
-
e.message.includes("duplicated input")) {
|
|
556
|
-
// VTXO is already being used in a concurrent
|
|
557
|
-
// user-initiated operation. Skip silently — the
|
|
558
|
-
// wallet's tx lock serializes these, but the
|
|
559
|
-
// renewal will retry on the next cycle.
|
|
560
|
-
return;
|
|
561
|
-
}
|
|
562
|
-
}
|
|
563
|
-
console.error("Error renewing VTXOs:", e);
|
|
564
|
-
});
|
|
587
|
+
console.error("Error renewing VTXOs:", e);
|
|
588
|
+
});
|
|
589
|
+
}
|
|
565
590
|
delegatorManager
|
|
566
591
|
?.delegate(event.vtxos, destination)
|
|
567
592
|
.catch((e) => {
|
|
@@ -601,7 +626,7 @@ export class VtxoManager {
|
|
|
601
626
|
this.pollBoardingUtxos();
|
|
602
627
|
}
|
|
603
628
|
schedulePoll() {
|
|
604
|
-
if (this.settlementConfig === false)
|
|
629
|
+
if (this.disposed || this.settlementConfig === false)
|
|
605
630
|
return;
|
|
606
631
|
const delay = this.getNextPollDelay();
|
|
607
632
|
this.pollTimeoutId = setTimeout(() => this.pollBoardingUtxos(), delay);
|
|
@@ -675,8 +700,8 @@ export class VtxoManager {
|
|
|
675
700
|
const expired = boardingUtxos.filter((utxo) => hasBoardingTxExpired(utxo, boardingTimelock, chainTipHeight));
|
|
676
701
|
expiredSet = new Set(expired.map((u) => `${u.txid}:${u.vout}`));
|
|
677
702
|
}
|
|
678
|
-
catch {
|
|
679
|
-
|
|
703
|
+
catch (e) {
|
|
704
|
+
throw e instanceof Error ? e : new Error(String(e));
|
|
680
705
|
}
|
|
681
706
|
const unsettledUtxos = boardingUtxos.filter((u) => !this.knownBoardingUtxos.has(`${u.txid}:${u.vout}`) &&
|
|
682
707
|
!expiredSet.has(`${u.txid}:${u.vout}`));
|
|
@@ -698,6 +723,11 @@ export class VtxoManager {
|
|
|
698
723
|
}
|
|
699
724
|
async dispose() {
|
|
700
725
|
this.disposePromise ?? (this.disposePromise = (async () => {
|
|
726
|
+
this.disposed = true;
|
|
727
|
+
if (this.startupPollTimeoutId) {
|
|
728
|
+
clearTimeout(this.startupPollTimeoutId);
|
|
729
|
+
this.startupPollTimeoutId = undefined;
|
|
730
|
+
}
|
|
701
731
|
if (this.pollTimeoutId) {
|
|
702
732
|
clearTimeout(this.pollTimeoutId);
|
|
703
733
|
this.pollTimeoutId = undefined;
|
|
@@ -713,3 +743,4 @@ export class VtxoManager {
|
|
|
713
743
|
}
|
|
714
744
|
}
|
|
715
745
|
VtxoManager.MAX_BACKOFF_MS = 5 * 60 * 1000; // 5 minutes
|
|
746
|
+
VtxoManager.RENEWAL_COOLDOWN_MS = 30000; // 30 seconds
|
|
@@ -273,27 +273,33 @@ export class ReadonlyWallet {
|
|
|
273
273
|
const scriptMap = await this.getScriptMap();
|
|
274
274
|
const f = filter ?? { withRecoverable: true, withUnrolled: false };
|
|
275
275
|
const allExtended = [];
|
|
276
|
-
//
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
if (
|
|
286
|
-
|
|
287
|
-
|
|
276
|
+
// Batch all scripts into a single indexer call
|
|
277
|
+
const allScripts = [...scriptMap.keys()];
|
|
278
|
+
const response = await this.indexerProvider.getVtxos({
|
|
279
|
+
scripts: allScripts,
|
|
280
|
+
});
|
|
281
|
+
for (const vtxo of response.vtxos) {
|
|
282
|
+
const vtxoScript = vtxo.script
|
|
283
|
+
? scriptMap.get(vtxo.script)
|
|
284
|
+
: undefined;
|
|
285
|
+
if (!vtxoScript)
|
|
286
|
+
continue;
|
|
287
|
+
if (isSpendable(vtxo)) {
|
|
288
|
+
if (!f.withRecoverable &&
|
|
289
|
+
(isRecoverable(vtxo) || isExpired(vtxo))) {
|
|
290
|
+
continue;
|
|
291
|
+
}
|
|
288
292
|
}
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
forfeitTapLeafScript: vtxoScript.forfeit(),
|
|
293
|
-
intentTapLeafScript: vtxoScript.forfeit(),
|
|
294
|
-
tapTree: vtxoScript.encode(),
|
|
295
|
-
});
|
|
293
|
+
else {
|
|
294
|
+
if (!f.withUnrolled || !vtxo.isUnrolled)
|
|
295
|
+
continue;
|
|
296
296
|
}
|
|
297
|
+
allExtended.push({
|
|
298
|
+
...vtxo,
|
|
299
|
+
forfeitTapLeafScript: vtxoScript.forfeit(),
|
|
300
|
+
intentTapLeafScript: vtxoScript.forfeit(),
|
|
301
|
+
tapTree: vtxoScript.encode(),
|
|
302
|
+
});
|
|
297
303
|
}
|
|
298
304
|
// Update cache with fresh data
|
|
299
305
|
await this.walletRepository.saveVtxos(address, allExtended);
|
|
@@ -305,7 +311,7 @@ export class ReadonlyWallet {
|
|
|
305
311
|
const { boardingTxs, commitmentsToIgnore } = await this.getBoardingTxs();
|
|
306
312
|
const getTxCreatedAt = (txid) => this.indexerProvider
|
|
307
313
|
.getVtxos({ outpoints: [{ txid, vout: 0 }] })
|
|
308
|
-
.then((res) => res.vtxos[0]?.createdAt.getTime()
|
|
314
|
+
.then((res) => res.vtxos[0]?.createdAt.getTime());
|
|
309
315
|
return buildTransactionHistory(response.vtxos, boardingTxs, commitmentsToIgnore, getTxCreatedAt);
|
|
310
316
|
}
|
|
311
317
|
async getBoardingTxs() {
|
|
@@ -1338,23 +1344,29 @@ export class Wallet extends ReadonlyWallet {
|
|
|
1338
1344
|
async finalizePendingTxs(vtxos) {
|
|
1339
1345
|
const MAX_INPUTS_PER_INTENT = 20;
|
|
1340
1346
|
if (!vtxos || vtxos.length === 0) {
|
|
1341
|
-
//
|
|
1347
|
+
// Batch all scripts into a single indexer call
|
|
1342
1348
|
const scriptMap = await this.getScriptMap();
|
|
1343
1349
|
const allExtended = [];
|
|
1344
|
-
|
|
1345
|
-
|
|
1346
|
-
|
|
1347
|
-
|
|
1348
|
-
|
|
1349
|
-
|
|
1350
|
-
|
|
1351
|
-
|
|
1352
|
-
|
|
1353
|
-
|
|
1354
|
-
|
|
1355
|
-
|
|
1356
|
-
|
|
1350
|
+
const allScripts = [...scriptMap.keys()];
|
|
1351
|
+
const { vtxos: fetchedVtxos } = await this.indexerProvider.getVtxos({
|
|
1352
|
+
scripts: allScripts,
|
|
1353
|
+
});
|
|
1354
|
+
for (const vtxo of fetchedVtxos) {
|
|
1355
|
+
const vtxoScript = vtxo.script
|
|
1356
|
+
? scriptMap.get(vtxo.script)
|
|
1357
|
+
: undefined;
|
|
1358
|
+
if (!vtxoScript)
|
|
1359
|
+
continue;
|
|
1360
|
+
if (vtxo.virtualStatus.state === "swept" ||
|
|
1361
|
+
vtxo.virtualStatus.state === "settled") {
|
|
1362
|
+
continue;
|
|
1357
1363
|
}
|
|
1364
|
+
allExtended.push({
|
|
1365
|
+
...vtxo,
|
|
1366
|
+
forfeitTapLeafScript: vtxoScript.forfeit(),
|
|
1367
|
+
intentTapLeafScript: vtxoScript.forfeit(),
|
|
1368
|
+
tapTree: vtxoScript.encode(),
|
|
1369
|
+
});
|
|
1358
1370
|
}
|
|
1359
1371
|
if (allExtended.length === 0) {
|
|
1360
1372
|
return { finalized: [], pending: [] };
|
|
@@ -1,12 +1,11 @@
|
|
|
1
|
+
export const MESSAGE_BUS_NOT_INITIALIZED = "MessageBus not initialized";
|
|
1
2
|
export class MessageBusNotInitializedError extends Error {
|
|
2
3
|
constructor() {
|
|
3
|
-
super(
|
|
4
|
-
this.name = "MessageBusNotInitializedError";
|
|
4
|
+
super(MESSAGE_BUS_NOT_INITIALIZED);
|
|
5
5
|
}
|
|
6
6
|
}
|
|
7
7
|
export class ServiceWorkerTimeoutError extends Error {
|
|
8
8
|
constructor(detail) {
|
|
9
9
|
super(detail);
|
|
10
|
-
this.name = "ServiceWorkerTimeoutError";
|
|
11
10
|
}
|
|
12
11
|
}
|
|
@@ -149,8 +149,12 @@ export class MessageBus {
|
|
|
149
149
|
identity,
|
|
150
150
|
arkServerUrl: config.arkServer.url,
|
|
151
151
|
arkServerPublicKey: config.arkServer.publicKey,
|
|
152
|
+
indexerUrl: config.indexerUrl,
|
|
153
|
+
esploraUrl: config.esploraUrl,
|
|
152
154
|
storage,
|
|
153
155
|
delegatorProvider,
|
|
156
|
+
settlementConfig: config.settlementConfig,
|
|
157
|
+
watcherConfig: config.watcherConfig,
|
|
154
158
|
});
|
|
155
159
|
return { wallet, arkProvider, readonlyWallet: wallet };
|
|
156
160
|
}
|
|
@@ -160,8 +164,11 @@ export class MessageBus {
|
|
|
160
164
|
identity,
|
|
161
165
|
arkServerUrl: config.arkServer.url,
|
|
162
166
|
arkServerPublicKey: config.arkServer.publicKey,
|
|
167
|
+
indexerUrl: config.indexerUrl,
|
|
168
|
+
esploraUrl: config.esploraUrl,
|
|
163
169
|
storage,
|
|
164
170
|
delegatorProvider,
|
|
171
|
+
watcherConfig: config.watcherConfig,
|
|
165
172
|
});
|
|
166
173
|
return { readonlyWallet, arkProvider };
|
|
167
174
|
}
|
|
@@ -102,6 +102,11 @@ export interface IContractManager extends Disposable {
|
|
|
102
102
|
* @returns Unsubscribe function
|
|
103
103
|
*/
|
|
104
104
|
onContractEvent(callback: ContractEventCallback): () => void;
|
|
105
|
+
/**
|
|
106
|
+
* Force a full VTXO refresh from the indexer for all contracts.
|
|
107
|
+
* Populates the wallet repository with complete VTXO history.
|
|
108
|
+
*/
|
|
109
|
+
refreshVtxos(): Promise<void>;
|
|
105
110
|
/**
|
|
106
111
|
* Whether the underlying watcher is currently active.
|
|
107
112
|
*/
|
|
@@ -292,6 +297,11 @@ export declare class ContractManager implements IContractManager {
|
|
|
292
297
|
* ```
|
|
293
298
|
*/
|
|
294
299
|
onContractEvent(callback: ContractEventCallback): () => void;
|
|
300
|
+
/**
|
|
301
|
+
* Force a full VTXO refresh from the indexer for all contracts.
|
|
302
|
+
* Populates the wallet repository with complete VTXO history.
|
|
303
|
+
*/
|
|
304
|
+
refreshVtxos(): Promise<void>;
|
|
295
305
|
/**
|
|
296
306
|
* Check if currently watching.
|
|
297
307
|
*/
|