@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.
Files changed (68) 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 -199
  4. package/dist/cjs/contracts/contractWatcher.js +86 -115
  5. package/dist/cjs/repositories/indexedDB/manager.js +6 -3
  6. package/dist/cjs/repositories/indexedDB/schema.js +47 -2
  7. package/dist/cjs/repositories/indexedDB/walletRepository.js +21 -2
  8. package/dist/cjs/repositories/realm/contractRepository.js +0 -4
  9. package/dist/cjs/repositories/realm/index.js +3 -1
  10. package/dist/cjs/repositories/realm/schemas.js +50 -1
  11. package/dist/cjs/repositories/realm/walletRepository.js +8 -4
  12. package/dist/cjs/repositories/scriptFromAddress.js +16 -0
  13. package/dist/cjs/repositories/sqlite/contractRepository.js +2 -6
  14. package/dist/cjs/repositories/sqlite/walletRepository.js +121 -33
  15. package/dist/cjs/utils/syncCursors.js +48 -56
  16. package/dist/cjs/wallet/expo/background.js +0 -13
  17. package/dist/cjs/wallet/expo/wallet.js +1 -6
  18. package/dist/cjs/wallet/serviceWorker/wallet-message-handler.js +16 -7
  19. package/dist/cjs/wallet/serviceWorker/wallet.js +19 -0
  20. package/dist/cjs/wallet/utils.js +41 -10
  21. package/dist/cjs/wallet/vtxo-manager.js +153 -39
  22. package/dist/cjs/wallet/wallet.js +84 -202
  23. package/dist/cjs/worker/expo/processors/contractPollProcessor.js +9 -13
  24. package/dist/cjs/worker/expo/taskRunner.js +2 -11
  25. package/dist/esm/contracts/arkcontract.js +0 -2
  26. package/dist/esm/contracts/contractManager.js +113 -201
  27. package/dist/esm/contracts/contractWatcher.js +86 -115
  28. package/dist/esm/repositories/indexedDB/manager.js +6 -3
  29. package/dist/esm/repositories/indexedDB/schema.js +46 -2
  30. package/dist/esm/repositories/indexedDB/walletRepository.js +21 -2
  31. package/dist/esm/repositories/realm/contractRepository.js +0 -4
  32. package/dist/esm/repositories/realm/index.js +1 -1
  33. package/dist/esm/repositories/realm/schemas.js +48 -0
  34. package/dist/esm/repositories/realm/walletRepository.js +8 -4
  35. package/dist/esm/repositories/scriptFromAddress.js +13 -0
  36. package/dist/esm/repositories/sqlite/contractRepository.js +2 -6
  37. package/dist/esm/repositories/sqlite/walletRepository.js +121 -33
  38. package/dist/esm/utils/syncCursors.js +47 -53
  39. package/dist/esm/wallet/expo/background.js +0 -13
  40. package/dist/esm/wallet/expo/wallet.js +2 -7
  41. package/dist/esm/wallet/serviceWorker/wallet-message-handler.js +17 -8
  42. package/dist/esm/wallet/serviceWorker/wallet.js +19 -0
  43. package/dist/esm/wallet/utils.js +41 -9
  44. package/dist/esm/wallet/vtxo-manager.js +153 -39
  45. package/dist/esm/wallet/wallet.js +87 -205
  46. package/dist/esm/worker/expo/processors/contractPollProcessor.js +9 -13
  47. package/dist/esm/worker/expo/taskRunner.js +3 -12
  48. package/dist/types/contracts/arkcontract.d.ts +0 -2
  49. package/dist/types/contracts/contractManager.d.ts +38 -12
  50. package/dist/types/contracts/contractWatcher.d.ts +22 -21
  51. package/dist/types/contracts/types.d.ts +0 -7
  52. package/dist/types/repositories/indexedDB/manager.d.ts +5 -2
  53. package/dist/types/repositories/indexedDB/schema.d.ts +3 -2
  54. package/dist/types/repositories/realm/index.d.ts +1 -1
  55. package/dist/types/repositories/realm/schemas.d.ts +41 -0
  56. package/dist/types/repositories/scriptFromAddress.d.ts +9 -0
  57. package/dist/types/repositories/serialization.d.ts +1 -1
  58. package/dist/types/repositories/sqlite/walletRepository.d.ts +22 -0
  59. package/dist/types/repositories/walletRepository.d.ts +10 -2
  60. package/dist/types/utils/syncCursors.d.ts +25 -23
  61. package/dist/types/wallet/index.d.ts +1 -1
  62. package/dist/types/wallet/serviceWorker/wallet-message-handler.d.ts +15 -3
  63. package/dist/types/wallet/utils.d.ts +20 -4
  64. package/dist/types/wallet/vtxo-manager.d.ts +16 -6
  65. package/dist/types/wallet/wallet.d.ts +5 -17
  66. package/dist/types/worker/expo/processors/contractPollProcessor.d.ts +9 -4
  67. package/dist/types/worker/expo/taskRunner.d.ts +6 -3
  68. package/package.json +1 -1
@@ -39,6 +39,10 @@ const contractManager_1 = require("../contracts/contractManager");
39
39
  const handlers_1 = require("../contracts/handlers");
40
40
  const helpers_1 = require("../contracts/handlers/helpers");
41
41
  const syncCursors_1 = require("../utils/syncCursors");
42
+ // Hardcoded unilateral exit delay for mainnet (~7 days in seconds).
43
+ // Pinned here so that address derivation stays stable for existing mainnet
44
+ // wallets even after the server lowers the delay it advertises.
45
+ const MAINNET_UNILATERAL_EXIT_DELAY = 605184n;
42
46
  /**
43
47
  * Type guard function to check if an identity has a toReadonly method.
44
48
  */
@@ -134,10 +138,16 @@ class ReadonlyWallet {
134
138
  throw new Error("invalid exitTimelock");
135
139
  }
136
140
  }
141
+ // On mainnet, pin the unilateral exit delay to the historical value so
142
+ // that addresses derived by existing wallets remain stable even if the
143
+ // server starts advertising a shorter delay.
144
+ const unilateralExitDelay = info.network === "bitcoin"
145
+ ? MAINNET_UNILATERAL_EXIT_DELAY
146
+ : info.unilateralExitDelay;
137
147
  // create unilateral exit timelock
138
148
  const exitTimelock = config.exitTimelock ?? {
139
- value: info.unilateralExitDelay,
140
- type: info.unilateralExitDelay < 512n ? "blocks" : "seconds",
149
+ value: unilateralExitDelay,
150
+ type: unilateralExitDelay < 512n ? "blocks" : "seconds",
141
151
  };
142
152
  // validate boarding timelock passed in config if any
143
153
  if (config.boardingTimelock) {
@@ -292,14 +302,12 @@ class ReadonlyWallet {
292
302
  * @param filter - Optional flags controlling whether recoverable or unrolled VTXOs are included
293
303
  */
294
304
  async getVtxos(filter) {
295
- const { isDelta, fetchedExtended, address } = await this.syncVtxos();
296
305
  const f = filter ?? { withRecoverable: true, withUnrolled: false };
297
- // For delta syncs, read the full merged set from cache so old
298
- // Virtual outputs that weren't in the delta are still returned.
299
- const vtxos = isDelta
300
- ? await this.walletRepository.getVtxos(address)
301
- : fetchedExtended;
302
- return vtxos.filter((vtxo) => {
306
+ const contractManager = await this.getContractManager();
307
+ const vtxos = await contractManager.getContractsWithVtxos();
308
+ return vtxos
309
+ .flatMap((_) => _.vtxos)
310
+ .filter((vtxo) => {
303
311
  if ((0, _1.isSpendable)(vtxo)) {
304
312
  if (!f.withRecoverable &&
305
313
  ((0, _1.isRecoverable)(vtxo) || (0, _1.isExpired)(vtxo))) {
@@ -314,11 +322,9 @@ class ReadonlyWallet {
314
322
  * Return wallet transaction history derived from Arkade state and boarding transactions.
315
323
  */
316
324
  async getTransactionHistory() {
317
- // Delta-sync virtual outputs into cache, then build history from the cache.
318
- const { isDelta, fetchedExtended, address } = await this.syncVtxos();
319
- const allVtxos = isDelta
320
- ? await this.walletRepository.getVtxos(address)
321
- : fetchedExtended;
325
+ const contractManager = await this.getContractManager();
326
+ const response = await contractManager.getContractsWithVtxos();
327
+ const allVtxos = response.flatMap((_) => _.vtxos);
322
328
  const { boardingTxs, commitmentsToIgnore } = await this.getBoardingTxs();
323
329
  const getTxCreatedAt = (txid) => this.indexerProvider
324
330
  .getVtxos({ outpoints: [{ txid, vout: 0 }] })
@@ -326,166 +332,11 @@ class ReadonlyWallet {
326
332
  return (0, transactionHistory_1.buildTransactionHistory)(allVtxos, boardingTxs, commitmentsToIgnore, getTxCreatedAt);
327
333
  }
328
334
  /**
329
- * Delta-sync wallet virtual outputs: fetch only changed virtual outputs since the last
330
- * cursor, or do a full bootstrap when no cursor exists. Upserts
331
- * the result into the cache and advances the sync cursors.
332
- *
333
- * Concurrent calls are deduplicated: if a sync is already in flight,
334
- * subsequent callers receive the same promise instead of triggering
335
- * a second network round-trip.
336
- */
337
- syncVtxos() {
338
- if (this._syncVtxosInflight)
339
- return this._syncVtxosInflight;
340
- const p = this.doSyncVtxos().finally(() => {
341
- this._syncVtxosInflight = undefined;
342
- });
343
- this._syncVtxosInflight = p;
344
- return p;
345
- }
346
- async doSyncVtxos() {
347
- const address = await this.getAddress();
348
- // Batch cursor read with script map to avoid extra async hops
349
- // before the fetch (background operations may run between hops).
350
- const [scriptMap, cursors] = await Promise.all([
351
- this.getScriptMap(),
352
- (0, syncCursors_1.getAllSyncCursors)(this.walletRepository),
353
- ]);
354
- const allScripts = [...scriptMap.keys()];
355
- // Partition scripts into bootstrap (no cursor) and delta (has cursor).
356
- const bootstrapScripts = [];
357
- const deltaScripts = [];
358
- for (const s of allScripts) {
359
- if (cursors[s] === undefined) {
360
- bootstrapScripts.push(s);
361
- }
362
- else {
363
- deltaScripts.push(s);
364
- }
365
- }
366
- const requestStartedAt = Date.now();
367
- const allVtxos = [];
368
- const extendWithScript = (vtxo) => {
369
- const vtxoScript = vtxo.script
370
- ? scriptMap.get(vtxo.script)
371
- : undefined;
372
- if (!vtxoScript)
373
- return undefined;
374
- return {
375
- ...vtxo,
376
- forfeitTapLeafScript: vtxoScript.forfeit(),
377
- intentTapLeafScript: vtxoScript.forfeit(),
378
- tapTree: vtxoScript.encode(),
379
- };
380
- };
381
- // Full fetch for scripts with no cursor.
382
- if (bootstrapScripts.length > 0) {
383
- const response = await this.indexerProvider.getVtxos({
384
- scripts: bootstrapScripts,
385
- });
386
- allVtxos.push(...response.vtxos);
387
- }
388
- // Delta fetch for scripts with an existing cursor.
389
- let hasDelta = false;
390
- if (deltaScripts.length > 0) {
391
- const minCursor = Math.min(...deltaScripts.map((s) => cursors[s]));
392
- const window = (0, syncCursors_1.computeSyncWindow)(minCursor);
393
- if (window) {
394
- hasDelta = true;
395
- const response = await this.indexerProvider.getVtxos({
396
- scripts: deltaScripts,
397
- after: window.after,
398
- });
399
- allVtxos.push(...response.vtxos);
400
- }
401
- }
402
- // Extend every fetched virtual output and upsert into the cache.
403
- const fetchedExtended = [];
404
- for (const vtxo of allVtxos) {
405
- const extended = extendWithScript(vtxo);
406
- if (extended)
407
- fetchedExtended.push(extended);
408
- }
409
- // Save virtual outputs first, then advance cursors only on success.
410
- const cutoff = (0, syncCursors_1.cursorCutoff)(requestStartedAt);
411
- await this.walletRepository.saveVtxos(address, fetchedExtended);
412
- await (0, syncCursors_1.advanceSyncCursors)(this.walletRepository, Object.fromEntries(allScripts.map((s) => [s, cutoff])));
413
- // Delta-sync reconciliation: full re-fetch for delta scripts.
414
- //
415
- // The delta fetch (above) only returns virtual outputs changed after the
416
- // cursor, so it can miss preconfirmed virtual outputs that were consumed
417
- // by a round between syncs. Rather than layering targeted
418
- // queries (pendingOnly, spendableOnly) with pagination guards
419
- // and set algebra, we perform a single unfiltered re-fetch for
420
- // delta scripts. This is slightly more data over the wire but
421
- // gives us complete, authoritative state in one call and keeps
422
- // the reconciliation logic simple.
423
- //
424
- // Any cached non-spent virtual output that is absent from the full
425
- // result set is marked spent; any virtual output whose state changed
426
- // (e.g. preconfirmed → settled) is updated in place.
427
- if (hasDelta) {
428
- const { vtxos: fullVtxos, page: fullPage } = await this.indexerProvider.getVtxos({
429
- scripts: deltaScripts,
430
- });
431
- // Reconciliation is best-effort: if the response is
432
- // paginated we don't have a complete picture, so we skip
433
- // rather than act on partial data. Wallets with enough
434
- // virtual outputs to exceed a single page rely solely on the
435
- // cursor-based delta mechanism for state updates.
436
- const fullSetComplete = !fullPage || fullPage.total <= 1;
437
- if (fullSetComplete) {
438
- const fullOutpoints = new Map(fullVtxos.map((v) => [`${v.txid}:${v.vout}`, v]));
439
- const deltaScriptSet = new Set(deltaScripts);
440
- const cachedVtxos = await this.walletRepository.getVtxos(address);
441
- const reconciledExtended = [];
442
- for (const cached of cachedVtxos) {
443
- if (!cached.script ||
444
- !deltaScriptSet.has(cached.script) ||
445
- cached.isSpent) {
446
- continue;
447
- }
448
- const outpoint = `${cached.txid}:${cached.vout}`;
449
- const fresh = fullOutpoints.get(outpoint);
450
- if (!fresh) {
451
- // Server no longer knows about this virtual output —
452
- // it was spent between syncs.
453
- reconciledExtended.push({
454
- ...cached,
455
- isSpent: true,
456
- });
457
- continue;
458
- }
459
- const extended = extendWithScript(fresh);
460
- if (extended &&
461
- extended.virtualStatus.state !==
462
- cached.virtualStatus.state) {
463
- // State transitioned (e.g. preconfirmed →
464
- // settled) — update the cached entry.
465
- reconciledExtended.push(extended);
466
- }
467
- }
468
- if (reconciledExtended.length > 0) {
469
- console.warn(`[ark-sdk] delta sync: reconciled ${reconciledExtended.length} stale VTXO(s) via full re-fetch`);
470
- await this.walletRepository.saveVtxos(address, reconciledExtended);
471
- }
472
- }
473
- else {
474
- console.warn("[ark-sdk] delta sync: skipping reconciliation — full re-fetch was paginated");
475
- }
476
- }
477
- return {
478
- isDelta: hasDelta || bootstrapScripts.length === 0,
479
- fetchedExtended,
480
- address,
481
- };
482
- }
483
- /**
484
- * Clear all virtual output sync cursors, forcing a full re-bootstrap on next sync.
335
+ * Clear the global VTXO sync cursor, forcing a full re-bootstrap on next sync.
485
336
  * Useful for recovery after indexer reprocessing or debugging.
486
337
  */
487
- async clearSyncCursors() {
488
- await (0, syncCursors_1.clearSyncCursors)(this.walletRepository);
338
+ async clearSyncCursor() {
339
+ await (0, syncCursors_1.clearSyncCursor)(this.walletRepository);
489
340
  }
490
341
  /**
491
342
  * Build a transaction history view for the wallet's boarding address.
@@ -528,6 +379,7 @@ class ReadonlyWallet {
528
379
  createdAt: tx.status.confirmed
529
380
  ? new Date(tx.status.block_time * 1000)
530
381
  : new Date(0),
382
+ script: base_1.hex.encode(this.boardingTapscript.pkScript),
531
383
  });
532
384
  }
533
385
  }
@@ -617,22 +469,41 @@ class ReadonlyWallet {
617
469
  await this.indexerProvider?.unsubscribeForScripts(subscriptionId);
618
470
  };
619
471
  // Handle subscription updates asynchronously without blocking.
620
- // Note: subscription covers all wallet scripts (default + delegate),
621
- // but we can't determine which script each virtual output belongs to from the
622
- // subscription event. Virtual outputs are extended with the current offchainTapscript;
623
- // this is for notification/display only not for spending.
624
- // For correct extension metadata, use getVtxos() which queries per-script.
472
+ // Subscription covers all wallet scripts (default + delegate) plus
473
+ // any additional registered contracts. Virtual outputs carry a
474
+ // `script` field from the indexer which the contract manager
475
+ // resolves to the owning contract so the extension uses the
476
+ // correct forfeit/intent tapscripts.
625
477
  (async () => {
626
478
  try {
479
+ const cm = await this.getContractManager();
627
480
  for await (const update of subscription) {
628
- if (update.newVtxos?.length > 0 ||
629
- update.spentVtxos?.length > 0) {
481
+ if (update.newVtxos?.length === 0 &&
482
+ update.spentVtxos?.length === 0) {
483
+ continue;
484
+ }
485
+ // Isolate per-update annotation failures (e.g. a VTXO
486
+ // arriving for a contract we haven't registered yet).
487
+ // Without this a single bad update would kill the
488
+ // for-await loop and silently drop every subsequent
489
+ // subscription event for the session.
490
+ try {
491
+ // Default to `[]` so a one-sided update (e.g.
492
+ // only `newVtxos`) doesn't pass `undefined` into
493
+ // annotateVtxos and throw on `.length`.
494
+ const [newVtxos, spentVtxos] = await Promise.all([
495
+ cm.annotateVtxos(update.newVtxos ?? []),
496
+ cm.annotateVtxos(update.spentVtxos ?? []),
497
+ ]);
630
498
  eventCallback({
631
499
  type: "vtxo",
632
- newVtxos: update.newVtxos.map((vtxo) => (0, utils_1.extendVirtualCoin)(this, vtxo)),
633
- spentVtxos: update.spentVtxos.map((vtxo) => (0, utils_1.extendVirtualCoin)(this, vtxo)),
500
+ newVtxos,
501
+ spentVtxos,
634
502
  });
635
503
  }
504
+ catch (error) {
505
+ console.warn("Dropping subscription update after annotation failed; next sync will reconcile:", error);
506
+ }
636
507
  }
637
508
  }
638
509
  catch (error) {
@@ -778,7 +649,6 @@ class ReadonlyWallet {
778
649
  indexerProvider: this.indexerProvider,
779
650
  contractRepository: this.contractRepository,
780
651
  walletRepository: this.walletRepository,
781
- getDefaultAddress: () => this.getAddress(),
782
652
  watcherConfig: this.watcherConfig,
783
653
  });
784
654
  // Register the wallet's current address as a contract
@@ -904,11 +774,10 @@ class Wallet extends ReadonlyWallet {
904
774
  }
905
775
  });
906
776
  }
907
- constructor(identity, network, networkName, onchainProvider, arkProvider, indexerProvider, arkServerPublicKey, offchainTapscript, boardingTapscript, serverUnrollScript, forfeitOutputScript, forfeitPubkey, dustAmount, walletRepository, contractRepository,
777
+ constructor(identity, network, onchainProvider, arkProvider, indexerProvider, arkServerPublicKey, offchainTapscript, boardingTapscript, serverUnrollScript, forfeitOutputScript, forfeitPubkey, dustAmount, walletRepository, contractRepository,
908
778
  /** @deprecated Use settlementConfig */
909
779
  renewalConfig, delegatorProvider, watcherConfig, settlementConfig) {
910
780
  super(identity, network, onchainProvider, indexerProvider, arkServerPublicKey, offchainTapscript, boardingTapscript, dustAmount, walletRepository, contractRepository, delegatorProvider, watcherConfig);
911
- this.networkName = networkName;
912
781
  this.arkProvider = arkProvider;
913
782
  this.serverUnrollScript = serverUnrollScript;
914
783
  this.forfeitOutputScript = forfeitOutputScript;
@@ -1028,7 +897,7 @@ class Wallet extends ReadonlyWallet {
1028
897
  const forfeitPubkey = base_1.hex.decode(setup.info.forfeitPubkey).slice(1);
1029
898
  const forfeitAddress = (0, btc_signer_1.Address)(setup.network).decode(setup.info.forfeitAddress);
1030
899
  const forfeitOutputScript = btc_signer_1.OutScript.encode(forfeitAddress);
1031
- const wallet = new Wallet(config.identity, setup.network, setup.networkName, setup.onchainProvider, setup.arkProvider, setup.indexerProvider, setup.serverPubKey, setup.offchainTapscript, setup.boardingTapscript, serverUnrollScript, forfeitOutputScript, forfeitPubkey, setup.dustAmount, setup.walletRepository, setup.contractRepository, config.renewalConfig, config.delegatorProvider, config.watcherConfig, config.settlementConfig);
900
+ const wallet = new Wallet(config.identity, setup.network, setup.onchainProvider, setup.arkProvider, setup.indexerProvider, setup.serverPubKey, setup.offchainTapscript, setup.boardingTapscript, serverUnrollScript, forfeitOutputScript, forfeitPubkey, setup.dustAmount, setup.walletRepository, setup.contractRepository, config.renewalConfig, config.delegatorProvider, config.watcherConfig, config.settlementConfig);
1032
901
  await wallet.getVtxoManager();
1033
902
  return wallet;
1034
903
  }
@@ -1291,7 +1160,7 @@ class Wallet extends ReadonlyWallet {
1291
1160
  const abortController = new AbortController();
1292
1161
  try {
1293
1162
  const stream = this.arkProvider.getEventStream(abortController.signal, topics);
1294
- const intentId = await this.safeRegisterIntent(intent);
1163
+ const intentId = await this.safeRegisterIntent(intent, params.inputs);
1295
1164
  const handler = this.createBatchHandler(intentId, params.inputs, recipients, session);
1296
1165
  const commitmentTxid = await batch_1.Batch.join(stream, handler, {
1297
1166
  abortController,
@@ -1304,8 +1173,16 @@ class Wallet extends ReadonlyWallet {
1304
1173
  return commitmentTxid;
1305
1174
  }
1306
1175
  catch (error) {
1307
- // delete the intent to not be stuck in the queue
1308
- await this.arkProvider.deleteIntent(deleteIntent).catch(() => { });
1176
+ // delete the intent to not be stuck in the queue. If deletion fails
1177
+ // the intent stays on the server and the next settle will hit
1178
+ // "duplicated input" in safeRegisterIntent — surface the failure
1179
+ // rather than silently swallowing it.
1180
+ const inputIds = params.inputs
1181
+ .map((i) => `${i.txid}:${i.vout}`)
1182
+ .join(",");
1183
+ await this.arkProvider.deleteIntent(deleteIntent).catch((e) => {
1184
+ console.warn(`Failed to delete intent after settle failure for inputs [${inputIds}]; intent may linger on server and cause 'duplicated input' on next settle`, e);
1185
+ });
1309
1186
  throw error;
1310
1187
  }
1311
1188
  finally {
@@ -1495,7 +1372,7 @@ class Wallet extends ReadonlyWallet {
1495
1372
  },
1496
1373
  };
1497
1374
  }
1498
- async safeRegisterIntent(intent) {
1375
+ async safeRegisterIntent(intent, inputs) {
1499
1376
  try {
1500
1377
  return await this.arkProvider.registerIntent(intent);
1501
1378
  }
@@ -1504,11 +1381,13 @@ class Wallet extends ReadonlyWallet {
1504
1381
  if (error instanceof errors_1.ArkError &&
1505
1382
  error.code === 0 &&
1506
1383
  error.message.includes("duplicated input")) {
1507
- // delete all intents spending one of the wallet coins
1508
- const allSpendableCoins = await this.getVtxos({
1509
- withRecoverable: true,
1510
- });
1511
- const deleteIntent = await this.makeDeleteIntentSignature(allSpendableCoins);
1384
+ // Clear any queued intent spending these exact inputs. The
1385
+ // previous implementation signed a proof over getVtxos() only,
1386
+ // which misses boarding UTXOs — the most common trigger for
1387
+ // "duplicated input" on the auto-settle path. Signing the
1388
+ // caller's own inputs keeps the proof surgical and correct
1389
+ // regardless of whether the stuck input is a VTXO or boarding.
1390
+ const deleteIntent = await this.makeDeleteIntentSignature(inputs);
1512
1391
  await this.arkProvider.deleteIntent(deleteIntent);
1513
1392
  // try again
1514
1393
  return this.arkProvider.registerIntent(intent);
@@ -1576,9 +1455,7 @@ class Wallet extends ReadonlyWallet {
1576
1455
  scripts: allScripts,
1577
1456
  });
1578
1457
  for (const vtxo of fetchedVtxos) {
1579
- const vtxoScript = vtxo.script
1580
- ? scriptMap.get(vtxo.script)
1581
- : undefined;
1458
+ const vtxoScript = scriptMap.get(vtxo.script);
1582
1459
  if (!vtxoScript)
1583
1460
  continue;
1584
1461
  if (vtxo.virtualStatus.state === "swept" ||
@@ -1881,8 +1758,9 @@ class Wallet extends ReadonlyWallet {
1881
1758
  console.warn(`updateDbAfterOffchainTx: inputs length (${inputs.length}) differs from signedCheckpointTxs length (${signedCheckpointTxs.length})`);
1882
1759
  }
1883
1760
  const safeLength = Math.min(inputs.length, signedCheckpointTxs.length);
1884
- for (const [inputIndex, input] of inputs.entries()) {
1885
- const vtxo = (0, utils_1.extendVirtualCoin)(this, input);
1761
+ const cm = await this.getContractManager();
1762
+ const annotatedInputs = await cm.annotateVtxos(inputs);
1763
+ for (const [inputIndex, vtxo] of annotatedInputs.entries()) {
1886
1764
  if (inputIndex < safeLength &&
1887
1765
  signedCheckpointTxs[inputIndex]) {
1888
1766
  const checkpoint = btc_signer_1.Transaction.fromPSBT(base_1.base64.decode(signedCheckpointTxs[inputIndex]));
@@ -1942,6 +1820,7 @@ class Wallet extends ReadonlyWallet {
1942
1820
  confirmed: false,
1943
1821
  },
1944
1822
  assets: changeAssets,
1823
+ script: base_1.hex.encode(this.offchainTapscript.pkScript),
1945
1824
  };
1946
1825
  }
1947
1826
  await this.walletRepository.saveVtxos(addr, changeVtxo ? [...spentVtxos, changeVtxo] : spentVtxos);
@@ -1972,10 +1851,14 @@ class Wallet extends ReadonlyWallet {
1972
1851
  const inputArkTxIds = new Set();
1973
1852
  const boardingUtxoToRemove = new Set();
1974
1853
  const isVtxo = (input) => "virtualStatus" in input;
1854
+ const vtxoInputs = inputs.filter(isVtxo);
1855
+ const cm = await this.getContractManager();
1856
+ const annotatedVtxos = await cm.annotateVtxos(vtxoInputs);
1857
+ const annotatedByKey = new Map(annotatedVtxos.map((v) => [`${v.txid}:${v.vout}`, v]));
1975
1858
  for (const input of inputs) {
1976
1859
  if (isVtxo(input)) {
1977
1860
  // virtual output = mark it settled
1978
- const vtxo = (0, utils_1.extendVirtualCoin)(this, input);
1861
+ const vtxo = annotatedByKey.get(`${input.txid}:${input.vout}`);
1979
1862
  if (vtxo.arkTxId) {
1980
1863
  inputArkTxIds.add(vtxo.arkTxId);
1981
1864
  }
@@ -2062,7 +1945,7 @@ function selectVirtualCoins(coins, targetAmount) {
2062
1945
  */
2063
1946
  async function waitForIncomingFunds(wallet) {
2064
1947
  let stopFunc;
2065
- const promise = new Promise((resolve) => {
1948
+ return new Promise((resolve) => {
2066
1949
  wallet
2067
1950
  .notifyIncomingFunds((coins) => {
2068
1951
  resolve(coins);
@@ -2073,5 +1956,4 @@ async function waitForIncomingFunds(wallet) {
2073
1956
  stopFunc = stop;
2074
1957
  });
2075
1958
  });
2076
- return promise;
2077
1959
  }
@@ -8,27 +8,24 @@ exports.CONTRACT_POLL_TASK_TYPE = "contract-poll";
8
8
  *
9
9
  * Replicates the polling subset of @see ContractManager.initialize:
10
10
  * 1. Load all contracts from the contract repository.
11
- * 2. Mark expired active contracts as inactive.
12
- * 3. Paginated fetch of spendable VTXOs from the indexer.
13
- * 4. Extend each VTXO with tapscript data.
14
- * 5. Save to the wallet repository.
11
+ * 2. Paginated fetch of every VTXO (including spent) from the indexer.
12
+ * 3. Extend each VTXO with tapscript data.
13
+ * 4. Save to the wallet repository.
14
+ *
15
+ * NOTE: the indexer query deliberately omits `spendableOnly`. Every
16
+ * repository implements `saveVtxos` as an upsert with no batch delete,
17
+ * so filtering to spendable-only would leave VTXOs that became spent
18
+ * between polls marked as spendable forever. Fetching the full set lets
19
+ * the upsert overwrite stale records with their latest state.
15
20
  */
16
21
  exports.contractPollProcessor = {
17
22
  taskType: exports.CONTRACT_POLL_TASK_TYPE,
18
23
  async execute(item, deps) {
19
24
  const { contractRepository, walletRepository, indexerProvider, extendVtxo, } = deps;
20
25
  const contracts = await contractRepository.getContracts();
21
- const now = Date.now();
22
26
  let contractsProcessed = 0;
23
27
  let vtxosSaved = 0;
24
28
  for (const contract of contracts) {
25
- // Mark expired active contracts as inactive
26
- if (contract.state === "active" &&
27
- contract.expiresAt &&
28
- contract.expiresAt <= now) {
29
- contract.state = "inactive";
30
- await contractRepository.saveContract(contract);
31
- }
32
29
  // Paginated fetch of spendable virtual outputs
33
30
  const pageSize = 100;
34
31
  let pageIndex = 0;
@@ -37,7 +34,6 @@ exports.contractPollProcessor = {
37
34
  while (hasMore) {
38
35
  const { vtxos, page } = await indexerProvider.getVtxos({
39
36
  scripts: [contract.script],
40
- spendableOnly: true,
41
37
  pageIndex,
42
38
  pageSize,
43
39
  });
@@ -66,17 +66,8 @@ async function runTasks(queue, processors, deps) {
66
66
  * can build deps without depending on Expo.
67
67
  */
68
68
  function createTaskDependencies(options) {
69
- const { walletRepository, contractRepository, indexerProvider, arkProvider, offchainTapscript, } = options;
70
69
  return {
71
- walletRepository,
72
- contractRepository,
73
- indexerProvider,
74
- arkProvider,
75
- extendVtxo: (vtxo, contract) => {
76
- if (contract) {
77
- return (0, utils_1.extendVtxoFromContract)(vtxo, contract);
78
- }
79
- return (0, utils_1.extendVirtualCoin)({ offchainTapscript }, vtxo);
80
- },
70
+ ...options,
71
+ extendVtxo: (vtxo, contract) => (0, utils_1.extendVirtualCoinForContract)(vtxo, contract),
81
72
  };
82
73
  }
@@ -103,7 +103,6 @@ export function contractFromArkContract(encoded, options = {}) {
103
103
  params,
104
104
  state: options.state || "active",
105
105
  createdAt: Date.now(),
106
- expiresAt: options.expiresAt,
107
106
  metadata: options.metadata,
108
107
  };
109
108
  }
@@ -129,7 +128,6 @@ export function contractFromArkContractWithAddress(encoded, serverPubKey, addres
129
128
  address: vtxoScript.address(addressPrefix, serverPubKey).encode(),
130
129
  state: options.state || "active",
131
130
  createdAt: Date.now(),
132
- expiresAt: options.expiresAt,
133
131
  metadata: options.metadata,
134
132
  };
135
133
  }