@arkade-os/swap 0.0.10 → 0.0.11

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.
package/README.md CHANGED
@@ -759,6 +759,23 @@ through their stored `preimageHex` or their HD descriptor, and `DB_VERSION` is u
759
759
 
760
760
  Notes from before 0.0.1, kept for consumers who tracked the branch.
761
761
 
762
+ - **`refundIfUnresolved` reports an exited lockup, and its input gained `paymentHash`.**
763
+ `RefundOutcome` has a new `{ outcome: "exited"; outpoints; status }` variant: a lockup whose
764
+ outputs were unilaterally exited lives onchain under the VHTLC script, where no offchain refund
765
+ reaches it. It used to come back as `nothing_to_refund`, which reads as "already resolved" over
766
+ money still sitting at the script — the swept case gets `needs_recovery` for the same reason, and
767
+ this is deliberately **not** that variant: recovery into a fresh batch is a spend no batch can
768
+ make for an onchain output. Complete the unroll and spend the outputs onchain instead.
769
+
770
+ Two required changes for direct callers. The input gains **`paymentHash`** (`sha256(P)` hex — the
771
+ quote's `payment_hash`, which callers already hold): the exit is read through `readLockupFate`,
772
+ and the VHTLC script cannot supply it, since its `preimageHash` is a `hash160` of the same secret.
773
+ And the `indexer` parameter widened from `RefundIndexer` to **`LockupSpendIndexer`**, so it must
774
+ now carry `getVirtualTxs` as well as `getVtxos` — a real `RestIndexerProvider` already does.
775
+
776
+ A lockup funded in two sends of which only one exited reports `exited` for the whole thing and
777
+ leaves the live half unrefunded. That matches `RfqSwapManager`, which reports the same lockup
778
+ `exited` on the same any-output rule; the two must not disagree.
762
779
  - **`RfqSwapManager` can own its own persistence.** New optional
763
780
  `RfqSwapManagerDeps.repository`, new `restoreFromRepository()` and `pruneRetiredSwaps()`, and
764
781
  `addSwap(swap, origin?)` gains an optional second argument. Nothing narrows and nothing is
@@ -422,8 +422,22 @@ var assertPairLength = (pair) => {
422
422
  };
423
423
  var verifyLockupAddress = (quote, derivedAddress) => {
424
424
  const quoted = quote.profile?.lockup_address;
425
- if (derivedAddress !== quoted) throw new AddressMismatch(derivedAddress, quoted);
426
- return derivedAddress;
425
+ const candidates = Array.isArray(derivedAddress) ? derivedAddress : [derivedAddress];
426
+ const matched = candidates.find((address) => address === quoted);
427
+ if (matched === void 0) throw new AddressMismatch(candidates, quoted);
428
+ return matched;
429
+ };
430
+ var LOCKUP_SHAPE_VARIANTS = [void 0, "preTimelockedRefund"];
431
+ var matchQuotedLockup = (quote, hrp, serverPubkey, build) => {
432
+ const candidates = LOCKUP_SHAPE_VARIANTS.map((legacy) => {
433
+ const script = build(legacy);
434
+ return { script, address: script.address(hrp, serverPubkey).encode(), legacy };
435
+ });
436
+ const matchedAddress = verifyLockupAddress(
437
+ quote,
438
+ candidates.map((candidate) => candidate.address)
439
+ );
440
+ return candidates.find((candidate) => candidate.address === matchedAddress);
427
441
  };
428
442
  var assertFundable = (input) => {
429
443
  const fail = (reason, message) => {
@@ -625,13 +639,11 @@ function lightningSendVtxoScript(params) {
625
639
  unilateralRefundWithoutReceiverDelay: seconds(
626
640
  unilateralRefundWithoutReceiverDelay(params.claimDelay)
627
641
  ),
628
- nonInteractiveClaim: {
642
+ nonInteractiveParameters: {
629
643
  receiverPkScript: params.receiverPkScript,
630
- emulatorPubkey: params.emulatorPubkey
631
- },
632
- nonInteractiveRefund: {
633
644
  senderPkScript: params.refundPkScript,
634
- emulatorPubkey: params.emulatorPubkey
645
+ emulatorPubkey: params.emulatorPubkey,
646
+ ...params.legacy !== void 0 && { legacy: params.legacy }
635
647
  }
636
648
  });
637
649
  }
@@ -677,9 +689,18 @@ async function requestLightningSend(wallet, arkServerUrl, transport, params) {
677
689
  receiverPkScript: solverHex(receiverPkScriptHex, "profile.receiver_pk_script"),
678
690
  refundPkScript: secrets.pkScript
679
691
  };
680
- const script = lightningSendVtxoScript(treeParams);
681
- const address = script.address(network.hrp, serverPubkey).encode();
682
- verifyLockupAddress(quote, address);
692
+ const matched = matchQuotedLockup(
693
+ quote,
694
+ network.hrp,
695
+ serverPubkey,
696
+ (legacy) => lightningSendVtxoScript({ ...treeParams, ...legacy !== void 0 && { legacy } })
697
+ );
698
+ const script = matched.script;
699
+ const address = matched.address;
700
+ const matchedTreeParams = {
701
+ ...treeParams,
702
+ ...matched.legacy !== void 0 && { legacy: matched.legacy }
703
+ };
683
704
  assertFundable({
684
705
  quote,
685
706
  invoiceExpiresAt: params.invoice.expiresAt,
@@ -698,7 +719,7 @@ async function requestLightningSend(wallet, arkServerUrl, transport, params) {
698
719
  refundAddress,
699
720
  senderPubkey,
700
721
  secrets,
701
- treeParams
722
+ treeParams: matchedTreeParams
702
723
  };
703
724
  }
704
725
  var offerTermsFromQuote = (quote, assets) => {
@@ -763,7 +784,7 @@ function deriveOnchainSend(input) {
763
784
  if (refundLocktime === void 0 || htlcPubkey === void 0 || htlcLocktime === void 0 || minConfirmations === void 0 || receiverPkScriptHex === void 0) {
764
785
  throw new Error("onchain-send quote is missing a binding field");
765
786
  }
766
- const script = lightningSendVtxoScript({
787
+ const treeParams = {
767
788
  solverPubkey: toXOnly(hex3.decode(quote.solver_pubkey), "solver key"),
768
789
  refundLocktime,
769
790
  serverPubkey: input.serverPubkey,
@@ -773,9 +794,13 @@ function deriveOnchainSend(input) {
773
794
  senderPubkey: input.senderPubkey,
774
795
  receiverPkScript: solverHex(receiverPkScriptHex, "profile.receiver_pk_script"),
775
796
  refundPkScript: ArkAddress2.decode(input.refundAddress).pkScript
776
- });
777
- const address = script.address(input.hrp, input.serverPubkey).encode();
778
- verifyLockupAddress(quote, address);
797
+ };
798
+ const { script, address } = matchQuotedLockup(
799
+ quote,
800
+ input.hrp,
801
+ input.serverPubkey,
802
+ (legacy) => lightningSendVtxoScript({ ...treeParams, ...legacy !== void 0 && { legacy } })
803
+ );
779
804
  const htlcParams = {
780
805
  paymentHash: input.paymentHash,
781
806
  claimKey: input.payoutPubkey,
@@ -950,13 +975,11 @@ function receiveVtxoScript(params) {
950
975
  unilateralRefundWithoutReceiverDelay: seconds(
951
976
  unilateralRefundWithoutReceiverDelay(params.claimDelay)
952
977
  ),
953
- nonInteractiveClaim: {
978
+ nonInteractiveParameters: {
954
979
  receiverPkScript: params.payoutPkScript,
955
- emulatorPubkey: params.emulatorPubkey
956
- },
957
- nonInteractiveRefund: {
958
980
  senderPkScript: params.solverRefundPkScript,
959
- emulatorPubkey: params.emulatorPubkey
981
+ emulatorPubkey: params.emulatorPubkey,
982
+ ...params.legacy !== void 0 && { legacy: params.legacy }
960
983
  }
961
984
  });
962
985
  }
@@ -980,10 +1003,23 @@ function deriveLightningReceive(input) {
980
1003
  payoutPubkey: input.payoutPubkey,
981
1004
  payoutPkScript: ArkAddress2.decode(input.payoutAddress).pkScript
982
1005
  };
983
- const script = receiveVtxoScript(treeParams);
984
- const address = script.address(input.hrp, input.serverPubkey).encode();
985
- verifyLockupAddress(quote, address);
986
- return { address, swapPkScript: script.pkScript, script, invoice, refundLocktime, treeParams };
1006
+ const matched = matchQuotedLockup(
1007
+ quote,
1008
+ input.hrp,
1009
+ input.serverPubkey,
1010
+ (legacy) => receiveVtxoScript({ ...treeParams, ...legacy !== void 0 && { legacy } })
1011
+ );
1012
+ return {
1013
+ address: matched.address,
1014
+ swapPkScript: matched.script.pkScript,
1015
+ script: matched.script,
1016
+ invoice,
1017
+ refundLocktime,
1018
+ treeParams: {
1019
+ ...treeParams,
1020
+ ...matched.legacy !== void 0 && { legacy: matched.legacy }
1021
+ }
1022
+ };
987
1023
  }
988
1024
  async function requestLightningReceive(wallet, arkServerUrl, transport, params) {
989
1025
  const rfqId = params.rfqId ?? newRfqId();
@@ -1071,7 +1107,7 @@ function deriveOnchainReceive(input) {
1071
1107
  if (refundLocktime === void 0 || claimPubkey === void 0 || htlcLocktime === void 0 || minConfirmations === void 0 || solverRefundPkScriptHex === void 0) {
1072
1108
  throw new Error("onchain-receive quote is missing a binding field");
1073
1109
  }
1074
- const script = receiveVtxoScript({
1110
+ const treeParams = {
1075
1111
  solverPubkey: toXOnly(hex3.decode(quote.solver_pubkey), "solver key"),
1076
1112
  refundLocktime,
1077
1113
  serverPubkey: input.serverPubkey,
@@ -1081,9 +1117,13 @@ function deriveOnchainReceive(input) {
1081
1117
  solverRefundPkScript: solverHex(solverRefundPkScriptHex, "profile.solver_refund_pk_script"),
1082
1118
  payoutPubkey: input.payoutPubkey,
1083
1119
  payoutPkScript: ArkAddress2.decode(input.payoutAddress).pkScript
1084
- });
1085
- const address = script.address(input.hrp, input.serverPubkey).encode();
1086
- verifyLockupAddress(quote, address);
1120
+ };
1121
+ const { script, address } = matchQuotedLockup(
1122
+ quote,
1123
+ input.hrp,
1124
+ input.serverPubkey,
1125
+ (legacy) => receiveVtxoScript({ ...treeParams, ...legacy !== void 0 && { legacy } })
1126
+ );
1087
1127
  const htlc = onchainHtlcScript(
1088
1128
  {
1089
1129
  paymentHash: input.paymentHash,
package/dist/index.cjs CHANGED
@@ -411,12 +411,30 @@ var swapPrograms = {
411
411
  wantAsset: import_sdk2.arkade.parseArtifact(swap_want_asset_program_default),
412
412
  wantBtc: import_sdk2.arkade.parseArtifact(swap_want_btc_program_default)
413
413
  };
414
+ function withExitClosure(program, exit) {
415
+ if (!exit) return program;
416
+ return {
417
+ ...program,
418
+ // typed params are authoritative: an undeclared `$exitDelay` fails
419
+ // validateProgram instead of compiling against an unbound value
420
+ params: [...program.params ?? [], { name: "exitDelay", type: "int" }],
421
+ functions: {
422
+ ...program.functions,
423
+ exit: {
424
+ tapscript: { signers: ["$user"], csv: { type: exit.type, value: "$exitDelay" } }
425
+ }
426
+ }
427
+ };
428
+ }
414
429
  function swapProgramBinding(offer, serverPubkey) {
415
430
  if (offer.makerPkScript.length !== FIELDS.makerPkScript.width) {
416
431
  throw new Error("makerPkScript is not a 34-byte taproot scriptPubKey");
417
432
  }
418
433
  return {
419
- program: offer.wantAsset ? swapPrograms.wantAsset : swapPrograms.wantBtc,
434
+ program: withExitClosure(
435
+ offer.wantAsset ? swapPrograms.wantAsset : swapPrograms.wantBtc,
436
+ offer.exitDelay
437
+ ),
420
438
  args: {
421
439
  makerWP: offer.makerPkScript.subarray(2),
422
440
  wantAmount: offer.wantAmount,
@@ -426,7 +444,8 @@ function swapProgramBinding(offer, serverPubkey) {
426
444
  ...offer.wantAsset && {
427
445
  wantAssetTxid: offer.wantAsset.txid.slice().reverse(),
428
446
  wantAssetGroupIndex: offer.wantAsset.groupIndex
429
- }
447
+ },
448
+ ...offer.exitDelay && { exitDelay: offer.exitDelay.value }
430
449
  },
431
450
  keys: {
432
451
  serverKey: serverPubkey,
@@ -447,9 +466,27 @@ var FIELDS = {
447
466
  makerPkScript: { tag: 5, width: 34 },
448
467
  makerPublicKey: { tag: 7, width: 32 },
449
468
  emulatorPubkey: { tag: 8, width: 32 },
450
- offerAsset: { tag: 11, width: void 0 }
469
+ ratioNum: { tag: 9, width: 8 },
470
+ ratioDen: { tag: 10, width: 8 },
471
+ offerAsset: { tag: 11, width: void 0 },
472
+ exitTimelock: { tag: 12, width: 9 }
451
473
  };
474
+ var EXIT_TYPES = ["blocks", "seconds"];
452
475
  var NAMES = Object.fromEntries(Object.entries(FIELDS).map(([k, f]) => [f.tag, k]));
476
+ function u64(name, value) {
477
+ if (value < BigInt(0) || value >> BigInt(64) > BigInt(0)) {
478
+ throw new Error(`${name} does not fit the offer wire format (u64)`);
479
+ }
480
+ const out = new Uint8Array(FIELDS.wantAmount.width);
481
+ new DataView(out.buffer).setBigUint64(0, value, false);
482
+ return out;
483
+ }
484
+ var readU64 = (value) => new DataView(value.buffer, value.byteOffset).getBigUint64(0, false);
485
+ var setRatio = (name, value) => {
486
+ if (value === void 0 || value === BigInt(0)) return void 0;
487
+ if (value < BigInt(0)) throw new Error(`${name} does not fit the offer wire format (u64)`);
488
+ return value;
489
+ };
453
490
  function tlv(type, value) {
454
491
  if (value.length > 65535) throw new Error("TLV value exceeds the u16 length field");
455
492
  return (0, import_utils.concatBytes)(Uint8Array.of(type, value.length >> 8 & 255, value.length & 255), value);
@@ -468,24 +505,45 @@ function encodeOffer(offer) {
468
505
  throw new Error(`${name} must be ${FIELDS[name].width} bytes`);
469
506
  }
470
507
  }
471
- if (offer.wantAmount < BigInt(0) || offer.wantAmount >> BigInt(64) > BigInt(0)) {
472
- throw new Error("wantAmount does not fit the offer wire format (u64)");
508
+ const ratioNum = setRatio("ratioNum", offer.ratioNum);
509
+ const ratioDen = setRatio("ratioDen", offer.ratioDen);
510
+ if (ratioNum === void 0 !== (ratioDen === void 0)) {
511
+ throw new Error("offer must carry both ratioNum and ratioDen, or neither");
473
512
  }
474
- const amount = new Uint8Array(FIELDS.wantAmount.width);
475
- new DataView(amount.buffer).setBigUint64(0, offer.wantAmount, false);
476
513
  const recs = [
477
514
  tlv(FIELDS.swapPkScript.tag, offer.swapPkScript),
478
- tlv(FIELDS.wantAmount.tag, amount)
515
+ tlv(FIELDS.wantAmount.tag, u64("wantAmount", offer.wantAmount))
479
516
  ];
480
517
  if (offer.wantAsset) recs.push(tlv(FIELDS.wantAsset.tag, offer.wantAsset.serialize()));
518
+ if (ratioNum !== void 0) recs.push(tlv(FIELDS.ratioNum.tag, u64("ratioNum", ratioNum)));
519
+ if (ratioDen !== void 0) recs.push(tlv(FIELDS.ratioDen.tag, u64("ratioDen", ratioDen)));
481
520
  if (offer.offerAsset) recs.push(tlv(FIELDS.offerAsset.tag, offer.offerAsset.serialize()));
482
521
  recs.push(
483
522
  tlv(FIELDS.makerPkScript.tag, offer.makerPkScript),
484
523
  tlv(FIELDS.makerPublicKey.tag, offer.makerPublicKey),
485
524
  tlv(FIELDS.emulatorPubkey.tag, offer.emulatorPubkey)
486
525
  );
526
+ if (offer.exitDelay) recs.push(tlv(FIELDS.exitTimelock.tag, encodeExitDelay(offer.exitDelay)));
487
527
  return (0, import_utils.concatBytes)(...recs);
488
528
  }
529
+ function encodeExitDelay(exit) {
530
+ return (0, import_utils.concatBytes)(
531
+ Uint8Array.of(EXIT_TYPES.indexOf(assertExitDelay(exit).type)),
532
+ u64("exitDelay", exit.value)
533
+ );
534
+ }
535
+ function assertExitDelay(exit) {
536
+ if (EXIT_TYPES.indexOf(exit.type) < 0) {
537
+ throw new Error(`unknown exitDelay locktime type: ${exit.type}`);
538
+ }
539
+ if (exit.value <= BigInt(0)) {
540
+ throw new Error("exitDelay must be a positive relative locktime");
541
+ }
542
+ if (exit.value >> BigInt(32) > BigInt(0)) {
543
+ throw new Error("exitDelay does not fit the locktime field (u32)");
544
+ }
545
+ return exit;
546
+ }
489
547
  function decodeOffer(data) {
490
548
  const fields = {};
491
549
  let off = 0;
@@ -505,6 +563,12 @@ function decodeOffer(data) {
505
563
  for (const name of ["wantAsset", "offerAsset"]) {
506
564
  if (fields[name]?.length === 0) throw new Error(`missing/invalid ${name}`);
507
565
  }
566
+ for (const [name, value] of Object.entries(fields)) {
567
+ const width = FIELDS[name].width;
568
+ if (width !== void 0 && value.length !== width) {
569
+ throw new Error(`missing/invalid ${name}`);
570
+ }
571
+ }
508
572
  const need = (name) => {
509
573
  const v = fields[name];
510
574
  const len = FIELDS[name].width;
@@ -516,16 +580,36 @@ function decodeOffer(data) {
516
580
  if (Boolean(fields.wantAsset) === Boolean(fields.offerAsset)) {
517
581
  throw new Error("offer must carry exactly one of wantAsset or offerAsset");
518
582
  }
583
+ const readRatio = (name) => {
584
+ const raw = fields[name];
585
+ if (!raw) return void 0;
586
+ const value = readU64(raw);
587
+ if (value === BigInt(0)) throw new Error(`missing/invalid ${name}`);
588
+ return value;
589
+ };
590
+ const ratioNum = readRatio("ratioNum");
591
+ const ratioDen = readRatio("ratioDen");
592
+ if (ratioNum === void 0 !== (ratioDen === void 0)) {
593
+ throw new Error("offer must carry both ratioNum and ratioDen, or neither");
594
+ }
519
595
  return {
520
596
  swapPkScript: need("swapPkScript"),
521
- wantAmount: new DataView(amount.buffer, amount.byteOffset).getBigUint64(0, false),
597
+ wantAmount: readU64(amount),
522
598
  ...fields.wantAsset && { wantAsset: import_sdk2.asset.AssetId.fromBytes(fields.wantAsset) },
523
599
  ...fields.offerAsset && { offerAsset: import_sdk2.asset.AssetId.fromBytes(fields.offerAsset) },
524
600
  makerPkScript: need("makerPkScript"),
525
601
  makerPublicKey: need("makerPublicKey"),
526
- emulatorPubkey: need("emulatorPubkey")
602
+ emulatorPubkey: need("emulatorPubkey"),
603
+ ...ratioNum !== void 0 && { ratioNum },
604
+ ...ratioDen !== void 0 && { ratioDen },
605
+ ...fields.exitTimelock && { exitDelay: decodeExitDelay(fields.exitTimelock) }
527
606
  };
528
607
  }
608
+ function decodeExitDelay(value) {
609
+ const type = EXIT_TYPES[value[0]];
610
+ if (!type) throw new Error(`unknown exitDelay locktime type: 0x${value[0].toString(16)}`);
611
+ return { type, value: readU64(value.subarray(1)) };
612
+ }
529
613
  var OFFER_CONTRACT_LABEL = "Arkade swap offer";
530
614
  var OFFER_CONTRACT_KIND = "asset-swap-offer";
531
615
  async function registerOfferContract(wallet, arkServerUrl, network, binding, serverPubkey, expectedPkScript) {
@@ -551,6 +635,14 @@ async function registerOfferContract(wallet, arkServerUrl, network, binding, ser
551
635
  });
552
636
  await promoteOfferContract(contractManager, import_base2.hex.encode(expectedPkScript));
553
637
  }
638
+ function serverExitDelay(delay) {
639
+ if (typeof delay !== "bigint" || delay <= BigInt(0)) {
640
+ throw new Error(
641
+ "the server reports no usable unilateralExitDelay; pass `exitDelay` to set the offer's exit closure explicitly, or `noExit: true` to publish without one"
642
+ );
643
+ }
644
+ return assertExitDelay({ value: delay, type: delay < BigInt(512) ? "blocks" : "seconds" });
645
+ }
554
646
  async function createOffer(wallet, arkServerUrl, params) {
555
647
  if (Boolean(params.wantAsset) === Boolean(params.offerAsset)) {
556
648
  throw new Error("set exactly one of wantAsset (BTC->asset) or offerAsset (asset->BTC)");
@@ -571,7 +663,11 @@ async function createOffer(wallet, arkServerUrl, params) {
571
663
  offerAsset: params.offerAsset,
572
664
  makerPkScript: import_sdk2.ArkAddress.decode(makerAddress).pkScript,
573
665
  makerPublicKey,
574
- emulatorPubkey: emuKey
666
+ emulatorPubkey: emuKey,
667
+ // checked HERE, before the covenant is derived and registered below:
668
+ // deferring it to `encodeOffer` leaves a registered contract behind for
669
+ // an offer that then fails to encode. @see assertExitDelay
670
+ exitDelay: params.noExit ? void 0 : params.exitDelay ? assertExitDelay(params.exitDelay) : serverExitDelay(info.unilateralExitDelay)
575
671
  };
576
672
  const script = offerVtxoScript(binding, serverPubKey);
577
673
  const offer = { ...binding, swapPkScript: script.pkScript };
@@ -1512,7 +1608,9 @@ function classifySpend(offer, serverPubkey, spendTx, deposit) {
1512
1608
  const script = offerVtxoScript(offer, serverPubkey);
1513
1609
  if (import_base6.hex.encode(script.pkScript) !== import_base6.hex.encode(offer.swapPkScript)) return "indeterminate";
1514
1610
  leaves = {
1515
- cancel: script.functionByName("cancel")?.leafScript,
1611
+ // both routes that hand the deposit back; `exit` is absent on an
1612
+ // offer that carries no exit closure, and drops out here
1613
+ returned: ["cancel", "exit"].map((name) => script.functionByName(name)?.leafScript).filter((leaf) => leaf !== void 0),
1516
1614
  fulfill: script.functionByName("fulfill")?.leafScript
1517
1615
  };
1518
1616
  } catch {
@@ -1524,7 +1622,7 @@ function classifySpend(offer, serverPubkey, spendTx, deposit) {
1524
1622
  if (import_base6.hex.encode(input.txid) !== deposit.txid) continue;
1525
1623
  for (const leaf of input.tapLeafScript ?? []) {
1526
1624
  const spent = import_base6.hex.encode((0, import_sdk6.scriptFromTapLeafScript)(leaf));
1527
- if (leaves.cancel && spent === import_base6.hex.encode(leaves.cancel)) return "cancelled";
1625
+ if (leaves.returned.some((back) => spent === import_base6.hex.encode(back))) return "cancelled";
1528
1626
  if (leaves.fulfill && spent === import_base6.hex.encode(leaves.fulfill)) return "fulfilled";
1529
1627
  }
1530
1628
  }
@@ -1918,8 +2016,22 @@ var assertPairLength = (pair) => {
1918
2016
  };
1919
2017
  var verifyLockupAddress = (quote, derivedAddress) => {
1920
2018
  const quoted = quote.profile?.lockup_address;
1921
- if (derivedAddress !== quoted) throw new AddressMismatch(derivedAddress, quoted);
1922
- return derivedAddress;
2019
+ const candidates = Array.isArray(derivedAddress) ? derivedAddress : [derivedAddress];
2020
+ const matched = candidates.find((address) => address === quoted);
2021
+ if (matched === void 0) throw new AddressMismatch(candidates, quoted);
2022
+ return matched;
2023
+ };
2024
+ var LOCKUP_SHAPE_VARIANTS = [void 0, "preTimelockedRefund"];
2025
+ var matchQuotedLockup = (quote, hrp, serverPubkey, build) => {
2026
+ const candidates = LOCKUP_SHAPE_VARIANTS.map((legacy) => {
2027
+ const script = build(legacy);
2028
+ return { script, address: script.address(hrp, serverPubkey).encode(), legacy };
2029
+ });
2030
+ const matchedAddress = verifyLockupAddress(
2031
+ quote,
2032
+ candidates.map((candidate) => candidate.address)
2033
+ );
2034
+ return candidates.find((candidate) => candidate.address === matchedAddress);
1923
2035
  };
1924
2036
  var assertFundable = (input) => {
1925
2037
  const fail = (reason, message) => {
@@ -2121,13 +2233,11 @@ function lightningSendVtxoScript(params) {
2121
2233
  unilateralRefundWithoutReceiverDelay: seconds(
2122
2234
  unilateralRefundWithoutReceiverDelay(params.claimDelay)
2123
2235
  ),
2124
- nonInteractiveClaim: {
2236
+ nonInteractiveParameters: {
2125
2237
  receiverPkScript: params.receiverPkScript,
2126
- emulatorPubkey: params.emulatorPubkey
2127
- },
2128
- nonInteractiveRefund: {
2129
2238
  senderPkScript: params.refundPkScript,
2130
- emulatorPubkey: params.emulatorPubkey
2239
+ emulatorPubkey: params.emulatorPubkey,
2240
+ ...params.legacy !== void 0 && { legacy: params.legacy }
2131
2241
  }
2132
2242
  });
2133
2243
  }
@@ -2173,9 +2283,18 @@ async function requestLightningSend(wallet, arkServerUrl, transport, params) {
2173
2283
  receiverPkScript: solverHex(receiverPkScriptHex, "profile.receiver_pk_script"),
2174
2284
  refundPkScript: secrets.pkScript
2175
2285
  };
2176
- const script = lightningSendVtxoScript(treeParams);
2177
- const address = script.address(network.hrp, serverPubkey).encode();
2178
- verifyLockupAddress(quote, address);
2286
+ const matched = matchQuotedLockup(
2287
+ quote,
2288
+ network.hrp,
2289
+ serverPubkey,
2290
+ (legacy) => lightningSendVtxoScript({ ...treeParams, ...legacy !== void 0 && { legacy } })
2291
+ );
2292
+ const script = matched.script;
2293
+ const address = matched.address;
2294
+ const matchedTreeParams = {
2295
+ ...treeParams,
2296
+ ...matched.legacy !== void 0 && { legacy: matched.legacy }
2297
+ };
2179
2298
  assertFundable({
2180
2299
  quote,
2181
2300
  invoiceExpiresAt: params.invoice.expiresAt,
@@ -2194,7 +2313,7 @@ async function requestLightningSend(wallet, arkServerUrl, transport, params) {
2194
2313
  refundAddress,
2195
2314
  senderPubkey,
2196
2315
  secrets,
2197
- treeParams
2316
+ treeParams: matchedTreeParams
2198
2317
  };
2199
2318
  }
2200
2319
  var offerTermsFromQuote = (quote, assets) => {
@@ -2259,7 +2378,7 @@ function deriveOnchainSend(input) {
2259
2378
  if (refundLocktime === void 0 || htlcPubkey === void 0 || htlcLocktime === void 0 || minConfirmations === void 0 || receiverPkScriptHex === void 0) {
2260
2379
  throw new Error("onchain-send quote is missing a binding field");
2261
2380
  }
2262
- const script = lightningSendVtxoScript({
2381
+ const treeParams = {
2263
2382
  solverPubkey: (0, import_sdk9.toXOnly)(import_base10.hex.decode(quote.solver_pubkey), "solver key"),
2264
2383
  refundLocktime,
2265
2384
  serverPubkey: input.serverPubkey,
@@ -2269,9 +2388,13 @@ function deriveOnchainSend(input) {
2269
2388
  senderPubkey: input.senderPubkey,
2270
2389
  receiverPkScript: solverHex(receiverPkScriptHex, "profile.receiver_pk_script"),
2271
2390
  refundPkScript: import_sdk9.ArkAddress.decode(input.refundAddress).pkScript
2272
- });
2273
- const address = script.address(input.hrp, input.serverPubkey).encode();
2274
- verifyLockupAddress(quote, address);
2391
+ };
2392
+ const { script, address } = matchQuotedLockup(
2393
+ quote,
2394
+ input.hrp,
2395
+ input.serverPubkey,
2396
+ (legacy) => lightningSendVtxoScript({ ...treeParams, ...legacy !== void 0 && { legacy } })
2397
+ );
2275
2398
  const htlcParams = {
2276
2399
  paymentHash: input.paymentHash,
2277
2400
  claimKey: input.payoutPubkey,
@@ -2446,13 +2569,11 @@ function receiveVtxoScript(params) {
2446
2569
  unilateralRefundWithoutReceiverDelay: seconds(
2447
2570
  unilateralRefundWithoutReceiverDelay(params.claimDelay)
2448
2571
  ),
2449
- nonInteractiveClaim: {
2572
+ nonInteractiveParameters: {
2450
2573
  receiverPkScript: params.payoutPkScript,
2451
- emulatorPubkey: params.emulatorPubkey
2452
- },
2453
- nonInteractiveRefund: {
2454
2574
  senderPkScript: params.solverRefundPkScript,
2455
- emulatorPubkey: params.emulatorPubkey
2575
+ emulatorPubkey: params.emulatorPubkey,
2576
+ ...params.legacy !== void 0 && { legacy: params.legacy }
2456
2577
  }
2457
2578
  });
2458
2579
  }
@@ -2476,10 +2597,23 @@ function deriveLightningReceive(input) {
2476
2597
  payoutPubkey: input.payoutPubkey,
2477
2598
  payoutPkScript: import_sdk9.ArkAddress.decode(input.payoutAddress).pkScript
2478
2599
  };
2479
- const script = receiveVtxoScript(treeParams);
2480
- const address = script.address(input.hrp, input.serverPubkey).encode();
2481
- verifyLockupAddress(quote, address);
2482
- return { address, swapPkScript: script.pkScript, script, invoice, refundLocktime, treeParams };
2600
+ const matched = matchQuotedLockup(
2601
+ quote,
2602
+ input.hrp,
2603
+ input.serverPubkey,
2604
+ (legacy) => receiveVtxoScript({ ...treeParams, ...legacy !== void 0 && { legacy } })
2605
+ );
2606
+ return {
2607
+ address: matched.address,
2608
+ swapPkScript: matched.script.pkScript,
2609
+ script: matched.script,
2610
+ invoice,
2611
+ refundLocktime,
2612
+ treeParams: {
2613
+ ...treeParams,
2614
+ ...matched.legacy !== void 0 && { legacy: matched.legacy }
2615
+ }
2616
+ };
2483
2617
  }
2484
2618
  async function requestLightningReceive(wallet, arkServerUrl, transport, params) {
2485
2619
  const rfqId = params.rfqId ?? newRfqId();
@@ -2567,7 +2701,7 @@ function deriveOnchainReceive(input) {
2567
2701
  if (refundLocktime === void 0 || claimPubkey === void 0 || htlcLocktime === void 0 || minConfirmations === void 0 || solverRefundPkScriptHex === void 0) {
2568
2702
  throw new Error("onchain-receive quote is missing a binding field");
2569
2703
  }
2570
- const script = receiveVtxoScript({
2704
+ const treeParams = {
2571
2705
  solverPubkey: (0, import_sdk9.toXOnly)(import_base10.hex.decode(quote.solver_pubkey), "solver key"),
2572
2706
  refundLocktime,
2573
2707
  serverPubkey: input.serverPubkey,
@@ -2577,9 +2711,13 @@ function deriveOnchainReceive(input) {
2577
2711
  solverRefundPkScript: solverHex(solverRefundPkScriptHex, "profile.solver_refund_pk_script"),
2578
2712
  payoutPubkey: input.payoutPubkey,
2579
2713
  payoutPkScript: import_sdk9.ArkAddress.decode(input.payoutAddress).pkScript
2580
- });
2581
- const address = script.address(input.hrp, input.serverPubkey).encode();
2582
- verifyLockupAddress(quote, address);
2714
+ };
2715
+ const { script, address } = matchQuotedLockup(
2716
+ quote,
2717
+ input.hrp,
2718
+ input.serverPubkey,
2719
+ (legacy) => receiveVtxoScript({ ...treeParams, ...legacy !== void 0 && { legacy } })
2720
+ );
2583
2721
  const htlc = onchainHtlcScript(
2584
2722
  {
2585
2723
  paymentHash: input.paymentHash,
@@ -2820,10 +2958,10 @@ async function pushRefundWithoutReceiver(ark, input) {
2820
2958
  input.script.options.refundLocktime
2821
2959
  );
2822
2960
  }
2823
- const refundPkScript = input.refundPkScript ?? input.script.options.nonInteractiveRefund?.senderPkScript;
2961
+ const refundPkScript = input.refundPkScript ?? input.script.options.nonInteractiveParameters?.senderPkScript;
2824
2962
  if (!refundPkScript) {
2825
2963
  throw new Error(
2826
- "no refund destination: the contract carries no nonInteractiveRefund leaf, so pass refundPkScript explicitly"
2964
+ "no refund destination: the contract carries no emulator covenant suite, so pass refundPkScript explicitly"
2827
2965
  );
2828
2966
  }
2829
2967
  const info = await ark.getInfo();
@@ -2875,6 +3013,21 @@ async function refundIfUnresolved(transport, ark, indexer, input) {
2875
3013
  const status = await transport.status(input.rfqId);
2876
3014
  if (status && isResolved(status.state)) return { outcome: "resolved", status };
2877
3015
  if (now() >= input.refundLocktime) {
3016
+ let fate = { fate: "unknown" };
3017
+ try {
3018
+ fate = await readLockupFate(indexer, {
3019
+ swapPkScript: input.script.pkScript,
3020
+ paymentHash: input.paymentHash
3021
+ });
3022
+ } catch {
3023
+ }
3024
+ if (fate.fate === "exited") {
3025
+ return {
3026
+ outcome: "exited",
3027
+ outpoints: fate.outpoints.map((o) => `${o.txid}:${o.vout}`),
3028
+ status
3029
+ };
3030
+ }
2878
3031
  const vtxos = await findLockupVtxos(indexer, input.script.pkScript);
2879
3032
  if (vtxos.length === 0) return { outcome: "nothing_to_refund", status };
2880
3033
  try {