@arkade-os/sdk 0.4.34 → 0.4.35

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 (69) hide show
  1. package/dist/adapters/expo.cjs +4 -4
  2. package/dist/adapters/expo.d.cts +2 -2
  3. package/dist/adapters/expo.d.ts +2 -2
  4. package/dist/adapters/expo.js +2 -2
  5. package/dist/adapters/indexedDB.cjs +3 -3
  6. package/dist/adapters/indexedDB.js +2 -2
  7. package/dist/{ark-Dsv5Jq4E.d.cts → ark-D6sau_6-.d.cts} +458 -3
  8. package/dist/{ark-Dsv5Jq4E.d.ts → ark-D6sau_6-.d.ts} +458 -3
  9. package/dist/{asyncStorageTaskQueue-D92ch8yI.d.cts → asyncStorageTaskQueue-CpC027t_.d.cts} +2 -2
  10. package/dist/{asyncStorageTaskQueue-BH-zuth5.d.ts → asyncStorageTaskQueue-GT8fmPUG.d.ts} +2 -2
  11. package/dist/{chunk-FXFBPXV3.js → chunk-3JR77WQ4.js} +139 -41
  12. package/dist/chunk-3JR77WQ4.js.map +1 -0
  13. package/dist/{chunk-ZS3OZHC7.cjs → chunk-FM7T7JVL.cjs} +7 -7
  14. package/dist/{chunk-ZS3OZHC7.cjs.map → chunk-FM7T7JVL.cjs.map} +1 -1
  15. package/dist/{chunk-XCHBQVMK.cjs → chunk-H2LX2KKY.cjs} +1540 -265
  16. package/dist/chunk-H2LX2KKY.cjs.map +1 -0
  17. package/dist/{chunk-HFXEUW55.js → chunk-NOR7XOKN.js} +1472 -202
  18. package/dist/chunk-NOR7XOKN.js.map +1 -0
  19. package/dist/{chunk-VVGD3JIP.js → chunk-OURFR4UR.js} +3 -3
  20. package/dist/{chunk-VVGD3JIP.js.map → chunk-OURFR4UR.js.map} +1 -1
  21. package/dist/{chunk-CCLNFHJ5.cjs → chunk-VYS3KGRI.cjs} +16 -10
  22. package/dist/chunk-VYS3KGRI.cjs.map +1 -0
  23. package/dist/{chunk-FSAXPBGP.cjs → chunk-X2EQLK4O.cjs} +143 -40
  24. package/dist/chunk-X2EQLK4O.cjs.map +1 -0
  25. package/dist/{chunk-5WDBHWX3.js → chunk-XQS2HW4Q.js} +10 -4
  26. package/dist/chunk-XQS2HW4Q.js.map +1 -0
  27. package/dist/contracts/handlers/index.d.cts +3 -3
  28. package/dist/contracts/handlers/index.d.ts +3 -3
  29. package/dist/{delegate-BaS5SCIW.d.cts → delegate-C-L6gSZx.d.cts} +1 -1
  30. package/dist/{delegate-Baz_hb83.d.ts → delegate-De5__fpZ.d.ts} +1 -1
  31. package/dist/{index-FwXZveaX.d.ts → index-BETdjE_o.d.ts} +2 -2
  32. package/dist/{index-lNZ6qaO3.d.cts → index-jwQfHP6D.d.cts} +2 -2
  33. package/dist/index.cjs +129 -105
  34. package/dist/index.d.cts +69 -9
  35. package/dist/index.d.ts +69 -9
  36. package/dist/index.js +2 -2
  37. package/dist/repositories/realm/index.cjs +12 -12
  38. package/dist/repositories/realm/index.cjs.map +1 -1
  39. package/dist/repositories/realm/index.d.cts +2 -2
  40. package/dist/repositories/realm/index.d.ts +2 -2
  41. package/dist/repositories/realm/index.js +3 -3
  42. package/dist/repositories/realm/index.js.map +1 -1
  43. package/dist/repositories/sqlite/index.cjs +11 -11
  44. package/dist/repositories/sqlite/index.d.cts +1 -1
  45. package/dist/repositories/sqlite/index.d.ts +1 -1
  46. package/dist/repositories/sqlite/index.js +2 -2
  47. package/dist/{taskRunner-vFRA3F9b.d.cts → taskRunner-DCyp6Gea.d.cts} +2 -2
  48. package/dist/{taskRunner-B1NUWyWR.d.ts → taskRunner-DnxtObeq.d.ts} +2 -2
  49. package/dist/wallet/expo/background.cjs +12 -12
  50. package/dist/wallet/expo/background.d.cts +3 -3
  51. package/dist/wallet/expo/background.d.ts +3 -3
  52. package/dist/wallet/expo/background.js +4 -4
  53. package/dist/wallet/expo/index.cjs +11 -11
  54. package/dist/wallet/expo/index.d.cts +4 -4
  55. package/dist/wallet/expo/index.d.ts +4 -4
  56. package/dist/wallet/expo/index.js +3 -3
  57. package/dist/{wallet-D6uoBLmS.d.ts → wallet-BWHbd5b1.d.cts} +231 -8
  58. package/dist/{wallet-By9HIo0Q.d.cts → wallet-Bth5uucA.d.ts} +231 -8
  59. package/dist/worker/expo/index.cjs +7 -7
  60. package/dist/worker/expo/index.d.cts +4 -4
  61. package/dist/worker/expo/index.d.ts +4 -4
  62. package/dist/worker/expo/index.js +3 -3
  63. package/package.json +2 -2
  64. package/dist/chunk-5WDBHWX3.js.map +0 -1
  65. package/dist/chunk-CCLNFHJ5.cjs.map +0 -1
  66. package/dist/chunk-FSAXPBGP.cjs.map +0 -1
  67. package/dist/chunk-FXFBPXV3.js.map +0 -1
  68. package/dist/chunk-HFXEUW55.js.map +0 -1
  69. package/dist/chunk-XCHBQVMK.cjs.map +0 -1
@@ -1,4 +1,4 @@
1
- import { craftToSpendTx, Transaction, OP_RETURN_EMPTY_PKSCRIPT, Intent, getArkPsbtFields, CosignerPublicKey, setArkPsbtField, VtxoTaprootTree, maybeArkError, AssetRef, AssetId, AssetOutput, AssetGroup, AssetInput, Packet, Metadata, isEventSourceError, RestArkProvider, RestIndexerProvider, ArkError, BufferReader } from './chunk-FXFBPXV3.js';
1
+ import { craftToSpendTx, Transaction, OP_RETURN_EMPTY_PKSCRIPT, baseFetch, Intent, getArkPsbtFields, CosignerPublicKey, setArkPsbtField, VtxoTaprootTree, maybeArkError, AssetRef, AssetId, AssetOutput, AssetGroup, AssetInput, Packet, Metadata, isEventSourceError, RestArkProvider, RestIndexerProvider, ArkError, BufferReader } from './chunk-3JR77WQ4.js';
2
2
  import { isMainnetDescriptor, descriptorIsOurs, contractHandlers, DEFAULT_PAGE_SIZE, DelegateVtxo, WALLET_RECEIVE_SOURCE, deriveDescriptorLeafPubKey, DefaultVtxo, BoardingContractHandler } from './chunk-CUSABEUQ.js';
3
3
  import { VtxoScript, timelockToSequence, DEFAULT_NETWORK, DEFAULT_NETWORK_NAME, decodeTapscript, scriptFromTapLeafScript, CLTVMultisigTapscript, ArkAddress, CSVMultisigTapscript, getSequence, getNetwork, MultisigTapscript, networks as networks$1, DEFAULT_ARKADE_SERVER_URL } from './chunk-OUVTG72A.js';
4
4
  import { sha256, hash160, sha256x2, concatBytes, randomPrivateKeyBytes, pubECDSA, pubSchnorr, equalBytes as equalBytes$1 } from '@scure/btc-signer/utils.js';
@@ -958,7 +958,7 @@ var RestDelegateProvider = class {
958
958
  */
959
959
  async delegate(intent, forfeitTxs, options) {
960
960
  const url = `${this.url}/v1/delegate`;
961
- const response = await fetch(url, {
961
+ const response = await baseFetch(url, {
962
962
  method: "POST",
963
963
  headers: {
964
964
  "Content-Type": "application/json"
@@ -973,8 +973,8 @@ var RestDelegateProvider = class {
973
973
  })
974
974
  });
975
975
  if (!response.ok) {
976
- const errorText = await response.text();
977
- throw new Error(`Failed to delegate: ${errorText}`);
976
+ const errorText2 = await response.text();
977
+ throw new Error(`Failed to delegate: ${errorText2}`);
978
978
  }
979
979
  }
980
980
  /**
@@ -985,10 +985,10 @@ var RestDelegateProvider = class {
985
985
  */
986
986
  async getDelegateInfo() {
987
987
  const url = `${this.url}/v1/delegator/info`;
988
- const response = await fetch(url);
988
+ const response = await baseFetch(url);
989
989
  if (!response.ok) {
990
- const errorText = await response.text();
991
- throw new Error(`Failed to get delegate info: ${errorText}`);
990
+ const errorText2 = await response.text();
991
+ throw new Error(`Failed to get delegate info: ${errorText2}`);
992
992
  }
993
993
  const data = await response.json();
994
994
  if (!isDelegateInfo(data)) {
@@ -1020,14 +1020,14 @@ var EsploraProvider = class {
1020
1020
  pollingInterval;
1021
1021
  forcePolling;
1022
1022
  async getCoins(address) {
1023
- const response = await fetch(`${this.baseUrl}/address/${address}/utxo`);
1023
+ const response = await baseFetch(`${this.baseUrl}/address/${address}/utxo`);
1024
1024
  if (!response.ok) {
1025
1025
  throw new Error(`Failed to fetch UTXOs: ${response.statusText}`);
1026
1026
  }
1027
1027
  return response.json();
1028
1028
  }
1029
1029
  async getFeeRate() {
1030
- const response = await fetch(`${this.baseUrl}/fee-estimates`);
1030
+ const response = await baseFetch(`${this.baseUrl}/fee-estimates`);
1031
1031
  if (response.status === 404) {
1032
1032
  return void 0;
1033
1033
  }
@@ -1048,7 +1048,7 @@ var EsploraProvider = class {
1048
1048
  }
1049
1049
  }
1050
1050
  async getTxOutspends(txid) {
1051
- const response = await fetch(`${this.baseUrl}/tx/${txid}/outspends`);
1051
+ const response = await baseFetch(`${this.baseUrl}/tx/${txid}/outspends`);
1052
1052
  if (!response.ok) {
1053
1053
  const error = await response.text();
1054
1054
  throw new Error(`Failed to get transaction outspends: ${error}`);
@@ -1056,7 +1056,7 @@ var EsploraProvider = class {
1056
1056
  return response.json();
1057
1057
  }
1058
1058
  async getTransactions(address) {
1059
- const response = await fetch(`${this.baseUrl}/address/${address}/txs`);
1059
+ const response = await baseFetch(`${this.baseUrl}/address/${address}/txs`);
1060
1060
  if (!response.ok) {
1061
1061
  const error = await response.text();
1062
1062
  throw new Error(`Failed to get transactions: ${error}`);
@@ -1064,7 +1064,7 @@ var EsploraProvider = class {
1064
1064
  return response.json();
1065
1065
  }
1066
1066
  async getTxStatus(txid) {
1067
- const txresponse = await fetch(`${this.baseUrl}/tx/${txid}`);
1067
+ const txresponse = await baseFetch(`${this.baseUrl}/tx/${txid}`);
1068
1068
  if (!txresponse.ok) {
1069
1069
  throw new Error(txresponse.statusText);
1070
1070
  }
@@ -1072,7 +1072,7 @@ var EsploraProvider = class {
1072
1072
  if (!tx.status.confirmed) {
1073
1073
  return { confirmed: false };
1074
1074
  }
1075
- const response = await fetch(`${this.baseUrl}/tx/${txid}/status`);
1075
+ const response = await baseFetch(`${this.baseUrl}/tx/${txid}/status`);
1076
1076
  if (!response.ok) {
1077
1077
  throw new Error(`Failed to get transaction status: ${response.statusText}`);
1078
1078
  }
@@ -1156,7 +1156,7 @@ var EsploraProvider = class {
1156
1156
  return stopFunc;
1157
1157
  }
1158
1158
  async getChainTip() {
1159
- const tipBlocks = await fetch(`${this.baseUrl}/blocks`);
1159
+ const tipBlocks = await baseFetch(`${this.baseUrl}/blocks`);
1160
1160
  if (!tipBlocks.ok) {
1161
1161
  throw new Error(`Failed to get chain tip: ${tipBlocks.statusText}`);
1162
1162
  }
@@ -1175,7 +1175,7 @@ var EsploraProvider = class {
1175
1175
  };
1176
1176
  }
1177
1177
  async broadcastPackage(parent, child) {
1178
- const response = await fetch(`${this.baseUrl}/txs/package`, {
1178
+ const response = await baseFetch(`${this.baseUrl}/txs/package`, {
1179
1179
  method: "POST",
1180
1180
  headers: {
1181
1181
  "Content-Type": "application/json"
@@ -1189,7 +1189,7 @@ var EsploraProvider = class {
1189
1189
  return response.json();
1190
1190
  }
1191
1191
  async broadcastTx(tx) {
1192
- const response = await fetch(`${this.baseUrl}/tx`, {
1192
+ const response = await baseFetch(`${this.baseUrl}/tx`, {
1193
1193
  method: "POST",
1194
1194
  headers: {
1195
1195
  "Content-Type": "text/plain"
@@ -1779,6 +1779,45 @@ function selectedCoinsToAssetInputs(selectedCoins) {
1779
1779
  }
1780
1780
  return assetInputs;
1781
1781
  }
1782
+ function toXOnlySignerHex(pubkeyHex) {
1783
+ const bytes = hex.decode(pubkeyHex);
1784
+ if (bytes.length === 33) return hex.encode(bytes.slice(1));
1785
+ if (bytes.length === 32) return hex.encode(bytes);
1786
+ throw new Error(`invalid signer pubkey length: expected 32 or 33 bytes, got ${bytes.length}`);
1787
+ }
1788
+ function signerSetFromInfo(info) {
1789
+ const active = toXOnlySignerHex(info.signerPubkey);
1790
+ const deprecated = /* @__PURE__ */ new Map();
1791
+ for (const signer of info.deprecatedSigners) {
1792
+ if (!signer.pubkey) continue;
1793
+ deprecated.set(toXOnlySignerHex(signer.pubkey), signer.cutoffDate);
1794
+ }
1795
+ return { active, deprecated };
1796
+ }
1797
+ function classifyAgainstSignerSet(contractServerPubKeyHex, signerSet, nowSeconds = Math.floor(Date.now() / 1e3)) {
1798
+ const signerPubKey = toXOnlySignerHex(contractServerPubKeyHex);
1799
+ if (signerPubKey === signerSet.active) {
1800
+ return { status: "CURRENT", signerPubKey };
1801
+ }
1802
+ if (!signerSet.deprecated.has(signerPubKey)) {
1803
+ return { status: "UNKNOWN_SIGNER", signerPubKey };
1804
+ }
1805
+ const cutoffDate = signerSet.deprecated.get(signerPubKey);
1806
+ if (cutoffDate === 0n) {
1807
+ return { status: "DUE_NOW", signerPubKey };
1808
+ }
1809
+ const secondsUntilCutoff = Number(cutoffDate) - nowSeconds;
1810
+ if (secondsUntilCutoff <= 0) {
1811
+ return { status: "EXPIRED", signerPubKey, cutoffDate, secondsUntilCutoff };
1812
+ }
1813
+ return { status: "MIGRATABLE", signerPubKey, cutoffDate, secondsUntilCutoff };
1814
+ }
1815
+ function classifyContractSigner(contractServerPubKeyHex, info, nowSeconds = Math.floor(Date.now() / 1e3)) {
1816
+ return classifyAgainstSignerSet(contractServerPubKeyHex, signerSetFromInfo(info), nowSeconds);
1817
+ }
1818
+ function isCooperativelyMigratable(status) {
1819
+ return status === "MIGRATABLE" || status === "DUE_NOW";
1820
+ }
1782
1821
  function buildOffchainTx(inputs, outputs, serverUnrollScript) {
1783
1822
  const MAX_OP_RETURN = 2;
1784
1823
  let countOpReturn = 0;
@@ -2359,6 +2398,20 @@ function validateRecipients(recipients, dustAmount) {
2359
2398
  }
2360
2399
 
2361
2400
  // src/wallet/vtxo-manager.ts
2401
+ function selectPendingRecoveryOutpoints(contractsWithVtxos, signerSet, nowSeconds = Math.floor(Date.now() / 1e3)) {
2402
+ const out = /* @__PURE__ */ new Set();
2403
+ for (const { contract, vtxos } of contractsWithVtxos) {
2404
+ const serverPubKey = contract.params.serverPubKey;
2405
+ if (!serverPubKey) continue;
2406
+ if (classifyAgainstSignerSet(serverPubKey, signerSet, nowSeconds).status !== "EXPIRED") {
2407
+ continue;
2408
+ }
2409
+ for (const v of vtxos) {
2410
+ if (isSpendable(v) && !isRecoverable(v)) out.add(`${v.txid}:${v.vout}`);
2411
+ }
2412
+ }
2413
+ return out;
2414
+ }
2362
2415
  function isSweepCapable(wallet) {
2363
2416
  return "boardingTapscript" in wallet && "onchainProvider" in wallet && "arkProvider" in wallet && "network" in wallet && "signOnchainBoardingTx" in wallet;
2364
2417
  }
@@ -2396,6 +2449,18 @@ function byExpiryAscending(vtxos) {
2396
2449
  };
2397
2450
  return [...vtxos].sort((a, b) => expiryKey(a) - expiryKey(b));
2398
2451
  }
2452
+ function capSettlementBatch(sorted, maxAmount) {
2453
+ const batch = [];
2454
+ let total = 0n;
2455
+ for (const vtxo of sorted) {
2456
+ if (batch.length >= MAX_VTXOS_PER_SETTLEMENT) break;
2457
+ const next = total + BigInt(vtxo.value);
2458
+ if (maxAmount >= 0n && next > maxAmount) continue;
2459
+ batch.push(vtxo);
2460
+ total = next;
2461
+ }
2462
+ return batch;
2463
+ }
2399
2464
  var DEFAULT_THRESHOLD_SECONDS = 259200;
2400
2465
  var DEFAULT_THRESHOLD_MS = DEFAULT_THRESHOLD_SECONDS * 1e3;
2401
2466
  var DEFAULT_RENEWAL_CONFIG = {
@@ -2405,7 +2470,8 @@ var DEFAULT_RENEWAL_CONFIG = {
2405
2470
  var DEFAULT_SETTLEMENT_CONFIG = {
2406
2471
  vtxoThreshold: DEFAULT_THRESHOLD_SECONDS,
2407
2472
  boardingUtxoSweep: true,
2408
- pollIntervalMs: 6e4
2473
+ pollIntervalMs: 6e4,
2474
+ deprecatedSignerMigration: true
2409
2475
  };
2410
2476
  function getRecoverableVtxos(vtxos, dustAmount) {
2411
2477
  return vtxos.filter((vtxo) => {
@@ -2459,6 +2525,51 @@ function getExpiringAndRecoverableVtxos(vtxos, thresholdMs, dustAmount) {
2459
2525
  (vtxo) => isVtxoExpiringSoon(vtxo, thresholdMs) || isRecoverable(vtxo) || isSpendable(vtxo) && isExpired(vtxo) || isSubdust(vtxo, dustAmount)
2460
2526
  );
2461
2527
  }
2528
+ function isMigrationCapable(wallet) {
2529
+ return "arkProvider" in wallet && "arkServerPublicKey" in wallet && "onchainProvider" in wallet && typeof wallet.rotateServerSigner === "function" && typeof wallet.sendSelectedVtxosToSelf === "function" && typeof wallet.getBoardingUtxosForSigners === "function";
2530
+ }
2531
+ function classifiedToRef(c) {
2532
+ return {
2533
+ txid: c.vtxo.txid,
2534
+ vout: c.vtxo.vout,
2535
+ value: c.vtxo.value,
2536
+ signerPubKey: c.classification.signerPubKey,
2537
+ cutoffDate: c.classification.cutoffDate
2538
+ };
2539
+ }
2540
+ function classifiedBoardingToRef(c) {
2541
+ return {
2542
+ txid: c.coin.txid,
2543
+ vout: c.coin.vout,
2544
+ value: c.coin.value,
2545
+ signerPubKey: c.classification.signerPubKey,
2546
+ cutoffDate: c.classification.cutoffDate
2547
+ };
2548
+ }
2549
+ function mergeSignerReports(...reportLists) {
2550
+ const bySigner = /* @__PURE__ */ new Map();
2551
+ for (const list of reportLists) {
2552
+ for (const r of list) {
2553
+ const existing = bySigner.get(r.signerPubKey);
2554
+ if (existing) {
2555
+ existing.vtxoCount += r.vtxoCount;
2556
+ existing.totalValue += r.totalValue;
2557
+ existing.boardingCount += r.boardingCount;
2558
+ existing.boardingValue += r.boardingValue;
2559
+ existing.recoverableCount += r.recoverableCount;
2560
+ existing.recoverableValue += r.recoverableValue;
2561
+ existing.awaitingSweepCount += r.awaitingSweepCount;
2562
+ existing.awaitingSweepValue += r.awaitingSweepValue;
2563
+ if (r.nextSweepEta !== void 0) {
2564
+ existing.nextSweepEta = existing.nextSweepEta === void 0 ? r.nextSweepEta : Math.min(existing.nextSweepEta, r.nextSweepEta);
2565
+ }
2566
+ } else {
2567
+ bySigner.set(r.signerPubKey, { ...r });
2568
+ }
2569
+ }
2570
+ }
2571
+ return Array.from(bySigner.values());
2572
+ }
2462
2573
  var VtxoManager = class _VtxoManager {
2463
2574
  constructor(wallet, renewalConfig, settlementConfig) {
2464
2575
  this.wallet = wallet;
@@ -2474,15 +2585,9 @@ var VtxoManager = class _VtxoManager {
2474
2585
  } else {
2475
2586
  this.settlementConfig = { ...DEFAULT_SETTLEMENT_CONFIG };
2476
2587
  }
2477
- this.contractEventsSubscriptionReady = this.initializeSubscription().then(
2478
- (subscription) => {
2479
- this.contractEventsSubscription = subscription;
2480
- return subscription;
2481
- }
2482
- );
2588
+ this.contractEventsSubscriptionReady = this.initializeSubscription();
2483
2589
  }
2484
2590
  settlementConfig;
2485
- contractEventsSubscription;
2486
2591
  contractEventsSubscriptionReady;
2487
2592
  disposePromise;
2488
2593
  pollTimeoutId;
@@ -2519,6 +2624,15 @@ var VtxoManager = class _VtxoManager {
2519
2624
  lastVtxoSpentRefreshTimestamp = 0;
2520
2625
  vtxoSpentRefreshPromise;
2521
2626
  static VTXO_SPENT_REFRESH_COOLDOWN_MS = 3e4;
2627
+ // Cooldown/backoff for the automatic deprecated-signer migration pass.
2628
+ // Mirrors the periodic-settle machinery so a server-side migration failure
2629
+ // (e.g. arkd not yet accepting old-key inputs, or a closed cutoff window)
2630
+ // backs off exponentially instead of re-submitting an identical intent on
2631
+ // every poll. The manual migrateDeprecatedSignerVtxos() bypasses this.
2632
+ lastMigrationTimestamp = 0;
2633
+ consecutiveMigrationFailures = 0;
2634
+ static MIGRATION_COOLDOWN_MS = 3e4;
2635
+ static MIGRATION_MAX_BACKOFF_MS = 5 * 60 * 1e3;
2522
2636
  // ========== Recovery Methods ==========
2523
2637
  /**
2524
2638
  * Recover swept/expired virtual outputs by settling them back to the wallet's Arkade address.
@@ -2559,16 +2673,21 @@ var VtxoManager = class _VtxoManager {
2559
2673
  if (vtxosToRecover.length === 0) {
2560
2674
  throw new Error("No recoverable VTXOs found");
2561
2675
  }
2562
- if (vtxosToRecover.length > MAX_VTXOS_PER_SETTLEMENT) {
2676
+ const info = await this.getInfoProvider()?.getInfo();
2677
+ const vtxoMaxAmount = info?.vtxoMaxAmount ?? -1n;
2678
+ const capped = capSettlementBatch(byValueDescending(vtxosToRecover), vtxoMaxAmount);
2679
+ if (capped.length < vtxosToRecover.length) {
2563
2680
  const recoverableCount = vtxosToRecover.length;
2564
- const capped = byValueDescending(vtxosToRecover).slice(0, MAX_VTXOS_PER_SETTLEMENT);
2565
2681
  ({ vtxosToRecover, totalAmount } = getRecoverableWithSubdust(capped, dustAmount));
2566
2682
  if (vtxosToRecover.length === 0) {
2567
2683
  throw new Error(
2568
- `Capped recovery batch (highest-value ${MAX_VTXOS_PER_SETTLEMENT} of ${recoverableCount} recoverable VTXOs) is below the dust threshold ${dustAmount}`
2684
+ `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}`
2569
2685
  );
2570
2686
  }
2571
2687
  }
2688
+ if (info && isMigrationCapable(this.wallet)) {
2689
+ await this.rotateForRecoverableInputs(vtxosToRecover, info);
2690
+ }
2572
2691
  const arkAddress = await this.wallet.getAddress();
2573
2692
  return this.wallet.settle(
2574
2693
  {
@@ -2718,8 +2837,24 @@ var VtxoManager = class _VtxoManager {
2718
2837
  if (vtxos.length === 0) {
2719
2838
  throw new Error("No VTXOs available to renew");
2720
2839
  }
2721
- if (vtxos.length > MAX_VTXOS_PER_SETTLEMENT) {
2722
- vtxos = byExpiryAscending(vtxos).slice(0, MAX_VTXOS_PER_SETTLEMENT);
2840
+ const info = await this.getInfoProvider()?.getInfo();
2841
+ const vtxoMaxAmount = info?.vtxoMaxAmount ?? -1n;
2842
+ const capped = capSettlementBatch(byExpiryAscending(vtxos), vtxoMaxAmount);
2843
+ if (vtxoMaxAmount >= 0n) {
2844
+ const oversized = vtxos.filter((vtxo) => BigInt(vtxo.value) > vtxoMaxAmount);
2845
+ if (oversized.length > 0) {
2846
+ console.warn(
2847
+ `Renewal: ${oversized.length} VTXO(s) exceed the per-output limit ${vtxoMaxAmount} and cannot be renewed; they risk unilateral exit`
2848
+ );
2849
+ }
2850
+ }
2851
+ if (capped.length < vtxos.length) {
2852
+ vtxos = capped;
2853
+ if (vtxos.length === 0) {
2854
+ throw new Error(
2855
+ `No VTXOs available to renew within the per-output limit ${vtxoMaxAmount}`
2856
+ );
2857
+ }
2723
2858
  }
2724
2859
  const totalAmount = vtxos.reduce((sum, vtxo) => sum + vtxo.value, 0);
2725
2860
  const dustAmount = getDustAmount(this.wallet);
@@ -2728,6 +2863,9 @@ var VtxoManager = class _VtxoManager {
2728
2863
  `Total amount ${totalAmount} is below dust threshold ${dustAmount}`
2729
2864
  );
2730
2865
  }
2866
+ if (info && isMigrationCapable(this.wallet)) {
2867
+ await this.rotateForRecoverableInputs(vtxos, info);
2868
+ }
2731
2869
  const arkAddress = await this.wallet.getAddress();
2732
2870
  const txid = await this.wallet.settle(
2733
2871
  {
@@ -2878,6 +3016,472 @@ var VtxoManager = class _VtxoManager {
2878
3016
  this.knownBoardingUtxos.add(`${txid}:0`);
2879
3017
  return txid;
2880
3018
  }
3019
+ // ========== Deprecated-Signer Migration Methods ==========
3020
+ /**
3021
+ * Cooperatively migrate VTXOs minted under a now-deprecated server signer
3022
+ * to the wallet's active-signer address. See {@link IVtxoManager}.
3023
+ */
3024
+ async migrateDeprecatedSignerVtxos(options) {
3025
+ return this.migrateCore(options);
3026
+ }
3027
+ /**
3028
+ * Machine-readable status of every deprecated server signer the wallet
3029
+ * currently holds funds under (Section 6), without migrating. Covers both
3030
+ * VTXO and boarding holdings (Section 7), merged per signer.
3031
+ *
3032
+ * @remarks This is no longer a pure repository/info read: surfacing boarding
3033
+ * holdings fans out per boarding address (`getCoins` round trips) and
3034
+ * refreshes the UTXO cache via `saveUtxos`.
3035
+ */
3036
+ async getDeprecatedSignerStatus() {
3037
+ const wallet = this.requireMigrationCapableWallet();
3038
+ const info = await wallet.arkProvider.getInfo();
3039
+ const { reports: vtxoReports } = await this.classifyDeprecatedSignerContracts(info);
3040
+ const { reports: boardingReports } = await this.classifyDeprecatedSignerBoarding(info);
3041
+ return mergeSignerReports(vtxoReports, boardingReports);
3042
+ }
3043
+ /**
3044
+ * Core migration routine shared by the manual API and the automatic poll
3045
+ * pass. Fetches a fresh {@link ArkInfo}, applies a mid-session signer
3046
+ * rotation when the wallet's own snapshot signer has been deprecated,
3047
+ * selects spendable VTXOs under deprecated-signer contracts (cutoff-first),
3048
+ * and settles them to the active-signer Ark address.
3049
+ */
3050
+ async migrateCore(options) {
3051
+ const wallet = this.requireMigrationCapableWallet();
3052
+ const info = await wallet.arkProvider.getInfo();
3053
+ const signerSet = signerSetFromInfo(info);
3054
+ const nowSeconds = Math.floor(Date.now() / 1e3);
3055
+ const walletSignerHex = hex.encode(wallet.arkServerPublicKey);
3056
+ const walletClass = classifyAgainstSignerSet(walletSignerHex, signerSet, nowSeconds);
3057
+ if (signerSet.deprecated.size === 0 && walletClass.status === "CURRENT") {
3058
+ return { rotated: false, expired: [], signers: [] };
3059
+ }
3060
+ if (walletClass.status === "UNKNOWN_SIGNER") {
3061
+ const { reports: vtxoReports2 } = await this.classifyDeprecatedSignerContracts(info);
3062
+ const { reports: boardingReports2 } = await this.classifyDeprecatedSignerBoarding(info);
3063
+ return {
3064
+ rotated: false,
3065
+ expired: [],
3066
+ signers: mergeSignerReports(vtxoReports2, boardingReports2),
3067
+ skipped: "unknown-wallet-signer"
3068
+ };
3069
+ }
3070
+ const rotated = await this.ensureReceiveOnActiveSigner(info);
3071
+ const {
3072
+ reports: vtxoReports,
3073
+ migratable: vtxoMigratable,
3074
+ expired: vtxoExpired
3075
+ } = await this.classifyDeprecatedSignerContracts(info);
3076
+ const {
3077
+ reports: boardingReports,
3078
+ migratable: boardingMigratable,
3079
+ expired: boardingExpired
3080
+ } = await this.classifyDeprecatedSignerBoarding(info);
3081
+ const reports = mergeSignerReports(vtxoReports, boardingReports);
3082
+ const expiredRefs = [
3083
+ ...vtxoExpired.map(classifiedToRef),
3084
+ ...boardingExpired.map(classifiedBoardingToRef)
3085
+ ];
3086
+ if (vtxoMigratable.length === 0 && boardingMigratable.length === 0) {
3087
+ return {
3088
+ rotated,
3089
+ expired: expiredRefs,
3090
+ signers: reports,
3091
+ skipped: "no-deprecated-vtxos"
3092
+ };
3093
+ }
3094
+ const vtxoMaxAmount = info.vtxoMaxAmount;
3095
+ const dustAmount = getDustAmount(this.wallet);
3096
+ const report = {
3097
+ rotated,
3098
+ expired: expiredRefs,
3099
+ signers: reports
3100
+ };
3101
+ if (vtxoMigratable.length > 0) {
3102
+ report.vtxos = await this.runMigrationLeg(
3103
+ vtxoMigratable,
3104
+ (c) => c.vtxo.value,
3105
+ classifiedToRef,
3106
+ vtxoMaxAmount,
3107
+ dustAmount,
3108
+ "VTXO",
3109
+ (capped) => wallet.sendSelectedVtxosToSelf(capped.map((c) => c.vtxo))
3110
+ );
3111
+ }
3112
+ if (boardingMigratable.length > 0) {
3113
+ report.boarding = await this.runMigrationLeg(
3114
+ boardingMigratable,
3115
+ (c) => c.coin.value,
3116
+ classifiedBoardingToRef,
3117
+ vtxoMaxAmount,
3118
+ dustAmount,
3119
+ "boarding",
3120
+ async (capped) => {
3121
+ const arkAddress = await this.wallet.getAddress();
3122
+ const totalAmount = capped.reduce((sum, c) => sum + BigInt(c.coin.value), 0n);
3123
+ return this.wallet.settle(
3124
+ {
3125
+ inputs: capped.map((c) => c.coin),
3126
+ outputs: [{ address: arkAddress, amount: totalAmount }]
3127
+ },
3128
+ options?.eventCallback
3129
+ );
3130
+ }
3131
+ );
3132
+ }
3133
+ return report;
3134
+ }
3135
+ /**
3136
+ * Size and submit one migration leg. Filters inputs whose value alone
3137
+ * exceeds the per-output ceiling (`vtxoMaxAmount`; `< 0` means no limit) —
3138
+ * those can never form a ≤-ceiling output and must exit unilaterally — then
3139
+ * caps the rest (highest-value first; bounded by {@link MAX_VTXOS_PER_SETTLEMENT}
3140
+ * AND a gross total within `vtxoMaxAmount`), applies the protocol dust floor,
3141
+ * and submits the capped batch through `submit`. A throw from `submit` lands
3142
+ * in `error`; the caller's other leg still runs.
3143
+ *
3144
+ * Migration is mandatory and fee-exempt: every selected input moves at its
3145
+ * full value, so the gross total IS the aggregated output amount (kept under
3146
+ * the server ceiling by the cap). The dust floor guards the degenerate cases
3147
+ * where every input was oversized or the whole holding sums below dust.
3148
+ */
3149
+ async runMigrationLeg(candidates, valueOf, toRef, vtxoMaxAmount, dustAmount, legName, submit) {
3150
+ const oversizedRefs = [];
3151
+ const sized = [];
3152
+ for (const c of candidates) {
3153
+ if (vtxoMaxAmount >= 0n && BigInt(valueOf(c)) > vtxoMaxAmount) {
3154
+ oversizedRefs.push(toRef(c));
3155
+ } else {
3156
+ sized.push(c);
3157
+ }
3158
+ }
3159
+ if (oversizedRefs.length > 0) {
3160
+ console.warn(
3161
+ `Deprecated-signer migration (${legName}): ${oversizedRefs.length} input(s) exceed the per-output limit ${vtxoMaxAmount} and cannot be migrated cooperatively; they require a unilateral exit.`
3162
+ );
3163
+ }
3164
+ const oversizedField = oversizedRefs.length > 0 ? { oversized: oversizedRefs } : {};
3165
+ const capped = capSettlementBatch(
3166
+ byValueDescending(sized.map((c) => ({ value: valueOf(c), c }))),
3167
+ vtxoMaxAmount
3168
+ ).map((w) => w.c);
3169
+ const deferred = sized.length - capped.length;
3170
+ const totalAmount = capped.reduce((sum, c) => sum + BigInt(valueOf(c)), 0n);
3171
+ if (totalAmount < dustAmount) {
3172
+ const onlyOversized = sized.length === 0 && oversizedRefs.length > 0;
3173
+ return {
3174
+ migrated: [],
3175
+ skipped: onlyOversized ? "oversized-only" : "below-dust",
3176
+ ...oversizedField
3177
+ };
3178
+ }
3179
+ try {
3180
+ const txid = await submit(capped);
3181
+ return {
3182
+ txid,
3183
+ migrated: capped.map(toRef),
3184
+ ...deferred > 0 ? { deferred } : {},
3185
+ ...oversizedField
3186
+ };
3187
+ } catch (e) {
3188
+ return {
3189
+ migrated: [],
3190
+ error: e instanceof Error ? e.message : String(e),
3191
+ ...oversizedField
3192
+ };
3193
+ }
3194
+ }
3195
+ /**
3196
+ * Enumerate the wallet's `default`/`delegate` contracts, classify each
3197
+ * against the fresh signer set, and split their spendable VTXOs into
3198
+ * cooperatively-migratable and cutoff-expired sets while building the
3199
+ * per-signer status report. Current-signer contracts are skipped; swept
3200
+ * (recoverable) VTXOs are excluded from the settle sets — those follow the
3201
+ * recovery path — but are still counted on EXPIRED report rows
3202
+ * (`recoverableCount`) so post-cutoff funds in flight stay visible.
3203
+ */
3204
+ async classifyDeprecatedSignerContracts(info) {
3205
+ const cm = await this.wallet.getContractManager();
3206
+ const signerSet = signerSetFromInfo(info);
3207
+ const nowSeconds = Math.floor(Date.now() / 1e3);
3208
+ const contractsWithVtxos = await cm.getContractsWithVtxos({
3209
+ type: ["default", "delegate"]
3210
+ });
3211
+ const reportsBySigner = /* @__PURE__ */ new Map();
3212
+ const migratable = [];
3213
+ const expired = [];
3214
+ for (const { contract, vtxos } of contractsWithVtxos) {
3215
+ const serverPubKey = contract.params.serverPubKey;
3216
+ if (!serverPubKey) continue;
3217
+ const cls = classifyAgainstSignerSet(serverPubKey, signerSet, nowSeconds);
3218
+ if (cls.status === "CURRENT") continue;
3219
+ const recoverable = vtxos.filter((v) => isRecoverable(v));
3220
+ const spendable = vtxos.filter((v) => isSpendable(v) && !isRecoverable(v));
3221
+ const value = spendable.reduce((sum, v) => sum + v.value, 0);
3222
+ let recoverableCount = 0;
3223
+ let recoverableValue = 0;
3224
+ let awaitingSweepCount = 0;
3225
+ let awaitingSweepValue = 0;
3226
+ let nextSweepEta;
3227
+ if (cls.status === "EXPIRED") {
3228
+ recoverableCount = recoverable.length;
3229
+ recoverableValue = recoverable.reduce((sum, v) => sum + v.value, 0);
3230
+ awaitingSweepCount = spendable.length;
3231
+ awaitingSweepValue = value;
3232
+ for (const v of spendable) {
3233
+ const exp = v.virtualStatus.batchExpiry;
3234
+ if (exp !== void 0 && (nextSweepEta === void 0 || exp < nextSweepEta)) {
3235
+ nextSweepEta = exp;
3236
+ }
3237
+ }
3238
+ }
3239
+ const existing = reportsBySigner.get(cls.signerPubKey);
3240
+ if (existing) {
3241
+ existing.vtxoCount += spendable.length;
3242
+ existing.totalValue += value;
3243
+ existing.recoverableCount += recoverableCount;
3244
+ existing.recoverableValue += recoverableValue;
3245
+ existing.awaitingSweepCount += awaitingSweepCount;
3246
+ existing.awaitingSweepValue += awaitingSweepValue;
3247
+ if (nextSweepEta !== void 0) {
3248
+ existing.nextSweepEta = existing.nextSweepEta === void 0 ? nextSweepEta : Math.min(existing.nextSweepEta, nextSweepEta);
3249
+ }
3250
+ } else {
3251
+ reportsBySigner.set(cls.signerPubKey, {
3252
+ signerPubKey: cls.signerPubKey,
3253
+ status: cls.status,
3254
+ cutoffDate: cls.cutoffDate,
3255
+ secondsUntilCutoff: cls.secondsUntilCutoff,
3256
+ vtxoCount: spendable.length,
3257
+ totalValue: value,
3258
+ boardingCount: 0,
3259
+ boardingValue: 0,
3260
+ recoverableCount,
3261
+ recoverableValue,
3262
+ awaitingSweepCount,
3263
+ awaitingSweepValue,
3264
+ nextSweepEta
3265
+ });
3266
+ }
3267
+ if (isCooperativelyMigratable(cls.status)) {
3268
+ for (const v of spendable) {
3269
+ if (!v.virtualStatus.batchExpiry) continue;
3270
+ migratable.push({ vtxo: v, classification: cls });
3271
+ }
3272
+ } else if (cls.status === "EXPIRED") {
3273
+ for (const v of spendable) expired.push({ vtxo: v, classification: cls });
3274
+ }
3275
+ }
3276
+ return {
3277
+ reports: Array.from(reportsBySigner.values()),
3278
+ migratable,
3279
+ expired
3280
+ };
3281
+ }
3282
+ /**
3283
+ * Boarding sibling of {@link classifyDeprecatedSignerContracts} (Section 7):
3284
+ * fan out over the wallet's boarding addresses (current + historical), group
3285
+ * the on-chain UTXOs per address, classify each address's signer against the
3286
+ * fresh signer set, and split the confirmed boarding coins into cooperatively-
3287
+ * migratable and cutoff-expired sets while building the per-signer report.
3288
+ *
3289
+ * Discovery sees the active signer plus EVERY deprecated key (EXPIRED
3290
+ * included), so expired-signer boarding is still reported; migration
3291
+ * eligibility is gated afterwards by {@link isCooperativelyMigratable} and a
3292
+ * per-row boarding-output CSV check — never by the fetch. Current-signer
3293
+ * coins are classified `CURRENT` and ignored; foreign-ASP rows are excluded
3294
+ * because their keys are not in the signer set.
3295
+ */
3296
+ async classifyDeprecatedSignerBoarding(info) {
3297
+ const wallet = this.requireMigrationCapableWallet();
3298
+ const signerSet = signerSetFromInfo(info);
3299
+ const nowSeconds = Math.floor(Date.now() / 1e3);
3300
+ const allowed = /* @__PURE__ */ new Set([signerSet.active, ...signerSet.deprecated.keys()]);
3301
+ const groups = await wallet.getBoardingUtxosForSigners(allowed);
3302
+ let chainTipHeight;
3303
+ if (groups.some((g) => g.csvTimelock.type === "blocks")) {
3304
+ const tip = await wallet.onchainProvider.getChainTip();
3305
+ chainTipHeight = tip.height;
3306
+ }
3307
+ const reportsBySigner = /* @__PURE__ */ new Map();
3308
+ const migratable = [];
3309
+ const expired = [];
3310
+ for (const group of groups) {
3311
+ const cls = classifyAgainstSignerSet(group.serverPubKey, signerSet, nowSeconds);
3312
+ if (cls.status === "CURRENT") continue;
3313
+ const confirmed = group.coins.filter((c) => c.status.confirmed);
3314
+ if (confirmed.length === 0) continue;
3315
+ const value = confirmed.reduce((sum, c) => sum + c.value, 0);
3316
+ const existing = reportsBySigner.get(cls.signerPubKey);
3317
+ if (existing) {
3318
+ existing.boardingCount += confirmed.length;
3319
+ existing.boardingValue += value;
3320
+ } else {
3321
+ reportsBySigner.set(cls.signerPubKey, {
3322
+ signerPubKey: cls.signerPubKey,
3323
+ status: cls.status,
3324
+ cutoffDate: cls.cutoffDate,
3325
+ secondsUntilCutoff: cls.secondsUntilCutoff,
3326
+ vtxoCount: 0,
3327
+ totalValue: 0,
3328
+ boardingCount: confirmed.length,
3329
+ boardingValue: value,
3330
+ // Boarding UTXOs don't carry an offchain sweep lifecycle; the
3331
+ // post-cutoff recover-on-sweep fields apply to VTXOs only and
3332
+ // are merged in from the VTXO classifier (mergeSignerReports).
3333
+ recoverableCount: 0,
3334
+ recoverableValue: 0,
3335
+ awaitingSweepCount: 0,
3336
+ awaitingSweepValue: 0
3337
+ });
3338
+ }
3339
+ for (const coin of confirmed) {
3340
+ const boardingExpired = hasBoardingTxExpired(
3341
+ coin,
3342
+ group.csvTimelock,
3343
+ chainTipHeight
3344
+ );
3345
+ if (isCooperativelyMigratable(cls.status) && !boardingExpired) {
3346
+ migratable.push({ coin, classification: cls });
3347
+ } else if (cls.status === "EXPIRED") {
3348
+ expired.push({ coin, classification: cls });
3349
+ }
3350
+ }
3351
+ }
3352
+ return {
3353
+ reports: Array.from(reportsBySigner.values()),
3354
+ migratable,
3355
+ expired
3356
+ };
3357
+ }
3358
+ /**
3359
+ * Automatic migration pass invoked from the poll loop. Self-contained:
3360
+ * respects an exponential cooldown and logs failures rather than throwing,
3361
+ * so a persistently failing migration backs off instead of re-submitting
3362
+ * an identical intent every cycle.
3363
+ */
3364
+ async runMigrationPass() {
3365
+ const cooldownMs = Math.min(
3366
+ _VtxoManager.MIGRATION_COOLDOWN_MS * Math.pow(2, this.consecutiveMigrationFailures),
3367
+ _VtxoManager.MIGRATION_MAX_BACKOFF_MS
3368
+ );
3369
+ if (Date.now() - this.lastMigrationTimestamp < cooldownMs) return;
3370
+ try {
3371
+ const report = await this.migrateCore();
3372
+ const legError = report.vtxos?.error ?? report.boarding?.error;
3373
+ if (legError) {
3374
+ this.consecutiveMigrationFailures++;
3375
+ console.error("Deprecated-signer migration leg failed:", legError);
3376
+ } else {
3377
+ this.consecutiveMigrationFailures = 0;
3378
+ }
3379
+ } catch (e) {
3380
+ this.consecutiveMigrationFailures++;
3381
+ console.error("Error during deprecated-signer migration:", e);
3382
+ } finally {
3383
+ this.lastMigrationTimestamp = Date.now();
3384
+ }
3385
+ }
3386
+ /** Asserts migration capability and returns the typed wallet. */
3387
+ requireMigrationCapableWallet() {
3388
+ if (!isMigrationCapable(this.wallet)) {
3389
+ throw new Error(
3390
+ "Deprecated-signer migration requires a Wallet instance with arkProvider, arkServerPublicKey, and rotateServerSigner"
3391
+ );
3392
+ }
3393
+ return this.wallet;
3394
+ }
3395
+ /**
3396
+ * If the wallet's own construction-time signer snapshot has been deprecated,
3397
+ * re-derive its receive/boarding state under the active signer so any output
3398
+ * built afterwards commits to the active key. No-op when the snapshot is
3399
+ * already current. Returns whether a rotation was applied. Treats an
3400
+ * unknown-signer snapshot as "do not rotate" (caller decides).
3401
+ *
3402
+ * Shared by the migration pass (where the wallet's own snapshot is the thing
3403
+ * being migrated) and the recovery/renewal/periodic-settle paths (via
3404
+ * {@link rotateForRecoverableInputs}), so a swept old-signer VTXO recovered
3405
+ * after cutoff re-mints under the active signer rather than re-committing to
3406
+ * the deprecated key (Section 6 / post-cutoff). `rotateServerSigner` is
3407
+ * idempotent and serializes itself against HD receive rotation, so repeated
3408
+ * calls across passes are safe.
3409
+ */
3410
+ async ensureReceiveOnActiveSigner(info) {
3411
+ const wallet = this.requireMigrationCapableWallet();
3412
+ const signerSet = signerSetFromInfo(info);
3413
+ const nowSeconds = Math.floor(Date.now() / 1e3);
3414
+ const walletClass = classifyAgainstSignerSet(
3415
+ hex.encode(wallet.arkServerPublicKey),
3416
+ signerSet,
3417
+ nowSeconds
3418
+ );
3419
+ if (walletClass.status === "CURRENT" || walletClass.status === "UNKNOWN_SIGNER") {
3420
+ return false;
3421
+ }
3422
+ await wallet.rotateServerSigner(hex.decode(info.signerPubkey), info.checkpointTapscript);
3423
+ return true;
3424
+ }
3425
+ /**
3426
+ * Rotation guard for the recovery-bearing settle paths (recover / renew /
3427
+ * periodic settle). Pins the wallet's receive snapshot to the active signer
3428
+ * before they build their output, but ONLY when this pass actually carries
3429
+ * an input minted under a deprecated signer — so a routine current-signer
3430
+ * settle on a long-lived pre-rotation instance does not eagerly rotate.
3431
+ *
3432
+ * Cheap in the common case: a watch-only/proxy wallet (not migration-capable)
3433
+ * and a current/unknown wallet snapshot both short-circuit before the
3434
+ * contract round-trip, so the only instance that pays for the input scan is
3435
+ * the long-lived deprecated-snapshot one that genuinely needs rotating.
3436
+ *
3437
+ * Runs OUTSIDE any `renewalInProgress` window the caller sets, and
3438
+ * `rotateServerSigner` does not depend on that flag, so it cannot deadlock
3439
+ * against the receive rotator. Returns whether a rotation was applied.
3440
+ */
3441
+ async rotateForRecoverableInputs(inputs, info) {
3442
+ if (!isMigrationCapable(this.wallet)) return false;
3443
+ const signerSet = signerSetFromInfo(info);
3444
+ const nowSeconds = Math.floor(Date.now() / 1e3);
3445
+ const walletClass = classifyAgainstSignerSet(
3446
+ hex.encode(this.wallet.arkServerPublicKey),
3447
+ signerSet,
3448
+ nowSeconds
3449
+ );
3450
+ if (walletClass.status === "CURRENT" || walletClass.status === "UNKNOWN_SIGNER") {
3451
+ return false;
3452
+ }
3453
+ if (!await this.anyInputUnderDeprecatedSigner(inputs, signerSet, nowSeconds)) {
3454
+ return false;
3455
+ }
3456
+ return this.ensureReceiveOnActiveSigner(info);
3457
+ }
3458
+ /**
3459
+ * Whether any of the given input outpoints belongs to a contract whose
3460
+ * server signer classifies as non-`CURRENT` against the fresh signer set —
3461
+ * i.e. a deprecated-signer (incl. EXPIRED) input. Maps outpoints to their
3462
+ * owning contract via the ContractManager so it works on the typed
3463
+ * {@link ExtendedVirtualCoin}/{@link ExtendedCoin} inputs the recovery paths
3464
+ * carry (which don't expose `contractScript`).
3465
+ */
3466
+ async anyInputUnderDeprecatedSigner(inputs, signerSet, nowSeconds) {
3467
+ if (inputs.length === 0) return false;
3468
+ const wanted = new Set(inputs.map((i) => `${i.txid}:${i.vout}`));
3469
+ const cm = await this.wallet.getContractManager();
3470
+ const contractsWithVtxos = await cm.getContractsWithVtxos({
3471
+ type: ["default", "delegate"]
3472
+ });
3473
+ for (const { contract, vtxos } of contractsWithVtxos) {
3474
+ const serverPubKey = contract.params.serverPubKey;
3475
+ if (!serverPubKey) continue;
3476
+ if (classifyAgainstSignerSet(serverPubKey, signerSet, nowSeconds).status === "CURRENT") {
3477
+ continue;
3478
+ }
3479
+ for (const v of vtxos) {
3480
+ if (wanted.has(`${v.txid}:${v.vout}`)) return true;
3481
+ }
3482
+ }
3483
+ return false;
3484
+ }
2881
3485
  // ========== Private Helpers ==========
2882
3486
  /** Asserts sweep capability and returns the typed wallet. */
2883
3487
  getSweepWallet() {
@@ -2904,6 +3508,16 @@ var VtxoManager = class _VtxoManager {
2904
3508
  getArkProvider() {
2905
3509
  return this.getSweepWallet().arkProvider;
2906
3510
  }
3511
+ /**
3512
+ * Read-only access to the ark provider for fetching server limits. Unlike
3513
+ * {@link getArkProvider}, this does not require full boarding-sweep
3514
+ * capability — recovery and renewal only need it to read `vtxoMaxAmount`.
3515
+ * Returns undefined when no provider is wired, which callers treat as
3516
+ * "no limit".
3517
+ */
3518
+ getInfoProvider() {
3519
+ return this.wallet.arkProvider;
3520
+ }
2907
3521
  /** Returns the Bitcoin network configuration from the wallet. */
2908
3522
  getNetwork() {
2909
3523
  return this.getSweepWallet().network;
@@ -3105,6 +3719,10 @@ var VtxoManager = class _VtxoManager {
3105
3719
  }
3106
3720
  }
3107
3721
  }
3722
+ const migrationEnabled = this.settlementConfig !== false && (this.settlementConfig?.deprecatedSignerMigration ?? DEFAULT_SETTLEMENT_CONFIG.deprecatedSignerMigration);
3723
+ if (migrationEnabled && isMigrationCapable(this.wallet)) {
3724
+ await this.runMigrationPass();
3725
+ }
3108
3726
  });
3109
3727
  } catch (e) {
3110
3728
  hadError = true;
@@ -3173,7 +3791,8 @@ var VtxoManager = class _VtxoManager {
3173
3791
  return;
3174
3792
  }
3175
3793
  const dustAmount = getDustAmount(this.wallet);
3176
- const { fees } = await this.getArkProvider().getInfo();
3794
+ const info = await this.getArkProvider().getInfo();
3795
+ const { fees, vtxoMaxAmount } = info;
3177
3796
  const estimator = new Estimator(fees.intentFee);
3178
3797
  let totalAmount = 0n;
3179
3798
  const filteredBoarding = [];
@@ -3202,12 +3821,19 @@ var VtxoManager = class _VtxoManager {
3202
3821
  if (inputFee.satoshis >= v.value) {
3203
3822
  continue;
3204
3823
  }
3824
+ const net = BigInt(v.value) - BigInt(inputFee.satoshis);
3825
+ if (vtxoMaxAmount >= 0n && totalAmount + net > vtxoMaxAmount) {
3826
+ continue;
3827
+ }
3205
3828
  filteredVtxos.push(v);
3206
- totalAmount += BigInt(v.value) - BigInt(inputFee.satoshis);
3829
+ totalAmount += net;
3207
3830
  }
3208
3831
  if (filteredBoarding.length === 0 && filteredVtxos.length === 0) {
3209
3832
  return;
3210
3833
  }
3834
+ if (isMigrationCapable(this.wallet)) {
3835
+ await this.rotateForRecoverableInputs([...filteredBoarding, ...filteredVtxos], info);
3836
+ }
3211
3837
  const arkAddress = await this.wallet.getAddress();
3212
3838
  const outputFee = estimator.evalOffchainOutput({
3213
3839
  amount: totalAmount,
@@ -3270,7 +3896,6 @@ var VtxoManager = class _VtxoManager {
3270
3896
  clearTimeout(timer);
3271
3897
  }
3272
3898
  const subscription = await this.contractEventsSubscriptionReady;
3273
- this.contractEventsSubscription = void 0;
3274
3899
  subscription?.();
3275
3900
  })();
3276
3901
  return this.disposePromise;
@@ -3811,15 +4436,17 @@ async function buildTransactionHistory(vtxos, allBoardingTxs, commitmentsToIgnor
3811
4436
  txTime = getTxCreatedAt ? await getTxCreatedAt(vtxo.arkTxId) ?? vtxo.createdAt.getTime() + 1 : vtxo.createdAt.getTime() + 1;
3812
4437
  }
3813
4438
  const assets = subtractAssets(allSpent, changes);
3814
- sent.push({
3815
- key: { ...txKey, arkTxid: vtxo.arkTxId },
3816
- tag: "offchain",
3817
- type: "SENT" /* TxSent */,
3818
- amount: txAmount,
3819
- settled: true,
3820
- createdAt: txTime,
3821
- ...assets && { assets }
3822
- });
4439
+ if (txAmount !== 0 || assets) {
4440
+ sent.push({
4441
+ key: { ...txKey, arkTxid: vtxo.arkTxId },
4442
+ tag: "offchain",
4443
+ type: "SENT" /* TxSent */,
4444
+ amount: txAmount,
4445
+ settled: true,
4446
+ createdAt: txTime,
4447
+ ...assets && { assets }
4448
+ });
4449
+ }
3823
4450
  }
3824
4451
  if (vtxo.settledBy && !commitmentsToIgnore.has(vtxo.settledBy) && !sent.some((s) => s.key.commitmentTxid === vtxo.settledBy)) {
3825
4452
  const changes = fromOldestVtxo.filter(
@@ -5992,6 +6619,16 @@ function isDiscoverable(handler) {
5992
6619
  }
5993
6620
 
5994
6621
  // src/contracts/contractWatcher.ts
6622
+ function computeReconnectDelay(attempt, baseMs, maxMs) {
6623
+ return Math.min(baseMs * Math.pow(2, attempt - 1), maxMs);
6624
+ }
6625
+ var DEFAULT_CONTRACT_WATCHER_CONFIG = {
6626
+ failsafePollIntervalMs: 2e4,
6627
+ reconnectDelayMs: 1e3,
6628
+ maxReconnectDelayMs: 5e3,
6629
+ maxReconnectAttempts: 0
6630
+ // unlimited
6631
+ };
5995
6632
  var ContractWatcher = class {
5996
6633
  config;
5997
6634
  contracts = /* @__PURE__ */ new Map();
@@ -6011,14 +6648,7 @@ var ContractWatcher = class {
6011
6648
  */
6012
6649
  constructor(config) {
6013
6650
  this.config = {
6014
- failsafePollIntervalMs: 6e4,
6015
- // 1 minute
6016
- reconnectDelayMs: 1e3,
6017
- // 1 second
6018
- maxReconnectDelayMs: 3e4,
6019
- // 30 seconds
6020
- maxReconnectAttempts: 0,
6021
- // unlimited
6651
+ ...DEFAULT_CONTRACT_WATCHER_CONFIG,
6022
6652
  ...config
6023
6653
  };
6024
6654
  }
@@ -6246,8 +6876,9 @@ var ContractWatcher = class {
6246
6876
  }
6247
6877
  this.connectionState = "reconnecting";
6248
6878
  this.reconnectAttempts++;
6249
- const delay = Math.min(
6250
- this.config.reconnectDelayMs * Math.pow(2, this.reconnectAttempts - 1),
6879
+ const delay = computeReconnectDelay(
6880
+ this.reconnectAttempts,
6881
+ this.config.reconnectDelayMs,
6251
6882
  this.config.maxReconnectDelayMs
6252
6883
  );
6253
6884
  this.reconnectTimeoutId = setTimeout(() => {
@@ -7618,6 +8249,22 @@ var WalletReceiveRotator = class _WalletReceiveRotator {
7618
8249
  async drain() {
7619
8250
  await this.chain.catch(() => void 0);
7620
8251
  }
8252
+ /**
8253
+ * Run `fn` on the rotator's serialization chain, so it cannot interleave
8254
+ * with a receive `rotate()`. Used by {@link Wallet.rotateServerSigner} to
8255
+ * serialize server-signer rotation against HD receive rotation: both
8256
+ * rebuild and swap `offchainTapscript`, so running them concurrently could
8257
+ * tear the wallet's visible receive state. The chain keeps advancing even
8258
+ * if `fn` rejects (its own caller still sees the rejection).
8259
+ */
8260
+ runExclusive(fn) {
8261
+ const run = this.chain.catch(() => void 0).then(fn);
8262
+ this.chain = run.then(
8263
+ () => void 0,
8264
+ () => void 0
8265
+ );
8266
+ return run;
8267
+ }
7621
8268
  /**
7622
8269
  * Tear down the subscription first so no late `vtxo_received` event
7623
8270
  * can queue work on a disposing wallet, then drain any in-flight
@@ -7975,7 +8622,6 @@ var ReadonlyWallet = class _ReadonlyWallet {
7975
8622
  this.network = network;
7976
8623
  this.onchainProvider = onchainProvider;
7977
8624
  this.indexerProvider = indexerProvider;
7978
- this.arkServerPublicKey = arkServerPublicKey;
7979
8625
  this.dustAmount = dustAmount;
7980
8626
  this.walletRepository = walletRepository;
7981
8627
  this.contractRepository = contractRepository;
@@ -7992,6 +8638,7 @@ var ReadonlyWallet = class _ReadonlyWallet {
7992
8638
  }
7993
8639
  this._offchainTapscript = offchainTapscript;
7994
8640
  this._boardingTapscript = boardingTapscript;
8641
+ this._arkServerPublicKey = arkServerPublicKey;
7995
8642
  this.watcherConfig = watcherConfig;
7996
8643
  this._assetManager = new ReadonlyAssetManager(this.indexerProvider);
7997
8644
  this.walletContractTimelocks = walletContractTimelocks && walletContractTimelocks.length > 0 ? dedupeTimelocks(walletContractTimelocks) : [this.offchainTapscript.options.csvTimelock];
@@ -8000,7 +8647,6 @@ var ReadonlyWallet = class _ReadonlyWallet {
8000
8647
  _contractManagerInitializing;
8001
8648
  watcherConfig;
8002
8649
  _assetManager;
8003
- _syncVtxosInflight;
8004
8650
  walletContractTimelocks;
8005
8651
  // Outpoints ("txid:vout") committed to an in-flight settle/send. Filtered
8006
8652
  // from getVtxos() so concurrent callers (UI, VtxoManager auto-renewal,
@@ -8029,6 +8675,59 @@ var ReadonlyWallet = class _ReadonlyWallet {
8029
8675
  * it stays the index-0 baseline for their lifetime.
8030
8676
  */
8031
8677
  _boardingTapscript;
8678
+ /**
8679
+ * Backing field for the active server signer (x-only, 32 bytes). Read via
8680
+ * the public {@link arkServerPublicKey} getter; written only by
8681
+ * {@link Wallet.setArkServerPublicKeyForRotation}, the sanctioned
8682
+ * server-signer rotation write path (analogue of `_offchainTapscript`). It
8683
+ * is a *current value*, not a fixed constructor constant, because
8684
+ * mid-session server-signer rotation (plan §4) swaps it when arkd rotates
8685
+ * its active signer. Wallets that never span a rotation keep their
8686
+ * construction-time snapshot for their lifetime.
8687
+ */
8688
+ _arkServerPublicKey;
8689
+ /**
8690
+ * x-only hex of the operator's deprecated signer keys (from
8691
+ * `ArkInfo.deprecatedSigners`), cached for the OFFLINE read/watch paths.
8692
+ * The boarding watch/history surfaces ({@link getBoardingAddresses},
8693
+ * {@link getBoardingTxs}) fan out over {current} ∪ this set so a deposit at
8694
+ * a boarding address minted under a now-rotated operator signer keeps being
8695
+ * watched. Refreshed from the server-info snapshot at construction (via the
8696
+ * create() factories) and on a detected signer change. Deliberately NOT
8697
+ * consulted by the spend path — {@link getBoardingUtxos} stays
8698
+ * current-signer-only (a deprecated-signer input in a plain settle() is
8699
+ * rejected; old-signer recovery goes through the migration API).
8700
+ */
8701
+ _deprecatedSigners = /* @__PURE__ */ new Map();
8702
+ /**
8703
+ * Refresh the cached deprecated-signer set from a fresh server-info
8704
+ * snapshot. Called by the create() factories at construction and by the
8705
+ * server-info-change handler mid-session. Lenient: a malformed deprecated
8706
+ * entry is skipped, never fatal to wallet creation.
8707
+ */
8708
+ refreshDeprecatedSigners(info) {
8709
+ const next = /* @__PURE__ */ new Map();
8710
+ for (const s of info.deprecatedSigners ?? []) {
8711
+ if (!s.pubkey) continue;
8712
+ try {
8713
+ next.set(toXOnlySignerHex(s.pubkey), s.cutoffDate ?? 0n);
8714
+ } catch (e) {
8715
+ console.warn("Skipping malformed deprecated signer pubkey", s.pubkey, e);
8716
+ }
8717
+ }
8718
+ this._deprecatedSigners = next;
8719
+ }
8720
+ /**
8721
+ * The signer set the boarding WATCH/HISTORY paths fan out over: the wallet's
8722
+ * current signer plus every cached deprecated signer. Distinct from the
8723
+ * spend path, which is current-signer-only.
8724
+ */
8725
+ watchedBoardingSigners() {
8726
+ return /* @__PURE__ */ new Set([
8727
+ toXOnlySignerHex(hex.encode(this.boardingTapscript.options.serverPubKey)),
8728
+ ...this._deprecatedSigners.keys()
8729
+ ]);
8730
+ }
8032
8731
  /**
8033
8732
  * Currently-active receive tapscript. Read-only from the outside;
8034
8733
  * mutated only via {@link Wallet.setOffchainTapscriptForRotation}
@@ -8037,6 +8736,16 @@ var ReadonlyWallet = class _ReadonlyWallet {
8037
8736
  get offchainTapscript() {
8038
8737
  return this._offchainTapscript;
8039
8738
  }
8739
+ /**
8740
+ * The wallet's current active server signer (x-only, 32 bytes). Read-only
8741
+ * from the outside; mutated only via
8742
+ * {@link Wallet.setArkServerPublicKeyForRotation} during mid-session
8743
+ * server-signer rotation (plan §4). Single-valued for wallets that never
8744
+ * span a rotation.
8745
+ */
8746
+ get arkServerPublicKey() {
8747
+ return this._arkServerPublicKey;
8748
+ }
8040
8749
  /**
8041
8750
  * The wallet's current boarding tapscript (the on-chain onboarding
8042
8751
  * target). Read-only from the outside; mutated only via
@@ -8195,7 +8904,7 @@ var ReadonlyWallet = class _ReadonlyWallet {
8195
8904
  throw new Error("Invalid configured public key");
8196
8905
  }
8197
8906
  const setup = await _ReadonlyWallet.setupWalletConfig(config, pubkey);
8198
- return new _ReadonlyWallet(
8907
+ const wallet = new _ReadonlyWallet(
8199
8908
  config.identity,
8200
8909
  setup.network,
8201
8910
  setup.onchainProvider,
@@ -8210,6 +8919,8 @@ var ReadonlyWallet = class _ReadonlyWallet {
8210
8919
  config.watcherConfig,
8211
8920
  setup.walletContractTimelocks
8212
8921
  );
8922
+ wallet.refreshDeprecatedSigners(setup.info);
8923
+ return wallet;
8213
8924
  }
8214
8925
  get arkAddress() {
8215
8926
  return this.offchainTapscript.address(this.network.hrp, this.arkServerPublicKey);
@@ -8233,10 +8944,12 @@ var ReadonlyWallet = class _ReadonlyWallet {
8233
8944
  * Return the wallet's combined onchain and offchain balances.
8234
8945
  */
8235
8946
  async getBalance() {
8236
- const [boardingUtxos, vtxos] = await Promise.all([
8947
+ const [boardingUtxos, vtxos, pendingOutpoints] = await Promise.all([
8237
8948
  this.getBoardingUtxos(),
8238
- this.getVtxos()
8949
+ this.getVtxos(),
8950
+ this.pendingRecoveryOutpoints()
8239
8951
  ]);
8952
+ const isPendingRecovery = (coin) => pendingOutpoints.has(`${coin.txid}:${coin.vout}`);
8240
8953
  let confirmed = 0;
8241
8954
  let unconfirmed = 0;
8242
8955
  for (const utxo of boardingUtxos) {
@@ -8249,11 +8962,15 @@ var ReadonlyWallet = class _ReadonlyWallet {
8249
8962
  let settled = 0;
8250
8963
  let preconfirmed = 0;
8251
8964
  let recoverable = 0;
8252
- settled = vtxos.filter((coin) => coin.virtualStatus.state === "settled").reduce((sum, coin) => sum + coin.value, 0);
8253
- preconfirmed = vtxos.filter((coin) => coin.virtualStatus.state === "preconfirmed").reduce((sum, coin) => sum + coin.value, 0);
8965
+ let pendingRecovery = 0;
8966
+ settled = vtxos.filter((coin) => coin.virtualStatus.state === "settled" && !isPendingRecovery(coin)).reduce((sum, coin) => sum + coin.value, 0);
8967
+ preconfirmed = vtxos.filter(
8968
+ (coin) => coin.virtualStatus.state === "preconfirmed" && !isPendingRecovery(coin)
8969
+ ).reduce((sum, coin) => sum + coin.value, 0);
8254
8970
  recoverable = vtxos.filter((coin) => isSpendable(coin) && coin.virtualStatus.state === "swept").reduce((sum, coin) => sum + coin.value, 0);
8971
+ pendingRecovery = vtxos.filter(isPendingRecovery).reduce((sum, coin) => sum + coin.value, 0);
8255
8972
  const totalBoarding = confirmed + unconfirmed;
8256
- const totalOffchain = settled + preconfirmed + recoverable;
8973
+ const totalOffchain = settled + preconfirmed + recoverable + pendingRecovery;
8257
8974
  const assetBalances = /* @__PURE__ */ new Map();
8258
8975
  for (const vtxo of vtxos) {
8259
8976
  if (!isSpendable(vtxo)) continue;
@@ -8278,6 +8995,7 @@ var ReadonlyWallet = class _ReadonlyWallet {
8278
8995
  preconfirmed,
8279
8996
  available: settled + preconfirmed,
8280
8997
  recoverable,
8998
+ pendingRecovery,
8281
8999
  total: totalBoarding + totalOffchain,
8282
9000
  assets
8283
9001
  };
@@ -8304,6 +9022,23 @@ var ReadonlyWallet = class _ReadonlyWallet {
8304
9022
  return !!(f.withUnrolled && vtxo.isUnrolled);
8305
9023
  });
8306
9024
  }
9025
+ /**
9026
+ * Outpoints of VTXOs whose deprecated signer is past its cutoff (EXPIRED) and
9027
+ * which have not yet been swept — unspendable until they recover. Offline:
9028
+ * classifies the repo's contracts against the cached signer set (active +
9029
+ * {@link _deprecatedSigners}, cutoffs included). Empty fast-path when no
9030
+ * signer is deprecated. Consumed by {@link getBalance} (the `pendingRecovery`
9031
+ * bucket) and the send coin-selection path so neither counts nor spends them.
9032
+ */
9033
+ async pendingRecoveryOutpoints() {
9034
+ if (this._deprecatedSigners.size === 0) return /* @__PURE__ */ new Set();
9035
+ const contractManager = await this.getContractManager();
9036
+ const contractsWithVtxos = await contractManager.getContractsWithVtxos();
9037
+ return selectPendingRecoveryOutpoints(contractsWithVtxos, {
9038
+ active: toXOnlySignerHex(hex.encode(this.offchainTapscript.options.serverPubKey)),
9039
+ deprecated: this._deprecatedSigners
9040
+ });
9041
+ }
8307
9042
  /**
8308
9043
  * Return wallet transaction history derived from Arkade state and boarding transactions.
8309
9044
  */
@@ -8330,7 +9065,7 @@ var ReadonlyWallet = class _ReadonlyWallet {
8330
9065
  * surfaced (plan §6-IV); {@link getBoardingAddress} stays single-valued.
8331
9066
  */
8332
9067
  async getBoardingAddresses() {
8333
- const tapscripts = await this.getBoardingTapscripts();
9068
+ const tapscripts = await this.getBoardingTapscripts(this.watchedBoardingSigners());
8334
9069
  return tapscripts.map((t) => t.onchainAddress(this.network));
8335
9070
  }
8336
9071
  /**
@@ -8340,7 +9075,7 @@ var ReadonlyWallet = class _ReadonlyWallet {
8340
9075
  async getBoardingTxs() {
8341
9076
  const utxos = [];
8342
9077
  const commitmentsToIgnore = /* @__PURE__ */ new Set();
8343
- const tapscripts = await this.getBoardingTapscripts();
9078
+ const tapscripts = await this.getBoardingTapscripts(this.watchedBoardingSigners());
8344
9079
  const outspendCache = /* @__PURE__ */ new Map();
8345
9080
  for (const tapscript of tapscripts) {
8346
9081
  const boardingAddress = tapscript.onchainAddress(this.network);
@@ -8415,8 +9150,15 @@ var ReadonlyWallet = class _ReadonlyWallet {
8415
9150
  * Always includes the index-0 baseline (identity x-only key), which covers
8416
9151
  * the degenerate equal-delay case where the index-0 boarding row is
8417
9152
  * coalesced onto a `default` row and so isn't a `boarding`-typed contract.
9153
+ *
9154
+ * @param allowedSigners - Optional set of x-only-hex server keys whose
9155
+ * persisted boarding rows are included. Defaults to `{current x-only
9156
+ * signer}`, preserving today's current-signer-only discovery (and the
9157
+ * foreign-ASP guard). The deprecated-signer migration path widens this to
9158
+ * reach old-signer boarding addresses. The index-0 baseline and the
9159
+ * current display tapscript are always included regardless of the set.
8418
9160
  */
8419
- async getBoardingTapscripts() {
9161
+ async getBoardingTapscripts(allowedSigners) {
8420
9162
  const byScript = /* @__PURE__ */ new Map();
8421
9163
  const add = (s) => byScript.set(hex.encode(s.pkScript), s);
8422
9164
  const boardingCsv = this.boardingTapscript.options.csvTimelock ?? DefaultVtxo.Script.DEFAULT_TIMELOCK;
@@ -8429,11 +9171,12 @@ var ReadonlyWallet = class _ReadonlyWallet {
8429
9171
  );
8430
9172
  add(this.boardingTapscript);
8431
9173
  const serverPubKeyHex = hex.encode(this.boardingTapscript.options.serverPubKey);
9174
+ const allowed = allowedSigners ?? /* @__PURE__ */ new Set([toXOnlySignerHex(serverPubKeyHex)]);
8432
9175
  const boardingContracts = await this.contractRepository.getContracts({
8433
9176
  type: ["boarding"]
8434
9177
  });
8435
9178
  for (const c of boardingContracts) {
8436
- if (c.params.serverPubKey !== serverPubKeyHex) continue;
9179
+ if (!allowed.has(toXOnlySignerHex(c.params.serverPubKey))) continue;
8437
9180
  try {
8438
9181
  add(BoardingContractHandler.createScript(c.params));
8439
9182
  } catch (e) {
@@ -8443,23 +9186,61 @@ var ReadonlyWallet = class _ReadonlyWallet {
8443
9186
  return [...byScript.values()];
8444
9187
  }
8445
9188
  /**
8446
- * Fetch and cache onchain inputs (UTXOs) received at the wallet's boarding
8447
- * addresses the current address plus any historical rotated boarding
8448
- * addresses that still hold unspent UTXOs (plan §6-III.1). Each UTXO is
8449
- * annotated with the tapscript of the address it actually sits on, so the
8450
- * spending path forfeits / exits it with the correct per-index leaves.
9189
+ * Fetch and cache onchain inputs (UTXOs) received at the boarding addresses
9190
+ * of the given signer set, grouped per boarding address so the caller keeps
9191
+ * the address↔signer association that {@link ExtendedCoin} cannot carry
9192
+ * (it retains only the encoded leaves/tapTree the spend needs, not the
9193
+ * `DefaultVtxo.Script` and its `serverPubKey`/CSV delay).
9194
+ *
9195
+ * Per group it does exactly what {@link getBoardingUtxos} does per tapscript:
9196
+ * `getCoins` → {@link extendCoinWithTapscript} → `saveUtxos`. Offline-first:
9197
+ * it does not call `getInfo()`; the caller supplies the allowed signer set,
9198
+ * so the only network calls are the per-address `getCoins`.
9199
+ *
9200
+ * @param allowedSigners - x-only-hex server keys whose boarding addresses to
9201
+ * fetch (passed through to {@link getBoardingTapscripts}).
8451
9202
  */
8452
- async getBoardingUtxos() {
8453
- const tapscripts = await this.getBoardingTapscripts();
8454
- const all = [];
9203
+ async getBoardingUtxosForSigners(allowedSigners) {
9204
+ const tapscripts = await this.getBoardingTapscripts(allowedSigners);
9205
+ const groups = [];
8455
9206
  for (const tapscript of tapscripts) {
8456
9207
  const address = tapscript.onchainAddress(this.network);
8457
9208
  const coins = await this.onchainProvider.getCoins(address);
8458
9209
  const utxos = coins.map((utxo) => extendCoinWithTapscript(tapscript, utxo));
8459
9210
  await this.walletRepository.saveUtxos(address, utxos);
8460
- all.push(...utxos);
9211
+ groups.push({
9212
+ tapscript,
9213
+ // Normalize so the group key matches the axis/contract x-only
9214
+ // form regardless of how the tapscript's key was stored.
9215
+ serverPubKey: toXOnlySignerHex(hex.encode(tapscript.options.serverPubKey)),
9216
+ // Per-row CSV delay decoded from THIS tapscript's exit leaf —
9217
+ // not the wallet's current boarding timelock, which a signer
9218
+ // rotation may have changed.
9219
+ csvTimelock: CSVMultisigTapscript.decode(hex.decode(tapscript.exitScript)).params.timelock,
9220
+ coins: utxos
9221
+ });
8461
9222
  }
8462
- return all;
9223
+ return groups;
9224
+ }
9225
+ /**
9226
+ * Fetch and cache onchain inputs (UTXOs) received at the wallet's boarding
9227
+ * addresses — the current address plus any historical rotated boarding
9228
+ * addresses that still hold unspent UTXOs (plan §6-III.1). Each UTXO is
9229
+ * annotated with the tapscript of the address it actually sits on, so the
9230
+ * spending path forfeits / exits it with the correct per-index leaves.
9231
+ *
9232
+ * Current-signer only: a flatten of {@link getBoardingUtxosForSigners} over
9233
+ * the wallet's current signer, so the two paths cannot drift. Old-signer
9234
+ * boarding recovery goes through the deprecated-signer migration API
9235
+ * instead (it would otherwise pull EXPIRED-signer inputs into a plain
9236
+ * `settle()` that the server must reject).
9237
+ */
9238
+ async getBoardingUtxos() {
9239
+ const currentOnly = /* @__PURE__ */ new Set([
9240
+ toXOnlySignerHex(hex.encode(this.boardingTapscript.options.serverPubKey))
9241
+ ]);
9242
+ const groups = await this.getBoardingUtxosForSigners(currentOnly);
9243
+ return groups.flatMap((g) => g.coins);
8463
9244
  }
8464
9245
  /**
8465
9246
  * Subscribe to onchain and offchain notifications for newly received funds.
@@ -8666,64 +9447,82 @@ var ReadonlyWallet = class _ReadonlyWallet {
8666
9447
  watcherConfig: this.watcherConfig
8667
9448
  });
8668
9449
  const baselinePubkey = await this.identity.xOnlyPublicKey();
8669
- for (const csvTimelock of this.walletContractTimelocks) {
8670
- const csvTimelockStr = timelockToSequence(csvTimelock).toString();
8671
- const defaultScript = new DefaultVtxo.Script({
9450
+ const delegatePubKey = this.offchainTapscript instanceof DelegateVtxo.Script ? this.offchainTapscript.options.delegatePubKey : void 0;
9451
+ const baselineSigners = [
9452
+ this.offchainTapscript.options.serverPubKey,
9453
+ ...[...this._deprecatedSigners.keys()].map((h) => hex.decode(h))
9454
+ ];
9455
+ const seenBaselineScripts = /* @__PURE__ */ new Set();
9456
+ for (const serverPubKey of baselineSigners) {
9457
+ for (const csvTimelock of this.walletContractTimelocks) {
9458
+ const csvTimelockStr = timelockToSequence(csvTimelock).toString();
9459
+ const defaultScript = new DefaultVtxo.Script({
9460
+ pubKey: baselinePubkey,
9461
+ serverPubKey,
9462
+ csvTimelock
9463
+ });
9464
+ const defaultScriptHex = hex.encode(defaultScript.pkScript);
9465
+ if (!seenBaselineScripts.has(defaultScriptHex)) {
9466
+ seenBaselineScripts.add(defaultScriptHex);
9467
+ await ensureWalletContract(manager, {
9468
+ type: "default",
9469
+ params: {
9470
+ pubKey: hex.encode(defaultScript.options.pubKey),
9471
+ serverPubKey: hex.encode(serverPubKey),
9472
+ csvTimelock: csvTimelockStr
9473
+ },
9474
+ script: defaultScriptHex,
9475
+ address: defaultScript.address(this.network.hrp, serverPubKey).encode(),
9476
+ state: "active"
9477
+ });
9478
+ }
9479
+ if (delegatePubKey) {
9480
+ const delegateScript = new DelegateVtxo.Script({
9481
+ pubKey: baselinePubkey,
9482
+ serverPubKey,
9483
+ delegatePubKey,
9484
+ csvTimelock
9485
+ });
9486
+ const delegateScriptHex = hex.encode(delegateScript.pkScript);
9487
+ if (seenBaselineScripts.has(delegateScriptHex)) continue;
9488
+ seenBaselineScripts.add(delegateScriptHex);
9489
+ await manager.createContract({
9490
+ type: "delegate",
9491
+ params: {
9492
+ pubKey: hex.encode(delegateScript.options.pubKey),
9493
+ serverPubKey: hex.encode(serverPubKey),
9494
+ delegatePubKey: hex.encode(delegateScript.options.delegatePubKey),
9495
+ csvTimelock: csvTimelockStr
9496
+ },
9497
+ script: delegateScriptHex,
9498
+ address: delegateScript.address(this.network.hrp, serverPubKey).encode(),
9499
+ state: "active"
9500
+ });
9501
+ }
9502
+ }
9503
+ }
9504
+ const boardingCsvTimelock = this.boardingTapscript.options.csvTimelock ?? DefaultVtxo.Script.DEFAULT_TIMELOCK;
9505
+ for (const serverPubKey of baselineSigners) {
9506
+ const baselineBoarding = new DefaultVtxo.Script({
8672
9507
  pubKey: baselinePubkey,
8673
- serverPubKey: this.offchainTapscript.options.serverPubKey,
8674
- csvTimelock
9508
+ serverPubKey,
9509
+ csvTimelock: boardingCsvTimelock
8675
9510
  });
8676
- const defaultScriptHex = hex.encode(defaultScript.pkScript);
9511
+ const boardingScriptHex = hex.encode(baselineBoarding.pkScript);
9512
+ if (seenBaselineScripts.has(boardingScriptHex)) continue;
9513
+ seenBaselineScripts.add(boardingScriptHex);
8677
9514
  await ensureWalletContract(manager, {
8678
- type: "default",
9515
+ type: "boarding",
8679
9516
  params: {
8680
- pubKey: hex.encode(defaultScript.options.pubKey),
8681
- serverPubKey: hex.encode(defaultScript.options.serverPubKey),
8682
- csvTimelock: csvTimelockStr
9517
+ pubKey: hex.encode(baselineBoarding.options.pubKey),
9518
+ serverPubKey: hex.encode(serverPubKey),
9519
+ csvTimelock: timelockToSequence(boardingCsvTimelock).toString()
8683
9520
  },
8684
- script: defaultScriptHex,
8685
- address: defaultScript.address(this.network.hrp, this.arkServerPublicKey).encode(),
9521
+ script: boardingScriptHex,
9522
+ address: baselineBoarding.address(this.network.hrp, serverPubKey).encode(),
8686
9523
  state: "active"
8687
9524
  });
8688
- if (this.offchainTapscript instanceof DelegateVtxo.Script) {
8689
- const delegateScript = new DelegateVtxo.Script({
8690
- pubKey: baselinePubkey,
8691
- serverPubKey: this.offchainTapscript.options.serverPubKey,
8692
- delegatePubKey: this.offchainTapscript.options.delegatePubKey,
8693
- csvTimelock
8694
- });
8695
- const delegateScriptHex = hex.encode(delegateScript.pkScript);
8696
- await manager.createContract({
8697
- type: "delegate",
8698
- params: {
8699
- pubKey: hex.encode(delegateScript.options.pubKey),
8700
- serverPubKey: hex.encode(delegateScript.options.serverPubKey),
8701
- delegatePubKey: hex.encode(delegateScript.options.delegatePubKey),
8702
- csvTimelock: csvTimelockStr
8703
- },
8704
- script: delegateScriptHex,
8705
- address: delegateScript.address(this.network.hrp, this.arkServerPublicKey).encode(),
8706
- state: "active"
8707
- });
8708
- }
8709
9525
  }
8710
- const boardingCsvTimelock = this.boardingTapscript.options.csvTimelock ?? DefaultVtxo.Script.DEFAULT_TIMELOCK;
8711
- const baselineBoarding = new DefaultVtxo.Script({
8712
- pubKey: baselinePubkey,
8713
- serverPubKey: this.boardingTapscript.options.serverPubKey,
8714
- csvTimelock: boardingCsvTimelock
8715
- });
8716
- await ensureWalletContract(manager, {
8717
- type: "boarding",
8718
- params: {
8719
- pubKey: hex.encode(baselineBoarding.options.pubKey),
8720
- serverPubKey: hex.encode(baselineBoarding.options.serverPubKey),
8721
- csvTimelock: timelockToSequence(boardingCsvTimelock).toString()
8722
- },
8723
- script: hex.encode(baselineBoarding.pkScript),
8724
- address: baselineBoarding.address(this.network.hrp, this.arkServerPublicKey).encode(),
8725
- state: "active"
8726
- });
8727
9526
  return manager;
8728
9527
  }
8729
9528
  /** Dispose wallet-owned managers and release background resources. */
@@ -8756,7 +9555,6 @@ var Wallet2 = class _Wallet extends ReadonlyWallet {
8756
9555
  walletContractTimelocks
8757
9556
  );
8758
9557
  this.arkProvider = arkProvider;
8759
- this.serverUnrollScript = serverUnrollScript;
8760
9558
  this.forfeitOutputScript = forfeitOutputScript;
8761
9559
  this.forfeitPubkey = forfeitPubkey;
8762
9560
  this.identity = identity;
@@ -8777,6 +9575,7 @@ var Wallet2 = class _Wallet extends ReadonlyWallet {
8777
9575
  this.settlementConfig = { ...DEFAULT_SETTLEMENT_CONFIG };
8778
9576
  }
8779
9577
  this._delegateManager = delegateProvider ? new DelegateManagerImpl(delegateProvider, arkProvider, identity) : void 0;
9578
+ this._serverUnrollScript = serverUnrollScript;
8780
9579
  this._receiveRotator = receiveRotator;
8781
9580
  this._descriptorProvider = descriptorProvider;
8782
9581
  this._signerRouter = new InputSignerRouter({
@@ -8802,6 +9601,43 @@ var Wallet2 = class _Wallet extends ReadonlyWallet {
8802
9601
  * the contract manager is up first.
8803
9602
  */
8804
9603
  _receiveRotator;
9604
+ /**
9605
+ * Unsubscribe handle for the arkProvider's `onServerInfoChanged` stream
9606
+ * (mid-session signer-rotation detection). Torn down in {@link dispose}.
9607
+ */
9608
+ _serverInfoUnsub;
9609
+ /**
9610
+ * Tail of the serialized {@link handleServerInfoChanged} chain. Each
9611
+ * `onServerInfoChanged` event chains onto it so handlers run one at a time,
9612
+ * and {@link dispose} awaits it so an in-flight re-derive/rotation settles
9613
+ * before the contract manager is torn down underneath it.
9614
+ */
9615
+ _serverInfoInFlight = Promise.resolve();
9616
+ /**
9617
+ * React to a mid-session server-info change (driven by the arkProvider's
9618
+ * `DIGEST_MISMATCH` detection). First refresh the cached deprecated-signer
9619
+ * set so the boarding WATCH path immediately widens to the just-deprecated
9620
+ * signer, then — only if the active signer actually changed — rotate the
9621
+ * wallet onto it via {@link rotateServerSigner} (re-deriving the offchain +
9622
+ * boarding display tapscripts and registering the current-signer rows).
9623
+ * Old-signer rows stay active, so existing funds remain watched. Failures
9624
+ * are logged, never thrown back into the provider's emit loop.
9625
+ */
9626
+ async handleServerInfoChanged(info) {
9627
+ this.refreshDeprecatedSigners(info);
9628
+ try {
9629
+ const newActive = toXOnlySignerHex(info.signerPubkey);
9630
+ const current = toXOnlySignerHex(hex.encode(this.arkServerPublicKey));
9631
+ if (newActive !== current) {
9632
+ await this.rotateServerSigner(
9633
+ hex.decode(info.signerPubkey),
9634
+ info.checkpointTapscript
9635
+ );
9636
+ }
9637
+ } catch (e) {
9638
+ console.warn("server-signer rotation on info change failed", e);
9639
+ }
9640
+ }
8805
9641
  _receiveRotatorInstalled = false;
8806
9642
  /**
8807
9643
  * Descriptor-aware signer used by {@link _signerRouter} to sign
@@ -8831,6 +9667,44 @@ var Wallet2 = class _Wallet extends ReadonlyWallet {
8831
9667
  this._boardingTapscript = tapscript;
8832
9668
  this.notifyBoardingRotation();
8833
9669
  }
9670
+ /**
9671
+ * @internal Sole write path for `arkServerPublicKey` after construction.
9672
+ * Called by {@link Wallet.rotateServerSigner} once the rotated offchain and
9673
+ * boarding contract rows have been persisted. External code must treat
9674
+ * `arkServerPublicKey` as read-only.
9675
+ */
9676
+ setArkServerPublicKeyForRotation(serverPubKey) {
9677
+ this._arkServerPublicKey = serverPubKey;
9678
+ }
9679
+ /**
9680
+ * Output script for checkpoint transactions, decoded from the server's
9681
+ * `checkpointTapscript`. Server-controlled state: pinned at construction
9682
+ * and re-sourced from a fresh `ArkInfo` on server-signer rotation. Read it
9683
+ * through {@link serverUnrollScript}; write it only through
9684
+ * {@link setServerUnrollScriptForRotation}.
9685
+ */
9686
+ _serverUnrollScript;
9687
+ get serverUnrollScript() {
9688
+ return this._serverUnrollScript;
9689
+ }
9690
+ /**
9691
+ * @internal Sole write path for `serverUnrollScript` after construction.
9692
+ * Called by {@link Wallet._doRotateServerSigner} with the checkpoint script
9693
+ * sourced from the fresh `ArkInfo` that triggered the rotation, so the send
9694
+ * path builds checkpoints against the new server epoch. External code must
9695
+ * treat `serverUnrollScript` as read-only.
9696
+ */
9697
+ setServerUnrollScriptForRotation(script) {
9698
+ this._serverUnrollScript = script;
9699
+ }
9700
+ /**
9701
+ * Serializes {@link rotateServerSigner} for static / non-HD wallets (which
9702
+ * have no {@link WalletReceiveRotator} chain to ride). Coalesces concurrent
9703
+ * migration passes so two callers cannot both rebuild and swap the
9704
+ * tapscripts. HD wallets serialize on the rotator's chain instead, via
9705
+ * {@link WalletReceiveRotator.runExclusive}.
9706
+ */
9707
+ _serverRotationChain = Promise.resolve();
8834
9708
  /**
8835
9709
  * Allocate and return a *fresh* on-chain boarding address, rotating the
8836
9710
  * wallet's current boarding tapscript to a new HD index.
@@ -8887,6 +9761,126 @@ var Wallet2 = class _Wallet extends ReadonlyWallet {
8887
9761
  this.setBoardingTapscriptForRotation(newBoarding);
8888
9762
  return newBoarding.onchainAddress(this.network);
8889
9763
  }
9764
+ /**
9765
+ * Mid-session server-signer rotation (plan §4). When arkd rotates its
9766
+ * active signer mid-session — the case the long-lived service worker and
9767
+ * Expo background processes that own automatic migration must handle — a
9768
+ * wallet constructed before the rotation keeps deriving old-signer receive
9769
+ * addresses. Building a migration output to such an address would produce a
9770
+ * VTXO the server must reject, so the wallet must first re-derive its own
9771
+ * receive state under the new active signer.
9772
+ *
9773
+ * Follows the {@link WalletReceiveRotator.rotate} write-path pattern with
9774
+ * the server key swapped instead of the user key: build the new offchain
9775
+ * and boarding tapscripts locally (preserving every other option),
9776
+ * register the matching `default`/`delegate` and `boarding` contract rows
9777
+ * through {@link ContractManager.createContract}, and only then commit the
9778
+ * new tapscripts and server key to the wallet's visible state. The signing
9779
+ * metadata of the current receive/boarding rows is carried onto the new
9780
+ * rows so a rotated (descriptor-backed) receive pubkey can still sign.
9781
+ *
9782
+ * The old-signer contract rows are intentionally left `active` and watched
9783
+ * — they are exactly the deprecated-signer contracts the migration pass
9784
+ * drains. Idempotent: a no-op when the wallet already tracks `xonly`.
9785
+ *
9786
+ * Serialized against HD receive rotation so the two paths (both of which
9787
+ * rebuild and swap `offchainTapscript`) cannot interleave.
9788
+ *
9789
+ * @internal Invoked by the {@link VtxoManager} migration pass; not part of
9790
+ * the stable public API.
9791
+ */
9792
+ async rotateServerSigner(newServerPubKey, checkpointTapscript) {
9793
+ const xonly = toXOnlyPubKey(newServerPubKey);
9794
+ let newServerUnrollScript;
9795
+ try {
9796
+ newServerUnrollScript = CSVMultisigTapscript.decode(hex.decode(checkpointTapscript));
9797
+ } catch (e) {
9798
+ throw new Error("Invalid checkpointTapscript from server");
9799
+ }
9800
+ if (equalBytes$1(xonly, this.arkServerPublicKey)) return;
9801
+ if (this._receiveRotator) {
9802
+ await this._receiveRotator.runExclusive(
9803
+ () => this._doRotateServerSigner(xonly, newServerUnrollScript)
9804
+ );
9805
+ return;
9806
+ }
9807
+ const run = this._serverRotationChain.catch(() => void 0).then(() => this._doRotateServerSigner(xonly, newServerUnrollScript));
9808
+ this._serverRotationChain = run.then(
9809
+ () => void 0,
9810
+ () => void 0
9811
+ );
9812
+ return run;
9813
+ }
9814
+ async _doRotateServerSigner(xonly, newServerUnrollScript) {
9815
+ if (equalBytes$1(xonly, this.arkServerPublicKey)) return;
9816
+ const manager = await this.getContractManager();
9817
+ const [currentOffchainRow] = await manager.getContracts({
9818
+ script: this.defaultContractScript
9819
+ });
9820
+ const currentBoardingScript = hex.encode(this._boardingTapscript.pkScript);
9821
+ const [currentBoardingRow] = await manager.getContracts({
9822
+ script: currentBoardingScript
9823
+ });
9824
+ const newOffchain = this.offchainTapscript instanceof DelegateVtxo.Script ? new DelegateVtxo.Script({
9825
+ ...this.offchainTapscript.options,
9826
+ serverPubKey: xonly
9827
+ }) : new DefaultVtxo.Script({
9828
+ ...this.offchainTapscript.options,
9829
+ serverPubKey: xonly
9830
+ });
9831
+ const newBoarding = new DefaultVtxo.Script({
9832
+ ...this._boardingTapscript.options,
9833
+ serverPubKey: xonly
9834
+ });
9835
+ const offchainCsv = timelockToSequence(newOffchain.options.csvTimelock).toString();
9836
+ const newOffchainScript = hex.encode(newOffchain.pkScript);
9837
+ const newOffchainAddress = newOffchain.address(this.network.hrp, xonly).encode();
9838
+ if (newOffchain instanceof DelegateVtxo.Script) {
9839
+ await manager.createContract({
9840
+ type: "delegate",
9841
+ params: {
9842
+ pubKey: hex.encode(newOffchain.options.pubKey),
9843
+ serverPubKey: hex.encode(xonly),
9844
+ delegatePubKey: hex.encode(newOffchain.options.delegatePubKey),
9845
+ csvTimelock: offchainCsv
9846
+ },
9847
+ script: newOffchainScript,
9848
+ address: newOffchainAddress,
9849
+ state: "active",
9850
+ metadata: currentOffchainRow?.metadata
9851
+ });
9852
+ } else {
9853
+ await manager.createContract({
9854
+ type: "default",
9855
+ params: {
9856
+ pubKey: hex.encode(newOffchain.options.pubKey),
9857
+ serverPubKey: hex.encode(xonly),
9858
+ csvTimelock: offchainCsv
9859
+ },
9860
+ script: newOffchainScript,
9861
+ address: newOffchainAddress,
9862
+ state: "active",
9863
+ metadata: currentOffchainRow?.metadata
9864
+ });
9865
+ }
9866
+ const boardingCsv = newBoarding.options.csvTimelock ?? DefaultVtxo.Script.DEFAULT_TIMELOCK;
9867
+ await manager.createContract({
9868
+ type: "boarding",
9869
+ params: {
9870
+ pubKey: hex.encode(newBoarding.options.pubKey),
9871
+ serverPubKey: hex.encode(xonly),
9872
+ csvTimelock: timelockToSequence(boardingCsv).toString()
9873
+ },
9874
+ script: hex.encode(newBoarding.pkScript),
9875
+ address: newBoarding.address(this.network.hrp, xonly).encode(),
9876
+ state: "active",
9877
+ metadata: currentBoardingRow?.metadata
9878
+ });
9879
+ this.setOffchainTapscriptForRotation(newOffchain);
9880
+ this.setBoardingTapscriptForRotation(newBoarding);
9881
+ this.setArkServerPublicKeyForRotation(xonly);
9882
+ this.setServerUnrollScriptForRotation(newServerUnrollScript);
9883
+ }
8890
9884
  /**
8891
9885
  * Async mutex that serializes all operations submitting VTXOs to the Arkade
8892
9886
  * server (`settle`, `send`, `sendBitcoin`). This prevents VtxoManager's
@@ -9047,6 +10041,9 @@ var Wallet2 = class _Wallet extends ReadonlyWallet {
9047
10041
  }
9048
10042
  async dispose() {
9049
10043
  await this._restoreInFlight?.catch(() => void 0);
10044
+ this._serverInfoUnsub?.();
10045
+ this._serverInfoUnsub = void 0;
10046
+ await this._serverInfoInFlight?.catch(() => void 0);
9050
10047
  let rotatorError;
9051
10048
  try {
9052
10049
  await this._receiveRotator?.dispose();
@@ -9121,6 +10118,15 @@ var Wallet2 = class _Wallet extends ReadonlyWallet {
9121
10118
  boot?.rotator,
9122
10119
  boot?.provider
9123
10120
  );
10121
+ wallet.refreshDeprecatedSigners(setup.info);
10122
+ {
10123
+ const ap = setup.arkProvider;
10124
+ if (typeof ap.onServerInfoChanged === "function") {
10125
+ wallet._serverInfoUnsub = ap.onServerInfoChanged((info) => {
10126
+ wallet._serverInfoInFlight = wallet._serverInfoInFlight.then(() => wallet.handleServerInfoChanged(info)).catch(() => void 0);
10127
+ });
10128
+ }
10129
+ }
9124
10130
  if (boot?.provider) {
9125
10131
  const resolvedBoarding = await resolveBoardingBootTapscript(
9126
10132
  setup.contractRepository,
@@ -9153,7 +10159,7 @@ var Wallet2 = class _Wallet extends ReadonlyWallet {
9153
10159
  */
9154
10160
  async toReadonly() {
9155
10161
  const readonlyIdentity = hasToReadonly(this.identity) ? await this.identity.toReadonly() : this.identity;
9156
- return new ReadonlyWallet(
10162
+ const readonly = new ReadonlyWallet(
9157
10163
  readonlyIdentity,
9158
10164
  this.network,
9159
10165
  this.onchainProvider,
@@ -9168,6 +10174,8 @@ var Wallet2 = class _Wallet extends ReadonlyWallet {
9168
10174
  this.watcherConfig,
9169
10175
  this.walletContractTimelocks
9170
10176
  );
10177
+ readonly._deprecatedSigners = new Map(this._deprecatedSigners);
10178
+ return readonly;
9171
10179
  }
9172
10180
  /** Returns the delegate manager when delegation support is configured. */
9173
10181
  async getDelegateManager() {
@@ -9193,10 +10201,9 @@ var Wallet2 = class _Wallet extends ReadonlyWallet {
9193
10201
  if (params.selectedVtxos && params.selectedVtxos.length > 0) {
9194
10202
  return this._withTxLock(async () => {
9195
10203
  const offchainTapscript = this.offchainTapscript;
9196
- const arkAddress = offchainTapscript.address(
9197
- this.network.hrp,
9198
- this.arkServerPublicKey
9199
- );
10204
+ const serverPubKey = this.arkServerPublicKey;
10205
+ const serverUnrollScript = this.serverUnrollScript;
10206
+ const arkAddress = offchainTapscript.address(this.network.hrp, serverPubKey);
9200
10207
  const selectedVtxoSum = params.selectedVtxos.map((v) => v.value).reduce((a, b) => a + b, 0);
9201
10208
  if (selectedVtxoSum < params.amount) {
9202
10209
  throw new Error("Selected VTXOs do not cover specified amount");
@@ -9221,25 +10228,14 @@ var Wallet2 = class _Wallet extends ReadonlyWallet {
9221
10228
  amount: BigInt(selected.changeAmount)
9222
10229
  });
9223
10230
  }
9224
- this._addPendingSpends(selected.inputs);
9225
- try {
9226
- const { arkTxid, signedCheckpointTxs } = await this.buildAndSubmitOffchainTx(
9227
- selected.inputs,
9228
- outputs
9229
- );
9230
- await this.updateDbAfterOffchainTx(
9231
- selected.inputs,
9232
- arkTxid,
9233
- signedCheckpointTxs,
9234
- params.amount,
9235
- selected.changeAmount,
9236
- selected.changeAmount > 0n ? outputs.length - 1 : 0,
9237
- offchainTapscript
9238
- );
9239
- return arkTxid;
9240
- } finally {
9241
- this._removePendingSpends(selected.inputs);
9242
- }
10231
+ return this._submitOffchainSpend(selected.inputs, outputs, {
10232
+ sentAmount: params.amount,
10233
+ changeAmount: selected.changeAmount,
10234
+ changeVout: selected.changeAmount > 0n ? outputs.length - 1 : 0,
10235
+ offchainTapscript,
10236
+ serverPubKey,
10237
+ serverUnrollScript
10238
+ });
9243
10239
  });
9244
10240
  }
9245
10241
  return this.send({
@@ -9269,8 +10265,11 @@ var Wallet2 = class _Wallet extends ReadonlyWallet {
9269
10265
  }
9270
10266
  }
9271
10267
  }
10268
+ const offchainAddress = await this.getAddress();
10269
+ const offchainPkScript = ArkAddress.decode(offchainAddress).pkScript;
10270
+ const offchainOutputScript = hex.encode(offchainPkScript);
9272
10271
  if (!params) {
9273
- const { fees } = await this.arkProvider.getInfo();
10272
+ const { fees, vtxoMaxAmount } = await this.arkProvider.getInfo();
9274
10273
  const estimator = new Estimator(fees.intentFee);
9275
10274
  let amount = 0;
9276
10275
  const exitScript = CSVMultisigTapscript.decode(
@@ -9312,20 +10311,31 @@ var Wallet2 = class _Wallet extends ReadonlyWallet {
9312
10311
  if (inputFee.satoshis >= vtxo.value) {
9313
10312
  continue;
9314
10313
  }
10314
+ const net = vtxo.value - inputFee.satoshis;
10315
+ if (vtxoMaxAmount >= 0n) {
10316
+ const projectedAmount = BigInt(amount + net);
10317
+ const projectedOutputFee = estimator.evalOffchainOutput({
10318
+ amount: projectedAmount,
10319
+ script: offchainOutputScript
10320
+ });
10321
+ if (projectedAmount - BigInt(projectedOutputFee.satoshis) > vtxoMaxAmount) {
10322
+ continue;
10323
+ }
10324
+ }
9315
10325
  filteredVtxos.push(vtxo);
9316
- amount += vtxo.value - inputFee.satoshis;
10326
+ amount += net;
9317
10327
  }
9318
10328
  const inputs = [...filteredBoardingUtxos, ...filteredVtxos];
9319
10329
  if (inputs.length === 0) {
9320
10330
  throw new Error("No inputs found");
9321
10331
  }
9322
10332
  const output = {
9323
- address: await this.getAddress(),
10333
+ address: offchainAddress,
9324
10334
  amount: BigInt(amount)
9325
10335
  };
9326
10336
  const outputFee = estimator.evalOffchainOutput({
9327
10337
  amount: output.amount,
9328
- script: hex.encode(ArkAddress.decode(output.address).pkScript)
10338
+ script: offchainOutputScript
9329
10339
  });
9330
10340
  output.amount -= BigInt(outputFee.satoshis);
9331
10341
  if (output.amount <= this.dustAmount) {
@@ -9365,8 +10375,7 @@ var Wallet2 = class _Wallet extends ReadonlyWallet {
9365
10375
  }
9366
10376
  }
9367
10377
  let outputAssets;
9368
- const destinationScript = ArkAddress.decode(await this.getAddress()).pkScript;
9369
- const assetOutputIndex = findDestinationOutputIndex(outputs, destinationScript);
10378
+ const assetOutputIndex = findDestinationOutputIndex(outputs, offchainPkScript);
9370
10379
  if (assetInputs.size > 0) {
9371
10380
  if (assetOutputIndex === -1) {
9372
10381
  throw new Error("Cannot assign assets: no output matches the destination address");
@@ -9905,12 +10914,16 @@ var Wallet2 = class _Wallet extends ReadonlyWallet {
9905
10914
  throw new Error("At least one receiver is required");
9906
10915
  }
9907
10916
  const offchainTapscript = this.offchainTapscript;
9908
- const outputAddress = offchainTapscript.address(this.network.hrp, this.arkServerPublicKey);
10917
+ const serverPubKey = this.arkServerPublicKey;
10918
+ const serverUnrollScript = this.serverUnrollScript;
10919
+ const outputAddress = offchainTapscript.address(this.network.hrp, serverPubKey);
9909
10920
  const address = outputAddress.encode();
9910
10921
  const recipients = validateRecipients(args, Number(this.dustAmount));
9911
- const virtualCoins = await this.getVtxos({
10922
+ const allVirtualCoins = await this.getVtxos({
9912
10923
  withRecoverable: false
9913
10924
  });
10925
+ const pendingRecovery = await this.pendingRecoveryOutpoints();
10926
+ const virtualCoins = pendingRecovery.size ? allVirtualCoins.filter((c) => !pendingRecovery.has(`${c.txid}:${c.vout}`)) : allVirtualCoins;
9914
10927
  const assetChanges = /* @__PURE__ */ new Map();
9915
10928
  let selectedCoins = [];
9916
10929
  let btcAmountToSelect = 0;
@@ -10032,33 +11045,128 @@ var Wallet2 = class _Wallet extends ReadonlyWallet {
10032
11045
  outputs.push(Extension.create([assetPacket]).txOut());
10033
11046
  }
10034
11047
  const sentAmount = recipients.reduce((sum, r) => sum + r.amount, 0);
10035
- this._addPendingSpends(selectedCoins);
11048
+ return this._submitOffchainSpend(selectedCoins, outputs, {
11049
+ sentAmount,
11050
+ changeAmount: BigInt(changeAmount),
11051
+ changeVout: changeReceiver ? changeIndex : 0,
11052
+ offchainTapscript,
11053
+ serverPubKey,
11054
+ serverUnrollScript,
11055
+ changeAssets: changeReceiver?.assets
11056
+ });
11057
+ }
11058
+ /**
11059
+ * Shared tail of every Ark-transaction spend path (`send`, selected-VTXO
11060
+ * `sendBitcoin`, and {@link sendSelectedVtxosToSelf}): hide the inputs from
11061
+ * concurrent `getVtxos()`, build+submit the offchain tx, persist the spent
11062
+ * inputs and any wallet-owned (change / self) output, then release the
11063
+ * pending-spend hold. Callers own coin selection, output construction, and
11064
+ * the synchronous epoch snapshot; this owns the submit/persist sequence.
11065
+ */
11066
+ async _submitOffchainSpend(inputs, outputs, persist) {
11067
+ this._addPendingSpends(inputs);
10036
11068
  try {
10037
11069
  const { arkTxid, signedCheckpointTxs } = await this.buildAndSubmitOffchainTx(
10038
- selectedCoins,
10039
- outputs
11070
+ inputs,
11071
+ outputs,
11072
+ persist.serverUnrollScript
10040
11073
  );
10041
11074
  await this.updateDbAfterOffchainTx(
10042
- selectedCoins,
11075
+ inputs,
10043
11076
  arkTxid,
10044
11077
  signedCheckpointTxs,
10045
- sentAmount,
10046
- BigInt(changeAmount),
10047
- changeReceiver ? changeIndex : 0,
10048
- offchainTapscript,
10049
- changeReceiver?.assets
11078
+ persist.sentAmount,
11079
+ persist.changeAmount,
11080
+ persist.changeVout,
11081
+ persist.offchainTapscript,
11082
+ persist.serverPubKey,
11083
+ persist.changeAssets,
11084
+ persist.recordSentHistory ?? true
10050
11085
  );
10051
11086
  return arkTxid;
10052
11087
  } finally {
10053
- this._removePendingSpends(selectedCoins);
10054
- }
11088
+ this._removePendingSpends(inputs);
11089
+ }
11090
+ }
11091
+ /**
11092
+ * @internal Migration primitive (deprecated-signer plan, step 1). Spend an
11093
+ * explicit set of the wallet's own deprecated-signer VTXOs into a single
11094
+ * full-value output on the wallet's *active* signer, through the Ark send
11095
+ * path (not `settle`) so arkd builds checkpoints against the active server
11096
+ * epoch. Consumed in-process by {@link VtxoManager}'s migration pass; not
11097
+ * part of the public `IWallet` API and never accepts boarding `ExtendedCoin`
11098
+ * inputs.
11099
+ *
11100
+ * The caller (`migrateCore`) must have already moved the wallet onto the
11101
+ * active signer (`ensureReceiveOnActiveSigner`) and sized the batch (caps +
11102
+ * dust floor); this method validates the inputs, preserves all input assets
11103
+ * on the self output, and persists the new active-signer VTXO even though
11104
+ * there is no separate change output. It records no `TxSent` history — the
11105
+ * funds never leave the wallet.
11106
+ */
11107
+ async sendSelectedVtxosToSelf(inputs) {
11108
+ if (inputs.length === 0) {
11109
+ throw new Error("sendSelectedVtxosToSelf: no inputs");
11110
+ }
11111
+ return this._withTxLock(async () => {
11112
+ const offchainTapscript = this.offchainTapscript;
11113
+ const serverPubKey = this.arkServerPublicKey;
11114
+ const serverUnrollScript = this.serverUnrollScript;
11115
+ const arkAddress = offchainTapscript.address(this.network.hrp, serverPubKey);
11116
+ for (const input of inputs) {
11117
+ if (!isSpendable(input) || isRecoverable(input)) {
11118
+ throw new Error(
11119
+ `sendSelectedVtxosToSelf: input ${input.txid}:${input.vout} is not cooperatively spendable`
11120
+ );
11121
+ }
11122
+ if (!input.virtualStatus.batchExpiry) {
11123
+ throw new Error(
11124
+ `sendSelectedVtxosToSelf: input ${input.txid}:${input.vout} has no batchExpiry`
11125
+ );
11126
+ }
11127
+ }
11128
+ const total = inputs.reduce((sum, c) => sum + BigInt(c.value), 0n);
11129
+ const outputs = [
11130
+ {
11131
+ script: total < this.dustAmount ? arkAddress.subdustPkScript : arkAddress.pkScript,
11132
+ amount: total
11133
+ }
11134
+ ];
11135
+ const assetInputs = selectedCoinsToAssetInputs(inputs);
11136
+ let selfAssets;
11137
+ if (assetInputs.size > 0) {
11138
+ const totals = /* @__PURE__ */ new Map();
11139
+ for (const [, assets] of assetInputs) {
11140
+ for (const a of assets) {
11141
+ totals.set(a.assetId, (totals.get(a.assetId) ?? 0n) + a.amount);
11142
+ }
11143
+ }
11144
+ selfAssets = [...totals].map(([assetId, amount]) => ({ assetId, amount }));
11145
+ const selfReceiver = {
11146
+ address: arkAddress.encode(),
11147
+ assets: selfAssets
11148
+ };
11149
+ const packet = createAssetPacket(assetInputs, [], selfReceiver);
11150
+ outputs.push(Extension.create([packet]).txOut());
11151
+ }
11152
+ return this._submitOffchainSpend(inputs, outputs, {
11153
+ sentAmount: 0,
11154
+ changeAmount: total,
11155
+ changeVout: 0,
11156
+ offchainTapscript,
11157
+ serverPubKey,
11158
+ changeAssets: selfAssets,
11159
+ recordSentHistory: false,
11160
+ serverUnrollScript
11161
+ });
11162
+ });
10055
11163
  }
10056
11164
  /**
10057
11165
  * Build an offchain transaction from the given inputs and outputs,
10058
11166
  * sign it, submit to the Arkade provider, and finalize.
10059
11167
  * @returns The Arkade transaction id and server-signed checkpoint PSBTs (for bookkeeping)
10060
11168
  */
10061
- async buildAndSubmitOffchainTx(inputs, outputs) {
11169
+ async buildAndSubmitOffchainTx(inputs, outputs, serverUnrollScript = this.serverUnrollScript) {
10062
11170
  const offchainTx = buildOffchainTx(
10063
11171
  inputs.map((input) => {
10064
11172
  return {
@@ -10067,7 +11175,7 @@ var Wallet2 = class _Wallet extends ReadonlyWallet {
10067
11175
  };
10068
11176
  }),
10069
11177
  outputs,
10070
- this.serverUnrollScript
11178
+ serverUnrollScript
10071
11179
  );
10072
11180
  const arkTxJobs = inputs.map((input, index) => ({
10073
11181
  index,
@@ -10141,14 +11249,14 @@ var Wallet2 = class _Wallet extends ReadonlyWallet {
10141
11249
  return { arkTxid, signedCheckpointTxs };
10142
11250
  }
10143
11251
  // mark virtual outputs as spent, save change outputs if any.
10144
- // `offchainTapscript` is the snapshot the caller captured under
10145
- // `_txLock` before any `await`; deriving both the change-VTXO
10146
- // metadata and `primaryAddress` from it here guarantees the local
10147
- // record matches the pkScript the server saw on the inbound
10148
- // transaction, even if `WalletReceiveRotator.rotate` swaps
10149
- // `this.offchainTapscript` mid-flight.
10150
- async updateDbAfterOffchainTx(inputs, arkTxid, signedCheckpointTxs, sentAmount, changeAmount, changeVout, offchainTapscript, changeAssets) {
10151
- const primaryAddress = offchainTapscript.address(this.network.hrp, this.arkServerPublicKey).encode();
11252
+ // `offchainTapscript` and `serverPubKey` are the epoch snapshot the
11253
+ // caller captured under `_txLock` before any `await`; deriving both the
11254
+ // change-VTXO metadata and `primaryAddress` from them here guarantees the
11255
+ // local record matches the address/pkScript the server saw on the inbound
11256
+ // transaction, even if `rotateServerSigner` swaps `this.offchainTapscript`
11257
+ // / `this.arkServerPublicKey` mid-flight.
11258
+ async updateDbAfterOffchainTx(inputs, arkTxid, signedCheckpointTxs, sentAmount, changeAmount, changeVout, offchainTapscript, serverPubKey, changeAssets, recordSentHistory = true) {
11259
+ const primaryAddress = offchainTapscript.address(this.network.hrp, serverPubKey).encode();
10152
11260
  try {
10153
11261
  const spentVtxos = [];
10154
11262
  const commitmentTxIds = /* @__PURE__ */ new Set();
@@ -10255,19 +11363,21 @@ var Wallet2 = class _Wallet extends ReadonlyWallet {
10255
11363
  [changeVtxo]
10256
11364
  );
10257
11365
  }
10258
- await this.walletRepository.saveTransactions(primaryAddress, [
10259
- {
10260
- key: {
10261
- boardingTxid: "",
10262
- commitmentTxid: "",
10263
- arkTxid
10264
- },
10265
- amount: sentAmount,
10266
- type: "SENT" /* TxSent */,
10267
- settled: false,
10268
- createdAt
10269
- }
10270
- ]);
11366
+ if (recordSentHistory) {
11367
+ await this.walletRepository.saveTransactions(primaryAddress, [
11368
+ {
11369
+ key: {
11370
+ boardingTxid: "",
11371
+ commitmentTxid: "",
11372
+ arkTxid
11373
+ },
11374
+ amount: sentAmount,
11375
+ type: "SENT" /* TxSent */,
11376
+ settled: false,
11377
+ createdAt
11378
+ }
11379
+ ]);
11380
+ }
10271
11381
  } catch (e) {
10272
11382
  console.warn("error saving offchain tx to repository", e);
10273
11383
  throw e;
@@ -11125,6 +12235,82 @@ var DelegateNotConfiguredError = class extends Error {
11125
12235
  };
11126
12236
  var DelegatorNotConfiguredError = DelegateNotConfiguredError;
11127
12237
  var DEFAULT_MESSAGE_TAG = "WALLET_UPDATER";
12238
+ var serializeMigrationVtxoRef = (ref) => ({
12239
+ txid: ref.txid,
12240
+ vout: ref.vout,
12241
+ value: ref.value,
12242
+ signerPubKey: ref.signerPubKey,
12243
+ cutoffDate: ref.cutoffDate?.toString()
12244
+ });
12245
+ var deserializeMigrationVtxoRef = (ref) => ({
12246
+ txid: ref.txid,
12247
+ vout: ref.vout,
12248
+ value: ref.value,
12249
+ signerPubKey: ref.signerPubKey,
12250
+ cutoffDate: ref.cutoffDate != null ? BigInt(ref.cutoffDate) : void 0
12251
+ });
12252
+ var serializeDeprecatedSignerReport = (report) => ({
12253
+ signerPubKey: report.signerPubKey,
12254
+ status: report.status,
12255
+ cutoffDate: report.cutoffDate?.toString(),
12256
+ secondsUntilCutoff: report.secondsUntilCutoff,
12257
+ vtxoCount: report.vtxoCount,
12258
+ totalValue: report.totalValue,
12259
+ boardingCount: report.boardingCount,
12260
+ boardingValue: report.boardingValue,
12261
+ recoverableCount: report.recoverableCount,
12262
+ recoverableValue: report.recoverableValue,
12263
+ awaitingSweepCount: report.awaitingSweepCount,
12264
+ awaitingSweepValue: report.awaitingSweepValue,
12265
+ nextSweepEta: report.nextSweepEta
12266
+ });
12267
+ var deserializeDeprecatedSignerReport = (report) => ({
12268
+ signerPubKey: report.signerPubKey,
12269
+ status: report.status,
12270
+ cutoffDate: report.cutoffDate != null ? BigInt(report.cutoffDate) : void 0,
12271
+ secondsUntilCutoff: report.secondsUntilCutoff,
12272
+ vtxoCount: report.vtxoCount,
12273
+ totalValue: report.totalValue,
12274
+ boardingCount: report.boardingCount,
12275
+ boardingValue: report.boardingValue,
12276
+ recoverableCount: report.recoverableCount,
12277
+ recoverableValue: report.recoverableValue,
12278
+ awaitingSweepCount: report.awaitingSweepCount,
12279
+ awaitingSweepValue: report.awaitingSweepValue,
12280
+ nextSweepEta: report.nextSweepEta
12281
+ });
12282
+ var serializeMigrationLegReport = (leg) => ({
12283
+ txid: leg.txid,
12284
+ migrated: leg.migrated.map(serializeMigrationVtxoRef),
12285
+ skipped: leg.skipped,
12286
+ deferred: leg.deferred,
12287
+ oversized: leg.oversized?.map(serializeMigrationVtxoRef),
12288
+ error: leg.error
12289
+ });
12290
+ var deserializeMigrationLegReport = (leg) => ({
12291
+ txid: leg.txid,
12292
+ migrated: leg.migrated.map(deserializeMigrationVtxoRef),
12293
+ skipped: leg.skipped,
12294
+ deferred: leg.deferred,
12295
+ oversized: leg.oversized?.map(deserializeMigrationVtxoRef),
12296
+ error: leg.error
12297
+ });
12298
+ var serializeMigrationReport = (report) => ({
12299
+ rotated: report.rotated,
12300
+ skipped: report.skipped,
12301
+ vtxos: report.vtxos ? serializeMigrationLegReport(report.vtxos) : void 0,
12302
+ boarding: report.boarding ? serializeMigrationLegReport(report.boarding) : void 0,
12303
+ expired: report.expired.map(serializeMigrationVtxoRef),
12304
+ signers: report.signers.map(serializeDeprecatedSignerReport)
12305
+ });
12306
+ var deserializeMigrationReport = (report) => ({
12307
+ rotated: report.rotated,
12308
+ skipped: report.skipped,
12309
+ vtxos: report.vtxos ? deserializeMigrationLegReport(report.vtxos) : void 0,
12310
+ boarding: report.boarding ? deserializeMigrationLegReport(report.boarding) : void 0,
12311
+ expired: report.expired.map(deserializeMigrationVtxoRef),
12312
+ signers: report.signers.map(deserializeDeprecatedSignerReport)
12313
+ });
11128
12314
  var WalletMessageHandler = class {
11129
12315
  messageTag;
11130
12316
  wallet;
@@ -11206,7 +12392,9 @@ var WalletMessageHandler = class {
11206
12392
  // page-side PING / MESSAGE_BUS_NOT_INITIALIZED path triggered by concurrent
11207
12393
  // short requests (GET_STATUS, GET_BALANCE, ...).
11208
12394
  isLongRunning(message) {
11209
- return message.type === "SETTLE" || message.type === "RECOVER_VTXOS" || message.type === "RENEW_VTXOS" || // HD restore walks the index range with one indexer round-trip per
12395
+ return message.type === "SETTLE" || message.type === "RECOVER_VTXOS" || message.type === "RENEW_VTXOS" || // Migration may apply a server-signer rotation and then run a full
12396
+ // settle, so it streams settlement events like RENEW_VTXOS.
12397
+ message.type === "MIGRATE_DEPRECATED_SIGNER_VTXOS" || // HD restore walks the index range with one indexer round-trip per
11210
12398
  // step until it hits gapLimit consecutive unused indices. The bus
11211
12399
  // deadline must not race the scan; liveness stays covered by PING.
11212
12400
  message.type === "RESTORE_WALLET";
@@ -11572,6 +12760,36 @@ var WalletMessageHandler = class {
11572
12760
  payload: { txid }
11573
12761
  });
11574
12762
  }
12763
+ case "MIGRATE_DEPRECATED_SIGNER_VTXOS": {
12764
+ const wallet = this.requireWallet();
12765
+ const vtxoManager = await wallet.getVtxoManager();
12766
+ const report = await vtxoManager.migrateDeprecatedSignerVtxos({
12767
+ eventCallback: (e) => {
12768
+ this.scheduleForNextTick(
12769
+ () => this.tagged({
12770
+ id,
12771
+ type: "MIGRATE_DEPRECATED_SIGNER_VTXOS_EVENT",
12772
+ payload: e
12773
+ })
12774
+ );
12775
+ }
12776
+ });
12777
+ return this.tagged({
12778
+ id,
12779
+ type: "MIGRATE_DEPRECATED_SIGNER_VTXOS_SUCCESS",
12780
+ payload: { report: serializeMigrationReport(report) }
12781
+ });
12782
+ }
12783
+ case "GET_DEPRECATED_SIGNER_STATUS": {
12784
+ const wallet = this.requireWallet();
12785
+ const vtxoManager = await wallet.getVtxoManager();
12786
+ const signers = await vtxoManager.getDeprecatedSignerStatus();
12787
+ return this.tagged({
12788
+ id,
12789
+ type: "DEPRECATED_SIGNER_STATUS",
12790
+ payload: { signers: signers.map(serializeDeprecatedSignerReport) }
12791
+ });
12792
+ }
11575
12793
  case "RESTORE_WALLET": {
11576
12794
  const wallet = this.requireWallet();
11577
12795
  try {
@@ -11605,9 +12823,10 @@ var WalletMessageHandler = class {
11605
12823
  await this.onWalletInitialized();
11606
12824
  }
11607
12825
  async handleGetBalance() {
11608
- const [boardingUtxos, allVtxos] = await Promise.all([
12826
+ const [boardingUtxos, allVtxos, pendingOutpoints] = await Promise.all([
11609
12827
  this.getAllBoardingUtxos(),
11610
- this.getVtxosFromRepo()
12828
+ this.getVtxosFromRepo(),
12829
+ this.readonlyWallet ? this.readonlyWallet.pendingRecoveryOutpoints() : Promise.resolve(/* @__PURE__ */ new Set())
11611
12830
  ]);
11612
12831
  let confirmed = 0;
11613
12832
  let unconfirmed = 0;
@@ -11623,8 +12842,11 @@ var WalletMessageHandler = class {
11623
12842
  let settled = 0;
11624
12843
  let preconfirmed = 0;
11625
12844
  let recoverable = 0;
12845
+ let pendingRecovery = 0;
11626
12846
  for (const vtxo of spendableVtxos) {
11627
- if (vtxo.virtualStatus.state === "settled") {
12847
+ if (pendingOutpoints.has(`${vtxo.txid}:${vtxo.vout}`)) {
12848
+ pendingRecovery += vtxo.value;
12849
+ } else if (vtxo.virtualStatus.state === "settled") {
11628
12850
  settled += vtxo.value;
11629
12851
  } else if (vtxo.virtualStatus.state === "preconfirmed") {
11630
12852
  preconfirmed += vtxo.value;
@@ -11636,7 +12858,7 @@ var WalletMessageHandler = class {
11636
12858
  }
11637
12859
  }
11638
12860
  const totalBoarding = confirmed + unconfirmed;
11639
- const totalOffchain = settled + preconfirmed + recoverable;
12861
+ const totalOffchain = settled + preconfirmed + recoverable + pendingRecovery;
11640
12862
  const assetBalances = /* @__PURE__ */ new Map();
11641
12863
  for (const vtxo of spendableVtxos) {
11642
12864
  if (vtxo.assets) {
@@ -11660,6 +12882,7 @@ var WalletMessageHandler = class {
11660
12882
  preconfirmed,
11661
12883
  available: settled + preconfirmed,
11662
12884
  recoverable,
12885
+ pendingRecovery,
11663
12886
  total: totalBoarding + totalOffchain,
11664
12887
  assets
11665
12888
  };
@@ -12045,6 +13268,7 @@ var DEFAULT_MESSAGE_TIMEOUTS = {
12045
13268
  GET_EXPIRING_VTXOS: 2e4,
12046
13269
  GET_EXPIRED_BOARDING_UTXOS: 2e4,
12047
13270
  GET_RECOVERABLE_BALANCE: 2e4,
13271
+ GET_DEPRECATED_SIGNER_STATUS: 2e4,
12048
13272
  RELOAD_WALLET: 2e4,
12049
13273
  // Transactions — need more headroom.
12050
13274
  // SETTLE / RECOVER_VTXOS / RENEW_VTXOS go through the streaming path and
@@ -12060,6 +13284,9 @@ var DEFAULT_MESSAGE_TIMEOUTS = {
12060
13284
  RECOVER_VTXOS: 5e4,
12061
13285
  RENEW_VTXOS: 5e4,
12062
13286
  SWEEP_EXPIRED_BOARDING_UTXOS: 5e4,
13287
+ // Streaming/long-running like RENEW_VTXOS (rotation + settle); the value is
13288
+ // kept for type completeness and is never enforced as an inactivity deadline.
13289
+ MIGRATE_DEPRECATED_SIGNER_VTXOS: 5e4,
12063
13290
  // RESTORE_WALLET is a streaming/long-running path (sendMessageWithEvents)
12064
13291
  // like SETTLE; the value here is kept for type completeness and is never
12065
13292
  // enforced as an inactivity deadline.
@@ -12085,6 +13312,7 @@ var DEDUPABLE_REQUEST_TYPES = /* @__PURE__ */ new Set([
12085
13312
  "GET_DELEGATE_INFO",
12086
13313
  "GET_RECOVERABLE_BALANCE",
12087
13314
  "GET_EXPIRED_BOARDING_UTXOS",
13315
+ "GET_DEPRECATED_SIGNER_STATUS",
12088
13316
  "GET_VTXOS",
12089
13317
  "GET_CONTRACTS",
12090
13318
  "GET_CONTRACTS_WITH_VTXOS",
@@ -13207,6 +14435,42 @@ var ServiceWorkerWallet = class _ServiceWorkerWallet extends ServiceWorkerReadon
13207
14435
  throw new Error(`Failed to sweep expired boarding utxos: ${e}`);
13208
14436
  }
13209
14437
  },
14438
+ async migrateDeprecatedSignerVtxos(options) {
14439
+ const message = {
14440
+ tag: messageTag,
14441
+ type: "MIGRATE_DEPRECATED_SIGNER_VTXOS",
14442
+ id: getRandomId()
14443
+ };
14444
+ try {
14445
+ const response = await wallet.sendMessageWithEvents(
14446
+ message,
14447
+ (resp) => options?.eventCallback?.(
14448
+ resp.payload
14449
+ ),
14450
+ (resp) => resp.type === "MIGRATE_DEPRECATED_SIGNER_VTXOS_SUCCESS"
14451
+ );
14452
+ return deserializeMigrationReport(
14453
+ response.payload.report
14454
+ );
14455
+ } catch (e) {
14456
+ throw new Error(`Failed to migrate deprecated-signer vtxos: ${e}`);
14457
+ }
14458
+ },
14459
+ async getDeprecatedSignerStatus() {
14460
+ const message = {
14461
+ tag: messageTag,
14462
+ type: "GET_DEPRECATED_SIGNER_STATUS",
14463
+ id: getRandomId()
14464
+ };
14465
+ try {
14466
+ const response = await wallet.sendMessage(message);
14467
+ return response.payload.signers.map(
14468
+ deserializeDeprecatedSignerReport
14469
+ );
14470
+ } catch (e) {
14471
+ throw new Error(`Failed to get deprecated-signer status: ${e}`);
14472
+ }
14473
+ },
13210
14474
  async dispose() {
13211
14475
  return;
13212
14476
  }
@@ -14112,13 +15376,19 @@ function isHeaderSubscribeResult(v) {
14112
15376
  const obj = v;
14113
15377
  return typeof obj.height === "number" && typeof obj.hex === "string";
14114
15378
  }
15379
+ function errorText(err) {
15380
+ if (typeof err === "string") return err;
15381
+ if (err && typeof err === "object") {
15382
+ const e = err;
15383
+ return [e.message, e.str].filter((v) => typeof v === "string").join(" ");
15384
+ }
15385
+ return "";
15386
+ }
14115
15387
  function isMissingHeightError(err) {
14116
- const msg = err instanceof Error ? err.message : typeof err === "string" ? err : "";
14117
- return msg.toLowerCase().includes("missingheight");
15388
+ return errorText(err).toLowerCase().includes("missingheight");
14118
15389
  }
14119
15390
  function isTxNotInBlockError(err) {
14120
- const msg = err instanceof Error ? err.message : typeof err === "string" ? err : "";
14121
- const normalized = msg.toLowerCase();
15391
+ const normalized = errorText(err).toLowerCase();
14122
15392
  return normalized.includes("not yet in a block") || normalized.includes("not in a block") || normalized.includes("not in block") || normalized.includes("no confirmed transaction");
14123
15393
  }
14124
15394
  function childTxidFromHex(txHex) {
@@ -14578,6 +15848,6 @@ function isArkContract(str) {
14578
15848
  return str.startsWith(ARKCONTRACT_PREFIX + "=");
14579
15849
  }
14580
15850
 
14581
- export { ArkNote, AssetManager, BIP322, Batch, ContractManager, ContractRepositoryImpl, ContractWatcher, DB_VERSION, DEFAULT_MESSAGE_TIMEOUTS, DelegateManagerImpl, DelegateNotConfiguredError, DelegatorManagerImpl, DelegatorNotConfiguredError, DescriptorSigningProviderMissingError, DustChangeError, ELECTRUM_TCP_HOST, ELECTRUM_WS_URL, ESPLORA_URL, ElectrumOnchainProvider, EsploraProvider, Estimator, HDDescriptorProvider, InMemoryContractRepository, InMemoryWalletRepository, IndexedDBContractRepository, IndexedDBWalletRepository, MESSAGE_BUS_NOT_INITIALIZED, MIGRATION_KEY, MessageBus, MessageBusNotInitializedError, MissingSigningDescriptorError, MnemonicIdentity, OnchainWallet, P2A, Ramps, ReadonlyAssetManager, ReadonlyDescriptorIdentity, ReadonlySingleKey, ReadonlyWallet, ReadonlyWalletError, RestDelegateProvider, RestDelegatorProvider, SeedIdentity, ServiceWorkerReadonlyWallet, ServiceWorkerTimeoutError, ServiceWorkerWallet, SingleKey, TxTree, TxType, TxWeightEstimator, Unroll, VtxoManager, Wallet2 as Wallet, WalletMessageHandler, WalletNotInitializedError, WalletRepositoryImpl, WsElectrumChainSource, buildForfeitTx, buildOffchainTx, closeDatabase, combineTapscriptSigs, contractFromArkContract, contractFromArkContractWithAddress, decodeArkContract, deserializeAssets, deserializeUtxo, deserializeVtxo, encodeArkContract, extendVirtualCoinForContract, getMigrationStatus, getRandomId, hasBoardingTxExpired, isArkContract, isBatchSignable, isDiscoverable, isExpired, isRecoverable, isSpendable, isSubdust, isValidArkAddress, isVtxoExpiringSoon, isVtxoForScript, migrateWalletRepository, openDatabase, requiresMigration, rollbackMigration, saveVtxosForContract, scriptFromArkAddress, serializeAssets, serializeUtxo, serializeVtxo, setupServiceWorker, validateConnectorsTxGraph, validateVtxoTxGraph, verifyTapscriptSignatures, waitForIncomingFunds, warnAndFilterVtxosForScript };
14582
- //# sourceMappingURL=chunk-HFXEUW55.js.map
14583
- //# sourceMappingURL=chunk-HFXEUW55.js.map
15851
+ export { ArkNote, AssetManager, BIP322, Batch, ContractManager, ContractRepositoryImpl, ContractWatcher, DB_VERSION, DEFAULT_MESSAGE_TIMEOUTS, DelegateManagerImpl, DelegateNotConfiguredError, DelegatorManagerImpl, DelegatorNotConfiguredError, DescriptorSigningProviderMissingError, DustChangeError, ELECTRUM_TCP_HOST, ELECTRUM_WS_URL, ESPLORA_URL, ElectrumOnchainProvider, EsploraProvider, Estimator, HDDescriptorProvider, InMemoryContractRepository, InMemoryWalletRepository, IndexedDBContractRepository, IndexedDBWalletRepository, MESSAGE_BUS_NOT_INITIALIZED, MIGRATION_KEY, MessageBus, MessageBusNotInitializedError, MissingSigningDescriptorError, MnemonicIdentity, OnchainWallet, P2A, Ramps, ReadonlyAssetManager, ReadonlyDescriptorIdentity, ReadonlySingleKey, ReadonlyWallet, ReadonlyWalletError, RestDelegateProvider, RestDelegatorProvider, SeedIdentity, ServiceWorkerReadonlyWallet, ServiceWorkerTimeoutError, ServiceWorkerWallet, SingleKey, TxTree, TxType, TxWeightEstimator, Unroll, VtxoManager, Wallet2 as Wallet, WalletMessageHandler, WalletNotInitializedError, WalletRepositoryImpl, WsElectrumChainSource, buildForfeitTx, buildOffchainTx, classifyAgainstSignerSet, classifyContractSigner, closeDatabase, combineTapscriptSigs, contractFromArkContract, contractFromArkContractWithAddress, decodeArkContract, deserializeAssets, deserializeUtxo, deserializeVtxo, encodeArkContract, extendVirtualCoinForContract, getMigrationStatus, getRandomId, hasBoardingTxExpired, isArkContract, isBatchSignable, isCooperativelyMigratable, isDiscoverable, isExpired, isRecoverable, isSpendable, isSubdust, isValidArkAddress, isVtxoExpiringSoon, isVtxoForScript, migrateWalletRepository, openDatabase, requiresMigration, rollbackMigration, saveVtxosForContract, scriptFromArkAddress, serializeAssets, serializeUtxo, serializeVtxo, setupServiceWorker, signerSetFromInfo, toXOnlySignerHex, validateConnectorsTxGraph, validateVtxoTxGraph, verifyTapscriptSignatures, waitForIncomingFunds, warnAndFilterVtxosForScript };
15852
+ //# sourceMappingURL=chunk-NOR7XOKN.js.map
15853
+ //# sourceMappingURL=chunk-NOR7XOKN.js.map