@arkade-os/swap 0.1.0-rc.11 → 0.1.0-rc.13

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.
Files changed (37) hide show
  1. package/README.md +3 -0
  2. package/dist/advanced.cjs +2057 -1801
  3. package/dist/advanced.d.cts +28 -10
  4. package/dist/advanced.d.ts +28 -10
  5. package/dist/advanced.js +9 -5
  6. package/dist/{chunk-DSRMQCKG.js → chunk-KRXPQQJ7.js} +1110 -88
  7. package/dist/{chunk-4MTMMYYC.js → chunk-SC3SAY3X.js} +147 -73
  8. package/dist/{chunk-WXV23FAY.js → chunk-ZDFMD4FA.js} +215 -937
  9. package/dist/{errors-D7YNbfwK.d.ts → errors-LXMcNhNS.d.ts} +7 -3
  10. package/dist/{errors-B75peUQo.d.cts → errors-XUs8r0LT.d.cts} +7 -3
  11. package/dist/index.cjs +2810 -2558
  12. package/dist/index.d.cts +6 -6
  13. package/dist/index.d.ts +6 -6
  14. package/dist/index.js +8 -8
  15. package/dist/{lockupContract-C7dM1xxn.d.ts → lockupContract-BhMebZCb.d.cts} +7 -10
  16. package/dist/{lockupContract-BwdgOqqe.d.cts → lockupContract-CAiN97fX.d.ts} +7 -10
  17. package/dist/node/index.d.cts +11 -2
  18. package/dist/node/index.d.ts +11 -2
  19. package/dist/nostr.cjs +109 -3
  20. package/dist/nostr.d.cts +1 -1
  21. package/dist/nostr.d.ts +1 -1
  22. package/dist/nostr.js +1 -1
  23. package/dist/protocol.cjs +3665 -3375
  24. package/dist/protocol.d.cts +30 -20
  25. package/dist/protocol.d.ts +30 -20
  26. package/dist/protocol.js +32 -20
  27. package/dist/repositories/realm/index.d.cts +2 -2
  28. package/dist/repositories/realm/index.d.ts +2 -2
  29. package/dist/repositories/sqlite/index.d.cts +2 -2
  30. package/dist/repositories/sqlite/index.d.ts +2 -2
  31. package/dist/{repository-Bi0iq1fK.d.cts → repository-BJLuVFQ0.d.ts} +72 -72
  32. package/dist/{repository-2bubkNM6.d.ts → repository-DsR3b0JJ.d.cts} +72 -72
  33. package/dist/{rfq-o1ncPWXH.d.cts → rfq-BWE8qkV2.d.cts} +133 -12
  34. package/dist/{rfq-o1ncPWXH.d.ts → rfq-BWE8qkV2.d.ts} +133 -12
  35. package/dist/{watch-DG6v5aOM.d.ts → watch-C4ReJR5z.d.cts} +1 -1
  36. package/dist/{watch-CGRIFtBG.d.cts → watch-D8Z78RAV.d.ts} +1 -1
  37. package/package.json +3 -3
package/dist/advanced.cjs CHANGED
@@ -35,1741 +35,1795 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
35
35
  ));
36
36
  var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
37
37
 
38
- // src/lockupContract.ts
39
- async function registerLockupContract(contracts, script, address) {
40
- try {
41
- await contracts.createContract({
42
- type: SWAP_LOCKUP_CONTRACT_TYPE,
43
- params: import_sdk3.VHTLCV2ContractHandler.serializeParams(script.options),
44
- script: import_base3.hex.encode(script.pkScript),
45
- address,
46
- label: SWAP_LOCKUP_CONTRACT_LABEL,
47
- metadata: { genericallySpendable: false, kind: SWAP_LOCKUP_CONTRACT_KIND }
48
- });
49
- } catch (error) {
50
- throw new LockupRegistrationFailed(script, address, error);
38
+ // src/swap-want-asset.program.json
39
+ var swap_want_asset_program_default;
40
+ var init_swap_want_asset_program = __esm({
41
+ "src/swap-want-asset.program.json"() {
42
+ swap_want_asset_program_default = {
43
+ version: 0,
44
+ name: "banco-btc-to-asset",
45
+ params: [
46
+ { name: "makerWP", type: "pubkey" },
47
+ { name: "wantAmount", type: "int" },
48
+ { name: "wantAssetTxid", type: "hash" },
49
+ { name: "wantAssetGroupIndex", type: "int" },
50
+ { name: "server", type: "pubkey" },
51
+ { name: "user", type: "pubkey" }
52
+ ],
53
+ functions: {
54
+ fulfill: {
55
+ tapscript: { signers: ["$server"] },
56
+ arkadeScript: {
57
+ asm: [
58
+ 0,
59
+ "$wantAssetTxid",
60
+ "$wantAssetGroupIndex",
61
+ "INSPECTOUTASSETLOOKUP",
62
+ "VERIFY",
63
+ "$wantAmount",
64
+ "GREATERTHANOREQUAL",
65
+ "VERIFY",
66
+ 0,
67
+ "INSPECTOUTPUTSCRIPTPUBKEY",
68
+ 1,
69
+ "EQUALVERIFY",
70
+ "$makerWP",
71
+ "EQUAL"
72
+ ]
73
+ }
74
+ },
75
+ cancel: {
76
+ tapscript: { signers: ["$user", "$server"] }
77
+ }
78
+ }
79
+ };
80
+ }
81
+ });
82
+
83
+ // src/swap-want-btc.program.json
84
+ var swap_want_btc_program_default;
85
+ var init_swap_want_btc_program = __esm({
86
+ "src/swap-want-btc.program.json"() {
87
+ swap_want_btc_program_default = {
88
+ version: 0,
89
+ name: "banco-asset-to-btc",
90
+ params: [
91
+ { name: "makerWP", type: "pubkey" },
92
+ { name: "wantAmount", type: "int" },
93
+ { name: "server", type: "pubkey" },
94
+ { name: "user", type: "pubkey" }
95
+ ],
96
+ functions: {
97
+ fulfill: {
98
+ tapscript: { signers: ["$server"] },
99
+ arkadeScript: {
100
+ asm: [
101
+ 0,
102
+ "INSPECTOUTPUTVALUE",
103
+ "$wantAmount",
104
+ "GREATERTHANOREQUAL",
105
+ "VERIFY",
106
+ 0,
107
+ "INSPECTOUTPUTSCRIPTPUBKEY",
108
+ 1,
109
+ "EQUALVERIFY",
110
+ "$makerWP",
111
+ "EQUAL"
112
+ ]
113
+ }
114
+ },
115
+ cancel: {
116
+ tapscript: { signers: ["$user", "$server"] }
117
+ }
118
+ }
119
+ };
51
120
  }
121
+ });
122
+
123
+ // src/coverage.ts
124
+ async function serialize(script, task) {
125
+ const previous = inFlight.get(script) ?? Promise.resolve();
126
+ const result = previous.then(task, task);
127
+ const settled = result.then(
128
+ () => {
129
+ },
130
+ () => {
131
+ }
132
+ );
133
+ inFlight.set(script, settled);
134
+ void settled.then(() => {
135
+ if (inFlight.get(script) === settled) inFlight.delete(script);
136
+ });
137
+ return result;
52
138
  }
53
- async function lockupContractParams(contracts, lockupAddress) {
54
- const script = import_base3.hex.encode(import_sdk3.ArkAddress.decode(lockupAddress).pkScript);
55
- const [row] = await contracts.getContracts({ script });
56
- if (!row) throw new LockupContractMissing(lockupAddress, script);
57
- return row.params;
139
+ function addressOutstanding(swaps, script) {
140
+ const issued = issuedAt.get(script);
141
+ if (issued === void 0) return false;
142
+ if (swaps.some((s) => s.swapPkScript === script && s.createdAt >= issued)) {
143
+ issuedAt.delete(script);
144
+ return false;
145
+ }
146
+ return true;
58
147
  }
59
- var import_base3, import_sdk3, SWAP_LOCKUP_CONTRACT_TYPE, SWAP_LOCKUP_CONTRACT_LABEL, SWAP_LOCKUP_CONTRACT_KIND, LockupContractMissing, LockupRegistrationFailed;
60
- var init_lockupContract = __esm({
61
- "src/lockupContract.ts"() {
148
+ async function promoteOfferContract(manager, script, issued = Date.now()) {
149
+ await serialize(script, async () => {
150
+ await manager.setContractWatchState(script, "watched");
151
+ issuedAt.set(script, issued);
152
+ });
153
+ }
154
+ async function retireOfferContract(manager, swaps, script) {
155
+ await serialize(script, async () => {
156
+ if (swaps.some((s) => s.swapPkScript === script && !RETIRABLE.includes(s.status))) return;
157
+ if (addressOutstanding(swaps, script)) return;
158
+ try {
159
+ await manager.setContractWatchState(script, "retained");
160
+ } catch (err) {
161
+ console.warn(`[swap] could not retire offer contract ${script}`, err);
162
+ }
163
+ });
164
+ }
165
+ async function retireSettledOfferContracts(manager, swaps) {
166
+ const settled = new Set(
167
+ swaps.filter((s) => RETIRABLE.includes(s.status)).map((s) => s.swapPkScript)
168
+ );
169
+ for (const script of settled) await retireOfferContract(manager, swaps, script);
170
+ }
171
+ var RETIRABLE, LOCKUP_RETIRABLE, issuedAt, inFlight;
172
+ var init_coverage = __esm({
173
+ "src/coverage.ts"() {
62
174
  "use strict";
63
- import_base3 = require("@scure/base");
64
- import_sdk3 = require("@arkade-os/sdk");
65
- SWAP_LOCKUP_CONTRACT_TYPE = "vhtlc-v2";
66
- SWAP_LOCKUP_CONTRACT_LABEL = "Arkade RFQ swap lockup";
67
- SWAP_LOCKUP_CONTRACT_KIND = "rfq-swap-lockup";
68
- LockupContractMissing = class extends Error {
69
- /** The lockup whose row is absent. */
70
- address;
71
- /** Its pkScript hex — the key the row would have been under. */
72
- script;
73
- constructor(address, script) {
74
- super(
75
- `no contract row for lockup ${address} (script ${script}); its covenant cannot be rebuilt from this wallet's contract store`
175
+ RETIRABLE = ["fulfilled", "cancelled"];
176
+ LOCKUP_RETIRABLE = ["settled", "refunded"];
177
+ issuedAt = /* @__PURE__ */ new Map();
178
+ inFlight = /* @__PURE__ */ new Map();
179
+ }
180
+ });
181
+
182
+ // src/store.ts
183
+ var import_base, import_sha2, import_sdk, BTC_ASSET_ID, byNewest, getAssetSwapsOrThrow, getAssetSwaps, saveSwapOrThrow, updateAssetSwap, updateAssetSwapBestEffort, swapSecretsToRecord, decodeHex32, PreimageNotRecoverableError, preimageForSwapRecord;
184
+ var init_store = __esm({
185
+ "src/store.ts"() {
186
+ "use strict";
187
+ import_base = require("@scure/base");
188
+ import_sha2 = require("@noble/hashes/sha2.js");
189
+ import_sdk = require("@arkade-os/sdk");
190
+ BTC_ASSET_ID = "btc";
191
+ byNewest = (a, b) => b.createdAt - a.createdAt;
192
+ getAssetSwapsOrThrow = async (repository) => {
193
+ return (await repository.getAllSwaps()).filter(
194
+ (s) => s && typeof s.id === "string" && // offer swaps carry the TLV; onchain-corridor swaps carry
195
+ // the payment hash instead — either marks a valid record
196
+ (typeof s.offerHex === "string" || typeof s.paymentHash === "string")
197
+ ).sort(byNewest);
198
+ };
199
+ getAssetSwaps = async (repository) => {
200
+ try {
201
+ return await getAssetSwapsOrThrow(repository);
202
+ } catch {
203
+ return [];
204
+ }
205
+ };
206
+ saveSwapOrThrow = async (repository, swap) => {
207
+ try {
208
+ await repository.saveSwap(swap);
209
+ } catch (error) {
210
+ const reason = error instanceof Error ? error.message : String(error);
211
+ throw new Error(`failed to save swap ${swap.id}: ${reason}`);
212
+ }
213
+ };
214
+ updateAssetSwap = async (repository, id, changes) => {
215
+ const swaps = (await getAssetSwapsOrThrow(repository)).map(
216
+ (s) => s.id === id ? { ...s, ...changes } : s
217
+ );
218
+ const updated = swaps.find((s) => s.id === id);
219
+ if (updated) await saveSwapOrThrow(repository, updated);
220
+ return swaps;
221
+ };
222
+ updateAssetSwapBestEffort = async (repository, id, changes) => {
223
+ try {
224
+ return { swaps: await updateAssetSwap(repository, id, changes), persisted: true };
225
+ } catch (error) {
226
+ console.warn(`[swap] failed to persist update for swap ${id}`, error);
227
+ const swaps = (await getAssetSwaps(repository)).map(
228
+ (s) => s.id === id ? { ...s, ...changes } : s
76
229
  );
77
- this.name = "LockupContractMissing";
78
- this.address = address;
79
- this.script = script;
230
+ return { swaps, persisted: false };
80
231
  }
81
232
  };
82
- LockupRegistrationFailed = class extends Error {
83
- /** The lockup address that was never registered — never fund it: nothing
84
- * is watching it. */
85
- address;
86
- /** The covenant the row would have been written from — the other half of
87
- * `registerLockupContract`, so the write is retryable without a quote. */
88
- script;
89
- constructor(script, address, cause) {
90
- super(`failed to register the lockup contract for ${address}`, { cause });
91
- this.name = "LockupRegistrationFailed";
92
- this.address = address;
93
- this.script = script;
233
+ swapSecretsToRecord = (secrets) => ({
234
+ signingDescriptor: secrets.descriptor,
235
+ ..."mustPersistPreimage" in secrets && secrets.mustPersistPreimage ? { preimageHex: import_base.hex.encode(secrets.preimage) } : {},
236
+ ..."preimageSalt" in secrets && secrets.preimageSalt ? { preimageSaltHex: import_base.hex.encode(secrets.preimageSalt) } : {}
237
+ });
238
+ decodeHex32 = (value, field) => {
239
+ const bytes = import_base.hex.decode(value);
240
+ if (bytes.length !== 32) {
241
+ throw new Error(`${field} must be 32 bytes, got ${bytes.length}`);
242
+ }
243
+ return bytes;
244
+ };
245
+ PreimageNotRecoverableError = class extends Error {
246
+ constructor(reason, message, options) {
247
+ super(message, options);
248
+ this.reason = reason;
249
+ }
250
+ reason;
251
+ name = "PreimageNotRecoverableError";
252
+ };
253
+ preimageForSwapRecord = async (wallet, record) => {
254
+ if (!record.signingDescriptor) {
255
+ throw new PreimageNotRecoverableError(
256
+ "no-secrets",
257
+ "this swap record carries no signing descriptor"
258
+ );
259
+ }
260
+ let stored;
261
+ let salt;
262
+ try {
263
+ stored = record.preimageHex ? decodeHex32(record.preimageHex, "preimageHex") : void 0;
264
+ salt = record.preimageSaltHex ? decodeHex32(record.preimageSaltHex, "preimageSaltHex") : void 0;
265
+ } catch (cause) {
266
+ throw new PreimageNotRecoverableError(
267
+ "malformed-record",
268
+ `this swap record's secrets projection is unreadable: ${String(cause)}`,
269
+ { cause }
270
+ );
271
+ }
272
+ let preimage;
273
+ try {
274
+ preimage = await (0, import_sdk.contractPreimage)(wallet, record.signingDescriptor, { stored, salt });
275
+ } catch (cause) {
276
+ throw new PreimageNotRecoverableError(
277
+ "not-derivable",
278
+ `this wallet cannot produce the preimage for ${record.signingDescriptor}`,
279
+ { cause }
280
+ );
94
281
  }
282
+ if (record.paymentHash && import_base.hex.encode((0, import_sha2.sha256)(preimage)) !== record.paymentHash.toLowerCase()) {
283
+ throw new PreimageNotRecoverableError(
284
+ "hash-mismatch",
285
+ "the derived preimage does not match this swap's payment hash: wrong wallet, or a tampered salt"
286
+ );
287
+ }
288
+ return preimage;
95
289
  };
96
290
  }
97
291
  });
98
292
 
99
- // src/onchainHtlc.ts
100
- function onchainHtlcScript(params, network) {
101
- if (params.claimKey.length !== 32 || params.refundKey.length !== 32) {
102
- throw new Error("claimKey and refundKey must be 32-byte x-only keys");
103
- }
104
- if (!Number.isInteger(params.refundLocktime) || params.refundLocktime <= 0) {
105
- throw new Error(
106
- `refundLocktime must be a positive unix timestamp, got ${params.refundLocktime}`
107
- );
108
- }
109
- if (params.refundLocktime < LOCKTIME_THRESHOLD) {
110
- throw new Error(
111
- `refundLocktime ${params.refundLocktime} is below LOCKTIME_THRESHOLD (${LOCKTIME_THRESHOLD}) and would be interpreted as a block height`
112
- );
113
- }
114
- if (!Object.hasOwn(L1_NETWORKS, network)) {
115
- throw new Error(
116
- `unknown L1 network '${String(network)}' \u2014 expected one of ${Object.keys(L1_NETWORKS).join(", ")}`
117
- );
118
- }
119
- const h160 = h160FromPaymentHash(params.paymentHash);
120
- const claim = btc.Script.encode([
121
- "SIZE",
122
- 32,
123
- "EQUALVERIFY",
124
- "HASH160",
125
- h160,
126
- "EQUALVERIFY",
127
- params.claimKey,
128
- "CHECKSIG"
129
- ]);
130
- const refund = btc.Script.encode([
131
- btc.ScriptNum().encode(BigInt(params.refundLocktime)),
132
- "CHECKLOCKTIMEVERIFY",
133
- "DROP",
134
- params.refundKey,
135
- "CHECKSIG"
136
- ]);
137
- const payment = btc.p2tr(
138
- btc.TAPROOT_UNSPENDABLE_KEY,
139
- btc.taprootListToTree([{ script: claim }, { script: refund }]),
140
- L1_NETWORKS[network],
141
- true
142
- );
143
- const controlBlockFor = (leaf) => {
144
- for (const [block, script] of payment.tapLeafScript ?? []) {
145
- if (script.length - 1 === leaf.length && import_base4.hex.encode(script.subarray(0, leaf.length)) === import_base4.hex.encode(leaf)) {
146
- return btc.TaprootControlBlock.encode(block);
293
+ // src/offer.ts
294
+ function withExitClosure(program, exit) {
295
+ if (!exit) return program;
296
+ return {
297
+ ...program,
298
+ // typed params are authoritative: an undeclared `$exitDelay` fails
299
+ // validateProgram instead of compiling against an unbound value
300
+ params: [...program.params ?? [], { name: "exitDelay", type: "int" }],
301
+ functions: {
302
+ ...program.functions,
303
+ exit: {
304
+ tapscript: { signers: ["$user"], csv: { type: exit.type, value: "$exitDelay" } }
147
305
  }
148
306
  }
149
- throw new Error("leaf missing from compiled taproot tree");
150
- };
151
- return {
152
- address: payment.address,
153
- pkScript: payment.script,
154
- leaves: { claim, refund },
155
- controlBlocks: { claim: controlBlockFor(claim), refund: controlBlockFor(refund) },
156
- paymentHash: params.paymentHash,
157
- refundLocktime: params.refundLocktime
158
307
  };
159
308
  }
160
- function extractPreimage(txHex, paymentHash) {
161
- let raw;
162
- try {
163
- raw = btc.RawTx.decode(import_base4.hex.decode(txHex));
164
- } catch {
165
- return null;
309
+ function swapProgramBinding(offer, operatorPubkey) {
310
+ if (offer.makerPkScript.length !== FIELDS.makerPkScript.width) {
311
+ throw new Error("makerPkScript is not a 34-byte taproot scriptPubKey");
166
312
  }
167
- for (const stack of raw.witnesses ?? []) {
168
- for (const item of stack) {
169
- if (item.length === 32 && import_base4.hex.encode((0, import_sha22.sha256)(item)) === paymentHash) return item;
313
+ return {
314
+ program: withExitClosure(
315
+ offer.wantAsset ? swapPrograms.wantAsset : swapPrograms.wantBtc,
316
+ offer.exitDelay
317
+ ),
318
+ args: {
319
+ makerWP: offer.makerPkScript.subarray(2),
320
+ wantAmount: offer.wantAmount,
321
+ server: operatorPubkey,
322
+ user: offer.makerPublicKey,
323
+ // internal byte order
324
+ ...offer.wantAsset && {
325
+ wantAssetTxid: offer.wantAsset.txid.slice().reverse(),
326
+ wantAssetGroupIndex: offer.wantAsset.groupIndex
327
+ },
328
+ ...offer.exitDelay && { exitDelay: offer.exitDelay.value }
329
+ },
330
+ keys: {
331
+ serverKey: operatorPubkey,
332
+ userKey: offer.makerPublicKey,
333
+ emulatorKey: offer.emulatorPubkey
170
334
  }
171
- }
172
- return null;
335
+ };
173
336
  }
174
- async function claimOnchainFill(chain, input) {
175
- const now = input.now ?? Math.floor(Date.now() / 1e3);
176
- if (input.htlc.refundLocktime - now < ONCHAIN_CLAIM_MARGIN_SECONDS) {
177
- const error = new Error(
178
- "refund leaf opens too soon to claim safely \u2014 take the covenant refund instead"
179
- );
180
- error.reason = "claim_window_closed";
181
- throw error;
182
- }
183
- const spend = await buildHtlcClaim(input);
184
- const txid = await chain.broadcast(spend.txHex);
185
- return { txid, payoutAmount: spend.payoutAmount };
186
- }
187
- async function classifyOnchainHtlc(chain, input) {
188
- const utxos = await chain.getScriptUtxos(input.htlc.pkScript);
189
- const best = utxos.sort((a, b) => b.amount > a.amount ? 1 : -1)[0];
190
- if (!best) {
191
- if (!input.funding) return { phase: "unfunded" };
192
- const spend = await chain.getSpendingTx(
193
- input.funding.txid,
194
- input.funding.vout,
195
- input.htlc.pkScript
196
- );
197
- if (!spend) return { phase: "unfunded" };
198
- const preimage = extractPreimage(spend.txHex, input.htlc.paymentHash);
199
- const txid = btc.Transaction.fromRaw(import_base4.hex.decode(spend.txHex), {
200
- allowUnknownInputs: true,
201
- allowUnknownOutputs: true
202
- }).id;
203
- return preimage ? { phase: "claimed", txid, preimage } : { phase: "swept", txid };
204
- }
205
- if (best.confirmations < input.minConfirmations)
206
- return { phase: "awaiting_confirmations", utxo: best };
207
- const mtp = await chain.getMtp();
208
- if (mtp >= input.htlc.refundLocktime) return { phase: "refundable", utxo: best };
209
- return { phase: "claimable", utxo: best };
337
+ function offerContract(offer, operatorPubkey) {
338
+ const { program, args, keys } = swapProgramBinding(offer, operatorPubkey);
339
+ return new import_sdk2.arkade.ArkadeProgramScript(program, args, keys);
210
340
  }
211
- var import_base4, import_sha22, import_legacy, btc, ONCHAIN_ORDER_MARGIN_SECONDS, ONCHAIN_CLAIM_MARGIN_SECONDS, MAX_MIN_CONFIRMATIONS, LOCKTIME_THRESHOLD, ONCHAIN_SECONDS_PER_BLOCK, ONCHAIN_DUST_SATS, ONCHAIN_CLAIM_VSIZE, paymentHashOf, h160FromPaymentHash, L1_NETWORKS, l1ScriptForAddress, buildLeafSpend, buildHtlcClaim;
212
- var init_onchainHtlc = __esm({
213
- "src/onchainHtlc.ts"() {
214
- "use strict";
215
- import_base4 = require("@scure/base");
216
- import_sha22 = require("@noble/hashes/sha2.js");
217
- import_legacy = require("@noble/hashes/legacy.js");
218
- btc = __toESM(require("@scure/btc-signer"), 1);
219
- ONCHAIN_ORDER_MARGIN_SECONDS = 2 * 60 * 60;
220
- ONCHAIN_CLAIM_MARGIN_SECONDS = 90 * 60;
221
- MAX_MIN_CONFIRMATIONS = 6;
222
- LOCKTIME_THRESHOLD = 5e8;
223
- ONCHAIN_SECONDS_PER_BLOCK = 600;
224
- ONCHAIN_DUST_SATS = BigInt(330);
225
- ONCHAIN_CLAIM_VSIZE = 152;
226
- paymentHashOf = (preimage) => import_base4.hex.encode((0, import_sha22.sha256)(preimage));
227
- h160FromPaymentHash = (paymentHash) => (0, import_legacy.ripemd160)(import_base4.hex.decode(paymentHash));
228
- L1_NETWORKS = {
229
- bitcoin: btc.NETWORK,
230
- testnet: btc.TEST_NETWORK,
231
- regtest: { ...btc.TEST_NETWORK, bech32: "bcrt" }
232
- };
233
- l1ScriptForAddress = (address, network) => btc.OutScript.encode(btc.Address(L1_NETWORKS[network]).decode(address));
234
- buildLeafSpend = async (input) => {
235
- if (!Number.isFinite(input.feeRateSatVb) || input.feeRateSatVb <= 0) {
236
- throw new Error(`feeRateSatVb must be positive, got ${input.feeRateSatVb}`);
237
- }
238
- const assemble = (payout2) => {
239
- const tx2 = new btc.Transaction({ lockTime: input.lockTime ?? 0 });
240
- tx2.addInput({
241
- txid: input.utxo.txid,
242
- index: input.utxo.vout,
243
- witnessUtxo: { script: input.htlc.pkScript, amount: input.utxo.amount },
244
- sequence: input.sequence
245
- });
246
- tx2.addOutput({ script: input.payoutPkScript, amount: payout2 });
247
- return tx2;
248
- };
249
- const witness = (sig2) => [
250
- sig2,
251
- ...input.stackAboveSig,
252
- input.leaf,
253
- input.controlBlock
254
- ];
255
- const sizing = assemble(input.utxo.amount);
256
- sizing.updateInput(0, { finalScriptWitness: witness(new Uint8Array(64)) }, true);
257
- const fee = BigInt(Math.ceil(sizing.vsize * input.feeRateSatVb));
258
- const payout = input.utxo.amount - fee;
259
- if (payout < ONCHAIN_DUST_SATS) {
260
- throw new Error(
261
- `fee ${fee} leaves ${payout} sats from a ${input.utxo.amount} sat HTLC \u2014 below the ${ONCHAIN_DUST_SATS} sat dust limit`
262
- );
263
- }
264
- const tx = assemble(payout);
265
- const sighash = tx.preimageWitnessV1(
266
- 0,
267
- [input.htlc.pkScript],
268
- btc.SigHash.DEFAULT,
269
- [input.utxo.amount],
270
- void 0,
271
- input.leaf,
272
- 192
273
- );
274
- const sig = await input.sign(sighash);
275
- if (sig.length !== 64)
276
- throw new Error(`sign() must return a 64-byte BIP340 signature, got ${sig.length}`);
277
- tx.updateInput(0, { finalScriptWitness: witness(sig) }, true);
278
- return { txHex: tx.hex, txid: tx.id, payoutAmount: payout };
279
- };
280
- buildHtlcClaim = async (input) => {
281
- if (paymentHashOf(input.preimage) !== input.htlc.paymentHash) {
282
- throw new Error("preimage does not hash to the HTLC's payment hash");
283
- }
284
- return buildLeafSpend({
285
- htlc: input.htlc,
286
- utxo: input.utxo,
287
- leaf: input.htlc.leaves.claim,
288
- controlBlock: input.htlc.controlBlocks.claim,
289
- stackAboveSig: [input.preimage],
290
- payoutPkScript: input.payoutPkScript,
291
- feeRateSatVb: input.feeRateSatVb,
292
- sign: input.sign,
293
- sequence: 4294967293
294
- });
295
- };
341
+ function u64(name, value) {
342
+ if (value < BigInt(0) || value >> BigInt(64) > BigInt(0)) {
343
+ throw new Error(`${name} does not fit the offer wire format (u64)`);
296
344
  }
297
- });
298
-
299
- // src/claimPacket.ts
300
- async function sealClaimPacket(input) {
301
- return sealWithEntropy(
302
- input,
303
- import_secp256k1.secp256k1.utils.randomSecretKey(),
304
- crypto.getRandomValues(new Uint8Array(12))
305
- );
345
+ const out = new Uint8Array(FIELDS.wantAmount.width);
346
+ new DataView(out.buffer).setBigUint64(0, value, false);
347
+ return out;
306
348
  }
307
- async function sealWithEntropy(input, ephemeralKey, nonce) {
308
- if (input.preimage.length !== 32) throw new Error("preimage must be 32 bytes");
309
- if (input.covclaimdPubkey.length !== 33) {
310
- throw new Error("covclaimd pubkey must be 33-byte compressed");
311
- }
312
- const ephemeralPub = import_secp256k1.secp256k1.getPublicKey(ephemeralKey, true);
313
- const sharedX = import_secp256k1.secp256k1.getSharedSecret(ephemeralKey, input.covclaimdPubkey, true).subarray(1);
314
- const key = (0, import_hkdf.hkdf)(import_sha23.sha256, sharedX, ephemeralPub, HKDF_INFO, 32);
315
- if (nonce.length !== 12) throw new Error("nonce must be 12 bytes");
316
- const sealed = (0, import_aes.gcm)(key, nonce, ephemeralPub).encrypt(input.preimage);
317
- const ciphertext = new Uint8Array(33 + 12 + sealed.length);
318
- ciphertext.set(ephemeralPub, 0);
319
- ciphertext.set(nonce, 33);
320
- ciphertext.set(sealed, 45);
321
- return {
322
- ciphertext: import_base6.base64.encode(ciphertext),
323
- packet: import_base6.base64.encode(
324
- (0, import_utils2.concatBytes)(
325
- tlv2(TLV_CIPHERTEXT, ciphertext),
326
- tlv2(TLV_COVCLAIMD_PUBKEY, input.covclaimdPubkey)
327
- )
328
- )
329
- };
349
+ function tlv(type, value) {
350
+ if (value.length > 65535) throw new Error("TLV value exceeds the u16 length field");
351
+ return (0, import_utils.concatBytes)(Uint8Array.of(type, value.length >> 8 & 255, value.length & 255), value);
330
352
  }
331
- var import_base6, import_utils2, import_aes, import_secp256k1, import_hkdf, import_sha23, HKDF_INFO, TLV_CIPHERTEXT, TLV_COVCLAIMD_PUBKEY, tlv2;
332
- var init_claimPacket = __esm({
333
- "src/claimPacket.ts"() {
334
- "use strict";
335
- import_base6 = require("@scure/base");
336
- import_utils2 = require("@scure/btc-signer/utils.js");
337
- import_aes = require("@noble/ciphers/aes.js");
338
- import_secp256k1 = require("@noble/curves/secp256k1.js");
339
- import_hkdf = require("@noble/hashes/hkdf.js");
340
- import_sha23 = require("@noble/hashes/sha2.js");
341
- HKDF_INFO = new TextEncoder().encode("covclaimd/preimage/v1");
342
- TLV_CIPHERTEXT = 1;
343
- TLV_COVCLAIMD_PUBKEY = 3;
344
- tlv2 = (type, value) => Uint8Array.from([type, value.length >> 8 & 255, value.length & 255, ...value]);
353
+ function encodeOffer(offer) {
354
+ if (Boolean(offer.wantAsset) === Boolean(offer.offerAsset)) {
355
+ throw new Error("offer must carry exactly one of wantAsset or offerAsset");
345
356
  }
346
- });
347
-
348
- // src/rfq.ts
349
- function lightningSendContract(params) {
350
- const seconds = (value) => ({
351
- type: "seconds",
352
- value: BigInt(value)
353
- });
354
- return new import_sdk5.VHTLC.ScriptV2({
355
- sender: params.senderPubkey,
356
- receiver: params.solverPubkey,
357
- server: params.operatorPubkey,
358
- preimageHash: (0, import_legacy2.ripemd160)(import_base7.hex.decode(params.paymentHash)),
359
- refundLocktime: BigInt(params.refundLocktime),
360
- unilateralClaimDelay: seconds(params.claimDelay),
361
- unilateralRefundDelay: seconds(unilateralRefundDelay(params.claimDelay)),
362
- unilateralRefundWithoutReceiverDelay: seconds(
363
- params.refundWithoutReceiverDelay ?? unilateralRefundWithoutReceiverDelay(params.claimDelay)
364
- ),
365
- nonInteractiveParameters: {
366
- receiverPkScript: params.receiverPkScript,
367
- senderPkScript: params.refundPkScript,
368
- emulatorPubkey: params.emulatorPubkey,
369
- ...params.legacy !== void 0 && { legacy: params.legacy }
357
+ for (const name of [
358
+ "swapPkScript",
359
+ "makerPkScript",
360
+ "makerPublicKey",
361
+ "emulatorPubkey"
362
+ ]) {
363
+ if (offer[name].length !== FIELDS[name].width) {
364
+ throw new Error(`${name} must be ${FIELDS[name].width} bytes`);
370
365
  }
371
- });
372
- }
373
- function deriveLightningSend(input) {
374
- const { quote } = input;
375
- if (quote.refund_locktime === void 0) {
376
- throw new Error("lightning-send quote is missing refund_locktime");
377
366
  }
378
- const refundWithoutReceiverDelay = quote.profile?.refund_without_receiver_delay;
379
- if (refundWithoutReceiverDelay === void 0) {
380
- throw new Error("lightning-send quote is missing profile.refund_without_receiver_delay");
367
+ const ratioNum = setRatio("ratioNum", offer.ratioNum);
368
+ const ratioDen = setRatio("ratioDen", offer.ratioDen);
369
+ if (ratioNum === void 0 !== (ratioDen === void 0)) {
370
+ throw new Error("offer must carry both ratioNum and ratioDen, or neither");
381
371
  }
382
- if (!Number.isSafeInteger(refundWithoutReceiverDelay) || refundWithoutReceiverDelay < input.claimDelay || refundWithoutReceiverDelay % SEQUENCE_GRANULARITY_SECONDS !== 0 || refundWithoutReceiverDelay > 65535 * SEQUENCE_GRANULARITY_SECONDS) {
383
- throw new Error("lightning-send quote carries an invalid refund_without_receiver_delay");
372
+ const recs = [
373
+ tlv(FIELDS.swapPkScript.tag, offer.swapPkScript),
374
+ tlv(FIELDS.wantAmount.tag, u64("wantAmount", offer.wantAmount))
375
+ ];
376
+ if (offer.wantAsset) recs.push(tlv(FIELDS.wantAsset.tag, offer.wantAsset.serialize()));
377
+ if (ratioNum !== void 0) recs.push(tlv(FIELDS.ratioNum.tag, u64("ratioNum", ratioNum)));
378
+ if (ratioDen !== void 0) recs.push(tlv(FIELDS.ratioDen.tag, u64("ratioDen", ratioDen)));
379
+ if (offer.offerAsset) recs.push(tlv(FIELDS.offerAsset.tag, offer.offerAsset.serialize()));
380
+ recs.push(
381
+ tlv(FIELDS.makerPkScript.tag, offer.makerPkScript),
382
+ tlv(FIELDS.makerPublicKey.tag, offer.makerPublicKey),
383
+ tlv(FIELDS.emulatorPubkey.tag, offer.emulatorPubkey)
384
+ );
385
+ if (offer.exitDelay) recs.push(tlv(FIELDS.exitTimelock.tag, encodeExitDelay(offer.exitDelay)));
386
+ return (0, import_utils.concatBytes)(...recs);
387
+ }
388
+ function encodeExitDelay(exit) {
389
+ return (0, import_utils.concatBytes)(
390
+ Uint8Array.of(EXIT_TYPES.indexOf(assertExitDelay(exit).type)),
391
+ u64("exitDelay", exit.value)
392
+ );
393
+ }
394
+ function assertExitDelay(exit) {
395
+ if (EXIT_TYPES.indexOf(exit.type) < 0) {
396
+ throw new Error(`unknown exitDelay locktime type: ${exit.type}`);
384
397
  }
385
- if (refundWithoutReceiverDelay < quote.refund_locktime - input.now) {
386
- throw new Error("lightning-send quote lets the solo refund open before refund_locktime");
398
+ if (exit.value <= BigInt(0)) {
399
+ throw new Error("exitDelay must be a positive relative locktime");
387
400
  }
388
- const receiverPkScriptHex = quote.profile?.receiver_pk_script;
389
- if (receiverPkScriptHex === void 0) {
390
- throw new Error("lightning-send quote is missing profile.receiver_pk_script");
401
+ if (exit.value >> BigInt(32) > BigInt(0)) {
402
+ throw new Error("exitDelay does not fit the locktime field (u32)");
391
403
  }
392
- const contractParams = {
393
- solverPubkey: (0, import_sdk5.toXOnly)(import_base7.hex.decode(quote.solver_pubkey), "solver key"),
394
- refundLocktime: quote.refund_locktime,
395
- operatorPubkey: input.operatorPubkey,
396
- paymentHash: input.paymentHash,
397
- claimDelay: input.claimDelay,
398
- refundWithoutReceiverDelay,
399
- emulatorPubkey: input.emulatorPubkey,
400
- senderPubkey: input.senderPubkey,
401
- receiverPkScript: solverHex(receiverPkScriptHex, "profile.receiver_pk_script"),
402
- refundPkScript: input.refundPkScript
403
- };
404
- const matched = matchQuotedLockup(
405
- quote,
406
- input.hrp,
407
- input.operatorPubkey,
408
- (legacy) => lightningSendContract({ ...contractParams, ...legacy !== void 0 && { legacy } })
409
- );
410
- return {
411
- address: matched.address,
412
- swapPkScript: matched.script.pkScript,
413
- script: matched.script,
414
- contractParams: {
415
- ...contractParams,
416
- ...matched.legacy !== void 0 && { legacy: matched.legacy }
417
- },
418
- refundLocktime: quote.refund_locktime
419
- };
404
+ return exit;
420
405
  }
421
- function deriveOnchainSend(input) {
422
- const { quote } = input;
423
- const profile = quote.profile ?? {};
424
- const refundLocktime = quote.refund_locktime ?? profile.refund_locktime;
425
- const htlcPubkey = profile.htlc_pubkey;
426
- const htlcLocktime = profile.htlc_locktime;
427
- const htlcAddress = profile.htlc_address;
428
- const minConfirmations = profile.min_confirmations;
429
- const receiverPkScriptHex = profile.receiver_pk_script;
430
- if (refundLocktime === void 0 || htlcPubkey === void 0 || htlcLocktime === void 0 || minConfirmations === void 0 || receiverPkScriptHex === void 0) {
431
- throw new Error("onchain-send quote is missing a binding field");
406
+ function decodeOffer(data) {
407
+ const fields = {};
408
+ let off = 0;
409
+ while (off < data.length) {
410
+ if (off + 3 > data.length) throw new Error("truncated TLV header");
411
+ const type = data[off];
412
+ const length = data[off + 1] << 8 | data[off + 2];
413
+ off += 3;
414
+ if (off + length > data.length)
415
+ throw new Error(`truncated TLV value for type 0x${type.toString(16)}`);
416
+ const name = NAMES[type];
417
+ if (!name) throw new Error(`unknown TLV type: 0x${type.toString(16)}`);
418
+ if (fields[name] !== void 0) throw new Error(`duplicate TLV record: ${name}`);
419
+ fields[name] = data.slice(off, off + length);
420
+ off += length;
432
421
  }
433
- const contractParams = {
434
- solverPubkey: (0, import_sdk5.toXOnly)(import_base7.hex.decode(quote.solver_pubkey), "solver key"),
435
- refundLocktime,
436
- operatorPubkey: input.operatorPubkey,
437
- paymentHash: input.paymentHash,
438
- claimDelay: input.claimDelay,
439
- emulatorPubkey: input.emulatorPubkey,
440
- senderPubkey: input.senderPubkey,
441
- receiverPkScript: solverHex(receiverPkScriptHex, "profile.receiver_pk_script"),
442
- refundPkScript: import_sdk5.ArkAddress.decode(input.refundAddress).pkScript
422
+ for (const name of ["wantAsset", "offerAsset"]) {
423
+ if (fields[name]?.length === 0) throw new Error(`missing/invalid ${name}`);
424
+ }
425
+ for (const [name, value] of Object.entries(fields)) {
426
+ const width = FIELDS[name].width;
427
+ if (width !== void 0 && value.length !== width) {
428
+ throw new Error(`missing/invalid ${name}`);
429
+ }
430
+ }
431
+ const need = (name) => {
432
+ const v = fields[name];
433
+ const len = FIELDS[name].width;
434
+ if (!v || len !== void 0 && v.length !== len)
435
+ throw new Error(`missing/invalid ${name}`);
436
+ return v;
443
437
  };
444
- const { script, address } = matchQuotedLockup(
445
- quote,
446
- input.hrp,
447
- input.operatorPubkey,
448
- (legacy) => lightningSendContract({ ...contractParams, ...legacy !== void 0 && { legacy } })
449
- );
450
- const htlcParams = {
451
- paymentHash: input.paymentHash,
452
- claimKey: input.payoutPubkey,
453
- refundKey: (0, import_sdk5.toXOnly)(import_base7.hex.decode(htlcPubkey), "solver L1 htlc key"),
454
- refundLocktime: htlcLocktime
438
+ const amount = need("wantAmount");
439
+ if (Boolean(fields.wantAsset) === Boolean(fields.offerAsset)) {
440
+ throw new Error("offer must carry exactly one of wantAsset or offerAsset");
441
+ }
442
+ const readRatio = (name) => {
443
+ const raw = fields[name];
444
+ if (!raw) return void 0;
445
+ const value = readU64(raw);
446
+ if (value === BigInt(0)) throw new Error(`missing/invalid ${name}`);
447
+ return value;
455
448
  };
456
- const htlc = onchainHtlcScript(htlcParams, input.l1Network);
457
- if (htlc.address !== htlcAddress) throw new AddressMismatch(htlc.address, htlcAddress);
449
+ const ratioNum = readRatio("ratioNum");
450
+ const ratioDen = readRatio("ratioDen");
451
+ if (ratioNum === void 0 !== (ratioDen === void 0)) {
452
+ throw new Error("offer must carry both ratioNum and ratioDen, or neither");
453
+ }
458
454
  return {
459
- address,
460
- swapPkScript: script.pkScript,
461
- script,
462
- htlc,
463
- htlcParams,
464
- l1Network: input.l1Network,
465
- refundLocktime,
466
- htlcLocktime,
467
- minConfirmations
455
+ swapPkScript: need("swapPkScript"),
456
+ wantAmount: readU64(amount),
457
+ ...fields.wantAsset && { wantAsset: import_sdk2.asset.AssetId.fromBytes(fields.wantAsset) },
458
+ ...fields.offerAsset && { offerAsset: import_sdk2.asset.AssetId.fromBytes(fields.offerAsset) },
459
+ makerPkScript: need("makerPkScript"),
460
+ makerPublicKey: need("makerPublicKey"),
461
+ emulatorPubkey: need("emulatorPubkey"),
462
+ ...ratioNum !== void 0 && { ratioNum },
463
+ ...ratioDen !== void 0 && { ratioDen },
464
+ ...fields.exitTimelock && { exitDelay: decodeExitDelay(fields.exitTimelock) }
468
465
  };
469
466
  }
470
- function lightningReceiveContract(params) {
471
- const seconds = (value) => ({
472
- type: "seconds",
473
- value: BigInt(value)
474
- });
475
- return new import_sdk5.VHTLC.ScriptV2({
476
- sender: params.solverPubkey,
477
- receiver: params.payoutPubkey,
478
- server: params.operatorPubkey,
479
- preimageHash: (0, import_legacy2.ripemd160)(import_base7.hex.decode(params.paymentHash)),
480
- refundLocktime: BigInt(params.refundLocktime),
481
- unilateralClaimDelay: seconds(params.claimDelay),
482
- unilateralRefundDelay: seconds(unilateralRefundDelay(params.claimDelay)),
483
- unilateralRefundWithoutReceiverDelay: seconds(
484
- unilateralRefundWithoutReceiverDelay(params.claimDelay)
485
- ),
486
- nonInteractiveParameters: {
487
- receiverPkScript: params.payoutPkScript,
488
- senderPkScript: params.solverRefundPkScript,
489
- emulatorPubkey: params.emulatorPubkey,
490
- ...params.legacy !== void 0 && { legacy: params.legacy }
491
- }
467
+ function decodeExitDelay(value) {
468
+ const type = EXIT_TYPES[value[0]];
469
+ if (!type) throw new Error(`unknown exitDelay locktime type: 0x${value[0].toString(16)}`);
470
+ return { type, value: readU64(value.subarray(1)) };
471
+ }
472
+ async function registerOfferContract(wallet, info, binding, operatorPubkey, expectedPkScript) {
473
+ const contractManager = await wallet.getContractManager();
474
+ const client = await import_sdk2.arkade.Arkade.connect({
475
+ // Registration derives and persists; it never broadcasts and never
476
+ // reads UTXOs, so the only thing the client needs off the server is the
477
+ // info the caller already resolved. Handing that back — rather than a
478
+ // provider built from a URL — is what lets `createOffer` take just a
479
+ // wallet, and spares a second `/v1/info` round-trip.
480
+ arkade: { getInfo: async () => info },
481
+ identity: wallet.identity,
482
+ // without this the row's `address` would be derived against the SDK's
483
+ // default network while its script is right — a row that disagrees with
484
+ // the address the user is about to fund
485
+ network: (0, import_sdk2.networkFromArkadeInfo)(info),
486
+ contractManager
492
487
  });
488
+ await contractManager.createContract(
489
+ offerContractParams(client, binding, operatorPubkey, expectedPkScript)
490
+ );
491
+ await promoteOfferContract(contractManager, import_base2.hex.encode(expectedPkScript));
493
492
  }
494
- function deriveLightningReceive(input) {
495
- const { quote } = input;
496
- const profile = quote.profile ?? {};
497
- const refundLocktime = quote.refund_locktime;
498
- const invoice = profile.invoice;
499
- const solverRefundPkScriptHex = profile.solver_refund_pk_script;
500
- if (refundLocktime === void 0 || invoice === void 0 || solverRefundPkScriptHex === void 0) {
501
- throw new Error("lightning-receive quote is missing a binding field");
493
+ function offerContractParams(client, binding, operatorPubkey, expectedPkScript) {
494
+ const { program, args, keys } = swapProgramBinding(binding, operatorPubkey);
495
+ const contract = new import_sdk2.arkade.ArkadeContract(client, program, args, keys);
496
+ if (import_base2.hex.encode(contract.pkScript) !== import_base2.hex.encode(expectedPkScript)) {
497
+ throw new Error("derived covenant does not match the offer's swapPkScript");
502
498
  }
503
- const contractParams = {
504
- solverPubkey: (0, import_sdk5.toXOnly)(import_base7.hex.decode(quote.solver_pubkey), "solver key"),
505
- refundLocktime,
506
- operatorPubkey: input.operatorPubkey,
507
- paymentHash: input.paymentHash,
508
- claimDelay: input.claimDelay,
509
- emulatorPubkey: input.emulatorPubkey,
510
- solverRefundPkScript: solverHex(solverRefundPkScriptHex, "profile.solver_refund_pk_script"),
511
- payoutPubkey: input.payoutPubkey,
512
- payoutPkScript: import_sdk5.ArkAddress.decode(input.payoutAddress).pkScript
513
- };
514
- const matched = matchQuotedLockup(
515
- quote,
516
- input.hrp,
517
- input.operatorPubkey,
518
- (legacy) => lightningReceiveContract({ ...contractParams, ...legacy !== void 0 && { legacy } })
519
- );
520
499
  return {
521
- address: matched.address,
522
- swapPkScript: matched.script.pkScript,
523
- script: matched.script,
524
- invoice,
525
- refundLocktime,
526
- contractParams: {
527
- ...contractParams,
528
- ...matched.legacy !== void 0 && { legacy: matched.legacy }
529
- }
500
+ ...contract.toContractParams(),
501
+ label: OFFER_CONTRACT_LABEL,
502
+ metadata: { genericallySpendable: false, kind: OFFER_CONTRACT_KIND }
530
503
  };
531
504
  }
532
- var import_base7, import_legacy2, import_sdk5, import_sdk6, solverHex, ARKADE_BTC, LIGHTNING_BTC, ONCHAIN_BTC, rfqPair, LIGHTNING_SEND_PAIR, LIGHTNING_RECEIVE_PAIR, ONCHAIN_SEND_PAIR, ONCHAIN_RECEIVE_PAIR, RFQ_REFUSAL_ERROR_CODES, isRfqRefusalErrorCode, REFUSAL_FIELDS, REFUSAL_UNITS, safeInteger, safeRefusalDetail, refusalMessage, SwapRefusal, AddressMismatch, newRfqId, lightningSendRequest, MIN_HEADROOM_SECONDS, gateError, assertFinite, MAX_PAIR_LENGTH, assertPairLength, verifyLockupAddress, LOCKUP_SHAPE_VARIANTS, matchQuotedLockup, assertFundable, expectQuote, pairOf, SEQUENCE_GRANULARITY_SECONDS, SOLO_REFUND_HEADROOM_SECONDS, unilateralClaimDelay, unilateralRefundDelay, unilateralRefundWithoutReceiverDelay, l1NetworkFromArk, onchainSendRequest, lightningReceiveRequest, MIN_CLAIM_WINDOW_SECONDS, assertReceivable;
533
- var init_rfq = __esm({
534
- "src/rfq.ts"() {
535
- "use strict";
536
- import_base7 = require("@scure/base");
537
- import_legacy2 = require("@noble/hashes/legacy.js");
538
- import_sdk5 = require("@arkade-os/sdk");
539
- init_onchainHtlc();
540
- init_onchainHtlc();
541
- import_sdk6 = require("@arkade-os/sdk");
542
- init_claimPacket();
543
- init_lockupContract();
544
- solverHex = (value, field) => {
545
- try {
546
- return import_base7.hex.decode(value);
547
- } catch {
548
- throw new Error(`solver sent malformed hex for ${field}`);
549
- }
550
- };
551
- ARKADE_BTC = "arkade:BTC";
552
- LIGHTNING_BTC = "lightning:BTC";
553
- ONCHAIN_BTC = "onchain:BTC";
554
- rfqPair = (from, to) => `${from}->${to}`;
555
- LIGHTNING_SEND_PAIR = rfqPair(ARKADE_BTC, LIGHTNING_BTC);
556
- LIGHTNING_RECEIVE_PAIR = rfqPair(LIGHTNING_BTC, ARKADE_BTC);
557
- ONCHAIN_SEND_PAIR = rfqPair(ARKADE_BTC, ONCHAIN_BTC);
558
- ONCHAIN_RECEIVE_PAIR = rfqPair(ONCHAIN_BTC, ARKADE_BTC);
559
- RFQ_REFUSAL_ERROR_CODES = [
560
- "amount_side_unsupported",
561
- "exact_out_unsupported",
562
- "invalid_amount",
563
- "invalid_payout_address",
564
- "invalid_refund_address",
565
- "invoice_amount_mismatch",
566
- "invoice_cltv_too_large",
567
- "invoice_malformed",
568
- "invoice_missing_amount",
569
- "invoice_missing_network",
570
- "invoice_missing_payment_hash",
571
- "invoice_missing_timestamp",
572
- "invoice_mixed_case",
573
- "invoice_sub_satoshi_amount",
574
- "invoice_too_long",
575
- "invoice_wrong_network"
576
- ];
577
- isRfqRefusalErrorCode = (value) => typeof value === "string" && RFQ_REFUSAL_ERROR_CODES.includes(value);
578
- REFUSAL_FIELDS = /* @__PURE__ */ new Set([
579
- "amount",
580
- "amount_side",
581
- "profile.invoice",
582
- "profile.payout_address",
583
- "profile.refund_address"
584
- ]);
585
- REFUSAL_UNITS = /* @__PURE__ */ new Set(["blocks", "characters", "sats"]);
586
- safeInteger = (value) => typeof value === "number" && Number.isSafeInteger(value) && value >= 0 ? value : void 0;
587
- safeRefusalDetail = (detail) => {
588
- if (!isRfqRefusalErrorCode(detail.errorCode)) return {};
589
- return {
590
- errorCode: detail.errorCode,
591
- field: typeof detail.field === "string" && REFUSAL_FIELDS.has(detail.field) ? detail.field : void 0,
592
- actual: safeInteger(detail.actual),
593
- expected: safeInteger(detail.expected),
594
- limit: safeInteger(detail.limit),
595
- unit: typeof detail.unit === "string" && REFUSAL_UNITS.has(detail.unit) ? detail.unit : void 0
596
- };
597
- };
598
- refusalMessage = (reason, detail) => {
599
- if (!detail.errorCode) return `solver refused: ${reason}`;
600
- const where = detail.field ? ` at ${detail.field}` : "";
601
- const unit = detail.unit ? ` ${detail.unit}` : "";
602
- if (detail.actual !== void 0 && detail.limit !== void 0) {
603
- return `solver refused: ${reason} (${detail.errorCode}${where}: ${detail.actual}${unit}, limit ${detail.limit})`;
604
- }
605
- if (detail.actual !== void 0 && detail.expected !== void 0) {
606
- return `solver refused: ${reason} (${detail.errorCode}${where}: ${detail.actual}${unit}, expected ${detail.expected})`;
607
- }
608
- return `solver refused: ${reason} (${detail.errorCode}${where})`;
505
+ function serverExitDelay(delay) {
506
+ if (typeof delay !== "bigint" || delay <= BigInt(0)) {
507
+ throw new Error(
508
+ "the server reports no usable unilateralExitDelay; pass `exitDelay` to set the offer's exit closure explicitly, or `noExit: true` to publish without one"
509
+ );
510
+ }
511
+ return assertExitDelay({ value: delay, type: delay < BigInt(512) ? "blocks" : "seconds" });
512
+ }
513
+ async function createOffer(wallet, params) {
514
+ if (Boolean(params.wantAsset) === Boolean(params.offerAsset)) {
515
+ throw new Error("set exactly one of wantAsset (BTC->asset) or offerAsset (asset->BTC)");
516
+ }
517
+ const [info, makerAddress, makerPublicKey] = await Promise.all([
518
+ // requireLive: this info binds signerPubkey into the covenant — a
519
+ // snapshot could derive an address the operator no longer co-signs
520
+ // for, so an unreachable operator fails the call instead (fail closed,
521
+ // the same behaviour the pre-#734 caller-held provider had)
522
+ wallet.getArkadeInfo({ requireLive: true }),
523
+ wallet.getAddress(),
524
+ wallet.identity.xOnlyPublicKey()
525
+ ]);
526
+ const operatorPubKey = import_base2.hex.decode((0, import_sdk2.toXOnlySignerHex)(info.signerPubkey));
527
+ const network = (0, import_sdk2.networkFromArkadeInfo)(info);
528
+ const emuKey = import_base2.hex.decode(
529
+ (0, import_sdk2.toXOnlySignerHex)((0, import_sdk2.resolveEmulatorPubkey)(network, params.emulatorPubkey))
530
+ );
531
+ const binding = {
532
+ wantAmount: params.wantAmount,
533
+ wantAsset: params.wantAsset,
534
+ offerAsset: params.offerAsset,
535
+ makerPkScript: import_sdk2.ArkAddress.decode(makerAddress).pkScript,
536
+ makerPublicKey,
537
+ emulatorPubkey: emuKey,
538
+ // checked HERE, before the covenant is derived and registered below:
539
+ // deferring it to `encodeOffer` leaves a registered contract behind for
540
+ // an offer that then fails to encode. @see assertExitDelay
541
+ exitDelay: params.noExit ? void 0 : params.exitDelay ? assertExitDelay(params.exitDelay) : serverExitDelay(info.unilateralExitDelay)
542
+ };
543
+ const script = offerContract(binding, operatorPubKey);
544
+ const offer = { ...binding, swapPkScript: script.pkScript };
545
+ await registerOfferContract(wallet, info, binding, operatorPubKey, script.pkScript);
546
+ const payload = encodeOffer(offer);
547
+ return {
548
+ offerHex: import_base2.hex.encode(payload),
549
+ extension: { type: OFFER_PACKET_TYPE, payload },
550
+ // the contract's .address() builds the address; assembling an ArkAddress
551
+ // from tweakedPublicKey here would silently miss any future step it gains
552
+ address: script.address(network.hrp, operatorPubKey).encode(),
553
+ swapPkScript: script.pkScript
554
+ };
555
+ }
556
+ async function prepareOfferCancel(wallet, offerHex, opts) {
557
+ const { fundingTxid, swapAddress } = opts;
558
+ const offer = decodeOffer(import_base2.hex.decode(offerHex));
559
+ const [contractManager, info, reader, broadcaster] = await Promise.all([
560
+ wallet.getContractManager(),
561
+ wallet.getArkadeInfo({ requireLive: true }),
562
+ wallet.getArkadeReader(),
563
+ wallet.getArkadeBroadcaster()
564
+ ]);
565
+ const client = await import_sdk2.arkade.Arkade.connect({
566
+ // info the wallet already holds, plus its broadcast pair — the shape
567
+ // `ArkadeServerProvider` describes, without a second `/v1/info`
568
+ arkade: { getInfo: async () => info, ...broadcaster },
569
+ indexer: reader,
570
+ identity: wallet.identity,
571
+ // registered offers resolve their VTXOs from the contract repository
572
+ // instead of a direct indexer query; the indexer above stays as the
573
+ // fallback for offers created before registration existed
574
+ contractManager
575
+ // no `network`, unlike registerOfferContract: the row lookup is by
576
+ // script and the payout script comes from wallet.getAddress(), so the
577
+ // client's network (which only shapes address derivation) is unused here
578
+ });
579
+ const operatorPubkey = swapAddress ? import_sdk2.ArkAddress.decode(swapAddress).serverPubKey : client.serverKey;
580
+ const { program, args, keys } = swapProgramBinding(offer, operatorPubkey);
581
+ let rebuilt;
582
+ try {
583
+ rebuilt = new import_sdk2.arkade.ArkadeProgramScript(program, args, keys);
584
+ } catch (cause) {
585
+ throw new OfferCovenantMismatchError(import_base2.hex.encode(offer.swapPkScript), { cause });
586
+ }
587
+ if (import_base2.hex.encode(rebuilt.pkScript) !== import_base2.hex.encode(offer.swapPkScript)) {
588
+ throw new OfferCovenantMismatchError(import_base2.hex.encode(offer.swapPkScript));
589
+ }
590
+ const contract = new import_sdk2.arkade.ArkadeContract(client, program, args, keys);
591
+ const [vtxos, makerAddress] = await Promise.all([contract.getUtxos(), wallet.getAddress()]);
592
+ if (!fundingTxid && vtxos.length > 1) {
593
+ throw new Error(
594
+ "multiple spendable deposits at the swap address \u2014 pass fundingTxid to select one"
595
+ );
596
+ }
597
+ const vtxo = fundingTxid ? vtxos.find((v) => v.txid === fundingTxid) : vtxos[0];
598
+ if (!vtxo) throw new NoSpendableDepositError();
599
+ const makerPkScript = import_sdk2.ArkAddress.decode(makerAddress).pkScript;
600
+ const cancel = contract.functions.cancel().from({ txid: vtxo.txid, vout: vtxo.vout, value: vtxo.value }).to(makerPkScript, BigInt(vtxo.value));
601
+ for (const a of vtxo.assets ?? []) {
602
+ cancel.withAsset({
603
+ assetId: a.assetId,
604
+ inputs: [{ vin: 0, amount: BigInt(a.amount) }],
605
+ outputs: [{ vout: 0, amount: BigInt(a.amount) }]
606
+ });
607
+ }
608
+ return {
609
+ offer,
610
+ vtxo: { txid: vtxo.txid, vout: vtxo.vout, value: vtxo.value },
611
+ send: async () => (await cancel.send()).txid
612
+ };
613
+ }
614
+ var import_base2, import_utils, import_sdk2, swapPrograms, OFFER_PACKET_TYPE, FIELDS, EXIT_TYPES, NAMES, readU64, setRatio, OFFER_CONTRACT_LABEL, OFFER_CONTRACT_KIND, OFFER_COVENANT_MISMATCH, NoSpendableDepositError, OfferCovenantMismatchError, ASSET_CARRIER_SATS;
615
+ var init_offer = __esm({
616
+ "src/offer.ts"() {
617
+ "use strict";
618
+ import_base2 = require("@scure/base");
619
+ import_utils = require("@scure/btc-signer/utils.js");
620
+ import_sdk2 = require("@arkade-os/sdk");
621
+ init_swap_want_asset_program();
622
+ init_swap_want_btc_program();
623
+ init_coverage();
624
+ init_store();
625
+ swapPrograms = {
626
+ wantAsset: import_sdk2.arkade.parseArtifact(swap_want_asset_program_default),
627
+ wantBtc: import_sdk2.arkade.parseArtifact(swap_want_btc_program_default)
609
628
  };
610
- SwapRefusal = class extends Error {
611
- /** Literal-typed so the v2 error taxonomy's union discriminates on `name`
612
- * — a `string` here collapses the discriminant for every member. Same value
613
- * the constructor has always set, moved to a field initializer. */
614
- name = "SwapRefusal";
615
- reason;
616
- rfqId;
617
- errorCode;
618
- field;
619
- actual;
620
- expected;
621
- limit;
622
- unit;
623
- constructor(reason, rfqId, detail = {}) {
624
- const safeDetail = safeRefusalDetail(detail);
625
- super(refusalMessage(reason, safeDetail));
626
- this.reason = reason;
627
- this.rfqId = rfqId;
628
- this.errorCode = safeDetail.errorCode;
629
- this.field = safeDetail.field;
630
- this.actual = safeDetail.actual;
631
- this.expected = safeDetail.expected;
632
- this.limit = safeDetail.limit;
633
- this.unit = safeDetail.unit;
634
- }
629
+ OFFER_PACKET_TYPE = 3;
630
+ FIELDS = {
631
+ swapPkScript: { tag: 1, width: 34 },
632
+ wantAmount: { tag: 2, width: 8 },
633
+ wantAsset: { tag: 3, width: void 0 },
634
+ makerPkScript: { tag: 5, width: 34 },
635
+ makerPublicKey: { tag: 7, width: 32 },
636
+ emulatorPubkey: { tag: 8, width: 32 },
637
+ ratioNum: { tag: 9, width: 8 },
638
+ ratioDen: { tag: 10, width: 8 },
639
+ offerAsset: { tag: 11, width: void 0 },
640
+ exitTimelock: { tag: 12, width: 9 }
635
641
  };
636
- AddressMismatch = class extends Error {
637
- derived;
638
- quoted;
639
- constructor(derived, quoted) {
640
- super("solver lockup address does not match local derivation \u2014 refusing to fund");
641
- this.name = "AddressMismatch";
642
- this.derived = derived;
643
- this.quoted = quoted;
644
- }
642
+ EXIT_TYPES = ["blocks", "seconds"];
643
+ NAMES = Object.fromEntries(Object.entries(FIELDS).map(([k, f]) => [f.tag, k]));
644
+ readU64 = (value) => new DataView(value.buffer, value.byteOffset).getBigUint64(0, false);
645
+ setRatio = (name, value) => {
646
+ if (value === void 0 || value === BigInt(0)) return void 0;
647
+ if (value < BigInt(0)) throw new Error(`${name} does not fit the offer wire format (u64)`);
648
+ return value;
645
649
  };
646
- newRfqId = () => import_base7.hex.encode(crypto.getRandomValues(new Uint8Array(32)));
647
- lightningSendRequest = (input) => ({
648
- v: 1,
649
- type: "rfq_request",
650
- rfq_id: input.rfqId,
651
- pair: LIGHTNING_SEND_PAIR,
652
- amount_side: "to",
653
- profile: {
654
- invoice: input.invoice,
655
- refund_address: input.refundAddress,
656
- client_refund_pubkey: import_base7.hex.encode(input.senderPubkey)
650
+ OFFER_CONTRACT_LABEL = "Arkade swap offer";
651
+ OFFER_CONTRACT_KIND = "asset-swap-offer";
652
+ OFFER_COVENANT_MISMATCH = "rebuilt covenant does not match the offer's swapPkScript";
653
+ NoSpendableDepositError = class extends Error {
654
+ name = "NoSpendableDepositError";
655
+ constructor(options) {
656
+ super("no spendable VTXO at the swap address", options);
657
657
  }
658
- });
659
- MIN_HEADROOM_SECONDS = 90 * 60;
660
- gateError = (reason, message) => {
661
- const error = new Error(message);
662
- error.reason = reason;
663
- return error;
664
658
  };
665
- assertFinite = (value, reason, label) => {
666
- if (value !== void 0 && !Number.isFinite(value)) {
667
- throw gateError(reason, `${label} is not a finite number (${String(value)})`);
659
+ OfferCovenantMismatchError = class extends Error {
660
+ constructor(swapPkScript, options) {
661
+ super(
662
+ `${OFFER_COVENANT_MISMATCH} \u2014 the operator signing key pinned by the rebuild does not reproduce the funded covenant; the signing key has likely rotated since funding (pass swapAddress, the funded address, to pin the original key), or the record is corrupt`,
663
+ options
664
+ );
665
+ this.swapPkScript = swapPkScript;
668
666
  }
667
+ swapPkScript;
668
+ name = "OfferCovenantMismatchError";
669
669
  };
670
- MAX_PAIR_LENGTH = 158;
671
- assertPairLength = (pair) => {
672
- if (pair.length > MAX_PAIR_LENGTH) {
673
- throw new Error(
674
- `pair is ${pair.length} characters, over the wire's ${MAX_PAIR_LENGTH}-character limit`
670
+ ASSET_CARRIER_SATS = BigInt(import_sdk2.ASSET_CARRIER_SATS);
671
+ }
672
+ });
673
+
674
+ // src/lockupContract.ts
675
+ async function registerLockupContract(contracts, script, address) {
676
+ try {
677
+ await contracts.createContract({
678
+ type: SWAP_LOCKUP_CONTRACT_TYPE,
679
+ params: import_sdk3.VHTLCV2ContractHandler.serializeParams(script.options),
680
+ script: import_base3.hex.encode(script.pkScript),
681
+ address,
682
+ label: SWAP_LOCKUP_CONTRACT_LABEL,
683
+ metadata: { genericallySpendable: false, kind: SWAP_LOCKUP_CONTRACT_KIND }
684
+ });
685
+ } catch (error) {
686
+ throw new LockupRegistrationFailed(script, address, error);
687
+ }
688
+ }
689
+ async function lockupContractParams(contracts, lockupAddress) {
690
+ const script = import_base3.hex.encode(import_sdk3.ArkAddress.decode(lockupAddress).pkScript);
691
+ const [row] = await contracts.getContracts({ script });
692
+ if (!row) throw new LockupContractMissing(lockupAddress, script);
693
+ return row.params;
694
+ }
695
+ var import_base3, import_sdk3, SWAP_LOCKUP_CONTRACT_TYPE, SWAP_LOCKUP_CONTRACT_LABEL, SWAP_LOCKUP_CONTRACT_KIND, LockupContractMissing, LockupRegistrationFailed;
696
+ var init_lockupContract = __esm({
697
+ "src/lockupContract.ts"() {
698
+ "use strict";
699
+ import_base3 = require("@scure/base");
700
+ import_sdk3 = require("@arkade-os/sdk");
701
+ SWAP_LOCKUP_CONTRACT_TYPE = "vhtlc-v2";
702
+ SWAP_LOCKUP_CONTRACT_LABEL = "Arkade RFQ swap lockup";
703
+ SWAP_LOCKUP_CONTRACT_KIND = "rfq-swap-lockup";
704
+ LockupContractMissing = class extends Error {
705
+ /** The lockup whose row is absent. */
706
+ address;
707
+ /** Its pkScript hex — the key the row would have been under. */
708
+ script;
709
+ constructor(address, script) {
710
+ super(
711
+ `no contract row for lockup ${address} (script ${script}); its covenant cannot be rebuilt from this wallet's contract store`
675
712
  );
713
+ this.name = "LockupContractMissing";
714
+ this.address = address;
715
+ this.script = script;
676
716
  }
677
717
  };
678
- verifyLockupAddress = (quote, derivedAddress) => {
679
- const quoted = quote.profile?.lockup_address;
680
- const candidates = Array.isArray(derivedAddress) ? derivedAddress : [derivedAddress];
681
- const matched = candidates.find((address) => address === quoted);
682
- if (matched === void 0) throw new AddressMismatch(candidates, quoted);
683
- return matched;
684
- };
685
- LOCKUP_SHAPE_VARIANTS = [void 0, "preTimelockedRefund"];
686
- matchQuotedLockup = (quote, hrp, serverPubkey, build) => {
687
- const candidates = LOCKUP_SHAPE_VARIANTS.map((legacy) => {
688
- const script = build(legacy);
689
- return { script, address: script.address(hrp, serverPubkey).encode(), legacy };
690
- });
691
- const matchedAddress = verifyLockupAddress(
692
- quote,
693
- candidates.map((candidate) => candidate.address)
694
- );
695
- return candidates.find((candidate) => candidate.address === matchedAddress);
696
- };
697
- assertFundable = (input) => {
698
- const fail = (reason, message) => {
699
- throw gateError(reason, message);
700
- };
701
- if (input.quote.valid_until === void 0) {
702
- fail("quote_malformed", "quote carries no valid_until");
703
- }
704
- assertFinite(input.quote.valid_until, "quote_malformed", "quote valid_until");
705
- if (input.invoiceExpiresAt !== void 0 && input.now >= input.invoiceExpiresAt) {
706
- fail("invoice_expired", "invoice expired");
707
- }
708
- if (input.now >= input.quote.valid_until)
709
- fail("quote_expired", "quote expired \u2014 request a fresh one");
710
- if (input.quote.refund_locktime !== void 0 && input.quote.refund_locktime - input.now < MIN_HEADROOM_SECONDS) {
711
- fail("insufficient_headroom", "refund deadline headroom below 90 minutes");
712
- }
713
- if (input.maxFee) {
714
- const { bps, sats, referenceRate } = input.maxFee;
715
- if (bps === void 0 && sats === void 0) {
716
- fail("max_fee_unbounded", "maxFee names neither bps nor sats");
717
- }
718
- if (bps !== void 0 && (!Number.isInteger(bps) || bps < 0 || bps > 1e4)) {
719
- fail("max_fee_out_of_range", `maxFee.bps must be an integer in 0..10000, got ${bps}`);
720
- }
721
- if (sats !== void 0 && (!Number.isInteger(sats) || sats < 0)) {
722
- fail("max_fee_out_of_range", `maxFee.sats must be a non-negative integer, got ${sats}`);
723
- }
724
- const legs = input.quote.pair.split("->");
725
- const assetOf2 = (leg) => leg.slice(leg.indexOf(":") + 1);
726
- const sameAsset2 = legs.length === 2 && assetOf2(legs[0]) === assetOf2(legs[1]);
727
- if (!sameAsset2 && referenceRate === void 0) {
728
- fail(
729
- "fee_gate_unavailable",
730
- `maxFee cannot gate ${input.quote.pair}: its legs name different assets, so from_amount - to_amount is not a fee. Supply maxFee.referenceRate (to-units per from-unit) from a source of your OWN \u2014 reading it off the solver's published feed would check the solver against its own number`
731
- );
732
- }
733
- if (!sameAsset2 && (!Number.isFinite(referenceRate) || referenceRate <= 0)) {
734
- fail(
735
- "max_fee_out_of_range",
736
- `maxFee.referenceRate must be a positive finite number, got ${referenceRate}`
737
- );
738
- }
739
- const fee = sameAsset2 ? input.quote.from_amount - input.quote.to_amount : Math.ceil(
740
- (input.quote.from_amount * referenceRate - input.quote.to_amount) / referenceRate
741
- );
742
- const allowed = Math.max(
743
- sats ?? 0,
744
- Math.floor(input.quote.from_amount * (bps ?? 0) / 1e4)
745
- );
746
- if (fee > allowed) {
747
- fail("fee_too_high", `fee ${fee} exceeds the ${allowed} this client allows`);
748
- }
749
- }
750
- if (input.onchain) {
751
- const { htlcLocktime, minConfirmations, direction } = input.onchain;
752
- if (!Number.isInteger(minConfirmations) || minConfirmations < 1 || minConfirmations > MAX_MIN_CONFIRMATIONS) {
753
- fail(
754
- "confirmations_out_of_range",
755
- `min_confirmations must be 1..${MAX_MIN_CONFIRMATIONS}, got ${minConfirmations}`
756
- );
757
- }
758
- const needed = minConfirmations * ONCHAIN_SECONDS_PER_BLOCK + ONCHAIN_CLAIM_MARGIN_SECONDS;
759
- if (htlcLocktime - input.now <= needed) {
760
- fail("claim_window_too_short", "L1 HTLC locktime leaves no safe claim window");
761
- }
762
- if (direction === "send") {
763
- if (input.quote.refund_locktime === void 0 || htlcLocktime + ONCHAIN_ORDER_MARGIN_SECONDS > input.quote.refund_locktime) {
764
- fail(
765
- "timelock_order",
766
- "L1 HTLC locktime + margin must fall before the Arkade refund locktime"
767
- );
768
- }
769
- }
770
- }
771
- };
772
- expectQuote = (payload, rfqId, requestedPair) => {
773
- const p = payload;
774
- if (p?.type === "rfq_refusal") {
775
- throw new SwapRefusal(p.reason ?? "unknown", p.rfq_id ?? rfqId, {
776
- errorCode: p.error_code,
777
- field: p.field,
778
- actual: p.actual,
779
- expected: p.expected,
780
- limit: p.limit,
781
- unit: p.unit
782
- });
783
- }
784
- if (p?.type !== "rfq_quote" || p.rfq_id !== rfqId) {
785
- throw new Error(`unexpected reply: ${p?.type ?? "no payload"}`);
786
- }
787
- if (requestedPair !== void 0 && p.pair !== requestedPair) {
788
- throw new Error(
789
- `solver quoted ${JSON.stringify(p.pair)}, not the requested ${requestedPair}`
790
- );
791
- }
792
- return payload;
793
- };
794
- pairOf = (payload) => typeof payload.pair === "string" ? payload.pair : void 0;
795
- SEQUENCE_GRANULARITY_SECONDS = 512;
796
- SOLO_REFUND_HEADROOM_SECONDS = 8 * SEQUENCE_GRANULARITY_SECONDS;
797
- unilateralClaimDelay = (operatorExitDelaySeconds) => {
798
- if (!Number.isFinite(operatorExitDelaySeconds) || operatorExitDelaySeconds < SEQUENCE_GRANULARITY_SECONDS) {
799
- throw new Error(
800
- `operator exit delay must be at least ${SEQUENCE_GRANULARITY_SECONDS}s of seconds, got ${operatorExitDelaySeconds}`
801
- );
802
- }
803
- if (operatorExitDelaySeconds > 65535 * SEQUENCE_GRANULARITY_SECONDS - SOLO_REFUND_HEADROOM_SECONDS) {
804
- throw new Error(
805
- `operator exit delay ${operatorExitDelaySeconds}s exceeds what BIP68 can encode once the solo refund's headroom is stacked above it`
806
- );
807
- }
808
- return Math.ceil(operatorExitDelaySeconds / SEQUENCE_GRANULARITY_SECONDS) * SEQUENCE_GRANULARITY_SECONDS;
809
- };
810
- unilateralRefundDelay = (claimDelay) => claimDelay;
811
- unilateralRefundWithoutReceiverDelay = (claimDelay) => claimDelay + SOLO_REFUND_HEADROOM_SECONDS;
812
- l1NetworkFromArk = (network) => network === "bitcoin" ? "bitcoin" : network === "regtest" ? "regtest" : "testnet";
813
- onchainSendRequest = (input) => ({
814
- v: 1,
815
- type: "rfq_request",
816
- rfq_id: input.rfqId,
817
- pair: ONCHAIN_SEND_PAIR,
818
- amount_side: input.amountSide,
819
- amount: input.amount,
820
- profile: {
821
- payment_hash: input.paymentHash,
822
- payout_pubkey: import_base7.hex.encode(input.payoutPubkey),
823
- refund_address: input.refundAddress,
824
- client_refund_pubkey: import_base7.hex.encode(input.senderPubkey)
825
- }
826
- });
827
- lightningReceiveRequest = (input) => ({
828
- v: 1,
829
- type: "rfq_request",
830
- rfq_id: input.rfqId,
831
- pair: LIGHTNING_RECEIVE_PAIR,
832
- amount_side: input.amountSide,
833
- amount: input.amount,
834
- profile: {
835
- payment_hash: input.paymentHash,
836
- payout_address: input.payoutAddress,
837
- payout_pubkey: import_base7.hex.encode(input.payoutPubkey),
838
- ...input.claimPacket === void 0 ? {} : { claim_packet: input.claimPacket }
839
- }
840
- });
841
- MIN_CLAIM_WINDOW_SECONDS = 30 * 60;
842
- assertReceivable = (input) => {
843
- assertFinite(input.payDeadline, "quote_malformed", "payDeadline");
844
- assertFinite(input.now, "invalid_gate_input", "now");
845
- assertFinite(input.minClaimWindowSeconds, "invalid_gate_input", "minClaimWindowSeconds");
846
- assertFinite(input.maxPayAmount, "invalid_gate_input", "maxPayAmount");
847
- const minClaimWindow = input.minClaimWindowSeconds ?? MIN_CLAIM_WINDOW_SECONDS;
848
- if (input.now >= input.payDeadline) {
849
- throw gateError("quote_expired", "quote or invoice already expired \u2014 request a fresh one");
850
- }
851
- if (input.quote.refund_locktime === void 0) {
852
- throw gateError("missing_refund_locktime", "receive quote carries no refund_locktime");
853
- }
854
- assertFinite(input.quote.refund_locktime, "quote_malformed", "quote refund_locktime");
855
- if (input.quote.refund_locktime - input.payDeadline < minClaimWindow) {
856
- throw gateError(
857
- "claim_window_too_short",
858
- `a payment at the deadline would leave under ${minClaimWindow}s to claim before the solver's refund opens`
859
- );
860
- }
861
- if (input.maxPayAmount !== void 0 && input.quote.from_amount > input.maxPayAmount) {
862
- throw gateError(
863
- "price_too_high",
864
- `quote asks ${input.quote.from_amount} sats, above the ${input.maxPayAmount} ceiling`
865
- );
718
+ LockupRegistrationFailed = class extends Error {
719
+ /** The lockup address that was never registered — never fund it: nothing
720
+ * is watching it. */
721
+ address;
722
+ /** The covenant the row would have been written from — the other half of
723
+ * `registerLockupContract`, so the write is retryable without a quote. */
724
+ script;
725
+ constructor(script, address, cause) {
726
+ super(`failed to register the lockup contract for ${address}`, { cause });
727
+ this.name = "LockupRegistrationFailed";
728
+ this.address = address;
729
+ this.script = script;
866
730
  }
867
731
  };
868
732
  }
869
733
  });
870
734
 
871
- // src/nostr.ts
872
- var nostr_exports = {};
873
- __export(nostr_exports, {
874
- RFQ_AD_KIND: () => RFQ_AD_KIND,
875
- RFQ_DIRECTED_KIND: () => RFQ_DIRECTED_KIND,
876
- RelayUnavailable: () => RelayUnavailable,
877
- TransportClosed: () => TransportClosed,
878
- nostrRfqTransport: () => nostrRfqTransport
879
- });
880
- var import_nostr_tools, RFQ_DIRECTED_KIND, RFQ_AD_KIND, DEFAULT_TIMEOUT_MS, RelayUnavailable, TransportClosed, closeReasons, nostrRfqTransport;
881
- var init_nostr = __esm({
882
- "src/nostr.ts"() {
883
- "use strict";
884
- init_rfq();
885
- import_nostr_tools = require("nostr-tools");
886
- RFQ_DIRECTED_KIND = 24859;
887
- RFQ_AD_KIND = 38859;
888
- DEFAULT_TIMEOUT_MS = 3e4;
889
- RelayUnavailable = class extends Error {
890
- reasons;
891
- constructor(reasons) {
892
- super(`lost every relay connection: ${reasons.join("; ") || "connection closed"}`);
893
- this.name = "RelayUnavailable";
894
- this.reasons = reasons;
895
- }
896
- };
897
- TransportClosed = class extends Error {
898
- constructor() {
899
- super("transport closed before the solver replied");
900
- this.name = "TransportClosed";
901
- }
902
- };
903
- closeReasons = (raw) => raw.map((entry) => {
904
- if (typeof entry === "string") return entry;
905
- const { url, reason } = entry ?? {};
906
- return url ? `${url}: ${reason ?? "closed"}` : reason ?? "closed";
907
- });
908
- nostrRfqTransport = (options) => {
909
- const relays = options.relays;
910
- const solverPubkey = options.solverPubkey;
911
- const secretKey = options.secretKey ?? (0, import_nostr_tools.generateSecretKey)();
912
- const pubkey = (0, import_nostr_tools.getPublicKey)(secretKey);
913
- const pool = options.pool ?? new import_nostr_tools.SimplePool();
914
- const ownsPool = !options.pool;
915
- const timeoutMs = options.timeoutMs ?? DEFAULT_TIMEOUT_MS;
916
- const conversationKey = import_nostr_tools.nip44.v2.utils.getConversationKey(secretKey, solverPubkey);
917
- let closed = false;
918
- const waiters = /* @__PURE__ */ new Map();
919
- const subscription = pool.subscribeMany(
920
- relays,
921
- { kinds: [RFQ_DIRECTED_KIND], "#p": [pubkey], authors: [solverPubkey] },
922
- {
923
- onevent(event) {
924
- let payload;
925
- try {
926
- payload = JSON.parse(import_nostr_tools.nip44.v2.decrypt(event.content, conversationKey));
927
- } catch {
928
- return;
929
- }
930
- const rfqId = payload?.rfq_id;
931
- if (!rfqId) return;
932
- waiters.get(rfqId)?.resolve(payload);
933
- },
934
- // subscribeMany calls this once every relay has closed. Nothing will
935
- // arrive after it, so failing now beats waiting out the timeout — and
936
- // it names the real cause instead of implicating the solver.
937
- onclose(reasons) {
938
- if (closed) return;
939
- const error = new RelayUnavailable(closeReasons(reasons));
940
- for (const waiter of waiters.values()) waiter.reject(error);
941
- waiters.clear();
942
- }
943
- }
944
- );
945
- const send = async (payload) => {
946
- const event = (0, import_nostr_tools.finalizeEvent)(
947
- {
948
- kind: RFQ_DIRECTED_KIND,
949
- created_at: Math.floor(Date.now() / 1e3),
950
- tags: [["p", solverPubkey]],
951
- content: import_nostr_tools.nip44.v2.encrypt(JSON.stringify(payload), conversationKey)
952
- },
953
- secretKey
954
- );
955
- const results = await Promise.allSettled(pool.publish(relays, event));
956
- if (!results.some((r) => r.status === "fulfilled")) {
957
- throw new Error("no relay accepted the RFQ message");
958
- }
959
- };
960
- const awaitReply = (rfqId) => new Promise((resolve, reject) => {
961
- const timer = setTimeout(() => {
962
- waiters.delete(rfqId);
963
- reject(new Error(`no solver reply within ${timeoutMs}ms`));
964
- }, timeoutMs);
965
- waiters.set(rfqId, {
966
- resolve: (payload) => {
967
- clearTimeout(timer);
968
- waiters.delete(rfqId);
969
- resolve(payload);
970
- },
971
- reject: (error) => {
972
- clearTimeout(timer);
973
- waiters.delete(rfqId);
974
- reject(error);
975
- }
735
+ // src/onchainHtlc.ts
736
+ function onchainHtlcScript(params, network) {
737
+ if (params.claimKey.length !== 32 || params.refundKey.length !== 32) {
738
+ throw new Error("claimKey and refundKey must be 32-byte x-only keys");
739
+ }
740
+ if (!Number.isInteger(params.refundLocktime) || params.refundLocktime <= 0) {
741
+ throw new Error(
742
+ `refundLocktime must be a positive unix timestamp, got ${params.refundLocktime}`
743
+ );
744
+ }
745
+ if (params.refundLocktime < LOCKTIME_THRESHOLD) {
746
+ throw new Error(
747
+ `refundLocktime ${params.refundLocktime} is below LOCKTIME_THRESHOLD (${LOCKTIME_THRESHOLD}) and would be interpreted as a block height`
748
+ );
749
+ }
750
+ if (!Object.hasOwn(L1_NETWORKS, network)) {
751
+ throw new Error(
752
+ `unknown L1 network '${String(network)}' \u2014 expected one of ${Object.keys(L1_NETWORKS).join(", ")}`
753
+ );
754
+ }
755
+ const h160 = h160FromPaymentHash(params.paymentHash);
756
+ const claim = btc.Script.encode([
757
+ "SIZE",
758
+ 32,
759
+ "EQUALVERIFY",
760
+ "HASH160",
761
+ h160,
762
+ "EQUALVERIFY",
763
+ params.claimKey,
764
+ "CHECKSIG"
765
+ ]);
766
+ const refund = btc.Script.encode([
767
+ btc.ScriptNum().encode(BigInt(params.refundLocktime)),
768
+ "CHECKLOCKTIMEVERIFY",
769
+ "DROP",
770
+ params.refundKey,
771
+ "CHECKSIG"
772
+ ]);
773
+ const payment = btc.p2tr(
774
+ btc.TAPROOT_UNSPENDABLE_KEY,
775
+ btc.taprootListToTree([{ script: claim }, { script: refund }]),
776
+ L1_NETWORKS[network],
777
+ true
778
+ );
779
+ const controlBlockFor = (leaf) => {
780
+ for (const [block, script] of payment.tapLeafScript ?? []) {
781
+ if (script.length - 1 === leaf.length && import_base4.hex.encode(script.subarray(0, leaf.length)) === import_base4.hex.encode(leaf)) {
782
+ return btc.TaprootControlBlock.encode(block);
783
+ }
784
+ }
785
+ throw new Error("leaf missing from compiled taproot tree");
786
+ };
787
+ return {
788
+ address: payment.address,
789
+ pkScript: payment.script,
790
+ leaves: { claim, refund },
791
+ controlBlocks: { claim: controlBlockFor(claim), refund: controlBlockFor(refund) },
792
+ paymentHash: params.paymentHash,
793
+ refundLocktime: params.refundLocktime
794
+ };
795
+ }
796
+ function extractPreimage(txHex, paymentHash) {
797
+ let raw;
798
+ try {
799
+ raw = btc.RawTx.decode(import_base4.hex.decode(txHex));
800
+ } catch {
801
+ return null;
802
+ }
803
+ for (const stack of raw.witnesses ?? []) {
804
+ for (const item of stack) {
805
+ if (item.length === 32 && import_base4.hex.encode((0, import_sha22.sha256)(item)) === paymentHash) return item;
806
+ }
807
+ }
808
+ return null;
809
+ }
810
+ async function claimOnchainFill(chain, input) {
811
+ const now = input.now ?? Math.floor(Date.now() / 1e3);
812
+ if (input.htlc.refundLocktime - now < ONCHAIN_CLAIM_MARGIN_SECONDS) {
813
+ const error = new Error(
814
+ "refund leaf opens too soon to claim safely \u2014 take the covenant refund instead"
815
+ );
816
+ error.reason = "claim_window_closed";
817
+ throw error;
818
+ }
819
+ const spend = await buildHtlcClaim(input);
820
+ const txid = await chain.broadcast(spend.txHex);
821
+ return { txid, payoutAmount: spend.payoutAmount };
822
+ }
823
+ async function classifyOnchainHtlc(chain, input) {
824
+ const utxos = await chain.getScriptUtxos(input.htlc.pkScript);
825
+ const best = utxos.sort((a, b) => b.amount > a.amount ? 1 : -1)[0];
826
+ if (!best) {
827
+ if (!input.funding) return { phase: "unfunded" };
828
+ const spend = await chain.getSpendingTx(
829
+ input.funding.txid,
830
+ input.funding.vout,
831
+ input.htlc.pkScript
832
+ );
833
+ if (!spend) return { phase: "unfunded" };
834
+ const preimage = extractPreimage(spend.txHex, input.htlc.paymentHash);
835
+ const txid = btc.Transaction.fromRaw(import_base4.hex.decode(spend.txHex), {
836
+ allowUnknownInputs: true,
837
+ allowUnknownOutputs: true
838
+ }).id;
839
+ return preimage ? { phase: "claimed", txid, preimage } : { phase: "swept", txid };
840
+ }
841
+ if (best.confirmations < input.minConfirmations)
842
+ return { phase: "awaiting_confirmations", utxo: best };
843
+ const mtp = await chain.getMtp();
844
+ if (mtp >= input.htlc.refundLocktime) return { phase: "refundable", utxo: best };
845
+ return { phase: "claimable", utxo: best };
846
+ }
847
+ var import_base4, import_sha22, import_legacy, btc, ONCHAIN_ORDER_MARGIN_SECONDS, ONCHAIN_CLAIM_MARGIN_SECONDS, MAX_MIN_CONFIRMATIONS, LOCKTIME_THRESHOLD, ONCHAIN_SECONDS_PER_BLOCK, ONCHAIN_DUST_SATS, ONCHAIN_CLAIM_VSIZE, paymentHashOf, h160FromPaymentHash, L1_NETWORKS, l1ScriptForAddress, buildLeafSpend, buildHtlcClaim;
848
+ var init_onchainHtlc = __esm({
849
+ "src/onchainHtlc.ts"() {
850
+ "use strict";
851
+ import_base4 = require("@scure/base");
852
+ import_sha22 = require("@noble/hashes/sha2.js");
853
+ import_legacy = require("@noble/hashes/legacy.js");
854
+ btc = __toESM(require("@scure/btc-signer"), 1);
855
+ ONCHAIN_ORDER_MARGIN_SECONDS = 2 * 60 * 60;
856
+ ONCHAIN_CLAIM_MARGIN_SECONDS = 90 * 60;
857
+ MAX_MIN_CONFIRMATIONS = 6;
858
+ LOCKTIME_THRESHOLD = 5e8;
859
+ ONCHAIN_SECONDS_PER_BLOCK = 600;
860
+ ONCHAIN_DUST_SATS = BigInt(330);
861
+ ONCHAIN_CLAIM_VSIZE = 152;
862
+ paymentHashOf = (preimage) => import_base4.hex.encode((0, import_sha22.sha256)(preimage));
863
+ h160FromPaymentHash = (paymentHash) => (0, import_legacy.ripemd160)(import_base4.hex.decode(paymentHash));
864
+ L1_NETWORKS = {
865
+ bitcoin: btc.NETWORK,
866
+ testnet: btc.TEST_NETWORK,
867
+ regtest: { ...btc.TEST_NETWORK, bech32: "bcrt" }
868
+ };
869
+ l1ScriptForAddress = (address, network) => btc.OutScript.encode(btc.Address(L1_NETWORKS[network]).decode(address));
870
+ buildLeafSpend = async (input) => {
871
+ if (!Number.isFinite(input.feeRateSatVb) || input.feeRateSatVb <= 0) {
872
+ throw new Error(`feeRateSatVb must be positive, got ${input.feeRateSatVb}`);
873
+ }
874
+ const assemble = (payout2) => {
875
+ const tx2 = new btc.Transaction({ lockTime: input.lockTime ?? 0 });
876
+ tx2.addInput({
877
+ txid: input.utxo.txid,
878
+ index: input.utxo.vout,
879
+ witnessUtxo: { script: input.htlc.pkScript, amount: input.utxo.amount },
880
+ sequence: input.sequence
976
881
  });
977
- });
978
- return {
979
- async requestQuote(payload) {
980
- const rfqId = String(payload.rfq_id);
981
- const reply = awaitReply(rfqId);
982
- await send(payload);
983
- return expectQuote(await reply, rfqId, pairOf(payload));
984
- },
985
- async status(rfqId) {
986
- const reply = awaitReply(rfqId);
987
- await send({ v: 1, type: "rfq_status_request", rfq_id: rfqId });
988
- const payload = await reply;
989
- if (payload?.type !== "rfq_status") return null;
990
- return payload;
991
- },
992
- async close() {
993
- closed = true;
994
- for (const waiter of waiters.values()) waiter.reject(new TransportClosed());
995
- waiters.clear();
996
- if (ownsPool) pool.close(relays);
997
- else subscription.close();
998
- }
882
+ tx2.addOutput({ script: input.payoutPkScript, amount: payout2 });
883
+ return tx2;
999
884
  };
885
+ const witness = (sig2) => [
886
+ sig2,
887
+ ...input.stackAboveSig,
888
+ input.leaf,
889
+ input.controlBlock
890
+ ];
891
+ const sizing = assemble(input.utxo.amount);
892
+ sizing.updateInput(0, { finalScriptWitness: witness(new Uint8Array(64)) }, true);
893
+ const fee = BigInt(Math.ceil(sizing.vsize * input.feeRateSatVb));
894
+ const payout = input.utxo.amount - fee;
895
+ if (payout < ONCHAIN_DUST_SATS) {
896
+ throw new Error(
897
+ `fee ${fee} leaves ${payout} sats from a ${input.utxo.amount} sat HTLC \u2014 below the ${ONCHAIN_DUST_SATS} sat dust limit`
898
+ );
899
+ }
900
+ const tx = assemble(payout);
901
+ const sighash = tx.preimageWitnessV1(
902
+ 0,
903
+ [input.htlc.pkScript],
904
+ btc.SigHash.DEFAULT,
905
+ [input.utxo.amount],
906
+ void 0,
907
+ input.leaf,
908
+ 192
909
+ );
910
+ const sig = await input.sign(sighash);
911
+ if (sig.length !== 64)
912
+ throw new Error(`sign() must return a 64-byte BIP340 signature, got ${sig.length}`);
913
+ tx.updateInput(0, { finalScriptWitness: witness(sig) }, true);
914
+ return { txHex: tx.hex, txid: tx.id, payoutAmount: payout };
915
+ };
916
+ buildHtlcClaim = async (input) => {
917
+ if (paymentHashOf(input.preimage) !== input.htlc.paymentHash) {
918
+ throw new Error("preimage does not hash to the HTLC's payment hash");
919
+ }
920
+ return buildLeafSpend({
921
+ htlc: input.htlc,
922
+ utxo: input.utxo,
923
+ leaf: input.htlc.leaves.claim,
924
+ controlBlock: input.htlc.controlBlocks.claim,
925
+ stackAboveSig: [input.preimage],
926
+ payoutPkScript: input.payoutPkScript,
927
+ feeRateSatVb: input.feeRateSatVb,
928
+ sign: input.sign,
929
+ sequence: 4294967293
930
+ });
1000
931
  };
1001
932
  }
1002
933
  });
1003
934
 
1004
- // src/advanced.ts
1005
- var advanced_exports = {};
1006
- __export(advanced_exports, {
1007
- ACTIVITY_TOKEN: () => ACTIVITY_TOKEN,
1008
- ARKADE_ASSET_NAMESPACE: () => ARKADE_ASSET_NAMESPACE,
1009
- AcceptConflict: () => AcceptConflict,
1010
- AmbiguousDestination: () => AmbiguousDestination,
1011
- Amount: () => Amount,
1012
- AmountEncodingUnsupported: () => AmountEncodingUnsupported,
1013
- AmountFormatError: () => AmountFormatError,
1014
- AmountMismatch: () => AmountMismatch,
1015
- AssetIdError: () => AssetIdError,
1016
- BITCOIN_RAILS: () => BITCOIN_RAILS,
1017
- BTC_ASSET_PART: () => BTC_ASSET_PART,
1018
- CORRIDORS: () => CORRIDORS,
1019
- CORRIDOR_FACTORIES: () => CORRIDOR_FACTORIES,
1020
- CORRIDOR_PASS: () => CORRIDOR_PASS,
1021
- ClientDisposed: () => ClientDisposed,
1022
- CovclaimdRevealError: () => CovclaimdRevealError,
1023
- DEFAULT_INVOICE_EXPIRY_SECONDS: () => DEFAULT_INVOICE_EXPIRY_SECONDS,
1024
- DiscoverySnapshotUnavailable: () => DiscoverySnapshotUnavailable,
1025
- FEED_TTL_MS: () => FEED_TTL_MS,
1026
- INDEXED_NETWORKS: () => INDEXED_NETWORKS,
1027
- INVOICE_HRPS: () => INVOICE_HRPS,
1028
- InconsistentRoute: () => InconsistentRoute,
1029
- InsufficientFunds: () => InsufficientFunds,
1030
- LIGHTNING_DRIVE: () => LIGHTNING_DRIVE,
1031
- LOCKUP_OWNER: () => LOCKUP_OWNER,
1032
- MaxFeeExceeded: () => MaxFeeExceeded,
1033
- MissingCorridorDep: () => MissingCorridorDep,
1034
- NotCancellable: () => NotCancellable,
1035
- OFFER_SWAP_ID_PREFIX: () => OFFER_SWAP_ID_PREFIX,
1036
- ONCHAIN_CLAIM_FEE_RATE_SATVB: () => ONCHAIN_CLAIM_FEE_RATE_SATVB,
1037
- ONCHAIN_DRIVE: () => ONCHAIN_DRIVE,
1038
- OperatorUnreachable: () => OperatorUnreachable,
1039
- QuoteExpired: () => QuoteExpired,
1040
- QuoteVerificationFailed: () => QuoteVerificationFailed,
1041
- RAILS: () => RAILS,
1042
- REGISTRY_URL: () => REGISTRY_URL,
1043
- RFQ_SWAP_ID_PREFIX: () => RFQ_SWAP_ID_PREFIX,
1044
- SWAP_ERROR_NAMES: () => SWAP_ERROR_NAMES,
1045
- SwapDriveRefusedError: () => SwapDriveRefusedError,
1046
- SwapRefusal: () => SwapRefusal,
1047
- UnsupportedRoute: () => UnsupportedRoute,
1048
- acceptQuote: () => acceptQuote,
1049
- aliasTableFrom: () => aliasTableFrom,
1050
- applyOfferSpend: () => applyOfferSpend,
1051
- arkadeAsset: () => arkadeAsset,
1052
- arkadeCorridor: () => arkadeCorridor,
1053
- artifactOf: () => artifactOf,
1054
- assembleRoute: () => assembleRoute,
1055
- assetPartOf: () => assetPartOf,
1056
- assetSwapIdOf: () => assetSwapIdOf,
1057
- attesting: () => attesting,
1058
- bitcoinNetworkOf: () => bitcoinNetworkOf,
1059
- btcOn: () => btcOn,
1060
- cancelSwap: () => cancelSwap,
1061
- canonicalAssetId: () => canonicalAssetId,
1062
- cardMarketOf: () => cardMarketOf,
1063
- chainSourceOver: () => chainSourceOver,
1064
- chooseMarket: () => chooseMarket,
1065
- conflictingFields: () => conflictingFields,
1066
- corridorOfRail: () => corridorOfRail,
1067
- corridorOutcome: () => corridorOutcome,
1068
- corridorRecordStore: () => corridorRecordStore,
1069
- corridorSet: () => corridorSet,
1070
- covclaimdClient: () => covclaimdClient,
1071
- createSwapClient: () => createSwapClient,
1072
- createSwapDrive: () => createSwapDrive,
1073
- decodeBolt11: () => decodeBolt11,
1074
- decodeRfqAmount: () => decodeRfqAmount,
1075
- discoveryIndex: () => discoveryIndex,
1076
- eligibleMarkets: () => eligibleMarkets,
1077
- encodeRfqAmount: () => encodeRfqAmount,
1078
- enforceFeeCeiling: () => enforceFeeCeiling,
1079
- esploraChainSource: () => esploraChainSource,
1080
- esploraTip: () => esploraTip,
1081
- exchange: () => exchange,
1082
- familyOfSwapId: () => familyOfSwapId,
1083
- feedFetch: () => feedFetch,
1084
- formatAssetId: () => formatAssetId,
1085
- fromAtomicDecimal: () => fromAtomicDecimal,
1086
- fromRfqAmountSide: () => fromRfqAmountSide,
1087
- fundsFromWallet: () => fundsFromWallet,
1088
- instrumentOf: () => instrumentOf,
1089
- isAddressable: () => isAddressable,
1090
- isAssetId: () => isAssetId,
1091
- isAtomicDecimal: () => isAtomicDecimal,
1092
- isCardMarket: () => isCardMarket,
1093
- isIndexedNetwork: () => isIndexedNetwork,
1094
- isNetworkRef: () => isNetworkRef,
1095
- isSwapError: () => isSwapError,
1096
- isUsableCard: () => isUsableCard,
1097
- issuanceOf: () => issuanceOf,
1098
- legOf: () => legOf,
1099
- lightningCorridor: () => lightningCorridor,
1100
- liveArkadeInfo: () => liveArkadeInfo,
1101
- marketBackendOf: () => marketBackendOf,
1102
- marketKeyOf: () => marketKeyOf,
1103
- marketRefOf: () => marketRefOf,
1104
- networksOfInvoiceHrp: () => networksOfInvoiceHrp,
1105
- nostrTransportFactory: () => nostrTransportFactory,
1106
- offerFactsOf: () => offerFactsOf,
1107
- offerOutcome: () => offerOutcome,
1108
- offerRecordSource: () => offerRecordSource,
1109
- onchainCorridor: () => onchainCorridor,
1110
- parseAssetId: () => parseAssetId,
1111
- parseRfqQuote: () => parseRfqQuote,
1112
- pay: () => pay,
1113
- publicAssetId: () => publicAssetId,
1114
- quoteFromFeed: () => quoteFromFeed,
1115
- quoteIdOfSwapId: () => quoteIdOfSwapId,
1116
- quoteViaRfq: () => quoteViaRfq,
1117
- railOf: () => railOf,
1118
- railOfCorridor: () => railOfCorridor,
1119
- readableRecord: () => readableRecord,
1120
- readsChain: () => readsChain,
1121
- receive: () => receive,
1122
- recordArtifact: () => recordArtifact,
1123
- recordEndpoint: () => recordEndpoint,
1124
- recordInstrument: () => recordInstrument,
1125
- recordLeg: () => recordLeg,
1126
- recordOutcome: () => recordOutcome,
1127
- resolveCorridorBase: () => resolveCorridorBase,
1128
- resolveCorridorDeps: () => resolveCorridorDeps,
1129
- resolveRoute: () => resolveRoute,
1130
- revealClaimPacket: () => revealClaimPacket,
1131
- revealFieldsFromScript: () => revealFieldsFromScript,
1132
- rfqLeg: () => rfqLeg,
1133
- rfqPairFor: () => rfqPairFor,
1134
- rfqRecordOf: () => rfqRecordOf,
1135
- sameAsset: () => sameAsset,
1136
- satsOf: () => satsOf,
1137
- scopedToRail: () => scopedToRail,
1138
- splitRecords: () => splitRecords,
1139
- swapOf: () => swapOf,
1140
- swapRecordOf: () => swapRecordOf,
1141
- toAtomicDecimal: () => toAtomicDecimal,
1142
- toDiscoveryLeg: () => toDiscoveryLeg,
1143
- toRfqAmountSide: () => toRfqAmountSide,
1144
- toSafeNumber: () => toSafeNumber,
1145
- usableMarkets: () => usableMarkets,
1146
- verifyPair: () => verifyPair,
1147
- verifyQuoteTtl: () => verifyQuoteTtl,
1148
- verifyQuotedAmount: () => verifyQuotedAmount,
1149
- verifyReceiveInvoiceFacts: () => verifyReceiveInvoiceFacts,
1150
- verifyReceiveWindow: () => verifyReceiveWindow,
1151
- verifyResponder: () => verifyResponder,
1152
- verifySendInvoice: () => verifySendInvoice,
1153
- verifySendWindow: () => verifySendWindow,
1154
- verifyingDerivation: () => verifyingDerivation,
1155
- walletLockupIndexer: () => walletLockupIndexer,
1156
- withCanonicalAmount: () => withCanonicalAmount,
1157
- withOfferStatus: () => withOfferStatus,
1158
- withRfqState: () => withRfqState
1159
- });
1160
- module.exports = __toCommonJS(advanced_exports);
1161
-
1162
- // src/client/accept.ts
1163
- var import_base8 = require("@scure/base");
1164
- var import_sdk9 = require("@arkade-os/sdk");
1165
-
1166
- // src/offer.ts
1167
- var import_base2 = require("@scure/base");
1168
- var import_utils = require("@scure/btc-signer/utils.js");
1169
- var import_sdk2 = require("@arkade-os/sdk");
1170
-
1171
- // src/swap-want-asset.program.json
1172
- var swap_want_asset_program_default = {
1173
- version: 0,
1174
- name: "banco-btc-to-asset",
1175
- params: [
1176
- { name: "makerWP", type: "pubkey" },
1177
- { name: "wantAmount", type: "int" },
1178
- { name: "wantAssetTxid", type: "hash" },
1179
- { name: "wantAssetGroupIndex", type: "int" },
1180
- { name: "server", type: "pubkey" },
1181
- { name: "user", type: "pubkey" }
1182
- ],
1183
- functions: {
1184
- fulfill: {
1185
- tapscript: { signers: ["$server"] },
1186
- arkadeScript: {
1187
- asm: [
1188
- 0,
1189
- "$wantAssetTxid",
1190
- "$wantAssetGroupIndex",
1191
- "INSPECTOUTASSETLOOKUP",
1192
- "VERIFY",
1193
- "$wantAmount",
1194
- "GREATERTHANOREQUAL",
1195
- "VERIFY",
1196
- 0,
1197
- "INSPECTOUTPUTSCRIPTPUBKEY",
1198
- 1,
1199
- "EQUALVERIFY",
1200
- "$makerWP",
1201
- "EQUAL"
1202
- ]
1203
- }
1204
- },
1205
- cancel: {
1206
- tapscript: { signers: ["$user", "$server"] }
1207
- }
935
+ // src/claimPacket.ts
936
+ async function sealClaimPacket(input) {
937
+ return sealWithEntropy(
938
+ input,
939
+ import_secp256k1.secp256k1.utils.randomSecretKey(),
940
+ crypto.getRandomValues(new Uint8Array(12))
941
+ );
942
+ }
943
+ async function sealWithEntropy(input, ephemeralKey, nonce) {
944
+ if (input.preimage.length !== 32) throw new Error("preimage must be 32 bytes");
945
+ if (input.covclaimdPubkey.length !== 33) {
946
+ throw new Error("covclaimd pubkey must be 33-byte compressed");
1208
947
  }
1209
- };
1210
-
1211
- // src/swap-want-btc.program.json
1212
- var swap_want_btc_program_default = {
1213
- version: 0,
1214
- name: "banco-asset-to-btc",
1215
- params: [
1216
- { name: "makerWP", type: "pubkey" },
1217
- { name: "wantAmount", type: "int" },
1218
- { name: "server", type: "pubkey" },
1219
- { name: "user", type: "pubkey" }
1220
- ],
1221
- functions: {
1222
- fulfill: {
1223
- tapscript: { signers: ["$server"] },
1224
- arkadeScript: {
1225
- asm: [
1226
- 0,
1227
- "INSPECTOUTPUTVALUE",
1228
- "$wantAmount",
1229
- "GREATERTHANOREQUAL",
1230
- "VERIFY",
1231
- 0,
1232
- "INSPECTOUTPUTSCRIPTPUBKEY",
1233
- 1,
1234
- "EQUALVERIFY",
1235
- "$makerWP",
1236
- "EQUAL"
1237
- ]
1238
- }
1239
- },
1240
- cancel: {
1241
- tapscript: { signers: ["$user", "$server"] }
1242
- }
948
+ const ephemeralPub = import_secp256k1.secp256k1.getPublicKey(ephemeralKey, true);
949
+ const sharedX = import_secp256k1.secp256k1.getSharedSecret(ephemeralKey, input.covclaimdPubkey, true).subarray(1);
950
+ const key = (0, import_hkdf.hkdf)(import_sha23.sha256, sharedX, ephemeralPub, HKDF_INFO, 32);
951
+ if (nonce.length !== 12) throw new Error("nonce must be 12 bytes");
952
+ const sealed = (0, import_aes.gcm)(key, nonce, ephemeralPub).encrypt(input.preimage);
953
+ const ciphertext = new Uint8Array(33 + 12 + sealed.length);
954
+ ciphertext.set(ephemeralPub, 0);
955
+ ciphertext.set(nonce, 33);
956
+ ciphertext.set(sealed, 45);
957
+ return {
958
+ ciphertext: import_base6.base64.encode(ciphertext),
959
+ packet: import_base6.base64.encode(
960
+ (0, import_utils2.concatBytes)(
961
+ tlv2(TLV_CIPHERTEXT, ciphertext),
962
+ tlv2(TLV_COVCLAIMD_PUBKEY, input.covclaimdPubkey)
963
+ )
964
+ )
965
+ };
966
+ }
967
+ var import_base6, import_utils2, import_aes, import_secp256k1, import_hkdf, import_sha23, HKDF_INFO, TLV_CIPHERTEXT, TLV_COVCLAIMD_PUBKEY, tlv2;
968
+ var init_claimPacket = __esm({
969
+ "src/claimPacket.ts"() {
970
+ "use strict";
971
+ import_base6 = require("@scure/base");
972
+ import_utils2 = require("@scure/btc-signer/utils.js");
973
+ import_aes = require("@noble/ciphers/aes.js");
974
+ import_secp256k1 = require("@noble/curves/secp256k1.js");
975
+ import_hkdf = require("@noble/hashes/hkdf.js");
976
+ import_sha23 = require("@noble/hashes/sha2.js");
977
+ HKDF_INFO = new TextEncoder().encode("covclaimd/preimage/v1");
978
+ TLV_CIPHERTEXT = 1;
979
+ TLV_COVCLAIMD_PUBKEY = 3;
980
+ tlv2 = (type, value) => Uint8Array.from([type, value.length >> 8 & 255, value.length & 255, ...value]);
1243
981
  }
1244
- };
982
+ });
1245
983
 
1246
- // src/coverage.ts
1247
- var RETIRABLE = ["fulfilled", "cancelled"];
1248
- var LOCKUP_RETIRABLE = ["settled", "refunded"];
1249
- var issuedAt = /* @__PURE__ */ new Map();
1250
- var inFlight = /* @__PURE__ */ new Map();
1251
- async function serialize(script, task) {
1252
- const previous = inFlight.get(script) ?? Promise.resolve();
1253
- const result = previous.then(task, task);
1254
- const settled = result.then(
1255
- () => {
1256
- },
1257
- () => {
984
+ // src/rfq.ts
985
+ function lightningSendContract(params) {
986
+ const seconds = (value) => ({
987
+ type: "seconds",
988
+ value: BigInt(value)
989
+ });
990
+ return new import_sdk5.VHTLC.ScriptV2({
991
+ sender: params.senderPubkey,
992
+ receiver: params.solverPubkey,
993
+ server: params.operatorPubkey,
994
+ preimageHash: (0, import_legacy2.ripemd160)(import_base7.hex.decode(params.paymentHash)),
995
+ refundLocktime: BigInt(params.refundLocktime),
996
+ unilateralClaimDelay: seconds(params.claimDelay),
997
+ unilateralRefundDelay: seconds(unilateralRefundDelay(params.claimDelay)),
998
+ unilateralRefundWithoutReceiverDelay: seconds(
999
+ params.refundWithoutReceiverDelay ?? unilateralRefundWithoutReceiverDelay(params.claimDelay)
1000
+ ),
1001
+ nonInteractiveParameters: {
1002
+ receiverPkScript: params.receiverPkScript,
1003
+ senderPkScript: params.refundPkScript,
1004
+ emulatorPubkey: params.emulatorPubkey,
1005
+ ...params.legacy !== void 0 && { legacy: params.legacy }
1258
1006
  }
1259
- );
1260
- inFlight.set(script, settled);
1261
- void settled.then(() => {
1262
- if (inFlight.get(script) === settled) inFlight.delete(script);
1263
1007
  });
1264
- return result;
1265
1008
  }
1266
- function addressOutstanding(swaps, script) {
1267
- const issued = issuedAt.get(script);
1268
- if (issued === void 0) return false;
1269
- if (swaps.some((s) => s.swapPkScript === script && s.createdAt >= issued)) {
1270
- issuedAt.delete(script);
1271
- return false;
1272
- }
1273
- return true;
1274
- }
1275
- async function promoteOfferContract(manager, script, issued = Date.now()) {
1276
- await serialize(script, async () => {
1277
- await manager.setContractWatchState(script, "watched");
1278
- issuedAt.set(script, issued);
1279
- });
1280
- }
1281
- async function retireOfferContract(manager, swaps, script) {
1282
- await serialize(script, async () => {
1283
- if (swaps.some((s) => s.swapPkScript === script && !RETIRABLE.includes(s.status))) return;
1284
- if (addressOutstanding(swaps, script)) return;
1285
- try {
1286
- await manager.setContractWatchState(script, "retained");
1287
- } catch (err) {
1288
- console.warn(`[swap] could not retire offer contract ${script}`, err);
1289
- }
1290
- });
1291
- }
1292
- async function retireSettledOfferContracts(manager, swaps) {
1293
- const settled = new Set(
1294
- swaps.filter((s) => RETIRABLE.includes(s.status)).map((s) => s.swapPkScript)
1295
- );
1296
- for (const script of settled) await retireOfferContract(manager, swaps, script);
1297
- }
1298
-
1299
- // src/store.ts
1300
- var import_base = require("@scure/base");
1301
- var import_sha2 = require("@noble/hashes/sha2.js");
1302
- var import_sdk = require("@arkade-os/sdk");
1303
- var BTC_ASSET_ID = "btc";
1304
- var byNewest = (a, b) => b.createdAt - a.createdAt;
1305
- var getAssetSwapsOrThrow = async (repository) => {
1306
- return (await repository.getAllSwaps()).filter(
1307
- (s) => s && typeof s.id === "string" && // offer swaps carry the TLV; onchain-corridor swaps carry
1308
- // the payment hash instead — either marks a valid record
1309
- (typeof s.offerHex === "string" || typeof s.paymentHash === "string")
1310
- ).sort(byNewest);
1311
- };
1312
- var getAssetSwaps = async (repository) => {
1313
- try {
1314
- return await getAssetSwapsOrThrow(repository);
1315
- } catch {
1316
- return [];
1317
- }
1318
- };
1319
- var saveSwapOrThrow = async (repository, swap) => {
1320
- try {
1321
- await repository.saveSwap(swap);
1322
- } catch (error) {
1323
- const reason = error instanceof Error ? error.message : String(error);
1324
- throw new Error(`failed to save swap ${swap.id}: ${reason}`);
1325
- }
1326
- };
1327
- var updateAssetSwap = async (repository, id, changes) => {
1328
- const swaps = (await getAssetSwapsOrThrow(repository)).map(
1329
- (s) => s.id === id ? { ...s, ...changes } : s
1330
- );
1331
- const updated = swaps.find((s) => s.id === id);
1332
- if (updated) await saveSwapOrThrow(repository, updated);
1333
- return swaps;
1334
- };
1335
- var updateAssetSwapBestEffort = async (repository, id, changes) => {
1336
- try {
1337
- return { swaps: await updateAssetSwap(repository, id, changes), persisted: true };
1338
- } catch (error) {
1339
- console.warn(`[swap] failed to persist update for swap ${id}`, error);
1340
- const swaps = (await getAssetSwaps(repository)).map(
1341
- (s) => s.id === id ? { ...s, ...changes } : s
1342
- );
1343
- return { swaps, persisted: false };
1344
- }
1345
- };
1346
- var swapSecretsToRecord = (secrets) => ({
1347
- signingDescriptor: secrets.descriptor,
1348
- ..."mustPersistPreimage" in secrets && secrets.mustPersistPreimage ? { preimageHex: import_base.hex.encode(secrets.preimage) } : {},
1349
- ..."preimageSalt" in secrets && secrets.preimageSalt ? { preimageSaltHex: import_base.hex.encode(secrets.preimageSalt) } : {}
1350
- });
1351
- var decodeHex32 = (value, field) => {
1352
- const bytes = import_base.hex.decode(value);
1353
- if (bytes.length !== 32) {
1354
- throw new Error(`${field} must be 32 bytes, got ${bytes.length}`);
1355
- }
1356
- return bytes;
1357
- };
1358
- var PreimageNotRecoverableError = class extends Error {
1359
- constructor(reason, message, options) {
1360
- super(message, options);
1361
- this.reason = reason;
1009
+ function deriveLightningSend(input) {
1010
+ const { quote } = input;
1011
+ if (quote.refund_locktime === void 0) {
1012
+ throw new Error("lightning-send quote is missing refund_locktime");
1362
1013
  }
1363
- reason;
1364
- name = "PreimageNotRecoverableError";
1365
- };
1366
- var preimageForSwapRecord = async (wallet, record) => {
1367
- if (!record.signingDescriptor) {
1368
- throw new PreimageNotRecoverableError(
1369
- "no-secrets",
1370
- "this swap record carries no signing descriptor"
1371
- );
1014
+ const refundWithoutReceiverDelay = quote.profile?.refund_without_receiver_delay;
1015
+ if (refundWithoutReceiverDelay === void 0) {
1016
+ throw new Error("lightning-send quote is missing profile.refund_without_receiver_delay");
1372
1017
  }
1373
- let stored;
1374
- let salt;
1375
- try {
1376
- stored = record.preimageHex ? decodeHex32(record.preimageHex, "preimageHex") : void 0;
1377
- salt = record.preimageSaltHex ? decodeHex32(record.preimageSaltHex, "preimageSaltHex") : void 0;
1378
- } catch (cause) {
1379
- throw new PreimageNotRecoverableError(
1380
- "malformed-record",
1381
- `this swap record's secrets projection is unreadable: ${String(cause)}`,
1382
- { cause }
1383
- );
1018
+ if (!Number.isSafeInteger(refundWithoutReceiverDelay) || refundWithoutReceiverDelay < input.claimDelay || refundWithoutReceiverDelay % SEQUENCE_GRANULARITY_SECONDS !== 0 || refundWithoutReceiverDelay > 65535 * SEQUENCE_GRANULARITY_SECONDS) {
1019
+ throw new Error("lightning-send quote carries an invalid refund_without_receiver_delay");
1384
1020
  }
1385
- let preimage;
1386
- try {
1387
- preimage = await (0, import_sdk.contractPreimage)(wallet, record.signingDescriptor, { stored, salt });
1388
- } catch (cause) {
1389
- throw new PreimageNotRecoverableError(
1390
- "not-derivable",
1391
- `this wallet cannot produce the preimage for ${record.signingDescriptor}`,
1392
- { cause }
1393
- );
1021
+ if (refundWithoutReceiverDelay < quote.refund_locktime - input.now) {
1022
+ throw new Error("lightning-send quote lets the solo refund open before refund_locktime");
1394
1023
  }
1395
- if (record.paymentHash && import_base.hex.encode((0, import_sha2.sha256)(preimage)) !== record.paymentHash.toLowerCase()) {
1396
- throw new PreimageNotRecoverableError(
1397
- "hash-mismatch",
1398
- "the derived preimage does not match this swap's payment hash: wrong wallet, or a tampered salt"
1399
- );
1024
+ const receiverPkScriptHex = quote.profile?.receiver_pk_script;
1025
+ if (receiverPkScriptHex === void 0) {
1026
+ throw new Error("lightning-send quote is missing profile.receiver_pk_script");
1400
1027
  }
1401
- return preimage;
1402
- };
1403
-
1404
- // src/offer.ts
1405
- var swapPrograms = {
1406
- wantAsset: import_sdk2.arkade.parseArtifact(swap_want_asset_program_default),
1407
- wantBtc: import_sdk2.arkade.parseArtifact(swap_want_btc_program_default)
1408
- };
1409
- function withExitClosure(program, exit) {
1410
- if (!exit) return program;
1411
- return {
1412
- ...program,
1413
- // typed params are authoritative: an undeclared `$exitDelay` fails
1414
- // validateProgram instead of compiling against an unbound value
1415
- params: [...program.params ?? [], { name: "exitDelay", type: "int" }],
1416
- functions: {
1417
- ...program.functions,
1418
- exit: {
1419
- tapscript: { signers: ["$user"], csv: { type: exit.type, value: "$exitDelay" } }
1420
- }
1421
- }
1028
+ const contractParams = {
1029
+ solverPubkey: (0, import_sdk5.toXOnly)(import_base7.hex.decode(quote.solver_pubkey), "solver key"),
1030
+ refundLocktime: quote.refund_locktime,
1031
+ operatorPubkey: input.operatorPubkey,
1032
+ paymentHash: input.paymentHash,
1033
+ claimDelay: input.claimDelay,
1034
+ refundWithoutReceiverDelay,
1035
+ emulatorPubkey: input.emulatorPubkey,
1036
+ senderPubkey: input.senderPubkey,
1037
+ receiverPkScript: solverHex(receiverPkScriptHex, "profile.receiver_pk_script"),
1038
+ refundPkScript: input.refundPkScript
1422
1039
  };
1423
- }
1424
- function swapProgramBinding(offer, operatorPubkey) {
1425
- if (offer.makerPkScript.length !== FIELDS.makerPkScript.width) {
1426
- throw new Error("makerPkScript is not a 34-byte taproot scriptPubKey");
1427
- }
1040
+ const matched = matchQuotedLockup(
1041
+ quote,
1042
+ input.hrp,
1043
+ input.operatorPubkey,
1044
+ (legacy) => lightningSendContract({ ...contractParams, ...legacy !== void 0 && { legacy } })
1045
+ );
1428
1046
  return {
1429
- program: withExitClosure(
1430
- offer.wantAsset ? swapPrograms.wantAsset : swapPrograms.wantBtc,
1431
- offer.exitDelay
1432
- ),
1433
- args: {
1434
- makerWP: offer.makerPkScript.subarray(2),
1435
- wantAmount: offer.wantAmount,
1436
- server: operatorPubkey,
1437
- user: offer.makerPublicKey,
1438
- // internal byte order
1439
- ...offer.wantAsset && {
1440
- wantAssetTxid: offer.wantAsset.txid.slice().reverse(),
1441
- wantAssetGroupIndex: offer.wantAsset.groupIndex
1442
- },
1443
- ...offer.exitDelay && { exitDelay: offer.exitDelay.value }
1047
+ address: matched.address,
1048
+ swapPkScript: matched.script.pkScript,
1049
+ script: matched.script,
1050
+ contractParams: {
1051
+ ...contractParams,
1052
+ ...matched.legacy !== void 0 && { legacy: matched.legacy }
1444
1053
  },
1445
- keys: {
1446
- serverKey: operatorPubkey,
1447
- userKey: offer.makerPublicKey,
1448
- emulatorKey: offer.emulatorPubkey
1449
- }
1054
+ refundLocktime: quote.refund_locktime
1450
1055
  };
1451
1056
  }
1452
- function offerContract(offer, operatorPubkey) {
1453
- const { program, args, keys } = swapProgramBinding(offer, operatorPubkey);
1454
- return new import_sdk2.arkade.ArkadeProgramScript(program, args, keys);
1455
- }
1456
- var OFFER_PACKET_TYPE = 3;
1457
- var FIELDS = {
1458
- swapPkScript: { tag: 1, width: 34 },
1459
- wantAmount: { tag: 2, width: 8 },
1460
- wantAsset: { tag: 3, width: void 0 },
1461
- makerPkScript: { tag: 5, width: 34 },
1462
- makerPublicKey: { tag: 7, width: 32 },
1463
- emulatorPubkey: { tag: 8, width: 32 },
1464
- ratioNum: { tag: 9, width: 8 },
1465
- ratioDen: { tag: 10, width: 8 },
1466
- offerAsset: { tag: 11, width: void 0 },
1467
- exitTimelock: { tag: 12, width: 9 }
1468
- };
1469
- var EXIT_TYPES = ["blocks", "seconds"];
1470
- var NAMES = Object.fromEntries(Object.entries(FIELDS).map(([k, f]) => [f.tag, k]));
1471
- function u64(name, value) {
1472
- if (value < BigInt(0) || value >> BigInt(64) > BigInt(0)) {
1473
- throw new Error(`${name} does not fit the offer wire format (u64)`);
1057
+ function deriveOnchainSend(input) {
1058
+ const { quote } = input;
1059
+ const profile = quote.profile ?? {};
1060
+ const refundLocktime = quote.refund_locktime ?? profile.refund_locktime;
1061
+ const htlcPubkey = profile.htlc_pubkey;
1062
+ const htlcLocktime = profile.htlc_locktime;
1063
+ const htlcAddress = profile.htlc_address;
1064
+ const minConfirmations = profile.min_confirmations;
1065
+ const receiverPkScriptHex = profile.receiver_pk_script;
1066
+ if (refundLocktime === void 0 || htlcPubkey === void 0 || htlcLocktime === void 0 || minConfirmations === void 0 || receiverPkScriptHex === void 0) {
1067
+ throw new Error("onchain-send quote is missing a binding field");
1474
1068
  }
1475
- const out = new Uint8Array(FIELDS.wantAmount.width);
1476
- new DataView(out.buffer).setBigUint64(0, value, false);
1477
- return out;
1069
+ const contractParams = {
1070
+ solverPubkey: (0, import_sdk5.toXOnly)(import_base7.hex.decode(quote.solver_pubkey), "solver key"),
1071
+ refundLocktime,
1072
+ operatorPubkey: input.operatorPubkey,
1073
+ paymentHash: input.paymentHash,
1074
+ claimDelay: input.claimDelay,
1075
+ emulatorPubkey: input.emulatorPubkey,
1076
+ senderPubkey: input.senderPubkey,
1077
+ receiverPkScript: solverHex(receiverPkScriptHex, "profile.receiver_pk_script"),
1078
+ refundPkScript: import_sdk5.ArkAddress.decode(input.refundAddress).pkScript
1079
+ };
1080
+ const { script, address } = matchQuotedLockup(
1081
+ quote,
1082
+ input.hrp,
1083
+ input.operatorPubkey,
1084
+ (legacy) => lightningSendContract({ ...contractParams, ...legacy !== void 0 && { legacy } })
1085
+ );
1086
+ const htlcParams = {
1087
+ paymentHash: input.paymentHash,
1088
+ claimKey: input.payoutPubkey,
1089
+ refundKey: (0, import_sdk5.toXOnly)(import_base7.hex.decode(htlcPubkey), "solver L1 htlc key"),
1090
+ refundLocktime: htlcLocktime
1091
+ };
1092
+ const htlc = onchainHtlcScript(htlcParams, input.l1Network);
1093
+ if (htlc.address !== htlcAddress) throw new AddressMismatch(htlc.address, htlcAddress);
1094
+ return {
1095
+ address,
1096
+ swapPkScript: script.pkScript,
1097
+ script,
1098
+ htlc,
1099
+ htlcParams,
1100
+ l1Network: input.l1Network,
1101
+ refundLocktime,
1102
+ htlcLocktime,
1103
+ minConfirmations,
1104
+ expectedAmount: quoteExpectedSats(quote.to_amount, "to_amount")
1105
+ };
1478
1106
  }
1479
- var readU64 = (value) => new DataView(value.buffer, value.byteOffset).getBigUint64(0, false);
1480
- var setRatio = (name, value) => {
1481
- if (value === void 0 || value === BigInt(0)) return void 0;
1482
- if (value < BigInt(0)) throw new Error(`${name} does not fit the offer wire format (u64)`);
1483
- return value;
1484
- };
1485
- function tlv(type, value) {
1486
- if (value.length > 65535) throw new Error("TLV value exceeds the u16 length field");
1487
- return (0, import_utils.concatBytes)(Uint8Array.of(type, value.length >> 8 & 255, value.length & 255), value);
1107
+ function lightningReceiveContract(params) {
1108
+ const seconds = (value) => ({
1109
+ type: "seconds",
1110
+ value: BigInt(value)
1111
+ });
1112
+ return new import_sdk5.VHTLC.ScriptV2({
1113
+ sender: params.solverPubkey,
1114
+ receiver: params.payoutPubkey,
1115
+ server: params.operatorPubkey,
1116
+ preimageHash: (0, import_legacy2.ripemd160)(import_base7.hex.decode(params.paymentHash)),
1117
+ refundLocktime: BigInt(params.refundLocktime),
1118
+ unilateralClaimDelay: seconds(params.claimDelay),
1119
+ unilateralRefundDelay: seconds(unilateralRefundDelay(params.claimDelay)),
1120
+ unilateralRefundWithoutReceiverDelay: seconds(
1121
+ unilateralRefundWithoutReceiverDelay(params.claimDelay)
1122
+ ),
1123
+ nonInteractiveParameters: {
1124
+ receiverPkScript: params.payoutPkScript,
1125
+ senderPkScript: params.solverRefundPkScript,
1126
+ emulatorPubkey: params.emulatorPubkey,
1127
+ ...params.legacy !== void 0 && { legacy: params.legacy }
1128
+ }
1129
+ });
1488
1130
  }
1489
- function encodeOffer(offer) {
1490
- if (Boolean(offer.wantAsset) === Boolean(offer.offerAsset)) {
1491
- throw new Error("offer must carry exactly one of wantAsset or offerAsset");
1131
+ function deriveLightningReceive(input) {
1132
+ const { quote } = input;
1133
+ const profile = quote.profile ?? {};
1134
+ const refundLocktime = quote.refund_locktime;
1135
+ const invoice = profile.invoice;
1136
+ const solverRefundPkScriptHex = profile.solver_refund_pk_script;
1137
+ if (refundLocktime === void 0 || invoice === void 0 || solverRefundPkScriptHex === void 0) {
1138
+ throw new Error("lightning-receive quote is missing a binding field");
1492
1139
  }
1493
- for (const name of [
1494
- "swapPkScript",
1495
- "makerPkScript",
1496
- "makerPublicKey",
1497
- "emulatorPubkey"
1498
- ]) {
1499
- if (offer[name].length !== FIELDS[name].width) {
1500
- throw new Error(`${name} must be ${FIELDS[name].width} bytes`);
1501
- }
1502
- }
1503
- const ratioNum = setRatio("ratioNum", offer.ratioNum);
1504
- const ratioDen = setRatio("ratioDen", offer.ratioDen);
1505
- if (ratioNum === void 0 !== (ratioDen === void 0)) {
1506
- throw new Error("offer must carry both ratioNum and ratioDen, or neither");
1507
- }
1508
- const recs = [
1509
- tlv(FIELDS.swapPkScript.tag, offer.swapPkScript),
1510
- tlv(FIELDS.wantAmount.tag, u64("wantAmount", offer.wantAmount))
1511
- ];
1512
- if (offer.wantAsset) recs.push(tlv(FIELDS.wantAsset.tag, offer.wantAsset.serialize()));
1513
- if (ratioNum !== void 0) recs.push(tlv(FIELDS.ratioNum.tag, u64("ratioNum", ratioNum)));
1514
- if (ratioDen !== void 0) recs.push(tlv(FIELDS.ratioDen.tag, u64("ratioDen", ratioDen)));
1515
- if (offer.offerAsset) recs.push(tlv(FIELDS.offerAsset.tag, offer.offerAsset.serialize()));
1516
- recs.push(
1517
- tlv(FIELDS.makerPkScript.tag, offer.makerPkScript),
1518
- tlv(FIELDS.makerPublicKey.tag, offer.makerPublicKey),
1519
- tlv(FIELDS.emulatorPubkey.tag, offer.emulatorPubkey)
1520
- );
1521
- if (offer.exitDelay) recs.push(tlv(FIELDS.exitTimelock.tag, encodeExitDelay(offer.exitDelay)));
1522
- return (0, import_utils.concatBytes)(...recs);
1523
- }
1524
- function encodeExitDelay(exit) {
1525
- return (0, import_utils.concatBytes)(
1526
- Uint8Array.of(EXIT_TYPES.indexOf(assertExitDelay(exit).type)),
1527
- u64("exitDelay", exit.value)
1528
- );
1529
- }
1530
- function assertExitDelay(exit) {
1531
- if (EXIT_TYPES.indexOf(exit.type) < 0) {
1532
- throw new Error(`unknown exitDelay locktime type: ${exit.type}`);
1533
- }
1534
- if (exit.value <= BigInt(0)) {
1535
- throw new Error("exitDelay must be a positive relative locktime");
1536
- }
1537
- if (exit.value >> BigInt(32) > BigInt(0)) {
1538
- throw new Error("exitDelay does not fit the locktime field (u32)");
1539
- }
1540
- return exit;
1541
- }
1542
- function decodeOffer(data) {
1543
- const fields = {};
1544
- let off = 0;
1545
- while (off < data.length) {
1546
- if (off + 3 > data.length) throw new Error("truncated TLV header");
1547
- const type = data[off];
1548
- const length = data[off + 1] << 8 | data[off + 2];
1549
- off += 3;
1550
- if (off + length > data.length)
1551
- throw new Error(`truncated TLV value for type 0x${type.toString(16)}`);
1552
- const name = NAMES[type];
1553
- if (!name) throw new Error(`unknown TLV type: 0x${type.toString(16)}`);
1554
- if (fields[name] !== void 0) throw new Error(`duplicate TLV record: ${name}`);
1555
- fields[name] = data.slice(off, off + length);
1556
- off += length;
1557
- }
1558
- for (const name of ["wantAsset", "offerAsset"]) {
1559
- if (fields[name]?.length === 0) throw new Error(`missing/invalid ${name}`);
1560
- }
1561
- for (const [name, value] of Object.entries(fields)) {
1562
- const width = FIELDS[name].width;
1563
- if (width !== void 0 && value.length !== width) {
1564
- throw new Error(`missing/invalid ${name}`);
1565
- }
1566
- }
1567
- const need = (name) => {
1568
- const v = fields[name];
1569
- const len = FIELDS[name].width;
1570
- if (!v || len !== void 0 && v.length !== len)
1571
- throw new Error(`missing/invalid ${name}`);
1572
- return v;
1573
- };
1574
- const amount = need("wantAmount");
1575
- if (Boolean(fields.wantAsset) === Boolean(fields.offerAsset)) {
1576
- throw new Error("offer must carry exactly one of wantAsset or offerAsset");
1577
- }
1578
- const readRatio = (name) => {
1579
- const raw = fields[name];
1580
- if (!raw) return void 0;
1581
- const value = readU64(raw);
1582
- if (value === BigInt(0)) throw new Error(`missing/invalid ${name}`);
1583
- return value;
1584
- };
1585
- const ratioNum = readRatio("ratioNum");
1586
- const ratioDen = readRatio("ratioDen");
1587
- if (ratioNum === void 0 !== (ratioDen === void 0)) {
1588
- throw new Error("offer must carry both ratioNum and ratioDen, or neither");
1589
- }
1590
- return {
1591
- swapPkScript: need("swapPkScript"),
1592
- wantAmount: readU64(amount),
1593
- ...fields.wantAsset && { wantAsset: import_sdk2.asset.AssetId.fromBytes(fields.wantAsset) },
1594
- ...fields.offerAsset && { offerAsset: import_sdk2.asset.AssetId.fromBytes(fields.offerAsset) },
1595
- makerPkScript: need("makerPkScript"),
1596
- makerPublicKey: need("makerPublicKey"),
1597
- emulatorPubkey: need("emulatorPubkey"),
1598
- ...ratioNum !== void 0 && { ratioNum },
1599
- ...ratioDen !== void 0 && { ratioDen },
1600
- ...fields.exitTimelock && { exitDelay: decodeExitDelay(fields.exitTimelock) }
1601
- };
1602
- }
1603
- function decodeExitDelay(value) {
1604
- const type = EXIT_TYPES[value[0]];
1605
- if (!type) throw new Error(`unknown exitDelay locktime type: 0x${value[0].toString(16)}`);
1606
- return { type, value: readU64(value.subarray(1)) };
1607
- }
1608
- var OFFER_CONTRACT_LABEL = "Arkade swap offer";
1609
- var OFFER_CONTRACT_KIND = "asset-swap-offer";
1610
- async function registerOfferContract(wallet, info, binding, operatorPubkey, expectedPkScript) {
1611
- const contractManager = await wallet.getContractManager();
1612
- const client = await import_sdk2.arkade.Arkade.connect({
1613
- // Registration derives and persists; it never broadcasts and never
1614
- // reads UTXOs, so the only thing the client needs off the server is the
1615
- // info the caller already resolved. Handing that back — rather than a
1616
- // provider built from a URL — is what lets `createOffer` take just a
1617
- // wallet, and spares a second `/v1/info` round-trip.
1618
- arkade: { getInfo: async () => info },
1619
- identity: wallet.identity,
1620
- // without this the row's `address` would be derived against the SDK's
1621
- // default network while its script is right — a row that disagrees with
1622
- // the address the user is about to fund
1623
- network: (0, import_sdk2.networkFromArkadeInfo)(info),
1624
- contractManager
1625
- });
1626
- await contractManager.createContract(
1627
- offerContractParams(client, binding, operatorPubkey, expectedPkScript)
1628
- );
1629
- await promoteOfferContract(contractManager, import_base2.hex.encode(expectedPkScript));
1630
- }
1631
- function offerContractParams(client, binding, operatorPubkey, expectedPkScript) {
1632
- const { program, args, keys } = swapProgramBinding(binding, operatorPubkey);
1633
- const contract = new import_sdk2.arkade.ArkadeContract(client, program, args, keys);
1634
- if (import_base2.hex.encode(contract.pkScript) !== import_base2.hex.encode(expectedPkScript)) {
1635
- throw new Error("derived covenant does not match the offer's swapPkScript");
1636
- }
1637
- return {
1638
- ...contract.toContractParams(),
1639
- label: OFFER_CONTRACT_LABEL,
1640
- metadata: { genericallySpendable: false, kind: OFFER_CONTRACT_KIND }
1140
+ const contractParams = {
1141
+ solverPubkey: (0, import_sdk5.toXOnly)(import_base7.hex.decode(quote.solver_pubkey), "solver key"),
1142
+ refundLocktime,
1143
+ operatorPubkey: input.operatorPubkey,
1144
+ paymentHash: input.paymentHash,
1145
+ claimDelay: input.claimDelay,
1146
+ emulatorPubkey: input.emulatorPubkey,
1147
+ solverRefundPkScript: solverHex(solverRefundPkScriptHex, "profile.solver_refund_pk_script"),
1148
+ payoutPubkey: input.payoutPubkey,
1149
+ payoutPkScript: import_sdk5.ArkAddress.decode(input.payoutAddress).pkScript
1641
1150
  };
1642
- }
1643
- function serverExitDelay(delay) {
1644
- if (typeof delay !== "bigint" || delay <= BigInt(0)) {
1645
- throw new Error(
1646
- "the server reports no usable unilateralExitDelay; pass `exitDelay` to set the offer's exit closure explicitly, or `noExit: true` to publish without one"
1647
- );
1648
- }
1649
- return assertExitDelay({ value: delay, type: delay < BigInt(512) ? "blocks" : "seconds" });
1650
- }
1651
- async function createOffer(wallet, params) {
1652
- if (Boolean(params.wantAsset) === Boolean(params.offerAsset)) {
1653
- throw new Error("set exactly one of wantAsset (BTC->asset) or offerAsset (asset->BTC)");
1654
- }
1655
- const [info, makerAddress, makerPublicKey] = await Promise.all([
1656
- // requireLive: this info binds signerPubkey into the covenant — a
1657
- // snapshot could derive an address the operator no longer co-signs
1658
- // for, so an unreachable operator fails the call instead (fail closed,
1659
- // the same behaviour the pre-#734 caller-held provider had)
1660
- wallet.getArkadeInfo({ requireLive: true }),
1661
- wallet.getAddress(),
1662
- wallet.identity.xOnlyPublicKey()
1663
- ]);
1664
- const operatorPubKey = import_base2.hex.decode((0, import_sdk2.toXOnlySignerHex)(info.signerPubkey));
1665
- const network = (0, import_sdk2.networkFromArkadeInfo)(info);
1666
- const emuKey = import_base2.hex.decode(
1667
- (0, import_sdk2.toXOnlySignerHex)((0, import_sdk2.resolveEmulatorPubkey)(network, params.emulatorPubkey))
1151
+ const matched = matchQuotedLockup(
1152
+ quote,
1153
+ input.hrp,
1154
+ input.operatorPubkey,
1155
+ (legacy) => lightningReceiveContract({ ...contractParams, ...legacy !== void 0 && { legacy } })
1668
1156
  );
1669
- const binding = {
1670
- wantAmount: params.wantAmount,
1671
- wantAsset: params.wantAsset,
1672
- offerAsset: params.offerAsset,
1673
- makerPkScript: import_sdk2.ArkAddress.decode(makerAddress).pkScript,
1674
- makerPublicKey,
1675
- emulatorPubkey: emuKey,
1676
- // checked HERE, before the covenant is derived and registered below:
1677
- // deferring it to `encodeOffer` leaves a registered contract behind for
1678
- // an offer that then fails to encode. @see assertExitDelay
1679
- exitDelay: params.noExit ? void 0 : params.exitDelay ? assertExitDelay(params.exitDelay) : serverExitDelay(info.unilateralExitDelay)
1680
- };
1681
- const script = offerContract(binding, operatorPubKey);
1682
- const offer = { ...binding, swapPkScript: script.pkScript };
1683
- await registerOfferContract(wallet, info, binding, operatorPubKey, script.pkScript);
1684
- const payload = encodeOffer(offer);
1685
1157
  return {
1686
- offerHex: import_base2.hex.encode(payload),
1687
- extension: { type: OFFER_PACKET_TYPE, payload },
1688
- // the contract's .address() builds the address; assembling an ArkAddress
1689
- // from tweakedPublicKey here would silently miss any future step it gains
1690
- address: script.address(network.hrp, operatorPubKey).encode(),
1691
- swapPkScript: script.pkScript
1158
+ address: matched.address,
1159
+ swapPkScript: matched.script.pkScript,
1160
+ script: matched.script,
1161
+ invoice,
1162
+ refundLocktime,
1163
+ contractParams: {
1164
+ ...contractParams,
1165
+ ...matched.legacy !== void 0 && { legacy: matched.legacy }
1166
+ }
1692
1167
  };
1693
1168
  }
1694
- var OFFER_COVENANT_MISMATCH = "rebuilt covenant does not match the offer's swapPkScript";
1695
- var NoSpendableDepositError = class extends Error {
1696
- name = "NoSpendableDepositError";
1697
- constructor(options) {
1698
- super("no spendable VTXO at the swap address", options);
1699
- }
1700
- };
1701
- var OfferCovenantMismatchError = class extends Error {
1702
- constructor(swapPkScript, options) {
1703
- super(
1704
- `${OFFER_COVENANT_MISMATCH} \u2014 the operator signing key pinned by the rebuild does not reproduce the funded covenant; the signing key has likely rotated since funding (pass swapAddress, the funded address, to pin the original key), or the record is corrupt`,
1705
- options
1706
- );
1707
- this.swapPkScript = swapPkScript;
1708
- }
1709
- swapPkScript;
1710
- name = "OfferCovenantMismatchError";
1711
- };
1712
- async function prepareOfferCancel(wallet, offerHex, opts) {
1713
- const { fundingTxid, swapAddress } = opts;
1714
- const offer = decodeOffer(import_base2.hex.decode(offerHex));
1715
- const [contractManager, info, reader, broadcaster] = await Promise.all([
1716
- wallet.getContractManager(),
1717
- wallet.getArkadeInfo({ requireLive: true }),
1718
- wallet.getArkadeReader(),
1719
- wallet.getArkadeBroadcaster()
1720
- ]);
1721
- const client = await import_sdk2.arkade.Arkade.connect({
1722
- // info the wallet already holds, plus its broadcast pair — the shape
1723
- // `ArkadeServerProvider` describes, without a second `/v1/info`
1724
- arkade: { getInfo: async () => info, ...broadcaster },
1725
- indexer: reader,
1726
- identity: wallet.identity,
1727
- // registered offers resolve their VTXOs from the contract repository
1728
- // instead of a direct indexer query; the indexer above stays as the
1729
- // fallback for offers created before registration existed
1730
- contractManager
1731
- // no `network`, unlike registerOfferContract: the row lookup is by
1732
- // script and the payout script comes from wallet.getAddress(), so the
1733
- // client's network (which only shapes address derivation) is unused here
1734
- });
1735
- const operatorPubkey = swapAddress ? import_sdk2.ArkAddress.decode(swapAddress).serverPubKey : client.serverKey;
1736
- const { program, args, keys } = swapProgramBinding(offer, operatorPubkey);
1737
- let rebuilt;
1738
- try {
1739
- rebuilt = new import_sdk2.arkade.ArkadeProgramScript(program, args, keys);
1740
- } catch (cause) {
1741
- throw new OfferCovenantMismatchError(import_base2.hex.encode(offer.swapPkScript), { cause });
1742
- }
1743
- if (import_base2.hex.encode(rebuilt.pkScript) !== import_base2.hex.encode(offer.swapPkScript)) {
1744
- throw new OfferCovenantMismatchError(import_base2.hex.encode(offer.swapPkScript));
1745
- }
1746
- const contract = new import_sdk2.arkade.ArkadeContract(client, program, args, keys);
1747
- const [vtxos, makerAddress] = await Promise.all([contract.getUtxos(), wallet.getAddress()]);
1748
- if (!fundingTxid && vtxos.length > 1) {
1749
- throw new Error(
1750
- "multiple spendable deposits at the swap address \u2014 pass fundingTxid to select one"
1751
- );
1169
+ var import_base7, import_solver_discovery, import_legacy2, import_sdk5, import_sdk6, solverHex, quoteSats, quoteAmountSats, quoteExpectedSats, ARKADE_BTC, LIGHTNING_BTC, ONCHAIN_BTC, rfqPair, LIGHTNING_SEND_PAIR, LIGHTNING_RECEIVE_PAIR, ONCHAIN_SEND_PAIR, ONCHAIN_RECEIVE_PAIR, RFQ_REFUSAL_ERROR_CODES, isRfqRefusalErrorCode, REFUSAL_FIELDS, REFUSAL_UNITS, safeInteger, safeRefusalDetail, refusalMessage, SwapRefusal, AddressMismatch, newRfqId, lightningSendRequest, MIN_HEADROOM_SECONDS, gateError, assertFinite, MAX_PAIR_LENGTH, assertPairLength, verifyLockupAddress, LOCKUP_SHAPE_VARIANTS, matchQuotedLockup, assertFundable, expectQuote, pairOf, SEQUENCE_GRANULARITY_SECONDS, SOLO_REFUND_HEADROOM_SECONDS, unilateralClaimDelay, unilateralRefundDelay, unilateralRefundWithoutReceiverDelay, l1NetworkFromArk, onchainSendRequest, lightningReceiveRequest, MIN_CLAIM_WINDOW_SECONDS, assertReceivable;
1170
+ var init_rfq = __esm({
1171
+ "src/rfq.ts"() {
1172
+ "use strict";
1173
+ import_base7 = require("@scure/base");
1174
+ import_solver_discovery = require("@arkade-os/solver-discovery");
1175
+ import_legacy2 = require("@noble/hashes/legacy.js");
1176
+ import_sdk5 = require("@arkade-os/sdk");
1177
+ init_onchainHtlc();
1178
+ init_onchainHtlc();
1179
+ import_sdk6 = require("@arkade-os/sdk");
1180
+ init_claimPacket();
1181
+ init_lockupContract();
1182
+ init_offer();
1183
+ solverHex = (value, field) => {
1184
+ try {
1185
+ return import_base7.hex.decode(value);
1186
+ } catch {
1187
+ throw new Error(`solver sent malformed hex for ${field}`);
1188
+ }
1189
+ };
1190
+ quoteSats = (value, field) => {
1191
+ if (typeof value !== "number" || !Number.isSafeInteger(value)) {
1192
+ throw new Error(`HTLC quote carries a non-sats ${field}: ${String(value)}`);
1193
+ }
1194
+ return value;
1195
+ };
1196
+ quoteAmountSats = (value, field) => {
1197
+ if (typeof value === "string" && (0, import_solver_discovery.isAmount)(value)) return BigInt(value);
1198
+ if (typeof value === "number" && Number.isSafeInteger(value)) return BigInt(value);
1199
+ throw new Error(`HTLC quote carries an unreadable ${field}: ${String(value)}`);
1200
+ };
1201
+ quoteExpectedSats = (value, field) => {
1202
+ const sats = quoteAmountSats(value, field);
1203
+ if (sats <= 0n || sats > BigInt(Number.MAX_SAFE_INTEGER)) {
1204
+ throw new Error(`HTLC quote carries an unusable ${field}: ${String(value)}`);
1205
+ }
1206
+ return Number(sats);
1207
+ };
1208
+ ARKADE_BTC = "arkade:BTC";
1209
+ LIGHTNING_BTC = "lightning:BTC";
1210
+ ONCHAIN_BTC = "onchain:BTC";
1211
+ rfqPair = (from, to) => `${from}->${to}`;
1212
+ LIGHTNING_SEND_PAIR = rfqPair(ARKADE_BTC, LIGHTNING_BTC);
1213
+ LIGHTNING_RECEIVE_PAIR = rfqPair(LIGHTNING_BTC, ARKADE_BTC);
1214
+ ONCHAIN_SEND_PAIR = rfqPair(ARKADE_BTC, ONCHAIN_BTC);
1215
+ ONCHAIN_RECEIVE_PAIR = rfqPair(ONCHAIN_BTC, ARKADE_BTC);
1216
+ RFQ_REFUSAL_ERROR_CODES = [
1217
+ "amount_side_unsupported",
1218
+ "exact_out_unsupported",
1219
+ "invalid_amount",
1220
+ "invalid_payout_address",
1221
+ "invalid_refund_address",
1222
+ "invoice_amount_mismatch",
1223
+ "invoice_cltv_too_large",
1224
+ "invoice_malformed",
1225
+ "invoice_missing_amount",
1226
+ "invoice_missing_network",
1227
+ "invoice_missing_payment_hash",
1228
+ "invoice_missing_timestamp",
1229
+ "invoice_mixed_case",
1230
+ "invoice_sub_satoshi_amount",
1231
+ "invoice_too_long",
1232
+ "invoice_wrong_network"
1233
+ ];
1234
+ isRfqRefusalErrorCode = (value) => typeof value === "string" && RFQ_REFUSAL_ERROR_CODES.includes(value);
1235
+ REFUSAL_FIELDS = /* @__PURE__ */ new Set([
1236
+ "amount",
1237
+ "amount_side",
1238
+ "profile.invoice",
1239
+ "profile.payout_address",
1240
+ "profile.refund_address"
1241
+ ]);
1242
+ REFUSAL_UNITS = /* @__PURE__ */ new Set(["blocks", "characters", "sats"]);
1243
+ safeInteger = (value) => typeof value === "number" && Number.isSafeInteger(value) && value >= 0 ? value : void 0;
1244
+ safeRefusalDetail = (detail) => {
1245
+ if (!isRfqRefusalErrorCode(detail.errorCode)) return {};
1246
+ return {
1247
+ errorCode: detail.errorCode,
1248
+ field: typeof detail.field === "string" && REFUSAL_FIELDS.has(detail.field) ? detail.field : void 0,
1249
+ actual: safeInteger(detail.actual),
1250
+ expected: safeInteger(detail.expected),
1251
+ limit: safeInteger(detail.limit),
1252
+ unit: typeof detail.unit === "string" && REFUSAL_UNITS.has(detail.unit) ? detail.unit : void 0
1253
+ };
1254
+ };
1255
+ refusalMessage = (reason, detail) => {
1256
+ if (!detail.errorCode) return `solver refused: ${reason}`;
1257
+ const where = detail.field ? ` at ${detail.field}` : "";
1258
+ const unit = detail.unit ? ` ${detail.unit}` : "";
1259
+ if (detail.actual !== void 0 && detail.limit !== void 0) {
1260
+ return `solver refused: ${reason} (${detail.errorCode}${where}: ${detail.actual}${unit}, limit ${detail.limit})`;
1261
+ }
1262
+ if (detail.actual !== void 0 && detail.expected !== void 0) {
1263
+ return `solver refused: ${reason} (${detail.errorCode}${where}: ${detail.actual}${unit}, expected ${detail.expected})`;
1264
+ }
1265
+ return `solver refused: ${reason} (${detail.errorCode}${where})`;
1266
+ };
1267
+ SwapRefusal = class extends Error {
1268
+ /** Literal-typed so the v2 error taxonomy's union discriminates on `name`
1269
+ * — a `string` here collapses the discriminant for every member. Same value
1270
+ * the constructor has always set, moved to a field initializer. */
1271
+ name = "SwapRefusal";
1272
+ reason;
1273
+ rfqId;
1274
+ errorCode;
1275
+ field;
1276
+ actual;
1277
+ expected;
1278
+ limit;
1279
+ unit;
1280
+ constructor(reason, rfqId, detail = {}) {
1281
+ const safeDetail = safeRefusalDetail(detail);
1282
+ super(refusalMessage(reason, safeDetail));
1283
+ this.reason = reason;
1284
+ this.rfqId = rfqId;
1285
+ this.errorCode = safeDetail.errorCode;
1286
+ this.field = safeDetail.field;
1287
+ this.actual = safeDetail.actual;
1288
+ this.expected = safeDetail.expected;
1289
+ this.limit = safeDetail.limit;
1290
+ this.unit = safeDetail.unit;
1291
+ }
1292
+ };
1293
+ AddressMismatch = class extends Error {
1294
+ derived;
1295
+ quoted;
1296
+ constructor(derived, quoted) {
1297
+ super("solver lockup address does not match local derivation \u2014 refusing to fund");
1298
+ this.name = "AddressMismatch";
1299
+ this.derived = derived;
1300
+ this.quoted = quoted;
1301
+ }
1302
+ };
1303
+ newRfqId = () => import_base7.hex.encode(crypto.getRandomValues(new Uint8Array(32)));
1304
+ lightningSendRequest = (input) => ({
1305
+ v: 1,
1306
+ type: "rfq_request",
1307
+ rfq_id: input.rfqId,
1308
+ pair: LIGHTNING_SEND_PAIR,
1309
+ amount_side: "to",
1310
+ profile: {
1311
+ invoice: input.invoice,
1312
+ refund_address: input.refundAddress,
1313
+ client_refund_pubkey: import_base7.hex.encode(input.senderPubkey)
1314
+ }
1315
+ });
1316
+ MIN_HEADROOM_SECONDS = 90 * 60;
1317
+ gateError = (reason, message) => {
1318
+ const error = new Error(message);
1319
+ error.reason = reason;
1320
+ return error;
1321
+ };
1322
+ assertFinite = (value, reason, label) => {
1323
+ if (value !== void 0 && !Number.isFinite(value)) {
1324
+ throw gateError(reason, `${label} is not a finite number (${String(value)})`);
1325
+ }
1326
+ };
1327
+ MAX_PAIR_LENGTH = 158;
1328
+ assertPairLength = (pair) => {
1329
+ if (pair.length > MAX_PAIR_LENGTH) {
1330
+ throw new Error(
1331
+ `pair is ${pair.length} characters, over the wire's ${MAX_PAIR_LENGTH}-character limit`
1332
+ );
1333
+ }
1334
+ };
1335
+ verifyLockupAddress = (quote, derivedAddress) => {
1336
+ const quoted = quote.profile?.lockup_address;
1337
+ const candidates = Array.isArray(derivedAddress) ? derivedAddress : [derivedAddress];
1338
+ const matched = candidates.find((address) => address === quoted);
1339
+ if (matched === void 0) throw new AddressMismatch(candidates, quoted);
1340
+ return matched;
1341
+ };
1342
+ LOCKUP_SHAPE_VARIANTS = [void 0, "preTimelockedRefund"];
1343
+ matchQuotedLockup = (quote, hrp, serverPubkey, build) => {
1344
+ const candidates = LOCKUP_SHAPE_VARIANTS.map((legacy) => {
1345
+ const script = build(legacy);
1346
+ return { script, address: script.address(hrp, serverPubkey).encode(), legacy };
1347
+ });
1348
+ const matchedAddress = verifyLockupAddress(
1349
+ quote,
1350
+ candidates.map((candidate) => candidate.address)
1351
+ );
1352
+ return candidates.find((candidate) => candidate.address === matchedAddress);
1353
+ };
1354
+ assertFundable = (input) => {
1355
+ const fail = (reason, message) => {
1356
+ throw gateError(reason, message);
1357
+ };
1358
+ if (input.quote.valid_until === void 0) {
1359
+ fail("quote_malformed", "quote carries no valid_until");
1360
+ }
1361
+ assertFinite(input.quote.valid_until, "quote_malformed", "quote valid_until");
1362
+ if (input.invoiceExpiresAt !== void 0 && input.now >= input.invoiceExpiresAt) {
1363
+ fail("invoice_expired", "invoice expired");
1364
+ }
1365
+ if (input.now >= input.quote.valid_until)
1366
+ fail("quote_expired", "quote expired \u2014 request a fresh one");
1367
+ if (quoteAmountSats(input.quote.from_amount, "from_amount") <= 0n || quoteAmountSats(input.quote.to_amount, "to_amount") <= 0n) {
1368
+ fail("non_positive_amount", "quote carries a non-positive amount");
1369
+ }
1370
+ if (input.quote.refund_locktime !== void 0 && input.quote.refund_locktime - input.now < MIN_HEADROOM_SECONDS) {
1371
+ fail("insufficient_headroom", "refund deadline headroom below 90 minutes");
1372
+ }
1373
+ if (input.maxFee) {
1374
+ const { bps, sats, referenceRate } = input.maxFee;
1375
+ if (bps === void 0 && sats === void 0) {
1376
+ fail("max_fee_unbounded", "maxFee names neither bps nor sats");
1377
+ }
1378
+ if (bps !== void 0 && (!Number.isInteger(bps) || bps < 0 || bps > 1e4)) {
1379
+ fail("max_fee_out_of_range", `maxFee.bps must be an integer in 0..10000, got ${bps}`);
1380
+ }
1381
+ if (sats !== void 0 && (!Number.isInteger(sats) || sats < 0)) {
1382
+ fail("max_fee_out_of_range", `maxFee.sats must be a non-negative integer, got ${sats}`);
1383
+ }
1384
+ const legs = input.quote.pair.split("->");
1385
+ const assetOf2 = (leg) => leg.slice(leg.indexOf(":") + 1);
1386
+ const sameAsset2 = legs.length === 2 && assetOf2(legs[0]) === assetOf2(legs[1]);
1387
+ if (!sameAsset2 && referenceRate === void 0) {
1388
+ fail(
1389
+ "fee_gate_unavailable",
1390
+ `maxFee cannot gate ${input.quote.pair}: its legs name different assets, so from_amount - to_amount is not a fee. Supply maxFee.referenceRate (to-units per from-unit) from a source of your OWN \u2014 reading it off the solver's published feed would check the solver against its own number`
1391
+ );
1392
+ }
1393
+ if (!sameAsset2 && (!Number.isFinite(referenceRate) || referenceRate <= 0)) {
1394
+ fail(
1395
+ "max_fee_out_of_range",
1396
+ `maxFee.referenceRate must be a positive finite number, got ${referenceRate}`
1397
+ );
1398
+ }
1399
+ const fromSats = quoteSats(input.quote.from_amount, "from_amount");
1400
+ const toSats = quoteSats(input.quote.to_amount, "to_amount");
1401
+ const fee = sameAsset2 ? fromSats - toSats : Math.ceil(
1402
+ (fromSats * referenceRate - toSats) / referenceRate
1403
+ );
1404
+ const allowed = Math.max(sats ?? 0, Math.floor(fromSats * (bps ?? 0) / 1e4));
1405
+ if (fee > allowed) {
1406
+ fail("fee_too_high", `fee ${fee} exceeds the ${allowed} this client allows`);
1407
+ }
1408
+ }
1409
+ if (input.onchain) {
1410
+ const { htlcLocktime, minConfirmations, direction } = input.onchain;
1411
+ if (!Number.isInteger(minConfirmations) || minConfirmations < 1 || minConfirmations > MAX_MIN_CONFIRMATIONS) {
1412
+ fail(
1413
+ "confirmations_out_of_range",
1414
+ `min_confirmations must be 1..${MAX_MIN_CONFIRMATIONS}, got ${minConfirmations}`
1415
+ );
1416
+ }
1417
+ const needed = minConfirmations * ONCHAIN_SECONDS_PER_BLOCK + ONCHAIN_CLAIM_MARGIN_SECONDS;
1418
+ if (htlcLocktime - input.now <= needed) {
1419
+ fail("claim_window_too_short", "L1 HTLC locktime leaves no safe claim window");
1420
+ }
1421
+ if (direction === "send") {
1422
+ if (input.quote.refund_locktime === void 0 || htlcLocktime + ONCHAIN_ORDER_MARGIN_SECONDS > input.quote.refund_locktime) {
1423
+ fail(
1424
+ "timelock_order",
1425
+ "L1 HTLC locktime + margin must fall before the Arkade refund locktime"
1426
+ );
1427
+ }
1428
+ }
1429
+ }
1430
+ };
1431
+ expectQuote = (payload, rfqId, requestedPair) => {
1432
+ const p = payload;
1433
+ if (p?.type === "rfq_refusal") {
1434
+ throw new SwapRefusal(p.reason ?? "unknown", p.rfq_id ?? rfqId, {
1435
+ errorCode: p.error_code,
1436
+ field: p.field,
1437
+ actual: p.actual,
1438
+ expected: p.expected,
1439
+ limit: p.limit,
1440
+ unit: p.unit
1441
+ });
1442
+ }
1443
+ if (p?.type !== "rfq_quote" || p.rfq_id !== rfqId) {
1444
+ throw new Error(`unexpected reply: ${p?.type ?? "no payload"}`);
1445
+ }
1446
+ if (requestedPair !== void 0 && p.pair !== requestedPair) {
1447
+ throw new Error(
1448
+ `solver quoted ${JSON.stringify(p.pair)}, not the requested ${requestedPair}`
1449
+ );
1450
+ }
1451
+ return payload;
1452
+ };
1453
+ pairOf = (payload) => typeof payload.pair === "string" ? payload.pair : void 0;
1454
+ SEQUENCE_GRANULARITY_SECONDS = 512;
1455
+ SOLO_REFUND_HEADROOM_SECONDS = 8 * SEQUENCE_GRANULARITY_SECONDS;
1456
+ unilateralClaimDelay = (operatorExitDelaySeconds) => {
1457
+ if (!Number.isFinite(operatorExitDelaySeconds) || operatorExitDelaySeconds < SEQUENCE_GRANULARITY_SECONDS) {
1458
+ throw new Error(
1459
+ `operator exit delay must be at least ${SEQUENCE_GRANULARITY_SECONDS}s of seconds, got ${operatorExitDelaySeconds}`
1460
+ );
1461
+ }
1462
+ if (operatorExitDelaySeconds > 65535 * SEQUENCE_GRANULARITY_SECONDS - SOLO_REFUND_HEADROOM_SECONDS) {
1463
+ throw new Error(
1464
+ `operator exit delay ${operatorExitDelaySeconds}s exceeds what BIP68 can encode once the solo refund's headroom is stacked above it`
1465
+ );
1466
+ }
1467
+ return Math.ceil(operatorExitDelaySeconds / SEQUENCE_GRANULARITY_SECONDS) * SEQUENCE_GRANULARITY_SECONDS;
1468
+ };
1469
+ unilateralRefundDelay = (claimDelay) => claimDelay;
1470
+ unilateralRefundWithoutReceiverDelay = (claimDelay) => claimDelay + SOLO_REFUND_HEADROOM_SECONDS;
1471
+ l1NetworkFromArk = (network) => network === "bitcoin" ? "bitcoin" : network === "regtest" ? "regtest" : "testnet";
1472
+ onchainSendRequest = (input) => ({
1473
+ v: 1,
1474
+ type: "rfq_request",
1475
+ rfq_id: input.rfqId,
1476
+ pair: ONCHAIN_SEND_PAIR,
1477
+ amount_side: input.amountSide,
1478
+ amount: input.amount,
1479
+ profile: {
1480
+ payment_hash: input.paymentHash,
1481
+ payout_pubkey: import_base7.hex.encode(input.payoutPubkey),
1482
+ refund_address: input.refundAddress,
1483
+ client_refund_pubkey: import_base7.hex.encode(input.senderPubkey)
1484
+ }
1485
+ });
1486
+ lightningReceiveRequest = (input) => ({
1487
+ v: 1,
1488
+ type: "rfq_request",
1489
+ rfq_id: input.rfqId,
1490
+ pair: LIGHTNING_RECEIVE_PAIR,
1491
+ amount_side: input.amountSide,
1492
+ amount: input.amount,
1493
+ profile: {
1494
+ payment_hash: input.paymentHash,
1495
+ payout_address: input.payoutAddress,
1496
+ payout_pubkey: import_base7.hex.encode(input.payoutPubkey),
1497
+ ...input.claimPacket === void 0 ? {} : { claim_packet: input.claimPacket }
1498
+ }
1499
+ });
1500
+ MIN_CLAIM_WINDOW_SECONDS = 30 * 60;
1501
+ assertReceivable = (input) => {
1502
+ assertFinite(input.payDeadline, "quote_malformed", "payDeadline");
1503
+ assertFinite(input.now, "invalid_gate_input", "now");
1504
+ assertFinite(input.minClaimWindowSeconds, "invalid_gate_input", "minClaimWindowSeconds");
1505
+ assertFinite(input.maxPayAmount, "invalid_gate_input", "maxPayAmount");
1506
+ const minClaimWindow = input.minClaimWindowSeconds ?? MIN_CLAIM_WINDOW_SECONDS;
1507
+ if (input.now >= input.payDeadline) {
1508
+ throw gateError("quote_expired", "quote or invoice already expired \u2014 request a fresh one");
1509
+ }
1510
+ if (input.quote.refund_locktime === void 0) {
1511
+ throw gateError("missing_refund_locktime", "receive quote carries no refund_locktime");
1512
+ }
1513
+ assertFinite(input.quote.refund_locktime, "quote_malformed", "quote refund_locktime");
1514
+ if (input.quote.refund_locktime - input.payDeadline < minClaimWindow) {
1515
+ throw gateError(
1516
+ "claim_window_too_short",
1517
+ `a payment at the deadline would leave under ${minClaimWindow}s to claim before the solver's refund opens`
1518
+ );
1519
+ }
1520
+ if (input.maxPayAmount !== void 0 && quoteSats(input.quote.from_amount, "from_amount") > input.maxPayAmount) {
1521
+ throw gateError(
1522
+ "price_too_high",
1523
+ `quote asks ${input.quote.from_amount} sats, above the ${input.maxPayAmount} ceiling`
1524
+ );
1525
+ }
1526
+ };
1752
1527
  }
1753
- const vtxo = fundingTxid ? vtxos.find((v) => v.txid === fundingTxid) : vtxos[0];
1754
- if (!vtxo) throw new NoSpendableDepositError();
1755
- const makerPkScript = import_sdk2.ArkAddress.decode(makerAddress).pkScript;
1756
- const cancel = contract.functions.cancel().from({ txid: vtxo.txid, vout: vtxo.vout, value: vtxo.value }).to(makerPkScript, BigInt(vtxo.value));
1757
- for (const a of vtxo.assets ?? []) {
1758
- cancel.withAsset({
1759
- assetId: a.assetId,
1760
- inputs: [{ vin: 0, amount: BigInt(a.amount) }],
1761
- outputs: [{ vout: 0, amount: BigInt(a.amount) }]
1528
+ });
1529
+
1530
+ // src/nostr.ts
1531
+ var nostr_exports = {};
1532
+ __export(nostr_exports, {
1533
+ RFQ_AD_KIND: () => RFQ_AD_KIND,
1534
+ RFQ_DIRECTED_KIND: () => RFQ_DIRECTED_KIND,
1535
+ RelayUnavailable: () => RelayUnavailable,
1536
+ TransportClosed: () => TransportClosed,
1537
+ nostrRfqTransport: () => nostrRfqTransport
1538
+ });
1539
+ var import_nostr_tools, RFQ_DIRECTED_KIND, RFQ_AD_KIND, DEFAULT_TIMEOUT_MS, RelayUnavailable, TransportClosed, closeReasons, nostrRfqTransport;
1540
+ var init_nostr = __esm({
1541
+ "src/nostr.ts"() {
1542
+ "use strict";
1543
+ init_rfq();
1544
+ import_nostr_tools = require("nostr-tools");
1545
+ RFQ_DIRECTED_KIND = 24859;
1546
+ RFQ_AD_KIND = 38859;
1547
+ DEFAULT_TIMEOUT_MS = 3e4;
1548
+ RelayUnavailable = class extends Error {
1549
+ reasons;
1550
+ constructor(reasons) {
1551
+ super(`lost every relay connection: ${reasons.join("; ") || "connection closed"}`);
1552
+ this.name = "RelayUnavailable";
1553
+ this.reasons = reasons;
1554
+ }
1555
+ };
1556
+ TransportClosed = class extends Error {
1557
+ constructor() {
1558
+ super("transport closed before the solver replied");
1559
+ this.name = "TransportClosed";
1560
+ }
1561
+ };
1562
+ closeReasons = (raw) => raw.map((entry) => {
1563
+ if (typeof entry === "string") return entry;
1564
+ const { url, reason } = entry ?? {};
1565
+ return url ? `${url}: ${reason ?? "closed"}` : reason ?? "closed";
1762
1566
  });
1567
+ nostrRfqTransport = (options) => {
1568
+ const relays = options.relays;
1569
+ const solverPubkey = options.solverPubkey;
1570
+ const secretKey = options.secretKey ?? (0, import_nostr_tools.generateSecretKey)();
1571
+ const pubkey = (0, import_nostr_tools.getPublicKey)(secretKey);
1572
+ const pool = options.pool ?? new import_nostr_tools.SimplePool();
1573
+ const ownsPool = !options.pool;
1574
+ const timeoutMs = options.timeoutMs ?? DEFAULT_TIMEOUT_MS;
1575
+ const conversationKey = import_nostr_tools.nip44.v2.utils.getConversationKey(secretKey, solverPubkey);
1576
+ let closed = false;
1577
+ const waiters = /* @__PURE__ */ new Map();
1578
+ const subscription = pool.subscribeMany(
1579
+ relays,
1580
+ { kinds: [RFQ_DIRECTED_KIND], "#p": [pubkey], authors: [solverPubkey] },
1581
+ {
1582
+ onevent(event) {
1583
+ let payload;
1584
+ try {
1585
+ payload = JSON.parse(import_nostr_tools.nip44.v2.decrypt(event.content, conversationKey));
1586
+ } catch {
1587
+ return;
1588
+ }
1589
+ const rfqId = payload?.rfq_id;
1590
+ if (!rfqId) return;
1591
+ waiters.get(rfqId)?.resolve(payload);
1592
+ },
1593
+ // subscribeMany calls this once every relay has closed. Nothing will
1594
+ // arrive after it, so failing now beats waiting out the timeout — and
1595
+ // it names the real cause instead of implicating the solver.
1596
+ onclose(reasons) {
1597
+ if (closed) return;
1598
+ const error = new RelayUnavailable(closeReasons(reasons));
1599
+ for (const waiter of waiters.values()) waiter.reject(error);
1600
+ waiters.clear();
1601
+ }
1602
+ }
1603
+ );
1604
+ const send = async (payload) => {
1605
+ const event = (0, import_nostr_tools.finalizeEvent)(
1606
+ {
1607
+ kind: RFQ_DIRECTED_KIND,
1608
+ created_at: Math.floor(Date.now() / 1e3),
1609
+ tags: [["p", solverPubkey]],
1610
+ content: import_nostr_tools.nip44.v2.encrypt(JSON.stringify(payload), conversationKey)
1611
+ },
1612
+ secretKey
1613
+ );
1614
+ const results = await Promise.allSettled(pool.publish(relays, event));
1615
+ if (!results.some((r) => r.status === "fulfilled")) {
1616
+ throw new Error("no relay accepted the RFQ message");
1617
+ }
1618
+ };
1619
+ const awaitReply = (rfqId) => new Promise((resolve, reject) => {
1620
+ const timer = setTimeout(() => {
1621
+ waiters.delete(rfqId);
1622
+ reject(new Error(`no solver reply within ${timeoutMs}ms`));
1623
+ }, timeoutMs);
1624
+ waiters.set(rfqId, {
1625
+ resolve: (payload) => {
1626
+ clearTimeout(timer);
1627
+ waiters.delete(rfqId);
1628
+ resolve(payload);
1629
+ },
1630
+ reject: (error) => {
1631
+ clearTimeout(timer);
1632
+ waiters.delete(rfqId);
1633
+ reject(error);
1634
+ }
1635
+ });
1636
+ });
1637
+ return {
1638
+ async requestQuote(payload) {
1639
+ const rfqId = String(payload.rfq_id);
1640
+ const reply = awaitReply(rfqId);
1641
+ await send(payload);
1642
+ return expectQuote(await reply, rfqId, pairOf(payload));
1643
+ },
1644
+ async status(rfqId) {
1645
+ const reply = awaitReply(rfqId);
1646
+ await send({ v: 1, type: "rfq_status_request", rfq_id: rfqId });
1647
+ const payload = await reply;
1648
+ if (payload?.type !== "rfq_status") return null;
1649
+ return payload;
1650
+ },
1651
+ async close() {
1652
+ closed = true;
1653
+ for (const waiter of waiters.values()) waiter.reject(new TransportClosed());
1654
+ waiters.clear();
1655
+ if (ownsPool) pool.close(relays);
1656
+ else subscription.close();
1657
+ }
1658
+ };
1659
+ };
1763
1660
  }
1764
- return {
1765
- offer,
1766
- vtxo: { txid: vtxo.txid, vout: vtxo.vout, value: vtxo.value },
1767
- send: async () => (await cancel.send()).txid
1768
- };
1769
- }
1770
- var ASSET_CARRIER_SATS = BigInt(import_sdk2.ASSET_CARRIER_SATS);
1661
+ });
1662
+
1663
+ // src/advanced.ts
1664
+ var advanced_exports = {};
1665
+ __export(advanced_exports, {
1666
+ ACTIVITY_TOKEN: () => ACTIVITY_TOKEN,
1667
+ ARKADE_ASSET_NAMESPACE: () => ARKADE_ASSET_NAMESPACE,
1668
+ AcceptConflict: () => AcceptConflict,
1669
+ AmbiguousDestination: () => AmbiguousDestination,
1670
+ Amount: () => Amount,
1671
+ AmountEncodingUnsupported: () => AmountEncodingUnsupported,
1672
+ AmountFormatError: () => AmountFormatError,
1673
+ AmountMismatch: () => AmountMismatch,
1674
+ AssetIdError: () => AssetIdError,
1675
+ BITCOIN_RAILS: () => BITCOIN_RAILS,
1676
+ BTC_ASSET_PART: () => BTC_ASSET_PART,
1677
+ CORRIDORS: () => CORRIDORS,
1678
+ CORRIDOR_FACTORIES: () => CORRIDOR_FACTORIES,
1679
+ CORRIDOR_PASS: () => CORRIDOR_PASS,
1680
+ ClientDisposed: () => ClientDisposed,
1681
+ CovclaimdRevealError: () => CovclaimdRevealError,
1682
+ DEFAULT_INVOICE_EXPIRY_SECONDS: () => DEFAULT_INVOICE_EXPIRY_SECONDS,
1683
+ DiscoverySnapshotUnavailable: () => DiscoverySnapshotUnavailable,
1684
+ FEED_TTL_MS: () => FEED_TTL_MS,
1685
+ INDEXED_NETWORKS: () => INDEXED_NETWORKS,
1686
+ INVOICE_HRPS: () => INVOICE_HRPS,
1687
+ InconsistentRoute: () => InconsistentRoute,
1688
+ InsufficientFunds: () => InsufficientFunds,
1689
+ LIGHTNING_DRIVE: () => LIGHTNING_DRIVE,
1690
+ LOCKUP_OWNER: () => LOCKUP_OWNER,
1691
+ MaxFeeExceeded: () => MaxFeeExceeded,
1692
+ MissingCorridorDep: () => MissingCorridorDep,
1693
+ NotCancellable: () => NotCancellable,
1694
+ OFFER_SWAP_ID_PREFIX: () => OFFER_SWAP_ID_PREFIX,
1695
+ ONCHAIN_CLAIM_FEE_RATE_SATVB: () => ONCHAIN_CLAIM_FEE_RATE_SATVB,
1696
+ ONCHAIN_DRIVE: () => ONCHAIN_DRIVE,
1697
+ OperatorUnreachable: () => OperatorUnreachable,
1698
+ QuoteExpired: () => QuoteExpired,
1699
+ QuoteVerificationFailed: () => QuoteVerificationFailed,
1700
+ RAILS: () => RAILS,
1701
+ REGISTRY_URL: () => REGISTRY_URL,
1702
+ RFQ_SWAP_ID_PREFIX: () => RFQ_SWAP_ID_PREFIX,
1703
+ SWAP_ERROR_NAMES: () => SWAP_ERROR_NAMES,
1704
+ SwapDriveRefusedError: () => SwapDriveRefusedError,
1705
+ SwapRefusal: () => SwapRefusal,
1706
+ UnsupportedRoute: () => UnsupportedRoute,
1707
+ acceptQuote: () => acceptQuote,
1708
+ aliasTableFrom: () => aliasTableFrom,
1709
+ applyOfferSpend: () => applyOfferSpend,
1710
+ arkadeAsset: () => arkadeAsset,
1711
+ arkadeCorridor: () => arkadeCorridor,
1712
+ artifactOf: () => artifactOf,
1713
+ assembleRoute: () => assembleRoute,
1714
+ assetPartOf: () => assetPartOf,
1715
+ assetSwapIdOf: () => assetSwapIdOf,
1716
+ attesting: () => attesting,
1717
+ bitcoinNetworkOf: () => bitcoinNetworkOf,
1718
+ btcOn: () => btcOn,
1719
+ cancelSwap: () => cancelSwap,
1720
+ canonicalAssetId: () => canonicalAssetId,
1721
+ cardMarketOf: () => cardMarketOf,
1722
+ chainSourceOver: () => chainSourceOver,
1723
+ chooseMarket: () => chooseMarket,
1724
+ conflictingFields: () => conflictingFields,
1725
+ corridorOfRail: () => corridorOfRail,
1726
+ corridorOutcome: () => corridorOutcome,
1727
+ corridorRecordStore: () => corridorRecordStore,
1728
+ corridorSet: () => corridorSet,
1729
+ covclaimdClient: () => covclaimdClient,
1730
+ createSwapClient: () => createSwapClient,
1731
+ createSwapDrive: () => createSwapDrive,
1732
+ decodeBolt11: () => decodeBolt11,
1733
+ decodeRfqAmount: () => decodeRfqAmount,
1734
+ discoveryIndex: () => discoveryIndex,
1735
+ eligibleMarkets: () => eligibleMarkets,
1736
+ encodeRfqAmount: () => encodeRfqAmount,
1737
+ enforceFeeCeiling: () => enforceFeeCeiling,
1738
+ esploraChainSource: () => esploraChainSource,
1739
+ esploraTip: () => esploraTip,
1740
+ exchange: () => exchange,
1741
+ familyOfSwapId: () => familyOfSwapId,
1742
+ fateMoved: () => fateMoved,
1743
+ feedFetch: () => feedFetch,
1744
+ formatAssetId: () => formatAssetId,
1745
+ fromAtomicDecimal: () => fromAtomicDecimal,
1746
+ fromRfqAmountSide: () => fromRfqAmountSide,
1747
+ fundsFromWallet: () => fundsFromWallet,
1748
+ instrumentOf: () => instrumentOf,
1749
+ isAddressable: () => isAddressable,
1750
+ isAssetId: () => isAssetId,
1751
+ isAtomicDecimal: () => isAtomicDecimal,
1752
+ isCardMarket: () => isCardMarket,
1753
+ isIndexedNetwork: () => isIndexedNetwork,
1754
+ isNetworkRef: () => isNetworkRef,
1755
+ isSwapError: () => isSwapError,
1756
+ isUsableCard: () => isUsableCard,
1757
+ issuanceOf: () => issuanceOf,
1758
+ legOf: () => legOf,
1759
+ lightningCorridor: () => lightningCorridor,
1760
+ liveArkadeInfo: () => liveArkadeInfo,
1761
+ marketBackendOf: () => marketBackendOf,
1762
+ marketKeyOf: () => marketKeyOf,
1763
+ marketRefOf: () => marketRefOf,
1764
+ networksOfInvoiceHrp: () => networksOfInvoiceHrp,
1765
+ nostrTransportFactory: () => nostrTransportFactory,
1766
+ offerFactsOf: () => offerFactsOf,
1767
+ offerOutcome: () => offerOutcome,
1768
+ offerRecordSource: () => offerRecordSource,
1769
+ onchainCorridor: () => onchainCorridor,
1770
+ parseAssetId: () => parseAssetId,
1771
+ parseRfqQuote: () => parseRfqQuote,
1772
+ pay: () => pay,
1773
+ publicAssetId: () => publicAssetId,
1774
+ quoteFromFeed: () => quoteFromFeed,
1775
+ quoteIdOfSwapId: () => quoteIdOfSwapId,
1776
+ quoteViaRfq: () => quoteViaRfq,
1777
+ railOf: () => railOf,
1778
+ railOfCorridor: () => railOfCorridor,
1779
+ readableRecord: () => readableRecord,
1780
+ readsChain: () => readsChain,
1781
+ receive: () => receive,
1782
+ recordArtifact: () => recordArtifact,
1783
+ recordEndpoint: () => recordEndpoint,
1784
+ recordInstrument: () => recordInstrument,
1785
+ recordLeg: () => recordLeg,
1786
+ recordOutcome: () => recordOutcome,
1787
+ resolveCorridorBase: () => resolveCorridorBase,
1788
+ resolveCorridorDeps: () => resolveCorridorDeps,
1789
+ resolveRoute: () => resolveRoute,
1790
+ restoredOfferRecord: () => restoredOfferRecord,
1791
+ revealClaimPacket: () => revealClaimPacket,
1792
+ revealFieldsFromScript: () => revealFieldsFromScript,
1793
+ rfqLeg: () => rfqLeg,
1794
+ rfqPairFor: () => rfqPairFor,
1795
+ rfqRecordOf: () => rfqRecordOf,
1796
+ sameAsset: () => sameAsset,
1797
+ satsOf: () => satsOf,
1798
+ scopedToRail: () => scopedToRail,
1799
+ splitRecords: () => splitRecords,
1800
+ swapOf: () => swapOf,
1801
+ swapRecordOf: () => swapRecordOf,
1802
+ toAtomicDecimal: () => toAtomicDecimal,
1803
+ toDiscoveryLeg: () => toDiscoveryLeg,
1804
+ toRfqAmountSide: () => toRfqAmountSide,
1805
+ toSafeNumber: () => toSafeNumber,
1806
+ usableMarkets: () => usableMarkets,
1807
+ verifyPair: () => verifyPair,
1808
+ verifyQuoteTtl: () => verifyQuoteTtl,
1809
+ verifyQuotedAmount: () => verifyQuotedAmount,
1810
+ verifyReceiveInvoiceFacts: () => verifyReceiveInvoiceFacts,
1811
+ verifyReceiveWindow: () => verifyReceiveWindow,
1812
+ verifyResponder: () => verifyResponder,
1813
+ verifySendInvoice: () => verifySendInvoice,
1814
+ verifySendWindow: () => verifySendWindow,
1815
+ verifyingDerivation: () => verifyingDerivation,
1816
+ walletLockupIndexer: () => walletLockupIndexer,
1817
+ withCanonicalAmount: () => withCanonicalAmount,
1818
+ withDepositFate: () => withDepositFate,
1819
+ withRfqState: () => withRfqState
1820
+ });
1821
+ module.exports = __toCommonJS(advanced_exports);
1771
1822
 
1772
1823
  // src/client/accept.ts
1824
+ var import_base8 = require("@scure/base");
1825
+ var import_sdk9 = require("@arkade-os/sdk");
1826
+ init_offer();
1773
1827
  init_lockupContract();
1774
1828
 
1775
1829
  // src/rfqCorridors.ts
@@ -1823,6 +1877,7 @@ var RfqCorridorRegistry = class {
1823
1877
  var rfqCorridorHandlers = new RfqCorridorRegistry();
1824
1878
 
1825
1879
  // src/rfqProfileParts.ts
1880
+ init_store();
1826
1881
  var rfqSecretsProfile = (secrets, paymentHash) => {
1827
1882
  const { signingDescriptor, ...preimage } = swapSecretsToRecord(secrets);
1828
1883
  return {
@@ -1952,6 +2007,7 @@ function onchainSendProfile(result) {
1952
2007
  network: result.l1Network,
1953
2008
  htlcAddress: result.htlc.address,
1954
2009
  minConfirmations: result.minConfirmations,
2010
+ expectedAmount: result.expectedAmount,
1955
2011
  payoutPkScript: import_base5.hex.encode(result.payoutPkScript)
1956
2012
  };
1957
2013
  }
@@ -1963,6 +2019,7 @@ var OnchainSendCorridor = {
1963
2019
  project: (swap) => {
1964
2020
  const send = swap;
1965
2021
  return {
2022
+ expectedAmount: send.expectedAmount,
1966
2023
  ...send.funding ? { funding: send.funding } : {},
1967
2024
  ...send.claimTxid ? { claimTxid: send.claimTxid } : {}
1968
2025
  };
@@ -1979,6 +2036,11 @@ var OnchainSendCorridor = {
1979
2036
  `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`
1980
2037
  );
1981
2038
  }
2039
+ if (!Number.isSafeInteger(profile.expectedAmount) || profile.expectedAmount <= 0) {
2040
+ throw new Error(
2041
+ `onchain_send record carries no usable expectedAmount (${String(profile.expectedAmount)}); the funded value cannot be checked \u2014 refusing to restore a swap that would claim an underfunded fill`
2042
+ );
2043
+ }
1982
2044
  const htlc = onchainHtlcScript(
1983
2045
  {
1984
2046
  // From the profile, not the covenant: the lockup commits to
@@ -2001,6 +2063,7 @@ var OnchainSendCorridor = {
2001
2063
  paymentHash,
2002
2064
  htlc,
2003
2065
  minConfirmations: profile.minConfirmations,
2066
+ expectedAmount: profile.expectedAmount,
2004
2067
  // Optional here, required at the write: throwing on an older
2005
2068
  // record would strand the refund it is still owed.
2006
2069
  ...profile.payoutPkScript ? { payoutPkScript: import_base5.hex.decode(profile.payoutPkScript) } : {},
@@ -2020,6 +2083,9 @@ rfqCorridorHandlers.register(LightningSendCorridor);
2020
2083
  rfqCorridorHandlers.register(LightningReceiveCorridor);
2021
2084
  rfqCorridorHandlers.register(OnchainSendCorridor);
2022
2085
 
2086
+ // src/client/accept.ts
2087
+ init_store();
2088
+
2023
2089
  // src/client/assetId.ts
2024
2090
  var import_sdk4 = require("@arkade-os/sdk");
2025
2091
  var RAILS = ["arkade", "bitcoin", "bolt11", "eip155"];
@@ -2168,7 +2234,8 @@ var issuanceOf = (id) => {
2168
2234
  };
2169
2235
 
2170
2236
  // src/client/aliases.ts
2171
- var import_solver_discovery = require("@arkade-os/solver-discovery");
2237
+ var import_solver_discovery2 = require("@arkade-os/solver-discovery");
2238
+ init_store();
2172
2239
 
2173
2240
  // src/client/errors.ts
2174
2241
  init_rfq();
@@ -2241,10 +2308,10 @@ var QuoteExpired = class extends Error {
2241
2308
  name = "QuoteExpired";
2242
2309
  };
2243
2310
  var MaxFeeExceeded = class extends Error {
2244
- constructor(quoteId, asset5, fee, maxFee) {
2245
- super(`fee ${fee} of ${asset5} exceeds the ${maxFee} ceiling`);
2311
+ constructor(quoteId, asset6, fee, maxFee) {
2312
+ super(`fee ${fee} of ${asset6} exceeds the ${maxFee} ceiling`);
2246
2313
  this.quoteId = quoteId;
2247
- this.asset = asset5;
2314
+ this.asset = asset6;
2248
2315
  this.fee = fee;
2249
2316
  this.maxFee = maxFee;
2250
2317
  }
@@ -2255,11 +2322,11 @@ var MaxFeeExceeded = class extends Error {
2255
2322
  name = "MaxFeeExceeded";
2256
2323
  };
2257
2324
  var InsufficientFunds = class extends Error {
2258
- constructor(asset5, required, available) {
2325
+ constructor(asset6, required, available) {
2259
2326
  super(
2260
- `funding needs ${required} of ${asset5}` + (available === void 0 ? "" : `, wallet holds ${available}`)
2327
+ `funding needs ${required} of ${asset6}` + (available === void 0 ? "" : `, wallet holds ${available}`)
2261
2328
  );
2262
- this.asset = asset5;
2329
+ this.asset = asset6;
2263
2330
  this.required = required;
2264
2331
  this.available = available;
2265
2332
  }
@@ -2297,9 +2364,9 @@ var NotCancellable = class extends Error {
2297
2364
  name = "NotCancellable";
2298
2365
  };
2299
2366
  var InconsistentRoute = class extends Error {
2300
- constructor(asset5, destination) {
2301
- super(`destination ${JSON.stringify(destination)} is not on ${asset5}'s chain`);
2302
- this.asset = asset5;
2367
+ constructor(asset6, destination) {
2368
+ super(`destination ${JSON.stringify(destination)} is not on ${asset6}'s chain`);
2369
+ this.asset = asset6;
2303
2370
  this.destination = destination;
2304
2371
  }
2305
2372
  asset;
@@ -2352,29 +2419,29 @@ function isSwapError(e, name) {
2352
2419
  // src/client/aliases.ts
2353
2420
  var toDiscoveryLeg = (id) => {
2354
2421
  const { rail, assetNamespace, assetReference } = parseAssetId(id);
2355
- const asset5 = `${assetNamespace}:${assetReference}`;
2422
+ const asset6 = `${assetNamespace}:${assetReference}`;
2356
2423
  if (rail === "arkade") {
2357
- if (asset5 === BTC_ASSET_PART)
2424
+ if (asset6 === BTC_ASSET_PART)
2358
2425
  return { corridor: "arkade", assetId: BTC_ASSET_ID, marketId: id };
2359
2426
  if (assetNamespace === ARKADE_ASSET_NAMESPACE) {
2360
2427
  return { corridor: "arkade", assetId: assetReference, marketId: id };
2361
2428
  }
2362
- throw new UnsupportedRoute(`the arkade corridor has no ${asset5}`);
2429
+ throw new UnsupportedRoute(`the arkade corridor has no ${asset6}`);
2363
2430
  }
2364
2431
  if (rail === "bolt11") {
2365
- if (asset5 === BTC_ASSET_PART)
2432
+ if (asset6 === BTC_ASSET_PART)
2366
2433
  return { corridor: "lightning", assetId: BTC_ASSET_ID, marketId: id };
2367
- throw new UnsupportedRoute(`the lightning corridor carries BTC only, not ${asset5}`);
2434
+ throw new UnsupportedRoute(`the lightning corridor carries BTC only, not ${asset6}`);
2368
2435
  }
2369
2436
  if (rail === "bitcoin") {
2370
- if (asset5 === BTC_ASSET_PART)
2437
+ if (asset6 === BTC_ASSET_PART)
2371
2438
  return { corridor: "onchain", assetId: BTC_ASSET_ID, marketId: id };
2372
- throw new UnsupportedRoute(`the onchain corridor carries BTC only, not ${asset5}`);
2439
+ throw new UnsupportedRoute(`the onchain corridor carries BTC only, not ${asset6}`);
2373
2440
  }
2374
2441
  throw new UnsupportedRoute(`no corridor serves ${id}`);
2375
2442
  };
2376
- var INDEXED_NETWORKS = import_solver_discovery.NETWORKS;
2377
- var isIndexedNetwork = (network) => (0, import_solver_discovery.isNetwork)(network);
2443
+ var INDEXED_NETWORKS = import_solver_discovery2.NETWORKS;
2444
+ var isIndexedNetwork = (network) => (0, import_solver_discovery2.isNetwork)(network);
2378
2445
  var networkOfRow = (row) => parseAssetId(row.id).reference;
2379
2446
  var canonicalAssetId = (input, table) => {
2380
2447
  if (isAssetId(input)) return input;
@@ -2606,7 +2673,7 @@ var ACTIVITY_TOKEN = {
2606
2673
  };
2607
2674
 
2608
2675
  // src/client/amount.ts
2609
- var import_solver_discovery2 = require("@arkade-os/solver-discovery");
2676
+ var import_solver_discovery3 = require("@arkade-os/solver-discovery");
2610
2677
  var AmountFormatError = class extends Error {
2611
2678
  constructor(reason, message) {
2612
2679
  super(message);
@@ -2619,10 +2686,10 @@ var SCALED_DECIMAL = /^([0-9]+)(?:\.([0-9]+))?$/;
2619
2686
  var ATOMIC_CEILING = 10n ** 30n;
2620
2687
  var MAX_SIGNIFICANT_DIGITS = 64;
2621
2688
  var assertDecimals = (decimals) => {
2622
- if (!Number.isInteger(decimals) || decimals < 0 || decimals > import_solver_discovery2.MAX_ASSET_DECIMALS) {
2689
+ if (!Number.isInteger(decimals) || decimals < 0 || decimals > import_solver_discovery3.MAX_ASSET_DECIMALS) {
2623
2690
  throw new AmountFormatError(
2624
2691
  "invalid_decimals",
2625
- `decimals must be an integer in [0, ${import_solver_discovery2.MAX_ASSET_DECIMALS}], got ${decimals}`
2692
+ `decimals must be an integer in [0, ${import_solver_discovery3.MAX_ASSET_DECIMALS}], got ${decimals}`
2626
2693
  );
2627
2694
  }
2628
2695
  };
@@ -2650,8 +2717,8 @@ var Amount = {
2650
2717
  * result would not survive the record boundary, so one law holds in both
2651
2718
  * directions.
2652
2719
  */
2653
- parse(display, asset5) {
2654
- assertDecimals(asset5.decimals);
2720
+ parse(display, asset6) {
2721
+ assertDecimals(asset6.decimals);
2655
2722
  const parts = SCALED_DECIMAL.exec(display);
2656
2723
  if (!parts) {
2657
2724
  throw new AmountFormatError(
@@ -2660,17 +2727,17 @@ var Amount = {
2660
2727
  );
2661
2728
  }
2662
2729
  const [, whole, fraction = ""] = parts;
2663
- if (fraction.length > asset5.decimals) {
2730
+ if (fraction.length > asset6.decimals) {
2664
2731
  throw new AmountFormatError(
2665
2732
  "too_precise",
2666
- `${display} has more precision than ${asset5.decimals} decimals allow`
2733
+ `${display} has more precision than ${asset6.decimals} decimals allow`
2667
2734
  );
2668
2735
  }
2669
2736
  if (whole.length + fraction.length > MAX_SIGNIFICANT_DIGITS) {
2670
2737
  throw new AmountFormatError("too_large", `${display} has too many digits`);
2671
2738
  }
2672
- const value = (0, import_solver_discovery2.toAtomic)(display, asset5.decimals);
2673
- assertRepresentable(value, `${display} at ${asset5.decimals} decimals`);
2739
+ const value = (0, import_solver_discovery3.toAtomic)(display, asset6.decimals);
2740
+ assertRepresentable(value, `${display} at ${asset6.decimals} decimals`);
2674
2741
  return value;
2675
2742
  },
2676
2743
  /**
@@ -2679,13 +2746,13 @@ var Amount = {
2679
2746
  * Refuses a negative: every amount on this surface is an obligation, and a
2680
2747
  * negative one is a defect upstream rather than a number to render.
2681
2748
  */
2682
- format(value, asset5) {
2683
- assertDecimals(asset5.decimals);
2749
+ format(value, asset6) {
2750
+ assertDecimals(asset6.decimals);
2684
2751
  if (value < 0n) {
2685
2752
  throw new AmountFormatError("negative", `amount must be non-negative, got ${value}`);
2686
2753
  }
2687
2754
  assertRepresentable(value, `${value}`);
2688
- return (0, import_solver_discovery2.fromAtomic)(value, asset5.decimals);
2755
+ return (0, import_solver_discovery3.fromAtomic)(value, asset6.decimals);
2689
2756
  }
2690
2757
  };
2691
2758
  var toAtomicDecimal = (value) => {
@@ -2696,7 +2763,7 @@ var toAtomicDecimal = (value) => {
2696
2763
  return value.toString();
2697
2764
  };
2698
2765
  var fromAtomicDecimal = (text) => {
2699
- if (!(0, import_solver_discovery2.isAmount)(text)) {
2766
+ if (!(0, import_solver_discovery3.isAmount)(text)) {
2700
2767
  throw new AmountFormatError(
2701
2768
  "not_canonical",
2702
2769
  `not a canonical atomic amount: ${JSON.stringify(text)}`
@@ -2704,10 +2771,10 @@ var fromAtomicDecimal = (text) => {
2704
2771
  }
2705
2772
  return BigInt(text);
2706
2773
  };
2707
- var isAtomicDecimal = (value) => typeof value === "string" && import_solver_discovery2.AMOUNT_PATTERN.test(value);
2774
+ var isAtomicDecimal = (value) => typeof value === "string" && import_solver_discovery3.AMOUNT_PATTERN.test(value);
2708
2775
 
2709
2776
  // src/client/rfqAmount.ts
2710
- var import_solver_discovery3 = require("@arkade-os/solver-discovery");
2777
+ var import_solver_discovery4 = require("@arkade-os/solver-discovery");
2711
2778
  var toRfqAmountSide = (on) => on === "give" ? "from" : "to";
2712
2779
  var fromRfqAmountSide = (side) => side === "from" ? "give" : "take";
2713
2780
  var encodeRfqAmount = (value, field) => {
@@ -2724,7 +2791,7 @@ var encodeRfqAmount = (value, field) => {
2724
2791
  };
2725
2792
  var decodeRfqAmount = (raw, field) => {
2726
2793
  if (typeof raw === "string") {
2727
- if (!(0, import_solver_discovery3.isAmount)(raw)) {
2794
+ if (!(0, import_solver_discovery4.isAmount)(raw)) {
2728
2795
  throw new AmountEncodingUnsupported(
2729
2796
  field,
2730
2797
  raw,
@@ -3010,6 +3077,7 @@ var profileOf = (preparation, paymentHash) => {
3010
3077
  htlcParams: preparation.htlcParams,
3011
3078
  l1Network: preparation.l1Network,
3012
3079
  minConfirmations: preparation.minConfirmations,
3080
+ expectedAmount: toSafeNumber(preparation.expectedAmount, "expectedAmount"),
3013
3081
  payoutPkScript: preparation.payoutPkScript
3014
3082
  })
3015
3083
  };
@@ -3136,6 +3204,9 @@ var depositMatches = (vtxo, record, expected) => {
3136
3204
  };
3137
3205
  var swapRecordOf = async (repository, id) => storageOf(repository).getSwapRecord(id);
3138
3206
 
3207
+ // src/client/aliasTable.ts
3208
+ init_store();
3209
+
3139
3210
  // src/marketShape.ts
3140
3211
  var MARKET_CORRIDORS = ["arkade", "bolt11", "bitcoin", "eip155"];
3141
3212
  var LEGACY_CORRIDORS = {
@@ -3146,9 +3217,9 @@ var LEGACY_CORRIDORS = {
3146
3217
  };
3147
3218
  var assetOf = (market, side) => side === "base" ? market.base_asset : market.quote_asset;
3148
3219
  var marketAssetId = (market, side) => {
3149
- const asset5 = assetOf(market, side);
3150
- if (typeof asset5?.caip19_id === "string") return asset5.caip19_id;
3151
- return typeof asset5?.id === "string" ? asset5.id : void 0;
3220
+ const asset6 = assetOf(market, side);
3221
+ if (typeof asset6?.caip19_id === "string") return asset6.caip19_id;
3222
+ return typeof asset6?.id === "string" ? asset6.id : void 0;
3152
3223
  };
3153
3224
  var chainNamespaceOf = (id) => {
3154
3225
  if (id === void 0) return void 0;
@@ -3260,30 +3331,26 @@ var LockupNeedsRecoveryError = class extends Error {
3260
3331
  this.recoverableAfter = recoverableAfter;
3261
3332
  }
3262
3333
  };
3263
- async function findLockupVtxos(indexer, swapPkScript) {
3264
- const scripts = [import_base9.hex.encode(swapPkScript)];
3265
- const [spendable, recoverable] = await Promise.all([
3266
- indexer.getVtxos({ scripts, spendableOnly: true }),
3267
- indexer.getVtxos({ scripts, recoverableOnly: true })
3268
- ]);
3334
+ async function findLockupVtxos(contracts, swapPkScript) {
3335
+ const [row] = await contracts.getContractsWithVtxos({
3336
+ script: import_base9.hex.encode(swapPkScript)
3337
+ });
3269
3338
  const seen = /* @__PURE__ */ new Set();
3270
3339
  const out = [];
3271
- for (const [vtxos, isRecoverable] of [
3272
- [spendable.vtxos ?? [], false],
3273
- [recoverable.vtxos ?? [], true]
3274
- ]) {
3275
- for (const vtxo of vtxos) {
3276
- if (vtxo.isUnrolled) continue;
3277
- const key = `${vtxo.txid}:${vtxo.vout}`;
3278
- if (seen.has(key)) continue;
3279
- seen.add(key);
3280
- out.push({
3281
- txid: vtxo.txid,
3282
- vout: vtxo.vout,
3283
- value: Number(vtxo.value),
3284
- recoverable: isRecoverable
3285
- });
3286
- }
3340
+ for (const vtxo of row?.vtxos ?? []) {
3341
+ if (vtxo.isUnrolled) continue;
3342
+ if ((0, import_sdk10.hasTerminalSpend)(vtxo)) continue;
3343
+ const key = `${vtxo.txid}:${vtxo.vout}`;
3344
+ if (seen.has(key)) continue;
3345
+ seen.add(key);
3346
+ out.push({
3347
+ txid: vtxo.txid,
3348
+ vout: vtxo.vout,
3349
+ value: Number(vtxo.value),
3350
+ // The manager's canonical `isSwept` fact; no coercion, because the
3351
+ // normalized row guarantees a boolean.
3352
+ recoverable: vtxo.isSwept
3353
+ });
3287
3354
  }
3288
3355
  return out;
3289
3356
  }
@@ -3684,11 +3751,12 @@ var corridorSet = (base, overrides) => {
3684
3751
  };
3685
3752
 
3686
3753
  // src/client/discovery.ts
3687
- var import_solver_discovery5 = require("@arkade-os/solver-discovery");
3754
+ var import_solver_discovery6 = require("@arkade-os/solver-discovery");
3688
3755
 
3689
3756
  // src/markets.ts
3690
- var import_solver_discovery4 = require("@arkade-os/solver-discovery");
3757
+ var import_solver_discovery5 = require("@arkade-os/solver-discovery");
3691
3758
  var import_sdk15 = require("@arkade-os/sdk");
3759
+ init_store();
3692
3760
  var QUOTE_OPTIONS = { safetyBps: 0 };
3693
3761
  var makeCachedFeedFetch = (ttlMs = 3e4, fetchImpl = fetch) => {
3694
3762
  const cache = /* @__PURE__ */ new Map();
@@ -3745,7 +3813,7 @@ var REGISTRY_URL = {
3745
3813
  };
3746
3814
  var HEX_64 = /^[0-9a-f]{64}$/;
3747
3815
  var LEGACY_ASSET_ID = /^(?:btc|[0-9a-f]{68})$/;
3748
- var isRelayList = (value) => Array.isArray(value) && value.length > 0 && value.length <= import_solver_discovery5.MAX_RELAYS && value.every((relay) => typeof relay === "string" && relay.startsWith("wss://"));
3816
+ var isRelayList = (value) => Array.isArray(value) && value.length > 0 && value.length <= import_solver_discovery6.MAX_RELAYS && value.every((relay) => typeof relay === "string" && relay.startsWith("wss://"));
3749
3817
  var isCorridorField = (value) => value === void 0 || CORRIDORS.includes(value);
3750
3818
  var isUsableCard = (value) => {
3751
3819
  const card = value;
@@ -3757,7 +3825,7 @@ var isUsableCard = (value) => {
3757
3825
  const id = marketAssetId(card, side);
3758
3826
  if (id === void 0 || !isAssetId(id) && !LEGACY_ASSET_ID.test(id)) return false;
3759
3827
  }
3760
- if ((0, import_solver_discovery5.isRfqMarket)(card)) {
3828
+ if ((0, import_solver_discovery6.isRfqMarket)(card)) {
3761
3829
  if (typeof card.discovery_pubkey !== "string" || !HEX_64.test(card.discovery_pubkey)) {
3762
3830
  return false;
3763
3831
  }
@@ -3833,7 +3901,7 @@ var discoveryIndex = (input) => {
3833
3901
  if (!indexed || !url) {
3834
3902
  return unavailable(whyUnavailable());
3835
3903
  }
3836
- const result = await (0, import_solver_discovery5.discover)({
3904
+ const result = await (0, import_solver_discovery6.discover)({
3837
3905
  registries: [url],
3838
3906
  localCards: [...config.localCards ?? []],
3839
3907
  network: indexed,
@@ -3869,11 +3937,15 @@ var discoveryIndex = (input) => {
3869
3937
 
3870
3938
  // src/client/cancel.ts
3871
3939
  var import_base12 = require("@scure/base");
3872
- var import_sdk17 = require("@arkade-os/sdk");
3940
+ var import_sdk18 = require("@arkade-os/sdk");
3941
+ init_coverage();
3942
+ init_offer();
3873
3943
 
3874
3944
  // src/restore.ts
3875
3945
  var import_base11 = require("@scure/base");
3876
3946
  var import_sdk16 = require("@arkade-os/sdk");
3947
+ init_offer();
3948
+ init_store();
3877
3949
  var TXS_PER_REQUEST = 50;
3878
3950
  async function fetchParsedTxs(indexer, txids) {
3879
3951
  const parsedByTxid = /* @__PURE__ */ new Map();
@@ -3935,7 +4007,7 @@ function classifyDepositSpend(offer, operatorPubkey, spendTxs, deposit) {
3935
4007
  return "indeterminate";
3936
4008
  }
3937
4009
  async function restoreAssetSwaps(indexer, txs, existingIds, opts) {
3938
- const { operatorPubkey, scanned = /* @__PURE__ */ new Set(), reopen = [] } = opts;
4010
+ const { operatorPubkey, scanned = /* @__PURE__ */ new Set(), reopen = [], hrp } = opts;
3939
4011
  const reopened = [];
3940
4012
  for (const swap of reopen) {
3941
4013
  try {
@@ -4036,17 +4108,27 @@ async function restoreAssetSwaps(indexer, txs, existingIds, opts) {
4036
4108
  restored.push({ ...existing, status, spentTxid, ...completion });
4037
4109
  continue;
4038
4110
  }
4111
+ let swapAddress = "";
4112
+ try {
4113
+ const compiled = offerContract(offer, operatorPubkey);
4114
+ if (import_base11.hex.encode(compiled.pkScript) !== swapPkScript) {
4115
+ unresolved.add(fundingTx.redeemTxid);
4116
+ continue;
4117
+ }
4118
+ if (hrp !== void 0) {
4119
+ swapAddress = compiled.address(hrp, operatorPubkey).encode();
4120
+ }
4121
+ } catch {
4122
+ unresolved.add(fundingTx.redeemTxid);
4123
+ continue;
4124
+ }
4039
4125
  restored.push({
4040
4126
  id: fundingTx.redeemTxid,
4041
4127
  fromAsset,
4042
4128
  toAsset,
4043
4129
  fromAmount,
4044
4130
  toAmount: offer.wantAmount.toString(),
4045
- // ponytail(arkade-os/ts-sdk#680): empty address makes cancel fall back
4046
- // to the current operator key; store the funded address if operator-key
4047
- // rotations become real (cancelOffer now at least diagnoses the
4048
- // mismatch instead of reporting a missing VTXO)
4049
- swapAddress: "",
4131
+ swapAddress,
4050
4132
  swapPkScript,
4051
4133
  offerHex,
4052
4134
  fundingTxid: fundingTx.redeemTxid,
@@ -4062,6 +4144,8 @@ async function restoreAssetSwaps(indexer, txs, existingIds, opts) {
4062
4144
  }
4063
4145
 
4064
4146
  // src/client/driveRecords.ts
4147
+ init_store();
4148
+ var import_sdk17 = require("@arkade-os/sdk");
4065
4149
  var splitRecords = (records) => {
4066
4150
  const corridor = [];
4067
4151
  const offer = [];
@@ -4203,7 +4287,44 @@ var applyOfferSpend = (record, changes, now) => ({
4203
4287
  ...changes.completedAt === void 0 ? {} : { completedAt: Math.floor(changes.completedAt / 1e3) },
4204
4288
  updatedAt: now
4205
4289
  });
4206
- var withOfferStatus = (record, status, now) => ({ ...record, status, updatedAt: now });
4290
+ var withDepositFate = (record, fate, now) => ({
4291
+ ...record,
4292
+ status: fate.status,
4293
+ ...fate.spentTxid === void 0 ? {} : { spentTxid: fate.spentTxid },
4294
+ ...fate.completedAt === void 0 ? {} : { completedAt: Math.floor(fate.completedAt / 1e3) },
4295
+ updatedAt: now
4296
+ });
4297
+ var fateMoved = (record, fate) => record.status !== fate.status || fate.spentTxid !== void 0 && record.spentTxid !== fate.spentTxid || fate.completedAt !== void 0 && record.completedAt !== Math.floor(fate.completedAt / 1e3);
4298
+ var restoredOfferRecord = (swap, network, now) => {
4299
+ const give = restoredAssetId(network, swap.fromAsset);
4300
+ const take = restoredAssetId(network, swap.toAsset);
4301
+ const createdAt = Math.floor(swap.createdAt / 1e3);
4302
+ return withDepositFate(
4303
+ {
4304
+ id: swap.fundingTxid,
4305
+ family: "offer",
4306
+ route: {
4307
+ give: { corridor: "arkade", asset: give, instrument: { kind: "wallet" } },
4308
+ take: { corridor: "arkade", asset: take, instrument: { kind: "wallet" } }
4309
+ },
4310
+ give: { asset: give, amount: toAtomicDecimal(BigInt(swap.fromAmount)) },
4311
+ take: { asset: take, amount: toAtomicDecimal(BigInt(swap.toAmount)) },
4312
+ fee: { asset: take, amount: toAtomicDecimal(BigInt(0)) },
4313
+ market: { kind: "restored", backend: "feed" },
4314
+ expiresAt: createdAt,
4315
+ status: swap.status,
4316
+ offerHex: swap.offerHex,
4317
+ swapAddress: swap.swapAddress,
4318
+ swapPkScript: swap.swapPkScript,
4319
+ fundingTxid: swap.fundingTxid,
4320
+ createdAt,
4321
+ updatedAt: now
4322
+ },
4323
+ swap,
4324
+ now
4325
+ );
4326
+ };
4327
+ var restoredAssetId = (network, id) => id === BTC_ASSET_ID ? btcOn("arkade", network) : arkadeAsset(network, import_sdk17.asset.AssetId.fromString(id));
4207
4328
  var walletLockupIndexer = (wallet) => {
4208
4329
  let reader;
4209
4330
  const reading = () => reader ??= wallet.getArkadeReader();
@@ -4293,12 +4414,12 @@ var reconcileDepositSpend = async (record, indexer) => {
4293
4414
  const { txs } = await indexer.getVirtualTxs(candidates);
4294
4415
  const parsed = txs.map((psbt) => {
4295
4416
  try {
4296
- return import_sdk17.Transaction.fromPSBT(import_base12.base64.decode(psbt));
4417
+ return import_sdk18.Transaction.fromPSBT(import_base12.base64.decode(psbt));
4297
4418
  } catch {
4298
4419
  return void 0;
4299
4420
  }
4300
4421
  }).filter((tx) => tx !== void 0);
4301
- const operatorPubkey = import_sdk17.ArkAddress.decode(record.swapAddress).serverPubKey;
4422
+ const operatorPubkey = import_sdk18.ArkAddress.decode(record.swapAddress).serverPubKey;
4302
4423
  const kind = classifyDepositSpend(offer, operatorPubkey, parsed, {
4303
4424
  txid: deposit.txid,
4304
4425
  vout: deposit.vout
@@ -4337,14 +4458,14 @@ var retireOfferScripts = async (wallet, repository, record) => {
4337
4458
  var isMissingVtxo = (error) => error instanceof NoSpendableDepositError;
4338
4459
 
4339
4460
  // src/client/drive.ts
4340
- var import_sdk23 = require("@arkade-os/sdk");
4461
+ var import_sdk24 = require("@arkade-os/sdk");
4341
4462
  var import_base17 = require("@scure/base");
4342
4463
 
4343
4464
  // src/claim.ts
4344
4465
  var import_base13 = require("@scure/base");
4345
4466
  var import_legacy3 = require("@noble/hashes/legacy.js");
4346
4467
  var import_sha25 = require("@noble/hashes/sha2.js");
4347
- var import_sdk18 = require("@arkade-os/sdk");
4468
+ var import_sdk19 = require("@arkade-os/sdk");
4348
4469
  var LockupAmountMismatchError = class extends Error {
4349
4470
  name = "LockupAmountMismatchError";
4350
4471
  reason = "amount_mismatch";
@@ -4388,14 +4509,14 @@ async function pushClaim(operator, input) {
4388
4509
  const info = await operator.getInfo();
4389
4510
  let operatorUnrollScript;
4390
4511
  try {
4391
- operatorUnrollScript = import_sdk18.CSVMultisigTapscript.decode(import_base13.hex.decode(info.checkpointTapscript));
4512
+ operatorUnrollScript = import_sdk19.CSVMultisigTapscript.decode(import_base13.hex.decode(info.checkpointTapscript));
4392
4513
  } catch {
4393
4514
  throw new Error("invalid checkpointTapscript from the operator");
4394
4515
  }
4395
4516
  const leaf = input.contract.claim();
4396
4517
  const tapTree = input.contract.encode();
4397
- const txid = await (0, import_sdk18.signAndSubmitOffchainTx)({
4398
- identity: (0, import_sdk18.claimWithPreimageIdentity)(input.receiver, input.preimage),
4518
+ const txid = await (0, import_sdk19.signAndSubmitOffchainTx)({
4519
+ identity: (0, import_sdk19.claimWithPreimageIdentity)(input.receiver, input.preimage),
4399
4520
  provider: operator,
4400
4521
  inputs: input.vtxos.map((vtxo) => ({
4401
4522
  txid: vtxo.txid,
@@ -4415,10 +4536,11 @@ async function pushClaim(operator, input) {
4415
4536
 
4416
4537
  // src/client/drive.ts
4417
4538
  init_onchainHtlc();
4539
+ init_coverage();
4418
4540
  init_lockupContract();
4419
4541
 
4420
4542
  // src/refundBlocked.ts
4421
- var import_sdk19 = require("@arkade-os/sdk");
4543
+ var import_sdk20 = require("@arkade-os/sdk");
4422
4544
  var RefundNotLocallyPossibleError = class extends Error {
4423
4545
  constructor(reason, message, options) {
4424
4546
  super(message, options);
@@ -4435,16 +4557,16 @@ async function senderIdentityForSwapRecord(wallet, record) {
4435
4557
  );
4436
4558
  }
4437
4559
  try {
4438
- return await (0, import_sdk19.contractSigner)(wallet, record.signingDescriptor);
4560
+ return await (0, import_sdk20.contractSigner)(wallet, record.signingDescriptor);
4439
4561
  } catch (cause) {
4440
- if (cause instanceof import_sdk19.WalletCannotSignError) {
4562
+ if (cause instanceof import_sdk20.WalletCannotSignError) {
4441
4563
  throw new RefundNotLocallyPossibleError(
4442
4564
  "unsignable-wallet",
4443
4565
  `this wallet holds ${record.signingDescriptor} but cannot sign with it; attach its signer`,
4444
4566
  { cause }
4445
4567
  );
4446
4568
  }
4447
- if (cause instanceof import_sdk19.ForeignDescriptorError) {
4569
+ if (cause instanceof import_sdk20.ForeignDescriptorError) {
4448
4570
  throw new RefundNotLocallyPossibleError(
4449
4571
  "foreign-descriptor",
4450
4572
  `this wallet cannot derive ${record.signingDescriptor}; the swap was created on another wallet`,
@@ -4464,7 +4586,7 @@ function arkadeRefunder(deps) {
4464
4586
  `swap ${swap.rfqId} carries no lockup covenant, so its refund cannot be built`
4465
4587
  );
4466
4588
  }
4467
- const vtxos = await findLockupVtxos(deps.indexer, swap.lockupPkScript);
4589
+ const vtxos = await findLockupVtxos(deps.contracts, swap.lockupPkScript);
4468
4590
  if (vtxos.length === 0) return null;
4469
4591
  const record = await deps.repository.getRfqSwap(swap.rfqId);
4470
4592
  if (!record) {
@@ -4479,7 +4601,7 @@ function arkadeRefunder(deps) {
4479
4601
  }
4480
4602
 
4481
4603
  // src/rfqRecord.ts
4482
- var import_sdk20 = require("@arkade-os/sdk");
4604
+ var import_sdk21 = require("@arkade-os/sdk");
4483
4605
  var import_base14 = require("@scure/base");
4484
4606
 
4485
4607
  // src/rfqSwapState.ts
@@ -4525,7 +4647,7 @@ function assertSameSwap(origin, swap) {
4525
4647
  `rfq swap record is a ${origin.kind} origin paired with a ${swap.kind} swap`
4526
4648
  );
4527
4649
  }
4528
- const funded = import_base14.hex.encode(import_sdk20.ArkAddress.decode(origin.lockupAddress).pkScript);
4650
+ const funded = import_base14.hex.encode(import_sdk21.ArkAddress.decode(origin.lockupAddress).pkScript);
4529
4651
  const watched = import_base14.hex.encode(swap.lockupPkScript);
4530
4652
  if (funded !== watched) {
4531
4653
  throw new Error(
@@ -4574,8 +4696,8 @@ function rfqSwapOriginOf(record) {
4574
4696
  };
4575
4697
  }
4576
4698
  function lockupScript(params, lockupAddress) {
4577
- const script = import_sdk20.VHTLCV2ContractHandler.createScript(params);
4578
- const funded = import_sdk20.ArkAddress.decode(lockupAddress).pkScript;
4699
+ const script = import_sdk21.VHTLCV2ContractHandler.createScript(params);
4700
+ const funded = import_sdk21.ArkAddress.decode(lockupAddress).pkScript;
4579
4701
  if (import_base14.hex.encode(funded) !== import_base14.hex.encode(script.pkScript)) {
4580
4702
  throw new Error(
4581
4703
  `rfq swap covenant params derive ${import_base14.hex.encode(script.pkScript)}, but the record's lockup address holds ${import_base14.hex.encode(funded)} \u2014 these params are not this swap's`
@@ -4615,11 +4737,15 @@ function shouldRetainRfqSwap(record, now) {
4615
4737
  return now - record.updatedAt < RFQ_SWAP_RETENTION_SECONDS;
4616
4738
  }
4617
4739
 
4740
+ // src/client/drive.ts
4741
+ init_store();
4742
+
4618
4743
  // src/swapManager.ts
4619
4744
  var import_base15 = require("@scure/base");
4620
- var import_sdk21 = require("@arkade-os/sdk");
4745
+ var import_sdk22 = require("@arkade-os/sdk");
4621
4746
  init_onchainHtlc();
4622
4747
  init_lockupContract();
4748
+ init_coverage();
4623
4749
  function nextOnchainAction(input) {
4624
4750
  switch (input.phase.phase) {
4625
4751
  case "unfunded":
@@ -5116,7 +5242,7 @@ var RfqSwapManager = class {
5116
5242
  });
5117
5243
  return;
5118
5244
  }
5119
- if (!(0, import_sdk21.isContractVtxoEvent)(event)) return;
5245
+ if (!(0, import_sdk22.isContractVtxoEvent)(event)) return;
5120
5246
  const swap = this.byLockupScript.get(event.contractScript);
5121
5247
  if (!swap) return;
5122
5248
  void this.pollSwap(swap).catch(() => {
@@ -5201,6 +5327,40 @@ var RfqSwapManager = class {
5201
5327
  void this.poll().then(() => this.arm());
5202
5328
  }, this.config.pollIntervalMs);
5203
5329
  }
5330
+ /*
5331
+ * Runs one serialized monitoring pass for a single swap.
5332
+ *
5333
+ * Flow
5334
+ *
5335
+ * 1. Prevents concurrent work
5336
+ * - Returns immediately if the swap is already in inProgress.
5337
+ * - Returns if the swap is no longer in monitored.
5338
+ * - Adds the swap ID to inProgress.
5339
+ *
5340
+ * 2. Runs the state machine
5341
+ * - Calls runPass(swap).
5342
+ * - runPass checks the lockup fate, drives claims, and may initiate refunds.
5343
+ *
5344
+ * 3. Persists changes
5345
+ * - If the pass marked the swap dirty, calls save(swap).
5346
+ * - A swap is considered persisted only if all configured sinks succeed:
5347
+ * - the repository, and
5348
+ * - callbacks.saveSwap, if provided.
5349
+ *
5350
+ * 4. Completes waiters and finalizes
5351
+ * - If persistence succeeds:
5352
+ * - clears the dirty flag,
5353
+ * - settles waitForSwapCompletion waiters,
5354
+ * - moves terminal swaps from monitored to finished,
5355
+ * - emits completion or failure notifications through finalize.
5356
+ *
5357
+ * 5. Releases the per-swap lock
5358
+ * - Deletes the swap ID from inProgress only after all asynchronous work completes.
5359
+ * - This prevents overlapping poll() calls from submitting duplicate claims or refunds.
5360
+ *
5361
+ * The important safety property is that terminal state is not finalized until its updated record has been
5362
+ * successfully persisted. A failed save leaves the swap dirty and monitored so a later poll can retry.
5363
+ */
5204
5364
  async pollSwap(swap) {
5205
5365
  if (this.inProgress.has(swap.rfqId)) return;
5206
5366
  if (!this.monitored.has(swap.rfqId)) return;
@@ -5217,6 +5377,42 @@ var RfqSwapManager = class {
5217
5377
  this.inProgress.delete(swap.rfqId);
5218
5378
  }
5219
5379
  }
5380
+ /*
5381
+ * executes one monitoring/state-machine pass for a swap.
5382
+ *
5383
+ * Flow
5384
+ * 1. Registers the lockup
5385
+ * - Calls ensureRegistered(swap).
5386
+ * - Registration is best effort and does not prevent the rest of the pass from running.
5387
+ *
5388
+ * 2. Reads the Arkade lockup fate
5389
+ * - Calls readLockupFate.
5390
+ * - A hash-verified claim is treated as claimed.
5391
+ * - A fully observed non-claim spend is treated as returned.
5392
+ * - Indexer errors become unknown, allowing deadline-driven logic to continue.
5393
+ * - For claimed or returned, it:
5394
+ * - records spend transaction IDs,
5395
+ * - records the settlement preimage when applicable,
5396
+ * - sets the swap state to settled or refunded,
5397
+ * - ends the pass.
5398
+ *
5399
+ * 3. Handles unilateral exits
5400
+ * - On receive swaps, an exited lockup is blocked immediately.
5401
+ * - On send swaps, the L1 half continues to be monitored because an exit only describes the Arkade side.
5402
+ *
5403
+ * 4. Drives the trader’s claim
5404
+ * - For lightning_receive, calls driveReceiveClaim and ends the pass.
5405
+ * - For onchain_send, calls driveOnchain to classify and potentially claim the L1 HTLC.
5406
+ * - A handled L1 action ends the pass; otherwise processing continues.
5407
+ *
5408
+ * 5. Drives an Arkade refund
5409
+ * - For send swaps whose lockup remains unresolved, calls driveArkadeRefund.
5410
+ * - This is gated by the refund timelock and local refund capability.
5411
+ * - Receive swaps never reach this step because their refund path belongs to the solver.
5412
+ *
5413
+ * runPass only decides and performs state-machine actions. Persistence, waiter settlement, terminal finalization,
5414
+ * and the per-swap lock are handled by pollSwap.
5415
+ */
5220
5416
  async runPass(swap) {
5221
5417
  await this.ensureRegistered(swap);
5222
5418
  let fate;
@@ -5235,7 +5431,10 @@ var RfqSwapManager = class {
5235
5431
  return;
5236
5432
  }
5237
5433
  if (swap.kind === "lightning_receive") {
5238
- return fate.fate === "exited" ? this.blockExitedLockup(swap, fate) : this.driveReceiveClaim(swap);
5434
+ if (fate.fate === "exited") return this.blockExitedLockup(swap, fate);
5435
+ if (fate.fate === "open") return this.driveReceiveClaim(swap);
5436
+ if (this.config.now() >= swap.refundLocktime) return this.driveReceiveClaim(swap);
5437
+ return;
5239
5438
  }
5240
5439
  if (swap.kind === "onchain_send" && swap.state !== "claimed") {
5241
5440
  if (await this.driveOnchain(swap) === "handled") return;
@@ -5290,7 +5489,7 @@ var RfqSwapManager = class {
5290
5489
  if (now < swap.refundLocktime) {
5291
5490
  let vtxos;
5292
5491
  try {
5293
- vtxos = await findLockupVtxos(this.deps.indexer, swap.lockupPkScript);
5492
+ vtxos = await findLockupVtxos(this.deps.contracts, swap.lockupPkScript);
5294
5493
  } catch (error) {
5295
5494
  this.emitFailed(swap, error);
5296
5495
  return;
@@ -5393,6 +5592,21 @@ var RfqSwapManager = class {
5393
5592
  });
5394
5593
  if (action === "claim" && phase.phase === "claimable") {
5395
5594
  if (swap.claimTxid) return "continue";
5595
+ if (!Number.isSafeInteger(swap.expectedAmount) || swap.expectedAmount <= 0) {
5596
+ this.block(
5597
+ swap,
5598
+ `expectedAmount is not a positive number of sats (${String(swap.expectedAmount)}), so the funded value cannot be checked \u2014 refusing to publish the preimage`
5599
+ );
5600
+ return "handled";
5601
+ }
5602
+ const filled = Number(phase.utxo.amount);
5603
+ if (!Number.isFinite(filled) || filled < swap.expectedAmount) {
5604
+ this.block(
5605
+ swap,
5606
+ `fill holds ${filled} sats, below the agreed ${swap.expectedAmount} \u2014 refusing to publish the preimage`
5607
+ );
5608
+ return "handled";
5609
+ }
5396
5610
  if (this.callbacks && !this.callbacks.claimOnchain) {
5397
5611
  this.block(swap, RFQ_CONFIGURATION_REFUSAL.noClaimOnchainCallback);
5398
5612
  return "handled";
@@ -5443,6 +5657,8 @@ var RfqSwapManager = class {
5443
5657
  if (pushed) {
5444
5658
  swap.refundTxid = pushed.txid;
5445
5659
  this.touch(swap);
5660
+ } else if (now < swap.refundLocktime + REFUND_MTP_LAG_SECONDS) {
5661
+ return;
5446
5662
  }
5447
5663
  this.setState(swap, "refunded");
5448
5664
  this.emitAction(swap, "refundArkade");
@@ -5677,7 +5893,10 @@ var outpointKey = (vtxo) => `${vtxo.txid}:${vtxo.vout}`;
5677
5893
 
5678
5894
  // src/watch.ts
5679
5895
  var import_base16 = require("@scure/base");
5680
- var import_sdk22 = require("@arkade-os/sdk");
5896
+ var import_sdk23 = require("@arkade-os/sdk");
5897
+ init_coverage();
5898
+ init_offer();
5899
+ init_store();
5681
5900
  var TERMINAL = ["fulfilled", "cancelled", "recoverable"];
5682
5901
  var assetSwapSource = (repository) => ({
5683
5902
  list: () => getAssetSwaps(repository),
@@ -5709,7 +5928,7 @@ async function watchOfferSwaps({
5709
5928
  wallet.getAddress(),
5710
5929
  wallet.getArkadeReader()
5711
5930
  ]);
5712
- const operatorPubkey = import_sdk22.ArkAddress.decode(address).serverPubKey;
5931
+ const operatorPubkey = import_sdk23.ArkAddress.decode(address).serverPubKey;
5713
5932
  let queue = Promise.resolve();
5714
5933
  const enqueue = (task) => {
5715
5934
  queue = queue.then(task).catch(() => {
@@ -5724,7 +5943,7 @@ async function watchOfferSwaps({
5724
5943
  return classifyDepositSpend(
5725
5944
  decodeOffer(import_base16.hex.decode(swap.offerHex)),
5726
5945
  operatorPubkey,
5727
- txs.map((psbt) => import_sdk22.Transaction.fromPSBT(import_base16.base64.decode(psbt))),
5946
+ txs.map((psbt) => import_sdk23.Transaction.fromPSBT(import_base16.base64.decode(psbt))),
5728
5947
  { txid: vtxo.txid, vout: vtxo.vout }
5729
5948
  );
5730
5949
  } catch {
@@ -5752,7 +5971,7 @@ async function watchOfferSwaps({
5752
5971
  }
5753
5972
  };
5754
5973
  const unsubscribe = manager.onContractEvent((event) => {
5755
- if (!(0, import_sdk22.isContractVtxoEvent)(event) || event.type !== "vtxo_spent") return;
5974
+ if (!(0, import_sdk23.isContractVtxoEvent)(event) || event.type !== "vtxo_spent") return;
5756
5975
  enqueue(() => handleSpend(event));
5757
5976
  });
5758
5977
  return {
@@ -5935,7 +6154,7 @@ var createSwapDrive = (config) => {
5935
6154
  const secret = rfqClaimSecretOf(record);
5936
6155
  if (!secret) throw new Error(`rfq swap ${swap.rfqId} carries no claim secret`);
5937
6156
  const preimage = await preimageForSwapRecord(wallet, secret);
5938
- const signer = await (0, import_sdk23.contractSigner)(wallet, await (0, import_sdk23.identityDescriptor)(wallet.identity));
6157
+ const signer = await (0, import_sdk24.contractSigner)(wallet, await (0, import_sdk24.identityDescriptor)(wallet.identity));
5939
6158
  const claimKey = record.profile.claimKey;
5940
6159
  if (typeof claimKey === "string") {
5941
6160
  const held = import_base17.hex.encode(await signer.xOnlyPublicKey());
@@ -5993,10 +6212,10 @@ var createSwapDrive = (config) => {
5993
6212
  return track(
5994
6213
  pushClaim(operator, {
5995
6214
  contract: script,
5996
- receiver: await (0, import_sdk23.contractSigner)(wallet, secret.signingDescriptor),
6215
+ receiver: await (0, import_sdk24.contractSigner)(wallet, secret.signingDescriptor),
5997
6216
  preimage: await preimageForSwapRecord(wallet, secret),
5998
6217
  vtxos,
5999
- destinationPkScript: import_sdk23.ArkAddress.decode(payoutAddress).pkScript,
6218
+ destinationPkScript: import_sdk24.ArkAddress.decode(payoutAddress).pkScript,
6000
6219
  // Passed straight through, both of them: the manager's own value
6001
6220
  // check decides WHEN to act, `pushClaim`'s decides whether `P` is
6002
6221
  // published, and it is the one with nothing between it and the
@@ -6029,7 +6248,7 @@ var createSwapDrive = (config) => {
6029
6248
  if (mode === "readonly") return;
6030
6249
  const push = arkadeRefunder({
6031
6250
  operator,
6032
- indexer,
6251
+ contracts: lockupSource,
6033
6252
  wallet,
6034
6253
  repository: corridorStore()
6035
6254
  });
@@ -6055,16 +6274,13 @@ var createSwapDrive = (config) => {
6055
6274
  };
6056
6275
  let offerSource;
6057
6276
  const offers = () => offerSource ??= offerRecordSource(storage(), remember, now);
6058
- const sweepOfferDeposits = async (offer) => {
6059
- const funded = offer.filter(
6060
- (record) => record.fundingTxid !== void 0 && !RETIRABLE.includes(record.status)
6061
- );
6062
- if (funded.length === 0) return;
6277
+ const restoreOfferDeposits = async (reopen) => {
6063
6278
  const store = storage();
6064
- const [history, scanned, address] = await Promise.all([
6279
+ const [history, scanned, address, network] = await Promise.all([
6065
6280
  wallet.getTransactionHistory(),
6066
6281
  store.getScannedTxids(),
6067
- wallet.getAddress()
6282
+ wallet.getAddress(),
6283
+ config.network()
6068
6284
  ]);
6069
6285
  const txs = history.map((tx) => ({
6070
6286
  // `TxType` is `"SENT"`/`"RECEIVED"`; the scan filters on `"sent"`.
@@ -6075,30 +6291,61 @@ var createSwapDrive = (config) => {
6075
6291
  // The scan reads unix SECONDS; a wallet transaction carries ms.
6076
6292
  ...tx.createdAt ? { createdAt: Math.floor(tx.createdAt / 1e3) } : {}
6077
6293
  }));
6294
+ const { hrp, serverPubKey: operatorPubkey } = import_sdk24.ArkAddress.decode(address);
6295
+ const cursor = reopen === void 0 ? scanned : new Set([...scanned].filter((txid) => txid !== reopen));
6078
6296
  const { restored, scannedTxids } = await restoreAssetSwaps(indexer, txs, /* @__PURE__ */ new Set(), {
6079
- operatorPubkey: import_sdk23.ArkAddress.decode(address).serverPubKey,
6080
- scanned
6297
+ operatorPubkey,
6298
+ scanned: cursor,
6299
+ hrp
6081
6300
  });
6082
- const byDeposit = new Map(restored.map((s) => [`${s.swapPkScript}:${s.fundingTxid}`, s]));
6083
- const stillLive = /* @__PURE__ */ new Set();
6084
- for (const record of funded) {
6085
- const found = byDeposit.get(`${record.swapPkScript}:${record.fundingTxid}`);
6086
- const status = found?.status ?? record.status;
6087
- if (status !== record.status) {
6088
- const updated = withOfferStatus(record, status, now());
6089
- await store.saveSwapRecord(updated);
6090
- remember(updated);
6301
+ const { offer } = splitRecords(await store.getAllSwapRecords());
6302
+ const current = new Map(offer.map((record) => [record.id, record]));
6303
+ const byDeposit = new Map(
6304
+ offer.filter((record) => record.fundingTxid !== void 0).map((record) => [`${record.swapPkScript}:${record.fundingTxid}`, record])
6305
+ );
6306
+ const unstamped = /* @__PURE__ */ new Map();
6307
+ for (const record of offer) {
6308
+ if (record.fundingTxid !== void 0 || !OFFER_LIVE(record.status)) continue;
6309
+ unstamped.set(record.swapPkScript, [
6310
+ ...unstamped.get(record.swapPkScript) ?? [],
6311
+ record
6312
+ ]);
6313
+ }
6314
+ const write = async (record) => {
6315
+ await store.saveSwapRecord(record);
6316
+ current.set(record.id, record);
6317
+ remember(record);
6318
+ };
6319
+ for (const found of restored) {
6320
+ const known = byDeposit.get(`${found.swapPkScript}:${found.fundingTxid}`);
6321
+ if (known !== void 0) {
6322
+ if (fateMoved(known, found)) await write(withDepositFate(known, found, now()));
6323
+ continue;
6324
+ }
6325
+ const orphan = unstamped.get(found.swapPkScript)?.shift();
6326
+ if (orphan !== void 0) {
6327
+ await write(
6328
+ withDepositFate({ ...orphan, fundingTxid: found.fundingTxid }, found, now())
6329
+ );
6330
+ continue;
6091
6331
  }
6092
- if (OFFER_LIVE(status) && record.fundingTxid) stillLive.add(record.fundingTxid);
6332
+ await write(restoredOfferRecord(found, network, now()));
6333
+ }
6334
+ const stillLive = /* @__PURE__ */ new Set();
6335
+ for (const record of current.values()) {
6336
+ if (OFFER_LIVE(record.status) && record.fundingTxid) stillLive.add(record.fundingTxid);
6093
6337
  }
6094
6338
  await store.markTxidsScanned(scannedTxids.filter((txid) => !stillLive.has(txid)));
6095
6339
  const registry = managerDeps.contracts;
6096
6340
  if (registry) {
6097
- const { offer: current } = splitRecords(await store.getAllSwapRecords());
6098
- await retireSettledOfferContracts(registry, current.map(offerFactsOf));
6341
+ await retireSettledOfferContracts(registry, [...current.values()].map(offerFactsOf));
6099
6342
  }
6343
+ return [...current.values()];
6100
6344
  };
6101
6345
  const contractsOf = async () => managerDeps.contracts ??= await wallet.getContractManager();
6346
+ const lockupSource = {
6347
+ getContractsWithVtxos: async (filter) => (await contractsOf()).getContractsWithVtxos(filter)
6348
+ };
6102
6349
  const registerCorridorSwap = async (record) => {
6103
6350
  if (mode === "readonly") return;
6104
6351
  if (!await drivable(record)) {
@@ -6169,13 +6416,14 @@ var createSwapDrive = (config) => {
6169
6416
  console.warn("[swap] the corridor restore could not drive", error);
6170
6417
  }
6171
6418
  }
6419
+ let offers2 = offer;
6172
6420
  try {
6173
- await sweepOfferDeposits(offer);
6421
+ offers2 = await restoreOfferDeposits();
6174
6422
  } catch (error) {
6175
- console.warn("[swap] the offer deposit sweep did not complete", error);
6423
+ console.warn("[swap] the offer deposit restore did not complete", error);
6176
6424
  }
6177
6425
  emitAll();
6178
- if (mode === "auto" && await hasLiveWork(offer)) {
6426
+ if (mode === "auto" && await hasLiveWork(offers2)) {
6179
6427
  try {
6180
6428
  await arm();
6181
6429
  } catch (error) {
@@ -6203,7 +6451,7 @@ var createSwapDrive = (config) => {
6203
6451
  }
6204
6452
  return probe.call(wallet);
6205
6453
  };
6206
- const recoverableAt = async (script) => (await findLockupVtxos(indexer, script)).filter((vtxo) => vtxo.recoverable);
6454
+ const recoverableAt = async (script) => (await findLockupVtxos(lockupSource, script)).filter((vtxo) => vtxo.recoverable);
6207
6455
  const recover = async (id) => {
6208
6456
  if (disposed) throw new ClientDisposed("recover");
6209
6457
  if (mode === "readonly") {
@@ -6253,7 +6501,7 @@ var createSwapDrive = (config) => {
6253
6501
  );
6254
6502
  }
6255
6503
  const txid = await track((await recoverer()).recoverVtxos());
6256
- await sweepOfferDeposits([records.get(id)]);
6504
+ await restoreOfferDeposits(record.fundingTxid);
6257
6505
  const after = records.get(id);
6258
6506
  const recovered = after?.family === "offer" && after.status !== "recoverable";
6259
6507
  return { recovered, txid, swap: swapView(id) };
@@ -6328,22 +6576,22 @@ var createSwapDrive = (config) => {
6328
6576
  };
6329
6577
 
6330
6578
  // src/client/quoteOffer.ts
6331
- var import_solver_discovery7 = require("@arkade-os/solver-discovery");
6579
+ var import_solver_discovery8 = require("@arkade-os/solver-discovery");
6332
6580
 
6333
6581
  // src/client/market.ts
6334
- var import_solver_discovery6 = require("@arkade-os/solver-discovery");
6582
+ var import_solver_discovery7 = require("@arkade-os/solver-discovery");
6335
6583
  var marketKeyOf = (card) => {
6336
- const base = (0, import_solver_discovery6.marketLegKey)(card, "base");
6337
- const quote = (0, import_solver_discovery6.marketLegKey)(card, "quote");
6338
- const baseIsArkade = (0, import_solver_discovery6.marketCorridor)(card, "base") === "arkade";
6339
- const quoteIsArkade = (0, import_solver_discovery6.marketCorridor)(card, "quote") === "arkade";
6584
+ const base = (0, import_solver_discovery7.marketLegKey)(card, "base");
6585
+ const quote = (0, import_solver_discovery7.marketLegKey)(card, "quote");
6586
+ const baseIsArkade = (0, import_solver_discovery7.marketCorridor)(card, "base") === "arkade";
6587
+ const quoteIsArkade = (0, import_solver_discovery7.marketCorridor)(card, "quote") === "arkade";
6340
6588
  if (baseIsArkade !== quoteIsArkade) {
6341
6589
  return baseIsArkade ? `${base}/${quote}` : `${quote}/${base}`;
6342
6590
  }
6343
6591
  return base <= quote ? `${base}/${quote}` : `${quote}/${base}`;
6344
6592
  };
6345
- var isAddressable = (card) => !(0, import_solver_discovery6.isRfqMarket)(card) || typeof card.discovery_pubkey === "string" && (card.transports?.nostr?.relays?.length ?? 0) > 0;
6346
- var marketBackendOf = (card) => (0, import_solver_discovery6.isRfqMarket)(card) ? "rfq" : "feed";
6593
+ var isAddressable = (card) => !(0, import_solver_discovery7.isRfqMarket)(card) || typeof card.discovery_pubkey === "string" && (card.transports?.nostr?.relays?.length ?? 0) > 0;
6594
+ var marketBackendOf = (card) => (0, import_solver_discovery7.isRfqMarket)(card) ? "rfq" : "feed";
6347
6595
  var usableMarkets = (snapshot, policy) => {
6348
6596
  const allowed = policy?.allowedRegistries;
6349
6597
  return (allowed ? snapshot.markets.filter((card) => allowed.includes(card.source)) : snapshot.markets).filter(isAddressable);
@@ -6361,12 +6609,12 @@ var eligibleMarkets = (snapshot, legs, policy, takeAmount) => {
6361
6609
  wantSide: side === "base" ? "quote" : "base",
6362
6610
  ...takeAmount === void 0 ? {} : { wantAmount: takeAmount }
6363
6611
  };
6364
- const canonical = (0, import_solver_discovery6.selectMarkets)([...markets], {
6612
+ const canonical = (0, import_solver_discovery7.selectMarkets)([...markets], {
6365
6613
  baseId: base.marketId,
6366
6614
  quoteId: quote.marketId,
6367
6615
  ...selection
6368
6616
  });
6369
- const legacy = (0, import_solver_discovery6.selectMarkets)([...markets], {
6617
+ const legacy = (0, import_solver_discovery7.selectMarkets)([...markets], {
6370
6618
  baseId: `${base.corridor}:${base.assetId}`,
6371
6619
  quoteId: `${quote.corridor}:${quote.assetId}`,
6372
6620
  ...selection
@@ -6426,16 +6674,16 @@ var assembleRoute = (give, take) => {
6426
6674
  }
6427
6675
  return { give, take };
6428
6676
  };
6429
- var endpoint = (corridor, asset5, instrument) => ({
6677
+ var endpoint = (corridor, asset6, instrument) => ({
6430
6678
  corridor,
6431
- asset: asset5,
6679
+ asset: asset6,
6432
6680
  ...instrument === void 0 ? {} : { instrument }
6433
6681
  });
6434
- var legFor = (asset5, expected) => {
6435
- const leg = toDiscoveryLeg(asset5);
6682
+ var legFor = (asset6, expected) => {
6683
+ const leg = toDiscoveryLeg(asset6);
6436
6684
  if (expected !== void 0 && leg.corridor !== expected) {
6437
6685
  throw new UnsupportedRoute(
6438
- `${asset5} settles on ${leg.corridor}, and this leg is ${expected}`,
6686
+ `${asset6} settles on ${leg.corridor}, and this leg is ${expected}`,
6439
6687
  { give: leg.corridor, take: expected }
6440
6688
  );
6441
6689
  }
@@ -6727,7 +6975,7 @@ var quoteFromFeed = async (input) => {
6727
6975
  if (amount === void 0) {
6728
6976
  throw new Error("an asset swap needs an amount and the side it pins");
6729
6977
  }
6730
- const plan = await (0, import_solver_discovery7.quoteOffer)(candidate.card, {
6978
+ const plan = await (0, import_solver_discovery8.quoteOffer)(candidate.card, {
6731
6979
  give: candidate.give,
6732
6980
  ...amount.on === "give" ? { giveAmount: amount.value } : { wantAmount: amount.value },
6733
6981
  ...QUOTE_OPTIONS,
@@ -6759,7 +7007,7 @@ var quoteFromFeed = async (input) => {
6759
7007
  };
6760
7008
  };
6761
7009
  var spreadOf = (plan) => {
6762
- const fair = (0, import_solver_discovery7.computeWantAmount)({
7010
+ const fair = (0, import_solver_discovery8.computeWantAmount)({
6763
7011
  deposit: plan.deposit.atomic,
6764
7012
  give: plan.give,
6765
7013
  price: plan.price,
@@ -6784,17 +7032,17 @@ var verifyPlanLegs = (plan, legs, give) => {
6784
7032
 
6785
7033
  // src/client/quoteRfq.ts
6786
7034
  var import_base18 = require("@scure/base");
6787
- var import_sdk26 = require("@arkade-os/sdk");
7035
+ var import_sdk27 = require("@arkade-os/sdk");
6788
7036
  init_claimPacket();
6789
7037
  init_onchainHtlc();
6790
7038
  init_onchainHtlc();
6791
7039
 
6792
7040
  // src/payment/onchainSwap.ts
6793
- var import_sdk25 = require("@arkade-os/sdk");
7041
+ var import_sdk26 = require("@arkade-os/sdk");
6794
7042
  init_onchainHtlc();
6795
7043
 
6796
7044
  // src/payment/swapRail.ts
6797
- var import_sdk24 = require("@arkade-os/sdk");
7045
+ var import_sdk25 = require("@arkade-os/sdk");
6798
7046
 
6799
7047
  // src/client/sats.ts
6800
7048
  var satsOf = (value, field) => {
@@ -6816,6 +7064,7 @@ init_rfq();
6816
7064
 
6817
7065
  // src/client/rfqWire.ts
6818
7066
  init_rfq();
7067
+ init_store();
6819
7068
  var BTC_LEG = {
6820
7069
  arkade: ARKADE_BTC,
6821
7070
  lightning: LIGHTNING_BTC,
@@ -6845,14 +7094,14 @@ var parseRfqQuote = (quote) => ({
6845
7094
 
6846
7095
  // src/client/quoteRfq.ts
6847
7096
  var covenantInputs = (input) => {
6848
- const network = (0, import_sdk26.networkFromArkadeInfo)(input.info);
7097
+ const network = (0, import_sdk27.networkFromArkadeInfo)(input.info);
6849
7098
  const arkade2 = input.corridors.get("arkade").deps;
6850
7099
  return {
6851
- operatorPubkey: (0, import_sdk26.toXOnly)(import_base18.hex.decode(input.info.signerPubkey), "ark signer key"),
7100
+ operatorPubkey: (0, import_sdk27.toXOnly)(import_base18.hex.decode(input.info.signerPubkey), "ark signer key"),
6852
7101
  // The per-network pin the arkade module already resolved, never a key
6853
7102
  // the operator reports about itself: this one ends up in a covenant leaf
6854
7103
  // that decides who can move the funds.
6855
- emulatorPubkey: (0, import_sdk26.toXOnly)(import_base18.hex.decode(arkade2.emulatorPubkey), "emulator signer key"),
7104
+ emulatorPubkey: (0, import_sdk27.toXOnly)(import_base18.hex.decode(arkade2.emulatorPubkey), "emulator signer key"),
6856
7105
  claimDelay: unilateralClaimDelay(Number(input.info.unilateralExitDelay)),
6857
7106
  hrp: network.hrp
6858
7107
  };
@@ -6896,7 +7145,7 @@ var quoteLightningSend = async (input) => {
6896
7145
  }
6897
7146
  const pair = rfqPairFor(input.legs.give, input.legs.take);
6898
7147
  const rfqId = newRfqId();
6899
- const secrets = await (0, import_sdk26.provisionRefundKey)(input.wallet);
7148
+ const secrets = await (0, import_sdk27.provisionRefundKey)(input.wallet);
6900
7149
  const wire = await input.transport.requestQuote(
6901
7150
  lightningSendRequest({
6902
7151
  rfqId,
@@ -6969,7 +7218,7 @@ var quoteLightningReceive = async (input) => {
6969
7218
  const pinned = pinnedFor(input);
6970
7219
  const pair = rfqPairFor(input.legs.give, input.legs.take);
6971
7220
  const rfqId = newRfqId();
6972
- const secrets = await (0, import_sdk26.provisionClaimSecret)(input.wallet);
7221
+ const secrets = await (0, import_sdk27.provisionClaimSecret)(input.wallet);
6973
7222
  const paymentHash = import_base18.hex.encode(secrets.paymentHash);
6974
7223
  const payoutAddress = await input.wallet.getAddress();
6975
7224
  const lightning = input.corridors.get("lightning").deps;
@@ -7092,8 +7341,8 @@ var quoteOnchainSend = async (input) => {
7092
7341
  claimVsize: onchain.claimVsize
7093
7342
  }) : void 0;
7094
7343
  const quoted = claimFee === void 0 ? pinned : { on: pinned.on, value: pinned.value + claimFee, source: pinned.source };
7095
- const secrets = await (0, import_sdk26.provisionClaimSecret)(input.wallet);
7096
- const payoutKey = await (0, import_sdk26.provisionRefundKey)(input.wallet);
7344
+ const secrets = await (0, import_sdk27.provisionClaimSecret)(input.wallet);
7345
+ const payoutKey = await (0, import_sdk27.provisionRefundKey)(input.wallet);
7097
7346
  const paymentHash = import_base18.hex.encode(secrets.paymentHash);
7098
7347
  const wire = await input.transport.requestQuote(
7099
7348
  withCanonicalAmount(
@@ -7189,6 +7438,10 @@ var quoteOnchainSend = async (input) => {
7189
7438
  secrets,
7190
7439
  refundAddress: payoutKey.address,
7191
7440
  fundAmount: parsed.give,
7441
+ // The gross wire leg, not `reportedTake`: the claim fee comes out of
7442
+ // the HTLC output, so netting it here would accept an HTLC funded
7443
+ // short by exactly that fee.
7444
+ expectedAmount: parsed.take,
7192
7445
  payoutKey,
7193
7446
  payoutPkScript,
7194
7447
  htlc: derived.htlc,
@@ -7200,7 +7453,7 @@ var quoteOnchainSend = async (input) => {
7200
7453
  };
7201
7454
 
7202
7455
  // src/client/verbs.ts
7203
- var import_sdk27 = require("@arkade-os/sdk");
7456
+ var import_sdk28 = require("@arkade-os/sdk");
7204
7457
  var enforceFeeCeiling = (quote, call, policy) => {
7205
7458
  let ceiling;
7206
7459
  for (const [source, limit] of [
@@ -7225,9 +7478,9 @@ var settle = async (deps, input, maxFee) => {
7225
7478
  return deps.accept(quote);
7226
7479
  };
7227
7480
  var pay = async (deps, destination, options = {}) => {
7228
- const arkade2 = (0, import_sdk27.arkTarget)(destination);
7481
+ const arkade2 = (0, import_sdk28.arkTarget)(destination);
7229
7482
  if (arkade2 !== void 0) {
7230
- const amount = (0, import_sdk27.resolveSendAmount)(
7483
+ const amount = (0, import_sdk28.resolveSendAmount)(
7231
7484
  "ark",
7232
7485
  destination,
7233
7486
  options.amount === void 0 ? void 0 : satsOf(options.amount, "amount")
@@ -7310,6 +7563,7 @@ var createSwapClient = (config) => {
7310
7563
  operator,
7311
7564
  ...config.repository === void 0 ? {} : { repository: config.repository },
7312
7565
  corridors: async () => (await resolved()).corridors,
7566
+ network: async () => (await resolved()).base.networkName,
7313
7567
  ...config.policy?.drive === void 0 ? {} : { mode: config.policy.drive },
7314
7568
  indexer
7315
7569
  });
@@ -7547,7 +7801,7 @@ var createSwapClient = (config) => {
7547
7801
  var import_base20 = require("@scure/base");
7548
7802
  var import_legacy4 = require("@noble/hashes/legacy.js");
7549
7803
  var import_sha26 = require("@noble/hashes/sha2.js");
7550
- var import_sdk28 = require("@arkade-os/sdk");
7804
+ var import_sdk29 = require("@arkade-os/sdk");
7551
7805
  init_claimPacket();
7552
7806
  var CovclaimdRevealError = class extends Error {
7553
7807
  name = "CovclaimdRevealError";
@@ -7652,7 +7906,7 @@ function revealFieldsFromScript(script) {
7652
7906
  }
7653
7907
  async function revealClaimPacket(client, input) {
7654
7908
  const fields = revealFieldsFromScript(input.script);
7655
- if (import_base20.hex.encode(import_sdk28.ArkAddress.decode(input.address).pkScript) !== import_base20.hex.encode(input.script.pkScript)) {
7909
+ if (import_base20.hex.encode(import_sdk29.ArkAddress.decode(input.address).pkScript) !== import_base20.hex.encode(input.script.pkScript)) {
7656
7910
  throw new Error("address does not belong to this covenant");
7657
7911
  }
7658
7912
  if (import_base20.hex.encode((0, import_legacy4.ripemd160)((0, import_sha26.sha256)(input.preimage))) !== import_base20.hex.encode(input.script.options.preimageHash)) {
@@ -7743,6 +7997,7 @@ async function revealClaimPacket(client, input) {
7743
7997
  esploraTip,
7744
7998
  exchange,
7745
7999
  familyOfSwapId,
8000
+ fateMoved,
7746
8001
  feedFetch,
7747
8002
  formatAssetId,
7748
8003
  fromAtomicDecimal,
@@ -7790,6 +8045,7 @@ async function revealClaimPacket(client, input) {
7790
8045
  resolveCorridorBase,
7791
8046
  resolveCorridorDeps,
7792
8047
  resolveRoute,
8048
+ restoredOfferRecord,
7793
8049
  revealClaimPacket,
7794
8050
  revealFieldsFromScript,
7795
8051
  rfqLeg,
@@ -7817,6 +8073,6 @@ async function revealClaimPacket(client, input) {
7817
8073
  verifyingDerivation,
7818
8074
  walletLockupIndexer,
7819
8075
  withCanonicalAmount,
7820
- withOfferStatus,
8076
+ withDepositFate,
7821
8077
  withRfqState
7822
8078
  });