@arkade-os/swap 0.0.9 → 0.0.11
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 +17 -0
- package/dist/{chunk-2FEMUOIH.js → chunk-RIC5ZTZK.js} +71 -33
- package/dist/index.cjs +235 -52
- package/dist/index.d.cts +61 -15
- package/dist/index.d.ts +61 -15
- package/dist/index.js +166 -20
- package/dist/nostr.d.cts +1 -1
- package/dist/nostr.d.ts +1 -1
- package/dist/nostr.js +1 -1
- package/dist/repositories/realm/index.d.cts +2 -2
- package/dist/repositories/realm/index.d.ts +2 -2
- package/dist/repositories/sqlite/index.d.cts +2 -2
- package/dist/repositories/sqlite/index.d.ts +2 -2
- package/dist/{repository-DEHLtD9l.d.cts → repository-B02vsgcV.d.cts} +106 -12
- package/dist/{repository-DIAr5XYk.d.ts → repository-CfE18Fif.d.ts} +106 -12
- package/dist/{rfq-hbzhTWHT.d.ts → rfq-DkckzRKK.d.cts} +54 -24
- package/dist/{rfq-hbzhTWHT.d.cts → rfq-DkckzRKK.d.ts} +54 -24
- package/package.json +4 -4
package/dist/index.cjs
CHANGED
|
@@ -411,12 +411,30 @@ var swapPrograms = {
|
|
|
411
411
|
wantAsset: import_sdk2.arkade.parseArtifact(swap_want_asset_program_default),
|
|
412
412
|
wantBtc: import_sdk2.arkade.parseArtifact(swap_want_btc_program_default)
|
|
413
413
|
};
|
|
414
|
+
function withExitClosure(program, exit) {
|
|
415
|
+
if (!exit) return program;
|
|
416
|
+
return {
|
|
417
|
+
...program,
|
|
418
|
+
// typed params are authoritative: an undeclared `$exitDelay` fails
|
|
419
|
+
// validateProgram instead of compiling against an unbound value
|
|
420
|
+
params: [...program.params ?? [], { name: "exitDelay", type: "int" }],
|
|
421
|
+
functions: {
|
|
422
|
+
...program.functions,
|
|
423
|
+
exit: {
|
|
424
|
+
tapscript: { signers: ["$user"], csv: { type: exit.type, value: "$exitDelay" } }
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
};
|
|
428
|
+
}
|
|
414
429
|
function swapProgramBinding(offer, serverPubkey) {
|
|
415
430
|
if (offer.makerPkScript.length !== FIELDS.makerPkScript.width) {
|
|
416
431
|
throw new Error("makerPkScript is not a 34-byte taproot scriptPubKey");
|
|
417
432
|
}
|
|
418
433
|
return {
|
|
419
|
-
program:
|
|
434
|
+
program: withExitClosure(
|
|
435
|
+
offer.wantAsset ? swapPrograms.wantAsset : swapPrograms.wantBtc,
|
|
436
|
+
offer.exitDelay
|
|
437
|
+
),
|
|
420
438
|
args: {
|
|
421
439
|
makerWP: offer.makerPkScript.subarray(2),
|
|
422
440
|
wantAmount: offer.wantAmount,
|
|
@@ -426,7 +444,8 @@ function swapProgramBinding(offer, serverPubkey) {
|
|
|
426
444
|
...offer.wantAsset && {
|
|
427
445
|
wantAssetTxid: offer.wantAsset.txid.slice().reverse(),
|
|
428
446
|
wantAssetGroupIndex: offer.wantAsset.groupIndex
|
|
429
|
-
}
|
|
447
|
+
},
|
|
448
|
+
...offer.exitDelay && { exitDelay: offer.exitDelay.value }
|
|
430
449
|
},
|
|
431
450
|
keys: {
|
|
432
451
|
serverKey: serverPubkey,
|
|
@@ -447,9 +466,27 @@ var FIELDS = {
|
|
|
447
466
|
makerPkScript: { tag: 5, width: 34 },
|
|
448
467
|
makerPublicKey: { tag: 7, width: 32 },
|
|
449
468
|
emulatorPubkey: { tag: 8, width: 32 },
|
|
450
|
-
|
|
469
|
+
ratioNum: { tag: 9, width: 8 },
|
|
470
|
+
ratioDen: { tag: 10, width: 8 },
|
|
471
|
+
offerAsset: { tag: 11, width: void 0 },
|
|
472
|
+
exitTimelock: { tag: 12, width: 9 }
|
|
451
473
|
};
|
|
474
|
+
var EXIT_TYPES = ["blocks", "seconds"];
|
|
452
475
|
var NAMES = Object.fromEntries(Object.entries(FIELDS).map(([k, f]) => [f.tag, k]));
|
|
476
|
+
function u64(name, value) {
|
|
477
|
+
if (value < BigInt(0) || value >> BigInt(64) > BigInt(0)) {
|
|
478
|
+
throw new Error(`${name} does not fit the offer wire format (u64)`);
|
|
479
|
+
}
|
|
480
|
+
const out = new Uint8Array(FIELDS.wantAmount.width);
|
|
481
|
+
new DataView(out.buffer).setBigUint64(0, value, false);
|
|
482
|
+
return out;
|
|
483
|
+
}
|
|
484
|
+
var readU64 = (value) => new DataView(value.buffer, value.byteOffset).getBigUint64(0, false);
|
|
485
|
+
var setRatio = (name, value) => {
|
|
486
|
+
if (value === void 0 || value === BigInt(0)) return void 0;
|
|
487
|
+
if (value < BigInt(0)) throw new Error(`${name} does not fit the offer wire format (u64)`);
|
|
488
|
+
return value;
|
|
489
|
+
};
|
|
453
490
|
function tlv(type, value) {
|
|
454
491
|
if (value.length > 65535) throw new Error("TLV value exceeds the u16 length field");
|
|
455
492
|
return (0, import_utils.concatBytes)(Uint8Array.of(type, value.length >> 8 & 255, value.length & 255), value);
|
|
@@ -468,24 +505,45 @@ function encodeOffer(offer) {
|
|
|
468
505
|
throw new Error(`${name} must be ${FIELDS[name].width} bytes`);
|
|
469
506
|
}
|
|
470
507
|
}
|
|
471
|
-
|
|
472
|
-
|
|
508
|
+
const ratioNum = setRatio("ratioNum", offer.ratioNum);
|
|
509
|
+
const ratioDen = setRatio("ratioDen", offer.ratioDen);
|
|
510
|
+
if (ratioNum === void 0 !== (ratioDen === void 0)) {
|
|
511
|
+
throw new Error("offer must carry both ratioNum and ratioDen, or neither");
|
|
473
512
|
}
|
|
474
|
-
const amount = new Uint8Array(FIELDS.wantAmount.width);
|
|
475
|
-
new DataView(amount.buffer).setBigUint64(0, offer.wantAmount, false);
|
|
476
513
|
const recs = [
|
|
477
514
|
tlv(FIELDS.swapPkScript.tag, offer.swapPkScript),
|
|
478
|
-
tlv(FIELDS.wantAmount.tag,
|
|
515
|
+
tlv(FIELDS.wantAmount.tag, u64("wantAmount", offer.wantAmount))
|
|
479
516
|
];
|
|
480
517
|
if (offer.wantAsset) recs.push(tlv(FIELDS.wantAsset.tag, offer.wantAsset.serialize()));
|
|
518
|
+
if (ratioNum !== void 0) recs.push(tlv(FIELDS.ratioNum.tag, u64("ratioNum", ratioNum)));
|
|
519
|
+
if (ratioDen !== void 0) recs.push(tlv(FIELDS.ratioDen.tag, u64("ratioDen", ratioDen)));
|
|
481
520
|
if (offer.offerAsset) recs.push(tlv(FIELDS.offerAsset.tag, offer.offerAsset.serialize()));
|
|
482
521
|
recs.push(
|
|
483
522
|
tlv(FIELDS.makerPkScript.tag, offer.makerPkScript),
|
|
484
523
|
tlv(FIELDS.makerPublicKey.tag, offer.makerPublicKey),
|
|
485
524
|
tlv(FIELDS.emulatorPubkey.tag, offer.emulatorPubkey)
|
|
486
525
|
);
|
|
526
|
+
if (offer.exitDelay) recs.push(tlv(FIELDS.exitTimelock.tag, encodeExitDelay(offer.exitDelay)));
|
|
487
527
|
return (0, import_utils.concatBytes)(...recs);
|
|
488
528
|
}
|
|
529
|
+
function encodeExitDelay(exit) {
|
|
530
|
+
return (0, import_utils.concatBytes)(
|
|
531
|
+
Uint8Array.of(EXIT_TYPES.indexOf(assertExitDelay(exit).type)),
|
|
532
|
+
u64("exitDelay", exit.value)
|
|
533
|
+
);
|
|
534
|
+
}
|
|
535
|
+
function assertExitDelay(exit) {
|
|
536
|
+
if (EXIT_TYPES.indexOf(exit.type) < 0) {
|
|
537
|
+
throw new Error(`unknown exitDelay locktime type: ${exit.type}`);
|
|
538
|
+
}
|
|
539
|
+
if (exit.value <= BigInt(0)) {
|
|
540
|
+
throw new Error("exitDelay must be a positive relative locktime");
|
|
541
|
+
}
|
|
542
|
+
if (exit.value >> BigInt(32) > BigInt(0)) {
|
|
543
|
+
throw new Error("exitDelay does not fit the locktime field (u32)");
|
|
544
|
+
}
|
|
545
|
+
return exit;
|
|
546
|
+
}
|
|
489
547
|
function decodeOffer(data) {
|
|
490
548
|
const fields = {};
|
|
491
549
|
let off = 0;
|
|
@@ -505,6 +563,12 @@ function decodeOffer(data) {
|
|
|
505
563
|
for (const name of ["wantAsset", "offerAsset"]) {
|
|
506
564
|
if (fields[name]?.length === 0) throw new Error(`missing/invalid ${name}`);
|
|
507
565
|
}
|
|
566
|
+
for (const [name, value] of Object.entries(fields)) {
|
|
567
|
+
const width = FIELDS[name].width;
|
|
568
|
+
if (width !== void 0 && value.length !== width) {
|
|
569
|
+
throw new Error(`missing/invalid ${name}`);
|
|
570
|
+
}
|
|
571
|
+
}
|
|
508
572
|
const need = (name) => {
|
|
509
573
|
const v = fields[name];
|
|
510
574
|
const len = FIELDS[name].width;
|
|
@@ -516,16 +580,36 @@ function decodeOffer(data) {
|
|
|
516
580
|
if (Boolean(fields.wantAsset) === Boolean(fields.offerAsset)) {
|
|
517
581
|
throw new Error("offer must carry exactly one of wantAsset or offerAsset");
|
|
518
582
|
}
|
|
583
|
+
const readRatio = (name) => {
|
|
584
|
+
const raw = fields[name];
|
|
585
|
+
if (!raw) return void 0;
|
|
586
|
+
const value = readU64(raw);
|
|
587
|
+
if (value === BigInt(0)) throw new Error(`missing/invalid ${name}`);
|
|
588
|
+
return value;
|
|
589
|
+
};
|
|
590
|
+
const ratioNum = readRatio("ratioNum");
|
|
591
|
+
const ratioDen = readRatio("ratioDen");
|
|
592
|
+
if (ratioNum === void 0 !== (ratioDen === void 0)) {
|
|
593
|
+
throw new Error("offer must carry both ratioNum and ratioDen, or neither");
|
|
594
|
+
}
|
|
519
595
|
return {
|
|
520
596
|
swapPkScript: need("swapPkScript"),
|
|
521
|
-
wantAmount:
|
|
597
|
+
wantAmount: readU64(amount),
|
|
522
598
|
...fields.wantAsset && { wantAsset: import_sdk2.asset.AssetId.fromBytes(fields.wantAsset) },
|
|
523
599
|
...fields.offerAsset && { offerAsset: import_sdk2.asset.AssetId.fromBytes(fields.offerAsset) },
|
|
524
600
|
makerPkScript: need("makerPkScript"),
|
|
525
601
|
makerPublicKey: need("makerPublicKey"),
|
|
526
|
-
emulatorPubkey: need("emulatorPubkey")
|
|
602
|
+
emulatorPubkey: need("emulatorPubkey"),
|
|
603
|
+
...ratioNum !== void 0 && { ratioNum },
|
|
604
|
+
...ratioDen !== void 0 && { ratioDen },
|
|
605
|
+
...fields.exitTimelock && { exitDelay: decodeExitDelay(fields.exitTimelock) }
|
|
527
606
|
};
|
|
528
607
|
}
|
|
608
|
+
function decodeExitDelay(value) {
|
|
609
|
+
const type = EXIT_TYPES[value[0]];
|
|
610
|
+
if (!type) throw new Error(`unknown exitDelay locktime type: 0x${value[0].toString(16)}`);
|
|
611
|
+
return { type, value: readU64(value.subarray(1)) };
|
|
612
|
+
}
|
|
529
613
|
var OFFER_CONTRACT_LABEL = "Arkade swap offer";
|
|
530
614
|
var OFFER_CONTRACT_KIND = "asset-swap-offer";
|
|
531
615
|
async function registerOfferContract(wallet, arkServerUrl, network, binding, serverPubkey, expectedPkScript) {
|
|
@@ -551,6 +635,14 @@ async function registerOfferContract(wallet, arkServerUrl, network, binding, ser
|
|
|
551
635
|
});
|
|
552
636
|
await promoteOfferContract(contractManager, import_base2.hex.encode(expectedPkScript));
|
|
553
637
|
}
|
|
638
|
+
function serverExitDelay(delay) {
|
|
639
|
+
if (typeof delay !== "bigint" || delay <= BigInt(0)) {
|
|
640
|
+
throw new Error(
|
|
641
|
+
"the server reports no usable unilateralExitDelay; pass `exitDelay` to set the offer's exit closure explicitly, or `noExit: true` to publish without one"
|
|
642
|
+
);
|
|
643
|
+
}
|
|
644
|
+
return assertExitDelay({ value: delay, type: delay < BigInt(512) ? "blocks" : "seconds" });
|
|
645
|
+
}
|
|
554
646
|
async function createOffer(wallet, arkServerUrl, params) {
|
|
555
647
|
if (Boolean(params.wantAsset) === Boolean(params.offerAsset)) {
|
|
556
648
|
throw new Error("set exactly one of wantAsset (BTC->asset) or offerAsset (asset->BTC)");
|
|
@@ -571,7 +663,11 @@ async function createOffer(wallet, arkServerUrl, params) {
|
|
|
571
663
|
offerAsset: params.offerAsset,
|
|
572
664
|
makerPkScript: import_sdk2.ArkAddress.decode(makerAddress).pkScript,
|
|
573
665
|
makerPublicKey,
|
|
574
|
-
emulatorPubkey: emuKey
|
|
666
|
+
emulatorPubkey: emuKey,
|
|
667
|
+
// checked HERE, before the covenant is derived and registered below:
|
|
668
|
+
// deferring it to `encodeOffer` leaves a registered contract behind for
|
|
669
|
+
// an offer that then fails to encode. @see assertExitDelay
|
|
670
|
+
exitDelay: params.noExit ? void 0 : params.exitDelay ? assertExitDelay(params.exitDelay) : serverExitDelay(info.unilateralExitDelay)
|
|
575
671
|
};
|
|
576
672
|
const script = offerVtxoScript(binding, serverPubKey);
|
|
577
673
|
const offer = { ...binding, swapPkScript: script.pkScript };
|
|
@@ -609,8 +705,8 @@ async function cancelOffer(wallet, arkServerUrl, offerHex, opts) {
|
|
|
609
705
|
// script and the payout script comes from wallet.getAddress(), so the
|
|
610
706
|
// client's network (which only shapes address derivation) is unused here
|
|
611
707
|
});
|
|
612
|
-
const
|
|
613
|
-
const { program, args, keys } = swapProgramBinding(offer,
|
|
708
|
+
const operatorPubkey = swapAddress ? import_sdk2.ArkAddress.decode(swapAddress).serverPubKey : client.serverKey;
|
|
709
|
+
const { program, args, keys } = swapProgramBinding(offer, operatorPubkey);
|
|
614
710
|
const rebuilt = new import_sdk2.arkade.ArkadeProgramScript(program, args, keys);
|
|
615
711
|
if (import_base2.hex.encode(rebuilt.pkScript) !== import_base2.hex.encode(offer.swapPkScript)) {
|
|
616
712
|
throw new Error(
|
|
@@ -1512,7 +1608,9 @@ function classifySpend(offer, serverPubkey, spendTx, deposit) {
|
|
|
1512
1608
|
const script = offerVtxoScript(offer, serverPubkey);
|
|
1513
1609
|
if (import_base6.hex.encode(script.pkScript) !== import_base6.hex.encode(offer.swapPkScript)) return "indeterminate";
|
|
1514
1610
|
leaves = {
|
|
1515
|
-
|
|
1611
|
+
// both routes that hand the deposit back; `exit` is absent on an
|
|
1612
|
+
// offer that carries no exit closure, and drops out here
|
|
1613
|
+
returned: ["cancel", "exit"].map((name) => script.functionByName(name)?.leafScript).filter((leaf) => leaf !== void 0),
|
|
1516
1614
|
fulfill: script.functionByName("fulfill")?.leafScript
|
|
1517
1615
|
};
|
|
1518
1616
|
} catch {
|
|
@@ -1524,7 +1622,7 @@ function classifySpend(offer, serverPubkey, spendTx, deposit) {
|
|
|
1524
1622
|
if (import_base6.hex.encode(input.txid) !== deposit.txid) continue;
|
|
1525
1623
|
for (const leaf of input.tapLeafScript ?? []) {
|
|
1526
1624
|
const spent = import_base6.hex.encode((0, import_sdk6.scriptFromTapLeafScript)(leaf));
|
|
1527
|
-
if (leaves.
|
|
1625
|
+
if (leaves.returned.some((back) => spent === import_base6.hex.encode(back))) return "cancelled";
|
|
1528
1626
|
if (leaves.fulfill && spent === import_base6.hex.encode(leaves.fulfill)) return "fulfilled";
|
|
1529
1627
|
}
|
|
1530
1628
|
}
|
|
@@ -1918,8 +2016,22 @@ var assertPairLength = (pair) => {
|
|
|
1918
2016
|
};
|
|
1919
2017
|
var verifyLockupAddress = (quote, derivedAddress) => {
|
|
1920
2018
|
const quoted = quote.profile?.lockup_address;
|
|
1921
|
-
|
|
1922
|
-
|
|
2019
|
+
const candidates = Array.isArray(derivedAddress) ? derivedAddress : [derivedAddress];
|
|
2020
|
+
const matched = candidates.find((address) => address === quoted);
|
|
2021
|
+
if (matched === void 0) throw new AddressMismatch(candidates, quoted);
|
|
2022
|
+
return matched;
|
|
2023
|
+
};
|
|
2024
|
+
var LOCKUP_SHAPE_VARIANTS = [void 0, "preTimelockedRefund"];
|
|
2025
|
+
var matchQuotedLockup = (quote, hrp, serverPubkey, build) => {
|
|
2026
|
+
const candidates = LOCKUP_SHAPE_VARIANTS.map((legacy) => {
|
|
2027
|
+
const script = build(legacy);
|
|
2028
|
+
return { script, address: script.address(hrp, serverPubkey).encode(), legacy };
|
|
2029
|
+
});
|
|
2030
|
+
const matchedAddress = verifyLockupAddress(
|
|
2031
|
+
quote,
|
|
2032
|
+
candidates.map((candidate) => candidate.address)
|
|
2033
|
+
);
|
|
2034
|
+
return candidates.find((candidate) => candidate.address === matchedAddress);
|
|
1923
2035
|
};
|
|
1924
2036
|
var assertFundable = (input) => {
|
|
1925
2037
|
const fail = (reason, message) => {
|
|
@@ -2121,13 +2233,11 @@ function lightningSendVtxoScript(params) {
|
|
|
2121
2233
|
unilateralRefundWithoutReceiverDelay: seconds(
|
|
2122
2234
|
unilateralRefundWithoutReceiverDelay(params.claimDelay)
|
|
2123
2235
|
),
|
|
2124
|
-
|
|
2236
|
+
nonInteractiveParameters: {
|
|
2125
2237
|
receiverPkScript: params.receiverPkScript,
|
|
2126
|
-
emulatorPubkey: params.emulatorPubkey
|
|
2127
|
-
},
|
|
2128
|
-
nonInteractiveRefund: {
|
|
2129
2238
|
senderPkScript: params.refundPkScript,
|
|
2130
|
-
emulatorPubkey: params.emulatorPubkey
|
|
2239
|
+
emulatorPubkey: params.emulatorPubkey,
|
|
2240
|
+
...params.legacy !== void 0 && { legacy: params.legacy }
|
|
2131
2241
|
}
|
|
2132
2242
|
});
|
|
2133
2243
|
}
|
|
@@ -2135,10 +2245,8 @@ async function requestLightningSend(wallet, arkServerUrl, transport, params) {
|
|
|
2135
2245
|
const rfqId = params.rfqId ?? newRfqId();
|
|
2136
2246
|
const secrets = await (0, import_sdk10.provisionRefundKey)(wallet);
|
|
2137
2247
|
const senderPubkey = secrets.pubkey;
|
|
2138
|
-
const
|
|
2139
|
-
|
|
2140
|
-
wallet.getAddress()
|
|
2141
|
-
]);
|
|
2248
|
+
const refundAddress = secrets.address;
|
|
2249
|
+
const info = await new import_sdk9.RestArkProvider(arkServerUrl).getInfo();
|
|
2142
2250
|
const quote = await transport.requestQuote(
|
|
2143
2251
|
lightningSendRequest({ rfqId, invoice: params.invoice.raw, refundAddress, senderPubkey })
|
|
2144
2252
|
);
|
|
@@ -2173,11 +2281,20 @@ async function requestLightningSend(wallet, arkServerUrl, transport, params) {
|
|
|
2173
2281
|
),
|
|
2174
2282
|
senderPubkey,
|
|
2175
2283
|
receiverPkScript: solverHex(receiverPkScriptHex, "profile.receiver_pk_script"),
|
|
2176
|
-
refundPkScript:
|
|
2284
|
+
refundPkScript: secrets.pkScript
|
|
2285
|
+
};
|
|
2286
|
+
const matched = matchQuotedLockup(
|
|
2287
|
+
quote,
|
|
2288
|
+
network.hrp,
|
|
2289
|
+
serverPubkey,
|
|
2290
|
+
(legacy) => lightningSendVtxoScript({ ...treeParams, ...legacy !== void 0 && { legacy } })
|
|
2291
|
+
);
|
|
2292
|
+
const script = matched.script;
|
|
2293
|
+
const address = matched.address;
|
|
2294
|
+
const matchedTreeParams = {
|
|
2295
|
+
...treeParams,
|
|
2296
|
+
...matched.legacy !== void 0 && { legacy: matched.legacy }
|
|
2177
2297
|
};
|
|
2178
|
-
const script = lightningSendVtxoScript(treeParams);
|
|
2179
|
-
const address = script.address(network.hrp, serverPubkey).encode();
|
|
2180
|
-
verifyLockupAddress(quote, address);
|
|
2181
2298
|
assertFundable({
|
|
2182
2299
|
quote,
|
|
2183
2300
|
invoiceExpiresAt: params.invoice.expiresAt,
|
|
@@ -2196,7 +2313,7 @@ async function requestLightningSend(wallet, arkServerUrl, transport, params) {
|
|
|
2196
2313
|
refundAddress,
|
|
2197
2314
|
senderPubkey,
|
|
2198
2315
|
secrets,
|
|
2199
|
-
treeParams
|
|
2316
|
+
treeParams: matchedTreeParams
|
|
2200
2317
|
};
|
|
2201
2318
|
}
|
|
2202
2319
|
var offerTermsFromQuote = (quote, assets) => {
|
|
@@ -2261,7 +2378,7 @@ function deriveOnchainSend(input) {
|
|
|
2261
2378
|
if (refundLocktime === void 0 || htlcPubkey === void 0 || htlcLocktime === void 0 || minConfirmations === void 0 || receiverPkScriptHex === void 0) {
|
|
2262
2379
|
throw new Error("onchain-send quote is missing a binding field");
|
|
2263
2380
|
}
|
|
2264
|
-
const
|
|
2381
|
+
const treeParams = {
|
|
2265
2382
|
solverPubkey: (0, import_sdk9.toXOnly)(import_base10.hex.decode(quote.solver_pubkey), "solver key"),
|
|
2266
2383
|
refundLocktime,
|
|
2267
2384
|
serverPubkey: input.serverPubkey,
|
|
@@ -2271,9 +2388,13 @@ function deriveOnchainSend(input) {
|
|
|
2271
2388
|
senderPubkey: input.senderPubkey,
|
|
2272
2389
|
receiverPkScript: solverHex(receiverPkScriptHex, "profile.receiver_pk_script"),
|
|
2273
2390
|
refundPkScript: import_sdk9.ArkAddress.decode(input.refundAddress).pkScript
|
|
2274
|
-
}
|
|
2275
|
-
const address =
|
|
2276
|
-
|
|
2391
|
+
};
|
|
2392
|
+
const { script, address } = matchQuotedLockup(
|
|
2393
|
+
quote,
|
|
2394
|
+
input.hrp,
|
|
2395
|
+
input.serverPubkey,
|
|
2396
|
+
(legacy) => lightningSendVtxoScript({ ...treeParams, ...legacy !== void 0 && { legacy } })
|
|
2397
|
+
);
|
|
2277
2398
|
const htlcParams = {
|
|
2278
2399
|
paymentHash: input.paymentHash,
|
|
2279
2400
|
claimKey: input.payoutPubkey,
|
|
@@ -2448,13 +2569,11 @@ function receiveVtxoScript(params) {
|
|
|
2448
2569
|
unilateralRefundWithoutReceiverDelay: seconds(
|
|
2449
2570
|
unilateralRefundWithoutReceiverDelay(params.claimDelay)
|
|
2450
2571
|
),
|
|
2451
|
-
|
|
2572
|
+
nonInteractiveParameters: {
|
|
2452
2573
|
receiverPkScript: params.payoutPkScript,
|
|
2453
|
-
emulatorPubkey: params.emulatorPubkey
|
|
2454
|
-
},
|
|
2455
|
-
nonInteractiveRefund: {
|
|
2456
2574
|
senderPkScript: params.solverRefundPkScript,
|
|
2457
|
-
emulatorPubkey: params.emulatorPubkey
|
|
2575
|
+
emulatorPubkey: params.emulatorPubkey,
|
|
2576
|
+
...params.legacy !== void 0 && { legacy: params.legacy }
|
|
2458
2577
|
}
|
|
2459
2578
|
});
|
|
2460
2579
|
}
|
|
@@ -2478,10 +2597,23 @@ function deriveLightningReceive(input) {
|
|
|
2478
2597
|
payoutPubkey: input.payoutPubkey,
|
|
2479
2598
|
payoutPkScript: import_sdk9.ArkAddress.decode(input.payoutAddress).pkScript
|
|
2480
2599
|
};
|
|
2481
|
-
const
|
|
2482
|
-
|
|
2483
|
-
|
|
2484
|
-
|
|
2600
|
+
const matched = matchQuotedLockup(
|
|
2601
|
+
quote,
|
|
2602
|
+
input.hrp,
|
|
2603
|
+
input.serverPubkey,
|
|
2604
|
+
(legacy) => receiveVtxoScript({ ...treeParams, ...legacy !== void 0 && { legacy } })
|
|
2605
|
+
);
|
|
2606
|
+
return {
|
|
2607
|
+
address: matched.address,
|
|
2608
|
+
swapPkScript: matched.script.pkScript,
|
|
2609
|
+
script: matched.script,
|
|
2610
|
+
invoice,
|
|
2611
|
+
refundLocktime,
|
|
2612
|
+
treeParams: {
|
|
2613
|
+
...treeParams,
|
|
2614
|
+
...matched.legacy !== void 0 && { legacy: matched.legacy }
|
|
2615
|
+
}
|
|
2616
|
+
};
|
|
2485
2617
|
}
|
|
2486
2618
|
async function requestLightningReceive(wallet, arkServerUrl, transport, params) {
|
|
2487
2619
|
const rfqId = params.rfqId ?? newRfqId();
|
|
@@ -2569,7 +2701,7 @@ function deriveOnchainReceive(input) {
|
|
|
2569
2701
|
if (refundLocktime === void 0 || claimPubkey === void 0 || htlcLocktime === void 0 || minConfirmations === void 0 || solverRefundPkScriptHex === void 0) {
|
|
2570
2702
|
throw new Error("onchain-receive quote is missing a binding field");
|
|
2571
2703
|
}
|
|
2572
|
-
const
|
|
2704
|
+
const treeParams = {
|
|
2573
2705
|
solverPubkey: (0, import_sdk9.toXOnly)(import_base10.hex.decode(quote.solver_pubkey), "solver key"),
|
|
2574
2706
|
refundLocktime,
|
|
2575
2707
|
serverPubkey: input.serverPubkey,
|
|
@@ -2579,9 +2711,13 @@ function deriveOnchainReceive(input) {
|
|
|
2579
2711
|
solverRefundPkScript: solverHex(solverRefundPkScriptHex, "profile.solver_refund_pk_script"),
|
|
2580
2712
|
payoutPubkey: input.payoutPubkey,
|
|
2581
2713
|
payoutPkScript: import_sdk9.ArkAddress.decode(input.payoutAddress).pkScript
|
|
2582
|
-
}
|
|
2583
|
-
const address =
|
|
2584
|
-
|
|
2714
|
+
};
|
|
2715
|
+
const { script, address } = matchQuotedLockup(
|
|
2716
|
+
quote,
|
|
2717
|
+
input.hrp,
|
|
2718
|
+
input.serverPubkey,
|
|
2719
|
+
(legacy) => receiveVtxoScript({ ...treeParams, ...legacy !== void 0 && { legacy } })
|
|
2720
|
+
);
|
|
2585
2721
|
const htlc = onchainHtlcScript(
|
|
2586
2722
|
{
|
|
2587
2723
|
paymentHash: input.paymentHash,
|
|
@@ -2747,6 +2883,7 @@ async function findLockupVtxos(indexer, swapPkScript) {
|
|
|
2747
2883
|
[recoverable.vtxos ?? [], true]
|
|
2748
2884
|
]) {
|
|
2749
2885
|
for (const vtxo of vtxos) {
|
|
2886
|
+
if (vtxo.isUnrolled) continue;
|
|
2750
2887
|
const key = `${vtxo.txid}:${vtxo.vout}`;
|
|
2751
2888
|
if (seen.has(key)) continue;
|
|
2752
2889
|
seen.add(key);
|
|
@@ -2769,10 +2906,17 @@ async function readLockupFate(indexer, input) {
|
|
|
2769
2906
|
const { vtxos } = await indexer.getVtxos({ scripts: [import_base11.hex.encode(input.swapPkScript)] });
|
|
2770
2907
|
const all = vtxos ?? [];
|
|
2771
2908
|
if (all.length === 0) return { fate: "unknown" };
|
|
2909
|
+
const exited = all.filter((vtxo) => vtxo.isUnrolled && !(0, import_sdk11.hasTerminalSpend)(vtxo));
|
|
2910
|
+
if (exited.length > 0) {
|
|
2911
|
+
return {
|
|
2912
|
+
fate: "exited",
|
|
2913
|
+
outpoints: exited.map((vtxo) => ({ txid: vtxo.txid, vout: vtxo.vout }))
|
|
2914
|
+
};
|
|
2915
|
+
}
|
|
2772
2916
|
const spentBy = /* @__PURE__ */ new Map();
|
|
2773
2917
|
let everySpendNamed = true;
|
|
2774
2918
|
for (const vtxo of all) {
|
|
2775
|
-
if (!
|
|
2919
|
+
if (!(0, import_sdk11.hasTerminalSpend)(vtxo)) return { fate: "open" };
|
|
2776
2920
|
if (vtxo.spentBy)
|
|
2777
2921
|
spentBy.set(vtxo.spentBy, {
|
|
2778
2922
|
checkpointTxid: vtxo.spentBy,
|
|
@@ -2814,10 +2958,10 @@ async function pushRefundWithoutReceiver(ark, input) {
|
|
|
2814
2958
|
input.script.options.refundLocktime
|
|
2815
2959
|
);
|
|
2816
2960
|
}
|
|
2817
|
-
const refundPkScript = input.refundPkScript ?? input.script.options.
|
|
2961
|
+
const refundPkScript = input.refundPkScript ?? input.script.options.nonInteractiveParameters?.senderPkScript;
|
|
2818
2962
|
if (!refundPkScript) {
|
|
2819
2963
|
throw new Error(
|
|
2820
|
-
"no refund destination: the contract carries no
|
|
2964
|
+
"no refund destination: the contract carries no emulator covenant suite, so pass refundPkScript explicitly"
|
|
2821
2965
|
);
|
|
2822
2966
|
}
|
|
2823
2967
|
const info = await ark.getInfo();
|
|
@@ -2869,6 +3013,21 @@ async function refundIfUnresolved(transport, ark, indexer, input) {
|
|
|
2869
3013
|
const status = await transport.status(input.rfqId);
|
|
2870
3014
|
if (status && isResolved(status.state)) return { outcome: "resolved", status };
|
|
2871
3015
|
if (now() >= input.refundLocktime) {
|
|
3016
|
+
let fate = { fate: "unknown" };
|
|
3017
|
+
try {
|
|
3018
|
+
fate = await readLockupFate(indexer, {
|
|
3019
|
+
swapPkScript: input.script.pkScript,
|
|
3020
|
+
paymentHash: input.paymentHash
|
|
3021
|
+
});
|
|
3022
|
+
} catch {
|
|
3023
|
+
}
|
|
3024
|
+
if (fate.fate === "exited") {
|
|
3025
|
+
return {
|
|
3026
|
+
outcome: "exited",
|
|
3027
|
+
outpoints: fate.outpoints.map((o) => `${o.txid}:${o.vout}`),
|
|
3028
|
+
status
|
|
3029
|
+
};
|
|
3030
|
+
}
|
|
2872
3031
|
const vtxos = await findLockupVtxos(indexer, input.script.pkScript);
|
|
2873
3032
|
if (vtxos.length === 0) return { outcome: "nothing_to_refund", status };
|
|
2874
3033
|
try {
|
|
@@ -3660,12 +3819,36 @@ var RfqSwapManager = class {
|
|
|
3660
3819
|
this.setState(swap, fate.fate === "claimed" ? "settled" : "refunded");
|
|
3661
3820
|
return;
|
|
3662
3821
|
}
|
|
3663
|
-
if (swap.kind === "lightning_receive")
|
|
3822
|
+
if (swap.kind === "lightning_receive") {
|
|
3823
|
+
return fate.fate === "exited" ? this.blockExitedLockup(swap, fate) : this.driveReceiveClaim(swap);
|
|
3824
|
+
}
|
|
3664
3825
|
if (swap.kind === "onchain_send" && swap.state !== "claimed") {
|
|
3665
3826
|
if (await this.driveOnchain(swap) === "handled") return;
|
|
3666
3827
|
}
|
|
3828
|
+
if (fate.fate === "exited") {
|
|
3829
|
+
const claiming = swap.state === "claimable" || swap.state === "claimed";
|
|
3830
|
+
if (this.config.now() < swap.refundLocktime && claiming) return;
|
|
3831
|
+
return this.blockExitedLockup(swap, fate);
|
|
3832
|
+
}
|
|
3667
3833
|
await this.driveArkadeRefund(swap);
|
|
3668
3834
|
}
|
|
3835
|
+
/**
|
|
3836
|
+
* The lockup was unilaterally exited: its outputs sit onchain under the
|
|
3837
|
+
* VHTLC script, where no offchain claim or refund can reach them.
|
|
3838
|
+
*
|
|
3839
|
+
* `needs_counterparty` rather than a terminal state, because the money still
|
|
3840
|
+
* needs action and the swap can still end either way — an onchain claim can
|
|
3841
|
+
* reveal the preimage, an onchain refund can return it — and that state is
|
|
3842
|
+
* documented as re-checked every pass. It must be set from HERE and not from
|
|
3843
|
+
* inside `driveArkadeRefund`, whose two `unblock` calls would lift it again
|
|
3844
|
+
* on the very next pass.
|
|
3845
|
+
*/
|
|
3846
|
+
blockExitedLockup(swap, fate) {
|
|
3847
|
+
this.block(
|
|
3848
|
+
swap,
|
|
3849
|
+
`the lockup was unilaterally exited (${fate.outpoints.length} output(s) onchain), so no offchain spend can move it \u2014 complete the unroll and spend it onchain`
|
|
3850
|
+
);
|
|
3851
|
+
}
|
|
3669
3852
|
/**
|
|
3670
3853
|
* The receive leg's whole state machine: claim the solver-funded lockup
|
|
3671
3854
|
* while the window is open, and recognise the shapes in which it can be
|
package/dist/index.d.cts
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
import { ProvisionedKey, ProvisionedClaimSecret, asset, IWallet, arkade, RestIndexerProvider, Transaction, IContractManager, VHTLC, Identity, ActivityResolver } from '@arkade-os/sdk';
|
|
2
|
-
import { S as SwapSecretsProjection, R as RfqSwapRecord, A as AssetSwapRepository, a as AssetSwap, M as MarketsCacheEntry, b as RefundArkProvider, c as RefundIndexer, L as LockupVtxo, d as RfqSwap, e as ArkadeRefundResult, f as LockupSpendIndexer, g as RfqSwapState } from './repository-
|
|
3
|
-
export { h as AssetSwapStatus, i as AvailableRfqSwapManagerCallbacks, B as BTC_ASSET_ID, I as InMemoryAssetSwapRepository, j as LightningReceiveSwap, k as LightningSendSwap, l as LockupFate, m as LockupNeedsRecoveryError, n as LockupParams, o as LockupSpend, O as OnchainSendAction, p as OnchainSendSwap, P as PersistableRfqSwap, q as PreimageBlockedReason, r as PreimageNotRecoverableError, s as REFUND_MTP_LAG_SECONDS, t as RFQ_RESOLVED_STATES, u as RFQ_SWAP_RETENTION_SECONDS, v as RFQ_SWAP_TERMINAL_STATES, w as RefundOutcome, x as RfqRestoreFailure, y as RfqRestoreOptions, z as RfqRestoreResult, C as RfqSwapActionName, D as RfqSwapLockup, E as RfqSwapManager, F as RfqSwapManagerCallbacks, G as RfqSwapManagerConfig, H as RfqSwapManagerDeps, J as RfqSwapManagerEvents, K as RfqSwapOrigin, N as RfqSwapOriginRequired, Q as RfqSwapOutcome, T as RfqSwapRecordStore, U as SwapContractRegistry, V as addAssetSwap, W as awaitRfqResolution, X as createRfqSwapRecord, Y as findLockupVtxos, Z as getAssetSwaps, _ as getAssetSwapsOrThrow, $ as isRfqSwapTerminal, a0 as isRfqTerminal, a1 as nextOnchainAction, a2 as preimageForSwapRecord, a3 as pushRefundWithoutReceiver, a4 as readLockupFate, a5 as rebuildRfqSwap, a6 as refundIfUnresolved, a7 as rfqSwapOriginOf, a8 as shouldRetainRfqSwap, a9 as swapSecretsToRecord, aa as updateAssetSwap, ab as updateAssetSwapBestEffort, ac as updateRfqSwapRecord } from './repository-
|
|
1
|
+
import { ProvisionedKey, ProvisionedClaimSecret, asset, RelativeTimelock, IWallet, arkade, RestIndexerProvider, Transaction, IContractManager, VHTLC, Identity, ActivityResolver } from '@arkade-os/sdk';
|
|
2
|
+
import { S as SwapSecretsProjection, R as RfqSwapRecord, A as AssetSwapRepository, a as AssetSwap, M as MarketsCacheEntry, b as RefundArkProvider, c as RefundIndexer, L as LockupVtxo, d as RfqSwap, e as ArkadeRefundResult, f as LockupSpendIndexer, g as RfqSwapState } from './repository-B02vsgcV.cjs';
|
|
3
|
+
export { h as AssetSwapStatus, i as AvailableRfqSwapManagerCallbacks, B as BTC_ASSET_ID, I as InMemoryAssetSwapRepository, j as LightningReceiveSwap, k as LightningSendSwap, l as LockupFate, m as LockupNeedsRecoveryError, n as LockupParams, o as LockupSpend, O as OnchainSendAction, p as OnchainSendSwap, P as PersistableRfqSwap, q as PreimageBlockedReason, r as PreimageNotRecoverableError, s as REFUND_MTP_LAG_SECONDS, t as RFQ_RESOLVED_STATES, u as RFQ_SWAP_RETENTION_SECONDS, v as RFQ_SWAP_TERMINAL_STATES, w as RefundOutcome, x as RfqRestoreFailure, y as RfqRestoreOptions, z as RfqRestoreResult, C as RfqSwapActionName, D as RfqSwapLockup, E as RfqSwapManager, F as RfqSwapManagerCallbacks, G as RfqSwapManagerConfig, H as RfqSwapManagerDeps, J as RfqSwapManagerEvents, K as RfqSwapOrigin, N as RfqSwapOriginRequired, Q as RfqSwapOutcome, T as RfqSwapRecordStore, U as SwapContractRegistry, V as addAssetSwap, W as awaitRfqResolution, X as createRfqSwapRecord, Y as findLockupVtxos, Z as getAssetSwaps, _ as getAssetSwapsOrThrow, $ as isRfqSwapTerminal, a0 as isRfqTerminal, a1 as nextOnchainAction, a2 as preimageForSwapRecord, a3 as pushRefundWithoutReceiver, a4 as readLockupFate, a5 as rebuildRfqSwap, a6 as refundIfUnresolved, a7 as rfqSwapOriginOf, a8 as shouldRetainRfqSwap, a9 as swapSecretsToRecord, aa as updateAssetSwap, ab as updateAssetSwapBestEffort, ac as updateRfqSwapRecord } from './repository-B02vsgcV.cjs';
|
|
4
4
|
import { Network, LocalCardInput, DiscoveredMarket, Side, OfferPlan } from '@arkade-os/solver-discovery';
|
|
5
|
-
import { O as OnchainNetwork, a as OnchainHtlc, b as OnchainHtlcParams } from './rfq-
|
|
6
|
-
export { A as ARKADE_ASSET, c as ARKADE_BTC, d as AddressMismatch, C as ChainSource, e as ChainUtxo, H as HtlcUtxo, I as InvoiceFacts, L as LIGHTNING_BTC, f as LIGHTNING_RECEIVE_PAIR, g as LIGHTNING_SEND_PAIR, h as LOCKTIME_THRESHOLD, i as LightningReceiveTreeParams, j as LightningSendTreeParams, M as MAX_MIN_CONFIRMATIONS, k as MIN_CLAIM_WINDOW_SECONDS, l as MIN_HEADROOM_SECONDS, m as ONCHAIN_BTC, n as ONCHAIN_CLAIM_MARGIN_SECONDS, o as ONCHAIN_DUST_SATS, p as ONCHAIN_ORDER_MARGIN_SECONDS, q as ONCHAIN_RECEIVE_PAIR, r as ONCHAIN_SECONDS_PER_BLOCK, s as ONCHAIN_SEND_PAIR, t as OnchainHtlcPhase, R as RFQ_TERMINAL_STATES, u as RelaySocket, v as RfqQuote, w as RfqRefusalReason, x as RfqStatus, y as RfqTransport, S as SOLO_REFUND_HEADROOM_SECONDS, z as SwapRefusal, B as arkadeAssetLeg, D as arkadeSwapRequest, E as assertFundable, F as assertReceivable, G as awaitOnchainFill, J as buildHtlcClaim, K as buildHtlcRefund, N as claimOnchainFill, P as classifyOnchainHtlc, Q as deriveLightningReceive, T as deriveOnchainReceive, U as deriveOnchainSend, V as extractPreimage, W as httpTransport, X as lightningReceiveRequest, Y as lightningSendRequest, Z as lightningSendVtxoScript, _ as newPreimage, $ as newRfqId, a0 as offerTermsFromQuote, a1 as onchainHtlcScript, a2 as onchainReceiveRequest, a3 as onchainSendRequest, a4 as paymentHashOf, a5 as receiveVtxoScript, a6 as relayTransport, a7 as requestLightningReceive, a8 as requestLightningSend, a9 as requestOnchainReceive, aa as requestOnchainSend, ab as rfqPair, ac as unilateralClaimDelay, ad as unilateralRefundDelay, ae as unilateralRefundWithoutReceiverDelay, af as verifyLockupAddress, ag as verifyReceiveInvoice } from './rfq-
|
|
5
|
+
import { O as OnchainNetwork, a as OnchainHtlc, b as OnchainHtlcParams } from './rfq-DkckzRKK.cjs';
|
|
6
|
+
export { A as ARKADE_ASSET, c as ARKADE_BTC, d as AddressMismatch, C as ChainSource, e as ChainUtxo, H as HtlcUtxo, I as InvoiceFacts, L as LIGHTNING_BTC, f as LIGHTNING_RECEIVE_PAIR, g as LIGHTNING_SEND_PAIR, h as LOCKTIME_THRESHOLD, i as LightningReceiveTreeParams, j as LightningSendTreeParams, M as MAX_MIN_CONFIRMATIONS, k as MIN_CLAIM_WINDOW_SECONDS, l as MIN_HEADROOM_SECONDS, m as ONCHAIN_BTC, n as ONCHAIN_CLAIM_MARGIN_SECONDS, o as ONCHAIN_DUST_SATS, p as ONCHAIN_ORDER_MARGIN_SECONDS, q as ONCHAIN_RECEIVE_PAIR, r as ONCHAIN_SECONDS_PER_BLOCK, s as ONCHAIN_SEND_PAIR, t as OnchainHtlcPhase, R as RFQ_TERMINAL_STATES, u as RelaySocket, v as RfqQuote, w as RfqRefusalReason, x as RfqStatus, y as RfqTransport, S as SOLO_REFUND_HEADROOM_SECONDS, z as SwapRefusal, B as arkadeAssetLeg, D as arkadeSwapRequest, E as assertFundable, F as assertReceivable, G as awaitOnchainFill, J as buildHtlcClaim, K as buildHtlcRefund, N as claimOnchainFill, P as classifyOnchainHtlc, Q as deriveLightningReceive, T as deriveOnchainReceive, U as deriveOnchainSend, V as extractPreimage, W as httpTransport, X as lightningReceiveRequest, Y as lightningSendRequest, Z as lightningSendVtxoScript, _ as newPreimage, $ as newRfqId, a0 as offerTermsFromQuote, a1 as onchainHtlcScript, a2 as onchainReceiveRequest, a3 as onchainSendRequest, a4 as paymentHashOf, a5 as receiveVtxoScript, a6 as relayTransport, a7 as requestLightningReceive, a8 as requestLightningSend, a9 as requestOnchainReceive, aa as requestOnchainSend, ab as rfqPair, ac as unilateralClaimDelay, ad as unilateralRefundDelay, ae as unilateralRefundWithoutReceiverDelay, af as verifyLockupAddress, ag as verifyReceiveInvoice } from './rfq-DkckzRKK.cjs';
|
|
7
7
|
|
|
8
8
|
/**
|
|
9
9
|
* Which wallet key signs this leg. Stored at `profile.signer`.
|
|
@@ -182,7 +182,18 @@ declare function onchainSendProfile(result: {
|
|
|
182
182
|
minConfirmations: number;
|
|
183
183
|
}): Omit<OnchainSendProfile, "signer" | "hashlock">;
|
|
184
184
|
|
|
185
|
-
/**
|
|
185
|
+
/**
|
|
186
|
+
* The contracts, one per WANT side — pure data, shared verbatim with any other
|
|
187
|
+
* implementation.
|
|
188
|
+
*
|
|
189
|
+
* **These are the base: an offer carrying an exit delay compiles to a third
|
|
190
|
+
* closure that is not in either file.** The tree is not fixed-shape — the
|
|
191
|
+
* protocol defines the exit as `iff ExitDelay`, and solverd appends it the same
|
|
192
|
+
* way (`pkg/swap/contract/offer.go`, `VtxoScript`) — so it cannot be a function
|
|
193
|
+
* in a static artifact without splitting these into one file per exit variant.
|
|
194
|
+
* {@link withExitClosure} owns that step, and the golden in `offer.test.ts`
|
|
195
|
+
* pins the artifact it produces so the whole contract is still readable as data.
|
|
196
|
+
*/
|
|
186
197
|
declare const swapPrograms: Record<"wantAsset" | "wantBtc", ReturnType<typeof arkade.parseArtifact>>;
|
|
187
198
|
/** A full-fill offer. Exactly one field names an asset: `wantAsset` set = the
|
|
188
199
|
* fill must deliver that asset (the deposit may be BTC or another asset,
|
|
@@ -203,6 +214,14 @@ interface Offer {
|
|
|
203
214
|
makerPublicKey: Uint8Array;
|
|
204
215
|
/** Covenant co-signer (emulator) x-only key (32 bytes). */
|
|
205
216
|
emulatorPubkey: Uint8Array;
|
|
217
|
+
/** Partial-fill numerator. Reserved wire space in V1: carried through the
|
|
218
|
+
* codec so an offer that sets it decodes, never interpreted here. */
|
|
219
|
+
ratioNum?: bigint;
|
|
220
|
+
/** Partial-fill denominator. Set with {@link Offer.ratioNum} or not at all. */
|
|
221
|
+
ratioDen?: bigint;
|
|
222
|
+
/** The maker's unilateral exit path. Present adds a third closure to the
|
|
223
|
+
* taproot tree, so it changes `swapPkScript` — see {@link offerVtxoScript}. */
|
|
224
|
+
exitDelay?: RelativeTimelock;
|
|
206
225
|
}
|
|
207
226
|
/** Compile the offer's contract: program + args -> taproot tree. */
|
|
208
227
|
declare function offerVtxoScript(offer: Omit<Offer, "swapPkScript">, serverPubkey: Uint8Array): InstanceType<typeof arkade.ArkadeProgramScript>;
|
|
@@ -233,6 +252,14 @@ declare function decodeOffer(data: Uint8Array): Offer;
|
|
|
233
252
|
* funding rather than after: nothing is at stake yet, so a failure can throw
|
|
234
253
|
* and be retried, where the same failure after `wallet.send` would leave a
|
|
235
254
|
* funded deposit unwatched with no way to notice.
|
|
255
|
+
*
|
|
256
|
+
* **The maker's unilateral exit closure is built by default**, at the server's
|
|
257
|
+
* own `unilateralExitDelay` — the same thing solverd does. Without it `cancel`
|
|
258
|
+
* is the only way back out, and `cancel` needs the server's signature: a server
|
|
259
|
+
* that will not co-sign leaves the deposit stuck at the swap address until the
|
|
260
|
+
* VTXO expires and the operator sweeps it. An offer has no expiry of its own,
|
|
261
|
+
* so that exposure has no end. `noExit` opts out for a caller who wants the
|
|
262
|
+
* smaller tree and accepts the dependency.
|
|
236
263
|
*/
|
|
237
264
|
declare function createOffer(wallet: IWallet, arkServerUrl: string, params: {
|
|
238
265
|
wantAmount: bigint;
|
|
@@ -241,6 +268,12 @@ declare function createOffer(wallet: IWallet, arkServerUrl: string, params: {
|
|
|
241
268
|
/** Co-signer key override (33-byte compressed hex); see
|
|
242
269
|
* {@link resolveEmulatorPubkey}. */
|
|
243
270
|
emulatorPubkey?: string;
|
|
271
|
+
/** Override the exit closure's delay. Defaults to the server's own
|
|
272
|
+
* `unilateralExitDelay`, which is the delay solverd uses too. */
|
|
273
|
+
exitDelay?: RelativeTimelock;
|
|
274
|
+
/** Publish without the exit closure, leaving `cancel` — which needs the
|
|
275
|
+
* server — as the only way back out. See the note on this function. */
|
|
276
|
+
noExit?: boolean;
|
|
244
277
|
}): Promise<{
|
|
245
278
|
/** The encoded offer, hex. **Persist this** — it is the only input
|
|
246
279
|
* `cancelOffer` needs to rebuild the covenant, and the restore scan reads
|
|
@@ -263,19 +296,23 @@ declare function createOffer(wallet: IWallet, arkServerUrl: string, params: {
|
|
|
263
296
|
/**
|
|
264
297
|
* Cancel an offer: spend the swap VTXO back to the user. Returns the ark txid.
|
|
265
298
|
*
|
|
266
|
-
* This is the refund path — how a user takes back a deposit no
|
|
267
|
-
* **
|
|
268
|
-
* place at the swap address rather than expiring:
|
|
269
|
-
* "expired" state to unwind, at the cost of the refund being something the
|
|
299
|
+
* This is the cooperative refund path — how a user takes back a deposit no
|
|
300
|
+
* solver filled. **No path here is on a deadline**, so an unfilled deposit
|
|
301
|
+
* keeps its place at the swap address rather than expiring: nothing to miss and
|
|
302
|
+
* no "expired" state to unwind, at the cost of the refund being something the
|
|
270
303
|
* user asks for rather than something a clock delivers.
|
|
271
304
|
*
|
|
272
|
-
*
|
|
305
|
+
* The routes out of the covenant are deliberately asymmetric:
|
|
273
306
|
* - `fulfill` is signed by the **server alone**, but the covenant constrains
|
|
274
307
|
* it to pay output 0 to `makerWP` for at least `wantAmount` — a solver
|
|
275
308
|
* cannot take the deposit without delivering.
|
|
276
309
|
* - `cancel` is a **2-of-2 of the user and the server**, so cancelling is
|
|
277
310
|
* cooperative: the server co-signs. No solver signature is involved, so the
|
|
278
311
|
* refund never depends on the counterparty being reachable.
|
|
312
|
+
* - `exit` is the user **alone** after a relative timelock, present unless the
|
|
313
|
+
* offer was created with `noExit`. It is the route that survives a server
|
|
314
|
+
* that will not co-sign this one, and it is reached by unrolling the VTXO
|
|
315
|
+
* onchain rather than through this function.
|
|
279
316
|
*
|
|
280
317
|
* Cancel therefore races a fill rather than pre-empting it. An offer the solver
|
|
281
318
|
* is filling in the same moment may be spent by `fulfill` first, in which case
|
|
@@ -451,10 +488,19 @@ type SpendKind = "cancelled" | "fulfilled" | "indeterminate";
|
|
|
451
488
|
/**
|
|
452
489
|
* Classify a spend by the covenant leaf it took.
|
|
453
490
|
*
|
|
454
|
-
* The
|
|
455
|
-
*
|
|
456
|
-
* each input's `tapLeafScript`, so the spend
|
|
457
|
-
* reads an answer rather than inferring one.
|
|
491
|
+
* The vocabulary is what became of the deposit, not which key moved it:
|
|
492
|
+
* `fulfill` is the solver paying for it, and everything else returns it to the
|
|
493
|
+
* user. A submitted ark tx keeps each input's `tapLeafScript`, so the spend
|
|
494
|
+
* *states* which one it used — this reads an answer rather than inferring one.
|
|
495
|
+
*
|
|
496
|
+
* **`exit` reports `cancelled`, like `cancel` does.** The two differ in who had
|
|
497
|
+
* to agree — `cancel` is cooperative with the signer, `exit` is the maker alone
|
|
498
|
+
* after a delay — but not in outcome: the deposit went back unfilled either
|
|
499
|
+
* way, which is the question this answers. Reporting the exit leaf as
|
|
500
|
+
* `indeterminate` instead would be worse than imprecise: no status is written,
|
|
501
|
+
* so the swap stays `pending`, `restoreAssetSwaps` re-queues it on every scan,
|
|
502
|
+
* and `retireOfferContract` never runs — leaving a dead script in the
|
|
503
|
+
* subscription and the failsafe poll for the life of the wallet.
|
|
458
504
|
*
|
|
459
505
|
* **Hand it the transaction that actually spends the deposit outpoint, which is
|
|
460
506
|
* the checkpoint, not the ark tx.** A spend is two linked transactions: the
|