@arkade-os/sdk 0.4.66 → 0.4.67

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 (80) hide show
  1. package/README.md +66 -15
  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/{asyncStorageTaskQueue-BZcIN7sA.d.cts → asyncStorageTaskQueue-LTpeXYaD.d.cts} +2 -2
  9. package/dist/{asyncStorageTaskQueue-B6SijMi3.d.ts → asyncStorageTaskQueue-Za-e_zMk.d.ts} +2 -2
  10. package/dist/{chunk-ZIRFXBKL.cjs → chunk-2JV4VQ7C.cjs} +33 -13
  11. package/dist/chunk-2JV4VQ7C.cjs.map +1 -0
  12. package/dist/{chunk-J33ZEM3A.js → chunk-3WIWNE4P.js} +33 -14
  13. package/dist/chunk-3WIWNE4P.js.map +1 -0
  14. package/dist/{chunk-CYOI4PEA.js → chunk-6TAQF6EV.js} +107 -13
  15. package/dist/chunk-6TAQF6EV.js.map +1 -0
  16. package/dist/{chunk-DSC3G6QZ.js → chunk-AEWJU6NZ.js} +1006 -167
  17. package/dist/chunk-AEWJU6NZ.js.map +1 -0
  18. package/dist/{chunk-3S3JZUEN.cjs → chunk-CO3KKSWM.cjs} +17 -17
  19. package/dist/{chunk-3S3JZUEN.cjs.map → chunk-CO3KKSWM.cjs.map} +1 -1
  20. package/dist/{chunk-55ZZ6RBL.js → chunk-EALWOVHK.js} +4 -4
  21. package/dist/{chunk-55ZZ6RBL.js.map → chunk-EALWOVHK.js.map} +1 -1
  22. package/dist/{chunk-5E6IZNRG.cjs → chunk-MJUTO3PY.cjs} +1413 -563
  23. package/dist/chunk-MJUTO3PY.cjs.map +1 -0
  24. package/dist/{chunk-R5POS3RS.js → chunk-MZFUZUTQ.js} +4 -4
  25. package/dist/{chunk-R5POS3RS.js.map → chunk-MZFUZUTQ.js.map} +1 -1
  26. package/dist/{chunk-PXPPZF5K.cjs → chunk-OIZ2V2VD.cjs} +152 -58
  27. package/dist/chunk-OIZ2V2VD.cjs.map +1 -0
  28. package/dist/{chunk-IM6IKNSL.cjs → chunk-R2YAMONM.cjs} +17 -17
  29. package/dist/{chunk-IM6IKNSL.cjs.map → chunk-R2YAMONM.cjs.map} +1 -1
  30. package/dist/{chunk-2JZGPXU4.js → chunk-X3VEMUVV.js} +3 -3
  31. package/dist/{chunk-2JZGPXU4.js.map → chunk-X3VEMUVV.js.map} +1 -1
  32. package/dist/{chunk-LL47QIHD.cjs → chunk-YDRZDZPP.cjs} +10 -10
  33. package/dist/{chunk-LL47QIHD.cjs.map → chunk-YDRZDZPP.cjs.map} +1 -1
  34. package/dist/contracts/handlers/index.cjs +9 -9
  35. package/dist/contracts/handlers/index.d.cts +3 -3
  36. package/dist/contracts/handlers/index.d.ts +3 -3
  37. package/dist/contracts/handlers/index.js +2 -2
  38. package/dist/{delegate-YXupcWWJ.d.cts → delegate-Bz2e2zXY.d.cts} +1 -1
  39. package/dist/{delegate-T-vgxMYZ.d.ts → delegate-cdmzb2f9.d.ts} +1 -1
  40. package/dist/{wallet-CNibc3yJ.d.ts → hdWalletCapable-BSjuUYOO.d.cts} +318 -127
  41. package/dist/{wallet-DJN1QgFt.d.cts → hdWalletCapable-Hi12xjCJ.d.ts} +318 -127
  42. package/dist/{index-e7LYXk1d.d.cts → index-BiACuWaf.d.ts} +109 -13
  43. package/dist/{index-D_FgwLH9.d.ts → index-D2GjZxBr.d.cts} +109 -13
  44. package/dist/{index-uUE_uQPn.d.ts → index-DU5o_hz7.d.cts} +206 -19
  45. package/dist/{index-uUE_uQPn.d.cts → index-DU5o_hz7.d.ts} +206 -19
  46. package/dist/index.cjs +322 -278
  47. package/dist/index.d.cts +291 -41
  48. package/dist/index.d.ts +291 -41
  49. package/dist/index.js +4 -4
  50. package/dist/repositories/realm/index.cjs +19 -19
  51. package/dist/repositories/realm/index.d.cts +2 -2
  52. package/dist/repositories/realm/index.d.ts +2 -2
  53. package/dist/repositories/realm/index.js +5 -5
  54. package/dist/repositories/sqlite/index.cjs +18 -18
  55. package/dist/repositories/sqlite/index.d.cts +2 -2
  56. package/dist/repositories/sqlite/index.d.ts +2 -2
  57. package/dist/repositories/sqlite/index.js +5 -5
  58. package/dist/{taskRunner-BOVcIH1N.d.cts → taskRunner-BZJMLQ6g.d.cts} +2 -2
  59. package/dist/{taskRunner-DNyD1kXG.d.ts → taskRunner-DATaRQus.d.ts} +2 -2
  60. package/dist/wallet/expo/background.cjs +14 -14
  61. package/dist/wallet/expo/background.d.cts +3 -3
  62. package/dist/wallet/expo/background.d.ts +3 -3
  63. package/dist/wallet/expo/background.js +6 -6
  64. package/dist/wallet/expo/index.cjs +18 -14
  65. package/dist/wallet/expo/index.cjs.map +1 -1
  66. package/dist/wallet/expo/index.d.cts +7 -5
  67. package/dist/wallet/expo/index.d.ts +7 -5
  68. package/dist/wallet/expo/index.js +10 -6
  69. package/dist/wallet/expo/index.js.map +1 -1
  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 +3 -3
  75. package/dist/chunk-5E6IZNRG.cjs.map +0 -1
  76. package/dist/chunk-CYOI4PEA.js.map +0 -1
  77. package/dist/chunk-DSC3G6QZ.js.map +0 -1
  78. package/dist/chunk-J33ZEM3A.js.map +0 -1
  79. package/dist/chunk-PXPPZF5K.cjs.map +0 -1
  80. package/dist/chunk-ZIRFXBKL.cjs.map +0 -1
@@ -1,6 +1,6 @@
1
- import { AssetId, AssetGroup, AssetInput, AssetOutput, Metadata, Packet, BufferReader, BufferWriter, AssetRef, RestIndexerProvider } from './chunk-2JZGPXU4.js';
2
- import { witnessRefToBytes, validateProgram, toASM, stringifyArtifact, serializeArkadeContractParams, resolveAsm, parseArtifact, getOpcodeValue, getOpcodeName, fromASM, deserializeArkadeContractParams, computeArkadeScriptPublicKey, bytesToASM, asmToBytes, arkadeWitnessHash, arkadeScriptHash, SUPPORTED_PROGRAM_VERSION, OPCODE_VALUES, OPCODE_NAMES, bignum_exports, ArkadeScript, ArkadeProgramScript, ARKADE_OPS, ARKADE_OPCODE_VALUES, ARKADE_OPCODE_NAMES, ARKADE_OPCODES, ARKADE_OP, toXOnly, inputName, contractHandlers, isMainnetDescriptor, descriptorIsOurs, deriveDescriptorLeafPubKey, deriveDescriptorLeafCompressedPubKey, parseHDDescriptor, DefaultVtxo, speculativeReceiveMetadata, DelegateVtxo, WALLET_RECEIVE_SOURCE, BoardingContractHandler, identityDescriptor } from './chunk-CYOI4PEA.js';
3
- import { VtxoScript, Transaction, isRecoverable, CSVMultisigTapscript, DEFAULT_NETWORK, resolveEmulatorPubkey, hasTerminalSpend, getNormalizedVtxos, setArkPsbtField, PrevArkTxField, ConditionWitness, craftToSpendTx, ArkAddress, isVirtualCoin, getArkPsbtFields, CosignerPublicKey, ServerResponseMismatchError, OP_RETURN_EMPTY_PKSCRIPT, sequenceToTimelock, decodeTapscript, scriptFromTapLeafScript, CLTVMultisigTapscript, VtxoTaprootTree, timelockToSequence, baseFetch, Intent, rateGate, DEFAULT_NETWORK_NAME, normalizeVtxo, getSequence, toBatchExpiry, maybeArkError, isArkError, ArkErrorName, toOffchainInputFeeParams, canRecoverOnchain, getNetwork, MultisigTapscript, isEventSourceError, isEventSourceUnavailableError, isRetryableProviderError, getAllNormalizedVtxos, DEFAULT_PAGE_SIZE, RestArkProvider, toVirtualStatus, ArkError, TapTreeCoder, resolveTimeHeight, canSpendOffchain, parseLegacyExpiry, isExpired, DEFAULT_ARKADE_SERVER_URL, ProviderUnavailableError, networks as networks$1, isPastExpiry, fetchVtxoCreatedAtByTxid } from './chunk-J33ZEM3A.js';
1
+ import { AssetId, AssetGroup, AssetInput, AssetOutput, Metadata, Packet, BufferReader, BufferWriter, AssetRef, RestIndexerProvider } from './chunk-X3VEMUVV.js';
2
+ import { witnessRefToBytes, validateProgram, toASM, stringifyArtifact, serializeArkadeContractParams, resolveAsm, parseArtifact, getOpcodeValue, getOpcodeName, fromASM, deserializeArkadeContractParams, computeArkadeScriptPublicKey, bytesToASM, asmToBytes, arkadeWitnessHash, arkadeScriptHash, SUPPORTED_PROGRAM_VERSION, OPCODE_VALUES, OPCODE_NAMES, bignum_exports, ArkadeScript, ArkadeProgramScript, ARKADE_OPS, ARKADE_OPCODE_VALUES, ARKADE_OPCODE_NAMES, ARKADE_OPCODES, ARKADE_OP, toXOnly, inputName, contractHandlers, isMainnetDescriptor, descriptorIsOurs, deriveDescriptorLeafPubKey, deriveDescriptorLeafCompressedPubKey, parseHDDescriptor, identityDescriptor, DefaultVtxo, speculativeReceiveMetadata, DelegateVtxo, WALLET_RECEIVE_SOURCE, BoardingContractHandler } from './chunk-6TAQF6EV.js';
3
+ import { VtxoScript, Transaction, isRecoverable, CSVMultisigTapscript, DEFAULT_NETWORK, resolveEmulatorPubkey, hasTerminalSpend, getNormalizedVtxos, setArkPsbtField, PrevArkTxField, ConditionWitness, craftToSpendTx, ArkAddress, isVirtualCoin, getArkPsbtFields, CosignerPublicKey, ServerResponseMismatchError, OP_RETURN_EMPTY_PKSCRIPT, canSweepOnchain, sequenceToTimelock, decodeTapscript, scriptFromTapLeafScript, CLTVMultisigTapscript, VtxoTaprootTree, timelockToSequence, baseFetch, Intent, rateGate, DEFAULT_NETWORK_NAME, normalizeVtxo, getSequence, toBatchExpiry, maybeArkError, isArkError, ArkErrorName, toOffchainInputFeeParams, canRecoverOnchain, getNetwork, MultisigTapscript, isEventSourceError, isEventSourceUnavailableError, isRetryableProviderError, getAllNormalizedVtxos, DEFAULT_PAGE_SIZE, RestArkProvider, toVirtualStatus, ArkError, TapTreeCoder, resolveTimeHeight, canSpendOffchain, parseLegacyExpiry, isExpired, PrevoutTxField, DEFAULT_ARKADE_SERVER_URL, ProviderUnavailableError, networks as networks$1, isPastExpiry, fetchVtxoCreatedAtByTxid } from './chunk-3WIWNE4P.js';
4
4
  import { __export } from './chunk-NSBPE2FW.js';
5
5
  import { sha256 as sha256$1, equalBytes, hash160, sha256x2, concatBytes, randomPrivateKeyBytes, pubECDSA, pubSchnorr } from '@scure/btc-signer/utils.js';
6
6
  import { SigHash, Script, OP, OutScript, Address, RawWitness, p2tr, p2wpkh, TaprootControlBlock, DEFAULT_SEQUENCE, Transaction as Transaction$2 } from '@scure/btc-signer';
@@ -917,6 +917,12 @@ var ForeignDescriptorError = class extends Error {
917
917
  descriptor;
918
918
  name = "ForeignDescriptorError";
919
919
  };
920
+ var WalletCannotAllocateAddressError = class extends Error {
921
+ name = "WalletCannotAllocateAddressError";
922
+ constructor(reason, options) {
923
+ super(`cannot allocate a fresh address: ${reason}`, options);
924
+ }
925
+ };
920
926
  async function resolveDescriptorSigner(descriptor, identity, provider) {
921
927
  const owner = provider?.isOurs(descriptor) ? provider : isHDCapableIdentity(identity) && identity.isOurs(descriptor) ? identity : void 0;
922
928
  if (owner && parseHDDescriptor(descriptor)) {
@@ -941,6 +947,10 @@ function isHDAllocationCapable(value) {
941
947
  const v = value;
942
948
  return typeof v.getNextSigningDescriptor === "function" && typeof v.advanceSigningDescriptorWatermark === "function";
943
949
  }
950
+ function isAddressAllocationCapable(value) {
951
+ if (typeof value !== "object" || value === null) return false;
952
+ return typeof value.getNewAddresses === "function";
953
+ }
944
954
 
945
955
  // ../../node_modules/@noble/hashes/utils.js
946
956
  function isBytes(a) {
@@ -1302,15 +1312,26 @@ async function provisionDescriptor(wallet) {
1302
1312
  return allocated ?? await identityDescriptor(wallet.identity);
1303
1313
  }
1304
1314
  async function provisionRefundKey(wallet) {
1305
- const descriptor = await provisionDescriptor(wallet);
1315
+ const descriptor = await identityDescriptor(wallet.identity);
1306
1316
  const signer = await contractSigner(wallet, descriptor);
1307
- return { descriptor, pubkey: await signer.xOnlyPublicKey() };
1317
+ const pubkey = await signer.xOnlyPublicKey();
1318
+ const identityPubkey = await wallet.identity.xOnlyPublicKey();
1319
+ if (!equalBytes(pubkey, identityPubkey)) {
1320
+ throw new Error(
1321
+ "provisionRefundKey: descriptor pubkey does not match wallet identity \u2014 the refund key and the refund address would be on different keys"
1322
+ );
1323
+ }
1324
+ const address = await wallet.getAddress();
1325
+ const { pkScript } = ArkAddress.decode(address);
1326
+ return { descriptor, pubkey, pkScript, address };
1308
1327
  }
1309
1328
  async function provisionClaimSecret(wallet, opts = {}) {
1310
1329
  if (opts.preimage && opts.preimage.length !== 32) {
1311
1330
  throw new Error(`preimage must be 32 bytes, got ${opts.preimage.length}`);
1312
1331
  }
1313
- const { descriptor, pubkey } = await provisionRefundKey(wallet);
1332
+ const descriptor = await provisionDescriptor(wallet);
1333
+ const signer = await contractSigner(wallet, descriptor);
1334
+ const pubkey = await signer.xOnlyPublicKey();
1314
1335
  const claim = async () => {
1315
1336
  if (opts.preimage) return { preimage: opts.preimage, mustPersistPreimage: true };
1316
1337
  if (isPerArtifactDescriptor(descriptor)) {
@@ -1610,8 +1631,6 @@ var RestDelegatorProvider = RestDelegateProvider;
1610
1631
  function isDelegateInfo(data) {
1611
1632
  return !!data && typeof data === "object" && "pubkey" in data && "fee" in data && typeof data.pubkey === "string" && typeof data.fee === "string" && data.pubkey !== "" && data.fee !== "" && (typeof data.delegateAddress === "string" && data.delegateAddress !== "" || typeof data.delegatorAddress === "string" && data.delegatorAddress !== "");
1612
1633
  }
1613
-
1614
- // src/providers/onchain.ts
1615
1634
  var ESPLORA_URL = {
1616
1635
  bitcoin: "https://mempool.arkade.sh/api",
1617
1636
  testnet: "https://mempool.space/testnet/api",
@@ -1672,6 +1691,14 @@ var EsploraProvider = class {
1672
1691
  }
1673
1692
  return response.json();
1674
1693
  }
1694
+ async getRawTransaction(txid) {
1695
+ const response = await baseFetch(`${this.baseUrl}/tx/${txid}/hex`);
1696
+ if (!response.ok) {
1697
+ const error = await response.text();
1698
+ throw new Error(`Failed to get raw transaction ${txid}: ${error}`);
1699
+ }
1700
+ return hex.decode((await response.text()).trim());
1701
+ }
1675
1702
  async getTxStatus(txid) {
1676
1703
  const txresponse = await baseFetch(`${this.baseUrl}/tx/${txid}`);
1677
1704
  if (!txresponse.ok) {
@@ -1795,7 +1822,9 @@ var EsploraProvider = class {
1795
1822
  const error = await response.text();
1796
1823
  throw new Error(`Failed to broadcast package: ${error}`);
1797
1824
  }
1798
- return response.json();
1825
+ const result = await response.json();
1826
+ assertPackageAccepted(result);
1827
+ return result;
1799
1828
  }
1800
1829
  async broadcastTx(tx) {
1801
1830
  const response = await baseFetch(`${this.baseUrl}/tx`, {
@@ -1812,6 +1841,24 @@ var EsploraProvider = class {
1812
1841
  return response.text();
1813
1842
  }
1814
1843
  };
1844
+ function assertPackageAccepted(result) {
1845
+ if (!result || typeof result !== "object") return;
1846
+ const r = result;
1847
+ const msg = typeof r.package_msg === "string" ? r.package_msg : void 0;
1848
+ if (msg === void 0 || msg === "success") return;
1849
+ const reasons = [];
1850
+ const results = r["tx-results"];
1851
+ if (results && typeof results === "object") {
1852
+ for (const entry of Object.values(results)) {
1853
+ if (!entry || typeof entry !== "object" || entry.error === void 0) continue;
1854
+ const txid = typeof entry.txid === "string" ? entry.txid : "unknown tx";
1855
+ reasons.push(`${txid}: ${String(entry.error)}`);
1856
+ }
1857
+ }
1858
+ throw new Error(
1859
+ reasons.length > 0 ? `Package rejected (${msg}) \u2014 ${reasons.join("; ")}` : `Package rejected (${msg})`
1860
+ );
1861
+ }
1815
1862
  function isValidBlocksTip(tip) {
1816
1863
  return Array.isArray(tip) && tip.every((t) => {
1817
1864
  return t && typeof t === "object" && typeof t.id === "string" && t.id.length > 0 && typeof t.height === "number" && t.height >= 0 && typeof t.mediantime === "number" && t.mediantime > 0;
@@ -3709,7 +3756,9 @@ function selectPendingRecoveryOutpoints(contractsWithVtxos, signerSet, nowSecond
3709
3756
  continue;
3710
3757
  }
3711
3758
  for (const v of vtxos) {
3712
- if (!hasTerminalSpend(v) && !v.isSwept) out.add(`${v.txid}:${v.vout}`);
3759
+ if (!hasTerminalSpend(v) && !v.isSwept && !v.isUnrolled) {
3760
+ out.add(`${v.txid}:${v.vout}`);
3761
+ }
3713
3762
  }
3714
3763
  }
3715
3764
  return out;
@@ -4518,6 +4567,7 @@ var VtxoManager = class _VtxoManager {
4518
4567
  async getDeprecatedSignerStatus() {
4519
4568
  const wallet = this.requireMigrationCapableWallet();
4520
4569
  const info = await wallet.arkProvider.getInfo();
4570
+ await wallet.settleServerInfoChanges?.();
4521
4571
  const { reports: vtxoReports } = await this.classifyDeprecatedSignerContracts(info);
4522
4572
  const { reports: boardingReports } = await this.classifyDeprecatedSignerBoarding(info);
4523
4573
  return mergeSignerReports(vtxoReports, boardingReports);
@@ -4531,26 +4581,33 @@ var VtxoManager = class _VtxoManager {
4531
4581
  */
4532
4582
  async migrateCore(options) {
4533
4583
  const wallet = this.requireMigrationCapableWallet();
4584
+ const signerBeforeRefresh = hex.encode(wallet.arkServerPublicKey);
4534
4585
  const info = await wallet.arkProvider.getInfo();
4586
+ await wallet.settleServerInfoChanges?.();
4587
+ const rotatedOnRefresh = signerBeforeRefresh !== hex.encode(wallet.arkServerPublicKey);
4535
4588
  wallet.refreshDeprecatedSigners(info);
4536
4589
  const signerSet = signerSetFromInfo(info);
4537
4590
  const nowSeconds = Math.floor(Date.now() / 1e3);
4538
4591
  const walletSignerHex = hex.encode(wallet.arkServerPublicKey);
4539
4592
  const walletClass = classifyAgainstSignerSet(walletSignerHex, signerSet, nowSeconds);
4540
4593
  if (signerSet.deprecated.size === 0 && walletClass.status === "CURRENT") {
4541
- return { rotated: false, expired: [], signers: [] };
4594
+ return { rotated: rotatedOnRefresh, expired: [], signers: [] };
4542
4595
  }
4543
4596
  if (walletClass.status === "UNKNOWN_SIGNER") {
4544
4597
  const { reports: vtxoReports2 } = await this.classifyDeprecatedSignerContracts(info);
4545
4598
  const { reports: boardingReports2 } = await this.classifyDeprecatedSignerBoarding(info);
4546
4599
  return {
4547
- rotated: false,
4600
+ // Normally false here — a drained rotation targets the active
4601
+ // signer, which classifies CURRENT, not UNKNOWN. It is true only
4602
+ // when a second rotation landed inside the drain, leaving us on a
4603
+ // signer newer than `info`: still a rotation this pass applied.
4604
+ rotated: rotatedOnRefresh,
4548
4605
  expired: [],
4549
4606
  signers: mergeSignerReports(vtxoReports2, boardingReports2),
4550
4607
  skipped: "unknown-wallet-signer"
4551
4608
  };
4552
4609
  }
4553
- const rotated = await this.ensureReceiveOnActiveSigner(info);
4610
+ const rotated = await this.ensureReceiveOnActiveSigner(info) || rotatedOnRefresh;
4554
4611
  const {
4555
4612
  reports: vtxoReports,
4556
4613
  migratable: vtxoMigratable,
@@ -4719,8 +4776,9 @@ var VtxoManager = class _VtxoManager {
4719
4776
  if (!serverPubKey) continue;
4720
4777
  const cls = classifyAgainstSignerSet(serverPubKey, signerSet, nowSeconds);
4721
4778
  if (cls.status === "CURRENT") continue;
4722
- const recoverable = vtxos.filter((v) => v.isSwept && !hasTerminalSpend(v));
4723
- const spendable = vtxos.filter((v) => !hasTerminalSpend(v) && !v.isSwept);
4779
+ const live = vtxos.filter((v) => !v.isUnrolled);
4780
+ const recoverable = live.filter((v) => v.isSwept && !hasTerminalSpend(v));
4781
+ const spendable = live.filter((v) => !hasTerminalSpend(v) && !v.isSwept);
4724
4782
  const value = spendable.reduce((sum, v) => sum + v.value, 0);
4725
4783
  let recoverableCount = 0;
4726
4784
  let recoverableValue = 0;
@@ -8985,7 +9043,7 @@ var NON_TERMINAL_INTENT_STATES = ALL_INTENT_STATES.filter(
8985
9043
  (s) => !isTerminalIntentState(s)
8986
9044
  );
8987
9045
  function isConsumed(vtxo) {
8988
- return vtxo !== void 0 && (vtxo.isSpent === true || vtxo.settledBy !== void 0);
9046
+ return vtxo !== void 0 && hasTerminalSpend(vtxo);
8989
9047
  }
8990
9048
  async function classifyIntent(intent, vtxosByOutpoint, now) {
8991
9049
  if (isTerminalIntentState(intent.state)) return void 0;
@@ -8995,6 +9053,11 @@ async function classifyIntent(intent, vtxosByOutpoint, now) {
8995
9053
  const commitmentTransactionId = intent.commitmentTransactionId ?? consumed.map((v) => v?.settledBy).find((s) => !!s);
8996
9054
  return terminal(intent, "batch_succeeded", now, { commitmentTransactionId });
8997
9055
  }
9056
+ if (consumed.some((vtxo) => vtxo?.isUnrolled)) {
9057
+ return terminal(intent, "cancelled", now, {
9058
+ cancellationReason: "reconciliation: an input was unilaterally exited"
9059
+ });
9060
+ }
8998
9061
  if (intent.state === "waiting_to_submit") {
8999
9062
  return terminal(intent, "cancelled", now, {
9000
9063
  cancellationReason: "reconciliation: intent was never submitted before restart"
@@ -11604,7 +11667,9 @@ var WalletReceiveRotator = class _WalletReceiveRotator {
11604
11667
  const { tapscript: newTapscript, params } = buildReceiveContract(
11605
11668
  wallet.offchainTapscript,
11606
11669
  descriptor,
11607
- wallet.network.hrp);
11670
+ wallet.network.hrp,
11671
+ true
11672
+ );
11608
11673
  const newScript = params.script;
11609
11674
  const manager = await wallet.getContractManager();
11610
11675
  await manager.createContract(params);
@@ -11646,7 +11711,7 @@ function buildReceiveContract(current, descriptor, hrp, tagSource) {
11646
11711
  // index-0 key. Without it, spends produce unsigned PSBTs that the
11647
11712
  // server rejects with `INVALID_PSBT_INPUT (5): missing tapscript spend sig`.
11648
11713
  metadata: {
11649
- ...{ source: WALLET_RECEIVE_SOURCE },
11714
+ ...tagSource && { source: WALLET_RECEIVE_SOURCE },
11650
11715
  signingDescriptor: descriptor
11651
11716
  }
11652
11717
  };
@@ -11730,6 +11795,7 @@ function computeOffchainBalance(vtxos, caps) {
11730
11795
  let intentLocked = 0;
11731
11796
  let recoverable = 0;
11732
11797
  let pendingRecovery = 0;
11798
+ let unrolled = 0;
11733
11799
  const owned = /* @__PURE__ */ new Map();
11734
11800
  const spendable = /* @__PURE__ */ new Map();
11735
11801
  const addAssets = (into, vtxo) => {
@@ -11740,6 +11806,10 @@ function computeOffchainBalance(vtxos, caps) {
11740
11806
  for (const vtxo of vtxos) {
11741
11807
  if (hasTerminalSpend(vtxo)) continue;
11742
11808
  addAssets(owned, vtxo);
11809
+ if (vtxo.isUnrolled) {
11810
+ unrolled += vtxo.value;
11811
+ continue;
11812
+ }
11743
11813
  if (isPendingRecovery(vtxo)) {
11744
11814
  pendingRecovery += vtxo.value;
11745
11815
  continue;
@@ -11772,7 +11842,8 @@ function computeOffchainBalance(vtxos, caps) {
11772
11842
  intentLocked,
11773
11843
  recoverable,
11774
11844
  pendingRecovery,
11775
- total: settled + preconfirmed + recoverable + pendingRecovery,
11845
+ unrolled,
11846
+ total: settled + preconfirmed + recoverable + pendingRecovery + unrolled,
11776
11847
  assets: toAssets(owned),
11777
11848
  availableAssets: toAssets(spendable)
11778
11849
  };
@@ -11995,13 +12066,16 @@ function filterSnapshotVtxos(snapshot, filter, pendingSpendOutpoints) {
11995
12066
  if (pendingSpendOutpoints.has(`${vtxo.txid}:${vtxo.vout}`)) {
11996
12067
  return false;
11997
12068
  }
11998
- if (!hasTerminalSpend(vtxo)) {
11999
- if (!f.withRecoverable && canRecoverOnchain(vtxo, now)) {
12000
- return false;
12001
- }
12002
- return true;
12069
+ if (vtxo.isUnrolled) {
12070
+ return !!f.withUnrolled;
12003
12071
  }
12004
- return !!(f.withUnrolled && vtxo.isUnrolled);
12072
+ if (hasTerminalSpend(vtxo)) {
12073
+ return false;
12074
+ }
12075
+ if (!f.withRecoverable && canRecoverOnchain(vtxo, now)) {
12076
+ return false;
12077
+ }
12078
+ return true;
12005
12079
  });
12006
12080
  }
12007
12081
  var PENDING_RECOVERY_REASON = "is past its operator signer's rotation cutoff \u2014 the operator will not co-sign it until it recovers";
@@ -12504,7 +12578,11 @@ var ReadonlyWallet = class _ReadonlyWallet {
12504
12578
  this.getBoardingUtxos(),
12505
12579
  this.contractSnapshot()
12506
12580
  ]);
12507
- const vtxos = filterSnapshotVtxos(snapshot, void 0, this._pendingSpendOutpoints);
12581
+ const vtxos = filterSnapshotVtxos(
12582
+ snapshot,
12583
+ { withRecoverable: true, withUnrolled: true },
12584
+ this._pendingSpendOutpoints
12585
+ );
12508
12586
  let confirmed = 0;
12509
12587
  let unconfirmed = 0;
12510
12588
  for (const utxo of boardingUtxos) {
@@ -12532,6 +12610,7 @@ var ReadonlyWallet = class _ReadonlyWallet {
12532
12610
  intentLocked: offchain.intentLocked,
12533
12611
  recoverable: offchain.recoverable,
12534
12612
  pendingRecovery: offchain.pendingRecovery,
12613
+ unrolled: offchain.unrolled,
12535
12614
  total: totalBoarding + offchain.total,
12536
12615
  assets: offchain.assets,
12537
12616
  availableAssets: offchain.availableAssets
@@ -13391,6 +13470,27 @@ var Wallet2 = class _Wallet extends ReadonlyWallet {
13391
13470
  console.warn("server-signer rotation on info change failed", e);
13392
13471
  }
13393
13472
  }
13473
+ /**
13474
+ * Await an `onServerInfoChanged` handler still applying a rotation.
13475
+ *
13476
+ * The provider emits synchronously but {@link handleServerInfoChanged} runs
13477
+ * off the emit, so between the two a caller can observe a half-applied
13478
+ * rotation: {@link rotateServerSigner} persists the active signer's contract
13479
+ * rows *before* committing the tapscripts, so `arkServerPublicKey` still
13480
+ * reads the old key while the new key's rows are already in the repository.
13481
+ * A path that refreshes server info and then reads signer-derived state must
13482
+ * drain the chain first, or it both reads that torn state and races the
13483
+ * handler for the rotation itself.
13484
+ *
13485
+ * Resolves once the chain is idle; a handler that threw already logged, and
13486
+ * its rejection is swallowed here.
13487
+ *
13488
+ * @internal Invoked by {@link dispose} and the {@link VtxoManager} migration
13489
+ * pass; not part of the stable public API.
13490
+ */
13491
+ async settleServerInfoChanges() {
13492
+ await this._serverInfoInFlight?.catch(() => void 0);
13493
+ }
13394
13494
  _receiveRotatorInstalled = false;
13395
13495
  /**
13396
13496
  * Descriptor-aware signer used by {@link _signerRouter} to sign
@@ -13525,43 +13625,250 @@ var Wallet2 = class _Wallet extends ReadonlyWallet {
13525
13625
  * provider and keeps a single index-0 boarding address for its lifetime,
13526
13626
  * so this returns the existing {@link getBoardingAddress} unchanged
13527
13627
  * (no rotation, no index burned).
13628
+ *
13629
+ * **Behaviour change.** A wallet configured with a custom
13630
+ * {@link DescriptorProvider} (`walletMode: <provider>`) now rotates here.
13631
+ * It previously did not: allocation went through the contract manager,
13632
+ * which only wires an `allocate` hook for {@link HDDescriptorProvider}, so
13633
+ * such a wallet read as "declined to allocate" and silently kept its
13634
+ * index-0 boarding address forever. Those wallets now burn an index per
13635
+ * call — including via {@link maybeRotateBoardingAfterBoard}, which fires
13636
+ * on every settle that consumes a boarding UTXO. Funds at retired
13637
+ * addresses stay reachable: each rotation persists its `boarding` contract
13638
+ * before swapping, and {@link getBoardingUtxos} fans out over the full
13639
+ * historical set.
13640
+ *
13641
+ * @deprecated Use {@link getNewAddresses} — it mints any combination of
13642
+ * address types at one shared index, and reports the contract row behind
13643
+ * each. Note the difference in display behaviour: this method *swaps* the
13644
+ * advertised boarding address, where `getNewAddresses` mints side
13645
+ * addresses and leaves {@link getBoardingAddress} alone. To keep this
13646
+ * method's behaviour, keep calling this method.
13528
13647
  */
13529
13648
  async getNewBoardingAddress() {
13530
- const provider = this._descriptorProvider;
13531
- if (!provider) {
13532
- return this.getBoardingAddress();
13649
+ const [minted] = await this.allocateAddresses(["boarding"], false, true);
13650
+ if (!minted?.allocated) return this.getBoardingAddress();
13651
+ if (minted.type === "boarding") this.setBoardingTapscriptForRotation(minted.tapscript);
13652
+ return minted.address;
13653
+ }
13654
+ /**
13655
+ * Allocate a *fresh* address of each requested type, all derived from one
13656
+ * newly allocated HD index.
13657
+ *
13658
+ * This is the explicit allocator the receive path otherwise lacks:
13659
+ * {@link getAddress} and {@link getBoardingAddress} are stable reads of the
13660
+ * wallet's display addresses, and the display receive address only advances
13661
+ * when a payment arrives ({@link WalletReceiveRotator} rotates on
13662
+ * `vtxo_received`). Issuing a second address before the first is paid — one
13663
+ * invoice for Alice, another for Bob — has to go through here.
13664
+ *
13665
+ * Each call:
13666
+ *
13667
+ * - allocates **one** index from the shared HD stream, however many types
13668
+ * were asked for, so a `default` + `boarding` pair are siblings rather
13669
+ * than two burnt indices;
13670
+ * - builds each requested script at that index, preserving every other
13671
+ * option of the wallet's current script for that flavour (including a
13672
+ * delegate wallet's `delegate` shape);
13673
+ * - persists each as an `active` contract carrying its `signingDescriptor`,
13674
+ * so the ContractWatcher monitors it, the balance counts it, and
13675
+ * {@link signerForDescriptor} can recover the key;
13676
+ * - slides the look-ahead band, since the watermark moved past indices an
13677
+ * external issuer may still be handing out.
13678
+ *
13679
+ * The minted rows are deliberately left **untagged**: the boot lookups
13680
+ * adopt the newest {@link WALLET_RECEIVE_SOURCE}-tagged row as the display
13681
+ * address, and a side address issued to one counterparty must not become
13682
+ * the address the wallet advertises to everyone else. {@link getAddress}
13683
+ * and {@link getBoardingAddress} are unchanged by this call.
13684
+ *
13685
+ * A wallet with no HD stream (`walletMode: 'static'` / `'auto'`) has one
13686
+ * address per flavour for its lifetime. Without `forceNew` it returns those
13687
+ * — the real persisted rows, no index burned; with `forceNew` it throws
13688
+ * {@link WalletCannotAllocateAddressError} rather than hand back an address
13689
+ * that is not in fact fresh.
13690
+ *
13691
+ * @example
13692
+ * ```typescript
13693
+ * const [invoice] = await wallet.getNewAddresses({ forceNew: true });
13694
+ * // hand `invoice.address` to Alice, keep the descriptor with the invoice
13695
+ * const signer = await wallet.signerForDescriptor(invoice.signingDescriptor);
13696
+ * ```
13697
+ */
13698
+ async getNewAddresses(opts) {
13699
+ const minted = await this.allocateAddresses(
13700
+ opts?.types ?? ["default"],
13701
+ opts?.forceNew ?? false,
13702
+ false
13703
+ );
13704
+ return minted.map(({ address, signingDescriptor, contract }) => ({
13705
+ address,
13706
+ signingDescriptor,
13707
+ contract
13708
+ }));
13709
+ }
13710
+ /**
13711
+ * Shared allocation core behind {@link getNewAddresses} and the deprecated
13712
+ * {@link getNewBoardingAddress}, so the two cannot drift on what an
13713
+ * allocation costs or what gets persisted.
13714
+ *
13715
+ * @param tagSource - Tag the persisted rows {@link WALLET_RECEIVE_SOURCE},
13716
+ * making the next boot adopt them as the display address. Only the
13717
+ * deprecated boarding allocator passes `true`; `getNewAddresses` mints side
13718
+ * addresses and must not hijack what the wallet advertises.
13719
+ */
13720
+ async allocateAddresses(types, forceNew, tagSource) {
13721
+ if (types.length === 0) {
13722
+ throw new Error("getNewAddresses: `types` must name at least one address type");
13533
13723
  }
13534
13724
  const manager = await this.getContractManager();
13535
- const descriptor = await manager.getNextSigningDescriptor();
13536
- if (!descriptor) return this.getBoardingAddress();
13537
- const pubKey = deriveDescriptorLeafPubKey(descriptor);
13538
- const newBoarding = new DefaultVtxo.Script({
13725
+ const provider = this._descriptorProvider;
13726
+ const descriptor = !provider ? void 0 : provider instanceof HDDescriptorProvider ? await manager.getNextSigningDescriptor() : await provider.getNextSigningDescriptor();
13727
+ if (!descriptor) {
13728
+ if (forceNew) {
13729
+ throw new WalletCannotAllocateAddressError(
13730
+ provider ? "the descriptor provider declined to allocate an index" : "this wallet has no HD stream (walletMode 'static' / 'auto')"
13731
+ );
13732
+ }
13733
+ return this.currentAddresses(manager, types);
13734
+ }
13735
+ const allocated = [];
13736
+ try {
13737
+ for (const type of types) {
13738
+ allocated.push(await this.mintAddress(manager, type, descriptor, tagSource, true));
13739
+ }
13740
+ } finally {
13741
+ try {
13742
+ await manager.refillLookAhead();
13743
+ } catch (e) {
13744
+ console.warn("look-ahead refill after address allocation failed", e);
13745
+ }
13746
+ }
13747
+ return allocated;
13748
+ }
13749
+ /**
13750
+ * The wallet's existing display addresses, as persisted contract rows —
13751
+ * the honest answer for a wallet with no index to burn.
13752
+ *
13753
+ * Each type is minted from the descriptor that already owns its display
13754
+ * script ({@link displayDescriptor}), not from the identity key, so the
13755
+ * returned `signingDescriptor` is the one that signs for the returned
13756
+ * `address` even on a wallet that has rotated.
13757
+ */
13758
+ async currentAddresses(manager, types) {
13759
+ const out = [];
13760
+ for (const type of types) {
13761
+ const descriptor = await this.displayDescriptor(manager, type);
13762
+ out.push(await this.mintAddress(manager, type, descriptor, false, false));
13763
+ }
13764
+ return out;
13765
+ }
13766
+ /**
13767
+ * The descriptor that owns the wallet's current display script for `type`,
13768
+ * read off the row the wallet persisted when it adopted that script.
13769
+ *
13770
+ * Deriving it from the identity instead is only correct for a wallet that
13771
+ * never rotates. On one that has — an HD wallet whose provider later
13772
+ * declines to allocate — the display script belongs to an HD child, and
13773
+ * pairing its address with the identity descriptor hands the caller a
13774
+ * {@link NewAddress} whose `signingDescriptor` resolves, via
13775
+ * {@link signerForDescriptor}, to a signer holding the wrong key.
13776
+ *
13777
+ * Falls back to the identity's pathless `tr(pubkey)` when no row carries a
13778
+ * descriptor: the static / `auto` case, where the identity key genuinely
13779
+ * owns the display script and this is what `getNextSigningDescriptor`
13780
+ * answers too. (The index-0 baselines registered at construction carry no
13781
+ * `signingDescriptor` metadata, so those wallets take this path.)
13782
+ */
13783
+ async displayDescriptor(manager, type) {
13784
+ const display = type === "boarding" ? this._boardingTapscript : this.offchainTapscript;
13785
+ const [row] = await manager.getContracts({ script: hex.encode(display.pkScript) });
13786
+ const persisted = row?.metadata?.signingDescriptor;
13787
+ if (typeof persisted === "string") return persisted;
13788
+ return identityDescriptor(this.identity);
13789
+ }
13790
+ /**
13791
+ * Build, persist and shape one address of `type` owned by `descriptor` —
13792
+ * the single build-and-register path behind both {@link allocateAddresses}
13793
+ * and {@link currentAddresses}, so a freshly allocated address and a
13794
+ * re-registered display address cannot disagree on what gets written.
13795
+ *
13796
+ * The descriptor is the sole source of the script's owner key in both
13797
+ * branches. Deriving one flavour from the descriptor and the other from a
13798
+ * tapscript field is what lets `address` and `signingDescriptor` drift
13799
+ * apart.
13800
+ */
13801
+ async mintAddress(manager, type, descriptor, tagSource, allocated) {
13802
+ if (type === "boarding") {
13803
+ const built2 = this.buildBoardingContract(
13804
+ deriveDescriptorLeafPubKey(descriptor),
13805
+ descriptor,
13806
+ tagSource
13807
+ );
13808
+ const contract2 = await manager.createContract(built2.params);
13809
+ return {
13810
+ type,
13811
+ // `built.tapscript` is a `DefaultVtxo.Script` here without a
13812
+ // cast — the branch, not a widened local, does the narrowing.
13813
+ tapscript: built2.tapscript,
13814
+ // A boarding row persists the *ark* encoding of its script, so
13815
+ // `contract.address` is unusable for an onchain sender: the
13816
+ // onchain form has to be re-derived.
13817
+ address: built2.tapscript.onchainAddress(this.network),
13818
+ signingDescriptor: descriptor,
13819
+ contract: contract2,
13820
+ allocated
13821
+ };
13822
+ }
13823
+ const built = buildReceiveContract(
13824
+ this.offchainTapscript,
13825
+ descriptor,
13826
+ this.network.hrp,
13827
+ tagSource
13828
+ );
13829
+ const contract = await manager.createContract(built.params);
13830
+ return {
13831
+ type,
13832
+ tapscript: built.tapscript,
13833
+ // Offchain rows already store the address they advertise, and
13834
+ // reading it back keeps a coalesced row (`createContract` is
13835
+ // first-wins on script) honest about what was actually persisted.
13836
+ address: contract.address,
13837
+ signingDescriptor: descriptor,
13838
+ contract,
13839
+ allocated
13840
+ };
13841
+ }
13842
+ /**
13843
+ * Build the boarding contract owned by `pubKey`, keeping every other option
13844
+ * of the wallet's current boarding tapscript (notably the boarding-exit
13845
+ * CSV, which is index-independent). The offchain analogue lives in
13846
+ * {@link buildReceiveContract}.
13847
+ */
13848
+ buildBoardingContract(pubKey, descriptor, tagSource) {
13849
+ const tapscript = new DefaultVtxo.Script({
13539
13850
  ...this._boardingTapscript.options,
13540
13851
  pubKey
13541
13852
  });
13542
- const csvTimelock = newBoarding.options.csvTimelock ?? DefaultVtxo.Script.DEFAULT_TIMELOCK;
13543
- await manager.createContract({
13544
- type: "boarding",
13853
+ const csvTimelock = tapscript.options.csvTimelock ?? DefaultVtxo.Script.DEFAULT_TIMELOCK;
13854
+ return {
13855
+ tapscript,
13545
13856
  params: {
13546
- pubKey: hex.encode(pubKey),
13547
- serverPubKey: hex.encode(newBoarding.options.serverPubKey),
13548
- csvTimelock: timelockToSequence(csvTimelock).toString()
13549
- },
13550
- script: hex.encode(newBoarding.pkScript),
13551
- address: newBoarding.address(this.network.hrp, this.arkServerPublicKey).encode(),
13552
- state: "active",
13553
- metadata: {
13554
- source: WALLET_RECEIVE_SOURCE,
13555
- signingDescriptor: descriptor
13857
+ type: "boarding",
13858
+ params: {
13859
+ pubKey: hex.encode(pubKey),
13860
+ serverPubKey: hex.encode(tapscript.options.serverPubKey),
13861
+ csvTimelock: timelockToSequence(csvTimelock).toString()
13862
+ },
13863
+ script: hex.encode(tapscript.pkScript),
13864
+ address: tapscript.address(this.network.hrp, this.arkServerPublicKey).encode(),
13865
+ state: "active",
13866
+ metadata: {
13867
+ ...tagSource && { source: WALLET_RECEIVE_SOURCE },
13868
+ signingDescriptor: descriptor
13869
+ }
13556
13870
  }
13557
- });
13558
- this.setBoardingTapscriptForRotation(newBoarding);
13559
- try {
13560
- await manager.refillLookAhead();
13561
- } catch (e) {
13562
- console.warn("look-ahead refill after boarding allocation failed", e);
13563
- }
13564
- return newBoarding.onchainAddress(this.network);
13871
+ };
13565
13872
  }
13566
13873
  /**
13567
13874
  * @see HDWalletCapable.getCurrentSigningDescriptor
@@ -13946,7 +14253,7 @@ var Wallet2 = class _Wallet extends ReadonlyWallet {
13946
14253
  await this._restoreInFlight?.catch(() => void 0);
13947
14254
  this._serverInfoUnsub?.();
13948
14255
  this._serverInfoUnsub = void 0;
13949
- await this._serverInfoInFlight?.catch(() => void 0);
14256
+ await this.settleServerInfoChanges();
13950
14257
  let rotatorError;
13951
14258
  try {
13952
14259
  await this._receiveRotator?.dispose();
@@ -14478,7 +14785,10 @@ var Wallet2 = class _Wallet extends ReadonlyWallet {
14478
14785
  * `typeof` guard skips the arknote strings settle also accepts.
14479
14786
  *
14480
14787
  * No-ops for static / `auto` wallets (no descriptor provider — boarding
14481
- * stays on its fixed index-0 address). Best-effort and non-fatal: the
14788
+ * stays on its fixed index-0 address). Wallets on a custom
14789
+ * {@link DescriptorProvider} DO rotate here now, where they previously
14790
+ * no-opped — see the behaviour-change note on {@link
14791
+ * getNewBoardingAddress}. Best-effort and non-fatal: the
14482
14792
  * settle has already committed and its txid must be returned, so a
14483
14793
  * rotation failure is logged and swallowed rather than thrown. Funds at
14484
14794
  * the retired boarding address remain discoverable — the old `boarding`
@@ -15136,6 +15446,8 @@ var Wallet2 = class _Wallet extends ReadonlyWallet {
15136
15446
  for (const vtxo of vtxos) {
15137
15447
  if (hasTerminalSpend(vtxo)) {
15138
15448
  unclaimed.push(cashReport(vtxo, "already-spent"));
15449
+ } else if (vtxo.isUnrolled) {
15450
+ unclaimed.push(cashReport(vtxo, "exited"));
15139
15451
  } else if (isSubdust(vtxo, this.dustAmount)) {
15140
15452
  unclaimed.push(cashReport(vtxo, "subdust"));
15141
15453
  } else if (vtxo.isSwept) {
@@ -16026,6 +16338,11 @@ var MessageBus = class {
16026
16338
  buildServicesFn;
16027
16339
  boundOnMessage = this.onMessage.bind(this);
16028
16340
  intentRepository;
16341
+ /** Pending broadcasts, drained FIFO by a single {@link drain} loop. */
16342
+ broadcastQueue = [];
16343
+ draining = false;
16344
+ /** Bumped wherever the generation owning the queued broadcasts ends. */
16345
+ broadcastEpoch = 0;
16029
16346
  /** Start the message bus and attach service-worker event listeners. */
16030
16347
  async start() {
16031
16348
  if (this.running) return;
@@ -16048,6 +16365,7 @@ var MessageBus = class {
16048
16365
  this.running = false;
16049
16366
  this.tickInProgress = false;
16050
16367
  this.initialized = false;
16368
+ this.invalidateBroadcasts();
16051
16369
  if (this.tickTimeout !== null) {
16052
16370
  self.clearTimeout(this.tickTimeout);
16053
16371
  this.tickTimeout = null;
@@ -16060,6 +16378,78 @@ var MessageBus = class {
16060
16378
  self.removeEventListener("message", this.boundOnMessage);
16061
16379
  await Promise.all(Array.from(this.handlers.values()).map((updater) => updater.stop()));
16062
16380
  }
16381
+ /** Invalidate every broadcast belonging to the outgoing generation. */
16382
+ invalidateBroadcasts() {
16383
+ this.broadcastEpoch++;
16384
+ this.broadcastQueue.length = 0;
16385
+ }
16386
+ broadcastToClients(responses) {
16387
+ this.broadcastQueue.push(...responses);
16388
+ void this.drain();
16389
+ }
16390
+ /**
16391
+ * Single fan-out loop, so pushes and tick responses keep one FIFO order.
16392
+ * A helper posting per response would race: `clients.matchAll()` gives no
16393
+ * cross-call resolution order, and it would de-batch `runTick`.
16394
+ */
16395
+ async drain() {
16396
+ if (this.draining) return;
16397
+ this.draining = true;
16398
+ try {
16399
+ while (this.broadcastQueue.length) {
16400
+ const epoch = this.broadcastEpoch;
16401
+ const batch = this.broadcastQueue.splice(0);
16402
+ if (!this.running) {
16403
+ this.logDroppedBroadcasts(batch, "bus stopped");
16404
+ return;
16405
+ }
16406
+ let clients;
16407
+ try {
16408
+ clients = await self.clients.matchAll({
16409
+ includeUncontrolled: true,
16410
+ type: "window"
16411
+ });
16412
+ } catch (err) {
16413
+ this.logDroppedBroadcasts(batch, "clients.matchAll failed", err);
16414
+ continue;
16415
+ }
16416
+ if (!this.running) {
16417
+ this.logDroppedBroadcasts(batch, "bus stopped during matchAll");
16418
+ return;
16419
+ }
16420
+ if (this.broadcastEpoch !== epoch) {
16421
+ this.logDroppedBroadcasts(batch, "re-init landed during matchAll");
16422
+ continue;
16423
+ }
16424
+ for (const message of batch) {
16425
+ for (const client of clients) {
16426
+ try {
16427
+ client.postMessage(message);
16428
+ } catch (err) {
16429
+ if (this.debug)
16430
+ console.warn(`[${message.tag}] broadcast to client failed`, err);
16431
+ }
16432
+ }
16433
+ }
16434
+ }
16435
+ } catch {
16436
+ } finally {
16437
+ this.draining = false;
16438
+ if (this.running && this.broadcastQueue.length) void this.drain();
16439
+ }
16440
+ }
16441
+ /**
16442
+ * A dropped batch has no periodic recovery to fall back on now that events
16443
+ * no longer ride the tick, so name the loss under {@link Options.debug}.
16444
+ */
16445
+ logDroppedBroadcasts(batch, reason, ...error) {
16446
+ if (!this.debug) return;
16447
+ console.warn(
16448
+ `dropping ${batch.length} broadcast(s): ${reason}`,
16449
+ batch.map((response) => response.tag),
16450
+ ...error
16451
+ );
16452
+ }
16063
16453
  scheduleNextTick() {
16064
16454
  if (!this.running) return;
16065
16455
  if (this.tickTimeout !== null) return;
@@ -16088,16 +16478,7 @@ var MessageBus = class {
16088
16478
  if (this.debug)
16089
16479
  console.log(`[${updater.messageTag}] outgoing tick response:`, response);
16090
16480
  if (response && response.length > 0) {
16091
- self.clients.matchAll({
16092
- includeUncontrolled: true,
16093
- type: "window"
16094
- }).then((clients) => {
16095
- for (const message of response) {
16096
- clients.forEach((client) => {
16097
- client.postMessage(message);
16098
- });
16099
- }
16100
- });
16481
+ this.broadcastToClients(response);
16101
16482
  }
16102
16483
  } catch (err) {
16103
16484
  if (this.debug) console.error(`[${updater.messageTag}] tick failed`, err);
@@ -16136,6 +16517,14 @@ var MessageBus = class {
16136
16517
  self.clearTimeout(this.tickTimeout);
16137
16518
  this.tickTimeout = null;
16138
16519
  }
16520
+ this.invalidateBroadcasts();
16521
+ const epoch = this.broadcastEpoch;
16522
+ const channel = {
16523
+ broadcast: (response) => {
16524
+ if (epoch !== this.broadcastEpoch) return;
16525
+ this.broadcastToClients([response]);
16526
+ }
16527
+ };
16139
16528
  if (this.initialized) {
16140
16529
  this.initialized = false;
16141
16530
  await Promise.all(
@@ -16152,9 +16541,7 @@ var MessageBus = class {
16152
16541
  try {
16153
16542
  for (const updater of this.handlers.values()) {
16154
16543
  if (this.debug) console.log(`Starting updater: ${updater.messageTag}`);
16155
- await updater.start(services, {
16156
- walletRepository: this.walletRepository
16157
- });
16544
+ await updater.start(services, { walletRepository: this.walletRepository }, channel);
16158
16545
  started.push(updater);
16159
16546
  }
16160
16547
  } catch (err) {
@@ -16824,6 +17211,17 @@ var WalletMessageHandler = class {
16824
17211
  incomingFundsSubscription;
16825
17212
  contractEventsSubscription;
16826
17213
  onNextTick = [];
17214
+ channel;
17215
+ // One counter per subscription field, bumped exactly where that field is
17216
+ // dropped — never where a neighbour's is. They cannot be merged:
17217
+ // `onWalletInitialized` re-subscribes incoming funds while
17218
+ // `ensureContractEventBroadcasting` deliberately keeps its subscription, so
17219
+ // a shared bump would leave the live contract-event emitter permanently
17220
+ // stale, and folding requests in would truncate an in-flight settle.
17221
+ incomingFundsEpoch = 0;
17222
+ contractEventsEpoch = 0;
17223
+ /** Per-request progress callbacks: ends with the handler, not with a re-init. */
17224
+ requestEpoch = 0;
16827
17225
  /**
16828
17226
  * Instantiate a new WalletUpdater.
16829
17227
  * Can override the default `messageTag` allowing more than one updater to run in parallel.
@@ -16834,13 +17232,20 @@ var WalletMessageHandler = class {
16834
17232
  }
16835
17233
  // lifecycle methods
16836
17234
  async start(...params) {
16837
- const [services, repositories] = params;
17235
+ const [services, repositories, channel] = params;
16838
17236
  this.readonlyWallet = services.readonlyWallet;
16839
17237
  this.wallet = services.wallet;
16840
17238
  this.arkProvider = services.arkProvider;
16841
17239
  this.walletRepository = repositories.walletRepository;
17240
+ this.channel = channel;
17241
+ this.onNextTick = [];
16842
17242
  }
16843
17243
  async stop() {
17244
+ this.requestEpoch++;
17245
+ this.incomingFundsEpoch++;
17246
+ this.contractEventsEpoch++;
17247
+ this.channel = void 0;
17248
+ this.onNextTick = [];
16844
17249
  if (this.incomingFundsSubscription) {
16845
17250
  this.incomingFundsSubscription();
16846
17251
  this.incomingFundsSubscription = void 0;
@@ -16863,8 +17268,8 @@ var WalletMessageHandler = class {
16863
17268
  this.indexerProvider = void 0;
16864
17269
  }
16865
17270
  async tick(_now) {
16866
- const results = await Promise.allSettled(this.onNextTick.map((fn) => fn()));
16867
- this.onNextTick = [];
17271
+ const pending = this.onNextTick.splice(0);
17272
+ const results = await Promise.allSettled(pending.map((fn) => fn()));
16868
17273
  return results.map((result) => {
16869
17274
  if (result.status === "fulfilled") {
16870
17275
  return result.value;
@@ -16874,8 +17279,46 @@ var WalletMessageHandler = class {
16874
17279
  }
16875
17280
  }).filter((response) => response !== null);
16876
17281
  }
16877
- scheduleForNextTick(callback) {
16878
- this.onNextTick.push(callback);
17282
+ epochOf(scope) {
17283
+ switch (scope) {
17284
+ case "incomingFunds":
17285
+ return this.incomingFundsEpoch;
17286
+ case "contractEvents":
17287
+ return this.contractEventsEpoch;
17288
+ case "request":
17289
+ return this.requestEpoch;
17290
+ default: {
17291
+ const _exhaustive = scope;
17292
+ throw new Error(`Unknown scope: ${_exhaustive}`);
17293
+ }
17294
+ }
17295
+ }
17296
+ newGeneration(scope) {
17297
+ const born = this.epochOf(scope);
17298
+ const stale = () => born !== this.epochOf(scope);
17299
+ return {
17300
+ get stale() {
17301
+ return stale();
17302
+ }
17303
+ };
17304
+ }
17305
+ /**
17306
+ * A handle rather than a `broadcast(response, epoch)` call, because it can
17307
+ * only be written where the closure it guards is created — an epoch passed
17308
+ * at the call site reads identically whether it guards anything or not.
17309
+ */
17310
+ newEmitter(scope) {
17311
+ const gen = this.newGeneration(scope);
17312
+ return {
17313
+ get stale() {
17314
+ return gen.stale;
17315
+ },
17316
+ emit: (response) => {
17317
+ if (!response || gen.stale) return;
17318
+ if (this.channel) return this.channel.broadcast(response);
17319
+ this.onNextTick.push(() => response);
17320
+ }
17321
+ };
16879
17322
  }
16880
17323
  requireWallet() {
16881
17324
  if (!this.wallet) {
@@ -16904,6 +17347,7 @@ var WalletMessageHandler = class {
16904
17347
  }
16905
17348
  async handleMessage(message) {
16906
17349
  const id = message.id;
17350
+ const emitter = this.newEmitter("request");
16907
17351
  if (message.type === "INIT_WALLET") {
16908
17352
  await this.handleInitWallet(message);
16909
17353
  return this.tagged({
@@ -17172,6 +17616,18 @@ var WalletMessageHandler = class {
17172
17616
  payload: { descriptor }
17173
17617
  });
17174
17618
  }
17619
+ case "GET_NEW_ADDRESSES": {
17620
+ const wallet = this.wallet;
17621
+ if (!wallet) throw new WalletNotInitializedError();
17622
+ const addresses = await wallet.getNewAddresses(
17623
+ message.payload
17624
+ );
17625
+ return this.tagged({
17626
+ id,
17627
+ type: "NEW_ADDRESSES",
17628
+ payload: { addresses }
17629
+ });
17630
+ }
17175
17631
  case "GET_USED_SIGNING_DESCRIPTORS": {
17176
17632
  const wallet = this.wallet;
17177
17633
  const descriptors = isHDWalletCapable(wallet) ? await wallet.getUsedSigningDescriptors(
@@ -17262,8 +17718,8 @@ var WalletMessageHandler = class {
17262
17718
  const wallet = this.requireWallet();
17263
17719
  const vtxoManager = await wallet.getVtxoManager();
17264
17720
  const txid = await vtxoManager.recoverVtxos((e) => {
17265
- this.scheduleForNextTick(
17266
- () => this.tagged({
17721
+ emitter.emit(
17722
+ this.tagged({
17267
17723
  id,
17268
17724
  type: "RECOVER_VTXOS_EVENT",
17269
17725
  payload: e
@@ -17307,8 +17763,8 @@ var WalletMessageHandler = class {
17307
17763
  const wallet = this.requireWallet();
17308
17764
  const vtxoManager = await wallet.getVtxoManager();
17309
17765
  const txid = await vtxoManager.renewVtxos((e) => {
17310
- this.scheduleForNextTick(
17311
- () => this.tagged({
17766
+ emitter.emit(
17767
+ this.tagged({
17312
17768
  id,
17313
17769
  type: "RENEW_VTXOS_EVENT",
17314
17770
  payload: e
@@ -17346,8 +17802,8 @@ var WalletMessageHandler = class {
17346
17802
  const vtxoManager = await wallet.getVtxoManager();
17347
17803
  const report = await vtxoManager.migrateDeprecatedSignerVtxos({
17348
17804
  eventCallback: (e) => {
17349
- this.scheduleForNextTick(
17350
- () => this.tagged({
17805
+ emitter.emit(
17806
+ this.tagged({
17351
17807
  id,
17352
17808
  type: "MIGRATE_DEPRECATED_SIGNER_VTXOS_EVENT",
17353
17809
  payload: e
@@ -17450,6 +17906,7 @@ var WalletMessageHandler = class {
17450
17906
  intentLocked: offchain.intentLocked,
17451
17907
  recoverable: offchain.recoverable,
17452
17908
  pendingRecovery: offchain.pendingRecovery,
17909
+ unrolled: offchain.unrolled,
17453
17910
  total: totalBoarding + offchain.total,
17454
17911
  assets: offchain.assets,
17455
17912
  availableAssets: offchain.availableAssets
@@ -17460,11 +17917,15 @@ var WalletMessageHandler = class {
17460
17917
  return this.readonlyWallet.getBoardingUtxos();
17461
17918
  }
17462
17919
  async onWalletInitialized() {
17463
- if (!this.readonlyWallet || !this.arkProvider || !this.indexerProvider || !this.walletRepository) {
17920
+ const wallet = this.readonlyWallet;
17921
+ if (!wallet || !this.arkProvider || !this.indexerProvider || !this.walletRepository) {
17464
17922
  return;
17465
17923
  }
17924
+ const init = this.newGeneration("incomingFunds");
17466
17925
  await this.ensureContractEventBroadcasting();
17926
+ if (init.stale) return;
17467
17927
  await this.refreshCachedData();
17928
+ if (init.stale) return;
17468
17929
  if (this.wallet) {
17469
17930
  try {
17470
17931
  const vtxos = await this.getVtxosFromRepo();
@@ -17477,72 +17938,81 @@ var WalletMessageHandler = class {
17477
17938
  } catch (error) {
17478
17939
  console.error("Error recovering pending transactions:", error);
17479
17940
  }
17941
+ if (init.stale) return;
17480
17942
  }
17481
17943
  if (this.incomingFundsSubscription) this.incomingFundsSubscription();
17482
- const address = await this.readonlyWallet.getAddress();
17483
- this.incomingFundsSubscription = await this.readonlyWallet.notifyIncomingFunds(
17484
- async (funds) => {
17485
- if (funds.type === "vtxo") {
17486
- const { newVtxos, spentVtxos } = funds;
17487
- if (newVtxos.length + spentVtxos.length === 0) return;
17488
- const byScript = /* @__PURE__ */ new Map();
17489
- for (const v of [...newVtxos, ...spentVtxos]) {
17490
- if (!v.script) {
17491
- console.warn(
17492
- `WalletMessageHandler.notifyIncomingFunds: dropping VTXO without script ${v.txid}:${v.vout}`
17493
- );
17494
- continue;
17495
- }
17496
- const arr = byScript.get(v.script) ?? [];
17497
- arr.push(v);
17498
- byScript.set(v.script, arr);
17499
- }
17500
- let walletScript;
17501
- try {
17502
- walletScript = scriptFromArkAddress(address);
17503
- } catch {
17504
- walletScript = void 0;
17505
- }
17506
- const cm = await this.readonlyWallet.getContractManager();
17507
- const contracts = await cm.getContracts();
17508
- const addrByScript = new Map(contracts.map((c) => [c.script, c.address]));
17509
- for (const [script, vtxos] of byScript) {
17510
- const filtered = warnAndFilterVtxosForScript(
17511
- vtxos,
17512
- script,
17513
- "WalletMessageHandler.notifyIncomingFunds"
17944
+ this.incomingFundsSubscription = void 0;
17945
+ this.incomingFundsEpoch++;
17946
+ const emitter = this.newEmitter("incomingFunds");
17947
+ const address = await wallet.getAddress();
17948
+ const unsubscribe = await wallet.notifyIncomingFunds(async (funds) => {
17949
+ if (emitter.stale) return;
17950
+ if (funds.type === "vtxo") {
17951
+ const { newVtxos, spentVtxos } = funds;
17952
+ if (newVtxos.length + spentVtxos.length === 0) return;
17953
+ const byScript = /* @__PURE__ */ new Map();
17954
+ for (const v of [...newVtxos, ...spentVtxos]) {
17955
+ if (!v.script) {
17956
+ console.warn(
17957
+ `WalletMessageHandler.notifyIncomingFunds: dropping VTXO without script ${v.txid}:${v.vout}`
17514
17958
  );
17515
- if (filtered.length === 0) continue;
17516
- const targetAddress = script === walletScript ? address : addrByScript.get(script);
17517
- if (!targetAddress) continue;
17518
- if (this.walletRepository) {
17519
- await saveVtxosForContract(
17520
- this.walletRepository,
17521
- { script, address: targetAddress },
17522
- filtered
17523
- );
17524
- }
17959
+ continue;
17525
17960
  }
17526
- this.scheduleForNextTick(
17527
- () => this.tagged({
17528
- type: "VTXO_UPDATE",
17529
- broadcast: true,
17530
- payload: { newVtxos, spentVtxos }
17531
- })
17532
- );
17961
+ const arr = byScript.get(v.script) ?? [];
17962
+ arr.push(v);
17963
+ byScript.set(v.script, arr);
17533
17964
  }
17534
- if (funds.type === "utxo") {
17535
- const utxos = await this.readonlyWallet.getBoardingUtxos();
17536
- this.scheduleForNextTick(
17537
- () => this.tagged({
17538
- type: "UTXO_UPDATE",
17539
- broadcast: true,
17540
- payload: { coins: utxos }
17541
- })
17965
+ let walletScript;
17966
+ try {
17967
+ walletScript = scriptFromArkAddress(address);
17968
+ } catch {
17969
+ walletScript = void 0;
17970
+ }
17971
+ const cm = await wallet.getContractManager();
17972
+ if (emitter.stale) return;
17973
+ const contracts = await cm.getContracts();
17974
+ if (emitter.stale) return;
17975
+ const addrByScript = new Map(contracts.map((c) => [c.script, c.address]));
17976
+ for (const [script, vtxos] of byScript) {
17977
+ const filtered = warnAndFilterVtxosForScript(
17978
+ vtxos,
17979
+ script,
17980
+ "WalletMessageHandler.notifyIncomingFunds"
17542
17981
  );
17982
+ if (filtered.length === 0) continue;
17983
+ const targetAddress = script === walletScript ? address : addrByScript.get(script);
17984
+ if (!targetAddress) continue;
17985
+ if (this.walletRepository) {
17986
+ if (emitter.stale) return;
17987
+ await saveVtxosForContract(
17988
+ this.walletRepository,
17989
+ { script, address: targetAddress },
17990
+ filtered
17991
+ );
17992
+ }
17543
17993
  }
17994
+ emitter.emit(
17995
+ this.tagged({
17996
+ type: "VTXO_UPDATE",
17997
+ broadcast: true,
17998
+ payload: { newVtxos, spentVtxos }
17999
+ })
18000
+ );
17544
18001
  }
17545
- );
18002
+ if (funds.type === "utxo") {
18003
+ if (emitter.stale) return;
18004
+ const utxos = await wallet.getBoardingUtxos();
18005
+ emitter.emit(
18006
+ this.tagged({
18007
+ type: "UTXO_UPDATE",
18008
+ broadcast: true,
18009
+ payload: { coins: utxos }
18010
+ })
18011
+ );
18012
+ }
18013
+ });
18014
+ if (emitter.stale) return unsubscribe();
18015
+ this.incomingFundsSubscription = unsubscribe;
17546
18016
  if (this.wallet) {
17547
18017
  try {
17548
18018
  await this.wallet.getVtxoManager();
@@ -17587,10 +18057,11 @@ var WalletMessageHandler = class {
17587
18057
  await this.refreshCachedData();
17588
18058
  }
17589
18059
  async handleSettle(message) {
18060
+ const emitter = this.newEmitter("request");
17590
18061
  const wallet = this.requireWallet();
17591
18062
  const txid = await wallet.settle(message.payload.params, (e) => {
17592
- this.scheduleForNextTick(
17593
- () => this.tagged({
18063
+ emitter.emit(
18064
+ this.tagged({
17594
18065
  id: message.id,
17595
18066
  type: "SETTLE_EVENT",
17596
18067
  payload: e
@@ -17667,10 +18138,19 @@ var WalletMessageHandler = class {
17667
18138
  throw new WalletNotInitializedError();
17668
18139
  }
17669
18140
  const allVtxos = await this.getVtxosFromRepo();
17670
- const vtxos = allVtxos.filter((v) => !hasTerminalSpend(v));
17671
18141
  const dustAmount = this.readonlyWallet.dustAmount;
18142
+ const withUnrolled = message.payload.filter?.withUnrolled ?? false;
17672
18143
  const includeRecoverable = message.payload.filter?.withRecoverable ?? false;
17673
- const filteredVtxos = includeRecoverable ? vtxos : vtxos.filter((v) => {
18144
+ return allVtxos.filter((v) => {
18145
+ if (v.isUnrolled) {
18146
+ return withUnrolled;
18147
+ }
18148
+ if (hasTerminalSpend(v)) {
18149
+ return false;
18150
+ }
18151
+ if (includeRecoverable) {
18152
+ return true;
18153
+ }
17674
18154
  if (dustAmount != null && isSubdust(v, dustAmount)) {
17675
18155
  return false;
17676
18156
  }
@@ -17682,15 +18162,15 @@ var WalletMessageHandler = class {
17682
18162
  }
17683
18163
  return true;
17684
18164
  });
17685
- if (!message.payload.filter?.withUnrolled) {
17686
- return filteredVtxos;
17687
- }
17688
- return filteredVtxos.concat(allVtxos.filter((v) => hasTerminalSpend(v) && v.isUnrolled));
17689
18165
  }
17690
18166
  /** Tear down handler subscriptions, then delegate the full wipe to the wallet. */
17691
18167
  async clear() {
17692
18168
  const wallet = this.wallet ?? this.readonlyWallet;
17693
18169
  if (!wallet) return;
18170
+ this.requestEpoch++;
18171
+ this.incomingFundsEpoch++;
18172
+ this.contractEventsEpoch++;
18173
+ this.onNextTick = [];
17694
18174
  if (this.incomingFundsSubscription) {
17695
18175
  this.incomingFundsSubscription();
17696
18176
  this.incomingFundsSubscription = void 0;
@@ -17779,13 +18259,17 @@ var WalletMessageHandler = class {
17779
18259
  return buildTransactionHistory(vtxos, boardingTxs, commitmentsToIgnore, resolveTxCreatedAt);
17780
18260
  }
17781
18261
  async ensureContractEventBroadcasting() {
17782
- if (!this.readonlyWallet) return;
18262
+ const wallet = this.readonlyWallet;
18263
+ if (!wallet) return;
17783
18264
  if (this.contractEventsSubscription) return;
18265
+ const gen = this.newGeneration("contractEvents");
17784
18266
  try {
17785
- const manager = await this.readonlyWallet.getContractManager();
18267
+ const manager = await wallet.getContractManager();
18268
+ if (gen.stale || this.contractEventsSubscription) return;
18269
+ const emitter = this.newEmitter("contractEvents");
17786
18270
  this.contractEventsSubscription = manager.onContractEvent((event) => {
17787
- this.scheduleForNextTick(
17788
- () => this.tagged({
18271
+ emitter.emit(
18272
+ this.tagged({
17789
18273
  type: "CONTRACT_EVENT",
17790
18274
  broadcast: true,
17791
18275
  payload: { event }
@@ -17816,6 +18300,9 @@ var DEFAULT_MESSAGE_TIMEOUTS = {
17816
18300
  // Allocation is a local repository write plus a fire-and-forget band
17817
18301
  // slide — no indexer round trip on the request path.
17818
18302
  GET_NEXT_SIGNING_DESCRIPTOR: 1e4,
18303
+ // Same shape as the bare allocation above, plus one contract write per
18304
+ // requested type — still local, still no indexer round trip.
18305
+ GET_NEW_ADDRESSES: 1e4,
17819
18306
  GET_USED_SIGNING_DESCRIPTORS: 2e4,
17820
18307
  ADVANCE_SIGNING_DESCRIPTOR_WATERMARK: 1e4,
17821
18308
  // Medium reads — may involve indexer queries
@@ -18961,6 +19448,35 @@ var ServiceWorkerWallet = class _ServiceWorkerWallet extends ServiceWorkerReadon
18961
19448
  throw new Error(`Failed to allocate next signing descriptor: ${error}`);
18962
19449
  }
18963
19450
  }
19451
+ /**
19452
+ * @see Wallet.getNewAddresses
19453
+ *
19454
+ * Proxied as a single request rather than rebuilt page-side out of
19455
+ * {@link getNextSigningDescriptor}: the worker owns the HD watermark *and*
19456
+ * the contract repository, so splitting the operation would let a page burn
19457
+ * an index and then fail to register the script, leaving an address nothing
19458
+ * watches.
19459
+ *
19460
+ * Note the error shape — `WalletCannotAllocateAddressError` is thrown
19461
+ * worker-side and cannot cross the message boundary as a class, so a
19462
+ * `forceNew` refusal surfaces here as a plain `Error` carrying its message
19463
+ * (and the worker-side error as `cause`). Callers that must branch on the
19464
+ * refusal match the message rather than `instanceof`.
19465
+ */
19466
+ async getNewAddresses(opts) {
19467
+ const message = {
19468
+ id: getRandomId(),
19469
+ tag: this.messageTag,
19470
+ type: "GET_NEW_ADDRESSES",
19471
+ ...opts ? { payload: opts } : {}
19472
+ };
19473
+ try {
19474
+ const response = await this.sendMessage(message);
19475
+ return response.payload.addresses;
19476
+ } catch (error) {
19477
+ throw new Error(`Failed to allocate new addresses: ${error}`, { cause: error });
19478
+ }
19479
+ }
18964
19480
  /** @see HDWalletCapable.getUsedSigningDescriptors */
18965
19481
  async getUsedSigningDescriptors(opts) {
18966
19482
  const message = {
@@ -19927,6 +20443,10 @@ var ElectrumOnchainProvider = class {
19927
20443
  }
19928
20444
  throw new Error("Only 1 or 1P1C package can be broadcast");
19929
20445
  }
20446
+ async getRawTransaction(txid) {
20447
+ const [tx] = await this.chain.fetchTransactions([txid]);
20448
+ return hex.decode(tx.hex);
20449
+ }
19930
20450
  async getTxOutspends(txid) {
19931
20451
  const [txResult] = await this.chain.fetchTransactions([txid]);
19932
20452
  const tx = Transaction$2.fromRaw(hex.decode(txResult.hex), {
@@ -20232,6 +20752,183 @@ function childTxidFromHex(txHex) {
20232
20752
  });
20233
20753
  return tx.id;
20234
20754
  }
20755
+
20756
+ // src/providers/cachingArk.ts
20757
+ var DEFAULT_TTL_MS = 6e4;
20758
+ var CachingArkProvider = class {
20759
+ constructor(inner, ttlMs = DEFAULT_TTL_MS) {
20760
+ this.inner = inner;
20761
+ this.ttlMs = ttlMs;
20762
+ this.unsubscribe = inner.onServerInfoChanged?.((info) => {
20763
+ this.cached = info;
20764
+ this.expiresAt = Date.now() + this.ttlMs;
20765
+ this.generation++;
20766
+ }) ?? (() => {
20767
+ });
20768
+ }
20769
+ inner;
20770
+ ttlMs;
20771
+ cached;
20772
+ expiresAt = 0;
20773
+ inflight;
20774
+ /** Bumped on every event-driven cache write; see {@link getInfo}. */
20775
+ generation = 0;
20776
+ unsubscribe;
20777
+ /** Releases the inner provider's server-info subscription. */
20778
+ dispose() {
20779
+ this.unsubscribe();
20780
+ }
20781
+ get serverUrl() {
20782
+ const url = this.inner.serverUrl;
20783
+ return typeof url === "string" ? url : void 0;
20784
+ }
20785
+ async getInfo() {
20786
+ if (this.cached && Date.now() < this.expiresAt) return this.cached;
20787
+ if (!this.inflight) {
20788
+ const generation = this.generation;
20789
+ this.inflight = this.inner.getInfo().then((info) => {
20790
+ if (this.generation !== generation) return this.cached ?? info;
20791
+ this.cached = info;
20792
+ this.expiresAt = Date.now() + this.ttlMs;
20793
+ return info;
20794
+ }).finally(() => {
20795
+ this.inflight = void 0;
20796
+ });
20797
+ }
20798
+ return this.inflight;
20799
+ }
20800
+ submitTx(signedArkTx, checkpointTxs) {
20801
+ return this.inner.submitTx(signedArkTx, checkpointTxs);
20802
+ }
20803
+ finalizeTx(arkTxid, finalCheckpointTxs) {
20804
+ return this.inner.finalizeTx(arkTxid, finalCheckpointTxs);
20805
+ }
20806
+ registerIntent(intent) {
20807
+ return this.inner.registerIntent(intent);
20808
+ }
20809
+ deleteIntent(intent) {
20810
+ return this.inner.deleteIntent(intent);
20811
+ }
20812
+ confirmRegistration(intentId) {
20813
+ return this.inner.confirmRegistration(intentId);
20814
+ }
20815
+ submitTreeNonces(batchId, pubkey, nonces) {
20816
+ return this.inner.submitTreeNonces(batchId, pubkey, nonces);
20817
+ }
20818
+ submitTreeSignatures(batchId, pubkey, signatures) {
20819
+ return this.inner.submitTreeSignatures(batchId, pubkey, signatures);
20820
+ }
20821
+ submitSignedForfeitTxs(signedForfeitTxs, signedCommitmentTx) {
20822
+ return this.inner.submitSignedForfeitTxs(signedForfeitTxs, signedCommitmentTx);
20823
+ }
20824
+ getEventStream(signal, topics) {
20825
+ return this.inner.getEventStream(signal, topics);
20826
+ }
20827
+ getTransactionsStream(signal) {
20828
+ return this.inner.getTransactionsStream(signal);
20829
+ }
20830
+ getPendingTxs(intent) {
20831
+ return this.inner.getPendingTxs(intent);
20832
+ }
20833
+ onServerInfoChanged(listener) {
20834
+ return this.inner.onServerInfoChanged?.(listener) ?? (() => {
20835
+ });
20836
+ }
20837
+ };
20838
+ var PrevTxUnavailableError = class extends Error {
20839
+ constructor(message, txids = []) {
20840
+ super(message);
20841
+ this.txids = txids;
20842
+ this.name = "PrevTxUnavailableError";
20843
+ }
20844
+ txids;
20845
+ };
20846
+ async function resolvePrevTxs(txids, source, onchain) {
20847
+ const resolved = /* @__PURE__ */ new Map();
20848
+ const wanted = [...new Set(txids)];
20849
+ if (wanted.length === 0) return resolved;
20850
+ try {
20851
+ const { txs } = await source.getVirtualTxs(wanted);
20852
+ for (const psbt of txs) {
20853
+ const tx = Transaction.fromPSBT(base64.decode(psbt));
20854
+ resolved.set(tx.id, tx.toBytes());
20855
+ }
20856
+ } catch (err) {
20857
+ if (!onchain) throw err;
20858
+ }
20859
+ let missing = wanted.filter((txid) => !resolved.has(txid));
20860
+ if (missing.length > 0 && onchain) {
20861
+ const raw = await Promise.all(
20862
+ missing.map(async (txid) => {
20863
+ try {
20864
+ return await onchain.getRawTransaction(txid);
20865
+ } catch {
20866
+ return void 0;
20867
+ }
20868
+ })
20869
+ );
20870
+ for (const [i, bytes] of raw.entries()) {
20871
+ if (bytes) resolved.set(missing[i], bytes);
20872
+ }
20873
+ missing = missing.filter((txid) => !resolved.has(txid));
20874
+ }
20875
+ if (missing.length > 0) {
20876
+ throw new PrevTxUnavailableError(
20877
+ `cannot resolve the previous transaction of ${missing.join(", ")}` + (onchain ? "" : " (no onchain source configured)"),
20878
+ missing
20879
+ );
20880
+ }
20881
+ return resolved;
20882
+ }
20883
+ async function attachPrevArkTxs(tx, txids, source, onchain) {
20884
+ const pending = [];
20885
+ for (let i = 0; i < tx.inputsLength; i++) {
20886
+ if (getArkPsbtFields(tx, i, PrevArkTxField).length === 0) pending.push(i);
20887
+ }
20888
+ if (pending.length === 0) return;
20889
+ const resolved = await resolvePrevTxs(
20890
+ pending.map((i) => txidAt(txids, i)),
20891
+ source,
20892
+ onchain
20893
+ );
20894
+ for (const i of pending) {
20895
+ setArkPsbtField(tx, i, PrevArkTxField, resolved.get(txidAt(txids, i)));
20896
+ }
20897
+ }
20898
+ async function attachPrevoutTxs(tx, source) {
20899
+ const pending = [];
20900
+ for (let i = 0; i < tx.inputsLength; i++) {
20901
+ if (getArkPsbtFields(tx, i, PrevoutTxField).length > 0) continue;
20902
+ const input = tx.getInput(i);
20903
+ if (!input?.txid) throw new PrevTxUnavailableError(`input ${i} has no outpoint`);
20904
+ pending.push({ index: i, txid: hex.encode(input.txid) });
20905
+ }
20906
+ if (pending.length === 0) return;
20907
+ const resolved = /* @__PURE__ */ new Map();
20908
+ for (const txid of new Set(pending.map((p) => p.txid))) {
20909
+ resolved.set(txid, await source.getRawTransaction(txid));
20910
+ }
20911
+ for (const { index, txid } of pending) {
20912
+ setArkPsbtField(tx, index, PrevoutTxField, resolved.get(txid));
20913
+ }
20914
+ }
20915
+ async function withPrevTxs(coins, source, onchain) {
20916
+ const pending = coins.filter((c) => !c.prevTx);
20917
+ if (pending.length === 0) return coins;
20918
+ const resolved = await resolvePrevTxs(
20919
+ pending.map((c) => c.txid),
20920
+ source,
20921
+ onchain
20922
+ );
20923
+ return coins.map((c) => c.prevTx ? c : { ...c, prevTx: resolved.get(c.txid) });
20924
+ }
20925
+ function txidAt(txids, index) {
20926
+ const txid = txids[index];
20927
+ if (txid === void 0) {
20928
+ throw new PrevTxUnavailableError(`no source txid supplied for input ${index}`);
20929
+ }
20930
+ return txid;
20931
+ }
20235
20932
  var TAG_BIP322 = "BIP0322-signed-message";
20236
20933
  var BIP322;
20237
20934
  ((BIP3222) => {
@@ -20609,6 +21306,22 @@ async function resolveUnilateralPath(params) {
20609
21306
  return { selection: best.selection, label: "default:exit" };
20610
21307
  }
20611
21308
 
21309
+ // src/wallet/exitObserver.ts
21310
+ function exitObserverFor(manager) {
21311
+ return (outpoint) => manager.refreshOutpoints([outpoint]);
21312
+ }
21313
+ async function notifyExitObserved(onExitObserved, outpoint) {
21314
+ if (!onExitObserved) return;
21315
+ try {
21316
+ await onExitObserved(outpoint);
21317
+ } catch (e) {
21318
+ console.error(
21319
+ `exit observer failed for ${outpoint.txid}:${outpoint.vout} \u2014 the exit is unaffected`,
21320
+ e
21321
+ );
21322
+ }
21323
+ }
21324
+
20612
21325
  // src/wallet/unroll.ts
20613
21326
  function chainTxTypeToChainedExit2(t) {
20614
21327
  switch (t) {
@@ -20633,27 +21346,30 @@ var Unroll;
20633
21346
  })(Unroll2.StepType || (Unroll2.StepType = {}));
20634
21347
  class Session2 {
20635
21348
  /** Create an unroll session from a virtual output outpoint and its dependency chain. */
20636
- constructor(toUnroll, bumper, explorer, indexer, virtualTxRepository) {
21349
+ constructor(toUnroll, bumper, explorer, indexer, virtualTxRepository, onExitObserved) {
20637
21350
  this.toUnroll = toUnroll;
20638
21351
  this.bumper = bumper;
20639
21352
  this.explorer = explorer;
20640
21353
  this.indexer = indexer;
20641
21354
  this.virtualTxRepository = virtualTxRepository;
21355
+ this.onExitObserved = onExitObserved;
20642
21356
  }
20643
21357
  toUnroll;
20644
21358
  bumper;
20645
21359
  explorer;
20646
21360
  indexer;
20647
21361
  virtualTxRepository;
21362
+ onExitObserved;
20648
21363
  /** Create an unroll session by loading the virtual output chain from the indexer. */
20649
- static async create(toUnroll, bumper, explorer, indexer, virtualTxRepository) {
21364
+ static async create(toUnroll, bumper, explorer, indexer, virtualTxRepository, onExitObserved) {
20650
21365
  const { chain } = await indexer.getVtxoChain(toUnroll);
20651
21366
  return new Session2(
20652
21367
  { ...toUnroll, chain },
20653
21368
  bumper,
20654
21369
  explorer,
20655
21370
  indexer,
20656
- virtualTxRepository
21371
+ virtualTxRepository,
21372
+ onExitObserved
20657
21373
  );
20658
21374
  }
20659
21375
  /**
@@ -20689,6 +21405,14 @@ var Unroll;
20689
21405
  }
20690
21406
  /**
20691
21407
  * Get the next step to be executed
21408
+ *
21409
+ * @remarks
21410
+ * `do()` is the step, not a formality: every effect lives there, and at
21411
+ * `StepType.DONE` that includes firing {@link onExitObserved}. A caller
21412
+ * driving `next()` by hand must always await it — the async iterator
21413
+ * below does. Reading `type` and skipping `do()` leaves the exit
21414
+ * unobserved, and on the other step kinds unbroadcast.
21415
+ *
20692
21416
  * @returns The next step to be executed + the function to execute it
20693
21417
  */
20694
21418
  async next() {
@@ -20717,7 +21441,12 @@ var Unroll;
20717
21441
  return {
20718
21442
  type: 2 /* DONE */,
20719
21443
  vtxoTxid: this.toUnroll.txid,
20720
- do: () => Promise.resolve()
21444
+ // The observation rides on `do` rather than the iterator so
21445
+ // a caller driving `next()` by hand fires it too.
21446
+ do: () => notifyExitObserved(this.onExitObserved, {
21447
+ txid: this.toUnroll.txid,
21448
+ vout: this.toUnroll.vout
21449
+ })
20721
21450
  };
20722
21451
  }
20723
21452
  const virtualTxBase64 = await this.resolveVirtualTxBase64(nextTxToBroadcast);
@@ -20751,9 +21480,22 @@ var Unroll;
20751
21480
  }
20752
21481
  }
20753
21482
  Unroll2.Session = Session2;
21483
+ async function sessionFor(wallet, toUnroll, bumper) {
21484
+ const manager = await wallet.getContractManager();
21485
+ return Session2.create(
21486
+ toUnroll,
21487
+ bumper,
21488
+ wallet.onchainProvider,
21489
+ wallet.indexerProvider,
21490
+ wallet.virtualTxRepository,
21491
+ exitObserverFor(manager)
21492
+ );
21493
+ }
21494
+ Unroll2.sessionFor = sessionFor;
20754
21495
  async function completeUnroll(wallet, vtxoTxids, outputAddress) {
20755
21496
  const signedTx = await prepareUnrollTransaction(wallet, vtxoTxids, outputAddress);
20756
21497
  await wallet.onchainProvider.broadcastTransaction(signedTx.hex);
21498
+ await refreshSpentExitOutputs(wallet, signedTx);
20757
21499
  return signedTx.id;
20758
21500
  }
20759
21501
  Unroll2.completeUnroll = completeUnroll;
@@ -20769,9 +21511,9 @@ async function prepareUnrollTransaction(wallet, vtxoTxIds, outputAddress) {
20769
21511
  let totalAmount = 0n;
20770
21512
  const txWeightEstimator = TxWeightEstimator.create();
20771
21513
  for (const vtxo of vtxos) {
20772
- if (!vtxo.isUnrolled) {
21514
+ if (!canSweepOnchain(vtxo)) {
20773
21515
  throw new Error(
20774
- `Vtxo ${vtxo.txid}:${vtxo.vout} is not fully unrolled, use unroll first`
21516
+ `Vtxo ${vtxo.txid}:${vtxo.vout} cannot be swept onchain: it is not fully unrolled (use unroll first), or its exit output was already spent`
20775
21517
  );
20776
21518
  }
20777
21519
  const txStatus = await wallet.onchainProvider.getTxStatus(vtxo.txid);
@@ -20832,6 +21574,22 @@ async function prepareUnrollTransaction(wallet, vtxoTxIds, outputAddress) {
20832
21574
  signedTx.finalize();
20833
21575
  return signedTx;
20834
21576
  }
21577
+ async function refreshSpentExitOutputs(wallet, signedTx) {
21578
+ const outpoints = [];
21579
+ for (let i = 0; i < signedTx.inputsLength; i++) {
21580
+ const input = signedTx.getInput(i);
21581
+ if (input.txid && input.index !== void 0) {
21582
+ outpoints.push({ txid: hex.encode(input.txid), vout: input.index });
21583
+ }
21584
+ }
21585
+ if (outpoints.length === 0) return;
21586
+ try {
21587
+ const manager = await wallet.getContractManager();
21588
+ await manager.refreshOutpoints(outpoints);
21589
+ } catch (e) {
21590
+ console.error("completeUnroll: failed to refresh the spent exit outputs", e);
21591
+ }
21592
+ }
20835
21593
  function sleep2(ms) {
20836
21594
  return new Promise((resolve) => setTimeout(resolve, ms));
20837
21595
  }
@@ -20910,7 +21668,7 @@ async function buildSignedSweep(params) {
20910
21668
  }
20911
21669
 
20912
21670
  // src/wallet/exit/estimate.ts
20913
- var CHILD_OUTPUT_DUST = 546;
21671
+ var CHILD_OUTPUT_DUST = CHILD_DUST_AMOUNT;
20914
21672
  function stepFundingAmount(stepFee) {
20915
21673
  return stepFee + CHILD_OUTPUT_DUST;
20916
21674
  }
@@ -21071,10 +21829,18 @@ async function computeExitLayout(opts, feeRate) {
21071
21829
  // splitter. funded: + 1 splitter tx and pre-signed children.
21072
21830
  txCount: (graph ? 0 : steps.length > 0 ? 1 : 0) + steps.length * 2 + sweeps.length,
21073
21831
  totalFeeSats: splitterFee + stepFees + sweepFees,
21074
- // graph funding is what the executor sends to its own fee address:
21075
- // just the CPFP fees (change recycles); funded also locks the
21076
- // per-child dust into the splitter.
21077
- fundingRequiredSats: graph ? stepFees : splitterFee + fundingTotal,
21832
+ // graph funding is what the executor sends to its own fee address: the
21833
+ // CPFP fees, plus one dust floor. Change recycles through that single
21834
+ // address, so each bump hands its remainder to the next and only the
21835
+ // *final* change has to clear `CHILD_DUST_AMOUNT` — but it does have to,
21836
+ // and `buildAnchorChild` throws rather than absorbing a short remainder
21837
+ // into the fee. Quoting bare fees quotes an amount that strands the last
21838
+ // bump and every sweep behind it.
21839
+ //
21840
+ // funded reserves the same floor per step instead of once overall (see
21841
+ // `stepFundingAmount`), because it pre-funds each child separately
21842
+ // through the splitter rather than recycling one output.
21843
+ fundingRequiredSats: graph ? steps.length > 0 ? stepFees + CHILD_OUTPUT_DUST : 0 : splitterFee + fundingTotal,
21078
21844
  recoveredSats: sweeps.reduce((s, x) => s + (x.vtxo.value - x.sweepFee), 0)
21079
21845
  };
21080
21846
  const expiries = pendingNodes.map((n) => n.expiresAt).filter((x) => x !== void 0);
@@ -21134,12 +21900,32 @@ var Executor = class {
21134
21900
  this.pollIntervalMs = opts?.pollIntervalMs ?? 5e3;
21135
21901
  this.feeWallet = opts?.feeWallet;
21136
21902
  this.signal = opts?.signal;
21903
+ this.onExitObserved = opts?.onExitObserved;
21137
21904
  }
21138
21905
  pkg;
21139
21906
  provider;
21140
21907
  pollIntervalMs;
21141
21908
  feeWallet;
21142
21909
  signal;
21910
+ onExitObserved;
21911
+ /**
21912
+ * Per-VTXO branch progress, which the executor otherwise does not track:
21913
+ * `dead` / `done` are per-step, and a VTXO is only onchain once EVERY
21914
+ * `package`/`bump` step naming it has confirmed. (`broadcast` — the funding
21915
+ * splitter — carries no `forVtxos` and serves no single VTXO.)
21916
+ */
21917
+ branchSteps() {
21918
+ const byVtxo = /* @__PURE__ */ new Map();
21919
+ this.pkg.steps.forEach((step, i) => {
21920
+ if (step.kind !== "package" && step.kind !== "bump") return;
21921
+ for (const vtxo of step.forVtxos) {
21922
+ let steps = byVtxo.get(vtxo);
21923
+ if (!steps) byVtxo.set(vtxo, steps = /* @__PURE__ */ new Set());
21924
+ steps.add(i);
21925
+ }
21926
+ });
21927
+ return byVtxo;
21928
+ }
21143
21929
  /**
21144
21930
  * Poll delay. Nearly all wall-clock time is spent here, so it must be the
21145
21931
  * thing that reacts to abort — checking only between polls would leave
@@ -21179,6 +21965,19 @@ var Executor = class {
21179
21965
  async *[Symbol.asyncIterator]() {
21180
21966
  throwIfAborted(this.signal);
21181
21967
  const dead = /* @__PURE__ */ new Set();
21968
+ const branchSteps = this.branchSteps();
21969
+ const onchainSteps = /* @__PURE__ */ new Set();
21970
+ const branchObserved = /* @__PURE__ */ new Set();
21971
+ const observeBranch = async (stepIndex, forVtxos) => {
21972
+ onchainSteps.add(stepIndex);
21973
+ for (const vtxo of forVtxos ?? []) {
21974
+ if (branchObserved.has(vtxo)) continue;
21975
+ const steps = branchSteps.get(vtxo);
21976
+ if (!steps || ![...steps].every((i) => onchainSteps.has(i))) continue;
21977
+ branchObserved.add(vtxo);
21978
+ await this.observe(vtxo);
21979
+ }
21980
+ };
21182
21981
  if (this.pkg.validUntil && Date.now() / 1e3 > this.pkg.validUntil) {
21183
21982
  yield {
21184
21983
  stepIndex: -1,
@@ -21208,6 +22007,7 @@ var Executor = class {
21208
22007
  const anchorTxid = step.kind === "package" || step.kind === "bump" ? step.parentTxid : step.txid;
21209
22008
  const existing = await this.status(anchorTxid);
21210
22009
  if (existing?.confirmed) {
22010
+ await observeBranch(i, forVtxos);
21211
22011
  yield {
21212
22012
  stepIndex: i,
21213
22013
  kind: step.kind,
@@ -21263,6 +22063,7 @@ var Executor = class {
21263
22063
  }
21264
22064
  }
21265
22065
  await this.waitConfirmed(anchorTxid);
22066
+ await observeBranch(i, forVtxos);
21266
22067
  yield {
21267
22068
  stepIndex: i,
21268
22069
  kind: step.kind,
@@ -21293,6 +22094,7 @@ var Executor = class {
21293
22094
  const swept = await this.status(step.txid);
21294
22095
  if (swept?.confirmed) {
21295
22096
  done.add(index);
22097
+ await this.observe(step.vtxo);
21296
22098
  yield {
21297
22099
  stepIndex: index,
21298
22100
  kind: "sweep",
@@ -21349,7 +22151,25 @@ var Executor = class {
21349
22151
  if (done.size < pending.length) await this.sleep();
21350
22152
  }
21351
22153
  }
22154
+ /** `"txid:vout"` -> outpoint, then hand it to the hook. A malformed entry is skipped. */
22155
+ async observe(outpoint) {
22156
+ if (!this.onExitObserved) return;
22157
+ const sep = outpoint.lastIndexOf(":");
22158
+ const rest = outpoint.slice(sep + 1);
22159
+ const vout = Number(rest);
22160
+ if (sep <= 0 || rest === "" || !Number.isInteger(vout)) return;
22161
+ await notifyExitObserved(this.onExitObserved, { txid: outpoint.slice(0, sep), vout });
22162
+ }
21352
22163
  };
22164
+
22165
+ // src/wallet/exit/execute.ts
22166
+ async function execute(wallet, pkg, opts) {
22167
+ const manager = await wallet.getContractManager();
22168
+ return new Executor(pkg, opts?.provider ?? wallet.onchainProvider, {
22169
+ ...opts,
22170
+ onExitObserved: opts?.onExitObserved ?? exitObserverFor(manager)
22171
+ });
22172
+ }
21353
22173
  async function prepare(opts) {
21354
22174
  const { wallet, onchainWallet } = opts;
21355
22175
  const feeRate = await resolveFeeRate(opts);
@@ -21424,7 +22244,10 @@ async function prepare(opts) {
21424
22244
  totals: {
21425
22245
  txCount: bumpSteps.length * 2 + sweepSteps.length,
21426
22246
  totalFeeSats: stepFees2 + sweepFees2,
21427
- fundingRequiredSats: stepFees2,
22247
+ // Fees plus the single dust floor the recycling change output
22248
+ // must keep clear; see the note in `estimate.ts`. Must match
22249
+ // the quote `estimate` gives for the same package.
22250
+ fundingRequiredSats: bumpSteps.length > 0 ? stepFees2 + CHILD_OUTPUT_DUST : 0,
21428
22251
  recoveredSats: recovered2
21429
22252
  },
21430
22253
  vtxos: layout.infos,
@@ -21593,6 +22416,7 @@ function isValidStep(step) {
21593
22416
  var UnilateralExit = {
21594
22417
  estimate,
21595
22418
  prepare,
22419
+ execute,
21596
22420
  Executor
21597
22421
  };
21598
22422
 
@@ -22018,7 +22842,9 @@ var ArkadeContract = class _ArkadeContract {
22018
22842
  const [registered] = await manager.getContracts({ script: scriptHex });
22019
22843
  if (registered) {
22020
22844
  const [withVtxos] = await manager.getContractsWithVtxos({ script: scriptHex });
22021
- return (withVtxos?.vtxos ?? []).filter((v) => !hasTerminalSpend(v));
22845
+ return (withVtxos?.vtxos ?? []).filter(
22846
+ (v) => !hasTerminalSpend(v) && !v.isUnrolled
22847
+ );
22022
22848
  }
22023
22849
  }
22024
22850
  if (!this.client.indexer) {
@@ -22128,6 +22954,19 @@ var ArkadeTransactionBuilder = class {
22128
22954
  if (coin.sourceTx) {
22129
22955
  setArkPsbtField(arkTx, 0, PrevArkTxField, coin.sourceTx);
22130
22956
  }
22957
+ if (this.fn.arkadeScript) {
22958
+ const indexer = this.contract.client.indexer;
22959
+ if (!indexer) {
22960
+ throw new PrevTxUnavailableError(
22961
+ "covenant spends require an `indexer` on the Arkade client to resolve the previous ark tx of each input"
22962
+ );
22963
+ }
22964
+ await attachPrevArkTxs(
22965
+ arkTx,
22966
+ [coin.txid, ...this.fundingCoins.map((c) => c.txid)],
22967
+ indexer
22968
+ );
22969
+ }
22131
22970
  const condition = (def.tapscript.witness ?? []).map((w) => this.witnessBytes(w));
22132
22971
  if (condition.length > 0) {
22133
22972
  setArkPsbtField(arkTx, 0, ConditionWitness, condition);
@@ -22633,6 +23472,6 @@ function createDefaultPaymentRouter(wallet) {
22633
23472
  (*! noble-hashes - MIT License (c) 2022 Paul Miller (paulmillr.com) *)
22634
23473
  */
22635
23474
 
22636
- export { ARKADE_MAGIC, ARKADE_SALTED_PREIMAGE_TAG, ARKADE_SWAP_PREIMAGE_TAG, ActivityRegistry, AmbiguousRouteError, ArkNote, ArkadeCash, ArkadeCashCreateError, AssetManager, BIP21, BIP21Error, BIP322, BLOCKED_UPGRADE_TIMEOUT_MS, Batch, ChainedTxType, ConnectionDisposedError, ContractManager, ContractRepositoryImpl, ContractWatcher, DB_VERSION, DEFAULT_MESSAGE_TIMEOUTS, DEFAULT_MIN_BATCH_EXPIRY_SECONDS, DEFAULT_MIN_CHECKPOINT_EXIT_DELAY_SECONDS, DatabaseUpgradeBlockedError, DelegateManagerImpl, DelegateNotConfiguredError, DelegatorManagerImpl, DelegatorNotConfiguredError, DescriptorIdentity, DescriptorSigningProviderMissingError, DustChangeError, ELECTRUM_TCP_HOST, ELECTRUM_WS_URL, ESPLORA_URL, ElectrumOnchainProvider, EmulatorPacket, EsploraProvider, Estimator, Extension, ExtensionNotFoundError, ForeignDescriptorError, HDDescriptorProvider, INTENT_TERMINAL_STATES, InMemoryContractRepository, InMemoryIntentRepository, InMemoryVirtualTxRepository, InMemoryWalletRepository, IndexedDBContractRepository, IndexedDBIntentRepository, IndexedDBVirtualTxRepository, IndexedDBWalletRepository, MAX_USED_SIGNING_DESCRIPTORS_LOOK_AHEAD, MESSAGE_BUS_INITIALIZING, MESSAGE_BUS_NOT_INITIALIZED, MIGRATION_KEY, MUTINYNET_MIN_CHECKPOINT_EXIT_DELAY_SECONDS, MessageBus, MessageBusInitializingError, MessageBusNotInitializedError, MissingSigningDescriptorError, MnemonicIdentity, OnchainWallet, P2A, PaymentRouter, REGTEST_MIN_BATCH_EXPIRY_SECONDS, REGTEST_MIN_CHECKPOINT_EXIT_DELAY_SECONDS, Ramps, ReadonlyAssetManager, ReadonlyDescriptorIdentity, ReadonlySingleKey, ReadonlyWallet, ReadonlyWalletError, RestDelegateProvider, RestDelegatorProvider, RestEmulatorProvider, SIGNET_MIN_CHECKPOINT_EXIT_DELAY_SECONDS, SeedIdentity, ServiceWorkerReadonlyWallet, ServiceWorkerTimeoutError, ServiceWorkerWallet, SingleKey, TxTree, TxType, TxWeightEstimator, UnannotatableInputError, UnilateralExit, UnknownPacket, Unroll, VtxoManager, Wallet2 as Wallet, WalletCannotSignError, WalletMessageHandler, WalletNotInitializedError, WalletRepositoryImpl, WsElectrumChainSource, adoptContractDescriptor, arkRail, arkTarget, arkade_exports, assertFinalCommitmentMatchesValidated, assertIntentIdUnique, assertSendableAmount, assertSubmittedArkTxid, assertValidBatchExpiry, assertValidServerUnrollScript, assetMintResolver, awaitTransaction, boardingResolver, btcTarget, buildForfeitTx, buildOffchainTx, buildPreimageMessage, buildSaltedPreimageMessage, claimWithPreimageIdentity, classifyAgainstSignerSet, classifyContractSigner, closeDatabase, collabExitResolver, combineTapscriptSigs, contractFromArkContract, contractFromArkContractWithAddress, contractPreimage, contractSigner, createAssetPacket, createDefaultActivityRegistry, createDefaultPaymentRouter, createExitChainResolver, createManagedConnection, decodeArkContract, defaultBatchExpiryPolicy, defaultCheckpointExitDelayPolicy, deleteByIndex, deserializeAssets, deserializeExitPackage, deserializeUtxo, deserializeVtxo, encodeArkContract, extendVirtualCoinForContract, gateExclusion, gatedContracts, getAllByIndexValues, getMigrationStatus, getRandomId, hasBoardingTxExpired, hasCandidates, intentMatchesFilter, intentPageBounds, invoiceTarget, isArkContract, isBatchSignable, isBtcAddress, isContractGenericallySpendable, isCooperativelyMigratable, isDeterministicSigner, isDiscoverable, isHDAllocationCapable, isHDDeterministicSignCapable, isHDWalletCapable, isLightningInvoice, isLnurl, isMessageBusInitializingError, isMessageBusNotInitializedError, isPerArtifactDescriptor, isSigningIdentity, isSubdust, isTerminalIntentState, isValidArkAddress, isVtxoExpiringSoon, isVtxoForScript, isWatchedContract, logExcludedVtxos, makeHandle, matchServerCheckpoints, mergeChainedTxType, migrateWalletRepository, onchainRail, openDatabase, outpointExclusion, outpointReasons, promisifyRequest, provisionClaimSecret, provisionRefundKey, requiresMigration, resolveBatchExpiryPolicy, resolveCheckpointExitDelayPolicy, resolveDescriptorSigner, resolveSendAmount, rollbackMigration, saveVtxosForContract, scriptFromArkAddress, selectCoinsWithAsset, selectVirtualCoins, serializeAssets, serializeExitPackage, serializeUtxo, serializeVtxo, setupServiceWorker, signAndSubmitOffchainTx, signerSetFromInfo, signingDescriptorIndex, strictSigningDescriptorIndex, toXOnlySignerHex, tryResolveSendAmount, validateBatchRecipients, validateConnectorsTxGraph, validateVtxoTxGraph, verifyTapscriptSignatures, waitForIncomingFunds, warnAndFilterVtxosForScript };
22637
- //# sourceMappingURL=chunk-DSC3G6QZ.js.map
22638
- //# sourceMappingURL=chunk-DSC3G6QZ.js.map
23475
+ export { ARKADE_MAGIC, ARKADE_SALTED_PREIMAGE_TAG, ARKADE_SWAP_PREIMAGE_TAG, ActivityRegistry, AmbiguousRouteError, ArkNote, ArkadeCash, ArkadeCashCreateError, AssetManager, BIP21, BIP21Error, BIP322, BLOCKED_UPGRADE_TIMEOUT_MS, Batch, CachingArkProvider, ChainedTxType, ConnectionDisposedError, ContractManager, ContractRepositoryImpl, ContractWatcher, DB_VERSION, DEFAULT_MESSAGE_TIMEOUTS, DEFAULT_MIN_BATCH_EXPIRY_SECONDS, DEFAULT_MIN_CHECKPOINT_EXIT_DELAY_SECONDS, DatabaseUpgradeBlockedError, DelegateManagerImpl, DelegateNotConfiguredError, DelegatorManagerImpl, DelegatorNotConfiguredError, DescriptorIdentity, DescriptorSigningProviderMissingError, DustChangeError, ELECTRUM_TCP_HOST, ELECTRUM_WS_URL, ESPLORA_URL, ElectrumOnchainProvider, EmulatorPacket, EsploraProvider, Estimator, Extension, ExtensionNotFoundError, ForeignDescriptorError, HDDescriptorProvider, INTENT_TERMINAL_STATES, InMemoryContractRepository, InMemoryIntentRepository, InMemoryVirtualTxRepository, InMemoryWalletRepository, IndexedDBContractRepository, IndexedDBIntentRepository, IndexedDBVirtualTxRepository, IndexedDBWalletRepository, MAX_USED_SIGNING_DESCRIPTORS_LOOK_AHEAD, MESSAGE_BUS_INITIALIZING, MESSAGE_BUS_NOT_INITIALIZED, MIGRATION_KEY, MUTINYNET_MIN_CHECKPOINT_EXIT_DELAY_SECONDS, MessageBus, MessageBusInitializingError, MessageBusNotInitializedError, MissingSigningDescriptorError, MnemonicIdentity, OnchainWallet, P2A, PaymentRouter, PrevTxUnavailableError, REGTEST_MIN_BATCH_EXPIRY_SECONDS, REGTEST_MIN_CHECKPOINT_EXIT_DELAY_SECONDS, Ramps, ReadonlyAssetManager, ReadonlyDescriptorIdentity, ReadonlySingleKey, ReadonlyWallet, ReadonlyWalletError, RestDelegateProvider, RestDelegatorProvider, RestEmulatorProvider, SIGNET_MIN_CHECKPOINT_EXIT_DELAY_SECONDS, SeedIdentity, ServiceWorkerReadonlyWallet, ServiceWorkerTimeoutError, ServiceWorkerWallet, SingleKey, TxTree, TxType, TxWeightEstimator, UnannotatableInputError, UnilateralExit, UnknownPacket, Unroll, VtxoManager, Wallet2 as Wallet, WalletCannotAllocateAddressError, WalletCannotSignError, WalletMessageHandler, WalletNotInitializedError, WalletRepositoryImpl, WsElectrumChainSource, adoptContractDescriptor, arkRail, arkTarget, arkade_exports, assertFinalCommitmentMatchesValidated, assertIntentIdUnique, assertSendableAmount, assertSubmittedArkTxid, assertValidBatchExpiry, assertValidServerUnrollScript, assetMintResolver, attachPrevArkTxs, attachPrevoutTxs, awaitTransaction, boardingResolver, btcTarget, buildForfeitTx, buildOffchainTx, buildPreimageMessage, buildSaltedPreimageMessage, claimWithPreimageIdentity, classifyAgainstSignerSet, classifyContractSigner, closeDatabase, collabExitResolver, combineTapscriptSigs, contractFromArkContract, contractFromArkContractWithAddress, contractPreimage, contractSigner, createAssetPacket, createDefaultActivityRegistry, createDefaultPaymentRouter, createExitChainResolver, createManagedConnection, decodeArkContract, defaultBatchExpiryPolicy, defaultCheckpointExitDelayPolicy, deleteByIndex, deserializeAssets, deserializeExitPackage, deserializeUtxo, deserializeVtxo, encodeArkContract, exitObserverFor, extendVirtualCoinForContract, extractArkProviderUrl, gateExclusion, gatedContracts, getAllByIndexValues, getMigrationStatus, getRandomId, hasBoardingTxExpired, hasCandidates, intentMatchesFilter, intentPageBounds, invoiceTarget, isAddressAllocationCapable, isArkContract, isBatchSignable, isBtcAddress, isContractGenericallySpendable, isCooperativelyMigratable, isDeterministicSigner, isDiscoverable, isHDAllocationCapable, isHDDeterministicSignCapable, isHDWalletCapable, isLightningInvoice, isLnurl, isMessageBusInitializingError, isMessageBusNotInitializedError, isPerArtifactDescriptor, isSigningIdentity, isSubdust, isTerminalIntentState, isValidArkAddress, isVtxoExpiringSoon, isVtxoForScript, isWatchedContract, logExcludedVtxos, makeHandle, matchServerCheckpoints, mergeChainedTxType, migrateWalletRepository, notifyExitObserved, onchainRail, openDatabase, outpointExclusion, outpointReasons, promisifyRequest, provisionClaimSecret, provisionRefundKey, requiresMigration, resolveBatchExpiryPolicy, resolveCheckpointExitDelayPolicy, resolveDescriptorSigner, resolvePrevTxs, resolveSendAmount, rollbackMigration, saveVtxosForContract, scriptFromArkAddress, selectCoinsWithAsset, selectVirtualCoins, serializeAssets, serializeExitPackage, serializeUtxo, serializeVtxo, setupServiceWorker, signAndSubmitOffchainTx, signerSetFromInfo, signingDescriptorIndex, strictSigningDescriptorIndex, toXOnlySignerHex, tryResolveSendAmount, validateBatchRecipients, validateConnectorsTxGraph, validateVtxoTxGraph, verifyTapscriptSignatures, waitForIncomingFunds, warnAndFilterVtxosForScript, withPrevTxs };
23476
+ //# sourceMappingURL=chunk-AEWJU6NZ.js.map
23477
+ //# sourceMappingURL=chunk-AEWJU6NZ.js.map