@arkade-os/swap 0.0.10 → 0.1.0-rc.0
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 +59 -15
- package/dist/{chunk-5NPYNQ5V.js → chunk-AM3NNUMR.js} +112 -66
- package/dist/index.cjs +763 -227
- package/dist/index.d.cts +184 -47
- package/dist/index.d.ts +184 -47
- package/dist/index.js +653 -169
- package/dist/nostr.cjs +4 -1
- 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 +4 -4
- package/dist/repositories/realm/index.d.ts +4 -4
- package/dist/repositories/sqlite/index.d.cts +2 -2
- package/dist/repositories/sqlite/index.d.ts +2 -2
- package/dist/{repository-C8FXlmHY.d.cts → repository-BWP1UstE.d.cts} +124 -59
- package/dist/{repository-Cw2EvvZG.d.ts → repository-DLpH41ZO.d.ts} +124 -59
- package/dist/{rfq-DglvHMNC.d.ts → rfq-CzCGjICq.d.cts} +86 -48
- package/dist/{rfq-DglvHMNC.d.cts → rfq-CzCGjICq.d.ts} +86 -48
- package/package.json +4 -3
package/dist/index.js
CHANGED
|
@@ -38,9 +38,10 @@ import {
|
|
|
38
38
|
deriveOnchainSend,
|
|
39
39
|
extractPreimage,
|
|
40
40
|
httpTransport,
|
|
41
|
+
lightningReceiveContract,
|
|
41
42
|
lightningReceiveRequest,
|
|
43
|
+
lightningSendContract,
|
|
42
44
|
lightningSendRequest,
|
|
43
|
-
lightningSendVtxoScript,
|
|
44
45
|
lockupContractParams,
|
|
45
46
|
newPreimage,
|
|
46
47
|
newRfqId,
|
|
@@ -49,7 +50,6 @@ import {
|
|
|
49
50
|
onchainReceiveRequest,
|
|
50
51
|
onchainSendRequest,
|
|
51
52
|
paymentHashOf,
|
|
52
|
-
receiveVtxoScript,
|
|
53
53
|
registerLockupContract,
|
|
54
54
|
relayTransport,
|
|
55
55
|
requestLightningReceive,
|
|
@@ -63,7 +63,7 @@ import {
|
|
|
63
63
|
unilateralRefundWithoutReceiverDelay,
|
|
64
64
|
verifyLockupAddress,
|
|
65
65
|
verifyReceiveInvoice
|
|
66
|
-
} from "./chunk-
|
|
66
|
+
} from "./chunk-AM3NNUMR.js";
|
|
67
67
|
import {
|
|
68
68
|
InMemoryAssetSwapRepository,
|
|
69
69
|
marketsCacheKey
|
|
@@ -74,11 +74,9 @@ import { hex as hex2 } from "@scure/base";
|
|
|
74
74
|
import { concatBytes } from "@scure/btc-signer/utils.js";
|
|
75
75
|
import {
|
|
76
76
|
ArkAddress,
|
|
77
|
-
RestArkProvider,
|
|
78
|
-
RestIndexerProvider,
|
|
79
77
|
arkade,
|
|
80
78
|
asset,
|
|
81
|
-
|
|
79
|
+
networkFromArkadeInfo,
|
|
82
80
|
resolveEmulatorPubkey,
|
|
83
81
|
toXOnlySignerHex
|
|
84
82
|
} from "@arkade-os/sdk";
|
|
@@ -329,32 +327,51 @@ var swapPrograms = {
|
|
|
329
327
|
wantAsset: arkade.parseArtifact(swap_want_asset_program_default),
|
|
330
328
|
wantBtc: arkade.parseArtifact(swap_want_btc_program_default)
|
|
331
329
|
};
|
|
332
|
-
function
|
|
330
|
+
function withExitClosure(program, exit) {
|
|
331
|
+
if (!exit) return program;
|
|
332
|
+
return {
|
|
333
|
+
...program,
|
|
334
|
+
// typed params are authoritative: an undeclared `$exitDelay` fails
|
|
335
|
+
// validateProgram instead of compiling against an unbound value
|
|
336
|
+
params: [...program.params ?? [], { name: "exitDelay", type: "int" }],
|
|
337
|
+
functions: {
|
|
338
|
+
...program.functions,
|
|
339
|
+
exit: {
|
|
340
|
+
tapscript: { signers: ["$user"], csv: { type: exit.type, value: "$exitDelay" } }
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
};
|
|
344
|
+
}
|
|
345
|
+
function swapProgramBinding(offer, operatorPubkey) {
|
|
333
346
|
if (offer.makerPkScript.length !== FIELDS.makerPkScript.width) {
|
|
334
347
|
throw new Error("makerPkScript is not a 34-byte taproot scriptPubKey");
|
|
335
348
|
}
|
|
336
349
|
return {
|
|
337
|
-
program:
|
|
350
|
+
program: withExitClosure(
|
|
351
|
+
offer.wantAsset ? swapPrograms.wantAsset : swapPrograms.wantBtc,
|
|
352
|
+
offer.exitDelay
|
|
353
|
+
),
|
|
338
354
|
args: {
|
|
339
355
|
makerWP: offer.makerPkScript.subarray(2),
|
|
340
356
|
wantAmount: offer.wantAmount,
|
|
341
|
-
server:
|
|
357
|
+
server: operatorPubkey,
|
|
342
358
|
user: offer.makerPublicKey,
|
|
343
359
|
// internal byte order
|
|
344
360
|
...offer.wantAsset && {
|
|
345
361
|
wantAssetTxid: offer.wantAsset.txid.slice().reverse(),
|
|
346
362
|
wantAssetGroupIndex: offer.wantAsset.groupIndex
|
|
347
|
-
}
|
|
363
|
+
},
|
|
364
|
+
...offer.exitDelay && { exitDelay: offer.exitDelay.value }
|
|
348
365
|
},
|
|
349
366
|
keys: {
|
|
350
|
-
serverKey:
|
|
367
|
+
serverKey: operatorPubkey,
|
|
351
368
|
userKey: offer.makerPublicKey,
|
|
352
369
|
emulatorKey: offer.emulatorPubkey
|
|
353
370
|
}
|
|
354
371
|
};
|
|
355
372
|
}
|
|
356
|
-
function
|
|
357
|
-
const { program, args, keys } = swapProgramBinding(offer,
|
|
373
|
+
function offerContract(offer, operatorPubkey) {
|
|
374
|
+
const { program, args, keys } = swapProgramBinding(offer, operatorPubkey);
|
|
358
375
|
return new arkade.ArkadeProgramScript(program, args, keys);
|
|
359
376
|
}
|
|
360
377
|
var OFFER_PACKET_TYPE = 3;
|
|
@@ -365,9 +382,27 @@ var FIELDS = {
|
|
|
365
382
|
makerPkScript: { tag: 5, width: 34 },
|
|
366
383
|
makerPublicKey: { tag: 7, width: 32 },
|
|
367
384
|
emulatorPubkey: { tag: 8, width: 32 },
|
|
368
|
-
|
|
385
|
+
ratioNum: { tag: 9, width: 8 },
|
|
386
|
+
ratioDen: { tag: 10, width: 8 },
|
|
387
|
+
offerAsset: { tag: 11, width: void 0 },
|
|
388
|
+
exitTimelock: { tag: 12, width: 9 }
|
|
369
389
|
};
|
|
390
|
+
var EXIT_TYPES = ["blocks", "seconds"];
|
|
370
391
|
var NAMES = Object.fromEntries(Object.entries(FIELDS).map(([k, f]) => [f.tag, k]));
|
|
392
|
+
function u64(name, value) {
|
|
393
|
+
if (value < BigInt(0) || value >> BigInt(64) > BigInt(0)) {
|
|
394
|
+
throw new Error(`${name} does not fit the offer wire format (u64)`);
|
|
395
|
+
}
|
|
396
|
+
const out = new Uint8Array(FIELDS.wantAmount.width);
|
|
397
|
+
new DataView(out.buffer).setBigUint64(0, value, false);
|
|
398
|
+
return out;
|
|
399
|
+
}
|
|
400
|
+
var readU64 = (value) => new DataView(value.buffer, value.byteOffset).getBigUint64(0, false);
|
|
401
|
+
var setRatio = (name, value) => {
|
|
402
|
+
if (value === void 0 || value === BigInt(0)) return void 0;
|
|
403
|
+
if (value < BigInt(0)) throw new Error(`${name} does not fit the offer wire format (u64)`);
|
|
404
|
+
return value;
|
|
405
|
+
};
|
|
371
406
|
function tlv(type, value) {
|
|
372
407
|
if (value.length > 65535) throw new Error("TLV value exceeds the u16 length field");
|
|
373
408
|
return concatBytes(Uint8Array.of(type, value.length >> 8 & 255, value.length & 255), value);
|
|
@@ -386,24 +421,45 @@ function encodeOffer(offer) {
|
|
|
386
421
|
throw new Error(`${name} must be ${FIELDS[name].width} bytes`);
|
|
387
422
|
}
|
|
388
423
|
}
|
|
389
|
-
|
|
390
|
-
|
|
424
|
+
const ratioNum = setRatio("ratioNum", offer.ratioNum);
|
|
425
|
+
const ratioDen = setRatio("ratioDen", offer.ratioDen);
|
|
426
|
+
if (ratioNum === void 0 !== (ratioDen === void 0)) {
|
|
427
|
+
throw new Error("offer must carry both ratioNum and ratioDen, or neither");
|
|
391
428
|
}
|
|
392
|
-
const amount = new Uint8Array(FIELDS.wantAmount.width);
|
|
393
|
-
new DataView(amount.buffer).setBigUint64(0, offer.wantAmount, false);
|
|
394
429
|
const recs = [
|
|
395
430
|
tlv(FIELDS.swapPkScript.tag, offer.swapPkScript),
|
|
396
|
-
tlv(FIELDS.wantAmount.tag,
|
|
431
|
+
tlv(FIELDS.wantAmount.tag, u64("wantAmount", offer.wantAmount))
|
|
397
432
|
];
|
|
398
433
|
if (offer.wantAsset) recs.push(tlv(FIELDS.wantAsset.tag, offer.wantAsset.serialize()));
|
|
434
|
+
if (ratioNum !== void 0) recs.push(tlv(FIELDS.ratioNum.tag, u64("ratioNum", ratioNum)));
|
|
435
|
+
if (ratioDen !== void 0) recs.push(tlv(FIELDS.ratioDen.tag, u64("ratioDen", ratioDen)));
|
|
399
436
|
if (offer.offerAsset) recs.push(tlv(FIELDS.offerAsset.tag, offer.offerAsset.serialize()));
|
|
400
437
|
recs.push(
|
|
401
438
|
tlv(FIELDS.makerPkScript.tag, offer.makerPkScript),
|
|
402
439
|
tlv(FIELDS.makerPublicKey.tag, offer.makerPublicKey),
|
|
403
440
|
tlv(FIELDS.emulatorPubkey.tag, offer.emulatorPubkey)
|
|
404
441
|
);
|
|
442
|
+
if (offer.exitDelay) recs.push(tlv(FIELDS.exitTimelock.tag, encodeExitDelay(offer.exitDelay)));
|
|
405
443
|
return concatBytes(...recs);
|
|
406
444
|
}
|
|
445
|
+
function encodeExitDelay(exit) {
|
|
446
|
+
return concatBytes(
|
|
447
|
+
Uint8Array.of(EXIT_TYPES.indexOf(assertExitDelay(exit).type)),
|
|
448
|
+
u64("exitDelay", exit.value)
|
|
449
|
+
);
|
|
450
|
+
}
|
|
451
|
+
function assertExitDelay(exit) {
|
|
452
|
+
if (EXIT_TYPES.indexOf(exit.type) < 0) {
|
|
453
|
+
throw new Error(`unknown exitDelay locktime type: ${exit.type}`);
|
|
454
|
+
}
|
|
455
|
+
if (exit.value <= BigInt(0)) {
|
|
456
|
+
throw new Error("exitDelay must be a positive relative locktime");
|
|
457
|
+
}
|
|
458
|
+
if (exit.value >> BigInt(32) > BigInt(0)) {
|
|
459
|
+
throw new Error("exitDelay does not fit the locktime field (u32)");
|
|
460
|
+
}
|
|
461
|
+
return exit;
|
|
462
|
+
}
|
|
407
463
|
function decodeOffer(data) {
|
|
408
464
|
const fields = {};
|
|
409
465
|
let off = 0;
|
|
@@ -423,40 +479,70 @@ function decodeOffer(data) {
|
|
|
423
479
|
for (const name of ["wantAsset", "offerAsset"]) {
|
|
424
480
|
if (fields[name]?.length === 0) throw new Error(`missing/invalid ${name}`);
|
|
425
481
|
}
|
|
426
|
-
const
|
|
482
|
+
for (const [name, value] of Object.entries(fields)) {
|
|
483
|
+
const width = FIELDS[name].width;
|
|
484
|
+
if (width !== void 0 && value.length !== width) {
|
|
485
|
+
throw new Error(`missing/invalid ${name}`);
|
|
486
|
+
}
|
|
487
|
+
}
|
|
488
|
+
const need2 = (name) => {
|
|
427
489
|
const v = fields[name];
|
|
428
490
|
const len = FIELDS[name].width;
|
|
429
491
|
if (!v || len !== void 0 && v.length !== len)
|
|
430
492
|
throw new Error(`missing/invalid ${name}`);
|
|
431
493
|
return v;
|
|
432
494
|
};
|
|
433
|
-
const amount =
|
|
495
|
+
const amount = need2("wantAmount");
|
|
434
496
|
if (Boolean(fields.wantAsset) === Boolean(fields.offerAsset)) {
|
|
435
497
|
throw new Error("offer must carry exactly one of wantAsset or offerAsset");
|
|
436
498
|
}
|
|
499
|
+
const readRatio = (name) => {
|
|
500
|
+
const raw = fields[name];
|
|
501
|
+
if (!raw) return void 0;
|
|
502
|
+
const value = readU64(raw);
|
|
503
|
+
if (value === BigInt(0)) throw new Error(`missing/invalid ${name}`);
|
|
504
|
+
return value;
|
|
505
|
+
};
|
|
506
|
+
const ratioNum = readRatio("ratioNum");
|
|
507
|
+
const ratioDen = readRatio("ratioDen");
|
|
508
|
+
if (ratioNum === void 0 !== (ratioDen === void 0)) {
|
|
509
|
+
throw new Error("offer must carry both ratioNum and ratioDen, or neither");
|
|
510
|
+
}
|
|
437
511
|
return {
|
|
438
|
-
swapPkScript:
|
|
439
|
-
wantAmount:
|
|
512
|
+
swapPkScript: need2("swapPkScript"),
|
|
513
|
+
wantAmount: readU64(amount),
|
|
440
514
|
...fields.wantAsset && { wantAsset: asset.AssetId.fromBytes(fields.wantAsset) },
|
|
441
515
|
...fields.offerAsset && { offerAsset: asset.AssetId.fromBytes(fields.offerAsset) },
|
|
442
|
-
makerPkScript:
|
|
443
|
-
makerPublicKey:
|
|
444
|
-
emulatorPubkey:
|
|
516
|
+
makerPkScript: need2("makerPkScript"),
|
|
517
|
+
makerPublicKey: need2("makerPublicKey"),
|
|
518
|
+
emulatorPubkey: need2("emulatorPubkey"),
|
|
519
|
+
...ratioNum !== void 0 && { ratioNum },
|
|
520
|
+
...ratioDen !== void 0 && { ratioDen },
|
|
521
|
+
...fields.exitTimelock && { exitDelay: decodeExitDelay(fields.exitTimelock) }
|
|
445
522
|
};
|
|
446
523
|
}
|
|
524
|
+
function decodeExitDelay(value) {
|
|
525
|
+
const type = EXIT_TYPES[value[0]];
|
|
526
|
+
if (!type) throw new Error(`unknown exitDelay locktime type: 0x${value[0].toString(16)}`);
|
|
527
|
+
return { type, value: readU64(value.subarray(1)) };
|
|
528
|
+
}
|
|
447
529
|
var OFFER_CONTRACT_LABEL = "Arkade swap offer";
|
|
448
530
|
var OFFER_CONTRACT_KIND = "asset-swap-offer";
|
|
449
|
-
async function registerOfferContract(wallet,
|
|
450
|
-
const { program, args, keys } = swapProgramBinding(binding,
|
|
531
|
+
async function registerOfferContract(wallet, info, binding, operatorPubkey, expectedPkScript) {
|
|
532
|
+
const { program, args, keys } = swapProgramBinding(binding, operatorPubkey);
|
|
451
533
|
const contractManager = await wallet.getContractManager();
|
|
452
534
|
const client = await arkade.Arkade.connect({
|
|
453
|
-
|
|
454
|
-
|
|
535
|
+
// Registration derives and persists; it never broadcasts and never
|
|
536
|
+
// reads UTXOs, so the only thing the client needs off the server is the
|
|
537
|
+
// info the caller already resolved. Handing that back — rather than a
|
|
538
|
+
// provider built from a URL — is what lets `createOffer` take just a
|
|
539
|
+
// wallet, and spares a second `/v1/info` round-trip.
|
|
540
|
+
arkade: { getInfo: async () => info },
|
|
455
541
|
identity: wallet.identity,
|
|
456
542
|
// without this the row's `address` would be derived against the SDK's
|
|
457
543
|
// default network while its script is right — a row that disagrees with
|
|
458
544
|
// the address the user is about to fund
|
|
459
|
-
network:
|
|
545
|
+
network: networkFromArkadeInfo(info),
|
|
460
546
|
contractManager
|
|
461
547
|
});
|
|
462
548
|
const contract = new arkade.ArkadeContract(client, program, args, keys);
|
|
@@ -469,17 +555,29 @@ async function registerOfferContract(wallet, arkServerUrl, network, binding, ser
|
|
|
469
555
|
});
|
|
470
556
|
await promoteOfferContract(contractManager, hex2.encode(expectedPkScript));
|
|
471
557
|
}
|
|
472
|
-
|
|
558
|
+
function serverExitDelay(delay) {
|
|
559
|
+
if (typeof delay !== "bigint" || delay <= BigInt(0)) {
|
|
560
|
+
throw new Error(
|
|
561
|
+
"the server reports no usable unilateralExitDelay; pass `exitDelay` to set the offer's exit closure explicitly, or `noExit: true` to publish without one"
|
|
562
|
+
);
|
|
563
|
+
}
|
|
564
|
+
return assertExitDelay({ value: delay, type: delay < BigInt(512) ? "blocks" : "seconds" });
|
|
565
|
+
}
|
|
566
|
+
async function createOffer(wallet, params) {
|
|
473
567
|
if (Boolean(params.wantAsset) === Boolean(params.offerAsset)) {
|
|
474
568
|
throw new Error("set exactly one of wantAsset (BTC->asset) or offerAsset (asset->BTC)");
|
|
475
569
|
}
|
|
476
570
|
const [info, makerAddress, makerPublicKey] = await Promise.all([
|
|
477
|
-
|
|
571
|
+
// requireLive: this info binds signerPubkey into the covenant — a
|
|
572
|
+
// snapshot could derive an address the operator no longer co-signs
|
|
573
|
+
// for, so an unreachable operator fails the call instead (fail closed,
|
|
574
|
+
// the same behaviour the pre-#734 caller-held provider had)
|
|
575
|
+
wallet.getArkadeInfo({ requireLive: true }),
|
|
478
576
|
wallet.getAddress(),
|
|
479
577
|
wallet.identity.xOnlyPublicKey()
|
|
480
578
|
]);
|
|
481
|
-
const
|
|
482
|
-
const network =
|
|
579
|
+
const operatorPubKey = hex2.decode(toXOnlySignerHex(info.signerPubkey));
|
|
580
|
+
const network = networkFromArkadeInfo(info);
|
|
483
581
|
const emuKey = hex2.decode(
|
|
484
582
|
toXOnlySignerHex(resolveEmulatorPubkey(network, params.emulatorPubkey))
|
|
485
583
|
);
|
|
@@ -489,35 +587,39 @@ async function createOffer(wallet, arkServerUrl, params) {
|
|
|
489
587
|
offerAsset: params.offerAsset,
|
|
490
588
|
makerPkScript: ArkAddress.decode(makerAddress).pkScript,
|
|
491
589
|
makerPublicKey,
|
|
492
|
-
emulatorPubkey: emuKey
|
|
590
|
+
emulatorPubkey: emuKey,
|
|
591
|
+
// checked HERE, before the covenant is derived and registered below:
|
|
592
|
+
// deferring it to `encodeOffer` leaves a registered contract behind for
|
|
593
|
+
// an offer that then fails to encode. @see assertExitDelay
|
|
594
|
+
exitDelay: params.noExit ? void 0 : params.exitDelay ? assertExitDelay(params.exitDelay) : serverExitDelay(info.unilateralExitDelay)
|
|
493
595
|
};
|
|
494
|
-
const script =
|
|
596
|
+
const script = offerContract(binding, operatorPubKey);
|
|
495
597
|
const offer = { ...binding, swapPkScript: script.pkScript };
|
|
496
|
-
await registerOfferContract(
|
|
497
|
-
wallet,
|
|
498
|
-
arkServerUrl,
|
|
499
|
-
info.network,
|
|
500
|
-
binding,
|
|
501
|
-
serverPubKey,
|
|
502
|
-
script.pkScript
|
|
503
|
-
);
|
|
598
|
+
await registerOfferContract(wallet, info, binding, operatorPubKey, script.pkScript);
|
|
504
599
|
const payload = encodeOffer(offer);
|
|
505
600
|
return {
|
|
506
601
|
offerHex: hex2.encode(payload),
|
|
507
602
|
extension: { type: OFFER_PACKET_TYPE, payload },
|
|
508
|
-
//
|
|
603
|
+
// the contract's .address() builds the address; assembling an ArkAddress
|
|
509
604
|
// from tweakedPublicKey here would silently miss any future step it gains
|
|
510
|
-
address: script.address(network.hrp,
|
|
605
|
+
address: script.address(network.hrp, operatorPubKey).encode(),
|
|
511
606
|
swapPkScript: script.pkScript
|
|
512
607
|
};
|
|
513
608
|
}
|
|
514
|
-
async function cancelOffer(wallet,
|
|
609
|
+
async function cancelOffer(wallet, offerHex, opts) {
|
|
515
610
|
const { repository, fundingTxid, swapAddress } = opts;
|
|
516
611
|
const offer = decodeOffer(hex2.decode(offerHex));
|
|
517
|
-
const contractManager = await
|
|
612
|
+
const [contractManager, info, reader, broadcaster] = await Promise.all([
|
|
613
|
+
wallet.getContractManager(),
|
|
614
|
+
wallet.getArkadeInfo({ requireLive: true }),
|
|
615
|
+
wallet.getArkadeReader(),
|
|
616
|
+
wallet.getArkadeBroadcaster()
|
|
617
|
+
]);
|
|
518
618
|
const client = await arkade.Arkade.connect({
|
|
519
|
-
|
|
520
|
-
|
|
619
|
+
// info the wallet already holds, plus its broadcast pair — the shape
|
|
620
|
+
// `ArkadeServerProvider` describes, without a second `/v1/info`
|
|
621
|
+
arkade: { getInfo: async () => info, ...broadcaster },
|
|
622
|
+
indexer: reader,
|
|
521
623
|
identity: wallet.identity,
|
|
522
624
|
// registered offers resolve their VTXOs from the contract repository
|
|
523
625
|
// instead of a direct indexer query; the indexer above stays as the
|
|
@@ -532,7 +634,7 @@ async function cancelOffer(wallet, arkServerUrl, offerHex, opts) {
|
|
|
532
634
|
const rebuilt = new arkade.ArkadeProgramScript(program, args, keys);
|
|
533
635
|
if (hex2.encode(rebuilt.pkScript) !== hex2.encode(offer.swapPkScript)) {
|
|
534
636
|
throw new Error(
|
|
535
|
-
"rebuilt covenant does not match the offer's swapPkScript \u2014 the
|
|
637
|
+
"rebuilt covenant does not match the offer's swapPkScript \u2014 the operator signing key has likely rotated since funding; pass swapAddress (the funded address) to pin the original key"
|
|
536
638
|
);
|
|
537
639
|
}
|
|
538
640
|
const contract = new arkade.ArkadeContract(client, program, args, keys);
|
|
@@ -940,7 +1042,7 @@ var LightningReceiveCorridor = {
|
|
|
940
1042
|
const receive = swap;
|
|
941
1043
|
return {
|
|
942
1044
|
expectedAmount: receive.expectedAmount,
|
|
943
|
-
...receive.
|
|
1045
|
+
...receive.claimTxid ? { claimTxid: receive.claimTxid } : {}
|
|
944
1046
|
};
|
|
945
1047
|
},
|
|
946
1048
|
hydrate(profile) {
|
|
@@ -950,13 +1052,13 @@ var LightningReceiveCorridor = {
|
|
|
950
1052
|
return {
|
|
951
1053
|
...hydrateHashlock(profile),
|
|
952
1054
|
expectedAmount: profile.expectedAmount,
|
|
953
|
-
...profile.
|
|
1055
|
+
...profile.claimTxid ? { claimTxid: profile.claimTxid } : {}
|
|
954
1056
|
};
|
|
955
1057
|
},
|
|
956
1058
|
// We are the claimant here, so the preimage material on the hashlock is
|
|
957
1059
|
// ours to use.
|
|
958
1060
|
claimSecret: (profile) => ({ ...profile.signer, ...profile.hashlock }),
|
|
959
|
-
activityTxids: (profile) => profile.
|
|
1061
|
+
activityTxids: (profile) => profile.claimTxid ? [profile.claimTxid] : []
|
|
960
1062
|
};
|
|
961
1063
|
function onchainSendProfile(result) {
|
|
962
1064
|
return {
|
|
@@ -1040,13 +1142,32 @@ var isRfqSwapTerminal = (state) => RFQ_SWAP_TERMINAL_STATES.includes(state);
|
|
|
1040
1142
|
|
|
1041
1143
|
// src/rfqRecord.ts
|
|
1042
1144
|
var RFQ_SWAP_RETENTION_SECONDS = 30 * 24 * 60 * 60;
|
|
1145
|
+
function renameLegacyClaimTxid(profile) {
|
|
1146
|
+
const { claimArkTxid, ...rest } = profile;
|
|
1147
|
+
return { ...rest, claimTxid: rest.claimTxid ?? claimArkTxid };
|
|
1148
|
+
}
|
|
1149
|
+
function normalizeRfqSwapRecord(record) {
|
|
1150
|
+
const { fundingArkTxid, refundArkTxid, lockupSpendArkTxids, ...rest } = record;
|
|
1151
|
+
const legacyClaim = record.kind === "lightning_receive" && record.profile.claimArkTxid !== void 0;
|
|
1152
|
+
if (!fundingArkTxid && !refundArkTxid && !lockupSpendArkTxids && !legacyClaim) return record;
|
|
1153
|
+
const fundingTxid = rest.fundingTxid ?? fundingArkTxid;
|
|
1154
|
+
const refundTxid = rest.refundTxid ?? refundArkTxid;
|
|
1155
|
+
const lockupSpendTxids = rest.lockupSpendTxids ?? lockupSpendArkTxids;
|
|
1156
|
+
return {
|
|
1157
|
+
...rest,
|
|
1158
|
+
...fundingTxid ? { fundingTxid } : {},
|
|
1159
|
+
...refundTxid ? { refundTxid } : {},
|
|
1160
|
+
...lockupSpendTxids?.length ? { lockupSpendTxids } : {},
|
|
1161
|
+
...legacyClaim ? { profile: renameLegacyClaimTxid(record.profile) } : {}
|
|
1162
|
+
};
|
|
1163
|
+
}
|
|
1043
1164
|
var managerState = (swap) => ({
|
|
1044
1165
|
rfqId: swap.rfqId,
|
|
1045
1166
|
state: swap.state,
|
|
1046
1167
|
createdAt: swap.createdAt,
|
|
1047
1168
|
updatedAt: swap.updatedAt,
|
|
1048
|
-
...swap.
|
|
1049
|
-
...swap.
|
|
1169
|
+
...swap.refundTxid ? { refundTxid: swap.refundTxid } : {},
|
|
1170
|
+
...swap.lockupSpendTxids?.length ? { lockupSpendTxids: [...swap.lockupSpendTxids] } : {},
|
|
1050
1171
|
...swap.failure ? { failure: swap.failure } : {},
|
|
1051
1172
|
...swap.blockedReason ? { blockedReason: swap.blockedReason } : {}
|
|
1052
1173
|
});
|
|
@@ -1077,27 +1198,29 @@ function createRfqSwapRecord(origin, swap) {
|
|
|
1077
1198
|
}
|
|
1078
1199
|
function updateRfqSwapRecord(record, swap) {
|
|
1079
1200
|
assertSameSwap(record, swap);
|
|
1201
|
+
const stored = normalizeRfqSwapRecord(record);
|
|
1080
1202
|
const {
|
|
1081
|
-
|
|
1082
|
-
|
|
1203
|
+
refundTxid: _refundTxid,
|
|
1204
|
+
lockupSpendTxids: _lockupSpendTxids,
|
|
1083
1205
|
failure: _failure,
|
|
1084
1206
|
blockedReason: _blockedReason,
|
|
1085
1207
|
...origin
|
|
1086
|
-
} =
|
|
1087
|
-
const handler = rfqCorridorHandlers.getOrThrow(
|
|
1208
|
+
} = stored;
|
|
1209
|
+
const handler = rfqCorridorHandlers.getOrThrow(stored.kind);
|
|
1088
1210
|
return {
|
|
1089
1211
|
...origin,
|
|
1090
1212
|
...managerState(swap),
|
|
1091
|
-
profile: { ...
|
|
1213
|
+
profile: { ...stored.profile, ...handler.project(swap) }
|
|
1092
1214
|
};
|
|
1093
1215
|
}
|
|
1094
1216
|
function rfqSwapOriginOf(record) {
|
|
1217
|
+
const stored = normalizeRfqSwapRecord(record);
|
|
1095
1218
|
return {
|
|
1096
|
-
kind:
|
|
1097
|
-
lockupAddress:
|
|
1098
|
-
profile: { ...
|
|
1099
|
-
...
|
|
1100
|
-
...
|
|
1219
|
+
kind: stored.kind,
|
|
1220
|
+
lockupAddress: stored.lockupAddress,
|
|
1221
|
+
profile: { ...stored.profile },
|
|
1222
|
+
...stored.amount !== void 0 ? { amount: stored.amount } : {},
|
|
1223
|
+
...stored.fundingTxid ? { fundingTxid: stored.fundingTxid } : {}
|
|
1101
1224
|
};
|
|
1102
1225
|
}
|
|
1103
1226
|
function lockupScript(params, lockupAddress) {
|
|
@@ -1111,27 +1234,28 @@ function lockupScript(params, lockupAddress) {
|
|
|
1111
1234
|
return script;
|
|
1112
1235
|
}
|
|
1113
1236
|
function rebuildRfqSwap(record, params) {
|
|
1114
|
-
const
|
|
1237
|
+
const stored = normalizeRfqSwapRecord(record);
|
|
1238
|
+
const script = lockupScript(params, stored.lockupAddress);
|
|
1115
1239
|
const common = {
|
|
1116
|
-
rfqId:
|
|
1117
|
-
state:
|
|
1240
|
+
rfqId: stored.rfqId,
|
|
1241
|
+
state: stored.state,
|
|
1118
1242
|
lockupPkScript: script.pkScript,
|
|
1119
|
-
lockup: { script, address:
|
|
1243
|
+
lockup: { script, address: stored.lockupAddress },
|
|
1120
1244
|
// From the covenant, which binds it: the record's own copy would be a
|
|
1121
1245
|
// second source for the deadline the refund is gated on.
|
|
1122
1246
|
refundLocktime: Number(script.options.refundLocktime),
|
|
1123
|
-
createdAt:
|
|
1124
|
-
updatedAt:
|
|
1125
|
-
...
|
|
1126
|
-
...
|
|
1127
|
-
...
|
|
1128
|
-
...
|
|
1247
|
+
createdAt: stored.createdAt,
|
|
1248
|
+
updatedAt: stored.updatedAt,
|
|
1249
|
+
...stored.refundTxid ? { refundTxid: stored.refundTxid } : {},
|
|
1250
|
+
...stored.lockupSpendTxids?.length ? { lockupSpendTxids: [...stored.lockupSpendTxids] } : {},
|
|
1251
|
+
...stored.failure ? { failure: stored.failure } : {},
|
|
1252
|
+
...stored.blockedReason ? { blockedReason: stored.blockedReason } : {}
|
|
1129
1253
|
};
|
|
1130
|
-
const handler = rfqCorridorHandlers.getOrThrow(
|
|
1254
|
+
const handler = rfqCorridorHandlers.getOrThrow(stored.kind);
|
|
1131
1255
|
return {
|
|
1132
1256
|
...common,
|
|
1133
|
-
kind:
|
|
1134
|
-
...handler.hydrate(
|
|
1257
|
+
kind: stored.kind,
|
|
1258
|
+
...handler.hydrate(stored.profile, { lockup: script })
|
|
1135
1259
|
};
|
|
1136
1260
|
}
|
|
1137
1261
|
function shouldRetainRfqSwap(record, now) {
|
|
@@ -1172,13 +1296,15 @@ async function fetchParsedTxs(indexer, txids) {
|
|
|
1172
1296
|
var unscannedSwapCandidates = (txs, existingIds, scanned) => txs.filter(
|
|
1173
1297
|
(tx) => tx.type === "sent" && tx.redeemTxid && !existingIds.has(tx.redeemTxid) && !scanned.has(tx.redeemTxid)
|
|
1174
1298
|
);
|
|
1175
|
-
function classifySpend(offer,
|
|
1299
|
+
function classifySpend(offer, operatorPubkey, spendTx, deposit) {
|
|
1176
1300
|
let leaves;
|
|
1177
1301
|
try {
|
|
1178
|
-
const script =
|
|
1302
|
+
const script = offerContract(offer, operatorPubkey);
|
|
1179
1303
|
if (hex5.encode(script.pkScript) !== hex5.encode(offer.swapPkScript)) return "indeterminate";
|
|
1180
1304
|
leaves = {
|
|
1181
|
-
|
|
1305
|
+
// both routes that hand the deposit back; `exit` is absent on an
|
|
1306
|
+
// offer that carries no exit closure, and drops out here
|
|
1307
|
+
returned: ["cancel", "exit"].map((name) => script.functionByName(name)?.leafScript).filter((leaf) => leaf !== void 0),
|
|
1182
1308
|
fulfill: script.functionByName("fulfill")?.leafScript
|
|
1183
1309
|
};
|
|
1184
1310
|
} catch {
|
|
@@ -1190,22 +1316,22 @@ function classifySpend(offer, serverPubkey, spendTx, deposit) {
|
|
|
1190
1316
|
if (hex5.encode(input.txid) !== deposit.txid) continue;
|
|
1191
1317
|
for (const leaf of input.tapLeafScript ?? []) {
|
|
1192
1318
|
const spent = hex5.encode(scriptFromTapLeafScript(leaf));
|
|
1193
|
-
if (leaves.
|
|
1319
|
+
if (leaves.returned.some((back) => spent === hex5.encode(back))) return "cancelled";
|
|
1194
1320
|
if (leaves.fulfill && spent === hex5.encode(leaves.fulfill)) return "fulfilled";
|
|
1195
1321
|
}
|
|
1196
1322
|
}
|
|
1197
1323
|
return "indeterminate";
|
|
1198
1324
|
}
|
|
1199
1325
|
var spendTxidsOf = (vtxo) => [vtxo.spentBy, vtxo.arkTxId].filter((id) => Boolean(id));
|
|
1200
|
-
function classifyDepositSpend(offer,
|
|
1326
|
+
function classifyDepositSpend(offer, operatorPubkey, spendTxs, deposit) {
|
|
1201
1327
|
for (const tx of spendTxs) {
|
|
1202
|
-
const kind = classifySpend(offer,
|
|
1328
|
+
const kind = classifySpend(offer, operatorPubkey, tx, deposit);
|
|
1203
1329
|
if (kind !== "indeterminate") return kind;
|
|
1204
1330
|
}
|
|
1205
1331
|
return "indeterminate";
|
|
1206
1332
|
}
|
|
1207
1333
|
async function restoreAssetSwaps(indexer, txs, existingIds, opts) {
|
|
1208
|
-
const {
|
|
1334
|
+
const { operatorPubkey, scanned = /* @__PURE__ */ new Set() } = opts;
|
|
1209
1335
|
const candidates = unscannedSwapCandidates(txs, existingIds, scanned);
|
|
1210
1336
|
if (candidates.length === 0) return { restored: [], scannedTxids: [] };
|
|
1211
1337
|
const byTxid = new Map(candidates.map((tx) => [tx.redeemTxid, tx]));
|
|
@@ -1274,7 +1400,7 @@ async function restoreAssetSwaps(indexer, txs, existingIds, opts) {
|
|
|
1274
1400
|
if (state === "swept") status = "recoverable";
|
|
1275
1401
|
else if (state === "spent") {
|
|
1276
1402
|
const spendTxs = spendTxidsOf(vtxo).map((id) => spendTxByTxid.get(id)).filter((tx) => tx !== void 0);
|
|
1277
|
-
const kind = classifyDepositSpend(offer,
|
|
1403
|
+
const kind = classifyDepositSpend(offer, operatorPubkey, spendTxs, {
|
|
1278
1404
|
txid: vtxo.txid,
|
|
1279
1405
|
vout: vtxo.vout
|
|
1280
1406
|
});
|
|
@@ -1291,7 +1417,7 @@ async function restoreAssetSwaps(indexer, txs, existingIds, opts) {
|
|
|
1291
1417
|
fromAmount,
|
|
1292
1418
|
toAmount: offer.wantAmount.toString(),
|
|
1293
1419
|
// ponytail(arkade-os/ts-sdk#680): empty address makes cancel fall back
|
|
1294
|
-
// to the current
|
|
1420
|
+
// to the current operator key; store the funded address if operator-key
|
|
1295
1421
|
// rotations become real (cancelOffer now at least diagnoses the
|
|
1296
1422
|
// mismatch instead of reporting a missing VTXO)
|
|
1297
1423
|
swapAddress: "",
|
|
@@ -1313,7 +1439,6 @@ async function restoreAssetSwaps(indexer, txs, existingIds, opts) {
|
|
|
1313
1439
|
import { base64 as base642, hex as hex6 } from "@scure/base";
|
|
1314
1440
|
import {
|
|
1315
1441
|
ArkAddress as ArkAddress3,
|
|
1316
|
-
RestIndexerProvider as RestIndexerProvider3,
|
|
1317
1442
|
Transaction as Transaction2
|
|
1318
1443
|
} from "@arkade-os/sdk";
|
|
1319
1444
|
var TERMINAL = ["fulfilled", "cancelled", "recoverable"];
|
|
@@ -1330,13 +1455,15 @@ function spendUpdate(swap, spend) {
|
|
|
1330
1455
|
}
|
|
1331
1456
|
async function watchOfferSwaps({
|
|
1332
1457
|
wallet,
|
|
1333
|
-
arkServerUrl,
|
|
1334
1458
|
repository,
|
|
1335
1459
|
onUpdate
|
|
1336
1460
|
}) {
|
|
1337
|
-
const manager = await
|
|
1338
|
-
|
|
1339
|
-
|
|
1461
|
+
const [manager, address, indexer] = await Promise.all([
|
|
1462
|
+
wallet.getContractManager(),
|
|
1463
|
+
wallet.getAddress(),
|
|
1464
|
+
wallet.getArkadeReader()
|
|
1465
|
+
]);
|
|
1466
|
+
const operatorPubkey = ArkAddress3.decode(address).serverPubKey;
|
|
1340
1467
|
let queue = Promise.resolve();
|
|
1341
1468
|
const enqueue = (task) => {
|
|
1342
1469
|
queue = queue.then(task).catch(() => {
|
|
@@ -1350,7 +1477,7 @@ async function watchOfferSwaps({
|
|
|
1350
1477
|
const { txs } = await indexer.getVirtualTxs(candidates);
|
|
1351
1478
|
return classifyDepositSpend(
|
|
1352
1479
|
decodeOffer(hex6.decode(swap.offerHex)),
|
|
1353
|
-
|
|
1480
|
+
operatorPubkey,
|
|
1354
1481
|
txs.map((psbt) => Transaction2.fromPSBT(base642.decode(psbt))),
|
|
1355
1482
|
{ txid: vtxo.txid, vout: vtxo.vout }
|
|
1356
1483
|
);
|
|
@@ -1445,11 +1572,10 @@ var LockupNeedsRecoveryError = class extends Error {
|
|
|
1445
1572
|
* into one settlement with no CLTV awareness, so an early attempt can fail
|
|
1446
1573
|
* the whole batch — including unrelated outputs that were otherwise fine.
|
|
1447
1574
|
*
|
|
1448
|
-
* Exposed as a value, not only inside the message, so a caller can
|
|
1449
|
-
*
|
|
1450
|
-
*
|
|
1451
|
-
*
|
|
1452
|
-
* not an exact alarm.
|
|
1575
|
+
* Exposed as a value, not only inside the message, so a caller can skip
|
|
1576
|
+
* pre-CLTV recoverable outputs without parsing prose. Seconds-based
|
|
1577
|
+
* locktimes mature against the chain tip's timestamp rather than wall
|
|
1578
|
+
* clock, so treat this as a floor to wait past, not an exact alarm.
|
|
1453
1579
|
*/
|
|
1454
1580
|
recoverableAfter;
|
|
1455
1581
|
constructor(outpoints, recoverableAfter) {
|
|
@@ -1510,7 +1636,7 @@ async function readLockupFate(indexer, input) {
|
|
|
1510
1636
|
if (vtxo.spentBy)
|
|
1511
1637
|
spentBy.set(vtxo.spentBy, {
|
|
1512
1638
|
checkpointTxid: vtxo.spentBy,
|
|
1513
|
-
|
|
1639
|
+
txid: vtxo.arkTxId
|
|
1514
1640
|
});
|
|
1515
1641
|
else everySpendNamed = false;
|
|
1516
1642
|
}
|
|
@@ -1539,32 +1665,32 @@ async function readLockupFate(indexer, input) {
|
|
|
1539
1665
|
}
|
|
1540
1666
|
return everySpendNamed && observed.size === spentBy.size ? { fate: "returned", spends } : { fate: "unknown" };
|
|
1541
1667
|
}
|
|
1542
|
-
async function pushRefundWithoutReceiver(
|
|
1668
|
+
async function pushRefundWithoutReceiver(operator, input) {
|
|
1543
1669
|
if (input.vtxos.length === 0) throw new Error("nothing to refund: no funded outputs");
|
|
1544
1670
|
const swept = input.vtxos.filter((vtxo) => vtxo.recoverable);
|
|
1545
1671
|
if (swept.length > 0) {
|
|
1546
1672
|
throw new LockupNeedsRecoveryError(
|
|
1547
1673
|
swept.map((vtxo) => `${vtxo.txid}:${vtxo.vout}`),
|
|
1548
|
-
input.
|
|
1674
|
+
input.contract.options.refundLocktime
|
|
1549
1675
|
);
|
|
1550
1676
|
}
|
|
1551
|
-
const refundPkScript = input.refundPkScript ?? input.
|
|
1677
|
+
const refundPkScript = input.refundPkScript ?? input.contract.options.nonInteractiveParameters?.senderPkScript;
|
|
1552
1678
|
if (!refundPkScript) {
|
|
1553
1679
|
throw new Error(
|
|
1554
|
-
"no refund destination: the contract carries no
|
|
1680
|
+
"no refund destination: the contract carries no emulator covenant suite, so pass refundPkScript explicitly"
|
|
1555
1681
|
);
|
|
1556
1682
|
}
|
|
1557
|
-
const info = await
|
|
1558
|
-
let
|
|
1683
|
+
const info = await operator.getInfo();
|
|
1684
|
+
let operatorUnrollScript;
|
|
1559
1685
|
try {
|
|
1560
|
-
|
|
1686
|
+
operatorUnrollScript = CSVMultisigTapscript.decode(hex7.decode(info.checkpointTapscript));
|
|
1561
1687
|
} catch {
|
|
1562
|
-
throw new Error("invalid checkpointTapscript from the
|
|
1688
|
+
throw new Error("invalid checkpointTapscript from the operator");
|
|
1563
1689
|
}
|
|
1564
|
-
const leaf = input.
|
|
1565
|
-
const tapTree = input.
|
|
1690
|
+
const leaf = input.contract.refundWithoutReceiver();
|
|
1691
|
+
const tapTree = input.contract.encode();
|
|
1566
1692
|
const amount = input.vtxos.reduce((sum, vtxo) => sum + vtxo.value, 0);
|
|
1567
|
-
const { arkTx, checkpoints } = buildOffchainTx(
|
|
1693
|
+
const { arkTx: tx, checkpoints } = buildOffchainTx(
|
|
1568
1694
|
input.vtxos.map((vtxo) => ({
|
|
1569
1695
|
txid: vtxo.txid,
|
|
1570
1696
|
vout: vtxo.vout,
|
|
@@ -1573,14 +1699,14 @@ async function pushRefundWithoutReceiver(ark, input) {
|
|
|
1573
1699
|
tapTree
|
|
1574
1700
|
})),
|
|
1575
1701
|
[{ script: refundPkScript, amount: BigInt(amount) }],
|
|
1576
|
-
|
|
1702
|
+
operatorUnrollScript
|
|
1577
1703
|
);
|
|
1578
|
-
const
|
|
1579
|
-
const submitted = await
|
|
1580
|
-
base643.encode(
|
|
1704
|
+
const signedTx = await input.sender.sign(tx);
|
|
1705
|
+
const submitted = await operator.submitTx(
|
|
1706
|
+
base643.encode(signedTx.toPSBT()),
|
|
1581
1707
|
checkpoints.map((c) => base643.encode(c.toPSBT()))
|
|
1582
1708
|
);
|
|
1583
|
-
assertSubmittedArkTxid(submitted,
|
|
1709
|
+
assertSubmittedArkTxid(submitted, signedTx, "refundWithoutReceiver");
|
|
1584
1710
|
const matched = matchServerCheckpoints(
|
|
1585
1711
|
submitted.signedCheckpointTxs,
|
|
1586
1712
|
checkpoints,
|
|
@@ -1591,11 +1717,11 @@ async function pushRefundWithoutReceiver(ark, input) {
|
|
|
1591
1717
|
async ({ server }) => base643.encode((await input.sender.sign(server, [0])).toPSBT())
|
|
1592
1718
|
)
|
|
1593
1719
|
);
|
|
1594
|
-
await
|
|
1595
|
-
return {
|
|
1720
|
+
await operator.finalizeTx(submitted.arkTxid, finalCheckpoints);
|
|
1721
|
+
return { txid: submitted.arkTxid, amount };
|
|
1596
1722
|
}
|
|
1597
1723
|
var REFUND_MTP_LAG_SECONDS = 2 * 60 * 60;
|
|
1598
|
-
async function refundIfUnresolved(transport,
|
|
1724
|
+
async function refundIfUnresolved(transport, operator, indexer, input) {
|
|
1599
1725
|
const pollMs = input.pollMs ?? 5e3;
|
|
1600
1726
|
const now = input.now ?? (() => Math.floor(Date.now() / 1e3));
|
|
1601
1727
|
const attemptDeadline = input.attemptDeadline ?? input.refundLocktime + REFUND_MTP_LAG_SECONDS;
|
|
@@ -1603,11 +1729,26 @@ async function refundIfUnresolved(transport, ark, indexer, input) {
|
|
|
1603
1729
|
const status = await transport.status(input.rfqId);
|
|
1604
1730
|
if (status && isResolved(status.state)) return { outcome: "resolved", status };
|
|
1605
1731
|
if (now() >= input.refundLocktime) {
|
|
1606
|
-
|
|
1732
|
+
let fate = { fate: "unknown" };
|
|
1733
|
+
try {
|
|
1734
|
+
fate = await readLockupFate(indexer, {
|
|
1735
|
+
swapPkScript: input.contract.pkScript,
|
|
1736
|
+
paymentHash: input.paymentHash
|
|
1737
|
+
});
|
|
1738
|
+
} catch {
|
|
1739
|
+
}
|
|
1740
|
+
if (fate.fate === "exited") {
|
|
1741
|
+
return {
|
|
1742
|
+
outcome: "exited",
|
|
1743
|
+
outpoints: fate.outpoints.map((o) => `${o.txid}:${o.vout}`),
|
|
1744
|
+
status
|
|
1745
|
+
};
|
|
1746
|
+
}
|
|
1747
|
+
const vtxos = await findLockupVtxos(indexer, input.contract.pkScript);
|
|
1607
1748
|
if (vtxos.length === 0) return { outcome: "nothing_to_refund", status };
|
|
1608
1749
|
try {
|
|
1609
|
-
const pushed = await pushRefundWithoutReceiver(
|
|
1610
|
-
|
|
1750
|
+
const pushed = await pushRefundWithoutReceiver(operator, {
|
|
1751
|
+
contract: input.contract,
|
|
1611
1752
|
sender: input.sender,
|
|
1612
1753
|
vtxos,
|
|
1613
1754
|
refundPkScript: input.refundPkScript
|
|
@@ -1650,13 +1791,13 @@ var assertFiniteAmount = (value, reason, label) => {
|
|
|
1650
1791
|
error.reason = reason;
|
|
1651
1792
|
throw error;
|
|
1652
1793
|
};
|
|
1653
|
-
async function pushClaim(
|
|
1794
|
+
async function pushClaim(operator, input) {
|
|
1654
1795
|
if (input.vtxos.length === 0) throw new Error("nothing to claim: no funded outputs");
|
|
1655
1796
|
const swept = input.vtxos.filter((vtxo) => vtxo.recoverable);
|
|
1656
1797
|
if (swept.length > 0) {
|
|
1657
1798
|
throw new LockupNeedsRecoveryError(
|
|
1658
1799
|
swept.map((vtxo) => `${vtxo.txid}:${vtxo.vout}`),
|
|
1659
|
-
input.
|
|
1800
|
+
input.contract.options.refundLocktime
|
|
1660
1801
|
);
|
|
1661
1802
|
}
|
|
1662
1803
|
const locked = input.vtxos.reduce((sum, vtxo) => sum + vtxo.value, 0);
|
|
@@ -1667,22 +1808,22 @@ async function pushClaim(ark, input) {
|
|
|
1667
1808
|
throw new LockupAmountMismatchError(input.expectedAmount, locked);
|
|
1668
1809
|
}
|
|
1669
1810
|
}
|
|
1670
|
-
const committed = input.
|
|
1811
|
+
const committed = input.contract.options.preimageHash;
|
|
1671
1812
|
if (hex8.encode(ripemd160(sha2563(input.preimage))) !== hex8.encode(committed)) {
|
|
1672
1813
|
throw new Error("preimage does not match the covenant's payment hash");
|
|
1673
1814
|
}
|
|
1674
|
-
const info = await
|
|
1675
|
-
let
|
|
1815
|
+
const info = await operator.getInfo();
|
|
1816
|
+
let operatorUnrollScript;
|
|
1676
1817
|
try {
|
|
1677
|
-
|
|
1818
|
+
operatorUnrollScript = CSVMultisigTapscript2.decode(hex8.decode(info.checkpointTapscript));
|
|
1678
1819
|
} catch {
|
|
1679
|
-
throw new Error("invalid checkpointTapscript from the
|
|
1820
|
+
throw new Error("invalid checkpointTapscript from the operator");
|
|
1680
1821
|
}
|
|
1681
|
-
const leaf = input.
|
|
1682
|
-
const tapTree = input.
|
|
1683
|
-
const
|
|
1822
|
+
const leaf = input.contract.claim();
|
|
1823
|
+
const tapTree = input.contract.encode();
|
|
1824
|
+
const txid = await signAndSubmitOffchainTx({
|
|
1684
1825
|
identity: claimWithPreimageIdentity(input.receiver, input.preimage),
|
|
1685
|
-
provider:
|
|
1826
|
+
provider: operator,
|
|
1686
1827
|
inputs: input.vtxos.map((vtxo) => ({
|
|
1687
1828
|
txid: vtxo.txid,
|
|
1688
1829
|
vout: vtxo.vout,
|
|
@@ -1693,10 +1834,10 @@ async function pushClaim(ark, input) {
|
|
|
1693
1834
|
// One aggregate output: unlike the covenant refund, this leaf inspects
|
|
1694
1835
|
// nothing about the output set.
|
|
1695
1836
|
outputs: [{ script: input.destinationPkScript, amount: BigInt(locked) }],
|
|
1696
|
-
serverUnrollScript,
|
|
1697
|
-
verifyServerSignatures: { serverPubkey: input.
|
|
1837
|
+
serverUnrollScript: operatorUnrollScript,
|
|
1838
|
+
verifyServerSignatures: { serverPubkey: input.contract.options.server }
|
|
1698
1839
|
});
|
|
1699
|
-
return {
|
|
1840
|
+
return { txid, amount: locked };
|
|
1700
1841
|
}
|
|
1701
1842
|
async function awaitLockupFunding(indexer, swapPkScript, options = {}) {
|
|
1702
1843
|
const pollMs = options.pollMs ?? 5e3;
|
|
@@ -1711,13 +1852,13 @@ async function awaitLockupFunding(indexer, swapPkScript, options = {}) {
|
|
|
1711
1852
|
await sleep2(pollMs);
|
|
1712
1853
|
}
|
|
1713
1854
|
}
|
|
1714
|
-
async function claimReceiveLockup(indexer,
|
|
1855
|
+
async function claimReceiveLockup(indexer, operator, input) {
|
|
1715
1856
|
const vtxos = await awaitLockupFunding(indexer, input.swapPkScript, {
|
|
1716
1857
|
pollMs: input.pollMs,
|
|
1717
1858
|
deadline: input.deadline
|
|
1718
1859
|
});
|
|
1719
|
-
return pushClaim(
|
|
1720
|
-
|
|
1860
|
+
return pushClaim(operator, {
|
|
1861
|
+
contract: input.contract,
|
|
1721
1862
|
receiver: input.receiver,
|
|
1722
1863
|
preimage: input.preimage,
|
|
1723
1864
|
vtxos,
|
|
@@ -1772,8 +1913,8 @@ async function senderIdentityForSwapRecord(wallet, record) {
|
|
|
1772
1913
|
// src/arkadeRefunder.ts
|
|
1773
1914
|
function arkadeRefunder(deps) {
|
|
1774
1915
|
return async (swap) => {
|
|
1775
|
-
const
|
|
1776
|
-
if (!
|
|
1916
|
+
const contract = swap.lockup?.script;
|
|
1917
|
+
if (!contract) {
|
|
1777
1918
|
throw new Error(
|
|
1778
1919
|
`swap ${swap.rfqId} carries no lockup covenant, so its refund cannot be built`
|
|
1779
1920
|
);
|
|
@@ -1788,7 +1929,7 @@ function arkadeRefunder(deps) {
|
|
|
1788
1929
|
);
|
|
1789
1930
|
}
|
|
1790
1931
|
const sender = await senderIdentityForSwapRecord(deps.wallet, rfqSignerOf(record) ?? {});
|
|
1791
|
-
return pushRefundWithoutReceiver(deps.
|
|
1932
|
+
return pushRefundWithoutReceiver(deps.operator, { contract, sender, vtxos });
|
|
1792
1933
|
};
|
|
1793
1934
|
}
|
|
1794
1935
|
|
|
@@ -2184,6 +2325,19 @@ var RfqSwapManager = class {
|
|
|
2184
2325
|
async getPendingSwaps() {
|
|
2185
2326
|
return [...this.monitored.values()];
|
|
2186
2327
|
}
|
|
2328
|
+
/**
|
|
2329
|
+
* Every swap this manager holds — {@link getPendingSwaps} plus the ones
|
|
2330
|
+
* that already ended. The two sets are disjoint: a swap leaves `monitored`
|
|
2331
|
+
* as it enters `finished`.
|
|
2332
|
+
*
|
|
2333
|
+
* The finished half is what this process has seen, which after
|
|
2334
|
+
* {@link restoreFromRepository} is the stored history minus what retention
|
|
2335
|
+
* pruned. A manager that has restored nothing answers with the live swaps
|
|
2336
|
+
* alone.
|
|
2337
|
+
*/
|
|
2338
|
+
async getAllSwaps() {
|
|
2339
|
+
return [...this.monitored.values(), ...this.finished.values()];
|
|
2340
|
+
}
|
|
2187
2341
|
async hasSwap(rfqId) {
|
|
2188
2342
|
return this.monitored.has(rfqId);
|
|
2189
2343
|
}
|
|
@@ -2462,14 +2616,14 @@ var RfqSwapManager = class {
|
|
|
2462
2616
|
return this.claimIfFunded(swap, vtxos);
|
|
2463
2617
|
}
|
|
2464
2618
|
if (now < swap.refundLocktime + REFUND_MTP_LAG_SECONDS) {
|
|
2465
|
-
if (swap.
|
|
2619
|
+
if (swap.claimTxid) return;
|
|
2466
2620
|
return this.block(
|
|
2467
2621
|
swap,
|
|
2468
2622
|
"the claim window closed with the lockup unclaimed \u2014 only the solver can act now"
|
|
2469
2623
|
);
|
|
2470
2624
|
}
|
|
2471
2625
|
const failure = this.lastClaimError.get(swap.rfqId);
|
|
2472
|
-
if (failure && !swap.
|
|
2626
|
+
if (failure && !swap.claimTxid) {
|
|
2473
2627
|
return this.fail(swap, new Error(failure));
|
|
2474
2628
|
}
|
|
2475
2629
|
this.setState(swap, "refunded");
|
|
@@ -2484,7 +2638,7 @@ var RfqSwapManager = class {
|
|
|
2484
2638
|
*/
|
|
2485
2639
|
async claimIfFunded(swap, vtxos) {
|
|
2486
2640
|
if (vtxos.length === 0) return this.unblock(swap);
|
|
2487
|
-
const partiallyClaimed = swap.
|
|
2641
|
+
const partiallyClaimed = swap.claimTxid !== void 0;
|
|
2488
2642
|
if (partiallyClaimed && !this.hasUnclaimedOutpoint(swap.rfqId, vtxos)) {
|
|
2489
2643
|
return;
|
|
2490
2644
|
}
|
|
@@ -2512,10 +2666,10 @@ var RfqSwapManager = class {
|
|
|
2512
2666
|
this.setState(swap, "claimable");
|
|
2513
2667
|
if (!this.config.enableAutoActions) return;
|
|
2514
2668
|
try {
|
|
2515
|
-
const {
|
|
2669
|
+
const { txid } = await this.callbacks.claimLockup(swap, vtxos, { partiallyClaimed });
|
|
2516
2670
|
this.lastClaimError.delete(swap.rfqId);
|
|
2517
2671
|
this.rememberClaimed(swap.rfqId, vtxos);
|
|
2518
|
-
swap.
|
|
2672
|
+
swap.claimTxid = txid;
|
|
2519
2673
|
this.touch(swap);
|
|
2520
2674
|
this.setState(swap, "claimed");
|
|
2521
2675
|
this.emitAction(swap, "claimLockup");
|
|
@@ -2607,7 +2761,7 @@ var RfqSwapManager = class {
|
|
|
2607
2761
|
try {
|
|
2608
2762
|
const pushed = await this.callbacks.refundArkade(swap);
|
|
2609
2763
|
if (pushed) {
|
|
2610
|
-
swap.
|
|
2764
|
+
swap.refundTxid = pushed.txid;
|
|
2611
2765
|
this.touch(swap);
|
|
2612
2766
|
}
|
|
2613
2767
|
this.setState(swap, "refunded");
|
|
@@ -2680,7 +2834,7 @@ var RfqSwapManager = class {
|
|
|
2680
2834
|
/**
|
|
2681
2835
|
* Record which ark transactions ended the lockup.
|
|
2682
2836
|
*
|
|
2683
|
-
* Only the ones the indexer actually named: `LockupSpend.
|
|
2837
|
+
* Only the ones the indexer actually named: `LockupSpend.txid` is
|
|
2684
2838
|
* optional, and a checkpoint txid is not what history correlates on — a
|
|
2685
2839
|
* record carrying one would name a transaction the wallet's own activity
|
|
2686
2840
|
* never shows. Fewer txids is the right failure here.
|
|
@@ -2690,9 +2844,9 @@ var RfqSwapManager = class {
|
|
|
2690
2844
|
* verdict once.
|
|
2691
2845
|
*/
|
|
2692
2846
|
stampLockupSpends(swap, spends) {
|
|
2693
|
-
const
|
|
2694
|
-
if (
|
|
2695
|
-
swap.
|
|
2847
|
+
const txids = spends.map((spend) => spend.txid).filter((txid) => txid !== void 0);
|
|
2848
|
+
if (txids.length === 0) return;
|
|
2849
|
+
swap.lockupSpendTxids = txids;
|
|
2696
2850
|
this.touch(swap);
|
|
2697
2851
|
}
|
|
2698
2852
|
touch(swap) {
|
|
@@ -2773,10 +2927,9 @@ var RfqSwapManager = class {
|
|
|
2773
2927
|
/**
|
|
2774
2928
|
* Drop a terminal swap from monitoring and report it exactly once.
|
|
2775
2929
|
*
|
|
2776
|
-
* `onSwapCompleted` and `onSwapFailed` are mutually exclusive here
|
|
2777
|
-
*
|
|
2778
|
-
*
|
|
2779
|
-
* also fires on failure is a trap worth not inheriting.
|
|
2930
|
+
* `onSwapCompleted` and `onSwapFailed` are mutually exclusive here: a
|
|
2931
|
+
* listener named "completed" that also fires on failure is a trap, so a
|
|
2932
|
+
* swap that leaves monitoring reports through exactly one of them.
|
|
2780
2933
|
*/
|
|
2781
2934
|
finalize(swap) {
|
|
2782
2935
|
if (!this.monitored.has(swap.rfqId)) return;
|
|
@@ -2812,7 +2965,7 @@ var traderClaimTxid = (swap) => {
|
|
|
2812
2965
|
case "onchain_send":
|
|
2813
2966
|
return swap.claimTxid;
|
|
2814
2967
|
case "lightning_receive":
|
|
2815
|
-
return swap.
|
|
2968
|
+
return swap.claimTxid;
|
|
2816
2969
|
default:
|
|
2817
2970
|
return void 0;
|
|
2818
2971
|
}
|
|
@@ -2822,7 +2975,7 @@ var outcomeOf = (swap) => {
|
|
|
2822
2975
|
const lostReceive = swap.kind === "lightning_receive" && swap.state === "refunded";
|
|
2823
2976
|
return {
|
|
2824
2977
|
state: swap.state,
|
|
2825
|
-
txid: lostReceive ? swap.
|
|
2978
|
+
txid: lostReceive ? swap.refundTxid : traderClaimTxid(swap) ?? swap.refundTxid
|
|
2826
2979
|
};
|
|
2827
2980
|
};
|
|
2828
2981
|
var errorMessage = (error) => error instanceof Error ? error.message : String(error);
|
|
@@ -2889,16 +3042,17 @@ async function rfqSwapActivityInputs(deps) {
|
|
|
2889
3042
|
const records = await deps.repository.getAllRfqSwaps();
|
|
2890
3043
|
return Promise.all(records.map((record) => activityInputOf(record, deps.indexer)));
|
|
2891
3044
|
}
|
|
2892
|
-
async function activityInputOf(
|
|
3045
|
+
async function activityInputOf(stored, indexer) {
|
|
3046
|
+
const record = normalizeRfqSwapRecord(stored);
|
|
2893
3047
|
const txids = /* @__PURE__ */ new Set();
|
|
2894
|
-
if (record.
|
|
2895
|
-
if (record.
|
|
3048
|
+
if (record.fundingTxid) txids.add(record.fundingTxid);
|
|
3049
|
+
if (record.refundTxid) txids.add(record.refundTxid);
|
|
2896
3050
|
const handler = rfqCorridorHandlers.getOrThrow(record.kind);
|
|
2897
3051
|
for (const txid of handler.activityTxids?.(record.profile) ?? []) txids.add(txid);
|
|
2898
|
-
for (const txid of record.
|
|
2899
|
-
const spendUnknown = isRfqSwapTerminal(record.state) && !record.
|
|
2900
|
-
if (indexer && (!record.
|
|
2901
|
-
for (const txid of await lockupTxids(indexer, record, !record.
|
|
3052
|
+
for (const txid of record.lockupSpendTxids ?? []) txids.add(txid);
|
|
3053
|
+
const spendUnknown = isRfqSwapTerminal(record.state) && !record.refundTxid && !record.lockupSpendTxids?.length;
|
|
3054
|
+
if (indexer && (!record.fundingTxid || spendUnknown)) {
|
|
3055
|
+
for (const txid of await lockupTxids(indexer, record, !record.fundingTxid)) {
|
|
2902
3056
|
txids.add(txid);
|
|
2903
3057
|
}
|
|
2904
3058
|
}
|
|
@@ -2923,6 +3077,334 @@ async function lockupTxids(indexer, record, wantFunding) {
|
|
|
2923
3077
|
return [];
|
|
2924
3078
|
}
|
|
2925
3079
|
}
|
|
3080
|
+
|
|
3081
|
+
// src/swapClient.ts
|
|
3082
|
+
import { hex as hex11 } from "@scure/base";
|
|
3083
|
+
import { ArkAddress as ArkAddress5, asset as asset2, contractSigner as contractSigner2 } from "@arkade-os/sdk";
|
|
3084
|
+
import { quoteOffer } from "@arkade-os/solver-discovery";
|
|
3085
|
+
var sideCorridorsOf = (market) => {
|
|
3086
|
+
const read = (value) => value === "lightning" || value === "onchain" ? value : "arkade";
|
|
3087
|
+
const m = market;
|
|
3088
|
+
return { base: read(m.base_corridor), quote: read(m.quote_corridor) };
|
|
3089
|
+
};
|
|
3090
|
+
var resolveKind = (market, give) => {
|
|
3091
|
+
const corridors = sideCorridorsOf(market);
|
|
3092
|
+
const giveCorridor = corridors[give];
|
|
3093
|
+
const receiveCorridor = corridors[give === "base" ? "quote" : "base"];
|
|
3094
|
+
if (giveCorridor === "arkade" && receiveCorridor === "arkade") return "spot";
|
|
3095
|
+
if (receiveCorridor === "lightning") return "ln_send";
|
|
3096
|
+
if (giveCorridor === "lightning") return "ln_receive";
|
|
3097
|
+
if (receiveCorridor === "onchain") return "onchain_send";
|
|
3098
|
+
return "onchain_receive";
|
|
3099
|
+
};
|
|
3100
|
+
var need = (value, what, leg) => {
|
|
3101
|
+
if (value === void 0) throw new Error(`a ${leg} quote needs ${what}`);
|
|
3102
|
+
return value;
|
|
3103
|
+
};
|
|
3104
|
+
var corridorAmount = (input, leg) => {
|
|
3105
|
+
const raw = need(input.amount, "an amount", leg);
|
|
3106
|
+
const amountOn = need(input.amountOn, "amountOn ('give' or 'receive')", leg);
|
|
3107
|
+
const amount = Number(raw);
|
|
3108
|
+
if (!Number.isSafeInteger(amount) || amount <= 0) {
|
|
3109
|
+
throw new Error(`a ${leg} amount must be a positive integer of sats, got ${String(raw)}`);
|
|
3110
|
+
}
|
|
3111
|
+
return { amount, amountSide: amountOn === "give" ? "from" : "to" };
|
|
3112
|
+
};
|
|
3113
|
+
function createSwapClient(deps) {
|
|
3114
|
+
const { wallet, repository, transportFor } = deps;
|
|
3115
|
+
let broadcaster;
|
|
3116
|
+
const broadcasting = () => broadcaster ??= wallet.getArkadeBroadcaster();
|
|
3117
|
+
const ark = deps.ark ?? {
|
|
3118
|
+
getInfo: () => wallet.getArkadeInfo({ requireLive: true }),
|
|
3119
|
+
submitTx: async (...args) => (await broadcasting()).submitTx(...args),
|
|
3120
|
+
finalizeTx: async (...args) => (await broadcasting()).finalizeTx(...args)
|
|
3121
|
+
};
|
|
3122
|
+
let reader;
|
|
3123
|
+
const reading = () => reader ??= wallet.getArkadeReader();
|
|
3124
|
+
const indexer = deps.indexer ?? {
|
|
3125
|
+
getVtxos: async (opts) => {
|
|
3126
|
+
if (!opts)
|
|
3127
|
+
throw new Error("getVtxos on the swap indexer requires scripts or outpoints");
|
|
3128
|
+
return (await reading()).getVtxos(opts);
|
|
3129
|
+
},
|
|
3130
|
+
getVirtualTxs: async (...args) => (await reading()).getVirtualTxs(...args)
|
|
3131
|
+
};
|
|
3132
|
+
const managerDeps = { indexer, chain: deps.chain, repository };
|
|
3133
|
+
const manager = new RfqSwapManager(managerDeps);
|
|
3134
|
+
const listeners = /* @__PURE__ */ new Set();
|
|
3135
|
+
const notify2 = (swap) => {
|
|
3136
|
+
for (const listener of listeners) {
|
|
3137
|
+
try {
|
|
3138
|
+
listener(swap);
|
|
3139
|
+
} catch {
|
|
3140
|
+
}
|
|
3141
|
+
}
|
|
3142
|
+
};
|
|
3143
|
+
manager.onSwapUpdate((swap) => notify2({ family: "rfq", swap }));
|
|
3144
|
+
const claimLockup = async (swap, vtxos, { partiallyClaimed }) => {
|
|
3145
|
+
const record = await repository.getRfqSwap(swap.rfqId);
|
|
3146
|
+
if (!record) throw new Error(`rfq swap ${swap.rfqId} has no stored record to claim from`);
|
|
3147
|
+
const secret = rfqClaimSecretOf(record);
|
|
3148
|
+
if (!secret) throw new Error(`rfq swap ${swap.rfqId} carries no claim secret`);
|
|
3149
|
+
const script = swap.lockup?.script;
|
|
3150
|
+
if (!script) throw new Error(`rfq swap ${swap.rfqId} carries no lockup covenant`);
|
|
3151
|
+
const payoutAddress = record.profile.payoutAddress;
|
|
3152
|
+
if (!payoutAddress) throw new Error(`rfq swap ${swap.rfqId} carries no payoutAddress`);
|
|
3153
|
+
return pushClaim(ark, {
|
|
3154
|
+
contract: script,
|
|
3155
|
+
receiver: await contractSigner2(wallet, secret.signingDescriptor),
|
|
3156
|
+
preimage: await preimageForSwapRecord(wallet, secret),
|
|
3157
|
+
vtxos,
|
|
3158
|
+
destinationPkScript: ArkAddress5.decode(payoutAddress).pkScript,
|
|
3159
|
+
expectedAmount: swap.expectedAmount,
|
|
3160
|
+
partiallyClaimed
|
|
3161
|
+
});
|
|
3162
|
+
};
|
|
3163
|
+
manager.setCallbacks({
|
|
3164
|
+
refundArkade: arkadeRefunder({ operator: ark, indexer, wallet, repository }),
|
|
3165
|
+
claimLockup,
|
|
3166
|
+
...deps.claimOnchain ? { claimOnchain: deps.claimOnchain } : {}
|
|
3167
|
+
});
|
|
3168
|
+
let watcher;
|
|
3169
|
+
const quote = async (market, input) => {
|
|
3170
|
+
const kind = resolveKind(market, input.give);
|
|
3171
|
+
switch (kind) {
|
|
3172
|
+
case "spot": {
|
|
3173
|
+
const raw = need(input.amount, "an amount", kind);
|
|
3174
|
+
const amountOn = need(input.amountOn, "amountOn ('give' or 'receive')", kind);
|
|
3175
|
+
const plan = await quoteOffer(market, {
|
|
3176
|
+
give: input.give,
|
|
3177
|
+
...amountOn === "give" ? { giveAmount: raw } : { wantAmount: raw },
|
|
3178
|
+
fetchImpl: deps.discovery.fetchImpl
|
|
3179
|
+
});
|
|
3180
|
+
return { kind, market, plan };
|
|
3181
|
+
}
|
|
3182
|
+
case "ln_send": {
|
|
3183
|
+
const invoice = need(input.invoice, "the invoice to pay", kind);
|
|
3184
|
+
const request = await requestLightningSend(wallet, transportFor(market), {
|
|
3185
|
+
invoice,
|
|
3186
|
+
emulatorPubkey: deps.emulatorPubkey
|
|
3187
|
+
});
|
|
3188
|
+
return { kind, market, request };
|
|
3189
|
+
}
|
|
3190
|
+
case "ln_receive": {
|
|
3191
|
+
const params = {
|
|
3192
|
+
...corridorAmount(input, kind),
|
|
3193
|
+
covclaimdPubkey: need(deps.covclaimdPubkey, "deps.covclaimdPubkey", kind),
|
|
3194
|
+
decodeInvoice: need(deps.decodeInvoice, "deps.decodeInvoice", kind),
|
|
3195
|
+
maxPayAmount: input.maxPayAmount,
|
|
3196
|
+
emulatorPubkey: deps.emulatorPubkey
|
|
3197
|
+
};
|
|
3198
|
+
const request = await requestLightningReceive(wallet, transportFor(market), params);
|
|
3199
|
+
return { kind, market, request, invoice: request.invoice };
|
|
3200
|
+
}
|
|
3201
|
+
case "onchain_send": {
|
|
3202
|
+
need(deps.chain, "deps.chain (L1 access)", kind);
|
|
3203
|
+
const params = {
|
|
3204
|
+
...corridorAmount(input, kind),
|
|
3205
|
+
payoutPubkey: need(input.payoutPubkey, "a payoutPubkey", kind),
|
|
3206
|
+
preimage: input.preimage,
|
|
3207
|
+
emulatorPubkey: deps.emulatorPubkey
|
|
3208
|
+
};
|
|
3209
|
+
const request = await requestOnchainSend(wallet, transportFor(market), params);
|
|
3210
|
+
return { kind, market, request };
|
|
3211
|
+
}
|
|
3212
|
+
case "onchain_receive":
|
|
3213
|
+
throw new Error(
|
|
3214
|
+
"onchain->arkade is not driven by RfqSwapManager yet; quote it directly with requestOnchainReceive"
|
|
3215
|
+
);
|
|
3216
|
+
}
|
|
3217
|
+
};
|
|
3218
|
+
const admit = async (swap, origin) => {
|
|
3219
|
+
let announced = false;
|
|
3220
|
+
const heard = manager.onSwapUpdate((updated) => {
|
|
3221
|
+
if (updated.rfqId === swap.rfqId) announced = true;
|
|
3222
|
+
});
|
|
3223
|
+
try {
|
|
3224
|
+
await manager.addSwap(swap, origin);
|
|
3225
|
+
} finally {
|
|
3226
|
+
heard();
|
|
3227
|
+
}
|
|
3228
|
+
const unified = { family: "rfq", swap };
|
|
3229
|
+
if (!announced) notify2(unified);
|
|
3230
|
+
return unified;
|
|
3231
|
+
};
|
|
3232
|
+
const fundPersisted = async (swap, origin, funding) => {
|
|
3233
|
+
await repository.saveRfqSwap(createRfqSwapRecord(origin, swap));
|
|
3234
|
+
return wallet.send({ address: funding.address, amount: funding.amount });
|
|
3235
|
+
};
|
|
3236
|
+
const accept = async (accepted) => {
|
|
3237
|
+
const now = Math.floor(Date.now() / 1e3);
|
|
3238
|
+
switch (accepted.kind) {
|
|
3239
|
+
case "spot": {
|
|
3240
|
+
const { plan } = accepted;
|
|
3241
|
+
const depositIsBtc = plan.deposit.asset.id === BTC_ASSET_ID;
|
|
3242
|
+
const offer = await createOffer(wallet, {
|
|
3243
|
+
wantAmount: plan.receive.atomic,
|
|
3244
|
+
...plan.receive.asset.id === BTC_ASSET_ID ? { offerAsset: asset2.AssetId.fromString(plan.deposit.asset.id) } : { wantAsset: asset2.AssetId.fromString(plan.receive.asset.id) },
|
|
3245
|
+
emulatorPubkey: deps.emulatorPubkey
|
|
3246
|
+
});
|
|
3247
|
+
const txid = await wallet.send({
|
|
3248
|
+
address: offer.address,
|
|
3249
|
+
// asset deposits ride the SDK's dust-sat carrier
|
|
3250
|
+
amount: depositIsBtc ? Number(plan.deposit.atomic) : void 0,
|
|
3251
|
+
assets: depositIsBtc ? void 0 : [{ assetId: plan.deposit.asset.id, amount: plan.deposit.atomic }],
|
|
3252
|
+
extensions: [offer.extension]
|
|
3253
|
+
});
|
|
3254
|
+
const swap = {
|
|
3255
|
+
id: txid,
|
|
3256
|
+
fromAsset: plan.deposit.asset.id,
|
|
3257
|
+
toAsset: plan.receive.asset.id,
|
|
3258
|
+
fromAmount: plan.deposit.atomic.toString(),
|
|
3259
|
+
toAmount: plan.receive.atomic.toString(),
|
|
3260
|
+
swapAddress: offer.address,
|
|
3261
|
+
swapPkScript: hex11.encode(offer.swapPkScript),
|
|
3262
|
+
offerHex: offer.offerHex,
|
|
3263
|
+
fundingTxid: txid,
|
|
3264
|
+
status: "pending",
|
|
3265
|
+
createdAt: Date.now()
|
|
3266
|
+
};
|
|
3267
|
+
await addAssetSwap(repository, swap);
|
|
3268
|
+
const unified = { family: "offer", swap };
|
|
3269
|
+
notify2(unified);
|
|
3270
|
+
return unified;
|
|
3271
|
+
}
|
|
3272
|
+
case "ln_send": {
|
|
3273
|
+
const { request } = accepted;
|
|
3274
|
+
const swap = {
|
|
3275
|
+
kind: "lightning_send",
|
|
3276
|
+
rfqId: request.rfqId,
|
|
3277
|
+
state: "pending",
|
|
3278
|
+
lockupPkScript: request.swapPkScript,
|
|
3279
|
+
lockup: { script: request.script, address: request.address },
|
|
3280
|
+
// the trader's own decode, never the solver's echo
|
|
3281
|
+
paymentHash: request.contractParams.paymentHash,
|
|
3282
|
+
refundLocktime: request.contractParams.refundLocktime,
|
|
3283
|
+
createdAt: now,
|
|
3284
|
+
updatedAt: now
|
|
3285
|
+
};
|
|
3286
|
+
const origin = {
|
|
3287
|
+
kind: "lightning_send",
|
|
3288
|
+
lockupAddress: request.address,
|
|
3289
|
+
profile: rfqSecretsProfile(request.secrets, swap.paymentHash),
|
|
3290
|
+
amount: request.fundAmount
|
|
3291
|
+
};
|
|
3292
|
+
const txid = await fundPersisted(swap, origin, {
|
|
3293
|
+
address: request.address,
|
|
3294
|
+
amount: request.fundAmount
|
|
3295
|
+
});
|
|
3296
|
+
return admit(swap, { ...origin, fundingTxid: txid });
|
|
3297
|
+
}
|
|
3298
|
+
case "ln_receive": {
|
|
3299
|
+
const { request } = accepted;
|
|
3300
|
+
const paymentHash = hex11.encode(request.secrets.paymentHash);
|
|
3301
|
+
const swap = {
|
|
3302
|
+
kind: "lightning_receive",
|
|
3303
|
+
rfqId: request.rfqId,
|
|
3304
|
+
state: "pending",
|
|
3305
|
+
lockupPkScript: request.swapPkScript,
|
|
3306
|
+
lockup: { script: request.script, address: request.address },
|
|
3307
|
+
paymentHash,
|
|
3308
|
+
refundLocktime: request.contractParams.refundLocktime,
|
|
3309
|
+
expectedAmount: request.expectedAmount,
|
|
3310
|
+
createdAt: now,
|
|
3311
|
+
updatedAt: now
|
|
3312
|
+
};
|
|
3313
|
+
return admit(swap, {
|
|
3314
|
+
kind: "lightning_receive",
|
|
3315
|
+
lockupAddress: request.address,
|
|
3316
|
+
profile: {
|
|
3317
|
+
...rfqSecretsProfile(request.secrets, paymentHash),
|
|
3318
|
+
expectedAmount: request.expectedAmount,
|
|
3319
|
+
payoutAddress: request.payoutAddress
|
|
3320
|
+
},
|
|
3321
|
+
amount: request.expectedAmount
|
|
3322
|
+
});
|
|
3323
|
+
}
|
|
3324
|
+
case "onchain_send": {
|
|
3325
|
+
const { request } = accepted;
|
|
3326
|
+
const paymentHash = hex11.encode(request.secrets.paymentHash);
|
|
3327
|
+
const swap = {
|
|
3328
|
+
kind: "onchain_send",
|
|
3329
|
+
rfqId: request.rfqId,
|
|
3330
|
+
state: "pending",
|
|
3331
|
+
lockupPkScript: request.swapPkScript,
|
|
3332
|
+
lockup: { script: request.script, address: request.address },
|
|
3333
|
+
paymentHash,
|
|
3334
|
+
refundLocktime: request.refundLocktime,
|
|
3335
|
+
htlc: request.htlc,
|
|
3336
|
+
minConfirmations: request.minConfirmations,
|
|
3337
|
+
createdAt: now,
|
|
3338
|
+
updatedAt: now
|
|
3339
|
+
};
|
|
3340
|
+
const origin = {
|
|
3341
|
+
kind: "onchain_send",
|
|
3342
|
+
lockupAddress: request.address,
|
|
3343
|
+
profile: {
|
|
3344
|
+
...rfqSecretsProfile(request.secrets, paymentHash),
|
|
3345
|
+
...onchainSendProfile(request)
|
|
3346
|
+
},
|
|
3347
|
+
amount: request.fundAmount
|
|
3348
|
+
};
|
|
3349
|
+
const txid = await fundPersisted(swap, origin, {
|
|
3350
|
+
address: request.address,
|
|
3351
|
+
amount: request.fundAmount
|
|
3352
|
+
});
|
|
3353
|
+
return admit(swap, { ...origin, fundingTxid: txid });
|
|
3354
|
+
}
|
|
3355
|
+
}
|
|
3356
|
+
};
|
|
3357
|
+
return {
|
|
3358
|
+
manager,
|
|
3359
|
+
markets: (useCache) => discoverMarkets({ ...deps.discovery, repository, useCache }),
|
|
3360
|
+
quote,
|
|
3361
|
+
accept,
|
|
3362
|
+
cancel: async (fundingTxid) => {
|
|
3363
|
+
const swaps = await getAssetSwapsOrThrow(repository);
|
|
3364
|
+
const swap = swaps.find((s) => s.id === fundingTxid);
|
|
3365
|
+
if (!swap) throw new Error(`no offer swap with funding txid ${fundingTxid}`);
|
|
3366
|
+
if (swap.status !== "pending") {
|
|
3367
|
+
throw new Error(`offer swap ${fundingTxid} is ${swap.status}, not cancellable`);
|
|
3368
|
+
}
|
|
3369
|
+
await updateAssetSwap(repository, fundingTxid, { status: "cancelling" });
|
|
3370
|
+
await cancelOffer(wallet, swap.offerHex, {
|
|
3371
|
+
repository,
|
|
3372
|
+
fundingTxid
|
|
3373
|
+
});
|
|
3374
|
+
},
|
|
3375
|
+
// Both families, live AND terminal — the offer half reads the whole
|
|
3376
|
+
// stored history, and the RFQ half is every swap the manager holds.
|
|
3377
|
+
// Its history is what `start()` restored plus what this session
|
|
3378
|
+
// accepted; records past `RFQ_SWAP_RETENTION_SECONDS` are pruned.
|
|
3379
|
+
swaps: async () => {
|
|
3380
|
+
const offers = await getAssetSwaps(repository);
|
|
3381
|
+
const rfq = await manager.getAllSwaps();
|
|
3382
|
+
return [
|
|
3383
|
+
...offers.map((swap) => ({ family: "offer", swap })),
|
|
3384
|
+
...rfq.map((swap) => ({ family: "rfq", swap }))
|
|
3385
|
+
];
|
|
3386
|
+
},
|
|
3387
|
+
onUpdate: (listener) => {
|
|
3388
|
+
listeners.add(listener);
|
|
3389
|
+
return () => listeners.delete(listener);
|
|
3390
|
+
},
|
|
3391
|
+
start: async () => {
|
|
3392
|
+
managerDeps.contracts ??= await wallet.getContractManager();
|
|
3393
|
+
await manager.restoreFromRepository();
|
|
3394
|
+
await manager.start();
|
|
3395
|
+
watcher ??= await watchOfferSwaps({
|
|
3396
|
+
wallet,
|
|
3397
|
+
repository,
|
|
3398
|
+
onUpdate: (swap) => notify2({ family: "offer", swap })
|
|
3399
|
+
});
|
|
3400
|
+
},
|
|
3401
|
+
stop: async () => {
|
|
3402
|
+
await manager.stop();
|
|
3403
|
+
watcher?.stop();
|
|
3404
|
+
watcher = void 0;
|
|
3405
|
+
}
|
|
3406
|
+
};
|
|
3407
|
+
}
|
|
2926
3408
|
export {
|
|
2927
3409
|
ARKADE_ASSET,
|
|
2928
3410
|
ARKADE_BTC,
|
|
@@ -2983,6 +3465,7 @@ export {
|
|
|
2983
3465
|
classifySpend,
|
|
2984
3466
|
createOffer,
|
|
2985
3467
|
createRfqSwapRecord,
|
|
3468
|
+
createSwapClient,
|
|
2986
3469
|
decodeOffer,
|
|
2987
3470
|
deriveLightningReceive,
|
|
2988
3471
|
deriveOnchainReceive,
|
|
@@ -2997,16 +3480,18 @@ export {
|
|
|
2997
3480
|
httpTransport,
|
|
2998
3481
|
isRfqSwapTerminal,
|
|
2999
3482
|
isRfqTerminal,
|
|
3483
|
+
lightningReceiveContract,
|
|
3000
3484
|
lightningReceiveRequest,
|
|
3485
|
+
lightningSendContract,
|
|
3001
3486
|
lightningSendRequest,
|
|
3002
|
-
lightningSendVtxoScript,
|
|
3003
3487
|
lockupContractParams,
|
|
3004
3488
|
makeCachedFeedFetch,
|
|
3005
3489
|
newPreimage,
|
|
3006
3490
|
newRfqId,
|
|
3007
3491
|
nextOnchainAction,
|
|
3492
|
+
normalizeRfqSwapRecord,
|
|
3493
|
+
offerContract,
|
|
3008
3494
|
offerTermsFromQuote,
|
|
3009
|
-
offerVtxoScript,
|
|
3010
3495
|
onchainHtlcScript,
|
|
3011
3496
|
onchainReceiveRequest,
|
|
3012
3497
|
onchainSendProfile,
|
|
@@ -3017,7 +3502,6 @@ export {
|
|
|
3017
3502
|
pushRefundWithoutReceiver,
|
|
3018
3503
|
readLockupFate,
|
|
3019
3504
|
rebuildRfqSwap,
|
|
3020
|
-
receiveVtxoScript,
|
|
3021
3505
|
refundIfUnresolved,
|
|
3022
3506
|
registerLockupContract,
|
|
3023
3507
|
relayTransport,
|