@arkade-os/swap 0.0.6 → 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,16 +58,20 @@ __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,
67
70
  SWAP_LOCKUP_CONTRACT_TYPE: () => SWAP_LOCKUP_CONTRACT_TYPE,
68
71
  SwapRefusal: () => SwapRefusal,
69
72
  addAssetSwap: () => addAssetSwap,
73
+ arkadeAssetLeg: () => arkadeAssetLeg,
74
+ arkadeRefunder: () => arkadeRefunder,
70
75
  arkadeSwapRequest: () => arkadeSwapRequest,
71
76
  assertFundable: () => assertFundable,
72
77
  assertReceivable: () => assertReceivable,
@@ -82,6 +87,7 @@ __export(index_exports, {
82
87
  classifyOnchainHtlc: () => classifyOnchainHtlc,
83
88
  classifySpend: () => classifySpend,
84
89
  createOffer: () => createOffer,
90
+ createRfqSwapRecord: () => createRfqSwapRecord,
85
91
  decodeOffer: () => decodeOffer,
86
92
  deriveLightningReceive: () => deriveLightningReceive,
87
93
  deriveOnchainReceive: () => deriveOnchainReceive,
@@ -99,6 +105,7 @@ __export(index_exports, {
99
105
  lightningReceiveRequest: () => lightningReceiveRequest,
100
106
  lightningSendRequest: () => lightningSendRequest,
101
107
  lightningSendVtxoScript: () => lightningSendVtxoScript,
108
+ lockupContractParams: () => lockupContractParams,
102
109
  makeCachedFeedFetch: () => makeCachedFeedFetch,
103
110
  newPreimage: () => newPreimage,
104
111
  newRfqId: () => newRfqId,
@@ -107,12 +114,14 @@ __export(index_exports, {
107
114
  offerVtxoScript: () => offerVtxoScript,
108
115
  onchainHtlcScript: () => onchainHtlcScript,
109
116
  onchainReceiveRequest: () => onchainReceiveRequest,
117
+ onchainSendProfile: () => onchainSendProfile,
110
118
  onchainSendRequest: () => onchainSendRequest,
111
119
  paymentHashOf: () => paymentHashOf,
112
120
  preimageForSwapRecord: () => preimageForSwapRecord,
113
121
  pushClaim: () => pushClaim,
114
122
  pushRefundWithoutReceiver: () => pushRefundWithoutReceiver,
115
123
  readLockupFate: () => readLockupFate,
124
+ rebuildRfqSwap: () => rebuildRfqSwap,
116
125
  receiveVtxoScript: () => receiveVtxoScript,
117
126
  refundIfUnresolved: () => refundIfUnresolved,
118
127
  registerLockupContract: () => registerLockupContract,
@@ -123,9 +132,15 @@ __export(index_exports, {
123
132
  requestOnchainSend: () => requestOnchainSend,
124
133
  restoreAssetSwaps: () => restoreAssetSwaps,
125
134
  retireSettledOfferContracts: () => retireSettledOfferContracts,
135
+ rfqClaimSecretOf: () => rfqClaimSecretOf,
126
136
  rfqPair: () => rfqPair,
137
+ rfqSecretsProfile: () => rfqSecretsProfile,
138
+ rfqSignerOf: () => rfqSignerOf,
139
+ rfqSwapActivityInputs: () => rfqSwapActivityInputs,
140
+ rfqSwapOriginOf: () => rfqSwapOriginOf,
127
141
  sealClaimPacket: () => sealClaimPacket,
128
142
  senderIdentityForSwapRecord: () => senderIdentityForSwapRecord,
143
+ shouldRetainRfqSwap: () => shouldRetainRfqSwap,
129
144
  spendTxidsOf: () => spendTxidsOf,
130
145
  spendUpdate: () => spendUpdate,
131
146
  swapActivityResolver: () => swapActivityResolver,
@@ -136,6 +151,7 @@ __export(index_exports, {
136
151
  unilateralRefundWithoutReceiverDelay: () => unilateralRefundWithoutReceiverDelay,
137
152
  updateAssetSwap: () => updateAssetSwap,
138
153
  updateAssetSwapBestEffort: () => updateAssetSwapBestEffort,
154
+ updateRfqSwapRecord: () => updateRfqSwapRecord,
139
155
  validatePlan: () => validatePlan,
140
156
  verifyLockupAddress: () => verifyLockupAddress,
141
157
  verifyReceiveInvoice: () => verifyReceiveInvoice,
@@ -761,8 +777,9 @@ var validatePlan = (plan, giveBalance, dust) => {
761
777
  // src/repository.ts
762
778
  var marketsCacheKey = (network, registry) => `arkade-intents-markets-${network}-${registry}`;
763
779
  var InMemoryAssetSwapRepository = class {
764
- version = 2;
780
+ version = 4;
765
781
  swaps = /* @__PURE__ */ new Map();
782
+ rfqSwaps = /* @__PURE__ */ new Map();
766
783
  scanned = /* @__PURE__ */ new Set();
767
784
  markets = /* @__PURE__ */ new Map();
768
785
  async saveSwap(swap) {
@@ -771,6 +788,18 @@ var InMemoryAssetSwapRepository = class {
771
788
  async getAllSwaps() {
772
789
  return [...this.swaps.values()];
773
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
+ }
774
803
  async getScannedTxids() {
775
804
  return new Set(this.scanned);
776
805
  }
@@ -785,6 +814,7 @@ var InMemoryAssetSwapRepository = class {
785
814
  }
786
815
  async clear() {
787
816
  this.swaps.clear();
817
+ this.rfqSwaps.clear();
788
818
  this.scanned.clear();
789
819
  this.markets.clear();
790
820
  }
@@ -795,58 +825,44 @@ var InMemoryAssetSwapRepository = class {
795
825
  // src/indexedDbRepository.ts
796
826
  var import_sdk4 = require("@arkade-os/sdk");
797
827
  var DEFAULT_DB_NAME = "arkade-intents";
798
- var DB_VERSION = 1;
828
+ var DB_VERSION = 2;
799
829
  var STORE_SWAPS = "swaps";
830
+ var STORE_RFQ_SWAPS = "rfqSwaps";
800
831
  var STORE_SCANNED = "scannedTxids";
801
832
  var STORE_MARKETS = "markets";
802
833
  var STORES = [
803
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" }],
804
838
  [STORE_SCANNED],
805
839
  [STORE_MARKETS]
806
840
  ];
807
- function initDatabase(db) {
841
+ function initDatabase(db, oldVersion, transaction) {
842
+ void oldVersion;
843
+ void transaction;
808
844
  for (const [name, options] of STORES) {
809
845
  if (!db.objectStoreNames.contains(name)) db.createObjectStore(name, options);
810
846
  }
811
847
  }
812
- var request = (req) => new Promise((resolve, reject) => {
813
- req.onsuccess = () => resolve(req.result);
814
- req.onerror = () => reject(req.error);
815
- });
816
- var txDone = (tx) => new Promise((resolve, reject) => {
817
- tx.oncomplete = () => resolve();
818
- tx.onerror = () => reject(tx.error);
819
- tx.onabort = () => reject(tx.error);
820
- });
821
848
  var IndexedDbAssetSwapRepository = class {
849
+ version = 4;
850
+ connection;
822
851
  constructor(dbName = DEFAULT_DB_NAME) {
823
- this.dbName = dbName;
824
- }
825
- dbName;
826
- version = 2;
827
- // the promise, not the resolved database: openDatabase bumps a refcount on
828
- // every call including cache hits, while dispose closes once, so two
829
- // concurrent first calls would strand the refcount above zero and leak the
830
- // connection for the process lifetime. Cleared on failure so a failed open
831
- // can be retried rather than cached forever.
832
- dbPromise = null;
852
+ this.connection = (0, import_sdk4.createManagedConnection)(dbName, DB_VERSION, initDatabase);
853
+ }
833
854
  ensureDb() {
834
- return this.dbPromise ??= (0, import_sdk4.openDatabase)(this.dbName, DB_VERSION, initDatabase).catch(
835
- (err) => {
836
- this.dbPromise = null;
837
- throw err;
838
- }
839
- );
855
+ return this.connection.get();
840
856
  }
841
857
  async readStore(name) {
842
858
  return (await this.ensureDb()).transaction([name], "readonly").objectStore(name);
843
859
  }
844
860
  /** Every write in one place, so none of them can forget to await the
845
861
  * commit. Requests need no individual await: a failed one aborts the
846
- * transaction, which `txDone` reports. */
862
+ * transaction, which `awaitTransaction` reports. */
847
863
  async write(name, apply) {
848
864
  const tx = (await this.ensureDb()).transaction([name], "readwrite");
849
- const done = txDone(tx);
865
+ const done = (0, import_sdk4.awaitTransaction)(tx);
850
866
  apply(tx.objectStore(name));
851
867
  await done;
852
868
  }
@@ -856,10 +872,26 @@ var IndexedDbAssetSwapRepository = class {
856
872
  });
857
873
  }
858
874
  async getAllSwaps() {
859
- 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
+ });
860
892
  }
861
893
  async getScannedTxids() {
862
- const keys = await request((await this.readStore(STORE_SCANNED)).getAllKeys());
894
+ const keys = await (0, import_sdk4.promisifyRequest)((await this.readStore(STORE_SCANNED)).getAllKeys());
863
895
  return new Set(keys);
864
896
  }
865
897
  async markTxidsScanned(txids) {
@@ -869,33 +901,578 @@ var IndexedDbAssetSwapRepository = class {
869
901
  }
870
902
  async getCachedMarkets(network, registry) {
871
903
  const store = await this.readStore(STORE_MARKETS);
872
- return request(store.get(marketsCacheKey(network, registry)));
904
+ return (0, import_sdk4.promisifyRequest)(store.get(marketsCacheKey(network, registry)));
873
905
  }
874
906
  async saveCachedMarkets(network, registry, entry) {
875
907
  await this.write(STORE_MARKETS, (store) => {
876
908
  store.put(entry, marketsCacheKey(network, registry));
877
909
  });
878
910
  }
879
- /** All stores in one transaction: clearing swaps but keeping scanned txids
880
- * would leave the restore scan permanently skipping those funding txs, so
881
- * a partial clear must not be observable. */
882
- async clear() {
883
- const stores = STORES.map(([name]) => name);
884
- const tx = (await this.ensureDb()).transaction(stores, "readwrite");
885
- const done = txDone(tx);
886
- for (const name of stores) tx.objectStore(name).clear();
887
- await done;
911
+ /** All stores in one transaction: clearing swaps but keeping scanned txids
912
+ * would leave the restore scan permanently skipping those funding txs, so
913
+ * a partial clear must not be observable. */
914
+ async clear() {
915
+ const stores = STORES.map(([name]) => name);
916
+ const tx = (await this.ensureDb()).transaction(stores, "readwrite");
917
+ const done = (0, import_sdk4.awaitTransaction)(tx);
918
+ for (const name of stores) tx.objectStore(name).clear();
919
+ await done;
920
+ }
921
+ async [Symbol.asyncDispose]() {
922
+ await this.connection[Symbol.asyncDispose]();
923
+ }
924
+ };
925
+
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
+ );
937
+ }
938
+ this.handlers.set(handler.kind, handler);
939
+ }
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);
946
+ }
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
+ );
960
+ }
961
+ return handler;
962
+ }
963
+ has(kind) {
964
+ return this.handlers.has(kind);
965
+ }
966
+ registeredKinds() {
967
+ return [...this.handlers.keys()];
968
+ }
969
+ /** Test seam, mirroring the contract registry's. */
970
+ unregister(kind) {
971
+ return this.handlers.delete(kind);
972
+ }
973
+ };
974
+ var rfqCorridorHandlers = new RfqCorridorRegistry();
975
+
976
+ // src/rfqProfileParts.ts
977
+ var rfqSecretsProfile = (secrets, paymentHash) => {
978
+ const { signingDescriptor, ...preimage } = swapSecretsToRecord(secrets);
979
+ return {
980
+ signer: { signingDescriptor },
981
+ ...paymentHash ? { hashlock: { paymentHash, ...preimage } } : {}
982
+ };
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")
1003
+ };
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
+ };
1044
+
1045
+ // src/onchainHtlc.ts
1046
+ var import_base3 = require("@scure/base");
1047
+ var import_sha22 = require("@noble/hashes/sha2.js");
1048
+ var import_legacy = require("@noble/hashes/legacy.js");
1049
+ var btc = __toESM(require("@scure/btc-signer"), 1);
1050
+ var ONCHAIN_ORDER_MARGIN_SECONDS = 2 * 60 * 60;
1051
+ var ONCHAIN_CLAIM_MARGIN_SECONDS = 90 * 60;
1052
+ var MAX_MIN_CONFIRMATIONS = 6;
1053
+ var ONCHAIN_SECONDS_PER_BLOCK = 600;
1054
+ var ONCHAIN_DUST_SATS = BigInt(330);
1055
+ var newPreimage = () => crypto.getRandomValues(new Uint8Array(32));
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));
1058
+ var L1_NETWORKS = {
1059
+ bitcoin: btc.NETWORK,
1060
+ testnet: btc.TEST_NETWORK,
1061
+ regtest: { ...btc.TEST_NETWORK, bech32: "bcrt" }
1062
+ };
1063
+ function onchainHtlcScript(params, network) {
1064
+ if (params.claimKey.length !== 32 || params.refundKey.length !== 32) {
1065
+ throw new Error("claimKey and refundKey must be 32-byte x-only keys");
1066
+ }
1067
+ if (!Number.isInteger(params.refundLocktime) || params.refundLocktime <= 0) {
1068
+ throw new Error(
1069
+ `refundLocktime must be a positive unix timestamp, got ${params.refundLocktime}`
1070
+ );
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
+ }
1077
+ const h160 = h160FromPaymentHash(params.paymentHash);
1078
+ const claim = btc.Script.encode([
1079
+ "SIZE",
1080
+ 32,
1081
+ "EQUALVERIFY",
1082
+ "HASH160",
1083
+ h160,
1084
+ "EQUALVERIFY",
1085
+ params.claimKey,
1086
+ "CHECKSIG"
1087
+ ]);
1088
+ const refund = btc.Script.encode([
1089
+ btc.ScriptNum().encode(BigInt(params.refundLocktime)),
1090
+ "CHECKLOCKTIMEVERIFY",
1091
+ "DROP",
1092
+ params.refundKey,
1093
+ "CHECKSIG"
1094
+ ]);
1095
+ const payment = btc.p2tr(
1096
+ btc.TAPROOT_UNSPENDABLE_KEY,
1097
+ btc.taprootListToTree([{ script: claim }, { script: refund }]),
1098
+ L1_NETWORKS[network],
1099
+ true
1100
+ );
1101
+ const controlBlockFor = (leaf) => {
1102
+ for (const [block, script] of payment.tapLeafScript ?? []) {
1103
+ if (script.length - 1 === leaf.length && import_base3.hex.encode(script.subarray(0, leaf.length)) === import_base3.hex.encode(leaf)) {
1104
+ return btc.TaprootControlBlock.encode(block);
1105
+ }
1106
+ }
1107
+ throw new Error("leaf missing from compiled taproot tree");
1108
+ };
1109
+ return {
1110
+ address: payment.address,
1111
+ pkScript: payment.script,
1112
+ leaves: { claim, refund },
1113
+ controlBlocks: { claim: controlBlockFor(claim), refund: controlBlockFor(refund) },
1114
+ paymentHash: params.paymentHash,
1115
+ refundLocktime: params.refundLocktime
1116
+ };
1117
+ }
1118
+ var buildLeafSpend = async (input) => {
1119
+ if (!Number.isFinite(input.feeRateSatVb) || input.feeRateSatVb <= 0) {
1120
+ throw new Error(`feeRateSatVb must be positive, got ${input.feeRateSatVb}`);
1121
+ }
1122
+ const assemble = (payout2) => {
1123
+ const tx2 = new btc.Transaction({ lockTime: input.lockTime ?? 0 });
1124
+ tx2.addInput({
1125
+ txid: input.utxo.txid,
1126
+ index: input.utxo.vout,
1127
+ witnessUtxo: { script: input.htlc.pkScript, amount: input.utxo.amount },
1128
+ sequence: input.sequence
1129
+ });
1130
+ tx2.addOutput({ script: input.payoutPkScript, amount: payout2 });
1131
+ return tx2;
1132
+ };
1133
+ const witness = (sig2) => [
1134
+ sig2,
1135
+ ...input.stackAboveSig,
1136
+ input.leaf,
1137
+ input.controlBlock
1138
+ ];
1139
+ const sizing = assemble(input.utxo.amount);
1140
+ sizing.updateInput(0, { finalScriptWitness: witness(new Uint8Array(64)) }, true);
1141
+ const fee = BigInt(Math.ceil(sizing.vsize * input.feeRateSatVb));
1142
+ const payout = input.utxo.amount - fee;
1143
+ if (payout < ONCHAIN_DUST_SATS) {
1144
+ throw new Error(
1145
+ `fee ${fee} leaves ${payout} sats from a ${input.utxo.amount} sat HTLC \u2014 below the ${ONCHAIN_DUST_SATS} sat dust limit`
1146
+ );
1147
+ }
1148
+ const tx = assemble(payout);
1149
+ const sighash = tx.preimageWitnessV1(
1150
+ 0,
1151
+ [input.htlc.pkScript],
1152
+ btc.SigHash.DEFAULT,
1153
+ [input.utxo.amount],
1154
+ void 0,
1155
+ input.leaf,
1156
+ 192
1157
+ );
1158
+ const sig = await input.sign(sighash);
1159
+ if (sig.length !== 64)
1160
+ throw new Error(`sign() must return a 64-byte BIP340 signature, got ${sig.length}`);
1161
+ tx.updateInput(0, { finalScriptWitness: witness(sig) }, true);
1162
+ return { txHex: tx.hex, txid: tx.id, payoutAmount: payout };
1163
+ };
1164
+ var buildHtlcClaim = async (input) => {
1165
+ if (paymentHashOf(input.preimage) !== input.htlc.paymentHash) {
1166
+ throw new Error("preimage does not hash to the HTLC's payment hash");
1167
+ }
1168
+ return buildLeafSpend({
1169
+ htlc: input.htlc,
1170
+ utxo: input.utxo,
1171
+ leaf: input.htlc.leaves.claim,
1172
+ controlBlock: input.htlc.controlBlocks.claim,
1173
+ stackAboveSig: [input.preimage],
1174
+ payoutPkScript: input.payoutPkScript,
1175
+ feeRateSatVb: input.feeRateSatVb,
1176
+ sign: input.sign,
1177
+ sequence: 4294967293
1178
+ });
1179
+ };
1180
+ var buildHtlcRefund = (input) => buildLeafSpend({
1181
+ htlc: input.htlc,
1182
+ utxo: input.utxo,
1183
+ leaf: input.htlc.leaves.refund,
1184
+ controlBlock: input.htlc.controlBlocks.refund,
1185
+ stackAboveSig: [],
1186
+ payoutPkScript: input.payoutPkScript,
1187
+ feeRateSatVb: input.feeRateSatVb,
1188
+ sign: input.sign,
1189
+ lockTime: input.htlc.refundLocktime,
1190
+ // any value below 0xffffffff enables nLockTime enforcement
1191
+ sequence: 4294967294
1192
+ });
1193
+ function extractPreimage(txHex, paymentHash) {
1194
+ let raw;
1195
+ try {
1196
+ raw = btc.RawTx.decode(import_base3.hex.decode(txHex));
1197
+ } catch {
1198
+ return null;
1199
+ }
1200
+ for (const stack of raw.witnesses ?? []) {
1201
+ for (const item of stack) {
1202
+ if (item.length === 32 && import_base3.hex.encode((0, import_sha22.sha256)(item)) === paymentHash) return item;
1203
+ }
1204
+ }
1205
+ return null;
1206
+ }
1207
+ var sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
1208
+ async function awaitOnchainFill(chain, htlc, minConfirmations, options = {}) {
1209
+ const pollMs = options.pollMs ?? 5e3;
1210
+ for (; ; ) {
1211
+ const utxos = await chain.getScriptUtxos(htlc.pkScript);
1212
+ const eligible = utxos.filter((u) => u.confirmations >= minConfirmations).sort((a, b) => b.amount > a.amount ? 1 : -1);
1213
+ if (eligible[0]) return eligible[0];
1214
+ if (options.deadline !== void 0 && Date.now() / 1e3 >= options.deadline) {
1215
+ const error = new Error("HTLC was not filled before the deadline");
1216
+ error.reason = "fill_timeout";
1217
+ throw error;
1218
+ }
1219
+ await sleep(pollMs);
1220
+ }
1221
+ }
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
+ );
888
1396
  }
889
- async [Symbol.asyncDispose]() {
890
- if (!this.dbPromise) return;
891
- await (0, import_sdk4.closeDatabase)(this.dbName);
892
- this.dbPromise = null;
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
+ );
893
1441
  }
894
- };
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
+ }
895
1472
 
896
1473
  // src/restore.ts
897
- var import_base3 = require("@scure/base");
898
- var import_sdk5 = require("@arkade-os/sdk");
1474
+ var import_base6 = require("@scure/base");
1475
+ var import_sdk6 = require("@arkade-os/sdk");
899
1476
  var TXS_PER_REQUEST = 50;
900
1477
  async function fetchParsedTxs(indexer, txids) {
901
1478
  const parsedByTxid = /* @__PURE__ */ new Map();
@@ -911,7 +1488,7 @@ async function fetchParsedTxs(indexer, txids) {
911
1488
  if (result.status !== "fulfilled") continue;
912
1489
  for (const psbt of result.value) {
913
1490
  try {
914
- const parsed = import_sdk5.Transaction.fromPSBT(import_base3.base64.decode(psbt));
1491
+ const parsed = import_sdk6.Transaction.fromPSBT(import_base6.base64.decode(psbt));
915
1492
  parsedByTxid.set(parsed.id, parsed);
916
1493
  } catch {
917
1494
  }
@@ -926,7 +1503,7 @@ function classifySpend(offer, serverPubkey, spendTx, deposit) {
926
1503
  let leaves;
927
1504
  try {
928
1505
  const script = offerVtxoScript(offer, serverPubkey);
929
- if (import_base3.hex.encode(script.pkScript) !== import_base3.hex.encode(offer.swapPkScript)) return "indeterminate";
1506
+ if (import_base6.hex.encode(script.pkScript) !== import_base6.hex.encode(offer.swapPkScript)) return "indeterminate";
930
1507
  leaves = {
931
1508
  cancel: script.functionByName("cancel")?.leafScript,
932
1509
  fulfill: script.functionByName("fulfill")?.leafScript
@@ -937,11 +1514,11 @@ function classifySpend(offer, serverPubkey, spendTx, deposit) {
937
1514
  for (let i = 0; i < spendTx.inputsLength; i++) {
938
1515
  const input = spendTx.getInput(i);
939
1516
  if (!input.txid || input.index !== deposit.vout) continue;
940
- if (import_base3.hex.encode(input.txid) !== deposit.txid) continue;
1517
+ if (import_base6.hex.encode(input.txid) !== deposit.txid) continue;
941
1518
  for (const leaf of input.tapLeafScript ?? []) {
942
- const spent = import_base3.hex.encode((0, import_sdk5.scriptFromTapLeafScript)(leaf));
943
- if (leaves.cancel && spent === import_base3.hex.encode(leaves.cancel)) return "cancelled";
944
- if (leaves.fulfill && spent === import_base3.hex.encode(leaves.fulfill)) return "fulfilled";
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";
945
1522
  }
946
1523
  }
947
1524
  return "indeterminate";
@@ -970,19 +1547,19 @@ async function restoreAssetSwaps(indexer, txs, existingIds, opts) {
970
1547
  if (!fundingTx) continue;
971
1548
  fetchedTxids.push(txid);
972
1549
  try {
973
- const packet = import_sdk5.Extension.fromTx(parsed).getPacketByType(OFFER_PACKET_TYPE);
1550
+ const packet = import_sdk6.Extension.fromTx(parsed).getPacketByType(OFFER_PACKET_TYPE);
974
1551
  if (!packet) continue;
975
1552
  const payload = packet.serialize();
976
1553
  found.push({
977
1554
  fundingTx,
978
1555
  offer: decodeOffer(payload),
979
- offerHex: import_base3.hex.encode(payload)
1556
+ offerHex: import_base6.hex.encode(payload)
980
1557
  });
981
1558
  } catch {
982
1559
  }
983
1560
  }
984
1561
  if (found.length === 0) return { restored: [], scannedTxids: fetchedTxids };
985
- const scripts = [...new Set(found.map((f) => import_base3.hex.encode(f.offer.swapPkScript)))];
1562
+ const scripts = [...new Set(found.map((f) => import_base6.hex.encode(f.offer.swapPkScript)))];
986
1563
  const { vtxos } = await indexer.getVtxos({ scripts });
987
1564
  const vtxoByScriptAndTxid = new Map(vtxos.map((v) => [`${v.script}:${v.txid}`, v]));
988
1565
  const txByAnyId = /* @__PURE__ */ new Map();
@@ -994,7 +1571,7 @@ async function restoreAssetSwaps(indexer, txs, existingIds, opts) {
994
1571
  const spendTxids = /* @__PURE__ */ new Set();
995
1572
  for (const { fundingTx, offer } of found) {
996
1573
  const vtxo = vtxoByScriptAndTxid.get(
997
- `${import_base3.hex.encode(offer.swapPkScript)}:${fundingTx.redeemTxid}`
1574
+ `${import_base6.hex.encode(offer.swapPkScript)}:${fundingTx.redeemTxid}`
998
1575
  );
999
1576
  if (vtxo?.virtualStatus.state !== "spent") continue;
1000
1577
  for (const txid of spendTxidsOf(vtxo)) spendTxids.add(txid);
@@ -1003,7 +1580,7 @@ async function restoreAssetSwaps(indexer, txs, existingIds, opts) {
1003
1580
  const restored = [];
1004
1581
  const unresolved = /* @__PURE__ */ new Set();
1005
1582
  for (const { fundingTx, offer, offerHex } of found) {
1006
- const swapPkScript = import_base3.hex.encode(offer.swapPkScript);
1583
+ const swapPkScript = import_base6.hex.encode(offer.swapPkScript);
1007
1584
  const vtxo = vtxoByScriptAndTxid.get(`${swapPkScript}:${fundingTx.redeemTxid}`);
1008
1585
  if (!vtxo) {
1009
1586
  unresolved.add(fundingTx.redeemTxid);
@@ -1060,8 +1637,8 @@ async function restoreAssetSwaps(indexer, txs, existingIds, opts) {
1060
1637
  }
1061
1638
 
1062
1639
  // src/watch.ts
1063
- var import_base4 = require("@scure/base");
1064
- var import_sdk6 = require("@arkade-os/sdk");
1640
+ var import_base7 = require("@scure/base");
1641
+ var import_sdk7 = require("@arkade-os/sdk");
1065
1642
  var TERMINAL = ["fulfilled", "cancelled", "recoverable"];
1066
1643
  function spendUpdate(swap, spend) {
1067
1644
  if (TERMINAL.includes(swap.status)) return void 0;
@@ -1081,8 +1658,8 @@ async function watchOfferSwaps({
1081
1658
  onUpdate
1082
1659
  }) {
1083
1660
  const manager = await wallet.getContractManager();
1084
- const serverPubkey = import_sdk6.ArkAddress.decode(await wallet.getAddress()).serverPubKey;
1085
- const indexer = new import_sdk6.RestIndexerProvider(arkServerUrl);
1661
+ const serverPubkey = import_sdk7.ArkAddress.decode(await wallet.getAddress()).serverPubKey;
1662
+ const indexer = new import_sdk7.RestIndexerProvider(arkServerUrl);
1086
1663
  let queue = Promise.resolve();
1087
1664
  const enqueue = (task) => {
1088
1665
  queue = queue.then(task).catch(() => {
@@ -1090,270 +1667,62 @@ async function watchOfferSwaps({
1090
1667
  };
1091
1668
  const classify = async (swap, vtxo, spentTxid) => {
1092
1669
  if (swap.spentTxid === spentTxid && swap.status === "cancelling") return "cancelled";
1093
- try {
1094
- const candidates = spendTxidsOf(vtxo);
1095
- if (candidates.length === 0) return "indeterminate";
1096
- const { txs } = await indexer.getVirtualTxs(candidates);
1097
- return classifyDepositSpend(
1098
- decodeOffer(import_base4.hex.decode(swap.offerHex)),
1099
- serverPubkey,
1100
- txs.map((psbt) => import_sdk6.Transaction.fromPSBT(import_base4.base64.decode(psbt))),
1101
- { txid: vtxo.txid, vout: vtxo.vout }
1102
- );
1103
- } catch {
1104
- return "indeterminate";
1105
- }
1106
- };
1107
- const handleSpend = async (event) => {
1108
- if (event.contract.metadata?.kind !== OFFER_CONTRACT_KIND) return;
1109
- for (const vtxo of event.vtxos) {
1110
- const spentTxid = vtxo.arkTxId || vtxo.spentBy;
1111
- if (!spentTxid) continue;
1112
- const swap = (await getAssetSwaps(repository)).find(
1113
- (s) => s.fundingTxid === vtxo.txid && s.swapPkScript === event.contractScript
1114
- );
1115
- if (!swap) continue;
1116
- const kind = await classify(swap, vtxo, spentTxid);
1117
- const changes = spendUpdate(swap, { txid: spentTxid, kind, at: event.timestamp });
1118
- if (!changes) continue;
1119
- const { persisted, swaps } = await updateAssetSwapBestEffort(
1120
- repository,
1121
- swap.id,
1122
- changes
1123
- );
1124
- if (!persisted) continue;
1125
- onUpdate?.({ ...swap, ...changes });
1126
- if (changes.status && RETIRABLE.includes(changes.status)) {
1127
- await retireOfferContract(manager, swaps, event.contractScript);
1128
- }
1129
- }
1130
- };
1131
- const unsubscribe = manager.onContractEvent((event) => {
1132
- if (event.type !== "vtxo_spent") return;
1133
- enqueue(() => handleSpend(event));
1134
- });
1135
- return {
1136
- stop: unsubscribe,
1137
- idle: () => queue
1138
- };
1139
- }
1140
-
1141
- // src/rfq.ts
1142
- var import_base8 = require("@scure/base");
1143
- var import_legacy2 = require("@noble/hashes/legacy.js");
1144
- var import_sdk8 = require("@arkade-os/sdk");
1145
-
1146
- // src/onchainHtlc.ts
1147
- var import_base5 = require("@scure/base");
1148
- var import_sha22 = require("@noble/hashes/sha2.js");
1149
- var import_legacy = require("@noble/hashes/legacy.js");
1150
- var btc = __toESM(require("@scure/btc-signer"), 1);
1151
- var ONCHAIN_ORDER_MARGIN_SECONDS = 2 * 60 * 60;
1152
- var ONCHAIN_CLAIM_MARGIN_SECONDS = 90 * 60;
1153
- var MAX_MIN_CONFIRMATIONS = 6;
1154
- var ONCHAIN_SECONDS_PER_BLOCK = 600;
1155
- var ONCHAIN_DUST_SATS = BigInt(330);
1156
- var newPreimage = () => crypto.getRandomValues(new Uint8Array(32));
1157
- var paymentHashOf = (preimage) => import_base5.hex.encode((0, import_sha22.sha256)(preimage));
1158
- var h160FromPaymentHash = (paymentHash) => (0, import_legacy.ripemd160)(import_base5.hex.decode(paymentHash));
1159
- var L1_NETWORKS = {
1160
- bitcoin: btc.NETWORK,
1161
- testnet: btc.TEST_NETWORK,
1162
- regtest: { ...btc.TEST_NETWORK, bech32: "bcrt" }
1163
- };
1164
- function onchainHtlcScript(params, network) {
1165
- if (params.claimKey.length !== 32 || params.refundKey.length !== 32) {
1166
- throw new Error("claimKey and refundKey must be 32-byte x-only keys");
1167
- }
1168
- if (!Number.isInteger(params.refundLocktime) || params.refundLocktime <= 0) {
1169
- throw new Error(
1170
- `refundLocktime must be a positive unix timestamp, got ${params.refundLocktime}`
1171
- );
1172
- }
1173
- const h160 = h160FromPaymentHash(params.paymentHash);
1174
- const claim = btc.Script.encode([
1175
- "SIZE",
1176
- 32,
1177
- "EQUALVERIFY",
1178
- "HASH160",
1179
- h160,
1180
- "EQUALVERIFY",
1181
- params.claimKey,
1182
- "CHECKSIG"
1183
- ]);
1184
- const refund = btc.Script.encode([
1185
- btc.ScriptNum().encode(BigInt(params.refundLocktime)),
1186
- "CHECKLOCKTIMEVERIFY",
1187
- "DROP",
1188
- params.refundKey,
1189
- "CHECKSIG"
1190
- ]);
1191
- const payment = btc.p2tr(
1192
- btc.TAPROOT_UNSPENDABLE_KEY,
1193
- btc.taprootListToTree([{ script: claim }, { script: refund }]),
1194
- L1_NETWORKS[network],
1195
- true
1196
- );
1197
- const controlBlockFor = (leaf) => {
1198
- for (const [block, script] of payment.tapLeafScript ?? []) {
1199
- if (script.length - 1 === leaf.length && import_base5.hex.encode(script.subarray(0, leaf.length)) === import_base5.hex.encode(leaf)) {
1200
- return btc.TaprootControlBlock.encode(block);
1201
- }
1202
- }
1203
- throw new Error("leaf missing from compiled taproot tree");
1204
- };
1205
- return {
1206
- address: payment.address,
1207
- pkScript: payment.script,
1208
- leaves: { claim, refund },
1209
- controlBlocks: { claim: controlBlockFor(claim), refund: controlBlockFor(refund) },
1210
- paymentHash: params.paymentHash,
1211
- refundLocktime: params.refundLocktime
1212
- };
1213
- }
1214
- var buildLeafSpend = async (input) => {
1215
- if (!Number.isFinite(input.feeRateSatVb) || input.feeRateSatVb <= 0) {
1216
- throw new Error(`feeRateSatVb must be positive, got ${input.feeRateSatVb}`);
1217
- }
1218
- const assemble = (payout2) => {
1219
- const tx2 = new btc.Transaction({ lockTime: input.lockTime ?? 0 });
1220
- tx2.addInput({
1221
- txid: input.utxo.txid,
1222
- index: input.utxo.vout,
1223
- witnessUtxo: { script: input.htlc.pkScript, amount: input.utxo.amount },
1224
- sequence: input.sequence
1225
- });
1226
- tx2.addOutput({ script: input.payoutPkScript, amount: payout2 });
1227
- return tx2;
1228
- };
1229
- const witness = (sig2) => [
1230
- sig2,
1231
- ...input.stackAboveSig,
1232
- input.leaf,
1233
- input.controlBlock
1234
- ];
1235
- const sizing = assemble(input.utxo.amount);
1236
- sizing.updateInput(0, { finalScriptWitness: witness(new Uint8Array(64)) }, true);
1237
- const fee = BigInt(Math.ceil(sizing.vsize * input.feeRateSatVb));
1238
- const payout = input.utxo.amount - fee;
1239
- if (payout < ONCHAIN_DUST_SATS) {
1240
- throw new Error(
1241
- `fee ${fee} leaves ${payout} sats from a ${input.utxo.amount} sat HTLC \u2014 below the ${ONCHAIN_DUST_SATS} sat dust limit`
1242
- );
1243
- }
1244
- const tx = assemble(payout);
1245
- const sighash = tx.preimageWitnessV1(
1246
- 0,
1247
- [input.htlc.pkScript],
1248
- btc.SigHash.DEFAULT,
1249
- [input.utxo.amount],
1250
- void 0,
1251
- input.leaf,
1252
- 192
1253
- );
1254
- const sig = await input.sign(sighash);
1255
- if (sig.length !== 64)
1256
- throw new Error(`sign() must return a 64-byte BIP340 signature, got ${sig.length}`);
1257
- tx.updateInput(0, { finalScriptWitness: witness(sig) }, true);
1258
- return { txHex: tx.hex, txid: tx.id, payoutAmount: payout };
1259
- };
1260
- var buildHtlcClaim = async (input) => {
1261
- if (paymentHashOf(input.preimage) !== input.htlc.paymentHash) {
1262
- throw new Error("preimage does not hash to the HTLC's payment hash");
1263
- }
1264
- return buildLeafSpend({
1265
- htlc: input.htlc,
1266
- utxo: input.utxo,
1267
- leaf: input.htlc.leaves.claim,
1268
- controlBlock: input.htlc.controlBlocks.claim,
1269
- stackAboveSig: [input.preimage],
1270
- payoutPkScript: input.payoutPkScript,
1271
- feeRateSatVb: input.feeRateSatVb,
1272
- sign: input.sign,
1273
- sequence: 4294967293
1274
- });
1275
- };
1276
- var buildHtlcRefund = (input) => buildLeafSpend({
1277
- htlc: input.htlc,
1278
- utxo: input.utxo,
1279
- leaf: input.htlc.leaves.refund,
1280
- controlBlock: input.htlc.controlBlocks.refund,
1281
- stackAboveSig: [],
1282
- payoutPkScript: input.payoutPkScript,
1283
- feeRateSatVb: input.feeRateSatVb,
1284
- sign: input.sign,
1285
- lockTime: input.htlc.refundLocktime,
1286
- // any value below 0xffffffff enables nLockTime enforcement
1287
- sequence: 4294967294
1288
- });
1289
- function extractPreimage(txHex, paymentHash) {
1290
- let raw;
1291
- try {
1292
- raw = btc.RawTx.decode(import_base5.hex.decode(txHex));
1293
- } catch {
1294
- return null;
1295
- }
1296
- for (const stack of raw.witnesses ?? []) {
1297
- for (const item of stack) {
1298
- if (item.length === 32 && import_base5.hex.encode((0, import_sha22.sha256)(item)) === paymentHash) return item;
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";
1299
1682
  }
1300
- }
1301
- return null;
1302
- }
1303
- var sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
1304
- async function awaitOnchainFill(chain, htlc, minConfirmations, options = {}) {
1305
- const pollMs = options.pollMs ?? 5e3;
1306
- for (; ; ) {
1307
- const utxos = await chain.getScriptUtxos(htlc.pkScript);
1308
- const eligible = utxos.filter((u) => u.confirmations >= minConfirmations).sort((a, b) => b.amount > a.amount ? 1 : -1);
1309
- if (eligible[0]) return eligible[0];
1310
- if (options.deadline !== void 0 && Date.now() / 1e3 >= options.deadline) {
1311
- const error = new Error("HTLC was not filled before the deadline");
1312
- error.reason = "fill_timeout";
1313
- throw error;
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
+ }
1314
1706
  }
1315
- await sleep(pollMs);
1316
- }
1317
- }
1318
- async function claimOnchainFill(chain, input) {
1319
- const now = input.now ?? Math.floor(Date.now() / 1e3);
1320
- if (input.htlc.refundLocktime - now < ONCHAIN_CLAIM_MARGIN_SECONDS) {
1321
- const error = new Error(
1322
- "refund leaf opens too soon to claim safely \u2014 take the covenant refund instead"
1323
- );
1324
- error.reason = "claim_window_closed";
1325
- throw error;
1326
- }
1327
- const spend = await buildHtlcClaim(input);
1328
- const txid = await chain.broadcast(spend.txHex);
1329
- return { txid, payoutAmount: spend.payoutAmount };
1330
- }
1331
- async function classifyOnchainHtlc(chain, input) {
1332
- const utxos = await chain.getScriptUtxos(input.htlc.pkScript);
1333
- const best = utxos.sort((a, b) => b.amount > a.amount ? 1 : -1)[0];
1334
- if (!best) {
1335
- if (!input.funding) return { phase: "unfunded" };
1336
- const spend = await chain.getSpendingTx(input.funding.txid, input.funding.vout);
1337
- if (!spend) return { phase: "unfunded" };
1338
- const preimage = extractPreimage(spend.txHex, input.htlc.paymentHash);
1339
- const txid = btc.Transaction.fromRaw(import_base5.hex.decode(spend.txHex), {
1340
- allowUnknownInputs: true,
1341
- allowUnknownOutputs: true
1342
- }).id;
1343
- return preimage ? { phase: "claimed", txid, preimage } : { phase: "swept", txid };
1344
- }
1345
- if (best.confirmations < input.minConfirmations)
1346
- return { phase: "awaiting_confirmations", utxo: best };
1347
- const mtp = await chain.getMtp();
1348
- if (mtp >= input.htlc.refundLocktime) return { phase: "refundable", utxo: best };
1349
- return { phase: "claimable", utxo: best };
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
+ };
1350
1716
  }
1351
1717
 
1352
1718
  // src/rfq.ts
1719
+ var import_base10 = require("@scure/base");
1720
+ var import_legacy2 = require("@noble/hashes/legacy.js");
1353
1721
  var import_sdk9 = require("@arkade-os/sdk");
1722
+ var import_sdk10 = require("@arkade-os/sdk");
1354
1723
 
1355
1724
  // src/claimPacket.ts
1356
- var import_base6 = require("@scure/base");
1725
+ var import_base8 = require("@scure/base");
1357
1726
  var import_secp256k1 = require("@noble/curves/secp256k1.js");
1358
1727
  var import_hkdf = require("@noble/hashes/hkdf.js");
1359
1728
  var import_sha23 = require("@noble/hashes/sha2.js");
@@ -1392,15 +1761,29 @@ async function sealWithEntropy(input, ephemeralKey, nonce) {
1392
1761
  packet.set(ephemeralPub, 0);
1393
1762
  packet.set(nonce, 33);
1394
1763
  packet.set(sealed, 45);
1395
- return { ciphertext: import_base6.base64.encode(packet) };
1764
+ return { ciphertext: import_base8.base64.encode(packet) };
1396
1765
  }
1397
1766
 
1398
1767
  // src/lockupContract.ts
1399
- var import_base7 = require("@scure/base");
1400
- var import_sdk7 = require("@arkade-os/sdk");
1768
+ var import_base9 = require("@scure/base");
1769
+ var import_sdk8 = require("@arkade-os/sdk");
1401
1770
  var SWAP_LOCKUP_CONTRACT_TYPE = "vhtlc-v2";
1402
1771
  var SWAP_LOCKUP_CONTRACT_LABEL = "Arkade RFQ swap lockup";
1403
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
+ };
1404
1787
  var LockupRegistrationFailed = class extends Error {
1405
1788
  /** The lockup address that was never registered — never fund it: nothing
1406
1789
  * is watching it. */
@@ -1419,8 +1802,8 @@ async function registerLockupContract(contracts, script, address) {
1419
1802
  try {
1420
1803
  await contracts.createContract({
1421
1804
  type: SWAP_LOCKUP_CONTRACT_TYPE,
1422
- params: import_sdk7.VHTLCV2ContractHandler.serializeParams(script.options),
1423
- script: import_base7.hex.encode(script.pkScript),
1805
+ params: import_sdk8.VHTLCV2ContractHandler.serializeParams(script.options),
1806
+ script: import_base9.hex.encode(script.pkScript),
1424
1807
  address,
1425
1808
  label: SWAP_LOCKUP_CONTRACT_LABEL,
1426
1809
  metadata: { genericallySpendable: false, kind: SWAP_LOCKUP_CONTRACT_KIND }
@@ -1429,6 +1812,12 @@ async function registerLockupContract(contracts, script, address) {
1429
1812
  throw new LockupRegistrationFailed(script, address, error);
1430
1813
  }
1431
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
+ }
1432
1821
 
1433
1822
  // src/rfq.ts
1434
1823
  var xOnly = (key, label) => {
@@ -1440,15 +1829,16 @@ var xOnly = (key, label) => {
1440
1829
  };
1441
1830
  var solverHex = (value, field) => {
1442
1831
  try {
1443
- return import_base8.hex.decode(value);
1832
+ return import_base10.hex.decode(value);
1444
1833
  } catch {
1445
1834
  throw new Error(`solver sent malformed hex for ${field}`);
1446
1835
  }
1447
1836
  };
1448
1837
  var ARKADE_BTC = "arkade:BTC";
1449
- var ARKADE_ASSET = "arkade:ASSET";
1450
1838
  var LIGHTNING_BTC = "lightning:BTC";
1451
1839
  var ONCHAIN_BTC = "onchain:BTC";
1840
+ var arkadeAssetLeg = (id) => `arkade:${id.toString()}`;
1841
+ var ARKADE_ASSET = "arkade:ASSET";
1452
1842
  var rfqPair = (from, to) => `${from}->${to}`;
1453
1843
  var LIGHTNING_SEND_PAIR = rfqPair(ARKADE_BTC, LIGHTNING_BTC);
1454
1844
  var LIGHTNING_RECEIVE_PAIR = rfqPair(LIGHTNING_BTC, ARKADE_BTC);
@@ -1475,7 +1865,7 @@ var AddressMismatch = class extends Error {
1475
1865
  this.quoted = quoted;
1476
1866
  }
1477
1867
  };
1478
- var newRfqId = () => import_base8.hex.encode(crypto.getRandomValues(new Uint8Array(32)));
1868
+ var newRfqId = () => import_base10.hex.encode(crypto.getRandomValues(new Uint8Array(32)));
1479
1869
  var lightningSendRequest = (input) => ({
1480
1870
  v: 1,
1481
1871
  type: "rfq_request",
@@ -1485,27 +1875,38 @@ var lightningSendRequest = (input) => ({
1485
1875
  profile: {
1486
1876
  invoice: input.invoice,
1487
1877
  refund_address: input.refundAddress,
1488
- client_refund_pubkey: import_base8.hex.encode(input.senderPubkey)
1878
+ client_refund_pubkey: import_base10.hex.encode(input.senderPubkey)
1489
1879
  }
1490
1880
  });
1491
1881
  var arkadeSwapRequest = (input) => {
1492
- if (Boolean(input.wantAsset) === Boolean(input.offerAsset)) {
1493
- throw new Error("set exactly one of wantAsset (BTC->asset) or offerAsset (asset->BTC)");
1882
+ if (!input.wantAsset && !input.offerAsset) {
1883
+ throw new Error(
1884
+ "set exactly one of wantAsset (BTC->asset) or offerAsset (asset->BTC) \u2014 with neither set both legs are BTC, which is not a swap"
1885
+ );
1886
+ }
1887
+ if (input.wantAsset && input.offerAsset) {
1888
+ throw new Error(
1889
+ "set exactly one of wantAsset (BTC->asset) or offerAsset (asset->BTC) \u2014 asset->asset is nameable on the wire but no solver quotes it yet"
1890
+ );
1494
1891
  }
1892
+ const pair = rfqPair(
1893
+ input.offerAsset ? arkadeAssetLeg(input.offerAsset) : ARKADE_BTC,
1894
+ input.wantAsset ? arkadeAssetLeg(input.wantAsset) : ARKADE_BTC
1895
+ );
1896
+ assertPairLength(pair);
1495
1897
  return {
1496
1898
  v: 1,
1497
1899
  type: "rfq_request",
1498
1900
  rfq_id: input.rfqId,
1499
- pair: rfqPair(
1500
- input.offerAsset ? ARKADE_ASSET : ARKADE_BTC,
1501
- input.wantAsset ? ARKADE_ASSET : ARKADE_BTC
1502
- ),
1901
+ pair,
1503
1902
  amount_side: input.amountSide,
1504
1903
  amount: input.amount,
1505
- profile: {
1506
- ...input.offerAsset && { offer_asset: import_base8.hex.encode(input.offerAsset.serialize()) },
1507
- ...input.wantAsset && { want_asset: import_base8.hex.encode(input.wantAsset.serialize()) }
1508
- }
1904
+ // The pair is the only place the asset ids appear. Repeating them here
1905
+ // would be a key the solver's `.strict()` profile schema does not
1906
+ // declare, and an undeclared key is `unsupported_payload` — a refusal,
1907
+ // not an ignored extra. Empty, not absent: `profile` is required on
1908
+ // every other request shape this wire carries.
1909
+ profile: {}
1509
1910
  };
1510
1911
  };
1511
1912
  var MIN_HEADROOM_SECONDS = 90 * 60;
@@ -1519,6 +1920,14 @@ var assertFinite = (value, reason, label) => {
1519
1920
  throw gateError(reason, `${label} is not a finite number (${String(value)})`);
1520
1921
  }
1521
1922
  };
1923
+ var MAX_PAIR_LENGTH = 158;
1924
+ var assertPairLength = (pair) => {
1925
+ if (pair.length > MAX_PAIR_LENGTH) {
1926
+ throw new Error(
1927
+ `pair is ${pair.length} characters, over the wire's ${MAX_PAIR_LENGTH}-character limit`
1928
+ );
1929
+ }
1930
+ };
1522
1931
  var verifyLockupAddress = (quote, derivedAddress) => {
1523
1932
  const quoted = quote.profile?.lockup_address;
1524
1933
  if (derivedAddress !== quoted) throw new AddressMismatch(derivedAddress, quoted);
@@ -1558,14 +1967,20 @@ var assertFundable = (input) => {
1558
1967
  }
1559
1968
  }
1560
1969
  };
1561
- var expectQuote = (payload, rfqId) => {
1970
+ var expectQuote = (payload, rfqId, requestedPair) => {
1562
1971
  const p = payload;
1563
1972
  if (p?.type === "rfq_refusal") throw new SwapRefusal(p.reason ?? "unknown", p.rfq_id ?? rfqId);
1564
1973
  if (p?.type !== "rfq_quote" || p.rfq_id !== rfqId) {
1565
1974
  throw new Error(`unexpected reply: ${p?.type ?? "no payload"}`);
1566
1975
  }
1976
+ if (requestedPair !== void 0 && p.pair !== requestedPair) {
1977
+ throw new Error(
1978
+ `solver quoted ${JSON.stringify(p.pair)}, not the requested ${requestedPair}`
1979
+ );
1980
+ }
1567
1981
  return payload;
1568
1982
  };
1983
+ var pairOf = (payload) => typeof payload.pair === "string" ? payload.pair : void 0;
1569
1984
  var httpTransport = (baseUrl, options = {}) => {
1570
1985
  const fetchImpl = options.fetchImpl ?? fetch;
1571
1986
  const readJson = async (response, what) => {
@@ -1585,7 +2000,11 @@ var httpTransport = (baseUrl, options = {}) => {
1585
2000
  headers: { "content-type": "application/json" },
1586
2001
  body: JSON.stringify(payload)
1587
2002
  });
1588
- return expectQuote(await readJson(response, "quote request"), String(payload.rfq_id));
2003
+ return expectQuote(
2004
+ await readJson(response, "quote request"),
2005
+ String(payload.rfq_id),
2006
+ pairOf(payload)
2007
+ );
1589
2008
  },
1590
2009
  async status(rfqId) {
1591
2010
  const response = await fetchImpl(`${baseUrl}/v1/rfq/${rfqId}`, { method: "GET" });
@@ -1662,7 +2081,8 @@ var relayTransport = (relayUrl, options) => {
1662
2081
  async requestQuote(payload) {
1663
2082
  return expectQuote(
1664
2083
  await roundTrip(payload, String(payload.rfq_id)),
1665
- String(payload.rfq_id)
2084
+ String(payload.rfq_id),
2085
+ pairOf(payload)
1666
2086
  );
1667
2087
  },
1668
2088
  async status(rfqId) {
@@ -1702,11 +2122,11 @@ function lightningSendVtxoScript(params) {
1702
2122
  type: "seconds",
1703
2123
  value: BigInt(value)
1704
2124
  });
1705
- return new import_sdk8.VHTLC.ScriptV2({
2125
+ return new import_sdk9.VHTLC.ScriptV2({
1706
2126
  sender: params.senderPubkey,
1707
2127
  receiver: params.solverPubkey,
1708
2128
  server: params.serverPubkey,
1709
- preimageHash: (0, import_legacy2.ripemd160)(import_base8.hex.decode(params.paymentHash)),
2129
+ preimageHash: (0, import_legacy2.ripemd160)(import_base10.hex.decode(params.paymentHash)),
1710
2130
  refundLocktime: BigInt(params.refundLocktime),
1711
2131
  unilateralClaimDelay: seconds(params.claimDelay),
1712
2132
  unilateralRefundDelay: seconds(unilateralRefundDelay(params.claimDelay)),
@@ -1725,10 +2145,10 @@ function lightningSendVtxoScript(params) {
1725
2145
  }
1726
2146
  async function requestLightningSend(wallet, arkServerUrl, transport, params) {
1727
2147
  const rfqId = params.rfqId ?? newRfqId();
1728
- const secrets = await (0, import_sdk9.provisionRefundKey)(wallet);
2148
+ const secrets = await (0, import_sdk10.provisionRefundKey)(wallet);
1729
2149
  const senderPubkey = secrets.pubkey;
1730
2150
  const [info, refundAddress] = await Promise.all([
1731
- new import_sdk8.RestArkProvider(arkServerUrl).getInfo(),
2151
+ new import_sdk9.RestArkProvider(arkServerUrl).getInfo(),
1732
2152
  wallet.getAddress()
1733
2153
  ]);
1734
2154
  const quote = await transport.requestQuote(
@@ -1751,22 +2171,23 @@ async function requestLightningSend(wallet, arkServerUrl, transport, params) {
1751
2171
  `quote from_amount ${quote.from_amount} is below the invoice amount \u2014 a negative spread is not a quote`
1752
2172
  );
1753
2173
  }
1754
- const serverPubkey = xOnly(import_base8.hex.decode(info.signerPubkey), "ark signer key");
1755
- const network = (0, import_sdk8.getNetwork)(info.network);
1756
- const script = lightningSendVtxoScript({
1757
- 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"),
1758
2178
  refundLocktime: quote.refund_locktime,
1759
2179
  serverPubkey,
1760
2180
  paymentHash: params.invoice.paymentHash,
1761
2181
  claimDelay: unilateralClaimDelay(Number(info.unilateralExitDelay)),
1762
2182
  emulatorPubkey: xOnly(
1763
- import_base8.hex.decode((0, import_sdk8.resolveEmulatorPubkey)(network, params.emulatorPubkey)),
2183
+ import_base10.hex.decode((0, import_sdk9.resolveEmulatorPubkey)(network, params.emulatorPubkey)),
1764
2184
  "emulator signer key"
1765
2185
  ),
1766
2186
  senderPubkey,
1767
2187
  receiverPkScript: solverHex(receiverPkScriptHex, "profile.receiver_pk_script"),
1768
- refundPkScript: import_sdk8.ArkAddress.decode(refundAddress).pkScript
1769
- });
2188
+ refundPkScript: import_sdk9.ArkAddress.decode(refundAddress).pkScript
2189
+ };
2190
+ const script = lightningSendVtxoScript(treeParams);
1770
2191
  const address = script.address(network.hrp, serverPubkey).encode();
1771
2192
  verifyLockupAddress(quote, address);
1772
2193
  assertFundable({
@@ -1786,7 +2207,8 @@ async function requestLightningSend(wallet, arkServerUrl, transport, params) {
1786
2207
  script,
1787
2208
  refundAddress,
1788
2209
  senderPubkey,
1789
- secrets
2210
+ secrets,
2211
+ treeParams
1790
2212
  };
1791
2213
  }
1792
2214
  var offerTermsFromQuote = (quote, assets) => {
@@ -1805,9 +2227,9 @@ var onchainSendRequest = (input) => ({
1805
2227
  amount: input.amount,
1806
2228
  profile: {
1807
2229
  payment_hash: input.paymentHash,
1808
- payout_pubkey: import_base8.hex.encode(input.payoutPubkey),
2230
+ payout_pubkey: import_base10.hex.encode(input.payoutPubkey),
1809
2231
  refund_address: input.refundAddress,
1810
- client_refund_pubkey: import_base8.hex.encode(input.senderPubkey)
2232
+ client_refund_pubkey: import_base10.hex.encode(input.senderPubkey)
1811
2233
  }
1812
2234
  });
1813
2235
  var lightningReceiveRequest = (input) => ({
@@ -1820,7 +2242,7 @@ var lightningReceiveRequest = (input) => ({
1820
2242
  profile: {
1821
2243
  payment_hash: input.paymentHash,
1822
2244
  payout_address: input.payoutAddress,
1823
- payout_pubkey: import_base8.hex.encode(input.payoutPubkey),
2245
+ payout_pubkey: import_base10.hex.encode(input.payoutPubkey),
1824
2246
  claim_packet: input.claimPacket
1825
2247
  }
1826
2248
  });
@@ -1834,9 +2256,9 @@ var onchainReceiveRequest = (input) => ({
1834
2256
  profile: {
1835
2257
  payment_hash: input.paymentHash,
1836
2258
  claim_packet: input.claimPacket,
1837
- refund_pubkey: import_base8.hex.encode(input.refundPubkey),
2259
+ refund_pubkey: import_base10.hex.encode(input.refundPubkey),
1838
2260
  payout_address: input.payoutAddress,
1839
- payout_pubkey: import_base8.hex.encode(input.payoutPubkey)
2261
+ payout_pubkey: import_base10.hex.encode(input.payoutPubkey)
1840
2262
  }
1841
2263
  });
1842
2264
  function deriveOnchainSend(input) {
@@ -1852,7 +2274,7 @@ function deriveOnchainSend(input) {
1852
2274
  throw new Error("onchain-send quote is missing a binding field");
1853
2275
  }
1854
2276
  const script = lightningSendVtxoScript({
1855
- solverPubkey: xOnly(import_base8.hex.decode(quote.solver_pubkey), "solver key"),
2277
+ solverPubkey: xOnly(import_base10.hex.decode(quote.solver_pubkey), "solver key"),
1856
2278
  refundLocktime,
1857
2279
  serverPubkey: input.serverPubkey,
1858
2280
  paymentHash: input.paymentHash,
@@ -1860,25 +2282,25 @@ function deriveOnchainSend(input) {
1860
2282
  emulatorPubkey: input.emulatorPubkey,
1861
2283
  senderPubkey: input.senderPubkey,
1862
2284
  receiverPkScript: solverHex(receiverPkScriptHex, "profile.receiver_pk_script"),
1863
- refundPkScript: import_sdk8.ArkAddress.decode(input.refundAddress).pkScript
2285
+ refundPkScript: import_sdk9.ArkAddress.decode(input.refundAddress).pkScript
1864
2286
  });
1865
2287
  const address = script.address(input.hrp, input.serverPubkey).encode();
1866
2288
  verifyLockupAddress(quote, address);
1867
- const htlc = onchainHtlcScript(
1868
- {
1869
- paymentHash: input.paymentHash,
1870
- claimKey: input.payoutPubkey,
1871
- refundKey: xOnly(import_base8.hex.decode(htlcPubkey), "solver L1 htlc key"),
1872
- refundLocktime: htlcLocktime
1873
- },
1874
- input.l1Network
1875
- );
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);
1876
2296
  if (htlc.address !== htlcAddress) throw new AddressMismatch(htlc.address, htlcAddress);
1877
2297
  return {
1878
2298
  address,
1879
2299
  swapPkScript: script.pkScript,
1880
2300
  script,
1881
2301
  htlc,
2302
+ htlcParams,
2303
+ l1Network: input.l1Network,
1882
2304
  refundLocktime,
1883
2305
  htlcLocktime,
1884
2306
  minConfirmations
@@ -1886,16 +2308,16 @@ function deriveOnchainSend(input) {
1886
2308
  }
1887
2309
  async function requestOnchainSend(wallet, arkServerUrl, transport, params) {
1888
2310
  const rfqId = params.rfqId ?? newRfqId();
1889
- const secrets = await (0, import_sdk9.provisionClaimSecret)(wallet, { preimage: params.preimage });
2311
+ const secrets = await (0, import_sdk10.provisionClaimSecret)(wallet, { preimage: params.preimage });
1890
2312
  if (secrets.mustPersistPreimage) {
1891
2313
  console.warn(
1892
2314
  "[swap] this swap's preimage cannot be re-derived from the seed and MUST be persisted with the record before funding"
1893
2315
  );
1894
2316
  }
1895
- const paymentHash = import_base8.hex.encode(secrets.paymentHash);
2317
+ const paymentHash = import_base10.hex.encode(secrets.paymentHash);
1896
2318
  const senderPubkey = secrets.pubkey;
1897
2319
  const [info, refundAddress] = await Promise.all([
1898
- new import_sdk8.RestArkProvider(arkServerUrl).getInfo(),
2320
+ new import_sdk9.RestArkProvider(arkServerUrl).getInfo(),
1899
2321
  wallet.getAddress()
1900
2322
  ]);
1901
2323
  const quote = await transport.requestQuote(
@@ -1909,14 +2331,14 @@ async function requestOnchainSend(wallet, arkServerUrl, transport, params) {
1909
2331
  amountSide: params.amountSide
1910
2332
  })
1911
2333
  );
1912
- const network = (0, import_sdk8.getNetwork)(info.network);
2334
+ const network = (0, import_sdk9.getNetwork)(info.network);
1913
2335
  const derived = deriveOnchainSend({
1914
2336
  quote,
1915
2337
  paymentHash,
1916
2338
  payoutPubkey: params.payoutPubkey,
1917
- serverPubkey: xOnly(import_base8.hex.decode(info.signerPubkey), "ark signer key"),
2339
+ serverPubkey: xOnly(import_base10.hex.decode(info.signerPubkey), "ark signer key"),
1918
2340
  emulatorPubkey: xOnly(
1919
- import_base8.hex.decode((0, import_sdk8.resolveEmulatorPubkey)(network, params.emulatorPubkey)),
2341
+ import_base10.hex.decode((0, import_sdk9.resolveEmulatorPubkey)(network, params.emulatorPubkey)),
1920
2342
  "emulator signer key"
1921
2343
  ),
1922
2344
  claimDelay: unilateralClaimDelay(Number(info.unilateralExitDelay)),
@@ -1948,6 +2370,9 @@ async function requestOnchainSend(wallet, arkServerUrl, transport, params) {
1948
2370
  script: derived.script,
1949
2371
  refundAddress,
1950
2372
  htlc: derived.htlc,
2373
+ htlcParams: derived.htlcParams,
2374
+ l1Network: derived.l1Network,
2375
+ minConfirmations: derived.minConfirmations,
1951
2376
  senderPubkey,
1952
2377
  secrets
1953
2378
  };
@@ -2024,11 +2449,11 @@ function receiveVtxoScript(params) {
2024
2449
  type: "seconds",
2025
2450
  value: BigInt(value)
2026
2451
  });
2027
- return new import_sdk8.VHTLC.ScriptV2({
2452
+ return new import_sdk9.VHTLC.ScriptV2({
2028
2453
  sender: params.solverPubkey,
2029
2454
  receiver: params.payoutPubkey,
2030
2455
  server: params.serverPubkey,
2031
- preimageHash: (0, import_legacy2.ripemd160)(import_base8.hex.decode(params.paymentHash)),
2456
+ preimageHash: (0, import_legacy2.ripemd160)(import_base10.hex.decode(params.paymentHash)),
2032
2457
  refundLocktime: BigInt(params.refundLocktime),
2033
2458
  unilateralClaimDelay: seconds(params.claimDelay),
2034
2459
  unilateralRefundDelay: seconds(unilateralRefundDelay(params.claimDelay)),
@@ -2054,8 +2479,8 @@ function deriveLightningReceive(input) {
2054
2479
  if (refundLocktime === void 0 || invoice === void 0 || solverRefundPkScriptHex === void 0) {
2055
2480
  throw new Error("lightning-receive quote is missing a binding field");
2056
2481
  }
2057
- const script = receiveVtxoScript({
2058
- 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"),
2059
2484
  refundLocktime,
2060
2485
  serverPubkey: input.serverPubkey,
2061
2486
  paymentHash: input.paymentHash,
@@ -2063,25 +2488,26 @@ function deriveLightningReceive(input) {
2063
2488
  emulatorPubkey: input.emulatorPubkey,
2064
2489
  solverRefundPkScript: solverHex(solverRefundPkScriptHex, "profile.solver_refund_pk_script"),
2065
2490
  payoutPubkey: input.payoutPubkey,
2066
- payoutPkScript: import_sdk8.ArkAddress.decode(input.payoutAddress).pkScript
2067
- });
2491
+ payoutPkScript: import_sdk9.ArkAddress.decode(input.payoutAddress).pkScript
2492
+ };
2493
+ const script = receiveVtxoScript(treeParams);
2068
2494
  const address = script.address(input.hrp, input.serverPubkey).encode();
2069
2495
  verifyLockupAddress(quote, address);
2070
- return { address, swapPkScript: script.pkScript, script, invoice, refundLocktime };
2496
+ return { address, swapPkScript: script.pkScript, script, invoice, refundLocktime, treeParams };
2071
2497
  }
2072
2498
  async function requestLightningReceive(wallet, arkServerUrl, transport, params) {
2073
2499
  const rfqId = params.rfqId ?? newRfqId();
2074
- const secrets = await (0, import_sdk9.provisionClaimSecret)(wallet);
2500
+ const secrets = await (0, import_sdk10.provisionClaimSecret)(wallet);
2075
2501
  if (secrets.mustPersistPreimage) {
2076
2502
  console.warn(
2077
2503
  "[swap] this swap's preimage cannot be re-derived from the seed and MUST be persisted with the record before paying"
2078
2504
  );
2079
2505
  }
2080
2506
  const preimage = secrets.preimage;
2081
- const paymentHash = import_base8.hex.encode(secrets.paymentHash);
2507
+ const paymentHash = import_base10.hex.encode(secrets.paymentHash);
2082
2508
  const payoutPubkey = secrets.pubkey;
2083
2509
  const [info, payoutAddress] = await Promise.all([
2084
- new import_sdk8.RestArkProvider(arkServerUrl).getInfo(),
2510
+ new import_sdk9.RestArkProvider(arkServerUrl).getInfo(),
2085
2511
  wallet.getAddress()
2086
2512
  ]);
2087
2513
  const claimPacket = await sealClaimPacket({
@@ -2100,15 +2526,15 @@ async function requestLightningReceive(wallet, arkServerUrl, transport, params)
2100
2526
  })
2101
2527
  );
2102
2528
  assertQuotedAmount(quote, params.amountSide, params.amount);
2103
- const network = (0, import_sdk8.getNetwork)(info.network);
2529
+ const network = (0, import_sdk9.getNetwork)(info.network);
2104
2530
  const derived = deriveLightningReceive({
2105
2531
  quote,
2106
2532
  paymentHash,
2107
2533
  payoutPubkey,
2108
2534
  payoutAddress,
2109
- serverPubkey: xOnly(import_base8.hex.decode(info.signerPubkey), "ark signer key"),
2535
+ serverPubkey: xOnly(import_base10.hex.decode(info.signerPubkey), "ark signer key"),
2110
2536
  emulatorPubkey: xOnly(
2111
- import_base8.hex.decode((0, import_sdk8.resolveEmulatorPubkey)(network, params.emulatorPubkey)),
2537
+ import_base10.hex.decode((0, import_sdk9.resolveEmulatorPubkey)(network, params.emulatorPubkey)),
2112
2538
  "emulator signer key"
2113
2539
  ),
2114
2540
  claimDelay: unilateralClaimDelay(Number(info.unilateralExitDelay)),
@@ -2139,7 +2565,8 @@ async function requestLightningReceive(wallet, arkServerUrl, transport, params)
2139
2565
  script: derived.script,
2140
2566
  payoutAddress,
2141
2567
  payoutPubkey,
2142
- secrets
2568
+ secrets,
2569
+ treeParams: derived.treeParams
2143
2570
  };
2144
2571
  }
2145
2572
  function deriveOnchainReceive(input) {
@@ -2155,7 +2582,7 @@ function deriveOnchainReceive(input) {
2155
2582
  throw new Error("onchain-receive quote is missing a binding field");
2156
2583
  }
2157
2584
  const script = receiveVtxoScript({
2158
- solverPubkey: xOnly(import_base8.hex.decode(quote.solver_pubkey), "solver key"),
2585
+ solverPubkey: xOnly(import_base10.hex.decode(quote.solver_pubkey), "solver key"),
2159
2586
  refundLocktime,
2160
2587
  serverPubkey: input.serverPubkey,
2161
2588
  paymentHash: input.paymentHash,
@@ -2163,14 +2590,14 @@ function deriveOnchainReceive(input) {
2163
2590
  emulatorPubkey: input.emulatorPubkey,
2164
2591
  solverRefundPkScript: solverHex(solverRefundPkScriptHex, "profile.solver_refund_pk_script"),
2165
2592
  payoutPubkey: input.payoutPubkey,
2166
- payoutPkScript: import_sdk8.ArkAddress.decode(input.payoutAddress).pkScript
2593
+ payoutPkScript: import_sdk9.ArkAddress.decode(input.payoutAddress).pkScript
2167
2594
  });
2168
2595
  const address = script.address(input.hrp, input.serverPubkey).encode();
2169
2596
  verifyLockupAddress(quote, address);
2170
2597
  const htlc = onchainHtlcScript(
2171
2598
  {
2172
2599
  paymentHash: input.paymentHash,
2173
- claimKey: xOnly(import_base8.hex.decode(claimPubkey), "solver L1 claim key"),
2600
+ claimKey: xOnly(import_base10.hex.decode(claimPubkey), "solver L1 claim key"),
2174
2601
  refundKey: input.refundPubkey,
2175
2602
  refundLocktime: htlcLocktime
2176
2603
  },
@@ -2189,17 +2616,17 @@ function deriveOnchainReceive(input) {
2189
2616
  }
2190
2617
  async function requestOnchainReceive(wallet, arkServerUrl, transport, params) {
2191
2618
  const rfqId = params.rfqId ?? newRfqId();
2192
- const secrets = await (0, import_sdk9.provisionClaimSecret)(wallet);
2619
+ const secrets = await (0, import_sdk10.provisionClaimSecret)(wallet);
2193
2620
  if (secrets.mustPersistPreimage) {
2194
2621
  console.warn(
2195
2622
  "[swap] this swap's preimage cannot be re-derived from the seed and MUST be persisted with the record before funding"
2196
2623
  );
2197
2624
  }
2198
2625
  const preimage = secrets.preimage;
2199
- const paymentHash = import_base8.hex.encode(secrets.paymentHash);
2626
+ const paymentHash = import_base10.hex.encode(secrets.paymentHash);
2200
2627
  const payoutPubkey = secrets.pubkey;
2201
2628
  const [info, payoutAddress] = await Promise.all([
2202
- new import_sdk8.RestArkProvider(arkServerUrl).getInfo(),
2629
+ new import_sdk9.RestArkProvider(arkServerUrl).getInfo(),
2203
2630
  wallet.getAddress()
2204
2631
  ]);
2205
2632
  const claimPacket = await sealClaimPacket({
@@ -2219,16 +2646,16 @@ async function requestOnchainReceive(wallet, arkServerUrl, transport, params) {
2219
2646
  })
2220
2647
  );
2221
2648
  assertQuotedAmount(quote, params.amountSide, params.amount);
2222
- const network = (0, import_sdk8.getNetwork)(info.network);
2649
+ const network = (0, import_sdk9.getNetwork)(info.network);
2223
2650
  const derived = deriveOnchainReceive({
2224
2651
  quote,
2225
2652
  paymentHash,
2226
2653
  payoutPubkey,
2227
2654
  payoutAddress,
2228
2655
  refundPubkey: params.refundPubkey,
2229
- serverPubkey: xOnly(import_base8.hex.decode(info.signerPubkey), "ark signer key"),
2656
+ serverPubkey: xOnly(import_base10.hex.decode(info.signerPubkey), "ark signer key"),
2230
2657
  emulatorPubkey: xOnly(
2231
- import_base8.hex.decode((0, import_sdk8.resolveEmulatorPubkey)(network, params.emulatorPubkey)),
2658
+ import_base10.hex.decode((0, import_sdk9.resolveEmulatorPubkey)(network, params.emulatorPubkey)),
2232
2659
  "emulator signer key"
2233
2660
  ),
2234
2661
  claimDelay: unilateralClaimDelay(Number(info.unilateralExitDelay)),
@@ -2265,15 +2692,15 @@ async function requestOnchainReceive(wallet, arkServerUrl, transport, params) {
2265
2692
  }
2266
2693
 
2267
2694
  // src/claim.ts
2268
- var import_base10 = require("@scure/base");
2695
+ var import_base12 = require("@scure/base");
2269
2696
  var import_legacy3 = require("@noble/hashes/legacy.js");
2270
2697
  var import_sha25 = require("@noble/hashes/sha2.js");
2271
- var import_sdk11 = require("@arkade-os/sdk");
2698
+ var import_sdk12 = require("@arkade-os/sdk");
2272
2699
 
2273
2700
  // src/refund.ts
2274
- var import_base9 = require("@scure/base");
2701
+ var import_base11 = require("@scure/base");
2275
2702
  var import_sha24 = require("@noble/hashes/sha2.js");
2276
- var import_sdk10 = require("@arkade-os/sdk");
2703
+ var import_sdk11 = require("@arkade-os/sdk");
2277
2704
  var sleep2 = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
2278
2705
  var isRfqTerminal = (state) => RFQ_TERMINAL_STATES.includes(state);
2279
2706
  var RFQ_RESOLVED_STATES = ["settled", "refunded"];
@@ -2320,7 +2747,7 @@ var LockupNeedsRecoveryError = class extends Error {
2320
2747
  }
2321
2748
  };
2322
2749
  async function findLockupVtxos(indexer, swapPkScript) {
2323
- const scripts = [import_base9.hex.encode(swapPkScript)];
2750
+ const scripts = [import_base11.hex.encode(swapPkScript)];
2324
2751
  const [spendable, recoverable] = await Promise.all([
2325
2752
  indexer.getVtxos({ scripts, spendableOnly: true }),
2326
2753
  indexer.getVtxos({ scripts, recoverableOnly: true })
@@ -2345,28 +2772,33 @@ async function findLockupVtxos(indexer, swapPkScript) {
2345
2772
  }
2346
2773
  return out;
2347
2774
  }
2348
- 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;
2349
2776
  var candidateWitnessItems = (tx, inputIndex) => [
2350
- ...(0, import_sdk10.getArkPsbtFields)(tx, inputIndex, import_sdk10.ConditionWitness).flat(),
2777
+ ...(0, import_sdk11.getArkPsbtFields)(tx, inputIndex, import_sdk11.ConditionWitness).flat(),
2351
2778
  ...tx.getInput(inputIndex).finalScriptWitness ?? []
2352
2779
  ];
2353
2780
  async function readLockupFate(indexer, input) {
2354
- 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)] });
2355
2782
  const all = vtxos ?? [];
2356
2783
  if (all.length === 0) return { fate: "unknown" };
2357
- const spentBy = /* @__PURE__ */ new Set();
2784
+ const spentBy = /* @__PURE__ */ new Map();
2358
2785
  let everySpendNamed = true;
2359
2786
  for (const vtxo of all) {
2360
2787
  if (!vtxo.isSpent && !vtxo.spentBy && !vtxo.settledBy) return { fate: "open" };
2361
- 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
+ });
2362
2793
  else everySpendNamed = false;
2363
2794
  }
2364
- const { txs } = await indexer.getVirtualTxs([...spentBy]);
2795
+ const spends = [...spentBy.values()];
2796
+ const { txs } = await indexer.getVirtualTxs([...spentBy.keys()]);
2365
2797
  const observed = /* @__PURE__ */ new Set();
2366
2798
  for (const raw of txs) {
2367
2799
  let tx;
2368
2800
  try {
2369
- tx = import_sdk10.Transaction.fromPSBT(import_base9.base64.decode(raw));
2801
+ tx = import_sdk11.Transaction.fromPSBT(import_base11.base64.decode(raw));
2370
2802
  } catch {
2371
2803
  continue;
2372
2804
  }
@@ -2374,16 +2806,16 @@ async function readLockupFate(indexer, input) {
2374
2806
  for (let i = 0; i < tx.inputsLength; i++) {
2375
2807
  const spent = tx.getInput(i);
2376
2808
  if (!spent.txid) continue;
2377
- const txid = import_base9.hex.encode(spent.txid);
2809
+ const txid = import_base11.hex.encode(spent.txid);
2378
2810
  if (!all.some((vtxo) => vtxo.txid === txid && vtxo.vout === spent.index)) continue;
2379
2811
  for (const candidate of candidateWitnessItems(tx, i)) {
2380
2812
  if (hashesTo(candidate, input.paymentHash)) {
2381
- return { fate: "claimed", preimage: candidate };
2813
+ return { fate: "claimed", preimage: candidate, spends };
2382
2814
  }
2383
2815
  }
2384
2816
  }
2385
2817
  }
2386
- return everySpendNamed && observed.size === spentBy.size ? { fate: "returned" } : { fate: "unknown" };
2818
+ return everySpendNamed && observed.size === spentBy.size ? { fate: "returned", spends } : { fate: "unknown" };
2387
2819
  }
2388
2820
  async function pushRefundWithoutReceiver(ark, input) {
2389
2821
  if (input.vtxos.length === 0) throw new Error("nothing to refund: no funded outputs");
@@ -2403,14 +2835,14 @@ async function pushRefundWithoutReceiver(ark, input) {
2403
2835
  const info = await ark.getInfo();
2404
2836
  let serverUnrollScript;
2405
2837
  try {
2406
- serverUnrollScript = import_sdk10.CSVMultisigTapscript.decode(import_base9.hex.decode(info.checkpointTapscript));
2838
+ serverUnrollScript = import_sdk11.CSVMultisigTapscript.decode(import_base11.hex.decode(info.checkpointTapscript));
2407
2839
  } catch {
2408
2840
  throw new Error("invalid checkpointTapscript from the Arkade server");
2409
2841
  }
2410
2842
  const leaf = input.script.refundWithoutReceiver();
2411
2843
  const tapTree = input.script.encode();
2412
2844
  const amount = input.vtxos.reduce((sum, vtxo) => sum + vtxo.value, 0);
2413
- const { arkTx, checkpoints } = (0, import_sdk10.buildOffchainTx)(
2845
+ const { arkTx, checkpoints } = (0, import_sdk11.buildOffchainTx)(
2414
2846
  input.vtxos.map((vtxo) => ({
2415
2847
  txid: vtxo.txid,
2416
2848
  vout: vtxo.vout,
@@ -2423,18 +2855,18 @@ async function pushRefundWithoutReceiver(ark, input) {
2423
2855
  );
2424
2856
  const signedArkTx = await input.sender.sign(arkTx);
2425
2857
  const submitted = await ark.submitTx(
2426
- import_base9.base64.encode(signedArkTx.toPSBT()),
2427
- 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()))
2428
2860
  );
2429
- (0, import_sdk10.assertSubmittedArkTxid)(submitted, signedArkTx, "refundWithoutReceiver");
2430
- const matched = (0, import_sdk10.matchServerCheckpoints)(
2861
+ (0, import_sdk11.assertSubmittedArkTxid)(submitted, signedArkTx, "refundWithoutReceiver");
2862
+ const matched = (0, import_sdk11.matchServerCheckpoints)(
2431
2863
  submitted.signedCheckpointTxs,
2432
2864
  checkpoints,
2433
2865
  "refundWithoutReceiver"
2434
2866
  );
2435
2867
  const finalCheckpoints = await Promise.all(
2436
2868
  matched.map(
2437
- 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())
2438
2870
  )
2439
2871
  );
2440
2872
  await ark.finalizeTx(submitted.arkTxid, finalCheckpoints);
@@ -2514,20 +2946,20 @@ async function pushClaim(ark, input) {
2514
2946
  }
2515
2947
  }
2516
2948
  const committed = input.script.options.preimageHash;
2517
- 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)) {
2518
2950
  throw new Error("preimage does not match the covenant's payment hash");
2519
2951
  }
2520
2952
  const info = await ark.getInfo();
2521
2953
  let serverUnrollScript;
2522
2954
  try {
2523
- serverUnrollScript = import_sdk11.CSVMultisigTapscript.decode(import_base10.hex.decode(info.checkpointTapscript));
2955
+ serverUnrollScript = import_sdk12.CSVMultisigTapscript.decode(import_base12.hex.decode(info.checkpointTapscript));
2524
2956
  } catch {
2525
2957
  throw new Error("invalid checkpointTapscript from the Arkade server");
2526
2958
  }
2527
2959
  const leaf = input.script.claim();
2528
2960
  const tapTree = input.script.encode();
2529
- const arkTxid = await (0, import_sdk11.signAndSubmitOffchainTx)({
2530
- 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),
2531
2963
  provider: ark,
2532
2964
  inputs: input.vtxos.map((vtxo) => ({
2533
2965
  txid: vtxo.txid,
@@ -2574,7 +3006,7 @@ async function claimReceiveLockup(indexer, ark, input) {
2574
3006
  }
2575
3007
 
2576
3008
  // src/refundBlocked.ts
2577
- var import_sdk12 = require("@arkade-os/sdk");
3009
+ var import_sdk13 = require("@arkade-os/sdk");
2578
3010
  var RefundNotLocallyPossibleError = class extends Error {
2579
3011
  constructor(reason, message, options) {
2580
3012
  super(message, options);
@@ -2591,16 +3023,16 @@ async function senderIdentityForSwapRecord(wallet, record) {
2591
3023
  );
2592
3024
  }
2593
3025
  try {
2594
- return await (0, import_sdk12.contractSigner)(wallet, record.signingDescriptor);
3026
+ return await (0, import_sdk13.contractSigner)(wallet, record.signingDescriptor);
2595
3027
  } catch (cause) {
2596
- if (cause instanceof import_sdk12.WalletCannotSignError) {
3028
+ if (cause instanceof import_sdk13.WalletCannotSignError) {
2597
3029
  throw new RefundNotLocallyPossibleError(
2598
3030
  "unsignable-wallet",
2599
3031
  `this wallet holds ${record.signingDescriptor} but cannot sign with it; attach its signer`,
2600
3032
  { cause }
2601
3033
  );
2602
3034
  }
2603
- if (cause instanceof import_sdk12.ForeignDescriptorError) {
3035
+ if (cause instanceof import_sdk13.ForeignDescriptorError) {
2604
3036
  throw new RefundNotLocallyPossibleError(
2605
3037
  "foreign-descriptor",
2606
3038
  `this wallet cannot derive ${record.signingDescriptor}; the swap was created on another wallet`,
@@ -2611,10 +3043,31 @@ async function senderIdentityForSwapRecord(wallet, record) {
2611
3043
  }
2612
3044
  }
2613
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
+
2614
3069
  // src/swapManager.ts
2615
- var import_base11 = require("@scure/base");
2616
- var RFQ_SWAP_TERMINAL_STATES = ["settled", "refunded", "failed"];
2617
- var isRfqSwapTerminal = (state) => RFQ_SWAP_TERMINAL_STATES.includes(state);
3070
+ var import_base13 = require("@scure/base");
2618
3071
  function nextOnchainAction(input) {
2619
3072
  switch (input.phase.phase) {
2620
3073
  case "unfunded":
@@ -2630,6 +3083,17 @@ function nextOnchainAction(input) {
2630
3083
  return input.htlcLocktime - input.now >= ONCHAIN_CLAIM_MARGIN_SECONDS ? "claim" : "claim_window_closed";
2631
3084
  }
2632
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
+ };
2633
3097
  var notify = (listeners, call) => {
2634
3098
  for (const listener of listeners) {
2635
3099
  try {
@@ -2707,7 +3171,20 @@ var RfqSwapManager = class {
2707
3171
  * answers instead of throwing "not found". Cleared by {@link removeSwap}. */
2708
3172
  finished = /* @__PURE__ */ new Map();
2709
3173
  waiters = /* @__PURE__ */ new Map();
2710
- /** 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`. */
2711
3188
  dirty = /* @__PURE__ */ new Set();
2712
3189
  /** Race guard: one action at a time per swap. */
2713
3190
  inProgress = /* @__PURE__ */ new Set();
@@ -2729,7 +3206,9 @@ var RfqSwapManager = class {
2729
3206
  this.actionExecutedListeners.add(config.events.onActionExecuted);
2730
3207
  }
2731
3208
  }
2732
- /** 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}. */
2733
3212
  setCallbacks(callbacks) {
2734
3213
  this.callbacks = callbacks;
2735
3214
  }
@@ -2749,6 +3228,116 @@ var RfqSwapManager = class {
2749
3228
  this.actionExecutedListeners.add(listener);
2750
3229
  return () => this.actionExecutedListeners.delete(listener);
2751
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
+ }
2752
3341
  /**
2753
3342
  * Load records and begin monitoring. Runs one pass immediately — a caller
2754
3343
  * resuming after a restart may be well past a deadline already — then
@@ -2758,8 +3347,17 @@ var RfqSwapManager = class {
2758
3347
  * Calling it again while running loads the records and returns rather than
2759
3348
  * re-arming — dropping them silently would strand a funded swap on a
2760
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.
2761
3358
  */
2762
3359
  async start(swaps = []) {
3360
+ for (const swap of swaps) await this.admit(swap);
2763
3361
  for (const swap of swaps) {
2764
3362
  if (isRfqSwapTerminal(swap.state)) this.finished.set(swap.rfqId, swap);
2765
3363
  else this.track(swap);
@@ -2791,9 +3389,22 @@ var RfqSwapManager = class {
2791
3389
  this.unsubscribeContracts?.();
2792
3390
  this.unsubscribeContracts = null;
2793
3391
  }
2794
- /** Begin monitoring a swap. Polled immediately when the manager is running,
2795
- * so a just-funded swap does not wait out a whole interval. */
2796
- 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);
2797
3408
  if (isRfqSwapTerminal(swap.state)) {
2798
3409
  this.finished.set(swap.rfqId, swap);
2799
3410
  return;
@@ -2801,6 +3412,29 @@ var RfqSwapManager = class {
2801
3412
  this.track(swap);
2802
3413
  if (this.running) await this.pollSwap(swap);
2803
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
+ }
2804
3438
  /** Forget a swap entirely, monitored or finished.
2805
3439
  *
2806
3440
  * Its contract row is left alone: registration is a wallet-level fact about
@@ -2818,6 +3452,7 @@ var RfqSwapManager = class {
2818
3452
  }
2819
3453
  this.waiters.delete(rfqId);
2820
3454
  this.dirty.delete(rfqId);
3455
+ this.origins.delete(rfqId);
2821
3456
  }
2822
3457
  /** Every swap still being monitored. */
2823
3458
  async getPendingSwaps() {
@@ -2880,7 +3515,7 @@ var RfqSwapManager = class {
2880
3515
  // ── internals ────────────────────────────────────────────────────────────
2881
3516
  track(swap) {
2882
3517
  this.monitored.set(swap.rfqId, swap);
2883
- this.byLockupScript.set(import_base11.hex.encode(swap.lockupPkScript), swap);
3518
+ this.byLockupScript.set(import_base13.hex.encode(swap.lockupPkScript), swap);
2884
3519
  }
2885
3520
  /** Drops the swap from BOTH indexes. The event index is the one that stops
2886
3521
  * a late event finding a swap that is gone; `pollSwap`'s own
@@ -2889,7 +3524,7 @@ var RfqSwapManager = class {
2889
3524
  * them from silently re-driving a cancelled swap. */
2890
3525
  untrack(rfqId) {
2891
3526
  const swap = this.monitored.get(rfqId);
2892
- if (swap) this.byLockupScript.delete(import_base11.hex.encode(swap.lockupPkScript));
3527
+ if (swap) this.byLockupScript.delete(import_base13.hex.encode(swap.lockupPkScript));
2893
3528
  this.monitored.delete(rfqId);
2894
3529
  this.refundRefused.delete(rfqId);
2895
3530
  this.lastClaimError.delete(rfqId);
@@ -2951,7 +3586,7 @@ var RfqSwapManager = class {
2951
3586
  if (!lockup) {
2952
3587
  try {
2953
3588
  const [existing] = await contracts.getContracts({
2954
- script: import_base11.hex.encode(swap.lockupPkScript)
3589
+ script: import_base13.hex.encode(swap.lockupPkScript)
2955
3590
  });
2956
3591
  if (existing) {
2957
3592
  this.registered.set(swap.rfqId, true);
@@ -2970,13 +3605,13 @@ var RfqSwapManager = class {
2970
3605
  );
2971
3606
  return;
2972
3607
  }
2973
- const script = import_base11.hex.encode(lockup.script.pkScript);
2974
- 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)) {
2975
3610
  this.registered.set(swap.rfqId, false);
2976
3611
  this.emitFailed(
2977
3612
  swap,
2978
3613
  new Error(
2979
- `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)}`
2980
3615
  )
2981
3616
  );
2982
3617
  return;
@@ -2995,7 +3630,7 @@ var RfqSwapManager = class {
2995
3630
  * script for its whole life. Best-effort — the swap is over either way. */
2996
3631
  retireContract(swap) {
2997
3632
  if (!this.deps.contracts || !this.registered.get(swap.rfqId)) return;
2998
- 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));
2999
3634
  }
3000
3635
  arm() {
3001
3636
  if (!this.running) return;
@@ -3033,6 +3668,7 @@ var RfqSwapManager = class {
3033
3668
  fate = { fate: "unknown" };
3034
3669
  }
3035
3670
  if (fate.fate === "claimed" || fate.fate === "returned") {
3671
+ this.stampLockupSpends(swap, fate.spends);
3036
3672
  this.setState(swap, fate.fate === "claimed" ? "settled" : "refunded");
3037
3673
  return;
3038
3674
  }
@@ -3117,10 +3753,10 @@ var RfqSwapManager = class {
3117
3753
  );
3118
3754
  }
3119
3755
  }
3120
- if (!this.callbacks) {
3756
+ if (!this.callbacks || !this.callbacks.claimLockup) {
3121
3757
  return this.block(
3122
3758
  swap,
3123
- "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"
3124
3760
  );
3125
3761
  }
3126
3762
  this.setState(swap, "claimable");
@@ -3170,8 +3806,15 @@ var RfqSwapManager = class {
3170
3806
  });
3171
3807
  if (action === "claim" && phase.phase === "claimable") {
3172
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
+ }
3173
3816
  this.setOnchainState(swap, "claimable");
3174
- if (!this.config.enableAutoActions || !this.callbacks) return "handled";
3817
+ if (!this.config.enableAutoActions || !this.callbacks?.claimOnchain) return "handled";
3175
3818
  try {
3176
3819
  const { txid } = await this.callbacks.claimOnchain(swap, phase.utxo);
3177
3820
  swap.claimTxid = txid;
@@ -3284,6 +3927,24 @@ var RfqSwapManager = class {
3284
3927
  if (swap.state !== "needs_counterparty") return;
3285
3928
  this.setState(swap, traderClaimTxid(swap) ? "claimed" : "pending");
3286
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
+ }
3287
3948
  touch(swap) {
3288
3949
  swap.updatedAt = this.config.now();
3289
3950
  this.dirty.add(swap.rfqId);
@@ -3309,9 +3970,29 @@ var RfqSwapManager = class {
3309
3970
  emitAction(swap, action) {
3310
3971
  notify(this.actionExecutedListeners, (listener) => listener(swap, action));
3311
3972
  }
3312
- /** 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
+ */
3313
3993
  async save(swap) {
3314
- if (!this.callbacks) return true;
3994
+ if (!await this.saveRecord(swap)) return false;
3995
+ if (!this.callbacks?.saveSwap) return true;
3315
3996
  try {
3316
3997
  await this.callbacks.saveSwap(swap);
3317
3998
  return true;
@@ -3320,6 +4001,25 @@ var RfqSwapManager = class {
3320
4001
  return false;
3321
4002
  }
3322
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
+ }
3323
4023
  /**
3324
4024
  * Drop a terminal swap from monitoring and report it exactly once.
3325
4025
  *
@@ -3379,6 +4079,8 @@ var errorMessage = (error) => error instanceof Error ? error.message : String(er
3379
4079
  var outpointKey = (vtxo) => `${vtxo.txid}:${vtxo.vout}`;
3380
4080
 
3381
4081
  // src/activity.ts
4082
+ var import_sdk14 = require("@arkade-os/sdk");
4083
+ var import_base14 = require("@scure/base");
3382
4084
  var LABELS = {
3383
4085
  lightning_send: "Lightning send",
3384
4086
  lightning_receive: "Lightning receive",
@@ -3431,6 +4133,44 @@ function swapActivityResolver(deps) {
3431
4133
  }
3432
4134
  };
3433
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
+ }
3434
4174
  // Annotate the CommonJS export names for ESM import in node:
3435
4175
  0 && (module.exports = {
3436
4176
  ARKADE_ASSET,
@@ -3443,6 +4183,7 @@ function swapActivityResolver(deps) {
3443
4183
  LIGHTNING_RECEIVE_PAIR,
3444
4184
  LIGHTNING_SEND_PAIR,
3445
4185
  LockupAmountMismatchError,
4186
+ LockupContractMissing,
3446
4187
  LockupNeedsRecoveryError,
3447
4188
  LockupRegistrationFailed,
3448
4189
  MAX_MIN_CONFIRMATIONS,
@@ -3460,16 +4201,20 @@ function swapActivityResolver(deps) {
3460
4201
  QUOTE_OPTIONS,
3461
4202
  REFUND_MTP_LAG_SECONDS,
3462
4203
  RFQ_RESOLVED_STATES,
4204
+ RFQ_SWAP_RETENTION_SECONDS,
3463
4205
  RFQ_SWAP_TERMINAL_STATES,
3464
4206
  RFQ_TERMINAL_STATES,
3465
4207
  RefundNotLocallyPossibleError,
3466
4208
  RfqSwapManager,
4209
+ RfqSwapOriginRequired,
3467
4210
  SOLO_REFUND_HEADROOM_SECONDS,
3468
4211
  SWAP_LOCKUP_CONTRACT_KIND,
3469
4212
  SWAP_LOCKUP_CONTRACT_LABEL,
3470
4213
  SWAP_LOCKUP_CONTRACT_TYPE,
3471
4214
  SwapRefusal,
3472
4215
  addAssetSwap,
4216
+ arkadeAssetLeg,
4217
+ arkadeRefunder,
3473
4218
  arkadeSwapRequest,
3474
4219
  assertFundable,
3475
4220
  assertReceivable,
@@ -3485,6 +4230,7 @@ function swapActivityResolver(deps) {
3485
4230
  classifyOnchainHtlc,
3486
4231
  classifySpend,
3487
4232
  createOffer,
4233
+ createRfqSwapRecord,
3488
4234
  decodeOffer,
3489
4235
  deriveLightningReceive,
3490
4236
  deriveOnchainReceive,
@@ -3502,6 +4248,7 @@ function swapActivityResolver(deps) {
3502
4248
  lightningReceiveRequest,
3503
4249
  lightningSendRequest,
3504
4250
  lightningSendVtxoScript,
4251
+ lockupContractParams,
3505
4252
  makeCachedFeedFetch,
3506
4253
  newPreimage,
3507
4254
  newRfqId,
@@ -3510,12 +4257,14 @@ function swapActivityResolver(deps) {
3510
4257
  offerVtxoScript,
3511
4258
  onchainHtlcScript,
3512
4259
  onchainReceiveRequest,
4260
+ onchainSendProfile,
3513
4261
  onchainSendRequest,
3514
4262
  paymentHashOf,
3515
4263
  preimageForSwapRecord,
3516
4264
  pushClaim,
3517
4265
  pushRefundWithoutReceiver,
3518
4266
  readLockupFate,
4267
+ rebuildRfqSwap,
3519
4268
  receiveVtxoScript,
3520
4269
  refundIfUnresolved,
3521
4270
  registerLockupContract,
@@ -3526,9 +4275,15 @@ function swapActivityResolver(deps) {
3526
4275
  requestOnchainSend,
3527
4276
  restoreAssetSwaps,
3528
4277
  retireSettledOfferContracts,
4278
+ rfqClaimSecretOf,
3529
4279
  rfqPair,
4280
+ rfqSecretsProfile,
4281
+ rfqSignerOf,
4282
+ rfqSwapActivityInputs,
4283
+ rfqSwapOriginOf,
3530
4284
  sealClaimPacket,
3531
4285
  senderIdentityForSwapRecord,
4286
+ shouldRetainRfqSwap,
3532
4287
  spendTxidsOf,
3533
4288
  spendUpdate,
3534
4289
  swapActivityResolver,
@@ -3539,6 +4294,7 @@ function swapActivityResolver(deps) {
3539
4294
  unilateralRefundWithoutReceiverDelay,
3540
4295
  updateAssetSwap,
3541
4296
  updateAssetSwapBestEffort,
4297
+ updateRfqSwapRecord,
3542
4298
  validatePlan,
3543
4299
  verifyLockupAddress,
3544
4300
  verifyReceiveInvoice,