@arkade-os/swap 0.0.12 → 0.1.0-rc.1

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
@@ -14,6 +14,7 @@ import {
14
14
  MIN_HEADROOM_SECONDS,
15
15
  ONCHAIN_BTC,
16
16
  ONCHAIN_CLAIM_MARGIN_SECONDS,
17
+ ONCHAIN_CLAIM_VSIZE,
17
18
  ONCHAIN_DUST_SATS,
18
19
  ONCHAIN_ORDER_MARGIN_SECONDS,
19
20
  ONCHAIN_RECEIVE_PAIR,
@@ -39,10 +40,12 @@ import {
39
40
  deriveOnchainSend,
40
41
  extractPreimage,
41
42
  httpTransport,
43
+ l1NetworkFromArk,
42
44
  l1ScriptForAddress,
45
+ lightningReceiveContract,
43
46
  lightningReceiveRequest,
47
+ lightningSendContract,
44
48
  lightningSendRequest,
45
- lightningSendVtxoScript,
46
49
  lockupContractParams,
47
50
  newPreimage,
48
51
  newRfqId,
@@ -51,7 +54,6 @@ import {
51
54
  onchainReceiveRequest,
52
55
  onchainSendRequest,
53
56
  paymentHashOf,
54
- receiveVtxoScript,
55
57
  registerLockupContract,
56
58
  relayTransport,
57
59
  requestLightningReceive,
@@ -65,22 +67,20 @@ import {
65
67
  unilateralRefundWithoutReceiverDelay,
66
68
  verifyLockupAddress,
67
69
  verifyReceiveInvoice
68
- } from "./chunk-XXWOODUE.js";
70
+ } from "./chunk-OEKSNYJW.js";
69
71
  import {
70
72
  InMemoryAssetSwapRepository,
71
73
  marketsCacheKey
72
- } from "./chunk-6ZUS47GA.js";
74
+ } from "./chunk-U7VWWTL6.js";
73
75
 
74
76
  // src/offer.ts
75
77
  import { hex as hex2 } from "@scure/base";
76
78
  import { concatBytes } from "@scure/btc-signer/utils.js";
77
79
  import {
78
80
  ArkAddress,
79
- RestArkProvider,
80
- RestIndexerProvider,
81
81
  arkade,
82
82
  asset,
83
- getNetwork,
83
+ networkFromArkadeInfo,
84
84
  resolveEmulatorPubkey,
85
85
  toXOnlySignerHex
86
86
  } from "@arkade-os/sdk";
@@ -244,9 +244,9 @@ var addAssetSwap = async (repository, swap) => {
244
244
  const swaps = await getAssetSwapsOrThrow(repository);
245
245
  if (swaps.some((s) => s.id === swap.id)) return swaps;
246
246
  await saveSwapOrThrow(repository, swap);
247
- const at = swaps.findIndex((s) => byNewest(swap, s) <= 0);
247
+ const at2 = swaps.findIndex((s) => byNewest(swap, s) <= 0);
248
248
  const merged = [...swaps];
249
- merged.splice(at === -1 ? merged.length : at, 0, swap);
249
+ merged.splice(at2 === -1 ? merged.length : at2, 0, swap);
250
250
  return merged;
251
251
  };
252
252
  var updateAssetSwap = async (repository, id, changes) => {
@@ -346,7 +346,7 @@ function withExitClosure(program, exit) {
346
346
  }
347
347
  };
348
348
  }
349
- function swapProgramBinding(offer, serverPubkey) {
349
+ function swapProgramBinding(offer, operatorPubkey) {
350
350
  if (offer.makerPkScript.length !== FIELDS.makerPkScript.width) {
351
351
  throw new Error("makerPkScript is not a 34-byte taproot scriptPubKey");
352
352
  }
@@ -358,7 +358,7 @@ function swapProgramBinding(offer, serverPubkey) {
358
358
  args: {
359
359
  makerWP: offer.makerPkScript.subarray(2),
360
360
  wantAmount: offer.wantAmount,
361
- server: serverPubkey,
361
+ server: operatorPubkey,
362
362
  user: offer.makerPublicKey,
363
363
  // internal byte order
364
364
  ...offer.wantAsset && {
@@ -368,14 +368,14 @@ function swapProgramBinding(offer, serverPubkey) {
368
368
  ...offer.exitDelay && { exitDelay: offer.exitDelay.value }
369
369
  },
370
370
  keys: {
371
- serverKey: serverPubkey,
371
+ serverKey: operatorPubkey,
372
372
  userKey: offer.makerPublicKey,
373
373
  emulatorKey: offer.emulatorPubkey
374
374
  }
375
375
  };
376
376
  }
377
- function offerVtxoScript(offer, serverPubkey) {
378
- const { program, args, keys } = swapProgramBinding(offer, serverPubkey);
377
+ function offerContract(offer, operatorPubkey) {
378
+ const { program, args, keys } = swapProgramBinding(offer, operatorPubkey);
379
379
  return new arkade.ArkadeProgramScript(program, args, keys);
380
380
  }
381
381
  var OFFER_PACKET_TYPE = 3;
@@ -489,14 +489,14 @@ function decodeOffer(data) {
489
489
  throw new Error(`missing/invalid ${name}`);
490
490
  }
491
491
  }
492
- const need = (name) => {
492
+ const need2 = (name) => {
493
493
  const v = fields[name];
494
494
  const len = FIELDS[name].width;
495
495
  if (!v || len !== void 0 && v.length !== len)
496
496
  throw new Error(`missing/invalid ${name}`);
497
497
  return v;
498
498
  };
499
- const amount = need("wantAmount");
499
+ const amount = need2("wantAmount");
500
500
  if (Boolean(fields.wantAsset) === Boolean(fields.offerAsset)) {
501
501
  throw new Error("offer must carry exactly one of wantAsset or offerAsset");
502
502
  }
@@ -513,13 +513,13 @@ function decodeOffer(data) {
513
513
  throw new Error("offer must carry both ratioNum and ratioDen, or neither");
514
514
  }
515
515
  return {
516
- swapPkScript: need("swapPkScript"),
516
+ swapPkScript: need2("swapPkScript"),
517
517
  wantAmount: readU64(amount),
518
518
  ...fields.wantAsset && { wantAsset: asset.AssetId.fromBytes(fields.wantAsset) },
519
519
  ...fields.offerAsset && { offerAsset: asset.AssetId.fromBytes(fields.offerAsset) },
520
- makerPkScript: need("makerPkScript"),
521
- makerPublicKey: need("makerPublicKey"),
522
- emulatorPubkey: need("emulatorPubkey"),
520
+ makerPkScript: need2("makerPkScript"),
521
+ makerPublicKey: need2("makerPublicKey"),
522
+ emulatorPubkey: need2("emulatorPubkey"),
523
523
  ...ratioNum !== void 0 && { ratioNum },
524
524
  ...ratioDen !== void 0 && { ratioDen },
525
525
  ...fields.exitTimelock && { exitDelay: decodeExitDelay(fields.exitTimelock) }
@@ -532,17 +532,21 @@ function decodeExitDelay(value) {
532
532
  }
533
533
  var OFFER_CONTRACT_LABEL = "Arkade swap offer";
534
534
  var OFFER_CONTRACT_KIND = "asset-swap-offer";
535
- async function registerOfferContract(wallet, arkServerUrl, network, binding, serverPubkey, expectedPkScript) {
536
- const { program, args, keys } = swapProgramBinding(binding, serverPubkey);
535
+ async function registerOfferContract(wallet, info, binding, operatorPubkey, expectedPkScript) {
536
+ const { program, args, keys } = swapProgramBinding(binding, operatorPubkey);
537
537
  const contractManager = await wallet.getContractManager();
538
538
  const client = await arkade.Arkade.connect({
539
- arkade: new RestArkProvider(arkServerUrl),
540
- indexer: new RestIndexerProvider(arkServerUrl),
539
+ // Registration derives and persists; it never broadcasts and never
540
+ // reads UTXOs, so the only thing the client needs off the server is the
541
+ // info the caller already resolved. Handing that back — rather than a
542
+ // provider built from a URL — is what lets `createOffer` take just a
543
+ // wallet, and spares a second `/v1/info` round-trip.
544
+ arkade: { getInfo: async () => info },
541
545
  identity: wallet.identity,
542
546
  // without this the row's `address` would be derived against the SDK's
543
547
  // default network while its script is right — a row that disagrees with
544
548
  // the address the user is about to fund
545
- network: getNetwork(network),
549
+ network: networkFromArkadeInfo(info),
546
550
  contractManager
547
551
  });
548
552
  const contract = new arkade.ArkadeContract(client, program, args, keys);
@@ -563,17 +567,21 @@ function serverExitDelay(delay) {
563
567
  }
564
568
  return assertExitDelay({ value: delay, type: delay < BigInt(512) ? "blocks" : "seconds" });
565
569
  }
566
- async function createOffer(wallet, arkServerUrl, params) {
570
+ async function createOffer(wallet, params) {
567
571
  if (Boolean(params.wantAsset) === Boolean(params.offerAsset)) {
568
572
  throw new Error("set exactly one of wantAsset (BTC->asset) or offerAsset (asset->BTC)");
569
573
  }
570
574
  const [info, makerAddress, makerPublicKey] = await Promise.all([
571
- new RestArkProvider(arkServerUrl).getInfo(),
575
+ // requireLive: this info binds signerPubkey into the covenant — a
576
+ // snapshot could derive an address the operator no longer co-signs
577
+ // for, so an unreachable operator fails the call instead (fail closed,
578
+ // the same behaviour the pre-#734 caller-held provider had)
579
+ wallet.getArkadeInfo({ requireLive: true }),
572
580
  wallet.getAddress(),
573
581
  wallet.identity.xOnlyPublicKey()
574
582
  ]);
575
- const serverPubKey = hex2.decode(toXOnlySignerHex(info.signerPubkey));
576
- const network = getNetwork(info.network);
583
+ const operatorPubKey = hex2.decode(toXOnlySignerHex(info.signerPubkey));
584
+ const network = networkFromArkadeInfo(info);
577
585
  const emuKey = hex2.decode(
578
586
  toXOnlySignerHex(resolveEmulatorPubkey(network, params.emulatorPubkey))
579
587
  );
@@ -589,33 +597,77 @@ async function createOffer(wallet, arkServerUrl, params) {
589
597
  // an offer that then fails to encode. @see assertExitDelay
590
598
  exitDelay: params.noExit ? void 0 : params.exitDelay ? assertExitDelay(params.exitDelay) : serverExitDelay(info.unilateralExitDelay)
591
599
  };
592
- const script = offerVtxoScript(binding, serverPubKey);
600
+ const script = offerContract(binding, operatorPubKey);
593
601
  const offer = { ...binding, swapPkScript: script.pkScript };
594
- await registerOfferContract(
595
- wallet,
596
- arkServerUrl,
597
- info.network,
598
- binding,
599
- serverPubKey,
600
- script.pkScript
601
- );
602
+ await registerOfferContract(wallet, info, binding, operatorPubKey, script.pkScript);
602
603
  const payload = encodeOffer(offer);
603
604
  return {
604
605
  offerHex: hex2.encode(payload),
605
606
  extension: { type: OFFER_PACKET_TYPE, payload },
606
- // VtxoScript.address owns address construction; assembling an ArkAddress
607
+ // the contract's .address() builds the address; assembling an ArkAddress
607
608
  // from tweakedPublicKey here would silently miss any future step it gains
608
- address: script.address(network.hrp, serverPubKey).encode(),
609
+ address: script.address(network.hrp, operatorPubKey).encode(),
609
610
  swapPkScript: script.pkScript
610
611
  };
611
612
  }
612
- async function cancelOffer(wallet, arkServerUrl, offerHex, opts) {
613
+ async function cancelOffer(wallet, offerHex, opts) {
613
614
  const { repository, fundingTxid, swapAddress } = opts;
615
+ const prepared = await prepareOfferCancel(wallet, offerHex, {
616
+ ...fundingTxid === void 0 ? {} : { fundingTxid },
617
+ ...swapAddress === void 0 ? {} : { swapAddress }
618
+ });
619
+ const swapId = fundingTxid ?? prepared.vtxo.txid;
620
+ const hasLocalRecord = (await getAssetSwapsOrThrow(repository)).some((s) => s.id === swapId);
621
+ if (hasLocalRecord) await updateAssetSwap(repository, swapId, { status: "cancelling" });
622
+ const txid = await prepared.send();
623
+ if (hasLocalRecord) {
624
+ const { persisted, swaps } = await updateAssetSwapBestEffort(repository, swapId, {
625
+ status: "cancelled",
626
+ spentTxid: txid
627
+ });
628
+ if (persisted) {
629
+ const contractManager = await wallet.getContractManager();
630
+ await retireOfferContract(
631
+ contractManager,
632
+ swaps,
633
+ hex2.encode(prepared.offer.swapPkScript)
634
+ );
635
+ }
636
+ }
637
+ return txid;
638
+ }
639
+ var OFFER_COVENANT_MISMATCH = "rebuilt covenant does not match the offer's swapPkScript";
640
+ var NoSpendableDepositError = class extends Error {
641
+ name = "NoSpendableDepositError";
642
+ constructor(options) {
643
+ super("no spendable VTXO at the swap address", options);
644
+ }
645
+ };
646
+ var OfferCovenantMismatchError = class extends Error {
647
+ constructor(swapPkScript, options) {
648
+ super(
649
+ `${OFFER_COVENANT_MISMATCH} \u2014 the operator signing key pinned by the rebuild does not reproduce the funded covenant; the signing key has likely rotated since funding (pass swapAddress, the funded address, to pin the original key), or the record is corrupt`,
650
+ options
651
+ );
652
+ this.swapPkScript = swapPkScript;
653
+ }
654
+ swapPkScript;
655
+ name = "OfferCovenantMismatchError";
656
+ };
657
+ async function prepareOfferCancel(wallet, offerHex, opts) {
658
+ const { fundingTxid, swapAddress } = opts;
614
659
  const offer = decodeOffer(hex2.decode(offerHex));
615
- const contractManager = await wallet.getContractManager();
660
+ const [contractManager, info, reader, broadcaster] = await Promise.all([
661
+ wallet.getContractManager(),
662
+ wallet.getArkadeInfo({ requireLive: true }),
663
+ wallet.getArkadeReader(),
664
+ wallet.getArkadeBroadcaster()
665
+ ]);
616
666
  const client = await arkade.Arkade.connect({
617
- arkade: new RestArkProvider(arkServerUrl),
618
- indexer: new RestIndexerProvider(arkServerUrl),
667
+ // info the wallet already holds, plus its broadcast pair — the shape
668
+ // `ArkadeServerProvider` describes, without a second `/v1/info`
669
+ arkade: { getInfo: async () => info, ...broadcaster },
670
+ indexer: reader,
619
671
  identity: wallet.identity,
620
672
  // registered offers resolve their VTXOs from the contract repository
621
673
  // instead of a direct indexer query; the indexer above stays as the
@@ -629,9 +681,7 @@ async function cancelOffer(wallet, arkServerUrl, offerHex, opts) {
629
681
  const { program, args, keys } = swapProgramBinding(offer, operatorPubkey);
630
682
  const rebuilt = new arkade.ArkadeProgramScript(program, args, keys);
631
683
  if (hex2.encode(rebuilt.pkScript) !== hex2.encode(offer.swapPkScript)) {
632
- throw new Error(
633
- "rebuilt covenant does not match the offer's swapPkScript \u2014 the server signing key has likely rotated since funding; pass swapAddress (the funded address) to pin the original key"
634
- );
684
+ throw new OfferCovenantMismatchError(hex2.encode(offer.swapPkScript));
635
685
  }
636
686
  const contract = new arkade.ArkadeContract(client, program, args, keys);
637
687
  const [vtxos, makerAddress] = await Promise.all([contract.getUtxos(), wallet.getAddress()]);
@@ -641,7 +691,7 @@ async function cancelOffer(wallet, arkServerUrl, offerHex, opts) {
641
691
  );
642
692
  }
643
693
  const vtxo = fundingTxid ? vtxos.find((v) => v.txid === fundingTxid) : vtxos[0];
644
- if (!vtxo) throw new Error("no spendable VTXO at the swap address");
694
+ if (!vtxo) throw new NoSpendableDepositError();
645
695
  const makerPkScript = ArkAddress.decode(makerAddress).pkScript;
646
696
  const cancel = contract.functions.cancel().from({ txid: vtxo.txid, vout: vtxo.vout, value: vtxo.value }).to(makerPkScript, BigInt(vtxo.value));
647
697
  for (const a of vtxo.assets ?? []) {
@@ -651,20 +701,11 @@ async function cancelOffer(wallet, arkServerUrl, offerHex, opts) {
651
701
  outputs: [{ vout: 0, amount: BigInt(a.amount) }]
652
702
  });
653
703
  }
654
- const swapId = fundingTxid ?? vtxo.txid;
655
- const hasLocalRecord = (await getAssetSwapsOrThrow(repository)).some((s) => s.id === swapId);
656
- if (hasLocalRecord) await updateAssetSwap(repository, swapId, { status: "cancelling" });
657
- const { txid } = await cancel.send();
658
- if (hasLocalRecord) {
659
- const { persisted, swaps } = await updateAssetSwapBestEffort(repository, swapId, {
660
- status: "cancelled",
661
- spentTxid: txid
662
- });
663
- if (persisted) {
664
- await retireOfferContract(contractManager, swaps, hex2.encode(offer.swapPkScript));
665
- }
666
- }
667
- return txid;
704
+ return {
705
+ offer,
706
+ vtxo: { txid: vtxo.txid, vout: vtxo.vout, value: vtxo.value },
707
+ send: async () => (await cancel.send()).txid
708
+ };
668
709
  }
669
710
 
670
711
  // src/markets.ts
@@ -803,9 +844,10 @@ import {
803
844
  promisifyRequest
804
845
  } from "@arkade-os/sdk";
805
846
  var DEFAULT_DB_NAME = "arkade-intents";
806
- var DB_VERSION = 2;
847
+ var DB_VERSION = 3;
807
848
  var STORE_SWAPS = "swaps";
808
849
  var STORE_RFQ_SWAPS = "rfqSwaps";
850
+ var STORE_SWAP_RECORDS = "swapRecords";
809
851
  var STORE_SCANNED = "scannedTxids";
810
852
  var STORE_MARKETS = "markets";
811
853
  var STORES = [
@@ -813,6 +855,11 @@ var STORES = [
813
855
  // v2. Separate from `swaps` rather than sharing it: the two record types
814
856
  // have different keys and no consumer wants them interleaved.
815
857
  [STORE_RFQ_SWAPS, { keyPath: "rfqId" }],
858
+ // v3. The v2 client's accept records, keyed by quote id. Its own store for
859
+ // the same reason, plus one more: the v1 read path drops a row carrying
860
+ // neither `offerHex` nor `paymentHash`, so sharing `swaps` would pin the
861
+ // v2 shape to a v1 predicate.
862
+ [STORE_SWAP_RECORDS, { keyPath: "id" }],
816
863
  [STORE_SCANNED],
817
864
  [STORE_MARKETS]
818
865
  ];
@@ -824,7 +871,7 @@ function initDatabase(db, oldVersion, transaction) {
824
871
  }
825
872
  }
826
873
  var IndexedDbAssetSwapRepository = class {
827
- version = 4;
874
+ version = 5;
828
875
  connection;
829
876
  constructor(dbName = DEFAULT_DB_NAME) {
830
877
  this.connection = createManagedConnection(dbName, DB_VERSION, initDatabase);
@@ -868,6 +915,22 @@ var IndexedDbAssetSwapRepository = class {
868
915
  store.delete(rfqId);
869
916
  });
870
917
  }
918
+ async saveSwapRecord(record) {
919
+ await this.write(STORE_SWAP_RECORDS, (store) => {
920
+ store.put(record);
921
+ });
922
+ }
923
+ async getSwapRecord(id) {
924
+ return promisifyRequest((await this.readStore(STORE_SWAP_RECORDS)).get(id));
925
+ }
926
+ async getAllSwapRecords() {
927
+ return promisifyRequest((await this.readStore(STORE_SWAP_RECORDS)).getAll());
928
+ }
929
+ async removeSwapRecord(id) {
930
+ await this.write(STORE_SWAP_RECORDS, (store) => {
931
+ store.delete(id);
932
+ });
933
+ }
871
934
  async getScannedTxids() {
872
935
  const keys = await promisifyRequest((await this.readStore(STORE_SCANNED)).getAllKeys());
873
936
  return new Set(keys);
@@ -974,6 +1037,14 @@ var parseSigner = (value) => {
974
1037
  }
975
1038
  return { signingDescriptor };
976
1039
  };
1040
+ var parsePayoutAddress = (value, kind) => {
1041
+ if (typeof value !== "string" || value.length === 0) {
1042
+ throw new Error(
1043
+ `this ${kind} record's claim destination is unusable: expected an Arkade address, got ${JSON.stringify(value)}`
1044
+ );
1045
+ }
1046
+ return value;
1047
+ };
977
1048
  var parseHashlock = (value) => {
978
1049
  const raw = value ?? {};
979
1050
  const hashlock = {
@@ -1019,6 +1090,11 @@ var rfqClaimSecretOf = (record) => {
1019
1090
  );
1020
1091
  }
1021
1092
  };
1093
+ var rfqClaimDestinationOf = (record) => {
1094
+ const handler = rfqCorridorHandlers.getOrThrow(record.kind);
1095
+ if (!handler.claimDestination) return void 0;
1096
+ return parsePayoutAddress(handler.claimDestination(record.profile), record.kind);
1097
+ };
1022
1098
 
1023
1099
  // src/rfqCorridors.ts
1024
1100
  var LightningSendCorridor = {
@@ -1038,7 +1114,7 @@ var LightningReceiveCorridor = {
1038
1114
  const receive = swap;
1039
1115
  return {
1040
1116
  expectedAmount: receive.expectedAmount,
1041
- ...receive.claimArkTxid ? { claimArkTxid: receive.claimArkTxid } : {}
1117
+ ...receive.claimTxid ? { claimTxid: receive.claimTxid } : {}
1042
1118
  };
1043
1119
  },
1044
1120
  hydrate(profile) {
@@ -1048,13 +1124,16 @@ var LightningReceiveCorridor = {
1048
1124
  return {
1049
1125
  ...hydrateHashlock(profile),
1050
1126
  expectedAmount: profile.expectedAmount,
1051
- ...profile.claimArkTxid ? { claimArkTxid: profile.claimArkTxid } : {}
1127
+ ...profile.claimTxid ? { claimTxid: profile.claimTxid } : {}
1052
1128
  };
1053
1129
  },
1054
1130
  // We are the claimant here, so the preimage material on the hashlock is
1055
1131
  // ours to use.
1056
1132
  claimSecret: (profile) => ({ ...profile.signer, ...profile.hashlock }),
1057
- activityTxids: (profile) => profile.claimArkTxid ? [profile.claimArkTxid] : []
1133
+ // The other half of claiming: where it pays. Written once at request time
1134
+ // and left alone by `project` above, so this hands back what was stored.
1135
+ claimDestination: (profile) => profile.payoutAddress,
1136
+ activityTxids: (profile) => profile.claimTxid ? [profile.claimTxid] : []
1058
1137
  };
1059
1138
  function onchainSendProfile(result) {
1060
1139
  return {
@@ -1142,14 +1221,34 @@ var isRfqSwapTerminal = (state) => RFQ_SWAP_TERMINAL_STATES.includes(state);
1142
1221
 
1143
1222
  // src/rfqRecord.ts
1144
1223
  var RFQ_SWAP_RETENTION_SECONDS = 30 * 24 * 60 * 60;
1224
+ function renameLegacyClaimTxid(profile) {
1225
+ const { claimArkTxid, ...rest } = profile;
1226
+ return { ...rest, claimTxid: rest.claimTxid ?? claimArkTxid };
1227
+ }
1228
+ function normalizeRfqSwapRecord(record) {
1229
+ const { fundingArkTxid, refundArkTxid, lockupSpendArkTxids, ...rest } = record;
1230
+ const legacyClaim = record.kind === "lightning_receive" && record.profile.claimArkTxid !== void 0;
1231
+ if (!fundingArkTxid && !refundArkTxid && !lockupSpendArkTxids && !legacyClaim) return record;
1232
+ const fundingTxid = rest.fundingTxid ?? fundingArkTxid;
1233
+ const refundTxid = rest.refundTxid ?? refundArkTxid;
1234
+ const lockupSpendTxids = rest.lockupSpendTxids ?? lockupSpendArkTxids;
1235
+ return {
1236
+ ...rest,
1237
+ ...fundingTxid ? { fundingTxid } : {},
1238
+ ...refundTxid ? { refundTxid } : {},
1239
+ ...lockupSpendTxids?.length ? { lockupSpendTxids } : {},
1240
+ ...legacyClaim ? { profile: renameLegacyClaimTxid(record.profile) } : {}
1241
+ };
1242
+ }
1145
1243
  var managerState = (swap) => ({
1146
1244
  rfqId: swap.rfqId,
1147
1245
  state: swap.state,
1148
1246
  createdAt: swap.createdAt,
1149
1247
  updatedAt: swap.updatedAt,
1150
- ...swap.refundArkTxid ? { refundArkTxid: swap.refundArkTxid } : {},
1151
- ...swap.lockupSpendArkTxids?.length ? { lockupSpendArkTxids: [...swap.lockupSpendArkTxids] } : {},
1248
+ ...swap.refundTxid ? { refundTxid: swap.refundTxid } : {},
1249
+ ...swap.lockupSpendTxids?.length ? { lockupSpendTxids: [...swap.lockupSpendTxids] } : {},
1152
1250
  ...swap.failure ? { failure: swap.failure } : {},
1251
+ ...swap.claimFailure ? { claimFailure: swap.claimFailure } : {},
1153
1252
  ...swap.blockedReason ? { blockedReason: swap.blockedReason } : {}
1154
1253
  });
1155
1254
  function assertSameSwap(origin, swap) {
@@ -1179,27 +1278,30 @@ function createRfqSwapRecord(origin, swap) {
1179
1278
  }
1180
1279
  function updateRfqSwapRecord(record, swap) {
1181
1280
  assertSameSwap(record, swap);
1281
+ const stored = normalizeRfqSwapRecord(record);
1182
1282
  const {
1183
- refundArkTxid: _refundArkTxid,
1184
- lockupSpendArkTxids: _lockupSpendArkTxids,
1283
+ refundTxid: _refundTxid,
1284
+ lockupSpendTxids: _lockupSpendTxids,
1185
1285
  failure: _failure,
1286
+ claimFailure: _claimFailure,
1186
1287
  blockedReason: _blockedReason,
1187
1288
  ...origin
1188
- } = record;
1189
- const handler = rfqCorridorHandlers.getOrThrow(record.kind);
1289
+ } = stored;
1290
+ const handler = rfqCorridorHandlers.getOrThrow(stored.kind);
1190
1291
  return {
1191
1292
  ...origin,
1192
1293
  ...managerState(swap),
1193
- profile: { ...record.profile, ...handler.project(swap) }
1294
+ profile: { ...stored.profile, ...handler.project(swap) }
1194
1295
  };
1195
1296
  }
1196
1297
  function rfqSwapOriginOf(record) {
1298
+ const stored = normalizeRfqSwapRecord(record);
1197
1299
  return {
1198
- kind: record.kind,
1199
- lockupAddress: record.lockupAddress,
1200
- profile: { ...record.profile },
1201
- ...record.amount !== void 0 ? { amount: record.amount } : {},
1202
- ...record.fundingArkTxid ? { fundingArkTxid: record.fundingArkTxid } : {}
1300
+ kind: stored.kind,
1301
+ lockupAddress: stored.lockupAddress,
1302
+ profile: { ...stored.profile },
1303
+ ...stored.amount !== void 0 ? { amount: stored.amount } : {},
1304
+ ...stored.fundingTxid ? { fundingTxid: stored.fundingTxid } : {}
1203
1305
  };
1204
1306
  }
1205
1307
  function lockupScript(params, lockupAddress) {
@@ -1213,27 +1315,29 @@ function lockupScript(params, lockupAddress) {
1213
1315
  return script;
1214
1316
  }
1215
1317
  function rebuildRfqSwap(record, params) {
1216
- const script = lockupScript(params, record.lockupAddress);
1318
+ const stored = normalizeRfqSwapRecord(record);
1319
+ const script = lockupScript(params, stored.lockupAddress);
1217
1320
  const common = {
1218
- rfqId: record.rfqId,
1219
- state: record.state,
1321
+ rfqId: stored.rfqId,
1322
+ state: stored.state,
1220
1323
  lockupPkScript: script.pkScript,
1221
- lockup: { script, address: record.lockupAddress },
1324
+ lockup: { script, address: stored.lockupAddress },
1222
1325
  // From the covenant, which binds it: the record's own copy would be a
1223
1326
  // second source for the deadline the refund is gated on.
1224
1327
  refundLocktime: Number(script.options.refundLocktime),
1225
- createdAt: record.createdAt,
1226
- updatedAt: record.updatedAt,
1227
- ...record.refundArkTxid ? { refundArkTxid: record.refundArkTxid } : {},
1228
- ...record.lockupSpendArkTxids?.length ? { lockupSpendArkTxids: [...record.lockupSpendArkTxids] } : {},
1229
- ...record.failure ? { failure: record.failure } : {},
1230
- ...record.blockedReason ? { blockedReason: record.blockedReason } : {}
1328
+ createdAt: stored.createdAt,
1329
+ updatedAt: stored.updatedAt,
1330
+ ...stored.refundTxid ? { refundTxid: stored.refundTxid } : {},
1331
+ ...stored.lockupSpendTxids?.length ? { lockupSpendTxids: [...stored.lockupSpendTxids] } : {},
1332
+ ...stored.failure ? { failure: stored.failure } : {},
1333
+ ...stored.claimFailure ? { claimFailure: stored.claimFailure } : {},
1334
+ ...stored.blockedReason ? { blockedReason: stored.blockedReason } : {}
1231
1335
  };
1232
- const handler = rfqCorridorHandlers.getOrThrow(record.kind);
1336
+ const handler = rfqCorridorHandlers.getOrThrow(stored.kind);
1233
1337
  return {
1234
1338
  ...common,
1235
- kind: record.kind,
1236
- ...handler.hydrate(record.profile, { lockup: script })
1339
+ kind: stored.kind,
1340
+ ...handler.hydrate(stored.profile, { lockup: script })
1237
1341
  };
1238
1342
  }
1239
1343
  function shouldRetainRfqSwap(record, now) {
@@ -1274,10 +1378,10 @@ async function fetchParsedTxs(indexer, txids) {
1274
1378
  var unscannedSwapCandidates = (txs, existingIds, scanned) => txs.filter(
1275
1379
  (tx) => tx.type === "sent" && tx.redeemTxid && !existingIds.has(tx.redeemTxid) && !scanned.has(tx.redeemTxid)
1276
1380
  );
1277
- function classifySpend(offer, serverPubkey, spendTx, deposit) {
1381
+ function classifySpend(offer, operatorPubkey, spendTx, deposit) {
1278
1382
  let leaves;
1279
1383
  try {
1280
- const script = offerVtxoScript(offer, serverPubkey);
1384
+ const script = offerContract(offer, operatorPubkey);
1281
1385
  if (hex5.encode(script.pkScript) !== hex5.encode(offer.swapPkScript)) return "indeterminate";
1282
1386
  leaves = {
1283
1387
  // both routes that hand the deposit back; `exit` is absent on an
@@ -1301,15 +1405,15 @@ function classifySpend(offer, serverPubkey, spendTx, deposit) {
1301
1405
  return "indeterminate";
1302
1406
  }
1303
1407
  var spendTxidsOf = (vtxo) => [vtxo.spentBy, vtxo.arkTxId].filter((id) => Boolean(id));
1304
- function classifyDepositSpend(offer, serverPubkey, spendTxs, deposit) {
1408
+ function classifyDepositSpend(offer, operatorPubkey, spendTxs, deposit) {
1305
1409
  for (const tx of spendTxs) {
1306
- const kind = classifySpend(offer, serverPubkey, tx, deposit);
1410
+ const kind = classifySpend(offer, operatorPubkey, tx, deposit);
1307
1411
  if (kind !== "indeterminate") return kind;
1308
1412
  }
1309
1413
  return "indeterminate";
1310
1414
  }
1311
1415
  async function restoreAssetSwaps(indexer, txs, existingIds, opts) {
1312
- const { serverPubkey, scanned = /* @__PURE__ */ new Set() } = opts;
1416
+ const { operatorPubkey, scanned = /* @__PURE__ */ new Set() } = opts;
1313
1417
  const candidates = unscannedSwapCandidates(txs, existingIds, scanned);
1314
1418
  if (candidates.length === 0) return { restored: [], scannedTxids: [] };
1315
1419
  const byTxid = new Map(candidates.map((tx) => [tx.redeemTxid, tx]));
@@ -1378,7 +1482,7 @@ async function restoreAssetSwaps(indexer, txs, existingIds, opts) {
1378
1482
  if (state === "swept") status = "recoverable";
1379
1483
  else if (state === "spent") {
1380
1484
  const spendTxs = spendTxidsOf(vtxo).map((id) => spendTxByTxid.get(id)).filter((tx) => tx !== void 0);
1381
- const kind = classifyDepositSpend(offer, serverPubkey, spendTxs, {
1485
+ const kind = classifyDepositSpend(offer, operatorPubkey, spendTxs, {
1382
1486
  txid: vtxo.txid,
1383
1487
  vout: vtxo.vout
1384
1488
  });
@@ -1395,7 +1499,7 @@ async function restoreAssetSwaps(indexer, txs, existingIds, opts) {
1395
1499
  fromAmount,
1396
1500
  toAmount: offer.wantAmount.toString(),
1397
1501
  // ponytail(arkade-os/ts-sdk#680): empty address makes cancel fall back
1398
- // to the current server key; store the funded address if server-key
1502
+ // to the current operator key; store the funded address if operator-key
1399
1503
  // rotations become real (cancelOffer now at least diagnoses the
1400
1504
  // mismatch instead of reporting a missing VTXO)
1401
1505
  swapAddress: "",
@@ -1417,10 +1521,13 @@ async function restoreAssetSwaps(indexer, txs, existingIds, opts) {
1417
1521
  import { base64 as base642, hex as hex6 } from "@scure/base";
1418
1522
  import {
1419
1523
  ArkAddress as ArkAddress3,
1420
- RestIndexerProvider as RestIndexerProvider3,
1421
1524
  Transaction as Transaction2
1422
1525
  } from "@arkade-os/sdk";
1423
1526
  var TERMINAL = ["fulfilled", "cancelled", "recoverable"];
1527
+ var assetSwapSource = (repository) => ({
1528
+ list: () => getAssetSwaps(repository),
1529
+ apply: async (swap, changes) => updateAssetSwapBestEffort(repository, swap.id, changes)
1530
+ });
1424
1531
  function spendUpdate(swap, spend) {
1425
1532
  if (TERMINAL.includes(swap.status)) return void 0;
1426
1533
  if (spend.kind === "indeterminate") return void 0;
@@ -1434,13 +1541,20 @@ function spendUpdate(swap, spend) {
1434
1541
  }
1435
1542
  async function watchOfferSwaps({
1436
1543
  wallet,
1437
- arkServerUrl,
1438
1544
  repository,
1545
+ source,
1439
1546
  onUpdate
1440
1547
  }) {
1441
- const manager = await wallet.getContractManager();
1442
- const serverPubkey = ArkAddress3.decode(await wallet.getAddress()).serverPubKey;
1443
- const indexer = new RestIndexerProvider3(arkServerUrl);
1548
+ if (!source && !repository) {
1549
+ throw new Error("watchOfferSwaps needs either a record source or a repository");
1550
+ }
1551
+ const records = source ?? assetSwapSource(repository);
1552
+ const [manager, address, indexer] = await Promise.all([
1553
+ wallet.getContractManager(),
1554
+ wallet.getAddress(),
1555
+ wallet.getArkadeReader()
1556
+ ]);
1557
+ const operatorPubkey = ArkAddress3.decode(address).serverPubKey;
1444
1558
  let queue = Promise.resolve();
1445
1559
  const enqueue = (task) => {
1446
1560
  queue = queue.then(task).catch(() => {
@@ -1454,7 +1568,7 @@ async function watchOfferSwaps({
1454
1568
  const { txs } = await indexer.getVirtualTxs(candidates);
1455
1569
  return classifyDepositSpend(
1456
1570
  decodeOffer(hex6.decode(swap.offerHex)),
1457
- serverPubkey,
1571
+ operatorPubkey,
1458
1572
  txs.map((psbt) => Transaction2.fromPSBT(base642.decode(psbt))),
1459
1573
  { txid: vtxo.txid, vout: vtxo.vout }
1460
1574
  );
@@ -1467,20 +1581,16 @@ async function watchOfferSwaps({
1467
1581
  for (const vtxo of event.vtxos) {
1468
1582
  const spentTxid = vtxo.arkTxId || vtxo.spentBy;
1469
1583
  if (!spentTxid) continue;
1470
- const swap = (await getAssetSwaps(repository)).find(
1584
+ const swap = (await records.list()).find(
1471
1585
  (s) => s.fundingTxid === vtxo.txid && s.swapPkScript === event.contractScript
1472
1586
  );
1473
1587
  if (!swap) continue;
1474
1588
  const kind = await classify(swap, vtxo, spentTxid);
1475
1589
  const changes = spendUpdate(swap, { txid: spentTxid, kind, at: event.timestamp });
1476
1590
  if (!changes) continue;
1477
- const { persisted, swaps } = await updateAssetSwapBestEffort(
1478
- repository,
1479
- swap.id,
1480
- changes
1481
- );
1591
+ const { persisted, swaps } = await records.apply(swap, changes);
1482
1592
  if (!persisted) continue;
1483
- onUpdate?.({ ...swap, ...changes });
1593
+ onUpdate?.(swaps.find((s) => s.id === swap.id) ?? swap);
1484
1594
  if (changes.status && RETIRABLE.includes(changes.status)) {
1485
1595
  await retireOfferContract(manager, swaps, event.contractScript);
1486
1596
  }
@@ -1616,7 +1726,7 @@ function solverOnchainRail(deps) {
1616
1726
  }
1617
1727
  const negotiated = await deps.connect(
1618
1728
  rendezvous,
1619
- (transport) => requestOnchainSend(ctx.wallet, deps.arkServerUrl, transport, {
1729
+ (transport) => requestOnchainSend(ctx.wallet, transport, {
1620
1730
  amount,
1621
1731
  amountSide: "to",
1622
1732
  payoutPubkey: deps.payoutPubkey,
@@ -1729,7 +1839,7 @@ function solverLightningRail(deps) {
1729
1839
  }
1730
1840
  const negotiated = await deps.connect(
1731
1841
  rendezvous,
1732
- (transport) => requestLightningSend(ctx.wallet, deps.arkServerUrl, transport, {
1842
+ (transport) => requestLightningSend(ctx.wallet, transport, {
1733
1843
  invoice: facts,
1734
1844
  ...deps.emulatorPubkey ? { emulatorPubkey: deps.emulatorPubkey } : {}
1735
1845
  })
@@ -1782,6 +1892,440 @@ function solverLightningRail(deps) {
1782
1892
  };
1783
1893
  }
1784
1894
 
1895
+ // src/payment/lightning.ts
1896
+ import { invoiceTarget as invoiceTarget2 } from "@arkade-os/sdk";
1897
+
1898
+ // src/payment/swapRail.ts
1899
+ import { makeHandle as makeHandle3 } from "@arkade-os/sdk";
1900
+
1901
+ // src/client/errors.ts
1902
+ var AmbiguousDestination = class extends Error {
1903
+ constructor(destination, detail) {
1904
+ super(`cannot route ${JSON.stringify(destination)}: ${detail}`);
1905
+ this.destination = destination;
1906
+ }
1907
+ destination;
1908
+ name = "AmbiguousDestination";
1909
+ };
1910
+ var UnsupportedRoute = class extends Error {
1911
+ name = "UnsupportedRoute";
1912
+ give;
1913
+ take;
1914
+ constructor(detail, corridors = {}) {
1915
+ super(`unsupported route: ${detail}`);
1916
+ this.give = corridors.give;
1917
+ this.take = corridors.take;
1918
+ }
1919
+ };
1920
+ var DiscoverySnapshotUnavailable = class extends Error {
1921
+ constructor(network, detail) {
1922
+ super(`cannot resolve on ${network} offline: ${detail}`);
1923
+ this.network = network;
1924
+ }
1925
+ network;
1926
+ name = "DiscoverySnapshotUnavailable";
1927
+ };
1928
+ var AmountMismatch = class extends Error {
1929
+ constructor(sources) {
1930
+ super(`exactly one amount may be pinned; got ${sources[0]} and ${sources[1]}`);
1931
+ this.sources = sources;
1932
+ }
1933
+ sources;
1934
+ name = "AmountMismatch";
1935
+ };
1936
+ var AmountEncodingUnsupported = class extends Error {
1937
+ constructor(field, value, detail, options) {
1938
+ super(`${field}: ${detail}`, options);
1939
+ this.field = field;
1940
+ this.value = value;
1941
+ }
1942
+ field;
1943
+ value;
1944
+ name = "AmountEncodingUnsupported";
1945
+ };
1946
+ var QuoteVerificationFailed = class extends Error {
1947
+ constructor(check, expected, actual, options) {
1948
+ super(`quote failed the ${check} check \u2014 refusing to fund`, options);
1949
+ this.check = check;
1950
+ this.expected = expected;
1951
+ this.actual = actual;
1952
+ }
1953
+ check;
1954
+ name = "QuoteVerificationFailed";
1955
+ expected;
1956
+ actual;
1957
+ };
1958
+ var QuoteExpired = class extends Error {
1959
+ constructor(quoteId, expiresAt, now) {
1960
+ super(`quote ${quoteId} expired at ${expiresAt}, ${now - expiresAt} ago`);
1961
+ this.quoteId = quoteId;
1962
+ this.expiresAt = expiresAt;
1963
+ this.now = now;
1964
+ }
1965
+ quoteId;
1966
+ expiresAt;
1967
+ now;
1968
+ name = "QuoteExpired";
1969
+ };
1970
+ var MaxFeeExceeded = class extends Error {
1971
+ constructor(quoteId, asset3, fee, maxFee) {
1972
+ super(`fee ${fee} of ${asset3} exceeds the ${maxFee} ceiling`);
1973
+ this.quoteId = quoteId;
1974
+ this.asset = asset3;
1975
+ this.fee = fee;
1976
+ this.maxFee = maxFee;
1977
+ }
1978
+ quoteId;
1979
+ asset;
1980
+ fee;
1981
+ maxFee;
1982
+ name = "MaxFeeExceeded";
1983
+ };
1984
+ var InsufficientFunds = class extends Error {
1985
+ constructor(asset3, required, available) {
1986
+ super(
1987
+ `funding needs ${required} of ${asset3}` + (available === void 0 ? "" : `, wallet holds ${available}`)
1988
+ );
1989
+ this.asset = asset3;
1990
+ this.required = required;
1991
+ this.available = available;
1992
+ }
1993
+ asset;
1994
+ required;
1995
+ name = "InsufficientFunds";
1996
+ available;
1997
+ };
1998
+ var AcceptConflict = class extends Error {
1999
+ constructor(quoteId, swapId, fields) {
2000
+ super(`accept ${quoteId} conflicts with swap ${swapId} on ${fields.join(", ")}`);
2001
+ this.quoteId = quoteId;
2002
+ this.swapId = swapId;
2003
+ this.fields = fields;
2004
+ }
2005
+ quoteId;
2006
+ swapId;
2007
+ fields;
2008
+ name = "AcceptConflict";
2009
+ };
2010
+ var ClientDisposed = class extends Error {
2011
+ constructor(method) {
2012
+ super(`${method}() called after the client was disposed`);
2013
+ this.method = method;
2014
+ }
2015
+ method;
2016
+ name = "ClientDisposed";
2017
+ };
2018
+ var NotCancellable = class extends Error {
2019
+ constructor(swapId) {
2020
+ super(`swap ${swapId} is not a cancellable asset swap`);
2021
+ this.swapId = swapId;
2022
+ }
2023
+ swapId;
2024
+ name = "NotCancellable";
2025
+ };
2026
+ var InconsistentRoute = class extends Error {
2027
+ constructor(asset3, destination) {
2028
+ super(`destination ${JSON.stringify(destination)} is not on ${asset3}'s chain`);
2029
+ this.asset = asset3;
2030
+ this.destination = destination;
2031
+ }
2032
+ asset;
2033
+ destination;
2034
+ name = "InconsistentRoute";
2035
+ };
2036
+ var OperatorUnreachable = class extends Error {
2037
+ name = "OperatorUnreachable";
2038
+ constructor(detail, options) {
2039
+ super(`cannot derive a covenant: ${detail}`, options);
2040
+ }
2041
+ };
2042
+ var MissingCorridorDep = class extends Error {
2043
+ constructor(corridor, dep) {
2044
+ super(`the ${corridor} corridor has no ${dep}`);
2045
+ this.corridor = corridor;
2046
+ this.dep = dep;
2047
+ }
2048
+ corridor;
2049
+ dep;
2050
+ name = "MissingCorridorDep";
2051
+ };
2052
+ var SWAP_ERRORS = {
2053
+ AmbiguousDestination,
2054
+ UnsupportedRoute,
2055
+ DiscoverySnapshotUnavailable,
2056
+ AmountMismatch,
2057
+ AmountEncodingUnsupported,
2058
+ QuoteVerificationFailed,
2059
+ SwapRefusal,
2060
+ QuoteExpired,
2061
+ MaxFeeExceeded,
2062
+ InsufficientFunds,
2063
+ AcceptConflict,
2064
+ ClientDisposed,
2065
+ NotCancellable,
2066
+ InconsistentRoute,
2067
+ OperatorUnreachable,
2068
+ MissingCorridorDep
2069
+ };
2070
+ var SWAP_ERROR_NAMES = Object.keys(SWAP_ERRORS);
2071
+ function isSwapError(e, name) {
2072
+ if (!(e instanceof Error)) return false;
2073
+ const table = SWAP_ERRORS;
2074
+ const ctor = table[e.name];
2075
+ if (ctor === void 0 || !(e instanceof ctor)) return false;
2076
+ return name === void 0 || e.name === name;
2077
+ }
2078
+
2079
+ // src/client/sats.ts
2080
+ var satsOf = (value, field) => {
2081
+ if (value < 0n || value > BigInt(Number.MAX_SAFE_INTEGER)) {
2082
+ throw new AmountEncodingUnsupported(
2083
+ field,
2084
+ `${value}`,
2085
+ "outside the non-negative safe-integer window core's payment amounts are numbers in"
2086
+ );
2087
+ }
2088
+ return Number(value);
2089
+ };
2090
+
2091
+ // src/payment/status.ts
2092
+ var PAYMENT_STATUS = {
2093
+ /** Persisted, funding not broadcast. */
2094
+ accepted: "pending",
2095
+ funding: "pending",
2096
+ /** The lockup is funded. Nothing has emitted `"sent"` in core since the
2097
+ * `onchain-swap` rail left with `packages/boltz-swap`. */
2098
+ funded: "sent",
2099
+ /** Asset swaps only: an unfilled offer. */
2100
+ open: "pending",
2101
+ /** Asset swaps only. */
2102
+ filled: "settled",
2103
+ /** The trader's L1 claim on `arkade -> onchain`. */
2104
+ claimed: "settled",
2105
+ /** Terminal success on `arkade -> lightning`: the solver's hash-verified
2106
+ * spend IS the invoice being paid. */
2107
+ paid: "settled",
2108
+ cancelling: "pending",
2109
+ /** Value returned, and the router has no non-loss terminal to say so with. */
2110
+ cancelled: "failed",
2111
+ /** Terminal HERE, not at `refunded` — see the module docblock. */
2112
+ refunding: "failed",
2113
+ /** The trader's value came back. Reaches `onUpdate`, not the handle. */
2114
+ refunded: "failed",
2115
+ /** The solver reclaimed a receive-leg lockup: a loss, and not `refunded`. */
2116
+ lapsed: "failed",
2117
+ /** Never retried silently; `client.recover` drives it. */
2118
+ needs_recovery: "failed",
2119
+ failed: "failed"
2120
+ };
2121
+ var paymentStatusOf = (outcome) => PAYMENT_STATUS[outcome];
2122
+ var isTerminalStatus = (status) => status === "settled" || status === "failed";
2123
+
2124
+ // src/payment/swapRail.ts
2125
+ var SwapPaymentFailedError = class extends Error {
2126
+ constructor(railId, outcome, swap) {
2127
+ super(
2128
+ `${railId}: swap ${swap.id} ended ${outcome}` + (swap.failure === void 0 ? "" : `: ${swap.failure}`) + (swap.blockedReason === void 0 ? "" : ` (${swap.blockedReason})`)
2129
+ );
2130
+ this.railId = railId;
2131
+ this.outcome = outcome;
2132
+ this.swap = swap;
2133
+ }
2134
+ railId;
2135
+ outcome;
2136
+ swap;
2137
+ name = "SwapPaymentFailedError";
2138
+ };
2139
+ var unroutable = (error) => isSwapError(error, "UnsupportedRoute") || isSwapError(error, "AmbiguousDestination");
2140
+ var railAvailable = async (client, input) => {
2141
+ try {
2142
+ return (await client.resolve(input)).eligible > 0;
2143
+ } catch (error) {
2144
+ if (unroutable(error)) return false;
2145
+ throw error;
2146
+ }
2147
+ };
2148
+ var receiverExact = (railId, parts) => {
2149
+ if (parts.total !== parts.amount + parts.fee) {
2150
+ throw new Error(
2151
+ `${railId}: ${parts.total} leaving the wallet is not ${parts.amount} delivered plus ${parts.fee} in fees \u2014 the quote is not receiver-exact`
2152
+ );
2153
+ }
2154
+ return {
2155
+ amount: satsOf(parts.amount, `${railId}.amount`),
2156
+ fee: satsOf(parts.fee, `${railId}.fee`),
2157
+ total: satsOf(parts.total, `${railId}.total`)
2158
+ };
2159
+ };
2160
+ var resultOf = (railId, swap) => ({
2161
+ railId,
2162
+ // The tagged public form M6 minted — round-trippable to `client.swaps()`,
2163
+ // which is where the outcomes past this handle's terminal one are read.
2164
+ swapId: swap.id,
2165
+ // The transaction that left THIS wallet, which is what `txid` means on
2166
+ // every other rail. A swap's later legs — the solver's fill, the trader's
2167
+ // L1 claim — are the client's to report, not the handle's.
2168
+ ...swap.fundingTxid === void 0 ? {} : { txid: swap.fundingTxid }
2169
+ });
2170
+ var swapHandle = async (railId, client, quote) => makeHandle3(railId, async (emit) => {
2171
+ const accepted = await client.accept(quote);
2172
+ return await new Promise((resolve, reject) => {
2173
+ let finished = false;
2174
+ let unsubscribe;
2175
+ const listener = (update) => {
2176
+ if (finished || update.swap.id !== accepted.id) return;
2177
+ const status = paymentStatusOf(update.outcome);
2178
+ const result = resultOf(railId, update.swap);
2179
+ if (!isTerminalStatus(status)) {
2180
+ emit({ status, result });
2181
+ return;
2182
+ }
2183
+ finished = true;
2184
+ unsubscribe?.();
2185
+ if (status === "settled") {
2186
+ emit({ status, result });
2187
+ resolve(result);
2188
+ return;
2189
+ }
2190
+ reject(new SwapPaymentFailedError(railId, update.outcome, update.swap));
2191
+ };
2192
+ unsubscribe = client.onUpdate(listener);
2193
+ if (finished) unsubscribe();
2194
+ });
2195
+ });
2196
+
2197
+ // src/payment/lightning.ts
2198
+ var LIGHTNING_RAIL = "lightning";
2199
+ var inputFor = (invoice, amount) => ({
2200
+ to: invoice,
2201
+ ...amount === void 0 ? {} : { amount: BigInt(amount), amountOn: "take" }
2202
+ });
2203
+ function lightningRail(client) {
2204
+ return {
2205
+ id: LIGHTNING_RAIL,
2206
+ // Amount-blind and non-throwing: classification only, which is what
2207
+ // keeps `match` from being a second gate on top of `available`.
2208
+ match: (req) => invoiceTarget2(req.raw) !== void 0,
2209
+ available: async (req) => {
2210
+ const invoice = invoiceTarget2(req.raw);
2211
+ if (invoice === void 0) return false;
2212
+ return railAvailable(client, inputFor(invoice, req.amount));
2213
+ },
2214
+ quote: async (req) => {
2215
+ const invoice = invoiceTarget2(req.raw);
2216
+ if (invoice === void 0) {
2217
+ throw new Error(`${LIGHTNING_RAIL}: the request carries no BOLT11 invoice`);
2218
+ }
2219
+ const quote = await client.quote(inputFor(invoice, req.amount));
2220
+ const amounts = receiverExact(LIGHTNING_RAIL, {
2221
+ amount: quote.take.amount,
2222
+ fee: quote.fee.amount,
2223
+ total: quote.give.amount
2224
+ });
2225
+ return {
2226
+ railId: LIGHTNING_RAIL,
2227
+ ...amounts,
2228
+ meta: {
2229
+ quoteId: quote.id,
2230
+ // A `RouteQuote` carries no expiry of its own, so a held one
2231
+ // can outlive the terms behind it. This is where a caller
2232
+ // can see that; `send()` refuses with `QuoteExpired` rather
2233
+ // than silently re-quoting.
2234
+ expiresAt: quote.expiresAt,
2235
+ ...quote.refundLocktime === void 0 ? {} : { refundLocktime: quote.refundLocktime },
2236
+ ...quote.solver === void 0 ? {} : { solver: quote.solver },
2237
+ ...quote.lock === void 0 ? {} : { paymentHash: quote.lock.hash },
2238
+ market: quote.market.key
2239
+ },
2240
+ send: () => swapHandle(LIGHTNING_RAIL, client, quote)
2241
+ };
2242
+ }
2243
+ };
2244
+ }
2245
+
2246
+ // src/payment/onchainSwap.ts
2247
+ import { btcTarget as btcTarget2, resolveSendAmount as resolveSendAmount2, tryResolveSendAmount as tryResolveSendAmount2 } from "@arkade-os/sdk";
2248
+ var ONCHAIN_SWAP_RAIL = "onchain-swap";
2249
+ var claimFeeSats = (deps) => BigInt(Math.ceil((deps.claimVsize ?? ONCHAIN_CLAIM_VSIZE) * deps.claimFeeRateSatVb));
2250
+ function onchainSwapRail(client, deps) {
2251
+ if (!Number.isFinite(deps.claimFeeRateSatVb) || deps.claimFeeRateSatVb <= 0) {
2252
+ throw new Error(
2253
+ `${ONCHAIN_SWAP_RAIL}: claimFeeRateSatVb must be positive, got ${deps.claimFeeRateSatVb}`
2254
+ );
2255
+ }
2256
+ const claimFee = claimFeeSats(deps);
2257
+ const inputFor2 = (address, amount) => ({
2258
+ to: address,
2259
+ amount: BigInt(amount) + claimFee,
2260
+ amountOn: "take"
2261
+ });
2262
+ return {
2263
+ id: ONCHAIN_SWAP_RAIL,
2264
+ match: (req) => btcTarget2(req.raw) !== void 0,
2265
+ available: async (req) => {
2266
+ const address = btcTarget2(req.raw);
2267
+ if (address === void 0) return false;
2268
+ const amount = tryResolveSendAmount2(req.raw, req.amount);
2269
+ if (amount === void 0) return false;
2270
+ return railAvailable(client, inputFor2(address, amount));
2271
+ },
2272
+ quote: async (req) => {
2273
+ const address = btcTarget2(req.raw);
2274
+ if (address === void 0) {
2275
+ throw new Error(`${ONCHAIN_SWAP_RAIL}: the request carries no bitcoin address`);
2276
+ }
2277
+ const amount = resolveSendAmount2(ONCHAIN_SWAP_RAIL, req.raw, req.amount);
2278
+ const quote = await client.quote(inputFor2(address, amount));
2279
+ const amounts = receiverExact(ONCHAIN_SWAP_RAIL, {
2280
+ // What lands at the address, which is the HTLC the solver locks
2281
+ // minus the claim this wallet will broadcast to take it.
2282
+ amount: quote.take.amount - claimFee,
2283
+ // Both halves of the cost, inside one number: the solver's
2284
+ // spread, and the claim the trader pays for out of the payout.
2285
+ fee: quote.fee.amount + claimFee,
2286
+ total: quote.give.amount
2287
+ });
2288
+ return {
2289
+ railId: ONCHAIN_SWAP_RAIL,
2290
+ ...amounts,
2291
+ meta: {
2292
+ quoteId: quote.id,
2293
+ expiresAt: quote.expiresAt,
2294
+ ...quote.refundLocktime === void 0 ? {} : { refundLocktime: quote.refundLocktime },
2295
+ ...quote.solver === void 0 ? {} : { solver: quote.solver },
2296
+ ...quote.lock === void 0 ? {} : { paymentHash: quote.lock.hash },
2297
+ market: quote.market.key,
2298
+ /** The estimate folded into `fee`, so a caller can see it. */
2299
+ claimFeeSats: Number(claimFee),
2300
+ /** What the solver locks on L1, before the claim's fee. */
2301
+ htlcAmountSats: Number(quote.take.amount)
2302
+ },
2303
+ send: () => swapHandle(ONCHAIN_SWAP_RAIL, client, quote)
2304
+ };
2305
+ }
2306
+ };
2307
+ }
2308
+
2309
+ // src/payment/router.ts
2310
+ import { PaymentRouter as Router, arkRail, onchainRail } from "@arkade-os/sdk";
2311
+ var SWAP_ROUTER_PRIORITY = [
2312
+ "ark",
2313
+ LIGHTNING_RAIL,
2314
+ ONCHAIN_SWAP_RAIL,
2315
+ "onchain"
2316
+ ];
2317
+ function createSwapPaymentRouter(wallet, client, config) {
2318
+ return new Router({
2319
+ wallet,
2320
+ prefs: { priority: [...SWAP_ROUTER_PRIORITY], ...config.prefs }
2321
+ }).use(arkRail()).use(onchainRail()).use(lightningRail(client)).use(
2322
+ onchainSwapRail(client, {
2323
+ claimFeeRateSatVb: config.claimFeeRateSatVb,
2324
+ ...config.claimVsize === void 0 ? {} : { claimVsize: config.claimVsize }
2325
+ })
2326
+ );
2327
+ }
2328
+
1785
2329
  // src/claim.ts
1786
2330
  import { hex as hex10 } from "@scure/base";
1787
2331
  import { ripemd160 } from "@noble/hashes/legacy.js";
@@ -1824,6 +2368,15 @@ async function awaitRfqResolution(transport, rfqId, options = {}) {
1824
2368
  await sleep(pollMs);
1825
2369
  }
1826
2370
  }
2371
+ var walletOperator = (wallet) => {
2372
+ let broadcaster;
2373
+ const broadcasting = () => broadcaster ??= wallet.getArkadeBroadcaster();
2374
+ return {
2375
+ getInfo: () => wallet.getArkadeInfo({ requireLive: true }),
2376
+ submitTx: async (...args) => (await broadcasting()).submitTx(...args),
2377
+ finalizeTx: async (...args) => (await broadcasting()).finalizeTx(...args)
2378
+ };
2379
+ };
1827
2380
  var LockupNeedsRecoveryError = class extends Error {
1828
2381
  name = "LockupNeedsRecoveryError";
1829
2382
  reason = "needs_recovery";
@@ -1835,11 +2388,10 @@ var LockupNeedsRecoveryError = class extends Error {
1835
2388
  * into one settlement with no CLTV awareness, so an early attempt can fail
1836
2389
  * the whole batch — including unrelated outputs that were otherwise fine.
1837
2390
  *
1838
- * Exposed as a value, not only inside the message, so a caller can encode
1839
- * `packages/boltz-swap`'s "pre-CLTV recoverable → skipped" rule without
1840
- * parsing prose. Seconds-based locktimes mature against the chain tip's
1841
- * timestamp rather than wall clock, so treat this as a floor to wait past,
1842
- * not an exact alarm.
2391
+ * Exposed as a value, not only inside the message, so a caller can skip
2392
+ * pre-CLTV recoverable outputs without parsing prose. Seconds-based
2393
+ * locktimes mature against the chain tip's timestamp rather than wall
2394
+ * clock, so treat this as a floor to wait past, not an exact alarm.
1843
2395
  */
1844
2396
  recoverableAfter;
1845
2397
  constructor(outpoints, recoverableAfter) {
@@ -1900,7 +2452,7 @@ async function readLockupFate(indexer, input) {
1900
2452
  if (vtxo.spentBy)
1901
2453
  spentBy.set(vtxo.spentBy, {
1902
2454
  checkpointTxid: vtxo.spentBy,
1903
- arkTxid: vtxo.arkTxId
2455
+ txid: vtxo.arkTxId
1904
2456
  });
1905
2457
  else everySpendNamed = false;
1906
2458
  }
@@ -1929,32 +2481,32 @@ async function readLockupFate(indexer, input) {
1929
2481
  }
1930
2482
  return everySpendNamed && observed.size === spentBy.size ? { fate: "returned", spends } : { fate: "unknown" };
1931
2483
  }
1932
- async function pushRefundWithoutReceiver(ark, input) {
2484
+ async function pushRefundWithoutReceiver(operator, input) {
1933
2485
  if (input.vtxos.length === 0) throw new Error("nothing to refund: no funded outputs");
1934
2486
  const swept = input.vtxos.filter((vtxo) => vtxo.recoverable);
1935
2487
  if (swept.length > 0) {
1936
2488
  throw new LockupNeedsRecoveryError(
1937
2489
  swept.map((vtxo) => `${vtxo.txid}:${vtxo.vout}`),
1938
- input.script.options.refundLocktime
2490
+ input.contract.options.refundLocktime
1939
2491
  );
1940
2492
  }
1941
- const refundPkScript = input.refundPkScript ?? input.script.options.nonInteractiveParameters?.senderPkScript;
2493
+ const refundPkScript = input.refundPkScript ?? input.contract.options.nonInteractiveParameters?.senderPkScript;
1942
2494
  if (!refundPkScript) {
1943
2495
  throw new Error(
1944
2496
  "no refund destination: the contract carries no emulator covenant suite, so pass refundPkScript explicitly"
1945
2497
  );
1946
2498
  }
1947
- const info = await ark.getInfo();
1948
- let serverUnrollScript;
2499
+ const info = await operator.getInfo();
2500
+ let operatorUnrollScript;
1949
2501
  try {
1950
- serverUnrollScript = CSVMultisigTapscript.decode(hex9.decode(info.checkpointTapscript));
2502
+ operatorUnrollScript = CSVMultisigTapscript.decode(hex9.decode(info.checkpointTapscript));
1951
2503
  } catch {
1952
- throw new Error("invalid checkpointTapscript from the Arkade server");
2504
+ throw new Error("invalid checkpointTapscript from the operator");
1953
2505
  }
1954
- const leaf = input.script.refundWithoutReceiver();
1955
- const tapTree = input.script.encode();
2506
+ const leaf = input.contract.refundWithoutReceiver();
2507
+ const tapTree = input.contract.encode();
1956
2508
  const amount = input.vtxos.reduce((sum, vtxo) => sum + vtxo.value, 0);
1957
- const { arkTx, checkpoints } = buildOffchainTx(
2509
+ const { arkTx: tx, checkpoints } = buildOffchainTx(
1958
2510
  input.vtxos.map((vtxo) => ({
1959
2511
  txid: vtxo.txid,
1960
2512
  vout: vtxo.vout,
@@ -1963,14 +2515,14 @@ async function pushRefundWithoutReceiver(ark, input) {
1963
2515
  tapTree
1964
2516
  })),
1965
2517
  [{ script: refundPkScript, amount: BigInt(amount) }],
1966
- serverUnrollScript
2518
+ operatorUnrollScript
1967
2519
  );
1968
- const signedArkTx = await input.sender.sign(arkTx);
1969
- const submitted = await ark.submitTx(
1970
- base643.encode(signedArkTx.toPSBT()),
2520
+ const signedTx = await input.sender.sign(tx);
2521
+ const submitted = await operator.submitTx(
2522
+ base643.encode(signedTx.toPSBT()),
1971
2523
  checkpoints.map((c) => base643.encode(c.toPSBT()))
1972
2524
  );
1973
- assertSubmittedArkTxid(submitted, signedArkTx, "refundWithoutReceiver");
2525
+ assertSubmittedArkTxid(submitted, signedTx, "refundWithoutReceiver");
1974
2526
  const matched = matchServerCheckpoints(
1975
2527
  submitted.signedCheckpointTxs,
1976
2528
  checkpoints,
@@ -1981,11 +2533,11 @@ async function pushRefundWithoutReceiver(ark, input) {
1981
2533
  async ({ server }) => base643.encode((await input.sender.sign(server, [0])).toPSBT())
1982
2534
  )
1983
2535
  );
1984
- await ark.finalizeTx(submitted.arkTxid, finalCheckpoints);
1985
- return { arkTxid: submitted.arkTxid, amount };
2536
+ await operator.finalizeTx(submitted.arkTxid, finalCheckpoints);
2537
+ return { txid: submitted.arkTxid, amount };
1986
2538
  }
1987
2539
  var REFUND_MTP_LAG_SECONDS = 2 * 60 * 60;
1988
- async function refundIfUnresolved(transport, ark, indexer, input) {
2540
+ async function refundIfUnresolved(transport, operator, indexer, input) {
1989
2541
  const pollMs = input.pollMs ?? 5e3;
1990
2542
  const now = input.now ?? (() => Math.floor(Date.now() / 1e3));
1991
2543
  const attemptDeadline = input.attemptDeadline ?? input.refundLocktime + REFUND_MTP_LAG_SECONDS;
@@ -1996,7 +2548,7 @@ async function refundIfUnresolved(transport, ark, indexer, input) {
1996
2548
  let fate = { fate: "unknown" };
1997
2549
  try {
1998
2550
  fate = await readLockupFate(indexer, {
1999
- swapPkScript: input.script.pkScript,
2551
+ swapPkScript: input.contract.pkScript,
2000
2552
  paymentHash: input.paymentHash
2001
2553
  });
2002
2554
  } catch {
@@ -2008,11 +2560,11 @@ async function refundIfUnresolved(transport, ark, indexer, input) {
2008
2560
  status
2009
2561
  };
2010
2562
  }
2011
- const vtxos = await findLockupVtxos(indexer, input.script.pkScript);
2563
+ const vtxos = await findLockupVtxos(indexer, input.contract.pkScript);
2012
2564
  if (vtxos.length === 0) return { outcome: "nothing_to_refund", status };
2013
2565
  try {
2014
- const pushed = await pushRefundWithoutReceiver(ark, {
2015
- script: input.script,
2566
+ const pushed = await pushRefundWithoutReceiver(operator, {
2567
+ contract: input.contract,
2016
2568
  sender: input.sender,
2017
2569
  vtxos,
2018
2570
  refundPkScript: input.refundPkScript
@@ -2055,13 +2607,13 @@ var assertFiniteAmount = (value, reason, label) => {
2055
2607
  error.reason = reason;
2056
2608
  throw error;
2057
2609
  };
2058
- async function pushClaim(ark, input) {
2610
+ async function pushClaim(operator, input) {
2059
2611
  if (input.vtxos.length === 0) throw new Error("nothing to claim: no funded outputs");
2060
2612
  const swept = input.vtxos.filter((vtxo) => vtxo.recoverable);
2061
2613
  if (swept.length > 0) {
2062
2614
  throw new LockupNeedsRecoveryError(
2063
2615
  swept.map((vtxo) => `${vtxo.txid}:${vtxo.vout}`),
2064
- input.script.options.refundLocktime
2616
+ input.contract.options.refundLocktime
2065
2617
  );
2066
2618
  }
2067
2619
  const locked = input.vtxos.reduce((sum, vtxo) => sum + vtxo.value, 0);
@@ -2072,22 +2624,22 @@ async function pushClaim(ark, input) {
2072
2624
  throw new LockupAmountMismatchError(input.expectedAmount, locked);
2073
2625
  }
2074
2626
  }
2075
- const committed = input.script.options.preimageHash;
2627
+ const committed = input.contract.options.preimageHash;
2076
2628
  if (hex10.encode(ripemd160(sha2563(input.preimage))) !== hex10.encode(committed)) {
2077
2629
  throw new Error("preimage does not match the covenant's payment hash");
2078
2630
  }
2079
- const info = await ark.getInfo();
2080
- let serverUnrollScript;
2631
+ const info = await operator.getInfo();
2632
+ let operatorUnrollScript;
2081
2633
  try {
2082
- serverUnrollScript = CSVMultisigTapscript2.decode(hex10.decode(info.checkpointTapscript));
2634
+ operatorUnrollScript = CSVMultisigTapscript2.decode(hex10.decode(info.checkpointTapscript));
2083
2635
  } catch {
2084
- throw new Error("invalid checkpointTapscript from the Arkade server");
2636
+ throw new Error("invalid checkpointTapscript from the operator");
2085
2637
  }
2086
- const leaf = input.script.claim();
2087
- const tapTree = input.script.encode();
2088
- const arkTxid = await signAndSubmitOffchainTx({
2638
+ const leaf = input.contract.claim();
2639
+ const tapTree = input.contract.encode();
2640
+ const txid = await signAndSubmitOffchainTx({
2089
2641
  identity: claimWithPreimageIdentity(input.receiver, input.preimage),
2090
- provider: ark,
2642
+ provider: operator,
2091
2643
  inputs: input.vtxos.map((vtxo) => ({
2092
2644
  txid: vtxo.txid,
2093
2645
  vout: vtxo.vout,
@@ -2098,10 +2650,10 @@ async function pushClaim(ark, input) {
2098
2650
  // One aggregate output: unlike the covenant refund, this leaf inspects
2099
2651
  // nothing about the output set.
2100
2652
  outputs: [{ script: input.destinationPkScript, amount: BigInt(locked) }],
2101
- serverUnrollScript,
2102
- verifyServerSignatures: { serverPubkey: input.script.options.server }
2653
+ serverUnrollScript: operatorUnrollScript,
2654
+ verifyServerSignatures: { serverPubkey: input.contract.options.server }
2103
2655
  });
2104
- return { arkTxid, amount: locked };
2656
+ return { txid, amount: locked };
2105
2657
  }
2106
2658
  async function awaitLockupFunding(indexer, swapPkScript, options = {}) {
2107
2659
  const pollMs = options.pollMs ?? 5e3;
@@ -2116,13 +2668,13 @@ async function awaitLockupFunding(indexer, swapPkScript, options = {}) {
2116
2668
  await sleep2(pollMs);
2117
2669
  }
2118
2670
  }
2119
- async function claimReceiveLockup(indexer, ark, input) {
2671
+ async function claimReceiveLockup(indexer, operator, input) {
2120
2672
  const vtxos = await awaitLockupFunding(indexer, input.swapPkScript, {
2121
2673
  pollMs: input.pollMs,
2122
2674
  deadline: input.deadline
2123
2675
  });
2124
- return pushClaim(ark, {
2125
- script: input.script,
2676
+ return pushClaim(operator, {
2677
+ contract: input.contract,
2126
2678
  receiver: input.receiver,
2127
2679
  preimage: input.preimage,
2128
2680
  vtxos,
@@ -2177,8 +2729,8 @@ async function senderIdentityForSwapRecord(wallet, record) {
2177
2729
  // src/arkadeRefunder.ts
2178
2730
  function arkadeRefunder(deps) {
2179
2731
  return async (swap) => {
2180
- const script = swap.lockup?.script;
2181
- if (!script) {
2732
+ const contract = swap.lockup?.script;
2733
+ if (!contract) {
2182
2734
  throw new Error(
2183
2735
  `swap ${swap.rfqId} carries no lockup covenant, so its refund cannot be built`
2184
2736
  );
@@ -2193,7 +2745,7 @@ function arkadeRefunder(deps) {
2193
2745
  );
2194
2746
  }
2195
2747
  const sender = await senderIdentityForSwapRecord(deps.wallet, rfqSignerOf(record) ?? {});
2196
- return pushRefundWithoutReceiver(deps.ark, { script, sender, vtxos });
2748
+ return pushRefundWithoutReceiver(deps.operator, { contract, sender, vtxos });
2197
2749
  };
2198
2750
  }
2199
2751
 
@@ -2214,6 +2766,14 @@ function nextOnchainAction(input) {
2214
2766
  return input.htlcLocktime - input.now >= ONCHAIN_CLAIM_MARGIN_SECONDS ? "claim" : "claim_window_closed";
2215
2767
  }
2216
2768
  }
2769
+ var RFQ_CONFIGURATION_REFUSAL = {
2770
+ noClaimLockupCallback: "no claimLockup callback is wired, so this wallet cannot claim the lockup",
2771
+ noCallbacksForClaim: "no callbacks are wired, so this wallet cannot claim the lockup",
2772
+ noClaimOnchainCallback: "no claimOnchain callback is wired, so this wallet cannot claim the L1 fill",
2773
+ autoActionsDisabled: "automatic actions are disabled, so this wallet will not push the refund",
2774
+ noCallbacksForRefund: "no callbacks are wired, so this wallet cannot push the refund"
2775
+ };
2776
+ var RFQ_CONFIGURATION_REFUSALS = Object.values(RFQ_CONFIGURATION_REFUSAL);
2217
2777
  var RfqSwapOriginRequired = class extends Error {
2218
2778
  /** The swap that could not be admitted. */
2219
2779
  rfqId;
@@ -2264,20 +2824,6 @@ var RfqSwapManager = class {
2264
2824
  * sign may well have been restored.
2265
2825
  */
2266
2826
  refundRefused = /* @__PURE__ */ new Set();
2267
- /**
2268
- * The last error a receive swap's claim callback threw, by rfqId.
2269
- *
2270
- * Kept only to tell two terminal outcomes apart once the claim window
2271
- * shuts: a swap whose claim was attempted and kept failing ends `failed`
2272
- * with that reason, while one that simply never became claimable ends
2273
- * `refunded`. Without it a broken claim callback would resolve a caller's
2274
- * {@link waitForSwapCompletion} as an ordinary unwind.
2275
- *
2276
- * Process-local, like {@link refundRefused}: after a restart the same swap
2277
- * ends `refunded` instead, which costs the caller a reason and nothing else
2278
- * — every throw was already reported through `onSwapFailed` as it happened.
2279
- */
2280
- lastClaimError = /* @__PURE__ */ new Map();
2281
2827
  /**
2282
2828
  * The lockup outpoints a receive swap's claim callback has already been
2283
2829
  * handed, by rfqId.
@@ -2589,6 +3135,19 @@ var RfqSwapManager = class {
2589
3135
  async getPendingSwaps() {
2590
3136
  return [...this.monitored.values()];
2591
3137
  }
3138
+ /**
3139
+ * Every swap this manager holds — {@link getPendingSwaps} plus the ones
3140
+ * that already ended. The two sets are disjoint: a swap leaves `monitored`
3141
+ * as it enters `finished`.
3142
+ *
3143
+ * The finished half is what this process has seen, which after
3144
+ * {@link restoreFromRepository} is the stored history minus what retention
3145
+ * pruned. A manager that has restored nothing answers with the live swaps
3146
+ * alone.
3147
+ */
3148
+ async getAllSwaps() {
3149
+ return [...this.monitored.values(), ...this.finished.values()];
3150
+ }
2592
3151
  async hasSwap(rfqId) {
2593
3152
  return this.monitored.has(rfqId);
2594
3153
  }
@@ -2658,7 +3217,6 @@ var RfqSwapManager = class {
2658
3217
  if (swap) this.byLockupScript.delete(hex11.encode(swap.lockupPkScript));
2659
3218
  this.monitored.delete(rfqId);
2660
3219
  this.refundRefused.delete(rfqId);
2661
- this.lastClaimError.delete(rfqId);
2662
3220
  this.claimedOutpoints.delete(rfqId);
2663
3221
  }
2664
3222
  /**
@@ -2867,14 +3425,14 @@ var RfqSwapManager = class {
2867
3425
  return this.claimIfFunded(swap, vtxos);
2868
3426
  }
2869
3427
  if (now < swap.refundLocktime + REFUND_MTP_LAG_SECONDS) {
2870
- if (swap.claimArkTxid) return;
3428
+ if (swap.claimTxid) return;
2871
3429
  return this.block(
2872
3430
  swap,
2873
3431
  "the claim window closed with the lockup unclaimed \u2014 only the solver can act now"
2874
3432
  );
2875
3433
  }
2876
- const failure = this.lastClaimError.get(swap.rfqId);
2877
- if (failure && !swap.claimArkTxid) {
3434
+ const failure = swap.claimFailure;
3435
+ if (failure && !swap.claimTxid) {
2878
3436
  return this.fail(swap, new Error(failure));
2879
3437
  }
2880
3438
  this.setState(swap, "refunded");
@@ -2889,7 +3447,7 @@ var RfqSwapManager = class {
2889
3447
  */
2890
3448
  async claimIfFunded(swap, vtxos) {
2891
3449
  if (vtxos.length === 0) return this.unblock(swap);
2892
- const partiallyClaimed = swap.claimArkTxid !== void 0;
3450
+ const partiallyClaimed = swap.claimTxid !== void 0;
2893
3451
  if (partiallyClaimed && !this.hasUnclaimedOutpoint(swap.rfqId, vtxos)) {
2894
3452
  return;
2895
3453
  }
@@ -2911,21 +3469,22 @@ var RfqSwapManager = class {
2911
3469
  if (!this.callbacks || !this.callbacks.claimLockup) {
2912
3470
  return this.block(
2913
3471
  swap,
2914
- this.callbacks ? "no claimLockup callback is wired, so this wallet cannot claim the lockup" : "no callbacks are wired, so this wallet cannot claim the lockup"
3472
+ this.callbacks ? RFQ_CONFIGURATION_REFUSAL.noClaimLockupCallback : RFQ_CONFIGURATION_REFUSAL.noCallbacksForClaim
2915
3473
  );
2916
3474
  }
2917
3475
  this.setState(swap, "claimable");
2918
3476
  if (!this.config.enableAutoActions) return;
2919
3477
  try {
2920
- const { arkTxid } = await this.callbacks.claimLockup(swap, vtxos, { partiallyClaimed });
2921
- this.lastClaimError.delete(swap.rfqId);
3478
+ const { txid } = await this.callbacks.claimLockup(swap, vtxos, { partiallyClaimed });
3479
+ delete swap.claimFailure;
2922
3480
  this.rememberClaimed(swap.rfqId, vtxos);
2923
- swap.claimArkTxid = arkTxid;
3481
+ swap.claimTxid = txid;
2924
3482
  this.touch(swap);
2925
3483
  this.setState(swap, "claimed");
2926
3484
  this.emitAction(swap, "claimLockup");
2927
3485
  } catch (error) {
2928
- this.lastClaimError.set(swap.rfqId, errorMessage(error));
3486
+ swap.claimFailure = errorMessage(error);
3487
+ this.touch(swap);
2929
3488
  this.emitFailed(swap, error);
2930
3489
  }
2931
3490
  }
@@ -2962,10 +3521,7 @@ var RfqSwapManager = class {
2962
3521
  if (action === "claim" && phase.phase === "claimable") {
2963
3522
  if (swap.claimTxid) return "continue";
2964
3523
  if (this.callbacks && !this.callbacks.claimOnchain) {
2965
- this.block(
2966
- swap,
2967
- "no claimOnchain callback is wired, so this wallet cannot claim the L1 fill"
2968
- );
3524
+ this.block(swap, RFQ_CONFIGURATION_REFUSAL.noClaimOnchainCallback);
2969
3525
  return "handled";
2970
3526
  }
2971
3527
  this.setOnchainState(swap, "claimable");
@@ -3005,14 +3561,14 @@ var RfqSwapManager = class {
3005
3561
  if (!this.config.enableAutoActions || !this.callbacks) {
3006
3562
  return this.block(
3007
3563
  swap,
3008
- this.callbacks ? "automatic actions are disabled, so this wallet will not push the refund" : "no callbacks are wired, so this wallet cannot push the refund"
3564
+ this.callbacks ? RFQ_CONFIGURATION_REFUSAL.autoActionsDisabled : RFQ_CONFIGURATION_REFUSAL.noCallbacksForRefund
3009
3565
  );
3010
3566
  }
3011
3567
  this.unblock(swap);
3012
3568
  try {
3013
3569
  const pushed = await this.callbacks.refundArkade(swap);
3014
3570
  if (pushed) {
3015
- swap.refundArkTxid = pushed.arkTxid;
3571
+ swap.refundTxid = pushed.txid;
3016
3572
  this.touch(swap);
3017
3573
  }
3018
3574
  this.setState(swap, "refunded");
@@ -3085,7 +3641,7 @@ var RfqSwapManager = class {
3085
3641
  /**
3086
3642
  * Record which ark transactions ended the lockup.
3087
3643
  *
3088
- * Only the ones the indexer actually named: `LockupSpend.arkTxid` is
3644
+ * Only the ones the indexer actually named: `LockupSpend.txid` is
3089
3645
  * optional, and a checkpoint txid is not what history correlates on — a
3090
3646
  * record carrying one would name a transaction the wallet's own activity
3091
3647
  * never shows. Fewer txids is the right failure here.
@@ -3095,9 +3651,9 @@ var RfqSwapManager = class {
3095
3651
  * verdict once.
3096
3652
  */
3097
3653
  stampLockupSpends(swap, spends) {
3098
- const arkTxids = spends.map((spend) => spend.arkTxid).filter((txid) => txid !== void 0);
3099
- if (arkTxids.length === 0) return;
3100
- swap.lockupSpendArkTxids = arkTxids;
3654
+ const txids = spends.map((spend) => spend.txid).filter((txid) => txid !== void 0);
3655
+ if (txids.length === 0) return;
3656
+ swap.lockupSpendTxids = txids;
3101
3657
  this.touch(swap);
3102
3658
  }
3103
3659
  touch(swap) {
@@ -3108,6 +3664,7 @@ var RfqSwapManager = class {
3108
3664
  if (swap.state === state) return;
3109
3665
  const previous = swap.state;
3110
3666
  if (previous === "needs_counterparty") delete swap.blockedReason;
3667
+ if (isRfqSwapTerminal(state)) delete swap.claimFailure;
3111
3668
  swap.state = state;
3112
3669
  this.touch(swap);
3113
3670
  notify(this.swapUpdateListeners, (listener) => listener(swap, previous));
@@ -3178,10 +3735,9 @@ var RfqSwapManager = class {
3178
3735
  /**
3179
3736
  * Drop a terminal swap from monitoring and report it exactly once.
3180
3737
  *
3181
- * `onSwapCompleted` and `onSwapFailed` are mutually exclusive here, unlike
3182
- * Boltz's manager, which fires completion for every swap that leaves
3183
- * monitoring including the failed ones — a listener named "completed" that
3184
- * also fires on failure is a trap worth not inheriting.
3738
+ * `onSwapCompleted` and `onSwapFailed` are mutually exclusive here: a
3739
+ * listener named "completed" that also fires on failure is a trap, so a
3740
+ * swap that leaves monitoring reports through exactly one of them.
3185
3741
  */
3186
3742
  finalize(swap) {
3187
3743
  if (!this.monitored.has(swap.rfqId)) return;
@@ -3217,7 +3773,7 @@ var traderClaimTxid = (swap) => {
3217
3773
  case "onchain_send":
3218
3774
  return swap.claimTxid;
3219
3775
  case "lightning_receive":
3220
- return swap.claimArkTxid;
3776
+ return swap.claimTxid;
3221
3777
  default:
3222
3778
  return void 0;
3223
3779
  }
@@ -3227,7 +3783,7 @@ var outcomeOf = (swap) => {
3227
3783
  const lostReceive = swap.kind === "lightning_receive" && swap.state === "refunded";
3228
3784
  return {
3229
3785
  state: swap.state,
3230
- txid: lostReceive ? swap.refundArkTxid : traderClaimTxid(swap) ?? swap.refundArkTxid
3786
+ txid: lostReceive ? swap.refundTxid : traderClaimTxid(swap) ?? swap.refundTxid
3231
3787
  };
3232
3788
  };
3233
3789
  var errorMessage = (error) => error instanceof Error ? error.message : String(error);
@@ -3238,27 +3794,190 @@ import {
3238
3794
  ArkAddress as ArkAddress4
3239
3795
  } from "@arkade-os/sdk";
3240
3796
  import { hex as hex12 } from "@scure/base";
3797
+
3798
+ // src/client/corridors/lightning.ts
3799
+ import { invoiceTarget as invoiceTarget3 } from "@arkade-os/sdk";
3800
+ var INVOICE_HRPS = [
3801
+ ["lnbcrt", ["regtest"]],
3802
+ ["lntbs", ["signet", "mutinynet"]],
3803
+ ["lnbc", ["bitcoin"]],
3804
+ ["lntb", ["testnet"]]
3805
+ ];
3806
+ var networksOfInvoiceHrp = (raw) => {
3807
+ const lower = raw.toLowerCase();
3808
+ return INVOICE_HRPS.find(([hrp]) => lower.startsWith(hrp))?.[1];
3809
+ };
3810
+ var PAYMENT_HASH = /^[0-9a-f]{64}$/;
3811
+ var LIGHTNING_DRIVE = {
3812
+ give: {
3813
+ lockups: [{ covenant: "arkade_lockup", owner: "solver", deadline: "refund_locktime" }],
3814
+ actions: ["claimLockup"],
3815
+ seams: ["indexer"]
3816
+ },
3817
+ take: {
3818
+ lockups: [{ covenant: "arkade_lockup", owner: "trader", deadline: "refund_locktime" }],
3819
+ actions: ["refundArkade"],
3820
+ seams: ["indexer"]
3821
+ }
3822
+ };
3823
+ var lightningCorridor = Object.assign(
3824
+ (deps) => ({
3825
+ corridor: "lightning",
3826
+ deps,
3827
+ drive: LIGHTNING_DRIVE,
3828
+ matches(raw) {
3829
+ const invoice = invoiceTarget3(raw);
3830
+ if (invoice === void 0) return void 0;
3831
+ const networks = networksOfInvoiceHrp(invoice);
3832
+ if (networks === void 0) {
3833
+ return { refused: "this invoice's network prefix names no network this SDK knows" };
3834
+ }
3835
+ if (!networks.includes(deps.networkName)) {
3836
+ return {
3837
+ refused: `this is a ${networks.join(" or ")} invoice and the wallet is on ${deps.networkName}`
3838
+ };
3839
+ }
3840
+ let facts;
3841
+ try {
3842
+ facts = deps.decode(invoice);
3843
+ if (!PAYMENT_HASH.test(facts.paymentHash)) {
3844
+ return { refused: "the decoded invoice carries no usable payment hash" };
3845
+ }
3846
+ } catch (error) {
3847
+ return {
3848
+ refused: `the invoice did not decode: ${error instanceof Error ? error.message : String(error)}`
3849
+ };
3850
+ }
3851
+ if (!Number.isSafeInteger(facts.expiresAt)) {
3852
+ return { refused: "the decoded invoice carries no usable expiry" };
3853
+ }
3854
+ const now = Math.floor(Date.now() / 1e3);
3855
+ if (facts.expiresAt <= now) {
3856
+ return { refused: `the invoice expired ${now - facts.expiresAt}s ago` };
3857
+ }
3858
+ if (!Number.isSafeInteger(facts.amountSats) || facts.amountSats < 0) {
3859
+ return { refused: "the decoded invoice carries no usable amount" };
3860
+ }
3861
+ if (facts.amountSats === 0) {
3862
+ return { refused: "the invoice names no amount, and a send route needs one" };
3863
+ }
3864
+ return {
3865
+ claimed: {
3866
+ kind: "invoice",
3867
+ bolt11: invoice,
3868
+ paymentHash: facts.paymentHash,
3869
+ amount: BigInt(facts.amountSats),
3870
+ expiresAt: facts.expiresAt
3871
+ }
3872
+ };
3873
+ }
3874
+ }),
3875
+ { target: invoiceTarget3 }
3876
+ );
3877
+
3878
+ // src/client/corridors/onchain.ts
3879
+ import { btcTarget as btcTarget3 } from "@arkade-os/sdk";
3880
+ import * as btc2 from "@scure/btc-signer";
3881
+ var ONCHAIN_DRIVE = {
3882
+ take: {
3883
+ lockups: [
3884
+ { covenant: "arkade_lockup", owner: "trader", deadline: "refund_locktime" },
3885
+ { covenant: "onchain_htlc", owner: "solver", deadline: "htlc_refund_locktime" }
3886
+ ],
3887
+ actions: ["claimOnchain", "refundArkade"],
3888
+ seams: ["indexer", "chain"]
3889
+ }
3890
+ };
3891
+ var onchainCorridor = Object.assign(
3892
+ (deps) => {
3893
+ const l1 = l1NetworkFromArk(deps.networkName);
3894
+ return {
3895
+ corridor: "onchain",
3896
+ deps,
3897
+ drive: ONCHAIN_DRIVE,
3898
+ matches(raw) {
3899
+ const target = btcTarget3(raw);
3900
+ if (target === void 0) return void 0;
3901
+ try {
3902
+ btc2.Address(L1_NETWORKS[l1]).decode(target);
3903
+ } catch {
3904
+ return { refused: `this is not a ${l1} address` };
3905
+ }
3906
+ return { claimed: { kind: "address", address: target } };
3907
+ }
3908
+ };
3909
+ },
3910
+ { target: btcTarget3 }
3911
+ );
3912
+
3913
+ // src/client/outcome.ts
3914
+ var CORRIDOR_PASS = {
3915
+ lightning_send: LIGHTNING_DRIVE.take,
3916
+ lightning_receive: LIGHTNING_DRIVE.give,
3917
+ onchain_send: ONCHAIN_DRIVE.take
3918
+ };
3919
+ var LOCKUP_OWNER = {
3920
+ lightning_send: CORRIDOR_PASS.lightning_send.lockups[0].owner,
3921
+ lightning_receive: CORRIDOR_PASS.lightning_receive.lockups[0].owner,
3922
+ onchain_send: CORRIDOR_PASS.onchain_send.lockups[0].owner
3923
+ };
3924
+ var at = (outcome) => () => outcome;
3925
+ var CORRIDOR_OUTCOME = {
3926
+ trader: {
3927
+ pending: at("funded"),
3928
+ claimable: at("funded"),
3929
+ claimed: at("funded"),
3930
+ needs_counterparty: at("needs_recovery"),
3931
+ settled: (kind) => kind === "onchain_send" ? "claimed" : "paid",
3932
+ refunded: at("refunded"),
3933
+ failed: at("failed")
3934
+ },
3935
+ solver: {
3936
+ // The solver has funded nothing yet: the invoice is shown and unpaid,
3937
+ // which is what makes this `open` where the trader's side is `funded`.
3938
+ pending: at("open"),
3939
+ claimable: at("funded"),
3940
+ claimed: at("funded"),
3941
+ needs_counterparty: at("needs_recovery"),
3942
+ // The trader's OWN claim, matched by the hash and not by our txid — so
3943
+ // a claim that lands without us still counts.
3944
+ settled: at("claimed"),
3945
+ // The one inversion this whole module exists for: every non-claim leaf
3946
+ // of a receive covenant is the solver's, so a lockup spent by one is
3947
+ // the trader's incoming payment never arriving.
3948
+ refunded: at("lapsed"),
3949
+ failed: at("failed")
3950
+ }
3951
+ };
3952
+ var corridorOutcome = (kind, state) => CORRIDOR_OUTCOME[LOCKUP_OWNER[kind]][state](kind);
3953
+ var ACTIVITY_TOKEN = {
3954
+ accepted: "pending",
3955
+ funding: "pending",
3956
+ funded: "pending",
3957
+ open: "pending",
3958
+ // The drive's in-flight states have no user-visible phase distinct from
3959
+ // "pending"; `needs_recovery` is different in kind — the swap is BLOCKED —
3960
+ // and collapsing it here is the same deliberate choice v1 made, which the
3961
+ // opaque-token design permits because apps map tokens themselves.
3962
+ needs_recovery: "pending",
3963
+ refunding: "pending",
3964
+ cancelling: "pending",
3965
+ cancelled: "cancelled",
3966
+ filled: "settled",
3967
+ claimed: "settled",
3968
+ paid: "settled",
3969
+ refunded: "refunded",
3970
+ lapsed: "lost",
3971
+ failed: "failed"
3972
+ };
3973
+
3974
+ // src/activity.ts
3241
3975
  var LABELS = {
3242
3976
  lightning_send: "Lightning send",
3243
3977
  lightning_receive: "Lightning receive",
3244
3978
  onchain_send: "Onchain send"
3245
3979
  };
3246
- var OUTCOME = {
3247
- pending: "pending",
3248
- // `claimable` and `claimed` are both in-progress states with no
3249
- // user-visible phase distinct from "pending". `needs_counterparty` is
3250
- // different in kind — the swap is BLOCKED, not merely in flight, since no
3251
- // unilateral trader move exists (see `RfqSwapState`). Collapsing it into
3252
- // `pending` here is a deliberate choice the opaque-token design permits —
3253
- // apps map tokens themselves — but a future reader weighing a `"blocked"`
3254
- // or `"stuck"` token should know this was already considered.
3255
- claimable: "pending",
3256
- claimed: "pending",
3257
- needs_counterparty: "pending",
3258
- settled: "settled",
3259
- refunded: "refunded",
3260
- failed: "failed"
3261
- };
3980
+ var outcomeToken = (kind, state) => ACTIVITY_TOKEN[corridorOutcome(kind, state)];
3262
3981
  function swapActivityResolver(deps) {
3263
3982
  let byTxid = /* @__PURE__ */ new Map();
3264
3983
  return {
@@ -3277,13 +3996,17 @@ function swapActivityResolver(deps) {
3277
3996
  const key = tx.key.arkTxid || tx.key.commitmentTxid || tx.key.boardingTxid;
3278
3997
  const swap = key ? byTxid.get(key) : void 0;
3279
3998
  if (!swap) return void 0;
3280
- const lostReceive = swap.kind === "lightning_receive" && swap.state === "refunded";
3281
3999
  return [
3282
4000
  {
3283
4001
  groupId: `swap:${swap.rfqId}`,
3284
4002
  label: LABELS[swap.kind],
3285
4003
  kind: "swap",
3286
- outcome: lostReceive ? "lost" : OUTCOME[swap.state],
4004
+ // `"lost"` for a receive leg that ended `refunded` comes out
4005
+ // of the translation rather than out of a branch here: that
4006
+ // leg has no trader-side refund, every non-claim leaf of its
4007
+ // covenant is the solver's, and the client's `lapsed` is
4008
+ // that fact.
4009
+ outcome: outcomeToken(swap.kind, swap.state),
3287
4010
  metadata: { rfqId: swap.rfqId, swapKind: swap.kind }
3288
4011
  }
3289
4012
  ];
@@ -3294,16 +4017,17 @@ async function rfqSwapActivityInputs(deps) {
3294
4017
  const records = await deps.repository.getAllRfqSwaps();
3295
4018
  return Promise.all(records.map((record) => activityInputOf(record, deps.indexer)));
3296
4019
  }
3297
- async function activityInputOf(record, indexer) {
4020
+ async function activityInputOf(stored, indexer) {
4021
+ const record = normalizeRfqSwapRecord(stored);
3298
4022
  const txids = /* @__PURE__ */ new Set();
3299
- if (record.fundingArkTxid) txids.add(record.fundingArkTxid);
3300
- if (record.refundArkTxid) txids.add(record.refundArkTxid);
4023
+ if (record.fundingTxid) txids.add(record.fundingTxid);
4024
+ if (record.refundTxid) txids.add(record.refundTxid);
3301
4025
  const handler = rfqCorridorHandlers.getOrThrow(record.kind);
3302
4026
  for (const txid of handler.activityTxids?.(record.profile) ?? []) txids.add(txid);
3303
- for (const txid of record.lockupSpendArkTxids ?? []) txids.add(txid);
3304
- const spendUnknown = isRfqSwapTerminal(record.state) && !record.refundArkTxid && !record.lockupSpendArkTxids?.length;
3305
- if (indexer && (!record.fundingArkTxid || spendUnknown)) {
3306
- for (const txid of await lockupTxids(indexer, record, !record.fundingArkTxid)) {
4027
+ for (const txid of record.lockupSpendTxids ?? []) txids.add(txid);
4028
+ const spendUnknown = isRfqSwapTerminal(record.state) && !record.refundTxid && !record.lockupSpendTxids?.length;
4029
+ if (indexer && (!record.fundingTxid || spendUnknown)) {
4030
+ for (const txid of await lockupTxids(indexer, record, !record.fundingTxid)) {
3307
4031
  txids.add(txid);
3308
4032
  }
3309
4033
  }
@@ -3328,6 +4052,346 @@ async function lockupTxids(indexer, record, wantFunding) {
3328
4052
  return [];
3329
4053
  }
3330
4054
  }
4055
+
4056
+ // src/swapClient.ts
4057
+ import { hex as hex13 } from "@scure/base";
4058
+ import { ArkAddress as ArkAddress5, asset as asset2, contractSigner as contractSigner2 } from "@arkade-os/sdk";
4059
+ import { quoteOffer } from "@arkade-os/solver-discovery";
4060
+ import * as btc3 from "@scure/btc-signer";
4061
+ var sideCorridorsOf = (market) => {
4062
+ const read = (value) => value === "lightning" || value === "onchain" ? value : "arkade";
4063
+ const m = market;
4064
+ return { base: read(m.base_corridor), quote: read(m.quote_corridor) };
4065
+ };
4066
+ var resolveKind = (market, give) => {
4067
+ const corridors = sideCorridorsOf(market);
4068
+ const giveCorridor = corridors[give];
4069
+ const receiveCorridor = corridors[give === "base" ? "quote" : "base"];
4070
+ if (giveCorridor === "arkade" && receiveCorridor === "arkade") return "spot";
4071
+ if (receiveCorridor === "lightning") return "ln_send";
4072
+ if (giveCorridor === "lightning") return "ln_receive";
4073
+ if (receiveCorridor === "onchain") return "onchain_send";
4074
+ return "onchain_receive";
4075
+ };
4076
+ var need = (value, what, leg) => {
4077
+ if (value === void 0) throw new Error(`a ${leg} quote needs ${what}`);
4078
+ return value;
4079
+ };
4080
+ var payoutScriptFor = (address, payoutPubkey, network) => address !== void 0 ? l1ScriptForAddress(address, network) : btc3.p2tr(payoutPubkey, void 0, L1_NETWORKS[network]).script;
4081
+ var corridorAmount = (input, leg) => {
4082
+ const raw = need(input.amount, "an amount", leg);
4083
+ const amountOn = need(input.amountOn, "amountOn ('give' or 'receive')", leg);
4084
+ const amount = Number(raw);
4085
+ if (!Number.isSafeInteger(amount) || amount <= 0) {
4086
+ throw new Error(`a ${leg} amount must be a positive integer of sats, got ${String(raw)}`);
4087
+ }
4088
+ return { amount, amountSide: amountOn === "give" ? "from" : "to" };
4089
+ };
4090
+ function createSwapClient(deps) {
4091
+ const { wallet, repository, transportFor } = deps;
4092
+ const ark = deps.ark ?? walletOperator(wallet);
4093
+ let reader;
4094
+ const reading = () => reader ??= wallet.getArkadeReader();
4095
+ const indexer = deps.indexer ?? {
4096
+ getVtxos: async (opts) => {
4097
+ if (!opts)
4098
+ throw new Error("getVtxos on the swap indexer requires scripts or outpoints");
4099
+ return (await reading()).getVtxos(opts);
4100
+ },
4101
+ getVirtualTxs: async (...args) => (await reading()).getVirtualTxs(...args)
4102
+ };
4103
+ const managerDeps = { indexer, chain: deps.chain, repository };
4104
+ const manager = new RfqSwapManager(managerDeps);
4105
+ const listeners = /* @__PURE__ */ new Set();
4106
+ const notify2 = (swap) => {
4107
+ for (const listener of listeners) {
4108
+ try {
4109
+ listener(swap);
4110
+ } catch {
4111
+ }
4112
+ }
4113
+ };
4114
+ manager.onSwapUpdate((swap) => notify2({ family: "rfq", swap }));
4115
+ const claimLockup = async (swap, vtxos, { partiallyClaimed }) => {
4116
+ const record = await repository.getRfqSwap(swap.rfqId);
4117
+ if (!record) throw new Error(`rfq swap ${swap.rfqId} has no stored record to claim from`);
4118
+ const secret = rfqClaimSecretOf(record);
4119
+ if (!secret) throw new Error(`rfq swap ${swap.rfqId} carries no claim secret`);
4120
+ const script = swap.lockup?.script;
4121
+ if (!script) throw new Error(`rfq swap ${swap.rfqId} carries no lockup covenant`);
4122
+ const payoutAddress = rfqClaimDestinationOf(record);
4123
+ if (!payoutAddress) {
4124
+ throw new Error(`rfq swap ${swap.rfqId} carries no claim destination`);
4125
+ }
4126
+ return pushClaim(ark, {
4127
+ contract: script,
4128
+ receiver: await contractSigner2(wallet, secret.signingDescriptor),
4129
+ preimage: await preimageForSwapRecord(wallet, secret),
4130
+ vtxos,
4131
+ destinationPkScript: ArkAddress5.decode(payoutAddress).pkScript,
4132
+ expectedAmount: swap.expectedAmount,
4133
+ partiallyClaimed
4134
+ });
4135
+ };
4136
+ manager.setCallbacks({
4137
+ refundArkade: arkadeRefunder({ operator: ark, indexer, wallet, repository }),
4138
+ claimLockup,
4139
+ ...deps.claimOnchain ? { claimOnchain: deps.claimOnchain } : {}
4140
+ });
4141
+ let watcher;
4142
+ const quote = async (market, input) => {
4143
+ const kind = resolveKind(market, input.give);
4144
+ switch (kind) {
4145
+ case "spot": {
4146
+ const raw = need(input.amount, "an amount", kind);
4147
+ const amountOn = need(input.amountOn, "amountOn ('give' or 'receive')", kind);
4148
+ const plan = await quoteOffer(market, {
4149
+ give: input.give,
4150
+ ...amountOn === "give" ? { giveAmount: raw } : { wantAmount: raw },
4151
+ fetchImpl: deps.discovery.fetchImpl
4152
+ });
4153
+ return { kind, market, plan };
4154
+ }
4155
+ case "ln_send": {
4156
+ const invoice = need(input.invoice, "the invoice to pay", kind);
4157
+ const request = await requestLightningSend(wallet, transportFor(market), {
4158
+ invoice,
4159
+ emulatorPubkey: deps.emulatorPubkey
4160
+ });
4161
+ return { kind, market, request };
4162
+ }
4163
+ case "ln_receive": {
4164
+ const params = {
4165
+ ...corridorAmount(input, kind),
4166
+ covclaimdPubkey: need(deps.covclaimdPubkey, "deps.covclaimdPubkey", kind),
4167
+ decodeInvoice: need(deps.decodeInvoice, "deps.decodeInvoice", kind),
4168
+ maxPayAmount: input.maxPayAmount,
4169
+ emulatorPubkey: deps.emulatorPubkey
4170
+ };
4171
+ const request = await requestLightningReceive(wallet, transportFor(market), params);
4172
+ return { kind, market, request, invoice: request.invoice };
4173
+ }
4174
+ case "onchain_send": {
4175
+ need(deps.chain, "deps.chain (L1 access)", kind);
4176
+ const payoutPubkey = need(input.payoutPubkey, "a payoutPubkey", kind);
4177
+ const params = {
4178
+ ...corridorAmount(input, kind),
4179
+ payoutPubkey,
4180
+ preimage: input.preimage,
4181
+ emulatorPubkey: deps.emulatorPubkey
4182
+ };
4183
+ const request = await requestOnchainSend(wallet, transportFor(market), params);
4184
+ return {
4185
+ kind,
4186
+ market,
4187
+ request,
4188
+ payoutPkScript: payoutScriptFor(
4189
+ input.payoutAddress,
4190
+ payoutPubkey,
4191
+ request.l1Network
4192
+ )
4193
+ };
4194
+ }
4195
+ case "onchain_receive":
4196
+ throw new Error(
4197
+ "onchain->arkade is not driven by RfqSwapManager yet; quote it directly with requestOnchainReceive"
4198
+ );
4199
+ }
4200
+ };
4201
+ const admit = async (swap, origin) => {
4202
+ let announced = false;
4203
+ const heard = manager.onSwapUpdate((updated) => {
4204
+ if (updated.rfqId === swap.rfqId) announced = true;
4205
+ });
4206
+ try {
4207
+ await manager.addSwap(swap, origin);
4208
+ } finally {
4209
+ heard();
4210
+ }
4211
+ const unified = { family: "rfq", swap };
4212
+ if (!announced) notify2(unified);
4213
+ return unified;
4214
+ };
4215
+ const fundPersisted = async (swap, origin, funding) => {
4216
+ await repository.saveRfqSwap(createRfqSwapRecord(origin, swap));
4217
+ return wallet.send({ address: funding.address, amount: funding.amount });
4218
+ };
4219
+ const accept = async (accepted) => {
4220
+ const now = Math.floor(Date.now() / 1e3);
4221
+ switch (accepted.kind) {
4222
+ case "spot": {
4223
+ const { plan } = accepted;
4224
+ const depositIsBtc = plan.deposit.asset.id === BTC_ASSET_ID;
4225
+ const offer = await createOffer(wallet, {
4226
+ wantAmount: plan.receive.atomic,
4227
+ ...plan.receive.asset.id === BTC_ASSET_ID ? { offerAsset: asset2.AssetId.fromString(plan.deposit.asset.id) } : { wantAsset: asset2.AssetId.fromString(plan.receive.asset.id) },
4228
+ emulatorPubkey: deps.emulatorPubkey
4229
+ });
4230
+ const txid = await wallet.send({
4231
+ address: offer.address,
4232
+ // asset deposits ride the SDK's dust-sat carrier
4233
+ amount: depositIsBtc ? Number(plan.deposit.atomic) : void 0,
4234
+ assets: depositIsBtc ? void 0 : [{ assetId: plan.deposit.asset.id, amount: plan.deposit.atomic }],
4235
+ extensions: [offer.extension]
4236
+ });
4237
+ const swap = {
4238
+ id: txid,
4239
+ fromAsset: plan.deposit.asset.id,
4240
+ toAsset: plan.receive.asset.id,
4241
+ fromAmount: plan.deposit.atomic.toString(),
4242
+ toAmount: plan.receive.atomic.toString(),
4243
+ swapAddress: offer.address,
4244
+ swapPkScript: hex13.encode(offer.swapPkScript),
4245
+ offerHex: offer.offerHex,
4246
+ fundingTxid: txid,
4247
+ status: "pending",
4248
+ createdAt: Date.now()
4249
+ };
4250
+ await addAssetSwap(repository, swap);
4251
+ const unified = { family: "offer", swap };
4252
+ notify2(unified);
4253
+ return unified;
4254
+ }
4255
+ case "ln_send": {
4256
+ const { request } = accepted;
4257
+ const swap = {
4258
+ kind: "lightning_send",
4259
+ rfqId: request.rfqId,
4260
+ state: "pending",
4261
+ lockupPkScript: request.swapPkScript,
4262
+ lockup: { script: request.script, address: request.address },
4263
+ // the trader's own decode, never the solver's echo
4264
+ paymentHash: request.contractParams.paymentHash,
4265
+ refundLocktime: request.contractParams.refundLocktime,
4266
+ createdAt: now,
4267
+ updatedAt: now
4268
+ };
4269
+ const origin = {
4270
+ kind: "lightning_send",
4271
+ lockupAddress: request.address,
4272
+ profile: rfqSecretsProfile(request.secrets, swap.paymentHash),
4273
+ amount: request.fundAmount
4274
+ };
4275
+ const txid = await fundPersisted(swap, origin, {
4276
+ address: request.address,
4277
+ amount: request.fundAmount
4278
+ });
4279
+ return admit(swap, { ...origin, fundingTxid: txid });
4280
+ }
4281
+ case "ln_receive": {
4282
+ const { request } = accepted;
4283
+ const paymentHash = hex13.encode(request.secrets.paymentHash);
4284
+ const swap = {
4285
+ kind: "lightning_receive",
4286
+ rfqId: request.rfqId,
4287
+ state: "pending",
4288
+ lockupPkScript: request.swapPkScript,
4289
+ lockup: { script: request.script, address: request.address },
4290
+ paymentHash,
4291
+ refundLocktime: request.contractParams.refundLocktime,
4292
+ expectedAmount: request.expectedAmount,
4293
+ createdAt: now,
4294
+ updatedAt: now
4295
+ };
4296
+ return admit(swap, {
4297
+ kind: "lightning_receive",
4298
+ lockupAddress: request.address,
4299
+ profile: {
4300
+ ...rfqSecretsProfile(request.secrets, paymentHash),
4301
+ expectedAmount: request.expectedAmount,
4302
+ payoutAddress: request.payoutAddress
4303
+ },
4304
+ amount: request.expectedAmount
4305
+ });
4306
+ }
4307
+ case "onchain_send": {
4308
+ const { request } = accepted;
4309
+ const paymentHash = hex13.encode(request.secrets.paymentHash);
4310
+ const swap = {
4311
+ kind: "onchain_send",
4312
+ rfqId: request.rfqId,
4313
+ state: "pending",
4314
+ lockupPkScript: request.swapPkScript,
4315
+ lockup: { script: request.script, address: request.address },
4316
+ paymentHash,
4317
+ refundLocktime: request.refundLocktime,
4318
+ htlc: request.htlc,
4319
+ minConfirmations: request.minConfirmations,
4320
+ payoutPkScript: accepted.payoutPkScript,
4321
+ createdAt: now,
4322
+ updatedAt: now
4323
+ };
4324
+ const origin = {
4325
+ kind: "onchain_send",
4326
+ lockupAddress: request.address,
4327
+ profile: {
4328
+ ...rfqSecretsProfile(request.secrets, paymentHash),
4329
+ ...onchainSendProfile({
4330
+ ...request,
4331
+ payoutPkScript: accepted.payoutPkScript
4332
+ })
4333
+ },
4334
+ amount: request.fundAmount
4335
+ };
4336
+ const txid = await fundPersisted(swap, origin, {
4337
+ address: request.address,
4338
+ amount: request.fundAmount
4339
+ });
4340
+ return admit(swap, { ...origin, fundingTxid: txid });
4341
+ }
4342
+ }
4343
+ };
4344
+ return {
4345
+ manager,
4346
+ markets: (useCache) => discoverMarkets({ ...deps.discovery, repository, useCache }),
4347
+ quote,
4348
+ accept,
4349
+ cancel: async (fundingTxid) => {
4350
+ const swaps = await getAssetSwapsOrThrow(repository);
4351
+ const swap = swaps.find((s) => s.id === fundingTxid);
4352
+ if (!swap) throw new Error(`no offer swap with funding txid ${fundingTxid}`);
4353
+ if (swap.status !== "pending") {
4354
+ throw new Error(`offer swap ${fundingTxid} is ${swap.status}, not cancellable`);
4355
+ }
4356
+ await updateAssetSwap(repository, fundingTxid, { status: "cancelling" });
4357
+ await cancelOffer(wallet, swap.offerHex, {
4358
+ repository,
4359
+ fundingTxid
4360
+ });
4361
+ },
4362
+ // Both families, live AND terminal — the offer half reads the whole
4363
+ // stored history, and the RFQ half is every swap the manager holds.
4364
+ // Its history is what `start()` restored plus what this session
4365
+ // accepted; records past `RFQ_SWAP_RETENTION_SECONDS` are pruned.
4366
+ swaps: async () => {
4367
+ const offers = await getAssetSwaps(repository);
4368
+ const rfq = await manager.getAllSwaps();
4369
+ return [
4370
+ ...offers.map((swap) => ({ family: "offer", swap })),
4371
+ ...rfq.map((swap) => ({ family: "rfq", swap }))
4372
+ ];
4373
+ },
4374
+ onUpdate: (listener) => {
4375
+ listeners.add(listener);
4376
+ return () => listeners.delete(listener);
4377
+ },
4378
+ start: async () => {
4379
+ managerDeps.contracts ??= await wallet.getContractManager();
4380
+ await manager.restoreFromRepository();
4381
+ await manager.start();
4382
+ watcher ??= await watchOfferSwaps({
4383
+ wallet,
4384
+ repository,
4385
+ onUpdate: (swap) => notify2({ family: "offer", swap })
4386
+ });
4387
+ },
4388
+ stop: async () => {
4389
+ await manager.stop();
4390
+ watcher?.stop();
4391
+ watcher = void 0;
4392
+ }
4393
+ };
4394
+ }
3331
4395
  export {
3332
4396
  ARKADE_ASSET,
3333
4397
  ARKADE_BTC,
@@ -3336,6 +4400,7 @@ export {
3336
4400
  InMemoryAssetSwapRepository,
3337
4401
  IndexedDbAssetSwapRepository,
3338
4402
  LIGHTNING_BTC,
4403
+ LIGHTNING_RAIL,
3339
4404
  LIGHTNING_RECEIVE_PAIR,
3340
4405
  LIGHTNING_SEND_PAIR,
3341
4406
  LOCKTIME_THRESHOLD,
@@ -3346,6 +4411,7 @@ export {
3346
4411
  MAX_MIN_CONFIRMATIONS,
3347
4412
  MIN_CLAIM_WINDOW_SECONDS,
3348
4413
  MIN_HEADROOM_SECONDS,
4414
+ NoSpendableDepositError,
3349
4415
  OFFER_PACKET_TYPE,
3350
4416
  ONCHAIN_BTC,
3351
4417
  ONCHAIN_CLAIM_MARGIN_SECONDS,
@@ -3354,6 +4420,9 @@ export {
3354
4420
  ONCHAIN_RECEIVE_PAIR,
3355
4421
  ONCHAIN_SECONDS_PER_BLOCK,
3356
4422
  ONCHAIN_SEND_PAIR,
4423
+ ONCHAIN_SWAP_RAIL,
4424
+ OfferCovenantMismatchError,
4425
+ PAYMENT_STATUS,
3357
4426
  PreimageNotRecoverableError,
3358
4427
  QUOTE_OPTIONS,
3359
4428
  REFUND_MTP_LAG_SECONDS,
@@ -3370,6 +4439,8 @@ export {
3370
4439
  SWAP_LOCKUP_CONTRACT_KIND,
3371
4440
  SWAP_LOCKUP_CONTRACT_LABEL,
3372
4441
  SWAP_LOCKUP_CONTRACT_TYPE,
4442
+ SWAP_ROUTER_PRIORITY,
4443
+ SwapPaymentFailedError,
3373
4444
  SwapRefusal,
3374
4445
  addAssetSwap,
3375
4446
  arkadeAssetLeg,
@@ -3384,6 +4455,7 @@ export {
3384
4455
  buildHtlcRefund,
3385
4456
  cancelOffer,
3386
4457
  chainSourceFrom,
4458
+ claimFeeSats,
3387
4459
  claimOnchainFill,
3388
4460
  claimReceiveLockup,
3389
4461
  classifyDepositSpend,
@@ -3391,6 +4463,8 @@ export {
3391
4463
  classifySpend,
3392
4464
  createOffer,
3393
4465
  createRfqSwapRecord,
4466
+ createSwapClient,
4467
+ createSwapPaymentRouter,
3394
4468
  decodeOffer,
3395
4469
  deriveLightningReceive,
3396
4470
  deriveOnchainReceive,
@@ -3405,28 +4479,35 @@ export {
3405
4479
  httpTransport,
3406
4480
  isRfqSwapTerminal,
3407
4481
  isRfqTerminal,
4482
+ isTerminalStatus,
3408
4483
  l1ScriptForAddress,
4484
+ lightningRail,
4485
+ lightningReceiveContract,
3409
4486
  lightningReceiveRequest,
4487
+ lightningSendContract,
3410
4488
  lightningSendRequest,
3411
- lightningSendVtxoScript,
3412
4489
  lockupContractParams,
3413
4490
  makeCachedFeedFetch,
3414
4491
  newPreimage,
3415
4492
  newRfqId,
3416
4493
  nextOnchainAction,
4494
+ normalizeRfqSwapRecord,
4495
+ offerContract,
3417
4496
  offerTermsFromQuote,
3418
- offerVtxoScript,
3419
4497
  onchainHtlcScript,
3420
4498
  onchainReceiveRequest,
3421
4499
  onchainSendProfile,
3422
4500
  onchainSendRequest,
4501
+ onchainSwapRail,
3423
4502
  paymentHashOf,
4503
+ paymentStatusOf,
3424
4504
  preimageForSwapRecord,
3425
4505
  pushClaim,
3426
4506
  pushRefundWithoutReceiver,
4507
+ railAvailable,
3427
4508
  readLockupFate,
3428
4509
  rebuildRfqSwap,
3429
- receiveVtxoScript,
4510
+ receiverExact,
3430
4511
  refundIfUnresolved,
3431
4512
  registerLockupContract,
3432
4513
  relayTransport,
@@ -3453,6 +4534,7 @@ export {
3453
4534
  spendTxidsOf,
3454
4535
  spendUpdate,
3455
4536
  swapActivityResolver,
4537
+ swapHandle,
3456
4538
  swapPrograms,
3457
4539
  swapSecretsToRecord,
3458
4540
  unilateralClaimDelay,