@arkade-os/sdk 0.4.32 → 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 (82) 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-ibLW4Hte.d.cts → ark-Dsv5Jq4E.d.cts} +81 -10
  9. package/dist/{ark-ibLW4Hte.d.ts → ark-Dsv5Jq4E.d.ts} +81 -10
  10. package/dist/{asyncStorageTaskQueue-BEOFPNc0.d.ts → asyncStorageTaskQueue-BH-zuth5.d.ts} +1 -1
  11. package/dist/{asyncStorageTaskQueue-VGHXWR9F.d.cts → asyncStorageTaskQueue-D92ch8yI.d.cts} +1 -1
  12. package/dist/{chunk-ABWRLTX5.js → chunk-5WDBHWX3.js} +4 -4
  13. package/dist/{chunk-ABWRLTX5.js.map → chunk-5WDBHWX3.js.map} +1 -1
  14. package/dist/{chunk-GIGILVVP.cjs → chunk-CCLNFHJ5.cjs} +11 -11
  15. package/dist/{chunk-GIGILVVP.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-YA4G7RFB.js → chunk-CUSABEUQ.js} +166 -38
  19. package/dist/chunk-CUSABEUQ.js.map +1 -0
  20. package/dist/{chunk-6FLL2Q36.cjs → chunk-FSAXPBGP.cjs} +9 -9
  21. package/dist/chunk-FSAXPBGP.cjs.map +1 -0
  22. package/dist/{chunk-6NWNOLL3.js → chunk-FXFBPXV3.js} +4 -4
  23. package/dist/chunk-FXFBPXV3.js.map +1 -0
  24. package/dist/{chunk-IEO3XDKI.cjs → chunk-GUTKJMSF.cjs} +190 -58
  25. package/dist/chunk-GUTKJMSF.cjs.map +1 -0
  26. package/dist/{chunk-XROGFOPX.js → chunk-HFXEUW55.js} +740 -175
  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-SHEBNWOQ.js → chunk-VVGD3JIP.js} +3 -3
  31. package/dist/{chunk-SHEBNWOQ.js.map → chunk-VVGD3JIP.js.map} +1 -1
  32. package/dist/{chunk-KQK4PP6L.cjs → chunk-XCHBQVMK.cjs} +879 -314
  33. package/dist/chunk-XCHBQVMK.cjs.map +1 -0
  34. package/dist/{chunk-I2UIKZM5.cjs → chunk-ZS3OZHC7.cjs} +7 -7
  35. package/dist/{chunk-I2UIKZM5.cjs.map → chunk-ZS3OZHC7.cjs.map} +1 -1
  36. package/dist/contracts/handlers/index.cjs +10 -6
  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-BvNTw44a.d.cts → delegate-BaS5SCIW.d.cts} +10 -2
  41. package/dist/{delegate-BXaR1RNG.d.ts → delegate-Baz_hb83.d.ts} +10 -2
  42. package/dist/{index-BusKawmy.d.ts → index-FwXZveaX.d.ts} +63 -3
  43. package/dist/{index-C-5Tw7VA.d.cts → index-lNZ6qaO3.d.cts} +63 -3
  44. package/dist/index.cjs +143 -127
  45. package/dist/index.d.cts +89 -16
  46. package/dist/index.d.ts +89 -16
  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-B1igKGAo.d.ts → taskRunner-B1NUWyWR.d.ts} +1 -1
  57. package/dist/{taskRunner-By92TQ1m.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 +14 -14
  63. package/dist/wallet/expo/index.cjs.map +1 -1
  64. package/dist/wallet/expo/index.d.cts +5 -5
  65. package/dist/wallet/expo/index.d.ts +5 -5
  66. package/dist/wallet/expo/index.js +6 -6
  67. package/dist/wallet/expo/index.js.map +1 -1
  68. package/dist/{wallet-B_rxgQTu.d.cts → wallet-By9HIo0Q.d.cts} +160 -5
  69. package/dist/{wallet-CyM4F7Bs.d.ts → wallet-D6uoBLmS.d.ts} +160 -5
  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 +4 -4
  75. package/dist/chunk-6FLL2Q36.cjs.map +0 -1
  76. package/dist/chunk-6NWNOLL3.js.map +0 -1
  77. package/dist/chunk-IEO3XDKI.cjs.map +0 -1
  78. package/dist/chunk-KQK4PP6L.cjs.map +0 -1
  79. package/dist/chunk-TU3LVAPX.js.map +0 -1
  80. package/dist/chunk-WMIPYZSB.cjs.map +0 -1
  81. package/dist/chunk-XROGFOPX.js.map +0 -1
  82. package/dist/chunk-YA4G7RFB.js.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-6NWNOLL3.js';
2
- import { isMainnetDescriptor, descriptorIsOurs, contractHandlers, DelegateVtxo, DefaultVtxo, WALLET_RECEIVE_SOURCE, deriveDescriptorLeafPubKey } from './chunk-YA4G7RFB.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
  }
@@ -1966,12 +1969,22 @@ function verifyTapscriptSignatures(tx, inputIndex, requiredSigners, excludePubke
1966
1969
  }
1967
1970
  }
1968
1971
  function combineTapscriptSigs(signedTx, originalTx) {
1972
+ if (signedTx.inputsLength !== originalTx.inputsLength) {
1973
+ throw new Error(
1974
+ `combineTapscriptSigs: input count mismatch (signedTx ${signedTx.inputsLength}, originalTx ${originalTx.inputsLength})`
1975
+ );
1976
+ }
1969
1977
  for (let i = 0; i < signedTx.inputsLength; i++) {
1970
1978
  const input = originalTx.getInput(i);
1971
1979
  const signedInput = signedTx.getInput(i);
1972
- if (!input.tapScriptSig) throw new Error("No tapScriptSig");
1980
+ if (!input.tapScriptSig) {
1981
+ throw new Error(`combineTapscriptSigs: originalTx input ${i} has no tapScriptSig`);
1982
+ }
1983
+ if (!signedInput.tapScriptSig) {
1984
+ throw new Error(`combineTapscriptSigs: signedTx input ${i} has no tapScriptSig`);
1985
+ }
1973
1986
  originalTx.updateInput(i, {
1974
- tapScriptSig: input.tapScriptSig?.concat(signedInput.tapScriptSig)
1987
+ tapScriptSig: input.tapScriptSig.concat(signedInput.tapScriptSig)
1975
1988
  });
1976
1989
  }
1977
1990
  return originalTx;
@@ -2248,12 +2261,12 @@ var FALLBACK_WALLET_DUST_AMOUNT = 330n;
2248
2261
  function getDustAmount(wallet) {
2249
2262
  return "dustAmount" in wallet ? wallet.dustAmount : FALLBACK_WALLET_DUST_AMOUNT;
2250
2263
  }
2251
- function extendCoin(wallet, utxo) {
2264
+ function extendCoinWithTapscript(boardingTapscript, utxo) {
2252
2265
  return {
2253
2266
  ...utxo,
2254
- forfeitTapLeafScript: wallet.boardingTapscript.forfeit(),
2255
- intentTapLeafScript: wallet.boardingTapscript.forfeit(),
2256
- tapTree: wallet.boardingTapscript.encode()
2267
+ forfeitTapLeafScript: boardingTapscript.forfeit(),
2268
+ intentTapLeafScript: boardingTapscript.forfeit(),
2269
+ tapTree: boardingTapscript.encode()
2257
2270
  };
2258
2271
  }
2259
2272
  function deriveContractTapscripts(contract) {
@@ -2347,12 +2360,12 @@ function validateRecipients(recipients, dustAmount) {
2347
2360
 
2348
2361
  // src/wallet/vtxo-manager.ts
2349
2362
  function isSweepCapable(wallet) {
2350
- 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;
2351
2364
  }
2352
2365
  function assertSweepCapable(wallet) {
2353
2366
  if (!isSweepCapable(wallet)) {
2354
2367
  throw new Error(
2355
- "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"
2356
2369
  );
2357
2370
  }
2358
2371
  }
@@ -2368,6 +2381,21 @@ async function runWithCrossInstanceLock(name, fn) {
2368
2381
  await fn();
2369
2382
  });
2370
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
+ }
2371
2399
  var DEFAULT_THRESHOLD_SECONDS = 259200;
2372
2400
  var DEFAULT_THRESHOLD_MS = DEFAULT_THRESHOLD_SECONDS * 1e3;
2373
2401
  var DEFAULT_RENEWAL_CONFIG = {
@@ -2527,10 +2555,20 @@ var VtxoManager = class _VtxoManager {
2527
2555
  withUnrolled: false
2528
2556
  });
2529
2557
  const dustAmount = getDustAmount(this.wallet);
2530
- const { vtxosToRecover, totalAmount } = getRecoverableWithSubdust(allVtxos, dustAmount);
2558
+ let { vtxosToRecover, totalAmount } = getRecoverableWithSubdust(allVtxos, dustAmount);
2531
2559
  if (vtxosToRecover.length === 0) {
2532
2560
  throw new Error("No recoverable VTXOs found");
2533
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
+ }
2534
2572
  const arkAddress = await this.wallet.getAddress();
2535
2573
  return this.wallet.settle(
2536
2574
  {
@@ -2680,6 +2718,9 @@ var VtxoManager = class _VtxoManager {
2680
2718
  if (vtxos.length === 0) {
2681
2719
  throw new Error("No VTXOs available to renew");
2682
2720
  }
2721
+ if (vtxos.length > MAX_VTXOS_PER_SETTLEMENT) {
2722
+ vtxos = byExpiryAscending(vtxos).slice(0, MAX_VTXOS_PER_SETTLEMENT);
2723
+ }
2683
2724
  const totalAmount = vtxos.reduce((sum, vtxo) => sum + vtxo.value, 0);
2684
2725
  const dustAmount = getDustAmount(this.wallet);
2685
2726
  if (BigInt(totalAmount) < dustAmount) {
@@ -2787,7 +2828,6 @@ var VtxoManager = class _VtxoManager {
2787
2828
  const boardingAddress = await this.wallet.getBoardingAddress();
2788
2829
  const feeRate = await this.getOnchainProvider().getFeeRate() ?? 1;
2789
2830
  const exitTapLeafScript = this.getBoardingExitLeaf();
2790
- const sequence = getSequence(exitTapLeafScript);
2791
2831
  const leafScript = exitTapLeafScript[1];
2792
2832
  const leafScriptSize = leafScript.length - 1;
2793
2833
  const controlBlockSize = exitTapLeafScript[0].merklePath.length * 32;
@@ -2808,19 +2848,28 @@ var VtxoManager = class _VtxoManager {
2808
2848
  }
2809
2849
  const tx = new Transaction();
2810
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
+ }
2811
2860
  tx.addInput({
2812
2861
  txid: utxo.txid,
2813
2862
  index: utxo.vout,
2814
2863
  witnessUtxo: {
2815
- script: this.getBoardingOutputScript(),
2864
+ script: utxoScript.pkScript,
2816
2865
  amount: BigInt(utxo.value)
2817
2866
  },
2818
- tapLeafScript: [exitTapLeafScript],
2819
- sequence
2867
+ tapLeafScript: [utxoExitLeaf],
2868
+ sequence: getSequence(utxoExitLeaf)
2820
2869
  });
2821
2870
  }
2822
2871
  tx.addOutputAddress(boardingAddress, outputAmount, this.getNetwork());
2823
- const signedTx = await this.getIdentity().sign(tx);
2872
+ const signedTx = await this.getSweepWallet().signOnchainBoardingTx(tx);
2824
2873
  signedTx.finalize();
2825
2874
  const txid = await this.getOnchainProvider().broadcastTransaction(signedTx.hex);
2826
2875
  for (const u of expiredUtxos) {
@@ -2847,10 +2896,6 @@ var VtxoManager = class _VtxoManager {
2847
2896
  getBoardingExitLeaf() {
2848
2897
  return this.getSweepWallet().boardingTapscript.exit();
2849
2898
  }
2850
- /** Returns the pkScript (output script) of the boarding tapscript. */
2851
- getBoardingOutputScript() {
2852
- return this.getSweepWallet().boardingTapscript.pkScript;
2853
- }
2854
2899
  /** Returns the onchain provider for fee estimation and broadcasting. */
2855
2900
  getOnchainProvider() {
2856
2901
  return this.getSweepWallet().onchainProvider;
@@ -2863,10 +2908,6 @@ var VtxoManager = class _VtxoManager {
2863
2908
  getNetwork() {
2864
2909
  return this.getSweepWallet().network;
2865
2910
  }
2866
- /** Returns the wallet's identity for transaction signing. */
2867
- getIdentity() {
2868
- return this.wallet.identity;
2869
- }
2870
2911
  async initializeSubscription() {
2871
2912
  if (this.settlementConfig === false) {
2872
2913
  return void 0;
@@ -3147,7 +3188,10 @@ var VtxoManager = class _VtxoManager {
3147
3188
  totalAmount += BigInt(u.value) - BigInt(inputFee.satoshis);
3148
3189
  }
3149
3190
  const filteredVtxos = [];
3150
- for (const v of expiringVtxos) {
3191
+ for (const v of byExpiryAscending(expiringVtxos)) {
3192
+ if (filteredVtxos.length >= MAX_VTXOS_PER_SETTLEMENT) {
3193
+ break;
3194
+ }
3151
3195
  const inputFee = estimator.evalOffchainInput({
3152
3196
  amount: BigInt(v.value),
3153
3197
  type: v.virtualStatus.state === "swept" ? "recoverable" : "vtxo",
@@ -6505,8 +6549,11 @@ function cursorCutoff(requestStartedAt) {
6505
6549
  }
6506
6550
 
6507
6551
  // src/contracts/contractManager.ts
6508
- var DEFAULT_PAGE_SIZE = 500;
6552
+ function areCoalescibleContractTypes(a, b) {
6553
+ return a === "default" && b === "boarding" || a === "boarding" && b === "default";
6554
+ }
6509
6555
  var SCAN_MAX_INDEX = 1e4;
6556
+ var DEFAULT_SCAN_BATCH = 10;
6510
6557
  var ContractManager = class _ContractManager {
6511
6558
  config;
6512
6559
  watcher;
@@ -6630,8 +6677,11 @@ var ContractManager = class _ContractManager {
6630
6677
  const [existing] = await this.getContracts({ script: params.script });
6631
6678
  if (existing) {
6632
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
+ }
6633
6683
  throw new Error(
6634
- `Contract with script ${params.script} already exists with with type ${existing.type}.`
6684
+ `Contract with script ${params.script} already exists with type ${existing.type}.`
6635
6685
  );
6636
6686
  }
6637
6687
  const contract = {
@@ -6660,6 +6710,19 @@ var ContractManager = class _ContractManager {
6660
6710
  * other handler hit it).
6661
6711
  * - `persistAndWatchContract` rejecting is operational/fatal and
6662
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.
6663
6726
  */
6664
6727
  async scanContracts(opts) {
6665
6728
  const gapLimit = opts.gapLimit ?? 20;
@@ -6668,35 +6731,69 @@ var ContractManager = class _ContractManager {
6668
6731
  `scanContracts: gapLimit must be a positive integer (got ${String(opts.gapLimit)})`
6669
6732
  );
6670
6733
  }
6671
- 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
+ ];
6672
6745
  const maxIdx = opts.hd ? SCAN_MAX_INDEX : 0;
6673
6746
  const handlerErrors = [];
6674
6747
  let lastIndexUsed = -1;
6675
6748
  let unused = 0;
6676
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
+ };
6677
6765
  while (i <= maxIdx && unused < gapLimit) {
6678
- const descriptor = opts.materialize(i);
6679
- let hitAtThisIndex = false;
6680
- for (const h of discoverables) {
6681
- let found;
6682
- try {
6683
- found = await h.discoverAt(i, descriptor, opts.deps);
6684
- } catch (error) {
6685
- handlerErrors.push({ handler: h.type, index: i, error });
6686
- 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
+ }
6687
6788
  }
6688
- for (const c of found) {
6689
- await this.persistAndWatchContract(c);
6690
- hitAtThisIndex = true;
6789
+ if (hitAtThisIndex) {
6790
+ lastIndexUsed = index;
6791
+ unused = 0;
6792
+ } else {
6793
+ unused += 1;
6691
6794
  }
6692
6795
  }
6693
- if (hitAtThisIndex) {
6694
- lastIndexUsed = i;
6695
- unused = 0;
6696
- } else {
6697
- unused += 1;
6698
- }
6699
- i += 1;
6796
+ i = windowEnd + 1;
6700
6797
  }
6701
6798
  if (opts.hd && i > maxIdx && unused < gapLimit) {
6702
6799
  throw new Error(
@@ -7402,7 +7499,8 @@ var WalletReceiveRotator = class _WalletReceiveRotator {
7402
7499
  walletRepository: setup.walletRepository,
7403
7500
  contractRepository: setup.contractRepository,
7404
7501
  serverPubKey: setup.serverPubKey,
7405
- expectedContractType
7502
+ expectedContractType,
7503
+ baselineReceivePubKey: setup.offchainTapscript.options.pubKey
7406
7504
  };
7407
7505
  let boot;
7408
7506
  try {
@@ -7447,14 +7545,17 @@ var WalletReceiveRotator = class _WalletReceiveRotator {
7447
7545
  receivePubkey: existing.pubKey
7448
7546
  };
7449
7547
  }
7450
- let descriptor;
7451
- if (hasPeekableDescriptor(provider)) {
7452
- 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
+ };
7453
7555
  }
7454
- descriptor ??= await provider.getNextSigningDescriptor();
7455
7556
  return {
7456
7557
  rotator: new _WalletReceiveRotator(provider, void 0, opts.logger),
7457
- receivePubkey: deriveLeafPubkey(descriptor)
7558
+ receivePubkey: opts.baselineReceivePubKey ?? deriveLeafPubkey(current)
7458
7559
  };
7459
7560
  }
7460
7561
  /**
@@ -7559,7 +7660,7 @@ var WalletReceiveRotator = class _WalletReceiveRotator {
7559
7660
  const newScript = hex.encode(newTapscript.pkScript);
7560
7661
  const newAddress = newTapscript.address(wallet.network.hrp, wallet.arkServerPublicKey).encode();
7561
7662
  const manager = await wallet.getContractManager();
7562
- const csvTimelock = newTapscript.options.csvTimelock ?? DefaultVtxo.Script.DEFAULT_TIMELOCK;
7663
+ const csvTimelock = newTapscript.options.csvTimelock;
7563
7664
  const csvTimelockStr = timelockToSequence(csvTimelock).toString();
7564
7665
  const serverPubKeyHex = hex.encode(newTapscript.options.serverPubKey);
7565
7666
  const baseParams = {
@@ -7687,13 +7788,29 @@ var DescriptorSigningProviderMissingError = class extends Error {
7687
7788
  };
7688
7789
 
7689
7790
  // src/wallet/inputSignerRouter.ts
7690
- var DESCRIPTOR_CAPABLE_CONTRACT_TYPES = /* @__PURE__ */ new Set(["default", "delegate"]);
7791
+ var DESCRIPTOR_CAPABLE_CONTRACT_TYPES = /* @__PURE__ */ new Set(["default", "delegate", "boarding"]);
7691
7792
  var InputSignerRouter = class {
7692
7793
  constructor(deps) {
7693
7794
  this.deps = deps;
7694
7795
  }
7695
- async sign(tx, jobs) {
7696
- if (jobs.length === 0) return tx;
7796
+ /**
7797
+ * Resolve each job to its target signer without invoking signing. The
7798
+ * returned plan is the single source of truth for both {@link sign} and
7799
+ * the batch-eligibility predicate {@link canBatch} — callers that want
7800
+ * to pre-flight a batch path call {@link canBatch} (which delegates
7801
+ * here) so the routing rules never live in two places.
7802
+ *
7803
+ * Throws {@link MissingSigningDescriptorError} for a non-baseline
7804
+ * default/delegate contract whose `metadata.signingDescriptor` is
7805
+ * missing — the same condition that would later abort signing. Failing
7806
+ * here moves the failure earlier, before any PSBT is mutated.
7807
+ */
7808
+ async classify(jobs) {
7809
+ const identityIndexes = [];
7810
+ const descriptorGroups = /* @__PURE__ */ new Map();
7811
+ if (jobs.length === 0) {
7812
+ return { identityIndexes, descriptorGroups };
7813
+ }
7697
7814
  const distinctScripts = Array.from(new Set(jobs.map((j) => hex.encode(j.lookupScript))));
7698
7815
  const contracts = await this.deps.contractRepository.getContracts({
7699
7816
  script: distinctScripts
@@ -7706,8 +7823,6 @@ var InputSignerRouter = class {
7706
7823
  }
7707
7824
  const baselinePubKeyHex = hex.encode(await this.deps.identity.xOnlyPublicKey());
7708
7825
  const boardingScriptHex = hex.encode(this.deps.boardingPkScript);
7709
- const identityIndexes = [];
7710
- const descriptorGroups = /* @__PURE__ */ new Map();
7711
7826
  for (const job of jobs) {
7712
7827
  const scriptHex = hex.encode(job.lookupScript);
7713
7828
  const contract = scriptToContract.get(scriptHex);
@@ -7740,6 +7855,31 @@ var InputSignerRouter = class {
7740
7855
  descriptorGroups.set(descriptor, [job.index]);
7741
7856
  }
7742
7857
  }
7858
+ return { identityIndexes, descriptorGroups };
7859
+ }
7860
+ /**
7861
+ * Returns `true` when every signable input across all `jobSets` resolves
7862
+ * to the baseline {@link Identity} key — i.e. the descriptor provider
7863
+ * would not be invoked. Used by the wallet's send/recovery paths to
7864
+ * pre-flight the {@link BatchSignableIdentity.signMultiple} fast path,
7865
+ * which can only fold work a single identity key can sign.
7866
+ *
7867
+ * Accepts several job sets (e.g. an arkTx's jobs plus one set per
7868
+ * checkpoint) and classifies their union in a single pass. Eligibility
7869
+ * is monotonic — the union routes entirely to the baseline key iff every
7870
+ * set does — so this returns the same answer as ANDing the per-set
7871
+ * results, but with one {@link classify} (one repo round-trip + one
7872
+ * `xOnlyPublicKey` call) instead of one per set. Only the routing buckets
7873
+ * matter here, so the input-index collisions produced by flattening jobs
7874
+ * from different transactions are irrelevant.
7875
+ */
7876
+ async canBatch(...jobSets) {
7877
+ const plan = await this.classify(jobSets.flat());
7878
+ return plan.descriptorGroups.size === 0;
7879
+ }
7880
+ async sign(tx, jobs) {
7881
+ if (jobs.length === 0) return tx;
7882
+ const { identityIndexes, descriptorGroups } = await this.classify(jobs);
7743
7883
  let signed = tx;
7744
7884
  if (identityIndexes.length > 0) {
7745
7885
  signed = await this.deps.identity.sign(signed, identityIndexes);
@@ -7780,6 +7920,11 @@ function extractArkProviderUrl(provider) {
7780
7920
  return typeof serverUrl === "string" && serverUrl.length > 0 ? serverUrl : void 0;
7781
7921
  }
7782
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
+ }
7783
7928
  function delayToTimelock(delay) {
7784
7929
  return {
7785
7930
  value: delay,
@@ -7797,6 +7942,30 @@ function dedupeTimelocks(timelocks) {
7797
7942
  }
7798
7943
  return deduped;
7799
7944
  }
7945
+ async function ensureWalletContract(manager, params) {
7946
+ await manager.createContract(params);
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
+ }
7800
7969
  function hasToReadonly(identity) {
7801
7970
  return typeof identity === "object" && identity !== null && "toReadonly" in identity && typeof identity.toReadonly === "function";
7802
7971
  }
@@ -7807,7 +7976,6 @@ var ReadonlyWallet = class _ReadonlyWallet {
7807
7976
  this.onchainProvider = onchainProvider;
7808
7977
  this.indexerProvider = indexerProvider;
7809
7978
  this.arkServerPublicKey = arkServerPublicKey;
7810
- this.boardingTapscript = boardingTapscript;
7811
7979
  this.dustAmount = dustAmount;
7812
7980
  this.walletRepository = walletRepository;
7813
7981
  this.contractRepository = contractRepository;
@@ -7823,11 +7991,10 @@ var ReadonlyWallet = class _ReadonlyWallet {
7823
7991
  }
7824
7992
  }
7825
7993
  this._offchainTapscript = offchainTapscript;
7994
+ this._boardingTapscript = boardingTapscript;
7826
7995
  this.watcherConfig = watcherConfig;
7827
7996
  this._assetManager = new ReadonlyAssetManager(this.indexerProvider);
7828
- this.walletContractTimelocks = walletContractTimelocks && walletContractTimelocks.length > 0 ? dedupeTimelocks(walletContractTimelocks) : [
7829
- this.offchainTapscript.options.csvTimelock ?? DefaultVtxo.Script.DEFAULT_TIMELOCK
7830
- ];
7997
+ this.walletContractTimelocks = walletContractTimelocks && walletContractTimelocks.length > 0 ? dedupeTimelocks(walletContractTimelocks) : [this.offchainTapscript.options.csvTimelock];
7831
7998
  }
7832
7999
  _contractManager;
7833
8000
  _contractManagerInitializing;
@@ -7851,6 +8018,17 @@ var ReadonlyWallet = class _ReadonlyWallet {
7851
8018
  * {@link WalletReceiveRotator.rotate} is the sole intended caller of.
7852
8019
  */
7853
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;
7854
8032
  /**
7855
8033
  * Currently-active receive tapscript. Read-only from the outside;
7856
8034
  * mutated only via {@link Wallet.setOffchainTapscriptForRotation}
@@ -7859,6 +8037,52 @@ var ReadonlyWallet = class _ReadonlyWallet {
7859
8037
  get offchainTapscript() {
7860
8038
  return this._offchainTapscript;
7861
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
+ }
7862
8086
  /**
7863
8087
  * Protected helper to set up shared wallet configuration.
7864
8088
  * Extracts common logic used by both ReadonlyWallet.create() and Wallet.create().
@@ -7933,9 +8157,10 @@ var ReadonlyWallet = class _ReadonlyWallet {
7933
8157
  csvTimelock: exitTimelock
7934
8158
  };
7935
8159
  const offchainTapscript = !delegatePubKey ? new DefaultVtxo.Script(offchainOptions) : new DelegateVtxo.Script({ ...offchainOptions, delegatePubKey });
7936
- const boardingTapscript = new DefaultVtxo.Script({
7937
- ...offchainOptions,
7938
- csvTimelock: boardingTimelock
8160
+ const boardingTapscript = BoardingContractHandler.createScript({
8161
+ pubKey: hex.encode(pubKey),
8162
+ serverPubKey: hex.encode(serverPubKey),
8163
+ csvTimelock: timelockToSequence(boardingTimelock).toString()
7939
8164
  });
7940
8165
  const walletRepository = config.storage?.walletRepository ?? new IndexedDBWalletRepository();
7941
8166
  const contractRepository = config.storage?.contractRepository ?? new IndexedDBContractRepository();
@@ -8098,43 +8323,59 @@ var ReadonlyWallet = class _ReadonlyWallet {
8098
8323
  await clearSyncCursor(this.walletRepository);
8099
8324
  }
8100
8325
  /**
8101
- * 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).
8102
8339
  */
8103
8340
  async getBoardingTxs() {
8104
8341
  const utxos = [];
8105
8342
  const commitmentsToIgnore = /* @__PURE__ */ new Set();
8106
- const boardingAddress = await this.getBoardingAddress();
8107
- const txs = await this.onchainProvider.getTransactions(boardingAddress);
8343
+ const tapscripts = await this.getBoardingTapscripts();
8108
8344
  const outspendCache = /* @__PURE__ */ new Map();
8109
- for (const tx of txs) {
8110
- for (let i = 0; i < tx.vout.length; i++) {
8111
- const vout = tx.vout[i];
8112
- if (vout.scriptpubkey_address === boardingAddress) {
8113
- let spentStatuses = outspendCache.get(tx.txid);
8114
- if (!spentStatuses) {
8115
- spentStatuses = await this.onchainProvider.getTxOutspends(tx.txid);
8116
- outspendCache.set(tx.txid, spentStatuses);
8117
- }
8118
- const spentStatus = spentStatuses[i];
8119
- if (spentStatus?.spent) {
8120
- 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
+ });
8121
8378
  }
8122
- utxos.push({
8123
- txid: tx.txid,
8124
- vout: i,
8125
- value: Number(vout.value),
8126
- status: {
8127
- confirmed: tx.status.confirmed,
8128
- block_time: tx.status.block_time
8129
- },
8130
- isUnrolled: true,
8131
- virtualStatus: {
8132
- state: spentStatus?.spent ? "spent" : "settled",
8133
- commitmentTxIds: spentStatus?.spent ? [spentStatus.txid] : void 0
8134
- },
8135
- createdAt: tx.status.confirmed ? new Date(tx.status.block_time * 1e3) : /* @__PURE__ */ new Date(0),
8136
- script: hex.encode(this.boardingTapscript.pkScript)
8137
- });
8138
8379
  }
8139
8380
  }
8140
8381
  }
@@ -8164,48 +8405,130 @@ var ReadonlyWallet = class _ReadonlyWallet {
8164
8405
  };
8165
8406
  }
8166
8407
  /**
8167
- * 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.
8168
8418
  */
8169
- async getBoardingUtxos() {
8170
- const boardingAddress = await this.getBoardingAddress();
8171
- const boardingUtxos = await this.onchainProvider.getCoins(boardingAddress);
8172
- const utxos = boardingUtxos.map((utxo) => {
8173
- 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"]
8174
8434
  });
8175
- await this.walletRepository.saveUtxos(boardingAddress, utxos);
8176
- 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;
8177
8463
  }
8178
8464
  /**
8179
8465
  * Subscribe to onchain and offchain notifications for newly received funds.
8180
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
+ *
8181
8476
  * @param eventCallback - Callback invoked when matching funds are detected
8182
8477
  * @returns A function that stops the subscriptions
8183
8478
  */
8184
8479
  async notifyIncomingFunds(eventCallback) {
8185
8480
  const arkAddress = await this.getAddress();
8186
- const boardingAddress = await this.getBoardingAddress();
8187
8481
  let onchainStopFunc;
8188
8482
  let indexerStopFunc;
8189
- if (this.onchainProvider && boardingAddress) {
8190
- const findVoutOnTx = (tx) => {
8191
- return tx.vout.findIndex((v) => v.scriptpubkey_address === boardingAddress);
8192
- };
8193
- onchainStopFunc = await this.onchainProvider.watchAddresses(
8194
- [boardingAddress],
8195
- (txs) => {
8196
- const coins = txs.filter((tx) => findVoutOnTx(tx) !== -1).map((tx) => {
8197
- const { txid, status } = tx;
8198
- const vout = findVoutOnTx(tx);
8199
- const value = Number(tx.vout[vout].value);
8200
- return { txid, vout, value, status };
8201
- });
8202
- eventCallback({
8203
- type: "utxo",
8204
- coins
8205
- });
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;
8206
8520
  }
8207
- );
8208
- }
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();
8209
8532
  if (this.indexerProvider && arkAddress) {
8210
8533
  const cm = await this.getContractManager();
8211
8534
  let annotationQueue = Promise.resolve();
@@ -8234,7 +8557,10 @@ var ReadonlyWallet = class _ReadonlyWallet {
8234
8557
  });
8235
8558
  }
8236
8559
  const stopFunc = () => {
8560
+ stopped = true;
8561
+ boardingRotationStopFunc?.();
8237
8562
  onchainStopFunc?.();
8563
+ onchainStopFunc = void 0;
8238
8564
  indexerStopFunc?.();
8239
8565
  };
8240
8566
  return stopFunc;
@@ -8348,7 +8674,7 @@ var ReadonlyWallet = class _ReadonlyWallet {
8348
8674
  csvTimelock
8349
8675
  });
8350
8676
  const defaultScriptHex = hex.encode(defaultScript.pkScript);
8351
- await manager.createContract({
8677
+ await ensureWalletContract(manager, {
8352
8678
  type: "default",
8353
8679
  params: {
8354
8680
  pubKey: hex.encode(defaultScript.options.pubKey),
@@ -8381,6 +8707,23 @@ var ReadonlyWallet = class _ReadonlyWallet {
8381
8707
  });
8382
8708
  }
8383
8709
  }
8710
+ const boardingCsvTimelock = this.boardingTapscript.options.csvTimelock ?? DefaultVtxo.Script.DEFAULT_TIMELOCK;
8711
+ const baselineBoarding = new DefaultVtxo.Script({
8712
+ pubKey: baselinePubkey,
8713
+ serverPubKey: this.boardingTapscript.options.serverPubKey,
8714
+ csvTimelock: boardingCsvTimelock
8715
+ });
8716
+ await ensureWalletContract(manager, {
8717
+ type: "boarding",
8718
+ params: {
8719
+ pubKey: hex.encode(baselineBoarding.options.pubKey),
8720
+ serverPubKey: hex.encode(baselineBoarding.options.serverPubKey),
8721
+ csvTimelock: timelockToSequence(boardingCsvTimelock).toString()
8722
+ },
8723
+ script: hex.encode(baselineBoarding.pkScript),
8724
+ address: baselineBoarding.address(this.network.hrp, this.arkServerPublicKey).encode(),
8725
+ state: "active"
8726
+ });
8384
8727
  return manager;
8385
8728
  }
8386
8729
  /** Dispose wallet-owned managers and release background resources. */
@@ -8478,6 +8821,72 @@ var Wallet2 = class _Wallet extends ReadonlyWallet {
8478
8821
  setOffchainTapscriptForRotation(tapscript) {
8479
8822
  this._offchainTapscript = tapscript;
8480
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
+ }
8481
8890
  /**
8482
8891
  * Async mutex that serializes all operations submitting VTXOs to the Arkade
8483
8892
  * server (`settle`, `send`, `sendBitcoin`). This prevents VtxoManager's
@@ -8562,12 +8971,25 @@ var Wallet2 = class _Wallet extends ReadonlyWallet {
8562
8971
  const staticDescriptor = hd ? void 0 : `tr(${hex.encode(await this.identity.xOnlyPublicKey())})`;
8563
8972
  const materialize = (index) => hd ? provider.materializeDescriptorAt(index) : staticDescriptor;
8564
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
+ );
8565
8979
  const deps = {
8566
8980
  indexerProvider: this.indexerProvider,
8567
8981
  onchainProvider: this.onchainProvider,
8568
8982
  network: { hrp: this.network.hrp },
8569
- 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,
8570
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,
8571
8993
  delegatePubKey
8572
8994
  };
8573
8995
  const result = await manager.scanContracts({
@@ -8699,6 +9121,16 @@ var Wallet2 = class _Wallet extends ReadonlyWallet {
8699
9121
  boot?.rotator,
8700
9122
  boot?.provider
8701
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
+ }
8702
9134
  await wallet.getVtxoManager();
8703
9135
  return wallet;
8704
9136
  }
@@ -8866,7 +9298,10 @@ var Wallet2 = class _Wallet extends ReadonlyWallet {
8866
9298
  }
8867
9299
  const vtxos = await this.getVtxos({ withRecoverable: true });
8868
9300
  const filteredVtxos = [];
8869
- for (const vtxo of vtxos) {
9301
+ for (const vtxo of byValueDescending(vtxos)) {
9302
+ if (filteredVtxos.length >= MAX_VTXOS_PER_SETTLEMENT) {
9303
+ break;
9304
+ }
8870
9305
  const inputFee = estimator.evalOffchainInput({
8871
9306
  amount: BigInt(vtxo.value),
8872
9307
  type: vtxo.virtualStatus.state === "swept" ? "recoverable" : "vtxo",
@@ -8999,6 +9434,7 @@ var Wallet2 = class _Wallet extends ReadonlyWallet {
8999
9434
  eventCallback: eventCallback ? (event) => Promise.resolve(eventCallback(event)) : void 0
9000
9435
  });
9001
9436
  await this.updateDbAfterSettle(params.inputs, commitmentTxid);
9437
+ await this.maybeRotateBoardingAfterBoard(params.inputs);
9002
9438
  return commitmentTxid;
9003
9439
  } catch (error) {
9004
9440
  const inputIds = params.inputs.map((i) => `${i.txid}:${i.vout}`).join(",");
@@ -9016,6 +9452,41 @@ var Wallet2 = class _Wallet extends ReadonlyWallet {
9016
9452
  });
9017
9453
  }
9018
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
+ }
9019
9490
  async handleSettlementFinalizationEvent(event, inputs, forfeitOutputScript, connectorsGraph) {
9020
9491
  const signedForfeits = [];
9021
9492
  const isVtxo = (input) => "virtualStatus" in input;
@@ -9226,6 +9697,19 @@ var Wallet2 = class _Wallet extends ReadonlyWallet {
9226
9697
  }
9227
9698
  return jobs;
9228
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
+ }
9229
9713
  async safeRegisterIntent(intent, inputs) {
9230
9714
  try {
9231
9715
  return await this.arkProvider.registerIntent(intent);
@@ -9330,16 +9814,38 @@ var Wallet2 = class _Wallet extends ReadonlyWallet {
9330
9814
  seen.add(pendingTx.arkTxid);
9331
9815
  batchPending.push(pendingTx.arkTxid);
9332
9816
  try {
9333
- const finalCheckpoints = await Promise.all(
9334
- pendingTx.signedCheckpointTxs.map(async (c) => {
9335
- const tx = Transaction$2.fromPSBT(base64.decode(c));
9336
- const signedCheckpoint = await this._signerRouter.sign(
9337
- tx,
9338
- this.inputSigningJobsFromWitnessUtxos(tx)
9339
- );
9340
- return base64.encode(signedCheckpoint.toPSBT());
9341
- })
9817
+ const checkpointTxs = pendingTx.signedCheckpointTxs.map(
9818
+ (c) => Transaction$2.fromPSBT(base64.decode(c))
9819
+ );
9820
+ const checkpointJobs = checkpointTxs.map(
9821
+ (tx) => this.inputSigningJobsFromWitnessUtxos(tx)
9342
9822
  );
9823
+ const identity = this.identity;
9824
+ const batchEligible = isBatchSignable(identity) && await this._signerRouter.canBatch(...checkpointJobs);
9825
+ let finalCheckpoints;
9826
+ if (batchEligible) {
9827
+ const requests = checkpointTxs.map((tx, i) => ({
9828
+ tx,
9829
+ inputIndexes: checkpointJobs[i].map((j) => j.index)
9830
+ }));
9831
+ const signed = await identity.signMultiple(requests);
9832
+ if (signed.length !== requests.length) {
9833
+ throw new Error(
9834
+ `signMultiple returned ${signed.length} transactions, expected ${requests.length}`
9835
+ );
9836
+ }
9837
+ finalCheckpoints = signed.map((tx) => base64.encode(tx.toPSBT()));
9838
+ } else {
9839
+ finalCheckpoints = await Promise.all(
9840
+ checkpointTxs.map(async (tx, i) => {
9841
+ const signedCheckpoint = await this._signerRouter.sign(
9842
+ tx,
9843
+ checkpointJobs[i]
9844
+ );
9845
+ return base64.encode(signedCheckpoint.toPSBT());
9846
+ })
9847
+ );
9848
+ }
9343
9849
  await this.arkProvider.finalizeTx(pendingTx.arkTxid, finalCheckpoints);
9344
9850
  batchFinalized.push(pendingTx.arkTxid);
9345
9851
  } catch (error) {
@@ -9567,22 +10073,65 @@ var Wallet2 = class _Wallet extends ReadonlyWallet {
9567
10073
  index,
9568
10074
  lookupScript: VtxoScript.decode(input.tapTree).pkScript
9569
10075
  }));
9570
- const signedVirtualTx = await this._signerRouter.sign(offchainTx.arkTx, arkTxJobs);
10076
+ const checkpointJobs = offchainTx.checkpoints.map(
10077
+ (c) => this.inputSigningJobsFromWitnessUtxos(c)
10078
+ );
10079
+ let signedVirtualTx;
10080
+ let userSignedCheckpoints;
10081
+ const identity = this.identity;
10082
+ const batchEligible = isBatchSignable(identity) && await this._signerRouter.canBatch(arkTxJobs, ...checkpointJobs);
10083
+ if (batchEligible) {
10084
+ const requests = [
10085
+ {
10086
+ tx: offchainTx.arkTx.clone(),
10087
+ inputIndexes: arkTxJobs.map((j) => j.index)
10088
+ },
10089
+ ...offchainTx.checkpoints.map((c, i) => ({
10090
+ tx: c.clone(),
10091
+ inputIndexes: checkpointJobs[i].map((j) => j.index)
10092
+ }))
10093
+ ];
10094
+ const signed = await identity.signMultiple(requests);
10095
+ if (signed.length !== requests.length) {
10096
+ throw new Error(
10097
+ `signMultiple returned ${signed.length} transactions, expected ${requests.length}`
10098
+ );
10099
+ }
10100
+ const [firstSignedTx, ...signedCheckpoints] = signed;
10101
+ signedVirtualTx = firstSignedTx;
10102
+ userSignedCheckpoints = signedCheckpoints;
10103
+ } else {
10104
+ signedVirtualTx = await this._signerRouter.sign(offchainTx.arkTx, arkTxJobs);
10105
+ }
9571
10106
  await this.setPendingTxFlag(true);
9572
10107
  const { arkTxid, signedCheckpointTxs } = await this.arkProvider.submitTx(
9573
10108
  base64.encode(signedVirtualTx.toPSBT()),
9574
10109
  offchainTx.checkpoints.map((c) => base64.encode(c.toPSBT()))
9575
10110
  );
9576
- const finalCheckpoints = await Promise.all(
9577
- signedCheckpointTxs.map(async (c) => {
9578
- const tx = Transaction$2.fromPSBT(base64.decode(c));
9579
- const signedCheckpoint = await this._signerRouter.sign(
9580
- tx,
9581
- this.inputSigningJobsFromWitnessUtxos(tx)
10111
+ let finalCheckpoints;
10112
+ if (userSignedCheckpoints) {
10113
+ if (signedCheckpointTxs.length !== userSignedCheckpoints.length) {
10114
+ throw new Error(
10115
+ `submitTx returned ${signedCheckpointTxs.length} checkpoints, expected ${userSignedCheckpoints.length}`
9582
10116
  );
9583
- return base64.encode(signedCheckpoint.toPSBT());
9584
- })
9585
- );
10117
+ }
10118
+ finalCheckpoints = signedCheckpointTxs.map((c, i) => {
10119
+ const serverSigned = Transaction$2.fromPSBT(base64.decode(c));
10120
+ combineTapscriptSigs(userSignedCheckpoints[i], serverSigned);
10121
+ return base64.encode(serverSigned.toPSBT());
10122
+ });
10123
+ } else {
10124
+ finalCheckpoints = await Promise.all(
10125
+ signedCheckpointTxs.map(async (c) => {
10126
+ const tx = Transaction$2.fromPSBT(base64.decode(c));
10127
+ const signedCheckpoint = await this._signerRouter.sign(
10128
+ tx,
10129
+ this.inputSigningJobsFromWitnessUtxos(tx)
10130
+ );
10131
+ return base64.encode(signedCheckpoint.toPSBT());
10132
+ })
10133
+ );
10134
+ }
9586
10135
  await this.arkProvider.finalizeTx(arkTxid, finalCheckpoints);
9587
10136
  try {
9588
10137
  await this.setPendingTxFlag(false);
@@ -9727,10 +10276,9 @@ var Wallet2 = class _Wallet extends ReadonlyWallet {
9727
10276
  // mark virtual outputs as spent/settled, remove boarding inputs
9728
10277
  async updateDbAfterSettle(inputs, commitmentTxid) {
9729
10278
  try {
9730
- const boardingAddress = await this.getBoardingAddress();
9731
10279
  const spentVtxos = [];
9732
10280
  const inputArkTxIds = /* @__PURE__ */ new Set();
9733
- const boardingUtxoToRemove = /* @__PURE__ */ new Set();
10281
+ const boardingRemovalsByAddress = /* @__PURE__ */ new Map();
9734
10282
  const isVtxo = (input) => "virtualStatus" in input;
9735
10283
  const vtxoInputs = inputs.filter(isVtxo);
9736
10284
  const cm = await this.getContractManager();
@@ -9752,7 +10300,20 @@ var Wallet2 = class _Wallet extends ReadonlyWallet {
9752
10300
  isSpent: true
9753
10301
  });
9754
10302
  } else {
9755
- 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}`);
9756
10317
  }
9757
10318
  }
9758
10319
  if (spentVtxos.length > 0) {
@@ -9784,14 +10345,12 @@ var Wallet2 = class _Wallet extends ReadonlyWallet {
9784
10345
  );
9785
10346
  }
9786
10347
  }
9787
- if (boardingUtxoToRemove.size > 0) {
9788
- const currentUtxos = await this.walletRepository.getUtxos(boardingAddress);
9789
- const filtered = currentUtxos.filter(
9790
- (u) => !boardingUtxoToRemove.has(`${u.txid}:${u.vout}`)
9791
- );
9792
- 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);
9793
10352
  if (filtered.length > 0) {
9794
- await this.walletRepository.saveUtxos(boardingAddress, filtered);
10353
+ await this.walletRepository.saveUtxos(address, filtered);
9795
10354
  }
9796
10355
  }
9797
10356
  } catch (e) {
@@ -9832,12 +10391,17 @@ function selectVirtualCoins(coins, targetAmount) {
9832
10391
  }
9833
10392
  async function waitForIncomingFunds(wallet) {
9834
10393
  let stopFunc;
10394
+ let settled = false;
9835
10395
  return new Promise((resolve) => {
9836
- wallet.notifyIncomingFunds((coins) => {
9837
- resolve(coins);
9838
- if (stopFunc) stopFunc();
10396
+ wallet.notifyIncomingFunds((funds) => {
10397
+ const hasFunds = funds.type === "utxo" ? funds.coins.length > 0 : funds.newVtxos.length > 0;
10398
+ if (settled || !hasFunds) return;
10399
+ settled = true;
10400
+ resolve(funds);
10401
+ stopFunc?.();
9839
10402
  }).then((stop) => {
9840
10403
  stopFunc = stop;
10404
+ if (settled) stop();
9841
10405
  });
9842
10406
  });
9843
10407
  }
@@ -11186,9 +11750,7 @@ var WalletMessageHandler = class {
11186
11750
  );
11187
11751
  }
11188
11752
  if (funds.type === "utxo") {
11189
- const utxos = funds.coins.map((utxo) => extendCoin(this.readonlyWallet, utxo));
11190
- const boardingAddress = await this.readonlyWallet.getBoardingAddress();
11191
- await this.walletRepository?.saveUtxos(boardingAddress, utxos);
11753
+ const utxos = await this.readonlyWallet.getBoardingUtxos();
11192
11754
  this.scheduleForNextTick(
11193
11755
  () => this.tagged({
11194
11756
  type: "UTXO_UPDATE",
@@ -11217,13 +11779,16 @@ var WalletMessageHandler = class {
11217
11779
  return;
11218
11780
  }
11219
11781
  const vtxos = await this.getVtxosFromRepo();
11220
- const boardingAddress = await this.readonlyWallet.getBoardingAddress();
11221
- const coins = await this.readonlyWallet.onchainProvider.getCoins(boardingAddress);
11222
- await this.walletRepository.deleteUtxos(boardingAddress);
11223
- await this.walletRepository.saveUtxos(
11224
- boardingAddress,
11225
- coins.map((utxo) => extendCoin(this.readonlyWallet, utxo))
11226
- );
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
+ }
11227
11792
  const address = await this.readonlyWallet.getAddress();
11228
11793
  const txs = await this.buildTransactionHistoryFromCache(vtxos);
11229
11794
  if (txs) await this.walletRepository.saveTransactions(address, txs);
@@ -14014,5 +14579,5 @@ function isArkContract(str) {
14014
14579
  }
14015
14580
 
14016
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 };
14017
- //# sourceMappingURL=chunk-XROGFOPX.js.map
14018
- //# sourceMappingURL=chunk-XROGFOPX.js.map
14582
+ //# sourceMappingURL=chunk-HFXEUW55.js.map
14583
+ //# sourceMappingURL=chunk-HFXEUW55.js.map