@arkade-os/swap 0.0.12 → 0.1.0-rc.1
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 +67 -22
- package/dist/{chunk-XXWOODUE.js → chunk-OEKSNYJW.js} +107 -76
- package/dist/{chunk-6ZUS47GA.js → chunk-U7VWWTL6.js} +15 -1
- package/dist/index.cjs +1457 -312
- package/dist/index.d.cts +542 -46
- package/dist/index.d.ts +542 -46
- package/dist/index.js +1312 -230
- package/dist/node/index.cjs +322 -0
- package/dist/node/index.d.cts +1055 -0
- package/dist/node/index.d.ts +1055 -0
- package/dist/node/index.js +295 -0
- 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.cjs +50 -2
- package/dist/repositories/realm/index.d.cts +55 -10
- package/dist/repositories/realm/index.d.ts +55 -10
- package/dist/repositories/realm/index.js +49 -3
- package/dist/repositories/sqlite/index.cjs +42 -1
- package/dist/repositories/sqlite/index.d.cts +8 -3
- package/dist/repositories/sqlite/index.d.ts +8 -3
- package/dist/repositories/sqlite/index.js +43 -2
- package/dist/{repository-DaK1RzRq.d.cts → repository-DoR-ahHc.d.ts} +1081 -84
- package/dist/{repository-DlLvj_y6.d.ts → repository-oeW8KZo1.d.cts} +1081 -84
- package/dist/{rfq-DzsmhXX3.d.cts → rfq-D0KGjmnn.d.cts} +42 -32
- package/dist/{rfq-DzsmhXX3.d.ts → rfq-D0KGjmnn.d.ts} +42 -32
- package/package.json +22 -6
package/dist/index.js
CHANGED
|
@@ -14,6 +14,7 @@ import {
|
|
|
14
14
|
MIN_HEADROOM_SECONDS,
|
|
15
15
|
ONCHAIN_BTC,
|
|
16
16
|
ONCHAIN_CLAIM_MARGIN_SECONDS,
|
|
17
|
+
ONCHAIN_CLAIM_VSIZE,
|
|
17
18
|
ONCHAIN_DUST_SATS,
|
|
18
19
|
ONCHAIN_ORDER_MARGIN_SECONDS,
|
|
19
20
|
ONCHAIN_RECEIVE_PAIR,
|
|
@@ -39,10 +40,12 @@ import {
|
|
|
39
40
|
deriveOnchainSend,
|
|
40
41
|
extractPreimage,
|
|
41
42
|
httpTransport,
|
|
43
|
+
l1NetworkFromArk,
|
|
42
44
|
l1ScriptForAddress,
|
|
45
|
+
lightningReceiveContract,
|
|
43
46
|
lightningReceiveRequest,
|
|
47
|
+
lightningSendContract,
|
|
44
48
|
lightningSendRequest,
|
|
45
|
-
lightningSendVtxoScript,
|
|
46
49
|
lockupContractParams,
|
|
47
50
|
newPreimage,
|
|
48
51
|
newRfqId,
|
|
@@ -51,7 +54,6 @@ import {
|
|
|
51
54
|
onchainReceiveRequest,
|
|
52
55
|
onchainSendRequest,
|
|
53
56
|
paymentHashOf,
|
|
54
|
-
receiveVtxoScript,
|
|
55
57
|
registerLockupContract,
|
|
56
58
|
relayTransport,
|
|
57
59
|
requestLightningReceive,
|
|
@@ -65,22 +67,20 @@ import {
|
|
|
65
67
|
unilateralRefundWithoutReceiverDelay,
|
|
66
68
|
verifyLockupAddress,
|
|
67
69
|
verifyReceiveInvoice
|
|
68
|
-
} from "./chunk-
|
|
70
|
+
} from "./chunk-OEKSNYJW.js";
|
|
69
71
|
import {
|
|
70
72
|
InMemoryAssetSwapRepository,
|
|
71
73
|
marketsCacheKey
|
|
72
|
-
} from "./chunk-
|
|
74
|
+
} from "./chunk-U7VWWTL6.js";
|
|
73
75
|
|
|
74
76
|
// src/offer.ts
|
|
75
77
|
import { hex as hex2 } from "@scure/base";
|
|
76
78
|
import { concatBytes } from "@scure/btc-signer/utils.js";
|
|
77
79
|
import {
|
|
78
80
|
ArkAddress,
|
|
79
|
-
RestArkProvider,
|
|
80
|
-
RestIndexerProvider,
|
|
81
81
|
arkade,
|
|
82
82
|
asset,
|
|
83
|
-
|
|
83
|
+
networkFromArkadeInfo,
|
|
84
84
|
resolveEmulatorPubkey,
|
|
85
85
|
toXOnlySignerHex
|
|
86
86
|
} from "@arkade-os/sdk";
|
|
@@ -244,9 +244,9 @@ var addAssetSwap = async (repository, swap) => {
|
|
|
244
244
|
const swaps = await getAssetSwapsOrThrow(repository);
|
|
245
245
|
if (swaps.some((s) => s.id === swap.id)) return swaps;
|
|
246
246
|
await saveSwapOrThrow(repository, swap);
|
|
247
|
-
const
|
|
247
|
+
const at2 = swaps.findIndex((s) => byNewest(swap, s) <= 0);
|
|
248
248
|
const merged = [...swaps];
|
|
249
|
-
merged.splice(
|
|
249
|
+
merged.splice(at2 === -1 ? merged.length : at2, 0, swap);
|
|
250
250
|
return merged;
|
|
251
251
|
};
|
|
252
252
|
var updateAssetSwap = async (repository, id, changes) => {
|
|
@@ -346,7 +346,7 @@ function withExitClosure(program, exit) {
|
|
|
346
346
|
}
|
|
347
347
|
};
|
|
348
348
|
}
|
|
349
|
-
function swapProgramBinding(offer,
|
|
349
|
+
function swapProgramBinding(offer, operatorPubkey) {
|
|
350
350
|
if (offer.makerPkScript.length !== FIELDS.makerPkScript.width) {
|
|
351
351
|
throw new Error("makerPkScript is not a 34-byte taproot scriptPubKey");
|
|
352
352
|
}
|
|
@@ -358,7 +358,7 @@ function swapProgramBinding(offer, serverPubkey) {
|
|
|
358
358
|
args: {
|
|
359
359
|
makerWP: offer.makerPkScript.subarray(2),
|
|
360
360
|
wantAmount: offer.wantAmount,
|
|
361
|
-
server:
|
|
361
|
+
server: operatorPubkey,
|
|
362
362
|
user: offer.makerPublicKey,
|
|
363
363
|
// internal byte order
|
|
364
364
|
...offer.wantAsset && {
|
|
@@ -368,14 +368,14 @@ function swapProgramBinding(offer, serverPubkey) {
|
|
|
368
368
|
...offer.exitDelay && { exitDelay: offer.exitDelay.value }
|
|
369
369
|
},
|
|
370
370
|
keys: {
|
|
371
|
-
serverKey:
|
|
371
|
+
serverKey: operatorPubkey,
|
|
372
372
|
userKey: offer.makerPublicKey,
|
|
373
373
|
emulatorKey: offer.emulatorPubkey
|
|
374
374
|
}
|
|
375
375
|
};
|
|
376
376
|
}
|
|
377
|
-
function
|
|
378
|
-
const { program, args, keys } = swapProgramBinding(offer,
|
|
377
|
+
function offerContract(offer, operatorPubkey) {
|
|
378
|
+
const { program, args, keys } = swapProgramBinding(offer, operatorPubkey);
|
|
379
379
|
return new arkade.ArkadeProgramScript(program, args, keys);
|
|
380
380
|
}
|
|
381
381
|
var OFFER_PACKET_TYPE = 3;
|
|
@@ -489,14 +489,14 @@ function decodeOffer(data) {
|
|
|
489
489
|
throw new Error(`missing/invalid ${name}`);
|
|
490
490
|
}
|
|
491
491
|
}
|
|
492
|
-
const
|
|
492
|
+
const need2 = (name) => {
|
|
493
493
|
const v = fields[name];
|
|
494
494
|
const len = FIELDS[name].width;
|
|
495
495
|
if (!v || len !== void 0 && v.length !== len)
|
|
496
496
|
throw new Error(`missing/invalid ${name}`);
|
|
497
497
|
return v;
|
|
498
498
|
};
|
|
499
|
-
const amount =
|
|
499
|
+
const amount = need2("wantAmount");
|
|
500
500
|
if (Boolean(fields.wantAsset) === Boolean(fields.offerAsset)) {
|
|
501
501
|
throw new Error("offer must carry exactly one of wantAsset or offerAsset");
|
|
502
502
|
}
|
|
@@ -513,13 +513,13 @@ function decodeOffer(data) {
|
|
|
513
513
|
throw new Error("offer must carry both ratioNum and ratioDen, or neither");
|
|
514
514
|
}
|
|
515
515
|
return {
|
|
516
|
-
swapPkScript:
|
|
516
|
+
swapPkScript: need2("swapPkScript"),
|
|
517
517
|
wantAmount: readU64(amount),
|
|
518
518
|
...fields.wantAsset && { wantAsset: asset.AssetId.fromBytes(fields.wantAsset) },
|
|
519
519
|
...fields.offerAsset && { offerAsset: asset.AssetId.fromBytes(fields.offerAsset) },
|
|
520
|
-
makerPkScript:
|
|
521
|
-
makerPublicKey:
|
|
522
|
-
emulatorPubkey:
|
|
520
|
+
makerPkScript: need2("makerPkScript"),
|
|
521
|
+
makerPublicKey: need2("makerPublicKey"),
|
|
522
|
+
emulatorPubkey: need2("emulatorPubkey"),
|
|
523
523
|
...ratioNum !== void 0 && { ratioNum },
|
|
524
524
|
...ratioDen !== void 0 && { ratioDen },
|
|
525
525
|
...fields.exitTimelock && { exitDelay: decodeExitDelay(fields.exitTimelock) }
|
|
@@ -532,17 +532,21 @@ function decodeExitDelay(value) {
|
|
|
532
532
|
}
|
|
533
533
|
var OFFER_CONTRACT_LABEL = "Arkade swap offer";
|
|
534
534
|
var OFFER_CONTRACT_KIND = "asset-swap-offer";
|
|
535
|
-
async function registerOfferContract(wallet,
|
|
536
|
-
const { program, args, keys } = swapProgramBinding(binding,
|
|
535
|
+
async function registerOfferContract(wallet, info, binding, operatorPubkey, expectedPkScript) {
|
|
536
|
+
const { program, args, keys } = swapProgramBinding(binding, operatorPubkey);
|
|
537
537
|
const contractManager = await wallet.getContractManager();
|
|
538
538
|
const client = await arkade.Arkade.connect({
|
|
539
|
-
|
|
540
|
-
|
|
539
|
+
// Registration derives and persists; it never broadcasts and never
|
|
540
|
+
// reads UTXOs, so the only thing the client needs off the server is the
|
|
541
|
+
// info the caller already resolved. Handing that back — rather than a
|
|
542
|
+
// provider built from a URL — is what lets `createOffer` take just a
|
|
543
|
+
// wallet, and spares a second `/v1/info` round-trip.
|
|
544
|
+
arkade: { getInfo: async () => info },
|
|
541
545
|
identity: wallet.identity,
|
|
542
546
|
// without this the row's `address` would be derived against the SDK's
|
|
543
547
|
// default network while its script is right — a row that disagrees with
|
|
544
548
|
// the address the user is about to fund
|
|
545
|
-
network:
|
|
549
|
+
network: networkFromArkadeInfo(info),
|
|
546
550
|
contractManager
|
|
547
551
|
});
|
|
548
552
|
const contract = new arkade.ArkadeContract(client, program, args, keys);
|
|
@@ -563,17 +567,21 @@ function serverExitDelay(delay) {
|
|
|
563
567
|
}
|
|
564
568
|
return assertExitDelay({ value: delay, type: delay < BigInt(512) ? "blocks" : "seconds" });
|
|
565
569
|
}
|
|
566
|
-
async function createOffer(wallet,
|
|
570
|
+
async function createOffer(wallet, params) {
|
|
567
571
|
if (Boolean(params.wantAsset) === Boolean(params.offerAsset)) {
|
|
568
572
|
throw new Error("set exactly one of wantAsset (BTC->asset) or offerAsset (asset->BTC)");
|
|
569
573
|
}
|
|
570
574
|
const [info, makerAddress, makerPublicKey] = await Promise.all([
|
|
571
|
-
|
|
575
|
+
// requireLive: this info binds signerPubkey into the covenant — a
|
|
576
|
+
// snapshot could derive an address the operator no longer co-signs
|
|
577
|
+
// for, so an unreachable operator fails the call instead (fail closed,
|
|
578
|
+
// the same behaviour the pre-#734 caller-held provider had)
|
|
579
|
+
wallet.getArkadeInfo({ requireLive: true }),
|
|
572
580
|
wallet.getAddress(),
|
|
573
581
|
wallet.identity.xOnlyPublicKey()
|
|
574
582
|
]);
|
|
575
|
-
const
|
|
576
|
-
const network =
|
|
583
|
+
const operatorPubKey = hex2.decode(toXOnlySignerHex(info.signerPubkey));
|
|
584
|
+
const network = networkFromArkadeInfo(info);
|
|
577
585
|
const emuKey = hex2.decode(
|
|
578
586
|
toXOnlySignerHex(resolveEmulatorPubkey(network, params.emulatorPubkey))
|
|
579
587
|
);
|
|
@@ -589,33 +597,77 @@ async function createOffer(wallet, arkServerUrl, params) {
|
|
|
589
597
|
// an offer that then fails to encode. @see assertExitDelay
|
|
590
598
|
exitDelay: params.noExit ? void 0 : params.exitDelay ? assertExitDelay(params.exitDelay) : serverExitDelay(info.unilateralExitDelay)
|
|
591
599
|
};
|
|
592
|
-
const script =
|
|
600
|
+
const script = offerContract(binding, operatorPubKey);
|
|
593
601
|
const offer = { ...binding, swapPkScript: script.pkScript };
|
|
594
|
-
await registerOfferContract(
|
|
595
|
-
wallet,
|
|
596
|
-
arkServerUrl,
|
|
597
|
-
info.network,
|
|
598
|
-
binding,
|
|
599
|
-
serverPubKey,
|
|
600
|
-
script.pkScript
|
|
601
|
-
);
|
|
602
|
+
await registerOfferContract(wallet, info, binding, operatorPubKey, script.pkScript);
|
|
602
603
|
const payload = encodeOffer(offer);
|
|
603
604
|
return {
|
|
604
605
|
offerHex: hex2.encode(payload),
|
|
605
606
|
extension: { type: OFFER_PACKET_TYPE, payload },
|
|
606
|
-
//
|
|
607
|
+
// the contract's .address() builds the address; assembling an ArkAddress
|
|
607
608
|
// from tweakedPublicKey here would silently miss any future step it gains
|
|
608
|
-
address: script.address(network.hrp,
|
|
609
|
+
address: script.address(network.hrp, operatorPubKey).encode(),
|
|
609
610
|
swapPkScript: script.pkScript
|
|
610
611
|
};
|
|
611
612
|
}
|
|
612
|
-
async function cancelOffer(wallet,
|
|
613
|
+
async function cancelOffer(wallet, offerHex, opts) {
|
|
613
614
|
const { repository, fundingTxid, swapAddress } = opts;
|
|
615
|
+
const prepared = await prepareOfferCancel(wallet, offerHex, {
|
|
616
|
+
...fundingTxid === void 0 ? {} : { fundingTxid },
|
|
617
|
+
...swapAddress === void 0 ? {} : { swapAddress }
|
|
618
|
+
});
|
|
619
|
+
const swapId = fundingTxid ?? prepared.vtxo.txid;
|
|
620
|
+
const hasLocalRecord = (await getAssetSwapsOrThrow(repository)).some((s) => s.id === swapId);
|
|
621
|
+
if (hasLocalRecord) await updateAssetSwap(repository, swapId, { status: "cancelling" });
|
|
622
|
+
const txid = await prepared.send();
|
|
623
|
+
if (hasLocalRecord) {
|
|
624
|
+
const { persisted, swaps } = await updateAssetSwapBestEffort(repository, swapId, {
|
|
625
|
+
status: "cancelled",
|
|
626
|
+
spentTxid: txid
|
|
627
|
+
});
|
|
628
|
+
if (persisted) {
|
|
629
|
+
const contractManager = await wallet.getContractManager();
|
|
630
|
+
await retireOfferContract(
|
|
631
|
+
contractManager,
|
|
632
|
+
swaps,
|
|
633
|
+
hex2.encode(prepared.offer.swapPkScript)
|
|
634
|
+
);
|
|
635
|
+
}
|
|
636
|
+
}
|
|
637
|
+
return txid;
|
|
638
|
+
}
|
|
639
|
+
var OFFER_COVENANT_MISMATCH = "rebuilt covenant does not match the offer's swapPkScript";
|
|
640
|
+
var NoSpendableDepositError = class extends Error {
|
|
641
|
+
name = "NoSpendableDepositError";
|
|
642
|
+
constructor(options) {
|
|
643
|
+
super("no spendable VTXO at the swap address", options);
|
|
644
|
+
}
|
|
645
|
+
};
|
|
646
|
+
var OfferCovenantMismatchError = class extends Error {
|
|
647
|
+
constructor(swapPkScript, options) {
|
|
648
|
+
super(
|
|
649
|
+
`${OFFER_COVENANT_MISMATCH} \u2014 the operator signing key pinned by the rebuild does not reproduce the funded covenant; the signing key has likely rotated since funding (pass swapAddress, the funded address, to pin the original key), or the record is corrupt`,
|
|
650
|
+
options
|
|
651
|
+
);
|
|
652
|
+
this.swapPkScript = swapPkScript;
|
|
653
|
+
}
|
|
654
|
+
swapPkScript;
|
|
655
|
+
name = "OfferCovenantMismatchError";
|
|
656
|
+
};
|
|
657
|
+
async function prepareOfferCancel(wallet, offerHex, opts) {
|
|
658
|
+
const { fundingTxid, swapAddress } = opts;
|
|
614
659
|
const offer = decodeOffer(hex2.decode(offerHex));
|
|
615
|
-
const contractManager = await
|
|
660
|
+
const [contractManager, info, reader, broadcaster] = await Promise.all([
|
|
661
|
+
wallet.getContractManager(),
|
|
662
|
+
wallet.getArkadeInfo({ requireLive: true }),
|
|
663
|
+
wallet.getArkadeReader(),
|
|
664
|
+
wallet.getArkadeBroadcaster()
|
|
665
|
+
]);
|
|
616
666
|
const client = await arkade.Arkade.connect({
|
|
617
|
-
|
|
618
|
-
|
|
667
|
+
// info the wallet already holds, plus its broadcast pair — the shape
|
|
668
|
+
// `ArkadeServerProvider` describes, without a second `/v1/info`
|
|
669
|
+
arkade: { getInfo: async () => info, ...broadcaster },
|
|
670
|
+
indexer: reader,
|
|
619
671
|
identity: wallet.identity,
|
|
620
672
|
// registered offers resolve their VTXOs from the contract repository
|
|
621
673
|
// instead of a direct indexer query; the indexer above stays as the
|
|
@@ -629,9 +681,7 @@ async function cancelOffer(wallet, arkServerUrl, offerHex, opts) {
|
|
|
629
681
|
const { program, args, keys } = swapProgramBinding(offer, operatorPubkey);
|
|
630
682
|
const rebuilt = new arkade.ArkadeProgramScript(program, args, keys);
|
|
631
683
|
if (hex2.encode(rebuilt.pkScript) !== hex2.encode(offer.swapPkScript)) {
|
|
632
|
-
throw new
|
|
633
|
-
"rebuilt covenant does not match the offer's swapPkScript \u2014 the server signing key has likely rotated since funding; pass swapAddress (the funded address) to pin the original key"
|
|
634
|
-
);
|
|
684
|
+
throw new OfferCovenantMismatchError(hex2.encode(offer.swapPkScript));
|
|
635
685
|
}
|
|
636
686
|
const contract = new arkade.ArkadeContract(client, program, args, keys);
|
|
637
687
|
const [vtxos, makerAddress] = await Promise.all([contract.getUtxos(), wallet.getAddress()]);
|
|
@@ -641,7 +691,7 @@ async function cancelOffer(wallet, arkServerUrl, offerHex, opts) {
|
|
|
641
691
|
);
|
|
642
692
|
}
|
|
643
693
|
const vtxo = fundingTxid ? vtxos.find((v) => v.txid === fundingTxid) : vtxos[0];
|
|
644
|
-
if (!vtxo) throw new
|
|
694
|
+
if (!vtxo) throw new NoSpendableDepositError();
|
|
645
695
|
const makerPkScript = ArkAddress.decode(makerAddress).pkScript;
|
|
646
696
|
const cancel = contract.functions.cancel().from({ txid: vtxo.txid, vout: vtxo.vout, value: vtxo.value }).to(makerPkScript, BigInt(vtxo.value));
|
|
647
697
|
for (const a of vtxo.assets ?? []) {
|
|
@@ -651,20 +701,11 @@ async function cancelOffer(wallet, arkServerUrl, offerHex, opts) {
|
|
|
651
701
|
outputs: [{ vout: 0, amount: BigInt(a.amount) }]
|
|
652
702
|
});
|
|
653
703
|
}
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
const { persisted, swaps } = await updateAssetSwapBestEffort(repository, swapId, {
|
|
660
|
-
status: "cancelled",
|
|
661
|
-
spentTxid: txid
|
|
662
|
-
});
|
|
663
|
-
if (persisted) {
|
|
664
|
-
await retireOfferContract(contractManager, swaps, hex2.encode(offer.swapPkScript));
|
|
665
|
-
}
|
|
666
|
-
}
|
|
667
|
-
return txid;
|
|
704
|
+
return {
|
|
705
|
+
offer,
|
|
706
|
+
vtxo: { txid: vtxo.txid, vout: vtxo.vout, value: vtxo.value },
|
|
707
|
+
send: async () => (await cancel.send()).txid
|
|
708
|
+
};
|
|
668
709
|
}
|
|
669
710
|
|
|
670
711
|
// src/markets.ts
|
|
@@ -803,9 +844,10 @@ import {
|
|
|
803
844
|
promisifyRequest
|
|
804
845
|
} from "@arkade-os/sdk";
|
|
805
846
|
var DEFAULT_DB_NAME = "arkade-intents";
|
|
806
|
-
var DB_VERSION =
|
|
847
|
+
var DB_VERSION = 3;
|
|
807
848
|
var STORE_SWAPS = "swaps";
|
|
808
849
|
var STORE_RFQ_SWAPS = "rfqSwaps";
|
|
850
|
+
var STORE_SWAP_RECORDS = "swapRecords";
|
|
809
851
|
var STORE_SCANNED = "scannedTxids";
|
|
810
852
|
var STORE_MARKETS = "markets";
|
|
811
853
|
var STORES = [
|
|
@@ -813,6 +855,11 @@ var STORES = [
|
|
|
813
855
|
// v2. Separate from `swaps` rather than sharing it: the two record types
|
|
814
856
|
// have different keys and no consumer wants them interleaved.
|
|
815
857
|
[STORE_RFQ_SWAPS, { keyPath: "rfqId" }],
|
|
858
|
+
// v3. The v2 client's accept records, keyed by quote id. Its own store for
|
|
859
|
+
// the same reason, plus one more: the v1 read path drops a row carrying
|
|
860
|
+
// neither `offerHex` nor `paymentHash`, so sharing `swaps` would pin the
|
|
861
|
+
// v2 shape to a v1 predicate.
|
|
862
|
+
[STORE_SWAP_RECORDS, { keyPath: "id" }],
|
|
816
863
|
[STORE_SCANNED],
|
|
817
864
|
[STORE_MARKETS]
|
|
818
865
|
];
|
|
@@ -824,7 +871,7 @@ function initDatabase(db, oldVersion, transaction) {
|
|
|
824
871
|
}
|
|
825
872
|
}
|
|
826
873
|
var IndexedDbAssetSwapRepository = class {
|
|
827
|
-
version =
|
|
874
|
+
version = 5;
|
|
828
875
|
connection;
|
|
829
876
|
constructor(dbName = DEFAULT_DB_NAME) {
|
|
830
877
|
this.connection = createManagedConnection(dbName, DB_VERSION, initDatabase);
|
|
@@ -868,6 +915,22 @@ var IndexedDbAssetSwapRepository = class {
|
|
|
868
915
|
store.delete(rfqId);
|
|
869
916
|
});
|
|
870
917
|
}
|
|
918
|
+
async saveSwapRecord(record) {
|
|
919
|
+
await this.write(STORE_SWAP_RECORDS, (store) => {
|
|
920
|
+
store.put(record);
|
|
921
|
+
});
|
|
922
|
+
}
|
|
923
|
+
async getSwapRecord(id) {
|
|
924
|
+
return promisifyRequest((await this.readStore(STORE_SWAP_RECORDS)).get(id));
|
|
925
|
+
}
|
|
926
|
+
async getAllSwapRecords() {
|
|
927
|
+
return promisifyRequest((await this.readStore(STORE_SWAP_RECORDS)).getAll());
|
|
928
|
+
}
|
|
929
|
+
async removeSwapRecord(id) {
|
|
930
|
+
await this.write(STORE_SWAP_RECORDS, (store) => {
|
|
931
|
+
store.delete(id);
|
|
932
|
+
});
|
|
933
|
+
}
|
|
871
934
|
async getScannedTxids() {
|
|
872
935
|
const keys = await promisifyRequest((await this.readStore(STORE_SCANNED)).getAllKeys());
|
|
873
936
|
return new Set(keys);
|
|
@@ -974,6 +1037,14 @@ var parseSigner = (value) => {
|
|
|
974
1037
|
}
|
|
975
1038
|
return { signingDescriptor };
|
|
976
1039
|
};
|
|
1040
|
+
var parsePayoutAddress = (value, kind) => {
|
|
1041
|
+
if (typeof value !== "string" || value.length === 0) {
|
|
1042
|
+
throw new Error(
|
|
1043
|
+
`this ${kind} record's claim destination is unusable: expected an Arkade address, got ${JSON.stringify(value)}`
|
|
1044
|
+
);
|
|
1045
|
+
}
|
|
1046
|
+
return value;
|
|
1047
|
+
};
|
|
977
1048
|
var parseHashlock = (value) => {
|
|
978
1049
|
const raw = value ?? {};
|
|
979
1050
|
const hashlock = {
|
|
@@ -1019,6 +1090,11 @@ var rfqClaimSecretOf = (record) => {
|
|
|
1019
1090
|
);
|
|
1020
1091
|
}
|
|
1021
1092
|
};
|
|
1093
|
+
var rfqClaimDestinationOf = (record) => {
|
|
1094
|
+
const handler = rfqCorridorHandlers.getOrThrow(record.kind);
|
|
1095
|
+
if (!handler.claimDestination) return void 0;
|
|
1096
|
+
return parsePayoutAddress(handler.claimDestination(record.profile), record.kind);
|
|
1097
|
+
};
|
|
1022
1098
|
|
|
1023
1099
|
// src/rfqCorridors.ts
|
|
1024
1100
|
var LightningSendCorridor = {
|
|
@@ -1038,7 +1114,7 @@ var LightningReceiveCorridor = {
|
|
|
1038
1114
|
const receive = swap;
|
|
1039
1115
|
return {
|
|
1040
1116
|
expectedAmount: receive.expectedAmount,
|
|
1041
|
-
...receive.
|
|
1117
|
+
...receive.claimTxid ? { claimTxid: receive.claimTxid } : {}
|
|
1042
1118
|
};
|
|
1043
1119
|
},
|
|
1044
1120
|
hydrate(profile) {
|
|
@@ -1048,13 +1124,16 @@ var LightningReceiveCorridor = {
|
|
|
1048
1124
|
return {
|
|
1049
1125
|
...hydrateHashlock(profile),
|
|
1050
1126
|
expectedAmount: profile.expectedAmount,
|
|
1051
|
-
...profile.
|
|
1127
|
+
...profile.claimTxid ? { claimTxid: profile.claimTxid } : {}
|
|
1052
1128
|
};
|
|
1053
1129
|
},
|
|
1054
1130
|
// We are the claimant here, so the preimage material on the hashlock is
|
|
1055
1131
|
// ours to use.
|
|
1056
1132
|
claimSecret: (profile) => ({ ...profile.signer, ...profile.hashlock }),
|
|
1057
|
-
|
|
1133
|
+
// The other half of claiming: where it pays. Written once at request time
|
|
1134
|
+
// and left alone by `project` above, so this hands back what was stored.
|
|
1135
|
+
claimDestination: (profile) => profile.payoutAddress,
|
|
1136
|
+
activityTxids: (profile) => profile.claimTxid ? [profile.claimTxid] : []
|
|
1058
1137
|
};
|
|
1059
1138
|
function onchainSendProfile(result) {
|
|
1060
1139
|
return {
|
|
@@ -1142,14 +1221,34 @@ var isRfqSwapTerminal = (state) => RFQ_SWAP_TERMINAL_STATES.includes(state);
|
|
|
1142
1221
|
|
|
1143
1222
|
// src/rfqRecord.ts
|
|
1144
1223
|
var RFQ_SWAP_RETENTION_SECONDS = 30 * 24 * 60 * 60;
|
|
1224
|
+
function renameLegacyClaimTxid(profile) {
|
|
1225
|
+
const { claimArkTxid, ...rest } = profile;
|
|
1226
|
+
return { ...rest, claimTxid: rest.claimTxid ?? claimArkTxid };
|
|
1227
|
+
}
|
|
1228
|
+
function normalizeRfqSwapRecord(record) {
|
|
1229
|
+
const { fundingArkTxid, refundArkTxid, lockupSpendArkTxids, ...rest } = record;
|
|
1230
|
+
const legacyClaim = record.kind === "lightning_receive" && record.profile.claimArkTxid !== void 0;
|
|
1231
|
+
if (!fundingArkTxid && !refundArkTxid && !lockupSpendArkTxids && !legacyClaim) return record;
|
|
1232
|
+
const fundingTxid = rest.fundingTxid ?? fundingArkTxid;
|
|
1233
|
+
const refundTxid = rest.refundTxid ?? refundArkTxid;
|
|
1234
|
+
const lockupSpendTxids = rest.lockupSpendTxids ?? lockupSpendArkTxids;
|
|
1235
|
+
return {
|
|
1236
|
+
...rest,
|
|
1237
|
+
...fundingTxid ? { fundingTxid } : {},
|
|
1238
|
+
...refundTxid ? { refundTxid } : {},
|
|
1239
|
+
...lockupSpendTxids?.length ? { lockupSpendTxids } : {},
|
|
1240
|
+
...legacyClaim ? { profile: renameLegacyClaimTxid(record.profile) } : {}
|
|
1241
|
+
};
|
|
1242
|
+
}
|
|
1145
1243
|
var managerState = (swap) => ({
|
|
1146
1244
|
rfqId: swap.rfqId,
|
|
1147
1245
|
state: swap.state,
|
|
1148
1246
|
createdAt: swap.createdAt,
|
|
1149
1247
|
updatedAt: swap.updatedAt,
|
|
1150
|
-
...swap.
|
|
1151
|
-
...swap.
|
|
1248
|
+
...swap.refundTxid ? { refundTxid: swap.refundTxid } : {},
|
|
1249
|
+
...swap.lockupSpendTxids?.length ? { lockupSpendTxids: [...swap.lockupSpendTxids] } : {},
|
|
1152
1250
|
...swap.failure ? { failure: swap.failure } : {},
|
|
1251
|
+
...swap.claimFailure ? { claimFailure: swap.claimFailure } : {},
|
|
1153
1252
|
...swap.blockedReason ? { blockedReason: swap.blockedReason } : {}
|
|
1154
1253
|
});
|
|
1155
1254
|
function assertSameSwap(origin, swap) {
|
|
@@ -1179,27 +1278,30 @@ function createRfqSwapRecord(origin, swap) {
|
|
|
1179
1278
|
}
|
|
1180
1279
|
function updateRfqSwapRecord(record, swap) {
|
|
1181
1280
|
assertSameSwap(record, swap);
|
|
1281
|
+
const stored = normalizeRfqSwapRecord(record);
|
|
1182
1282
|
const {
|
|
1183
|
-
|
|
1184
|
-
|
|
1283
|
+
refundTxid: _refundTxid,
|
|
1284
|
+
lockupSpendTxids: _lockupSpendTxids,
|
|
1185
1285
|
failure: _failure,
|
|
1286
|
+
claimFailure: _claimFailure,
|
|
1186
1287
|
blockedReason: _blockedReason,
|
|
1187
1288
|
...origin
|
|
1188
|
-
} =
|
|
1189
|
-
const handler = rfqCorridorHandlers.getOrThrow(
|
|
1289
|
+
} = stored;
|
|
1290
|
+
const handler = rfqCorridorHandlers.getOrThrow(stored.kind);
|
|
1190
1291
|
return {
|
|
1191
1292
|
...origin,
|
|
1192
1293
|
...managerState(swap),
|
|
1193
|
-
profile: { ...
|
|
1294
|
+
profile: { ...stored.profile, ...handler.project(swap) }
|
|
1194
1295
|
};
|
|
1195
1296
|
}
|
|
1196
1297
|
function rfqSwapOriginOf(record) {
|
|
1298
|
+
const stored = normalizeRfqSwapRecord(record);
|
|
1197
1299
|
return {
|
|
1198
|
-
kind:
|
|
1199
|
-
lockupAddress:
|
|
1200
|
-
profile: { ...
|
|
1201
|
-
...
|
|
1202
|
-
...
|
|
1300
|
+
kind: stored.kind,
|
|
1301
|
+
lockupAddress: stored.lockupAddress,
|
|
1302
|
+
profile: { ...stored.profile },
|
|
1303
|
+
...stored.amount !== void 0 ? { amount: stored.amount } : {},
|
|
1304
|
+
...stored.fundingTxid ? { fundingTxid: stored.fundingTxid } : {}
|
|
1203
1305
|
};
|
|
1204
1306
|
}
|
|
1205
1307
|
function lockupScript(params, lockupAddress) {
|
|
@@ -1213,27 +1315,29 @@ function lockupScript(params, lockupAddress) {
|
|
|
1213
1315
|
return script;
|
|
1214
1316
|
}
|
|
1215
1317
|
function rebuildRfqSwap(record, params) {
|
|
1216
|
-
const
|
|
1318
|
+
const stored = normalizeRfqSwapRecord(record);
|
|
1319
|
+
const script = lockupScript(params, stored.lockupAddress);
|
|
1217
1320
|
const common = {
|
|
1218
|
-
rfqId:
|
|
1219
|
-
state:
|
|
1321
|
+
rfqId: stored.rfqId,
|
|
1322
|
+
state: stored.state,
|
|
1220
1323
|
lockupPkScript: script.pkScript,
|
|
1221
|
-
lockup: { script, address:
|
|
1324
|
+
lockup: { script, address: stored.lockupAddress },
|
|
1222
1325
|
// From the covenant, which binds it: the record's own copy would be a
|
|
1223
1326
|
// second source for the deadline the refund is gated on.
|
|
1224
1327
|
refundLocktime: Number(script.options.refundLocktime),
|
|
1225
|
-
createdAt:
|
|
1226
|
-
updatedAt:
|
|
1227
|
-
...
|
|
1228
|
-
...
|
|
1229
|
-
...
|
|
1230
|
-
...
|
|
1328
|
+
createdAt: stored.createdAt,
|
|
1329
|
+
updatedAt: stored.updatedAt,
|
|
1330
|
+
...stored.refundTxid ? { refundTxid: stored.refundTxid } : {},
|
|
1331
|
+
...stored.lockupSpendTxids?.length ? { lockupSpendTxids: [...stored.lockupSpendTxids] } : {},
|
|
1332
|
+
...stored.failure ? { failure: stored.failure } : {},
|
|
1333
|
+
...stored.claimFailure ? { claimFailure: stored.claimFailure } : {},
|
|
1334
|
+
...stored.blockedReason ? { blockedReason: stored.blockedReason } : {}
|
|
1231
1335
|
};
|
|
1232
|
-
const handler = rfqCorridorHandlers.getOrThrow(
|
|
1336
|
+
const handler = rfqCorridorHandlers.getOrThrow(stored.kind);
|
|
1233
1337
|
return {
|
|
1234
1338
|
...common,
|
|
1235
|
-
kind:
|
|
1236
|
-
...handler.hydrate(
|
|
1339
|
+
kind: stored.kind,
|
|
1340
|
+
...handler.hydrate(stored.profile, { lockup: script })
|
|
1237
1341
|
};
|
|
1238
1342
|
}
|
|
1239
1343
|
function shouldRetainRfqSwap(record, now) {
|
|
@@ -1274,10 +1378,10 @@ async function fetchParsedTxs(indexer, txids) {
|
|
|
1274
1378
|
var unscannedSwapCandidates = (txs, existingIds, scanned) => txs.filter(
|
|
1275
1379
|
(tx) => tx.type === "sent" && tx.redeemTxid && !existingIds.has(tx.redeemTxid) && !scanned.has(tx.redeemTxid)
|
|
1276
1380
|
);
|
|
1277
|
-
function classifySpend(offer,
|
|
1381
|
+
function classifySpend(offer, operatorPubkey, spendTx, deposit) {
|
|
1278
1382
|
let leaves;
|
|
1279
1383
|
try {
|
|
1280
|
-
const script =
|
|
1384
|
+
const script = offerContract(offer, operatorPubkey);
|
|
1281
1385
|
if (hex5.encode(script.pkScript) !== hex5.encode(offer.swapPkScript)) return "indeterminate";
|
|
1282
1386
|
leaves = {
|
|
1283
1387
|
// both routes that hand the deposit back; `exit` is absent on an
|
|
@@ -1301,15 +1405,15 @@ function classifySpend(offer, serverPubkey, spendTx, deposit) {
|
|
|
1301
1405
|
return "indeterminate";
|
|
1302
1406
|
}
|
|
1303
1407
|
var spendTxidsOf = (vtxo) => [vtxo.spentBy, vtxo.arkTxId].filter((id) => Boolean(id));
|
|
1304
|
-
function classifyDepositSpend(offer,
|
|
1408
|
+
function classifyDepositSpend(offer, operatorPubkey, spendTxs, deposit) {
|
|
1305
1409
|
for (const tx of spendTxs) {
|
|
1306
|
-
const kind = classifySpend(offer,
|
|
1410
|
+
const kind = classifySpend(offer, operatorPubkey, tx, deposit);
|
|
1307
1411
|
if (kind !== "indeterminate") return kind;
|
|
1308
1412
|
}
|
|
1309
1413
|
return "indeterminate";
|
|
1310
1414
|
}
|
|
1311
1415
|
async function restoreAssetSwaps(indexer, txs, existingIds, opts) {
|
|
1312
|
-
const {
|
|
1416
|
+
const { operatorPubkey, scanned = /* @__PURE__ */ new Set() } = opts;
|
|
1313
1417
|
const candidates = unscannedSwapCandidates(txs, existingIds, scanned);
|
|
1314
1418
|
if (candidates.length === 0) return { restored: [], scannedTxids: [] };
|
|
1315
1419
|
const byTxid = new Map(candidates.map((tx) => [tx.redeemTxid, tx]));
|
|
@@ -1378,7 +1482,7 @@ async function restoreAssetSwaps(indexer, txs, existingIds, opts) {
|
|
|
1378
1482
|
if (state === "swept") status = "recoverable";
|
|
1379
1483
|
else if (state === "spent") {
|
|
1380
1484
|
const spendTxs = spendTxidsOf(vtxo).map((id) => spendTxByTxid.get(id)).filter((tx) => tx !== void 0);
|
|
1381
|
-
const kind = classifyDepositSpend(offer,
|
|
1485
|
+
const kind = classifyDepositSpend(offer, operatorPubkey, spendTxs, {
|
|
1382
1486
|
txid: vtxo.txid,
|
|
1383
1487
|
vout: vtxo.vout
|
|
1384
1488
|
});
|
|
@@ -1395,7 +1499,7 @@ async function restoreAssetSwaps(indexer, txs, existingIds, opts) {
|
|
|
1395
1499
|
fromAmount,
|
|
1396
1500
|
toAmount: offer.wantAmount.toString(),
|
|
1397
1501
|
// ponytail(arkade-os/ts-sdk#680): empty address makes cancel fall back
|
|
1398
|
-
// to the current
|
|
1502
|
+
// to the current operator key; store the funded address if operator-key
|
|
1399
1503
|
// rotations become real (cancelOffer now at least diagnoses the
|
|
1400
1504
|
// mismatch instead of reporting a missing VTXO)
|
|
1401
1505
|
swapAddress: "",
|
|
@@ -1417,10 +1521,13 @@ async function restoreAssetSwaps(indexer, txs, existingIds, opts) {
|
|
|
1417
1521
|
import { base64 as base642, hex as hex6 } from "@scure/base";
|
|
1418
1522
|
import {
|
|
1419
1523
|
ArkAddress as ArkAddress3,
|
|
1420
|
-
RestIndexerProvider as RestIndexerProvider3,
|
|
1421
1524
|
Transaction as Transaction2
|
|
1422
1525
|
} from "@arkade-os/sdk";
|
|
1423
1526
|
var TERMINAL = ["fulfilled", "cancelled", "recoverable"];
|
|
1527
|
+
var assetSwapSource = (repository) => ({
|
|
1528
|
+
list: () => getAssetSwaps(repository),
|
|
1529
|
+
apply: async (swap, changes) => updateAssetSwapBestEffort(repository, swap.id, changes)
|
|
1530
|
+
});
|
|
1424
1531
|
function spendUpdate(swap, spend) {
|
|
1425
1532
|
if (TERMINAL.includes(swap.status)) return void 0;
|
|
1426
1533
|
if (spend.kind === "indeterminate") return void 0;
|
|
@@ -1434,13 +1541,20 @@ function spendUpdate(swap, spend) {
|
|
|
1434
1541
|
}
|
|
1435
1542
|
async function watchOfferSwaps({
|
|
1436
1543
|
wallet,
|
|
1437
|
-
arkServerUrl,
|
|
1438
1544
|
repository,
|
|
1545
|
+
source,
|
|
1439
1546
|
onUpdate
|
|
1440
1547
|
}) {
|
|
1441
|
-
|
|
1442
|
-
|
|
1443
|
-
|
|
1548
|
+
if (!source && !repository) {
|
|
1549
|
+
throw new Error("watchOfferSwaps needs either a record source or a repository");
|
|
1550
|
+
}
|
|
1551
|
+
const records = source ?? assetSwapSource(repository);
|
|
1552
|
+
const [manager, address, indexer] = await Promise.all([
|
|
1553
|
+
wallet.getContractManager(),
|
|
1554
|
+
wallet.getAddress(),
|
|
1555
|
+
wallet.getArkadeReader()
|
|
1556
|
+
]);
|
|
1557
|
+
const operatorPubkey = ArkAddress3.decode(address).serverPubKey;
|
|
1444
1558
|
let queue = Promise.resolve();
|
|
1445
1559
|
const enqueue = (task) => {
|
|
1446
1560
|
queue = queue.then(task).catch(() => {
|
|
@@ -1454,7 +1568,7 @@ async function watchOfferSwaps({
|
|
|
1454
1568
|
const { txs } = await indexer.getVirtualTxs(candidates);
|
|
1455
1569
|
return classifyDepositSpend(
|
|
1456
1570
|
decodeOffer(hex6.decode(swap.offerHex)),
|
|
1457
|
-
|
|
1571
|
+
operatorPubkey,
|
|
1458
1572
|
txs.map((psbt) => Transaction2.fromPSBT(base642.decode(psbt))),
|
|
1459
1573
|
{ txid: vtxo.txid, vout: vtxo.vout }
|
|
1460
1574
|
);
|
|
@@ -1467,20 +1581,16 @@ async function watchOfferSwaps({
|
|
|
1467
1581
|
for (const vtxo of event.vtxos) {
|
|
1468
1582
|
const spentTxid = vtxo.arkTxId || vtxo.spentBy;
|
|
1469
1583
|
if (!spentTxid) continue;
|
|
1470
|
-
const swap = (await
|
|
1584
|
+
const swap = (await records.list()).find(
|
|
1471
1585
|
(s) => s.fundingTxid === vtxo.txid && s.swapPkScript === event.contractScript
|
|
1472
1586
|
);
|
|
1473
1587
|
if (!swap) continue;
|
|
1474
1588
|
const kind = await classify(swap, vtxo, spentTxid);
|
|
1475
1589
|
const changes = spendUpdate(swap, { txid: spentTxid, kind, at: event.timestamp });
|
|
1476
1590
|
if (!changes) continue;
|
|
1477
|
-
const { persisted, swaps } = await
|
|
1478
|
-
repository,
|
|
1479
|
-
swap.id,
|
|
1480
|
-
changes
|
|
1481
|
-
);
|
|
1591
|
+
const { persisted, swaps } = await records.apply(swap, changes);
|
|
1482
1592
|
if (!persisted) continue;
|
|
1483
|
-
onUpdate?.(
|
|
1593
|
+
onUpdate?.(swaps.find((s) => s.id === swap.id) ?? swap);
|
|
1484
1594
|
if (changes.status && RETIRABLE.includes(changes.status)) {
|
|
1485
1595
|
await retireOfferContract(manager, swaps, event.contractScript);
|
|
1486
1596
|
}
|
|
@@ -1616,7 +1726,7 @@ function solverOnchainRail(deps) {
|
|
|
1616
1726
|
}
|
|
1617
1727
|
const negotiated = await deps.connect(
|
|
1618
1728
|
rendezvous,
|
|
1619
|
-
(transport) => requestOnchainSend(ctx.wallet,
|
|
1729
|
+
(transport) => requestOnchainSend(ctx.wallet, transport, {
|
|
1620
1730
|
amount,
|
|
1621
1731
|
amountSide: "to",
|
|
1622
1732
|
payoutPubkey: deps.payoutPubkey,
|
|
@@ -1729,7 +1839,7 @@ function solverLightningRail(deps) {
|
|
|
1729
1839
|
}
|
|
1730
1840
|
const negotiated = await deps.connect(
|
|
1731
1841
|
rendezvous,
|
|
1732
|
-
(transport) => requestLightningSend(ctx.wallet,
|
|
1842
|
+
(transport) => requestLightningSend(ctx.wallet, transport, {
|
|
1733
1843
|
invoice: facts,
|
|
1734
1844
|
...deps.emulatorPubkey ? { emulatorPubkey: deps.emulatorPubkey } : {}
|
|
1735
1845
|
})
|
|
@@ -1782,6 +1892,440 @@ function solverLightningRail(deps) {
|
|
|
1782
1892
|
};
|
|
1783
1893
|
}
|
|
1784
1894
|
|
|
1895
|
+
// src/payment/lightning.ts
|
|
1896
|
+
import { invoiceTarget as invoiceTarget2 } from "@arkade-os/sdk";
|
|
1897
|
+
|
|
1898
|
+
// src/payment/swapRail.ts
|
|
1899
|
+
import { makeHandle as makeHandle3 } from "@arkade-os/sdk";
|
|
1900
|
+
|
|
1901
|
+
// src/client/errors.ts
|
|
1902
|
+
var AmbiguousDestination = class extends Error {
|
|
1903
|
+
constructor(destination, detail) {
|
|
1904
|
+
super(`cannot route ${JSON.stringify(destination)}: ${detail}`);
|
|
1905
|
+
this.destination = destination;
|
|
1906
|
+
}
|
|
1907
|
+
destination;
|
|
1908
|
+
name = "AmbiguousDestination";
|
|
1909
|
+
};
|
|
1910
|
+
var UnsupportedRoute = class extends Error {
|
|
1911
|
+
name = "UnsupportedRoute";
|
|
1912
|
+
give;
|
|
1913
|
+
take;
|
|
1914
|
+
constructor(detail, corridors = {}) {
|
|
1915
|
+
super(`unsupported route: ${detail}`);
|
|
1916
|
+
this.give = corridors.give;
|
|
1917
|
+
this.take = corridors.take;
|
|
1918
|
+
}
|
|
1919
|
+
};
|
|
1920
|
+
var DiscoverySnapshotUnavailable = class extends Error {
|
|
1921
|
+
constructor(network, detail) {
|
|
1922
|
+
super(`cannot resolve on ${network} offline: ${detail}`);
|
|
1923
|
+
this.network = network;
|
|
1924
|
+
}
|
|
1925
|
+
network;
|
|
1926
|
+
name = "DiscoverySnapshotUnavailable";
|
|
1927
|
+
};
|
|
1928
|
+
var AmountMismatch = class extends Error {
|
|
1929
|
+
constructor(sources) {
|
|
1930
|
+
super(`exactly one amount may be pinned; got ${sources[0]} and ${sources[1]}`);
|
|
1931
|
+
this.sources = sources;
|
|
1932
|
+
}
|
|
1933
|
+
sources;
|
|
1934
|
+
name = "AmountMismatch";
|
|
1935
|
+
};
|
|
1936
|
+
var AmountEncodingUnsupported = class extends Error {
|
|
1937
|
+
constructor(field, value, detail, options) {
|
|
1938
|
+
super(`${field}: ${detail}`, options);
|
|
1939
|
+
this.field = field;
|
|
1940
|
+
this.value = value;
|
|
1941
|
+
}
|
|
1942
|
+
field;
|
|
1943
|
+
value;
|
|
1944
|
+
name = "AmountEncodingUnsupported";
|
|
1945
|
+
};
|
|
1946
|
+
var QuoteVerificationFailed = class extends Error {
|
|
1947
|
+
constructor(check, expected, actual, options) {
|
|
1948
|
+
super(`quote failed the ${check} check \u2014 refusing to fund`, options);
|
|
1949
|
+
this.check = check;
|
|
1950
|
+
this.expected = expected;
|
|
1951
|
+
this.actual = actual;
|
|
1952
|
+
}
|
|
1953
|
+
check;
|
|
1954
|
+
name = "QuoteVerificationFailed";
|
|
1955
|
+
expected;
|
|
1956
|
+
actual;
|
|
1957
|
+
};
|
|
1958
|
+
var QuoteExpired = class extends Error {
|
|
1959
|
+
constructor(quoteId, expiresAt, now) {
|
|
1960
|
+
super(`quote ${quoteId} expired at ${expiresAt}, ${now - expiresAt} ago`);
|
|
1961
|
+
this.quoteId = quoteId;
|
|
1962
|
+
this.expiresAt = expiresAt;
|
|
1963
|
+
this.now = now;
|
|
1964
|
+
}
|
|
1965
|
+
quoteId;
|
|
1966
|
+
expiresAt;
|
|
1967
|
+
now;
|
|
1968
|
+
name = "QuoteExpired";
|
|
1969
|
+
};
|
|
1970
|
+
var MaxFeeExceeded = class extends Error {
|
|
1971
|
+
constructor(quoteId, asset3, fee, maxFee) {
|
|
1972
|
+
super(`fee ${fee} of ${asset3} exceeds the ${maxFee} ceiling`);
|
|
1973
|
+
this.quoteId = quoteId;
|
|
1974
|
+
this.asset = asset3;
|
|
1975
|
+
this.fee = fee;
|
|
1976
|
+
this.maxFee = maxFee;
|
|
1977
|
+
}
|
|
1978
|
+
quoteId;
|
|
1979
|
+
asset;
|
|
1980
|
+
fee;
|
|
1981
|
+
maxFee;
|
|
1982
|
+
name = "MaxFeeExceeded";
|
|
1983
|
+
};
|
|
1984
|
+
var InsufficientFunds = class extends Error {
|
|
1985
|
+
constructor(asset3, required, available) {
|
|
1986
|
+
super(
|
|
1987
|
+
`funding needs ${required} of ${asset3}` + (available === void 0 ? "" : `, wallet holds ${available}`)
|
|
1988
|
+
);
|
|
1989
|
+
this.asset = asset3;
|
|
1990
|
+
this.required = required;
|
|
1991
|
+
this.available = available;
|
|
1992
|
+
}
|
|
1993
|
+
asset;
|
|
1994
|
+
required;
|
|
1995
|
+
name = "InsufficientFunds";
|
|
1996
|
+
available;
|
|
1997
|
+
};
|
|
1998
|
+
var AcceptConflict = class extends Error {
|
|
1999
|
+
constructor(quoteId, swapId, fields) {
|
|
2000
|
+
super(`accept ${quoteId} conflicts with swap ${swapId} on ${fields.join(", ")}`);
|
|
2001
|
+
this.quoteId = quoteId;
|
|
2002
|
+
this.swapId = swapId;
|
|
2003
|
+
this.fields = fields;
|
|
2004
|
+
}
|
|
2005
|
+
quoteId;
|
|
2006
|
+
swapId;
|
|
2007
|
+
fields;
|
|
2008
|
+
name = "AcceptConflict";
|
|
2009
|
+
};
|
|
2010
|
+
var ClientDisposed = class extends Error {
|
|
2011
|
+
constructor(method) {
|
|
2012
|
+
super(`${method}() called after the client was disposed`);
|
|
2013
|
+
this.method = method;
|
|
2014
|
+
}
|
|
2015
|
+
method;
|
|
2016
|
+
name = "ClientDisposed";
|
|
2017
|
+
};
|
|
2018
|
+
var NotCancellable = class extends Error {
|
|
2019
|
+
constructor(swapId) {
|
|
2020
|
+
super(`swap ${swapId} is not a cancellable asset swap`);
|
|
2021
|
+
this.swapId = swapId;
|
|
2022
|
+
}
|
|
2023
|
+
swapId;
|
|
2024
|
+
name = "NotCancellable";
|
|
2025
|
+
};
|
|
2026
|
+
var InconsistentRoute = class extends Error {
|
|
2027
|
+
constructor(asset3, destination) {
|
|
2028
|
+
super(`destination ${JSON.stringify(destination)} is not on ${asset3}'s chain`);
|
|
2029
|
+
this.asset = asset3;
|
|
2030
|
+
this.destination = destination;
|
|
2031
|
+
}
|
|
2032
|
+
asset;
|
|
2033
|
+
destination;
|
|
2034
|
+
name = "InconsistentRoute";
|
|
2035
|
+
};
|
|
2036
|
+
var OperatorUnreachable = class extends Error {
|
|
2037
|
+
name = "OperatorUnreachable";
|
|
2038
|
+
constructor(detail, options) {
|
|
2039
|
+
super(`cannot derive a covenant: ${detail}`, options);
|
|
2040
|
+
}
|
|
2041
|
+
};
|
|
2042
|
+
var MissingCorridorDep = class extends Error {
|
|
2043
|
+
constructor(corridor, dep) {
|
|
2044
|
+
super(`the ${corridor} corridor has no ${dep}`);
|
|
2045
|
+
this.corridor = corridor;
|
|
2046
|
+
this.dep = dep;
|
|
2047
|
+
}
|
|
2048
|
+
corridor;
|
|
2049
|
+
dep;
|
|
2050
|
+
name = "MissingCorridorDep";
|
|
2051
|
+
};
|
|
2052
|
+
var SWAP_ERRORS = {
|
|
2053
|
+
AmbiguousDestination,
|
|
2054
|
+
UnsupportedRoute,
|
|
2055
|
+
DiscoverySnapshotUnavailable,
|
|
2056
|
+
AmountMismatch,
|
|
2057
|
+
AmountEncodingUnsupported,
|
|
2058
|
+
QuoteVerificationFailed,
|
|
2059
|
+
SwapRefusal,
|
|
2060
|
+
QuoteExpired,
|
|
2061
|
+
MaxFeeExceeded,
|
|
2062
|
+
InsufficientFunds,
|
|
2063
|
+
AcceptConflict,
|
|
2064
|
+
ClientDisposed,
|
|
2065
|
+
NotCancellable,
|
|
2066
|
+
InconsistentRoute,
|
|
2067
|
+
OperatorUnreachable,
|
|
2068
|
+
MissingCorridorDep
|
|
2069
|
+
};
|
|
2070
|
+
var SWAP_ERROR_NAMES = Object.keys(SWAP_ERRORS);
|
|
2071
|
+
function isSwapError(e, name) {
|
|
2072
|
+
if (!(e instanceof Error)) return false;
|
|
2073
|
+
const table = SWAP_ERRORS;
|
|
2074
|
+
const ctor = table[e.name];
|
|
2075
|
+
if (ctor === void 0 || !(e instanceof ctor)) return false;
|
|
2076
|
+
return name === void 0 || e.name === name;
|
|
2077
|
+
}
|
|
2078
|
+
|
|
2079
|
+
// src/client/sats.ts
|
|
2080
|
+
var satsOf = (value, field) => {
|
|
2081
|
+
if (value < 0n || value > BigInt(Number.MAX_SAFE_INTEGER)) {
|
|
2082
|
+
throw new AmountEncodingUnsupported(
|
|
2083
|
+
field,
|
|
2084
|
+
`${value}`,
|
|
2085
|
+
"outside the non-negative safe-integer window core's payment amounts are numbers in"
|
|
2086
|
+
);
|
|
2087
|
+
}
|
|
2088
|
+
return Number(value);
|
|
2089
|
+
};
|
|
2090
|
+
|
|
2091
|
+
// src/payment/status.ts
|
|
2092
|
+
var PAYMENT_STATUS = {
|
|
2093
|
+
/** Persisted, funding not broadcast. */
|
|
2094
|
+
accepted: "pending",
|
|
2095
|
+
funding: "pending",
|
|
2096
|
+
/** The lockup is funded. Nothing has emitted `"sent"` in core since the
|
|
2097
|
+
* `onchain-swap` rail left with `packages/boltz-swap`. */
|
|
2098
|
+
funded: "sent",
|
|
2099
|
+
/** Asset swaps only: an unfilled offer. */
|
|
2100
|
+
open: "pending",
|
|
2101
|
+
/** Asset swaps only. */
|
|
2102
|
+
filled: "settled",
|
|
2103
|
+
/** The trader's L1 claim on `arkade -> onchain`. */
|
|
2104
|
+
claimed: "settled",
|
|
2105
|
+
/** Terminal success on `arkade -> lightning`: the solver's hash-verified
|
|
2106
|
+
* spend IS the invoice being paid. */
|
|
2107
|
+
paid: "settled",
|
|
2108
|
+
cancelling: "pending",
|
|
2109
|
+
/** Value returned, and the router has no non-loss terminal to say so with. */
|
|
2110
|
+
cancelled: "failed",
|
|
2111
|
+
/** Terminal HERE, not at `refunded` — see the module docblock. */
|
|
2112
|
+
refunding: "failed",
|
|
2113
|
+
/** The trader's value came back. Reaches `onUpdate`, not the handle. */
|
|
2114
|
+
refunded: "failed",
|
|
2115
|
+
/** The solver reclaimed a receive-leg lockup: a loss, and not `refunded`. */
|
|
2116
|
+
lapsed: "failed",
|
|
2117
|
+
/** Never retried silently; `client.recover` drives it. */
|
|
2118
|
+
needs_recovery: "failed",
|
|
2119
|
+
failed: "failed"
|
|
2120
|
+
};
|
|
2121
|
+
var paymentStatusOf = (outcome) => PAYMENT_STATUS[outcome];
|
|
2122
|
+
var isTerminalStatus = (status) => status === "settled" || status === "failed";
|
|
2123
|
+
|
|
2124
|
+
// src/payment/swapRail.ts
|
|
2125
|
+
var SwapPaymentFailedError = class extends Error {
|
|
2126
|
+
constructor(railId, outcome, swap) {
|
|
2127
|
+
super(
|
|
2128
|
+
`${railId}: swap ${swap.id} ended ${outcome}` + (swap.failure === void 0 ? "" : `: ${swap.failure}`) + (swap.blockedReason === void 0 ? "" : ` (${swap.blockedReason})`)
|
|
2129
|
+
);
|
|
2130
|
+
this.railId = railId;
|
|
2131
|
+
this.outcome = outcome;
|
|
2132
|
+
this.swap = swap;
|
|
2133
|
+
}
|
|
2134
|
+
railId;
|
|
2135
|
+
outcome;
|
|
2136
|
+
swap;
|
|
2137
|
+
name = "SwapPaymentFailedError";
|
|
2138
|
+
};
|
|
2139
|
+
var unroutable = (error) => isSwapError(error, "UnsupportedRoute") || isSwapError(error, "AmbiguousDestination");
|
|
2140
|
+
var railAvailable = async (client, input) => {
|
|
2141
|
+
try {
|
|
2142
|
+
return (await client.resolve(input)).eligible > 0;
|
|
2143
|
+
} catch (error) {
|
|
2144
|
+
if (unroutable(error)) return false;
|
|
2145
|
+
throw error;
|
|
2146
|
+
}
|
|
2147
|
+
};
|
|
2148
|
+
var receiverExact = (railId, parts) => {
|
|
2149
|
+
if (parts.total !== parts.amount + parts.fee) {
|
|
2150
|
+
throw new Error(
|
|
2151
|
+
`${railId}: ${parts.total} leaving the wallet is not ${parts.amount} delivered plus ${parts.fee} in fees \u2014 the quote is not receiver-exact`
|
|
2152
|
+
);
|
|
2153
|
+
}
|
|
2154
|
+
return {
|
|
2155
|
+
amount: satsOf(parts.amount, `${railId}.amount`),
|
|
2156
|
+
fee: satsOf(parts.fee, `${railId}.fee`),
|
|
2157
|
+
total: satsOf(parts.total, `${railId}.total`)
|
|
2158
|
+
};
|
|
2159
|
+
};
|
|
2160
|
+
var resultOf = (railId, swap) => ({
|
|
2161
|
+
railId,
|
|
2162
|
+
// The tagged public form M6 minted — round-trippable to `client.swaps()`,
|
|
2163
|
+
// which is where the outcomes past this handle's terminal one are read.
|
|
2164
|
+
swapId: swap.id,
|
|
2165
|
+
// The transaction that left THIS wallet, which is what `txid` means on
|
|
2166
|
+
// every other rail. A swap's later legs — the solver's fill, the trader's
|
|
2167
|
+
// L1 claim — are the client's to report, not the handle's.
|
|
2168
|
+
...swap.fundingTxid === void 0 ? {} : { txid: swap.fundingTxid }
|
|
2169
|
+
});
|
|
2170
|
+
var swapHandle = async (railId, client, quote) => makeHandle3(railId, async (emit) => {
|
|
2171
|
+
const accepted = await client.accept(quote);
|
|
2172
|
+
return await new Promise((resolve, reject) => {
|
|
2173
|
+
let finished = false;
|
|
2174
|
+
let unsubscribe;
|
|
2175
|
+
const listener = (update) => {
|
|
2176
|
+
if (finished || update.swap.id !== accepted.id) return;
|
|
2177
|
+
const status = paymentStatusOf(update.outcome);
|
|
2178
|
+
const result = resultOf(railId, update.swap);
|
|
2179
|
+
if (!isTerminalStatus(status)) {
|
|
2180
|
+
emit({ status, result });
|
|
2181
|
+
return;
|
|
2182
|
+
}
|
|
2183
|
+
finished = true;
|
|
2184
|
+
unsubscribe?.();
|
|
2185
|
+
if (status === "settled") {
|
|
2186
|
+
emit({ status, result });
|
|
2187
|
+
resolve(result);
|
|
2188
|
+
return;
|
|
2189
|
+
}
|
|
2190
|
+
reject(new SwapPaymentFailedError(railId, update.outcome, update.swap));
|
|
2191
|
+
};
|
|
2192
|
+
unsubscribe = client.onUpdate(listener);
|
|
2193
|
+
if (finished) unsubscribe();
|
|
2194
|
+
});
|
|
2195
|
+
});
|
|
2196
|
+
|
|
2197
|
+
// src/payment/lightning.ts
|
|
2198
|
+
var LIGHTNING_RAIL = "lightning";
|
|
2199
|
+
var inputFor = (invoice, amount) => ({
|
|
2200
|
+
to: invoice,
|
|
2201
|
+
...amount === void 0 ? {} : { amount: BigInt(amount), amountOn: "take" }
|
|
2202
|
+
});
|
|
2203
|
+
function lightningRail(client) {
|
|
2204
|
+
return {
|
|
2205
|
+
id: LIGHTNING_RAIL,
|
|
2206
|
+
// Amount-blind and non-throwing: classification only, which is what
|
|
2207
|
+
// keeps `match` from being a second gate on top of `available`.
|
|
2208
|
+
match: (req) => invoiceTarget2(req.raw) !== void 0,
|
|
2209
|
+
available: async (req) => {
|
|
2210
|
+
const invoice = invoiceTarget2(req.raw);
|
|
2211
|
+
if (invoice === void 0) return false;
|
|
2212
|
+
return railAvailable(client, inputFor(invoice, req.amount));
|
|
2213
|
+
},
|
|
2214
|
+
quote: async (req) => {
|
|
2215
|
+
const invoice = invoiceTarget2(req.raw);
|
|
2216
|
+
if (invoice === void 0) {
|
|
2217
|
+
throw new Error(`${LIGHTNING_RAIL}: the request carries no BOLT11 invoice`);
|
|
2218
|
+
}
|
|
2219
|
+
const quote = await client.quote(inputFor(invoice, req.amount));
|
|
2220
|
+
const amounts = receiverExact(LIGHTNING_RAIL, {
|
|
2221
|
+
amount: quote.take.amount,
|
|
2222
|
+
fee: quote.fee.amount,
|
|
2223
|
+
total: quote.give.amount
|
|
2224
|
+
});
|
|
2225
|
+
return {
|
|
2226
|
+
railId: LIGHTNING_RAIL,
|
|
2227
|
+
...amounts,
|
|
2228
|
+
meta: {
|
|
2229
|
+
quoteId: quote.id,
|
|
2230
|
+
// A `RouteQuote` carries no expiry of its own, so a held one
|
|
2231
|
+
// can outlive the terms behind it. This is where a caller
|
|
2232
|
+
// can see that; `send()` refuses with `QuoteExpired` rather
|
|
2233
|
+
// than silently re-quoting.
|
|
2234
|
+
expiresAt: quote.expiresAt,
|
|
2235
|
+
...quote.refundLocktime === void 0 ? {} : { refundLocktime: quote.refundLocktime },
|
|
2236
|
+
...quote.solver === void 0 ? {} : { solver: quote.solver },
|
|
2237
|
+
...quote.lock === void 0 ? {} : { paymentHash: quote.lock.hash },
|
|
2238
|
+
market: quote.market.key
|
|
2239
|
+
},
|
|
2240
|
+
send: () => swapHandle(LIGHTNING_RAIL, client, quote)
|
|
2241
|
+
};
|
|
2242
|
+
}
|
|
2243
|
+
};
|
|
2244
|
+
}
|
|
2245
|
+
|
|
2246
|
+
// src/payment/onchainSwap.ts
|
|
2247
|
+
import { btcTarget as btcTarget2, resolveSendAmount as resolveSendAmount2, tryResolveSendAmount as tryResolveSendAmount2 } from "@arkade-os/sdk";
|
|
2248
|
+
var ONCHAIN_SWAP_RAIL = "onchain-swap";
|
|
2249
|
+
var claimFeeSats = (deps) => BigInt(Math.ceil((deps.claimVsize ?? ONCHAIN_CLAIM_VSIZE) * deps.claimFeeRateSatVb));
|
|
2250
|
+
function onchainSwapRail(client, deps) {
|
|
2251
|
+
if (!Number.isFinite(deps.claimFeeRateSatVb) || deps.claimFeeRateSatVb <= 0) {
|
|
2252
|
+
throw new Error(
|
|
2253
|
+
`${ONCHAIN_SWAP_RAIL}: claimFeeRateSatVb must be positive, got ${deps.claimFeeRateSatVb}`
|
|
2254
|
+
);
|
|
2255
|
+
}
|
|
2256
|
+
const claimFee = claimFeeSats(deps);
|
|
2257
|
+
const inputFor2 = (address, amount) => ({
|
|
2258
|
+
to: address,
|
|
2259
|
+
amount: BigInt(amount) + claimFee,
|
|
2260
|
+
amountOn: "take"
|
|
2261
|
+
});
|
|
2262
|
+
return {
|
|
2263
|
+
id: ONCHAIN_SWAP_RAIL,
|
|
2264
|
+
match: (req) => btcTarget2(req.raw) !== void 0,
|
|
2265
|
+
available: async (req) => {
|
|
2266
|
+
const address = btcTarget2(req.raw);
|
|
2267
|
+
if (address === void 0) return false;
|
|
2268
|
+
const amount = tryResolveSendAmount2(req.raw, req.amount);
|
|
2269
|
+
if (amount === void 0) return false;
|
|
2270
|
+
return railAvailable(client, inputFor2(address, amount));
|
|
2271
|
+
},
|
|
2272
|
+
quote: async (req) => {
|
|
2273
|
+
const address = btcTarget2(req.raw);
|
|
2274
|
+
if (address === void 0) {
|
|
2275
|
+
throw new Error(`${ONCHAIN_SWAP_RAIL}: the request carries no bitcoin address`);
|
|
2276
|
+
}
|
|
2277
|
+
const amount = resolveSendAmount2(ONCHAIN_SWAP_RAIL, req.raw, req.amount);
|
|
2278
|
+
const quote = await client.quote(inputFor2(address, amount));
|
|
2279
|
+
const amounts = receiverExact(ONCHAIN_SWAP_RAIL, {
|
|
2280
|
+
// What lands at the address, which is the HTLC the solver locks
|
|
2281
|
+
// minus the claim this wallet will broadcast to take it.
|
|
2282
|
+
amount: quote.take.amount - claimFee,
|
|
2283
|
+
// Both halves of the cost, inside one number: the solver's
|
|
2284
|
+
// spread, and the claim the trader pays for out of the payout.
|
|
2285
|
+
fee: quote.fee.amount + claimFee,
|
|
2286
|
+
total: quote.give.amount
|
|
2287
|
+
});
|
|
2288
|
+
return {
|
|
2289
|
+
railId: ONCHAIN_SWAP_RAIL,
|
|
2290
|
+
...amounts,
|
|
2291
|
+
meta: {
|
|
2292
|
+
quoteId: quote.id,
|
|
2293
|
+
expiresAt: quote.expiresAt,
|
|
2294
|
+
...quote.refundLocktime === void 0 ? {} : { refundLocktime: quote.refundLocktime },
|
|
2295
|
+
...quote.solver === void 0 ? {} : { solver: quote.solver },
|
|
2296
|
+
...quote.lock === void 0 ? {} : { paymentHash: quote.lock.hash },
|
|
2297
|
+
market: quote.market.key,
|
|
2298
|
+
/** The estimate folded into `fee`, so a caller can see it. */
|
|
2299
|
+
claimFeeSats: Number(claimFee),
|
|
2300
|
+
/** What the solver locks on L1, before the claim's fee. */
|
|
2301
|
+
htlcAmountSats: Number(quote.take.amount)
|
|
2302
|
+
},
|
|
2303
|
+
send: () => swapHandle(ONCHAIN_SWAP_RAIL, client, quote)
|
|
2304
|
+
};
|
|
2305
|
+
}
|
|
2306
|
+
};
|
|
2307
|
+
}
|
|
2308
|
+
|
|
2309
|
+
// src/payment/router.ts
|
|
2310
|
+
import { PaymentRouter as Router, arkRail, onchainRail } from "@arkade-os/sdk";
|
|
2311
|
+
var SWAP_ROUTER_PRIORITY = [
|
|
2312
|
+
"ark",
|
|
2313
|
+
LIGHTNING_RAIL,
|
|
2314
|
+
ONCHAIN_SWAP_RAIL,
|
|
2315
|
+
"onchain"
|
|
2316
|
+
];
|
|
2317
|
+
function createSwapPaymentRouter(wallet, client, config) {
|
|
2318
|
+
return new Router({
|
|
2319
|
+
wallet,
|
|
2320
|
+
prefs: { priority: [...SWAP_ROUTER_PRIORITY], ...config.prefs }
|
|
2321
|
+
}).use(arkRail()).use(onchainRail()).use(lightningRail(client)).use(
|
|
2322
|
+
onchainSwapRail(client, {
|
|
2323
|
+
claimFeeRateSatVb: config.claimFeeRateSatVb,
|
|
2324
|
+
...config.claimVsize === void 0 ? {} : { claimVsize: config.claimVsize }
|
|
2325
|
+
})
|
|
2326
|
+
);
|
|
2327
|
+
}
|
|
2328
|
+
|
|
1785
2329
|
// src/claim.ts
|
|
1786
2330
|
import { hex as hex10 } from "@scure/base";
|
|
1787
2331
|
import { ripemd160 } from "@noble/hashes/legacy.js";
|
|
@@ -1824,6 +2368,15 @@ async function awaitRfqResolution(transport, rfqId, options = {}) {
|
|
|
1824
2368
|
await sleep(pollMs);
|
|
1825
2369
|
}
|
|
1826
2370
|
}
|
|
2371
|
+
var walletOperator = (wallet) => {
|
|
2372
|
+
let broadcaster;
|
|
2373
|
+
const broadcasting = () => broadcaster ??= wallet.getArkadeBroadcaster();
|
|
2374
|
+
return {
|
|
2375
|
+
getInfo: () => wallet.getArkadeInfo({ requireLive: true }),
|
|
2376
|
+
submitTx: async (...args) => (await broadcasting()).submitTx(...args),
|
|
2377
|
+
finalizeTx: async (...args) => (await broadcasting()).finalizeTx(...args)
|
|
2378
|
+
};
|
|
2379
|
+
};
|
|
1827
2380
|
var LockupNeedsRecoveryError = class extends Error {
|
|
1828
2381
|
name = "LockupNeedsRecoveryError";
|
|
1829
2382
|
reason = "needs_recovery";
|
|
@@ -1835,11 +2388,10 @@ var LockupNeedsRecoveryError = class extends Error {
|
|
|
1835
2388
|
* into one settlement with no CLTV awareness, so an early attempt can fail
|
|
1836
2389
|
* the whole batch — including unrelated outputs that were otherwise fine.
|
|
1837
2390
|
*
|
|
1838
|
-
* Exposed as a value, not only inside the message, so a caller can
|
|
1839
|
-
*
|
|
1840
|
-
*
|
|
1841
|
-
*
|
|
1842
|
-
* not an exact alarm.
|
|
2391
|
+
* Exposed as a value, not only inside the message, so a caller can skip
|
|
2392
|
+
* pre-CLTV recoverable outputs without parsing prose. Seconds-based
|
|
2393
|
+
* locktimes mature against the chain tip's timestamp rather than wall
|
|
2394
|
+
* clock, so treat this as a floor to wait past, not an exact alarm.
|
|
1843
2395
|
*/
|
|
1844
2396
|
recoverableAfter;
|
|
1845
2397
|
constructor(outpoints, recoverableAfter) {
|
|
@@ -1900,7 +2452,7 @@ async function readLockupFate(indexer, input) {
|
|
|
1900
2452
|
if (vtxo.spentBy)
|
|
1901
2453
|
spentBy.set(vtxo.spentBy, {
|
|
1902
2454
|
checkpointTxid: vtxo.spentBy,
|
|
1903
|
-
|
|
2455
|
+
txid: vtxo.arkTxId
|
|
1904
2456
|
});
|
|
1905
2457
|
else everySpendNamed = false;
|
|
1906
2458
|
}
|
|
@@ -1929,32 +2481,32 @@ async function readLockupFate(indexer, input) {
|
|
|
1929
2481
|
}
|
|
1930
2482
|
return everySpendNamed && observed.size === spentBy.size ? { fate: "returned", spends } : { fate: "unknown" };
|
|
1931
2483
|
}
|
|
1932
|
-
async function pushRefundWithoutReceiver(
|
|
2484
|
+
async function pushRefundWithoutReceiver(operator, input) {
|
|
1933
2485
|
if (input.vtxos.length === 0) throw new Error("nothing to refund: no funded outputs");
|
|
1934
2486
|
const swept = input.vtxos.filter((vtxo) => vtxo.recoverable);
|
|
1935
2487
|
if (swept.length > 0) {
|
|
1936
2488
|
throw new LockupNeedsRecoveryError(
|
|
1937
2489
|
swept.map((vtxo) => `${vtxo.txid}:${vtxo.vout}`),
|
|
1938
|
-
input.
|
|
2490
|
+
input.contract.options.refundLocktime
|
|
1939
2491
|
);
|
|
1940
2492
|
}
|
|
1941
|
-
const refundPkScript = input.refundPkScript ?? input.
|
|
2493
|
+
const refundPkScript = input.refundPkScript ?? input.contract.options.nonInteractiveParameters?.senderPkScript;
|
|
1942
2494
|
if (!refundPkScript) {
|
|
1943
2495
|
throw new Error(
|
|
1944
2496
|
"no refund destination: the contract carries no emulator covenant suite, so pass refundPkScript explicitly"
|
|
1945
2497
|
);
|
|
1946
2498
|
}
|
|
1947
|
-
const info = await
|
|
1948
|
-
let
|
|
2499
|
+
const info = await operator.getInfo();
|
|
2500
|
+
let operatorUnrollScript;
|
|
1949
2501
|
try {
|
|
1950
|
-
|
|
2502
|
+
operatorUnrollScript = CSVMultisigTapscript.decode(hex9.decode(info.checkpointTapscript));
|
|
1951
2503
|
} catch {
|
|
1952
|
-
throw new Error("invalid checkpointTapscript from the
|
|
2504
|
+
throw new Error("invalid checkpointTapscript from the operator");
|
|
1953
2505
|
}
|
|
1954
|
-
const leaf = input.
|
|
1955
|
-
const tapTree = input.
|
|
2506
|
+
const leaf = input.contract.refundWithoutReceiver();
|
|
2507
|
+
const tapTree = input.contract.encode();
|
|
1956
2508
|
const amount = input.vtxos.reduce((sum, vtxo) => sum + vtxo.value, 0);
|
|
1957
|
-
const { arkTx, checkpoints } = buildOffchainTx(
|
|
2509
|
+
const { arkTx: tx, checkpoints } = buildOffchainTx(
|
|
1958
2510
|
input.vtxos.map((vtxo) => ({
|
|
1959
2511
|
txid: vtxo.txid,
|
|
1960
2512
|
vout: vtxo.vout,
|
|
@@ -1963,14 +2515,14 @@ async function pushRefundWithoutReceiver(ark, input) {
|
|
|
1963
2515
|
tapTree
|
|
1964
2516
|
})),
|
|
1965
2517
|
[{ script: refundPkScript, amount: BigInt(amount) }],
|
|
1966
|
-
|
|
2518
|
+
operatorUnrollScript
|
|
1967
2519
|
);
|
|
1968
|
-
const
|
|
1969
|
-
const submitted = await
|
|
1970
|
-
base643.encode(
|
|
2520
|
+
const signedTx = await input.sender.sign(tx);
|
|
2521
|
+
const submitted = await operator.submitTx(
|
|
2522
|
+
base643.encode(signedTx.toPSBT()),
|
|
1971
2523
|
checkpoints.map((c) => base643.encode(c.toPSBT()))
|
|
1972
2524
|
);
|
|
1973
|
-
assertSubmittedArkTxid(submitted,
|
|
2525
|
+
assertSubmittedArkTxid(submitted, signedTx, "refundWithoutReceiver");
|
|
1974
2526
|
const matched = matchServerCheckpoints(
|
|
1975
2527
|
submitted.signedCheckpointTxs,
|
|
1976
2528
|
checkpoints,
|
|
@@ -1981,11 +2533,11 @@ async function pushRefundWithoutReceiver(ark, input) {
|
|
|
1981
2533
|
async ({ server }) => base643.encode((await input.sender.sign(server, [0])).toPSBT())
|
|
1982
2534
|
)
|
|
1983
2535
|
);
|
|
1984
|
-
await
|
|
1985
|
-
return {
|
|
2536
|
+
await operator.finalizeTx(submitted.arkTxid, finalCheckpoints);
|
|
2537
|
+
return { txid: submitted.arkTxid, amount };
|
|
1986
2538
|
}
|
|
1987
2539
|
var REFUND_MTP_LAG_SECONDS = 2 * 60 * 60;
|
|
1988
|
-
async function refundIfUnresolved(transport,
|
|
2540
|
+
async function refundIfUnresolved(transport, operator, indexer, input) {
|
|
1989
2541
|
const pollMs = input.pollMs ?? 5e3;
|
|
1990
2542
|
const now = input.now ?? (() => Math.floor(Date.now() / 1e3));
|
|
1991
2543
|
const attemptDeadline = input.attemptDeadline ?? input.refundLocktime + REFUND_MTP_LAG_SECONDS;
|
|
@@ -1996,7 +2548,7 @@ async function refundIfUnresolved(transport, ark, indexer, input) {
|
|
|
1996
2548
|
let fate = { fate: "unknown" };
|
|
1997
2549
|
try {
|
|
1998
2550
|
fate = await readLockupFate(indexer, {
|
|
1999
|
-
swapPkScript: input.
|
|
2551
|
+
swapPkScript: input.contract.pkScript,
|
|
2000
2552
|
paymentHash: input.paymentHash
|
|
2001
2553
|
});
|
|
2002
2554
|
} catch {
|
|
@@ -2008,11 +2560,11 @@ async function refundIfUnresolved(transport, ark, indexer, input) {
|
|
|
2008
2560
|
status
|
|
2009
2561
|
};
|
|
2010
2562
|
}
|
|
2011
|
-
const vtxos = await findLockupVtxos(indexer, input.
|
|
2563
|
+
const vtxos = await findLockupVtxos(indexer, input.contract.pkScript);
|
|
2012
2564
|
if (vtxos.length === 0) return { outcome: "nothing_to_refund", status };
|
|
2013
2565
|
try {
|
|
2014
|
-
const pushed = await pushRefundWithoutReceiver(
|
|
2015
|
-
|
|
2566
|
+
const pushed = await pushRefundWithoutReceiver(operator, {
|
|
2567
|
+
contract: input.contract,
|
|
2016
2568
|
sender: input.sender,
|
|
2017
2569
|
vtxos,
|
|
2018
2570
|
refundPkScript: input.refundPkScript
|
|
@@ -2055,13 +2607,13 @@ var assertFiniteAmount = (value, reason, label) => {
|
|
|
2055
2607
|
error.reason = reason;
|
|
2056
2608
|
throw error;
|
|
2057
2609
|
};
|
|
2058
|
-
async function pushClaim(
|
|
2610
|
+
async function pushClaim(operator, input) {
|
|
2059
2611
|
if (input.vtxos.length === 0) throw new Error("nothing to claim: no funded outputs");
|
|
2060
2612
|
const swept = input.vtxos.filter((vtxo) => vtxo.recoverable);
|
|
2061
2613
|
if (swept.length > 0) {
|
|
2062
2614
|
throw new LockupNeedsRecoveryError(
|
|
2063
2615
|
swept.map((vtxo) => `${vtxo.txid}:${vtxo.vout}`),
|
|
2064
|
-
input.
|
|
2616
|
+
input.contract.options.refundLocktime
|
|
2065
2617
|
);
|
|
2066
2618
|
}
|
|
2067
2619
|
const locked = input.vtxos.reduce((sum, vtxo) => sum + vtxo.value, 0);
|
|
@@ -2072,22 +2624,22 @@ async function pushClaim(ark, input) {
|
|
|
2072
2624
|
throw new LockupAmountMismatchError(input.expectedAmount, locked);
|
|
2073
2625
|
}
|
|
2074
2626
|
}
|
|
2075
|
-
const committed = input.
|
|
2627
|
+
const committed = input.contract.options.preimageHash;
|
|
2076
2628
|
if (hex10.encode(ripemd160(sha2563(input.preimage))) !== hex10.encode(committed)) {
|
|
2077
2629
|
throw new Error("preimage does not match the covenant's payment hash");
|
|
2078
2630
|
}
|
|
2079
|
-
const info = await
|
|
2080
|
-
let
|
|
2631
|
+
const info = await operator.getInfo();
|
|
2632
|
+
let operatorUnrollScript;
|
|
2081
2633
|
try {
|
|
2082
|
-
|
|
2634
|
+
operatorUnrollScript = CSVMultisigTapscript2.decode(hex10.decode(info.checkpointTapscript));
|
|
2083
2635
|
} catch {
|
|
2084
|
-
throw new Error("invalid checkpointTapscript from the
|
|
2636
|
+
throw new Error("invalid checkpointTapscript from the operator");
|
|
2085
2637
|
}
|
|
2086
|
-
const leaf = input.
|
|
2087
|
-
const tapTree = input.
|
|
2088
|
-
const
|
|
2638
|
+
const leaf = input.contract.claim();
|
|
2639
|
+
const tapTree = input.contract.encode();
|
|
2640
|
+
const txid = await signAndSubmitOffchainTx({
|
|
2089
2641
|
identity: claimWithPreimageIdentity(input.receiver, input.preimage),
|
|
2090
|
-
provider:
|
|
2642
|
+
provider: operator,
|
|
2091
2643
|
inputs: input.vtxos.map((vtxo) => ({
|
|
2092
2644
|
txid: vtxo.txid,
|
|
2093
2645
|
vout: vtxo.vout,
|
|
@@ -2098,10 +2650,10 @@ async function pushClaim(ark, input) {
|
|
|
2098
2650
|
// One aggregate output: unlike the covenant refund, this leaf inspects
|
|
2099
2651
|
// nothing about the output set.
|
|
2100
2652
|
outputs: [{ script: input.destinationPkScript, amount: BigInt(locked) }],
|
|
2101
|
-
serverUnrollScript,
|
|
2102
|
-
verifyServerSignatures: { serverPubkey: input.
|
|
2653
|
+
serverUnrollScript: operatorUnrollScript,
|
|
2654
|
+
verifyServerSignatures: { serverPubkey: input.contract.options.server }
|
|
2103
2655
|
});
|
|
2104
|
-
return {
|
|
2656
|
+
return { txid, amount: locked };
|
|
2105
2657
|
}
|
|
2106
2658
|
async function awaitLockupFunding(indexer, swapPkScript, options = {}) {
|
|
2107
2659
|
const pollMs = options.pollMs ?? 5e3;
|
|
@@ -2116,13 +2668,13 @@ async function awaitLockupFunding(indexer, swapPkScript, options = {}) {
|
|
|
2116
2668
|
await sleep2(pollMs);
|
|
2117
2669
|
}
|
|
2118
2670
|
}
|
|
2119
|
-
async function claimReceiveLockup(indexer,
|
|
2671
|
+
async function claimReceiveLockup(indexer, operator, input) {
|
|
2120
2672
|
const vtxos = await awaitLockupFunding(indexer, input.swapPkScript, {
|
|
2121
2673
|
pollMs: input.pollMs,
|
|
2122
2674
|
deadline: input.deadline
|
|
2123
2675
|
});
|
|
2124
|
-
return pushClaim(
|
|
2125
|
-
|
|
2676
|
+
return pushClaim(operator, {
|
|
2677
|
+
contract: input.contract,
|
|
2126
2678
|
receiver: input.receiver,
|
|
2127
2679
|
preimage: input.preimage,
|
|
2128
2680
|
vtxos,
|
|
@@ -2177,8 +2729,8 @@ async function senderIdentityForSwapRecord(wallet, record) {
|
|
|
2177
2729
|
// src/arkadeRefunder.ts
|
|
2178
2730
|
function arkadeRefunder(deps) {
|
|
2179
2731
|
return async (swap) => {
|
|
2180
|
-
const
|
|
2181
|
-
if (!
|
|
2732
|
+
const contract = swap.lockup?.script;
|
|
2733
|
+
if (!contract) {
|
|
2182
2734
|
throw new Error(
|
|
2183
2735
|
`swap ${swap.rfqId} carries no lockup covenant, so its refund cannot be built`
|
|
2184
2736
|
);
|
|
@@ -2193,7 +2745,7 @@ function arkadeRefunder(deps) {
|
|
|
2193
2745
|
);
|
|
2194
2746
|
}
|
|
2195
2747
|
const sender = await senderIdentityForSwapRecord(deps.wallet, rfqSignerOf(record) ?? {});
|
|
2196
|
-
return pushRefundWithoutReceiver(deps.
|
|
2748
|
+
return pushRefundWithoutReceiver(deps.operator, { contract, sender, vtxos });
|
|
2197
2749
|
};
|
|
2198
2750
|
}
|
|
2199
2751
|
|
|
@@ -2214,6 +2766,14 @@ function nextOnchainAction(input) {
|
|
|
2214
2766
|
return input.htlcLocktime - input.now >= ONCHAIN_CLAIM_MARGIN_SECONDS ? "claim" : "claim_window_closed";
|
|
2215
2767
|
}
|
|
2216
2768
|
}
|
|
2769
|
+
var RFQ_CONFIGURATION_REFUSAL = {
|
|
2770
|
+
noClaimLockupCallback: "no claimLockup callback is wired, so this wallet cannot claim the lockup",
|
|
2771
|
+
noCallbacksForClaim: "no callbacks are wired, so this wallet cannot claim the lockup",
|
|
2772
|
+
noClaimOnchainCallback: "no claimOnchain callback is wired, so this wallet cannot claim the L1 fill",
|
|
2773
|
+
autoActionsDisabled: "automatic actions are disabled, so this wallet will not push the refund",
|
|
2774
|
+
noCallbacksForRefund: "no callbacks are wired, so this wallet cannot push the refund"
|
|
2775
|
+
};
|
|
2776
|
+
var RFQ_CONFIGURATION_REFUSALS = Object.values(RFQ_CONFIGURATION_REFUSAL);
|
|
2217
2777
|
var RfqSwapOriginRequired = class extends Error {
|
|
2218
2778
|
/** The swap that could not be admitted. */
|
|
2219
2779
|
rfqId;
|
|
@@ -2264,20 +2824,6 @@ var RfqSwapManager = class {
|
|
|
2264
2824
|
* sign may well have been restored.
|
|
2265
2825
|
*/
|
|
2266
2826
|
refundRefused = /* @__PURE__ */ new Set();
|
|
2267
|
-
/**
|
|
2268
|
-
* The last error a receive swap's claim callback threw, by rfqId.
|
|
2269
|
-
*
|
|
2270
|
-
* Kept only to tell two terminal outcomes apart once the claim window
|
|
2271
|
-
* shuts: a swap whose claim was attempted and kept failing ends `failed`
|
|
2272
|
-
* with that reason, while one that simply never became claimable ends
|
|
2273
|
-
* `refunded`. Without it a broken claim callback would resolve a caller's
|
|
2274
|
-
* {@link waitForSwapCompletion} as an ordinary unwind.
|
|
2275
|
-
*
|
|
2276
|
-
* Process-local, like {@link refundRefused}: after a restart the same swap
|
|
2277
|
-
* ends `refunded` instead, which costs the caller a reason and nothing else
|
|
2278
|
-
* — every throw was already reported through `onSwapFailed` as it happened.
|
|
2279
|
-
*/
|
|
2280
|
-
lastClaimError = /* @__PURE__ */ new Map();
|
|
2281
2827
|
/**
|
|
2282
2828
|
* The lockup outpoints a receive swap's claim callback has already been
|
|
2283
2829
|
* handed, by rfqId.
|
|
@@ -2589,6 +3135,19 @@ var RfqSwapManager = class {
|
|
|
2589
3135
|
async getPendingSwaps() {
|
|
2590
3136
|
return [...this.monitored.values()];
|
|
2591
3137
|
}
|
|
3138
|
+
/**
|
|
3139
|
+
* Every swap this manager holds — {@link getPendingSwaps} plus the ones
|
|
3140
|
+
* that already ended. The two sets are disjoint: a swap leaves `monitored`
|
|
3141
|
+
* as it enters `finished`.
|
|
3142
|
+
*
|
|
3143
|
+
* The finished half is what this process has seen, which after
|
|
3144
|
+
* {@link restoreFromRepository} is the stored history minus what retention
|
|
3145
|
+
* pruned. A manager that has restored nothing answers with the live swaps
|
|
3146
|
+
* alone.
|
|
3147
|
+
*/
|
|
3148
|
+
async getAllSwaps() {
|
|
3149
|
+
return [...this.monitored.values(), ...this.finished.values()];
|
|
3150
|
+
}
|
|
2592
3151
|
async hasSwap(rfqId) {
|
|
2593
3152
|
return this.monitored.has(rfqId);
|
|
2594
3153
|
}
|
|
@@ -2658,7 +3217,6 @@ var RfqSwapManager = class {
|
|
|
2658
3217
|
if (swap) this.byLockupScript.delete(hex11.encode(swap.lockupPkScript));
|
|
2659
3218
|
this.monitored.delete(rfqId);
|
|
2660
3219
|
this.refundRefused.delete(rfqId);
|
|
2661
|
-
this.lastClaimError.delete(rfqId);
|
|
2662
3220
|
this.claimedOutpoints.delete(rfqId);
|
|
2663
3221
|
}
|
|
2664
3222
|
/**
|
|
@@ -2867,14 +3425,14 @@ var RfqSwapManager = class {
|
|
|
2867
3425
|
return this.claimIfFunded(swap, vtxos);
|
|
2868
3426
|
}
|
|
2869
3427
|
if (now < swap.refundLocktime + REFUND_MTP_LAG_SECONDS) {
|
|
2870
|
-
if (swap.
|
|
3428
|
+
if (swap.claimTxid) return;
|
|
2871
3429
|
return this.block(
|
|
2872
3430
|
swap,
|
|
2873
3431
|
"the claim window closed with the lockup unclaimed \u2014 only the solver can act now"
|
|
2874
3432
|
);
|
|
2875
3433
|
}
|
|
2876
|
-
const failure =
|
|
2877
|
-
if (failure && !swap.
|
|
3434
|
+
const failure = swap.claimFailure;
|
|
3435
|
+
if (failure && !swap.claimTxid) {
|
|
2878
3436
|
return this.fail(swap, new Error(failure));
|
|
2879
3437
|
}
|
|
2880
3438
|
this.setState(swap, "refunded");
|
|
@@ -2889,7 +3447,7 @@ var RfqSwapManager = class {
|
|
|
2889
3447
|
*/
|
|
2890
3448
|
async claimIfFunded(swap, vtxos) {
|
|
2891
3449
|
if (vtxos.length === 0) return this.unblock(swap);
|
|
2892
|
-
const partiallyClaimed = swap.
|
|
3450
|
+
const partiallyClaimed = swap.claimTxid !== void 0;
|
|
2893
3451
|
if (partiallyClaimed && !this.hasUnclaimedOutpoint(swap.rfqId, vtxos)) {
|
|
2894
3452
|
return;
|
|
2895
3453
|
}
|
|
@@ -2911,21 +3469,22 @@ var RfqSwapManager = class {
|
|
|
2911
3469
|
if (!this.callbacks || !this.callbacks.claimLockup) {
|
|
2912
3470
|
return this.block(
|
|
2913
3471
|
swap,
|
|
2914
|
-
this.callbacks ?
|
|
3472
|
+
this.callbacks ? RFQ_CONFIGURATION_REFUSAL.noClaimLockupCallback : RFQ_CONFIGURATION_REFUSAL.noCallbacksForClaim
|
|
2915
3473
|
);
|
|
2916
3474
|
}
|
|
2917
3475
|
this.setState(swap, "claimable");
|
|
2918
3476
|
if (!this.config.enableAutoActions) return;
|
|
2919
3477
|
try {
|
|
2920
|
-
const {
|
|
2921
|
-
|
|
3478
|
+
const { txid } = await this.callbacks.claimLockup(swap, vtxos, { partiallyClaimed });
|
|
3479
|
+
delete swap.claimFailure;
|
|
2922
3480
|
this.rememberClaimed(swap.rfqId, vtxos);
|
|
2923
|
-
swap.
|
|
3481
|
+
swap.claimTxid = txid;
|
|
2924
3482
|
this.touch(swap);
|
|
2925
3483
|
this.setState(swap, "claimed");
|
|
2926
3484
|
this.emitAction(swap, "claimLockup");
|
|
2927
3485
|
} catch (error) {
|
|
2928
|
-
|
|
3486
|
+
swap.claimFailure = errorMessage(error);
|
|
3487
|
+
this.touch(swap);
|
|
2929
3488
|
this.emitFailed(swap, error);
|
|
2930
3489
|
}
|
|
2931
3490
|
}
|
|
@@ -2962,10 +3521,7 @@ var RfqSwapManager = class {
|
|
|
2962
3521
|
if (action === "claim" && phase.phase === "claimable") {
|
|
2963
3522
|
if (swap.claimTxid) return "continue";
|
|
2964
3523
|
if (this.callbacks && !this.callbacks.claimOnchain) {
|
|
2965
|
-
this.block(
|
|
2966
|
-
swap,
|
|
2967
|
-
"no claimOnchain callback is wired, so this wallet cannot claim the L1 fill"
|
|
2968
|
-
);
|
|
3524
|
+
this.block(swap, RFQ_CONFIGURATION_REFUSAL.noClaimOnchainCallback);
|
|
2969
3525
|
return "handled";
|
|
2970
3526
|
}
|
|
2971
3527
|
this.setOnchainState(swap, "claimable");
|
|
@@ -3005,14 +3561,14 @@ var RfqSwapManager = class {
|
|
|
3005
3561
|
if (!this.config.enableAutoActions || !this.callbacks) {
|
|
3006
3562
|
return this.block(
|
|
3007
3563
|
swap,
|
|
3008
|
-
this.callbacks ?
|
|
3564
|
+
this.callbacks ? RFQ_CONFIGURATION_REFUSAL.autoActionsDisabled : RFQ_CONFIGURATION_REFUSAL.noCallbacksForRefund
|
|
3009
3565
|
);
|
|
3010
3566
|
}
|
|
3011
3567
|
this.unblock(swap);
|
|
3012
3568
|
try {
|
|
3013
3569
|
const pushed = await this.callbacks.refundArkade(swap);
|
|
3014
3570
|
if (pushed) {
|
|
3015
|
-
swap.
|
|
3571
|
+
swap.refundTxid = pushed.txid;
|
|
3016
3572
|
this.touch(swap);
|
|
3017
3573
|
}
|
|
3018
3574
|
this.setState(swap, "refunded");
|
|
@@ -3085,7 +3641,7 @@ var RfqSwapManager = class {
|
|
|
3085
3641
|
/**
|
|
3086
3642
|
* Record which ark transactions ended the lockup.
|
|
3087
3643
|
*
|
|
3088
|
-
* Only the ones the indexer actually named: `LockupSpend.
|
|
3644
|
+
* Only the ones the indexer actually named: `LockupSpend.txid` is
|
|
3089
3645
|
* optional, and a checkpoint txid is not what history correlates on — a
|
|
3090
3646
|
* record carrying one would name a transaction the wallet's own activity
|
|
3091
3647
|
* never shows. Fewer txids is the right failure here.
|
|
@@ -3095,9 +3651,9 @@ var RfqSwapManager = class {
|
|
|
3095
3651
|
* verdict once.
|
|
3096
3652
|
*/
|
|
3097
3653
|
stampLockupSpends(swap, spends) {
|
|
3098
|
-
const
|
|
3099
|
-
if (
|
|
3100
|
-
swap.
|
|
3654
|
+
const txids = spends.map((spend) => spend.txid).filter((txid) => txid !== void 0);
|
|
3655
|
+
if (txids.length === 0) return;
|
|
3656
|
+
swap.lockupSpendTxids = txids;
|
|
3101
3657
|
this.touch(swap);
|
|
3102
3658
|
}
|
|
3103
3659
|
touch(swap) {
|
|
@@ -3108,6 +3664,7 @@ var RfqSwapManager = class {
|
|
|
3108
3664
|
if (swap.state === state) return;
|
|
3109
3665
|
const previous = swap.state;
|
|
3110
3666
|
if (previous === "needs_counterparty") delete swap.blockedReason;
|
|
3667
|
+
if (isRfqSwapTerminal(state)) delete swap.claimFailure;
|
|
3111
3668
|
swap.state = state;
|
|
3112
3669
|
this.touch(swap);
|
|
3113
3670
|
notify(this.swapUpdateListeners, (listener) => listener(swap, previous));
|
|
@@ -3178,10 +3735,9 @@ var RfqSwapManager = class {
|
|
|
3178
3735
|
/**
|
|
3179
3736
|
* Drop a terminal swap from monitoring and report it exactly once.
|
|
3180
3737
|
*
|
|
3181
|
-
* `onSwapCompleted` and `onSwapFailed` are mutually exclusive here
|
|
3182
|
-
*
|
|
3183
|
-
*
|
|
3184
|
-
* also fires on failure is a trap worth not inheriting.
|
|
3738
|
+
* `onSwapCompleted` and `onSwapFailed` are mutually exclusive here: a
|
|
3739
|
+
* listener named "completed" that also fires on failure is a trap, so a
|
|
3740
|
+
* swap that leaves monitoring reports through exactly one of them.
|
|
3185
3741
|
*/
|
|
3186
3742
|
finalize(swap) {
|
|
3187
3743
|
if (!this.monitored.has(swap.rfqId)) return;
|
|
@@ -3217,7 +3773,7 @@ var traderClaimTxid = (swap) => {
|
|
|
3217
3773
|
case "onchain_send":
|
|
3218
3774
|
return swap.claimTxid;
|
|
3219
3775
|
case "lightning_receive":
|
|
3220
|
-
return swap.
|
|
3776
|
+
return swap.claimTxid;
|
|
3221
3777
|
default:
|
|
3222
3778
|
return void 0;
|
|
3223
3779
|
}
|
|
@@ -3227,7 +3783,7 @@ var outcomeOf = (swap) => {
|
|
|
3227
3783
|
const lostReceive = swap.kind === "lightning_receive" && swap.state === "refunded";
|
|
3228
3784
|
return {
|
|
3229
3785
|
state: swap.state,
|
|
3230
|
-
txid: lostReceive ? swap.
|
|
3786
|
+
txid: lostReceive ? swap.refundTxid : traderClaimTxid(swap) ?? swap.refundTxid
|
|
3231
3787
|
};
|
|
3232
3788
|
};
|
|
3233
3789
|
var errorMessage = (error) => error instanceof Error ? error.message : String(error);
|
|
@@ -3238,27 +3794,190 @@ import {
|
|
|
3238
3794
|
ArkAddress as ArkAddress4
|
|
3239
3795
|
} from "@arkade-os/sdk";
|
|
3240
3796
|
import { hex as hex12 } from "@scure/base";
|
|
3797
|
+
|
|
3798
|
+
// src/client/corridors/lightning.ts
|
|
3799
|
+
import { invoiceTarget as invoiceTarget3 } from "@arkade-os/sdk";
|
|
3800
|
+
var INVOICE_HRPS = [
|
|
3801
|
+
["lnbcrt", ["regtest"]],
|
|
3802
|
+
["lntbs", ["signet", "mutinynet"]],
|
|
3803
|
+
["lnbc", ["bitcoin"]],
|
|
3804
|
+
["lntb", ["testnet"]]
|
|
3805
|
+
];
|
|
3806
|
+
var networksOfInvoiceHrp = (raw) => {
|
|
3807
|
+
const lower = raw.toLowerCase();
|
|
3808
|
+
return INVOICE_HRPS.find(([hrp]) => lower.startsWith(hrp))?.[1];
|
|
3809
|
+
};
|
|
3810
|
+
var PAYMENT_HASH = /^[0-9a-f]{64}$/;
|
|
3811
|
+
var LIGHTNING_DRIVE = {
|
|
3812
|
+
give: {
|
|
3813
|
+
lockups: [{ covenant: "arkade_lockup", owner: "solver", deadline: "refund_locktime" }],
|
|
3814
|
+
actions: ["claimLockup"],
|
|
3815
|
+
seams: ["indexer"]
|
|
3816
|
+
},
|
|
3817
|
+
take: {
|
|
3818
|
+
lockups: [{ covenant: "arkade_lockup", owner: "trader", deadline: "refund_locktime" }],
|
|
3819
|
+
actions: ["refundArkade"],
|
|
3820
|
+
seams: ["indexer"]
|
|
3821
|
+
}
|
|
3822
|
+
};
|
|
3823
|
+
var lightningCorridor = Object.assign(
|
|
3824
|
+
(deps) => ({
|
|
3825
|
+
corridor: "lightning",
|
|
3826
|
+
deps,
|
|
3827
|
+
drive: LIGHTNING_DRIVE,
|
|
3828
|
+
matches(raw) {
|
|
3829
|
+
const invoice = invoiceTarget3(raw);
|
|
3830
|
+
if (invoice === void 0) return void 0;
|
|
3831
|
+
const networks = networksOfInvoiceHrp(invoice);
|
|
3832
|
+
if (networks === void 0) {
|
|
3833
|
+
return { refused: "this invoice's network prefix names no network this SDK knows" };
|
|
3834
|
+
}
|
|
3835
|
+
if (!networks.includes(deps.networkName)) {
|
|
3836
|
+
return {
|
|
3837
|
+
refused: `this is a ${networks.join(" or ")} invoice and the wallet is on ${deps.networkName}`
|
|
3838
|
+
};
|
|
3839
|
+
}
|
|
3840
|
+
let facts;
|
|
3841
|
+
try {
|
|
3842
|
+
facts = deps.decode(invoice);
|
|
3843
|
+
if (!PAYMENT_HASH.test(facts.paymentHash)) {
|
|
3844
|
+
return { refused: "the decoded invoice carries no usable payment hash" };
|
|
3845
|
+
}
|
|
3846
|
+
} catch (error) {
|
|
3847
|
+
return {
|
|
3848
|
+
refused: `the invoice did not decode: ${error instanceof Error ? error.message : String(error)}`
|
|
3849
|
+
};
|
|
3850
|
+
}
|
|
3851
|
+
if (!Number.isSafeInteger(facts.expiresAt)) {
|
|
3852
|
+
return { refused: "the decoded invoice carries no usable expiry" };
|
|
3853
|
+
}
|
|
3854
|
+
const now = Math.floor(Date.now() / 1e3);
|
|
3855
|
+
if (facts.expiresAt <= now) {
|
|
3856
|
+
return { refused: `the invoice expired ${now - facts.expiresAt}s ago` };
|
|
3857
|
+
}
|
|
3858
|
+
if (!Number.isSafeInteger(facts.amountSats) || facts.amountSats < 0) {
|
|
3859
|
+
return { refused: "the decoded invoice carries no usable amount" };
|
|
3860
|
+
}
|
|
3861
|
+
if (facts.amountSats === 0) {
|
|
3862
|
+
return { refused: "the invoice names no amount, and a send route needs one" };
|
|
3863
|
+
}
|
|
3864
|
+
return {
|
|
3865
|
+
claimed: {
|
|
3866
|
+
kind: "invoice",
|
|
3867
|
+
bolt11: invoice,
|
|
3868
|
+
paymentHash: facts.paymentHash,
|
|
3869
|
+
amount: BigInt(facts.amountSats),
|
|
3870
|
+
expiresAt: facts.expiresAt
|
|
3871
|
+
}
|
|
3872
|
+
};
|
|
3873
|
+
}
|
|
3874
|
+
}),
|
|
3875
|
+
{ target: invoiceTarget3 }
|
|
3876
|
+
);
|
|
3877
|
+
|
|
3878
|
+
// src/client/corridors/onchain.ts
|
|
3879
|
+
import { btcTarget as btcTarget3 } from "@arkade-os/sdk";
|
|
3880
|
+
import * as btc2 from "@scure/btc-signer";
|
|
3881
|
+
var ONCHAIN_DRIVE = {
|
|
3882
|
+
take: {
|
|
3883
|
+
lockups: [
|
|
3884
|
+
{ covenant: "arkade_lockup", owner: "trader", deadline: "refund_locktime" },
|
|
3885
|
+
{ covenant: "onchain_htlc", owner: "solver", deadline: "htlc_refund_locktime" }
|
|
3886
|
+
],
|
|
3887
|
+
actions: ["claimOnchain", "refundArkade"],
|
|
3888
|
+
seams: ["indexer", "chain"]
|
|
3889
|
+
}
|
|
3890
|
+
};
|
|
3891
|
+
var onchainCorridor = Object.assign(
|
|
3892
|
+
(deps) => {
|
|
3893
|
+
const l1 = l1NetworkFromArk(deps.networkName);
|
|
3894
|
+
return {
|
|
3895
|
+
corridor: "onchain",
|
|
3896
|
+
deps,
|
|
3897
|
+
drive: ONCHAIN_DRIVE,
|
|
3898
|
+
matches(raw) {
|
|
3899
|
+
const target = btcTarget3(raw);
|
|
3900
|
+
if (target === void 0) return void 0;
|
|
3901
|
+
try {
|
|
3902
|
+
btc2.Address(L1_NETWORKS[l1]).decode(target);
|
|
3903
|
+
} catch {
|
|
3904
|
+
return { refused: `this is not a ${l1} address` };
|
|
3905
|
+
}
|
|
3906
|
+
return { claimed: { kind: "address", address: target } };
|
|
3907
|
+
}
|
|
3908
|
+
};
|
|
3909
|
+
},
|
|
3910
|
+
{ target: btcTarget3 }
|
|
3911
|
+
);
|
|
3912
|
+
|
|
3913
|
+
// src/client/outcome.ts
|
|
3914
|
+
var CORRIDOR_PASS = {
|
|
3915
|
+
lightning_send: LIGHTNING_DRIVE.take,
|
|
3916
|
+
lightning_receive: LIGHTNING_DRIVE.give,
|
|
3917
|
+
onchain_send: ONCHAIN_DRIVE.take
|
|
3918
|
+
};
|
|
3919
|
+
var LOCKUP_OWNER = {
|
|
3920
|
+
lightning_send: CORRIDOR_PASS.lightning_send.lockups[0].owner,
|
|
3921
|
+
lightning_receive: CORRIDOR_PASS.lightning_receive.lockups[0].owner,
|
|
3922
|
+
onchain_send: CORRIDOR_PASS.onchain_send.lockups[0].owner
|
|
3923
|
+
};
|
|
3924
|
+
var at = (outcome) => () => outcome;
|
|
3925
|
+
var CORRIDOR_OUTCOME = {
|
|
3926
|
+
trader: {
|
|
3927
|
+
pending: at("funded"),
|
|
3928
|
+
claimable: at("funded"),
|
|
3929
|
+
claimed: at("funded"),
|
|
3930
|
+
needs_counterparty: at("needs_recovery"),
|
|
3931
|
+
settled: (kind) => kind === "onchain_send" ? "claimed" : "paid",
|
|
3932
|
+
refunded: at("refunded"),
|
|
3933
|
+
failed: at("failed")
|
|
3934
|
+
},
|
|
3935
|
+
solver: {
|
|
3936
|
+
// The solver has funded nothing yet: the invoice is shown and unpaid,
|
|
3937
|
+
// which is what makes this `open` where the trader's side is `funded`.
|
|
3938
|
+
pending: at("open"),
|
|
3939
|
+
claimable: at("funded"),
|
|
3940
|
+
claimed: at("funded"),
|
|
3941
|
+
needs_counterparty: at("needs_recovery"),
|
|
3942
|
+
// The trader's OWN claim, matched by the hash and not by our txid — so
|
|
3943
|
+
// a claim that lands without us still counts.
|
|
3944
|
+
settled: at("claimed"),
|
|
3945
|
+
// The one inversion this whole module exists for: every non-claim leaf
|
|
3946
|
+
// of a receive covenant is the solver's, so a lockup spent by one is
|
|
3947
|
+
// the trader's incoming payment never arriving.
|
|
3948
|
+
refunded: at("lapsed"),
|
|
3949
|
+
failed: at("failed")
|
|
3950
|
+
}
|
|
3951
|
+
};
|
|
3952
|
+
var corridorOutcome = (kind, state) => CORRIDOR_OUTCOME[LOCKUP_OWNER[kind]][state](kind);
|
|
3953
|
+
var ACTIVITY_TOKEN = {
|
|
3954
|
+
accepted: "pending",
|
|
3955
|
+
funding: "pending",
|
|
3956
|
+
funded: "pending",
|
|
3957
|
+
open: "pending",
|
|
3958
|
+
// The drive's in-flight states have no user-visible phase distinct from
|
|
3959
|
+
// "pending"; `needs_recovery` is different in kind — the swap is BLOCKED —
|
|
3960
|
+
// and collapsing it here is the same deliberate choice v1 made, which the
|
|
3961
|
+
// opaque-token design permits because apps map tokens themselves.
|
|
3962
|
+
needs_recovery: "pending",
|
|
3963
|
+
refunding: "pending",
|
|
3964
|
+
cancelling: "pending",
|
|
3965
|
+
cancelled: "cancelled",
|
|
3966
|
+
filled: "settled",
|
|
3967
|
+
claimed: "settled",
|
|
3968
|
+
paid: "settled",
|
|
3969
|
+
refunded: "refunded",
|
|
3970
|
+
lapsed: "lost",
|
|
3971
|
+
failed: "failed"
|
|
3972
|
+
};
|
|
3973
|
+
|
|
3974
|
+
// src/activity.ts
|
|
3241
3975
|
var LABELS = {
|
|
3242
3976
|
lightning_send: "Lightning send",
|
|
3243
3977
|
lightning_receive: "Lightning receive",
|
|
3244
3978
|
onchain_send: "Onchain send"
|
|
3245
3979
|
};
|
|
3246
|
-
var
|
|
3247
|
-
pending: "pending",
|
|
3248
|
-
// `claimable` and `claimed` are both in-progress states with no
|
|
3249
|
-
// user-visible phase distinct from "pending". `needs_counterparty` is
|
|
3250
|
-
// different in kind — the swap is BLOCKED, not merely in flight, since no
|
|
3251
|
-
// unilateral trader move exists (see `RfqSwapState`). Collapsing it into
|
|
3252
|
-
// `pending` here is a deliberate choice the opaque-token design permits —
|
|
3253
|
-
// apps map tokens themselves — but a future reader weighing a `"blocked"`
|
|
3254
|
-
// or `"stuck"` token should know this was already considered.
|
|
3255
|
-
claimable: "pending",
|
|
3256
|
-
claimed: "pending",
|
|
3257
|
-
needs_counterparty: "pending",
|
|
3258
|
-
settled: "settled",
|
|
3259
|
-
refunded: "refunded",
|
|
3260
|
-
failed: "failed"
|
|
3261
|
-
};
|
|
3980
|
+
var outcomeToken = (kind, state) => ACTIVITY_TOKEN[corridorOutcome(kind, state)];
|
|
3262
3981
|
function swapActivityResolver(deps) {
|
|
3263
3982
|
let byTxid = /* @__PURE__ */ new Map();
|
|
3264
3983
|
return {
|
|
@@ -3277,13 +3996,17 @@ function swapActivityResolver(deps) {
|
|
|
3277
3996
|
const key = tx.key.arkTxid || tx.key.commitmentTxid || tx.key.boardingTxid;
|
|
3278
3997
|
const swap = key ? byTxid.get(key) : void 0;
|
|
3279
3998
|
if (!swap) return void 0;
|
|
3280
|
-
const lostReceive = swap.kind === "lightning_receive" && swap.state === "refunded";
|
|
3281
3999
|
return [
|
|
3282
4000
|
{
|
|
3283
4001
|
groupId: `swap:${swap.rfqId}`,
|
|
3284
4002
|
label: LABELS[swap.kind],
|
|
3285
4003
|
kind: "swap",
|
|
3286
|
-
|
|
4004
|
+
// `"lost"` for a receive leg that ended `refunded` comes out
|
|
4005
|
+
// of the translation rather than out of a branch here: that
|
|
4006
|
+
// leg has no trader-side refund, every non-claim leaf of its
|
|
4007
|
+
// covenant is the solver's, and the client's `lapsed` is
|
|
4008
|
+
// that fact.
|
|
4009
|
+
outcome: outcomeToken(swap.kind, swap.state),
|
|
3287
4010
|
metadata: { rfqId: swap.rfqId, swapKind: swap.kind }
|
|
3288
4011
|
}
|
|
3289
4012
|
];
|
|
@@ -3294,16 +4017,17 @@ async function rfqSwapActivityInputs(deps) {
|
|
|
3294
4017
|
const records = await deps.repository.getAllRfqSwaps();
|
|
3295
4018
|
return Promise.all(records.map((record) => activityInputOf(record, deps.indexer)));
|
|
3296
4019
|
}
|
|
3297
|
-
async function activityInputOf(
|
|
4020
|
+
async function activityInputOf(stored, indexer) {
|
|
4021
|
+
const record = normalizeRfqSwapRecord(stored);
|
|
3298
4022
|
const txids = /* @__PURE__ */ new Set();
|
|
3299
|
-
if (record.
|
|
3300
|
-
if (record.
|
|
4023
|
+
if (record.fundingTxid) txids.add(record.fundingTxid);
|
|
4024
|
+
if (record.refundTxid) txids.add(record.refundTxid);
|
|
3301
4025
|
const handler = rfqCorridorHandlers.getOrThrow(record.kind);
|
|
3302
4026
|
for (const txid of handler.activityTxids?.(record.profile) ?? []) txids.add(txid);
|
|
3303
|
-
for (const txid of record.
|
|
3304
|
-
const spendUnknown = isRfqSwapTerminal(record.state) && !record.
|
|
3305
|
-
if (indexer && (!record.
|
|
3306
|
-
for (const txid of await lockupTxids(indexer, record, !record.
|
|
4027
|
+
for (const txid of record.lockupSpendTxids ?? []) txids.add(txid);
|
|
4028
|
+
const spendUnknown = isRfqSwapTerminal(record.state) && !record.refundTxid && !record.lockupSpendTxids?.length;
|
|
4029
|
+
if (indexer && (!record.fundingTxid || spendUnknown)) {
|
|
4030
|
+
for (const txid of await lockupTxids(indexer, record, !record.fundingTxid)) {
|
|
3307
4031
|
txids.add(txid);
|
|
3308
4032
|
}
|
|
3309
4033
|
}
|
|
@@ -3328,6 +4052,346 @@ async function lockupTxids(indexer, record, wantFunding) {
|
|
|
3328
4052
|
return [];
|
|
3329
4053
|
}
|
|
3330
4054
|
}
|
|
4055
|
+
|
|
4056
|
+
// src/swapClient.ts
|
|
4057
|
+
import { hex as hex13 } from "@scure/base";
|
|
4058
|
+
import { ArkAddress as ArkAddress5, asset as asset2, contractSigner as contractSigner2 } from "@arkade-os/sdk";
|
|
4059
|
+
import { quoteOffer } from "@arkade-os/solver-discovery";
|
|
4060
|
+
import * as btc3 from "@scure/btc-signer";
|
|
4061
|
+
var sideCorridorsOf = (market) => {
|
|
4062
|
+
const read = (value) => value === "lightning" || value === "onchain" ? value : "arkade";
|
|
4063
|
+
const m = market;
|
|
4064
|
+
return { base: read(m.base_corridor), quote: read(m.quote_corridor) };
|
|
4065
|
+
};
|
|
4066
|
+
var resolveKind = (market, give) => {
|
|
4067
|
+
const corridors = sideCorridorsOf(market);
|
|
4068
|
+
const giveCorridor = corridors[give];
|
|
4069
|
+
const receiveCorridor = corridors[give === "base" ? "quote" : "base"];
|
|
4070
|
+
if (giveCorridor === "arkade" && receiveCorridor === "arkade") return "spot";
|
|
4071
|
+
if (receiveCorridor === "lightning") return "ln_send";
|
|
4072
|
+
if (giveCorridor === "lightning") return "ln_receive";
|
|
4073
|
+
if (receiveCorridor === "onchain") return "onchain_send";
|
|
4074
|
+
return "onchain_receive";
|
|
4075
|
+
};
|
|
4076
|
+
var need = (value, what, leg) => {
|
|
4077
|
+
if (value === void 0) throw new Error(`a ${leg} quote needs ${what}`);
|
|
4078
|
+
return value;
|
|
4079
|
+
};
|
|
4080
|
+
var payoutScriptFor = (address, payoutPubkey, network) => address !== void 0 ? l1ScriptForAddress(address, network) : btc3.p2tr(payoutPubkey, void 0, L1_NETWORKS[network]).script;
|
|
4081
|
+
var corridorAmount = (input, leg) => {
|
|
4082
|
+
const raw = need(input.amount, "an amount", leg);
|
|
4083
|
+
const amountOn = need(input.amountOn, "amountOn ('give' or 'receive')", leg);
|
|
4084
|
+
const amount = Number(raw);
|
|
4085
|
+
if (!Number.isSafeInteger(amount) || amount <= 0) {
|
|
4086
|
+
throw new Error(`a ${leg} amount must be a positive integer of sats, got ${String(raw)}`);
|
|
4087
|
+
}
|
|
4088
|
+
return { amount, amountSide: amountOn === "give" ? "from" : "to" };
|
|
4089
|
+
};
|
|
4090
|
+
function createSwapClient(deps) {
|
|
4091
|
+
const { wallet, repository, transportFor } = deps;
|
|
4092
|
+
const ark = deps.ark ?? walletOperator(wallet);
|
|
4093
|
+
let reader;
|
|
4094
|
+
const reading = () => reader ??= wallet.getArkadeReader();
|
|
4095
|
+
const indexer = deps.indexer ?? {
|
|
4096
|
+
getVtxos: async (opts) => {
|
|
4097
|
+
if (!opts)
|
|
4098
|
+
throw new Error("getVtxos on the swap indexer requires scripts or outpoints");
|
|
4099
|
+
return (await reading()).getVtxos(opts);
|
|
4100
|
+
},
|
|
4101
|
+
getVirtualTxs: async (...args) => (await reading()).getVirtualTxs(...args)
|
|
4102
|
+
};
|
|
4103
|
+
const managerDeps = { indexer, chain: deps.chain, repository };
|
|
4104
|
+
const manager = new RfqSwapManager(managerDeps);
|
|
4105
|
+
const listeners = /* @__PURE__ */ new Set();
|
|
4106
|
+
const notify2 = (swap) => {
|
|
4107
|
+
for (const listener of listeners) {
|
|
4108
|
+
try {
|
|
4109
|
+
listener(swap);
|
|
4110
|
+
} catch {
|
|
4111
|
+
}
|
|
4112
|
+
}
|
|
4113
|
+
};
|
|
4114
|
+
manager.onSwapUpdate((swap) => notify2({ family: "rfq", swap }));
|
|
4115
|
+
const claimLockup = async (swap, vtxos, { partiallyClaimed }) => {
|
|
4116
|
+
const record = await repository.getRfqSwap(swap.rfqId);
|
|
4117
|
+
if (!record) throw new Error(`rfq swap ${swap.rfqId} has no stored record to claim from`);
|
|
4118
|
+
const secret = rfqClaimSecretOf(record);
|
|
4119
|
+
if (!secret) throw new Error(`rfq swap ${swap.rfqId} carries no claim secret`);
|
|
4120
|
+
const script = swap.lockup?.script;
|
|
4121
|
+
if (!script) throw new Error(`rfq swap ${swap.rfqId} carries no lockup covenant`);
|
|
4122
|
+
const payoutAddress = rfqClaimDestinationOf(record);
|
|
4123
|
+
if (!payoutAddress) {
|
|
4124
|
+
throw new Error(`rfq swap ${swap.rfqId} carries no claim destination`);
|
|
4125
|
+
}
|
|
4126
|
+
return pushClaim(ark, {
|
|
4127
|
+
contract: script,
|
|
4128
|
+
receiver: await contractSigner2(wallet, secret.signingDescriptor),
|
|
4129
|
+
preimage: await preimageForSwapRecord(wallet, secret),
|
|
4130
|
+
vtxos,
|
|
4131
|
+
destinationPkScript: ArkAddress5.decode(payoutAddress).pkScript,
|
|
4132
|
+
expectedAmount: swap.expectedAmount,
|
|
4133
|
+
partiallyClaimed
|
|
4134
|
+
});
|
|
4135
|
+
};
|
|
4136
|
+
manager.setCallbacks({
|
|
4137
|
+
refundArkade: arkadeRefunder({ operator: ark, indexer, wallet, repository }),
|
|
4138
|
+
claimLockup,
|
|
4139
|
+
...deps.claimOnchain ? { claimOnchain: deps.claimOnchain } : {}
|
|
4140
|
+
});
|
|
4141
|
+
let watcher;
|
|
4142
|
+
const quote = async (market, input) => {
|
|
4143
|
+
const kind = resolveKind(market, input.give);
|
|
4144
|
+
switch (kind) {
|
|
4145
|
+
case "spot": {
|
|
4146
|
+
const raw = need(input.amount, "an amount", kind);
|
|
4147
|
+
const amountOn = need(input.amountOn, "amountOn ('give' or 'receive')", kind);
|
|
4148
|
+
const plan = await quoteOffer(market, {
|
|
4149
|
+
give: input.give,
|
|
4150
|
+
...amountOn === "give" ? { giveAmount: raw } : { wantAmount: raw },
|
|
4151
|
+
fetchImpl: deps.discovery.fetchImpl
|
|
4152
|
+
});
|
|
4153
|
+
return { kind, market, plan };
|
|
4154
|
+
}
|
|
4155
|
+
case "ln_send": {
|
|
4156
|
+
const invoice = need(input.invoice, "the invoice to pay", kind);
|
|
4157
|
+
const request = await requestLightningSend(wallet, transportFor(market), {
|
|
4158
|
+
invoice,
|
|
4159
|
+
emulatorPubkey: deps.emulatorPubkey
|
|
4160
|
+
});
|
|
4161
|
+
return { kind, market, request };
|
|
4162
|
+
}
|
|
4163
|
+
case "ln_receive": {
|
|
4164
|
+
const params = {
|
|
4165
|
+
...corridorAmount(input, kind),
|
|
4166
|
+
covclaimdPubkey: need(deps.covclaimdPubkey, "deps.covclaimdPubkey", kind),
|
|
4167
|
+
decodeInvoice: need(deps.decodeInvoice, "deps.decodeInvoice", kind),
|
|
4168
|
+
maxPayAmount: input.maxPayAmount,
|
|
4169
|
+
emulatorPubkey: deps.emulatorPubkey
|
|
4170
|
+
};
|
|
4171
|
+
const request = await requestLightningReceive(wallet, transportFor(market), params);
|
|
4172
|
+
return { kind, market, request, invoice: request.invoice };
|
|
4173
|
+
}
|
|
4174
|
+
case "onchain_send": {
|
|
4175
|
+
need(deps.chain, "deps.chain (L1 access)", kind);
|
|
4176
|
+
const payoutPubkey = need(input.payoutPubkey, "a payoutPubkey", kind);
|
|
4177
|
+
const params = {
|
|
4178
|
+
...corridorAmount(input, kind),
|
|
4179
|
+
payoutPubkey,
|
|
4180
|
+
preimage: input.preimage,
|
|
4181
|
+
emulatorPubkey: deps.emulatorPubkey
|
|
4182
|
+
};
|
|
4183
|
+
const request = await requestOnchainSend(wallet, transportFor(market), params);
|
|
4184
|
+
return {
|
|
4185
|
+
kind,
|
|
4186
|
+
market,
|
|
4187
|
+
request,
|
|
4188
|
+
payoutPkScript: payoutScriptFor(
|
|
4189
|
+
input.payoutAddress,
|
|
4190
|
+
payoutPubkey,
|
|
4191
|
+
request.l1Network
|
|
4192
|
+
)
|
|
4193
|
+
};
|
|
4194
|
+
}
|
|
4195
|
+
case "onchain_receive":
|
|
4196
|
+
throw new Error(
|
|
4197
|
+
"onchain->arkade is not driven by RfqSwapManager yet; quote it directly with requestOnchainReceive"
|
|
4198
|
+
);
|
|
4199
|
+
}
|
|
4200
|
+
};
|
|
4201
|
+
const admit = async (swap, origin) => {
|
|
4202
|
+
let announced = false;
|
|
4203
|
+
const heard = manager.onSwapUpdate((updated) => {
|
|
4204
|
+
if (updated.rfqId === swap.rfqId) announced = true;
|
|
4205
|
+
});
|
|
4206
|
+
try {
|
|
4207
|
+
await manager.addSwap(swap, origin);
|
|
4208
|
+
} finally {
|
|
4209
|
+
heard();
|
|
4210
|
+
}
|
|
4211
|
+
const unified = { family: "rfq", swap };
|
|
4212
|
+
if (!announced) notify2(unified);
|
|
4213
|
+
return unified;
|
|
4214
|
+
};
|
|
4215
|
+
const fundPersisted = async (swap, origin, funding) => {
|
|
4216
|
+
await repository.saveRfqSwap(createRfqSwapRecord(origin, swap));
|
|
4217
|
+
return wallet.send({ address: funding.address, amount: funding.amount });
|
|
4218
|
+
};
|
|
4219
|
+
const accept = async (accepted) => {
|
|
4220
|
+
const now = Math.floor(Date.now() / 1e3);
|
|
4221
|
+
switch (accepted.kind) {
|
|
4222
|
+
case "spot": {
|
|
4223
|
+
const { plan } = accepted;
|
|
4224
|
+
const depositIsBtc = plan.deposit.asset.id === BTC_ASSET_ID;
|
|
4225
|
+
const offer = await createOffer(wallet, {
|
|
4226
|
+
wantAmount: plan.receive.atomic,
|
|
4227
|
+
...plan.receive.asset.id === BTC_ASSET_ID ? { offerAsset: asset2.AssetId.fromString(plan.deposit.asset.id) } : { wantAsset: asset2.AssetId.fromString(plan.receive.asset.id) },
|
|
4228
|
+
emulatorPubkey: deps.emulatorPubkey
|
|
4229
|
+
});
|
|
4230
|
+
const txid = await wallet.send({
|
|
4231
|
+
address: offer.address,
|
|
4232
|
+
// asset deposits ride the SDK's dust-sat carrier
|
|
4233
|
+
amount: depositIsBtc ? Number(plan.deposit.atomic) : void 0,
|
|
4234
|
+
assets: depositIsBtc ? void 0 : [{ assetId: plan.deposit.asset.id, amount: plan.deposit.atomic }],
|
|
4235
|
+
extensions: [offer.extension]
|
|
4236
|
+
});
|
|
4237
|
+
const swap = {
|
|
4238
|
+
id: txid,
|
|
4239
|
+
fromAsset: plan.deposit.asset.id,
|
|
4240
|
+
toAsset: plan.receive.asset.id,
|
|
4241
|
+
fromAmount: plan.deposit.atomic.toString(),
|
|
4242
|
+
toAmount: plan.receive.atomic.toString(),
|
|
4243
|
+
swapAddress: offer.address,
|
|
4244
|
+
swapPkScript: hex13.encode(offer.swapPkScript),
|
|
4245
|
+
offerHex: offer.offerHex,
|
|
4246
|
+
fundingTxid: txid,
|
|
4247
|
+
status: "pending",
|
|
4248
|
+
createdAt: Date.now()
|
|
4249
|
+
};
|
|
4250
|
+
await addAssetSwap(repository, swap);
|
|
4251
|
+
const unified = { family: "offer", swap };
|
|
4252
|
+
notify2(unified);
|
|
4253
|
+
return unified;
|
|
4254
|
+
}
|
|
4255
|
+
case "ln_send": {
|
|
4256
|
+
const { request } = accepted;
|
|
4257
|
+
const swap = {
|
|
4258
|
+
kind: "lightning_send",
|
|
4259
|
+
rfqId: request.rfqId,
|
|
4260
|
+
state: "pending",
|
|
4261
|
+
lockupPkScript: request.swapPkScript,
|
|
4262
|
+
lockup: { script: request.script, address: request.address },
|
|
4263
|
+
// the trader's own decode, never the solver's echo
|
|
4264
|
+
paymentHash: request.contractParams.paymentHash,
|
|
4265
|
+
refundLocktime: request.contractParams.refundLocktime,
|
|
4266
|
+
createdAt: now,
|
|
4267
|
+
updatedAt: now
|
|
4268
|
+
};
|
|
4269
|
+
const origin = {
|
|
4270
|
+
kind: "lightning_send",
|
|
4271
|
+
lockupAddress: request.address,
|
|
4272
|
+
profile: rfqSecretsProfile(request.secrets, swap.paymentHash),
|
|
4273
|
+
amount: request.fundAmount
|
|
4274
|
+
};
|
|
4275
|
+
const txid = await fundPersisted(swap, origin, {
|
|
4276
|
+
address: request.address,
|
|
4277
|
+
amount: request.fundAmount
|
|
4278
|
+
});
|
|
4279
|
+
return admit(swap, { ...origin, fundingTxid: txid });
|
|
4280
|
+
}
|
|
4281
|
+
case "ln_receive": {
|
|
4282
|
+
const { request } = accepted;
|
|
4283
|
+
const paymentHash = hex13.encode(request.secrets.paymentHash);
|
|
4284
|
+
const swap = {
|
|
4285
|
+
kind: "lightning_receive",
|
|
4286
|
+
rfqId: request.rfqId,
|
|
4287
|
+
state: "pending",
|
|
4288
|
+
lockupPkScript: request.swapPkScript,
|
|
4289
|
+
lockup: { script: request.script, address: request.address },
|
|
4290
|
+
paymentHash,
|
|
4291
|
+
refundLocktime: request.contractParams.refundLocktime,
|
|
4292
|
+
expectedAmount: request.expectedAmount,
|
|
4293
|
+
createdAt: now,
|
|
4294
|
+
updatedAt: now
|
|
4295
|
+
};
|
|
4296
|
+
return admit(swap, {
|
|
4297
|
+
kind: "lightning_receive",
|
|
4298
|
+
lockupAddress: request.address,
|
|
4299
|
+
profile: {
|
|
4300
|
+
...rfqSecretsProfile(request.secrets, paymentHash),
|
|
4301
|
+
expectedAmount: request.expectedAmount,
|
|
4302
|
+
payoutAddress: request.payoutAddress
|
|
4303
|
+
},
|
|
4304
|
+
amount: request.expectedAmount
|
|
4305
|
+
});
|
|
4306
|
+
}
|
|
4307
|
+
case "onchain_send": {
|
|
4308
|
+
const { request } = accepted;
|
|
4309
|
+
const paymentHash = hex13.encode(request.secrets.paymentHash);
|
|
4310
|
+
const swap = {
|
|
4311
|
+
kind: "onchain_send",
|
|
4312
|
+
rfqId: request.rfqId,
|
|
4313
|
+
state: "pending",
|
|
4314
|
+
lockupPkScript: request.swapPkScript,
|
|
4315
|
+
lockup: { script: request.script, address: request.address },
|
|
4316
|
+
paymentHash,
|
|
4317
|
+
refundLocktime: request.refundLocktime,
|
|
4318
|
+
htlc: request.htlc,
|
|
4319
|
+
minConfirmations: request.minConfirmations,
|
|
4320
|
+
payoutPkScript: accepted.payoutPkScript,
|
|
4321
|
+
createdAt: now,
|
|
4322
|
+
updatedAt: now
|
|
4323
|
+
};
|
|
4324
|
+
const origin = {
|
|
4325
|
+
kind: "onchain_send",
|
|
4326
|
+
lockupAddress: request.address,
|
|
4327
|
+
profile: {
|
|
4328
|
+
...rfqSecretsProfile(request.secrets, paymentHash),
|
|
4329
|
+
...onchainSendProfile({
|
|
4330
|
+
...request,
|
|
4331
|
+
payoutPkScript: accepted.payoutPkScript
|
|
4332
|
+
})
|
|
4333
|
+
},
|
|
4334
|
+
amount: request.fundAmount
|
|
4335
|
+
};
|
|
4336
|
+
const txid = await fundPersisted(swap, origin, {
|
|
4337
|
+
address: request.address,
|
|
4338
|
+
amount: request.fundAmount
|
|
4339
|
+
});
|
|
4340
|
+
return admit(swap, { ...origin, fundingTxid: txid });
|
|
4341
|
+
}
|
|
4342
|
+
}
|
|
4343
|
+
};
|
|
4344
|
+
return {
|
|
4345
|
+
manager,
|
|
4346
|
+
markets: (useCache) => discoverMarkets({ ...deps.discovery, repository, useCache }),
|
|
4347
|
+
quote,
|
|
4348
|
+
accept,
|
|
4349
|
+
cancel: async (fundingTxid) => {
|
|
4350
|
+
const swaps = await getAssetSwapsOrThrow(repository);
|
|
4351
|
+
const swap = swaps.find((s) => s.id === fundingTxid);
|
|
4352
|
+
if (!swap) throw new Error(`no offer swap with funding txid ${fundingTxid}`);
|
|
4353
|
+
if (swap.status !== "pending") {
|
|
4354
|
+
throw new Error(`offer swap ${fundingTxid} is ${swap.status}, not cancellable`);
|
|
4355
|
+
}
|
|
4356
|
+
await updateAssetSwap(repository, fundingTxid, { status: "cancelling" });
|
|
4357
|
+
await cancelOffer(wallet, swap.offerHex, {
|
|
4358
|
+
repository,
|
|
4359
|
+
fundingTxid
|
|
4360
|
+
});
|
|
4361
|
+
},
|
|
4362
|
+
// Both families, live AND terminal — the offer half reads the whole
|
|
4363
|
+
// stored history, and the RFQ half is every swap the manager holds.
|
|
4364
|
+
// Its history is what `start()` restored plus what this session
|
|
4365
|
+
// accepted; records past `RFQ_SWAP_RETENTION_SECONDS` are pruned.
|
|
4366
|
+
swaps: async () => {
|
|
4367
|
+
const offers = await getAssetSwaps(repository);
|
|
4368
|
+
const rfq = await manager.getAllSwaps();
|
|
4369
|
+
return [
|
|
4370
|
+
...offers.map((swap) => ({ family: "offer", swap })),
|
|
4371
|
+
...rfq.map((swap) => ({ family: "rfq", swap }))
|
|
4372
|
+
];
|
|
4373
|
+
},
|
|
4374
|
+
onUpdate: (listener) => {
|
|
4375
|
+
listeners.add(listener);
|
|
4376
|
+
return () => listeners.delete(listener);
|
|
4377
|
+
},
|
|
4378
|
+
start: async () => {
|
|
4379
|
+
managerDeps.contracts ??= await wallet.getContractManager();
|
|
4380
|
+
await manager.restoreFromRepository();
|
|
4381
|
+
await manager.start();
|
|
4382
|
+
watcher ??= await watchOfferSwaps({
|
|
4383
|
+
wallet,
|
|
4384
|
+
repository,
|
|
4385
|
+
onUpdate: (swap) => notify2({ family: "offer", swap })
|
|
4386
|
+
});
|
|
4387
|
+
},
|
|
4388
|
+
stop: async () => {
|
|
4389
|
+
await manager.stop();
|
|
4390
|
+
watcher?.stop();
|
|
4391
|
+
watcher = void 0;
|
|
4392
|
+
}
|
|
4393
|
+
};
|
|
4394
|
+
}
|
|
3331
4395
|
export {
|
|
3332
4396
|
ARKADE_ASSET,
|
|
3333
4397
|
ARKADE_BTC,
|
|
@@ -3336,6 +4400,7 @@ export {
|
|
|
3336
4400
|
InMemoryAssetSwapRepository,
|
|
3337
4401
|
IndexedDbAssetSwapRepository,
|
|
3338
4402
|
LIGHTNING_BTC,
|
|
4403
|
+
LIGHTNING_RAIL,
|
|
3339
4404
|
LIGHTNING_RECEIVE_PAIR,
|
|
3340
4405
|
LIGHTNING_SEND_PAIR,
|
|
3341
4406
|
LOCKTIME_THRESHOLD,
|
|
@@ -3346,6 +4411,7 @@ export {
|
|
|
3346
4411
|
MAX_MIN_CONFIRMATIONS,
|
|
3347
4412
|
MIN_CLAIM_WINDOW_SECONDS,
|
|
3348
4413
|
MIN_HEADROOM_SECONDS,
|
|
4414
|
+
NoSpendableDepositError,
|
|
3349
4415
|
OFFER_PACKET_TYPE,
|
|
3350
4416
|
ONCHAIN_BTC,
|
|
3351
4417
|
ONCHAIN_CLAIM_MARGIN_SECONDS,
|
|
@@ -3354,6 +4420,9 @@ export {
|
|
|
3354
4420
|
ONCHAIN_RECEIVE_PAIR,
|
|
3355
4421
|
ONCHAIN_SECONDS_PER_BLOCK,
|
|
3356
4422
|
ONCHAIN_SEND_PAIR,
|
|
4423
|
+
ONCHAIN_SWAP_RAIL,
|
|
4424
|
+
OfferCovenantMismatchError,
|
|
4425
|
+
PAYMENT_STATUS,
|
|
3357
4426
|
PreimageNotRecoverableError,
|
|
3358
4427
|
QUOTE_OPTIONS,
|
|
3359
4428
|
REFUND_MTP_LAG_SECONDS,
|
|
@@ -3370,6 +4439,8 @@ export {
|
|
|
3370
4439
|
SWAP_LOCKUP_CONTRACT_KIND,
|
|
3371
4440
|
SWAP_LOCKUP_CONTRACT_LABEL,
|
|
3372
4441
|
SWAP_LOCKUP_CONTRACT_TYPE,
|
|
4442
|
+
SWAP_ROUTER_PRIORITY,
|
|
4443
|
+
SwapPaymentFailedError,
|
|
3373
4444
|
SwapRefusal,
|
|
3374
4445
|
addAssetSwap,
|
|
3375
4446
|
arkadeAssetLeg,
|
|
@@ -3384,6 +4455,7 @@ export {
|
|
|
3384
4455
|
buildHtlcRefund,
|
|
3385
4456
|
cancelOffer,
|
|
3386
4457
|
chainSourceFrom,
|
|
4458
|
+
claimFeeSats,
|
|
3387
4459
|
claimOnchainFill,
|
|
3388
4460
|
claimReceiveLockup,
|
|
3389
4461
|
classifyDepositSpend,
|
|
@@ -3391,6 +4463,8 @@ export {
|
|
|
3391
4463
|
classifySpend,
|
|
3392
4464
|
createOffer,
|
|
3393
4465
|
createRfqSwapRecord,
|
|
4466
|
+
createSwapClient,
|
|
4467
|
+
createSwapPaymentRouter,
|
|
3394
4468
|
decodeOffer,
|
|
3395
4469
|
deriveLightningReceive,
|
|
3396
4470
|
deriveOnchainReceive,
|
|
@@ -3405,28 +4479,35 @@ export {
|
|
|
3405
4479
|
httpTransport,
|
|
3406
4480
|
isRfqSwapTerminal,
|
|
3407
4481
|
isRfqTerminal,
|
|
4482
|
+
isTerminalStatus,
|
|
3408
4483
|
l1ScriptForAddress,
|
|
4484
|
+
lightningRail,
|
|
4485
|
+
lightningReceiveContract,
|
|
3409
4486
|
lightningReceiveRequest,
|
|
4487
|
+
lightningSendContract,
|
|
3410
4488
|
lightningSendRequest,
|
|
3411
|
-
lightningSendVtxoScript,
|
|
3412
4489
|
lockupContractParams,
|
|
3413
4490
|
makeCachedFeedFetch,
|
|
3414
4491
|
newPreimage,
|
|
3415
4492
|
newRfqId,
|
|
3416
4493
|
nextOnchainAction,
|
|
4494
|
+
normalizeRfqSwapRecord,
|
|
4495
|
+
offerContract,
|
|
3417
4496
|
offerTermsFromQuote,
|
|
3418
|
-
offerVtxoScript,
|
|
3419
4497
|
onchainHtlcScript,
|
|
3420
4498
|
onchainReceiveRequest,
|
|
3421
4499
|
onchainSendProfile,
|
|
3422
4500
|
onchainSendRequest,
|
|
4501
|
+
onchainSwapRail,
|
|
3423
4502
|
paymentHashOf,
|
|
4503
|
+
paymentStatusOf,
|
|
3424
4504
|
preimageForSwapRecord,
|
|
3425
4505
|
pushClaim,
|
|
3426
4506
|
pushRefundWithoutReceiver,
|
|
4507
|
+
railAvailable,
|
|
3427
4508
|
readLockupFate,
|
|
3428
4509
|
rebuildRfqSwap,
|
|
3429
|
-
|
|
4510
|
+
receiverExact,
|
|
3430
4511
|
refundIfUnresolved,
|
|
3431
4512
|
registerLockupContract,
|
|
3432
4513
|
relayTransport,
|
|
@@ -3453,6 +4534,7 @@ export {
|
|
|
3453
4534
|
spendTxidsOf,
|
|
3454
4535
|
spendUpdate,
|
|
3455
4536
|
swapActivityResolver,
|
|
4537
|
+
swapHandle,
|
|
3456
4538
|
swapPrograms,
|
|
3457
4539
|
swapSecretsToRecord,
|
|
3458
4540
|
unilateralClaimDelay,
|