@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
@@ -74,7 +74,6 @@ export declare function decodeArkContract(encoded: string): ParsedArkContract;
74
74
  export declare function contractFromArkContract(encoded: string, options?: {
75
75
  label?: string;
76
76
  state?: "active" | "inactive";
77
- expiresAt?: number;
78
77
  metadata?: Record<string, unknown>;
79
78
  }): Omit<Contract, "script" | "address"> & {
80
79
  script?: string;
@@ -92,7 +91,6 @@ export declare function contractFromArkContract(encoded: string, options?: {
92
91
  export declare function contractFromArkContractWithAddress(encoded: string, serverPubKey: Uint8Array, addressPrefix: string, options?: {
93
92
  label?: string;
94
93
  state?: "active" | "inactive";
95
- expiresAt?: number;
96
94
  metadata?: Record<string, unknown>;
97
95
  }): Contract;
98
96
  /**
@@ -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 { VirtualCoin } from "../wallet";
5
+ import { ExtendedVirtualCoin, VirtualCoin } from "../wallet";
6
6
  import { ContractRepository } from "../repositories";
7
7
  export type RefreshVtxosOptions = {
8
8
  scripts?: string[];
@@ -36,6 +36,19 @@ export interface IContractManager extends Disposable {
36
36
  * If no filter is provided, returns all contracts with their virtual outputs.
37
37
  */
38
38
  getContractsWithVtxos(filter?: GetContractsFilter): Promise<ContractWithVtxos[]>;
39
+ /**
40
+ * Stamp raw virtual outputs with the correct per-contract tapscripts
41
+ * (forfeit, intent, tap tree).
42
+ *
43
+ * Resolves each vtxo's `script` to its owning contract via the contract
44
+ * repository and attaches the matching tapscripts. Throws when any vtxo
45
+ * references a script with no registered contract — callers are expected
46
+ * to register the contract before asking for annotation. This is the
47
+ * single shared path that replaces scattered `extendVirtualCoin*` calls
48
+ * in wallet/handler code, and keeps the wallet from silently stamping the
49
+ * default tapscript onto a non-default vtxo.
50
+ */
51
+ annotateVtxos(vtxos: VirtualCoin[]): Promise<ExtendedVirtualCoin[]>;
39
52
  /**
40
53
  * Update mutable contract fields.
41
54
  *
@@ -135,7 +148,7 @@ export type CreateContractParams = Omit<Contract, "createdAt" | "state"> & {
135
148
  * - Create and persist contracts
136
149
  * - Query stored contracts (optionally with their virtual outputs)
137
150
  * - Provide spendable path selection for a contract
138
- * - Emit contract-related events (virtual output received/spent/expired, connection reset)
151
+ * - Emit contract-related events (virtual output received/spent, connection reset)
139
152
  *
140
153
  * Notes:
141
154
  * - Implementations typically start watching automatically during initialization
@@ -181,7 +194,6 @@ export declare class ContractManager implements IContractManager {
181
194
  private initialized;
182
195
  private eventCallbacks;
183
196
  private stopWatcherFn?;
184
- private syncVtxosCallInflight?;
185
197
  private constructor();
186
198
  /**
187
199
  * Static factory method for creating a new ContractManager.
@@ -194,6 +206,17 @@ export declare class ContractManager implements IContractManager {
194
206
  */
195
207
  static create(config: ContractManagerConfig): Promise<ContractManager>;
196
208
  private initialize;
209
+ /**
210
+ * Delta-sync the full watched set and reconcile the pending frontier.
211
+ *
212
+ * Shared recovery path used on initial boot and after a subscription
213
+ * reconnect. `syncContracts({})` scopes to the current watched set
214
+ * (see {@link ContractWatcher.getWatchedContracts}), uses the
215
+ * cursor-derived delta window, and advances the cursor on success.
216
+ * `reconcilePendingFrontier` catches not-yet-finalized virtual
217
+ * outputs that could sit outside any delta window.
218
+ */
219
+ private reconcileWatched;
197
220
  /**
198
221
  * Create and register a new contract.
199
222
  *
@@ -218,6 +241,7 @@ export declare class ContractManager implements IContractManager {
218
241
  */
219
242
  getContracts(filter?: GetContractsFilter): Promise<Contract[]>;
220
243
  getContractsWithVtxos(filter?: GetContractsFilter, pageSize?: number): Promise<ContractWithVtxos[]>;
244
+ annotateVtxos(vtxos: VirtualCoin[]): Promise<ExtendedVirtualCoin[]>;
221
245
  private buildContractsDbFilter;
222
246
  /**
223
247
  * Update a contract.
@@ -281,8 +305,9 @@ export declare class ContractManager implements IContractManager {
281
305
  /**
282
306
  * Force refresh virtual outputs from the indexer.
283
307
  *
284
- * Without options, clears all sync cursors and re-fetches every contract.
308
+ * Without options, re-fetches every contract and advances the global cursor.
285
309
  * With options, narrows the refresh to specific scripts and/or a time window.
310
+ * Subset refreshes (scripts filter) intentionally do not advance the cursor.
286
311
  */
287
312
  refreshVtxos(opts?: RefreshVtxosOptions): Promise<void>;
288
313
  /**
@@ -299,11 +324,16 @@ export declare class ContractManager implements IContractManager {
299
324
  private handleContractEvent;
300
325
  private getVtxosForContracts;
301
326
  /**
302
- * Incrementally sync virtual outputs for the given contracts.
303
- * Uses per-script cursors to fetch only what changed since the last sync.
304
- * Scripts without a cursor are bootstrapped with a full fetch.
327
+ * Sync virtual outputs for the given contracts against the indexer.
328
+ *
329
+ * When `options.contracts` is omitted the sync covers the full
330
+ * watched set (active contracts plus any inactive contracts still
331
+ * holding cached VTXOs) and the global cursor is advanced on
332
+ * success. Passing an explicit subset leaves the cursor alone so a
333
+ * narrow poll can't hide data that other contracts still need to
334
+ * pick up.
305
335
  */
306
- private deltaSyncContracts;
336
+ private syncContracts;
307
337
  /**
308
338
  * Fetch all pending (unfinalized) virtual outputs and upsert them into the
309
339
  * repository. This catches virtual outputs whose state changed outside the delta
@@ -312,7 +342,6 @@ export declare class ContractManager implements IContractManager {
312
342
  private reconcilePendingFrontier;
313
343
  private fetchContractVxosFromIndexer;
314
344
  private fetchContractVtxosBulk;
315
- private fetchContractVtxosPaginated;
316
345
  /**
317
346
  * Dispose of the ContractManager and release all resources.
318
347
  *
@@ -108,6 +108,14 @@ export declare class ContractWatcher {
108
108
  * (which may cause them to be watched even if inactive).
109
109
  */
110
110
  addContract(contract: Contract): Promise<void>;
111
+ /**
112
+ * Pre-populate `lastKnownVtxos` from the wallet repository.
113
+ *
114
+ * Runs on add (and can be re-run after reconnect) so polling always
115
+ * compares the indexer's view against what is already persisted,
116
+ * emitting only genuine deltas.
117
+ */
118
+ private seedLastKnownVtxos;
111
119
  /**
112
120
  * Update an existing contract.
113
121
  */
@@ -121,20 +129,17 @@ export declare class ContractWatcher {
121
129
  */
122
130
  getAllContracts(): Contract[];
123
131
  /**
124
- * Get all active in-memory contracts.
125
- */
126
- getActiveContracts(): Contract[];
127
- /**
128
- * Get scripts that should be watched.
132
+ * Contracts the watcher is actually tracking:
133
+ * - all active contracts, plus
134
+ * - inactive contracts that still hold known virtual outputs
135
+ * (the subscription keeps watching them so `vtxo_spent` events for
136
+ * those unspent outputs are still observed).
129
137
  *
130
- * Returns scripts for:
131
- * - All active contracts
132
- * - All contracts with known virtual outputs (regardless of state)
133
- *
134
- * This ensures we continue monitoring contracts even after they're
135
- * deactivated, as long as they have unspent virtual outputs.
138
+ * This is the single source of truth for "contracts whose VTXO state
139
+ * we still care about" — callers and the subscription itself fan out
140
+ * over the same set so nothing is reconciled that isn't also watched.
136
141
  */
137
- private getScriptsToWatch;
142
+ getWatchedContracts(): Contract[];
138
143
  /**
139
144
  * Get virtual outputs for contracts, grouped by contract script.
140
145
  * @see WalletRepository for `repo`
@@ -161,10 +166,6 @@ export declare class ContractWatcher {
161
166
  * Useful for manual refresh or after app resume.
162
167
  */
163
168
  forcePoll(): Promise<void>;
164
- /**
165
- * Check for expired contracts, update their state, and emit events.
166
- */
167
- private checkExpiredContracts;
168
169
  /**
169
170
  * Connect to the subscription.
170
171
  */
@@ -177,9 +178,6 @@ export declare class ContractWatcher {
177
178
  * Start the failsafe polling interval.
178
179
  */
179
180
  private startFailsafePolling;
180
- /**
181
- * Poll all active contracts for current state.
182
- */
183
181
  private pollAllContracts;
184
182
  /**
185
183
  * Poll specific contracts and emit events for changes.
@@ -201,8 +199,11 @@ export declare class ContractWatcher {
201
199
  */
202
200
  private handleSubscriptionUpdate;
203
201
  /**
204
- * Process virtual outputs from subscription and route to correct contracts.
205
- * Uses the scripts from the subscription response to determine contract ownership.
202
+ * Process virtual outputs from subscription and route each VTXO to the
203
+ * single contract that actually locks it via `vtxo.script`. If the script
204
+ * doesn't match any watched contract, skip the VTXO rather than fan it
205
+ * out to every matching contract — fan-out produced phantom state in
206
+ * non-owning contracts that then never reconciled.
206
207
  */
207
208
  private processSubscriptionVtxos;
208
209
  /**
@@ -58,8 +58,6 @@ export interface Contract {
58
58
  state: ContractState;
59
59
  /** Unix timestamp in milliseconds when this contract was created. */
60
60
  createdAt: number;
61
- /** Unix timestamp in milliseconds when this contract expires. */
62
- expiresAt?: number;
63
61
  /**
64
62
  * Optional metadata for external integrations.
65
63
  */
@@ -194,11 +192,6 @@ export type ContractEvent = {
194
192
  vtxos: ContractVtxo[];
195
193
  contract: Contract;
196
194
  timestamp: number;
197
- } | {
198
- type: "contract_expired";
199
- contractScript: string;
200
- contract: Contract;
201
- timestamp: number;
202
195
  } | {
203
196
  type: "connection_reset";
204
197
  timestamp: number;
@@ -7,11 +7,14 @@ export declare function getGlobalObject(): {
7
7
  *
8
8
  * @param dbName The name of the database to open.
9
9
  * @param dbVersion The database version to open.
10
- * @param initDatabase A function that migrates the database schema, called on `onupgradeneeded` only.
10
+ * @param initDatabase A function that migrates the database schema, called
11
+ * on `onupgradeneeded` only. Receives the database, the previous version
12
+ * (0 for fresh installs), and the upgrade transaction — the transaction is
13
+ * required for data migrations (cursor/update on existing stores).
11
14
  *
12
15
  * @returns A promise that resolves to the database instance.
13
16
  */
14
- export declare function openDatabase(dbName: string, dbVersion: number, initDatabase: (db: IDBDatabase) => void): Promise<IDBDatabase>;
17
+ export declare function openDatabase(dbName: string, dbVersion: number, initDatabase: (db: IDBDatabase, oldVersion: number, transaction: IDBTransaction | null) => void): Promise<IDBDatabase>;
15
18
  /**
16
19
  * Decrements the reference count and closes the database when no references remain.
17
20
  *
@@ -4,5 +4,6 @@ export declare const STORE_TRANSACTIONS = "transactions";
4
4
  export declare const STORE_WALLET_STATE = "walletState";
5
5
  export declare const STORE_CONTRACTS = "contracts";
6
6
  export declare const LEGACY_STORE_CONTRACT_COLLECTIONS = "contractsCollections";
7
- export declare const DB_VERSION = 2;
8
- export declare function initDatabase(db: IDBDatabase): void;
7
+ export declare const DB_VERSION = 3;
8
+ export declare function initDatabase(db: IDBDatabase, oldVersion: number, transaction: IDBTransaction | null): void;
9
+ export declare function backfillVtxoScripts(transaction: IDBTransaction): void;
@@ -1,4 +1,4 @@
1
1
  export { RealmWalletRepository } from "./walletRepository";
2
2
  export { RealmContractRepository } from "./contractRepository";
3
- export { ArkRealmSchemas } from "./schemas";
3
+ export { ArkRealmSchemas, ARK_REALM_SCHEMA_VERSION, runArkRealmMigrations, } from "./schemas";
4
4
  export type { RealmLike, RealmResults } from "./types";
@@ -35,6 +35,10 @@ export declare const ArkVtxoSchema: {
35
35
  readonly isUnrolled: "bool";
36
36
  readonly isSpent: "bool?";
37
37
  readonly assetsJson: "string?";
38
+ readonly script: {
39
+ readonly type: "string";
40
+ readonly indexed: true;
41
+ };
38
42
  };
39
43
  };
40
44
  export declare const ArkUtxoSchema: {
@@ -138,6 +142,10 @@ export declare const ArkRealmSchemas: ({
138
142
  readonly isUnrolled: "bool";
139
143
  readonly isSpent: "bool?";
140
144
  readonly assetsJson: "string?";
145
+ readonly script: {
146
+ readonly type: "string";
147
+ readonly indexed: true;
148
+ };
141
149
  };
142
150
  } | {
143
151
  readonly name: "ArkUtxo";
@@ -206,3 +214,36 @@ export declare const ArkRealmSchemas: ({
206
214
  readonly metadataJson: "string?";
207
215
  };
208
216
  })[];
217
+ /**
218
+ * Current Realm schema version for the Arkade wallet.
219
+ *
220
+ * Consumers opening Realm must pass a `schemaVersion` at least this high so
221
+ * legacy databases get migrated; merge it with your own app's version:
222
+ *
223
+ * ```ts
224
+ * await Realm.open({
225
+ * schema: [...ArkRealmSchemas, ...yourSchemas],
226
+ * schemaVersion: Math.max(ARK_REALM_SCHEMA_VERSION, yourSchemaVersion),
227
+ * onMigration: (oldRealm, newRealm) => {
228
+ * runArkRealmMigrations(oldRealm, newRealm);
229
+ * // your own migrations
230
+ * },
231
+ * });
232
+ * ```
233
+ *
234
+ * History:
235
+ * - v1: initial ArkVtxo/ArkUtxo/... schemas, `script` nullable.
236
+ * - v2: ArkVtxo.script becomes required; NULL values are backfilled from
237
+ * the owning Ark address during migration.
238
+ */
239
+ export declare const ARK_REALM_SCHEMA_VERSION = 2;
240
+ /**
241
+ * Run every Arkade schema migration applicable to the open Realm.
242
+ *
243
+ * Designed to be composed with the consumer's own migrations inside a single
244
+ * `onMigration` callback. Each migration step does a per-row check so it
245
+ * remains idempotent and independent of the app's global `schemaVersion` —
246
+ * a consumer whose app is already at version 10 can still trigger the
247
+ * Arkade v1→v2 script backfill when the row has never been populated.
248
+ */
249
+ export declare function runArkRealmMigrations(oldRealm: any, newRealm: any): void;
@@ -0,0 +1,9 @@
1
+ /**
2
+ * Compute the hex-encoded `scriptPubKey` locking a VTXO from its owning Ark
3
+ * address. Used by repository-layer migrations to backfill `script` on legacy
4
+ * rows that pre-date the column (the indexer now guarantees the field, so new
5
+ * rows never go through this path). The `script` field is required by the
6
+ * domain type, so backfill must produce the same value the indexer would
7
+ * have returned — which is the hex of the address's `pkScript`.
8
+ */
9
+ export declare function scriptFromArkAddress(address: string): string;
@@ -20,7 +20,7 @@ export declare const serializeVtxo: (v: ExtendedVirtualCoin) => {
20
20
  isUnrolled: boolean;
21
21
  isSpent?: boolean;
22
22
  assets?: import("../wallet").Asset[];
23
- script?: string;
23
+ script: string;
24
24
  value: number;
25
25
  status: import("../wallet").Status;
26
26
  txid: string;
@@ -23,6 +23,28 @@ export declare class SQLiteWalletRepository implements WalletRepository {
23
23
  constructor(db: SQLExecutor, options?: SQLiteWalletRepositoryOptions);
24
24
  private ensureInit;
25
25
  private init;
26
+ /**
27
+ * Bring the `vtxos` table to the current schema (v1 = `script` NOT NULL).
28
+ *
29
+ * Three cases:
30
+ * - Fresh install: create the v1 schema directly.
31
+ * - Legacy install without a `script` column: add it, backfill from
32
+ * `address`, then rebuild the table with NOT NULL (SQLite cannot add
33
+ * the NOT NULL constraint in place).
34
+ * - Legacy install with a nullable `script` column: backfill the NULLs
35
+ * and rebuild.
36
+ *
37
+ * The backfill derives `script` from the Ark address, matching what the
38
+ * indexer would have returned — new rows from the indexer always carry a
39
+ * populated `script`, so the migration is idempotent.
40
+ *
41
+ * The rebuild path is wrapped in a transaction: without it, a crash
42
+ * between the `DROP TABLE vtxos` and the `RENAME tmp → vtxos` commits
43
+ * would leave the next startup seeing no `vtxos` table and create a
44
+ * fresh empty one, silently orphaning every row in the temp table.
45
+ */
46
+ private migrateVtxosTable;
47
+ private vtxosCreateSql;
26
48
  [Symbol.asyncDispose](): Promise<void>;
27
49
  clear(): Promise<void>;
28
50
  getVtxos(address: string): Promise<ExtendedVirtualCoin[]>;
@@ -1,9 +1,17 @@
1
1
  import { ArkTransaction, ExtendedCoin, ExtendedVirtualCoin } from "../wallet";
2
2
  export interface WalletState {
3
- /** Timestamp of the last successful wallet sync, in milliseconds. */
4
- lastSyncTime?: number;
5
3
  /** Arbitrary stored wallet settings. */
6
4
  settings?: Record<string, any>;
5
+ /**
6
+ * High-water mark for VTXO indexer syncs, in milliseconds.
7
+ *
8
+ * Reused the legacy `lastSyncTime` column name to avoid an
9
+ * `ALTER TABLE` migration; the value is interpreted as the new
10
+ * "max indexer `updatedAt`" cursor only after `settings.vtxoCursorMigrated`
11
+ * is set, so pre-existing values written by the buggy pre-PR sync
12
+ * are ignored and force a one-shot re-bootstrap on upgrade.
13
+ */
14
+ lastSyncTime?: number;
7
15
  }
8
16
  /** Stored commitment transaction metadata. */
9
17
  export type CommitmentTxRecord = {
@@ -2,8 +2,7 @@ import { WalletRepository, WalletState } from "../repositories/walletRepository"
2
2
  /** Lag behind real-time to avoid racing with indexer writes. */
3
3
  export declare const SAFETY_LAG_MS = 30000;
4
4
  /** Overlap window so boundary virtual outputs are never missed. */
5
- export declare const OVERLAP_MS = 60000;
6
- type SyncCursors = Record<string, number>;
5
+ export declare const OVERLAP_MS: number;
7
6
  /**
8
7
  * Atomically read, mutate, and persist wallet state.
9
8
  * All callers that modify wallet state should go through this helper
@@ -11,39 +10,43 @@ type SyncCursors = Record<string, number>;
11
10
  */
12
11
  export declare function updateWalletState(repo: WalletRepository, updater: (state: WalletState) => WalletState): Promise<void>;
13
12
  /**
14
- * Read the high-water mark for a single script.
15
- * Returns `undefined` when the script has never been synced (bootstrap case).
16
- */
17
- export declare function getSyncCursor(repo: WalletRepository, script: string): Promise<number | undefined>;
18
- /**
19
- * Read cursors for every previously-synced script.
20
- */
21
- export declare function getAllSyncCursors(repo: WalletRepository): Promise<SyncCursors>;
22
- /**
23
- * Advance the cursor for one script after a successful delta sync.
24
- * `cursor` should be the `before` cutoff used in the request.
13
+ * Read the global high-water mark for VTXO indexer syncs.
14
+ *
15
+ * Returns `0` when:
16
+ * - the wallet has never been synced (bootstrap case), or
17
+ * - the stored `lastSyncTime` was written by pre-PR code and is not
18
+ * safe to reuse under the new semantics (see {@link CURSOR_MIGRATED_KEY}).
25
19
  */
26
- export declare function advanceSyncCursor(repo: WalletRepository, script: string, cursor: number): Promise<void>;
20
+ export declare function getSyncCursor(repo: WalletRepository): Promise<number>;
27
21
  /**
28
- * Advance cursors for multiple scripts in a single write.
22
+ * Advance the global cursor after a successful full-scope delta sync.
23
+ *
24
+ * Clamped with `Math.max` against the current value so concurrent syncs
25
+ * that finish out of order can't rewind the cursor: `lastUpdatedAt` is
26
+ * captured before each sync enters the `updateWalletState` mutex, and
27
+ * the later-started sync would otherwise overwrite the earlier-captured
28
+ * one with a smaller value. The legacy value is discarded on the first
29
+ * advance if the migration marker is absent so pre-PR data doesn't
30
+ * survive the upgrade.
29
31
  */
30
- export declare function advanceSyncCursors(repo: WalletRepository, updates: Record<string, number>): Promise<void>;
32
+ export declare function advanceSyncCursor(repo: WalletRepository, lastUpdatedAt: number): Promise<void>;
31
33
  /**
32
- * Remove sync cursors, forcing a full re-bootstrap on next sync.
33
- * When `scripts` is provided, only those cursors are cleared.
34
+ * Remove the sync cursor, forcing a full re-bootstrap on next sync.
35
+ *
36
+ * Also clears the migration marker so any stored `lastSyncTime` is
37
+ * treated as untrusted on the next read.
34
38
  */
35
- export declare function clearSyncCursors(repo: WalletRepository, scripts?: string[]): Promise<void>;
39
+ export declare function clearSyncCursor(repo: WalletRepository): Promise<void>;
36
40
  /**
37
41
  * Compute the `after` lower-bound for a delta sync query.
38
- * Returns `undefined` when the script has no cursor (bootstrap needed).
39
42
  *
40
43
  * No upper bound (`before`) is applied to the query so that freshly
41
44
  * created virtual outputs are never excluded. The safety lag is applied only
42
45
  * when advancing the cursor (see @see cursorCutoff).
43
46
  */
44
- export declare function computeSyncWindow(cursor: number | undefined): {
47
+ export declare function computeSyncWindow(cursor: number): {
45
48
  after: number;
46
- } | undefined;
49
+ };
47
50
  /**
48
51
  * The safe high-water mark for cursor advancement.
49
52
  * Lags behind real-time by @see SAFETY_LAG_MS so that virtual outputs still
@@ -55,4 +58,3 @@ export declare function computeSyncWindow(cursor: number | undefined): {
55
58
  * data they actually observed.
56
59
  */
57
60
  export declare function cursorCutoff(requestStartedAt?: number): number;
58
- export {};
@@ -480,7 +480,7 @@ export interface VirtualCoin extends Coin {
480
480
  /** Assets carried by this virtual output, if any. */
481
481
  assets?: Asset[];
482
482
  /** The scriptPubKey (hex) locking this virtual output, as returned by the indexer. */
483
- script?: string;
483
+ script: string;
484
484
  }
485
485
  /** Wallet transaction direction. */
486
486
  export declare enum TxType {
@@ -1,7 +1,7 @@
1
1
  import { SettlementEvent } from "../../providers/ark";
2
2
  import type { Contract, ContractEvent, ContractWithVtxos, GetContractsFilter, PathSelection } from "../../contracts";
3
3
  import type { CreateContractParams, GetAllSpendingPathsOptions, GetSpendablePathsOptions } from "../../contracts/contractManager";
4
- import { ArkTransaction, AssetDetails, BurnParams, ExtendedCoin, ExtendedVirtualCoin, GetVtxosFilter, IssuanceParams, IssuanceResult, IWallet, Recipient, ReissuanceParams, SendBitcoinParams, SettleParams, WalletBalance } from "../index";
4
+ import { ArkTransaction, AssetDetails, BurnParams, ExtendedCoin, ExtendedVirtualCoin, GetVtxosFilter, IssuanceParams, IssuanceResult, IWallet, Recipient, ReissuanceParams, SendBitcoinParams, SettleParams, VirtualCoin, WalletBalance } from "../index";
5
5
  import { DelegateInfo } from "../../providers/delegator";
6
6
  import { MessageHandler, RequestEnvelope, ResponseEnvelope } from "../../worker/messageBus";
7
7
  import { Transaction } from "../../utils/transaction";
@@ -182,6 +182,18 @@ export type ResponseGetContractsWithVtxos = ResponseEnvelope & {
182
182
  contracts: ContractWithVtxos[];
183
183
  };
184
184
  };
185
+ export type RequestAnnotateVtxos = RequestEnvelope & {
186
+ type: "ANNOTATE_VTXOS";
187
+ payload: {
188
+ vtxos: VirtualCoin[];
189
+ };
190
+ };
191
+ export type ResponseAnnotateVtxos = ResponseEnvelope & {
192
+ type: "ANNOTATED_VTXOS";
193
+ payload: {
194
+ vtxos: ExtendedVirtualCoin[];
195
+ };
196
+ };
185
197
  export type RequestUpdateContract = RequestEnvelope & {
186
198
  type: "UPDATE_CONTRACT";
187
199
  payload: {
@@ -443,8 +455,8 @@ export type ResponseSweepExpiredBoardingUtxos = ResponseEnvelope & {
443
455
  txid: string;
444
456
  };
445
457
  };
446
- export type WalletUpdaterRequest = RequestInitWallet | RequestSettle | RequestSendBitcoin | RequestGetAddress | RequestGetBoardingAddress | RequestGetBalance | RequestGetVtxos | RequestGetBoardingUtxos | RequestGetTransactionHistory | RequestGetStatus | RequestClear | RequestReloadWallet | RequestSignTransaction | RequestCreateContract | RequestGetContracts | RequestGetContractsWithVtxos | RequestUpdateContract | RequestDeleteContract | RequestGetSpendablePaths | RequestGetAllSpendingPaths | RequestIsContractManagerWatching | RequestRefreshVtxos | RequestSend | RequestGetAssetDetails | RequestIssue | RequestReissue | RequestBurn | RequestDelegate | RequestGetDelegateInfo | RequestRecoverVtxos | RequestGetRecoverableBalance | RequestGetExpiringVtxos | RequestRenewVtxos | RequestGetExpiredBoardingUtxos | RequestSweepExpiredBoardingUtxos;
447
- export type WalletUpdaterResponse = ResponseEnvelope & (ResponseInitWallet | ResponseSettle | ResponseSettleEvent | ResponseSendBitcoin | ResponseGetAddress | ResponseGetBoardingAddress | ResponseGetBalance | ResponseGetVtxos | ResponseGetBoardingUtxos | ResponseGetTransactionHistory | ResponseGetStatus | ResponseClear | ResponseReloadWallet | ResponseUtxoUpdate | ResponseVtxoUpdate | ResponseSignTransaction | ResponseCreateContract | ResponseGetContracts | ResponseGetContractsWithVtxos | ResponseUpdateContract | ResponseDeleteContract | ResponseGetSpendablePaths | ResponseGetAllSpendingPaths | ResponseIsContractManagerWatching | ResponseRefreshVtxos | ResponseContractEvent | ResponseSend | ResponseGetAssetDetails | ResponseIssue | ResponseReissue | ResponseBurn | ResponseDelegate | ResponseGetDelegateInfo | ResponseRecoverVtxos | ResponseRecoverVtxosEvent | ResponseGetRecoverableBalance | ResponseGetExpiringVtxos | ResponseRenewVtxos | ResponseRenewVtxosEvent | ResponseGetExpiredBoardingUtxos | ResponseSweepExpiredBoardingUtxos);
458
+ 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;
459
+ 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);
448
460
  export declare class WalletMessageHandler implements MessageHandler<WalletUpdaterRequest, WalletUpdaterResponse> {
449
461
  readonly messageTag: string;
450
462
  private wallet;
@@ -4,13 +4,29 @@ import type { Contract } from "../contracts/types";
4
4
  import { ReadonlyWallet } from "./wallet";
5
5
  import { Bytes } from "@scure/btc-signer/utils";
6
6
  export declare const DUST_AMOUNT = 546;
7
- export declare function extendVirtualCoin(wallet: {
8
- offchainTapscript: ReadonlyWallet["offchainTapscript"];
9
- }, vtxo: VirtualCoin): ExtendedVirtualCoin;
10
7
  export declare function extendCoin(wallet: {
11
8
  boardingTapscript: ReadonlyWallet["boardingTapscript"];
12
9
  }, utxo: Coin): ExtendedCoin;
13
- export declare function extendVtxoFromContract(vtxo: VirtualCoin, contract: Contract): ExtendedVirtualCoin;
10
+ /**
11
+ * Extend a VirtualCoin with the tap scripts of whichever contract locks it.
12
+ *
13
+ * The second argument accepts either form, so each callsite passes what it
14
+ * already has:
15
+ * - a single `Contract` (when the caller already knows the owning contract,
16
+ * e.g. the contract manager iterating its own `scriptToContract` map), or
17
+ * - a `ReadonlyMap<script, Contract>` (when the caller resolves by
18
+ * `vtxo.script`, populated by the indexer).
19
+ *
20
+ * Throws when no contract can be resolved — there is intentionally no
21
+ * default-tapscript fallback. When the wallet owns multiple contracts
22
+ * (default + delegate, several active vHTLCs, etc.) a default-tapscript path
23
+ * silently stamps every VTXO with the same forfeit/intent data, overwriting
24
+ * the correct data for any VTXO locked to a non-default contract. Callers
25
+ * must feed a Contract or a populated script→Contract map; otherwise the
26
+ * caller (typically `ContractManager.annotateVtxos`) should fetch the owning
27
+ * contract first.
28
+ */
29
+ export declare function extendVirtualCoinForContract(vtxo: VirtualCoin, contractOrMap?: Contract | ReadonlyMap<string, Contract>): ExtendedVirtualCoin;
14
30
  export declare function getRandomId(): string;
15
31
  export declare function isValidArkAddress(address: string): boolean;
16
32
  type ValidatedRecipient = Required<Recipient> & {
@@ -220,6 +220,13 @@ export declare class VtxoManager implements AsyncDisposable, IVtxoManager {
220
220
  private renewalInProgress;
221
221
  private lastRenewalTimestamp;
222
222
  private static readonly RENEWAL_COOLDOWN_MS;
223
+ private lastPeriodicSettleTimestamp;
224
+ private consecutivePeriodicSettleFailures;
225
+ private static readonly PERIODIC_SETTLE_COOLDOWN_MS;
226
+ private static readonly PERIODIC_SETTLE_MAX_BACKOFF_MS;
227
+ private lastVtxoSpentRefreshTimestamp;
228
+ private vtxoSpentRefreshPromise?;
229
+ private static readonly VTXO_SPENT_REFRESH_COOLDOWN_MS;
223
230
  constructor(wallet: IWallet,
224
231
  /** @deprecated Use settlementConfig instead */
225
232
  renewalConfig?: RenewalConfig | undefined, settlementConfig?: SettlementConfig | false);
@@ -398,6 +405,16 @@ export declare class VtxoManager implements AsyncDisposable, IVtxoManager {
398
405
  /** Returns the wallet's identity for transaction signing. */
399
406
  private getIdentity;
400
407
  private initializeSubscription;
408
+ /**
409
+ * VTXO_ALREADY_SPENT means the server's authoritative view of VTXO state
410
+ * is ahead of ours — cross-instance race, pre-lock snapshot drift, or an
411
+ * SSE gap left stale data in the local cache. Silent-swallowing guarantees
412
+ * the same error on the next cycle because nothing reconciles the cache,
413
+ * so instead we trigger a full refreshVtxos() to advance the global sync
414
+ * cursor. Throttled to prevent a buggy indexer from causing a refresh
415
+ * storm.
416
+ */
417
+ private maybeRefreshAfterVtxoSpent;
401
418
  /** Computes the next poll delay, applying exponential backoff on failures. */
402
419
  private getNextPollDelay;
403
420
  /**
@@ -412,13 +429,19 @@ export declare class VtxoManager implements AsyncDisposable, IVtxoManager {
412
429
  private schedulePoll;
413
430
  private pollBoardingUtxos;
414
431
  /**
415
- * Auto-settle new (unexpired) boarding inputs into Arkade.
416
- * Skips UTXOs that are already expired (those are handled by sweep).
417
- * Only settles UTXOs not already in-flight (tracked in knownBoardingUtxos).
418
- * UTXOs are marked as known only after a successful settle, so failed
419
- * attempts will be retried on the next poll.
432
+ * Auto-settle new (unexpired) boarding inputs AND near-expiry VTXOs into
433
+ * Arkade in a single intent. Skips boarding UTXOs that are already expired
434
+ * (those are handled by sweep) and those already in-flight (tracked in
435
+ * knownBoardingUtxos). If the event-driven renewal path is currently
436
+ * running, VTXOs are omitted from this cycle to avoid double-spending.
437
+ *
438
+ * Failure bookkeeping: after every settle *attempt*, lastPeriodicSettleTimestamp
439
+ * is armed and consecutive failures are counted so the next attempt is
440
+ * blocked by an exponentially growing cooldown (capped). This stops a
441
+ * persistently failing input from producing identical RegisterIntent +
442
+ * DeleteIntent retries on every 60s poll.
420
443
  */
421
- private settleBoardingUtxos;
444
+ private runPeriodicSettle;
422
445
  dispose(): Promise<void>;
423
446
  [Symbol.asyncDispose](): Promise<void>;
424
447
  }