@arkade-os/swap 0.0.1

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.
@@ -0,0 +1,1363 @@
1
+ // src/onchainHtlc.ts
2
+ import { hex } from "@scure/base";
3
+ import { sha256 } from "@noble/hashes/sha2.js";
4
+ import { ripemd160 } from "@noble/hashes/legacy.js";
5
+ import * as btc from "@scure/btc-signer";
6
+ var ONCHAIN_ORDER_MARGIN_SECONDS = 2 * 60 * 60;
7
+ var ONCHAIN_CLAIM_MARGIN_SECONDS = 90 * 60;
8
+ var MAX_MIN_CONFIRMATIONS = 6;
9
+ var ONCHAIN_SECONDS_PER_BLOCK = 600;
10
+ var ONCHAIN_DUST_SATS = BigInt(330);
11
+ var newPreimage = () => crypto.getRandomValues(new Uint8Array(32));
12
+ var paymentHashOf = (preimage) => hex.encode(sha256(preimage));
13
+ var h160FromPaymentHash = (paymentHash) => ripemd160(hex.decode(paymentHash));
14
+ var L1_NETWORKS = {
15
+ bitcoin: btc.NETWORK,
16
+ testnet: btc.TEST_NETWORK,
17
+ regtest: { ...btc.TEST_NETWORK, bech32: "bcrt" }
18
+ };
19
+ function onchainHtlcScript(params, network) {
20
+ if (params.claimKey.length !== 32 || params.refundKey.length !== 32) {
21
+ throw new Error("claimKey and refundKey must be 32-byte x-only keys");
22
+ }
23
+ if (!Number.isInteger(params.refundLocktime) || params.refundLocktime <= 0) {
24
+ throw new Error(
25
+ `refundLocktime must be a positive unix timestamp, got ${params.refundLocktime}`
26
+ );
27
+ }
28
+ const h160 = h160FromPaymentHash(params.paymentHash);
29
+ const claim = btc.Script.encode([
30
+ "SIZE",
31
+ 32,
32
+ "EQUALVERIFY",
33
+ "HASH160",
34
+ h160,
35
+ "EQUALVERIFY",
36
+ params.claimKey,
37
+ "CHECKSIG"
38
+ ]);
39
+ const refund = btc.Script.encode([
40
+ btc.ScriptNum().encode(BigInt(params.refundLocktime)),
41
+ "CHECKLOCKTIMEVERIFY",
42
+ "DROP",
43
+ params.refundKey,
44
+ "CHECKSIG"
45
+ ]);
46
+ const payment = btc.p2tr(
47
+ btc.TAPROOT_UNSPENDABLE_KEY,
48
+ btc.taprootListToTree([{ script: claim }, { script: refund }]),
49
+ L1_NETWORKS[network],
50
+ true
51
+ );
52
+ const controlBlockFor = (leaf) => {
53
+ for (const [block, script] of payment.tapLeafScript ?? []) {
54
+ if (script.length - 1 === leaf.length && hex.encode(script.subarray(0, leaf.length)) === hex.encode(leaf)) {
55
+ return btc.TaprootControlBlock.encode(block);
56
+ }
57
+ }
58
+ throw new Error("leaf missing from compiled taproot tree");
59
+ };
60
+ return {
61
+ address: payment.address,
62
+ pkScript: payment.script,
63
+ leaves: { claim, refund },
64
+ controlBlocks: { claim: controlBlockFor(claim), refund: controlBlockFor(refund) },
65
+ paymentHash: params.paymentHash,
66
+ refundLocktime: params.refundLocktime
67
+ };
68
+ }
69
+ var buildLeafSpend = async (input) => {
70
+ if (!Number.isFinite(input.feeRateSatVb) || input.feeRateSatVb <= 0) {
71
+ throw new Error(`feeRateSatVb must be positive, got ${input.feeRateSatVb}`);
72
+ }
73
+ const assemble = (payout2) => {
74
+ const tx2 = new btc.Transaction({ lockTime: input.lockTime ?? 0 });
75
+ tx2.addInput({
76
+ txid: input.utxo.txid,
77
+ index: input.utxo.vout,
78
+ witnessUtxo: { script: input.htlc.pkScript, amount: input.utxo.amount },
79
+ sequence: input.sequence
80
+ });
81
+ tx2.addOutput({ script: input.payoutPkScript, amount: payout2 });
82
+ return tx2;
83
+ };
84
+ const witness = (sig2) => [
85
+ sig2,
86
+ ...input.stackAboveSig,
87
+ input.leaf,
88
+ input.controlBlock
89
+ ];
90
+ const sizing = assemble(input.utxo.amount);
91
+ sizing.updateInput(0, { finalScriptWitness: witness(new Uint8Array(64)) }, true);
92
+ const fee = BigInt(Math.ceil(sizing.vsize * input.feeRateSatVb));
93
+ const payout = input.utxo.amount - fee;
94
+ if (payout < ONCHAIN_DUST_SATS) {
95
+ throw new Error(
96
+ `fee ${fee} leaves ${payout} sats from a ${input.utxo.amount} sat HTLC \u2014 below the ${ONCHAIN_DUST_SATS} sat dust limit`
97
+ );
98
+ }
99
+ const tx = assemble(payout);
100
+ const sighash = tx.preimageWitnessV1(
101
+ 0,
102
+ [input.htlc.pkScript],
103
+ btc.SigHash.DEFAULT,
104
+ [input.utxo.amount],
105
+ void 0,
106
+ input.leaf,
107
+ 192
108
+ );
109
+ const sig = await input.sign(sighash);
110
+ if (sig.length !== 64)
111
+ throw new Error(`sign() must return a 64-byte BIP340 signature, got ${sig.length}`);
112
+ tx.updateInput(0, { finalScriptWitness: witness(sig) }, true);
113
+ return { txHex: tx.hex, txid: tx.id, payoutAmount: payout };
114
+ };
115
+ var buildHtlcClaim = async (input) => {
116
+ if (paymentHashOf(input.preimage) !== input.htlc.paymentHash) {
117
+ throw new Error("preimage does not hash to the HTLC's payment hash");
118
+ }
119
+ return buildLeafSpend({
120
+ htlc: input.htlc,
121
+ utxo: input.utxo,
122
+ leaf: input.htlc.leaves.claim,
123
+ controlBlock: input.htlc.controlBlocks.claim,
124
+ stackAboveSig: [input.preimage],
125
+ payoutPkScript: input.payoutPkScript,
126
+ feeRateSatVb: input.feeRateSatVb,
127
+ sign: input.sign,
128
+ sequence: 4294967293
129
+ });
130
+ };
131
+ var buildHtlcRefund = (input) => buildLeafSpend({
132
+ htlc: input.htlc,
133
+ utxo: input.utxo,
134
+ leaf: input.htlc.leaves.refund,
135
+ controlBlock: input.htlc.controlBlocks.refund,
136
+ stackAboveSig: [],
137
+ payoutPkScript: input.payoutPkScript,
138
+ feeRateSatVb: input.feeRateSatVb,
139
+ sign: input.sign,
140
+ lockTime: input.htlc.refundLocktime,
141
+ // any value below 0xffffffff enables nLockTime enforcement
142
+ sequence: 4294967294
143
+ });
144
+ function extractPreimage(txHex, paymentHash) {
145
+ let raw;
146
+ try {
147
+ raw = btc.RawTx.decode(hex.decode(txHex));
148
+ } catch {
149
+ return null;
150
+ }
151
+ for (const stack of raw.witnesses ?? []) {
152
+ for (const item of stack) {
153
+ if (item.length === 32 && hex.encode(sha256(item)) === paymentHash) return item;
154
+ }
155
+ }
156
+ return null;
157
+ }
158
+ var sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
159
+ async function awaitOnchainFill(chain, htlc, minConfirmations, options = {}) {
160
+ const pollMs = options.pollMs ?? 5e3;
161
+ for (; ; ) {
162
+ const utxos = await chain.getScriptUtxos(htlc.pkScript);
163
+ const eligible = utxos.filter((u) => u.confirmations >= minConfirmations).sort((a, b) => b.amount > a.amount ? 1 : -1);
164
+ if (eligible[0]) return eligible[0];
165
+ if (options.deadline !== void 0 && Date.now() / 1e3 >= options.deadline) {
166
+ const error = new Error("HTLC was not filled before the deadline");
167
+ error.reason = "fill_timeout";
168
+ throw error;
169
+ }
170
+ await sleep(pollMs);
171
+ }
172
+ }
173
+ async function claimOnchainFill(chain, input) {
174
+ const now = input.now ?? Math.floor(Date.now() / 1e3);
175
+ if (input.htlc.refundLocktime - now < ONCHAIN_CLAIM_MARGIN_SECONDS) {
176
+ const error = new Error(
177
+ "refund leaf opens too soon to claim safely \u2014 take the covenant refund instead"
178
+ );
179
+ error.reason = "claim_window_closed";
180
+ throw error;
181
+ }
182
+ const spend = await buildHtlcClaim(input);
183
+ const txid = await chain.broadcast(spend.txHex);
184
+ return { txid, payoutAmount: spend.payoutAmount };
185
+ }
186
+ async function classifyOnchainHtlc(chain, input) {
187
+ const utxos = await chain.getScriptUtxos(input.htlc.pkScript);
188
+ const best = utxos.sort((a, b) => b.amount > a.amount ? 1 : -1)[0];
189
+ if (!best) {
190
+ if (!input.funding) return { phase: "unfunded" };
191
+ const spend = await chain.getSpendingTx(input.funding.txid, input.funding.vout);
192
+ if (!spend) return { phase: "unfunded" };
193
+ const preimage = extractPreimage(spend.txHex, input.htlc.paymentHash);
194
+ const txid = btc.Transaction.fromRaw(hex.decode(spend.txHex), {
195
+ allowUnknownInputs: true,
196
+ allowUnknownOutputs: true
197
+ }).id;
198
+ return preimage ? { phase: "claimed", txid, preimage } : { phase: "swept", txid };
199
+ }
200
+ if (best.confirmations < input.minConfirmations)
201
+ return { phase: "awaiting_confirmations", utxo: best };
202
+ const mtp = await chain.getMtp();
203
+ if (mtp >= input.htlc.refundLocktime) return { phase: "refundable", utxo: best };
204
+ return { phase: "claimable", utxo: best };
205
+ }
206
+
207
+ // src/secrets.ts
208
+ import { hex as hex2 } from "@scure/base";
209
+ import { sha256 as sha2562 } from "@noble/hashes/sha2.js";
210
+ import { randomBytes } from "@noble/hashes/utils.js";
211
+ import { schnorr } from "@noble/curves/secp256k1.js";
212
+ import {
213
+ SingleKey,
214
+ isHDAllocationCapable,
215
+ isHDWalletCapable
216
+ } from "@arkade-os/sdk";
217
+ var RFQ_PREIMAGE_TAG = "Arkade-RFQ-Preimage-v1";
218
+ function buildPreimageMessage(xonly, index) {
219
+ if (xonly.length !== 32) {
220
+ throw new Error(`x-only pubkey must be 32 bytes, got ${xonly.length}`);
221
+ }
222
+ if (!Number.isInteger(index) || index < 0 || index > 4294967295) {
223
+ throw new Error(`index must be a u32, got ${index}`);
224
+ }
225
+ const tag = new TextEncoder().encode(RFQ_PREIMAGE_TAG);
226
+ const message = new Uint8Array(tag.length + 32 + 4);
227
+ message.set(tag, 0);
228
+ message.set(xonly, tag.length);
229
+ new DataView(message.buffer).setUint32(tag.length + 32, index, true);
230
+ return message;
231
+ }
232
+ var PREIMAGE_INDEX = 0;
233
+ async function deriveSwapSecrets(wallet) {
234
+ if (!isHDAllocationCapable(wallet)) return void 0;
235
+ const signingDescriptor = await wallet.getNextSigningDescriptor();
236
+ if (!signingDescriptor) return void 0;
237
+ return { derivable: true, signingDescriptor };
238
+ }
239
+ function randomSwapSecrets(opts = {}) {
240
+ if (opts.preimage instanceof Uint8Array && opts.preimage.length !== 32) {
241
+ throw new Error(`preimage must be 32 bytes, got ${opts.preimage.length}`);
242
+ }
243
+ const preimage = opts.preimage instanceof Uint8Array ? opts.preimage : opts.preimage ? randomBytes(32) : void 0;
244
+ return {
245
+ derivable: false,
246
+ senderPrivateKey: schnorr.utils.randomSecretKey(),
247
+ ...preimage ? { preimage } : {}
248
+ };
249
+ }
250
+ function rfqSecretsToRecord(secrets) {
251
+ if (secrets.derivable) {
252
+ return {
253
+ signingDescriptor: secrets.signingDescriptor,
254
+ ...secrets.preimage ? { preimageHex: hex2.encode(secrets.preimage) } : {}
255
+ };
256
+ }
257
+ return {
258
+ fallbackSecrets: {
259
+ version: 1,
260
+ type: "stored",
261
+ senderPrivateKeyHex: hex2.encode(secrets.senderPrivateKey),
262
+ ...secrets.preimage ? { preimageHex: hex2.encode(secrets.preimage) } : {}
263
+ }
264
+ };
265
+ }
266
+ function rfqSecretsOfRecord(record) {
267
+ if (record.signingDescriptor) {
268
+ return {
269
+ derivable: true,
270
+ signingDescriptor: record.signingDescriptor,
271
+ ...record.preimageHex ? { preimage: decodeHex32(record.preimageHex, "preimageHex") } : {}
272
+ };
273
+ }
274
+ const fallback = record.fallbackSecrets;
275
+ if (!fallback) return void 0;
276
+ if (fallback.version !== 1 || fallback.type !== "stored") {
277
+ throw new Error("unsupported RFQ fallback secrets record");
278
+ }
279
+ return {
280
+ derivable: false,
281
+ senderPrivateKey: decodeHex32(fallback.senderPrivateKeyHex, "senderPrivateKeyHex"),
282
+ ...fallback.preimageHex ? { preimage: decodeHex32(fallback.preimageHex, "preimageHex") } : {}
283
+ };
284
+ }
285
+ function decodeHex32(value, label) {
286
+ const bytes = hex2.decode(value);
287
+ if (bytes.length !== 32) {
288
+ throw new Error(label + " must be 32 bytes, got " + bytes.length);
289
+ }
290
+ return bytes;
291
+ }
292
+ async function adoptSwapDescriptor(wallet, signingDescriptor) {
293
+ if (!isHDAllocationCapable(wallet)) return;
294
+ await wallet.advanceSigningDescriptorWatermark(signingDescriptor);
295
+ }
296
+ var RefundNotLocallyPossibleError = class extends Error {
297
+ constructor(reason, message, options) {
298
+ super(message, options);
299
+ this.reason = reason;
300
+ }
301
+ reason;
302
+ name = "RefundNotLocallyPossibleError";
303
+ };
304
+ async function senderIdentityForRfqSecrets(wallet, secrets) {
305
+ if (!secrets.derivable) return SingleKey.fromPrivateKey(secrets.senderPrivateKey);
306
+ const signer = isHDWalletCapable(wallet) ? await wallet.signerForDescriptor(secrets.signingDescriptor) : void 0;
307
+ if (!signer || !isDeterministicSigner(signer)) {
308
+ throw new RefundNotLocallyPossibleError(
309
+ "foreign-descriptor",
310
+ `this wallet cannot derive ${secrets.signingDescriptor}; the swap was created on another wallet`
311
+ );
312
+ }
313
+ return signer;
314
+ }
315
+ async function senderIdentityForSwapRecord(wallet, record) {
316
+ let secrets;
317
+ try {
318
+ secrets = rfqSecretsOfRecord(record);
319
+ } catch (error) {
320
+ throw new RefundNotLocallyPossibleError(
321
+ "unreadable-secrets",
322
+ error instanceof Error ? error.message : String(error),
323
+ // the record's own diagnosis, kept for a caller that wants more
324
+ // than the message
325
+ { cause: error }
326
+ );
327
+ }
328
+ if (!secrets) {
329
+ throw new RefundNotLocallyPossibleError(
330
+ "no-secrets",
331
+ "this swap record carries no signing descriptor and no fallback secrets"
332
+ );
333
+ }
334
+ return senderIdentityForRfqSecrets(wallet, secrets);
335
+ }
336
+ async function senderPubkeyForRfqSecrets(wallet, secrets) {
337
+ if (!secrets.derivable) return schnorr.getPublicKey(secrets.senderPrivateKey);
338
+ return (await senderIdentityForRfqSecrets(wallet, secrets)).xOnlyPublicKey();
339
+ }
340
+ function isDeterministicSigner(value) {
341
+ if (typeof value !== "object" || value === null) return false;
342
+ const v = value;
343
+ return typeof v.signSchnorrDeterministic === "function" && typeof v.xOnlyPublicKey === "function";
344
+ }
345
+ async function derivePreimage(signer) {
346
+ const xonly = await signer.xOnlyPublicKey();
347
+ const message = buildPreimageMessage(xonly, PREIMAGE_INDEX);
348
+ return sha2562(await signer.signSchnorrDeterministic(sha2562(message)));
349
+ }
350
+ async function preimageForRfqSecrets(wallet, secrets) {
351
+ if (!secrets.derivable) {
352
+ if (!secrets.preimage) throw new Error("this swap carries no stored preimage");
353
+ return secrets.preimage;
354
+ }
355
+ if (secrets.preimage) return secrets.preimage;
356
+ const signer = await senderIdentityForRfqSecrets(wallet, secrets);
357
+ if (!isDeterministicSigner(signer)) {
358
+ throw new Error(
359
+ `wallet cannot sign deterministically for ${secrets.signingDescriptor}; its preimage is not derivable`
360
+ );
361
+ }
362
+ try {
363
+ return await derivePreimage(signer);
364
+ } catch (cause) {
365
+ throw new Error(
366
+ `wallet cannot sign deterministically for ${secrets.signingDescriptor}; its preimage is not derivable`,
367
+ { cause }
368
+ );
369
+ }
370
+ }
371
+
372
+ // src/claimPacket.ts
373
+ import { base64 } from "@scure/base";
374
+ import { secp256k1 } from "@noble/curves/secp256k1.js";
375
+ import { hkdf } from "@noble/hashes/hkdf.js";
376
+ import { sha256 as sha2563 } from "@noble/hashes/sha2.js";
377
+ var HKDF_INFO = new TextEncoder().encode("covclaimd/preimage/v1");
378
+ async function sealClaimPacket(input) {
379
+ return sealWithEntropy(
380
+ input,
381
+ secp256k1.utils.randomSecretKey(),
382
+ crypto.getRandomValues(new Uint8Array(12))
383
+ );
384
+ }
385
+ async function sealWithEntropy(input, ephemeralKey, nonce) {
386
+ if (input.preimage.length !== 32) throw new Error("preimage must be 32 bytes");
387
+ if (input.covclaimdPubkey.length !== 33) {
388
+ throw new Error("covclaimd pubkey must be 33-byte compressed");
389
+ }
390
+ const ephemeralPub = secp256k1.getPublicKey(ephemeralKey, true);
391
+ const sharedX = secp256k1.getSharedSecret(ephemeralKey, input.covclaimdPubkey, true).subarray(1);
392
+ const key = hkdf(sha2563, sharedX, ephemeralPub, HKDF_INFO, 32);
393
+ if (nonce.length !== 12) throw new Error("nonce must be 12 bytes");
394
+ const aesKey = await crypto.subtle.importKey("raw", key, "AES-GCM", false, [
395
+ "encrypt"
396
+ ]);
397
+ const sealed = new Uint8Array(
398
+ await crypto.subtle.encrypt(
399
+ {
400
+ name: "AES-GCM",
401
+ iv: nonce,
402
+ additionalData: ephemeralPub
403
+ },
404
+ aesKey,
405
+ input.preimage
406
+ )
407
+ );
408
+ const packet = new Uint8Array(33 + 12 + sealed.length);
409
+ packet.set(ephemeralPub, 0);
410
+ packet.set(nonce, 33);
411
+ packet.set(sealed, 45);
412
+ return { ciphertext: base64.encode(packet) };
413
+ }
414
+
415
+ // src/lockupContract.ts
416
+ import { hex as hex3 } from "@scure/base";
417
+ import { VHTLCV2ContractHandler } from "@arkade-os/sdk";
418
+ var SWAP_LOCKUP_CONTRACT_TYPE = "vhtlc-v2";
419
+ var SWAP_LOCKUP_CONTRACT_LABEL = "Arkade RFQ swap lockup";
420
+ var SWAP_LOCKUP_CONTRACT_KIND = "rfq-swap-lockup";
421
+ var LockupRegistrationFailed = class extends Error {
422
+ /** The lockup address that was never registered — never fund it: nothing
423
+ * is watching it. */
424
+ address;
425
+ /** The covenant the row would have been written from — the other half of
426
+ * `registerLockupContract`, so the write is retryable without a quote. */
427
+ script;
428
+ constructor(script, address, cause) {
429
+ super(`failed to register the lockup contract for ${address}`, { cause });
430
+ this.name = "LockupRegistrationFailed";
431
+ this.address = address;
432
+ this.script = script;
433
+ }
434
+ };
435
+ async function registerLockupContract(contracts, script, address) {
436
+ try {
437
+ await contracts.createContract({
438
+ type: SWAP_LOCKUP_CONTRACT_TYPE,
439
+ params: VHTLCV2ContractHandler.serializeParams(script.options),
440
+ script: hex3.encode(script.pkScript),
441
+ address,
442
+ label: SWAP_LOCKUP_CONTRACT_LABEL,
443
+ metadata: { genericallySpendable: false, kind: SWAP_LOCKUP_CONTRACT_KIND }
444
+ });
445
+ } catch (error) {
446
+ throw new LockupRegistrationFailed(script, address, error);
447
+ }
448
+ }
449
+
450
+ // src/rfq.ts
451
+ import { hex as hex4 } from "@scure/base";
452
+ import { ripemd160 as ripemd1602 } from "@noble/hashes/legacy.js";
453
+ import {
454
+ ArkAddress,
455
+ RestArkProvider,
456
+ VHTLC,
457
+ getNetwork
458
+ } from "@arkade-os/sdk";
459
+ var xOnly = (key, label) => {
460
+ if (key.length === 32) return key;
461
+ if (key.length !== 33 || key[0] !== 2 && key[0] !== 3) {
462
+ throw new Error(`${label} is not a compressed or x-only public key`);
463
+ }
464
+ return key.slice(1);
465
+ };
466
+ var solverHex = (value, field) => {
467
+ try {
468
+ return hex4.decode(value);
469
+ } catch {
470
+ throw new Error(`solver sent malformed hex for ${field}`);
471
+ }
472
+ };
473
+ var ARKADE_BTC = "arkade:BTC";
474
+ var ARKADE_ASSET = "arkade:ASSET";
475
+ var LIGHTNING_BTC = "lightning:BTC";
476
+ var ONCHAIN_BTC = "onchain:BTC";
477
+ var rfqPair = (from, to) => `${from}->${to}`;
478
+ var LIGHTNING_SEND_PAIR = rfqPair(ARKADE_BTC, LIGHTNING_BTC);
479
+ var LIGHTNING_RECEIVE_PAIR = rfqPair(LIGHTNING_BTC, ARKADE_BTC);
480
+ var ONCHAIN_SEND_PAIR = rfqPair(ARKADE_BTC, ONCHAIN_BTC);
481
+ var ONCHAIN_RECEIVE_PAIR = rfqPair(ONCHAIN_BTC, ARKADE_BTC);
482
+ var RFQ_TERMINAL_STATES = ["settled", "refused", "expired", "refunded", "stuck"];
483
+ var SwapRefusal = class extends Error {
484
+ reason;
485
+ rfqId;
486
+ constructor(reason, rfqId) {
487
+ super(`solver refused: ${reason}`);
488
+ this.name = "SwapRefusal";
489
+ this.reason = reason;
490
+ this.rfqId = rfqId;
491
+ }
492
+ };
493
+ var AddressMismatch = class extends Error {
494
+ derived;
495
+ quoted;
496
+ constructor(derived, quoted) {
497
+ super("solver lockup address does not match local derivation \u2014 refusing to fund");
498
+ this.name = "AddressMismatch";
499
+ this.derived = derived;
500
+ this.quoted = quoted;
501
+ }
502
+ };
503
+ var newRfqId = () => hex4.encode(crypto.getRandomValues(new Uint8Array(32)));
504
+ var lightningSendRequest = (input) => ({
505
+ v: 1,
506
+ type: "rfq_request",
507
+ rfq_id: input.rfqId,
508
+ pair: LIGHTNING_SEND_PAIR,
509
+ amount_side: "to",
510
+ profile: {
511
+ invoice: input.invoice,
512
+ refund_address: input.refundAddress,
513
+ client_refund_pubkey: hex4.encode(input.senderPubkey)
514
+ }
515
+ });
516
+ var arkadeSwapRequest = (input) => {
517
+ if (Boolean(input.wantAsset) === Boolean(input.offerAsset)) {
518
+ throw new Error("set exactly one of wantAsset (BTC->asset) or offerAsset (asset->BTC)");
519
+ }
520
+ return {
521
+ v: 1,
522
+ type: "rfq_request",
523
+ rfq_id: input.rfqId,
524
+ pair: rfqPair(
525
+ input.offerAsset ? ARKADE_ASSET : ARKADE_BTC,
526
+ input.wantAsset ? ARKADE_ASSET : ARKADE_BTC
527
+ ),
528
+ amount_side: input.amountSide,
529
+ amount: input.amount,
530
+ profile: {
531
+ ...input.offerAsset && { offer_asset: hex4.encode(input.offerAsset.serialize()) },
532
+ ...input.wantAsset && { want_asset: hex4.encode(input.wantAsset.serialize()) }
533
+ }
534
+ };
535
+ };
536
+ var MIN_HEADROOM_SECONDS = 90 * 60;
537
+ var gateError = (reason, message) => {
538
+ const error = new Error(message);
539
+ error.reason = reason;
540
+ return error;
541
+ };
542
+ var assertFinite = (value, reason, label) => {
543
+ if (value !== void 0 && !Number.isFinite(value)) {
544
+ throw gateError(reason, `${label} is not a finite number (${String(value)})`);
545
+ }
546
+ };
547
+ var verifyLockupAddress = (quote, derivedAddress) => {
548
+ const quoted = quote.profile?.lockup_address;
549
+ if (derivedAddress !== quoted) throw new AddressMismatch(derivedAddress, quoted);
550
+ return derivedAddress;
551
+ };
552
+ var assertFundable = (input) => {
553
+ const fail = (reason, message) => {
554
+ throw gateError(reason, message);
555
+ };
556
+ if (input.invoiceExpiresAt !== void 0 && input.now >= input.invoiceExpiresAt) {
557
+ fail("invoice_expired", "invoice expired");
558
+ }
559
+ if (input.now >= input.quote.valid_until)
560
+ fail("quote_expired", "quote expired \u2014 request a fresh one");
561
+ if (input.quote.refund_locktime !== void 0 && input.quote.refund_locktime - input.now < MIN_HEADROOM_SECONDS) {
562
+ fail("insufficient_headroom", "refund deadline headroom below 90 minutes");
563
+ }
564
+ if (input.onchain) {
565
+ const { htlcLocktime, minConfirmations, direction } = input.onchain;
566
+ if (!Number.isInteger(minConfirmations) || minConfirmations < 1 || minConfirmations > MAX_MIN_CONFIRMATIONS) {
567
+ fail(
568
+ "confirmations_out_of_range",
569
+ `min_confirmations must be 1..${MAX_MIN_CONFIRMATIONS}, got ${minConfirmations}`
570
+ );
571
+ }
572
+ const needed = minConfirmations * ONCHAIN_SECONDS_PER_BLOCK + ONCHAIN_CLAIM_MARGIN_SECONDS;
573
+ if (htlcLocktime - input.now <= needed) {
574
+ fail("claim_window_too_short", "L1 HTLC locktime leaves no safe claim window");
575
+ }
576
+ if (direction === "send") {
577
+ if (input.quote.refund_locktime === void 0 || htlcLocktime + ONCHAIN_ORDER_MARGIN_SECONDS > input.quote.refund_locktime) {
578
+ fail(
579
+ "timelock_order",
580
+ "L1 HTLC locktime + margin must fall before the Arkade refund locktime"
581
+ );
582
+ }
583
+ }
584
+ }
585
+ };
586
+ var expectQuote = (payload, rfqId) => {
587
+ const p = payload;
588
+ if (p?.type === "rfq_refusal") throw new SwapRefusal(p.reason ?? "unknown", p.rfq_id ?? rfqId);
589
+ if (p?.type !== "rfq_quote" || p.rfq_id !== rfqId) {
590
+ throw new Error(`unexpected reply: ${p?.type ?? "no payload"}`);
591
+ }
592
+ return payload;
593
+ };
594
+ var httpTransport = (baseUrl, options = {}) => {
595
+ const fetchImpl = options.fetchImpl ?? fetch;
596
+ const readJson = async (response, what) => {
597
+ const body = await response.text();
598
+ try {
599
+ return JSON.parse(body);
600
+ } catch {
601
+ throw new Error(
602
+ `${what} returned HTTP ${response.status} with a non-JSON body: ${body.slice(0, 200)}`
603
+ );
604
+ }
605
+ };
606
+ return {
607
+ async requestQuote(payload) {
608
+ const response = await fetchImpl(`${baseUrl}/v1/swap`, {
609
+ method: "POST",
610
+ headers: { "content-type": "application/json" },
611
+ body: JSON.stringify(payload)
612
+ });
613
+ return expectQuote(await readJson(response, "quote request"), String(payload.rfq_id));
614
+ },
615
+ async status(rfqId) {
616
+ const response = await fetchImpl(`${baseUrl}/v1/rfq/${rfqId}`, { method: "GET" });
617
+ if (response.status === 404) return null;
618
+ const payload = await readJson(response, "status request");
619
+ return payload?.type === "rfq_status" ? payload : null;
620
+ },
621
+ async close() {
622
+ }
623
+ };
624
+ };
625
+ var relayTransport = (relayUrl, options) => {
626
+ const timeoutMs = options.timeoutMs ?? 3e4;
627
+ const Ctor = options.WebSocketCtor ?? WebSocket;
628
+ const pending = /* @__PURE__ */ new Map();
629
+ let sequence = 0;
630
+ const socketReady = new Promise((resolve, reject) => {
631
+ const ws = new Ctor(relayUrl);
632
+ ws.addEventListener("open", () => {
633
+ ws.send(
634
+ JSON.stringify({
635
+ op: "sub",
636
+ id: "s1",
637
+ filter: { recipient: options.clientPubkey }
638
+ })
639
+ );
640
+ resolve(ws);
641
+ });
642
+ ws.addEventListener("error", () => reject(new Error("relay connection failed")));
643
+ ws.addEventListener("message", (event) => {
644
+ let frame;
645
+ try {
646
+ frame = JSON.parse(String(event.data));
647
+ } catch {
648
+ return;
649
+ }
650
+ if (frame.op !== "event") return;
651
+ const payload = frame.event?.payload;
652
+ const rfqId = payload?.rfq_id;
653
+ const settle = rfqId !== void 0 ? pending.get(rfqId) : void 0;
654
+ if (settle && rfqId !== void 0) {
655
+ pending.delete(rfqId);
656
+ settle(payload);
657
+ }
658
+ });
659
+ });
660
+ const roundTrip = async (payload, rfqId) => {
661
+ const ws = await socketReady;
662
+ const reply = new Promise((resolve, reject) => {
663
+ const timer = setTimeout(() => {
664
+ pending.delete(rfqId);
665
+ reject(new Error(`no reply within ${timeoutMs}ms`));
666
+ }, timeoutMs);
667
+ pending.set(rfqId, (p) => {
668
+ clearTimeout(timer);
669
+ resolve(p);
670
+ });
671
+ });
672
+ ws.send(
673
+ JSON.stringify({
674
+ op: "event",
675
+ event: {
676
+ id: `${options.clientPubkey}:${sequence += 1}`,
677
+ author: options.clientPubkey,
678
+ recipient: options.solverPubkey,
679
+ createdAtMs: Date.now(),
680
+ payload
681
+ }
682
+ })
683
+ );
684
+ return reply;
685
+ };
686
+ return {
687
+ async requestQuote(payload) {
688
+ return expectQuote(
689
+ await roundTrip(payload, String(payload.rfq_id)),
690
+ String(payload.rfq_id)
691
+ );
692
+ },
693
+ async status(rfqId) {
694
+ const payload = await roundTrip(
695
+ { v: 1, type: "rfq_status_request", rfq_id: rfqId },
696
+ rfqId
697
+ );
698
+ return payload?.type === "rfq_status" ? payload : null;
699
+ },
700
+ async close() {
701
+ try {
702
+ (await socketReady).close();
703
+ } catch {
704
+ }
705
+ }
706
+ };
707
+ };
708
+ var SEQUENCE_GRANULARITY_SECONDS = 512;
709
+ var unilateralClaimDelay = (serverExitDelaySeconds) => {
710
+ if (!Number.isFinite(serverExitDelaySeconds) || serverExitDelaySeconds < SEQUENCE_GRANULARITY_SECONDS) {
711
+ throw new Error(
712
+ `server exit delay must be at least ${SEQUENCE_GRANULARITY_SECONDS}s of seconds, got ${serverExitDelaySeconds}`
713
+ );
714
+ }
715
+ if (serverExitDelaySeconds > (65535 - 2) * SEQUENCE_GRANULARITY_SECONDS) {
716
+ throw new Error(
717
+ `server exit delay ${serverExitDelaySeconds}s exceeds what BIP68 can encode once the two refund tiers are stacked above it`
718
+ );
719
+ }
720
+ return Math.ceil(serverExitDelaySeconds / SEQUENCE_GRANULARITY_SECONDS) * SEQUENCE_GRANULARITY_SECONDS;
721
+ };
722
+ var unilateralRefundDelay = (claimDelay) => claimDelay + SEQUENCE_GRANULARITY_SECONDS;
723
+ var unilateralRefundWithoutReceiverDelay = (claimDelay) => claimDelay + 2 * SEQUENCE_GRANULARITY_SECONDS;
724
+ function lightningSendVtxoScript(params) {
725
+ const seconds = (value) => ({
726
+ type: "seconds",
727
+ value: BigInt(value)
728
+ });
729
+ return new VHTLC.ScriptV2({
730
+ sender: params.senderPubkey,
731
+ receiver: params.solverPubkey,
732
+ server: params.serverPubkey,
733
+ preimageHash: ripemd1602(hex4.decode(params.paymentHash)),
734
+ refundLocktime: BigInt(params.refundLocktime),
735
+ unilateralClaimDelay: seconds(params.claimDelay),
736
+ unilateralRefundDelay: seconds(unilateralRefundDelay(params.claimDelay)),
737
+ unilateralRefundWithoutReceiverDelay: seconds(
738
+ unilateralRefundWithoutReceiverDelay(params.claimDelay)
739
+ ),
740
+ nonInteractiveClaim: {
741
+ receiverPkScript: params.receiverPkScript,
742
+ emulatorPubkey: params.emulatorPubkey
743
+ },
744
+ nonInteractiveRefund: {
745
+ senderPkScript: params.refundPkScript,
746
+ emulatorPubkey: params.emulatorPubkey
747
+ }
748
+ });
749
+ }
750
+ async function requestLightningSend(wallet, arkServerUrl, emulatorPubkey, transport, params) {
751
+ const rfqId = params.rfqId ?? newRfqId();
752
+ const secrets = await deriveSwapSecrets(wallet) ?? randomSwapSecrets();
753
+ if (!secrets.derivable) {
754
+ console.warn(
755
+ "[swap] wallet cannot allocate an HD descriptor: the sender key is random and MUST be persisted before funding"
756
+ );
757
+ }
758
+ const senderPubkey = await senderPubkeyForRfqSecrets(wallet, secrets);
759
+ const [info, refundAddress] = await Promise.all([
760
+ new RestArkProvider(arkServerUrl).getInfo(),
761
+ wallet.getAddress()
762
+ ]);
763
+ const quote = await transport.requestQuote(
764
+ lightningSendRequest({ rfqId, invoice: params.invoice.raw, refundAddress, senderPubkey })
765
+ );
766
+ if (quote.refund_locktime === void 0) {
767
+ throw new Error("lightning-send quote is missing refund_locktime");
768
+ }
769
+ const receiverPkScriptHex = quote.profile?.receiver_pk_script;
770
+ if (receiverPkScriptHex === void 0) {
771
+ throw new Error("lightning-send quote is missing profile.receiver_pk_script");
772
+ }
773
+ if (quote.to_amount !== params.invoice.amountSats) {
774
+ throw new Error(
775
+ `quote to_amount ${quote.to_amount} does not match the invoice's ${params.invoice.amountSats}`
776
+ );
777
+ }
778
+ if (quote.from_amount < quote.to_amount) {
779
+ throw new Error(
780
+ `quote from_amount ${quote.from_amount} is below the invoice amount \u2014 a negative spread is not a quote`
781
+ );
782
+ }
783
+ const serverPubkey = xOnly(hex4.decode(info.signerPubkey), "ark signer key");
784
+ const script = lightningSendVtxoScript({
785
+ solverPubkey: xOnly(hex4.decode(quote.solver_pubkey), "solver key"),
786
+ refundLocktime: quote.refund_locktime,
787
+ serverPubkey,
788
+ paymentHash: params.invoice.paymentHash,
789
+ claimDelay: unilateralClaimDelay(Number(info.unilateralExitDelay)),
790
+ emulatorPubkey: xOnly(emulatorPubkey, "emulator pubkey"),
791
+ senderPubkey,
792
+ receiverPkScript: solverHex(receiverPkScriptHex, "profile.receiver_pk_script"),
793
+ refundPkScript: ArkAddress.decode(refundAddress).pkScript
794
+ });
795
+ const address = script.address(getNetwork(info.network).hrp, serverPubkey).encode();
796
+ verifyLockupAddress(quote, address);
797
+ assertFundable({
798
+ quote,
799
+ invoiceExpiresAt: params.invoice.expiresAt,
800
+ now: Math.floor(Date.now() / 1e3)
801
+ });
802
+ await registerLockupContract(await wallet.getContractManager(), script, address);
803
+ return {
804
+ rfqId,
805
+ quote,
806
+ address,
807
+ // What the lockup must carry: the quote's `from_amount` — the invoice
808
+ // PLUS the corridor's fee, never the bare invoice amount.
809
+ fundAmount: quote.from_amount,
810
+ swapPkScript: script.pkScript,
811
+ script,
812
+ refundAddress,
813
+ senderPubkey,
814
+ secrets
815
+ };
816
+ }
817
+ var offerTermsFromQuote = (quote, assets) => {
818
+ if (Boolean(assets.wantAsset) === Boolean(assets.offerAsset)) {
819
+ throw new Error("set exactly one of wantAsset or offerAsset");
820
+ }
821
+ return { wantAmount: BigInt(quote.to_amount), ...assets };
822
+ };
823
+ var l1NetworkFromArk = (network) => network === "bitcoin" ? "bitcoin" : network === "regtest" ? "regtest" : "testnet";
824
+ var onchainSendRequest = (input) => ({
825
+ v: 1,
826
+ type: "rfq_request",
827
+ rfq_id: input.rfqId,
828
+ pair: ONCHAIN_SEND_PAIR,
829
+ amount_side: input.amountSide,
830
+ amount: input.amount,
831
+ profile: {
832
+ payment_hash: input.paymentHash,
833
+ payout_pubkey: hex4.encode(input.payoutPubkey),
834
+ refund_address: input.refundAddress,
835
+ client_refund_pubkey: hex4.encode(input.senderPubkey)
836
+ }
837
+ });
838
+ var lightningReceiveRequest = (input) => ({
839
+ v: 1,
840
+ type: "rfq_request",
841
+ rfq_id: input.rfqId,
842
+ pair: LIGHTNING_RECEIVE_PAIR,
843
+ amount_side: input.amountSide,
844
+ amount: input.amount,
845
+ profile: {
846
+ payment_hash: input.paymentHash,
847
+ payout_address: input.payoutAddress,
848
+ payout_pubkey: hex4.encode(input.payoutPubkey),
849
+ claim_packet: input.claimPacket
850
+ }
851
+ });
852
+ var onchainReceiveRequest = (input) => ({
853
+ v: 1,
854
+ type: "rfq_request",
855
+ rfq_id: input.rfqId,
856
+ pair: ONCHAIN_RECEIVE_PAIR,
857
+ amount_side: input.amountSide,
858
+ amount: input.amount,
859
+ profile: {
860
+ payment_hash: input.paymentHash,
861
+ claim_packet: input.claimPacket,
862
+ refund_pubkey: hex4.encode(input.refundPubkey),
863
+ payout_address: input.payoutAddress,
864
+ payout_pubkey: hex4.encode(input.payoutPubkey)
865
+ }
866
+ });
867
+ function deriveOnchainSend(input) {
868
+ const { quote } = input;
869
+ const profile = quote.profile ?? {};
870
+ const refundLocktime = quote.refund_locktime ?? profile.refund_locktime;
871
+ const htlcPubkey = profile.htlc_pubkey;
872
+ const htlcLocktime = profile.htlc_locktime;
873
+ const htlcAddress = profile.htlc_address;
874
+ const minConfirmations = profile.min_confirmations;
875
+ const receiverPkScriptHex = profile.receiver_pk_script;
876
+ if (refundLocktime === void 0 || htlcPubkey === void 0 || htlcLocktime === void 0 || minConfirmations === void 0 || receiverPkScriptHex === void 0) {
877
+ throw new Error("onchain-send quote is missing a binding field");
878
+ }
879
+ const script = lightningSendVtxoScript({
880
+ solverPubkey: xOnly(hex4.decode(quote.solver_pubkey), "solver key"),
881
+ refundLocktime,
882
+ serverPubkey: input.serverPubkey,
883
+ paymentHash: input.paymentHash,
884
+ claimDelay: input.claimDelay,
885
+ emulatorPubkey: input.emulatorPubkey,
886
+ senderPubkey: input.senderPubkey,
887
+ receiverPkScript: solverHex(receiverPkScriptHex, "profile.receiver_pk_script"),
888
+ refundPkScript: ArkAddress.decode(input.refundAddress).pkScript
889
+ });
890
+ const address = script.address(input.hrp, input.serverPubkey).encode();
891
+ verifyLockupAddress(quote, address);
892
+ const htlc = onchainHtlcScript(
893
+ {
894
+ paymentHash: input.paymentHash,
895
+ claimKey: input.payoutPubkey,
896
+ refundKey: xOnly(hex4.decode(htlcPubkey), "solver L1 htlc key"),
897
+ refundLocktime: htlcLocktime
898
+ },
899
+ input.l1Network
900
+ );
901
+ if (htlc.address !== htlcAddress) throw new AddressMismatch(htlc.address, htlcAddress);
902
+ return {
903
+ address,
904
+ swapPkScript: script.pkScript,
905
+ script,
906
+ htlc,
907
+ refundLocktime,
908
+ htlcLocktime,
909
+ minConfirmations
910
+ };
911
+ }
912
+ async function requestOnchainSend(wallet, arkServerUrl, emulatorPubkey, transport, params) {
913
+ const rfqId = params.rfqId ?? newRfqId();
914
+ if (params.preimage && params.preimage.length !== 32) {
915
+ throw new Error(`preimage must be 32 bytes, got ${params.preimage.length}`);
916
+ }
917
+ const derivedSecrets = await deriveSwapSecrets(wallet);
918
+ const secrets = derivedSecrets ? params.preimage ? { ...derivedSecrets, preimage: params.preimage } : derivedSecrets : randomSwapSecrets({ preimage: params.preimage ?? true });
919
+ if (!secrets.derivable) {
920
+ console.warn(
921
+ params.preimage ? "[swap] this swap's sender key is random; the supplied preimage and sender key MUST be persisted before funding" : "[swap] this swap's preimage and sender key are random and MUST be persisted before funding"
922
+ );
923
+ } else if (params.preimage) {
924
+ console.warn(
925
+ "[swap] this swap's preimage was supplied by the caller and MUST be persisted with the signing descriptor before funding"
926
+ );
927
+ }
928
+ const preimage = await preimageForRfqSecrets(wallet, secrets);
929
+ const paymentHash = paymentHashOf(preimage);
930
+ const senderPubkey = await senderPubkeyForRfqSecrets(wallet, secrets);
931
+ const [info, refundAddress] = await Promise.all([
932
+ new RestArkProvider(arkServerUrl).getInfo(),
933
+ wallet.getAddress()
934
+ ]);
935
+ const quote = await transport.requestQuote(
936
+ onchainSendRequest({
937
+ rfqId,
938
+ paymentHash,
939
+ payoutPubkey: params.payoutPubkey,
940
+ refundAddress,
941
+ senderPubkey,
942
+ amount: params.amount,
943
+ amountSide: params.amountSide
944
+ })
945
+ );
946
+ const derived = deriveOnchainSend({
947
+ quote,
948
+ paymentHash,
949
+ payoutPubkey: params.payoutPubkey,
950
+ serverPubkey: xOnly(hex4.decode(info.signerPubkey), "ark signer key"),
951
+ emulatorPubkey: xOnly(emulatorPubkey, "emulator pubkey"),
952
+ claimDelay: unilateralClaimDelay(Number(info.unilateralExitDelay)),
953
+ hrp: getNetwork(info.network).hrp,
954
+ l1Network: l1NetworkFromArk(info.network),
955
+ refundAddress,
956
+ senderPubkey
957
+ });
958
+ assertFundable({
959
+ quote,
960
+ now: Math.floor(Date.now() / 1e3),
961
+ onchain: {
962
+ htlcLocktime: derived.htlcLocktime,
963
+ minConfirmations: derived.minConfirmations,
964
+ direction: "send"
965
+ }
966
+ });
967
+ await registerLockupContract(
968
+ await wallet.getContractManager(),
969
+ derived.script,
970
+ derived.address
971
+ );
972
+ return {
973
+ rfqId,
974
+ quote,
975
+ address: derived.address,
976
+ fundAmount: quote.from_amount,
977
+ swapPkScript: derived.swapPkScript,
978
+ script: derived.script,
979
+ refundAddress,
980
+ htlc: derived.htlc,
981
+ senderPubkey,
982
+ secrets
983
+ };
984
+ }
985
+ var assertQuotedAmount = (quote, amountSide, amount) => {
986
+ const quoted = amountSide === "from" ? quote.from_amount : quote.to_amount;
987
+ if (quoted !== amount) {
988
+ throw new Error(
989
+ `quote ${amountSide === "from" ? "from_amount" : "to_amount"} ${quoted} does not match the requested ${amount} \u2014 not this trade's quote`
990
+ );
991
+ }
992
+ if (quote.to_amount > quote.from_amount) {
993
+ throw new Error("quote pays out more than it takes in \u2014 not a quote to fund");
994
+ }
995
+ };
996
+ var MIN_CLAIM_WINDOW_SECONDS = 30 * 60;
997
+ var verifyReceiveInvoice = (input) => {
998
+ let decoded;
999
+ try {
1000
+ decoded = input.decode(input.invoice);
1001
+ } catch (error) {
1002
+ throw gateError(
1003
+ "invoice_undecodable",
1004
+ `solver sent an undecodable invoice: ${error instanceof Error ? error.message : String(error)}`
1005
+ );
1006
+ }
1007
+ assertFinite(decoded.expiresAt, "invoice_undecodable", "the decoded invoice expiry");
1008
+ assertFinite(input.quote.valid_until, "quote_malformed", "quote valid_until");
1009
+ if (decoded.paymentHash !== input.paymentHash) {
1010
+ throw gateError(
1011
+ "invoice_hash_mismatch",
1012
+ `solver's invoice pays ${decoded.paymentHash}, not this swap's ${input.paymentHash}`
1013
+ );
1014
+ }
1015
+ if (decoded.amountSats <= 0) {
1016
+ throw gateError("invoice_amount_mismatch", "solver's invoice names no amount");
1017
+ }
1018
+ if (decoded.amountSats !== input.quote.from_amount) {
1019
+ throw gateError(
1020
+ "invoice_amount_mismatch",
1021
+ `solver's invoice asks for ${decoded.amountSats}, not the quoted from_amount ${input.quote.from_amount}`
1022
+ );
1023
+ }
1024
+ return { payDeadline: Math.min(decoded.expiresAt, input.quote.valid_until) };
1025
+ };
1026
+ var assertReceivable = (input) => {
1027
+ assertFinite(input.payDeadline, "quote_malformed", "payDeadline");
1028
+ assertFinite(input.now, "invalid_gate_input", "now");
1029
+ assertFinite(input.minClaimWindowSeconds, "invalid_gate_input", "minClaimWindowSeconds");
1030
+ assertFinite(input.maxPayAmount, "invalid_gate_input", "maxPayAmount");
1031
+ const minClaimWindow = input.minClaimWindowSeconds ?? MIN_CLAIM_WINDOW_SECONDS;
1032
+ if (input.now >= input.payDeadline) {
1033
+ throw gateError("quote_expired", "quote or invoice already expired \u2014 request a fresh one");
1034
+ }
1035
+ if (input.quote.refund_locktime === void 0) {
1036
+ throw gateError("missing_refund_locktime", "receive quote carries no refund_locktime");
1037
+ }
1038
+ assertFinite(input.quote.refund_locktime, "quote_malformed", "quote refund_locktime");
1039
+ if (input.quote.refund_locktime - input.payDeadline < minClaimWindow) {
1040
+ throw gateError(
1041
+ "claim_window_too_short",
1042
+ `a payment at the deadline would leave under ${minClaimWindow}s to claim before the solver's refund opens`
1043
+ );
1044
+ }
1045
+ if (input.maxPayAmount !== void 0 && input.quote.from_amount > input.maxPayAmount) {
1046
+ throw gateError(
1047
+ "price_too_high",
1048
+ `quote asks ${input.quote.from_amount} sats, above the ${input.maxPayAmount} ceiling`
1049
+ );
1050
+ }
1051
+ };
1052
+ function receiveVtxoScript(params) {
1053
+ const seconds = (value) => ({
1054
+ type: "seconds",
1055
+ value: BigInt(value)
1056
+ });
1057
+ return new VHTLC.ScriptV2({
1058
+ sender: params.solverPubkey,
1059
+ receiver: params.payoutPubkey,
1060
+ server: params.serverPubkey,
1061
+ preimageHash: ripemd1602(hex4.decode(params.paymentHash)),
1062
+ refundLocktime: BigInt(params.refundLocktime),
1063
+ unilateralClaimDelay: seconds(params.claimDelay),
1064
+ unilateralRefundDelay: seconds(unilateralRefundDelay(params.claimDelay)),
1065
+ unilateralRefundWithoutReceiverDelay: seconds(
1066
+ unilateralRefundWithoutReceiverDelay(params.claimDelay)
1067
+ ),
1068
+ nonInteractiveClaim: {
1069
+ receiverPkScript: params.payoutPkScript,
1070
+ emulatorPubkey: params.emulatorPubkey
1071
+ },
1072
+ nonInteractiveRefund: {
1073
+ senderPkScript: params.solverRefundPkScript,
1074
+ emulatorPubkey: params.emulatorPubkey
1075
+ }
1076
+ });
1077
+ }
1078
+ function deriveLightningReceive(input) {
1079
+ const { quote } = input;
1080
+ const profile = quote.profile ?? {};
1081
+ const refundLocktime = quote.refund_locktime;
1082
+ const invoice = profile.invoice;
1083
+ const solverRefundPkScriptHex = profile.solver_refund_pk_script;
1084
+ if (refundLocktime === void 0 || invoice === void 0 || solverRefundPkScriptHex === void 0) {
1085
+ throw new Error("lightning-receive quote is missing a binding field");
1086
+ }
1087
+ const script = receiveVtxoScript({
1088
+ solverPubkey: xOnly(hex4.decode(quote.solver_pubkey), "solver key"),
1089
+ refundLocktime,
1090
+ serverPubkey: input.serverPubkey,
1091
+ paymentHash: input.paymentHash,
1092
+ claimDelay: input.claimDelay,
1093
+ emulatorPubkey: input.emulatorPubkey,
1094
+ solverRefundPkScript: solverHex(solverRefundPkScriptHex, "profile.solver_refund_pk_script"),
1095
+ payoutPubkey: input.payoutPubkey,
1096
+ payoutPkScript: ArkAddress.decode(input.payoutAddress).pkScript
1097
+ });
1098
+ const address = script.address(input.hrp, input.serverPubkey).encode();
1099
+ verifyLockupAddress(quote, address);
1100
+ return { address, swapPkScript: script.pkScript, script, invoice, refundLocktime };
1101
+ }
1102
+ async function requestLightningReceive(wallet, arkServerUrl, emulatorPubkey, transport, params) {
1103
+ const rfqId = params.rfqId ?? newRfqId();
1104
+ const secrets = await deriveSwapSecrets(wallet) ?? randomSwapSecrets({ preimage: true });
1105
+ if (!secrets.derivable) {
1106
+ console.warn(
1107
+ "[swap] this swap's preimage and payout key are random and MUST be persisted before paying"
1108
+ );
1109
+ }
1110
+ const preimage = await preimageForRfqSecrets(wallet, secrets);
1111
+ const paymentHash = paymentHashOf(preimage);
1112
+ const payoutPubkey = await senderPubkeyForRfqSecrets(wallet, secrets);
1113
+ const [info, payoutAddress] = await Promise.all([
1114
+ new RestArkProvider(arkServerUrl).getInfo(),
1115
+ wallet.getAddress()
1116
+ ]);
1117
+ const claimPacket = await sealClaimPacket({
1118
+ preimage,
1119
+ covclaimdPubkey: params.covclaimdPubkey
1120
+ });
1121
+ const quote = await transport.requestQuote(
1122
+ lightningReceiveRequest({
1123
+ rfqId,
1124
+ paymentHash,
1125
+ payoutAddress,
1126
+ payoutPubkey,
1127
+ claimPacket: claimPacket.ciphertext,
1128
+ amount: params.amount,
1129
+ amountSide: params.amountSide
1130
+ })
1131
+ );
1132
+ assertQuotedAmount(quote, params.amountSide, params.amount);
1133
+ const derived = deriveLightningReceive({
1134
+ quote,
1135
+ paymentHash,
1136
+ payoutPubkey,
1137
+ payoutAddress,
1138
+ serverPubkey: xOnly(hex4.decode(info.signerPubkey), "ark signer key"),
1139
+ emulatorPubkey: xOnly(emulatorPubkey, "emulator pubkey"),
1140
+ claimDelay: unilateralClaimDelay(Number(info.unilateralExitDelay)),
1141
+ hrp: getNetwork(info.network).hrp
1142
+ });
1143
+ const now = Math.floor(Date.now() / 1e3);
1144
+ const { payDeadline } = verifyReceiveInvoice({
1145
+ invoice: derived.invoice,
1146
+ decode: params.decodeInvoice,
1147
+ paymentHash,
1148
+ quote
1149
+ });
1150
+ assertReceivable({ quote, payDeadline, now, maxPayAmount: params.maxPayAmount });
1151
+ await registerLockupContract(
1152
+ await wallet.getContractManager(),
1153
+ derived.script,
1154
+ derived.address
1155
+ );
1156
+ return {
1157
+ rfqId,
1158
+ quote,
1159
+ invoice: derived.invoice,
1160
+ payAmount: quote.from_amount,
1161
+ expectedAmount: quote.to_amount,
1162
+ invoiceExpiresAt: payDeadline,
1163
+ address: derived.address,
1164
+ swapPkScript: derived.swapPkScript,
1165
+ script: derived.script,
1166
+ payoutAddress,
1167
+ payoutPubkey,
1168
+ secrets
1169
+ };
1170
+ }
1171
+ function deriveOnchainReceive(input) {
1172
+ const { quote } = input;
1173
+ const profile = quote.profile ?? {};
1174
+ const refundLocktime = quote.refund_locktime;
1175
+ const claimPubkey = profile.claim_pubkey;
1176
+ const htlcLocktime = profile.htlc_locktime;
1177
+ const htlcAddress = profile.htlc_address;
1178
+ const minConfirmations = profile.min_confirmations;
1179
+ const solverRefundPkScriptHex = profile.solver_refund_pk_script;
1180
+ if (refundLocktime === void 0 || claimPubkey === void 0 || htlcLocktime === void 0 || minConfirmations === void 0 || solverRefundPkScriptHex === void 0) {
1181
+ throw new Error("onchain-receive quote is missing a binding field");
1182
+ }
1183
+ const script = receiveVtxoScript({
1184
+ solverPubkey: xOnly(hex4.decode(quote.solver_pubkey), "solver key"),
1185
+ refundLocktime,
1186
+ serverPubkey: input.serverPubkey,
1187
+ paymentHash: input.paymentHash,
1188
+ claimDelay: input.claimDelay,
1189
+ emulatorPubkey: input.emulatorPubkey,
1190
+ solverRefundPkScript: solverHex(solverRefundPkScriptHex, "profile.solver_refund_pk_script"),
1191
+ payoutPubkey: input.payoutPubkey,
1192
+ payoutPkScript: ArkAddress.decode(input.payoutAddress).pkScript
1193
+ });
1194
+ const address = script.address(input.hrp, input.serverPubkey).encode();
1195
+ verifyLockupAddress(quote, address);
1196
+ const htlc = onchainHtlcScript(
1197
+ {
1198
+ paymentHash: input.paymentHash,
1199
+ claimKey: xOnly(hex4.decode(claimPubkey), "solver L1 claim key"),
1200
+ refundKey: input.refundPubkey,
1201
+ refundLocktime: htlcLocktime
1202
+ },
1203
+ input.l1Network
1204
+ );
1205
+ if (htlc.address !== htlcAddress) throw new AddressMismatch(htlc.address, htlcAddress);
1206
+ return {
1207
+ address,
1208
+ swapPkScript: script.pkScript,
1209
+ script,
1210
+ htlc,
1211
+ refundLocktime,
1212
+ htlcLocktime,
1213
+ minConfirmations
1214
+ };
1215
+ }
1216
+ async function requestOnchainReceive(wallet, arkServerUrl, emulatorPubkey, transport, params) {
1217
+ const rfqId = params.rfqId ?? newRfqId();
1218
+ const secrets = await deriveSwapSecrets(wallet) ?? randomSwapSecrets({ preimage: true });
1219
+ if (!secrets.derivable) {
1220
+ console.warn(
1221
+ "[swap] this swap's preimage and payout key are random and MUST be persisted before funding"
1222
+ );
1223
+ }
1224
+ const preimage = await preimageForRfqSecrets(wallet, secrets);
1225
+ const paymentHash = paymentHashOf(preimage);
1226
+ const payoutPubkey = await senderPubkeyForRfqSecrets(wallet, secrets);
1227
+ const [info, payoutAddress] = await Promise.all([
1228
+ new RestArkProvider(arkServerUrl).getInfo(),
1229
+ wallet.getAddress()
1230
+ ]);
1231
+ const claimPacket = await sealClaimPacket({
1232
+ preimage,
1233
+ covclaimdPubkey: params.covclaimdPubkey
1234
+ });
1235
+ const quote = await transport.requestQuote(
1236
+ onchainReceiveRequest({
1237
+ rfqId,
1238
+ paymentHash,
1239
+ payoutAddress,
1240
+ payoutPubkey,
1241
+ refundPubkey: params.refundPubkey,
1242
+ claimPacket: claimPacket.ciphertext,
1243
+ amount: params.amount,
1244
+ amountSide: params.amountSide
1245
+ })
1246
+ );
1247
+ assertQuotedAmount(quote, params.amountSide, params.amount);
1248
+ const derived = deriveOnchainReceive({
1249
+ quote,
1250
+ paymentHash,
1251
+ payoutPubkey,
1252
+ payoutAddress,
1253
+ refundPubkey: params.refundPubkey,
1254
+ serverPubkey: xOnly(hex4.decode(info.signerPubkey), "ark signer key"),
1255
+ emulatorPubkey: xOnly(emulatorPubkey, "emulator pubkey"),
1256
+ claimDelay: unilateralClaimDelay(Number(info.unilateralExitDelay)),
1257
+ hrp: getNetwork(info.network).hrp,
1258
+ l1Network: l1NetworkFromArk(info.network)
1259
+ });
1260
+ assertFundable({
1261
+ quote,
1262
+ now: Math.floor(Date.now() / 1e3),
1263
+ onchain: {
1264
+ htlcLocktime: derived.htlcLocktime,
1265
+ minConfirmations: derived.minConfirmations,
1266
+ direction: "receive"
1267
+ }
1268
+ });
1269
+ await registerLockupContract(
1270
+ await wallet.getContractManager(),
1271
+ derived.script,
1272
+ derived.address
1273
+ );
1274
+ return {
1275
+ rfqId,
1276
+ quote,
1277
+ address: derived.address,
1278
+ fundAmount: quote.from_amount,
1279
+ expectedAmount: quote.to_amount,
1280
+ swapPkScript: derived.swapPkScript,
1281
+ script: derived.script,
1282
+ htlc: derived.htlc,
1283
+ payoutAddress,
1284
+ payoutPubkey,
1285
+ secrets
1286
+ };
1287
+ }
1288
+
1289
+ export {
1290
+ ONCHAIN_ORDER_MARGIN_SECONDS,
1291
+ ONCHAIN_CLAIM_MARGIN_SECONDS,
1292
+ MAX_MIN_CONFIRMATIONS,
1293
+ ONCHAIN_SECONDS_PER_BLOCK,
1294
+ ONCHAIN_DUST_SATS,
1295
+ newPreimage,
1296
+ paymentHashOf,
1297
+ onchainHtlcScript,
1298
+ buildHtlcClaim,
1299
+ buildHtlcRefund,
1300
+ extractPreimage,
1301
+ awaitOnchainFill,
1302
+ claimOnchainFill,
1303
+ classifyOnchainHtlc,
1304
+ RFQ_PREIMAGE_TAG,
1305
+ buildPreimageMessage,
1306
+ deriveSwapSecrets,
1307
+ randomSwapSecrets,
1308
+ rfqSecretsToRecord,
1309
+ rfqSecretsOfRecord,
1310
+ adoptSwapDescriptor,
1311
+ RefundNotLocallyPossibleError,
1312
+ senderIdentityForRfqSecrets,
1313
+ senderIdentityForSwapRecord,
1314
+ senderPubkeyForRfqSecrets,
1315
+ isDeterministicSigner,
1316
+ derivePreimage,
1317
+ preimageForRfqSecrets,
1318
+ sealClaimPacket,
1319
+ SWAP_LOCKUP_CONTRACT_TYPE,
1320
+ SWAP_LOCKUP_CONTRACT_LABEL,
1321
+ SWAP_LOCKUP_CONTRACT_KIND,
1322
+ LockupRegistrationFailed,
1323
+ registerLockupContract,
1324
+ ARKADE_BTC,
1325
+ ARKADE_ASSET,
1326
+ LIGHTNING_BTC,
1327
+ ONCHAIN_BTC,
1328
+ rfqPair,
1329
+ LIGHTNING_SEND_PAIR,
1330
+ LIGHTNING_RECEIVE_PAIR,
1331
+ ONCHAIN_SEND_PAIR,
1332
+ ONCHAIN_RECEIVE_PAIR,
1333
+ RFQ_TERMINAL_STATES,
1334
+ SwapRefusal,
1335
+ AddressMismatch,
1336
+ newRfqId,
1337
+ lightningSendRequest,
1338
+ arkadeSwapRequest,
1339
+ MIN_HEADROOM_SECONDS,
1340
+ verifyLockupAddress,
1341
+ assertFundable,
1342
+ httpTransport,
1343
+ relayTransport,
1344
+ unilateralClaimDelay,
1345
+ unilateralRefundDelay,
1346
+ unilateralRefundWithoutReceiverDelay,
1347
+ lightningSendVtxoScript,
1348
+ requestLightningSend,
1349
+ offerTermsFromQuote,
1350
+ onchainSendRequest,
1351
+ lightningReceiveRequest,
1352
+ onchainReceiveRequest,
1353
+ deriveOnchainSend,
1354
+ requestOnchainSend,
1355
+ MIN_CLAIM_WINDOW_SECONDS,
1356
+ verifyReceiveInvoice,
1357
+ assertReceivable,
1358
+ receiveVtxoScript,
1359
+ deriveLightningReceive,
1360
+ requestLightningReceive,
1361
+ deriveOnchainReceive,
1362
+ requestOnchainReceive
1363
+ };