@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.
Files changed (39) hide show
  1. package/dist/cjs/contracts/contractManager.js +59 -11
  2. package/dist/cjs/contracts/contractWatcher.js +21 -2
  3. package/dist/cjs/identity/seedIdentity.js +2 -2
  4. package/dist/cjs/index.js +9 -2
  5. package/dist/cjs/providers/expoIndexer.js +1 -0
  6. package/dist/cjs/providers/indexer.js +1 -0
  7. package/dist/cjs/utils/transactionHistory.js +2 -1
  8. package/dist/cjs/wallet/serviceWorker/wallet-message-handler.js +249 -36
  9. package/dist/cjs/wallet/serviceWorker/wallet.js +286 -34
  10. package/dist/cjs/wallet/vtxo-manager.js +123 -86
  11. package/dist/cjs/wallet/wallet.js +140 -68
  12. package/dist/cjs/worker/errors.js +17 -0
  13. package/dist/cjs/worker/messageBus.js +14 -2
  14. package/dist/esm/contracts/contractManager.js +59 -11
  15. package/dist/esm/contracts/contractWatcher.js +21 -2
  16. package/dist/esm/identity/seedIdentity.js +2 -2
  17. package/dist/esm/index.js +3 -2
  18. package/dist/esm/providers/expoIndexer.js +1 -0
  19. package/dist/esm/providers/indexer.js +1 -0
  20. package/dist/esm/utils/transactionHistory.js +2 -1
  21. package/dist/esm/wallet/serviceWorker/wallet-message-handler.js +245 -35
  22. package/dist/esm/wallet/serviceWorker/wallet.js +286 -34
  23. package/dist/esm/wallet/vtxo-manager.js +123 -86
  24. package/dist/esm/wallet/wallet.js +140 -68
  25. package/dist/esm/worker/errors.js +12 -0
  26. package/dist/esm/worker/messageBus.js +14 -2
  27. package/dist/types/contracts/contractManager.d.ts +10 -0
  28. package/dist/types/identity/seedIdentity.d.ts +5 -2
  29. package/dist/types/index.d.ts +5 -4
  30. package/dist/types/repositories/serialization.d.ts +1 -0
  31. package/dist/types/utils/transactionHistory.d.ts +1 -1
  32. package/dist/types/wallet/index.d.ts +2 -0
  33. package/dist/types/wallet/serviceWorker/wallet-message-handler.d.ts +101 -7
  34. package/dist/types/wallet/serviceWorker/wallet.d.ts +16 -0
  35. package/dist/types/wallet/vtxo-manager.d.ts +29 -2
  36. package/dist/types/wallet/wallet.d.ts +10 -0
  37. package/dist/types/worker/errors.d.ts +6 -0
  38. package/dist/types/worker/messageBus.d.ts +6 -0
  39. 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
- // Get all VTXOs (including recoverable ones)
381
- // Use default threshold to bypass settlementConfig gate (manual API should always work)
382
- const vtxos = await this.getExpiringVtxos(this.settlementConfig !== false &&
383
- this.settlementConfig?.vtxoThreshold !== undefined
384
- ? this.settlementConfig.vtxoThreshold * 1000
385
- : exports.DEFAULT_RENEWAL_CONFIG.thresholdMs);
386
- if (vtxos.length === 0) {
387
- throw new Error("No VTXOs available to renew");
388
- }
389
- const totalAmount = vtxos.reduce((sum, vtxo) => sum + vtxo.value, 0);
390
- // Get dust amount from wallet
391
- const dustAmount = getDustAmount(this.wallet);
392
- // Check if total amount is above dust threshold
393
- if (BigInt(totalAmount) < dustAmount) {
394
- throw new Error(`Total amount ${totalAmount} is below dust threshold ${dustAmount}`);
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(() => this.startBoardingUtxoPoll(), 1000);
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
- this.renewVtxos().catch((e) => {
585
- if (e instanceof Error) {
586
- if (e.message.includes("No VTXOs available to renew")) {
587
- // Not an error, just no VTXO eligible for renewal.
588
- return;
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
- if (e.message.includes("is below dust threshold")) {
591
- // Not an error, just below dust threshold.
592
- // As more VTXOs are received, the threshold will be raised.
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
- const intervalMs = this.settlementConfig.pollIntervalMs ??
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
- this.pollIntervalId = setInterval(() => this.pollBoardingUtxos(), intervalMs);
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 expired = await this.getExpiredBoardingUtxos();
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
- return;
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
- if (this.pollIntervalId) {
702
- clearInterval(this.pollIntervalId);
703
- this.pollIntervalId = undefined;
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
- // Query each script separately so we can extend VTXOs with the correct tapscript
251
- for (const [scriptHex, vtxoScript] of scriptMap) {
252
- const response = await this.indexerProvider.getVtxos({
253
- scripts: [scriptHex],
254
- });
255
- let vtxos = response.vtxos.filter(_1.isSpendable);
256
- if (!f.withRecoverable) {
257
- vtxos = vtxos.filter((vtxo) => !(0, _1.isRecoverable)(vtxo) && !(0, _1.isExpired)(vtxo));
258
- }
259
- if (f.withUnrolled) {
260
- const spentVtxos = response.vtxos.filter((vtxo) => !(0, _1.isSpendable)(vtxo));
261
- vtxos.push(...spentVtxos.filter((vtxo) => vtxo.isUnrolled));
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
- for (const vtxo of vtxos) {
264
- allExtended.push({
265
- ...vtxo,
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() || 0);
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
- const selectedVtxoSum = params.selectedVtxos
815
- .map((v) => v.value)
816
- .reduce((a, b) => a + b, 0);
817
- if (selectedVtxoSum < params.amount) {
818
- throw new Error("Selected VTXOs do not cover specified amount");
819
- }
820
- const changeAmount = selectedVtxoSum - params.amount;
821
- const selected = {
822
- inputs: params.selectedVtxos,
823
- changeAmount: BigInt(changeAmount),
824
- };
825
- const outputAddress = address_1.ArkAddress.decode(params.address);
826
- const outputScript = BigInt(params.amount) < this.dustAmount
827
- ? outputAddress.subdustPkScript
828
- : outputAddress.pkScript;
829
- const outputs = [
830
- {
831
- script: outputScript,
832
- amount: BigInt(params.amount),
833
- },
834
- ];
835
- // add change output if needed
836
- if (selected.changeAmount > 0n) {
837
- const changeOutputScript = selected.changeAmount < this.dustAmount
838
- ? this.arkAddress.subdustPkScript
839
- : this.arkAddress.pkScript;
840
- outputs.push({
841
- script: changeOutputScript,
842
- amount: BigInt(selected.changeAmount),
843
- });
844
- }
845
- const { arkTxid, signedCheckpointTxs } = await this.buildAndSubmitOffchainTx(selected.inputs, outputs);
846
- await this.updateDbAfterOffchainTx(selected.inputs, arkTxid, signedCheckpointTxs, params.amount, selected.changeAmount, selected.changeAmount > 0n ? outputs.length - 1 : 0);
847
- return arkTxid;
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
- // Query per-script so each VTXO is extended with the correct tapscript
1353
+ // Batch all scripts into a single indexer call
1291
1354
  const scriptMap = await this.getScriptMap();
1292
1355
  const allExtended = [];
1293
- for (const [scriptHex, vtxoScript] of scriptMap) {
1294
- const { vtxos: fetchedVtxos } = await this.indexerProvider.getVtxos({
1295
- scripts: [scriptHex],
1296
- });
1297
- const pending = fetchedVtxos.filter((vtxo) => vtxo.virtualStatus.state !== "swept" &&
1298
- vtxo.virtualStatus.state !== "settled");
1299
- for (const vtxo of pending) {
1300
- allExtended.push({
1301
- ...vtxo,
1302
- forfeitTapLeafScript: vtxoScript.forfeit(),
1303
- intentTapLeafScript: vtxoScript.forfeit(),
1304
- tapTree: vtxoScript.encode(),
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;