@arkade-os/swap 0.0.14 → 0.0.16
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 +71 -8
- package/dist/{chunk-IFZOBTTG.js → chunk-7N7XO4PY.js} +82 -3
- package/dist/index.cjs +808 -306
- package/dist/index.d.cts +192 -9
- package/dist/index.d.ts +192 -9
- package/dist/index.js +624 -207
- package/dist/nostr.cjs +80 -3
- package/dist/nostr.d.cts +1 -1
- package/dist/nostr.d.ts +1 -1
- package/dist/nostr.js +1 -1
- package/dist/repositories/realm/index.d.cts +2 -2
- package/dist/repositories/realm/index.d.ts +2 -2
- package/dist/repositories/sqlite/index.d.cts +2 -2
- package/dist/repositories/sqlite/index.d.ts +2 -2
- package/dist/{repository-D5JoeqwB.d.ts → repository-DH5zjdw4.d.ts} +1 -1
- package/dist/{repository-Vfx65uF1.d.cts → repository-rwU1Sv2-.d.cts} +1 -1
- package/dist/{rfq-CYfKvAPp.d.ts → rfq-DirJn_4k.d.cts} +20 -2
- package/dist/{rfq-CYfKvAPp.d.cts → rfq-DirJn_4k.d.ts} +20 -2
- package/package.json +2 -2
package/dist/index.js
CHANGED
|
@@ -19,6 +19,7 @@ import {
|
|
|
19
19
|
ONCHAIN_RECEIVE_PAIR,
|
|
20
20
|
ONCHAIN_SECONDS_PER_BLOCK,
|
|
21
21
|
ONCHAIN_SEND_PAIR,
|
|
22
|
+
RFQ_REFUSAL_ERROR_CODES,
|
|
22
23
|
RFQ_TERMINAL_STATES,
|
|
23
24
|
SOLO_REFUND_HEADROOM_SECONDS,
|
|
24
25
|
SWAP_LOCKUP_CONTRACT_KIND,
|
|
@@ -39,6 +40,7 @@ import {
|
|
|
39
40
|
deriveOnchainSend,
|
|
40
41
|
extractPreimage,
|
|
41
42
|
httpTransport,
|
|
43
|
+
isRfqRefusalErrorCode,
|
|
42
44
|
l1ScriptForAddress,
|
|
43
45
|
lightningReceiveRequest,
|
|
44
46
|
lightningSendRequest,
|
|
@@ -65,7 +67,7 @@ import {
|
|
|
65
67
|
unilateralRefundWithoutReceiverDelay,
|
|
66
68
|
verifyLockupAddress,
|
|
67
69
|
verifyReceiveInvoice
|
|
68
|
-
} from "./chunk-
|
|
70
|
+
} from "./chunk-7N7XO4PY.js";
|
|
69
71
|
import {
|
|
70
72
|
InMemoryAssetSwapRepository,
|
|
71
73
|
marketsCacheKey
|
|
@@ -75,8 +77,10 @@ import {
|
|
|
75
77
|
import { hex as hex2 } from "@scure/base";
|
|
76
78
|
import { concatBytes } from "@scure/btc-signer/utils.js";
|
|
77
79
|
import {
|
|
80
|
+
ASSET_CARRIER_SATS as SDK_ASSET_CARRIER_SATS,
|
|
78
81
|
ArkAddress,
|
|
79
82
|
RestArkProvider,
|
|
83
|
+
RestEmulatorProvider,
|
|
80
84
|
RestIndexerProvider,
|
|
81
85
|
arkade,
|
|
82
86
|
asset,
|
|
@@ -188,10 +192,10 @@ function addressOutstanding(swaps, script) {
|
|
|
188
192
|
}
|
|
189
193
|
return true;
|
|
190
194
|
}
|
|
191
|
-
async function promoteOfferContract(manager, script) {
|
|
195
|
+
async function promoteOfferContract(manager, script, issued = Date.now()) {
|
|
192
196
|
await serialize(script, async () => {
|
|
193
197
|
await manager.setContractWatchState(script, "watched");
|
|
194
|
-
issuedAt.set(script,
|
|
198
|
+
issuedAt.set(script, issued);
|
|
195
199
|
});
|
|
196
200
|
}
|
|
197
201
|
async function retireOfferContract(manager, swaps, script) {
|
|
@@ -397,7 +401,7 @@ function u64(name, value) {
|
|
|
397
401
|
if (value < BigInt(0) || value >> BigInt(64) > BigInt(0)) {
|
|
398
402
|
throw new Error(`${name} does not fit the offer wire format (u64)`);
|
|
399
403
|
}
|
|
400
|
-
const out = new Uint8Array(
|
|
404
|
+
const out = new Uint8Array(8);
|
|
401
405
|
new DataView(out.buffer).setBigUint64(0, value, false);
|
|
402
406
|
return out;
|
|
403
407
|
}
|
|
@@ -532,10 +536,9 @@ function decodeExitDelay(value) {
|
|
|
532
536
|
}
|
|
533
537
|
var OFFER_CONTRACT_LABEL = "Arkade swap offer";
|
|
534
538
|
var OFFER_CONTRACT_KIND = "asset-swap-offer";
|
|
535
|
-
async function registerOfferContract(wallet, arkServerUrl, network, binding, serverPubkey, expectedPkScript) {
|
|
536
|
-
const
|
|
537
|
-
const
|
|
538
|
-
const client = await arkade.Arkade.connect({
|
|
539
|
+
async function registerOfferContract(wallet, arkServerUrl, network, binding, serverPubkey, expectedPkScript, opts = {}) {
|
|
540
|
+
const contractManager = opts.contractManager ?? await wallet.getContractManager();
|
|
541
|
+
const client = opts.client ?? await arkade.Arkade.connect({
|
|
539
542
|
arkade: new RestArkProvider(arkServerUrl),
|
|
540
543
|
indexer: new RestIndexerProvider(arkServerUrl),
|
|
541
544
|
identity: wallet.identity,
|
|
@@ -545,15 +548,66 @@ async function registerOfferContract(wallet, arkServerUrl, network, binding, ser
|
|
|
545
548
|
network: getNetwork(network),
|
|
546
549
|
contractManager
|
|
547
550
|
});
|
|
551
|
+
await contractManager.createContract(
|
|
552
|
+
offerContractParams(client, binding, serverPubkey, expectedPkScript)
|
|
553
|
+
);
|
|
554
|
+
await promoteOfferContract(contractManager, hex2.encode(expectedPkScript), opts.issued);
|
|
555
|
+
}
|
|
556
|
+
function offerContractParams(client, binding, serverPubkey, expectedPkScript) {
|
|
557
|
+
const { program, args, keys } = swapProgramBinding(binding, serverPubkey);
|
|
548
558
|
const contract = new arkade.ArkadeContract(client, program, args, keys);
|
|
549
559
|
if (hex2.encode(contract.pkScript) !== hex2.encode(expectedPkScript)) {
|
|
550
560
|
throw new Error("derived covenant does not match the offer's swapPkScript");
|
|
551
561
|
}
|
|
552
|
-
|
|
562
|
+
return {
|
|
563
|
+
...contract.toContractParams(),
|
|
553
564
|
label: OFFER_CONTRACT_LABEL,
|
|
554
565
|
metadata: { genericallySpendable: false, kind: OFFER_CONTRACT_KIND }
|
|
566
|
+
};
|
|
567
|
+
}
|
|
568
|
+
async function restoreOfferCoverage(wallet, arkServerUrl, swaps) {
|
|
569
|
+
const live = swaps.filter((s) => !RETIRABLE.includes(s.status));
|
|
570
|
+
if (live.length === 0) return;
|
|
571
|
+
const arkProvider = new RestArkProvider(arkServerUrl);
|
|
572
|
+
const info = await arkProvider.getInfo();
|
|
573
|
+
const serverPubKey = hex2.decode(toXOnlySignerHex(info.signerPubkey));
|
|
574
|
+
const contractManager = await wallet.getContractManager();
|
|
575
|
+
const client = await arkade.Arkade.connect({
|
|
576
|
+
arkade: arkProvider,
|
|
577
|
+
indexer: new RestIndexerProvider(arkServerUrl),
|
|
578
|
+
identity: wallet.identity,
|
|
579
|
+
network: getNetwork(info.network),
|
|
580
|
+
contractManager
|
|
555
581
|
});
|
|
556
|
-
|
|
582
|
+
const done = /* @__PURE__ */ new Set();
|
|
583
|
+
const covenants = [];
|
|
584
|
+
for (const swap of live) {
|
|
585
|
+
if (done.has(swap.swapPkScript)) continue;
|
|
586
|
+
done.add(swap.swapPkScript);
|
|
587
|
+
try {
|
|
588
|
+
const { swapPkScript: _, ...binding } = decodeOffer(hex2.decode(swap.offerHex));
|
|
589
|
+
covenants.push({
|
|
590
|
+
script: swap.swapPkScript,
|
|
591
|
+
issued: swap.createdAt,
|
|
592
|
+
params: offerContractParams(
|
|
593
|
+
client,
|
|
594
|
+
binding,
|
|
595
|
+
serverPubKey,
|
|
596
|
+
hex2.decode(swap.swapPkScript)
|
|
597
|
+
)
|
|
598
|
+
});
|
|
599
|
+
} catch (err) {
|
|
600
|
+
console.warn(`[swap] could not restore coverage for ${swap.swapPkScript}`, err);
|
|
601
|
+
}
|
|
602
|
+
}
|
|
603
|
+
if (covenants.length === 0) return;
|
|
604
|
+
const paramsList = covenants.map((c) => c.params);
|
|
605
|
+
if (contractManager.createContracts) {
|
|
606
|
+
await contractManager.createContracts(paramsList);
|
|
607
|
+
} else {
|
|
608
|
+
for (const params of paramsList) await contractManager.createContract(params);
|
|
609
|
+
}
|
|
610
|
+
for (const c of covenants) await promoteOfferContract(contractManager, c.script, c.issued);
|
|
557
611
|
}
|
|
558
612
|
function serverExitDelay(delay) {
|
|
559
613
|
if (typeof delay !== "bigint" || delay <= BigInt(0)) {
|
|
@@ -666,6 +720,127 @@ async function cancelOffer(wallet, arkServerUrl, offerHex, opts) {
|
|
|
666
720
|
}
|
|
667
721
|
return txid;
|
|
668
722
|
}
|
|
723
|
+
var ASSET_CARRIER_SATS = BigInt(SDK_ASSET_CARRIER_SATS);
|
|
724
|
+
async function fillOffer(wallet, arkServerUrl, offerHex, opts) {
|
|
725
|
+
const {
|
|
726
|
+
fund,
|
|
727
|
+
payoutScript,
|
|
728
|
+
fundingTxid,
|
|
729
|
+
swapAddress,
|
|
730
|
+
assetCarrierSats = ASSET_CARRIER_SATS,
|
|
731
|
+
emulator,
|
|
732
|
+
emulatorPubkey
|
|
733
|
+
} = opts;
|
|
734
|
+
const offer = decodeOffer(hex2.decode(offerHex));
|
|
735
|
+
const wantedAssetId = offer.wantAsset?.toString();
|
|
736
|
+
if (fund.length === 0) {
|
|
737
|
+
throw new Error("fillOffer needs coins to pay wantAmount with \u2014 `fund` is empty");
|
|
738
|
+
}
|
|
739
|
+
if (wantedAssetId !== void 0) {
|
|
740
|
+
const supplied = fund.reduce(
|
|
741
|
+
(sum, coin) => sum + amountOfAsset(coin.assets, wantedAssetId),
|
|
742
|
+
BigInt(0)
|
|
743
|
+
);
|
|
744
|
+
if (supplied < offer.wantAmount) {
|
|
745
|
+
throw new Error(
|
|
746
|
+
`fillOffer needs ${offer.wantAmount} of ${wantedAssetId} to pay the maker, but \`fund\` declares ${supplied} \u2014 pass coins carrying it, and declare their assets`
|
|
747
|
+
);
|
|
748
|
+
}
|
|
749
|
+
}
|
|
750
|
+
const contractManager = await wallet.getContractManager();
|
|
751
|
+
const client = await arkade.Arkade.connect({
|
|
752
|
+
arkade: new RestArkProvider(arkServerUrl),
|
|
753
|
+
indexer: new RestIndexerProvider(arkServerUrl),
|
|
754
|
+
identity: wallet.identity,
|
|
755
|
+
contractManager,
|
|
756
|
+
// `fulfill` is a covenant path, so the emulator executes the arkade
|
|
757
|
+
// script and finalizes with arkd. Without it the spend is built and then
|
|
758
|
+
// refused at submission — there is no default to fall back on.
|
|
759
|
+
emulator: typeof emulator === "string" ? new RestEmulatorProvider(emulator) : emulator,
|
|
760
|
+
// Only reached for the client's own emulatorKey, which this fill never
|
|
761
|
+
// derives against — the offer's own key is bound below. It still has to
|
|
762
|
+
// resolve, and on a network with no pinned key it throws without this.
|
|
763
|
+
...emulatorPubkey ? { emulatorPubkey } : {}
|
|
764
|
+
});
|
|
765
|
+
const serverKey = swapAddress ? ArkAddress.decode(swapAddress).serverPubKey : client.serverKey;
|
|
766
|
+
const { program, args, keys } = swapProgramBinding(offer, serverKey);
|
|
767
|
+
const rebuilt = new arkade.ArkadeProgramScript(program, args, keys);
|
|
768
|
+
if (hex2.encode(rebuilt.pkScript) !== hex2.encode(offer.swapPkScript)) {
|
|
769
|
+
throw new Error(
|
|
770
|
+
"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"
|
|
771
|
+
);
|
|
772
|
+
}
|
|
773
|
+
const contract = new arkade.ArkadeContract(client, program, args, keys);
|
|
774
|
+
const [vtxos, takerAddress] = await Promise.all([contract.getUtxos(), wallet.getAddress()]);
|
|
775
|
+
if (!fundingTxid && vtxos.length > 1) {
|
|
776
|
+
throw new Error(
|
|
777
|
+
"multiple spendable deposits at the swap address \u2014 pass fundingTxid to select one"
|
|
778
|
+
);
|
|
779
|
+
}
|
|
780
|
+
const vtxo = fundingTxid ? vtxos.find((v) => v.txid === fundingTxid) : vtxos[0];
|
|
781
|
+
if (!vtxo) throw new Error("no spendable VTXO at the swap address");
|
|
782
|
+
if (offer.offerAsset) {
|
|
783
|
+
const offered = offer.offerAsset.toString();
|
|
784
|
+
const deposited = amountOfAsset(vtxo.assets, offered);
|
|
785
|
+
if (deposited <= BigInt(0)) {
|
|
786
|
+
throw new Error(
|
|
787
|
+
`the deposit at the swap address carries no ${offered}, which this offer sells \u2014 filling it would pay wantAmount for nothing. The indexer may be behind, or the offer may not be backed by what it advertises`
|
|
788
|
+
);
|
|
789
|
+
}
|
|
790
|
+
}
|
|
791
|
+
const payout = payoutScript ?? ArkAddress.decode(takerAddress).pkScript;
|
|
792
|
+
const makerSats = wantedAssetId === void 0 ? offer.wantAmount : assetCarrierSats;
|
|
793
|
+
const fill = contract.functions.fulfill().from({ txid: vtxo.txid, vout: vtxo.vout, value: vtxo.value }).fund(fund).to(offer.makerPkScript, makerSats).change(payout);
|
|
794
|
+
const held = /* @__PURE__ */ new Map();
|
|
795
|
+
const hold = (vin, assets) => {
|
|
796
|
+
for (const a of assets ?? []) {
|
|
797
|
+
const amount = BigInt(a.amount);
|
|
798
|
+
if (amount <= BigInt(0)) continue;
|
|
799
|
+
held.set(a.assetId, [...held.get(a.assetId) ?? [], { vin, amount }]);
|
|
800
|
+
}
|
|
801
|
+
};
|
|
802
|
+
hold(0, vtxo.assets);
|
|
803
|
+
fund.forEach((coin, i) => hold(i + 1, coin.assets));
|
|
804
|
+
const outputsSum = makerSats;
|
|
805
|
+
const inputsSum = fund.reduce((s, c) => s + BigInt(c.value), BigInt(vtxo.value));
|
|
806
|
+
const hasPayoutOutput = inputsSum > outputsSum;
|
|
807
|
+
const emitWanted = () => {
|
|
808
|
+
if (wantedAssetId === void 0) return;
|
|
809
|
+
const supplying = held.get(wantedAssetId) ?? [];
|
|
810
|
+
const supplied = supplying.reduce((s, i) => s + i.amount, BigInt(0));
|
|
811
|
+
const outputs = [{ vout: 0, amount: offer.wantAmount }];
|
|
812
|
+
const surplus = supplied - offer.wantAmount;
|
|
813
|
+
if (surplus > BigInt(0)) {
|
|
814
|
+
if (!hasPayoutOutput) {
|
|
815
|
+
throw new Error(
|
|
816
|
+
`fillOffer has ${surplus} of ${wantedAssetId} to return but no payout output \u2014 fund with more sats than the maker's output takes`
|
|
817
|
+
);
|
|
818
|
+
}
|
|
819
|
+
outputs.push({ vout: 1, amount: surplus });
|
|
820
|
+
}
|
|
821
|
+
fill.withAsset({ assetId: wantedAssetId, inputs: supplying, outputs });
|
|
822
|
+
held.delete(wantedAssetId);
|
|
823
|
+
};
|
|
824
|
+
emitWanted();
|
|
825
|
+
for (const [assetId, inputs] of held) {
|
|
826
|
+
const amount = inputs.reduce((s, i) => s + i.amount, BigInt(0));
|
|
827
|
+
if (!hasPayoutOutput) {
|
|
828
|
+
throw new Error(
|
|
829
|
+
`fillOffer has ${amount} of ${assetId} to return but no payout output \u2014 fund with more sats than the maker's output takes`
|
|
830
|
+
);
|
|
831
|
+
}
|
|
832
|
+
fill.withAsset({ assetId, inputs, outputs: [{ vout: 1, amount }] });
|
|
833
|
+
}
|
|
834
|
+
const { txid } = await fill.send();
|
|
835
|
+
return txid;
|
|
836
|
+
}
|
|
837
|
+
var amountOfAsset = (assets, assetId) => {
|
|
838
|
+
let total = BigInt(0);
|
|
839
|
+
for (const a of assets ?? []) {
|
|
840
|
+
if (a.assetId === assetId) total += BigInt(a.amount);
|
|
841
|
+
}
|
|
842
|
+
return total;
|
|
843
|
+
};
|
|
669
844
|
|
|
670
845
|
// src/markets.ts
|
|
671
846
|
import {
|
|
@@ -675,6 +850,45 @@ import {
|
|
|
675
850
|
sideLimits
|
|
676
851
|
} from "@arkade-os/solver-discovery";
|
|
677
852
|
import { isSubdust } from "@arkade-os/sdk";
|
|
853
|
+
|
|
854
|
+
// src/marketShape.ts
|
|
855
|
+
var MARKET_CORRIDORS = ["arkade", "bolt11", "bitcoin", "eip155"];
|
|
856
|
+
var LEGACY_CORRIDORS = {
|
|
857
|
+
arkade: "arkade",
|
|
858
|
+
lightning: "bolt11",
|
|
859
|
+
onchain: "bitcoin",
|
|
860
|
+
eip155: "eip155"
|
|
861
|
+
};
|
|
862
|
+
var assetOf = (market, side) => side === "base" ? market.base_asset : market.quote_asset;
|
|
863
|
+
var marketAssetId = (market, side) => {
|
|
864
|
+
const asset2 = assetOf(market, side);
|
|
865
|
+
if (typeof asset2?.caip19_id === "string") return asset2.caip19_id;
|
|
866
|
+
return typeof asset2?.id === "string" ? asset2.id : void 0;
|
|
867
|
+
};
|
|
868
|
+
var chainNamespaceOf = (id) => {
|
|
869
|
+
if (id === void 0) return void 0;
|
|
870
|
+
const slash = id.indexOf("/");
|
|
871
|
+
const colon = id.indexOf(":");
|
|
872
|
+
return slash === -1 || colon === -1 || colon > slash ? void 0 : id.slice(0, colon);
|
|
873
|
+
};
|
|
874
|
+
var marketCorridor = (market, side) => {
|
|
875
|
+
const namespace = chainNamespaceOf(marketAssetId(market, side));
|
|
876
|
+
if (namespace !== void 0) {
|
|
877
|
+
return MARKET_CORRIDORS.includes(namespace) ? namespace : "arkade";
|
|
878
|
+
}
|
|
879
|
+
const legacy = side === "base" ? market.base_corridor : market.quote_corridor;
|
|
880
|
+
return typeof legacy === "string" ? LEGACY_CORRIDORS[legacy] ?? "arkade" : "arkade";
|
|
881
|
+
};
|
|
882
|
+
var isRfqMarket = (market) => marketCorridor(market, "base") !== "arkade" || marketCorridor(market, "quote") !== "arkade";
|
|
883
|
+
var sideLabel = (market, side) => {
|
|
884
|
+
const ticker = assetOf(market, side)?.ticker;
|
|
885
|
+
const label = typeof ticker === "string" && ticker.length > 0 ? ticker : "?";
|
|
886
|
+
const corridor = marketCorridor(market, side);
|
|
887
|
+
return corridor === "arkade" ? label : `${corridor}:${label}`;
|
|
888
|
+
};
|
|
889
|
+
var marketPairLabel = (market) => `${sideLabel(market, "base")}/${sideLabel(market, "quote")}`;
|
|
890
|
+
|
|
891
|
+
// src/markets.ts
|
|
678
892
|
var QUOTE_OPTIONS = { safetyBps: 0 };
|
|
679
893
|
var makeCachedFeedFetch = (ttlMs = 3e4, fetchImpl = fetch) => {
|
|
680
894
|
const cache = /* @__PURE__ */ new Map();
|
|
@@ -723,14 +937,17 @@ var makeCachedFeedFetch = (ttlMs = 3e4, fetchImpl = fetch) => {
|
|
|
723
937
|
var MARKETS_CACHE_TTL_MS = 60 * 60 * 1e3;
|
|
724
938
|
var isMarketShaped = (m) => {
|
|
725
939
|
const market = m;
|
|
726
|
-
|
|
940
|
+
if (!market) return false;
|
|
941
|
+
return typeof market.base_asset?.id === "string" && typeof market.quote_asset?.id === "string" && typeof market.quote_asset.decimals === "number";
|
|
727
942
|
};
|
|
728
943
|
var readMarketsCache = async (repository, network, registry) => {
|
|
729
944
|
try {
|
|
730
945
|
const entry = await repository.getCachedMarkets(network, registry);
|
|
731
946
|
if (!Array.isArray(entry?.markets) || typeof entry?.fetchedAt !== "number")
|
|
732
947
|
return void 0;
|
|
733
|
-
|
|
948
|
+
const markets = entry.markets.filter(isMarketShaped);
|
|
949
|
+
if (entry.markets.length > 0 && markets.length === 0) return void 0;
|
|
950
|
+
return { ...entry, markets };
|
|
734
951
|
} catch {
|
|
735
952
|
return void 0;
|
|
736
953
|
}
|
|
@@ -771,10 +988,33 @@ var discoverMarkets = async (options) => {
|
|
|
771
988
|
};
|
|
772
989
|
var findMarket = (markets, fromId, toId) => {
|
|
773
990
|
if (fromId === toId) return void 0;
|
|
774
|
-
const
|
|
991
|
+
const resolveMarketId = (id) => {
|
|
992
|
+
for (const market of markets) {
|
|
993
|
+
for (const side of ["base", "quote"]) {
|
|
994
|
+
const asset2 = side === "base" ? market.base_asset : market.quote_asset;
|
|
995
|
+
if (asset2.id === id) return id;
|
|
996
|
+
const canonical = marketAssetId(market, side) ?? "";
|
|
997
|
+
const matchesBtc = id === BTC_ASSET_ID && /^arkade:[^/]+\/slip44:(?:0|1)$/.test(canonical);
|
|
998
|
+
const matchesAsset = /^[0-9a-f]{68}$/.test(id) && canonical.endsWith(`/asset:${id}`);
|
|
999
|
+
if ((matchesBtc || matchesAsset) && typeof asset2.id === "string") return asset2.id;
|
|
1000
|
+
}
|
|
1001
|
+
}
|
|
1002
|
+
return id;
|
|
1003
|
+
};
|
|
1004
|
+
const resolvedFrom = resolveMarketId(fromId);
|
|
1005
|
+
const resolvedTo = resolveMarketId(toId);
|
|
1006
|
+
const givingBase = bestMarket(markets, {
|
|
1007
|
+
baseId: resolvedFrom,
|
|
1008
|
+
quoteId: resolvedTo,
|
|
1009
|
+
wantSide: "quote"
|
|
1010
|
+
});
|
|
775
1011
|
if (givingBase) return { market: givingBase, give: "base" };
|
|
776
1012
|
return {
|
|
777
|
-
market: bestMarket(markets, {
|
|
1013
|
+
market: bestMarket(markets, {
|
|
1014
|
+
baseId: resolvedTo,
|
|
1015
|
+
quoteId: resolvedFrom,
|
|
1016
|
+
wantSide: "base"
|
|
1017
|
+
}),
|
|
778
1018
|
give: "quote"
|
|
779
1019
|
};
|
|
780
1020
|
};
|
|
@@ -901,8 +1141,320 @@ var IndexedDbAssetSwapRepository = class {
|
|
|
901
1141
|
}
|
|
902
1142
|
};
|
|
903
1143
|
|
|
1144
|
+
// src/restore.ts
|
|
1145
|
+
import { base64, hex as hex3 } from "@scure/base";
|
|
1146
|
+
import {
|
|
1147
|
+
Extension,
|
|
1148
|
+
Transaction,
|
|
1149
|
+
scriptFromTapLeafScript
|
|
1150
|
+
} from "@arkade-os/sdk";
|
|
1151
|
+
var TXS_PER_REQUEST = 50;
|
|
1152
|
+
async function fetchParsedTxs(indexer, txids) {
|
|
1153
|
+
const parsedByTxid = /* @__PURE__ */ new Map();
|
|
1154
|
+
if (txids.length === 0) return parsedByTxid;
|
|
1155
|
+
const chunks = [];
|
|
1156
|
+
for (let i = 0; i < txids.length; i += TXS_PER_REQUEST) {
|
|
1157
|
+
chunks.push(txids.slice(i, i + TXS_PER_REQUEST));
|
|
1158
|
+
}
|
|
1159
|
+
const chunkResults = await Promise.allSettled(
|
|
1160
|
+
chunks.map(async (ids) => (await indexer.getVirtualTxs(ids)).txs)
|
|
1161
|
+
);
|
|
1162
|
+
for (const result of chunkResults) {
|
|
1163
|
+
if (result.status !== "fulfilled") continue;
|
|
1164
|
+
for (const psbt of result.value) {
|
|
1165
|
+
try {
|
|
1166
|
+
const parsed = Transaction.fromPSBT(base64.decode(psbt));
|
|
1167
|
+
parsedByTxid.set(parsed.id, parsed);
|
|
1168
|
+
} catch {
|
|
1169
|
+
}
|
|
1170
|
+
}
|
|
1171
|
+
}
|
|
1172
|
+
return parsedByTxid;
|
|
1173
|
+
}
|
|
1174
|
+
var unscannedSwapCandidates = (txs, existingIds, scanned) => txs.filter(
|
|
1175
|
+
(tx) => tx.type === "sent" && tx.redeemTxid && !existingIds.has(tx.redeemTxid) && !scanned.has(tx.redeemTxid)
|
|
1176
|
+
);
|
|
1177
|
+
function classifySpend(offer, serverPubkey, spendTx, deposit) {
|
|
1178
|
+
let leaves;
|
|
1179
|
+
try {
|
|
1180
|
+
const script = offerVtxoScript(offer, serverPubkey);
|
|
1181
|
+
if (hex3.encode(script.pkScript) !== hex3.encode(offer.swapPkScript)) return "indeterminate";
|
|
1182
|
+
leaves = {
|
|
1183
|
+
// both routes that hand the deposit back; `exit` is absent on an
|
|
1184
|
+
// offer that carries no exit closure, and drops out here
|
|
1185
|
+
returned: ["cancel", "exit"].map((name) => script.functionByName(name)?.leafScript).filter((leaf) => leaf !== void 0),
|
|
1186
|
+
fulfill: script.functionByName("fulfill")?.leafScript
|
|
1187
|
+
};
|
|
1188
|
+
} catch {
|
|
1189
|
+
return "indeterminate";
|
|
1190
|
+
}
|
|
1191
|
+
for (let i = 0; i < spendTx.inputsLength; i++) {
|
|
1192
|
+
const input = spendTx.getInput(i);
|
|
1193
|
+
if (!input.txid || input.index !== deposit.vout) continue;
|
|
1194
|
+
if (hex3.encode(input.txid) !== deposit.txid) continue;
|
|
1195
|
+
for (const leaf of input.tapLeafScript ?? []) {
|
|
1196
|
+
const spent = hex3.encode(scriptFromTapLeafScript(leaf));
|
|
1197
|
+
if (leaves.returned.some((back) => spent === hex3.encode(back))) return "cancelled";
|
|
1198
|
+
if (leaves.fulfill && spent === hex3.encode(leaves.fulfill)) return "fulfilled";
|
|
1199
|
+
}
|
|
1200
|
+
}
|
|
1201
|
+
return "indeterminate";
|
|
1202
|
+
}
|
|
1203
|
+
var spendTxidsOf = (vtxo) => [vtxo.spentBy, vtxo.arkTxId].filter((id) => Boolean(id));
|
|
1204
|
+
function classifyDepositSpend(offer, serverPubkey, spendTxs, deposit) {
|
|
1205
|
+
for (const tx of spendTxs) {
|
|
1206
|
+
const kind = classifySpend(offer, serverPubkey, tx, deposit);
|
|
1207
|
+
if (kind !== "indeterminate") return kind;
|
|
1208
|
+
}
|
|
1209
|
+
return "indeterminate";
|
|
1210
|
+
}
|
|
1211
|
+
async function restoreAssetSwaps(indexer, txs, existingIds, opts) {
|
|
1212
|
+
const { serverPubkey, scanned = /* @__PURE__ */ new Set(), reopen = [] } = opts;
|
|
1213
|
+
const reopened = [];
|
|
1214
|
+
for (const swap of reopen) {
|
|
1215
|
+
try {
|
|
1216
|
+
reopened.push({
|
|
1217
|
+
fundingTx: { type: "sent", redeemTxid: swap.fundingTxid },
|
|
1218
|
+
offer: decodeOffer(hex3.decode(swap.offerHex)),
|
|
1219
|
+
offerHex: swap.offerHex,
|
|
1220
|
+
existing: swap
|
|
1221
|
+
});
|
|
1222
|
+
} catch {
|
|
1223
|
+
}
|
|
1224
|
+
}
|
|
1225
|
+
const reopenedTxids = new Set(reopened.map((f) => f.fundingTx.redeemTxid));
|
|
1226
|
+
const candidates = unscannedSwapCandidates(txs, existingIds, scanned).filter(
|
|
1227
|
+
(tx) => !reopenedTxids.has(tx.redeemTxid)
|
|
1228
|
+
);
|
|
1229
|
+
if (candidates.length === 0 && reopened.length === 0) {
|
|
1230
|
+
return { restored: [], scannedTxids: [] };
|
|
1231
|
+
}
|
|
1232
|
+
const byTxid = new Map(candidates.map((tx) => [tx.redeemTxid, tx]));
|
|
1233
|
+
const parsedByTxid = await fetchParsedTxs(
|
|
1234
|
+
indexer,
|
|
1235
|
+
candidates.map((tx) => tx.redeemTxid)
|
|
1236
|
+
);
|
|
1237
|
+
const fetchedTxids = [];
|
|
1238
|
+
const found = [...reopened];
|
|
1239
|
+
for (const [txid, parsed] of parsedByTxid) {
|
|
1240
|
+
const fundingTx = byTxid.get(txid);
|
|
1241
|
+
if (!fundingTx) continue;
|
|
1242
|
+
fetchedTxids.push(txid);
|
|
1243
|
+
try {
|
|
1244
|
+
const packet = Extension.fromTx(parsed).getPacketByType(OFFER_PACKET_TYPE);
|
|
1245
|
+
if (!packet) continue;
|
|
1246
|
+
const payload = packet.serialize();
|
|
1247
|
+
found.push({
|
|
1248
|
+
fundingTx,
|
|
1249
|
+
offer: decodeOffer(payload),
|
|
1250
|
+
offerHex: hex3.encode(payload)
|
|
1251
|
+
});
|
|
1252
|
+
} catch {
|
|
1253
|
+
}
|
|
1254
|
+
}
|
|
1255
|
+
if (found.length === 0) return { restored: [], scannedTxids: fetchedTxids };
|
|
1256
|
+
const scripts = [...new Set(found.map((f) => hex3.encode(f.offer.swapPkScript)))];
|
|
1257
|
+
const { vtxos } = await indexer.getVtxos({ scripts });
|
|
1258
|
+
const vtxoByScriptAndTxid = new Map(vtxos.map((v) => [`${v.script}:${v.txid}`, v]));
|
|
1259
|
+
const txByAnyId = /* @__PURE__ */ new Map();
|
|
1260
|
+
for (const tx of txs) {
|
|
1261
|
+
for (const id of [tx.boardingTxid, tx.redeemTxid, tx.roundTxid]) {
|
|
1262
|
+
if (id) txByAnyId.set(id, tx);
|
|
1263
|
+
}
|
|
1264
|
+
}
|
|
1265
|
+
const spendTxids = /* @__PURE__ */ new Set();
|
|
1266
|
+
for (const { fundingTx, offer } of found) {
|
|
1267
|
+
const vtxo = vtxoByScriptAndTxid.get(
|
|
1268
|
+
`${hex3.encode(offer.swapPkScript)}:${fundingTx.redeemTxid}`
|
|
1269
|
+
);
|
|
1270
|
+
if (vtxo?.virtualStatus.state !== "spent") continue;
|
|
1271
|
+
for (const txid of spendTxidsOf(vtxo)) spendTxids.add(txid);
|
|
1272
|
+
}
|
|
1273
|
+
const spendTxByTxid = await fetchParsedTxs(indexer, [...spendTxids]);
|
|
1274
|
+
const restored = [];
|
|
1275
|
+
const unresolved = /* @__PURE__ */ new Set();
|
|
1276
|
+
for (const { fundingTx, offer, offerHex, existing } of found) {
|
|
1277
|
+
const swapPkScript = hex3.encode(offer.swapPkScript);
|
|
1278
|
+
const vtxo = vtxoByScriptAndTxid.get(`${swapPkScript}:${fundingTx.redeemTxid}`);
|
|
1279
|
+
if (!vtxo) {
|
|
1280
|
+
unresolved.add(fundingTx.redeemTxid);
|
|
1281
|
+
continue;
|
|
1282
|
+
}
|
|
1283
|
+
const depositRider = vtxo.assets?.length === 1 && vtxo.assets[0].amount > BigInt(0) ? vtxo.assets[0] : void 0;
|
|
1284
|
+
const fromAsset = offer.offerAsset?.toString() ?? depositRider?.assetId ?? BTC_ASSET_ID;
|
|
1285
|
+
const toAsset = offer.wantAsset?.toString() ?? BTC_ASSET_ID;
|
|
1286
|
+
const depositAmount = fromAsset === BTC_ASSET_ID ? BigInt(vtxo.value) : vtxo.assets?.find((a) => a.assetId === fromAsset)?.amount;
|
|
1287
|
+
if (depositAmount === void 0) {
|
|
1288
|
+
unresolved.add(fundingTx.redeemTxid);
|
|
1289
|
+
continue;
|
|
1290
|
+
}
|
|
1291
|
+
const fromAmount = depositAmount.toString();
|
|
1292
|
+
const state = vtxo.virtualStatus.state;
|
|
1293
|
+
const spentTxid = state === "spent" ? vtxo.arkTxId || vtxo.spentBy : void 0;
|
|
1294
|
+
let status = "pending";
|
|
1295
|
+
if (state === "swept") status = "recoverable";
|
|
1296
|
+
else if (state === "spent") {
|
|
1297
|
+
const spendTxs = spendTxidsOf(vtxo).map((id) => spendTxByTxid.get(id)).filter((tx) => tx !== void 0);
|
|
1298
|
+
const kind = classifyDepositSpend(offer, serverPubkey, spendTxs, {
|
|
1299
|
+
txid: vtxo.txid,
|
|
1300
|
+
vout: vtxo.vout
|
|
1301
|
+
});
|
|
1302
|
+
if (kind === "indeterminate") {
|
|
1303
|
+
unresolved.add(fundingTx.redeemTxid);
|
|
1304
|
+
continue;
|
|
1305
|
+
}
|
|
1306
|
+
status = kind;
|
|
1307
|
+
}
|
|
1308
|
+
const completion = status === "fulfilled" && spentTxid && txByAnyId.get(spentTxid)?.createdAt ? { completedAt: txByAnyId.get(spentTxid).createdAt * 1e3 } : {};
|
|
1309
|
+
if (existing) {
|
|
1310
|
+
if (status === "pending") continue;
|
|
1311
|
+
restored.push({ ...existing, status, spentTxid, ...completion });
|
|
1312
|
+
continue;
|
|
1313
|
+
}
|
|
1314
|
+
restored.push({
|
|
1315
|
+
id: fundingTx.redeemTxid,
|
|
1316
|
+
fromAsset,
|
|
1317
|
+
toAsset,
|
|
1318
|
+
fromAmount,
|
|
1319
|
+
toAmount: offer.wantAmount.toString(),
|
|
1320
|
+
// ponytail(arkade-os/ts-sdk#680): empty address makes cancel fall back
|
|
1321
|
+
// to the current server key; store the funded address if server-key
|
|
1322
|
+
// rotations become real (cancelOffer now at least diagnoses the
|
|
1323
|
+
// mismatch instead of reporting a missing VTXO)
|
|
1324
|
+
swapAddress: "",
|
|
1325
|
+
swapPkScript,
|
|
1326
|
+
offerHex,
|
|
1327
|
+
fundingTxid: fundingTx.redeemTxid,
|
|
1328
|
+
spentTxid,
|
|
1329
|
+
status,
|
|
1330
|
+
createdAt: fundingTx.createdAt ? fundingTx.createdAt * 1e3 : vtxo.createdAt.getTime(),
|
|
1331
|
+
...completion
|
|
1332
|
+
});
|
|
1333
|
+
}
|
|
1334
|
+
return { restored, scannedTxids: fetchedTxids.filter((id) => !unresolved.has(id)) };
|
|
1335
|
+
}
|
|
1336
|
+
|
|
1337
|
+
// src/restoreRepository.ts
|
|
1338
|
+
var isOpen = (swap) => swap.status === "pending" || swap.status === "cancelling";
|
|
1339
|
+
var isOfferSwap = (swap) => typeof swap.offerHex === "string";
|
|
1340
|
+
var restoreCoverage = async (wallet, arkServerUrl, swaps) => {
|
|
1341
|
+
const offers = swaps.filter(isOfferSwap);
|
|
1342
|
+
if (offers.length > 0) await restoreOfferCoverage(wallet, arkServerUrl, offers);
|
|
1343
|
+
};
|
|
1344
|
+
var aborted = (swaps) => ({
|
|
1345
|
+
swaps,
|
|
1346
|
+
changes: [],
|
|
1347
|
+
scannedTxids: [],
|
|
1348
|
+
aborted: true
|
|
1349
|
+
});
|
|
1350
|
+
async function restoreAssetSwapRepository(opts) {
|
|
1351
|
+
const { wallet, arkServerUrl, indexer, repository, txs, serverPubkey, prepareNew, signal } = opts;
|
|
1352
|
+
const [existing, scanned] = await Promise.all([
|
|
1353
|
+
getAssetSwapsOrThrow(repository),
|
|
1354
|
+
repository.getScannedTxids()
|
|
1355
|
+
]);
|
|
1356
|
+
if (signal?.aborted) return aborted(existing);
|
|
1357
|
+
let scan;
|
|
1358
|
+
try {
|
|
1359
|
+
scan = await restoreAssetSwaps(indexer, txs, new Set(existing.map((swap) => swap.id)), {
|
|
1360
|
+
serverPubkey,
|
|
1361
|
+
scanned,
|
|
1362
|
+
reopen: existing.filter((swap) => isOpen(swap) && isOfferSwap(swap))
|
|
1363
|
+
});
|
|
1364
|
+
} catch (scanError) {
|
|
1365
|
+
if (!signal?.aborted) {
|
|
1366
|
+
try {
|
|
1367
|
+
await restoreCoverage(wallet, arkServerUrl, existing);
|
|
1368
|
+
} catch (coverageError2) {
|
|
1369
|
+
throw new AggregateError(
|
|
1370
|
+
[scanError, coverageError2],
|
|
1371
|
+
"asset-swap scan and covenant coverage restore both failed"
|
|
1372
|
+
);
|
|
1373
|
+
}
|
|
1374
|
+
}
|
|
1375
|
+
throw scanError;
|
|
1376
|
+
}
|
|
1377
|
+
if (signal?.aborted) return aborted(existing);
|
|
1378
|
+
const before = new Map(existing.map((swap) => [swap.id, swap]));
|
|
1379
|
+
const changes = [];
|
|
1380
|
+
const cancelledAfterCommit = async (scannedTxids = []) => ({
|
|
1381
|
+
swaps: await getAssetSwapsOrThrow(repository),
|
|
1382
|
+
changes,
|
|
1383
|
+
scannedTxids,
|
|
1384
|
+
aborted: true
|
|
1385
|
+
});
|
|
1386
|
+
for (const restored of scan.restored) {
|
|
1387
|
+
if (signal?.aborted) return cancelledAfterCommit();
|
|
1388
|
+
const previous = before.get(restored.id);
|
|
1389
|
+
const current = previous !== void 0 || !prepareNew ? restored : await prepareNew(restored);
|
|
1390
|
+
if (current.id !== restored.id) {
|
|
1391
|
+
throw new Error("prepareNew must not change an asset swap id");
|
|
1392
|
+
}
|
|
1393
|
+
if (signal?.aborted) return cancelledAfterCommit();
|
|
1394
|
+
await repository.saveSwap(current);
|
|
1395
|
+
changes.push(previous ? { previous, current } : { current });
|
|
1396
|
+
if (signal?.aborted) return cancelledAfterCommit();
|
|
1397
|
+
}
|
|
1398
|
+
if (signal?.aborted) return cancelledAfterCommit();
|
|
1399
|
+
if (scan.scannedTxids.length > 0) {
|
|
1400
|
+
await repository.markTxidsScanned(scan.scannedTxids);
|
|
1401
|
+
}
|
|
1402
|
+
const swaps = await getAssetSwapsOrThrow(repository);
|
|
1403
|
+
if (signal?.aborted) return cancelledAfterCommit(scan.scannedTxids);
|
|
1404
|
+
let coverageError;
|
|
1405
|
+
try {
|
|
1406
|
+
await restoreCoverage(wallet, arkServerUrl, swaps);
|
|
1407
|
+
} catch (error) {
|
|
1408
|
+
coverageError = error;
|
|
1409
|
+
}
|
|
1410
|
+
return {
|
|
1411
|
+
swaps,
|
|
1412
|
+
changes,
|
|
1413
|
+
scannedTxids: scan.scannedTxids,
|
|
1414
|
+
aborted: false,
|
|
1415
|
+
...coverageError === void 0 ? {} : { coverageError }
|
|
1416
|
+
};
|
|
1417
|
+
}
|
|
1418
|
+
|
|
1419
|
+
// src/registerRestore.ts
|
|
1420
|
+
import { registerWalletRestoreHook } from "@arkade-os/sdk";
|
|
1421
|
+
var toRestoreTx = (tx) => ({
|
|
1422
|
+
type: tx.type.toLowerCase(),
|
|
1423
|
+
redeemTxid: tx.key.arkTxid,
|
|
1424
|
+
boardingTxid: tx.key.boardingTxid,
|
|
1425
|
+
roundTxid: tx.key.commitmentTxid,
|
|
1426
|
+
createdAt: Math.floor(tx.createdAt / 1e3)
|
|
1427
|
+
});
|
|
1428
|
+
function registerAssetSwapRestore(wallet, options) {
|
|
1429
|
+
return registerWalletRestoreHook(wallet, {
|
|
1430
|
+
id: "arkade-os:asset-swap",
|
|
1431
|
+
restore: async (restoredWallet) => {
|
|
1432
|
+
const dependencies = restoredWallet;
|
|
1433
|
+
const indexer = options.indexer ?? dependencies.indexerProvider;
|
|
1434
|
+
const serverPubkey = options.serverPubkey ?? dependencies.arkServerPublicKey;
|
|
1435
|
+
if (!indexer) {
|
|
1436
|
+
throw new Error("asset-swap restore requires an indexer");
|
|
1437
|
+
}
|
|
1438
|
+
if (!serverPubkey) {
|
|
1439
|
+
throw new Error("asset-swap restore requires an Ark server public key");
|
|
1440
|
+
}
|
|
1441
|
+
const history = await restoredWallet.getTransactionHistory();
|
|
1442
|
+
const result = await restoreAssetSwapRepository({
|
|
1443
|
+
wallet: restoredWallet,
|
|
1444
|
+
arkServerUrl: options.arkServerUrl,
|
|
1445
|
+
indexer,
|
|
1446
|
+
repository: options.repository,
|
|
1447
|
+
txs: history.map(toRestoreTx),
|
|
1448
|
+
serverPubkey,
|
|
1449
|
+
prepareNew: options.prepareNew
|
|
1450
|
+
});
|
|
1451
|
+
await options.onResult?.(result);
|
|
1452
|
+
}
|
|
1453
|
+
});
|
|
1454
|
+
}
|
|
1455
|
+
|
|
904
1456
|
// src/rfqCorridors.ts
|
|
905
|
-
import { hex as
|
|
1457
|
+
import { hex as hex4 } from "@scure/base";
|
|
906
1458
|
|
|
907
1459
|
// src/rfqCorridor.ts
|
|
908
1460
|
var RfqCorridorRegistry = class {
|
|
@@ -1058,13 +1610,13 @@ var LightningReceiveCorridor = {
|
|
|
1058
1610
|
};
|
|
1059
1611
|
function onchainSendProfile(result) {
|
|
1060
1612
|
return {
|
|
1061
|
-
claimKey:
|
|
1062
|
-
refundKey:
|
|
1613
|
+
claimKey: hex4.encode(result.htlcParams.claimKey),
|
|
1614
|
+
refundKey: hex4.encode(result.htlcParams.refundKey),
|
|
1063
1615
|
htlcLocktime: result.htlcParams.refundLocktime,
|
|
1064
1616
|
network: result.l1Network,
|
|
1065
1617
|
htlcAddress: result.htlc.address,
|
|
1066
1618
|
minConfirmations: result.minConfirmations,
|
|
1067
|
-
payoutPkScript:
|
|
1619
|
+
payoutPkScript: hex4.encode(result.payoutPkScript)
|
|
1068
1620
|
};
|
|
1069
1621
|
}
|
|
1070
1622
|
var OnchainSendCorridor = {
|
|
@@ -1098,8 +1650,8 @@ var OnchainSendCorridor = {
|
|
|
1098
1650
|
// unlocks both legs, but only one of the two hashes of it
|
|
1099
1651
|
// is recoverable here.
|
|
1100
1652
|
paymentHash,
|
|
1101
|
-
claimKey:
|
|
1102
|
-
refundKey:
|
|
1653
|
+
claimKey: hex4.decode(profile.claimKey),
|
|
1654
|
+
refundKey: hex4.decode(profile.refundKey),
|
|
1103
1655
|
refundLocktime: profile.htlcLocktime
|
|
1104
1656
|
},
|
|
1105
1657
|
profile.network
|
|
@@ -1115,7 +1667,7 @@ var OnchainSendCorridor = {
|
|
|
1115
1667
|
minConfirmations: profile.minConfirmations,
|
|
1116
1668
|
// Optional here, required at the write: throwing on an older
|
|
1117
1669
|
// record would strand the refund it is still owed.
|
|
1118
|
-
...profile.payoutPkScript ? { payoutPkScript:
|
|
1670
|
+
...profile.payoutPkScript ? { payoutPkScript: hex4.decode(profile.payoutPkScript) } : {},
|
|
1119
1671
|
...profile.funding ? { funding: profile.funding } : {},
|
|
1120
1672
|
...profile.claimTxid ? { claimTxid: profile.claimTxid } : {}
|
|
1121
1673
|
};
|
|
@@ -1134,7 +1686,7 @@ rfqCorridorHandlers.register(OnchainSendCorridor);
|
|
|
1134
1686
|
|
|
1135
1687
|
// src/rfqRecord.ts
|
|
1136
1688
|
import { ArkAddress as ArkAddress2, VHTLCV2ContractHandler } from "@arkade-os/sdk";
|
|
1137
|
-
import { hex as
|
|
1689
|
+
import { hex as hex5 } from "@scure/base";
|
|
1138
1690
|
|
|
1139
1691
|
// src/rfqSwapState.ts
|
|
1140
1692
|
var RFQ_SWAP_TERMINAL_STATES = ["settled", "refunded", "failed"];
|
|
@@ -1158,8 +1710,8 @@ function assertSameSwap(origin, swap) {
|
|
|
1158
1710
|
`rfq swap record is a ${origin.kind} origin paired with a ${swap.kind} swap`
|
|
1159
1711
|
);
|
|
1160
1712
|
}
|
|
1161
|
-
const funded =
|
|
1162
|
-
const watched =
|
|
1713
|
+
const funded = hex5.encode(ArkAddress2.decode(origin.lockupAddress).pkScript);
|
|
1714
|
+
const watched = hex5.encode(swap.lockupPkScript);
|
|
1163
1715
|
if (funded !== watched) {
|
|
1164
1716
|
throw new Error(
|
|
1165
1717
|
`rfq swap record's lockup address holds ${funded}, but the swap watches ${watched} \u2014 these are not the same swap`
|
|
@@ -1205,9 +1757,9 @@ function rfqSwapOriginOf(record) {
|
|
|
1205
1757
|
function lockupScript(params, lockupAddress) {
|
|
1206
1758
|
const script = VHTLCV2ContractHandler.createScript(params);
|
|
1207
1759
|
const funded = ArkAddress2.decode(lockupAddress).pkScript;
|
|
1208
|
-
if (
|
|
1760
|
+
if (hex5.encode(funded) !== hex5.encode(script.pkScript)) {
|
|
1209
1761
|
throw new Error(
|
|
1210
|
-
`rfq swap covenant params derive ${
|
|
1762
|
+
`rfq swap covenant params derive ${hex5.encode(script.pkScript)}, but the record's lockup address holds ${hex5.encode(funded)} \u2014 these params are not this swap's`
|
|
1211
1763
|
);
|
|
1212
1764
|
}
|
|
1213
1765
|
return script;
|
|
@@ -1241,178 +1793,6 @@ function shouldRetainRfqSwap(record, now) {
|
|
|
1241
1793
|
return now - record.updatedAt < RFQ_SWAP_RETENTION_SECONDS;
|
|
1242
1794
|
}
|
|
1243
1795
|
|
|
1244
|
-
// src/restore.ts
|
|
1245
|
-
import { base64, hex as hex5 } from "@scure/base";
|
|
1246
|
-
import {
|
|
1247
|
-
Extension,
|
|
1248
|
-
Transaction,
|
|
1249
|
-
scriptFromTapLeafScript
|
|
1250
|
-
} from "@arkade-os/sdk";
|
|
1251
|
-
var TXS_PER_REQUEST = 50;
|
|
1252
|
-
async function fetchParsedTxs(indexer, txids) {
|
|
1253
|
-
const parsedByTxid = /* @__PURE__ */ new Map();
|
|
1254
|
-
if (txids.length === 0) return parsedByTxid;
|
|
1255
|
-
const chunks = [];
|
|
1256
|
-
for (let i = 0; i < txids.length; i += TXS_PER_REQUEST) {
|
|
1257
|
-
chunks.push(txids.slice(i, i + TXS_PER_REQUEST));
|
|
1258
|
-
}
|
|
1259
|
-
const chunkResults = await Promise.allSettled(
|
|
1260
|
-
chunks.map(async (ids) => (await indexer.getVirtualTxs(ids)).txs)
|
|
1261
|
-
);
|
|
1262
|
-
for (const result of chunkResults) {
|
|
1263
|
-
if (result.status !== "fulfilled") continue;
|
|
1264
|
-
for (const psbt of result.value) {
|
|
1265
|
-
try {
|
|
1266
|
-
const parsed = Transaction.fromPSBT(base64.decode(psbt));
|
|
1267
|
-
parsedByTxid.set(parsed.id, parsed);
|
|
1268
|
-
} catch {
|
|
1269
|
-
}
|
|
1270
|
-
}
|
|
1271
|
-
}
|
|
1272
|
-
return parsedByTxid;
|
|
1273
|
-
}
|
|
1274
|
-
var unscannedSwapCandidates = (txs, existingIds, scanned) => txs.filter(
|
|
1275
|
-
(tx) => tx.type === "sent" && tx.redeemTxid && !existingIds.has(tx.redeemTxid) && !scanned.has(tx.redeemTxid)
|
|
1276
|
-
);
|
|
1277
|
-
function classifySpend(offer, serverPubkey, spendTx, deposit) {
|
|
1278
|
-
let leaves;
|
|
1279
|
-
try {
|
|
1280
|
-
const script = offerVtxoScript(offer, serverPubkey);
|
|
1281
|
-
if (hex5.encode(script.pkScript) !== hex5.encode(offer.swapPkScript)) return "indeterminate";
|
|
1282
|
-
leaves = {
|
|
1283
|
-
// both routes that hand the deposit back; `exit` is absent on an
|
|
1284
|
-
// offer that carries no exit closure, and drops out here
|
|
1285
|
-
returned: ["cancel", "exit"].map((name) => script.functionByName(name)?.leafScript).filter((leaf) => leaf !== void 0),
|
|
1286
|
-
fulfill: script.functionByName("fulfill")?.leafScript
|
|
1287
|
-
};
|
|
1288
|
-
} catch {
|
|
1289
|
-
return "indeterminate";
|
|
1290
|
-
}
|
|
1291
|
-
for (let i = 0; i < spendTx.inputsLength; i++) {
|
|
1292
|
-
const input = spendTx.getInput(i);
|
|
1293
|
-
if (!input.txid || input.index !== deposit.vout) continue;
|
|
1294
|
-
if (hex5.encode(input.txid) !== deposit.txid) continue;
|
|
1295
|
-
for (const leaf of input.tapLeafScript ?? []) {
|
|
1296
|
-
const spent = hex5.encode(scriptFromTapLeafScript(leaf));
|
|
1297
|
-
if (leaves.returned.some((back) => spent === hex5.encode(back))) return "cancelled";
|
|
1298
|
-
if (leaves.fulfill && spent === hex5.encode(leaves.fulfill)) return "fulfilled";
|
|
1299
|
-
}
|
|
1300
|
-
}
|
|
1301
|
-
return "indeterminate";
|
|
1302
|
-
}
|
|
1303
|
-
var spendTxidsOf = (vtxo) => [vtxo.spentBy, vtxo.arkTxId].filter((id) => Boolean(id));
|
|
1304
|
-
function classifyDepositSpend(offer, serverPubkey, spendTxs, deposit) {
|
|
1305
|
-
for (const tx of spendTxs) {
|
|
1306
|
-
const kind = classifySpend(offer, serverPubkey, tx, deposit);
|
|
1307
|
-
if (kind !== "indeterminate") return kind;
|
|
1308
|
-
}
|
|
1309
|
-
return "indeterminate";
|
|
1310
|
-
}
|
|
1311
|
-
async function restoreAssetSwaps(indexer, txs, existingIds, opts) {
|
|
1312
|
-
const { serverPubkey, scanned = /* @__PURE__ */ new Set() } = opts;
|
|
1313
|
-
const candidates = unscannedSwapCandidates(txs, existingIds, scanned);
|
|
1314
|
-
if (candidates.length === 0) return { restored: [], scannedTxids: [] };
|
|
1315
|
-
const byTxid = new Map(candidates.map((tx) => [tx.redeemTxid, tx]));
|
|
1316
|
-
const parsedByTxid = await fetchParsedTxs(
|
|
1317
|
-
indexer,
|
|
1318
|
-
candidates.map((tx) => tx.redeemTxid)
|
|
1319
|
-
);
|
|
1320
|
-
const fetchedTxids = [];
|
|
1321
|
-
const found = [];
|
|
1322
|
-
for (const [txid, parsed] of parsedByTxid) {
|
|
1323
|
-
const fundingTx = byTxid.get(txid);
|
|
1324
|
-
if (!fundingTx) continue;
|
|
1325
|
-
fetchedTxids.push(txid);
|
|
1326
|
-
try {
|
|
1327
|
-
const packet = Extension.fromTx(parsed).getPacketByType(OFFER_PACKET_TYPE);
|
|
1328
|
-
if (!packet) continue;
|
|
1329
|
-
const payload = packet.serialize();
|
|
1330
|
-
found.push({
|
|
1331
|
-
fundingTx,
|
|
1332
|
-
offer: decodeOffer(payload),
|
|
1333
|
-
offerHex: hex5.encode(payload)
|
|
1334
|
-
});
|
|
1335
|
-
} catch {
|
|
1336
|
-
}
|
|
1337
|
-
}
|
|
1338
|
-
if (found.length === 0) return { restored: [], scannedTxids: fetchedTxids };
|
|
1339
|
-
const scripts = [...new Set(found.map((f) => hex5.encode(f.offer.swapPkScript)))];
|
|
1340
|
-
const { vtxos } = await indexer.getVtxos({ scripts });
|
|
1341
|
-
const vtxoByScriptAndTxid = new Map(vtxos.map((v) => [`${v.script}:${v.txid}`, v]));
|
|
1342
|
-
const txByAnyId = /* @__PURE__ */ new Map();
|
|
1343
|
-
for (const tx of txs) {
|
|
1344
|
-
for (const id of [tx.boardingTxid, tx.redeemTxid, tx.roundTxid]) {
|
|
1345
|
-
if (id) txByAnyId.set(id, tx);
|
|
1346
|
-
}
|
|
1347
|
-
}
|
|
1348
|
-
const spendTxids = /* @__PURE__ */ new Set();
|
|
1349
|
-
for (const { fundingTx, offer } of found) {
|
|
1350
|
-
const vtxo = vtxoByScriptAndTxid.get(
|
|
1351
|
-
`${hex5.encode(offer.swapPkScript)}:${fundingTx.redeemTxid}`
|
|
1352
|
-
);
|
|
1353
|
-
if (vtxo?.virtualStatus.state !== "spent") continue;
|
|
1354
|
-
for (const txid of spendTxidsOf(vtxo)) spendTxids.add(txid);
|
|
1355
|
-
}
|
|
1356
|
-
const spendTxByTxid = await fetchParsedTxs(indexer, [...spendTxids]);
|
|
1357
|
-
const restored = [];
|
|
1358
|
-
const unresolved = /* @__PURE__ */ new Set();
|
|
1359
|
-
for (const { fundingTx, offer, offerHex } of found) {
|
|
1360
|
-
const swapPkScript = hex5.encode(offer.swapPkScript);
|
|
1361
|
-
const vtxo = vtxoByScriptAndTxid.get(`${swapPkScript}:${fundingTx.redeemTxid}`);
|
|
1362
|
-
if (!vtxo) {
|
|
1363
|
-
unresolved.add(fundingTx.redeemTxid);
|
|
1364
|
-
continue;
|
|
1365
|
-
}
|
|
1366
|
-
const depositRider = vtxo.assets?.length === 1 && vtxo.assets[0].amount > BigInt(0) ? vtxo.assets[0] : void 0;
|
|
1367
|
-
const fromAsset = offer.offerAsset?.toString() ?? depositRider?.assetId ?? BTC_ASSET_ID;
|
|
1368
|
-
const toAsset = offer.wantAsset?.toString() ?? BTC_ASSET_ID;
|
|
1369
|
-
const depositAmount = fromAsset === BTC_ASSET_ID ? BigInt(vtxo.value) : vtxo.assets?.find((a) => a.assetId === fromAsset)?.amount;
|
|
1370
|
-
if (depositAmount === void 0) {
|
|
1371
|
-
unresolved.add(fundingTx.redeemTxid);
|
|
1372
|
-
continue;
|
|
1373
|
-
}
|
|
1374
|
-
const fromAmount = depositAmount.toString();
|
|
1375
|
-
const state = vtxo.virtualStatus.state;
|
|
1376
|
-
const spentTxid = state === "spent" ? vtxo.arkTxId || vtxo.spentBy : void 0;
|
|
1377
|
-
let status = "pending";
|
|
1378
|
-
if (state === "swept") status = "recoverable";
|
|
1379
|
-
else if (state === "spent") {
|
|
1380
|
-
const spendTxs = spendTxidsOf(vtxo).map((id) => spendTxByTxid.get(id)).filter((tx) => tx !== void 0);
|
|
1381
|
-
const kind = classifyDepositSpend(offer, serverPubkey, spendTxs, {
|
|
1382
|
-
txid: vtxo.txid,
|
|
1383
|
-
vout: vtxo.vout
|
|
1384
|
-
});
|
|
1385
|
-
if (kind === "indeterminate") {
|
|
1386
|
-
unresolved.add(fundingTx.redeemTxid);
|
|
1387
|
-
continue;
|
|
1388
|
-
}
|
|
1389
|
-
status = kind;
|
|
1390
|
-
}
|
|
1391
|
-
restored.push({
|
|
1392
|
-
id: fundingTx.redeemTxid,
|
|
1393
|
-
fromAsset,
|
|
1394
|
-
toAsset,
|
|
1395
|
-
fromAmount,
|
|
1396
|
-
toAmount: offer.wantAmount.toString(),
|
|
1397
|
-
// ponytail(arkade-os/ts-sdk#680): empty address makes cancel fall back
|
|
1398
|
-
// to the current server key; store the funded address if server-key
|
|
1399
|
-
// rotations become real (cancelOffer now at least diagnoses the
|
|
1400
|
-
// mismatch instead of reporting a missing VTXO)
|
|
1401
|
-
swapAddress: "",
|
|
1402
|
-
swapPkScript,
|
|
1403
|
-
offerHex,
|
|
1404
|
-
fundingTxid: fundingTx.redeemTxid,
|
|
1405
|
-
spentTxid,
|
|
1406
|
-
status,
|
|
1407
|
-
createdAt: fundingTx.createdAt ? fundingTx.createdAt * 1e3 : vtxo.createdAt.getTime(),
|
|
1408
|
-
// the completion time is the caller's record of the spend, if it
|
|
1409
|
-
// has one — the psbt that classified it carries no timestamp
|
|
1410
|
-
...status === "fulfilled" && spentTxid && txByAnyId.get(spentTxid)?.createdAt ? { completedAt: txByAnyId.get(spentTxid).createdAt * 1e3 } : {}
|
|
1411
|
-
});
|
|
1412
|
-
}
|
|
1413
|
-
return { restored, scannedTxids: fetchedTxids.filter((id) => !unresolved.has(id)) };
|
|
1414
|
-
}
|
|
1415
|
-
|
|
1416
1796
|
// src/watch.ts
|
|
1417
1797
|
import { base64 as base642, hex as hex6 } from "@scure/base";
|
|
1418
1798
|
import {
|
|
@@ -1463,34 +1843,54 @@ async function watchOfferSwaps({
|
|
|
1463
1843
|
return "indeterminate";
|
|
1464
1844
|
}
|
|
1465
1845
|
};
|
|
1466
|
-
const
|
|
1467
|
-
|
|
1468
|
-
for (const vtxo of
|
|
1846
|
+
const resolveSpends = async (contractScript, vtxos, { at, known } = {}) => {
|
|
1847
|
+
let swaps = known ?? await getAssetSwaps(repository);
|
|
1848
|
+
for (const vtxo of vtxos) {
|
|
1469
1849
|
const spentTxid = vtxo.arkTxId || vtxo.spentBy;
|
|
1470
1850
|
if (!spentTxid) continue;
|
|
1471
|
-
const swap =
|
|
1472
|
-
(s) => s.fundingTxid === vtxo.txid && s.swapPkScript ===
|
|
1851
|
+
const swap = swaps.find(
|
|
1852
|
+
(s) => s.fundingTxid === vtxo.txid && s.swapPkScript === contractScript
|
|
1473
1853
|
);
|
|
1474
1854
|
if (!swap) continue;
|
|
1475
1855
|
const kind = await classify(swap, vtxo, spentTxid);
|
|
1476
|
-
const changes = spendUpdate(swap, { txid: spentTxid, kind, at
|
|
1856
|
+
const changes = spendUpdate(swap, { txid: spentTxid, kind, at });
|
|
1477
1857
|
if (!changes) continue;
|
|
1478
|
-
const { persisted, swaps } = await updateAssetSwapBestEffort(
|
|
1858
|
+
const { persisted, swaps: written } = await updateAssetSwapBestEffort(
|
|
1479
1859
|
repository,
|
|
1480
1860
|
swap.id,
|
|
1481
1861
|
changes
|
|
1482
1862
|
);
|
|
1483
1863
|
if (!persisted) continue;
|
|
1864
|
+
swaps = written;
|
|
1484
1865
|
onUpdate?.({ ...swap, ...changes });
|
|
1485
1866
|
if (changes.status && RETIRABLE.includes(changes.status)) {
|
|
1486
|
-
await retireOfferContract(manager, swaps,
|
|
1867
|
+
await retireOfferContract(manager, swaps, contractScript);
|
|
1487
1868
|
}
|
|
1488
1869
|
}
|
|
1489
1870
|
};
|
|
1871
|
+
const handleSpend = async (event) => {
|
|
1872
|
+
if (event.contract.metadata?.kind !== OFFER_CONTRACT_KIND) return;
|
|
1873
|
+
await resolveSpends(event.contractScript, event.vtxos, { at: event.timestamp });
|
|
1874
|
+
};
|
|
1875
|
+
const resolveOpenSwaps = async () => {
|
|
1876
|
+
const swaps = await getAssetSwaps(repository);
|
|
1877
|
+
const open = swaps.filter((s) => !TERMINAL.includes(s.status));
|
|
1878
|
+
if (open.length === 0) return;
|
|
1879
|
+
const script = [...new Set(open.map((s) => s.swapPkScript))];
|
|
1880
|
+
for (const { contract, vtxos } of await manager.getContractsWithVtxos({ script })) {
|
|
1881
|
+
if (contract.metadata?.kind !== OFFER_CONTRACT_KIND) continue;
|
|
1882
|
+
await resolveSpends(
|
|
1883
|
+
contract.script,
|
|
1884
|
+
vtxos.filter((v) => v.isSpent),
|
|
1885
|
+
{ known: swaps }
|
|
1886
|
+
);
|
|
1887
|
+
}
|
|
1888
|
+
};
|
|
1490
1889
|
const unsubscribe = manager.onContractEvent((event) => {
|
|
1491
1890
|
if (!isContractVtxoEvent(event) || event.type !== "vtxo_spent") return;
|
|
1492
1891
|
enqueue(() => handleSpend(event));
|
|
1493
1892
|
});
|
|
1893
|
+
enqueue(resolveOpenSwaps);
|
|
1494
1894
|
return {
|
|
1495
1895
|
stop: unsubscribe,
|
|
1496
1896
|
idle: () => queue
|
|
@@ -1644,6 +2044,7 @@ function solverOnchainRail(deps) {
|
|
|
1644
2044
|
amount,
|
|
1645
2045
|
fee: swap.fundAmount - amount,
|
|
1646
2046
|
total: swap.fundAmount,
|
|
2047
|
+
validUntil: swap.quote.valid_until,
|
|
1647
2048
|
meta: {
|
|
1648
2049
|
rfqId: swap.rfqId,
|
|
1649
2050
|
validUntil: swap.quote.valid_until,
|
|
@@ -1754,6 +2155,9 @@ function solverLightningRail(deps) {
|
|
|
1754
2155
|
amount: facts.amountSats,
|
|
1755
2156
|
fee: swap.fundAmount - facts.amountSats,
|
|
1756
2157
|
total: swap.fundAmount,
|
|
2158
|
+
// The invoice can lapse before the quote does; the earlier bound
|
|
2159
|
+
// is the one a holder must observe.
|
|
2160
|
+
validUntil: Math.min(swap.quote.valid_until, facts.expiresAt),
|
|
1757
2161
|
meta: {
|
|
1758
2162
|
rfqId: swap.rfqId,
|
|
1759
2163
|
validUntil: swap.quote.valid_until,
|
|
@@ -1811,6 +2215,7 @@ var parseAssetId = (assetId) => {
|
|
|
1811
2215
|
return void 0;
|
|
1812
2216
|
}
|
|
1813
2217
|
};
|
|
2218
|
+
var marketLabel = (market) => `${market.base_asset.ticker || market.base_asset.id}/${market.quote_asset.ticker || market.quote_asset.id}`;
|
|
1814
2219
|
function crossAssetRail(deps) {
|
|
1815
2220
|
const planFor = async (asset2) => {
|
|
1816
2221
|
const markets = await deps.discover();
|
|
@@ -1873,7 +2278,7 @@ function crossAssetRail(deps) {
|
|
|
1873
2278
|
spent: { assetId: BTC_ASSET_ID, amount: plan.deposit.atomic }
|
|
1874
2279
|
},
|
|
1875
2280
|
meta: {
|
|
1876
|
-
market: plan.market
|
|
2281
|
+
market: marketLabel(plan.market),
|
|
1877
2282
|
priceDisplay: plan.priceDisplay,
|
|
1878
2283
|
give: plan.give
|
|
1879
2284
|
},
|
|
@@ -3597,6 +4002,7 @@ async function lockupTxids(indexer, record, wantFunding) {
|
|
|
3597
4002
|
export {
|
|
3598
4003
|
ARKADE_ASSET,
|
|
3599
4004
|
ARKADE_BTC,
|
|
4005
|
+
ASSET_CARRIER_SATS,
|
|
3600
4006
|
AddressMismatch,
|
|
3601
4007
|
BTC_ASSET_ID,
|
|
3602
4008
|
CROSS_ASSET_RAIL,
|
|
@@ -3611,6 +4017,7 @@ export {
|
|
|
3611
4017
|
LockupContractMissing,
|
|
3612
4018
|
LockupNeedsRecoveryError,
|
|
3613
4019
|
LockupRegistrationFailed,
|
|
4020
|
+
MARKET_CORRIDORS,
|
|
3614
4021
|
MAX_MIN_CONFIRMATIONS,
|
|
3615
4022
|
MIN_CLAIM_WINDOW_SECONDS,
|
|
3616
4023
|
MIN_HEADROOM_SECONDS,
|
|
@@ -3625,6 +4032,7 @@ export {
|
|
|
3625
4032
|
PreimageNotRecoverableError,
|
|
3626
4033
|
QUOTE_OPTIONS,
|
|
3627
4034
|
REFUND_MTP_LAG_SECONDS,
|
|
4035
|
+
RFQ_REFUSAL_ERROR_CODES,
|
|
3628
4036
|
RFQ_RESOLVED_STATES,
|
|
3629
4037
|
RFQ_SWAP_RETENTION_SECONDS,
|
|
3630
4038
|
RFQ_SWAP_TERMINAL_STATES,
|
|
@@ -3668,11 +4076,14 @@ export {
|
|
|
3668
4076
|
discoverMarkets,
|
|
3669
4077
|
encodeOffer,
|
|
3670
4078
|
extractPreimage,
|
|
4079
|
+
fillOffer,
|
|
3671
4080
|
findLockupVtxos,
|
|
3672
4081
|
findMarket,
|
|
3673
4082
|
getAssetSwaps,
|
|
3674
4083
|
getAssetSwapsOrThrow,
|
|
3675
4084
|
httpTransport,
|
|
4085
|
+
isRfqMarket,
|
|
4086
|
+
isRfqRefusalErrorCode,
|
|
3676
4087
|
isRfqSwapTerminal,
|
|
3677
4088
|
isRfqTerminal,
|
|
3678
4089
|
l1ScriptForAddress,
|
|
@@ -3681,6 +4092,9 @@ export {
|
|
|
3681
4092
|
lightningSendVtxoScript,
|
|
3682
4093
|
lockupContractParams,
|
|
3683
4094
|
makeCachedFeedFetch,
|
|
4095
|
+
marketAssetId,
|
|
4096
|
+
marketCorridor,
|
|
4097
|
+
marketPairLabel,
|
|
3684
4098
|
newPreimage,
|
|
3685
4099
|
newRfqId,
|
|
3686
4100
|
nextOnchainAction,
|
|
@@ -3698,13 +4112,16 @@ export {
|
|
|
3698
4112
|
rebuildRfqSwap,
|
|
3699
4113
|
receiveVtxoScript,
|
|
3700
4114
|
refundIfUnresolved,
|
|
4115
|
+
registerAssetSwapRestore,
|
|
3701
4116
|
registerLockupContract,
|
|
3702
4117
|
relayTransport,
|
|
3703
4118
|
requestLightningReceive,
|
|
3704
4119
|
requestLightningSend,
|
|
3705
4120
|
requestOnchainReceive,
|
|
3706
4121
|
requestOnchainSend,
|
|
4122
|
+
restoreAssetSwapRepository,
|
|
3707
4123
|
restoreAssetSwaps,
|
|
4124
|
+
restoreOfferCoverage,
|
|
3708
4125
|
retireSettledOfferContracts,
|
|
3709
4126
|
revealClaimPacket,
|
|
3710
4127
|
revealFieldsFromScript,
|