@arkade-os/sdk 0.4.33 → 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 (84) hide show
  1. package/README.md +1 -1
  2. package/dist/adapters/expo.cjs +5 -5
  3. package/dist/adapters/expo.d.cts +2 -2
  4. package/dist/adapters/expo.d.ts +2 -2
  5. package/dist/adapters/expo.js +3 -3
  6. package/dist/adapters/indexedDB.cjs +5 -5
  7. package/dist/adapters/indexedDB.js +4 -4
  8. package/dist/{ark-DEsDMYGv.d.cts → ark-D6sau_6-.d.cts} +522 -9
  9. package/dist/{ark-DEsDMYGv.d.ts → ark-D6sau_6-.d.ts} +522 -9
  10. package/dist/{asyncStorageTaskQueue-D8T1VXEx.d.cts → asyncStorageTaskQueue-CpC027t_.d.cts} +2 -2
  11. package/dist/{asyncStorageTaskQueue-CMrTYlKG.d.ts → asyncStorageTaskQueue-GT8fmPUG.d.ts} +2 -2
  12. package/dist/{chunk-E22HEKLN.js → chunk-3JR77WQ4.js} +140 -42
  13. package/dist/chunk-3JR77WQ4.js.map +1 -0
  14. package/dist/{chunk-WMIPYZSB.cjs → chunk-CMPJR3HS.cjs} +42 -9
  15. package/dist/chunk-CMPJR3HS.cjs.map +1 -0
  16. package/dist/{chunk-AOJUURHM.js → chunk-CUSABEUQ.js} +141 -37
  17. package/dist/chunk-CUSABEUQ.js.map +1 -0
  18. package/dist/{chunk-HAVA4XB7.cjs → chunk-FM7T7JVL.cjs} +7 -7
  19. package/dist/{chunk-HAVA4XB7.cjs.map → chunk-FM7T7JVL.cjs.map} +1 -1
  20. package/dist/{chunk-GYSK5R57.cjs → chunk-GUTKJMSF.cjs} +164 -59
  21. package/dist/chunk-GUTKJMSF.cjs.map +1 -0
  22. package/dist/{chunk-7K3ROJF6.cjs → chunk-H2LX2KKY.cjs} +2161 -466
  23. package/dist/chunk-H2LX2KKY.cjs.map +1 -0
  24. package/dist/{chunk-DSS2GQUG.js → chunk-NOR7XOKN.js} +2021 -331
  25. package/dist/chunk-NOR7XOKN.js.map +1 -0
  26. package/dist/{chunk-BU3BU6XK.js → chunk-OURFR4UR.js} +3 -3
  27. package/dist/{chunk-BU3BU6XK.js.map → chunk-OURFR4UR.js.map} +1 -1
  28. package/dist/{chunk-TU3LVAPX.js → chunk-OUVTG72A.js} +43 -11
  29. package/dist/chunk-OUVTG72A.js.map +1 -0
  30. package/dist/{chunk-5CCRRL5S.cjs → chunk-VYS3KGRI.cjs} +19 -13
  31. package/dist/chunk-VYS3KGRI.cjs.map +1 -0
  32. package/dist/{chunk-SPDNHPM4.cjs → chunk-X2EQLK4O.cjs} +149 -46
  33. package/dist/chunk-X2EQLK4O.cjs.map +1 -0
  34. package/dist/{chunk-L6ZETTX3.js → chunk-XQS2HW4Q.js} +11 -5
  35. package/dist/chunk-XQS2HW4Q.js.map +1 -0
  36. package/dist/contracts/handlers/index.cjs +7 -7
  37. package/dist/contracts/handlers/index.d.cts +3 -3
  38. package/dist/contracts/handlers/index.d.ts +3 -3
  39. package/dist/contracts/handlers/index.js +2 -2
  40. package/dist/{delegate-BJeBNP5a.d.cts → delegate-C-L6gSZx.d.cts} +1 -1
  41. package/dist/{delegate-EXN2mfkb.d.ts → delegate-De5__fpZ.d.ts} +1 -1
  42. package/dist/{index-BG2ooYKO.d.ts → index-BETdjE_o.d.ts} +22 -16
  43. package/dist/{index-DHjEeHEp.d.cts → index-jwQfHP6D.d.cts} +22 -16
  44. package/dist/index.cjs +158 -130
  45. package/dist/index.d.cts +125 -16
  46. package/dist/index.d.ts +125 -16
  47. package/dist/index.js +4 -4
  48. package/dist/repositories/realm/index.cjs +14 -14
  49. package/dist/repositories/realm/index.cjs.map +1 -1
  50. package/dist/repositories/realm/index.d.cts +2 -2
  51. package/dist/repositories/realm/index.d.ts +2 -2
  52. package/dist/repositories/realm/index.js +5 -5
  53. package/dist/repositories/realm/index.js.map +1 -1
  54. package/dist/repositories/sqlite/index.cjs +13 -13
  55. package/dist/repositories/sqlite/index.d.cts +1 -1
  56. package/dist/repositories/sqlite/index.d.ts +1 -1
  57. package/dist/repositories/sqlite/index.js +4 -4
  58. package/dist/{taskRunner-pIGyarFG.d.cts → taskRunner-DCyp6Gea.d.cts} +2 -2
  59. package/dist/{taskRunner-B7lBU45X.d.ts → taskRunner-DnxtObeq.d.ts} +2 -2
  60. package/dist/wallet/expo/background.cjs +14 -14
  61. package/dist/wallet/expo/background.d.cts +3 -3
  62. package/dist/wallet/expo/background.d.ts +3 -3
  63. package/dist/wallet/expo/background.js +6 -6
  64. package/dist/wallet/expo/index.cjs +13 -13
  65. package/dist/wallet/expo/index.d.cts +5 -5
  66. package/dist/wallet/expo/index.d.ts +5 -5
  67. package/dist/wallet/expo/index.js +5 -5
  68. package/dist/{wallet-D4Dll5Gu.d.cts → wallet-BWHbd5b1.d.cts} +388 -10
  69. package/dist/{wallet-C4L_X0i6.d.ts → wallet-Bth5uucA.d.ts} +388 -10
  70. package/dist/worker/expo/index.cjs +9 -9
  71. package/dist/worker/expo/index.d.cts +4 -4
  72. package/dist/worker/expo/index.d.ts +4 -4
  73. package/dist/worker/expo/index.js +5 -5
  74. package/package.json +5 -5
  75. package/dist/chunk-5CCRRL5S.cjs.map +0 -1
  76. package/dist/chunk-7K3ROJF6.cjs.map +0 -1
  77. package/dist/chunk-AOJUURHM.js.map +0 -1
  78. package/dist/chunk-DSS2GQUG.js.map +0 -1
  79. package/dist/chunk-E22HEKLN.js.map +0 -1
  80. package/dist/chunk-GYSK5R57.cjs.map +0 -1
  81. package/dist/chunk-L6ZETTX3.js.map +0 -1
  82. package/dist/chunk-SPDNHPM4.cjs.map +0 -1
  83. package/dist/chunk-TU3LVAPX.js.map +0 -1
  84. package/dist/chunk-WMIPYZSB.cjs.map +0 -1
@@ -1,6 +1,6 @@
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-E22HEKLN.js';
2
- import { isMainnetDescriptor, descriptorIsOurs, contractHandlers, DelegateVtxo, WALLET_RECEIVE_SOURCE, deriveDescriptorLeafPubKey, DefaultVtxo, BoardingContractHandler } from './chunk-AOJUURHM.js';
3
- import { VtxoScript, timelockToSequence, DEFAULT_NETWORK, DEFAULT_NETWORK_NAME, decodeTapscript, scriptFromTapLeafScript, CLTVMultisigTapscript, ArkAddress, getSequence, CSVMultisigTapscript, getNetwork, MultisigTapscript, networks as networks$1, DEFAULT_ARKADE_SERVER_URL } from './chunk-TU3LVAPX.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
+ import { isMainnetDescriptor, descriptorIsOurs, contractHandlers, DEFAULT_PAGE_SIZE, DelegateVtxo, WALLET_RECEIVE_SOURCE, deriveDescriptorLeafPubKey, DefaultVtxo, BoardingContractHandler } from './chunk-CUSABEUQ.js';
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';
5
5
  import { SigHash, Script, p2tr, RawWitness, Address, OutScript, p2wpkh, TaprootControlBlock, DEFAULT_SEQUENCE, Transaction as Transaction$2 } from '@scure/btc-signer';
6
6
  import { hex, base58, base64 } from '@scure/base';
@@ -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)) {
@@ -1009,7 +1009,7 @@ var ESPLORA_URL = {
1009
1009
  testnet: "https://mempool.space/testnet/api",
1010
1010
  signet: "https://mempool.signet.arkade.sh/api",
1011
1011
  mutinynet: "https://mempool.mutinynet.arkade.sh/api",
1012
- regtest: "http://localhost:3000"
1012
+ regtest: "http://localhost:3000/api"
1013
1013
  };
1014
1014
  var EsploraProvider = class {
1015
1015
  constructor(baseUrl = ESPLORA_URL[DEFAULT_NETWORK_NAME], opts) {
@@ -1020,14 +1020,17 @@ 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
+ if (response.status === 404) {
1032
+ return void 0;
1033
+ }
1031
1034
  if (!response.ok) {
1032
1035
  throw new Error(`Failed to fetch fee rate: ${response.statusText}`);
1033
1036
  }
@@ -1045,7 +1048,7 @@ var EsploraProvider = class {
1045
1048
  }
1046
1049
  }
1047
1050
  async getTxOutspends(txid) {
1048
- const response = await fetch(`${this.baseUrl}/tx/${txid}/outspends`);
1051
+ const response = await baseFetch(`${this.baseUrl}/tx/${txid}/outspends`);
1049
1052
  if (!response.ok) {
1050
1053
  const error = await response.text();
1051
1054
  throw new Error(`Failed to get transaction outspends: ${error}`);
@@ -1053,7 +1056,7 @@ var EsploraProvider = class {
1053
1056
  return response.json();
1054
1057
  }
1055
1058
  async getTransactions(address) {
1056
- const response = await fetch(`${this.baseUrl}/address/${address}/txs`);
1059
+ const response = await baseFetch(`${this.baseUrl}/address/${address}/txs`);
1057
1060
  if (!response.ok) {
1058
1061
  const error = await response.text();
1059
1062
  throw new Error(`Failed to get transactions: ${error}`);
@@ -1061,7 +1064,7 @@ var EsploraProvider = class {
1061
1064
  return response.json();
1062
1065
  }
1063
1066
  async getTxStatus(txid) {
1064
- const txresponse = await fetch(`${this.baseUrl}/tx/${txid}`);
1067
+ const txresponse = await baseFetch(`${this.baseUrl}/tx/${txid}`);
1065
1068
  if (!txresponse.ok) {
1066
1069
  throw new Error(txresponse.statusText);
1067
1070
  }
@@ -1069,7 +1072,7 @@ var EsploraProvider = class {
1069
1072
  if (!tx.status.confirmed) {
1070
1073
  return { confirmed: false };
1071
1074
  }
1072
- const response = await fetch(`${this.baseUrl}/tx/${txid}/status`);
1075
+ const response = await baseFetch(`${this.baseUrl}/tx/${txid}/status`);
1073
1076
  if (!response.ok) {
1074
1077
  throw new Error(`Failed to get transaction status: ${response.statusText}`);
1075
1078
  }
@@ -1153,7 +1156,7 @@ var EsploraProvider = class {
1153
1156
  return stopFunc;
1154
1157
  }
1155
1158
  async getChainTip() {
1156
- const tipBlocks = await fetch(`${this.baseUrl}/blocks/tip`);
1159
+ const tipBlocks = await baseFetch(`${this.baseUrl}/blocks`);
1157
1160
  if (!tipBlocks.ok) {
1158
1161
  throw new Error(`Failed to get chain tip: ${tipBlocks.statusText}`);
1159
1162
  }
@@ -1172,7 +1175,7 @@ var EsploraProvider = class {
1172
1175
  };
1173
1176
  }
1174
1177
  async broadcastPackage(parent, child) {
1175
- const response = await fetch(`${this.baseUrl}/txs/package`, {
1178
+ const response = await baseFetch(`${this.baseUrl}/txs/package`, {
1176
1179
  method: "POST",
1177
1180
  headers: {
1178
1181
  "Content-Type": "application/json"
@@ -1186,7 +1189,7 @@ var EsploraProvider = class {
1186
1189
  return response.json();
1187
1190
  }
1188
1191
  async broadcastTx(tx) {
1189
- const response = await fetch(`${this.baseUrl}/tx`, {
1192
+ const response = await baseFetch(`${this.baseUrl}/tx`, {
1190
1193
  method: "POST",
1191
1194
  headers: {
1192
1195
  "Content-Type": "text/plain"
@@ -1776,6 +1779,45 @@ function selectedCoinsToAssetInputs(selectedCoins) {
1776
1779
  }
1777
1780
  return assetInputs;
1778
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
+ }
1779
1821
  function buildOffchainTx(inputs, outputs, serverUnrollScript) {
1780
1822
  const MAX_OP_RETURN = 2;
1781
1823
  let countOpReturn = 0;
@@ -2258,12 +2300,12 @@ var FALLBACK_WALLET_DUST_AMOUNT = 330n;
2258
2300
  function getDustAmount(wallet) {
2259
2301
  return "dustAmount" in wallet ? wallet.dustAmount : FALLBACK_WALLET_DUST_AMOUNT;
2260
2302
  }
2261
- function extendCoin(wallet, utxo) {
2303
+ function extendCoinWithTapscript(boardingTapscript, utxo) {
2262
2304
  return {
2263
2305
  ...utxo,
2264
- forfeitTapLeafScript: wallet.boardingTapscript.forfeit(),
2265
- intentTapLeafScript: wallet.boardingTapscript.forfeit(),
2266
- tapTree: wallet.boardingTapscript.encode()
2306
+ forfeitTapLeafScript: boardingTapscript.forfeit(),
2307
+ intentTapLeafScript: boardingTapscript.forfeit(),
2308
+ tapTree: boardingTapscript.encode()
2267
2309
  };
2268
2310
  }
2269
2311
  function deriveContractTapscripts(contract) {
@@ -2356,13 +2398,27 @@ function validateRecipients(recipients, dustAmount) {
2356
2398
  }
2357
2399
 
2358
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
+ }
2359
2415
  function isSweepCapable(wallet) {
2360
- return "boardingTapscript" in wallet && "onchainProvider" in wallet && "arkProvider" in wallet && "network" in wallet;
2416
+ return "boardingTapscript" in wallet && "onchainProvider" in wallet && "arkProvider" in wallet && "network" in wallet && "signOnchainBoardingTx" in wallet;
2361
2417
  }
2362
2418
  function assertSweepCapable(wallet) {
2363
2419
  if (!isSweepCapable(wallet)) {
2364
2420
  throw new Error(
2365
- "Boarding UTXO sweep requires a Wallet instance with boardingTapscript, onchainProvider, arkProvider, and network"
2421
+ "Boarding UTXO sweep requires a Wallet instance with boardingTapscript, onchainProvider, arkProvider, network, and signOnchainBoardingTx"
2366
2422
  );
2367
2423
  }
2368
2424
  }
@@ -2378,6 +2434,33 @@ async function runWithCrossInstanceLock(name, fn) {
2378
2434
  await fn();
2379
2435
  });
2380
2436
  }
2437
+ var MAX_VTXOS_PER_SETTLEMENT = 50;
2438
+ function byValueDescending(vtxos) {
2439
+ return [...vtxos].sort((a, b) => b.value - a.value);
2440
+ }
2441
+ function byExpiryAscending(vtxos) {
2442
+ const expiryKey = (vtxo) => {
2443
+ if (isRecoverable(vtxo)) return -Infinity;
2444
+ const batchExpiry = vtxo.virtualStatus.batchExpiry;
2445
+ if (isExpired(vtxo)) return batchExpiry ?? -Infinity;
2446
+ if (!batchExpiry) return Infinity;
2447
+ if (new Date(batchExpiry).getFullYear() < 2025) return Infinity;
2448
+ return batchExpiry;
2449
+ };
2450
+ return [...vtxos].sort((a, b) => expiryKey(a) - expiryKey(b));
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
+ }
2381
2464
  var DEFAULT_THRESHOLD_SECONDS = 259200;
2382
2465
  var DEFAULT_THRESHOLD_MS = DEFAULT_THRESHOLD_SECONDS * 1e3;
2383
2466
  var DEFAULT_RENEWAL_CONFIG = {
@@ -2387,7 +2470,8 @@ var DEFAULT_RENEWAL_CONFIG = {
2387
2470
  var DEFAULT_SETTLEMENT_CONFIG = {
2388
2471
  vtxoThreshold: DEFAULT_THRESHOLD_SECONDS,
2389
2472
  boardingUtxoSweep: true,
2390
- pollIntervalMs: 6e4
2473
+ pollIntervalMs: 6e4,
2474
+ deprecatedSignerMigration: true
2391
2475
  };
2392
2476
  function getRecoverableVtxos(vtxos, dustAmount) {
2393
2477
  return vtxos.filter((vtxo) => {
@@ -2441,6 +2525,51 @@ function getExpiringAndRecoverableVtxos(vtxos, thresholdMs, dustAmount) {
2441
2525
  (vtxo) => isVtxoExpiringSoon(vtxo, thresholdMs) || isRecoverable(vtxo) || isSpendable(vtxo) && isExpired(vtxo) || isSubdust(vtxo, dustAmount)
2442
2526
  );
2443
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
+ }
2444
2573
  var VtxoManager = class _VtxoManager {
2445
2574
  constructor(wallet, renewalConfig, settlementConfig) {
2446
2575
  this.wallet = wallet;
@@ -2456,15 +2585,9 @@ var VtxoManager = class _VtxoManager {
2456
2585
  } else {
2457
2586
  this.settlementConfig = { ...DEFAULT_SETTLEMENT_CONFIG };
2458
2587
  }
2459
- this.contractEventsSubscriptionReady = this.initializeSubscription().then(
2460
- (subscription) => {
2461
- this.contractEventsSubscription = subscription;
2462
- return subscription;
2463
- }
2464
- );
2588
+ this.contractEventsSubscriptionReady = this.initializeSubscription();
2465
2589
  }
2466
2590
  settlementConfig;
2467
- contractEventsSubscription;
2468
2591
  contractEventsSubscriptionReady;
2469
2592
  disposePromise;
2470
2593
  pollTimeoutId;
@@ -2501,6 +2624,15 @@ var VtxoManager = class _VtxoManager {
2501
2624
  lastVtxoSpentRefreshTimestamp = 0;
2502
2625
  vtxoSpentRefreshPromise;
2503
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;
2504
2636
  // ========== Recovery Methods ==========
2505
2637
  /**
2506
2638
  * Recover swept/expired virtual outputs by settling them back to the wallet's Arkade address.
@@ -2537,10 +2669,25 @@ var VtxoManager = class _VtxoManager {
2537
2669
  withUnrolled: false
2538
2670
  });
2539
2671
  const dustAmount = getDustAmount(this.wallet);
2540
- const { vtxosToRecover, totalAmount } = getRecoverableWithSubdust(allVtxos, dustAmount);
2672
+ let { vtxosToRecover, totalAmount } = getRecoverableWithSubdust(allVtxos, dustAmount);
2541
2673
  if (vtxosToRecover.length === 0) {
2542
2674
  throw new Error("No recoverable VTXOs found");
2543
2675
  }
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) {
2680
+ const recoverableCount = vtxosToRecover.length;
2681
+ ({ vtxosToRecover, totalAmount } = getRecoverableWithSubdust(capped, dustAmount));
2682
+ if (vtxosToRecover.length === 0) {
2683
+ throw new Error(
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}`
2685
+ );
2686
+ }
2687
+ }
2688
+ if (info && isMigrationCapable(this.wallet)) {
2689
+ await this.rotateForRecoverableInputs(vtxosToRecover, info);
2690
+ }
2544
2691
  const arkAddress = await this.wallet.getAddress();
2545
2692
  return this.wallet.settle(
2546
2693
  {
@@ -2690,6 +2837,25 @@ var VtxoManager = class _VtxoManager {
2690
2837
  if (vtxos.length === 0) {
2691
2838
  throw new Error("No VTXOs available to renew");
2692
2839
  }
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
+ }
2858
+ }
2693
2859
  const totalAmount = vtxos.reduce((sum, vtxo) => sum + vtxo.value, 0);
2694
2860
  const dustAmount = getDustAmount(this.wallet);
2695
2861
  if (BigInt(totalAmount) < dustAmount) {
@@ -2697,6 +2863,9 @@ var VtxoManager = class _VtxoManager {
2697
2863
  `Total amount ${totalAmount} is below dust threshold ${dustAmount}`
2698
2864
  );
2699
2865
  }
2866
+ if (info && isMigrationCapable(this.wallet)) {
2867
+ await this.rotateForRecoverableInputs(vtxos, info);
2868
+ }
2700
2869
  const arkAddress = await this.wallet.getAddress();
2701
2870
  const txid = await this.wallet.settle(
2702
2871
  {
@@ -2797,7 +2966,6 @@ var VtxoManager = class _VtxoManager {
2797
2966
  const boardingAddress = await this.wallet.getBoardingAddress();
2798
2967
  const feeRate = await this.getOnchainProvider().getFeeRate() ?? 1;
2799
2968
  const exitTapLeafScript = this.getBoardingExitLeaf();
2800
- const sequence = getSequence(exitTapLeafScript);
2801
2969
  const leafScript = exitTapLeafScript[1];
2802
2970
  const leafScriptSize = leafScript.length - 1;
2803
2971
  const controlBlockSize = exitTapLeafScript[0].merklePath.length * 32;
@@ -2818,19 +2986,28 @@ var VtxoManager = class _VtxoManager {
2818
2986
  }
2819
2987
  const tx = new Transaction();
2820
2988
  for (const utxo of expiredUtxos) {
2989
+ const utxoScript = VtxoScript.decode(utxo.tapTree);
2990
+ const utxoExitLeaf = utxoScript.leaves.find(
2991
+ (leaf) => CSVMultisigTapscript.isScriptValid(scriptFromTapLeafScript(leaf)) === true
2992
+ );
2993
+ if (!utxoExitLeaf) {
2994
+ throw new Error(
2995
+ `Boarding sweep: no CSV exit leaf for UTXO ${utxo.txid}:${utxo.vout}`
2996
+ );
2997
+ }
2821
2998
  tx.addInput({
2822
2999
  txid: utxo.txid,
2823
3000
  index: utxo.vout,
2824
3001
  witnessUtxo: {
2825
- script: this.getBoardingOutputScript(),
3002
+ script: utxoScript.pkScript,
2826
3003
  amount: BigInt(utxo.value)
2827
3004
  },
2828
- tapLeafScript: [exitTapLeafScript],
2829
- sequence
3005
+ tapLeafScript: [utxoExitLeaf],
3006
+ sequence: getSequence(utxoExitLeaf)
2830
3007
  });
2831
3008
  }
2832
3009
  tx.addOutputAddress(boardingAddress, outputAmount, this.getNetwork());
2833
- const signedTx = await this.getIdentity().sign(tx);
3010
+ const signedTx = await this.getSweepWallet().signOnchainBoardingTx(tx);
2834
3011
  signedTx.finalize();
2835
3012
  const txid = await this.getOnchainProvider().broadcastTransaction(signedTx.hex);
2836
3013
  for (const u of expiredUtxos) {
@@ -2839,6 +3016,472 @@ var VtxoManager = class _VtxoManager {
2839
3016
  this.knownBoardingUtxos.add(`${txid}:0`);
2840
3017
  return txid;
2841
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
+ }
2842
3485
  // ========== Private Helpers ==========
2843
3486
  /** Asserts sweep capability and returns the typed wallet. */
2844
3487
  getSweepWallet() {
@@ -2857,10 +3500,6 @@ var VtxoManager = class _VtxoManager {
2857
3500
  getBoardingExitLeaf() {
2858
3501
  return this.getSweepWallet().boardingTapscript.exit();
2859
3502
  }
2860
- /** Returns the pkScript (output script) of the boarding tapscript. */
2861
- getBoardingOutputScript() {
2862
- return this.getSweepWallet().boardingTapscript.pkScript;
2863
- }
2864
3503
  /** Returns the onchain provider for fee estimation and broadcasting. */
2865
3504
  getOnchainProvider() {
2866
3505
  return this.getSweepWallet().onchainProvider;
@@ -2869,14 +3508,20 @@ var VtxoManager = class _VtxoManager {
2869
3508
  getArkProvider() {
2870
3509
  return this.getSweepWallet().arkProvider;
2871
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
+ }
2872
3521
  /** Returns the Bitcoin network configuration from the wallet. */
2873
3522
  getNetwork() {
2874
3523
  return this.getSweepWallet().network;
2875
3524
  }
2876
- /** Returns the wallet's identity for transaction signing. */
2877
- getIdentity() {
2878
- return this.wallet.identity;
2879
- }
2880
3525
  async initializeSubscription() {
2881
3526
  if (this.settlementConfig === false) {
2882
3527
  return void 0;
@@ -3074,6 +3719,10 @@ var VtxoManager = class _VtxoManager {
3074
3719
  }
3075
3720
  }
3076
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
+ }
3077
3726
  });
3078
3727
  } catch (e) {
3079
3728
  hadError = true;
@@ -3142,7 +3791,8 @@ var VtxoManager = class _VtxoManager {
3142
3791
  return;
3143
3792
  }
3144
3793
  const dustAmount = getDustAmount(this.wallet);
3145
- const { fees } = await this.getArkProvider().getInfo();
3794
+ const info = await this.getArkProvider().getInfo();
3795
+ const { fees, vtxoMaxAmount } = info;
3146
3796
  const estimator = new Estimator(fees.intentFee);
3147
3797
  let totalAmount = 0n;
3148
3798
  const filteredBoarding = [];
@@ -3157,7 +3807,10 @@ var VtxoManager = class _VtxoManager {
3157
3807
  totalAmount += BigInt(u.value) - BigInt(inputFee.satoshis);
3158
3808
  }
3159
3809
  const filteredVtxos = [];
3160
- for (const v of expiringVtxos) {
3810
+ for (const v of byExpiryAscending(expiringVtxos)) {
3811
+ if (filteredVtxos.length >= MAX_VTXOS_PER_SETTLEMENT) {
3812
+ break;
3813
+ }
3161
3814
  const inputFee = estimator.evalOffchainInput({
3162
3815
  amount: BigInt(v.value),
3163
3816
  type: v.virtualStatus.state === "swept" ? "recoverable" : "vtxo",
@@ -3168,12 +3821,19 @@ var VtxoManager = class _VtxoManager {
3168
3821
  if (inputFee.satoshis >= v.value) {
3169
3822
  continue;
3170
3823
  }
3824
+ const net = BigInt(v.value) - BigInt(inputFee.satoshis);
3825
+ if (vtxoMaxAmount >= 0n && totalAmount + net > vtxoMaxAmount) {
3826
+ continue;
3827
+ }
3171
3828
  filteredVtxos.push(v);
3172
- totalAmount += BigInt(v.value) - BigInt(inputFee.satoshis);
3829
+ totalAmount += net;
3173
3830
  }
3174
3831
  if (filteredBoarding.length === 0 && filteredVtxos.length === 0) {
3175
3832
  return;
3176
3833
  }
3834
+ if (isMigrationCapable(this.wallet)) {
3835
+ await this.rotateForRecoverableInputs([...filteredBoarding, ...filteredVtxos], info);
3836
+ }
3177
3837
  const arkAddress = await this.wallet.getAddress();
3178
3838
  const outputFee = estimator.evalOffchainOutput({
3179
3839
  amount: totalAmount,
@@ -3236,7 +3896,6 @@ var VtxoManager = class _VtxoManager {
3236
3896
  clearTimeout(timer);
3237
3897
  }
3238
3898
  const subscription = await this.contractEventsSubscriptionReady;
3239
- this.contractEventsSubscription = void 0;
3240
3899
  subscription?.();
3241
3900
  })();
3242
3901
  return this.disposePromise;
@@ -3777,15 +4436,17 @@ async function buildTransactionHistory(vtxos, allBoardingTxs, commitmentsToIgnor
3777
4436
  txTime = getTxCreatedAt ? await getTxCreatedAt(vtxo.arkTxId) ?? vtxo.createdAt.getTime() + 1 : vtxo.createdAt.getTime() + 1;
3778
4437
  }
3779
4438
  const assets = subtractAssets(allSpent, changes);
3780
- sent.push({
3781
- key: { ...txKey, arkTxid: vtxo.arkTxId },
3782
- tag: "offchain",
3783
- type: "SENT" /* TxSent */,
3784
- amount: txAmount,
3785
- settled: true,
3786
- createdAt: txTime,
3787
- ...assets && { assets }
3788
- });
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
+ }
3789
4450
  }
3790
4451
  if (vtxo.settledBy && !commitmentsToIgnore.has(vtxo.settledBy) && !sent.some((s) => s.key.commitmentTxid === vtxo.settledBy)) {
3791
4452
  const changes = fromOldestVtxo.filter(
@@ -5958,6 +6619,16 @@ function isDiscoverable(handler) {
5958
6619
  }
5959
6620
 
5960
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
+ };
5961
6632
  var ContractWatcher = class {
5962
6633
  config;
5963
6634
  contracts = /* @__PURE__ */ new Map();
@@ -5977,14 +6648,7 @@ var ContractWatcher = class {
5977
6648
  */
5978
6649
  constructor(config) {
5979
6650
  this.config = {
5980
- failsafePollIntervalMs: 6e4,
5981
- // 1 minute
5982
- reconnectDelayMs: 1e3,
5983
- // 1 second
5984
- maxReconnectDelayMs: 3e4,
5985
- // 30 seconds
5986
- maxReconnectAttempts: 0,
5987
- // unlimited
6651
+ ...DEFAULT_CONTRACT_WATCHER_CONFIG,
5988
6652
  ...config
5989
6653
  };
5990
6654
  }
@@ -6212,8 +6876,9 @@ var ContractWatcher = class {
6212
6876
  }
6213
6877
  this.connectionState = "reconnecting";
6214
6878
  this.reconnectAttempts++;
6215
- const delay = Math.min(
6216
- this.config.reconnectDelayMs * Math.pow(2, this.reconnectAttempts - 1),
6879
+ const delay = computeReconnectDelay(
6880
+ this.reconnectAttempts,
6881
+ this.config.reconnectDelayMs,
6217
6882
  this.config.maxReconnectDelayMs
6218
6883
  );
6219
6884
  this.reconnectTimeoutId = setTimeout(() => {
@@ -6515,8 +7180,11 @@ function cursorCutoff(requestStartedAt) {
6515
7180
  }
6516
7181
 
6517
7182
  // src/contracts/contractManager.ts
6518
- var DEFAULT_PAGE_SIZE = 500;
7183
+ function areCoalescibleContractTypes(a, b) {
7184
+ return a === "default" && b === "boarding" || a === "boarding" && b === "default";
7185
+ }
6519
7186
  var SCAN_MAX_INDEX = 1e4;
7187
+ var DEFAULT_SCAN_BATCH = 10;
6520
7188
  var ContractManager = class _ContractManager {
6521
7189
  config;
6522
7190
  watcher;
@@ -6640,8 +7308,11 @@ var ContractManager = class _ContractManager {
6640
7308
  const [existing] = await this.getContracts({ script: params.script });
6641
7309
  if (existing) {
6642
7310
  if (existing.type === params.type) return { contract: existing, persisted: false };
7311
+ if (areCoalescibleContractTypes(existing.type, params.type)) {
7312
+ return { contract: existing, persisted: false };
7313
+ }
6643
7314
  throw new Error(
6644
- `Contract with script ${params.script} already exists with with type ${existing.type}.`
7315
+ `Contract with script ${params.script} already exists with type ${existing.type}.`
6645
7316
  );
6646
7317
  }
6647
7318
  const contract = {
@@ -6670,6 +7341,19 @@ var ContractManager = class _ContractManager {
6670
7341
  * other handler hit it).
6671
7342
  * - `persistAndWatchContract` rejecting is operational/fatal and
6672
7343
  * propagates (only `discoverAt` is guarded).
7344
+ * - Within an index the handler probes run concurrently (independent
7345
+ * network reads); their hits are persisted sequentially in
7346
+ * `discoverables` order to preserve the first-wins collision tie-break.
7347
+ * - Indices are probed `batchSize` at a time (a second concurrency layer
7348
+ * over the per-index probes), but each window is CAPPED to
7349
+ * `gapLimit - unused` indices — the most a serial scan could still reach
7350
+ * before the gap window is guaranteed to close. So every index probed in
7351
+ * a window is one a one-index-at-a-time scan would also reach: nothing is
7352
+ * over-scanned, nothing is discarded, and `materialize`/`discoverAt` are
7353
+ * invoked on exactly the same index set. The window's hits are still
7354
+ * processed strictly in ascending index order, so the discovered set,
7355
+ * persisted rows, `lastIndexUsed`, and `handlerErrors` are byte-for-byte
7356
+ * identical to the serial path — only the wall-clock differs.
6673
7357
  */
6674
7358
  async scanContracts(opts) {
6675
7359
  const gapLimit = opts.gapLimit ?? 20;
@@ -6678,35 +7362,69 @@ var ContractManager = class _ContractManager {
6678
7362
  `scanContracts: gapLimit must be a positive integer (got ${String(opts.gapLimit)})`
6679
7363
  );
6680
7364
  }
6681
- const discoverables = contractHandlers.getRegisteredTypes().map((t) => contractHandlers.get(t)).filter(isDiscoverable);
7365
+ const batchSize = opts.batchSize ?? DEFAULT_SCAN_BATCH;
7366
+ if (!Number.isInteger(batchSize) || batchSize <= 0) {
7367
+ throw new Error(
7368
+ `scanContracts: batchSize must be a positive integer (got ${String(opts.batchSize)})`
7369
+ );
7370
+ }
7371
+ const registered = contractHandlers.getRegisteredTypes().map((t) => contractHandlers.get(t)).filter(isDiscoverable);
7372
+ const discoverables = [
7373
+ ...registered.filter((h) => h.type === "boarding"),
7374
+ ...registered.filter((h) => h.type !== "boarding")
7375
+ ];
6682
7376
  const maxIdx = opts.hd ? SCAN_MAX_INDEX : 0;
6683
7377
  const handlerErrors = [];
6684
7378
  let lastIndexUsed = -1;
6685
7379
  let unused = 0;
6686
7380
  let i = 0;
7381
+ const probeIndex = async (index) => {
7382
+ const descriptor = opts.materialize(index);
7383
+ return Promise.all(
7384
+ discoverables.map(async (h) => {
7385
+ try {
7386
+ return {
7387
+ ok: true,
7388
+ found: await h.discoverAt(index, descriptor, opts.deps)
7389
+ };
7390
+ } catch (error) {
7391
+ return { ok: false, error };
7392
+ }
7393
+ })
7394
+ );
7395
+ };
6687
7396
  while (i <= maxIdx && unused < gapLimit) {
6688
- const descriptor = opts.materialize(i);
6689
- let hitAtThisIndex = false;
6690
- for (const h of discoverables) {
6691
- let found;
6692
- try {
6693
- found = await h.discoverAt(i, descriptor, opts.deps);
6694
- } catch (error) {
6695
- handlerErrors.push({ handler: h.type, index: i, error });
6696
- continue;
7397
+ const windowEnd = Math.min(maxIdx, i + Math.min(batchSize, gapLimit - unused) - 1);
7398
+ const windowIndices = [];
7399
+ for (let idx = i; idx <= windowEnd; idx++) windowIndices.push(idx);
7400
+ const windowProbes = await Promise.all(windowIndices.map(probeIndex));
7401
+ for (let w = 0; w < windowIndices.length; w++) {
7402
+ const index = windowIndices[w];
7403
+ const probes = windowProbes[w];
7404
+ let hitAtThisIndex = false;
7405
+ for (let h = 0; h < discoverables.length; h++) {
7406
+ const probe = probes[h];
7407
+ if (!probe.ok) {
7408
+ handlerErrors.push({
7409
+ handler: discoverables[h].type,
7410
+ index,
7411
+ error: probe.error
7412
+ });
7413
+ continue;
7414
+ }
7415
+ for (const c of probe.found) {
7416
+ await this.persistAndWatchContract(c);
7417
+ hitAtThisIndex = true;
7418
+ }
6697
7419
  }
6698
- for (const c of found) {
6699
- await this.persistAndWatchContract(c);
6700
- hitAtThisIndex = true;
7420
+ if (hitAtThisIndex) {
7421
+ lastIndexUsed = index;
7422
+ unused = 0;
7423
+ } else {
7424
+ unused += 1;
6701
7425
  }
6702
7426
  }
6703
- if (hitAtThisIndex) {
6704
- lastIndexUsed = i;
6705
- unused = 0;
6706
- } else {
6707
- unused += 1;
6708
- }
6709
- i += 1;
7427
+ i = windowEnd + 1;
6710
7428
  }
6711
7429
  if (opts.hd && i > maxIdx && unused < gapLimit) {
6712
7430
  throw new Error(
@@ -7412,7 +8130,8 @@ var WalletReceiveRotator = class _WalletReceiveRotator {
7412
8130
  walletRepository: setup.walletRepository,
7413
8131
  contractRepository: setup.contractRepository,
7414
8132
  serverPubKey: setup.serverPubKey,
7415
- expectedContractType
8133
+ expectedContractType,
8134
+ baselineReceivePubKey: setup.offchainTapscript.options.pubKey
7416
8135
  };
7417
8136
  let boot;
7418
8137
  try {
@@ -7457,14 +8176,17 @@ var WalletReceiveRotator = class _WalletReceiveRotator {
7457
8176
  receivePubkey: existing.pubKey
7458
8177
  };
7459
8178
  }
7460
- let descriptor;
7461
- if (hasPeekableDescriptor(provider)) {
7462
- descriptor = await provider.getCurrentSigningDescriptor();
8179
+ const current = hasPeekableDescriptor(provider) ? await provider.getCurrentSigningDescriptor() : void 0;
8180
+ if (current === void 0) {
8181
+ const descriptor = await provider.getNextSigningDescriptor();
8182
+ return {
8183
+ rotator: new _WalletReceiveRotator(provider, void 0, opts.logger),
8184
+ receivePubkey: deriveLeafPubkey(descriptor)
8185
+ };
7463
8186
  }
7464
- descriptor ??= await provider.getNextSigningDescriptor();
7465
8187
  return {
7466
8188
  rotator: new _WalletReceiveRotator(provider, void 0, opts.logger),
7467
- receivePubkey: deriveLeafPubkey(descriptor)
8189
+ receivePubkey: opts.baselineReceivePubKey ?? deriveLeafPubkey(current)
7468
8190
  };
7469
8191
  }
7470
8192
  /**
@@ -7527,6 +8249,22 @@ var WalletReceiveRotator = class _WalletReceiveRotator {
7527
8249
  async drain() {
7528
8250
  await this.chain.catch(() => void 0);
7529
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
+ }
7530
8268
  /**
7531
8269
  * Tear down the subscription first so no late `vtxo_received` event
7532
8270
  * can queue work on a disposing wallet, then drain any in-flight
@@ -7697,7 +8435,7 @@ var DescriptorSigningProviderMissingError = class extends Error {
7697
8435
  };
7698
8436
 
7699
8437
  // src/wallet/inputSignerRouter.ts
7700
- var DESCRIPTOR_CAPABLE_CONTRACT_TYPES = /* @__PURE__ */ new Set(["default", "delegate"]);
8438
+ var DESCRIPTOR_CAPABLE_CONTRACT_TYPES = /* @__PURE__ */ new Set(["default", "delegate", "boarding"]);
7701
8439
  var InputSignerRouter = class {
7702
8440
  constructor(deps) {
7703
8441
  this.deps = deps;
@@ -7829,6 +8567,11 @@ function extractArkProviderUrl(provider) {
7829
8567
  return typeof serverUrl === "string" && serverUrl.length > 0 ? serverUrl : void 0;
7830
8568
  }
7831
8569
  var MAINNET_UNILATERAL_EXIT_DELAY = 605184n;
8570
+ function toXOnlyPubKey(pubkey) {
8571
+ if (pubkey.length === 33) return pubkey.slice(1);
8572
+ if (pubkey.length === 32) return pubkey;
8573
+ throw new Error(`invalid signer pubkey length: expected 32 or 33, got ${pubkey.length}`);
8574
+ }
7832
8575
  function delayToTimelock(delay) {
7833
8576
  return {
7834
8577
  value: delay,
@@ -7846,20 +8589,30 @@ function dedupeTimelocks(timelocks) {
7846
8589
  }
7847
8590
  return deduped;
7848
8591
  }
7849
- function areSameScriptBaselineTypesCompatible(existingType, requestedType) {
7850
- if (existingType === requestedType) return true;
7851
- return existingType === "default" && requestedType === "boarding" || existingType === "boarding" && requestedType === "default";
7852
- }
7853
8592
  async function ensureWalletContract(manager, params) {
7854
- const [existing] = await manager.getContracts({ script: params.script });
7855
- if (existing && existing.type !== params.type && areSameScriptBaselineTypesCompatible(existing.type, params.type)) {
7856
- if (params.type === "default" && existing.type === "boarding") {
7857
- await manager.updateContract(params.script, { type: "default" });
7858
- }
7859
- return;
7860
- }
7861
8593
  await manager.createContract(params);
7862
8594
  }
8595
+ async function resolveBoardingBootTapscript(contractRepository, serverPubKey, baseline) {
8596
+ const serverPubKeyHex = hex.encode(serverPubKey);
8597
+ const candidates = await contractRepository.getContracts({
8598
+ type: ["boarding"],
8599
+ state: "active"
8600
+ });
8601
+ const newest = candidates.filter(
8602
+ (c) => c.params.serverPubKey === serverPubKeyHex && c.metadata?.source === WALLET_RECEIVE_SOURCE
8603
+ ).sort((a, b) => {
8604
+ if (b.createdAt !== a.createdAt) return b.createdAt - a.createdAt;
8605
+ return signingDescriptorIndex(b.metadata?.signingDescriptor) - signingDescriptorIndex(a.metadata?.signingDescriptor);
8606
+ })[0];
8607
+ if (!newest?.params.pubKey) return baseline;
8608
+ try {
8609
+ const pubKey = hex.decode(newest.params.pubKey);
8610
+ return new DefaultVtxo.Script({ ...baseline.options, pubKey });
8611
+ } catch (e) {
8612
+ console.warn("Skipping malformed boarding contract at boot", newest.script, e);
8613
+ return baseline;
8614
+ }
8615
+ }
7863
8616
  function hasToReadonly(identity) {
7864
8617
  return typeof identity === "object" && identity !== null && "toReadonly" in identity && typeof identity.toReadonly === "function";
7865
8618
  }
@@ -7869,8 +8622,6 @@ var ReadonlyWallet = class _ReadonlyWallet {
7869
8622
  this.network = network;
7870
8623
  this.onchainProvider = onchainProvider;
7871
8624
  this.indexerProvider = indexerProvider;
7872
- this.arkServerPublicKey = arkServerPublicKey;
7873
- this.boardingTapscript = boardingTapscript;
7874
8625
  this.dustAmount = dustAmount;
7875
8626
  this.walletRepository = walletRepository;
7876
8627
  this.contractRepository = contractRepository;
@@ -7886,6 +8637,8 @@ var ReadonlyWallet = class _ReadonlyWallet {
7886
8637
  }
7887
8638
  }
7888
8639
  this._offchainTapscript = offchainTapscript;
8640
+ this._boardingTapscript = boardingTapscript;
8641
+ this._arkServerPublicKey = arkServerPublicKey;
7889
8642
  this.watcherConfig = watcherConfig;
7890
8643
  this._assetManager = new ReadonlyAssetManager(this.indexerProvider);
7891
8644
  this.walletContractTimelocks = walletContractTimelocks && walletContractTimelocks.length > 0 ? dedupeTimelocks(walletContractTimelocks) : [this.offchainTapscript.options.csvTimelock];
@@ -7894,7 +8647,6 @@ var ReadonlyWallet = class _ReadonlyWallet {
7894
8647
  _contractManagerInitializing;
7895
8648
  watcherConfig;
7896
8649
  _assetManager;
7897
- _syncVtxosInflight;
7898
8650
  walletContractTimelocks;
7899
8651
  // Outpoints ("txid:vout") committed to an in-flight settle/send. Filtered
7900
8652
  // from getVtxos() so concurrent callers (UI, VtxoManager auto-renewal,
@@ -7912,6 +8664,70 @@ var ReadonlyWallet = class _ReadonlyWallet {
7912
8664
  * {@link WalletReceiveRotator.rotate} is the sole intended caller of.
7913
8665
  */
7914
8666
  _offchainTapscript;
8667
+ /**
8668
+ * Backing field for the current boarding tapscript (the QR / onboarding
8669
+ * target). Read via the public `boardingTapscript` getter; written only
8670
+ * by {@link Wallet.setBoardingTapscriptForRotation}, the sanctioned
8671
+ * boarding-rotation write path (analogue of `_offchainTapscript`). It is
8672
+ * a *current value*, not a fixed setup constant, because per-derivation
8673
+ * boarding rotation (plan §6-II) swaps it when a fresh boarding address
8674
+ * is explicitly allocated. Static / `auto` wallets never rotate it, so
8675
+ * it stays the index-0 baseline for their lifetime.
8676
+ */
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
+ }
7915
8731
  /**
7916
8732
  * Currently-active receive tapscript. Read-only from the outside;
7917
8733
  * mutated only via {@link Wallet.setOffchainTapscriptForRotation}
@@ -7921,13 +8737,69 @@ var ReadonlyWallet = class _ReadonlyWallet {
7921
8737
  return this._offchainTapscript;
7922
8738
  }
7923
8739
  /**
7924
- * Protected helper to set up shared wallet configuration.
7925
- * Extracts common logic used by both ReadonlyWallet.create() and Wallet.create().
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.
7926
8745
  */
7927
- static async setupWalletConfig(config, pubKey) {
7928
- const arkadeServerUrl = getArkadeServerUrl(config);
7929
- const arkProvider = config.arkProvider || new RestArkProvider(arkadeServerUrl);
7930
- let indexerProvider = config.indexerProvider;
8746
+ get arkServerPublicKey() {
8747
+ return this._arkServerPublicKey;
8748
+ }
8749
+ /**
8750
+ * The wallet's current boarding tapscript (the on-chain onboarding
8751
+ * target). Read-only from the outside; mutated only via
8752
+ * {@link Wallet.setBoardingTapscriptForRotation} when a fresh boarding
8753
+ * address is explicitly allocated. Single-valued for static / `auto`
8754
+ * wallets.
8755
+ */
8756
+ get boardingTapscript() {
8757
+ return this._boardingTapscript;
8758
+ }
8759
+ /**
8760
+ * Listeners fired after the boarding tapscript rotates to a fresh index
8761
+ * (see {@link Wallet.setBoardingTapscriptForRotation}). A live
8762
+ * {@link notifyIncomingFunds} onchain watcher registers one so it can
8763
+ * re-subscribe to include the newly allocated boarding address within the
8764
+ * same session — without it, a deposit to the fresh address wouldn't fire
8765
+ * a notification until the watcher's next re-init. Always empty for
8766
+ * readonly / static / `auto` wallets, which never rotate boarding.
8767
+ */
8768
+ _boardingRotationListeners = /* @__PURE__ */ new Set();
8769
+ /**
8770
+ * Register a listener invoked synchronously after each boarding rotation.
8771
+ * Returns an unsubscribe function. Protected: only internal subscribers
8772
+ * (the incoming-funds watcher) participate.
8773
+ */
8774
+ onBoardingRotation(listener) {
8775
+ this._boardingRotationListeners.add(listener);
8776
+ return () => {
8777
+ this._boardingRotationListeners.delete(listener);
8778
+ };
8779
+ }
8780
+ /**
8781
+ * Notify boarding-rotation listeners. Called by the boarding-rotation
8782
+ * write path ({@link Wallet.setBoardingTapscriptForRotation}) once the new
8783
+ * tapscript is in place. A throwing listener is isolated so it can neither
8784
+ * break the rotation nor starve sibling listeners.
8785
+ */
8786
+ notifyBoardingRotation() {
8787
+ for (const listener of this._boardingRotationListeners) {
8788
+ try {
8789
+ listener();
8790
+ } catch (e) {
8791
+ console.warn("Boarding-rotation listener failed", e);
8792
+ }
8793
+ }
8794
+ }
8795
+ /**
8796
+ * Protected helper to set up shared wallet configuration.
8797
+ * Extracts common logic used by both ReadonlyWallet.create() and Wallet.create().
8798
+ */
8799
+ static async setupWalletConfig(config, pubKey) {
8800
+ const arkadeServerUrl = getArkadeServerUrl(config);
8801
+ const arkProvider = config.arkProvider || new RestArkProvider(arkadeServerUrl);
8802
+ let indexerProvider = config.indexerProvider;
7931
8803
  if (!indexerProvider) {
7932
8804
  let indexerUrl = config.indexerUrl;
7933
8805
  if (!indexerUrl) {
@@ -8032,7 +8904,7 @@ var ReadonlyWallet = class _ReadonlyWallet {
8032
8904
  throw new Error("Invalid configured public key");
8033
8905
  }
8034
8906
  const setup = await _ReadonlyWallet.setupWalletConfig(config, pubkey);
8035
- return new _ReadonlyWallet(
8907
+ const wallet = new _ReadonlyWallet(
8036
8908
  config.identity,
8037
8909
  setup.network,
8038
8910
  setup.onchainProvider,
@@ -8047,6 +8919,8 @@ var ReadonlyWallet = class _ReadonlyWallet {
8047
8919
  config.watcherConfig,
8048
8920
  setup.walletContractTimelocks
8049
8921
  );
8922
+ wallet.refreshDeprecatedSigners(setup.info);
8923
+ return wallet;
8050
8924
  }
8051
8925
  get arkAddress() {
8052
8926
  return this.offchainTapscript.address(this.network.hrp, this.arkServerPublicKey);
@@ -8070,10 +8944,12 @@ var ReadonlyWallet = class _ReadonlyWallet {
8070
8944
  * Return the wallet's combined onchain and offchain balances.
8071
8945
  */
8072
8946
  async getBalance() {
8073
- const [boardingUtxos, vtxos] = await Promise.all([
8947
+ const [boardingUtxos, vtxos, pendingOutpoints] = await Promise.all([
8074
8948
  this.getBoardingUtxos(),
8075
- this.getVtxos()
8949
+ this.getVtxos(),
8950
+ this.pendingRecoveryOutpoints()
8076
8951
  ]);
8952
+ const isPendingRecovery = (coin) => pendingOutpoints.has(`${coin.txid}:${coin.vout}`);
8077
8953
  let confirmed = 0;
8078
8954
  let unconfirmed = 0;
8079
8955
  for (const utxo of boardingUtxos) {
@@ -8086,11 +8962,15 @@ var ReadonlyWallet = class _ReadonlyWallet {
8086
8962
  let settled = 0;
8087
8963
  let preconfirmed = 0;
8088
8964
  let recoverable = 0;
8089
- settled = vtxos.filter((coin) => coin.virtualStatus.state === "settled").reduce((sum, coin) => sum + coin.value, 0);
8090
- 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);
8091
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);
8092
8972
  const totalBoarding = confirmed + unconfirmed;
8093
- const totalOffchain = settled + preconfirmed + recoverable;
8973
+ const totalOffchain = settled + preconfirmed + recoverable + pendingRecovery;
8094
8974
  const assetBalances = /* @__PURE__ */ new Map();
8095
8975
  for (const vtxo of vtxos) {
8096
8976
  if (!isSpendable(vtxo)) continue;
@@ -8115,6 +8995,7 @@ var ReadonlyWallet = class _ReadonlyWallet {
8115
8995
  preconfirmed,
8116
8996
  available: settled + preconfirmed,
8117
8997
  recoverable,
8998
+ pendingRecovery,
8118
8999
  total: totalBoarding + totalOffchain,
8119
9000
  assets
8120
9001
  };
@@ -8141,6 +9022,23 @@ var ReadonlyWallet = class _ReadonlyWallet {
8141
9022
  return !!(f.withUnrolled && vtxo.isUnrolled);
8142
9023
  });
8143
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
+ }
8144
9042
  /**
8145
9043
  * Return wallet transaction history derived from Arkade state and boarding transactions.
8146
9044
  */
@@ -8160,43 +9058,59 @@ var ReadonlyWallet = class _ReadonlyWallet {
8160
9058
  await clearSyncCursor(this.walletRepository);
8161
9059
  }
8162
9060
  /**
8163
- * Build a transaction history view for the wallet's boarding address.
9061
+ * The on-chain (P2TR) addresses of every boarding tapscript this wallet
9062
+ * uses — the current address plus any historical rotated boarding
9063
+ * addresses. The aggregating boarding readers (history, notifications) fan
9064
+ * out over this set so deposits at previous boarding addresses are still
9065
+ * surfaced (plan §6-IV); {@link getBoardingAddress} stays single-valued.
9066
+ */
9067
+ async getBoardingAddresses() {
9068
+ const tapscripts = await this.getBoardingTapscripts(this.watchedBoardingSigners());
9069
+ return tapscripts.map((t) => t.onchainAddress(this.network));
9070
+ }
9071
+ /**
9072
+ * Build a transaction history view across the wallet's boarding addresses
9073
+ * (current + historical rotated; plan §6-IV.1).
8164
9074
  */
8165
9075
  async getBoardingTxs() {
8166
9076
  const utxos = [];
8167
9077
  const commitmentsToIgnore = /* @__PURE__ */ new Set();
8168
- const boardingAddress = await this.getBoardingAddress();
8169
- const txs = await this.onchainProvider.getTransactions(boardingAddress);
9078
+ const tapscripts = await this.getBoardingTapscripts(this.watchedBoardingSigners());
8170
9079
  const outspendCache = /* @__PURE__ */ new Map();
8171
- for (const tx of txs) {
8172
- for (let i = 0; i < tx.vout.length; i++) {
8173
- const vout = tx.vout[i];
8174
- if (vout.scriptpubkey_address === boardingAddress) {
8175
- let spentStatuses = outspendCache.get(tx.txid);
8176
- if (!spentStatuses) {
8177
- spentStatuses = await this.onchainProvider.getTxOutspends(tx.txid);
8178
- outspendCache.set(tx.txid, spentStatuses);
8179
- }
8180
- const spentStatus = spentStatuses[i];
8181
- if (spentStatus?.spent) {
8182
- commitmentsToIgnore.add(spentStatus.txid);
9080
+ for (const tapscript of tapscripts) {
9081
+ const boardingAddress = tapscript.onchainAddress(this.network);
9082
+ const scriptHex = hex.encode(tapscript.pkScript);
9083
+ const txs = await this.onchainProvider.getTransactions(boardingAddress);
9084
+ for (const tx of txs) {
9085
+ for (let i = 0; i < tx.vout.length; i++) {
9086
+ const vout = tx.vout[i];
9087
+ if (vout.scriptpubkey_address === boardingAddress) {
9088
+ let spentStatuses = outspendCache.get(tx.txid);
9089
+ if (!spentStatuses) {
9090
+ spentStatuses = await this.onchainProvider.getTxOutspends(tx.txid);
9091
+ outspendCache.set(tx.txid, spentStatuses);
9092
+ }
9093
+ const spentStatus = spentStatuses[i];
9094
+ if (spentStatus?.spent) {
9095
+ commitmentsToIgnore.add(spentStatus.txid);
9096
+ }
9097
+ utxos.push({
9098
+ txid: tx.txid,
9099
+ vout: i,
9100
+ value: Number(vout.value),
9101
+ status: {
9102
+ confirmed: tx.status.confirmed,
9103
+ block_time: tx.status.block_time
9104
+ },
9105
+ isUnrolled: true,
9106
+ virtualStatus: {
9107
+ state: spentStatus?.spent ? "spent" : "settled",
9108
+ commitmentTxIds: spentStatus?.spent ? [spentStatus.txid] : void 0
9109
+ },
9110
+ createdAt: tx.status.confirmed ? new Date(tx.status.block_time * 1e3) : /* @__PURE__ */ new Date(0),
9111
+ script: scriptHex
9112
+ });
8183
9113
  }
8184
- utxos.push({
8185
- txid: tx.txid,
8186
- vout: i,
8187
- value: Number(vout.value),
8188
- status: {
8189
- confirmed: tx.status.confirmed,
8190
- block_time: tx.status.block_time
8191
- },
8192
- isUnrolled: true,
8193
- virtualStatus: {
8194
- state: spentStatus?.spent ? "spent" : "settled",
8195
- commitmentTxIds: spentStatus?.spent ? [spentStatus.txid] : void 0
8196
- },
8197
- createdAt: tx.status.confirmed ? new Date(tx.status.block_time * 1e3) : /* @__PURE__ */ new Date(0),
8198
- script: hex.encode(this.boardingTapscript.pkScript)
8199
- });
8200
9114
  }
8201
9115
  }
8202
9116
  }
@@ -8226,48 +9140,176 @@ var ReadonlyWallet = class _ReadonlyWallet {
8226
9140
  };
8227
9141
  }
8228
9142
  /**
8229
- * Fetch and cache onchain inputs (UTXOs) received at the boarding address.
9143
+ * The set of boarding tapscripts whose on-chain UTXOs belong to this
9144
+ * wallet — the current display tapscript plus every historical boarding
9145
+ * address it has used. Under per-derivation rotation (plan §6-II) a wallet
9146
+ * can hold unspent boarding UTXOs at several addresses at once, so fund
9147
+ * discovery / spending must enumerate them all, not just the current one
9148
+ * (plan §6-III.1). Deduplicated by scriptPubKey.
9149
+ *
9150
+ * Always includes the index-0 baseline (identity x-only key), which covers
9151
+ * the degenerate equal-delay case where the index-0 boarding row is
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.
8230
9160
  */
8231
- async getBoardingUtxos() {
8232
- const boardingAddress = await this.getBoardingAddress();
8233
- const boardingUtxos = await this.onchainProvider.getCoins(boardingAddress);
8234
- const utxos = boardingUtxos.map((utxo) => {
8235
- return extendCoin(this, utxo);
9161
+ async getBoardingTapscripts(allowedSigners) {
9162
+ const byScript = /* @__PURE__ */ new Map();
9163
+ const add = (s) => byScript.set(hex.encode(s.pkScript), s);
9164
+ const boardingCsv = this.boardingTapscript.options.csvTimelock ?? DefaultVtxo.Script.DEFAULT_TIMELOCK;
9165
+ add(
9166
+ new DefaultVtxo.Script({
9167
+ pubKey: await this.identity.xOnlyPublicKey(),
9168
+ serverPubKey: this.boardingTapscript.options.serverPubKey,
9169
+ csvTimelock: boardingCsv
9170
+ })
9171
+ );
9172
+ add(this.boardingTapscript);
9173
+ const serverPubKeyHex = hex.encode(this.boardingTapscript.options.serverPubKey);
9174
+ const allowed = allowedSigners ?? /* @__PURE__ */ new Set([toXOnlySignerHex(serverPubKeyHex)]);
9175
+ const boardingContracts = await this.contractRepository.getContracts({
9176
+ type: ["boarding"]
8236
9177
  });
8237
- await this.walletRepository.saveUtxos(boardingAddress, utxos);
8238
- return utxos;
9178
+ for (const c of boardingContracts) {
9179
+ if (!allowed.has(toXOnlySignerHex(c.params.serverPubKey))) continue;
9180
+ try {
9181
+ add(BoardingContractHandler.createScript(c.params));
9182
+ } catch (e) {
9183
+ console.warn("Skipping malformed boarding contract", c.script, e);
9184
+ }
9185
+ }
9186
+ return [...byScript.values()];
9187
+ }
9188
+ /**
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}).
9202
+ */
9203
+ async getBoardingUtxosForSigners(allowedSigners) {
9204
+ const tapscripts = await this.getBoardingTapscripts(allowedSigners);
9205
+ const groups = [];
9206
+ for (const tapscript of tapscripts) {
9207
+ const address = tapscript.onchainAddress(this.network);
9208
+ const coins = await this.onchainProvider.getCoins(address);
9209
+ const utxos = coins.map((utxo) => extendCoinWithTapscript(tapscript, utxo));
9210
+ await this.walletRepository.saveUtxos(address, 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
+ });
9222
+ }
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);
8239
9244
  }
8240
9245
  /**
8241
9246
  * Subscribe to onchain and offchain notifications for newly received funds.
8242
9247
  *
9248
+ * The onchain watcher tracks the full boarding-address set (current +
9249
+ * historical rotated). When boarding rotates *after* subscribing — e.g.
9250
+ * rotate-on-board allocates a fresh address via
9251
+ * {@link getNewBoardingAddress} — the watcher automatically re-subscribes
9252
+ * to widen its set, so a deposit to the new address fires a notification
9253
+ * within the same session (no watcher re-init required). The re-subscribe
9254
+ * is driven by {@link onBoardingRotation}; static / `auto` / readonly
9255
+ * wallets never rotate boarding, so it never fires for them.
9256
+ *
8243
9257
  * @param eventCallback - Callback invoked when matching funds are detected
8244
9258
  * @returns A function that stops the subscriptions
8245
9259
  */
8246
9260
  async notifyIncomingFunds(eventCallback) {
8247
9261
  const arkAddress = await this.getAddress();
8248
- const boardingAddress = await this.getBoardingAddress();
8249
9262
  let onchainStopFunc;
8250
9263
  let indexerStopFunc;
8251
- if (this.onchainProvider && boardingAddress) {
8252
- const findVoutOnTx = (tx) => {
8253
- return tx.vout.findIndex((v) => v.scriptpubkey_address === boardingAddress);
8254
- };
8255
- onchainStopFunc = await this.onchainProvider.watchAddresses(
8256
- [boardingAddress],
8257
- (txs) => {
8258
- const coins = txs.filter((tx) => findVoutOnTx(tx) !== -1).map((tx) => {
8259
- const { txid, status } = tx;
8260
- const vout = findVoutOnTx(tx);
8261
- const value = Number(tx.vout[vout].value);
8262
- return { txid, vout, value, status };
8263
- });
8264
- eventCallback({
8265
- type: "utxo",
8266
- coins
8267
- });
9264
+ let boardingRotationStopFunc;
9265
+ let stopped = false;
9266
+ let onchainChain = Promise.resolve();
9267
+ const subscribeOnchain = () => {
9268
+ onchainChain = onchainChain.then(async () => {
9269
+ if (stopped || !this.onchainProvider) return;
9270
+ const boardingAddresses = await this.getBoardingAddresses();
9271
+ if (boardingAddresses.length === 0) return;
9272
+ const boardingAddressSet = new Set(boardingAddresses);
9273
+ const previousStop = onchainStopFunc;
9274
+ const stop = await this.onchainProvider.watchAddresses(
9275
+ boardingAddresses,
9276
+ (txs) => {
9277
+ const coins = txs.flatMap((tx) => {
9278
+ const { txid, status } = tx;
9279
+ const matched = [];
9280
+ tx.vout.forEach((v, vout) => {
9281
+ if (boardingAddressSet.has(v.scriptpubkey_address)) {
9282
+ matched.push({
9283
+ txid,
9284
+ vout,
9285
+ value: Number(v.value),
9286
+ status
9287
+ });
9288
+ }
9289
+ });
9290
+ return matched;
9291
+ });
9292
+ eventCallback({
9293
+ type: "utxo",
9294
+ coins
9295
+ });
9296
+ }
9297
+ );
9298
+ if (stopped) {
9299
+ stop();
9300
+ return;
8268
9301
  }
8269
- );
8270
- }
9302
+ onchainStopFunc = stop;
9303
+ previousStop?.();
9304
+ }).catch((e) => {
9305
+ console.warn("Failed to (re)subscribe boarding-funds watcher", e);
9306
+ });
9307
+ return onchainChain;
9308
+ };
9309
+ boardingRotationStopFunc = this.onBoardingRotation(() => {
9310
+ void subscribeOnchain();
9311
+ });
9312
+ await subscribeOnchain();
8271
9313
  if (this.indexerProvider && arkAddress) {
8272
9314
  const cm = await this.getContractManager();
8273
9315
  let annotationQueue = Promise.resolve();
@@ -8296,7 +9338,10 @@ var ReadonlyWallet = class _ReadonlyWallet {
8296
9338
  });
8297
9339
  }
8298
9340
  const stopFunc = () => {
9341
+ stopped = true;
9342
+ boardingRotationStopFunc?.();
8299
9343
  onchainStopFunc?.();
9344
+ onchainStopFunc = void 0;
8300
9345
  indexerStopFunc?.();
8301
9346
  };
8302
9347
  return stopFunc;
@@ -8402,60 +9447,82 @@ var ReadonlyWallet = class _ReadonlyWallet {
8402
9447
  watcherConfig: this.watcherConfig
8403
9448
  });
8404
9449
  const baselinePubkey = await this.identity.xOnlyPublicKey();
8405
- for (const csvTimelock of this.walletContractTimelocks) {
8406
- const csvTimelockStr = timelockToSequence(csvTimelock).toString();
8407
- 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({
8408
9507
  pubKey: baselinePubkey,
8409
- serverPubKey: this.offchainTapscript.options.serverPubKey,
8410
- csvTimelock
9508
+ serverPubKey,
9509
+ csvTimelock: boardingCsvTimelock
8411
9510
  });
8412
- const defaultScriptHex = hex.encode(defaultScript.pkScript);
9511
+ const boardingScriptHex = hex.encode(baselineBoarding.pkScript);
9512
+ if (seenBaselineScripts.has(boardingScriptHex)) continue;
9513
+ seenBaselineScripts.add(boardingScriptHex);
8413
9514
  await ensureWalletContract(manager, {
8414
- type: "default",
9515
+ type: "boarding",
8415
9516
  params: {
8416
- pubKey: hex.encode(defaultScript.options.pubKey),
8417
- serverPubKey: hex.encode(defaultScript.options.serverPubKey),
8418
- csvTimelock: csvTimelockStr
9517
+ pubKey: hex.encode(baselineBoarding.options.pubKey),
9518
+ serverPubKey: hex.encode(serverPubKey),
9519
+ csvTimelock: timelockToSequence(boardingCsvTimelock).toString()
8419
9520
  },
8420
- script: defaultScriptHex,
8421
- address: defaultScript.address(this.network.hrp, this.arkServerPublicKey).encode(),
9521
+ script: boardingScriptHex,
9522
+ address: baselineBoarding.address(this.network.hrp, serverPubKey).encode(),
8422
9523
  state: "active"
8423
9524
  });
8424
- if (this.offchainTapscript instanceof DelegateVtxo.Script) {
8425
- const delegateScript = new DelegateVtxo.Script({
8426
- pubKey: baselinePubkey,
8427
- serverPubKey: this.offchainTapscript.options.serverPubKey,
8428
- delegatePubKey: this.offchainTapscript.options.delegatePubKey,
8429
- csvTimelock
8430
- });
8431
- const delegateScriptHex = hex.encode(delegateScript.pkScript);
8432
- await manager.createContract({
8433
- type: "delegate",
8434
- params: {
8435
- pubKey: hex.encode(delegateScript.options.pubKey),
8436
- serverPubKey: hex.encode(delegateScript.options.serverPubKey),
8437
- delegatePubKey: hex.encode(delegateScript.options.delegatePubKey),
8438
- csvTimelock: csvTimelockStr
8439
- },
8440
- script: delegateScriptHex,
8441
- address: delegateScript.address(this.network.hrp, this.arkServerPublicKey).encode(),
8442
- state: "active"
8443
- });
8444
- }
8445
9525
  }
8446
- const boardingScriptHex = hex.encode(this.boardingTapscript.pkScript);
8447
- const boardingCsvTimelock = this.boardingTapscript.options.csvTimelock ?? DefaultVtxo.Script.DEFAULT_TIMELOCK;
8448
- await ensureWalletContract(manager, {
8449
- type: "boarding",
8450
- params: {
8451
- pubKey: hex.encode(this.boardingTapscript.options.pubKey),
8452
- serverPubKey: hex.encode(this.boardingTapscript.options.serverPubKey),
8453
- csvTimelock: timelockToSequence(boardingCsvTimelock).toString()
8454
- },
8455
- script: boardingScriptHex,
8456
- address: this.boardingTapscript.address(this.network.hrp, this.arkServerPublicKey).encode(),
8457
- state: "active"
8458
- });
8459
9526
  return manager;
8460
9527
  }
8461
9528
  /** Dispose wallet-owned managers and release background resources. */
@@ -8488,7 +9555,6 @@ var Wallet2 = class _Wallet extends ReadonlyWallet {
8488
9555
  walletContractTimelocks
8489
9556
  );
8490
9557
  this.arkProvider = arkProvider;
8491
- this.serverUnrollScript = serverUnrollScript;
8492
9558
  this.forfeitOutputScript = forfeitOutputScript;
8493
9559
  this.forfeitPubkey = forfeitPubkey;
8494
9560
  this.identity = identity;
@@ -8509,6 +9575,7 @@ var Wallet2 = class _Wallet extends ReadonlyWallet {
8509
9575
  this.settlementConfig = { ...DEFAULT_SETTLEMENT_CONFIG };
8510
9576
  }
8511
9577
  this._delegateManager = delegateProvider ? new DelegateManagerImpl(delegateProvider, arkProvider, identity) : void 0;
9578
+ this._serverUnrollScript = serverUnrollScript;
8512
9579
  this._receiveRotator = receiveRotator;
8513
9580
  this._descriptorProvider = descriptorProvider;
8514
9581
  this._signerRouter = new InputSignerRouter({
@@ -8534,6 +9601,43 @@ var Wallet2 = class _Wallet extends ReadonlyWallet {
8534
9601
  * the contract manager is up first.
8535
9602
  */
8536
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
+ }
8537
9641
  _receiveRotatorInstalled = false;
8538
9642
  /**
8539
9643
  * Descriptor-aware signer used by {@link _signerRouter} to sign
@@ -8553,6 +9657,230 @@ var Wallet2 = class _Wallet extends ReadonlyWallet {
8553
9657
  setOffchainTapscriptForRotation(tapscript) {
8554
9658
  this._offchainTapscript = tapscript;
8555
9659
  }
9660
+ /**
9661
+ * @internal Sole write path for `boardingTapscript` after construction.
9662
+ * Called by {@link Wallet.getNewBoardingAddress} once the rotated
9663
+ * boarding contract has been persisted. External code must treat
9664
+ * `boardingTapscript` as read-only.
9665
+ */
9666
+ setBoardingTapscriptForRotation(tapscript) {
9667
+ this._boardingTapscript = tapscript;
9668
+ this.notifyBoardingRotation();
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();
9708
+ /**
9709
+ * Allocate and return a *fresh* on-chain boarding address, rotating the
9710
+ * wallet's current boarding tapscript to a new HD index.
9711
+ *
9712
+ * This is the explicit boarding allocator — the analogue of dotnet's
9713
+ * `GetNextContract(NextContractPurpose.Boarding)`. Unlike
9714
+ * {@link getBoardingAddress} (a stable read of the current display
9715
+ * address that never burns an index), each call here:
9716
+ *
9717
+ * - allocates the next index from the shared HD stream (so boarding and
9718
+ * L2 receive interleave on one monotonic index);
9719
+ * - builds the boarding tapscript at that index with the boarding-exit
9720
+ * CSV;
9721
+ * - persists an `active` `boarding` contract tagged
9722
+ * {@link WALLET_RECEIVE_SOURCE} (with its `signingDescriptor`) so the
9723
+ * ContractWatcher monitors it, boot can restore it as the current
9724
+ * boarding address, and descriptor-aware signing can recover the
9725
+ * per-index key;
9726
+ * - swaps the wallet's current `boardingTapscript`.
9727
+ *
9728
+ * Gated by `walletMode`: a static / `auto` wallet has no descriptor
9729
+ * provider and keeps a single index-0 boarding address for its lifetime,
9730
+ * so this returns the existing {@link getBoardingAddress} unchanged
9731
+ * (no rotation, no index burned).
9732
+ */
9733
+ async getNewBoardingAddress() {
9734
+ const provider = this._descriptorProvider;
9735
+ if (!provider) {
9736
+ return this.getBoardingAddress();
9737
+ }
9738
+ const descriptor = await provider.getNextSigningDescriptor();
9739
+ const pubKey = deriveDescriptorLeafPubKey(descriptor);
9740
+ const newBoarding = new DefaultVtxo.Script({
9741
+ ...this._boardingTapscript.options,
9742
+ pubKey
9743
+ });
9744
+ const csvTimelock = newBoarding.options.csvTimelock ?? DefaultVtxo.Script.DEFAULT_TIMELOCK;
9745
+ const manager = await this.getContractManager();
9746
+ await manager.createContract({
9747
+ type: "boarding",
9748
+ params: {
9749
+ pubKey: hex.encode(pubKey),
9750
+ serverPubKey: hex.encode(newBoarding.options.serverPubKey),
9751
+ csvTimelock: timelockToSequence(csvTimelock).toString()
9752
+ },
9753
+ script: hex.encode(newBoarding.pkScript),
9754
+ address: newBoarding.address(this.network.hrp, this.arkServerPublicKey).encode(),
9755
+ state: "active",
9756
+ metadata: {
9757
+ source: WALLET_RECEIVE_SOURCE,
9758
+ signingDescriptor: descriptor
9759
+ }
9760
+ });
9761
+ this.setBoardingTapscriptForRotation(newBoarding);
9762
+ return newBoarding.onchainAddress(this.network);
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
+ }
8556
9884
  /**
8557
9885
  * Async mutex that serializes all operations submitting VTXOs to the Arkade
8558
9886
  * server (`settle`, `send`, `sendBitcoin`). This prevents VtxoManager's
@@ -8637,12 +9965,25 @@ var Wallet2 = class _Wallet extends ReadonlyWallet {
8637
9965
  const staticDescriptor = hd ? void 0 : `tr(${hex.encode(await this.identity.xOnlyPublicKey())})`;
8638
9966
  const materialize = (index) => hd ? provider.materializeDescriptorAt(index) : staticDescriptor;
8639
9967
  const delegatePubKey = this.offchainTapscript instanceof DelegateVtxo.Script ? this.offchainTapscript.options.delegatePubKey : void 0;
9968
+ const arkInfo = await this.arkProvider.getInfo();
9969
+ const currentSignerPubKey = toXOnlyPubKey(hex.decode(arkInfo.signerPubkey));
9970
+ const deprecatedSignerPubKeys = arkInfo.deprecatedSigners.map(
9971
+ (s) => toXOnlyPubKey(hex.decode(s.pubkey))
9972
+ );
8640
9973
  const deps = {
8641
9974
  indexerProvider: this.indexerProvider,
8642
9975
  onchainProvider: this.onchainProvider,
8643
9976
  network: { hrp: this.network.hrp },
8644
- serverPubKey: this.offchainTapscript.options.serverPubKey,
9977
+ // Full network for the boarding on-chain (P2TR) probe — the
9978
+ // `{ hrp }` shape above lacks the `bech32` data
9979
+ // `VtxoScript.onchainAddress` needs (plan §6-I.1).
9980
+ onchainNetwork: this.network,
9981
+ serverPubKey: currentSignerPubKey,
9982
+ deprecatedSignerPubKeys,
8645
9983
  csvTimelocks: this.walletContractTimelocks,
9984
+ // Boarding-exit CSV so the boarding handler can build its
9985
+ // candidate script (distinct from the unilateral-exit matrix).
9986
+ boardingTimelock: this.boardingTapscript.options.csvTimelock ?? DefaultVtxo.Script.DEFAULT_TIMELOCK,
8646
9987
  delegatePubKey
8647
9988
  };
8648
9989
  const result = await manager.scanContracts({
@@ -8700,6 +10041,9 @@ var Wallet2 = class _Wallet extends ReadonlyWallet {
8700
10041
  }
8701
10042
  async dispose() {
8702
10043
  await this._restoreInFlight?.catch(() => void 0);
10044
+ this._serverInfoUnsub?.();
10045
+ this._serverInfoUnsub = void 0;
10046
+ await this._serverInfoInFlight?.catch(() => void 0);
8703
10047
  let rotatorError;
8704
10048
  try {
8705
10049
  await this._receiveRotator?.dispose();
@@ -8774,6 +10118,25 @@ var Wallet2 = class _Wallet extends ReadonlyWallet {
8774
10118
  boot?.rotator,
8775
10119
  boot?.provider
8776
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
+ }
10130
+ if (boot?.provider) {
10131
+ const resolvedBoarding = await resolveBoardingBootTapscript(
10132
+ setup.contractRepository,
10133
+ setup.serverPubKey,
10134
+ setup.boardingTapscript
10135
+ );
10136
+ if (resolvedBoarding !== setup.boardingTapscript) {
10137
+ wallet.setBoardingTapscriptForRotation(resolvedBoarding);
10138
+ }
10139
+ }
8777
10140
  await wallet.getVtxoManager();
8778
10141
  return wallet;
8779
10142
  }
@@ -8796,7 +10159,7 @@ var Wallet2 = class _Wallet extends ReadonlyWallet {
8796
10159
  */
8797
10160
  async toReadonly() {
8798
10161
  const readonlyIdentity = hasToReadonly(this.identity) ? await this.identity.toReadonly() : this.identity;
8799
- return new ReadonlyWallet(
10162
+ const readonly = new ReadonlyWallet(
8800
10163
  readonlyIdentity,
8801
10164
  this.network,
8802
10165
  this.onchainProvider,
@@ -8811,6 +10174,8 @@ var Wallet2 = class _Wallet extends ReadonlyWallet {
8811
10174
  this.watcherConfig,
8812
10175
  this.walletContractTimelocks
8813
10176
  );
10177
+ readonly._deprecatedSigners = new Map(this._deprecatedSigners);
10178
+ return readonly;
8814
10179
  }
8815
10180
  /** Returns the delegate manager when delegation support is configured. */
8816
10181
  async getDelegateManager() {
@@ -8836,10 +10201,9 @@ var Wallet2 = class _Wallet extends ReadonlyWallet {
8836
10201
  if (params.selectedVtxos && params.selectedVtxos.length > 0) {
8837
10202
  return this._withTxLock(async () => {
8838
10203
  const offchainTapscript = this.offchainTapscript;
8839
- const arkAddress = offchainTapscript.address(
8840
- this.network.hrp,
8841
- this.arkServerPublicKey
8842
- );
10204
+ const serverPubKey = this.arkServerPublicKey;
10205
+ const serverUnrollScript = this.serverUnrollScript;
10206
+ const arkAddress = offchainTapscript.address(this.network.hrp, serverPubKey);
8843
10207
  const selectedVtxoSum = params.selectedVtxos.map((v) => v.value).reduce((a, b) => a + b, 0);
8844
10208
  if (selectedVtxoSum < params.amount) {
8845
10209
  throw new Error("Selected VTXOs do not cover specified amount");
@@ -8864,25 +10228,14 @@ var Wallet2 = class _Wallet extends ReadonlyWallet {
8864
10228
  amount: BigInt(selected.changeAmount)
8865
10229
  });
8866
10230
  }
8867
- this._addPendingSpends(selected.inputs);
8868
- try {
8869
- const { arkTxid, signedCheckpointTxs } = await this.buildAndSubmitOffchainTx(
8870
- selected.inputs,
8871
- outputs
8872
- );
8873
- await this.updateDbAfterOffchainTx(
8874
- selected.inputs,
8875
- arkTxid,
8876
- signedCheckpointTxs,
8877
- params.amount,
8878
- selected.changeAmount,
8879
- selected.changeAmount > 0n ? outputs.length - 1 : 0,
8880
- offchainTapscript
8881
- );
8882
- return arkTxid;
8883
- } finally {
8884
- this._removePendingSpends(selected.inputs);
8885
- }
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
+ });
8886
10239
  });
8887
10240
  }
8888
10241
  return this.send({
@@ -8912,8 +10265,11 @@ var Wallet2 = class _Wallet extends ReadonlyWallet {
8912
10265
  }
8913
10266
  }
8914
10267
  }
10268
+ const offchainAddress = await this.getAddress();
10269
+ const offchainPkScript = ArkAddress.decode(offchainAddress).pkScript;
10270
+ const offchainOutputScript = hex.encode(offchainPkScript);
8915
10271
  if (!params) {
8916
- const { fees } = await this.arkProvider.getInfo();
10272
+ const { fees, vtxoMaxAmount } = await this.arkProvider.getInfo();
8917
10273
  const estimator = new Estimator(fees.intentFee);
8918
10274
  let amount = 0;
8919
10275
  const exitScript = CSVMultisigTapscript.decode(
@@ -8941,7 +10297,10 @@ var Wallet2 = class _Wallet extends ReadonlyWallet {
8941
10297
  }
8942
10298
  const vtxos = await this.getVtxos({ withRecoverable: true });
8943
10299
  const filteredVtxos = [];
8944
- for (const vtxo of vtxos) {
10300
+ for (const vtxo of byValueDescending(vtxos)) {
10301
+ if (filteredVtxos.length >= MAX_VTXOS_PER_SETTLEMENT) {
10302
+ break;
10303
+ }
8945
10304
  const inputFee = estimator.evalOffchainInput({
8946
10305
  amount: BigInt(vtxo.value),
8947
10306
  type: vtxo.virtualStatus.state === "swept" ? "recoverable" : "vtxo",
@@ -8952,20 +10311,31 @@ var Wallet2 = class _Wallet extends ReadonlyWallet {
8952
10311
  if (inputFee.satoshis >= vtxo.value) {
8953
10312
  continue;
8954
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
+ }
8955
10325
  filteredVtxos.push(vtxo);
8956
- amount += vtxo.value - inputFee.satoshis;
10326
+ amount += net;
8957
10327
  }
8958
10328
  const inputs = [...filteredBoardingUtxos, ...filteredVtxos];
8959
10329
  if (inputs.length === 0) {
8960
10330
  throw new Error("No inputs found");
8961
10331
  }
8962
10332
  const output = {
8963
- address: await this.getAddress(),
10333
+ address: offchainAddress,
8964
10334
  amount: BigInt(amount)
8965
10335
  };
8966
10336
  const outputFee = estimator.evalOffchainOutput({
8967
10337
  amount: output.amount,
8968
- script: hex.encode(ArkAddress.decode(output.address).pkScript)
10338
+ script: offchainOutputScript
8969
10339
  });
8970
10340
  output.amount -= BigInt(outputFee.satoshis);
8971
10341
  if (output.amount <= this.dustAmount) {
@@ -9005,8 +10375,7 @@ var Wallet2 = class _Wallet extends ReadonlyWallet {
9005
10375
  }
9006
10376
  }
9007
10377
  let outputAssets;
9008
- const destinationScript = ArkAddress.decode(await this.getAddress()).pkScript;
9009
- const assetOutputIndex = findDestinationOutputIndex(outputs, destinationScript);
10378
+ const assetOutputIndex = findDestinationOutputIndex(outputs, offchainPkScript);
9010
10379
  if (assetInputs.size > 0) {
9011
10380
  if (assetOutputIndex === -1) {
9012
10381
  throw new Error("Cannot assign assets: no output matches the destination address");
@@ -9074,6 +10443,7 @@ var Wallet2 = class _Wallet extends ReadonlyWallet {
9074
10443
  eventCallback: eventCallback ? (event) => Promise.resolve(eventCallback(event)) : void 0
9075
10444
  });
9076
10445
  await this.updateDbAfterSettle(params.inputs, commitmentTxid);
10446
+ await this.maybeRotateBoardingAfterBoard(params.inputs);
9077
10447
  return commitmentTxid;
9078
10448
  } catch (error) {
9079
10449
  const inputIds = params.inputs.map((i) => `${i.txid}:${i.vout}`).join(",");
@@ -9091,6 +10461,41 @@ var Wallet2 = class _Wallet extends ReadonlyWallet {
9091
10461
  });
9092
10462
  }
9093
10463
  }
10464
+ /**
10465
+ * Rotate the boarding address after a board (rotate-on-board trigger).
10466
+ *
10467
+ * Mirrors {@link WalletReceiveRotator}'s L2 rotation, but driven by a
10468
+ * board instead of a `vtxo_received` event: when a settle consumes at
10469
+ * least one boarding (on-chain) UTXO, the current boarding address has
10470
+ * served its purpose, so we allocate a fresh one via
10471
+ * {@link getNewBoardingAddress}. A settle that consumed only VTXOs (a
10472
+ * renewal / offboard) is not a board and leaves the boarding address
10473
+ * untouched.
10474
+ *
10475
+ * Boarding inputs are the non-VTXO coins (no `virtualStatus`), the same
10476
+ * discriminator {@link handleSettlementFinalizationEvent} uses; the
10477
+ * `typeof` guard skips arknote string inputs before the `in` test.
10478
+ *
10479
+ * No-ops for static / `auto` wallets (no descriptor provider — boarding
10480
+ * stays on its fixed index-0 address). Best-effort and non-fatal: the
10481
+ * settle has already committed and its txid must be returned, so a
10482
+ * rotation failure is logged and swallowed rather than thrown. Funds at
10483
+ * the retired boarding address remain discoverable — the old `boarding`
10484
+ * contract stays active and {@link getBoardingUtxos} fans out over the
10485
+ * full historical boarding set.
10486
+ */
10487
+ async maybeRotateBoardingAfterBoard(inputs) {
10488
+ if (!this._descriptorProvider) return;
10489
+ const consumedBoarding = inputs.some(
10490
+ (input) => typeof input !== "string" && !("virtualStatus" in input)
10491
+ );
10492
+ if (!consumedBoarding) return;
10493
+ try {
10494
+ await this.getNewBoardingAddress();
10495
+ } catch (e) {
10496
+ console.warn("Failed to rotate boarding address after board", e);
10497
+ }
10498
+ }
9094
10499
  async handleSettlementFinalizationEvent(event, inputs, forfeitOutputScript, connectorsGraph) {
9095
10500
  const signedForfeits = [];
9096
10501
  const isVtxo = (input) => "virtualStatus" in input;
@@ -9301,6 +10706,19 @@ var Wallet2 = class _Wallet extends ReadonlyWallet {
9301
10706
  }
9302
10707
  return jobs;
9303
10708
  }
10709
+ /**
10710
+ * @internal Sign an on-chain boarding exit / sweep transaction, routing
10711
+ * each input to the correct key by its `witnessUtxo.script`: the identity
10712
+ * for index-0 / static boarding, the per-index descriptor for a rotated
10713
+ * boarding UTXO (plan §6-III.3). Used by
10714
+ * {@link VtxoManager.sweepExpiredBoardingUtxos}; without it, the
10715
+ * unilateral exit of a rotated boarding UTXO would be signed with the
10716
+ * wrong (index-0) key and rejected.
10717
+ */
10718
+ async signOnchainBoardingTx(tx) {
10719
+ const signed = await this._signerRouter.sign(tx, this.inputSigningJobsFromWitnessUtxos(tx));
10720
+ return signed;
10721
+ }
9304
10722
  async safeRegisterIntent(intent, inputs) {
9305
10723
  try {
9306
10724
  return await this.arkProvider.registerIntent(intent);
@@ -9496,12 +10914,16 @@ var Wallet2 = class _Wallet extends ReadonlyWallet {
9496
10914
  throw new Error("At least one receiver is required");
9497
10915
  }
9498
10916
  const offchainTapscript = this.offchainTapscript;
9499
- 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);
9500
10920
  const address = outputAddress.encode();
9501
10921
  const recipients = validateRecipients(args, Number(this.dustAmount));
9502
- const virtualCoins = await this.getVtxos({
10922
+ const allVirtualCoins = await this.getVtxos({
9503
10923
  withRecoverable: false
9504
10924
  });
10925
+ const pendingRecovery = await this.pendingRecoveryOutpoints();
10926
+ const virtualCoins = pendingRecovery.size ? allVirtualCoins.filter((c) => !pendingRecovery.has(`${c.txid}:${c.vout}`)) : allVirtualCoins;
9505
10927
  const assetChanges = /* @__PURE__ */ new Map();
9506
10928
  let selectedCoins = [];
9507
10929
  let btcAmountToSelect = 0;
@@ -9623,33 +11045,128 @@ var Wallet2 = class _Wallet extends ReadonlyWallet {
9623
11045
  outputs.push(Extension.create([assetPacket]).txOut());
9624
11046
  }
9625
11047
  const sentAmount = recipients.reduce((sum, r) => sum + r.amount, 0);
9626
- 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);
9627
11068
  try {
9628
11069
  const { arkTxid, signedCheckpointTxs } = await this.buildAndSubmitOffchainTx(
9629
- selectedCoins,
9630
- outputs
11070
+ inputs,
11071
+ outputs,
11072
+ persist.serverUnrollScript
9631
11073
  );
9632
11074
  await this.updateDbAfterOffchainTx(
9633
- selectedCoins,
11075
+ inputs,
9634
11076
  arkTxid,
9635
11077
  signedCheckpointTxs,
9636
- sentAmount,
9637
- BigInt(changeAmount),
9638
- changeReceiver ? changeIndex : 0,
9639
- offchainTapscript,
9640
- changeReceiver?.assets
11078
+ persist.sentAmount,
11079
+ persist.changeAmount,
11080
+ persist.changeVout,
11081
+ persist.offchainTapscript,
11082
+ persist.serverPubKey,
11083
+ persist.changeAssets,
11084
+ persist.recordSentHistory ?? true
9641
11085
  );
9642
11086
  return arkTxid;
9643
11087
  } finally {
9644
- this._removePendingSpends(selectedCoins);
9645
- }
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
+ });
9646
11163
  }
9647
11164
  /**
9648
11165
  * Build an offchain transaction from the given inputs and outputs,
9649
11166
  * sign it, submit to the Arkade provider, and finalize.
9650
11167
  * @returns The Arkade transaction id and server-signed checkpoint PSBTs (for bookkeeping)
9651
11168
  */
9652
- async buildAndSubmitOffchainTx(inputs, outputs) {
11169
+ async buildAndSubmitOffchainTx(inputs, outputs, serverUnrollScript = this.serverUnrollScript) {
9653
11170
  const offchainTx = buildOffchainTx(
9654
11171
  inputs.map((input) => {
9655
11172
  return {
@@ -9658,7 +11175,7 @@ var Wallet2 = class _Wallet extends ReadonlyWallet {
9658
11175
  };
9659
11176
  }),
9660
11177
  outputs,
9661
- this.serverUnrollScript
11178
+ serverUnrollScript
9662
11179
  );
9663
11180
  const arkTxJobs = inputs.map((input, index) => ({
9664
11181
  index,
@@ -9732,14 +11249,14 @@ var Wallet2 = class _Wallet extends ReadonlyWallet {
9732
11249
  return { arkTxid, signedCheckpointTxs };
9733
11250
  }
9734
11251
  // mark virtual outputs as spent, save change outputs if any.
9735
- // `offchainTapscript` is the snapshot the caller captured under
9736
- // `_txLock` before any `await`; deriving both the change-VTXO
9737
- // metadata and `primaryAddress` from it here guarantees the local
9738
- // record matches the pkScript the server saw on the inbound
9739
- // transaction, even if `WalletReceiveRotator.rotate` swaps
9740
- // `this.offchainTapscript` mid-flight.
9741
- async updateDbAfterOffchainTx(inputs, arkTxid, signedCheckpointTxs, sentAmount, changeAmount, changeVout, offchainTapscript, changeAssets) {
9742
- 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();
9743
11260
  try {
9744
11261
  const spentVtxos = [];
9745
11262
  const commitmentTxIds = /* @__PURE__ */ new Set();
@@ -9846,19 +11363,21 @@ var Wallet2 = class _Wallet extends ReadonlyWallet {
9846
11363
  [changeVtxo]
9847
11364
  );
9848
11365
  }
9849
- await this.walletRepository.saveTransactions(primaryAddress, [
9850
- {
9851
- key: {
9852
- boardingTxid: "",
9853
- commitmentTxid: "",
9854
- arkTxid
9855
- },
9856
- amount: sentAmount,
9857
- type: "SENT" /* TxSent */,
9858
- settled: false,
9859
- createdAt
9860
- }
9861
- ]);
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
+ }
9862
11381
  } catch (e) {
9863
11382
  console.warn("error saving offchain tx to repository", e);
9864
11383
  throw e;
@@ -9867,10 +11386,9 @@ var Wallet2 = class _Wallet extends ReadonlyWallet {
9867
11386
  // mark virtual outputs as spent/settled, remove boarding inputs
9868
11387
  async updateDbAfterSettle(inputs, commitmentTxid) {
9869
11388
  try {
9870
- const boardingAddress = await this.getBoardingAddress();
9871
11389
  const spentVtxos = [];
9872
11390
  const inputArkTxIds = /* @__PURE__ */ new Set();
9873
- const boardingUtxoToRemove = /* @__PURE__ */ new Set();
11391
+ const boardingRemovalsByAddress = /* @__PURE__ */ new Map();
9874
11392
  const isVtxo = (input) => "virtualStatus" in input;
9875
11393
  const vtxoInputs = inputs.filter(isVtxo);
9876
11394
  const cm = await this.getContractManager();
@@ -9892,7 +11410,20 @@ var Wallet2 = class _Wallet extends ReadonlyWallet {
9892
11410
  isSpent: true
9893
11411
  });
9894
11412
  } else {
9895
- boardingUtxoToRemove.add(`${input.txid}:${input.vout}`);
11413
+ let sourceAddress;
11414
+ try {
11415
+ sourceAddress = VtxoScript.decode(input.tapTree).onchainAddress(
11416
+ this.network
11417
+ );
11418
+ } catch {
11419
+ sourceAddress = this.boardingTapscript.onchainAddress(this.network);
11420
+ }
11421
+ let set = boardingRemovalsByAddress.get(sourceAddress);
11422
+ if (!set) {
11423
+ set = /* @__PURE__ */ new Set();
11424
+ boardingRemovalsByAddress.set(sourceAddress, set);
11425
+ }
11426
+ set.add(`${input.txid}:${input.vout}`);
9896
11427
  }
9897
11428
  }
9898
11429
  if (spentVtxos.length > 0) {
@@ -9924,14 +11455,12 @@ var Wallet2 = class _Wallet extends ReadonlyWallet {
9924
11455
  );
9925
11456
  }
9926
11457
  }
9927
- if (boardingUtxoToRemove.size > 0) {
9928
- const currentUtxos = await this.walletRepository.getUtxos(boardingAddress);
9929
- const filtered = currentUtxos.filter(
9930
- (u) => !boardingUtxoToRemove.has(`${u.txid}:${u.vout}`)
9931
- );
9932
- await this.walletRepository.deleteUtxos(boardingAddress);
11458
+ for (const [address, toRemove] of boardingRemovalsByAddress) {
11459
+ const currentUtxos = await this.walletRepository.getUtxos(address);
11460
+ const filtered = currentUtxos.filter((u) => !toRemove.has(`${u.txid}:${u.vout}`));
11461
+ await this.walletRepository.deleteUtxos(address);
9933
11462
  if (filtered.length > 0) {
9934
- await this.walletRepository.saveUtxos(boardingAddress, filtered);
11463
+ await this.walletRepository.saveUtxos(address, filtered);
9935
11464
  }
9936
11465
  }
9937
11466
  } catch (e) {
@@ -10706,6 +12235,82 @@ var DelegateNotConfiguredError = class extends Error {
10706
12235
  };
10707
12236
  var DelegatorNotConfiguredError = DelegateNotConfiguredError;
10708
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
+ });
10709
12314
  var WalletMessageHandler = class {
10710
12315
  messageTag;
10711
12316
  wallet;
@@ -10787,7 +12392,9 @@ var WalletMessageHandler = class {
10787
12392
  // page-side PING / MESSAGE_BUS_NOT_INITIALIZED path triggered by concurrent
10788
12393
  // short requests (GET_STATUS, GET_BALANCE, ...).
10789
12394
  isLongRunning(message) {
10790
- 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
10791
12398
  // step until it hits gapLimit consecutive unused indices. The bus
10792
12399
  // deadline must not race the scan; liveness stays covered by PING.
10793
12400
  message.type === "RESTORE_WALLET";
@@ -11153,6 +12760,36 @@ var WalletMessageHandler = class {
11153
12760
  payload: { txid }
11154
12761
  });
11155
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
+ }
11156
12793
  case "RESTORE_WALLET": {
11157
12794
  const wallet = this.requireWallet();
11158
12795
  try {
@@ -11186,9 +12823,10 @@ var WalletMessageHandler = class {
11186
12823
  await this.onWalletInitialized();
11187
12824
  }
11188
12825
  async handleGetBalance() {
11189
- const [boardingUtxos, allVtxos] = await Promise.all([
12826
+ const [boardingUtxos, allVtxos, pendingOutpoints] = await Promise.all([
11190
12827
  this.getAllBoardingUtxos(),
11191
- this.getVtxosFromRepo()
12828
+ this.getVtxosFromRepo(),
12829
+ this.readonlyWallet ? this.readonlyWallet.pendingRecoveryOutpoints() : Promise.resolve(/* @__PURE__ */ new Set())
11192
12830
  ]);
11193
12831
  let confirmed = 0;
11194
12832
  let unconfirmed = 0;
@@ -11204,8 +12842,11 @@ var WalletMessageHandler = class {
11204
12842
  let settled = 0;
11205
12843
  let preconfirmed = 0;
11206
12844
  let recoverable = 0;
12845
+ let pendingRecovery = 0;
11207
12846
  for (const vtxo of spendableVtxos) {
11208
- if (vtxo.virtualStatus.state === "settled") {
12847
+ if (pendingOutpoints.has(`${vtxo.txid}:${vtxo.vout}`)) {
12848
+ pendingRecovery += vtxo.value;
12849
+ } else if (vtxo.virtualStatus.state === "settled") {
11209
12850
  settled += vtxo.value;
11210
12851
  } else if (vtxo.virtualStatus.state === "preconfirmed") {
11211
12852
  preconfirmed += vtxo.value;
@@ -11217,7 +12858,7 @@ var WalletMessageHandler = class {
11217
12858
  }
11218
12859
  }
11219
12860
  const totalBoarding = confirmed + unconfirmed;
11220
- const totalOffchain = settled + preconfirmed + recoverable;
12861
+ const totalOffchain = settled + preconfirmed + recoverable + pendingRecovery;
11221
12862
  const assetBalances = /* @__PURE__ */ new Map();
11222
12863
  for (const vtxo of spendableVtxos) {
11223
12864
  if (vtxo.assets) {
@@ -11241,6 +12882,7 @@ var WalletMessageHandler = class {
11241
12882
  preconfirmed,
11242
12883
  available: settled + preconfirmed,
11243
12884
  recoverable,
12885
+ pendingRecovery,
11244
12886
  total: totalBoarding + totalOffchain,
11245
12887
  assets
11246
12888
  };
@@ -11331,9 +12973,7 @@ var WalletMessageHandler = class {
11331
12973
  );
11332
12974
  }
11333
12975
  if (funds.type === "utxo") {
11334
- const utxos = funds.coins.map((utxo) => extendCoin(this.readonlyWallet, utxo));
11335
- const boardingAddress = await this.readonlyWallet.getBoardingAddress();
11336
- await this.walletRepository?.saveUtxos(boardingAddress, utxos);
12976
+ const utxos = await this.readonlyWallet.getBoardingUtxos();
11337
12977
  this.scheduleForNextTick(
11338
12978
  () => this.tagged({
11339
12979
  type: "UTXO_UPDATE",
@@ -11362,13 +13002,16 @@ var WalletMessageHandler = class {
11362
13002
  return;
11363
13003
  }
11364
13004
  const vtxos = await this.getVtxosFromRepo();
11365
- const boardingAddress = await this.readonlyWallet.getBoardingAddress();
11366
- const coins = await this.readonlyWallet.onchainProvider.getCoins(boardingAddress);
11367
- await this.walletRepository.deleteUtxos(boardingAddress);
11368
- await this.walletRepository.saveUtxos(
11369
- boardingAddress,
11370
- coins.map((utxo) => extendCoin(this.readonlyWallet, utxo))
11371
- );
13005
+ const boardingAddresses = await this.readonlyWallet.getBoardingAddresses();
13006
+ const fresh = await this.readonlyWallet.getBoardingUtxos();
13007
+ const freshKeys = new Set(fresh.map((u) => `${u.txid}:${u.vout}`));
13008
+ for (const addr of boardingAddresses) {
13009
+ const cached = await this.walletRepository.getUtxos(addr);
13010
+ const kept = cached.filter((u) => freshKeys.has(`${u.txid}:${u.vout}`));
13011
+ if (kept.length === cached.length) continue;
13012
+ await this.walletRepository.deleteUtxos(addr);
13013
+ if (kept.length > 0) await this.walletRepository.saveUtxos(addr, kept);
13014
+ }
11372
13015
  const address = await this.readonlyWallet.getAddress();
11373
13016
  const txs = await this.buildTransactionHistoryFromCache(vtxos);
11374
13017
  if (txs) await this.walletRepository.saveTransactions(address, txs);
@@ -11625,6 +13268,7 @@ var DEFAULT_MESSAGE_TIMEOUTS = {
11625
13268
  GET_EXPIRING_VTXOS: 2e4,
11626
13269
  GET_EXPIRED_BOARDING_UTXOS: 2e4,
11627
13270
  GET_RECOVERABLE_BALANCE: 2e4,
13271
+ GET_DEPRECATED_SIGNER_STATUS: 2e4,
11628
13272
  RELOAD_WALLET: 2e4,
11629
13273
  // Transactions — need more headroom.
11630
13274
  // SETTLE / RECOVER_VTXOS / RENEW_VTXOS go through the streaming path and
@@ -11640,6 +13284,9 @@ var DEFAULT_MESSAGE_TIMEOUTS = {
11640
13284
  RECOVER_VTXOS: 5e4,
11641
13285
  RENEW_VTXOS: 5e4,
11642
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,
11643
13290
  // RESTORE_WALLET is a streaming/long-running path (sendMessageWithEvents)
11644
13291
  // like SETTLE; the value here is kept for type completeness and is never
11645
13292
  // enforced as an inactivity deadline.
@@ -11665,6 +13312,7 @@ var DEDUPABLE_REQUEST_TYPES = /* @__PURE__ */ new Set([
11665
13312
  "GET_DELEGATE_INFO",
11666
13313
  "GET_RECOVERABLE_BALANCE",
11667
13314
  "GET_EXPIRED_BOARDING_UTXOS",
13315
+ "GET_DEPRECATED_SIGNER_STATUS",
11668
13316
  "GET_VTXOS",
11669
13317
  "GET_CONTRACTS",
11670
13318
  "GET_CONTRACTS_WITH_VTXOS",
@@ -12787,6 +14435,42 @@ var ServiceWorkerWallet = class _ServiceWorkerWallet extends ServiceWorkerReadon
12787
14435
  throw new Error(`Failed to sweep expired boarding utxos: ${e}`);
12788
14436
  }
12789
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
+ },
12790
14474
  async dispose() {
12791
14475
  return;
12792
14476
  }
@@ -13692,13 +15376,19 @@ function isHeaderSubscribeResult(v) {
13692
15376
  const obj = v;
13693
15377
  return typeof obj.height === "number" && typeof obj.hex === "string";
13694
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
+ }
13695
15387
  function isMissingHeightError(err) {
13696
- const msg = err instanceof Error ? err.message : typeof err === "string" ? err : "";
13697
- return msg.toLowerCase().includes("missingheight");
15388
+ return errorText(err).toLowerCase().includes("missingheight");
13698
15389
  }
13699
15390
  function isTxNotInBlockError(err) {
13700
- const msg = err instanceof Error ? err.message : typeof err === "string" ? err : "";
13701
- const normalized = msg.toLowerCase();
15391
+ const normalized = errorText(err).toLowerCase();
13702
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");
13703
15393
  }
13704
15394
  function childTxidFromHex(txHex) {
@@ -14158,6 +15848,6 @@ function isArkContract(str) {
14158
15848
  return str.startsWith(ARKCONTRACT_PREFIX + "=");
14159
15849
  }
14160
15850
 
14161
- 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 };
14162
- //# sourceMappingURL=chunk-DSS2GQUG.js.map
14163
- //# sourceMappingURL=chunk-DSS2GQUG.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