@arkade-os/sdk 0.4.24 → 0.4.25
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/dist/cjs/contracts/contractManager.js +44 -8
- package/dist/cjs/contracts/contractWatcher.js +2 -2
- package/dist/cjs/contracts/vtxoOwnership.js +18 -0
- package/dist/cjs/repositories/inMemory/walletRepository.js +35 -0
- package/dist/cjs/repositories/indexedDB/walletRepository.js +117 -0
- package/dist/cjs/repositories/realm/walletRepository.js +28 -0
- package/dist/cjs/repositories/sqlite/walletRepository.js +23 -0
- package/dist/cjs/wallet/serviceWorker/wallet-message-handler.js +14 -3
- package/dist/cjs/wallet/serviceWorker/wallet.js +10 -0
- package/dist/cjs/wallet/vtxo-manager.js +112 -16
- package/dist/cjs/wallet/wallet.js +3 -17
- package/dist/cjs/worker/expo/processors/contractPollProcessor.js +1 -1
- package/dist/esm/contracts/contractManager.js +45 -9
- package/dist/esm/contracts/contractWatcher.js +3 -3
- package/dist/esm/contracts/vtxoOwnership.js +16 -0
- package/dist/esm/repositories/inMemory/walletRepository.js +35 -0
- package/dist/esm/repositories/indexedDB/walletRepository.js +117 -0
- package/dist/esm/repositories/realm/walletRepository.js +28 -0
- package/dist/esm/repositories/sqlite/walletRepository.js +23 -0
- package/dist/esm/wallet/serviceWorker/wallet-message-handler.js +15 -4
- package/dist/esm/wallet/serviceWorker/wallet.js +10 -0
- package/dist/esm/wallet/vtxo-manager.js +112 -16
- package/dist/esm/wallet/wallet.js +4 -18
- package/dist/esm/worker/expo/processors/contractPollProcessor.js +2 -2
- package/dist/types/contracts/contractManager.d.ts +17 -1
- package/dist/types/contracts/vtxoOwnership.d.ts +9 -1
- package/dist/types/repositories/inMemory/walletRepository.d.ts +4 -1
- package/dist/types/repositories/indexedDB/walletRepository.d.ts +4 -1
- package/dist/types/repositories/realm/walletRepository.d.ts +4 -1
- package/dist/types/repositories/sqlite/walletRepository.d.ts +4 -1
- package/dist/types/repositories/walletRepository.d.ts +21 -0
- package/dist/types/wallet/serviceWorker/wallet-message-handler.d.ts +14 -2
- package/dist/types/wallet/vtxo-manager.d.ts +32 -5
- package/package.json +1 -1
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { isExpired, isRecoverable, isSpendable, isSubdust, } from './index.js';
|
|
2
|
+
import { maybeArkError } from '../providers/errors.js';
|
|
2
3
|
import { hasBoardingTxExpired } from '../utils/arkTransaction.js';
|
|
3
4
|
import { CSVMultisigTapscript } from '../script/tapscript.js';
|
|
4
5
|
import { hex } from "@scure/base";
|
|
@@ -432,10 +433,22 @@ export class VtxoManager {
|
|
|
432
433
|
try {
|
|
433
434
|
// Get all virtual outputs (including recoverable ones)
|
|
434
435
|
// Use default threshold to bypass settlementConfig gate (manual API should always work)
|
|
435
|
-
const
|
|
436
|
+
const threshold = this.settlementConfig !== false &&
|
|
436
437
|
this.settlementConfig?.vtxoThreshold !== undefined
|
|
437
438
|
? this.settlementConfig.vtxoThreshold * 1000
|
|
438
|
-
: DEFAULT_RENEWAL_CONFIG.thresholdMs
|
|
439
|
+
: DEFAULT_RENEWAL_CONFIG.thresholdMs;
|
|
440
|
+
let vtxos = await this.getExpiringVtxos(threshold);
|
|
441
|
+
if (vtxos.length === 0) {
|
|
442
|
+
throw new Error("No VTXOs available to renew");
|
|
443
|
+
}
|
|
444
|
+
// Pre-flight: validate the chosen inputs against the indexer's
|
|
445
|
+
// authoritative state before submitting. The cursor-derived
|
|
446
|
+
// delta sync filters by `created_at`, so a VTXO created
|
|
447
|
+
// before the cursor and spent recently can sit in the local
|
|
448
|
+
// cache forever; settling against it yields a guaranteed
|
|
449
|
+
// VTXO_ALREADY_SPENT 400. Refreshing the candidates here
|
|
450
|
+
// catches that BEFORE the network round-trip.
|
|
451
|
+
vtxos = await this.revalidateBeforeSettle(vtxos, threshold);
|
|
439
452
|
if (vtxos.length === 0) {
|
|
440
453
|
throw new Error("No VTXOs available to renew");
|
|
441
454
|
}
|
|
@@ -684,9 +697,11 @@ export class VtxoManager {
|
|
|
684
697
|
if (e.message.includes("VTXO_ALREADY_SPENT")) {
|
|
685
698
|
// Our local VTXO cache is stale vs. the
|
|
686
699
|
// server's authoritative view. Trigger a
|
|
687
|
-
// throttled refresh
|
|
688
|
-
//
|
|
689
|
-
|
|
700
|
+
// throttled, targeted refresh on the
|
|
701
|
+
// offending outpoint (if the server told
|
|
702
|
+
// us which one), then skip — the next
|
|
703
|
+
// cycle will see fresh data.
|
|
704
|
+
void this.maybeRefreshAfterVtxoSpent(this.extractSpentOutpoint(e));
|
|
690
705
|
return;
|
|
691
706
|
}
|
|
692
707
|
}
|
|
@@ -711,13 +726,20 @@ export class VtxoManager {
|
|
|
711
726
|
/**
|
|
712
727
|
* VTXO_ALREADY_SPENT means the server's authoritative view of VTXO state
|
|
713
728
|
* is ahead of ours — cross-instance race, pre-lock snapshot drift, or an
|
|
714
|
-
* SSE gap left stale data in the local cache. Silent-swallowing
|
|
715
|
-
* the same error on the next cycle because nothing
|
|
716
|
-
*
|
|
717
|
-
*
|
|
718
|
-
*
|
|
729
|
+
* SSE gap left stale data in the local cache. Silent-swallowing
|
|
730
|
+
* guarantees the same error on the next cycle because nothing
|
|
731
|
+
* reconciles the cache.
|
|
732
|
+
*
|
|
733
|
+
* The cursor-derived delta sync filters by `created_at`, so a VTXO that
|
|
734
|
+
* was created before the cursor but spent recently can never be
|
|
735
|
+
* reconciled by `refreshVtxos()`. Use `refreshOutpoints` for surgical
|
|
736
|
+
* recovery: query the indexer for the specific stale outpoint and
|
|
737
|
+
* upsert its authoritative state into the wallet repository.
|
|
738
|
+
*
|
|
739
|
+
* Throttled because the same VTXO can fire repeatedly before the
|
|
740
|
+
* upsert observably propagates through the renewal selector.
|
|
719
741
|
*/
|
|
720
|
-
maybeRefreshAfterVtxoSpent() {
|
|
742
|
+
maybeRefreshAfterVtxoSpent(spentOutpoint) {
|
|
721
743
|
if (this.vtxoSpentRefreshPromise) {
|
|
722
744
|
return this.vtxoSpentRefreshPromise;
|
|
723
745
|
}
|
|
@@ -730,7 +752,13 @@ export class VtxoManager {
|
|
|
730
752
|
this.vtxoSpentRefreshPromise = (async () => {
|
|
731
753
|
try {
|
|
732
754
|
const contractManager = await this.wallet.getContractManager();
|
|
733
|
-
|
|
755
|
+
if (spentOutpoint) {
|
|
756
|
+
await contractManager.refreshOutpoints([spentOutpoint]);
|
|
757
|
+
}
|
|
758
|
+
else {
|
|
759
|
+
// No outpoint metadata — fall back to the broader refresh.
|
|
760
|
+
await contractManager.refreshVtxos();
|
|
761
|
+
}
|
|
734
762
|
}
|
|
735
763
|
catch (e) {
|
|
736
764
|
console.error("Error refreshing VTXOs after VTXO_ALREADY_SPENT:", e);
|
|
@@ -741,6 +769,66 @@ export class VtxoManager {
|
|
|
741
769
|
})();
|
|
742
770
|
return this.vtxoSpentRefreshPromise;
|
|
743
771
|
}
|
|
772
|
+
/**
|
|
773
|
+
* Extract the offending VTXO outpoint from a `VTXO_ALREADY_SPENT` error,
|
|
774
|
+
* if the server attached one in `metadata.vtxo_outpoint`. Returns
|
|
775
|
+
* `undefined` when the error isn't a parsed ArkError, isn't this code,
|
|
776
|
+
* or doesn't carry the metadata.
|
|
777
|
+
*/
|
|
778
|
+
extractSpentOutpoint(error) {
|
|
779
|
+
const ark = maybeArkError(error);
|
|
780
|
+
if (!ark || ark.name !== "VTXO_ALREADY_SPENT")
|
|
781
|
+
return undefined;
|
|
782
|
+
const raw = ark.metadata?.vtxo_outpoint;
|
|
783
|
+
if (typeof raw !== "string")
|
|
784
|
+
return undefined;
|
|
785
|
+
const [txid, voutStr] = raw.split(":");
|
|
786
|
+
if (!txid || !voutStr)
|
|
787
|
+
return undefined;
|
|
788
|
+
const vout = Number(voutStr);
|
|
789
|
+
if (!Number.isInteger(vout) || vout < 0)
|
|
790
|
+
return undefined;
|
|
791
|
+
return { txid, vout };
|
|
792
|
+
}
|
|
793
|
+
/**
|
|
794
|
+
* Reconcile the chosen VTXOs with the indexer's authoritative state
|
|
795
|
+
* before submitting a settle intent. Pulls the canonical record for
|
|
796
|
+
* each candidate outpoint via {@link IContractManager.refreshOutpoints}
|
|
797
|
+
* (which upserts the result into the wallet repository), then
|
|
798
|
+
* re-selects through the standard expiring-vtxo filter so anything
|
|
799
|
+
* the refresh flagged as spent is dropped.
|
|
800
|
+
*
|
|
801
|
+
* Best-effort: a failed refresh just falls back to the original
|
|
802
|
+
* candidates and lets the post-submit `VTXO_ALREADY_SPENT` recovery
|
|
803
|
+
* handle whatever slipped through.
|
|
804
|
+
*/
|
|
805
|
+
async revalidateBeforeSettle(candidates, thresholdMs) {
|
|
806
|
+
if (candidates.length === 0)
|
|
807
|
+
return candidates;
|
|
808
|
+
try {
|
|
809
|
+
const cm = await this.wallet.getContractManager();
|
|
810
|
+
await cm.refreshOutpoints(candidates.map((v) => ({ txid: v.txid, vout: v.vout })));
|
|
811
|
+
}
|
|
812
|
+
catch (e) {
|
|
813
|
+
console.error("Error pre-validating VTXOs before settle:", e);
|
|
814
|
+
return candidates;
|
|
815
|
+
}
|
|
816
|
+
// Re-select from the now-fresh local cache. Anything previously
|
|
817
|
+
// selected but spent gets filtered out by the standard
|
|
818
|
+
// `isSpendable`/`isSpent` checks inside getVtxos / getExpiringVtxos.
|
|
819
|
+
try {
|
|
820
|
+
const refreshed = await this.getExpiringVtxos(thresholdMs);
|
|
821
|
+
const candidateKeys = new Set(candidates.map((v) => `${v.txid}:${v.vout}`));
|
|
822
|
+
// Restrict to vtxos that were also in the original candidate set
|
|
823
|
+
// — `getExpiringVtxos` may surface NEW vtxos and we don't want
|
|
824
|
+
// pre-flight to silently expand the input set.
|
|
825
|
+
return refreshed.filter((v) => candidateKeys.has(`${v.txid}:${v.vout}`));
|
|
826
|
+
}
|
|
827
|
+
catch (e) {
|
|
828
|
+
console.error("Error re-selecting VTXOs after pre-validate:", e);
|
|
829
|
+
return candidates;
|
|
830
|
+
}
|
|
831
|
+
}
|
|
744
832
|
/** Computes the next poll delay, applying exponential backoff on failures. */
|
|
745
833
|
getNextPollDelay() {
|
|
746
834
|
if (this.settlementConfig === false)
|
|
@@ -882,6 +970,13 @@ export class VtxoManager {
|
|
|
882
970
|
if (!this.renewalInProgress) {
|
|
883
971
|
try {
|
|
884
972
|
expiringVtxos = await this.getExpiringVtxos();
|
|
973
|
+
// Pre-flight validation: see comment in `renewVtxos`. The
|
|
974
|
+
// local cache may carry vtxos that the indexer already
|
|
975
|
+
// marks spent because the cursor-derived delta sync only
|
|
976
|
+
// catches `created_at`-recent updates, not status changes
|
|
977
|
+
// for older VTXOs.
|
|
978
|
+
expiringVtxos =
|
|
979
|
+
await this.revalidateBeforeSettle(expiringVtxos);
|
|
885
980
|
}
|
|
886
981
|
catch (e) {
|
|
887
982
|
// Non-fatal: fall back to boarding-only settle.
|
|
@@ -973,11 +1068,12 @@ export class VtxoManager {
|
|
|
973
1068
|
e.message.includes("VTXO_ALREADY_SPENT")) {
|
|
974
1069
|
// Local VTXO cache is stale vs. the server's
|
|
975
1070
|
// authoritative view — not a transient failure.
|
|
976
|
-
// Trigger a throttled refresh
|
|
977
|
-
//
|
|
978
|
-
//
|
|
1071
|
+
// Trigger a throttled, targeted refresh on the
|
|
1072
|
+
// offending outpoint and skip this cycle without
|
|
1073
|
+
// bumping the failure counter, so the next poll
|
|
1074
|
+
// can retry once the cache reconciles.
|
|
979
1075
|
staleCacheSkip = true;
|
|
980
|
-
void this.maybeRefreshAfterVtxoSpent();
|
|
1076
|
+
void this.maybeRefreshAfterVtxoSpent(this.extractSpentOutpoint(e));
|
|
981
1077
|
}
|
|
982
1078
|
else {
|
|
983
1079
|
throw e;
|
|
@@ -34,7 +34,7 @@ import { ContractManager } from '../contracts/contractManager.js';
|
|
|
34
34
|
import { contractHandlers } from '../contracts/handlers/index.js';
|
|
35
35
|
import { timelockToSequence } from '../utils/timelock.js';
|
|
36
36
|
import { clearSyncCursor, updateWalletState } from '../utils/syncCursors.js';
|
|
37
|
-
import { validateVtxosForScript } from '../contracts/vtxoOwnership.js';
|
|
37
|
+
import { validateVtxosForScript, saveVtxosForContract, } from '../contracts/vtxoOwnership.js';
|
|
38
38
|
export const getArkadeServerUrl = ({ arkServerUrl, }) => arkServerUrl || DEFAULT_ARKADE_SERVER_URL;
|
|
39
39
|
// Historical unilateral exit delay for mainnet (~7 days in seconds).
|
|
40
40
|
// Kept so existing wallets can still discover and spend VTXOs sent to the
|
|
@@ -1866,7 +1866,6 @@ export class Wallet extends ReadonlyWallet {
|
|
|
1866
1866
|
arr.push(v);
|
|
1867
1867
|
spentByScript.set(v.script, arr);
|
|
1868
1868
|
}
|
|
1869
|
-
const byAddress = new Map();
|
|
1870
1869
|
for (const [script, vtxos] of spentByScript) {
|
|
1871
1870
|
// User-initiated send path: a wrong-script row here means the
|
|
1872
1871
|
// wallet is about to record ownership against the wrong
|
|
@@ -1876,18 +1875,11 @@ export class Wallet extends ReadonlyWallet {
|
|
|
1876
1875
|
if (!targetAddr) {
|
|
1877
1876
|
throw new Error(`Wallet.updateDbAfterOffchainTx: no contract owns script ${script}`);
|
|
1878
1877
|
}
|
|
1879
|
-
|
|
1880
|
-
bucket.push(...vtxos);
|
|
1881
|
-
byAddress.set(targetAddr, bucket);
|
|
1878
|
+
await saveVtxosForContract(this.walletRepository, { script, address: targetAddr }, vtxos);
|
|
1882
1879
|
}
|
|
1883
1880
|
// Change is always primary-script by construction.
|
|
1884
1881
|
if (changeVtxo) {
|
|
1885
|
-
|
|
1886
|
-
bucket.push(changeVtxo);
|
|
1887
|
-
byAddress.set(primaryAddr, bucket);
|
|
1888
|
-
}
|
|
1889
|
-
for (const [addr, vtxos] of byAddress) {
|
|
1890
|
-
await this.walletRepository.saveVtxos(addr, vtxos);
|
|
1882
|
+
await saveVtxosForContract(this.walletRepository, { script: changeVtxo.script, address: primaryAddr }, [changeVtxo]);
|
|
1891
1883
|
}
|
|
1892
1884
|
await this.walletRepository.saveTransactions(primaryAddr, [
|
|
1893
1885
|
{
|
|
@@ -1950,7 +1942,6 @@ export class Wallet extends ReadonlyWallet {
|
|
|
1950
1942
|
// alongside the rest.
|
|
1951
1943
|
const contracts = await cm.getContracts();
|
|
1952
1944
|
const addrByScript = new Map(contracts.map((c) => [c.script, c.address]));
|
|
1953
|
-
const byAddress = new Map();
|
|
1954
1945
|
const byScript = new Map();
|
|
1955
1946
|
for (const v of spentVtxos) {
|
|
1956
1947
|
if (!v.script) {
|
|
@@ -1968,12 +1959,7 @@ export class Wallet extends ReadonlyWallet {
|
|
|
1968
1959
|
if (!targetAddr) {
|
|
1969
1960
|
throw new Error(`Wallet.updateDbAfterSettle: no contract owns script ${script}`);
|
|
1970
1961
|
}
|
|
1971
|
-
|
|
1972
|
-
bucket.push(...vtxos);
|
|
1973
|
-
byAddress.set(targetAddr, bucket);
|
|
1974
|
-
}
|
|
1975
|
-
for (const [bucketAddr, vtxos] of byAddress) {
|
|
1976
|
-
await this.walletRepository.saveVtxos(bucketAddr, vtxos);
|
|
1962
|
+
await saveVtxosForContract(this.walletRepository, { script, address: targetAddr }, vtxos);
|
|
1977
1963
|
}
|
|
1978
1964
|
}
|
|
1979
1965
|
if (boardingUtxoToRemove.size > 0) {
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { warnAndFilterVtxosForScript } from '../../../contracts/vtxoOwnership.js';
|
|
1
|
+
import { warnAndFilterVtxosForScript, saveVtxosForContract, } from '../../../contracts/vtxoOwnership.js';
|
|
2
2
|
export const CONTRACT_POLL_TASK_TYPE = "contract-poll";
|
|
3
3
|
/**
|
|
4
4
|
* Polls the indexer for the latest VTXO state of every contract and
|
|
@@ -45,7 +45,7 @@ export const contractPollProcessor = {
|
|
|
45
45
|
// before persisting; the loop must keep going for the remaining
|
|
46
46
|
// contracts even when one row is rejected.
|
|
47
47
|
const filtered = warnAndFilterVtxosForScript(allVtxos, contract.script, "contractPollProcessor");
|
|
48
|
-
await walletRepository
|
|
48
|
+
await saveVtxosForContract(walletRepository, contract, filtered);
|
|
49
49
|
vtxosSaved += filtered.length;
|
|
50
50
|
contractsProcessed++;
|
|
51
51
|
}
|
|
@@ -2,7 +2,7 @@ import { IndexerProvider } from "../providers/indexer";
|
|
|
2
2
|
import { WalletRepository } from "../repositories/walletRepository";
|
|
3
3
|
import { Contract, ContractEventCallback, ContractState, ContractWithVtxos, GetContractsFilter, PathSelection } from "./types";
|
|
4
4
|
import { ContractWatcherConfig } from "./contractWatcher";
|
|
5
|
-
import { ExtendedVirtualCoin, VirtualCoin } from "../wallet";
|
|
5
|
+
import { ExtendedVirtualCoin, Outpoint, VirtualCoin } from "../wallet";
|
|
6
6
|
import { ContractRepository } from "../repositories";
|
|
7
7
|
export type RefreshVtxosOptions = {
|
|
8
8
|
scripts?: string[];
|
|
@@ -88,6 +88,21 @@ export interface IContractManager extends Disposable {
|
|
|
88
88
|
* With options, narrows the refresh to specific scripts and/or a time window.
|
|
89
89
|
*/
|
|
90
90
|
refreshVtxos(opts?: RefreshVtxosOptions): Promise<void>;
|
|
91
|
+
/**
|
|
92
|
+
* Reconcile specific outpoints with the indexer's authoritative state and
|
|
93
|
+
* upsert the result into the wallet repository.
|
|
94
|
+
*
|
|
95
|
+
* The cursor-derived delta sync filters by `created_at`, so a VTXO that
|
|
96
|
+
* was created before the cursor but spent recently won't surface in a
|
|
97
|
+
* standard `refreshVtxos()` call. This method is the surgical recovery
|
|
98
|
+
* path for that case: when something hands us a stale outpoint (e.g. the
|
|
99
|
+
* server returns `VTXO_ALREADY_SPENT` with a `vtxo_outpoint` in its
|
|
100
|
+
* error metadata), call this to pull the latest state and unblock the
|
|
101
|
+
* caller — no full re-scan, no cursor change.
|
|
102
|
+
*
|
|
103
|
+
* Outpoints not owned by any tracked contract are silently dropped.
|
|
104
|
+
*/
|
|
105
|
+
refreshOutpoints(outpoints: Outpoint[]): Promise<void>;
|
|
91
106
|
/**
|
|
92
107
|
* Whether the underlying watcher is currently active.
|
|
93
108
|
*/
|
|
@@ -310,6 +325,7 @@ export declare class ContractManager implements IContractManager {
|
|
|
310
325
|
* Subset refreshes (scripts filter) intentionally do not advance the cursor.
|
|
311
326
|
*/
|
|
312
327
|
refreshVtxos(opts?: RefreshVtxosOptions): Promise<void>;
|
|
328
|
+
refreshOutpoints(outpoints: Outpoint[]): Promise<void>;
|
|
313
329
|
/**
|
|
314
330
|
* Check if currently watching.
|
|
315
331
|
*/
|
|
@@ -1,4 +1,6 @@
|
|
|
1
|
-
import type { VirtualCoin } from "../wallet";
|
|
1
|
+
import type { ExtendedVirtualCoin, VirtualCoin } from "../wallet";
|
|
2
|
+
import type { WalletRepository } from "../repositories/walletRepository";
|
|
3
|
+
import type { Contract } from "./types";
|
|
2
4
|
/**
|
|
3
5
|
* Tier 1 helpers that enforce VTXO ownership at call sites that already know
|
|
4
6
|
* the intended contract script. Address-keyed repositories may still hand back
|
|
@@ -23,3 +25,9 @@ export declare function warnAndFilterVtxosForScript<T extends Pick<VirtualCoin,
|
|
|
23
25
|
* serious bug in the wallet's spend path.
|
|
24
26
|
*/
|
|
25
27
|
export declare function validateVtxosForScript(vtxos: Array<Pick<VirtualCoin, "txid" | "vout" | "script">>, script: string, context: string): void;
|
|
28
|
+
/**
|
|
29
|
+
* Tier 2 dispatch helpers: route to script-scoped repository methods when
|
|
30
|
+
* available, falling back to Tier 1 address-based filtering otherwise.
|
|
31
|
+
*/
|
|
32
|
+
export declare function getVtxosForContract(repo: WalletRepository, contract: Pick<Contract, "script" | "address">): Promise<ExtendedVirtualCoin[]>;
|
|
33
|
+
export declare function saveVtxosForContract(repo: WalletRepository, contract: Pick<Contract, "script" | "address">, vtxos: ExtendedVirtualCoin[]): Promise<void>;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { ArkTransaction, ExtendedCoin, ExtendedVirtualCoin } from "../../wallet";
|
|
2
|
-
import { WalletRepository, WalletState } from "../walletRepository";
|
|
2
|
+
import { WalletRepository, WalletState, VtxoRepositoryKey } from "../walletRepository";
|
|
3
3
|
/**
|
|
4
4
|
* In-memory implementation of WalletRepository.
|
|
5
5
|
* Data is ephemeral and scoped to the instance.
|
|
@@ -13,6 +13,9 @@ export declare class InMemoryWalletRepository implements WalletRepository {
|
|
|
13
13
|
getVtxos(address: string): Promise<ExtendedVirtualCoin[]>;
|
|
14
14
|
saveVtxos(address: string, vtxos: ExtendedVirtualCoin[]): Promise<void>;
|
|
15
15
|
deleteVtxos(address: string): Promise<void>;
|
|
16
|
+
getVtxosForScript(script: string): Promise<ExtendedVirtualCoin[]>;
|
|
17
|
+
saveVtxosForScript(key: VtxoRepositoryKey, vtxos: ExtendedVirtualCoin[]): Promise<void>;
|
|
18
|
+
deleteVtxosForScript(script: string): Promise<void>;
|
|
16
19
|
getUtxos(address: string): Promise<ExtendedCoin[]>;
|
|
17
20
|
saveUtxos(address: string, utxos: ExtendedCoin[]): Promise<void>;
|
|
18
21
|
deleteUtxos(address: string): Promise<void>;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { ExtendedCoin, ExtendedVirtualCoin, ArkTransaction } from "../../wallet";
|
|
2
|
-
import { WalletRepository, WalletState } from "../walletRepository";
|
|
2
|
+
import { WalletRepository, WalletState, VtxoRepositoryKey } from "../walletRepository";
|
|
3
3
|
/**
|
|
4
4
|
* IndexedDB-based implementation of WalletRepository.
|
|
5
5
|
*/
|
|
@@ -13,6 +13,9 @@ export declare class IndexedDBWalletRepository implements WalletRepository {
|
|
|
13
13
|
getVtxos(address: string): Promise<ExtendedVirtualCoin[]>;
|
|
14
14
|
saveVtxos(address: string, vtxos: ExtendedVirtualCoin[]): Promise<void>;
|
|
15
15
|
deleteVtxos(address: string): Promise<void>;
|
|
16
|
+
getVtxosForScript(script: string): Promise<ExtendedVirtualCoin[]>;
|
|
17
|
+
saveVtxosForScript(key: VtxoRepositoryKey, vtxos: ExtendedVirtualCoin[]): Promise<void>;
|
|
18
|
+
deleteVtxosForScript(script: string): Promise<void>;
|
|
16
19
|
getUtxos(address: string): Promise<ExtendedCoin[]>;
|
|
17
20
|
saveUtxos(address: string, utxos: ExtendedCoin[]): Promise<void>;
|
|
18
21
|
deleteUtxos(address: string): Promise<void>;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { ArkTransaction, ExtendedCoin, ExtendedVirtualCoin } from "../../wallet";
|
|
2
|
-
import { WalletRepository, WalletState } from "../walletRepository";
|
|
2
|
+
import { WalletRepository, WalletState, VtxoRepositoryKey } from "../walletRepository";
|
|
3
3
|
import { RealmLike } from "./types";
|
|
4
4
|
/**
|
|
5
5
|
* Realm-based implementation of WalletRepository.
|
|
@@ -20,6 +20,9 @@ export declare class RealmWalletRepository implements WalletRepository {
|
|
|
20
20
|
getVtxos(address: string): Promise<ExtendedVirtualCoin[]>;
|
|
21
21
|
saveVtxos(address: string, vtxos: ExtendedVirtualCoin[]): Promise<void>;
|
|
22
22
|
deleteVtxos(address: string): Promise<void>;
|
|
23
|
+
getVtxosForScript(script: string): Promise<ExtendedVirtualCoin[]>;
|
|
24
|
+
saveVtxosForScript(key: VtxoRepositoryKey, vtxos: ExtendedVirtualCoin[]): Promise<void>;
|
|
25
|
+
deleteVtxosForScript(script: string): Promise<void>;
|
|
23
26
|
getUtxos(address: string): Promise<ExtendedCoin[]>;
|
|
24
27
|
saveUtxos(address: string, utxos: ExtendedCoin[]): Promise<void>;
|
|
25
28
|
deleteUtxos(address: string): Promise<void>;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { ArkTransaction, ExtendedCoin, ExtendedVirtualCoin } from "../../wallet";
|
|
2
|
-
import { WalletRepository, WalletState } from "../walletRepository";
|
|
2
|
+
import { WalletRepository, WalletState, VtxoRepositoryKey } from "../walletRepository";
|
|
3
3
|
import { SQLExecutor } from "./types";
|
|
4
4
|
interface SQLiteWalletRepositoryOptions {
|
|
5
5
|
/** Table name prefix (default: "ark_") */
|
|
@@ -50,6 +50,9 @@ export declare class SQLiteWalletRepository implements WalletRepository {
|
|
|
50
50
|
getVtxos(address: string): Promise<ExtendedVirtualCoin[]>;
|
|
51
51
|
saveVtxos(address: string, vtxos: ExtendedVirtualCoin[]): Promise<void>;
|
|
52
52
|
deleteVtxos(address: string): Promise<void>;
|
|
53
|
+
getVtxosForScript(script: string): Promise<ExtendedVirtualCoin[]>;
|
|
54
|
+
saveVtxosForScript(key: VtxoRepositoryKey, vtxos: ExtendedVirtualCoin[]): Promise<void>;
|
|
55
|
+
deleteVtxosForScript(script: string): Promise<void>;
|
|
53
56
|
getUtxos(address: string): Promise<ExtendedCoin[]>;
|
|
54
57
|
saveUtxos(address: string, utxos: ExtendedCoin[]): Promise<void>;
|
|
55
58
|
deleteUtxos(address: string): Promise<void>;
|
|
@@ -20,6 +20,12 @@ export type CommitmentTxRecord = {
|
|
|
20
20
|
/** Creation timestamp in milliseconds. */
|
|
21
21
|
createdAt: number;
|
|
22
22
|
};
|
|
23
|
+
export interface VtxoRepositoryKey {
|
|
24
|
+
/** Authoritative ownership key. */
|
|
25
|
+
script: string;
|
|
26
|
+
/** Legacy storage bucket. Required by all current backends; throw if absent. */
|
|
27
|
+
address?: string;
|
|
28
|
+
}
|
|
23
29
|
export interface WalletRepository extends AsyncDisposable {
|
|
24
30
|
readonly version: 1;
|
|
25
31
|
/**
|
|
@@ -32,6 +38,21 @@ export interface WalletRepository extends AsyncDisposable {
|
|
|
32
38
|
saveVtxos(address: string, vtxos: ExtendedVirtualCoin[]): Promise<void>;
|
|
33
39
|
/** Delete stored virtual outputs for an address. */
|
|
34
40
|
deleteVtxos(address: string): Promise<void>;
|
|
41
|
+
/**
|
|
42
|
+
* Fetch stored virtual outputs for a script.
|
|
43
|
+
* @optional SDK backends implement this; custom backends fall back to Tier 1.
|
|
44
|
+
*/
|
|
45
|
+
getVtxosForScript?(script: string): Promise<ExtendedVirtualCoin[]>;
|
|
46
|
+
/**
|
|
47
|
+
* Save virtual outputs for a script.
|
|
48
|
+
* @optional SDK backends implement this; custom backends fall back to Tier 1.
|
|
49
|
+
*/
|
|
50
|
+
saveVtxosForScript?(key: VtxoRepositoryKey, vtxos: ExtendedVirtualCoin[]): Promise<void>;
|
|
51
|
+
/**
|
|
52
|
+
* Delete stored virtual outputs for a script.
|
|
53
|
+
* @optional SDK backends implement this; custom backends fall back to Tier 1.
|
|
54
|
+
*/
|
|
55
|
+
deleteVtxosForScript?(script: string): Promise<void>;
|
|
35
56
|
/** Fetch stored boarding inputs for an address. */
|
|
36
57
|
getUtxos(address: string): Promise<ExtendedCoin[]>;
|
|
37
58
|
/** Save boarding inputs for an address. */
|
|
@@ -259,6 +259,18 @@ export type RequestRefreshVtxos = RequestEnvelope & {
|
|
|
259
259
|
export type ResponseRefreshVtxos = ResponseEnvelope & {
|
|
260
260
|
type: "REFRESH_VTXOS_SUCCESS";
|
|
261
261
|
};
|
|
262
|
+
export type RequestRefreshOutpoints = RequestEnvelope & {
|
|
263
|
+
type: "REFRESH_OUTPOINTS";
|
|
264
|
+
payload: {
|
|
265
|
+
outpoints: {
|
|
266
|
+
txid: string;
|
|
267
|
+
vout: number;
|
|
268
|
+
}[];
|
|
269
|
+
};
|
|
270
|
+
};
|
|
271
|
+
export type ResponseRefreshOutpoints = ResponseEnvelope & {
|
|
272
|
+
type: "REFRESH_OUTPOINTS_SUCCESS";
|
|
273
|
+
};
|
|
262
274
|
export type RequestGetAllSpendingPaths = RequestEnvelope & {
|
|
263
275
|
type: "GET_ALL_SPENDING_PATHS";
|
|
264
276
|
payload: {
|
|
@@ -463,8 +475,8 @@ export type ResponseSweepExpiredBoardingUtxos = ResponseEnvelope & {
|
|
|
463
475
|
txid: string;
|
|
464
476
|
};
|
|
465
477
|
};
|
|
466
|
-
export type WalletUpdaterRequest = RequestInitWallet | RequestSettle | RequestSendBitcoin | RequestGetAddress | RequestGetBoardingAddress | RequestGetBalance | RequestGetVtxos | RequestGetBoardingUtxos | RequestGetTransactionHistory | RequestGetStatus | RequestClear | RequestReloadWallet | RequestSignTransaction | RequestCreateContract | RequestGetContracts | RequestGetContractsWithVtxos | RequestAnnotateVtxos | RequestUpdateContract | RequestDeleteContract | RequestGetSpendablePaths | RequestGetAllSpendingPaths | RequestIsContractManagerWatching | RequestRefreshVtxos | RequestSend | RequestGetAssetDetails | RequestIssue | RequestReissue | RequestBurn | RequestDelegate | RequestGetDelegateInfo | RequestRecoverVtxos | RequestGetRecoverableBalance | RequestGetExpiringVtxos | RequestRenewVtxos | RequestGetExpiredBoardingUtxos | RequestSweepExpiredBoardingUtxos;
|
|
467
|
-
export type WalletUpdaterResponse = ResponseEnvelope & (ResponseInitWallet | ResponseSettle | ResponseSettleEvent | ResponseSendBitcoin | ResponseGetAddress | ResponseGetBoardingAddress | ResponseGetBalance | ResponseGetVtxos | ResponseGetBoardingUtxos | ResponseGetTransactionHistory | ResponseGetStatus | ResponseClear | ResponseReloadWallet | ResponseUtxoUpdate | ResponseVtxoUpdate | ResponseSignTransaction | ResponseCreateContract | ResponseGetContracts | ResponseGetContractsWithVtxos | ResponseAnnotateVtxos | ResponseUpdateContract | ResponseDeleteContract | ResponseGetSpendablePaths | ResponseGetAllSpendingPaths | ResponseIsContractManagerWatching | ResponseRefreshVtxos | ResponseContractEvent | ResponseSend | ResponseGetAssetDetails | ResponseIssue | ResponseReissue | ResponseBurn | ResponseDelegate | ResponseGetDelegateInfo | ResponseRecoverVtxos | ResponseRecoverVtxosEvent | ResponseGetRecoverableBalance | ResponseGetExpiringVtxos | ResponseRenewVtxos | ResponseRenewVtxosEvent | ResponseGetExpiredBoardingUtxos | ResponseSweepExpiredBoardingUtxos);
|
|
478
|
+
export type WalletUpdaterRequest = RequestInitWallet | RequestSettle | RequestSendBitcoin | RequestGetAddress | RequestGetBoardingAddress | RequestGetBalance | RequestGetVtxos | RequestGetBoardingUtxos | RequestGetTransactionHistory | RequestGetStatus | RequestClear | RequestReloadWallet | RequestSignTransaction | RequestCreateContract | RequestGetContracts | RequestGetContractsWithVtxos | RequestAnnotateVtxos | RequestUpdateContract | RequestDeleteContract | RequestGetSpendablePaths | RequestGetAllSpendingPaths | RequestIsContractManagerWatching | RequestRefreshVtxos | RequestRefreshOutpoints | RequestSend | RequestGetAssetDetails | RequestIssue | RequestReissue | RequestBurn | RequestDelegate | RequestGetDelegateInfo | RequestRecoverVtxos | RequestGetRecoverableBalance | RequestGetExpiringVtxos | RequestRenewVtxos | RequestGetExpiredBoardingUtxos | RequestSweepExpiredBoardingUtxos;
|
|
479
|
+
export type WalletUpdaterResponse = ResponseEnvelope & (ResponseInitWallet | ResponseSettle | ResponseSettleEvent | ResponseSendBitcoin | ResponseGetAddress | ResponseGetBoardingAddress | ResponseGetBalance | ResponseGetVtxos | ResponseGetBoardingUtxos | ResponseGetTransactionHistory | ResponseGetStatus | ResponseClear | ResponseReloadWallet | ResponseUtxoUpdate | ResponseVtxoUpdate | ResponseSignTransaction | ResponseCreateContract | ResponseGetContracts | ResponseGetContractsWithVtxos | ResponseAnnotateVtxos | ResponseUpdateContract | ResponseDeleteContract | ResponseGetSpendablePaths | ResponseGetAllSpendingPaths | ResponseIsContractManagerWatching | ResponseRefreshVtxos | ResponseRefreshOutpoints | ResponseContractEvent | ResponseSend | ResponseGetAssetDetails | ResponseIssue | ResponseReissue | ResponseBurn | ResponseDelegate | ResponseGetDelegateInfo | ResponseRecoverVtxos | ResponseRecoverVtxosEvent | ResponseGetRecoverableBalance | ResponseGetExpiringVtxos | ResponseRenewVtxos | ResponseRenewVtxosEvent | ResponseGetExpiredBoardingUtxos | ResponseSweepExpiredBoardingUtxos);
|
|
468
480
|
export declare class WalletMessageHandler implements MessageHandler<WalletUpdaterRequest, WalletUpdaterResponse> {
|
|
469
481
|
readonly messageTag: string;
|
|
470
482
|
private wallet;
|
|
@@ -410,13 +410,40 @@ export declare class VtxoManager implements AsyncDisposable, IVtxoManager {
|
|
|
410
410
|
/**
|
|
411
411
|
* VTXO_ALREADY_SPENT means the server's authoritative view of VTXO state
|
|
412
412
|
* is ahead of ours — cross-instance race, pre-lock snapshot drift, or an
|
|
413
|
-
* SSE gap left stale data in the local cache. Silent-swallowing
|
|
414
|
-
* the same error on the next cycle because nothing
|
|
415
|
-
*
|
|
416
|
-
*
|
|
417
|
-
*
|
|
413
|
+
* SSE gap left stale data in the local cache. Silent-swallowing
|
|
414
|
+
* guarantees the same error on the next cycle because nothing
|
|
415
|
+
* reconciles the cache.
|
|
416
|
+
*
|
|
417
|
+
* The cursor-derived delta sync filters by `created_at`, so a VTXO that
|
|
418
|
+
* was created before the cursor but spent recently can never be
|
|
419
|
+
* reconciled by `refreshVtxos()`. Use `refreshOutpoints` for surgical
|
|
420
|
+
* recovery: query the indexer for the specific stale outpoint and
|
|
421
|
+
* upsert its authoritative state into the wallet repository.
|
|
422
|
+
*
|
|
423
|
+
* Throttled because the same VTXO can fire repeatedly before the
|
|
424
|
+
* upsert observably propagates through the renewal selector.
|
|
418
425
|
*/
|
|
419
426
|
private maybeRefreshAfterVtxoSpent;
|
|
427
|
+
/**
|
|
428
|
+
* Extract the offending VTXO outpoint from a `VTXO_ALREADY_SPENT` error,
|
|
429
|
+
* if the server attached one in `metadata.vtxo_outpoint`. Returns
|
|
430
|
+
* `undefined` when the error isn't a parsed ArkError, isn't this code,
|
|
431
|
+
* or doesn't carry the metadata.
|
|
432
|
+
*/
|
|
433
|
+
private extractSpentOutpoint;
|
|
434
|
+
/**
|
|
435
|
+
* Reconcile the chosen VTXOs with the indexer's authoritative state
|
|
436
|
+
* before submitting a settle intent. Pulls the canonical record for
|
|
437
|
+
* each candidate outpoint via {@link IContractManager.refreshOutpoints}
|
|
438
|
+
* (which upserts the result into the wallet repository), then
|
|
439
|
+
* re-selects through the standard expiring-vtxo filter so anything
|
|
440
|
+
* the refresh flagged as spent is dropped.
|
|
441
|
+
*
|
|
442
|
+
* Best-effort: a failed refresh just falls back to the original
|
|
443
|
+
* candidates and lets the post-submit `VTXO_ALREADY_SPENT` recovery
|
|
444
|
+
* handle whatever slipped through.
|
|
445
|
+
*/
|
|
446
|
+
private revalidateBeforeSettle;
|
|
420
447
|
/** Computes the next poll delay, applying exponential backoff on failures. */
|
|
421
448
|
private getNextPollDelay;
|
|
422
449
|
/**
|