@arkade-os/swap 0.0.7 → 0.0.8

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
@@ -40,6 +40,7 @@ __export(index_exports, {
40
40
  LIGHTNING_RECEIVE_PAIR: () => LIGHTNING_RECEIVE_PAIR,
41
41
  LIGHTNING_SEND_PAIR: () => LIGHTNING_SEND_PAIR,
42
42
  LockupAmountMismatchError: () => LockupAmountMismatchError,
43
+ LockupContractMissing: () => LockupContractMissing,
43
44
  LockupNeedsRecoveryError: () => LockupNeedsRecoveryError,
44
45
  LockupRegistrationFailed: () => LockupRegistrationFailed,
45
46
  MAX_MIN_CONFIRMATIONS: () => MAX_MIN_CONFIRMATIONS,
@@ -57,10 +58,12 @@ __export(index_exports, {
57
58
  QUOTE_OPTIONS: () => QUOTE_OPTIONS,
58
59
  REFUND_MTP_LAG_SECONDS: () => REFUND_MTP_LAG_SECONDS,
59
60
  RFQ_RESOLVED_STATES: () => RFQ_RESOLVED_STATES,
61
+ RFQ_SWAP_RETENTION_SECONDS: () => RFQ_SWAP_RETENTION_SECONDS,
60
62
  RFQ_SWAP_TERMINAL_STATES: () => RFQ_SWAP_TERMINAL_STATES,
61
63
  RFQ_TERMINAL_STATES: () => RFQ_TERMINAL_STATES,
62
64
  RefundNotLocallyPossibleError: () => RefundNotLocallyPossibleError,
63
65
  RfqSwapManager: () => RfqSwapManager,
66
+ RfqSwapOriginRequired: () => RfqSwapOriginRequired,
64
67
  SOLO_REFUND_HEADROOM_SECONDS: () => SOLO_REFUND_HEADROOM_SECONDS,
65
68
  SWAP_LOCKUP_CONTRACT_KIND: () => SWAP_LOCKUP_CONTRACT_KIND,
66
69
  SWAP_LOCKUP_CONTRACT_LABEL: () => SWAP_LOCKUP_CONTRACT_LABEL,
@@ -68,6 +71,7 @@ __export(index_exports, {
68
71
  SwapRefusal: () => SwapRefusal,
69
72
  addAssetSwap: () => addAssetSwap,
70
73
  arkadeAssetLeg: () => arkadeAssetLeg,
74
+ arkadeRefunder: () => arkadeRefunder,
71
75
  arkadeSwapRequest: () => arkadeSwapRequest,
72
76
  assertFundable: () => assertFundable,
73
77
  assertReceivable: () => assertReceivable,
@@ -83,6 +87,7 @@ __export(index_exports, {
83
87
  classifyOnchainHtlc: () => classifyOnchainHtlc,
84
88
  classifySpend: () => classifySpend,
85
89
  createOffer: () => createOffer,
90
+ createRfqSwapRecord: () => createRfqSwapRecord,
86
91
  decodeOffer: () => decodeOffer,
87
92
  deriveLightningReceive: () => deriveLightningReceive,
88
93
  deriveOnchainReceive: () => deriveOnchainReceive,
@@ -100,6 +105,7 @@ __export(index_exports, {
100
105
  lightningReceiveRequest: () => lightningReceiveRequest,
101
106
  lightningSendRequest: () => lightningSendRequest,
102
107
  lightningSendVtxoScript: () => lightningSendVtxoScript,
108
+ lockupContractParams: () => lockupContractParams,
103
109
  makeCachedFeedFetch: () => makeCachedFeedFetch,
104
110
  newPreimage: () => newPreimage,
105
111
  newRfqId: () => newRfqId,
@@ -108,12 +114,14 @@ __export(index_exports, {
108
114
  offerVtxoScript: () => offerVtxoScript,
109
115
  onchainHtlcScript: () => onchainHtlcScript,
110
116
  onchainReceiveRequest: () => onchainReceiveRequest,
117
+ onchainSendProfile: () => onchainSendProfile,
111
118
  onchainSendRequest: () => onchainSendRequest,
112
119
  paymentHashOf: () => paymentHashOf,
113
120
  preimageForSwapRecord: () => preimageForSwapRecord,
114
121
  pushClaim: () => pushClaim,
115
122
  pushRefundWithoutReceiver: () => pushRefundWithoutReceiver,
116
123
  readLockupFate: () => readLockupFate,
124
+ rebuildRfqSwap: () => rebuildRfqSwap,
117
125
  receiveVtxoScript: () => receiveVtxoScript,
118
126
  refundIfUnresolved: () => refundIfUnresolved,
119
127
  registerLockupContract: () => registerLockupContract,
@@ -124,9 +132,15 @@ __export(index_exports, {
124
132
  requestOnchainSend: () => requestOnchainSend,
125
133
  restoreAssetSwaps: () => restoreAssetSwaps,
126
134
  retireSettledOfferContracts: () => retireSettledOfferContracts,
135
+ rfqClaimSecretOf: () => rfqClaimSecretOf,
127
136
  rfqPair: () => rfqPair,
137
+ rfqSecretsProfile: () => rfqSecretsProfile,
138
+ rfqSignerOf: () => rfqSignerOf,
139
+ rfqSwapActivityInputs: () => rfqSwapActivityInputs,
140
+ rfqSwapOriginOf: () => rfqSwapOriginOf,
128
141
  sealClaimPacket: () => sealClaimPacket,
129
142
  senderIdentityForSwapRecord: () => senderIdentityForSwapRecord,
143
+ shouldRetainRfqSwap: () => shouldRetainRfqSwap,
130
144
  spendTxidsOf: () => spendTxidsOf,
131
145
  spendUpdate: () => spendUpdate,
132
146
  swapActivityResolver: () => swapActivityResolver,
@@ -137,6 +151,7 @@ __export(index_exports, {
137
151
  unilateralRefundWithoutReceiverDelay: () => unilateralRefundWithoutReceiverDelay,
138
152
  updateAssetSwap: () => updateAssetSwap,
139
153
  updateAssetSwapBestEffort: () => updateAssetSwapBestEffort,
154
+ updateRfqSwapRecord: () => updateRfqSwapRecord,
140
155
  validatePlan: () => validatePlan,
141
156
  verifyLockupAddress: () => verifyLockupAddress,
142
157
  verifyReceiveInvoice: () => verifyReceiveInvoice,
@@ -762,8 +777,9 @@ var validatePlan = (plan, giveBalance, dust) => {
762
777
  // src/repository.ts
763
778
  var marketsCacheKey = (network, registry) => `arkade-intents-markets-${network}-${registry}`;
764
779
  var InMemoryAssetSwapRepository = class {
765
- version = 2;
780
+ version = 4;
766
781
  swaps = /* @__PURE__ */ new Map();
782
+ rfqSwaps = /* @__PURE__ */ new Map();
767
783
  scanned = /* @__PURE__ */ new Set();
768
784
  markets = /* @__PURE__ */ new Map();
769
785
  async saveSwap(swap) {
@@ -772,6 +788,18 @@ var InMemoryAssetSwapRepository = class {
772
788
  async getAllSwaps() {
773
789
  return [...this.swaps.values()];
774
790
  }
791
+ async saveRfqSwap(record) {
792
+ this.rfqSwaps.set(record.rfqId, record);
793
+ }
794
+ async getRfqSwap(rfqId) {
795
+ return this.rfqSwaps.get(rfqId);
796
+ }
797
+ async getAllRfqSwaps() {
798
+ return [...this.rfqSwaps.values()];
799
+ }
800
+ async removeRfqSwap(rfqId) {
801
+ this.rfqSwaps.delete(rfqId);
802
+ }
775
803
  async getScannedTxids() {
776
804
  return new Set(this.scanned);
777
805
  }
@@ -786,6 +814,7 @@ var InMemoryAssetSwapRepository = class {
786
814
  }
787
815
  async clear() {
788
816
  this.swaps.clear();
817
+ this.rfqSwaps.clear();
789
818
  this.scanned.clear();
790
819
  this.markets.clear();
791
820
  }
@@ -796,58 +825,44 @@ var InMemoryAssetSwapRepository = class {
796
825
  // src/indexedDbRepository.ts
797
826
  var import_sdk4 = require("@arkade-os/sdk");
798
827
  var DEFAULT_DB_NAME = "arkade-intents";
799
- var DB_VERSION = 1;
828
+ var DB_VERSION = 2;
800
829
  var STORE_SWAPS = "swaps";
830
+ var STORE_RFQ_SWAPS = "rfqSwaps";
801
831
  var STORE_SCANNED = "scannedTxids";
802
832
  var STORE_MARKETS = "markets";
803
833
  var STORES = [
804
834
  [STORE_SWAPS, { keyPath: "id" }],
835
+ // v2. Separate from `swaps` rather than sharing it: the two record types
836
+ // have different keys and no consumer wants them interleaved.
837
+ [STORE_RFQ_SWAPS, { keyPath: "rfqId" }],
805
838
  [STORE_SCANNED],
806
839
  [STORE_MARKETS]
807
840
  ];
808
- function initDatabase(db) {
841
+ function initDatabase(db, oldVersion, transaction) {
842
+ void oldVersion;
843
+ void transaction;
809
844
  for (const [name, options] of STORES) {
810
845
  if (!db.objectStoreNames.contains(name)) db.createObjectStore(name, options);
811
846
  }
812
847
  }
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
848
  var IndexedDbAssetSwapRepository = class {
849
+ version = 4;
850
+ connection;
823
851
  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;
852
+ this.connection = (0, import_sdk4.createManagedConnection)(dbName, DB_VERSION, initDatabase);
853
+ }
834
854
  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
- );
855
+ return this.connection.get();
841
856
  }
842
857
  async readStore(name) {
843
858
  return (await this.ensureDb()).transaction([name], "readonly").objectStore(name);
844
859
  }
845
860
  /** Every write in one place, so none of them can forget to await the
846
861
  * commit. Requests need no individual await: a failed one aborts the
847
- * transaction, which `txDone` reports. */
862
+ * transaction, which `awaitTransaction` reports. */
848
863
  async write(name, apply) {
849
864
  const tx = (await this.ensureDb()).transaction([name], "readwrite");
850
- const done = txDone(tx);
865
+ const done = (0, import_sdk4.awaitTransaction)(tx);
851
866
  apply(tx.objectStore(name));
852
867
  await done;
853
868
  }
@@ -857,10 +872,26 @@ var IndexedDbAssetSwapRepository = class {
857
872
  });
858
873
  }
859
874
  async getAllSwaps() {
860
- return request((await this.readStore(STORE_SWAPS)).getAll());
875
+ return (0, import_sdk4.promisifyRequest)((await this.readStore(STORE_SWAPS)).getAll());
876
+ }
877
+ async saveRfqSwap(record) {
878
+ await this.write(STORE_RFQ_SWAPS, (store) => {
879
+ store.put(record);
880
+ });
881
+ }
882
+ async getRfqSwap(rfqId) {
883
+ return (0, import_sdk4.promisifyRequest)((await this.readStore(STORE_RFQ_SWAPS)).get(rfqId));
884
+ }
885
+ async getAllRfqSwaps() {
886
+ return (0, import_sdk4.promisifyRequest)((await this.readStore(STORE_RFQ_SWAPS)).getAll());
887
+ }
888
+ async removeRfqSwap(rfqId) {
889
+ await this.write(STORE_RFQ_SWAPS, (store) => {
890
+ store.delete(rfqId);
891
+ });
861
892
  }
862
893
  async getScannedTxids() {
863
- const keys = await request((await this.readStore(STORE_SCANNED)).getAllKeys());
894
+ const keys = await (0, import_sdk4.promisifyRequest)((await this.readStore(STORE_SCANNED)).getAllKeys());
864
895
  return new Set(keys);
865
896
  }
866
897
  async markTxidsScanned(txids) {
@@ -870,7 +901,7 @@ var IndexedDbAssetSwapRepository = class {
870
901
  }
871
902
  async getCachedMarkets(network, registry) {
872
903
  const store = await this.readStore(STORE_MARKETS);
873
- return request(store.get(marketsCacheKey(network, registry)));
904
+ return (0, import_sdk4.promisifyRequest)(store.get(marketsCacheKey(network, registry)));
874
905
  }
875
906
  async saveCachedMarkets(network, registry, entry) {
876
907
  await this.write(STORE_MARKETS, (store) => {
@@ -883,269 +914,136 @@ var IndexedDbAssetSwapRepository = class {
883
914
  async clear() {
884
915
  const stores = STORES.map(([name]) => name);
885
916
  const tx = (await this.ensureDb()).transaction(stores, "readwrite");
886
- const done = txDone(tx);
917
+ const done = (0, import_sdk4.awaitTransaction)(tx);
887
918
  for (const name of stores) tx.objectStore(name).clear();
888
919
  await done;
889
920
  }
890
921
  async [Symbol.asyncDispose]() {
891
- if (!this.dbPromise) return;
892
- await (0, import_sdk4.closeDatabase)(this.dbName);
893
- this.dbPromise = null;
922
+ await this.connection[Symbol.asyncDispose]();
894
923
  }
895
924
  };
896
925
 
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
- }
926
+ // src/rfqCorridors.ts
927
+ var import_base4 = require("@scure/base");
928
+
929
+ // src/rfqCorridor.ts
930
+ var RfqCorridorRegistry = class {
931
+ handlers = /* @__PURE__ */ new Map();
932
+ register(handler) {
933
+ if (this.handlers.has(handler.kind)) {
934
+ throw new Error(
935
+ `RFQ corridor handler for kind '${handler.kind}' is already registered`
936
+ );
919
937
  }
938
+ this.handlers.set(handler.kind, handler);
920
939
  }
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
- }
940
+ /** Takes a bare `string`, deliberately, where {@link RfqCorridorHandler.kind}
941
+ * is the manager's union: a lookup key comes off a record a backend handed
942
+ * back, and that it typechecks as a `kind` is a claim about the type, not
943
+ * about what was stored. Narrowing this would read as a check already made. */
944
+ get(kind) {
945
+ return this.handlers.get(kind);
947
946
  }
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 {
947
+ /**
948
+ * The handler for a kind, or a loud failure.
949
+ *
950
+ * A record whose corridor is not registered cannot be restored, and
951
+ * guessing would monitor a swap with no idea how to drive it. Names the
952
+ * registered kinds so a missing `register()` call is obvious.
953
+ */
954
+ getOrThrow(kind) {
955
+ const handler = this.get(kind);
956
+ if (!handler) {
957
+ throw new Error(
958
+ `no RFQ corridor handler registered for kind '${kind}'; registered: ${this.registeredKinds().join(", ") || "none"}`
959
+ );
983
960
  }
961
+ return handler;
984
962
  }
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
- }
963
+ has(kind) {
964
+ return this.handlers.has(kind);
994
965
  }
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);
966
+ registeredKinds() {
967
+ return [...this.handlers.keys()];
1002
968
  }
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
- });
969
+ /** Test seam, mirroring the contract registry's. */
970
+ unregister(kind) {
971
+ return this.handlers.delete(kind);
1059
972
  }
1060
- return { restored, scannedTxids: fetchedTxids.filter((id) => !unresolved.has(id)) };
1061
- }
973
+ };
974
+ var rfqCorridorHandlers = new RfqCorridorRegistry();
1062
975
 
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";
976
+ // src/rfqProfileParts.ts
977
+ var rfqSecretsProfile = (secrets, paymentHash) => {
978
+ const { signingDescriptor, ...preimage } = swapSecretsToRecord(secrets);
1071
979
  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 } : {}
980
+ signer: { signingDescriptor },
981
+ ...paymentHash ? { hashlock: { paymentHash, ...preimage } } : {}
1076
982
  };
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
983
+ };
984
+ var parseHex32 = (value, field) => {
985
+ if (typeof value !== "string" || !/^[0-9a-fA-F]{64}$/.test(value)) {
986
+ throw new Error(`${field} must be 32 bytes of hex, got ${JSON.stringify(value)}`);
987
+ }
988
+ return value.toLowerCase();
989
+ };
990
+ var parseSigner = (value) => {
991
+ const signingDescriptor = value?.signingDescriptor;
992
+ if (typeof signingDescriptor !== "string" || signingDescriptor.length === 0) {
993
+ throw new Error(
994
+ `rfq profile.signer carries no signingDescriptor (got ${JSON.stringify(signingDescriptor)})`
995
+ );
996
+ }
997
+ return { signingDescriptor };
998
+ };
999
+ var parseHashlock = (value) => {
1000
+ const raw = value ?? {};
1001
+ const hashlock = {
1002
+ paymentHash: parseHex32(raw.paymentHash, "rfq profile.hashlock.paymentHash")
1139
1003
  };
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");
1004
+ if (raw.preimageHex !== void 0) {
1005
+ hashlock.preimageHex = parseHex32(raw.preimageHex, "rfq profile.hashlock.preimageHex");
1006
+ }
1007
+ if (raw.preimageSaltHex !== void 0) {
1008
+ hashlock.preimageSaltHex = parseHex32(
1009
+ raw.preimageSaltHex,
1010
+ "rfq profile.hashlock.preimageSaltHex"
1011
+ );
1012
+ }
1013
+ return hashlock;
1014
+ };
1015
+ var hydrateHashlock = (profile) => {
1016
+ try {
1017
+ return { paymentHash: parseHashlock(profile.hashlock).paymentHash };
1018
+ } catch (cause) {
1019
+ throw new Error(
1020
+ `rfq record carries no usable hashlock; it cannot verify a preimage: ${String(cause)}`,
1021
+ { cause }
1022
+ );
1023
+ }
1024
+ };
1025
+ var rfqSignerOf = (record) => {
1026
+ const signer = record.profile.signer;
1027
+ if (signer === void 0) return void 0;
1028
+ return parseSigner(signer);
1029
+ };
1030
+ var rfqClaimSecretOf = (record) => {
1031
+ const handler = rfqCorridorHandlers.getOrThrow(record.kind);
1032
+ if (!handler.claimSecret) return void 0;
1033
+ try {
1034
+ const claim = handler.claimSecret(record.profile);
1035
+ return { ...parseSigner(claim), ...parseHashlock(claim) };
1036
+ } catch (cause) {
1037
+ throw new PreimageNotRecoverableError(
1038
+ "malformed-record",
1039
+ `this ${record.kind} record's claim secret is unreadable: ${String(cause)}`,
1040
+ { cause }
1041
+ );
1042
+ }
1043
+ };
1146
1044
 
1147
1045
  // src/onchainHtlc.ts
1148
- var import_base5 = require("@scure/base");
1046
+ var import_base3 = require("@scure/base");
1149
1047
  var import_sha22 = require("@noble/hashes/sha2.js");
1150
1048
  var import_legacy = require("@noble/hashes/legacy.js");
1151
1049
  var btc = __toESM(require("@scure/btc-signer"), 1);
@@ -1155,8 +1053,8 @@ var MAX_MIN_CONFIRMATIONS = 6;
1155
1053
  var ONCHAIN_SECONDS_PER_BLOCK = 600;
1156
1054
  var ONCHAIN_DUST_SATS = BigInt(330);
1157
1055
  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));
1056
+ var paymentHashOf = (preimage) => import_base3.hex.encode((0, import_sha22.sha256)(preimage));
1057
+ var h160FromPaymentHash = (paymentHash) => (0, import_legacy.ripemd160)(import_base3.hex.decode(paymentHash));
1160
1058
  var L1_NETWORKS = {
1161
1059
  bitcoin: btc.NETWORK,
1162
1060
  testnet: btc.TEST_NETWORK,
@@ -1171,6 +1069,11 @@ function onchainHtlcScript(params, network) {
1171
1069
  `refundLocktime must be a positive unix timestamp, got ${params.refundLocktime}`
1172
1070
  );
1173
1071
  }
1072
+ if (!Object.hasOwn(L1_NETWORKS, network)) {
1073
+ throw new Error(
1074
+ `unknown L1 network '${String(network)}' \u2014 expected one of ${Object.keys(L1_NETWORKS).join(", ")}`
1075
+ );
1076
+ }
1174
1077
  const h160 = h160FromPaymentHash(params.paymentHash);
1175
1078
  const claim = btc.Script.encode([
1176
1079
  "SIZE",
@@ -1197,7 +1100,7 @@ function onchainHtlcScript(params, network) {
1197
1100
  );
1198
1101
  const controlBlockFor = (leaf) => {
1199
1102
  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)) {
1103
+ if (script.length - 1 === leaf.length && import_base3.hex.encode(script.subarray(0, leaf.length)) === import_base3.hex.encode(leaf)) {
1201
1104
  return btc.TaprootControlBlock.encode(block);
1202
1105
  }
1203
1106
  }
@@ -1290,13 +1193,13 @@ var buildHtlcRefund = (input) => buildLeafSpend({
1290
1193
  function extractPreimage(txHex, paymentHash) {
1291
1194
  let raw;
1292
1195
  try {
1293
- raw = btc.RawTx.decode(import_base5.hex.decode(txHex));
1196
+ raw = btc.RawTx.decode(import_base3.hex.decode(txHex));
1294
1197
  } catch {
1295
1198
  return null;
1296
1199
  }
1297
1200
  for (const stack of raw.witnesses ?? []) {
1298
1201
  for (const item of stack) {
1299
- if (item.length === 32 && import_base5.hex.encode((0, import_sha22.sha256)(item)) === paymentHash) return item;
1202
+ if (item.length === 32 && import_base3.hex.encode((0, import_sha22.sha256)(item)) === paymentHash) return item;
1300
1203
  }
1301
1204
  }
1302
1205
  return null;
@@ -1316,45 +1219,510 @@ async function awaitOnchainFill(chain, htlc, minConfirmations, options = {}) {
1316
1219
  await sleep(pollMs);
1317
1220
  }
1318
1221
  }
1319
- async function claimOnchainFill(chain, input) {
1320
- const now = input.now ?? Math.floor(Date.now() / 1e3);
1321
- if (input.htlc.refundLocktime - now < ONCHAIN_CLAIM_MARGIN_SECONDS) {
1322
- const error = new Error(
1323
- "refund leaf opens too soon to claim safely \u2014 take the covenant refund instead"
1324
- );
1325
- error.reason = "claim_window_closed";
1326
- throw error;
1327
- }
1328
- const spend = await buildHtlcClaim(input);
1329
- const txid = await chain.broadcast(spend.txHex);
1330
- return { txid, payoutAmount: spend.payoutAmount };
1222
+ async function claimOnchainFill(chain, input) {
1223
+ const now = input.now ?? Math.floor(Date.now() / 1e3);
1224
+ if (input.htlc.refundLocktime - now < ONCHAIN_CLAIM_MARGIN_SECONDS) {
1225
+ const error = new Error(
1226
+ "refund leaf opens too soon to claim safely \u2014 take the covenant refund instead"
1227
+ );
1228
+ error.reason = "claim_window_closed";
1229
+ throw error;
1230
+ }
1231
+ const spend = await buildHtlcClaim(input);
1232
+ const txid = await chain.broadcast(spend.txHex);
1233
+ return { txid, payoutAmount: spend.payoutAmount };
1234
+ }
1235
+ async function classifyOnchainHtlc(chain, input) {
1236
+ const utxos = await chain.getScriptUtxos(input.htlc.pkScript);
1237
+ const best = utxos.sort((a, b) => b.amount > a.amount ? 1 : -1)[0];
1238
+ if (!best) {
1239
+ if (!input.funding) return { phase: "unfunded" };
1240
+ const spend = await chain.getSpendingTx(input.funding.txid, input.funding.vout);
1241
+ if (!spend) return { phase: "unfunded" };
1242
+ const preimage = extractPreimage(spend.txHex, input.htlc.paymentHash);
1243
+ const txid = btc.Transaction.fromRaw(import_base3.hex.decode(spend.txHex), {
1244
+ allowUnknownInputs: true,
1245
+ allowUnknownOutputs: true
1246
+ }).id;
1247
+ return preimage ? { phase: "claimed", txid, preimage } : { phase: "swept", txid };
1248
+ }
1249
+ if (best.confirmations < input.minConfirmations)
1250
+ return { phase: "awaiting_confirmations", utxo: best };
1251
+ const mtp = await chain.getMtp();
1252
+ if (mtp >= input.htlc.refundLocktime) return { phase: "refundable", utxo: best };
1253
+ return { phase: "claimable", utxo: best };
1254
+ }
1255
+
1256
+ // src/rfqCorridors.ts
1257
+ var LightningSendCorridor = {
1258
+ kind: "lightning_send",
1259
+ project: () => ({}),
1260
+ hydrate: (profile) => hydrateHashlock(profile)
1261
+ // No `claimSecret`, deliberately: `provisionRefundKey` mints no preimage at
1262
+ // all, and deriving one off the refund descriptor would fail the payment
1263
+ // hash check — a `hash-mismatch` on a swap that was never broken.
1264
+ };
1265
+ var LightningReceiveCorridor = {
1266
+ kind: "lightning_receive",
1267
+ // What the manager holds: the amount gate, and its own claim once it lands.
1268
+ // `payoutAddress` comes from the request result and never changes, so the
1269
+ // caller writes it once and this leaves it alone.
1270
+ project: (swap) => {
1271
+ const receive = swap;
1272
+ return {
1273
+ expectedAmount: receive.expectedAmount,
1274
+ ...receive.claimArkTxid ? { claimArkTxid: receive.claimArkTxid } : {}
1275
+ };
1276
+ },
1277
+ hydrate(profile) {
1278
+ if (typeof profile.expectedAmount !== "number" || !Number.isFinite(profile.expectedAmount)) {
1279
+ throw new Error("lightning_receive record carries no expectedAmount; it cannot claim");
1280
+ }
1281
+ return {
1282
+ ...hydrateHashlock(profile),
1283
+ expectedAmount: profile.expectedAmount,
1284
+ ...profile.claimArkTxid ? { claimArkTxid: profile.claimArkTxid } : {}
1285
+ };
1286
+ },
1287
+ // We are the claimant here, so the preimage material on the hashlock is
1288
+ // ours to use.
1289
+ claimSecret: (profile) => ({ ...profile.signer, ...profile.hashlock }),
1290
+ activityTxids: (profile) => profile.claimArkTxid ? [profile.claimArkTxid] : []
1291
+ };
1292
+ function onchainSendProfile(result) {
1293
+ return {
1294
+ claimKey: import_base4.hex.encode(result.htlcParams.claimKey),
1295
+ refundKey: import_base4.hex.encode(result.htlcParams.refundKey),
1296
+ htlcLocktime: result.htlcParams.refundLocktime,
1297
+ network: result.l1Network,
1298
+ htlcAddress: result.htlc.address,
1299
+ minConfirmations: result.minConfirmations
1300
+ };
1301
+ }
1302
+ var OnchainSendCorridor = {
1303
+ kind: "onchain_send",
1304
+ // The L1 keys and the network are only in the request result — nothing on
1305
+ // `OnchainSendSwap` carries them — so the caller writes them once. What the
1306
+ // manager learns as it drives the swap is the fill and our own claim.
1307
+ project: (swap) => {
1308
+ const send = swap;
1309
+ return {
1310
+ ...send.funding ? { funding: send.funding } : {},
1311
+ ...send.claimTxid ? { claimTxid: send.claimTxid } : {}
1312
+ };
1313
+ },
1314
+ hydrate(profile) {
1315
+ const { paymentHash } = hydrateHashlock(profile);
1316
+ if (!profile.claimKey || !profile.refundKey) {
1317
+ throw new Error(
1318
+ "onchain_send record carries no L1 keys; its HTLC cannot be rebuilt and its refund window would pass unwatched"
1319
+ );
1320
+ }
1321
+ if (!Number.isInteger(profile.minConfirmations) || profile.minConfirmations < 1) {
1322
+ throw new Error(
1323
+ `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`
1324
+ );
1325
+ }
1326
+ const htlc = onchainHtlcScript(
1327
+ {
1328
+ // From the profile, not the covenant: the lockup commits to
1329
+ // `hash160(P)`, and the HTLC needs `sha256(P)`. One P
1330
+ // unlocks both legs, but only one of the two hashes of it
1331
+ // is recoverable here.
1332
+ paymentHash,
1333
+ claimKey: import_base4.hex.decode(profile.claimKey),
1334
+ refundKey: import_base4.hex.decode(profile.refundKey),
1335
+ refundLocktime: profile.htlcLocktime
1336
+ },
1337
+ profile.network
1338
+ );
1339
+ if (htlc.address !== profile.htlcAddress) {
1340
+ throw new Error(
1341
+ `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`
1342
+ );
1343
+ }
1344
+ return {
1345
+ paymentHash,
1346
+ htlc,
1347
+ minConfirmations: profile.minConfirmations,
1348
+ ...profile.funding ? { funding: profile.funding } : {},
1349
+ ...profile.claimTxid ? { claimTxid: profile.claimTxid } : {}
1350
+ };
1351
+ },
1352
+ // The trader claims the L1 HTLC with P, so this leg's preimage material is
1353
+ // ours.
1354
+ claimSecret: (profile) => ({ ...profile.signer, ...profile.hashlock }),
1355
+ // Our own L1 claim only. `funding` is the SOLVER's fill into the HTLC —
1356
+ // not a transaction of ours, so grouping it would claim a row this wallet
1357
+ // never made.
1358
+ activityTxids: (profile) => profile.claimTxid ? [profile.claimTxid] : []
1359
+ };
1360
+ rfqCorridorHandlers.register(LightningSendCorridor);
1361
+ rfqCorridorHandlers.register(LightningReceiveCorridor);
1362
+ rfqCorridorHandlers.register(OnchainSendCorridor);
1363
+
1364
+ // src/rfqRecord.ts
1365
+ var import_sdk5 = require("@arkade-os/sdk");
1366
+ var import_base5 = require("@scure/base");
1367
+
1368
+ // src/rfqSwapState.ts
1369
+ var RFQ_SWAP_TERMINAL_STATES = ["settled", "refunded", "failed"];
1370
+ var isRfqSwapTerminal = (state) => RFQ_SWAP_TERMINAL_STATES.includes(state);
1371
+
1372
+ // src/rfqRecord.ts
1373
+ var RFQ_SWAP_RETENTION_SECONDS = 30 * 24 * 60 * 60;
1374
+ var managerState = (swap) => ({
1375
+ rfqId: swap.rfqId,
1376
+ state: swap.state,
1377
+ createdAt: swap.createdAt,
1378
+ updatedAt: swap.updatedAt,
1379
+ ...swap.refundArkTxid ? { refundArkTxid: swap.refundArkTxid } : {},
1380
+ ...swap.lockupSpendArkTxids?.length ? { lockupSpendArkTxids: [...swap.lockupSpendArkTxids] } : {},
1381
+ ...swap.failure ? { failure: swap.failure } : {},
1382
+ ...swap.blockedReason ? { blockedReason: swap.blockedReason } : {}
1383
+ });
1384
+ function assertSameSwap(origin, swap) {
1385
+ if (origin.kind !== swap.kind) {
1386
+ throw new Error(
1387
+ `rfq swap record is a ${origin.kind} origin paired with a ${swap.kind} swap`
1388
+ );
1389
+ }
1390
+ const funded = import_base5.hex.encode(import_sdk5.ArkAddress.decode(origin.lockupAddress).pkScript);
1391
+ const watched = import_base5.hex.encode(swap.lockupPkScript);
1392
+ if (funded !== watched) {
1393
+ throw new Error(
1394
+ `rfq swap record's lockup address holds ${funded}, but the swap watches ${watched} \u2014 these are not the same swap`
1395
+ );
1396
+ }
1397
+ }
1398
+ function createRfqSwapRecord(origin, swap) {
1399
+ assertSameSwap(origin, swap);
1400
+ const handler = rfqCorridorHandlers.getOrThrow(origin.kind);
1401
+ return {
1402
+ ...origin,
1403
+ ...managerState(swap),
1404
+ // The caller wrote what only the request result knows; the handler adds
1405
+ // what the live swap already carries.
1406
+ profile: { ...origin.profile, ...handler.project(swap) }
1407
+ };
1408
+ }
1409
+ function updateRfqSwapRecord(record, swap) {
1410
+ assertSameSwap(record, swap);
1411
+ const {
1412
+ refundArkTxid: _refundArkTxid,
1413
+ lockupSpendArkTxids: _lockupSpendArkTxids,
1414
+ failure: _failure,
1415
+ blockedReason: _blockedReason,
1416
+ ...origin
1417
+ } = record;
1418
+ const handler = rfqCorridorHandlers.getOrThrow(record.kind);
1419
+ return {
1420
+ ...origin,
1421
+ ...managerState(swap),
1422
+ profile: { ...record.profile, ...handler.project(swap) }
1423
+ };
1424
+ }
1425
+ function rfqSwapOriginOf(record) {
1426
+ return {
1427
+ kind: record.kind,
1428
+ lockupAddress: record.lockupAddress,
1429
+ profile: { ...record.profile },
1430
+ ...record.amount !== void 0 ? { amount: record.amount } : {},
1431
+ ...record.fundingArkTxid ? { fundingArkTxid: record.fundingArkTxid } : {}
1432
+ };
1433
+ }
1434
+ function lockupScript(params, lockupAddress) {
1435
+ const script = import_sdk5.VHTLCV2ContractHandler.createScript(params);
1436
+ const funded = import_sdk5.ArkAddress.decode(lockupAddress).pkScript;
1437
+ if (import_base5.hex.encode(funded) !== import_base5.hex.encode(script.pkScript)) {
1438
+ throw new Error(
1439
+ `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`
1440
+ );
1441
+ }
1442
+ return script;
1443
+ }
1444
+ function rebuildRfqSwap(record, params) {
1445
+ const script = lockupScript(params, record.lockupAddress);
1446
+ const common = {
1447
+ rfqId: record.rfqId,
1448
+ state: record.state,
1449
+ lockupPkScript: script.pkScript,
1450
+ lockup: { script, address: record.lockupAddress },
1451
+ // From the covenant, which binds it: the record's own copy would be a
1452
+ // second source for the deadline the refund is gated on.
1453
+ refundLocktime: Number(script.options.refundLocktime),
1454
+ createdAt: record.createdAt,
1455
+ updatedAt: record.updatedAt,
1456
+ ...record.refundArkTxid ? { refundArkTxid: record.refundArkTxid } : {},
1457
+ ...record.lockupSpendArkTxids?.length ? { lockupSpendArkTxids: [...record.lockupSpendArkTxids] } : {},
1458
+ ...record.failure ? { failure: record.failure } : {},
1459
+ ...record.blockedReason ? { blockedReason: record.blockedReason } : {}
1460
+ };
1461
+ const handler = rfqCorridorHandlers.getOrThrow(record.kind);
1462
+ return {
1463
+ ...common,
1464
+ kind: record.kind,
1465
+ ...handler.hydrate(record.profile, { lockup: script })
1466
+ };
1467
+ }
1468
+ function shouldRetainRfqSwap(record, now) {
1469
+ if (!isRfqSwapTerminal(record.state)) return true;
1470
+ return now - record.updatedAt < RFQ_SWAP_RETENTION_SECONDS;
1471
+ }
1472
+
1473
+ // src/restore.ts
1474
+ var import_base6 = require("@scure/base");
1475
+ var import_sdk6 = require("@arkade-os/sdk");
1476
+ var TXS_PER_REQUEST = 50;
1477
+ async function fetchParsedTxs(indexer, txids) {
1478
+ const parsedByTxid = /* @__PURE__ */ new Map();
1479
+ if (txids.length === 0) return parsedByTxid;
1480
+ const chunks = [];
1481
+ for (let i = 0; i < txids.length; i += TXS_PER_REQUEST) {
1482
+ chunks.push(txids.slice(i, i + TXS_PER_REQUEST));
1483
+ }
1484
+ const chunkResults = await Promise.allSettled(
1485
+ chunks.map(async (ids) => (await indexer.getVirtualTxs(ids)).txs)
1486
+ );
1487
+ for (const result of chunkResults) {
1488
+ if (result.status !== "fulfilled") continue;
1489
+ for (const psbt of result.value) {
1490
+ try {
1491
+ const parsed = import_sdk6.Transaction.fromPSBT(import_base6.base64.decode(psbt));
1492
+ parsedByTxid.set(parsed.id, parsed);
1493
+ } catch {
1494
+ }
1495
+ }
1496
+ }
1497
+ return parsedByTxid;
1498
+ }
1499
+ var unscannedSwapCandidates = (txs, existingIds, scanned) => txs.filter(
1500
+ (tx) => tx.type === "sent" && tx.redeemTxid && !existingIds.has(tx.redeemTxid) && !scanned.has(tx.redeemTxid)
1501
+ );
1502
+ function classifySpend(offer, serverPubkey, spendTx, deposit) {
1503
+ let leaves;
1504
+ try {
1505
+ const script = offerVtxoScript(offer, serverPubkey);
1506
+ if (import_base6.hex.encode(script.pkScript) !== import_base6.hex.encode(offer.swapPkScript)) return "indeterminate";
1507
+ leaves = {
1508
+ cancel: script.functionByName("cancel")?.leafScript,
1509
+ fulfill: script.functionByName("fulfill")?.leafScript
1510
+ };
1511
+ } catch {
1512
+ return "indeterminate";
1513
+ }
1514
+ for (let i = 0; i < spendTx.inputsLength; i++) {
1515
+ const input = spendTx.getInput(i);
1516
+ if (!input.txid || input.index !== deposit.vout) continue;
1517
+ if (import_base6.hex.encode(input.txid) !== deposit.txid) continue;
1518
+ for (const leaf of input.tapLeafScript ?? []) {
1519
+ const spent = import_base6.hex.encode((0, import_sdk6.scriptFromTapLeafScript)(leaf));
1520
+ if (leaves.cancel && spent === import_base6.hex.encode(leaves.cancel)) return "cancelled";
1521
+ if (leaves.fulfill && spent === import_base6.hex.encode(leaves.fulfill)) return "fulfilled";
1522
+ }
1523
+ }
1524
+ return "indeterminate";
1525
+ }
1526
+ var spendTxidsOf = (vtxo) => [vtxo.spentBy, vtxo.arkTxId].filter((id) => Boolean(id));
1527
+ function classifyDepositSpend(offer, serverPubkey, spendTxs, deposit) {
1528
+ for (const tx of spendTxs) {
1529
+ const kind = classifySpend(offer, serverPubkey, tx, deposit);
1530
+ if (kind !== "indeterminate") return kind;
1531
+ }
1532
+ return "indeterminate";
1533
+ }
1534
+ async function restoreAssetSwaps(indexer, txs, existingIds, opts) {
1535
+ const { serverPubkey, scanned = /* @__PURE__ */ new Set() } = opts;
1536
+ const candidates = unscannedSwapCandidates(txs, existingIds, scanned);
1537
+ if (candidates.length === 0) return { restored: [], scannedTxids: [] };
1538
+ const byTxid = new Map(candidates.map((tx) => [tx.redeemTxid, tx]));
1539
+ const parsedByTxid = await fetchParsedTxs(
1540
+ indexer,
1541
+ candidates.map((tx) => tx.redeemTxid)
1542
+ );
1543
+ const fetchedTxids = [];
1544
+ const found = [];
1545
+ for (const [txid, parsed] of parsedByTxid) {
1546
+ const fundingTx = byTxid.get(txid);
1547
+ if (!fundingTx) continue;
1548
+ fetchedTxids.push(txid);
1549
+ try {
1550
+ const packet = import_sdk6.Extension.fromTx(parsed).getPacketByType(OFFER_PACKET_TYPE);
1551
+ if (!packet) continue;
1552
+ const payload = packet.serialize();
1553
+ found.push({
1554
+ fundingTx,
1555
+ offer: decodeOffer(payload),
1556
+ offerHex: import_base6.hex.encode(payload)
1557
+ });
1558
+ } catch {
1559
+ }
1560
+ }
1561
+ if (found.length === 0) return { restored: [], scannedTxids: fetchedTxids };
1562
+ const scripts = [...new Set(found.map((f) => import_base6.hex.encode(f.offer.swapPkScript)))];
1563
+ const { vtxos } = await indexer.getVtxos({ scripts });
1564
+ const vtxoByScriptAndTxid = new Map(vtxos.map((v) => [`${v.script}:${v.txid}`, v]));
1565
+ const txByAnyId = /* @__PURE__ */ new Map();
1566
+ for (const tx of txs) {
1567
+ for (const id of [tx.boardingTxid, tx.redeemTxid, tx.roundTxid]) {
1568
+ if (id) txByAnyId.set(id, tx);
1569
+ }
1570
+ }
1571
+ const spendTxids = /* @__PURE__ */ new Set();
1572
+ for (const { fundingTx, offer } of found) {
1573
+ const vtxo = vtxoByScriptAndTxid.get(
1574
+ `${import_base6.hex.encode(offer.swapPkScript)}:${fundingTx.redeemTxid}`
1575
+ );
1576
+ if (vtxo?.virtualStatus.state !== "spent") continue;
1577
+ for (const txid of spendTxidsOf(vtxo)) spendTxids.add(txid);
1578
+ }
1579
+ const spendTxByTxid = await fetchParsedTxs(indexer, [...spendTxids]);
1580
+ const restored = [];
1581
+ const unresolved = /* @__PURE__ */ new Set();
1582
+ for (const { fundingTx, offer, offerHex } of found) {
1583
+ const swapPkScript = import_base6.hex.encode(offer.swapPkScript);
1584
+ const vtxo = vtxoByScriptAndTxid.get(`${swapPkScript}:${fundingTx.redeemTxid}`);
1585
+ if (!vtxo) {
1586
+ unresolved.add(fundingTx.redeemTxid);
1587
+ continue;
1588
+ }
1589
+ const depositRider = vtxo.assets?.length === 1 && vtxo.assets[0].amount > BigInt(0) ? vtxo.assets[0] : void 0;
1590
+ const fromAsset = offer.offerAsset?.toString() ?? depositRider?.assetId ?? BTC_ASSET_ID;
1591
+ const toAsset = offer.wantAsset?.toString() ?? BTC_ASSET_ID;
1592
+ const depositAmount = fromAsset === BTC_ASSET_ID ? BigInt(vtxo.value) : vtxo.assets?.find((a) => a.assetId === fromAsset)?.amount;
1593
+ if (depositAmount === void 0) {
1594
+ unresolved.add(fundingTx.redeemTxid);
1595
+ continue;
1596
+ }
1597
+ const fromAmount = depositAmount.toString();
1598
+ const state = vtxo.virtualStatus.state;
1599
+ const spentTxid = state === "spent" ? vtxo.arkTxId || vtxo.spentBy : void 0;
1600
+ let status = "pending";
1601
+ if (state === "swept") status = "recoverable";
1602
+ else if (state === "spent") {
1603
+ const spendTxs = spendTxidsOf(vtxo).map((id) => spendTxByTxid.get(id)).filter((tx) => tx !== void 0);
1604
+ const kind = classifyDepositSpend(offer, serverPubkey, spendTxs, {
1605
+ txid: vtxo.txid,
1606
+ vout: vtxo.vout
1607
+ });
1608
+ if (kind === "indeterminate") {
1609
+ unresolved.add(fundingTx.redeemTxid);
1610
+ continue;
1611
+ }
1612
+ status = kind;
1613
+ }
1614
+ restored.push({
1615
+ id: fundingTx.redeemTxid,
1616
+ fromAsset,
1617
+ toAsset,
1618
+ fromAmount,
1619
+ toAmount: offer.wantAmount.toString(),
1620
+ // ponytail(arkade-os/ts-sdk#680): empty address makes cancel fall back
1621
+ // to the current server key; store the funded address if server-key
1622
+ // rotations become real (cancelOffer now at least diagnoses the
1623
+ // mismatch instead of reporting a missing VTXO)
1624
+ swapAddress: "",
1625
+ swapPkScript,
1626
+ offerHex,
1627
+ fundingTxid: fundingTx.redeemTxid,
1628
+ spentTxid,
1629
+ status,
1630
+ createdAt: fundingTx.createdAt ? fundingTx.createdAt * 1e3 : vtxo.createdAt.getTime(),
1631
+ // the completion time is the caller's record of the spend, if it
1632
+ // has one — the psbt that classified it carries no timestamp
1633
+ ...status === "fulfilled" && spentTxid && txByAnyId.get(spentTxid)?.createdAt ? { completedAt: txByAnyId.get(spentTxid).createdAt * 1e3 } : {}
1634
+ });
1635
+ }
1636
+ return { restored, scannedTxids: fetchedTxids.filter((id) => !unresolved.has(id)) };
1637
+ }
1638
+
1639
+ // src/watch.ts
1640
+ var import_base7 = require("@scure/base");
1641
+ var import_sdk7 = require("@arkade-os/sdk");
1642
+ var TERMINAL = ["fulfilled", "cancelled", "recoverable"];
1643
+ function spendUpdate(swap, spend) {
1644
+ if (TERMINAL.includes(swap.status)) return void 0;
1645
+ if (spend.kind === "indeterminate") return void 0;
1646
+ const status = spend.kind === "cancelled" ? "cancelled" : "fulfilled";
1647
+ return {
1648
+ status,
1649
+ spentTxid: spend.txid,
1650
+ // mirrors restore.ts: a completion time is a fill's, not a cancel's
1651
+ ...status === "fulfilled" && spend.at ? { completedAt: spend.at } : {}
1652
+ };
1331
1653
  }
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 };
1654
+ async function watchOfferSwaps({
1655
+ wallet,
1656
+ arkServerUrl,
1657
+ repository,
1658
+ onUpdate
1659
+ }) {
1660
+ const manager = await wallet.getContractManager();
1661
+ const serverPubkey = import_sdk7.ArkAddress.decode(await wallet.getAddress()).serverPubKey;
1662
+ const indexer = new import_sdk7.RestIndexerProvider(arkServerUrl);
1663
+ let queue = Promise.resolve();
1664
+ const enqueue = (task) => {
1665
+ queue = queue.then(task).catch(() => {
1666
+ });
1667
+ };
1668
+ const classify = async (swap, vtxo, spentTxid) => {
1669
+ if (swap.spentTxid === spentTxid && swap.status === "cancelling") return "cancelled";
1670
+ try {
1671
+ const candidates = spendTxidsOf(vtxo);
1672
+ if (candidates.length === 0) return "indeterminate";
1673
+ const { txs } = await indexer.getVirtualTxs(candidates);
1674
+ return classifyDepositSpend(
1675
+ decodeOffer(import_base7.hex.decode(swap.offerHex)),
1676
+ serverPubkey,
1677
+ txs.map((psbt) => import_sdk7.Transaction.fromPSBT(import_base7.base64.decode(psbt))),
1678
+ { txid: vtxo.txid, vout: vtxo.vout }
1679
+ );
1680
+ } catch {
1681
+ return "indeterminate";
1682
+ }
1683
+ };
1684
+ const handleSpend = async (event) => {
1685
+ if (event.contract.metadata?.kind !== OFFER_CONTRACT_KIND) return;
1686
+ for (const vtxo of event.vtxos) {
1687
+ const spentTxid = vtxo.arkTxId || vtxo.spentBy;
1688
+ if (!spentTxid) continue;
1689
+ const swap = (await getAssetSwaps(repository)).find(
1690
+ (s) => s.fundingTxid === vtxo.txid && s.swapPkScript === event.contractScript
1691
+ );
1692
+ if (!swap) continue;
1693
+ const kind = await classify(swap, vtxo, spentTxid);
1694
+ const changes = spendUpdate(swap, { txid: spentTxid, kind, at: event.timestamp });
1695
+ if (!changes) continue;
1696
+ const { persisted, swaps } = await updateAssetSwapBestEffort(
1697
+ repository,
1698
+ swap.id,
1699
+ changes
1700
+ );
1701
+ if (!persisted) continue;
1702
+ onUpdate?.({ ...swap, ...changes });
1703
+ if (changes.status && RETIRABLE.includes(changes.status)) {
1704
+ await retireOfferContract(manager, swaps, event.contractScript);
1705
+ }
1706
+ }
1707
+ };
1708
+ const unsubscribe = manager.onContractEvent((event) => {
1709
+ if (event.type !== "vtxo_spent") return;
1710
+ enqueue(() => handleSpend(event));
1711
+ });
1712
+ return {
1713
+ stop: unsubscribe,
1714
+ idle: () => queue
1715
+ };
1351
1716
  }
1352
1717
 
1353
1718
  // src/rfq.ts
1719
+ var import_base10 = require("@scure/base");
1720
+ var import_legacy2 = require("@noble/hashes/legacy.js");
1354
1721
  var import_sdk9 = require("@arkade-os/sdk");
1722
+ var import_sdk10 = require("@arkade-os/sdk");
1355
1723
 
1356
1724
  // src/claimPacket.ts
1357
- var import_base6 = require("@scure/base");
1725
+ var import_base8 = require("@scure/base");
1358
1726
  var import_secp256k1 = require("@noble/curves/secp256k1.js");
1359
1727
  var import_hkdf = require("@noble/hashes/hkdf.js");
1360
1728
  var import_sha23 = require("@noble/hashes/sha2.js");
@@ -1393,15 +1761,29 @@ async function sealWithEntropy(input, ephemeralKey, nonce) {
1393
1761
  packet.set(ephemeralPub, 0);
1394
1762
  packet.set(nonce, 33);
1395
1763
  packet.set(sealed, 45);
1396
- return { ciphertext: import_base6.base64.encode(packet) };
1764
+ return { ciphertext: import_base8.base64.encode(packet) };
1397
1765
  }
1398
1766
 
1399
1767
  // src/lockupContract.ts
1400
- var import_base7 = require("@scure/base");
1401
- var import_sdk7 = require("@arkade-os/sdk");
1768
+ var import_base9 = require("@scure/base");
1769
+ var import_sdk8 = require("@arkade-os/sdk");
1402
1770
  var SWAP_LOCKUP_CONTRACT_TYPE = "vhtlc-v2";
1403
1771
  var SWAP_LOCKUP_CONTRACT_LABEL = "Arkade RFQ swap lockup";
1404
1772
  var SWAP_LOCKUP_CONTRACT_KIND = "rfq-swap-lockup";
1773
+ var LockupContractMissing = class extends Error {
1774
+ /** The lockup whose row is absent. */
1775
+ address;
1776
+ /** Its pkScript hex — the key the row would have been under. */
1777
+ script;
1778
+ constructor(address, script) {
1779
+ super(
1780
+ `no contract row for lockup ${address} (script ${script}); its covenant cannot be rebuilt from this wallet's contract store`
1781
+ );
1782
+ this.name = "LockupContractMissing";
1783
+ this.address = address;
1784
+ this.script = script;
1785
+ }
1786
+ };
1405
1787
  var LockupRegistrationFailed = class extends Error {
1406
1788
  /** The lockup address that was never registered — never fund it: nothing
1407
1789
  * is watching it. */
@@ -1420,8 +1802,8 @@ async function registerLockupContract(contracts, script, address) {
1420
1802
  try {
1421
1803
  await contracts.createContract({
1422
1804
  type: SWAP_LOCKUP_CONTRACT_TYPE,
1423
- params: import_sdk7.VHTLCV2ContractHandler.serializeParams(script.options),
1424
- script: import_base7.hex.encode(script.pkScript),
1805
+ params: import_sdk8.VHTLCV2ContractHandler.serializeParams(script.options),
1806
+ script: import_base9.hex.encode(script.pkScript),
1425
1807
  address,
1426
1808
  label: SWAP_LOCKUP_CONTRACT_LABEL,
1427
1809
  metadata: { genericallySpendable: false, kind: SWAP_LOCKUP_CONTRACT_KIND }
@@ -1430,6 +1812,12 @@ async function registerLockupContract(contracts, script, address) {
1430
1812
  throw new LockupRegistrationFailed(script, address, error);
1431
1813
  }
1432
1814
  }
1815
+ async function lockupContractParams(contracts, lockupAddress) {
1816
+ const script = import_base9.hex.encode(import_sdk8.ArkAddress.decode(lockupAddress).pkScript);
1817
+ const [row] = await contracts.getContracts({ script });
1818
+ if (!row) throw new LockupContractMissing(lockupAddress, script);
1819
+ return row.params;
1820
+ }
1433
1821
 
1434
1822
  // src/rfq.ts
1435
1823
  var xOnly = (key, label) => {
@@ -1441,7 +1829,7 @@ var xOnly = (key, label) => {
1441
1829
  };
1442
1830
  var solverHex = (value, field) => {
1443
1831
  try {
1444
- return import_base8.hex.decode(value);
1832
+ return import_base10.hex.decode(value);
1445
1833
  } catch {
1446
1834
  throw new Error(`solver sent malformed hex for ${field}`);
1447
1835
  }
@@ -1477,7 +1865,7 @@ var AddressMismatch = class extends Error {
1477
1865
  this.quoted = quoted;
1478
1866
  }
1479
1867
  };
1480
- var newRfqId = () => import_base8.hex.encode(crypto.getRandomValues(new Uint8Array(32)));
1868
+ var newRfqId = () => import_base10.hex.encode(crypto.getRandomValues(new Uint8Array(32)));
1481
1869
  var lightningSendRequest = (input) => ({
1482
1870
  v: 1,
1483
1871
  type: "rfq_request",
@@ -1487,7 +1875,7 @@ var lightningSendRequest = (input) => ({
1487
1875
  profile: {
1488
1876
  invoice: input.invoice,
1489
1877
  refund_address: input.refundAddress,
1490
- client_refund_pubkey: import_base8.hex.encode(input.senderPubkey)
1878
+ client_refund_pubkey: import_base10.hex.encode(input.senderPubkey)
1491
1879
  }
1492
1880
  });
1493
1881
  var arkadeSwapRequest = (input) => {
@@ -1734,11 +2122,11 @@ function lightningSendVtxoScript(params) {
1734
2122
  type: "seconds",
1735
2123
  value: BigInt(value)
1736
2124
  });
1737
- return new import_sdk8.VHTLC.ScriptV2({
2125
+ return new import_sdk9.VHTLC.ScriptV2({
1738
2126
  sender: params.senderPubkey,
1739
2127
  receiver: params.solverPubkey,
1740
2128
  server: params.serverPubkey,
1741
- preimageHash: (0, import_legacy2.ripemd160)(import_base8.hex.decode(params.paymentHash)),
2129
+ preimageHash: (0, import_legacy2.ripemd160)(import_base10.hex.decode(params.paymentHash)),
1742
2130
  refundLocktime: BigInt(params.refundLocktime),
1743
2131
  unilateralClaimDelay: seconds(params.claimDelay),
1744
2132
  unilateralRefundDelay: seconds(unilateralRefundDelay(params.claimDelay)),
@@ -1757,10 +2145,10 @@ function lightningSendVtxoScript(params) {
1757
2145
  }
1758
2146
  async function requestLightningSend(wallet, arkServerUrl, transport, params) {
1759
2147
  const rfqId = params.rfqId ?? newRfqId();
1760
- const secrets = await (0, import_sdk9.provisionRefundKey)(wallet);
2148
+ const secrets = await (0, import_sdk10.provisionRefundKey)(wallet);
1761
2149
  const senderPubkey = secrets.pubkey;
1762
2150
  const [info, refundAddress] = await Promise.all([
1763
- new import_sdk8.RestArkProvider(arkServerUrl).getInfo(),
2151
+ new import_sdk9.RestArkProvider(arkServerUrl).getInfo(),
1764
2152
  wallet.getAddress()
1765
2153
  ]);
1766
2154
  const quote = await transport.requestQuote(
@@ -1783,22 +2171,23 @@ async function requestLightningSend(wallet, arkServerUrl, transport, params) {
1783
2171
  `quote from_amount ${quote.from_amount} is below the invoice amount \u2014 a negative spread is not a quote`
1784
2172
  );
1785
2173
  }
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"),
2174
+ const serverPubkey = xOnly(import_base10.hex.decode(info.signerPubkey), "ark signer key");
2175
+ const network = (0, import_sdk9.getNetwork)(info.network);
2176
+ const treeParams = {
2177
+ solverPubkey: xOnly(import_base10.hex.decode(quote.solver_pubkey), "solver key"),
1790
2178
  refundLocktime: quote.refund_locktime,
1791
2179
  serverPubkey,
1792
2180
  paymentHash: params.invoice.paymentHash,
1793
2181
  claimDelay: unilateralClaimDelay(Number(info.unilateralExitDelay)),
1794
2182
  emulatorPubkey: xOnly(
1795
- import_base8.hex.decode((0, import_sdk8.resolveEmulatorPubkey)(network, params.emulatorPubkey)),
2183
+ import_base10.hex.decode((0, import_sdk9.resolveEmulatorPubkey)(network, params.emulatorPubkey)),
1796
2184
  "emulator signer key"
1797
2185
  ),
1798
2186
  senderPubkey,
1799
2187
  receiverPkScript: solverHex(receiverPkScriptHex, "profile.receiver_pk_script"),
1800
- refundPkScript: import_sdk8.ArkAddress.decode(refundAddress).pkScript
1801
- });
2188
+ refundPkScript: import_sdk9.ArkAddress.decode(refundAddress).pkScript
2189
+ };
2190
+ const script = lightningSendVtxoScript(treeParams);
1802
2191
  const address = script.address(network.hrp, serverPubkey).encode();
1803
2192
  verifyLockupAddress(quote, address);
1804
2193
  assertFundable({
@@ -1818,7 +2207,8 @@ async function requestLightningSend(wallet, arkServerUrl, transport, params) {
1818
2207
  script,
1819
2208
  refundAddress,
1820
2209
  senderPubkey,
1821
- secrets
2210
+ secrets,
2211
+ treeParams
1822
2212
  };
1823
2213
  }
1824
2214
  var offerTermsFromQuote = (quote, assets) => {
@@ -1837,9 +2227,9 @@ var onchainSendRequest = (input) => ({
1837
2227
  amount: input.amount,
1838
2228
  profile: {
1839
2229
  payment_hash: input.paymentHash,
1840
- payout_pubkey: import_base8.hex.encode(input.payoutPubkey),
2230
+ payout_pubkey: import_base10.hex.encode(input.payoutPubkey),
1841
2231
  refund_address: input.refundAddress,
1842
- client_refund_pubkey: import_base8.hex.encode(input.senderPubkey)
2232
+ client_refund_pubkey: import_base10.hex.encode(input.senderPubkey)
1843
2233
  }
1844
2234
  });
1845
2235
  var lightningReceiveRequest = (input) => ({
@@ -1852,7 +2242,7 @@ var lightningReceiveRequest = (input) => ({
1852
2242
  profile: {
1853
2243
  payment_hash: input.paymentHash,
1854
2244
  payout_address: input.payoutAddress,
1855
- payout_pubkey: import_base8.hex.encode(input.payoutPubkey),
2245
+ payout_pubkey: import_base10.hex.encode(input.payoutPubkey),
1856
2246
  claim_packet: input.claimPacket
1857
2247
  }
1858
2248
  });
@@ -1866,9 +2256,9 @@ var onchainReceiveRequest = (input) => ({
1866
2256
  profile: {
1867
2257
  payment_hash: input.paymentHash,
1868
2258
  claim_packet: input.claimPacket,
1869
- refund_pubkey: import_base8.hex.encode(input.refundPubkey),
2259
+ refund_pubkey: import_base10.hex.encode(input.refundPubkey),
1870
2260
  payout_address: input.payoutAddress,
1871
- payout_pubkey: import_base8.hex.encode(input.payoutPubkey)
2261
+ payout_pubkey: import_base10.hex.encode(input.payoutPubkey)
1872
2262
  }
1873
2263
  });
1874
2264
  function deriveOnchainSend(input) {
@@ -1884,7 +2274,7 @@ function deriveOnchainSend(input) {
1884
2274
  throw new Error("onchain-send quote is missing a binding field");
1885
2275
  }
1886
2276
  const script = lightningSendVtxoScript({
1887
- solverPubkey: xOnly(import_base8.hex.decode(quote.solver_pubkey), "solver key"),
2277
+ solverPubkey: xOnly(import_base10.hex.decode(quote.solver_pubkey), "solver key"),
1888
2278
  refundLocktime,
1889
2279
  serverPubkey: input.serverPubkey,
1890
2280
  paymentHash: input.paymentHash,
@@ -1892,25 +2282,25 @@ function deriveOnchainSend(input) {
1892
2282
  emulatorPubkey: input.emulatorPubkey,
1893
2283
  senderPubkey: input.senderPubkey,
1894
2284
  receiverPkScript: solverHex(receiverPkScriptHex, "profile.receiver_pk_script"),
1895
- refundPkScript: import_sdk8.ArkAddress.decode(input.refundAddress).pkScript
2285
+ refundPkScript: import_sdk9.ArkAddress.decode(input.refundAddress).pkScript
1896
2286
  });
1897
2287
  const address = script.address(input.hrp, input.serverPubkey).encode();
1898
2288
  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
- );
2289
+ const htlcParams = {
2290
+ paymentHash: input.paymentHash,
2291
+ claimKey: input.payoutPubkey,
2292
+ refundKey: xOnly(import_base10.hex.decode(htlcPubkey), "solver L1 htlc key"),
2293
+ refundLocktime: htlcLocktime
2294
+ };
2295
+ const htlc = onchainHtlcScript(htlcParams, input.l1Network);
1908
2296
  if (htlc.address !== htlcAddress) throw new AddressMismatch(htlc.address, htlcAddress);
1909
2297
  return {
1910
2298
  address,
1911
2299
  swapPkScript: script.pkScript,
1912
2300
  script,
1913
2301
  htlc,
2302
+ htlcParams,
2303
+ l1Network: input.l1Network,
1914
2304
  refundLocktime,
1915
2305
  htlcLocktime,
1916
2306
  minConfirmations
@@ -1918,16 +2308,16 @@ function deriveOnchainSend(input) {
1918
2308
  }
1919
2309
  async function requestOnchainSend(wallet, arkServerUrl, transport, params) {
1920
2310
  const rfqId = params.rfqId ?? newRfqId();
1921
- const secrets = await (0, import_sdk9.provisionClaimSecret)(wallet, { preimage: params.preimage });
2311
+ const secrets = await (0, import_sdk10.provisionClaimSecret)(wallet, { preimage: params.preimage });
1922
2312
  if (secrets.mustPersistPreimage) {
1923
2313
  console.warn(
1924
2314
  "[swap] this swap's preimage cannot be re-derived from the seed and MUST be persisted with the record before funding"
1925
2315
  );
1926
2316
  }
1927
- const paymentHash = import_base8.hex.encode(secrets.paymentHash);
2317
+ const paymentHash = import_base10.hex.encode(secrets.paymentHash);
1928
2318
  const senderPubkey = secrets.pubkey;
1929
2319
  const [info, refundAddress] = await Promise.all([
1930
- new import_sdk8.RestArkProvider(arkServerUrl).getInfo(),
2320
+ new import_sdk9.RestArkProvider(arkServerUrl).getInfo(),
1931
2321
  wallet.getAddress()
1932
2322
  ]);
1933
2323
  const quote = await transport.requestQuote(
@@ -1941,14 +2331,14 @@ async function requestOnchainSend(wallet, arkServerUrl, transport, params) {
1941
2331
  amountSide: params.amountSide
1942
2332
  })
1943
2333
  );
1944
- const network = (0, import_sdk8.getNetwork)(info.network);
2334
+ const network = (0, import_sdk9.getNetwork)(info.network);
1945
2335
  const derived = deriveOnchainSend({
1946
2336
  quote,
1947
2337
  paymentHash,
1948
2338
  payoutPubkey: params.payoutPubkey,
1949
- serverPubkey: xOnly(import_base8.hex.decode(info.signerPubkey), "ark signer key"),
2339
+ serverPubkey: xOnly(import_base10.hex.decode(info.signerPubkey), "ark signer key"),
1950
2340
  emulatorPubkey: xOnly(
1951
- import_base8.hex.decode((0, import_sdk8.resolveEmulatorPubkey)(network, params.emulatorPubkey)),
2341
+ import_base10.hex.decode((0, import_sdk9.resolveEmulatorPubkey)(network, params.emulatorPubkey)),
1952
2342
  "emulator signer key"
1953
2343
  ),
1954
2344
  claimDelay: unilateralClaimDelay(Number(info.unilateralExitDelay)),
@@ -1980,6 +2370,9 @@ async function requestOnchainSend(wallet, arkServerUrl, transport, params) {
1980
2370
  script: derived.script,
1981
2371
  refundAddress,
1982
2372
  htlc: derived.htlc,
2373
+ htlcParams: derived.htlcParams,
2374
+ l1Network: derived.l1Network,
2375
+ minConfirmations: derived.minConfirmations,
1983
2376
  senderPubkey,
1984
2377
  secrets
1985
2378
  };
@@ -2056,11 +2449,11 @@ function receiveVtxoScript(params) {
2056
2449
  type: "seconds",
2057
2450
  value: BigInt(value)
2058
2451
  });
2059
- return new import_sdk8.VHTLC.ScriptV2({
2452
+ return new import_sdk9.VHTLC.ScriptV2({
2060
2453
  sender: params.solverPubkey,
2061
2454
  receiver: params.payoutPubkey,
2062
2455
  server: params.serverPubkey,
2063
- preimageHash: (0, import_legacy2.ripemd160)(import_base8.hex.decode(params.paymentHash)),
2456
+ preimageHash: (0, import_legacy2.ripemd160)(import_base10.hex.decode(params.paymentHash)),
2064
2457
  refundLocktime: BigInt(params.refundLocktime),
2065
2458
  unilateralClaimDelay: seconds(params.claimDelay),
2066
2459
  unilateralRefundDelay: seconds(unilateralRefundDelay(params.claimDelay)),
@@ -2086,8 +2479,8 @@ function deriveLightningReceive(input) {
2086
2479
  if (refundLocktime === void 0 || invoice === void 0 || solverRefundPkScriptHex === void 0) {
2087
2480
  throw new Error("lightning-receive quote is missing a binding field");
2088
2481
  }
2089
- const script = receiveVtxoScript({
2090
- solverPubkey: xOnly(import_base8.hex.decode(quote.solver_pubkey), "solver key"),
2482
+ const treeParams = {
2483
+ solverPubkey: xOnly(import_base10.hex.decode(quote.solver_pubkey), "solver key"),
2091
2484
  refundLocktime,
2092
2485
  serverPubkey: input.serverPubkey,
2093
2486
  paymentHash: input.paymentHash,
@@ -2095,25 +2488,26 @@ function deriveLightningReceive(input) {
2095
2488
  emulatorPubkey: input.emulatorPubkey,
2096
2489
  solverRefundPkScript: solverHex(solverRefundPkScriptHex, "profile.solver_refund_pk_script"),
2097
2490
  payoutPubkey: input.payoutPubkey,
2098
- payoutPkScript: import_sdk8.ArkAddress.decode(input.payoutAddress).pkScript
2099
- });
2491
+ payoutPkScript: import_sdk9.ArkAddress.decode(input.payoutAddress).pkScript
2492
+ };
2493
+ const script = receiveVtxoScript(treeParams);
2100
2494
  const address = script.address(input.hrp, input.serverPubkey).encode();
2101
2495
  verifyLockupAddress(quote, address);
2102
- return { address, swapPkScript: script.pkScript, script, invoice, refundLocktime };
2496
+ return { address, swapPkScript: script.pkScript, script, invoice, refundLocktime, treeParams };
2103
2497
  }
2104
2498
  async function requestLightningReceive(wallet, arkServerUrl, transport, params) {
2105
2499
  const rfqId = params.rfqId ?? newRfqId();
2106
- const secrets = await (0, import_sdk9.provisionClaimSecret)(wallet);
2500
+ const secrets = await (0, import_sdk10.provisionClaimSecret)(wallet);
2107
2501
  if (secrets.mustPersistPreimage) {
2108
2502
  console.warn(
2109
2503
  "[swap] this swap's preimage cannot be re-derived from the seed and MUST be persisted with the record before paying"
2110
2504
  );
2111
2505
  }
2112
2506
  const preimage = secrets.preimage;
2113
- const paymentHash = import_base8.hex.encode(secrets.paymentHash);
2507
+ const paymentHash = import_base10.hex.encode(secrets.paymentHash);
2114
2508
  const payoutPubkey = secrets.pubkey;
2115
2509
  const [info, payoutAddress] = await Promise.all([
2116
- new import_sdk8.RestArkProvider(arkServerUrl).getInfo(),
2510
+ new import_sdk9.RestArkProvider(arkServerUrl).getInfo(),
2117
2511
  wallet.getAddress()
2118
2512
  ]);
2119
2513
  const claimPacket = await sealClaimPacket({
@@ -2132,15 +2526,15 @@ async function requestLightningReceive(wallet, arkServerUrl, transport, params)
2132
2526
  })
2133
2527
  );
2134
2528
  assertQuotedAmount(quote, params.amountSide, params.amount);
2135
- const network = (0, import_sdk8.getNetwork)(info.network);
2529
+ const network = (0, import_sdk9.getNetwork)(info.network);
2136
2530
  const derived = deriveLightningReceive({
2137
2531
  quote,
2138
2532
  paymentHash,
2139
2533
  payoutPubkey,
2140
2534
  payoutAddress,
2141
- serverPubkey: xOnly(import_base8.hex.decode(info.signerPubkey), "ark signer key"),
2535
+ serverPubkey: xOnly(import_base10.hex.decode(info.signerPubkey), "ark signer key"),
2142
2536
  emulatorPubkey: xOnly(
2143
- import_base8.hex.decode((0, import_sdk8.resolveEmulatorPubkey)(network, params.emulatorPubkey)),
2537
+ import_base10.hex.decode((0, import_sdk9.resolveEmulatorPubkey)(network, params.emulatorPubkey)),
2144
2538
  "emulator signer key"
2145
2539
  ),
2146
2540
  claimDelay: unilateralClaimDelay(Number(info.unilateralExitDelay)),
@@ -2171,7 +2565,8 @@ async function requestLightningReceive(wallet, arkServerUrl, transport, params)
2171
2565
  script: derived.script,
2172
2566
  payoutAddress,
2173
2567
  payoutPubkey,
2174
- secrets
2568
+ secrets,
2569
+ treeParams: derived.treeParams
2175
2570
  };
2176
2571
  }
2177
2572
  function deriveOnchainReceive(input) {
@@ -2187,7 +2582,7 @@ function deriveOnchainReceive(input) {
2187
2582
  throw new Error("onchain-receive quote is missing a binding field");
2188
2583
  }
2189
2584
  const script = receiveVtxoScript({
2190
- solverPubkey: xOnly(import_base8.hex.decode(quote.solver_pubkey), "solver key"),
2585
+ solverPubkey: xOnly(import_base10.hex.decode(quote.solver_pubkey), "solver key"),
2191
2586
  refundLocktime,
2192
2587
  serverPubkey: input.serverPubkey,
2193
2588
  paymentHash: input.paymentHash,
@@ -2195,14 +2590,14 @@ function deriveOnchainReceive(input) {
2195
2590
  emulatorPubkey: input.emulatorPubkey,
2196
2591
  solverRefundPkScript: solverHex(solverRefundPkScriptHex, "profile.solver_refund_pk_script"),
2197
2592
  payoutPubkey: input.payoutPubkey,
2198
- payoutPkScript: import_sdk8.ArkAddress.decode(input.payoutAddress).pkScript
2593
+ payoutPkScript: import_sdk9.ArkAddress.decode(input.payoutAddress).pkScript
2199
2594
  });
2200
2595
  const address = script.address(input.hrp, input.serverPubkey).encode();
2201
2596
  verifyLockupAddress(quote, address);
2202
2597
  const htlc = onchainHtlcScript(
2203
2598
  {
2204
2599
  paymentHash: input.paymentHash,
2205
- claimKey: xOnly(import_base8.hex.decode(claimPubkey), "solver L1 claim key"),
2600
+ claimKey: xOnly(import_base10.hex.decode(claimPubkey), "solver L1 claim key"),
2206
2601
  refundKey: input.refundPubkey,
2207
2602
  refundLocktime: htlcLocktime
2208
2603
  },
@@ -2221,17 +2616,17 @@ function deriveOnchainReceive(input) {
2221
2616
  }
2222
2617
  async function requestOnchainReceive(wallet, arkServerUrl, transport, params) {
2223
2618
  const rfqId = params.rfqId ?? newRfqId();
2224
- const secrets = await (0, import_sdk9.provisionClaimSecret)(wallet);
2619
+ const secrets = await (0, import_sdk10.provisionClaimSecret)(wallet);
2225
2620
  if (secrets.mustPersistPreimage) {
2226
2621
  console.warn(
2227
2622
  "[swap] this swap's preimage cannot be re-derived from the seed and MUST be persisted with the record before funding"
2228
2623
  );
2229
2624
  }
2230
2625
  const preimage = secrets.preimage;
2231
- const paymentHash = import_base8.hex.encode(secrets.paymentHash);
2626
+ const paymentHash = import_base10.hex.encode(secrets.paymentHash);
2232
2627
  const payoutPubkey = secrets.pubkey;
2233
2628
  const [info, payoutAddress] = await Promise.all([
2234
- new import_sdk8.RestArkProvider(arkServerUrl).getInfo(),
2629
+ new import_sdk9.RestArkProvider(arkServerUrl).getInfo(),
2235
2630
  wallet.getAddress()
2236
2631
  ]);
2237
2632
  const claimPacket = await sealClaimPacket({
@@ -2251,16 +2646,16 @@ async function requestOnchainReceive(wallet, arkServerUrl, transport, params) {
2251
2646
  })
2252
2647
  );
2253
2648
  assertQuotedAmount(quote, params.amountSide, params.amount);
2254
- const network = (0, import_sdk8.getNetwork)(info.network);
2649
+ const network = (0, import_sdk9.getNetwork)(info.network);
2255
2650
  const derived = deriveOnchainReceive({
2256
2651
  quote,
2257
2652
  paymentHash,
2258
2653
  payoutPubkey,
2259
2654
  payoutAddress,
2260
2655
  refundPubkey: params.refundPubkey,
2261
- serverPubkey: xOnly(import_base8.hex.decode(info.signerPubkey), "ark signer key"),
2656
+ serverPubkey: xOnly(import_base10.hex.decode(info.signerPubkey), "ark signer key"),
2262
2657
  emulatorPubkey: xOnly(
2263
- import_base8.hex.decode((0, import_sdk8.resolveEmulatorPubkey)(network, params.emulatorPubkey)),
2658
+ import_base10.hex.decode((0, import_sdk9.resolveEmulatorPubkey)(network, params.emulatorPubkey)),
2264
2659
  "emulator signer key"
2265
2660
  ),
2266
2661
  claimDelay: unilateralClaimDelay(Number(info.unilateralExitDelay)),
@@ -2297,15 +2692,15 @@ async function requestOnchainReceive(wallet, arkServerUrl, transport, params) {
2297
2692
  }
2298
2693
 
2299
2694
  // src/claim.ts
2300
- var import_base10 = require("@scure/base");
2695
+ var import_base12 = require("@scure/base");
2301
2696
  var import_legacy3 = require("@noble/hashes/legacy.js");
2302
2697
  var import_sha25 = require("@noble/hashes/sha2.js");
2303
- var import_sdk11 = require("@arkade-os/sdk");
2698
+ var import_sdk12 = require("@arkade-os/sdk");
2304
2699
 
2305
2700
  // src/refund.ts
2306
- var import_base9 = require("@scure/base");
2701
+ var import_base11 = require("@scure/base");
2307
2702
  var import_sha24 = require("@noble/hashes/sha2.js");
2308
- var import_sdk10 = require("@arkade-os/sdk");
2703
+ var import_sdk11 = require("@arkade-os/sdk");
2309
2704
  var sleep2 = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
2310
2705
  var isRfqTerminal = (state) => RFQ_TERMINAL_STATES.includes(state);
2311
2706
  var RFQ_RESOLVED_STATES = ["settled", "refunded"];
@@ -2352,7 +2747,7 @@ var LockupNeedsRecoveryError = class extends Error {
2352
2747
  }
2353
2748
  };
2354
2749
  async function findLockupVtxos(indexer, swapPkScript) {
2355
- const scripts = [import_base9.hex.encode(swapPkScript)];
2750
+ const scripts = [import_base11.hex.encode(swapPkScript)];
2356
2751
  const [spendable, recoverable] = await Promise.all([
2357
2752
  indexer.getVtxos({ scripts, spendableOnly: true }),
2358
2753
  indexer.getVtxos({ scripts, recoverableOnly: true })
@@ -2377,28 +2772,33 @@ async function findLockupVtxos(indexer, swapPkScript) {
2377
2772
  }
2378
2773
  return out;
2379
2774
  }
2380
- var hashesTo = (candidate, paymentHash) => import_base9.hex.encode((0, import_sha24.sha256)(candidate)) === paymentHash;
2775
+ var hashesTo = (candidate, paymentHash) => import_base11.hex.encode((0, import_sha24.sha256)(candidate)) === paymentHash;
2381
2776
  var candidateWitnessItems = (tx, inputIndex) => [
2382
- ...(0, import_sdk10.getArkPsbtFields)(tx, inputIndex, import_sdk10.ConditionWitness).flat(),
2777
+ ...(0, import_sdk11.getArkPsbtFields)(tx, inputIndex, import_sdk11.ConditionWitness).flat(),
2383
2778
  ...tx.getInput(inputIndex).finalScriptWitness ?? []
2384
2779
  ];
2385
2780
  async function readLockupFate(indexer, input) {
2386
- const { vtxos } = await indexer.getVtxos({ scripts: [import_base9.hex.encode(input.swapPkScript)] });
2781
+ const { vtxos } = await indexer.getVtxos({ scripts: [import_base11.hex.encode(input.swapPkScript)] });
2387
2782
  const all = vtxos ?? [];
2388
2783
  if (all.length === 0) return { fate: "unknown" };
2389
- const spentBy = /* @__PURE__ */ new Set();
2784
+ const spentBy = /* @__PURE__ */ new Map();
2390
2785
  let everySpendNamed = true;
2391
2786
  for (const vtxo of all) {
2392
2787
  if (!vtxo.isSpent && !vtxo.spentBy && !vtxo.settledBy) return { fate: "open" };
2393
- if (vtxo.spentBy) spentBy.add(vtxo.spentBy);
2788
+ if (vtxo.spentBy)
2789
+ spentBy.set(vtxo.spentBy, {
2790
+ checkpointTxid: vtxo.spentBy,
2791
+ arkTxid: vtxo.arkTxId
2792
+ });
2394
2793
  else everySpendNamed = false;
2395
2794
  }
2396
- const { txs } = await indexer.getVirtualTxs([...spentBy]);
2795
+ const spends = [...spentBy.values()];
2796
+ const { txs } = await indexer.getVirtualTxs([...spentBy.keys()]);
2397
2797
  const observed = /* @__PURE__ */ new Set();
2398
2798
  for (const raw of txs) {
2399
2799
  let tx;
2400
2800
  try {
2401
- tx = import_sdk10.Transaction.fromPSBT(import_base9.base64.decode(raw));
2801
+ tx = import_sdk11.Transaction.fromPSBT(import_base11.base64.decode(raw));
2402
2802
  } catch {
2403
2803
  continue;
2404
2804
  }
@@ -2406,16 +2806,16 @@ async function readLockupFate(indexer, input) {
2406
2806
  for (let i = 0; i < tx.inputsLength; i++) {
2407
2807
  const spent = tx.getInput(i);
2408
2808
  if (!spent.txid) continue;
2409
- const txid = import_base9.hex.encode(spent.txid);
2809
+ const txid = import_base11.hex.encode(spent.txid);
2410
2810
  if (!all.some((vtxo) => vtxo.txid === txid && vtxo.vout === spent.index)) continue;
2411
2811
  for (const candidate of candidateWitnessItems(tx, i)) {
2412
2812
  if (hashesTo(candidate, input.paymentHash)) {
2413
- return { fate: "claimed", preimage: candidate };
2813
+ return { fate: "claimed", preimage: candidate, spends };
2414
2814
  }
2415
2815
  }
2416
2816
  }
2417
2817
  }
2418
- return everySpendNamed && observed.size === spentBy.size ? { fate: "returned" } : { fate: "unknown" };
2818
+ return everySpendNamed && observed.size === spentBy.size ? { fate: "returned", spends } : { fate: "unknown" };
2419
2819
  }
2420
2820
  async function pushRefundWithoutReceiver(ark, input) {
2421
2821
  if (input.vtxos.length === 0) throw new Error("nothing to refund: no funded outputs");
@@ -2435,14 +2835,14 @@ async function pushRefundWithoutReceiver(ark, input) {
2435
2835
  const info = await ark.getInfo();
2436
2836
  let serverUnrollScript;
2437
2837
  try {
2438
- serverUnrollScript = import_sdk10.CSVMultisigTapscript.decode(import_base9.hex.decode(info.checkpointTapscript));
2838
+ serverUnrollScript = import_sdk11.CSVMultisigTapscript.decode(import_base11.hex.decode(info.checkpointTapscript));
2439
2839
  } catch {
2440
2840
  throw new Error("invalid checkpointTapscript from the Arkade server");
2441
2841
  }
2442
2842
  const leaf = input.script.refundWithoutReceiver();
2443
2843
  const tapTree = input.script.encode();
2444
2844
  const amount = input.vtxos.reduce((sum, vtxo) => sum + vtxo.value, 0);
2445
- const { arkTx, checkpoints } = (0, import_sdk10.buildOffchainTx)(
2845
+ const { arkTx, checkpoints } = (0, import_sdk11.buildOffchainTx)(
2446
2846
  input.vtxos.map((vtxo) => ({
2447
2847
  txid: vtxo.txid,
2448
2848
  vout: vtxo.vout,
@@ -2455,18 +2855,18 @@ async function pushRefundWithoutReceiver(ark, input) {
2455
2855
  );
2456
2856
  const signedArkTx = await input.sender.sign(arkTx);
2457
2857
  const submitted = await ark.submitTx(
2458
- import_base9.base64.encode(signedArkTx.toPSBT()),
2459
- checkpoints.map((c) => import_base9.base64.encode(c.toPSBT()))
2858
+ import_base11.base64.encode(signedArkTx.toPSBT()),
2859
+ checkpoints.map((c) => import_base11.base64.encode(c.toPSBT()))
2460
2860
  );
2461
- (0, import_sdk10.assertSubmittedArkTxid)(submitted, signedArkTx, "refundWithoutReceiver");
2462
- const matched = (0, import_sdk10.matchServerCheckpoints)(
2861
+ (0, import_sdk11.assertSubmittedArkTxid)(submitted, signedArkTx, "refundWithoutReceiver");
2862
+ const matched = (0, import_sdk11.matchServerCheckpoints)(
2463
2863
  submitted.signedCheckpointTxs,
2464
2864
  checkpoints,
2465
2865
  "refundWithoutReceiver"
2466
2866
  );
2467
2867
  const finalCheckpoints = await Promise.all(
2468
2868
  matched.map(
2469
- async ({ server }) => import_base9.base64.encode((await input.sender.sign(server, [0])).toPSBT())
2869
+ async ({ server }) => import_base11.base64.encode((await input.sender.sign(server, [0])).toPSBT())
2470
2870
  )
2471
2871
  );
2472
2872
  await ark.finalizeTx(submitted.arkTxid, finalCheckpoints);
@@ -2546,20 +2946,20 @@ async function pushClaim(ark, input) {
2546
2946
  }
2547
2947
  }
2548
2948
  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)) {
2949
+ if (import_base12.hex.encode((0, import_legacy3.ripemd160)((0, import_sha25.sha256)(input.preimage))) !== import_base12.hex.encode(committed)) {
2550
2950
  throw new Error("preimage does not match the covenant's payment hash");
2551
2951
  }
2552
2952
  const info = await ark.getInfo();
2553
2953
  let serverUnrollScript;
2554
2954
  try {
2555
- serverUnrollScript = import_sdk11.CSVMultisigTapscript.decode(import_base10.hex.decode(info.checkpointTapscript));
2955
+ serverUnrollScript = import_sdk12.CSVMultisigTapscript.decode(import_base12.hex.decode(info.checkpointTapscript));
2556
2956
  } catch {
2557
2957
  throw new Error("invalid checkpointTapscript from the Arkade server");
2558
2958
  }
2559
2959
  const leaf = input.script.claim();
2560
2960
  const tapTree = input.script.encode();
2561
- const arkTxid = await (0, import_sdk11.signAndSubmitOffchainTx)({
2562
- identity: (0, import_sdk11.claimWithPreimageIdentity)(input.receiver, input.preimage),
2961
+ const arkTxid = await (0, import_sdk12.signAndSubmitOffchainTx)({
2962
+ identity: (0, import_sdk12.claimWithPreimageIdentity)(input.receiver, input.preimage),
2563
2963
  provider: ark,
2564
2964
  inputs: input.vtxos.map((vtxo) => ({
2565
2965
  txid: vtxo.txid,
@@ -2606,7 +3006,7 @@ async function claimReceiveLockup(indexer, ark, input) {
2606
3006
  }
2607
3007
 
2608
3008
  // src/refundBlocked.ts
2609
- var import_sdk12 = require("@arkade-os/sdk");
3009
+ var import_sdk13 = require("@arkade-os/sdk");
2610
3010
  var RefundNotLocallyPossibleError = class extends Error {
2611
3011
  constructor(reason, message, options) {
2612
3012
  super(message, options);
@@ -2623,16 +3023,16 @@ async function senderIdentityForSwapRecord(wallet, record) {
2623
3023
  );
2624
3024
  }
2625
3025
  try {
2626
- return await (0, import_sdk12.contractSigner)(wallet, record.signingDescriptor);
3026
+ return await (0, import_sdk13.contractSigner)(wallet, record.signingDescriptor);
2627
3027
  } catch (cause) {
2628
- if (cause instanceof import_sdk12.WalletCannotSignError) {
3028
+ if (cause instanceof import_sdk13.WalletCannotSignError) {
2629
3029
  throw new RefundNotLocallyPossibleError(
2630
3030
  "unsignable-wallet",
2631
3031
  `this wallet holds ${record.signingDescriptor} but cannot sign with it; attach its signer`,
2632
3032
  { cause }
2633
3033
  );
2634
3034
  }
2635
- if (cause instanceof import_sdk12.ForeignDescriptorError) {
3035
+ if (cause instanceof import_sdk13.ForeignDescriptorError) {
2636
3036
  throw new RefundNotLocallyPossibleError(
2637
3037
  "foreign-descriptor",
2638
3038
  `this wallet cannot derive ${record.signingDescriptor}; the swap was created on another wallet`,
@@ -2643,10 +3043,31 @@ async function senderIdentityForSwapRecord(wallet, record) {
2643
3043
  }
2644
3044
  }
2645
3045
 
3046
+ // src/arkadeRefunder.ts
3047
+ function arkadeRefunder(deps) {
3048
+ return async (swap) => {
3049
+ const script = swap.lockup?.script;
3050
+ if (!script) {
3051
+ throw new Error(
3052
+ `swap ${swap.rfqId} carries no lockup covenant, so its refund cannot be built`
3053
+ );
3054
+ }
3055
+ const vtxos = await findLockupVtxos(deps.indexer, swap.lockupPkScript);
3056
+ if (vtxos.length === 0) return null;
3057
+ const record = await deps.repository.getRfqSwap(swap.rfqId);
3058
+ if (!record) {
3059
+ throw new RefundNotLocallyPossibleError(
3060
+ "no-secrets",
3061
+ `no stored record for ${swap.rfqId}; the descriptor that signs its refund lives there`
3062
+ );
3063
+ }
3064
+ const sender = await senderIdentityForSwapRecord(deps.wallet, rfqSignerOf(record) ?? {});
3065
+ return pushRefundWithoutReceiver(deps.ark, { script, sender, vtxos });
3066
+ };
3067
+ }
3068
+
2646
3069
  // 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);
3070
+ var import_base13 = require("@scure/base");
2650
3071
  function nextOnchainAction(input) {
2651
3072
  switch (input.phase.phase) {
2652
3073
  case "unfunded":
@@ -2662,6 +3083,17 @@ function nextOnchainAction(input) {
2662
3083
  return input.htlcLocktime - input.now >= ONCHAIN_CLAIM_MARGIN_SECONDS ? "claim" : "claim_window_closed";
2663
3084
  }
2664
3085
  }
3086
+ var RfqSwapOriginRequired = class extends Error {
3087
+ /** The swap that could not be admitted. */
3088
+ rfqId;
3089
+ constructor(rfqId) {
3090
+ super(
3091
+ `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`
3092
+ );
3093
+ this.name = "RfqSwapOriginRequired";
3094
+ this.rfqId = rfqId;
3095
+ }
3096
+ };
2665
3097
  var notify = (listeners, call) => {
2666
3098
  for (const listener of listeners) {
2667
3099
  try {
@@ -2739,7 +3171,20 @@ var RfqSwapManager = class {
2739
3171
  * answers instead of throwing "not found". Cleared by {@link removeSwap}. */
2740
3172
  finished = /* @__PURE__ */ new Map();
2741
3173
  waiters = /* @__PURE__ */ new Map();
2742
- /** Records changed during the current pass, flushed through `saveSwap`. */
3174
+ /**
3175
+ * The request-time origin of each swap the manager may have to CREATE a
3176
+ * record for, by rfqId.
3177
+ *
3178
+ * Needed only for a swap the store has never seen: once a record exists,
3179
+ * `updateRfqSwapRecord` carries the origin half through and the map is
3180
+ * redundant. It is kept anyway for the swap's whole life, so a record the
3181
+ * store loses between passes is rewritten rather than lost — and dropped
3182
+ * by {@link removeSwap} and by retention, which are the two places a swap
3183
+ * stops being this manager's business.
3184
+ */
3185
+ origins = /* @__PURE__ */ new Map();
3186
+ /** Records changed during the current pass, flushed to the repository and
3187
+ * through `saveSwap`. */
2743
3188
  dirty = /* @__PURE__ */ new Set();
2744
3189
  /** Race guard: one action at a time per swap. */
2745
3190
  inProgress = /* @__PURE__ */ new Set();
@@ -2761,7 +3206,9 @@ var RfqSwapManager = class {
2761
3206
  this.actionExecutedListeners.add(config.events.onActionExecuted);
2762
3207
  }
2763
3208
  }
2764
- /** Wire the money-moving half. Without it the manager only watches. */
3209
+ /** Wire the money-moving half. Without it the manager only watches. The
3210
+ * two claims may be omitted for a consumer that drives no kind reaching
3211
+ * them — see {@link AvailableRfqSwapManagerCallbacks}. */
2765
3212
  setCallbacks(callbacks) {
2766
3213
  this.callbacks = callbacks;
2767
3214
  }
@@ -2781,6 +3228,116 @@ var RfqSwapManager = class {
2781
3228
  this.actionExecutedListeners.add(listener);
2782
3229
  return () => this.actionExecutedListeners.delete(listener);
2783
3230
  }
3231
+ /**
3232
+ * Rebuild every stored swap and take over monitoring them.
3233
+ *
3234
+ * The composition a consumer otherwise writes by hand, and the one place
3235
+ * all four pieces meet: retention decides what to keep
3236
+ * (`shouldRetainRfqSwap`), the lockup's contract row supplies the covenant
3237
+ * (`lockupContractParams`), `rebuildRfqSwap` turns a record back into a
3238
+ * live swap, and each rebuilt swap arrives with its own origin, so nothing
3239
+ * is asked of the caller.
3240
+ *
3241
+ * **Deliberately not part of {@link start}.** A consumer that wants to
3242
+ * look at its records — count them, show them, prune and stop — is not
3243
+ * forced to start driving money to do it. Call this first and `start()`
3244
+ * after; a manager already running polls the restored swaps at once.
3245
+ *
3246
+ * Retention runs BEFORE the rebuild, so a record past
3247
+ * `RFQ_SWAP_RETENTION_SECONDS` costs no contract lookup on its way to being
3248
+ * dropped.
3249
+ *
3250
+ * **A record that cannot be rebuilt is reported, never swallowed and never
3251
+ * fatal.** `rebuildRfqSwap` throws by design when the covenant params do
3252
+ * not derive the funded address, and `lockupContractParams` throws
3253
+ * `LockupContractMissing` when the wallet has no row for the lockup — both
3254
+ * say something true about that one record, and neither is a reason to
3255
+ * strand the others. They come back in {@link RfqRestoreResult.failed}.
3256
+ */
3257
+ async restoreFromRepository(options = {}) {
3258
+ const repository = this.requireRepository("restoreFromRepository");
3259
+ const params = options.params ?? this.paramsFromContracts();
3260
+ const records = await repository.getAllRfqSwaps();
3261
+ const pruned = await this.dropRetired(repository, records);
3262
+ const retired = new Set(pruned);
3263
+ const restored = [];
3264
+ const failed = [];
3265
+ for (const record of records) {
3266
+ if (retired.has(record.rfqId)) continue;
3267
+ let swap;
3268
+ try {
3269
+ swap = rebuildRfqSwap(record, await params(record));
3270
+ } catch (error) {
3271
+ failed.push({
3272
+ rfqId: record.rfqId,
3273
+ error: error instanceof Error ? error : new Error(errorMessage(error))
3274
+ });
3275
+ continue;
3276
+ }
3277
+ this.origins.set(record.rfqId, rfqSwapOriginOf(record));
3278
+ if (isRfqSwapTerminal(swap.state)) this.finished.set(swap.rfqId, swap);
3279
+ else this.track(swap);
3280
+ restored.push(swap);
3281
+ }
3282
+ if (this.running) {
3283
+ await Promise.allSettled(
3284
+ restored.filter((swap) => this.monitored.has(swap.rfqId)).map((swap) => this.pollSwap(swap))
3285
+ );
3286
+ }
3287
+ return { restored, failed, pruned };
3288
+ }
3289
+ /**
3290
+ * Drop stored records that are terminal and past
3291
+ * `RFQ_SWAP_RETENTION_SECONDS`, and return their ids.
3292
+ *
3293
+ * `needs_counterparty` is never dropped, however old: the money is still at
3294
+ * the lockup and the counterparty's move still ends the swap. That rule
3295
+ * lives in `shouldRetainRfqSwap`, which this defers to rather than
3296
+ * restating.
3297
+ *
3298
+ * Public because retention is a caller's cadence, not the manager's: a
3299
+ * long-lived process wants it on a timer of its own, a mobile app wants it
3300
+ * at boot. {@link restoreFromRepository} runs it first, so the boot path
3301
+ * needs no separate call.
3302
+ */
3303
+ async pruneRetiredSwaps() {
3304
+ const repository = this.deps.repository;
3305
+ if (!repository) return [];
3306
+ return this.dropRetired(repository, await repository.getAllRfqSwaps());
3307
+ }
3308
+ async dropRetired(repository, records) {
3309
+ const now = this.config.now();
3310
+ const dropped = [];
3311
+ for (const record of records) {
3312
+ if (shouldRetainRfqSwap(record, now)) continue;
3313
+ await repository.removeRfqSwap(record.rfqId);
3314
+ this.finished.delete(record.rfqId);
3315
+ if (!this.monitored.has(record.rfqId)) this.origins.delete(record.rfqId);
3316
+ dropped.push(record.rfqId);
3317
+ }
3318
+ return dropped;
3319
+ }
3320
+ requireRepository(method) {
3321
+ const repository = this.deps.repository;
3322
+ if (!repository) {
3323
+ throw new Error(
3324
+ `${method} needs a record store; pass one as RfqSwapManagerDeps.repository`
3325
+ );
3326
+ }
3327
+ return repository;
3328
+ }
3329
+ /** The default covenant source: the wallet's own contract row for each
3330
+ * lockup, which is where registration put it before the address could be
3331
+ * funded. */
3332
+ paramsFromContracts() {
3333
+ const contracts = this.deps.contracts;
3334
+ if (!contracts) {
3335
+ throw new Error(
3336
+ "restoreFromRepository needs the covenant parameters: wire RfqSwapManagerDeps.contracts so each lockup's contract row can be read, or pass options.params"
3337
+ );
3338
+ }
3339
+ return (record) => lockupContractParams(contracts, record.lockupAddress);
3340
+ }
2784
3341
  /**
2785
3342
  * Load records and begin monitoring. Runs one pass immediately — a caller
2786
3343
  * resuming after a restart may be well past a deadline already — then
@@ -2790,8 +3347,17 @@ var RfqSwapManager = class {
2790
3347
  * Calling it again while running loads the records and returns rather than
2791
3348
  * re-arming — dropping them silently would strand a funded swap on a
2792
3349
  * caller's harmless double-start.
3350
+ *
3351
+ * The signature is unchanged with a {@link RfqSwapManagerDeps.repository}
3352
+ * wired; the origins are resolved from the store instead, by the same rule
3353
+ * {@link addSwap} applies. A swap the store has never seen throws
3354
+ * {@link RfqSwapOriginRequired} — hand that one to `addSwap` with its
3355
+ * origin, or restore the whole set with {@link restoreFromRepository},
3356
+ * which needs no caller input at all. Every swap is checked before any is
3357
+ * tracked, so a bad one in the list does not leave a half-loaded manager.
2793
3358
  */
2794
3359
  async start(swaps = []) {
3360
+ for (const swap of swaps) await this.admit(swap);
2795
3361
  for (const swap of swaps) {
2796
3362
  if (isRfqSwapTerminal(swap.state)) this.finished.set(swap.rfqId, swap);
2797
3363
  else this.track(swap);
@@ -2823,9 +3389,22 @@ var RfqSwapManager = class {
2823
3389
  this.unsubscribeContracts?.();
2824
3390
  this.unsubscribeContracts = null;
2825
3391
  }
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) {
3392
+ /**
3393
+ * Begin monitoring a swap. Polled immediately when the manager is running,
3394
+ * so a just-funded swap does not wait out a whole interval.
3395
+ *
3396
+ * `origin` is the request-time half a live swap cannot carry — the corridor,
3397
+ * the funded address, the corridor's profile, the funding txid — and it is
3398
+ * what lets the manager write this swap's FIRST record. Supply it whenever
3399
+ * a {@link RfqSwapManagerDeps.repository} is wired and the swap is new. It
3400
+ * may be omitted for a swap the store already holds a record for, which is
3401
+ * then read to confirm it; omitting it for one the store has never seen
3402
+ * throws {@link RfqSwapOriginRequired} rather than admitting a swap whose
3403
+ * record could never be written. With no repository wired the parameter is
3404
+ * inert.
3405
+ */
3406
+ async addSwap(swap, origin) {
3407
+ await this.admit(swap, origin);
2829
3408
  if (isRfqSwapTerminal(swap.state)) {
2830
3409
  this.finished.set(swap.rfqId, swap);
2831
3410
  return;
@@ -2833,6 +3412,29 @@ var RfqSwapManager = class {
2833
3412
  this.track(swap);
2834
3413
  if (this.running) await this.pollSwap(swap);
2835
3414
  }
3415
+ /**
3416
+ * Settle where this swap's record will come from, before it is monitored.
3417
+ *
3418
+ * Three ways it can be answered, in order: the caller passed an origin, one
3419
+ * is already remembered from an earlier `addSwap`, or the store holds a
3420
+ * record — which is the origin, already written. Only the last costs a read,
3421
+ * and only when the first two are absent.
3422
+ */
3423
+ async admit(swap, origin) {
3424
+ if (origin) {
3425
+ assertSameSwap(origin, swap);
3426
+ this.origins.set(swap.rfqId, origin);
3427
+ if (this.deps.repository && !isRfqSwapTerminal(swap.state)) {
3428
+ this.dirty.add(swap.rfqId);
3429
+ }
3430
+ return;
3431
+ }
3432
+ const repository = this.deps.repository;
3433
+ if (!repository || this.origins.has(swap.rfqId)) return;
3434
+ const stored = await repository.getRfqSwap(swap.rfqId);
3435
+ if (!stored) throw new RfqSwapOriginRequired(swap.rfqId);
3436
+ this.origins.set(swap.rfqId, rfqSwapOriginOf(stored));
3437
+ }
2836
3438
  /** Forget a swap entirely, monitored or finished.
2837
3439
  *
2838
3440
  * Its contract row is left alone: registration is a wallet-level fact about
@@ -2850,6 +3452,7 @@ var RfqSwapManager = class {
2850
3452
  }
2851
3453
  this.waiters.delete(rfqId);
2852
3454
  this.dirty.delete(rfqId);
3455
+ this.origins.delete(rfqId);
2853
3456
  }
2854
3457
  /** Every swap still being monitored. */
2855
3458
  async getPendingSwaps() {
@@ -2912,7 +3515,7 @@ var RfqSwapManager = class {
2912
3515
  // ── internals ────────────────────────────────────────────────────────────
2913
3516
  track(swap) {
2914
3517
  this.monitored.set(swap.rfqId, swap);
2915
- this.byLockupScript.set(import_base11.hex.encode(swap.lockupPkScript), swap);
3518
+ this.byLockupScript.set(import_base13.hex.encode(swap.lockupPkScript), swap);
2916
3519
  }
2917
3520
  /** Drops the swap from BOTH indexes. The event index is the one that stops
2918
3521
  * a late event finding a swap that is gone; `pollSwap`'s own
@@ -2921,7 +3524,7 @@ var RfqSwapManager = class {
2921
3524
  * them from silently re-driving a cancelled swap. */
2922
3525
  untrack(rfqId) {
2923
3526
  const swap = this.monitored.get(rfqId);
2924
- if (swap) this.byLockupScript.delete(import_base11.hex.encode(swap.lockupPkScript));
3527
+ if (swap) this.byLockupScript.delete(import_base13.hex.encode(swap.lockupPkScript));
2925
3528
  this.monitored.delete(rfqId);
2926
3529
  this.refundRefused.delete(rfqId);
2927
3530
  this.lastClaimError.delete(rfqId);
@@ -2983,7 +3586,7 @@ var RfqSwapManager = class {
2983
3586
  if (!lockup) {
2984
3587
  try {
2985
3588
  const [existing] = await contracts.getContracts({
2986
- script: import_base11.hex.encode(swap.lockupPkScript)
3589
+ script: import_base13.hex.encode(swap.lockupPkScript)
2987
3590
  });
2988
3591
  if (existing) {
2989
3592
  this.registered.set(swap.rfqId, true);
@@ -3002,13 +3605,13 @@ var RfqSwapManager = class {
3002
3605
  );
3003
3606
  return;
3004
3607
  }
3005
- const script = import_base11.hex.encode(lockup.script.pkScript);
3006
- if (script !== import_base11.hex.encode(swap.lockupPkScript)) {
3608
+ const script = import_base13.hex.encode(lockup.script.pkScript);
3609
+ if (script !== import_base13.hex.encode(swap.lockupPkScript)) {
3007
3610
  this.registered.set(swap.rfqId, false);
3008
3611
  this.emitFailed(
3009
3612
  swap,
3010
3613
  new Error(
3011
- `swap ${swap.rfqId} lockup script ${script} does not match its lockupPkScript ${import_base11.hex.encode(swap.lockupPkScript)}`
3614
+ `swap ${swap.rfqId} lockup script ${script} does not match its lockupPkScript ${import_base13.hex.encode(swap.lockupPkScript)}`
3012
3615
  )
3013
3616
  );
3014
3617
  return;
@@ -3027,7 +3630,7 @@ var RfqSwapManager = class {
3027
3630
  * script for its whole life. Best-effort — the swap is over either way. */
3028
3631
  retireContract(swap) {
3029
3632
  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));
3633
+ void this.deps.contracts.setContractWatchState(import_base13.hex.encode(swap.lockupPkScript), "retained").catch((error) => this.emitFailed(swap, error));
3031
3634
  }
3032
3635
  arm() {
3033
3636
  if (!this.running) return;
@@ -3065,6 +3668,7 @@ var RfqSwapManager = class {
3065
3668
  fate = { fate: "unknown" };
3066
3669
  }
3067
3670
  if (fate.fate === "claimed" || fate.fate === "returned") {
3671
+ this.stampLockupSpends(swap, fate.spends);
3068
3672
  this.setState(swap, fate.fate === "claimed" ? "settled" : "refunded");
3069
3673
  return;
3070
3674
  }
@@ -3149,10 +3753,10 @@ var RfqSwapManager = class {
3149
3753
  );
3150
3754
  }
3151
3755
  }
3152
- if (!this.callbacks) {
3756
+ if (!this.callbacks || !this.callbacks.claimLockup) {
3153
3757
  return this.block(
3154
3758
  swap,
3155
- "no callbacks are wired, so this wallet cannot claim the lockup"
3759
+ 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
3760
  );
3157
3761
  }
3158
3762
  this.setState(swap, "claimable");
@@ -3202,8 +3806,15 @@ var RfqSwapManager = class {
3202
3806
  });
3203
3807
  if (action === "claim" && phase.phase === "claimable") {
3204
3808
  if (swap.claimTxid) return "continue";
3809
+ if (this.callbacks && !this.callbacks.claimOnchain) {
3810
+ this.block(
3811
+ swap,
3812
+ "no claimOnchain callback is wired, so this wallet cannot claim the L1 fill"
3813
+ );
3814
+ return "handled";
3815
+ }
3205
3816
  this.setOnchainState(swap, "claimable");
3206
- if (!this.config.enableAutoActions || !this.callbacks) return "handled";
3817
+ if (!this.config.enableAutoActions || !this.callbacks?.claimOnchain) return "handled";
3207
3818
  try {
3208
3819
  const { txid } = await this.callbacks.claimOnchain(swap, phase.utxo);
3209
3820
  swap.claimTxid = txid;
@@ -3316,6 +3927,24 @@ var RfqSwapManager = class {
3316
3927
  if (swap.state !== "needs_counterparty") return;
3317
3928
  this.setState(swap, traderClaimTxid(swap) ? "claimed" : "pending");
3318
3929
  }
3930
+ /**
3931
+ * Record which ark transactions ended the lockup.
3932
+ *
3933
+ * Only the ones the indexer actually named: `LockupSpend.arkTxid` is
3934
+ * optional, and a checkpoint txid is not what history correlates on — a
3935
+ * record carrying one would name a transaction the wallet's own activity
3936
+ * never shows. Fewer txids is the right failure here.
3937
+ *
3938
+ * Assigned rather than merged: the fate is one read of the whole lockup,
3939
+ * so it is the complete answer for this swap, and a swap only reaches a
3940
+ * verdict once.
3941
+ */
3942
+ stampLockupSpends(swap, spends) {
3943
+ const arkTxids = spends.map((spend) => spend.arkTxid).filter((txid) => txid !== void 0);
3944
+ if (arkTxids.length === 0) return;
3945
+ swap.lockupSpendArkTxids = arkTxids;
3946
+ this.touch(swap);
3947
+ }
3319
3948
  touch(swap) {
3320
3949
  swap.updatedAt = this.config.now();
3321
3950
  this.dirty.add(swap.rfqId);
@@ -3341,9 +3970,29 @@ var RfqSwapManager = class {
3341
3970
  emitAction(swap, action) {
3342
3971
  notify(this.actionExecutedListeners, (listener) => listener(swap, action));
3343
3972
  }
3344
- /** Whether the record is now persisted — false only when `saveSwap` threw. */
3973
+ /**
3974
+ * Flush a changed record to every sink that is wired, and say whether all
3975
+ * of them took it.
3976
+ *
3977
+ * Up to two writes, in this order: the canonical `RfqSwapRecord` to
3978
+ * {@link RfqSwapManagerDeps.repository}, then
3979
+ * {@link RfqSwapManagerCallbacks.saveSwap}. Both gate — a rejection from
3980
+ * either leaves the record dirty and monitored, so waiters stay unsettled
3981
+ * and a terminal swap is not finalized until the write it claims lands.
3982
+ * That is exactly today's rule for `saveSwap`, applied to whichever sinks
3983
+ * exist; wiring the repository does not weaken it.
3984
+ *
3985
+ * The canonical write goes FIRST and a failure there skips the second.
3986
+ * `saveSwap` is a projection of the record, and projecting a state the
3987
+ * record of record has just refused would leave the secondary sink ahead
3988
+ * of the primary — the one ordering that survives no restart.
3989
+ *
3990
+ * With neither wired the state is process-local, which is what a manager
3991
+ * with no callbacks has always done.
3992
+ */
3345
3993
  async save(swap) {
3346
- if (!this.callbacks) return true;
3994
+ if (!await this.saveRecord(swap)) return false;
3995
+ if (!this.callbacks?.saveSwap) return true;
3347
3996
  try {
3348
3997
  await this.callbacks.saveSwap(swap);
3349
3998
  return true;
@@ -3352,6 +4001,25 @@ var RfqSwapManager = class {
3352
4001
  return false;
3353
4002
  }
3354
4003
  }
4004
+ /** The canonical write. True when there is no repository to write to. */
4005
+ async saveRecord(swap) {
4006
+ const repository = this.deps.repository;
4007
+ if (!repository) return true;
4008
+ try {
4009
+ const stored = await repository.getRfqSwap(swap.rfqId);
4010
+ const record = stored ? updateRfqSwapRecord(stored, swap) : createRfqSwapRecord(this.originOrThrow(swap), swap);
4011
+ await repository.saveRfqSwap(record);
4012
+ return true;
4013
+ } catch (error) {
4014
+ this.emitFailed(swap, error);
4015
+ return false;
4016
+ }
4017
+ }
4018
+ originOrThrow(swap) {
4019
+ const origin = this.origins.get(swap.rfqId);
4020
+ if (!origin) throw new RfqSwapOriginRequired(swap.rfqId);
4021
+ return origin;
4022
+ }
3355
4023
  /**
3356
4024
  * Drop a terminal swap from monitoring and report it exactly once.
3357
4025
  *
@@ -3411,6 +4079,8 @@ var errorMessage = (error) => error instanceof Error ? error.message : String(er
3411
4079
  var outpointKey = (vtxo) => `${vtxo.txid}:${vtxo.vout}`;
3412
4080
 
3413
4081
  // src/activity.ts
4082
+ var import_sdk14 = require("@arkade-os/sdk");
4083
+ var import_base14 = require("@scure/base");
3414
4084
  var LABELS = {
3415
4085
  lightning_send: "Lightning send",
3416
4086
  lightning_receive: "Lightning receive",
@@ -3463,6 +4133,44 @@ function swapActivityResolver(deps) {
3463
4133
  }
3464
4134
  };
3465
4135
  }
4136
+ async function rfqSwapActivityInputs(deps) {
4137
+ const records = await deps.repository.getAllRfqSwaps();
4138
+ return Promise.all(records.map((record) => activityInputOf(record, deps.indexer)));
4139
+ }
4140
+ async function activityInputOf(record, indexer) {
4141
+ const txids = /* @__PURE__ */ new Set();
4142
+ if (record.fundingArkTxid) txids.add(record.fundingArkTxid);
4143
+ if (record.refundArkTxid) txids.add(record.refundArkTxid);
4144
+ const handler = rfqCorridorHandlers.getOrThrow(record.kind);
4145
+ for (const txid of handler.activityTxids?.(record.profile) ?? []) txids.add(txid);
4146
+ for (const txid of record.lockupSpendArkTxids ?? []) txids.add(txid);
4147
+ const spendUnknown = isRfqSwapTerminal(record.state) && !record.refundArkTxid && !record.lockupSpendArkTxids?.length;
4148
+ if (indexer && (!record.fundingArkTxid || spendUnknown)) {
4149
+ for (const txid of await lockupTxids(indexer, record, !record.fundingArkTxid)) {
4150
+ txids.add(txid);
4151
+ }
4152
+ }
4153
+ return { rfqId: record.rfqId, kind: record.kind, state: record.state, txids: [...txids] };
4154
+ }
4155
+ async function lockupTxids(indexer, record, wantFunding) {
4156
+ let script;
4157
+ try {
4158
+ script = import_base14.hex.encode(import_sdk14.ArkAddress.decode(record.lockupAddress).pkScript);
4159
+ } catch {
4160
+ return [];
4161
+ }
4162
+ try {
4163
+ const { vtxos } = await indexer.getVtxos({ scripts: [script] });
4164
+ const out = [];
4165
+ for (const vtxo of vtxos ?? []) {
4166
+ if (wantFunding) out.push(vtxo.txid);
4167
+ if (vtxo.arkTxId) out.push(vtxo.arkTxId);
4168
+ }
4169
+ return out;
4170
+ } catch {
4171
+ return [];
4172
+ }
4173
+ }
3466
4174
  // Annotate the CommonJS export names for ESM import in node:
3467
4175
  0 && (module.exports = {
3468
4176
  ARKADE_ASSET,
@@ -3475,6 +4183,7 @@ function swapActivityResolver(deps) {
3475
4183
  LIGHTNING_RECEIVE_PAIR,
3476
4184
  LIGHTNING_SEND_PAIR,
3477
4185
  LockupAmountMismatchError,
4186
+ LockupContractMissing,
3478
4187
  LockupNeedsRecoveryError,
3479
4188
  LockupRegistrationFailed,
3480
4189
  MAX_MIN_CONFIRMATIONS,
@@ -3492,10 +4201,12 @@ function swapActivityResolver(deps) {
3492
4201
  QUOTE_OPTIONS,
3493
4202
  REFUND_MTP_LAG_SECONDS,
3494
4203
  RFQ_RESOLVED_STATES,
4204
+ RFQ_SWAP_RETENTION_SECONDS,
3495
4205
  RFQ_SWAP_TERMINAL_STATES,
3496
4206
  RFQ_TERMINAL_STATES,
3497
4207
  RefundNotLocallyPossibleError,
3498
4208
  RfqSwapManager,
4209
+ RfqSwapOriginRequired,
3499
4210
  SOLO_REFUND_HEADROOM_SECONDS,
3500
4211
  SWAP_LOCKUP_CONTRACT_KIND,
3501
4212
  SWAP_LOCKUP_CONTRACT_LABEL,
@@ -3503,6 +4214,7 @@ function swapActivityResolver(deps) {
3503
4214
  SwapRefusal,
3504
4215
  addAssetSwap,
3505
4216
  arkadeAssetLeg,
4217
+ arkadeRefunder,
3506
4218
  arkadeSwapRequest,
3507
4219
  assertFundable,
3508
4220
  assertReceivable,
@@ -3518,6 +4230,7 @@ function swapActivityResolver(deps) {
3518
4230
  classifyOnchainHtlc,
3519
4231
  classifySpend,
3520
4232
  createOffer,
4233
+ createRfqSwapRecord,
3521
4234
  decodeOffer,
3522
4235
  deriveLightningReceive,
3523
4236
  deriveOnchainReceive,
@@ -3535,6 +4248,7 @@ function swapActivityResolver(deps) {
3535
4248
  lightningReceiveRequest,
3536
4249
  lightningSendRequest,
3537
4250
  lightningSendVtxoScript,
4251
+ lockupContractParams,
3538
4252
  makeCachedFeedFetch,
3539
4253
  newPreimage,
3540
4254
  newRfqId,
@@ -3543,12 +4257,14 @@ function swapActivityResolver(deps) {
3543
4257
  offerVtxoScript,
3544
4258
  onchainHtlcScript,
3545
4259
  onchainReceiveRequest,
4260
+ onchainSendProfile,
3546
4261
  onchainSendRequest,
3547
4262
  paymentHashOf,
3548
4263
  preimageForSwapRecord,
3549
4264
  pushClaim,
3550
4265
  pushRefundWithoutReceiver,
3551
4266
  readLockupFate,
4267
+ rebuildRfqSwap,
3552
4268
  receiveVtxoScript,
3553
4269
  refundIfUnresolved,
3554
4270
  registerLockupContract,
@@ -3559,9 +4275,15 @@ function swapActivityResolver(deps) {
3559
4275
  requestOnchainSend,
3560
4276
  restoreAssetSwaps,
3561
4277
  retireSettledOfferContracts,
4278
+ rfqClaimSecretOf,
3562
4279
  rfqPair,
4280
+ rfqSecretsProfile,
4281
+ rfqSignerOf,
4282
+ rfqSwapActivityInputs,
4283
+ rfqSwapOriginOf,
3563
4284
  sealClaimPacket,
3564
4285
  senderIdentityForSwapRecord,
4286
+ shouldRetainRfqSwap,
3565
4287
  spendTxidsOf,
3566
4288
  spendUpdate,
3567
4289
  swapActivityResolver,
@@ -3572,6 +4294,7 @@ function swapActivityResolver(deps) {
3572
4294
  unilateralRefundWithoutReceiverDelay,
3573
4295
  updateAssetSwap,
3574
4296
  updateAssetSwapBestEffort,
4297
+ updateRfqSwapRecord,
3575
4298
  validatePlan,
3576
4299
  verifyLockupAddress,
3577
4300
  verifyReceiveInvoice,