@arkade-os/swap 0.0.10 → 0.0.12

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
@@ -17,6 +17,7 @@ var L1_NETWORKS = {
17
17
  testnet: btc.TEST_NETWORK,
18
18
  regtest: { ...btc.TEST_NETWORK, bech32: "bcrt" }
19
19
  };
20
+ var l1ScriptForAddress = (address, network) => btc.OutScript.encode(btc.Address(L1_NETWORKS[network]).decode(address));
20
21
  function onchainHtlcScript(params, network) {
21
22
  if (params.claimKey.length !== 32 || params.refundKey.length !== 32) {
22
23
  throw new Error("claimKey and refundKey must be 32-byte x-only keys");
@@ -199,7 +200,11 @@ async function classifyOnchainHtlc(chain, input) {
199
200
  const best = utxos.sort((a, b) => b.amount > a.amount ? 1 : -1)[0];
200
201
  if (!best) {
201
202
  if (!input.funding) return { phase: "unfunded" };
202
- const spend = await chain.getSpendingTx(input.funding.txid, input.funding.vout);
203
+ const spend = await chain.getSpendingTx(
204
+ input.funding.txid,
205
+ input.funding.vout,
206
+ input.htlc.pkScript
207
+ );
203
208
  if (!spend) return { phase: "unfunded" };
204
209
  const preimage = extractPreimage(spend.txHex, input.htlc.paymentHash);
205
210
  const txid = btc.Transaction.fromRaw(hex.decode(spend.txHex), {
@@ -422,8 +427,22 @@ var assertPairLength = (pair) => {
422
427
  };
423
428
  var verifyLockupAddress = (quote, derivedAddress) => {
424
429
  const quoted = quote.profile?.lockup_address;
425
- if (derivedAddress !== quoted) throw new AddressMismatch(derivedAddress, quoted);
426
- return derivedAddress;
430
+ const candidates = Array.isArray(derivedAddress) ? derivedAddress : [derivedAddress];
431
+ const matched = candidates.find((address) => address === quoted);
432
+ if (matched === void 0) throw new AddressMismatch(candidates, quoted);
433
+ return matched;
434
+ };
435
+ var LOCKUP_SHAPE_VARIANTS = [void 0, "preTimelockedRefund"];
436
+ var matchQuotedLockup = (quote, hrp, serverPubkey, build) => {
437
+ const candidates = LOCKUP_SHAPE_VARIANTS.map((legacy) => {
438
+ const script = build(legacy);
439
+ return { script, address: script.address(hrp, serverPubkey).encode(), legacy };
440
+ });
441
+ const matchedAddress = verifyLockupAddress(
442
+ quote,
443
+ candidates.map((candidate) => candidate.address)
444
+ );
445
+ return candidates.find((candidate) => candidate.address === matchedAddress);
427
446
  };
428
447
  var assertFundable = (input) => {
429
448
  const fail = (reason, message) => {
@@ -437,6 +456,43 @@ var assertFundable = (input) => {
437
456
  if (input.quote.refund_locktime !== void 0 && input.quote.refund_locktime - input.now < MIN_HEADROOM_SECONDS) {
438
457
  fail("insufficient_headroom", "refund deadline headroom below 90 minutes");
439
458
  }
459
+ if (input.maxFee) {
460
+ const { bps, sats, referenceRate } = input.maxFee;
461
+ if (bps === void 0 && sats === void 0) {
462
+ fail("max_fee_unbounded", "maxFee names neither bps nor sats");
463
+ }
464
+ if (bps !== void 0 && (!Number.isInteger(bps) || bps < 0 || bps > 1e4)) {
465
+ fail("max_fee_out_of_range", `maxFee.bps must be an integer in 0..10000, got ${bps}`);
466
+ }
467
+ if (sats !== void 0 && (!Number.isInteger(sats) || sats < 0)) {
468
+ fail("max_fee_out_of_range", `maxFee.sats must be a non-negative integer, got ${sats}`);
469
+ }
470
+ const legs = input.quote.pair.split("->");
471
+ const assetOf = (leg) => leg.slice(leg.indexOf(":") + 1);
472
+ const sameAsset = legs.length === 2 && assetOf(legs[0]) === assetOf(legs[1]);
473
+ if (!sameAsset && referenceRate === void 0) {
474
+ fail(
475
+ "fee_gate_unavailable",
476
+ `maxFee cannot gate ${input.quote.pair}: its legs name different assets, so from_amount - to_amount is not a fee. Supply maxFee.referenceRate (to-units per from-unit) from a source of your OWN \u2014 reading it off the solver's published feed would check the solver against its own number`
477
+ );
478
+ }
479
+ if (!sameAsset && (!Number.isFinite(referenceRate) || referenceRate <= 0)) {
480
+ fail(
481
+ "max_fee_out_of_range",
482
+ `maxFee.referenceRate must be a positive finite number, got ${referenceRate}`
483
+ );
484
+ }
485
+ const fee = sameAsset ? input.quote.from_amount - input.quote.to_amount : Math.ceil(
486
+ (input.quote.from_amount * referenceRate - input.quote.to_amount) / referenceRate
487
+ );
488
+ const allowed = Math.max(
489
+ sats ?? 0,
490
+ Math.floor(input.quote.from_amount * (bps ?? 0) / 1e4)
491
+ );
492
+ if (fee > allowed) {
493
+ fail("fee_too_high", `fee ${fee} exceeds the ${allowed} this client allows`);
494
+ }
495
+ }
440
496
  if (input.onchain) {
441
497
  const { htlcLocktime, minConfirmations, direction } = input.onchain;
442
498
  if (!Number.isInteger(minConfirmations) || minConfirmations < 1 || minConfirmations > MAX_MIN_CONFIRMATIONS) {
@@ -625,13 +681,11 @@ function lightningSendVtxoScript(params) {
625
681
  unilateralRefundWithoutReceiverDelay: seconds(
626
682
  unilateralRefundWithoutReceiverDelay(params.claimDelay)
627
683
  ),
628
- nonInteractiveClaim: {
684
+ nonInteractiveParameters: {
629
685
  receiverPkScript: params.receiverPkScript,
630
- emulatorPubkey: params.emulatorPubkey
631
- },
632
- nonInteractiveRefund: {
633
686
  senderPkScript: params.refundPkScript,
634
- emulatorPubkey: params.emulatorPubkey
687
+ emulatorPubkey: params.emulatorPubkey,
688
+ ...params.legacy !== void 0 && { legacy: params.legacy }
635
689
  }
636
690
  });
637
691
  }
@@ -677,9 +731,18 @@ async function requestLightningSend(wallet, arkServerUrl, transport, params) {
677
731
  receiverPkScript: solverHex(receiverPkScriptHex, "profile.receiver_pk_script"),
678
732
  refundPkScript: secrets.pkScript
679
733
  };
680
- const script = lightningSendVtxoScript(treeParams);
681
- const address = script.address(network.hrp, serverPubkey).encode();
682
- verifyLockupAddress(quote, address);
734
+ const matched = matchQuotedLockup(
735
+ quote,
736
+ network.hrp,
737
+ serverPubkey,
738
+ (legacy) => lightningSendVtxoScript({ ...treeParams, ...legacy !== void 0 && { legacy } })
739
+ );
740
+ const script = matched.script;
741
+ const address = matched.address;
742
+ const matchedTreeParams = {
743
+ ...treeParams,
744
+ ...matched.legacy !== void 0 && { legacy: matched.legacy }
745
+ };
683
746
  assertFundable({
684
747
  quote,
685
748
  invoiceExpiresAt: params.invoice.expiresAt,
@@ -698,7 +761,7 @@ async function requestLightningSend(wallet, arkServerUrl, transport, params) {
698
761
  refundAddress,
699
762
  senderPubkey,
700
763
  secrets,
701
- treeParams
764
+ treeParams: matchedTreeParams
702
765
  };
703
766
  }
704
767
  var offerTermsFromQuote = (quote, assets) => {
@@ -763,7 +826,7 @@ function deriveOnchainSend(input) {
763
826
  if (refundLocktime === void 0 || htlcPubkey === void 0 || htlcLocktime === void 0 || minConfirmations === void 0 || receiverPkScriptHex === void 0) {
764
827
  throw new Error("onchain-send quote is missing a binding field");
765
828
  }
766
- const script = lightningSendVtxoScript({
829
+ const treeParams = {
767
830
  solverPubkey: toXOnly(hex3.decode(quote.solver_pubkey), "solver key"),
768
831
  refundLocktime,
769
832
  serverPubkey: input.serverPubkey,
@@ -773,9 +836,13 @@ function deriveOnchainSend(input) {
773
836
  senderPubkey: input.senderPubkey,
774
837
  receiverPkScript: solverHex(receiverPkScriptHex, "profile.receiver_pk_script"),
775
838
  refundPkScript: ArkAddress2.decode(input.refundAddress).pkScript
776
- });
777
- const address = script.address(input.hrp, input.serverPubkey).encode();
778
- verifyLockupAddress(quote, address);
839
+ };
840
+ const { script, address } = matchQuotedLockup(
841
+ quote,
842
+ input.hrp,
843
+ input.serverPubkey,
844
+ (legacy) => lightningSendVtxoScript({ ...treeParams, ...legacy !== void 0 && { legacy } })
845
+ );
779
846
  const htlcParams = {
780
847
  paymentHash: input.paymentHash,
781
848
  claimKey: input.payoutPubkey,
@@ -821,6 +888,7 @@ async function requestOnchainSend(wallet, arkServerUrl, transport, params) {
821
888
  amountSide: params.amountSide
822
889
  })
823
890
  );
891
+ assertQuotedAmount(quote, params.amountSide, params.amount);
824
892
  const network = getNetwork(info.network);
825
893
  const derived = deriveOnchainSend({
826
894
  quote,
@@ -950,13 +1018,11 @@ function receiveVtxoScript(params) {
950
1018
  unilateralRefundWithoutReceiverDelay: seconds(
951
1019
  unilateralRefundWithoutReceiverDelay(params.claimDelay)
952
1020
  ),
953
- nonInteractiveClaim: {
1021
+ nonInteractiveParameters: {
954
1022
  receiverPkScript: params.payoutPkScript,
955
- emulatorPubkey: params.emulatorPubkey
956
- },
957
- nonInteractiveRefund: {
958
1023
  senderPkScript: params.solverRefundPkScript,
959
- emulatorPubkey: params.emulatorPubkey
1024
+ emulatorPubkey: params.emulatorPubkey,
1025
+ ...params.legacy !== void 0 && { legacy: params.legacy }
960
1026
  }
961
1027
  });
962
1028
  }
@@ -980,10 +1046,23 @@ function deriveLightningReceive(input) {
980
1046
  payoutPubkey: input.payoutPubkey,
981
1047
  payoutPkScript: ArkAddress2.decode(input.payoutAddress).pkScript
982
1048
  };
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 };
1049
+ const matched = matchQuotedLockup(
1050
+ quote,
1051
+ input.hrp,
1052
+ input.serverPubkey,
1053
+ (legacy) => receiveVtxoScript({ ...treeParams, ...legacy !== void 0 && { legacy } })
1054
+ );
1055
+ return {
1056
+ address: matched.address,
1057
+ swapPkScript: matched.script.pkScript,
1058
+ script: matched.script,
1059
+ invoice,
1060
+ refundLocktime,
1061
+ treeParams: {
1062
+ ...treeParams,
1063
+ ...matched.legacy !== void 0 && { legacy: matched.legacy }
1064
+ }
1065
+ };
987
1066
  }
988
1067
  async function requestLightningReceive(wallet, arkServerUrl, transport, params) {
989
1068
  const rfqId = params.rfqId ?? newRfqId();
@@ -1071,7 +1150,7 @@ function deriveOnchainReceive(input) {
1071
1150
  if (refundLocktime === void 0 || claimPubkey === void 0 || htlcLocktime === void 0 || minConfirmations === void 0 || solverRefundPkScriptHex === void 0) {
1072
1151
  throw new Error("onchain-receive quote is missing a binding field");
1073
1152
  }
1074
- const script = receiveVtxoScript({
1153
+ const treeParams = {
1075
1154
  solverPubkey: toXOnly(hex3.decode(quote.solver_pubkey), "solver key"),
1076
1155
  refundLocktime,
1077
1156
  serverPubkey: input.serverPubkey,
@@ -1081,9 +1160,13 @@ function deriveOnchainReceive(input) {
1081
1160
  solverRefundPkScript: solverHex(solverRefundPkScriptHex, "profile.solver_refund_pk_script"),
1082
1161
  payoutPubkey: input.payoutPubkey,
1083
1162
  payoutPkScript: ArkAddress2.decode(input.payoutAddress).pkScript
1084
- });
1085
- const address = script.address(input.hrp, input.serverPubkey).encode();
1086
- verifyLockupAddress(quote, address);
1163
+ };
1164
+ const { script, address } = matchQuotedLockup(
1165
+ quote,
1166
+ input.hrp,
1167
+ input.serverPubkey,
1168
+ (legacy) => receiveVtxoScript({ ...treeParams, ...legacy !== void 0 && { legacy } })
1169
+ );
1087
1170
  const htlc = onchainHtlcScript(
1088
1171
  {
1089
1172
  paymentHash: input.paymentHash,
@@ -1190,6 +1273,8 @@ export {
1190
1273
  ONCHAIN_DUST_SATS,
1191
1274
  newPreimage,
1192
1275
  paymentHashOf,
1276
+ L1_NETWORKS,
1277
+ l1ScriptForAddress,
1193
1278
  onchainHtlcScript,
1194
1279
  buildHtlcClaim,
1195
1280
  buildHtlcRefund,