@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.cjs CHANGED
@@ -37,6 +37,7 @@ __export(index_exports, {
37
37
  InMemoryAssetSwapRepository: () => InMemoryAssetSwapRepository,
38
38
  IndexedDbAssetSwapRepository: () => IndexedDbAssetSwapRepository,
39
39
  LIGHTNING_BTC: () => LIGHTNING_BTC,
40
+ LIGHTNING_RAIL: () => LIGHTNING_RAIL,
40
41
  LIGHTNING_RECEIVE_PAIR: () => LIGHTNING_RECEIVE_PAIR,
41
42
  LIGHTNING_SEND_PAIR: () => LIGHTNING_SEND_PAIR,
42
43
  LOCKTIME_THRESHOLD: () => LOCKTIME_THRESHOLD,
@@ -47,6 +48,7 @@ __export(index_exports, {
47
48
  MAX_MIN_CONFIRMATIONS: () => MAX_MIN_CONFIRMATIONS,
48
49
  MIN_CLAIM_WINDOW_SECONDS: () => MIN_CLAIM_WINDOW_SECONDS,
49
50
  MIN_HEADROOM_SECONDS: () => MIN_HEADROOM_SECONDS,
51
+ NoSpendableDepositError: () => NoSpendableDepositError,
50
52
  OFFER_PACKET_TYPE: () => OFFER_PACKET_TYPE,
51
53
  ONCHAIN_BTC: () => ONCHAIN_BTC,
52
54
  ONCHAIN_CLAIM_MARGIN_SECONDS: () => ONCHAIN_CLAIM_MARGIN_SECONDS,
@@ -55,6 +57,9 @@ __export(index_exports, {
55
57
  ONCHAIN_RECEIVE_PAIR: () => ONCHAIN_RECEIVE_PAIR,
56
58
  ONCHAIN_SECONDS_PER_BLOCK: () => ONCHAIN_SECONDS_PER_BLOCK,
57
59
  ONCHAIN_SEND_PAIR: () => ONCHAIN_SEND_PAIR,
60
+ ONCHAIN_SWAP_RAIL: () => ONCHAIN_SWAP_RAIL,
61
+ OfferCovenantMismatchError: () => OfferCovenantMismatchError,
62
+ PAYMENT_STATUS: () => PAYMENT_STATUS,
58
63
  PreimageNotRecoverableError: () => PreimageNotRecoverableError,
59
64
  QUOTE_OPTIONS: () => QUOTE_OPTIONS,
60
65
  REFUND_MTP_LAG_SECONDS: () => REFUND_MTP_LAG_SECONDS,
@@ -71,6 +76,8 @@ __export(index_exports, {
71
76
  SWAP_LOCKUP_CONTRACT_KIND: () => SWAP_LOCKUP_CONTRACT_KIND,
72
77
  SWAP_LOCKUP_CONTRACT_LABEL: () => SWAP_LOCKUP_CONTRACT_LABEL,
73
78
  SWAP_LOCKUP_CONTRACT_TYPE: () => SWAP_LOCKUP_CONTRACT_TYPE,
79
+ SWAP_ROUTER_PRIORITY: () => SWAP_ROUTER_PRIORITY,
80
+ SwapPaymentFailedError: () => SwapPaymentFailedError,
74
81
  SwapRefusal: () => SwapRefusal,
75
82
  addAssetSwap: () => addAssetSwap,
76
83
  arkadeAssetLeg: () => arkadeAssetLeg,
@@ -85,6 +92,7 @@ __export(index_exports, {
85
92
  buildHtlcRefund: () => buildHtlcRefund,
86
93
  cancelOffer: () => cancelOffer,
87
94
  chainSourceFrom: () => chainSourceFrom,
95
+ claimFeeSats: () => claimFeeSats,
88
96
  claimOnchainFill: () => claimOnchainFill,
89
97
  claimReceiveLockup: () => claimReceiveLockup,
90
98
  classifyDepositSpend: () => classifyDepositSpend,
@@ -92,6 +100,8 @@ __export(index_exports, {
92
100
  classifySpend: () => classifySpend,
93
101
  createOffer: () => createOffer,
94
102
  createRfqSwapRecord: () => createRfqSwapRecord,
103
+ createSwapClient: () => createSwapClient,
104
+ createSwapPaymentRouter: () => createSwapPaymentRouter,
95
105
  decodeOffer: () => decodeOffer,
96
106
  deriveLightningReceive: () => deriveLightningReceive,
97
107
  deriveOnchainReceive: () => deriveOnchainReceive,
@@ -106,28 +116,35 @@ __export(index_exports, {
106
116
  httpTransport: () => httpTransport,
107
117
  isRfqSwapTerminal: () => isRfqSwapTerminal,
108
118
  isRfqTerminal: () => isRfqTerminal,
119
+ isTerminalStatus: () => isTerminalStatus,
109
120
  l1ScriptForAddress: () => l1ScriptForAddress,
121
+ lightningRail: () => lightningRail,
122
+ lightningReceiveContract: () => lightningReceiveContract,
110
123
  lightningReceiveRequest: () => lightningReceiveRequest,
124
+ lightningSendContract: () => lightningSendContract,
111
125
  lightningSendRequest: () => lightningSendRequest,
112
- lightningSendVtxoScript: () => lightningSendVtxoScript,
113
126
  lockupContractParams: () => lockupContractParams,
114
127
  makeCachedFeedFetch: () => makeCachedFeedFetch,
115
128
  newPreimage: () => newPreimage,
116
129
  newRfqId: () => newRfqId,
117
130
  nextOnchainAction: () => nextOnchainAction,
131
+ normalizeRfqSwapRecord: () => normalizeRfqSwapRecord,
132
+ offerContract: () => offerContract,
118
133
  offerTermsFromQuote: () => offerTermsFromQuote,
119
- offerVtxoScript: () => offerVtxoScript,
120
134
  onchainHtlcScript: () => onchainHtlcScript,
121
135
  onchainReceiveRequest: () => onchainReceiveRequest,
122
136
  onchainSendProfile: () => onchainSendProfile,
123
137
  onchainSendRequest: () => onchainSendRequest,
138
+ onchainSwapRail: () => onchainSwapRail,
124
139
  paymentHashOf: () => paymentHashOf,
140
+ paymentStatusOf: () => paymentStatusOf,
125
141
  preimageForSwapRecord: () => preimageForSwapRecord,
126
142
  pushClaim: () => pushClaim,
127
143
  pushRefundWithoutReceiver: () => pushRefundWithoutReceiver,
144
+ railAvailable: () => railAvailable,
128
145
  readLockupFate: () => readLockupFate,
129
146
  rebuildRfqSwap: () => rebuildRfqSwap,
130
- receiveVtxoScript: () => receiveVtxoScript,
147
+ receiverExact: () => receiverExact,
131
148
  refundIfUnresolved: () => refundIfUnresolved,
132
149
  registerLockupContract: () => registerLockupContract,
133
150
  relayTransport: () => relayTransport,
@@ -154,6 +171,7 @@ __export(index_exports, {
154
171
  spendTxidsOf: () => spendTxidsOf,
155
172
  spendUpdate: () => spendUpdate,
156
173
  swapActivityResolver: () => swapActivityResolver,
174
+ swapHandle: () => swapHandle,
157
175
  swapPrograms: () => swapPrograms,
158
176
  swapSecretsToRecord: () => swapSecretsToRecord,
159
177
  unilateralClaimDelay: () => unilateralClaimDelay,
@@ -333,9 +351,9 @@ var addAssetSwap = async (repository, swap) => {
333
351
  const swaps = await getAssetSwapsOrThrow(repository);
334
352
  if (swaps.some((s) => s.id === swap.id)) return swaps;
335
353
  await saveSwapOrThrow(repository, swap);
336
- const at = swaps.findIndex((s) => byNewest(swap, s) <= 0);
354
+ const at2 = swaps.findIndex((s) => byNewest(swap, s) <= 0);
337
355
  const merged = [...swaps];
338
- merged.splice(at === -1 ? merged.length : at, 0, swap);
356
+ merged.splice(at2 === -1 ? merged.length : at2, 0, swap);
339
357
  return merged;
340
358
  };
341
359
  var updateAssetSwap = async (repository, id, changes) => {
@@ -435,7 +453,7 @@ function withExitClosure(program, exit) {
435
453
  }
436
454
  };
437
455
  }
438
- function swapProgramBinding(offer, serverPubkey) {
456
+ function swapProgramBinding(offer, operatorPubkey) {
439
457
  if (offer.makerPkScript.length !== FIELDS.makerPkScript.width) {
440
458
  throw new Error("makerPkScript is not a 34-byte taproot scriptPubKey");
441
459
  }
@@ -447,7 +465,7 @@ function swapProgramBinding(offer, serverPubkey) {
447
465
  args: {
448
466
  makerWP: offer.makerPkScript.subarray(2),
449
467
  wantAmount: offer.wantAmount,
450
- server: serverPubkey,
468
+ server: operatorPubkey,
451
469
  user: offer.makerPublicKey,
452
470
  // internal byte order
453
471
  ...offer.wantAsset && {
@@ -457,14 +475,14 @@ function swapProgramBinding(offer, serverPubkey) {
457
475
  ...offer.exitDelay && { exitDelay: offer.exitDelay.value }
458
476
  },
459
477
  keys: {
460
- serverKey: serverPubkey,
478
+ serverKey: operatorPubkey,
461
479
  userKey: offer.makerPublicKey,
462
480
  emulatorKey: offer.emulatorPubkey
463
481
  }
464
482
  };
465
483
  }
466
- function offerVtxoScript(offer, serverPubkey) {
467
- const { program, args, keys } = swapProgramBinding(offer, serverPubkey);
484
+ function offerContract(offer, operatorPubkey) {
485
+ const { program, args, keys } = swapProgramBinding(offer, operatorPubkey);
468
486
  return new import_sdk2.arkade.ArkadeProgramScript(program, args, keys);
469
487
  }
470
488
  var OFFER_PACKET_TYPE = 3;
@@ -578,14 +596,14 @@ function decodeOffer(data) {
578
596
  throw new Error(`missing/invalid ${name}`);
579
597
  }
580
598
  }
581
- const need = (name) => {
599
+ const need2 = (name) => {
582
600
  const v = fields[name];
583
601
  const len = FIELDS[name].width;
584
602
  if (!v || len !== void 0 && v.length !== len)
585
603
  throw new Error(`missing/invalid ${name}`);
586
604
  return v;
587
605
  };
588
- const amount = need("wantAmount");
606
+ const amount = need2("wantAmount");
589
607
  if (Boolean(fields.wantAsset) === Boolean(fields.offerAsset)) {
590
608
  throw new Error("offer must carry exactly one of wantAsset or offerAsset");
591
609
  }
@@ -602,13 +620,13 @@ function decodeOffer(data) {
602
620
  throw new Error("offer must carry both ratioNum and ratioDen, or neither");
603
621
  }
604
622
  return {
605
- swapPkScript: need("swapPkScript"),
623
+ swapPkScript: need2("swapPkScript"),
606
624
  wantAmount: readU64(amount),
607
625
  ...fields.wantAsset && { wantAsset: import_sdk2.asset.AssetId.fromBytes(fields.wantAsset) },
608
626
  ...fields.offerAsset && { offerAsset: import_sdk2.asset.AssetId.fromBytes(fields.offerAsset) },
609
- makerPkScript: need("makerPkScript"),
610
- makerPublicKey: need("makerPublicKey"),
611
- emulatorPubkey: need("emulatorPubkey"),
627
+ makerPkScript: need2("makerPkScript"),
628
+ makerPublicKey: need2("makerPublicKey"),
629
+ emulatorPubkey: need2("emulatorPubkey"),
612
630
  ...ratioNum !== void 0 && { ratioNum },
613
631
  ...ratioDen !== void 0 && { ratioDen },
614
632
  ...fields.exitTimelock && { exitDelay: decodeExitDelay(fields.exitTimelock) }
@@ -621,17 +639,21 @@ function decodeExitDelay(value) {
621
639
  }
622
640
  var OFFER_CONTRACT_LABEL = "Arkade swap offer";
623
641
  var OFFER_CONTRACT_KIND = "asset-swap-offer";
624
- async function registerOfferContract(wallet, arkServerUrl, network, binding, serverPubkey, expectedPkScript) {
625
- const { program, args, keys } = swapProgramBinding(binding, serverPubkey);
642
+ async function registerOfferContract(wallet, info, binding, operatorPubkey, expectedPkScript) {
643
+ const { program, args, keys } = swapProgramBinding(binding, operatorPubkey);
626
644
  const contractManager = await wallet.getContractManager();
627
645
  const client = await import_sdk2.arkade.Arkade.connect({
628
- arkade: new import_sdk2.RestArkProvider(arkServerUrl),
629
- indexer: new import_sdk2.RestIndexerProvider(arkServerUrl),
646
+ // Registration derives and persists; it never broadcasts and never
647
+ // reads UTXOs, so the only thing the client needs off the server is the
648
+ // info the caller already resolved. Handing that back — rather than a
649
+ // provider built from a URL — is what lets `createOffer` take just a
650
+ // wallet, and spares a second `/v1/info` round-trip.
651
+ arkade: { getInfo: async () => info },
630
652
  identity: wallet.identity,
631
653
  // without this the row's `address` would be derived against the SDK's
632
654
  // default network while its script is right — a row that disagrees with
633
655
  // the address the user is about to fund
634
- network: (0, import_sdk2.getNetwork)(network),
656
+ network: (0, import_sdk2.networkFromArkadeInfo)(info),
635
657
  contractManager
636
658
  });
637
659
  const contract = new import_sdk2.arkade.ArkadeContract(client, program, args, keys);
@@ -652,17 +674,21 @@ function serverExitDelay(delay) {
652
674
  }
653
675
  return assertExitDelay({ value: delay, type: delay < BigInt(512) ? "blocks" : "seconds" });
654
676
  }
655
- async function createOffer(wallet, arkServerUrl, params) {
677
+ async function createOffer(wallet, params) {
656
678
  if (Boolean(params.wantAsset) === Boolean(params.offerAsset)) {
657
679
  throw new Error("set exactly one of wantAsset (BTC->asset) or offerAsset (asset->BTC)");
658
680
  }
659
681
  const [info, makerAddress, makerPublicKey] = await Promise.all([
660
- new import_sdk2.RestArkProvider(arkServerUrl).getInfo(),
682
+ // requireLive: this info binds signerPubkey into the covenant — a
683
+ // snapshot could derive an address the operator no longer co-signs
684
+ // for, so an unreachable operator fails the call instead (fail closed,
685
+ // the same behaviour the pre-#734 caller-held provider had)
686
+ wallet.getArkadeInfo({ requireLive: true }),
661
687
  wallet.getAddress(),
662
688
  wallet.identity.xOnlyPublicKey()
663
689
  ]);
664
- const serverPubKey = import_base2.hex.decode((0, import_sdk2.toXOnlySignerHex)(info.signerPubkey));
665
- const network = (0, import_sdk2.getNetwork)(info.network);
690
+ const operatorPubKey = import_base2.hex.decode((0, import_sdk2.toXOnlySignerHex)(info.signerPubkey));
691
+ const network = (0, import_sdk2.networkFromArkadeInfo)(info);
666
692
  const emuKey = import_base2.hex.decode(
667
693
  (0, import_sdk2.toXOnlySignerHex)((0, import_sdk2.resolveEmulatorPubkey)(network, params.emulatorPubkey))
668
694
  );
@@ -678,33 +704,77 @@ async function createOffer(wallet, arkServerUrl, params) {
678
704
  // an offer that then fails to encode. @see assertExitDelay
679
705
  exitDelay: params.noExit ? void 0 : params.exitDelay ? assertExitDelay(params.exitDelay) : serverExitDelay(info.unilateralExitDelay)
680
706
  };
681
- const script = offerVtxoScript(binding, serverPubKey);
707
+ const script = offerContract(binding, operatorPubKey);
682
708
  const offer = { ...binding, swapPkScript: script.pkScript };
683
- await registerOfferContract(
684
- wallet,
685
- arkServerUrl,
686
- info.network,
687
- binding,
688
- serverPubKey,
689
- script.pkScript
690
- );
709
+ await registerOfferContract(wallet, info, binding, operatorPubKey, script.pkScript);
691
710
  const payload = encodeOffer(offer);
692
711
  return {
693
712
  offerHex: import_base2.hex.encode(payload),
694
713
  extension: { type: OFFER_PACKET_TYPE, payload },
695
- // VtxoScript.address owns address construction; assembling an ArkAddress
714
+ // the contract's .address() builds the address; assembling an ArkAddress
696
715
  // from tweakedPublicKey here would silently miss any future step it gains
697
- address: script.address(network.hrp, serverPubKey).encode(),
716
+ address: script.address(network.hrp, operatorPubKey).encode(),
698
717
  swapPkScript: script.pkScript
699
718
  };
700
719
  }
701
- async function cancelOffer(wallet, arkServerUrl, offerHex, opts) {
720
+ async function cancelOffer(wallet, offerHex, opts) {
702
721
  const { repository, fundingTxid, swapAddress } = opts;
722
+ const prepared = await prepareOfferCancel(wallet, offerHex, {
723
+ ...fundingTxid === void 0 ? {} : { fundingTxid },
724
+ ...swapAddress === void 0 ? {} : { swapAddress }
725
+ });
726
+ const swapId = fundingTxid ?? prepared.vtxo.txid;
727
+ const hasLocalRecord = (await getAssetSwapsOrThrow(repository)).some((s) => s.id === swapId);
728
+ if (hasLocalRecord) await updateAssetSwap(repository, swapId, { status: "cancelling" });
729
+ const txid = await prepared.send();
730
+ if (hasLocalRecord) {
731
+ const { persisted, swaps } = await updateAssetSwapBestEffort(repository, swapId, {
732
+ status: "cancelled",
733
+ spentTxid: txid
734
+ });
735
+ if (persisted) {
736
+ const contractManager = await wallet.getContractManager();
737
+ await retireOfferContract(
738
+ contractManager,
739
+ swaps,
740
+ import_base2.hex.encode(prepared.offer.swapPkScript)
741
+ );
742
+ }
743
+ }
744
+ return txid;
745
+ }
746
+ var OFFER_COVENANT_MISMATCH = "rebuilt covenant does not match the offer's swapPkScript";
747
+ var NoSpendableDepositError = class extends Error {
748
+ name = "NoSpendableDepositError";
749
+ constructor(options) {
750
+ super("no spendable VTXO at the swap address", options);
751
+ }
752
+ };
753
+ var OfferCovenantMismatchError = class extends Error {
754
+ constructor(swapPkScript, options) {
755
+ super(
756
+ `${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`,
757
+ options
758
+ );
759
+ this.swapPkScript = swapPkScript;
760
+ }
761
+ swapPkScript;
762
+ name = "OfferCovenantMismatchError";
763
+ };
764
+ async function prepareOfferCancel(wallet, offerHex, opts) {
765
+ const { fundingTxid, swapAddress } = opts;
703
766
  const offer = decodeOffer(import_base2.hex.decode(offerHex));
704
- const contractManager = await wallet.getContractManager();
767
+ const [contractManager, info, reader, broadcaster] = await Promise.all([
768
+ wallet.getContractManager(),
769
+ wallet.getArkadeInfo({ requireLive: true }),
770
+ wallet.getArkadeReader(),
771
+ wallet.getArkadeBroadcaster()
772
+ ]);
705
773
  const client = await import_sdk2.arkade.Arkade.connect({
706
- arkade: new import_sdk2.RestArkProvider(arkServerUrl),
707
- indexer: new import_sdk2.RestIndexerProvider(arkServerUrl),
774
+ // info the wallet already holds, plus its broadcast pair — the shape
775
+ // `ArkadeServerProvider` describes, without a second `/v1/info`
776
+ arkade: { getInfo: async () => info, ...broadcaster },
777
+ indexer: reader,
708
778
  identity: wallet.identity,
709
779
  // registered offers resolve their VTXOs from the contract repository
710
780
  // instead of a direct indexer query; the indexer above stays as the
@@ -718,9 +788,7 @@ async function cancelOffer(wallet, arkServerUrl, offerHex, opts) {
718
788
  const { program, args, keys } = swapProgramBinding(offer, operatorPubkey);
719
789
  const rebuilt = new import_sdk2.arkade.ArkadeProgramScript(program, args, keys);
720
790
  if (import_base2.hex.encode(rebuilt.pkScript) !== import_base2.hex.encode(offer.swapPkScript)) {
721
- throw new Error(
722
- "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"
723
- );
791
+ throw new OfferCovenantMismatchError(import_base2.hex.encode(offer.swapPkScript));
724
792
  }
725
793
  const contract = new import_sdk2.arkade.ArkadeContract(client, program, args, keys);
726
794
  const [vtxos, makerAddress] = await Promise.all([contract.getUtxos(), wallet.getAddress()]);
@@ -730,7 +798,7 @@ async function cancelOffer(wallet, arkServerUrl, offerHex, opts) {
730
798
  );
731
799
  }
732
800
  const vtxo = fundingTxid ? vtxos.find((v) => v.txid === fundingTxid) : vtxos[0];
733
- if (!vtxo) throw new Error("no spendable VTXO at the swap address");
801
+ if (!vtxo) throw new NoSpendableDepositError();
734
802
  const makerPkScript = import_sdk2.ArkAddress.decode(makerAddress).pkScript;
735
803
  const cancel = contract.functions.cancel().from({ txid: vtxo.txid, vout: vtxo.vout, value: vtxo.value }).to(makerPkScript, BigInt(vtxo.value));
736
804
  for (const a of vtxo.assets ?? []) {
@@ -740,20 +808,11 @@ async function cancelOffer(wallet, arkServerUrl, offerHex, opts) {
740
808
  outputs: [{ vout: 0, amount: BigInt(a.amount) }]
741
809
  });
742
810
  }
743
- const swapId = fundingTxid ?? vtxo.txid;
744
- const hasLocalRecord = (await getAssetSwapsOrThrow(repository)).some((s) => s.id === swapId);
745
- if (hasLocalRecord) await updateAssetSwap(repository, swapId, { status: "cancelling" });
746
- const { txid } = await cancel.send();
747
- if (hasLocalRecord) {
748
- const { persisted, swaps } = await updateAssetSwapBestEffort(repository, swapId, {
749
- status: "cancelled",
750
- spentTxid: txid
751
- });
752
- if (persisted) {
753
- await retireOfferContract(contractManager, swaps, import_base2.hex.encode(offer.swapPkScript));
754
- }
755
- }
756
- return txid;
811
+ return {
812
+ offer,
813
+ vtxo: { txid: vtxo.txid, vout: vtxo.vout, value: vtxo.value },
814
+ send: async () => (await cancel.send()).txid
815
+ };
757
816
  }
758
817
 
759
818
  // src/markets.ts
@@ -883,8 +942,9 @@ var validatePlan = (plan, giveBalance, dust) => {
883
942
  // src/repository.ts
884
943
  var marketsCacheKey = (network, registry) => `arkade-intents-markets-${network}-${registry}`;
885
944
  var InMemoryAssetSwapRepository = class {
886
- version = 4;
945
+ version = 5;
887
946
  swaps = /* @__PURE__ */ new Map();
947
+ records = /* @__PURE__ */ new Map();
888
948
  rfqSwaps = /* @__PURE__ */ new Map();
889
949
  scanned = /* @__PURE__ */ new Set();
890
950
  markets = /* @__PURE__ */ new Map();
@@ -906,6 +966,18 @@ var InMemoryAssetSwapRepository = class {
906
966
  async removeRfqSwap(rfqId) {
907
967
  this.rfqSwaps.delete(rfqId);
908
968
  }
969
+ async saveSwapRecord(record) {
970
+ this.records.set(record.id, record);
971
+ }
972
+ async getSwapRecord(id) {
973
+ return this.records.get(id);
974
+ }
975
+ async getAllSwapRecords() {
976
+ return [...this.records.values()];
977
+ }
978
+ async removeSwapRecord(id) {
979
+ this.records.delete(id);
980
+ }
909
981
  async getScannedTxids() {
910
982
  return new Set(this.scanned);
911
983
  }
@@ -921,6 +993,7 @@ var InMemoryAssetSwapRepository = class {
921
993
  async clear() {
922
994
  this.swaps.clear();
923
995
  this.rfqSwaps.clear();
996
+ this.records.clear();
924
997
  this.scanned.clear();
925
998
  this.markets.clear();
926
999
  }
@@ -931,9 +1004,10 @@ var InMemoryAssetSwapRepository = class {
931
1004
  // src/indexedDbRepository.ts
932
1005
  var import_sdk4 = require("@arkade-os/sdk");
933
1006
  var DEFAULT_DB_NAME = "arkade-intents";
934
- var DB_VERSION = 2;
1007
+ var DB_VERSION = 3;
935
1008
  var STORE_SWAPS = "swaps";
936
1009
  var STORE_RFQ_SWAPS = "rfqSwaps";
1010
+ var STORE_SWAP_RECORDS = "swapRecords";
937
1011
  var STORE_SCANNED = "scannedTxids";
938
1012
  var STORE_MARKETS = "markets";
939
1013
  var STORES = [
@@ -941,6 +1015,11 @@ var STORES = [
941
1015
  // v2. Separate from `swaps` rather than sharing it: the two record types
942
1016
  // have different keys and no consumer wants them interleaved.
943
1017
  [STORE_RFQ_SWAPS, { keyPath: "rfqId" }],
1018
+ // v3. The v2 client's accept records, keyed by quote id. Its own store for
1019
+ // the same reason, plus one more: the v1 read path drops a row carrying
1020
+ // neither `offerHex` nor `paymentHash`, so sharing `swaps` would pin the
1021
+ // v2 shape to a v1 predicate.
1022
+ [STORE_SWAP_RECORDS, { keyPath: "id" }],
944
1023
  [STORE_SCANNED],
945
1024
  [STORE_MARKETS]
946
1025
  ];
@@ -952,7 +1031,7 @@ function initDatabase(db, oldVersion, transaction) {
952
1031
  }
953
1032
  }
954
1033
  var IndexedDbAssetSwapRepository = class {
955
- version = 4;
1034
+ version = 5;
956
1035
  connection;
957
1036
  constructor(dbName = DEFAULT_DB_NAME) {
958
1037
  this.connection = (0, import_sdk4.createManagedConnection)(dbName, DB_VERSION, initDatabase);
@@ -996,6 +1075,22 @@ var IndexedDbAssetSwapRepository = class {
996
1075
  store.delete(rfqId);
997
1076
  });
998
1077
  }
1078
+ async saveSwapRecord(record) {
1079
+ await this.write(STORE_SWAP_RECORDS, (store) => {
1080
+ store.put(record);
1081
+ });
1082
+ }
1083
+ async getSwapRecord(id) {
1084
+ return (0, import_sdk4.promisifyRequest)((await this.readStore(STORE_SWAP_RECORDS)).get(id));
1085
+ }
1086
+ async getAllSwapRecords() {
1087
+ return (0, import_sdk4.promisifyRequest)((await this.readStore(STORE_SWAP_RECORDS)).getAll());
1088
+ }
1089
+ async removeSwapRecord(id) {
1090
+ await this.write(STORE_SWAP_RECORDS, (store) => {
1091
+ store.delete(id);
1092
+ });
1093
+ }
999
1094
  async getScannedTxids() {
1000
1095
  const keys = await (0, import_sdk4.promisifyRequest)((await this.readStore(STORE_SCANNED)).getAllKeys());
1001
1096
  return new Set(keys);
@@ -1102,6 +1197,14 @@ var parseSigner = (value) => {
1102
1197
  }
1103
1198
  return { signingDescriptor };
1104
1199
  };
1200
+ var parsePayoutAddress = (value, kind) => {
1201
+ if (typeof value !== "string" || value.length === 0) {
1202
+ throw new Error(
1203
+ `this ${kind} record's claim destination is unusable: expected an Arkade address, got ${JSON.stringify(value)}`
1204
+ );
1205
+ }
1206
+ return value;
1207
+ };
1105
1208
  var parseHashlock = (value) => {
1106
1209
  const raw = value ?? {};
1107
1210
  const hashlock = {
@@ -1147,6 +1250,11 @@ var rfqClaimSecretOf = (record) => {
1147
1250
  );
1148
1251
  }
1149
1252
  };
1253
+ var rfqClaimDestinationOf = (record) => {
1254
+ const handler = rfqCorridorHandlers.getOrThrow(record.kind);
1255
+ if (!handler.claimDestination) return void 0;
1256
+ return parsePayoutAddress(handler.claimDestination(record.profile), record.kind);
1257
+ };
1150
1258
 
1151
1259
  // src/onchainHtlc.ts
1152
1260
  var import_base3 = require("@scure/base");
@@ -1159,6 +1267,7 @@ var MAX_MIN_CONFIRMATIONS = 6;
1159
1267
  var LOCKTIME_THRESHOLD = 5e8;
1160
1268
  var ONCHAIN_SECONDS_PER_BLOCK = 600;
1161
1269
  var ONCHAIN_DUST_SATS = BigInt(330);
1270
+ var ONCHAIN_CLAIM_VSIZE = 152;
1162
1271
  var newPreimage = () => crypto.getRandomValues(new Uint8Array(32));
1163
1272
  var paymentHashOf = (preimage) => import_base3.hex.encode((0, import_sha22.sha256)(preimage));
1164
1273
  var h160FromPaymentHash = (paymentHash) => (0, import_legacy.ripemd160)(import_base3.hex.decode(paymentHash));
@@ -1388,7 +1497,7 @@ var LightningReceiveCorridor = {
1388
1497
  const receive = swap;
1389
1498
  return {
1390
1499
  expectedAmount: receive.expectedAmount,
1391
- ...receive.claimArkTxid ? { claimArkTxid: receive.claimArkTxid } : {}
1500
+ ...receive.claimTxid ? { claimTxid: receive.claimTxid } : {}
1392
1501
  };
1393
1502
  },
1394
1503
  hydrate(profile) {
@@ -1398,13 +1507,16 @@ var LightningReceiveCorridor = {
1398
1507
  return {
1399
1508
  ...hydrateHashlock(profile),
1400
1509
  expectedAmount: profile.expectedAmount,
1401
- ...profile.claimArkTxid ? { claimArkTxid: profile.claimArkTxid } : {}
1510
+ ...profile.claimTxid ? { claimTxid: profile.claimTxid } : {}
1402
1511
  };
1403
1512
  },
1404
1513
  // We are the claimant here, so the preimage material on the hashlock is
1405
1514
  // ours to use.
1406
1515
  claimSecret: (profile) => ({ ...profile.signer, ...profile.hashlock }),
1407
- activityTxids: (profile) => profile.claimArkTxid ? [profile.claimArkTxid] : []
1516
+ // The other half of claiming: where it pays. Written once at request time
1517
+ // and left alone by `project` above, so this hands back what was stored.
1518
+ claimDestination: (profile) => profile.payoutAddress,
1519
+ activityTxids: (profile) => profile.claimTxid ? [profile.claimTxid] : []
1408
1520
  };
1409
1521
  function onchainSendProfile(result) {
1410
1522
  return {
@@ -1492,14 +1604,34 @@ var isRfqSwapTerminal = (state) => RFQ_SWAP_TERMINAL_STATES.includes(state);
1492
1604
 
1493
1605
  // src/rfqRecord.ts
1494
1606
  var RFQ_SWAP_RETENTION_SECONDS = 30 * 24 * 60 * 60;
1607
+ function renameLegacyClaimTxid(profile) {
1608
+ const { claimArkTxid, ...rest } = profile;
1609
+ return { ...rest, claimTxid: rest.claimTxid ?? claimArkTxid };
1610
+ }
1611
+ function normalizeRfqSwapRecord(record) {
1612
+ const { fundingArkTxid, refundArkTxid, lockupSpendArkTxids, ...rest } = record;
1613
+ const legacyClaim = record.kind === "lightning_receive" && record.profile.claimArkTxid !== void 0;
1614
+ if (!fundingArkTxid && !refundArkTxid && !lockupSpendArkTxids && !legacyClaim) return record;
1615
+ const fundingTxid = rest.fundingTxid ?? fundingArkTxid;
1616
+ const refundTxid = rest.refundTxid ?? refundArkTxid;
1617
+ const lockupSpendTxids = rest.lockupSpendTxids ?? lockupSpendArkTxids;
1618
+ return {
1619
+ ...rest,
1620
+ ...fundingTxid ? { fundingTxid } : {},
1621
+ ...refundTxid ? { refundTxid } : {},
1622
+ ...lockupSpendTxids?.length ? { lockupSpendTxids } : {},
1623
+ ...legacyClaim ? { profile: renameLegacyClaimTxid(record.profile) } : {}
1624
+ };
1625
+ }
1495
1626
  var managerState = (swap) => ({
1496
1627
  rfqId: swap.rfqId,
1497
1628
  state: swap.state,
1498
1629
  createdAt: swap.createdAt,
1499
1630
  updatedAt: swap.updatedAt,
1500
- ...swap.refundArkTxid ? { refundArkTxid: swap.refundArkTxid } : {},
1501
- ...swap.lockupSpendArkTxids?.length ? { lockupSpendArkTxids: [...swap.lockupSpendArkTxids] } : {},
1631
+ ...swap.refundTxid ? { refundTxid: swap.refundTxid } : {},
1632
+ ...swap.lockupSpendTxids?.length ? { lockupSpendTxids: [...swap.lockupSpendTxids] } : {},
1502
1633
  ...swap.failure ? { failure: swap.failure } : {},
1634
+ ...swap.claimFailure ? { claimFailure: swap.claimFailure } : {},
1503
1635
  ...swap.blockedReason ? { blockedReason: swap.blockedReason } : {}
1504
1636
  });
1505
1637
  function assertSameSwap(origin, swap) {
@@ -1529,27 +1661,30 @@ function createRfqSwapRecord(origin, swap) {
1529
1661
  }
1530
1662
  function updateRfqSwapRecord(record, swap) {
1531
1663
  assertSameSwap(record, swap);
1664
+ const stored = normalizeRfqSwapRecord(record);
1532
1665
  const {
1533
- refundArkTxid: _refundArkTxid,
1534
- lockupSpendArkTxids: _lockupSpendArkTxids,
1666
+ refundTxid: _refundTxid,
1667
+ lockupSpendTxids: _lockupSpendTxids,
1535
1668
  failure: _failure,
1669
+ claimFailure: _claimFailure,
1536
1670
  blockedReason: _blockedReason,
1537
1671
  ...origin
1538
- } = record;
1539
- const handler = rfqCorridorHandlers.getOrThrow(record.kind);
1672
+ } = stored;
1673
+ const handler = rfqCorridorHandlers.getOrThrow(stored.kind);
1540
1674
  return {
1541
1675
  ...origin,
1542
1676
  ...managerState(swap),
1543
- profile: { ...record.profile, ...handler.project(swap) }
1677
+ profile: { ...stored.profile, ...handler.project(swap) }
1544
1678
  };
1545
1679
  }
1546
1680
  function rfqSwapOriginOf(record) {
1681
+ const stored = normalizeRfqSwapRecord(record);
1547
1682
  return {
1548
- kind: record.kind,
1549
- lockupAddress: record.lockupAddress,
1550
- profile: { ...record.profile },
1551
- ...record.amount !== void 0 ? { amount: record.amount } : {},
1552
- ...record.fundingArkTxid ? { fundingArkTxid: record.fundingArkTxid } : {}
1683
+ kind: stored.kind,
1684
+ lockupAddress: stored.lockupAddress,
1685
+ profile: { ...stored.profile },
1686
+ ...stored.amount !== void 0 ? { amount: stored.amount } : {},
1687
+ ...stored.fundingTxid ? { fundingTxid: stored.fundingTxid } : {}
1553
1688
  };
1554
1689
  }
1555
1690
  function lockupScript(params, lockupAddress) {
@@ -1563,27 +1698,29 @@ function lockupScript(params, lockupAddress) {
1563
1698
  return script;
1564
1699
  }
1565
1700
  function rebuildRfqSwap(record, params) {
1566
- const script = lockupScript(params, record.lockupAddress);
1701
+ const stored = normalizeRfqSwapRecord(record);
1702
+ const script = lockupScript(params, stored.lockupAddress);
1567
1703
  const common = {
1568
- rfqId: record.rfqId,
1569
- state: record.state,
1704
+ rfqId: stored.rfqId,
1705
+ state: stored.state,
1570
1706
  lockupPkScript: script.pkScript,
1571
- lockup: { script, address: record.lockupAddress },
1707
+ lockup: { script, address: stored.lockupAddress },
1572
1708
  // From the covenant, which binds it: the record's own copy would be a
1573
1709
  // second source for the deadline the refund is gated on.
1574
1710
  refundLocktime: Number(script.options.refundLocktime),
1575
- createdAt: record.createdAt,
1576
- updatedAt: record.updatedAt,
1577
- ...record.refundArkTxid ? { refundArkTxid: record.refundArkTxid } : {},
1578
- ...record.lockupSpendArkTxids?.length ? { lockupSpendArkTxids: [...record.lockupSpendArkTxids] } : {},
1579
- ...record.failure ? { failure: record.failure } : {},
1580
- ...record.blockedReason ? { blockedReason: record.blockedReason } : {}
1711
+ createdAt: stored.createdAt,
1712
+ updatedAt: stored.updatedAt,
1713
+ ...stored.refundTxid ? { refundTxid: stored.refundTxid } : {},
1714
+ ...stored.lockupSpendTxids?.length ? { lockupSpendTxids: [...stored.lockupSpendTxids] } : {},
1715
+ ...stored.failure ? { failure: stored.failure } : {},
1716
+ ...stored.claimFailure ? { claimFailure: stored.claimFailure } : {},
1717
+ ...stored.blockedReason ? { blockedReason: stored.blockedReason } : {}
1581
1718
  };
1582
- const handler = rfqCorridorHandlers.getOrThrow(record.kind);
1719
+ const handler = rfqCorridorHandlers.getOrThrow(stored.kind);
1583
1720
  return {
1584
1721
  ...common,
1585
- kind: record.kind,
1586
- ...handler.hydrate(record.profile, { lockup: script })
1722
+ kind: stored.kind,
1723
+ ...handler.hydrate(stored.profile, { lockup: script })
1587
1724
  };
1588
1725
  }
1589
1726
  function shouldRetainRfqSwap(record, now) {
@@ -1620,10 +1757,10 @@ async function fetchParsedTxs(indexer, txids) {
1620
1757
  var unscannedSwapCandidates = (txs, existingIds, scanned) => txs.filter(
1621
1758
  (tx) => tx.type === "sent" && tx.redeemTxid && !existingIds.has(tx.redeemTxid) && !scanned.has(tx.redeemTxid)
1622
1759
  );
1623
- function classifySpend(offer, serverPubkey, spendTx, deposit) {
1760
+ function classifySpend(offer, operatorPubkey, spendTx, deposit) {
1624
1761
  let leaves;
1625
1762
  try {
1626
- const script = offerVtxoScript(offer, serverPubkey);
1763
+ const script = offerContract(offer, operatorPubkey);
1627
1764
  if (import_base6.hex.encode(script.pkScript) !== import_base6.hex.encode(offer.swapPkScript)) return "indeterminate";
1628
1765
  leaves = {
1629
1766
  // both routes that hand the deposit back; `exit` is absent on an
@@ -1647,15 +1784,15 @@ function classifySpend(offer, serverPubkey, spendTx, deposit) {
1647
1784
  return "indeterminate";
1648
1785
  }
1649
1786
  var spendTxidsOf = (vtxo) => [vtxo.spentBy, vtxo.arkTxId].filter((id) => Boolean(id));
1650
- function classifyDepositSpend(offer, serverPubkey, spendTxs, deposit) {
1787
+ function classifyDepositSpend(offer, operatorPubkey, spendTxs, deposit) {
1651
1788
  for (const tx of spendTxs) {
1652
- const kind = classifySpend(offer, serverPubkey, tx, deposit);
1789
+ const kind = classifySpend(offer, operatorPubkey, tx, deposit);
1653
1790
  if (kind !== "indeterminate") return kind;
1654
1791
  }
1655
1792
  return "indeterminate";
1656
1793
  }
1657
1794
  async function restoreAssetSwaps(indexer, txs, existingIds, opts) {
1658
- const { serverPubkey, scanned = /* @__PURE__ */ new Set() } = opts;
1795
+ const { operatorPubkey, scanned = /* @__PURE__ */ new Set() } = opts;
1659
1796
  const candidates = unscannedSwapCandidates(txs, existingIds, scanned);
1660
1797
  if (candidates.length === 0) return { restored: [], scannedTxids: [] };
1661
1798
  const byTxid = new Map(candidates.map((tx) => [tx.redeemTxid, tx]));
@@ -1724,7 +1861,7 @@ async function restoreAssetSwaps(indexer, txs, existingIds, opts) {
1724
1861
  if (state === "swept") status = "recoverable";
1725
1862
  else if (state === "spent") {
1726
1863
  const spendTxs = spendTxidsOf(vtxo).map((id) => spendTxByTxid.get(id)).filter((tx) => tx !== void 0);
1727
- const kind = classifyDepositSpend(offer, serverPubkey, spendTxs, {
1864
+ const kind = classifyDepositSpend(offer, operatorPubkey, spendTxs, {
1728
1865
  txid: vtxo.txid,
1729
1866
  vout: vtxo.vout
1730
1867
  });
@@ -1741,7 +1878,7 @@ async function restoreAssetSwaps(indexer, txs, existingIds, opts) {
1741
1878
  fromAmount,
1742
1879
  toAmount: offer.wantAmount.toString(),
1743
1880
  // ponytail(arkade-os/ts-sdk#680): empty address makes cancel fall back
1744
- // to the current server key; store the funded address if server-key
1881
+ // to the current operator key; store the funded address if operator-key
1745
1882
  // rotations become real (cancelOffer now at least diagnoses the
1746
1883
  // mismatch instead of reporting a missing VTXO)
1747
1884
  swapAddress: "",
@@ -1763,6 +1900,10 @@ async function restoreAssetSwaps(indexer, txs, existingIds, opts) {
1763
1900
  var import_base7 = require("@scure/base");
1764
1901
  var import_sdk7 = require("@arkade-os/sdk");
1765
1902
  var TERMINAL = ["fulfilled", "cancelled", "recoverable"];
1903
+ var assetSwapSource = (repository) => ({
1904
+ list: () => getAssetSwaps(repository),
1905
+ apply: async (swap, changes) => updateAssetSwapBestEffort(repository, swap.id, changes)
1906
+ });
1766
1907
  function spendUpdate(swap, spend) {
1767
1908
  if (TERMINAL.includes(swap.status)) return void 0;
1768
1909
  if (spend.kind === "indeterminate") return void 0;
@@ -1776,13 +1917,20 @@ function spendUpdate(swap, spend) {
1776
1917
  }
1777
1918
  async function watchOfferSwaps({
1778
1919
  wallet,
1779
- arkServerUrl,
1780
1920
  repository,
1921
+ source,
1781
1922
  onUpdate
1782
1923
  }) {
1783
- const manager = await wallet.getContractManager();
1784
- const serverPubkey = import_sdk7.ArkAddress.decode(await wallet.getAddress()).serverPubKey;
1785
- const indexer = new import_sdk7.RestIndexerProvider(arkServerUrl);
1924
+ if (!source && !repository) {
1925
+ throw new Error("watchOfferSwaps needs either a record source or a repository");
1926
+ }
1927
+ const records = source ?? assetSwapSource(repository);
1928
+ const [manager, address, indexer] = await Promise.all([
1929
+ wallet.getContractManager(),
1930
+ wallet.getAddress(),
1931
+ wallet.getArkadeReader()
1932
+ ]);
1933
+ const operatorPubkey = import_sdk7.ArkAddress.decode(address).serverPubKey;
1786
1934
  let queue = Promise.resolve();
1787
1935
  const enqueue = (task) => {
1788
1936
  queue = queue.then(task).catch(() => {
@@ -1796,7 +1944,7 @@ async function watchOfferSwaps({
1796
1944
  const { txs } = await indexer.getVirtualTxs(candidates);
1797
1945
  return classifyDepositSpend(
1798
1946
  decodeOffer(import_base7.hex.decode(swap.offerHex)),
1799
- serverPubkey,
1947
+ operatorPubkey,
1800
1948
  txs.map((psbt) => import_sdk7.Transaction.fromPSBT(import_base7.base64.decode(psbt))),
1801
1949
  { txid: vtxo.txid, vout: vtxo.vout }
1802
1950
  );
@@ -1809,20 +1957,16 @@ async function watchOfferSwaps({
1809
1957
  for (const vtxo of event.vtxos) {
1810
1958
  const spentTxid = vtxo.arkTxId || vtxo.spentBy;
1811
1959
  if (!spentTxid) continue;
1812
- const swap = (await getAssetSwaps(repository)).find(
1960
+ const swap = (await records.list()).find(
1813
1961
  (s) => s.fundingTxid === vtxo.txid && s.swapPkScript === event.contractScript
1814
1962
  );
1815
1963
  if (!swap) continue;
1816
1964
  const kind = await classify(swap, vtxo, spentTxid);
1817
1965
  const changes = spendUpdate(swap, { txid: spentTxid, kind, at: event.timestamp });
1818
1966
  if (!changes) continue;
1819
- const { persisted, swaps } = await updateAssetSwapBestEffort(
1820
- repository,
1821
- swap.id,
1822
- changes
1823
- );
1967
+ const { persisted, swaps } = await records.apply(swap, changes);
1824
1968
  if (!persisted) continue;
1825
- onUpdate?.({ ...swap, ...changes });
1969
+ onUpdate?.(swaps.find((s) => s.id === swap.id) ?? swap);
1826
1970
  if (changes.status && RETIRABLE.includes(changes.status)) {
1827
1971
  await retireOfferContract(manager, swaps, event.contractScript);
1828
1972
  }
@@ -1950,11 +2094,14 @@ var ONCHAIN_SEND_PAIR = rfqPair(ARKADE_BTC, ONCHAIN_BTC);
1950
2094
  var ONCHAIN_RECEIVE_PAIR = rfqPair(ONCHAIN_BTC, ARKADE_BTC);
1951
2095
  var RFQ_TERMINAL_STATES = ["settled", "refused", "expired", "refunded", "stuck"];
1952
2096
  var SwapRefusal = class extends Error {
2097
+ /** Literal-typed so the v2 error taxonomy's union discriminates on `name`
2098
+ * — a `string` here collapses the discriminant for every member. Same value
2099
+ * the constructor has always set, moved to a field initializer. */
2100
+ name = "SwapRefusal";
1953
2101
  reason;
1954
2102
  rfqId;
1955
2103
  constructor(reason, rfqId) {
1956
2104
  super(`solver refused: ${reason}`);
1957
- this.name = "SwapRefusal";
1958
2105
  this.reason = reason;
1959
2106
  this.rfqId = rfqId;
1960
2107
  }
@@ -2055,6 +2202,10 @@ var assertFundable = (input) => {
2055
2202
  const fail = (reason, message) => {
2056
2203
  throw gateError(reason, message);
2057
2204
  };
2205
+ if (input.quote.valid_until === void 0) {
2206
+ fail("quote_malformed", "quote carries no valid_until");
2207
+ }
2208
+ assertFinite(input.quote.valid_until, "quote_malformed", "quote valid_until");
2058
2209
  if (input.invoiceExpiresAt !== void 0 && input.now >= input.invoiceExpiresAt) {
2059
2210
  fail("invoice_expired", "invoice expired");
2060
2211
  }
@@ -2257,22 +2408,22 @@ var relayTransport = (relayUrl, options) => {
2257
2408
  };
2258
2409
  var SEQUENCE_GRANULARITY_SECONDS = 512;
2259
2410
  var SOLO_REFUND_HEADROOM_SECONDS = 8 * SEQUENCE_GRANULARITY_SECONDS;
2260
- var unilateralClaimDelay = (serverExitDelaySeconds) => {
2261
- if (!Number.isFinite(serverExitDelaySeconds) || serverExitDelaySeconds < SEQUENCE_GRANULARITY_SECONDS) {
2411
+ var unilateralClaimDelay = (operatorExitDelaySeconds) => {
2412
+ if (!Number.isFinite(operatorExitDelaySeconds) || operatorExitDelaySeconds < SEQUENCE_GRANULARITY_SECONDS) {
2262
2413
  throw new Error(
2263
- `server exit delay must be at least ${SEQUENCE_GRANULARITY_SECONDS}s of seconds, got ${serverExitDelaySeconds}`
2414
+ `operator exit delay must be at least ${SEQUENCE_GRANULARITY_SECONDS}s of seconds, got ${operatorExitDelaySeconds}`
2264
2415
  );
2265
2416
  }
2266
- if (serverExitDelaySeconds > 65535 * SEQUENCE_GRANULARITY_SECONDS - SOLO_REFUND_HEADROOM_SECONDS) {
2417
+ if (operatorExitDelaySeconds > 65535 * SEQUENCE_GRANULARITY_SECONDS - SOLO_REFUND_HEADROOM_SECONDS) {
2267
2418
  throw new Error(
2268
- `server exit delay ${serverExitDelaySeconds}s exceeds what BIP68 can encode once the solo refund's headroom is stacked above it`
2419
+ `operator exit delay ${operatorExitDelaySeconds}s exceeds what BIP68 can encode once the solo refund's headroom is stacked above it`
2269
2420
  );
2270
2421
  }
2271
- return Math.ceil(serverExitDelaySeconds / SEQUENCE_GRANULARITY_SECONDS) * SEQUENCE_GRANULARITY_SECONDS;
2422
+ return Math.ceil(operatorExitDelaySeconds / SEQUENCE_GRANULARITY_SECONDS) * SEQUENCE_GRANULARITY_SECONDS;
2272
2423
  };
2273
2424
  var unilateralRefundDelay = (claimDelay) => claimDelay;
2274
2425
  var unilateralRefundWithoutReceiverDelay = (claimDelay) => claimDelay + SOLO_REFUND_HEADROOM_SECONDS;
2275
- function lightningSendVtxoScript(params) {
2426
+ function lightningSendContract(params) {
2276
2427
  const seconds = (value) => ({
2277
2428
  type: "seconds",
2278
2429
  value: BigInt(value)
@@ -2280,7 +2431,7 @@ function lightningSendVtxoScript(params) {
2280
2431
  return new import_sdk9.VHTLC.ScriptV2({
2281
2432
  sender: params.senderPubkey,
2282
2433
  receiver: params.solverPubkey,
2283
- server: params.serverPubkey,
2434
+ server: params.operatorPubkey,
2284
2435
  preimageHash: (0, import_legacy2.ripemd160)(import_base10.hex.decode(params.paymentHash)),
2285
2436
  refundLocktime: BigInt(params.refundLocktime),
2286
2437
  unilateralClaimDelay: seconds(params.claimDelay),
@@ -2296,15 +2447,8 @@ function lightningSendVtxoScript(params) {
2296
2447
  }
2297
2448
  });
2298
2449
  }
2299
- async function requestLightningSend(wallet, arkServerUrl, transport, params) {
2300
- const rfqId = params.rfqId ?? newRfqId();
2301
- const secrets = await (0, import_sdk10.provisionRefundKey)(wallet);
2302
- const senderPubkey = secrets.pubkey;
2303
- const refundAddress = secrets.address;
2304
- const info = await new import_sdk9.RestArkProvider(arkServerUrl).getInfo();
2305
- const quote = await transport.requestQuote(
2306
- lightningSendRequest({ rfqId, invoice: params.invoice.raw, refundAddress, senderPubkey })
2307
- );
2450
+ function deriveLightningSend(input) {
2451
+ const { quote } = input;
2308
2452
  if (quote.refund_locktime === void 0) {
2309
2453
  throw new Error("lightning-send quote is missing refund_locktime");
2310
2454
  }
@@ -2312,6 +2456,43 @@ async function requestLightningSend(wallet, arkServerUrl, transport, params) {
2312
2456
  if (receiverPkScriptHex === void 0) {
2313
2457
  throw new Error("lightning-send quote is missing profile.receiver_pk_script");
2314
2458
  }
2459
+ const contractParams = {
2460
+ solverPubkey: (0, import_sdk9.toXOnly)(import_base10.hex.decode(quote.solver_pubkey), "solver key"),
2461
+ refundLocktime: quote.refund_locktime,
2462
+ operatorPubkey: input.operatorPubkey,
2463
+ paymentHash: input.paymentHash,
2464
+ claimDelay: input.claimDelay,
2465
+ emulatorPubkey: input.emulatorPubkey,
2466
+ senderPubkey: input.senderPubkey,
2467
+ receiverPkScript: solverHex(receiverPkScriptHex, "profile.receiver_pk_script"),
2468
+ refundPkScript: input.refundPkScript
2469
+ };
2470
+ const matched = matchQuotedLockup(
2471
+ quote,
2472
+ input.hrp,
2473
+ input.operatorPubkey,
2474
+ (legacy) => lightningSendContract({ ...contractParams, ...legacy !== void 0 && { legacy } })
2475
+ );
2476
+ return {
2477
+ address: matched.address,
2478
+ swapPkScript: matched.script.pkScript,
2479
+ script: matched.script,
2480
+ contractParams: {
2481
+ ...contractParams,
2482
+ ...matched.legacy !== void 0 && { legacy: matched.legacy }
2483
+ },
2484
+ refundLocktime: quote.refund_locktime
2485
+ };
2486
+ }
2487
+ async function requestLightningSend(wallet, transport, params) {
2488
+ const rfqId = params.rfqId ?? newRfqId();
2489
+ const secrets = await (0, import_sdk10.provisionRefundKey)(wallet);
2490
+ const senderPubkey = secrets.pubkey;
2491
+ const refundAddress = secrets.address;
2492
+ const info = await wallet.getArkadeInfo({ requireLive: true });
2493
+ const quote = await transport.requestQuote(
2494
+ lightningSendRequest({ rfqId, invoice: params.invoice.raw, refundAddress, senderPubkey })
2495
+ );
2315
2496
  if (quote.to_amount !== params.invoice.amountSats) {
2316
2497
  throw new Error(
2317
2498
  `quote to_amount ${quote.to_amount} does not match the invoice's ${params.invoice.amountSats}`
@@ -2322,34 +2503,22 @@ async function requestLightningSend(wallet, arkServerUrl, transport, params) {
2322
2503
  `quote from_amount ${quote.from_amount} is below the invoice amount \u2014 a negative spread is not a quote`
2323
2504
  );
2324
2505
  }
2325
- const serverPubkey = (0, import_sdk9.toXOnly)(import_base10.hex.decode(info.signerPubkey), "ark signer key");
2326
- const network = (0, import_sdk9.getNetwork)(info.network);
2327
- const treeParams = {
2328
- solverPubkey: (0, import_sdk9.toXOnly)(import_base10.hex.decode(quote.solver_pubkey), "solver key"),
2329
- refundLocktime: quote.refund_locktime,
2330
- serverPubkey,
2506
+ const operatorPubkey = (0, import_sdk9.toXOnly)(import_base10.hex.decode(info.signerPubkey), "ark signer key");
2507
+ const network = (0, import_sdk9.networkFromArkadeInfo)(info);
2508
+ const derived = deriveLightningSend({
2509
+ quote,
2331
2510
  paymentHash: params.invoice.paymentHash,
2332
- claimDelay: unilateralClaimDelay(Number(info.unilateralExitDelay)),
2511
+ senderPubkey,
2512
+ refundPkScript: secrets.pkScript,
2513
+ operatorPubkey,
2333
2514
  emulatorPubkey: (0, import_sdk9.toXOnly)(
2334
2515
  import_base10.hex.decode((0, import_sdk9.resolveEmulatorPubkey)(network, params.emulatorPubkey)),
2335
2516
  "emulator signer key"
2336
2517
  ),
2337
- senderPubkey,
2338
- receiverPkScript: solverHex(receiverPkScriptHex, "profile.receiver_pk_script"),
2339
- refundPkScript: secrets.pkScript
2340
- };
2341
- const matched = matchQuotedLockup(
2342
- quote,
2343
- network.hrp,
2344
- serverPubkey,
2345
- (legacy) => lightningSendVtxoScript({ ...treeParams, ...legacy !== void 0 && { legacy } })
2346
- );
2347
- const script = matched.script;
2348
- const address = matched.address;
2349
- const matchedTreeParams = {
2350
- ...treeParams,
2351
- ...matched.legacy !== void 0 && { legacy: matched.legacy }
2352
- };
2518
+ claimDelay: unilateralClaimDelay(Number(info.unilateralExitDelay)),
2519
+ hrp: network.hrp
2520
+ });
2521
+ const { address, script, contractParams } = derived;
2353
2522
  assertFundable({
2354
2523
  quote,
2355
2524
  invoiceExpiresAt: params.invoice.expiresAt,
@@ -2363,12 +2532,12 @@ async function requestLightningSend(wallet, arkServerUrl, transport, params) {
2363
2532
  // What the lockup must carry: the quote's `from_amount` — the invoice
2364
2533
  // PLUS the corridor's fee, never the bare invoice amount.
2365
2534
  fundAmount: quote.from_amount,
2366
- swapPkScript: script.pkScript,
2535
+ swapPkScript: derived.swapPkScript,
2367
2536
  script,
2368
2537
  refundAddress,
2369
2538
  senderPubkey,
2370
2539
  secrets,
2371
- treeParams: matchedTreeParams
2540
+ contractParams
2372
2541
  };
2373
2542
  }
2374
2543
  var offerTermsFromQuote = (quote, assets) => {
@@ -2433,10 +2602,10 @@ function deriveOnchainSend(input) {
2433
2602
  if (refundLocktime === void 0 || htlcPubkey === void 0 || htlcLocktime === void 0 || minConfirmations === void 0 || receiverPkScriptHex === void 0) {
2434
2603
  throw new Error("onchain-send quote is missing a binding field");
2435
2604
  }
2436
- const treeParams = {
2605
+ const contractParams = {
2437
2606
  solverPubkey: (0, import_sdk9.toXOnly)(import_base10.hex.decode(quote.solver_pubkey), "solver key"),
2438
2607
  refundLocktime,
2439
- serverPubkey: input.serverPubkey,
2608
+ operatorPubkey: input.operatorPubkey,
2440
2609
  paymentHash: input.paymentHash,
2441
2610
  claimDelay: input.claimDelay,
2442
2611
  emulatorPubkey: input.emulatorPubkey,
@@ -2447,8 +2616,8 @@ function deriveOnchainSend(input) {
2447
2616
  const { script, address } = matchQuotedLockup(
2448
2617
  quote,
2449
2618
  input.hrp,
2450
- input.serverPubkey,
2451
- (legacy) => lightningSendVtxoScript({ ...treeParams, ...legacy !== void 0 && { legacy } })
2619
+ input.operatorPubkey,
2620
+ (legacy) => lightningSendContract({ ...contractParams, ...legacy !== void 0 && { legacy } })
2452
2621
  );
2453
2622
  const htlcParams = {
2454
2623
  paymentHash: input.paymentHash,
@@ -2470,7 +2639,7 @@ function deriveOnchainSend(input) {
2470
2639
  minConfirmations
2471
2640
  };
2472
2641
  }
2473
- async function requestOnchainSend(wallet, arkServerUrl, transport, params) {
2642
+ async function requestOnchainSend(wallet, transport, params) {
2474
2643
  const rfqId = params.rfqId ?? newRfqId();
2475
2644
  const secrets = await (0, import_sdk10.provisionClaimSecret)(wallet, { preimage: params.preimage });
2476
2645
  if (secrets.mustPersistPreimage) {
@@ -2481,7 +2650,7 @@ async function requestOnchainSend(wallet, arkServerUrl, transport, params) {
2481
2650
  const paymentHash = import_base10.hex.encode(secrets.paymentHash);
2482
2651
  const senderPubkey = secrets.pubkey;
2483
2652
  const [info, refundAddress] = await Promise.all([
2484
- new import_sdk9.RestArkProvider(arkServerUrl).getInfo(),
2653
+ wallet.getArkadeInfo({ requireLive: true }),
2485
2654
  wallet.getAddress()
2486
2655
  ]);
2487
2656
  const quote = await transport.requestQuote(
@@ -2496,12 +2665,12 @@ async function requestOnchainSend(wallet, arkServerUrl, transport, params) {
2496
2665
  })
2497
2666
  );
2498
2667
  assertQuotedAmount(quote, params.amountSide, params.amount);
2499
- const network = (0, import_sdk9.getNetwork)(info.network);
2668
+ const network = (0, import_sdk9.networkFromArkadeInfo)(info);
2500
2669
  const derived = deriveOnchainSend({
2501
2670
  quote,
2502
2671
  paymentHash,
2503
2672
  payoutPubkey: params.payoutPubkey,
2504
- serverPubkey: (0, import_sdk9.toXOnly)(import_base10.hex.decode(info.signerPubkey), "ark signer key"),
2673
+ operatorPubkey: (0, import_sdk9.toXOnly)(import_base10.hex.decode(info.signerPubkey), "ark signer key"),
2505
2674
  emulatorPubkey: (0, import_sdk9.toXOnly)(
2506
2675
  import_base10.hex.decode((0, import_sdk9.resolveEmulatorPubkey)(network, params.emulatorPubkey)),
2507
2676
  "emulator signer key"
@@ -2538,6 +2707,7 @@ async function requestOnchainSend(wallet, arkServerUrl, transport, params) {
2538
2707
  htlcParams: derived.htlcParams,
2539
2708
  l1Network: derived.l1Network,
2540
2709
  minConfirmations: derived.minConfirmations,
2710
+ refundLocktime: derived.refundLocktime,
2541
2711
  senderPubkey,
2542
2712
  secrets
2543
2713
  };
@@ -2609,7 +2779,7 @@ var assertReceivable = (input) => {
2609
2779
  );
2610
2780
  }
2611
2781
  };
2612
- function receiveVtxoScript(params) {
2782
+ function lightningReceiveContract(params) {
2613
2783
  const seconds = (value) => ({
2614
2784
  type: "seconds",
2615
2785
  value: BigInt(value)
@@ -2617,7 +2787,7 @@ function receiveVtxoScript(params) {
2617
2787
  return new import_sdk9.VHTLC.ScriptV2({
2618
2788
  sender: params.solverPubkey,
2619
2789
  receiver: params.payoutPubkey,
2620
- server: params.serverPubkey,
2790
+ server: params.operatorPubkey,
2621
2791
  preimageHash: (0, import_legacy2.ripemd160)(import_base10.hex.decode(params.paymentHash)),
2622
2792
  refundLocktime: BigInt(params.refundLocktime),
2623
2793
  unilateralClaimDelay: seconds(params.claimDelay),
@@ -2642,10 +2812,10 @@ function deriveLightningReceive(input) {
2642
2812
  if (refundLocktime === void 0 || invoice === void 0 || solverRefundPkScriptHex === void 0) {
2643
2813
  throw new Error("lightning-receive quote is missing a binding field");
2644
2814
  }
2645
- const treeParams = {
2815
+ const contractParams = {
2646
2816
  solverPubkey: (0, import_sdk9.toXOnly)(import_base10.hex.decode(quote.solver_pubkey), "solver key"),
2647
2817
  refundLocktime,
2648
- serverPubkey: input.serverPubkey,
2818
+ operatorPubkey: input.operatorPubkey,
2649
2819
  paymentHash: input.paymentHash,
2650
2820
  claimDelay: input.claimDelay,
2651
2821
  emulatorPubkey: input.emulatorPubkey,
@@ -2656,8 +2826,8 @@ function deriveLightningReceive(input) {
2656
2826
  const matched = matchQuotedLockup(
2657
2827
  quote,
2658
2828
  input.hrp,
2659
- input.serverPubkey,
2660
- (legacy) => receiveVtxoScript({ ...treeParams, ...legacy !== void 0 && { legacy } })
2829
+ input.operatorPubkey,
2830
+ (legacy) => lightningReceiveContract({ ...contractParams, ...legacy !== void 0 && { legacy } })
2661
2831
  );
2662
2832
  return {
2663
2833
  address: matched.address,
@@ -2665,13 +2835,13 @@ function deriveLightningReceive(input) {
2665
2835
  script: matched.script,
2666
2836
  invoice,
2667
2837
  refundLocktime,
2668
- treeParams: {
2669
- ...treeParams,
2838
+ contractParams: {
2839
+ ...contractParams,
2670
2840
  ...matched.legacy !== void 0 && { legacy: matched.legacy }
2671
2841
  }
2672
2842
  };
2673
2843
  }
2674
- async function requestLightningReceive(wallet, arkServerUrl, transport, params) {
2844
+ async function requestLightningReceive(wallet, transport, params) {
2675
2845
  const rfqId = params.rfqId ?? newRfqId();
2676
2846
  const secrets = await (0, import_sdk10.provisionClaimSecret)(wallet);
2677
2847
  if (secrets.mustPersistPreimage) {
@@ -2683,7 +2853,7 @@ async function requestLightningReceive(wallet, arkServerUrl, transport, params)
2683
2853
  const paymentHash = import_base10.hex.encode(secrets.paymentHash);
2684
2854
  const payoutPubkey = secrets.pubkey;
2685
2855
  const [info, payoutAddress] = await Promise.all([
2686
- new import_sdk9.RestArkProvider(arkServerUrl).getInfo(),
2856
+ wallet.getArkadeInfo({ requireLive: true }),
2687
2857
  wallet.getAddress()
2688
2858
  ]);
2689
2859
  const claimPacket = await sealClaimPacket({
@@ -2702,13 +2872,13 @@ async function requestLightningReceive(wallet, arkServerUrl, transport, params)
2702
2872
  })
2703
2873
  );
2704
2874
  assertQuotedAmount(quote, params.amountSide, params.amount);
2705
- const network = (0, import_sdk9.getNetwork)(info.network);
2875
+ const network = (0, import_sdk9.networkFromArkadeInfo)(info);
2706
2876
  const derived = deriveLightningReceive({
2707
2877
  quote,
2708
2878
  paymentHash,
2709
2879
  payoutPubkey,
2710
2880
  payoutAddress,
2711
- serverPubkey: (0, import_sdk9.toXOnly)(import_base10.hex.decode(info.signerPubkey), "ark signer key"),
2881
+ operatorPubkey: (0, import_sdk9.toXOnly)(import_base10.hex.decode(info.signerPubkey), "ark signer key"),
2712
2882
  emulatorPubkey: (0, import_sdk9.toXOnly)(
2713
2883
  import_base10.hex.decode((0, import_sdk9.resolveEmulatorPubkey)(network, params.emulatorPubkey)),
2714
2884
  "emulator signer key"
@@ -2742,7 +2912,7 @@ async function requestLightningReceive(wallet, arkServerUrl, transport, params)
2742
2912
  payoutAddress,
2743
2913
  payoutPubkey,
2744
2914
  secrets,
2745
- treeParams: derived.treeParams
2915
+ contractParams: derived.contractParams
2746
2916
  };
2747
2917
  }
2748
2918
  function deriveOnchainReceive(input) {
@@ -2757,10 +2927,10 @@ function deriveOnchainReceive(input) {
2757
2927
  if (refundLocktime === void 0 || claimPubkey === void 0 || htlcLocktime === void 0 || minConfirmations === void 0 || solverRefundPkScriptHex === void 0) {
2758
2928
  throw new Error("onchain-receive quote is missing a binding field");
2759
2929
  }
2760
- const treeParams = {
2930
+ const contractParams = {
2761
2931
  solverPubkey: (0, import_sdk9.toXOnly)(import_base10.hex.decode(quote.solver_pubkey), "solver key"),
2762
2932
  refundLocktime,
2763
- serverPubkey: input.serverPubkey,
2933
+ operatorPubkey: input.operatorPubkey,
2764
2934
  paymentHash: input.paymentHash,
2765
2935
  claimDelay: input.claimDelay,
2766
2936
  emulatorPubkey: input.emulatorPubkey,
@@ -2771,8 +2941,11 @@ function deriveOnchainReceive(input) {
2771
2941
  const { script, address } = matchQuotedLockup(
2772
2942
  quote,
2773
2943
  input.hrp,
2774
- input.serverPubkey,
2775
- (legacy) => receiveVtxoScript({ ...treeParams, ...legacy !== void 0 && { legacy } })
2944
+ input.operatorPubkey,
2945
+ (legacy) => lightningReceiveContract({
2946
+ ...contractParams,
2947
+ ...legacy !== void 0 && { legacy }
2948
+ })
2776
2949
  );
2777
2950
  const htlc = onchainHtlcScript(
2778
2951
  {
@@ -2794,7 +2967,7 @@ function deriveOnchainReceive(input) {
2794
2967
  minConfirmations
2795
2968
  };
2796
2969
  }
2797
- async function requestOnchainReceive(wallet, arkServerUrl, transport, params) {
2970
+ async function requestOnchainReceive(wallet, transport, params) {
2798
2971
  const rfqId = params.rfqId ?? newRfqId();
2799
2972
  const secrets = await (0, import_sdk10.provisionClaimSecret)(wallet);
2800
2973
  if (secrets.mustPersistPreimage) {
@@ -2806,7 +2979,7 @@ async function requestOnchainReceive(wallet, arkServerUrl, transport, params) {
2806
2979
  const paymentHash = import_base10.hex.encode(secrets.paymentHash);
2807
2980
  const payoutPubkey = secrets.pubkey;
2808
2981
  const [info, payoutAddress] = await Promise.all([
2809
- new import_sdk9.RestArkProvider(arkServerUrl).getInfo(),
2982
+ wallet.getArkadeInfo({ requireLive: true }),
2810
2983
  wallet.getAddress()
2811
2984
  ]);
2812
2985
  const claimPacket = await sealClaimPacket({
@@ -2826,14 +2999,14 @@ async function requestOnchainReceive(wallet, arkServerUrl, transport, params) {
2826
2999
  })
2827
3000
  );
2828
3001
  assertQuotedAmount(quote, params.amountSide, params.amount);
2829
- const network = (0, import_sdk9.getNetwork)(info.network);
3002
+ const network = (0, import_sdk9.networkFromArkadeInfo)(info);
2830
3003
  const derived = deriveOnchainReceive({
2831
3004
  quote,
2832
3005
  paymentHash,
2833
3006
  payoutPubkey,
2834
3007
  payoutAddress,
2835
3008
  refundPubkey: params.refundPubkey,
2836
- serverPubkey: (0, import_sdk9.toXOnly)(import_base10.hex.decode(info.signerPubkey), "ark signer key"),
3009
+ operatorPubkey: (0, import_sdk9.toXOnly)(import_base10.hex.decode(info.signerPubkey), "ark signer key"),
2837
3010
  emulatorPubkey: (0, import_sdk9.toXOnly)(
2838
3011
  import_base10.hex.decode((0, import_sdk9.resolveEmulatorPubkey)(network, params.emulatorPubkey)),
2839
3012
  "emulator signer key"
@@ -2991,7 +3164,7 @@ function solverOnchainRail(deps) {
2991
3164
  }
2992
3165
  const negotiated = await deps.connect(
2993
3166
  rendezvous,
2994
- (transport) => requestOnchainSend(ctx.wallet, deps.arkServerUrl, transport, {
3167
+ (transport) => requestOnchainSend(ctx.wallet, transport, {
2995
3168
  amount,
2996
3169
  amountSide: "to",
2997
3170
  payoutPubkey: deps.payoutPubkey,
@@ -3104,7 +3277,7 @@ function solverLightningRail(deps) {
3104
3277
  }
3105
3278
  const negotiated = await deps.connect(
3106
3279
  rendezvous,
3107
- (transport) => requestLightningSend(ctx.wallet, deps.arkServerUrl, transport, {
3280
+ (transport) => requestLightningSend(ctx.wallet, transport, {
3108
3281
  invoice: facts,
3109
3282
  ...deps.emulatorPubkey ? { emulatorPubkey: deps.emulatorPubkey } : {}
3110
3283
  })
@@ -3157,16 +3330,450 @@ function solverLightningRail(deps) {
3157
3330
  };
3158
3331
  }
3159
3332
 
3333
+ // src/payment/lightning.ts
3334
+ var import_sdk14 = require("@arkade-os/sdk");
3335
+
3336
+ // src/payment/swapRail.ts
3337
+ var import_sdk13 = require("@arkade-os/sdk");
3338
+
3339
+ // src/client/errors.ts
3340
+ var AmbiguousDestination = class extends Error {
3341
+ constructor(destination, detail) {
3342
+ super(`cannot route ${JSON.stringify(destination)}: ${detail}`);
3343
+ this.destination = destination;
3344
+ }
3345
+ destination;
3346
+ name = "AmbiguousDestination";
3347
+ };
3348
+ var UnsupportedRoute = class extends Error {
3349
+ name = "UnsupportedRoute";
3350
+ give;
3351
+ take;
3352
+ constructor(detail, corridors = {}) {
3353
+ super(`unsupported route: ${detail}`);
3354
+ this.give = corridors.give;
3355
+ this.take = corridors.take;
3356
+ }
3357
+ };
3358
+ var DiscoverySnapshotUnavailable = class extends Error {
3359
+ constructor(network, detail) {
3360
+ super(`cannot resolve on ${network} offline: ${detail}`);
3361
+ this.network = network;
3362
+ }
3363
+ network;
3364
+ name = "DiscoverySnapshotUnavailable";
3365
+ };
3366
+ var AmountMismatch = class extends Error {
3367
+ constructor(sources) {
3368
+ super(`exactly one amount may be pinned; got ${sources[0]} and ${sources[1]}`);
3369
+ this.sources = sources;
3370
+ }
3371
+ sources;
3372
+ name = "AmountMismatch";
3373
+ };
3374
+ var AmountEncodingUnsupported = class extends Error {
3375
+ constructor(field, value, detail, options) {
3376
+ super(`${field}: ${detail}`, options);
3377
+ this.field = field;
3378
+ this.value = value;
3379
+ }
3380
+ field;
3381
+ value;
3382
+ name = "AmountEncodingUnsupported";
3383
+ };
3384
+ var QuoteVerificationFailed = class extends Error {
3385
+ constructor(check, expected, actual, options) {
3386
+ super(`quote failed the ${check} check \u2014 refusing to fund`, options);
3387
+ this.check = check;
3388
+ this.expected = expected;
3389
+ this.actual = actual;
3390
+ }
3391
+ check;
3392
+ name = "QuoteVerificationFailed";
3393
+ expected;
3394
+ actual;
3395
+ };
3396
+ var QuoteExpired = class extends Error {
3397
+ constructor(quoteId, expiresAt, now) {
3398
+ super(`quote ${quoteId} expired at ${expiresAt}, ${now - expiresAt} ago`);
3399
+ this.quoteId = quoteId;
3400
+ this.expiresAt = expiresAt;
3401
+ this.now = now;
3402
+ }
3403
+ quoteId;
3404
+ expiresAt;
3405
+ now;
3406
+ name = "QuoteExpired";
3407
+ };
3408
+ var MaxFeeExceeded = class extends Error {
3409
+ constructor(quoteId, asset4, fee, maxFee) {
3410
+ super(`fee ${fee} of ${asset4} exceeds the ${maxFee} ceiling`);
3411
+ this.quoteId = quoteId;
3412
+ this.asset = asset4;
3413
+ this.fee = fee;
3414
+ this.maxFee = maxFee;
3415
+ }
3416
+ quoteId;
3417
+ asset;
3418
+ fee;
3419
+ maxFee;
3420
+ name = "MaxFeeExceeded";
3421
+ };
3422
+ var InsufficientFunds = class extends Error {
3423
+ constructor(asset4, required, available) {
3424
+ super(
3425
+ `funding needs ${required} of ${asset4}` + (available === void 0 ? "" : `, wallet holds ${available}`)
3426
+ );
3427
+ this.asset = asset4;
3428
+ this.required = required;
3429
+ this.available = available;
3430
+ }
3431
+ asset;
3432
+ required;
3433
+ name = "InsufficientFunds";
3434
+ available;
3435
+ };
3436
+ var AcceptConflict = class extends Error {
3437
+ constructor(quoteId, swapId, fields) {
3438
+ super(`accept ${quoteId} conflicts with swap ${swapId} on ${fields.join(", ")}`);
3439
+ this.quoteId = quoteId;
3440
+ this.swapId = swapId;
3441
+ this.fields = fields;
3442
+ }
3443
+ quoteId;
3444
+ swapId;
3445
+ fields;
3446
+ name = "AcceptConflict";
3447
+ };
3448
+ var ClientDisposed = class extends Error {
3449
+ constructor(method) {
3450
+ super(`${method}() called after the client was disposed`);
3451
+ this.method = method;
3452
+ }
3453
+ method;
3454
+ name = "ClientDisposed";
3455
+ };
3456
+ var NotCancellable = class extends Error {
3457
+ constructor(swapId) {
3458
+ super(`swap ${swapId} is not a cancellable asset swap`);
3459
+ this.swapId = swapId;
3460
+ }
3461
+ swapId;
3462
+ name = "NotCancellable";
3463
+ };
3464
+ var InconsistentRoute = class extends Error {
3465
+ constructor(asset4, destination) {
3466
+ super(`destination ${JSON.stringify(destination)} is not on ${asset4}'s chain`);
3467
+ this.asset = asset4;
3468
+ this.destination = destination;
3469
+ }
3470
+ asset;
3471
+ destination;
3472
+ name = "InconsistentRoute";
3473
+ };
3474
+ var OperatorUnreachable = class extends Error {
3475
+ name = "OperatorUnreachable";
3476
+ constructor(detail, options) {
3477
+ super(`cannot derive a covenant: ${detail}`, options);
3478
+ }
3479
+ };
3480
+ var MissingCorridorDep = class extends Error {
3481
+ constructor(corridor, dep) {
3482
+ super(`the ${corridor} corridor has no ${dep}`);
3483
+ this.corridor = corridor;
3484
+ this.dep = dep;
3485
+ }
3486
+ corridor;
3487
+ dep;
3488
+ name = "MissingCorridorDep";
3489
+ };
3490
+ var SWAP_ERRORS = {
3491
+ AmbiguousDestination,
3492
+ UnsupportedRoute,
3493
+ DiscoverySnapshotUnavailable,
3494
+ AmountMismatch,
3495
+ AmountEncodingUnsupported,
3496
+ QuoteVerificationFailed,
3497
+ SwapRefusal,
3498
+ QuoteExpired,
3499
+ MaxFeeExceeded,
3500
+ InsufficientFunds,
3501
+ AcceptConflict,
3502
+ ClientDisposed,
3503
+ NotCancellable,
3504
+ InconsistentRoute,
3505
+ OperatorUnreachable,
3506
+ MissingCorridorDep
3507
+ };
3508
+ var SWAP_ERROR_NAMES = Object.keys(SWAP_ERRORS);
3509
+ function isSwapError(e, name) {
3510
+ if (!(e instanceof Error)) return false;
3511
+ const table = SWAP_ERRORS;
3512
+ const ctor = table[e.name];
3513
+ if (ctor === void 0 || !(e instanceof ctor)) return false;
3514
+ return name === void 0 || e.name === name;
3515
+ }
3516
+
3517
+ // src/client/sats.ts
3518
+ var satsOf = (value, field) => {
3519
+ if (value < 0n || value > BigInt(Number.MAX_SAFE_INTEGER)) {
3520
+ throw new AmountEncodingUnsupported(
3521
+ field,
3522
+ `${value}`,
3523
+ "outside the non-negative safe-integer window core's payment amounts are numbers in"
3524
+ );
3525
+ }
3526
+ return Number(value);
3527
+ };
3528
+
3529
+ // src/payment/status.ts
3530
+ var PAYMENT_STATUS = {
3531
+ /** Persisted, funding not broadcast. */
3532
+ accepted: "pending",
3533
+ funding: "pending",
3534
+ /** The lockup is funded. Nothing has emitted `"sent"` in core since the
3535
+ * `onchain-swap` rail left with `packages/boltz-swap`. */
3536
+ funded: "sent",
3537
+ /** Asset swaps only: an unfilled offer. */
3538
+ open: "pending",
3539
+ /** Asset swaps only. */
3540
+ filled: "settled",
3541
+ /** The trader's L1 claim on `arkade -> onchain`. */
3542
+ claimed: "settled",
3543
+ /** Terminal success on `arkade -> lightning`: the solver's hash-verified
3544
+ * spend IS the invoice being paid. */
3545
+ paid: "settled",
3546
+ cancelling: "pending",
3547
+ /** Value returned, and the router has no non-loss terminal to say so with. */
3548
+ cancelled: "failed",
3549
+ /** Terminal HERE, not at `refunded` — see the module docblock. */
3550
+ refunding: "failed",
3551
+ /** The trader's value came back. Reaches `onUpdate`, not the handle. */
3552
+ refunded: "failed",
3553
+ /** The solver reclaimed a receive-leg lockup: a loss, and not `refunded`. */
3554
+ lapsed: "failed",
3555
+ /** Never retried silently; `client.recover` drives it. */
3556
+ needs_recovery: "failed",
3557
+ failed: "failed"
3558
+ };
3559
+ var paymentStatusOf = (outcome) => PAYMENT_STATUS[outcome];
3560
+ var isTerminalStatus = (status) => status === "settled" || status === "failed";
3561
+
3562
+ // src/payment/swapRail.ts
3563
+ var SwapPaymentFailedError = class extends Error {
3564
+ constructor(railId, outcome, swap) {
3565
+ super(
3566
+ `${railId}: swap ${swap.id} ended ${outcome}` + (swap.failure === void 0 ? "" : `: ${swap.failure}`) + (swap.blockedReason === void 0 ? "" : ` (${swap.blockedReason})`)
3567
+ );
3568
+ this.railId = railId;
3569
+ this.outcome = outcome;
3570
+ this.swap = swap;
3571
+ }
3572
+ railId;
3573
+ outcome;
3574
+ swap;
3575
+ name = "SwapPaymentFailedError";
3576
+ };
3577
+ var unroutable = (error) => isSwapError(error, "UnsupportedRoute") || isSwapError(error, "AmbiguousDestination");
3578
+ var railAvailable = async (client, input) => {
3579
+ try {
3580
+ return (await client.resolve(input)).eligible > 0;
3581
+ } catch (error) {
3582
+ if (unroutable(error)) return false;
3583
+ throw error;
3584
+ }
3585
+ };
3586
+ var receiverExact = (railId, parts) => {
3587
+ if (parts.total !== parts.amount + parts.fee) {
3588
+ throw new Error(
3589
+ `${railId}: ${parts.total} leaving the wallet is not ${parts.amount} delivered plus ${parts.fee} in fees \u2014 the quote is not receiver-exact`
3590
+ );
3591
+ }
3592
+ return {
3593
+ amount: satsOf(parts.amount, `${railId}.amount`),
3594
+ fee: satsOf(parts.fee, `${railId}.fee`),
3595
+ total: satsOf(parts.total, `${railId}.total`)
3596
+ };
3597
+ };
3598
+ var resultOf = (railId, swap) => ({
3599
+ railId,
3600
+ // The tagged public form M6 minted — round-trippable to `client.swaps()`,
3601
+ // which is where the outcomes past this handle's terminal one are read.
3602
+ swapId: swap.id,
3603
+ // The transaction that left THIS wallet, which is what `txid` means on
3604
+ // every other rail. A swap's later legs — the solver's fill, the trader's
3605
+ // L1 claim — are the client's to report, not the handle's.
3606
+ ...swap.fundingTxid === void 0 ? {} : { txid: swap.fundingTxid }
3607
+ });
3608
+ var swapHandle = async (railId, client, quote) => (0, import_sdk13.makeHandle)(railId, async (emit) => {
3609
+ const accepted = await client.accept(quote);
3610
+ return await new Promise((resolve, reject) => {
3611
+ let finished = false;
3612
+ let unsubscribe;
3613
+ const listener = (update) => {
3614
+ if (finished || update.swap.id !== accepted.id) return;
3615
+ const status = paymentStatusOf(update.outcome);
3616
+ const result = resultOf(railId, update.swap);
3617
+ if (!isTerminalStatus(status)) {
3618
+ emit({ status, result });
3619
+ return;
3620
+ }
3621
+ finished = true;
3622
+ unsubscribe?.();
3623
+ if (status === "settled") {
3624
+ emit({ status, result });
3625
+ resolve(result);
3626
+ return;
3627
+ }
3628
+ reject(new SwapPaymentFailedError(railId, update.outcome, update.swap));
3629
+ };
3630
+ unsubscribe = client.onUpdate(listener);
3631
+ if (finished) unsubscribe();
3632
+ });
3633
+ });
3634
+
3635
+ // src/payment/lightning.ts
3636
+ var LIGHTNING_RAIL = "lightning";
3637
+ var inputFor = (invoice, amount) => ({
3638
+ to: invoice,
3639
+ ...amount === void 0 ? {} : { amount: BigInt(amount), amountOn: "take" }
3640
+ });
3641
+ function lightningRail(client) {
3642
+ return {
3643
+ id: LIGHTNING_RAIL,
3644
+ // Amount-blind and non-throwing: classification only, which is what
3645
+ // keeps `match` from being a second gate on top of `available`.
3646
+ match: (req) => (0, import_sdk14.invoiceTarget)(req.raw) !== void 0,
3647
+ available: async (req) => {
3648
+ const invoice = (0, import_sdk14.invoiceTarget)(req.raw);
3649
+ if (invoice === void 0) return false;
3650
+ return railAvailable(client, inputFor(invoice, req.amount));
3651
+ },
3652
+ quote: async (req) => {
3653
+ const invoice = (0, import_sdk14.invoiceTarget)(req.raw);
3654
+ if (invoice === void 0) {
3655
+ throw new Error(`${LIGHTNING_RAIL}: the request carries no BOLT11 invoice`);
3656
+ }
3657
+ const quote = await client.quote(inputFor(invoice, req.amount));
3658
+ const amounts = receiverExact(LIGHTNING_RAIL, {
3659
+ amount: quote.take.amount,
3660
+ fee: quote.fee.amount,
3661
+ total: quote.give.amount
3662
+ });
3663
+ return {
3664
+ railId: LIGHTNING_RAIL,
3665
+ ...amounts,
3666
+ meta: {
3667
+ quoteId: quote.id,
3668
+ // A `RouteQuote` carries no expiry of its own, so a held one
3669
+ // can outlive the terms behind it. This is where a caller
3670
+ // can see that; `send()` refuses with `QuoteExpired` rather
3671
+ // than silently re-quoting.
3672
+ expiresAt: quote.expiresAt,
3673
+ ...quote.refundLocktime === void 0 ? {} : { refundLocktime: quote.refundLocktime },
3674
+ ...quote.solver === void 0 ? {} : { solver: quote.solver },
3675
+ ...quote.lock === void 0 ? {} : { paymentHash: quote.lock.hash },
3676
+ market: quote.market.key
3677
+ },
3678
+ send: () => swapHandle(LIGHTNING_RAIL, client, quote)
3679
+ };
3680
+ }
3681
+ };
3682
+ }
3683
+
3684
+ // src/payment/onchainSwap.ts
3685
+ var import_sdk15 = require("@arkade-os/sdk");
3686
+ var ONCHAIN_SWAP_RAIL = "onchain-swap";
3687
+ var claimFeeSats = (deps) => BigInt(Math.ceil((deps.claimVsize ?? ONCHAIN_CLAIM_VSIZE) * deps.claimFeeRateSatVb));
3688
+ function onchainSwapRail(client, deps) {
3689
+ if (!Number.isFinite(deps.claimFeeRateSatVb) || deps.claimFeeRateSatVb <= 0) {
3690
+ throw new Error(
3691
+ `${ONCHAIN_SWAP_RAIL}: claimFeeRateSatVb must be positive, got ${deps.claimFeeRateSatVb}`
3692
+ );
3693
+ }
3694
+ const claimFee = claimFeeSats(deps);
3695
+ const inputFor2 = (address, amount) => ({
3696
+ to: address,
3697
+ amount: BigInt(amount) + claimFee,
3698
+ amountOn: "take"
3699
+ });
3700
+ return {
3701
+ id: ONCHAIN_SWAP_RAIL,
3702
+ match: (req) => (0, import_sdk15.btcTarget)(req.raw) !== void 0,
3703
+ available: async (req) => {
3704
+ const address = (0, import_sdk15.btcTarget)(req.raw);
3705
+ if (address === void 0) return false;
3706
+ const amount = (0, import_sdk15.tryResolveSendAmount)(req.raw, req.amount);
3707
+ if (amount === void 0) return false;
3708
+ return railAvailable(client, inputFor2(address, amount));
3709
+ },
3710
+ quote: async (req) => {
3711
+ const address = (0, import_sdk15.btcTarget)(req.raw);
3712
+ if (address === void 0) {
3713
+ throw new Error(`${ONCHAIN_SWAP_RAIL}: the request carries no bitcoin address`);
3714
+ }
3715
+ const amount = (0, import_sdk15.resolveSendAmount)(ONCHAIN_SWAP_RAIL, req.raw, req.amount);
3716
+ const quote = await client.quote(inputFor2(address, amount));
3717
+ const amounts = receiverExact(ONCHAIN_SWAP_RAIL, {
3718
+ // What lands at the address, which is the HTLC the solver locks
3719
+ // minus the claim this wallet will broadcast to take it.
3720
+ amount: quote.take.amount - claimFee,
3721
+ // Both halves of the cost, inside one number: the solver's
3722
+ // spread, and the claim the trader pays for out of the payout.
3723
+ fee: quote.fee.amount + claimFee,
3724
+ total: quote.give.amount
3725
+ });
3726
+ return {
3727
+ railId: ONCHAIN_SWAP_RAIL,
3728
+ ...amounts,
3729
+ meta: {
3730
+ quoteId: quote.id,
3731
+ expiresAt: quote.expiresAt,
3732
+ ...quote.refundLocktime === void 0 ? {} : { refundLocktime: quote.refundLocktime },
3733
+ ...quote.solver === void 0 ? {} : { solver: quote.solver },
3734
+ ...quote.lock === void 0 ? {} : { paymentHash: quote.lock.hash },
3735
+ market: quote.market.key,
3736
+ /** The estimate folded into `fee`, so a caller can see it. */
3737
+ claimFeeSats: Number(claimFee),
3738
+ /** What the solver locks on L1, before the claim's fee. */
3739
+ htlcAmountSats: Number(quote.take.amount)
3740
+ },
3741
+ send: () => swapHandle(ONCHAIN_SWAP_RAIL, client, quote)
3742
+ };
3743
+ }
3744
+ };
3745
+ }
3746
+
3747
+ // src/payment/router.ts
3748
+ var import_sdk16 = require("@arkade-os/sdk");
3749
+ var SWAP_ROUTER_PRIORITY = [
3750
+ "ark",
3751
+ LIGHTNING_RAIL,
3752
+ ONCHAIN_SWAP_RAIL,
3753
+ "onchain"
3754
+ ];
3755
+ function createSwapPaymentRouter(wallet, client, config) {
3756
+ return new import_sdk16.PaymentRouter({
3757
+ wallet,
3758
+ prefs: { priority: [...SWAP_ROUTER_PRIORITY], ...config.prefs }
3759
+ }).use((0, import_sdk16.arkRail)()).use((0, import_sdk16.onchainRail)()).use(lightningRail(client)).use(
3760
+ onchainSwapRail(client, {
3761
+ claimFeeRateSatVb: config.claimFeeRateSatVb,
3762
+ ...config.claimVsize === void 0 ? {} : { claimVsize: config.claimVsize }
3763
+ })
3764
+ );
3765
+ }
3766
+
3160
3767
  // src/claim.ts
3161
3768
  var import_base14 = require("@scure/base");
3162
3769
  var import_legacy3 = require("@noble/hashes/legacy.js");
3163
3770
  var import_sha25 = require("@noble/hashes/sha2.js");
3164
- var import_sdk14 = require("@arkade-os/sdk");
3771
+ var import_sdk18 = require("@arkade-os/sdk");
3165
3772
 
3166
3773
  // src/refund.ts
3167
3774
  var import_base13 = require("@scure/base");
3168
3775
  var import_sha24 = require("@noble/hashes/sha2.js");
3169
- var import_sdk13 = require("@arkade-os/sdk");
3776
+ var import_sdk17 = require("@arkade-os/sdk");
3170
3777
  var sleep2 = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
3171
3778
  var isRfqTerminal = (state) => RFQ_TERMINAL_STATES.includes(state);
3172
3779
  var RFQ_RESOLVED_STATES = ["settled", "refunded"];
@@ -3186,6 +3793,15 @@ async function awaitRfqResolution(transport, rfqId, options = {}) {
3186
3793
  await sleep2(pollMs);
3187
3794
  }
3188
3795
  }
3796
+ var walletOperator = (wallet) => {
3797
+ let broadcaster;
3798
+ const broadcasting = () => broadcaster ??= wallet.getArkadeBroadcaster();
3799
+ return {
3800
+ getInfo: () => wallet.getArkadeInfo({ requireLive: true }),
3801
+ submitTx: async (...args) => (await broadcasting()).submitTx(...args),
3802
+ finalizeTx: async (...args) => (await broadcasting()).finalizeTx(...args)
3803
+ };
3804
+ };
3189
3805
  var LockupNeedsRecoveryError = class extends Error {
3190
3806
  name = "LockupNeedsRecoveryError";
3191
3807
  reason = "needs_recovery";
@@ -3197,11 +3813,10 @@ var LockupNeedsRecoveryError = class extends Error {
3197
3813
  * into one settlement with no CLTV awareness, so an early attempt can fail
3198
3814
  * the whole batch — including unrelated outputs that were otherwise fine.
3199
3815
  *
3200
- * Exposed as a value, not only inside the message, so a caller can encode
3201
- * `packages/boltz-swap`'s "pre-CLTV recoverable → skipped" rule without
3202
- * parsing prose. Seconds-based locktimes mature against the chain tip's
3203
- * timestamp rather than wall clock, so treat this as a floor to wait past,
3204
- * not an exact alarm.
3816
+ * Exposed as a value, not only inside the message, so a caller can skip
3817
+ * pre-CLTV recoverable outputs without parsing prose. Seconds-based
3818
+ * locktimes mature against the chain tip's timestamp rather than wall
3819
+ * clock, so treat this as a floor to wait past, not an exact alarm.
3205
3820
  */
3206
3821
  recoverableAfter;
3207
3822
  constructor(outpoints, recoverableAfter) {
@@ -3241,14 +3856,14 @@ async function findLockupVtxos(indexer, swapPkScript) {
3241
3856
  }
3242
3857
  var hashesTo = (candidate, paymentHash) => import_base13.hex.encode((0, import_sha24.sha256)(candidate)) === paymentHash;
3243
3858
  var candidateWitnessItems = (tx, inputIndex) => [
3244
- ...(0, import_sdk13.getArkPsbtFields)(tx, inputIndex, import_sdk13.ConditionWitness).flat(),
3859
+ ...(0, import_sdk17.getArkPsbtFields)(tx, inputIndex, import_sdk17.ConditionWitness).flat(),
3245
3860
  ...tx.getInput(inputIndex).finalScriptWitness ?? []
3246
3861
  ];
3247
3862
  async function readLockupFate(indexer, input) {
3248
3863
  const { vtxos } = await indexer.getVtxos({ scripts: [import_base13.hex.encode(input.swapPkScript)] });
3249
3864
  const all = vtxos ?? [];
3250
3865
  if (all.length === 0) return { fate: "unknown" };
3251
- const exited = all.filter((vtxo) => vtxo.isUnrolled && !(0, import_sdk13.hasTerminalSpend)(vtxo));
3866
+ const exited = all.filter((vtxo) => vtxo.isUnrolled && !(0, import_sdk17.hasTerminalSpend)(vtxo));
3252
3867
  if (exited.length > 0) {
3253
3868
  return {
3254
3869
  fate: "exited",
@@ -3258,11 +3873,11 @@ async function readLockupFate(indexer, input) {
3258
3873
  const spentBy = /* @__PURE__ */ new Map();
3259
3874
  let everySpendNamed = true;
3260
3875
  for (const vtxo of all) {
3261
- if (!(0, import_sdk13.hasTerminalSpend)(vtxo)) return { fate: "open" };
3876
+ if (!(0, import_sdk17.hasTerminalSpend)(vtxo)) return { fate: "open" };
3262
3877
  if (vtxo.spentBy)
3263
3878
  spentBy.set(vtxo.spentBy, {
3264
3879
  checkpointTxid: vtxo.spentBy,
3265
- arkTxid: vtxo.arkTxId
3880
+ txid: vtxo.arkTxId
3266
3881
  });
3267
3882
  else everySpendNamed = false;
3268
3883
  }
@@ -3272,7 +3887,7 @@ async function readLockupFate(indexer, input) {
3272
3887
  for (const raw of txs) {
3273
3888
  let tx;
3274
3889
  try {
3275
- tx = import_sdk13.Transaction.fromPSBT(import_base13.base64.decode(raw));
3890
+ tx = import_sdk17.Transaction.fromPSBT(import_base13.base64.decode(raw));
3276
3891
  } catch {
3277
3892
  continue;
3278
3893
  }
@@ -3291,32 +3906,32 @@ async function readLockupFate(indexer, input) {
3291
3906
  }
3292
3907
  return everySpendNamed && observed.size === spentBy.size ? { fate: "returned", spends } : { fate: "unknown" };
3293
3908
  }
3294
- async function pushRefundWithoutReceiver(ark, input) {
3909
+ async function pushRefundWithoutReceiver(operator, input) {
3295
3910
  if (input.vtxos.length === 0) throw new Error("nothing to refund: no funded outputs");
3296
3911
  const swept = input.vtxos.filter((vtxo) => vtxo.recoverable);
3297
3912
  if (swept.length > 0) {
3298
3913
  throw new LockupNeedsRecoveryError(
3299
3914
  swept.map((vtxo) => `${vtxo.txid}:${vtxo.vout}`),
3300
- input.script.options.refundLocktime
3915
+ input.contract.options.refundLocktime
3301
3916
  );
3302
3917
  }
3303
- const refundPkScript = input.refundPkScript ?? input.script.options.nonInteractiveParameters?.senderPkScript;
3918
+ const refundPkScript = input.refundPkScript ?? input.contract.options.nonInteractiveParameters?.senderPkScript;
3304
3919
  if (!refundPkScript) {
3305
3920
  throw new Error(
3306
3921
  "no refund destination: the contract carries no emulator covenant suite, so pass refundPkScript explicitly"
3307
3922
  );
3308
3923
  }
3309
- const info = await ark.getInfo();
3310
- let serverUnrollScript;
3924
+ const info = await operator.getInfo();
3925
+ let operatorUnrollScript;
3311
3926
  try {
3312
- serverUnrollScript = import_sdk13.CSVMultisigTapscript.decode(import_base13.hex.decode(info.checkpointTapscript));
3927
+ operatorUnrollScript = import_sdk17.CSVMultisigTapscript.decode(import_base13.hex.decode(info.checkpointTapscript));
3313
3928
  } catch {
3314
- throw new Error("invalid checkpointTapscript from the Arkade server");
3929
+ throw new Error("invalid checkpointTapscript from the operator");
3315
3930
  }
3316
- const leaf = input.script.refundWithoutReceiver();
3317
- const tapTree = input.script.encode();
3931
+ const leaf = input.contract.refundWithoutReceiver();
3932
+ const tapTree = input.contract.encode();
3318
3933
  const amount = input.vtxos.reduce((sum, vtxo) => sum + vtxo.value, 0);
3319
- const { arkTx, checkpoints } = (0, import_sdk13.buildOffchainTx)(
3934
+ const { arkTx: tx, checkpoints } = (0, import_sdk17.buildOffchainTx)(
3320
3935
  input.vtxos.map((vtxo) => ({
3321
3936
  txid: vtxo.txid,
3322
3937
  vout: vtxo.vout,
@@ -3325,15 +3940,15 @@ async function pushRefundWithoutReceiver(ark, input) {
3325
3940
  tapTree
3326
3941
  })),
3327
3942
  [{ script: refundPkScript, amount: BigInt(amount) }],
3328
- serverUnrollScript
3943
+ operatorUnrollScript
3329
3944
  );
3330
- const signedArkTx = await input.sender.sign(arkTx);
3331
- const submitted = await ark.submitTx(
3332
- import_base13.base64.encode(signedArkTx.toPSBT()),
3945
+ const signedTx = await input.sender.sign(tx);
3946
+ const submitted = await operator.submitTx(
3947
+ import_base13.base64.encode(signedTx.toPSBT()),
3333
3948
  checkpoints.map((c) => import_base13.base64.encode(c.toPSBT()))
3334
3949
  );
3335
- (0, import_sdk13.assertSubmittedArkTxid)(submitted, signedArkTx, "refundWithoutReceiver");
3336
- const matched = (0, import_sdk13.matchServerCheckpoints)(
3950
+ (0, import_sdk17.assertSubmittedArkTxid)(submitted, signedTx, "refundWithoutReceiver");
3951
+ const matched = (0, import_sdk17.matchServerCheckpoints)(
3337
3952
  submitted.signedCheckpointTxs,
3338
3953
  checkpoints,
3339
3954
  "refundWithoutReceiver"
@@ -3343,11 +3958,11 @@ async function pushRefundWithoutReceiver(ark, input) {
3343
3958
  async ({ server }) => import_base13.base64.encode((await input.sender.sign(server, [0])).toPSBT())
3344
3959
  )
3345
3960
  );
3346
- await ark.finalizeTx(submitted.arkTxid, finalCheckpoints);
3347
- return { arkTxid: submitted.arkTxid, amount };
3961
+ await operator.finalizeTx(submitted.arkTxid, finalCheckpoints);
3962
+ return { txid: submitted.arkTxid, amount };
3348
3963
  }
3349
3964
  var REFUND_MTP_LAG_SECONDS = 2 * 60 * 60;
3350
- async function refundIfUnresolved(transport, ark, indexer, input) {
3965
+ async function refundIfUnresolved(transport, operator, indexer, input) {
3351
3966
  const pollMs = input.pollMs ?? 5e3;
3352
3967
  const now = input.now ?? (() => Math.floor(Date.now() / 1e3));
3353
3968
  const attemptDeadline = input.attemptDeadline ?? input.refundLocktime + REFUND_MTP_LAG_SECONDS;
@@ -3358,7 +3973,7 @@ async function refundIfUnresolved(transport, ark, indexer, input) {
3358
3973
  let fate = { fate: "unknown" };
3359
3974
  try {
3360
3975
  fate = await readLockupFate(indexer, {
3361
- swapPkScript: input.script.pkScript,
3976
+ swapPkScript: input.contract.pkScript,
3362
3977
  paymentHash: input.paymentHash
3363
3978
  });
3364
3979
  } catch {
@@ -3370,11 +3985,11 @@ async function refundIfUnresolved(transport, ark, indexer, input) {
3370
3985
  status
3371
3986
  };
3372
3987
  }
3373
- const vtxos = await findLockupVtxos(indexer, input.script.pkScript);
3988
+ const vtxos = await findLockupVtxos(indexer, input.contract.pkScript);
3374
3989
  if (vtxos.length === 0) return { outcome: "nothing_to_refund", status };
3375
3990
  try {
3376
- const pushed = await pushRefundWithoutReceiver(ark, {
3377
- script: input.script,
3991
+ const pushed = await pushRefundWithoutReceiver(operator, {
3992
+ contract: input.contract,
3378
3993
  sender: input.sender,
3379
3994
  vtxos,
3380
3995
  refundPkScript: input.refundPkScript
@@ -3417,13 +4032,13 @@ var assertFiniteAmount = (value, reason, label) => {
3417
4032
  error.reason = reason;
3418
4033
  throw error;
3419
4034
  };
3420
- async function pushClaim(ark, input) {
4035
+ async function pushClaim(operator, input) {
3421
4036
  if (input.vtxos.length === 0) throw new Error("nothing to claim: no funded outputs");
3422
4037
  const swept = input.vtxos.filter((vtxo) => vtxo.recoverable);
3423
4038
  if (swept.length > 0) {
3424
4039
  throw new LockupNeedsRecoveryError(
3425
4040
  swept.map((vtxo) => `${vtxo.txid}:${vtxo.vout}`),
3426
- input.script.options.refundLocktime
4041
+ input.contract.options.refundLocktime
3427
4042
  );
3428
4043
  }
3429
4044
  const locked = input.vtxos.reduce((sum, vtxo) => sum + vtxo.value, 0);
@@ -3434,22 +4049,22 @@ async function pushClaim(ark, input) {
3434
4049
  throw new LockupAmountMismatchError(input.expectedAmount, locked);
3435
4050
  }
3436
4051
  }
3437
- const committed = input.script.options.preimageHash;
4052
+ const committed = input.contract.options.preimageHash;
3438
4053
  if (import_base14.hex.encode((0, import_legacy3.ripemd160)((0, import_sha25.sha256)(input.preimage))) !== import_base14.hex.encode(committed)) {
3439
4054
  throw new Error("preimage does not match the covenant's payment hash");
3440
4055
  }
3441
- const info = await ark.getInfo();
3442
- let serverUnrollScript;
4056
+ const info = await operator.getInfo();
4057
+ let operatorUnrollScript;
3443
4058
  try {
3444
- serverUnrollScript = import_sdk14.CSVMultisigTapscript.decode(import_base14.hex.decode(info.checkpointTapscript));
4059
+ operatorUnrollScript = import_sdk18.CSVMultisigTapscript.decode(import_base14.hex.decode(info.checkpointTapscript));
3445
4060
  } catch {
3446
- throw new Error("invalid checkpointTapscript from the Arkade server");
4061
+ throw new Error("invalid checkpointTapscript from the operator");
3447
4062
  }
3448
- const leaf = input.script.claim();
3449
- const tapTree = input.script.encode();
3450
- const arkTxid = await (0, import_sdk14.signAndSubmitOffchainTx)({
3451
- identity: (0, import_sdk14.claimWithPreimageIdentity)(input.receiver, input.preimage),
3452
- provider: ark,
4063
+ const leaf = input.contract.claim();
4064
+ const tapTree = input.contract.encode();
4065
+ const txid = await (0, import_sdk18.signAndSubmitOffchainTx)({
4066
+ identity: (0, import_sdk18.claimWithPreimageIdentity)(input.receiver, input.preimage),
4067
+ provider: operator,
3453
4068
  inputs: input.vtxos.map((vtxo) => ({
3454
4069
  txid: vtxo.txid,
3455
4070
  vout: vtxo.vout,
@@ -3460,10 +4075,10 @@ async function pushClaim(ark, input) {
3460
4075
  // One aggregate output: unlike the covenant refund, this leaf inspects
3461
4076
  // nothing about the output set.
3462
4077
  outputs: [{ script: input.destinationPkScript, amount: BigInt(locked) }],
3463
- serverUnrollScript,
3464
- verifyServerSignatures: { serverPubkey: input.script.options.server }
4078
+ serverUnrollScript: operatorUnrollScript,
4079
+ verifyServerSignatures: { serverPubkey: input.contract.options.server }
3465
4080
  });
3466
- return { arkTxid, amount: locked };
4081
+ return { txid, amount: locked };
3467
4082
  }
3468
4083
  async function awaitLockupFunding(indexer, swapPkScript, options = {}) {
3469
4084
  const pollMs = options.pollMs ?? 5e3;
@@ -3478,13 +4093,13 @@ async function awaitLockupFunding(indexer, swapPkScript, options = {}) {
3478
4093
  await sleep3(pollMs);
3479
4094
  }
3480
4095
  }
3481
- async function claimReceiveLockup(indexer, ark, input) {
4096
+ async function claimReceiveLockup(indexer, operator, input) {
3482
4097
  const vtxos = await awaitLockupFunding(indexer, input.swapPkScript, {
3483
4098
  pollMs: input.pollMs,
3484
4099
  deadline: input.deadline
3485
4100
  });
3486
- return pushClaim(ark, {
3487
- script: input.script,
4101
+ return pushClaim(operator, {
4102
+ contract: input.contract,
3488
4103
  receiver: input.receiver,
3489
4104
  preimage: input.preimage,
3490
4105
  vtxos,
@@ -3495,7 +4110,7 @@ async function claimReceiveLockup(indexer, ark, input) {
3495
4110
  }
3496
4111
 
3497
4112
  // src/refundBlocked.ts
3498
- var import_sdk15 = require("@arkade-os/sdk");
4113
+ var import_sdk19 = require("@arkade-os/sdk");
3499
4114
  var RefundNotLocallyPossibleError = class extends Error {
3500
4115
  constructor(reason, message, options) {
3501
4116
  super(message, options);
@@ -3512,16 +4127,16 @@ async function senderIdentityForSwapRecord(wallet, record) {
3512
4127
  );
3513
4128
  }
3514
4129
  try {
3515
- return await (0, import_sdk15.contractSigner)(wallet, record.signingDescriptor);
4130
+ return await (0, import_sdk19.contractSigner)(wallet, record.signingDescriptor);
3516
4131
  } catch (cause) {
3517
- if (cause instanceof import_sdk15.WalletCannotSignError) {
4132
+ if (cause instanceof import_sdk19.WalletCannotSignError) {
3518
4133
  throw new RefundNotLocallyPossibleError(
3519
4134
  "unsignable-wallet",
3520
4135
  `this wallet holds ${record.signingDescriptor} but cannot sign with it; attach its signer`,
3521
4136
  { cause }
3522
4137
  );
3523
4138
  }
3524
- if (cause instanceof import_sdk15.ForeignDescriptorError) {
4139
+ if (cause instanceof import_sdk19.ForeignDescriptorError) {
3525
4140
  throw new RefundNotLocallyPossibleError(
3526
4141
  "foreign-descriptor",
3527
4142
  `this wallet cannot derive ${record.signingDescriptor}; the swap was created on another wallet`,
@@ -3535,8 +4150,8 @@ async function senderIdentityForSwapRecord(wallet, record) {
3535
4150
  // src/arkadeRefunder.ts
3536
4151
  function arkadeRefunder(deps) {
3537
4152
  return async (swap) => {
3538
- const script = swap.lockup?.script;
3539
- if (!script) {
4153
+ const contract = swap.lockup?.script;
4154
+ if (!contract) {
3540
4155
  throw new Error(
3541
4156
  `swap ${swap.rfqId} carries no lockup covenant, so its refund cannot be built`
3542
4157
  );
@@ -3551,7 +4166,7 @@ function arkadeRefunder(deps) {
3551
4166
  );
3552
4167
  }
3553
4168
  const sender = await senderIdentityForSwapRecord(deps.wallet, rfqSignerOf(record) ?? {});
3554
- return pushRefundWithoutReceiver(deps.ark, { script, sender, vtxos });
4169
+ return pushRefundWithoutReceiver(deps.operator, { contract, sender, vtxos });
3555
4170
  };
3556
4171
  }
3557
4172
 
@@ -3572,6 +4187,14 @@ function nextOnchainAction(input) {
3572
4187
  return input.htlcLocktime - input.now >= ONCHAIN_CLAIM_MARGIN_SECONDS ? "claim" : "claim_window_closed";
3573
4188
  }
3574
4189
  }
4190
+ var RFQ_CONFIGURATION_REFUSAL = {
4191
+ noClaimLockupCallback: "no claimLockup callback is wired, so this wallet cannot claim the lockup",
4192
+ noCallbacksForClaim: "no callbacks are wired, so this wallet cannot claim the lockup",
4193
+ noClaimOnchainCallback: "no claimOnchain callback is wired, so this wallet cannot claim the L1 fill",
4194
+ autoActionsDisabled: "automatic actions are disabled, so this wallet will not push the refund",
4195
+ noCallbacksForRefund: "no callbacks are wired, so this wallet cannot push the refund"
4196
+ };
4197
+ var RFQ_CONFIGURATION_REFUSALS = Object.values(RFQ_CONFIGURATION_REFUSAL);
3575
4198
  var RfqSwapOriginRequired = class extends Error {
3576
4199
  /** The swap that could not be admitted. */
3577
4200
  rfqId;
@@ -3622,20 +4245,6 @@ var RfqSwapManager = class {
3622
4245
  * sign may well have been restored.
3623
4246
  */
3624
4247
  refundRefused = /* @__PURE__ */ new Set();
3625
- /**
3626
- * The last error a receive swap's claim callback threw, by rfqId.
3627
- *
3628
- * Kept only to tell two terminal outcomes apart once the claim window
3629
- * shuts: a swap whose claim was attempted and kept failing ends `failed`
3630
- * with that reason, while one that simply never became claimable ends
3631
- * `refunded`. Without it a broken claim callback would resolve a caller's
3632
- * {@link waitForSwapCompletion} as an ordinary unwind.
3633
- *
3634
- * Process-local, like {@link refundRefused}: after a restart the same swap
3635
- * ends `refunded` instead, which costs the caller a reason and nothing else
3636
- * — every throw was already reported through `onSwapFailed` as it happened.
3637
- */
3638
- lastClaimError = /* @__PURE__ */ new Map();
3639
4248
  /**
3640
4249
  * The lockup outpoints a receive swap's claim callback has already been
3641
4250
  * handed, by rfqId.
@@ -3947,6 +4556,19 @@ var RfqSwapManager = class {
3947
4556
  async getPendingSwaps() {
3948
4557
  return [...this.monitored.values()];
3949
4558
  }
4559
+ /**
4560
+ * Every swap this manager holds — {@link getPendingSwaps} plus the ones
4561
+ * that already ended. The two sets are disjoint: a swap leaves `monitored`
4562
+ * as it enters `finished`.
4563
+ *
4564
+ * The finished half is what this process has seen, which after
4565
+ * {@link restoreFromRepository} is the stored history minus what retention
4566
+ * pruned. A manager that has restored nothing answers with the live swaps
4567
+ * alone.
4568
+ */
4569
+ async getAllSwaps() {
4570
+ return [...this.monitored.values(), ...this.finished.values()];
4571
+ }
3950
4572
  async hasSwap(rfqId) {
3951
4573
  return this.monitored.has(rfqId);
3952
4574
  }
@@ -4016,7 +4638,6 @@ var RfqSwapManager = class {
4016
4638
  if (swap) this.byLockupScript.delete(import_base15.hex.encode(swap.lockupPkScript));
4017
4639
  this.monitored.delete(rfqId);
4018
4640
  this.refundRefused.delete(rfqId);
4019
- this.lastClaimError.delete(rfqId);
4020
4641
  this.claimedOutpoints.delete(rfqId);
4021
4642
  }
4022
4643
  /**
@@ -4225,14 +4846,14 @@ var RfqSwapManager = class {
4225
4846
  return this.claimIfFunded(swap, vtxos);
4226
4847
  }
4227
4848
  if (now < swap.refundLocktime + REFUND_MTP_LAG_SECONDS) {
4228
- if (swap.claimArkTxid) return;
4849
+ if (swap.claimTxid) return;
4229
4850
  return this.block(
4230
4851
  swap,
4231
4852
  "the claim window closed with the lockup unclaimed \u2014 only the solver can act now"
4232
4853
  );
4233
4854
  }
4234
- const failure = this.lastClaimError.get(swap.rfqId);
4235
- if (failure && !swap.claimArkTxid) {
4855
+ const failure = swap.claimFailure;
4856
+ if (failure && !swap.claimTxid) {
4236
4857
  return this.fail(swap, new Error(failure));
4237
4858
  }
4238
4859
  this.setState(swap, "refunded");
@@ -4247,7 +4868,7 @@ var RfqSwapManager = class {
4247
4868
  */
4248
4869
  async claimIfFunded(swap, vtxos) {
4249
4870
  if (vtxos.length === 0) return this.unblock(swap);
4250
- const partiallyClaimed = swap.claimArkTxid !== void 0;
4871
+ const partiallyClaimed = swap.claimTxid !== void 0;
4251
4872
  if (partiallyClaimed && !this.hasUnclaimedOutpoint(swap.rfqId, vtxos)) {
4252
4873
  return;
4253
4874
  }
@@ -4269,21 +4890,22 @@ var RfqSwapManager = class {
4269
4890
  if (!this.callbacks || !this.callbacks.claimLockup) {
4270
4891
  return this.block(
4271
4892
  swap,
4272
- 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"
4893
+ this.callbacks ? RFQ_CONFIGURATION_REFUSAL.noClaimLockupCallback : RFQ_CONFIGURATION_REFUSAL.noCallbacksForClaim
4273
4894
  );
4274
4895
  }
4275
4896
  this.setState(swap, "claimable");
4276
4897
  if (!this.config.enableAutoActions) return;
4277
4898
  try {
4278
- const { arkTxid } = await this.callbacks.claimLockup(swap, vtxos, { partiallyClaimed });
4279
- this.lastClaimError.delete(swap.rfqId);
4899
+ const { txid } = await this.callbacks.claimLockup(swap, vtxos, { partiallyClaimed });
4900
+ delete swap.claimFailure;
4280
4901
  this.rememberClaimed(swap.rfqId, vtxos);
4281
- swap.claimArkTxid = arkTxid;
4902
+ swap.claimTxid = txid;
4282
4903
  this.touch(swap);
4283
4904
  this.setState(swap, "claimed");
4284
4905
  this.emitAction(swap, "claimLockup");
4285
4906
  } catch (error) {
4286
- this.lastClaimError.set(swap.rfqId, errorMessage(error));
4907
+ swap.claimFailure = errorMessage(error);
4908
+ this.touch(swap);
4287
4909
  this.emitFailed(swap, error);
4288
4910
  }
4289
4911
  }
@@ -4320,10 +4942,7 @@ var RfqSwapManager = class {
4320
4942
  if (action === "claim" && phase.phase === "claimable") {
4321
4943
  if (swap.claimTxid) return "continue";
4322
4944
  if (this.callbacks && !this.callbacks.claimOnchain) {
4323
- this.block(
4324
- swap,
4325
- "no claimOnchain callback is wired, so this wallet cannot claim the L1 fill"
4326
- );
4945
+ this.block(swap, RFQ_CONFIGURATION_REFUSAL.noClaimOnchainCallback);
4327
4946
  return "handled";
4328
4947
  }
4329
4948
  this.setOnchainState(swap, "claimable");
@@ -4363,14 +4982,14 @@ var RfqSwapManager = class {
4363
4982
  if (!this.config.enableAutoActions || !this.callbacks) {
4364
4983
  return this.block(
4365
4984
  swap,
4366
- 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"
4985
+ this.callbacks ? RFQ_CONFIGURATION_REFUSAL.autoActionsDisabled : RFQ_CONFIGURATION_REFUSAL.noCallbacksForRefund
4367
4986
  );
4368
4987
  }
4369
4988
  this.unblock(swap);
4370
4989
  try {
4371
4990
  const pushed = await this.callbacks.refundArkade(swap);
4372
4991
  if (pushed) {
4373
- swap.refundArkTxid = pushed.arkTxid;
4992
+ swap.refundTxid = pushed.txid;
4374
4993
  this.touch(swap);
4375
4994
  }
4376
4995
  this.setState(swap, "refunded");
@@ -4443,7 +5062,7 @@ var RfqSwapManager = class {
4443
5062
  /**
4444
5063
  * Record which ark transactions ended the lockup.
4445
5064
  *
4446
- * Only the ones the indexer actually named: `LockupSpend.arkTxid` is
5065
+ * Only the ones the indexer actually named: `LockupSpend.txid` is
4447
5066
  * optional, and a checkpoint txid is not what history correlates on — a
4448
5067
  * record carrying one would name a transaction the wallet's own activity
4449
5068
  * never shows. Fewer txids is the right failure here.
@@ -4453,9 +5072,9 @@ var RfqSwapManager = class {
4453
5072
  * verdict once.
4454
5073
  */
4455
5074
  stampLockupSpends(swap, spends) {
4456
- const arkTxids = spends.map((spend) => spend.arkTxid).filter((txid) => txid !== void 0);
4457
- if (arkTxids.length === 0) return;
4458
- swap.lockupSpendArkTxids = arkTxids;
5075
+ const txids = spends.map((spend) => spend.txid).filter((txid) => txid !== void 0);
5076
+ if (txids.length === 0) return;
5077
+ swap.lockupSpendTxids = txids;
4459
5078
  this.touch(swap);
4460
5079
  }
4461
5080
  touch(swap) {
@@ -4466,6 +5085,7 @@ var RfqSwapManager = class {
4466
5085
  if (swap.state === state) return;
4467
5086
  const previous = swap.state;
4468
5087
  if (previous === "needs_counterparty") delete swap.blockedReason;
5088
+ if (isRfqSwapTerminal(state)) delete swap.claimFailure;
4469
5089
  swap.state = state;
4470
5090
  this.touch(swap);
4471
5091
  notify(this.swapUpdateListeners, (listener) => listener(swap, previous));
@@ -4536,10 +5156,9 @@ var RfqSwapManager = class {
4536
5156
  /**
4537
5157
  * Drop a terminal swap from monitoring and report it exactly once.
4538
5158
  *
4539
- * `onSwapCompleted` and `onSwapFailed` are mutually exclusive here, unlike
4540
- * Boltz's manager, which fires completion for every swap that leaves
4541
- * monitoring including the failed ones — a listener named "completed" that
4542
- * also fires on failure is a trap worth not inheriting.
5159
+ * `onSwapCompleted` and `onSwapFailed` are mutually exclusive here: a
5160
+ * listener named "completed" that also fires on failure is a trap, so a
5161
+ * swap that leaves monitoring reports through exactly one of them.
4543
5162
  */
4544
5163
  finalize(swap) {
4545
5164
  if (!this.monitored.has(swap.rfqId)) return;
@@ -4575,7 +5194,7 @@ var traderClaimTxid = (swap) => {
4575
5194
  case "onchain_send":
4576
5195
  return swap.claimTxid;
4577
5196
  case "lightning_receive":
4578
- return swap.claimArkTxid;
5197
+ return swap.claimTxid;
4579
5198
  default:
4580
5199
  return void 0;
4581
5200
  }
@@ -4585,36 +5204,199 @@ var outcomeOf = (swap) => {
4585
5204
  const lostReceive = swap.kind === "lightning_receive" && swap.state === "refunded";
4586
5205
  return {
4587
5206
  state: swap.state,
4588
- txid: lostReceive ? swap.refundArkTxid : traderClaimTxid(swap) ?? swap.refundArkTxid
5207
+ txid: lostReceive ? swap.refundTxid : traderClaimTxid(swap) ?? swap.refundTxid
4589
5208
  };
4590
5209
  };
4591
5210
  var errorMessage = (error) => error instanceof Error ? error.message : String(error);
4592
5211
  var outpointKey = (vtxo) => `${vtxo.txid}:${vtxo.vout}`;
4593
5212
 
4594
5213
  // src/activity.ts
4595
- var import_sdk16 = require("@arkade-os/sdk");
5214
+ var import_sdk22 = require("@arkade-os/sdk");
4596
5215
  var import_base16 = require("@scure/base");
5216
+
5217
+ // src/client/corridors/lightning.ts
5218
+ var import_sdk20 = require("@arkade-os/sdk");
5219
+ var INVOICE_HRPS = [
5220
+ ["lnbcrt", ["regtest"]],
5221
+ ["lntbs", ["signet", "mutinynet"]],
5222
+ ["lnbc", ["bitcoin"]],
5223
+ ["lntb", ["testnet"]]
5224
+ ];
5225
+ var networksOfInvoiceHrp = (raw) => {
5226
+ const lower = raw.toLowerCase();
5227
+ return INVOICE_HRPS.find(([hrp]) => lower.startsWith(hrp))?.[1];
5228
+ };
5229
+ var PAYMENT_HASH = /^[0-9a-f]{64}$/;
5230
+ var LIGHTNING_DRIVE = {
5231
+ give: {
5232
+ lockups: [{ covenant: "arkade_lockup", owner: "solver", deadline: "refund_locktime" }],
5233
+ actions: ["claimLockup"],
5234
+ seams: ["indexer"]
5235
+ },
5236
+ take: {
5237
+ lockups: [{ covenant: "arkade_lockup", owner: "trader", deadline: "refund_locktime" }],
5238
+ actions: ["refundArkade"],
5239
+ seams: ["indexer"]
5240
+ }
5241
+ };
5242
+ var lightningCorridor = Object.assign(
5243
+ (deps) => ({
5244
+ corridor: "lightning",
5245
+ deps,
5246
+ drive: LIGHTNING_DRIVE,
5247
+ matches(raw) {
5248
+ const invoice = (0, import_sdk20.invoiceTarget)(raw);
5249
+ if (invoice === void 0) return void 0;
5250
+ const networks = networksOfInvoiceHrp(invoice);
5251
+ if (networks === void 0) {
5252
+ return { refused: "this invoice's network prefix names no network this SDK knows" };
5253
+ }
5254
+ if (!networks.includes(deps.networkName)) {
5255
+ return {
5256
+ refused: `this is a ${networks.join(" or ")} invoice and the wallet is on ${deps.networkName}`
5257
+ };
5258
+ }
5259
+ let facts;
5260
+ try {
5261
+ facts = deps.decode(invoice);
5262
+ if (!PAYMENT_HASH.test(facts.paymentHash)) {
5263
+ return { refused: "the decoded invoice carries no usable payment hash" };
5264
+ }
5265
+ } catch (error) {
5266
+ return {
5267
+ refused: `the invoice did not decode: ${error instanceof Error ? error.message : String(error)}`
5268
+ };
5269
+ }
5270
+ if (!Number.isSafeInteger(facts.expiresAt)) {
5271
+ return { refused: "the decoded invoice carries no usable expiry" };
5272
+ }
5273
+ const now = Math.floor(Date.now() / 1e3);
5274
+ if (facts.expiresAt <= now) {
5275
+ return { refused: `the invoice expired ${now - facts.expiresAt}s ago` };
5276
+ }
5277
+ if (!Number.isSafeInteger(facts.amountSats) || facts.amountSats < 0) {
5278
+ return { refused: "the decoded invoice carries no usable amount" };
5279
+ }
5280
+ if (facts.amountSats === 0) {
5281
+ return { refused: "the invoice names no amount, and a send route needs one" };
5282
+ }
5283
+ return {
5284
+ claimed: {
5285
+ kind: "invoice",
5286
+ bolt11: invoice,
5287
+ paymentHash: facts.paymentHash,
5288
+ amount: BigInt(facts.amountSats),
5289
+ expiresAt: facts.expiresAt
5290
+ }
5291
+ };
5292
+ }
5293
+ }),
5294
+ { target: import_sdk20.invoiceTarget }
5295
+ );
5296
+
5297
+ // src/client/corridors/onchain.ts
5298
+ var import_sdk21 = require("@arkade-os/sdk");
5299
+ var btc3 = __toESM(require("@scure/btc-signer"), 1);
5300
+ var ONCHAIN_DRIVE = {
5301
+ take: {
5302
+ lockups: [
5303
+ { covenant: "arkade_lockup", owner: "trader", deadline: "refund_locktime" },
5304
+ { covenant: "onchain_htlc", owner: "solver", deadline: "htlc_refund_locktime" }
5305
+ ],
5306
+ actions: ["claimOnchain", "refundArkade"],
5307
+ seams: ["indexer", "chain"]
5308
+ }
5309
+ };
5310
+ var onchainCorridor = Object.assign(
5311
+ (deps) => {
5312
+ const l1 = l1NetworkFromArk(deps.networkName);
5313
+ return {
5314
+ corridor: "onchain",
5315
+ deps,
5316
+ drive: ONCHAIN_DRIVE,
5317
+ matches(raw) {
5318
+ const target = (0, import_sdk21.btcTarget)(raw);
5319
+ if (target === void 0) return void 0;
5320
+ try {
5321
+ btc3.Address(L1_NETWORKS[l1]).decode(target);
5322
+ } catch {
5323
+ return { refused: `this is not a ${l1} address` };
5324
+ }
5325
+ return { claimed: { kind: "address", address: target } };
5326
+ }
5327
+ };
5328
+ },
5329
+ { target: import_sdk21.btcTarget }
5330
+ );
5331
+
5332
+ // src/client/outcome.ts
5333
+ var CORRIDOR_PASS = {
5334
+ lightning_send: LIGHTNING_DRIVE.take,
5335
+ lightning_receive: LIGHTNING_DRIVE.give,
5336
+ onchain_send: ONCHAIN_DRIVE.take
5337
+ };
5338
+ var LOCKUP_OWNER = {
5339
+ lightning_send: CORRIDOR_PASS.lightning_send.lockups[0].owner,
5340
+ lightning_receive: CORRIDOR_PASS.lightning_receive.lockups[0].owner,
5341
+ onchain_send: CORRIDOR_PASS.onchain_send.lockups[0].owner
5342
+ };
5343
+ var at = (outcome) => () => outcome;
5344
+ var CORRIDOR_OUTCOME = {
5345
+ trader: {
5346
+ pending: at("funded"),
5347
+ claimable: at("funded"),
5348
+ claimed: at("funded"),
5349
+ needs_counterparty: at("needs_recovery"),
5350
+ settled: (kind) => kind === "onchain_send" ? "claimed" : "paid",
5351
+ refunded: at("refunded"),
5352
+ failed: at("failed")
5353
+ },
5354
+ solver: {
5355
+ // The solver has funded nothing yet: the invoice is shown and unpaid,
5356
+ // which is what makes this `open` where the trader's side is `funded`.
5357
+ pending: at("open"),
5358
+ claimable: at("funded"),
5359
+ claimed: at("funded"),
5360
+ needs_counterparty: at("needs_recovery"),
5361
+ // The trader's OWN claim, matched by the hash and not by our txid — so
5362
+ // a claim that lands without us still counts.
5363
+ settled: at("claimed"),
5364
+ // The one inversion this whole module exists for: every non-claim leaf
5365
+ // of a receive covenant is the solver's, so a lockup spent by one is
5366
+ // the trader's incoming payment never arriving.
5367
+ refunded: at("lapsed"),
5368
+ failed: at("failed")
5369
+ }
5370
+ };
5371
+ var corridorOutcome = (kind, state) => CORRIDOR_OUTCOME[LOCKUP_OWNER[kind]][state](kind);
5372
+ var ACTIVITY_TOKEN = {
5373
+ accepted: "pending",
5374
+ funding: "pending",
5375
+ funded: "pending",
5376
+ open: "pending",
5377
+ // The drive's in-flight states have no user-visible phase distinct from
5378
+ // "pending"; `needs_recovery` is different in kind — the swap is BLOCKED —
5379
+ // and collapsing it here is the same deliberate choice v1 made, which the
5380
+ // opaque-token design permits because apps map tokens themselves.
5381
+ needs_recovery: "pending",
5382
+ refunding: "pending",
5383
+ cancelling: "pending",
5384
+ cancelled: "cancelled",
5385
+ filled: "settled",
5386
+ claimed: "settled",
5387
+ paid: "settled",
5388
+ refunded: "refunded",
5389
+ lapsed: "lost",
5390
+ failed: "failed"
5391
+ };
5392
+
5393
+ // src/activity.ts
4597
5394
  var LABELS = {
4598
5395
  lightning_send: "Lightning send",
4599
5396
  lightning_receive: "Lightning receive",
4600
5397
  onchain_send: "Onchain send"
4601
5398
  };
4602
- var OUTCOME = {
4603
- pending: "pending",
4604
- // `claimable` and `claimed` are both in-progress states with no
4605
- // user-visible phase distinct from "pending". `needs_counterparty` is
4606
- // different in kind — the swap is BLOCKED, not merely in flight, since no
4607
- // unilateral trader move exists (see `RfqSwapState`). Collapsing it into
4608
- // `pending` here is a deliberate choice the opaque-token design permits —
4609
- // apps map tokens themselves — but a future reader weighing a `"blocked"`
4610
- // or `"stuck"` token should know this was already considered.
4611
- claimable: "pending",
4612
- claimed: "pending",
4613
- needs_counterparty: "pending",
4614
- settled: "settled",
4615
- refunded: "refunded",
4616
- failed: "failed"
4617
- };
5399
+ var outcomeToken = (kind, state) => ACTIVITY_TOKEN[corridorOutcome(kind, state)];
4618
5400
  function swapActivityResolver(deps) {
4619
5401
  let byTxid = /* @__PURE__ */ new Map();
4620
5402
  return {
@@ -4633,13 +5415,17 @@ function swapActivityResolver(deps) {
4633
5415
  const key = tx.key.arkTxid || tx.key.commitmentTxid || tx.key.boardingTxid;
4634
5416
  const swap = key ? byTxid.get(key) : void 0;
4635
5417
  if (!swap) return void 0;
4636
- const lostReceive = swap.kind === "lightning_receive" && swap.state === "refunded";
4637
5418
  return [
4638
5419
  {
4639
5420
  groupId: `swap:${swap.rfqId}`,
4640
5421
  label: LABELS[swap.kind],
4641
5422
  kind: "swap",
4642
- outcome: lostReceive ? "lost" : OUTCOME[swap.state],
5423
+ // `"lost"` for a receive leg that ended `refunded` comes out
5424
+ // of the translation rather than out of a branch here: that
5425
+ // leg has no trader-side refund, every non-claim leaf of its
5426
+ // covenant is the solver's, and the client's `lapsed` is
5427
+ // that fact.
5428
+ outcome: outcomeToken(swap.kind, swap.state),
4643
5429
  metadata: { rfqId: swap.rfqId, swapKind: swap.kind }
4644
5430
  }
4645
5431
  ];
@@ -4650,16 +5436,17 @@ async function rfqSwapActivityInputs(deps) {
4650
5436
  const records = await deps.repository.getAllRfqSwaps();
4651
5437
  return Promise.all(records.map((record) => activityInputOf(record, deps.indexer)));
4652
5438
  }
4653
- async function activityInputOf(record, indexer) {
5439
+ async function activityInputOf(stored, indexer) {
5440
+ const record = normalizeRfqSwapRecord(stored);
4654
5441
  const txids = /* @__PURE__ */ new Set();
4655
- if (record.fundingArkTxid) txids.add(record.fundingArkTxid);
4656
- if (record.refundArkTxid) txids.add(record.refundArkTxid);
5442
+ if (record.fundingTxid) txids.add(record.fundingTxid);
5443
+ if (record.refundTxid) txids.add(record.refundTxid);
4657
5444
  const handler = rfqCorridorHandlers.getOrThrow(record.kind);
4658
5445
  for (const txid of handler.activityTxids?.(record.profile) ?? []) txids.add(txid);
4659
- for (const txid of record.lockupSpendArkTxids ?? []) txids.add(txid);
4660
- const spendUnknown = isRfqSwapTerminal(record.state) && !record.refundArkTxid && !record.lockupSpendArkTxids?.length;
4661
- if (indexer && (!record.fundingArkTxid || spendUnknown)) {
4662
- for (const txid of await lockupTxids(indexer, record, !record.fundingArkTxid)) {
5446
+ for (const txid of record.lockupSpendTxids ?? []) txids.add(txid);
5447
+ const spendUnknown = isRfqSwapTerminal(record.state) && !record.refundTxid && !record.lockupSpendTxids?.length;
5448
+ if (indexer && (!record.fundingTxid || spendUnknown)) {
5449
+ for (const txid of await lockupTxids(indexer, record, !record.fundingTxid)) {
4663
5450
  txids.add(txid);
4664
5451
  }
4665
5452
  }
@@ -4668,7 +5455,7 @@ async function activityInputOf(record, indexer) {
4668
5455
  async function lockupTxids(indexer, record, wantFunding) {
4669
5456
  let script;
4670
5457
  try {
4671
- script = import_base16.hex.encode(import_sdk16.ArkAddress.decode(record.lockupAddress).pkScript);
5458
+ script = import_base16.hex.encode(import_sdk22.ArkAddress.decode(record.lockupAddress).pkScript);
4672
5459
  } catch {
4673
5460
  return [];
4674
5461
  }
@@ -4684,6 +5471,346 @@ async function lockupTxids(indexer, record, wantFunding) {
4684
5471
  return [];
4685
5472
  }
4686
5473
  }
5474
+
5475
+ // src/swapClient.ts
5476
+ var import_base17 = require("@scure/base");
5477
+ var import_sdk23 = require("@arkade-os/sdk");
5478
+ var import_solver_discovery3 = require("@arkade-os/solver-discovery");
5479
+ var btc4 = __toESM(require("@scure/btc-signer"), 1);
5480
+ var sideCorridorsOf = (market) => {
5481
+ const read = (value) => value === "lightning" || value === "onchain" ? value : "arkade";
5482
+ const m = market;
5483
+ return { base: read(m.base_corridor), quote: read(m.quote_corridor) };
5484
+ };
5485
+ var resolveKind = (market, give) => {
5486
+ const corridors = sideCorridorsOf(market);
5487
+ const giveCorridor = corridors[give];
5488
+ const receiveCorridor = corridors[give === "base" ? "quote" : "base"];
5489
+ if (giveCorridor === "arkade" && receiveCorridor === "arkade") return "spot";
5490
+ if (receiveCorridor === "lightning") return "ln_send";
5491
+ if (giveCorridor === "lightning") return "ln_receive";
5492
+ if (receiveCorridor === "onchain") return "onchain_send";
5493
+ return "onchain_receive";
5494
+ };
5495
+ var need = (value, what, leg) => {
5496
+ if (value === void 0) throw new Error(`a ${leg} quote needs ${what}`);
5497
+ return value;
5498
+ };
5499
+ var payoutScriptFor = (address, payoutPubkey, network) => address !== void 0 ? l1ScriptForAddress(address, network) : btc4.p2tr(payoutPubkey, void 0, L1_NETWORKS[network]).script;
5500
+ var corridorAmount = (input, leg) => {
5501
+ const raw = need(input.amount, "an amount", leg);
5502
+ const amountOn = need(input.amountOn, "amountOn ('give' or 'receive')", leg);
5503
+ const amount = Number(raw);
5504
+ if (!Number.isSafeInteger(amount) || amount <= 0) {
5505
+ throw new Error(`a ${leg} amount must be a positive integer of sats, got ${String(raw)}`);
5506
+ }
5507
+ return { amount, amountSide: amountOn === "give" ? "from" : "to" };
5508
+ };
5509
+ function createSwapClient(deps) {
5510
+ const { wallet, repository, transportFor } = deps;
5511
+ const ark = deps.ark ?? walletOperator(wallet);
5512
+ let reader;
5513
+ const reading = () => reader ??= wallet.getArkadeReader();
5514
+ const indexer = deps.indexer ?? {
5515
+ getVtxos: async (opts) => {
5516
+ if (!opts)
5517
+ throw new Error("getVtxos on the swap indexer requires scripts or outpoints");
5518
+ return (await reading()).getVtxos(opts);
5519
+ },
5520
+ getVirtualTxs: async (...args) => (await reading()).getVirtualTxs(...args)
5521
+ };
5522
+ const managerDeps = { indexer, chain: deps.chain, repository };
5523
+ const manager = new RfqSwapManager(managerDeps);
5524
+ const listeners = /* @__PURE__ */ new Set();
5525
+ const notify2 = (swap) => {
5526
+ for (const listener of listeners) {
5527
+ try {
5528
+ listener(swap);
5529
+ } catch {
5530
+ }
5531
+ }
5532
+ };
5533
+ manager.onSwapUpdate((swap) => notify2({ family: "rfq", swap }));
5534
+ const claimLockup = async (swap, vtxos, { partiallyClaimed }) => {
5535
+ const record = await repository.getRfqSwap(swap.rfqId);
5536
+ if (!record) throw new Error(`rfq swap ${swap.rfqId} has no stored record to claim from`);
5537
+ const secret = rfqClaimSecretOf(record);
5538
+ if (!secret) throw new Error(`rfq swap ${swap.rfqId} carries no claim secret`);
5539
+ const script = swap.lockup?.script;
5540
+ if (!script) throw new Error(`rfq swap ${swap.rfqId} carries no lockup covenant`);
5541
+ const payoutAddress = rfqClaimDestinationOf(record);
5542
+ if (!payoutAddress) {
5543
+ throw new Error(`rfq swap ${swap.rfqId} carries no claim destination`);
5544
+ }
5545
+ return pushClaim(ark, {
5546
+ contract: script,
5547
+ receiver: await (0, import_sdk23.contractSigner)(wallet, secret.signingDescriptor),
5548
+ preimage: await preimageForSwapRecord(wallet, secret),
5549
+ vtxos,
5550
+ destinationPkScript: import_sdk23.ArkAddress.decode(payoutAddress).pkScript,
5551
+ expectedAmount: swap.expectedAmount,
5552
+ partiallyClaimed
5553
+ });
5554
+ };
5555
+ manager.setCallbacks({
5556
+ refundArkade: arkadeRefunder({ operator: ark, indexer, wallet, repository }),
5557
+ claimLockup,
5558
+ ...deps.claimOnchain ? { claimOnchain: deps.claimOnchain } : {}
5559
+ });
5560
+ let watcher;
5561
+ const quote = async (market, input) => {
5562
+ const kind = resolveKind(market, input.give);
5563
+ switch (kind) {
5564
+ case "spot": {
5565
+ const raw = need(input.amount, "an amount", kind);
5566
+ const amountOn = need(input.amountOn, "amountOn ('give' or 'receive')", kind);
5567
+ const plan = await (0, import_solver_discovery3.quoteOffer)(market, {
5568
+ give: input.give,
5569
+ ...amountOn === "give" ? { giveAmount: raw } : { wantAmount: raw },
5570
+ fetchImpl: deps.discovery.fetchImpl
5571
+ });
5572
+ return { kind, market, plan };
5573
+ }
5574
+ case "ln_send": {
5575
+ const invoice = need(input.invoice, "the invoice to pay", kind);
5576
+ const request = await requestLightningSend(wallet, transportFor(market), {
5577
+ invoice,
5578
+ emulatorPubkey: deps.emulatorPubkey
5579
+ });
5580
+ return { kind, market, request };
5581
+ }
5582
+ case "ln_receive": {
5583
+ const params = {
5584
+ ...corridorAmount(input, kind),
5585
+ covclaimdPubkey: need(deps.covclaimdPubkey, "deps.covclaimdPubkey", kind),
5586
+ decodeInvoice: need(deps.decodeInvoice, "deps.decodeInvoice", kind),
5587
+ maxPayAmount: input.maxPayAmount,
5588
+ emulatorPubkey: deps.emulatorPubkey
5589
+ };
5590
+ const request = await requestLightningReceive(wallet, transportFor(market), params);
5591
+ return { kind, market, request, invoice: request.invoice };
5592
+ }
5593
+ case "onchain_send": {
5594
+ need(deps.chain, "deps.chain (L1 access)", kind);
5595
+ const payoutPubkey = need(input.payoutPubkey, "a payoutPubkey", kind);
5596
+ const params = {
5597
+ ...corridorAmount(input, kind),
5598
+ payoutPubkey,
5599
+ preimage: input.preimage,
5600
+ emulatorPubkey: deps.emulatorPubkey
5601
+ };
5602
+ const request = await requestOnchainSend(wallet, transportFor(market), params);
5603
+ return {
5604
+ kind,
5605
+ market,
5606
+ request,
5607
+ payoutPkScript: payoutScriptFor(
5608
+ input.payoutAddress,
5609
+ payoutPubkey,
5610
+ request.l1Network
5611
+ )
5612
+ };
5613
+ }
5614
+ case "onchain_receive":
5615
+ throw new Error(
5616
+ "onchain->arkade is not driven by RfqSwapManager yet; quote it directly with requestOnchainReceive"
5617
+ );
5618
+ }
5619
+ };
5620
+ const admit = async (swap, origin) => {
5621
+ let announced = false;
5622
+ const heard = manager.onSwapUpdate((updated) => {
5623
+ if (updated.rfqId === swap.rfqId) announced = true;
5624
+ });
5625
+ try {
5626
+ await manager.addSwap(swap, origin);
5627
+ } finally {
5628
+ heard();
5629
+ }
5630
+ const unified = { family: "rfq", swap };
5631
+ if (!announced) notify2(unified);
5632
+ return unified;
5633
+ };
5634
+ const fundPersisted = async (swap, origin, funding) => {
5635
+ await repository.saveRfqSwap(createRfqSwapRecord(origin, swap));
5636
+ return wallet.send({ address: funding.address, amount: funding.amount });
5637
+ };
5638
+ const accept = async (accepted) => {
5639
+ const now = Math.floor(Date.now() / 1e3);
5640
+ switch (accepted.kind) {
5641
+ case "spot": {
5642
+ const { plan } = accepted;
5643
+ const depositIsBtc = plan.deposit.asset.id === BTC_ASSET_ID;
5644
+ const offer = await createOffer(wallet, {
5645
+ wantAmount: plan.receive.atomic,
5646
+ ...plan.receive.asset.id === BTC_ASSET_ID ? { offerAsset: import_sdk23.asset.AssetId.fromString(plan.deposit.asset.id) } : { wantAsset: import_sdk23.asset.AssetId.fromString(plan.receive.asset.id) },
5647
+ emulatorPubkey: deps.emulatorPubkey
5648
+ });
5649
+ const txid = await wallet.send({
5650
+ address: offer.address,
5651
+ // asset deposits ride the SDK's dust-sat carrier
5652
+ amount: depositIsBtc ? Number(plan.deposit.atomic) : void 0,
5653
+ assets: depositIsBtc ? void 0 : [{ assetId: plan.deposit.asset.id, amount: plan.deposit.atomic }],
5654
+ extensions: [offer.extension]
5655
+ });
5656
+ const swap = {
5657
+ id: txid,
5658
+ fromAsset: plan.deposit.asset.id,
5659
+ toAsset: plan.receive.asset.id,
5660
+ fromAmount: plan.deposit.atomic.toString(),
5661
+ toAmount: plan.receive.atomic.toString(),
5662
+ swapAddress: offer.address,
5663
+ swapPkScript: import_base17.hex.encode(offer.swapPkScript),
5664
+ offerHex: offer.offerHex,
5665
+ fundingTxid: txid,
5666
+ status: "pending",
5667
+ createdAt: Date.now()
5668
+ };
5669
+ await addAssetSwap(repository, swap);
5670
+ const unified = { family: "offer", swap };
5671
+ notify2(unified);
5672
+ return unified;
5673
+ }
5674
+ case "ln_send": {
5675
+ const { request } = accepted;
5676
+ const swap = {
5677
+ kind: "lightning_send",
5678
+ rfqId: request.rfqId,
5679
+ state: "pending",
5680
+ lockupPkScript: request.swapPkScript,
5681
+ lockup: { script: request.script, address: request.address },
5682
+ // the trader's own decode, never the solver's echo
5683
+ paymentHash: request.contractParams.paymentHash,
5684
+ refundLocktime: request.contractParams.refundLocktime,
5685
+ createdAt: now,
5686
+ updatedAt: now
5687
+ };
5688
+ const origin = {
5689
+ kind: "lightning_send",
5690
+ lockupAddress: request.address,
5691
+ profile: rfqSecretsProfile(request.secrets, swap.paymentHash),
5692
+ amount: request.fundAmount
5693
+ };
5694
+ const txid = await fundPersisted(swap, origin, {
5695
+ address: request.address,
5696
+ amount: request.fundAmount
5697
+ });
5698
+ return admit(swap, { ...origin, fundingTxid: txid });
5699
+ }
5700
+ case "ln_receive": {
5701
+ const { request } = accepted;
5702
+ const paymentHash = import_base17.hex.encode(request.secrets.paymentHash);
5703
+ const swap = {
5704
+ kind: "lightning_receive",
5705
+ rfqId: request.rfqId,
5706
+ state: "pending",
5707
+ lockupPkScript: request.swapPkScript,
5708
+ lockup: { script: request.script, address: request.address },
5709
+ paymentHash,
5710
+ refundLocktime: request.contractParams.refundLocktime,
5711
+ expectedAmount: request.expectedAmount,
5712
+ createdAt: now,
5713
+ updatedAt: now
5714
+ };
5715
+ return admit(swap, {
5716
+ kind: "lightning_receive",
5717
+ lockupAddress: request.address,
5718
+ profile: {
5719
+ ...rfqSecretsProfile(request.secrets, paymentHash),
5720
+ expectedAmount: request.expectedAmount,
5721
+ payoutAddress: request.payoutAddress
5722
+ },
5723
+ amount: request.expectedAmount
5724
+ });
5725
+ }
5726
+ case "onchain_send": {
5727
+ const { request } = accepted;
5728
+ const paymentHash = import_base17.hex.encode(request.secrets.paymentHash);
5729
+ const swap = {
5730
+ kind: "onchain_send",
5731
+ rfqId: request.rfqId,
5732
+ state: "pending",
5733
+ lockupPkScript: request.swapPkScript,
5734
+ lockup: { script: request.script, address: request.address },
5735
+ paymentHash,
5736
+ refundLocktime: request.refundLocktime,
5737
+ htlc: request.htlc,
5738
+ minConfirmations: request.minConfirmations,
5739
+ payoutPkScript: accepted.payoutPkScript,
5740
+ createdAt: now,
5741
+ updatedAt: now
5742
+ };
5743
+ const origin = {
5744
+ kind: "onchain_send",
5745
+ lockupAddress: request.address,
5746
+ profile: {
5747
+ ...rfqSecretsProfile(request.secrets, paymentHash),
5748
+ ...onchainSendProfile({
5749
+ ...request,
5750
+ payoutPkScript: accepted.payoutPkScript
5751
+ })
5752
+ },
5753
+ amount: request.fundAmount
5754
+ };
5755
+ const txid = await fundPersisted(swap, origin, {
5756
+ address: request.address,
5757
+ amount: request.fundAmount
5758
+ });
5759
+ return admit(swap, { ...origin, fundingTxid: txid });
5760
+ }
5761
+ }
5762
+ };
5763
+ return {
5764
+ manager,
5765
+ markets: (useCache) => discoverMarkets({ ...deps.discovery, repository, useCache }),
5766
+ quote,
5767
+ accept,
5768
+ cancel: async (fundingTxid) => {
5769
+ const swaps = await getAssetSwapsOrThrow(repository);
5770
+ const swap = swaps.find((s) => s.id === fundingTxid);
5771
+ if (!swap) throw new Error(`no offer swap with funding txid ${fundingTxid}`);
5772
+ if (swap.status !== "pending") {
5773
+ throw new Error(`offer swap ${fundingTxid} is ${swap.status}, not cancellable`);
5774
+ }
5775
+ await updateAssetSwap(repository, fundingTxid, { status: "cancelling" });
5776
+ await cancelOffer(wallet, swap.offerHex, {
5777
+ repository,
5778
+ fundingTxid
5779
+ });
5780
+ },
5781
+ // Both families, live AND terminal — the offer half reads the whole
5782
+ // stored history, and the RFQ half is every swap the manager holds.
5783
+ // Its history is what `start()` restored plus what this session
5784
+ // accepted; records past `RFQ_SWAP_RETENTION_SECONDS` are pruned.
5785
+ swaps: async () => {
5786
+ const offers = await getAssetSwaps(repository);
5787
+ const rfq = await manager.getAllSwaps();
5788
+ return [
5789
+ ...offers.map((swap) => ({ family: "offer", swap })),
5790
+ ...rfq.map((swap) => ({ family: "rfq", swap }))
5791
+ ];
5792
+ },
5793
+ onUpdate: (listener) => {
5794
+ listeners.add(listener);
5795
+ return () => listeners.delete(listener);
5796
+ },
5797
+ start: async () => {
5798
+ managerDeps.contracts ??= await wallet.getContractManager();
5799
+ await manager.restoreFromRepository();
5800
+ await manager.start();
5801
+ watcher ??= await watchOfferSwaps({
5802
+ wallet,
5803
+ repository,
5804
+ onUpdate: (swap) => notify2({ family: "offer", swap })
5805
+ });
5806
+ },
5807
+ stop: async () => {
5808
+ await manager.stop();
5809
+ watcher?.stop();
5810
+ watcher = void 0;
5811
+ }
5812
+ };
5813
+ }
4687
5814
  // Annotate the CommonJS export names for ESM import in node:
4688
5815
  0 && (module.exports = {
4689
5816
  ARKADE_ASSET,
@@ -4693,6 +5820,7 @@ async function lockupTxids(indexer, record, wantFunding) {
4693
5820
  InMemoryAssetSwapRepository,
4694
5821
  IndexedDbAssetSwapRepository,
4695
5822
  LIGHTNING_BTC,
5823
+ LIGHTNING_RAIL,
4696
5824
  LIGHTNING_RECEIVE_PAIR,
4697
5825
  LIGHTNING_SEND_PAIR,
4698
5826
  LOCKTIME_THRESHOLD,
@@ -4703,6 +5831,7 @@ async function lockupTxids(indexer, record, wantFunding) {
4703
5831
  MAX_MIN_CONFIRMATIONS,
4704
5832
  MIN_CLAIM_WINDOW_SECONDS,
4705
5833
  MIN_HEADROOM_SECONDS,
5834
+ NoSpendableDepositError,
4706
5835
  OFFER_PACKET_TYPE,
4707
5836
  ONCHAIN_BTC,
4708
5837
  ONCHAIN_CLAIM_MARGIN_SECONDS,
@@ -4711,6 +5840,9 @@ async function lockupTxids(indexer, record, wantFunding) {
4711
5840
  ONCHAIN_RECEIVE_PAIR,
4712
5841
  ONCHAIN_SECONDS_PER_BLOCK,
4713
5842
  ONCHAIN_SEND_PAIR,
5843
+ ONCHAIN_SWAP_RAIL,
5844
+ OfferCovenantMismatchError,
5845
+ PAYMENT_STATUS,
4714
5846
  PreimageNotRecoverableError,
4715
5847
  QUOTE_OPTIONS,
4716
5848
  REFUND_MTP_LAG_SECONDS,
@@ -4727,6 +5859,8 @@ async function lockupTxids(indexer, record, wantFunding) {
4727
5859
  SWAP_LOCKUP_CONTRACT_KIND,
4728
5860
  SWAP_LOCKUP_CONTRACT_LABEL,
4729
5861
  SWAP_LOCKUP_CONTRACT_TYPE,
5862
+ SWAP_ROUTER_PRIORITY,
5863
+ SwapPaymentFailedError,
4730
5864
  SwapRefusal,
4731
5865
  addAssetSwap,
4732
5866
  arkadeAssetLeg,
@@ -4741,6 +5875,7 @@ async function lockupTxids(indexer, record, wantFunding) {
4741
5875
  buildHtlcRefund,
4742
5876
  cancelOffer,
4743
5877
  chainSourceFrom,
5878
+ claimFeeSats,
4744
5879
  claimOnchainFill,
4745
5880
  claimReceiveLockup,
4746
5881
  classifyDepositSpend,
@@ -4748,6 +5883,8 @@ async function lockupTxids(indexer, record, wantFunding) {
4748
5883
  classifySpend,
4749
5884
  createOffer,
4750
5885
  createRfqSwapRecord,
5886
+ createSwapClient,
5887
+ createSwapPaymentRouter,
4751
5888
  decodeOffer,
4752
5889
  deriveLightningReceive,
4753
5890
  deriveOnchainReceive,
@@ -4762,28 +5899,35 @@ async function lockupTxids(indexer, record, wantFunding) {
4762
5899
  httpTransport,
4763
5900
  isRfqSwapTerminal,
4764
5901
  isRfqTerminal,
5902
+ isTerminalStatus,
4765
5903
  l1ScriptForAddress,
5904
+ lightningRail,
5905
+ lightningReceiveContract,
4766
5906
  lightningReceiveRequest,
5907
+ lightningSendContract,
4767
5908
  lightningSendRequest,
4768
- lightningSendVtxoScript,
4769
5909
  lockupContractParams,
4770
5910
  makeCachedFeedFetch,
4771
5911
  newPreimage,
4772
5912
  newRfqId,
4773
5913
  nextOnchainAction,
5914
+ normalizeRfqSwapRecord,
5915
+ offerContract,
4774
5916
  offerTermsFromQuote,
4775
- offerVtxoScript,
4776
5917
  onchainHtlcScript,
4777
5918
  onchainReceiveRequest,
4778
5919
  onchainSendProfile,
4779
5920
  onchainSendRequest,
5921
+ onchainSwapRail,
4780
5922
  paymentHashOf,
5923
+ paymentStatusOf,
4781
5924
  preimageForSwapRecord,
4782
5925
  pushClaim,
4783
5926
  pushRefundWithoutReceiver,
5927
+ railAvailable,
4784
5928
  readLockupFate,
4785
5929
  rebuildRfqSwap,
4786
- receiveVtxoScript,
5930
+ receiverExact,
4787
5931
  refundIfUnresolved,
4788
5932
  registerLockupContract,
4789
5933
  relayTransport,
@@ -4810,6 +5954,7 @@ async function lockupTxids(indexer, record, wantFunding) {
4810
5954
  spendTxidsOf,
4811
5955
  spendUpdate,
4812
5956
  swapActivityResolver,
5957
+ swapHandle,
4813
5958
  swapPrograms,
4814
5959
  swapSecretsToRecord,
4815
5960
  unilateralClaimDelay,