@arkade-os/swap 0.0.7 → 0.0.9

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
@@ -39,7 +39,9 @@ __export(index_exports, {
39
39
  LIGHTNING_BTC: () => LIGHTNING_BTC,
40
40
  LIGHTNING_RECEIVE_PAIR: () => LIGHTNING_RECEIVE_PAIR,
41
41
  LIGHTNING_SEND_PAIR: () => LIGHTNING_SEND_PAIR,
42
+ LOCKTIME_THRESHOLD: () => LOCKTIME_THRESHOLD,
42
43
  LockupAmountMismatchError: () => LockupAmountMismatchError,
44
+ LockupContractMissing: () => LockupContractMissing,
43
45
  LockupNeedsRecoveryError: () => LockupNeedsRecoveryError,
44
46
  LockupRegistrationFailed: () => LockupRegistrationFailed,
45
47
  MAX_MIN_CONFIRMATIONS: () => MAX_MIN_CONFIRMATIONS,
@@ -57,10 +59,12 @@ __export(index_exports, {
57
59
  QUOTE_OPTIONS: () => QUOTE_OPTIONS,
58
60
  REFUND_MTP_LAG_SECONDS: () => REFUND_MTP_LAG_SECONDS,
59
61
  RFQ_RESOLVED_STATES: () => RFQ_RESOLVED_STATES,
62
+ RFQ_SWAP_RETENTION_SECONDS: () => RFQ_SWAP_RETENTION_SECONDS,
60
63
  RFQ_SWAP_TERMINAL_STATES: () => RFQ_SWAP_TERMINAL_STATES,
61
64
  RFQ_TERMINAL_STATES: () => RFQ_TERMINAL_STATES,
62
65
  RefundNotLocallyPossibleError: () => RefundNotLocallyPossibleError,
63
66
  RfqSwapManager: () => RfqSwapManager,
67
+ RfqSwapOriginRequired: () => RfqSwapOriginRequired,
64
68
  SOLO_REFUND_HEADROOM_SECONDS: () => SOLO_REFUND_HEADROOM_SECONDS,
65
69
  SWAP_LOCKUP_CONTRACT_KIND: () => SWAP_LOCKUP_CONTRACT_KIND,
66
70
  SWAP_LOCKUP_CONTRACT_LABEL: () => SWAP_LOCKUP_CONTRACT_LABEL,
@@ -68,6 +72,7 @@ __export(index_exports, {
68
72
  SwapRefusal: () => SwapRefusal,
69
73
  addAssetSwap: () => addAssetSwap,
70
74
  arkadeAssetLeg: () => arkadeAssetLeg,
75
+ arkadeRefunder: () => arkadeRefunder,
71
76
  arkadeSwapRequest: () => arkadeSwapRequest,
72
77
  assertFundable: () => assertFundable,
73
78
  assertReceivable: () => assertReceivable,
@@ -83,6 +88,7 @@ __export(index_exports, {
83
88
  classifyOnchainHtlc: () => classifyOnchainHtlc,
84
89
  classifySpend: () => classifySpend,
85
90
  createOffer: () => createOffer,
91
+ createRfqSwapRecord: () => createRfqSwapRecord,
86
92
  decodeOffer: () => decodeOffer,
87
93
  deriveLightningReceive: () => deriveLightningReceive,
88
94
  deriveOnchainReceive: () => deriveOnchainReceive,
@@ -100,6 +106,7 @@ __export(index_exports, {
100
106
  lightningReceiveRequest: () => lightningReceiveRequest,
101
107
  lightningSendRequest: () => lightningSendRequest,
102
108
  lightningSendVtxoScript: () => lightningSendVtxoScript,
109
+ lockupContractParams: () => lockupContractParams,
103
110
  makeCachedFeedFetch: () => makeCachedFeedFetch,
104
111
  newPreimage: () => newPreimage,
105
112
  newRfqId: () => newRfqId,
@@ -108,12 +115,14 @@ __export(index_exports, {
108
115
  offerVtxoScript: () => offerVtxoScript,
109
116
  onchainHtlcScript: () => onchainHtlcScript,
110
117
  onchainReceiveRequest: () => onchainReceiveRequest,
118
+ onchainSendProfile: () => onchainSendProfile,
111
119
  onchainSendRequest: () => onchainSendRequest,
112
120
  paymentHashOf: () => paymentHashOf,
113
121
  preimageForSwapRecord: () => preimageForSwapRecord,
114
122
  pushClaim: () => pushClaim,
115
123
  pushRefundWithoutReceiver: () => pushRefundWithoutReceiver,
116
124
  readLockupFate: () => readLockupFate,
125
+ rebuildRfqSwap: () => rebuildRfqSwap,
117
126
  receiveVtxoScript: () => receiveVtxoScript,
118
127
  refundIfUnresolved: () => refundIfUnresolved,
119
128
  registerLockupContract: () => registerLockupContract,
@@ -124,9 +133,15 @@ __export(index_exports, {
124
133
  requestOnchainSend: () => requestOnchainSend,
125
134
  restoreAssetSwaps: () => restoreAssetSwaps,
126
135
  retireSettledOfferContracts: () => retireSettledOfferContracts,
136
+ rfqClaimSecretOf: () => rfqClaimSecretOf,
127
137
  rfqPair: () => rfqPair,
138
+ rfqSecretsProfile: () => rfqSecretsProfile,
139
+ rfqSignerOf: () => rfqSignerOf,
140
+ rfqSwapActivityInputs: () => rfqSwapActivityInputs,
141
+ rfqSwapOriginOf: () => rfqSwapOriginOf,
128
142
  sealClaimPacket: () => sealClaimPacket,
129
143
  senderIdentityForSwapRecord: () => senderIdentityForSwapRecord,
144
+ shouldRetainRfqSwap: () => shouldRetainRfqSwap,
130
145
  spendTxidsOf: () => spendTxidsOf,
131
146
  spendUpdate: () => spendUpdate,
132
147
  swapActivityResolver: () => swapActivityResolver,
@@ -137,6 +152,7 @@ __export(index_exports, {
137
152
  unilateralRefundWithoutReceiverDelay: () => unilateralRefundWithoutReceiverDelay,
138
153
  updateAssetSwap: () => updateAssetSwap,
139
154
  updateAssetSwapBestEffort: () => updateAssetSwapBestEffort,
155
+ updateRfqSwapRecord: () => updateRfqSwapRecord,
140
156
  validatePlan: () => validatePlan,
141
157
  verifyLockupAddress: () => verifyLockupAddress,
142
158
  verifyReceiveInvoice: () => verifyReceiveInvoice,
@@ -762,8 +778,9 @@ var validatePlan = (plan, giveBalance, dust) => {
762
778
  // src/repository.ts
763
779
  var marketsCacheKey = (network, registry) => `arkade-intents-markets-${network}-${registry}`;
764
780
  var InMemoryAssetSwapRepository = class {
765
- version = 2;
781
+ version = 4;
766
782
  swaps = /* @__PURE__ */ new Map();
783
+ rfqSwaps = /* @__PURE__ */ new Map();
767
784
  scanned = /* @__PURE__ */ new Set();
768
785
  markets = /* @__PURE__ */ new Map();
769
786
  async saveSwap(swap) {
@@ -772,6 +789,18 @@ var InMemoryAssetSwapRepository = class {
772
789
  async getAllSwaps() {
773
790
  return [...this.swaps.values()];
774
791
  }
792
+ async saveRfqSwap(record) {
793
+ this.rfqSwaps.set(record.rfqId, record);
794
+ }
795
+ async getRfqSwap(rfqId) {
796
+ return this.rfqSwaps.get(rfqId);
797
+ }
798
+ async getAllRfqSwaps() {
799
+ return [...this.rfqSwaps.values()];
800
+ }
801
+ async removeRfqSwap(rfqId) {
802
+ this.rfqSwaps.delete(rfqId);
803
+ }
775
804
  async getScannedTxids() {
776
805
  return new Set(this.scanned);
777
806
  }
@@ -786,6 +815,7 @@ var InMemoryAssetSwapRepository = class {
786
815
  }
787
816
  async clear() {
788
817
  this.swaps.clear();
818
+ this.rfqSwaps.clear();
789
819
  this.scanned.clear();
790
820
  this.markets.clear();
791
821
  }
@@ -796,58 +826,44 @@ var InMemoryAssetSwapRepository = class {
796
826
  // src/indexedDbRepository.ts
797
827
  var import_sdk4 = require("@arkade-os/sdk");
798
828
  var DEFAULT_DB_NAME = "arkade-intents";
799
- var DB_VERSION = 1;
829
+ var DB_VERSION = 2;
800
830
  var STORE_SWAPS = "swaps";
831
+ var STORE_RFQ_SWAPS = "rfqSwaps";
801
832
  var STORE_SCANNED = "scannedTxids";
802
833
  var STORE_MARKETS = "markets";
803
834
  var STORES = [
804
835
  [STORE_SWAPS, { keyPath: "id" }],
836
+ // v2. Separate from `swaps` rather than sharing it: the two record types
837
+ // have different keys and no consumer wants them interleaved.
838
+ [STORE_RFQ_SWAPS, { keyPath: "rfqId" }],
805
839
  [STORE_SCANNED],
806
840
  [STORE_MARKETS]
807
841
  ];
808
- function initDatabase(db) {
842
+ function initDatabase(db, oldVersion, transaction) {
843
+ void oldVersion;
844
+ void transaction;
809
845
  for (const [name, options] of STORES) {
810
846
  if (!db.objectStoreNames.contains(name)) db.createObjectStore(name, options);
811
847
  }
812
848
  }
813
- var request = (req) => new Promise((resolve, reject) => {
814
- req.onsuccess = () => resolve(req.result);
815
- req.onerror = () => reject(req.error);
816
- });
817
- var txDone = (tx) => new Promise((resolve, reject) => {
818
- tx.oncomplete = () => resolve();
819
- tx.onerror = () => reject(tx.error);
820
- tx.onabort = () => reject(tx.error);
821
- });
822
849
  var IndexedDbAssetSwapRepository = class {
850
+ version = 4;
851
+ connection;
823
852
  constructor(dbName = DEFAULT_DB_NAME) {
824
- this.dbName = dbName;
825
- }
826
- dbName;
827
- version = 2;
828
- // the promise, not the resolved database: openDatabase bumps a refcount on
829
- // every call including cache hits, while dispose closes once, so two
830
- // concurrent first calls would strand the refcount above zero and leak the
831
- // connection for the process lifetime. Cleared on failure so a failed open
832
- // can be retried rather than cached forever.
833
- dbPromise = null;
853
+ this.connection = (0, import_sdk4.createManagedConnection)(dbName, DB_VERSION, initDatabase);
854
+ }
834
855
  ensureDb() {
835
- return this.dbPromise ??= (0, import_sdk4.openDatabase)(this.dbName, DB_VERSION, initDatabase).catch(
836
- (err) => {
837
- this.dbPromise = null;
838
- throw err;
839
- }
840
- );
856
+ return this.connection.get();
841
857
  }
842
858
  async readStore(name) {
843
859
  return (await this.ensureDb()).transaction([name], "readonly").objectStore(name);
844
860
  }
845
861
  /** Every write in one place, so none of them can forget to await the
846
862
  * commit. Requests need no individual await: a failed one aborts the
847
- * transaction, which `txDone` reports. */
863
+ * transaction, which `awaitTransaction` reports. */
848
864
  async write(name, apply) {
849
865
  const tx = (await this.ensureDb()).transaction([name], "readwrite");
850
- const done = txDone(tx);
866
+ const done = (0, import_sdk4.awaitTransaction)(tx);
851
867
  apply(tx.objectStore(name));
852
868
  await done;
853
869
  }
@@ -857,10 +873,26 @@ var IndexedDbAssetSwapRepository = class {
857
873
  });
858
874
  }
859
875
  async getAllSwaps() {
860
- return request((await this.readStore(STORE_SWAPS)).getAll());
876
+ return (0, import_sdk4.promisifyRequest)((await this.readStore(STORE_SWAPS)).getAll());
877
+ }
878
+ async saveRfqSwap(record) {
879
+ await this.write(STORE_RFQ_SWAPS, (store) => {
880
+ store.put(record);
881
+ });
882
+ }
883
+ async getRfqSwap(rfqId) {
884
+ return (0, import_sdk4.promisifyRequest)((await this.readStore(STORE_RFQ_SWAPS)).get(rfqId));
885
+ }
886
+ async getAllRfqSwaps() {
887
+ return (0, import_sdk4.promisifyRequest)((await this.readStore(STORE_RFQ_SWAPS)).getAll());
888
+ }
889
+ async removeRfqSwap(rfqId) {
890
+ await this.write(STORE_RFQ_SWAPS, (store) => {
891
+ store.delete(rfqId);
892
+ });
861
893
  }
862
894
  async getScannedTxids() {
863
- const keys = await request((await this.readStore(STORE_SCANNED)).getAllKeys());
895
+ const keys = await (0, import_sdk4.promisifyRequest)((await this.readStore(STORE_SCANNED)).getAllKeys());
864
896
  return new Set(keys);
865
897
  }
866
898
  async markTxidsScanned(txids) {
@@ -870,7 +902,7 @@ var IndexedDbAssetSwapRepository = class {
870
902
  }
871
903
  async getCachedMarkets(network, registry) {
872
904
  const store = await this.readStore(STORE_MARKETS);
873
- return request(store.get(marketsCacheKey(network, registry)));
905
+ return (0, import_sdk4.promisifyRequest)(store.get(marketsCacheKey(network, registry)));
874
906
  }
875
907
  async saveCachedMarkets(network, registry, entry) {
876
908
  await this.write(STORE_MARKETS, (store) => {
@@ -883,280 +915,148 @@ var IndexedDbAssetSwapRepository = class {
883
915
  async clear() {
884
916
  const stores = STORES.map(([name]) => name);
885
917
  const tx = (await this.ensureDb()).transaction(stores, "readwrite");
886
- const done = txDone(tx);
918
+ const done = (0, import_sdk4.awaitTransaction)(tx);
887
919
  for (const name of stores) tx.objectStore(name).clear();
888
920
  await done;
889
921
  }
890
922
  async [Symbol.asyncDispose]() {
891
- if (!this.dbPromise) return;
892
- await (0, import_sdk4.closeDatabase)(this.dbName);
893
- this.dbPromise = null;
923
+ await this.connection[Symbol.asyncDispose]();
894
924
  }
895
925
  };
896
926
 
897
- // src/restore.ts
898
- var import_base3 = require("@scure/base");
899
- var import_sdk5 = require("@arkade-os/sdk");
900
- var TXS_PER_REQUEST = 50;
901
- async function fetchParsedTxs(indexer, txids) {
902
- const parsedByTxid = /* @__PURE__ */ new Map();
903
- if (txids.length === 0) return parsedByTxid;
904
- const chunks = [];
905
- for (let i = 0; i < txids.length; i += TXS_PER_REQUEST) {
906
- chunks.push(txids.slice(i, i + TXS_PER_REQUEST));
907
- }
908
- const chunkResults = await Promise.allSettled(
909
- chunks.map(async (ids) => (await indexer.getVirtualTxs(ids)).txs)
910
- );
911
- for (const result of chunkResults) {
912
- if (result.status !== "fulfilled") continue;
913
- for (const psbt of result.value) {
914
- try {
915
- const parsed = import_sdk5.Transaction.fromPSBT(import_base3.base64.decode(psbt));
916
- parsedByTxid.set(parsed.id, parsed);
917
- } catch {
918
- }
927
+ // src/rfqCorridors.ts
928
+ var import_base4 = require("@scure/base");
929
+
930
+ // src/rfqCorridor.ts
931
+ var RfqCorridorRegistry = class {
932
+ handlers = /* @__PURE__ */ new Map();
933
+ register(handler) {
934
+ if (this.handlers.has(handler.kind)) {
935
+ throw new Error(
936
+ `RFQ corridor handler for kind '${handler.kind}' is already registered`
937
+ );
919
938
  }
939
+ this.handlers.set(handler.kind, handler);
920
940
  }
921
- return parsedByTxid;
922
- }
923
- var unscannedSwapCandidates = (txs, existingIds, scanned) => txs.filter(
924
- (tx) => tx.type === "sent" && tx.redeemTxid && !existingIds.has(tx.redeemTxid) && !scanned.has(tx.redeemTxid)
925
- );
926
- function classifySpend(offer, serverPubkey, spendTx, deposit) {
927
- let leaves;
928
- try {
929
- const script = offerVtxoScript(offer, serverPubkey);
930
- if (import_base3.hex.encode(script.pkScript) !== import_base3.hex.encode(offer.swapPkScript)) return "indeterminate";
931
- leaves = {
932
- cancel: script.functionByName("cancel")?.leafScript,
933
- fulfill: script.functionByName("fulfill")?.leafScript
934
- };
935
- } catch {
936
- return "indeterminate";
937
- }
938
- for (let i = 0; i < spendTx.inputsLength; i++) {
939
- const input = spendTx.getInput(i);
940
- if (!input.txid || input.index !== deposit.vout) continue;
941
- if (import_base3.hex.encode(input.txid) !== deposit.txid) continue;
942
- for (const leaf of input.tapLeafScript ?? []) {
943
- const spent = import_base3.hex.encode((0, import_sdk5.scriptFromTapLeafScript)(leaf));
944
- if (leaves.cancel && spent === import_base3.hex.encode(leaves.cancel)) return "cancelled";
945
- if (leaves.fulfill && spent === import_base3.hex.encode(leaves.fulfill)) return "fulfilled";
946
- }
941
+ /** Takes a bare `string`, deliberately, where {@link RfqCorridorHandler.kind}
942
+ * is the manager's union: a lookup key comes off a record a backend handed
943
+ * back, and that it typechecks as a `kind` is a claim about the type, not
944
+ * about what was stored. Narrowing this would read as a check already made. */
945
+ get(kind) {
946
+ return this.handlers.get(kind);
947
947
  }
948
- return "indeterminate";
949
- }
950
- var spendTxidsOf = (vtxo) => [vtxo.spentBy, vtxo.arkTxId].filter((id) => Boolean(id));
951
- function classifyDepositSpend(offer, serverPubkey, spendTxs, deposit) {
952
- for (const tx of spendTxs) {
953
- const kind = classifySpend(offer, serverPubkey, tx, deposit);
954
- if (kind !== "indeterminate") return kind;
955
- }
956
- return "indeterminate";
957
- }
958
- async function restoreAssetSwaps(indexer, txs, existingIds, opts) {
959
- const { serverPubkey, scanned = /* @__PURE__ */ new Set() } = opts;
960
- const candidates = unscannedSwapCandidates(txs, existingIds, scanned);
961
- if (candidates.length === 0) return { restored: [], scannedTxids: [] };
962
- const byTxid = new Map(candidates.map((tx) => [tx.redeemTxid, tx]));
963
- const parsedByTxid = await fetchParsedTxs(
964
- indexer,
965
- candidates.map((tx) => tx.redeemTxid)
966
- );
967
- const fetchedTxids = [];
968
- const found = [];
969
- for (const [txid, parsed] of parsedByTxid) {
970
- const fundingTx = byTxid.get(txid);
971
- if (!fundingTx) continue;
972
- fetchedTxids.push(txid);
973
- try {
974
- const packet = import_sdk5.Extension.fromTx(parsed).getPacketByType(OFFER_PACKET_TYPE);
975
- if (!packet) continue;
976
- const payload = packet.serialize();
977
- found.push({
978
- fundingTx,
979
- offer: decodeOffer(payload),
980
- offerHex: import_base3.hex.encode(payload)
981
- });
982
- } catch {
948
+ /**
949
+ * The handler for a kind, or a loud failure.
950
+ *
951
+ * A record whose corridor is not registered cannot be restored, and
952
+ * guessing would monitor a swap with no idea how to drive it. Names the
953
+ * registered kinds so a missing `register()` call is obvious.
954
+ */
955
+ getOrThrow(kind) {
956
+ const handler = this.get(kind);
957
+ if (!handler) {
958
+ throw new Error(
959
+ `no RFQ corridor handler registered for kind '${kind}'; registered: ${this.registeredKinds().join(", ") || "none"}`
960
+ );
983
961
  }
962
+ return handler;
984
963
  }
985
- if (found.length === 0) return { restored: [], scannedTxids: fetchedTxids };
986
- const scripts = [...new Set(found.map((f) => import_base3.hex.encode(f.offer.swapPkScript)))];
987
- const { vtxos } = await indexer.getVtxos({ scripts });
988
- const vtxoByScriptAndTxid = new Map(vtxos.map((v) => [`${v.script}:${v.txid}`, v]));
989
- const txByAnyId = /* @__PURE__ */ new Map();
990
- for (const tx of txs) {
991
- for (const id of [tx.boardingTxid, tx.redeemTxid, tx.roundTxid]) {
992
- if (id) txByAnyId.set(id, tx);
993
- }
964
+ has(kind) {
965
+ return this.handlers.has(kind);
994
966
  }
995
- const spendTxids = /* @__PURE__ */ new Set();
996
- for (const { fundingTx, offer } of found) {
997
- const vtxo = vtxoByScriptAndTxid.get(
998
- `${import_base3.hex.encode(offer.swapPkScript)}:${fundingTx.redeemTxid}`
999
- );
1000
- if (vtxo?.virtualStatus.state !== "spent") continue;
1001
- for (const txid of spendTxidsOf(vtxo)) spendTxids.add(txid);
967
+ registeredKinds() {
968
+ return [...this.handlers.keys()];
1002
969
  }
1003
- const spendTxByTxid = await fetchParsedTxs(indexer, [...spendTxids]);
1004
- const restored = [];
1005
- const unresolved = /* @__PURE__ */ new Set();
1006
- for (const { fundingTx, offer, offerHex } of found) {
1007
- const swapPkScript = import_base3.hex.encode(offer.swapPkScript);
1008
- const vtxo = vtxoByScriptAndTxid.get(`${swapPkScript}:${fundingTx.redeemTxid}`);
1009
- if (!vtxo) {
1010
- unresolved.add(fundingTx.redeemTxid);
1011
- continue;
1012
- }
1013
- const depositRider = vtxo.assets?.length === 1 && vtxo.assets[0].amount > BigInt(0) ? vtxo.assets[0] : void 0;
1014
- const fromAsset = offer.offerAsset?.toString() ?? depositRider?.assetId ?? BTC_ASSET_ID;
1015
- const toAsset = offer.wantAsset?.toString() ?? BTC_ASSET_ID;
1016
- const depositAmount = fromAsset === BTC_ASSET_ID ? BigInt(vtxo.value) : vtxo.assets?.find((a) => a.assetId === fromAsset)?.amount;
1017
- if (depositAmount === void 0) {
1018
- unresolved.add(fundingTx.redeemTxid);
1019
- continue;
1020
- }
1021
- const fromAmount = depositAmount.toString();
1022
- const state = vtxo.virtualStatus.state;
1023
- const spentTxid = state === "spent" ? vtxo.arkTxId || vtxo.spentBy : void 0;
1024
- let status = "pending";
1025
- if (state === "swept") status = "recoverable";
1026
- else if (state === "spent") {
1027
- const spendTxs = spendTxidsOf(vtxo).map((id) => spendTxByTxid.get(id)).filter((tx) => tx !== void 0);
1028
- const kind = classifyDepositSpend(offer, serverPubkey, spendTxs, {
1029
- txid: vtxo.txid,
1030
- vout: vtxo.vout
1031
- });
1032
- if (kind === "indeterminate") {
1033
- unresolved.add(fundingTx.redeemTxid);
1034
- continue;
1035
- }
1036
- status = kind;
1037
- }
1038
- restored.push({
1039
- id: fundingTx.redeemTxid,
1040
- fromAsset,
1041
- toAsset,
1042
- fromAmount,
1043
- toAmount: offer.wantAmount.toString(),
1044
- // ponytail(arkade-os/ts-sdk#680): empty address makes cancel fall back
1045
- // to the current server key; store the funded address if server-key
1046
- // rotations become real (cancelOffer now at least diagnoses the
1047
- // mismatch instead of reporting a missing VTXO)
1048
- swapAddress: "",
1049
- swapPkScript,
1050
- offerHex,
1051
- fundingTxid: fundingTx.redeemTxid,
1052
- spentTxid,
1053
- status,
1054
- createdAt: fundingTx.createdAt ? fundingTx.createdAt * 1e3 : vtxo.createdAt.getTime(),
1055
- // the completion time is the caller's record of the spend, if it
1056
- // has one — the psbt that classified it carries no timestamp
1057
- ...status === "fulfilled" && spentTxid && txByAnyId.get(spentTxid)?.createdAt ? { completedAt: txByAnyId.get(spentTxid).createdAt * 1e3 } : {}
1058
- });
970
+ /** Test seam, mirroring the contract registry's. */
971
+ unregister(kind) {
972
+ return this.handlers.delete(kind);
1059
973
  }
1060
- return { restored, scannedTxids: fetchedTxids.filter((id) => !unresolved.has(id)) };
1061
- }
974
+ };
975
+ var rfqCorridorHandlers = new RfqCorridorRegistry();
1062
976
 
1063
- // src/watch.ts
1064
- var import_base4 = require("@scure/base");
1065
- var import_sdk6 = require("@arkade-os/sdk");
1066
- var TERMINAL = ["fulfilled", "cancelled", "recoverable"];
1067
- function spendUpdate(swap, spend) {
1068
- if (TERMINAL.includes(swap.status)) return void 0;
1069
- if (spend.kind === "indeterminate") return void 0;
1070
- const status = spend.kind === "cancelled" ? "cancelled" : "fulfilled";
977
+ // src/rfqProfileParts.ts
978
+ var rfqSecretsProfile = (secrets, paymentHash) => {
979
+ const { signingDescriptor, ...preimage } = swapSecretsToRecord(secrets);
1071
980
  return {
1072
- status,
1073
- spentTxid: spend.txid,
1074
- // mirrors restore.ts: a completion time is a fill's, not a cancel's
1075
- ...status === "fulfilled" && spend.at ? { completedAt: spend.at } : {}
981
+ signer: { signingDescriptor },
982
+ ...paymentHash ? { hashlock: { paymentHash, ...preimage } } : {}
1076
983
  };
1077
- }
1078
- async function watchOfferSwaps({
1079
- wallet,
1080
- arkServerUrl,
1081
- repository,
1082
- onUpdate
1083
- }) {
1084
- const manager = await wallet.getContractManager();
1085
- const serverPubkey = import_sdk6.ArkAddress.decode(await wallet.getAddress()).serverPubKey;
1086
- const indexer = new import_sdk6.RestIndexerProvider(arkServerUrl);
1087
- let queue = Promise.resolve();
1088
- const enqueue = (task) => {
1089
- queue = queue.then(task).catch(() => {
1090
- });
1091
- };
1092
- const classify = async (swap, vtxo, spentTxid) => {
1093
- if (swap.spentTxid === spentTxid && swap.status === "cancelling") return "cancelled";
1094
- try {
1095
- const candidates = spendTxidsOf(vtxo);
1096
- if (candidates.length === 0) return "indeterminate";
1097
- const { txs } = await indexer.getVirtualTxs(candidates);
1098
- return classifyDepositSpend(
1099
- decodeOffer(import_base4.hex.decode(swap.offerHex)),
1100
- serverPubkey,
1101
- txs.map((psbt) => import_sdk6.Transaction.fromPSBT(import_base4.base64.decode(psbt))),
1102
- { txid: vtxo.txid, vout: vtxo.vout }
1103
- );
1104
- } catch {
1105
- return "indeterminate";
1106
- }
1107
- };
1108
- const handleSpend = async (event) => {
1109
- if (event.contract.metadata?.kind !== OFFER_CONTRACT_KIND) return;
1110
- for (const vtxo of event.vtxos) {
1111
- const spentTxid = vtxo.arkTxId || vtxo.spentBy;
1112
- if (!spentTxid) continue;
1113
- const swap = (await getAssetSwaps(repository)).find(
1114
- (s) => s.fundingTxid === vtxo.txid && s.swapPkScript === event.contractScript
1115
- );
1116
- if (!swap) continue;
1117
- const kind = await classify(swap, vtxo, spentTxid);
1118
- const changes = spendUpdate(swap, { txid: spentTxid, kind, at: event.timestamp });
1119
- if (!changes) continue;
1120
- const { persisted, swaps } = await updateAssetSwapBestEffort(
1121
- repository,
1122
- swap.id,
1123
- changes
1124
- );
1125
- if (!persisted) continue;
1126
- onUpdate?.({ ...swap, ...changes });
1127
- if (changes.status && RETIRABLE.includes(changes.status)) {
1128
- await retireOfferContract(manager, swaps, event.contractScript);
1129
- }
1130
- }
1131
- };
1132
- const unsubscribe = manager.onContractEvent((event) => {
1133
- if (event.type !== "vtxo_spent") return;
1134
- enqueue(() => handleSpend(event));
1135
- });
1136
- return {
1137
- stop: unsubscribe,
1138
- idle: () => queue
984
+ };
985
+ var parseHex32 = (value, field) => {
986
+ if (typeof value !== "string" || !/^[0-9a-fA-F]{64}$/.test(value)) {
987
+ throw new Error(`${field} must be 32 bytes of hex, got ${JSON.stringify(value)}`);
988
+ }
989
+ return value.toLowerCase();
990
+ };
991
+ var parseSigner = (value) => {
992
+ const signingDescriptor = value?.signingDescriptor;
993
+ if (typeof signingDescriptor !== "string" || signingDescriptor.length === 0) {
994
+ throw new Error(
995
+ `rfq profile.signer carries no signingDescriptor (got ${JSON.stringify(signingDescriptor)})`
996
+ );
997
+ }
998
+ return { signingDescriptor };
999
+ };
1000
+ var parseHashlock = (value) => {
1001
+ const raw = value ?? {};
1002
+ const hashlock = {
1003
+ paymentHash: parseHex32(raw.paymentHash, "rfq profile.hashlock.paymentHash")
1139
1004
  };
1140
- }
1141
-
1142
- // src/rfq.ts
1143
- var import_base8 = require("@scure/base");
1144
- var import_legacy2 = require("@noble/hashes/legacy.js");
1145
- var import_sdk8 = require("@arkade-os/sdk");
1005
+ if (raw.preimageHex !== void 0) {
1006
+ hashlock.preimageHex = parseHex32(raw.preimageHex, "rfq profile.hashlock.preimageHex");
1007
+ }
1008
+ if (raw.preimageSaltHex !== void 0) {
1009
+ hashlock.preimageSaltHex = parseHex32(
1010
+ raw.preimageSaltHex,
1011
+ "rfq profile.hashlock.preimageSaltHex"
1012
+ );
1013
+ }
1014
+ return hashlock;
1015
+ };
1016
+ var hydrateHashlock = (profile) => {
1017
+ try {
1018
+ return { paymentHash: parseHashlock(profile.hashlock).paymentHash };
1019
+ } catch (cause) {
1020
+ throw new Error(
1021
+ `rfq record carries no usable hashlock; it cannot verify a preimage: ${String(cause)}`,
1022
+ { cause }
1023
+ );
1024
+ }
1025
+ };
1026
+ var rfqSignerOf = (record) => {
1027
+ const signer = record.profile.signer;
1028
+ if (signer === void 0) return void 0;
1029
+ return parseSigner(signer);
1030
+ };
1031
+ var rfqClaimSecretOf = (record) => {
1032
+ const handler = rfqCorridorHandlers.getOrThrow(record.kind);
1033
+ if (!handler.claimSecret) return void 0;
1034
+ try {
1035
+ const claim = handler.claimSecret(record.profile);
1036
+ return { ...parseSigner(claim), ...parseHashlock(claim) };
1037
+ } catch (cause) {
1038
+ throw new PreimageNotRecoverableError(
1039
+ "malformed-record",
1040
+ `this ${record.kind} record's claim secret is unreadable: ${String(cause)}`,
1041
+ { cause }
1042
+ );
1043
+ }
1044
+ };
1146
1045
 
1147
1046
  // src/onchainHtlc.ts
1148
- var import_base5 = require("@scure/base");
1047
+ var import_base3 = require("@scure/base");
1149
1048
  var import_sha22 = require("@noble/hashes/sha2.js");
1150
1049
  var import_legacy = require("@noble/hashes/legacy.js");
1151
1050
  var btc = __toESM(require("@scure/btc-signer"), 1);
1152
1051
  var ONCHAIN_ORDER_MARGIN_SECONDS = 2 * 60 * 60;
1153
1052
  var ONCHAIN_CLAIM_MARGIN_SECONDS = 90 * 60;
1154
1053
  var MAX_MIN_CONFIRMATIONS = 6;
1054
+ var LOCKTIME_THRESHOLD = 5e8;
1155
1055
  var ONCHAIN_SECONDS_PER_BLOCK = 600;
1156
1056
  var ONCHAIN_DUST_SATS = BigInt(330);
1157
1057
  var newPreimage = () => crypto.getRandomValues(new Uint8Array(32));
1158
- var paymentHashOf = (preimage) => import_base5.hex.encode((0, import_sha22.sha256)(preimage));
1159
- var h160FromPaymentHash = (paymentHash) => (0, import_legacy.ripemd160)(import_base5.hex.decode(paymentHash));
1058
+ var paymentHashOf = (preimage) => import_base3.hex.encode((0, import_sha22.sha256)(preimage));
1059
+ var h160FromPaymentHash = (paymentHash) => (0, import_legacy.ripemd160)(import_base3.hex.decode(paymentHash));
1160
1060
  var L1_NETWORKS = {
1161
1061
  bitcoin: btc.NETWORK,
1162
1062
  testnet: btc.TEST_NETWORK,
@@ -1171,6 +1071,16 @@ function onchainHtlcScript(params, network) {
1171
1071
  `refundLocktime must be a positive unix timestamp, got ${params.refundLocktime}`
1172
1072
  );
1173
1073
  }
1074
+ if (params.refundLocktime < LOCKTIME_THRESHOLD) {
1075
+ throw new Error(
1076
+ `refundLocktime ${params.refundLocktime} is below LOCKTIME_THRESHOLD (${LOCKTIME_THRESHOLD}) and would be interpreted as a block height`
1077
+ );
1078
+ }
1079
+ if (!Object.hasOwn(L1_NETWORKS, network)) {
1080
+ throw new Error(
1081
+ `unknown L1 network '${String(network)}' \u2014 expected one of ${Object.keys(L1_NETWORKS).join(", ")}`
1082
+ );
1083
+ }
1174
1084
  const h160 = h160FromPaymentHash(params.paymentHash);
1175
1085
  const claim = btc.Script.encode([
1176
1086
  "SIZE",
@@ -1197,7 +1107,7 @@ function onchainHtlcScript(params, network) {
1197
1107
  );
1198
1108
  const controlBlockFor = (leaf) => {
1199
1109
  for (const [block, script] of payment.tapLeafScript ?? []) {
1200
- if (script.length - 1 === leaf.length && import_base5.hex.encode(script.subarray(0, leaf.length)) === import_base5.hex.encode(leaf)) {
1110
+ if (script.length - 1 === leaf.length && import_base3.hex.encode(script.subarray(0, leaf.length)) === import_base3.hex.encode(leaf)) {
1201
1111
  return btc.TaprootControlBlock.encode(block);
1202
1112
  }
1203
1113
  }
@@ -1290,13 +1200,13 @@ var buildHtlcRefund = (input) => buildLeafSpend({
1290
1200
  function extractPreimage(txHex, paymentHash) {
1291
1201
  let raw;
1292
1202
  try {
1293
- raw = btc.RawTx.decode(import_base5.hex.decode(txHex));
1203
+ raw = btc.RawTx.decode(import_base3.hex.decode(txHex));
1294
1204
  } catch {
1295
1205
  return null;
1296
1206
  }
1297
1207
  for (const stack of raw.witnesses ?? []) {
1298
1208
  for (const item of stack) {
1299
- if (item.length === 32 && import_base5.hex.encode((0, import_sha22.sha256)(item)) === paymentHash) return item;
1209
+ if (item.length === 32 && import_base3.hex.encode((0, import_sha22.sha256)(item)) === paymentHash) return item;
1300
1210
  }
1301
1211
  }
1302
1212
  return null;
@@ -1325,36 +1235,502 @@ async function claimOnchainFill(chain, input) {
1325
1235
  error.reason = "claim_window_closed";
1326
1236
  throw error;
1327
1237
  }
1328
- const spend = await buildHtlcClaim(input);
1329
- const txid = await chain.broadcast(spend.txHex);
1330
- return { txid, payoutAmount: spend.payoutAmount };
1238
+ const spend = await buildHtlcClaim(input);
1239
+ const txid = await chain.broadcast(spend.txHex);
1240
+ return { txid, payoutAmount: spend.payoutAmount };
1241
+ }
1242
+ async function classifyOnchainHtlc(chain, input) {
1243
+ const utxos = await chain.getScriptUtxos(input.htlc.pkScript);
1244
+ const best = utxos.sort((a, b) => b.amount > a.amount ? 1 : -1)[0];
1245
+ if (!best) {
1246
+ if (!input.funding) return { phase: "unfunded" };
1247
+ const spend = await chain.getSpendingTx(input.funding.txid, input.funding.vout);
1248
+ if (!spend) return { phase: "unfunded" };
1249
+ const preimage = extractPreimage(spend.txHex, input.htlc.paymentHash);
1250
+ const txid = btc.Transaction.fromRaw(import_base3.hex.decode(spend.txHex), {
1251
+ allowUnknownInputs: true,
1252
+ allowUnknownOutputs: true
1253
+ }).id;
1254
+ return preimage ? { phase: "claimed", txid, preimage } : { phase: "swept", txid };
1255
+ }
1256
+ if (best.confirmations < input.minConfirmations)
1257
+ return { phase: "awaiting_confirmations", utxo: best };
1258
+ const mtp = await chain.getMtp();
1259
+ if (mtp >= input.htlc.refundLocktime) return { phase: "refundable", utxo: best };
1260
+ return { phase: "claimable", utxo: best };
1261
+ }
1262
+
1263
+ // src/rfqCorridors.ts
1264
+ var LightningSendCorridor = {
1265
+ kind: "lightning_send",
1266
+ project: () => ({}),
1267
+ hydrate: (profile) => hydrateHashlock(profile)
1268
+ // No `claimSecret`, deliberately: `provisionRefundKey` mints no preimage at
1269
+ // all, and deriving one off the refund descriptor would fail the payment
1270
+ // hash check — a `hash-mismatch` on a swap that was never broken.
1271
+ };
1272
+ var LightningReceiveCorridor = {
1273
+ kind: "lightning_receive",
1274
+ // What the manager holds: the amount gate, and its own claim once it lands.
1275
+ // `payoutAddress` comes from the request result and never changes, so the
1276
+ // caller writes it once and this leaves it alone.
1277
+ project: (swap) => {
1278
+ const receive = swap;
1279
+ return {
1280
+ expectedAmount: receive.expectedAmount,
1281
+ ...receive.claimArkTxid ? { claimArkTxid: receive.claimArkTxid } : {}
1282
+ };
1283
+ },
1284
+ hydrate(profile) {
1285
+ if (typeof profile.expectedAmount !== "number" || !Number.isFinite(profile.expectedAmount)) {
1286
+ throw new Error("lightning_receive record carries no expectedAmount; it cannot claim");
1287
+ }
1288
+ return {
1289
+ ...hydrateHashlock(profile),
1290
+ expectedAmount: profile.expectedAmount,
1291
+ ...profile.claimArkTxid ? { claimArkTxid: profile.claimArkTxid } : {}
1292
+ };
1293
+ },
1294
+ // We are the claimant here, so the preimage material on the hashlock is
1295
+ // ours to use.
1296
+ claimSecret: (profile) => ({ ...profile.signer, ...profile.hashlock }),
1297
+ activityTxids: (profile) => profile.claimArkTxid ? [profile.claimArkTxid] : []
1298
+ };
1299
+ function onchainSendProfile(result) {
1300
+ return {
1301
+ claimKey: import_base4.hex.encode(result.htlcParams.claimKey),
1302
+ refundKey: import_base4.hex.encode(result.htlcParams.refundKey),
1303
+ htlcLocktime: result.htlcParams.refundLocktime,
1304
+ network: result.l1Network,
1305
+ htlcAddress: result.htlc.address,
1306
+ minConfirmations: result.minConfirmations
1307
+ };
1308
+ }
1309
+ var OnchainSendCorridor = {
1310
+ kind: "onchain_send",
1311
+ // The L1 keys and the network are only in the request result — nothing on
1312
+ // `OnchainSendSwap` carries them — so the caller writes them once. What the
1313
+ // manager learns as it drives the swap is the fill and our own claim.
1314
+ project: (swap) => {
1315
+ const send = swap;
1316
+ return {
1317
+ ...send.funding ? { funding: send.funding } : {},
1318
+ ...send.claimTxid ? { claimTxid: send.claimTxid } : {}
1319
+ };
1320
+ },
1321
+ hydrate(profile) {
1322
+ const { paymentHash } = hydrateHashlock(profile);
1323
+ if (!profile.claimKey || !profile.refundKey) {
1324
+ throw new Error(
1325
+ "onchain_send record carries no L1 keys; its HTLC cannot be rebuilt and its refund window would pass unwatched"
1326
+ );
1327
+ }
1328
+ if (!Number.isInteger(profile.minConfirmations) || profile.minConfirmations < 1) {
1329
+ throw new Error(
1330
+ `onchain_send record carries no usable minConfirmations (${String(profile.minConfirmations)}); the confirmation gate cannot be checked \u2014 refusing to restore a swap that would claim an unconfirmed fill`
1331
+ );
1332
+ }
1333
+ const htlc = onchainHtlcScript(
1334
+ {
1335
+ // From the profile, not the covenant: the lockup commits to
1336
+ // `hash160(P)`, and the HTLC needs `sha256(P)`. One P
1337
+ // unlocks both legs, but only one of the two hashes of it
1338
+ // is recoverable here.
1339
+ paymentHash,
1340
+ claimKey: import_base4.hex.decode(profile.claimKey),
1341
+ refundKey: import_base4.hex.decode(profile.refundKey),
1342
+ refundLocktime: profile.htlcLocktime
1343
+ },
1344
+ profile.network
1345
+ );
1346
+ if (htlc.address !== profile.htlcAddress) {
1347
+ throw new Error(
1348
+ `onchain_send record's L1 inputs derive ${htlc.address}, but the fill was expected at ${String(profile.htlcAddress)} \u2014 these are not this swap's`
1349
+ );
1350
+ }
1351
+ return {
1352
+ paymentHash,
1353
+ htlc,
1354
+ minConfirmations: profile.minConfirmations,
1355
+ ...profile.funding ? { funding: profile.funding } : {},
1356
+ ...profile.claimTxid ? { claimTxid: profile.claimTxid } : {}
1357
+ };
1358
+ },
1359
+ // The trader claims the L1 HTLC with P, so this leg's preimage material is
1360
+ // ours.
1361
+ claimSecret: (profile) => ({ ...profile.signer, ...profile.hashlock }),
1362
+ // Our own L1 claim only. `funding` is the SOLVER's fill into the HTLC —
1363
+ // not a transaction of ours, so grouping it would claim a row this wallet
1364
+ // never made.
1365
+ activityTxids: (profile) => profile.claimTxid ? [profile.claimTxid] : []
1366
+ };
1367
+ rfqCorridorHandlers.register(LightningSendCorridor);
1368
+ rfqCorridorHandlers.register(LightningReceiveCorridor);
1369
+ rfqCorridorHandlers.register(OnchainSendCorridor);
1370
+
1371
+ // src/rfqRecord.ts
1372
+ var import_sdk5 = require("@arkade-os/sdk");
1373
+ var import_base5 = require("@scure/base");
1374
+
1375
+ // src/rfqSwapState.ts
1376
+ var RFQ_SWAP_TERMINAL_STATES = ["settled", "refunded", "failed"];
1377
+ var isRfqSwapTerminal = (state) => RFQ_SWAP_TERMINAL_STATES.includes(state);
1378
+
1379
+ // src/rfqRecord.ts
1380
+ var RFQ_SWAP_RETENTION_SECONDS = 30 * 24 * 60 * 60;
1381
+ var managerState = (swap) => ({
1382
+ rfqId: swap.rfqId,
1383
+ state: swap.state,
1384
+ createdAt: swap.createdAt,
1385
+ updatedAt: swap.updatedAt,
1386
+ ...swap.refundArkTxid ? { refundArkTxid: swap.refundArkTxid } : {},
1387
+ ...swap.lockupSpendArkTxids?.length ? { lockupSpendArkTxids: [...swap.lockupSpendArkTxids] } : {},
1388
+ ...swap.failure ? { failure: swap.failure } : {},
1389
+ ...swap.blockedReason ? { blockedReason: swap.blockedReason } : {}
1390
+ });
1391
+ function assertSameSwap(origin, swap) {
1392
+ if (origin.kind !== swap.kind) {
1393
+ throw new Error(
1394
+ `rfq swap record is a ${origin.kind} origin paired with a ${swap.kind} swap`
1395
+ );
1396
+ }
1397
+ const funded = import_base5.hex.encode(import_sdk5.ArkAddress.decode(origin.lockupAddress).pkScript);
1398
+ const watched = import_base5.hex.encode(swap.lockupPkScript);
1399
+ if (funded !== watched) {
1400
+ throw new Error(
1401
+ `rfq swap record's lockup address holds ${funded}, but the swap watches ${watched} \u2014 these are not the same swap`
1402
+ );
1403
+ }
1404
+ }
1405
+ function createRfqSwapRecord(origin, swap) {
1406
+ assertSameSwap(origin, swap);
1407
+ const handler = rfqCorridorHandlers.getOrThrow(origin.kind);
1408
+ return {
1409
+ ...origin,
1410
+ ...managerState(swap),
1411
+ // The caller wrote what only the request result knows; the handler adds
1412
+ // what the live swap already carries.
1413
+ profile: { ...origin.profile, ...handler.project(swap) }
1414
+ };
1415
+ }
1416
+ function updateRfqSwapRecord(record, swap) {
1417
+ assertSameSwap(record, swap);
1418
+ const {
1419
+ refundArkTxid: _refundArkTxid,
1420
+ lockupSpendArkTxids: _lockupSpendArkTxids,
1421
+ failure: _failure,
1422
+ blockedReason: _blockedReason,
1423
+ ...origin
1424
+ } = record;
1425
+ const handler = rfqCorridorHandlers.getOrThrow(record.kind);
1426
+ return {
1427
+ ...origin,
1428
+ ...managerState(swap),
1429
+ profile: { ...record.profile, ...handler.project(swap) }
1430
+ };
1431
+ }
1432
+ function rfqSwapOriginOf(record) {
1433
+ return {
1434
+ kind: record.kind,
1435
+ lockupAddress: record.lockupAddress,
1436
+ profile: { ...record.profile },
1437
+ ...record.amount !== void 0 ? { amount: record.amount } : {},
1438
+ ...record.fundingArkTxid ? { fundingArkTxid: record.fundingArkTxid } : {}
1439
+ };
1440
+ }
1441
+ function lockupScript(params, lockupAddress) {
1442
+ const script = import_sdk5.VHTLCV2ContractHandler.createScript(params);
1443
+ const funded = import_sdk5.ArkAddress.decode(lockupAddress).pkScript;
1444
+ if (import_base5.hex.encode(funded) !== import_base5.hex.encode(script.pkScript)) {
1445
+ throw new Error(
1446
+ `rfq swap covenant params derive ${import_base5.hex.encode(script.pkScript)}, but the record's lockup address holds ${import_base5.hex.encode(funded)} \u2014 these params are not this swap's`
1447
+ );
1448
+ }
1449
+ return script;
1450
+ }
1451
+ function rebuildRfqSwap(record, params) {
1452
+ const script = lockupScript(params, record.lockupAddress);
1453
+ const common = {
1454
+ rfqId: record.rfqId,
1455
+ state: record.state,
1456
+ lockupPkScript: script.pkScript,
1457
+ lockup: { script, address: record.lockupAddress },
1458
+ // From the covenant, which binds it: the record's own copy would be a
1459
+ // second source for the deadline the refund is gated on.
1460
+ refundLocktime: Number(script.options.refundLocktime),
1461
+ createdAt: record.createdAt,
1462
+ updatedAt: record.updatedAt,
1463
+ ...record.refundArkTxid ? { refundArkTxid: record.refundArkTxid } : {},
1464
+ ...record.lockupSpendArkTxids?.length ? { lockupSpendArkTxids: [...record.lockupSpendArkTxids] } : {},
1465
+ ...record.failure ? { failure: record.failure } : {},
1466
+ ...record.blockedReason ? { blockedReason: record.blockedReason } : {}
1467
+ };
1468
+ const handler = rfqCorridorHandlers.getOrThrow(record.kind);
1469
+ return {
1470
+ ...common,
1471
+ kind: record.kind,
1472
+ ...handler.hydrate(record.profile, { lockup: script })
1473
+ };
1474
+ }
1475
+ function shouldRetainRfqSwap(record, now) {
1476
+ if (!isRfqSwapTerminal(record.state)) return true;
1477
+ return now - record.updatedAt < RFQ_SWAP_RETENTION_SECONDS;
1478
+ }
1479
+
1480
+ // src/restore.ts
1481
+ var import_base6 = require("@scure/base");
1482
+ var import_sdk6 = require("@arkade-os/sdk");
1483
+ var TXS_PER_REQUEST = 50;
1484
+ async function fetchParsedTxs(indexer, txids) {
1485
+ const parsedByTxid = /* @__PURE__ */ new Map();
1486
+ if (txids.length === 0) return parsedByTxid;
1487
+ const chunks = [];
1488
+ for (let i = 0; i < txids.length; i += TXS_PER_REQUEST) {
1489
+ chunks.push(txids.slice(i, i + TXS_PER_REQUEST));
1490
+ }
1491
+ const chunkResults = await Promise.allSettled(
1492
+ chunks.map(async (ids) => (await indexer.getVirtualTxs(ids)).txs)
1493
+ );
1494
+ for (const result of chunkResults) {
1495
+ if (result.status !== "fulfilled") continue;
1496
+ for (const psbt of result.value) {
1497
+ try {
1498
+ const parsed = import_sdk6.Transaction.fromPSBT(import_base6.base64.decode(psbt));
1499
+ parsedByTxid.set(parsed.id, parsed);
1500
+ } catch {
1501
+ }
1502
+ }
1503
+ }
1504
+ return parsedByTxid;
1505
+ }
1506
+ var unscannedSwapCandidates = (txs, existingIds, scanned) => txs.filter(
1507
+ (tx) => tx.type === "sent" && tx.redeemTxid && !existingIds.has(tx.redeemTxid) && !scanned.has(tx.redeemTxid)
1508
+ );
1509
+ function classifySpend(offer, serverPubkey, spendTx, deposit) {
1510
+ let leaves;
1511
+ try {
1512
+ const script = offerVtxoScript(offer, serverPubkey);
1513
+ if (import_base6.hex.encode(script.pkScript) !== import_base6.hex.encode(offer.swapPkScript)) return "indeterminate";
1514
+ leaves = {
1515
+ cancel: script.functionByName("cancel")?.leafScript,
1516
+ fulfill: script.functionByName("fulfill")?.leafScript
1517
+ };
1518
+ } catch {
1519
+ return "indeterminate";
1520
+ }
1521
+ for (let i = 0; i < spendTx.inputsLength; i++) {
1522
+ const input = spendTx.getInput(i);
1523
+ if (!input.txid || input.index !== deposit.vout) continue;
1524
+ if (import_base6.hex.encode(input.txid) !== deposit.txid) continue;
1525
+ for (const leaf of input.tapLeafScript ?? []) {
1526
+ const spent = import_base6.hex.encode((0, import_sdk6.scriptFromTapLeafScript)(leaf));
1527
+ if (leaves.cancel && spent === import_base6.hex.encode(leaves.cancel)) return "cancelled";
1528
+ if (leaves.fulfill && spent === import_base6.hex.encode(leaves.fulfill)) return "fulfilled";
1529
+ }
1530
+ }
1531
+ return "indeterminate";
1532
+ }
1533
+ var spendTxidsOf = (vtxo) => [vtxo.spentBy, vtxo.arkTxId].filter((id) => Boolean(id));
1534
+ function classifyDepositSpend(offer, serverPubkey, spendTxs, deposit) {
1535
+ for (const tx of spendTxs) {
1536
+ const kind = classifySpend(offer, serverPubkey, tx, deposit);
1537
+ if (kind !== "indeterminate") return kind;
1538
+ }
1539
+ return "indeterminate";
1540
+ }
1541
+ async function restoreAssetSwaps(indexer, txs, existingIds, opts) {
1542
+ const { serverPubkey, scanned = /* @__PURE__ */ new Set() } = opts;
1543
+ const candidates = unscannedSwapCandidates(txs, existingIds, scanned);
1544
+ if (candidates.length === 0) return { restored: [], scannedTxids: [] };
1545
+ const byTxid = new Map(candidates.map((tx) => [tx.redeemTxid, tx]));
1546
+ const parsedByTxid = await fetchParsedTxs(
1547
+ indexer,
1548
+ candidates.map((tx) => tx.redeemTxid)
1549
+ );
1550
+ const fetchedTxids = [];
1551
+ const found = [];
1552
+ for (const [txid, parsed] of parsedByTxid) {
1553
+ const fundingTx = byTxid.get(txid);
1554
+ if (!fundingTx) continue;
1555
+ fetchedTxids.push(txid);
1556
+ try {
1557
+ const packet = import_sdk6.Extension.fromTx(parsed).getPacketByType(OFFER_PACKET_TYPE);
1558
+ if (!packet) continue;
1559
+ const payload = packet.serialize();
1560
+ found.push({
1561
+ fundingTx,
1562
+ offer: decodeOffer(payload),
1563
+ offerHex: import_base6.hex.encode(payload)
1564
+ });
1565
+ } catch {
1566
+ }
1567
+ }
1568
+ if (found.length === 0) return { restored: [], scannedTxids: fetchedTxids };
1569
+ const scripts = [...new Set(found.map((f) => import_base6.hex.encode(f.offer.swapPkScript)))];
1570
+ const { vtxos } = await indexer.getVtxos({ scripts });
1571
+ const vtxoByScriptAndTxid = new Map(vtxos.map((v) => [`${v.script}:${v.txid}`, v]));
1572
+ const txByAnyId = /* @__PURE__ */ new Map();
1573
+ for (const tx of txs) {
1574
+ for (const id of [tx.boardingTxid, tx.redeemTxid, tx.roundTxid]) {
1575
+ if (id) txByAnyId.set(id, tx);
1576
+ }
1577
+ }
1578
+ const spendTxids = /* @__PURE__ */ new Set();
1579
+ for (const { fundingTx, offer } of found) {
1580
+ const vtxo = vtxoByScriptAndTxid.get(
1581
+ `${import_base6.hex.encode(offer.swapPkScript)}:${fundingTx.redeemTxid}`
1582
+ );
1583
+ if (vtxo?.virtualStatus.state !== "spent") continue;
1584
+ for (const txid of spendTxidsOf(vtxo)) spendTxids.add(txid);
1585
+ }
1586
+ const spendTxByTxid = await fetchParsedTxs(indexer, [...spendTxids]);
1587
+ const restored = [];
1588
+ const unresolved = /* @__PURE__ */ new Set();
1589
+ for (const { fundingTx, offer, offerHex } of found) {
1590
+ const swapPkScript = import_base6.hex.encode(offer.swapPkScript);
1591
+ const vtxo = vtxoByScriptAndTxid.get(`${swapPkScript}:${fundingTx.redeemTxid}`);
1592
+ if (!vtxo) {
1593
+ unresolved.add(fundingTx.redeemTxid);
1594
+ continue;
1595
+ }
1596
+ const depositRider = vtxo.assets?.length === 1 && vtxo.assets[0].amount > BigInt(0) ? vtxo.assets[0] : void 0;
1597
+ const fromAsset = offer.offerAsset?.toString() ?? depositRider?.assetId ?? BTC_ASSET_ID;
1598
+ const toAsset = offer.wantAsset?.toString() ?? BTC_ASSET_ID;
1599
+ const depositAmount = fromAsset === BTC_ASSET_ID ? BigInt(vtxo.value) : vtxo.assets?.find((a) => a.assetId === fromAsset)?.amount;
1600
+ if (depositAmount === void 0) {
1601
+ unresolved.add(fundingTx.redeemTxid);
1602
+ continue;
1603
+ }
1604
+ const fromAmount = depositAmount.toString();
1605
+ const state = vtxo.virtualStatus.state;
1606
+ const spentTxid = state === "spent" ? vtxo.arkTxId || vtxo.spentBy : void 0;
1607
+ let status = "pending";
1608
+ if (state === "swept") status = "recoverable";
1609
+ else if (state === "spent") {
1610
+ const spendTxs = spendTxidsOf(vtxo).map((id) => spendTxByTxid.get(id)).filter((tx) => tx !== void 0);
1611
+ const kind = classifyDepositSpend(offer, serverPubkey, spendTxs, {
1612
+ txid: vtxo.txid,
1613
+ vout: vtxo.vout
1614
+ });
1615
+ if (kind === "indeterminate") {
1616
+ unresolved.add(fundingTx.redeemTxid);
1617
+ continue;
1618
+ }
1619
+ status = kind;
1620
+ }
1621
+ restored.push({
1622
+ id: fundingTx.redeemTxid,
1623
+ fromAsset,
1624
+ toAsset,
1625
+ fromAmount,
1626
+ toAmount: offer.wantAmount.toString(),
1627
+ // ponytail(arkade-os/ts-sdk#680): empty address makes cancel fall back
1628
+ // to the current server key; store the funded address if server-key
1629
+ // rotations become real (cancelOffer now at least diagnoses the
1630
+ // mismatch instead of reporting a missing VTXO)
1631
+ swapAddress: "",
1632
+ swapPkScript,
1633
+ offerHex,
1634
+ fundingTxid: fundingTx.redeemTxid,
1635
+ spentTxid,
1636
+ status,
1637
+ createdAt: fundingTx.createdAt ? fundingTx.createdAt * 1e3 : vtxo.createdAt.getTime(),
1638
+ // the completion time is the caller's record of the spend, if it
1639
+ // has one — the psbt that classified it carries no timestamp
1640
+ ...status === "fulfilled" && spentTxid && txByAnyId.get(spentTxid)?.createdAt ? { completedAt: txByAnyId.get(spentTxid).createdAt * 1e3 } : {}
1641
+ });
1642
+ }
1643
+ return { restored, scannedTxids: fetchedTxids.filter((id) => !unresolved.has(id)) };
1331
1644
  }
1332
- async function classifyOnchainHtlc(chain, input) {
1333
- const utxos = await chain.getScriptUtxos(input.htlc.pkScript);
1334
- const best = utxos.sort((a, b) => b.amount > a.amount ? 1 : -1)[0];
1335
- if (!best) {
1336
- if (!input.funding) return { phase: "unfunded" };
1337
- const spend = await chain.getSpendingTx(input.funding.txid, input.funding.vout);
1338
- if (!spend) return { phase: "unfunded" };
1339
- const preimage = extractPreimage(spend.txHex, input.htlc.paymentHash);
1340
- const txid = btc.Transaction.fromRaw(import_base5.hex.decode(spend.txHex), {
1341
- allowUnknownInputs: true,
1342
- allowUnknownOutputs: true
1343
- }).id;
1344
- return preimage ? { phase: "claimed", txid, preimage } : { phase: "swept", txid };
1345
- }
1346
- if (best.confirmations < input.minConfirmations)
1347
- return { phase: "awaiting_confirmations", utxo: best };
1348
- const mtp = await chain.getMtp();
1349
- if (mtp >= input.htlc.refundLocktime) return { phase: "refundable", utxo: best };
1350
- return { phase: "claimable", utxo: best };
1645
+
1646
+ // src/watch.ts
1647
+ var import_base7 = require("@scure/base");
1648
+ var import_sdk7 = require("@arkade-os/sdk");
1649
+ var TERMINAL = ["fulfilled", "cancelled", "recoverable"];
1650
+ function spendUpdate(swap, spend) {
1651
+ if (TERMINAL.includes(swap.status)) return void 0;
1652
+ if (spend.kind === "indeterminate") return void 0;
1653
+ const status = spend.kind === "cancelled" ? "cancelled" : "fulfilled";
1654
+ return {
1655
+ status,
1656
+ spentTxid: spend.txid,
1657
+ // mirrors restore.ts: a completion time is a fill's, not a cancel's
1658
+ ...status === "fulfilled" && spend.at ? { completedAt: spend.at } : {}
1659
+ };
1660
+ }
1661
+ async function watchOfferSwaps({
1662
+ wallet,
1663
+ arkServerUrl,
1664
+ repository,
1665
+ onUpdate
1666
+ }) {
1667
+ const manager = await wallet.getContractManager();
1668
+ const serverPubkey = import_sdk7.ArkAddress.decode(await wallet.getAddress()).serverPubKey;
1669
+ const indexer = new import_sdk7.RestIndexerProvider(arkServerUrl);
1670
+ let queue = Promise.resolve();
1671
+ const enqueue = (task) => {
1672
+ queue = queue.then(task).catch(() => {
1673
+ });
1674
+ };
1675
+ const classify = async (swap, vtxo, spentTxid) => {
1676
+ if (swap.spentTxid === spentTxid && swap.status === "cancelling") return "cancelled";
1677
+ try {
1678
+ const candidates = spendTxidsOf(vtxo);
1679
+ if (candidates.length === 0) return "indeterminate";
1680
+ const { txs } = await indexer.getVirtualTxs(candidates);
1681
+ return classifyDepositSpend(
1682
+ decodeOffer(import_base7.hex.decode(swap.offerHex)),
1683
+ serverPubkey,
1684
+ txs.map((psbt) => import_sdk7.Transaction.fromPSBT(import_base7.base64.decode(psbt))),
1685
+ { txid: vtxo.txid, vout: vtxo.vout }
1686
+ );
1687
+ } catch {
1688
+ return "indeterminate";
1689
+ }
1690
+ };
1691
+ const handleSpend = async (event) => {
1692
+ if (event.contract.metadata?.kind !== OFFER_CONTRACT_KIND) return;
1693
+ for (const vtxo of event.vtxos) {
1694
+ const spentTxid = vtxo.arkTxId || vtxo.spentBy;
1695
+ if (!spentTxid) continue;
1696
+ const swap = (await getAssetSwaps(repository)).find(
1697
+ (s) => s.fundingTxid === vtxo.txid && s.swapPkScript === event.contractScript
1698
+ );
1699
+ if (!swap) continue;
1700
+ const kind = await classify(swap, vtxo, spentTxid);
1701
+ const changes = spendUpdate(swap, { txid: spentTxid, kind, at: event.timestamp });
1702
+ if (!changes) continue;
1703
+ const { persisted, swaps } = await updateAssetSwapBestEffort(
1704
+ repository,
1705
+ swap.id,
1706
+ changes
1707
+ );
1708
+ if (!persisted) continue;
1709
+ onUpdate?.({ ...swap, ...changes });
1710
+ if (changes.status && RETIRABLE.includes(changes.status)) {
1711
+ await retireOfferContract(manager, swaps, event.contractScript);
1712
+ }
1713
+ }
1714
+ };
1715
+ const unsubscribe = manager.onContractEvent((event) => {
1716
+ if (event.type !== "vtxo_spent") return;
1717
+ enqueue(() => handleSpend(event));
1718
+ });
1719
+ return {
1720
+ stop: unsubscribe,
1721
+ idle: () => queue
1722
+ };
1351
1723
  }
1352
1724
 
1353
1725
  // src/rfq.ts
1726
+ var import_base10 = require("@scure/base");
1727
+ var import_legacy2 = require("@noble/hashes/legacy.js");
1354
1728
  var import_sdk9 = require("@arkade-os/sdk");
1729
+ var import_sdk10 = require("@arkade-os/sdk");
1355
1730
 
1356
1731
  // src/claimPacket.ts
1357
- var import_base6 = require("@scure/base");
1732
+ var import_base8 = require("@scure/base");
1733
+ var import_aes = require("@noble/ciphers/aes.js");
1358
1734
  var import_secp256k1 = require("@noble/curves/secp256k1.js");
1359
1735
  var import_hkdf = require("@noble/hashes/hkdf.js");
1360
1736
  var import_sha23 = require("@noble/hashes/sha2.js");
@@ -1375,33 +1751,34 @@ async function sealWithEntropy(input, ephemeralKey, nonce) {
1375
1751
  const sharedX = import_secp256k1.secp256k1.getSharedSecret(ephemeralKey, input.covclaimdPubkey, true).subarray(1);
1376
1752
  const key = (0, import_hkdf.hkdf)(import_sha23.sha256, sharedX, ephemeralPub, HKDF_INFO, 32);
1377
1753
  if (nonce.length !== 12) throw new Error("nonce must be 12 bytes");
1378
- const aesKey = await crypto.subtle.importKey("raw", key, "AES-GCM", false, [
1379
- "encrypt"
1380
- ]);
1381
- const sealed = new Uint8Array(
1382
- await crypto.subtle.encrypt(
1383
- {
1384
- name: "AES-GCM",
1385
- iv: nonce,
1386
- additionalData: ephemeralPub
1387
- },
1388
- aesKey,
1389
- input.preimage
1390
- )
1391
- );
1754
+ const sealed = (0, import_aes.gcm)(key, nonce, ephemeralPub).encrypt(input.preimage);
1392
1755
  const packet = new Uint8Array(33 + 12 + sealed.length);
1393
1756
  packet.set(ephemeralPub, 0);
1394
1757
  packet.set(nonce, 33);
1395
1758
  packet.set(sealed, 45);
1396
- return { ciphertext: import_base6.base64.encode(packet) };
1759
+ return { ciphertext: import_base8.base64.encode(packet) };
1397
1760
  }
1398
1761
 
1399
1762
  // src/lockupContract.ts
1400
- var import_base7 = require("@scure/base");
1401
- var import_sdk7 = require("@arkade-os/sdk");
1763
+ var import_base9 = require("@scure/base");
1764
+ var import_sdk8 = require("@arkade-os/sdk");
1402
1765
  var SWAP_LOCKUP_CONTRACT_TYPE = "vhtlc-v2";
1403
1766
  var SWAP_LOCKUP_CONTRACT_LABEL = "Arkade RFQ swap lockup";
1404
1767
  var SWAP_LOCKUP_CONTRACT_KIND = "rfq-swap-lockup";
1768
+ var LockupContractMissing = class extends Error {
1769
+ /** The lockup whose row is absent. */
1770
+ address;
1771
+ /** Its pkScript hex — the key the row would have been under. */
1772
+ script;
1773
+ constructor(address, script) {
1774
+ super(
1775
+ `no contract row for lockup ${address} (script ${script}); its covenant cannot be rebuilt from this wallet's contract store`
1776
+ );
1777
+ this.name = "LockupContractMissing";
1778
+ this.address = address;
1779
+ this.script = script;
1780
+ }
1781
+ };
1405
1782
  var LockupRegistrationFailed = class extends Error {
1406
1783
  /** The lockup address that was never registered — never fund it: nothing
1407
1784
  * is watching it. */
@@ -1420,8 +1797,8 @@ async function registerLockupContract(contracts, script, address) {
1420
1797
  try {
1421
1798
  await contracts.createContract({
1422
1799
  type: SWAP_LOCKUP_CONTRACT_TYPE,
1423
- params: import_sdk7.VHTLCV2ContractHandler.serializeParams(script.options),
1424
- script: import_base7.hex.encode(script.pkScript),
1800
+ params: import_sdk8.VHTLCV2ContractHandler.serializeParams(script.options),
1801
+ script: import_base9.hex.encode(script.pkScript),
1425
1802
  address,
1426
1803
  label: SWAP_LOCKUP_CONTRACT_LABEL,
1427
1804
  metadata: { genericallySpendable: false, kind: SWAP_LOCKUP_CONTRACT_KIND }
@@ -1430,18 +1807,17 @@ async function registerLockupContract(contracts, script, address) {
1430
1807
  throw new LockupRegistrationFailed(script, address, error);
1431
1808
  }
1432
1809
  }
1810
+ async function lockupContractParams(contracts, lockupAddress) {
1811
+ const script = import_base9.hex.encode(import_sdk8.ArkAddress.decode(lockupAddress).pkScript);
1812
+ const [row] = await contracts.getContracts({ script });
1813
+ if (!row) throw new LockupContractMissing(lockupAddress, script);
1814
+ return row.params;
1815
+ }
1433
1816
 
1434
1817
  // src/rfq.ts
1435
- var xOnly = (key, label) => {
1436
- if (key.length === 32) return key;
1437
- if (key.length !== 33 || key[0] !== 2 && key[0] !== 3) {
1438
- throw new Error(`${label} is not a compressed or x-only public key`);
1439
- }
1440
- return key.slice(1);
1441
- };
1442
1818
  var solverHex = (value, field) => {
1443
1819
  try {
1444
- return import_base8.hex.decode(value);
1820
+ return import_base10.hex.decode(value);
1445
1821
  } catch {
1446
1822
  throw new Error(`solver sent malformed hex for ${field}`);
1447
1823
  }
@@ -1477,7 +1853,7 @@ var AddressMismatch = class extends Error {
1477
1853
  this.quoted = quoted;
1478
1854
  }
1479
1855
  };
1480
- var newRfqId = () => import_base8.hex.encode(crypto.getRandomValues(new Uint8Array(32)));
1856
+ var newRfqId = () => import_base10.hex.encode(crypto.getRandomValues(new Uint8Array(32)));
1481
1857
  var lightningSendRequest = (input) => ({
1482
1858
  v: 1,
1483
1859
  type: "rfq_request",
@@ -1487,7 +1863,7 @@ var lightningSendRequest = (input) => ({
1487
1863
  profile: {
1488
1864
  invoice: input.invoice,
1489
1865
  refund_address: input.refundAddress,
1490
- client_refund_pubkey: import_base8.hex.encode(input.senderPubkey)
1866
+ client_refund_pubkey: import_base10.hex.encode(input.senderPubkey)
1491
1867
  }
1492
1868
  });
1493
1869
  var arkadeSwapRequest = (input) => {
@@ -1734,11 +2110,11 @@ function lightningSendVtxoScript(params) {
1734
2110
  type: "seconds",
1735
2111
  value: BigInt(value)
1736
2112
  });
1737
- return new import_sdk8.VHTLC.ScriptV2({
2113
+ return new import_sdk9.VHTLC.ScriptV2({
1738
2114
  sender: params.senderPubkey,
1739
2115
  receiver: params.solverPubkey,
1740
2116
  server: params.serverPubkey,
1741
- preimageHash: (0, import_legacy2.ripemd160)(import_base8.hex.decode(params.paymentHash)),
2117
+ preimageHash: (0, import_legacy2.ripemd160)(import_base10.hex.decode(params.paymentHash)),
1742
2118
  refundLocktime: BigInt(params.refundLocktime),
1743
2119
  unilateralClaimDelay: seconds(params.claimDelay),
1744
2120
  unilateralRefundDelay: seconds(unilateralRefundDelay(params.claimDelay)),
@@ -1757,10 +2133,10 @@ function lightningSendVtxoScript(params) {
1757
2133
  }
1758
2134
  async function requestLightningSend(wallet, arkServerUrl, transport, params) {
1759
2135
  const rfqId = params.rfqId ?? newRfqId();
1760
- const secrets = await (0, import_sdk9.provisionRefundKey)(wallet);
2136
+ const secrets = await (0, import_sdk10.provisionRefundKey)(wallet);
1761
2137
  const senderPubkey = secrets.pubkey;
1762
2138
  const [info, refundAddress] = await Promise.all([
1763
- new import_sdk8.RestArkProvider(arkServerUrl).getInfo(),
2139
+ new import_sdk9.RestArkProvider(arkServerUrl).getInfo(),
1764
2140
  wallet.getAddress()
1765
2141
  ]);
1766
2142
  const quote = await transport.requestQuote(
@@ -1783,22 +2159,23 @@ async function requestLightningSend(wallet, arkServerUrl, transport, params) {
1783
2159
  `quote from_amount ${quote.from_amount} is below the invoice amount \u2014 a negative spread is not a quote`
1784
2160
  );
1785
2161
  }
1786
- const serverPubkey = xOnly(import_base8.hex.decode(info.signerPubkey), "ark signer key");
1787
- const network = (0, import_sdk8.getNetwork)(info.network);
1788
- const script = lightningSendVtxoScript({
1789
- solverPubkey: xOnly(import_base8.hex.decode(quote.solver_pubkey), "solver key"),
2162
+ const serverPubkey = (0, import_sdk9.toXOnly)(import_base10.hex.decode(info.signerPubkey), "ark signer key");
2163
+ const network = (0, import_sdk9.getNetwork)(info.network);
2164
+ const treeParams = {
2165
+ solverPubkey: (0, import_sdk9.toXOnly)(import_base10.hex.decode(quote.solver_pubkey), "solver key"),
1790
2166
  refundLocktime: quote.refund_locktime,
1791
2167
  serverPubkey,
1792
2168
  paymentHash: params.invoice.paymentHash,
1793
2169
  claimDelay: unilateralClaimDelay(Number(info.unilateralExitDelay)),
1794
- emulatorPubkey: xOnly(
1795
- import_base8.hex.decode((0, import_sdk8.resolveEmulatorPubkey)(network, params.emulatorPubkey)),
2170
+ emulatorPubkey: (0, import_sdk9.toXOnly)(
2171
+ import_base10.hex.decode((0, import_sdk9.resolveEmulatorPubkey)(network, params.emulatorPubkey)),
1796
2172
  "emulator signer key"
1797
2173
  ),
1798
2174
  senderPubkey,
1799
2175
  receiverPkScript: solverHex(receiverPkScriptHex, "profile.receiver_pk_script"),
1800
- refundPkScript: import_sdk8.ArkAddress.decode(refundAddress).pkScript
1801
- });
2176
+ refundPkScript: import_sdk9.ArkAddress.decode(refundAddress).pkScript
2177
+ };
2178
+ const script = lightningSendVtxoScript(treeParams);
1802
2179
  const address = script.address(network.hrp, serverPubkey).encode();
1803
2180
  verifyLockupAddress(quote, address);
1804
2181
  assertFundable({
@@ -1818,7 +2195,8 @@ async function requestLightningSend(wallet, arkServerUrl, transport, params) {
1818
2195
  script,
1819
2196
  refundAddress,
1820
2197
  senderPubkey,
1821
- secrets
2198
+ secrets,
2199
+ treeParams
1822
2200
  };
1823
2201
  }
1824
2202
  var offerTermsFromQuote = (quote, assets) => {
@@ -1837,9 +2215,9 @@ var onchainSendRequest = (input) => ({
1837
2215
  amount: input.amount,
1838
2216
  profile: {
1839
2217
  payment_hash: input.paymentHash,
1840
- payout_pubkey: import_base8.hex.encode(input.payoutPubkey),
2218
+ payout_pubkey: import_base10.hex.encode(input.payoutPubkey),
1841
2219
  refund_address: input.refundAddress,
1842
- client_refund_pubkey: import_base8.hex.encode(input.senderPubkey)
2220
+ client_refund_pubkey: import_base10.hex.encode(input.senderPubkey)
1843
2221
  }
1844
2222
  });
1845
2223
  var lightningReceiveRequest = (input) => ({
@@ -1852,7 +2230,7 @@ var lightningReceiveRequest = (input) => ({
1852
2230
  profile: {
1853
2231
  payment_hash: input.paymentHash,
1854
2232
  payout_address: input.payoutAddress,
1855
- payout_pubkey: import_base8.hex.encode(input.payoutPubkey),
2233
+ payout_pubkey: import_base10.hex.encode(input.payoutPubkey),
1856
2234
  claim_packet: input.claimPacket
1857
2235
  }
1858
2236
  });
@@ -1866,9 +2244,9 @@ var onchainReceiveRequest = (input) => ({
1866
2244
  profile: {
1867
2245
  payment_hash: input.paymentHash,
1868
2246
  claim_packet: input.claimPacket,
1869
- refund_pubkey: import_base8.hex.encode(input.refundPubkey),
2247
+ refund_pubkey: import_base10.hex.encode(input.refundPubkey),
1870
2248
  payout_address: input.payoutAddress,
1871
- payout_pubkey: import_base8.hex.encode(input.payoutPubkey)
2249
+ payout_pubkey: import_base10.hex.encode(input.payoutPubkey)
1872
2250
  }
1873
2251
  });
1874
2252
  function deriveOnchainSend(input) {
@@ -1884,7 +2262,7 @@ function deriveOnchainSend(input) {
1884
2262
  throw new Error("onchain-send quote is missing a binding field");
1885
2263
  }
1886
2264
  const script = lightningSendVtxoScript({
1887
- solverPubkey: xOnly(import_base8.hex.decode(quote.solver_pubkey), "solver key"),
2265
+ solverPubkey: (0, import_sdk9.toXOnly)(import_base10.hex.decode(quote.solver_pubkey), "solver key"),
1888
2266
  refundLocktime,
1889
2267
  serverPubkey: input.serverPubkey,
1890
2268
  paymentHash: input.paymentHash,
@@ -1892,25 +2270,25 @@ function deriveOnchainSend(input) {
1892
2270
  emulatorPubkey: input.emulatorPubkey,
1893
2271
  senderPubkey: input.senderPubkey,
1894
2272
  receiverPkScript: solverHex(receiverPkScriptHex, "profile.receiver_pk_script"),
1895
- refundPkScript: import_sdk8.ArkAddress.decode(input.refundAddress).pkScript
2273
+ refundPkScript: import_sdk9.ArkAddress.decode(input.refundAddress).pkScript
1896
2274
  });
1897
2275
  const address = script.address(input.hrp, input.serverPubkey).encode();
1898
2276
  verifyLockupAddress(quote, address);
1899
- const htlc = onchainHtlcScript(
1900
- {
1901
- paymentHash: input.paymentHash,
1902
- claimKey: input.payoutPubkey,
1903
- refundKey: xOnly(import_base8.hex.decode(htlcPubkey), "solver L1 htlc key"),
1904
- refundLocktime: htlcLocktime
1905
- },
1906
- input.l1Network
1907
- );
2277
+ const htlcParams = {
2278
+ paymentHash: input.paymentHash,
2279
+ claimKey: input.payoutPubkey,
2280
+ refundKey: (0, import_sdk9.toXOnly)(import_base10.hex.decode(htlcPubkey), "solver L1 htlc key"),
2281
+ refundLocktime: htlcLocktime
2282
+ };
2283
+ const htlc = onchainHtlcScript(htlcParams, input.l1Network);
1908
2284
  if (htlc.address !== htlcAddress) throw new AddressMismatch(htlc.address, htlcAddress);
1909
2285
  return {
1910
2286
  address,
1911
2287
  swapPkScript: script.pkScript,
1912
2288
  script,
1913
2289
  htlc,
2290
+ htlcParams,
2291
+ l1Network: input.l1Network,
1914
2292
  refundLocktime,
1915
2293
  htlcLocktime,
1916
2294
  minConfirmations
@@ -1918,16 +2296,16 @@ function deriveOnchainSend(input) {
1918
2296
  }
1919
2297
  async function requestOnchainSend(wallet, arkServerUrl, transport, params) {
1920
2298
  const rfqId = params.rfqId ?? newRfqId();
1921
- const secrets = await (0, import_sdk9.provisionClaimSecret)(wallet, { preimage: params.preimage });
2299
+ const secrets = await (0, import_sdk10.provisionClaimSecret)(wallet, { preimage: params.preimage });
1922
2300
  if (secrets.mustPersistPreimage) {
1923
2301
  console.warn(
1924
2302
  "[swap] this swap's preimage cannot be re-derived from the seed and MUST be persisted with the record before funding"
1925
2303
  );
1926
2304
  }
1927
- const paymentHash = import_base8.hex.encode(secrets.paymentHash);
2305
+ const paymentHash = import_base10.hex.encode(secrets.paymentHash);
1928
2306
  const senderPubkey = secrets.pubkey;
1929
2307
  const [info, refundAddress] = await Promise.all([
1930
- new import_sdk8.RestArkProvider(arkServerUrl).getInfo(),
2308
+ new import_sdk9.RestArkProvider(arkServerUrl).getInfo(),
1931
2309
  wallet.getAddress()
1932
2310
  ]);
1933
2311
  const quote = await transport.requestQuote(
@@ -1941,14 +2319,14 @@ async function requestOnchainSend(wallet, arkServerUrl, transport, params) {
1941
2319
  amountSide: params.amountSide
1942
2320
  })
1943
2321
  );
1944
- const network = (0, import_sdk8.getNetwork)(info.network);
2322
+ const network = (0, import_sdk9.getNetwork)(info.network);
1945
2323
  const derived = deriveOnchainSend({
1946
2324
  quote,
1947
2325
  paymentHash,
1948
2326
  payoutPubkey: params.payoutPubkey,
1949
- serverPubkey: xOnly(import_base8.hex.decode(info.signerPubkey), "ark signer key"),
1950
- emulatorPubkey: xOnly(
1951
- import_base8.hex.decode((0, import_sdk8.resolveEmulatorPubkey)(network, params.emulatorPubkey)),
2327
+ serverPubkey: (0, import_sdk9.toXOnly)(import_base10.hex.decode(info.signerPubkey), "ark signer key"),
2328
+ emulatorPubkey: (0, import_sdk9.toXOnly)(
2329
+ import_base10.hex.decode((0, import_sdk9.resolveEmulatorPubkey)(network, params.emulatorPubkey)),
1952
2330
  "emulator signer key"
1953
2331
  ),
1954
2332
  claimDelay: unilateralClaimDelay(Number(info.unilateralExitDelay)),
@@ -1980,6 +2358,9 @@ async function requestOnchainSend(wallet, arkServerUrl, transport, params) {
1980
2358
  script: derived.script,
1981
2359
  refundAddress,
1982
2360
  htlc: derived.htlc,
2361
+ htlcParams: derived.htlcParams,
2362
+ l1Network: derived.l1Network,
2363
+ minConfirmations: derived.minConfirmations,
1983
2364
  senderPubkey,
1984
2365
  secrets
1985
2366
  };
@@ -2056,11 +2437,11 @@ function receiveVtxoScript(params) {
2056
2437
  type: "seconds",
2057
2438
  value: BigInt(value)
2058
2439
  });
2059
- return new import_sdk8.VHTLC.ScriptV2({
2440
+ return new import_sdk9.VHTLC.ScriptV2({
2060
2441
  sender: params.solverPubkey,
2061
2442
  receiver: params.payoutPubkey,
2062
2443
  server: params.serverPubkey,
2063
- preimageHash: (0, import_legacy2.ripemd160)(import_base8.hex.decode(params.paymentHash)),
2444
+ preimageHash: (0, import_legacy2.ripemd160)(import_base10.hex.decode(params.paymentHash)),
2064
2445
  refundLocktime: BigInt(params.refundLocktime),
2065
2446
  unilateralClaimDelay: seconds(params.claimDelay),
2066
2447
  unilateralRefundDelay: seconds(unilateralRefundDelay(params.claimDelay)),
@@ -2086,8 +2467,8 @@ function deriveLightningReceive(input) {
2086
2467
  if (refundLocktime === void 0 || invoice === void 0 || solverRefundPkScriptHex === void 0) {
2087
2468
  throw new Error("lightning-receive quote is missing a binding field");
2088
2469
  }
2089
- const script = receiveVtxoScript({
2090
- solverPubkey: xOnly(import_base8.hex.decode(quote.solver_pubkey), "solver key"),
2470
+ const treeParams = {
2471
+ solverPubkey: (0, import_sdk9.toXOnly)(import_base10.hex.decode(quote.solver_pubkey), "solver key"),
2091
2472
  refundLocktime,
2092
2473
  serverPubkey: input.serverPubkey,
2093
2474
  paymentHash: input.paymentHash,
@@ -2095,25 +2476,26 @@ function deriveLightningReceive(input) {
2095
2476
  emulatorPubkey: input.emulatorPubkey,
2096
2477
  solverRefundPkScript: solverHex(solverRefundPkScriptHex, "profile.solver_refund_pk_script"),
2097
2478
  payoutPubkey: input.payoutPubkey,
2098
- payoutPkScript: import_sdk8.ArkAddress.decode(input.payoutAddress).pkScript
2099
- });
2479
+ payoutPkScript: import_sdk9.ArkAddress.decode(input.payoutAddress).pkScript
2480
+ };
2481
+ const script = receiveVtxoScript(treeParams);
2100
2482
  const address = script.address(input.hrp, input.serverPubkey).encode();
2101
2483
  verifyLockupAddress(quote, address);
2102
- return { address, swapPkScript: script.pkScript, script, invoice, refundLocktime };
2484
+ return { address, swapPkScript: script.pkScript, script, invoice, refundLocktime, treeParams };
2103
2485
  }
2104
2486
  async function requestLightningReceive(wallet, arkServerUrl, transport, params) {
2105
2487
  const rfqId = params.rfqId ?? newRfqId();
2106
- const secrets = await (0, import_sdk9.provisionClaimSecret)(wallet);
2488
+ const secrets = await (0, import_sdk10.provisionClaimSecret)(wallet);
2107
2489
  if (secrets.mustPersistPreimage) {
2108
2490
  console.warn(
2109
2491
  "[swap] this swap's preimage cannot be re-derived from the seed and MUST be persisted with the record before paying"
2110
2492
  );
2111
2493
  }
2112
2494
  const preimage = secrets.preimage;
2113
- const paymentHash = import_base8.hex.encode(secrets.paymentHash);
2495
+ const paymentHash = import_base10.hex.encode(secrets.paymentHash);
2114
2496
  const payoutPubkey = secrets.pubkey;
2115
2497
  const [info, payoutAddress] = await Promise.all([
2116
- new import_sdk8.RestArkProvider(arkServerUrl).getInfo(),
2498
+ new import_sdk9.RestArkProvider(arkServerUrl).getInfo(),
2117
2499
  wallet.getAddress()
2118
2500
  ]);
2119
2501
  const claimPacket = await sealClaimPacket({
@@ -2132,15 +2514,15 @@ async function requestLightningReceive(wallet, arkServerUrl, transport, params)
2132
2514
  })
2133
2515
  );
2134
2516
  assertQuotedAmount(quote, params.amountSide, params.amount);
2135
- const network = (0, import_sdk8.getNetwork)(info.network);
2517
+ const network = (0, import_sdk9.getNetwork)(info.network);
2136
2518
  const derived = deriveLightningReceive({
2137
2519
  quote,
2138
2520
  paymentHash,
2139
2521
  payoutPubkey,
2140
2522
  payoutAddress,
2141
- serverPubkey: xOnly(import_base8.hex.decode(info.signerPubkey), "ark signer key"),
2142
- emulatorPubkey: xOnly(
2143
- import_base8.hex.decode((0, import_sdk8.resolveEmulatorPubkey)(network, params.emulatorPubkey)),
2523
+ serverPubkey: (0, import_sdk9.toXOnly)(import_base10.hex.decode(info.signerPubkey), "ark signer key"),
2524
+ emulatorPubkey: (0, import_sdk9.toXOnly)(
2525
+ import_base10.hex.decode((0, import_sdk9.resolveEmulatorPubkey)(network, params.emulatorPubkey)),
2144
2526
  "emulator signer key"
2145
2527
  ),
2146
2528
  claimDelay: unilateralClaimDelay(Number(info.unilateralExitDelay)),
@@ -2171,7 +2553,8 @@ async function requestLightningReceive(wallet, arkServerUrl, transport, params)
2171
2553
  script: derived.script,
2172
2554
  payoutAddress,
2173
2555
  payoutPubkey,
2174
- secrets
2556
+ secrets,
2557
+ treeParams: derived.treeParams
2175
2558
  };
2176
2559
  }
2177
2560
  function deriveOnchainReceive(input) {
@@ -2187,7 +2570,7 @@ function deriveOnchainReceive(input) {
2187
2570
  throw new Error("onchain-receive quote is missing a binding field");
2188
2571
  }
2189
2572
  const script = receiveVtxoScript({
2190
- solverPubkey: xOnly(import_base8.hex.decode(quote.solver_pubkey), "solver key"),
2573
+ solverPubkey: (0, import_sdk9.toXOnly)(import_base10.hex.decode(quote.solver_pubkey), "solver key"),
2191
2574
  refundLocktime,
2192
2575
  serverPubkey: input.serverPubkey,
2193
2576
  paymentHash: input.paymentHash,
@@ -2195,14 +2578,14 @@ function deriveOnchainReceive(input) {
2195
2578
  emulatorPubkey: input.emulatorPubkey,
2196
2579
  solverRefundPkScript: solverHex(solverRefundPkScriptHex, "profile.solver_refund_pk_script"),
2197
2580
  payoutPubkey: input.payoutPubkey,
2198
- payoutPkScript: import_sdk8.ArkAddress.decode(input.payoutAddress).pkScript
2581
+ payoutPkScript: import_sdk9.ArkAddress.decode(input.payoutAddress).pkScript
2199
2582
  });
2200
2583
  const address = script.address(input.hrp, input.serverPubkey).encode();
2201
2584
  verifyLockupAddress(quote, address);
2202
2585
  const htlc = onchainHtlcScript(
2203
2586
  {
2204
2587
  paymentHash: input.paymentHash,
2205
- claimKey: xOnly(import_base8.hex.decode(claimPubkey), "solver L1 claim key"),
2588
+ claimKey: (0, import_sdk9.toXOnly)(import_base10.hex.decode(claimPubkey), "solver L1 claim key"),
2206
2589
  refundKey: input.refundPubkey,
2207
2590
  refundLocktime: htlcLocktime
2208
2591
  },
@@ -2221,17 +2604,17 @@ function deriveOnchainReceive(input) {
2221
2604
  }
2222
2605
  async function requestOnchainReceive(wallet, arkServerUrl, transport, params) {
2223
2606
  const rfqId = params.rfqId ?? newRfqId();
2224
- const secrets = await (0, import_sdk9.provisionClaimSecret)(wallet);
2607
+ const secrets = await (0, import_sdk10.provisionClaimSecret)(wallet);
2225
2608
  if (secrets.mustPersistPreimage) {
2226
2609
  console.warn(
2227
2610
  "[swap] this swap's preimage cannot be re-derived from the seed and MUST be persisted with the record before funding"
2228
2611
  );
2229
2612
  }
2230
2613
  const preimage = secrets.preimage;
2231
- const paymentHash = import_base8.hex.encode(secrets.paymentHash);
2614
+ const paymentHash = import_base10.hex.encode(secrets.paymentHash);
2232
2615
  const payoutPubkey = secrets.pubkey;
2233
2616
  const [info, payoutAddress] = await Promise.all([
2234
- new import_sdk8.RestArkProvider(arkServerUrl).getInfo(),
2617
+ new import_sdk9.RestArkProvider(arkServerUrl).getInfo(),
2235
2618
  wallet.getAddress()
2236
2619
  ]);
2237
2620
  const claimPacket = await sealClaimPacket({
@@ -2251,16 +2634,16 @@ async function requestOnchainReceive(wallet, arkServerUrl, transport, params) {
2251
2634
  })
2252
2635
  );
2253
2636
  assertQuotedAmount(quote, params.amountSide, params.amount);
2254
- const network = (0, import_sdk8.getNetwork)(info.network);
2637
+ const network = (0, import_sdk9.getNetwork)(info.network);
2255
2638
  const derived = deriveOnchainReceive({
2256
2639
  quote,
2257
2640
  paymentHash,
2258
2641
  payoutPubkey,
2259
2642
  payoutAddress,
2260
2643
  refundPubkey: params.refundPubkey,
2261
- serverPubkey: xOnly(import_base8.hex.decode(info.signerPubkey), "ark signer key"),
2262
- emulatorPubkey: xOnly(
2263
- import_base8.hex.decode((0, import_sdk8.resolveEmulatorPubkey)(network, params.emulatorPubkey)),
2644
+ serverPubkey: (0, import_sdk9.toXOnly)(import_base10.hex.decode(info.signerPubkey), "ark signer key"),
2645
+ emulatorPubkey: (0, import_sdk9.toXOnly)(
2646
+ import_base10.hex.decode((0, import_sdk9.resolveEmulatorPubkey)(network, params.emulatorPubkey)),
2264
2647
  "emulator signer key"
2265
2648
  ),
2266
2649
  claimDelay: unilateralClaimDelay(Number(info.unilateralExitDelay)),
@@ -2297,15 +2680,15 @@ async function requestOnchainReceive(wallet, arkServerUrl, transport, params) {
2297
2680
  }
2298
2681
 
2299
2682
  // src/claim.ts
2300
- var import_base10 = require("@scure/base");
2683
+ var import_base12 = require("@scure/base");
2301
2684
  var import_legacy3 = require("@noble/hashes/legacy.js");
2302
2685
  var import_sha25 = require("@noble/hashes/sha2.js");
2303
- var import_sdk11 = require("@arkade-os/sdk");
2686
+ var import_sdk12 = require("@arkade-os/sdk");
2304
2687
 
2305
2688
  // src/refund.ts
2306
- var import_base9 = require("@scure/base");
2689
+ var import_base11 = require("@scure/base");
2307
2690
  var import_sha24 = require("@noble/hashes/sha2.js");
2308
- var import_sdk10 = require("@arkade-os/sdk");
2691
+ var import_sdk11 = require("@arkade-os/sdk");
2309
2692
  var sleep2 = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
2310
2693
  var isRfqTerminal = (state) => RFQ_TERMINAL_STATES.includes(state);
2311
2694
  var RFQ_RESOLVED_STATES = ["settled", "refunded"];
@@ -2352,7 +2735,7 @@ var LockupNeedsRecoveryError = class extends Error {
2352
2735
  }
2353
2736
  };
2354
2737
  async function findLockupVtxos(indexer, swapPkScript) {
2355
- const scripts = [import_base9.hex.encode(swapPkScript)];
2738
+ const scripts = [import_base11.hex.encode(swapPkScript)];
2356
2739
  const [spendable, recoverable] = await Promise.all([
2357
2740
  indexer.getVtxos({ scripts, spendableOnly: true }),
2358
2741
  indexer.getVtxos({ scripts, recoverableOnly: true })
@@ -2377,28 +2760,33 @@ async function findLockupVtxos(indexer, swapPkScript) {
2377
2760
  }
2378
2761
  return out;
2379
2762
  }
2380
- var hashesTo = (candidate, paymentHash) => import_base9.hex.encode((0, import_sha24.sha256)(candidate)) === paymentHash;
2763
+ var hashesTo = (candidate, paymentHash) => import_base11.hex.encode((0, import_sha24.sha256)(candidate)) === paymentHash;
2381
2764
  var candidateWitnessItems = (tx, inputIndex) => [
2382
- ...(0, import_sdk10.getArkPsbtFields)(tx, inputIndex, import_sdk10.ConditionWitness).flat(),
2765
+ ...(0, import_sdk11.getArkPsbtFields)(tx, inputIndex, import_sdk11.ConditionWitness).flat(),
2383
2766
  ...tx.getInput(inputIndex).finalScriptWitness ?? []
2384
2767
  ];
2385
2768
  async function readLockupFate(indexer, input) {
2386
- const { vtxos } = await indexer.getVtxos({ scripts: [import_base9.hex.encode(input.swapPkScript)] });
2769
+ const { vtxos } = await indexer.getVtxos({ scripts: [import_base11.hex.encode(input.swapPkScript)] });
2387
2770
  const all = vtxos ?? [];
2388
2771
  if (all.length === 0) return { fate: "unknown" };
2389
- const spentBy = /* @__PURE__ */ new Set();
2772
+ const spentBy = /* @__PURE__ */ new Map();
2390
2773
  let everySpendNamed = true;
2391
2774
  for (const vtxo of all) {
2392
2775
  if (!vtxo.isSpent && !vtxo.spentBy && !vtxo.settledBy) return { fate: "open" };
2393
- if (vtxo.spentBy) spentBy.add(vtxo.spentBy);
2776
+ if (vtxo.spentBy)
2777
+ spentBy.set(vtxo.spentBy, {
2778
+ checkpointTxid: vtxo.spentBy,
2779
+ arkTxid: vtxo.arkTxId
2780
+ });
2394
2781
  else everySpendNamed = false;
2395
2782
  }
2396
- const { txs } = await indexer.getVirtualTxs([...spentBy]);
2783
+ const spends = [...spentBy.values()];
2784
+ const { txs } = await indexer.getVirtualTxs([...spentBy.keys()]);
2397
2785
  const observed = /* @__PURE__ */ new Set();
2398
2786
  for (const raw of txs) {
2399
2787
  let tx;
2400
2788
  try {
2401
- tx = import_sdk10.Transaction.fromPSBT(import_base9.base64.decode(raw));
2789
+ tx = import_sdk11.Transaction.fromPSBT(import_base11.base64.decode(raw));
2402
2790
  } catch {
2403
2791
  continue;
2404
2792
  }
@@ -2406,16 +2794,16 @@ async function readLockupFate(indexer, input) {
2406
2794
  for (let i = 0; i < tx.inputsLength; i++) {
2407
2795
  const spent = tx.getInput(i);
2408
2796
  if (!spent.txid) continue;
2409
- const txid = import_base9.hex.encode(spent.txid);
2797
+ const txid = import_base11.hex.encode(spent.txid);
2410
2798
  if (!all.some((vtxo) => vtxo.txid === txid && vtxo.vout === spent.index)) continue;
2411
2799
  for (const candidate of candidateWitnessItems(tx, i)) {
2412
2800
  if (hashesTo(candidate, input.paymentHash)) {
2413
- return { fate: "claimed", preimage: candidate };
2801
+ return { fate: "claimed", preimage: candidate, spends };
2414
2802
  }
2415
2803
  }
2416
2804
  }
2417
2805
  }
2418
- return everySpendNamed && observed.size === spentBy.size ? { fate: "returned" } : { fate: "unknown" };
2806
+ return everySpendNamed && observed.size === spentBy.size ? { fate: "returned", spends } : { fate: "unknown" };
2419
2807
  }
2420
2808
  async function pushRefundWithoutReceiver(ark, input) {
2421
2809
  if (input.vtxos.length === 0) throw new Error("nothing to refund: no funded outputs");
@@ -2435,14 +2823,14 @@ async function pushRefundWithoutReceiver(ark, input) {
2435
2823
  const info = await ark.getInfo();
2436
2824
  let serverUnrollScript;
2437
2825
  try {
2438
- serverUnrollScript = import_sdk10.CSVMultisigTapscript.decode(import_base9.hex.decode(info.checkpointTapscript));
2826
+ serverUnrollScript = import_sdk11.CSVMultisigTapscript.decode(import_base11.hex.decode(info.checkpointTapscript));
2439
2827
  } catch {
2440
2828
  throw new Error("invalid checkpointTapscript from the Arkade server");
2441
2829
  }
2442
2830
  const leaf = input.script.refundWithoutReceiver();
2443
2831
  const tapTree = input.script.encode();
2444
2832
  const amount = input.vtxos.reduce((sum, vtxo) => sum + vtxo.value, 0);
2445
- const { arkTx, checkpoints } = (0, import_sdk10.buildOffchainTx)(
2833
+ const { arkTx, checkpoints } = (0, import_sdk11.buildOffchainTx)(
2446
2834
  input.vtxos.map((vtxo) => ({
2447
2835
  txid: vtxo.txid,
2448
2836
  vout: vtxo.vout,
@@ -2455,18 +2843,18 @@ async function pushRefundWithoutReceiver(ark, input) {
2455
2843
  );
2456
2844
  const signedArkTx = await input.sender.sign(arkTx);
2457
2845
  const submitted = await ark.submitTx(
2458
- import_base9.base64.encode(signedArkTx.toPSBT()),
2459
- checkpoints.map((c) => import_base9.base64.encode(c.toPSBT()))
2846
+ import_base11.base64.encode(signedArkTx.toPSBT()),
2847
+ checkpoints.map((c) => import_base11.base64.encode(c.toPSBT()))
2460
2848
  );
2461
- (0, import_sdk10.assertSubmittedArkTxid)(submitted, signedArkTx, "refundWithoutReceiver");
2462
- const matched = (0, import_sdk10.matchServerCheckpoints)(
2849
+ (0, import_sdk11.assertSubmittedArkTxid)(submitted, signedArkTx, "refundWithoutReceiver");
2850
+ const matched = (0, import_sdk11.matchServerCheckpoints)(
2463
2851
  submitted.signedCheckpointTxs,
2464
2852
  checkpoints,
2465
2853
  "refundWithoutReceiver"
2466
2854
  );
2467
2855
  const finalCheckpoints = await Promise.all(
2468
2856
  matched.map(
2469
- async ({ server }) => import_base9.base64.encode((await input.sender.sign(server, [0])).toPSBT())
2857
+ async ({ server }) => import_base11.base64.encode((await input.sender.sign(server, [0])).toPSBT())
2470
2858
  )
2471
2859
  );
2472
2860
  await ark.finalizeTx(submitted.arkTxid, finalCheckpoints);
@@ -2546,20 +2934,20 @@ async function pushClaim(ark, input) {
2546
2934
  }
2547
2935
  }
2548
2936
  const committed = input.script.options.preimageHash;
2549
- if (import_base10.hex.encode((0, import_legacy3.ripemd160)((0, import_sha25.sha256)(input.preimage))) !== import_base10.hex.encode(committed)) {
2937
+ if (import_base12.hex.encode((0, import_legacy3.ripemd160)((0, import_sha25.sha256)(input.preimage))) !== import_base12.hex.encode(committed)) {
2550
2938
  throw new Error("preimage does not match the covenant's payment hash");
2551
2939
  }
2552
2940
  const info = await ark.getInfo();
2553
2941
  let serverUnrollScript;
2554
2942
  try {
2555
- serverUnrollScript = import_sdk11.CSVMultisigTapscript.decode(import_base10.hex.decode(info.checkpointTapscript));
2943
+ serverUnrollScript = import_sdk12.CSVMultisigTapscript.decode(import_base12.hex.decode(info.checkpointTapscript));
2556
2944
  } catch {
2557
2945
  throw new Error("invalid checkpointTapscript from the Arkade server");
2558
2946
  }
2559
2947
  const leaf = input.script.claim();
2560
2948
  const tapTree = input.script.encode();
2561
- const arkTxid = await (0, import_sdk11.signAndSubmitOffchainTx)({
2562
- identity: (0, import_sdk11.claimWithPreimageIdentity)(input.receiver, input.preimage),
2949
+ const arkTxid = await (0, import_sdk12.signAndSubmitOffchainTx)({
2950
+ identity: (0, import_sdk12.claimWithPreimageIdentity)(input.receiver, input.preimage),
2563
2951
  provider: ark,
2564
2952
  inputs: input.vtxos.map((vtxo) => ({
2565
2953
  txid: vtxo.txid,
@@ -2606,7 +2994,7 @@ async function claimReceiveLockup(indexer, ark, input) {
2606
2994
  }
2607
2995
 
2608
2996
  // src/refundBlocked.ts
2609
- var import_sdk12 = require("@arkade-os/sdk");
2997
+ var import_sdk13 = require("@arkade-os/sdk");
2610
2998
  var RefundNotLocallyPossibleError = class extends Error {
2611
2999
  constructor(reason, message, options) {
2612
3000
  super(message, options);
@@ -2623,16 +3011,16 @@ async function senderIdentityForSwapRecord(wallet, record) {
2623
3011
  );
2624
3012
  }
2625
3013
  try {
2626
- return await (0, import_sdk12.contractSigner)(wallet, record.signingDescriptor);
3014
+ return await (0, import_sdk13.contractSigner)(wallet, record.signingDescriptor);
2627
3015
  } catch (cause) {
2628
- if (cause instanceof import_sdk12.WalletCannotSignError) {
3016
+ if (cause instanceof import_sdk13.WalletCannotSignError) {
2629
3017
  throw new RefundNotLocallyPossibleError(
2630
3018
  "unsignable-wallet",
2631
3019
  `this wallet holds ${record.signingDescriptor} but cannot sign with it; attach its signer`,
2632
3020
  { cause }
2633
3021
  );
2634
3022
  }
2635
- if (cause instanceof import_sdk12.ForeignDescriptorError) {
3023
+ if (cause instanceof import_sdk13.ForeignDescriptorError) {
2636
3024
  throw new RefundNotLocallyPossibleError(
2637
3025
  "foreign-descriptor",
2638
3026
  `this wallet cannot derive ${record.signingDescriptor}; the swap was created on another wallet`,
@@ -2643,10 +3031,31 @@ async function senderIdentityForSwapRecord(wallet, record) {
2643
3031
  }
2644
3032
  }
2645
3033
 
3034
+ // src/arkadeRefunder.ts
3035
+ function arkadeRefunder(deps) {
3036
+ return async (swap) => {
3037
+ const script = swap.lockup?.script;
3038
+ if (!script) {
3039
+ throw new Error(
3040
+ `swap ${swap.rfqId} carries no lockup covenant, so its refund cannot be built`
3041
+ );
3042
+ }
3043
+ const vtxos = await findLockupVtxos(deps.indexer, swap.lockupPkScript);
3044
+ if (vtxos.length === 0) return null;
3045
+ const record = await deps.repository.getRfqSwap(swap.rfqId);
3046
+ if (!record) {
3047
+ throw new RefundNotLocallyPossibleError(
3048
+ "no-secrets",
3049
+ `no stored record for ${swap.rfqId}; the descriptor that signs its refund lives there`
3050
+ );
3051
+ }
3052
+ const sender = await senderIdentityForSwapRecord(deps.wallet, rfqSignerOf(record) ?? {});
3053
+ return pushRefundWithoutReceiver(deps.ark, { script, sender, vtxos });
3054
+ };
3055
+ }
3056
+
2646
3057
  // src/swapManager.ts
2647
- var import_base11 = require("@scure/base");
2648
- var RFQ_SWAP_TERMINAL_STATES = ["settled", "refunded", "failed"];
2649
- var isRfqSwapTerminal = (state) => RFQ_SWAP_TERMINAL_STATES.includes(state);
3058
+ var import_base13 = require("@scure/base");
2650
3059
  function nextOnchainAction(input) {
2651
3060
  switch (input.phase.phase) {
2652
3061
  case "unfunded":
@@ -2662,6 +3071,17 @@ function nextOnchainAction(input) {
2662
3071
  return input.htlcLocktime - input.now >= ONCHAIN_CLAIM_MARGIN_SECONDS ? "claim" : "claim_window_closed";
2663
3072
  }
2664
3073
  }
3074
+ var RfqSwapOriginRequired = class extends Error {
3075
+ /** The swap that could not be admitted. */
3076
+ rfqId;
3077
+ constructor(rfqId) {
3078
+ super(
3079
+ `rfq swap ${rfqId} has no stored record and no origin was supplied; pass the request-time origin as addSwap's second argument so its first record can be written`
3080
+ );
3081
+ this.name = "RfqSwapOriginRequired";
3082
+ this.rfqId = rfqId;
3083
+ }
3084
+ };
2665
3085
  var notify = (listeners, call) => {
2666
3086
  for (const listener of listeners) {
2667
3087
  try {
@@ -2739,7 +3159,20 @@ var RfqSwapManager = class {
2739
3159
  * answers instead of throwing "not found". Cleared by {@link removeSwap}. */
2740
3160
  finished = /* @__PURE__ */ new Map();
2741
3161
  waiters = /* @__PURE__ */ new Map();
2742
- /** Records changed during the current pass, flushed through `saveSwap`. */
3162
+ /**
3163
+ * The request-time origin of each swap the manager may have to CREATE a
3164
+ * record for, by rfqId.
3165
+ *
3166
+ * Needed only for a swap the store has never seen: once a record exists,
3167
+ * `updateRfqSwapRecord` carries the origin half through and the map is
3168
+ * redundant. It is kept anyway for the swap's whole life, so a record the
3169
+ * store loses between passes is rewritten rather than lost — and dropped
3170
+ * by {@link removeSwap} and by retention, which are the two places a swap
3171
+ * stops being this manager's business.
3172
+ */
3173
+ origins = /* @__PURE__ */ new Map();
3174
+ /** Records changed during the current pass, flushed to the repository and
3175
+ * through `saveSwap`. */
2743
3176
  dirty = /* @__PURE__ */ new Set();
2744
3177
  /** Race guard: one action at a time per swap. */
2745
3178
  inProgress = /* @__PURE__ */ new Set();
@@ -2761,7 +3194,9 @@ var RfqSwapManager = class {
2761
3194
  this.actionExecutedListeners.add(config.events.onActionExecuted);
2762
3195
  }
2763
3196
  }
2764
- /** Wire the money-moving half. Without it the manager only watches. */
3197
+ /** Wire the money-moving half. Without it the manager only watches. The
3198
+ * two claims may be omitted for a consumer that drives no kind reaching
3199
+ * them — see {@link AvailableRfqSwapManagerCallbacks}. */
2765
3200
  setCallbacks(callbacks) {
2766
3201
  this.callbacks = callbacks;
2767
3202
  }
@@ -2781,6 +3216,116 @@ var RfqSwapManager = class {
2781
3216
  this.actionExecutedListeners.add(listener);
2782
3217
  return () => this.actionExecutedListeners.delete(listener);
2783
3218
  }
3219
+ /**
3220
+ * Rebuild every stored swap and take over monitoring them.
3221
+ *
3222
+ * The composition a consumer otherwise writes by hand, and the one place
3223
+ * all four pieces meet: retention decides what to keep
3224
+ * (`shouldRetainRfqSwap`), the lockup's contract row supplies the covenant
3225
+ * (`lockupContractParams`), `rebuildRfqSwap` turns a record back into a
3226
+ * live swap, and each rebuilt swap arrives with its own origin, so nothing
3227
+ * is asked of the caller.
3228
+ *
3229
+ * **Deliberately not part of {@link start}.** A consumer that wants to
3230
+ * look at its records — count them, show them, prune and stop — is not
3231
+ * forced to start driving money to do it. Call this first and `start()`
3232
+ * after; a manager already running polls the restored swaps at once.
3233
+ *
3234
+ * Retention runs BEFORE the rebuild, so a record past
3235
+ * `RFQ_SWAP_RETENTION_SECONDS` costs no contract lookup on its way to being
3236
+ * dropped.
3237
+ *
3238
+ * **A record that cannot be rebuilt is reported, never swallowed and never
3239
+ * fatal.** `rebuildRfqSwap` throws by design when the covenant params do
3240
+ * not derive the funded address, and `lockupContractParams` throws
3241
+ * `LockupContractMissing` when the wallet has no row for the lockup — both
3242
+ * say something true about that one record, and neither is a reason to
3243
+ * strand the others. They come back in {@link RfqRestoreResult.failed}.
3244
+ */
3245
+ async restoreFromRepository(options = {}) {
3246
+ const repository = this.requireRepository("restoreFromRepository");
3247
+ const params = options.params ?? this.paramsFromContracts();
3248
+ const records = await repository.getAllRfqSwaps();
3249
+ const pruned = await this.dropRetired(repository, records);
3250
+ const retired = new Set(pruned);
3251
+ const restored = [];
3252
+ const failed = [];
3253
+ for (const record of records) {
3254
+ if (retired.has(record.rfqId)) continue;
3255
+ let swap;
3256
+ try {
3257
+ swap = rebuildRfqSwap(record, await params(record));
3258
+ } catch (error) {
3259
+ failed.push({
3260
+ rfqId: record.rfqId,
3261
+ error: error instanceof Error ? error : new Error(errorMessage(error))
3262
+ });
3263
+ continue;
3264
+ }
3265
+ this.origins.set(record.rfqId, rfqSwapOriginOf(record));
3266
+ if (isRfqSwapTerminal(swap.state)) this.finished.set(swap.rfqId, swap);
3267
+ else this.track(swap);
3268
+ restored.push(swap);
3269
+ }
3270
+ if (this.running) {
3271
+ await Promise.allSettled(
3272
+ restored.filter((swap) => this.monitored.has(swap.rfqId)).map((swap) => this.pollSwap(swap))
3273
+ );
3274
+ }
3275
+ return { restored, failed, pruned };
3276
+ }
3277
+ /**
3278
+ * Drop stored records that are terminal and past
3279
+ * `RFQ_SWAP_RETENTION_SECONDS`, and return their ids.
3280
+ *
3281
+ * `needs_counterparty` is never dropped, however old: the money is still at
3282
+ * the lockup and the counterparty's move still ends the swap. That rule
3283
+ * lives in `shouldRetainRfqSwap`, which this defers to rather than
3284
+ * restating.
3285
+ *
3286
+ * Public because retention is a caller's cadence, not the manager's: a
3287
+ * long-lived process wants it on a timer of its own, a mobile app wants it
3288
+ * at boot. {@link restoreFromRepository} runs it first, so the boot path
3289
+ * needs no separate call.
3290
+ */
3291
+ async pruneRetiredSwaps() {
3292
+ const repository = this.deps.repository;
3293
+ if (!repository) return [];
3294
+ return this.dropRetired(repository, await repository.getAllRfqSwaps());
3295
+ }
3296
+ async dropRetired(repository, records) {
3297
+ const now = this.config.now();
3298
+ const dropped = [];
3299
+ for (const record of records) {
3300
+ if (shouldRetainRfqSwap(record, now)) continue;
3301
+ await repository.removeRfqSwap(record.rfqId);
3302
+ this.finished.delete(record.rfqId);
3303
+ if (!this.monitored.has(record.rfqId)) this.origins.delete(record.rfqId);
3304
+ dropped.push(record.rfqId);
3305
+ }
3306
+ return dropped;
3307
+ }
3308
+ requireRepository(method) {
3309
+ const repository = this.deps.repository;
3310
+ if (!repository) {
3311
+ throw new Error(
3312
+ `${method} needs a record store; pass one as RfqSwapManagerDeps.repository`
3313
+ );
3314
+ }
3315
+ return repository;
3316
+ }
3317
+ /** The default covenant source: the wallet's own contract row for each
3318
+ * lockup, which is where registration put it before the address could be
3319
+ * funded. */
3320
+ paramsFromContracts() {
3321
+ const contracts = this.deps.contracts;
3322
+ if (!contracts) {
3323
+ throw new Error(
3324
+ "restoreFromRepository needs the covenant parameters: wire RfqSwapManagerDeps.contracts so each lockup's contract row can be read, or pass options.params"
3325
+ );
3326
+ }
3327
+ return (record) => lockupContractParams(contracts, record.lockupAddress);
3328
+ }
2784
3329
  /**
2785
3330
  * Load records and begin monitoring. Runs one pass immediately — a caller
2786
3331
  * resuming after a restart may be well past a deadline already — then
@@ -2790,8 +3335,17 @@ var RfqSwapManager = class {
2790
3335
  * Calling it again while running loads the records and returns rather than
2791
3336
  * re-arming — dropping them silently would strand a funded swap on a
2792
3337
  * caller's harmless double-start.
3338
+ *
3339
+ * The signature is unchanged with a {@link RfqSwapManagerDeps.repository}
3340
+ * wired; the origins are resolved from the store instead, by the same rule
3341
+ * {@link addSwap} applies. A swap the store has never seen throws
3342
+ * {@link RfqSwapOriginRequired} — hand that one to `addSwap` with its
3343
+ * origin, or restore the whole set with {@link restoreFromRepository},
3344
+ * which needs no caller input at all. Every swap is checked before any is
3345
+ * tracked, so a bad one in the list does not leave a half-loaded manager.
2793
3346
  */
2794
3347
  async start(swaps = []) {
3348
+ for (const swap of swaps) await this.admit(swap);
2795
3349
  for (const swap of swaps) {
2796
3350
  if (isRfqSwapTerminal(swap.state)) this.finished.set(swap.rfqId, swap);
2797
3351
  else this.track(swap);
@@ -2823,9 +3377,22 @@ var RfqSwapManager = class {
2823
3377
  this.unsubscribeContracts?.();
2824
3378
  this.unsubscribeContracts = null;
2825
3379
  }
2826
- /** Begin monitoring a swap. Polled immediately when the manager is running,
2827
- * so a just-funded swap does not wait out a whole interval. */
2828
- async addSwap(swap) {
3380
+ /**
3381
+ * Begin monitoring a swap. Polled immediately when the manager is running,
3382
+ * so a just-funded swap does not wait out a whole interval.
3383
+ *
3384
+ * `origin` is the request-time half a live swap cannot carry — the corridor,
3385
+ * the funded address, the corridor's profile, the funding txid — and it is
3386
+ * what lets the manager write this swap's FIRST record. Supply it whenever
3387
+ * a {@link RfqSwapManagerDeps.repository} is wired and the swap is new. It
3388
+ * may be omitted for a swap the store already holds a record for, which is
3389
+ * then read to confirm it; omitting it for one the store has never seen
3390
+ * throws {@link RfqSwapOriginRequired} rather than admitting a swap whose
3391
+ * record could never be written. With no repository wired the parameter is
3392
+ * inert.
3393
+ */
3394
+ async addSwap(swap, origin) {
3395
+ await this.admit(swap, origin);
2829
3396
  if (isRfqSwapTerminal(swap.state)) {
2830
3397
  this.finished.set(swap.rfqId, swap);
2831
3398
  return;
@@ -2833,6 +3400,29 @@ var RfqSwapManager = class {
2833
3400
  this.track(swap);
2834
3401
  if (this.running) await this.pollSwap(swap);
2835
3402
  }
3403
+ /**
3404
+ * Settle where this swap's record will come from, before it is monitored.
3405
+ *
3406
+ * Three ways it can be answered, in order: the caller passed an origin, one
3407
+ * is already remembered from an earlier `addSwap`, or the store holds a
3408
+ * record — which is the origin, already written. Only the last costs a read,
3409
+ * and only when the first two are absent.
3410
+ */
3411
+ async admit(swap, origin) {
3412
+ if (origin) {
3413
+ assertSameSwap(origin, swap);
3414
+ this.origins.set(swap.rfqId, origin);
3415
+ if (this.deps.repository && !isRfqSwapTerminal(swap.state)) {
3416
+ this.dirty.add(swap.rfqId);
3417
+ }
3418
+ return;
3419
+ }
3420
+ const repository = this.deps.repository;
3421
+ if (!repository || this.origins.has(swap.rfqId)) return;
3422
+ const stored = await repository.getRfqSwap(swap.rfqId);
3423
+ if (!stored) throw new RfqSwapOriginRequired(swap.rfqId);
3424
+ this.origins.set(swap.rfqId, rfqSwapOriginOf(stored));
3425
+ }
2836
3426
  /** Forget a swap entirely, monitored or finished.
2837
3427
  *
2838
3428
  * Its contract row is left alone: registration is a wallet-level fact about
@@ -2850,6 +3440,7 @@ var RfqSwapManager = class {
2850
3440
  }
2851
3441
  this.waiters.delete(rfqId);
2852
3442
  this.dirty.delete(rfqId);
3443
+ this.origins.delete(rfqId);
2853
3444
  }
2854
3445
  /** Every swap still being monitored. */
2855
3446
  async getPendingSwaps() {
@@ -2912,7 +3503,7 @@ var RfqSwapManager = class {
2912
3503
  // ── internals ────────────────────────────────────────────────────────────
2913
3504
  track(swap) {
2914
3505
  this.monitored.set(swap.rfqId, swap);
2915
- this.byLockupScript.set(import_base11.hex.encode(swap.lockupPkScript), swap);
3506
+ this.byLockupScript.set(import_base13.hex.encode(swap.lockupPkScript), swap);
2916
3507
  }
2917
3508
  /** Drops the swap from BOTH indexes. The event index is the one that stops
2918
3509
  * a late event finding a swap that is gone; `pollSwap`'s own
@@ -2921,7 +3512,7 @@ var RfqSwapManager = class {
2921
3512
  * them from silently re-driving a cancelled swap. */
2922
3513
  untrack(rfqId) {
2923
3514
  const swap = this.monitored.get(rfqId);
2924
- if (swap) this.byLockupScript.delete(import_base11.hex.encode(swap.lockupPkScript));
3515
+ if (swap) this.byLockupScript.delete(import_base13.hex.encode(swap.lockupPkScript));
2925
3516
  this.monitored.delete(rfqId);
2926
3517
  this.refundRefused.delete(rfqId);
2927
3518
  this.lastClaimError.delete(rfqId);
@@ -2983,7 +3574,7 @@ var RfqSwapManager = class {
2983
3574
  if (!lockup) {
2984
3575
  try {
2985
3576
  const [existing] = await contracts.getContracts({
2986
- script: import_base11.hex.encode(swap.lockupPkScript)
3577
+ script: import_base13.hex.encode(swap.lockupPkScript)
2987
3578
  });
2988
3579
  if (existing) {
2989
3580
  this.registered.set(swap.rfqId, true);
@@ -3002,13 +3593,13 @@ var RfqSwapManager = class {
3002
3593
  );
3003
3594
  return;
3004
3595
  }
3005
- const script = import_base11.hex.encode(lockup.script.pkScript);
3006
- if (script !== import_base11.hex.encode(swap.lockupPkScript)) {
3596
+ const script = import_base13.hex.encode(lockup.script.pkScript);
3597
+ if (script !== import_base13.hex.encode(swap.lockupPkScript)) {
3007
3598
  this.registered.set(swap.rfqId, false);
3008
3599
  this.emitFailed(
3009
3600
  swap,
3010
3601
  new Error(
3011
- `swap ${swap.rfqId} lockup script ${script} does not match its lockupPkScript ${import_base11.hex.encode(swap.lockupPkScript)}`
3602
+ `swap ${swap.rfqId} lockup script ${script} does not match its lockupPkScript ${import_base13.hex.encode(swap.lockupPkScript)}`
3012
3603
  )
3013
3604
  );
3014
3605
  return;
@@ -3027,7 +3618,7 @@ var RfqSwapManager = class {
3027
3618
  * script for its whole life. Best-effort — the swap is over either way. */
3028
3619
  retireContract(swap) {
3029
3620
  if (!this.deps.contracts || !this.registered.get(swap.rfqId)) return;
3030
- void this.deps.contracts.setContractWatchState(import_base11.hex.encode(swap.lockupPkScript), "retained").catch((error) => this.emitFailed(swap, error));
3621
+ void this.deps.contracts.setContractWatchState(import_base13.hex.encode(swap.lockupPkScript), "retained").catch((error) => this.emitFailed(swap, error));
3031
3622
  }
3032
3623
  arm() {
3033
3624
  if (!this.running) return;
@@ -3065,6 +3656,7 @@ var RfqSwapManager = class {
3065
3656
  fate = { fate: "unknown" };
3066
3657
  }
3067
3658
  if (fate.fate === "claimed" || fate.fate === "returned") {
3659
+ this.stampLockupSpends(swap, fate.spends);
3068
3660
  this.setState(swap, fate.fate === "claimed" ? "settled" : "refunded");
3069
3661
  return;
3070
3662
  }
@@ -3149,10 +3741,10 @@ var RfqSwapManager = class {
3149
3741
  );
3150
3742
  }
3151
3743
  }
3152
- if (!this.callbacks) {
3744
+ if (!this.callbacks || !this.callbacks.claimLockup) {
3153
3745
  return this.block(
3154
3746
  swap,
3155
- "no callbacks are wired, so this wallet cannot claim the lockup"
3747
+ 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"
3156
3748
  );
3157
3749
  }
3158
3750
  this.setState(swap, "claimable");
@@ -3202,8 +3794,15 @@ var RfqSwapManager = class {
3202
3794
  });
3203
3795
  if (action === "claim" && phase.phase === "claimable") {
3204
3796
  if (swap.claimTxid) return "continue";
3797
+ if (this.callbacks && !this.callbacks.claimOnchain) {
3798
+ this.block(
3799
+ swap,
3800
+ "no claimOnchain callback is wired, so this wallet cannot claim the L1 fill"
3801
+ );
3802
+ return "handled";
3803
+ }
3205
3804
  this.setOnchainState(swap, "claimable");
3206
- if (!this.config.enableAutoActions || !this.callbacks) return "handled";
3805
+ if (!this.config.enableAutoActions || !this.callbacks?.claimOnchain) return "handled";
3207
3806
  try {
3208
3807
  const { txid } = await this.callbacks.claimOnchain(swap, phase.utxo);
3209
3808
  swap.claimTxid = txid;
@@ -3316,6 +3915,24 @@ var RfqSwapManager = class {
3316
3915
  if (swap.state !== "needs_counterparty") return;
3317
3916
  this.setState(swap, traderClaimTxid(swap) ? "claimed" : "pending");
3318
3917
  }
3918
+ /**
3919
+ * Record which ark transactions ended the lockup.
3920
+ *
3921
+ * Only the ones the indexer actually named: `LockupSpend.arkTxid` is
3922
+ * optional, and a checkpoint txid is not what history correlates on — a
3923
+ * record carrying one would name a transaction the wallet's own activity
3924
+ * never shows. Fewer txids is the right failure here.
3925
+ *
3926
+ * Assigned rather than merged: the fate is one read of the whole lockup,
3927
+ * so it is the complete answer for this swap, and a swap only reaches a
3928
+ * verdict once.
3929
+ */
3930
+ stampLockupSpends(swap, spends) {
3931
+ const arkTxids = spends.map((spend) => spend.arkTxid).filter((txid) => txid !== void 0);
3932
+ if (arkTxids.length === 0) return;
3933
+ swap.lockupSpendArkTxids = arkTxids;
3934
+ this.touch(swap);
3935
+ }
3319
3936
  touch(swap) {
3320
3937
  swap.updatedAt = this.config.now();
3321
3938
  this.dirty.add(swap.rfqId);
@@ -3341,9 +3958,29 @@ var RfqSwapManager = class {
3341
3958
  emitAction(swap, action) {
3342
3959
  notify(this.actionExecutedListeners, (listener) => listener(swap, action));
3343
3960
  }
3344
- /** Whether the record is now persisted — false only when `saveSwap` threw. */
3961
+ /**
3962
+ * Flush a changed record to every sink that is wired, and say whether all
3963
+ * of them took it.
3964
+ *
3965
+ * Up to two writes, in this order: the canonical `RfqSwapRecord` to
3966
+ * {@link RfqSwapManagerDeps.repository}, then
3967
+ * {@link RfqSwapManagerCallbacks.saveSwap}. Both gate — a rejection from
3968
+ * either leaves the record dirty and monitored, so waiters stay unsettled
3969
+ * and a terminal swap is not finalized until the write it claims lands.
3970
+ * That is exactly today's rule for `saveSwap`, applied to whichever sinks
3971
+ * exist; wiring the repository does not weaken it.
3972
+ *
3973
+ * The canonical write goes FIRST and a failure there skips the second.
3974
+ * `saveSwap` is a projection of the record, and projecting a state the
3975
+ * record of record has just refused would leave the secondary sink ahead
3976
+ * of the primary — the one ordering that survives no restart.
3977
+ *
3978
+ * With neither wired the state is process-local, which is what a manager
3979
+ * with no callbacks has always done.
3980
+ */
3345
3981
  async save(swap) {
3346
- if (!this.callbacks) return true;
3982
+ if (!await this.saveRecord(swap)) return false;
3983
+ if (!this.callbacks?.saveSwap) return true;
3347
3984
  try {
3348
3985
  await this.callbacks.saveSwap(swap);
3349
3986
  return true;
@@ -3352,6 +3989,25 @@ var RfqSwapManager = class {
3352
3989
  return false;
3353
3990
  }
3354
3991
  }
3992
+ /** The canonical write. True when there is no repository to write to. */
3993
+ async saveRecord(swap) {
3994
+ const repository = this.deps.repository;
3995
+ if (!repository) return true;
3996
+ try {
3997
+ const stored = await repository.getRfqSwap(swap.rfqId);
3998
+ const record = stored ? updateRfqSwapRecord(stored, swap) : createRfqSwapRecord(this.originOrThrow(swap), swap);
3999
+ await repository.saveRfqSwap(record);
4000
+ return true;
4001
+ } catch (error) {
4002
+ this.emitFailed(swap, error);
4003
+ return false;
4004
+ }
4005
+ }
4006
+ originOrThrow(swap) {
4007
+ const origin = this.origins.get(swap.rfqId);
4008
+ if (!origin) throw new RfqSwapOriginRequired(swap.rfqId);
4009
+ return origin;
4010
+ }
3355
4011
  /**
3356
4012
  * Drop a terminal swap from monitoring and report it exactly once.
3357
4013
  *
@@ -3411,6 +4067,8 @@ var errorMessage = (error) => error instanceof Error ? error.message : String(er
3411
4067
  var outpointKey = (vtxo) => `${vtxo.txid}:${vtxo.vout}`;
3412
4068
 
3413
4069
  // src/activity.ts
4070
+ var import_sdk14 = require("@arkade-os/sdk");
4071
+ var import_base14 = require("@scure/base");
3414
4072
  var LABELS = {
3415
4073
  lightning_send: "Lightning send",
3416
4074
  lightning_receive: "Lightning receive",
@@ -3463,6 +4121,44 @@ function swapActivityResolver(deps) {
3463
4121
  }
3464
4122
  };
3465
4123
  }
4124
+ async function rfqSwapActivityInputs(deps) {
4125
+ const records = await deps.repository.getAllRfqSwaps();
4126
+ return Promise.all(records.map((record) => activityInputOf(record, deps.indexer)));
4127
+ }
4128
+ async function activityInputOf(record, indexer) {
4129
+ const txids = /* @__PURE__ */ new Set();
4130
+ if (record.fundingArkTxid) txids.add(record.fundingArkTxid);
4131
+ if (record.refundArkTxid) txids.add(record.refundArkTxid);
4132
+ const handler = rfqCorridorHandlers.getOrThrow(record.kind);
4133
+ for (const txid of handler.activityTxids?.(record.profile) ?? []) txids.add(txid);
4134
+ for (const txid of record.lockupSpendArkTxids ?? []) txids.add(txid);
4135
+ const spendUnknown = isRfqSwapTerminal(record.state) && !record.refundArkTxid && !record.lockupSpendArkTxids?.length;
4136
+ if (indexer && (!record.fundingArkTxid || spendUnknown)) {
4137
+ for (const txid of await lockupTxids(indexer, record, !record.fundingArkTxid)) {
4138
+ txids.add(txid);
4139
+ }
4140
+ }
4141
+ return { rfqId: record.rfqId, kind: record.kind, state: record.state, txids: [...txids] };
4142
+ }
4143
+ async function lockupTxids(indexer, record, wantFunding) {
4144
+ let script;
4145
+ try {
4146
+ script = import_base14.hex.encode(import_sdk14.ArkAddress.decode(record.lockupAddress).pkScript);
4147
+ } catch {
4148
+ return [];
4149
+ }
4150
+ try {
4151
+ const { vtxos } = await indexer.getVtxos({ scripts: [script] });
4152
+ const out = [];
4153
+ for (const vtxo of vtxos ?? []) {
4154
+ if (wantFunding) out.push(vtxo.txid);
4155
+ if (vtxo.arkTxId) out.push(vtxo.arkTxId);
4156
+ }
4157
+ return out;
4158
+ } catch {
4159
+ return [];
4160
+ }
4161
+ }
3466
4162
  // Annotate the CommonJS export names for ESM import in node:
3467
4163
  0 && (module.exports = {
3468
4164
  ARKADE_ASSET,
@@ -3474,7 +4170,9 @@ function swapActivityResolver(deps) {
3474
4170
  LIGHTNING_BTC,
3475
4171
  LIGHTNING_RECEIVE_PAIR,
3476
4172
  LIGHTNING_SEND_PAIR,
4173
+ LOCKTIME_THRESHOLD,
3477
4174
  LockupAmountMismatchError,
4175
+ LockupContractMissing,
3478
4176
  LockupNeedsRecoveryError,
3479
4177
  LockupRegistrationFailed,
3480
4178
  MAX_MIN_CONFIRMATIONS,
@@ -3492,10 +4190,12 @@ function swapActivityResolver(deps) {
3492
4190
  QUOTE_OPTIONS,
3493
4191
  REFUND_MTP_LAG_SECONDS,
3494
4192
  RFQ_RESOLVED_STATES,
4193
+ RFQ_SWAP_RETENTION_SECONDS,
3495
4194
  RFQ_SWAP_TERMINAL_STATES,
3496
4195
  RFQ_TERMINAL_STATES,
3497
4196
  RefundNotLocallyPossibleError,
3498
4197
  RfqSwapManager,
4198
+ RfqSwapOriginRequired,
3499
4199
  SOLO_REFUND_HEADROOM_SECONDS,
3500
4200
  SWAP_LOCKUP_CONTRACT_KIND,
3501
4201
  SWAP_LOCKUP_CONTRACT_LABEL,
@@ -3503,6 +4203,7 @@ function swapActivityResolver(deps) {
3503
4203
  SwapRefusal,
3504
4204
  addAssetSwap,
3505
4205
  arkadeAssetLeg,
4206
+ arkadeRefunder,
3506
4207
  arkadeSwapRequest,
3507
4208
  assertFundable,
3508
4209
  assertReceivable,
@@ -3518,6 +4219,7 @@ function swapActivityResolver(deps) {
3518
4219
  classifyOnchainHtlc,
3519
4220
  classifySpend,
3520
4221
  createOffer,
4222
+ createRfqSwapRecord,
3521
4223
  decodeOffer,
3522
4224
  deriveLightningReceive,
3523
4225
  deriveOnchainReceive,
@@ -3535,6 +4237,7 @@ function swapActivityResolver(deps) {
3535
4237
  lightningReceiveRequest,
3536
4238
  lightningSendRequest,
3537
4239
  lightningSendVtxoScript,
4240
+ lockupContractParams,
3538
4241
  makeCachedFeedFetch,
3539
4242
  newPreimage,
3540
4243
  newRfqId,
@@ -3543,12 +4246,14 @@ function swapActivityResolver(deps) {
3543
4246
  offerVtxoScript,
3544
4247
  onchainHtlcScript,
3545
4248
  onchainReceiveRequest,
4249
+ onchainSendProfile,
3546
4250
  onchainSendRequest,
3547
4251
  paymentHashOf,
3548
4252
  preimageForSwapRecord,
3549
4253
  pushClaim,
3550
4254
  pushRefundWithoutReceiver,
3551
4255
  readLockupFate,
4256
+ rebuildRfqSwap,
3552
4257
  receiveVtxoScript,
3553
4258
  refundIfUnresolved,
3554
4259
  registerLockupContract,
@@ -3559,9 +4264,15 @@ function swapActivityResolver(deps) {
3559
4264
  requestOnchainSend,
3560
4265
  restoreAssetSwaps,
3561
4266
  retireSettledOfferContracts,
4267
+ rfqClaimSecretOf,
3562
4268
  rfqPair,
4269
+ rfqSecretsProfile,
4270
+ rfqSignerOf,
4271
+ rfqSwapActivityInputs,
4272
+ rfqSwapOriginOf,
3563
4273
  sealClaimPacket,
3564
4274
  senderIdentityForSwapRecord,
4275
+ shouldRetainRfqSwap,
3565
4276
  spendTxidsOf,
3566
4277
  spendUpdate,
3567
4278
  swapActivityResolver,
@@ -3572,6 +4283,7 @@ function swapActivityResolver(deps) {
3572
4283
  unilateralRefundWithoutReceiverDelay,
3573
4284
  updateAssetSwap,
3574
4285
  updateAssetSwapBestEffort,
4286
+ updateRfqSwapRecord,
3575
4287
  validatePlan,
3576
4288
  verifyLockupAddress,
3577
4289
  verifyReceiveInvoice,