@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.cjs
CHANGED
|
@@ -89,6 +89,7 @@ __export(index_exports, {
|
|
|
89
89
|
classifySpend: () => classifySpend,
|
|
90
90
|
createOffer: () => createOffer,
|
|
91
91
|
createRfqSwapRecord: () => createRfqSwapRecord,
|
|
92
|
+
createSwapClient: () => createSwapClient,
|
|
92
93
|
decodeOffer: () => decodeOffer,
|
|
93
94
|
deriveLightningReceive: () => deriveLightningReceive,
|
|
94
95
|
deriveOnchainReceive: () => deriveOnchainReceive,
|
|
@@ -103,16 +104,18 @@ __export(index_exports, {
|
|
|
103
104
|
httpTransport: () => httpTransport,
|
|
104
105
|
isRfqSwapTerminal: () => isRfqSwapTerminal,
|
|
105
106
|
isRfqTerminal: () => isRfqTerminal,
|
|
107
|
+
lightningReceiveContract: () => lightningReceiveContract,
|
|
106
108
|
lightningReceiveRequest: () => lightningReceiveRequest,
|
|
109
|
+
lightningSendContract: () => lightningSendContract,
|
|
107
110
|
lightningSendRequest: () => lightningSendRequest,
|
|
108
|
-
lightningSendVtxoScript: () => lightningSendVtxoScript,
|
|
109
111
|
lockupContractParams: () => lockupContractParams,
|
|
110
112
|
makeCachedFeedFetch: () => makeCachedFeedFetch,
|
|
111
113
|
newPreimage: () => newPreimage,
|
|
112
114
|
newRfqId: () => newRfqId,
|
|
113
115
|
nextOnchainAction: () => nextOnchainAction,
|
|
116
|
+
normalizeRfqSwapRecord: () => normalizeRfqSwapRecord,
|
|
117
|
+
offerContract: () => offerContract,
|
|
114
118
|
offerTermsFromQuote: () => offerTermsFromQuote,
|
|
115
|
-
offerVtxoScript: () => offerVtxoScript,
|
|
116
119
|
onchainHtlcScript: () => onchainHtlcScript,
|
|
117
120
|
onchainReceiveRequest: () => onchainReceiveRequest,
|
|
118
121
|
onchainSendProfile: () => onchainSendProfile,
|
|
@@ -123,7 +126,6 @@ __export(index_exports, {
|
|
|
123
126
|
pushRefundWithoutReceiver: () => pushRefundWithoutReceiver,
|
|
124
127
|
readLockupFate: () => readLockupFate,
|
|
125
128
|
rebuildRfqSwap: () => rebuildRfqSwap,
|
|
126
|
-
receiveVtxoScript: () => receiveVtxoScript,
|
|
127
129
|
refundIfUnresolved: () => refundIfUnresolved,
|
|
128
130
|
registerLockupContract: () => registerLockupContract,
|
|
129
131
|
relayTransport: () => relayTransport,
|
|
@@ -411,32 +413,51 @@ var swapPrograms = {
|
|
|
411
413
|
wantAsset: import_sdk2.arkade.parseArtifact(swap_want_asset_program_default),
|
|
412
414
|
wantBtc: import_sdk2.arkade.parseArtifact(swap_want_btc_program_default)
|
|
413
415
|
};
|
|
414
|
-
function
|
|
416
|
+
function withExitClosure(program, exit) {
|
|
417
|
+
if (!exit) return program;
|
|
418
|
+
return {
|
|
419
|
+
...program,
|
|
420
|
+
// typed params are authoritative: an undeclared `$exitDelay` fails
|
|
421
|
+
// validateProgram instead of compiling against an unbound value
|
|
422
|
+
params: [...program.params ?? [], { name: "exitDelay", type: "int" }],
|
|
423
|
+
functions: {
|
|
424
|
+
...program.functions,
|
|
425
|
+
exit: {
|
|
426
|
+
tapscript: { signers: ["$user"], csv: { type: exit.type, value: "$exitDelay" } }
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
};
|
|
430
|
+
}
|
|
431
|
+
function swapProgramBinding(offer, operatorPubkey) {
|
|
415
432
|
if (offer.makerPkScript.length !== FIELDS.makerPkScript.width) {
|
|
416
433
|
throw new Error("makerPkScript is not a 34-byte taproot scriptPubKey");
|
|
417
434
|
}
|
|
418
435
|
return {
|
|
419
|
-
program:
|
|
436
|
+
program: withExitClosure(
|
|
437
|
+
offer.wantAsset ? swapPrograms.wantAsset : swapPrograms.wantBtc,
|
|
438
|
+
offer.exitDelay
|
|
439
|
+
),
|
|
420
440
|
args: {
|
|
421
441
|
makerWP: offer.makerPkScript.subarray(2),
|
|
422
442
|
wantAmount: offer.wantAmount,
|
|
423
|
-
server:
|
|
443
|
+
server: operatorPubkey,
|
|
424
444
|
user: offer.makerPublicKey,
|
|
425
445
|
// internal byte order
|
|
426
446
|
...offer.wantAsset && {
|
|
427
447
|
wantAssetTxid: offer.wantAsset.txid.slice().reverse(),
|
|
428
448
|
wantAssetGroupIndex: offer.wantAsset.groupIndex
|
|
429
|
-
}
|
|
449
|
+
},
|
|
450
|
+
...offer.exitDelay && { exitDelay: offer.exitDelay.value }
|
|
430
451
|
},
|
|
431
452
|
keys: {
|
|
432
|
-
serverKey:
|
|
453
|
+
serverKey: operatorPubkey,
|
|
433
454
|
userKey: offer.makerPublicKey,
|
|
434
455
|
emulatorKey: offer.emulatorPubkey
|
|
435
456
|
}
|
|
436
457
|
};
|
|
437
458
|
}
|
|
438
|
-
function
|
|
439
|
-
const { program, args, keys } = swapProgramBinding(offer,
|
|
459
|
+
function offerContract(offer, operatorPubkey) {
|
|
460
|
+
const { program, args, keys } = swapProgramBinding(offer, operatorPubkey);
|
|
440
461
|
return new import_sdk2.arkade.ArkadeProgramScript(program, args, keys);
|
|
441
462
|
}
|
|
442
463
|
var OFFER_PACKET_TYPE = 3;
|
|
@@ -447,9 +468,27 @@ var FIELDS = {
|
|
|
447
468
|
makerPkScript: { tag: 5, width: 34 },
|
|
448
469
|
makerPublicKey: { tag: 7, width: 32 },
|
|
449
470
|
emulatorPubkey: { tag: 8, width: 32 },
|
|
450
|
-
|
|
471
|
+
ratioNum: { tag: 9, width: 8 },
|
|
472
|
+
ratioDen: { tag: 10, width: 8 },
|
|
473
|
+
offerAsset: { tag: 11, width: void 0 },
|
|
474
|
+
exitTimelock: { tag: 12, width: 9 }
|
|
451
475
|
};
|
|
476
|
+
var EXIT_TYPES = ["blocks", "seconds"];
|
|
452
477
|
var NAMES = Object.fromEntries(Object.entries(FIELDS).map(([k, f]) => [f.tag, k]));
|
|
478
|
+
function u64(name, value) {
|
|
479
|
+
if (value < BigInt(0) || value >> BigInt(64) > BigInt(0)) {
|
|
480
|
+
throw new Error(`${name} does not fit the offer wire format (u64)`);
|
|
481
|
+
}
|
|
482
|
+
const out = new Uint8Array(FIELDS.wantAmount.width);
|
|
483
|
+
new DataView(out.buffer).setBigUint64(0, value, false);
|
|
484
|
+
return out;
|
|
485
|
+
}
|
|
486
|
+
var readU64 = (value) => new DataView(value.buffer, value.byteOffset).getBigUint64(0, false);
|
|
487
|
+
var setRatio = (name, value) => {
|
|
488
|
+
if (value === void 0 || value === BigInt(0)) return void 0;
|
|
489
|
+
if (value < BigInt(0)) throw new Error(`${name} does not fit the offer wire format (u64)`);
|
|
490
|
+
return value;
|
|
491
|
+
};
|
|
453
492
|
function tlv(type, value) {
|
|
454
493
|
if (value.length > 65535) throw new Error("TLV value exceeds the u16 length field");
|
|
455
494
|
return (0, import_utils.concatBytes)(Uint8Array.of(type, value.length >> 8 & 255, value.length & 255), value);
|
|
@@ -468,24 +507,45 @@ function encodeOffer(offer) {
|
|
|
468
507
|
throw new Error(`${name} must be ${FIELDS[name].width} bytes`);
|
|
469
508
|
}
|
|
470
509
|
}
|
|
471
|
-
|
|
472
|
-
|
|
510
|
+
const ratioNum = setRatio("ratioNum", offer.ratioNum);
|
|
511
|
+
const ratioDen = setRatio("ratioDen", offer.ratioDen);
|
|
512
|
+
if (ratioNum === void 0 !== (ratioDen === void 0)) {
|
|
513
|
+
throw new Error("offer must carry both ratioNum and ratioDen, or neither");
|
|
473
514
|
}
|
|
474
|
-
const amount = new Uint8Array(FIELDS.wantAmount.width);
|
|
475
|
-
new DataView(amount.buffer).setBigUint64(0, offer.wantAmount, false);
|
|
476
515
|
const recs = [
|
|
477
516
|
tlv(FIELDS.swapPkScript.tag, offer.swapPkScript),
|
|
478
|
-
tlv(FIELDS.wantAmount.tag,
|
|
517
|
+
tlv(FIELDS.wantAmount.tag, u64("wantAmount", offer.wantAmount))
|
|
479
518
|
];
|
|
480
519
|
if (offer.wantAsset) recs.push(tlv(FIELDS.wantAsset.tag, offer.wantAsset.serialize()));
|
|
520
|
+
if (ratioNum !== void 0) recs.push(tlv(FIELDS.ratioNum.tag, u64("ratioNum", ratioNum)));
|
|
521
|
+
if (ratioDen !== void 0) recs.push(tlv(FIELDS.ratioDen.tag, u64("ratioDen", ratioDen)));
|
|
481
522
|
if (offer.offerAsset) recs.push(tlv(FIELDS.offerAsset.tag, offer.offerAsset.serialize()));
|
|
482
523
|
recs.push(
|
|
483
524
|
tlv(FIELDS.makerPkScript.tag, offer.makerPkScript),
|
|
484
525
|
tlv(FIELDS.makerPublicKey.tag, offer.makerPublicKey),
|
|
485
526
|
tlv(FIELDS.emulatorPubkey.tag, offer.emulatorPubkey)
|
|
486
527
|
);
|
|
528
|
+
if (offer.exitDelay) recs.push(tlv(FIELDS.exitTimelock.tag, encodeExitDelay(offer.exitDelay)));
|
|
487
529
|
return (0, import_utils.concatBytes)(...recs);
|
|
488
530
|
}
|
|
531
|
+
function encodeExitDelay(exit) {
|
|
532
|
+
return (0, import_utils.concatBytes)(
|
|
533
|
+
Uint8Array.of(EXIT_TYPES.indexOf(assertExitDelay(exit).type)),
|
|
534
|
+
u64("exitDelay", exit.value)
|
|
535
|
+
);
|
|
536
|
+
}
|
|
537
|
+
function assertExitDelay(exit) {
|
|
538
|
+
if (EXIT_TYPES.indexOf(exit.type) < 0) {
|
|
539
|
+
throw new Error(`unknown exitDelay locktime type: ${exit.type}`);
|
|
540
|
+
}
|
|
541
|
+
if (exit.value <= BigInt(0)) {
|
|
542
|
+
throw new Error("exitDelay must be a positive relative locktime");
|
|
543
|
+
}
|
|
544
|
+
if (exit.value >> BigInt(32) > BigInt(0)) {
|
|
545
|
+
throw new Error("exitDelay does not fit the locktime field (u32)");
|
|
546
|
+
}
|
|
547
|
+
return exit;
|
|
548
|
+
}
|
|
489
549
|
function decodeOffer(data) {
|
|
490
550
|
const fields = {};
|
|
491
551
|
let off = 0;
|
|
@@ -505,40 +565,70 @@ function decodeOffer(data) {
|
|
|
505
565
|
for (const name of ["wantAsset", "offerAsset"]) {
|
|
506
566
|
if (fields[name]?.length === 0) throw new Error(`missing/invalid ${name}`);
|
|
507
567
|
}
|
|
508
|
-
const
|
|
568
|
+
for (const [name, value] of Object.entries(fields)) {
|
|
569
|
+
const width = FIELDS[name].width;
|
|
570
|
+
if (width !== void 0 && value.length !== width) {
|
|
571
|
+
throw new Error(`missing/invalid ${name}`);
|
|
572
|
+
}
|
|
573
|
+
}
|
|
574
|
+
const need2 = (name) => {
|
|
509
575
|
const v = fields[name];
|
|
510
576
|
const len = FIELDS[name].width;
|
|
511
577
|
if (!v || len !== void 0 && v.length !== len)
|
|
512
578
|
throw new Error(`missing/invalid ${name}`);
|
|
513
579
|
return v;
|
|
514
580
|
};
|
|
515
|
-
const amount =
|
|
581
|
+
const amount = need2("wantAmount");
|
|
516
582
|
if (Boolean(fields.wantAsset) === Boolean(fields.offerAsset)) {
|
|
517
583
|
throw new Error("offer must carry exactly one of wantAsset or offerAsset");
|
|
518
584
|
}
|
|
585
|
+
const readRatio = (name) => {
|
|
586
|
+
const raw = fields[name];
|
|
587
|
+
if (!raw) return void 0;
|
|
588
|
+
const value = readU64(raw);
|
|
589
|
+
if (value === BigInt(0)) throw new Error(`missing/invalid ${name}`);
|
|
590
|
+
return value;
|
|
591
|
+
};
|
|
592
|
+
const ratioNum = readRatio("ratioNum");
|
|
593
|
+
const ratioDen = readRatio("ratioDen");
|
|
594
|
+
if (ratioNum === void 0 !== (ratioDen === void 0)) {
|
|
595
|
+
throw new Error("offer must carry both ratioNum and ratioDen, or neither");
|
|
596
|
+
}
|
|
519
597
|
return {
|
|
520
|
-
swapPkScript:
|
|
521
|
-
wantAmount:
|
|
598
|
+
swapPkScript: need2("swapPkScript"),
|
|
599
|
+
wantAmount: readU64(amount),
|
|
522
600
|
...fields.wantAsset && { wantAsset: import_sdk2.asset.AssetId.fromBytes(fields.wantAsset) },
|
|
523
601
|
...fields.offerAsset && { offerAsset: import_sdk2.asset.AssetId.fromBytes(fields.offerAsset) },
|
|
524
|
-
makerPkScript:
|
|
525
|
-
makerPublicKey:
|
|
526
|
-
emulatorPubkey:
|
|
602
|
+
makerPkScript: need2("makerPkScript"),
|
|
603
|
+
makerPublicKey: need2("makerPublicKey"),
|
|
604
|
+
emulatorPubkey: need2("emulatorPubkey"),
|
|
605
|
+
...ratioNum !== void 0 && { ratioNum },
|
|
606
|
+
...ratioDen !== void 0 && { ratioDen },
|
|
607
|
+
...fields.exitTimelock && { exitDelay: decodeExitDelay(fields.exitTimelock) }
|
|
527
608
|
};
|
|
528
609
|
}
|
|
610
|
+
function decodeExitDelay(value) {
|
|
611
|
+
const type = EXIT_TYPES[value[0]];
|
|
612
|
+
if (!type) throw new Error(`unknown exitDelay locktime type: 0x${value[0].toString(16)}`);
|
|
613
|
+
return { type, value: readU64(value.subarray(1)) };
|
|
614
|
+
}
|
|
529
615
|
var OFFER_CONTRACT_LABEL = "Arkade swap offer";
|
|
530
616
|
var OFFER_CONTRACT_KIND = "asset-swap-offer";
|
|
531
|
-
async function registerOfferContract(wallet,
|
|
532
|
-
const { program, args, keys } = swapProgramBinding(binding,
|
|
617
|
+
async function registerOfferContract(wallet, info, binding, operatorPubkey, expectedPkScript) {
|
|
618
|
+
const { program, args, keys } = swapProgramBinding(binding, operatorPubkey);
|
|
533
619
|
const contractManager = await wallet.getContractManager();
|
|
534
620
|
const client = await import_sdk2.arkade.Arkade.connect({
|
|
535
|
-
|
|
536
|
-
|
|
621
|
+
// Registration derives and persists; it never broadcasts and never
|
|
622
|
+
// reads UTXOs, so the only thing the client needs off the server is the
|
|
623
|
+
// info the caller already resolved. Handing that back — rather than a
|
|
624
|
+
// provider built from a URL — is what lets `createOffer` take just a
|
|
625
|
+
// wallet, and spares a second `/v1/info` round-trip.
|
|
626
|
+
arkade: { getInfo: async () => info },
|
|
537
627
|
identity: wallet.identity,
|
|
538
628
|
// without this the row's `address` would be derived against the SDK's
|
|
539
629
|
// default network while its script is right — a row that disagrees with
|
|
540
630
|
// the address the user is about to fund
|
|
541
|
-
network: (0, import_sdk2.
|
|
631
|
+
network: (0, import_sdk2.networkFromArkadeInfo)(info),
|
|
542
632
|
contractManager
|
|
543
633
|
});
|
|
544
634
|
const contract = new import_sdk2.arkade.ArkadeContract(client, program, args, keys);
|
|
@@ -551,17 +641,29 @@ async function registerOfferContract(wallet, arkServerUrl, network, binding, ser
|
|
|
551
641
|
});
|
|
552
642
|
await promoteOfferContract(contractManager, import_base2.hex.encode(expectedPkScript));
|
|
553
643
|
}
|
|
554
|
-
|
|
644
|
+
function serverExitDelay(delay) {
|
|
645
|
+
if (typeof delay !== "bigint" || delay <= BigInt(0)) {
|
|
646
|
+
throw new Error(
|
|
647
|
+
"the server reports no usable unilateralExitDelay; pass `exitDelay` to set the offer's exit closure explicitly, or `noExit: true` to publish without one"
|
|
648
|
+
);
|
|
649
|
+
}
|
|
650
|
+
return assertExitDelay({ value: delay, type: delay < BigInt(512) ? "blocks" : "seconds" });
|
|
651
|
+
}
|
|
652
|
+
async function createOffer(wallet, params) {
|
|
555
653
|
if (Boolean(params.wantAsset) === Boolean(params.offerAsset)) {
|
|
556
654
|
throw new Error("set exactly one of wantAsset (BTC->asset) or offerAsset (asset->BTC)");
|
|
557
655
|
}
|
|
558
656
|
const [info, makerAddress, makerPublicKey] = await Promise.all([
|
|
559
|
-
|
|
657
|
+
// requireLive: this info binds signerPubkey into the covenant — a
|
|
658
|
+
// snapshot could derive an address the operator no longer co-signs
|
|
659
|
+
// for, so an unreachable operator fails the call instead (fail closed,
|
|
660
|
+
// the same behaviour the pre-#734 caller-held provider had)
|
|
661
|
+
wallet.getArkadeInfo({ requireLive: true }),
|
|
560
662
|
wallet.getAddress(),
|
|
561
663
|
wallet.identity.xOnlyPublicKey()
|
|
562
664
|
]);
|
|
563
|
-
const
|
|
564
|
-
const network = (0, import_sdk2.
|
|
665
|
+
const operatorPubKey = import_base2.hex.decode((0, import_sdk2.toXOnlySignerHex)(info.signerPubkey));
|
|
666
|
+
const network = (0, import_sdk2.networkFromArkadeInfo)(info);
|
|
565
667
|
const emuKey = import_base2.hex.decode(
|
|
566
668
|
(0, import_sdk2.toXOnlySignerHex)((0, import_sdk2.resolveEmulatorPubkey)(network, params.emulatorPubkey))
|
|
567
669
|
);
|
|
@@ -571,35 +673,39 @@ async function createOffer(wallet, arkServerUrl, params) {
|
|
|
571
673
|
offerAsset: params.offerAsset,
|
|
572
674
|
makerPkScript: import_sdk2.ArkAddress.decode(makerAddress).pkScript,
|
|
573
675
|
makerPublicKey,
|
|
574
|
-
emulatorPubkey: emuKey
|
|
676
|
+
emulatorPubkey: emuKey,
|
|
677
|
+
// checked HERE, before the covenant is derived and registered below:
|
|
678
|
+
// deferring it to `encodeOffer` leaves a registered contract behind for
|
|
679
|
+
// an offer that then fails to encode. @see assertExitDelay
|
|
680
|
+
exitDelay: params.noExit ? void 0 : params.exitDelay ? assertExitDelay(params.exitDelay) : serverExitDelay(info.unilateralExitDelay)
|
|
575
681
|
};
|
|
576
|
-
const script =
|
|
682
|
+
const script = offerContract(binding, operatorPubKey);
|
|
577
683
|
const offer = { ...binding, swapPkScript: script.pkScript };
|
|
578
|
-
await registerOfferContract(
|
|
579
|
-
wallet,
|
|
580
|
-
arkServerUrl,
|
|
581
|
-
info.network,
|
|
582
|
-
binding,
|
|
583
|
-
serverPubKey,
|
|
584
|
-
script.pkScript
|
|
585
|
-
);
|
|
684
|
+
await registerOfferContract(wallet, info, binding, operatorPubKey, script.pkScript);
|
|
586
685
|
const payload = encodeOffer(offer);
|
|
587
686
|
return {
|
|
588
687
|
offerHex: import_base2.hex.encode(payload),
|
|
589
688
|
extension: { type: OFFER_PACKET_TYPE, payload },
|
|
590
|
-
//
|
|
689
|
+
// the contract's .address() builds the address; assembling an ArkAddress
|
|
591
690
|
// from tweakedPublicKey here would silently miss any future step it gains
|
|
592
|
-
address: script.address(network.hrp,
|
|
691
|
+
address: script.address(network.hrp, operatorPubKey).encode(),
|
|
593
692
|
swapPkScript: script.pkScript
|
|
594
693
|
};
|
|
595
694
|
}
|
|
596
|
-
async function cancelOffer(wallet,
|
|
695
|
+
async function cancelOffer(wallet, offerHex, opts) {
|
|
597
696
|
const { repository, fundingTxid, swapAddress } = opts;
|
|
598
697
|
const offer = decodeOffer(import_base2.hex.decode(offerHex));
|
|
599
|
-
const contractManager = await
|
|
698
|
+
const [contractManager, info, reader, broadcaster] = await Promise.all([
|
|
699
|
+
wallet.getContractManager(),
|
|
700
|
+
wallet.getArkadeInfo({ requireLive: true }),
|
|
701
|
+
wallet.getArkadeReader(),
|
|
702
|
+
wallet.getArkadeBroadcaster()
|
|
703
|
+
]);
|
|
600
704
|
const client = await import_sdk2.arkade.Arkade.connect({
|
|
601
|
-
|
|
602
|
-
|
|
705
|
+
// info the wallet already holds, plus its broadcast pair — the shape
|
|
706
|
+
// `ArkadeServerProvider` describes, without a second `/v1/info`
|
|
707
|
+
arkade: { getInfo: async () => info, ...broadcaster },
|
|
708
|
+
indexer: reader,
|
|
603
709
|
identity: wallet.identity,
|
|
604
710
|
// registered offers resolve their VTXOs from the contract repository
|
|
605
711
|
// instead of a direct indexer query; the indexer above stays as the
|
|
@@ -614,7 +720,7 @@ async function cancelOffer(wallet, arkServerUrl, offerHex, opts) {
|
|
|
614
720
|
const rebuilt = new import_sdk2.arkade.ArkadeProgramScript(program, args, keys);
|
|
615
721
|
if (import_base2.hex.encode(rebuilt.pkScript) !== import_base2.hex.encode(offer.swapPkScript)) {
|
|
616
722
|
throw new Error(
|
|
617
|
-
"rebuilt covenant does not match the offer's swapPkScript \u2014 the
|
|
723
|
+
"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"
|
|
618
724
|
);
|
|
619
725
|
}
|
|
620
726
|
const contract = new import_sdk2.arkade.ArkadeContract(client, program, args, keys);
|
|
@@ -1278,7 +1384,7 @@ var LightningReceiveCorridor = {
|
|
|
1278
1384
|
const receive = swap;
|
|
1279
1385
|
return {
|
|
1280
1386
|
expectedAmount: receive.expectedAmount,
|
|
1281
|
-
...receive.
|
|
1387
|
+
...receive.claimTxid ? { claimTxid: receive.claimTxid } : {}
|
|
1282
1388
|
};
|
|
1283
1389
|
},
|
|
1284
1390
|
hydrate(profile) {
|
|
@@ -1288,13 +1394,13 @@ var LightningReceiveCorridor = {
|
|
|
1288
1394
|
return {
|
|
1289
1395
|
...hydrateHashlock(profile),
|
|
1290
1396
|
expectedAmount: profile.expectedAmount,
|
|
1291
|
-
...profile.
|
|
1397
|
+
...profile.claimTxid ? { claimTxid: profile.claimTxid } : {}
|
|
1292
1398
|
};
|
|
1293
1399
|
},
|
|
1294
1400
|
// We are the claimant here, so the preimage material on the hashlock is
|
|
1295
1401
|
// ours to use.
|
|
1296
1402
|
claimSecret: (profile) => ({ ...profile.signer, ...profile.hashlock }),
|
|
1297
|
-
activityTxids: (profile) => profile.
|
|
1403
|
+
activityTxids: (profile) => profile.claimTxid ? [profile.claimTxid] : []
|
|
1298
1404
|
};
|
|
1299
1405
|
function onchainSendProfile(result) {
|
|
1300
1406
|
return {
|
|
@@ -1378,13 +1484,32 @@ var isRfqSwapTerminal = (state) => RFQ_SWAP_TERMINAL_STATES.includes(state);
|
|
|
1378
1484
|
|
|
1379
1485
|
// src/rfqRecord.ts
|
|
1380
1486
|
var RFQ_SWAP_RETENTION_SECONDS = 30 * 24 * 60 * 60;
|
|
1487
|
+
function renameLegacyClaimTxid(profile) {
|
|
1488
|
+
const { claimArkTxid, ...rest } = profile;
|
|
1489
|
+
return { ...rest, claimTxid: rest.claimTxid ?? claimArkTxid };
|
|
1490
|
+
}
|
|
1491
|
+
function normalizeRfqSwapRecord(record) {
|
|
1492
|
+
const { fundingArkTxid, refundArkTxid, lockupSpendArkTxids, ...rest } = record;
|
|
1493
|
+
const legacyClaim = record.kind === "lightning_receive" && record.profile.claimArkTxid !== void 0;
|
|
1494
|
+
if (!fundingArkTxid && !refundArkTxid && !lockupSpendArkTxids && !legacyClaim) return record;
|
|
1495
|
+
const fundingTxid = rest.fundingTxid ?? fundingArkTxid;
|
|
1496
|
+
const refundTxid = rest.refundTxid ?? refundArkTxid;
|
|
1497
|
+
const lockupSpendTxids = rest.lockupSpendTxids ?? lockupSpendArkTxids;
|
|
1498
|
+
return {
|
|
1499
|
+
...rest,
|
|
1500
|
+
...fundingTxid ? { fundingTxid } : {},
|
|
1501
|
+
...refundTxid ? { refundTxid } : {},
|
|
1502
|
+
...lockupSpendTxids?.length ? { lockupSpendTxids } : {},
|
|
1503
|
+
...legacyClaim ? { profile: renameLegacyClaimTxid(record.profile) } : {}
|
|
1504
|
+
};
|
|
1505
|
+
}
|
|
1381
1506
|
var managerState = (swap) => ({
|
|
1382
1507
|
rfqId: swap.rfqId,
|
|
1383
1508
|
state: swap.state,
|
|
1384
1509
|
createdAt: swap.createdAt,
|
|
1385
1510
|
updatedAt: swap.updatedAt,
|
|
1386
|
-
...swap.
|
|
1387
|
-
...swap.
|
|
1511
|
+
...swap.refundTxid ? { refundTxid: swap.refundTxid } : {},
|
|
1512
|
+
...swap.lockupSpendTxids?.length ? { lockupSpendTxids: [...swap.lockupSpendTxids] } : {},
|
|
1388
1513
|
...swap.failure ? { failure: swap.failure } : {},
|
|
1389
1514
|
...swap.blockedReason ? { blockedReason: swap.blockedReason } : {}
|
|
1390
1515
|
});
|
|
@@ -1415,27 +1540,29 @@ function createRfqSwapRecord(origin, swap) {
|
|
|
1415
1540
|
}
|
|
1416
1541
|
function updateRfqSwapRecord(record, swap) {
|
|
1417
1542
|
assertSameSwap(record, swap);
|
|
1543
|
+
const stored = normalizeRfqSwapRecord(record);
|
|
1418
1544
|
const {
|
|
1419
|
-
|
|
1420
|
-
|
|
1545
|
+
refundTxid: _refundTxid,
|
|
1546
|
+
lockupSpendTxids: _lockupSpendTxids,
|
|
1421
1547
|
failure: _failure,
|
|
1422
1548
|
blockedReason: _blockedReason,
|
|
1423
1549
|
...origin
|
|
1424
|
-
} =
|
|
1425
|
-
const handler = rfqCorridorHandlers.getOrThrow(
|
|
1550
|
+
} = stored;
|
|
1551
|
+
const handler = rfqCorridorHandlers.getOrThrow(stored.kind);
|
|
1426
1552
|
return {
|
|
1427
1553
|
...origin,
|
|
1428
1554
|
...managerState(swap),
|
|
1429
|
-
profile: { ...
|
|
1555
|
+
profile: { ...stored.profile, ...handler.project(swap) }
|
|
1430
1556
|
};
|
|
1431
1557
|
}
|
|
1432
1558
|
function rfqSwapOriginOf(record) {
|
|
1559
|
+
const stored = normalizeRfqSwapRecord(record);
|
|
1433
1560
|
return {
|
|
1434
|
-
kind:
|
|
1435
|
-
lockupAddress:
|
|
1436
|
-
profile: { ...
|
|
1437
|
-
...
|
|
1438
|
-
...
|
|
1561
|
+
kind: stored.kind,
|
|
1562
|
+
lockupAddress: stored.lockupAddress,
|
|
1563
|
+
profile: { ...stored.profile },
|
|
1564
|
+
...stored.amount !== void 0 ? { amount: stored.amount } : {},
|
|
1565
|
+
...stored.fundingTxid ? { fundingTxid: stored.fundingTxid } : {}
|
|
1439
1566
|
};
|
|
1440
1567
|
}
|
|
1441
1568
|
function lockupScript(params, lockupAddress) {
|
|
@@ -1449,27 +1576,28 @@ function lockupScript(params, lockupAddress) {
|
|
|
1449
1576
|
return script;
|
|
1450
1577
|
}
|
|
1451
1578
|
function rebuildRfqSwap(record, params) {
|
|
1452
|
-
const
|
|
1579
|
+
const stored = normalizeRfqSwapRecord(record);
|
|
1580
|
+
const script = lockupScript(params, stored.lockupAddress);
|
|
1453
1581
|
const common = {
|
|
1454
|
-
rfqId:
|
|
1455
|
-
state:
|
|
1582
|
+
rfqId: stored.rfqId,
|
|
1583
|
+
state: stored.state,
|
|
1456
1584
|
lockupPkScript: script.pkScript,
|
|
1457
|
-
lockup: { script, address:
|
|
1585
|
+
lockup: { script, address: stored.lockupAddress },
|
|
1458
1586
|
// From the covenant, which binds it: the record's own copy would be a
|
|
1459
1587
|
// second source for the deadline the refund is gated on.
|
|
1460
1588
|
refundLocktime: Number(script.options.refundLocktime),
|
|
1461
|
-
createdAt:
|
|
1462
|
-
updatedAt:
|
|
1463
|
-
...
|
|
1464
|
-
...
|
|
1465
|
-
...
|
|
1466
|
-
...
|
|
1589
|
+
createdAt: stored.createdAt,
|
|
1590
|
+
updatedAt: stored.updatedAt,
|
|
1591
|
+
...stored.refundTxid ? { refundTxid: stored.refundTxid } : {},
|
|
1592
|
+
...stored.lockupSpendTxids?.length ? { lockupSpendTxids: [...stored.lockupSpendTxids] } : {},
|
|
1593
|
+
...stored.failure ? { failure: stored.failure } : {},
|
|
1594
|
+
...stored.blockedReason ? { blockedReason: stored.blockedReason } : {}
|
|
1467
1595
|
};
|
|
1468
|
-
const handler = rfqCorridorHandlers.getOrThrow(
|
|
1596
|
+
const handler = rfqCorridorHandlers.getOrThrow(stored.kind);
|
|
1469
1597
|
return {
|
|
1470
1598
|
...common,
|
|
1471
|
-
kind:
|
|
1472
|
-
...handler.hydrate(
|
|
1599
|
+
kind: stored.kind,
|
|
1600
|
+
...handler.hydrate(stored.profile, { lockup: script })
|
|
1473
1601
|
};
|
|
1474
1602
|
}
|
|
1475
1603
|
function shouldRetainRfqSwap(record, now) {
|
|
@@ -1506,13 +1634,15 @@ async function fetchParsedTxs(indexer, txids) {
|
|
|
1506
1634
|
var unscannedSwapCandidates = (txs, existingIds, scanned) => txs.filter(
|
|
1507
1635
|
(tx) => tx.type === "sent" && tx.redeemTxid && !existingIds.has(tx.redeemTxid) && !scanned.has(tx.redeemTxid)
|
|
1508
1636
|
);
|
|
1509
|
-
function classifySpend(offer,
|
|
1637
|
+
function classifySpend(offer, operatorPubkey, spendTx, deposit) {
|
|
1510
1638
|
let leaves;
|
|
1511
1639
|
try {
|
|
1512
|
-
const script =
|
|
1640
|
+
const script = offerContract(offer, operatorPubkey);
|
|
1513
1641
|
if (import_base6.hex.encode(script.pkScript) !== import_base6.hex.encode(offer.swapPkScript)) return "indeterminate";
|
|
1514
1642
|
leaves = {
|
|
1515
|
-
|
|
1643
|
+
// both routes that hand the deposit back; `exit` is absent on an
|
|
1644
|
+
// offer that carries no exit closure, and drops out here
|
|
1645
|
+
returned: ["cancel", "exit"].map((name) => script.functionByName(name)?.leafScript).filter((leaf) => leaf !== void 0),
|
|
1516
1646
|
fulfill: script.functionByName("fulfill")?.leafScript
|
|
1517
1647
|
};
|
|
1518
1648
|
} catch {
|
|
@@ -1524,22 +1654,22 @@ function classifySpend(offer, serverPubkey, spendTx, deposit) {
|
|
|
1524
1654
|
if (import_base6.hex.encode(input.txid) !== deposit.txid) continue;
|
|
1525
1655
|
for (const leaf of input.tapLeafScript ?? []) {
|
|
1526
1656
|
const spent = import_base6.hex.encode((0, import_sdk6.scriptFromTapLeafScript)(leaf));
|
|
1527
|
-
if (leaves.
|
|
1657
|
+
if (leaves.returned.some((back) => spent === import_base6.hex.encode(back))) return "cancelled";
|
|
1528
1658
|
if (leaves.fulfill && spent === import_base6.hex.encode(leaves.fulfill)) return "fulfilled";
|
|
1529
1659
|
}
|
|
1530
1660
|
}
|
|
1531
1661
|
return "indeterminate";
|
|
1532
1662
|
}
|
|
1533
1663
|
var spendTxidsOf = (vtxo) => [vtxo.spentBy, vtxo.arkTxId].filter((id) => Boolean(id));
|
|
1534
|
-
function classifyDepositSpend(offer,
|
|
1664
|
+
function classifyDepositSpend(offer, operatorPubkey, spendTxs, deposit) {
|
|
1535
1665
|
for (const tx of spendTxs) {
|
|
1536
|
-
const kind = classifySpend(offer,
|
|
1666
|
+
const kind = classifySpend(offer, operatorPubkey, tx, deposit);
|
|
1537
1667
|
if (kind !== "indeterminate") return kind;
|
|
1538
1668
|
}
|
|
1539
1669
|
return "indeterminate";
|
|
1540
1670
|
}
|
|
1541
1671
|
async function restoreAssetSwaps(indexer, txs, existingIds, opts) {
|
|
1542
|
-
const {
|
|
1672
|
+
const { operatorPubkey, scanned = /* @__PURE__ */ new Set() } = opts;
|
|
1543
1673
|
const candidates = unscannedSwapCandidates(txs, existingIds, scanned);
|
|
1544
1674
|
if (candidates.length === 0) return { restored: [], scannedTxids: [] };
|
|
1545
1675
|
const byTxid = new Map(candidates.map((tx) => [tx.redeemTxid, tx]));
|
|
@@ -1608,7 +1738,7 @@ async function restoreAssetSwaps(indexer, txs, existingIds, opts) {
|
|
|
1608
1738
|
if (state === "swept") status = "recoverable";
|
|
1609
1739
|
else if (state === "spent") {
|
|
1610
1740
|
const spendTxs = spendTxidsOf(vtxo).map((id) => spendTxByTxid.get(id)).filter((tx) => tx !== void 0);
|
|
1611
|
-
const kind = classifyDepositSpend(offer,
|
|
1741
|
+
const kind = classifyDepositSpend(offer, operatorPubkey, spendTxs, {
|
|
1612
1742
|
txid: vtxo.txid,
|
|
1613
1743
|
vout: vtxo.vout
|
|
1614
1744
|
});
|
|
@@ -1625,7 +1755,7 @@ async function restoreAssetSwaps(indexer, txs, existingIds, opts) {
|
|
|
1625
1755
|
fromAmount,
|
|
1626
1756
|
toAmount: offer.wantAmount.toString(),
|
|
1627
1757
|
// ponytail(arkade-os/ts-sdk#680): empty address makes cancel fall back
|
|
1628
|
-
// to the current
|
|
1758
|
+
// to the current operator key; store the funded address if operator-key
|
|
1629
1759
|
// rotations become real (cancelOffer now at least diagnoses the
|
|
1630
1760
|
// mismatch instead of reporting a missing VTXO)
|
|
1631
1761
|
swapAddress: "",
|
|
@@ -1660,13 +1790,15 @@ function spendUpdate(swap, spend) {
|
|
|
1660
1790
|
}
|
|
1661
1791
|
async function watchOfferSwaps({
|
|
1662
1792
|
wallet,
|
|
1663
|
-
arkServerUrl,
|
|
1664
1793
|
repository,
|
|
1665
1794
|
onUpdate
|
|
1666
1795
|
}) {
|
|
1667
|
-
const manager = await
|
|
1668
|
-
|
|
1669
|
-
|
|
1796
|
+
const [manager, address, indexer] = await Promise.all([
|
|
1797
|
+
wallet.getContractManager(),
|
|
1798
|
+
wallet.getAddress(),
|
|
1799
|
+
wallet.getArkadeReader()
|
|
1800
|
+
]);
|
|
1801
|
+
const operatorPubkey = import_sdk7.ArkAddress.decode(address).serverPubKey;
|
|
1670
1802
|
let queue = Promise.resolve();
|
|
1671
1803
|
const enqueue = (task) => {
|
|
1672
1804
|
queue = queue.then(task).catch(() => {
|
|
@@ -1680,7 +1812,7 @@ async function watchOfferSwaps({
|
|
|
1680
1812
|
const { txs } = await indexer.getVirtualTxs(candidates);
|
|
1681
1813
|
return classifyDepositSpend(
|
|
1682
1814
|
decodeOffer(import_base7.hex.decode(swap.offerHex)),
|
|
1683
|
-
|
|
1815
|
+
operatorPubkey,
|
|
1684
1816
|
txs.map((psbt) => import_sdk7.Transaction.fromPSBT(import_base7.base64.decode(psbt))),
|
|
1685
1817
|
{ txid: vtxo.txid, vout: vtxo.vout }
|
|
1686
1818
|
);
|
|
@@ -1834,11 +1966,14 @@ var ONCHAIN_SEND_PAIR = rfqPair(ARKADE_BTC, ONCHAIN_BTC);
|
|
|
1834
1966
|
var ONCHAIN_RECEIVE_PAIR = rfqPair(ONCHAIN_BTC, ARKADE_BTC);
|
|
1835
1967
|
var RFQ_TERMINAL_STATES = ["settled", "refused", "expired", "refunded", "stuck"];
|
|
1836
1968
|
var SwapRefusal = class extends Error {
|
|
1969
|
+
/** Literal-typed so the v2 error taxonomy's union discriminates on `name`
|
|
1970
|
+
* — a `string` here collapses the discriminant for every member. Same value
|
|
1971
|
+
* the constructor has always set, moved to a field initializer. */
|
|
1972
|
+
name = "SwapRefusal";
|
|
1837
1973
|
reason;
|
|
1838
1974
|
rfqId;
|
|
1839
1975
|
constructor(reason, rfqId) {
|
|
1840
1976
|
super(`solver refused: ${reason}`);
|
|
1841
|
-
this.name = "SwapRefusal";
|
|
1842
1977
|
this.reason = reason;
|
|
1843
1978
|
this.rfqId = rfqId;
|
|
1844
1979
|
}
|
|
@@ -1918,8 +2053,22 @@ var assertPairLength = (pair) => {
|
|
|
1918
2053
|
};
|
|
1919
2054
|
var verifyLockupAddress = (quote, derivedAddress) => {
|
|
1920
2055
|
const quoted = quote.profile?.lockup_address;
|
|
1921
|
-
|
|
1922
|
-
|
|
2056
|
+
const candidates = Array.isArray(derivedAddress) ? derivedAddress : [derivedAddress];
|
|
2057
|
+
const matched = candidates.find((address) => address === quoted);
|
|
2058
|
+
if (matched === void 0) throw new AddressMismatch(candidates, quoted);
|
|
2059
|
+
return matched;
|
|
2060
|
+
};
|
|
2061
|
+
var LOCKUP_SHAPE_VARIANTS = [void 0, "preTimelockedRefund"];
|
|
2062
|
+
var matchQuotedLockup = (quote, hrp, serverPubkey, build) => {
|
|
2063
|
+
const candidates = LOCKUP_SHAPE_VARIANTS.map((legacy) => {
|
|
2064
|
+
const script = build(legacy);
|
|
2065
|
+
return { script, address: script.address(hrp, serverPubkey).encode(), legacy };
|
|
2066
|
+
});
|
|
2067
|
+
const matchedAddress = verifyLockupAddress(
|
|
2068
|
+
quote,
|
|
2069
|
+
candidates.map((candidate) => candidate.address)
|
|
2070
|
+
);
|
|
2071
|
+
return candidates.find((candidate) => candidate.address === matchedAddress);
|
|
1923
2072
|
};
|
|
1924
2073
|
var assertFundable = (input) => {
|
|
1925
2074
|
const fail = (reason, message) => {
|
|
@@ -2090,22 +2239,22 @@ var relayTransport = (relayUrl, options) => {
|
|
|
2090
2239
|
};
|
|
2091
2240
|
var SEQUENCE_GRANULARITY_SECONDS = 512;
|
|
2092
2241
|
var SOLO_REFUND_HEADROOM_SECONDS = 8 * SEQUENCE_GRANULARITY_SECONDS;
|
|
2093
|
-
var unilateralClaimDelay = (
|
|
2094
|
-
if (!Number.isFinite(
|
|
2242
|
+
var unilateralClaimDelay = (operatorExitDelaySeconds) => {
|
|
2243
|
+
if (!Number.isFinite(operatorExitDelaySeconds) || operatorExitDelaySeconds < SEQUENCE_GRANULARITY_SECONDS) {
|
|
2095
2244
|
throw new Error(
|
|
2096
|
-
`
|
|
2245
|
+
`operator exit delay must be at least ${SEQUENCE_GRANULARITY_SECONDS}s of seconds, got ${operatorExitDelaySeconds}`
|
|
2097
2246
|
);
|
|
2098
2247
|
}
|
|
2099
|
-
if (
|
|
2248
|
+
if (operatorExitDelaySeconds > 65535 * SEQUENCE_GRANULARITY_SECONDS - SOLO_REFUND_HEADROOM_SECONDS) {
|
|
2100
2249
|
throw new Error(
|
|
2101
|
-
`
|
|
2250
|
+
`operator exit delay ${operatorExitDelaySeconds}s exceeds what BIP68 can encode once the solo refund's headroom is stacked above it`
|
|
2102
2251
|
);
|
|
2103
2252
|
}
|
|
2104
|
-
return Math.ceil(
|
|
2253
|
+
return Math.ceil(operatorExitDelaySeconds / SEQUENCE_GRANULARITY_SECONDS) * SEQUENCE_GRANULARITY_SECONDS;
|
|
2105
2254
|
};
|
|
2106
2255
|
var unilateralRefundDelay = (claimDelay) => claimDelay;
|
|
2107
2256
|
var unilateralRefundWithoutReceiverDelay = (claimDelay) => claimDelay + SOLO_REFUND_HEADROOM_SECONDS;
|
|
2108
|
-
function
|
|
2257
|
+
function lightningSendContract(params) {
|
|
2109
2258
|
const seconds = (value) => ({
|
|
2110
2259
|
type: "seconds",
|
|
2111
2260
|
value: BigInt(value)
|
|
@@ -2113,7 +2262,7 @@ function lightningSendVtxoScript(params) {
|
|
|
2113
2262
|
return new import_sdk9.VHTLC.ScriptV2({
|
|
2114
2263
|
sender: params.senderPubkey,
|
|
2115
2264
|
receiver: params.solverPubkey,
|
|
2116
|
-
server: params.
|
|
2265
|
+
server: params.operatorPubkey,
|
|
2117
2266
|
preimageHash: (0, import_legacy2.ripemd160)(import_base10.hex.decode(params.paymentHash)),
|
|
2118
2267
|
refundLocktime: BigInt(params.refundLocktime),
|
|
2119
2268
|
unilateralClaimDelay: seconds(params.claimDelay),
|
|
@@ -2121,22 +2270,20 @@ function lightningSendVtxoScript(params) {
|
|
|
2121
2270
|
unilateralRefundWithoutReceiverDelay: seconds(
|
|
2122
2271
|
unilateralRefundWithoutReceiverDelay(params.claimDelay)
|
|
2123
2272
|
),
|
|
2124
|
-
|
|
2273
|
+
nonInteractiveParameters: {
|
|
2125
2274
|
receiverPkScript: params.receiverPkScript,
|
|
2126
|
-
emulatorPubkey: params.emulatorPubkey
|
|
2127
|
-
},
|
|
2128
|
-
nonInteractiveRefund: {
|
|
2129
2275
|
senderPkScript: params.refundPkScript,
|
|
2130
|
-
emulatorPubkey: params.emulatorPubkey
|
|
2276
|
+
emulatorPubkey: params.emulatorPubkey,
|
|
2277
|
+
...params.legacy !== void 0 && { legacy: params.legacy }
|
|
2131
2278
|
}
|
|
2132
2279
|
});
|
|
2133
2280
|
}
|
|
2134
|
-
async function requestLightningSend(wallet,
|
|
2281
|
+
async function requestLightningSend(wallet, transport, params) {
|
|
2135
2282
|
const rfqId = params.rfqId ?? newRfqId();
|
|
2136
2283
|
const secrets = await (0, import_sdk10.provisionRefundKey)(wallet);
|
|
2137
2284
|
const senderPubkey = secrets.pubkey;
|
|
2138
2285
|
const refundAddress = secrets.address;
|
|
2139
|
-
const info = await
|
|
2286
|
+
const info = await wallet.getArkadeInfo({ requireLive: true });
|
|
2140
2287
|
const quote = await transport.requestQuote(
|
|
2141
2288
|
lightningSendRequest({ rfqId, invoice: params.invoice.raw, refundAddress, senderPubkey })
|
|
2142
2289
|
);
|
|
@@ -2157,12 +2304,12 @@ async function requestLightningSend(wallet, arkServerUrl, transport, params) {
|
|
|
2157
2304
|
`quote from_amount ${quote.from_amount} is below the invoice amount \u2014 a negative spread is not a quote`
|
|
2158
2305
|
);
|
|
2159
2306
|
}
|
|
2160
|
-
const
|
|
2161
|
-
const network = (0, import_sdk9.
|
|
2162
|
-
const
|
|
2307
|
+
const operatorPubkey = (0, import_sdk9.toXOnly)(import_base10.hex.decode(info.signerPubkey), "ark signer key");
|
|
2308
|
+
const network = (0, import_sdk9.networkFromArkadeInfo)(info);
|
|
2309
|
+
const contractParams = {
|
|
2163
2310
|
solverPubkey: (0, import_sdk9.toXOnly)(import_base10.hex.decode(quote.solver_pubkey), "solver key"),
|
|
2164
2311
|
refundLocktime: quote.refund_locktime,
|
|
2165
|
-
|
|
2312
|
+
operatorPubkey,
|
|
2166
2313
|
paymentHash: params.invoice.paymentHash,
|
|
2167
2314
|
claimDelay: unilateralClaimDelay(Number(info.unilateralExitDelay)),
|
|
2168
2315
|
emulatorPubkey: (0, import_sdk9.toXOnly)(
|
|
@@ -2173,9 +2320,18 @@ async function requestLightningSend(wallet, arkServerUrl, transport, params) {
|
|
|
2173
2320
|
receiverPkScript: solverHex(receiverPkScriptHex, "profile.receiver_pk_script"),
|
|
2174
2321
|
refundPkScript: secrets.pkScript
|
|
2175
2322
|
};
|
|
2176
|
-
const
|
|
2177
|
-
|
|
2178
|
-
|
|
2323
|
+
const matched = matchQuotedLockup(
|
|
2324
|
+
quote,
|
|
2325
|
+
network.hrp,
|
|
2326
|
+
operatorPubkey,
|
|
2327
|
+
(legacy) => lightningSendContract({ ...contractParams, ...legacy !== void 0 && { legacy } })
|
|
2328
|
+
);
|
|
2329
|
+
const script = matched.script;
|
|
2330
|
+
const address = matched.address;
|
|
2331
|
+
const matchedContractParams = {
|
|
2332
|
+
...contractParams,
|
|
2333
|
+
...matched.legacy !== void 0 && { legacy: matched.legacy }
|
|
2334
|
+
};
|
|
2179
2335
|
assertFundable({
|
|
2180
2336
|
quote,
|
|
2181
2337
|
invoiceExpiresAt: params.invoice.expiresAt,
|
|
@@ -2194,7 +2350,7 @@ async function requestLightningSend(wallet, arkServerUrl, transport, params) {
|
|
|
2194
2350
|
refundAddress,
|
|
2195
2351
|
senderPubkey,
|
|
2196
2352
|
secrets,
|
|
2197
|
-
|
|
2353
|
+
contractParams: matchedContractParams
|
|
2198
2354
|
};
|
|
2199
2355
|
}
|
|
2200
2356
|
var offerTermsFromQuote = (quote, assets) => {
|
|
@@ -2259,19 +2415,23 @@ function deriveOnchainSend(input) {
|
|
|
2259
2415
|
if (refundLocktime === void 0 || htlcPubkey === void 0 || htlcLocktime === void 0 || minConfirmations === void 0 || receiverPkScriptHex === void 0) {
|
|
2260
2416
|
throw new Error("onchain-send quote is missing a binding field");
|
|
2261
2417
|
}
|
|
2262
|
-
const
|
|
2418
|
+
const contractParams = {
|
|
2263
2419
|
solverPubkey: (0, import_sdk9.toXOnly)(import_base10.hex.decode(quote.solver_pubkey), "solver key"),
|
|
2264
2420
|
refundLocktime,
|
|
2265
|
-
|
|
2421
|
+
operatorPubkey: input.operatorPubkey,
|
|
2266
2422
|
paymentHash: input.paymentHash,
|
|
2267
2423
|
claimDelay: input.claimDelay,
|
|
2268
2424
|
emulatorPubkey: input.emulatorPubkey,
|
|
2269
2425
|
senderPubkey: input.senderPubkey,
|
|
2270
2426
|
receiverPkScript: solverHex(receiverPkScriptHex, "profile.receiver_pk_script"),
|
|
2271
2427
|
refundPkScript: import_sdk9.ArkAddress.decode(input.refundAddress).pkScript
|
|
2272
|
-
}
|
|
2273
|
-
const address =
|
|
2274
|
-
|
|
2428
|
+
};
|
|
2429
|
+
const { script, address } = matchQuotedLockup(
|
|
2430
|
+
quote,
|
|
2431
|
+
input.hrp,
|
|
2432
|
+
input.operatorPubkey,
|
|
2433
|
+
(legacy) => lightningSendContract({ ...contractParams, ...legacy !== void 0 && { legacy } })
|
|
2434
|
+
);
|
|
2275
2435
|
const htlcParams = {
|
|
2276
2436
|
paymentHash: input.paymentHash,
|
|
2277
2437
|
claimKey: input.payoutPubkey,
|
|
@@ -2292,7 +2452,7 @@ function deriveOnchainSend(input) {
|
|
|
2292
2452
|
minConfirmations
|
|
2293
2453
|
};
|
|
2294
2454
|
}
|
|
2295
|
-
async function requestOnchainSend(wallet,
|
|
2455
|
+
async function requestOnchainSend(wallet, transport, params) {
|
|
2296
2456
|
const rfqId = params.rfqId ?? newRfqId();
|
|
2297
2457
|
const secrets = await (0, import_sdk10.provisionClaimSecret)(wallet, { preimage: params.preimage });
|
|
2298
2458
|
if (secrets.mustPersistPreimage) {
|
|
@@ -2303,7 +2463,7 @@ async function requestOnchainSend(wallet, arkServerUrl, transport, params) {
|
|
|
2303
2463
|
const paymentHash = import_base10.hex.encode(secrets.paymentHash);
|
|
2304
2464
|
const senderPubkey = secrets.pubkey;
|
|
2305
2465
|
const [info, refundAddress] = await Promise.all([
|
|
2306
|
-
|
|
2466
|
+
wallet.getArkadeInfo({ requireLive: true }),
|
|
2307
2467
|
wallet.getAddress()
|
|
2308
2468
|
]);
|
|
2309
2469
|
const quote = await transport.requestQuote(
|
|
@@ -2317,12 +2477,12 @@ async function requestOnchainSend(wallet, arkServerUrl, transport, params) {
|
|
|
2317
2477
|
amountSide: params.amountSide
|
|
2318
2478
|
})
|
|
2319
2479
|
);
|
|
2320
|
-
const network = (0, import_sdk9.
|
|
2480
|
+
const network = (0, import_sdk9.networkFromArkadeInfo)(info);
|
|
2321
2481
|
const derived = deriveOnchainSend({
|
|
2322
2482
|
quote,
|
|
2323
2483
|
paymentHash,
|
|
2324
2484
|
payoutPubkey: params.payoutPubkey,
|
|
2325
|
-
|
|
2485
|
+
operatorPubkey: (0, import_sdk9.toXOnly)(import_base10.hex.decode(info.signerPubkey), "ark signer key"),
|
|
2326
2486
|
emulatorPubkey: (0, import_sdk9.toXOnly)(
|
|
2327
2487
|
import_base10.hex.decode((0, import_sdk9.resolveEmulatorPubkey)(network, params.emulatorPubkey)),
|
|
2328
2488
|
"emulator signer key"
|
|
@@ -2359,6 +2519,7 @@ async function requestOnchainSend(wallet, arkServerUrl, transport, params) {
|
|
|
2359
2519
|
htlcParams: derived.htlcParams,
|
|
2360
2520
|
l1Network: derived.l1Network,
|
|
2361
2521
|
minConfirmations: derived.minConfirmations,
|
|
2522
|
+
refundLocktime: derived.refundLocktime,
|
|
2362
2523
|
senderPubkey,
|
|
2363
2524
|
secrets
|
|
2364
2525
|
};
|
|
@@ -2430,7 +2591,7 @@ var assertReceivable = (input) => {
|
|
|
2430
2591
|
);
|
|
2431
2592
|
}
|
|
2432
2593
|
};
|
|
2433
|
-
function
|
|
2594
|
+
function lightningReceiveContract(params) {
|
|
2434
2595
|
const seconds = (value) => ({
|
|
2435
2596
|
type: "seconds",
|
|
2436
2597
|
value: BigInt(value)
|
|
@@ -2438,7 +2599,7 @@ function receiveVtxoScript(params) {
|
|
|
2438
2599
|
return new import_sdk9.VHTLC.ScriptV2({
|
|
2439
2600
|
sender: params.solverPubkey,
|
|
2440
2601
|
receiver: params.payoutPubkey,
|
|
2441
|
-
server: params.
|
|
2602
|
+
server: params.operatorPubkey,
|
|
2442
2603
|
preimageHash: (0, import_legacy2.ripemd160)(import_base10.hex.decode(params.paymentHash)),
|
|
2443
2604
|
refundLocktime: BigInt(params.refundLocktime),
|
|
2444
2605
|
unilateralClaimDelay: seconds(params.claimDelay),
|
|
@@ -2446,13 +2607,11 @@ function receiveVtxoScript(params) {
|
|
|
2446
2607
|
unilateralRefundWithoutReceiverDelay: seconds(
|
|
2447
2608
|
unilateralRefundWithoutReceiverDelay(params.claimDelay)
|
|
2448
2609
|
),
|
|
2449
|
-
|
|
2610
|
+
nonInteractiveParameters: {
|
|
2450
2611
|
receiverPkScript: params.payoutPkScript,
|
|
2451
|
-
emulatorPubkey: params.emulatorPubkey
|
|
2452
|
-
},
|
|
2453
|
-
nonInteractiveRefund: {
|
|
2454
2612
|
senderPkScript: params.solverRefundPkScript,
|
|
2455
|
-
emulatorPubkey: params.emulatorPubkey
|
|
2613
|
+
emulatorPubkey: params.emulatorPubkey,
|
|
2614
|
+
...params.legacy !== void 0 && { legacy: params.legacy }
|
|
2456
2615
|
}
|
|
2457
2616
|
});
|
|
2458
2617
|
}
|
|
@@ -2465,10 +2624,10 @@ function deriveLightningReceive(input) {
|
|
|
2465
2624
|
if (refundLocktime === void 0 || invoice === void 0 || solverRefundPkScriptHex === void 0) {
|
|
2466
2625
|
throw new Error("lightning-receive quote is missing a binding field");
|
|
2467
2626
|
}
|
|
2468
|
-
const
|
|
2627
|
+
const contractParams = {
|
|
2469
2628
|
solverPubkey: (0, import_sdk9.toXOnly)(import_base10.hex.decode(quote.solver_pubkey), "solver key"),
|
|
2470
2629
|
refundLocktime,
|
|
2471
|
-
|
|
2630
|
+
operatorPubkey: input.operatorPubkey,
|
|
2472
2631
|
paymentHash: input.paymentHash,
|
|
2473
2632
|
claimDelay: input.claimDelay,
|
|
2474
2633
|
emulatorPubkey: input.emulatorPubkey,
|
|
@@ -2476,12 +2635,25 @@ function deriveLightningReceive(input) {
|
|
|
2476
2635
|
payoutPubkey: input.payoutPubkey,
|
|
2477
2636
|
payoutPkScript: import_sdk9.ArkAddress.decode(input.payoutAddress).pkScript
|
|
2478
2637
|
};
|
|
2479
|
-
const
|
|
2480
|
-
|
|
2481
|
-
|
|
2482
|
-
|
|
2638
|
+
const matched = matchQuotedLockup(
|
|
2639
|
+
quote,
|
|
2640
|
+
input.hrp,
|
|
2641
|
+
input.operatorPubkey,
|
|
2642
|
+
(legacy) => lightningReceiveContract({ ...contractParams, ...legacy !== void 0 && { legacy } })
|
|
2643
|
+
);
|
|
2644
|
+
return {
|
|
2645
|
+
address: matched.address,
|
|
2646
|
+
swapPkScript: matched.script.pkScript,
|
|
2647
|
+
script: matched.script,
|
|
2648
|
+
invoice,
|
|
2649
|
+
refundLocktime,
|
|
2650
|
+
contractParams: {
|
|
2651
|
+
...contractParams,
|
|
2652
|
+
...matched.legacy !== void 0 && { legacy: matched.legacy }
|
|
2653
|
+
}
|
|
2654
|
+
};
|
|
2483
2655
|
}
|
|
2484
|
-
async function requestLightningReceive(wallet,
|
|
2656
|
+
async function requestLightningReceive(wallet, transport, params) {
|
|
2485
2657
|
const rfqId = params.rfqId ?? newRfqId();
|
|
2486
2658
|
const secrets = await (0, import_sdk10.provisionClaimSecret)(wallet);
|
|
2487
2659
|
if (secrets.mustPersistPreimage) {
|
|
@@ -2493,7 +2665,7 @@ async function requestLightningReceive(wallet, arkServerUrl, transport, params)
|
|
|
2493
2665
|
const paymentHash = import_base10.hex.encode(secrets.paymentHash);
|
|
2494
2666
|
const payoutPubkey = secrets.pubkey;
|
|
2495
2667
|
const [info, payoutAddress] = await Promise.all([
|
|
2496
|
-
|
|
2668
|
+
wallet.getArkadeInfo({ requireLive: true }),
|
|
2497
2669
|
wallet.getAddress()
|
|
2498
2670
|
]);
|
|
2499
2671
|
const claimPacket = await sealClaimPacket({
|
|
@@ -2512,13 +2684,13 @@ async function requestLightningReceive(wallet, arkServerUrl, transport, params)
|
|
|
2512
2684
|
})
|
|
2513
2685
|
);
|
|
2514
2686
|
assertQuotedAmount(quote, params.amountSide, params.amount);
|
|
2515
|
-
const network = (0, import_sdk9.
|
|
2687
|
+
const network = (0, import_sdk9.networkFromArkadeInfo)(info);
|
|
2516
2688
|
const derived = deriveLightningReceive({
|
|
2517
2689
|
quote,
|
|
2518
2690
|
paymentHash,
|
|
2519
2691
|
payoutPubkey,
|
|
2520
2692
|
payoutAddress,
|
|
2521
|
-
|
|
2693
|
+
operatorPubkey: (0, import_sdk9.toXOnly)(import_base10.hex.decode(info.signerPubkey), "ark signer key"),
|
|
2522
2694
|
emulatorPubkey: (0, import_sdk9.toXOnly)(
|
|
2523
2695
|
import_base10.hex.decode((0, import_sdk9.resolveEmulatorPubkey)(network, params.emulatorPubkey)),
|
|
2524
2696
|
"emulator signer key"
|
|
@@ -2552,7 +2724,7 @@ async function requestLightningReceive(wallet, arkServerUrl, transport, params)
|
|
|
2552
2724
|
payoutAddress,
|
|
2553
2725
|
payoutPubkey,
|
|
2554
2726
|
secrets,
|
|
2555
|
-
|
|
2727
|
+
contractParams: derived.contractParams
|
|
2556
2728
|
};
|
|
2557
2729
|
}
|
|
2558
2730
|
function deriveOnchainReceive(input) {
|
|
@@ -2567,19 +2739,26 @@ function deriveOnchainReceive(input) {
|
|
|
2567
2739
|
if (refundLocktime === void 0 || claimPubkey === void 0 || htlcLocktime === void 0 || minConfirmations === void 0 || solverRefundPkScriptHex === void 0) {
|
|
2568
2740
|
throw new Error("onchain-receive quote is missing a binding field");
|
|
2569
2741
|
}
|
|
2570
|
-
const
|
|
2742
|
+
const contractParams = {
|
|
2571
2743
|
solverPubkey: (0, import_sdk9.toXOnly)(import_base10.hex.decode(quote.solver_pubkey), "solver key"),
|
|
2572
2744
|
refundLocktime,
|
|
2573
|
-
|
|
2745
|
+
operatorPubkey: input.operatorPubkey,
|
|
2574
2746
|
paymentHash: input.paymentHash,
|
|
2575
2747
|
claimDelay: input.claimDelay,
|
|
2576
2748
|
emulatorPubkey: input.emulatorPubkey,
|
|
2577
2749
|
solverRefundPkScript: solverHex(solverRefundPkScriptHex, "profile.solver_refund_pk_script"),
|
|
2578
2750
|
payoutPubkey: input.payoutPubkey,
|
|
2579
2751
|
payoutPkScript: import_sdk9.ArkAddress.decode(input.payoutAddress).pkScript
|
|
2580
|
-
}
|
|
2581
|
-
const address =
|
|
2582
|
-
|
|
2752
|
+
};
|
|
2753
|
+
const { script, address } = matchQuotedLockup(
|
|
2754
|
+
quote,
|
|
2755
|
+
input.hrp,
|
|
2756
|
+
input.operatorPubkey,
|
|
2757
|
+
(legacy) => lightningReceiveContract({
|
|
2758
|
+
...contractParams,
|
|
2759
|
+
...legacy !== void 0 && { legacy }
|
|
2760
|
+
})
|
|
2761
|
+
);
|
|
2583
2762
|
const htlc = onchainHtlcScript(
|
|
2584
2763
|
{
|
|
2585
2764
|
paymentHash: input.paymentHash,
|
|
@@ -2600,7 +2779,7 @@ function deriveOnchainReceive(input) {
|
|
|
2600
2779
|
minConfirmations
|
|
2601
2780
|
};
|
|
2602
2781
|
}
|
|
2603
|
-
async function requestOnchainReceive(wallet,
|
|
2782
|
+
async function requestOnchainReceive(wallet, transport, params) {
|
|
2604
2783
|
const rfqId = params.rfqId ?? newRfqId();
|
|
2605
2784
|
const secrets = await (0, import_sdk10.provisionClaimSecret)(wallet);
|
|
2606
2785
|
if (secrets.mustPersistPreimage) {
|
|
@@ -2612,7 +2791,7 @@ async function requestOnchainReceive(wallet, arkServerUrl, transport, params) {
|
|
|
2612
2791
|
const paymentHash = import_base10.hex.encode(secrets.paymentHash);
|
|
2613
2792
|
const payoutPubkey = secrets.pubkey;
|
|
2614
2793
|
const [info, payoutAddress] = await Promise.all([
|
|
2615
|
-
|
|
2794
|
+
wallet.getArkadeInfo({ requireLive: true }),
|
|
2616
2795
|
wallet.getAddress()
|
|
2617
2796
|
]);
|
|
2618
2797
|
const claimPacket = await sealClaimPacket({
|
|
@@ -2632,14 +2811,14 @@ async function requestOnchainReceive(wallet, arkServerUrl, transport, params) {
|
|
|
2632
2811
|
})
|
|
2633
2812
|
);
|
|
2634
2813
|
assertQuotedAmount(quote, params.amountSide, params.amount);
|
|
2635
|
-
const network = (0, import_sdk9.
|
|
2814
|
+
const network = (0, import_sdk9.networkFromArkadeInfo)(info);
|
|
2636
2815
|
const derived = deriveOnchainReceive({
|
|
2637
2816
|
quote,
|
|
2638
2817
|
paymentHash,
|
|
2639
2818
|
payoutPubkey,
|
|
2640
2819
|
payoutAddress,
|
|
2641
2820
|
refundPubkey: params.refundPubkey,
|
|
2642
|
-
|
|
2821
|
+
operatorPubkey: (0, import_sdk9.toXOnly)(import_base10.hex.decode(info.signerPubkey), "ark signer key"),
|
|
2643
2822
|
emulatorPubkey: (0, import_sdk9.toXOnly)(
|
|
2644
2823
|
import_base10.hex.decode((0, import_sdk9.resolveEmulatorPubkey)(network, params.emulatorPubkey)),
|
|
2645
2824
|
"emulator signer key"
|
|
@@ -2717,11 +2896,10 @@ var LockupNeedsRecoveryError = class extends Error {
|
|
|
2717
2896
|
* into one settlement with no CLTV awareness, so an early attempt can fail
|
|
2718
2897
|
* the whole batch — including unrelated outputs that were otherwise fine.
|
|
2719
2898
|
*
|
|
2720
|
-
* Exposed as a value, not only inside the message, so a caller can
|
|
2721
|
-
*
|
|
2722
|
-
*
|
|
2723
|
-
*
|
|
2724
|
-
* not an exact alarm.
|
|
2899
|
+
* Exposed as a value, not only inside the message, so a caller can skip
|
|
2900
|
+
* pre-CLTV recoverable outputs without parsing prose. Seconds-based
|
|
2901
|
+
* locktimes mature against the chain tip's timestamp rather than wall
|
|
2902
|
+
* clock, so treat this as a floor to wait past, not an exact alarm.
|
|
2725
2903
|
*/
|
|
2726
2904
|
recoverableAfter;
|
|
2727
2905
|
constructor(outpoints, recoverableAfter) {
|
|
@@ -2782,7 +2960,7 @@ async function readLockupFate(indexer, input) {
|
|
|
2782
2960
|
if (vtxo.spentBy)
|
|
2783
2961
|
spentBy.set(vtxo.spentBy, {
|
|
2784
2962
|
checkpointTxid: vtxo.spentBy,
|
|
2785
|
-
|
|
2963
|
+
txid: vtxo.arkTxId
|
|
2786
2964
|
});
|
|
2787
2965
|
else everySpendNamed = false;
|
|
2788
2966
|
}
|
|
@@ -2811,32 +2989,32 @@ async function readLockupFate(indexer, input) {
|
|
|
2811
2989
|
}
|
|
2812
2990
|
return everySpendNamed && observed.size === spentBy.size ? { fate: "returned", spends } : { fate: "unknown" };
|
|
2813
2991
|
}
|
|
2814
|
-
async function pushRefundWithoutReceiver(
|
|
2992
|
+
async function pushRefundWithoutReceiver(operator, input) {
|
|
2815
2993
|
if (input.vtxos.length === 0) throw new Error("nothing to refund: no funded outputs");
|
|
2816
2994
|
const swept = input.vtxos.filter((vtxo) => vtxo.recoverable);
|
|
2817
2995
|
if (swept.length > 0) {
|
|
2818
2996
|
throw new LockupNeedsRecoveryError(
|
|
2819
2997
|
swept.map((vtxo) => `${vtxo.txid}:${vtxo.vout}`),
|
|
2820
|
-
input.
|
|
2998
|
+
input.contract.options.refundLocktime
|
|
2821
2999
|
);
|
|
2822
3000
|
}
|
|
2823
|
-
const refundPkScript = input.refundPkScript ?? input.
|
|
3001
|
+
const refundPkScript = input.refundPkScript ?? input.contract.options.nonInteractiveParameters?.senderPkScript;
|
|
2824
3002
|
if (!refundPkScript) {
|
|
2825
3003
|
throw new Error(
|
|
2826
|
-
"no refund destination: the contract carries no
|
|
3004
|
+
"no refund destination: the contract carries no emulator covenant suite, so pass refundPkScript explicitly"
|
|
2827
3005
|
);
|
|
2828
3006
|
}
|
|
2829
|
-
const info = await
|
|
2830
|
-
let
|
|
3007
|
+
const info = await operator.getInfo();
|
|
3008
|
+
let operatorUnrollScript;
|
|
2831
3009
|
try {
|
|
2832
|
-
|
|
3010
|
+
operatorUnrollScript = import_sdk11.CSVMultisigTapscript.decode(import_base11.hex.decode(info.checkpointTapscript));
|
|
2833
3011
|
} catch {
|
|
2834
|
-
throw new Error("invalid checkpointTapscript from the
|
|
3012
|
+
throw new Error("invalid checkpointTapscript from the operator");
|
|
2835
3013
|
}
|
|
2836
|
-
const leaf = input.
|
|
2837
|
-
const tapTree = input.
|
|
3014
|
+
const leaf = input.contract.refundWithoutReceiver();
|
|
3015
|
+
const tapTree = input.contract.encode();
|
|
2838
3016
|
const amount = input.vtxos.reduce((sum, vtxo) => sum + vtxo.value, 0);
|
|
2839
|
-
const { arkTx, checkpoints } = (0, import_sdk11.buildOffchainTx)(
|
|
3017
|
+
const { arkTx: tx, checkpoints } = (0, import_sdk11.buildOffchainTx)(
|
|
2840
3018
|
input.vtxos.map((vtxo) => ({
|
|
2841
3019
|
txid: vtxo.txid,
|
|
2842
3020
|
vout: vtxo.vout,
|
|
@@ -2845,14 +3023,14 @@ async function pushRefundWithoutReceiver(ark, input) {
|
|
|
2845
3023
|
tapTree
|
|
2846
3024
|
})),
|
|
2847
3025
|
[{ script: refundPkScript, amount: BigInt(amount) }],
|
|
2848
|
-
|
|
3026
|
+
operatorUnrollScript
|
|
2849
3027
|
);
|
|
2850
|
-
const
|
|
2851
|
-
const submitted = await
|
|
2852
|
-
import_base11.base64.encode(
|
|
3028
|
+
const signedTx = await input.sender.sign(tx);
|
|
3029
|
+
const submitted = await operator.submitTx(
|
|
3030
|
+
import_base11.base64.encode(signedTx.toPSBT()),
|
|
2853
3031
|
checkpoints.map((c) => import_base11.base64.encode(c.toPSBT()))
|
|
2854
3032
|
);
|
|
2855
|
-
(0, import_sdk11.assertSubmittedArkTxid)(submitted,
|
|
3033
|
+
(0, import_sdk11.assertSubmittedArkTxid)(submitted, signedTx, "refundWithoutReceiver");
|
|
2856
3034
|
const matched = (0, import_sdk11.matchServerCheckpoints)(
|
|
2857
3035
|
submitted.signedCheckpointTxs,
|
|
2858
3036
|
checkpoints,
|
|
@@ -2863,11 +3041,11 @@ async function pushRefundWithoutReceiver(ark, input) {
|
|
|
2863
3041
|
async ({ server }) => import_base11.base64.encode((await input.sender.sign(server, [0])).toPSBT())
|
|
2864
3042
|
)
|
|
2865
3043
|
);
|
|
2866
|
-
await
|
|
2867
|
-
return {
|
|
3044
|
+
await operator.finalizeTx(submitted.arkTxid, finalCheckpoints);
|
|
3045
|
+
return { txid: submitted.arkTxid, amount };
|
|
2868
3046
|
}
|
|
2869
3047
|
var REFUND_MTP_LAG_SECONDS = 2 * 60 * 60;
|
|
2870
|
-
async function refundIfUnresolved(transport,
|
|
3048
|
+
async function refundIfUnresolved(transport, operator, indexer, input) {
|
|
2871
3049
|
const pollMs = input.pollMs ?? 5e3;
|
|
2872
3050
|
const now = input.now ?? (() => Math.floor(Date.now() / 1e3));
|
|
2873
3051
|
const attemptDeadline = input.attemptDeadline ?? input.refundLocktime + REFUND_MTP_LAG_SECONDS;
|
|
@@ -2875,11 +3053,26 @@ async function refundIfUnresolved(transport, ark, indexer, input) {
|
|
|
2875
3053
|
const status = await transport.status(input.rfqId);
|
|
2876
3054
|
if (status && isResolved(status.state)) return { outcome: "resolved", status };
|
|
2877
3055
|
if (now() >= input.refundLocktime) {
|
|
2878
|
-
|
|
3056
|
+
let fate = { fate: "unknown" };
|
|
3057
|
+
try {
|
|
3058
|
+
fate = await readLockupFate(indexer, {
|
|
3059
|
+
swapPkScript: input.contract.pkScript,
|
|
3060
|
+
paymentHash: input.paymentHash
|
|
3061
|
+
});
|
|
3062
|
+
} catch {
|
|
3063
|
+
}
|
|
3064
|
+
if (fate.fate === "exited") {
|
|
3065
|
+
return {
|
|
3066
|
+
outcome: "exited",
|
|
3067
|
+
outpoints: fate.outpoints.map((o) => `${o.txid}:${o.vout}`),
|
|
3068
|
+
status
|
|
3069
|
+
};
|
|
3070
|
+
}
|
|
3071
|
+
const vtxos = await findLockupVtxos(indexer, input.contract.pkScript);
|
|
2879
3072
|
if (vtxos.length === 0) return { outcome: "nothing_to_refund", status };
|
|
2880
3073
|
try {
|
|
2881
|
-
const pushed = await pushRefundWithoutReceiver(
|
|
2882
|
-
|
|
3074
|
+
const pushed = await pushRefundWithoutReceiver(operator, {
|
|
3075
|
+
contract: input.contract,
|
|
2883
3076
|
sender: input.sender,
|
|
2884
3077
|
vtxos,
|
|
2885
3078
|
refundPkScript: input.refundPkScript
|
|
@@ -2922,13 +3115,13 @@ var assertFiniteAmount = (value, reason, label) => {
|
|
|
2922
3115
|
error.reason = reason;
|
|
2923
3116
|
throw error;
|
|
2924
3117
|
};
|
|
2925
|
-
async function pushClaim(
|
|
3118
|
+
async function pushClaim(operator, input) {
|
|
2926
3119
|
if (input.vtxos.length === 0) throw new Error("nothing to claim: no funded outputs");
|
|
2927
3120
|
const swept = input.vtxos.filter((vtxo) => vtxo.recoverable);
|
|
2928
3121
|
if (swept.length > 0) {
|
|
2929
3122
|
throw new LockupNeedsRecoveryError(
|
|
2930
3123
|
swept.map((vtxo) => `${vtxo.txid}:${vtxo.vout}`),
|
|
2931
|
-
input.
|
|
3124
|
+
input.contract.options.refundLocktime
|
|
2932
3125
|
);
|
|
2933
3126
|
}
|
|
2934
3127
|
const locked = input.vtxos.reduce((sum, vtxo) => sum + vtxo.value, 0);
|
|
@@ -2939,22 +3132,22 @@ async function pushClaim(ark, input) {
|
|
|
2939
3132
|
throw new LockupAmountMismatchError(input.expectedAmount, locked);
|
|
2940
3133
|
}
|
|
2941
3134
|
}
|
|
2942
|
-
const committed = input.
|
|
3135
|
+
const committed = input.contract.options.preimageHash;
|
|
2943
3136
|
if (import_base12.hex.encode((0, import_legacy3.ripemd160)((0, import_sha25.sha256)(input.preimage))) !== import_base12.hex.encode(committed)) {
|
|
2944
3137
|
throw new Error("preimage does not match the covenant's payment hash");
|
|
2945
3138
|
}
|
|
2946
|
-
const info = await
|
|
2947
|
-
let
|
|
3139
|
+
const info = await operator.getInfo();
|
|
3140
|
+
let operatorUnrollScript;
|
|
2948
3141
|
try {
|
|
2949
|
-
|
|
3142
|
+
operatorUnrollScript = import_sdk12.CSVMultisigTapscript.decode(import_base12.hex.decode(info.checkpointTapscript));
|
|
2950
3143
|
} catch {
|
|
2951
|
-
throw new Error("invalid checkpointTapscript from the
|
|
3144
|
+
throw new Error("invalid checkpointTapscript from the operator");
|
|
2952
3145
|
}
|
|
2953
|
-
const leaf = input.
|
|
2954
|
-
const tapTree = input.
|
|
2955
|
-
const
|
|
3146
|
+
const leaf = input.contract.claim();
|
|
3147
|
+
const tapTree = input.contract.encode();
|
|
3148
|
+
const txid = await (0, import_sdk12.signAndSubmitOffchainTx)({
|
|
2956
3149
|
identity: (0, import_sdk12.claimWithPreimageIdentity)(input.receiver, input.preimage),
|
|
2957
|
-
provider:
|
|
3150
|
+
provider: operator,
|
|
2958
3151
|
inputs: input.vtxos.map((vtxo) => ({
|
|
2959
3152
|
txid: vtxo.txid,
|
|
2960
3153
|
vout: vtxo.vout,
|
|
@@ -2965,10 +3158,10 @@ async function pushClaim(ark, input) {
|
|
|
2965
3158
|
// One aggregate output: unlike the covenant refund, this leaf inspects
|
|
2966
3159
|
// nothing about the output set.
|
|
2967
3160
|
outputs: [{ script: input.destinationPkScript, amount: BigInt(locked) }],
|
|
2968
|
-
serverUnrollScript,
|
|
2969
|
-
verifyServerSignatures: { serverPubkey: input.
|
|
3161
|
+
serverUnrollScript: operatorUnrollScript,
|
|
3162
|
+
verifyServerSignatures: { serverPubkey: input.contract.options.server }
|
|
2970
3163
|
});
|
|
2971
|
-
return {
|
|
3164
|
+
return { txid, amount: locked };
|
|
2972
3165
|
}
|
|
2973
3166
|
async function awaitLockupFunding(indexer, swapPkScript, options = {}) {
|
|
2974
3167
|
const pollMs = options.pollMs ?? 5e3;
|
|
@@ -2983,13 +3176,13 @@ async function awaitLockupFunding(indexer, swapPkScript, options = {}) {
|
|
|
2983
3176
|
await sleep3(pollMs);
|
|
2984
3177
|
}
|
|
2985
3178
|
}
|
|
2986
|
-
async function claimReceiveLockup(indexer,
|
|
3179
|
+
async function claimReceiveLockup(indexer, operator, input) {
|
|
2987
3180
|
const vtxos = await awaitLockupFunding(indexer, input.swapPkScript, {
|
|
2988
3181
|
pollMs: input.pollMs,
|
|
2989
3182
|
deadline: input.deadline
|
|
2990
3183
|
});
|
|
2991
|
-
return pushClaim(
|
|
2992
|
-
|
|
3184
|
+
return pushClaim(operator, {
|
|
3185
|
+
contract: input.contract,
|
|
2993
3186
|
receiver: input.receiver,
|
|
2994
3187
|
preimage: input.preimage,
|
|
2995
3188
|
vtxos,
|
|
@@ -3040,8 +3233,8 @@ async function senderIdentityForSwapRecord(wallet, record) {
|
|
|
3040
3233
|
// src/arkadeRefunder.ts
|
|
3041
3234
|
function arkadeRefunder(deps) {
|
|
3042
3235
|
return async (swap) => {
|
|
3043
|
-
const
|
|
3044
|
-
if (!
|
|
3236
|
+
const contract = swap.lockup?.script;
|
|
3237
|
+
if (!contract) {
|
|
3045
3238
|
throw new Error(
|
|
3046
3239
|
`swap ${swap.rfqId} carries no lockup covenant, so its refund cannot be built`
|
|
3047
3240
|
);
|
|
@@ -3056,7 +3249,7 @@ function arkadeRefunder(deps) {
|
|
|
3056
3249
|
);
|
|
3057
3250
|
}
|
|
3058
3251
|
const sender = await senderIdentityForSwapRecord(deps.wallet, rfqSignerOf(record) ?? {});
|
|
3059
|
-
return pushRefundWithoutReceiver(deps.
|
|
3252
|
+
return pushRefundWithoutReceiver(deps.operator, { contract, sender, vtxos });
|
|
3060
3253
|
};
|
|
3061
3254
|
}
|
|
3062
3255
|
|
|
@@ -3452,6 +3645,19 @@ var RfqSwapManager = class {
|
|
|
3452
3645
|
async getPendingSwaps() {
|
|
3453
3646
|
return [...this.monitored.values()];
|
|
3454
3647
|
}
|
|
3648
|
+
/**
|
|
3649
|
+
* Every swap this manager holds — {@link getPendingSwaps} plus the ones
|
|
3650
|
+
* that already ended. The two sets are disjoint: a swap leaves `monitored`
|
|
3651
|
+
* as it enters `finished`.
|
|
3652
|
+
*
|
|
3653
|
+
* The finished half is what this process has seen, which after
|
|
3654
|
+
* {@link restoreFromRepository} is the stored history minus what retention
|
|
3655
|
+
* pruned. A manager that has restored nothing answers with the live swaps
|
|
3656
|
+
* alone.
|
|
3657
|
+
*/
|
|
3658
|
+
async getAllSwaps() {
|
|
3659
|
+
return [...this.monitored.values(), ...this.finished.values()];
|
|
3660
|
+
}
|
|
3455
3661
|
async hasSwap(rfqId) {
|
|
3456
3662
|
return this.monitored.has(rfqId);
|
|
3457
3663
|
}
|
|
@@ -3730,14 +3936,14 @@ var RfqSwapManager = class {
|
|
|
3730
3936
|
return this.claimIfFunded(swap, vtxos);
|
|
3731
3937
|
}
|
|
3732
3938
|
if (now < swap.refundLocktime + REFUND_MTP_LAG_SECONDS) {
|
|
3733
|
-
if (swap.
|
|
3939
|
+
if (swap.claimTxid) return;
|
|
3734
3940
|
return this.block(
|
|
3735
3941
|
swap,
|
|
3736
3942
|
"the claim window closed with the lockup unclaimed \u2014 only the solver can act now"
|
|
3737
3943
|
);
|
|
3738
3944
|
}
|
|
3739
3945
|
const failure = this.lastClaimError.get(swap.rfqId);
|
|
3740
|
-
if (failure && !swap.
|
|
3946
|
+
if (failure && !swap.claimTxid) {
|
|
3741
3947
|
return this.fail(swap, new Error(failure));
|
|
3742
3948
|
}
|
|
3743
3949
|
this.setState(swap, "refunded");
|
|
@@ -3752,7 +3958,7 @@ var RfqSwapManager = class {
|
|
|
3752
3958
|
*/
|
|
3753
3959
|
async claimIfFunded(swap, vtxos) {
|
|
3754
3960
|
if (vtxos.length === 0) return this.unblock(swap);
|
|
3755
|
-
const partiallyClaimed = swap.
|
|
3961
|
+
const partiallyClaimed = swap.claimTxid !== void 0;
|
|
3756
3962
|
if (partiallyClaimed && !this.hasUnclaimedOutpoint(swap.rfqId, vtxos)) {
|
|
3757
3963
|
return;
|
|
3758
3964
|
}
|
|
@@ -3780,10 +3986,10 @@ var RfqSwapManager = class {
|
|
|
3780
3986
|
this.setState(swap, "claimable");
|
|
3781
3987
|
if (!this.config.enableAutoActions) return;
|
|
3782
3988
|
try {
|
|
3783
|
-
const {
|
|
3989
|
+
const { txid } = await this.callbacks.claimLockup(swap, vtxos, { partiallyClaimed });
|
|
3784
3990
|
this.lastClaimError.delete(swap.rfqId);
|
|
3785
3991
|
this.rememberClaimed(swap.rfqId, vtxos);
|
|
3786
|
-
swap.
|
|
3992
|
+
swap.claimTxid = txid;
|
|
3787
3993
|
this.touch(swap);
|
|
3788
3994
|
this.setState(swap, "claimed");
|
|
3789
3995
|
this.emitAction(swap, "claimLockup");
|
|
@@ -3875,7 +4081,7 @@ var RfqSwapManager = class {
|
|
|
3875
4081
|
try {
|
|
3876
4082
|
const pushed = await this.callbacks.refundArkade(swap);
|
|
3877
4083
|
if (pushed) {
|
|
3878
|
-
swap.
|
|
4084
|
+
swap.refundTxid = pushed.txid;
|
|
3879
4085
|
this.touch(swap);
|
|
3880
4086
|
}
|
|
3881
4087
|
this.setState(swap, "refunded");
|
|
@@ -3948,7 +4154,7 @@ var RfqSwapManager = class {
|
|
|
3948
4154
|
/**
|
|
3949
4155
|
* Record which ark transactions ended the lockup.
|
|
3950
4156
|
*
|
|
3951
|
-
* Only the ones the indexer actually named: `LockupSpend.
|
|
4157
|
+
* Only the ones the indexer actually named: `LockupSpend.txid` is
|
|
3952
4158
|
* optional, and a checkpoint txid is not what history correlates on — a
|
|
3953
4159
|
* record carrying one would name a transaction the wallet's own activity
|
|
3954
4160
|
* never shows. Fewer txids is the right failure here.
|
|
@@ -3958,9 +4164,9 @@ var RfqSwapManager = class {
|
|
|
3958
4164
|
* verdict once.
|
|
3959
4165
|
*/
|
|
3960
4166
|
stampLockupSpends(swap, spends) {
|
|
3961
|
-
const
|
|
3962
|
-
if (
|
|
3963
|
-
swap.
|
|
4167
|
+
const txids = spends.map((spend) => spend.txid).filter((txid) => txid !== void 0);
|
|
4168
|
+
if (txids.length === 0) return;
|
|
4169
|
+
swap.lockupSpendTxids = txids;
|
|
3964
4170
|
this.touch(swap);
|
|
3965
4171
|
}
|
|
3966
4172
|
touch(swap) {
|
|
@@ -4041,10 +4247,9 @@ var RfqSwapManager = class {
|
|
|
4041
4247
|
/**
|
|
4042
4248
|
* Drop a terminal swap from monitoring and report it exactly once.
|
|
4043
4249
|
*
|
|
4044
|
-
* `onSwapCompleted` and `onSwapFailed` are mutually exclusive here
|
|
4045
|
-
*
|
|
4046
|
-
*
|
|
4047
|
-
* also fires on failure is a trap worth not inheriting.
|
|
4250
|
+
* `onSwapCompleted` and `onSwapFailed` are mutually exclusive here: a
|
|
4251
|
+
* listener named "completed" that also fires on failure is a trap, so a
|
|
4252
|
+
* swap that leaves monitoring reports through exactly one of them.
|
|
4048
4253
|
*/
|
|
4049
4254
|
finalize(swap) {
|
|
4050
4255
|
if (!this.monitored.has(swap.rfqId)) return;
|
|
@@ -4080,7 +4285,7 @@ var traderClaimTxid = (swap) => {
|
|
|
4080
4285
|
case "onchain_send":
|
|
4081
4286
|
return swap.claimTxid;
|
|
4082
4287
|
case "lightning_receive":
|
|
4083
|
-
return swap.
|
|
4288
|
+
return swap.claimTxid;
|
|
4084
4289
|
default:
|
|
4085
4290
|
return void 0;
|
|
4086
4291
|
}
|
|
@@ -4090,7 +4295,7 @@ var outcomeOf = (swap) => {
|
|
|
4090
4295
|
const lostReceive = swap.kind === "lightning_receive" && swap.state === "refunded";
|
|
4091
4296
|
return {
|
|
4092
4297
|
state: swap.state,
|
|
4093
|
-
txid: lostReceive ? swap.
|
|
4298
|
+
txid: lostReceive ? swap.refundTxid : traderClaimTxid(swap) ?? swap.refundTxid
|
|
4094
4299
|
};
|
|
4095
4300
|
};
|
|
4096
4301
|
var errorMessage = (error) => error instanceof Error ? error.message : String(error);
|
|
@@ -4155,16 +4360,17 @@ async function rfqSwapActivityInputs(deps) {
|
|
|
4155
4360
|
const records = await deps.repository.getAllRfqSwaps();
|
|
4156
4361
|
return Promise.all(records.map((record) => activityInputOf(record, deps.indexer)));
|
|
4157
4362
|
}
|
|
4158
|
-
async function activityInputOf(
|
|
4363
|
+
async function activityInputOf(stored, indexer) {
|
|
4364
|
+
const record = normalizeRfqSwapRecord(stored);
|
|
4159
4365
|
const txids = /* @__PURE__ */ new Set();
|
|
4160
|
-
if (record.
|
|
4161
|
-
if (record.
|
|
4366
|
+
if (record.fundingTxid) txids.add(record.fundingTxid);
|
|
4367
|
+
if (record.refundTxid) txids.add(record.refundTxid);
|
|
4162
4368
|
const handler = rfqCorridorHandlers.getOrThrow(record.kind);
|
|
4163
4369
|
for (const txid of handler.activityTxids?.(record.profile) ?? []) txids.add(txid);
|
|
4164
|
-
for (const txid of record.
|
|
4165
|
-
const spendUnknown = isRfqSwapTerminal(record.state) && !record.
|
|
4166
|
-
if (indexer && (!record.
|
|
4167
|
-
for (const txid of await lockupTxids(indexer, record, !record.
|
|
4370
|
+
for (const txid of record.lockupSpendTxids ?? []) txids.add(txid);
|
|
4371
|
+
const spendUnknown = isRfqSwapTerminal(record.state) && !record.refundTxid && !record.lockupSpendTxids?.length;
|
|
4372
|
+
if (indexer && (!record.fundingTxid || spendUnknown)) {
|
|
4373
|
+
for (const txid of await lockupTxids(indexer, record, !record.fundingTxid)) {
|
|
4168
4374
|
txids.add(txid);
|
|
4169
4375
|
}
|
|
4170
4376
|
}
|
|
@@ -4189,6 +4395,334 @@ async function lockupTxids(indexer, record, wantFunding) {
|
|
|
4189
4395
|
return [];
|
|
4190
4396
|
}
|
|
4191
4397
|
}
|
|
4398
|
+
|
|
4399
|
+
// src/swapClient.ts
|
|
4400
|
+
var import_base15 = require("@scure/base");
|
|
4401
|
+
var import_sdk15 = require("@arkade-os/sdk");
|
|
4402
|
+
var import_solver_discovery2 = require("@arkade-os/solver-discovery");
|
|
4403
|
+
var sideCorridorsOf = (market) => {
|
|
4404
|
+
const read = (value) => value === "lightning" || value === "onchain" ? value : "arkade";
|
|
4405
|
+
const m = market;
|
|
4406
|
+
return { base: read(m.base_corridor), quote: read(m.quote_corridor) };
|
|
4407
|
+
};
|
|
4408
|
+
var resolveKind = (market, give) => {
|
|
4409
|
+
const corridors = sideCorridorsOf(market);
|
|
4410
|
+
const giveCorridor = corridors[give];
|
|
4411
|
+
const receiveCorridor = corridors[give === "base" ? "quote" : "base"];
|
|
4412
|
+
if (giveCorridor === "arkade" && receiveCorridor === "arkade") return "spot";
|
|
4413
|
+
if (receiveCorridor === "lightning") return "ln_send";
|
|
4414
|
+
if (giveCorridor === "lightning") return "ln_receive";
|
|
4415
|
+
if (receiveCorridor === "onchain") return "onchain_send";
|
|
4416
|
+
return "onchain_receive";
|
|
4417
|
+
};
|
|
4418
|
+
var need = (value, what, leg) => {
|
|
4419
|
+
if (value === void 0) throw new Error(`a ${leg} quote needs ${what}`);
|
|
4420
|
+
return value;
|
|
4421
|
+
};
|
|
4422
|
+
var corridorAmount = (input, leg) => {
|
|
4423
|
+
const raw = need(input.amount, "an amount", leg);
|
|
4424
|
+
const amountOn = need(input.amountOn, "amountOn ('give' or 'receive')", leg);
|
|
4425
|
+
const amount = Number(raw);
|
|
4426
|
+
if (!Number.isSafeInteger(amount) || amount <= 0) {
|
|
4427
|
+
throw new Error(`a ${leg} amount must be a positive integer of sats, got ${String(raw)}`);
|
|
4428
|
+
}
|
|
4429
|
+
return { amount, amountSide: amountOn === "give" ? "from" : "to" };
|
|
4430
|
+
};
|
|
4431
|
+
function createSwapClient(deps) {
|
|
4432
|
+
const { wallet, repository, transportFor } = deps;
|
|
4433
|
+
let broadcaster;
|
|
4434
|
+
const broadcasting = () => broadcaster ??= wallet.getArkadeBroadcaster();
|
|
4435
|
+
const ark = deps.ark ?? {
|
|
4436
|
+
getInfo: () => wallet.getArkadeInfo({ requireLive: true }),
|
|
4437
|
+
submitTx: async (...args) => (await broadcasting()).submitTx(...args),
|
|
4438
|
+
finalizeTx: async (...args) => (await broadcasting()).finalizeTx(...args)
|
|
4439
|
+
};
|
|
4440
|
+
let reader;
|
|
4441
|
+
const reading = () => reader ??= wallet.getArkadeReader();
|
|
4442
|
+
const indexer = deps.indexer ?? {
|
|
4443
|
+
getVtxos: async (opts) => {
|
|
4444
|
+
if (!opts)
|
|
4445
|
+
throw new Error("getVtxos on the swap indexer requires scripts or outpoints");
|
|
4446
|
+
return (await reading()).getVtxos(opts);
|
|
4447
|
+
},
|
|
4448
|
+
getVirtualTxs: async (...args) => (await reading()).getVirtualTxs(...args)
|
|
4449
|
+
};
|
|
4450
|
+
const managerDeps = { indexer, chain: deps.chain, repository };
|
|
4451
|
+
const manager = new RfqSwapManager(managerDeps);
|
|
4452
|
+
const listeners = /* @__PURE__ */ new Set();
|
|
4453
|
+
const notify2 = (swap) => {
|
|
4454
|
+
for (const listener of listeners) {
|
|
4455
|
+
try {
|
|
4456
|
+
listener(swap);
|
|
4457
|
+
} catch {
|
|
4458
|
+
}
|
|
4459
|
+
}
|
|
4460
|
+
};
|
|
4461
|
+
manager.onSwapUpdate((swap) => notify2({ family: "rfq", swap }));
|
|
4462
|
+
const claimLockup = async (swap, vtxos, { partiallyClaimed }) => {
|
|
4463
|
+
const record = await repository.getRfqSwap(swap.rfqId);
|
|
4464
|
+
if (!record) throw new Error(`rfq swap ${swap.rfqId} has no stored record to claim from`);
|
|
4465
|
+
const secret = rfqClaimSecretOf(record);
|
|
4466
|
+
if (!secret) throw new Error(`rfq swap ${swap.rfqId} carries no claim secret`);
|
|
4467
|
+
const script = swap.lockup?.script;
|
|
4468
|
+
if (!script) throw new Error(`rfq swap ${swap.rfqId} carries no lockup covenant`);
|
|
4469
|
+
const payoutAddress = record.profile.payoutAddress;
|
|
4470
|
+
if (!payoutAddress) throw new Error(`rfq swap ${swap.rfqId} carries no payoutAddress`);
|
|
4471
|
+
return pushClaim(ark, {
|
|
4472
|
+
contract: script,
|
|
4473
|
+
receiver: await (0, import_sdk15.contractSigner)(wallet, secret.signingDescriptor),
|
|
4474
|
+
preimage: await preimageForSwapRecord(wallet, secret),
|
|
4475
|
+
vtxos,
|
|
4476
|
+
destinationPkScript: import_sdk15.ArkAddress.decode(payoutAddress).pkScript,
|
|
4477
|
+
expectedAmount: swap.expectedAmount,
|
|
4478
|
+
partiallyClaimed
|
|
4479
|
+
});
|
|
4480
|
+
};
|
|
4481
|
+
manager.setCallbacks({
|
|
4482
|
+
refundArkade: arkadeRefunder({ operator: ark, indexer, wallet, repository }),
|
|
4483
|
+
claimLockup,
|
|
4484
|
+
...deps.claimOnchain ? { claimOnchain: deps.claimOnchain } : {}
|
|
4485
|
+
});
|
|
4486
|
+
let watcher;
|
|
4487
|
+
const quote = async (market, input) => {
|
|
4488
|
+
const kind = resolveKind(market, input.give);
|
|
4489
|
+
switch (kind) {
|
|
4490
|
+
case "spot": {
|
|
4491
|
+
const raw = need(input.amount, "an amount", kind);
|
|
4492
|
+
const amountOn = need(input.amountOn, "amountOn ('give' or 'receive')", kind);
|
|
4493
|
+
const plan = await (0, import_solver_discovery2.quoteOffer)(market, {
|
|
4494
|
+
give: input.give,
|
|
4495
|
+
...amountOn === "give" ? { giveAmount: raw } : { wantAmount: raw },
|
|
4496
|
+
fetchImpl: deps.discovery.fetchImpl
|
|
4497
|
+
});
|
|
4498
|
+
return { kind, market, plan };
|
|
4499
|
+
}
|
|
4500
|
+
case "ln_send": {
|
|
4501
|
+
const invoice = need(input.invoice, "the invoice to pay", kind);
|
|
4502
|
+
const request = await requestLightningSend(wallet, transportFor(market), {
|
|
4503
|
+
invoice,
|
|
4504
|
+
emulatorPubkey: deps.emulatorPubkey
|
|
4505
|
+
});
|
|
4506
|
+
return { kind, market, request };
|
|
4507
|
+
}
|
|
4508
|
+
case "ln_receive": {
|
|
4509
|
+
const params = {
|
|
4510
|
+
...corridorAmount(input, kind),
|
|
4511
|
+
covclaimdPubkey: need(deps.covclaimdPubkey, "deps.covclaimdPubkey", kind),
|
|
4512
|
+
decodeInvoice: need(deps.decodeInvoice, "deps.decodeInvoice", kind),
|
|
4513
|
+
maxPayAmount: input.maxPayAmount,
|
|
4514
|
+
emulatorPubkey: deps.emulatorPubkey
|
|
4515
|
+
};
|
|
4516
|
+
const request = await requestLightningReceive(wallet, transportFor(market), params);
|
|
4517
|
+
return { kind, market, request, invoice: request.invoice };
|
|
4518
|
+
}
|
|
4519
|
+
case "onchain_send": {
|
|
4520
|
+
need(deps.chain, "deps.chain (L1 access)", kind);
|
|
4521
|
+
const params = {
|
|
4522
|
+
...corridorAmount(input, kind),
|
|
4523
|
+
payoutPubkey: need(input.payoutPubkey, "a payoutPubkey", kind),
|
|
4524
|
+
preimage: input.preimage,
|
|
4525
|
+
emulatorPubkey: deps.emulatorPubkey
|
|
4526
|
+
};
|
|
4527
|
+
const request = await requestOnchainSend(wallet, transportFor(market), params);
|
|
4528
|
+
return { kind, market, request };
|
|
4529
|
+
}
|
|
4530
|
+
case "onchain_receive":
|
|
4531
|
+
throw new Error(
|
|
4532
|
+
"onchain->arkade is not driven by RfqSwapManager yet; quote it directly with requestOnchainReceive"
|
|
4533
|
+
);
|
|
4534
|
+
}
|
|
4535
|
+
};
|
|
4536
|
+
const admit = async (swap, origin) => {
|
|
4537
|
+
let announced = false;
|
|
4538
|
+
const heard = manager.onSwapUpdate((updated) => {
|
|
4539
|
+
if (updated.rfqId === swap.rfqId) announced = true;
|
|
4540
|
+
});
|
|
4541
|
+
try {
|
|
4542
|
+
await manager.addSwap(swap, origin);
|
|
4543
|
+
} finally {
|
|
4544
|
+
heard();
|
|
4545
|
+
}
|
|
4546
|
+
const unified = { family: "rfq", swap };
|
|
4547
|
+
if (!announced) notify2(unified);
|
|
4548
|
+
return unified;
|
|
4549
|
+
};
|
|
4550
|
+
const fundPersisted = async (swap, origin, funding) => {
|
|
4551
|
+
await repository.saveRfqSwap(createRfqSwapRecord(origin, swap));
|
|
4552
|
+
return wallet.send({ address: funding.address, amount: funding.amount });
|
|
4553
|
+
};
|
|
4554
|
+
const accept = async (accepted) => {
|
|
4555
|
+
const now = Math.floor(Date.now() / 1e3);
|
|
4556
|
+
switch (accepted.kind) {
|
|
4557
|
+
case "spot": {
|
|
4558
|
+
const { plan } = accepted;
|
|
4559
|
+
const depositIsBtc = plan.deposit.asset.id === BTC_ASSET_ID;
|
|
4560
|
+
const offer = await createOffer(wallet, {
|
|
4561
|
+
wantAmount: plan.receive.atomic,
|
|
4562
|
+
...plan.receive.asset.id === BTC_ASSET_ID ? { offerAsset: import_sdk15.asset.AssetId.fromString(plan.deposit.asset.id) } : { wantAsset: import_sdk15.asset.AssetId.fromString(plan.receive.asset.id) },
|
|
4563
|
+
emulatorPubkey: deps.emulatorPubkey
|
|
4564
|
+
});
|
|
4565
|
+
const txid = await wallet.send({
|
|
4566
|
+
address: offer.address,
|
|
4567
|
+
// asset deposits ride the SDK's dust-sat carrier
|
|
4568
|
+
amount: depositIsBtc ? Number(plan.deposit.atomic) : void 0,
|
|
4569
|
+
assets: depositIsBtc ? void 0 : [{ assetId: plan.deposit.asset.id, amount: plan.deposit.atomic }],
|
|
4570
|
+
extensions: [offer.extension]
|
|
4571
|
+
});
|
|
4572
|
+
const swap = {
|
|
4573
|
+
id: txid,
|
|
4574
|
+
fromAsset: plan.deposit.asset.id,
|
|
4575
|
+
toAsset: plan.receive.asset.id,
|
|
4576
|
+
fromAmount: plan.deposit.atomic.toString(),
|
|
4577
|
+
toAmount: plan.receive.atomic.toString(),
|
|
4578
|
+
swapAddress: offer.address,
|
|
4579
|
+
swapPkScript: import_base15.hex.encode(offer.swapPkScript),
|
|
4580
|
+
offerHex: offer.offerHex,
|
|
4581
|
+
fundingTxid: txid,
|
|
4582
|
+
status: "pending",
|
|
4583
|
+
createdAt: Date.now()
|
|
4584
|
+
};
|
|
4585
|
+
await addAssetSwap(repository, swap);
|
|
4586
|
+
const unified = { family: "offer", swap };
|
|
4587
|
+
notify2(unified);
|
|
4588
|
+
return unified;
|
|
4589
|
+
}
|
|
4590
|
+
case "ln_send": {
|
|
4591
|
+
const { request } = accepted;
|
|
4592
|
+
const swap = {
|
|
4593
|
+
kind: "lightning_send",
|
|
4594
|
+
rfqId: request.rfqId,
|
|
4595
|
+
state: "pending",
|
|
4596
|
+
lockupPkScript: request.swapPkScript,
|
|
4597
|
+
lockup: { script: request.script, address: request.address },
|
|
4598
|
+
// the trader's own decode, never the solver's echo
|
|
4599
|
+
paymentHash: request.contractParams.paymentHash,
|
|
4600
|
+
refundLocktime: request.contractParams.refundLocktime,
|
|
4601
|
+
createdAt: now,
|
|
4602
|
+
updatedAt: now
|
|
4603
|
+
};
|
|
4604
|
+
const origin = {
|
|
4605
|
+
kind: "lightning_send",
|
|
4606
|
+
lockupAddress: request.address,
|
|
4607
|
+
profile: rfqSecretsProfile(request.secrets, swap.paymentHash),
|
|
4608
|
+
amount: request.fundAmount
|
|
4609
|
+
};
|
|
4610
|
+
const txid = await fundPersisted(swap, origin, {
|
|
4611
|
+
address: request.address,
|
|
4612
|
+
amount: request.fundAmount
|
|
4613
|
+
});
|
|
4614
|
+
return admit(swap, { ...origin, fundingTxid: txid });
|
|
4615
|
+
}
|
|
4616
|
+
case "ln_receive": {
|
|
4617
|
+
const { request } = accepted;
|
|
4618
|
+
const paymentHash = import_base15.hex.encode(request.secrets.paymentHash);
|
|
4619
|
+
const swap = {
|
|
4620
|
+
kind: "lightning_receive",
|
|
4621
|
+
rfqId: request.rfqId,
|
|
4622
|
+
state: "pending",
|
|
4623
|
+
lockupPkScript: request.swapPkScript,
|
|
4624
|
+
lockup: { script: request.script, address: request.address },
|
|
4625
|
+
paymentHash,
|
|
4626
|
+
refundLocktime: request.contractParams.refundLocktime,
|
|
4627
|
+
expectedAmount: request.expectedAmount,
|
|
4628
|
+
createdAt: now,
|
|
4629
|
+
updatedAt: now
|
|
4630
|
+
};
|
|
4631
|
+
return admit(swap, {
|
|
4632
|
+
kind: "lightning_receive",
|
|
4633
|
+
lockupAddress: request.address,
|
|
4634
|
+
profile: {
|
|
4635
|
+
...rfqSecretsProfile(request.secrets, paymentHash),
|
|
4636
|
+
expectedAmount: request.expectedAmount,
|
|
4637
|
+
payoutAddress: request.payoutAddress
|
|
4638
|
+
},
|
|
4639
|
+
amount: request.expectedAmount
|
|
4640
|
+
});
|
|
4641
|
+
}
|
|
4642
|
+
case "onchain_send": {
|
|
4643
|
+
const { request } = accepted;
|
|
4644
|
+
const paymentHash = import_base15.hex.encode(request.secrets.paymentHash);
|
|
4645
|
+
const swap = {
|
|
4646
|
+
kind: "onchain_send",
|
|
4647
|
+
rfqId: request.rfqId,
|
|
4648
|
+
state: "pending",
|
|
4649
|
+
lockupPkScript: request.swapPkScript,
|
|
4650
|
+
lockup: { script: request.script, address: request.address },
|
|
4651
|
+
paymentHash,
|
|
4652
|
+
refundLocktime: request.refundLocktime,
|
|
4653
|
+
htlc: request.htlc,
|
|
4654
|
+
minConfirmations: request.minConfirmations,
|
|
4655
|
+
createdAt: now,
|
|
4656
|
+
updatedAt: now
|
|
4657
|
+
};
|
|
4658
|
+
const origin = {
|
|
4659
|
+
kind: "onchain_send",
|
|
4660
|
+
lockupAddress: request.address,
|
|
4661
|
+
profile: {
|
|
4662
|
+
...rfqSecretsProfile(request.secrets, paymentHash),
|
|
4663
|
+
...onchainSendProfile(request)
|
|
4664
|
+
},
|
|
4665
|
+
amount: request.fundAmount
|
|
4666
|
+
};
|
|
4667
|
+
const txid = await fundPersisted(swap, origin, {
|
|
4668
|
+
address: request.address,
|
|
4669
|
+
amount: request.fundAmount
|
|
4670
|
+
});
|
|
4671
|
+
return admit(swap, { ...origin, fundingTxid: txid });
|
|
4672
|
+
}
|
|
4673
|
+
}
|
|
4674
|
+
};
|
|
4675
|
+
return {
|
|
4676
|
+
manager,
|
|
4677
|
+
markets: (useCache) => discoverMarkets({ ...deps.discovery, repository, useCache }),
|
|
4678
|
+
quote,
|
|
4679
|
+
accept,
|
|
4680
|
+
cancel: async (fundingTxid) => {
|
|
4681
|
+
const swaps = await getAssetSwapsOrThrow(repository);
|
|
4682
|
+
const swap = swaps.find((s) => s.id === fundingTxid);
|
|
4683
|
+
if (!swap) throw new Error(`no offer swap with funding txid ${fundingTxid}`);
|
|
4684
|
+
if (swap.status !== "pending") {
|
|
4685
|
+
throw new Error(`offer swap ${fundingTxid} is ${swap.status}, not cancellable`);
|
|
4686
|
+
}
|
|
4687
|
+
await updateAssetSwap(repository, fundingTxid, { status: "cancelling" });
|
|
4688
|
+
await cancelOffer(wallet, swap.offerHex, {
|
|
4689
|
+
repository,
|
|
4690
|
+
fundingTxid
|
|
4691
|
+
});
|
|
4692
|
+
},
|
|
4693
|
+
// Both families, live AND terminal — the offer half reads the whole
|
|
4694
|
+
// stored history, and the RFQ half is every swap the manager holds.
|
|
4695
|
+
// Its history is what `start()` restored plus what this session
|
|
4696
|
+
// accepted; records past `RFQ_SWAP_RETENTION_SECONDS` are pruned.
|
|
4697
|
+
swaps: async () => {
|
|
4698
|
+
const offers = await getAssetSwaps(repository);
|
|
4699
|
+
const rfq = await manager.getAllSwaps();
|
|
4700
|
+
return [
|
|
4701
|
+
...offers.map((swap) => ({ family: "offer", swap })),
|
|
4702
|
+
...rfq.map((swap) => ({ family: "rfq", swap }))
|
|
4703
|
+
];
|
|
4704
|
+
},
|
|
4705
|
+
onUpdate: (listener) => {
|
|
4706
|
+
listeners.add(listener);
|
|
4707
|
+
return () => listeners.delete(listener);
|
|
4708
|
+
},
|
|
4709
|
+
start: async () => {
|
|
4710
|
+
managerDeps.contracts ??= await wallet.getContractManager();
|
|
4711
|
+
await manager.restoreFromRepository();
|
|
4712
|
+
await manager.start();
|
|
4713
|
+
watcher ??= await watchOfferSwaps({
|
|
4714
|
+
wallet,
|
|
4715
|
+
repository,
|
|
4716
|
+
onUpdate: (swap) => notify2({ family: "offer", swap })
|
|
4717
|
+
});
|
|
4718
|
+
},
|
|
4719
|
+
stop: async () => {
|
|
4720
|
+
await manager.stop();
|
|
4721
|
+
watcher?.stop();
|
|
4722
|
+
watcher = void 0;
|
|
4723
|
+
}
|
|
4724
|
+
};
|
|
4725
|
+
}
|
|
4192
4726
|
// Annotate the CommonJS export names for ESM import in node:
|
|
4193
4727
|
0 && (module.exports = {
|
|
4194
4728
|
ARKADE_ASSET,
|
|
@@ -4250,6 +4784,7 @@ async function lockupTxids(indexer, record, wantFunding) {
|
|
|
4250
4784
|
classifySpend,
|
|
4251
4785
|
createOffer,
|
|
4252
4786
|
createRfqSwapRecord,
|
|
4787
|
+
createSwapClient,
|
|
4253
4788
|
decodeOffer,
|
|
4254
4789
|
deriveLightningReceive,
|
|
4255
4790
|
deriveOnchainReceive,
|
|
@@ -4264,16 +4799,18 @@ async function lockupTxids(indexer, record, wantFunding) {
|
|
|
4264
4799
|
httpTransport,
|
|
4265
4800
|
isRfqSwapTerminal,
|
|
4266
4801
|
isRfqTerminal,
|
|
4802
|
+
lightningReceiveContract,
|
|
4267
4803
|
lightningReceiveRequest,
|
|
4804
|
+
lightningSendContract,
|
|
4268
4805
|
lightningSendRequest,
|
|
4269
|
-
lightningSendVtxoScript,
|
|
4270
4806
|
lockupContractParams,
|
|
4271
4807
|
makeCachedFeedFetch,
|
|
4272
4808
|
newPreimage,
|
|
4273
4809
|
newRfqId,
|
|
4274
4810
|
nextOnchainAction,
|
|
4811
|
+
normalizeRfqSwapRecord,
|
|
4812
|
+
offerContract,
|
|
4275
4813
|
offerTermsFromQuote,
|
|
4276
|
-
offerVtxoScript,
|
|
4277
4814
|
onchainHtlcScript,
|
|
4278
4815
|
onchainReceiveRequest,
|
|
4279
4816
|
onchainSendProfile,
|
|
@@ -4284,7 +4821,6 @@ async function lockupTxids(indexer, record, wantFunding) {
|
|
|
4284
4821
|
pushRefundWithoutReceiver,
|
|
4285
4822
|
readLockupFate,
|
|
4286
4823
|
rebuildRfqSwap,
|
|
4287
|
-
receiveVtxoScript,
|
|
4288
4824
|
refundIfUnresolved,
|
|
4289
4825
|
registerLockupContract,
|
|
4290
4826
|
relayTransport,
|