@arkade-os/swap 0.1.0-rc.11 → 0.1.0-rc.13
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +3 -0
- package/dist/advanced.cjs +2057 -1801
- package/dist/advanced.d.cts +28 -10
- package/dist/advanced.d.ts +28 -10
- package/dist/advanced.js +9 -5
- package/dist/{chunk-DSRMQCKG.js → chunk-KRXPQQJ7.js} +1110 -88
- package/dist/{chunk-4MTMMYYC.js → chunk-SC3SAY3X.js} +147 -73
- package/dist/{chunk-WXV23FAY.js → chunk-ZDFMD4FA.js} +215 -937
- package/dist/{errors-D7YNbfwK.d.ts → errors-LXMcNhNS.d.ts} +7 -3
- package/dist/{errors-B75peUQo.d.cts → errors-XUs8r0LT.d.cts} +7 -3
- package/dist/index.cjs +2810 -2558
- package/dist/index.d.cts +6 -6
- package/dist/index.d.ts +6 -6
- package/dist/index.js +8 -8
- package/dist/{lockupContract-C7dM1xxn.d.ts → lockupContract-BhMebZCb.d.cts} +7 -10
- package/dist/{lockupContract-BwdgOqqe.d.cts → lockupContract-CAiN97fX.d.ts} +7 -10
- package/dist/node/index.d.cts +11 -2
- package/dist/node/index.d.ts +11 -2
- 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 +3665 -3375
- package/dist/protocol.d.cts +30 -20
- package/dist/protocol.d.ts +30 -20
- 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-Bi0iq1fK.d.cts → repository-BJLuVFQ0.d.ts} +72 -72
- package/dist/{repository-2bubkNM6.d.ts → repository-DsR3b0JJ.d.cts} +72 -72
- package/dist/{rfq-o1ncPWXH.d.cts → rfq-BWE8qkV2.d.cts} +133 -12
- package/dist/{rfq-o1ncPWXH.d.ts → rfq-BWE8qkV2.d.ts} +133 -12
- package/dist/{watch-DG6v5aOM.d.ts → watch-C4ReJR5z.d.cts} +1 -1
- package/dist/{watch-CGRIFtBG.d.cts → watch-D8Z78RAV.d.ts} +1 -1
- package/package.json +3 -3
package/dist/advanced.cjs
CHANGED
|
@@ -35,1741 +35,1795 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
|
|
|
35
35
|
));
|
|
36
36
|
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
37
37
|
|
|
38
|
-
// src/
|
|
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
|
+
fateMoved: () => fateMoved,
|
|
1743
|
+
feedFetch: () => feedFetch,
|
|
1744
|
+
formatAssetId: () => formatAssetId,
|
|
1745
|
+
fromAtomicDecimal: () => fromAtomicDecimal,
|
|
1746
|
+
fromRfqAmountSide: () => fromRfqAmountSide,
|
|
1747
|
+
fundsFromWallet: () => fundsFromWallet,
|
|
1748
|
+
instrumentOf: () => instrumentOf,
|
|
1749
|
+
isAddressable: () => isAddressable,
|
|
1750
|
+
isAssetId: () => isAssetId,
|
|
1751
|
+
isAtomicDecimal: () => isAtomicDecimal,
|
|
1752
|
+
isCardMarket: () => isCardMarket,
|
|
1753
|
+
isIndexedNetwork: () => isIndexedNetwork,
|
|
1754
|
+
isNetworkRef: () => isNetworkRef,
|
|
1755
|
+
isSwapError: () => isSwapError,
|
|
1756
|
+
isUsableCard: () => isUsableCard,
|
|
1757
|
+
issuanceOf: () => issuanceOf,
|
|
1758
|
+
legOf: () => legOf,
|
|
1759
|
+
lightningCorridor: () => lightningCorridor,
|
|
1760
|
+
liveArkadeInfo: () => liveArkadeInfo,
|
|
1761
|
+
marketBackendOf: () => marketBackendOf,
|
|
1762
|
+
marketKeyOf: () => marketKeyOf,
|
|
1763
|
+
marketRefOf: () => marketRefOf,
|
|
1764
|
+
networksOfInvoiceHrp: () => networksOfInvoiceHrp,
|
|
1765
|
+
nostrTransportFactory: () => nostrTransportFactory,
|
|
1766
|
+
offerFactsOf: () => offerFactsOf,
|
|
1767
|
+
offerOutcome: () => offerOutcome,
|
|
1768
|
+
offerRecordSource: () => offerRecordSource,
|
|
1769
|
+
onchainCorridor: () => onchainCorridor,
|
|
1770
|
+
parseAssetId: () => parseAssetId,
|
|
1771
|
+
parseRfqQuote: () => parseRfqQuote,
|
|
1772
|
+
pay: () => pay,
|
|
1773
|
+
publicAssetId: () => publicAssetId,
|
|
1774
|
+
quoteFromFeed: () => quoteFromFeed,
|
|
1775
|
+
quoteIdOfSwapId: () => quoteIdOfSwapId,
|
|
1776
|
+
quoteViaRfq: () => quoteViaRfq,
|
|
1777
|
+
railOf: () => railOf,
|
|
1778
|
+
railOfCorridor: () => railOfCorridor,
|
|
1779
|
+
readableRecord: () => readableRecord,
|
|
1780
|
+
readsChain: () => readsChain,
|
|
1781
|
+
receive: () => receive,
|
|
1782
|
+
recordArtifact: () => recordArtifact,
|
|
1783
|
+
recordEndpoint: () => recordEndpoint,
|
|
1784
|
+
recordInstrument: () => recordInstrument,
|
|
1785
|
+
recordLeg: () => recordLeg,
|
|
1786
|
+
recordOutcome: () => recordOutcome,
|
|
1787
|
+
resolveCorridorBase: () => resolveCorridorBase,
|
|
1788
|
+
resolveCorridorDeps: () => resolveCorridorDeps,
|
|
1789
|
+
resolveRoute: () => resolveRoute,
|
|
1790
|
+
restoredOfferRecord: () => restoredOfferRecord,
|
|
1791
|
+
revealClaimPacket: () => revealClaimPacket,
|
|
1792
|
+
revealFieldsFromScript: () => revealFieldsFromScript,
|
|
1793
|
+
rfqLeg: () => rfqLeg,
|
|
1794
|
+
rfqPairFor: () => rfqPairFor,
|
|
1795
|
+
rfqRecordOf: () => rfqRecordOf,
|
|
1796
|
+
sameAsset: () => sameAsset,
|
|
1797
|
+
satsOf: () => satsOf,
|
|
1798
|
+
scopedToRail: () => scopedToRail,
|
|
1799
|
+
splitRecords: () => splitRecords,
|
|
1800
|
+
swapOf: () => swapOf,
|
|
1801
|
+
swapRecordOf: () => swapRecordOf,
|
|
1802
|
+
toAtomicDecimal: () => toAtomicDecimal,
|
|
1803
|
+
toDiscoveryLeg: () => toDiscoveryLeg,
|
|
1804
|
+
toRfqAmountSide: () => toRfqAmountSide,
|
|
1805
|
+
toSafeNumber: () => toSafeNumber,
|
|
1806
|
+
usableMarkets: () => usableMarkets,
|
|
1807
|
+
verifyPair: () => verifyPair,
|
|
1808
|
+
verifyQuoteTtl: () => verifyQuoteTtl,
|
|
1809
|
+
verifyQuotedAmount: () => verifyQuotedAmount,
|
|
1810
|
+
verifyReceiveInvoiceFacts: () => verifyReceiveInvoiceFacts,
|
|
1811
|
+
verifyReceiveWindow: () => verifyReceiveWindow,
|
|
1812
|
+
verifyResponder: () => verifyResponder,
|
|
1813
|
+
verifySendInvoice: () => verifySendInvoice,
|
|
1814
|
+
verifySendWindow: () => verifySendWindow,
|
|
1815
|
+
verifyingDerivation: () => verifyingDerivation,
|
|
1816
|
+
walletLockupIndexer: () => walletLockupIndexer,
|
|
1817
|
+
withCanonicalAmount: () => withCanonicalAmount,
|
|
1818
|
+
withDepositFate: () => withDepositFate,
|
|
1819
|
+
withRfqState: () => withRfqState
|
|
1820
|
+
});
|
|
1821
|
+
module.exports = __toCommonJS(advanced_exports);
|
|
1771
1822
|
|
|
1772
1823
|
// src/client/accept.ts
|
|
1824
|
+
var import_base8 = require("@scure/base");
|
|
1825
|
+
var import_sdk9 = require("@arkade-os/sdk");
|
|
1826
|
+
init_offer();
|
|
1773
1827
|
init_lockupContract();
|
|
1774
1828
|
|
|
1775
1829
|
// src/rfqCorridors.ts
|
|
@@ -1823,6 +1877,7 @@ var RfqCorridorRegistry = class {
|
|
|
1823
1877
|
var rfqCorridorHandlers = new RfqCorridorRegistry();
|
|
1824
1878
|
|
|
1825
1879
|
// src/rfqProfileParts.ts
|
|
1880
|
+
init_store();
|
|
1826
1881
|
var rfqSecretsProfile = (secrets, paymentHash) => {
|
|
1827
1882
|
const { signingDescriptor, ...preimage } = swapSecretsToRecord(secrets);
|
|
1828
1883
|
return {
|
|
@@ -1952,6 +2007,7 @@ function onchainSendProfile(result) {
|
|
|
1952
2007
|
network: result.l1Network,
|
|
1953
2008
|
htlcAddress: result.htlc.address,
|
|
1954
2009
|
minConfirmations: result.minConfirmations,
|
|
2010
|
+
expectedAmount: result.expectedAmount,
|
|
1955
2011
|
payoutPkScript: import_base5.hex.encode(result.payoutPkScript)
|
|
1956
2012
|
};
|
|
1957
2013
|
}
|
|
@@ -1963,6 +2019,7 @@ var OnchainSendCorridor = {
|
|
|
1963
2019
|
project: (swap) => {
|
|
1964
2020
|
const send = swap;
|
|
1965
2021
|
return {
|
|
2022
|
+
expectedAmount: send.expectedAmount,
|
|
1966
2023
|
...send.funding ? { funding: send.funding } : {},
|
|
1967
2024
|
...send.claimTxid ? { claimTxid: send.claimTxid } : {}
|
|
1968
2025
|
};
|
|
@@ -1979,6 +2036,11 @@ var OnchainSendCorridor = {
|
|
|
1979
2036
|
`onchain_send record carries no usable minConfirmations (${String(profile.minConfirmations)}); the confirmation gate cannot be checked \u2014 refusing to restore a swap that would claim an unconfirmed fill`
|
|
1980
2037
|
);
|
|
1981
2038
|
}
|
|
2039
|
+
if (!Number.isSafeInteger(profile.expectedAmount) || profile.expectedAmount <= 0) {
|
|
2040
|
+
throw new Error(
|
|
2041
|
+
`onchain_send record carries no usable expectedAmount (${String(profile.expectedAmount)}); the funded value cannot be checked \u2014 refusing to restore a swap that would claim an underfunded fill`
|
|
2042
|
+
);
|
|
2043
|
+
}
|
|
1982
2044
|
const htlc = onchainHtlcScript(
|
|
1983
2045
|
{
|
|
1984
2046
|
// From the profile, not the covenant: the lockup commits to
|
|
@@ -2001,6 +2063,7 @@ var OnchainSendCorridor = {
|
|
|
2001
2063
|
paymentHash,
|
|
2002
2064
|
htlc,
|
|
2003
2065
|
minConfirmations: profile.minConfirmations,
|
|
2066
|
+
expectedAmount: profile.expectedAmount,
|
|
2004
2067
|
// Optional here, required at the write: throwing on an older
|
|
2005
2068
|
// record would strand the refund it is still owed.
|
|
2006
2069
|
...profile.payoutPkScript ? { payoutPkScript: import_base5.hex.decode(profile.payoutPkScript) } : {},
|
|
@@ -2020,6 +2083,9 @@ rfqCorridorHandlers.register(LightningSendCorridor);
|
|
|
2020
2083
|
rfqCorridorHandlers.register(LightningReceiveCorridor);
|
|
2021
2084
|
rfqCorridorHandlers.register(OnchainSendCorridor);
|
|
2022
2085
|
|
|
2086
|
+
// src/client/accept.ts
|
|
2087
|
+
init_store();
|
|
2088
|
+
|
|
2023
2089
|
// src/client/assetId.ts
|
|
2024
2090
|
var import_sdk4 = require("@arkade-os/sdk");
|
|
2025
2091
|
var RAILS = ["arkade", "bitcoin", "bolt11", "eip155"];
|
|
@@ -2168,7 +2234,8 @@ var issuanceOf = (id) => {
|
|
|
2168
2234
|
};
|
|
2169
2235
|
|
|
2170
2236
|
// src/client/aliases.ts
|
|
2171
|
-
var
|
|
2237
|
+
var import_solver_discovery2 = require("@arkade-os/solver-discovery");
|
|
2238
|
+
init_store();
|
|
2172
2239
|
|
|
2173
2240
|
// src/client/errors.ts
|
|
2174
2241
|
init_rfq();
|
|
@@ -2241,10 +2308,10 @@ var QuoteExpired = class extends Error {
|
|
|
2241
2308
|
name = "QuoteExpired";
|
|
2242
2309
|
};
|
|
2243
2310
|
var MaxFeeExceeded = class extends Error {
|
|
2244
|
-
constructor(quoteId,
|
|
2245
|
-
super(`fee ${fee} of ${
|
|
2311
|
+
constructor(quoteId, asset6, fee, maxFee) {
|
|
2312
|
+
super(`fee ${fee} of ${asset6} exceeds the ${maxFee} ceiling`);
|
|
2246
2313
|
this.quoteId = quoteId;
|
|
2247
|
-
this.asset =
|
|
2314
|
+
this.asset = asset6;
|
|
2248
2315
|
this.fee = fee;
|
|
2249
2316
|
this.maxFee = maxFee;
|
|
2250
2317
|
}
|
|
@@ -2255,11 +2322,11 @@ var MaxFeeExceeded = class extends Error {
|
|
|
2255
2322
|
name = "MaxFeeExceeded";
|
|
2256
2323
|
};
|
|
2257
2324
|
var InsufficientFunds = class extends Error {
|
|
2258
|
-
constructor(
|
|
2325
|
+
constructor(asset6, required, available) {
|
|
2259
2326
|
super(
|
|
2260
|
-
`funding needs ${required} of ${
|
|
2327
|
+
`funding needs ${required} of ${asset6}` + (available === void 0 ? "" : `, wallet holds ${available}`)
|
|
2261
2328
|
);
|
|
2262
|
-
this.asset =
|
|
2329
|
+
this.asset = asset6;
|
|
2263
2330
|
this.required = required;
|
|
2264
2331
|
this.available = available;
|
|
2265
2332
|
}
|
|
@@ -2297,9 +2364,9 @@ var NotCancellable = class extends Error {
|
|
|
2297
2364
|
name = "NotCancellable";
|
|
2298
2365
|
};
|
|
2299
2366
|
var InconsistentRoute = class extends Error {
|
|
2300
|
-
constructor(
|
|
2301
|
-
super(`destination ${JSON.stringify(destination)} is not on ${
|
|
2302
|
-
this.asset =
|
|
2367
|
+
constructor(asset6, destination) {
|
|
2368
|
+
super(`destination ${JSON.stringify(destination)} is not on ${asset6}'s chain`);
|
|
2369
|
+
this.asset = asset6;
|
|
2303
2370
|
this.destination = destination;
|
|
2304
2371
|
}
|
|
2305
2372
|
asset;
|
|
@@ -2352,29 +2419,29 @@ function isSwapError(e, name) {
|
|
|
2352
2419
|
// src/client/aliases.ts
|
|
2353
2420
|
var toDiscoveryLeg = (id) => {
|
|
2354
2421
|
const { rail, assetNamespace, assetReference } = parseAssetId(id);
|
|
2355
|
-
const
|
|
2422
|
+
const asset6 = `${assetNamespace}:${assetReference}`;
|
|
2356
2423
|
if (rail === "arkade") {
|
|
2357
|
-
if (
|
|
2424
|
+
if (asset6 === BTC_ASSET_PART)
|
|
2358
2425
|
return { corridor: "arkade", assetId: BTC_ASSET_ID, marketId: id };
|
|
2359
2426
|
if (assetNamespace === ARKADE_ASSET_NAMESPACE) {
|
|
2360
2427
|
return { corridor: "arkade", assetId: assetReference, marketId: id };
|
|
2361
2428
|
}
|
|
2362
|
-
throw new UnsupportedRoute(`the arkade corridor has no ${
|
|
2429
|
+
throw new UnsupportedRoute(`the arkade corridor has no ${asset6}`);
|
|
2363
2430
|
}
|
|
2364
2431
|
if (rail === "bolt11") {
|
|
2365
|
-
if (
|
|
2432
|
+
if (asset6 === BTC_ASSET_PART)
|
|
2366
2433
|
return { corridor: "lightning", assetId: BTC_ASSET_ID, marketId: id };
|
|
2367
|
-
throw new UnsupportedRoute(`the lightning corridor carries BTC only, not ${
|
|
2434
|
+
throw new UnsupportedRoute(`the lightning corridor carries BTC only, not ${asset6}`);
|
|
2368
2435
|
}
|
|
2369
2436
|
if (rail === "bitcoin") {
|
|
2370
|
-
if (
|
|
2437
|
+
if (asset6 === BTC_ASSET_PART)
|
|
2371
2438
|
return { corridor: "onchain", assetId: BTC_ASSET_ID, marketId: id };
|
|
2372
|
-
throw new UnsupportedRoute(`the onchain corridor carries BTC only, not ${
|
|
2439
|
+
throw new UnsupportedRoute(`the onchain corridor carries BTC only, not ${asset6}`);
|
|
2373
2440
|
}
|
|
2374
2441
|
throw new UnsupportedRoute(`no corridor serves ${id}`);
|
|
2375
2442
|
};
|
|
2376
|
-
var INDEXED_NETWORKS =
|
|
2377
|
-
var isIndexedNetwork = (network) => (0,
|
|
2443
|
+
var INDEXED_NETWORKS = import_solver_discovery2.NETWORKS;
|
|
2444
|
+
var isIndexedNetwork = (network) => (0, import_solver_discovery2.isNetwork)(network);
|
|
2378
2445
|
var networkOfRow = (row) => parseAssetId(row.id).reference;
|
|
2379
2446
|
var canonicalAssetId = (input, table) => {
|
|
2380
2447
|
if (isAssetId(input)) return input;
|
|
@@ -2606,7 +2673,7 @@ var ACTIVITY_TOKEN = {
|
|
|
2606
2673
|
};
|
|
2607
2674
|
|
|
2608
2675
|
// src/client/amount.ts
|
|
2609
|
-
var
|
|
2676
|
+
var import_solver_discovery3 = require("@arkade-os/solver-discovery");
|
|
2610
2677
|
var AmountFormatError = class extends Error {
|
|
2611
2678
|
constructor(reason, message) {
|
|
2612
2679
|
super(message);
|
|
@@ -2619,10 +2686,10 @@ var SCALED_DECIMAL = /^([0-9]+)(?:\.([0-9]+))?$/;
|
|
|
2619
2686
|
var ATOMIC_CEILING = 10n ** 30n;
|
|
2620
2687
|
var MAX_SIGNIFICANT_DIGITS = 64;
|
|
2621
2688
|
var assertDecimals = (decimals) => {
|
|
2622
|
-
if (!Number.isInteger(decimals) || decimals < 0 || decimals >
|
|
2689
|
+
if (!Number.isInteger(decimals) || decimals < 0 || decimals > import_solver_discovery3.MAX_ASSET_DECIMALS) {
|
|
2623
2690
|
throw new AmountFormatError(
|
|
2624
2691
|
"invalid_decimals",
|
|
2625
|
-
`decimals must be an integer in [0, ${
|
|
2692
|
+
`decimals must be an integer in [0, ${import_solver_discovery3.MAX_ASSET_DECIMALS}], got ${decimals}`
|
|
2626
2693
|
);
|
|
2627
2694
|
}
|
|
2628
2695
|
};
|
|
@@ -2650,8 +2717,8 @@ var Amount = {
|
|
|
2650
2717
|
* result would not survive the record boundary, so one law holds in both
|
|
2651
2718
|
* directions.
|
|
2652
2719
|
*/
|
|
2653
|
-
parse(display,
|
|
2654
|
-
assertDecimals(
|
|
2720
|
+
parse(display, asset6) {
|
|
2721
|
+
assertDecimals(asset6.decimals);
|
|
2655
2722
|
const parts = SCALED_DECIMAL.exec(display);
|
|
2656
2723
|
if (!parts) {
|
|
2657
2724
|
throw new AmountFormatError(
|
|
@@ -2660,17 +2727,17 @@ var Amount = {
|
|
|
2660
2727
|
);
|
|
2661
2728
|
}
|
|
2662
2729
|
const [, whole, fraction = ""] = parts;
|
|
2663
|
-
if (fraction.length >
|
|
2730
|
+
if (fraction.length > asset6.decimals) {
|
|
2664
2731
|
throw new AmountFormatError(
|
|
2665
2732
|
"too_precise",
|
|
2666
|
-
`${display} has more precision than ${
|
|
2733
|
+
`${display} has more precision than ${asset6.decimals} decimals allow`
|
|
2667
2734
|
);
|
|
2668
2735
|
}
|
|
2669
2736
|
if (whole.length + fraction.length > MAX_SIGNIFICANT_DIGITS) {
|
|
2670
2737
|
throw new AmountFormatError("too_large", `${display} has too many digits`);
|
|
2671
2738
|
}
|
|
2672
|
-
const value = (0,
|
|
2673
|
-
assertRepresentable(value, `${display} at ${
|
|
2739
|
+
const value = (0, import_solver_discovery3.toAtomic)(display, asset6.decimals);
|
|
2740
|
+
assertRepresentable(value, `${display} at ${asset6.decimals} decimals`);
|
|
2674
2741
|
return value;
|
|
2675
2742
|
},
|
|
2676
2743
|
/**
|
|
@@ -2679,13 +2746,13 @@ var Amount = {
|
|
|
2679
2746
|
* Refuses a negative: every amount on this surface is an obligation, and a
|
|
2680
2747
|
* negative one is a defect upstream rather than a number to render.
|
|
2681
2748
|
*/
|
|
2682
|
-
format(value,
|
|
2683
|
-
assertDecimals(
|
|
2749
|
+
format(value, asset6) {
|
|
2750
|
+
assertDecimals(asset6.decimals);
|
|
2684
2751
|
if (value < 0n) {
|
|
2685
2752
|
throw new AmountFormatError("negative", `amount must be non-negative, got ${value}`);
|
|
2686
2753
|
}
|
|
2687
2754
|
assertRepresentable(value, `${value}`);
|
|
2688
|
-
return (0,
|
|
2755
|
+
return (0, import_solver_discovery3.fromAtomic)(value, asset6.decimals);
|
|
2689
2756
|
}
|
|
2690
2757
|
};
|
|
2691
2758
|
var toAtomicDecimal = (value) => {
|
|
@@ -2696,7 +2763,7 @@ var toAtomicDecimal = (value) => {
|
|
|
2696
2763
|
return value.toString();
|
|
2697
2764
|
};
|
|
2698
2765
|
var fromAtomicDecimal = (text) => {
|
|
2699
|
-
if (!(0,
|
|
2766
|
+
if (!(0, import_solver_discovery3.isAmount)(text)) {
|
|
2700
2767
|
throw new AmountFormatError(
|
|
2701
2768
|
"not_canonical",
|
|
2702
2769
|
`not a canonical atomic amount: ${JSON.stringify(text)}`
|
|
@@ -2704,10 +2771,10 @@ var fromAtomicDecimal = (text) => {
|
|
|
2704
2771
|
}
|
|
2705
2772
|
return BigInt(text);
|
|
2706
2773
|
};
|
|
2707
|
-
var isAtomicDecimal = (value) => typeof value === "string" &&
|
|
2774
|
+
var isAtomicDecimal = (value) => typeof value === "string" && import_solver_discovery3.AMOUNT_PATTERN.test(value);
|
|
2708
2775
|
|
|
2709
2776
|
// src/client/rfqAmount.ts
|
|
2710
|
-
var
|
|
2777
|
+
var import_solver_discovery4 = require("@arkade-os/solver-discovery");
|
|
2711
2778
|
var toRfqAmountSide = (on) => on === "give" ? "from" : "to";
|
|
2712
2779
|
var fromRfqAmountSide = (side) => side === "from" ? "give" : "take";
|
|
2713
2780
|
var encodeRfqAmount = (value, field) => {
|
|
@@ -2724,7 +2791,7 @@ var encodeRfqAmount = (value, field) => {
|
|
|
2724
2791
|
};
|
|
2725
2792
|
var decodeRfqAmount = (raw, field) => {
|
|
2726
2793
|
if (typeof raw === "string") {
|
|
2727
|
-
if (!(0,
|
|
2794
|
+
if (!(0, import_solver_discovery4.isAmount)(raw)) {
|
|
2728
2795
|
throw new AmountEncodingUnsupported(
|
|
2729
2796
|
field,
|
|
2730
2797
|
raw,
|
|
@@ -3010,6 +3077,7 @@ var profileOf = (preparation, paymentHash) => {
|
|
|
3010
3077
|
htlcParams: preparation.htlcParams,
|
|
3011
3078
|
l1Network: preparation.l1Network,
|
|
3012
3079
|
minConfirmations: preparation.minConfirmations,
|
|
3080
|
+
expectedAmount: toSafeNumber(preparation.expectedAmount, "expectedAmount"),
|
|
3013
3081
|
payoutPkScript: preparation.payoutPkScript
|
|
3014
3082
|
})
|
|
3015
3083
|
};
|
|
@@ -3136,6 +3204,9 @@ var depositMatches = (vtxo, record, expected) => {
|
|
|
3136
3204
|
};
|
|
3137
3205
|
var swapRecordOf = async (repository, id) => storageOf(repository).getSwapRecord(id);
|
|
3138
3206
|
|
|
3207
|
+
// src/client/aliasTable.ts
|
|
3208
|
+
init_store();
|
|
3209
|
+
|
|
3139
3210
|
// src/marketShape.ts
|
|
3140
3211
|
var MARKET_CORRIDORS = ["arkade", "bolt11", "bitcoin", "eip155"];
|
|
3141
3212
|
var LEGACY_CORRIDORS = {
|
|
@@ -3146,9 +3217,9 @@ var LEGACY_CORRIDORS = {
|
|
|
3146
3217
|
};
|
|
3147
3218
|
var assetOf = (market, side) => side === "base" ? market.base_asset : market.quote_asset;
|
|
3148
3219
|
var marketAssetId = (market, side) => {
|
|
3149
|
-
const
|
|
3150
|
-
if (typeof
|
|
3151
|
-
return typeof
|
|
3220
|
+
const asset6 = assetOf(market, side);
|
|
3221
|
+
if (typeof asset6?.caip19_id === "string") return asset6.caip19_id;
|
|
3222
|
+
return typeof asset6?.id === "string" ? asset6.id : void 0;
|
|
3152
3223
|
};
|
|
3153
3224
|
var chainNamespaceOf = (id) => {
|
|
3154
3225
|
if (id === void 0) return void 0;
|
|
@@ -3260,30 +3331,26 @@ var LockupNeedsRecoveryError = class extends Error {
|
|
|
3260
3331
|
this.recoverableAfter = recoverableAfter;
|
|
3261
3332
|
}
|
|
3262
3333
|
};
|
|
3263
|
-
async function findLockupVtxos(
|
|
3264
|
-
const
|
|
3265
|
-
|
|
3266
|
-
|
|
3267
|
-
indexer.getVtxos({ scripts, recoverableOnly: true })
|
|
3268
|
-
]);
|
|
3334
|
+
async function findLockupVtxos(contracts, swapPkScript) {
|
|
3335
|
+
const [row] = await contracts.getContractsWithVtxos({
|
|
3336
|
+
script: import_base9.hex.encode(swapPkScript)
|
|
3337
|
+
});
|
|
3269
3338
|
const seen = /* @__PURE__ */ new Set();
|
|
3270
3339
|
const out = [];
|
|
3271
|
-
for (const
|
|
3272
|
-
|
|
3273
|
-
|
|
3274
|
-
|
|
3275
|
-
|
|
3276
|
-
|
|
3277
|
-
|
|
3278
|
-
|
|
3279
|
-
|
|
3280
|
-
|
|
3281
|
-
|
|
3282
|
-
|
|
3283
|
-
|
|
3284
|
-
|
|
3285
|
-
});
|
|
3286
|
-
}
|
|
3340
|
+
for (const vtxo of row?.vtxos ?? []) {
|
|
3341
|
+
if (vtxo.isUnrolled) continue;
|
|
3342
|
+
if ((0, import_sdk10.hasTerminalSpend)(vtxo)) continue;
|
|
3343
|
+
const key = `${vtxo.txid}:${vtxo.vout}`;
|
|
3344
|
+
if (seen.has(key)) continue;
|
|
3345
|
+
seen.add(key);
|
|
3346
|
+
out.push({
|
|
3347
|
+
txid: vtxo.txid,
|
|
3348
|
+
vout: vtxo.vout,
|
|
3349
|
+
value: Number(vtxo.value),
|
|
3350
|
+
// The manager's canonical `isSwept` fact; no coercion, because the
|
|
3351
|
+
// normalized row guarantees a boolean.
|
|
3352
|
+
recoverable: vtxo.isSwept
|
|
3353
|
+
});
|
|
3287
3354
|
}
|
|
3288
3355
|
return out;
|
|
3289
3356
|
}
|
|
@@ -3684,11 +3751,12 @@ var corridorSet = (base, overrides) => {
|
|
|
3684
3751
|
};
|
|
3685
3752
|
|
|
3686
3753
|
// src/client/discovery.ts
|
|
3687
|
-
var
|
|
3754
|
+
var import_solver_discovery6 = require("@arkade-os/solver-discovery");
|
|
3688
3755
|
|
|
3689
3756
|
// src/markets.ts
|
|
3690
|
-
var
|
|
3757
|
+
var import_solver_discovery5 = require("@arkade-os/solver-discovery");
|
|
3691
3758
|
var import_sdk15 = require("@arkade-os/sdk");
|
|
3759
|
+
init_store();
|
|
3692
3760
|
var QUOTE_OPTIONS = { safetyBps: 0 };
|
|
3693
3761
|
var makeCachedFeedFetch = (ttlMs = 3e4, fetchImpl = fetch) => {
|
|
3694
3762
|
const cache = /* @__PURE__ */ new Map();
|
|
@@ -3745,7 +3813,7 @@ var REGISTRY_URL = {
|
|
|
3745
3813
|
};
|
|
3746
3814
|
var HEX_64 = /^[0-9a-f]{64}$/;
|
|
3747
3815
|
var LEGACY_ASSET_ID = /^(?:btc|[0-9a-f]{68})$/;
|
|
3748
|
-
var isRelayList = (value) => Array.isArray(value) && value.length > 0 && value.length <=
|
|
3816
|
+
var isRelayList = (value) => Array.isArray(value) && value.length > 0 && value.length <= import_solver_discovery6.MAX_RELAYS && value.every((relay) => typeof relay === "string" && relay.startsWith("wss://"));
|
|
3749
3817
|
var isCorridorField = (value) => value === void 0 || CORRIDORS.includes(value);
|
|
3750
3818
|
var isUsableCard = (value) => {
|
|
3751
3819
|
const card = value;
|
|
@@ -3757,7 +3825,7 @@ var isUsableCard = (value) => {
|
|
|
3757
3825
|
const id = marketAssetId(card, side);
|
|
3758
3826
|
if (id === void 0 || !isAssetId(id) && !LEGACY_ASSET_ID.test(id)) return false;
|
|
3759
3827
|
}
|
|
3760
|
-
if ((0,
|
|
3828
|
+
if ((0, import_solver_discovery6.isRfqMarket)(card)) {
|
|
3761
3829
|
if (typeof card.discovery_pubkey !== "string" || !HEX_64.test(card.discovery_pubkey)) {
|
|
3762
3830
|
return false;
|
|
3763
3831
|
}
|
|
@@ -3833,7 +3901,7 @@ var discoveryIndex = (input) => {
|
|
|
3833
3901
|
if (!indexed || !url) {
|
|
3834
3902
|
return unavailable(whyUnavailable());
|
|
3835
3903
|
}
|
|
3836
|
-
const result = await (0,
|
|
3904
|
+
const result = await (0, import_solver_discovery6.discover)({
|
|
3837
3905
|
registries: [url],
|
|
3838
3906
|
localCards: [...config.localCards ?? []],
|
|
3839
3907
|
network: indexed,
|
|
@@ -3869,11 +3937,15 @@ var discoveryIndex = (input) => {
|
|
|
3869
3937
|
|
|
3870
3938
|
// src/client/cancel.ts
|
|
3871
3939
|
var import_base12 = require("@scure/base");
|
|
3872
|
-
var
|
|
3940
|
+
var import_sdk18 = require("@arkade-os/sdk");
|
|
3941
|
+
init_coverage();
|
|
3942
|
+
init_offer();
|
|
3873
3943
|
|
|
3874
3944
|
// src/restore.ts
|
|
3875
3945
|
var import_base11 = require("@scure/base");
|
|
3876
3946
|
var import_sdk16 = require("@arkade-os/sdk");
|
|
3947
|
+
init_offer();
|
|
3948
|
+
init_store();
|
|
3877
3949
|
var TXS_PER_REQUEST = 50;
|
|
3878
3950
|
async function fetchParsedTxs(indexer, txids) {
|
|
3879
3951
|
const parsedByTxid = /* @__PURE__ */ new Map();
|
|
@@ -3935,7 +4007,7 @@ function classifyDepositSpend(offer, operatorPubkey, spendTxs, deposit) {
|
|
|
3935
4007
|
return "indeterminate";
|
|
3936
4008
|
}
|
|
3937
4009
|
async function restoreAssetSwaps(indexer, txs, existingIds, opts) {
|
|
3938
|
-
const { operatorPubkey, scanned = /* @__PURE__ */ new Set(), reopen = [] } = opts;
|
|
4010
|
+
const { operatorPubkey, scanned = /* @__PURE__ */ new Set(), reopen = [], hrp } = opts;
|
|
3939
4011
|
const reopened = [];
|
|
3940
4012
|
for (const swap of reopen) {
|
|
3941
4013
|
try {
|
|
@@ -4036,17 +4108,27 @@ async function restoreAssetSwaps(indexer, txs, existingIds, opts) {
|
|
|
4036
4108
|
restored.push({ ...existing, status, spentTxid, ...completion });
|
|
4037
4109
|
continue;
|
|
4038
4110
|
}
|
|
4111
|
+
let swapAddress = "";
|
|
4112
|
+
try {
|
|
4113
|
+
const compiled = offerContract(offer, operatorPubkey);
|
|
4114
|
+
if (import_base11.hex.encode(compiled.pkScript) !== swapPkScript) {
|
|
4115
|
+
unresolved.add(fundingTx.redeemTxid);
|
|
4116
|
+
continue;
|
|
4117
|
+
}
|
|
4118
|
+
if (hrp !== void 0) {
|
|
4119
|
+
swapAddress = compiled.address(hrp, operatorPubkey).encode();
|
|
4120
|
+
}
|
|
4121
|
+
} catch {
|
|
4122
|
+
unresolved.add(fundingTx.redeemTxid);
|
|
4123
|
+
continue;
|
|
4124
|
+
}
|
|
4039
4125
|
restored.push({
|
|
4040
4126
|
id: fundingTx.redeemTxid,
|
|
4041
4127
|
fromAsset,
|
|
4042
4128
|
toAsset,
|
|
4043
4129
|
fromAmount,
|
|
4044
4130
|
toAmount: offer.wantAmount.toString(),
|
|
4045
|
-
|
|
4046
|
-
// to the current operator key; store the funded address if operator-key
|
|
4047
|
-
// rotations become real (cancelOffer now at least diagnoses the
|
|
4048
|
-
// mismatch instead of reporting a missing VTXO)
|
|
4049
|
-
swapAddress: "",
|
|
4131
|
+
swapAddress,
|
|
4050
4132
|
swapPkScript,
|
|
4051
4133
|
offerHex,
|
|
4052
4134
|
fundingTxid: fundingTx.redeemTxid,
|
|
@@ -4062,6 +4144,8 @@ async function restoreAssetSwaps(indexer, txs, existingIds, opts) {
|
|
|
4062
4144
|
}
|
|
4063
4145
|
|
|
4064
4146
|
// src/client/driveRecords.ts
|
|
4147
|
+
init_store();
|
|
4148
|
+
var import_sdk17 = require("@arkade-os/sdk");
|
|
4065
4149
|
var splitRecords = (records) => {
|
|
4066
4150
|
const corridor = [];
|
|
4067
4151
|
const offer = [];
|
|
@@ -4203,7 +4287,44 @@ var applyOfferSpend = (record, changes, now) => ({
|
|
|
4203
4287
|
...changes.completedAt === void 0 ? {} : { completedAt: Math.floor(changes.completedAt / 1e3) },
|
|
4204
4288
|
updatedAt: now
|
|
4205
4289
|
});
|
|
4206
|
-
var
|
|
4290
|
+
var withDepositFate = (record, fate, now) => ({
|
|
4291
|
+
...record,
|
|
4292
|
+
status: fate.status,
|
|
4293
|
+
...fate.spentTxid === void 0 ? {} : { spentTxid: fate.spentTxid },
|
|
4294
|
+
...fate.completedAt === void 0 ? {} : { completedAt: Math.floor(fate.completedAt / 1e3) },
|
|
4295
|
+
updatedAt: now
|
|
4296
|
+
});
|
|
4297
|
+
var fateMoved = (record, fate) => record.status !== fate.status || fate.spentTxid !== void 0 && record.spentTxid !== fate.spentTxid || fate.completedAt !== void 0 && record.completedAt !== Math.floor(fate.completedAt / 1e3);
|
|
4298
|
+
var restoredOfferRecord = (swap, network, now) => {
|
|
4299
|
+
const give = restoredAssetId(network, swap.fromAsset);
|
|
4300
|
+
const take = restoredAssetId(network, swap.toAsset);
|
|
4301
|
+
const createdAt = Math.floor(swap.createdAt / 1e3);
|
|
4302
|
+
return withDepositFate(
|
|
4303
|
+
{
|
|
4304
|
+
id: swap.fundingTxid,
|
|
4305
|
+
family: "offer",
|
|
4306
|
+
route: {
|
|
4307
|
+
give: { corridor: "arkade", asset: give, instrument: { kind: "wallet" } },
|
|
4308
|
+
take: { corridor: "arkade", asset: take, instrument: { kind: "wallet" } }
|
|
4309
|
+
},
|
|
4310
|
+
give: { asset: give, amount: toAtomicDecimal(BigInt(swap.fromAmount)) },
|
|
4311
|
+
take: { asset: take, amount: toAtomicDecimal(BigInt(swap.toAmount)) },
|
|
4312
|
+
fee: { asset: take, amount: toAtomicDecimal(BigInt(0)) },
|
|
4313
|
+
market: { kind: "restored", backend: "feed" },
|
|
4314
|
+
expiresAt: createdAt,
|
|
4315
|
+
status: swap.status,
|
|
4316
|
+
offerHex: swap.offerHex,
|
|
4317
|
+
swapAddress: swap.swapAddress,
|
|
4318
|
+
swapPkScript: swap.swapPkScript,
|
|
4319
|
+
fundingTxid: swap.fundingTxid,
|
|
4320
|
+
createdAt,
|
|
4321
|
+
updatedAt: now
|
|
4322
|
+
},
|
|
4323
|
+
swap,
|
|
4324
|
+
now
|
|
4325
|
+
);
|
|
4326
|
+
};
|
|
4327
|
+
var restoredAssetId = (network, id) => id === BTC_ASSET_ID ? btcOn("arkade", network) : arkadeAsset(network, import_sdk17.asset.AssetId.fromString(id));
|
|
4207
4328
|
var walletLockupIndexer = (wallet) => {
|
|
4208
4329
|
let reader;
|
|
4209
4330
|
const reading = () => reader ??= wallet.getArkadeReader();
|
|
@@ -4293,12 +4414,12 @@ var reconcileDepositSpend = async (record, indexer) => {
|
|
|
4293
4414
|
const { txs } = await indexer.getVirtualTxs(candidates);
|
|
4294
4415
|
const parsed = txs.map((psbt) => {
|
|
4295
4416
|
try {
|
|
4296
|
-
return
|
|
4417
|
+
return import_sdk18.Transaction.fromPSBT(import_base12.base64.decode(psbt));
|
|
4297
4418
|
} catch {
|
|
4298
4419
|
return void 0;
|
|
4299
4420
|
}
|
|
4300
4421
|
}).filter((tx) => tx !== void 0);
|
|
4301
|
-
const operatorPubkey =
|
|
4422
|
+
const operatorPubkey = import_sdk18.ArkAddress.decode(record.swapAddress).serverPubKey;
|
|
4302
4423
|
const kind = classifyDepositSpend(offer, operatorPubkey, parsed, {
|
|
4303
4424
|
txid: deposit.txid,
|
|
4304
4425
|
vout: deposit.vout
|
|
@@ -4337,14 +4458,14 @@ var retireOfferScripts = async (wallet, repository, record) => {
|
|
|
4337
4458
|
var isMissingVtxo = (error) => error instanceof NoSpendableDepositError;
|
|
4338
4459
|
|
|
4339
4460
|
// src/client/drive.ts
|
|
4340
|
-
var
|
|
4461
|
+
var import_sdk24 = require("@arkade-os/sdk");
|
|
4341
4462
|
var import_base17 = require("@scure/base");
|
|
4342
4463
|
|
|
4343
4464
|
// src/claim.ts
|
|
4344
4465
|
var import_base13 = require("@scure/base");
|
|
4345
4466
|
var import_legacy3 = require("@noble/hashes/legacy.js");
|
|
4346
4467
|
var import_sha25 = require("@noble/hashes/sha2.js");
|
|
4347
|
-
var
|
|
4468
|
+
var import_sdk19 = require("@arkade-os/sdk");
|
|
4348
4469
|
var LockupAmountMismatchError = class extends Error {
|
|
4349
4470
|
name = "LockupAmountMismatchError";
|
|
4350
4471
|
reason = "amount_mismatch";
|
|
@@ -4388,14 +4509,14 @@ async function pushClaim(operator, input) {
|
|
|
4388
4509
|
const info = await operator.getInfo();
|
|
4389
4510
|
let operatorUnrollScript;
|
|
4390
4511
|
try {
|
|
4391
|
-
operatorUnrollScript =
|
|
4512
|
+
operatorUnrollScript = import_sdk19.CSVMultisigTapscript.decode(import_base13.hex.decode(info.checkpointTapscript));
|
|
4392
4513
|
} catch {
|
|
4393
4514
|
throw new Error("invalid checkpointTapscript from the operator");
|
|
4394
4515
|
}
|
|
4395
4516
|
const leaf = input.contract.claim();
|
|
4396
4517
|
const tapTree = input.contract.encode();
|
|
4397
|
-
const txid = await (0,
|
|
4398
|
-
identity: (0,
|
|
4518
|
+
const txid = await (0, import_sdk19.signAndSubmitOffchainTx)({
|
|
4519
|
+
identity: (0, import_sdk19.claimWithPreimageIdentity)(input.receiver, input.preimage),
|
|
4399
4520
|
provider: operator,
|
|
4400
4521
|
inputs: input.vtxos.map((vtxo) => ({
|
|
4401
4522
|
txid: vtxo.txid,
|
|
@@ -4415,10 +4536,11 @@ async function pushClaim(operator, input) {
|
|
|
4415
4536
|
|
|
4416
4537
|
// src/client/drive.ts
|
|
4417
4538
|
init_onchainHtlc();
|
|
4539
|
+
init_coverage();
|
|
4418
4540
|
init_lockupContract();
|
|
4419
4541
|
|
|
4420
4542
|
// src/refundBlocked.ts
|
|
4421
|
-
var
|
|
4543
|
+
var import_sdk20 = require("@arkade-os/sdk");
|
|
4422
4544
|
var RefundNotLocallyPossibleError = class extends Error {
|
|
4423
4545
|
constructor(reason, message, options) {
|
|
4424
4546
|
super(message, options);
|
|
@@ -4435,16 +4557,16 @@ async function senderIdentityForSwapRecord(wallet, record) {
|
|
|
4435
4557
|
);
|
|
4436
4558
|
}
|
|
4437
4559
|
try {
|
|
4438
|
-
return await (0,
|
|
4560
|
+
return await (0, import_sdk20.contractSigner)(wallet, record.signingDescriptor);
|
|
4439
4561
|
} catch (cause) {
|
|
4440
|
-
if (cause instanceof
|
|
4562
|
+
if (cause instanceof import_sdk20.WalletCannotSignError) {
|
|
4441
4563
|
throw new RefundNotLocallyPossibleError(
|
|
4442
4564
|
"unsignable-wallet",
|
|
4443
4565
|
`this wallet holds ${record.signingDescriptor} but cannot sign with it; attach its signer`,
|
|
4444
4566
|
{ cause }
|
|
4445
4567
|
);
|
|
4446
4568
|
}
|
|
4447
|
-
if (cause instanceof
|
|
4569
|
+
if (cause instanceof import_sdk20.ForeignDescriptorError) {
|
|
4448
4570
|
throw new RefundNotLocallyPossibleError(
|
|
4449
4571
|
"foreign-descriptor",
|
|
4450
4572
|
`this wallet cannot derive ${record.signingDescriptor}; the swap was created on another wallet`,
|
|
@@ -4464,7 +4586,7 @@ function arkadeRefunder(deps) {
|
|
|
4464
4586
|
`swap ${swap.rfqId} carries no lockup covenant, so its refund cannot be built`
|
|
4465
4587
|
);
|
|
4466
4588
|
}
|
|
4467
|
-
const vtxos = await findLockupVtxos(deps.
|
|
4589
|
+
const vtxos = await findLockupVtxos(deps.contracts, swap.lockupPkScript);
|
|
4468
4590
|
if (vtxos.length === 0) return null;
|
|
4469
4591
|
const record = await deps.repository.getRfqSwap(swap.rfqId);
|
|
4470
4592
|
if (!record) {
|
|
@@ -4479,7 +4601,7 @@ function arkadeRefunder(deps) {
|
|
|
4479
4601
|
}
|
|
4480
4602
|
|
|
4481
4603
|
// src/rfqRecord.ts
|
|
4482
|
-
var
|
|
4604
|
+
var import_sdk21 = require("@arkade-os/sdk");
|
|
4483
4605
|
var import_base14 = require("@scure/base");
|
|
4484
4606
|
|
|
4485
4607
|
// src/rfqSwapState.ts
|
|
@@ -4525,7 +4647,7 @@ function assertSameSwap(origin, swap) {
|
|
|
4525
4647
|
`rfq swap record is a ${origin.kind} origin paired with a ${swap.kind} swap`
|
|
4526
4648
|
);
|
|
4527
4649
|
}
|
|
4528
|
-
const funded = import_base14.hex.encode(
|
|
4650
|
+
const funded = import_base14.hex.encode(import_sdk21.ArkAddress.decode(origin.lockupAddress).pkScript);
|
|
4529
4651
|
const watched = import_base14.hex.encode(swap.lockupPkScript);
|
|
4530
4652
|
if (funded !== watched) {
|
|
4531
4653
|
throw new Error(
|
|
@@ -4574,8 +4696,8 @@ function rfqSwapOriginOf(record) {
|
|
|
4574
4696
|
};
|
|
4575
4697
|
}
|
|
4576
4698
|
function lockupScript(params, lockupAddress) {
|
|
4577
|
-
const script =
|
|
4578
|
-
const funded =
|
|
4699
|
+
const script = import_sdk21.VHTLCV2ContractHandler.createScript(params);
|
|
4700
|
+
const funded = import_sdk21.ArkAddress.decode(lockupAddress).pkScript;
|
|
4579
4701
|
if (import_base14.hex.encode(funded) !== import_base14.hex.encode(script.pkScript)) {
|
|
4580
4702
|
throw new Error(
|
|
4581
4703
|
`rfq swap covenant params derive ${import_base14.hex.encode(script.pkScript)}, but the record's lockup address holds ${import_base14.hex.encode(funded)} \u2014 these params are not this swap's`
|
|
@@ -4615,11 +4737,15 @@ function shouldRetainRfqSwap(record, now) {
|
|
|
4615
4737
|
return now - record.updatedAt < RFQ_SWAP_RETENTION_SECONDS;
|
|
4616
4738
|
}
|
|
4617
4739
|
|
|
4740
|
+
// src/client/drive.ts
|
|
4741
|
+
init_store();
|
|
4742
|
+
|
|
4618
4743
|
// src/swapManager.ts
|
|
4619
4744
|
var import_base15 = require("@scure/base");
|
|
4620
|
-
var
|
|
4745
|
+
var import_sdk22 = require("@arkade-os/sdk");
|
|
4621
4746
|
init_onchainHtlc();
|
|
4622
4747
|
init_lockupContract();
|
|
4748
|
+
init_coverage();
|
|
4623
4749
|
function nextOnchainAction(input) {
|
|
4624
4750
|
switch (input.phase.phase) {
|
|
4625
4751
|
case "unfunded":
|
|
@@ -5116,7 +5242,7 @@ var RfqSwapManager = class {
|
|
|
5116
5242
|
});
|
|
5117
5243
|
return;
|
|
5118
5244
|
}
|
|
5119
|
-
if (!(0,
|
|
5245
|
+
if (!(0, import_sdk22.isContractVtxoEvent)(event)) return;
|
|
5120
5246
|
const swap = this.byLockupScript.get(event.contractScript);
|
|
5121
5247
|
if (!swap) return;
|
|
5122
5248
|
void this.pollSwap(swap).catch(() => {
|
|
@@ -5201,6 +5327,40 @@ var RfqSwapManager = class {
|
|
|
5201
5327
|
void this.poll().then(() => this.arm());
|
|
5202
5328
|
}, this.config.pollIntervalMs);
|
|
5203
5329
|
}
|
|
5330
|
+
/*
|
|
5331
|
+
* Runs one serialized monitoring pass for a single swap.
|
|
5332
|
+
*
|
|
5333
|
+
* Flow
|
|
5334
|
+
*
|
|
5335
|
+
* 1. Prevents concurrent work
|
|
5336
|
+
* - Returns immediately if the swap is already in inProgress.
|
|
5337
|
+
* - Returns if the swap is no longer in monitored.
|
|
5338
|
+
* - Adds the swap ID to inProgress.
|
|
5339
|
+
*
|
|
5340
|
+
* 2. Runs the state machine
|
|
5341
|
+
* - Calls runPass(swap).
|
|
5342
|
+
* - runPass checks the lockup fate, drives claims, and may initiate refunds.
|
|
5343
|
+
*
|
|
5344
|
+
* 3. Persists changes
|
|
5345
|
+
* - If the pass marked the swap dirty, calls save(swap).
|
|
5346
|
+
* - A swap is considered persisted only if all configured sinks succeed:
|
|
5347
|
+
* - the repository, and
|
|
5348
|
+
* - callbacks.saveSwap, if provided.
|
|
5349
|
+
*
|
|
5350
|
+
* 4. Completes waiters and finalizes
|
|
5351
|
+
* - If persistence succeeds:
|
|
5352
|
+
* - clears the dirty flag,
|
|
5353
|
+
* - settles waitForSwapCompletion waiters,
|
|
5354
|
+
* - moves terminal swaps from monitored to finished,
|
|
5355
|
+
* - emits completion or failure notifications through finalize.
|
|
5356
|
+
*
|
|
5357
|
+
* 5. Releases the per-swap lock
|
|
5358
|
+
* - Deletes the swap ID from inProgress only after all asynchronous work completes.
|
|
5359
|
+
* - This prevents overlapping poll() calls from submitting duplicate claims or refunds.
|
|
5360
|
+
*
|
|
5361
|
+
* The important safety property is that terminal state is not finalized until its updated record has been
|
|
5362
|
+
* successfully persisted. A failed save leaves the swap dirty and monitored so a later poll can retry.
|
|
5363
|
+
*/
|
|
5204
5364
|
async pollSwap(swap) {
|
|
5205
5365
|
if (this.inProgress.has(swap.rfqId)) return;
|
|
5206
5366
|
if (!this.monitored.has(swap.rfqId)) return;
|
|
@@ -5217,6 +5377,42 @@ var RfqSwapManager = class {
|
|
|
5217
5377
|
this.inProgress.delete(swap.rfqId);
|
|
5218
5378
|
}
|
|
5219
5379
|
}
|
|
5380
|
+
/*
|
|
5381
|
+
* executes one monitoring/state-machine pass for a swap.
|
|
5382
|
+
*
|
|
5383
|
+
* Flow
|
|
5384
|
+
* 1. Registers the lockup
|
|
5385
|
+
* - Calls ensureRegistered(swap).
|
|
5386
|
+
* - Registration is best effort and does not prevent the rest of the pass from running.
|
|
5387
|
+
*
|
|
5388
|
+
* 2. Reads the Arkade lockup fate
|
|
5389
|
+
* - Calls readLockupFate.
|
|
5390
|
+
* - A hash-verified claim is treated as claimed.
|
|
5391
|
+
* - A fully observed non-claim spend is treated as returned.
|
|
5392
|
+
* - Indexer errors become unknown, allowing deadline-driven logic to continue.
|
|
5393
|
+
* - For claimed or returned, it:
|
|
5394
|
+
* - records spend transaction IDs,
|
|
5395
|
+
* - records the settlement preimage when applicable,
|
|
5396
|
+
* - sets the swap state to settled or refunded,
|
|
5397
|
+
* - ends the pass.
|
|
5398
|
+
*
|
|
5399
|
+
* 3. Handles unilateral exits
|
|
5400
|
+
* - On receive swaps, an exited lockup is blocked immediately.
|
|
5401
|
+
* - On send swaps, the L1 half continues to be monitored because an exit only describes the Arkade side.
|
|
5402
|
+
*
|
|
5403
|
+
* 4. Drives the trader’s claim
|
|
5404
|
+
* - For lightning_receive, calls driveReceiveClaim and ends the pass.
|
|
5405
|
+
* - For onchain_send, calls driveOnchain to classify and potentially claim the L1 HTLC.
|
|
5406
|
+
* - A handled L1 action ends the pass; otherwise processing continues.
|
|
5407
|
+
*
|
|
5408
|
+
* 5. Drives an Arkade refund
|
|
5409
|
+
* - For send swaps whose lockup remains unresolved, calls driveArkadeRefund.
|
|
5410
|
+
* - This is gated by the refund timelock and local refund capability.
|
|
5411
|
+
* - Receive swaps never reach this step because their refund path belongs to the solver.
|
|
5412
|
+
*
|
|
5413
|
+
* runPass only decides and performs state-machine actions. Persistence, waiter settlement, terminal finalization,
|
|
5414
|
+
* and the per-swap lock are handled by pollSwap.
|
|
5415
|
+
*/
|
|
5220
5416
|
async runPass(swap) {
|
|
5221
5417
|
await this.ensureRegistered(swap);
|
|
5222
5418
|
let fate;
|
|
@@ -5235,7 +5431,10 @@ var RfqSwapManager = class {
|
|
|
5235
5431
|
return;
|
|
5236
5432
|
}
|
|
5237
5433
|
if (swap.kind === "lightning_receive") {
|
|
5238
|
-
|
|
5434
|
+
if (fate.fate === "exited") return this.blockExitedLockup(swap, fate);
|
|
5435
|
+
if (fate.fate === "open") return this.driveReceiveClaim(swap);
|
|
5436
|
+
if (this.config.now() >= swap.refundLocktime) return this.driveReceiveClaim(swap);
|
|
5437
|
+
return;
|
|
5239
5438
|
}
|
|
5240
5439
|
if (swap.kind === "onchain_send" && swap.state !== "claimed") {
|
|
5241
5440
|
if (await this.driveOnchain(swap) === "handled") return;
|
|
@@ -5290,7 +5489,7 @@ var RfqSwapManager = class {
|
|
|
5290
5489
|
if (now < swap.refundLocktime) {
|
|
5291
5490
|
let vtxos;
|
|
5292
5491
|
try {
|
|
5293
|
-
vtxos = await findLockupVtxos(this.deps.
|
|
5492
|
+
vtxos = await findLockupVtxos(this.deps.contracts, swap.lockupPkScript);
|
|
5294
5493
|
} catch (error) {
|
|
5295
5494
|
this.emitFailed(swap, error);
|
|
5296
5495
|
return;
|
|
@@ -5393,6 +5592,21 @@ var RfqSwapManager = class {
|
|
|
5393
5592
|
});
|
|
5394
5593
|
if (action === "claim" && phase.phase === "claimable") {
|
|
5395
5594
|
if (swap.claimTxid) return "continue";
|
|
5595
|
+
if (!Number.isSafeInteger(swap.expectedAmount) || swap.expectedAmount <= 0) {
|
|
5596
|
+
this.block(
|
|
5597
|
+
swap,
|
|
5598
|
+
`expectedAmount is not a positive number of sats (${String(swap.expectedAmount)}), so the funded value cannot be checked \u2014 refusing to publish the preimage`
|
|
5599
|
+
);
|
|
5600
|
+
return "handled";
|
|
5601
|
+
}
|
|
5602
|
+
const filled = Number(phase.utxo.amount);
|
|
5603
|
+
if (!Number.isFinite(filled) || filled < swap.expectedAmount) {
|
|
5604
|
+
this.block(
|
|
5605
|
+
swap,
|
|
5606
|
+
`fill holds ${filled} sats, below the agreed ${swap.expectedAmount} \u2014 refusing to publish the preimage`
|
|
5607
|
+
);
|
|
5608
|
+
return "handled";
|
|
5609
|
+
}
|
|
5396
5610
|
if (this.callbacks && !this.callbacks.claimOnchain) {
|
|
5397
5611
|
this.block(swap, RFQ_CONFIGURATION_REFUSAL.noClaimOnchainCallback);
|
|
5398
5612
|
return "handled";
|
|
@@ -5443,6 +5657,8 @@ var RfqSwapManager = class {
|
|
|
5443
5657
|
if (pushed) {
|
|
5444
5658
|
swap.refundTxid = pushed.txid;
|
|
5445
5659
|
this.touch(swap);
|
|
5660
|
+
} else if (now < swap.refundLocktime + REFUND_MTP_LAG_SECONDS) {
|
|
5661
|
+
return;
|
|
5446
5662
|
}
|
|
5447
5663
|
this.setState(swap, "refunded");
|
|
5448
5664
|
this.emitAction(swap, "refundArkade");
|
|
@@ -5677,7 +5893,10 @@ var outpointKey = (vtxo) => `${vtxo.txid}:${vtxo.vout}`;
|
|
|
5677
5893
|
|
|
5678
5894
|
// src/watch.ts
|
|
5679
5895
|
var import_base16 = require("@scure/base");
|
|
5680
|
-
var
|
|
5896
|
+
var import_sdk23 = require("@arkade-os/sdk");
|
|
5897
|
+
init_coverage();
|
|
5898
|
+
init_offer();
|
|
5899
|
+
init_store();
|
|
5681
5900
|
var TERMINAL = ["fulfilled", "cancelled", "recoverable"];
|
|
5682
5901
|
var assetSwapSource = (repository) => ({
|
|
5683
5902
|
list: () => getAssetSwaps(repository),
|
|
@@ -5709,7 +5928,7 @@ async function watchOfferSwaps({
|
|
|
5709
5928
|
wallet.getAddress(),
|
|
5710
5929
|
wallet.getArkadeReader()
|
|
5711
5930
|
]);
|
|
5712
|
-
const operatorPubkey =
|
|
5931
|
+
const operatorPubkey = import_sdk23.ArkAddress.decode(address).serverPubKey;
|
|
5713
5932
|
let queue = Promise.resolve();
|
|
5714
5933
|
const enqueue = (task) => {
|
|
5715
5934
|
queue = queue.then(task).catch(() => {
|
|
@@ -5724,7 +5943,7 @@ async function watchOfferSwaps({
|
|
|
5724
5943
|
return classifyDepositSpend(
|
|
5725
5944
|
decodeOffer(import_base16.hex.decode(swap.offerHex)),
|
|
5726
5945
|
operatorPubkey,
|
|
5727
|
-
txs.map((psbt) =>
|
|
5946
|
+
txs.map((psbt) => import_sdk23.Transaction.fromPSBT(import_base16.base64.decode(psbt))),
|
|
5728
5947
|
{ txid: vtxo.txid, vout: vtxo.vout }
|
|
5729
5948
|
);
|
|
5730
5949
|
} catch {
|
|
@@ -5752,7 +5971,7 @@ async function watchOfferSwaps({
|
|
|
5752
5971
|
}
|
|
5753
5972
|
};
|
|
5754
5973
|
const unsubscribe = manager.onContractEvent((event) => {
|
|
5755
|
-
if (!(0,
|
|
5974
|
+
if (!(0, import_sdk23.isContractVtxoEvent)(event) || event.type !== "vtxo_spent") return;
|
|
5756
5975
|
enqueue(() => handleSpend(event));
|
|
5757
5976
|
});
|
|
5758
5977
|
return {
|
|
@@ -5935,7 +6154,7 @@ var createSwapDrive = (config) => {
|
|
|
5935
6154
|
const secret = rfqClaimSecretOf(record);
|
|
5936
6155
|
if (!secret) throw new Error(`rfq swap ${swap.rfqId} carries no claim secret`);
|
|
5937
6156
|
const preimage = await preimageForSwapRecord(wallet, secret);
|
|
5938
|
-
const signer = await (0,
|
|
6157
|
+
const signer = await (0, import_sdk24.contractSigner)(wallet, await (0, import_sdk24.identityDescriptor)(wallet.identity));
|
|
5939
6158
|
const claimKey = record.profile.claimKey;
|
|
5940
6159
|
if (typeof claimKey === "string") {
|
|
5941
6160
|
const held = import_base17.hex.encode(await signer.xOnlyPublicKey());
|
|
@@ -5993,10 +6212,10 @@ var createSwapDrive = (config) => {
|
|
|
5993
6212
|
return track(
|
|
5994
6213
|
pushClaim(operator, {
|
|
5995
6214
|
contract: script,
|
|
5996
|
-
receiver: await (0,
|
|
6215
|
+
receiver: await (0, import_sdk24.contractSigner)(wallet, secret.signingDescriptor),
|
|
5997
6216
|
preimage: await preimageForSwapRecord(wallet, secret),
|
|
5998
6217
|
vtxos,
|
|
5999
|
-
destinationPkScript:
|
|
6218
|
+
destinationPkScript: import_sdk24.ArkAddress.decode(payoutAddress).pkScript,
|
|
6000
6219
|
// Passed straight through, both of them: the manager's own value
|
|
6001
6220
|
// check decides WHEN to act, `pushClaim`'s decides whether `P` is
|
|
6002
6221
|
// published, and it is the one with nothing between it and the
|
|
@@ -6029,7 +6248,7 @@ var createSwapDrive = (config) => {
|
|
|
6029
6248
|
if (mode === "readonly") return;
|
|
6030
6249
|
const push = arkadeRefunder({
|
|
6031
6250
|
operator,
|
|
6032
|
-
|
|
6251
|
+
contracts: lockupSource,
|
|
6033
6252
|
wallet,
|
|
6034
6253
|
repository: corridorStore()
|
|
6035
6254
|
});
|
|
@@ -6055,16 +6274,13 @@ var createSwapDrive = (config) => {
|
|
|
6055
6274
|
};
|
|
6056
6275
|
let offerSource;
|
|
6057
6276
|
const offers = () => offerSource ??= offerRecordSource(storage(), remember, now);
|
|
6058
|
-
const
|
|
6059
|
-
const funded = offer.filter(
|
|
6060
|
-
(record) => record.fundingTxid !== void 0 && !RETIRABLE.includes(record.status)
|
|
6061
|
-
);
|
|
6062
|
-
if (funded.length === 0) return;
|
|
6277
|
+
const restoreOfferDeposits = async (reopen) => {
|
|
6063
6278
|
const store = storage();
|
|
6064
|
-
const [history, scanned, address] = await Promise.all([
|
|
6279
|
+
const [history, scanned, address, network] = await Promise.all([
|
|
6065
6280
|
wallet.getTransactionHistory(),
|
|
6066
6281
|
store.getScannedTxids(),
|
|
6067
|
-
wallet.getAddress()
|
|
6282
|
+
wallet.getAddress(),
|
|
6283
|
+
config.network()
|
|
6068
6284
|
]);
|
|
6069
6285
|
const txs = history.map((tx) => ({
|
|
6070
6286
|
// `TxType` is `"SENT"`/`"RECEIVED"`; the scan filters on `"sent"`.
|
|
@@ -6075,30 +6291,61 @@ var createSwapDrive = (config) => {
|
|
|
6075
6291
|
// The scan reads unix SECONDS; a wallet transaction carries ms.
|
|
6076
6292
|
...tx.createdAt ? { createdAt: Math.floor(tx.createdAt / 1e3) } : {}
|
|
6077
6293
|
}));
|
|
6294
|
+
const { hrp, serverPubKey: operatorPubkey } = import_sdk24.ArkAddress.decode(address);
|
|
6295
|
+
const cursor = reopen === void 0 ? scanned : new Set([...scanned].filter((txid) => txid !== reopen));
|
|
6078
6296
|
const { restored, scannedTxids } = await restoreAssetSwaps(indexer, txs, /* @__PURE__ */ new Set(), {
|
|
6079
|
-
operatorPubkey
|
|
6080
|
-
scanned
|
|
6297
|
+
operatorPubkey,
|
|
6298
|
+
scanned: cursor,
|
|
6299
|
+
hrp
|
|
6081
6300
|
});
|
|
6082
|
-
const
|
|
6083
|
-
const
|
|
6084
|
-
|
|
6085
|
-
|
|
6086
|
-
|
|
6087
|
-
|
|
6088
|
-
|
|
6089
|
-
|
|
6090
|
-
|
|
6301
|
+
const { offer } = splitRecords(await store.getAllSwapRecords());
|
|
6302
|
+
const current = new Map(offer.map((record) => [record.id, record]));
|
|
6303
|
+
const byDeposit = new Map(
|
|
6304
|
+
offer.filter((record) => record.fundingTxid !== void 0).map((record) => [`${record.swapPkScript}:${record.fundingTxid}`, record])
|
|
6305
|
+
);
|
|
6306
|
+
const unstamped = /* @__PURE__ */ new Map();
|
|
6307
|
+
for (const record of offer) {
|
|
6308
|
+
if (record.fundingTxid !== void 0 || !OFFER_LIVE(record.status)) continue;
|
|
6309
|
+
unstamped.set(record.swapPkScript, [
|
|
6310
|
+
...unstamped.get(record.swapPkScript) ?? [],
|
|
6311
|
+
record
|
|
6312
|
+
]);
|
|
6313
|
+
}
|
|
6314
|
+
const write = async (record) => {
|
|
6315
|
+
await store.saveSwapRecord(record);
|
|
6316
|
+
current.set(record.id, record);
|
|
6317
|
+
remember(record);
|
|
6318
|
+
};
|
|
6319
|
+
for (const found of restored) {
|
|
6320
|
+
const known = byDeposit.get(`${found.swapPkScript}:${found.fundingTxid}`);
|
|
6321
|
+
if (known !== void 0) {
|
|
6322
|
+
if (fateMoved(known, found)) await write(withDepositFate(known, found, now()));
|
|
6323
|
+
continue;
|
|
6324
|
+
}
|
|
6325
|
+
const orphan = unstamped.get(found.swapPkScript)?.shift();
|
|
6326
|
+
if (orphan !== void 0) {
|
|
6327
|
+
await write(
|
|
6328
|
+
withDepositFate({ ...orphan, fundingTxid: found.fundingTxid }, found, now())
|
|
6329
|
+
);
|
|
6330
|
+
continue;
|
|
6091
6331
|
}
|
|
6092
|
-
|
|
6332
|
+
await write(restoredOfferRecord(found, network, now()));
|
|
6333
|
+
}
|
|
6334
|
+
const stillLive = /* @__PURE__ */ new Set();
|
|
6335
|
+
for (const record of current.values()) {
|
|
6336
|
+
if (OFFER_LIVE(record.status) && record.fundingTxid) stillLive.add(record.fundingTxid);
|
|
6093
6337
|
}
|
|
6094
6338
|
await store.markTxidsScanned(scannedTxids.filter((txid) => !stillLive.has(txid)));
|
|
6095
6339
|
const registry = managerDeps.contracts;
|
|
6096
6340
|
if (registry) {
|
|
6097
|
-
|
|
6098
|
-
await retireSettledOfferContracts(registry, current.map(offerFactsOf));
|
|
6341
|
+
await retireSettledOfferContracts(registry, [...current.values()].map(offerFactsOf));
|
|
6099
6342
|
}
|
|
6343
|
+
return [...current.values()];
|
|
6100
6344
|
};
|
|
6101
6345
|
const contractsOf = async () => managerDeps.contracts ??= await wallet.getContractManager();
|
|
6346
|
+
const lockupSource = {
|
|
6347
|
+
getContractsWithVtxos: async (filter) => (await contractsOf()).getContractsWithVtxos(filter)
|
|
6348
|
+
};
|
|
6102
6349
|
const registerCorridorSwap = async (record) => {
|
|
6103
6350
|
if (mode === "readonly") return;
|
|
6104
6351
|
if (!await drivable(record)) {
|
|
@@ -6169,13 +6416,14 @@ var createSwapDrive = (config) => {
|
|
|
6169
6416
|
console.warn("[swap] the corridor restore could not drive", error);
|
|
6170
6417
|
}
|
|
6171
6418
|
}
|
|
6419
|
+
let offers2 = offer;
|
|
6172
6420
|
try {
|
|
6173
|
-
await
|
|
6421
|
+
offers2 = await restoreOfferDeposits();
|
|
6174
6422
|
} catch (error) {
|
|
6175
|
-
console.warn("[swap] the offer deposit
|
|
6423
|
+
console.warn("[swap] the offer deposit restore did not complete", error);
|
|
6176
6424
|
}
|
|
6177
6425
|
emitAll();
|
|
6178
|
-
if (mode === "auto" && await hasLiveWork(
|
|
6426
|
+
if (mode === "auto" && await hasLiveWork(offers2)) {
|
|
6179
6427
|
try {
|
|
6180
6428
|
await arm();
|
|
6181
6429
|
} catch (error) {
|
|
@@ -6203,7 +6451,7 @@ var createSwapDrive = (config) => {
|
|
|
6203
6451
|
}
|
|
6204
6452
|
return probe.call(wallet);
|
|
6205
6453
|
};
|
|
6206
|
-
const recoverableAt = async (script) => (await findLockupVtxos(
|
|
6454
|
+
const recoverableAt = async (script) => (await findLockupVtxos(lockupSource, script)).filter((vtxo) => vtxo.recoverable);
|
|
6207
6455
|
const recover = async (id) => {
|
|
6208
6456
|
if (disposed) throw new ClientDisposed("recover");
|
|
6209
6457
|
if (mode === "readonly") {
|
|
@@ -6253,7 +6501,7 @@ var createSwapDrive = (config) => {
|
|
|
6253
6501
|
);
|
|
6254
6502
|
}
|
|
6255
6503
|
const txid = await track((await recoverer()).recoverVtxos());
|
|
6256
|
-
await
|
|
6504
|
+
await restoreOfferDeposits(record.fundingTxid);
|
|
6257
6505
|
const after = records.get(id);
|
|
6258
6506
|
const recovered = after?.family === "offer" && after.status !== "recoverable";
|
|
6259
6507
|
return { recovered, txid, swap: swapView(id) };
|
|
@@ -6328,22 +6576,22 @@ var createSwapDrive = (config) => {
|
|
|
6328
6576
|
};
|
|
6329
6577
|
|
|
6330
6578
|
// src/client/quoteOffer.ts
|
|
6331
|
-
var
|
|
6579
|
+
var import_solver_discovery8 = require("@arkade-os/solver-discovery");
|
|
6332
6580
|
|
|
6333
6581
|
// src/client/market.ts
|
|
6334
|
-
var
|
|
6582
|
+
var import_solver_discovery7 = require("@arkade-os/solver-discovery");
|
|
6335
6583
|
var marketKeyOf = (card) => {
|
|
6336
|
-
const base = (0,
|
|
6337
|
-
const quote = (0,
|
|
6338
|
-
const baseIsArkade = (0,
|
|
6339
|
-
const quoteIsArkade = (0,
|
|
6584
|
+
const base = (0, import_solver_discovery7.marketLegKey)(card, "base");
|
|
6585
|
+
const quote = (0, import_solver_discovery7.marketLegKey)(card, "quote");
|
|
6586
|
+
const baseIsArkade = (0, import_solver_discovery7.marketCorridor)(card, "base") === "arkade";
|
|
6587
|
+
const quoteIsArkade = (0, import_solver_discovery7.marketCorridor)(card, "quote") === "arkade";
|
|
6340
6588
|
if (baseIsArkade !== quoteIsArkade) {
|
|
6341
6589
|
return baseIsArkade ? `${base}/${quote}` : `${quote}/${base}`;
|
|
6342
6590
|
}
|
|
6343
6591
|
return base <= quote ? `${base}/${quote}` : `${quote}/${base}`;
|
|
6344
6592
|
};
|
|
6345
|
-
var isAddressable = (card) => !(0,
|
|
6346
|
-
var marketBackendOf = (card) => (0,
|
|
6593
|
+
var isAddressable = (card) => !(0, import_solver_discovery7.isRfqMarket)(card) || typeof card.discovery_pubkey === "string" && (card.transports?.nostr?.relays?.length ?? 0) > 0;
|
|
6594
|
+
var marketBackendOf = (card) => (0, import_solver_discovery7.isRfqMarket)(card) ? "rfq" : "feed";
|
|
6347
6595
|
var usableMarkets = (snapshot, policy) => {
|
|
6348
6596
|
const allowed = policy?.allowedRegistries;
|
|
6349
6597
|
return (allowed ? snapshot.markets.filter((card) => allowed.includes(card.source)) : snapshot.markets).filter(isAddressable);
|
|
@@ -6361,12 +6609,12 @@ var eligibleMarkets = (snapshot, legs, policy, takeAmount) => {
|
|
|
6361
6609
|
wantSide: side === "base" ? "quote" : "base",
|
|
6362
6610
|
...takeAmount === void 0 ? {} : { wantAmount: takeAmount }
|
|
6363
6611
|
};
|
|
6364
|
-
const canonical = (0,
|
|
6612
|
+
const canonical = (0, import_solver_discovery7.selectMarkets)([...markets], {
|
|
6365
6613
|
baseId: base.marketId,
|
|
6366
6614
|
quoteId: quote.marketId,
|
|
6367
6615
|
...selection
|
|
6368
6616
|
});
|
|
6369
|
-
const legacy = (0,
|
|
6617
|
+
const legacy = (0, import_solver_discovery7.selectMarkets)([...markets], {
|
|
6370
6618
|
baseId: `${base.corridor}:${base.assetId}`,
|
|
6371
6619
|
quoteId: `${quote.corridor}:${quote.assetId}`,
|
|
6372
6620
|
...selection
|
|
@@ -6426,16 +6674,16 @@ var assembleRoute = (give, take) => {
|
|
|
6426
6674
|
}
|
|
6427
6675
|
return { give, take };
|
|
6428
6676
|
};
|
|
6429
|
-
var endpoint = (corridor,
|
|
6677
|
+
var endpoint = (corridor, asset6, instrument) => ({
|
|
6430
6678
|
corridor,
|
|
6431
|
-
asset:
|
|
6679
|
+
asset: asset6,
|
|
6432
6680
|
...instrument === void 0 ? {} : { instrument }
|
|
6433
6681
|
});
|
|
6434
|
-
var legFor = (
|
|
6435
|
-
const leg = toDiscoveryLeg(
|
|
6682
|
+
var legFor = (asset6, expected) => {
|
|
6683
|
+
const leg = toDiscoveryLeg(asset6);
|
|
6436
6684
|
if (expected !== void 0 && leg.corridor !== expected) {
|
|
6437
6685
|
throw new UnsupportedRoute(
|
|
6438
|
-
`${
|
|
6686
|
+
`${asset6} settles on ${leg.corridor}, and this leg is ${expected}`,
|
|
6439
6687
|
{ give: leg.corridor, take: expected }
|
|
6440
6688
|
);
|
|
6441
6689
|
}
|
|
@@ -6727,7 +6975,7 @@ var quoteFromFeed = async (input) => {
|
|
|
6727
6975
|
if (amount === void 0) {
|
|
6728
6976
|
throw new Error("an asset swap needs an amount and the side it pins");
|
|
6729
6977
|
}
|
|
6730
|
-
const plan = await (0,
|
|
6978
|
+
const plan = await (0, import_solver_discovery8.quoteOffer)(candidate.card, {
|
|
6731
6979
|
give: candidate.give,
|
|
6732
6980
|
...amount.on === "give" ? { giveAmount: amount.value } : { wantAmount: amount.value },
|
|
6733
6981
|
...QUOTE_OPTIONS,
|
|
@@ -6759,7 +7007,7 @@ var quoteFromFeed = async (input) => {
|
|
|
6759
7007
|
};
|
|
6760
7008
|
};
|
|
6761
7009
|
var spreadOf = (plan) => {
|
|
6762
|
-
const fair = (0,
|
|
7010
|
+
const fair = (0, import_solver_discovery8.computeWantAmount)({
|
|
6763
7011
|
deposit: plan.deposit.atomic,
|
|
6764
7012
|
give: plan.give,
|
|
6765
7013
|
price: plan.price,
|
|
@@ -6784,17 +7032,17 @@ var verifyPlanLegs = (plan, legs, give) => {
|
|
|
6784
7032
|
|
|
6785
7033
|
// src/client/quoteRfq.ts
|
|
6786
7034
|
var import_base18 = require("@scure/base");
|
|
6787
|
-
var
|
|
7035
|
+
var import_sdk27 = require("@arkade-os/sdk");
|
|
6788
7036
|
init_claimPacket();
|
|
6789
7037
|
init_onchainHtlc();
|
|
6790
7038
|
init_onchainHtlc();
|
|
6791
7039
|
|
|
6792
7040
|
// src/payment/onchainSwap.ts
|
|
6793
|
-
var
|
|
7041
|
+
var import_sdk26 = require("@arkade-os/sdk");
|
|
6794
7042
|
init_onchainHtlc();
|
|
6795
7043
|
|
|
6796
7044
|
// src/payment/swapRail.ts
|
|
6797
|
-
var
|
|
7045
|
+
var import_sdk25 = require("@arkade-os/sdk");
|
|
6798
7046
|
|
|
6799
7047
|
// src/client/sats.ts
|
|
6800
7048
|
var satsOf = (value, field) => {
|
|
@@ -6816,6 +7064,7 @@ init_rfq();
|
|
|
6816
7064
|
|
|
6817
7065
|
// src/client/rfqWire.ts
|
|
6818
7066
|
init_rfq();
|
|
7067
|
+
init_store();
|
|
6819
7068
|
var BTC_LEG = {
|
|
6820
7069
|
arkade: ARKADE_BTC,
|
|
6821
7070
|
lightning: LIGHTNING_BTC,
|
|
@@ -6845,14 +7094,14 @@ var parseRfqQuote = (quote) => ({
|
|
|
6845
7094
|
|
|
6846
7095
|
// src/client/quoteRfq.ts
|
|
6847
7096
|
var covenantInputs = (input) => {
|
|
6848
|
-
const network = (0,
|
|
7097
|
+
const network = (0, import_sdk27.networkFromArkadeInfo)(input.info);
|
|
6849
7098
|
const arkade2 = input.corridors.get("arkade").deps;
|
|
6850
7099
|
return {
|
|
6851
|
-
operatorPubkey: (0,
|
|
7100
|
+
operatorPubkey: (0, import_sdk27.toXOnly)(import_base18.hex.decode(input.info.signerPubkey), "ark signer key"),
|
|
6852
7101
|
// The per-network pin the arkade module already resolved, never a key
|
|
6853
7102
|
// the operator reports about itself: this one ends up in a covenant leaf
|
|
6854
7103
|
// that decides who can move the funds.
|
|
6855
|
-
emulatorPubkey: (0,
|
|
7104
|
+
emulatorPubkey: (0, import_sdk27.toXOnly)(import_base18.hex.decode(arkade2.emulatorPubkey), "emulator signer key"),
|
|
6856
7105
|
claimDelay: unilateralClaimDelay(Number(input.info.unilateralExitDelay)),
|
|
6857
7106
|
hrp: network.hrp
|
|
6858
7107
|
};
|
|
@@ -6896,7 +7145,7 @@ var quoteLightningSend = async (input) => {
|
|
|
6896
7145
|
}
|
|
6897
7146
|
const pair = rfqPairFor(input.legs.give, input.legs.take);
|
|
6898
7147
|
const rfqId = newRfqId();
|
|
6899
|
-
const secrets = await (0,
|
|
7148
|
+
const secrets = await (0, import_sdk27.provisionRefundKey)(input.wallet);
|
|
6900
7149
|
const wire = await input.transport.requestQuote(
|
|
6901
7150
|
lightningSendRequest({
|
|
6902
7151
|
rfqId,
|
|
@@ -6969,7 +7218,7 @@ var quoteLightningReceive = async (input) => {
|
|
|
6969
7218
|
const pinned = pinnedFor(input);
|
|
6970
7219
|
const pair = rfqPairFor(input.legs.give, input.legs.take);
|
|
6971
7220
|
const rfqId = newRfqId();
|
|
6972
|
-
const secrets = await (0,
|
|
7221
|
+
const secrets = await (0, import_sdk27.provisionClaimSecret)(input.wallet);
|
|
6973
7222
|
const paymentHash = import_base18.hex.encode(secrets.paymentHash);
|
|
6974
7223
|
const payoutAddress = await input.wallet.getAddress();
|
|
6975
7224
|
const lightning = input.corridors.get("lightning").deps;
|
|
@@ -7092,8 +7341,8 @@ var quoteOnchainSend = async (input) => {
|
|
|
7092
7341
|
claimVsize: onchain.claimVsize
|
|
7093
7342
|
}) : void 0;
|
|
7094
7343
|
const quoted = claimFee === void 0 ? pinned : { on: pinned.on, value: pinned.value + claimFee, source: pinned.source };
|
|
7095
|
-
const secrets = await (0,
|
|
7096
|
-
const payoutKey = await (0,
|
|
7344
|
+
const secrets = await (0, import_sdk27.provisionClaimSecret)(input.wallet);
|
|
7345
|
+
const payoutKey = await (0, import_sdk27.provisionRefundKey)(input.wallet);
|
|
7097
7346
|
const paymentHash = import_base18.hex.encode(secrets.paymentHash);
|
|
7098
7347
|
const wire = await input.transport.requestQuote(
|
|
7099
7348
|
withCanonicalAmount(
|
|
@@ -7189,6 +7438,10 @@ var quoteOnchainSend = async (input) => {
|
|
|
7189
7438
|
secrets,
|
|
7190
7439
|
refundAddress: payoutKey.address,
|
|
7191
7440
|
fundAmount: parsed.give,
|
|
7441
|
+
// The gross wire leg, not `reportedTake`: the claim fee comes out of
|
|
7442
|
+
// the HTLC output, so netting it here would accept an HTLC funded
|
|
7443
|
+
// short by exactly that fee.
|
|
7444
|
+
expectedAmount: parsed.take,
|
|
7192
7445
|
payoutKey,
|
|
7193
7446
|
payoutPkScript,
|
|
7194
7447
|
htlc: derived.htlc,
|
|
@@ -7200,7 +7453,7 @@ var quoteOnchainSend = async (input) => {
|
|
|
7200
7453
|
};
|
|
7201
7454
|
|
|
7202
7455
|
// src/client/verbs.ts
|
|
7203
|
-
var
|
|
7456
|
+
var import_sdk28 = require("@arkade-os/sdk");
|
|
7204
7457
|
var enforceFeeCeiling = (quote, call, policy) => {
|
|
7205
7458
|
let ceiling;
|
|
7206
7459
|
for (const [source, limit] of [
|
|
@@ -7225,9 +7478,9 @@ var settle = async (deps, input, maxFee) => {
|
|
|
7225
7478
|
return deps.accept(quote);
|
|
7226
7479
|
};
|
|
7227
7480
|
var pay = async (deps, destination, options = {}) => {
|
|
7228
|
-
const arkade2 = (0,
|
|
7481
|
+
const arkade2 = (0, import_sdk28.arkTarget)(destination);
|
|
7229
7482
|
if (arkade2 !== void 0) {
|
|
7230
|
-
const amount = (0,
|
|
7483
|
+
const amount = (0, import_sdk28.resolveSendAmount)(
|
|
7231
7484
|
"ark",
|
|
7232
7485
|
destination,
|
|
7233
7486
|
options.amount === void 0 ? void 0 : satsOf(options.amount, "amount")
|
|
@@ -7310,6 +7563,7 @@ var createSwapClient = (config) => {
|
|
|
7310
7563
|
operator,
|
|
7311
7564
|
...config.repository === void 0 ? {} : { repository: config.repository },
|
|
7312
7565
|
corridors: async () => (await resolved()).corridors,
|
|
7566
|
+
network: async () => (await resolved()).base.networkName,
|
|
7313
7567
|
...config.policy?.drive === void 0 ? {} : { mode: config.policy.drive },
|
|
7314
7568
|
indexer
|
|
7315
7569
|
});
|
|
@@ -7547,7 +7801,7 @@ var createSwapClient = (config) => {
|
|
|
7547
7801
|
var import_base20 = require("@scure/base");
|
|
7548
7802
|
var import_legacy4 = require("@noble/hashes/legacy.js");
|
|
7549
7803
|
var import_sha26 = require("@noble/hashes/sha2.js");
|
|
7550
|
-
var
|
|
7804
|
+
var import_sdk29 = require("@arkade-os/sdk");
|
|
7551
7805
|
init_claimPacket();
|
|
7552
7806
|
var CovclaimdRevealError = class extends Error {
|
|
7553
7807
|
name = "CovclaimdRevealError";
|
|
@@ -7652,7 +7906,7 @@ function revealFieldsFromScript(script) {
|
|
|
7652
7906
|
}
|
|
7653
7907
|
async function revealClaimPacket(client, input) {
|
|
7654
7908
|
const fields = revealFieldsFromScript(input.script);
|
|
7655
|
-
if (import_base20.hex.encode(
|
|
7909
|
+
if (import_base20.hex.encode(import_sdk29.ArkAddress.decode(input.address).pkScript) !== import_base20.hex.encode(input.script.pkScript)) {
|
|
7656
7910
|
throw new Error("address does not belong to this covenant");
|
|
7657
7911
|
}
|
|
7658
7912
|
if (import_base20.hex.encode((0, import_legacy4.ripemd160)((0, import_sha26.sha256)(input.preimage))) !== import_base20.hex.encode(input.script.options.preimageHash)) {
|
|
@@ -7743,6 +7997,7 @@ async function revealClaimPacket(client, input) {
|
|
|
7743
7997
|
esploraTip,
|
|
7744
7998
|
exchange,
|
|
7745
7999
|
familyOfSwapId,
|
|
8000
|
+
fateMoved,
|
|
7746
8001
|
feedFetch,
|
|
7747
8002
|
formatAssetId,
|
|
7748
8003
|
fromAtomicDecimal,
|
|
@@ -7790,6 +8045,7 @@ async function revealClaimPacket(client, input) {
|
|
|
7790
8045
|
resolveCorridorBase,
|
|
7791
8046
|
resolveCorridorDeps,
|
|
7792
8047
|
resolveRoute,
|
|
8048
|
+
restoredOfferRecord,
|
|
7793
8049
|
revealClaimPacket,
|
|
7794
8050
|
revealFieldsFromScript,
|
|
7795
8051
|
rfqLeg,
|
|
@@ -7817,6 +8073,6 @@ async function revealClaimPacket(client, input) {
|
|
|
7817
8073
|
verifyingDerivation,
|
|
7818
8074
|
walletLockupIndexer,
|
|
7819
8075
|
withCanonicalAmount,
|
|
7820
|
-
|
|
8076
|
+
withDepositFate,
|
|
7821
8077
|
withRfqState
|
|
7822
8078
|
});
|