@arkade-os/sdk 0.4.16 → 0.4.18
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 -199
- package/dist/cjs/contracts/contractWatcher.js +86 -115
- 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 +153 -39
- package/dist/cjs/wallet/wallet.js +84 -202
- 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 -201
- package/dist/esm/contracts/contractWatcher.js +86 -115
- 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 +153 -39
- package/dist/esm/wallet/wallet.js +87 -205
- 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 -12
- 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 +16 -6
- package/dist/types/wallet/wallet.d.ts +5 -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
|
@@ -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,15 @@ 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;
|
|
194
235
|
// Normalize: prefer settlementConfig, fall back to renewalConfig, default to enabled
|
|
195
236
|
if (settlementConfig !== undefined) {
|
|
196
237
|
this.settlementConfig = settlementConfig;
|
|
@@ -412,10 +453,15 @@ class VtxoManager {
|
|
|
412
453
|
},
|
|
413
454
|
],
|
|
414
455
|
}, eventCallback);
|
|
415
|
-
this.lastRenewalTimestamp = Date.now();
|
|
416
456
|
return txid;
|
|
417
457
|
}
|
|
418
458
|
finally {
|
|
459
|
+
// Update cooldown on EVERY attempt (success or failure) so transient
|
|
460
|
+
// settle failures (stream close, connector mismatch, duplicated input)
|
|
461
|
+
// don't allow the next vtxo_received event to re-enter renewal
|
|
462
|
+
// immediately. Without this, a failed settle leaves lastRenewalTimestamp
|
|
463
|
+
// at its previous value and the cooldown check becomes a no-op.
|
|
464
|
+
this.lastRenewalTimestamp = Date.now();
|
|
419
465
|
this.renewalInProgress = false;
|
|
420
466
|
}
|
|
421
467
|
}
|
|
@@ -693,33 +739,42 @@ class VtxoManager {
|
|
|
693
739
|
this.pollDone = { promise, resolve: resolve };
|
|
694
740
|
let hadError = false;
|
|
695
741
|
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) {
|
|
742
|
+
// Cross-instance guard: in browser / service worker environments,
|
|
743
|
+
// serialize the poll body across tabs and SW contexts so only one
|
|
744
|
+
// of them registers intents per interval. Without this, every tab
|
|
745
|
+
// submits a parallel RegisterIntent for the same boarding input
|
|
746
|
+
// and N-1 of them collide on the server's duplicated-input check,
|
|
747
|
+
// each producing a DeleteIntent RPC. No-op outside the browser.
|
|
748
|
+
await runWithCrossInstanceLock(BOARDING_POLL_LOCK_NAME, async () => {
|
|
749
|
+
// Fetch boarding inputs once for the entire poll cycle so that
|
|
750
|
+
// settle and sweep don't each hit the network independently.
|
|
751
|
+
const boardingUtxos = await this.wallet.getBoardingUtxos();
|
|
752
|
+
// Settle new (unexpired) boarding inputs + any near-expiry
|
|
753
|
+
// VTXOs in a single intent, then sweep expired boarding
|
|
754
|
+
// inputs. Sequential to avoid racing for the same inputs.
|
|
712
755
|
try {
|
|
713
|
-
await this.
|
|
756
|
+
await this.runPeriodicSettle(boardingUtxos);
|
|
714
757
|
}
|
|
715
758
|
catch (e) {
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
759
|
+
hadError = true;
|
|
760
|
+
console.error("Error during periodic settle:", e);
|
|
761
|
+
}
|
|
762
|
+
const sweepEnabled = this.settlementConfig !== false &&
|
|
763
|
+
(this.settlementConfig?.boardingUtxoSweep ??
|
|
764
|
+
exports.DEFAULT_SETTLEMENT_CONFIG.boardingUtxoSweep);
|
|
765
|
+
if (sweepEnabled) {
|
|
766
|
+
try {
|
|
767
|
+
await this.sweepExpiredBoardingUtxos(boardingUtxos);
|
|
768
|
+
}
|
|
769
|
+
catch (e) {
|
|
770
|
+
if (!(e instanceof Error) ||
|
|
771
|
+
!e.message.includes("No expired boarding UTXOs")) {
|
|
772
|
+
hadError = true;
|
|
773
|
+
console.error("Error auto-sweeping boarding UTXOs:", e);
|
|
774
|
+
}
|
|
720
775
|
}
|
|
721
776
|
}
|
|
722
|
-
}
|
|
777
|
+
});
|
|
723
778
|
}
|
|
724
779
|
catch (e) {
|
|
725
780
|
hadError = true;
|
|
@@ -739,13 +794,19 @@ class VtxoManager {
|
|
|
739
794
|
}
|
|
740
795
|
}
|
|
741
796
|
/**
|
|
742
|
-
* Auto-settle new (unexpired) boarding inputs into
|
|
743
|
-
* Skips UTXOs that are already expired
|
|
744
|
-
*
|
|
745
|
-
*
|
|
746
|
-
*
|
|
797
|
+
* Auto-settle new (unexpired) boarding inputs AND near-expiry VTXOs into
|
|
798
|
+
* Arkade in a single intent. Skips boarding UTXOs that are already expired
|
|
799
|
+
* (those are handled by sweep) and those already in-flight (tracked in
|
|
800
|
+
* knownBoardingUtxos). If the event-driven renewal path is currently
|
|
801
|
+
* running, VTXOs are omitted from this cycle to avoid double-spending.
|
|
802
|
+
*
|
|
803
|
+
* Failure bookkeeping: after every settle *attempt*, lastPeriodicSettleTimestamp
|
|
804
|
+
* is armed and consecutive failures are counted so the next attempt is
|
|
805
|
+
* blocked by an exponentially growing cooldown (capped). This stops a
|
|
806
|
+
* persistently failing input from producing identical RegisterIntent +
|
|
807
|
+
* DeleteIntent retries on every 60s poll.
|
|
747
808
|
*/
|
|
748
|
-
async
|
|
809
|
+
async runPeriodicSettle(boardingUtxos) {
|
|
749
810
|
// Exclude expired boarding inputs — those should be swept, not settled.
|
|
750
811
|
// If we can't determine expired status, bail out entirely to avoid
|
|
751
812
|
// accidentally settling expired inputs (which would conflict with sweep).
|
|
@@ -763,22 +824,73 @@ class VtxoManager {
|
|
|
763
824
|
catch (e) {
|
|
764
825
|
throw e instanceof Error ? e : new Error(String(e));
|
|
765
826
|
}
|
|
766
|
-
const
|
|
827
|
+
const unsettledBoarding = boardingUtxos.filter((u) => !this.knownBoardingUtxos.has(`${u.txid}:${u.vout}`) &&
|
|
767
828
|
!expiredSet.has(`${u.txid}:${u.vout}`));
|
|
768
|
-
|
|
829
|
+
// Collect near-expiry VTXOs unless the event-driven path is mid-renewal.
|
|
830
|
+
// Skipping when renewalInProgress avoids double-submitting the same VTXOs.
|
|
831
|
+
let expiringVtxos = [];
|
|
832
|
+
if (!this.renewalInProgress) {
|
|
833
|
+
try {
|
|
834
|
+
expiringVtxos = await this.getExpiringVtxos();
|
|
835
|
+
}
|
|
836
|
+
catch (e) {
|
|
837
|
+
// Non-fatal: fall back to boarding-only settle.
|
|
838
|
+
console.error("Error fetching expiring VTXOs:", e);
|
|
839
|
+
}
|
|
840
|
+
}
|
|
841
|
+
if (unsettledBoarding.length === 0 && expiringVtxos.length === 0) {
|
|
842
|
+
return;
|
|
843
|
+
}
|
|
844
|
+
// Respect the cooldown armed by the previous attempt. Cooldown grows
|
|
845
|
+
// exponentially with consecutive failures and is capped by
|
|
846
|
+
// PERIODIC_SETTLE_MAX_BACKOFF_MS.
|
|
847
|
+
const cooldownMs = Math.min(VtxoManager.PERIODIC_SETTLE_COOLDOWN_MS *
|
|
848
|
+
Math.pow(2, this.consecutivePeriodicSettleFailures), VtxoManager.PERIODIC_SETTLE_MAX_BACKOFF_MS);
|
|
849
|
+
if (Date.now() - this.lastPeriodicSettleTimestamp < cooldownMs) {
|
|
769
850
|
return;
|
|
851
|
+
}
|
|
770
852
|
const dustAmount = getDustAmount(this.wallet);
|
|
771
|
-
const
|
|
853
|
+
const boardingTotal = unsettledBoarding.reduce((sum, u) => sum + BigInt(u.value), 0n);
|
|
854
|
+
const vtxoTotal = expiringVtxos.reduce((sum, v) => sum + BigInt(v.value), 0n);
|
|
855
|
+
const totalAmount = boardingTotal + vtxoTotal;
|
|
772
856
|
if (totalAmount < dustAmount)
|
|
773
857
|
return;
|
|
774
858
|
const arkAddress = await this.wallet.getAddress();
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
859
|
+
const includesVtxos = expiringVtxos.length > 0;
|
|
860
|
+
// Block the event-driven renewal path while this settle is in flight
|
|
861
|
+
// when VTXOs are part of the intent. Mirrors renewVtxos()'s guard so
|
|
862
|
+
// the two paths can't race on the same VTXO inputs.
|
|
863
|
+
if (includesVtxos) {
|
|
864
|
+
this.renewalInProgress = true;
|
|
865
|
+
}
|
|
866
|
+
let success = false;
|
|
867
|
+
try {
|
|
868
|
+
await this.wallet.settle({
|
|
869
|
+
inputs: [...unsettledBoarding, ...expiringVtxos],
|
|
870
|
+
outputs: [{ address: arkAddress, amount: totalAmount }],
|
|
871
|
+
});
|
|
872
|
+
// Mark boarding inputs as known only after successful settle.
|
|
873
|
+
for (const u of unsettledBoarding) {
|
|
874
|
+
this.knownBoardingUtxos.add(`${u.txid}:${u.vout}`);
|
|
875
|
+
}
|
|
876
|
+
success = true;
|
|
877
|
+
}
|
|
878
|
+
finally {
|
|
879
|
+
this.lastPeriodicSettleTimestamp = Date.now();
|
|
880
|
+
if (includesVtxos) {
|
|
881
|
+
// Match event-path semantics: bump the renewal cooldown
|
|
882
|
+
// whether we succeeded or failed so a failed periodic settle
|
|
883
|
+
// doesn't let the next vtxo_received event re-enter renewal
|
|
884
|
+
// immediately.
|
|
885
|
+
this.lastRenewalTimestamp = Date.now();
|
|
886
|
+
this.renewalInProgress = false;
|
|
887
|
+
}
|
|
888
|
+
if (success) {
|
|
889
|
+
this.consecutivePeriodicSettleFailures = 0;
|
|
890
|
+
}
|
|
891
|
+
else {
|
|
892
|
+
this.consecutivePeriodicSettleFailures++;
|
|
893
|
+
}
|
|
782
894
|
}
|
|
783
895
|
}
|
|
784
896
|
async dispose() {
|
|
@@ -812,3 +924,5 @@ class VtxoManager {
|
|
|
812
924
|
exports.VtxoManager = VtxoManager;
|
|
813
925
|
VtxoManager.MAX_BACKOFF_MS = 5 * 60 * 1000; // 5 minutes
|
|
814
926
|
VtxoManager.RENEWAL_COOLDOWN_MS = 30000; // 30 seconds
|
|
927
|
+
VtxoManager.PERIODIC_SETTLE_COOLDOWN_MS = 30000;
|
|
928
|
+
VtxoManager.PERIODIC_SETTLE_MAX_BACKOFF_MS = 5 * 60 * 1000;
|