@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.
- package/dist/advanced.cjs +1844 -1670
- package/dist/advanced.d.cts +7 -7
- package/dist/advanced.d.ts +7 -7
- package/dist/advanced.js +3 -3
- package/dist/{chunk-WXV23FAY.js → chunk-7DKKTLPB.js} +170 -901
- package/dist/{chunk-DSRMQCKG.js → chunk-EYSUBQFB.js} +1087 -85
- package/dist/{chunk-4MTMMYYC.js → chunk-RSUVIWT5.js} +18 -13
- package/dist/{errors-B75peUQo.d.cts → errors-Bg1bbSUK.d.cts} +4 -2
- package/dist/{errors-D7YNbfwK.d.ts → errors-Do2Y18da.d.ts} +4 -2
- package/dist/index.cjs +2606 -2432
- package/dist/index.d.cts +6 -6
- package/dist/index.d.ts +6 -6
- package/dist/index.js +7 -7
- package/dist/{lockupContract-C7dM1xxn.d.ts → lockupContract-BL2D3js9.d.ts} +1 -1
- package/dist/{lockupContract-BwdgOqqe.d.cts → lockupContract-rWaIvZ11.d.cts} +1 -1
- package/dist/node/index.d.cts +3 -0
- package/dist/node/index.d.ts +3 -0
- package/dist/nostr.cjs +109 -3
- package/dist/nostr.d.cts +1 -1
- package/dist/nostr.d.ts +1 -1
- package/dist/nostr.js +1 -1
- package/dist/protocol.cjs +3626 -3365
- package/dist/protocol.d.cts +18 -15
- package/dist/protocol.d.ts +18 -15
- package/dist/protocol.js +32 -20
- package/dist/repositories/realm/index.d.cts +2 -2
- package/dist/repositories/realm/index.d.ts +2 -2
- package/dist/repositories/sqlite/index.d.cts +2 -2
- package/dist/repositories/sqlite/index.d.ts +2 -2
- package/dist/{repository-2bubkNM6.d.ts → repository-By-tSjDD.d.ts} +6 -24
- package/dist/{repository-Bi0iq1fK.d.cts → repository-DIZnasB7.d.cts} +6 -24
- package/dist/{rfq-o1ncPWXH.d.cts → rfq-CoGJB3_s.d.cts} +128 -12
- package/dist/{rfq-o1ncPWXH.d.ts → rfq-CoGJB3_s.d.ts} +128 -12
- package/dist/{watch-DG6v5aOM.d.ts → watch-Coy0_YHu.d.ts} +1 -1
- package/dist/{watch-CGRIFtBG.d.cts → watch-DQaFg3Hd.d.cts} +1 -1
- 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/
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
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
|
-
|
|
54
|
-
const
|
|
55
|
-
|
|
56
|
-
if (
|
|
57
|
-
|
|
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
|
-
|
|
60
|
-
|
|
61
|
-
|
|
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
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
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
|
-
|
|
78
|
-
this.address = address;
|
|
79
|
-
this.script = script;
|
|
230
|
+
return { swaps, persisted: false };
|
|
80
231
|
}
|
|
81
232
|
};
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
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/
|
|
100
|
-
function
|
|
101
|
-
if (
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
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
|
|
161
|
-
|
|
162
|
-
|
|
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
|
-
|
|
168
|
-
|
|
169
|
-
|
|
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
|
-
|
|
175
|
-
const
|
|
176
|
-
|
|
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
|
-
|
|
212
|
-
|
|
213
|
-
|
|
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
|
-
|
|
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
|
-
|
|
308
|
-
if (
|
|
309
|
-
|
|
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
|
-
|
|
332
|
-
|
|
333
|
-
|
|
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
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
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
|
|
379
|
-
|
|
380
|
-
|
|
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
|
-
|
|
383
|
-
|
|
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 (
|
|
386
|
-
throw new Error("
|
|
398
|
+
if (exit.value <= BigInt(0)) {
|
|
399
|
+
throw new Error("exitDelay must be a positive relative locktime");
|
|
387
400
|
}
|
|
388
|
-
|
|
389
|
-
|
|
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
|
-
|
|
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
|
|
422
|
-
const
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
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
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
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
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
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
|
|
457
|
-
|
|
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
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
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
|
|
471
|
-
const
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
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
|
|
495
|
-
const {
|
|
496
|
-
const
|
|
497
|
-
|
|
498
|
-
|
|
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
|
-
|
|
522
|
-
|
|
523
|
-
|
|
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
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
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
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
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
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
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
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
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
|
-
|
|
666
|
-
|
|
667
|
-
|
|
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
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
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
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
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/
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
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
|
-
|
|
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/
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
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
|
-
|
|
1212
|
-
|
|
1213
|
-
|
|
1214
|
-
|
|
1215
|
-
|
|
1216
|
-
|
|
1217
|
-
|
|
1218
|
-
|
|
1219
|
-
|
|
1220
|
-
|
|
1221
|
-
|
|
1222
|
-
|
|
1223
|
-
|
|
1224
|
-
|
|
1225
|
-
|
|
1226
|
-
|
|
1227
|
-
|
|
1228
|
-
|
|
1229
|
-
|
|
1230
|
-
|
|
1231
|
-
|
|
1232
|
-
|
|
1233
|
-
|
|
1234
|
-
|
|
1235
|
-
|
|
1236
|
-
|
|
1237
|
-
|
|
1238
|
-
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
|
|
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/
|
|
1247
|
-
|
|
1248
|
-
|
|
1249
|
-
|
|
1250
|
-
|
|
1251
|
-
|
|
1252
|
-
|
|
1253
|
-
|
|
1254
|
-
|
|
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
|
|
1267
|
-
const
|
|
1268
|
-
if (
|
|
1269
|
-
|
|
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
|
-
|
|
1364
|
-
|
|
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
|
-
|
|
1374
|
-
|
|
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
|
-
|
|
1386
|
-
|
|
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
|
-
|
|
1396
|
-
|
|
1397
|
-
|
|
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
|
-
|
|
1402
|
-
|
|
1403
|
-
|
|
1404
|
-
|
|
1405
|
-
|
|
1406
|
-
|
|
1407
|
-
|
|
1408
|
-
|
|
1409
|
-
|
|
1410
|
-
|
|
1411
|
-
|
|
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
|
-
|
|
1425
|
-
|
|
1426
|
-
|
|
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
|
-
|
|
1430
|
-
|
|
1431
|
-
|
|
1432
|
-
|
|
1433
|
-
|
|
1434
|
-
|
|
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
|
-
|
|
1446
|
-
serverKey: operatorPubkey,
|
|
1447
|
-
userKey: offer.makerPublicKey,
|
|
1448
|
-
emulatorKey: offer.emulatorPubkey
|
|
1449
|
-
}
|
|
1054
|
+
refundLocktime: quote.refund_locktime
|
|
1450
1055
|
};
|
|
1451
1056
|
}
|
|
1452
|
-
function
|
|
1453
|
-
const {
|
|
1454
|
-
|
|
1455
|
-
|
|
1456
|
-
|
|
1457
|
-
|
|
1458
|
-
|
|
1459
|
-
|
|
1460
|
-
|
|
1461
|
-
|
|
1462
|
-
|
|
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
|
|
1476
|
-
|
|
1477
|
-
|
|
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
|
-
|
|
1480
|
-
|
|
1481
|
-
|
|
1482
|
-
|
|
1483
|
-
|
|
1484
|
-
|
|
1485
|
-
|
|
1486
|
-
|
|
1487
|
-
|
|
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
|
|
1490
|
-
|
|
1491
|
-
|
|
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
|
-
|
|
1494
|
-
"
|
|
1495
|
-
|
|
1496
|
-
|
|
1497
|
-
|
|
1498
|
-
|
|
1499
|
-
|
|
1500
|
-
|
|
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
|
-
|
|
1644
|
-
|
|
1645
|
-
|
|
1646
|
-
|
|
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
|
-
|
|
1687
|
-
|
|
1688
|
-
|
|
1689
|
-
|
|
1690
|
-
|
|
1691
|
-
|
|
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
|
|
1695
|
-
var
|
|
1696
|
-
|
|
1697
|
-
|
|
1698
|
-
|
|
1699
|
-
|
|
1700
|
-
|
|
1701
|
-
|
|
1702
|
-
|
|
1703
|
-
|
|
1704
|
-
|
|
1705
|
-
|
|
1706
|
-
);
|
|
1707
|
-
|
|
1708
|
-
|
|
1709
|
-
|
|
1710
|
-
|
|
1711
|
-
}
|
|
1712
|
-
|
|
1713
|
-
|
|
1714
|
-
|
|
1715
|
-
|
|
1716
|
-
|
|
1717
|
-
|
|
1718
|
-
|
|
1719
|
-
|
|
1720
|
-
|
|
1721
|
-
|
|
1722
|
-
|
|
1723
|
-
|
|
1724
|
-
|
|
1725
|
-
|
|
1726
|
-
|
|
1727
|
-
|
|
1728
|
-
|
|
1729
|
-
|
|
1730
|
-
|
|
1731
|
-
|
|
1732
|
-
|
|
1733
|
-
|
|
1734
|
-
|
|
1735
|
-
|
|
1736
|
-
|
|
1737
|
-
|
|
1738
|
-
|
|
1739
|
-
|
|
1740
|
-
|
|
1741
|
-
|
|
1742
|
-
|
|
1743
|
-
|
|
1744
|
-
|
|
1745
|
-
|
|
1746
|
-
|
|
1747
|
-
|
|
1748
|
-
|
|
1749
|
-
|
|
1750
|
-
"
|
|
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
|
-
|
|
1754
|
-
|
|
1755
|
-
|
|
1756
|
-
|
|
1757
|
-
|
|
1758
|
-
|
|
1759
|
-
|
|
1760
|
-
|
|
1761
|
-
|
|
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
|
-
|
|
1765
|
-
|
|
1766
|
-
|
|
1767
|
-
|
|
1768
|
-
|
|
1769
|
-
|
|
1770
|
-
|
|
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
|
|
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 =
|
|
2377
|
-
var isIndexedNetwork = (network) => (0,
|
|
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
|
|
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 >
|
|
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, ${
|
|
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,
|
|
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,
|
|
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,
|
|
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" &&
|
|
2772
|
+
var isAtomicDecimal = (value) => typeof value === "string" && import_solver_discovery3.AMOUNT_PATTERN.test(value);
|
|
2708
2773
|
|
|
2709
2774
|
// src/client/rfqAmount.ts
|
|
2710
|
-
var
|
|
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,
|
|
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
|
|
3756
|
+
var import_solver_discovery6 = require("@arkade-os/solver-discovery");
|
|
3688
3757
|
|
|
3689
3758
|
// src/markets.ts
|
|
3690
|
-
var
|
|
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 <=
|
|
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,
|
|
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,
|
|
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
|
|
6500
|
+
var import_solver_discovery8 = require("@arkade-os/solver-discovery");
|
|
6332
6501
|
|
|
6333
6502
|
// src/client/market.ts
|
|
6334
|
-
var
|
|
6503
|
+
var import_solver_discovery7 = require("@arkade-os/solver-discovery");
|
|
6335
6504
|
var marketKeyOf = (card) => {
|
|
6336
|
-
const base = (0,
|
|
6337
|
-
const quote = (0,
|
|
6338
|
-
const baseIsArkade = (0,
|
|
6339
|
-
const quoteIsArkade = (0,
|
|
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,
|
|
6346
|
-
var marketBackendOf = (card) => (0,
|
|
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,
|
|
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,
|
|
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,
|
|
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,
|
|
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,
|