@arkade-os/sdk 0.4.33 → 0.4.34

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 (78) 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-Dsv5Jq4E.d.cts} +65 -7
  9. package/dist/{ark-DEsDMYGv.d.ts → ark-Dsv5Jq4E.d.ts} +65 -7
  10. package/dist/{asyncStorageTaskQueue-CMrTYlKG.d.ts → asyncStorageTaskQueue-BH-zuth5.d.ts} +1 -1
  11. package/dist/{asyncStorageTaskQueue-D8T1VXEx.d.cts → asyncStorageTaskQueue-D92ch8yI.d.cts} +1 -1
  12. package/dist/{chunk-L6ZETTX3.js → chunk-5WDBHWX3.js} +4 -4
  13. package/dist/{chunk-L6ZETTX3.js.map → chunk-5WDBHWX3.js.map} +1 -1
  14. package/dist/{chunk-5CCRRL5S.cjs → chunk-CCLNFHJ5.cjs} +11 -11
  15. package/dist/{chunk-5CCRRL5S.cjs.map → chunk-CCLNFHJ5.cjs.map} +1 -1
  16. package/dist/{chunk-WMIPYZSB.cjs → chunk-CMPJR3HS.cjs} +42 -9
  17. package/dist/chunk-CMPJR3HS.cjs.map +1 -0
  18. package/dist/{chunk-AOJUURHM.js → chunk-CUSABEUQ.js} +141 -37
  19. package/dist/chunk-CUSABEUQ.js.map +1 -0
  20. package/dist/{chunk-SPDNHPM4.cjs → chunk-FSAXPBGP.cjs} +8 -8
  21. package/dist/{chunk-SPDNHPM4.cjs.map → chunk-FSAXPBGP.cjs.map} +1 -1
  22. package/dist/{chunk-E22HEKLN.js → chunk-FXFBPXV3.js} +3 -3
  23. package/dist/{chunk-E22HEKLN.js.map → chunk-FXFBPXV3.js.map} +1 -1
  24. package/dist/{chunk-GYSK5R57.cjs → chunk-GUTKJMSF.cjs} +164 -59
  25. package/dist/chunk-GUTKJMSF.cjs.map +1 -0
  26. package/dist/{chunk-DSS2GQUG.js → chunk-HFXEUW55.js} +575 -155
  27. package/dist/chunk-HFXEUW55.js.map +1 -0
  28. package/dist/{chunk-TU3LVAPX.js → chunk-OUVTG72A.js} +43 -11
  29. package/dist/chunk-OUVTG72A.js.map +1 -0
  30. package/dist/{chunk-BU3BU6XK.js → chunk-VVGD3JIP.js} +3 -3
  31. package/dist/{chunk-BU3BU6XK.js.map → chunk-VVGD3JIP.js.map} +1 -1
  32. package/dist/{chunk-7K3ROJF6.cjs → chunk-XCHBQVMK.cjs} +718 -298
  33. package/dist/chunk-XCHBQVMK.cjs.map +1 -0
  34. package/dist/{chunk-HAVA4XB7.cjs → chunk-ZS3OZHC7.cjs} +7 -7
  35. package/dist/{chunk-HAVA4XB7.cjs.map → chunk-ZS3OZHC7.cjs.map} +1 -1
  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-BaS5SCIW.d.cts} +1 -1
  41. package/dist/{delegate-EXN2mfkb.d.ts → delegate-Baz_hb83.d.ts} +1 -1
  42. package/dist/{index-BG2ooYKO.d.ts → index-FwXZveaX.d.ts} +22 -16
  43. package/dist/{index-DHjEeHEp.d.cts → index-lNZ6qaO3.d.cts} +22 -16
  44. package/dist/index.cjs +134 -130
  45. package/dist/index.d.cts +63 -14
  46. package/dist/index.d.ts +63 -14
  47. package/dist/index.js +4 -4
  48. package/dist/repositories/realm/index.cjs +13 -13
  49. package/dist/repositories/realm/index.d.cts +1 -1
  50. package/dist/repositories/realm/index.d.ts +1 -1
  51. package/dist/repositories/realm/index.js +4 -4
  52. package/dist/repositories/sqlite/index.cjs +13 -13
  53. package/dist/repositories/sqlite/index.d.cts +1 -1
  54. package/dist/repositories/sqlite/index.d.ts +1 -1
  55. package/dist/repositories/sqlite/index.js +4 -4
  56. package/dist/{taskRunner-B7lBU45X.d.ts → taskRunner-B1NUWyWR.d.ts} +1 -1
  57. package/dist/{taskRunner-pIGyarFG.d.cts → taskRunner-vFRA3F9b.d.cts} +1 -1
  58. package/dist/wallet/expo/background.cjs +14 -14
  59. package/dist/wallet/expo/background.d.cts +3 -3
  60. package/dist/wallet/expo/background.d.ts +3 -3
  61. package/dist/wallet/expo/background.js +6 -6
  62. package/dist/wallet/expo/index.cjs +13 -13
  63. package/dist/wallet/expo/index.d.cts +5 -5
  64. package/dist/wallet/expo/index.d.ts +5 -5
  65. package/dist/wallet/expo/index.js +5 -5
  66. package/dist/{wallet-C4L_X0i6.d.ts → wallet-By9HIo0Q.d.cts} +160 -5
  67. package/dist/{wallet-D4Dll5Gu.d.cts → wallet-D6uoBLmS.d.ts} +160 -5
  68. package/dist/worker/expo/index.cjs +9 -9
  69. package/dist/worker/expo/index.d.cts +4 -4
  70. package/dist/worker/expo/index.d.ts +4 -4
  71. package/dist/worker/expo/index.js +5 -5
  72. package/package.json +4 -4
  73. package/dist/chunk-7K3ROJF6.cjs.map +0 -1
  74. package/dist/chunk-AOJUURHM.js.map +0 -1
  75. package/dist/chunk-DSS2GQUG.js.map +0 -1
  76. package/dist/chunk-GYSK5R57.cjs.map +0 -1
  77. package/dist/chunk-TU3LVAPX.js.map +0 -1
  78. 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, Intent, getArkPsbtFields, CosignerPublicKey, setArkPsbtField, VtxoTaprootTree, maybeArkError, AssetRef, AssetId, AssetOutput, AssetGroup, AssetInput, Packet, Metadata, isEventSourceError, RestArkProvider, RestIndexerProvider, ArkError, BufferReader } from './chunk-FXFBPXV3.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';
@@ -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) {
@@ -1028,6 +1028,9 @@ var EsploraProvider = class {
1028
1028
  }
1029
1029
  async getFeeRate() {
1030
1030
  const response = await fetch(`${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
  }
@@ -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 fetch(`${this.baseUrl}/blocks`);
1157
1160
  if (!tipBlocks.ok) {
1158
1161
  throw new Error(`Failed to get chain tip: ${tipBlocks.statusText}`);
1159
1162
  }
@@ -2258,12 +2261,12 @@ var FALLBACK_WALLET_DUST_AMOUNT = 330n;
2258
2261
  function getDustAmount(wallet) {
2259
2262
  return "dustAmount" in wallet ? wallet.dustAmount : FALLBACK_WALLET_DUST_AMOUNT;
2260
2263
  }
2261
- function extendCoin(wallet, utxo) {
2264
+ function extendCoinWithTapscript(boardingTapscript, utxo) {
2262
2265
  return {
2263
2266
  ...utxo,
2264
- forfeitTapLeafScript: wallet.boardingTapscript.forfeit(),
2265
- intentTapLeafScript: wallet.boardingTapscript.forfeit(),
2266
- tapTree: wallet.boardingTapscript.encode()
2267
+ forfeitTapLeafScript: boardingTapscript.forfeit(),
2268
+ intentTapLeafScript: boardingTapscript.forfeit(),
2269
+ tapTree: boardingTapscript.encode()
2267
2270
  };
2268
2271
  }
2269
2272
  function deriveContractTapscripts(contract) {
@@ -2357,12 +2360,12 @@ function validateRecipients(recipients, dustAmount) {
2357
2360
 
2358
2361
  // src/wallet/vtxo-manager.ts
2359
2362
  function isSweepCapable(wallet) {
2360
- return "boardingTapscript" in wallet && "onchainProvider" in wallet && "arkProvider" in wallet && "network" in wallet;
2363
+ return "boardingTapscript" in wallet && "onchainProvider" in wallet && "arkProvider" in wallet && "network" in wallet && "signOnchainBoardingTx" in wallet;
2361
2364
  }
2362
2365
  function assertSweepCapable(wallet) {
2363
2366
  if (!isSweepCapable(wallet)) {
2364
2367
  throw new Error(
2365
- "Boarding UTXO sweep requires a Wallet instance with boardingTapscript, onchainProvider, arkProvider, and network"
2368
+ "Boarding UTXO sweep requires a Wallet instance with boardingTapscript, onchainProvider, arkProvider, network, and signOnchainBoardingTx"
2366
2369
  );
2367
2370
  }
2368
2371
  }
@@ -2378,6 +2381,21 @@ async function runWithCrossInstanceLock(name, fn) {
2378
2381
  await fn();
2379
2382
  });
2380
2383
  }
2384
+ var MAX_VTXOS_PER_SETTLEMENT = 50;
2385
+ function byValueDescending(vtxos) {
2386
+ return [...vtxos].sort((a, b) => b.value - a.value);
2387
+ }
2388
+ function byExpiryAscending(vtxos) {
2389
+ const expiryKey = (vtxo) => {
2390
+ if (isRecoverable(vtxo)) return -Infinity;
2391
+ const batchExpiry = vtxo.virtualStatus.batchExpiry;
2392
+ if (isExpired(vtxo)) return batchExpiry ?? -Infinity;
2393
+ if (!batchExpiry) return Infinity;
2394
+ if (new Date(batchExpiry).getFullYear() < 2025) return Infinity;
2395
+ return batchExpiry;
2396
+ };
2397
+ return [...vtxos].sort((a, b) => expiryKey(a) - expiryKey(b));
2398
+ }
2381
2399
  var DEFAULT_THRESHOLD_SECONDS = 259200;
2382
2400
  var DEFAULT_THRESHOLD_MS = DEFAULT_THRESHOLD_SECONDS * 1e3;
2383
2401
  var DEFAULT_RENEWAL_CONFIG = {
@@ -2537,10 +2555,20 @@ var VtxoManager = class _VtxoManager {
2537
2555
  withUnrolled: false
2538
2556
  });
2539
2557
  const dustAmount = getDustAmount(this.wallet);
2540
- const { vtxosToRecover, totalAmount } = getRecoverableWithSubdust(allVtxos, dustAmount);
2558
+ let { vtxosToRecover, totalAmount } = getRecoverableWithSubdust(allVtxos, dustAmount);
2541
2559
  if (vtxosToRecover.length === 0) {
2542
2560
  throw new Error("No recoverable VTXOs found");
2543
2561
  }
2562
+ if (vtxosToRecover.length > MAX_VTXOS_PER_SETTLEMENT) {
2563
+ const recoverableCount = vtxosToRecover.length;
2564
+ const capped = byValueDescending(vtxosToRecover).slice(0, MAX_VTXOS_PER_SETTLEMENT);
2565
+ ({ vtxosToRecover, totalAmount } = getRecoverableWithSubdust(capped, dustAmount));
2566
+ if (vtxosToRecover.length === 0) {
2567
+ throw new Error(
2568
+ `Capped recovery batch (highest-value ${MAX_VTXOS_PER_SETTLEMENT} of ${recoverableCount} recoverable VTXOs) is below the dust threshold ${dustAmount}`
2569
+ );
2570
+ }
2571
+ }
2544
2572
  const arkAddress = await this.wallet.getAddress();
2545
2573
  return this.wallet.settle(
2546
2574
  {
@@ -2690,6 +2718,9 @@ var VtxoManager = class _VtxoManager {
2690
2718
  if (vtxos.length === 0) {
2691
2719
  throw new Error("No VTXOs available to renew");
2692
2720
  }
2721
+ if (vtxos.length > MAX_VTXOS_PER_SETTLEMENT) {
2722
+ vtxos = byExpiryAscending(vtxos).slice(0, MAX_VTXOS_PER_SETTLEMENT);
2723
+ }
2693
2724
  const totalAmount = vtxos.reduce((sum, vtxo) => sum + vtxo.value, 0);
2694
2725
  const dustAmount = getDustAmount(this.wallet);
2695
2726
  if (BigInt(totalAmount) < dustAmount) {
@@ -2797,7 +2828,6 @@ var VtxoManager = class _VtxoManager {
2797
2828
  const boardingAddress = await this.wallet.getBoardingAddress();
2798
2829
  const feeRate = await this.getOnchainProvider().getFeeRate() ?? 1;
2799
2830
  const exitTapLeafScript = this.getBoardingExitLeaf();
2800
- const sequence = getSequence(exitTapLeafScript);
2801
2831
  const leafScript = exitTapLeafScript[1];
2802
2832
  const leafScriptSize = leafScript.length - 1;
2803
2833
  const controlBlockSize = exitTapLeafScript[0].merklePath.length * 32;
@@ -2818,19 +2848,28 @@ var VtxoManager = class _VtxoManager {
2818
2848
  }
2819
2849
  const tx = new Transaction();
2820
2850
  for (const utxo of expiredUtxos) {
2851
+ const utxoScript = VtxoScript.decode(utxo.tapTree);
2852
+ const utxoExitLeaf = utxoScript.leaves.find(
2853
+ (leaf) => CSVMultisigTapscript.isScriptValid(scriptFromTapLeafScript(leaf)) === true
2854
+ );
2855
+ if (!utxoExitLeaf) {
2856
+ throw new Error(
2857
+ `Boarding sweep: no CSV exit leaf for UTXO ${utxo.txid}:${utxo.vout}`
2858
+ );
2859
+ }
2821
2860
  tx.addInput({
2822
2861
  txid: utxo.txid,
2823
2862
  index: utxo.vout,
2824
2863
  witnessUtxo: {
2825
- script: this.getBoardingOutputScript(),
2864
+ script: utxoScript.pkScript,
2826
2865
  amount: BigInt(utxo.value)
2827
2866
  },
2828
- tapLeafScript: [exitTapLeafScript],
2829
- sequence
2867
+ tapLeafScript: [utxoExitLeaf],
2868
+ sequence: getSequence(utxoExitLeaf)
2830
2869
  });
2831
2870
  }
2832
2871
  tx.addOutputAddress(boardingAddress, outputAmount, this.getNetwork());
2833
- const signedTx = await this.getIdentity().sign(tx);
2872
+ const signedTx = await this.getSweepWallet().signOnchainBoardingTx(tx);
2834
2873
  signedTx.finalize();
2835
2874
  const txid = await this.getOnchainProvider().broadcastTransaction(signedTx.hex);
2836
2875
  for (const u of expiredUtxos) {
@@ -2857,10 +2896,6 @@ var VtxoManager = class _VtxoManager {
2857
2896
  getBoardingExitLeaf() {
2858
2897
  return this.getSweepWallet().boardingTapscript.exit();
2859
2898
  }
2860
- /** Returns the pkScript (output script) of the boarding tapscript. */
2861
- getBoardingOutputScript() {
2862
- return this.getSweepWallet().boardingTapscript.pkScript;
2863
- }
2864
2899
  /** Returns the onchain provider for fee estimation and broadcasting. */
2865
2900
  getOnchainProvider() {
2866
2901
  return this.getSweepWallet().onchainProvider;
@@ -2873,10 +2908,6 @@ var VtxoManager = class _VtxoManager {
2873
2908
  getNetwork() {
2874
2909
  return this.getSweepWallet().network;
2875
2910
  }
2876
- /** Returns the wallet's identity for transaction signing. */
2877
- getIdentity() {
2878
- return this.wallet.identity;
2879
- }
2880
2911
  async initializeSubscription() {
2881
2912
  if (this.settlementConfig === false) {
2882
2913
  return void 0;
@@ -3157,7 +3188,10 @@ var VtxoManager = class _VtxoManager {
3157
3188
  totalAmount += BigInt(u.value) - BigInt(inputFee.satoshis);
3158
3189
  }
3159
3190
  const filteredVtxos = [];
3160
- for (const v of expiringVtxos) {
3191
+ for (const v of byExpiryAscending(expiringVtxos)) {
3192
+ if (filteredVtxos.length >= MAX_VTXOS_PER_SETTLEMENT) {
3193
+ break;
3194
+ }
3161
3195
  const inputFee = estimator.evalOffchainInput({
3162
3196
  amount: BigInt(v.value),
3163
3197
  type: v.virtualStatus.state === "swept" ? "recoverable" : "vtxo",
@@ -6515,8 +6549,11 @@ function cursorCutoff(requestStartedAt) {
6515
6549
  }
6516
6550
 
6517
6551
  // src/contracts/contractManager.ts
6518
- var DEFAULT_PAGE_SIZE = 500;
6552
+ function areCoalescibleContractTypes(a, b) {
6553
+ return a === "default" && b === "boarding" || a === "boarding" && b === "default";
6554
+ }
6519
6555
  var SCAN_MAX_INDEX = 1e4;
6556
+ var DEFAULT_SCAN_BATCH = 10;
6520
6557
  var ContractManager = class _ContractManager {
6521
6558
  config;
6522
6559
  watcher;
@@ -6640,8 +6677,11 @@ var ContractManager = class _ContractManager {
6640
6677
  const [existing] = await this.getContracts({ script: params.script });
6641
6678
  if (existing) {
6642
6679
  if (existing.type === params.type) return { contract: existing, persisted: false };
6680
+ if (areCoalescibleContractTypes(existing.type, params.type)) {
6681
+ return { contract: existing, persisted: false };
6682
+ }
6643
6683
  throw new Error(
6644
- `Contract with script ${params.script} already exists with with type ${existing.type}.`
6684
+ `Contract with script ${params.script} already exists with type ${existing.type}.`
6645
6685
  );
6646
6686
  }
6647
6687
  const contract = {
@@ -6670,6 +6710,19 @@ var ContractManager = class _ContractManager {
6670
6710
  * other handler hit it).
6671
6711
  * - `persistAndWatchContract` rejecting is operational/fatal and
6672
6712
  * propagates (only `discoverAt` is guarded).
6713
+ * - Within an index the handler probes run concurrently (independent
6714
+ * network reads); their hits are persisted sequentially in
6715
+ * `discoverables` order to preserve the first-wins collision tie-break.
6716
+ * - Indices are probed `batchSize` at a time (a second concurrency layer
6717
+ * over the per-index probes), but each window is CAPPED to
6718
+ * `gapLimit - unused` indices — the most a serial scan could still reach
6719
+ * before the gap window is guaranteed to close. So every index probed in
6720
+ * a window is one a one-index-at-a-time scan would also reach: nothing is
6721
+ * over-scanned, nothing is discarded, and `materialize`/`discoverAt` are
6722
+ * invoked on exactly the same index set. The window's hits are still
6723
+ * processed strictly in ascending index order, so the discovered set,
6724
+ * persisted rows, `lastIndexUsed`, and `handlerErrors` are byte-for-byte
6725
+ * identical to the serial path — only the wall-clock differs.
6673
6726
  */
6674
6727
  async scanContracts(opts) {
6675
6728
  const gapLimit = opts.gapLimit ?? 20;
@@ -6678,35 +6731,69 @@ var ContractManager = class _ContractManager {
6678
6731
  `scanContracts: gapLimit must be a positive integer (got ${String(opts.gapLimit)})`
6679
6732
  );
6680
6733
  }
6681
- const discoverables = contractHandlers.getRegisteredTypes().map((t) => contractHandlers.get(t)).filter(isDiscoverable);
6734
+ const batchSize = opts.batchSize ?? DEFAULT_SCAN_BATCH;
6735
+ if (!Number.isInteger(batchSize) || batchSize <= 0) {
6736
+ throw new Error(
6737
+ `scanContracts: batchSize must be a positive integer (got ${String(opts.batchSize)})`
6738
+ );
6739
+ }
6740
+ const registered = contractHandlers.getRegisteredTypes().map((t) => contractHandlers.get(t)).filter(isDiscoverable);
6741
+ const discoverables = [
6742
+ ...registered.filter((h) => h.type === "boarding"),
6743
+ ...registered.filter((h) => h.type !== "boarding")
6744
+ ];
6682
6745
  const maxIdx = opts.hd ? SCAN_MAX_INDEX : 0;
6683
6746
  const handlerErrors = [];
6684
6747
  let lastIndexUsed = -1;
6685
6748
  let unused = 0;
6686
6749
  let i = 0;
6750
+ const probeIndex = async (index) => {
6751
+ const descriptor = opts.materialize(index);
6752
+ return Promise.all(
6753
+ discoverables.map(async (h) => {
6754
+ try {
6755
+ return {
6756
+ ok: true,
6757
+ found: await h.discoverAt(index, descriptor, opts.deps)
6758
+ };
6759
+ } catch (error) {
6760
+ return { ok: false, error };
6761
+ }
6762
+ })
6763
+ );
6764
+ };
6687
6765
  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;
6766
+ const windowEnd = Math.min(maxIdx, i + Math.min(batchSize, gapLimit - unused) - 1);
6767
+ const windowIndices = [];
6768
+ for (let idx = i; idx <= windowEnd; idx++) windowIndices.push(idx);
6769
+ const windowProbes = await Promise.all(windowIndices.map(probeIndex));
6770
+ for (let w = 0; w < windowIndices.length; w++) {
6771
+ const index = windowIndices[w];
6772
+ const probes = windowProbes[w];
6773
+ let hitAtThisIndex = false;
6774
+ for (let h = 0; h < discoverables.length; h++) {
6775
+ const probe = probes[h];
6776
+ if (!probe.ok) {
6777
+ handlerErrors.push({
6778
+ handler: discoverables[h].type,
6779
+ index,
6780
+ error: probe.error
6781
+ });
6782
+ continue;
6783
+ }
6784
+ for (const c of probe.found) {
6785
+ await this.persistAndWatchContract(c);
6786
+ hitAtThisIndex = true;
6787
+ }
6697
6788
  }
6698
- for (const c of found) {
6699
- await this.persistAndWatchContract(c);
6700
- hitAtThisIndex = true;
6789
+ if (hitAtThisIndex) {
6790
+ lastIndexUsed = index;
6791
+ unused = 0;
6792
+ } else {
6793
+ unused += 1;
6701
6794
  }
6702
6795
  }
6703
- if (hitAtThisIndex) {
6704
- lastIndexUsed = i;
6705
- unused = 0;
6706
- } else {
6707
- unused += 1;
6708
- }
6709
- i += 1;
6796
+ i = windowEnd + 1;
6710
6797
  }
6711
6798
  if (opts.hd && i > maxIdx && unused < gapLimit) {
6712
6799
  throw new Error(
@@ -7412,7 +7499,8 @@ var WalletReceiveRotator = class _WalletReceiveRotator {
7412
7499
  walletRepository: setup.walletRepository,
7413
7500
  contractRepository: setup.contractRepository,
7414
7501
  serverPubKey: setup.serverPubKey,
7415
- expectedContractType
7502
+ expectedContractType,
7503
+ baselineReceivePubKey: setup.offchainTapscript.options.pubKey
7416
7504
  };
7417
7505
  let boot;
7418
7506
  try {
@@ -7457,14 +7545,17 @@ var WalletReceiveRotator = class _WalletReceiveRotator {
7457
7545
  receivePubkey: existing.pubKey
7458
7546
  };
7459
7547
  }
7460
- let descriptor;
7461
- if (hasPeekableDescriptor(provider)) {
7462
- descriptor = await provider.getCurrentSigningDescriptor();
7548
+ const current = hasPeekableDescriptor(provider) ? await provider.getCurrentSigningDescriptor() : void 0;
7549
+ if (current === void 0) {
7550
+ const descriptor = await provider.getNextSigningDescriptor();
7551
+ return {
7552
+ rotator: new _WalletReceiveRotator(provider, void 0, opts.logger),
7553
+ receivePubkey: deriveLeafPubkey(descriptor)
7554
+ };
7463
7555
  }
7464
- descriptor ??= await provider.getNextSigningDescriptor();
7465
7556
  return {
7466
7557
  rotator: new _WalletReceiveRotator(provider, void 0, opts.logger),
7467
- receivePubkey: deriveLeafPubkey(descriptor)
7558
+ receivePubkey: opts.baselineReceivePubKey ?? deriveLeafPubkey(current)
7468
7559
  };
7469
7560
  }
7470
7561
  /**
@@ -7697,7 +7788,7 @@ var DescriptorSigningProviderMissingError = class extends Error {
7697
7788
  };
7698
7789
 
7699
7790
  // src/wallet/inputSignerRouter.ts
7700
- var DESCRIPTOR_CAPABLE_CONTRACT_TYPES = /* @__PURE__ */ new Set(["default", "delegate"]);
7791
+ var DESCRIPTOR_CAPABLE_CONTRACT_TYPES = /* @__PURE__ */ new Set(["default", "delegate", "boarding"]);
7701
7792
  var InputSignerRouter = class {
7702
7793
  constructor(deps) {
7703
7794
  this.deps = deps;
@@ -7829,6 +7920,11 @@ function extractArkProviderUrl(provider) {
7829
7920
  return typeof serverUrl === "string" && serverUrl.length > 0 ? serverUrl : void 0;
7830
7921
  }
7831
7922
  var MAINNET_UNILATERAL_EXIT_DELAY = 605184n;
7923
+ function toXOnlyPubKey(pubkey) {
7924
+ if (pubkey.length === 33) return pubkey.slice(1);
7925
+ if (pubkey.length === 32) return pubkey;
7926
+ throw new Error(`invalid signer pubkey length: expected 32 or 33, got ${pubkey.length}`);
7927
+ }
7832
7928
  function delayToTimelock(delay) {
7833
7929
  return {
7834
7930
  value: delay,
@@ -7846,20 +7942,30 @@ function dedupeTimelocks(timelocks) {
7846
7942
  }
7847
7943
  return deduped;
7848
7944
  }
7849
- function areSameScriptBaselineTypesCompatible(existingType, requestedType) {
7850
- if (existingType === requestedType) return true;
7851
- return existingType === "default" && requestedType === "boarding" || existingType === "boarding" && requestedType === "default";
7852
- }
7853
7945
  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
7946
  await manager.createContract(params);
7862
7947
  }
7948
+ async function resolveBoardingBootTapscript(contractRepository, serverPubKey, baseline) {
7949
+ const serverPubKeyHex = hex.encode(serverPubKey);
7950
+ const candidates = await contractRepository.getContracts({
7951
+ type: ["boarding"],
7952
+ state: "active"
7953
+ });
7954
+ const newest = candidates.filter(
7955
+ (c) => c.params.serverPubKey === serverPubKeyHex && c.metadata?.source === WALLET_RECEIVE_SOURCE
7956
+ ).sort((a, b) => {
7957
+ if (b.createdAt !== a.createdAt) return b.createdAt - a.createdAt;
7958
+ return signingDescriptorIndex(b.metadata?.signingDescriptor) - signingDescriptorIndex(a.metadata?.signingDescriptor);
7959
+ })[0];
7960
+ if (!newest?.params.pubKey) return baseline;
7961
+ try {
7962
+ const pubKey = hex.decode(newest.params.pubKey);
7963
+ return new DefaultVtxo.Script({ ...baseline.options, pubKey });
7964
+ } catch (e) {
7965
+ console.warn("Skipping malformed boarding contract at boot", newest.script, e);
7966
+ return baseline;
7967
+ }
7968
+ }
7863
7969
  function hasToReadonly(identity) {
7864
7970
  return typeof identity === "object" && identity !== null && "toReadonly" in identity && typeof identity.toReadonly === "function";
7865
7971
  }
@@ -7870,7 +7976,6 @@ var ReadonlyWallet = class _ReadonlyWallet {
7870
7976
  this.onchainProvider = onchainProvider;
7871
7977
  this.indexerProvider = indexerProvider;
7872
7978
  this.arkServerPublicKey = arkServerPublicKey;
7873
- this.boardingTapscript = boardingTapscript;
7874
7979
  this.dustAmount = dustAmount;
7875
7980
  this.walletRepository = walletRepository;
7876
7981
  this.contractRepository = contractRepository;
@@ -7886,6 +7991,7 @@ var ReadonlyWallet = class _ReadonlyWallet {
7886
7991
  }
7887
7992
  }
7888
7993
  this._offchainTapscript = offchainTapscript;
7994
+ this._boardingTapscript = boardingTapscript;
7889
7995
  this.watcherConfig = watcherConfig;
7890
7996
  this._assetManager = new ReadonlyAssetManager(this.indexerProvider);
7891
7997
  this.walletContractTimelocks = walletContractTimelocks && walletContractTimelocks.length > 0 ? dedupeTimelocks(walletContractTimelocks) : [this.offchainTapscript.options.csvTimelock];
@@ -7912,6 +8018,17 @@ var ReadonlyWallet = class _ReadonlyWallet {
7912
8018
  * {@link WalletReceiveRotator.rotate} is the sole intended caller of.
7913
8019
  */
7914
8020
  _offchainTapscript;
8021
+ /**
8022
+ * Backing field for the current boarding tapscript (the QR / onboarding
8023
+ * target). Read via the public `boardingTapscript` getter; written only
8024
+ * by {@link Wallet.setBoardingTapscriptForRotation}, the sanctioned
8025
+ * boarding-rotation write path (analogue of `_offchainTapscript`). It is
8026
+ * a *current value*, not a fixed setup constant, because per-derivation
8027
+ * boarding rotation (plan §6-II) swaps it when a fresh boarding address
8028
+ * is explicitly allocated. Static / `auto` wallets never rotate it, so
8029
+ * it stays the index-0 baseline for their lifetime.
8030
+ */
8031
+ _boardingTapscript;
7915
8032
  /**
7916
8033
  * Currently-active receive tapscript. Read-only from the outside;
7917
8034
  * mutated only via {@link Wallet.setOffchainTapscriptForRotation}
@@ -7920,6 +8037,52 @@ var ReadonlyWallet = class _ReadonlyWallet {
7920
8037
  get offchainTapscript() {
7921
8038
  return this._offchainTapscript;
7922
8039
  }
8040
+ /**
8041
+ * The wallet's current boarding tapscript (the on-chain onboarding
8042
+ * target). Read-only from the outside; mutated only via
8043
+ * {@link Wallet.setBoardingTapscriptForRotation} when a fresh boarding
8044
+ * address is explicitly allocated. Single-valued for static / `auto`
8045
+ * wallets.
8046
+ */
8047
+ get boardingTapscript() {
8048
+ return this._boardingTapscript;
8049
+ }
8050
+ /**
8051
+ * Listeners fired after the boarding tapscript rotates to a fresh index
8052
+ * (see {@link Wallet.setBoardingTapscriptForRotation}). A live
8053
+ * {@link notifyIncomingFunds} onchain watcher registers one so it can
8054
+ * re-subscribe to include the newly allocated boarding address within the
8055
+ * same session — without it, a deposit to the fresh address wouldn't fire
8056
+ * a notification until the watcher's next re-init. Always empty for
8057
+ * readonly / static / `auto` wallets, which never rotate boarding.
8058
+ */
8059
+ _boardingRotationListeners = /* @__PURE__ */ new Set();
8060
+ /**
8061
+ * Register a listener invoked synchronously after each boarding rotation.
8062
+ * Returns an unsubscribe function. Protected: only internal subscribers
8063
+ * (the incoming-funds watcher) participate.
8064
+ */
8065
+ onBoardingRotation(listener) {
8066
+ this._boardingRotationListeners.add(listener);
8067
+ return () => {
8068
+ this._boardingRotationListeners.delete(listener);
8069
+ };
8070
+ }
8071
+ /**
8072
+ * Notify boarding-rotation listeners. Called by the boarding-rotation
8073
+ * write path ({@link Wallet.setBoardingTapscriptForRotation}) once the new
8074
+ * tapscript is in place. A throwing listener is isolated so it can neither
8075
+ * break the rotation nor starve sibling listeners.
8076
+ */
8077
+ notifyBoardingRotation() {
8078
+ for (const listener of this._boardingRotationListeners) {
8079
+ try {
8080
+ listener();
8081
+ } catch (e) {
8082
+ console.warn("Boarding-rotation listener failed", e);
8083
+ }
8084
+ }
8085
+ }
7923
8086
  /**
7924
8087
  * Protected helper to set up shared wallet configuration.
7925
8088
  * Extracts common logic used by both ReadonlyWallet.create() and Wallet.create().
@@ -8160,43 +8323,59 @@ var ReadonlyWallet = class _ReadonlyWallet {
8160
8323
  await clearSyncCursor(this.walletRepository);
8161
8324
  }
8162
8325
  /**
8163
- * Build a transaction history view for the wallet's boarding address.
8326
+ * The on-chain (P2TR) addresses of every boarding tapscript this wallet
8327
+ * uses — the current address plus any historical rotated boarding
8328
+ * addresses. The aggregating boarding readers (history, notifications) fan
8329
+ * out over this set so deposits at previous boarding addresses are still
8330
+ * surfaced (plan §6-IV); {@link getBoardingAddress} stays single-valued.
8331
+ */
8332
+ async getBoardingAddresses() {
8333
+ const tapscripts = await this.getBoardingTapscripts();
8334
+ return tapscripts.map((t) => t.onchainAddress(this.network));
8335
+ }
8336
+ /**
8337
+ * Build a transaction history view across the wallet's boarding addresses
8338
+ * (current + historical rotated; plan §6-IV.1).
8164
8339
  */
8165
8340
  async getBoardingTxs() {
8166
8341
  const utxos = [];
8167
8342
  const commitmentsToIgnore = /* @__PURE__ */ new Set();
8168
- const boardingAddress = await this.getBoardingAddress();
8169
- const txs = await this.onchainProvider.getTransactions(boardingAddress);
8343
+ const tapscripts = await this.getBoardingTapscripts();
8170
8344
  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);
8345
+ for (const tapscript of tapscripts) {
8346
+ const boardingAddress = tapscript.onchainAddress(this.network);
8347
+ const scriptHex = hex.encode(tapscript.pkScript);
8348
+ const txs = await this.onchainProvider.getTransactions(boardingAddress);
8349
+ for (const tx of txs) {
8350
+ for (let i = 0; i < tx.vout.length; i++) {
8351
+ const vout = tx.vout[i];
8352
+ if (vout.scriptpubkey_address === boardingAddress) {
8353
+ let spentStatuses = outspendCache.get(tx.txid);
8354
+ if (!spentStatuses) {
8355
+ spentStatuses = await this.onchainProvider.getTxOutspends(tx.txid);
8356
+ outspendCache.set(tx.txid, spentStatuses);
8357
+ }
8358
+ const spentStatus = spentStatuses[i];
8359
+ if (spentStatus?.spent) {
8360
+ commitmentsToIgnore.add(spentStatus.txid);
8361
+ }
8362
+ utxos.push({
8363
+ txid: tx.txid,
8364
+ vout: i,
8365
+ value: Number(vout.value),
8366
+ status: {
8367
+ confirmed: tx.status.confirmed,
8368
+ block_time: tx.status.block_time
8369
+ },
8370
+ isUnrolled: true,
8371
+ virtualStatus: {
8372
+ state: spentStatus?.spent ? "spent" : "settled",
8373
+ commitmentTxIds: spentStatus?.spent ? [spentStatus.txid] : void 0
8374
+ },
8375
+ createdAt: tx.status.confirmed ? new Date(tx.status.block_time * 1e3) : /* @__PURE__ */ new Date(0),
8376
+ script: scriptHex
8377
+ });
8183
8378
  }
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
8379
  }
8201
8380
  }
8202
8381
  }
@@ -8226,48 +8405,130 @@ var ReadonlyWallet = class _ReadonlyWallet {
8226
8405
  };
8227
8406
  }
8228
8407
  /**
8229
- * Fetch and cache onchain inputs (UTXOs) received at the boarding address.
8408
+ * The set of boarding tapscripts whose on-chain UTXOs belong to this
8409
+ * wallet — the current display tapscript plus every historical boarding
8410
+ * address it has used. Under per-derivation rotation (plan §6-II) a wallet
8411
+ * can hold unspent boarding UTXOs at several addresses at once, so fund
8412
+ * discovery / spending must enumerate them all, not just the current one
8413
+ * (plan §6-III.1). Deduplicated by scriptPubKey.
8414
+ *
8415
+ * Always includes the index-0 baseline (identity x-only key), which covers
8416
+ * the degenerate equal-delay case where the index-0 boarding row is
8417
+ * coalesced onto a `default` row and so isn't a `boarding`-typed contract.
8230
8418
  */
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);
8419
+ async getBoardingTapscripts() {
8420
+ const byScript = /* @__PURE__ */ new Map();
8421
+ const add = (s) => byScript.set(hex.encode(s.pkScript), s);
8422
+ const boardingCsv = this.boardingTapscript.options.csvTimelock ?? DefaultVtxo.Script.DEFAULT_TIMELOCK;
8423
+ add(
8424
+ new DefaultVtxo.Script({
8425
+ pubKey: await this.identity.xOnlyPublicKey(),
8426
+ serverPubKey: this.boardingTapscript.options.serverPubKey,
8427
+ csvTimelock: boardingCsv
8428
+ })
8429
+ );
8430
+ add(this.boardingTapscript);
8431
+ const serverPubKeyHex = hex.encode(this.boardingTapscript.options.serverPubKey);
8432
+ const boardingContracts = await this.contractRepository.getContracts({
8433
+ type: ["boarding"]
8236
8434
  });
8237
- await this.walletRepository.saveUtxos(boardingAddress, utxos);
8238
- return utxos;
8435
+ for (const c of boardingContracts) {
8436
+ if (c.params.serverPubKey !== serverPubKeyHex) continue;
8437
+ try {
8438
+ add(BoardingContractHandler.createScript(c.params));
8439
+ } catch (e) {
8440
+ console.warn("Skipping malformed boarding contract", c.script, e);
8441
+ }
8442
+ }
8443
+ return [...byScript.values()];
8444
+ }
8445
+ /**
8446
+ * Fetch and cache onchain inputs (UTXOs) received at the wallet's boarding
8447
+ * addresses — the current address plus any historical rotated boarding
8448
+ * addresses that still hold unspent UTXOs (plan §6-III.1). Each UTXO is
8449
+ * annotated with the tapscript of the address it actually sits on, so the
8450
+ * spending path forfeits / exits it with the correct per-index leaves.
8451
+ */
8452
+ async getBoardingUtxos() {
8453
+ const tapscripts = await this.getBoardingTapscripts();
8454
+ const all = [];
8455
+ for (const tapscript of tapscripts) {
8456
+ const address = tapscript.onchainAddress(this.network);
8457
+ const coins = await this.onchainProvider.getCoins(address);
8458
+ const utxos = coins.map((utxo) => extendCoinWithTapscript(tapscript, utxo));
8459
+ await this.walletRepository.saveUtxos(address, utxos);
8460
+ all.push(...utxos);
8461
+ }
8462
+ return all;
8239
8463
  }
8240
8464
  /**
8241
8465
  * Subscribe to onchain and offchain notifications for newly received funds.
8242
8466
  *
8467
+ * The onchain watcher tracks the full boarding-address set (current +
8468
+ * historical rotated). When boarding rotates *after* subscribing — e.g.
8469
+ * rotate-on-board allocates a fresh address via
8470
+ * {@link getNewBoardingAddress} — the watcher automatically re-subscribes
8471
+ * to widen its set, so a deposit to the new address fires a notification
8472
+ * within the same session (no watcher re-init required). The re-subscribe
8473
+ * is driven by {@link onBoardingRotation}; static / `auto` / readonly
8474
+ * wallets never rotate boarding, so it never fires for them.
8475
+ *
8243
8476
  * @param eventCallback - Callback invoked when matching funds are detected
8244
8477
  * @returns A function that stops the subscriptions
8245
8478
  */
8246
8479
  async notifyIncomingFunds(eventCallback) {
8247
8480
  const arkAddress = await this.getAddress();
8248
- const boardingAddress = await this.getBoardingAddress();
8249
8481
  let onchainStopFunc;
8250
8482
  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
- });
8483
+ let boardingRotationStopFunc;
8484
+ let stopped = false;
8485
+ let onchainChain = Promise.resolve();
8486
+ const subscribeOnchain = () => {
8487
+ onchainChain = onchainChain.then(async () => {
8488
+ if (stopped || !this.onchainProvider) return;
8489
+ const boardingAddresses = await this.getBoardingAddresses();
8490
+ if (boardingAddresses.length === 0) return;
8491
+ const boardingAddressSet = new Set(boardingAddresses);
8492
+ const previousStop = onchainStopFunc;
8493
+ const stop = await this.onchainProvider.watchAddresses(
8494
+ boardingAddresses,
8495
+ (txs) => {
8496
+ const coins = txs.flatMap((tx) => {
8497
+ const { txid, status } = tx;
8498
+ const matched = [];
8499
+ tx.vout.forEach((v, vout) => {
8500
+ if (boardingAddressSet.has(v.scriptpubkey_address)) {
8501
+ matched.push({
8502
+ txid,
8503
+ vout,
8504
+ value: Number(v.value),
8505
+ status
8506
+ });
8507
+ }
8508
+ });
8509
+ return matched;
8510
+ });
8511
+ eventCallback({
8512
+ type: "utxo",
8513
+ coins
8514
+ });
8515
+ }
8516
+ );
8517
+ if (stopped) {
8518
+ stop();
8519
+ return;
8268
8520
  }
8269
- );
8270
- }
8521
+ onchainStopFunc = stop;
8522
+ previousStop?.();
8523
+ }).catch((e) => {
8524
+ console.warn("Failed to (re)subscribe boarding-funds watcher", e);
8525
+ });
8526
+ return onchainChain;
8527
+ };
8528
+ boardingRotationStopFunc = this.onBoardingRotation(() => {
8529
+ void subscribeOnchain();
8530
+ });
8531
+ await subscribeOnchain();
8271
8532
  if (this.indexerProvider && arkAddress) {
8272
8533
  const cm = await this.getContractManager();
8273
8534
  let annotationQueue = Promise.resolve();
@@ -8296,7 +8557,10 @@ var ReadonlyWallet = class _ReadonlyWallet {
8296
8557
  });
8297
8558
  }
8298
8559
  const stopFunc = () => {
8560
+ stopped = true;
8561
+ boardingRotationStopFunc?.();
8299
8562
  onchainStopFunc?.();
8563
+ onchainStopFunc = void 0;
8300
8564
  indexerStopFunc?.();
8301
8565
  };
8302
8566
  return stopFunc;
@@ -8443,17 +8707,21 @@ var ReadonlyWallet = class _ReadonlyWallet {
8443
8707
  });
8444
8708
  }
8445
8709
  }
8446
- const boardingScriptHex = hex.encode(this.boardingTapscript.pkScript);
8447
8710
  const boardingCsvTimelock = this.boardingTapscript.options.csvTimelock ?? DefaultVtxo.Script.DEFAULT_TIMELOCK;
8711
+ const baselineBoarding = new DefaultVtxo.Script({
8712
+ pubKey: baselinePubkey,
8713
+ serverPubKey: this.boardingTapscript.options.serverPubKey,
8714
+ csvTimelock: boardingCsvTimelock
8715
+ });
8448
8716
  await ensureWalletContract(manager, {
8449
8717
  type: "boarding",
8450
8718
  params: {
8451
- pubKey: hex.encode(this.boardingTapscript.options.pubKey),
8452
- serverPubKey: hex.encode(this.boardingTapscript.options.serverPubKey),
8719
+ pubKey: hex.encode(baselineBoarding.options.pubKey),
8720
+ serverPubKey: hex.encode(baselineBoarding.options.serverPubKey),
8453
8721
  csvTimelock: timelockToSequence(boardingCsvTimelock).toString()
8454
8722
  },
8455
- script: boardingScriptHex,
8456
- address: this.boardingTapscript.address(this.network.hrp, this.arkServerPublicKey).encode(),
8723
+ script: hex.encode(baselineBoarding.pkScript),
8724
+ address: baselineBoarding.address(this.network.hrp, this.arkServerPublicKey).encode(),
8457
8725
  state: "active"
8458
8726
  });
8459
8727
  return manager;
@@ -8553,6 +8821,72 @@ var Wallet2 = class _Wallet extends ReadonlyWallet {
8553
8821
  setOffchainTapscriptForRotation(tapscript) {
8554
8822
  this._offchainTapscript = tapscript;
8555
8823
  }
8824
+ /**
8825
+ * @internal Sole write path for `boardingTapscript` after construction.
8826
+ * Called by {@link Wallet.getNewBoardingAddress} once the rotated
8827
+ * boarding contract has been persisted. External code must treat
8828
+ * `boardingTapscript` as read-only.
8829
+ */
8830
+ setBoardingTapscriptForRotation(tapscript) {
8831
+ this._boardingTapscript = tapscript;
8832
+ this.notifyBoardingRotation();
8833
+ }
8834
+ /**
8835
+ * Allocate and return a *fresh* on-chain boarding address, rotating the
8836
+ * wallet's current boarding tapscript to a new HD index.
8837
+ *
8838
+ * This is the explicit boarding allocator — the analogue of dotnet's
8839
+ * `GetNextContract(NextContractPurpose.Boarding)`. Unlike
8840
+ * {@link getBoardingAddress} (a stable read of the current display
8841
+ * address that never burns an index), each call here:
8842
+ *
8843
+ * - allocates the next index from the shared HD stream (so boarding and
8844
+ * L2 receive interleave on one monotonic index);
8845
+ * - builds the boarding tapscript at that index with the boarding-exit
8846
+ * CSV;
8847
+ * - persists an `active` `boarding` contract tagged
8848
+ * {@link WALLET_RECEIVE_SOURCE} (with its `signingDescriptor`) so the
8849
+ * ContractWatcher monitors it, boot can restore it as the current
8850
+ * boarding address, and descriptor-aware signing can recover the
8851
+ * per-index key;
8852
+ * - swaps the wallet's current `boardingTapscript`.
8853
+ *
8854
+ * Gated by `walletMode`: a static / `auto` wallet has no descriptor
8855
+ * provider and keeps a single index-0 boarding address for its lifetime,
8856
+ * so this returns the existing {@link getBoardingAddress} unchanged
8857
+ * (no rotation, no index burned).
8858
+ */
8859
+ async getNewBoardingAddress() {
8860
+ const provider = this._descriptorProvider;
8861
+ if (!provider) {
8862
+ return this.getBoardingAddress();
8863
+ }
8864
+ const descriptor = await provider.getNextSigningDescriptor();
8865
+ const pubKey = deriveDescriptorLeafPubKey(descriptor);
8866
+ const newBoarding = new DefaultVtxo.Script({
8867
+ ...this._boardingTapscript.options,
8868
+ pubKey
8869
+ });
8870
+ const csvTimelock = newBoarding.options.csvTimelock ?? DefaultVtxo.Script.DEFAULT_TIMELOCK;
8871
+ const manager = await this.getContractManager();
8872
+ await manager.createContract({
8873
+ type: "boarding",
8874
+ params: {
8875
+ pubKey: hex.encode(pubKey),
8876
+ serverPubKey: hex.encode(newBoarding.options.serverPubKey),
8877
+ csvTimelock: timelockToSequence(csvTimelock).toString()
8878
+ },
8879
+ script: hex.encode(newBoarding.pkScript),
8880
+ address: newBoarding.address(this.network.hrp, this.arkServerPublicKey).encode(),
8881
+ state: "active",
8882
+ metadata: {
8883
+ source: WALLET_RECEIVE_SOURCE,
8884
+ signingDescriptor: descriptor
8885
+ }
8886
+ });
8887
+ this.setBoardingTapscriptForRotation(newBoarding);
8888
+ return newBoarding.onchainAddress(this.network);
8889
+ }
8556
8890
  /**
8557
8891
  * Async mutex that serializes all operations submitting VTXOs to the Arkade
8558
8892
  * server (`settle`, `send`, `sendBitcoin`). This prevents VtxoManager's
@@ -8637,12 +8971,25 @@ var Wallet2 = class _Wallet extends ReadonlyWallet {
8637
8971
  const staticDescriptor = hd ? void 0 : `tr(${hex.encode(await this.identity.xOnlyPublicKey())})`;
8638
8972
  const materialize = (index) => hd ? provider.materializeDescriptorAt(index) : staticDescriptor;
8639
8973
  const delegatePubKey = this.offchainTapscript instanceof DelegateVtxo.Script ? this.offchainTapscript.options.delegatePubKey : void 0;
8974
+ const arkInfo = await this.arkProvider.getInfo();
8975
+ const currentSignerPubKey = toXOnlyPubKey(hex.decode(arkInfo.signerPubkey));
8976
+ const deprecatedSignerPubKeys = arkInfo.deprecatedSigners.map(
8977
+ (s) => toXOnlyPubKey(hex.decode(s.pubkey))
8978
+ );
8640
8979
  const deps = {
8641
8980
  indexerProvider: this.indexerProvider,
8642
8981
  onchainProvider: this.onchainProvider,
8643
8982
  network: { hrp: this.network.hrp },
8644
- serverPubKey: this.offchainTapscript.options.serverPubKey,
8983
+ // Full network for the boarding on-chain (P2TR) probe — the
8984
+ // `{ hrp }` shape above lacks the `bech32` data
8985
+ // `VtxoScript.onchainAddress` needs (plan §6-I.1).
8986
+ onchainNetwork: this.network,
8987
+ serverPubKey: currentSignerPubKey,
8988
+ deprecatedSignerPubKeys,
8645
8989
  csvTimelocks: this.walletContractTimelocks,
8990
+ // Boarding-exit CSV so the boarding handler can build its
8991
+ // candidate script (distinct from the unilateral-exit matrix).
8992
+ boardingTimelock: this.boardingTapscript.options.csvTimelock ?? DefaultVtxo.Script.DEFAULT_TIMELOCK,
8646
8993
  delegatePubKey
8647
8994
  };
8648
8995
  const result = await manager.scanContracts({
@@ -8774,6 +9121,16 @@ var Wallet2 = class _Wallet extends ReadonlyWallet {
8774
9121
  boot?.rotator,
8775
9122
  boot?.provider
8776
9123
  );
9124
+ if (boot?.provider) {
9125
+ const resolvedBoarding = await resolveBoardingBootTapscript(
9126
+ setup.contractRepository,
9127
+ setup.serverPubKey,
9128
+ setup.boardingTapscript
9129
+ );
9130
+ if (resolvedBoarding !== setup.boardingTapscript) {
9131
+ wallet.setBoardingTapscriptForRotation(resolvedBoarding);
9132
+ }
9133
+ }
8777
9134
  await wallet.getVtxoManager();
8778
9135
  return wallet;
8779
9136
  }
@@ -8941,7 +9298,10 @@ var Wallet2 = class _Wallet extends ReadonlyWallet {
8941
9298
  }
8942
9299
  const vtxos = await this.getVtxos({ withRecoverable: true });
8943
9300
  const filteredVtxos = [];
8944
- for (const vtxo of vtxos) {
9301
+ for (const vtxo of byValueDescending(vtxos)) {
9302
+ if (filteredVtxos.length >= MAX_VTXOS_PER_SETTLEMENT) {
9303
+ break;
9304
+ }
8945
9305
  const inputFee = estimator.evalOffchainInput({
8946
9306
  amount: BigInt(vtxo.value),
8947
9307
  type: vtxo.virtualStatus.state === "swept" ? "recoverable" : "vtxo",
@@ -9074,6 +9434,7 @@ var Wallet2 = class _Wallet extends ReadonlyWallet {
9074
9434
  eventCallback: eventCallback ? (event) => Promise.resolve(eventCallback(event)) : void 0
9075
9435
  });
9076
9436
  await this.updateDbAfterSettle(params.inputs, commitmentTxid);
9437
+ await this.maybeRotateBoardingAfterBoard(params.inputs);
9077
9438
  return commitmentTxid;
9078
9439
  } catch (error) {
9079
9440
  const inputIds = params.inputs.map((i) => `${i.txid}:${i.vout}`).join(",");
@@ -9091,6 +9452,41 @@ var Wallet2 = class _Wallet extends ReadonlyWallet {
9091
9452
  });
9092
9453
  }
9093
9454
  }
9455
+ /**
9456
+ * Rotate the boarding address after a board (rotate-on-board trigger).
9457
+ *
9458
+ * Mirrors {@link WalletReceiveRotator}'s L2 rotation, but driven by a
9459
+ * board instead of a `vtxo_received` event: when a settle consumes at
9460
+ * least one boarding (on-chain) UTXO, the current boarding address has
9461
+ * served its purpose, so we allocate a fresh one via
9462
+ * {@link getNewBoardingAddress}. A settle that consumed only VTXOs (a
9463
+ * renewal / offboard) is not a board and leaves the boarding address
9464
+ * untouched.
9465
+ *
9466
+ * Boarding inputs are the non-VTXO coins (no `virtualStatus`), the same
9467
+ * discriminator {@link handleSettlementFinalizationEvent} uses; the
9468
+ * `typeof` guard skips arknote string inputs before the `in` test.
9469
+ *
9470
+ * No-ops for static / `auto` wallets (no descriptor provider — boarding
9471
+ * stays on its fixed index-0 address). Best-effort and non-fatal: the
9472
+ * settle has already committed and its txid must be returned, so a
9473
+ * rotation failure is logged and swallowed rather than thrown. Funds at
9474
+ * the retired boarding address remain discoverable — the old `boarding`
9475
+ * contract stays active and {@link getBoardingUtxos} fans out over the
9476
+ * full historical boarding set.
9477
+ */
9478
+ async maybeRotateBoardingAfterBoard(inputs) {
9479
+ if (!this._descriptorProvider) return;
9480
+ const consumedBoarding = inputs.some(
9481
+ (input) => typeof input !== "string" && !("virtualStatus" in input)
9482
+ );
9483
+ if (!consumedBoarding) return;
9484
+ try {
9485
+ await this.getNewBoardingAddress();
9486
+ } catch (e) {
9487
+ console.warn("Failed to rotate boarding address after board", e);
9488
+ }
9489
+ }
9094
9490
  async handleSettlementFinalizationEvent(event, inputs, forfeitOutputScript, connectorsGraph) {
9095
9491
  const signedForfeits = [];
9096
9492
  const isVtxo = (input) => "virtualStatus" in input;
@@ -9301,6 +9697,19 @@ var Wallet2 = class _Wallet extends ReadonlyWallet {
9301
9697
  }
9302
9698
  return jobs;
9303
9699
  }
9700
+ /**
9701
+ * @internal Sign an on-chain boarding exit / sweep transaction, routing
9702
+ * each input to the correct key by its `witnessUtxo.script`: the identity
9703
+ * for index-0 / static boarding, the per-index descriptor for a rotated
9704
+ * boarding UTXO (plan §6-III.3). Used by
9705
+ * {@link VtxoManager.sweepExpiredBoardingUtxos}; without it, the
9706
+ * unilateral exit of a rotated boarding UTXO would be signed with the
9707
+ * wrong (index-0) key and rejected.
9708
+ */
9709
+ async signOnchainBoardingTx(tx) {
9710
+ const signed = await this._signerRouter.sign(tx, this.inputSigningJobsFromWitnessUtxos(tx));
9711
+ return signed;
9712
+ }
9304
9713
  async safeRegisterIntent(intent, inputs) {
9305
9714
  try {
9306
9715
  return await this.arkProvider.registerIntent(intent);
@@ -9867,10 +10276,9 @@ var Wallet2 = class _Wallet extends ReadonlyWallet {
9867
10276
  // mark virtual outputs as spent/settled, remove boarding inputs
9868
10277
  async updateDbAfterSettle(inputs, commitmentTxid) {
9869
10278
  try {
9870
- const boardingAddress = await this.getBoardingAddress();
9871
10279
  const spentVtxos = [];
9872
10280
  const inputArkTxIds = /* @__PURE__ */ new Set();
9873
- const boardingUtxoToRemove = /* @__PURE__ */ new Set();
10281
+ const boardingRemovalsByAddress = /* @__PURE__ */ new Map();
9874
10282
  const isVtxo = (input) => "virtualStatus" in input;
9875
10283
  const vtxoInputs = inputs.filter(isVtxo);
9876
10284
  const cm = await this.getContractManager();
@@ -9892,7 +10300,20 @@ var Wallet2 = class _Wallet extends ReadonlyWallet {
9892
10300
  isSpent: true
9893
10301
  });
9894
10302
  } else {
9895
- boardingUtxoToRemove.add(`${input.txid}:${input.vout}`);
10303
+ let sourceAddress;
10304
+ try {
10305
+ sourceAddress = VtxoScript.decode(input.tapTree).onchainAddress(
10306
+ this.network
10307
+ );
10308
+ } catch {
10309
+ sourceAddress = this.boardingTapscript.onchainAddress(this.network);
10310
+ }
10311
+ let set = boardingRemovalsByAddress.get(sourceAddress);
10312
+ if (!set) {
10313
+ set = /* @__PURE__ */ new Set();
10314
+ boardingRemovalsByAddress.set(sourceAddress, set);
10315
+ }
10316
+ set.add(`${input.txid}:${input.vout}`);
9896
10317
  }
9897
10318
  }
9898
10319
  if (spentVtxos.length > 0) {
@@ -9924,14 +10345,12 @@ var Wallet2 = class _Wallet extends ReadonlyWallet {
9924
10345
  );
9925
10346
  }
9926
10347
  }
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);
10348
+ for (const [address, toRemove] of boardingRemovalsByAddress) {
10349
+ const currentUtxos = await this.walletRepository.getUtxos(address);
10350
+ const filtered = currentUtxos.filter((u) => !toRemove.has(`${u.txid}:${u.vout}`));
10351
+ await this.walletRepository.deleteUtxos(address);
9933
10352
  if (filtered.length > 0) {
9934
- await this.walletRepository.saveUtxos(boardingAddress, filtered);
10353
+ await this.walletRepository.saveUtxos(address, filtered);
9935
10354
  }
9936
10355
  }
9937
10356
  } catch (e) {
@@ -11331,9 +11750,7 @@ var WalletMessageHandler = class {
11331
11750
  );
11332
11751
  }
11333
11752
  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);
11753
+ const utxos = await this.readonlyWallet.getBoardingUtxos();
11337
11754
  this.scheduleForNextTick(
11338
11755
  () => this.tagged({
11339
11756
  type: "UTXO_UPDATE",
@@ -11362,13 +11779,16 @@ var WalletMessageHandler = class {
11362
11779
  return;
11363
11780
  }
11364
11781
  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
- );
11782
+ const boardingAddresses = await this.readonlyWallet.getBoardingAddresses();
11783
+ const fresh = await this.readonlyWallet.getBoardingUtxos();
11784
+ const freshKeys = new Set(fresh.map((u) => `${u.txid}:${u.vout}`));
11785
+ for (const addr of boardingAddresses) {
11786
+ const cached = await this.walletRepository.getUtxos(addr);
11787
+ const kept = cached.filter((u) => freshKeys.has(`${u.txid}:${u.vout}`));
11788
+ if (kept.length === cached.length) continue;
11789
+ await this.walletRepository.deleteUtxos(addr);
11790
+ if (kept.length > 0) await this.walletRepository.saveUtxos(addr, kept);
11791
+ }
11372
11792
  const address = await this.readonlyWallet.getAddress();
11373
11793
  const txs = await this.buildTransactionHistoryFromCache(vtxos);
11374
11794
  if (txs) await this.walletRepository.saveTransactions(address, txs);
@@ -14159,5 +14579,5 @@ function isArkContract(str) {
14159
14579
  }
14160
14580
 
14161
14581
  export { ArkNote, AssetManager, BIP322, Batch, ContractManager, ContractRepositoryImpl, ContractWatcher, DB_VERSION, DEFAULT_MESSAGE_TIMEOUTS, DelegateManagerImpl, DelegateNotConfiguredError, DelegatorManagerImpl, DelegatorNotConfiguredError, DescriptorSigningProviderMissingError, DustChangeError, ELECTRUM_TCP_HOST, ELECTRUM_WS_URL, ESPLORA_URL, ElectrumOnchainProvider, EsploraProvider, Estimator, HDDescriptorProvider, InMemoryContractRepository, InMemoryWalletRepository, IndexedDBContractRepository, IndexedDBWalletRepository, MESSAGE_BUS_NOT_INITIALIZED, MIGRATION_KEY, MessageBus, MessageBusNotInitializedError, MissingSigningDescriptorError, MnemonicIdentity, OnchainWallet, P2A, Ramps, ReadonlyAssetManager, ReadonlyDescriptorIdentity, ReadonlySingleKey, ReadonlyWallet, ReadonlyWalletError, RestDelegateProvider, RestDelegatorProvider, SeedIdentity, ServiceWorkerReadonlyWallet, ServiceWorkerTimeoutError, ServiceWorkerWallet, SingleKey, TxTree, TxType, TxWeightEstimator, Unroll, VtxoManager, Wallet2 as Wallet, WalletMessageHandler, WalletNotInitializedError, WalletRepositoryImpl, WsElectrumChainSource, buildForfeitTx, buildOffchainTx, closeDatabase, combineTapscriptSigs, contractFromArkContract, contractFromArkContractWithAddress, decodeArkContract, deserializeAssets, deserializeUtxo, deserializeVtxo, encodeArkContract, extendVirtualCoinForContract, getMigrationStatus, getRandomId, hasBoardingTxExpired, isArkContract, isBatchSignable, isDiscoverable, isExpired, isRecoverable, isSpendable, isSubdust, isValidArkAddress, isVtxoExpiringSoon, isVtxoForScript, migrateWalletRepository, openDatabase, requiresMigration, rollbackMigration, saveVtxosForContract, scriptFromArkAddress, serializeAssets, serializeUtxo, serializeVtxo, setupServiceWorker, validateConnectorsTxGraph, validateVtxoTxGraph, verifyTapscriptSignatures, waitForIncomingFunds, warnAndFilterVtxosForScript };
14162
- //# sourceMappingURL=chunk-DSS2GQUG.js.map
14163
- //# sourceMappingURL=chunk-DSS2GQUG.js.map
14582
+ //# sourceMappingURL=chunk-HFXEUW55.js.map
14583
+ //# sourceMappingURL=chunk-HFXEUW55.js.map