@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.
- package/README.md +16 -6
- package/dist/cjs/contracts/arkcontract.js +0 -2
- package/dist/cjs/contracts/contractManager.js +111 -215
- package/dist/cjs/contracts/contractWatcher.js +86 -115
- package/dist/cjs/providers/ark.js +36 -33
- package/dist/cjs/repositories/indexedDB/manager.js +6 -3
- package/dist/cjs/repositories/indexedDB/schema.js +47 -2
- package/dist/cjs/repositories/indexedDB/walletRepository.js +21 -2
- package/dist/cjs/repositories/realm/contractRepository.js +0 -4
- package/dist/cjs/repositories/realm/index.js +3 -1
- package/dist/cjs/repositories/realm/schemas.js +50 -1
- package/dist/cjs/repositories/realm/walletRepository.js +8 -4
- package/dist/cjs/repositories/scriptFromAddress.js +16 -0
- package/dist/cjs/repositories/sqlite/contractRepository.js +2 -6
- package/dist/cjs/repositories/sqlite/walletRepository.js +121 -33
- package/dist/cjs/utils/syncCursors.js +48 -56
- package/dist/cjs/wallet/expo/background.js +0 -13
- package/dist/cjs/wallet/expo/wallet.js +1 -6
- package/dist/cjs/wallet/serviceWorker/wallet-message-handler.js +16 -7
- package/dist/cjs/wallet/serviceWorker/wallet.js +19 -0
- package/dist/cjs/wallet/utils.js +41 -10
- package/dist/cjs/wallet/vtxo-manager.js +222 -40
- package/dist/cjs/wallet/wallet.js +149 -211
- package/dist/cjs/worker/expo/processors/contractPollProcessor.js +9 -13
- package/dist/cjs/worker/expo/taskRunner.js +2 -11
- package/dist/esm/contracts/arkcontract.js +0 -2
- package/dist/esm/contracts/contractManager.js +113 -217
- package/dist/esm/contracts/contractWatcher.js +86 -115
- package/dist/esm/providers/ark.js +36 -33
- package/dist/esm/repositories/indexedDB/manager.js +6 -3
- package/dist/esm/repositories/indexedDB/schema.js +46 -2
- package/dist/esm/repositories/indexedDB/walletRepository.js +21 -2
- package/dist/esm/repositories/realm/contractRepository.js +0 -4
- package/dist/esm/repositories/realm/index.js +1 -1
- package/dist/esm/repositories/realm/schemas.js +48 -0
- package/dist/esm/repositories/realm/walletRepository.js +8 -4
- package/dist/esm/repositories/scriptFromAddress.js +13 -0
- package/dist/esm/repositories/sqlite/contractRepository.js +2 -6
- package/dist/esm/repositories/sqlite/walletRepository.js +121 -33
- package/dist/esm/utils/syncCursors.js +47 -53
- package/dist/esm/wallet/expo/background.js +0 -13
- package/dist/esm/wallet/expo/wallet.js +2 -7
- package/dist/esm/wallet/serviceWorker/wallet-message-handler.js +17 -8
- package/dist/esm/wallet/serviceWorker/wallet.js +19 -0
- package/dist/esm/wallet/utils.js +41 -9
- package/dist/esm/wallet/vtxo-manager.js +222 -40
- package/dist/esm/wallet/wallet.js +152 -214
- package/dist/esm/worker/expo/processors/contractPollProcessor.js +9 -13
- package/dist/esm/worker/expo/taskRunner.js +3 -12
- package/dist/types/contracts/arkcontract.d.ts +0 -2
- package/dist/types/contracts/contractManager.d.ts +38 -9
- package/dist/types/contracts/contractWatcher.d.ts +22 -21
- package/dist/types/contracts/types.d.ts +0 -7
- package/dist/types/repositories/indexedDB/manager.d.ts +5 -2
- package/dist/types/repositories/indexedDB/schema.d.ts +3 -2
- package/dist/types/repositories/realm/index.d.ts +1 -1
- package/dist/types/repositories/realm/schemas.d.ts +41 -0
- package/dist/types/repositories/scriptFromAddress.d.ts +9 -0
- package/dist/types/repositories/serialization.d.ts +1 -1
- package/dist/types/repositories/sqlite/walletRepository.d.ts +22 -0
- package/dist/types/repositories/walletRepository.d.ts +10 -2
- package/dist/types/utils/syncCursors.d.ts +25 -23
- package/dist/types/wallet/index.d.ts +1 -1
- package/dist/types/wallet/serviceWorker/wallet-message-handler.d.ts +15 -3
- package/dist/types/wallet/utils.d.ts +20 -4
- package/dist/types/wallet/vtxo-manager.d.ts +29 -6
- package/dist/types/wallet/wallet.d.ts +8 -17
- package/dist/types/worker/expo/processors/contractPollProcessor.d.ts +9 -4
- package/dist/types/worker/expo/taskRunner.d.ts +6 -3
- package/package.json +1 -1
package/dist/cjs/wallet/utils.js
CHANGED
|
@@ -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.
|
|
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
|
-
//
|
|
697
|
-
//
|
|
698
|
-
|
|
699
|
-
//
|
|
700
|
-
//
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
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.
|
|
801
|
+
await this.runPeriodicSettle(boardingUtxos);
|
|
714
802
|
}
|
|
715
803
|
catch (e) {
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
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
|
|
743
|
-
* Skips UTXOs that are already expired
|
|
744
|
-
*
|
|
745
|
-
*
|
|
746
|
-
*
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
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;
|