@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/dist/index.cjs CHANGED
@@ -66,6 +66,8 @@ __export(index_exports, {
66
66
  RfqSwapManager: () => RfqSwapManager,
67
67
  RfqSwapOriginRequired: () => RfqSwapOriginRequired,
68
68
  SOLO_REFUND_HEADROOM_SECONDS: () => SOLO_REFUND_HEADROOM_SECONDS,
69
+ SOLVER_LIGHTNING_RAIL: () => SOLVER_LIGHTNING_RAIL,
70
+ SOLVER_ONCHAIN_RAIL: () => SOLVER_ONCHAIN_RAIL,
69
71
  SWAP_LOCKUP_CONTRACT_KIND: () => SWAP_LOCKUP_CONTRACT_KIND,
70
72
  SWAP_LOCKUP_CONTRACT_LABEL: () => SWAP_LOCKUP_CONTRACT_LABEL,
71
73
  SWAP_LOCKUP_CONTRACT_TYPE: () => SWAP_LOCKUP_CONTRACT_TYPE,
@@ -82,6 +84,7 @@ __export(index_exports, {
82
84
  buildHtlcClaim: () => buildHtlcClaim,
83
85
  buildHtlcRefund: () => buildHtlcRefund,
84
86
  cancelOffer: () => cancelOffer,
87
+ chainSourceFrom: () => chainSourceFrom,
85
88
  claimOnchainFill: () => claimOnchainFill,
86
89
  claimReceiveLockup: () => claimReceiveLockup,
87
90
  classifyDepositSpend: () => classifyDepositSpend,
@@ -103,6 +106,7 @@ __export(index_exports, {
103
106
  httpTransport: () => httpTransport,
104
107
  isRfqSwapTerminal: () => isRfqSwapTerminal,
105
108
  isRfqTerminal: () => isRfqTerminal,
109
+ l1ScriptForAddress: () => l1ScriptForAddress,
106
110
  lightningReceiveRequest: () => lightningReceiveRequest,
107
111
  lightningSendRequest: () => lightningSendRequest,
108
112
  lightningSendVtxoScript: () => lightningSendVtxoScript,
@@ -142,6 +146,11 @@ __export(index_exports, {
142
146
  sealClaimPacket: () => sealClaimPacket,
143
147
  senderIdentityForSwapRecord: () => senderIdentityForSwapRecord,
144
148
  shouldRetainRfqSwap: () => shouldRetainRfqSwap,
149
+ solverLightningRail: () => solverLightningRail,
150
+ solverLightningRendezvous: () => solverLightningRendezvous,
151
+ solverOnchainRail: () => solverOnchainRail,
152
+ solverOnchainRendezvous: () => solverOnchainRendezvous,
153
+ solverRendezvous: () => solverRendezvous,
145
154
  spendTxidsOf: () => spendTxidsOf,
146
155
  spendUpdate: () => spendUpdate,
147
156
  swapActivityResolver: () => swapActivityResolver,
@@ -411,12 +420,30 @@ var swapPrograms = {
411
420
  wantAsset: import_sdk2.arkade.parseArtifact(swap_want_asset_program_default),
412
421
  wantBtc: import_sdk2.arkade.parseArtifact(swap_want_btc_program_default)
413
422
  };
423
+ function withExitClosure(program, exit) {
424
+ if (!exit) return program;
425
+ return {
426
+ ...program,
427
+ // typed params are authoritative: an undeclared `$exitDelay` fails
428
+ // validateProgram instead of compiling against an unbound value
429
+ params: [...program.params ?? [], { name: "exitDelay", type: "int" }],
430
+ functions: {
431
+ ...program.functions,
432
+ exit: {
433
+ tapscript: { signers: ["$user"], csv: { type: exit.type, value: "$exitDelay" } }
434
+ }
435
+ }
436
+ };
437
+ }
414
438
  function swapProgramBinding(offer, serverPubkey) {
415
439
  if (offer.makerPkScript.length !== FIELDS.makerPkScript.width) {
416
440
  throw new Error("makerPkScript is not a 34-byte taproot scriptPubKey");
417
441
  }
418
442
  return {
419
- program: offer.wantAsset ? swapPrograms.wantAsset : swapPrograms.wantBtc,
443
+ program: withExitClosure(
444
+ offer.wantAsset ? swapPrograms.wantAsset : swapPrograms.wantBtc,
445
+ offer.exitDelay
446
+ ),
420
447
  args: {
421
448
  makerWP: offer.makerPkScript.subarray(2),
422
449
  wantAmount: offer.wantAmount,
@@ -426,7 +453,8 @@ function swapProgramBinding(offer, serverPubkey) {
426
453
  ...offer.wantAsset && {
427
454
  wantAssetTxid: offer.wantAsset.txid.slice().reverse(),
428
455
  wantAssetGroupIndex: offer.wantAsset.groupIndex
429
- }
456
+ },
457
+ ...offer.exitDelay && { exitDelay: offer.exitDelay.value }
430
458
  },
431
459
  keys: {
432
460
  serverKey: serverPubkey,
@@ -447,9 +475,27 @@ var FIELDS = {
447
475
  makerPkScript: { tag: 5, width: 34 },
448
476
  makerPublicKey: { tag: 7, width: 32 },
449
477
  emulatorPubkey: { tag: 8, width: 32 },
450
- offerAsset: { tag: 11, width: void 0 }
478
+ ratioNum: { tag: 9, width: 8 },
479
+ ratioDen: { tag: 10, width: 8 },
480
+ offerAsset: { tag: 11, width: void 0 },
481
+ exitTimelock: { tag: 12, width: 9 }
451
482
  };
483
+ var EXIT_TYPES = ["blocks", "seconds"];
452
484
  var NAMES = Object.fromEntries(Object.entries(FIELDS).map(([k, f]) => [f.tag, k]));
485
+ function u64(name, value) {
486
+ if (value < BigInt(0) || value >> BigInt(64) > BigInt(0)) {
487
+ throw new Error(`${name} does not fit the offer wire format (u64)`);
488
+ }
489
+ const out = new Uint8Array(FIELDS.wantAmount.width);
490
+ new DataView(out.buffer).setBigUint64(0, value, false);
491
+ return out;
492
+ }
493
+ var readU64 = (value) => new DataView(value.buffer, value.byteOffset).getBigUint64(0, false);
494
+ var setRatio = (name, value) => {
495
+ if (value === void 0 || value === BigInt(0)) return void 0;
496
+ if (value < BigInt(0)) throw new Error(`${name} does not fit the offer wire format (u64)`);
497
+ return value;
498
+ };
453
499
  function tlv(type, value) {
454
500
  if (value.length > 65535) throw new Error("TLV value exceeds the u16 length field");
455
501
  return (0, import_utils.concatBytes)(Uint8Array.of(type, value.length >> 8 & 255, value.length & 255), value);
@@ -468,24 +514,45 @@ function encodeOffer(offer) {
468
514
  throw new Error(`${name} must be ${FIELDS[name].width} bytes`);
469
515
  }
470
516
  }
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)");
517
+ const ratioNum = setRatio("ratioNum", offer.ratioNum);
518
+ const ratioDen = setRatio("ratioDen", offer.ratioDen);
519
+ if (ratioNum === void 0 !== (ratioDen === void 0)) {
520
+ throw new Error("offer must carry both ratioNum and ratioDen, or neither");
473
521
  }
474
- const amount = new Uint8Array(FIELDS.wantAmount.width);
475
- new DataView(amount.buffer).setBigUint64(0, offer.wantAmount, false);
476
522
  const recs = [
477
523
  tlv(FIELDS.swapPkScript.tag, offer.swapPkScript),
478
- tlv(FIELDS.wantAmount.tag, amount)
524
+ tlv(FIELDS.wantAmount.tag, u64("wantAmount", offer.wantAmount))
479
525
  ];
480
526
  if (offer.wantAsset) recs.push(tlv(FIELDS.wantAsset.tag, offer.wantAsset.serialize()));
527
+ if (ratioNum !== void 0) recs.push(tlv(FIELDS.ratioNum.tag, u64("ratioNum", ratioNum)));
528
+ if (ratioDen !== void 0) recs.push(tlv(FIELDS.ratioDen.tag, u64("ratioDen", ratioDen)));
481
529
  if (offer.offerAsset) recs.push(tlv(FIELDS.offerAsset.tag, offer.offerAsset.serialize()));
482
530
  recs.push(
483
531
  tlv(FIELDS.makerPkScript.tag, offer.makerPkScript),
484
532
  tlv(FIELDS.makerPublicKey.tag, offer.makerPublicKey),
485
533
  tlv(FIELDS.emulatorPubkey.tag, offer.emulatorPubkey)
486
534
  );
535
+ if (offer.exitDelay) recs.push(tlv(FIELDS.exitTimelock.tag, encodeExitDelay(offer.exitDelay)));
487
536
  return (0, import_utils.concatBytes)(...recs);
488
537
  }
538
+ function encodeExitDelay(exit) {
539
+ return (0, import_utils.concatBytes)(
540
+ Uint8Array.of(EXIT_TYPES.indexOf(assertExitDelay(exit).type)),
541
+ u64("exitDelay", exit.value)
542
+ );
543
+ }
544
+ function assertExitDelay(exit) {
545
+ if (EXIT_TYPES.indexOf(exit.type) < 0) {
546
+ throw new Error(`unknown exitDelay locktime type: ${exit.type}`);
547
+ }
548
+ if (exit.value <= BigInt(0)) {
549
+ throw new Error("exitDelay must be a positive relative locktime");
550
+ }
551
+ if (exit.value >> BigInt(32) > BigInt(0)) {
552
+ throw new Error("exitDelay does not fit the locktime field (u32)");
553
+ }
554
+ return exit;
555
+ }
489
556
  function decodeOffer(data) {
490
557
  const fields = {};
491
558
  let off = 0;
@@ -505,6 +572,12 @@ function decodeOffer(data) {
505
572
  for (const name of ["wantAsset", "offerAsset"]) {
506
573
  if (fields[name]?.length === 0) throw new Error(`missing/invalid ${name}`);
507
574
  }
575
+ for (const [name, value] of Object.entries(fields)) {
576
+ const width = FIELDS[name].width;
577
+ if (width !== void 0 && value.length !== width) {
578
+ throw new Error(`missing/invalid ${name}`);
579
+ }
580
+ }
508
581
  const need = (name) => {
509
582
  const v = fields[name];
510
583
  const len = FIELDS[name].width;
@@ -516,16 +589,36 @@ function decodeOffer(data) {
516
589
  if (Boolean(fields.wantAsset) === Boolean(fields.offerAsset)) {
517
590
  throw new Error("offer must carry exactly one of wantAsset or offerAsset");
518
591
  }
592
+ const readRatio = (name) => {
593
+ const raw = fields[name];
594
+ if (!raw) return void 0;
595
+ const value = readU64(raw);
596
+ if (value === BigInt(0)) throw new Error(`missing/invalid ${name}`);
597
+ return value;
598
+ };
599
+ const ratioNum = readRatio("ratioNum");
600
+ const ratioDen = readRatio("ratioDen");
601
+ if (ratioNum === void 0 !== (ratioDen === void 0)) {
602
+ throw new Error("offer must carry both ratioNum and ratioDen, or neither");
603
+ }
519
604
  return {
520
605
  swapPkScript: need("swapPkScript"),
521
- wantAmount: new DataView(amount.buffer, amount.byteOffset).getBigUint64(0, false),
606
+ wantAmount: readU64(amount),
522
607
  ...fields.wantAsset && { wantAsset: import_sdk2.asset.AssetId.fromBytes(fields.wantAsset) },
523
608
  ...fields.offerAsset && { offerAsset: import_sdk2.asset.AssetId.fromBytes(fields.offerAsset) },
524
609
  makerPkScript: need("makerPkScript"),
525
610
  makerPublicKey: need("makerPublicKey"),
526
- emulatorPubkey: need("emulatorPubkey")
611
+ emulatorPubkey: need("emulatorPubkey"),
612
+ ...ratioNum !== void 0 && { ratioNum },
613
+ ...ratioDen !== void 0 && { ratioDen },
614
+ ...fields.exitTimelock && { exitDelay: decodeExitDelay(fields.exitTimelock) }
527
615
  };
528
616
  }
617
+ function decodeExitDelay(value) {
618
+ const type = EXIT_TYPES[value[0]];
619
+ if (!type) throw new Error(`unknown exitDelay locktime type: 0x${value[0].toString(16)}`);
620
+ return { type, value: readU64(value.subarray(1)) };
621
+ }
529
622
  var OFFER_CONTRACT_LABEL = "Arkade swap offer";
530
623
  var OFFER_CONTRACT_KIND = "asset-swap-offer";
531
624
  async function registerOfferContract(wallet, arkServerUrl, network, binding, serverPubkey, expectedPkScript) {
@@ -551,6 +644,14 @@ async function registerOfferContract(wallet, arkServerUrl, network, binding, ser
551
644
  });
552
645
  await promoteOfferContract(contractManager, import_base2.hex.encode(expectedPkScript));
553
646
  }
647
+ function serverExitDelay(delay) {
648
+ if (typeof delay !== "bigint" || delay <= BigInt(0)) {
649
+ throw new Error(
650
+ "the server reports no usable unilateralExitDelay; pass `exitDelay` to set the offer's exit closure explicitly, or `noExit: true` to publish without one"
651
+ );
652
+ }
653
+ return assertExitDelay({ value: delay, type: delay < BigInt(512) ? "blocks" : "seconds" });
654
+ }
554
655
  async function createOffer(wallet, arkServerUrl, params) {
555
656
  if (Boolean(params.wantAsset) === Boolean(params.offerAsset)) {
556
657
  throw new Error("set exactly one of wantAsset (BTC->asset) or offerAsset (asset->BTC)");
@@ -571,7 +672,11 @@ async function createOffer(wallet, arkServerUrl, params) {
571
672
  offerAsset: params.offerAsset,
572
673
  makerPkScript: import_sdk2.ArkAddress.decode(makerAddress).pkScript,
573
674
  makerPublicKey,
574
- emulatorPubkey: emuKey
675
+ emulatorPubkey: emuKey,
676
+ // checked HERE, before the covenant is derived and registered below:
677
+ // deferring it to `encodeOffer` leaves a registered contract behind for
678
+ // an offer that then fails to encode. @see assertExitDelay
679
+ exitDelay: params.noExit ? void 0 : params.exitDelay ? assertExitDelay(params.exitDelay) : serverExitDelay(info.unilateralExitDelay)
575
680
  };
576
681
  const script = offerVtxoScript(binding, serverPubKey);
577
682
  const offer = { ...binding, swapPkScript: script.pkScript };
@@ -1062,6 +1167,7 @@ var L1_NETWORKS = {
1062
1167
  testnet: btc.TEST_NETWORK,
1063
1168
  regtest: { ...btc.TEST_NETWORK, bech32: "bcrt" }
1064
1169
  };
1170
+ var l1ScriptForAddress = (address, network) => btc.OutScript.encode(btc.Address(L1_NETWORKS[network]).decode(address));
1065
1171
  function onchainHtlcScript(params, network) {
1066
1172
  if (params.claimKey.length !== 32 || params.refundKey.length !== 32) {
1067
1173
  throw new Error("claimKey and refundKey must be 32-byte x-only keys");
@@ -1244,7 +1350,11 @@ async function classifyOnchainHtlc(chain, input) {
1244
1350
  const best = utxos.sort((a, b) => b.amount > a.amount ? 1 : -1)[0];
1245
1351
  if (!best) {
1246
1352
  if (!input.funding) return { phase: "unfunded" };
1247
- const spend = await chain.getSpendingTx(input.funding.txid, input.funding.vout);
1353
+ const spend = await chain.getSpendingTx(
1354
+ input.funding.txid,
1355
+ input.funding.vout,
1356
+ input.htlc.pkScript
1357
+ );
1248
1358
  if (!spend) return { phase: "unfunded" };
1249
1359
  const preimage = extractPreimage(spend.txHex, input.htlc.paymentHash);
1250
1360
  const txid = btc.Transaction.fromRaw(import_base3.hex.decode(spend.txHex), {
@@ -1303,7 +1413,8 @@ function onchainSendProfile(result) {
1303
1413
  htlcLocktime: result.htlcParams.refundLocktime,
1304
1414
  network: result.l1Network,
1305
1415
  htlcAddress: result.htlc.address,
1306
- minConfirmations: result.minConfirmations
1416
+ minConfirmations: result.minConfirmations,
1417
+ payoutPkScript: import_base4.hex.encode(result.payoutPkScript)
1307
1418
  };
1308
1419
  }
1309
1420
  var OnchainSendCorridor = {
@@ -1352,6 +1463,9 @@ var OnchainSendCorridor = {
1352
1463
  paymentHash,
1353
1464
  htlc,
1354
1465
  minConfirmations: profile.minConfirmations,
1466
+ // Optional here, required at the write: throwing on an older
1467
+ // record would strand the refund it is still owed.
1468
+ ...profile.payoutPkScript ? { payoutPkScript: import_base4.hex.decode(profile.payoutPkScript) } : {},
1355
1469
  ...profile.funding ? { funding: profile.funding } : {},
1356
1470
  ...profile.claimTxid ? { claimTxid: profile.claimTxid } : {}
1357
1471
  };
@@ -1512,7 +1626,9 @@ function classifySpend(offer, serverPubkey, spendTx, deposit) {
1512
1626
  const script = offerVtxoScript(offer, serverPubkey);
1513
1627
  if (import_base6.hex.encode(script.pkScript) !== import_base6.hex.encode(offer.swapPkScript)) return "indeterminate";
1514
1628
  leaves = {
1515
- cancel: script.functionByName("cancel")?.leafScript,
1629
+ // both routes that hand the deposit back; `exit` is absent on an
1630
+ // offer that carries no exit closure, and drops out here
1631
+ returned: ["cancel", "exit"].map((name) => script.functionByName(name)?.leafScript).filter((leaf) => leaf !== void 0),
1516
1632
  fulfill: script.functionByName("fulfill")?.leafScript
1517
1633
  };
1518
1634
  } catch {
@@ -1524,7 +1640,7 @@ function classifySpend(offer, serverPubkey, spendTx, deposit) {
1524
1640
  if (import_base6.hex.encode(input.txid) !== deposit.txid) continue;
1525
1641
  for (const leaf of input.tapLeafScript ?? []) {
1526
1642
  const spent = import_base6.hex.encode((0, import_sdk6.scriptFromTapLeafScript)(leaf));
1527
- if (leaves.cancel && spent === import_base6.hex.encode(leaves.cancel)) return "cancelled";
1643
+ if (leaves.returned.some((back) => spent === import_base6.hex.encode(back))) return "cancelled";
1528
1644
  if (leaves.fulfill && spent === import_base6.hex.encode(leaves.fulfill)) return "fulfilled";
1529
1645
  }
1530
1646
  }
@@ -1918,8 +2034,22 @@ var assertPairLength = (pair) => {
1918
2034
  };
1919
2035
  var verifyLockupAddress = (quote, derivedAddress) => {
1920
2036
  const quoted = quote.profile?.lockup_address;
1921
- if (derivedAddress !== quoted) throw new AddressMismatch(derivedAddress, quoted);
1922
- return derivedAddress;
2037
+ const candidates = Array.isArray(derivedAddress) ? derivedAddress : [derivedAddress];
2038
+ const matched = candidates.find((address) => address === quoted);
2039
+ if (matched === void 0) throw new AddressMismatch(candidates, quoted);
2040
+ return matched;
2041
+ };
2042
+ var LOCKUP_SHAPE_VARIANTS = [void 0, "preTimelockedRefund"];
2043
+ var matchQuotedLockup = (quote, hrp, serverPubkey, build) => {
2044
+ const candidates = LOCKUP_SHAPE_VARIANTS.map((legacy) => {
2045
+ const script = build(legacy);
2046
+ return { script, address: script.address(hrp, serverPubkey).encode(), legacy };
2047
+ });
2048
+ const matchedAddress = verifyLockupAddress(
2049
+ quote,
2050
+ candidates.map((candidate) => candidate.address)
2051
+ );
2052
+ return candidates.find((candidate) => candidate.address === matchedAddress);
1923
2053
  };
1924
2054
  var assertFundable = (input) => {
1925
2055
  const fail = (reason, message) => {
@@ -1933,6 +2063,43 @@ var assertFundable = (input) => {
1933
2063
  if (input.quote.refund_locktime !== void 0 && input.quote.refund_locktime - input.now < MIN_HEADROOM_SECONDS) {
1934
2064
  fail("insufficient_headroom", "refund deadline headroom below 90 minutes");
1935
2065
  }
2066
+ if (input.maxFee) {
2067
+ const { bps, sats, referenceRate } = input.maxFee;
2068
+ if (bps === void 0 && sats === void 0) {
2069
+ fail("max_fee_unbounded", "maxFee names neither bps nor sats");
2070
+ }
2071
+ if (bps !== void 0 && (!Number.isInteger(bps) || bps < 0 || bps > 1e4)) {
2072
+ fail("max_fee_out_of_range", `maxFee.bps must be an integer in 0..10000, got ${bps}`);
2073
+ }
2074
+ if (sats !== void 0 && (!Number.isInteger(sats) || sats < 0)) {
2075
+ fail("max_fee_out_of_range", `maxFee.sats must be a non-negative integer, got ${sats}`);
2076
+ }
2077
+ const legs = input.quote.pair.split("->");
2078
+ const assetOf = (leg) => leg.slice(leg.indexOf(":") + 1);
2079
+ const sameAsset = legs.length === 2 && assetOf(legs[0]) === assetOf(legs[1]);
2080
+ if (!sameAsset && referenceRate === void 0) {
2081
+ fail(
2082
+ "fee_gate_unavailable",
2083
+ `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`
2084
+ );
2085
+ }
2086
+ if (!sameAsset && (!Number.isFinite(referenceRate) || referenceRate <= 0)) {
2087
+ fail(
2088
+ "max_fee_out_of_range",
2089
+ `maxFee.referenceRate must be a positive finite number, got ${referenceRate}`
2090
+ );
2091
+ }
2092
+ const fee = sameAsset ? input.quote.from_amount - input.quote.to_amount : Math.ceil(
2093
+ (input.quote.from_amount * referenceRate - input.quote.to_amount) / referenceRate
2094
+ );
2095
+ const allowed = Math.max(
2096
+ sats ?? 0,
2097
+ Math.floor(input.quote.from_amount * (bps ?? 0) / 1e4)
2098
+ );
2099
+ if (fee > allowed) {
2100
+ fail("fee_too_high", `fee ${fee} exceeds the ${allowed} this client allows`);
2101
+ }
2102
+ }
1936
2103
  if (input.onchain) {
1937
2104
  const { htlcLocktime, minConfirmations, direction } = input.onchain;
1938
2105
  if (!Number.isInteger(minConfirmations) || minConfirmations < 1 || minConfirmations > MAX_MIN_CONFIRMATIONS) {
@@ -2121,13 +2288,11 @@ function lightningSendVtxoScript(params) {
2121
2288
  unilateralRefundWithoutReceiverDelay: seconds(
2122
2289
  unilateralRefundWithoutReceiverDelay(params.claimDelay)
2123
2290
  ),
2124
- nonInteractiveClaim: {
2291
+ nonInteractiveParameters: {
2125
2292
  receiverPkScript: params.receiverPkScript,
2126
- emulatorPubkey: params.emulatorPubkey
2127
- },
2128
- nonInteractiveRefund: {
2129
2293
  senderPkScript: params.refundPkScript,
2130
- emulatorPubkey: params.emulatorPubkey
2294
+ emulatorPubkey: params.emulatorPubkey,
2295
+ ...params.legacy !== void 0 && { legacy: params.legacy }
2131
2296
  }
2132
2297
  });
2133
2298
  }
@@ -2173,9 +2338,18 @@ async function requestLightningSend(wallet, arkServerUrl, transport, params) {
2173
2338
  receiverPkScript: solverHex(receiverPkScriptHex, "profile.receiver_pk_script"),
2174
2339
  refundPkScript: secrets.pkScript
2175
2340
  };
2176
- const script = lightningSendVtxoScript(treeParams);
2177
- const address = script.address(network.hrp, serverPubkey).encode();
2178
- verifyLockupAddress(quote, address);
2341
+ const matched = matchQuotedLockup(
2342
+ quote,
2343
+ network.hrp,
2344
+ serverPubkey,
2345
+ (legacy) => lightningSendVtxoScript({ ...treeParams, ...legacy !== void 0 && { legacy } })
2346
+ );
2347
+ const script = matched.script;
2348
+ const address = matched.address;
2349
+ const matchedTreeParams = {
2350
+ ...treeParams,
2351
+ ...matched.legacy !== void 0 && { legacy: matched.legacy }
2352
+ };
2179
2353
  assertFundable({
2180
2354
  quote,
2181
2355
  invoiceExpiresAt: params.invoice.expiresAt,
@@ -2194,7 +2368,7 @@ async function requestLightningSend(wallet, arkServerUrl, transport, params) {
2194
2368
  refundAddress,
2195
2369
  senderPubkey,
2196
2370
  secrets,
2197
- treeParams
2371
+ treeParams: matchedTreeParams
2198
2372
  };
2199
2373
  }
2200
2374
  var offerTermsFromQuote = (quote, assets) => {
@@ -2259,7 +2433,7 @@ function deriveOnchainSend(input) {
2259
2433
  if (refundLocktime === void 0 || htlcPubkey === void 0 || htlcLocktime === void 0 || minConfirmations === void 0 || receiverPkScriptHex === void 0) {
2260
2434
  throw new Error("onchain-send quote is missing a binding field");
2261
2435
  }
2262
- const script = lightningSendVtxoScript({
2436
+ const treeParams = {
2263
2437
  solverPubkey: (0, import_sdk9.toXOnly)(import_base10.hex.decode(quote.solver_pubkey), "solver key"),
2264
2438
  refundLocktime,
2265
2439
  serverPubkey: input.serverPubkey,
@@ -2269,9 +2443,13 @@ function deriveOnchainSend(input) {
2269
2443
  senderPubkey: input.senderPubkey,
2270
2444
  receiverPkScript: solverHex(receiverPkScriptHex, "profile.receiver_pk_script"),
2271
2445
  refundPkScript: import_sdk9.ArkAddress.decode(input.refundAddress).pkScript
2272
- });
2273
- const address = script.address(input.hrp, input.serverPubkey).encode();
2274
- verifyLockupAddress(quote, address);
2446
+ };
2447
+ const { script, address } = matchQuotedLockup(
2448
+ quote,
2449
+ input.hrp,
2450
+ input.serverPubkey,
2451
+ (legacy) => lightningSendVtxoScript({ ...treeParams, ...legacy !== void 0 && { legacy } })
2452
+ );
2275
2453
  const htlcParams = {
2276
2454
  paymentHash: input.paymentHash,
2277
2455
  claimKey: input.payoutPubkey,
@@ -2317,6 +2495,7 @@ async function requestOnchainSend(wallet, arkServerUrl, transport, params) {
2317
2495
  amountSide: params.amountSide
2318
2496
  })
2319
2497
  );
2498
+ assertQuotedAmount(quote, params.amountSide, params.amount);
2320
2499
  const network = (0, import_sdk9.getNetwork)(info.network);
2321
2500
  const derived = deriveOnchainSend({
2322
2501
  quote,
@@ -2446,13 +2625,11 @@ function receiveVtxoScript(params) {
2446
2625
  unilateralRefundWithoutReceiverDelay: seconds(
2447
2626
  unilateralRefundWithoutReceiverDelay(params.claimDelay)
2448
2627
  ),
2449
- nonInteractiveClaim: {
2628
+ nonInteractiveParameters: {
2450
2629
  receiverPkScript: params.payoutPkScript,
2451
- emulatorPubkey: params.emulatorPubkey
2452
- },
2453
- nonInteractiveRefund: {
2454
2630
  senderPkScript: params.solverRefundPkScript,
2455
- emulatorPubkey: params.emulatorPubkey
2631
+ emulatorPubkey: params.emulatorPubkey,
2632
+ ...params.legacy !== void 0 && { legacy: params.legacy }
2456
2633
  }
2457
2634
  });
2458
2635
  }
@@ -2476,10 +2653,23 @@ function deriveLightningReceive(input) {
2476
2653
  payoutPubkey: input.payoutPubkey,
2477
2654
  payoutPkScript: import_sdk9.ArkAddress.decode(input.payoutAddress).pkScript
2478
2655
  };
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 };
2656
+ const matched = matchQuotedLockup(
2657
+ quote,
2658
+ input.hrp,
2659
+ input.serverPubkey,
2660
+ (legacy) => receiveVtxoScript({ ...treeParams, ...legacy !== void 0 && { legacy } })
2661
+ );
2662
+ return {
2663
+ address: matched.address,
2664
+ swapPkScript: matched.script.pkScript,
2665
+ script: matched.script,
2666
+ invoice,
2667
+ refundLocktime,
2668
+ treeParams: {
2669
+ ...treeParams,
2670
+ ...matched.legacy !== void 0 && { legacy: matched.legacy }
2671
+ }
2672
+ };
2483
2673
  }
2484
2674
  async function requestLightningReceive(wallet, arkServerUrl, transport, params) {
2485
2675
  const rfqId = params.rfqId ?? newRfqId();
@@ -2567,7 +2757,7 @@ function deriveOnchainReceive(input) {
2567
2757
  if (refundLocktime === void 0 || claimPubkey === void 0 || htlcLocktime === void 0 || minConfirmations === void 0 || solverRefundPkScriptHex === void 0) {
2568
2758
  throw new Error("onchain-receive quote is missing a binding field");
2569
2759
  }
2570
- const script = receiveVtxoScript({
2760
+ const treeParams = {
2571
2761
  solverPubkey: (0, import_sdk9.toXOnly)(import_base10.hex.decode(quote.solver_pubkey), "solver key"),
2572
2762
  refundLocktime,
2573
2763
  serverPubkey: input.serverPubkey,
@@ -2577,9 +2767,13 @@ function deriveOnchainReceive(input) {
2577
2767
  solverRefundPkScript: solverHex(solverRefundPkScriptHex, "profile.solver_refund_pk_script"),
2578
2768
  payoutPubkey: input.payoutPubkey,
2579
2769
  payoutPkScript: import_sdk9.ArkAddress.decode(input.payoutAddress).pkScript
2580
- });
2581
- const address = script.address(input.hrp, input.serverPubkey).encode();
2582
- verifyLockupAddress(quote, address);
2770
+ };
2771
+ const { script, address } = matchQuotedLockup(
2772
+ quote,
2773
+ input.hrp,
2774
+ input.serverPubkey,
2775
+ (legacy) => receiveVtxoScript({ ...treeParams, ...legacy !== void 0 && { legacy } })
2776
+ );
2583
2777
  const htlc = onchainHtlcScript(
2584
2778
  {
2585
2779
  paymentHash: input.paymentHash,
@@ -2677,16 +2871,302 @@ async function requestOnchainReceive(wallet, arkServerUrl, transport, params) {
2677
2871
  };
2678
2872
  }
2679
2873
 
2680
- // src/claim.ts
2874
+ // src/chainSource.ts
2875
+ var btc2 = __toESM(require("@scure/btc-signer"), 1);
2876
+ var import_base11 = require("@scure/base");
2877
+ var addressOf = (pkScript, network) => btc2.Address(L1_NETWORKS[network]).encode(btc2.OutScript.decode(pkScript));
2878
+ var chainSourceFrom = (provider, network) => {
2879
+ const spenderFromHistory = async (txid, vout, pkScript) => {
2880
+ const txs = await provider.getTransactions(addressOf(pkScript, network));
2881
+ return txs.find((tx) => tx.vin?.some((i) => i.txid === txid && i.vout === vout))?.txid;
2882
+ };
2883
+ return {
2884
+ async getScriptUtxos(pkScript) {
2885
+ const address = addressOf(pkScript, network);
2886
+ const [coins, tip] = await Promise.all([
2887
+ provider.getCoins(address),
2888
+ provider.getChainTip()
2889
+ ]);
2890
+ return coins.map((coin) => ({
2891
+ txid: coin.txid,
2892
+ vout: coin.vout,
2893
+ amount: BigInt(coin.value),
2894
+ // Zero, not one: calling a mempool output "1 deep" would let a
2895
+ // 1-confirmation policy claim against a replaceable transaction.
2896
+ confirmations: coin.status.confirmed && typeof coin.status.block_height === "number" ? Math.max(0, tip.height - coin.status.block_height + 1) : 0
2897
+ }));
2898
+ },
2899
+ async getSpendingTx(txid, vout, pkScript) {
2900
+ const outspends = await provider.getTxOutspends(txid);
2901
+ const outspend = outspends[vout];
2902
+ if (!outspend?.spent) return null;
2903
+ const spender = outspend.txid || await spenderFromHistory(txid, vout, pkScript);
2904
+ if (!spender) return null;
2905
+ const raw = await provider.getRawTransaction(spender);
2906
+ return { txHex: import_base11.hex.encode(raw) };
2907
+ },
2908
+ broadcast(txHex) {
2909
+ return provider.broadcastTransaction(txHex);
2910
+ },
2911
+ async getMtp() {
2912
+ return (await provider.getChainTip()).time;
2913
+ }
2914
+ };
2915
+ };
2916
+
2917
+ // src/payment/rendezvous.ts
2918
+ var import_solver_discovery2 = require("@arkade-os/solver-discovery");
2681
2919
  var import_base12 = require("@scure/base");
2920
+ var XONLY_HEX = /^[0-9a-f]{64}$/;
2921
+ var rendezvousOf = (market, pinned) => {
2922
+ const transports = { nostr: { relays: market.transports?.nostr?.relays ?? [] } };
2923
+ if (!market.discovery_pubkey || !transports.nostr.relays.length) return void 0;
2924
+ const advertised = market.emulator_pubkey;
2925
+ const emulatorPubkey = advertised === void 0 || advertised === null || advertised === "" ? pinned : typeof advertised === "string" && XONLY_HEX.test(advertised) ? advertised : void 0;
2926
+ if (!emulatorPubkey) return void 0;
2927
+ if (pinned && emulatorPubkey !== pinned) return void 0;
2928
+ const bounds = (0, import_solver_discovery2.sideLimits)(market, "quote");
2929
+ if (!bounds) return void 0;
2930
+ return {
2931
+ solverPubkey: market.discovery_pubkey,
2932
+ transports,
2933
+ minSats: Number(bounds.min),
2934
+ maxSats: Number(bounds.max)
2935
+ };
2936
+ };
2937
+ var solverRendezvous = (markets, payoutCorridor, amountSats, fallbackEmulatorPubkey) => {
2938
+ const encoded = fallbackEmulatorPubkey ? import_base12.hex.encode(fallbackEmulatorPubkey) : void 0;
2939
+ if (encoded !== void 0 && !XONLY_HEX.test(encoded)) return void 0;
2940
+ const pinned = encoded;
2941
+ const candidates = (0, import_solver_discovery2.selectMarkets)(markets, {
2942
+ baseId: BTC_ASSET_ID,
2943
+ quoteId: BTC_ASSET_ID,
2944
+ baseCorridor: "arkade",
2945
+ quoteCorridor: payoutCorridor
2946
+ });
2947
+ for (const market of candidates) {
2948
+ const rendezvous = rendezvousOf(market, pinned);
2949
+ if (!rendezvous) continue;
2950
+ if (amountSats >= rendezvous.minSats && amountSats <= rendezvous.maxSats) {
2951
+ return rendezvous;
2952
+ }
2953
+ }
2954
+ return void 0;
2955
+ };
2956
+
2957
+ // src/payment/solverOnchain.ts
2958
+ var import_sdk11 = require("@arkade-os/sdk");
2959
+ var SOLVER_ONCHAIN_RAIL = "solver-onchain";
2960
+ var solverOnchainRendezvous = (markets, amountSats, fallbackEmulatorPubkey) => solverRendezvous(markets, "onchain", amountSats, fallbackEmulatorPubkey);
2961
+ function solverOnchainRail(deps) {
2962
+ const rendezvousFor = async (amount) => {
2963
+ if (amount === void 0) return void 0;
2964
+ const markets = await deps.discover();
2965
+ return solverOnchainRendezvous(markets, amount, deps.fallbackEmulatorPubkey);
2966
+ };
2967
+ return {
2968
+ id: SOLVER_ONCHAIN_RAIL,
2969
+ match: (req) => (0, import_sdk11.btcTarget)(req.raw) !== void 0,
2970
+ available: async (req) => {
2971
+ const address = (0, import_sdk11.btcTarget)(req.raw);
2972
+ if (!address) return false;
2973
+ try {
2974
+ l1ScriptForAddress(address, deps.l1Network);
2975
+ } catch {
2976
+ return false;
2977
+ }
2978
+ const amount = (0, import_sdk11.tryResolveSendAmount)(req.raw, req.amount);
2979
+ if (amount === void 0) return false;
2980
+ return await rendezvousFor(amount) !== void 0;
2981
+ },
2982
+ quote: async (req, ctx) => {
2983
+ const address = (0, import_sdk11.btcTarget)(req.raw);
2984
+ const amount = (0, import_sdk11.resolveSendAmount)(SOLVER_ONCHAIN_RAIL, req.raw, req.amount);
2985
+ const payoutPkScript = l1ScriptForAddress(address, deps.l1Network);
2986
+ const rendezvous = await rendezvousFor(amount);
2987
+ if (!rendezvous) {
2988
+ throw new Error(
2989
+ `${SOLVER_ONCHAIN_RAIL}: no solver serves arkade:BTC -> onchain:BTC at ${amount} sats`
2990
+ );
2991
+ }
2992
+ const negotiated = await deps.connect(
2993
+ rendezvous,
2994
+ (transport) => requestOnchainSend(ctx.wallet, deps.arkServerUrl, transport, {
2995
+ amount,
2996
+ amountSide: "to",
2997
+ payoutPubkey: deps.payoutPubkey,
2998
+ ...deps.emulatorPubkey ? { emulatorPubkey: deps.emulatorPubkey } : {}
2999
+ })
3000
+ );
3001
+ if (negotiated.l1Network !== deps.l1Network) {
3002
+ throw new Error(
3003
+ `${SOLVER_ONCHAIN_RAIL}: rail built for ${deps.l1Network} but the swap was negotiated on ${negotiated.l1Network}`
3004
+ );
3005
+ }
3006
+ const swap = { ...negotiated, rendezvous, payoutPkScript };
3007
+ return {
3008
+ railId: SOLVER_ONCHAIN_RAIL,
3009
+ amount,
3010
+ fee: swap.fundAmount - amount,
3011
+ total: swap.fundAmount,
3012
+ meta: {
3013
+ rfqId: swap.rfqId,
3014
+ validUntil: swap.quote.valid_until,
3015
+ htlcAddress: swap.htlc.address,
3016
+ minConfirmations: swap.minConfirmations,
3017
+ solverPubkey: rendezvous.solverPubkey,
3018
+ // The claim tx's fee comes out of the HTLC output at a rate
3019
+ // not knowable now, so `amount` is the payout, not the net.
3020
+ claimFeeDeductedFromPayout: true
3021
+ },
3022
+ send: async () => (0, import_sdk11.makeHandle)(SOLVER_ONCHAIN_RAIL, async (emit) => {
3023
+ assertFundable({
3024
+ quote: swap.quote,
3025
+ now: Math.floor(Date.now() / 1e3),
3026
+ onchain: {
3027
+ htlcLocktime: swap.htlcParams.refundLocktime,
3028
+ minConfirmations: swap.minConfirmations,
3029
+ direction: "send"
3030
+ }
3031
+ });
3032
+ await deps.persist(swap);
3033
+ await ctx.wallet.send({
3034
+ address: swap.address,
3035
+ amount: swap.fundAmount
3036
+ });
3037
+ emit({ status: "sent" });
3038
+ const result = { railId: SOLVER_ONCHAIN_RAIL, swapId: swap.rfqId };
3039
+ if (!deps.awaitSettlement) return result;
3040
+ let txid;
3041
+ try {
3042
+ ({ txid } = await deps.awaitSettlement(swap));
3043
+ } catch (e) {
3044
+ console.warn(
3045
+ `${SOLVER_ONCHAIN_RAIL}: settlement watch failed; the payment is sent`,
3046
+ e
3047
+ );
3048
+ return result;
3049
+ }
3050
+ const settled = { ...result, txid };
3051
+ emit({ status: "settled", result: settled });
3052
+ return settled;
3053
+ })
3054
+ };
3055
+ }
3056
+ };
3057
+ }
3058
+
3059
+ // src/payment/solverLightning.ts
3060
+ var import_sdk12 = require("@arkade-os/sdk");
3061
+ var SOLVER_LIGHTNING_RAIL = "solver-lightning";
3062
+ var factsOf = (raw, decode, now) => {
3063
+ const invoice = (0, import_sdk12.invoiceTarget)(raw);
3064
+ if (!invoice) return void 0;
3065
+ let facts;
3066
+ try {
3067
+ facts = decode(invoice);
3068
+ } catch {
3069
+ return void 0;
3070
+ }
3071
+ if (!Number.isInteger(facts.amountSats) || facts.amountSats <= 0) return void 0;
3072
+ if (facts.expiresAt <= now) return void 0;
3073
+ return facts;
3074
+ };
3075
+ var solverLightningRendezvous = (markets, amountSats, fallbackEmulatorPubkey) => solverRendezvous(markets, "lightning", amountSats, fallbackEmulatorPubkey);
3076
+ function solverLightningRail(deps) {
3077
+ const rendezvousFor = async (amountSats) => solverLightningRendezvous(await deps.discover(), amountSats, deps.fallbackEmulatorPubkey);
3078
+ return {
3079
+ id: SOLVER_LIGHTNING_RAIL,
3080
+ match: (req) => (0, import_sdk12.invoiceTarget)(req.raw) !== void 0,
3081
+ available: async (req) => {
3082
+ const facts = factsOf(req.raw, deps.decodeInvoice, Math.floor(Date.now() / 1e3));
3083
+ if (!facts) return false;
3084
+ if (req.amount !== void 0 && req.amount !== facts.amountSats) return false;
3085
+ return await rendezvousFor(facts.amountSats) !== void 0;
3086
+ },
3087
+ quote: async (req, ctx) => {
3088
+ const facts = factsOf(req.raw, deps.decodeInvoice, Math.floor(Date.now() / 1e3));
3089
+ if (!facts) {
3090
+ throw new Error(
3091
+ `${SOLVER_LIGHTNING_RAIL}: the request carries no payable BOLT11 invoice (amountless, expired, or undecodable)`
3092
+ );
3093
+ }
3094
+ if (req.amount !== void 0 && req.amount !== facts.amountSats) {
3095
+ throw new Error(
3096
+ `${SOLVER_LIGHTNING_RAIL}: the request names ${req.amount} sats but the invoice is for ${facts.amountSats} \u2014 the payee is paid the invoice`
3097
+ );
3098
+ }
3099
+ const rendezvous = await rendezvousFor(facts.amountSats);
3100
+ if (!rendezvous) {
3101
+ throw new Error(
3102
+ `${SOLVER_LIGHTNING_RAIL}: no solver serves arkade:BTC -> lightning:BTC at ${facts.amountSats} sats`
3103
+ );
3104
+ }
3105
+ const negotiated = await deps.connect(
3106
+ rendezvous,
3107
+ (transport) => requestLightningSend(ctx.wallet, deps.arkServerUrl, transport, {
3108
+ invoice: facts,
3109
+ ...deps.emulatorPubkey ? { emulatorPubkey: deps.emulatorPubkey } : {}
3110
+ })
3111
+ );
3112
+ const swap = { ...negotiated, invoice: facts, rendezvous };
3113
+ return {
3114
+ railId: SOLVER_LIGHTNING_RAIL,
3115
+ // `requestLightningSend` refuses a quote that reprices the
3116
+ // invoice, so the spread is a fee on top.
3117
+ amount: facts.amountSats,
3118
+ fee: swap.fundAmount - facts.amountSats,
3119
+ total: swap.fundAmount,
3120
+ meta: {
3121
+ rfqId: swap.rfqId,
3122
+ validUntil: swap.quote.valid_until,
3123
+ paymentHash: facts.paymentHash,
3124
+ invoiceExpiresAt: facts.expiresAt,
3125
+ solverPubkey: rendezvous.solverPubkey
3126
+ },
3127
+ send: async () => (0, import_sdk12.makeHandle)(SOLVER_LIGHTNING_RAIL, async (emit) => {
3128
+ assertFundable({
3129
+ quote: swap.quote,
3130
+ invoiceExpiresAt: facts.expiresAt,
3131
+ now: Math.floor(Date.now() / 1e3)
3132
+ });
3133
+ await deps.persist(swap);
3134
+ await ctx.wallet.send({
3135
+ address: swap.address,
3136
+ amount: swap.fundAmount
3137
+ });
3138
+ emit({ status: "sent" });
3139
+ const result = { railId: SOLVER_LIGHTNING_RAIL, swapId: swap.rfqId };
3140
+ if (!deps.awaitSettlement) return result;
3141
+ let preimage;
3142
+ try {
3143
+ ({ preimage } = await deps.awaitSettlement(swap));
3144
+ } catch (e) {
3145
+ console.warn(
3146
+ `${SOLVER_LIGHTNING_RAIL}: settlement watch failed; the payment is sent`,
3147
+ e
3148
+ );
3149
+ return result;
3150
+ }
3151
+ const settled = { ...result, ...preimage !== void 0 && { preimage } };
3152
+ emit({ status: "settled", result: settled });
3153
+ return settled;
3154
+ })
3155
+ };
3156
+ }
3157
+ };
3158
+ }
3159
+
3160
+ // src/claim.ts
3161
+ var import_base14 = require("@scure/base");
2682
3162
  var import_legacy3 = require("@noble/hashes/legacy.js");
2683
3163
  var import_sha25 = require("@noble/hashes/sha2.js");
2684
- var import_sdk12 = require("@arkade-os/sdk");
3164
+ var import_sdk14 = require("@arkade-os/sdk");
2685
3165
 
2686
3166
  // src/refund.ts
2687
- var import_base11 = require("@scure/base");
3167
+ var import_base13 = require("@scure/base");
2688
3168
  var import_sha24 = require("@noble/hashes/sha2.js");
2689
- var import_sdk11 = require("@arkade-os/sdk");
3169
+ var import_sdk13 = require("@arkade-os/sdk");
2690
3170
  var sleep2 = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
2691
3171
  var isRfqTerminal = (state) => RFQ_TERMINAL_STATES.includes(state);
2692
3172
  var RFQ_RESOLVED_STATES = ["settled", "refunded"];
@@ -2733,7 +3213,7 @@ var LockupNeedsRecoveryError = class extends Error {
2733
3213
  }
2734
3214
  };
2735
3215
  async function findLockupVtxos(indexer, swapPkScript) {
2736
- const scripts = [import_base11.hex.encode(swapPkScript)];
3216
+ const scripts = [import_base13.hex.encode(swapPkScript)];
2737
3217
  const [spendable, recoverable] = await Promise.all([
2738
3218
  indexer.getVtxos({ scripts, spendableOnly: true }),
2739
3219
  indexer.getVtxos({ scripts, recoverableOnly: true })
@@ -2759,16 +3239,16 @@ async function findLockupVtxos(indexer, swapPkScript) {
2759
3239
  }
2760
3240
  return out;
2761
3241
  }
2762
- var hashesTo = (candidate, paymentHash) => import_base11.hex.encode((0, import_sha24.sha256)(candidate)) === paymentHash;
3242
+ var hashesTo = (candidate, paymentHash) => import_base13.hex.encode((0, import_sha24.sha256)(candidate)) === paymentHash;
2763
3243
  var candidateWitnessItems = (tx, inputIndex) => [
2764
- ...(0, import_sdk11.getArkPsbtFields)(tx, inputIndex, import_sdk11.ConditionWitness).flat(),
3244
+ ...(0, import_sdk13.getArkPsbtFields)(tx, inputIndex, import_sdk13.ConditionWitness).flat(),
2765
3245
  ...tx.getInput(inputIndex).finalScriptWitness ?? []
2766
3246
  ];
2767
3247
  async function readLockupFate(indexer, input) {
2768
- const { vtxos } = await indexer.getVtxos({ scripts: [import_base11.hex.encode(input.swapPkScript)] });
3248
+ const { vtxos } = await indexer.getVtxos({ scripts: [import_base13.hex.encode(input.swapPkScript)] });
2769
3249
  const all = vtxos ?? [];
2770
3250
  if (all.length === 0) return { fate: "unknown" };
2771
- const exited = all.filter((vtxo) => vtxo.isUnrolled && !(0, import_sdk11.hasTerminalSpend)(vtxo));
3251
+ const exited = all.filter((vtxo) => vtxo.isUnrolled && !(0, import_sdk13.hasTerminalSpend)(vtxo));
2772
3252
  if (exited.length > 0) {
2773
3253
  return {
2774
3254
  fate: "exited",
@@ -2778,7 +3258,7 @@ async function readLockupFate(indexer, input) {
2778
3258
  const spentBy = /* @__PURE__ */ new Map();
2779
3259
  let everySpendNamed = true;
2780
3260
  for (const vtxo of all) {
2781
- if (!(0, import_sdk11.hasTerminalSpend)(vtxo)) return { fate: "open" };
3261
+ if (!(0, import_sdk13.hasTerminalSpend)(vtxo)) return { fate: "open" };
2782
3262
  if (vtxo.spentBy)
2783
3263
  spentBy.set(vtxo.spentBy, {
2784
3264
  checkpointTxid: vtxo.spentBy,
@@ -2792,7 +3272,7 @@ async function readLockupFate(indexer, input) {
2792
3272
  for (const raw of txs) {
2793
3273
  let tx;
2794
3274
  try {
2795
- tx = import_sdk11.Transaction.fromPSBT(import_base11.base64.decode(raw));
3275
+ tx = import_sdk13.Transaction.fromPSBT(import_base13.base64.decode(raw));
2796
3276
  } catch {
2797
3277
  continue;
2798
3278
  }
@@ -2800,7 +3280,7 @@ async function readLockupFate(indexer, input) {
2800
3280
  for (let i = 0; i < tx.inputsLength; i++) {
2801
3281
  const spent = tx.getInput(i);
2802
3282
  if (!spent.txid) continue;
2803
- const txid = import_base11.hex.encode(spent.txid);
3283
+ const txid = import_base13.hex.encode(spent.txid);
2804
3284
  if (!all.some((vtxo) => vtxo.txid === txid && vtxo.vout === spent.index)) continue;
2805
3285
  for (const candidate of candidateWitnessItems(tx, i)) {
2806
3286
  if (hashesTo(candidate, input.paymentHash)) {
@@ -2820,23 +3300,23 @@ async function pushRefundWithoutReceiver(ark, input) {
2820
3300
  input.script.options.refundLocktime
2821
3301
  );
2822
3302
  }
2823
- const refundPkScript = input.refundPkScript ?? input.script.options.nonInteractiveRefund?.senderPkScript;
3303
+ const refundPkScript = input.refundPkScript ?? input.script.options.nonInteractiveParameters?.senderPkScript;
2824
3304
  if (!refundPkScript) {
2825
3305
  throw new Error(
2826
- "no refund destination: the contract carries no nonInteractiveRefund leaf, so pass refundPkScript explicitly"
3306
+ "no refund destination: the contract carries no emulator covenant suite, so pass refundPkScript explicitly"
2827
3307
  );
2828
3308
  }
2829
3309
  const info = await ark.getInfo();
2830
3310
  let serverUnrollScript;
2831
3311
  try {
2832
- serverUnrollScript = import_sdk11.CSVMultisigTapscript.decode(import_base11.hex.decode(info.checkpointTapscript));
3312
+ serverUnrollScript = import_sdk13.CSVMultisigTapscript.decode(import_base13.hex.decode(info.checkpointTapscript));
2833
3313
  } catch {
2834
3314
  throw new Error("invalid checkpointTapscript from the Arkade server");
2835
3315
  }
2836
3316
  const leaf = input.script.refundWithoutReceiver();
2837
3317
  const tapTree = input.script.encode();
2838
3318
  const amount = input.vtxos.reduce((sum, vtxo) => sum + vtxo.value, 0);
2839
- const { arkTx, checkpoints } = (0, import_sdk11.buildOffchainTx)(
3319
+ const { arkTx, checkpoints } = (0, import_sdk13.buildOffchainTx)(
2840
3320
  input.vtxos.map((vtxo) => ({
2841
3321
  txid: vtxo.txid,
2842
3322
  vout: vtxo.vout,
@@ -2849,18 +3329,18 @@ async function pushRefundWithoutReceiver(ark, input) {
2849
3329
  );
2850
3330
  const signedArkTx = await input.sender.sign(arkTx);
2851
3331
  const submitted = await ark.submitTx(
2852
- import_base11.base64.encode(signedArkTx.toPSBT()),
2853
- checkpoints.map((c) => import_base11.base64.encode(c.toPSBT()))
3332
+ import_base13.base64.encode(signedArkTx.toPSBT()),
3333
+ checkpoints.map((c) => import_base13.base64.encode(c.toPSBT()))
2854
3334
  );
2855
- (0, import_sdk11.assertSubmittedArkTxid)(submitted, signedArkTx, "refundWithoutReceiver");
2856
- const matched = (0, import_sdk11.matchServerCheckpoints)(
3335
+ (0, import_sdk13.assertSubmittedArkTxid)(submitted, signedArkTx, "refundWithoutReceiver");
3336
+ const matched = (0, import_sdk13.matchServerCheckpoints)(
2857
3337
  submitted.signedCheckpointTxs,
2858
3338
  checkpoints,
2859
3339
  "refundWithoutReceiver"
2860
3340
  );
2861
3341
  const finalCheckpoints = await Promise.all(
2862
3342
  matched.map(
2863
- async ({ server }) => import_base11.base64.encode((await input.sender.sign(server, [0])).toPSBT())
3343
+ async ({ server }) => import_base13.base64.encode((await input.sender.sign(server, [0])).toPSBT())
2864
3344
  )
2865
3345
  );
2866
3346
  await ark.finalizeTx(submitted.arkTxid, finalCheckpoints);
@@ -2875,6 +3355,21 @@ async function refundIfUnresolved(transport, ark, indexer, input) {
2875
3355
  const status = await transport.status(input.rfqId);
2876
3356
  if (status && isResolved(status.state)) return { outcome: "resolved", status };
2877
3357
  if (now() >= input.refundLocktime) {
3358
+ let fate = { fate: "unknown" };
3359
+ try {
3360
+ fate = await readLockupFate(indexer, {
3361
+ swapPkScript: input.script.pkScript,
3362
+ paymentHash: input.paymentHash
3363
+ });
3364
+ } catch {
3365
+ }
3366
+ if (fate.fate === "exited") {
3367
+ return {
3368
+ outcome: "exited",
3369
+ outpoints: fate.outpoints.map((o) => `${o.txid}:${o.vout}`),
3370
+ status
3371
+ };
3372
+ }
2878
3373
  const vtxos = await findLockupVtxos(indexer, input.script.pkScript);
2879
3374
  if (vtxos.length === 0) return { outcome: "nothing_to_refund", status };
2880
3375
  try {
@@ -2940,20 +3435,20 @@ async function pushClaim(ark, input) {
2940
3435
  }
2941
3436
  }
2942
3437
  const committed = input.script.options.preimageHash;
2943
- if (import_base12.hex.encode((0, import_legacy3.ripemd160)((0, import_sha25.sha256)(input.preimage))) !== import_base12.hex.encode(committed)) {
3438
+ if (import_base14.hex.encode((0, import_legacy3.ripemd160)((0, import_sha25.sha256)(input.preimage))) !== import_base14.hex.encode(committed)) {
2944
3439
  throw new Error("preimage does not match the covenant's payment hash");
2945
3440
  }
2946
3441
  const info = await ark.getInfo();
2947
3442
  let serverUnrollScript;
2948
3443
  try {
2949
- serverUnrollScript = import_sdk12.CSVMultisigTapscript.decode(import_base12.hex.decode(info.checkpointTapscript));
3444
+ serverUnrollScript = import_sdk14.CSVMultisigTapscript.decode(import_base14.hex.decode(info.checkpointTapscript));
2950
3445
  } catch {
2951
3446
  throw new Error("invalid checkpointTapscript from the Arkade server");
2952
3447
  }
2953
3448
  const leaf = input.script.claim();
2954
3449
  const tapTree = input.script.encode();
2955
- const arkTxid = await (0, import_sdk12.signAndSubmitOffchainTx)({
2956
- identity: (0, import_sdk12.claimWithPreimageIdentity)(input.receiver, input.preimage),
3450
+ const arkTxid = await (0, import_sdk14.signAndSubmitOffchainTx)({
3451
+ identity: (0, import_sdk14.claimWithPreimageIdentity)(input.receiver, input.preimage),
2957
3452
  provider: ark,
2958
3453
  inputs: input.vtxos.map((vtxo) => ({
2959
3454
  txid: vtxo.txid,
@@ -3000,7 +3495,7 @@ async function claimReceiveLockup(indexer, ark, input) {
3000
3495
  }
3001
3496
 
3002
3497
  // src/refundBlocked.ts
3003
- var import_sdk13 = require("@arkade-os/sdk");
3498
+ var import_sdk15 = require("@arkade-os/sdk");
3004
3499
  var RefundNotLocallyPossibleError = class extends Error {
3005
3500
  constructor(reason, message, options) {
3006
3501
  super(message, options);
@@ -3017,16 +3512,16 @@ async function senderIdentityForSwapRecord(wallet, record) {
3017
3512
  );
3018
3513
  }
3019
3514
  try {
3020
- return await (0, import_sdk13.contractSigner)(wallet, record.signingDescriptor);
3515
+ return await (0, import_sdk15.contractSigner)(wallet, record.signingDescriptor);
3021
3516
  } catch (cause) {
3022
- if (cause instanceof import_sdk13.WalletCannotSignError) {
3517
+ if (cause instanceof import_sdk15.WalletCannotSignError) {
3023
3518
  throw new RefundNotLocallyPossibleError(
3024
3519
  "unsignable-wallet",
3025
3520
  `this wallet holds ${record.signingDescriptor} but cannot sign with it; attach its signer`,
3026
3521
  { cause }
3027
3522
  );
3028
3523
  }
3029
- if (cause instanceof import_sdk13.ForeignDescriptorError) {
3524
+ if (cause instanceof import_sdk15.ForeignDescriptorError) {
3030
3525
  throw new RefundNotLocallyPossibleError(
3031
3526
  "foreign-descriptor",
3032
3527
  `this wallet cannot derive ${record.signingDescriptor}; the swap was created on another wallet`,
@@ -3061,7 +3556,7 @@ function arkadeRefunder(deps) {
3061
3556
  }
3062
3557
 
3063
3558
  // src/swapManager.ts
3064
- var import_base13 = require("@scure/base");
3559
+ var import_base15 = require("@scure/base");
3065
3560
  function nextOnchainAction(input) {
3066
3561
  switch (input.phase.phase) {
3067
3562
  case "unfunded":
@@ -3509,7 +4004,7 @@ var RfqSwapManager = class {
3509
4004
  // ── internals ────────────────────────────────────────────────────────────
3510
4005
  track(swap) {
3511
4006
  this.monitored.set(swap.rfqId, swap);
3512
- this.byLockupScript.set(import_base13.hex.encode(swap.lockupPkScript), swap);
4007
+ this.byLockupScript.set(import_base15.hex.encode(swap.lockupPkScript), swap);
3513
4008
  }
3514
4009
  /** Drops the swap from BOTH indexes. The event index is the one that stops
3515
4010
  * a late event finding a swap that is gone; `pollSwap`'s own
@@ -3518,7 +4013,7 @@ var RfqSwapManager = class {
3518
4013
  * them from silently re-driving a cancelled swap. */
3519
4014
  untrack(rfqId) {
3520
4015
  const swap = this.monitored.get(rfqId);
3521
- if (swap) this.byLockupScript.delete(import_base13.hex.encode(swap.lockupPkScript));
4016
+ if (swap) this.byLockupScript.delete(import_base15.hex.encode(swap.lockupPkScript));
3522
4017
  this.monitored.delete(rfqId);
3523
4018
  this.refundRefused.delete(rfqId);
3524
4019
  this.lastClaimError.delete(rfqId);
@@ -3580,7 +4075,7 @@ var RfqSwapManager = class {
3580
4075
  if (!lockup) {
3581
4076
  try {
3582
4077
  const [existing] = await contracts.getContracts({
3583
- script: import_base13.hex.encode(swap.lockupPkScript)
4078
+ script: import_base15.hex.encode(swap.lockupPkScript)
3584
4079
  });
3585
4080
  if (existing) {
3586
4081
  this.registered.set(swap.rfqId, true);
@@ -3599,13 +4094,13 @@ var RfqSwapManager = class {
3599
4094
  );
3600
4095
  return;
3601
4096
  }
3602
- const script = import_base13.hex.encode(lockup.script.pkScript);
3603
- if (script !== import_base13.hex.encode(swap.lockupPkScript)) {
4097
+ const script = import_base15.hex.encode(lockup.script.pkScript);
4098
+ if (script !== import_base15.hex.encode(swap.lockupPkScript)) {
3604
4099
  this.registered.set(swap.rfqId, false);
3605
4100
  this.emitFailed(
3606
4101
  swap,
3607
4102
  new Error(
3608
- `swap ${swap.rfqId} lockup script ${script} does not match its lockupPkScript ${import_base13.hex.encode(swap.lockupPkScript)}`
4103
+ `swap ${swap.rfqId} lockup script ${script} does not match its lockupPkScript ${import_base15.hex.encode(swap.lockupPkScript)}`
3609
4104
  )
3610
4105
  );
3611
4106
  return;
@@ -3624,7 +4119,7 @@ var RfqSwapManager = class {
3624
4119
  * script for its whole life. Best-effort — the swap is over either way. */
3625
4120
  retireContract(swap) {
3626
4121
  if (!this.deps.contracts || !this.registered.get(swap.rfqId)) return;
3627
- void this.deps.contracts.setContractWatchState(import_base13.hex.encode(swap.lockupPkScript), "retained").catch((error) => this.emitFailed(swap, error));
4122
+ void this.deps.contracts.setContractWatchState(import_base15.hex.encode(swap.lockupPkScript), "retained").catch((error) => this.emitFailed(swap, error));
3628
4123
  }
3629
4124
  arm() {
3630
4125
  if (!this.running) return;
@@ -4097,8 +4592,8 @@ var errorMessage = (error) => error instanceof Error ? error.message : String(er
4097
4592
  var outpointKey = (vtxo) => `${vtxo.txid}:${vtxo.vout}`;
4098
4593
 
4099
4594
  // src/activity.ts
4100
- var import_sdk14 = require("@arkade-os/sdk");
4101
- var import_base14 = require("@scure/base");
4595
+ var import_sdk16 = require("@arkade-os/sdk");
4596
+ var import_base16 = require("@scure/base");
4102
4597
  var LABELS = {
4103
4598
  lightning_send: "Lightning send",
4104
4599
  lightning_receive: "Lightning receive",
@@ -4173,7 +4668,7 @@ async function activityInputOf(record, indexer) {
4173
4668
  async function lockupTxids(indexer, record, wantFunding) {
4174
4669
  let script;
4175
4670
  try {
4176
- script = import_base14.hex.encode(import_sdk14.ArkAddress.decode(record.lockupAddress).pkScript);
4671
+ script = import_base16.hex.encode(import_sdk16.ArkAddress.decode(record.lockupAddress).pkScript);
4177
4672
  } catch {
4178
4673
  return [];
4179
4674
  }
@@ -4227,6 +4722,8 @@ async function lockupTxids(indexer, record, wantFunding) {
4227
4722
  RfqSwapManager,
4228
4723
  RfqSwapOriginRequired,
4229
4724
  SOLO_REFUND_HEADROOM_SECONDS,
4725
+ SOLVER_LIGHTNING_RAIL,
4726
+ SOLVER_ONCHAIN_RAIL,
4230
4727
  SWAP_LOCKUP_CONTRACT_KIND,
4231
4728
  SWAP_LOCKUP_CONTRACT_LABEL,
4232
4729
  SWAP_LOCKUP_CONTRACT_TYPE,
@@ -4243,6 +4740,7 @@ async function lockupTxids(indexer, record, wantFunding) {
4243
4740
  buildHtlcClaim,
4244
4741
  buildHtlcRefund,
4245
4742
  cancelOffer,
4743
+ chainSourceFrom,
4246
4744
  claimOnchainFill,
4247
4745
  claimReceiveLockup,
4248
4746
  classifyDepositSpend,
@@ -4264,6 +4762,7 @@ async function lockupTxids(indexer, record, wantFunding) {
4264
4762
  httpTransport,
4265
4763
  isRfqSwapTerminal,
4266
4764
  isRfqTerminal,
4765
+ l1ScriptForAddress,
4267
4766
  lightningReceiveRequest,
4268
4767
  lightningSendRequest,
4269
4768
  lightningSendVtxoScript,
@@ -4303,6 +4802,11 @@ async function lockupTxids(indexer, record, wantFunding) {
4303
4802
  sealClaimPacket,
4304
4803
  senderIdentityForSwapRecord,
4305
4804
  shouldRetainRfqSwap,
4805
+ solverLightningRail,
4806
+ solverLightningRendezvous,
4807
+ solverOnchainRail,
4808
+ solverOnchainRendezvous,
4809
+ solverRendezvous,
4306
4810
  spendTxidsOf,
4307
4811
  spendUpdate,
4308
4812
  swapActivityResolver,