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

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 (36) hide show
  1. package/dist/advanced.cjs +1844 -1670
  2. package/dist/advanced.d.cts +7 -7
  3. package/dist/advanced.d.ts +7 -7
  4. package/dist/advanced.js +3 -3
  5. package/dist/{chunk-WXV23FAY.js → chunk-7DKKTLPB.js} +170 -901
  6. package/dist/{chunk-DSRMQCKG.js → chunk-EYSUBQFB.js} +1087 -85
  7. package/dist/{chunk-4MTMMYYC.js → chunk-RSUVIWT5.js} +18 -13
  8. package/dist/{errors-B75peUQo.d.cts → errors-Bg1bbSUK.d.cts} +4 -2
  9. package/dist/{errors-D7YNbfwK.d.ts → errors-Do2Y18da.d.ts} +4 -2
  10. package/dist/index.cjs +2606 -2432
  11. package/dist/index.d.cts +6 -6
  12. package/dist/index.d.ts +6 -6
  13. package/dist/index.js +7 -7
  14. package/dist/{lockupContract-C7dM1xxn.d.ts → lockupContract-BL2D3js9.d.ts} +1 -1
  15. package/dist/{lockupContract-BwdgOqqe.d.cts → lockupContract-rWaIvZ11.d.cts} +1 -1
  16. package/dist/node/index.d.cts +3 -0
  17. package/dist/node/index.d.ts +3 -0
  18. package/dist/nostr.cjs +109 -3
  19. package/dist/nostr.d.cts +1 -1
  20. package/dist/nostr.d.ts +1 -1
  21. package/dist/nostr.js +1 -1
  22. package/dist/protocol.cjs +3626 -3365
  23. package/dist/protocol.d.cts +18 -15
  24. package/dist/protocol.d.ts +18 -15
  25. package/dist/protocol.js +32 -20
  26. package/dist/repositories/realm/index.d.cts +2 -2
  27. package/dist/repositories/realm/index.d.ts +2 -2
  28. package/dist/repositories/sqlite/index.d.cts +2 -2
  29. package/dist/repositories/sqlite/index.d.ts +2 -2
  30. package/dist/{repository-2bubkNM6.d.ts → repository-By-tSjDD.d.ts} +6 -24
  31. package/dist/{repository-Bi0iq1fK.d.cts → repository-DIZnasB7.d.cts} +6 -24
  32. package/dist/{rfq-o1ncPWXH.d.cts → rfq-CoGJB3_s.d.cts} +128 -12
  33. package/dist/{rfq-o1ncPWXH.d.ts → rfq-CoGJB3_s.d.ts} +128 -12
  34. package/dist/{watch-DG6v5aOM.d.ts → watch-Coy0_YHu.d.ts} +1 -1
  35. package/dist/{watch-CGRIFtBG.d.cts → watch-DQaFg3Hd.d.cts} +1 -1
  36. package/package.json +3 -3
package/dist/advanced.cjs CHANGED
@@ -35,1741 +35,1793 @@ 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
+ feedFetch: () => feedFetch,
1743
+ formatAssetId: () => formatAssetId,
1744
+ fromAtomicDecimal: () => fromAtomicDecimal,
1745
+ fromRfqAmountSide: () => fromRfqAmountSide,
1746
+ fundsFromWallet: () => fundsFromWallet,
1747
+ instrumentOf: () => instrumentOf,
1748
+ isAddressable: () => isAddressable,
1749
+ isAssetId: () => isAssetId,
1750
+ isAtomicDecimal: () => isAtomicDecimal,
1751
+ isCardMarket: () => isCardMarket,
1752
+ isIndexedNetwork: () => isIndexedNetwork,
1753
+ isNetworkRef: () => isNetworkRef,
1754
+ isSwapError: () => isSwapError,
1755
+ isUsableCard: () => isUsableCard,
1756
+ issuanceOf: () => issuanceOf,
1757
+ legOf: () => legOf,
1758
+ lightningCorridor: () => lightningCorridor,
1759
+ liveArkadeInfo: () => liveArkadeInfo,
1760
+ marketBackendOf: () => marketBackendOf,
1761
+ marketKeyOf: () => marketKeyOf,
1762
+ marketRefOf: () => marketRefOf,
1763
+ networksOfInvoiceHrp: () => networksOfInvoiceHrp,
1764
+ nostrTransportFactory: () => nostrTransportFactory,
1765
+ offerFactsOf: () => offerFactsOf,
1766
+ offerOutcome: () => offerOutcome,
1767
+ offerRecordSource: () => offerRecordSource,
1768
+ onchainCorridor: () => onchainCorridor,
1769
+ parseAssetId: () => parseAssetId,
1770
+ parseRfqQuote: () => parseRfqQuote,
1771
+ pay: () => pay,
1772
+ publicAssetId: () => publicAssetId,
1773
+ quoteFromFeed: () => quoteFromFeed,
1774
+ quoteIdOfSwapId: () => quoteIdOfSwapId,
1775
+ quoteViaRfq: () => quoteViaRfq,
1776
+ railOf: () => railOf,
1777
+ railOfCorridor: () => railOfCorridor,
1778
+ readableRecord: () => readableRecord,
1779
+ readsChain: () => readsChain,
1780
+ receive: () => receive,
1781
+ recordArtifact: () => recordArtifact,
1782
+ recordEndpoint: () => recordEndpoint,
1783
+ recordInstrument: () => recordInstrument,
1784
+ recordLeg: () => recordLeg,
1785
+ recordOutcome: () => recordOutcome,
1786
+ resolveCorridorBase: () => resolveCorridorBase,
1787
+ resolveCorridorDeps: () => resolveCorridorDeps,
1788
+ resolveRoute: () => resolveRoute,
1789
+ revealClaimPacket: () => revealClaimPacket,
1790
+ revealFieldsFromScript: () => revealFieldsFromScript,
1791
+ rfqLeg: () => rfqLeg,
1792
+ rfqPairFor: () => rfqPairFor,
1793
+ rfqRecordOf: () => rfqRecordOf,
1794
+ sameAsset: () => sameAsset,
1795
+ satsOf: () => satsOf,
1796
+ scopedToRail: () => scopedToRail,
1797
+ splitRecords: () => splitRecords,
1798
+ swapOf: () => swapOf,
1799
+ swapRecordOf: () => swapRecordOf,
1800
+ toAtomicDecimal: () => toAtomicDecimal,
1801
+ toDiscoveryLeg: () => toDiscoveryLeg,
1802
+ toRfqAmountSide: () => toRfqAmountSide,
1803
+ toSafeNumber: () => toSafeNumber,
1804
+ usableMarkets: () => usableMarkets,
1805
+ verifyPair: () => verifyPair,
1806
+ verifyQuoteTtl: () => verifyQuoteTtl,
1807
+ verifyQuotedAmount: () => verifyQuotedAmount,
1808
+ verifyReceiveInvoiceFacts: () => verifyReceiveInvoiceFacts,
1809
+ verifyReceiveWindow: () => verifyReceiveWindow,
1810
+ verifyResponder: () => verifyResponder,
1811
+ verifySendInvoice: () => verifySendInvoice,
1812
+ verifySendWindow: () => verifySendWindow,
1813
+ verifyingDerivation: () => verifyingDerivation,
1814
+ walletLockupIndexer: () => walletLockupIndexer,
1815
+ withCanonicalAmount: () => withCanonicalAmount,
1816
+ withOfferStatus: () => withOfferStatus,
1817
+ withRfqState: () => withRfqState
1818
+ });
1819
+ module.exports = __toCommonJS(advanced_exports);
1771
1820
 
1772
1821
  // src/client/accept.ts
1822
+ var import_base8 = require("@scure/base");
1823
+ var import_sdk9 = require("@arkade-os/sdk");
1824
+ init_offer();
1773
1825
  init_lockupContract();
1774
1826
 
1775
1827
  // src/rfqCorridors.ts
@@ -1823,6 +1875,7 @@ var RfqCorridorRegistry = class {
1823
1875
  var rfqCorridorHandlers = new RfqCorridorRegistry();
1824
1876
 
1825
1877
  // src/rfqProfileParts.ts
1878
+ init_store();
1826
1879
  var rfqSecretsProfile = (secrets, paymentHash) => {
1827
1880
  const { signingDescriptor, ...preimage } = swapSecretsToRecord(secrets);
1828
1881
  return {
@@ -1952,6 +2005,7 @@ function onchainSendProfile(result) {
1952
2005
  network: result.l1Network,
1953
2006
  htlcAddress: result.htlc.address,
1954
2007
  minConfirmations: result.minConfirmations,
2008
+ expectedAmount: result.expectedAmount,
1955
2009
  payoutPkScript: import_base5.hex.encode(result.payoutPkScript)
1956
2010
  };
1957
2011
  }
@@ -1963,6 +2017,7 @@ var OnchainSendCorridor = {
1963
2017
  project: (swap) => {
1964
2018
  const send = swap;
1965
2019
  return {
2020
+ expectedAmount: send.expectedAmount,
1966
2021
  ...send.funding ? { funding: send.funding } : {},
1967
2022
  ...send.claimTxid ? { claimTxid: send.claimTxid } : {}
1968
2023
  };
@@ -1979,6 +2034,11 @@ var OnchainSendCorridor = {
1979
2034
  `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
2035
  );
1981
2036
  }
2037
+ if (!Number.isSafeInteger(profile.expectedAmount) || profile.expectedAmount <= 0) {
2038
+ throw new Error(
2039
+ `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`
2040
+ );
2041
+ }
1982
2042
  const htlc = onchainHtlcScript(
1983
2043
  {
1984
2044
  // From the profile, not the covenant: the lockup commits to
@@ -2001,6 +2061,7 @@ var OnchainSendCorridor = {
2001
2061
  paymentHash,
2002
2062
  htlc,
2003
2063
  minConfirmations: profile.minConfirmations,
2064
+ expectedAmount: profile.expectedAmount,
2004
2065
  // Optional here, required at the write: throwing on an older
2005
2066
  // record would strand the refund it is still owed.
2006
2067
  ...profile.payoutPkScript ? { payoutPkScript: import_base5.hex.decode(profile.payoutPkScript) } : {},
@@ -2020,6 +2081,9 @@ rfqCorridorHandlers.register(LightningSendCorridor);
2020
2081
  rfqCorridorHandlers.register(LightningReceiveCorridor);
2021
2082
  rfqCorridorHandlers.register(OnchainSendCorridor);
2022
2083
 
2084
+ // src/client/accept.ts
2085
+ init_store();
2086
+
2023
2087
  // src/client/assetId.ts
2024
2088
  var import_sdk4 = require("@arkade-os/sdk");
2025
2089
  var RAILS = ["arkade", "bitcoin", "bolt11", "eip155"];
@@ -2168,7 +2232,8 @@ var issuanceOf = (id) => {
2168
2232
  };
2169
2233
 
2170
2234
  // src/client/aliases.ts
2171
- var import_solver_discovery = require("@arkade-os/solver-discovery");
2235
+ var import_solver_discovery2 = require("@arkade-os/solver-discovery");
2236
+ init_store();
2172
2237
 
2173
2238
  // src/client/errors.ts
2174
2239
  init_rfq();
@@ -2373,8 +2438,8 @@ var toDiscoveryLeg = (id) => {
2373
2438
  }
2374
2439
  throw new UnsupportedRoute(`no corridor serves ${id}`);
2375
2440
  };
2376
- var INDEXED_NETWORKS = import_solver_discovery.NETWORKS;
2377
- var isIndexedNetwork = (network) => (0, import_solver_discovery.isNetwork)(network);
2441
+ var INDEXED_NETWORKS = import_solver_discovery2.NETWORKS;
2442
+ var isIndexedNetwork = (network) => (0, import_solver_discovery2.isNetwork)(network);
2378
2443
  var networkOfRow = (row) => parseAssetId(row.id).reference;
2379
2444
  var canonicalAssetId = (input, table) => {
2380
2445
  if (isAssetId(input)) return input;
@@ -2606,7 +2671,7 @@ var ACTIVITY_TOKEN = {
2606
2671
  };
2607
2672
 
2608
2673
  // src/client/amount.ts
2609
- var import_solver_discovery2 = require("@arkade-os/solver-discovery");
2674
+ var import_solver_discovery3 = require("@arkade-os/solver-discovery");
2610
2675
  var AmountFormatError = class extends Error {
2611
2676
  constructor(reason, message) {
2612
2677
  super(message);
@@ -2619,10 +2684,10 @@ var SCALED_DECIMAL = /^([0-9]+)(?:\.([0-9]+))?$/;
2619
2684
  var ATOMIC_CEILING = 10n ** 30n;
2620
2685
  var MAX_SIGNIFICANT_DIGITS = 64;
2621
2686
  var assertDecimals = (decimals) => {
2622
- if (!Number.isInteger(decimals) || decimals < 0 || decimals > import_solver_discovery2.MAX_ASSET_DECIMALS) {
2687
+ if (!Number.isInteger(decimals) || decimals < 0 || decimals > import_solver_discovery3.MAX_ASSET_DECIMALS) {
2623
2688
  throw new AmountFormatError(
2624
2689
  "invalid_decimals",
2625
- `decimals must be an integer in [0, ${import_solver_discovery2.MAX_ASSET_DECIMALS}], got ${decimals}`
2690
+ `decimals must be an integer in [0, ${import_solver_discovery3.MAX_ASSET_DECIMALS}], got ${decimals}`
2626
2691
  );
2627
2692
  }
2628
2693
  };
@@ -2669,7 +2734,7 @@ var Amount = {
2669
2734
  if (whole.length + fraction.length > MAX_SIGNIFICANT_DIGITS) {
2670
2735
  throw new AmountFormatError("too_large", `${display} has too many digits`);
2671
2736
  }
2672
- const value = (0, import_solver_discovery2.toAtomic)(display, asset5.decimals);
2737
+ const value = (0, import_solver_discovery3.toAtomic)(display, asset5.decimals);
2673
2738
  assertRepresentable(value, `${display} at ${asset5.decimals} decimals`);
2674
2739
  return value;
2675
2740
  },
@@ -2685,7 +2750,7 @@ var Amount = {
2685
2750
  throw new AmountFormatError("negative", `amount must be non-negative, got ${value}`);
2686
2751
  }
2687
2752
  assertRepresentable(value, `${value}`);
2688
- return (0, import_solver_discovery2.fromAtomic)(value, asset5.decimals);
2753
+ return (0, import_solver_discovery3.fromAtomic)(value, asset5.decimals);
2689
2754
  }
2690
2755
  };
2691
2756
  var toAtomicDecimal = (value) => {
@@ -2696,7 +2761,7 @@ var toAtomicDecimal = (value) => {
2696
2761
  return value.toString();
2697
2762
  };
2698
2763
  var fromAtomicDecimal = (text) => {
2699
- if (!(0, import_solver_discovery2.isAmount)(text)) {
2764
+ if (!(0, import_solver_discovery3.isAmount)(text)) {
2700
2765
  throw new AmountFormatError(
2701
2766
  "not_canonical",
2702
2767
  `not a canonical atomic amount: ${JSON.stringify(text)}`
@@ -2704,10 +2769,10 @@ var fromAtomicDecimal = (text) => {
2704
2769
  }
2705
2770
  return BigInt(text);
2706
2771
  };
2707
- var isAtomicDecimal = (value) => typeof value === "string" && import_solver_discovery2.AMOUNT_PATTERN.test(value);
2772
+ var isAtomicDecimal = (value) => typeof value === "string" && import_solver_discovery3.AMOUNT_PATTERN.test(value);
2708
2773
 
2709
2774
  // src/client/rfqAmount.ts
2710
- var import_solver_discovery3 = require("@arkade-os/solver-discovery");
2775
+ var import_solver_discovery4 = require("@arkade-os/solver-discovery");
2711
2776
  var toRfqAmountSide = (on) => on === "give" ? "from" : "to";
2712
2777
  var fromRfqAmountSide = (side) => side === "from" ? "give" : "take";
2713
2778
  var encodeRfqAmount = (value, field) => {
@@ -2724,7 +2789,7 @@ var encodeRfqAmount = (value, field) => {
2724
2789
  };
2725
2790
  var decodeRfqAmount = (raw, field) => {
2726
2791
  if (typeof raw === "string") {
2727
- if (!(0, import_solver_discovery3.isAmount)(raw)) {
2792
+ if (!(0, import_solver_discovery4.isAmount)(raw)) {
2728
2793
  throw new AmountEncodingUnsupported(
2729
2794
  field,
2730
2795
  raw,
@@ -3010,6 +3075,7 @@ var profileOf = (preparation, paymentHash) => {
3010
3075
  htlcParams: preparation.htlcParams,
3011
3076
  l1Network: preparation.l1Network,
3012
3077
  minConfirmations: preparation.minConfirmations,
3078
+ expectedAmount: toSafeNumber(preparation.expectedAmount, "expectedAmount"),
3013
3079
  payoutPkScript: preparation.payoutPkScript
3014
3080
  })
3015
3081
  };
@@ -3136,6 +3202,9 @@ var depositMatches = (vtxo, record, expected) => {
3136
3202
  };
3137
3203
  var swapRecordOf = async (repository, id) => storageOf(repository).getSwapRecord(id);
3138
3204
 
3205
+ // src/client/aliasTable.ts
3206
+ init_store();
3207
+
3139
3208
  // src/marketShape.ts
3140
3209
  var MARKET_CORRIDORS = ["arkade", "bolt11", "bitcoin", "eip155"];
3141
3210
  var LEGACY_CORRIDORS = {
@@ -3684,11 +3753,12 @@ var corridorSet = (base, overrides) => {
3684
3753
  };
3685
3754
 
3686
3755
  // src/client/discovery.ts
3687
- var import_solver_discovery5 = require("@arkade-os/solver-discovery");
3756
+ var import_solver_discovery6 = require("@arkade-os/solver-discovery");
3688
3757
 
3689
3758
  // src/markets.ts
3690
- var import_solver_discovery4 = require("@arkade-os/solver-discovery");
3759
+ var import_solver_discovery5 = require("@arkade-os/solver-discovery");
3691
3760
  var import_sdk15 = require("@arkade-os/sdk");
3761
+ init_store();
3692
3762
  var QUOTE_OPTIONS = { safetyBps: 0 };
3693
3763
  var makeCachedFeedFetch = (ttlMs = 3e4, fetchImpl = fetch) => {
3694
3764
  const cache = /* @__PURE__ */ new Map();
@@ -3745,7 +3815,7 @@ var REGISTRY_URL = {
3745
3815
  };
3746
3816
  var HEX_64 = /^[0-9a-f]{64}$/;
3747
3817
  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://"));
3818
+ 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
3819
  var isCorridorField = (value) => value === void 0 || CORRIDORS.includes(value);
3750
3820
  var isUsableCard = (value) => {
3751
3821
  const card = value;
@@ -3757,7 +3827,7 @@ var isUsableCard = (value) => {
3757
3827
  const id = marketAssetId(card, side);
3758
3828
  if (id === void 0 || !isAssetId(id) && !LEGACY_ASSET_ID.test(id)) return false;
3759
3829
  }
3760
- if ((0, import_solver_discovery5.isRfqMarket)(card)) {
3830
+ if ((0, import_solver_discovery6.isRfqMarket)(card)) {
3761
3831
  if (typeof card.discovery_pubkey !== "string" || !HEX_64.test(card.discovery_pubkey)) {
3762
3832
  return false;
3763
3833
  }
@@ -3833,7 +3903,7 @@ var discoveryIndex = (input) => {
3833
3903
  if (!indexed || !url) {
3834
3904
  return unavailable(whyUnavailable());
3835
3905
  }
3836
- const result = await (0, import_solver_discovery5.discover)({
3906
+ const result = await (0, import_solver_discovery6.discover)({
3837
3907
  registries: [url],
3838
3908
  localCards: [...config.localCards ?? []],
3839
3909
  network: indexed,
@@ -3870,10 +3940,14 @@ var discoveryIndex = (input) => {
3870
3940
  // src/client/cancel.ts
3871
3941
  var import_base12 = require("@scure/base");
3872
3942
  var import_sdk17 = require("@arkade-os/sdk");
3943
+ init_coverage();
3944
+ init_offer();
3873
3945
 
3874
3946
  // src/restore.ts
3875
3947
  var import_base11 = require("@scure/base");
3876
3948
  var import_sdk16 = require("@arkade-os/sdk");
3949
+ init_offer();
3950
+ init_store();
3877
3951
  var TXS_PER_REQUEST = 50;
3878
3952
  async function fetchParsedTxs(indexer, txids) {
3879
3953
  const parsedByTxid = /* @__PURE__ */ new Map();
@@ -4415,6 +4489,7 @@ async function pushClaim(operator, input) {
4415
4489
 
4416
4490
  // src/client/drive.ts
4417
4491
  init_onchainHtlc();
4492
+ init_coverage();
4418
4493
  init_lockupContract();
4419
4494
 
4420
4495
  // src/refundBlocked.ts
@@ -4615,11 +4690,15 @@ function shouldRetainRfqSwap(record, now) {
4615
4690
  return now - record.updatedAt < RFQ_SWAP_RETENTION_SECONDS;
4616
4691
  }
4617
4692
 
4693
+ // src/client/drive.ts
4694
+ init_store();
4695
+
4618
4696
  // src/swapManager.ts
4619
4697
  var import_base15 = require("@scure/base");
4620
4698
  var import_sdk21 = require("@arkade-os/sdk");
4621
4699
  init_onchainHtlc();
4622
4700
  init_lockupContract();
4701
+ init_coverage();
4623
4702
  function nextOnchainAction(input) {
4624
4703
  switch (input.phase.phase) {
4625
4704
  case "unfunded":
@@ -5201,6 +5280,40 @@ var RfqSwapManager = class {
5201
5280
  void this.poll().then(() => this.arm());
5202
5281
  }, this.config.pollIntervalMs);
5203
5282
  }
5283
+ /*
5284
+ * Runs one serialized monitoring pass for a single swap.
5285
+ *
5286
+ * Flow
5287
+ *
5288
+ * 1. Prevents concurrent work
5289
+ * - Returns immediately if the swap is already in inProgress.
5290
+ * - Returns if the swap is no longer in monitored.
5291
+ * - Adds the swap ID to inProgress.
5292
+ *
5293
+ * 2. Runs the state machine
5294
+ * - Calls runPass(swap).
5295
+ * - runPass checks the lockup fate, drives claims, and may initiate refunds.
5296
+ *
5297
+ * 3. Persists changes
5298
+ * - If the pass marked the swap dirty, calls save(swap).
5299
+ * - A swap is considered persisted only if all configured sinks succeed:
5300
+ * - the repository, and
5301
+ * - callbacks.saveSwap, if provided.
5302
+ *
5303
+ * 4. Completes waiters and finalizes
5304
+ * - If persistence succeeds:
5305
+ * - clears the dirty flag,
5306
+ * - settles waitForSwapCompletion waiters,
5307
+ * - moves terminal swaps from monitored to finished,
5308
+ * - emits completion or failure notifications through finalize.
5309
+ *
5310
+ * 5. Releases the per-swap lock
5311
+ * - Deletes the swap ID from inProgress only after all asynchronous work completes.
5312
+ * - This prevents overlapping poll() calls from submitting duplicate claims or refunds.
5313
+ *
5314
+ * The important safety property is that terminal state is not finalized until its updated record has been
5315
+ * successfully persisted. A failed save leaves the swap dirty and monitored so a later poll can retry.
5316
+ */
5204
5317
  async pollSwap(swap) {
5205
5318
  if (this.inProgress.has(swap.rfqId)) return;
5206
5319
  if (!this.monitored.has(swap.rfqId)) return;
@@ -5217,6 +5330,42 @@ var RfqSwapManager = class {
5217
5330
  this.inProgress.delete(swap.rfqId);
5218
5331
  }
5219
5332
  }
5333
+ /*
5334
+ * executes one monitoring/state-machine pass for a swap.
5335
+ *
5336
+ * Flow
5337
+ * 1. Registers the lockup
5338
+ * - Calls ensureRegistered(swap).
5339
+ * - Registration is best effort and does not prevent the rest of the pass from running.
5340
+ *
5341
+ * 2. Reads the Arkade lockup fate
5342
+ * - Calls readLockupFate.
5343
+ * - A hash-verified claim is treated as claimed.
5344
+ * - A fully observed non-claim spend is treated as returned.
5345
+ * - Indexer errors become unknown, allowing deadline-driven logic to continue.
5346
+ * - For claimed or returned, it:
5347
+ * - records spend transaction IDs,
5348
+ * - records the settlement preimage when applicable,
5349
+ * - sets the swap state to settled or refunded,
5350
+ * - ends the pass.
5351
+ *
5352
+ * 3. Handles unilateral exits
5353
+ * - On receive swaps, an exited lockup is blocked immediately.
5354
+ * - On send swaps, the L1 half continues to be monitored because an exit only describes the Arkade side.
5355
+ *
5356
+ * 4. Drives the trader’s claim
5357
+ * - For lightning_receive, calls driveReceiveClaim and ends the pass.
5358
+ * - For onchain_send, calls driveOnchain to classify and potentially claim the L1 HTLC.
5359
+ * - A handled L1 action ends the pass; otherwise processing continues.
5360
+ *
5361
+ * 5. Drives an Arkade refund
5362
+ * - For send swaps whose lockup remains unresolved, calls driveArkadeRefund.
5363
+ * - This is gated by the refund timelock and local refund capability.
5364
+ * - Receive swaps never reach this step because their refund path belongs to the solver.
5365
+ *
5366
+ * runPass only decides and performs state-machine actions. Persistence, waiter settlement, terminal finalization,
5367
+ * and the per-swap lock are handled by pollSwap.
5368
+ */
5220
5369
  async runPass(swap) {
5221
5370
  await this.ensureRegistered(swap);
5222
5371
  let fate;
@@ -5393,6 +5542,21 @@ var RfqSwapManager = class {
5393
5542
  });
5394
5543
  if (action === "claim" && phase.phase === "claimable") {
5395
5544
  if (swap.claimTxid) return "continue";
5545
+ if (!Number.isSafeInteger(swap.expectedAmount) || swap.expectedAmount <= 0) {
5546
+ this.block(
5547
+ swap,
5548
+ `expectedAmount is not a positive number of sats (${String(swap.expectedAmount)}), so the funded value cannot be checked \u2014 refusing to publish the preimage`
5549
+ );
5550
+ return "handled";
5551
+ }
5552
+ const filled = Number(phase.utxo.amount);
5553
+ if (!Number.isFinite(filled) || filled < swap.expectedAmount) {
5554
+ this.block(
5555
+ swap,
5556
+ `fill holds ${filled} sats, below the agreed ${swap.expectedAmount} \u2014 refusing to publish the preimage`
5557
+ );
5558
+ return "handled";
5559
+ }
5396
5560
  if (this.callbacks && !this.callbacks.claimOnchain) {
5397
5561
  this.block(swap, RFQ_CONFIGURATION_REFUSAL.noClaimOnchainCallback);
5398
5562
  return "handled";
@@ -5443,6 +5607,8 @@ var RfqSwapManager = class {
5443
5607
  if (pushed) {
5444
5608
  swap.refundTxid = pushed.txid;
5445
5609
  this.touch(swap);
5610
+ } else if (now < swap.refundLocktime + REFUND_MTP_LAG_SECONDS) {
5611
+ return;
5446
5612
  }
5447
5613
  this.setState(swap, "refunded");
5448
5614
  this.emitAction(swap, "refundArkade");
@@ -5678,6 +5844,9 @@ var outpointKey = (vtxo) => `${vtxo.txid}:${vtxo.vout}`;
5678
5844
  // src/watch.ts
5679
5845
  var import_base16 = require("@scure/base");
5680
5846
  var import_sdk22 = require("@arkade-os/sdk");
5847
+ init_coverage();
5848
+ init_offer();
5849
+ init_store();
5681
5850
  var TERMINAL = ["fulfilled", "cancelled", "recoverable"];
5682
5851
  var assetSwapSource = (repository) => ({
5683
5852
  list: () => getAssetSwaps(repository),
@@ -6328,22 +6497,22 @@ var createSwapDrive = (config) => {
6328
6497
  };
6329
6498
 
6330
6499
  // src/client/quoteOffer.ts
6331
- var import_solver_discovery7 = require("@arkade-os/solver-discovery");
6500
+ var import_solver_discovery8 = require("@arkade-os/solver-discovery");
6332
6501
 
6333
6502
  // src/client/market.ts
6334
- var import_solver_discovery6 = require("@arkade-os/solver-discovery");
6503
+ var import_solver_discovery7 = require("@arkade-os/solver-discovery");
6335
6504
  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";
6505
+ const base = (0, import_solver_discovery7.marketLegKey)(card, "base");
6506
+ const quote = (0, import_solver_discovery7.marketLegKey)(card, "quote");
6507
+ const baseIsArkade = (0, import_solver_discovery7.marketCorridor)(card, "base") === "arkade";
6508
+ const quoteIsArkade = (0, import_solver_discovery7.marketCorridor)(card, "quote") === "arkade";
6340
6509
  if (baseIsArkade !== quoteIsArkade) {
6341
6510
  return baseIsArkade ? `${base}/${quote}` : `${quote}/${base}`;
6342
6511
  }
6343
6512
  return base <= quote ? `${base}/${quote}` : `${quote}/${base}`;
6344
6513
  };
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";
6514
+ var isAddressable = (card) => !(0, import_solver_discovery7.isRfqMarket)(card) || typeof card.discovery_pubkey === "string" && (card.transports?.nostr?.relays?.length ?? 0) > 0;
6515
+ var marketBackendOf = (card) => (0, import_solver_discovery7.isRfqMarket)(card) ? "rfq" : "feed";
6347
6516
  var usableMarkets = (snapshot, policy) => {
6348
6517
  const allowed = policy?.allowedRegistries;
6349
6518
  return (allowed ? snapshot.markets.filter((card) => allowed.includes(card.source)) : snapshot.markets).filter(isAddressable);
@@ -6361,12 +6530,12 @@ var eligibleMarkets = (snapshot, legs, policy, takeAmount) => {
6361
6530
  wantSide: side === "base" ? "quote" : "base",
6362
6531
  ...takeAmount === void 0 ? {} : { wantAmount: takeAmount }
6363
6532
  };
6364
- const canonical = (0, import_solver_discovery6.selectMarkets)([...markets], {
6533
+ const canonical = (0, import_solver_discovery7.selectMarkets)([...markets], {
6365
6534
  baseId: base.marketId,
6366
6535
  quoteId: quote.marketId,
6367
6536
  ...selection
6368
6537
  });
6369
- const legacy = (0, import_solver_discovery6.selectMarkets)([...markets], {
6538
+ const legacy = (0, import_solver_discovery7.selectMarkets)([...markets], {
6370
6539
  baseId: `${base.corridor}:${base.assetId}`,
6371
6540
  quoteId: `${quote.corridor}:${quote.assetId}`,
6372
6541
  ...selection
@@ -6727,7 +6896,7 @@ var quoteFromFeed = async (input) => {
6727
6896
  if (amount === void 0) {
6728
6897
  throw new Error("an asset swap needs an amount and the side it pins");
6729
6898
  }
6730
- const plan = await (0, import_solver_discovery7.quoteOffer)(candidate.card, {
6899
+ const plan = await (0, import_solver_discovery8.quoteOffer)(candidate.card, {
6731
6900
  give: candidate.give,
6732
6901
  ...amount.on === "give" ? { giveAmount: amount.value } : { wantAmount: amount.value },
6733
6902
  ...QUOTE_OPTIONS,
@@ -6759,7 +6928,7 @@ var quoteFromFeed = async (input) => {
6759
6928
  };
6760
6929
  };
6761
6930
  var spreadOf = (plan) => {
6762
- const fair = (0, import_solver_discovery7.computeWantAmount)({
6931
+ const fair = (0, import_solver_discovery8.computeWantAmount)({
6763
6932
  deposit: plan.deposit.atomic,
6764
6933
  give: plan.give,
6765
6934
  price: plan.price,
@@ -6816,6 +6985,7 @@ init_rfq();
6816
6985
 
6817
6986
  // src/client/rfqWire.ts
6818
6987
  init_rfq();
6988
+ init_store();
6819
6989
  var BTC_LEG = {
6820
6990
  arkade: ARKADE_BTC,
6821
6991
  lightning: LIGHTNING_BTC,
@@ -7189,6 +7359,10 @@ var quoteOnchainSend = async (input) => {
7189
7359
  secrets,
7190
7360
  refundAddress: payoutKey.address,
7191
7361
  fundAmount: parsed.give,
7362
+ // The gross wire leg, not `reportedTake`: the claim fee comes out of
7363
+ // the HTLC output, so netting it here would accept an HTLC funded
7364
+ // short by exactly that fee.
7365
+ expectedAmount: parsed.take,
7192
7366
  payoutKey,
7193
7367
  payoutPkScript,
7194
7368
  htlc: derived.htlc,