@arkade-os/swap 0.0.17 → 0.0.19
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 +1102 -1101
- package/dist/{chunk-IRDBAAJZ.js → chunk-D7GDHZCG.js} +1040 -96
- package/dist/index.cjs +223 -38
- package/dist/index.d.cts +16 -8
- package/dist/index.d.ts +16 -8
- package/dist/index.js +156 -862
- package/dist/nostr.cjs +108 -3
- package/dist/nostr.d.cts +1 -1
- package/dist/nostr.d.ts +1 -1
- package/dist/nostr.js +1 -1
- 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-DH5zjdw4.d.ts → repository-B6VPDGHC.d.ts} +4 -1
- package/dist/{repository-rwU1Sv2-.d.cts → repository-BXIcmIIC.d.cts} +4 -1
- package/dist/{rfq-DirJn_4k.d.ts → rfq-D96aLvCA.d.cts} +123 -10
- package/dist/{rfq-DirJn_4k.d.cts → rfq-D96aLvCA.d.ts} +123 -10
- package/package.json +3 -3
|
@@ -1,6 +1,775 @@
|
|
|
1
|
-
// src/
|
|
1
|
+
// src/coverage.ts
|
|
2
|
+
var RETIRABLE = ["fulfilled", "cancelled"];
|
|
3
|
+
var issuedAt = /* @__PURE__ */ new Map();
|
|
4
|
+
var inFlight = /* @__PURE__ */ new Map();
|
|
5
|
+
async function serialize(script, task) {
|
|
6
|
+
const previous = inFlight.get(script) ?? Promise.resolve();
|
|
7
|
+
const result = previous.then(task, task);
|
|
8
|
+
const settled = result.then(
|
|
9
|
+
() => {
|
|
10
|
+
},
|
|
11
|
+
() => {
|
|
12
|
+
}
|
|
13
|
+
);
|
|
14
|
+
inFlight.set(script, settled);
|
|
15
|
+
void settled.then(() => {
|
|
16
|
+
if (inFlight.get(script) === settled) inFlight.delete(script);
|
|
17
|
+
});
|
|
18
|
+
return result;
|
|
19
|
+
}
|
|
20
|
+
function addressOutstanding(swaps, script) {
|
|
21
|
+
const issued = issuedAt.get(script);
|
|
22
|
+
if (issued === void 0) return false;
|
|
23
|
+
if (swaps.some((s) => s.swapPkScript === script && s.createdAt >= issued)) {
|
|
24
|
+
issuedAt.delete(script);
|
|
25
|
+
return false;
|
|
26
|
+
}
|
|
27
|
+
return true;
|
|
28
|
+
}
|
|
29
|
+
async function promoteOfferContract(manager, script, issued = Date.now()) {
|
|
30
|
+
await serialize(script, async () => {
|
|
31
|
+
await manager.setContractWatchState(script, "watched");
|
|
32
|
+
issuedAt.set(script, issued);
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
async function retireOfferContract(manager, swaps, script) {
|
|
36
|
+
await serialize(script, async () => {
|
|
37
|
+
if (swaps.some((s) => s.swapPkScript === script && !RETIRABLE.includes(s.status))) return;
|
|
38
|
+
if (addressOutstanding(swaps, script)) return;
|
|
39
|
+
try {
|
|
40
|
+
await manager.setContractWatchState(script, "retained");
|
|
41
|
+
} catch (err) {
|
|
42
|
+
console.warn(`[swap] could not retire offer contract ${script}`, err);
|
|
43
|
+
}
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
async function retireSettledOfferContracts(manager, swaps) {
|
|
47
|
+
const settled = new Set(
|
|
48
|
+
swaps.filter((s) => RETIRABLE.includes(s.status)).map((s) => s.swapPkScript)
|
|
49
|
+
);
|
|
50
|
+
for (const script of settled) await retireOfferContract(manager, swaps, script);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// src/store.ts
|
|
2
54
|
import { hex } from "@scure/base";
|
|
3
55
|
import { sha256 } from "@noble/hashes/sha2.js";
|
|
56
|
+
import { contractPreimage } from "@arkade-os/sdk";
|
|
57
|
+
var BTC_ASSET_ID = "btc";
|
|
58
|
+
var byNewest = (a, b) => b.createdAt - a.createdAt;
|
|
59
|
+
var getAssetSwapsOrThrow = async (repository) => {
|
|
60
|
+
return (await repository.getAllSwaps()).filter(
|
|
61
|
+
(s) => s && typeof s.id === "string" && // offer swaps carry the TLV; onchain-corridor swaps carry
|
|
62
|
+
// the payment hash instead — either marks a valid record
|
|
63
|
+
(typeof s.offerHex === "string" || typeof s.paymentHash === "string")
|
|
64
|
+
).sort(byNewest);
|
|
65
|
+
};
|
|
66
|
+
var getAssetSwaps = async (repository) => {
|
|
67
|
+
try {
|
|
68
|
+
return await getAssetSwapsOrThrow(repository);
|
|
69
|
+
} catch {
|
|
70
|
+
return [];
|
|
71
|
+
}
|
|
72
|
+
};
|
|
73
|
+
var saveSwapOrThrow = async (repository, swap) => {
|
|
74
|
+
try {
|
|
75
|
+
await repository.saveSwap(swap);
|
|
76
|
+
} catch (error) {
|
|
77
|
+
const reason = error instanceof Error ? error.message : String(error);
|
|
78
|
+
throw new Error(`failed to save swap ${swap.id}: ${reason}`);
|
|
79
|
+
}
|
|
80
|
+
};
|
|
81
|
+
var addAssetSwap = async (repository, swap) => {
|
|
82
|
+
const swaps = await getAssetSwapsOrThrow(repository);
|
|
83
|
+
if (swaps.some((s) => s.id === swap.id)) return swaps;
|
|
84
|
+
await saveSwapOrThrow(repository, swap);
|
|
85
|
+
const at = swaps.findIndex((s) => byNewest(swap, s) <= 0);
|
|
86
|
+
const merged = [...swaps];
|
|
87
|
+
merged.splice(at === -1 ? merged.length : at, 0, swap);
|
|
88
|
+
return merged;
|
|
89
|
+
};
|
|
90
|
+
var updateAssetSwap = async (repository, id, changes) => {
|
|
91
|
+
const swaps = (await getAssetSwapsOrThrow(repository)).map(
|
|
92
|
+
(s) => s.id === id ? { ...s, ...changes } : s
|
|
93
|
+
);
|
|
94
|
+
const updated = swaps.find((s) => s.id === id);
|
|
95
|
+
if (updated) await saveSwapOrThrow(repository, updated);
|
|
96
|
+
return swaps;
|
|
97
|
+
};
|
|
98
|
+
var updateAssetSwapBestEffort = async (repository, id, changes) => {
|
|
99
|
+
try {
|
|
100
|
+
return { swaps: await updateAssetSwap(repository, id, changes), persisted: true };
|
|
101
|
+
} catch (error) {
|
|
102
|
+
console.warn(`[swap] failed to persist update for swap ${id}`, error);
|
|
103
|
+
const swaps = (await getAssetSwaps(repository)).map(
|
|
104
|
+
(s) => s.id === id ? { ...s, ...changes } : s
|
|
105
|
+
);
|
|
106
|
+
return { swaps, persisted: false };
|
|
107
|
+
}
|
|
108
|
+
};
|
|
109
|
+
var swapSecretsToRecord = (secrets) => ({
|
|
110
|
+
signingDescriptor: secrets.descriptor,
|
|
111
|
+
..."mustPersistPreimage" in secrets && secrets.mustPersistPreimage ? { preimageHex: hex.encode(secrets.preimage) } : {},
|
|
112
|
+
..."preimageSalt" in secrets && secrets.preimageSalt ? { preimageSaltHex: hex.encode(secrets.preimageSalt) } : {}
|
|
113
|
+
});
|
|
114
|
+
var decodeHex32 = (value, field) => {
|
|
115
|
+
const bytes = hex.decode(value);
|
|
116
|
+
if (bytes.length !== 32) {
|
|
117
|
+
throw new Error(`${field} must be 32 bytes, got ${bytes.length}`);
|
|
118
|
+
}
|
|
119
|
+
return bytes;
|
|
120
|
+
};
|
|
121
|
+
var PreimageNotRecoverableError = class extends Error {
|
|
122
|
+
constructor(reason, message, options) {
|
|
123
|
+
super(message, options);
|
|
124
|
+
this.reason = reason;
|
|
125
|
+
}
|
|
126
|
+
reason;
|
|
127
|
+
name = "PreimageNotRecoverableError";
|
|
128
|
+
};
|
|
129
|
+
var preimageForSwapRecord = async (wallet, record) => {
|
|
130
|
+
if (!record.signingDescriptor) {
|
|
131
|
+
throw new PreimageNotRecoverableError(
|
|
132
|
+
"no-secrets",
|
|
133
|
+
"this swap record carries no signing descriptor"
|
|
134
|
+
);
|
|
135
|
+
}
|
|
136
|
+
let stored;
|
|
137
|
+
let salt;
|
|
138
|
+
try {
|
|
139
|
+
stored = record.preimageHex ? decodeHex32(record.preimageHex, "preimageHex") : void 0;
|
|
140
|
+
salt = record.preimageSaltHex ? decodeHex32(record.preimageSaltHex, "preimageSaltHex") : void 0;
|
|
141
|
+
} catch (cause) {
|
|
142
|
+
throw new PreimageNotRecoverableError(
|
|
143
|
+
"malformed-record",
|
|
144
|
+
`this swap record's secrets projection is unreadable: ${String(cause)}`,
|
|
145
|
+
{ cause }
|
|
146
|
+
);
|
|
147
|
+
}
|
|
148
|
+
let preimage;
|
|
149
|
+
try {
|
|
150
|
+
preimage = await contractPreimage(wallet, record.signingDescriptor, { stored, salt });
|
|
151
|
+
} catch (cause) {
|
|
152
|
+
throw new PreimageNotRecoverableError(
|
|
153
|
+
"not-derivable",
|
|
154
|
+
`this wallet cannot produce the preimage for ${record.signingDescriptor}`,
|
|
155
|
+
{ cause }
|
|
156
|
+
);
|
|
157
|
+
}
|
|
158
|
+
if (record.paymentHash && hex.encode(sha256(preimage)) !== record.paymentHash.toLowerCase()) {
|
|
159
|
+
throw new PreimageNotRecoverableError(
|
|
160
|
+
"hash-mismatch",
|
|
161
|
+
"the derived preimage does not match this swap's payment hash: wrong wallet, or a tampered salt"
|
|
162
|
+
);
|
|
163
|
+
}
|
|
164
|
+
return preimage;
|
|
165
|
+
};
|
|
166
|
+
|
|
167
|
+
// src/offer.ts
|
|
168
|
+
import { hex as hex2 } from "@scure/base";
|
|
169
|
+
import { concatBytes } from "@scure/btc-signer/utils.js";
|
|
170
|
+
import {
|
|
171
|
+
ASSET_CARRIER_SATS as SDK_ASSET_CARRIER_SATS,
|
|
172
|
+
ArkAddress,
|
|
173
|
+
RestArkProvider,
|
|
174
|
+
RestEmulatorProvider,
|
|
175
|
+
RestIndexerProvider,
|
|
176
|
+
arkade,
|
|
177
|
+
asset,
|
|
178
|
+
getNetwork,
|
|
179
|
+
resolveEmulatorPubkey,
|
|
180
|
+
toXOnlySignerHex
|
|
181
|
+
} from "@arkade-os/sdk";
|
|
182
|
+
|
|
183
|
+
// src/swap-want-asset.program.json
|
|
184
|
+
var swap_want_asset_program_default = {
|
|
185
|
+
version: 0,
|
|
186
|
+
name: "banco-btc-to-asset",
|
|
187
|
+
params: [
|
|
188
|
+
{ name: "makerWP", type: "pubkey" },
|
|
189
|
+
{ name: "wantAmount", type: "int" },
|
|
190
|
+
{ name: "wantAssetTxid", type: "hash" },
|
|
191
|
+
{ name: "wantAssetGroupIndex", type: "int" },
|
|
192
|
+
{ name: "server", type: "pubkey" },
|
|
193
|
+
{ name: "user", type: "pubkey" }
|
|
194
|
+
],
|
|
195
|
+
functions: {
|
|
196
|
+
fulfill: {
|
|
197
|
+
tapscript: { signers: ["$server"] },
|
|
198
|
+
arkadeScript: {
|
|
199
|
+
asm: [
|
|
200
|
+
0,
|
|
201
|
+
"$wantAssetTxid",
|
|
202
|
+
"$wantAssetGroupIndex",
|
|
203
|
+
"INSPECTOUTASSETLOOKUP",
|
|
204
|
+
"VERIFY",
|
|
205
|
+
"$wantAmount",
|
|
206
|
+
"GREATERTHANOREQUAL",
|
|
207
|
+
"VERIFY",
|
|
208
|
+
0,
|
|
209
|
+
"INSPECTOUTPUTSCRIPTPUBKEY",
|
|
210
|
+
1,
|
|
211
|
+
"EQUALVERIFY",
|
|
212
|
+
"$makerWP",
|
|
213
|
+
"EQUAL"
|
|
214
|
+
]
|
|
215
|
+
}
|
|
216
|
+
},
|
|
217
|
+
cancel: {
|
|
218
|
+
tapscript: { signers: ["$user", "$server"] }
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
};
|
|
222
|
+
|
|
223
|
+
// src/swap-want-btc.program.json
|
|
224
|
+
var swap_want_btc_program_default = {
|
|
225
|
+
version: 0,
|
|
226
|
+
name: "banco-asset-to-btc",
|
|
227
|
+
params: [
|
|
228
|
+
{ name: "makerWP", type: "pubkey" },
|
|
229
|
+
{ name: "wantAmount", type: "int" },
|
|
230
|
+
{ name: "server", type: "pubkey" },
|
|
231
|
+
{ name: "user", type: "pubkey" }
|
|
232
|
+
],
|
|
233
|
+
functions: {
|
|
234
|
+
fulfill: {
|
|
235
|
+
tapscript: { signers: ["$server"] },
|
|
236
|
+
arkadeScript: {
|
|
237
|
+
asm: [
|
|
238
|
+
0,
|
|
239
|
+
"INSPECTOUTPUTVALUE",
|
|
240
|
+
"$wantAmount",
|
|
241
|
+
"GREATERTHANOREQUAL",
|
|
242
|
+
"VERIFY",
|
|
243
|
+
0,
|
|
244
|
+
"INSPECTOUTPUTSCRIPTPUBKEY",
|
|
245
|
+
1,
|
|
246
|
+
"EQUALVERIFY",
|
|
247
|
+
"$makerWP",
|
|
248
|
+
"EQUAL"
|
|
249
|
+
]
|
|
250
|
+
}
|
|
251
|
+
},
|
|
252
|
+
cancel: {
|
|
253
|
+
tapscript: { signers: ["$user", "$server"] }
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
};
|
|
257
|
+
|
|
258
|
+
// src/offer.ts
|
|
259
|
+
var swapPrograms = {
|
|
260
|
+
wantAsset: arkade.parseArtifact(swap_want_asset_program_default),
|
|
261
|
+
wantBtc: arkade.parseArtifact(swap_want_btc_program_default)
|
|
262
|
+
};
|
|
263
|
+
function withExitClosure(program, exit) {
|
|
264
|
+
if (!exit) return program;
|
|
265
|
+
return {
|
|
266
|
+
...program,
|
|
267
|
+
// typed params are authoritative: an undeclared `$exitDelay` fails
|
|
268
|
+
// validateProgram instead of compiling against an unbound value
|
|
269
|
+
params: [...program.params ?? [], { name: "exitDelay", type: "int" }],
|
|
270
|
+
functions: {
|
|
271
|
+
...program.functions,
|
|
272
|
+
exit: {
|
|
273
|
+
tapscript: { signers: ["$user"], csv: { type: exit.type, value: "$exitDelay" } }
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
};
|
|
277
|
+
}
|
|
278
|
+
function swapProgramBinding(offer, serverPubkey) {
|
|
279
|
+
if (offer.makerPkScript.length !== FIELDS.makerPkScript.width) {
|
|
280
|
+
throw new Error("makerPkScript is not a 34-byte taproot scriptPubKey");
|
|
281
|
+
}
|
|
282
|
+
return {
|
|
283
|
+
program: withExitClosure(
|
|
284
|
+
offer.wantAsset ? swapPrograms.wantAsset : swapPrograms.wantBtc,
|
|
285
|
+
offer.exitDelay
|
|
286
|
+
),
|
|
287
|
+
args: {
|
|
288
|
+
makerWP: offer.makerPkScript.subarray(2),
|
|
289
|
+
wantAmount: offer.wantAmount,
|
|
290
|
+
server: serverPubkey,
|
|
291
|
+
user: offer.makerPublicKey,
|
|
292
|
+
// internal byte order
|
|
293
|
+
...offer.wantAsset && {
|
|
294
|
+
wantAssetTxid: offer.wantAsset.txid.slice().reverse(),
|
|
295
|
+
wantAssetGroupIndex: offer.wantAsset.groupIndex
|
|
296
|
+
},
|
|
297
|
+
...offer.exitDelay && { exitDelay: offer.exitDelay.value }
|
|
298
|
+
},
|
|
299
|
+
keys: {
|
|
300
|
+
serverKey: serverPubkey,
|
|
301
|
+
userKey: offer.makerPublicKey,
|
|
302
|
+
emulatorKey: offer.emulatorPubkey
|
|
303
|
+
}
|
|
304
|
+
};
|
|
305
|
+
}
|
|
306
|
+
function offerVtxoScript(offer, serverPubkey) {
|
|
307
|
+
const { program, args, keys } = swapProgramBinding(offer, serverPubkey);
|
|
308
|
+
return new arkade.ArkadeProgramScript(program, args, keys);
|
|
309
|
+
}
|
|
310
|
+
var OFFER_PACKET_TYPE = 3;
|
|
311
|
+
var FIELDS = {
|
|
312
|
+
swapPkScript: { tag: 1, width: 34 },
|
|
313
|
+
wantAmount: { tag: 2, width: 8 },
|
|
314
|
+
wantAsset: { tag: 3, width: void 0 },
|
|
315
|
+
makerPkScript: { tag: 5, width: 34 },
|
|
316
|
+
makerPublicKey: { tag: 7, width: 32 },
|
|
317
|
+
emulatorPubkey: { tag: 8, width: 32 },
|
|
318
|
+
ratioNum: { tag: 9, width: 8 },
|
|
319
|
+
ratioDen: { tag: 10, width: 8 },
|
|
320
|
+
offerAsset: { tag: 11, width: void 0 },
|
|
321
|
+
exitTimelock: { tag: 12, width: 9 }
|
|
322
|
+
};
|
|
323
|
+
var EXIT_TYPES = ["blocks", "seconds"];
|
|
324
|
+
var NAMES = Object.fromEntries(Object.entries(FIELDS).map(([k, f]) => [f.tag, k]));
|
|
325
|
+
function u64(name, value) {
|
|
326
|
+
if (value < BigInt(0) || value >> BigInt(64) > BigInt(0)) {
|
|
327
|
+
throw new Error(`${name} does not fit the offer wire format (u64)`);
|
|
328
|
+
}
|
|
329
|
+
const out = new Uint8Array(8);
|
|
330
|
+
new DataView(out.buffer).setBigUint64(0, value, false);
|
|
331
|
+
return out;
|
|
332
|
+
}
|
|
333
|
+
var readU64 = (value) => new DataView(value.buffer, value.byteOffset).getBigUint64(0, false);
|
|
334
|
+
var setRatio = (name, value) => {
|
|
335
|
+
if (value === void 0 || value === BigInt(0)) return void 0;
|
|
336
|
+
if (value < BigInt(0)) throw new Error(`${name} does not fit the offer wire format (u64)`);
|
|
337
|
+
return value;
|
|
338
|
+
};
|
|
339
|
+
function tlv(type, value) {
|
|
340
|
+
if (value.length > 65535) throw new Error("TLV value exceeds the u16 length field");
|
|
341
|
+
return concatBytes(Uint8Array.of(type, value.length >> 8 & 255, value.length & 255), value);
|
|
342
|
+
}
|
|
343
|
+
function encodeOffer(offer) {
|
|
344
|
+
if (Boolean(offer.wantAsset) === Boolean(offer.offerAsset)) {
|
|
345
|
+
throw new Error("offer must carry exactly one of wantAsset or offerAsset");
|
|
346
|
+
}
|
|
347
|
+
for (const name of [
|
|
348
|
+
"swapPkScript",
|
|
349
|
+
"makerPkScript",
|
|
350
|
+
"makerPublicKey",
|
|
351
|
+
"emulatorPubkey"
|
|
352
|
+
]) {
|
|
353
|
+
if (offer[name].length !== FIELDS[name].width) {
|
|
354
|
+
throw new Error(`${name} must be ${FIELDS[name].width} bytes`);
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
const ratioNum = setRatio("ratioNum", offer.ratioNum);
|
|
358
|
+
const ratioDen = setRatio("ratioDen", offer.ratioDen);
|
|
359
|
+
if (ratioNum === void 0 !== (ratioDen === void 0)) {
|
|
360
|
+
throw new Error("offer must carry both ratioNum and ratioDen, or neither");
|
|
361
|
+
}
|
|
362
|
+
const recs = [
|
|
363
|
+
tlv(FIELDS.swapPkScript.tag, offer.swapPkScript),
|
|
364
|
+
tlv(FIELDS.wantAmount.tag, u64("wantAmount", offer.wantAmount))
|
|
365
|
+
];
|
|
366
|
+
if (offer.wantAsset) recs.push(tlv(FIELDS.wantAsset.tag, offer.wantAsset.serialize()));
|
|
367
|
+
if (ratioNum !== void 0) recs.push(tlv(FIELDS.ratioNum.tag, u64("ratioNum", ratioNum)));
|
|
368
|
+
if (ratioDen !== void 0) recs.push(tlv(FIELDS.ratioDen.tag, u64("ratioDen", ratioDen)));
|
|
369
|
+
if (offer.offerAsset) recs.push(tlv(FIELDS.offerAsset.tag, offer.offerAsset.serialize()));
|
|
370
|
+
recs.push(
|
|
371
|
+
tlv(FIELDS.makerPkScript.tag, offer.makerPkScript),
|
|
372
|
+
tlv(FIELDS.makerPublicKey.tag, offer.makerPublicKey),
|
|
373
|
+
tlv(FIELDS.emulatorPubkey.tag, offer.emulatorPubkey)
|
|
374
|
+
);
|
|
375
|
+
if (offer.exitDelay) recs.push(tlv(FIELDS.exitTimelock.tag, encodeExitDelay(offer.exitDelay)));
|
|
376
|
+
return concatBytes(...recs);
|
|
377
|
+
}
|
|
378
|
+
function encodeExitDelay(exit) {
|
|
379
|
+
return concatBytes(
|
|
380
|
+
Uint8Array.of(EXIT_TYPES.indexOf(assertExitDelay(exit).type)),
|
|
381
|
+
u64("exitDelay", exit.value)
|
|
382
|
+
);
|
|
383
|
+
}
|
|
384
|
+
function assertExitDelay(exit) {
|
|
385
|
+
if (EXIT_TYPES.indexOf(exit.type) < 0) {
|
|
386
|
+
throw new Error(`unknown exitDelay locktime type: ${exit.type}`);
|
|
387
|
+
}
|
|
388
|
+
if (exit.value <= BigInt(0)) {
|
|
389
|
+
throw new Error("exitDelay must be a positive relative locktime");
|
|
390
|
+
}
|
|
391
|
+
if (exit.value >> BigInt(32) > BigInt(0)) {
|
|
392
|
+
throw new Error("exitDelay does not fit the locktime field (u32)");
|
|
393
|
+
}
|
|
394
|
+
return exit;
|
|
395
|
+
}
|
|
396
|
+
function decodeOffer(data) {
|
|
397
|
+
const fields = {};
|
|
398
|
+
let off = 0;
|
|
399
|
+
while (off < data.length) {
|
|
400
|
+
if (off + 3 > data.length) throw new Error("truncated TLV header");
|
|
401
|
+
const type = data[off];
|
|
402
|
+
const length = data[off + 1] << 8 | data[off + 2];
|
|
403
|
+
off += 3;
|
|
404
|
+
if (off + length > data.length)
|
|
405
|
+
throw new Error(`truncated TLV value for type 0x${type.toString(16)}`);
|
|
406
|
+
const name = NAMES[type];
|
|
407
|
+
if (!name) throw new Error(`unknown TLV type: 0x${type.toString(16)}`);
|
|
408
|
+
if (fields[name] !== void 0) throw new Error(`duplicate TLV record: ${name}`);
|
|
409
|
+
fields[name] = data.slice(off, off + length);
|
|
410
|
+
off += length;
|
|
411
|
+
}
|
|
412
|
+
for (const name of ["wantAsset", "offerAsset"]) {
|
|
413
|
+
if (fields[name]?.length === 0) throw new Error(`missing/invalid ${name}`);
|
|
414
|
+
}
|
|
415
|
+
for (const [name, value] of Object.entries(fields)) {
|
|
416
|
+
const width = FIELDS[name].width;
|
|
417
|
+
if (width !== void 0 && value.length !== width) {
|
|
418
|
+
throw new Error(`missing/invalid ${name}`);
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
const need = (name) => {
|
|
422
|
+
const v = fields[name];
|
|
423
|
+
const len = FIELDS[name].width;
|
|
424
|
+
if (!v || len !== void 0 && v.length !== len)
|
|
425
|
+
throw new Error(`missing/invalid ${name}`);
|
|
426
|
+
return v;
|
|
427
|
+
};
|
|
428
|
+
const amount = need("wantAmount");
|
|
429
|
+
if (Boolean(fields.wantAsset) === Boolean(fields.offerAsset)) {
|
|
430
|
+
throw new Error("offer must carry exactly one of wantAsset or offerAsset");
|
|
431
|
+
}
|
|
432
|
+
const readRatio = (name) => {
|
|
433
|
+
const raw = fields[name];
|
|
434
|
+
if (!raw) return void 0;
|
|
435
|
+
const value = readU64(raw);
|
|
436
|
+
if (value === BigInt(0)) throw new Error(`missing/invalid ${name}`);
|
|
437
|
+
return value;
|
|
438
|
+
};
|
|
439
|
+
const ratioNum = readRatio("ratioNum");
|
|
440
|
+
const ratioDen = readRatio("ratioDen");
|
|
441
|
+
if (ratioNum === void 0 !== (ratioDen === void 0)) {
|
|
442
|
+
throw new Error("offer must carry both ratioNum and ratioDen, or neither");
|
|
443
|
+
}
|
|
444
|
+
return {
|
|
445
|
+
swapPkScript: need("swapPkScript"),
|
|
446
|
+
wantAmount: readU64(amount),
|
|
447
|
+
...fields.wantAsset && { wantAsset: asset.AssetId.fromBytes(fields.wantAsset) },
|
|
448
|
+
...fields.offerAsset && { offerAsset: asset.AssetId.fromBytes(fields.offerAsset) },
|
|
449
|
+
makerPkScript: need("makerPkScript"),
|
|
450
|
+
makerPublicKey: need("makerPublicKey"),
|
|
451
|
+
emulatorPubkey: need("emulatorPubkey"),
|
|
452
|
+
...ratioNum !== void 0 && { ratioNum },
|
|
453
|
+
...ratioDen !== void 0 && { ratioDen },
|
|
454
|
+
...fields.exitTimelock && { exitDelay: decodeExitDelay(fields.exitTimelock) }
|
|
455
|
+
};
|
|
456
|
+
}
|
|
457
|
+
function decodeExitDelay(value) {
|
|
458
|
+
const type = EXIT_TYPES[value[0]];
|
|
459
|
+
if (!type) throw new Error(`unknown exitDelay locktime type: 0x${value[0].toString(16)}`);
|
|
460
|
+
return { type, value: readU64(value.subarray(1)) };
|
|
461
|
+
}
|
|
462
|
+
var OFFER_CONTRACT_LABEL = "Arkade swap offer";
|
|
463
|
+
var OFFER_CONTRACT_KIND = "asset-swap-offer";
|
|
464
|
+
async function registerOfferContract(wallet, arkServerUrl, network, binding, serverPubkey, expectedPkScript, opts = {}) {
|
|
465
|
+
const contractManager = opts.contractManager ?? await wallet.getContractManager();
|
|
466
|
+
const client = opts.client ?? await arkade.Arkade.connect({
|
|
467
|
+
arkade: new RestArkProvider(arkServerUrl),
|
|
468
|
+
indexer: new RestIndexerProvider(arkServerUrl),
|
|
469
|
+
identity: wallet.identity,
|
|
470
|
+
// without this the row's `address` would be derived against the SDK's
|
|
471
|
+
// default network while its script is right — a row that disagrees with
|
|
472
|
+
// the address the user is about to fund
|
|
473
|
+
network: getNetwork(network),
|
|
474
|
+
contractManager
|
|
475
|
+
});
|
|
476
|
+
await contractManager.createContract(
|
|
477
|
+
offerContractParams(client, binding, serverPubkey, expectedPkScript)
|
|
478
|
+
);
|
|
479
|
+
await promoteOfferContract(contractManager, hex2.encode(expectedPkScript), opts.issued);
|
|
480
|
+
}
|
|
481
|
+
function offerContractParams(client, binding, serverPubkey, expectedPkScript) {
|
|
482
|
+
const { program, args, keys } = swapProgramBinding(binding, serverPubkey);
|
|
483
|
+
const contract = new arkade.ArkadeContract(client, program, args, keys);
|
|
484
|
+
if (hex2.encode(contract.pkScript) !== hex2.encode(expectedPkScript)) {
|
|
485
|
+
throw new Error("derived covenant does not match the offer's swapPkScript");
|
|
486
|
+
}
|
|
487
|
+
return {
|
|
488
|
+
...contract.toContractParams(),
|
|
489
|
+
label: OFFER_CONTRACT_LABEL,
|
|
490
|
+
metadata: { genericallySpendable: false, kind: OFFER_CONTRACT_KIND }
|
|
491
|
+
};
|
|
492
|
+
}
|
|
493
|
+
async function restoreOfferCoverage(wallet, arkServerUrl, swaps) {
|
|
494
|
+
const live = swaps.filter((s) => !RETIRABLE.includes(s.status));
|
|
495
|
+
if (live.length === 0) return;
|
|
496
|
+
const arkProvider = new RestArkProvider(arkServerUrl);
|
|
497
|
+
const info = await arkProvider.getInfo();
|
|
498
|
+
const serverPubKey = hex2.decode(toXOnlySignerHex(info.signerPubkey));
|
|
499
|
+
const contractManager = await wallet.getContractManager();
|
|
500
|
+
const client = await arkade.Arkade.connect({
|
|
501
|
+
arkade: arkProvider,
|
|
502
|
+
indexer: new RestIndexerProvider(arkServerUrl),
|
|
503
|
+
identity: wallet.identity,
|
|
504
|
+
network: getNetwork(info.network),
|
|
505
|
+
contractManager
|
|
506
|
+
});
|
|
507
|
+
const done = /* @__PURE__ */ new Set();
|
|
508
|
+
const covenants = [];
|
|
509
|
+
for (const swap of live) {
|
|
510
|
+
if (done.has(swap.swapPkScript)) continue;
|
|
511
|
+
done.add(swap.swapPkScript);
|
|
512
|
+
try {
|
|
513
|
+
const { swapPkScript: _, ...binding } = decodeOffer(hex2.decode(swap.offerHex));
|
|
514
|
+
covenants.push({
|
|
515
|
+
script: swap.swapPkScript,
|
|
516
|
+
issued: swap.createdAt,
|
|
517
|
+
params: offerContractParams(
|
|
518
|
+
client,
|
|
519
|
+
binding,
|
|
520
|
+
serverPubKey,
|
|
521
|
+
hex2.decode(swap.swapPkScript)
|
|
522
|
+
)
|
|
523
|
+
});
|
|
524
|
+
} catch (err) {
|
|
525
|
+
console.warn(`[swap] could not restore coverage for ${swap.swapPkScript}`, err);
|
|
526
|
+
}
|
|
527
|
+
}
|
|
528
|
+
if (covenants.length === 0) return;
|
|
529
|
+
const paramsList = covenants.map((c) => c.params);
|
|
530
|
+
if (contractManager.createContracts) {
|
|
531
|
+
await contractManager.createContracts(paramsList);
|
|
532
|
+
} else {
|
|
533
|
+
for (const params of paramsList) await contractManager.createContract(params);
|
|
534
|
+
}
|
|
535
|
+
for (const c of covenants) await promoteOfferContract(contractManager, c.script, c.issued);
|
|
536
|
+
}
|
|
537
|
+
function serverExitDelay(delay) {
|
|
538
|
+
if (typeof delay !== "bigint" || delay <= BigInt(0)) {
|
|
539
|
+
throw new Error(
|
|
540
|
+
"the server reports no usable unilateralExitDelay; pass `exitDelay` to set the offer's exit closure explicitly, or `noExit: true` to publish without one"
|
|
541
|
+
);
|
|
542
|
+
}
|
|
543
|
+
return assertExitDelay({ value: delay, type: delay < BigInt(512) ? "blocks" : "seconds" });
|
|
544
|
+
}
|
|
545
|
+
async function createOffer(wallet, arkServerUrl, params) {
|
|
546
|
+
if (Boolean(params.wantAsset) === Boolean(params.offerAsset)) {
|
|
547
|
+
throw new Error("set exactly one of wantAsset (BTC->asset) or offerAsset (asset->BTC)");
|
|
548
|
+
}
|
|
549
|
+
const [info, makerAddress, makerPublicKey] = await Promise.all([
|
|
550
|
+
new RestArkProvider(arkServerUrl).getInfo(),
|
|
551
|
+
wallet.getAddress(),
|
|
552
|
+
wallet.identity.xOnlyPublicKey()
|
|
553
|
+
]);
|
|
554
|
+
const serverPubKey = hex2.decode(toXOnlySignerHex(info.signerPubkey));
|
|
555
|
+
const network = getNetwork(info.network);
|
|
556
|
+
const emuKey = hex2.decode(
|
|
557
|
+
toXOnlySignerHex(resolveEmulatorPubkey(network, params.emulatorPubkey))
|
|
558
|
+
);
|
|
559
|
+
const binding = {
|
|
560
|
+
wantAmount: params.wantAmount,
|
|
561
|
+
wantAsset: params.wantAsset,
|
|
562
|
+
offerAsset: params.offerAsset,
|
|
563
|
+
makerPkScript: ArkAddress.decode(makerAddress).pkScript,
|
|
564
|
+
makerPublicKey,
|
|
565
|
+
emulatorPubkey: emuKey,
|
|
566
|
+
// checked HERE, before the covenant is derived and registered below:
|
|
567
|
+
// deferring it to `encodeOffer` leaves a registered contract behind for
|
|
568
|
+
// an offer that then fails to encode. @see assertExitDelay
|
|
569
|
+
exitDelay: params.noExit ? void 0 : params.exitDelay ? assertExitDelay(params.exitDelay) : serverExitDelay(info.unilateralExitDelay)
|
|
570
|
+
};
|
|
571
|
+
const script = offerVtxoScript(binding, serverPubKey);
|
|
572
|
+
const offer = { ...binding, swapPkScript: script.pkScript };
|
|
573
|
+
await registerOfferContract(
|
|
574
|
+
wallet,
|
|
575
|
+
arkServerUrl,
|
|
576
|
+
info.network,
|
|
577
|
+
binding,
|
|
578
|
+
serverPubKey,
|
|
579
|
+
script.pkScript
|
|
580
|
+
);
|
|
581
|
+
const payload = encodeOffer(offer);
|
|
582
|
+
return {
|
|
583
|
+
offerHex: hex2.encode(payload),
|
|
584
|
+
extension: { type: OFFER_PACKET_TYPE, payload },
|
|
585
|
+
// VtxoScript.address owns address construction; assembling an ArkAddress
|
|
586
|
+
// from tweakedPublicKey here would silently miss any future step it gains
|
|
587
|
+
address: script.address(network.hrp, serverPubKey).encode(),
|
|
588
|
+
swapPkScript: script.pkScript
|
|
589
|
+
};
|
|
590
|
+
}
|
|
591
|
+
async function cancelOffer(wallet, arkServerUrl, offerHex, opts) {
|
|
592
|
+
const { repository, fundingTxid, swapAddress } = opts;
|
|
593
|
+
const offer = decodeOffer(hex2.decode(offerHex));
|
|
594
|
+
const contractManager = await wallet.getContractManager();
|
|
595
|
+
const client = await arkade.Arkade.connect({
|
|
596
|
+
arkade: new RestArkProvider(arkServerUrl),
|
|
597
|
+
indexer: new RestIndexerProvider(arkServerUrl),
|
|
598
|
+
identity: wallet.identity,
|
|
599
|
+
// registered offers resolve their VTXOs from the contract repository
|
|
600
|
+
// instead of a direct indexer query; the indexer above stays as the
|
|
601
|
+
// fallback for offers created before registration existed
|
|
602
|
+
contractManager
|
|
603
|
+
// no `network`, unlike registerOfferContract: the row lookup is by
|
|
604
|
+
// script and the payout script comes from wallet.getAddress(), so the
|
|
605
|
+
// client's network (which only shapes address derivation) is unused here
|
|
606
|
+
});
|
|
607
|
+
const operatorPubkey = swapAddress ? ArkAddress.decode(swapAddress).serverPubKey : client.serverKey;
|
|
608
|
+
const { program, args, keys } = swapProgramBinding(offer, operatorPubkey);
|
|
609
|
+
const rebuilt = new arkade.ArkadeProgramScript(program, args, keys);
|
|
610
|
+
if (hex2.encode(rebuilt.pkScript) !== hex2.encode(offer.swapPkScript)) {
|
|
611
|
+
throw new Error(
|
|
612
|
+
"rebuilt covenant does not match the offer's swapPkScript \u2014 the server signing key has likely rotated since funding; pass swapAddress (the funded address) to pin the original key"
|
|
613
|
+
);
|
|
614
|
+
}
|
|
615
|
+
const contract = new arkade.ArkadeContract(client, program, args, keys);
|
|
616
|
+
const [vtxos, makerAddress] = await Promise.all([contract.getUtxos(), wallet.getAddress()]);
|
|
617
|
+
if (!fundingTxid && vtxos.length > 1) {
|
|
618
|
+
throw new Error(
|
|
619
|
+
"multiple spendable deposits at the swap address \u2014 pass fundingTxid to select one"
|
|
620
|
+
);
|
|
621
|
+
}
|
|
622
|
+
const vtxo = fundingTxid ? vtxos.find((v) => v.txid === fundingTxid) : vtxos[0];
|
|
623
|
+
if (!vtxo) throw new Error("no spendable VTXO at the swap address");
|
|
624
|
+
const makerPkScript = ArkAddress.decode(makerAddress).pkScript;
|
|
625
|
+
const cancel = contract.functions.cancel().from({ txid: vtxo.txid, vout: vtxo.vout, value: vtxo.value }).to(makerPkScript, BigInt(vtxo.value));
|
|
626
|
+
for (const a of vtxo.assets ?? []) {
|
|
627
|
+
cancel.withAsset({
|
|
628
|
+
assetId: a.assetId,
|
|
629
|
+
inputs: [{ vin: 0, amount: BigInt(a.amount) }],
|
|
630
|
+
outputs: [{ vout: 0, amount: BigInt(a.amount) }]
|
|
631
|
+
});
|
|
632
|
+
}
|
|
633
|
+
const swapId = fundingTxid ?? vtxo.txid;
|
|
634
|
+
const hasLocalRecord = (await getAssetSwapsOrThrow(repository)).some((s) => s.id === swapId);
|
|
635
|
+
if (hasLocalRecord) await updateAssetSwap(repository, swapId, { status: "cancelling" });
|
|
636
|
+
const { txid } = await cancel.send();
|
|
637
|
+
if (hasLocalRecord) {
|
|
638
|
+
const { persisted, swaps } = await updateAssetSwapBestEffort(repository, swapId, {
|
|
639
|
+
status: "cancelled",
|
|
640
|
+
spentTxid: txid
|
|
641
|
+
});
|
|
642
|
+
if (persisted) {
|
|
643
|
+
await retireOfferContract(contractManager, swaps, hex2.encode(offer.swapPkScript));
|
|
644
|
+
}
|
|
645
|
+
}
|
|
646
|
+
return txid;
|
|
647
|
+
}
|
|
648
|
+
var ASSET_CARRIER_SATS = BigInt(SDK_ASSET_CARRIER_SATS);
|
|
649
|
+
async function fillOffer(wallet, arkServerUrl, offerHex, opts) {
|
|
650
|
+
const {
|
|
651
|
+
fund,
|
|
652
|
+
payoutScript,
|
|
653
|
+
fundingTxid,
|
|
654
|
+
swapAddress,
|
|
655
|
+
assetCarrierSats = ASSET_CARRIER_SATS,
|
|
656
|
+
emulator,
|
|
657
|
+
emulatorPubkey
|
|
658
|
+
} = opts;
|
|
659
|
+
const offer = decodeOffer(hex2.decode(offerHex));
|
|
660
|
+
const wantedAssetId = offer.wantAsset?.toString();
|
|
661
|
+
if (fund.length === 0) {
|
|
662
|
+
throw new Error("fillOffer needs coins to pay wantAmount with \u2014 `fund` is empty");
|
|
663
|
+
}
|
|
664
|
+
if (wantedAssetId !== void 0) {
|
|
665
|
+
const supplied = fund.reduce(
|
|
666
|
+
(sum, coin) => sum + amountOfAsset(coin.assets, wantedAssetId),
|
|
667
|
+
BigInt(0)
|
|
668
|
+
);
|
|
669
|
+
if (supplied < offer.wantAmount) {
|
|
670
|
+
throw new Error(
|
|
671
|
+
`fillOffer needs ${offer.wantAmount} of ${wantedAssetId} to pay the maker, but \`fund\` declares ${supplied} \u2014 pass coins carrying it, and declare their assets`
|
|
672
|
+
);
|
|
673
|
+
}
|
|
674
|
+
}
|
|
675
|
+
const contractManager = await wallet.getContractManager();
|
|
676
|
+
const client = await arkade.Arkade.connect({
|
|
677
|
+
arkade: new RestArkProvider(arkServerUrl),
|
|
678
|
+
indexer: new RestIndexerProvider(arkServerUrl),
|
|
679
|
+
identity: wallet.identity,
|
|
680
|
+
contractManager,
|
|
681
|
+
// `fulfill` is a covenant path, so the emulator executes the arkade
|
|
682
|
+
// script and finalizes with arkd. Without it the spend is built and then
|
|
683
|
+
// refused at submission — there is no default to fall back on.
|
|
684
|
+
emulator: typeof emulator === "string" ? new RestEmulatorProvider(emulator) : emulator,
|
|
685
|
+
// Only reached for the client's own emulatorKey, which this fill never
|
|
686
|
+
// derives against — the offer's own key is bound below. It still has to
|
|
687
|
+
// resolve, and on a network with no pinned key it throws without this.
|
|
688
|
+
...emulatorPubkey ? { emulatorPubkey } : {}
|
|
689
|
+
});
|
|
690
|
+
const serverKey = swapAddress ? ArkAddress.decode(swapAddress).serverPubKey : client.serverKey;
|
|
691
|
+
const { program, args, keys } = swapProgramBinding(offer, serverKey);
|
|
692
|
+
const rebuilt = new arkade.ArkadeProgramScript(program, args, keys);
|
|
693
|
+
if (hex2.encode(rebuilt.pkScript) !== hex2.encode(offer.swapPkScript)) {
|
|
694
|
+
throw new Error(
|
|
695
|
+
"rebuilt covenant does not match the offer's swapPkScript \u2014 the server signing key has likely rotated since funding; pass swapAddress (the funded address) to pin the original key"
|
|
696
|
+
);
|
|
697
|
+
}
|
|
698
|
+
const contract = new arkade.ArkadeContract(client, program, args, keys);
|
|
699
|
+
const [vtxos, takerAddress] = await Promise.all([contract.getUtxos(), wallet.getAddress()]);
|
|
700
|
+
if (!fundingTxid && vtxos.length > 1) {
|
|
701
|
+
throw new Error(
|
|
702
|
+
"multiple spendable deposits at the swap address \u2014 pass fundingTxid to select one"
|
|
703
|
+
);
|
|
704
|
+
}
|
|
705
|
+
const vtxo = fundingTxid ? vtxos.find((v) => v.txid === fundingTxid) : vtxos[0];
|
|
706
|
+
if (!vtxo) throw new Error("no spendable VTXO at the swap address");
|
|
707
|
+
if (offer.offerAsset) {
|
|
708
|
+
const offered = offer.offerAsset.toString();
|
|
709
|
+
const deposited = amountOfAsset(vtxo.assets, offered);
|
|
710
|
+
if (deposited <= BigInt(0)) {
|
|
711
|
+
throw new Error(
|
|
712
|
+
`the deposit at the swap address carries no ${offered}, which this offer sells \u2014 filling it would pay wantAmount for nothing. The indexer may be behind, or the offer may not be backed by what it advertises`
|
|
713
|
+
);
|
|
714
|
+
}
|
|
715
|
+
}
|
|
716
|
+
const payout = payoutScript ?? ArkAddress.decode(takerAddress).pkScript;
|
|
717
|
+
const makerSats = wantedAssetId === void 0 ? offer.wantAmount : assetCarrierSats;
|
|
718
|
+
const fill = contract.functions.fulfill().from({ txid: vtxo.txid, vout: vtxo.vout, value: vtxo.value }).fund(fund).to(offer.makerPkScript, makerSats).change(payout);
|
|
719
|
+
const held = /* @__PURE__ */ new Map();
|
|
720
|
+
const hold = (vin, assets) => {
|
|
721
|
+
for (const a of assets ?? []) {
|
|
722
|
+
const amount = BigInt(a.amount);
|
|
723
|
+
if (amount <= BigInt(0)) continue;
|
|
724
|
+
held.set(a.assetId, [...held.get(a.assetId) ?? [], { vin, amount }]);
|
|
725
|
+
}
|
|
726
|
+
};
|
|
727
|
+
hold(0, vtxo.assets);
|
|
728
|
+
fund.forEach((coin, i) => hold(i + 1, coin.assets));
|
|
729
|
+
const outputsSum = makerSats;
|
|
730
|
+
const inputsSum = fund.reduce((s, c) => s + BigInt(c.value), BigInt(vtxo.value));
|
|
731
|
+
const hasPayoutOutput = inputsSum > outputsSum;
|
|
732
|
+
const emitWanted = () => {
|
|
733
|
+
if (wantedAssetId === void 0) return;
|
|
734
|
+
const supplying = held.get(wantedAssetId) ?? [];
|
|
735
|
+
const supplied = supplying.reduce((s, i) => s + i.amount, BigInt(0));
|
|
736
|
+
const outputs = [{ vout: 0, amount: offer.wantAmount }];
|
|
737
|
+
const surplus = supplied - offer.wantAmount;
|
|
738
|
+
if (surplus > BigInt(0)) {
|
|
739
|
+
if (!hasPayoutOutput) {
|
|
740
|
+
throw new Error(
|
|
741
|
+
`fillOffer has ${surplus} of ${wantedAssetId} to return but no payout output \u2014 fund with more sats than the maker's output takes`
|
|
742
|
+
);
|
|
743
|
+
}
|
|
744
|
+
outputs.push({ vout: 1, amount: surplus });
|
|
745
|
+
}
|
|
746
|
+
fill.withAsset({ assetId: wantedAssetId, inputs: supplying, outputs });
|
|
747
|
+
held.delete(wantedAssetId);
|
|
748
|
+
};
|
|
749
|
+
emitWanted();
|
|
750
|
+
for (const [assetId, inputs] of held) {
|
|
751
|
+
const amount = inputs.reduce((s, i) => s + i.amount, BigInt(0));
|
|
752
|
+
if (!hasPayoutOutput) {
|
|
753
|
+
throw new Error(
|
|
754
|
+
`fillOffer has ${amount} of ${assetId} to return but no payout output \u2014 fund with more sats than the maker's output takes`
|
|
755
|
+
);
|
|
756
|
+
}
|
|
757
|
+
fill.withAsset({ assetId, inputs, outputs: [{ vout: 1, amount }] });
|
|
758
|
+
}
|
|
759
|
+
const { txid } = await fill.send();
|
|
760
|
+
return txid;
|
|
761
|
+
}
|
|
762
|
+
var amountOfAsset = (assets, assetId) => {
|
|
763
|
+
let total = BigInt(0);
|
|
764
|
+
for (const a of assets ?? []) {
|
|
765
|
+
if (a.assetId === assetId) total += BigInt(a.amount);
|
|
766
|
+
}
|
|
767
|
+
return total;
|
|
768
|
+
};
|
|
769
|
+
|
|
770
|
+
// src/onchainHtlc.ts
|
|
771
|
+
import { hex as hex3 } from "@scure/base";
|
|
772
|
+
import { sha256 as sha2562 } from "@noble/hashes/sha2.js";
|
|
4
773
|
import { ripemd160 } from "@noble/hashes/legacy.js";
|
|
5
774
|
import * as btc from "@scure/btc-signer";
|
|
6
775
|
var ONCHAIN_ORDER_MARGIN_SECONDS = 2 * 60 * 60;
|
|
@@ -10,8 +779,8 @@ var LOCKTIME_THRESHOLD = 5e8;
|
|
|
10
779
|
var ONCHAIN_SECONDS_PER_BLOCK = 600;
|
|
11
780
|
var ONCHAIN_DUST_SATS = BigInt(330);
|
|
12
781
|
var newPreimage = () => crypto.getRandomValues(new Uint8Array(32));
|
|
13
|
-
var paymentHashOf = (preimage) =>
|
|
14
|
-
var h160FromPaymentHash = (paymentHash) => ripemd160(
|
|
782
|
+
var paymentHashOf = (preimage) => hex3.encode(sha2562(preimage));
|
|
783
|
+
var h160FromPaymentHash = (paymentHash) => ripemd160(hex3.decode(paymentHash));
|
|
15
784
|
var L1_NETWORKS = {
|
|
16
785
|
bitcoin: btc.NETWORK,
|
|
17
786
|
testnet: btc.TEST_NETWORK,
|
|
@@ -63,7 +832,7 @@ function onchainHtlcScript(params, network) {
|
|
|
63
832
|
);
|
|
64
833
|
const controlBlockFor = (leaf) => {
|
|
65
834
|
for (const [block, script] of payment.tapLeafScript ?? []) {
|
|
66
|
-
if (script.length - 1 === leaf.length &&
|
|
835
|
+
if (script.length - 1 === leaf.length && hex3.encode(script.subarray(0, leaf.length)) === hex3.encode(leaf)) {
|
|
67
836
|
return btc.TaprootControlBlock.encode(block);
|
|
68
837
|
}
|
|
69
838
|
}
|
|
@@ -156,13 +925,13 @@ var buildHtlcRefund = (input) => buildLeafSpend({
|
|
|
156
925
|
function extractPreimage(txHex, paymentHash) {
|
|
157
926
|
let raw;
|
|
158
927
|
try {
|
|
159
|
-
raw = btc.RawTx.decode(
|
|
928
|
+
raw = btc.RawTx.decode(hex3.decode(txHex));
|
|
160
929
|
} catch {
|
|
161
930
|
return null;
|
|
162
931
|
}
|
|
163
932
|
for (const stack of raw.witnesses ?? []) {
|
|
164
933
|
for (const item of stack) {
|
|
165
|
-
if (item.length === 32 &&
|
|
934
|
+
if (item.length === 32 && hex3.encode(sha2562(item)) === paymentHash) return item;
|
|
166
935
|
}
|
|
167
936
|
}
|
|
168
937
|
return null;
|
|
@@ -207,7 +976,7 @@ async function classifyOnchainHtlc(chain, input) {
|
|
|
207
976
|
);
|
|
208
977
|
if (!spend) return { phase: "unfunded" };
|
|
209
978
|
const preimage = extractPreimage(spend.txHex, input.htlc.paymentHash);
|
|
210
|
-
const txid = btc.Transaction.fromRaw(
|
|
979
|
+
const txid = btc.Transaction.fromRaw(hex3.decode(spend.txHex), {
|
|
211
980
|
allowUnknownInputs: true,
|
|
212
981
|
allowUnknownOutputs: true
|
|
213
982
|
}).id;
|
|
@@ -222,11 +991,11 @@ async function classifyOnchainHtlc(chain, input) {
|
|
|
222
991
|
|
|
223
992
|
// src/claimPacket.ts
|
|
224
993
|
import { base64 } from "@scure/base";
|
|
225
|
-
import { concatBytes } from "@scure/btc-signer/utils.js";
|
|
994
|
+
import { concatBytes as concatBytes2 } from "@scure/btc-signer/utils.js";
|
|
226
995
|
import { gcm } from "@noble/ciphers/aes.js";
|
|
227
996
|
import { secp256k1 } from "@noble/curves/secp256k1.js";
|
|
228
997
|
import { hkdf } from "@noble/hashes/hkdf.js";
|
|
229
|
-
import { sha256 as
|
|
998
|
+
import { sha256 as sha2563 } from "@noble/hashes/sha2.js";
|
|
230
999
|
var HKDF_INFO = new TextEncoder().encode("covclaimd/preimage/v1");
|
|
231
1000
|
async function sealClaimPacket(input) {
|
|
232
1001
|
return sealWithEntropy(
|
|
@@ -242,7 +1011,7 @@ async function sealWithEntropy(input, ephemeralKey, nonce) {
|
|
|
242
1011
|
}
|
|
243
1012
|
const ephemeralPub = secp256k1.getPublicKey(ephemeralKey, true);
|
|
244
1013
|
const sharedX = secp256k1.getSharedSecret(ephemeralKey, input.covclaimdPubkey, true).subarray(1);
|
|
245
|
-
const key = hkdf(
|
|
1014
|
+
const key = hkdf(sha2563, sharedX, ephemeralPub, HKDF_INFO, 32);
|
|
246
1015
|
if (nonce.length !== 12) throw new Error("nonce must be 12 bytes");
|
|
247
1016
|
const sealed = gcm(key, nonce, ephemeralPub).encrypt(input.preimage);
|
|
248
1017
|
const ciphertext = new Uint8Array(33 + 12 + sealed.length);
|
|
@@ -252,9 +1021,9 @@ async function sealWithEntropy(input, ephemeralKey, nonce) {
|
|
|
252
1021
|
return {
|
|
253
1022
|
ciphertext: base64.encode(ciphertext),
|
|
254
1023
|
packet: base64.encode(
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
1024
|
+
concatBytes2(
|
|
1025
|
+
tlv2(TLV_CIPHERTEXT, ciphertext),
|
|
1026
|
+
tlv2(TLV_COVCLAIMD_PUBKEY, input.covclaimdPubkey)
|
|
258
1027
|
)
|
|
259
1028
|
)
|
|
260
1029
|
};
|
|
@@ -265,8 +1034,8 @@ var TLV_COVCLAIMD_PUBKEY = 3;
|
|
|
265
1034
|
var COMPRESSED_PUBKEY_LENGTH = 33;
|
|
266
1035
|
var SEALED_CIPHERTEXT_LENGTH = 93;
|
|
267
1036
|
var CLAIM_PACKET_TYPE = 4;
|
|
268
|
-
var
|
|
269
|
-
var appendArkadeScript = (body, arkadeScript) => Uint8Array.from([...body, ...
|
|
1037
|
+
var tlv2 = (type, value) => Uint8Array.from([type, value.length >> 8 & 255, value.length & 255, ...value]);
|
|
1038
|
+
var appendArkadeScript = (body, arkadeScript) => Uint8Array.from([...body, ...tlv2(TLV_ARKADE_SCRIPT, arkadeScript)]);
|
|
270
1039
|
var parseTlv = (data) => {
|
|
271
1040
|
let ciphertextLength;
|
|
272
1041
|
let hasArkadeScript = false;
|
|
@@ -315,9 +1084,9 @@ var claimPacketShape = (b64) => {
|
|
|
315
1084
|
};
|
|
316
1085
|
|
|
317
1086
|
// src/lockupContract.ts
|
|
318
|
-
import { hex as
|
|
1087
|
+
import { hex as hex4 } from "@scure/base";
|
|
319
1088
|
import {
|
|
320
|
-
ArkAddress,
|
|
1089
|
+
ArkAddress as ArkAddress2,
|
|
321
1090
|
VHTLCV2ContractHandler
|
|
322
1091
|
} from "@arkade-os/sdk";
|
|
323
1092
|
var SWAP_LOCKUP_CONTRACT_TYPE = "vhtlc-v2";
|
|
@@ -356,7 +1125,7 @@ async function registerLockupContract(contracts, script, address) {
|
|
|
356
1125
|
await contracts.createContract({
|
|
357
1126
|
type: SWAP_LOCKUP_CONTRACT_TYPE,
|
|
358
1127
|
params: VHTLCV2ContractHandler.serializeParams(script.options),
|
|
359
|
-
script:
|
|
1128
|
+
script: hex4.encode(script.pkScript),
|
|
360
1129
|
address,
|
|
361
1130
|
label: SWAP_LOCKUP_CONTRACT_LABEL,
|
|
362
1131
|
metadata: { genericallySpendable: false, kind: SWAP_LOCKUP_CONTRACT_KIND }
|
|
@@ -366,21 +1135,21 @@ async function registerLockupContract(contracts, script, address) {
|
|
|
366
1135
|
}
|
|
367
1136
|
}
|
|
368
1137
|
async function lockupContractParams(contracts, lockupAddress) {
|
|
369
|
-
const script =
|
|
1138
|
+
const script = hex4.encode(ArkAddress2.decode(lockupAddress).pkScript);
|
|
370
1139
|
const [row] = await contracts.getContracts({ script });
|
|
371
1140
|
if (!row) throw new LockupContractMissing(lockupAddress, script);
|
|
372
1141
|
return row.params;
|
|
373
1142
|
}
|
|
374
1143
|
|
|
375
1144
|
// src/rfq.ts
|
|
376
|
-
import { hex as
|
|
1145
|
+
import { hex as hex5 } from "@scure/base";
|
|
377
1146
|
import { ripemd160 as ripemd1602 } from "@noble/hashes/legacy.js";
|
|
378
1147
|
import {
|
|
379
|
-
ArkAddress as
|
|
380
|
-
RestArkProvider,
|
|
1148
|
+
ArkAddress as ArkAddress3,
|
|
1149
|
+
RestArkProvider as RestArkProvider2,
|
|
381
1150
|
VHTLC,
|
|
382
|
-
getNetwork,
|
|
383
|
-
resolveEmulatorPubkey,
|
|
1151
|
+
getNetwork as getNetwork2,
|
|
1152
|
+
resolveEmulatorPubkey as resolveEmulatorPubkey2,
|
|
384
1153
|
toXOnly
|
|
385
1154
|
} from "@arkade-os/sdk";
|
|
386
1155
|
import {
|
|
@@ -389,11 +1158,17 @@ import {
|
|
|
389
1158
|
} from "@arkade-os/sdk";
|
|
390
1159
|
var solverHex = (value, field) => {
|
|
391
1160
|
try {
|
|
392
|
-
return
|
|
1161
|
+
return hex5.decode(value);
|
|
393
1162
|
} catch {
|
|
394
1163
|
throw new Error(`solver sent malformed hex for ${field}`);
|
|
395
1164
|
}
|
|
396
1165
|
};
|
|
1166
|
+
var quoteSats = (value, field) => {
|
|
1167
|
+
if (typeof value !== "number" || !Number.isSafeInteger(value)) {
|
|
1168
|
+
throw new Error(`HTLC quote carries a non-sats ${field}: ${String(value)}`);
|
|
1169
|
+
}
|
|
1170
|
+
return value;
|
|
1171
|
+
};
|
|
397
1172
|
var ARKADE_BTC = "arkade:BTC";
|
|
398
1173
|
var LIGHTNING_BTC = "lightning:BTC";
|
|
399
1174
|
var ONCHAIN_BTC = "onchain:BTC";
|
|
@@ -489,7 +1264,7 @@ var AddressMismatch = class extends Error {
|
|
|
489
1264
|
this.quoted = quoted;
|
|
490
1265
|
}
|
|
491
1266
|
};
|
|
492
|
-
var newRfqId = () =>
|
|
1267
|
+
var newRfqId = () => hex5.encode(crypto.getRandomValues(new Uint8Array(32)));
|
|
493
1268
|
var lightningSendRequest = (input) => ({
|
|
494
1269
|
v: 1,
|
|
495
1270
|
type: "rfq_request",
|
|
@@ -499,18 +1274,18 @@ var lightningSendRequest = (input) => ({
|
|
|
499
1274
|
profile: {
|
|
500
1275
|
invoice: input.invoice,
|
|
501
1276
|
refund_address: input.refundAddress,
|
|
502
|
-
client_refund_pubkey:
|
|
1277
|
+
client_refund_pubkey: hex5.encode(input.senderPubkey)
|
|
503
1278
|
}
|
|
504
1279
|
});
|
|
505
1280
|
var arkadeSwapRequest = (input) => {
|
|
506
1281
|
if (!input.wantAsset && !input.offerAsset) {
|
|
507
1282
|
throw new Error(
|
|
508
|
-
"set
|
|
1283
|
+
"set at least one of wantAsset or offerAsset \u2014 with neither set both legs are BTC, which is not a swap"
|
|
509
1284
|
);
|
|
510
1285
|
}
|
|
511
|
-
if (input.
|
|
1286
|
+
if (input.amountSide !== void 0 && input.amountSide !== "from") {
|
|
512
1287
|
throw new Error(
|
|
513
|
-
|
|
1288
|
+
'arkade\u2194arkade quotes are exact-in only: use amountSide "from" with the deposit amount'
|
|
514
1289
|
);
|
|
515
1290
|
}
|
|
516
1291
|
const pair = rfqPair(
|
|
@@ -523,16 +1298,59 @@ var arkadeSwapRequest = (input) => {
|
|
|
523
1298
|
type: "rfq_request",
|
|
524
1299
|
rfq_id: input.rfqId,
|
|
525
1300
|
pair,
|
|
526
|
-
amount_side:
|
|
527
|
-
amount: input.amount,
|
|
528
|
-
// The pair
|
|
529
|
-
//
|
|
530
|
-
//
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
1301
|
+
amount_side: "from",
|
|
1302
|
+
amount: canonicalAssetAmount(input.amount),
|
|
1303
|
+
// The pair names the assets; the profile names the trader. Both are
|
|
1304
|
+
// covenant parameters, and the solver's `.strict()` schema refuses
|
|
1305
|
+
// anything else — an undeclared key is `unsupported_payload`.
|
|
1306
|
+
profile: {
|
|
1307
|
+
maker_pk_script: normalizeMakerPkScript(input.makerPkScript),
|
|
1308
|
+
maker_public_key: normalizeMakerPublicKey(input.makerPublicKey)
|
|
1309
|
+
}
|
|
534
1310
|
};
|
|
535
1311
|
};
|
|
1312
|
+
var canonicalAssetAmount = (amount) => {
|
|
1313
|
+
if (typeof amount === "bigint") {
|
|
1314
|
+
if (amount <= 0n) throw new Error(`amount must be positive, got ${amount}`);
|
|
1315
|
+
return amount.toString();
|
|
1316
|
+
}
|
|
1317
|
+
if (typeof amount === "number") {
|
|
1318
|
+
if (!Number.isSafeInteger(amount) || amount <= 0) {
|
|
1319
|
+
throw new Error(`amount must be a positive safe integer, got ${amount}`);
|
|
1320
|
+
}
|
|
1321
|
+
return String(amount);
|
|
1322
|
+
}
|
|
1323
|
+
if (!/^(0|[1-9][0-9]*)$/.test(amount) || BigInt(amount) <= 0n) {
|
|
1324
|
+
throw new Error(`amount must be a canonical decimal string of atomic units, got ${amount}`);
|
|
1325
|
+
}
|
|
1326
|
+
return amount;
|
|
1327
|
+
};
|
|
1328
|
+
var normalizeHexBytes = (value, expectedBytes, label) => {
|
|
1329
|
+
if (value === void 0 || value === null) {
|
|
1330
|
+
throw new Error(`${label} is required`);
|
|
1331
|
+
}
|
|
1332
|
+
const bytes = typeof value === "string" ? solverHex(value, label) : value;
|
|
1333
|
+
if (!(bytes instanceof Uint8Array) || bytes.length !== expectedBytes) {
|
|
1334
|
+
const got = bytes instanceof Uint8Array ? bytes.length : typeof bytes;
|
|
1335
|
+
throw new Error(`${label} must be ${expectedBytes} bytes, got ${got}`);
|
|
1336
|
+
}
|
|
1337
|
+
return hex5.encode(bytes);
|
|
1338
|
+
};
|
|
1339
|
+
var normalizeMakerPkScript = (value) => {
|
|
1340
|
+
const encoded = normalizeHexBytes(value, 34, "maker_pk_script");
|
|
1341
|
+
if (!encoded.startsWith("5100") && !encoded.startsWith("5120")) {
|
|
1342
|
+
throw new Error("maker_pk_script must be a taproot scriptPubKey (OP_1 <32-byte program>)");
|
|
1343
|
+
}
|
|
1344
|
+
return encoded;
|
|
1345
|
+
};
|
|
1346
|
+
var normalizeMakerPublicKey = (value) => {
|
|
1347
|
+
const raw = typeof value === "string" ? value : hex5.encode(value);
|
|
1348
|
+
const bytes = solverHex(raw, "maker_public_key");
|
|
1349
|
+
if (bytes.length === 33) {
|
|
1350
|
+
throw new Error("maker_public_key must be x-only (32 bytes), not compressed (33 bytes)");
|
|
1351
|
+
}
|
|
1352
|
+
return normalizeHexBytes(bytes, 32, "maker_public_key");
|
|
1353
|
+
};
|
|
536
1354
|
var MIN_HEADROOM_SECONDS = 90 * 60;
|
|
537
1355
|
var gateError = (reason, message) => {
|
|
538
1356
|
const error = new Error(message);
|
|
@@ -580,6 +1398,9 @@ var assertFundable = (input) => {
|
|
|
580
1398
|
}
|
|
581
1399
|
if (input.now >= input.quote.valid_until)
|
|
582
1400
|
fail("quote_expired", "quote expired \u2014 request a fresh one");
|
|
1401
|
+
if (quoteSats(input.quote.from_amount, "from_amount") <= 0 || quoteSats(input.quote.to_amount, "to_amount") <= 0) {
|
|
1402
|
+
fail("non_positive_amount", "quote carries a non-positive amount");
|
|
1403
|
+
}
|
|
583
1404
|
if (input.quote.refund_locktime !== void 0 && input.quote.refund_locktime - input.now < MIN_HEADROOM_SECONDS) {
|
|
584
1405
|
fail("insufficient_headroom", "refund deadline headroom below 90 minutes");
|
|
585
1406
|
}
|
|
@@ -609,13 +1430,12 @@ var assertFundable = (input) => {
|
|
|
609
1430
|
`maxFee.referenceRate must be a positive finite number, got ${referenceRate}`
|
|
610
1431
|
);
|
|
611
1432
|
}
|
|
612
|
-
const
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
sats ?? 0,
|
|
617
|
-
Math.floor(input.quote.from_amount * (bps ?? 0) / 1e4)
|
|
1433
|
+
const fromSats = quoteSats(input.quote.from_amount, "from_amount");
|
|
1434
|
+
const toSats = quoteSats(input.quote.to_amount, "to_amount");
|
|
1435
|
+
const fee = sameAsset ? fromSats - toSats : Math.ceil(
|
|
1436
|
+
(fromSats * referenceRate - toSats) / referenceRate
|
|
618
1437
|
);
|
|
1438
|
+
const allowed = Math.max(sats ?? 0, Math.floor(fromSats * (bps ?? 0) / 1e4));
|
|
619
1439
|
if (fee > allowed) {
|
|
620
1440
|
fail("fee_too_high", `fee ${fee} exceeds the ${allowed} this client allows`);
|
|
621
1441
|
}
|
|
@@ -814,12 +1634,12 @@ function lightningSendVtxoScript(params) {
|
|
|
814
1634
|
sender: params.senderPubkey,
|
|
815
1635
|
receiver: params.solverPubkey,
|
|
816
1636
|
server: params.serverPubkey,
|
|
817
|
-
preimageHash: ripemd1602(
|
|
1637
|
+
preimageHash: ripemd1602(hex5.decode(params.paymentHash)),
|
|
818
1638
|
refundLocktime: BigInt(params.refundLocktime),
|
|
819
1639
|
unilateralClaimDelay: seconds(params.claimDelay),
|
|
820
1640
|
unilateralRefundDelay: seconds(unilateralRefundDelay(params.claimDelay)),
|
|
821
1641
|
unilateralRefundWithoutReceiverDelay: seconds(
|
|
822
|
-
unilateralRefundWithoutReceiverDelay(params.claimDelay)
|
|
1642
|
+
params.refundWithoutReceiverDelay ?? unilateralRefundWithoutReceiverDelay(params.claimDelay)
|
|
823
1643
|
),
|
|
824
1644
|
nonInteractiveParameters: {
|
|
825
1645
|
receiverPkScript: params.receiverPkScript,
|
|
@@ -834,37 +1654,50 @@ async function requestLightningSend(wallet, arkServerUrl, transport, params) {
|
|
|
834
1654
|
const secrets = await provisionRefundKey(wallet);
|
|
835
1655
|
const senderPubkey = secrets.pubkey;
|
|
836
1656
|
const refundAddress = secrets.address;
|
|
837
|
-
const info = await new
|
|
1657
|
+
const info = await new RestArkProvider2(arkServerUrl).getInfo();
|
|
838
1658
|
const quote = await transport.requestQuote(
|
|
839
1659
|
lightningSendRequest({ rfqId, invoice: params.invoice.raw, refundAddress, senderPubkey })
|
|
840
1660
|
);
|
|
841
1661
|
if (quote.refund_locktime === void 0) {
|
|
842
1662
|
throw new Error("lightning-send quote is missing refund_locktime");
|
|
843
1663
|
}
|
|
1664
|
+
const now = Math.floor(Date.now() / 1e3);
|
|
1665
|
+
const claimDelay = unilateralClaimDelay(Number(info.unilateralExitDelay));
|
|
1666
|
+
const refundWithoutReceiverDelay = quote.profile?.refund_without_receiver_delay;
|
|
1667
|
+
if (refundWithoutReceiverDelay === void 0) {
|
|
1668
|
+
throw new Error("lightning-send quote is missing profile.refund_without_receiver_delay");
|
|
1669
|
+
}
|
|
1670
|
+
if (!Number.isSafeInteger(refundWithoutReceiverDelay) || refundWithoutReceiverDelay < claimDelay || refundWithoutReceiverDelay % SEQUENCE_GRANULARITY_SECONDS !== 0 || refundWithoutReceiverDelay > 65535 * SEQUENCE_GRANULARITY_SECONDS) {
|
|
1671
|
+
throw new Error("lightning-send quote carries an invalid refund_without_receiver_delay");
|
|
1672
|
+
}
|
|
1673
|
+
if (refundWithoutReceiverDelay < quote.refund_locktime - now) {
|
|
1674
|
+
throw new Error("lightning-send quote lets the solo refund open before refund_locktime");
|
|
1675
|
+
}
|
|
844
1676
|
const receiverPkScriptHex = quote.profile?.receiver_pk_script;
|
|
845
1677
|
if (receiverPkScriptHex === void 0) {
|
|
846
1678
|
throw new Error("lightning-send quote is missing profile.receiver_pk_script");
|
|
847
1679
|
}
|
|
848
|
-
if (quote.to_amount !== params.invoice.amountSats) {
|
|
1680
|
+
if (quoteSats(quote.to_amount, "to_amount") !== params.invoice.amountSats) {
|
|
849
1681
|
throw new Error(
|
|
850
1682
|
`quote to_amount ${quote.to_amount} does not match the invoice's ${params.invoice.amountSats}`
|
|
851
1683
|
);
|
|
852
1684
|
}
|
|
853
|
-
if (quote.from_amount < quote.to_amount) {
|
|
1685
|
+
if (quoteSats(quote.from_amount, "from_amount") < quoteSats(quote.to_amount, "to_amount")) {
|
|
854
1686
|
throw new Error(
|
|
855
1687
|
`quote from_amount ${quote.from_amount} is below the invoice amount \u2014 a negative spread is not a quote`
|
|
856
1688
|
);
|
|
857
1689
|
}
|
|
858
|
-
const serverPubkey = toXOnly(
|
|
859
|
-
const network =
|
|
1690
|
+
const serverPubkey = toXOnly(hex5.decode(info.signerPubkey), "ark signer key");
|
|
1691
|
+
const network = getNetwork2(info.network);
|
|
860
1692
|
const treeParams = {
|
|
861
|
-
solverPubkey: toXOnly(
|
|
1693
|
+
solverPubkey: toXOnly(hex5.decode(quote.solver_pubkey), "solver key"),
|
|
862
1694
|
refundLocktime: quote.refund_locktime,
|
|
863
1695
|
serverPubkey,
|
|
864
1696
|
paymentHash: params.invoice.paymentHash,
|
|
865
|
-
claimDelay
|
|
1697
|
+
claimDelay,
|
|
1698
|
+
refundWithoutReceiverDelay,
|
|
866
1699
|
emulatorPubkey: toXOnly(
|
|
867
|
-
|
|
1700
|
+
hex5.decode(resolveEmulatorPubkey2(network, params.emulatorPubkey)),
|
|
868
1701
|
"emulator signer key"
|
|
869
1702
|
),
|
|
870
1703
|
senderPubkey,
|
|
@@ -886,7 +1719,7 @@ async function requestLightningSend(wallet, arkServerUrl, transport, params) {
|
|
|
886
1719
|
assertFundable({
|
|
887
1720
|
quote,
|
|
888
1721
|
invoiceExpiresAt: params.invoice.expiresAt,
|
|
889
|
-
now
|
|
1722
|
+
now
|
|
890
1723
|
});
|
|
891
1724
|
await registerLockupContract(await wallet.getContractManager(), script, address);
|
|
892
1725
|
return {
|
|
@@ -895,7 +1728,7 @@ async function requestLightningSend(wallet, arkServerUrl, transport, params) {
|
|
|
895
1728
|
address,
|
|
896
1729
|
// What the lockup must carry: the quote's `from_amount` — the invoice
|
|
897
1730
|
// PLUS the corridor's fee, never the bare invoice amount.
|
|
898
|
-
fundAmount: quote.from_amount,
|
|
1731
|
+
fundAmount: quoteSats(quote.from_amount, "from_amount"),
|
|
899
1732
|
swapPkScript: script.pkScript,
|
|
900
1733
|
script,
|
|
901
1734
|
refundAddress,
|
|
@@ -905,11 +1738,91 @@ async function requestLightningSend(wallet, arkServerUrl, transport, params) {
|
|
|
905
1738
|
};
|
|
906
1739
|
}
|
|
907
1740
|
var offerTermsFromQuote = (quote, assets) => {
|
|
908
|
-
if (
|
|
909
|
-
throw new Error("set
|
|
1741
|
+
if (!assets.wantAsset && !assets.offerAsset) {
|
|
1742
|
+
throw new Error("set at least one of wantAsset or offerAsset");
|
|
1743
|
+
}
|
|
1744
|
+
const wantAmount = BigInt(quote.to_amount);
|
|
1745
|
+
if (assets.wantAsset) return { wantAmount, wantAsset: assets.wantAsset };
|
|
1746
|
+
return { wantAmount, offerAsset: assets.offerAsset };
|
|
1747
|
+
};
|
|
1748
|
+
var verifyOfferAddress = (quote, derived) => {
|
|
1749
|
+
const quotedAddress = quote.profile?.offer_address;
|
|
1750
|
+
const quotedScript = quote.profile?.offer_pk_script;
|
|
1751
|
+
const derivedScript = hex5.encode(derived.swapPkScript);
|
|
1752
|
+
if (quotedAddress !== derived.address || quotedScript !== derivedScript) {
|
|
1753
|
+
throw new AddressMismatch([derived.address], quotedAddress);
|
|
1754
|
+
}
|
|
1755
|
+
return derived;
|
|
1756
|
+
};
|
|
1757
|
+
var assertArkadeFundable = (input) => {
|
|
1758
|
+
const now = input.now ?? Math.floor(Date.now() / 1e3);
|
|
1759
|
+
if (!Number.isSafeInteger(input.quote.valid_until)) {
|
|
1760
|
+
throw gateError("quote_expired", "quote carries no valid_until");
|
|
1761
|
+
}
|
|
1762
|
+
if (now >= input.quote.valid_until) {
|
|
1763
|
+
throw gateError("quote_expired", `quote lapsed at ${input.quote.valid_until} (now ${now})`);
|
|
1764
|
+
}
|
|
1765
|
+
if (BigInt(input.quote.from_amount) <= 0n || BigInt(input.quote.to_amount) <= 0n) {
|
|
1766
|
+
throw gateError("quote_expired", "quote carries a non-positive amount");
|
|
910
1767
|
}
|
|
911
|
-
return { wantAmount: BigInt(quote.to_amount), ...assets };
|
|
912
1768
|
};
|
|
1769
|
+
async function requestArkadeSwap(wallet, arkServerUrl, transport, params) {
|
|
1770
|
+
if (!params.wantAsset && !params.offerAsset) {
|
|
1771
|
+
throw new Error("set at least one of wantAsset or offerAsset; BTC-to-BTC is not a swap");
|
|
1772
|
+
}
|
|
1773
|
+
const rfqId = params.rfqId ?? newRfqId();
|
|
1774
|
+
const [makerAddress, makerPublicKey] = await Promise.all([
|
|
1775
|
+
wallet.getAddress(),
|
|
1776
|
+
wallet.identity.xOnlyPublicKey()
|
|
1777
|
+
]);
|
|
1778
|
+
const makerPkScript = ArkAddress3.decode(makerAddress).pkScript;
|
|
1779
|
+
const pair = rfqPair(
|
|
1780
|
+
params.offerAsset ? arkadeAssetLeg(params.offerAsset) : ARKADE_BTC,
|
|
1781
|
+
params.wantAsset ? arkadeAssetLeg(params.wantAsset) : ARKADE_BTC
|
|
1782
|
+
);
|
|
1783
|
+
const quote = await transport.requestQuote(
|
|
1784
|
+
arkadeSwapRequest({
|
|
1785
|
+
rfqId,
|
|
1786
|
+
...params.offerAsset !== void 0 ? { offerAsset: params.offerAsset } : {},
|
|
1787
|
+
...params.wantAsset !== void 0 ? { wantAsset: params.wantAsset } : {},
|
|
1788
|
+
amount: params.amount,
|
|
1789
|
+
makerPkScript,
|
|
1790
|
+
makerPublicKey
|
|
1791
|
+
})
|
|
1792
|
+
);
|
|
1793
|
+
if (quote.pair !== pair) {
|
|
1794
|
+
throw new Error(`solver quoted ${JSON.stringify(quote.pair)}, not the requested ${pair}`);
|
|
1795
|
+
}
|
|
1796
|
+
assertArkadeFundable({ quote, ...params.now !== void 0 ? { now: params.now } : {} });
|
|
1797
|
+
const terms = offerTermsFromQuote(quote, {
|
|
1798
|
+
...params.offerAsset !== void 0 ? { offerAsset: params.offerAsset } : {},
|
|
1799
|
+
...params.wantAsset !== void 0 ? { wantAsset: params.wantAsset } : {}
|
|
1800
|
+
});
|
|
1801
|
+
if (BigInt(quote.from_amount).toString() !== canonicalAssetAmount(params.amount)) {
|
|
1802
|
+
throw new Error(
|
|
1803
|
+
`quote from_amount ${quote.from_amount} does not match the requested ${canonicalAssetAmount(params.amount)}`
|
|
1804
|
+
);
|
|
1805
|
+
}
|
|
1806
|
+
const offer = await createOffer(wallet, arkServerUrl, {
|
|
1807
|
+
wantAmount: terms.wantAmount,
|
|
1808
|
+
...terms.wantAsset !== void 0 ? { wantAsset: terms.wantAsset } : {},
|
|
1809
|
+
...terms.offerAsset !== void 0 ? { offerAsset: terms.offerAsset } : {},
|
|
1810
|
+
...params.emulatorPubkey !== void 0 ? { emulatorPubkey: params.emulatorPubkey } : {}
|
|
1811
|
+
});
|
|
1812
|
+
verifyOfferAddress(quote, offer);
|
|
1813
|
+
const fundAmount = BigInt(quote.from_amount);
|
|
1814
|
+
return {
|
|
1815
|
+
rfqId,
|
|
1816
|
+
quote,
|
|
1817
|
+
pair,
|
|
1818
|
+
address: offer.address,
|
|
1819
|
+
fundAmount,
|
|
1820
|
+
carrierSats: params.offerAsset !== void 0 ? ASSET_CARRIER_SATS : 0n,
|
|
1821
|
+
swapPkScript: offer.swapPkScript,
|
|
1822
|
+
offerHex: offer.offerHex,
|
|
1823
|
+
extension: offer.extension
|
|
1824
|
+
};
|
|
1825
|
+
}
|
|
913
1826
|
var l1NetworkFromArk = (network) => network === "bitcoin" ? "bitcoin" : network === "regtest" ? "regtest" : "testnet";
|
|
914
1827
|
var onchainSendRequest = (input) => ({
|
|
915
1828
|
v: 1,
|
|
@@ -920,9 +1833,9 @@ var onchainSendRequest = (input) => ({
|
|
|
920
1833
|
amount: input.amount,
|
|
921
1834
|
profile: {
|
|
922
1835
|
payment_hash: input.paymentHash,
|
|
923
|
-
payout_pubkey:
|
|
1836
|
+
payout_pubkey: hex5.encode(input.payoutPubkey),
|
|
924
1837
|
refund_address: input.refundAddress,
|
|
925
|
-
client_refund_pubkey:
|
|
1838
|
+
client_refund_pubkey: hex5.encode(input.senderPubkey)
|
|
926
1839
|
}
|
|
927
1840
|
});
|
|
928
1841
|
var lightningReceiveRequest = (input) => ({
|
|
@@ -935,7 +1848,7 @@ var lightningReceiveRequest = (input) => ({
|
|
|
935
1848
|
profile: {
|
|
936
1849
|
payment_hash: input.paymentHash,
|
|
937
1850
|
payout_address: input.payoutAddress,
|
|
938
|
-
payout_pubkey:
|
|
1851
|
+
payout_pubkey: hex5.encode(input.payoutPubkey),
|
|
939
1852
|
...input.claimPacket === void 0 ? {} : { claim_packet: input.claimPacket }
|
|
940
1853
|
}
|
|
941
1854
|
});
|
|
@@ -949,9 +1862,9 @@ var onchainReceiveRequest = (input) => ({
|
|
|
949
1862
|
profile: {
|
|
950
1863
|
payment_hash: input.paymentHash,
|
|
951
1864
|
...input.claimPacket === void 0 ? {} : { claim_packet: input.claimPacket },
|
|
952
|
-
refund_pubkey:
|
|
1865
|
+
refund_pubkey: hex5.encode(input.refundPubkey),
|
|
953
1866
|
payout_address: input.payoutAddress,
|
|
954
|
-
payout_pubkey:
|
|
1867
|
+
payout_pubkey: hex5.encode(input.payoutPubkey)
|
|
955
1868
|
}
|
|
956
1869
|
});
|
|
957
1870
|
function deriveOnchainSend(input) {
|
|
@@ -967,7 +1880,7 @@ function deriveOnchainSend(input) {
|
|
|
967
1880
|
throw new Error("onchain-send quote is missing a binding field");
|
|
968
1881
|
}
|
|
969
1882
|
const treeParams = {
|
|
970
|
-
solverPubkey: toXOnly(
|
|
1883
|
+
solverPubkey: toXOnly(hex5.decode(quote.solver_pubkey), "solver key"),
|
|
971
1884
|
refundLocktime,
|
|
972
1885
|
serverPubkey: input.serverPubkey,
|
|
973
1886
|
paymentHash: input.paymentHash,
|
|
@@ -975,7 +1888,7 @@ function deriveOnchainSend(input) {
|
|
|
975
1888
|
emulatorPubkey: input.emulatorPubkey,
|
|
976
1889
|
senderPubkey: input.senderPubkey,
|
|
977
1890
|
receiverPkScript: solverHex(receiverPkScriptHex, "profile.receiver_pk_script"),
|
|
978
|
-
refundPkScript:
|
|
1891
|
+
refundPkScript: ArkAddress3.decode(input.refundAddress).pkScript
|
|
979
1892
|
};
|
|
980
1893
|
const { script, address } = matchQuotedLockup(
|
|
981
1894
|
quote,
|
|
@@ -986,7 +1899,7 @@ function deriveOnchainSend(input) {
|
|
|
986
1899
|
const htlcParams = {
|
|
987
1900
|
paymentHash: input.paymentHash,
|
|
988
1901
|
claimKey: input.payoutPubkey,
|
|
989
|
-
refundKey: toXOnly(
|
|
1902
|
+
refundKey: toXOnly(hex5.decode(htlcPubkey), "solver L1 htlc key"),
|
|
990
1903
|
refundLocktime: htlcLocktime
|
|
991
1904
|
};
|
|
992
1905
|
const htlc = onchainHtlcScript(htlcParams, input.l1Network);
|
|
@@ -1000,7 +1913,8 @@ function deriveOnchainSend(input) {
|
|
|
1000
1913
|
l1Network: input.l1Network,
|
|
1001
1914
|
refundLocktime,
|
|
1002
1915
|
htlcLocktime,
|
|
1003
|
-
minConfirmations
|
|
1916
|
+
minConfirmations,
|
|
1917
|
+
expectedAmount: quoteSats(quote.to_amount, "to_amount")
|
|
1004
1918
|
};
|
|
1005
1919
|
}
|
|
1006
1920
|
async function requestOnchainSend(wallet, arkServerUrl, transport, params) {
|
|
@@ -1011,10 +1925,10 @@ async function requestOnchainSend(wallet, arkServerUrl, transport, params) {
|
|
|
1011
1925
|
"[swap] this swap's preimage cannot be re-derived from the seed and MUST be persisted with the record before funding"
|
|
1012
1926
|
);
|
|
1013
1927
|
}
|
|
1014
|
-
const paymentHash =
|
|
1928
|
+
const paymentHash = hex5.encode(secrets.paymentHash);
|
|
1015
1929
|
const senderPubkey = secrets.pubkey;
|
|
1016
1930
|
const [info, refundAddress] = await Promise.all([
|
|
1017
|
-
new
|
|
1931
|
+
new RestArkProvider2(arkServerUrl).getInfo(),
|
|
1018
1932
|
wallet.getAddress()
|
|
1019
1933
|
]);
|
|
1020
1934
|
const quote = await transport.requestQuote(
|
|
@@ -1029,14 +1943,14 @@ async function requestOnchainSend(wallet, arkServerUrl, transport, params) {
|
|
|
1029
1943
|
})
|
|
1030
1944
|
);
|
|
1031
1945
|
assertQuotedAmount(quote, params.amountSide, params.amount);
|
|
1032
|
-
const network =
|
|
1946
|
+
const network = getNetwork2(info.network);
|
|
1033
1947
|
const derived = deriveOnchainSend({
|
|
1034
1948
|
quote,
|
|
1035
1949
|
paymentHash,
|
|
1036
1950
|
payoutPubkey: params.payoutPubkey,
|
|
1037
|
-
serverPubkey: toXOnly(
|
|
1951
|
+
serverPubkey: toXOnly(hex5.decode(info.signerPubkey), "ark signer key"),
|
|
1038
1952
|
emulatorPubkey: toXOnly(
|
|
1039
|
-
|
|
1953
|
+
hex5.decode(resolveEmulatorPubkey2(network, params.emulatorPubkey)),
|
|
1040
1954
|
"emulator signer key"
|
|
1041
1955
|
),
|
|
1042
1956
|
claimDelay: unilateralClaimDelay(Number(info.unilateralExitDelay)),
|
|
@@ -1063,7 +1977,8 @@ async function requestOnchainSend(wallet, arkServerUrl, transport, params) {
|
|
|
1063
1977
|
rfqId,
|
|
1064
1978
|
quote,
|
|
1065
1979
|
address: derived.address,
|
|
1066
|
-
fundAmount: quote.from_amount,
|
|
1980
|
+
fundAmount: quoteSats(quote.from_amount, "from_amount"),
|
|
1981
|
+
expectedAmount: quoteSats(quote.to_amount, "to_amount"),
|
|
1067
1982
|
swapPkScript: derived.swapPkScript,
|
|
1068
1983
|
script: derived.script,
|
|
1069
1984
|
refundAddress,
|
|
@@ -1076,7 +1991,7 @@ async function requestOnchainSend(wallet, arkServerUrl, transport, params) {
|
|
|
1076
1991
|
};
|
|
1077
1992
|
}
|
|
1078
1993
|
var assertQuotedAmount = (quote, amountSide, amount) => {
|
|
1079
|
-
const quoted = amountSide === "from" ? quote.from_amount : quote.to_amount;
|
|
1994
|
+
const quoted = amountSide === "from" ? quoteSats(quote.from_amount, "from_amount") : quoteSats(quote.to_amount, "to_amount");
|
|
1080
1995
|
if (quoted !== amount) {
|
|
1081
1996
|
throw new Error(
|
|
1082
1997
|
`quote ${amountSide === "from" ? "from_amount" : "to_amount"} ${quoted} does not match the requested ${amount} \u2014 not this trade's quote`
|
|
@@ -1135,7 +2050,7 @@ var assertReceivable = (input) => {
|
|
|
1135
2050
|
`a payment at the deadline would leave under ${minClaimWindow}s to claim before the solver's refund opens`
|
|
1136
2051
|
);
|
|
1137
2052
|
}
|
|
1138
|
-
if (input.maxPayAmount !== void 0 && input.quote.from_amount > input.maxPayAmount) {
|
|
2053
|
+
if (input.maxPayAmount !== void 0 && quoteSats(input.quote.from_amount, "from_amount") > input.maxPayAmount) {
|
|
1139
2054
|
throw gateError(
|
|
1140
2055
|
"price_too_high",
|
|
1141
2056
|
`quote asks ${input.quote.from_amount} sats, above the ${input.maxPayAmount} ceiling`
|
|
@@ -1151,7 +2066,7 @@ function receiveVtxoScript(params) {
|
|
|
1151
2066
|
sender: params.solverPubkey,
|
|
1152
2067
|
receiver: params.payoutPubkey,
|
|
1153
2068
|
server: params.serverPubkey,
|
|
1154
|
-
preimageHash: ripemd1602(
|
|
2069
|
+
preimageHash: ripemd1602(hex5.decode(params.paymentHash)),
|
|
1155
2070
|
refundLocktime: BigInt(params.refundLocktime),
|
|
1156
2071
|
unilateralClaimDelay: seconds(params.claimDelay),
|
|
1157
2072
|
unilateralRefundDelay: seconds(unilateralRefundDelay(params.claimDelay)),
|
|
@@ -1176,7 +2091,7 @@ function deriveLightningReceive(input) {
|
|
|
1176
2091
|
throw new Error("lightning-receive quote is missing a binding field");
|
|
1177
2092
|
}
|
|
1178
2093
|
const treeParams = {
|
|
1179
|
-
solverPubkey: toXOnly(
|
|
2094
|
+
solverPubkey: toXOnly(hex5.decode(quote.solver_pubkey), "solver key"),
|
|
1180
2095
|
refundLocktime,
|
|
1181
2096
|
serverPubkey: input.serverPubkey,
|
|
1182
2097
|
paymentHash: input.paymentHash,
|
|
@@ -1184,7 +2099,7 @@ function deriveLightningReceive(input) {
|
|
|
1184
2099
|
emulatorPubkey: input.emulatorPubkey,
|
|
1185
2100
|
solverRefundPkScript: solverHex(solverRefundPkScriptHex, "profile.solver_refund_pk_script"),
|
|
1186
2101
|
payoutPubkey: input.payoutPubkey,
|
|
1187
|
-
payoutPkScript:
|
|
2102
|
+
payoutPkScript: ArkAddress3.decode(input.payoutAddress).pkScript
|
|
1188
2103
|
};
|
|
1189
2104
|
const matched = matchQuotedLockup(
|
|
1190
2105
|
quote,
|
|
@@ -1213,10 +2128,10 @@ async function requestLightningReceive(wallet, arkServerUrl, transport, params)
|
|
|
1213
2128
|
);
|
|
1214
2129
|
}
|
|
1215
2130
|
const preimage = secrets.preimage;
|
|
1216
|
-
const paymentHash =
|
|
2131
|
+
const paymentHash = hex5.encode(secrets.paymentHash);
|
|
1217
2132
|
const payoutPubkey = secrets.pubkey;
|
|
1218
2133
|
const [info, payoutAddress] = await Promise.all([
|
|
1219
|
-
new
|
|
2134
|
+
new RestArkProvider2(arkServerUrl).getInfo(),
|
|
1220
2135
|
wallet.getAddress()
|
|
1221
2136
|
]);
|
|
1222
2137
|
const claimPacket = params.covclaimdPubkey ? await sealClaimPacket({
|
|
@@ -1235,15 +2150,15 @@ async function requestLightningReceive(wallet, arkServerUrl, transport, params)
|
|
|
1235
2150
|
})
|
|
1236
2151
|
);
|
|
1237
2152
|
assertQuotedAmount(quote, params.amountSide, params.amount);
|
|
1238
|
-
const network =
|
|
2153
|
+
const network = getNetwork2(info.network);
|
|
1239
2154
|
const derived = deriveLightningReceive({
|
|
1240
2155
|
quote,
|
|
1241
2156
|
paymentHash,
|
|
1242
2157
|
payoutPubkey,
|
|
1243
2158
|
payoutAddress,
|
|
1244
|
-
serverPubkey: toXOnly(
|
|
2159
|
+
serverPubkey: toXOnly(hex5.decode(info.signerPubkey), "ark signer key"),
|
|
1245
2160
|
emulatorPubkey: toXOnly(
|
|
1246
|
-
|
|
2161
|
+
hex5.decode(resolveEmulatorPubkey2(network, params.emulatorPubkey)),
|
|
1247
2162
|
"emulator signer key"
|
|
1248
2163
|
),
|
|
1249
2164
|
claimDelay: unilateralClaimDelay(Number(info.unilateralExitDelay)),
|
|
@@ -1266,8 +2181,8 @@ async function requestLightningReceive(wallet, arkServerUrl, transport, params)
|
|
|
1266
2181
|
rfqId,
|
|
1267
2182
|
quote,
|
|
1268
2183
|
invoice: derived.invoice,
|
|
1269
|
-
payAmount: quote.from_amount,
|
|
1270
|
-
expectedAmount: quote.to_amount,
|
|
2184
|
+
payAmount: quoteSats(quote.from_amount, "from_amount"),
|
|
2185
|
+
expectedAmount: quoteSats(quote.to_amount, "to_amount"),
|
|
1271
2186
|
invoiceExpiresAt: payDeadline,
|
|
1272
2187
|
address: derived.address,
|
|
1273
2188
|
swapPkScript: derived.swapPkScript,
|
|
@@ -1291,7 +2206,7 @@ function deriveOnchainReceive(input) {
|
|
|
1291
2206
|
throw new Error("onchain-receive quote is missing a binding field");
|
|
1292
2207
|
}
|
|
1293
2208
|
const treeParams = {
|
|
1294
|
-
solverPubkey: toXOnly(
|
|
2209
|
+
solverPubkey: toXOnly(hex5.decode(quote.solver_pubkey), "solver key"),
|
|
1295
2210
|
refundLocktime,
|
|
1296
2211
|
serverPubkey: input.serverPubkey,
|
|
1297
2212
|
paymentHash: input.paymentHash,
|
|
@@ -1299,7 +2214,7 @@ function deriveOnchainReceive(input) {
|
|
|
1299
2214
|
emulatorPubkey: input.emulatorPubkey,
|
|
1300
2215
|
solverRefundPkScript: solverHex(solverRefundPkScriptHex, "profile.solver_refund_pk_script"),
|
|
1301
2216
|
payoutPubkey: input.payoutPubkey,
|
|
1302
|
-
payoutPkScript:
|
|
2217
|
+
payoutPkScript: ArkAddress3.decode(input.payoutAddress).pkScript
|
|
1303
2218
|
};
|
|
1304
2219
|
const { script, address } = matchQuotedLockup(
|
|
1305
2220
|
quote,
|
|
@@ -1310,7 +2225,7 @@ function deriveOnchainReceive(input) {
|
|
|
1310
2225
|
const htlc = onchainHtlcScript(
|
|
1311
2226
|
{
|
|
1312
2227
|
paymentHash: input.paymentHash,
|
|
1313
|
-
claimKey: toXOnly(
|
|
2228
|
+
claimKey: toXOnly(hex5.decode(claimPubkey), "solver L1 claim key"),
|
|
1314
2229
|
refundKey: input.refundPubkey,
|
|
1315
2230
|
refundLocktime: htlcLocktime
|
|
1316
2231
|
},
|
|
@@ -1336,10 +2251,10 @@ async function requestOnchainReceive(wallet, arkServerUrl, transport, params) {
|
|
|
1336
2251
|
);
|
|
1337
2252
|
}
|
|
1338
2253
|
const preimage = secrets.preimage;
|
|
1339
|
-
const paymentHash =
|
|
2254
|
+
const paymentHash = hex5.encode(secrets.paymentHash);
|
|
1340
2255
|
const payoutPubkey = secrets.pubkey;
|
|
1341
2256
|
const [info, payoutAddress] = await Promise.all([
|
|
1342
|
-
new
|
|
2257
|
+
new RestArkProvider2(arkServerUrl).getInfo(),
|
|
1343
2258
|
wallet.getAddress()
|
|
1344
2259
|
]);
|
|
1345
2260
|
const claimPacket = params.covclaimdPubkey ? await sealClaimPacket({
|
|
@@ -1359,16 +2274,16 @@ async function requestOnchainReceive(wallet, arkServerUrl, transport, params) {
|
|
|
1359
2274
|
})
|
|
1360
2275
|
);
|
|
1361
2276
|
assertQuotedAmount(quote, params.amountSide, params.amount);
|
|
1362
|
-
const network =
|
|
2277
|
+
const network = getNetwork2(info.network);
|
|
1363
2278
|
const derived = deriveOnchainReceive({
|
|
1364
2279
|
quote,
|
|
1365
2280
|
paymentHash,
|
|
1366
2281
|
payoutPubkey,
|
|
1367
2282
|
payoutAddress,
|
|
1368
2283
|
refundPubkey: params.refundPubkey,
|
|
1369
|
-
serverPubkey: toXOnly(
|
|
2284
|
+
serverPubkey: toXOnly(hex5.decode(info.signerPubkey), "ark signer key"),
|
|
1370
2285
|
emulatorPubkey: toXOnly(
|
|
1371
|
-
|
|
2286
|
+
hex5.decode(resolveEmulatorPubkey2(network, params.emulatorPubkey)),
|
|
1372
2287
|
"emulator signer key"
|
|
1373
2288
|
),
|
|
1374
2289
|
claimDelay: unilateralClaimDelay(Number(info.unilateralExitDelay)),
|
|
@@ -1393,8 +2308,8 @@ async function requestOnchainReceive(wallet, arkServerUrl, transport, params) {
|
|
|
1393
2308
|
rfqId,
|
|
1394
2309
|
quote,
|
|
1395
2310
|
address: derived.address,
|
|
1396
|
-
fundAmount: quote.from_amount,
|
|
1397
|
-
expectedAmount: quote.to_amount,
|
|
2311
|
+
fundAmount: quoteSats(quote.from_amount, "from_amount"),
|
|
2312
|
+
expectedAmount: quoteSats(quote.to_amount, "to_amount"),
|
|
1398
2313
|
swapPkScript: derived.swapPkScript,
|
|
1399
2314
|
script: derived.script,
|
|
1400
2315
|
htlc: derived.htlc,
|
|
@@ -1405,6 +2320,29 @@ async function requestOnchainReceive(wallet, arkServerUrl, transport, params) {
|
|
|
1405
2320
|
}
|
|
1406
2321
|
|
|
1407
2322
|
export {
|
|
2323
|
+
RETIRABLE,
|
|
2324
|
+
retireOfferContract,
|
|
2325
|
+
retireSettledOfferContracts,
|
|
2326
|
+
BTC_ASSET_ID,
|
|
2327
|
+
getAssetSwapsOrThrow,
|
|
2328
|
+
getAssetSwaps,
|
|
2329
|
+
addAssetSwap,
|
|
2330
|
+
updateAssetSwap,
|
|
2331
|
+
updateAssetSwapBestEffort,
|
|
2332
|
+
swapSecretsToRecord,
|
|
2333
|
+
PreimageNotRecoverableError,
|
|
2334
|
+
preimageForSwapRecord,
|
|
2335
|
+
swapPrograms,
|
|
2336
|
+
offerVtxoScript,
|
|
2337
|
+
OFFER_PACKET_TYPE,
|
|
2338
|
+
encodeOffer,
|
|
2339
|
+
decodeOffer,
|
|
2340
|
+
OFFER_CONTRACT_KIND,
|
|
2341
|
+
restoreOfferCoverage,
|
|
2342
|
+
createOffer,
|
|
2343
|
+
cancelOffer,
|
|
2344
|
+
ASSET_CARRIER_SATS,
|
|
2345
|
+
fillOffer,
|
|
1408
2346
|
ONCHAIN_ORDER_MARGIN_SECONDS,
|
|
1409
2347
|
ONCHAIN_CLAIM_MARGIN_SECONDS,
|
|
1410
2348
|
MAX_MIN_CONFIRMATIONS,
|
|
@@ -1452,6 +2390,9 @@ export {
|
|
|
1452
2390
|
newRfqId,
|
|
1453
2391
|
lightningSendRequest,
|
|
1454
2392
|
arkadeSwapRequest,
|
|
2393
|
+
canonicalAssetAmount,
|
|
2394
|
+
normalizeMakerPkScript,
|
|
2395
|
+
normalizeMakerPublicKey,
|
|
1455
2396
|
MIN_HEADROOM_SECONDS,
|
|
1456
2397
|
verifyLockupAddress,
|
|
1457
2398
|
assertFundable,
|
|
@@ -1466,6 +2407,9 @@ export {
|
|
|
1466
2407
|
lightningSendVtxoScript,
|
|
1467
2408
|
requestLightningSend,
|
|
1468
2409
|
offerTermsFromQuote,
|
|
2410
|
+
verifyOfferAddress,
|
|
2411
|
+
assertArkadeFundable,
|
|
2412
|
+
requestArkadeSwap,
|
|
1469
2413
|
onchainSendRequest,
|
|
1470
2414
|
lightningReceiveRequest,
|
|
1471
2415
|
onchainReceiveRequest,
|