@arkade-os/sdk 0.5.1 → 0.5.3

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 (76) hide show
  1. package/README.md +9 -5
  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-CdWheMqT.d.ts → asyncStorageTaskQueue-B3U5gQ-i.d.ts} +1 -1
  7. package/dist/{asyncStorageTaskQueue-DFt5LgW5.d.cts → asyncStorageTaskQueue-C9PkPcW_.d.cts} +1 -1
  8. package/dist/{chunk-JJ6QKTFZ.cjs → chunk-5WRA66RV.cjs} +10 -10
  9. package/dist/{chunk-JJ6QKTFZ.cjs.map → chunk-5WRA66RV.cjs.map} +1 -1
  10. package/dist/{chunk-6MIGYYWB.js → chunk-6FL4N4S2.js} +3 -3
  11. package/dist/{chunk-6MIGYYWB.js.map → chunk-6FL4N4S2.js.map} +1 -1
  12. package/dist/{chunk-GW42FS2X.cjs → chunk-BYPBVHVA.cjs} +48 -48
  13. package/dist/{chunk-GW42FS2X.cjs.map → chunk-BYPBVHVA.cjs.map} +1 -1
  14. package/dist/{chunk-NKKJEA3Z.js → chunk-HAQAWBYG.js} +739 -392
  15. package/dist/chunk-HAQAWBYG.js.map +1 -0
  16. package/dist/{chunk-XLHUGP3G.cjs → chunk-KW7ZEWYS.cjs} +6 -3
  17. package/dist/chunk-KW7ZEWYS.cjs.map +1 -0
  18. package/dist/{chunk-QFB55EOZ.js → chunk-OPX2HRWM.js} +6 -3
  19. package/dist/chunk-OPX2HRWM.js.map +1 -0
  20. package/dist/{chunk-QKNSMVBW.cjs → chunk-OUTWUYJ4.cjs} +1222 -875
  21. package/dist/chunk-OUTWUYJ4.cjs.map +1 -0
  22. package/dist/{chunk-3M22PJVB.js → chunk-P7B4LQ3H.js} +3 -3
  23. package/dist/{chunk-3M22PJVB.js.map → chunk-P7B4LQ3H.js.map} +1 -1
  24. package/dist/{chunk-FXVEOEYB.js → chunk-T7C3E2HL.js} +4 -4
  25. package/dist/{chunk-FXVEOEYB.js.map → chunk-T7C3E2HL.js.map} +1 -1
  26. package/dist/{chunk-XQRIY5K5.js → chunk-Y4BL3YYX.js} +4 -4
  27. package/dist/{chunk-XQRIY5K5.js.map → chunk-Y4BL3YYX.js.map} +1 -1
  28. package/dist/{chunk-HQNQ3CLQ.cjs → chunk-ZMSM3NOH.cjs} +18 -18
  29. package/dist/{chunk-HQNQ3CLQ.cjs.map → chunk-ZMSM3NOH.cjs.map} +1 -1
  30. package/dist/{chunk-GQUFPTN7.cjs → chunk-ZPF4I3K2.cjs} +16 -16
  31. package/dist/{chunk-GQUFPTN7.cjs.map → chunk-ZPF4I3K2.cjs.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-CFCfrReG.d.cts → delegate-ByfKszb6.d.cts} +1 -1
  37. package/dist/{delegate-BSbGQabI.d.ts → delegate-Cd28gxpM.d.ts} +1 -1
  38. package/dist/{index-CzUVvIxA.d.ts → index-B6X2E7W_.d.ts} +2 -2
  39. package/dist/{index-B3fRWO5i.d.cts → index-BURzbRup.d.cts} +38 -16
  40. package/dist/{index-B3fRWO5i.d.ts → index-BURzbRup.d.ts} +38 -16
  41. package/dist/{index-Fpf4mQZr.d.cts → index-DgH_0j14.d.cts} +2 -2
  42. package/dist/index.cjs +313 -313
  43. package/dist/index.d.cts +107 -27
  44. package/dist/index.d.ts +107 -27
  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-CdfoCMaw.d.ts → taskRunner-BaNCSxqK.d.ts} +1 -1
  55. package/dist/{taskRunner-Dk6oDAtd.d.cts → taskRunner-i56-drQV.d.cts} +1 -1
  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 +18 -15
  61. package/dist/wallet/expo/index.cjs.map +1 -1
  62. package/dist/wallet/expo/index.d.cts +5 -4
  63. package/dist/wallet/expo/index.d.ts +5 -4
  64. package/dist/wallet/expo/index.js +8 -5
  65. package/dist/wallet/expo/index.js.map +1 -1
  66. package/dist/{wallet-D9dvXu-K.d.cts → wallet-BjQugVUt.d.cts} +11 -6
  67. package/dist/{wallet-BQHNgc_v.d.ts → wallet-BnmhohgZ.d.ts} +11 -6
  68. package/dist/worker/expo/index.cjs +9 -9
  69. package/dist/worker/expo/index.d.cts +4 -4
  70. package/dist/worker/expo/index.d.ts +4 -4
  71. package/dist/worker/expo/index.js +5 -5
  72. package/package.json +1 -1
  73. package/dist/chunk-NKKJEA3Z.js.map +0 -1
  74. package/dist/chunk-QFB55EOZ.js.map +0 -1
  75. package/dist/chunk-QKNSMVBW.cjs.map +0 -1
  76. package/dist/chunk-XLHUGP3G.cjs.map +0 -1
@@ -1,6 +1,6 @@
1
- import { AssetId, AssetGroup, AssetInput, AssetOutput, Metadata, Packet, RestIndexerProvider, BufferReader, BufferWriter, AssetRef } from './chunk-6MIGYYWB.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-3M22PJVB.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-QFB55EOZ.js';
1
+ import { Packet, AssetId, AssetGroup, AssetInput, AssetOutput, Metadata, RestIndexerProvider, BufferReader, BufferWriter, AssetRef } from './chunk-6FL4N4S2.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-P7B4LQ3H.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, assertAllowedSighashTypes, OP_RETURN_EMPTY_PKSCRIPT, canSweepOnchain, sequenceToTimelock, DEFAULT_ARKADE_SERVER_URL, baseFetch, DEFAULT_NETWORK_NAME, sequenceToCanonicalTimelock, assertTimerMs, isEventSourceError, isEventSourceUnavailableError, normalizeVtxo, isPastExpiry, decodeTapscript, scriptFromTapLeafScript, CLTVMultisigTapscript, VtxoTaprootTree, formatSighash, isArkError, ArkErrorName, Intent, rateGate, 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-OPX2HRWM.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,
@@ -3826,32 +3833,36 @@ function assertCheckpointsMatchInputs(checkpoints, inputs, unrollCandidates, con
3826
3833
  }
3827
3834
  }
3828
3835
  async function submitOffchainTx(provider, offchainTx, signer, hooks, options) {
3829
- const { arkTx: signedArkTx, userSignedCheckpoints } = await signer.signArkTx(
3836
+ const oneShot = options?.oneShot !== false;
3837
+ const { arkTx: signedArkTx, userSignedCheckpoints: batchSigned } = await signer.signArkTx(
3830
3838
  offchainTx.arkTx,
3831
3839
  offchainTx.checkpoints
3832
3840
  );
3833
- if (userSignedCheckpoints && userSignedCheckpoints.length !== offchainTx.checkpoints.length) {
3841
+ if (batchSigned && batchSigned.length !== offchainTx.checkpoints.length) {
3834
3842
  throw new Error(
3835
- `signer returned ${userSignedCheckpoints.length} signed checkpoints, expected ${offchainTx.checkpoints.length}`
3843
+ `signer returned ${batchSigned.length} signed checkpoints, expected ${offchainTx.checkpoints.length}`
3836
3844
  );
3837
3845
  }
3846
+ const userSignedCheckpoints = (batchSigned ?? (oneShot ? await Promise.all(offchainTx.checkpoints.map((c) => signer.signCheckpoint(c.clone()))) : void 0))?.map((c) => withArkInputWitness(c, signedArkTx));
3838
3847
  await hooks?.beforeSubmit?.();
3839
3848
  const response = await provider.submitTx(
3840
3849
  base64.encode(signedArkTx.toPSBT()),
3841
- offchainTx.checkpoints.map((c) => base64.encode(c.toPSBT()))
3850
+ (oneShot ? userSignedCheckpoints : offchainTx.checkpoints.map((c) => withArkInputWitness(c, signedArkTx))).map((c) => base64.encode(c.toPSBT()))
3842
3851
  );
3843
3852
  assertSubmittedArkTxid(response, signedArkTx, "submitTx");
3844
3853
  const { arkTxid, signedCheckpointTxs } = response;
3845
3854
  const matched = matchServerCheckpoints(signedCheckpointTxs, offchainTx.checkpoints, "submitTx");
3846
- matched.forEach(({ server }, index) => {
3847
- try {
3848
- assertAllowedSighashTypes(server);
3849
- } catch (error) {
3850
- throw new ServerResponseMismatchError(
3851
- `submitTx checkpoint ${index}: ${error instanceof Error ? error.message : String(error)}`
3852
- );
3853
- }
3854
- });
3855
+ if (!oneShot) {
3856
+ matched.forEach(({ server }, index) => {
3857
+ try {
3858
+ assertAllowedSighashTypes(server);
3859
+ } catch (error) {
3860
+ throw new ServerResponseMismatchError(
3861
+ `submitTx checkpoint ${index}: ${error instanceof Error ? error.message : String(error)}`
3862
+ );
3863
+ }
3864
+ });
3865
+ }
3855
3866
  const verify = options?.verifyServerSignatures;
3856
3867
  if (verify) {
3857
3868
  if (response.finalArkTx === void 0) {
@@ -3878,26 +3889,62 @@ async function submitOffchainTx(provider, offchainTx, signer, hooks, options) {
3878
3889
  ({ server, local }, index) => assertServerSignedLeaf(server, local, 0, serverPubkeys, `submitTx checkpoint ${index}`)
3879
3890
  );
3880
3891
  }
3892
+ const result = { arkTxid, finalArkTx: response.finalArkTx, signedCheckpointTxs };
3881
3893
  let finalCheckpoints;
3882
3894
  if (userSignedCheckpoints) {
3883
3895
  const userByTxid = new Map(
3884
3896
  userSignedCheckpoints.map((c, i) => [offchainTx.checkpoints[i].id, c])
3885
3897
  );
3898
+ if (oneShot && matched.every(
3899
+ ({ server, local }) => carriesTapscriptSigs(server, userByTxid.get(local.id))
3900
+ )) {
3901
+ await hooks?.afterFinalize?.();
3902
+ return result;
3903
+ }
3886
3904
  finalCheckpoints = matched.map(({ server, local }) => {
3887
- combineTapscriptSigs(userByTxid.get(local.id), server);
3905
+ const userSigned = userByTxid.get(local.id);
3906
+ combineTapscriptSigs(userSigned, server);
3907
+ carryConditionWitness(userSigned, server);
3888
3908
  return base64.encode(server.toPSBT());
3889
3909
  });
3890
3910
  } else {
3891
3911
  finalCheckpoints = await Promise.all(
3892
- matched.map(async ({ server }) => {
3912
+ matched.map(async ({ server, local }) => {
3893
3913
  const signed = await signer.signCheckpoint(server);
3914
+ carryConditionWitness(local, signed);
3894
3915
  return base64.encode(signed.toPSBT());
3895
3916
  })
3896
3917
  );
3897
3918
  }
3898
3919
  await provider.finalizeTx(arkTxid, finalCheckpoints);
3899
3920
  await hooks?.afterFinalize?.();
3900
- return { arkTxid, signedCheckpointTxs };
3921
+ return result;
3922
+ }
3923
+ function carryConditionWitness(ours, server) {
3924
+ if (getArkPsbtFields(server, 0, ConditionWitness).length > 0) return;
3925
+ const [witness] = getArkPsbtFields(ours, 0, ConditionWitness);
3926
+ if (witness) setArkPsbtField(server, 0, ConditionWitness, witness);
3927
+ }
3928
+ function withArkInputWitness(checkpoint, signedArkTx) {
3929
+ if (getArkPsbtFields(checkpoint, 0, ConditionWitness).length > 0) return checkpoint;
3930
+ for (let i = 0; i < signedArkTx.inputsLength; i++) {
3931
+ const { txid } = signedArkTx.getInput(i);
3932
+ if (!txid || hex.encode(txid) !== checkpoint.id) continue;
3933
+ const [witness] = getArkPsbtFields(signedArkTx, i, ConditionWitness);
3934
+ if (!witness) return checkpoint;
3935
+ const carried = checkpoint.clone();
3936
+ setArkPsbtField(carried, 0, ConditionWitness, witness);
3937
+ return carried;
3938
+ }
3939
+ return checkpoint;
3940
+ }
3941
+ function carriesTapscriptSigs(server, userSigned) {
3942
+ const entries = (tx) => (tx.getInput(0).tapScriptSig ?? []).map(
3943
+ ([{ pubKey, leafHash }, sig]) => hex.encode(pubKey) + hex.encode(leafHash) + hex.encode(sig)
3944
+ );
3945
+ const have = new Set(entries(server));
3946
+ const want = entries(userSigned);
3947
+ return want.length > 0 && want.every((entry) => have.has(entry));
3901
3948
  }
3902
3949
  async function signAndSubmitOffchainTx(params) {
3903
3950
  const offchainTx = buildOffchainTx(params.inputs, params.outputs, params.serverUnrollScript);
@@ -3906,7 +3953,8 @@ async function signAndSubmitOffchainTx(params) {
3906
3953
  signCheckpoint: (checkpoint) => params.identity.sign(checkpoint)
3907
3954
  };
3908
3955
  const { arkTxid } = await submitOffchainTx(params.provider, offchainTx, signer, void 0, {
3909
- verifyServerSignatures: params.verifyServerSignatures
3956
+ verifyServerSignatures: params.verifyServerSignatures,
3957
+ oneShot: params.oneShot
3910
3958
  });
3911
3959
  return arkTxid;
3912
3960
  }
@@ -4676,12 +4724,14 @@ var VtxoManager = class _VtxoManager {
4676
4724
  * ```
4677
4725
  */
4678
4726
  async recoverVtxos(eventCallback) {
4679
- const allVtxos = await this.wallet.getVtxos({
4680
- withRecoverable: true,
4681
- withUnrolled: false
4682
- });
4727
+ const [allVtxos, now] = await Promise.all([
4728
+ this.wallet.getVtxos({
4729
+ withRecoverable: true,
4730
+ withUnrolled: false
4731
+ }),
4732
+ fetchTimeHeight(this.wallet)
4733
+ ]);
4683
4734
  const dustAmount = getDustAmount(this.wallet);
4684
- const now = await fetchTimeHeight(this.wallet);
4685
4735
  let { vtxosToRecover } = getRecoverableWithSubdust(allVtxos, dustAmount, now);
4686
4736
  if (vtxosToRecover.length === 0) {
4687
4737
  throw new Error("No recoverable VTXOs found");
@@ -4782,12 +4832,17 @@ var VtxoManager = class _VtxoManager {
4782
4832
  * ```
4783
4833
  */
4784
4834
  async getRecoverableBalance() {
4785
- const allVtxos = await this.wallet.getVtxos({
4786
- withRecoverable: true,
4787
- withUnrolled: false
4788
- });
4835
+ const infoProvider = this.getInfoProvider();
4836
+ const [allVtxos, now, info, arkAddress] = await Promise.all([
4837
+ this.wallet.getVtxos({
4838
+ withRecoverable: true,
4839
+ withUnrolled: false
4840
+ }),
4841
+ fetchTimeHeight(this.wallet),
4842
+ infoProvider?.getInfo(),
4843
+ this.wallet.getAddress()
4844
+ ]);
4789
4845
  const dustAmount = getDustAmount(this.wallet);
4790
- const now = await fetchTimeHeight(this.wallet);
4791
4846
  let { vtxosToRecover, includesSubdust } = getRecoverableWithSubdust(
4792
4847
  allVtxos,
4793
4848
  dustAmount,
@@ -4805,14 +4860,9 @@ var VtxoManager = class _VtxoManager {
4805
4860
  now
4806
4861
  ));
4807
4862
  }
4808
- const info = await this.getInfoProvider()?.getInfo();
4809
4863
  const estimator = new Estimator(info?.fees.intentFee ?? {});
4810
4864
  const { payable, net } = priceSettlementInputs(vtxosToRecover, estimator);
4811
- const priced = deductOffchainOutputFee(
4812
- subtotalOf(payable, net),
4813
- estimator,
4814
- await this.wallet.getAddress()
4815
- );
4865
+ const priced = deductOffchainOutputFee(subtotalOf(payable, net), estimator, arkAddress);
4816
4866
  const recoverable = priced > 0n ? priced : 0n;
4817
4867
  const subdustAmount = subtotalOf(
4818
4868
  payable.filter((v) => BigInt(v.value) < dustAmount),
@@ -4866,8 +4916,11 @@ var VtxoManager = class _VtxoManager {
4866
4916
  const cm = await wallet.getContractManager();
4867
4917
  const recoveryScripts = (await cm.getContracts()).filter(isRecoveryOnlyContract).map((contract) => contract.script);
4868
4918
  if (recoveryScripts.length === 0) return;
4869
- const recoveryContracts = await cm.getContractsWithVtxos({ script: recoveryScripts });
4870
- const info = await this.getInfoProvider()?.getInfo();
4919
+ const infoProvider = this.getInfoProvider();
4920
+ const [recoveryContracts, info] = await Promise.all([
4921
+ cm.getContractsWithVtxos({ script: recoveryScripts }),
4922
+ infoProvider?.getInfo()
4923
+ ]);
4871
4924
  const dustAmount = getDustAmount(wallet);
4872
4925
  for (const { contract, vtxos } of recoveryContracts) {
4873
4926
  try {
@@ -4962,12 +5015,13 @@ var VtxoManager = class _VtxoManager {
4962
5015
  if (this.settlementConfig === false && thresholdMs === void 0) {
4963
5016
  return [];
4964
5017
  }
4965
- const vtxos = await this.wallet.getSpendableVtxos({
5018
+ const threshold = thresholdMs !== void 0 ? thresholdMs : this.configuredThresholdMs();
5019
+ const vtxosPromise = this.wallet.getSpendableVtxos({
4966
5020
  withRecoverable: true,
4967
5021
  genericallySpendableOnly: true
4968
5022
  });
4969
- const threshold = thresholdMs !== void 0 ? thresholdMs : this.configuredThresholdMs();
4970
5023
  const tip = now ?? await fetchTimeHeight(this.wallet);
5024
+ const vtxos = await vtxosPromise;
4971
5025
  await this.warnHeldExpiring(threshold, tip);
4972
5026
  return getExpiringAndRecoverableVtxos(vtxos, threshold, getDustAmount(this.wallet), tip);
4973
5027
  }
@@ -5261,8 +5315,10 @@ var VtxoManager = class _VtxoManager {
5261
5315
  const wallet = this.requireMigrationCapableWallet();
5262
5316
  const info = await wallet.arkProvider.getInfo();
5263
5317
  await wallet.settleServerInfoChanges?.();
5264
- const { reports: vtxoReports } = await this.classifyDeprecatedSignerContracts(info);
5265
- const { reports: boardingReports } = await this.classifyDeprecatedSignerBoarding(info);
5318
+ const [{ reports: vtxoReports }, { reports: boardingReports }] = await Promise.all([
5319
+ this.classifyDeprecatedSignerContracts(info),
5320
+ this.classifyDeprecatedSignerBoarding(info)
5321
+ ]);
5266
5322
  return mergeSignerReports(vtxoReports, boardingReports);
5267
5323
  }
5268
5324
  /**
@@ -5284,8 +5340,10 @@ var VtxoManager = class _VtxoManager {
5284
5340
  return { rotated: rotatedOnRefresh, expired: [], signers: [] };
5285
5341
  }
5286
5342
  if (walletClass.status === "UNKNOWN_SIGNER") {
5287
- const { reports: vtxoReports2 } = await this.classifyDeprecatedSignerContracts(info);
5288
- const { reports: boardingReports2 } = await this.classifyDeprecatedSignerBoarding(info);
5343
+ const [{ reports: vtxoReports2 }, { reports: boardingReports2 }] = await Promise.all([
5344
+ this.classifyDeprecatedSignerContracts(info),
5345
+ this.classifyDeprecatedSignerBoarding(info)
5346
+ ]);
5289
5347
  return {
5290
5348
  // True only if a second rotation landed inside the drain, leaving us on a signer
5291
5349
  // newer than `info` (a drained rotation otherwise classifies CURRENT).
@@ -5296,16 +5354,13 @@ var VtxoManager = class _VtxoManager {
5296
5354
  };
5297
5355
  }
5298
5356
  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);
5357
+ const [
5358
+ { reports: vtxoReports, migratable: vtxoMigratable, expired: vtxoExpired },
5359
+ { reports: boardingReports, migratable: boardingMigratable, expired: boardingExpired }
5360
+ ] = await Promise.all([
5361
+ this.classifyDeprecatedSignerContracts(info),
5362
+ this.classifyDeprecatedSignerBoarding(info)
5363
+ ]);
5309
5364
  const reports = mergeSignerReports(vtxoReports, boardingReports);
5310
5365
  const expiredRefs = [
5311
5366
  ...vtxoExpired.map(classifiedToRef),
@@ -6304,11 +6359,6 @@ async function classifyIntent(intent, vtxosByOutpoint, now) {
6304
6359
  cancellationReason: "reconciliation: an input was unilaterally exited"
6305
6360
  });
6306
6361
  }
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
6362
  if (intent.validUntil !== void 0 && intent.validUntil < now) {
6313
6363
  return terminal(intent, "cancelled", now, {
6314
6364
  cancellationReason: "reconciliation: intent expired"
@@ -6510,9 +6560,16 @@ function chainTxTypeToChainedExit(t) {
6510
6560
  var isCommitment = (t) => t === "INDEXER_CHAINED_TX_TYPE_COMMITMENT" /* COMMITMENT */ || t === "INDEXER_CHAINED_TX_TYPE_UNSPECIFIED" /* UNSPECIFIED */;
6511
6561
  async function buildExitDag(params) {
6512
6562
  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 });
6563
+ const chains = await Promise.allSettled(
6564
+ params.vtxos.map(async (vtxo) => ({
6565
+ outpoint: `${vtxo.txid}:${vtxo.vout}`,
6566
+ // The resolver returns the full chain (all pages merged, local-first).
6567
+ chain: await params.chain.getVtxoChain({ txid: vtxo.txid, vout: vtxo.vout })
6568
+ }))
6569
+ );
6570
+ for (const result of chains) {
6571
+ if (result.status === "rejected") throw result.reason;
6572
+ const { outpoint, chain } = result.value;
6516
6573
  for (const chainTx of chain) {
6517
6574
  const existing = byTxid.get(chainTx.txid);
6518
6575
  if (existing) {
@@ -6545,25 +6602,26 @@ async function buildExitDag(params) {
6545
6602
  if (order.length !== pending.size) {
6546
6603
  throw new Error("inconsistent vtxo chain from indexer (cycle detected)");
6547
6604
  }
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
- }
6605
+ const confirmed = await Promise.all(
6606
+ order.map(async (tx) => {
6607
+ try {
6608
+ const status = await params.onchain.getTxStatus(tx.txid);
6609
+ return status.confirmed;
6610
+ } catch {
6611
+ return false;
6612
+ }
6613
+ })
6614
+ );
6615
+ return order.map((tx, i) => {
6557
6616
  const expires = Number(tx.expiresAt);
6558
- nodes.push({
6617
+ return {
6559
6618
  txid: tx.txid,
6560
6619
  type: tx.type,
6561
6620
  expiresAt: Number.isFinite(expires) && expires > 0 ? expires : void 0,
6562
6621
  forVtxos: [...tx.forVtxos].sort(),
6563
- confirmed
6564
- });
6565
- }
6566
- return nodes;
6622
+ confirmed: confirmed[i]
6623
+ };
6624
+ });
6567
6625
  }
6568
6626
  function topoSortByDeps(items, idOf, depsOf) {
6569
6627
  const produced = new Set(items.map(idOf));
@@ -6732,11 +6790,12 @@ var RepositoryExitDataSource = class {
6732
6790
  return chain;
6733
6791
  }
6734
6792
  async getVirtualTxs(txids) {
6793
+ const stored = await Promise.all(txids.map((txid) => this.repo.getVirtualTx(txid)));
6735
6794
  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
- }
6795
+ txids.forEach((txid, i) => {
6796
+ const row = stored[i];
6797
+ if (row?.psbt) out.set(txid, row.psbt);
6798
+ });
6740
6799
  return out;
6741
6800
  }
6742
6801
  };
@@ -7854,8 +7913,10 @@ var DelegateManagerImpl = class {
7854
7913
  return { delegated: [], failed: [] };
7855
7914
  }
7856
7915
  const destinationScript = ArkAddress.decode(destination).pkScript;
7857
- const arkInfo = await this.arkInfoProvider.getInfo();
7858
- const delegateInfo = await this.delegateProvider.getDelegateInfo();
7916
+ const [arkInfo, delegateInfo] = await Promise.all([
7917
+ this.arkInfoProvider.getInfo(),
7918
+ this.delegateProvider.getDelegateInfo()
7919
+ ]);
7859
7920
  const eligible = vtxos.map(normalizeVtxo).filter(
7860
7921
  (v) => isAnnotated(v) && findDelegateTapLeaf(v, delegateInfo.pubkey) !== void 0
7861
7922
  );
@@ -10646,8 +10707,10 @@ var ContractManager = class _ContractManager {
10646
10707
  return contract !== void 0 && contractHandlers.get(contract.type)?.assertSpendableNow !== void 0;
10647
10708
  });
10648
10709
  if (asking.length === 0) return refused;
10649
- const tip = await this.currentChainTip();
10650
- const walletDescriptorValue = await walletDescriptor?.();
10710
+ const [tip, walletDescriptorValue] = await Promise.all([
10711
+ this.currentChainTip(),
10712
+ walletDescriptor?.()
10713
+ ]);
10651
10714
  for (const vtxo of asking) {
10652
10715
  const contract = byScript.get(vtxo.script);
10653
10716
  const handler = contractHandlers.get(contract.type);
@@ -12548,7 +12611,10 @@ var WalletReceiveRotator = class _WalletReceiveRotator {
12548
12611
  * watched; it just stops being advertised). The untagged index-0 baseline is never retired.
12549
12612
  */
12550
12613
  async rotate(wallet) {
12551
- const descriptor = await this.provider.getNextSigningDescriptor();
12614
+ const descriptorPromise = this.provider.getNextSigningDescriptor();
12615
+ const managerPromise = wallet.getContractManager();
12616
+ void managerPromise.catch(() => void 0);
12617
+ const descriptor = await descriptorPromise;
12552
12618
  const { tapscript: newTapscript, params } = buildReceiveContract(
12553
12619
  wallet.offchainTapscript,
12554
12620
  descriptor,
@@ -12556,7 +12622,7 @@ var WalletReceiveRotator = class _WalletReceiveRotator {
12556
12622
  true
12557
12623
  );
12558
12624
  const newScript = params.script;
12559
- const manager = await wallet.getContractManager();
12625
+ const manager = await managerPromise;
12560
12626
  await manager.createContract(params);
12561
12627
  wallet.setOffchainTapscriptForRotation(newTapscript);
12562
12628
  const previousTagged = this.currentTaggedScript;
@@ -12666,11 +12732,11 @@ function computeOffchainBalance(vtxos, caps) {
12666
12732
  let settled = 0;
12667
12733
  let preconfirmed = 0;
12668
12734
  let available = 0;
12669
- let gated = 0;
12735
+ let contractLocked = 0;
12670
12736
  let pendingSettlement = 0;
12671
12737
  let reserved = 0;
12672
12738
  let recoverable = 0;
12673
- let pendingRecovery = 0;
12739
+ let deprecatedSignerLocked = 0;
12674
12740
  let unrolled = 0;
12675
12741
  const owned = /* @__PURE__ */ new Map();
12676
12742
  const spendable = /* @__PURE__ */ new Map();
@@ -12687,7 +12753,7 @@ function computeOffchainBalance(vtxos, caps) {
12687
12753
  continue;
12688
12754
  }
12689
12755
  if (isPendingRecovery(vtxo)) {
12690
- pendingRecovery += vtxo.value;
12756
+ deprecatedSignerLocked += vtxo.value;
12691
12757
  continue;
12692
12758
  }
12693
12759
  if (canRecoverOnchain(vtxo, now)) {
@@ -12701,7 +12767,7 @@ function computeOffchainBalance(vtxos, caps) {
12701
12767
  settled += vtxo.value;
12702
12768
  }
12703
12769
  if (!isGenericallySpendable(vtxo)) {
12704
- gated += vtxo.value;
12770
+ contractLocked += vtxo.value;
12705
12771
  } else if (!isUnlocked(vtxo)) {
12706
12772
  pendingSettlement += vtxo.value;
12707
12773
  } else if (isReserved?.(vtxo)) {
@@ -12719,13 +12785,13 @@ function computeOffchainBalance(vtxos, caps) {
12719
12785
  settled,
12720
12786
  preconfirmed,
12721
12787
  available,
12722
- gated,
12788
+ contractLocked,
12723
12789
  pendingSettlement,
12724
12790
  reserved,
12725
12791
  recoverable,
12726
- pendingRecovery,
12792
+ deprecatedSignerLocked,
12727
12793
  unrolled,
12728
- total: settled + preconfirmed + recoverable + pendingRecovery + unrolled,
12794
+ total: settled + preconfirmed + recoverable + deprecatedSignerLocked + unrolled,
12729
12795
  assets: toAssets(owned),
12730
12796
  availableAssets: toAssets(spendable)
12731
12797
  };
@@ -12750,15 +12816,19 @@ function toWalletBalance(boardingUtxos, offchain) {
12750
12816
  settled: offchain.settled,
12751
12817
  preconfirmed: offchain.preconfirmed,
12752
12818
  available: offchain.available,
12753
- gated: offchain.gated,
12819
+ contractLocked: offchain.contractLocked,
12754
12820
  pendingSettlement: offchain.pendingSettlement,
12755
12821
  reserved: offchain.reserved,
12756
12822
  recoverable: offchain.recoverable,
12757
- pendingRecovery: offchain.pendingRecovery,
12823
+ deprecatedSignerLocked: offchain.deprecatedSignerLocked,
12758
12824
  unrolled: offchain.unrolled,
12759
12825
  total: totalBoarding + offchain.total,
12760
12826
  assets: offchain.assets,
12761
- availableAssets: offchain.availableAssets
12827
+ availableAssets: offchain.availableAssets,
12828
+ // Deprecated aliases, so code written against older releases still reads a value.
12829
+ gated: offchain.contractLocked,
12830
+ intentLocked: offchain.pendingSettlement,
12831
+ pendingRecovery: offchain.deprecatedSignerLocked
12762
12832
  };
12763
12833
  }
12764
12834
 
@@ -13806,22 +13876,42 @@ var ReadonlyWallet = class _ReadonlyWallet {
13806
13876
  const utxos = [];
13807
13877
  const commitmentsToIgnore = /* @__PURE__ */ new Set();
13808
13878
  const tapscripts = await this.getBoardingTapscripts(this.watchedBoardingSigners());
13809
- const outspendCache = /* @__PURE__ */ new Map();
13810
- const outspendOf = async (txid, index) => {
13811
- if (this.onchainProvider.getTxOutspend) {
13812
- return this.onchainProvider.getTxOutspend(txid, index);
13813
- }
13814
- let statuses = outspendCache.get(txid);
13815
- if (!statuses) {
13816
- statuses = await this.onchainProvider.getTxOutspends(txid);
13817
- outspendCache.set(txid, statuses);
13818
- }
13819
- return statuses[index];
13879
+ const getOne = this.onchainProvider.getTxOutspend?.bind(this.onchainProvider);
13880
+ const listInflight = /* @__PURE__ */ new Map();
13881
+ const oneInflight = /* @__PURE__ */ new Map();
13882
+ const outspendOf = (txid, index) => {
13883
+ if (getOne) {
13884
+ const key = `${txid}:${index}`;
13885
+ let pending2 = oneInflight.get(key);
13886
+ if (!pending2) {
13887
+ pending2 = getOne(txid, index);
13888
+ oneInflight.set(key, pending2);
13889
+ }
13890
+ return pending2;
13891
+ }
13892
+ let pending = listInflight.get(txid);
13893
+ if (!pending) {
13894
+ pending = this.onchainProvider.getTxOutspends(txid);
13895
+ listInflight.set(txid, pending);
13896
+ }
13897
+ return pending.then((statuses) => statuses[index]);
13820
13898
  };
13821
- for (const tapscript of tapscripts) {
13822
- const boardingAddress = tapscript.onchainAddress(this.network);
13823
- const scriptHex = hex.encode(tapscript.pkScript);
13824
- const txs = await this.onchainProvider.getTransactions(boardingAddress);
13899
+ const historyResults = await Promise.allSettled(
13900
+ tapscripts.map(async (tapscript) => {
13901
+ const boardingAddress = tapscript.onchainAddress(this.network);
13902
+ return {
13903
+ boardingAddress,
13904
+ scriptHex: hex.encode(tapscript.pkScript),
13905
+ txs: await this.onchainProvider.getTransactions(boardingAddress)
13906
+ };
13907
+ })
13908
+ );
13909
+ const histories = historyResults.map((result) => {
13910
+ if (result.status === "rejected") throw result.reason;
13911
+ return result.value;
13912
+ });
13913
+ const outputs = [];
13914
+ for (const { boardingAddress, scriptHex, txs } of histories) {
13825
13915
  const commitmentByOutpoint = /* @__PURE__ */ new Map();
13826
13916
  for (const tx of txs) {
13827
13917
  for (const input of tx.vin ?? []) {
@@ -13831,37 +13921,51 @@ var ReadonlyWallet = class _ReadonlyWallet {
13831
13921
  for (const tx of txs) {
13832
13922
  for (let i = 0; i < tx.vout.length; i++) {
13833
13923
  const vout = tx.vout[i];
13834
- if (vout.scriptpubkey_address === boardingAddress) {
13835
- const spentStatus = await outspendOf(tx.txid, i);
13836
- const commitmentTxid = spentStatus?.txid || commitmentByOutpoint.get(`${tx.txid}:${i}`);
13837
- const spent = Boolean(spentStatus?.spent) || commitmentTxid !== void 0;
13838
- if (spent && commitmentTxid) {
13839
- commitmentsToIgnore.add(commitmentTxid);
13840
- }
13841
- const boardingFacts = {
13842
- isSpent: spent,
13843
- isSwept: false,
13844
- isPreconfirmed: false,
13845
- commitmentTxIds: spent && commitmentTxid ? [commitmentTxid] : []
13846
- };
13847
- utxos.push({
13848
- txid: tx.txid,
13849
- vout: i,
13850
- value: Number(vout.value),
13851
- status: {
13852
- confirmed: tx.status.confirmed,
13853
- block_time: tx.status.block_time
13854
- },
13855
- isUnrolled: true,
13856
- ...boardingFacts,
13857
- spentBy: "",
13858
- createdAt: tx.status.confirmed ? new Date(tx.status.block_time * 1e3) : /* @__PURE__ */ new Date(0),
13859
- script: scriptHex
13860
- });
13861
- }
13924
+ if (vout.scriptpubkey_address !== boardingAddress) continue;
13925
+ outputs.push({
13926
+ txid: tx.txid,
13927
+ vout: i,
13928
+ value: Number(vout.value),
13929
+ confirmed: tx.status.confirmed,
13930
+ blockTime: tx.status.block_time,
13931
+ scriptHex,
13932
+ commitmentFromVin: commitmentByOutpoint.get(`${tx.txid}:${i}`),
13933
+ spentStatus: outspendOf(tx.txid, i)
13934
+ });
13862
13935
  }
13863
13936
  }
13864
13937
  }
13938
+ const spentResults = await Promise.allSettled(outputs.map((output) => output.spentStatus));
13939
+ for (const [i, output] of outputs.entries()) {
13940
+ const spentResult = spentResults[i];
13941
+ if (spentResult.status === "rejected") throw spentResult.reason;
13942
+ const spentStatus = spentResult.value;
13943
+ const commitmentTxid = spentStatus?.txid || output.commitmentFromVin;
13944
+ const spent = Boolean(spentStatus?.spent) || commitmentTxid !== void 0;
13945
+ if (spent && commitmentTxid) {
13946
+ commitmentsToIgnore.add(commitmentTxid);
13947
+ }
13948
+ const boardingFacts = {
13949
+ isSpent: spent,
13950
+ isSwept: false,
13951
+ isPreconfirmed: false,
13952
+ commitmentTxIds: spent && commitmentTxid ? [commitmentTxid] : []
13953
+ };
13954
+ utxos.push({
13955
+ txid: output.txid,
13956
+ vout: output.vout,
13957
+ value: output.value,
13958
+ status: {
13959
+ confirmed: output.confirmed,
13960
+ block_time: output.blockTime
13961
+ },
13962
+ isUnrolled: true,
13963
+ ...boardingFacts,
13964
+ spentBy: "",
13965
+ createdAt: output.confirmed ? new Date(output.blockTime * 1e3) : /* @__PURE__ */ new Date(0),
13966
+ script: output.scriptHex
13967
+ });
13968
+ }
13865
13969
  const unconfirmedTxs = [];
13866
13970
  const confirmedTxs = [];
13867
13971
  for (const utxo of utxos) {
@@ -13953,9 +14057,12 @@ var ReadonlyWallet = class _ReadonlyWallet {
13953
14057
  await this.walletRepository.saveUtxos(
13954
14058
  new Map(groups.map((group, i) => [addresses[i], group.coins]))
13955
14059
  );
14060
+ const cachedByAddress = await Promise.all(
14061
+ addresses.map((address) => collectUtxos(this.walletRepository, address))
14062
+ );
13956
14063
  for (const [i, group] of groups.entries()) {
13957
14064
  const fresh = new Set(group.coins.map((coin) => `${coin.txid}:${coin.vout}`));
13958
- const cached = await collectUtxos(this.walletRepository, addresses[i]);
14065
+ const cached = cachedByAddress[i];
13959
14066
  const kept = cached.filter((coin) => fresh.has(`${coin.txid}:${coin.vout}`));
13960
14067
  if (kept.length === cached.length) continue;
13961
14068
  await this.walletRepository.deleteUtxos(addresses[i]);
@@ -13978,14 +14085,20 @@ var ReadonlyWallet = class _ReadonlyWallet {
13978
14085
  return groups.flatMap((g) => g.coins);
13979
14086
  }
13980
14087
  /**
13981
- * @internal {@link getBoardingUtxos} minus those an in-flight settle is spending, for SDK
13982
- * picks. The getter itself lists them: callers that mirror it (the worker's cache and
13983
- * broadcasts, balances) must not see a settle's inputs vanish, and then stay gone if it fails.
14088
+ * @internal {@link getBoardingUtxos} minus those an in-flight settle is spending or an
14089
+ * unresolved intent locks, for SDK picks. The getter itself lists them: callers that mirror it
14090
+ * (the worker's cache and broadcasts, balances) must not see a settle's inputs vanish, and then
14091
+ * stay gone if it fails.
13984
14092
  */
13985
14093
  async getSelectableBoardingUtxos() {
13986
- return (await this.getBoardingUtxos()).filter(
13987
- (coin) => !this._pendingSpendOutpoints.has(`${coin.txid}:${coin.vout}`)
13988
- );
14094
+ const [utxos, locked] = await Promise.all([
14095
+ this.getBoardingUtxos(),
14096
+ this.lockedOutpoints()
14097
+ ]);
14098
+ return utxos.filter((coin) => {
14099
+ const key = `${coin.txid}:${coin.vout}`;
14100
+ return !this._pendingSpendOutpoints.has(key) && !locked.has(key);
14101
+ });
13989
14102
  }
13990
14103
  async getStoredBoardingUtxos() {
13991
14104
  const currentOnly = /* @__PURE__ */ new Set([
@@ -14010,6 +14123,7 @@ var ReadonlyWallet = class _ReadonlyWallet {
14010
14123
  * @returns A function that stops the subscriptions
14011
14124
  */
14012
14125
  async notifyIncomingFunds(eventCallback) {
14126
+ const cm = this.indexerProvider ? await this.getContractManager() : void 0;
14013
14127
  const arkAddress = await this.getAddress();
14014
14128
  let onchainStopFunc;
14015
14129
  let indexerStopFunc;
@@ -14062,8 +14176,7 @@ var ReadonlyWallet = class _ReadonlyWallet {
14062
14176
  void subscribeOnchain();
14063
14177
  });
14064
14178
  await subscribeOnchain();
14065
- if (this.indexerProvider && arkAddress) {
14066
- const cm = await this.getContractManager();
14179
+ if (cm && arkAddress) {
14067
14180
  let annotationQueue = Promise.resolve();
14068
14181
  indexerStopFunc = cm.onContractEvent((event) => {
14069
14182
  if (!isContractVtxoEvent(event)) {
@@ -14209,21 +14322,25 @@ var ReadonlyWallet = class _ReadonlyWallet {
14209
14322
  extraSources: capture?.sources
14210
14323
  });
14211
14324
  onVtxosPersisted = async (_contract, vtxos) => {
14212
- for (const v of vtxos) {
14213
- if (v.isSpent) continue;
14214
- await captureExitBranch({
14215
- resolver,
14216
- repository: virtualTxRepository,
14217
- vtxo: { txid: v.txid, vout: v.vout },
14218
- value: v.value,
14219
- mode: capture?.mode ?? DEFAULT_EXIT_CAPTURE_MODE,
14220
- minExitWorthSats: capture?.minExitWorthSats ?? DEFAULT_MIN_EXIT_WORTH_SATS
14221
- }).catch(() => {
14222
- });
14223
- }
14325
+ await Promise.all(
14326
+ vtxos.map((v) => {
14327
+ if (v.isSpent) return void 0;
14328
+ return captureExitBranch({
14329
+ resolver,
14330
+ repository: virtualTxRepository,
14331
+ vtxo: { txid: v.txid, vout: v.vout },
14332
+ value: v.value,
14333
+ mode: capture?.mode ?? DEFAULT_EXIT_CAPTURE_MODE,
14334
+ minExitWorthSats: capture?.minExitWorthSats ?? DEFAULT_MIN_EXIT_WORTH_SATS
14335
+ }).catch(() => {
14336
+ });
14337
+ })
14338
+ );
14224
14339
  };
14225
14340
  onVtxosSpent = (vtxos) => pruneExitBranches(virtualTxRepository, vtxos);
14226
14341
  }
14342
+ const baselinePubkeyPromise = this.identity.xOnlyPublicKey();
14343
+ void baselinePubkeyPromise.catch(() => void 0);
14227
14344
  const manager = await ContractManager.create({
14228
14345
  lazyInitialization: this.lazyInitialization,
14229
14346
  indexerProvider: this.indexerProvider,
@@ -14243,7 +14360,7 @@ var ReadonlyWallet = class _ReadonlyWallet {
14243
14360
  return { height, time };
14244
14361
  }
14245
14362
  });
14246
- const baselinePubkey = await this.identity.xOnlyPublicKey();
14363
+ const baselinePubkey = await baselinePubkeyPromise;
14247
14364
  const delegatePubKey = this.offchainTapscript instanceof DelegateVtxo.Script ? this.offchainTapscript.options.delegatePubKey : void 0;
14248
14365
  const baselineSigners = [
14249
14366
  this.offchainTapscript.options.serverPubKey,
@@ -14337,7 +14454,7 @@ var ReadonlyWallet = class _ReadonlyWallet {
14337
14454
  }
14338
14455
  };
14339
14456
  var Wallet2 = class extends ReadonlyWallet {
14340
- constructor(identity, network, onchainProvider, arkProvider, indexerProvider, arkServerPublicKey, offchainTapscript, boardingTapscript, serverUnrollScript, forfeitOutputScript, forfeitPubkey, dustAmount, walletRepository, contractRepository, delegateProvider, watcherConfig, settlementConfig, walletContractTimelocks, receiveRotator, descriptorProvider, keyring, lookAheadWindow, batchExpiryPolicy) {
14457
+ constructor(identity, network, onchainProvider, arkProvider, indexerProvider, arkServerPublicKey, offchainTapscript, boardingTapscript, serverUnrollScript, forfeitOutputScript, forfeitPubkey, dustAmount, walletRepository, contractRepository, delegateProvider, watcherConfig, settlementConfig, walletContractTimelocks, receiveRotator, descriptorProvider, keyring, lookAheadWindow, batchExpiryPolicy, oneShotSubmit = true) {
14341
14458
  super(
14342
14459
  identity,
14343
14460
  network,
@@ -14357,6 +14474,7 @@ var Wallet2 = class extends ReadonlyWallet {
14357
14474
  this.forfeitOutputScript = forfeitOutputScript;
14358
14475
  this.forfeitPubkey = forfeitPubkey;
14359
14476
  this.batchExpiryPolicy = batchExpiryPolicy;
14477
+ this.oneShotSubmit = oneShotSubmit;
14360
14478
  this.identity = identity;
14361
14479
  this.settlementConfig = settlementConfig !== void 0 ? settlementConfig : { ...DEFAULT_SETTLEMENT_CONFIG };
14362
14480
  this._delegateManager = delegateProvider ? new DelegateManagerImpl(delegateProvider, arkProvider, identity) : void 0;
@@ -14378,6 +14496,7 @@ var Wallet2 = class extends ReadonlyWallet {
14378
14496
  forfeitOutputScript;
14379
14497
  forfeitPubkey;
14380
14498
  batchExpiryPolicy;
14499
+ oneShotSubmit;
14381
14500
  static MIN_FEE_RATE = 1;
14382
14501
  // sats/vbyte
14383
14502
  identity;
@@ -14846,13 +14965,13 @@ var Wallet2 = class extends ReadonlyWallet {
14846
14965
  async _doRotateServerSigner(xonly, newServerUnrollScript) {
14847
14966
  if (equalBytes(xonly, this.arkServerPublicKey)) return;
14848
14967
  const manager = await this.getContractManager();
14849
- const [currentOffchainRow] = await manager.getContracts({
14850
- script: this.defaultContractScript
14851
- });
14852
14968
  const currentBoardingScript = hex.encode(this._boardingTapscript.pkScript);
14853
- const [currentBoardingRow] = await manager.getContracts({
14854
- script: currentBoardingScript
14855
- });
14969
+ const [offchainRows, boardingRows] = await Promise.all([
14970
+ manager.getContracts({ script: this.defaultContractScript }),
14971
+ manager.getContracts({ script: currentBoardingScript })
14972
+ ]);
14973
+ const [currentOffchainRow] = offchainRows;
14974
+ const [currentBoardingRow] = boardingRows;
14856
14975
  const newOffchain = this.offchainTapscript instanceof DelegateVtxo.Script ? new DelegateVtxo.Script({
14857
14976
  ...this.offchainTapscript.options,
14858
14977
  serverPubKey: xonly
@@ -15035,13 +15154,15 @@ var Wallet2 = class extends ReadonlyWallet {
15035
15154
  return this._restoreInFlight;
15036
15155
  }
15037
15156
  async _runRestore(gapLimit) {
15038
- const manager = await this.getContractManager();
15039
15157
  const provider = this._hdProvider;
15040
15158
  const hd = provider !== void 0;
15041
- const staticDescriptor = hd ? void 0 : await identityDescriptor(this.identity);
15159
+ const [manager, staticDescriptor, arkInfo] = await Promise.all([
15160
+ this.getContractManager(),
15161
+ hd ? void 0 : identityDescriptor(this.identity),
15162
+ this.arkProvider.getInfo()
15163
+ ]);
15042
15164
  const materialize = (index) => hd ? provider.materializeDescriptorAt(index) : staticDescriptor;
15043
15165
  const delegatePubKey = this.offchainTapscript instanceof DelegateVtxo.Script ? this.offchainTapscript.options.delegatePubKey : void 0;
15044
- const arkInfo = await this.arkProvider.getInfo();
15045
15166
  const currentSignerPubKey = toXOnly(hex.decode(arkInfo.signerPubkey), "ark signer key");
15046
15167
  const deprecatedSignerPubKeys = arkInfo.deprecatedSigners.map(
15047
15168
  (s) => toXOnly(hex.decode(s.pubkey), "deprecated signer key")
@@ -15221,7 +15342,8 @@ var Wallet2 = class extends ReadonlyWallet {
15221
15342
  {
15222
15343
  advertisedVtxoTreeExpiry: setup.info.vtxoTreeExpiry,
15223
15344
  ...config.minBatchExpirySeconds !== void 0 ? { minSeconds: config.minBatchExpirySeconds } : {}
15224
- }
15345
+ },
15346
+ config.oneShotSubmit
15225
15347
  );
15226
15348
  wallet.lazyInitialization = config.lazyInitialization ?? false;
15227
15349
  wallet._serverInfoSource = setup.serverInfoSource;
@@ -15589,25 +15711,33 @@ var Wallet2 = class extends ReadonlyWallet {
15589
15711
  throw error;
15590
15712
  }
15591
15713
  const inputIds = params.inputs.map((i) => `${i.txid}:${i.vout}`).join(",");
15714
+ let released = !registering;
15592
15715
  if (registering) {
15593
- await this.arkProvider.deleteIntent(deleteIntent).catch((e) => {
15594
- if (isNoMatchingIntentsError(e)) return;
15595
- console.warn(
15596
- `Failed to delete intent after settle failure for inputs [${inputIds}]; intent may linger on server and cause 'duplicated input' on next settle`,
15597
- e
15598
- );
15599
- });
15600
- }
15601
- await this.persistIntentSnapshot(
15602
- intentTxId,
15603
- "cancelled",
15604
- intent,
15605
- deleteIntent,
15606
- params.inputs,
15607
- {
15608
- cancellationReason: error instanceof Error ? error.message : String(error)
15716
+ try {
15717
+ await this.arkProvider.deleteIntent(deleteIntent);
15718
+ released = true;
15719
+ } catch (e) {
15720
+ if (isNoMatchingIntentsError(e)) released = true;
15721
+ else {
15722
+ console.warn(
15723
+ `Failed to delete intent after settle failure for inputs [${inputIds}]; intent may linger on server and cause 'duplicated input' on next settle`,
15724
+ e
15725
+ );
15726
+ }
15609
15727
  }
15610
- );
15728
+ }
15729
+ if (released) {
15730
+ await this.persistIntentSnapshot(
15731
+ intentTxId,
15732
+ "cancelled",
15733
+ intent,
15734
+ deleteIntent,
15735
+ params.inputs,
15736
+ {
15737
+ cancellationReason: error instanceof Error ? error.message : String(error)
15738
+ }
15739
+ );
15740
+ }
15611
15741
  throw error;
15612
15742
  } finally {
15613
15743
  this._removePendingSpends(params.inputs);
@@ -16254,7 +16384,8 @@ var Wallet2 = class extends ReadonlyWallet {
16254
16384
  }
16255
16385
  ],
16256
16386
  outputs: [{ script: myPkScript, amount: BigInt(vtxo.value) }],
16257
- serverUnrollScript
16387
+ serverUnrollScript,
16388
+ oneShot: this.oneShotSubmit
16258
16389
  });
16259
16390
  swept += vtxo.value;
16260
16391
  } catch (error) {
@@ -17096,7 +17227,8 @@ var Wallet2 = class extends ReadonlyWallet {
17096
17227
  deprecatedServerPubkeys: [...this._deprecatedSigners.keys()].map(
17097
17228
  (h) => hex.decode(h)
17098
17229
  )
17099
- }
17230
+ },
17231
+ oneShot: this.oneShotSubmit
17100
17232
  }
17101
17233
  );
17102
17234
  } finally {
@@ -17744,6 +17876,7 @@ var MessageBus = class {
17744
17876
  lazyInitialization: config.lazyInitialization,
17745
17877
  watcherConfig: config.watcherConfig,
17746
17878
  lookAheadWindow: config.lookAheadWindow,
17879
+ oneShotSubmit: config.oneShotSubmit,
17747
17880
  minBatchExpirySeconds: config.minBatchExpirySeconds
17748
17881
  });
17749
17882
  return { wallet, arkProvider, readonlyWallet: wallet };
@@ -18536,6 +18669,7 @@ var WalletMessageHandler = class {
18536
18669
  indexerProvider;
18537
18670
  walletRepository;
18538
18671
  incomingFundsSubscription;
18672
+ incomingFundsRegistrations = /* @__PURE__ */ new WeakMap();
18539
18673
  contractEventsSubscription;
18540
18674
  onNextTick = [];
18541
18675
  channel;
@@ -18547,6 +18681,11 @@ var WalletMessageHandler = class {
18547
18681
  // stale, folding requests in would truncate an in-flight settle, and
18548
18682
  // sharing with boarding would kill every lazy refresh (see `boardingEpoch`).
18549
18683
  incomingFundsEpoch = 0;
18684
+ /**
18685
+ * Supersedes older initializers without touching `incomingFundsEpoch`, so a
18686
+ * re-init that fails before registering leaves the live subscription delivering.
18687
+ */
18688
+ initEpoch = 0;
18550
18689
  contractEventsEpoch = 0;
18551
18690
  boardingEpoch = 0;
18552
18691
  /** Per-request progress callbacks: ends with the handler, not with a re-init. */
@@ -18571,6 +18710,7 @@ var WalletMessageHandler = class {
18571
18710
  }
18572
18711
  async stop() {
18573
18712
  this.requestEpoch++;
18713
+ this.initEpoch++;
18574
18714
  this.incomingFundsEpoch++;
18575
18715
  this.contractEventsEpoch++;
18576
18716
  this.boardingEpoch++;
@@ -18611,6 +18751,8 @@ var WalletMessageHandler = class {
18611
18751
  }
18612
18752
  epochOf(scope) {
18613
18753
  switch (scope) {
18754
+ case "init":
18755
+ return this.initEpoch;
18614
18756
  case "incomingFunds":
18615
18757
  return this.incomingFundsEpoch;
18616
18758
  case "contractEvents":
@@ -19367,18 +19509,22 @@ var WalletMessageHandler = class {
19367
19509
  }
19368
19510
  async onWalletInitialized() {
19369
19511
  const wallet = this.readonlyWallet;
19512
+ const signingWallet = this.wallet;
19513
+ const lazyBoarding = this.lazyBoardingInit;
19370
19514
  if (!wallet || !this.arkProvider || !this.indexerProvider || !this.walletRepository) {
19371
19515
  return;
19372
19516
  }
19373
- const init = this.newGeneration("incomingFunds");
19517
+ this.initEpoch++;
19518
+ const init = this.newGeneration("init");
19374
19519
  await this.ensureContractEventBroadcasting();
19375
19520
  if (init.stale) return;
19376
- await this.refreshCachedData({ lazyBoarding: this.lazyBoardingInit });
19521
+ await this.refreshCachedData({ lazyBoarding });
19377
19522
  if (init.stale) return;
19378
- if (this.wallet) {
19523
+ if (signingWallet) {
19379
19524
  try {
19380
19525
  const vtxos = await this.getVtxosFromRepo();
19381
- const { pending, finalized } = await this.wallet.finalizePendingTxs(
19526
+ if (init.stale) return;
19527
+ const { pending, finalized } = await signingWallet.finalizePendingTxs(
19382
19528
  vtxos.filter((vtxo) => vtxo.isSpent || vtxo.isPreconfirmed && !vtxo.isSwept)
19383
19529
  );
19384
19530
  console.info(
@@ -19389,11 +19535,34 @@ var WalletMessageHandler = class {
19389
19535
  }
19390
19536
  if (init.stale) return;
19391
19537
  }
19392
- if (this.incomingFundsSubscription) this.incomingFundsSubscription();
19393
- this.incomingFundsSubscription = void 0;
19394
19538
  this.incomingFundsEpoch++;
19395
19539
  const emitter = this.newEmitter("incomingFunds");
19540
+ const provider = wallet.onchainProvider ?? this;
19541
+ const previousRegistration = this.incomingFundsRegistrations.get(provider) ?? Promise.resolve();
19542
+ const registration = previousRegistration.then(
19543
+ () => this.registerIncomingFunds(wallet, emitter)
19544
+ );
19545
+ this.incomingFundsRegistrations.set(
19546
+ provider,
19547
+ registration.catch((error) => {
19548
+ console.error("Error subscribing to incoming funds:", error);
19549
+ })
19550
+ );
19551
+ if (!lazyBoarding) await registration;
19552
+ if (signingWallet && !emitter.stale) {
19553
+ try {
19554
+ await signingWallet.getVtxoManager();
19555
+ } catch (error) {
19556
+ console.error("Error starting VtxoManager:", error);
19557
+ }
19558
+ }
19559
+ }
19560
+ async registerIncomingFunds(wallet, emitter) {
19561
+ if (emitter.stale) return;
19562
+ if (this.incomingFundsSubscription) this.incomingFundsSubscription();
19563
+ this.incomingFundsSubscription = void 0;
19396
19564
  const address = await wallet.getAddress();
19565
+ if (emitter.stale) return;
19397
19566
  const unsubscribe = await wallet.notifyIncomingFunds(async (funds) => {
19398
19567
  if (emitter.stale) return;
19399
19568
  if (funds.type === "vtxo") {
@@ -19462,13 +19631,6 @@ var WalletMessageHandler = class {
19462
19631
  });
19463
19632
  if (emitter.stale) return unsubscribe();
19464
19633
  this.incomingFundsSubscription = unsubscribe;
19465
- if (this.wallet) {
19466
- try {
19467
- await this.wallet.getVtxoManager();
19468
- } catch (error) {
19469
- console.error("Error starting VtxoManager:", error);
19470
- }
19471
- }
19472
19634
  }
19473
19635
  /**
19474
19636
  * Refresh boarding inputs and transaction history. The virtual outputs come
@@ -19497,24 +19659,30 @@ var WalletMessageHandler = class {
19497
19659
  payload: { coins }
19498
19660
  })
19499
19661
  );
19500
- await this.refreshHistoryCache(emitter);
19662
+ await this.storeHistoryCache(await this.loadHistoryCache(), emitter);
19501
19663
  })().catch((error) => {
19502
19664
  console.warn("[wallet] background refresh failed", error);
19503
19665
  });
19504
19666
  return;
19505
19667
  }
19668
+ const history = this.loadHistoryCache();
19669
+ void history.catch(() => void 0);
19506
19670
  await this.refreshBoardingCache();
19507
19671
  this.boardingLoaded = true;
19508
- await this.refreshHistoryCache();
19672
+ await this.storeHistoryCache(await history);
19509
19673
  }
19510
- /** @param gen - set by a background caller; each write is skipped once it goes stale. */
19511
- async refreshHistoryCache(gen) {
19674
+ /** History rows plus the address they are stored under. Does not write. */
19675
+ async loadHistoryCache() {
19512
19676
  const wallet = this.readonlyWallet;
19513
- const repository = this.walletRepository;
19514
- if (!wallet || !repository) return;
19677
+ if (!wallet || !this.walletRepository) return void 0;
19515
19678
  const address = await wallet.getAddress();
19516
19679
  const txs = await this.buildTransactionHistoryFromCache();
19517
- if (txs && !gen?.stale) await repository.saveTransactions(address, txs);
19680
+ if (!txs) return void 0;
19681
+ return { address, txs };
19682
+ }
19683
+ async storeHistoryCache(history, gen) {
19684
+ if (!history || gen?.stale || !this.walletRepository) return;
19685
+ await this.walletRepository.saveTransactions(history.address, history.txs);
19518
19686
  }
19519
19687
  /**
19520
19688
  * @param gen - set by a background caller; each write here is skipped once it goes stale.
@@ -19529,11 +19697,17 @@ var WalletMessageHandler = class {
19529
19697
  hex.encode(wallet.boardingTapscript.options.serverPubKey)
19530
19698
  );
19531
19699
  const groups = await wallet.getBoardingUtxosForSigners(/* @__PURE__ */ new Set([currentSigner]));
19532
- for (const { tapscript, coins } of groups) {
19700
+ if (gen?.stale) return;
19701
+ const cachedByAddress = await Promise.all(
19702
+ groups.map(
19703
+ ({ tapscript }) => collectUtxos(repository, tapscript.onchainAddress(wallet.network))
19704
+ )
19705
+ );
19706
+ for (const [i, { tapscript, coins }] of groups.entries()) {
19533
19707
  if (gen?.stale) return;
19534
19708
  const address = tapscript.onchainAddress(wallet.network);
19535
19709
  const freshKeys = new Set(coins.map((coin) => `${coin.txid}:${coin.vout}`));
19536
- const cached = await collectUtxos(repository, address);
19710
+ const cached = cachedByAddress[i];
19537
19711
  const kept = cached.filter((coin) => freshKeys.has(`${coin.txid}:${coin.vout}`));
19538
19712
  if (kept.length === cached.length) continue;
19539
19713
  if (gen?.stale) return;
@@ -19631,6 +19805,7 @@ var WalletMessageHandler = class {
19631
19805
  const wallet = this.wallet ?? this.readonlyWallet;
19632
19806
  if (!wallet) return;
19633
19807
  this.requestEpoch++;
19808
+ this.initEpoch++;
19634
19809
  this.incomingFundsEpoch++;
19635
19810
  this.contractEventsEpoch++;
19636
19811
  this.boardingEpoch++;
@@ -19682,7 +19857,9 @@ var WalletMessageHandler = class {
19682
19857
  * {@link getVtxosFromRepo}).
19683
19858
  */
19684
19859
  async repoSnapshot(options = {}) {
19685
- if (!this.walletRepository || !this.readonlyWallet) return { snapshot: [], vtxos: [] };
19860
+ const repository = this.walletRepository;
19861
+ const wallet = this.readonlyWallet;
19862
+ if (!repository || !wallet) return { snapshot: [], vtxos: [] };
19686
19863
  const seen = /* @__PURE__ */ new Set();
19687
19864
  const allVtxos = [];
19688
19865
  const addVtxos = (vtxos) => {
@@ -19697,19 +19874,18 @@ var WalletMessageHandler = class {
19697
19874
  }
19698
19875
  return fresh;
19699
19876
  };
19700
- const manager = await this.readonlyWallet.getContractManager();
19877
+ const addressPromise = wallet.getAddress();
19878
+ void addressPromise.catch(() => void 0);
19879
+ const manager = await wallet.getContractManager();
19701
19880
  const contracts = await manager.getContracts();
19702
- const snapshot = [];
19703
- for (const contract of contracts) {
19704
- if (!options.includeRecoveryOnly && isRecoveryOnlyContract(contract)) continue;
19705
- snapshot.push({
19706
- contract,
19707
- vtxos: addVtxos(
19708
- await getVtxosForContract(this.walletRepository, contract, options)
19709
- )
19710
- });
19711
- }
19712
- const walletAddress = await this.readonlyWallet.getAddress();
19881
+ const eligible = contracts.filter(
19882
+ (contract) => options.includeRecoveryOnly || !isRecoveryOnlyContract(contract)
19883
+ );
19884
+ const listsPromise = Promise.all(
19885
+ eligible.map((contract) => getVtxosForContract(repository, contract, options))
19886
+ );
19887
+ void listsPromise.catch(() => void 0);
19888
+ const walletAddress = await addressPromise;
19713
19889
  let walletScript;
19714
19890
  try {
19715
19891
  walletScript = scriptFromArkAddress(walletAddress);
@@ -19718,13 +19894,26 @@ var WalletMessageHandler = class {
19718
19894
  `WalletMessageHandler.getVtxosFromRepo: failed to derive script from wallet address ${walletAddress}: ${e instanceof Error ? e.message : String(e)}`
19719
19895
  );
19720
19896
  }
19721
- addVtxos(
19722
- await getVtxosForContract(
19723
- this.walletRepository,
19897
+ const [lists, primaryVtxos] = await Promise.all([
19898
+ listsPromise,
19899
+ // Routed through the same helper as the contract buckets rather than reading the
19900
+ // repository directly, so this bucket normalizes too. Anything left here
19901
+ // is outside every contract row, so it carries no contract to judge it
19902
+ // by — the wallet's own receive address, which is never gated.
19903
+ getVtxosForContract(
19904
+ repository,
19724
19905
  { script: walletScript, address: walletAddress },
19725
19906
  options
19726
19907
  )
19727
- );
19908
+ ]);
19909
+ const snapshot = [];
19910
+ for (const [i, contract] of eligible.entries()) {
19911
+ snapshot.push({
19912
+ contract,
19913
+ vtxos: addVtxos(lists[i])
19914
+ });
19915
+ }
19916
+ addVtxos(primaryVtxos);
19728
19917
  return { snapshot, vtxos: allVtxos };
19729
19918
  }
19730
19919
  /**
@@ -20427,7 +20616,12 @@ var ServiceWorkerReadonlyWallet = class _ServiceWorkerReadonlyWallet {
20427
20616
  };
20428
20617
  try {
20429
20618
  const response = await this.sendMessage(message);
20430
- return response.payload;
20619
+ const balance = response.payload;
20620
+ return {
20621
+ ...balance,
20622
+ contractLocked: balance.contractLocked ?? balance.gated,
20623
+ deprecatedSignerLocked: balance.deprecatedSignerLocked ?? balance.pendingRecovery
20624
+ };
20431
20625
  } catch (error) {
20432
20626
  throw new Error(`Failed to get balance: ${error}`);
20433
20627
  }
@@ -20924,9 +21118,10 @@ var ServiceWorkerReadonlyWallet = class _ServiceWorkerReadonlyWallet {
20924
21118
  }
20925
21119
  };
20926
21120
  var ServiceWorkerWallet = class _ServiceWorkerWallet extends ServiceWorkerReadonlyWallet {
20927
- constructor(serviceWorker, identity, walletRepository, contractRepository, messageTag, hasDelegate) {
21121
+ constructor(serviceWorker, identity, walletRepository, contractRepository, messageTag, hasDelegate, oneShotSubmit) {
20928
21122
  super(serviceWorker, identity, walletRepository, contractRepository, messageTag);
20929
21123
  this.serviceWorker = serviceWorker;
21124
+ this.oneShotSubmit = oneShotSubmit;
20930
21125
  this.identity = identity;
20931
21126
  this.walletRepository = walletRepository;
20932
21127
  this.contractRepository = contractRepository;
@@ -20937,6 +21132,7 @@ var ServiceWorkerWallet = class _ServiceWorkerWallet extends ServiceWorkerReadon
20937
21132
  this.hasDelegate = hasDelegate;
20938
21133
  }
20939
21134
  serviceWorker;
21135
+ oneShotSubmit;
20940
21136
  walletRepository;
20941
21137
  contractRepository;
20942
21138
  identity;
@@ -20973,7 +21169,8 @@ var ServiceWorkerWallet = class _ServiceWorkerWallet extends ServiceWorkerReadon
20973
21169
  walletRepository,
20974
21170
  contractRepository,
20975
21171
  messageTag,
20976
- !!options.delegateUrl
21172
+ !!options.delegateUrl,
21173
+ options.oneShotSubmit
20977
21174
  );
20978
21175
  wallet._concurrentSpending = options.concurrentSpending === true;
20979
21176
  return _ServiceWorkerWallet.bootstrap(wallet, options, serializedWallet, {
@@ -20984,6 +21181,7 @@ var ServiceWorkerWallet = class _ServiceWorkerWallet extends ServiceWorkerReadon
20984
21181
  lazyInitialization: options.lazyInitialization,
20985
21182
  watcherConfig: options.watcherConfig,
20986
21183
  lookAheadWindow: options.lookAheadWindow,
21184
+ oneShotSubmit: options.oneShotSubmit,
20987
21185
  minBatchExpirySeconds: options.minBatchExpirySeconds
20988
21186
  });
20989
21187
  }
@@ -22621,8 +22819,12 @@ async function attachPrevoutTxs(tx, source) {
22621
22819
  }
22622
22820
  if (pending.length === 0) return;
22623
22821
  const resolved = /* @__PURE__ */ new Map();
22624
- for (const txid of new Set(pending.map((p) => p.txid))) {
22625
- resolved.set(txid, await source.getRawTransaction(txid));
22822
+ const txids = [...new Set(pending.map((p) => p.txid))];
22823
+ const fetched = await Promise.allSettled(txids.map((txid) => source.getRawTransaction(txid)));
22824
+ for (let i = 0; i < fetched.length; i++) {
22825
+ const result = fetched[i];
22826
+ if (result.status === "rejected") throw result.reason;
22827
+ resolved.set(txids[i], result.value);
22626
22828
  }
22627
22829
  for (const { index, txid } of pending) {
22628
22830
  setArkPsbtField(tx, index, PrevoutTxField, resolved.get(txid));
@@ -23076,40 +23278,53 @@ var Unroll;
23076
23278
  Unroll2.completeUnroll = completeUnroll;
23077
23279
  })(Unroll || (Unroll = {}));
23078
23280
  async function prepareUnrollTransaction(wallet, vtxoTxIds, outputAddress) {
23079
- const chainTip = await wallet.onchainProvider.getChainTip();
23080
- let vtxos = await wallet.getVtxos({ withUnrolled: true });
23081
- vtxos = vtxos.filter((vtxo) => vtxoTxIds.includes(vtxo.txid));
23281
+ const [chainTip, storedVtxos, walletDescriptor] = await Promise.all([
23282
+ wallet.onchainProvider.getChainTip(),
23283
+ wallet.getVtxos({ withUnrolled: true }),
23284
+ wallet.identity.xOnlyPublicKey().then((key) => hex.encode(key))
23285
+ ]);
23286
+ const vtxos = storedVtxos.filter((vtxo) => vtxoTxIds.includes(vtxo.txid));
23082
23287
  if (vtxos.length === 0) {
23083
23288
  throw new Error("No vtxos to complete unroll");
23084
23289
  }
23085
23290
  const inputs = [];
23086
23291
  let totalAmount = 0n;
23087
23292
  const txWeightEstimator = TxWeightEstimator.create();
23088
- const walletDescriptor = hex.encode(await wallet.identity.xOnlyPublicKey());
23089
23293
  for (const vtxo of vtxos) {
23090
23294
  if (!canSweepOnchain(vtxo)) {
23091
23295
  throw new Error(
23092
23296
  `Vtxo ${vtxo.txid}:${vtxo.vout} cannot be swept onchain: it is not fully unrolled (use unroll first), or its exit output was already spent`
23093
23297
  );
23094
23298
  }
23095
- const txStatus = await wallet.onchainProvider.getTxStatus(vtxo.txid);
23096
- if (!txStatus.confirmed) {
23097
- throw new Error(`tx ${vtxo.txid} is not confirmed`);
23098
- }
23099
- const resolved = await resolveUnilateralPath({
23100
- vtxo,
23101
- scriptHex: hex.encode(VtxoScript.decode(vtxo.tapTree).pkScript),
23102
- contractRepository: wallet.contractRepository,
23103
- walletDescriptor,
23104
- currentTime: Date.now()
23105
- });
23106
- const spendingLeaf = resolved.selection.leaf;
23107
- const sequence = resolved.selection.sequence;
23108
- const timelock = sequenceToTimelock(sequence);
23109
- const elapsed = timelock.type === "blocks" ? chainTip.height >= txStatus.blockHeight + Number(timelock.value) : chainTip.time >= txStatus.blockTime + Number(timelock.value);
23110
- if (!elapsed) {
23111
- throw new Error(`no available exit path found for vtxo ${vtxo.txid}:${vtxo.vout}`);
23112
- }
23299
+ }
23300
+ const prepared = await Promise.allSettled(
23301
+ vtxos.map(async (vtxo) => {
23302
+ const [txStatus, resolved] = await Promise.all([
23303
+ wallet.onchainProvider.getTxStatus(vtxo.txid),
23304
+ resolveUnilateralPath({
23305
+ vtxo,
23306
+ scriptHex: hex.encode(VtxoScript.decode(vtxo.tapTree).pkScript),
23307
+ contractRepository: wallet.contractRepository,
23308
+ walletDescriptor,
23309
+ currentTime: Date.now()
23310
+ })
23311
+ ]);
23312
+ if (!txStatus.confirmed) {
23313
+ throw new Error(`tx ${vtxo.txid} is not confirmed`);
23314
+ }
23315
+ const spendingLeaf = resolved.selection.leaf;
23316
+ const sequence = resolved.selection.sequence;
23317
+ const timelock = sequenceToTimelock(sequence);
23318
+ const elapsed = timelock.type === "blocks" ? chainTip.height >= txStatus.blockHeight + Number(timelock.value) : chainTip.time >= txStatus.blockTime + Number(timelock.value);
23319
+ if (!elapsed) {
23320
+ throw new Error(`no available exit path found for vtxo ${vtxo.txid}:${vtxo.vout}`);
23321
+ }
23322
+ return { vtxo, spendingLeaf, sequence };
23323
+ })
23324
+ );
23325
+ for (const result of prepared) {
23326
+ if (result.status === "rejected") throw result.reason;
23327
+ const { vtxo, spendingLeaf, sequence } = result.value;
23113
23328
  totalAmount += BigInt(vtxo.value);
23114
23329
  inputs.push({
23115
23330
  txid: vtxo.txid,
@@ -23267,15 +23482,18 @@ async function resolveFeeRate(opts) {
23267
23482
  async function selectExitVtxos(opts) {
23268
23483
  if (!opts.vtxos) return opts.wallet.getVtxos();
23269
23484
  const res = await getNormalizedVtxos(opts.wallet.indexerProvider, { outpoints: opts.vtxos });
23485
+ const scripts = [
23486
+ ...new Set(res.vtxos.filter((vtxo) => !vtxo.isSpent).map((vtxo) => vtxo.script))
23487
+ ];
23488
+ const contracts = scripts.length === 0 ? [] : await collectContracts(opts.wallet.contractRepository, { script: scripts });
23489
+ const byScript = new Map(contracts.map((contract) => [contract.script, contract]));
23270
23490
  const tapTrees = /* @__PURE__ */ new Map();
23271
23491
  const out = [];
23272
23492
  for (const vtxo of res.vtxos) {
23273
23493
  if (vtxo.isSpent) continue;
23274
23494
  let tapTree = tapTrees.get(vtxo.script);
23275
23495
  if (!tapTree) {
23276
- const [contract] = await collectContracts(opts.wallet.contractRepository, {
23277
- script: vtxo.script
23278
- });
23496
+ const contract = byScript.get(vtxo.script);
23279
23497
  if (!contract) {
23280
23498
  throw new Error(
23281
23499
  `no contract registered for vtxo script ${vtxo.script} \u2014 register the contract before exiting`
@@ -23294,6 +23512,10 @@ async function selectExitVtxos(opts) {
23294
23512
  }
23295
23513
  async function computeExitLayout(opts, feeRate) {
23296
23514
  const { wallet, onchainWallet } = opts;
23515
+ const coinsPromise = onchainWallet.getCoins();
23516
+ const pubkeyPromise = wallet.identity.xOnlyPublicKey();
23517
+ void coinsPromise.catch(() => void 0);
23518
+ void pubkeyPromise.catch(() => void 0);
23297
23519
  const vtxos = await selectExitVtxos(opts);
23298
23520
  if (vtxos.length === 0) throw new Error("no vtxos to exit");
23299
23521
  const resolver = createExitChainResolver({
@@ -23331,46 +23553,57 @@ async function computeExitLayout(opts, feeRate) {
23331
23553
  (s) => s.parent.id,
23332
23554
  (s) => txInputTxids(s.parent)
23333
23555
  );
23334
- const walletDescriptor = hex.encode(await wallet.identity.xOnlyPublicKey());
23556
+ const walletDescriptor = hex.encode(await pubkeyPromise);
23557
+ const resolvedPaths = await Promise.allSettled(
23558
+ vtxos.map(async (vtxo) => {
23559
+ try {
23560
+ return {
23561
+ vtxo,
23562
+ resolved: await resolveUnilateralPath({
23563
+ vtxo,
23564
+ scriptHex: hex.encode(VtxoScript.decode(vtxo.tapTree).pkScript),
23565
+ contractRepository: wallet.contractRepository,
23566
+ walletDescriptor,
23567
+ currentTime: Date.now()
23568
+ })
23569
+ };
23570
+ } catch (e) {
23571
+ if (e instanceof ExitPathError) return { vtxo, skipped: e.message };
23572
+ throw e;
23573
+ }
23574
+ })
23575
+ );
23335
23576
  const infos = [];
23336
23577
  const sweeps = [];
23337
- for (const vtxo of vtxos) {
23578
+ for (const result of resolvedPaths) {
23579
+ if (result.status === "rejected") throw result.reason;
23580
+ const { vtxo } = result.value;
23338
23581
  const outpoint = `${vtxo.txid}:${vtxo.vout}`;
23339
- try {
23340
- const resolved = await resolveUnilateralPath({
23341
- vtxo,
23342
- scriptHex: hex.encode(VtxoScript.decode(vtxo.tapTree).pkScript),
23343
- contractRepository: wallet.contractRepository,
23344
- walletDescriptor,
23345
- currentTime: Date.now()
23582
+ if ("skipped" in result.value) {
23583
+ infos.push({ outpoint, value: vtxo.value, skipped: result.value.skipped });
23584
+ continue;
23585
+ }
23586
+ const { resolved } = result.value;
23587
+ const sweepFee = sweepFeeFor(
23588
+ resolved.selection,
23589
+ opts.sweepAddress,
23590
+ wallet.network,
23591
+ feeRate
23592
+ );
23593
+ if (vtxo.value - sweepFee < CHILD_OUTPUT_DUST) {
23594
+ infos.push({
23595
+ outpoint,
23596
+ value: vtxo.value,
23597
+ skipped: `uneconomic: value ${vtxo.value} <= sweep fee + dust`
23346
23598
  });
23347
- const sweepFee = sweepFeeFor(
23348
- resolved.selection,
23349
- opts.sweepAddress,
23350
- wallet.network,
23351
- feeRate
23352
- );
23353
- if (vtxo.value - sweepFee < CHILD_OUTPUT_DUST) {
23354
- infos.push({
23355
- outpoint,
23356
- value: vtxo.value,
23357
- skipped: `uneconomic: value ${vtxo.value} <= sweep fee + dust`
23358
- });
23359
- continue;
23360
- }
23361
- const t = sequenceToTimelock(resolved.selection.sequence);
23362
- const delay = { type: t.type, value: Number(t.value) };
23363
- infos.push({ outpoint, value: vtxo.value, sweepFee, path: resolved.label, delay });
23364
- sweeps.push({ vtxo, resolved, sweepFee, delay });
23365
- } catch (e) {
23366
- if (e instanceof ExitPathError) {
23367
- infos.push({ outpoint, value: vtxo.value, skipped: e.message });
23368
- continue;
23369
- }
23370
- throw e;
23599
+ continue;
23371
23600
  }
23601
+ const t = sequenceToTimelock(resolved.selection.sequence);
23602
+ const delay = { type: t.type, value: Number(t.value) };
23603
+ infos.push({ outpoint, value: vtxo.value, sweepFee, path: resolved.label, delay });
23604
+ sweeps.push({ vtxo, resolved, sweepFee, delay });
23372
23605
  }
23373
- const coins = (await onchainWallet.getCoins()).filter((c) => c.status.confirmed);
23606
+ const coins = (await coinsPromise).filter((c) => c.status.confirmed);
23374
23607
  const balance = coins.reduce((sum, c) => sum + c.value, 0);
23375
23608
  const fundingTotal = steps.reduce((sum, s) => sum + s.funding, 0);
23376
23609
  const splitterFeeFor = (inputCount) => {
@@ -24578,6 +24811,7 @@ var Arkade = class _Arkade {
24578
24811
  userKey;
24579
24812
  /** The contract manager, when contract persistence is wired up (passed in or owned). */
24580
24813
  contractManager;
24814
+ oneShotSubmit;
24581
24815
  /** Whether {@link connect} built {@link contractManager}, so {@link dispose} must stop it. */
24582
24816
  ownsContractManager;
24583
24817
  /** {@link ArkadeContract.on} handles by contract script (hex). */
@@ -24597,6 +24831,7 @@ var Arkade = class _Arkade {
24597
24831
  this.identity = fields.identity;
24598
24832
  this.userKey = fields.userKey;
24599
24833
  this.contractManager = fields.contractManager;
24834
+ this.oneShotSubmit = fields.oneShotSubmit;
24600
24835
  this.ownsContractManager = fields.ownsContractManager ?? false;
24601
24836
  }
24602
24837
  /**
@@ -24673,7 +24908,8 @@ var Arkade = class _Arkade {
24673
24908
  identity: opts.identity,
24674
24909
  userKey,
24675
24910
  contractManager,
24676
- ownsContractManager
24911
+ ownsContractManager,
24912
+ oneShotSubmit: opts.oneShotSubmit
24677
24913
  });
24678
24914
  }
24679
24915
  /**
@@ -24770,6 +25006,13 @@ var Arkade = class _Arkade {
24770
25006
  }
24771
25007
  return new ArkadeContract(this, program, args);
24772
25008
  }
25009
+ /**
25010
+ * Start a spend with no function of its own: each `.from(coins, contract.unlock.<name>(...))`
25011
+ * names the function its coins are spent through.
25012
+ */
25013
+ transaction() {
25014
+ return new ArkadeTransactionBuilder(this);
25015
+ }
24773
25016
  };
24774
25017
  var ArkadeContract = class _ArkadeContract {
24775
25018
  constructor(client, program, args = {}, keys) {
@@ -24831,7 +25074,23 @@ var ArkadeContract = class _ArkadeContract {
24831
25074
  get functions() {
24832
25075
  const out = /* @__PURE__ */ Object.create(null);
24833
25076
  for (const fn of this.compiled) {
24834
- out[fn.name] = (...callArgs) => new ArkadeTransactionBuilder(this, fn, bindInputs(fn, callArgs));
25077
+ out[fn.name] = (...callArgs) => new ArkadeTransactionBuilder(this.client, {
25078
+ contract: this,
25079
+ fn,
25080
+ args: bindInputs(fn, callArgs)
25081
+ });
25082
+ }
25083
+ return out;
25084
+ }
25085
+ /** Spending paths bound to their arguments, for `.from(coins, contract.unlock.<name>(...))`. */
25086
+ get unlock() {
25087
+ const out = /* @__PURE__ */ Object.create(null);
25088
+ for (const fn of this.compiled) {
25089
+ out[fn.name] = (...callArgs) => ({
25090
+ contract: this,
25091
+ fn,
25092
+ args: bindInputs(fn, callArgs)
25093
+ });
24835
25094
  }
24836
25095
  return out;
24837
25096
  }
@@ -24949,27 +25208,62 @@ var ArkadeContract = class _ArkadeContract {
24949
25208
  };
24950
25209
  var ArkadeTransactionBuilder = class {
24951
25210
  /** @internal */
24952
- constructor(contract, fn, args) {
24953
- this.contract = contract;
24954
- this.fn = fn;
24955
- this.args = args;
25211
+ constructor(client, unlocker) {
25212
+ this.client = client;
25213
+ this.unlocker = unlocker;
24956
25214
  }
24957
- contract;
24958
- fn;
24959
- args;
25215
+ client;
25216
+ unlocker;
24960
25217
  outputs = [];
24961
25218
  fundingCoins = [];
24962
25219
  assetSpecs = [];
24963
25220
  extraPackets = [];
24964
- coin;
25221
+ inputs = [];
24965
25222
  changeScript;
24966
- from(coin) {
24967
- this.coin = coin;
25223
+ /**
25224
+ * Spend coins through this builder's function, or through `unlocker`
25225
+ * (`other.unlock.<name>(...args)`, same client), which `ark.transaction()` requires. Calls add
25226
+ * up; contract coins come first, in call order, then `.fund()` inputs.
25227
+ *
25228
+ * Every coin's script sees the same outputs: "output 0 pays X" passes once for all of them.
25229
+ * For one payment per coin, the contract must check `INSPECTNUMINPUTS` or `PUSHCURRENTINPUTINDEX`.
25230
+ */
25231
+ from(coins, unlocker = this.unlocker) {
25232
+ const list = Array.isArray(coins) ? coins : [coins];
25233
+ if (list.length === 0) throw new Error("from(): no coins given");
25234
+ if (!unlocker) {
25235
+ throw new Error("from(): name the function to spend through: contract.unlock.<name>()");
25236
+ }
25237
+ const { contract, fn } = unlocker;
25238
+ if (contract.client !== this.client) {
25239
+ throw new Error(`from(): ${fn.name} belongs to a contract on another Arkade client`);
25240
+ }
25241
+ const script = hex.encode(contract.pkScript);
25242
+ const taken = new Set(this.inputs.map(({ coin: c }) => `${c.txid}:${c.vout}`));
25243
+ const now = Date.now();
25244
+ for (const coin of list) {
25245
+ const outpoint = `${coin.txid}:${coin.vout}`;
25246
+ if (coin.script && coin.script !== script) {
25247
+ throw new Error(
25248
+ `from(): coin ${outpoint} is not locked by the contract of ${fn.name}`
25249
+ );
25250
+ }
25251
+ if (taken.has(outpoint))
25252
+ throw new Error(`from(): coin ${outpoint} is already an input`);
25253
+ const reason = unspendable(coin, now);
25254
+ if (reason) {
25255
+ throw new Error(
25256
+ `from(): coin ${outpoint} ${reason}; recover it into a batch instead`
25257
+ );
25258
+ }
25259
+ taken.add(outpoint);
25260
+ }
25261
+ this.inputs.push(...list.map((coin) => ({ coin, unlocker })));
24968
25262
  return this;
24969
25263
  }
24970
25264
  /**
24971
25265
  * Add extra inputs the caller funds (e.g. a taker's own coins in a swap).
24972
- * These become inputs 1..n and are signed with the client identity.
25266
+ * These come after the contract coins and are signed with the client identity.
24973
25267
  */
24974
25268
  fund(coins) {
24975
25269
  this.fundingCoins.push(...coins);
@@ -25006,23 +25300,39 @@ var ArkadeTransactionBuilder = class {
25006
25300
  }
25007
25301
  /** Assemble the unsigned ark transaction and its checkpoints. */
25008
25302
  async build() {
25009
- if (this.fn.def.tapscript.csv) {
25010
- throw new Error(
25011
- `${this.fn.name}: CSV leaves require an aged onchain output; the offchain builder creates fresh checkpoints \u2014 use the wallet's unilateral exit flow`
25012
- );
25303
+ const { arkTx, checkpoints } = await this.assemble();
25304
+ return { arkTx, checkpoints };
25305
+ }
25306
+ async assemble() {
25307
+ for (const { fn } of this.unlockers()) {
25308
+ if (fn.def.tapscript.csv) {
25309
+ throw new Error(
25310
+ `${fn.name}: CSV leaves require an aged onchain output; the offchain builder creates fresh checkpoints \u2014 use the wallet's unilateral exit flow`
25311
+ );
25312
+ }
25013
25313
  }
25014
25314
  if (this.outputs.length === 0) {
25015
25315
  throw new Error("ArkadeTransactionBuilder: at least one output is required");
25016
25316
  }
25017
25317
  const outputsSum = this.outputs.reduce((s, o) => s + (o.amount ?? 0n), 0n);
25018
- const coin = this.coin ?? await this.selectCoin(outputsSum);
25019
- const def = this.fn.def;
25318
+ let inputs = [...this.inputs];
25319
+ if (inputs.length === 0) {
25320
+ if (!this.unlocker) {
25321
+ throw new Error(
25322
+ "ArkadeTransactionBuilder: add coins with .from(coins, contract.unlock.<name>())"
25323
+ );
25324
+ }
25325
+ inputs = [
25326
+ { coin: await this.selectCoin(this.unlocker, outputsSum), unlocker: this.unlocker }
25327
+ ];
25328
+ }
25329
+ const coinsSum = inputs.reduce((s, { coin }) => s + BigInt(coin.value), 0n);
25020
25330
  const outputs = [...this.outputs];
25021
25331
  const fundingSum = this.fundingCoins.reduce((s, f) => s + BigInt(f.value), 0n);
25022
- const surplus = BigInt(coin.value) + fundingSum - outputsSum;
25332
+ const surplus = coinsSum + fundingSum - outputsSum;
25023
25333
  if (surplus < 0n) {
25024
25334
  throw new Error(
25025
- `ArkadeTransactionBuilder: insufficient inputs \u2014 outputs ${outputsSum} exceed inputs ${BigInt(coin.value) + fundingSum}`
25335
+ `ArkadeTransactionBuilder: insufficient inputs \u2014 outputs ${outputsSum} exceed inputs ${coinsSum + fundingSum}`
25026
25336
  );
25027
25337
  }
25028
25338
  if (surplus > 0n) {
@@ -25035,23 +25345,23 @@ var ArkadeTransactionBuilder = class {
25035
25345
  }
25036
25346
  const { arkTx, checkpoints } = buildOffchainTx(
25037
25347
  [
25038
- {
25348
+ ...inputs.map(({ coin, unlocker }) => ({
25039
25349
  txid: coin.txid,
25040
25350
  vout: coin.vout,
25041
25351
  value: coin.value,
25042
- tapLeafScript: this.fn.tapLeafScript,
25043
- tapTree: this.contract.tapTree
25044
- },
25352
+ tapLeafScript: unlocker.fn.tapLeafScript,
25353
+ tapTree: unlocker.contract.tapTree
25354
+ })),
25045
25355
  ...this.fundingCoins
25046
25356
  ],
25047
25357
  outputs,
25048
- this.contract.client.checkpoint
25358
+ this.client.checkpoint
25049
25359
  );
25050
- if (coin.sourceTx) {
25051
- setArkPsbtField(arkTx, 0, PrevArkTxField, coin.sourceTx);
25052
- }
25053
- if (this.fn.arkadeScript) {
25054
- const indexer = this.contract.client.indexer;
25360
+ inputs.forEach(({ coin }, i) => {
25361
+ if (coin.sourceTx) setArkPsbtField(arkTx, i, PrevArkTxField, coin.sourceTx);
25362
+ });
25363
+ if (inputs.some(({ unlocker }) => unlocker.fn.arkadeScript)) {
25364
+ const indexer = this.client.indexer;
25055
25365
  if (!indexer) {
25056
25366
  throw new PrevTxUnavailableError(
25057
25367
  "covenant spends require an `indexer` on the Arkade client to resolve the previous ark tx of each input"
@@ -25059,56 +25369,76 @@ var ArkadeTransactionBuilder = class {
25059
25369
  }
25060
25370
  await attachPrevArkTxs(
25061
25371
  arkTx,
25062
- [coin.txid, ...this.fundingCoins.map((c) => c.txid)],
25372
+ [...inputs.map(({ coin }) => coin), ...this.fundingCoins].map((c) => c.txid),
25063
25373
  indexer
25064
25374
  );
25065
25375
  }
25066
- const condition = (def.tapscript.witness ?? []).map((w) => this.witnessBytes(w));
25067
- if (condition.length > 0) {
25068
- setArkPsbtField(arkTx, 0, ConditionWitness, condition);
25069
- setArkPsbtField(checkpoints[0], 0, ConditionWitness, condition);
25070
- }
25376
+ inputs.forEach(({ unlocker }, i) => {
25377
+ const condition = (unlocker.fn.def.tapscript.witness ?? []).map(
25378
+ (w) => witnessBytes(unlocker, w)
25379
+ );
25380
+ if (condition.length === 0) return;
25381
+ setArkPsbtField(arkTx, i, ConditionWitness, condition);
25382
+ setArkPsbtField(checkpoints[i], 0, ConditionWitness, condition);
25383
+ });
25071
25384
  const packets = [];
25072
25385
  if (this.assetSpecs.length > 0) {
25073
25386
  packets.push(this.buildAssetPacket());
25074
25387
  }
25075
- const arkadeScript = this.fn.arkadeScript;
25076
- if (arkadeScript) {
25077
- const stack = (def.arkadeScript?.witness ?? []).map((w) => this.witnessBytes(w));
25078
- packets.push(
25079
- EmulatorPacket.create([
25080
- { vin: 0, script: arkadeScript, witness: RawWitness.encode(stack) }
25081
- ])
25082
- );
25388
+ const entries = inputs.flatMap(({ unlocker }, vin) => {
25389
+ const { arkadeScript, def } = unlocker.fn;
25390
+ if (!arkadeScript) return [];
25391
+ const stack = (def.arkadeScript?.witness ?? []).map((w) => witnessBytes(unlocker, w));
25392
+ return [{ vin, script: arkadeScript, witness: RawWitness.encode(stack) }];
25393
+ });
25394
+ if (entries.length > 0) {
25395
+ packets.push(EmulatorPacket.create(entries));
25083
25396
  }
25084
25397
  packets.push(...this.extraPackets);
25085
25398
  if (packets.length > 0) {
25086
25399
  attachExtension(arkTx, packets);
25087
25400
  }
25088
- return { arkTx, checkpoints };
25401
+ return { arkTx, checkpoints, inputs };
25089
25402
  }
25090
25403
  /** Build, submit and return the finalized transaction. */
25091
25404
  async send() {
25092
- if (this.fn.def.tapscript.signers.some(
25093
- (signer) => typeof signer === "object" && "tweak" in signer
25094
- )) {
25095
- throw new Error(
25096
- `${this.fn.name}: covenant-tweaked leaf signing is not supported by Identity; use a signer that supports the covenant tweak with build()`
25097
- );
25405
+ const client = this.client;
25406
+ const userKey = client.userKey;
25407
+ for (const { contract, fn } of this.unlockers()) {
25408
+ if (fn.def.tapscript.signers.some(
25409
+ (signer) => typeof signer === "object" && "tweak" in signer
25410
+ )) {
25411
+ throw new Error(
25412
+ `${fn.name}: covenant-tweaked leaf signing is not supported by Identity; use a signer that supports the covenant tweak with build()`
25413
+ );
25414
+ }
25415
+ const { serverKey } = contract.keys;
25416
+ if (fn.signerKeys.some(
25417
+ (key) => !equalBytes(key, serverKey) && (!userKey || !equalBytes(key, userKey))
25418
+ )) {
25419
+ throw new Error(
25420
+ `${fn.name}: external leaf signatures are required; use build() and sign the PSBT with all required signers`
25421
+ );
25422
+ }
25098
25423
  }
25099
- const { serverKey } = this.contract.keys;
25100
- const userKey = this.contract.client.userKey;
25101
- if (this.fn.signerKeys.some(
25102
- (key) => !equalBytes(key, serverKey) && (!userKey || !equalBytes(key, userKey))
25103
- )) {
25424
+ const { arkTx, checkpoints, inputs } = await this.assemble();
25425
+ const userInputs = this.userInputIndexes(inputs);
25426
+ const serverOnly = inputs.find(
25427
+ ({ unlocker }, i) => !unlocker.fn.arkadeScript && !userInputs.includes(i)
25428
+ );
25429
+ if (serverOnly) {
25104
25430
  throw new Error(
25105
- `${this.fn.name}: external leaf signatures are required; use build() and sign the PSBT with all required signers`
25431
+ `${serverOnly.unlocker.fn.name}: a leaf without a covenant needs the user's signature, and this one has only the server's`
25106
25432
  );
25107
25433
  }
25108
- const { arkTx, checkpoints } = await this.build();
25109
- const client = this.contract.client;
25110
- const userInputs = this.userInputIndexes();
25111
- if (this.fn.arkadeScript) {
25434
+ inputs.forEach(({ coin }, vin) => {
25435
+ if (coin.assets?.length && !this.routesAssetsFrom(vin)) {
25436
+ throw new Error(
25437
+ `coin ${coin.txid}:${coin.vout} carries assets; route them with .withAsset() from input ${vin}`
25438
+ );
25439
+ }
25440
+ });
25441
+ if (inputs.some(({ unlocker }) => unlocker.fn.arkadeScript)) {
25112
25442
  if (!client.emulator) {
25113
25443
  throw new Error("covenant spends require an `emulator` on the Arkade client");
25114
25444
  }
@@ -25116,7 +25446,8 @@ var ArkadeTransactionBuilder = class {
25116
25446
  inputIndexes: userInputs
25117
25447
  });
25118
25448
  }
25119
- if (!client.identity) {
25449
+ const identity = client.identity;
25450
+ if (!identity) {
25120
25451
  throw new Error("a signing identity is required for non-covenant spends");
25121
25452
  }
25122
25453
  const ark = client.arkProvider;
@@ -25125,21 +25456,19 @@ var ArkadeTransactionBuilder = class {
25125
25456
  "broadcasting requires an `arkade` provider with `submitTx`/`finalizeTx` on the Arkade client"
25126
25457
  );
25127
25458
  }
25128
- const signedArk = await this.signArk(arkTx, userInputs);
25129
- const res = await ark.submitTx(
25130
- base64.encode(signedArk.toPSBT()),
25131
- checkpoints.map((c) => base64.encode(c.toPSBT()))
25132
- );
25133
- assertSubmittedArkTxid(res, signedArk, "submitTx");
25134
- const matched = matchServerCheckpoints(res.signedCheckpointTxs, checkpoints, "submitTx");
25135
- const finalCps = await Promise.all(
25136
- matched.map(
25137
- async ({ server }) => base64.encode((await client.identity.sign(server, [0])).toPSBT())
25138
- )
25459
+ const res = await submitOffchainTx(
25460
+ ark,
25461
+ { arkTx, checkpoints },
25462
+ {
25463
+ signArkTx: async (tx) => ({ arkTx: await this.signArk(tx, userInputs) }),
25464
+ signCheckpoint: (checkpoint) => identity.sign(checkpoint, [0])
25465
+ },
25466
+ void 0,
25467
+ { oneShot: client.oneShotSubmit }
25139
25468
  );
25140
- await ark.finalizeTx(res.arkTxid, finalCps);
25141
25469
  return {
25142
25470
  txid: res.arkTxid,
25471
+ // Required by ArkadeBroadcaster; RestArkProvider.submitTx rejects a response without it.
25143
25472
  signedArkTx: res.finalArkTx,
25144
25473
  signedCheckpointTxs: res.signedCheckpointTxs
25145
25474
  };
@@ -25147,31 +25476,44 @@ var ArkadeTransactionBuilder = class {
25147
25476
  /** Sign the client-owned inputs of the ark tx (no-op when there are none). */
25148
25477
  async signArk(arkTx, userInputs) {
25149
25478
  if (userInputs.length === 0) return arkTx;
25150
- const { identity } = this.contract.client;
25479
+ const { identity } = this.client;
25151
25480
  if (!identity) {
25152
25481
  throw new Error("this spend requires an `identity` to sign its user/funding inputs");
25153
25482
  }
25154
25483
  return identity.sign(arkTx, userInputs);
25155
25484
  }
25156
- /** Indexes of inputs the client owns and must sign (contract input + funded inputs). */
25157
- userInputIndexes() {
25158
- const idxs = [];
25159
- const userKey = this.contract.client.userKey;
25160
- if (userKey && this.fn.signerKeys.some((k) => equalBytes(k, userKey))) {
25161
- idxs.push(0);
25162
- }
25163
- for (let i = 0; i < this.fundingCoins.length; i++) {
25164
- idxs.push(i + 1);
25165
- }
25485
+ /** The functions this spend goes through: the `.from()` ones, else this builder's own. */
25486
+ unlockers() {
25487
+ if (this.inputs.length > 0) return this.inputs.map((i) => i.unlocker);
25488
+ return this.unlocker ? [this.unlocker] : [];
25489
+ }
25490
+ /** Indexes of inputs the client owns and must sign (contract inputs + funded inputs). */
25491
+ userInputIndexes(inputs) {
25492
+ const userKey = this.client.userKey;
25493
+ const idxs = inputs.flatMap(
25494
+ ({ unlocker }, i) => userKey && unlocker.fn.signerKeys.some((k) => equalBytes(k, userKey)) ? [i] : []
25495
+ );
25496
+ for (let i = 0; i < this.fundingCoins.length; i++) idxs.push(inputs.length + i);
25166
25497
  return idxs;
25167
25498
  }
25168
- async selectCoin(amount) {
25169
- const utxos = await this.contract.getUtxos();
25499
+ async selectCoin(unlocker, amount) {
25500
+ const now = Date.now();
25501
+ const routed = this.routesAssetsFrom(0);
25502
+ const utxos = (await unlocker.contract.getUtxos()).filter(
25503
+ (u) => !unspendable(u, now) && (routed || !u.assets?.length)
25504
+ );
25170
25505
  if (utxos.length === 0) throw new Error("no spendable coins for this contract");
25171
25506
  const covering = utxos.filter((u) => BigInt(u.value) >= amount).sort((a, b) => a.value - b.value);
25172
25507
  if (covering.length > 0) return covering[0];
25173
25508
  return [...utxos].sort((a, b) => b.value - a.value)[0];
25174
25509
  }
25510
+ /**
25511
+ * Whether input `vin`'s assets are routed: by a `.withAsset()` input, or a caller's asset packet.
25512
+ * A caller's packet counts for every input, unread; arkd checks what it actually routes.
25513
+ */
25514
+ routesAssetsFrom(vin) {
25515
+ return this.extraPackets.some((p) => p.type() === Packet.PACKET_TYPE) || this.assetSpecs.some((spec) => spec.inputs.some((i) => i.vin === vin));
25516
+ }
25175
25517
  buildAssetPacket() {
25176
25518
  const groups = this.assetSpecs.map((s) => {
25177
25519
  const id = typeof s.assetId === "string" ? AssetId.fromString(s.assetId) : AssetId.fromBytes(s.assetId);
@@ -25185,10 +25527,15 @@ var ArkadeTransactionBuilder = class {
25185
25527
  });
25186
25528
  return Packet.create(groups);
25187
25529
  }
25188
- witnessBytes(ref) {
25189
- return witnessRefToBytes(ref, this.args, this.contract.args);
25190
- }
25191
25530
  };
25531
+ function unspendable(coin, now) {
25532
+ if (coin.isSwept) return "was swept";
25533
+ if (coin.expiresAt && now >= new Date(coin.expiresAt).getTime()) return "has expired";
25534
+ return void 0;
25535
+ }
25536
+ function witnessBytes({ contract, args }, ref) {
25537
+ return witnessRefToBytes(ref, args, contract.args);
25538
+ }
25192
25539
  function bindInputs(fn, callArgs) {
25193
25540
  const inputs = fn.def.inputs ?? [];
25194
25541
  if (callArgs.length !== inputs.length) {
@@ -25688,5 +26035,5 @@ function createDefaultPaymentRouter(wallet) {
25688
26035
  }
25689
26036
 
25690
26037
  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 };
25691
- //# sourceMappingURL=chunk-NKKJEA3Z.js.map
25692
- //# sourceMappingURL=chunk-NKKJEA3Z.js.map
26038
+ //# sourceMappingURL=chunk-HAQAWBYG.js.map
26039
+ //# sourceMappingURL=chunk-HAQAWBYG.js.map