@arkade-os/sdk 0.4.7 → 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/identity/seedIdentity.js +2 -2
- package/dist/cjs/index.js +9 -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 +249 -36
- package/dist/cjs/wallet/serviceWorker/wallet.js +286 -34
- package/dist/cjs/wallet/vtxo-manager.js +123 -86
- package/dist/cjs/wallet/wallet.js +140 -68
- package/dist/cjs/worker/errors.js +17 -0
- package/dist/cjs/worker/messageBus.js +14 -2
- package/dist/esm/contracts/contractManager.js +59 -11
- package/dist/esm/contracts/contractWatcher.js +21 -2
- package/dist/esm/identity/seedIdentity.js +2 -2
- package/dist/esm/index.js +3 -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 +245 -35
- package/dist/esm/wallet/serviceWorker/wallet.js +286 -34
- package/dist/esm/wallet/vtxo-manager.js +123 -86
- package/dist/esm/wallet/wallet.js +140 -68
- package/dist/esm/worker/errors.js +12 -0
- package/dist/esm/worker/messageBus.js +14 -2
- package/dist/types/contracts/contractManager.d.ts +10 -0
- package/dist/types/identity/seedIdentity.d.ts +5 -2
- package/dist/types/index.d.ts +5 -4
- 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 +101 -7
- package/dist/types/wallet/serviceWorker/wallet.d.ts +16 -0
- package/dist/types/wallet/vtxo-manager.d.ts +29 -2
- package/dist/types/wallet/wallet.d.ts +10 -0
- package/dist/types/worker/errors.d.ts +6 -0
- package/dist/types/worker/messageBus.d.ts +6 -0
- package/package.json +1 -1
|
@@ -148,43 +148,6 @@ function getExpiringAndRecoverableVtxos(vtxos, thresholdMs, dustAmount) {
|
|
|
148
148
|
((0, _1.isSpendable)(vtxo) && (0, _1.isExpired)(vtxo)) ||
|
|
149
149
|
(0, _1.isSubdust)(vtxo, dustAmount));
|
|
150
150
|
}
|
|
151
|
-
/**
|
|
152
|
-
* VtxoManager is a unified class for managing VTXO lifecycle operations including
|
|
153
|
-
* recovery of swept/expired VTXOs and renewal to prevent expiration.
|
|
154
|
-
*
|
|
155
|
-
* Key Features:
|
|
156
|
-
* - **Recovery**: Reclaim swept or expired VTXOs back to the wallet
|
|
157
|
-
* - **Renewal**: Refresh VTXO expiration time before they expire
|
|
158
|
-
* - **Smart subdust handling**: Automatically includes subdust VTXOs when economically viable
|
|
159
|
-
* - **Expiry monitoring**: Check for VTXOs that are expiring soon
|
|
160
|
-
*
|
|
161
|
-
* VTXOs become recoverable when:
|
|
162
|
-
* - The Ark server sweeps them (virtualStatus.state === "swept") and they remain spendable
|
|
163
|
-
* - They are preconfirmed subdust (to consolidate small amounts without locking liquidity on settled VTXOs)
|
|
164
|
-
*
|
|
165
|
-
* @example
|
|
166
|
-
* ```typescript
|
|
167
|
-
* // Initialize with renewal config
|
|
168
|
-
* const manager = new VtxoManager(wallet, {
|
|
169
|
-
* enabled: true,
|
|
170
|
-
* thresholdMs: 86400000
|
|
171
|
-
* });
|
|
172
|
-
*
|
|
173
|
-
* // Check recoverable balance
|
|
174
|
-
* const balance = await manager.getRecoverableBalance();
|
|
175
|
-
* if (balance.recoverable > 0n) {
|
|
176
|
-
* console.log(`Can recover ${balance.recoverable} sats`);
|
|
177
|
-
* const txid = await manager.recoverVtxos();
|
|
178
|
-
* }
|
|
179
|
-
*
|
|
180
|
-
* // Check for expiring VTXOs
|
|
181
|
-
* const expiring = await manager.getExpiringVtxos();
|
|
182
|
-
* if (expiring.length > 0) {
|
|
183
|
-
* console.log(`${expiring.length} VTXOs expiring soon`);
|
|
184
|
-
* const txid = await manager.renewVtxos();
|
|
185
|
-
* }
|
|
186
|
-
* ```
|
|
187
|
-
*/
|
|
188
151
|
class VtxoManager {
|
|
189
152
|
constructor(wallet,
|
|
190
153
|
/** @deprecated Use settlementConfig instead */
|
|
@@ -194,6 +157,12 @@ class VtxoManager {
|
|
|
194
157
|
this.knownBoardingUtxos = new Set();
|
|
195
158
|
this.sweptBoardingUtxos = new Set();
|
|
196
159
|
this.pollInProgress = false;
|
|
160
|
+
this.disposed = false;
|
|
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;
|
|
197
166
|
// Normalize: prefer settlementConfig, fall back to renewalConfig, default to enabled
|
|
198
167
|
if (settlementConfig !== undefined) {
|
|
199
168
|
this.settlementConfig = settlementConfig;
|
|
@@ -377,32 +346,43 @@ class VtxoManager {
|
|
|
377
346
|
* ```
|
|
378
347
|
*/
|
|
379
348
|
async renewVtxos(eventCallback) {
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
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;
|
|
395
385
|
}
|
|
396
|
-
const arkAddress = await this.wallet.getAddress();
|
|
397
|
-
return this.wallet.settle({
|
|
398
|
-
inputs: vtxos,
|
|
399
|
-
outputs: [
|
|
400
|
-
{
|
|
401
|
-
address: arkAddress,
|
|
402
|
-
amount: BigInt(totalAmount),
|
|
403
|
-
},
|
|
404
|
-
],
|
|
405
|
-
}, eventCallback);
|
|
406
386
|
}
|
|
407
387
|
// ========== Boarding UTXO Sweep Methods ==========
|
|
408
388
|
/**
|
|
@@ -570,7 +550,11 @@ class VtxoManager {
|
|
|
570
550
|
}
|
|
571
551
|
// Start polling for boarding UTXOs independently of contract manager
|
|
572
552
|
// SSE setup. Use a short delay to let the wallet finish construction.
|
|
573
|
-
setTimeout(() =>
|
|
553
|
+
this.startupPollTimeoutId = setTimeout(() => {
|
|
554
|
+
if (this.disposed)
|
|
555
|
+
return;
|
|
556
|
+
this.startBoardingUtxoPoll();
|
|
557
|
+
}, 1000);
|
|
574
558
|
try {
|
|
575
559
|
const [delegatorManager, contractManager, destination] = await Promise.all([
|
|
576
560
|
this.wallet.getDelegatorManager(),
|
|
@@ -581,20 +565,33 @@ class VtxoManager {
|
|
|
581
565
|
if (event.type !== "vtxo_received") {
|
|
582
566
|
return;
|
|
583
567
|
}
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
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
|
+
}
|
|
589
591
|
}
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
return;
|
|
594
|
-
}
|
|
595
|
-
}
|
|
596
|
-
console.error("Error renewing VTXOs:", e);
|
|
597
|
-
});
|
|
592
|
+
console.error("Error renewing VTXOs:", e);
|
|
593
|
+
});
|
|
594
|
+
}
|
|
598
595
|
delegatorManager
|
|
599
596
|
?.delegate(event.vtxos, destination)
|
|
600
597
|
.catch((e) => {
|
|
@@ -608,19 +605,36 @@ class VtxoManager {
|
|
|
608
605
|
return undefined;
|
|
609
606
|
}
|
|
610
607
|
}
|
|
608
|
+
/** Computes the next poll delay, applying exponential backoff on failures. */
|
|
609
|
+
getNextPollDelay() {
|
|
610
|
+
if (this.settlementConfig === false)
|
|
611
|
+
return 0;
|
|
612
|
+
const baseMs = this.settlementConfig.pollIntervalMs ??
|
|
613
|
+
exports.DEFAULT_SETTLEMENT_CONFIG.pollIntervalMs;
|
|
614
|
+
if (this.consecutivePollFailures === 0)
|
|
615
|
+
return baseMs;
|
|
616
|
+
const backoff = Math.min(baseMs * Math.pow(2, this.consecutivePollFailures), VtxoManager.MAX_BACKOFF_MS);
|
|
617
|
+
return backoff;
|
|
618
|
+
}
|
|
611
619
|
/**
|
|
612
620
|
* Starts a polling loop that:
|
|
613
621
|
* 1. Auto-settles new boarding UTXOs into Ark
|
|
614
622
|
* 2. Sweeps expired boarding UTXOs (when boardingUtxoSweep is enabled)
|
|
623
|
+
*
|
|
624
|
+
* Uses setTimeout chaining (not setInterval) so a slow/blocked poll
|
|
625
|
+
* cannot stack up and the next delay can incorporate backoff.
|
|
615
626
|
*/
|
|
616
627
|
startBoardingUtxoPoll() {
|
|
617
628
|
if (this.settlementConfig === false)
|
|
618
629
|
return;
|
|
619
|
-
|
|
620
|
-
exports.DEFAULT_SETTLEMENT_CONFIG.pollIntervalMs;
|
|
621
|
-
// Run once immediately, then on interval
|
|
630
|
+
// Run once immediately, then schedule next
|
|
622
631
|
this.pollBoardingUtxos();
|
|
623
|
-
|
|
632
|
+
}
|
|
633
|
+
schedulePoll() {
|
|
634
|
+
if (this.disposed || this.settlementConfig === false)
|
|
635
|
+
return;
|
|
636
|
+
const delay = this.getNextPollDelay();
|
|
637
|
+
this.pollTimeoutId = setTimeout(() => this.pollBoardingUtxos(), delay);
|
|
624
638
|
}
|
|
625
639
|
async pollBoardingUtxos() {
|
|
626
640
|
// Guard: wallet must support boarding UTXO + sweep operations
|
|
@@ -630,6 +644,7 @@ class VtxoManager {
|
|
|
630
644
|
if (this.pollInProgress)
|
|
631
645
|
return;
|
|
632
646
|
this.pollInProgress = true;
|
|
647
|
+
let hadError = false;
|
|
633
648
|
try {
|
|
634
649
|
// Settle new (unexpired) UTXOs first, then sweep expired ones.
|
|
635
650
|
// Sequential to avoid racing for the same UTXOs.
|
|
@@ -637,6 +652,7 @@ class VtxoManager {
|
|
|
637
652
|
await this.settleBoardingUtxos();
|
|
638
653
|
}
|
|
639
654
|
catch (e) {
|
|
655
|
+
hadError = true;
|
|
640
656
|
console.error("Error auto-settling boarding UTXOs:", e);
|
|
641
657
|
}
|
|
642
658
|
const sweepEnabled = this.settlementConfig !== false &&
|
|
@@ -649,13 +665,21 @@ class VtxoManager {
|
|
|
649
665
|
catch (e) {
|
|
650
666
|
if (!(e instanceof Error) ||
|
|
651
667
|
!e.message.includes("No expired boarding UTXOs")) {
|
|
668
|
+
hadError = true;
|
|
652
669
|
console.error("Error auto-sweeping boarding UTXOs:", e);
|
|
653
670
|
}
|
|
654
671
|
}
|
|
655
672
|
}
|
|
656
673
|
}
|
|
657
674
|
finally {
|
|
675
|
+
if (hadError) {
|
|
676
|
+
this.consecutivePollFailures++;
|
|
677
|
+
}
|
|
678
|
+
else {
|
|
679
|
+
this.consecutivePollFailures = 0;
|
|
680
|
+
}
|
|
658
681
|
this.pollInProgress = false;
|
|
682
|
+
this.schedulePoll();
|
|
659
683
|
}
|
|
660
684
|
}
|
|
661
685
|
/**
|
|
@@ -672,11 +696,17 @@ class VtxoManager {
|
|
|
672
696
|
// accidentally settling expired UTXOs (which would conflict with sweep).
|
|
673
697
|
let expiredSet;
|
|
674
698
|
try {
|
|
675
|
-
const
|
|
699
|
+
const boardingTimelock = this.getBoardingTimelock();
|
|
700
|
+
let chainTipHeight;
|
|
701
|
+
if (boardingTimelock.type === "blocks") {
|
|
702
|
+
const tip = await this.getOnchainProvider().getChainTip();
|
|
703
|
+
chainTipHeight = tip.height;
|
|
704
|
+
}
|
|
705
|
+
const expired = boardingUtxos.filter((utxo) => (0, arkTransaction_1.hasBoardingTxExpired)(utxo, boardingTimelock, chainTipHeight));
|
|
676
706
|
expiredSet = new Set(expired.map((u) => `${u.txid}:${u.vout}`));
|
|
677
707
|
}
|
|
678
|
-
catch {
|
|
679
|
-
|
|
708
|
+
catch (e) {
|
|
709
|
+
throw e instanceof Error ? e : new Error(String(e));
|
|
680
710
|
}
|
|
681
711
|
const unsettledUtxos = boardingUtxos.filter((u) => !this.knownBoardingUtxos.has(`${u.txid}:${u.vout}`) &&
|
|
682
712
|
!expiredSet.has(`${u.txid}:${u.vout}`));
|
|
@@ -698,9 +728,14 @@ class VtxoManager {
|
|
|
698
728
|
}
|
|
699
729
|
async dispose() {
|
|
700
730
|
this.disposePromise ?? (this.disposePromise = (async () => {
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
this.
|
|
731
|
+
this.disposed = true;
|
|
732
|
+
if (this.startupPollTimeoutId) {
|
|
733
|
+
clearTimeout(this.startupPollTimeoutId);
|
|
734
|
+
this.startupPollTimeoutId = undefined;
|
|
735
|
+
}
|
|
736
|
+
if (this.pollTimeoutId) {
|
|
737
|
+
clearTimeout(this.pollTimeoutId);
|
|
738
|
+
this.pollTimeoutId = undefined;
|
|
704
739
|
}
|
|
705
740
|
const subscription = await this.contractEventsSubscriptionReady;
|
|
706
741
|
this.contractEventsSubscription = undefined;
|
|
@@ -713,3 +748,5 @@ class VtxoManager {
|
|
|
713
748
|
}
|
|
714
749
|
}
|
|
715
750
|
exports.VtxoManager = VtxoManager;
|
|
751
|
+
VtxoManager.MAX_BACKOFF_MS = 5 * 60 * 1000; // 5 minutes
|
|
752
|
+
VtxoManager.RENEWAL_COOLDOWN_MS = 30000; // 30 seconds
|
|
@@ -62,6 +62,19 @@ class ReadonlyWallet {
|
|
|
62
62
|
this.walletRepository = walletRepository;
|
|
63
63
|
this.contractRepository = contractRepository;
|
|
64
64
|
this.delegatorProvider = delegatorProvider;
|
|
65
|
+
// Guard: detect identity/server network mismatch for descriptor-based identities.
|
|
66
|
+
// This duplicates the check in setupWalletConfig() so that subclasses
|
|
67
|
+
// bypassing the factory still get the safety net.
|
|
68
|
+
if ("descriptor" in identity) {
|
|
69
|
+
const descriptor = identity.descriptor;
|
|
70
|
+
const identityIsMainnet = !descriptor.includes("tpub");
|
|
71
|
+
const serverIsMainnet = network.bech32 === "bc";
|
|
72
|
+
if (identityIsMainnet !== serverIsMainnet) {
|
|
73
|
+
throw new Error(`Network mismatch: identity uses ${identityIsMainnet ? "mainnet" : "testnet"} derivation ` +
|
|
74
|
+
`but wallet network is ${serverIsMainnet ? "mainnet" : "testnet"}. ` +
|
|
75
|
+
`Create identity with { isMainnet: ${serverIsMainnet} } to match.`);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
65
78
|
this.watcherConfig = watcherConfig;
|
|
66
79
|
this._assetManager = new asset_manager_1.ReadonlyAssetManager(this.indexerProvider);
|
|
67
80
|
}
|
|
@@ -89,6 +102,24 @@ class ReadonlyWallet {
|
|
|
89
102
|
const indexerProvider = config.indexerProvider || new indexer_1.RestIndexerProvider(indexerUrl);
|
|
90
103
|
const info = await arkProvider.getInfo();
|
|
91
104
|
const network = (0, networks_1.getNetwork)(info.network);
|
|
105
|
+
// Guard: detect identity/server network mismatch for seed-based identities.
|
|
106
|
+
// A mainnet descriptor (xpub, coin type 0) connected to a testnet server
|
|
107
|
+
// (or vice versa) means wrong derivation path → wrong keys → potential fund loss.
|
|
108
|
+
if ("descriptor" in config.identity) {
|
|
109
|
+
const descriptor = config.identity.descriptor;
|
|
110
|
+
const identityIsMainnet = !descriptor.includes("tpub");
|
|
111
|
+
const serverIsMainnet = info.network === "bitcoin";
|
|
112
|
+
if (identityIsMainnet && !serverIsMainnet) {
|
|
113
|
+
throw new Error(`Network mismatch: identity uses mainnet derivation (coin type 0) ` +
|
|
114
|
+
`but Ark server is on ${info.network}. ` +
|
|
115
|
+
`Create identity with { isMainnet: false } to use testnet derivation.`);
|
|
116
|
+
}
|
|
117
|
+
if (!identityIsMainnet && serverIsMainnet) {
|
|
118
|
+
throw new Error(`Network mismatch: identity uses testnet derivation (coin type 1) ` +
|
|
119
|
+
`but Ark server is on mainnet. ` +
|
|
120
|
+
`Create identity with { isMainnet: true } or omit isMainnet (defaults to mainnet).`);
|
|
121
|
+
}
|
|
122
|
+
}
|
|
92
123
|
// Extract esploraUrl from provider if not explicitly provided
|
|
93
124
|
const esploraUrl = config.esploraUrl || onchain_1.ESPLORA_URL[info.network];
|
|
94
125
|
// Use provided onchainProvider instance or create a new one
|
|
@@ -247,27 +278,33 @@ class ReadonlyWallet {
|
|
|
247
278
|
const scriptMap = await this.getScriptMap();
|
|
248
279
|
const f = filter ?? { withRecoverable: true, withUnrolled: false };
|
|
249
280
|
const allExtended = [];
|
|
250
|
-
//
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
if (
|
|
260
|
-
|
|
261
|
-
|
|
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
|
+
}
|
|
262
297
|
}
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
forfeitTapLeafScript: vtxoScript.forfeit(),
|
|
267
|
-
intentTapLeafScript: vtxoScript.forfeit(),
|
|
268
|
-
tapTree: vtxoScript.encode(),
|
|
269
|
-
});
|
|
298
|
+
else {
|
|
299
|
+
if (!f.withUnrolled || !vtxo.isUnrolled)
|
|
300
|
+
continue;
|
|
270
301
|
}
|
|
302
|
+
allExtended.push({
|
|
303
|
+
...vtxo,
|
|
304
|
+
forfeitTapLeafScript: vtxoScript.forfeit(),
|
|
305
|
+
intentTapLeafScript: vtxoScript.forfeit(),
|
|
306
|
+
tapTree: vtxoScript.encode(),
|
|
307
|
+
});
|
|
271
308
|
}
|
|
272
309
|
// Update cache with fresh data
|
|
273
310
|
await this.walletRepository.saveVtxos(address, allExtended);
|
|
@@ -279,7 +316,7 @@ class ReadonlyWallet {
|
|
|
279
316
|
const { boardingTxs, commitmentsToIgnore } = await this.getBoardingTxs();
|
|
280
317
|
const getTxCreatedAt = (txid) => this.indexerProvider
|
|
281
318
|
.getVtxos({ outpoints: [{ txid, vout: 0 }] })
|
|
282
|
-
.then((res) => res.vtxos[0]?.createdAt.getTime()
|
|
319
|
+
.then((res) => res.vtxos[0]?.createdAt.getTime());
|
|
283
320
|
return (0, transactionHistory_1.buildTransactionHistory)(response.vtxos, boardingTxs, commitmentsToIgnore, getTxCreatedAt);
|
|
284
321
|
}
|
|
285
322
|
async getBoardingTxs() {
|
|
@@ -664,6 +701,20 @@ exports.ReadonlyWallet = ReadonlyWallet;
|
|
|
664
701
|
* ```
|
|
665
702
|
*/
|
|
666
703
|
class Wallet extends ReadonlyWallet {
|
|
704
|
+
_withTxLock(fn) {
|
|
705
|
+
let release;
|
|
706
|
+
const lock = new Promise((r) => (release = r));
|
|
707
|
+
const prev = this._txLock;
|
|
708
|
+
this._txLock = lock;
|
|
709
|
+
return prev.then(async () => {
|
|
710
|
+
try {
|
|
711
|
+
return await fn();
|
|
712
|
+
}
|
|
713
|
+
finally {
|
|
714
|
+
release();
|
|
715
|
+
}
|
|
716
|
+
});
|
|
717
|
+
}
|
|
667
718
|
constructor(identity, network, networkName, onchainProvider, arkProvider, indexerProvider, arkServerPublicKey, offchainTapscript, boardingTapscript, serverUnrollScript, forfeitOutputScript, forfeitPubkey, dustAmount, walletRepository, contractRepository,
|
|
668
719
|
/** @deprecated Use settlementConfig */
|
|
669
720
|
renewalConfig, delegatorProvider, watcherConfig, settlementConfig) {
|
|
@@ -673,6 +724,13 @@ class Wallet extends ReadonlyWallet {
|
|
|
673
724
|
this.serverUnrollScript = serverUnrollScript;
|
|
674
725
|
this.forfeitOutputScript = forfeitOutputScript;
|
|
675
726
|
this.forfeitPubkey = forfeitPubkey;
|
|
727
|
+
/**
|
|
728
|
+
* Async mutex that serializes all operations submitting VTXOs to the Ark
|
|
729
|
+
* server (`settle`, `send`, `sendBitcoin`). This prevents VtxoManager's
|
|
730
|
+
* background renewal from racing with user-initiated transactions for the
|
|
731
|
+
* same VTXO inputs.
|
|
732
|
+
*/
|
|
733
|
+
this._txLock = Promise.resolve();
|
|
676
734
|
this.identity = identity;
|
|
677
735
|
// Backwards-compatible: keep renewalConfig populated for any code reading it
|
|
678
736
|
this.renewalConfig = {
|
|
@@ -811,40 +869,42 @@ class Wallet extends ReadonlyWallet {
|
|
|
811
869
|
throw new Error("Invalid Ark address " + params.address);
|
|
812
870
|
}
|
|
813
871
|
if (params.selectedVtxos && params.selectedVtxos.length > 0) {
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
872
|
+
return this._withTxLock(async () => {
|
|
873
|
+
const selectedVtxoSum = params
|
|
874
|
+
.selectedVtxos.map((v) => v.value)
|
|
875
|
+
.reduce((a, b) => a + b, 0);
|
|
876
|
+
if (selectedVtxoSum < params.amount) {
|
|
877
|
+
throw new Error("Selected VTXOs do not cover specified amount");
|
|
878
|
+
}
|
|
879
|
+
const changeAmount = selectedVtxoSum - params.amount;
|
|
880
|
+
const selected = {
|
|
881
|
+
inputs: params.selectedVtxos,
|
|
882
|
+
changeAmount: BigInt(changeAmount),
|
|
883
|
+
};
|
|
884
|
+
const outputAddress = address_1.ArkAddress.decode(params.address);
|
|
885
|
+
const outputScript = BigInt(params.amount) < this.dustAmount
|
|
886
|
+
? outputAddress.subdustPkScript
|
|
887
|
+
: outputAddress.pkScript;
|
|
888
|
+
const outputs = [
|
|
889
|
+
{
|
|
890
|
+
script: outputScript,
|
|
891
|
+
amount: BigInt(params.amount),
|
|
892
|
+
},
|
|
893
|
+
];
|
|
894
|
+
// add change output if needed
|
|
895
|
+
if (selected.changeAmount > 0n) {
|
|
896
|
+
const changeOutputScript = selected.changeAmount < this.dustAmount
|
|
897
|
+
? this.arkAddress.subdustPkScript
|
|
898
|
+
: this.arkAddress.pkScript;
|
|
899
|
+
outputs.push({
|
|
900
|
+
script: changeOutputScript,
|
|
901
|
+
amount: BigInt(selected.changeAmount),
|
|
902
|
+
});
|
|
903
|
+
}
|
|
904
|
+
const { arkTxid, signedCheckpointTxs } = await this.buildAndSubmitOffchainTx(selected.inputs, outputs);
|
|
905
|
+
await this.updateDbAfterOffchainTx(selected.inputs, arkTxid, signedCheckpointTxs, params.amount, selected.changeAmount, selected.changeAmount > 0n ? outputs.length - 1 : 0);
|
|
906
|
+
return arkTxid;
|
|
907
|
+
});
|
|
848
908
|
}
|
|
849
909
|
return this.send({
|
|
850
910
|
address: params.address,
|
|
@@ -852,6 +912,9 @@ class Wallet extends ReadonlyWallet {
|
|
|
852
912
|
});
|
|
853
913
|
}
|
|
854
914
|
async settle(params, eventCallback) {
|
|
915
|
+
return this._withTxLock(() => this._settleImpl(params, eventCallback));
|
|
916
|
+
}
|
|
917
|
+
async _settleImpl(params, eventCallback) {
|
|
855
918
|
if (params?.inputs) {
|
|
856
919
|
for (const input of params.inputs) {
|
|
857
920
|
// validate arknotes inputs
|
|
@@ -1287,23 +1350,29 @@ class Wallet extends ReadonlyWallet {
|
|
|
1287
1350
|
async finalizePendingTxs(vtxos) {
|
|
1288
1351
|
const MAX_INPUTS_PER_INTENT = 20;
|
|
1289
1352
|
if (!vtxos || vtxos.length === 0) {
|
|
1290
|
-
//
|
|
1353
|
+
// Batch all scripts into a single indexer call
|
|
1291
1354
|
const scriptMap = await this.getScriptMap();
|
|
1292
1355
|
const allExtended = [];
|
|
1293
|
-
|
|
1294
|
-
|
|
1295
|
-
|
|
1296
|
-
|
|
1297
|
-
|
|
1298
|
-
|
|
1299
|
-
|
|
1300
|
-
|
|
1301
|
-
|
|
1302
|
-
|
|
1303
|
-
|
|
1304
|
-
|
|
1305
|
-
|
|
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;
|
|
1306
1369
|
}
|
|
1370
|
+
allExtended.push({
|
|
1371
|
+
...vtxo,
|
|
1372
|
+
forfeitTapLeafScript: vtxoScript.forfeit(),
|
|
1373
|
+
intentTapLeafScript: vtxoScript.forfeit(),
|
|
1374
|
+
tapTree: vtxoScript.encode(),
|
|
1375
|
+
});
|
|
1307
1376
|
}
|
|
1308
1377
|
if (allExtended.length === 0) {
|
|
1309
1378
|
return { finalized: [], pending: [] };
|
|
@@ -1353,6 +1422,9 @@ class Wallet extends ReadonlyWallet {
|
|
|
1353
1422
|
* ```
|
|
1354
1423
|
*/
|
|
1355
1424
|
async send(...args) {
|
|
1425
|
+
return this._withTxLock(() => this._sendImpl(...args));
|
|
1426
|
+
}
|
|
1427
|
+
async _sendImpl(...args) {
|
|
1356
1428
|
if (args.length === 0) {
|
|
1357
1429
|
throw new Error("At least one receiver is required");
|
|
1358
1430
|
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.ServiceWorkerTimeoutError = exports.MessageBusNotInitializedError = void 0;
|
|
4
|
+
class MessageBusNotInitializedError extends Error {
|
|
5
|
+
constructor() {
|
|
6
|
+
super("MessageBus not initialized");
|
|
7
|
+
this.name = "MessageBusNotInitializedError";
|
|
8
|
+
}
|
|
9
|
+
}
|
|
10
|
+
exports.MessageBusNotInitializedError = MessageBusNotInitializedError;
|
|
11
|
+
class ServiceWorkerTimeoutError extends Error {
|
|
12
|
+
constructor(detail) {
|
|
13
|
+
super(detail);
|
|
14
|
+
this.name = "ServiceWorkerTimeoutError";
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
exports.ServiceWorkerTimeoutError = ServiceWorkerTimeoutError;
|