@arkade-os/sdk 0.4.47 → 0.4.48

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 (85) hide show
  1. package/dist/adapters/expo.cjs +5 -5
  2. package/dist/adapters/expo.d.cts +2 -2
  3. package/dist/adapters/expo.d.ts +2 -2
  4. package/dist/adapters/expo.js +3 -3
  5. package/dist/adapters/indexedDB.cjs +5 -5
  6. package/dist/adapters/indexedDB.js +4 -4
  7. package/dist/{ark-CoJgwTi_.d.cts → ark-CjE0KkGp.d.cts} +405 -250
  8. package/dist/{ark-CoJgwTi_.d.ts → ark-CjE0KkGp.d.ts} +405 -250
  9. package/dist/{asyncStorageTaskQueue-Dowyo59a.d.cts → asyncStorageTaskQueue-CeLZhZFU.d.cts} +2 -2
  10. package/dist/{asyncStorageTaskQueue-Z3aFoXCl.d.ts → asyncStorageTaskQueue-D9SOPMPC.d.ts} +2 -2
  11. package/dist/{chunk-XN3554FT.js → chunk-4VVTRPH3.js} +4 -4
  12. package/dist/chunk-4VVTRPH3.js.map +1 -0
  13. package/dist/{chunk-RQSA7ADZ.cjs → chunk-5XMVTK5S.cjs} +37 -37
  14. package/dist/chunk-5XMVTK5S.cjs.map +1 -0
  15. package/dist/{chunk-L5ND2FJJ.cjs → chunk-7TXSPRN5.cjs} +161 -2
  16. package/dist/chunk-7TXSPRN5.cjs.map +1 -0
  17. package/dist/{chunk-CDDWXBEM.js → chunk-BHHLRZCS.js} +169 -42
  18. package/dist/chunk-BHHLRZCS.js.map +1 -0
  19. package/dist/{chunk-HHV5FPC6.cjs → chunk-EB3ALGTM.cjs} +178 -50
  20. package/dist/chunk-EB3ALGTM.cjs.map +1 -0
  21. package/dist/{chunk-JAYOXOLT.cjs → chunk-ENDNQPZA.cjs} +17 -44
  22. package/dist/chunk-ENDNQPZA.cjs.map +1 -0
  23. package/dist/{chunk-WTTLIFS7.cjs → chunk-ENZ4YPR2.cjs} +944 -544
  24. package/dist/chunk-ENZ4YPR2.cjs.map +1 -0
  25. package/dist/{chunk-3N5I6GT6.js → chunk-G6J6AEMH.js} +5 -4
  26. package/dist/chunk-G6J6AEMH.js.map +1 -0
  27. package/dist/{chunk-OCS5S3L3.js → chunk-NYR5H6GR.js} +685 -284
  28. package/dist/chunk-NYR5H6GR.js.map +1 -0
  29. package/dist/{chunk-VE67GMGM.cjs → chunk-SYDANBJI.cjs} +9 -8
  30. package/dist/chunk-SYDANBJI.cjs.map +1 -0
  31. package/dist/{chunk-AJQ4AOT3.js → chunk-XFMGIFBL.js} +4 -31
  32. package/dist/chunk-XFMGIFBL.js.map +1 -0
  33. package/dist/{chunk-C7TXXTMS.js → chunk-ZDDBTFVO.js} +146 -3
  34. package/dist/chunk-ZDDBTFVO.js.map +1 -0
  35. package/dist/contracts/handlers/index.cjs +8 -8
  36. package/dist/contracts/handlers/index.d.cts +3 -3
  37. package/dist/contracts/handlers/index.d.ts +3 -3
  38. package/dist/contracts/handlers/index.js +2 -2
  39. package/dist/{delegate-Dl87_zMA.d.cts → delegate-D3dY-2vS.d.cts} +1 -1
  40. package/dist/{delegate-CuaDqqQi.d.ts → delegate-DhGZk1r2.d.ts} +1 -1
  41. package/dist/{index-BC_G0VTh.d.cts → index-D6RQROds.d.cts} +2 -2
  42. package/dist/{index-CKwpZEpw.d.ts → index-DK4_iWYy.d.ts} +2 -2
  43. package/dist/index.cjs +211 -183
  44. package/dist/index.d.cts +61 -11
  45. package/dist/index.d.ts +61 -11
  46. package/dist/index.js +4 -4
  47. package/dist/repositories/realm/index.cjs +19 -19
  48. package/dist/repositories/realm/index.d.cts +2 -2
  49. package/dist/repositories/realm/index.d.ts +2 -2
  50. package/dist/repositories/realm/index.js +5 -5
  51. package/dist/repositories/sqlite/index.cjs +18 -18
  52. package/dist/repositories/sqlite/index.d.cts +2 -2
  53. package/dist/repositories/sqlite/index.d.ts +2 -2
  54. package/dist/repositories/sqlite/index.js +5 -5
  55. package/dist/{taskRunner-Dz-TI4Rb.d.ts → taskRunner-B-2MHCYG.d.ts} +2 -2
  56. package/dist/{taskRunner-Dz47k0lp.d.cts → taskRunner-Brnak0Rc.d.cts} +2 -2
  57. package/dist/wallet/expo/background.cjs +14 -14
  58. package/dist/wallet/expo/background.d.cts +3 -3
  59. package/dist/wallet/expo/background.d.ts +3 -3
  60. package/dist/wallet/expo/background.js +6 -6
  61. package/dist/wallet/expo/index.cjs +14 -14
  62. package/dist/wallet/expo/index.cjs.map +1 -1
  63. package/dist/wallet/expo/index.d.cts +5 -5
  64. package/dist/wallet/expo/index.d.ts +5 -5
  65. package/dist/wallet/expo/index.js +5 -5
  66. package/dist/wallet/expo/index.js.map +1 -1
  67. package/dist/{wallet-Oxz_wLxo.d.cts → wallet-B5bwixQa.d.ts} +130 -7
  68. package/dist/{wallet-x7egmXq6.d.ts → wallet-DiOrtYic.d.cts} +130 -7
  69. package/dist/worker/expo/index.cjs +9 -9
  70. package/dist/worker/expo/index.d.cts +4 -4
  71. package/dist/worker/expo/index.d.ts +4 -4
  72. package/dist/worker/expo/index.js +5 -5
  73. package/package.json +3 -2
  74. package/dist/chunk-3N5I6GT6.js.map +0 -1
  75. package/dist/chunk-AJQ4AOT3.js.map +0 -1
  76. package/dist/chunk-C7TXXTMS.js.map +0 -1
  77. package/dist/chunk-CDDWXBEM.js.map +0 -1
  78. package/dist/chunk-HHV5FPC6.cjs.map +0 -1
  79. package/dist/chunk-JAYOXOLT.cjs.map +0 -1
  80. package/dist/chunk-L5ND2FJJ.cjs.map +0 -1
  81. package/dist/chunk-OCS5S3L3.js.map +0 -1
  82. package/dist/chunk-RQSA7ADZ.cjs.map +0 -1
  83. package/dist/chunk-VE67GMGM.cjs.map +0 -1
  84. package/dist/chunk-WTTLIFS7.cjs.map +0 -1
  85. package/dist/chunk-XN3554FT.js.map +0 -1
@@ -1,10 +1,10 @@
1
- import { Transaction, setArkPsbtField, PrevArkTxField, ConditionWitness, AssetId, AssetGroup, AssetInput, AssetOutput, Metadata, Packet, craftToSpendTx, getArkPsbtFields, CosignerPublicKey, BufferReader, BufferWriter, OP_RETURN_EMPTY_PKSCRIPT, VtxoTaprootTree, baseFetch, Intent, maybeArkError, isArkError, ArkErrorName, ProviderUnavailableError, FetchError, isFetchTimeoutError, AssetRef, isEventSourceError, RestArkProvider, RestIndexerProvider, ArkError } from './chunk-CDDWXBEM.js';
2
- import { witnessRefToBytes, validateProgram, toASM, stringifyArtifact, serializeArkadeContractParams, resolveAsm, parseArtifact, getOpcodeValue, getOpcodeName, fromASM, deserializeArkadeContractParams, computeArkadeScriptPublicKey, bytesToASM, asmToBytes, arkadeWitnessHash, arkadeScriptHash, SUPPORTED_PROGRAM_VERSION, OPCODE_VALUES, OPCODE_NAMES, bignum_exports, ArkadeScript, ArkadeProgramScript, ARKADE_OPS, ARKADE_OPCODE_VALUES, ARKADE_OPCODE_NAMES, ARKADE_OPCODES, ARKADE_OP, inputName, contractHandlers, isMainnetDescriptor, descriptorIsOurs, DEFAULT_PAGE_SIZE, DelegateVtxo, WALLET_RECEIVE_SOURCE, deriveDescriptorLeafPubKey, DefaultVtxo, BoardingContractHandler } from './chunk-XN3554FT.js';
3
- import { VtxoScript, CSVMultisigTapscript, DEFAULT_NETWORK, ArkAddress, sequenceToTimelock, decodeTapscript, scriptFromTapLeafScript, CLTVMultisigTapscript, timelockToSequence, DEFAULT_NETWORK_NAME, getSequence, getNetwork, MultisigTapscript, networks as networks$1, DEFAULT_ARKADE_SERVER_URL } from './chunk-C7TXXTMS.js';
1
+ import { Transaction, setArkPsbtField, PrevArkTxField, ConditionWitness, AssetId, AssetGroup, AssetInput, AssetOutput, Metadata, Packet, craftToSpendTx, getArkPsbtFields, CosignerPublicKey, BufferReader, BufferWriter, OP_RETURN_EMPTY_PKSCRIPT, VtxoTaprootTree, baseFetch, Intent, rateGate, maybeArkError, isArkError, ArkErrorName, ProviderUnavailableError, FetchError, isFetchTimeoutError, AssetRef, isEventSourceError, RestArkProvider, RestIndexerProvider, ArkError } from './chunk-BHHLRZCS.js';
2
+ import { witnessRefToBytes, validateProgram, toASM, stringifyArtifact, serializeArkadeContractParams, resolveAsm, parseArtifact, getOpcodeValue, getOpcodeName, fromASM, deserializeArkadeContractParams, computeArkadeScriptPublicKey, bytesToASM, asmToBytes, arkadeWitnessHash, arkadeScriptHash, SUPPORTED_PROGRAM_VERSION, OPCODE_VALUES, OPCODE_NAMES, bignum_exports, ArkadeScript, ArkadeProgramScript, ARKADE_OPS, ARKADE_OPCODE_VALUES, ARKADE_OPCODE_NAMES, ARKADE_OPCODES, ARKADE_OP, inputName, contractHandlers, isMainnetDescriptor, descriptorIsOurs, DefaultVtxo, DEFAULT_PAGE_SIZE, DelegateVtxo, WALLET_RECEIVE_SOURCE, deriveDescriptorLeafPubKey, BoardingContractHandler } from './chunk-4VVTRPH3.js';
3
+ import { VtxoScript, isRecoverable, CSVMultisigTapscript, DEFAULT_NETWORK, hasTerminalSpend, getNormalizedVtxos, isVirtualCoin, ArkAddress, sequenceToTimelock, decodeTapscript, scriptFromTapLeafScript, CLTVMultisigTapscript, timelockToSequence, DEFAULT_NETWORK_NAME, normalizeVtxo, getSequence, toBatchExpiry, toOffchainInputFeeParams, canRecoverOnchain, getNetwork, MultisigTapscript, canSpendOffchain, toVirtualStatus, resolveTimeHeight, parseLegacyExpiry, networks as networks$1, isExpired, DEFAULT_ARKADE_SERVER_URL, isPastExpiry } from './chunk-ZDDBTFVO.js';
4
4
  import { __export } from './chunk-NSBPE2FW.js';
5
5
  import { sha256, equalBytes, hash160, sha256x2, concatBytes, randomPrivateKeyBytes, pubECDSA, pubSchnorr } from '@scure/btc-signer/utils.js';
6
6
  import { SigHash, Script, OP, OutScript, Address, RawWitness, p2tr, p2wpkh, TaprootControlBlock, DEFAULT_SEQUENCE, Transaction as Transaction$2 } from '@scure/btc-signer';
7
- import { hex, base58, base64 } from '@scure/base';
7
+ import { hex, base58, base64, bech32m } from '@scure/base';
8
8
  import * as musig2 from '@scure/btc-signer/musig2.js';
9
9
  import { equalBytes as equalBytes$1, bytesToNumberBE } from '@noble/curves/utils.js';
10
10
  import { signAsync, schnorr as schnorr$1, Point } from '@noble/secp256k1';
@@ -991,7 +991,7 @@ var RestDelegateProvider = class {
991
991
  */
992
992
  async getDelegateInfo() {
993
993
  const url = `${this.url}/v1/delegator/info`;
994
- const response = await baseFetch(url);
994
+ const response = await rateGate.runHttp(url, () => baseFetch(url));
995
995
  if (!response.ok) {
996
996
  const errorText2 = await response.text();
997
997
  throw new Error(`Failed to get delegate info: ${errorText2}`);
@@ -2042,20 +2042,6 @@ var TxType = /* @__PURE__ */ ((TxType2) => {
2042
2042
  TxType2["TxReceived"] = "RECEIVED";
2043
2043
  return TxType2;
2044
2044
  })(TxType || {});
2045
- function isSpendable(vtxo) {
2046
- return !vtxo.isSpent;
2047
- }
2048
- function isRecoverable(vtxo) {
2049
- return vtxo.virtualStatus.state === "swept" && isSpendable(vtxo);
2050
- }
2051
- function isExpired(vtxo) {
2052
- if (vtxo.virtualStatus.state === "swept") return true;
2053
- const expiry = vtxo.virtualStatus.batchExpiry;
2054
- if (!expiry) return false;
2055
- const expireAt = new Date(expiry);
2056
- if (expireAt.getFullYear() < 2025) return false;
2057
- return expiry <= Date.now();
2058
- }
2059
2045
  function isSubdust(vtxo, dust) {
2060
2046
  if (typeof vtxo === "bigint") return vtxo < dust;
2061
2047
  return vtxo.value < dust;
@@ -2437,6 +2423,15 @@ async function submitOffchainTx(provider, offchainTx, signer, hooks) {
2437
2423
  await hooks?.afterFinalize?.();
2438
2424
  return { arkTxid, signedCheckpointTxs };
2439
2425
  }
2426
+ async function signAndSubmitOffchainTx(params) {
2427
+ const offchainTx = buildOffchainTx(params.inputs, params.outputs, params.serverUnrollScript);
2428
+ const signer = {
2429
+ signArkTx: async (arkTx) => ({ arkTx: await params.identity.sign(arkTx) }),
2430
+ signCheckpoint: (checkpoint) => params.identity.sign(checkpoint)
2431
+ };
2432
+ const { arkTxid } = await submitOffchainTx(params.provider, offchainTx, signer);
2433
+ return arkTxid;
2434
+ }
2440
2435
  var AmountVariableName = "amount";
2441
2436
  var ExpiryVariableName = "expiry";
2442
2437
  var BirthVariableName = "birth";
@@ -2716,7 +2711,7 @@ function selectPendingRecoveryOutpoints(contractsWithVtxos, signerSet, nowSecond
2716
2711
  continue;
2717
2712
  }
2718
2713
  for (const v of vtxos) {
2719
- if (isSpendable(v) && !isRecoverable(v)) out.add(`${v.txid}:${v.vout}`);
2714
+ if (!hasTerminalSpend(v) && !v.isSwept) out.add(`${v.txid}:${v.vout}`);
2720
2715
  }
2721
2716
  }
2722
2717
  return out;
@@ -2744,19 +2739,32 @@ async function runWithCrossInstanceLock(name, fn) {
2744
2739
  });
2745
2740
  }
2746
2741
  var MAX_VTXOS_PER_SETTLEMENT = 50;
2742
+ var MAX_INPUTS_PER_INTENT = 20;
2747
2743
  function byValueDescending(vtxos) {
2748
2744
  return [...vtxos].sort((a, b) => b.value - a.value);
2749
2745
  }
2750
- function byExpiryAscending(vtxos) {
2746
+ async function fetchTimeHeight(wallet) {
2747
+ return resolveTimeHeight(wallet.onchainProvider);
2748
+ }
2749
+ function canMigrateBySend(vtxo, now) {
2750
+ if (!canSpendOffchain(vtxo, now)) return false;
2751
+ return vtxo.expiresAt !== void 0 || vtxo.expiresAtHeight !== void 0;
2752
+ }
2753
+ function byExpiryAscending(vtxos, now) {
2751
2754
  const expiryKey = (vtxo) => {
2752
- if (isRecoverable(vtxo)) return -Infinity;
2753
- const batchExpiry = vtxo.virtualStatus.batchExpiry;
2754
- if (isExpired(vtxo)) return batchExpiry ?? -Infinity;
2755
- if (!batchExpiry) return Infinity;
2756
- if (new Date(batchExpiry).getFullYear() < 2025) return Infinity;
2757
- return batchExpiry;
2755
+ if (vtxo.isSwept) return -Infinity;
2756
+ if (vtxo.expiresAt !== void 0) return vtxo.expiresAt.getTime();
2757
+ if (vtxo.expiresAtHeight !== void 0) {
2758
+ return isPastExpiry(vtxo, now) ? -Infinity : Infinity;
2759
+ }
2760
+ return Infinity;
2758
2761
  };
2759
- return [...vtxos].sort((a, b) => expiryKey(a) - expiryKey(b));
2762
+ return [...vtxos].sort((a, b) => {
2763
+ const ka = expiryKey(a);
2764
+ const kb = expiryKey(b);
2765
+ if (ka === kb) return 0;
2766
+ return ka < kb ? -1 : 1;
2767
+ });
2760
2768
  }
2761
2769
  function capSettlementBatch(sorted, maxAmount) {
2762
2770
  const batch = [];
@@ -2782,22 +2790,19 @@ var DEFAULT_SETTLEMENT_CONFIG = {
2782
2790
  pollIntervalMs: 6e4,
2783
2791
  deprecatedSignerMigration: true
2784
2792
  };
2785
- function getRecoverableVtxos(vtxos, dustAmount) {
2793
+ function getRecoverableVtxos(vtxos, dustAmount, now) {
2786
2794
  return vtxos.filter((vtxo) => {
2787
- if (isRecoverable(vtxo)) {
2795
+ if (canRecoverOnchain(vtxo, now)) {
2788
2796
  return true;
2789
2797
  }
2790
- if (isSpendable(vtxo) && isExpired(vtxo)) {
2791
- return true;
2792
- }
2793
- if (vtxo.virtualStatus.state === "preconfirmed" && isSubdust(vtxo, dustAmount)) {
2798
+ if (canSpendOffchain(vtxo, now) && vtxo.isPreconfirmed && isSubdust(vtxo, dustAmount)) {
2794
2799
  return true;
2795
2800
  }
2796
2801
  return false;
2797
2802
  });
2798
2803
  }
2799
- function getRecoverableWithSubdust(vtxos, dustAmount) {
2800
- const recoverableVtxos = getRecoverableVtxos(vtxos, dustAmount);
2804
+ function getRecoverableWithSubdust(vtxos, dustAmount, now) {
2805
+ const recoverableVtxos = getRecoverableVtxos(vtxos, dustAmount, now);
2801
2806
  const subdust = [];
2802
2807
  const regular = [];
2803
2808
  for (const vtxo of recoverableVtxos) {
@@ -2821,17 +2826,16 @@ function getRecoverableWithSubdust(vtxos, dustAmount) {
2821
2826
  }
2822
2827
  function isVtxoExpiringSoon(vtxo, thresholdMs) {
2823
2828
  const realThresholdMs = thresholdMs <= 100 ? DEFAULT_THRESHOLD_MS : thresholdMs;
2824
- const { batchExpiry } = vtxo.virtualStatus;
2825
- if (!batchExpiry) return false;
2826
- const expireAt = new Date(batchExpiry);
2827
- if (expireAt.getFullYear() < 2025) return false;
2829
+ const expiresAt = normalizeVtxo(vtxo).expiresAt;
2830
+ if (expiresAt === void 0) return false;
2828
2831
  const now = Date.now();
2832
+ const batchExpiry = expiresAt.getTime();
2829
2833
  if (batchExpiry <= now) return false;
2830
2834
  return batchExpiry - now <= realThresholdMs;
2831
2835
  }
2832
- function getExpiringAndRecoverableVtxos(vtxos, thresholdMs, dustAmount) {
2836
+ function getExpiringAndRecoverableVtxos(vtxos, thresholdMs, dustAmount, now) {
2833
2837
  return vtxos.filter(
2834
- (vtxo) => isVtxoExpiringSoon(vtxo, thresholdMs) || isRecoverable(vtxo) || isSpendable(vtxo) && isExpired(vtxo) || isSubdust(vtxo, dustAmount)
2838
+ (vtxo) => isVtxoExpiringSoon(vtxo, thresholdMs) || canRecoverOnchain(vtxo, now) || isSubdust(vtxo, dustAmount)
2835
2839
  );
2836
2840
  }
2837
2841
  function isMigrationCapable(wallet) {
@@ -2980,7 +2984,8 @@ var VtxoManager = class _VtxoManager {
2980
2984
  withUnrolled: false
2981
2985
  });
2982
2986
  const dustAmount = getDustAmount(this.wallet);
2983
- let { vtxosToRecover, totalAmount } = getRecoverableWithSubdust(allVtxos, dustAmount);
2987
+ const now = await fetchTimeHeight(this.wallet);
2988
+ let { vtxosToRecover, totalAmount } = getRecoverableWithSubdust(allVtxos, dustAmount, now);
2984
2989
  if (vtxosToRecover.length === 0) {
2985
2990
  throw new Error("No recoverable VTXOs found");
2986
2991
  }
@@ -2989,7 +2994,7 @@ var VtxoManager = class _VtxoManager {
2989
2994
  const capped = capSettlementBatch(byValueDescending(vtxosToRecover), vtxoMaxAmount);
2990
2995
  if (capped.length < vtxosToRecover.length) {
2991
2996
  const recoverableCount = vtxosToRecover.length;
2992
- ({ vtxosToRecover, totalAmount } = getRecoverableWithSubdust(capped, dustAmount));
2997
+ ({ vtxosToRecover, totalAmount } = getRecoverableWithSubdust(capped, dustAmount, now));
2993
2998
  if (vtxosToRecover.length === 0) {
2994
2999
  throw new Error(
2995
3000
  `Capped recovery batch (highest-value subset of ${recoverableCount} recoverable VTXOs within the ${MAX_VTXOS_PER_SETTLEMENT}-input and ${vtxoMaxAmount}-sat limits) is below the dust threshold ${dustAmount}`
@@ -3041,7 +3046,8 @@ var VtxoManager = class _VtxoManager {
3041
3046
  const dustAmount = getDustAmount(this.wallet);
3042
3047
  const { vtxosToRecover, includesSubdust, totalAmount } = getRecoverableWithSubdust(
3043
3048
  allVtxos,
3044
- dustAmount
3049
+ dustAmount,
3050
+ await fetchTimeHeight(this.wallet)
3045
3051
  );
3046
3052
  const subdustAmount = vtxosToRecover.filter((v) => BigInt(v.value) < dustAmount).reduce((sum, v) => sum + BigInt(v.value), 0n);
3047
3053
  return {
@@ -3075,6 +3081,16 @@ var VtxoManager = class _VtxoManager {
3075
3081
  * ```
3076
3082
  */
3077
3083
  async getExpiringVtxos(thresholdMs) {
3084
+ return this.selectExpiringVtxos(thresholdMs);
3085
+ }
3086
+ /**
3087
+ * {@link getExpiringVtxos}, against a caller-supplied chain tip.
3088
+ *
3089
+ * The settle paths select, re-select after pre-flight, and sort by expiry within one pass;
3090
+ * each of those judges expiry, so they share one tip rather than fetching (and possibly
3091
+ * disagreeing on) one apiece. Fetches its own when `now` is omitted.
3092
+ */
3093
+ async selectExpiringVtxos(thresholdMs, now) {
3078
3094
  if (this.settlementConfig === false && thresholdMs === void 0) {
3079
3095
  return [];
3080
3096
  }
@@ -3087,7 +3103,12 @@ var VtxoManager = class _VtxoManager {
3087
3103
  } else {
3088
3104
  threshold = this.renewalConfig?.thresholdMs ?? DEFAULT_RENEWAL_CONFIG.thresholdMs;
3089
3105
  }
3090
- return getExpiringAndRecoverableVtxos(vtxos, threshold, getDustAmount(this.wallet));
3106
+ return getExpiringAndRecoverableVtxos(
3107
+ vtxos,
3108
+ threshold,
3109
+ getDustAmount(this.wallet),
3110
+ now ?? await fetchTimeHeight(this.wallet)
3111
+ );
3091
3112
  }
3092
3113
  /**
3093
3114
  * Renew expiring virtual outputs by settling them back to the wallet's address
@@ -3140,17 +3161,18 @@ var VtxoManager = class _VtxoManager {
3140
3161
  } else {
3141
3162
  threshold = DEFAULT_RENEWAL_CONFIG.thresholdMs;
3142
3163
  }
3143
- let vtxos = await this.getExpiringVtxos(threshold);
3164
+ const now = await fetchTimeHeight(this.wallet);
3165
+ let vtxos = await this.selectExpiringVtxos(threshold, now);
3144
3166
  if (vtxos.length === 0) {
3145
3167
  throw new Error("No VTXOs available to renew");
3146
3168
  }
3147
- vtxos = await this.revalidateBeforeSettle(vtxos, threshold);
3169
+ vtxos = await this.revalidateBeforeSettle(vtxos, threshold, now);
3148
3170
  if (vtxos.length === 0) {
3149
3171
  throw new Error("No VTXOs available to renew");
3150
3172
  }
3151
3173
  const info = await this.getInfoProvider()?.getInfo();
3152
3174
  const vtxoMaxAmount = info?.vtxoMaxAmount ?? -1n;
3153
- const capped = capSettlementBatch(byExpiryAscending(vtxos), vtxoMaxAmount);
3175
+ const capped = capSettlementBatch(byExpiryAscending(vtxos, now), vtxoMaxAmount);
3154
3176
  if (vtxoMaxAmount >= 0n) {
3155
3177
  const oversized = vtxos.filter((vtxo) => BigInt(vtxo.value) > vtxoMaxAmount);
3156
3178
  if (oversized.length > 0) {
@@ -3411,6 +3433,7 @@ var VtxoManager = class _VtxoManager {
3411
3433
  signers: reports
3412
3434
  };
3413
3435
  if (vtxoMigratable.length > 0) {
3436
+ const now = await fetchTimeHeight(this.wallet);
3414
3437
  report.vtxos = await this.runMigrationLeg(
3415
3438
  vtxoMigratable,
3416
3439
  (c) => c.vtxo.value,
@@ -3418,7 +3441,11 @@ var VtxoManager = class _VtxoManager {
3418
3441
  vtxoMaxAmount,
3419
3442
  dustAmount,
3420
3443
  "VTXO",
3421
- (capped) => wallet.sendSelectedVtxosToSelf(capped.map((c) => c.vtxo))
3444
+ (capped) => wallet.sendSelectedVtxosToSelf(
3445
+ capped.map((c) => c.vtxo),
3446
+ now
3447
+ ),
3448
+ (c) => canMigrateBySend(c.vtxo, now)
3422
3449
  );
3423
3450
  }
3424
3451
  if (boardingMigratable.length > 0) {
@@ -3458,10 +3485,22 @@ var VtxoManager = class _VtxoManager {
3458
3485
  * the server ceiling by the cap). The dust floor guards the degenerate cases
3459
3486
  * where every input was oversized or the whole holding sums below dust.
3460
3487
  */
3461
- async runMigrationLeg(candidates, valueOf, toRef, vtxoMaxAmount, dustAmount, legName, submit) {
3488
+ async runMigrationLeg(candidates, valueOf, toRef, vtxoMaxAmount, dustAmount, legName, submit, eligible) {
3489
+ const notSpendableRefs = [];
3490
+ const migratable = eligible ? candidates.filter((c) => {
3491
+ if (eligible(c)) return true;
3492
+ notSpendableRefs.push(toRef(c));
3493
+ return false;
3494
+ }) : candidates;
3495
+ if (notSpendableRefs.length > 0) {
3496
+ console.warn(
3497
+ `Deprecated-signer migration (${legName}): ${notSpendableRefs.length} input(s) are no longer cooperatively spendable at the current chain tip and were excluded; they recover through the sweep/recovery path instead.`
3498
+ );
3499
+ }
3500
+ const notSpendableField = notSpendableRefs.length > 0 ? { notSpendableOffchain: notSpendableRefs } : {};
3462
3501
  const oversizedRefs = [];
3463
3502
  const sized = [];
3464
- for (const c of candidates) {
3503
+ for (const c of migratable) {
3465
3504
  if (vtxoMaxAmount >= 0n && BigInt(valueOf(c)) > vtxoMaxAmount) {
3466
3505
  oversizedRefs.push(toRef(c));
3467
3506
  } else {
@@ -3481,11 +3520,12 @@ var VtxoManager = class _VtxoManager {
3481
3520
  const deferred = sized.length - capped.length;
3482
3521
  const totalAmount = capped.reduce((sum, c) => sum + BigInt(valueOf(c)), 0n);
3483
3522
  if (totalAmount < dustAmount) {
3484
- const onlyOversized = sized.length === 0 && oversizedRefs.length > 0;
3523
+ const skipped = sized.length > 0 ? "below-dust" : oversizedRefs.length > 0 ? "oversized-only" : notSpendableRefs.length > 0 ? "not-spendable-only" : "below-dust";
3485
3524
  return {
3486
3525
  migrated: [],
3487
- skipped: onlyOversized ? "oversized-only" : "below-dust",
3488
- ...oversizedField
3526
+ skipped,
3527
+ ...oversizedField,
3528
+ ...notSpendableField
3489
3529
  };
3490
3530
  }
3491
3531
  try {
@@ -3494,13 +3534,15 @@ var VtxoManager = class _VtxoManager {
3494
3534
  txid,
3495
3535
  migrated: capped.map(toRef),
3496
3536
  ...deferred > 0 ? { deferred } : {},
3497
- ...oversizedField
3537
+ ...oversizedField,
3538
+ ...notSpendableField
3498
3539
  };
3499
3540
  } catch (e) {
3500
3541
  return {
3501
3542
  migrated: [],
3502
3543
  error: e instanceof Error ? e.message : String(e),
3503
- ...oversizedField
3544
+ ...oversizedField,
3545
+ ...notSpendableField
3504
3546
  };
3505
3547
  }
3506
3548
  }
@@ -3528,8 +3570,8 @@ var VtxoManager = class _VtxoManager {
3528
3570
  if (!serverPubKey) continue;
3529
3571
  const cls = classifyAgainstSignerSet(serverPubKey, signerSet, nowSeconds);
3530
3572
  if (cls.status === "CURRENT") continue;
3531
- const recoverable = vtxos.filter((v) => isRecoverable(v));
3532
- const spendable = vtxos.filter((v) => isSpendable(v) && !isRecoverable(v));
3573
+ const recoverable = vtxos.filter((v) => v.isSwept && !hasTerminalSpend(v));
3574
+ const spendable = vtxos.filter((v) => !hasTerminalSpend(v) && !v.isSwept);
3533
3575
  const value = spendable.reduce((sum, v) => sum + v.value, 0);
3534
3576
  let recoverableCount = 0;
3535
3577
  let recoverableValue = 0;
@@ -3542,7 +3584,7 @@ var VtxoManager = class _VtxoManager {
3542
3584
  awaitingSweepCount = spendable.length;
3543
3585
  awaitingSweepValue = value;
3544
3586
  for (const v of spendable) {
3545
- const exp = v.virtualStatus.batchExpiry;
3587
+ const exp = toBatchExpiry(v);
3546
3588
  if (exp !== void 0 && (nextSweepEta === void 0 || exp < nextSweepEta)) {
3547
3589
  nextSweepEta = exp;
3548
3590
  }
@@ -3578,7 +3620,7 @@ var VtxoManager = class _VtxoManager {
3578
3620
  }
3579
3621
  if (isCooperativelyMigratable(cls.status)) {
3580
3622
  for (const v of spendable) {
3581
- if (!v.virtualStatus.batchExpiry) continue;
3623
+ if (v.expiresAt === void 0 && v.expiresAtHeight === void 0) continue;
3582
3624
  migratable.push({ vtxo: v, classification: cls });
3583
3625
  }
3584
3626
  } else if (cls.status === "EXPIRED") {
@@ -3956,7 +3998,7 @@ var VtxoManager = class _VtxoManager {
3956
3998
  * candidates and lets the post-submit `VTXO_ALREADY_SPENT` recovery
3957
3999
  * handle whatever slipped through.
3958
4000
  */
3959
- async revalidateBeforeSettle(candidates, thresholdMs) {
4001
+ async revalidateBeforeSettle(candidates, thresholdMs, now) {
3960
4002
  if (candidates.length === 0) return candidates;
3961
4003
  try {
3962
4004
  const cm = await this.wallet.getContractManager();
@@ -3966,7 +4008,7 @@ var VtxoManager = class _VtxoManager {
3966
4008
  return candidates;
3967
4009
  }
3968
4010
  try {
3969
- const refreshed = await this.getExpiringVtxos(thresholdMs);
4011
+ const refreshed = await this.selectExpiringVtxos(thresholdMs, now);
3970
4012
  const candidateKeys = new Set(candidates.map((v) => `${v.txid}:${v.vout}`));
3971
4013
  return refreshed.filter((v) => candidateKeys.has(`${v.txid}:${v.vout}`));
3972
4014
  } catch (e) {
@@ -4084,10 +4126,12 @@ var VtxoManager = class _VtxoManager {
4084
4126
  (u) => u.status.confirmed && !this.knownBoardingUtxos.has(`${u.txid}:${u.vout}`) && !expiredSet.has(`${u.txid}:${u.vout}`)
4085
4127
  );
4086
4128
  let expiringVtxos = [];
4129
+ let now;
4087
4130
  if (!this.renewalInProgress) {
4088
4131
  try {
4089
- expiringVtxos = await this.getExpiringVtxos();
4090
- expiringVtxos = await this.revalidateBeforeSettle(expiringVtxos);
4132
+ now = await fetchTimeHeight(this.wallet);
4133
+ expiringVtxos = await this.selectExpiringVtxos(void 0, now);
4134
+ expiringVtxos = await this.revalidateBeforeSettle(expiringVtxos, void 0, now);
4091
4135
  } catch (e) {
4092
4136
  console.error("Error fetching expiring VTXOs:", e);
4093
4137
  }
@@ -4119,17 +4163,11 @@ var VtxoManager = class _VtxoManager {
4119
4163
  totalAmount += BigInt(u.value) - BigInt(inputFee.satoshis);
4120
4164
  }
4121
4165
  const filteredVtxos = [];
4122
- for (const v of byExpiryAscending(expiringVtxos)) {
4166
+ for (const v of byExpiryAscending(expiringVtxos, now ?? { timestamp: /* @__PURE__ */ new Date() })) {
4123
4167
  if (filteredVtxos.length >= MAX_VTXOS_PER_SETTLEMENT) {
4124
4168
  break;
4125
4169
  }
4126
- const inputFee = estimator.evalOffchainInput({
4127
- amount: BigInt(v.value),
4128
- type: v.virtualStatus.state === "swept" ? "recoverable" : "vtxo",
4129
- weight: 0,
4130
- birth: v.createdAt,
4131
- expiry: v.virtualStatus.batchExpiry ? new Date(v.virtualStatus.batchExpiry) : void 0
4132
- });
4170
+ const inputFee = estimator.evalOffchainInput(toOffchainInputFeeParams(v));
4133
4171
  if (inputFee.satoshis >= v.value) {
4134
4172
  continue;
4135
4173
  }
@@ -4333,6 +4371,96 @@ function readUInt32BE(array, offset) {
4333
4371
  function noteTapscript(preimageHash) {
4334
4372
  return Script.encode(["SHA256", preimageHash, "EQUAL"]);
4335
4373
  }
4374
+ var ArkadeCash = class _ArkadeCash {
4375
+ constructor(privateKey, serverPubKey, csvTimelock, hrp = _ArkadeCash.DefaultHRP) {
4376
+ this.csvTimelock = csvTimelock;
4377
+ this.hrp = hrp;
4378
+ if (privateKey.length !== 32) {
4379
+ throw new Error(
4380
+ `Invalid private key length: expected 32 bytes, got ${privateKey.length}`
4381
+ );
4382
+ }
4383
+ if (serverPubKey.length !== 32) {
4384
+ throw new Error(
4385
+ `Invalid server public key length: expected 32 bytes, got ${serverPubKey.length}`
4386
+ );
4387
+ }
4388
+ this._privateKey = privateKey.slice();
4389
+ this._serverPubKey = serverPubKey.slice();
4390
+ this._publicKey = pubSchnorr(this._privateKey);
4391
+ }
4392
+ csvTimelock;
4393
+ hrp;
4394
+ static DefaultHRP = "arkadecash";
4395
+ static Version = 0;
4396
+ static PayloadLength = 1 + 32 + 32 + 4;
4397
+ // 69 bytes
4398
+ // Keys are held as private copies so a caller mutating the array it passed
4399
+ // in — or one it reads back from a getter — cannot alter this note's
4400
+ // identity or the outputs derived from it.
4401
+ _privateKey;
4402
+ _serverPubKey;
4403
+ _publicKey;
4404
+ /** The note's private key. Returns a fresh copy; mutating it is a no-op. */
4405
+ get privateKey() {
4406
+ return this._privateKey.slice();
4407
+ }
4408
+ /** The server's public key. Returns a fresh copy; mutating it is a no-op. */
4409
+ get serverPubKey() {
4410
+ return this._serverPubKey.slice();
4411
+ }
4412
+ /** The note's public key. Returns a fresh copy; mutating it is a no-op. */
4413
+ get publicKey() {
4414
+ return this._publicKey.slice();
4415
+ }
4416
+ static generate(serverPubKey, csvTimelock, hrp) {
4417
+ return new _ArkadeCash(randomPrivateKeyBytes(), serverPubKey, csvTimelock, hrp);
4418
+ }
4419
+ static fromString(encoded) {
4420
+ const decoded = bech32m.decodeUnsafe(encoded.trim().toLowerCase(), 1023);
4421
+ if (!decoded) {
4422
+ throw new Error("Invalid arkadeCash string: failed to decode bech32m");
4423
+ }
4424
+ const data = new Uint8Array(bech32m.fromWords(decoded.words));
4425
+ if (data.length !== _ArkadeCash.PayloadLength) {
4426
+ throw new Error(
4427
+ `Invalid arkadeCash data length: expected ${_ArkadeCash.PayloadLength} bytes, got ${data.length}`
4428
+ );
4429
+ }
4430
+ const version = data[0];
4431
+ if (version !== _ArkadeCash.Version) {
4432
+ throw new Error(`Unsupported arkadeCash version: ${version}`);
4433
+ }
4434
+ const privateKey = data.slice(1, 33);
4435
+ const serverPubKey = data.slice(33, 65);
4436
+ const sequence = new DataView(data.buffer, data.byteOffset + 65, 4).getUint32(0, false);
4437
+ const csvTimelock = sequenceToTimelock(sequence);
4438
+ return new _ArkadeCash(privateKey, serverPubKey, csvTimelock, decoded.prefix);
4439
+ }
4440
+ toString() {
4441
+ const data = new Uint8Array(_ArkadeCash.PayloadLength);
4442
+ data[0] = _ArkadeCash.Version;
4443
+ data.set(this._privateKey, 1);
4444
+ data.set(this._serverPubKey, 33);
4445
+ const sequence = timelockToSequence(this.csvTimelock);
4446
+ new DataView(data.buffer, data.byteOffset + 65, 4).setUint32(0, sequence, false);
4447
+ const words = bech32m.toWords(data);
4448
+ return bech32m.encode(this.hrp, words, 1023);
4449
+ }
4450
+ get identity() {
4451
+ return SingleKey.fromPrivateKey(this._privateKey);
4452
+ }
4453
+ get vtxoScript() {
4454
+ return new DefaultVtxo.Script({
4455
+ pubKey: this._publicKey,
4456
+ serverPubKey: this._serverPubKey,
4457
+ csvTimelock: this.csvTimelock
4458
+ });
4459
+ }
4460
+ address(addressHrp) {
4461
+ return this.vtxoScript.address(addressHrp, this._serverPubKey);
4462
+ }
4463
+ };
4336
4464
 
4337
4465
  // src/repositories/intentRepository.ts
4338
4466
  var ALL_INTENT_STATES = [
@@ -5397,13 +5525,13 @@ function subtractAssets(spent, change) {
5397
5525
  return Array.from(map, ([assetId, amount]) => ({ assetId, amount }));
5398
5526
  }
5399
5527
  async function buildTransactionHistory(vtxos, allBoardingTxs, commitmentsToIgnore, getTxCreatedAt) {
5400
- const fromOldestVtxo = [...vtxos].sort((a, b) => a.createdAt.getTime() - b.createdAt.getTime());
5528
+ const fromOldestVtxo = vtxos.map(normalizeVtxo).sort((a, b) => a.createdAt.getTime() - b.createdAt.getTime());
5401
5529
  const unmatchedSettledBoardingTxs = allBoardingTxs.filter(isSettledBoardingReceive).sort((a, b) => a.createdAt - b.createdAt);
5402
5530
  const sent = [];
5403
5531
  let received = [];
5404
5532
  for (const vtxo of fromOldestVtxo) {
5405
5533
  if (vtxo.status.isLeaf) {
5406
- const commitmentTxid = vtxo.virtualStatus.commitmentTxIds[0];
5534
+ const commitmentTxid = vtxo.commitmentTxIds[0];
5407
5535
  const vtxoCreatedAt = vtxo.createdAt.getTime();
5408
5536
  const ignoredCommitment = commitmentsToIgnore.has(commitmentTxid) || !!vtxo.settledBy && commitmentsToIgnore.has(vtxo.settledBy);
5409
5537
  if (ignoredCommitment) {
@@ -5411,7 +5539,7 @@ async function buildTransactionHistory(vtxos, allBoardingTxs, commitmentsToIgnor
5411
5539
  unmatchedSettledBoardingTxs,
5412
5540
  (tx) => tx.createdAt <= vtxoCreatedAt && (tx.key.commitmentTxid === commitmentTxid || tx.key.commitmentTxid === vtxo.settledBy)
5413
5541
  );
5414
- } else if (fromOldestVtxo.filter((v) => v.settledBy === vtxo.virtualStatus.commitmentTxIds[0]).length === 0) {
5542
+ } else if (fromOldestVtxo.filter((v) => v.settledBy === vtxo.commitmentTxIds[0]).length === 0) {
5415
5543
  const duplicateBoardingReceive = consumeBoardingReceive(
5416
5544
  unmatchedSettledBoardingTxs,
5417
5545
  (tx) => tx.amount === vtxo.value && tx.createdAt <= vtxoCreatedAt
@@ -5474,7 +5602,7 @@ async function buildTransactionHistory(vtxos, allBoardingTxs, commitmentsToIgnor
5474
5602
  }
5475
5603
  if (vtxo.settledBy && !commitmentsToIgnore.has(vtxo.settledBy) && !sent.some((s) => s.key.commitmentTxid === vtxo.settledBy)) {
5476
5604
  const changes = fromOldestVtxo.filter(
5477
- (v) => v.status.isLeaf && v.virtualStatus.commitmentTxIds?.every((_) => vtxo.settledBy === _)
5605
+ (v) => v.status.isLeaf && v.commitmentTxIds.length > 0 && v.commitmentTxIds.every((_) => vtxo.settledBy === _)
5478
5606
  );
5479
5607
  const forfeitVtxos = fromOldestVtxo.filter((v) => v.settledBy === vtxo.settledBy);
5480
5608
  const forfeitAmount = forfeitVtxos.reduce((acc, v) => acc + v.value, 0);
@@ -5863,7 +5991,7 @@ var DelegateManagerImpl = class {
5863
5991
  const destinationScript = ArkAddress.decode(destination).pkScript;
5864
5992
  const arkInfo = await this.arkInfoProvider.getInfo();
5865
5993
  const delegateInfo = await this.delegateProvider.getDelegateInfo();
5866
- const eligible = vtxos.filter(
5994
+ const eligible = vtxos.map(normalizeVtxo).filter(
5867
5995
  (v) => isAnnotated(v) && findDelegateTapLeaf(v, delegateInfo.pubkey) !== void 0
5868
5996
  );
5869
5997
  if (eligible.length === 0) {
@@ -5888,14 +6016,15 @@ var DelegateManagerImpl = class {
5888
6016
  }
5889
6017
  return { delegated: eligible, failed: [] };
5890
6018
  }
6019
+ const now = { timestamp: /* @__PURE__ */ new Date() };
5891
6020
  const groupByExpiry = /* @__PURE__ */ new Map();
5892
6021
  let recoverableVtxos = [];
5893
6022
  for (const vtxo of eligible) {
5894
- if (isRecoverable(vtxo)) {
6023
+ if (canRecoverOnchain(vtxo, now)) {
5895
6024
  recoverableVtxos.push(vtxo);
5896
6025
  continue;
5897
6026
  }
5898
- const expiry = vtxo.virtualStatus.batchExpiry;
6027
+ const expiry = toBatchExpiry(vtxo);
5899
6028
  if (!expiry) continue;
5900
6029
  const dayKey = getDayTimestamp(expiry);
5901
6030
  groupByExpiry.set(dayKey, [...groupByExpiry.get(dayKey) ?? [], vtxo]);
@@ -5959,10 +6088,8 @@ async function delegate(identity, delegateProvider, arkInfo, delegateInfo, vtxos
5959
6088
  throw new Error("unable to delegate: delegate provider not configured");
5960
6089
  }
5961
6090
  if (!delegateAt) {
5962
- const expiryTimestamp = vtxos.filter((coin) => !isRecoverable(coin) && coin.virtualStatus.batchExpiry).reduce(
5963
- (min, coin) => Math.min(min, coin.virtualStatus.batchExpiry),
5964
- Number.MAX_SAFE_INTEGER
5965
- );
6091
+ const now = { timestamp: /* @__PURE__ */ new Date() };
6092
+ const expiryTimestamp = vtxos.filter((coin) => !canRecoverOnchain(coin, now) && toBatchExpiry(coin)).reduce((min, coin) => Math.min(min, toBatchExpiry(coin)), Number.MAX_SAFE_INTEGER);
5966
6093
  if (!expiryTimestamp || expiryTimestamp === Number.MAX_SAFE_INTEGER) {
5967
6094
  delegateAt = new Date(Date.now() + 1 * 60 * 1e3);
5968
6095
  } else {
@@ -5991,11 +6118,8 @@ async function delegate(identity, delegateProvider, arkInfo, delegateInfo, vtxos
5991
6118
  let amount = 0n;
5992
6119
  for (const coin of vtxos) {
5993
6120
  const inputFee = estimator.evalOffchainInput({
5994
- amount: BigInt(coin.value),
5995
- type: "vtxo",
5996
- weight: 0,
5997
- birth: coin.createdAt,
5998
- expiry: coin.virtualStatus.batchExpiry ? new Date(coin.virtualStatus.batchExpiry) : void 0
6121
+ ...toOffchainInputFeeParams(coin),
6122
+ type: "vtxo"
5999
6123
  });
6000
6124
  if (inputFee.value >= coin.value) {
6001
6125
  continue;
@@ -6044,7 +6168,7 @@ async function delegate(identity, delegateProvider, arkInfo, delegateInfo, vtxos
6044
6168
  Address(getNetwork(network)).decode(forfeitAddress)
6045
6169
  );
6046
6170
  const forfeits = await Promise.all(
6047
- vtxos.filter((v) => !isRecoverable(v)).map(async (coin) => {
6171
+ vtxos.filter((v) => !canRecoverOnchain(v, { timestamp: /* @__PURE__ */ new Date() })).map(async (coin) => {
6048
6172
  const forfeit = await makeDelegateForfeitTx(
6049
6173
  coin,
6050
6174
  dust,
@@ -6832,7 +6956,8 @@ function validateVtxosForScript(vtxos, script, context) {
6832
6956
  );
6833
6957
  }
6834
6958
  async function getVtxosForContract(repo, contract) {
6835
- return repo.getVtxosForScript ? repo.getVtxosForScript(contract.script) : filterVtxosForScript(await repo.getVtxos(contract.address), contract.script);
6959
+ const vtxos = repo.getVtxosForScript ? await repo.getVtxosForScript(contract.script) : filterVtxosForScript(await repo.getVtxos(contract.address), contract.script);
6960
+ return vtxos.map(normalizeVtxo);
6836
6961
  }
6837
6962
  async function saveVtxosForContract(repo, contract, vtxos) {
6838
6963
  if (repo.saveVtxosForScript) {
@@ -7036,7 +7161,7 @@ var deserializeTapLeaf = (t) => {
7036
7161
  const s = hex.decode(t.s);
7037
7162
  return [cb, s];
7038
7163
  };
7039
- var deserializeVtxo = (o) => ({
7164
+ var deserializeVtxo = (o) => normalizeVtxo({
7040
7165
  ...o,
7041
7166
  createdAt: new Date(o.createdAt),
7042
7167
  tapTree: hex.decode(o.tapTree),
@@ -7635,50 +7760,12 @@ function getLifecycleWeight(v) {
7635
7760
  if (v.arkTxId) weight += 2;
7636
7761
  return weight;
7637
7762
  }
7763
+
7764
+ // src/repositories/migrations/walletRepositoryImpl.ts
7638
7765
  var getVtxosStorageKey = (address) => `vtxos:${address}`;
7639
7766
  var getUtxosStorageKey = (address) => `utxos:${address}`;
7640
7767
  var getTransactionsStorageKey = (address) => `tx:${address}`;
7641
7768
  var walletStateStorageKey = "wallet:state";
7642
- var serializeVtxo2 = (v) => ({
7643
- ...v,
7644
- tapTree: hex.encode(v.tapTree),
7645
- forfeitTapLeafScript: serializeTapLeaf2(v.forfeitTapLeafScript),
7646
- intentTapLeafScript: serializeTapLeaf2(v.intentTapLeafScript),
7647
- extraWitness: v.extraWitness?.map(hex.encode),
7648
- assets: serializeAssets(v.assets)
7649
- });
7650
- var serializeUtxo2 = (u) => ({
7651
- ...u,
7652
- tapTree: hex.encode(u.tapTree),
7653
- forfeitTapLeafScript: serializeTapLeaf2(u.forfeitTapLeafScript),
7654
- intentTapLeafScript: serializeTapLeaf2(u.intentTapLeafScript),
7655
- extraWitness: u.extraWitness?.map(hex.encode)
7656
- });
7657
- var deserializeVtxo2 = (o) => ({
7658
- ...o,
7659
- createdAt: new Date(o.createdAt),
7660
- tapTree: hex.decode(o.tapTree),
7661
- forfeitTapLeafScript: deserializeTapLeaf2(o.forfeitTapLeafScript),
7662
- intentTapLeafScript: deserializeTapLeaf2(o.intentTapLeafScript),
7663
- extraWitness: o.extraWitness?.map(hex.decode),
7664
- assets: deserializeAssets(o.assets)
7665
- });
7666
- var deserializeUtxo2 = (o) => ({
7667
- ...o,
7668
- tapTree: hex.decode(o.tapTree),
7669
- forfeitTapLeafScript: deserializeTapLeaf2(o.forfeitTapLeafScript),
7670
- intentTapLeafScript: deserializeTapLeaf2(o.intentTapLeafScript),
7671
- extraWitness: o.extraWitness?.map(hex.decode)
7672
- });
7673
- var serializeTapLeaf2 = ([cb, s]) => ({
7674
- cb: hex.encode(TaprootControlBlock.encode(cb)),
7675
- s: hex.encode(s)
7676
- });
7677
- var deserializeTapLeaf2 = (t) => {
7678
- const cb = TaprootControlBlock.decode(hex.decode(t.cb));
7679
- const s = hex.decode(t.s);
7680
- return [cb, s];
7681
- };
7682
7769
  var WalletRepositoryImpl = class {
7683
7770
  version = 1;
7684
7771
  storage;
@@ -7690,7 +7777,7 @@ var WalletRepositoryImpl = class {
7690
7777
  if (!stored) return [];
7691
7778
  try {
7692
7779
  const parsed = JSON.parse(stored);
7693
- return parsed.map(deserializeVtxo2);
7780
+ return parsed.map(deserializeVtxo);
7694
7781
  } catch (error) {
7695
7782
  console.error(`Failed to parse VTXOs for address ${address}:`, error);
7696
7783
  return [];
@@ -7710,7 +7797,7 @@ var WalletRepositoryImpl = class {
7710
7797
  }
7711
7798
  await this.storage.setItem(
7712
7799
  getVtxosStorageKey(address),
7713
- JSON.stringify(storedVtxos.map(serializeVtxo2))
7800
+ JSON.stringify(storedVtxos.map(serializeVtxo))
7714
7801
  );
7715
7802
  }
7716
7803
  async clearVtxos(address) {
@@ -7724,7 +7811,7 @@ var WalletRepositoryImpl = class {
7724
7811
  if (!stored) return [];
7725
7812
  try {
7726
7813
  const parsed = JSON.parse(stored);
7727
- return parsed.map(deserializeUtxo2);
7814
+ return parsed.map(deserializeUtxo);
7728
7815
  } catch (error) {
7729
7816
  console.error(`Failed to parse UTXOs for address ${address}:`, error);
7730
7817
  return [];
@@ -7744,7 +7831,7 @@ var WalletRepositoryImpl = class {
7744
7831
  });
7745
7832
  await this.storage.setItem(
7746
7833
  getUtxosStorageKey(address),
7747
- JSON.stringify(storedUtxos.map(serializeUtxo2))
7834
+ JSON.stringify(storedUtxos.map(serializeUtxo))
7748
7835
  );
7749
7836
  }
7750
7837
  async clearUtxos(address) {
@@ -8396,15 +8483,27 @@ var ContractWatcher = class {
8396
8483
  }
8397
8484
  /**
8398
8485
  * Handle a subscription update.
8486
+ *
8487
+ * Normalization boundary: `getSubscription` is part of the public `IndexerProvider` interface,
8488
+ * so a consumer implementation may yield legacy-shaped VTXOs. Normalizing on ingest also fixes
8489
+ * the shape of the payloads emitted to external event consumers.
8399
8490
  */
8400
8491
  handleSubscriptionUpdate(update) {
8401
8492
  if (!this.eventCallback) return;
8402
8493
  const timestamp = Date.now();
8403
8494
  if (update.newVtxos?.length) {
8404
- this.processSubscriptionVtxos(update.newVtxos, "vtxo_received", timestamp);
8495
+ this.processSubscriptionVtxos(
8496
+ update.newVtxos.map(normalizeVtxo),
8497
+ "vtxo_received",
8498
+ timestamp
8499
+ );
8405
8500
  }
8406
8501
  if (update.spentVtxos?.length) {
8407
- this.processSubscriptionVtxos(update.spentVtxos, "vtxo_spent", timestamp);
8502
+ this.processSubscriptionVtxos(
8503
+ update.spentVtxos.map(normalizeVtxo),
8504
+ "vtxo_spent",
8505
+ timestamp
8506
+ );
8408
8507
  }
8409
8508
  }
8410
8509
  /**
@@ -8534,7 +8633,7 @@ async function reconcileIntents(deps) {
8534
8633
  let vtxosByOutpoint = /* @__PURE__ */ new Map();
8535
8634
  if (outpoints.length > 0) {
8536
8635
  try {
8537
- const { vtxos } = await deps.indexerProvider.getVtxos({ outpoints });
8636
+ const { vtxos } = await getNormalizedVtxos(deps.indexerProvider, { outpoints });
8538
8637
  vtxosByOutpoint = new Map(vtxos.map((v) => [outpointKey(v), v]));
8539
8638
  } catch (e) {
8540
8639
  console.error("reconcileIntents: failed to fetch input VTXO state", e);
@@ -8787,9 +8886,10 @@ var ContractManager = class _ContractManager {
8787
8886
  * Safety-critical invariants (spec §2.C / §4):
8788
8887
  * - `opts.materialize(i)` throwing is structural/fatal: it is NOT
8789
8888
  * wrapped — it propagates and aborts the scan.
8790
- * - A `discoverAt` rejection is collected into `handlerErrors` and the
8791
- * loop continues (the gap counter still advances for that index if no
8792
- * other handler hit it).
8889
+ * - A `discoverAt` rejection is collected into `handlerErrors` and makes
8890
+ * its index *indeterminate*: it does NOT advance the gap counter, and
8891
+ * the scan stops verifying there, reporting `truncatedAt`. Only an index
8892
+ * every handler answered for can be a confirmed miss.
8793
8893
  * - `persistAndWatchContract` rejecting is operational/fatal and
8794
8894
  * propagates (only `discoverAt` is guarded).
8795
8895
  * - Within an index the handler probes run concurrently (independent
@@ -8803,8 +8903,12 @@ var ContractManager = class _ContractManager {
8803
8903
  * over-scanned, nothing is discarded, and `materialize`/`discoverAt` are
8804
8904
  * invoked on exactly the same index set. The window's hits are still
8805
8905
  * processed strictly in ascending index order, so the discovered set,
8806
- * persisted rows, `lastIndexUsed`, and `handlerErrors` are byte-for-byte
8807
- * identical to the serial path — only the wall-clock differs.
8906
+ * persisted rows, `highestConfirmedUsedIndex`, and `handlerErrors` are
8907
+ * byte-for-byte identical to the serial path — only the wall-clock
8908
+ * differs. Truncation is the one exception: a window's concurrent probes
8909
+ * can surface hits above the failed index that a serial scan would never
8910
+ * have reached, so a truncated batched scan discovers a superset — never
8911
+ * a subset — of the serial one.
8808
8912
  */
8809
8913
  async scanContracts(opts) {
8810
8914
  const gapLimit = opts.gapLimit ?? 20;
@@ -8826,7 +8930,8 @@ var ContractManager = class _ContractManager {
8826
8930
  ];
8827
8931
  const maxIdx = opts.hd ? SCAN_MAX_INDEX : 0;
8828
8932
  const handlerErrors = [];
8829
- let lastIndexUsed = -1;
8933
+ let highestConfirmedUsedIndex = -1;
8934
+ let truncatedAt;
8830
8935
  let unused = 0;
8831
8936
  let i = 0;
8832
8937
  const probeIndex = async (index) => {
@@ -8853,6 +8958,7 @@ var ContractManager = class _ContractManager {
8853
8958
  const index = windowIndices[w];
8854
8959
  const probes = windowProbes[w];
8855
8960
  let hitAtThisIndex = false;
8961
+ let indeterminate = false;
8856
8962
  for (let h = 0; h < discoverables.length; h++) {
8857
8963
  const probe = probes[h];
8858
8964
  if (!probe.ok) {
@@ -8861,6 +8967,7 @@ var ContractManager = class _ContractManager {
8861
8967
  index,
8862
8968
  error: probe.error
8863
8969
  });
8970
+ indeterminate = true;
8864
8971
  continue;
8865
8972
  }
8866
8973
  for (const c of probe.found) {
@@ -8868,21 +8975,26 @@ var ContractManager = class _ContractManager {
8868
8975
  hitAtThisIndex = true;
8869
8976
  }
8870
8977
  }
8871
- if (hitAtThisIndex) {
8872
- lastIndexUsed = index;
8873
- unused = 0;
8874
- } else {
8875
- unused += 1;
8876
- }
8978
+ if (indeterminate && truncatedAt === void 0) truncatedAt = index;
8979
+ if (hitAtThisIndex) highestConfirmedUsedIndex = index;
8980
+ if (truncatedAt !== void 0) continue;
8981
+ if (hitAtThisIndex) unused = 0;
8982
+ else unused += 1;
8877
8983
  }
8984
+ if (truncatedAt !== void 0) break;
8878
8985
  i = windowEnd + 1;
8879
8986
  }
8880
- if (opts.hd && i > maxIdx && unused < gapLimit) {
8987
+ if (opts.hd && truncatedAt === void 0 && i > maxIdx && unused < gapLimit) {
8881
8988
  throw new Error(
8882
8989
  `scanContracts: reached SCAN_MAX_INDEX (${SCAN_MAX_INDEX}) without closing the ${gapLimit}-index gap window; a Discoverable handler may be returning unconditional hits`
8883
8990
  );
8884
8991
  }
8885
- return { lastIndexUsed, handlerErrors };
8992
+ return {
8993
+ lastIndexUsed: highestConfirmedUsedIndex,
8994
+ highestConfirmedUsedIndex,
8995
+ ...truncatedAt !== void 0 && { truncatedAt },
8996
+ handlerErrors
8997
+ };
8886
8998
  }
8887
8999
  /**
8888
9000
  * Get contracts with optional filters.
@@ -8929,7 +9041,9 @@ var ContractManager = class _ContractManager {
8929
9041
  byScript.set(contract.script, contract);
8930
9042
  }
8931
9043
  const tapscriptCache = /* @__PURE__ */ new Map();
8932
- return vtxos.map((vtxo) => extendVirtualCoinForContract(vtxo, byScript, tapscriptCache));
9044
+ return vtxos.map(
9045
+ (vtxo) => extendVirtualCoinForContract(normalizeVtxo(vtxo), byScript, tapscriptCache)
9046
+ );
8933
9047
  }
8934
9048
  buildContractsDbFilter(filter) {
8935
9049
  return {
@@ -9097,7 +9211,7 @@ var ContractManager = class _ContractManager {
9097
9211
  }
9098
9212
  async refreshOutpoints(outpoints) {
9099
9213
  if (outpoints.length === 0) return;
9100
- const { vtxos } = await this.config.indexerProvider.getVtxos({
9214
+ const { vtxos } = await getNormalizedVtxos(this.config.indexerProvider, {
9101
9215
  outpoints
9102
9216
  });
9103
9217
  if (vtxos.length === 0) return;
@@ -9225,7 +9339,7 @@ var ContractManager = class _ContractManager {
9225
9339
  async reconcilePendingFrontier(contracts) {
9226
9340
  const scripts = contracts.map((c) => c.script);
9227
9341
  const scriptToContract = new Map(contracts.map((c) => [c.script, c]));
9228
- const { vtxos } = await this.config.indexerProvider.getVtxos({
9342
+ const { vtxos } = await getNormalizedVtxos(this.config.indexerProvider, {
9229
9343
  scripts,
9230
9344
  pendingOnly: true
9231
9345
  });
@@ -9316,7 +9430,7 @@ var ContractManager = class _ContractManager {
9316
9430
  let pageIndex = 0;
9317
9431
  let hasMore = true;
9318
9432
  while (hasMore) {
9319
- const { vtxos, page } = await this.config.indexerProvider.getVtxos({
9433
+ const { vtxos, page } = await getNormalizedVtxos(this.config.indexerProvider, {
9320
9434
  scripts,
9321
9435
  ...windowOpts,
9322
9436
  pageIndex,
@@ -10127,6 +10241,24 @@ async function spendableVtxosExcludingLocked(vtxos, intentRepository) {
10127
10241
  }
10128
10242
  return excludeLockedOutpoints(vtxos, locked);
10129
10243
  }
10244
+ var cashReport = (vtxo, reason) => ({
10245
+ txid: vtxo.txid,
10246
+ vout: vtxo.vout,
10247
+ value: vtxo.value,
10248
+ reason
10249
+ });
10250
+ var ArkadeCashCreateError = class extends Error {
10251
+ constructor(cash, cause) {
10252
+ super(
10253
+ `Failed to create ArkadeCash: send failed after the note may have been submitted. Recover with claimCash using the token on this error's \`cash\` field.`
10254
+ );
10255
+ this.cash = cash;
10256
+ this.cause = cause;
10257
+ this.name = "ArkadeCashCreateError";
10258
+ }
10259
+ cash;
10260
+ cause;
10261
+ };
10130
10262
  var ReadonlyWallet = class _ReadonlyWallet {
10131
10263
  constructor(identity, network, onchainProvider, indexerProvider, arkServerPublicKey, offchainTapscript, boardingTapscript, dustAmount, walletRepository, contractRepository, delegateProvider, watcherConfig, walletContractTimelocks) {
10132
10264
  this.identity = identity;
@@ -10585,20 +10717,21 @@ var ReadonlyWallet = class _ReadonlyWallet {
10585
10717
  }
10586
10718
  let recoverable = 0;
10587
10719
  let pendingRecovery = 0;
10588
- const settled = vtxos.filter((coin) => coin.virtualStatus.state === "settled" && !isPendingRecovery(coin)).reduce((sum, coin) => sum + coin.value, 0);
10589
- const preconfirmed = vtxos.filter(
10590
- (coin) => coin.virtualStatus.state === "preconfirmed" && !isPendingRecovery(coin)
10720
+ const now = { timestamp: /* @__PURE__ */ new Date() };
10721
+ const settled = vtxos.filter(
10722
+ (coin) => canSpendOffchain(coin, now) && !coin.isPreconfirmed && !isPendingRecovery(coin)
10591
10723
  ).reduce((sum, coin) => sum + coin.value, 0);
10592
- const available = unlockedVtxos.filter(
10593
- (coin) => (coin.virtualStatus.state === "settled" || coin.virtualStatus.state === "preconfirmed") && !isPendingRecovery(coin)
10724
+ const preconfirmed = vtxos.filter(
10725
+ (coin) => canSpendOffchain(coin, now) && coin.isPreconfirmed && !isPendingRecovery(coin)
10594
10726
  ).reduce((sum, coin) => sum + coin.value, 0);
10595
- recoverable = vtxos.filter((coin) => isSpendable(coin) && coin.virtualStatus.state === "swept").reduce((sum, coin) => sum + coin.value, 0);
10727
+ const available = unlockedVtxos.filter((coin) => canSpendOffchain(coin, now) && !isPendingRecovery(coin)).reduce((sum, coin) => sum + coin.value, 0);
10728
+ recoverable = vtxos.filter((coin) => canRecoverOnchain(coin, now) && !isPendingRecovery(coin)).reduce((sum, coin) => sum + coin.value, 0);
10596
10729
  pendingRecovery = vtxos.filter(isPendingRecovery).reduce((sum, coin) => sum + coin.value, 0);
10597
10730
  const totalBoarding = confirmed + unconfirmed;
10598
10731
  const totalOffchain = settled + preconfirmed + recoverable + pendingRecovery;
10599
10732
  const assetBalances = /* @__PURE__ */ new Map();
10600
10733
  for (const vtxo of vtxos) {
10601
- if (!isSpendable(vtxo)) continue;
10734
+ if (hasTerminalSpend(vtxo)) continue;
10602
10735
  if (vtxo.assets) {
10603
10736
  for (const a of vtxo.assets) {
10604
10737
  const current = assetBalances.get(a.assetId) ?? 0n;
@@ -10634,12 +10767,13 @@ var ReadonlyWallet = class _ReadonlyWallet {
10634
10767
  const f = filter ?? { withRecoverable: true, withUnrolled: false };
10635
10768
  const contractManager = await this.getContractManager();
10636
10769
  const vtxos = await contractManager.getContractsWithVtxos();
10770
+ const now = { timestamp: /* @__PURE__ */ new Date() };
10637
10771
  return vtxos.flatMap((_) => _.vtxos).filter((vtxo) => {
10638
10772
  if (this._pendingSpendOutpoints.has(`${vtxo.txid}:${vtxo.vout}`)) {
10639
10773
  return false;
10640
10774
  }
10641
- if (isSpendable(vtxo)) {
10642
- if (!f.withRecoverable && (isRecoverable(vtxo) || isExpired(vtxo))) {
10775
+ if (!hasTerminalSpend(vtxo)) {
10776
+ if (!f.withRecoverable && canRecoverOnchain(vtxo, now)) {
10643
10777
  return false;
10644
10778
  }
10645
10779
  return true;
@@ -10672,7 +10806,7 @@ var ReadonlyWallet = class _ReadonlyWallet {
10672
10806
  const response = await contractManager.getContractsWithVtxos();
10673
10807
  const allVtxos = response.flatMap((_) => _.vtxos);
10674
10808
  const { boardingTxs, commitmentsToIgnore } = await this.getBoardingTxs();
10675
- const getTxCreatedAt = (txid) => this.indexerProvider.getVtxos({ outpoints: [{ txid, vout: 0 }] }).then((res) => res.vtxos[0]?.createdAt.getTime()).catch((err) => {
10809
+ const getTxCreatedAt = (txid) => getNormalizedVtxos(this.indexerProvider, { outpoints: [{ txid, vout: 0 }] }).then((res) => res.vtxos[0]?.createdAt.getTime()).catch((err) => {
10676
10810
  if (isRetryableProviderError(err)) return void 0;
10677
10811
  throw err;
10678
10812
  });
@@ -10748,6 +10882,12 @@ var ReadonlyWallet = class _ReadonlyWallet {
10748
10882
  if (spent && commitmentTxid) {
10749
10883
  commitmentsToIgnore.add(commitmentTxid);
10750
10884
  }
10885
+ const boardingFacts = {
10886
+ isSpent: spent,
10887
+ isSwept: false,
10888
+ isPreconfirmed: false,
10889
+ commitmentTxIds: spent && commitmentTxid ? [commitmentTxid] : []
10890
+ };
10751
10891
  utxos.push({
10752
10892
  txid: tx.txid,
10753
10893
  vout: i,
@@ -10757,10 +10897,9 @@ var ReadonlyWallet = class _ReadonlyWallet {
10757
10897
  block_time: tx.status.block_time
10758
10898
  },
10759
10899
  isUnrolled: true,
10760
- virtualStatus: {
10761
- state: spent ? "spent" : "settled",
10762
- commitmentTxIds: spent && commitmentTxid ? [commitmentTxid] : void 0
10763
- },
10900
+ ...boardingFacts,
10901
+ virtualStatus: toVirtualStatus(boardingFacts),
10902
+ spentBy: "",
10764
10903
  createdAt: tx.status.confirmed ? new Date(tx.status.block_time * 1e3) : /* @__PURE__ */ new Date(0),
10765
10904
  script: scriptHex
10766
10905
  });
@@ -10774,12 +10913,12 @@ var ReadonlyWallet = class _ReadonlyWallet {
10774
10913
  const tx = {
10775
10914
  key: {
10776
10915
  boardingTxid: utxo.txid,
10777
- commitmentTxid: utxo.virtualStatus.commitmentTxIds?.[0] ?? "",
10916
+ commitmentTxid: utxo.commitmentTxIds[0] ?? "",
10778
10917
  arkTxid: ""
10779
10918
  },
10780
10919
  amount: utxo.value,
10781
10920
  type: "RECEIVED" /* TxReceived */,
10782
- settled: utxo.virtualStatus.state === "spent",
10921
+ settled: utxo.isSpent,
10783
10922
  createdAt: utxo.status.block_time ? new Date(utxo.status.block_time * 1e3).getTime() : 0
10784
10923
  };
10785
10924
  if (!utxo.status.block_time) {
@@ -11003,11 +11142,11 @@ var ReadonlyWallet = class _ReadonlyWallet {
11003
11142
  /** Fetch Arkade transaction ids that are still pending final settlement. */
11004
11143
  async fetchPendingTxs() {
11005
11144
  const scripts = await this.getWalletScripts();
11006
- let { vtxos } = await this.indexerProvider.getVtxos({
11145
+ let { vtxos } = await getNormalizedVtxos(this.indexerProvider, {
11007
11146
  scripts
11008
11147
  });
11009
11148
  return vtxos.filter(
11010
- (vtxo) => vtxo.virtualStatus.state !== "swept" && vtxo.virtualStatus.state !== "settled" && vtxo.arkTxId !== void 0
11149
+ (vtxo) => (vtxo.isSpent || vtxo.isPreconfirmed && !vtxo.isSwept) && vtxo.arkTxId !== void 0
11011
11150
  ).map((_) => _.arkTxId);
11012
11151
  }
11013
11152
  // ========================================================================
@@ -11106,7 +11245,7 @@ var ReadonlyWallet = class _ReadonlyWallet {
11106
11245
  });
11107
11246
  onVtxosPersisted = async (_contract, vtxos) => {
11108
11247
  for (const v of vtxos) {
11109
- if (v.virtualStatus.state === "spent") continue;
11248
+ if (v.isSpent) continue;
11110
11249
  await captureExitBranch({
11111
11250
  resolver,
11112
11251
  repository: virtualTxRepository,
@@ -11583,14 +11722,14 @@ var Wallet2 = class _Wallet extends ReadonlyWallet {
11583
11722
  _restoreInFlight;
11584
11723
  _addPendingSpends(inputs) {
11585
11724
  for (const input of inputs) {
11586
- if ("virtualStatus" in input) {
11725
+ if (isVirtualCoin(input)) {
11587
11726
  this._pendingSpendOutpoints.add(`${input.txid}:${input.vout}`);
11588
11727
  }
11589
11728
  }
11590
11729
  }
11591
11730
  _removePendingSpends(inputs) {
11592
11731
  for (const input of inputs) {
11593
- if ("virtualStatus" in input) {
11732
+ if (isVirtualCoin(input)) {
11594
11733
  this._pendingSpendOutpoints.delete(`${input.txid}:${input.vout}`);
11595
11734
  }
11596
11735
  }
@@ -11678,15 +11817,22 @@ var Wallet2 = class _Wallet extends ReadonlyWallet {
11678
11817
  materialize,
11679
11818
  deps
11680
11819
  });
11681
- if (hd && result.lastIndexUsed >= 0) {
11682
- await provider.advanceLastIndexUsed(result.lastIndexUsed);
11820
+ if (hd && result.highestConfirmedUsedIndex >= 0) {
11821
+ await provider.advanceLastIndexUsed(result.highestConfirmedUsedIndex);
11683
11822
  }
11684
11823
  await manager.refreshVtxos({ includeInactive: true });
11824
+ const causes = result.handlerErrors.map(
11825
+ (e) => e.error instanceof Error ? e.error : new Error(String(e.error))
11826
+ );
11827
+ if (result.truncatedAt !== void 0) {
11828
+ throw new AggregateError(
11829
+ causes,
11830
+ `restore: scan truncated at index ${result.truncatedAt}; indices >= ${result.truncatedAt} are unverified (${result.handlerErrors.length} discovery handler failure(s)). Retry is safe (idempotent).`
11831
+ );
11832
+ }
11685
11833
  if (result.handlerErrors.length > 0) {
11686
11834
  throw new AggregateError(
11687
- result.handlerErrors.map(
11688
- (e) => e.error instanceof Error ? e.error : new Error(String(e.error))
11689
- ),
11835
+ causes,
11690
11836
  `restore: ${result.handlerErrors.length} discovery handler(s) failed; the gap window may have closed early \u2014 retry is safe (idempotent).`
11691
11837
  );
11692
11838
  }
@@ -11998,13 +12144,7 @@ var Wallet2 = class _Wallet extends ReadonlyWallet {
11998
12144
  if (filteredVtxos.length >= MAX_VTXOS_PER_SETTLEMENT) {
11999
12145
  break;
12000
12146
  }
12001
- const inputFee = estimator.evalOffchainInput({
12002
- amount: BigInt(vtxo.value),
12003
- type: vtxo.virtualStatus.state === "swept" ? "recoverable" : "vtxo",
12004
- weight: 0,
12005
- birth: vtxo.createdAt,
12006
- expiry: vtxo.virtualStatus.batchExpiry ? new Date(vtxo.virtualStatus.batchExpiry) : void 0
12007
- });
12147
+ const inputFee = estimator.evalOffchainInput(toOffchainInputFeeParams(vtxo));
12008
12148
  if (inputFee.satoshis >= vtxo.value) {
12009
12149
  continue;
12010
12150
  }
@@ -12220,9 +12360,9 @@ var Wallet2 = class _Wallet extends ReadonlyWallet {
12220
12360
  * renewal / offboard) is not a board and leaves the boarding address
12221
12361
  * untouched.
12222
12362
  *
12223
- * Boarding inputs are the non-VTXO coins (no `virtualStatus`), the same
12363
+ * Boarding inputs are the non-VTXO coins (no `script`), the same
12224
12364
  * discriminator {@link handleSettlementFinalizationEvent} uses; the
12225
- * `typeof` guard skips arknote string inputs before the `in` test.
12365
+ * `typeof` guard skips the arknote strings settle also accepts.
12226
12366
  *
12227
12367
  * No-ops for static / `auto` wallets (no descriptor provider — boarding
12228
12368
  * stays on its fixed index-0 address). Best-effort and non-fatal: the
@@ -12235,7 +12375,7 @@ var Wallet2 = class _Wallet extends ReadonlyWallet {
12235
12375
  async maybeRotateBoardingAfterBoard(inputs) {
12236
12376
  if (!this._descriptorProvider) return;
12237
12377
  const consumedBoarding = inputs.some(
12238
- (input) => typeof input !== "string" && !("virtualStatus" in input)
12378
+ (input) => typeof input !== "string" && !isVirtualCoin(input)
12239
12379
  );
12240
12380
  if (!consumedBoarding) return;
12241
12381
  try {
@@ -12246,13 +12386,12 @@ var Wallet2 = class _Wallet extends ReadonlyWallet {
12246
12386
  }
12247
12387
  async handleSettlementFinalizationEvent(event, inputs, forfeitOutputScript, connectorsGraph) {
12248
12388
  const signedForfeits = [];
12249
- const isVtxo = (input) => "virtualStatus" in input;
12250
12389
  let settlementPsbt = Transaction$2.fromPSBT(base64.decode(event.commitmentTx));
12251
12390
  let hasBoardingUtxos = false;
12252
12391
  let connectorIndex = 0;
12253
12392
  const connectorsLeaves = connectorsGraph?.leaves() || [];
12254
12393
  for (const input of inputs) {
12255
- if (!isVtxo(input)) {
12394
+ if (!isVirtualCoin(input)) {
12256
12395
  for (let i = 0; i < settlementPsbt.inputsLength; i++) {
12257
12396
  const settlementInput = settlementPsbt.getInput(i);
12258
12397
  if (!settlementInput.txid || settlementInput.index === void 0) {
@@ -12283,7 +12422,7 @@ var Wallet2 = class _Wallet extends ReadonlyWallet {
12283
12422
  }
12284
12423
  continue;
12285
12424
  }
12286
- if (isRecoverable(input) || isSubdust({ value: input.value }, this.dustAmount)) {
12425
+ if (canRecoverOnchain(input, { timestamp: /* @__PURE__ */ new Date() }) || isSubdust({ value: input.value }, this.dustAmount)) {
12287
12426
  continue;
12288
12427
  }
12289
12428
  if (connectorsLeaves.length === 0) {
@@ -12586,18 +12725,17 @@ var Wallet2 = class _Wallet extends ReadonlyWallet {
12586
12725
  if (!hasPending) {
12587
12726
  return { finalized: [], pending: [] };
12588
12727
  }
12589
- const MAX_INPUTS_PER_INTENT = 20;
12590
12728
  if (!vtxos || vtxos.length === 0) {
12591
12729
  const scriptMap = await this.getScriptMap();
12592
12730
  const allExtended = [];
12593
12731
  const allScripts = [...scriptMap.keys()];
12594
- const { vtxos: fetchedVtxos } = await this.indexerProvider.getVtxos({
12732
+ const { vtxos: fetchedVtxos } = await getNormalizedVtxos(this.indexerProvider, {
12595
12733
  scripts: allScripts
12596
12734
  });
12597
12735
  for (const vtxo of fetchedVtxos) {
12598
12736
  const vtxoScript = scriptMap.get(vtxo.script);
12599
12737
  if (!vtxoScript) continue;
12600
- if (vtxo.virtualStatus.state === "swept" || vtxo.virtualStatus.state === "settled") {
12738
+ if (!vtxo.isSpent && !(vtxo.isPreconfirmed && !vtxo.isSwept)) {
12601
12739
  continue;
12602
12740
  }
12603
12741
  allExtended.push({
@@ -12696,6 +12834,271 @@ var Wallet2 = class _Wallet extends ReadonlyWallet {
12696
12834
  settings: { ...state.settings, hasPendingTx: value }
12697
12835
  }));
12698
12836
  }
12837
+ /**
12838
+ * Create an ArkadeCash bearer instrument.
12839
+ *
12840
+ * Generates a fresh keypair, sends the specified amount to a DefaultVtxo
12841
+ * controlled by the new key, and returns the encoded arkadeCash string.
12842
+ * The receiver can claim the funds using `claimCash()` without ever
12843
+ * sharing their address.
12844
+ *
12845
+ * ArkadeCash is a short-lived instrument: it carries a private key, so it is
12846
+ * meant to be handed over and claimed promptly, not held. A note left
12847
+ * unclaimed past its batch expiry is swept by the server and `claimCash`
12848
+ * can only report it, not move it.
12849
+ *
12850
+ * @param amount - Amount in satoshis to send. Must be a whole number of
12851
+ * sats at or above the dust threshold — a below-dust amount would mint an
12852
+ * OP_RETURN output that is unspendable as cash.
12853
+ * @returns The encoded arkadeCash string (e.g., "arkadecash1...")
12854
+ */
12855
+ async createCash(amount) {
12856
+ if (!Number.isSafeInteger(amount) || amount < Number(this.dustAmount)) {
12857
+ throw new Error(
12858
+ `Invalid ArkadeCash amount ${amount}: must be a whole number of sats >= dust (${this.dustAmount})`
12859
+ );
12860
+ }
12861
+ const cashHrp = this.network.hrp.replace(/ark$/, "arkadecash");
12862
+ const cash = ArkadeCash.generate(
12863
+ this.arkServerPublicKey,
12864
+ this.offchainTapscript.options.csvTimelock,
12865
+ cashHrp
12866
+ );
12867
+ const address = cash.address(this.network.hrp).encode();
12868
+ const cashStr = cash.toString();
12869
+ try {
12870
+ await this.send({ address, amount });
12871
+ } catch (error) {
12872
+ throw new ArkadeCashCreateError(cashStr, error);
12873
+ }
12874
+ return cashStr;
12875
+ }
12876
+ /**
12877
+ * Claim an ArkadeCash bearer instrument: sweep what can be swept, report the
12878
+ * rest.
12879
+ *
12880
+ * Every spendable VTXO at the arkadeCash address is swept to this wallet in
12881
+ * its own offchain transaction, signed with the key carried by the arkadeCash
12882
+ * string. Nothing is ever persisted: no contract is imported, and the
12883
+ * arkadeCash key is not stored. Anything that cannot be swept — server-swept,
12884
+ * subdust, already claimed, or asset-bearing — is returned in `unclaimed`
12885
+ * with a per-VTXO reason and otherwise ignored.
12886
+ *
12887
+ * The arkadeCash string is the recovery token: a claim interrupted between
12888
+ * submit and finalize is completed by simply re-running `claimCash`, which
12889
+ * drains any pending arkadeCash transaction on the server before sweeping.
12890
+ *
12891
+ * @param cashStr - The encoded arkadeCash string (e.g., "arkadecash1...")
12892
+ * @returns The swept total and the report of what was left behind. The
12893
+ * shape is open: further buckets may be added alongside `unclaimed`.
12894
+ */
12895
+ async claimCash(cashStr) {
12896
+ const cash = ArkadeCash.fromString(cashStr);
12897
+ const cashScript = cash.vtxoScript;
12898
+ const cashPkScript = hex.encode(cashScript.pkScript);
12899
+ const scripts = [cashPkScript];
12900
+ let vtxos = await this.fetchAllVtxos(scripts);
12901
+ if (vtxos.length === 0) {
12902
+ throw new Error("No VTXOs found for this arkadeCash");
12903
+ }
12904
+ const myPkScript = ArkAddress.decode(await this.getAddress()).pkScript;
12905
+ let { spendable, unclaimed } = this.classifyCashVtxos(vtxos);
12906
+ let swept = 0;
12907
+ const drainable = vtxos.filter((vtxo) => !isSubdust(vtxo, this.dustAmount));
12908
+ if (drainable.length > 0) {
12909
+ let drained = { count: 0, swept: 0, claimed: /* @__PURE__ */ new Set() };
12910
+ try {
12911
+ drained = await this.finalizePendingCashTxs(
12912
+ cash,
12913
+ drainable,
12914
+ cashScript,
12915
+ myPkScript
12916
+ );
12917
+ } catch (error) {
12918
+ console.error("Failed to drain pending arkadeCash txs:", error);
12919
+ }
12920
+ if (drained.count > 0) {
12921
+ vtxos = await this.fetchAllVtxos(scripts);
12922
+ ({ spendable, unclaimed } = this.classifyCashVtxos(vtxos));
12923
+ swept += drained.swept;
12924
+ unclaimed = unclaimed.filter(
12925
+ (vtxo) => !drained.claimed.has(`${vtxo.txid}:${vtxo.vout}`)
12926
+ );
12927
+ }
12928
+ }
12929
+ if (spendable.length > 0) {
12930
+ const info = await this.arkProvider.getInfo();
12931
+ const serverUnrollScript = CSVMultisigTapscript.decode(
12932
+ hex.decode(info.checkpointTapscript)
12933
+ );
12934
+ for (const vtxo of spendable) {
12935
+ try {
12936
+ await signAndSubmitOffchainTx({
12937
+ identity: cash.identity,
12938
+ provider: this.arkProvider,
12939
+ inputs: [
12940
+ {
12941
+ txid: vtxo.txid,
12942
+ vout: vtxo.vout,
12943
+ value: vtxo.value,
12944
+ tapLeafScript: cashScript.forfeit(),
12945
+ tapTree: cashScript.encode()
12946
+ }
12947
+ ],
12948
+ outputs: [{ script: myPkScript, amount: BigInt(vtxo.value) }],
12949
+ serverUnrollScript
12950
+ });
12951
+ swept += vtxo.value;
12952
+ } catch (error) {
12953
+ console.error(
12954
+ `Failed to sweep arkadeCash VTXO ${vtxo.txid}:${vtxo.vout}:`,
12955
+ error
12956
+ );
12957
+ unclaimed.push(cashReport(vtxo, "sweep-failed"));
12958
+ }
12959
+ }
12960
+ }
12961
+ return {
12962
+ swept,
12963
+ unclaimed: {
12964
+ amount: unclaimed.reduce((total, vtxo) => total + vtxo.value, 0),
12965
+ vtxos: unclaimed
12966
+ }
12967
+ };
12968
+ }
12969
+ /**
12970
+ * Split the VTXOs at an arkadeCash address into the ones the thin sweep can
12971
+ * move and the ones it can only report.
12972
+ */
12973
+ classifyCashVtxos(vtxos) {
12974
+ const spendable = [];
12975
+ const unclaimed = [];
12976
+ for (const vtxo of vtxos) {
12977
+ if (hasTerminalSpend(vtxo)) {
12978
+ unclaimed.push(cashReport(vtxo, "already-spent"));
12979
+ } else if (isSubdust(vtxo, this.dustAmount)) {
12980
+ unclaimed.push(cashReport(vtxo, "subdust"));
12981
+ } else if (vtxo.isSwept) {
12982
+ unclaimed.push(cashReport(vtxo, "swept"));
12983
+ } else if (vtxo.assets && vtxo.assets.length > 0) {
12984
+ unclaimed.push(cashReport(vtxo, "has-assets"));
12985
+ } else {
12986
+ spendable.push(vtxo);
12987
+ }
12988
+ }
12989
+ return { spendable, unclaimed };
12990
+ }
12991
+ /** Read every page of an unfiltered VTXO query for the given scripts. */
12992
+ async fetchAllVtxos(scripts) {
12993
+ const pageSize = 100;
12994
+ const all = [];
12995
+ const seen = /* @__PURE__ */ new Set();
12996
+ for (let pageIndex = 0; ; pageIndex++) {
12997
+ const { vtxos, page } = await getNormalizedVtxos(this.indexerProvider, {
12998
+ scripts,
12999
+ pageIndex,
13000
+ pageSize
13001
+ });
13002
+ for (const vtxo of vtxos) {
13003
+ const outpoint = `${vtxo.txid}:${vtxo.vout}`;
13004
+ if (seen.has(outpoint)) continue;
13005
+ seen.add(outpoint);
13006
+ all.push(vtxo);
13007
+ }
13008
+ if (!page || vtxos.length < pageSize) return all;
13009
+ }
13010
+ }
13011
+ /**
13012
+ * Complete any sweep a previous `claimCash` registered but never finalized.
13013
+ * A thin variant of {@link finalizePendingTxs}, scoped to the arkadeCash key:
13014
+ * the intent is signed with it, and so are the returned checkpoints.
13015
+ *
13016
+ * A drained sweep is not automatically *ours*. Whoever registered it chose
13017
+ * the destination, so a tx a different claimer left half-finished pays that
13018
+ * claimer — finalizing it is still right (the input is spent either way, and
13019
+ * completing it unsticks the funds), but only the value it actually pays to
13020
+ * `myPkScript` may be counted as claimed here.
13021
+ *
13022
+ * @param vtxos - VTXOs at the arkadeCash contract's own pkScript, spent ones
13023
+ * included (a registered sweep marks its input spent). Any other outpoint —
13024
+ * subdust above all — would make the proof invalid, since it describes every
13025
+ * input by that pkScript.
13026
+ * @returns The number of pending txs finalized, the sats among them paid to
13027
+ * this wallet, and the outpoints those txs consumed to pay it.
13028
+ */
13029
+ async finalizePendingCashTxs(cash, vtxos, cashScript, myPkScript) {
13030
+ const result = { count: 0, swept: 0, claimed: /* @__PURE__ */ new Set() };
13031
+ const inputs = vtxos.map((vtxo) => ({
13032
+ ...vtxo,
13033
+ forfeitTapLeafScript: cashScript.forfeit(),
13034
+ intentTapLeafScript: cashScript.forfeit(),
13035
+ tapTree: cashScript.encode()
13036
+ }));
13037
+ if (inputs.length === 0) return result;
13038
+ const identity = cash.identity;
13039
+ const seenTxids = /* @__PURE__ */ new Set();
13040
+ const pendingTxs = [];
13041
+ for (let i = 0; i < inputs.length; i += MAX_INPUTS_PER_INTENT) {
13042
+ const batch = inputs.slice(i, i + MAX_INPUTS_PER_INTENT);
13043
+ const message = { type: "get-pending-tx", expire_at: 0 };
13044
+ const proof = Intent.create(message, batch, []);
13045
+ const signedProof = await identity.sign(
13046
+ proof,
13047
+ Array.from({ length: batch.length + 1 }, (_, j) => j)
13048
+ );
13049
+ const batchPending = await this.arkProvider.getPendingTxs({
13050
+ proof: base64.encode(signedProof.toPSBT()),
13051
+ message
13052
+ });
13053
+ for (const pendingTx of batchPending) {
13054
+ if (seenTxids.has(pendingTx.arkTxid)) continue;
13055
+ seenTxids.add(pendingTx.arkTxid);
13056
+ pendingTxs.push(pendingTx);
13057
+ }
13058
+ }
13059
+ const myPkScriptHex = hex.encode(myPkScript);
13060
+ for (const pendingTx of pendingTxs) {
13061
+ try {
13062
+ const checkpointTxs = pendingTx.signedCheckpointTxs.map(
13063
+ (checkpoint) => Transaction$2.fromPSBT(base64.decode(checkpoint))
13064
+ );
13065
+ const finalCheckpoints = await Promise.all(
13066
+ checkpointTxs.map(
13067
+ async (checkpoint) => base64.encode((await identity.sign(checkpoint)).toPSBT())
13068
+ )
13069
+ );
13070
+ await this.arkProvider.finalizeTx(pendingTx.arkTxid, finalCheckpoints);
13071
+ result.count++;
13072
+ const paidToMe = this.arkTxAmountPaidTo(pendingTx.finalArkTx, myPkScriptHex);
13073
+ if (paidToMe === 0) continue;
13074
+ result.swept += paidToMe;
13075
+ for (const checkpoint of checkpointTxs) {
13076
+ for (let i = 0; i < checkpoint.inputsLength; i++) {
13077
+ const input = checkpoint.getInput(i);
13078
+ if (!input.txid) continue;
13079
+ result.claimed.add(`${hex.encode(input.txid)}:${input.index}`);
13080
+ }
13081
+ }
13082
+ } catch (error) {
13083
+ console.error(
13084
+ `Failed to finalize pending arkadeCash tx ${pendingTx.arkTxid}:`,
13085
+ error
13086
+ );
13087
+ }
13088
+ }
13089
+ return result;
13090
+ }
13091
+ /** Sum the outputs of an ark tx PSBT that pay the given scriptPubKey. */
13092
+ arkTxAmountPaidTo(arkTxPsbt, pkScriptHex) {
13093
+ const arkTx = Transaction$2.fromPSBT(base64.decode(arkTxPsbt));
13094
+ let total = 0;
13095
+ for (let i = 0; i < arkTx.outputsLength; i++) {
13096
+ const output = arkTx.getOutput(i);
13097
+ if (!output.script || output.amount === void 0) continue;
13098
+ if (hex.encode(output.script) === pkScriptHex) total += Number(output.amount);
13099
+ }
13100
+ return total;
13101
+ }
12699
13102
  /**
12700
13103
  * Send BTC and/or assets to one or more recipients.
12701
13104
  *
@@ -12924,7 +13327,7 @@ var Wallet2 = class _Wallet extends ReadonlyWallet {
12924
13327
  * there is no separate change output. It records no `TxSent` history — the
12925
13328
  * funds never leave the wallet.
12926
13329
  */
12927
- async sendSelectedVtxosToSelf(inputs) {
13330
+ async sendSelectedVtxosToSelf(inputs, now) {
12928
13331
  if (inputs.length === 0) {
12929
13332
  throw new Error("sendSelectedVtxosToSelf: no inputs");
12930
13333
  }
@@ -12933,26 +13336,28 @@ var Wallet2 = class _Wallet extends ReadonlyWallet {
12933
13336
  const serverPubKey = this.arkServerPublicKey;
12934
13337
  const serverUnrollScript = this.serverUnrollScript;
12935
13338
  const arkAddress = offchainTapscript.address(this.network.hrp, serverPubKey);
12936
- for (const input of inputs) {
12937
- if (!isSpendable(input) || isRecoverable(input)) {
13339
+ const normalizedInputs = inputs.map(normalizeVtxo);
13340
+ const at = now ?? await resolveTimeHeight(this.onchainProvider);
13341
+ for (const input of normalizedInputs) {
13342
+ if (!canSpendOffchain(input, at)) {
12938
13343
  throw new Error(
12939
13344
  `sendSelectedVtxosToSelf: input ${input.txid}:${input.vout} is not cooperatively spendable`
12940
13345
  );
12941
13346
  }
12942
- if (!input.virtualStatus.batchExpiry) {
13347
+ if (input.expiresAt === void 0 && input.expiresAtHeight === void 0) {
12943
13348
  throw new Error(
12944
- `sendSelectedVtxosToSelf: input ${input.txid}:${input.vout} has no batchExpiry`
13349
+ `sendSelectedVtxosToSelf: input ${input.txid}:${input.vout} has no batch expiry`
12945
13350
  );
12946
13351
  }
12947
13352
  }
12948
- const total = inputs.reduce((sum, c) => sum + BigInt(c.value), 0n);
13353
+ const total = normalizedInputs.reduce((sum, c) => sum + BigInt(c.value), 0n);
12949
13354
  const outputs = [
12950
13355
  {
12951
13356
  script: total < this.dustAmount ? arkAddress.subdustPkScript : arkAddress.pkScript,
12952
13357
  amount: total
12953
13358
  }
12954
13359
  ];
12955
- const assetInputs = selectedCoinsToAssetInputs(inputs);
13360
+ const assetInputs = selectedCoinsToAssetInputs(normalizedInputs);
12956
13361
  let selfAssets;
12957
13362
  if (assetInputs.size > 0) {
12958
13363
  const totals = /* @__PURE__ */ new Map();
@@ -12969,7 +13374,7 @@ var Wallet2 = class _Wallet extends ReadonlyWallet {
12969
13374
  const packet = createAssetPacket(assetInputs, [], selfReceiver);
12970
13375
  outputs.push(Extension.create([packet]).txOut());
12971
13376
  }
12972
- return this._submitOffchainSpend(inputs, outputs, {
13377
+ return this._submitOffchainSpend(normalizedInputs, outputs, {
12973
13378
  sentAmount: 0,
12974
13379
  changeAmount: total,
12975
13380
  changeVout: 0,
@@ -13070,43 +13475,42 @@ var Wallet2 = class _Wallet extends ReadonlyWallet {
13070
13475
  const cm = await this.getContractManager();
13071
13476
  const annotatedInputs = await cm.annotateVtxos(inputs);
13072
13477
  for (const [inputIndex, vtxo] of annotatedInputs.entries()) {
13478
+ const spentFacts = { ...vtxo, isSpent: true };
13073
13479
  if (inputIndex < safeLength && signedCheckpointTxs[inputIndex]) {
13074
13480
  const checkpoint = Transaction$2.fromPSBT(
13075
13481
  base64.decode(signedCheckpointTxs[inputIndex])
13076
13482
  );
13077
13483
  spentVtxos.push({
13078
- ...vtxo,
13079
- virtualStatus: {
13080
- ...vtxo.virtualStatus,
13081
- state: "spent"
13082
- },
13484
+ ...spentFacts,
13485
+ virtualStatus: toVirtualStatus(spentFacts),
13083
13486
  spentBy: checkpoint.id,
13084
- arkTxId: arkTxid,
13085
- isSpent: true
13487
+ arkTxId: arkTxid
13086
13488
  });
13087
13489
  } else {
13088
13490
  spentVtxos.push({
13089
- ...vtxo,
13090
- virtualStatus: {
13091
- ...vtxo.virtualStatus,
13092
- state: "spent"
13093
- },
13094
- arkTxId: arkTxid,
13095
- isSpent: true
13491
+ ...spentFacts,
13492
+ virtualStatus: toVirtualStatus(spentFacts),
13493
+ arkTxId: arkTxid
13096
13494
  });
13097
13495
  }
13098
- if (vtxo.virtualStatus.commitmentTxIds) {
13099
- for (const id of vtxo.virtualStatus.commitmentTxIds) {
13100
- commitmentTxIds.add(id);
13101
- }
13496
+ for (const id of vtxo.commitmentTxIds) {
13497
+ commitmentTxIds.add(id);
13102
13498
  }
13103
- if (vtxo.virtualStatus.batchExpiry) {
13104
- batchExpiry = Math.min(batchExpiry, vtxo.virtualStatus.batchExpiry);
13499
+ const vtxoExpiry = toBatchExpiry(vtxo);
13500
+ if (vtxoExpiry) {
13501
+ batchExpiry = Math.min(batchExpiry, vtxoExpiry);
13105
13502
  }
13106
13503
  }
13107
13504
  const createdAt = Date.now();
13108
13505
  let changeVtxo;
13109
13506
  if (changeAmount > 0n && batchExpiry !== Number.MAX_SAFE_INTEGER) {
13507
+ const changeFacts = {
13508
+ isSpent: false,
13509
+ isSwept: false,
13510
+ isPreconfirmed: true,
13511
+ commitmentTxIds: Array.from(commitmentTxIds),
13512
+ ...parseLegacyExpiry(batchExpiry)
13513
+ };
13110
13514
  changeVtxo = {
13111
13515
  txid: arkTxid,
13112
13516
  vout: changeVout,
@@ -13114,14 +13518,11 @@ var Wallet2 = class _Wallet extends ReadonlyWallet {
13114
13518
  forfeitTapLeafScript: offchainTapscript.forfeit(),
13115
13519
  intentTapLeafScript: offchainTapscript.forfeit(),
13116
13520
  isUnrolled: false,
13117
- isSpent: false,
13118
13521
  tapTree: offchainTapscript.encode(),
13119
13522
  value: Number(changeAmount),
13120
- virtualStatus: {
13121
- state: "preconfirmed",
13122
- commitmentTxIds: Array.from(commitmentTxIds),
13123
- batchExpiry
13124
- },
13523
+ ...changeFacts,
13524
+ virtualStatus: toVirtualStatus(changeFacts),
13525
+ spentBy: "",
13125
13526
  status: {
13126
13527
  confirmed: false
13127
13528
  },
@@ -13189,25 +13590,25 @@ var Wallet2 = class _Wallet extends ReadonlyWallet {
13189
13590
  const spentVtxos = [];
13190
13591
  const inputArkTxIds = /* @__PURE__ */ new Set();
13191
13592
  const boardingRemovalsByAddress = /* @__PURE__ */ new Map();
13192
- const isVtxo = (input) => "virtualStatus" in input;
13193
- const vtxoInputs = inputs.filter(isVtxo);
13593
+ const vtxoInputs = inputs.filter(isVirtualCoin);
13194
13594
  const cm = await this.getContractManager();
13195
13595
  const annotatedVtxos = await cm.annotateVtxos(vtxoInputs);
13196
13596
  const annotatedByKey = new Map(annotatedVtxos.map((v) => [`${v.txid}:${v.vout}`, v]));
13197
13597
  for (const input of inputs) {
13198
- if (isVtxo(input)) {
13199
- const vtxo = annotatedByKey.get(`${input.txid}:${input.vout}`);
13598
+ if (isVirtualCoin(input)) {
13599
+ const outpoint = `${input.txid}:${input.vout}`;
13600
+ const vtxo = annotatedByKey.get(outpoint);
13601
+ if (!vtxo) {
13602
+ throw new Error(`missing annotation for virtual coin ${outpoint}`);
13603
+ }
13200
13604
  if (vtxo.arkTxId) {
13201
13605
  inputArkTxIds.add(vtxo.arkTxId);
13202
13606
  }
13607
+ const settledFacts = { ...vtxo, isSpent: true };
13203
13608
  spentVtxos.push({
13204
- ...vtxo,
13205
- virtualStatus: {
13206
- ...vtxo.virtualStatus,
13207
- state: "settled"
13208
- },
13209
- settledBy: commitmentTxid,
13210
- isSpent: true
13609
+ ...settledFacts,
13610
+ virtualStatus: toVirtualStatus(settledFacts),
13611
+ settledBy: commitmentTxid
13211
13612
  });
13212
13613
  } else {
13213
13614
  let sourceAddress;
@@ -13270,9 +13671,9 @@ var Wallet2 = class _Wallet extends ReadonlyWallet {
13270
13671
  }
13271
13672
  };
13272
13673
  function selectVirtualCoins(coins, targetAmount) {
13273
- const sortedCoins = [...coins].sort((a, b) => {
13274
- const expiryA = a.virtualStatus.batchExpiry || Number.MAX_SAFE_INTEGER;
13275
- const expiryB = b.virtualStatus.batchExpiry || Number.MAX_SAFE_INTEGER;
13674
+ const sortedCoins = coins.map(normalizeVtxo).sort((a, b) => {
13675
+ const expiryA = toBatchExpiry(a) || Number.MAX_SAFE_INTEGER;
13676
+ const expiryB = toBatchExpiry(b) || Number.MAX_SAFE_INTEGER;
13276
13677
  if (expiryA !== expiryB) {
13277
13678
  return expiryA - expiryB;
13278
13679
  }
@@ -13966,13 +14367,7 @@ var Ramps = class {
13966
14367
  const filteredVtxos = [];
13967
14368
  let totalAmount = 0n;
13968
14369
  for (const vtxo of vtxos) {
13969
- const inputFee = estimator.evalOffchainInput({
13970
- amount: BigInt(vtxo.value),
13971
- type: vtxo.virtualStatus.state === "swept" ? "recoverable" : "vtxo",
13972
- weight: 0,
13973
- birth: vtxo.createdAt,
13974
- expiry: vtxo.virtualStatus.batchExpiry ? new Date(vtxo.virtualStatus.batchExpiry) : void 0
13975
- });
14370
+ const inputFee = estimator.evalOffchainInput(toOffchainInputFeeParams(vtxo));
13976
14371
  if (inputFee.satoshis >= vtxo.value) {
13977
14372
  continue;
13978
14373
  }
@@ -14707,30 +15102,30 @@ var WalletMessageHandler = class {
14707
15102
  unconfirmed += utxo.value;
14708
15103
  }
14709
15104
  }
14710
- const spendableVtxos = allVtxos.filter(isSpendable);
14711
- const sweptVtxos = allVtxos.filter((vtxo) => vtxo.virtualStatus.state === "swept");
15105
+ const now = { timestamp: /* @__PURE__ */ new Date() };
14712
15106
  let settled = 0;
14713
15107
  let preconfirmed = 0;
14714
15108
  let recoverable = 0;
14715
15109
  let pendingRecovery = 0;
14716
- for (const vtxo of spendableVtxos) {
15110
+ for (const vtxo of allVtxos) {
15111
+ if (hasTerminalSpend(vtxo)) continue;
14717
15112
  if (pendingOutpoints.has(`${vtxo.txid}:${vtxo.vout}`)) {
14718
15113
  pendingRecovery += vtxo.value;
14719
- } else if (vtxo.virtualStatus.state === "settled") {
14720
- settled += vtxo.value;
14721
- } else if (vtxo.virtualStatus.state === "preconfirmed") {
14722
- preconfirmed += vtxo.value;
14723
- }
14724
- }
14725
- for (const vtxo of sweptVtxos) {
14726
- if (isSpendable(vtxo)) {
15114
+ } else if (canRecoverOnchain(vtxo, now)) {
14727
15115
  recoverable += vtxo.value;
15116
+ } else if (canSpendOffchain(vtxo, now)) {
15117
+ if (vtxo.isPreconfirmed) {
15118
+ preconfirmed += vtxo.value;
15119
+ } else {
15120
+ settled += vtxo.value;
15121
+ }
14728
15122
  }
14729
15123
  }
14730
15124
  const totalBoarding = confirmed + unconfirmed;
14731
15125
  const totalOffchain = settled + preconfirmed + recoverable + pendingRecovery;
14732
15126
  const assetBalances = /* @__PURE__ */ new Map();
14733
- for (const vtxo of spendableVtxos) {
15127
+ for (const vtxo of allVtxos) {
15128
+ if (hasTerminalSpend(vtxo)) continue;
14734
15129
  if (vtxo.assets) {
14735
15130
  for (const a of vtxo.assets) {
14736
15131
  const current = assetBalances.get(a.assetId) ?? 0n;
@@ -14766,7 +15161,7 @@ var WalletMessageHandler = class {
14766
15161
  */
14767
15162
  async getSpendableVtxos() {
14768
15163
  const vtxos = await this.getVtxosFromRepo();
14769
- return vtxos.filter(isSpendable);
15164
+ return vtxos.filter((v) => !hasTerminalSpend(v));
14770
15165
  }
14771
15166
  async onWalletInitialized() {
14772
15167
  if (!this.readonlyWallet || !this.arkProvider || !this.indexerProvider || !this.walletRepository) {
@@ -14778,9 +15173,7 @@ var WalletMessageHandler = class {
14778
15173
  try {
14779
15174
  const vtxos = await this.getVtxosFromRepo();
14780
15175
  const { pending, finalized } = await this.wallet.finalizePendingTxs(
14781
- vtxos.filter(
14782
- (vtxo) => vtxo.virtualStatus.state !== "swept" && vtxo.virtualStatus.state !== "settled"
14783
- )
15176
+ vtxos.filter((vtxo) => vtxo.isSpent || vtxo.isPreconfirmed && !vtxo.isSwept)
14784
15177
  );
14785
15178
  console.info(
14786
15179
  `Recovered ${finalized.length}/${pending.length} pending transactions: ${finalized.join(", ")}`
@@ -15039,8 +15432,12 @@ var WalletMessageHandler = class {
15039
15432
  `WalletMessageHandler.getVtxosFromRepo: failed to derive script from wallet address ${walletAddress}: ${e instanceof Error ? e.message : String(e)}`
15040
15433
  );
15041
15434
  }
15042
- const walletVtxos = await this.walletRepository.getVtxos(walletAddress);
15043
- addVtxos(filterVtxosForScript(walletVtxos, walletScript));
15435
+ addVtxos(
15436
+ await getVtxosForContract(this.walletRepository, {
15437
+ script: walletScript,
15438
+ address: walletAddress
15439
+ })
15440
+ );
15044
15441
  return allVtxos;
15045
15442
  }
15046
15443
  /**
@@ -15072,7 +15469,7 @@ var WalletMessageHandler = class {
15072
15469
  }));
15073
15470
  const BATCH_SIZE = 100;
15074
15471
  for (let i = 0; i < outpoints.length; i += BATCH_SIZE) {
15075
- const res = await this.indexerProvider.getVtxos({
15472
+ const res = await getNormalizedVtxos(this.indexerProvider, {
15076
15473
  outpoints: outpoints.slice(i, i + BATCH_SIZE)
15077
15474
  });
15078
15475
  for (const v of res.vtxos) {
@@ -15774,7 +16171,7 @@ var ServiceWorkerReadonlyWallet = class _ServiceWorkerReadonlyWallet {
15774
16171
  };
15775
16172
  try {
15776
16173
  const response = await this.sendMessage(message);
15777
- return response.payload.vtxos;
16174
+ return response.payload.vtxos.map(normalizeVtxo);
15778
16175
  } catch (error) {
15779
16176
  throw new Error(`Failed to get vtxos: ${error}`);
15780
16177
  }
@@ -15887,7 +16284,7 @@ var ServiceWorkerReadonlyWallet = class _ServiceWorkerReadonlyWallet {
15887
16284
  };
15888
16285
  try {
15889
16286
  const response = await sendContractMessage(message);
15890
- return response.payload.vtxos;
16287
+ return response.payload.vtxos.map(normalizeVtxo);
15891
16288
  } catch (e) {
15892
16289
  throw new Error("Failed to annotate vtxos");
15893
16290
  }
@@ -16332,7 +16729,7 @@ var ServiceWorkerWallet = class _ServiceWorkerWallet extends ServiceWorkerReadon
16332
16729
  };
16333
16730
  try {
16334
16731
  const response = await wallet.sendMessage(message);
16335
- return response.payload.vtxos;
16732
+ return response.payload.vtxos.map(normalizeVtxo);
16336
16733
  } catch (e) {
16337
16734
  throw new Error(`Failed to get expiring vtxos: ${e}`);
16338
16735
  }
@@ -18072,7 +18469,7 @@ async function resolveFeeRate(opts) {
18072
18469
  }
18073
18470
  async function selectExitVtxos(opts) {
18074
18471
  if (!opts.vtxos) return opts.wallet.getVtxos();
18075
- const res = await opts.wallet.indexerProvider.getVtxos({ outpoints: opts.vtxos });
18472
+ const res = await getNormalizedVtxos(opts.wallet.indexerProvider, { outpoints: opts.vtxos });
18076
18473
  const tapTrees = /* @__PURE__ */ new Map();
18077
18474
  const out = [];
18078
18475
  for (const vtxo of res.vtxos) {
@@ -18741,7 +19138,7 @@ __export(arkade_exports, {
18741
19138
  validateProgram: () => validateProgram,
18742
19139
  witnessRefToBytes: () => witnessRefToBytes
18743
19140
  });
18744
- var isVtxoCoin = (input) => "virtualStatus" in input;
19141
+ var isVtxoCoin = (input) => isVirtualCoin(input);
18745
19142
  function createArkadeBatchHandler(intentId, inputs, signer, signedProof, message, session, arkProvider, emulator, network, recipients) {
18746
19143
  let batchId;
18747
19144
  let sweepTapTreeRoot;
@@ -18818,7 +19215,11 @@ function createArkadeBatchHandler(intentId, inputs, signer, signedProof, message
18818
19215
  break;
18819
19216
  }
18820
19217
  }
18821
- if (boardingIdx === null) continue;
19218
+ if (boardingIdx === null) {
19219
+ throw new Error(
19220
+ `input ${input.txid}:${input.vout} is neither a virtual output nor an input of the commitment tx; a virtual output must carry its \`script\``
19221
+ );
19222
+ }
18822
19223
  commitmentPsbt.updateInput(boardingIdx, {
18823
19224
  tapLeafScript: [input.forfeitTapLeafScript]
18824
19225
  });
@@ -19098,13 +19499,13 @@ var ArkadeContract = class _ArkadeContract {
19098
19499
  const [registered] = await manager.getContracts({ script: scriptHex });
19099
19500
  if (registered) {
19100
19501
  const [withVtxos] = await manager.getContractsWithVtxos({ script: scriptHex });
19101
- return (withVtxos?.vtxos ?? []).filter(isSpendable);
19502
+ return (withVtxos?.vtxos ?? []).filter((v) => !hasTerminalSpend(v));
19102
19503
  }
19103
19504
  }
19104
19505
  if (!this.client.indexer) {
19105
19506
  throw new Error("ArkadeContract.getUtxos: an indexer is required");
19106
19507
  }
19107
- const { vtxos } = await this.client.indexer.getVtxos({
19508
+ const { vtxos } = await getNormalizedVtxos(this.client.indexer, {
19108
19509
  scripts: [scriptHex],
19109
19510
  spendableOnly: true
19110
19511
  });
@@ -19412,6 +19813,6 @@ function isArkContract(str) {
19412
19813
  return str.startsWith(ARKCONTRACT_PREFIX + "=");
19413
19814
  }
19414
19815
 
19415
- export { ARKADE_MAGIC, ActivityRegistry, ArkNote, AssetManager, BIP322, Batch, ChainedTxType, ContractManager, ContractRepositoryImpl, ContractWatcher, DB_VERSION, DEFAULT_MESSAGE_TIMEOUTS, DelegateManagerImpl, DelegateNotConfiguredError, DelegatorManagerImpl, DelegatorNotConfiguredError, DescriptorSigningProviderMissingError, DustChangeError, ELECTRUM_TCP_HOST, ELECTRUM_WS_URL, ESPLORA_URL, ElectrumOnchainProvider, EmulatorPacket, EsploraProvider, Estimator, Extension, ExtensionNotFoundError, HDDescriptorProvider, INTENT_TERMINAL_STATES, InMemoryContractRepository, InMemoryIntentRepository, InMemoryVirtualTxRepository, InMemoryWalletRepository, IndexedDBContractRepository, IndexedDBIntentRepository, IndexedDBVirtualTxRepository, IndexedDBWalletRepository, MESSAGE_BUS_INITIALIZING, MESSAGE_BUS_NOT_INITIALIZED, MIGRATION_KEY, MessageBus, MessageBusInitializingError, MessageBusNotInitializedError, MissingSigningDescriptorError, MnemonicIdentity, OnchainWallet, P2A, Ramps, ReadonlyAssetManager, ReadonlyDescriptorIdentity, ReadonlySingleKey, ReadonlyWallet, ReadonlyWalletError, RestDelegateProvider, RestDelegatorProvider, RestEmulatorProvider, SeedIdentity, ServiceWorkerReadonlyWallet, ServiceWorkerTimeoutError, ServiceWorkerWallet, SingleKey, TxTree, TxType, TxWeightEstimator, UnilateralExit, UnknownPacket, Unroll, VtxoManager, Wallet2 as Wallet, WalletMessageHandler, WalletNotInitializedError, WalletRepositoryImpl, WsElectrumChainSource, arkade_exports, assertIntentIdUnique, boardingResolver, buildForfeitTx, buildOffchainTx, classifyAgainstSignerSet, classifyContractSigner, closeDatabase, combineTapscriptSigs, contractFromArkContract, contractFromArkContractWithAddress, createAssetPacket, createDefaultActivityRegistry, createExitChainResolver, decodeArkContract, deserializeAssets, deserializeExitPackage, deserializeUtxo, deserializeVtxo, encodeArkContract, extendVirtualCoinForContract, getMigrationStatus, getRandomId, hasBoardingTxExpired, intentMatchesFilter, intentPageBounds, isArkContract, isBatchSignable, isCooperativelyMigratable, isDiscoverable, isExpired, isRecoverable, isRetryableProviderError, isSpendable, isSubdust, isTerminalIntentState, isValidArkAddress, isVtxoExpiringSoon, isVtxoForScript, mergeChainedTxType, migrateWalletRepository, openDatabase, requiresMigration, rollbackMigration, saveVtxosForContract, scriptFromArkAddress, selectCoinsWithAsset, selectVirtualCoins, serializeAssets, serializeExitPackage, serializeUtxo, serializeVtxo, setupServiceWorker, signerSetFromInfo, toXOnlySignerHex, validateConnectorsTxGraph, validateVtxoTxGraph, verifyTapscriptSignatures, waitForIncomingFunds, warnAndFilterVtxosForScript };
19416
- //# sourceMappingURL=chunk-OCS5S3L3.js.map
19417
- //# sourceMappingURL=chunk-OCS5S3L3.js.map
19816
+ export { ARKADE_MAGIC, ActivityRegistry, ArkNote, ArkadeCash, ArkadeCashCreateError, AssetManager, BIP322, Batch, ChainedTxType, ContractManager, ContractRepositoryImpl, ContractWatcher, DB_VERSION, DEFAULT_MESSAGE_TIMEOUTS, DelegateManagerImpl, DelegateNotConfiguredError, DelegatorManagerImpl, DelegatorNotConfiguredError, DescriptorSigningProviderMissingError, DustChangeError, ELECTRUM_TCP_HOST, ELECTRUM_WS_URL, ESPLORA_URL, ElectrumOnchainProvider, EmulatorPacket, EsploraProvider, Estimator, Extension, ExtensionNotFoundError, HDDescriptorProvider, INTENT_TERMINAL_STATES, InMemoryContractRepository, InMemoryIntentRepository, InMemoryVirtualTxRepository, InMemoryWalletRepository, IndexedDBContractRepository, IndexedDBIntentRepository, IndexedDBVirtualTxRepository, IndexedDBWalletRepository, MESSAGE_BUS_INITIALIZING, MESSAGE_BUS_NOT_INITIALIZED, MIGRATION_KEY, MessageBus, MessageBusInitializingError, MessageBusNotInitializedError, MissingSigningDescriptorError, MnemonicIdentity, OnchainWallet, P2A, Ramps, ReadonlyAssetManager, ReadonlyDescriptorIdentity, ReadonlySingleKey, ReadonlyWallet, ReadonlyWalletError, RestDelegateProvider, RestDelegatorProvider, RestEmulatorProvider, SeedIdentity, ServiceWorkerReadonlyWallet, ServiceWorkerTimeoutError, ServiceWorkerWallet, SingleKey, TxTree, TxType, TxWeightEstimator, UnilateralExit, UnknownPacket, Unroll, VtxoManager, Wallet2 as Wallet, WalletMessageHandler, WalletNotInitializedError, WalletRepositoryImpl, WsElectrumChainSource, arkade_exports, assertIntentIdUnique, boardingResolver, buildForfeitTx, buildOffchainTx, classifyAgainstSignerSet, classifyContractSigner, closeDatabase, combineTapscriptSigs, contractFromArkContract, contractFromArkContractWithAddress, createAssetPacket, createDefaultActivityRegistry, createExitChainResolver, decodeArkContract, deserializeAssets, deserializeExitPackage, deserializeUtxo, deserializeVtxo, encodeArkContract, extendVirtualCoinForContract, getMigrationStatus, getRandomId, hasBoardingTxExpired, intentMatchesFilter, intentPageBounds, isArkContract, isBatchSignable, isCooperativelyMigratable, isDiscoverable, isRetryableProviderError, isSubdust, isTerminalIntentState, isValidArkAddress, isVtxoExpiringSoon, isVtxoForScript, mergeChainedTxType, migrateWalletRepository, openDatabase, requiresMigration, rollbackMigration, saveVtxosForContract, scriptFromArkAddress, selectCoinsWithAsset, selectVirtualCoins, serializeAssets, serializeExitPackage, serializeUtxo, serializeVtxo, setupServiceWorker, signerSetFromInfo, toXOnlySignerHex, validateConnectorsTxGraph, validateVtxoTxGraph, verifyTapscriptSignatures, waitForIncomingFunds, warnAndFilterVtxosForScript };
19817
+ //# sourceMappingURL=chunk-NYR5H6GR.js.map
19818
+ //# sourceMappingURL=chunk-NYR5H6GR.js.map