@arkade-os/sdk 0.4.17 → 0.4.19

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 (70) hide show
  1. package/README.md +16 -6
  2. package/dist/cjs/contracts/arkcontract.js +0 -2
  3. package/dist/cjs/contracts/contractManager.js +111 -215
  4. package/dist/cjs/contracts/contractWatcher.js +86 -115
  5. package/dist/cjs/providers/ark.js +36 -33
  6. package/dist/cjs/repositories/indexedDB/manager.js +6 -3
  7. package/dist/cjs/repositories/indexedDB/schema.js +47 -2
  8. package/dist/cjs/repositories/indexedDB/walletRepository.js +21 -2
  9. package/dist/cjs/repositories/realm/contractRepository.js +0 -4
  10. package/dist/cjs/repositories/realm/index.js +3 -1
  11. package/dist/cjs/repositories/realm/schemas.js +50 -1
  12. package/dist/cjs/repositories/realm/walletRepository.js +8 -4
  13. package/dist/cjs/repositories/scriptFromAddress.js +16 -0
  14. package/dist/cjs/repositories/sqlite/contractRepository.js +2 -6
  15. package/dist/cjs/repositories/sqlite/walletRepository.js +121 -33
  16. package/dist/cjs/utils/syncCursors.js +48 -56
  17. package/dist/cjs/wallet/expo/background.js +0 -13
  18. package/dist/cjs/wallet/expo/wallet.js +1 -6
  19. package/dist/cjs/wallet/serviceWorker/wallet-message-handler.js +16 -7
  20. package/dist/cjs/wallet/serviceWorker/wallet.js +19 -0
  21. package/dist/cjs/wallet/utils.js +41 -10
  22. package/dist/cjs/wallet/vtxo-manager.js +222 -40
  23. package/dist/cjs/wallet/wallet.js +149 -211
  24. package/dist/cjs/worker/expo/processors/contractPollProcessor.js +9 -13
  25. package/dist/cjs/worker/expo/taskRunner.js +2 -11
  26. package/dist/esm/contracts/arkcontract.js +0 -2
  27. package/dist/esm/contracts/contractManager.js +113 -217
  28. package/dist/esm/contracts/contractWatcher.js +86 -115
  29. package/dist/esm/providers/ark.js +36 -33
  30. package/dist/esm/repositories/indexedDB/manager.js +6 -3
  31. package/dist/esm/repositories/indexedDB/schema.js +46 -2
  32. package/dist/esm/repositories/indexedDB/walletRepository.js +21 -2
  33. package/dist/esm/repositories/realm/contractRepository.js +0 -4
  34. package/dist/esm/repositories/realm/index.js +1 -1
  35. package/dist/esm/repositories/realm/schemas.js +48 -0
  36. package/dist/esm/repositories/realm/walletRepository.js +8 -4
  37. package/dist/esm/repositories/scriptFromAddress.js +13 -0
  38. package/dist/esm/repositories/sqlite/contractRepository.js +2 -6
  39. package/dist/esm/repositories/sqlite/walletRepository.js +121 -33
  40. package/dist/esm/utils/syncCursors.js +47 -53
  41. package/dist/esm/wallet/expo/background.js +0 -13
  42. package/dist/esm/wallet/expo/wallet.js +2 -7
  43. package/dist/esm/wallet/serviceWorker/wallet-message-handler.js +17 -8
  44. package/dist/esm/wallet/serviceWorker/wallet.js +19 -0
  45. package/dist/esm/wallet/utils.js +41 -9
  46. package/dist/esm/wallet/vtxo-manager.js +222 -40
  47. package/dist/esm/wallet/wallet.js +152 -214
  48. package/dist/esm/worker/expo/processors/contractPollProcessor.js +9 -13
  49. package/dist/esm/worker/expo/taskRunner.js +3 -12
  50. package/dist/types/contracts/arkcontract.d.ts +0 -2
  51. package/dist/types/contracts/contractManager.d.ts +38 -9
  52. package/dist/types/contracts/contractWatcher.d.ts +22 -21
  53. package/dist/types/contracts/types.d.ts +0 -7
  54. package/dist/types/repositories/indexedDB/manager.d.ts +5 -2
  55. package/dist/types/repositories/indexedDB/schema.d.ts +3 -2
  56. package/dist/types/repositories/realm/index.d.ts +1 -1
  57. package/dist/types/repositories/realm/schemas.d.ts +41 -0
  58. package/dist/types/repositories/scriptFromAddress.d.ts +9 -0
  59. package/dist/types/repositories/serialization.d.ts +1 -1
  60. package/dist/types/repositories/sqlite/walletRepository.d.ts +22 -0
  61. package/dist/types/repositories/walletRepository.d.ts +10 -2
  62. package/dist/types/utils/syncCursors.d.ts +25 -23
  63. package/dist/types/wallet/index.d.ts +1 -1
  64. package/dist/types/wallet/serviceWorker/wallet-message-handler.d.ts +15 -3
  65. package/dist/types/wallet/utils.d.ts +20 -4
  66. package/dist/types/wallet/vtxo-manager.d.ts +29 -6
  67. package/dist/types/wallet/wallet.d.ts +8 -17
  68. package/dist/types/worker/expo/processors/contractPollProcessor.d.ts +9 -4
  69. package/dist/types/worker/expo/taskRunner.d.ts +6 -3
  70. package/package.json +1 -1
@@ -1,9 +1,8 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.DUST_AMOUNT = void 0;
4
- exports.extendVirtualCoin = extendVirtualCoin;
5
4
  exports.extendCoin = extendCoin;
6
- exports.extendVtxoFromContract = extendVtxoFromContract;
5
+ exports.extendVirtualCoinForContract = extendVirtualCoinForContract;
7
6
  exports.getRandomId = getRandomId;
8
7
  exports.isValidArkAddress = isValidArkAddress;
9
8
  exports.validateRecipients = validateRecipients;
@@ -11,14 +10,6 @@ const __1 = require("..");
11
10
  const handlers_1 = require("../contracts/handlers");
12
11
  const base_1 = require("@scure/base");
13
12
  exports.DUST_AMOUNT = 546; // sats
14
- function extendVirtualCoin(wallet, vtxo) {
15
- return {
16
- ...vtxo,
17
- forfeitTapLeafScript: wallet.offchainTapscript.forfeit(),
18
- intentTapLeafScript: wallet.offchainTapscript.forfeit(),
19
- tapTree: wallet.offchainTapscript.encode(),
20
- };
21
- }
22
13
  function extendCoin(wallet, utxo) {
23
14
  return {
24
15
  ...utxo,
@@ -40,6 +31,46 @@ function extendVtxoFromContract(vtxo, contract) {
40
31
  tapTree: script.encode(),
41
32
  };
42
33
  }
34
+ /**
35
+ * Extend a VirtualCoin with the tap scripts of whichever contract locks it.
36
+ *
37
+ * The second argument accepts either form, so each callsite passes what it
38
+ * already has:
39
+ * - a single `Contract` (when the caller already knows the owning contract,
40
+ * e.g. the contract manager iterating its own `scriptToContract` map), or
41
+ * - a `ReadonlyMap<script, Contract>` (when the caller resolves by
42
+ * `vtxo.script`, populated by the indexer).
43
+ *
44
+ * Throws when no contract can be resolved — there is intentionally no
45
+ * default-tapscript fallback. When the wallet owns multiple contracts
46
+ * (default + delegate, several active vHTLCs, etc.) a default-tapscript path
47
+ * silently stamps every VTXO with the same forfeit/intent data, overwriting
48
+ * the correct data for any VTXO locked to a non-default contract. Callers
49
+ * must feed a Contract or a populated script→Contract map; otherwise the
50
+ * caller (typically `ContractManager.annotateVtxos`) should fetch the owning
51
+ * contract first.
52
+ */
53
+ function extendVirtualCoinForContract(vtxo, contractOrMap) {
54
+ const contract = resolveContract(vtxo, contractOrMap);
55
+ if (!contract) {
56
+ throw new Error("extendVirtualCoinForContract: no contract matched vtxo.script — callers must resolve the owning contract before annotating");
57
+ }
58
+ return extendVtxoFromContract(vtxo, contract);
59
+ }
60
+ function isContractMap(value) {
61
+ // A `Contract` is a plain object with a string `type`. `ReadonlyMap` is
62
+ // an interface so `instanceof Map` is not enough to narrow it — but a
63
+ // contract has no `get` method, so duck-typing on that is unambiguous.
64
+ return typeof value.get === "function";
65
+ }
66
+ function resolveContract(vtxo, contractOrMap) {
67
+ if (!contractOrMap)
68
+ return undefined;
69
+ if (isContractMap(contractOrMap)) {
70
+ return contractOrMap.get(vtxo.script);
71
+ }
72
+ return contractOrMap;
73
+ }
43
74
  function getRandomId() {
44
75
  const randomValue = crypto.getRandomValues(new Uint8Array(16));
45
76
  return base_1.hex.encode(randomValue);
@@ -32,6 +32,38 @@ function assertSweepCapable(wallet) {
32
32
  throw new Error("Boarding UTXO sweep requires a Wallet instance with boardingTapscript, onchainProvider, and network");
33
33
  }
34
34
  }
35
+ /**
36
+ * Web Locks name used to serialize boarding-poll work across same-origin
37
+ * browser contexts (tabs, service worker). Static because the goal is to
38
+ * deduplicate polls for the *same* wallet — two distinct wallets on the
39
+ * same origin will take turns, which is acceptable.
40
+ */
41
+ const BOARDING_POLL_LOCK_NAME = "arkade-boarding-poll";
42
+ /**
43
+ * Run `fn` under an exclusive Web Lock when the runtime provides one
44
+ * (browser main thread, service worker). In environments without
45
+ * `navigator.locks` (Node, React Native) the callback runs immediately
46
+ * with no coordination.
47
+ *
48
+ * Uses `ifAvailable: true`: if another context already holds the lock,
49
+ * skip this cycle entirely rather than queueing — the other context will
50
+ * do the work and the next poll will re-check.
51
+ */
52
+ async function runWithCrossInstanceLock(name, fn) {
53
+ const locks = typeof globalThis !== "undefined" &&
54
+ typeof globalThis.navigator !== "undefined"
55
+ ? globalThis.navigator.locks
56
+ : undefined;
57
+ if (!locks) {
58
+ await fn();
59
+ return;
60
+ }
61
+ await locks.request(name, { ifAvailable: true, mode: "exclusive" }, async (lock) => {
62
+ if (lock === null)
63
+ return;
64
+ await fn();
65
+ });
66
+ }
35
67
  /** Default renewal threshold in seconds (3 days). */
36
68
  exports.DEFAULT_THRESHOLD_SECONDS = 3 * 24 * 60 * 60;
37
69
  /**
@@ -191,6 +223,20 @@ class VtxoManager {
191
223
  // server emits new VTXOs → vtxo_received → renewVtxos() again → infinite loop.
192
224
  this.renewalInProgress = false;
193
225
  this.lastRenewalTimestamp = 0;
226
+ // Guards against a retry treadmill on the periodic-settle path: a failing
227
+ // settle would otherwise re-submit identical intents on every 60s poll,
228
+ // producing per-minute DeleteIntent RPCs forever. Mirrors the renewal
229
+ // cooldown but with exponential backoff on consecutive failures, so a
230
+ // persistently broken input eventually drops to the backoff cap instead
231
+ // of hammering the server. Shared across boarding + expiring-VTXO work
232
+ // because they now ride on the same settle intent.
233
+ this.lastPeriodicSettleTimestamp = 0;
234
+ this.consecutivePeriodicSettleFailures = 0;
235
+ // Throttle for the VTXO_ALREADY_SPENT -> refreshVtxos() reconciliation.
236
+ // The server's authoritative view says our local cache is stale, so we
237
+ // trigger a full refresh to advance the global sync cursor. Rate-limit
238
+ // to guard against a buggy indexer cycling us into a refresh storm.
239
+ this.lastVtxoSpentRefreshTimestamp = 0;
194
240
  // Normalize: prefer settlementConfig, fall back to renewalConfig, default to enabled
195
241
  if (settlementConfig !== undefined) {
196
242
  this.settlementConfig = settlementConfig;
@@ -412,10 +458,15 @@ class VtxoManager {
412
458
  },
413
459
  ],
414
460
  }, eventCallback);
415
- this.lastRenewalTimestamp = Date.now();
416
461
  return txid;
417
462
  }
418
463
  finally {
464
+ // Update cooldown on EVERY attempt (success or failure) so transient
465
+ // settle failures (stream close, connector mismatch, duplicated input)
466
+ // don't allow the next vtxo_received event to re-enter renewal
467
+ // immediately. Without this, a failed settle leaves lastRenewalTimestamp
468
+ // at its previous value and the cooldown check becomes a no-op.
469
+ this.lastRenewalTimestamp = Date.now();
419
470
  this.renewalInProgress = false;
420
471
  }
421
472
  }
@@ -621,7 +672,6 @@ class VtxoManager {
621
672
  return;
622
673
  }
623
674
  if (e.message.includes("VTXO_ALREADY_REGISTERED") ||
624
- e.message.includes("VTXO_ALREADY_SPENT") ||
625
675
  e.message.includes("duplicated input")) {
626
676
  // Virtual output is already being used in a concurrent
627
677
  // user-initiated operation. Skip silently — the
@@ -629,6 +679,14 @@ class VtxoManager {
629
679
  // renewal will retry on the next cycle.
630
680
  return;
631
681
  }
682
+ if (e.message.includes("VTXO_ALREADY_SPENT")) {
683
+ // Our local VTXO cache is stale vs. the
684
+ // server's authoritative view. Trigger a
685
+ // throttled refresh to reconcile, then skip
686
+ // — the next cycle will see fresh data.
687
+ void this.maybeRefreshAfterVtxoSpent();
688
+ return;
689
+ }
632
690
  }
633
691
  console.error("Error renewing VTXOs:", e);
634
692
  });
@@ -646,6 +704,39 @@ class VtxoManager {
646
704
  return undefined;
647
705
  }
648
706
  }
707
+ /**
708
+ * VTXO_ALREADY_SPENT means the server's authoritative view of VTXO state
709
+ * is ahead of ours — cross-instance race, pre-lock snapshot drift, or an
710
+ * SSE gap left stale data in the local cache. Silent-swallowing guarantees
711
+ * the same error on the next cycle because nothing reconciles the cache,
712
+ * so instead we trigger a full refreshVtxos() to advance the global sync
713
+ * cursor. Throttled to prevent a buggy indexer from causing a refresh
714
+ * storm.
715
+ */
716
+ maybeRefreshAfterVtxoSpent() {
717
+ if (this.vtxoSpentRefreshPromise) {
718
+ return this.vtxoSpentRefreshPromise;
719
+ }
720
+ const now = Date.now();
721
+ if (now - this.lastVtxoSpentRefreshTimestamp <
722
+ VtxoManager.VTXO_SPENT_REFRESH_COOLDOWN_MS) {
723
+ return Promise.resolve();
724
+ }
725
+ this.lastVtxoSpentRefreshTimestamp = now;
726
+ this.vtxoSpentRefreshPromise = (async () => {
727
+ try {
728
+ const contractManager = await this.wallet.getContractManager();
729
+ await contractManager.refreshVtxos();
730
+ }
731
+ catch (e) {
732
+ console.error("Error refreshing VTXOs after VTXO_ALREADY_SPENT:", e);
733
+ }
734
+ finally {
735
+ this.vtxoSpentRefreshPromise = undefined;
736
+ }
737
+ })();
738
+ return this.vtxoSpentRefreshPromise;
739
+ }
649
740
  /** Computes the next poll delay, applying exponential backoff on failures. */
650
741
  getNextPollDelay() {
651
742
  if (this.settlementConfig === false)
@@ -693,33 +784,42 @@ class VtxoManager {
693
784
  this.pollDone = { promise, resolve: resolve };
694
785
  let hadError = false;
695
786
  try {
696
- // Fetch boarding inputs once for the entire poll cycle so that
697
- // settle and sweep don't each hit the network independently.
698
- const boardingUtxos = await this.wallet.getBoardingUtxos();
699
- // Settle new (unexpired) boarding inputs first, then sweep expired ones.
700
- // Sequential to avoid racing for the same inputs.
701
- try {
702
- await this.settleBoardingUtxos(boardingUtxos);
703
- }
704
- catch (e) {
705
- hadError = true;
706
- console.error("Error auto-settling boarding UTXOs:", e);
707
- }
708
- const sweepEnabled = this.settlementConfig !== false &&
709
- (this.settlementConfig?.boardingUtxoSweep ??
710
- exports.DEFAULT_SETTLEMENT_CONFIG.boardingUtxoSweep);
711
- if (sweepEnabled) {
787
+ // Cross-instance guard: in browser / service worker environments,
788
+ // serialize the poll body across tabs and SW contexts so only one
789
+ // of them registers intents per interval. Without this, every tab
790
+ // submits a parallel RegisterIntent for the same boarding input
791
+ // and N-1 of them collide on the server's duplicated-input check,
792
+ // each producing a DeleteIntent RPC. No-op outside the browser.
793
+ await runWithCrossInstanceLock(BOARDING_POLL_LOCK_NAME, async () => {
794
+ // Fetch boarding inputs once for the entire poll cycle so that
795
+ // settle and sweep don't each hit the network independently.
796
+ const boardingUtxos = await this.wallet.getBoardingUtxos();
797
+ // Settle new (unexpired) boarding inputs + any near-expiry
798
+ // VTXOs in a single intent, then sweep expired boarding
799
+ // inputs. Sequential to avoid racing for the same inputs.
712
800
  try {
713
- await this.sweepExpiredBoardingUtxos(boardingUtxos);
801
+ await this.runPeriodicSettle(boardingUtxos);
714
802
  }
715
803
  catch (e) {
716
- if (!(e instanceof Error) ||
717
- !e.message.includes("No expired boarding UTXOs")) {
718
- hadError = true;
719
- console.error("Error auto-sweeping boarding UTXOs:", e);
804
+ hadError = true;
805
+ console.error("Error during periodic settle:", e);
806
+ }
807
+ const sweepEnabled = this.settlementConfig !== false &&
808
+ (this.settlementConfig?.boardingUtxoSweep ??
809
+ exports.DEFAULT_SETTLEMENT_CONFIG.boardingUtxoSweep);
810
+ if (sweepEnabled) {
811
+ try {
812
+ await this.sweepExpiredBoardingUtxos(boardingUtxos);
813
+ }
814
+ catch (e) {
815
+ if (!(e instanceof Error) ||
816
+ !e.message.includes("No expired boarding UTXOs")) {
817
+ hadError = true;
818
+ console.error("Error auto-sweeping boarding UTXOs:", e);
819
+ }
720
820
  }
721
821
  }
722
- }
822
+ });
723
823
  }
724
824
  catch (e) {
725
825
  hadError = true;
@@ -739,13 +839,19 @@ class VtxoManager {
739
839
  }
740
840
  }
741
841
  /**
742
- * Auto-settle new (unexpired) boarding inputs into Arkade.
743
- * Skips UTXOs that are already expired (those are handled by sweep).
744
- * Only settles UTXOs not already in-flight (tracked in knownBoardingUtxos).
745
- * UTXOs are marked as known only after a successful settle, so failed
746
- * attempts will be retried on the next poll.
842
+ * Auto-settle new (unexpired) boarding inputs AND near-expiry VTXOs into
843
+ * Arkade in a single intent. Skips boarding UTXOs that are already expired
844
+ * (those are handled by sweep) and those already in-flight (tracked in
845
+ * knownBoardingUtxos). If the event-driven renewal path is currently
846
+ * running, VTXOs are omitted from this cycle to avoid double-spending.
847
+ *
848
+ * Failure bookkeeping: after every settle *attempt*, lastPeriodicSettleTimestamp
849
+ * is armed and consecutive failures are counted so the next attempt is
850
+ * blocked by an exponentially growing cooldown (capped). This stops a
851
+ * persistently failing input from producing identical RegisterIntent +
852
+ * DeleteIntent retries on every 60s poll.
747
853
  */
748
- async settleBoardingUtxos(boardingUtxos) {
854
+ async runPeriodicSettle(boardingUtxos) {
749
855
  // Exclude expired boarding inputs — those should be swept, not settled.
750
856
  // If we can't determine expired status, bail out entirely to avoid
751
857
  // accidentally settling expired inputs (which would conflict with sweep).
@@ -763,22 +869,95 @@ class VtxoManager {
763
869
  catch (e) {
764
870
  throw e instanceof Error ? e : new Error(String(e));
765
871
  }
766
- const unsettledUtxos = boardingUtxos.filter((u) => !this.knownBoardingUtxos.has(`${u.txid}:${u.vout}`) &&
872
+ const unsettledBoarding = boardingUtxos.filter((u) => u.status.confirmed &&
873
+ !this.knownBoardingUtxos.has(`${u.txid}:${u.vout}`) &&
767
874
  !expiredSet.has(`${u.txid}:${u.vout}`));
768
- if (unsettledUtxos.length === 0)
875
+ // Collect near-expiry VTXOs unless the event-driven path is mid-renewal.
876
+ // Skipping when renewalInProgress avoids double-submitting the same VTXOs.
877
+ let expiringVtxos = [];
878
+ if (!this.renewalInProgress) {
879
+ try {
880
+ expiringVtxos = await this.getExpiringVtxos();
881
+ }
882
+ catch (e) {
883
+ // Non-fatal: fall back to boarding-only settle.
884
+ console.error("Error fetching expiring VTXOs:", e);
885
+ }
886
+ }
887
+ if (unsettledBoarding.length === 0 && expiringVtxos.length === 0) {
769
888
  return;
889
+ }
890
+ // Respect the cooldown armed by the previous attempt. Cooldown grows
891
+ // exponentially with consecutive failures and is capped by
892
+ // PERIODIC_SETTLE_MAX_BACKOFF_MS.
893
+ const cooldownMs = Math.min(VtxoManager.PERIODIC_SETTLE_COOLDOWN_MS *
894
+ Math.pow(2, this.consecutivePeriodicSettleFailures), VtxoManager.PERIODIC_SETTLE_MAX_BACKOFF_MS);
895
+ if (Date.now() - this.lastPeriodicSettleTimestamp < cooldownMs) {
896
+ return;
897
+ }
770
898
  const dustAmount = getDustAmount(this.wallet);
771
- const totalAmount = unsettledUtxos.reduce((sum, u) => sum + BigInt(u.value), 0n);
899
+ const boardingTotal = unsettledBoarding.reduce((sum, u) => sum + BigInt(u.value), 0n);
900
+ const vtxoTotal = expiringVtxos.reduce((sum, v) => sum + BigInt(v.value), 0n);
901
+ const totalAmount = boardingTotal + vtxoTotal;
772
902
  if (totalAmount < dustAmount)
773
903
  return;
774
904
  const arkAddress = await this.wallet.getAddress();
775
- await this.wallet.settle({
776
- inputs: unsettledUtxos,
777
- outputs: [{ address: arkAddress, amount: totalAmount }],
778
- });
779
- // Mark as known only after successful settle
780
- for (const u of unsettledUtxos) {
781
- this.knownBoardingUtxos.add(`${u.txid}:${u.vout}`);
905
+ const includesVtxos = expiringVtxos.length > 0;
906
+ // Block the event-driven renewal path while this settle is in flight
907
+ // when VTXOs are part of the intent. Mirrors renewVtxos()'s guard so
908
+ // the two paths can't race on the same VTXO inputs.
909
+ if (includesVtxos) {
910
+ this.renewalInProgress = true;
911
+ }
912
+ let success = false;
913
+ let staleCacheSkip = false;
914
+ try {
915
+ try {
916
+ await this.wallet.settle({
917
+ inputs: [...unsettledBoarding, ...expiringVtxos],
918
+ outputs: [{ address: arkAddress, amount: totalAmount }],
919
+ });
920
+ // Mark boarding inputs as known only after successful settle.
921
+ for (const u of unsettledBoarding) {
922
+ this.knownBoardingUtxos.add(`${u.txid}:${u.vout}`);
923
+ }
924
+ success = true;
925
+ }
926
+ catch (e) {
927
+ if (e instanceof Error &&
928
+ e.message.includes("VTXO_ALREADY_SPENT")) {
929
+ // Local VTXO cache is stale vs. the server's
930
+ // authoritative view — not a transient failure.
931
+ // Trigger a throttled refresh and skip this cycle
932
+ // without bumping the failure counter, so the next
933
+ // poll can retry once the cache reconciles.
934
+ staleCacheSkip = true;
935
+ void this.maybeRefreshAfterVtxoSpent();
936
+ }
937
+ else {
938
+ throw e;
939
+ }
940
+ }
941
+ }
942
+ finally {
943
+ this.lastPeriodicSettleTimestamp = Date.now();
944
+ if (includesVtxos) {
945
+ // Match event-path semantics: bump the renewal cooldown
946
+ // whether we succeeded or failed so a failed periodic settle
947
+ // doesn't let the next vtxo_received event re-enter renewal
948
+ // immediately.
949
+ this.lastRenewalTimestamp = Date.now();
950
+ this.renewalInProgress = false;
951
+ }
952
+ if (success) {
953
+ this.consecutivePeriodicSettleFailures = 0;
954
+ }
955
+ else if (!staleCacheSkip) {
956
+ // Don't bump on stale-cache skip: it's not a transient
957
+ // failure, and the next cycle should try immediately
958
+ // after the refresh lands.
959
+ this.consecutivePeriodicSettleFailures++;
960
+ }
782
961
  }
783
962
  }
784
963
  async dispose() {
@@ -812,3 +991,6 @@ class VtxoManager {
812
991
  exports.VtxoManager = VtxoManager;
813
992
  VtxoManager.MAX_BACKOFF_MS = 5 * 60 * 1000; // 5 minutes
814
993
  VtxoManager.RENEWAL_COOLDOWN_MS = 30000; // 30 seconds
994
+ VtxoManager.PERIODIC_SETTLE_COOLDOWN_MS = 30000;
995
+ VtxoManager.PERIODIC_SETTLE_MAX_BACKOFF_MS = 5 * 60 * 1000;
996
+ VtxoManager.VTXO_SPENT_REFRESH_COOLDOWN_MS = 30000;