@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
|
@@ -27,6 +27,38 @@ function assertSweepCapable(wallet) {
|
|
|
27
27
|
throw new Error("Boarding UTXO sweep requires a Wallet instance with boardingTapscript, onchainProvider, and network");
|
|
28
28
|
}
|
|
29
29
|
}
|
|
30
|
+
/**
|
|
31
|
+
* Web Locks name used to serialize boarding-poll work across same-origin
|
|
32
|
+
* browser contexts (tabs, service worker). Static because the goal is to
|
|
33
|
+
* deduplicate polls for the *same* wallet — two distinct wallets on the
|
|
34
|
+
* same origin will take turns, which is acceptable.
|
|
35
|
+
*/
|
|
36
|
+
const BOARDING_POLL_LOCK_NAME = "arkade-boarding-poll";
|
|
37
|
+
/**
|
|
38
|
+
* Run `fn` under an exclusive Web Lock when the runtime provides one
|
|
39
|
+
* (browser main thread, service worker). In environments without
|
|
40
|
+
* `navigator.locks` (Node, React Native) the callback runs immediately
|
|
41
|
+
* with no coordination.
|
|
42
|
+
*
|
|
43
|
+
* Uses `ifAvailable: true`: if another context already holds the lock,
|
|
44
|
+
* skip this cycle entirely rather than queueing — the other context will
|
|
45
|
+
* do the work and the next poll will re-check.
|
|
46
|
+
*/
|
|
47
|
+
async function runWithCrossInstanceLock(name, fn) {
|
|
48
|
+
const locks = typeof globalThis !== "undefined" &&
|
|
49
|
+
typeof globalThis.navigator !== "undefined"
|
|
50
|
+
? globalThis.navigator.locks
|
|
51
|
+
: undefined;
|
|
52
|
+
if (!locks) {
|
|
53
|
+
await fn();
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
await locks.request(name, { ifAvailable: true, mode: "exclusive" }, async (lock) => {
|
|
57
|
+
if (lock === null)
|
|
58
|
+
return;
|
|
59
|
+
await fn();
|
|
60
|
+
});
|
|
61
|
+
}
|
|
30
62
|
/** Default renewal threshold in seconds (3 days). */
|
|
31
63
|
export const DEFAULT_THRESHOLD_SECONDS = 3 * 24 * 60 * 60;
|
|
32
64
|
/**
|
|
@@ -186,6 +218,15 @@ export class VtxoManager {
|
|
|
186
218
|
// server emits new VTXOs → vtxo_received → renewVtxos() again → infinite loop.
|
|
187
219
|
this.renewalInProgress = false;
|
|
188
220
|
this.lastRenewalTimestamp = 0;
|
|
221
|
+
// Guards against a retry treadmill on the periodic-settle path: a failing
|
|
222
|
+
// settle would otherwise re-submit identical intents on every 60s poll,
|
|
223
|
+
// producing per-minute DeleteIntent RPCs forever. Mirrors the renewal
|
|
224
|
+
// cooldown but with exponential backoff on consecutive failures, so a
|
|
225
|
+
// persistently broken input eventually drops to the backoff cap instead
|
|
226
|
+
// of hammering the server. Shared across boarding + expiring-VTXO work
|
|
227
|
+
// because they now ride on the same settle intent.
|
|
228
|
+
this.lastPeriodicSettleTimestamp = 0;
|
|
229
|
+
this.consecutivePeriodicSettleFailures = 0;
|
|
189
230
|
// Normalize: prefer settlementConfig, fall back to renewalConfig, default to enabled
|
|
190
231
|
if (settlementConfig !== undefined) {
|
|
191
232
|
this.settlementConfig = settlementConfig;
|
|
@@ -407,10 +448,15 @@ export class VtxoManager {
|
|
|
407
448
|
},
|
|
408
449
|
],
|
|
409
450
|
}, eventCallback);
|
|
410
|
-
this.lastRenewalTimestamp = Date.now();
|
|
411
451
|
return txid;
|
|
412
452
|
}
|
|
413
453
|
finally {
|
|
454
|
+
// Update cooldown on EVERY attempt (success or failure) so transient
|
|
455
|
+
// settle failures (stream close, connector mismatch, duplicated input)
|
|
456
|
+
// don't allow the next vtxo_received event to re-enter renewal
|
|
457
|
+
// immediately. Without this, a failed settle leaves lastRenewalTimestamp
|
|
458
|
+
// at its previous value and the cooldown check becomes a no-op.
|
|
459
|
+
this.lastRenewalTimestamp = Date.now();
|
|
414
460
|
this.renewalInProgress = false;
|
|
415
461
|
}
|
|
416
462
|
}
|
|
@@ -688,33 +734,42 @@ export class VtxoManager {
|
|
|
688
734
|
this.pollDone = { promise, resolve: resolve };
|
|
689
735
|
let hadError = false;
|
|
690
736
|
try {
|
|
691
|
-
//
|
|
692
|
-
//
|
|
693
|
-
|
|
694
|
-
//
|
|
695
|
-
//
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
(this.settlementConfig?.boardingUtxoSweep ??
|
|
705
|
-
DEFAULT_SETTLEMENT_CONFIG.boardingUtxoSweep);
|
|
706
|
-
if (sweepEnabled) {
|
|
737
|
+
// Cross-instance guard: in browser / service worker environments,
|
|
738
|
+
// serialize the poll body across tabs and SW contexts so only one
|
|
739
|
+
// of them registers intents per interval. Without this, every tab
|
|
740
|
+
// submits a parallel RegisterIntent for the same boarding input
|
|
741
|
+
// and N-1 of them collide on the server's duplicated-input check,
|
|
742
|
+
// each producing a DeleteIntent RPC. No-op outside the browser.
|
|
743
|
+
await runWithCrossInstanceLock(BOARDING_POLL_LOCK_NAME, async () => {
|
|
744
|
+
// Fetch boarding inputs once for the entire poll cycle so that
|
|
745
|
+
// settle and sweep don't each hit the network independently.
|
|
746
|
+
const boardingUtxos = await this.wallet.getBoardingUtxos();
|
|
747
|
+
// Settle new (unexpired) boarding inputs + any near-expiry
|
|
748
|
+
// VTXOs in a single intent, then sweep expired boarding
|
|
749
|
+
// inputs. Sequential to avoid racing for the same inputs.
|
|
707
750
|
try {
|
|
708
|
-
await this.
|
|
751
|
+
await this.runPeriodicSettle(boardingUtxos);
|
|
709
752
|
}
|
|
710
753
|
catch (e) {
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
754
|
+
hadError = true;
|
|
755
|
+
console.error("Error during periodic settle:", e);
|
|
756
|
+
}
|
|
757
|
+
const sweepEnabled = this.settlementConfig !== false &&
|
|
758
|
+
(this.settlementConfig?.boardingUtxoSweep ??
|
|
759
|
+
DEFAULT_SETTLEMENT_CONFIG.boardingUtxoSweep);
|
|
760
|
+
if (sweepEnabled) {
|
|
761
|
+
try {
|
|
762
|
+
await this.sweepExpiredBoardingUtxos(boardingUtxos);
|
|
763
|
+
}
|
|
764
|
+
catch (e) {
|
|
765
|
+
if (!(e instanceof Error) ||
|
|
766
|
+
!e.message.includes("No expired boarding UTXOs")) {
|
|
767
|
+
hadError = true;
|
|
768
|
+
console.error("Error auto-sweeping boarding UTXOs:", e);
|
|
769
|
+
}
|
|
715
770
|
}
|
|
716
771
|
}
|
|
717
|
-
}
|
|
772
|
+
});
|
|
718
773
|
}
|
|
719
774
|
catch (e) {
|
|
720
775
|
hadError = true;
|
|
@@ -734,13 +789,19 @@ export class VtxoManager {
|
|
|
734
789
|
}
|
|
735
790
|
}
|
|
736
791
|
/**
|
|
737
|
-
* Auto-settle new (unexpired) boarding inputs into
|
|
738
|
-
* Skips UTXOs that are already expired
|
|
739
|
-
*
|
|
740
|
-
*
|
|
741
|
-
*
|
|
792
|
+
* Auto-settle new (unexpired) boarding inputs AND near-expiry VTXOs into
|
|
793
|
+
* Arkade in a single intent. Skips boarding UTXOs that are already expired
|
|
794
|
+
* (those are handled by sweep) and those already in-flight (tracked in
|
|
795
|
+
* knownBoardingUtxos). If the event-driven renewal path is currently
|
|
796
|
+
* running, VTXOs are omitted from this cycle to avoid double-spending.
|
|
797
|
+
*
|
|
798
|
+
* Failure bookkeeping: after every settle *attempt*, lastPeriodicSettleTimestamp
|
|
799
|
+
* is armed and consecutive failures are counted so the next attempt is
|
|
800
|
+
* blocked by an exponentially growing cooldown (capped). This stops a
|
|
801
|
+
* persistently failing input from producing identical RegisterIntent +
|
|
802
|
+
* DeleteIntent retries on every 60s poll.
|
|
742
803
|
*/
|
|
743
|
-
async
|
|
804
|
+
async runPeriodicSettle(boardingUtxos) {
|
|
744
805
|
// Exclude expired boarding inputs — those should be swept, not settled.
|
|
745
806
|
// If we can't determine expired status, bail out entirely to avoid
|
|
746
807
|
// accidentally settling expired inputs (which would conflict with sweep).
|
|
@@ -758,22 +819,73 @@ export class VtxoManager {
|
|
|
758
819
|
catch (e) {
|
|
759
820
|
throw e instanceof Error ? e : new Error(String(e));
|
|
760
821
|
}
|
|
761
|
-
const
|
|
822
|
+
const unsettledBoarding = boardingUtxos.filter((u) => !this.knownBoardingUtxos.has(`${u.txid}:${u.vout}`) &&
|
|
762
823
|
!expiredSet.has(`${u.txid}:${u.vout}`));
|
|
763
|
-
|
|
824
|
+
// Collect near-expiry VTXOs unless the event-driven path is mid-renewal.
|
|
825
|
+
// Skipping when renewalInProgress avoids double-submitting the same VTXOs.
|
|
826
|
+
let expiringVtxos = [];
|
|
827
|
+
if (!this.renewalInProgress) {
|
|
828
|
+
try {
|
|
829
|
+
expiringVtxos = await this.getExpiringVtxos();
|
|
830
|
+
}
|
|
831
|
+
catch (e) {
|
|
832
|
+
// Non-fatal: fall back to boarding-only settle.
|
|
833
|
+
console.error("Error fetching expiring VTXOs:", e);
|
|
834
|
+
}
|
|
835
|
+
}
|
|
836
|
+
if (unsettledBoarding.length === 0 && expiringVtxos.length === 0) {
|
|
837
|
+
return;
|
|
838
|
+
}
|
|
839
|
+
// Respect the cooldown armed by the previous attempt. Cooldown grows
|
|
840
|
+
// exponentially with consecutive failures and is capped by
|
|
841
|
+
// PERIODIC_SETTLE_MAX_BACKOFF_MS.
|
|
842
|
+
const cooldownMs = Math.min(VtxoManager.PERIODIC_SETTLE_COOLDOWN_MS *
|
|
843
|
+
Math.pow(2, this.consecutivePeriodicSettleFailures), VtxoManager.PERIODIC_SETTLE_MAX_BACKOFF_MS);
|
|
844
|
+
if (Date.now() - this.lastPeriodicSettleTimestamp < cooldownMs) {
|
|
764
845
|
return;
|
|
846
|
+
}
|
|
765
847
|
const dustAmount = getDustAmount(this.wallet);
|
|
766
|
-
const
|
|
848
|
+
const boardingTotal = unsettledBoarding.reduce((sum, u) => sum + BigInt(u.value), 0n);
|
|
849
|
+
const vtxoTotal = expiringVtxos.reduce((sum, v) => sum + BigInt(v.value), 0n);
|
|
850
|
+
const totalAmount = boardingTotal + vtxoTotal;
|
|
767
851
|
if (totalAmount < dustAmount)
|
|
768
852
|
return;
|
|
769
853
|
const arkAddress = await this.wallet.getAddress();
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
854
|
+
const includesVtxos = expiringVtxos.length > 0;
|
|
855
|
+
// Block the event-driven renewal path while this settle is in flight
|
|
856
|
+
// when VTXOs are part of the intent. Mirrors renewVtxos()'s guard so
|
|
857
|
+
// the two paths can't race on the same VTXO inputs.
|
|
858
|
+
if (includesVtxos) {
|
|
859
|
+
this.renewalInProgress = true;
|
|
860
|
+
}
|
|
861
|
+
let success = false;
|
|
862
|
+
try {
|
|
863
|
+
await this.wallet.settle({
|
|
864
|
+
inputs: [...unsettledBoarding, ...expiringVtxos],
|
|
865
|
+
outputs: [{ address: arkAddress, amount: totalAmount }],
|
|
866
|
+
});
|
|
867
|
+
// Mark boarding inputs as known only after successful settle.
|
|
868
|
+
for (const u of unsettledBoarding) {
|
|
869
|
+
this.knownBoardingUtxos.add(`${u.txid}:${u.vout}`);
|
|
870
|
+
}
|
|
871
|
+
success = true;
|
|
872
|
+
}
|
|
873
|
+
finally {
|
|
874
|
+
this.lastPeriodicSettleTimestamp = Date.now();
|
|
875
|
+
if (includesVtxos) {
|
|
876
|
+
// Match event-path semantics: bump the renewal cooldown
|
|
877
|
+
// whether we succeeded or failed so a failed periodic settle
|
|
878
|
+
// doesn't let the next vtxo_received event re-enter renewal
|
|
879
|
+
// immediately.
|
|
880
|
+
this.lastRenewalTimestamp = Date.now();
|
|
881
|
+
this.renewalInProgress = false;
|
|
882
|
+
}
|
|
883
|
+
if (success) {
|
|
884
|
+
this.consecutivePeriodicSettleFailures = 0;
|
|
885
|
+
}
|
|
886
|
+
else {
|
|
887
|
+
this.consecutivePeriodicSettleFailures++;
|
|
888
|
+
}
|
|
777
889
|
}
|
|
778
890
|
}
|
|
779
891
|
async dispose() {
|
|
@@ -806,3 +918,5 @@ export class VtxoManager {
|
|
|
806
918
|
}
|
|
807
919
|
VtxoManager.MAX_BACKOFF_MS = 5 * 60 * 1000; // 5 minutes
|
|
808
920
|
VtxoManager.RENEWAL_COOLDOWN_MS = 30000; // 30 seconds
|
|
921
|
+
VtxoManager.PERIODIC_SETTLE_COOLDOWN_MS = 30000;
|
|
922
|
+
VtxoManager.PERIODIC_SETTLE_MAX_BACKOFF_MS = 5 * 60 * 1000;
|