@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.js CHANGED
@@ -2,6 +2,7 @@ import {
2
2
  ARKADE_ASSET,
3
3
  ARKADE_BTC,
4
4
  AddressMismatch,
5
+ L1_NETWORKS,
5
6
  LIGHTNING_BTC,
6
7
  LIGHTNING_RECEIVE_PAIR,
7
8
  LIGHTNING_SEND_PAIR,
@@ -38,6 +39,7 @@ import {
38
39
  deriveOnchainSend,
39
40
  extractPreimage,
40
41
  httpTransport,
42
+ l1ScriptForAddress,
41
43
  lightningReceiveRequest,
42
44
  lightningSendRequest,
43
45
  lightningSendVtxoScript,
@@ -63,7 +65,7 @@ import {
63
65
  unilateralRefundWithoutReceiverDelay,
64
66
  verifyLockupAddress,
65
67
  verifyReceiveInvoice
66
- } from "./chunk-5NPYNQ5V.js";
68
+ } from "./chunk-XXWOODUE.js";
67
69
  import {
68
70
  InMemoryAssetSwapRepository,
69
71
  marketsCacheKey
@@ -329,12 +331,30 @@ var swapPrograms = {
329
331
  wantAsset: arkade.parseArtifact(swap_want_asset_program_default),
330
332
  wantBtc: arkade.parseArtifact(swap_want_btc_program_default)
331
333
  };
334
+ function withExitClosure(program, exit) {
335
+ if (!exit) return program;
336
+ return {
337
+ ...program,
338
+ // typed params are authoritative: an undeclared `$exitDelay` fails
339
+ // validateProgram instead of compiling against an unbound value
340
+ params: [...program.params ?? [], { name: "exitDelay", type: "int" }],
341
+ functions: {
342
+ ...program.functions,
343
+ exit: {
344
+ tapscript: { signers: ["$user"], csv: { type: exit.type, value: "$exitDelay" } }
345
+ }
346
+ }
347
+ };
348
+ }
332
349
  function swapProgramBinding(offer, serverPubkey) {
333
350
  if (offer.makerPkScript.length !== FIELDS.makerPkScript.width) {
334
351
  throw new Error("makerPkScript is not a 34-byte taproot scriptPubKey");
335
352
  }
336
353
  return {
337
- program: offer.wantAsset ? swapPrograms.wantAsset : swapPrograms.wantBtc,
354
+ program: withExitClosure(
355
+ offer.wantAsset ? swapPrograms.wantAsset : swapPrograms.wantBtc,
356
+ offer.exitDelay
357
+ ),
338
358
  args: {
339
359
  makerWP: offer.makerPkScript.subarray(2),
340
360
  wantAmount: offer.wantAmount,
@@ -344,7 +364,8 @@ function swapProgramBinding(offer, serverPubkey) {
344
364
  ...offer.wantAsset && {
345
365
  wantAssetTxid: offer.wantAsset.txid.slice().reverse(),
346
366
  wantAssetGroupIndex: offer.wantAsset.groupIndex
347
- }
367
+ },
368
+ ...offer.exitDelay && { exitDelay: offer.exitDelay.value }
348
369
  },
349
370
  keys: {
350
371
  serverKey: serverPubkey,
@@ -365,9 +386,27 @@ var FIELDS = {
365
386
  makerPkScript: { tag: 5, width: 34 },
366
387
  makerPublicKey: { tag: 7, width: 32 },
367
388
  emulatorPubkey: { tag: 8, width: 32 },
368
- offerAsset: { tag: 11, width: void 0 }
389
+ ratioNum: { tag: 9, width: 8 },
390
+ ratioDen: { tag: 10, width: 8 },
391
+ offerAsset: { tag: 11, width: void 0 },
392
+ exitTimelock: { tag: 12, width: 9 }
369
393
  };
394
+ var EXIT_TYPES = ["blocks", "seconds"];
370
395
  var NAMES = Object.fromEntries(Object.entries(FIELDS).map(([k, f]) => [f.tag, k]));
396
+ function u64(name, value) {
397
+ if (value < BigInt(0) || value >> BigInt(64) > BigInt(0)) {
398
+ throw new Error(`${name} does not fit the offer wire format (u64)`);
399
+ }
400
+ const out = new Uint8Array(FIELDS.wantAmount.width);
401
+ new DataView(out.buffer).setBigUint64(0, value, false);
402
+ return out;
403
+ }
404
+ var readU64 = (value) => new DataView(value.buffer, value.byteOffset).getBigUint64(0, false);
405
+ var setRatio = (name, value) => {
406
+ if (value === void 0 || value === BigInt(0)) return void 0;
407
+ if (value < BigInt(0)) throw new Error(`${name} does not fit the offer wire format (u64)`);
408
+ return value;
409
+ };
371
410
  function tlv(type, value) {
372
411
  if (value.length > 65535) throw new Error("TLV value exceeds the u16 length field");
373
412
  return concatBytes(Uint8Array.of(type, value.length >> 8 & 255, value.length & 255), value);
@@ -386,24 +425,45 @@ function encodeOffer(offer) {
386
425
  throw new Error(`${name} must be ${FIELDS[name].width} bytes`);
387
426
  }
388
427
  }
389
- if (offer.wantAmount < BigInt(0) || offer.wantAmount >> BigInt(64) > BigInt(0)) {
390
- throw new Error("wantAmount does not fit the offer wire format (u64)");
428
+ const ratioNum = setRatio("ratioNum", offer.ratioNum);
429
+ const ratioDen = setRatio("ratioDen", offer.ratioDen);
430
+ if (ratioNum === void 0 !== (ratioDen === void 0)) {
431
+ throw new Error("offer must carry both ratioNum and ratioDen, or neither");
391
432
  }
392
- const amount = new Uint8Array(FIELDS.wantAmount.width);
393
- new DataView(amount.buffer).setBigUint64(0, offer.wantAmount, false);
394
433
  const recs = [
395
434
  tlv(FIELDS.swapPkScript.tag, offer.swapPkScript),
396
- tlv(FIELDS.wantAmount.tag, amount)
435
+ tlv(FIELDS.wantAmount.tag, u64("wantAmount", offer.wantAmount))
397
436
  ];
398
437
  if (offer.wantAsset) recs.push(tlv(FIELDS.wantAsset.tag, offer.wantAsset.serialize()));
438
+ if (ratioNum !== void 0) recs.push(tlv(FIELDS.ratioNum.tag, u64("ratioNum", ratioNum)));
439
+ if (ratioDen !== void 0) recs.push(tlv(FIELDS.ratioDen.tag, u64("ratioDen", ratioDen)));
399
440
  if (offer.offerAsset) recs.push(tlv(FIELDS.offerAsset.tag, offer.offerAsset.serialize()));
400
441
  recs.push(
401
442
  tlv(FIELDS.makerPkScript.tag, offer.makerPkScript),
402
443
  tlv(FIELDS.makerPublicKey.tag, offer.makerPublicKey),
403
444
  tlv(FIELDS.emulatorPubkey.tag, offer.emulatorPubkey)
404
445
  );
446
+ if (offer.exitDelay) recs.push(tlv(FIELDS.exitTimelock.tag, encodeExitDelay(offer.exitDelay)));
405
447
  return concatBytes(...recs);
406
448
  }
449
+ function encodeExitDelay(exit) {
450
+ return concatBytes(
451
+ Uint8Array.of(EXIT_TYPES.indexOf(assertExitDelay(exit).type)),
452
+ u64("exitDelay", exit.value)
453
+ );
454
+ }
455
+ function assertExitDelay(exit) {
456
+ if (EXIT_TYPES.indexOf(exit.type) < 0) {
457
+ throw new Error(`unknown exitDelay locktime type: ${exit.type}`);
458
+ }
459
+ if (exit.value <= BigInt(0)) {
460
+ throw new Error("exitDelay must be a positive relative locktime");
461
+ }
462
+ if (exit.value >> BigInt(32) > BigInt(0)) {
463
+ throw new Error("exitDelay does not fit the locktime field (u32)");
464
+ }
465
+ return exit;
466
+ }
407
467
  function decodeOffer(data) {
408
468
  const fields = {};
409
469
  let off = 0;
@@ -423,6 +483,12 @@ function decodeOffer(data) {
423
483
  for (const name of ["wantAsset", "offerAsset"]) {
424
484
  if (fields[name]?.length === 0) throw new Error(`missing/invalid ${name}`);
425
485
  }
486
+ for (const [name, value] of Object.entries(fields)) {
487
+ const width = FIELDS[name].width;
488
+ if (width !== void 0 && value.length !== width) {
489
+ throw new Error(`missing/invalid ${name}`);
490
+ }
491
+ }
426
492
  const need = (name) => {
427
493
  const v = fields[name];
428
494
  const len = FIELDS[name].width;
@@ -434,16 +500,36 @@ function decodeOffer(data) {
434
500
  if (Boolean(fields.wantAsset) === Boolean(fields.offerAsset)) {
435
501
  throw new Error("offer must carry exactly one of wantAsset or offerAsset");
436
502
  }
503
+ const readRatio = (name) => {
504
+ const raw = fields[name];
505
+ if (!raw) return void 0;
506
+ const value = readU64(raw);
507
+ if (value === BigInt(0)) throw new Error(`missing/invalid ${name}`);
508
+ return value;
509
+ };
510
+ const ratioNum = readRatio("ratioNum");
511
+ const ratioDen = readRatio("ratioDen");
512
+ if (ratioNum === void 0 !== (ratioDen === void 0)) {
513
+ throw new Error("offer must carry both ratioNum and ratioDen, or neither");
514
+ }
437
515
  return {
438
516
  swapPkScript: need("swapPkScript"),
439
- wantAmount: new DataView(amount.buffer, amount.byteOffset).getBigUint64(0, false),
517
+ wantAmount: readU64(amount),
440
518
  ...fields.wantAsset && { wantAsset: asset.AssetId.fromBytes(fields.wantAsset) },
441
519
  ...fields.offerAsset && { offerAsset: asset.AssetId.fromBytes(fields.offerAsset) },
442
520
  makerPkScript: need("makerPkScript"),
443
521
  makerPublicKey: need("makerPublicKey"),
444
- emulatorPubkey: need("emulatorPubkey")
522
+ emulatorPubkey: need("emulatorPubkey"),
523
+ ...ratioNum !== void 0 && { ratioNum },
524
+ ...ratioDen !== void 0 && { ratioDen },
525
+ ...fields.exitTimelock && { exitDelay: decodeExitDelay(fields.exitTimelock) }
445
526
  };
446
527
  }
528
+ function decodeExitDelay(value) {
529
+ const type = EXIT_TYPES[value[0]];
530
+ if (!type) throw new Error(`unknown exitDelay locktime type: 0x${value[0].toString(16)}`);
531
+ return { type, value: readU64(value.subarray(1)) };
532
+ }
447
533
  var OFFER_CONTRACT_LABEL = "Arkade swap offer";
448
534
  var OFFER_CONTRACT_KIND = "asset-swap-offer";
449
535
  async function registerOfferContract(wallet, arkServerUrl, network, binding, serverPubkey, expectedPkScript) {
@@ -469,6 +555,14 @@ async function registerOfferContract(wallet, arkServerUrl, network, binding, ser
469
555
  });
470
556
  await promoteOfferContract(contractManager, hex2.encode(expectedPkScript));
471
557
  }
558
+ function serverExitDelay(delay) {
559
+ if (typeof delay !== "bigint" || delay <= BigInt(0)) {
560
+ throw new Error(
561
+ "the server reports no usable unilateralExitDelay; pass `exitDelay` to set the offer's exit closure explicitly, or `noExit: true` to publish without one"
562
+ );
563
+ }
564
+ return assertExitDelay({ value: delay, type: delay < BigInt(512) ? "blocks" : "seconds" });
565
+ }
472
566
  async function createOffer(wallet, arkServerUrl, params) {
473
567
  if (Boolean(params.wantAsset) === Boolean(params.offerAsset)) {
474
568
  throw new Error("set exactly one of wantAsset (BTC->asset) or offerAsset (asset->BTC)");
@@ -489,7 +583,11 @@ async function createOffer(wallet, arkServerUrl, params) {
489
583
  offerAsset: params.offerAsset,
490
584
  makerPkScript: ArkAddress.decode(makerAddress).pkScript,
491
585
  makerPublicKey,
492
- emulatorPubkey: emuKey
586
+ emulatorPubkey: emuKey,
587
+ // checked HERE, before the covenant is derived and registered below:
588
+ // deferring it to `encodeOffer` leaves a registered contract behind for
589
+ // an offer that then fails to encode. @see assertExitDelay
590
+ exitDelay: params.noExit ? void 0 : params.exitDelay ? assertExitDelay(params.exitDelay) : serverExitDelay(info.unilateralExitDelay)
493
591
  };
494
592
  const script = offerVtxoScript(binding, serverPubKey);
495
593
  const offer = { ...binding, swapPkScript: script.pkScript };
@@ -965,7 +1063,8 @@ function onchainSendProfile(result) {
965
1063
  htlcLocktime: result.htlcParams.refundLocktime,
966
1064
  network: result.l1Network,
967
1065
  htlcAddress: result.htlc.address,
968
- minConfirmations: result.minConfirmations
1066
+ minConfirmations: result.minConfirmations,
1067
+ payoutPkScript: hex3.encode(result.payoutPkScript)
969
1068
  };
970
1069
  }
971
1070
  var OnchainSendCorridor = {
@@ -1014,6 +1113,9 @@ var OnchainSendCorridor = {
1014
1113
  paymentHash,
1015
1114
  htlc,
1016
1115
  minConfirmations: profile.minConfirmations,
1116
+ // Optional here, required at the write: throwing on an older
1117
+ // record would strand the refund it is still owed.
1118
+ ...profile.payoutPkScript ? { payoutPkScript: hex3.decode(profile.payoutPkScript) } : {},
1017
1119
  ...profile.funding ? { funding: profile.funding } : {},
1018
1120
  ...profile.claimTxid ? { claimTxid: profile.claimTxid } : {}
1019
1121
  };
@@ -1178,7 +1280,9 @@ function classifySpend(offer, serverPubkey, spendTx, deposit) {
1178
1280
  const script = offerVtxoScript(offer, serverPubkey);
1179
1281
  if (hex5.encode(script.pkScript) !== hex5.encode(offer.swapPkScript)) return "indeterminate";
1180
1282
  leaves = {
1181
- cancel: script.functionByName("cancel")?.leafScript,
1283
+ // both routes that hand the deposit back; `exit` is absent on an
1284
+ // offer that carries no exit closure, and drops out here
1285
+ returned: ["cancel", "exit"].map((name) => script.functionByName(name)?.leafScript).filter((leaf) => leaf !== void 0),
1182
1286
  fulfill: script.functionByName("fulfill")?.leafScript
1183
1287
  };
1184
1288
  } catch {
@@ -1190,7 +1294,7 @@ function classifySpend(offer, serverPubkey, spendTx, deposit) {
1190
1294
  if (hex5.encode(input.txid) !== deposit.txid) continue;
1191
1295
  for (const leaf of input.tapLeafScript ?? []) {
1192
1296
  const spent = hex5.encode(scriptFromTapLeafScript(leaf));
1193
- if (leaves.cancel && spent === hex5.encode(leaves.cancel)) return "cancelled";
1297
+ if (leaves.returned.some((back) => spent === hex5.encode(back))) return "cancelled";
1194
1298
  if (leaves.fulfill && spent === hex5.encode(leaves.fulfill)) return "fulfilled";
1195
1299
  }
1196
1300
  }
@@ -1392,8 +1496,294 @@ async function watchOfferSwaps({
1392
1496
  };
1393
1497
  }
1394
1498
 
1395
- // src/claim.ts
1499
+ // src/chainSource.ts
1500
+ import * as btc from "@scure/btc-signer";
1501
+ import { hex as hex7 } from "@scure/base";
1502
+ var addressOf = (pkScript, network) => btc.Address(L1_NETWORKS[network]).encode(btc.OutScript.decode(pkScript));
1503
+ var chainSourceFrom = (provider, network) => {
1504
+ const spenderFromHistory = async (txid, vout, pkScript) => {
1505
+ const txs = await provider.getTransactions(addressOf(pkScript, network));
1506
+ return txs.find((tx) => tx.vin?.some((i) => i.txid === txid && i.vout === vout))?.txid;
1507
+ };
1508
+ return {
1509
+ async getScriptUtxos(pkScript) {
1510
+ const address = addressOf(pkScript, network);
1511
+ const [coins, tip] = await Promise.all([
1512
+ provider.getCoins(address),
1513
+ provider.getChainTip()
1514
+ ]);
1515
+ return coins.map((coin) => ({
1516
+ txid: coin.txid,
1517
+ vout: coin.vout,
1518
+ amount: BigInt(coin.value),
1519
+ // Zero, not one: calling a mempool output "1 deep" would let a
1520
+ // 1-confirmation policy claim against a replaceable transaction.
1521
+ confirmations: coin.status.confirmed && typeof coin.status.block_height === "number" ? Math.max(0, tip.height - coin.status.block_height + 1) : 0
1522
+ }));
1523
+ },
1524
+ async getSpendingTx(txid, vout, pkScript) {
1525
+ const outspends = await provider.getTxOutspends(txid);
1526
+ const outspend = outspends[vout];
1527
+ if (!outspend?.spent) return null;
1528
+ const spender = outspend.txid || await spenderFromHistory(txid, vout, pkScript);
1529
+ if (!spender) return null;
1530
+ const raw = await provider.getRawTransaction(spender);
1531
+ return { txHex: hex7.encode(raw) };
1532
+ },
1533
+ broadcast(txHex) {
1534
+ return provider.broadcastTransaction(txHex);
1535
+ },
1536
+ async getMtp() {
1537
+ return (await provider.getChainTip()).time;
1538
+ }
1539
+ };
1540
+ };
1541
+
1542
+ // src/payment/rendezvous.ts
1543
+ import { selectMarkets, sideLimits as sideLimits2 } from "@arkade-os/solver-discovery";
1396
1544
  import { hex as hex8 } from "@scure/base";
1545
+ var XONLY_HEX = /^[0-9a-f]{64}$/;
1546
+ var rendezvousOf = (market, pinned) => {
1547
+ const transports = { nostr: { relays: market.transports?.nostr?.relays ?? [] } };
1548
+ if (!market.discovery_pubkey || !transports.nostr.relays.length) return void 0;
1549
+ const advertised = market.emulator_pubkey;
1550
+ const emulatorPubkey = advertised === void 0 || advertised === null || advertised === "" ? pinned : typeof advertised === "string" && XONLY_HEX.test(advertised) ? advertised : void 0;
1551
+ if (!emulatorPubkey) return void 0;
1552
+ if (pinned && emulatorPubkey !== pinned) return void 0;
1553
+ const bounds = sideLimits2(market, "quote");
1554
+ if (!bounds) return void 0;
1555
+ return {
1556
+ solverPubkey: market.discovery_pubkey,
1557
+ transports,
1558
+ minSats: Number(bounds.min),
1559
+ maxSats: Number(bounds.max)
1560
+ };
1561
+ };
1562
+ var solverRendezvous = (markets, payoutCorridor, amountSats, fallbackEmulatorPubkey) => {
1563
+ const encoded = fallbackEmulatorPubkey ? hex8.encode(fallbackEmulatorPubkey) : void 0;
1564
+ if (encoded !== void 0 && !XONLY_HEX.test(encoded)) return void 0;
1565
+ const pinned = encoded;
1566
+ const candidates = selectMarkets(markets, {
1567
+ baseId: BTC_ASSET_ID,
1568
+ quoteId: BTC_ASSET_ID,
1569
+ baseCorridor: "arkade",
1570
+ quoteCorridor: payoutCorridor
1571
+ });
1572
+ for (const market of candidates) {
1573
+ const rendezvous = rendezvousOf(market, pinned);
1574
+ if (!rendezvous) continue;
1575
+ if (amountSats >= rendezvous.minSats && amountSats <= rendezvous.maxSats) {
1576
+ return rendezvous;
1577
+ }
1578
+ }
1579
+ return void 0;
1580
+ };
1581
+
1582
+ // src/payment/solverOnchain.ts
1583
+ import { btcTarget, makeHandle, resolveSendAmount, tryResolveSendAmount } from "@arkade-os/sdk";
1584
+ var SOLVER_ONCHAIN_RAIL = "solver-onchain";
1585
+ var solverOnchainRendezvous = (markets, amountSats, fallbackEmulatorPubkey) => solverRendezvous(markets, "onchain", amountSats, fallbackEmulatorPubkey);
1586
+ function solverOnchainRail(deps) {
1587
+ const rendezvousFor = async (amount) => {
1588
+ if (amount === void 0) return void 0;
1589
+ const markets = await deps.discover();
1590
+ return solverOnchainRendezvous(markets, amount, deps.fallbackEmulatorPubkey);
1591
+ };
1592
+ return {
1593
+ id: SOLVER_ONCHAIN_RAIL,
1594
+ match: (req) => btcTarget(req.raw) !== void 0,
1595
+ available: async (req) => {
1596
+ const address = btcTarget(req.raw);
1597
+ if (!address) return false;
1598
+ try {
1599
+ l1ScriptForAddress(address, deps.l1Network);
1600
+ } catch {
1601
+ return false;
1602
+ }
1603
+ const amount = tryResolveSendAmount(req.raw, req.amount);
1604
+ if (amount === void 0) return false;
1605
+ return await rendezvousFor(amount) !== void 0;
1606
+ },
1607
+ quote: async (req, ctx) => {
1608
+ const address = btcTarget(req.raw);
1609
+ const amount = resolveSendAmount(SOLVER_ONCHAIN_RAIL, req.raw, req.amount);
1610
+ const payoutPkScript = l1ScriptForAddress(address, deps.l1Network);
1611
+ const rendezvous = await rendezvousFor(amount);
1612
+ if (!rendezvous) {
1613
+ throw new Error(
1614
+ `${SOLVER_ONCHAIN_RAIL}: no solver serves arkade:BTC -> onchain:BTC at ${amount} sats`
1615
+ );
1616
+ }
1617
+ const negotiated = await deps.connect(
1618
+ rendezvous,
1619
+ (transport) => requestOnchainSend(ctx.wallet, deps.arkServerUrl, transport, {
1620
+ amount,
1621
+ amountSide: "to",
1622
+ payoutPubkey: deps.payoutPubkey,
1623
+ ...deps.emulatorPubkey ? { emulatorPubkey: deps.emulatorPubkey } : {}
1624
+ })
1625
+ );
1626
+ if (negotiated.l1Network !== deps.l1Network) {
1627
+ throw new Error(
1628
+ `${SOLVER_ONCHAIN_RAIL}: rail built for ${deps.l1Network} but the swap was negotiated on ${negotiated.l1Network}`
1629
+ );
1630
+ }
1631
+ const swap = { ...negotiated, rendezvous, payoutPkScript };
1632
+ return {
1633
+ railId: SOLVER_ONCHAIN_RAIL,
1634
+ amount,
1635
+ fee: swap.fundAmount - amount,
1636
+ total: swap.fundAmount,
1637
+ meta: {
1638
+ rfqId: swap.rfqId,
1639
+ validUntil: swap.quote.valid_until,
1640
+ htlcAddress: swap.htlc.address,
1641
+ minConfirmations: swap.minConfirmations,
1642
+ solverPubkey: rendezvous.solverPubkey,
1643
+ // The claim tx's fee comes out of the HTLC output at a rate
1644
+ // not knowable now, so `amount` is the payout, not the net.
1645
+ claimFeeDeductedFromPayout: true
1646
+ },
1647
+ send: async () => makeHandle(SOLVER_ONCHAIN_RAIL, async (emit) => {
1648
+ assertFundable({
1649
+ quote: swap.quote,
1650
+ now: Math.floor(Date.now() / 1e3),
1651
+ onchain: {
1652
+ htlcLocktime: swap.htlcParams.refundLocktime,
1653
+ minConfirmations: swap.minConfirmations,
1654
+ direction: "send"
1655
+ }
1656
+ });
1657
+ await deps.persist(swap);
1658
+ await ctx.wallet.send({
1659
+ address: swap.address,
1660
+ amount: swap.fundAmount
1661
+ });
1662
+ emit({ status: "sent" });
1663
+ const result = { railId: SOLVER_ONCHAIN_RAIL, swapId: swap.rfqId };
1664
+ if (!deps.awaitSettlement) return result;
1665
+ let txid;
1666
+ try {
1667
+ ({ txid } = await deps.awaitSettlement(swap));
1668
+ } catch (e) {
1669
+ console.warn(
1670
+ `${SOLVER_ONCHAIN_RAIL}: settlement watch failed; the payment is sent`,
1671
+ e
1672
+ );
1673
+ return result;
1674
+ }
1675
+ const settled = { ...result, txid };
1676
+ emit({ status: "settled", result: settled });
1677
+ return settled;
1678
+ })
1679
+ };
1680
+ }
1681
+ };
1682
+ }
1683
+
1684
+ // src/payment/solverLightning.ts
1685
+ import { invoiceTarget, makeHandle as makeHandle2 } from "@arkade-os/sdk";
1686
+ var SOLVER_LIGHTNING_RAIL = "solver-lightning";
1687
+ var factsOf = (raw, decode, now) => {
1688
+ const invoice = invoiceTarget(raw);
1689
+ if (!invoice) return void 0;
1690
+ let facts;
1691
+ try {
1692
+ facts = decode(invoice);
1693
+ } catch {
1694
+ return void 0;
1695
+ }
1696
+ if (!Number.isInteger(facts.amountSats) || facts.amountSats <= 0) return void 0;
1697
+ if (facts.expiresAt <= now) return void 0;
1698
+ return facts;
1699
+ };
1700
+ var solverLightningRendezvous = (markets, amountSats, fallbackEmulatorPubkey) => solverRendezvous(markets, "lightning", amountSats, fallbackEmulatorPubkey);
1701
+ function solverLightningRail(deps) {
1702
+ const rendezvousFor = async (amountSats) => solverLightningRendezvous(await deps.discover(), amountSats, deps.fallbackEmulatorPubkey);
1703
+ return {
1704
+ id: SOLVER_LIGHTNING_RAIL,
1705
+ match: (req) => invoiceTarget(req.raw) !== void 0,
1706
+ available: async (req) => {
1707
+ const facts = factsOf(req.raw, deps.decodeInvoice, Math.floor(Date.now() / 1e3));
1708
+ if (!facts) return false;
1709
+ if (req.amount !== void 0 && req.amount !== facts.amountSats) return false;
1710
+ return await rendezvousFor(facts.amountSats) !== void 0;
1711
+ },
1712
+ quote: async (req, ctx) => {
1713
+ const facts = factsOf(req.raw, deps.decodeInvoice, Math.floor(Date.now() / 1e3));
1714
+ if (!facts) {
1715
+ throw new Error(
1716
+ `${SOLVER_LIGHTNING_RAIL}: the request carries no payable BOLT11 invoice (amountless, expired, or undecodable)`
1717
+ );
1718
+ }
1719
+ if (req.amount !== void 0 && req.amount !== facts.amountSats) {
1720
+ throw new Error(
1721
+ `${SOLVER_LIGHTNING_RAIL}: the request names ${req.amount} sats but the invoice is for ${facts.amountSats} \u2014 the payee is paid the invoice`
1722
+ );
1723
+ }
1724
+ const rendezvous = await rendezvousFor(facts.amountSats);
1725
+ if (!rendezvous) {
1726
+ throw new Error(
1727
+ `${SOLVER_LIGHTNING_RAIL}: no solver serves arkade:BTC -> lightning:BTC at ${facts.amountSats} sats`
1728
+ );
1729
+ }
1730
+ const negotiated = await deps.connect(
1731
+ rendezvous,
1732
+ (transport) => requestLightningSend(ctx.wallet, deps.arkServerUrl, transport, {
1733
+ invoice: facts,
1734
+ ...deps.emulatorPubkey ? { emulatorPubkey: deps.emulatorPubkey } : {}
1735
+ })
1736
+ );
1737
+ const swap = { ...negotiated, invoice: facts, rendezvous };
1738
+ return {
1739
+ railId: SOLVER_LIGHTNING_RAIL,
1740
+ // `requestLightningSend` refuses a quote that reprices the
1741
+ // invoice, so the spread is a fee on top.
1742
+ amount: facts.amountSats,
1743
+ fee: swap.fundAmount - facts.amountSats,
1744
+ total: swap.fundAmount,
1745
+ meta: {
1746
+ rfqId: swap.rfqId,
1747
+ validUntil: swap.quote.valid_until,
1748
+ paymentHash: facts.paymentHash,
1749
+ invoiceExpiresAt: facts.expiresAt,
1750
+ solverPubkey: rendezvous.solverPubkey
1751
+ },
1752
+ send: async () => makeHandle2(SOLVER_LIGHTNING_RAIL, async (emit) => {
1753
+ assertFundable({
1754
+ quote: swap.quote,
1755
+ invoiceExpiresAt: facts.expiresAt,
1756
+ now: Math.floor(Date.now() / 1e3)
1757
+ });
1758
+ await deps.persist(swap);
1759
+ await ctx.wallet.send({
1760
+ address: swap.address,
1761
+ amount: swap.fundAmount
1762
+ });
1763
+ emit({ status: "sent" });
1764
+ const result = { railId: SOLVER_LIGHTNING_RAIL, swapId: swap.rfqId };
1765
+ if (!deps.awaitSettlement) return result;
1766
+ let preimage;
1767
+ try {
1768
+ ({ preimage } = await deps.awaitSettlement(swap));
1769
+ } catch (e) {
1770
+ console.warn(
1771
+ `${SOLVER_LIGHTNING_RAIL}: settlement watch failed; the payment is sent`,
1772
+ e
1773
+ );
1774
+ return result;
1775
+ }
1776
+ const settled = { ...result, ...preimage !== void 0 && { preimage } };
1777
+ emit({ status: "settled", result: settled });
1778
+ return settled;
1779
+ })
1780
+ };
1781
+ }
1782
+ };
1783
+ }
1784
+
1785
+ // src/claim.ts
1786
+ import { hex as hex10 } from "@scure/base";
1397
1787
  import { ripemd160 } from "@noble/hashes/legacy.js";
1398
1788
  import { sha256 as sha2563 } from "@noble/hashes/sha2.js";
1399
1789
  import {
@@ -1403,7 +1793,7 @@ import {
1403
1793
  } from "@arkade-os/sdk";
1404
1794
 
1405
1795
  // src/refund.ts
1406
- import { base64 as base643, hex as hex7 } from "@scure/base";
1796
+ import { base64 as base643, hex as hex9 } from "@scure/base";
1407
1797
  import { sha256 as sha2562 } from "@noble/hashes/sha2.js";
1408
1798
  import {
1409
1799
  CSVMultisigTapscript,
@@ -1461,7 +1851,7 @@ var LockupNeedsRecoveryError = class extends Error {
1461
1851
  }
1462
1852
  };
1463
1853
  async function findLockupVtxos(indexer, swapPkScript) {
1464
- const scripts = [hex7.encode(swapPkScript)];
1854
+ const scripts = [hex9.encode(swapPkScript)];
1465
1855
  const [spendable, recoverable] = await Promise.all([
1466
1856
  indexer.getVtxos({ scripts, spendableOnly: true }),
1467
1857
  indexer.getVtxos({ scripts, recoverableOnly: true })
@@ -1487,13 +1877,13 @@ async function findLockupVtxos(indexer, swapPkScript) {
1487
1877
  }
1488
1878
  return out;
1489
1879
  }
1490
- var hashesTo = (candidate, paymentHash) => hex7.encode(sha2562(candidate)) === paymentHash;
1880
+ var hashesTo = (candidate, paymentHash) => hex9.encode(sha2562(candidate)) === paymentHash;
1491
1881
  var candidateWitnessItems = (tx, inputIndex) => [
1492
1882
  ...getArkPsbtFields(tx, inputIndex, ConditionWitness).flat(),
1493
1883
  ...tx.getInput(inputIndex).finalScriptWitness ?? []
1494
1884
  ];
1495
1885
  async function readLockupFate(indexer, input) {
1496
- const { vtxos } = await indexer.getVtxos({ scripts: [hex7.encode(input.swapPkScript)] });
1886
+ const { vtxos } = await indexer.getVtxos({ scripts: [hex9.encode(input.swapPkScript)] });
1497
1887
  const all = vtxos ?? [];
1498
1888
  if (all.length === 0) return { fate: "unknown" };
1499
1889
  const exited = all.filter((vtxo) => vtxo.isUnrolled && !hasTerminalSpend(vtxo));
@@ -1528,7 +1918,7 @@ async function readLockupFate(indexer, input) {
1528
1918
  for (let i = 0; i < tx.inputsLength; i++) {
1529
1919
  const spent = tx.getInput(i);
1530
1920
  if (!spent.txid) continue;
1531
- const txid = hex7.encode(spent.txid);
1921
+ const txid = hex9.encode(spent.txid);
1532
1922
  if (!all.some((vtxo) => vtxo.txid === txid && vtxo.vout === spent.index)) continue;
1533
1923
  for (const candidate of candidateWitnessItems(tx, i)) {
1534
1924
  if (hashesTo(candidate, input.paymentHash)) {
@@ -1548,16 +1938,16 @@ async function pushRefundWithoutReceiver(ark, input) {
1548
1938
  input.script.options.refundLocktime
1549
1939
  );
1550
1940
  }
1551
- const refundPkScript = input.refundPkScript ?? input.script.options.nonInteractiveRefund?.senderPkScript;
1941
+ const refundPkScript = input.refundPkScript ?? input.script.options.nonInteractiveParameters?.senderPkScript;
1552
1942
  if (!refundPkScript) {
1553
1943
  throw new Error(
1554
- "no refund destination: the contract carries no nonInteractiveRefund leaf, so pass refundPkScript explicitly"
1944
+ "no refund destination: the contract carries no emulator covenant suite, so pass refundPkScript explicitly"
1555
1945
  );
1556
1946
  }
1557
1947
  const info = await ark.getInfo();
1558
1948
  let serverUnrollScript;
1559
1949
  try {
1560
- serverUnrollScript = CSVMultisigTapscript.decode(hex7.decode(info.checkpointTapscript));
1950
+ serverUnrollScript = CSVMultisigTapscript.decode(hex9.decode(info.checkpointTapscript));
1561
1951
  } catch {
1562
1952
  throw new Error("invalid checkpointTapscript from the Arkade server");
1563
1953
  }
@@ -1603,6 +1993,21 @@ async function refundIfUnresolved(transport, ark, indexer, input) {
1603
1993
  const status = await transport.status(input.rfqId);
1604
1994
  if (status && isResolved(status.state)) return { outcome: "resolved", status };
1605
1995
  if (now() >= input.refundLocktime) {
1996
+ let fate = { fate: "unknown" };
1997
+ try {
1998
+ fate = await readLockupFate(indexer, {
1999
+ swapPkScript: input.script.pkScript,
2000
+ paymentHash: input.paymentHash
2001
+ });
2002
+ } catch {
2003
+ }
2004
+ if (fate.fate === "exited") {
2005
+ return {
2006
+ outcome: "exited",
2007
+ outpoints: fate.outpoints.map((o) => `${o.txid}:${o.vout}`),
2008
+ status
2009
+ };
2010
+ }
1606
2011
  const vtxos = await findLockupVtxos(indexer, input.script.pkScript);
1607
2012
  if (vtxos.length === 0) return { outcome: "nothing_to_refund", status };
1608
2013
  try {
@@ -1668,13 +2073,13 @@ async function pushClaim(ark, input) {
1668
2073
  }
1669
2074
  }
1670
2075
  const committed = input.script.options.preimageHash;
1671
- if (hex8.encode(ripemd160(sha2563(input.preimage))) !== hex8.encode(committed)) {
2076
+ if (hex10.encode(ripemd160(sha2563(input.preimage))) !== hex10.encode(committed)) {
1672
2077
  throw new Error("preimage does not match the covenant's payment hash");
1673
2078
  }
1674
2079
  const info = await ark.getInfo();
1675
2080
  let serverUnrollScript;
1676
2081
  try {
1677
- serverUnrollScript = CSVMultisigTapscript2.decode(hex8.decode(info.checkpointTapscript));
2082
+ serverUnrollScript = CSVMultisigTapscript2.decode(hex10.decode(info.checkpointTapscript));
1678
2083
  } catch {
1679
2084
  throw new Error("invalid checkpointTapscript from the Arkade server");
1680
2085
  }
@@ -1793,7 +2198,7 @@ function arkadeRefunder(deps) {
1793
2198
  }
1794
2199
 
1795
2200
  // src/swapManager.ts
1796
- import { hex as hex9 } from "@scure/base";
2201
+ import { hex as hex11 } from "@scure/base";
1797
2202
  function nextOnchainAction(input) {
1798
2203
  switch (input.phase.phase) {
1799
2204
  case "unfunded":
@@ -2241,7 +2646,7 @@ var RfqSwapManager = class {
2241
2646
  // ── internals ────────────────────────────────────────────────────────────
2242
2647
  track(swap) {
2243
2648
  this.monitored.set(swap.rfqId, swap);
2244
- this.byLockupScript.set(hex9.encode(swap.lockupPkScript), swap);
2649
+ this.byLockupScript.set(hex11.encode(swap.lockupPkScript), swap);
2245
2650
  }
2246
2651
  /** Drops the swap from BOTH indexes. The event index is the one that stops
2247
2652
  * a late event finding a swap that is gone; `pollSwap`'s own
@@ -2250,7 +2655,7 @@ var RfqSwapManager = class {
2250
2655
  * them from silently re-driving a cancelled swap. */
2251
2656
  untrack(rfqId) {
2252
2657
  const swap = this.monitored.get(rfqId);
2253
- if (swap) this.byLockupScript.delete(hex9.encode(swap.lockupPkScript));
2658
+ if (swap) this.byLockupScript.delete(hex11.encode(swap.lockupPkScript));
2254
2659
  this.monitored.delete(rfqId);
2255
2660
  this.refundRefused.delete(rfqId);
2256
2661
  this.lastClaimError.delete(rfqId);
@@ -2312,7 +2717,7 @@ var RfqSwapManager = class {
2312
2717
  if (!lockup) {
2313
2718
  try {
2314
2719
  const [existing] = await contracts.getContracts({
2315
- script: hex9.encode(swap.lockupPkScript)
2720
+ script: hex11.encode(swap.lockupPkScript)
2316
2721
  });
2317
2722
  if (existing) {
2318
2723
  this.registered.set(swap.rfqId, true);
@@ -2331,13 +2736,13 @@ var RfqSwapManager = class {
2331
2736
  );
2332
2737
  return;
2333
2738
  }
2334
- const script = hex9.encode(lockup.script.pkScript);
2335
- if (script !== hex9.encode(swap.lockupPkScript)) {
2739
+ const script = hex11.encode(lockup.script.pkScript);
2740
+ if (script !== hex11.encode(swap.lockupPkScript)) {
2336
2741
  this.registered.set(swap.rfqId, false);
2337
2742
  this.emitFailed(
2338
2743
  swap,
2339
2744
  new Error(
2340
- `swap ${swap.rfqId} lockup script ${script} does not match its lockupPkScript ${hex9.encode(swap.lockupPkScript)}`
2745
+ `swap ${swap.rfqId} lockup script ${script} does not match its lockupPkScript ${hex11.encode(swap.lockupPkScript)}`
2341
2746
  )
2342
2747
  );
2343
2748
  return;
@@ -2356,7 +2761,7 @@ var RfqSwapManager = class {
2356
2761
  * script for its whole life. Best-effort — the swap is over either way. */
2357
2762
  retireContract(swap) {
2358
2763
  if (!this.deps.contracts || !this.registered.get(swap.rfqId)) return;
2359
- void this.deps.contracts.setContractWatchState(hex9.encode(swap.lockupPkScript), "retained").catch((error) => this.emitFailed(swap, error));
2764
+ void this.deps.contracts.setContractWatchState(hex11.encode(swap.lockupPkScript), "retained").catch((error) => this.emitFailed(swap, error));
2360
2765
  }
2361
2766
  arm() {
2362
2767
  if (!this.running) return;
@@ -2832,7 +3237,7 @@ var outpointKey = (vtxo) => `${vtxo.txid}:${vtxo.vout}`;
2832
3237
  import {
2833
3238
  ArkAddress as ArkAddress4
2834
3239
  } from "@arkade-os/sdk";
2835
- import { hex as hex10 } from "@scure/base";
3240
+ import { hex as hex12 } from "@scure/base";
2836
3241
  var LABELS = {
2837
3242
  lightning_send: "Lightning send",
2838
3243
  lightning_receive: "Lightning receive",
@@ -2907,7 +3312,7 @@ async function activityInputOf(record, indexer) {
2907
3312
  async function lockupTxids(indexer, record, wantFunding) {
2908
3313
  let script;
2909
3314
  try {
2910
- script = hex10.encode(ArkAddress4.decode(record.lockupAddress).pkScript);
3315
+ script = hex12.encode(ArkAddress4.decode(record.lockupAddress).pkScript);
2911
3316
  } catch {
2912
3317
  return [];
2913
3318
  }
@@ -2960,6 +3365,8 @@ export {
2960
3365
  RfqSwapManager,
2961
3366
  RfqSwapOriginRequired,
2962
3367
  SOLO_REFUND_HEADROOM_SECONDS,
3368
+ SOLVER_LIGHTNING_RAIL,
3369
+ SOLVER_ONCHAIN_RAIL,
2963
3370
  SWAP_LOCKUP_CONTRACT_KIND,
2964
3371
  SWAP_LOCKUP_CONTRACT_LABEL,
2965
3372
  SWAP_LOCKUP_CONTRACT_TYPE,
@@ -2976,6 +3383,7 @@ export {
2976
3383
  buildHtlcClaim,
2977
3384
  buildHtlcRefund,
2978
3385
  cancelOffer,
3386
+ chainSourceFrom,
2979
3387
  claimOnchainFill,
2980
3388
  claimReceiveLockup,
2981
3389
  classifyDepositSpend,
@@ -2997,6 +3405,7 @@ export {
2997
3405
  httpTransport,
2998
3406
  isRfqSwapTerminal,
2999
3407
  isRfqTerminal,
3408
+ l1ScriptForAddress,
3000
3409
  lightningReceiveRequest,
3001
3410
  lightningSendRequest,
3002
3411
  lightningSendVtxoScript,
@@ -3036,6 +3445,11 @@ export {
3036
3445
  sealClaimPacket,
3037
3446
  senderIdentityForSwapRecord,
3038
3447
  shouldRetainRfqSwap,
3448
+ solverLightningRail,
3449
+ solverLightningRendezvous,
3450
+ solverOnchainRail,
3451
+ solverOnchainRendezvous,
3452
+ solverRendezvous,
3039
3453
  spendTxidsOf,
3040
3454
  spendUpdate,
3041
3455
  swapActivityResolver,