@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.cjs
CHANGED
|
@@ -37,6 +37,7 @@ __export(index_exports, {
|
|
|
37
37
|
InMemoryAssetSwapRepository: () => InMemoryAssetSwapRepository,
|
|
38
38
|
IndexedDbAssetSwapRepository: () => IndexedDbAssetSwapRepository,
|
|
39
39
|
LIGHTNING_BTC: () => LIGHTNING_BTC,
|
|
40
|
+
LIGHTNING_RAIL: () => LIGHTNING_RAIL,
|
|
40
41
|
LIGHTNING_RECEIVE_PAIR: () => LIGHTNING_RECEIVE_PAIR,
|
|
41
42
|
LIGHTNING_SEND_PAIR: () => LIGHTNING_SEND_PAIR,
|
|
42
43
|
LOCKTIME_THRESHOLD: () => LOCKTIME_THRESHOLD,
|
|
@@ -47,6 +48,7 @@ __export(index_exports, {
|
|
|
47
48
|
MAX_MIN_CONFIRMATIONS: () => MAX_MIN_CONFIRMATIONS,
|
|
48
49
|
MIN_CLAIM_WINDOW_SECONDS: () => MIN_CLAIM_WINDOW_SECONDS,
|
|
49
50
|
MIN_HEADROOM_SECONDS: () => MIN_HEADROOM_SECONDS,
|
|
51
|
+
NoSpendableDepositError: () => NoSpendableDepositError,
|
|
50
52
|
OFFER_PACKET_TYPE: () => OFFER_PACKET_TYPE,
|
|
51
53
|
ONCHAIN_BTC: () => ONCHAIN_BTC,
|
|
52
54
|
ONCHAIN_CLAIM_MARGIN_SECONDS: () => ONCHAIN_CLAIM_MARGIN_SECONDS,
|
|
@@ -55,6 +57,9 @@ __export(index_exports, {
|
|
|
55
57
|
ONCHAIN_RECEIVE_PAIR: () => ONCHAIN_RECEIVE_PAIR,
|
|
56
58
|
ONCHAIN_SECONDS_PER_BLOCK: () => ONCHAIN_SECONDS_PER_BLOCK,
|
|
57
59
|
ONCHAIN_SEND_PAIR: () => ONCHAIN_SEND_PAIR,
|
|
60
|
+
ONCHAIN_SWAP_RAIL: () => ONCHAIN_SWAP_RAIL,
|
|
61
|
+
OfferCovenantMismatchError: () => OfferCovenantMismatchError,
|
|
62
|
+
PAYMENT_STATUS: () => PAYMENT_STATUS,
|
|
58
63
|
PreimageNotRecoverableError: () => PreimageNotRecoverableError,
|
|
59
64
|
QUOTE_OPTIONS: () => QUOTE_OPTIONS,
|
|
60
65
|
REFUND_MTP_LAG_SECONDS: () => REFUND_MTP_LAG_SECONDS,
|
|
@@ -71,6 +76,8 @@ __export(index_exports, {
|
|
|
71
76
|
SWAP_LOCKUP_CONTRACT_KIND: () => SWAP_LOCKUP_CONTRACT_KIND,
|
|
72
77
|
SWAP_LOCKUP_CONTRACT_LABEL: () => SWAP_LOCKUP_CONTRACT_LABEL,
|
|
73
78
|
SWAP_LOCKUP_CONTRACT_TYPE: () => SWAP_LOCKUP_CONTRACT_TYPE,
|
|
79
|
+
SWAP_ROUTER_PRIORITY: () => SWAP_ROUTER_PRIORITY,
|
|
80
|
+
SwapPaymentFailedError: () => SwapPaymentFailedError,
|
|
74
81
|
SwapRefusal: () => SwapRefusal,
|
|
75
82
|
addAssetSwap: () => addAssetSwap,
|
|
76
83
|
arkadeAssetLeg: () => arkadeAssetLeg,
|
|
@@ -85,6 +92,7 @@ __export(index_exports, {
|
|
|
85
92
|
buildHtlcRefund: () => buildHtlcRefund,
|
|
86
93
|
cancelOffer: () => cancelOffer,
|
|
87
94
|
chainSourceFrom: () => chainSourceFrom,
|
|
95
|
+
claimFeeSats: () => claimFeeSats,
|
|
88
96
|
claimOnchainFill: () => claimOnchainFill,
|
|
89
97
|
claimReceiveLockup: () => claimReceiveLockup,
|
|
90
98
|
classifyDepositSpend: () => classifyDepositSpend,
|
|
@@ -92,6 +100,8 @@ __export(index_exports, {
|
|
|
92
100
|
classifySpend: () => classifySpend,
|
|
93
101
|
createOffer: () => createOffer,
|
|
94
102
|
createRfqSwapRecord: () => createRfqSwapRecord,
|
|
103
|
+
createSwapClient: () => createSwapClient,
|
|
104
|
+
createSwapPaymentRouter: () => createSwapPaymentRouter,
|
|
95
105
|
decodeOffer: () => decodeOffer,
|
|
96
106
|
deriveLightningReceive: () => deriveLightningReceive,
|
|
97
107
|
deriveOnchainReceive: () => deriveOnchainReceive,
|
|
@@ -106,28 +116,35 @@ __export(index_exports, {
|
|
|
106
116
|
httpTransport: () => httpTransport,
|
|
107
117
|
isRfqSwapTerminal: () => isRfqSwapTerminal,
|
|
108
118
|
isRfqTerminal: () => isRfqTerminal,
|
|
119
|
+
isTerminalStatus: () => isTerminalStatus,
|
|
109
120
|
l1ScriptForAddress: () => l1ScriptForAddress,
|
|
121
|
+
lightningRail: () => lightningRail,
|
|
122
|
+
lightningReceiveContract: () => lightningReceiveContract,
|
|
110
123
|
lightningReceiveRequest: () => lightningReceiveRequest,
|
|
124
|
+
lightningSendContract: () => lightningSendContract,
|
|
111
125
|
lightningSendRequest: () => lightningSendRequest,
|
|
112
|
-
lightningSendVtxoScript: () => lightningSendVtxoScript,
|
|
113
126
|
lockupContractParams: () => lockupContractParams,
|
|
114
127
|
makeCachedFeedFetch: () => makeCachedFeedFetch,
|
|
115
128
|
newPreimage: () => newPreimage,
|
|
116
129
|
newRfqId: () => newRfqId,
|
|
117
130
|
nextOnchainAction: () => nextOnchainAction,
|
|
131
|
+
normalizeRfqSwapRecord: () => normalizeRfqSwapRecord,
|
|
132
|
+
offerContract: () => offerContract,
|
|
118
133
|
offerTermsFromQuote: () => offerTermsFromQuote,
|
|
119
|
-
offerVtxoScript: () => offerVtxoScript,
|
|
120
134
|
onchainHtlcScript: () => onchainHtlcScript,
|
|
121
135
|
onchainReceiveRequest: () => onchainReceiveRequest,
|
|
122
136
|
onchainSendProfile: () => onchainSendProfile,
|
|
123
137
|
onchainSendRequest: () => onchainSendRequest,
|
|
138
|
+
onchainSwapRail: () => onchainSwapRail,
|
|
124
139
|
paymentHashOf: () => paymentHashOf,
|
|
140
|
+
paymentStatusOf: () => paymentStatusOf,
|
|
125
141
|
preimageForSwapRecord: () => preimageForSwapRecord,
|
|
126
142
|
pushClaim: () => pushClaim,
|
|
127
143
|
pushRefundWithoutReceiver: () => pushRefundWithoutReceiver,
|
|
144
|
+
railAvailable: () => railAvailable,
|
|
128
145
|
readLockupFate: () => readLockupFate,
|
|
129
146
|
rebuildRfqSwap: () => rebuildRfqSwap,
|
|
130
|
-
|
|
147
|
+
receiverExact: () => receiverExact,
|
|
131
148
|
refundIfUnresolved: () => refundIfUnresolved,
|
|
132
149
|
registerLockupContract: () => registerLockupContract,
|
|
133
150
|
relayTransport: () => relayTransport,
|
|
@@ -154,6 +171,7 @@ __export(index_exports, {
|
|
|
154
171
|
spendTxidsOf: () => spendTxidsOf,
|
|
155
172
|
spendUpdate: () => spendUpdate,
|
|
156
173
|
swapActivityResolver: () => swapActivityResolver,
|
|
174
|
+
swapHandle: () => swapHandle,
|
|
157
175
|
swapPrograms: () => swapPrograms,
|
|
158
176
|
swapSecretsToRecord: () => swapSecretsToRecord,
|
|
159
177
|
unilateralClaimDelay: () => unilateralClaimDelay,
|
|
@@ -333,9 +351,9 @@ var addAssetSwap = async (repository, swap) => {
|
|
|
333
351
|
const swaps = await getAssetSwapsOrThrow(repository);
|
|
334
352
|
if (swaps.some((s) => s.id === swap.id)) return swaps;
|
|
335
353
|
await saveSwapOrThrow(repository, swap);
|
|
336
|
-
const
|
|
354
|
+
const at2 = swaps.findIndex((s) => byNewest(swap, s) <= 0);
|
|
337
355
|
const merged = [...swaps];
|
|
338
|
-
merged.splice(
|
|
356
|
+
merged.splice(at2 === -1 ? merged.length : at2, 0, swap);
|
|
339
357
|
return merged;
|
|
340
358
|
};
|
|
341
359
|
var updateAssetSwap = async (repository, id, changes) => {
|
|
@@ -435,7 +453,7 @@ function withExitClosure(program, exit) {
|
|
|
435
453
|
}
|
|
436
454
|
};
|
|
437
455
|
}
|
|
438
|
-
function swapProgramBinding(offer,
|
|
456
|
+
function swapProgramBinding(offer, operatorPubkey) {
|
|
439
457
|
if (offer.makerPkScript.length !== FIELDS.makerPkScript.width) {
|
|
440
458
|
throw new Error("makerPkScript is not a 34-byte taproot scriptPubKey");
|
|
441
459
|
}
|
|
@@ -447,7 +465,7 @@ function swapProgramBinding(offer, serverPubkey) {
|
|
|
447
465
|
args: {
|
|
448
466
|
makerWP: offer.makerPkScript.subarray(2),
|
|
449
467
|
wantAmount: offer.wantAmount,
|
|
450
|
-
server:
|
|
468
|
+
server: operatorPubkey,
|
|
451
469
|
user: offer.makerPublicKey,
|
|
452
470
|
// internal byte order
|
|
453
471
|
...offer.wantAsset && {
|
|
@@ -457,14 +475,14 @@ function swapProgramBinding(offer, serverPubkey) {
|
|
|
457
475
|
...offer.exitDelay && { exitDelay: offer.exitDelay.value }
|
|
458
476
|
},
|
|
459
477
|
keys: {
|
|
460
|
-
serverKey:
|
|
478
|
+
serverKey: operatorPubkey,
|
|
461
479
|
userKey: offer.makerPublicKey,
|
|
462
480
|
emulatorKey: offer.emulatorPubkey
|
|
463
481
|
}
|
|
464
482
|
};
|
|
465
483
|
}
|
|
466
|
-
function
|
|
467
|
-
const { program, args, keys } = swapProgramBinding(offer,
|
|
484
|
+
function offerContract(offer, operatorPubkey) {
|
|
485
|
+
const { program, args, keys } = swapProgramBinding(offer, operatorPubkey);
|
|
468
486
|
return new import_sdk2.arkade.ArkadeProgramScript(program, args, keys);
|
|
469
487
|
}
|
|
470
488
|
var OFFER_PACKET_TYPE = 3;
|
|
@@ -578,14 +596,14 @@ function decodeOffer(data) {
|
|
|
578
596
|
throw new Error(`missing/invalid ${name}`);
|
|
579
597
|
}
|
|
580
598
|
}
|
|
581
|
-
const
|
|
599
|
+
const need2 = (name) => {
|
|
582
600
|
const v = fields[name];
|
|
583
601
|
const len = FIELDS[name].width;
|
|
584
602
|
if (!v || len !== void 0 && v.length !== len)
|
|
585
603
|
throw new Error(`missing/invalid ${name}`);
|
|
586
604
|
return v;
|
|
587
605
|
};
|
|
588
|
-
const amount =
|
|
606
|
+
const amount = need2("wantAmount");
|
|
589
607
|
if (Boolean(fields.wantAsset) === Boolean(fields.offerAsset)) {
|
|
590
608
|
throw new Error("offer must carry exactly one of wantAsset or offerAsset");
|
|
591
609
|
}
|
|
@@ -602,13 +620,13 @@ function decodeOffer(data) {
|
|
|
602
620
|
throw new Error("offer must carry both ratioNum and ratioDen, or neither");
|
|
603
621
|
}
|
|
604
622
|
return {
|
|
605
|
-
swapPkScript:
|
|
623
|
+
swapPkScript: need2("swapPkScript"),
|
|
606
624
|
wantAmount: readU64(amount),
|
|
607
625
|
...fields.wantAsset && { wantAsset: import_sdk2.asset.AssetId.fromBytes(fields.wantAsset) },
|
|
608
626
|
...fields.offerAsset && { offerAsset: import_sdk2.asset.AssetId.fromBytes(fields.offerAsset) },
|
|
609
|
-
makerPkScript:
|
|
610
|
-
makerPublicKey:
|
|
611
|
-
emulatorPubkey:
|
|
627
|
+
makerPkScript: need2("makerPkScript"),
|
|
628
|
+
makerPublicKey: need2("makerPublicKey"),
|
|
629
|
+
emulatorPubkey: need2("emulatorPubkey"),
|
|
612
630
|
...ratioNum !== void 0 && { ratioNum },
|
|
613
631
|
...ratioDen !== void 0 && { ratioDen },
|
|
614
632
|
...fields.exitTimelock && { exitDelay: decodeExitDelay(fields.exitTimelock) }
|
|
@@ -621,17 +639,21 @@ function decodeExitDelay(value) {
|
|
|
621
639
|
}
|
|
622
640
|
var OFFER_CONTRACT_LABEL = "Arkade swap offer";
|
|
623
641
|
var OFFER_CONTRACT_KIND = "asset-swap-offer";
|
|
624
|
-
async function registerOfferContract(wallet,
|
|
625
|
-
const { program, args, keys } = swapProgramBinding(binding,
|
|
642
|
+
async function registerOfferContract(wallet, info, binding, operatorPubkey, expectedPkScript) {
|
|
643
|
+
const { program, args, keys } = swapProgramBinding(binding, operatorPubkey);
|
|
626
644
|
const contractManager = await wallet.getContractManager();
|
|
627
645
|
const client = await import_sdk2.arkade.Arkade.connect({
|
|
628
|
-
|
|
629
|
-
|
|
646
|
+
// Registration derives and persists; it never broadcasts and never
|
|
647
|
+
// reads UTXOs, so the only thing the client needs off the server is the
|
|
648
|
+
// info the caller already resolved. Handing that back — rather than a
|
|
649
|
+
// provider built from a URL — is what lets `createOffer` take just a
|
|
650
|
+
// wallet, and spares a second `/v1/info` round-trip.
|
|
651
|
+
arkade: { getInfo: async () => info },
|
|
630
652
|
identity: wallet.identity,
|
|
631
653
|
// without this the row's `address` would be derived against the SDK's
|
|
632
654
|
// default network while its script is right — a row that disagrees with
|
|
633
655
|
// the address the user is about to fund
|
|
634
|
-
network: (0, import_sdk2.
|
|
656
|
+
network: (0, import_sdk2.networkFromArkadeInfo)(info),
|
|
635
657
|
contractManager
|
|
636
658
|
});
|
|
637
659
|
const contract = new import_sdk2.arkade.ArkadeContract(client, program, args, keys);
|
|
@@ -652,17 +674,21 @@ function serverExitDelay(delay) {
|
|
|
652
674
|
}
|
|
653
675
|
return assertExitDelay({ value: delay, type: delay < BigInt(512) ? "blocks" : "seconds" });
|
|
654
676
|
}
|
|
655
|
-
async function createOffer(wallet,
|
|
677
|
+
async function createOffer(wallet, params) {
|
|
656
678
|
if (Boolean(params.wantAsset) === Boolean(params.offerAsset)) {
|
|
657
679
|
throw new Error("set exactly one of wantAsset (BTC->asset) or offerAsset (asset->BTC)");
|
|
658
680
|
}
|
|
659
681
|
const [info, makerAddress, makerPublicKey] = await Promise.all([
|
|
660
|
-
|
|
682
|
+
// requireLive: this info binds signerPubkey into the covenant — a
|
|
683
|
+
// snapshot could derive an address the operator no longer co-signs
|
|
684
|
+
// for, so an unreachable operator fails the call instead (fail closed,
|
|
685
|
+
// the same behaviour the pre-#734 caller-held provider had)
|
|
686
|
+
wallet.getArkadeInfo({ requireLive: true }),
|
|
661
687
|
wallet.getAddress(),
|
|
662
688
|
wallet.identity.xOnlyPublicKey()
|
|
663
689
|
]);
|
|
664
|
-
const
|
|
665
|
-
const network = (0, import_sdk2.
|
|
690
|
+
const operatorPubKey = import_base2.hex.decode((0, import_sdk2.toXOnlySignerHex)(info.signerPubkey));
|
|
691
|
+
const network = (0, import_sdk2.networkFromArkadeInfo)(info);
|
|
666
692
|
const emuKey = import_base2.hex.decode(
|
|
667
693
|
(0, import_sdk2.toXOnlySignerHex)((0, import_sdk2.resolveEmulatorPubkey)(network, params.emulatorPubkey))
|
|
668
694
|
);
|
|
@@ -678,33 +704,77 @@ async function createOffer(wallet, arkServerUrl, params) {
|
|
|
678
704
|
// an offer that then fails to encode. @see assertExitDelay
|
|
679
705
|
exitDelay: params.noExit ? void 0 : params.exitDelay ? assertExitDelay(params.exitDelay) : serverExitDelay(info.unilateralExitDelay)
|
|
680
706
|
};
|
|
681
|
-
const script =
|
|
707
|
+
const script = offerContract(binding, operatorPubKey);
|
|
682
708
|
const offer = { ...binding, swapPkScript: script.pkScript };
|
|
683
|
-
await registerOfferContract(
|
|
684
|
-
wallet,
|
|
685
|
-
arkServerUrl,
|
|
686
|
-
info.network,
|
|
687
|
-
binding,
|
|
688
|
-
serverPubKey,
|
|
689
|
-
script.pkScript
|
|
690
|
-
);
|
|
709
|
+
await registerOfferContract(wallet, info, binding, operatorPubKey, script.pkScript);
|
|
691
710
|
const payload = encodeOffer(offer);
|
|
692
711
|
return {
|
|
693
712
|
offerHex: import_base2.hex.encode(payload),
|
|
694
713
|
extension: { type: OFFER_PACKET_TYPE, payload },
|
|
695
|
-
//
|
|
714
|
+
// the contract's .address() builds the address; assembling an ArkAddress
|
|
696
715
|
// from tweakedPublicKey here would silently miss any future step it gains
|
|
697
|
-
address: script.address(network.hrp,
|
|
716
|
+
address: script.address(network.hrp, operatorPubKey).encode(),
|
|
698
717
|
swapPkScript: script.pkScript
|
|
699
718
|
};
|
|
700
719
|
}
|
|
701
|
-
async function cancelOffer(wallet,
|
|
720
|
+
async function cancelOffer(wallet, offerHex, opts) {
|
|
702
721
|
const { repository, fundingTxid, swapAddress } = opts;
|
|
722
|
+
const prepared = await prepareOfferCancel(wallet, offerHex, {
|
|
723
|
+
...fundingTxid === void 0 ? {} : { fundingTxid },
|
|
724
|
+
...swapAddress === void 0 ? {} : { swapAddress }
|
|
725
|
+
});
|
|
726
|
+
const swapId = fundingTxid ?? prepared.vtxo.txid;
|
|
727
|
+
const hasLocalRecord = (await getAssetSwapsOrThrow(repository)).some((s) => s.id === swapId);
|
|
728
|
+
if (hasLocalRecord) await updateAssetSwap(repository, swapId, { status: "cancelling" });
|
|
729
|
+
const txid = await prepared.send();
|
|
730
|
+
if (hasLocalRecord) {
|
|
731
|
+
const { persisted, swaps } = await updateAssetSwapBestEffort(repository, swapId, {
|
|
732
|
+
status: "cancelled",
|
|
733
|
+
spentTxid: txid
|
|
734
|
+
});
|
|
735
|
+
if (persisted) {
|
|
736
|
+
const contractManager = await wallet.getContractManager();
|
|
737
|
+
await retireOfferContract(
|
|
738
|
+
contractManager,
|
|
739
|
+
swaps,
|
|
740
|
+
import_base2.hex.encode(prepared.offer.swapPkScript)
|
|
741
|
+
);
|
|
742
|
+
}
|
|
743
|
+
}
|
|
744
|
+
return txid;
|
|
745
|
+
}
|
|
746
|
+
var OFFER_COVENANT_MISMATCH = "rebuilt covenant does not match the offer's swapPkScript";
|
|
747
|
+
var NoSpendableDepositError = class extends Error {
|
|
748
|
+
name = "NoSpendableDepositError";
|
|
749
|
+
constructor(options) {
|
|
750
|
+
super("no spendable VTXO at the swap address", options);
|
|
751
|
+
}
|
|
752
|
+
};
|
|
753
|
+
var OfferCovenantMismatchError = class extends Error {
|
|
754
|
+
constructor(swapPkScript, options) {
|
|
755
|
+
super(
|
|
756
|
+
`${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`,
|
|
757
|
+
options
|
|
758
|
+
);
|
|
759
|
+
this.swapPkScript = swapPkScript;
|
|
760
|
+
}
|
|
761
|
+
swapPkScript;
|
|
762
|
+
name = "OfferCovenantMismatchError";
|
|
763
|
+
};
|
|
764
|
+
async function prepareOfferCancel(wallet, offerHex, opts) {
|
|
765
|
+
const { fundingTxid, swapAddress } = opts;
|
|
703
766
|
const offer = decodeOffer(import_base2.hex.decode(offerHex));
|
|
704
|
-
const contractManager = await
|
|
767
|
+
const [contractManager, info, reader, broadcaster] = await Promise.all([
|
|
768
|
+
wallet.getContractManager(),
|
|
769
|
+
wallet.getArkadeInfo({ requireLive: true }),
|
|
770
|
+
wallet.getArkadeReader(),
|
|
771
|
+
wallet.getArkadeBroadcaster()
|
|
772
|
+
]);
|
|
705
773
|
const client = await import_sdk2.arkade.Arkade.connect({
|
|
706
|
-
|
|
707
|
-
|
|
774
|
+
// info the wallet already holds, plus its broadcast pair — the shape
|
|
775
|
+
// `ArkadeServerProvider` describes, without a second `/v1/info`
|
|
776
|
+
arkade: { getInfo: async () => info, ...broadcaster },
|
|
777
|
+
indexer: reader,
|
|
708
778
|
identity: wallet.identity,
|
|
709
779
|
// registered offers resolve their VTXOs from the contract repository
|
|
710
780
|
// instead of a direct indexer query; the indexer above stays as the
|
|
@@ -718,9 +788,7 @@ async function cancelOffer(wallet, arkServerUrl, offerHex, opts) {
|
|
|
718
788
|
const { program, args, keys } = swapProgramBinding(offer, operatorPubkey);
|
|
719
789
|
const rebuilt = new import_sdk2.arkade.ArkadeProgramScript(program, args, keys);
|
|
720
790
|
if (import_base2.hex.encode(rebuilt.pkScript) !== import_base2.hex.encode(offer.swapPkScript)) {
|
|
721
|
-
throw new
|
|
722
|
-
"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"
|
|
723
|
-
);
|
|
791
|
+
throw new OfferCovenantMismatchError(import_base2.hex.encode(offer.swapPkScript));
|
|
724
792
|
}
|
|
725
793
|
const contract = new import_sdk2.arkade.ArkadeContract(client, program, args, keys);
|
|
726
794
|
const [vtxos, makerAddress] = await Promise.all([contract.getUtxos(), wallet.getAddress()]);
|
|
@@ -730,7 +798,7 @@ async function cancelOffer(wallet, arkServerUrl, offerHex, opts) {
|
|
|
730
798
|
);
|
|
731
799
|
}
|
|
732
800
|
const vtxo = fundingTxid ? vtxos.find((v) => v.txid === fundingTxid) : vtxos[0];
|
|
733
|
-
if (!vtxo) throw new
|
|
801
|
+
if (!vtxo) throw new NoSpendableDepositError();
|
|
734
802
|
const makerPkScript = import_sdk2.ArkAddress.decode(makerAddress).pkScript;
|
|
735
803
|
const cancel = contract.functions.cancel().from({ txid: vtxo.txid, vout: vtxo.vout, value: vtxo.value }).to(makerPkScript, BigInt(vtxo.value));
|
|
736
804
|
for (const a of vtxo.assets ?? []) {
|
|
@@ -740,20 +808,11 @@ async function cancelOffer(wallet, arkServerUrl, offerHex, opts) {
|
|
|
740
808
|
outputs: [{ vout: 0, amount: BigInt(a.amount) }]
|
|
741
809
|
});
|
|
742
810
|
}
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
const { persisted, swaps } = await updateAssetSwapBestEffort(repository, swapId, {
|
|
749
|
-
status: "cancelled",
|
|
750
|
-
spentTxid: txid
|
|
751
|
-
});
|
|
752
|
-
if (persisted) {
|
|
753
|
-
await retireOfferContract(contractManager, swaps, import_base2.hex.encode(offer.swapPkScript));
|
|
754
|
-
}
|
|
755
|
-
}
|
|
756
|
-
return txid;
|
|
811
|
+
return {
|
|
812
|
+
offer,
|
|
813
|
+
vtxo: { txid: vtxo.txid, vout: vtxo.vout, value: vtxo.value },
|
|
814
|
+
send: async () => (await cancel.send()).txid
|
|
815
|
+
};
|
|
757
816
|
}
|
|
758
817
|
|
|
759
818
|
// src/markets.ts
|
|
@@ -883,8 +942,9 @@ var validatePlan = (plan, giveBalance, dust) => {
|
|
|
883
942
|
// src/repository.ts
|
|
884
943
|
var marketsCacheKey = (network, registry) => `arkade-intents-markets-${network}-${registry}`;
|
|
885
944
|
var InMemoryAssetSwapRepository = class {
|
|
886
|
-
version =
|
|
945
|
+
version = 5;
|
|
887
946
|
swaps = /* @__PURE__ */ new Map();
|
|
947
|
+
records = /* @__PURE__ */ new Map();
|
|
888
948
|
rfqSwaps = /* @__PURE__ */ new Map();
|
|
889
949
|
scanned = /* @__PURE__ */ new Set();
|
|
890
950
|
markets = /* @__PURE__ */ new Map();
|
|
@@ -906,6 +966,18 @@ var InMemoryAssetSwapRepository = class {
|
|
|
906
966
|
async removeRfqSwap(rfqId) {
|
|
907
967
|
this.rfqSwaps.delete(rfqId);
|
|
908
968
|
}
|
|
969
|
+
async saveSwapRecord(record) {
|
|
970
|
+
this.records.set(record.id, record);
|
|
971
|
+
}
|
|
972
|
+
async getSwapRecord(id) {
|
|
973
|
+
return this.records.get(id);
|
|
974
|
+
}
|
|
975
|
+
async getAllSwapRecords() {
|
|
976
|
+
return [...this.records.values()];
|
|
977
|
+
}
|
|
978
|
+
async removeSwapRecord(id) {
|
|
979
|
+
this.records.delete(id);
|
|
980
|
+
}
|
|
909
981
|
async getScannedTxids() {
|
|
910
982
|
return new Set(this.scanned);
|
|
911
983
|
}
|
|
@@ -921,6 +993,7 @@ var InMemoryAssetSwapRepository = class {
|
|
|
921
993
|
async clear() {
|
|
922
994
|
this.swaps.clear();
|
|
923
995
|
this.rfqSwaps.clear();
|
|
996
|
+
this.records.clear();
|
|
924
997
|
this.scanned.clear();
|
|
925
998
|
this.markets.clear();
|
|
926
999
|
}
|
|
@@ -931,9 +1004,10 @@ var InMemoryAssetSwapRepository = class {
|
|
|
931
1004
|
// src/indexedDbRepository.ts
|
|
932
1005
|
var import_sdk4 = require("@arkade-os/sdk");
|
|
933
1006
|
var DEFAULT_DB_NAME = "arkade-intents";
|
|
934
|
-
var DB_VERSION =
|
|
1007
|
+
var DB_VERSION = 3;
|
|
935
1008
|
var STORE_SWAPS = "swaps";
|
|
936
1009
|
var STORE_RFQ_SWAPS = "rfqSwaps";
|
|
1010
|
+
var STORE_SWAP_RECORDS = "swapRecords";
|
|
937
1011
|
var STORE_SCANNED = "scannedTxids";
|
|
938
1012
|
var STORE_MARKETS = "markets";
|
|
939
1013
|
var STORES = [
|
|
@@ -941,6 +1015,11 @@ var STORES = [
|
|
|
941
1015
|
// v2. Separate from `swaps` rather than sharing it: the two record types
|
|
942
1016
|
// have different keys and no consumer wants them interleaved.
|
|
943
1017
|
[STORE_RFQ_SWAPS, { keyPath: "rfqId" }],
|
|
1018
|
+
// v3. The v2 client's accept records, keyed by quote id. Its own store for
|
|
1019
|
+
// the same reason, plus one more: the v1 read path drops a row carrying
|
|
1020
|
+
// neither `offerHex` nor `paymentHash`, so sharing `swaps` would pin the
|
|
1021
|
+
// v2 shape to a v1 predicate.
|
|
1022
|
+
[STORE_SWAP_RECORDS, { keyPath: "id" }],
|
|
944
1023
|
[STORE_SCANNED],
|
|
945
1024
|
[STORE_MARKETS]
|
|
946
1025
|
];
|
|
@@ -952,7 +1031,7 @@ function initDatabase(db, oldVersion, transaction) {
|
|
|
952
1031
|
}
|
|
953
1032
|
}
|
|
954
1033
|
var IndexedDbAssetSwapRepository = class {
|
|
955
|
-
version =
|
|
1034
|
+
version = 5;
|
|
956
1035
|
connection;
|
|
957
1036
|
constructor(dbName = DEFAULT_DB_NAME) {
|
|
958
1037
|
this.connection = (0, import_sdk4.createManagedConnection)(dbName, DB_VERSION, initDatabase);
|
|
@@ -996,6 +1075,22 @@ var IndexedDbAssetSwapRepository = class {
|
|
|
996
1075
|
store.delete(rfqId);
|
|
997
1076
|
});
|
|
998
1077
|
}
|
|
1078
|
+
async saveSwapRecord(record) {
|
|
1079
|
+
await this.write(STORE_SWAP_RECORDS, (store) => {
|
|
1080
|
+
store.put(record);
|
|
1081
|
+
});
|
|
1082
|
+
}
|
|
1083
|
+
async getSwapRecord(id) {
|
|
1084
|
+
return (0, import_sdk4.promisifyRequest)((await this.readStore(STORE_SWAP_RECORDS)).get(id));
|
|
1085
|
+
}
|
|
1086
|
+
async getAllSwapRecords() {
|
|
1087
|
+
return (0, import_sdk4.promisifyRequest)((await this.readStore(STORE_SWAP_RECORDS)).getAll());
|
|
1088
|
+
}
|
|
1089
|
+
async removeSwapRecord(id) {
|
|
1090
|
+
await this.write(STORE_SWAP_RECORDS, (store) => {
|
|
1091
|
+
store.delete(id);
|
|
1092
|
+
});
|
|
1093
|
+
}
|
|
999
1094
|
async getScannedTxids() {
|
|
1000
1095
|
const keys = await (0, import_sdk4.promisifyRequest)((await this.readStore(STORE_SCANNED)).getAllKeys());
|
|
1001
1096
|
return new Set(keys);
|
|
@@ -1102,6 +1197,14 @@ var parseSigner = (value) => {
|
|
|
1102
1197
|
}
|
|
1103
1198
|
return { signingDescriptor };
|
|
1104
1199
|
};
|
|
1200
|
+
var parsePayoutAddress = (value, kind) => {
|
|
1201
|
+
if (typeof value !== "string" || value.length === 0) {
|
|
1202
|
+
throw new Error(
|
|
1203
|
+
`this ${kind} record's claim destination is unusable: expected an Arkade address, got ${JSON.stringify(value)}`
|
|
1204
|
+
);
|
|
1205
|
+
}
|
|
1206
|
+
return value;
|
|
1207
|
+
};
|
|
1105
1208
|
var parseHashlock = (value) => {
|
|
1106
1209
|
const raw = value ?? {};
|
|
1107
1210
|
const hashlock = {
|
|
@@ -1147,6 +1250,11 @@ var rfqClaimSecretOf = (record) => {
|
|
|
1147
1250
|
);
|
|
1148
1251
|
}
|
|
1149
1252
|
};
|
|
1253
|
+
var rfqClaimDestinationOf = (record) => {
|
|
1254
|
+
const handler = rfqCorridorHandlers.getOrThrow(record.kind);
|
|
1255
|
+
if (!handler.claimDestination) return void 0;
|
|
1256
|
+
return parsePayoutAddress(handler.claimDestination(record.profile), record.kind);
|
|
1257
|
+
};
|
|
1150
1258
|
|
|
1151
1259
|
// src/onchainHtlc.ts
|
|
1152
1260
|
var import_base3 = require("@scure/base");
|
|
@@ -1159,6 +1267,7 @@ var MAX_MIN_CONFIRMATIONS = 6;
|
|
|
1159
1267
|
var LOCKTIME_THRESHOLD = 5e8;
|
|
1160
1268
|
var ONCHAIN_SECONDS_PER_BLOCK = 600;
|
|
1161
1269
|
var ONCHAIN_DUST_SATS = BigInt(330);
|
|
1270
|
+
var ONCHAIN_CLAIM_VSIZE = 152;
|
|
1162
1271
|
var newPreimage = () => crypto.getRandomValues(new Uint8Array(32));
|
|
1163
1272
|
var paymentHashOf = (preimage) => import_base3.hex.encode((0, import_sha22.sha256)(preimage));
|
|
1164
1273
|
var h160FromPaymentHash = (paymentHash) => (0, import_legacy.ripemd160)(import_base3.hex.decode(paymentHash));
|
|
@@ -1388,7 +1497,7 @@ var LightningReceiveCorridor = {
|
|
|
1388
1497
|
const receive = swap;
|
|
1389
1498
|
return {
|
|
1390
1499
|
expectedAmount: receive.expectedAmount,
|
|
1391
|
-
...receive.
|
|
1500
|
+
...receive.claimTxid ? { claimTxid: receive.claimTxid } : {}
|
|
1392
1501
|
};
|
|
1393
1502
|
},
|
|
1394
1503
|
hydrate(profile) {
|
|
@@ -1398,13 +1507,16 @@ var LightningReceiveCorridor = {
|
|
|
1398
1507
|
return {
|
|
1399
1508
|
...hydrateHashlock(profile),
|
|
1400
1509
|
expectedAmount: profile.expectedAmount,
|
|
1401
|
-
...profile.
|
|
1510
|
+
...profile.claimTxid ? { claimTxid: profile.claimTxid } : {}
|
|
1402
1511
|
};
|
|
1403
1512
|
},
|
|
1404
1513
|
// We are the claimant here, so the preimage material on the hashlock is
|
|
1405
1514
|
// ours to use.
|
|
1406
1515
|
claimSecret: (profile) => ({ ...profile.signer, ...profile.hashlock }),
|
|
1407
|
-
|
|
1516
|
+
// The other half of claiming: where it pays. Written once at request time
|
|
1517
|
+
// and left alone by `project` above, so this hands back what was stored.
|
|
1518
|
+
claimDestination: (profile) => profile.payoutAddress,
|
|
1519
|
+
activityTxids: (profile) => profile.claimTxid ? [profile.claimTxid] : []
|
|
1408
1520
|
};
|
|
1409
1521
|
function onchainSendProfile(result) {
|
|
1410
1522
|
return {
|
|
@@ -1492,14 +1604,34 @@ var isRfqSwapTerminal = (state) => RFQ_SWAP_TERMINAL_STATES.includes(state);
|
|
|
1492
1604
|
|
|
1493
1605
|
// src/rfqRecord.ts
|
|
1494
1606
|
var RFQ_SWAP_RETENTION_SECONDS = 30 * 24 * 60 * 60;
|
|
1607
|
+
function renameLegacyClaimTxid(profile) {
|
|
1608
|
+
const { claimArkTxid, ...rest } = profile;
|
|
1609
|
+
return { ...rest, claimTxid: rest.claimTxid ?? claimArkTxid };
|
|
1610
|
+
}
|
|
1611
|
+
function normalizeRfqSwapRecord(record) {
|
|
1612
|
+
const { fundingArkTxid, refundArkTxid, lockupSpendArkTxids, ...rest } = record;
|
|
1613
|
+
const legacyClaim = record.kind === "lightning_receive" && record.profile.claimArkTxid !== void 0;
|
|
1614
|
+
if (!fundingArkTxid && !refundArkTxid && !lockupSpendArkTxids && !legacyClaim) return record;
|
|
1615
|
+
const fundingTxid = rest.fundingTxid ?? fundingArkTxid;
|
|
1616
|
+
const refundTxid = rest.refundTxid ?? refundArkTxid;
|
|
1617
|
+
const lockupSpendTxids = rest.lockupSpendTxids ?? lockupSpendArkTxids;
|
|
1618
|
+
return {
|
|
1619
|
+
...rest,
|
|
1620
|
+
...fundingTxid ? { fundingTxid } : {},
|
|
1621
|
+
...refundTxid ? { refundTxid } : {},
|
|
1622
|
+
...lockupSpendTxids?.length ? { lockupSpendTxids } : {},
|
|
1623
|
+
...legacyClaim ? { profile: renameLegacyClaimTxid(record.profile) } : {}
|
|
1624
|
+
};
|
|
1625
|
+
}
|
|
1495
1626
|
var managerState = (swap) => ({
|
|
1496
1627
|
rfqId: swap.rfqId,
|
|
1497
1628
|
state: swap.state,
|
|
1498
1629
|
createdAt: swap.createdAt,
|
|
1499
1630
|
updatedAt: swap.updatedAt,
|
|
1500
|
-
...swap.
|
|
1501
|
-
...swap.
|
|
1631
|
+
...swap.refundTxid ? { refundTxid: swap.refundTxid } : {},
|
|
1632
|
+
...swap.lockupSpendTxids?.length ? { lockupSpendTxids: [...swap.lockupSpendTxids] } : {},
|
|
1502
1633
|
...swap.failure ? { failure: swap.failure } : {},
|
|
1634
|
+
...swap.claimFailure ? { claimFailure: swap.claimFailure } : {},
|
|
1503
1635
|
...swap.blockedReason ? { blockedReason: swap.blockedReason } : {}
|
|
1504
1636
|
});
|
|
1505
1637
|
function assertSameSwap(origin, swap) {
|
|
@@ -1529,27 +1661,30 @@ function createRfqSwapRecord(origin, swap) {
|
|
|
1529
1661
|
}
|
|
1530
1662
|
function updateRfqSwapRecord(record, swap) {
|
|
1531
1663
|
assertSameSwap(record, swap);
|
|
1664
|
+
const stored = normalizeRfqSwapRecord(record);
|
|
1532
1665
|
const {
|
|
1533
|
-
|
|
1534
|
-
|
|
1666
|
+
refundTxid: _refundTxid,
|
|
1667
|
+
lockupSpendTxids: _lockupSpendTxids,
|
|
1535
1668
|
failure: _failure,
|
|
1669
|
+
claimFailure: _claimFailure,
|
|
1536
1670
|
blockedReason: _blockedReason,
|
|
1537
1671
|
...origin
|
|
1538
|
-
} =
|
|
1539
|
-
const handler = rfqCorridorHandlers.getOrThrow(
|
|
1672
|
+
} = stored;
|
|
1673
|
+
const handler = rfqCorridorHandlers.getOrThrow(stored.kind);
|
|
1540
1674
|
return {
|
|
1541
1675
|
...origin,
|
|
1542
1676
|
...managerState(swap),
|
|
1543
|
-
profile: { ...
|
|
1677
|
+
profile: { ...stored.profile, ...handler.project(swap) }
|
|
1544
1678
|
};
|
|
1545
1679
|
}
|
|
1546
1680
|
function rfqSwapOriginOf(record) {
|
|
1681
|
+
const stored = normalizeRfqSwapRecord(record);
|
|
1547
1682
|
return {
|
|
1548
|
-
kind:
|
|
1549
|
-
lockupAddress:
|
|
1550
|
-
profile: { ...
|
|
1551
|
-
...
|
|
1552
|
-
...
|
|
1683
|
+
kind: stored.kind,
|
|
1684
|
+
lockupAddress: stored.lockupAddress,
|
|
1685
|
+
profile: { ...stored.profile },
|
|
1686
|
+
...stored.amount !== void 0 ? { amount: stored.amount } : {},
|
|
1687
|
+
...stored.fundingTxid ? { fundingTxid: stored.fundingTxid } : {}
|
|
1553
1688
|
};
|
|
1554
1689
|
}
|
|
1555
1690
|
function lockupScript(params, lockupAddress) {
|
|
@@ -1563,27 +1698,29 @@ function lockupScript(params, lockupAddress) {
|
|
|
1563
1698
|
return script;
|
|
1564
1699
|
}
|
|
1565
1700
|
function rebuildRfqSwap(record, params) {
|
|
1566
|
-
const
|
|
1701
|
+
const stored = normalizeRfqSwapRecord(record);
|
|
1702
|
+
const script = lockupScript(params, stored.lockupAddress);
|
|
1567
1703
|
const common = {
|
|
1568
|
-
rfqId:
|
|
1569
|
-
state:
|
|
1704
|
+
rfqId: stored.rfqId,
|
|
1705
|
+
state: stored.state,
|
|
1570
1706
|
lockupPkScript: script.pkScript,
|
|
1571
|
-
lockup: { script, address:
|
|
1707
|
+
lockup: { script, address: stored.lockupAddress },
|
|
1572
1708
|
// From the covenant, which binds it: the record's own copy would be a
|
|
1573
1709
|
// second source for the deadline the refund is gated on.
|
|
1574
1710
|
refundLocktime: Number(script.options.refundLocktime),
|
|
1575
|
-
createdAt:
|
|
1576
|
-
updatedAt:
|
|
1577
|
-
...
|
|
1578
|
-
...
|
|
1579
|
-
...
|
|
1580
|
-
...
|
|
1711
|
+
createdAt: stored.createdAt,
|
|
1712
|
+
updatedAt: stored.updatedAt,
|
|
1713
|
+
...stored.refundTxid ? { refundTxid: stored.refundTxid } : {},
|
|
1714
|
+
...stored.lockupSpendTxids?.length ? { lockupSpendTxids: [...stored.lockupSpendTxids] } : {},
|
|
1715
|
+
...stored.failure ? { failure: stored.failure } : {},
|
|
1716
|
+
...stored.claimFailure ? { claimFailure: stored.claimFailure } : {},
|
|
1717
|
+
...stored.blockedReason ? { blockedReason: stored.blockedReason } : {}
|
|
1581
1718
|
};
|
|
1582
|
-
const handler = rfqCorridorHandlers.getOrThrow(
|
|
1719
|
+
const handler = rfqCorridorHandlers.getOrThrow(stored.kind);
|
|
1583
1720
|
return {
|
|
1584
1721
|
...common,
|
|
1585
|
-
kind:
|
|
1586
|
-
...handler.hydrate(
|
|
1722
|
+
kind: stored.kind,
|
|
1723
|
+
...handler.hydrate(stored.profile, { lockup: script })
|
|
1587
1724
|
};
|
|
1588
1725
|
}
|
|
1589
1726
|
function shouldRetainRfqSwap(record, now) {
|
|
@@ -1620,10 +1757,10 @@ async function fetchParsedTxs(indexer, txids) {
|
|
|
1620
1757
|
var unscannedSwapCandidates = (txs, existingIds, scanned) => txs.filter(
|
|
1621
1758
|
(tx) => tx.type === "sent" && tx.redeemTxid && !existingIds.has(tx.redeemTxid) && !scanned.has(tx.redeemTxid)
|
|
1622
1759
|
);
|
|
1623
|
-
function classifySpend(offer,
|
|
1760
|
+
function classifySpend(offer, operatorPubkey, spendTx, deposit) {
|
|
1624
1761
|
let leaves;
|
|
1625
1762
|
try {
|
|
1626
|
-
const script =
|
|
1763
|
+
const script = offerContract(offer, operatorPubkey);
|
|
1627
1764
|
if (import_base6.hex.encode(script.pkScript) !== import_base6.hex.encode(offer.swapPkScript)) return "indeterminate";
|
|
1628
1765
|
leaves = {
|
|
1629
1766
|
// both routes that hand the deposit back; `exit` is absent on an
|
|
@@ -1647,15 +1784,15 @@ function classifySpend(offer, serverPubkey, spendTx, deposit) {
|
|
|
1647
1784
|
return "indeterminate";
|
|
1648
1785
|
}
|
|
1649
1786
|
var spendTxidsOf = (vtxo) => [vtxo.spentBy, vtxo.arkTxId].filter((id) => Boolean(id));
|
|
1650
|
-
function classifyDepositSpend(offer,
|
|
1787
|
+
function classifyDepositSpend(offer, operatorPubkey, spendTxs, deposit) {
|
|
1651
1788
|
for (const tx of spendTxs) {
|
|
1652
|
-
const kind = classifySpend(offer,
|
|
1789
|
+
const kind = classifySpend(offer, operatorPubkey, tx, deposit);
|
|
1653
1790
|
if (kind !== "indeterminate") return kind;
|
|
1654
1791
|
}
|
|
1655
1792
|
return "indeterminate";
|
|
1656
1793
|
}
|
|
1657
1794
|
async function restoreAssetSwaps(indexer, txs, existingIds, opts) {
|
|
1658
|
-
const {
|
|
1795
|
+
const { operatorPubkey, scanned = /* @__PURE__ */ new Set() } = opts;
|
|
1659
1796
|
const candidates = unscannedSwapCandidates(txs, existingIds, scanned);
|
|
1660
1797
|
if (candidates.length === 0) return { restored: [], scannedTxids: [] };
|
|
1661
1798
|
const byTxid = new Map(candidates.map((tx) => [tx.redeemTxid, tx]));
|
|
@@ -1724,7 +1861,7 @@ async function restoreAssetSwaps(indexer, txs, existingIds, opts) {
|
|
|
1724
1861
|
if (state === "swept") status = "recoverable";
|
|
1725
1862
|
else if (state === "spent") {
|
|
1726
1863
|
const spendTxs = spendTxidsOf(vtxo).map((id) => spendTxByTxid.get(id)).filter((tx) => tx !== void 0);
|
|
1727
|
-
const kind = classifyDepositSpend(offer,
|
|
1864
|
+
const kind = classifyDepositSpend(offer, operatorPubkey, spendTxs, {
|
|
1728
1865
|
txid: vtxo.txid,
|
|
1729
1866
|
vout: vtxo.vout
|
|
1730
1867
|
});
|
|
@@ -1741,7 +1878,7 @@ async function restoreAssetSwaps(indexer, txs, existingIds, opts) {
|
|
|
1741
1878
|
fromAmount,
|
|
1742
1879
|
toAmount: offer.wantAmount.toString(),
|
|
1743
1880
|
// ponytail(arkade-os/ts-sdk#680): empty address makes cancel fall back
|
|
1744
|
-
// to the current
|
|
1881
|
+
// to the current operator key; store the funded address if operator-key
|
|
1745
1882
|
// rotations become real (cancelOffer now at least diagnoses the
|
|
1746
1883
|
// mismatch instead of reporting a missing VTXO)
|
|
1747
1884
|
swapAddress: "",
|
|
@@ -1763,6 +1900,10 @@ async function restoreAssetSwaps(indexer, txs, existingIds, opts) {
|
|
|
1763
1900
|
var import_base7 = require("@scure/base");
|
|
1764
1901
|
var import_sdk7 = require("@arkade-os/sdk");
|
|
1765
1902
|
var TERMINAL = ["fulfilled", "cancelled", "recoverable"];
|
|
1903
|
+
var assetSwapSource = (repository) => ({
|
|
1904
|
+
list: () => getAssetSwaps(repository),
|
|
1905
|
+
apply: async (swap, changes) => updateAssetSwapBestEffort(repository, swap.id, changes)
|
|
1906
|
+
});
|
|
1766
1907
|
function spendUpdate(swap, spend) {
|
|
1767
1908
|
if (TERMINAL.includes(swap.status)) return void 0;
|
|
1768
1909
|
if (spend.kind === "indeterminate") return void 0;
|
|
@@ -1776,13 +1917,20 @@ function spendUpdate(swap, spend) {
|
|
|
1776
1917
|
}
|
|
1777
1918
|
async function watchOfferSwaps({
|
|
1778
1919
|
wallet,
|
|
1779
|
-
arkServerUrl,
|
|
1780
1920
|
repository,
|
|
1921
|
+
source,
|
|
1781
1922
|
onUpdate
|
|
1782
1923
|
}) {
|
|
1783
|
-
|
|
1784
|
-
|
|
1785
|
-
|
|
1924
|
+
if (!source && !repository) {
|
|
1925
|
+
throw new Error("watchOfferSwaps needs either a record source or a repository");
|
|
1926
|
+
}
|
|
1927
|
+
const records = source ?? assetSwapSource(repository);
|
|
1928
|
+
const [manager, address, indexer] = await Promise.all([
|
|
1929
|
+
wallet.getContractManager(),
|
|
1930
|
+
wallet.getAddress(),
|
|
1931
|
+
wallet.getArkadeReader()
|
|
1932
|
+
]);
|
|
1933
|
+
const operatorPubkey = import_sdk7.ArkAddress.decode(address).serverPubKey;
|
|
1786
1934
|
let queue = Promise.resolve();
|
|
1787
1935
|
const enqueue = (task) => {
|
|
1788
1936
|
queue = queue.then(task).catch(() => {
|
|
@@ -1796,7 +1944,7 @@ async function watchOfferSwaps({
|
|
|
1796
1944
|
const { txs } = await indexer.getVirtualTxs(candidates);
|
|
1797
1945
|
return classifyDepositSpend(
|
|
1798
1946
|
decodeOffer(import_base7.hex.decode(swap.offerHex)),
|
|
1799
|
-
|
|
1947
|
+
operatorPubkey,
|
|
1800
1948
|
txs.map((psbt) => import_sdk7.Transaction.fromPSBT(import_base7.base64.decode(psbt))),
|
|
1801
1949
|
{ txid: vtxo.txid, vout: vtxo.vout }
|
|
1802
1950
|
);
|
|
@@ -1809,20 +1957,16 @@ async function watchOfferSwaps({
|
|
|
1809
1957
|
for (const vtxo of event.vtxos) {
|
|
1810
1958
|
const spentTxid = vtxo.arkTxId || vtxo.spentBy;
|
|
1811
1959
|
if (!spentTxid) continue;
|
|
1812
|
-
const swap = (await
|
|
1960
|
+
const swap = (await records.list()).find(
|
|
1813
1961
|
(s) => s.fundingTxid === vtxo.txid && s.swapPkScript === event.contractScript
|
|
1814
1962
|
);
|
|
1815
1963
|
if (!swap) continue;
|
|
1816
1964
|
const kind = await classify(swap, vtxo, spentTxid);
|
|
1817
1965
|
const changes = spendUpdate(swap, { txid: spentTxid, kind, at: event.timestamp });
|
|
1818
1966
|
if (!changes) continue;
|
|
1819
|
-
const { persisted, swaps } = await
|
|
1820
|
-
repository,
|
|
1821
|
-
swap.id,
|
|
1822
|
-
changes
|
|
1823
|
-
);
|
|
1967
|
+
const { persisted, swaps } = await records.apply(swap, changes);
|
|
1824
1968
|
if (!persisted) continue;
|
|
1825
|
-
onUpdate?.(
|
|
1969
|
+
onUpdate?.(swaps.find((s) => s.id === swap.id) ?? swap);
|
|
1826
1970
|
if (changes.status && RETIRABLE.includes(changes.status)) {
|
|
1827
1971
|
await retireOfferContract(manager, swaps, event.contractScript);
|
|
1828
1972
|
}
|
|
@@ -1950,11 +2094,14 @@ var ONCHAIN_SEND_PAIR = rfqPair(ARKADE_BTC, ONCHAIN_BTC);
|
|
|
1950
2094
|
var ONCHAIN_RECEIVE_PAIR = rfqPair(ONCHAIN_BTC, ARKADE_BTC);
|
|
1951
2095
|
var RFQ_TERMINAL_STATES = ["settled", "refused", "expired", "refunded", "stuck"];
|
|
1952
2096
|
var SwapRefusal = class extends Error {
|
|
2097
|
+
/** Literal-typed so the v2 error taxonomy's union discriminates on `name`
|
|
2098
|
+
* — a `string` here collapses the discriminant for every member. Same value
|
|
2099
|
+
* the constructor has always set, moved to a field initializer. */
|
|
2100
|
+
name = "SwapRefusal";
|
|
1953
2101
|
reason;
|
|
1954
2102
|
rfqId;
|
|
1955
2103
|
constructor(reason, rfqId) {
|
|
1956
2104
|
super(`solver refused: ${reason}`);
|
|
1957
|
-
this.name = "SwapRefusal";
|
|
1958
2105
|
this.reason = reason;
|
|
1959
2106
|
this.rfqId = rfqId;
|
|
1960
2107
|
}
|
|
@@ -2055,6 +2202,10 @@ var assertFundable = (input) => {
|
|
|
2055
2202
|
const fail = (reason, message) => {
|
|
2056
2203
|
throw gateError(reason, message);
|
|
2057
2204
|
};
|
|
2205
|
+
if (input.quote.valid_until === void 0) {
|
|
2206
|
+
fail("quote_malformed", "quote carries no valid_until");
|
|
2207
|
+
}
|
|
2208
|
+
assertFinite(input.quote.valid_until, "quote_malformed", "quote valid_until");
|
|
2058
2209
|
if (input.invoiceExpiresAt !== void 0 && input.now >= input.invoiceExpiresAt) {
|
|
2059
2210
|
fail("invoice_expired", "invoice expired");
|
|
2060
2211
|
}
|
|
@@ -2257,22 +2408,22 @@ var relayTransport = (relayUrl, options) => {
|
|
|
2257
2408
|
};
|
|
2258
2409
|
var SEQUENCE_GRANULARITY_SECONDS = 512;
|
|
2259
2410
|
var SOLO_REFUND_HEADROOM_SECONDS = 8 * SEQUENCE_GRANULARITY_SECONDS;
|
|
2260
|
-
var unilateralClaimDelay = (
|
|
2261
|
-
if (!Number.isFinite(
|
|
2411
|
+
var unilateralClaimDelay = (operatorExitDelaySeconds) => {
|
|
2412
|
+
if (!Number.isFinite(operatorExitDelaySeconds) || operatorExitDelaySeconds < SEQUENCE_GRANULARITY_SECONDS) {
|
|
2262
2413
|
throw new Error(
|
|
2263
|
-
`
|
|
2414
|
+
`operator exit delay must be at least ${SEQUENCE_GRANULARITY_SECONDS}s of seconds, got ${operatorExitDelaySeconds}`
|
|
2264
2415
|
);
|
|
2265
2416
|
}
|
|
2266
|
-
if (
|
|
2417
|
+
if (operatorExitDelaySeconds > 65535 * SEQUENCE_GRANULARITY_SECONDS - SOLO_REFUND_HEADROOM_SECONDS) {
|
|
2267
2418
|
throw new Error(
|
|
2268
|
-
`
|
|
2419
|
+
`operator exit delay ${operatorExitDelaySeconds}s exceeds what BIP68 can encode once the solo refund's headroom is stacked above it`
|
|
2269
2420
|
);
|
|
2270
2421
|
}
|
|
2271
|
-
return Math.ceil(
|
|
2422
|
+
return Math.ceil(operatorExitDelaySeconds / SEQUENCE_GRANULARITY_SECONDS) * SEQUENCE_GRANULARITY_SECONDS;
|
|
2272
2423
|
};
|
|
2273
2424
|
var unilateralRefundDelay = (claimDelay) => claimDelay;
|
|
2274
2425
|
var unilateralRefundWithoutReceiverDelay = (claimDelay) => claimDelay + SOLO_REFUND_HEADROOM_SECONDS;
|
|
2275
|
-
function
|
|
2426
|
+
function lightningSendContract(params) {
|
|
2276
2427
|
const seconds = (value) => ({
|
|
2277
2428
|
type: "seconds",
|
|
2278
2429
|
value: BigInt(value)
|
|
@@ -2280,7 +2431,7 @@ function lightningSendVtxoScript(params) {
|
|
|
2280
2431
|
return new import_sdk9.VHTLC.ScriptV2({
|
|
2281
2432
|
sender: params.senderPubkey,
|
|
2282
2433
|
receiver: params.solverPubkey,
|
|
2283
|
-
server: params.
|
|
2434
|
+
server: params.operatorPubkey,
|
|
2284
2435
|
preimageHash: (0, import_legacy2.ripemd160)(import_base10.hex.decode(params.paymentHash)),
|
|
2285
2436
|
refundLocktime: BigInt(params.refundLocktime),
|
|
2286
2437
|
unilateralClaimDelay: seconds(params.claimDelay),
|
|
@@ -2296,15 +2447,8 @@ function lightningSendVtxoScript(params) {
|
|
|
2296
2447
|
}
|
|
2297
2448
|
});
|
|
2298
2449
|
}
|
|
2299
|
-
|
|
2300
|
-
const
|
|
2301
|
-
const secrets = await (0, import_sdk10.provisionRefundKey)(wallet);
|
|
2302
|
-
const senderPubkey = secrets.pubkey;
|
|
2303
|
-
const refundAddress = secrets.address;
|
|
2304
|
-
const info = await new import_sdk9.RestArkProvider(arkServerUrl).getInfo();
|
|
2305
|
-
const quote = await transport.requestQuote(
|
|
2306
|
-
lightningSendRequest({ rfqId, invoice: params.invoice.raw, refundAddress, senderPubkey })
|
|
2307
|
-
);
|
|
2450
|
+
function deriveLightningSend(input) {
|
|
2451
|
+
const { quote } = input;
|
|
2308
2452
|
if (quote.refund_locktime === void 0) {
|
|
2309
2453
|
throw new Error("lightning-send quote is missing refund_locktime");
|
|
2310
2454
|
}
|
|
@@ -2312,6 +2456,43 @@ async function requestLightningSend(wallet, arkServerUrl, transport, params) {
|
|
|
2312
2456
|
if (receiverPkScriptHex === void 0) {
|
|
2313
2457
|
throw new Error("lightning-send quote is missing profile.receiver_pk_script");
|
|
2314
2458
|
}
|
|
2459
|
+
const contractParams = {
|
|
2460
|
+
solverPubkey: (0, import_sdk9.toXOnly)(import_base10.hex.decode(quote.solver_pubkey), "solver key"),
|
|
2461
|
+
refundLocktime: quote.refund_locktime,
|
|
2462
|
+
operatorPubkey: input.operatorPubkey,
|
|
2463
|
+
paymentHash: input.paymentHash,
|
|
2464
|
+
claimDelay: input.claimDelay,
|
|
2465
|
+
emulatorPubkey: input.emulatorPubkey,
|
|
2466
|
+
senderPubkey: input.senderPubkey,
|
|
2467
|
+
receiverPkScript: solverHex(receiverPkScriptHex, "profile.receiver_pk_script"),
|
|
2468
|
+
refundPkScript: input.refundPkScript
|
|
2469
|
+
};
|
|
2470
|
+
const matched = matchQuotedLockup(
|
|
2471
|
+
quote,
|
|
2472
|
+
input.hrp,
|
|
2473
|
+
input.operatorPubkey,
|
|
2474
|
+
(legacy) => lightningSendContract({ ...contractParams, ...legacy !== void 0 && { legacy } })
|
|
2475
|
+
);
|
|
2476
|
+
return {
|
|
2477
|
+
address: matched.address,
|
|
2478
|
+
swapPkScript: matched.script.pkScript,
|
|
2479
|
+
script: matched.script,
|
|
2480
|
+
contractParams: {
|
|
2481
|
+
...contractParams,
|
|
2482
|
+
...matched.legacy !== void 0 && { legacy: matched.legacy }
|
|
2483
|
+
},
|
|
2484
|
+
refundLocktime: quote.refund_locktime
|
|
2485
|
+
};
|
|
2486
|
+
}
|
|
2487
|
+
async function requestLightningSend(wallet, transport, params) {
|
|
2488
|
+
const rfqId = params.rfqId ?? newRfqId();
|
|
2489
|
+
const secrets = await (0, import_sdk10.provisionRefundKey)(wallet);
|
|
2490
|
+
const senderPubkey = secrets.pubkey;
|
|
2491
|
+
const refundAddress = secrets.address;
|
|
2492
|
+
const info = await wallet.getArkadeInfo({ requireLive: true });
|
|
2493
|
+
const quote = await transport.requestQuote(
|
|
2494
|
+
lightningSendRequest({ rfqId, invoice: params.invoice.raw, refundAddress, senderPubkey })
|
|
2495
|
+
);
|
|
2315
2496
|
if (quote.to_amount !== params.invoice.amountSats) {
|
|
2316
2497
|
throw new Error(
|
|
2317
2498
|
`quote to_amount ${quote.to_amount} does not match the invoice's ${params.invoice.amountSats}`
|
|
@@ -2322,34 +2503,22 @@ async function requestLightningSend(wallet, arkServerUrl, transport, params) {
|
|
|
2322
2503
|
`quote from_amount ${quote.from_amount} is below the invoice amount \u2014 a negative spread is not a quote`
|
|
2323
2504
|
);
|
|
2324
2505
|
}
|
|
2325
|
-
const
|
|
2326
|
-
const network = (0, import_sdk9.
|
|
2327
|
-
const
|
|
2328
|
-
|
|
2329
|
-
refundLocktime: quote.refund_locktime,
|
|
2330
|
-
serverPubkey,
|
|
2506
|
+
const operatorPubkey = (0, import_sdk9.toXOnly)(import_base10.hex.decode(info.signerPubkey), "ark signer key");
|
|
2507
|
+
const network = (0, import_sdk9.networkFromArkadeInfo)(info);
|
|
2508
|
+
const derived = deriveLightningSend({
|
|
2509
|
+
quote,
|
|
2331
2510
|
paymentHash: params.invoice.paymentHash,
|
|
2332
|
-
|
|
2511
|
+
senderPubkey,
|
|
2512
|
+
refundPkScript: secrets.pkScript,
|
|
2513
|
+
operatorPubkey,
|
|
2333
2514
|
emulatorPubkey: (0, import_sdk9.toXOnly)(
|
|
2334
2515
|
import_base10.hex.decode((0, import_sdk9.resolveEmulatorPubkey)(network, params.emulatorPubkey)),
|
|
2335
2516
|
"emulator signer key"
|
|
2336
2517
|
),
|
|
2337
|
-
|
|
2338
|
-
|
|
2339
|
-
|
|
2340
|
-
};
|
|
2341
|
-
const matched = matchQuotedLockup(
|
|
2342
|
-
quote,
|
|
2343
|
-
network.hrp,
|
|
2344
|
-
serverPubkey,
|
|
2345
|
-
(legacy) => lightningSendVtxoScript({ ...treeParams, ...legacy !== void 0 && { legacy } })
|
|
2346
|
-
);
|
|
2347
|
-
const script = matched.script;
|
|
2348
|
-
const address = matched.address;
|
|
2349
|
-
const matchedTreeParams = {
|
|
2350
|
-
...treeParams,
|
|
2351
|
-
...matched.legacy !== void 0 && { legacy: matched.legacy }
|
|
2352
|
-
};
|
|
2518
|
+
claimDelay: unilateralClaimDelay(Number(info.unilateralExitDelay)),
|
|
2519
|
+
hrp: network.hrp
|
|
2520
|
+
});
|
|
2521
|
+
const { address, script, contractParams } = derived;
|
|
2353
2522
|
assertFundable({
|
|
2354
2523
|
quote,
|
|
2355
2524
|
invoiceExpiresAt: params.invoice.expiresAt,
|
|
@@ -2363,12 +2532,12 @@ async function requestLightningSend(wallet, arkServerUrl, transport, params) {
|
|
|
2363
2532
|
// What the lockup must carry: the quote's `from_amount` — the invoice
|
|
2364
2533
|
// PLUS the corridor's fee, never the bare invoice amount.
|
|
2365
2534
|
fundAmount: quote.from_amount,
|
|
2366
|
-
swapPkScript:
|
|
2535
|
+
swapPkScript: derived.swapPkScript,
|
|
2367
2536
|
script,
|
|
2368
2537
|
refundAddress,
|
|
2369
2538
|
senderPubkey,
|
|
2370
2539
|
secrets,
|
|
2371
|
-
|
|
2540
|
+
contractParams
|
|
2372
2541
|
};
|
|
2373
2542
|
}
|
|
2374
2543
|
var offerTermsFromQuote = (quote, assets) => {
|
|
@@ -2433,10 +2602,10 @@ function deriveOnchainSend(input) {
|
|
|
2433
2602
|
if (refundLocktime === void 0 || htlcPubkey === void 0 || htlcLocktime === void 0 || minConfirmations === void 0 || receiverPkScriptHex === void 0) {
|
|
2434
2603
|
throw new Error("onchain-send quote is missing a binding field");
|
|
2435
2604
|
}
|
|
2436
|
-
const
|
|
2605
|
+
const contractParams = {
|
|
2437
2606
|
solverPubkey: (0, import_sdk9.toXOnly)(import_base10.hex.decode(quote.solver_pubkey), "solver key"),
|
|
2438
2607
|
refundLocktime,
|
|
2439
|
-
|
|
2608
|
+
operatorPubkey: input.operatorPubkey,
|
|
2440
2609
|
paymentHash: input.paymentHash,
|
|
2441
2610
|
claimDelay: input.claimDelay,
|
|
2442
2611
|
emulatorPubkey: input.emulatorPubkey,
|
|
@@ -2447,8 +2616,8 @@ function deriveOnchainSend(input) {
|
|
|
2447
2616
|
const { script, address } = matchQuotedLockup(
|
|
2448
2617
|
quote,
|
|
2449
2618
|
input.hrp,
|
|
2450
|
-
input.
|
|
2451
|
-
(legacy) =>
|
|
2619
|
+
input.operatorPubkey,
|
|
2620
|
+
(legacy) => lightningSendContract({ ...contractParams, ...legacy !== void 0 && { legacy } })
|
|
2452
2621
|
);
|
|
2453
2622
|
const htlcParams = {
|
|
2454
2623
|
paymentHash: input.paymentHash,
|
|
@@ -2470,7 +2639,7 @@ function deriveOnchainSend(input) {
|
|
|
2470
2639
|
minConfirmations
|
|
2471
2640
|
};
|
|
2472
2641
|
}
|
|
2473
|
-
async function requestOnchainSend(wallet,
|
|
2642
|
+
async function requestOnchainSend(wallet, transport, params) {
|
|
2474
2643
|
const rfqId = params.rfqId ?? newRfqId();
|
|
2475
2644
|
const secrets = await (0, import_sdk10.provisionClaimSecret)(wallet, { preimage: params.preimage });
|
|
2476
2645
|
if (secrets.mustPersistPreimage) {
|
|
@@ -2481,7 +2650,7 @@ async function requestOnchainSend(wallet, arkServerUrl, transport, params) {
|
|
|
2481
2650
|
const paymentHash = import_base10.hex.encode(secrets.paymentHash);
|
|
2482
2651
|
const senderPubkey = secrets.pubkey;
|
|
2483
2652
|
const [info, refundAddress] = await Promise.all([
|
|
2484
|
-
|
|
2653
|
+
wallet.getArkadeInfo({ requireLive: true }),
|
|
2485
2654
|
wallet.getAddress()
|
|
2486
2655
|
]);
|
|
2487
2656
|
const quote = await transport.requestQuote(
|
|
@@ -2496,12 +2665,12 @@ async function requestOnchainSend(wallet, arkServerUrl, transport, params) {
|
|
|
2496
2665
|
})
|
|
2497
2666
|
);
|
|
2498
2667
|
assertQuotedAmount(quote, params.amountSide, params.amount);
|
|
2499
|
-
const network = (0, import_sdk9.
|
|
2668
|
+
const network = (0, import_sdk9.networkFromArkadeInfo)(info);
|
|
2500
2669
|
const derived = deriveOnchainSend({
|
|
2501
2670
|
quote,
|
|
2502
2671
|
paymentHash,
|
|
2503
2672
|
payoutPubkey: params.payoutPubkey,
|
|
2504
|
-
|
|
2673
|
+
operatorPubkey: (0, import_sdk9.toXOnly)(import_base10.hex.decode(info.signerPubkey), "ark signer key"),
|
|
2505
2674
|
emulatorPubkey: (0, import_sdk9.toXOnly)(
|
|
2506
2675
|
import_base10.hex.decode((0, import_sdk9.resolveEmulatorPubkey)(network, params.emulatorPubkey)),
|
|
2507
2676
|
"emulator signer key"
|
|
@@ -2538,6 +2707,7 @@ async function requestOnchainSend(wallet, arkServerUrl, transport, params) {
|
|
|
2538
2707
|
htlcParams: derived.htlcParams,
|
|
2539
2708
|
l1Network: derived.l1Network,
|
|
2540
2709
|
minConfirmations: derived.minConfirmations,
|
|
2710
|
+
refundLocktime: derived.refundLocktime,
|
|
2541
2711
|
senderPubkey,
|
|
2542
2712
|
secrets
|
|
2543
2713
|
};
|
|
@@ -2609,7 +2779,7 @@ var assertReceivable = (input) => {
|
|
|
2609
2779
|
);
|
|
2610
2780
|
}
|
|
2611
2781
|
};
|
|
2612
|
-
function
|
|
2782
|
+
function lightningReceiveContract(params) {
|
|
2613
2783
|
const seconds = (value) => ({
|
|
2614
2784
|
type: "seconds",
|
|
2615
2785
|
value: BigInt(value)
|
|
@@ -2617,7 +2787,7 @@ function receiveVtxoScript(params) {
|
|
|
2617
2787
|
return new import_sdk9.VHTLC.ScriptV2({
|
|
2618
2788
|
sender: params.solverPubkey,
|
|
2619
2789
|
receiver: params.payoutPubkey,
|
|
2620
|
-
server: params.
|
|
2790
|
+
server: params.operatorPubkey,
|
|
2621
2791
|
preimageHash: (0, import_legacy2.ripemd160)(import_base10.hex.decode(params.paymentHash)),
|
|
2622
2792
|
refundLocktime: BigInt(params.refundLocktime),
|
|
2623
2793
|
unilateralClaimDelay: seconds(params.claimDelay),
|
|
@@ -2642,10 +2812,10 @@ function deriveLightningReceive(input) {
|
|
|
2642
2812
|
if (refundLocktime === void 0 || invoice === void 0 || solverRefundPkScriptHex === void 0) {
|
|
2643
2813
|
throw new Error("lightning-receive quote is missing a binding field");
|
|
2644
2814
|
}
|
|
2645
|
-
const
|
|
2815
|
+
const contractParams = {
|
|
2646
2816
|
solverPubkey: (0, import_sdk9.toXOnly)(import_base10.hex.decode(quote.solver_pubkey), "solver key"),
|
|
2647
2817
|
refundLocktime,
|
|
2648
|
-
|
|
2818
|
+
operatorPubkey: input.operatorPubkey,
|
|
2649
2819
|
paymentHash: input.paymentHash,
|
|
2650
2820
|
claimDelay: input.claimDelay,
|
|
2651
2821
|
emulatorPubkey: input.emulatorPubkey,
|
|
@@ -2656,8 +2826,8 @@ function deriveLightningReceive(input) {
|
|
|
2656
2826
|
const matched = matchQuotedLockup(
|
|
2657
2827
|
quote,
|
|
2658
2828
|
input.hrp,
|
|
2659
|
-
input.
|
|
2660
|
-
(legacy) =>
|
|
2829
|
+
input.operatorPubkey,
|
|
2830
|
+
(legacy) => lightningReceiveContract({ ...contractParams, ...legacy !== void 0 && { legacy } })
|
|
2661
2831
|
);
|
|
2662
2832
|
return {
|
|
2663
2833
|
address: matched.address,
|
|
@@ -2665,13 +2835,13 @@ function deriveLightningReceive(input) {
|
|
|
2665
2835
|
script: matched.script,
|
|
2666
2836
|
invoice,
|
|
2667
2837
|
refundLocktime,
|
|
2668
|
-
|
|
2669
|
-
...
|
|
2838
|
+
contractParams: {
|
|
2839
|
+
...contractParams,
|
|
2670
2840
|
...matched.legacy !== void 0 && { legacy: matched.legacy }
|
|
2671
2841
|
}
|
|
2672
2842
|
};
|
|
2673
2843
|
}
|
|
2674
|
-
async function requestLightningReceive(wallet,
|
|
2844
|
+
async function requestLightningReceive(wallet, transport, params) {
|
|
2675
2845
|
const rfqId = params.rfqId ?? newRfqId();
|
|
2676
2846
|
const secrets = await (0, import_sdk10.provisionClaimSecret)(wallet);
|
|
2677
2847
|
if (secrets.mustPersistPreimage) {
|
|
@@ -2683,7 +2853,7 @@ async function requestLightningReceive(wallet, arkServerUrl, transport, params)
|
|
|
2683
2853
|
const paymentHash = import_base10.hex.encode(secrets.paymentHash);
|
|
2684
2854
|
const payoutPubkey = secrets.pubkey;
|
|
2685
2855
|
const [info, payoutAddress] = await Promise.all([
|
|
2686
|
-
|
|
2856
|
+
wallet.getArkadeInfo({ requireLive: true }),
|
|
2687
2857
|
wallet.getAddress()
|
|
2688
2858
|
]);
|
|
2689
2859
|
const claimPacket = await sealClaimPacket({
|
|
@@ -2702,13 +2872,13 @@ async function requestLightningReceive(wallet, arkServerUrl, transport, params)
|
|
|
2702
2872
|
})
|
|
2703
2873
|
);
|
|
2704
2874
|
assertQuotedAmount(quote, params.amountSide, params.amount);
|
|
2705
|
-
const network = (0, import_sdk9.
|
|
2875
|
+
const network = (0, import_sdk9.networkFromArkadeInfo)(info);
|
|
2706
2876
|
const derived = deriveLightningReceive({
|
|
2707
2877
|
quote,
|
|
2708
2878
|
paymentHash,
|
|
2709
2879
|
payoutPubkey,
|
|
2710
2880
|
payoutAddress,
|
|
2711
|
-
|
|
2881
|
+
operatorPubkey: (0, import_sdk9.toXOnly)(import_base10.hex.decode(info.signerPubkey), "ark signer key"),
|
|
2712
2882
|
emulatorPubkey: (0, import_sdk9.toXOnly)(
|
|
2713
2883
|
import_base10.hex.decode((0, import_sdk9.resolveEmulatorPubkey)(network, params.emulatorPubkey)),
|
|
2714
2884
|
"emulator signer key"
|
|
@@ -2742,7 +2912,7 @@ async function requestLightningReceive(wallet, arkServerUrl, transport, params)
|
|
|
2742
2912
|
payoutAddress,
|
|
2743
2913
|
payoutPubkey,
|
|
2744
2914
|
secrets,
|
|
2745
|
-
|
|
2915
|
+
contractParams: derived.contractParams
|
|
2746
2916
|
};
|
|
2747
2917
|
}
|
|
2748
2918
|
function deriveOnchainReceive(input) {
|
|
@@ -2757,10 +2927,10 @@ function deriveOnchainReceive(input) {
|
|
|
2757
2927
|
if (refundLocktime === void 0 || claimPubkey === void 0 || htlcLocktime === void 0 || minConfirmations === void 0 || solverRefundPkScriptHex === void 0) {
|
|
2758
2928
|
throw new Error("onchain-receive quote is missing a binding field");
|
|
2759
2929
|
}
|
|
2760
|
-
const
|
|
2930
|
+
const contractParams = {
|
|
2761
2931
|
solverPubkey: (0, import_sdk9.toXOnly)(import_base10.hex.decode(quote.solver_pubkey), "solver key"),
|
|
2762
2932
|
refundLocktime,
|
|
2763
|
-
|
|
2933
|
+
operatorPubkey: input.operatorPubkey,
|
|
2764
2934
|
paymentHash: input.paymentHash,
|
|
2765
2935
|
claimDelay: input.claimDelay,
|
|
2766
2936
|
emulatorPubkey: input.emulatorPubkey,
|
|
@@ -2771,8 +2941,11 @@ function deriveOnchainReceive(input) {
|
|
|
2771
2941
|
const { script, address } = matchQuotedLockup(
|
|
2772
2942
|
quote,
|
|
2773
2943
|
input.hrp,
|
|
2774
|
-
input.
|
|
2775
|
-
(legacy) =>
|
|
2944
|
+
input.operatorPubkey,
|
|
2945
|
+
(legacy) => lightningReceiveContract({
|
|
2946
|
+
...contractParams,
|
|
2947
|
+
...legacy !== void 0 && { legacy }
|
|
2948
|
+
})
|
|
2776
2949
|
);
|
|
2777
2950
|
const htlc = onchainHtlcScript(
|
|
2778
2951
|
{
|
|
@@ -2794,7 +2967,7 @@ function deriveOnchainReceive(input) {
|
|
|
2794
2967
|
minConfirmations
|
|
2795
2968
|
};
|
|
2796
2969
|
}
|
|
2797
|
-
async function requestOnchainReceive(wallet,
|
|
2970
|
+
async function requestOnchainReceive(wallet, transport, params) {
|
|
2798
2971
|
const rfqId = params.rfqId ?? newRfqId();
|
|
2799
2972
|
const secrets = await (0, import_sdk10.provisionClaimSecret)(wallet);
|
|
2800
2973
|
if (secrets.mustPersistPreimage) {
|
|
@@ -2806,7 +2979,7 @@ async function requestOnchainReceive(wallet, arkServerUrl, transport, params) {
|
|
|
2806
2979
|
const paymentHash = import_base10.hex.encode(secrets.paymentHash);
|
|
2807
2980
|
const payoutPubkey = secrets.pubkey;
|
|
2808
2981
|
const [info, payoutAddress] = await Promise.all([
|
|
2809
|
-
|
|
2982
|
+
wallet.getArkadeInfo({ requireLive: true }),
|
|
2810
2983
|
wallet.getAddress()
|
|
2811
2984
|
]);
|
|
2812
2985
|
const claimPacket = await sealClaimPacket({
|
|
@@ -2826,14 +2999,14 @@ async function requestOnchainReceive(wallet, arkServerUrl, transport, params) {
|
|
|
2826
2999
|
})
|
|
2827
3000
|
);
|
|
2828
3001
|
assertQuotedAmount(quote, params.amountSide, params.amount);
|
|
2829
|
-
const network = (0, import_sdk9.
|
|
3002
|
+
const network = (0, import_sdk9.networkFromArkadeInfo)(info);
|
|
2830
3003
|
const derived = deriveOnchainReceive({
|
|
2831
3004
|
quote,
|
|
2832
3005
|
paymentHash,
|
|
2833
3006
|
payoutPubkey,
|
|
2834
3007
|
payoutAddress,
|
|
2835
3008
|
refundPubkey: params.refundPubkey,
|
|
2836
|
-
|
|
3009
|
+
operatorPubkey: (0, import_sdk9.toXOnly)(import_base10.hex.decode(info.signerPubkey), "ark signer key"),
|
|
2837
3010
|
emulatorPubkey: (0, import_sdk9.toXOnly)(
|
|
2838
3011
|
import_base10.hex.decode((0, import_sdk9.resolveEmulatorPubkey)(network, params.emulatorPubkey)),
|
|
2839
3012
|
"emulator signer key"
|
|
@@ -2991,7 +3164,7 @@ function solverOnchainRail(deps) {
|
|
|
2991
3164
|
}
|
|
2992
3165
|
const negotiated = await deps.connect(
|
|
2993
3166
|
rendezvous,
|
|
2994
|
-
(transport) => requestOnchainSend(ctx.wallet,
|
|
3167
|
+
(transport) => requestOnchainSend(ctx.wallet, transport, {
|
|
2995
3168
|
amount,
|
|
2996
3169
|
amountSide: "to",
|
|
2997
3170
|
payoutPubkey: deps.payoutPubkey,
|
|
@@ -3104,7 +3277,7 @@ function solverLightningRail(deps) {
|
|
|
3104
3277
|
}
|
|
3105
3278
|
const negotiated = await deps.connect(
|
|
3106
3279
|
rendezvous,
|
|
3107
|
-
(transport) => requestLightningSend(ctx.wallet,
|
|
3280
|
+
(transport) => requestLightningSend(ctx.wallet, transport, {
|
|
3108
3281
|
invoice: facts,
|
|
3109
3282
|
...deps.emulatorPubkey ? { emulatorPubkey: deps.emulatorPubkey } : {}
|
|
3110
3283
|
})
|
|
@@ -3157,16 +3330,450 @@ function solverLightningRail(deps) {
|
|
|
3157
3330
|
};
|
|
3158
3331
|
}
|
|
3159
3332
|
|
|
3333
|
+
// src/payment/lightning.ts
|
|
3334
|
+
var import_sdk14 = require("@arkade-os/sdk");
|
|
3335
|
+
|
|
3336
|
+
// src/payment/swapRail.ts
|
|
3337
|
+
var import_sdk13 = require("@arkade-os/sdk");
|
|
3338
|
+
|
|
3339
|
+
// src/client/errors.ts
|
|
3340
|
+
var AmbiguousDestination = class extends Error {
|
|
3341
|
+
constructor(destination, detail) {
|
|
3342
|
+
super(`cannot route ${JSON.stringify(destination)}: ${detail}`);
|
|
3343
|
+
this.destination = destination;
|
|
3344
|
+
}
|
|
3345
|
+
destination;
|
|
3346
|
+
name = "AmbiguousDestination";
|
|
3347
|
+
};
|
|
3348
|
+
var UnsupportedRoute = class extends Error {
|
|
3349
|
+
name = "UnsupportedRoute";
|
|
3350
|
+
give;
|
|
3351
|
+
take;
|
|
3352
|
+
constructor(detail, corridors = {}) {
|
|
3353
|
+
super(`unsupported route: ${detail}`);
|
|
3354
|
+
this.give = corridors.give;
|
|
3355
|
+
this.take = corridors.take;
|
|
3356
|
+
}
|
|
3357
|
+
};
|
|
3358
|
+
var DiscoverySnapshotUnavailable = class extends Error {
|
|
3359
|
+
constructor(network, detail) {
|
|
3360
|
+
super(`cannot resolve on ${network} offline: ${detail}`);
|
|
3361
|
+
this.network = network;
|
|
3362
|
+
}
|
|
3363
|
+
network;
|
|
3364
|
+
name = "DiscoverySnapshotUnavailable";
|
|
3365
|
+
};
|
|
3366
|
+
var AmountMismatch = class extends Error {
|
|
3367
|
+
constructor(sources) {
|
|
3368
|
+
super(`exactly one amount may be pinned; got ${sources[0]} and ${sources[1]}`);
|
|
3369
|
+
this.sources = sources;
|
|
3370
|
+
}
|
|
3371
|
+
sources;
|
|
3372
|
+
name = "AmountMismatch";
|
|
3373
|
+
};
|
|
3374
|
+
var AmountEncodingUnsupported = class extends Error {
|
|
3375
|
+
constructor(field, value, detail, options) {
|
|
3376
|
+
super(`${field}: ${detail}`, options);
|
|
3377
|
+
this.field = field;
|
|
3378
|
+
this.value = value;
|
|
3379
|
+
}
|
|
3380
|
+
field;
|
|
3381
|
+
value;
|
|
3382
|
+
name = "AmountEncodingUnsupported";
|
|
3383
|
+
};
|
|
3384
|
+
var QuoteVerificationFailed = class extends Error {
|
|
3385
|
+
constructor(check, expected, actual, options) {
|
|
3386
|
+
super(`quote failed the ${check} check \u2014 refusing to fund`, options);
|
|
3387
|
+
this.check = check;
|
|
3388
|
+
this.expected = expected;
|
|
3389
|
+
this.actual = actual;
|
|
3390
|
+
}
|
|
3391
|
+
check;
|
|
3392
|
+
name = "QuoteVerificationFailed";
|
|
3393
|
+
expected;
|
|
3394
|
+
actual;
|
|
3395
|
+
};
|
|
3396
|
+
var QuoteExpired = class extends Error {
|
|
3397
|
+
constructor(quoteId, expiresAt, now) {
|
|
3398
|
+
super(`quote ${quoteId} expired at ${expiresAt}, ${now - expiresAt} ago`);
|
|
3399
|
+
this.quoteId = quoteId;
|
|
3400
|
+
this.expiresAt = expiresAt;
|
|
3401
|
+
this.now = now;
|
|
3402
|
+
}
|
|
3403
|
+
quoteId;
|
|
3404
|
+
expiresAt;
|
|
3405
|
+
now;
|
|
3406
|
+
name = "QuoteExpired";
|
|
3407
|
+
};
|
|
3408
|
+
var MaxFeeExceeded = class extends Error {
|
|
3409
|
+
constructor(quoteId, asset4, fee, maxFee) {
|
|
3410
|
+
super(`fee ${fee} of ${asset4} exceeds the ${maxFee} ceiling`);
|
|
3411
|
+
this.quoteId = quoteId;
|
|
3412
|
+
this.asset = asset4;
|
|
3413
|
+
this.fee = fee;
|
|
3414
|
+
this.maxFee = maxFee;
|
|
3415
|
+
}
|
|
3416
|
+
quoteId;
|
|
3417
|
+
asset;
|
|
3418
|
+
fee;
|
|
3419
|
+
maxFee;
|
|
3420
|
+
name = "MaxFeeExceeded";
|
|
3421
|
+
};
|
|
3422
|
+
var InsufficientFunds = class extends Error {
|
|
3423
|
+
constructor(asset4, required, available) {
|
|
3424
|
+
super(
|
|
3425
|
+
`funding needs ${required} of ${asset4}` + (available === void 0 ? "" : `, wallet holds ${available}`)
|
|
3426
|
+
);
|
|
3427
|
+
this.asset = asset4;
|
|
3428
|
+
this.required = required;
|
|
3429
|
+
this.available = available;
|
|
3430
|
+
}
|
|
3431
|
+
asset;
|
|
3432
|
+
required;
|
|
3433
|
+
name = "InsufficientFunds";
|
|
3434
|
+
available;
|
|
3435
|
+
};
|
|
3436
|
+
var AcceptConflict = class extends Error {
|
|
3437
|
+
constructor(quoteId, swapId, fields) {
|
|
3438
|
+
super(`accept ${quoteId} conflicts with swap ${swapId} on ${fields.join(", ")}`);
|
|
3439
|
+
this.quoteId = quoteId;
|
|
3440
|
+
this.swapId = swapId;
|
|
3441
|
+
this.fields = fields;
|
|
3442
|
+
}
|
|
3443
|
+
quoteId;
|
|
3444
|
+
swapId;
|
|
3445
|
+
fields;
|
|
3446
|
+
name = "AcceptConflict";
|
|
3447
|
+
};
|
|
3448
|
+
var ClientDisposed = class extends Error {
|
|
3449
|
+
constructor(method) {
|
|
3450
|
+
super(`${method}() called after the client was disposed`);
|
|
3451
|
+
this.method = method;
|
|
3452
|
+
}
|
|
3453
|
+
method;
|
|
3454
|
+
name = "ClientDisposed";
|
|
3455
|
+
};
|
|
3456
|
+
var NotCancellable = class extends Error {
|
|
3457
|
+
constructor(swapId) {
|
|
3458
|
+
super(`swap ${swapId} is not a cancellable asset swap`);
|
|
3459
|
+
this.swapId = swapId;
|
|
3460
|
+
}
|
|
3461
|
+
swapId;
|
|
3462
|
+
name = "NotCancellable";
|
|
3463
|
+
};
|
|
3464
|
+
var InconsistentRoute = class extends Error {
|
|
3465
|
+
constructor(asset4, destination) {
|
|
3466
|
+
super(`destination ${JSON.stringify(destination)} is not on ${asset4}'s chain`);
|
|
3467
|
+
this.asset = asset4;
|
|
3468
|
+
this.destination = destination;
|
|
3469
|
+
}
|
|
3470
|
+
asset;
|
|
3471
|
+
destination;
|
|
3472
|
+
name = "InconsistentRoute";
|
|
3473
|
+
};
|
|
3474
|
+
var OperatorUnreachable = class extends Error {
|
|
3475
|
+
name = "OperatorUnreachable";
|
|
3476
|
+
constructor(detail, options) {
|
|
3477
|
+
super(`cannot derive a covenant: ${detail}`, options);
|
|
3478
|
+
}
|
|
3479
|
+
};
|
|
3480
|
+
var MissingCorridorDep = class extends Error {
|
|
3481
|
+
constructor(corridor, dep) {
|
|
3482
|
+
super(`the ${corridor} corridor has no ${dep}`);
|
|
3483
|
+
this.corridor = corridor;
|
|
3484
|
+
this.dep = dep;
|
|
3485
|
+
}
|
|
3486
|
+
corridor;
|
|
3487
|
+
dep;
|
|
3488
|
+
name = "MissingCorridorDep";
|
|
3489
|
+
};
|
|
3490
|
+
var SWAP_ERRORS = {
|
|
3491
|
+
AmbiguousDestination,
|
|
3492
|
+
UnsupportedRoute,
|
|
3493
|
+
DiscoverySnapshotUnavailable,
|
|
3494
|
+
AmountMismatch,
|
|
3495
|
+
AmountEncodingUnsupported,
|
|
3496
|
+
QuoteVerificationFailed,
|
|
3497
|
+
SwapRefusal,
|
|
3498
|
+
QuoteExpired,
|
|
3499
|
+
MaxFeeExceeded,
|
|
3500
|
+
InsufficientFunds,
|
|
3501
|
+
AcceptConflict,
|
|
3502
|
+
ClientDisposed,
|
|
3503
|
+
NotCancellable,
|
|
3504
|
+
InconsistentRoute,
|
|
3505
|
+
OperatorUnreachable,
|
|
3506
|
+
MissingCorridorDep
|
|
3507
|
+
};
|
|
3508
|
+
var SWAP_ERROR_NAMES = Object.keys(SWAP_ERRORS);
|
|
3509
|
+
function isSwapError(e, name) {
|
|
3510
|
+
if (!(e instanceof Error)) return false;
|
|
3511
|
+
const table = SWAP_ERRORS;
|
|
3512
|
+
const ctor = table[e.name];
|
|
3513
|
+
if (ctor === void 0 || !(e instanceof ctor)) return false;
|
|
3514
|
+
return name === void 0 || e.name === name;
|
|
3515
|
+
}
|
|
3516
|
+
|
|
3517
|
+
// src/client/sats.ts
|
|
3518
|
+
var satsOf = (value, field) => {
|
|
3519
|
+
if (value < 0n || value > BigInt(Number.MAX_SAFE_INTEGER)) {
|
|
3520
|
+
throw new AmountEncodingUnsupported(
|
|
3521
|
+
field,
|
|
3522
|
+
`${value}`,
|
|
3523
|
+
"outside the non-negative safe-integer window core's payment amounts are numbers in"
|
|
3524
|
+
);
|
|
3525
|
+
}
|
|
3526
|
+
return Number(value);
|
|
3527
|
+
};
|
|
3528
|
+
|
|
3529
|
+
// src/payment/status.ts
|
|
3530
|
+
var PAYMENT_STATUS = {
|
|
3531
|
+
/** Persisted, funding not broadcast. */
|
|
3532
|
+
accepted: "pending",
|
|
3533
|
+
funding: "pending",
|
|
3534
|
+
/** The lockup is funded. Nothing has emitted `"sent"` in core since the
|
|
3535
|
+
* `onchain-swap` rail left with `packages/boltz-swap`. */
|
|
3536
|
+
funded: "sent",
|
|
3537
|
+
/** Asset swaps only: an unfilled offer. */
|
|
3538
|
+
open: "pending",
|
|
3539
|
+
/** Asset swaps only. */
|
|
3540
|
+
filled: "settled",
|
|
3541
|
+
/** The trader's L1 claim on `arkade -> onchain`. */
|
|
3542
|
+
claimed: "settled",
|
|
3543
|
+
/** Terminal success on `arkade -> lightning`: the solver's hash-verified
|
|
3544
|
+
* spend IS the invoice being paid. */
|
|
3545
|
+
paid: "settled",
|
|
3546
|
+
cancelling: "pending",
|
|
3547
|
+
/** Value returned, and the router has no non-loss terminal to say so with. */
|
|
3548
|
+
cancelled: "failed",
|
|
3549
|
+
/** Terminal HERE, not at `refunded` — see the module docblock. */
|
|
3550
|
+
refunding: "failed",
|
|
3551
|
+
/** The trader's value came back. Reaches `onUpdate`, not the handle. */
|
|
3552
|
+
refunded: "failed",
|
|
3553
|
+
/** The solver reclaimed a receive-leg lockup: a loss, and not `refunded`. */
|
|
3554
|
+
lapsed: "failed",
|
|
3555
|
+
/** Never retried silently; `client.recover` drives it. */
|
|
3556
|
+
needs_recovery: "failed",
|
|
3557
|
+
failed: "failed"
|
|
3558
|
+
};
|
|
3559
|
+
var paymentStatusOf = (outcome) => PAYMENT_STATUS[outcome];
|
|
3560
|
+
var isTerminalStatus = (status) => status === "settled" || status === "failed";
|
|
3561
|
+
|
|
3562
|
+
// src/payment/swapRail.ts
|
|
3563
|
+
var SwapPaymentFailedError = class extends Error {
|
|
3564
|
+
constructor(railId, outcome, swap) {
|
|
3565
|
+
super(
|
|
3566
|
+
`${railId}: swap ${swap.id} ended ${outcome}` + (swap.failure === void 0 ? "" : `: ${swap.failure}`) + (swap.blockedReason === void 0 ? "" : ` (${swap.blockedReason})`)
|
|
3567
|
+
);
|
|
3568
|
+
this.railId = railId;
|
|
3569
|
+
this.outcome = outcome;
|
|
3570
|
+
this.swap = swap;
|
|
3571
|
+
}
|
|
3572
|
+
railId;
|
|
3573
|
+
outcome;
|
|
3574
|
+
swap;
|
|
3575
|
+
name = "SwapPaymentFailedError";
|
|
3576
|
+
};
|
|
3577
|
+
var unroutable = (error) => isSwapError(error, "UnsupportedRoute") || isSwapError(error, "AmbiguousDestination");
|
|
3578
|
+
var railAvailable = async (client, input) => {
|
|
3579
|
+
try {
|
|
3580
|
+
return (await client.resolve(input)).eligible > 0;
|
|
3581
|
+
} catch (error) {
|
|
3582
|
+
if (unroutable(error)) return false;
|
|
3583
|
+
throw error;
|
|
3584
|
+
}
|
|
3585
|
+
};
|
|
3586
|
+
var receiverExact = (railId, parts) => {
|
|
3587
|
+
if (parts.total !== parts.amount + parts.fee) {
|
|
3588
|
+
throw new Error(
|
|
3589
|
+
`${railId}: ${parts.total} leaving the wallet is not ${parts.amount} delivered plus ${parts.fee} in fees \u2014 the quote is not receiver-exact`
|
|
3590
|
+
);
|
|
3591
|
+
}
|
|
3592
|
+
return {
|
|
3593
|
+
amount: satsOf(parts.amount, `${railId}.amount`),
|
|
3594
|
+
fee: satsOf(parts.fee, `${railId}.fee`),
|
|
3595
|
+
total: satsOf(parts.total, `${railId}.total`)
|
|
3596
|
+
};
|
|
3597
|
+
};
|
|
3598
|
+
var resultOf = (railId, swap) => ({
|
|
3599
|
+
railId,
|
|
3600
|
+
// The tagged public form M6 minted — round-trippable to `client.swaps()`,
|
|
3601
|
+
// which is where the outcomes past this handle's terminal one are read.
|
|
3602
|
+
swapId: swap.id,
|
|
3603
|
+
// The transaction that left THIS wallet, which is what `txid` means on
|
|
3604
|
+
// every other rail. A swap's later legs — the solver's fill, the trader's
|
|
3605
|
+
// L1 claim — are the client's to report, not the handle's.
|
|
3606
|
+
...swap.fundingTxid === void 0 ? {} : { txid: swap.fundingTxid }
|
|
3607
|
+
});
|
|
3608
|
+
var swapHandle = async (railId, client, quote) => (0, import_sdk13.makeHandle)(railId, async (emit) => {
|
|
3609
|
+
const accepted = await client.accept(quote);
|
|
3610
|
+
return await new Promise((resolve, reject) => {
|
|
3611
|
+
let finished = false;
|
|
3612
|
+
let unsubscribe;
|
|
3613
|
+
const listener = (update) => {
|
|
3614
|
+
if (finished || update.swap.id !== accepted.id) return;
|
|
3615
|
+
const status = paymentStatusOf(update.outcome);
|
|
3616
|
+
const result = resultOf(railId, update.swap);
|
|
3617
|
+
if (!isTerminalStatus(status)) {
|
|
3618
|
+
emit({ status, result });
|
|
3619
|
+
return;
|
|
3620
|
+
}
|
|
3621
|
+
finished = true;
|
|
3622
|
+
unsubscribe?.();
|
|
3623
|
+
if (status === "settled") {
|
|
3624
|
+
emit({ status, result });
|
|
3625
|
+
resolve(result);
|
|
3626
|
+
return;
|
|
3627
|
+
}
|
|
3628
|
+
reject(new SwapPaymentFailedError(railId, update.outcome, update.swap));
|
|
3629
|
+
};
|
|
3630
|
+
unsubscribe = client.onUpdate(listener);
|
|
3631
|
+
if (finished) unsubscribe();
|
|
3632
|
+
});
|
|
3633
|
+
});
|
|
3634
|
+
|
|
3635
|
+
// src/payment/lightning.ts
|
|
3636
|
+
var LIGHTNING_RAIL = "lightning";
|
|
3637
|
+
var inputFor = (invoice, amount) => ({
|
|
3638
|
+
to: invoice,
|
|
3639
|
+
...amount === void 0 ? {} : { amount: BigInt(amount), amountOn: "take" }
|
|
3640
|
+
});
|
|
3641
|
+
function lightningRail(client) {
|
|
3642
|
+
return {
|
|
3643
|
+
id: LIGHTNING_RAIL,
|
|
3644
|
+
// Amount-blind and non-throwing: classification only, which is what
|
|
3645
|
+
// keeps `match` from being a second gate on top of `available`.
|
|
3646
|
+
match: (req) => (0, import_sdk14.invoiceTarget)(req.raw) !== void 0,
|
|
3647
|
+
available: async (req) => {
|
|
3648
|
+
const invoice = (0, import_sdk14.invoiceTarget)(req.raw);
|
|
3649
|
+
if (invoice === void 0) return false;
|
|
3650
|
+
return railAvailable(client, inputFor(invoice, req.amount));
|
|
3651
|
+
},
|
|
3652
|
+
quote: async (req) => {
|
|
3653
|
+
const invoice = (0, import_sdk14.invoiceTarget)(req.raw);
|
|
3654
|
+
if (invoice === void 0) {
|
|
3655
|
+
throw new Error(`${LIGHTNING_RAIL}: the request carries no BOLT11 invoice`);
|
|
3656
|
+
}
|
|
3657
|
+
const quote = await client.quote(inputFor(invoice, req.amount));
|
|
3658
|
+
const amounts = receiverExact(LIGHTNING_RAIL, {
|
|
3659
|
+
amount: quote.take.amount,
|
|
3660
|
+
fee: quote.fee.amount,
|
|
3661
|
+
total: quote.give.amount
|
|
3662
|
+
});
|
|
3663
|
+
return {
|
|
3664
|
+
railId: LIGHTNING_RAIL,
|
|
3665
|
+
...amounts,
|
|
3666
|
+
meta: {
|
|
3667
|
+
quoteId: quote.id,
|
|
3668
|
+
// A `RouteQuote` carries no expiry of its own, so a held one
|
|
3669
|
+
// can outlive the terms behind it. This is where a caller
|
|
3670
|
+
// can see that; `send()` refuses with `QuoteExpired` rather
|
|
3671
|
+
// than silently re-quoting.
|
|
3672
|
+
expiresAt: quote.expiresAt,
|
|
3673
|
+
...quote.refundLocktime === void 0 ? {} : { refundLocktime: quote.refundLocktime },
|
|
3674
|
+
...quote.solver === void 0 ? {} : { solver: quote.solver },
|
|
3675
|
+
...quote.lock === void 0 ? {} : { paymentHash: quote.lock.hash },
|
|
3676
|
+
market: quote.market.key
|
|
3677
|
+
},
|
|
3678
|
+
send: () => swapHandle(LIGHTNING_RAIL, client, quote)
|
|
3679
|
+
};
|
|
3680
|
+
}
|
|
3681
|
+
};
|
|
3682
|
+
}
|
|
3683
|
+
|
|
3684
|
+
// src/payment/onchainSwap.ts
|
|
3685
|
+
var import_sdk15 = require("@arkade-os/sdk");
|
|
3686
|
+
var ONCHAIN_SWAP_RAIL = "onchain-swap";
|
|
3687
|
+
var claimFeeSats = (deps) => BigInt(Math.ceil((deps.claimVsize ?? ONCHAIN_CLAIM_VSIZE) * deps.claimFeeRateSatVb));
|
|
3688
|
+
function onchainSwapRail(client, deps) {
|
|
3689
|
+
if (!Number.isFinite(deps.claimFeeRateSatVb) || deps.claimFeeRateSatVb <= 0) {
|
|
3690
|
+
throw new Error(
|
|
3691
|
+
`${ONCHAIN_SWAP_RAIL}: claimFeeRateSatVb must be positive, got ${deps.claimFeeRateSatVb}`
|
|
3692
|
+
);
|
|
3693
|
+
}
|
|
3694
|
+
const claimFee = claimFeeSats(deps);
|
|
3695
|
+
const inputFor2 = (address, amount) => ({
|
|
3696
|
+
to: address,
|
|
3697
|
+
amount: BigInt(amount) + claimFee,
|
|
3698
|
+
amountOn: "take"
|
|
3699
|
+
});
|
|
3700
|
+
return {
|
|
3701
|
+
id: ONCHAIN_SWAP_RAIL,
|
|
3702
|
+
match: (req) => (0, import_sdk15.btcTarget)(req.raw) !== void 0,
|
|
3703
|
+
available: async (req) => {
|
|
3704
|
+
const address = (0, import_sdk15.btcTarget)(req.raw);
|
|
3705
|
+
if (address === void 0) return false;
|
|
3706
|
+
const amount = (0, import_sdk15.tryResolveSendAmount)(req.raw, req.amount);
|
|
3707
|
+
if (amount === void 0) return false;
|
|
3708
|
+
return railAvailable(client, inputFor2(address, amount));
|
|
3709
|
+
},
|
|
3710
|
+
quote: async (req) => {
|
|
3711
|
+
const address = (0, import_sdk15.btcTarget)(req.raw);
|
|
3712
|
+
if (address === void 0) {
|
|
3713
|
+
throw new Error(`${ONCHAIN_SWAP_RAIL}: the request carries no bitcoin address`);
|
|
3714
|
+
}
|
|
3715
|
+
const amount = (0, import_sdk15.resolveSendAmount)(ONCHAIN_SWAP_RAIL, req.raw, req.amount);
|
|
3716
|
+
const quote = await client.quote(inputFor2(address, amount));
|
|
3717
|
+
const amounts = receiverExact(ONCHAIN_SWAP_RAIL, {
|
|
3718
|
+
// What lands at the address, which is the HTLC the solver locks
|
|
3719
|
+
// minus the claim this wallet will broadcast to take it.
|
|
3720
|
+
amount: quote.take.amount - claimFee,
|
|
3721
|
+
// Both halves of the cost, inside one number: the solver's
|
|
3722
|
+
// spread, and the claim the trader pays for out of the payout.
|
|
3723
|
+
fee: quote.fee.amount + claimFee,
|
|
3724
|
+
total: quote.give.amount
|
|
3725
|
+
});
|
|
3726
|
+
return {
|
|
3727
|
+
railId: ONCHAIN_SWAP_RAIL,
|
|
3728
|
+
...amounts,
|
|
3729
|
+
meta: {
|
|
3730
|
+
quoteId: quote.id,
|
|
3731
|
+
expiresAt: quote.expiresAt,
|
|
3732
|
+
...quote.refundLocktime === void 0 ? {} : { refundLocktime: quote.refundLocktime },
|
|
3733
|
+
...quote.solver === void 0 ? {} : { solver: quote.solver },
|
|
3734
|
+
...quote.lock === void 0 ? {} : { paymentHash: quote.lock.hash },
|
|
3735
|
+
market: quote.market.key,
|
|
3736
|
+
/** The estimate folded into `fee`, so a caller can see it. */
|
|
3737
|
+
claimFeeSats: Number(claimFee),
|
|
3738
|
+
/** What the solver locks on L1, before the claim's fee. */
|
|
3739
|
+
htlcAmountSats: Number(quote.take.amount)
|
|
3740
|
+
},
|
|
3741
|
+
send: () => swapHandle(ONCHAIN_SWAP_RAIL, client, quote)
|
|
3742
|
+
};
|
|
3743
|
+
}
|
|
3744
|
+
};
|
|
3745
|
+
}
|
|
3746
|
+
|
|
3747
|
+
// src/payment/router.ts
|
|
3748
|
+
var import_sdk16 = require("@arkade-os/sdk");
|
|
3749
|
+
var SWAP_ROUTER_PRIORITY = [
|
|
3750
|
+
"ark",
|
|
3751
|
+
LIGHTNING_RAIL,
|
|
3752
|
+
ONCHAIN_SWAP_RAIL,
|
|
3753
|
+
"onchain"
|
|
3754
|
+
];
|
|
3755
|
+
function createSwapPaymentRouter(wallet, client, config) {
|
|
3756
|
+
return new import_sdk16.PaymentRouter({
|
|
3757
|
+
wallet,
|
|
3758
|
+
prefs: { priority: [...SWAP_ROUTER_PRIORITY], ...config.prefs }
|
|
3759
|
+
}).use((0, import_sdk16.arkRail)()).use((0, import_sdk16.onchainRail)()).use(lightningRail(client)).use(
|
|
3760
|
+
onchainSwapRail(client, {
|
|
3761
|
+
claimFeeRateSatVb: config.claimFeeRateSatVb,
|
|
3762
|
+
...config.claimVsize === void 0 ? {} : { claimVsize: config.claimVsize }
|
|
3763
|
+
})
|
|
3764
|
+
);
|
|
3765
|
+
}
|
|
3766
|
+
|
|
3160
3767
|
// src/claim.ts
|
|
3161
3768
|
var import_base14 = require("@scure/base");
|
|
3162
3769
|
var import_legacy3 = require("@noble/hashes/legacy.js");
|
|
3163
3770
|
var import_sha25 = require("@noble/hashes/sha2.js");
|
|
3164
|
-
var
|
|
3771
|
+
var import_sdk18 = require("@arkade-os/sdk");
|
|
3165
3772
|
|
|
3166
3773
|
// src/refund.ts
|
|
3167
3774
|
var import_base13 = require("@scure/base");
|
|
3168
3775
|
var import_sha24 = require("@noble/hashes/sha2.js");
|
|
3169
|
-
var
|
|
3776
|
+
var import_sdk17 = require("@arkade-os/sdk");
|
|
3170
3777
|
var sleep2 = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
3171
3778
|
var isRfqTerminal = (state) => RFQ_TERMINAL_STATES.includes(state);
|
|
3172
3779
|
var RFQ_RESOLVED_STATES = ["settled", "refunded"];
|
|
@@ -3186,6 +3793,15 @@ async function awaitRfqResolution(transport, rfqId, options = {}) {
|
|
|
3186
3793
|
await sleep2(pollMs);
|
|
3187
3794
|
}
|
|
3188
3795
|
}
|
|
3796
|
+
var walletOperator = (wallet) => {
|
|
3797
|
+
let broadcaster;
|
|
3798
|
+
const broadcasting = () => broadcaster ??= wallet.getArkadeBroadcaster();
|
|
3799
|
+
return {
|
|
3800
|
+
getInfo: () => wallet.getArkadeInfo({ requireLive: true }),
|
|
3801
|
+
submitTx: async (...args) => (await broadcasting()).submitTx(...args),
|
|
3802
|
+
finalizeTx: async (...args) => (await broadcasting()).finalizeTx(...args)
|
|
3803
|
+
};
|
|
3804
|
+
};
|
|
3189
3805
|
var LockupNeedsRecoveryError = class extends Error {
|
|
3190
3806
|
name = "LockupNeedsRecoveryError";
|
|
3191
3807
|
reason = "needs_recovery";
|
|
@@ -3197,11 +3813,10 @@ var LockupNeedsRecoveryError = class extends Error {
|
|
|
3197
3813
|
* into one settlement with no CLTV awareness, so an early attempt can fail
|
|
3198
3814
|
* the whole batch — including unrelated outputs that were otherwise fine.
|
|
3199
3815
|
*
|
|
3200
|
-
* Exposed as a value, not only inside the message, so a caller can
|
|
3201
|
-
*
|
|
3202
|
-
*
|
|
3203
|
-
*
|
|
3204
|
-
* not an exact alarm.
|
|
3816
|
+
* Exposed as a value, not only inside the message, so a caller can skip
|
|
3817
|
+
* pre-CLTV recoverable outputs without parsing prose. Seconds-based
|
|
3818
|
+
* locktimes mature against the chain tip's timestamp rather than wall
|
|
3819
|
+
* clock, so treat this as a floor to wait past, not an exact alarm.
|
|
3205
3820
|
*/
|
|
3206
3821
|
recoverableAfter;
|
|
3207
3822
|
constructor(outpoints, recoverableAfter) {
|
|
@@ -3241,14 +3856,14 @@ async function findLockupVtxos(indexer, swapPkScript) {
|
|
|
3241
3856
|
}
|
|
3242
3857
|
var hashesTo = (candidate, paymentHash) => import_base13.hex.encode((0, import_sha24.sha256)(candidate)) === paymentHash;
|
|
3243
3858
|
var candidateWitnessItems = (tx, inputIndex) => [
|
|
3244
|
-
...(0,
|
|
3859
|
+
...(0, import_sdk17.getArkPsbtFields)(tx, inputIndex, import_sdk17.ConditionWitness).flat(),
|
|
3245
3860
|
...tx.getInput(inputIndex).finalScriptWitness ?? []
|
|
3246
3861
|
];
|
|
3247
3862
|
async function readLockupFate(indexer, input) {
|
|
3248
3863
|
const { vtxos } = await indexer.getVtxos({ scripts: [import_base13.hex.encode(input.swapPkScript)] });
|
|
3249
3864
|
const all = vtxos ?? [];
|
|
3250
3865
|
if (all.length === 0) return { fate: "unknown" };
|
|
3251
|
-
const exited = all.filter((vtxo) => vtxo.isUnrolled && !(0,
|
|
3866
|
+
const exited = all.filter((vtxo) => vtxo.isUnrolled && !(0, import_sdk17.hasTerminalSpend)(vtxo));
|
|
3252
3867
|
if (exited.length > 0) {
|
|
3253
3868
|
return {
|
|
3254
3869
|
fate: "exited",
|
|
@@ -3258,11 +3873,11 @@ async function readLockupFate(indexer, input) {
|
|
|
3258
3873
|
const spentBy = /* @__PURE__ */ new Map();
|
|
3259
3874
|
let everySpendNamed = true;
|
|
3260
3875
|
for (const vtxo of all) {
|
|
3261
|
-
if (!(0,
|
|
3876
|
+
if (!(0, import_sdk17.hasTerminalSpend)(vtxo)) return { fate: "open" };
|
|
3262
3877
|
if (vtxo.spentBy)
|
|
3263
3878
|
spentBy.set(vtxo.spentBy, {
|
|
3264
3879
|
checkpointTxid: vtxo.spentBy,
|
|
3265
|
-
|
|
3880
|
+
txid: vtxo.arkTxId
|
|
3266
3881
|
});
|
|
3267
3882
|
else everySpendNamed = false;
|
|
3268
3883
|
}
|
|
@@ -3272,7 +3887,7 @@ async function readLockupFate(indexer, input) {
|
|
|
3272
3887
|
for (const raw of txs) {
|
|
3273
3888
|
let tx;
|
|
3274
3889
|
try {
|
|
3275
|
-
tx =
|
|
3890
|
+
tx = import_sdk17.Transaction.fromPSBT(import_base13.base64.decode(raw));
|
|
3276
3891
|
} catch {
|
|
3277
3892
|
continue;
|
|
3278
3893
|
}
|
|
@@ -3291,32 +3906,32 @@ async function readLockupFate(indexer, input) {
|
|
|
3291
3906
|
}
|
|
3292
3907
|
return everySpendNamed && observed.size === spentBy.size ? { fate: "returned", spends } : { fate: "unknown" };
|
|
3293
3908
|
}
|
|
3294
|
-
async function pushRefundWithoutReceiver(
|
|
3909
|
+
async function pushRefundWithoutReceiver(operator, input) {
|
|
3295
3910
|
if (input.vtxos.length === 0) throw new Error("nothing to refund: no funded outputs");
|
|
3296
3911
|
const swept = input.vtxos.filter((vtxo) => vtxo.recoverable);
|
|
3297
3912
|
if (swept.length > 0) {
|
|
3298
3913
|
throw new LockupNeedsRecoveryError(
|
|
3299
3914
|
swept.map((vtxo) => `${vtxo.txid}:${vtxo.vout}`),
|
|
3300
|
-
input.
|
|
3915
|
+
input.contract.options.refundLocktime
|
|
3301
3916
|
);
|
|
3302
3917
|
}
|
|
3303
|
-
const refundPkScript = input.refundPkScript ?? input.
|
|
3918
|
+
const refundPkScript = input.refundPkScript ?? input.contract.options.nonInteractiveParameters?.senderPkScript;
|
|
3304
3919
|
if (!refundPkScript) {
|
|
3305
3920
|
throw new Error(
|
|
3306
3921
|
"no refund destination: the contract carries no emulator covenant suite, so pass refundPkScript explicitly"
|
|
3307
3922
|
);
|
|
3308
3923
|
}
|
|
3309
|
-
const info = await
|
|
3310
|
-
let
|
|
3924
|
+
const info = await operator.getInfo();
|
|
3925
|
+
let operatorUnrollScript;
|
|
3311
3926
|
try {
|
|
3312
|
-
|
|
3927
|
+
operatorUnrollScript = import_sdk17.CSVMultisigTapscript.decode(import_base13.hex.decode(info.checkpointTapscript));
|
|
3313
3928
|
} catch {
|
|
3314
|
-
throw new Error("invalid checkpointTapscript from the
|
|
3929
|
+
throw new Error("invalid checkpointTapscript from the operator");
|
|
3315
3930
|
}
|
|
3316
|
-
const leaf = input.
|
|
3317
|
-
const tapTree = input.
|
|
3931
|
+
const leaf = input.contract.refundWithoutReceiver();
|
|
3932
|
+
const tapTree = input.contract.encode();
|
|
3318
3933
|
const amount = input.vtxos.reduce((sum, vtxo) => sum + vtxo.value, 0);
|
|
3319
|
-
const { arkTx, checkpoints } = (0,
|
|
3934
|
+
const { arkTx: tx, checkpoints } = (0, import_sdk17.buildOffchainTx)(
|
|
3320
3935
|
input.vtxos.map((vtxo) => ({
|
|
3321
3936
|
txid: vtxo.txid,
|
|
3322
3937
|
vout: vtxo.vout,
|
|
@@ -3325,15 +3940,15 @@ async function pushRefundWithoutReceiver(ark, input) {
|
|
|
3325
3940
|
tapTree
|
|
3326
3941
|
})),
|
|
3327
3942
|
[{ script: refundPkScript, amount: BigInt(amount) }],
|
|
3328
|
-
|
|
3943
|
+
operatorUnrollScript
|
|
3329
3944
|
);
|
|
3330
|
-
const
|
|
3331
|
-
const submitted = await
|
|
3332
|
-
import_base13.base64.encode(
|
|
3945
|
+
const signedTx = await input.sender.sign(tx);
|
|
3946
|
+
const submitted = await operator.submitTx(
|
|
3947
|
+
import_base13.base64.encode(signedTx.toPSBT()),
|
|
3333
3948
|
checkpoints.map((c) => import_base13.base64.encode(c.toPSBT()))
|
|
3334
3949
|
);
|
|
3335
|
-
(0,
|
|
3336
|
-
const matched = (0,
|
|
3950
|
+
(0, import_sdk17.assertSubmittedArkTxid)(submitted, signedTx, "refundWithoutReceiver");
|
|
3951
|
+
const matched = (0, import_sdk17.matchServerCheckpoints)(
|
|
3337
3952
|
submitted.signedCheckpointTxs,
|
|
3338
3953
|
checkpoints,
|
|
3339
3954
|
"refundWithoutReceiver"
|
|
@@ -3343,11 +3958,11 @@ async function pushRefundWithoutReceiver(ark, input) {
|
|
|
3343
3958
|
async ({ server }) => import_base13.base64.encode((await input.sender.sign(server, [0])).toPSBT())
|
|
3344
3959
|
)
|
|
3345
3960
|
);
|
|
3346
|
-
await
|
|
3347
|
-
return {
|
|
3961
|
+
await operator.finalizeTx(submitted.arkTxid, finalCheckpoints);
|
|
3962
|
+
return { txid: submitted.arkTxid, amount };
|
|
3348
3963
|
}
|
|
3349
3964
|
var REFUND_MTP_LAG_SECONDS = 2 * 60 * 60;
|
|
3350
|
-
async function refundIfUnresolved(transport,
|
|
3965
|
+
async function refundIfUnresolved(transport, operator, indexer, input) {
|
|
3351
3966
|
const pollMs = input.pollMs ?? 5e3;
|
|
3352
3967
|
const now = input.now ?? (() => Math.floor(Date.now() / 1e3));
|
|
3353
3968
|
const attemptDeadline = input.attemptDeadline ?? input.refundLocktime + REFUND_MTP_LAG_SECONDS;
|
|
@@ -3358,7 +3973,7 @@ async function refundIfUnresolved(transport, ark, indexer, input) {
|
|
|
3358
3973
|
let fate = { fate: "unknown" };
|
|
3359
3974
|
try {
|
|
3360
3975
|
fate = await readLockupFate(indexer, {
|
|
3361
|
-
swapPkScript: input.
|
|
3976
|
+
swapPkScript: input.contract.pkScript,
|
|
3362
3977
|
paymentHash: input.paymentHash
|
|
3363
3978
|
});
|
|
3364
3979
|
} catch {
|
|
@@ -3370,11 +3985,11 @@ async function refundIfUnresolved(transport, ark, indexer, input) {
|
|
|
3370
3985
|
status
|
|
3371
3986
|
};
|
|
3372
3987
|
}
|
|
3373
|
-
const vtxos = await findLockupVtxos(indexer, input.
|
|
3988
|
+
const vtxos = await findLockupVtxos(indexer, input.contract.pkScript);
|
|
3374
3989
|
if (vtxos.length === 0) return { outcome: "nothing_to_refund", status };
|
|
3375
3990
|
try {
|
|
3376
|
-
const pushed = await pushRefundWithoutReceiver(
|
|
3377
|
-
|
|
3991
|
+
const pushed = await pushRefundWithoutReceiver(operator, {
|
|
3992
|
+
contract: input.contract,
|
|
3378
3993
|
sender: input.sender,
|
|
3379
3994
|
vtxos,
|
|
3380
3995
|
refundPkScript: input.refundPkScript
|
|
@@ -3417,13 +4032,13 @@ var assertFiniteAmount = (value, reason, label) => {
|
|
|
3417
4032
|
error.reason = reason;
|
|
3418
4033
|
throw error;
|
|
3419
4034
|
};
|
|
3420
|
-
async function pushClaim(
|
|
4035
|
+
async function pushClaim(operator, input) {
|
|
3421
4036
|
if (input.vtxos.length === 0) throw new Error("nothing to claim: no funded outputs");
|
|
3422
4037
|
const swept = input.vtxos.filter((vtxo) => vtxo.recoverable);
|
|
3423
4038
|
if (swept.length > 0) {
|
|
3424
4039
|
throw new LockupNeedsRecoveryError(
|
|
3425
4040
|
swept.map((vtxo) => `${vtxo.txid}:${vtxo.vout}`),
|
|
3426
|
-
input.
|
|
4041
|
+
input.contract.options.refundLocktime
|
|
3427
4042
|
);
|
|
3428
4043
|
}
|
|
3429
4044
|
const locked = input.vtxos.reduce((sum, vtxo) => sum + vtxo.value, 0);
|
|
@@ -3434,22 +4049,22 @@ async function pushClaim(ark, input) {
|
|
|
3434
4049
|
throw new LockupAmountMismatchError(input.expectedAmount, locked);
|
|
3435
4050
|
}
|
|
3436
4051
|
}
|
|
3437
|
-
const committed = input.
|
|
4052
|
+
const committed = input.contract.options.preimageHash;
|
|
3438
4053
|
if (import_base14.hex.encode((0, import_legacy3.ripemd160)((0, import_sha25.sha256)(input.preimage))) !== import_base14.hex.encode(committed)) {
|
|
3439
4054
|
throw new Error("preimage does not match the covenant's payment hash");
|
|
3440
4055
|
}
|
|
3441
|
-
const info = await
|
|
3442
|
-
let
|
|
4056
|
+
const info = await operator.getInfo();
|
|
4057
|
+
let operatorUnrollScript;
|
|
3443
4058
|
try {
|
|
3444
|
-
|
|
4059
|
+
operatorUnrollScript = import_sdk18.CSVMultisigTapscript.decode(import_base14.hex.decode(info.checkpointTapscript));
|
|
3445
4060
|
} catch {
|
|
3446
|
-
throw new Error("invalid checkpointTapscript from the
|
|
4061
|
+
throw new Error("invalid checkpointTapscript from the operator");
|
|
3447
4062
|
}
|
|
3448
|
-
const leaf = input.
|
|
3449
|
-
const tapTree = input.
|
|
3450
|
-
const
|
|
3451
|
-
identity: (0,
|
|
3452
|
-
provider:
|
|
4063
|
+
const leaf = input.contract.claim();
|
|
4064
|
+
const tapTree = input.contract.encode();
|
|
4065
|
+
const txid = await (0, import_sdk18.signAndSubmitOffchainTx)({
|
|
4066
|
+
identity: (0, import_sdk18.claimWithPreimageIdentity)(input.receiver, input.preimage),
|
|
4067
|
+
provider: operator,
|
|
3453
4068
|
inputs: input.vtxos.map((vtxo) => ({
|
|
3454
4069
|
txid: vtxo.txid,
|
|
3455
4070
|
vout: vtxo.vout,
|
|
@@ -3460,10 +4075,10 @@ async function pushClaim(ark, input) {
|
|
|
3460
4075
|
// One aggregate output: unlike the covenant refund, this leaf inspects
|
|
3461
4076
|
// nothing about the output set.
|
|
3462
4077
|
outputs: [{ script: input.destinationPkScript, amount: BigInt(locked) }],
|
|
3463
|
-
serverUnrollScript,
|
|
3464
|
-
verifyServerSignatures: { serverPubkey: input.
|
|
4078
|
+
serverUnrollScript: operatorUnrollScript,
|
|
4079
|
+
verifyServerSignatures: { serverPubkey: input.contract.options.server }
|
|
3465
4080
|
});
|
|
3466
|
-
return {
|
|
4081
|
+
return { txid, amount: locked };
|
|
3467
4082
|
}
|
|
3468
4083
|
async function awaitLockupFunding(indexer, swapPkScript, options = {}) {
|
|
3469
4084
|
const pollMs = options.pollMs ?? 5e3;
|
|
@@ -3478,13 +4093,13 @@ async function awaitLockupFunding(indexer, swapPkScript, options = {}) {
|
|
|
3478
4093
|
await sleep3(pollMs);
|
|
3479
4094
|
}
|
|
3480
4095
|
}
|
|
3481
|
-
async function claimReceiveLockup(indexer,
|
|
4096
|
+
async function claimReceiveLockup(indexer, operator, input) {
|
|
3482
4097
|
const vtxos = await awaitLockupFunding(indexer, input.swapPkScript, {
|
|
3483
4098
|
pollMs: input.pollMs,
|
|
3484
4099
|
deadline: input.deadline
|
|
3485
4100
|
});
|
|
3486
|
-
return pushClaim(
|
|
3487
|
-
|
|
4101
|
+
return pushClaim(operator, {
|
|
4102
|
+
contract: input.contract,
|
|
3488
4103
|
receiver: input.receiver,
|
|
3489
4104
|
preimage: input.preimage,
|
|
3490
4105
|
vtxos,
|
|
@@ -3495,7 +4110,7 @@ async function claimReceiveLockup(indexer, ark, input) {
|
|
|
3495
4110
|
}
|
|
3496
4111
|
|
|
3497
4112
|
// src/refundBlocked.ts
|
|
3498
|
-
var
|
|
4113
|
+
var import_sdk19 = require("@arkade-os/sdk");
|
|
3499
4114
|
var RefundNotLocallyPossibleError = class extends Error {
|
|
3500
4115
|
constructor(reason, message, options) {
|
|
3501
4116
|
super(message, options);
|
|
@@ -3512,16 +4127,16 @@ async function senderIdentityForSwapRecord(wallet, record) {
|
|
|
3512
4127
|
);
|
|
3513
4128
|
}
|
|
3514
4129
|
try {
|
|
3515
|
-
return await (0,
|
|
4130
|
+
return await (0, import_sdk19.contractSigner)(wallet, record.signingDescriptor);
|
|
3516
4131
|
} catch (cause) {
|
|
3517
|
-
if (cause instanceof
|
|
4132
|
+
if (cause instanceof import_sdk19.WalletCannotSignError) {
|
|
3518
4133
|
throw new RefundNotLocallyPossibleError(
|
|
3519
4134
|
"unsignable-wallet",
|
|
3520
4135
|
`this wallet holds ${record.signingDescriptor} but cannot sign with it; attach its signer`,
|
|
3521
4136
|
{ cause }
|
|
3522
4137
|
);
|
|
3523
4138
|
}
|
|
3524
|
-
if (cause instanceof
|
|
4139
|
+
if (cause instanceof import_sdk19.ForeignDescriptorError) {
|
|
3525
4140
|
throw new RefundNotLocallyPossibleError(
|
|
3526
4141
|
"foreign-descriptor",
|
|
3527
4142
|
`this wallet cannot derive ${record.signingDescriptor}; the swap was created on another wallet`,
|
|
@@ -3535,8 +4150,8 @@ async function senderIdentityForSwapRecord(wallet, record) {
|
|
|
3535
4150
|
// src/arkadeRefunder.ts
|
|
3536
4151
|
function arkadeRefunder(deps) {
|
|
3537
4152
|
return async (swap) => {
|
|
3538
|
-
const
|
|
3539
|
-
if (!
|
|
4153
|
+
const contract = swap.lockup?.script;
|
|
4154
|
+
if (!contract) {
|
|
3540
4155
|
throw new Error(
|
|
3541
4156
|
`swap ${swap.rfqId} carries no lockup covenant, so its refund cannot be built`
|
|
3542
4157
|
);
|
|
@@ -3551,7 +4166,7 @@ function arkadeRefunder(deps) {
|
|
|
3551
4166
|
);
|
|
3552
4167
|
}
|
|
3553
4168
|
const sender = await senderIdentityForSwapRecord(deps.wallet, rfqSignerOf(record) ?? {});
|
|
3554
|
-
return pushRefundWithoutReceiver(deps.
|
|
4169
|
+
return pushRefundWithoutReceiver(deps.operator, { contract, sender, vtxos });
|
|
3555
4170
|
};
|
|
3556
4171
|
}
|
|
3557
4172
|
|
|
@@ -3572,6 +4187,14 @@ function nextOnchainAction(input) {
|
|
|
3572
4187
|
return input.htlcLocktime - input.now >= ONCHAIN_CLAIM_MARGIN_SECONDS ? "claim" : "claim_window_closed";
|
|
3573
4188
|
}
|
|
3574
4189
|
}
|
|
4190
|
+
var RFQ_CONFIGURATION_REFUSAL = {
|
|
4191
|
+
noClaimLockupCallback: "no claimLockup callback is wired, so this wallet cannot claim the lockup",
|
|
4192
|
+
noCallbacksForClaim: "no callbacks are wired, so this wallet cannot claim the lockup",
|
|
4193
|
+
noClaimOnchainCallback: "no claimOnchain callback is wired, so this wallet cannot claim the L1 fill",
|
|
4194
|
+
autoActionsDisabled: "automatic actions are disabled, so this wallet will not push the refund",
|
|
4195
|
+
noCallbacksForRefund: "no callbacks are wired, so this wallet cannot push the refund"
|
|
4196
|
+
};
|
|
4197
|
+
var RFQ_CONFIGURATION_REFUSALS = Object.values(RFQ_CONFIGURATION_REFUSAL);
|
|
3575
4198
|
var RfqSwapOriginRequired = class extends Error {
|
|
3576
4199
|
/** The swap that could not be admitted. */
|
|
3577
4200
|
rfqId;
|
|
@@ -3622,20 +4245,6 @@ var RfqSwapManager = class {
|
|
|
3622
4245
|
* sign may well have been restored.
|
|
3623
4246
|
*/
|
|
3624
4247
|
refundRefused = /* @__PURE__ */ new Set();
|
|
3625
|
-
/**
|
|
3626
|
-
* The last error a receive swap's claim callback threw, by rfqId.
|
|
3627
|
-
*
|
|
3628
|
-
* Kept only to tell two terminal outcomes apart once the claim window
|
|
3629
|
-
* shuts: a swap whose claim was attempted and kept failing ends `failed`
|
|
3630
|
-
* with that reason, while one that simply never became claimable ends
|
|
3631
|
-
* `refunded`. Without it a broken claim callback would resolve a caller's
|
|
3632
|
-
* {@link waitForSwapCompletion} as an ordinary unwind.
|
|
3633
|
-
*
|
|
3634
|
-
* Process-local, like {@link refundRefused}: after a restart the same swap
|
|
3635
|
-
* ends `refunded` instead, which costs the caller a reason and nothing else
|
|
3636
|
-
* — every throw was already reported through `onSwapFailed` as it happened.
|
|
3637
|
-
*/
|
|
3638
|
-
lastClaimError = /* @__PURE__ */ new Map();
|
|
3639
4248
|
/**
|
|
3640
4249
|
* The lockup outpoints a receive swap's claim callback has already been
|
|
3641
4250
|
* handed, by rfqId.
|
|
@@ -3947,6 +4556,19 @@ var RfqSwapManager = class {
|
|
|
3947
4556
|
async getPendingSwaps() {
|
|
3948
4557
|
return [...this.monitored.values()];
|
|
3949
4558
|
}
|
|
4559
|
+
/**
|
|
4560
|
+
* Every swap this manager holds — {@link getPendingSwaps} plus the ones
|
|
4561
|
+
* that already ended. The two sets are disjoint: a swap leaves `monitored`
|
|
4562
|
+
* as it enters `finished`.
|
|
4563
|
+
*
|
|
4564
|
+
* The finished half is what this process has seen, which after
|
|
4565
|
+
* {@link restoreFromRepository} is the stored history minus what retention
|
|
4566
|
+
* pruned. A manager that has restored nothing answers with the live swaps
|
|
4567
|
+
* alone.
|
|
4568
|
+
*/
|
|
4569
|
+
async getAllSwaps() {
|
|
4570
|
+
return [...this.monitored.values(), ...this.finished.values()];
|
|
4571
|
+
}
|
|
3950
4572
|
async hasSwap(rfqId) {
|
|
3951
4573
|
return this.monitored.has(rfqId);
|
|
3952
4574
|
}
|
|
@@ -4016,7 +4638,6 @@ var RfqSwapManager = class {
|
|
|
4016
4638
|
if (swap) this.byLockupScript.delete(import_base15.hex.encode(swap.lockupPkScript));
|
|
4017
4639
|
this.monitored.delete(rfqId);
|
|
4018
4640
|
this.refundRefused.delete(rfqId);
|
|
4019
|
-
this.lastClaimError.delete(rfqId);
|
|
4020
4641
|
this.claimedOutpoints.delete(rfqId);
|
|
4021
4642
|
}
|
|
4022
4643
|
/**
|
|
@@ -4225,14 +4846,14 @@ var RfqSwapManager = class {
|
|
|
4225
4846
|
return this.claimIfFunded(swap, vtxos);
|
|
4226
4847
|
}
|
|
4227
4848
|
if (now < swap.refundLocktime + REFUND_MTP_LAG_SECONDS) {
|
|
4228
|
-
if (swap.
|
|
4849
|
+
if (swap.claimTxid) return;
|
|
4229
4850
|
return this.block(
|
|
4230
4851
|
swap,
|
|
4231
4852
|
"the claim window closed with the lockup unclaimed \u2014 only the solver can act now"
|
|
4232
4853
|
);
|
|
4233
4854
|
}
|
|
4234
|
-
const failure =
|
|
4235
|
-
if (failure && !swap.
|
|
4855
|
+
const failure = swap.claimFailure;
|
|
4856
|
+
if (failure && !swap.claimTxid) {
|
|
4236
4857
|
return this.fail(swap, new Error(failure));
|
|
4237
4858
|
}
|
|
4238
4859
|
this.setState(swap, "refunded");
|
|
@@ -4247,7 +4868,7 @@ var RfqSwapManager = class {
|
|
|
4247
4868
|
*/
|
|
4248
4869
|
async claimIfFunded(swap, vtxos) {
|
|
4249
4870
|
if (vtxos.length === 0) return this.unblock(swap);
|
|
4250
|
-
const partiallyClaimed = swap.
|
|
4871
|
+
const partiallyClaimed = swap.claimTxid !== void 0;
|
|
4251
4872
|
if (partiallyClaimed && !this.hasUnclaimedOutpoint(swap.rfqId, vtxos)) {
|
|
4252
4873
|
return;
|
|
4253
4874
|
}
|
|
@@ -4269,21 +4890,22 @@ var RfqSwapManager = class {
|
|
|
4269
4890
|
if (!this.callbacks || !this.callbacks.claimLockup) {
|
|
4270
4891
|
return this.block(
|
|
4271
4892
|
swap,
|
|
4272
|
-
this.callbacks ?
|
|
4893
|
+
this.callbacks ? RFQ_CONFIGURATION_REFUSAL.noClaimLockupCallback : RFQ_CONFIGURATION_REFUSAL.noCallbacksForClaim
|
|
4273
4894
|
);
|
|
4274
4895
|
}
|
|
4275
4896
|
this.setState(swap, "claimable");
|
|
4276
4897
|
if (!this.config.enableAutoActions) return;
|
|
4277
4898
|
try {
|
|
4278
|
-
const {
|
|
4279
|
-
|
|
4899
|
+
const { txid } = await this.callbacks.claimLockup(swap, vtxos, { partiallyClaimed });
|
|
4900
|
+
delete swap.claimFailure;
|
|
4280
4901
|
this.rememberClaimed(swap.rfqId, vtxos);
|
|
4281
|
-
swap.
|
|
4902
|
+
swap.claimTxid = txid;
|
|
4282
4903
|
this.touch(swap);
|
|
4283
4904
|
this.setState(swap, "claimed");
|
|
4284
4905
|
this.emitAction(swap, "claimLockup");
|
|
4285
4906
|
} catch (error) {
|
|
4286
|
-
|
|
4907
|
+
swap.claimFailure = errorMessage(error);
|
|
4908
|
+
this.touch(swap);
|
|
4287
4909
|
this.emitFailed(swap, error);
|
|
4288
4910
|
}
|
|
4289
4911
|
}
|
|
@@ -4320,10 +4942,7 @@ var RfqSwapManager = class {
|
|
|
4320
4942
|
if (action === "claim" && phase.phase === "claimable") {
|
|
4321
4943
|
if (swap.claimTxid) return "continue";
|
|
4322
4944
|
if (this.callbacks && !this.callbacks.claimOnchain) {
|
|
4323
|
-
this.block(
|
|
4324
|
-
swap,
|
|
4325
|
-
"no claimOnchain callback is wired, so this wallet cannot claim the L1 fill"
|
|
4326
|
-
);
|
|
4945
|
+
this.block(swap, RFQ_CONFIGURATION_REFUSAL.noClaimOnchainCallback);
|
|
4327
4946
|
return "handled";
|
|
4328
4947
|
}
|
|
4329
4948
|
this.setOnchainState(swap, "claimable");
|
|
@@ -4363,14 +4982,14 @@ var RfqSwapManager = class {
|
|
|
4363
4982
|
if (!this.config.enableAutoActions || !this.callbacks) {
|
|
4364
4983
|
return this.block(
|
|
4365
4984
|
swap,
|
|
4366
|
-
this.callbacks ?
|
|
4985
|
+
this.callbacks ? RFQ_CONFIGURATION_REFUSAL.autoActionsDisabled : RFQ_CONFIGURATION_REFUSAL.noCallbacksForRefund
|
|
4367
4986
|
);
|
|
4368
4987
|
}
|
|
4369
4988
|
this.unblock(swap);
|
|
4370
4989
|
try {
|
|
4371
4990
|
const pushed = await this.callbacks.refundArkade(swap);
|
|
4372
4991
|
if (pushed) {
|
|
4373
|
-
swap.
|
|
4992
|
+
swap.refundTxid = pushed.txid;
|
|
4374
4993
|
this.touch(swap);
|
|
4375
4994
|
}
|
|
4376
4995
|
this.setState(swap, "refunded");
|
|
@@ -4443,7 +5062,7 @@ var RfqSwapManager = class {
|
|
|
4443
5062
|
/**
|
|
4444
5063
|
* Record which ark transactions ended the lockup.
|
|
4445
5064
|
*
|
|
4446
|
-
* Only the ones the indexer actually named: `LockupSpend.
|
|
5065
|
+
* Only the ones the indexer actually named: `LockupSpend.txid` is
|
|
4447
5066
|
* optional, and a checkpoint txid is not what history correlates on — a
|
|
4448
5067
|
* record carrying one would name a transaction the wallet's own activity
|
|
4449
5068
|
* never shows. Fewer txids is the right failure here.
|
|
@@ -4453,9 +5072,9 @@ var RfqSwapManager = class {
|
|
|
4453
5072
|
* verdict once.
|
|
4454
5073
|
*/
|
|
4455
5074
|
stampLockupSpends(swap, spends) {
|
|
4456
|
-
const
|
|
4457
|
-
if (
|
|
4458
|
-
swap.
|
|
5075
|
+
const txids = spends.map((spend) => spend.txid).filter((txid) => txid !== void 0);
|
|
5076
|
+
if (txids.length === 0) return;
|
|
5077
|
+
swap.lockupSpendTxids = txids;
|
|
4459
5078
|
this.touch(swap);
|
|
4460
5079
|
}
|
|
4461
5080
|
touch(swap) {
|
|
@@ -4466,6 +5085,7 @@ var RfqSwapManager = class {
|
|
|
4466
5085
|
if (swap.state === state) return;
|
|
4467
5086
|
const previous = swap.state;
|
|
4468
5087
|
if (previous === "needs_counterparty") delete swap.blockedReason;
|
|
5088
|
+
if (isRfqSwapTerminal(state)) delete swap.claimFailure;
|
|
4469
5089
|
swap.state = state;
|
|
4470
5090
|
this.touch(swap);
|
|
4471
5091
|
notify(this.swapUpdateListeners, (listener) => listener(swap, previous));
|
|
@@ -4536,10 +5156,9 @@ var RfqSwapManager = class {
|
|
|
4536
5156
|
/**
|
|
4537
5157
|
* Drop a terminal swap from monitoring and report it exactly once.
|
|
4538
5158
|
*
|
|
4539
|
-
* `onSwapCompleted` and `onSwapFailed` are mutually exclusive here
|
|
4540
|
-
*
|
|
4541
|
-
*
|
|
4542
|
-
* also fires on failure is a trap worth not inheriting.
|
|
5159
|
+
* `onSwapCompleted` and `onSwapFailed` are mutually exclusive here: a
|
|
5160
|
+
* listener named "completed" that also fires on failure is a trap, so a
|
|
5161
|
+
* swap that leaves monitoring reports through exactly one of them.
|
|
4543
5162
|
*/
|
|
4544
5163
|
finalize(swap) {
|
|
4545
5164
|
if (!this.monitored.has(swap.rfqId)) return;
|
|
@@ -4575,7 +5194,7 @@ var traderClaimTxid = (swap) => {
|
|
|
4575
5194
|
case "onchain_send":
|
|
4576
5195
|
return swap.claimTxid;
|
|
4577
5196
|
case "lightning_receive":
|
|
4578
|
-
return swap.
|
|
5197
|
+
return swap.claimTxid;
|
|
4579
5198
|
default:
|
|
4580
5199
|
return void 0;
|
|
4581
5200
|
}
|
|
@@ -4585,36 +5204,199 @@ var outcomeOf = (swap) => {
|
|
|
4585
5204
|
const lostReceive = swap.kind === "lightning_receive" && swap.state === "refunded";
|
|
4586
5205
|
return {
|
|
4587
5206
|
state: swap.state,
|
|
4588
|
-
txid: lostReceive ? swap.
|
|
5207
|
+
txid: lostReceive ? swap.refundTxid : traderClaimTxid(swap) ?? swap.refundTxid
|
|
4589
5208
|
};
|
|
4590
5209
|
};
|
|
4591
5210
|
var errorMessage = (error) => error instanceof Error ? error.message : String(error);
|
|
4592
5211
|
var outpointKey = (vtxo) => `${vtxo.txid}:${vtxo.vout}`;
|
|
4593
5212
|
|
|
4594
5213
|
// src/activity.ts
|
|
4595
|
-
var
|
|
5214
|
+
var import_sdk22 = require("@arkade-os/sdk");
|
|
4596
5215
|
var import_base16 = require("@scure/base");
|
|
5216
|
+
|
|
5217
|
+
// src/client/corridors/lightning.ts
|
|
5218
|
+
var import_sdk20 = require("@arkade-os/sdk");
|
|
5219
|
+
var INVOICE_HRPS = [
|
|
5220
|
+
["lnbcrt", ["regtest"]],
|
|
5221
|
+
["lntbs", ["signet", "mutinynet"]],
|
|
5222
|
+
["lnbc", ["bitcoin"]],
|
|
5223
|
+
["lntb", ["testnet"]]
|
|
5224
|
+
];
|
|
5225
|
+
var networksOfInvoiceHrp = (raw) => {
|
|
5226
|
+
const lower = raw.toLowerCase();
|
|
5227
|
+
return INVOICE_HRPS.find(([hrp]) => lower.startsWith(hrp))?.[1];
|
|
5228
|
+
};
|
|
5229
|
+
var PAYMENT_HASH = /^[0-9a-f]{64}$/;
|
|
5230
|
+
var LIGHTNING_DRIVE = {
|
|
5231
|
+
give: {
|
|
5232
|
+
lockups: [{ covenant: "arkade_lockup", owner: "solver", deadline: "refund_locktime" }],
|
|
5233
|
+
actions: ["claimLockup"],
|
|
5234
|
+
seams: ["indexer"]
|
|
5235
|
+
},
|
|
5236
|
+
take: {
|
|
5237
|
+
lockups: [{ covenant: "arkade_lockup", owner: "trader", deadline: "refund_locktime" }],
|
|
5238
|
+
actions: ["refundArkade"],
|
|
5239
|
+
seams: ["indexer"]
|
|
5240
|
+
}
|
|
5241
|
+
};
|
|
5242
|
+
var lightningCorridor = Object.assign(
|
|
5243
|
+
(deps) => ({
|
|
5244
|
+
corridor: "lightning",
|
|
5245
|
+
deps,
|
|
5246
|
+
drive: LIGHTNING_DRIVE,
|
|
5247
|
+
matches(raw) {
|
|
5248
|
+
const invoice = (0, import_sdk20.invoiceTarget)(raw);
|
|
5249
|
+
if (invoice === void 0) return void 0;
|
|
5250
|
+
const networks = networksOfInvoiceHrp(invoice);
|
|
5251
|
+
if (networks === void 0) {
|
|
5252
|
+
return { refused: "this invoice's network prefix names no network this SDK knows" };
|
|
5253
|
+
}
|
|
5254
|
+
if (!networks.includes(deps.networkName)) {
|
|
5255
|
+
return {
|
|
5256
|
+
refused: `this is a ${networks.join(" or ")} invoice and the wallet is on ${deps.networkName}`
|
|
5257
|
+
};
|
|
5258
|
+
}
|
|
5259
|
+
let facts;
|
|
5260
|
+
try {
|
|
5261
|
+
facts = deps.decode(invoice);
|
|
5262
|
+
if (!PAYMENT_HASH.test(facts.paymentHash)) {
|
|
5263
|
+
return { refused: "the decoded invoice carries no usable payment hash" };
|
|
5264
|
+
}
|
|
5265
|
+
} catch (error) {
|
|
5266
|
+
return {
|
|
5267
|
+
refused: `the invoice did not decode: ${error instanceof Error ? error.message : String(error)}`
|
|
5268
|
+
};
|
|
5269
|
+
}
|
|
5270
|
+
if (!Number.isSafeInteger(facts.expiresAt)) {
|
|
5271
|
+
return { refused: "the decoded invoice carries no usable expiry" };
|
|
5272
|
+
}
|
|
5273
|
+
const now = Math.floor(Date.now() / 1e3);
|
|
5274
|
+
if (facts.expiresAt <= now) {
|
|
5275
|
+
return { refused: `the invoice expired ${now - facts.expiresAt}s ago` };
|
|
5276
|
+
}
|
|
5277
|
+
if (!Number.isSafeInteger(facts.amountSats) || facts.amountSats < 0) {
|
|
5278
|
+
return { refused: "the decoded invoice carries no usable amount" };
|
|
5279
|
+
}
|
|
5280
|
+
if (facts.amountSats === 0) {
|
|
5281
|
+
return { refused: "the invoice names no amount, and a send route needs one" };
|
|
5282
|
+
}
|
|
5283
|
+
return {
|
|
5284
|
+
claimed: {
|
|
5285
|
+
kind: "invoice",
|
|
5286
|
+
bolt11: invoice,
|
|
5287
|
+
paymentHash: facts.paymentHash,
|
|
5288
|
+
amount: BigInt(facts.amountSats),
|
|
5289
|
+
expiresAt: facts.expiresAt
|
|
5290
|
+
}
|
|
5291
|
+
};
|
|
5292
|
+
}
|
|
5293
|
+
}),
|
|
5294
|
+
{ target: import_sdk20.invoiceTarget }
|
|
5295
|
+
);
|
|
5296
|
+
|
|
5297
|
+
// src/client/corridors/onchain.ts
|
|
5298
|
+
var import_sdk21 = require("@arkade-os/sdk");
|
|
5299
|
+
var btc3 = __toESM(require("@scure/btc-signer"), 1);
|
|
5300
|
+
var ONCHAIN_DRIVE = {
|
|
5301
|
+
take: {
|
|
5302
|
+
lockups: [
|
|
5303
|
+
{ covenant: "arkade_lockup", owner: "trader", deadline: "refund_locktime" },
|
|
5304
|
+
{ covenant: "onchain_htlc", owner: "solver", deadline: "htlc_refund_locktime" }
|
|
5305
|
+
],
|
|
5306
|
+
actions: ["claimOnchain", "refundArkade"],
|
|
5307
|
+
seams: ["indexer", "chain"]
|
|
5308
|
+
}
|
|
5309
|
+
};
|
|
5310
|
+
var onchainCorridor = Object.assign(
|
|
5311
|
+
(deps) => {
|
|
5312
|
+
const l1 = l1NetworkFromArk(deps.networkName);
|
|
5313
|
+
return {
|
|
5314
|
+
corridor: "onchain",
|
|
5315
|
+
deps,
|
|
5316
|
+
drive: ONCHAIN_DRIVE,
|
|
5317
|
+
matches(raw) {
|
|
5318
|
+
const target = (0, import_sdk21.btcTarget)(raw);
|
|
5319
|
+
if (target === void 0) return void 0;
|
|
5320
|
+
try {
|
|
5321
|
+
btc3.Address(L1_NETWORKS[l1]).decode(target);
|
|
5322
|
+
} catch {
|
|
5323
|
+
return { refused: `this is not a ${l1} address` };
|
|
5324
|
+
}
|
|
5325
|
+
return { claimed: { kind: "address", address: target } };
|
|
5326
|
+
}
|
|
5327
|
+
};
|
|
5328
|
+
},
|
|
5329
|
+
{ target: import_sdk21.btcTarget }
|
|
5330
|
+
);
|
|
5331
|
+
|
|
5332
|
+
// src/client/outcome.ts
|
|
5333
|
+
var CORRIDOR_PASS = {
|
|
5334
|
+
lightning_send: LIGHTNING_DRIVE.take,
|
|
5335
|
+
lightning_receive: LIGHTNING_DRIVE.give,
|
|
5336
|
+
onchain_send: ONCHAIN_DRIVE.take
|
|
5337
|
+
};
|
|
5338
|
+
var LOCKUP_OWNER = {
|
|
5339
|
+
lightning_send: CORRIDOR_PASS.lightning_send.lockups[0].owner,
|
|
5340
|
+
lightning_receive: CORRIDOR_PASS.lightning_receive.lockups[0].owner,
|
|
5341
|
+
onchain_send: CORRIDOR_PASS.onchain_send.lockups[0].owner
|
|
5342
|
+
};
|
|
5343
|
+
var at = (outcome) => () => outcome;
|
|
5344
|
+
var CORRIDOR_OUTCOME = {
|
|
5345
|
+
trader: {
|
|
5346
|
+
pending: at("funded"),
|
|
5347
|
+
claimable: at("funded"),
|
|
5348
|
+
claimed: at("funded"),
|
|
5349
|
+
needs_counterparty: at("needs_recovery"),
|
|
5350
|
+
settled: (kind) => kind === "onchain_send" ? "claimed" : "paid",
|
|
5351
|
+
refunded: at("refunded"),
|
|
5352
|
+
failed: at("failed")
|
|
5353
|
+
},
|
|
5354
|
+
solver: {
|
|
5355
|
+
// The solver has funded nothing yet: the invoice is shown and unpaid,
|
|
5356
|
+
// which is what makes this `open` where the trader's side is `funded`.
|
|
5357
|
+
pending: at("open"),
|
|
5358
|
+
claimable: at("funded"),
|
|
5359
|
+
claimed: at("funded"),
|
|
5360
|
+
needs_counterparty: at("needs_recovery"),
|
|
5361
|
+
// The trader's OWN claim, matched by the hash and not by our txid — so
|
|
5362
|
+
// a claim that lands without us still counts.
|
|
5363
|
+
settled: at("claimed"),
|
|
5364
|
+
// The one inversion this whole module exists for: every non-claim leaf
|
|
5365
|
+
// of a receive covenant is the solver's, so a lockup spent by one is
|
|
5366
|
+
// the trader's incoming payment never arriving.
|
|
5367
|
+
refunded: at("lapsed"),
|
|
5368
|
+
failed: at("failed")
|
|
5369
|
+
}
|
|
5370
|
+
};
|
|
5371
|
+
var corridorOutcome = (kind, state) => CORRIDOR_OUTCOME[LOCKUP_OWNER[kind]][state](kind);
|
|
5372
|
+
var ACTIVITY_TOKEN = {
|
|
5373
|
+
accepted: "pending",
|
|
5374
|
+
funding: "pending",
|
|
5375
|
+
funded: "pending",
|
|
5376
|
+
open: "pending",
|
|
5377
|
+
// The drive's in-flight states have no user-visible phase distinct from
|
|
5378
|
+
// "pending"; `needs_recovery` is different in kind — the swap is BLOCKED —
|
|
5379
|
+
// and collapsing it here is the same deliberate choice v1 made, which the
|
|
5380
|
+
// opaque-token design permits because apps map tokens themselves.
|
|
5381
|
+
needs_recovery: "pending",
|
|
5382
|
+
refunding: "pending",
|
|
5383
|
+
cancelling: "pending",
|
|
5384
|
+
cancelled: "cancelled",
|
|
5385
|
+
filled: "settled",
|
|
5386
|
+
claimed: "settled",
|
|
5387
|
+
paid: "settled",
|
|
5388
|
+
refunded: "refunded",
|
|
5389
|
+
lapsed: "lost",
|
|
5390
|
+
failed: "failed"
|
|
5391
|
+
};
|
|
5392
|
+
|
|
5393
|
+
// src/activity.ts
|
|
4597
5394
|
var LABELS = {
|
|
4598
5395
|
lightning_send: "Lightning send",
|
|
4599
5396
|
lightning_receive: "Lightning receive",
|
|
4600
5397
|
onchain_send: "Onchain send"
|
|
4601
5398
|
};
|
|
4602
|
-
var
|
|
4603
|
-
pending: "pending",
|
|
4604
|
-
// `claimable` and `claimed` are both in-progress states with no
|
|
4605
|
-
// user-visible phase distinct from "pending". `needs_counterparty` is
|
|
4606
|
-
// different in kind — the swap is BLOCKED, not merely in flight, since no
|
|
4607
|
-
// unilateral trader move exists (see `RfqSwapState`). Collapsing it into
|
|
4608
|
-
// `pending` here is a deliberate choice the opaque-token design permits —
|
|
4609
|
-
// apps map tokens themselves — but a future reader weighing a `"blocked"`
|
|
4610
|
-
// or `"stuck"` token should know this was already considered.
|
|
4611
|
-
claimable: "pending",
|
|
4612
|
-
claimed: "pending",
|
|
4613
|
-
needs_counterparty: "pending",
|
|
4614
|
-
settled: "settled",
|
|
4615
|
-
refunded: "refunded",
|
|
4616
|
-
failed: "failed"
|
|
4617
|
-
};
|
|
5399
|
+
var outcomeToken = (kind, state) => ACTIVITY_TOKEN[corridorOutcome(kind, state)];
|
|
4618
5400
|
function swapActivityResolver(deps) {
|
|
4619
5401
|
let byTxid = /* @__PURE__ */ new Map();
|
|
4620
5402
|
return {
|
|
@@ -4633,13 +5415,17 @@ function swapActivityResolver(deps) {
|
|
|
4633
5415
|
const key = tx.key.arkTxid || tx.key.commitmentTxid || tx.key.boardingTxid;
|
|
4634
5416
|
const swap = key ? byTxid.get(key) : void 0;
|
|
4635
5417
|
if (!swap) return void 0;
|
|
4636
|
-
const lostReceive = swap.kind === "lightning_receive" && swap.state === "refunded";
|
|
4637
5418
|
return [
|
|
4638
5419
|
{
|
|
4639
5420
|
groupId: `swap:${swap.rfqId}`,
|
|
4640
5421
|
label: LABELS[swap.kind],
|
|
4641
5422
|
kind: "swap",
|
|
4642
|
-
|
|
5423
|
+
// `"lost"` for a receive leg that ended `refunded` comes out
|
|
5424
|
+
// of the translation rather than out of a branch here: that
|
|
5425
|
+
// leg has no trader-side refund, every non-claim leaf of its
|
|
5426
|
+
// covenant is the solver's, and the client's `lapsed` is
|
|
5427
|
+
// that fact.
|
|
5428
|
+
outcome: outcomeToken(swap.kind, swap.state),
|
|
4643
5429
|
metadata: { rfqId: swap.rfqId, swapKind: swap.kind }
|
|
4644
5430
|
}
|
|
4645
5431
|
];
|
|
@@ -4650,16 +5436,17 @@ async function rfqSwapActivityInputs(deps) {
|
|
|
4650
5436
|
const records = await deps.repository.getAllRfqSwaps();
|
|
4651
5437
|
return Promise.all(records.map((record) => activityInputOf(record, deps.indexer)));
|
|
4652
5438
|
}
|
|
4653
|
-
async function activityInputOf(
|
|
5439
|
+
async function activityInputOf(stored, indexer) {
|
|
5440
|
+
const record = normalizeRfqSwapRecord(stored);
|
|
4654
5441
|
const txids = /* @__PURE__ */ new Set();
|
|
4655
|
-
if (record.
|
|
4656
|
-
if (record.
|
|
5442
|
+
if (record.fundingTxid) txids.add(record.fundingTxid);
|
|
5443
|
+
if (record.refundTxid) txids.add(record.refundTxid);
|
|
4657
5444
|
const handler = rfqCorridorHandlers.getOrThrow(record.kind);
|
|
4658
5445
|
for (const txid of handler.activityTxids?.(record.profile) ?? []) txids.add(txid);
|
|
4659
|
-
for (const txid of record.
|
|
4660
|
-
const spendUnknown = isRfqSwapTerminal(record.state) && !record.
|
|
4661
|
-
if (indexer && (!record.
|
|
4662
|
-
for (const txid of await lockupTxids(indexer, record, !record.
|
|
5446
|
+
for (const txid of record.lockupSpendTxids ?? []) txids.add(txid);
|
|
5447
|
+
const spendUnknown = isRfqSwapTerminal(record.state) && !record.refundTxid && !record.lockupSpendTxids?.length;
|
|
5448
|
+
if (indexer && (!record.fundingTxid || spendUnknown)) {
|
|
5449
|
+
for (const txid of await lockupTxids(indexer, record, !record.fundingTxid)) {
|
|
4663
5450
|
txids.add(txid);
|
|
4664
5451
|
}
|
|
4665
5452
|
}
|
|
@@ -4668,7 +5455,7 @@ async function activityInputOf(record, indexer) {
|
|
|
4668
5455
|
async function lockupTxids(indexer, record, wantFunding) {
|
|
4669
5456
|
let script;
|
|
4670
5457
|
try {
|
|
4671
|
-
script = import_base16.hex.encode(
|
|
5458
|
+
script = import_base16.hex.encode(import_sdk22.ArkAddress.decode(record.lockupAddress).pkScript);
|
|
4672
5459
|
} catch {
|
|
4673
5460
|
return [];
|
|
4674
5461
|
}
|
|
@@ -4684,6 +5471,346 @@ async function lockupTxids(indexer, record, wantFunding) {
|
|
|
4684
5471
|
return [];
|
|
4685
5472
|
}
|
|
4686
5473
|
}
|
|
5474
|
+
|
|
5475
|
+
// src/swapClient.ts
|
|
5476
|
+
var import_base17 = require("@scure/base");
|
|
5477
|
+
var import_sdk23 = require("@arkade-os/sdk");
|
|
5478
|
+
var import_solver_discovery3 = require("@arkade-os/solver-discovery");
|
|
5479
|
+
var btc4 = __toESM(require("@scure/btc-signer"), 1);
|
|
5480
|
+
var sideCorridorsOf = (market) => {
|
|
5481
|
+
const read = (value) => value === "lightning" || value === "onchain" ? value : "arkade";
|
|
5482
|
+
const m = market;
|
|
5483
|
+
return { base: read(m.base_corridor), quote: read(m.quote_corridor) };
|
|
5484
|
+
};
|
|
5485
|
+
var resolveKind = (market, give) => {
|
|
5486
|
+
const corridors = sideCorridorsOf(market);
|
|
5487
|
+
const giveCorridor = corridors[give];
|
|
5488
|
+
const receiveCorridor = corridors[give === "base" ? "quote" : "base"];
|
|
5489
|
+
if (giveCorridor === "arkade" && receiveCorridor === "arkade") return "spot";
|
|
5490
|
+
if (receiveCorridor === "lightning") return "ln_send";
|
|
5491
|
+
if (giveCorridor === "lightning") return "ln_receive";
|
|
5492
|
+
if (receiveCorridor === "onchain") return "onchain_send";
|
|
5493
|
+
return "onchain_receive";
|
|
5494
|
+
};
|
|
5495
|
+
var need = (value, what, leg) => {
|
|
5496
|
+
if (value === void 0) throw new Error(`a ${leg} quote needs ${what}`);
|
|
5497
|
+
return value;
|
|
5498
|
+
};
|
|
5499
|
+
var payoutScriptFor = (address, payoutPubkey, network) => address !== void 0 ? l1ScriptForAddress(address, network) : btc4.p2tr(payoutPubkey, void 0, L1_NETWORKS[network]).script;
|
|
5500
|
+
var corridorAmount = (input, leg) => {
|
|
5501
|
+
const raw = need(input.amount, "an amount", leg);
|
|
5502
|
+
const amountOn = need(input.amountOn, "amountOn ('give' or 'receive')", leg);
|
|
5503
|
+
const amount = Number(raw);
|
|
5504
|
+
if (!Number.isSafeInteger(amount) || amount <= 0) {
|
|
5505
|
+
throw new Error(`a ${leg} amount must be a positive integer of sats, got ${String(raw)}`);
|
|
5506
|
+
}
|
|
5507
|
+
return { amount, amountSide: amountOn === "give" ? "from" : "to" };
|
|
5508
|
+
};
|
|
5509
|
+
function createSwapClient(deps) {
|
|
5510
|
+
const { wallet, repository, transportFor } = deps;
|
|
5511
|
+
const ark = deps.ark ?? walletOperator(wallet);
|
|
5512
|
+
let reader;
|
|
5513
|
+
const reading = () => reader ??= wallet.getArkadeReader();
|
|
5514
|
+
const indexer = deps.indexer ?? {
|
|
5515
|
+
getVtxos: async (opts) => {
|
|
5516
|
+
if (!opts)
|
|
5517
|
+
throw new Error("getVtxos on the swap indexer requires scripts or outpoints");
|
|
5518
|
+
return (await reading()).getVtxos(opts);
|
|
5519
|
+
},
|
|
5520
|
+
getVirtualTxs: async (...args) => (await reading()).getVirtualTxs(...args)
|
|
5521
|
+
};
|
|
5522
|
+
const managerDeps = { indexer, chain: deps.chain, repository };
|
|
5523
|
+
const manager = new RfqSwapManager(managerDeps);
|
|
5524
|
+
const listeners = /* @__PURE__ */ new Set();
|
|
5525
|
+
const notify2 = (swap) => {
|
|
5526
|
+
for (const listener of listeners) {
|
|
5527
|
+
try {
|
|
5528
|
+
listener(swap);
|
|
5529
|
+
} catch {
|
|
5530
|
+
}
|
|
5531
|
+
}
|
|
5532
|
+
};
|
|
5533
|
+
manager.onSwapUpdate((swap) => notify2({ family: "rfq", swap }));
|
|
5534
|
+
const claimLockup = async (swap, vtxos, { partiallyClaimed }) => {
|
|
5535
|
+
const record = await repository.getRfqSwap(swap.rfqId);
|
|
5536
|
+
if (!record) throw new Error(`rfq swap ${swap.rfqId} has no stored record to claim from`);
|
|
5537
|
+
const secret = rfqClaimSecretOf(record);
|
|
5538
|
+
if (!secret) throw new Error(`rfq swap ${swap.rfqId} carries no claim secret`);
|
|
5539
|
+
const script = swap.lockup?.script;
|
|
5540
|
+
if (!script) throw new Error(`rfq swap ${swap.rfqId} carries no lockup covenant`);
|
|
5541
|
+
const payoutAddress = rfqClaimDestinationOf(record);
|
|
5542
|
+
if (!payoutAddress) {
|
|
5543
|
+
throw new Error(`rfq swap ${swap.rfqId} carries no claim destination`);
|
|
5544
|
+
}
|
|
5545
|
+
return pushClaim(ark, {
|
|
5546
|
+
contract: script,
|
|
5547
|
+
receiver: await (0, import_sdk23.contractSigner)(wallet, secret.signingDescriptor),
|
|
5548
|
+
preimage: await preimageForSwapRecord(wallet, secret),
|
|
5549
|
+
vtxos,
|
|
5550
|
+
destinationPkScript: import_sdk23.ArkAddress.decode(payoutAddress).pkScript,
|
|
5551
|
+
expectedAmount: swap.expectedAmount,
|
|
5552
|
+
partiallyClaimed
|
|
5553
|
+
});
|
|
5554
|
+
};
|
|
5555
|
+
manager.setCallbacks({
|
|
5556
|
+
refundArkade: arkadeRefunder({ operator: ark, indexer, wallet, repository }),
|
|
5557
|
+
claimLockup,
|
|
5558
|
+
...deps.claimOnchain ? { claimOnchain: deps.claimOnchain } : {}
|
|
5559
|
+
});
|
|
5560
|
+
let watcher;
|
|
5561
|
+
const quote = async (market, input) => {
|
|
5562
|
+
const kind = resolveKind(market, input.give);
|
|
5563
|
+
switch (kind) {
|
|
5564
|
+
case "spot": {
|
|
5565
|
+
const raw = need(input.amount, "an amount", kind);
|
|
5566
|
+
const amountOn = need(input.amountOn, "amountOn ('give' or 'receive')", kind);
|
|
5567
|
+
const plan = await (0, import_solver_discovery3.quoteOffer)(market, {
|
|
5568
|
+
give: input.give,
|
|
5569
|
+
...amountOn === "give" ? { giveAmount: raw } : { wantAmount: raw },
|
|
5570
|
+
fetchImpl: deps.discovery.fetchImpl
|
|
5571
|
+
});
|
|
5572
|
+
return { kind, market, plan };
|
|
5573
|
+
}
|
|
5574
|
+
case "ln_send": {
|
|
5575
|
+
const invoice = need(input.invoice, "the invoice to pay", kind);
|
|
5576
|
+
const request = await requestLightningSend(wallet, transportFor(market), {
|
|
5577
|
+
invoice,
|
|
5578
|
+
emulatorPubkey: deps.emulatorPubkey
|
|
5579
|
+
});
|
|
5580
|
+
return { kind, market, request };
|
|
5581
|
+
}
|
|
5582
|
+
case "ln_receive": {
|
|
5583
|
+
const params = {
|
|
5584
|
+
...corridorAmount(input, kind),
|
|
5585
|
+
covclaimdPubkey: need(deps.covclaimdPubkey, "deps.covclaimdPubkey", kind),
|
|
5586
|
+
decodeInvoice: need(deps.decodeInvoice, "deps.decodeInvoice", kind),
|
|
5587
|
+
maxPayAmount: input.maxPayAmount,
|
|
5588
|
+
emulatorPubkey: deps.emulatorPubkey
|
|
5589
|
+
};
|
|
5590
|
+
const request = await requestLightningReceive(wallet, transportFor(market), params);
|
|
5591
|
+
return { kind, market, request, invoice: request.invoice };
|
|
5592
|
+
}
|
|
5593
|
+
case "onchain_send": {
|
|
5594
|
+
need(deps.chain, "deps.chain (L1 access)", kind);
|
|
5595
|
+
const payoutPubkey = need(input.payoutPubkey, "a payoutPubkey", kind);
|
|
5596
|
+
const params = {
|
|
5597
|
+
...corridorAmount(input, kind),
|
|
5598
|
+
payoutPubkey,
|
|
5599
|
+
preimage: input.preimage,
|
|
5600
|
+
emulatorPubkey: deps.emulatorPubkey
|
|
5601
|
+
};
|
|
5602
|
+
const request = await requestOnchainSend(wallet, transportFor(market), params);
|
|
5603
|
+
return {
|
|
5604
|
+
kind,
|
|
5605
|
+
market,
|
|
5606
|
+
request,
|
|
5607
|
+
payoutPkScript: payoutScriptFor(
|
|
5608
|
+
input.payoutAddress,
|
|
5609
|
+
payoutPubkey,
|
|
5610
|
+
request.l1Network
|
|
5611
|
+
)
|
|
5612
|
+
};
|
|
5613
|
+
}
|
|
5614
|
+
case "onchain_receive":
|
|
5615
|
+
throw new Error(
|
|
5616
|
+
"onchain->arkade is not driven by RfqSwapManager yet; quote it directly with requestOnchainReceive"
|
|
5617
|
+
);
|
|
5618
|
+
}
|
|
5619
|
+
};
|
|
5620
|
+
const admit = async (swap, origin) => {
|
|
5621
|
+
let announced = false;
|
|
5622
|
+
const heard = manager.onSwapUpdate((updated) => {
|
|
5623
|
+
if (updated.rfqId === swap.rfqId) announced = true;
|
|
5624
|
+
});
|
|
5625
|
+
try {
|
|
5626
|
+
await manager.addSwap(swap, origin);
|
|
5627
|
+
} finally {
|
|
5628
|
+
heard();
|
|
5629
|
+
}
|
|
5630
|
+
const unified = { family: "rfq", swap };
|
|
5631
|
+
if (!announced) notify2(unified);
|
|
5632
|
+
return unified;
|
|
5633
|
+
};
|
|
5634
|
+
const fundPersisted = async (swap, origin, funding) => {
|
|
5635
|
+
await repository.saveRfqSwap(createRfqSwapRecord(origin, swap));
|
|
5636
|
+
return wallet.send({ address: funding.address, amount: funding.amount });
|
|
5637
|
+
};
|
|
5638
|
+
const accept = async (accepted) => {
|
|
5639
|
+
const now = Math.floor(Date.now() / 1e3);
|
|
5640
|
+
switch (accepted.kind) {
|
|
5641
|
+
case "spot": {
|
|
5642
|
+
const { plan } = accepted;
|
|
5643
|
+
const depositIsBtc = plan.deposit.asset.id === BTC_ASSET_ID;
|
|
5644
|
+
const offer = await createOffer(wallet, {
|
|
5645
|
+
wantAmount: plan.receive.atomic,
|
|
5646
|
+
...plan.receive.asset.id === BTC_ASSET_ID ? { offerAsset: import_sdk23.asset.AssetId.fromString(plan.deposit.asset.id) } : { wantAsset: import_sdk23.asset.AssetId.fromString(plan.receive.asset.id) },
|
|
5647
|
+
emulatorPubkey: deps.emulatorPubkey
|
|
5648
|
+
});
|
|
5649
|
+
const txid = await wallet.send({
|
|
5650
|
+
address: offer.address,
|
|
5651
|
+
// asset deposits ride the SDK's dust-sat carrier
|
|
5652
|
+
amount: depositIsBtc ? Number(plan.deposit.atomic) : void 0,
|
|
5653
|
+
assets: depositIsBtc ? void 0 : [{ assetId: plan.deposit.asset.id, amount: plan.deposit.atomic }],
|
|
5654
|
+
extensions: [offer.extension]
|
|
5655
|
+
});
|
|
5656
|
+
const swap = {
|
|
5657
|
+
id: txid,
|
|
5658
|
+
fromAsset: plan.deposit.asset.id,
|
|
5659
|
+
toAsset: plan.receive.asset.id,
|
|
5660
|
+
fromAmount: plan.deposit.atomic.toString(),
|
|
5661
|
+
toAmount: plan.receive.atomic.toString(),
|
|
5662
|
+
swapAddress: offer.address,
|
|
5663
|
+
swapPkScript: import_base17.hex.encode(offer.swapPkScript),
|
|
5664
|
+
offerHex: offer.offerHex,
|
|
5665
|
+
fundingTxid: txid,
|
|
5666
|
+
status: "pending",
|
|
5667
|
+
createdAt: Date.now()
|
|
5668
|
+
};
|
|
5669
|
+
await addAssetSwap(repository, swap);
|
|
5670
|
+
const unified = { family: "offer", swap };
|
|
5671
|
+
notify2(unified);
|
|
5672
|
+
return unified;
|
|
5673
|
+
}
|
|
5674
|
+
case "ln_send": {
|
|
5675
|
+
const { request } = accepted;
|
|
5676
|
+
const swap = {
|
|
5677
|
+
kind: "lightning_send",
|
|
5678
|
+
rfqId: request.rfqId,
|
|
5679
|
+
state: "pending",
|
|
5680
|
+
lockupPkScript: request.swapPkScript,
|
|
5681
|
+
lockup: { script: request.script, address: request.address },
|
|
5682
|
+
// the trader's own decode, never the solver's echo
|
|
5683
|
+
paymentHash: request.contractParams.paymentHash,
|
|
5684
|
+
refundLocktime: request.contractParams.refundLocktime,
|
|
5685
|
+
createdAt: now,
|
|
5686
|
+
updatedAt: now
|
|
5687
|
+
};
|
|
5688
|
+
const origin = {
|
|
5689
|
+
kind: "lightning_send",
|
|
5690
|
+
lockupAddress: request.address,
|
|
5691
|
+
profile: rfqSecretsProfile(request.secrets, swap.paymentHash),
|
|
5692
|
+
amount: request.fundAmount
|
|
5693
|
+
};
|
|
5694
|
+
const txid = await fundPersisted(swap, origin, {
|
|
5695
|
+
address: request.address,
|
|
5696
|
+
amount: request.fundAmount
|
|
5697
|
+
});
|
|
5698
|
+
return admit(swap, { ...origin, fundingTxid: txid });
|
|
5699
|
+
}
|
|
5700
|
+
case "ln_receive": {
|
|
5701
|
+
const { request } = accepted;
|
|
5702
|
+
const paymentHash = import_base17.hex.encode(request.secrets.paymentHash);
|
|
5703
|
+
const swap = {
|
|
5704
|
+
kind: "lightning_receive",
|
|
5705
|
+
rfqId: request.rfqId,
|
|
5706
|
+
state: "pending",
|
|
5707
|
+
lockupPkScript: request.swapPkScript,
|
|
5708
|
+
lockup: { script: request.script, address: request.address },
|
|
5709
|
+
paymentHash,
|
|
5710
|
+
refundLocktime: request.contractParams.refundLocktime,
|
|
5711
|
+
expectedAmount: request.expectedAmount,
|
|
5712
|
+
createdAt: now,
|
|
5713
|
+
updatedAt: now
|
|
5714
|
+
};
|
|
5715
|
+
return admit(swap, {
|
|
5716
|
+
kind: "lightning_receive",
|
|
5717
|
+
lockupAddress: request.address,
|
|
5718
|
+
profile: {
|
|
5719
|
+
...rfqSecretsProfile(request.secrets, paymentHash),
|
|
5720
|
+
expectedAmount: request.expectedAmount,
|
|
5721
|
+
payoutAddress: request.payoutAddress
|
|
5722
|
+
},
|
|
5723
|
+
amount: request.expectedAmount
|
|
5724
|
+
});
|
|
5725
|
+
}
|
|
5726
|
+
case "onchain_send": {
|
|
5727
|
+
const { request } = accepted;
|
|
5728
|
+
const paymentHash = import_base17.hex.encode(request.secrets.paymentHash);
|
|
5729
|
+
const swap = {
|
|
5730
|
+
kind: "onchain_send",
|
|
5731
|
+
rfqId: request.rfqId,
|
|
5732
|
+
state: "pending",
|
|
5733
|
+
lockupPkScript: request.swapPkScript,
|
|
5734
|
+
lockup: { script: request.script, address: request.address },
|
|
5735
|
+
paymentHash,
|
|
5736
|
+
refundLocktime: request.refundLocktime,
|
|
5737
|
+
htlc: request.htlc,
|
|
5738
|
+
minConfirmations: request.minConfirmations,
|
|
5739
|
+
payoutPkScript: accepted.payoutPkScript,
|
|
5740
|
+
createdAt: now,
|
|
5741
|
+
updatedAt: now
|
|
5742
|
+
};
|
|
5743
|
+
const origin = {
|
|
5744
|
+
kind: "onchain_send",
|
|
5745
|
+
lockupAddress: request.address,
|
|
5746
|
+
profile: {
|
|
5747
|
+
...rfqSecretsProfile(request.secrets, paymentHash),
|
|
5748
|
+
...onchainSendProfile({
|
|
5749
|
+
...request,
|
|
5750
|
+
payoutPkScript: accepted.payoutPkScript
|
|
5751
|
+
})
|
|
5752
|
+
},
|
|
5753
|
+
amount: request.fundAmount
|
|
5754
|
+
};
|
|
5755
|
+
const txid = await fundPersisted(swap, origin, {
|
|
5756
|
+
address: request.address,
|
|
5757
|
+
amount: request.fundAmount
|
|
5758
|
+
});
|
|
5759
|
+
return admit(swap, { ...origin, fundingTxid: txid });
|
|
5760
|
+
}
|
|
5761
|
+
}
|
|
5762
|
+
};
|
|
5763
|
+
return {
|
|
5764
|
+
manager,
|
|
5765
|
+
markets: (useCache) => discoverMarkets({ ...deps.discovery, repository, useCache }),
|
|
5766
|
+
quote,
|
|
5767
|
+
accept,
|
|
5768
|
+
cancel: async (fundingTxid) => {
|
|
5769
|
+
const swaps = await getAssetSwapsOrThrow(repository);
|
|
5770
|
+
const swap = swaps.find((s) => s.id === fundingTxid);
|
|
5771
|
+
if (!swap) throw new Error(`no offer swap with funding txid ${fundingTxid}`);
|
|
5772
|
+
if (swap.status !== "pending") {
|
|
5773
|
+
throw new Error(`offer swap ${fundingTxid} is ${swap.status}, not cancellable`);
|
|
5774
|
+
}
|
|
5775
|
+
await updateAssetSwap(repository, fundingTxid, { status: "cancelling" });
|
|
5776
|
+
await cancelOffer(wallet, swap.offerHex, {
|
|
5777
|
+
repository,
|
|
5778
|
+
fundingTxid
|
|
5779
|
+
});
|
|
5780
|
+
},
|
|
5781
|
+
// Both families, live AND terminal — the offer half reads the whole
|
|
5782
|
+
// stored history, and the RFQ half is every swap the manager holds.
|
|
5783
|
+
// Its history is what `start()` restored plus what this session
|
|
5784
|
+
// accepted; records past `RFQ_SWAP_RETENTION_SECONDS` are pruned.
|
|
5785
|
+
swaps: async () => {
|
|
5786
|
+
const offers = await getAssetSwaps(repository);
|
|
5787
|
+
const rfq = await manager.getAllSwaps();
|
|
5788
|
+
return [
|
|
5789
|
+
...offers.map((swap) => ({ family: "offer", swap })),
|
|
5790
|
+
...rfq.map((swap) => ({ family: "rfq", swap }))
|
|
5791
|
+
];
|
|
5792
|
+
},
|
|
5793
|
+
onUpdate: (listener) => {
|
|
5794
|
+
listeners.add(listener);
|
|
5795
|
+
return () => listeners.delete(listener);
|
|
5796
|
+
},
|
|
5797
|
+
start: async () => {
|
|
5798
|
+
managerDeps.contracts ??= await wallet.getContractManager();
|
|
5799
|
+
await manager.restoreFromRepository();
|
|
5800
|
+
await manager.start();
|
|
5801
|
+
watcher ??= await watchOfferSwaps({
|
|
5802
|
+
wallet,
|
|
5803
|
+
repository,
|
|
5804
|
+
onUpdate: (swap) => notify2({ family: "offer", swap })
|
|
5805
|
+
});
|
|
5806
|
+
},
|
|
5807
|
+
stop: async () => {
|
|
5808
|
+
await manager.stop();
|
|
5809
|
+
watcher?.stop();
|
|
5810
|
+
watcher = void 0;
|
|
5811
|
+
}
|
|
5812
|
+
};
|
|
5813
|
+
}
|
|
4687
5814
|
// Annotate the CommonJS export names for ESM import in node:
|
|
4688
5815
|
0 && (module.exports = {
|
|
4689
5816
|
ARKADE_ASSET,
|
|
@@ -4693,6 +5820,7 @@ async function lockupTxids(indexer, record, wantFunding) {
|
|
|
4693
5820
|
InMemoryAssetSwapRepository,
|
|
4694
5821
|
IndexedDbAssetSwapRepository,
|
|
4695
5822
|
LIGHTNING_BTC,
|
|
5823
|
+
LIGHTNING_RAIL,
|
|
4696
5824
|
LIGHTNING_RECEIVE_PAIR,
|
|
4697
5825
|
LIGHTNING_SEND_PAIR,
|
|
4698
5826
|
LOCKTIME_THRESHOLD,
|
|
@@ -4703,6 +5831,7 @@ async function lockupTxids(indexer, record, wantFunding) {
|
|
|
4703
5831
|
MAX_MIN_CONFIRMATIONS,
|
|
4704
5832
|
MIN_CLAIM_WINDOW_SECONDS,
|
|
4705
5833
|
MIN_HEADROOM_SECONDS,
|
|
5834
|
+
NoSpendableDepositError,
|
|
4706
5835
|
OFFER_PACKET_TYPE,
|
|
4707
5836
|
ONCHAIN_BTC,
|
|
4708
5837
|
ONCHAIN_CLAIM_MARGIN_SECONDS,
|
|
@@ -4711,6 +5840,9 @@ async function lockupTxids(indexer, record, wantFunding) {
|
|
|
4711
5840
|
ONCHAIN_RECEIVE_PAIR,
|
|
4712
5841
|
ONCHAIN_SECONDS_PER_BLOCK,
|
|
4713
5842
|
ONCHAIN_SEND_PAIR,
|
|
5843
|
+
ONCHAIN_SWAP_RAIL,
|
|
5844
|
+
OfferCovenantMismatchError,
|
|
5845
|
+
PAYMENT_STATUS,
|
|
4714
5846
|
PreimageNotRecoverableError,
|
|
4715
5847
|
QUOTE_OPTIONS,
|
|
4716
5848
|
REFUND_MTP_LAG_SECONDS,
|
|
@@ -4727,6 +5859,8 @@ async function lockupTxids(indexer, record, wantFunding) {
|
|
|
4727
5859
|
SWAP_LOCKUP_CONTRACT_KIND,
|
|
4728
5860
|
SWAP_LOCKUP_CONTRACT_LABEL,
|
|
4729
5861
|
SWAP_LOCKUP_CONTRACT_TYPE,
|
|
5862
|
+
SWAP_ROUTER_PRIORITY,
|
|
5863
|
+
SwapPaymentFailedError,
|
|
4730
5864
|
SwapRefusal,
|
|
4731
5865
|
addAssetSwap,
|
|
4732
5866
|
arkadeAssetLeg,
|
|
@@ -4741,6 +5875,7 @@ async function lockupTxids(indexer, record, wantFunding) {
|
|
|
4741
5875
|
buildHtlcRefund,
|
|
4742
5876
|
cancelOffer,
|
|
4743
5877
|
chainSourceFrom,
|
|
5878
|
+
claimFeeSats,
|
|
4744
5879
|
claimOnchainFill,
|
|
4745
5880
|
claimReceiveLockup,
|
|
4746
5881
|
classifyDepositSpend,
|
|
@@ -4748,6 +5883,8 @@ async function lockupTxids(indexer, record, wantFunding) {
|
|
|
4748
5883
|
classifySpend,
|
|
4749
5884
|
createOffer,
|
|
4750
5885
|
createRfqSwapRecord,
|
|
5886
|
+
createSwapClient,
|
|
5887
|
+
createSwapPaymentRouter,
|
|
4751
5888
|
decodeOffer,
|
|
4752
5889
|
deriveLightningReceive,
|
|
4753
5890
|
deriveOnchainReceive,
|
|
@@ -4762,28 +5899,35 @@ async function lockupTxids(indexer, record, wantFunding) {
|
|
|
4762
5899
|
httpTransport,
|
|
4763
5900
|
isRfqSwapTerminal,
|
|
4764
5901
|
isRfqTerminal,
|
|
5902
|
+
isTerminalStatus,
|
|
4765
5903
|
l1ScriptForAddress,
|
|
5904
|
+
lightningRail,
|
|
5905
|
+
lightningReceiveContract,
|
|
4766
5906
|
lightningReceiveRequest,
|
|
5907
|
+
lightningSendContract,
|
|
4767
5908
|
lightningSendRequest,
|
|
4768
|
-
lightningSendVtxoScript,
|
|
4769
5909
|
lockupContractParams,
|
|
4770
5910
|
makeCachedFeedFetch,
|
|
4771
5911
|
newPreimage,
|
|
4772
5912
|
newRfqId,
|
|
4773
5913
|
nextOnchainAction,
|
|
5914
|
+
normalizeRfqSwapRecord,
|
|
5915
|
+
offerContract,
|
|
4774
5916
|
offerTermsFromQuote,
|
|
4775
|
-
offerVtxoScript,
|
|
4776
5917
|
onchainHtlcScript,
|
|
4777
5918
|
onchainReceiveRequest,
|
|
4778
5919
|
onchainSendProfile,
|
|
4779
5920
|
onchainSendRequest,
|
|
5921
|
+
onchainSwapRail,
|
|
4780
5922
|
paymentHashOf,
|
|
5923
|
+
paymentStatusOf,
|
|
4781
5924
|
preimageForSwapRecord,
|
|
4782
5925
|
pushClaim,
|
|
4783
5926
|
pushRefundWithoutReceiver,
|
|
5927
|
+
railAvailable,
|
|
4784
5928
|
readLockupFate,
|
|
4785
5929
|
rebuildRfqSwap,
|
|
4786
|
-
|
|
5930
|
+
receiverExact,
|
|
4787
5931
|
refundIfUnresolved,
|
|
4788
5932
|
registerLockupContract,
|
|
4789
5933
|
relayTransport,
|
|
@@ -4810,6 +5954,7 @@ async function lockupTxids(indexer, record, wantFunding) {
|
|
|
4810
5954
|
spendTxidsOf,
|
|
4811
5955
|
spendUpdate,
|
|
4812
5956
|
swapActivityResolver,
|
|
5957
|
+
swapHandle,
|
|
4813
5958
|
swapPrograms,
|
|
4814
5959
|
swapSecretsToRecord,
|
|
4815
5960
|
unilateralClaimDelay,
|