@arkade-os/sdk 0.5.0 → 0.5.2

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 (72) hide show
  1. package/README.md +10 -6
  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/{asyncStorageTaskQueue-qRRQUOE8.d.cts → asyncStorageTaskQueue-BtEc3Qca.d.cts} +2 -2
  7. package/dist/{asyncStorageTaskQueue-BFv2O1y1.d.ts → asyncStorageTaskQueue-DIn7bTqN.d.ts} +2 -2
  8. package/dist/{chunk-AU77RYOC.cjs → chunk-2OCYL6BJ.cjs} +1177 -864
  9. package/dist/chunk-2OCYL6BJ.cjs.map +1 -0
  10. package/dist/{chunk-K2E6X6JT.js → chunk-2QRUQXLS.js} +4 -4
  11. package/dist/{chunk-K2E6X6JT.js.map → chunk-2QRUQXLS.js.map} +1 -1
  12. package/dist/{chunk-IQWLFYT2.cjs → chunk-64R7CRKN.cjs} +18 -18
  13. package/dist/{chunk-IQWLFYT2.cjs.map → chunk-64R7CRKN.cjs.map} +1 -1
  14. package/dist/{chunk-GWJWXVUB.js → chunk-AJWXVQYG.js} +692 -379
  15. package/dist/chunk-AJWXVQYG.js.map +1 -0
  16. package/dist/{chunk-UUSKJOSX.cjs → chunk-GGKR4O5X.cjs} +3 -3
  17. package/dist/{chunk-UUSKJOSX.cjs.map → chunk-GGKR4O5X.cjs.map} +1 -1
  18. package/dist/{chunk-ZOEB53NQ.js → chunk-HKG26NUT.js} +3 -3
  19. package/dist/{chunk-ZOEB53NQ.js.map → chunk-HKG26NUT.js.map} +1 -1
  20. package/dist/{chunk-K352GMVL.js → chunk-LWRMSC4M.js} +3 -3
  21. package/dist/{chunk-K352GMVL.js.map → chunk-LWRMSC4M.js.map} +1 -1
  22. package/dist/{chunk-TZSKLERU.js → chunk-MOBDJ7U2.js} +4 -4
  23. package/dist/{chunk-TZSKLERU.js.map → chunk-MOBDJ7U2.js.map} +1 -1
  24. package/dist/{chunk-GN2K4WBP.cjs → chunk-OM6FJ76H.cjs} +48 -48
  25. package/dist/{chunk-GN2K4WBP.cjs.map → chunk-OM6FJ76H.cjs.map} +1 -1
  26. package/dist/{chunk-GOM26K6H.cjs → chunk-PVFQFQBY.cjs} +10 -10
  27. package/dist/{chunk-GOM26K6H.cjs.map → chunk-PVFQFQBY.cjs.map} +1 -1
  28. package/dist/{chunk-JT5YVMCJ.cjs → chunk-TAIWH3HG.cjs} +16 -16
  29. package/dist/{chunk-JT5YVMCJ.cjs.map → chunk-TAIWH3HG.cjs.map} +1 -1
  30. package/dist/{chunk-HABGVRLF.js → chunk-U5XG7SWA.js} +3 -3
  31. package/dist/{chunk-HABGVRLF.js.map → chunk-U5XG7SWA.js.map} +1 -1
  32. package/dist/contracts/handlers/index.cjs +9 -9
  33. package/dist/contracts/handlers/index.d.cts +3 -3
  34. package/dist/contracts/handlers/index.d.ts +3 -3
  35. package/dist/contracts/handlers/index.js +2 -2
  36. package/dist/{delegate-B5BmYkEY.d.cts → delegate-CaL8EN4Q.d.cts} +1 -1
  37. package/dist/{delegate-DNbiUgy8.d.ts → delegate-Jd4zs88Q.d.ts} +1 -1
  38. package/dist/{index-B4QuO9sH.d.cts → index-BwdGzNSn.d.cts} +2 -2
  39. package/dist/{index-CYeyT3kn.d.ts → index-D-xqVzf8.d.ts} +2 -2
  40. package/dist/{index-CZw-MMfY.d.cts → index-DdKxfC29.d.cts} +35 -17
  41. package/dist/{index-CZw-MMfY.d.ts → index-DdKxfC29.d.ts} +35 -17
  42. package/dist/index.cjs +313 -313
  43. package/dist/index.d.cts +100 -26
  44. package/dist/index.d.ts +100 -26
  45. package/dist/index.js +4 -4
  46. package/dist/repositories/realm/index.cjs +35 -35
  47. package/dist/repositories/realm/index.d.cts +2 -2
  48. package/dist/repositories/realm/index.d.ts +2 -2
  49. package/dist/repositories/realm/index.js +5 -5
  50. package/dist/repositories/sqlite/index.cjs +33 -33
  51. package/dist/repositories/sqlite/index.d.cts +2 -2
  52. package/dist/repositories/sqlite/index.d.ts +2 -2
  53. package/dist/repositories/sqlite/index.js +5 -5
  54. package/dist/{taskRunner-B3SLvUfL.d.cts → taskRunner-67YSEpsz.d.cts} +2 -2
  55. package/dist/{taskRunner-wm1bhmtP.d.ts → taskRunner-ZyGGjtIq.d.ts} +2 -2
  56. package/dist/wallet/expo/background.cjs +14 -14
  57. package/dist/wallet/expo/background.d.cts +3 -3
  58. package/dist/wallet/expo/background.d.ts +3 -3
  59. package/dist/wallet/expo/background.js +6 -6
  60. package/dist/wallet/expo/index.cjs +15 -15
  61. package/dist/wallet/expo/index.d.cts +4 -4
  62. package/dist/wallet/expo/index.d.ts +4 -4
  63. package/dist/wallet/expo/index.js +5 -5
  64. package/dist/{wallet-DGLXuwXT.d.ts → wallet-B8LW7cV1.d.cts} +7 -6
  65. package/dist/{wallet-DCZ2-Bea.d.cts → wallet-bFS7VybL.d.ts} +7 -6
  66. package/dist/worker/expo/index.cjs +9 -9
  67. package/dist/worker/expo/index.d.cts +4 -4
  68. package/dist/worker/expo/index.d.ts +4 -4
  69. package/dist/worker/expo/index.js +5 -5
  70. package/package.json +1 -1
  71. package/dist/chunk-AU77RYOC.cjs.map +0 -1
  72. package/dist/chunk-GWJWXVUB.js.map +0 -1
@@ -1,6 +1,6 @@
1
- import { AssetId, AssetGroup, AssetInput, AssetOutput, Metadata, Packet, RestIndexerProvider, BufferReader, BufferWriter, AssetRef } from './chunk-HABGVRLF.js';
2
- import { witnessRefToBytes, validateProgram, toASM, stringifyArtifact, serializeArkadeContractParams, resolveAsm, parseArtifact, getOpcodeValue, getOpcodeName, fromASM, deserializeArkadeContractParams, computeArkadeScriptPublicKey, bytesToASM, asmToBytes, arkadeWitnessHash, arkadeScriptHash, UnsupportedProgramVersionError, SUPPORTED_PROGRAM_VERSION, OPCODE_VALUES, OPCODE_NAMES, bignum_exports, ArkadeScript, ArkadeProgramScript, ARKADE_OPS, ARKADE_OPCODE_VALUES, ARKADE_OPCODE_NAMES, ARKADE_OPCODES, ARKADE_OP, toXOnly, ARKADE_TYPE_ENCODINGS, validateParamValue, contractHandlers, speculativeReceiveMetadata, isMainnetDescriptor, descriptorIsOurs, deriveDescriptorLeafPubKey, deriveDescriptorLeafCompressedPubKey, normalizeToDescriptor, extractPubKey, parseHDDescriptor, identityDescriptor, DefaultVtxo, DelegateVtxo, WALLET_RECEIVE_SOURCE, BoardingContractHandler } from './chunk-K352GMVL.js';
3
- import { VtxoScript, __export, Transaction, requiresForfeit, CSVMultisigTapscript, networks, getNetwork, DEFAULT_NETWORK, resolveEmulatorPubkey, timelockToSequence, isVtxoSpent, getNormalizedVtxos, setArkPsbtField, PrevArkTxField, ConditionWitness, craftToSpendTx, RestArkProvider, ArkAddress, isVirtualCoin, getArkPsbtFields, CosignerPublicKey, ServerResponseMismatchError, assertNonNegativeMs, isRetryableProviderError, getAllNormalizedVtxos, DEFAULT_PAGE_SIZE, OP_RETURN_EMPTY_PKSCRIPT, canSweepOnchain, sequenceToTimelock, DEFAULT_ARKADE_SERVER_URL, baseFetch, DEFAULT_NETWORK_NAME, sequenceToCanonicalTimelock, assertTimerMs, isEventSourceError, isEventSourceUnavailableError, normalizeVtxo, isPastExpiry, decodeTapscript, scriptFromTapLeafScript, CLTVMultisigTapscript, VtxoTaprootTree, isArkError, ArkErrorName, assertAllowedSighashTypes, Intent, rateGate, formatSighash, getSequence, maybeArkError, toOffchainInputFeeParams, canRecoverOnchain, MultisigTapscript, DEFAULT_ARKADE_EMULATOR_URL, networkFromArkadeInfo, ArkError, toBIP371TapTree, resolveTimeHeight, canSpendOffchain, INFO_FETCH_TIMEOUT_MS, PrevoutTxField, fetchVtxoCreatedAtByTxid, EXPIRY_MIN_PLAUSIBLE_MS, ProviderUnavailableError } from './chunk-ZOEB53NQ.js';
1
+ import { Packet, AssetId, AssetGroup, AssetInput, AssetOutput, Metadata, RestIndexerProvider, BufferReader, BufferWriter, AssetRef } from './chunk-U5XG7SWA.js';
2
+ import { witnessRefToBytes, validateProgram, toASM, stringifyArtifact, serializeArkadeContractParams, resolveAsm, parseArtifact, getOpcodeValue, getOpcodeName, fromASM, deserializeArkadeContractParams, computeArkadeScriptPublicKey, bytesToASM, asmToBytes, arkadeWitnessHash, arkadeScriptHash, UnsupportedProgramVersionError, SUPPORTED_PROGRAM_VERSION, OPCODE_VALUES, OPCODE_NAMES, bignum_exports, ArkadeScript, ArkadeProgramScript, ARKADE_OPS, ARKADE_OPCODE_VALUES, ARKADE_OPCODE_NAMES, ARKADE_OPCODES, ARKADE_OP, toXOnly, ARKADE_TYPE_ENCODINGS, validateParamValue, contractHandlers, speculativeReceiveMetadata, isMainnetDescriptor, descriptorIsOurs, deriveDescriptorLeafPubKey, deriveDescriptorLeafCompressedPubKey, normalizeToDescriptor, extractPubKey, parseHDDescriptor, identityDescriptor, DefaultVtxo, DelegateVtxo, WALLET_RECEIVE_SOURCE, BoardingContractHandler } from './chunk-LWRMSC4M.js';
3
+ import { VtxoScript, __export, Transaction, requiresForfeit, CSVMultisigTapscript, networks, getNetwork, DEFAULT_NETWORK, resolveEmulatorPubkey, timelockToSequence, isVtxoSpent, getNormalizedVtxos, setArkPsbtField, PrevArkTxField, ConditionWitness, getArkPsbtFields, craftToSpendTx, RestArkProvider, ArkAddress, isVirtualCoin, CosignerPublicKey, ServerResponseMismatchError, assertNonNegativeMs, isRetryableProviderError, getAllNormalizedVtxos, DEFAULT_PAGE_SIZE, OP_RETURN_EMPTY_PKSCRIPT, canSweepOnchain, sequenceToTimelock, DEFAULT_ARKADE_SERVER_URL, baseFetch, DEFAULT_NETWORK_NAME, sequenceToCanonicalTimelock, assertTimerMs, isEventSourceError, isEventSourceUnavailableError, normalizeVtxo, isPastExpiry, decodeTapscript, scriptFromTapLeafScript, CLTVMultisigTapscript, VtxoTaprootTree, isArkError, ArkErrorName, assertAllowedSighashTypes, Intent, rateGate, formatSighash, getSequence, maybeArkError, toOffchainInputFeeParams, canRecoverOnchain, MultisigTapscript, DEFAULT_ARKADE_EMULATOR_URL, networkFromArkadeInfo, ArkError, toBIP371TapTree, resolveTimeHeight, canSpendOffchain, INFO_FETCH_TIMEOUT_MS, PrevoutTxField, fetchVtxoCreatedAtByTxid, EXPIRY_MIN_PLAUSIBLE_MS, ProviderUnavailableError } from './chunk-HKG26NUT.js';
4
4
  import { sha256, equalBytes, hash160, sha256x2, concatBytes, randomPrivateKeyBytes, pubECDSA, pubSchnorr } from '@scure/btc-signer/utils.js';
5
5
  import { SigHash, Script, OP, OutScript, Address, RawWitness, p2tr, p2wpkh, TaprootControlBlock, DEFAULT_SEQUENCE, Transaction as Transaction$1 } from '@scure/btc-signer';
6
6
  import { hex, base58, base64, bech32m } from '@scure/base';
@@ -1328,10 +1328,15 @@ async function provisionDescriptor(wallet) {
1328
1328
  return allocated ?? await identityDescriptor(wallet.identity);
1329
1329
  }
1330
1330
  async function provisionRefundKey(wallet) {
1331
- const descriptor = await identityDescriptor(wallet.identity);
1331
+ const descriptorPromise = identityDescriptor(wallet.identity);
1332
+ const identityPubkeyPromise = wallet.identity.xOnlyPublicKey();
1333
+ void identityPubkeyPromise.catch(() => void 0);
1334
+ const descriptor = await descriptorPromise;
1332
1335
  const signer = await contractSigner(wallet, descriptor);
1333
- const pubkey = await signer.xOnlyPublicKey();
1334
- const identityPubkey = await wallet.identity.xOnlyPublicKey();
1336
+ const [pubkey, identityPubkey] = await Promise.all([
1337
+ signer.xOnlyPublicKey(),
1338
+ identityPubkeyPromise
1339
+ ]);
1335
1340
  if (!equalBytes(pubkey, identityPubkey)) {
1336
1341
  throw new Error(
1337
1342
  "provisionRefundKey: descriptor pubkey does not match wallet identity \u2014 the refund key and the refund address would be on different keys"
@@ -1347,7 +1352,6 @@ async function provisionClaimSecret(wallet, opts = {}) {
1347
1352
  }
1348
1353
  const descriptor = await provisionDescriptor(wallet);
1349
1354
  const signer = await contractSigner(wallet, descriptor);
1350
- const pubkey = await signer.xOnlyPublicKey();
1351
1355
  const claim2 = async () => {
1352
1356
  if (opts.preimage) return { preimage: opts.preimage, mustPersistPreimage: true };
1353
1357
  if (isPerArtifactDescriptor(descriptor)) {
@@ -1370,7 +1374,10 @@ async function provisionClaimSecret(wallet, opts = {}) {
1370
1374
  return { preimage: randomBytes(32), mustPersistPreimage: true };
1371
1375
  }
1372
1376
  };
1373
- const { preimage, preimageSalt, mustPersistPreimage } = await claim2();
1377
+ const [pubkey, { preimage, preimageSalt, mustPersistPreimage }] = await Promise.all([
1378
+ signer.xOnlyPublicKey(),
1379
+ claim2()
1380
+ ]);
1374
1381
  return {
1375
1382
  descriptor,
1376
1383
  pubkey,
@@ -4676,12 +4683,14 @@ var VtxoManager = class _VtxoManager {
4676
4683
  * ```
4677
4684
  */
4678
4685
  async recoverVtxos(eventCallback) {
4679
- const allVtxos = await this.wallet.getVtxos({
4680
- withRecoverable: true,
4681
- withUnrolled: false
4682
- });
4686
+ const [allVtxos, now] = await Promise.all([
4687
+ this.wallet.getVtxos({
4688
+ withRecoverable: true,
4689
+ withUnrolled: false
4690
+ }),
4691
+ fetchTimeHeight(this.wallet)
4692
+ ]);
4683
4693
  const dustAmount = getDustAmount(this.wallet);
4684
- const now = await fetchTimeHeight(this.wallet);
4685
4694
  let { vtxosToRecover } = getRecoverableWithSubdust(allVtxos, dustAmount, now);
4686
4695
  if (vtxosToRecover.length === 0) {
4687
4696
  throw new Error("No recoverable VTXOs found");
@@ -4782,12 +4791,17 @@ var VtxoManager = class _VtxoManager {
4782
4791
  * ```
4783
4792
  */
4784
4793
  async getRecoverableBalance() {
4785
- const allVtxos = await this.wallet.getVtxos({
4786
- withRecoverable: true,
4787
- withUnrolled: false
4788
- });
4794
+ const infoProvider = this.getInfoProvider();
4795
+ const [allVtxos, now, info, arkAddress] = await Promise.all([
4796
+ this.wallet.getVtxos({
4797
+ withRecoverable: true,
4798
+ withUnrolled: false
4799
+ }),
4800
+ fetchTimeHeight(this.wallet),
4801
+ infoProvider?.getInfo(),
4802
+ this.wallet.getAddress()
4803
+ ]);
4789
4804
  const dustAmount = getDustAmount(this.wallet);
4790
- const now = await fetchTimeHeight(this.wallet);
4791
4805
  let { vtxosToRecover, includesSubdust } = getRecoverableWithSubdust(
4792
4806
  allVtxos,
4793
4807
  dustAmount,
@@ -4805,14 +4819,9 @@ var VtxoManager = class _VtxoManager {
4805
4819
  now
4806
4820
  ));
4807
4821
  }
4808
- const info = await this.getInfoProvider()?.getInfo();
4809
4822
  const estimator = new Estimator(info?.fees.intentFee ?? {});
4810
4823
  const { payable, net } = priceSettlementInputs(vtxosToRecover, estimator);
4811
- const priced = deductOffchainOutputFee(
4812
- subtotalOf(payable, net),
4813
- estimator,
4814
- await this.wallet.getAddress()
4815
- );
4824
+ const priced = deductOffchainOutputFee(subtotalOf(payable, net), estimator, arkAddress);
4816
4825
  const recoverable = priced > 0n ? priced : 0n;
4817
4826
  const subdustAmount = subtotalOf(
4818
4827
  payable.filter((v) => BigInt(v.value) < dustAmount),
@@ -4866,8 +4875,11 @@ var VtxoManager = class _VtxoManager {
4866
4875
  const cm = await wallet.getContractManager();
4867
4876
  const recoveryScripts = (await cm.getContracts()).filter(isRecoveryOnlyContract).map((contract) => contract.script);
4868
4877
  if (recoveryScripts.length === 0) return;
4869
- const recoveryContracts = await cm.getContractsWithVtxos({ script: recoveryScripts });
4870
- const info = await this.getInfoProvider()?.getInfo();
4878
+ const infoProvider = this.getInfoProvider();
4879
+ const [recoveryContracts, info] = await Promise.all([
4880
+ cm.getContractsWithVtxos({ script: recoveryScripts }),
4881
+ infoProvider?.getInfo()
4882
+ ]);
4871
4883
  const dustAmount = getDustAmount(wallet);
4872
4884
  for (const { contract, vtxos } of recoveryContracts) {
4873
4885
  try {
@@ -4962,12 +4974,13 @@ var VtxoManager = class _VtxoManager {
4962
4974
  if (this.settlementConfig === false && thresholdMs === void 0) {
4963
4975
  return [];
4964
4976
  }
4965
- const vtxos = await this.wallet.getSpendableVtxos({
4977
+ const threshold = thresholdMs !== void 0 ? thresholdMs : this.configuredThresholdMs();
4978
+ const vtxosPromise = this.wallet.getSpendableVtxos({
4966
4979
  withRecoverable: true,
4967
4980
  genericallySpendableOnly: true
4968
4981
  });
4969
- const threshold = thresholdMs !== void 0 ? thresholdMs : this.configuredThresholdMs();
4970
4982
  const tip = now ?? await fetchTimeHeight(this.wallet);
4983
+ const vtxos = await vtxosPromise;
4971
4984
  await this.warnHeldExpiring(threshold, tip);
4972
4985
  return getExpiringAndRecoverableVtxos(vtxos, threshold, getDustAmount(this.wallet), tip);
4973
4986
  }
@@ -5261,8 +5274,10 @@ var VtxoManager = class _VtxoManager {
5261
5274
  const wallet = this.requireMigrationCapableWallet();
5262
5275
  const info = await wallet.arkProvider.getInfo();
5263
5276
  await wallet.settleServerInfoChanges?.();
5264
- const { reports: vtxoReports } = await this.classifyDeprecatedSignerContracts(info);
5265
- const { reports: boardingReports } = await this.classifyDeprecatedSignerBoarding(info);
5277
+ const [{ reports: vtxoReports }, { reports: boardingReports }] = await Promise.all([
5278
+ this.classifyDeprecatedSignerContracts(info),
5279
+ this.classifyDeprecatedSignerBoarding(info)
5280
+ ]);
5266
5281
  return mergeSignerReports(vtxoReports, boardingReports);
5267
5282
  }
5268
5283
  /**
@@ -5284,8 +5299,10 @@ var VtxoManager = class _VtxoManager {
5284
5299
  return { rotated: rotatedOnRefresh, expired: [], signers: [] };
5285
5300
  }
5286
5301
  if (walletClass.status === "UNKNOWN_SIGNER") {
5287
- const { reports: vtxoReports2 } = await this.classifyDeprecatedSignerContracts(info);
5288
- const { reports: boardingReports2 } = await this.classifyDeprecatedSignerBoarding(info);
5302
+ const [{ reports: vtxoReports2 }, { reports: boardingReports2 }] = await Promise.all([
5303
+ this.classifyDeprecatedSignerContracts(info),
5304
+ this.classifyDeprecatedSignerBoarding(info)
5305
+ ]);
5289
5306
  return {
5290
5307
  // True only if a second rotation landed inside the drain, leaving us on a signer
5291
5308
  // newer than `info` (a drained rotation otherwise classifies CURRENT).
@@ -5296,16 +5313,13 @@ var VtxoManager = class _VtxoManager {
5296
5313
  };
5297
5314
  }
5298
5315
  const rotated = await this.ensureReceiveOnActiveSigner(info) || rotatedOnRefresh;
5299
- const {
5300
- reports: vtxoReports,
5301
- migratable: vtxoMigratable,
5302
- expired: vtxoExpired
5303
- } = await this.classifyDeprecatedSignerContracts(info);
5304
- const {
5305
- reports: boardingReports,
5306
- migratable: boardingMigratable,
5307
- expired: boardingExpired
5308
- } = await this.classifyDeprecatedSignerBoarding(info);
5316
+ const [
5317
+ { reports: vtxoReports, migratable: vtxoMigratable, expired: vtxoExpired },
5318
+ { reports: boardingReports, migratable: boardingMigratable, expired: boardingExpired }
5319
+ ] = await Promise.all([
5320
+ this.classifyDeprecatedSignerContracts(info),
5321
+ this.classifyDeprecatedSignerBoarding(info)
5322
+ ]);
5309
5323
  const reports = mergeSignerReports(vtxoReports, boardingReports);
5310
5324
  const expiredRefs = [
5311
5325
  ...vtxoExpired.map(classifiedToRef),
@@ -6304,11 +6318,6 @@ async function classifyIntent(intent, vtxosByOutpoint, now) {
6304
6318
  cancellationReason: "reconciliation: an input was unilaterally exited"
6305
6319
  });
6306
6320
  }
6307
- if (intent.state === "waiting_to_submit") {
6308
- return terminal(intent, "cancelled", now, {
6309
- cancellationReason: "reconciliation: intent was never submitted before restart"
6310
- });
6311
- }
6312
6321
  if (intent.validUntil !== void 0 && intent.validUntil < now) {
6313
6322
  return terminal(intent, "cancelled", now, {
6314
6323
  cancellationReason: "reconciliation: intent expired"
@@ -6510,9 +6519,16 @@ function chainTxTypeToChainedExit(t) {
6510
6519
  var isCommitment = (t) => t === "INDEXER_CHAINED_TX_TYPE_COMMITMENT" /* COMMITMENT */ || t === "INDEXER_CHAINED_TX_TYPE_UNSPECIFIED" /* UNSPECIFIED */;
6511
6520
  async function buildExitDag(params) {
6512
6521
  const byTxid = /* @__PURE__ */ new Map();
6513
- for (const vtxo of params.vtxos) {
6514
- const outpoint = `${vtxo.txid}:${vtxo.vout}`;
6515
- const chain = await params.chain.getVtxoChain({ txid: vtxo.txid, vout: vtxo.vout });
6522
+ const chains = await Promise.allSettled(
6523
+ params.vtxos.map(async (vtxo) => ({
6524
+ outpoint: `${vtxo.txid}:${vtxo.vout}`,
6525
+ // The resolver returns the full chain (all pages merged, local-first).
6526
+ chain: await params.chain.getVtxoChain({ txid: vtxo.txid, vout: vtxo.vout })
6527
+ }))
6528
+ );
6529
+ for (const result of chains) {
6530
+ if (result.status === "rejected") throw result.reason;
6531
+ const { outpoint, chain } = result.value;
6516
6532
  for (const chainTx of chain) {
6517
6533
  const existing = byTxid.get(chainTx.txid);
6518
6534
  if (existing) {
@@ -6545,25 +6561,26 @@ async function buildExitDag(params) {
6545
6561
  if (order.length !== pending.size) {
6546
6562
  throw new Error("inconsistent vtxo chain from indexer (cycle detected)");
6547
6563
  }
6548
- const nodes = [];
6549
- for (const tx of order) {
6550
- let confirmed = false;
6551
- try {
6552
- const status = await params.onchain.getTxStatus(tx.txid);
6553
- confirmed = status.confirmed;
6554
- } catch {
6555
- confirmed = false;
6556
- }
6564
+ const confirmed = await Promise.all(
6565
+ order.map(async (tx) => {
6566
+ try {
6567
+ const status = await params.onchain.getTxStatus(tx.txid);
6568
+ return status.confirmed;
6569
+ } catch {
6570
+ return false;
6571
+ }
6572
+ })
6573
+ );
6574
+ return order.map((tx, i) => {
6557
6575
  const expires = Number(tx.expiresAt);
6558
- nodes.push({
6576
+ return {
6559
6577
  txid: tx.txid,
6560
6578
  type: tx.type,
6561
6579
  expiresAt: Number.isFinite(expires) && expires > 0 ? expires : void 0,
6562
6580
  forVtxos: [...tx.forVtxos].sort(),
6563
- confirmed
6564
- });
6565
- }
6566
- return nodes;
6581
+ confirmed: confirmed[i]
6582
+ };
6583
+ });
6567
6584
  }
6568
6585
  function topoSortByDeps(items, idOf, depsOf) {
6569
6586
  const produced = new Set(items.map(idOf));
@@ -6732,11 +6749,12 @@ var RepositoryExitDataSource = class {
6732
6749
  return chain;
6733
6750
  }
6734
6751
  async getVirtualTxs(txids) {
6752
+ const stored = await Promise.all(txids.map((txid) => this.repo.getVirtualTx(txid)));
6735
6753
  const out = /* @__PURE__ */ new Map();
6736
- for (const txid of txids) {
6737
- const stored = await this.repo.getVirtualTx(txid);
6738
- if (stored?.psbt) out.set(txid, stored.psbt);
6739
- }
6754
+ txids.forEach((txid, i) => {
6755
+ const row = stored[i];
6756
+ if (row?.psbt) out.set(txid, row.psbt);
6757
+ });
6740
6758
  return out;
6741
6759
  }
6742
6760
  };
@@ -7854,8 +7872,10 @@ var DelegateManagerImpl = class {
7854
7872
  return { delegated: [], failed: [] };
7855
7873
  }
7856
7874
  const destinationScript = ArkAddress.decode(destination).pkScript;
7857
- const arkInfo = await this.arkInfoProvider.getInfo();
7858
- const delegateInfo = await this.delegateProvider.getDelegateInfo();
7875
+ const [arkInfo, delegateInfo] = await Promise.all([
7876
+ this.arkInfoProvider.getInfo(),
7877
+ this.delegateProvider.getDelegateInfo()
7878
+ ]);
7859
7879
  const eligible = vtxos.map(normalizeVtxo).filter(
7860
7880
  (v) => isAnnotated(v) && findDelegateTapLeaf(v, delegateInfo.pubkey) !== void 0
7861
7881
  );
@@ -7947,23 +7967,16 @@ async function delegate(identity, delegateProvider, arkInfo, delegateInfo, vtxos
7947
7967
  if (!expiryTimestamp || expiryTimestamp === Number.MAX_SAFE_INTEGER) {
7948
7968
  delegateAt = new Date(Date.now() + 1 * 60 * 1e3);
7949
7969
  } else {
7950
- delegateAt = defaultDelegateAt(expiryTimestamp, arkInfo.sessionDuration);
7970
+ const nowMs = now.timestamp.getTime();
7971
+ const target = expiryTimestamp - Number(arkInfo.sessionDuration * 3000n);
7972
+ delegateAt = new Date(
7973
+ target > nowMs ? target : expiryTimestamp > nowMs ? nowMs + 2e3 : nowMs + 6e4
7974
+ );
7951
7975
  }
7952
7976
  }
7953
7977
  const { fees, dust, forfeitAddress, network } = arkInfo;
7954
7978
  const delegateAtSeconds = delegateAt.getTime() / 1e3;
7955
- const estimator = new Estimator({
7956
- ...fees.intentFee,
7957
- // replace now() function with the delegateAt timestamp
7958
- offchainInput: fees.intentFee.offchainInput?.replace(
7959
- "now()",
7960
- `double(${delegateAtSeconds})`
7961
- ),
7962
- offchainOutput: fees.intentFee.offchainOutput?.replace(
7963
- "now()",
7964
- `double(${delegateAtSeconds})`
7965
- )
7966
- });
7979
+ const estimator = estimatorAt(fees.intentFee, delegateAt);
7967
7980
  let amount = 0n;
7968
7981
  for (const coin of vtxos) {
7969
7982
  const inputFee = estimator.evalOffchainInput({
@@ -8107,14 +8120,6 @@ async function makeSignedDelegateIntent(identity, coins, outputs, onchainOutputs
8107
8120
  message
8108
8121
  };
8109
8122
  }
8110
- function defaultDelegateAt(expiryTimestamp, sessionDurationSeconds, now = Date.now()) {
8111
- const remainingTimeMs = expiryTimestamp - now;
8112
- if (remainingTimeMs <= 0) return new Date(now + 6e4);
8113
- const sessionLeadMs = Number(sessionDurationSeconds * 1000n);
8114
- const leadTimeMs = Math.max(remainingTimeMs * 0.1, sessionLeadMs);
8115
- const delegateAt = expiryTimestamp - leadTimeMs;
8116
- return new Date(delegateAt > now ? delegateAt : now + 2e3);
8117
- }
8118
8123
  function findDestinationOutputIndex(outputs, destinationScript) {
8119
8124
  return outputs.findIndex((o) => o.script && equalBytes(o.script, destinationScript));
8120
8125
  }
@@ -8136,6 +8141,14 @@ function findDelegateTapLeaf(vtxo, delegatePubkey) {
8136
8141
  function isAnnotated(v) {
8137
8142
  return v.tapTree !== void 0 && v.forfeitTapLeafScript !== void 0 && v.intentTapLeafScript !== void 0;
8138
8143
  }
8144
+ function estimatorAt(intentFee, at) {
8145
+ const now = `double(${at.getTime() / 1e3})`;
8146
+ return new Estimator({
8147
+ ...intentFee,
8148
+ offchainInput: intentFee.offchainInput?.replace("now()", now),
8149
+ offchainOutput: intentFee.offchainOutput?.replace("now()", now)
8150
+ });
8151
+ }
8139
8152
 
8140
8153
  // src/repositories/inMemory/intentRepository.ts
8141
8154
  var InMemoryIntentRepository = class {
@@ -10653,8 +10666,10 @@ var ContractManager = class _ContractManager {
10653
10666
  return contract !== void 0 && contractHandlers.get(contract.type)?.assertSpendableNow !== void 0;
10654
10667
  });
10655
10668
  if (asking.length === 0) return refused;
10656
- const tip = await this.currentChainTip();
10657
- const walletDescriptorValue = await walletDescriptor?.();
10669
+ const [tip, walletDescriptorValue] = await Promise.all([
10670
+ this.currentChainTip(),
10671
+ walletDescriptor?.()
10672
+ ]);
10658
10673
  for (const vtxo of asking) {
10659
10674
  const contract = byScript.get(vtxo.script);
10660
10675
  const handler = contractHandlers.get(contract.type);
@@ -11527,7 +11542,9 @@ async function setupServiceWorker(pathOrOptions) {
11527
11542
  }
11528
11543
  const { path, activationTimeoutMs } = normalizeOptions2(pathOrOptions);
11529
11544
  const registration = await navigator.serviceWorker.register(path);
11530
- await registration.update();
11545
+ registration.update().catch(
11546
+ (error) => console.warn("Service worker update failed; continuing with the active worker", error)
11547
+ );
11531
11548
  const serviceWorker = registration.active || registration.waiting || registration.installing;
11532
11549
  if (!serviceWorker) {
11533
11550
  throw new Error("Failed to get service worker instance");
@@ -12553,7 +12570,10 @@ var WalletReceiveRotator = class _WalletReceiveRotator {
12553
12570
  * watched; it just stops being advertised). The untagged index-0 baseline is never retired.
12554
12571
  */
12555
12572
  async rotate(wallet) {
12556
- const descriptor = await this.provider.getNextSigningDescriptor();
12573
+ const descriptorPromise = this.provider.getNextSigningDescriptor();
12574
+ const managerPromise = wallet.getContractManager();
12575
+ void managerPromise.catch(() => void 0);
12576
+ const descriptor = await descriptorPromise;
12557
12577
  const { tapscript: newTapscript, params } = buildReceiveContract(
12558
12578
  wallet.offchainTapscript,
12559
12579
  descriptor,
@@ -12561,7 +12581,7 @@ var WalletReceiveRotator = class _WalletReceiveRotator {
12561
12581
  true
12562
12582
  );
12563
12583
  const newScript = params.script;
12564
- const manager = await wallet.getContractManager();
12584
+ const manager = await managerPromise;
12565
12585
  await manager.createContract(params);
12566
12586
  wallet.setOffchainTapscriptForRotation(newTapscript);
12567
12587
  const previousTagged = this.currentTaggedScript;
@@ -12671,11 +12691,11 @@ function computeOffchainBalance(vtxos, caps) {
12671
12691
  let settled = 0;
12672
12692
  let preconfirmed = 0;
12673
12693
  let available = 0;
12674
- let gated = 0;
12694
+ let contractLocked = 0;
12675
12695
  let pendingSettlement = 0;
12676
12696
  let reserved = 0;
12677
12697
  let recoverable = 0;
12678
- let pendingRecovery = 0;
12698
+ let deprecatedSignerLocked = 0;
12679
12699
  let unrolled = 0;
12680
12700
  const owned = /* @__PURE__ */ new Map();
12681
12701
  const spendable = /* @__PURE__ */ new Map();
@@ -12692,7 +12712,7 @@ function computeOffchainBalance(vtxos, caps) {
12692
12712
  continue;
12693
12713
  }
12694
12714
  if (isPendingRecovery(vtxo)) {
12695
- pendingRecovery += vtxo.value;
12715
+ deprecatedSignerLocked += vtxo.value;
12696
12716
  continue;
12697
12717
  }
12698
12718
  if (canRecoverOnchain(vtxo, now)) {
@@ -12706,7 +12726,7 @@ function computeOffchainBalance(vtxos, caps) {
12706
12726
  settled += vtxo.value;
12707
12727
  }
12708
12728
  if (!isGenericallySpendable(vtxo)) {
12709
- gated += vtxo.value;
12729
+ contractLocked += vtxo.value;
12710
12730
  } else if (!isUnlocked(vtxo)) {
12711
12731
  pendingSettlement += vtxo.value;
12712
12732
  } else if (isReserved?.(vtxo)) {
@@ -12724,13 +12744,13 @@ function computeOffchainBalance(vtxos, caps) {
12724
12744
  settled,
12725
12745
  preconfirmed,
12726
12746
  available,
12727
- gated,
12747
+ contractLocked,
12728
12748
  pendingSettlement,
12729
12749
  reserved,
12730
12750
  recoverable,
12731
- pendingRecovery,
12751
+ deprecatedSignerLocked,
12732
12752
  unrolled,
12733
- total: settled + preconfirmed + recoverable + pendingRecovery + unrolled,
12753
+ total: settled + preconfirmed + recoverable + deprecatedSignerLocked + unrolled,
12734
12754
  assets: toAssets(owned),
12735
12755
  availableAssets: toAssets(spendable)
12736
12756
  };
@@ -12755,15 +12775,19 @@ function toWalletBalance(boardingUtxos, offchain) {
12755
12775
  settled: offchain.settled,
12756
12776
  preconfirmed: offchain.preconfirmed,
12757
12777
  available: offchain.available,
12758
- gated: offchain.gated,
12778
+ contractLocked: offchain.contractLocked,
12759
12779
  pendingSettlement: offchain.pendingSettlement,
12760
12780
  reserved: offchain.reserved,
12761
12781
  recoverable: offchain.recoverable,
12762
- pendingRecovery: offchain.pendingRecovery,
12782
+ deprecatedSignerLocked: offchain.deprecatedSignerLocked,
12763
12783
  unrolled: offchain.unrolled,
12764
12784
  total: totalBoarding + offchain.total,
12765
12785
  assets: offchain.assets,
12766
- availableAssets: offchain.availableAssets
12786
+ availableAssets: offchain.availableAssets,
12787
+ // Deprecated aliases, so code written against older releases still reads a value.
12788
+ gated: offchain.contractLocked,
12789
+ intentLocked: offchain.pendingSettlement,
12790
+ pendingRecovery: offchain.deprecatedSignerLocked
12767
12791
  };
12768
12792
  }
12769
12793
 
@@ -13811,11 +13835,42 @@ var ReadonlyWallet = class _ReadonlyWallet {
13811
13835
  const utxos = [];
13812
13836
  const commitmentsToIgnore = /* @__PURE__ */ new Set();
13813
13837
  const tapscripts = await this.getBoardingTapscripts(this.watchedBoardingSigners());
13814
- const outspendCache = /* @__PURE__ */ new Map();
13815
- for (const tapscript of tapscripts) {
13816
- const boardingAddress = tapscript.onchainAddress(this.network);
13817
- const scriptHex = hex.encode(tapscript.pkScript);
13818
- const txs = await this.onchainProvider.getTransactions(boardingAddress);
13838
+ const getOne = this.onchainProvider.getTxOutspend?.bind(this.onchainProvider);
13839
+ const listInflight = /* @__PURE__ */ new Map();
13840
+ const oneInflight = /* @__PURE__ */ new Map();
13841
+ const outspendOf = (txid, index) => {
13842
+ if (getOne) {
13843
+ const key = `${txid}:${index}`;
13844
+ let pending2 = oneInflight.get(key);
13845
+ if (!pending2) {
13846
+ pending2 = getOne(txid, index);
13847
+ oneInflight.set(key, pending2);
13848
+ }
13849
+ return pending2;
13850
+ }
13851
+ let pending = listInflight.get(txid);
13852
+ if (!pending) {
13853
+ pending = this.onchainProvider.getTxOutspends(txid);
13854
+ listInflight.set(txid, pending);
13855
+ }
13856
+ return pending.then((statuses) => statuses[index]);
13857
+ };
13858
+ const historyResults = await Promise.allSettled(
13859
+ tapscripts.map(async (tapscript) => {
13860
+ const boardingAddress = tapscript.onchainAddress(this.network);
13861
+ return {
13862
+ boardingAddress,
13863
+ scriptHex: hex.encode(tapscript.pkScript),
13864
+ txs: await this.onchainProvider.getTransactions(boardingAddress)
13865
+ };
13866
+ })
13867
+ );
13868
+ const histories = historyResults.map((result) => {
13869
+ if (result.status === "rejected") throw result.reason;
13870
+ return result.value;
13871
+ });
13872
+ const outputs = [];
13873
+ for (const { boardingAddress, scriptHex, txs } of histories) {
13819
13874
  const commitmentByOutpoint = /* @__PURE__ */ new Map();
13820
13875
  for (const tx of txs) {
13821
13876
  for (const input of tx.vin ?? []) {
@@ -13825,42 +13880,51 @@ var ReadonlyWallet = class _ReadonlyWallet {
13825
13880
  for (const tx of txs) {
13826
13881
  for (let i = 0; i < tx.vout.length; i++) {
13827
13882
  const vout = tx.vout[i];
13828
- if (vout.scriptpubkey_address === boardingAddress) {
13829
- let spentStatuses = outspendCache.get(tx.txid);
13830
- if (!spentStatuses) {
13831
- spentStatuses = await this.onchainProvider.getTxOutspends(tx.txid);
13832
- outspendCache.set(tx.txid, spentStatuses);
13833
- }
13834
- const spentStatus = spentStatuses[i];
13835
- const commitmentTxid = spentStatus?.txid || commitmentByOutpoint.get(`${tx.txid}:${i}`);
13836
- const spent = Boolean(spentStatus?.spent) || commitmentTxid !== void 0;
13837
- if (spent && commitmentTxid) {
13838
- commitmentsToIgnore.add(commitmentTxid);
13839
- }
13840
- const boardingFacts = {
13841
- isSpent: spent,
13842
- isSwept: false,
13843
- isPreconfirmed: false,
13844
- commitmentTxIds: spent && commitmentTxid ? [commitmentTxid] : []
13845
- };
13846
- utxos.push({
13847
- txid: tx.txid,
13848
- vout: i,
13849
- value: Number(vout.value),
13850
- status: {
13851
- confirmed: tx.status.confirmed,
13852
- block_time: tx.status.block_time
13853
- },
13854
- isUnrolled: true,
13855
- ...boardingFacts,
13856
- spentBy: "",
13857
- createdAt: tx.status.confirmed ? new Date(tx.status.block_time * 1e3) : /* @__PURE__ */ new Date(0),
13858
- script: scriptHex
13859
- });
13860
- }
13883
+ if (vout.scriptpubkey_address !== boardingAddress) continue;
13884
+ outputs.push({
13885
+ txid: tx.txid,
13886
+ vout: i,
13887
+ value: Number(vout.value),
13888
+ confirmed: tx.status.confirmed,
13889
+ blockTime: tx.status.block_time,
13890
+ scriptHex,
13891
+ commitmentFromVin: commitmentByOutpoint.get(`${tx.txid}:${i}`),
13892
+ spentStatus: outspendOf(tx.txid, i)
13893
+ });
13861
13894
  }
13862
13895
  }
13863
13896
  }
13897
+ const spentResults = await Promise.allSettled(outputs.map((output) => output.spentStatus));
13898
+ for (const [i, output] of outputs.entries()) {
13899
+ const spentResult = spentResults[i];
13900
+ if (spentResult.status === "rejected") throw spentResult.reason;
13901
+ const spentStatus = spentResult.value;
13902
+ const commitmentTxid = spentStatus?.txid || output.commitmentFromVin;
13903
+ const spent = Boolean(spentStatus?.spent) || commitmentTxid !== void 0;
13904
+ if (spent && commitmentTxid) {
13905
+ commitmentsToIgnore.add(commitmentTxid);
13906
+ }
13907
+ const boardingFacts = {
13908
+ isSpent: spent,
13909
+ isSwept: false,
13910
+ isPreconfirmed: false,
13911
+ commitmentTxIds: spent && commitmentTxid ? [commitmentTxid] : []
13912
+ };
13913
+ utxos.push({
13914
+ txid: output.txid,
13915
+ vout: output.vout,
13916
+ value: output.value,
13917
+ status: {
13918
+ confirmed: output.confirmed,
13919
+ block_time: output.blockTime
13920
+ },
13921
+ isUnrolled: true,
13922
+ ...boardingFacts,
13923
+ spentBy: "",
13924
+ createdAt: output.confirmed ? new Date(output.blockTime * 1e3) : /* @__PURE__ */ new Date(0),
13925
+ script: output.scriptHex
13926
+ });
13927
+ }
13864
13928
  const unconfirmedTxs = [];
13865
13929
  const confirmedTxs = [];
13866
13930
  for (const utxo of utxos) {
@@ -13952,9 +14016,12 @@ var ReadonlyWallet = class _ReadonlyWallet {
13952
14016
  await this.walletRepository.saveUtxos(
13953
14017
  new Map(groups.map((group, i) => [addresses[i], group.coins]))
13954
14018
  );
14019
+ const cachedByAddress = await Promise.all(
14020
+ addresses.map((address) => collectUtxos(this.walletRepository, address))
14021
+ );
13955
14022
  for (const [i, group] of groups.entries()) {
13956
14023
  const fresh = new Set(group.coins.map((coin) => `${coin.txid}:${coin.vout}`));
13957
- const cached = await collectUtxos(this.walletRepository, addresses[i]);
14024
+ const cached = cachedByAddress[i];
13958
14025
  const kept = cached.filter((coin) => fresh.has(`${coin.txid}:${coin.vout}`));
13959
14026
  if (kept.length === cached.length) continue;
13960
14027
  await this.walletRepository.deleteUtxos(addresses[i]);
@@ -13977,14 +14044,20 @@ var ReadonlyWallet = class _ReadonlyWallet {
13977
14044
  return groups.flatMap((g) => g.coins);
13978
14045
  }
13979
14046
  /**
13980
- * @internal {@link getBoardingUtxos} minus those an in-flight settle is spending, for SDK
13981
- * picks. The getter itself lists them: callers that mirror it (the worker's cache and
13982
- * broadcasts, balances) must not see a settle's inputs vanish, and then stay gone if it fails.
14047
+ * @internal {@link getBoardingUtxos} minus those an in-flight settle is spending or an
14048
+ * unresolved intent locks, for SDK picks. The getter itself lists them: callers that mirror it
14049
+ * (the worker's cache and broadcasts, balances) must not see a settle's inputs vanish, and then
14050
+ * stay gone if it fails.
13983
14051
  */
13984
14052
  async getSelectableBoardingUtxos() {
13985
- return (await this.getBoardingUtxos()).filter(
13986
- (coin) => !this._pendingSpendOutpoints.has(`${coin.txid}:${coin.vout}`)
13987
- );
14053
+ const [utxos, locked] = await Promise.all([
14054
+ this.getBoardingUtxos(),
14055
+ this.lockedOutpoints()
14056
+ ]);
14057
+ return utxos.filter((coin) => {
14058
+ const key = `${coin.txid}:${coin.vout}`;
14059
+ return !this._pendingSpendOutpoints.has(key) && !locked.has(key);
14060
+ });
13988
14061
  }
13989
14062
  async getStoredBoardingUtxos() {
13990
14063
  const currentOnly = /* @__PURE__ */ new Set([
@@ -14009,6 +14082,7 @@ var ReadonlyWallet = class _ReadonlyWallet {
14009
14082
  * @returns A function that stops the subscriptions
14010
14083
  */
14011
14084
  async notifyIncomingFunds(eventCallback) {
14085
+ const cm = this.indexerProvider ? await this.getContractManager() : void 0;
14012
14086
  const arkAddress = await this.getAddress();
14013
14087
  let onchainStopFunc;
14014
14088
  let indexerStopFunc;
@@ -14061,8 +14135,7 @@ var ReadonlyWallet = class _ReadonlyWallet {
14061
14135
  void subscribeOnchain();
14062
14136
  });
14063
14137
  await subscribeOnchain();
14064
- if (this.indexerProvider && arkAddress) {
14065
- const cm = await this.getContractManager();
14138
+ if (cm && arkAddress) {
14066
14139
  let annotationQueue = Promise.resolve();
14067
14140
  indexerStopFunc = cm.onContractEvent((event) => {
14068
14141
  if (!isContractVtxoEvent(event)) {
@@ -14208,21 +14281,25 @@ var ReadonlyWallet = class _ReadonlyWallet {
14208
14281
  extraSources: capture?.sources
14209
14282
  });
14210
14283
  onVtxosPersisted = async (_contract, vtxos) => {
14211
- for (const v of vtxos) {
14212
- if (v.isSpent) continue;
14213
- await captureExitBranch({
14214
- resolver,
14215
- repository: virtualTxRepository,
14216
- vtxo: { txid: v.txid, vout: v.vout },
14217
- value: v.value,
14218
- mode: capture?.mode ?? DEFAULT_EXIT_CAPTURE_MODE,
14219
- minExitWorthSats: capture?.minExitWorthSats ?? DEFAULT_MIN_EXIT_WORTH_SATS
14220
- }).catch(() => {
14221
- });
14222
- }
14284
+ await Promise.all(
14285
+ vtxos.map((v) => {
14286
+ if (v.isSpent) return void 0;
14287
+ return captureExitBranch({
14288
+ resolver,
14289
+ repository: virtualTxRepository,
14290
+ vtxo: { txid: v.txid, vout: v.vout },
14291
+ value: v.value,
14292
+ mode: capture?.mode ?? DEFAULT_EXIT_CAPTURE_MODE,
14293
+ minExitWorthSats: capture?.minExitWorthSats ?? DEFAULT_MIN_EXIT_WORTH_SATS
14294
+ }).catch(() => {
14295
+ });
14296
+ })
14297
+ );
14223
14298
  };
14224
14299
  onVtxosSpent = (vtxos) => pruneExitBranches(virtualTxRepository, vtxos);
14225
14300
  }
14301
+ const baselinePubkeyPromise = this.identity.xOnlyPublicKey();
14302
+ void baselinePubkeyPromise.catch(() => void 0);
14226
14303
  const manager = await ContractManager.create({
14227
14304
  lazyInitialization: this.lazyInitialization,
14228
14305
  indexerProvider: this.indexerProvider,
@@ -14242,13 +14319,14 @@ var ReadonlyWallet = class _ReadonlyWallet {
14242
14319
  return { height, time };
14243
14320
  }
14244
14321
  });
14245
- const baselinePubkey = await this.identity.xOnlyPublicKey();
14322
+ const baselinePubkey = await baselinePubkeyPromise;
14246
14323
  const delegatePubKey = this.offchainTapscript instanceof DelegateVtxo.Script ? this.offchainTapscript.options.delegatePubKey : void 0;
14247
14324
  const baselineSigners = [
14248
14325
  this.offchainTapscript.options.serverPubKey,
14249
14326
  ...[...this._deprecatedSigners.keys()].map((h) => hex.decode(h))
14250
14327
  ];
14251
14328
  const seenBaselineScripts = /* @__PURE__ */ new Set();
14329
+ const baseline = [];
14252
14330
  for (const serverPubKey of baselineSigners) {
14253
14331
  for (const csvTimelock2 of this.walletContractTimelocks) {
14254
14332
  const csvTimelockStr = timelockToSequence(csvTimelock2).toString();
@@ -14260,7 +14338,7 @@ var ReadonlyWallet = class _ReadonlyWallet {
14260
14338
  const defaultScriptHex = hex.encode(defaultScript.pkScript);
14261
14339
  if (!seenBaselineScripts.has(defaultScriptHex)) {
14262
14340
  seenBaselineScripts.add(defaultScriptHex);
14263
- await manager.createContract({
14341
+ baseline.push({
14264
14342
  type: "default",
14265
14343
  params: {
14266
14344
  pubKey: hex.encode(defaultScript.options.pubKey),
@@ -14282,7 +14360,7 @@ var ReadonlyWallet = class _ReadonlyWallet {
14282
14360
  const delegateScriptHex = hex.encode(delegateScript.pkScript);
14283
14361
  if (seenBaselineScripts.has(delegateScriptHex)) continue;
14284
14362
  seenBaselineScripts.add(delegateScriptHex);
14285
- await manager.createContract({
14363
+ baseline.push({
14286
14364
  type: "delegate",
14287
14365
  params: {
14288
14366
  pubKey: hex.encode(delegateScript.options.pubKey),
@@ -14307,7 +14385,7 @@ var ReadonlyWallet = class _ReadonlyWallet {
14307
14385
  const boardingScriptHex = hex.encode(baselineBoarding.pkScript);
14308
14386
  if (seenBaselineScripts.has(boardingScriptHex)) continue;
14309
14387
  seenBaselineScripts.add(boardingScriptHex);
14310
- await manager.createContract({
14388
+ baseline.push({
14311
14389
  type: "boarding",
14312
14390
  params: {
14313
14391
  pubKey: hex.encode(baselineBoarding.options.pubKey),
@@ -14319,6 +14397,7 @@ var ReadonlyWallet = class _ReadonlyWallet {
14319
14397
  state: "active"
14320
14398
  });
14321
14399
  }
14400
+ await manager.createContracts(baseline);
14322
14401
  return manager;
14323
14402
  }
14324
14403
  /** Dispose wallet-owned managers and release background resources. */
@@ -14843,13 +14922,13 @@ var Wallet2 = class extends ReadonlyWallet {
14843
14922
  async _doRotateServerSigner(xonly, newServerUnrollScript) {
14844
14923
  if (equalBytes(xonly, this.arkServerPublicKey)) return;
14845
14924
  const manager = await this.getContractManager();
14846
- const [currentOffchainRow] = await manager.getContracts({
14847
- script: this.defaultContractScript
14848
- });
14849
14925
  const currentBoardingScript = hex.encode(this._boardingTapscript.pkScript);
14850
- const [currentBoardingRow] = await manager.getContracts({
14851
- script: currentBoardingScript
14852
- });
14926
+ const [offchainRows, boardingRows] = await Promise.all([
14927
+ manager.getContracts({ script: this.defaultContractScript }),
14928
+ manager.getContracts({ script: currentBoardingScript })
14929
+ ]);
14930
+ const [currentOffchainRow] = offchainRows;
14931
+ const [currentBoardingRow] = boardingRows;
14853
14932
  const newOffchain = this.offchainTapscript instanceof DelegateVtxo.Script ? new DelegateVtxo.Script({
14854
14933
  ...this.offchainTapscript.options,
14855
14934
  serverPubKey: xonly
@@ -15032,13 +15111,15 @@ var Wallet2 = class extends ReadonlyWallet {
15032
15111
  return this._restoreInFlight;
15033
15112
  }
15034
15113
  async _runRestore(gapLimit) {
15035
- const manager = await this.getContractManager();
15036
15114
  const provider = this._hdProvider;
15037
15115
  const hd = provider !== void 0;
15038
- const staticDescriptor = hd ? void 0 : await identityDescriptor(this.identity);
15116
+ const [manager, staticDescriptor, arkInfo] = await Promise.all([
15117
+ this.getContractManager(),
15118
+ hd ? void 0 : identityDescriptor(this.identity),
15119
+ this.arkProvider.getInfo()
15120
+ ]);
15039
15121
  const materialize = (index) => hd ? provider.materializeDescriptorAt(index) : staticDescriptor;
15040
15122
  const delegatePubKey = this.offchainTapscript instanceof DelegateVtxo.Script ? this.offchainTapscript.options.delegatePubKey : void 0;
15041
- const arkInfo = await this.arkProvider.getInfo();
15042
15123
  const currentSignerPubKey = toXOnly(hex.decode(arkInfo.signerPubkey), "ark signer key");
15043
15124
  const deprecatedSignerPubKeys = arkInfo.deprecatedSigners.map(
15044
15125
  (s) => toXOnly(hex.decode(s.pubkey), "deprecated signer key")
@@ -15586,25 +15667,33 @@ var Wallet2 = class extends ReadonlyWallet {
15586
15667
  throw error;
15587
15668
  }
15588
15669
  const inputIds = params.inputs.map((i) => `${i.txid}:${i.vout}`).join(",");
15670
+ let released = !registering;
15589
15671
  if (registering) {
15590
- await this.arkProvider.deleteIntent(deleteIntent).catch((e) => {
15591
- if (isNoMatchingIntentsError(e)) return;
15592
- console.warn(
15593
- `Failed to delete intent after settle failure for inputs [${inputIds}]; intent may linger on server and cause 'duplicated input' on next settle`,
15594
- e
15595
- );
15596
- });
15597
- }
15598
- await this.persistIntentSnapshot(
15599
- intentTxId,
15600
- "cancelled",
15601
- intent,
15602
- deleteIntent,
15603
- params.inputs,
15604
- {
15605
- cancellationReason: error instanceof Error ? error.message : String(error)
15672
+ try {
15673
+ await this.arkProvider.deleteIntent(deleteIntent);
15674
+ released = true;
15675
+ } catch (e) {
15676
+ if (isNoMatchingIntentsError(e)) released = true;
15677
+ else {
15678
+ console.warn(
15679
+ `Failed to delete intent after settle failure for inputs [${inputIds}]; intent may linger on server and cause 'duplicated input' on next settle`,
15680
+ e
15681
+ );
15682
+ }
15606
15683
  }
15607
- );
15684
+ }
15685
+ if (released) {
15686
+ await this.persistIntentSnapshot(
15687
+ intentTxId,
15688
+ "cancelled",
15689
+ intent,
15690
+ deleteIntent,
15691
+ params.inputs,
15692
+ {
15693
+ cancellationReason: error instanceof Error ? error.message : String(error)
15694
+ }
15695
+ );
15696
+ }
15608
15697
  throw error;
15609
15698
  } finally {
15610
15699
  this._removePendingSpends(params.inputs);
@@ -18533,6 +18622,7 @@ var WalletMessageHandler = class {
18533
18622
  indexerProvider;
18534
18623
  walletRepository;
18535
18624
  incomingFundsSubscription;
18625
+ incomingFundsRegistrations = /* @__PURE__ */ new WeakMap();
18536
18626
  contractEventsSubscription;
18537
18627
  onNextTick = [];
18538
18628
  channel;
@@ -18544,6 +18634,11 @@ var WalletMessageHandler = class {
18544
18634
  // stale, folding requests in would truncate an in-flight settle, and
18545
18635
  // sharing with boarding would kill every lazy refresh (see `boardingEpoch`).
18546
18636
  incomingFundsEpoch = 0;
18637
+ /**
18638
+ * Supersedes older initializers without touching `incomingFundsEpoch`, so a
18639
+ * re-init that fails before registering leaves the live subscription delivering.
18640
+ */
18641
+ initEpoch = 0;
18547
18642
  contractEventsEpoch = 0;
18548
18643
  boardingEpoch = 0;
18549
18644
  /** Per-request progress callbacks: ends with the handler, not with a re-init. */
@@ -18568,6 +18663,7 @@ var WalletMessageHandler = class {
18568
18663
  }
18569
18664
  async stop() {
18570
18665
  this.requestEpoch++;
18666
+ this.initEpoch++;
18571
18667
  this.incomingFundsEpoch++;
18572
18668
  this.contractEventsEpoch++;
18573
18669
  this.boardingEpoch++;
@@ -18608,6 +18704,8 @@ var WalletMessageHandler = class {
18608
18704
  }
18609
18705
  epochOf(scope) {
18610
18706
  switch (scope) {
18707
+ case "init":
18708
+ return this.initEpoch;
18611
18709
  case "incomingFunds":
18612
18710
  return this.incomingFundsEpoch;
18613
18711
  case "contractEvents":
@@ -19364,18 +19462,22 @@ var WalletMessageHandler = class {
19364
19462
  }
19365
19463
  async onWalletInitialized() {
19366
19464
  const wallet = this.readonlyWallet;
19465
+ const signingWallet = this.wallet;
19466
+ const lazyBoarding = this.lazyBoardingInit;
19367
19467
  if (!wallet || !this.arkProvider || !this.indexerProvider || !this.walletRepository) {
19368
19468
  return;
19369
19469
  }
19370
- const init = this.newGeneration("incomingFunds");
19470
+ this.initEpoch++;
19471
+ const init = this.newGeneration("init");
19371
19472
  await this.ensureContractEventBroadcasting();
19372
19473
  if (init.stale) return;
19373
- await this.refreshCachedData({ lazyBoarding: this.lazyBoardingInit });
19474
+ await this.refreshCachedData({ lazyBoarding });
19374
19475
  if (init.stale) return;
19375
- if (this.wallet) {
19476
+ if (signingWallet) {
19376
19477
  try {
19377
19478
  const vtxos = await this.getVtxosFromRepo();
19378
- const { pending, finalized } = await this.wallet.finalizePendingTxs(
19479
+ if (init.stale) return;
19480
+ const { pending, finalized } = await signingWallet.finalizePendingTxs(
19379
19481
  vtxos.filter((vtxo) => vtxo.isSpent || vtxo.isPreconfirmed && !vtxo.isSwept)
19380
19482
  );
19381
19483
  console.info(
@@ -19386,11 +19488,34 @@ var WalletMessageHandler = class {
19386
19488
  }
19387
19489
  if (init.stale) return;
19388
19490
  }
19389
- if (this.incomingFundsSubscription) this.incomingFundsSubscription();
19390
- this.incomingFundsSubscription = void 0;
19391
19491
  this.incomingFundsEpoch++;
19392
19492
  const emitter = this.newEmitter("incomingFunds");
19493
+ const provider = wallet.onchainProvider ?? this;
19494
+ const previousRegistration = this.incomingFundsRegistrations.get(provider) ?? Promise.resolve();
19495
+ const registration = previousRegistration.then(
19496
+ () => this.registerIncomingFunds(wallet, emitter)
19497
+ );
19498
+ this.incomingFundsRegistrations.set(
19499
+ provider,
19500
+ registration.catch((error) => {
19501
+ console.error("Error subscribing to incoming funds:", error);
19502
+ })
19503
+ );
19504
+ if (!lazyBoarding) await registration;
19505
+ if (signingWallet && !emitter.stale) {
19506
+ try {
19507
+ await signingWallet.getVtxoManager();
19508
+ } catch (error) {
19509
+ console.error("Error starting VtxoManager:", error);
19510
+ }
19511
+ }
19512
+ }
19513
+ async registerIncomingFunds(wallet, emitter) {
19514
+ if (emitter.stale) return;
19515
+ if (this.incomingFundsSubscription) this.incomingFundsSubscription();
19516
+ this.incomingFundsSubscription = void 0;
19393
19517
  const address = await wallet.getAddress();
19518
+ if (emitter.stale) return;
19394
19519
  const unsubscribe = await wallet.notifyIncomingFunds(async (funds) => {
19395
19520
  if (emitter.stale) return;
19396
19521
  if (funds.type === "vtxo") {
@@ -19459,13 +19584,6 @@ var WalletMessageHandler = class {
19459
19584
  });
19460
19585
  if (emitter.stale) return unsubscribe();
19461
19586
  this.incomingFundsSubscription = unsubscribe;
19462
- if (this.wallet) {
19463
- try {
19464
- await this.wallet.getVtxoManager();
19465
- } catch (error) {
19466
- console.error("Error starting VtxoManager:", error);
19467
- }
19468
- }
19469
19587
  }
19470
19588
  /**
19471
19589
  * Refresh boarding inputs and transaction history. The virtual outputs come
@@ -19494,24 +19612,30 @@ var WalletMessageHandler = class {
19494
19612
  payload: { coins }
19495
19613
  })
19496
19614
  );
19497
- await this.refreshHistoryCache(emitter);
19615
+ await this.storeHistoryCache(await this.loadHistoryCache(), emitter);
19498
19616
  })().catch((error) => {
19499
19617
  console.warn("[wallet] background refresh failed", error);
19500
19618
  });
19501
19619
  return;
19502
19620
  }
19621
+ const history = this.loadHistoryCache();
19622
+ void history.catch(() => void 0);
19503
19623
  await this.refreshBoardingCache();
19504
19624
  this.boardingLoaded = true;
19505
- await this.refreshHistoryCache();
19625
+ await this.storeHistoryCache(await history);
19506
19626
  }
19507
- /** @param gen - set by a background caller; each write is skipped once it goes stale. */
19508
- async refreshHistoryCache(gen) {
19627
+ /** History rows plus the address they are stored under. Does not write. */
19628
+ async loadHistoryCache() {
19509
19629
  const wallet = this.readonlyWallet;
19510
- const repository = this.walletRepository;
19511
- if (!wallet || !repository) return;
19630
+ if (!wallet || !this.walletRepository) return void 0;
19512
19631
  const address = await wallet.getAddress();
19513
19632
  const txs = await this.buildTransactionHistoryFromCache();
19514
- if (txs && !gen?.stale) await repository.saveTransactions(address, txs);
19633
+ if (!txs) return void 0;
19634
+ return { address, txs };
19635
+ }
19636
+ async storeHistoryCache(history, gen) {
19637
+ if (!history || gen?.stale || !this.walletRepository) return;
19638
+ await this.walletRepository.saveTransactions(history.address, history.txs);
19515
19639
  }
19516
19640
  /**
19517
19641
  * @param gen - set by a background caller; each write here is skipped once it goes stale.
@@ -19526,11 +19650,17 @@ var WalletMessageHandler = class {
19526
19650
  hex.encode(wallet.boardingTapscript.options.serverPubKey)
19527
19651
  );
19528
19652
  const groups = await wallet.getBoardingUtxosForSigners(/* @__PURE__ */ new Set([currentSigner]));
19529
- for (const { tapscript, coins } of groups) {
19653
+ if (gen?.stale) return;
19654
+ const cachedByAddress = await Promise.all(
19655
+ groups.map(
19656
+ ({ tapscript }) => collectUtxos(repository, tapscript.onchainAddress(wallet.network))
19657
+ )
19658
+ );
19659
+ for (const [i, { tapscript, coins }] of groups.entries()) {
19530
19660
  if (gen?.stale) return;
19531
19661
  const address = tapscript.onchainAddress(wallet.network);
19532
19662
  const freshKeys = new Set(coins.map((coin) => `${coin.txid}:${coin.vout}`));
19533
- const cached = await collectUtxos(repository, address);
19663
+ const cached = cachedByAddress[i];
19534
19664
  const kept = cached.filter((coin) => freshKeys.has(`${coin.txid}:${coin.vout}`));
19535
19665
  if (kept.length === cached.length) continue;
19536
19666
  if (gen?.stale) return;
@@ -19628,6 +19758,7 @@ var WalletMessageHandler = class {
19628
19758
  const wallet = this.wallet ?? this.readonlyWallet;
19629
19759
  if (!wallet) return;
19630
19760
  this.requestEpoch++;
19761
+ this.initEpoch++;
19631
19762
  this.incomingFundsEpoch++;
19632
19763
  this.contractEventsEpoch++;
19633
19764
  this.boardingEpoch++;
@@ -19679,7 +19810,9 @@ var WalletMessageHandler = class {
19679
19810
  * {@link getVtxosFromRepo}).
19680
19811
  */
19681
19812
  async repoSnapshot(options = {}) {
19682
- if (!this.walletRepository || !this.readonlyWallet) return { snapshot: [], vtxos: [] };
19813
+ const repository = this.walletRepository;
19814
+ const wallet = this.readonlyWallet;
19815
+ if (!repository || !wallet) return { snapshot: [], vtxos: [] };
19683
19816
  const seen = /* @__PURE__ */ new Set();
19684
19817
  const allVtxos = [];
19685
19818
  const addVtxos = (vtxos) => {
@@ -19694,19 +19827,18 @@ var WalletMessageHandler = class {
19694
19827
  }
19695
19828
  return fresh;
19696
19829
  };
19697
- const manager = await this.readonlyWallet.getContractManager();
19830
+ const addressPromise = wallet.getAddress();
19831
+ void addressPromise.catch(() => void 0);
19832
+ const manager = await wallet.getContractManager();
19698
19833
  const contracts = await manager.getContracts();
19699
- const snapshot = [];
19700
- for (const contract of contracts) {
19701
- if (!options.includeRecoveryOnly && isRecoveryOnlyContract(contract)) continue;
19702
- snapshot.push({
19703
- contract,
19704
- vtxos: addVtxos(
19705
- await getVtxosForContract(this.walletRepository, contract, options)
19706
- )
19707
- });
19708
- }
19709
- const walletAddress = await this.readonlyWallet.getAddress();
19834
+ const eligible = contracts.filter(
19835
+ (contract) => options.includeRecoveryOnly || !isRecoveryOnlyContract(contract)
19836
+ );
19837
+ const listsPromise = Promise.all(
19838
+ eligible.map((contract) => getVtxosForContract(repository, contract, options))
19839
+ );
19840
+ void listsPromise.catch(() => void 0);
19841
+ const walletAddress = await addressPromise;
19710
19842
  let walletScript;
19711
19843
  try {
19712
19844
  walletScript = scriptFromArkAddress(walletAddress);
@@ -19715,13 +19847,26 @@ var WalletMessageHandler = class {
19715
19847
  `WalletMessageHandler.getVtxosFromRepo: failed to derive script from wallet address ${walletAddress}: ${e instanceof Error ? e.message : String(e)}`
19716
19848
  );
19717
19849
  }
19718
- addVtxos(
19719
- await getVtxosForContract(
19720
- this.walletRepository,
19850
+ const [lists, primaryVtxos] = await Promise.all([
19851
+ listsPromise,
19852
+ // Routed through the same helper as the contract buckets rather than reading the
19853
+ // repository directly, so this bucket normalizes too. Anything left here
19854
+ // is outside every contract row, so it carries no contract to judge it
19855
+ // by — the wallet's own receive address, which is never gated.
19856
+ getVtxosForContract(
19857
+ repository,
19721
19858
  { script: walletScript, address: walletAddress },
19722
19859
  options
19723
19860
  )
19724
- );
19861
+ ]);
19862
+ const snapshot = [];
19863
+ for (const [i, contract] of eligible.entries()) {
19864
+ snapshot.push({
19865
+ contract,
19866
+ vtxos: addVtxos(lists[i])
19867
+ });
19868
+ }
19869
+ addVtxos(primaryVtxos);
19725
19870
  return { snapshot, vtxos: allVtxos };
19726
19871
  }
19727
19872
  /**
@@ -20424,7 +20569,12 @@ var ServiceWorkerReadonlyWallet = class _ServiceWorkerReadonlyWallet {
20424
20569
  };
20425
20570
  try {
20426
20571
  const response = await this.sendMessage(message);
20427
- return response.payload;
20572
+ const balance = response.payload;
20573
+ return {
20574
+ ...balance,
20575
+ contractLocked: balance.contractLocked ?? balance.gated,
20576
+ deprecatedSignerLocked: balance.deprecatedSignerLocked ?? balance.pendingRecovery
20577
+ };
20428
20578
  } catch (error) {
20429
20579
  throw new Error(`Failed to get balance: ${error}`);
20430
20580
  }
@@ -22106,6 +22256,14 @@ var ElectrumOnchainProvider = class {
22106
22256
  return hex.decode(tx.hex);
22107
22257
  }
22108
22258
  async getTxOutspends(txid) {
22259
+ return this.resolveOutspends(txid);
22260
+ }
22261
+ /** Reads `vout`'s script alone: another output of a tx someone else built (their change, a
22262
+ * service's fee address) can have a history Fulcrum answers empty and electrs refuses. */
22263
+ async getTxOutspend(txid, vout) {
22264
+ return (await this.resolveOutspends(txid, vout))[vout] ?? { spent: false, txid: "" };
22265
+ }
22266
+ async resolveOutspends(txid, only) {
22109
22267
  const [txResult] = await this.chain.fetchTransactions([txid]);
22110
22268
  const tx = Transaction$1.fromRaw(hex.decode(txResult.hex), {
22111
22269
  allowUnknownOutputs: true
@@ -22114,7 +22272,10 @@ var ElectrumOnchainProvider = class {
22114
22272
  const outputScriptHashes = [];
22115
22273
  for (let i = 0; i < outputCount; i++) {
22116
22274
  const output = tx.getOutput(i);
22117
- outputScriptHashes.push(output.script ? toScriptHash(output.script) : void 0);
22275
+ const wanted = only === void 0 || i === only;
22276
+ outputScriptHashes.push(
22277
+ wanted && output.script ? toScriptHash(output.script) : void 0
22278
+ );
22118
22279
  }
22119
22280
  const validScriptHashes = outputScriptHashes.filter((h) => h !== void 0);
22120
22281
  const results = Array.from(
@@ -22607,8 +22768,12 @@ async function attachPrevoutTxs(tx, source) {
22607
22768
  }
22608
22769
  if (pending.length === 0) return;
22609
22770
  const resolved = /* @__PURE__ */ new Map();
22610
- for (const txid of new Set(pending.map((p) => p.txid))) {
22611
- resolved.set(txid, await source.getRawTransaction(txid));
22771
+ const txids = [...new Set(pending.map((p) => p.txid))];
22772
+ const fetched = await Promise.allSettled(txids.map((txid) => source.getRawTransaction(txid)));
22773
+ for (let i = 0; i < fetched.length; i++) {
22774
+ const result = fetched[i];
22775
+ if (result.status === "rejected") throw result.reason;
22776
+ resolved.set(txids[i], result.value);
22612
22777
  }
22613
22778
  for (const { index, txid } of pending) {
22614
22779
  setArkPsbtField(tx, index, PrevoutTxField, resolved.get(txid));
@@ -23062,40 +23227,53 @@ var Unroll;
23062
23227
  Unroll2.completeUnroll = completeUnroll;
23063
23228
  })(Unroll || (Unroll = {}));
23064
23229
  async function prepareUnrollTransaction(wallet, vtxoTxIds, outputAddress) {
23065
- const chainTip = await wallet.onchainProvider.getChainTip();
23066
- let vtxos = await wallet.getVtxos({ withUnrolled: true });
23067
- vtxos = vtxos.filter((vtxo) => vtxoTxIds.includes(vtxo.txid));
23230
+ const [chainTip, storedVtxos, walletDescriptor] = await Promise.all([
23231
+ wallet.onchainProvider.getChainTip(),
23232
+ wallet.getVtxos({ withUnrolled: true }),
23233
+ wallet.identity.xOnlyPublicKey().then((key) => hex.encode(key))
23234
+ ]);
23235
+ const vtxos = storedVtxos.filter((vtxo) => vtxoTxIds.includes(vtxo.txid));
23068
23236
  if (vtxos.length === 0) {
23069
23237
  throw new Error("No vtxos to complete unroll");
23070
23238
  }
23071
23239
  const inputs = [];
23072
23240
  let totalAmount = 0n;
23073
23241
  const txWeightEstimator = TxWeightEstimator.create();
23074
- const walletDescriptor = hex.encode(await wallet.identity.xOnlyPublicKey());
23075
23242
  for (const vtxo of vtxos) {
23076
23243
  if (!canSweepOnchain(vtxo)) {
23077
23244
  throw new Error(
23078
23245
  `Vtxo ${vtxo.txid}:${vtxo.vout} cannot be swept onchain: it is not fully unrolled (use unroll first), or its exit output was already spent`
23079
23246
  );
23080
23247
  }
23081
- const txStatus = await wallet.onchainProvider.getTxStatus(vtxo.txid);
23082
- if (!txStatus.confirmed) {
23083
- throw new Error(`tx ${vtxo.txid} is not confirmed`);
23084
- }
23085
- const resolved = await resolveUnilateralPath({
23086
- vtxo,
23087
- scriptHex: hex.encode(VtxoScript.decode(vtxo.tapTree).pkScript),
23088
- contractRepository: wallet.contractRepository,
23089
- walletDescriptor,
23090
- currentTime: Date.now()
23091
- });
23092
- const spendingLeaf = resolved.selection.leaf;
23093
- const sequence = resolved.selection.sequence;
23094
- const timelock = sequenceToTimelock(sequence);
23095
- const elapsed = timelock.type === "blocks" ? chainTip.height >= txStatus.blockHeight + Number(timelock.value) : chainTip.time >= txStatus.blockTime + Number(timelock.value);
23096
- if (!elapsed) {
23097
- throw new Error(`no available exit path found for vtxo ${vtxo.txid}:${vtxo.vout}`);
23098
- }
23248
+ }
23249
+ const prepared = await Promise.allSettled(
23250
+ vtxos.map(async (vtxo) => {
23251
+ const [txStatus, resolved] = await Promise.all([
23252
+ wallet.onchainProvider.getTxStatus(vtxo.txid),
23253
+ resolveUnilateralPath({
23254
+ vtxo,
23255
+ scriptHex: hex.encode(VtxoScript.decode(vtxo.tapTree).pkScript),
23256
+ contractRepository: wallet.contractRepository,
23257
+ walletDescriptor,
23258
+ currentTime: Date.now()
23259
+ })
23260
+ ]);
23261
+ if (!txStatus.confirmed) {
23262
+ throw new Error(`tx ${vtxo.txid} is not confirmed`);
23263
+ }
23264
+ const spendingLeaf = resolved.selection.leaf;
23265
+ const sequence = resolved.selection.sequence;
23266
+ const timelock = sequenceToTimelock(sequence);
23267
+ const elapsed = timelock.type === "blocks" ? chainTip.height >= txStatus.blockHeight + Number(timelock.value) : chainTip.time >= txStatus.blockTime + Number(timelock.value);
23268
+ if (!elapsed) {
23269
+ throw new Error(`no available exit path found for vtxo ${vtxo.txid}:${vtxo.vout}`);
23270
+ }
23271
+ return { vtxo, spendingLeaf, sequence };
23272
+ })
23273
+ );
23274
+ for (const result of prepared) {
23275
+ if (result.status === "rejected") throw result.reason;
23276
+ const { vtxo, spendingLeaf, sequence } = result.value;
23099
23277
  totalAmount += BigInt(vtxo.value);
23100
23278
  inputs.push({
23101
23279
  txid: vtxo.txid,
@@ -23253,15 +23431,18 @@ async function resolveFeeRate(opts) {
23253
23431
  async function selectExitVtxos(opts) {
23254
23432
  if (!opts.vtxos) return opts.wallet.getVtxos();
23255
23433
  const res = await getNormalizedVtxos(opts.wallet.indexerProvider, { outpoints: opts.vtxos });
23434
+ const scripts = [
23435
+ ...new Set(res.vtxos.filter((vtxo) => !vtxo.isSpent).map((vtxo) => vtxo.script))
23436
+ ];
23437
+ const contracts = scripts.length === 0 ? [] : await collectContracts(opts.wallet.contractRepository, { script: scripts });
23438
+ const byScript = new Map(contracts.map((contract) => [contract.script, contract]));
23256
23439
  const tapTrees = /* @__PURE__ */ new Map();
23257
23440
  const out = [];
23258
23441
  for (const vtxo of res.vtxos) {
23259
23442
  if (vtxo.isSpent) continue;
23260
23443
  let tapTree = tapTrees.get(vtxo.script);
23261
23444
  if (!tapTree) {
23262
- const [contract] = await collectContracts(opts.wallet.contractRepository, {
23263
- script: vtxo.script
23264
- });
23445
+ const contract = byScript.get(vtxo.script);
23265
23446
  if (!contract) {
23266
23447
  throw new Error(
23267
23448
  `no contract registered for vtxo script ${vtxo.script} \u2014 register the contract before exiting`
@@ -23280,6 +23461,10 @@ async function selectExitVtxos(opts) {
23280
23461
  }
23281
23462
  async function computeExitLayout(opts, feeRate) {
23282
23463
  const { wallet, onchainWallet } = opts;
23464
+ const coinsPromise = onchainWallet.getCoins();
23465
+ const pubkeyPromise = wallet.identity.xOnlyPublicKey();
23466
+ void coinsPromise.catch(() => void 0);
23467
+ void pubkeyPromise.catch(() => void 0);
23283
23468
  const vtxos = await selectExitVtxos(opts);
23284
23469
  if (vtxos.length === 0) throw new Error("no vtxos to exit");
23285
23470
  const resolver = createExitChainResolver({
@@ -23317,46 +23502,57 @@ async function computeExitLayout(opts, feeRate) {
23317
23502
  (s) => s.parent.id,
23318
23503
  (s) => txInputTxids(s.parent)
23319
23504
  );
23320
- const walletDescriptor = hex.encode(await wallet.identity.xOnlyPublicKey());
23505
+ const walletDescriptor = hex.encode(await pubkeyPromise);
23506
+ const resolvedPaths = await Promise.allSettled(
23507
+ vtxos.map(async (vtxo) => {
23508
+ try {
23509
+ return {
23510
+ vtxo,
23511
+ resolved: await resolveUnilateralPath({
23512
+ vtxo,
23513
+ scriptHex: hex.encode(VtxoScript.decode(vtxo.tapTree).pkScript),
23514
+ contractRepository: wallet.contractRepository,
23515
+ walletDescriptor,
23516
+ currentTime: Date.now()
23517
+ })
23518
+ };
23519
+ } catch (e) {
23520
+ if (e instanceof ExitPathError) return { vtxo, skipped: e.message };
23521
+ throw e;
23522
+ }
23523
+ })
23524
+ );
23321
23525
  const infos = [];
23322
23526
  const sweeps = [];
23323
- for (const vtxo of vtxos) {
23527
+ for (const result of resolvedPaths) {
23528
+ if (result.status === "rejected") throw result.reason;
23529
+ const { vtxo } = result.value;
23324
23530
  const outpoint = `${vtxo.txid}:${vtxo.vout}`;
23325
- try {
23326
- const resolved = await resolveUnilateralPath({
23327
- vtxo,
23328
- scriptHex: hex.encode(VtxoScript.decode(vtxo.tapTree).pkScript),
23329
- contractRepository: wallet.contractRepository,
23330
- walletDescriptor,
23331
- currentTime: Date.now()
23531
+ if ("skipped" in result.value) {
23532
+ infos.push({ outpoint, value: vtxo.value, skipped: result.value.skipped });
23533
+ continue;
23534
+ }
23535
+ const { resolved } = result.value;
23536
+ const sweepFee = sweepFeeFor(
23537
+ resolved.selection,
23538
+ opts.sweepAddress,
23539
+ wallet.network,
23540
+ feeRate
23541
+ );
23542
+ if (vtxo.value - sweepFee < CHILD_OUTPUT_DUST) {
23543
+ infos.push({
23544
+ outpoint,
23545
+ value: vtxo.value,
23546
+ skipped: `uneconomic: value ${vtxo.value} <= sweep fee + dust`
23332
23547
  });
23333
- const sweepFee = sweepFeeFor(
23334
- resolved.selection,
23335
- opts.sweepAddress,
23336
- wallet.network,
23337
- feeRate
23338
- );
23339
- if (vtxo.value - sweepFee < CHILD_OUTPUT_DUST) {
23340
- infos.push({
23341
- outpoint,
23342
- value: vtxo.value,
23343
- skipped: `uneconomic: value ${vtxo.value} <= sweep fee + dust`
23344
- });
23345
- continue;
23346
- }
23347
- const t = sequenceToTimelock(resolved.selection.sequence);
23348
- const delay = { type: t.type, value: Number(t.value) };
23349
- infos.push({ outpoint, value: vtxo.value, sweepFee, path: resolved.label, delay });
23350
- sweeps.push({ vtxo, resolved, sweepFee, delay });
23351
- } catch (e) {
23352
- if (e instanceof ExitPathError) {
23353
- infos.push({ outpoint, value: vtxo.value, skipped: e.message });
23354
- continue;
23355
- }
23356
- throw e;
23548
+ continue;
23357
23549
  }
23550
+ const t = sequenceToTimelock(resolved.selection.sequence);
23551
+ const delay = { type: t.type, value: Number(t.value) };
23552
+ infos.push({ outpoint, value: vtxo.value, sweepFee, path: resolved.label, delay });
23553
+ sweeps.push({ vtxo, resolved, sweepFee, delay });
23358
23554
  }
23359
- const coins = (await onchainWallet.getCoins()).filter((c) => c.status.confirmed);
23555
+ const coins = (await coinsPromise).filter((c) => c.status.confirmed);
23360
23556
  const balance = coins.reduce((sum, c) => sum + c.value, 0);
23361
23557
  const fundingTotal = steps.reduce((sum, s) => sum + s.funding, 0);
23362
23558
  const splitterFeeFor = (inputCount) => {
@@ -24756,6 +24952,13 @@ var Arkade = class _Arkade {
24756
24952
  }
24757
24953
  return new ArkadeContract(this, program, args);
24758
24954
  }
24955
+ /**
24956
+ * Start a spend with no function of its own: each `.from(coins, contract.unlock.<name>(...))`
24957
+ * names the function its coins are spent through.
24958
+ */
24959
+ transaction() {
24960
+ return new ArkadeTransactionBuilder(this);
24961
+ }
24759
24962
  };
24760
24963
  var ArkadeContract = class _ArkadeContract {
24761
24964
  constructor(client, program, args = {}, keys) {
@@ -24817,7 +25020,23 @@ var ArkadeContract = class _ArkadeContract {
24817
25020
  get functions() {
24818
25021
  const out = /* @__PURE__ */ Object.create(null);
24819
25022
  for (const fn of this.compiled) {
24820
- out[fn.name] = (...callArgs) => new ArkadeTransactionBuilder(this, fn, bindInputs(fn, callArgs));
25023
+ out[fn.name] = (...callArgs) => new ArkadeTransactionBuilder(this.client, {
25024
+ contract: this,
25025
+ fn,
25026
+ args: bindInputs(fn, callArgs)
25027
+ });
25028
+ }
25029
+ return out;
25030
+ }
25031
+ /** Spending paths bound to their arguments, for `.from(coins, contract.unlock.<name>(...))`. */
25032
+ get unlock() {
25033
+ const out = /* @__PURE__ */ Object.create(null);
25034
+ for (const fn of this.compiled) {
25035
+ out[fn.name] = (...callArgs) => ({
25036
+ contract: this,
25037
+ fn,
25038
+ args: bindInputs(fn, callArgs)
25039
+ });
24821
25040
  }
24822
25041
  return out;
24823
25042
  }
@@ -24935,27 +25154,62 @@ var ArkadeContract = class _ArkadeContract {
24935
25154
  };
24936
25155
  var ArkadeTransactionBuilder = class {
24937
25156
  /** @internal */
24938
- constructor(contract, fn, args) {
24939
- this.contract = contract;
24940
- this.fn = fn;
24941
- this.args = args;
25157
+ constructor(client, unlocker) {
25158
+ this.client = client;
25159
+ this.unlocker = unlocker;
24942
25160
  }
24943
- contract;
24944
- fn;
24945
- args;
25161
+ client;
25162
+ unlocker;
24946
25163
  outputs = [];
24947
25164
  fundingCoins = [];
24948
25165
  assetSpecs = [];
24949
25166
  extraPackets = [];
24950
- coin;
25167
+ inputs = [];
24951
25168
  changeScript;
24952
- from(coin) {
24953
- this.coin = coin;
25169
+ /**
25170
+ * Spend coins through this builder's function, or through `unlocker`
25171
+ * (`other.unlock.<name>(...args)`, same client), which `ark.transaction()` requires. Calls add
25172
+ * up; contract coins come first, in call order, then `.fund()` inputs.
25173
+ *
25174
+ * Every coin's script sees the same outputs: "output 0 pays X" passes once for all of them.
25175
+ * For one payment per coin, the contract must check `INSPECTNUMINPUTS` or `PUSHCURRENTINPUTINDEX`.
25176
+ */
25177
+ from(coins, unlocker = this.unlocker) {
25178
+ const list = Array.isArray(coins) ? coins : [coins];
25179
+ if (list.length === 0) throw new Error("from(): no coins given");
25180
+ if (!unlocker) {
25181
+ throw new Error("from(): name the function to spend through: contract.unlock.<name>()");
25182
+ }
25183
+ const { contract, fn } = unlocker;
25184
+ if (contract.client !== this.client) {
25185
+ throw new Error(`from(): ${fn.name} belongs to a contract on another Arkade client`);
25186
+ }
25187
+ const script = hex.encode(contract.pkScript);
25188
+ const taken = new Set(this.inputs.map(({ coin: c }) => `${c.txid}:${c.vout}`));
25189
+ const now = Date.now();
25190
+ for (const coin of list) {
25191
+ const outpoint = `${coin.txid}:${coin.vout}`;
25192
+ if (coin.script && coin.script !== script) {
25193
+ throw new Error(
25194
+ `from(): coin ${outpoint} is not locked by the contract of ${fn.name}`
25195
+ );
25196
+ }
25197
+ if (taken.has(outpoint))
25198
+ throw new Error(`from(): coin ${outpoint} is already an input`);
25199
+ const reason = unspendable(coin, now);
25200
+ if (reason) {
25201
+ throw new Error(
25202
+ `from(): coin ${outpoint} ${reason}; recover it into a batch instead`
25203
+ );
25204
+ }
25205
+ taken.add(outpoint);
25206
+ }
25207
+ this.inputs.push(...list.map((coin) => ({ coin, unlocker })));
24954
25208
  return this;
24955
25209
  }
24956
25210
  /**
24957
25211
  * Add extra inputs the caller funds (e.g. a taker's own coins in a swap).
24958
- * These become inputs 1..n and are signed with the client identity.
25212
+ * These come after the contract coins and are signed with the client identity.
24959
25213
  */
24960
25214
  fund(coins) {
24961
25215
  this.fundingCoins.push(...coins);
@@ -24992,23 +25246,39 @@ var ArkadeTransactionBuilder = class {
24992
25246
  }
24993
25247
  /** Assemble the unsigned ark transaction and its checkpoints. */
24994
25248
  async build() {
24995
- if (this.fn.def.tapscript.csv) {
24996
- throw new Error(
24997
- `${this.fn.name}: CSV leaves require an aged onchain output; the offchain builder creates fresh checkpoints \u2014 use the wallet's unilateral exit flow`
24998
- );
25249
+ const { arkTx, checkpoints } = await this.assemble();
25250
+ return { arkTx, checkpoints };
25251
+ }
25252
+ async assemble() {
25253
+ for (const { fn } of this.unlockers()) {
25254
+ if (fn.def.tapscript.csv) {
25255
+ throw new Error(
25256
+ `${fn.name}: CSV leaves require an aged onchain output; the offchain builder creates fresh checkpoints \u2014 use the wallet's unilateral exit flow`
25257
+ );
25258
+ }
24999
25259
  }
25000
25260
  if (this.outputs.length === 0) {
25001
25261
  throw new Error("ArkadeTransactionBuilder: at least one output is required");
25002
25262
  }
25003
25263
  const outputsSum = this.outputs.reduce((s, o) => s + (o.amount ?? 0n), 0n);
25004
- const coin = this.coin ?? await this.selectCoin(outputsSum);
25005
- const def = this.fn.def;
25264
+ let inputs = [...this.inputs];
25265
+ if (inputs.length === 0) {
25266
+ if (!this.unlocker) {
25267
+ throw new Error(
25268
+ "ArkadeTransactionBuilder: add coins with .from(coins, contract.unlock.<name>())"
25269
+ );
25270
+ }
25271
+ inputs = [
25272
+ { coin: await this.selectCoin(this.unlocker, outputsSum), unlocker: this.unlocker }
25273
+ ];
25274
+ }
25275
+ const coinsSum = inputs.reduce((s, { coin }) => s + BigInt(coin.value), 0n);
25006
25276
  const outputs = [...this.outputs];
25007
25277
  const fundingSum = this.fundingCoins.reduce((s, f) => s + BigInt(f.value), 0n);
25008
- const surplus = BigInt(coin.value) + fundingSum - outputsSum;
25278
+ const surplus = coinsSum + fundingSum - outputsSum;
25009
25279
  if (surplus < 0n) {
25010
25280
  throw new Error(
25011
- `ArkadeTransactionBuilder: insufficient inputs \u2014 outputs ${outputsSum} exceed inputs ${BigInt(coin.value) + fundingSum}`
25281
+ `ArkadeTransactionBuilder: insufficient inputs \u2014 outputs ${outputsSum} exceed inputs ${coinsSum + fundingSum}`
25012
25282
  );
25013
25283
  }
25014
25284
  if (surplus > 0n) {
@@ -25021,23 +25291,23 @@ var ArkadeTransactionBuilder = class {
25021
25291
  }
25022
25292
  const { arkTx, checkpoints } = buildOffchainTx(
25023
25293
  [
25024
- {
25294
+ ...inputs.map(({ coin, unlocker }) => ({
25025
25295
  txid: coin.txid,
25026
25296
  vout: coin.vout,
25027
25297
  value: coin.value,
25028
- tapLeafScript: this.fn.tapLeafScript,
25029
- tapTree: this.contract.tapTree
25030
- },
25298
+ tapLeafScript: unlocker.fn.tapLeafScript,
25299
+ tapTree: unlocker.contract.tapTree
25300
+ })),
25031
25301
  ...this.fundingCoins
25032
25302
  ],
25033
25303
  outputs,
25034
- this.contract.client.checkpoint
25304
+ this.client.checkpoint
25035
25305
  );
25036
- if (coin.sourceTx) {
25037
- setArkPsbtField(arkTx, 0, PrevArkTxField, coin.sourceTx);
25038
- }
25039
- if (this.fn.arkadeScript) {
25040
- const indexer = this.contract.client.indexer;
25306
+ inputs.forEach(({ coin }, i) => {
25307
+ if (coin.sourceTx) setArkPsbtField(arkTx, i, PrevArkTxField, coin.sourceTx);
25308
+ });
25309
+ if (inputs.some(({ unlocker }) => unlocker.fn.arkadeScript)) {
25310
+ const indexer = this.client.indexer;
25041
25311
  if (!indexer) {
25042
25312
  throw new PrevTxUnavailableError(
25043
25313
  "covenant spends require an `indexer` on the Arkade client to resolve the previous ark tx of each input"
@@ -25045,56 +25315,76 @@ var ArkadeTransactionBuilder = class {
25045
25315
  }
25046
25316
  await attachPrevArkTxs(
25047
25317
  arkTx,
25048
- [coin.txid, ...this.fundingCoins.map((c) => c.txid)],
25318
+ [...inputs.map(({ coin }) => coin), ...this.fundingCoins].map((c) => c.txid),
25049
25319
  indexer
25050
25320
  );
25051
25321
  }
25052
- const condition = (def.tapscript.witness ?? []).map((w) => this.witnessBytes(w));
25053
- if (condition.length > 0) {
25054
- setArkPsbtField(arkTx, 0, ConditionWitness, condition);
25055
- setArkPsbtField(checkpoints[0], 0, ConditionWitness, condition);
25056
- }
25322
+ inputs.forEach(({ unlocker }, i) => {
25323
+ const condition = (unlocker.fn.def.tapscript.witness ?? []).map(
25324
+ (w) => witnessBytes(unlocker, w)
25325
+ );
25326
+ if (condition.length === 0) return;
25327
+ setArkPsbtField(arkTx, i, ConditionWitness, condition);
25328
+ setArkPsbtField(checkpoints[i], 0, ConditionWitness, condition);
25329
+ });
25057
25330
  const packets = [];
25058
25331
  if (this.assetSpecs.length > 0) {
25059
25332
  packets.push(this.buildAssetPacket());
25060
25333
  }
25061
- const arkadeScript = this.fn.arkadeScript;
25062
- if (arkadeScript) {
25063
- const stack = (def.arkadeScript?.witness ?? []).map((w) => this.witnessBytes(w));
25064
- packets.push(
25065
- EmulatorPacket.create([
25066
- { vin: 0, script: arkadeScript, witness: RawWitness.encode(stack) }
25067
- ])
25068
- );
25334
+ const entries = inputs.flatMap(({ unlocker }, vin) => {
25335
+ const { arkadeScript, def } = unlocker.fn;
25336
+ if (!arkadeScript) return [];
25337
+ const stack = (def.arkadeScript?.witness ?? []).map((w) => witnessBytes(unlocker, w));
25338
+ return [{ vin, script: arkadeScript, witness: RawWitness.encode(stack) }];
25339
+ });
25340
+ if (entries.length > 0) {
25341
+ packets.push(EmulatorPacket.create(entries));
25069
25342
  }
25070
25343
  packets.push(...this.extraPackets);
25071
25344
  if (packets.length > 0) {
25072
25345
  attachExtension(arkTx, packets);
25073
25346
  }
25074
- return { arkTx, checkpoints };
25347
+ return { arkTx, checkpoints, inputs };
25075
25348
  }
25076
25349
  /** Build, submit and return the finalized transaction. */
25077
25350
  async send() {
25078
- if (this.fn.def.tapscript.signers.some(
25079
- (signer) => typeof signer === "object" && "tweak" in signer
25080
- )) {
25081
- throw new Error(
25082
- `${this.fn.name}: covenant-tweaked leaf signing is not supported by Identity; use a signer that supports the covenant tweak with build()`
25083
- );
25351
+ const client = this.client;
25352
+ const userKey = client.userKey;
25353
+ for (const { contract, fn } of this.unlockers()) {
25354
+ if (fn.def.tapscript.signers.some(
25355
+ (signer) => typeof signer === "object" && "tweak" in signer
25356
+ )) {
25357
+ throw new Error(
25358
+ `${fn.name}: covenant-tweaked leaf signing is not supported by Identity; use a signer that supports the covenant tweak with build()`
25359
+ );
25360
+ }
25361
+ const { serverKey } = contract.keys;
25362
+ if (fn.signerKeys.some(
25363
+ (key) => !equalBytes(key, serverKey) && (!userKey || !equalBytes(key, userKey))
25364
+ )) {
25365
+ throw new Error(
25366
+ `${fn.name}: external leaf signatures are required; use build() and sign the PSBT with all required signers`
25367
+ );
25368
+ }
25084
25369
  }
25085
- const { serverKey } = this.contract.keys;
25086
- const userKey = this.contract.client.userKey;
25087
- if (this.fn.signerKeys.some(
25088
- (key) => !equalBytes(key, serverKey) && (!userKey || !equalBytes(key, userKey))
25089
- )) {
25370
+ const { arkTx, checkpoints, inputs } = await this.assemble();
25371
+ const userInputs = this.userInputIndexes(inputs);
25372
+ const serverOnly = inputs.find(
25373
+ ({ unlocker }, i) => !unlocker.fn.arkadeScript && !userInputs.includes(i)
25374
+ );
25375
+ if (serverOnly) {
25090
25376
  throw new Error(
25091
- `${this.fn.name}: external leaf signatures are required; use build() and sign the PSBT with all required signers`
25377
+ `${serverOnly.unlocker.fn.name}: a leaf without a covenant needs the user's signature, and this one has only the server's`
25092
25378
  );
25093
25379
  }
25094
- const { arkTx, checkpoints } = await this.build();
25095
- const client = this.contract.client;
25096
- const userInputs = this.userInputIndexes();
25097
- if (this.fn.arkadeScript) {
25380
+ inputs.forEach(({ coin }, vin) => {
25381
+ if (coin.assets?.length && !this.routesAssetsFrom(vin)) {
25382
+ throw new Error(
25383
+ `coin ${coin.txid}:${coin.vout} carries assets; route them with .withAsset() from input ${vin}`
25384
+ );
25385
+ }
25386
+ });
25387
+ if (inputs.some(({ unlocker }) => unlocker.fn.arkadeScript)) {
25098
25388
  if (!client.emulator) {
25099
25389
  throw new Error("covenant spends require an `emulator` on the Arkade client");
25100
25390
  }
@@ -25119,9 +25409,14 @@ var ArkadeTransactionBuilder = class {
25119
25409
  assertSubmittedArkTxid(res, signedArk, "submitTx");
25120
25410
  const matched = matchServerCheckpoints(res.signedCheckpointTxs, checkpoints, "submitTx");
25121
25411
  const finalCps = await Promise.all(
25122
- matched.map(
25123
- async ({ server }) => base64.encode((await client.identity.sign(server, [0])).toPSBT())
25124
- )
25412
+ matched.map(async ({ server, local }) => {
25413
+ const signed = await client.identity.sign(server, [0]);
25414
+ const [condition] = getArkPsbtFields(local, 0, ConditionWitness);
25415
+ if (condition && getArkPsbtFields(signed, 0, ConditionWitness).length === 0) {
25416
+ setArkPsbtField(signed, 0, ConditionWitness, condition);
25417
+ }
25418
+ return base64.encode(signed.toPSBT());
25419
+ })
25125
25420
  );
25126
25421
  await ark.finalizeTx(res.arkTxid, finalCps);
25127
25422
  return {
@@ -25133,31 +25428,44 @@ var ArkadeTransactionBuilder = class {
25133
25428
  /** Sign the client-owned inputs of the ark tx (no-op when there are none). */
25134
25429
  async signArk(arkTx, userInputs) {
25135
25430
  if (userInputs.length === 0) return arkTx;
25136
- const { identity } = this.contract.client;
25431
+ const { identity } = this.client;
25137
25432
  if (!identity) {
25138
25433
  throw new Error("this spend requires an `identity` to sign its user/funding inputs");
25139
25434
  }
25140
25435
  return identity.sign(arkTx, userInputs);
25141
25436
  }
25142
- /** Indexes of inputs the client owns and must sign (contract input + funded inputs). */
25143
- userInputIndexes() {
25144
- const idxs = [];
25145
- const userKey = this.contract.client.userKey;
25146
- if (userKey && this.fn.signerKeys.some((k) => equalBytes(k, userKey))) {
25147
- idxs.push(0);
25148
- }
25149
- for (let i = 0; i < this.fundingCoins.length; i++) {
25150
- idxs.push(i + 1);
25151
- }
25437
+ /** The functions this spend goes through: the `.from()` ones, else this builder's own. */
25438
+ unlockers() {
25439
+ if (this.inputs.length > 0) return this.inputs.map((i) => i.unlocker);
25440
+ return this.unlocker ? [this.unlocker] : [];
25441
+ }
25442
+ /** Indexes of inputs the client owns and must sign (contract inputs + funded inputs). */
25443
+ userInputIndexes(inputs) {
25444
+ const userKey = this.client.userKey;
25445
+ const idxs = inputs.flatMap(
25446
+ ({ unlocker }, i) => userKey && unlocker.fn.signerKeys.some((k) => equalBytes(k, userKey)) ? [i] : []
25447
+ );
25448
+ for (let i = 0; i < this.fundingCoins.length; i++) idxs.push(inputs.length + i);
25152
25449
  return idxs;
25153
25450
  }
25154
- async selectCoin(amount) {
25155
- const utxos = await this.contract.getUtxos();
25451
+ async selectCoin(unlocker, amount) {
25452
+ const now = Date.now();
25453
+ const routed = this.routesAssetsFrom(0);
25454
+ const utxos = (await unlocker.contract.getUtxos()).filter(
25455
+ (u) => !unspendable(u, now) && (routed || !u.assets?.length)
25456
+ );
25156
25457
  if (utxos.length === 0) throw new Error("no spendable coins for this contract");
25157
25458
  const covering = utxos.filter((u) => BigInt(u.value) >= amount).sort((a, b) => a.value - b.value);
25158
25459
  if (covering.length > 0) return covering[0];
25159
25460
  return [...utxos].sort((a, b) => b.value - a.value)[0];
25160
25461
  }
25462
+ /**
25463
+ * Whether input `vin`'s assets are routed: by a `.withAsset()` input, or a caller's asset packet.
25464
+ * A caller's packet counts for every input, unread; arkd checks what it actually routes.
25465
+ */
25466
+ routesAssetsFrom(vin) {
25467
+ return this.extraPackets.some((p) => p.type() === Packet.PACKET_TYPE) || this.assetSpecs.some((spec) => spec.inputs.some((i) => i.vin === vin));
25468
+ }
25161
25469
  buildAssetPacket() {
25162
25470
  const groups = this.assetSpecs.map((s) => {
25163
25471
  const id = typeof s.assetId === "string" ? AssetId.fromString(s.assetId) : AssetId.fromBytes(s.assetId);
@@ -25171,10 +25479,15 @@ var ArkadeTransactionBuilder = class {
25171
25479
  });
25172
25480
  return Packet.create(groups);
25173
25481
  }
25174
- witnessBytes(ref) {
25175
- return witnessRefToBytes(ref, this.args, this.contract.args);
25176
- }
25177
25482
  };
25483
+ function unspendable(coin, now) {
25484
+ if (coin.isSwept) return "was swept";
25485
+ if (coin.expiresAt && now >= new Date(coin.expiresAt).getTime()) return "has expired";
25486
+ return void 0;
25487
+ }
25488
+ function witnessBytes({ contract, args }, ref) {
25489
+ return witnessRefToBytes(ref, args, contract.args);
25490
+ }
25178
25491
  function bindInputs(fn, callArgs) {
25179
25492
  const inputs = fn.def.inputs ?? [];
25180
25493
  if (callArgs.length !== inputs.length) {
@@ -25674,5 +25987,5 @@ function createDefaultPaymentRouter(wallet) {
25674
25987
  }
25675
25988
 
25676
25989
  export { ARKADE_MAGIC, ARKADE_SALTED_PREIMAGE_TAG, ARKADE_SWAP_PREIMAGE_TAG, ASSET_CARRIER_SATS, AbortError, ActivityRegistry, AmbiguousRouteError, ArkNote, Arkade, ArkadeCash, ArkadeCashCreateError, ArkadeCashOperatorMismatchError, AssetManager, BIP21, BIP21Error, BIP322, BLOCKED_UPGRADE_TIMEOUT_MS, Batch, CachingArkProvider, ChainedTxType, ConnectionDisposedError, ContractManager, ContractWatcher, DEFAULT_MESSAGE_TIMEOUTS, DEFAULT_MIN_BATCH_EXPIRY_SECONDS, DatabaseUpgradeBlockedError, DelegateManagerImpl, DelegateNotConfiguredError, 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, KeyringDescriptorProvider, MAX_PAGE_SIZE, MAX_USED_SIGNING_DESCRIPTORS_LOOK_AHEAD, MESSAGE_BUS_INITIALIZING, MESSAGE_BUS_NOT_INITIALIZED, MessageBus, MessageBusInitializingError, MessageBusNotInitializedError, MissingSigningDescriptorError, MnemonicIdentity, OnchainWallet, OversizedChangeError, P2A, PaymentRouter, PrevTxUnavailableError, REGTEST_MIN_BATCH_EXPIRY_SECONDS, Ramps, ReadonlyAssetManager, ReadonlyDescriptorIdentity, ReadonlySingleKey, ReadonlyWallet, ReadonlyWalletError, RestDelegateProvider, RestEmulatorProvider, SeedIdentity, SendDeadlineExceededError, ServiceWorkerReadonlyWallet, ServiceWorkerTimeoutError, ServiceWorkerWallet, SingleKey, StaticDescriptorProvider, TxTree, TxType, TxWeightEstimator, UnannotatableInputError, UnilateralExit, UnknownPacket, Unroll, VtxoManager, VtxoReservedError, Wallet2 as Wallet, WalletCannotAllocateAddressError, WalletCannotSignError, WalletMessageHandler, WalletNotInitializedError, WsElectrumChainSource, adoptContractDescriptor, arkAssetRail, arkRail, arkTarget, arkade_exports, assertFinalCommitmentMatchesValidated, assertHistoryPageFilter, assertIntentIdUnique, assertNoAssets, assertNoSelection, assertPageRequest, assertRecipientArkadeAddress, assertSendDeadline, assertSendableAmount, assertSubmittedArkTxid, assertValidBatchExpiry, assertValidServerUnrollScript, assetMintResolver, assetsOf, attachPrevArkTxs, attachPrevoutTxs, awaitTransaction, boardingResolver, btcTarget, buildForfeitTx, buildOffchainTx, buildPreimageMessage, buildSaltedPreimageMessage, captureSendDeadline, checkSaveVtxosForScript, claimWithPreimageIdentity, classifyAgainstSignerSet, classifyContractSigner, closeDatabase, collabExitResolver, collectContracts, collectIntents, collectPages, collectScriptVtxos, collectTransactionHistory, collectUtxos, collectVtxos, combineTapscriptSigs, compareHistoryCursors, contractFilterMatchesNothing, contractFromArkContract, contractFromArkContractWithAddress, contractPreimage, contractSigner, createAssetPacket, createDefaultActivityRegistry, createDefaultPaymentRouter, createExitChainResolver, createManagedConnection, createdAtToIso, decodeArkContract, defaultBatchExpiryPolicy, deleteByIndex, deserializeAssets, deserializeExitPackage, deserializeUtxo, deserializeVtxo, encodeArkContract, exitObserverFor, extendVirtualCoinForContract, extractArkProviderUrl, gateExclusion, gatedContracts, getAllByIndexValues, getRandomId, hasBoardingTxExpired, hasCandidates, intentMatchesFilter, invoiceTarget, isAddressAllocationCapable, isArkContract, isBatchSignable, isBtcAddress, isContractGenericallySpendable, isContractVtxoEvent, isCooperativelyMigratable, isDeterministicSigner, isDiscoverable, isHDAllocationCapable, isHDDeterministicSignCapable, isHDWalletCapable, isLightningInvoice, isLnurl, isMessageBusInitializingError, isMessageBusNotInitializedError, isPerArtifactDescriptor, isSigningIdentity, isSubdust, isTerminalIntentState, isValidArkAddress, isVtxoExpiringSoon, iteratePages, legacyVtxoFacts, logExcludedVtxos, makeHandle, matchServerCheckpoints, mergeChainedTxType, notifyExitObserved, onchainRail, openDatabase, outpointExclusion, outpointReasons, pageResult, promisifyRequest, provisionClaimSecret, provisionRefundKey, registerWalletRestoreHook, resolveAssetAmount, resolveBatchExpiryPolicy, resolveDescriptorSigner, resolvePrevTxs, resolveSendAmount, saveVtxosForContract, scriptFromArkAddress, selectCoinsWithAsset, selectVirtualCoins, selectionOf, serializeAssets, serializeExitPackage, serializeUtxo, serializeVtxo, setupServiceWorker, signAndSubmitOffchainTx, signerSetFromInfo, signingDescriptorIndex, strictSigningDescriptorIndex, submitViaEmulator, toXOnlySignerHex, tryResolveSendAmount, utxoEntries, validateBatchRecipients, validateConnectorsTxGraph, validateVtxoTxGraph, verifyTapscriptSignatures, waitForIncomingFunds, walletFeeSource, warnAndFilterVtxosForScript, withPrevTxs };
25677
- //# sourceMappingURL=chunk-GWJWXVUB.js.map
25678
- //# sourceMappingURL=chunk-GWJWXVUB.js.map
25990
+ //# sourceMappingURL=chunk-AJWXVQYG.js.map
25991
+ //# sourceMappingURL=chunk-AJWXVQYG.js.map