@arkade-os/swap 0.0.7 → 0.0.8
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 +271 -12
- package/dist/{chunk-WGRU2DBF.js → chunk-6ZUS47GA.js} +15 -1
- package/dist/{chunk-ZDTRQZE2.js → chunk-TU4NGZDP.js} +59 -22
- package/dist/index.cjs +1150 -427
- package/dist/index.d.cts +292 -1139
- package/dist/index.d.ts +292 -1139
- package/dist/index.js +759 -87
- 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 +49 -5
- package/dist/repositories/realm/index.d.cts +28 -6
- package/dist/repositories/realm/index.d.ts +28 -6
- package/dist/repositories/realm/index.js +50 -6
- package/dist/repositories/sqlite/index.cjs +45 -4
- package/dist/repositories/sqlite/index.d.cts +20 -7
- package/dist/repositories/sqlite/index.d.ts +20 -7
- package/dist/repositories/sqlite/index.js +46 -5
- package/dist/repository-BcZ9LXRP.d.ts +1862 -0
- package/dist/repository-Dso34L4D.d.cts +1862 -0
- package/dist/{rfq-DfT9dAss.d.cts → rfq-C-aq5LDJ.d.cts} +63 -2
- package/dist/{rfq-DfT9dAss.d.ts → rfq-C-aq5LDJ.d.ts} +63 -2
- package/package.json +2 -2
- package/dist/repository-BwnZ8N62.d.cts +0 -236
- package/dist/repository-BwnZ8N62.d.ts +0 -236
package/dist/index.cjs
CHANGED
|
@@ -40,6 +40,7 @@ __export(index_exports, {
|
|
|
40
40
|
LIGHTNING_RECEIVE_PAIR: () => LIGHTNING_RECEIVE_PAIR,
|
|
41
41
|
LIGHTNING_SEND_PAIR: () => LIGHTNING_SEND_PAIR,
|
|
42
42
|
LockupAmountMismatchError: () => LockupAmountMismatchError,
|
|
43
|
+
LockupContractMissing: () => LockupContractMissing,
|
|
43
44
|
LockupNeedsRecoveryError: () => LockupNeedsRecoveryError,
|
|
44
45
|
LockupRegistrationFailed: () => LockupRegistrationFailed,
|
|
45
46
|
MAX_MIN_CONFIRMATIONS: () => MAX_MIN_CONFIRMATIONS,
|
|
@@ -57,10 +58,12 @@ __export(index_exports, {
|
|
|
57
58
|
QUOTE_OPTIONS: () => QUOTE_OPTIONS,
|
|
58
59
|
REFUND_MTP_LAG_SECONDS: () => REFUND_MTP_LAG_SECONDS,
|
|
59
60
|
RFQ_RESOLVED_STATES: () => RFQ_RESOLVED_STATES,
|
|
61
|
+
RFQ_SWAP_RETENTION_SECONDS: () => RFQ_SWAP_RETENTION_SECONDS,
|
|
60
62
|
RFQ_SWAP_TERMINAL_STATES: () => RFQ_SWAP_TERMINAL_STATES,
|
|
61
63
|
RFQ_TERMINAL_STATES: () => RFQ_TERMINAL_STATES,
|
|
62
64
|
RefundNotLocallyPossibleError: () => RefundNotLocallyPossibleError,
|
|
63
65
|
RfqSwapManager: () => RfqSwapManager,
|
|
66
|
+
RfqSwapOriginRequired: () => RfqSwapOriginRequired,
|
|
64
67
|
SOLO_REFUND_HEADROOM_SECONDS: () => SOLO_REFUND_HEADROOM_SECONDS,
|
|
65
68
|
SWAP_LOCKUP_CONTRACT_KIND: () => SWAP_LOCKUP_CONTRACT_KIND,
|
|
66
69
|
SWAP_LOCKUP_CONTRACT_LABEL: () => SWAP_LOCKUP_CONTRACT_LABEL,
|
|
@@ -68,6 +71,7 @@ __export(index_exports, {
|
|
|
68
71
|
SwapRefusal: () => SwapRefusal,
|
|
69
72
|
addAssetSwap: () => addAssetSwap,
|
|
70
73
|
arkadeAssetLeg: () => arkadeAssetLeg,
|
|
74
|
+
arkadeRefunder: () => arkadeRefunder,
|
|
71
75
|
arkadeSwapRequest: () => arkadeSwapRequest,
|
|
72
76
|
assertFundable: () => assertFundable,
|
|
73
77
|
assertReceivable: () => assertReceivable,
|
|
@@ -83,6 +87,7 @@ __export(index_exports, {
|
|
|
83
87
|
classifyOnchainHtlc: () => classifyOnchainHtlc,
|
|
84
88
|
classifySpend: () => classifySpend,
|
|
85
89
|
createOffer: () => createOffer,
|
|
90
|
+
createRfqSwapRecord: () => createRfqSwapRecord,
|
|
86
91
|
decodeOffer: () => decodeOffer,
|
|
87
92
|
deriveLightningReceive: () => deriveLightningReceive,
|
|
88
93
|
deriveOnchainReceive: () => deriveOnchainReceive,
|
|
@@ -100,6 +105,7 @@ __export(index_exports, {
|
|
|
100
105
|
lightningReceiveRequest: () => lightningReceiveRequest,
|
|
101
106
|
lightningSendRequest: () => lightningSendRequest,
|
|
102
107
|
lightningSendVtxoScript: () => lightningSendVtxoScript,
|
|
108
|
+
lockupContractParams: () => lockupContractParams,
|
|
103
109
|
makeCachedFeedFetch: () => makeCachedFeedFetch,
|
|
104
110
|
newPreimage: () => newPreimage,
|
|
105
111
|
newRfqId: () => newRfqId,
|
|
@@ -108,12 +114,14 @@ __export(index_exports, {
|
|
|
108
114
|
offerVtxoScript: () => offerVtxoScript,
|
|
109
115
|
onchainHtlcScript: () => onchainHtlcScript,
|
|
110
116
|
onchainReceiveRequest: () => onchainReceiveRequest,
|
|
117
|
+
onchainSendProfile: () => onchainSendProfile,
|
|
111
118
|
onchainSendRequest: () => onchainSendRequest,
|
|
112
119
|
paymentHashOf: () => paymentHashOf,
|
|
113
120
|
preimageForSwapRecord: () => preimageForSwapRecord,
|
|
114
121
|
pushClaim: () => pushClaim,
|
|
115
122
|
pushRefundWithoutReceiver: () => pushRefundWithoutReceiver,
|
|
116
123
|
readLockupFate: () => readLockupFate,
|
|
124
|
+
rebuildRfqSwap: () => rebuildRfqSwap,
|
|
117
125
|
receiveVtxoScript: () => receiveVtxoScript,
|
|
118
126
|
refundIfUnresolved: () => refundIfUnresolved,
|
|
119
127
|
registerLockupContract: () => registerLockupContract,
|
|
@@ -124,9 +132,15 @@ __export(index_exports, {
|
|
|
124
132
|
requestOnchainSend: () => requestOnchainSend,
|
|
125
133
|
restoreAssetSwaps: () => restoreAssetSwaps,
|
|
126
134
|
retireSettledOfferContracts: () => retireSettledOfferContracts,
|
|
135
|
+
rfqClaimSecretOf: () => rfqClaimSecretOf,
|
|
127
136
|
rfqPair: () => rfqPair,
|
|
137
|
+
rfqSecretsProfile: () => rfqSecretsProfile,
|
|
138
|
+
rfqSignerOf: () => rfqSignerOf,
|
|
139
|
+
rfqSwapActivityInputs: () => rfqSwapActivityInputs,
|
|
140
|
+
rfqSwapOriginOf: () => rfqSwapOriginOf,
|
|
128
141
|
sealClaimPacket: () => sealClaimPacket,
|
|
129
142
|
senderIdentityForSwapRecord: () => senderIdentityForSwapRecord,
|
|
143
|
+
shouldRetainRfqSwap: () => shouldRetainRfqSwap,
|
|
130
144
|
spendTxidsOf: () => spendTxidsOf,
|
|
131
145
|
spendUpdate: () => spendUpdate,
|
|
132
146
|
swapActivityResolver: () => swapActivityResolver,
|
|
@@ -137,6 +151,7 @@ __export(index_exports, {
|
|
|
137
151
|
unilateralRefundWithoutReceiverDelay: () => unilateralRefundWithoutReceiverDelay,
|
|
138
152
|
updateAssetSwap: () => updateAssetSwap,
|
|
139
153
|
updateAssetSwapBestEffort: () => updateAssetSwapBestEffort,
|
|
154
|
+
updateRfqSwapRecord: () => updateRfqSwapRecord,
|
|
140
155
|
validatePlan: () => validatePlan,
|
|
141
156
|
verifyLockupAddress: () => verifyLockupAddress,
|
|
142
157
|
verifyReceiveInvoice: () => verifyReceiveInvoice,
|
|
@@ -762,8 +777,9 @@ var validatePlan = (plan, giveBalance, dust) => {
|
|
|
762
777
|
// src/repository.ts
|
|
763
778
|
var marketsCacheKey = (network, registry) => `arkade-intents-markets-${network}-${registry}`;
|
|
764
779
|
var InMemoryAssetSwapRepository = class {
|
|
765
|
-
version =
|
|
780
|
+
version = 4;
|
|
766
781
|
swaps = /* @__PURE__ */ new Map();
|
|
782
|
+
rfqSwaps = /* @__PURE__ */ new Map();
|
|
767
783
|
scanned = /* @__PURE__ */ new Set();
|
|
768
784
|
markets = /* @__PURE__ */ new Map();
|
|
769
785
|
async saveSwap(swap) {
|
|
@@ -772,6 +788,18 @@ var InMemoryAssetSwapRepository = class {
|
|
|
772
788
|
async getAllSwaps() {
|
|
773
789
|
return [...this.swaps.values()];
|
|
774
790
|
}
|
|
791
|
+
async saveRfqSwap(record) {
|
|
792
|
+
this.rfqSwaps.set(record.rfqId, record);
|
|
793
|
+
}
|
|
794
|
+
async getRfqSwap(rfqId) {
|
|
795
|
+
return this.rfqSwaps.get(rfqId);
|
|
796
|
+
}
|
|
797
|
+
async getAllRfqSwaps() {
|
|
798
|
+
return [...this.rfqSwaps.values()];
|
|
799
|
+
}
|
|
800
|
+
async removeRfqSwap(rfqId) {
|
|
801
|
+
this.rfqSwaps.delete(rfqId);
|
|
802
|
+
}
|
|
775
803
|
async getScannedTxids() {
|
|
776
804
|
return new Set(this.scanned);
|
|
777
805
|
}
|
|
@@ -786,6 +814,7 @@ var InMemoryAssetSwapRepository = class {
|
|
|
786
814
|
}
|
|
787
815
|
async clear() {
|
|
788
816
|
this.swaps.clear();
|
|
817
|
+
this.rfqSwaps.clear();
|
|
789
818
|
this.scanned.clear();
|
|
790
819
|
this.markets.clear();
|
|
791
820
|
}
|
|
@@ -796,58 +825,44 @@ var InMemoryAssetSwapRepository = class {
|
|
|
796
825
|
// src/indexedDbRepository.ts
|
|
797
826
|
var import_sdk4 = require("@arkade-os/sdk");
|
|
798
827
|
var DEFAULT_DB_NAME = "arkade-intents";
|
|
799
|
-
var DB_VERSION =
|
|
828
|
+
var DB_VERSION = 2;
|
|
800
829
|
var STORE_SWAPS = "swaps";
|
|
830
|
+
var STORE_RFQ_SWAPS = "rfqSwaps";
|
|
801
831
|
var STORE_SCANNED = "scannedTxids";
|
|
802
832
|
var STORE_MARKETS = "markets";
|
|
803
833
|
var STORES = [
|
|
804
834
|
[STORE_SWAPS, { keyPath: "id" }],
|
|
835
|
+
// v2. Separate from `swaps` rather than sharing it: the two record types
|
|
836
|
+
// have different keys and no consumer wants them interleaved.
|
|
837
|
+
[STORE_RFQ_SWAPS, { keyPath: "rfqId" }],
|
|
805
838
|
[STORE_SCANNED],
|
|
806
839
|
[STORE_MARKETS]
|
|
807
840
|
];
|
|
808
|
-
function initDatabase(db) {
|
|
841
|
+
function initDatabase(db, oldVersion, transaction) {
|
|
842
|
+
void oldVersion;
|
|
843
|
+
void transaction;
|
|
809
844
|
for (const [name, options] of STORES) {
|
|
810
845
|
if (!db.objectStoreNames.contains(name)) db.createObjectStore(name, options);
|
|
811
846
|
}
|
|
812
847
|
}
|
|
813
|
-
var request = (req) => new Promise((resolve, reject) => {
|
|
814
|
-
req.onsuccess = () => resolve(req.result);
|
|
815
|
-
req.onerror = () => reject(req.error);
|
|
816
|
-
});
|
|
817
|
-
var txDone = (tx) => new Promise((resolve, reject) => {
|
|
818
|
-
tx.oncomplete = () => resolve();
|
|
819
|
-
tx.onerror = () => reject(tx.error);
|
|
820
|
-
tx.onabort = () => reject(tx.error);
|
|
821
|
-
});
|
|
822
848
|
var IndexedDbAssetSwapRepository = class {
|
|
849
|
+
version = 4;
|
|
850
|
+
connection;
|
|
823
851
|
constructor(dbName = DEFAULT_DB_NAME) {
|
|
824
|
-
this.
|
|
825
|
-
}
|
|
826
|
-
dbName;
|
|
827
|
-
version = 2;
|
|
828
|
-
// the promise, not the resolved database: openDatabase bumps a refcount on
|
|
829
|
-
// every call including cache hits, while dispose closes once, so two
|
|
830
|
-
// concurrent first calls would strand the refcount above zero and leak the
|
|
831
|
-
// connection for the process lifetime. Cleared on failure so a failed open
|
|
832
|
-
// can be retried rather than cached forever.
|
|
833
|
-
dbPromise = null;
|
|
852
|
+
this.connection = (0, import_sdk4.createManagedConnection)(dbName, DB_VERSION, initDatabase);
|
|
853
|
+
}
|
|
834
854
|
ensureDb() {
|
|
835
|
-
return this.
|
|
836
|
-
(err) => {
|
|
837
|
-
this.dbPromise = null;
|
|
838
|
-
throw err;
|
|
839
|
-
}
|
|
840
|
-
);
|
|
855
|
+
return this.connection.get();
|
|
841
856
|
}
|
|
842
857
|
async readStore(name) {
|
|
843
858
|
return (await this.ensureDb()).transaction([name], "readonly").objectStore(name);
|
|
844
859
|
}
|
|
845
860
|
/** Every write in one place, so none of them can forget to await the
|
|
846
861
|
* commit. Requests need no individual await: a failed one aborts the
|
|
847
|
-
* transaction, which `
|
|
862
|
+
* transaction, which `awaitTransaction` reports. */
|
|
848
863
|
async write(name, apply) {
|
|
849
864
|
const tx = (await this.ensureDb()).transaction([name], "readwrite");
|
|
850
|
-
const done =
|
|
865
|
+
const done = (0, import_sdk4.awaitTransaction)(tx);
|
|
851
866
|
apply(tx.objectStore(name));
|
|
852
867
|
await done;
|
|
853
868
|
}
|
|
@@ -857,10 +872,26 @@ var IndexedDbAssetSwapRepository = class {
|
|
|
857
872
|
});
|
|
858
873
|
}
|
|
859
874
|
async getAllSwaps() {
|
|
860
|
-
return
|
|
875
|
+
return (0, import_sdk4.promisifyRequest)((await this.readStore(STORE_SWAPS)).getAll());
|
|
876
|
+
}
|
|
877
|
+
async saveRfqSwap(record) {
|
|
878
|
+
await this.write(STORE_RFQ_SWAPS, (store) => {
|
|
879
|
+
store.put(record);
|
|
880
|
+
});
|
|
881
|
+
}
|
|
882
|
+
async getRfqSwap(rfqId) {
|
|
883
|
+
return (0, import_sdk4.promisifyRequest)((await this.readStore(STORE_RFQ_SWAPS)).get(rfqId));
|
|
884
|
+
}
|
|
885
|
+
async getAllRfqSwaps() {
|
|
886
|
+
return (0, import_sdk4.promisifyRequest)((await this.readStore(STORE_RFQ_SWAPS)).getAll());
|
|
887
|
+
}
|
|
888
|
+
async removeRfqSwap(rfqId) {
|
|
889
|
+
await this.write(STORE_RFQ_SWAPS, (store) => {
|
|
890
|
+
store.delete(rfqId);
|
|
891
|
+
});
|
|
861
892
|
}
|
|
862
893
|
async getScannedTxids() {
|
|
863
|
-
const keys = await
|
|
894
|
+
const keys = await (0, import_sdk4.promisifyRequest)((await this.readStore(STORE_SCANNED)).getAllKeys());
|
|
864
895
|
return new Set(keys);
|
|
865
896
|
}
|
|
866
897
|
async markTxidsScanned(txids) {
|
|
@@ -870,7 +901,7 @@ var IndexedDbAssetSwapRepository = class {
|
|
|
870
901
|
}
|
|
871
902
|
async getCachedMarkets(network, registry) {
|
|
872
903
|
const store = await this.readStore(STORE_MARKETS);
|
|
873
|
-
return
|
|
904
|
+
return (0, import_sdk4.promisifyRequest)(store.get(marketsCacheKey(network, registry)));
|
|
874
905
|
}
|
|
875
906
|
async saveCachedMarkets(network, registry, entry) {
|
|
876
907
|
await this.write(STORE_MARKETS, (store) => {
|
|
@@ -883,269 +914,136 @@ var IndexedDbAssetSwapRepository = class {
|
|
|
883
914
|
async clear() {
|
|
884
915
|
const stores = STORES.map(([name]) => name);
|
|
885
916
|
const tx = (await this.ensureDb()).transaction(stores, "readwrite");
|
|
886
|
-
const done =
|
|
917
|
+
const done = (0, import_sdk4.awaitTransaction)(tx);
|
|
887
918
|
for (const name of stores) tx.objectStore(name).clear();
|
|
888
919
|
await done;
|
|
889
920
|
}
|
|
890
921
|
async [Symbol.asyncDispose]() {
|
|
891
|
-
|
|
892
|
-
await (0, import_sdk4.closeDatabase)(this.dbName);
|
|
893
|
-
this.dbPromise = null;
|
|
922
|
+
await this.connection[Symbol.asyncDispose]();
|
|
894
923
|
}
|
|
895
924
|
};
|
|
896
925
|
|
|
897
|
-
// src/
|
|
898
|
-
var
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
const chunkResults = await Promise.allSettled(
|
|
909
|
-
chunks.map(async (ids) => (await indexer.getVirtualTxs(ids)).txs)
|
|
910
|
-
);
|
|
911
|
-
for (const result of chunkResults) {
|
|
912
|
-
if (result.status !== "fulfilled") continue;
|
|
913
|
-
for (const psbt of result.value) {
|
|
914
|
-
try {
|
|
915
|
-
const parsed = import_sdk5.Transaction.fromPSBT(import_base3.base64.decode(psbt));
|
|
916
|
-
parsedByTxid.set(parsed.id, parsed);
|
|
917
|
-
} catch {
|
|
918
|
-
}
|
|
926
|
+
// src/rfqCorridors.ts
|
|
927
|
+
var import_base4 = require("@scure/base");
|
|
928
|
+
|
|
929
|
+
// src/rfqCorridor.ts
|
|
930
|
+
var RfqCorridorRegistry = class {
|
|
931
|
+
handlers = /* @__PURE__ */ new Map();
|
|
932
|
+
register(handler) {
|
|
933
|
+
if (this.handlers.has(handler.kind)) {
|
|
934
|
+
throw new Error(
|
|
935
|
+
`RFQ corridor handler for kind '${handler.kind}' is already registered`
|
|
936
|
+
);
|
|
919
937
|
}
|
|
938
|
+
this.handlers.set(handler.kind, handler);
|
|
920
939
|
}
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
)
|
|
926
|
-
|
|
927
|
-
let leaves;
|
|
928
|
-
try {
|
|
929
|
-
const script = offerVtxoScript(offer, serverPubkey);
|
|
930
|
-
if (import_base3.hex.encode(script.pkScript) !== import_base3.hex.encode(offer.swapPkScript)) return "indeterminate";
|
|
931
|
-
leaves = {
|
|
932
|
-
cancel: script.functionByName("cancel")?.leafScript,
|
|
933
|
-
fulfill: script.functionByName("fulfill")?.leafScript
|
|
934
|
-
};
|
|
935
|
-
} catch {
|
|
936
|
-
return "indeterminate";
|
|
937
|
-
}
|
|
938
|
-
for (let i = 0; i < spendTx.inputsLength; i++) {
|
|
939
|
-
const input = spendTx.getInput(i);
|
|
940
|
-
if (!input.txid || input.index !== deposit.vout) continue;
|
|
941
|
-
if (import_base3.hex.encode(input.txid) !== deposit.txid) continue;
|
|
942
|
-
for (const leaf of input.tapLeafScript ?? []) {
|
|
943
|
-
const spent = import_base3.hex.encode((0, import_sdk5.scriptFromTapLeafScript)(leaf));
|
|
944
|
-
if (leaves.cancel && spent === import_base3.hex.encode(leaves.cancel)) return "cancelled";
|
|
945
|
-
if (leaves.fulfill && spent === import_base3.hex.encode(leaves.fulfill)) return "fulfilled";
|
|
946
|
-
}
|
|
940
|
+
/** Takes a bare `string`, deliberately, where {@link RfqCorridorHandler.kind}
|
|
941
|
+
* is the manager's union: a lookup key comes off a record a backend handed
|
|
942
|
+
* back, and that it typechecks as a `kind` is a claim about the type, not
|
|
943
|
+
* about what was stored. Narrowing this would read as a check already made. */
|
|
944
|
+
get(kind) {
|
|
945
|
+
return this.handlers.get(kind);
|
|
947
946
|
}
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
if (candidates.length === 0) return { restored: [], scannedTxids: [] };
|
|
962
|
-
const byTxid = new Map(candidates.map((tx) => [tx.redeemTxid, tx]));
|
|
963
|
-
const parsedByTxid = await fetchParsedTxs(
|
|
964
|
-
indexer,
|
|
965
|
-
candidates.map((tx) => tx.redeemTxid)
|
|
966
|
-
);
|
|
967
|
-
const fetchedTxids = [];
|
|
968
|
-
const found = [];
|
|
969
|
-
for (const [txid, parsed] of parsedByTxid) {
|
|
970
|
-
const fundingTx = byTxid.get(txid);
|
|
971
|
-
if (!fundingTx) continue;
|
|
972
|
-
fetchedTxids.push(txid);
|
|
973
|
-
try {
|
|
974
|
-
const packet = import_sdk5.Extension.fromTx(parsed).getPacketByType(OFFER_PACKET_TYPE);
|
|
975
|
-
if (!packet) continue;
|
|
976
|
-
const payload = packet.serialize();
|
|
977
|
-
found.push({
|
|
978
|
-
fundingTx,
|
|
979
|
-
offer: decodeOffer(payload),
|
|
980
|
-
offerHex: import_base3.hex.encode(payload)
|
|
981
|
-
});
|
|
982
|
-
} catch {
|
|
947
|
+
/**
|
|
948
|
+
* The handler for a kind, or a loud failure.
|
|
949
|
+
*
|
|
950
|
+
* A record whose corridor is not registered cannot be restored, and
|
|
951
|
+
* guessing would monitor a swap with no idea how to drive it. Names the
|
|
952
|
+
* registered kinds so a missing `register()` call is obvious.
|
|
953
|
+
*/
|
|
954
|
+
getOrThrow(kind) {
|
|
955
|
+
const handler = this.get(kind);
|
|
956
|
+
if (!handler) {
|
|
957
|
+
throw new Error(
|
|
958
|
+
`no RFQ corridor handler registered for kind '${kind}'; registered: ${this.registeredKinds().join(", ") || "none"}`
|
|
959
|
+
);
|
|
983
960
|
}
|
|
961
|
+
return handler;
|
|
984
962
|
}
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
const { vtxos } = await indexer.getVtxos({ scripts });
|
|
988
|
-
const vtxoByScriptAndTxid = new Map(vtxos.map((v) => [`${v.script}:${v.txid}`, v]));
|
|
989
|
-
const txByAnyId = /* @__PURE__ */ new Map();
|
|
990
|
-
for (const tx of txs) {
|
|
991
|
-
for (const id of [tx.boardingTxid, tx.redeemTxid, tx.roundTxid]) {
|
|
992
|
-
if (id) txByAnyId.set(id, tx);
|
|
993
|
-
}
|
|
963
|
+
has(kind) {
|
|
964
|
+
return this.handlers.has(kind);
|
|
994
965
|
}
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
const vtxo = vtxoByScriptAndTxid.get(
|
|
998
|
-
`${import_base3.hex.encode(offer.swapPkScript)}:${fundingTx.redeemTxid}`
|
|
999
|
-
);
|
|
1000
|
-
if (vtxo?.virtualStatus.state !== "spent") continue;
|
|
1001
|
-
for (const txid of spendTxidsOf(vtxo)) spendTxids.add(txid);
|
|
966
|
+
registeredKinds() {
|
|
967
|
+
return [...this.handlers.keys()];
|
|
1002
968
|
}
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
for (const { fundingTx, offer, offerHex } of found) {
|
|
1007
|
-
const swapPkScript = import_base3.hex.encode(offer.swapPkScript);
|
|
1008
|
-
const vtxo = vtxoByScriptAndTxid.get(`${swapPkScript}:${fundingTx.redeemTxid}`);
|
|
1009
|
-
if (!vtxo) {
|
|
1010
|
-
unresolved.add(fundingTx.redeemTxid);
|
|
1011
|
-
continue;
|
|
1012
|
-
}
|
|
1013
|
-
const depositRider = vtxo.assets?.length === 1 && vtxo.assets[0].amount > BigInt(0) ? vtxo.assets[0] : void 0;
|
|
1014
|
-
const fromAsset = offer.offerAsset?.toString() ?? depositRider?.assetId ?? BTC_ASSET_ID;
|
|
1015
|
-
const toAsset = offer.wantAsset?.toString() ?? BTC_ASSET_ID;
|
|
1016
|
-
const depositAmount = fromAsset === BTC_ASSET_ID ? BigInt(vtxo.value) : vtxo.assets?.find((a) => a.assetId === fromAsset)?.amount;
|
|
1017
|
-
if (depositAmount === void 0) {
|
|
1018
|
-
unresolved.add(fundingTx.redeemTxid);
|
|
1019
|
-
continue;
|
|
1020
|
-
}
|
|
1021
|
-
const fromAmount = depositAmount.toString();
|
|
1022
|
-
const state = vtxo.virtualStatus.state;
|
|
1023
|
-
const spentTxid = state === "spent" ? vtxo.arkTxId || vtxo.spentBy : void 0;
|
|
1024
|
-
let status = "pending";
|
|
1025
|
-
if (state === "swept") status = "recoverable";
|
|
1026
|
-
else if (state === "spent") {
|
|
1027
|
-
const spendTxs = spendTxidsOf(vtxo).map((id) => spendTxByTxid.get(id)).filter((tx) => tx !== void 0);
|
|
1028
|
-
const kind = classifyDepositSpend(offer, serverPubkey, spendTxs, {
|
|
1029
|
-
txid: vtxo.txid,
|
|
1030
|
-
vout: vtxo.vout
|
|
1031
|
-
});
|
|
1032
|
-
if (kind === "indeterminate") {
|
|
1033
|
-
unresolved.add(fundingTx.redeemTxid);
|
|
1034
|
-
continue;
|
|
1035
|
-
}
|
|
1036
|
-
status = kind;
|
|
1037
|
-
}
|
|
1038
|
-
restored.push({
|
|
1039
|
-
id: fundingTx.redeemTxid,
|
|
1040
|
-
fromAsset,
|
|
1041
|
-
toAsset,
|
|
1042
|
-
fromAmount,
|
|
1043
|
-
toAmount: offer.wantAmount.toString(),
|
|
1044
|
-
// ponytail(arkade-os/ts-sdk#680): empty address makes cancel fall back
|
|
1045
|
-
// to the current server key; store the funded address if server-key
|
|
1046
|
-
// rotations become real (cancelOffer now at least diagnoses the
|
|
1047
|
-
// mismatch instead of reporting a missing VTXO)
|
|
1048
|
-
swapAddress: "",
|
|
1049
|
-
swapPkScript,
|
|
1050
|
-
offerHex,
|
|
1051
|
-
fundingTxid: fundingTx.redeemTxid,
|
|
1052
|
-
spentTxid,
|
|
1053
|
-
status,
|
|
1054
|
-
createdAt: fundingTx.createdAt ? fundingTx.createdAt * 1e3 : vtxo.createdAt.getTime(),
|
|
1055
|
-
// the completion time is the caller's record of the spend, if it
|
|
1056
|
-
// has one — the psbt that classified it carries no timestamp
|
|
1057
|
-
...status === "fulfilled" && spentTxid && txByAnyId.get(spentTxid)?.createdAt ? { completedAt: txByAnyId.get(spentTxid).createdAt * 1e3 } : {}
|
|
1058
|
-
});
|
|
969
|
+
/** Test seam, mirroring the contract registry's. */
|
|
970
|
+
unregister(kind) {
|
|
971
|
+
return this.handlers.delete(kind);
|
|
1059
972
|
}
|
|
1060
|
-
|
|
1061
|
-
|
|
973
|
+
};
|
|
974
|
+
var rfqCorridorHandlers = new RfqCorridorRegistry();
|
|
1062
975
|
|
|
1063
|
-
// src/
|
|
1064
|
-
var
|
|
1065
|
-
|
|
1066
|
-
var TERMINAL = ["fulfilled", "cancelled", "recoverable"];
|
|
1067
|
-
function spendUpdate(swap, spend) {
|
|
1068
|
-
if (TERMINAL.includes(swap.status)) return void 0;
|
|
1069
|
-
if (spend.kind === "indeterminate") return void 0;
|
|
1070
|
-
const status = spend.kind === "cancelled" ? "cancelled" : "fulfilled";
|
|
976
|
+
// src/rfqProfileParts.ts
|
|
977
|
+
var rfqSecretsProfile = (secrets, paymentHash) => {
|
|
978
|
+
const { signingDescriptor, ...preimage } = swapSecretsToRecord(secrets);
|
|
1071
979
|
return {
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
// mirrors restore.ts: a completion time is a fill's, not a cancel's
|
|
1075
|
-
...status === "fulfilled" && spend.at ? { completedAt: spend.at } : {}
|
|
980
|
+
signer: { signingDescriptor },
|
|
981
|
+
...paymentHash ? { hashlock: { paymentHash, ...preimage } } : {}
|
|
1076
982
|
};
|
|
1077
|
-
}
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
}
|
|
1084
|
-
|
|
1085
|
-
const
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
};
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
const { txs } = await indexer.getVirtualTxs(candidates);
|
|
1098
|
-
return classifyDepositSpend(
|
|
1099
|
-
decodeOffer(import_base4.hex.decode(swap.offerHex)),
|
|
1100
|
-
serverPubkey,
|
|
1101
|
-
txs.map((psbt) => import_sdk6.Transaction.fromPSBT(import_base4.base64.decode(psbt))),
|
|
1102
|
-
{ txid: vtxo.txid, vout: vtxo.vout }
|
|
1103
|
-
);
|
|
1104
|
-
} catch {
|
|
1105
|
-
return "indeterminate";
|
|
1106
|
-
}
|
|
1107
|
-
};
|
|
1108
|
-
const handleSpend = async (event) => {
|
|
1109
|
-
if (event.contract.metadata?.kind !== OFFER_CONTRACT_KIND) return;
|
|
1110
|
-
for (const vtxo of event.vtxos) {
|
|
1111
|
-
const spentTxid = vtxo.arkTxId || vtxo.spentBy;
|
|
1112
|
-
if (!spentTxid) continue;
|
|
1113
|
-
const swap = (await getAssetSwaps(repository)).find(
|
|
1114
|
-
(s) => s.fundingTxid === vtxo.txid && s.swapPkScript === event.contractScript
|
|
1115
|
-
);
|
|
1116
|
-
if (!swap) continue;
|
|
1117
|
-
const kind = await classify(swap, vtxo, spentTxid);
|
|
1118
|
-
const changes = spendUpdate(swap, { txid: spentTxid, kind, at: event.timestamp });
|
|
1119
|
-
if (!changes) continue;
|
|
1120
|
-
const { persisted, swaps } = await updateAssetSwapBestEffort(
|
|
1121
|
-
repository,
|
|
1122
|
-
swap.id,
|
|
1123
|
-
changes
|
|
1124
|
-
);
|
|
1125
|
-
if (!persisted) continue;
|
|
1126
|
-
onUpdate?.({ ...swap, ...changes });
|
|
1127
|
-
if (changes.status && RETIRABLE.includes(changes.status)) {
|
|
1128
|
-
await retireOfferContract(manager, swaps, event.contractScript);
|
|
1129
|
-
}
|
|
1130
|
-
}
|
|
1131
|
-
};
|
|
1132
|
-
const unsubscribe = manager.onContractEvent((event) => {
|
|
1133
|
-
if (event.type !== "vtxo_spent") return;
|
|
1134
|
-
enqueue(() => handleSpend(event));
|
|
1135
|
-
});
|
|
1136
|
-
return {
|
|
1137
|
-
stop: unsubscribe,
|
|
1138
|
-
idle: () => queue
|
|
983
|
+
};
|
|
984
|
+
var parseHex32 = (value, field) => {
|
|
985
|
+
if (typeof value !== "string" || !/^[0-9a-fA-F]{64}$/.test(value)) {
|
|
986
|
+
throw new Error(`${field} must be 32 bytes of hex, got ${JSON.stringify(value)}`);
|
|
987
|
+
}
|
|
988
|
+
return value.toLowerCase();
|
|
989
|
+
};
|
|
990
|
+
var parseSigner = (value) => {
|
|
991
|
+
const signingDescriptor = value?.signingDescriptor;
|
|
992
|
+
if (typeof signingDescriptor !== "string" || signingDescriptor.length === 0) {
|
|
993
|
+
throw new Error(
|
|
994
|
+
`rfq profile.signer carries no signingDescriptor (got ${JSON.stringify(signingDescriptor)})`
|
|
995
|
+
);
|
|
996
|
+
}
|
|
997
|
+
return { signingDescriptor };
|
|
998
|
+
};
|
|
999
|
+
var parseHashlock = (value) => {
|
|
1000
|
+
const raw = value ?? {};
|
|
1001
|
+
const hashlock = {
|
|
1002
|
+
paymentHash: parseHex32(raw.paymentHash, "rfq profile.hashlock.paymentHash")
|
|
1139
1003
|
};
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
|
|
1004
|
+
if (raw.preimageHex !== void 0) {
|
|
1005
|
+
hashlock.preimageHex = parseHex32(raw.preimageHex, "rfq profile.hashlock.preimageHex");
|
|
1006
|
+
}
|
|
1007
|
+
if (raw.preimageSaltHex !== void 0) {
|
|
1008
|
+
hashlock.preimageSaltHex = parseHex32(
|
|
1009
|
+
raw.preimageSaltHex,
|
|
1010
|
+
"rfq profile.hashlock.preimageSaltHex"
|
|
1011
|
+
);
|
|
1012
|
+
}
|
|
1013
|
+
return hashlock;
|
|
1014
|
+
};
|
|
1015
|
+
var hydrateHashlock = (profile) => {
|
|
1016
|
+
try {
|
|
1017
|
+
return { paymentHash: parseHashlock(profile.hashlock).paymentHash };
|
|
1018
|
+
} catch (cause) {
|
|
1019
|
+
throw new Error(
|
|
1020
|
+
`rfq record carries no usable hashlock; it cannot verify a preimage: ${String(cause)}`,
|
|
1021
|
+
{ cause }
|
|
1022
|
+
);
|
|
1023
|
+
}
|
|
1024
|
+
};
|
|
1025
|
+
var rfqSignerOf = (record) => {
|
|
1026
|
+
const signer = record.profile.signer;
|
|
1027
|
+
if (signer === void 0) return void 0;
|
|
1028
|
+
return parseSigner(signer);
|
|
1029
|
+
};
|
|
1030
|
+
var rfqClaimSecretOf = (record) => {
|
|
1031
|
+
const handler = rfqCorridorHandlers.getOrThrow(record.kind);
|
|
1032
|
+
if (!handler.claimSecret) return void 0;
|
|
1033
|
+
try {
|
|
1034
|
+
const claim = handler.claimSecret(record.profile);
|
|
1035
|
+
return { ...parseSigner(claim), ...parseHashlock(claim) };
|
|
1036
|
+
} catch (cause) {
|
|
1037
|
+
throw new PreimageNotRecoverableError(
|
|
1038
|
+
"malformed-record",
|
|
1039
|
+
`this ${record.kind} record's claim secret is unreadable: ${String(cause)}`,
|
|
1040
|
+
{ cause }
|
|
1041
|
+
);
|
|
1042
|
+
}
|
|
1043
|
+
};
|
|
1146
1044
|
|
|
1147
1045
|
// src/onchainHtlc.ts
|
|
1148
|
-
var
|
|
1046
|
+
var import_base3 = require("@scure/base");
|
|
1149
1047
|
var import_sha22 = require("@noble/hashes/sha2.js");
|
|
1150
1048
|
var import_legacy = require("@noble/hashes/legacy.js");
|
|
1151
1049
|
var btc = __toESM(require("@scure/btc-signer"), 1);
|
|
@@ -1155,8 +1053,8 @@ var MAX_MIN_CONFIRMATIONS = 6;
|
|
|
1155
1053
|
var ONCHAIN_SECONDS_PER_BLOCK = 600;
|
|
1156
1054
|
var ONCHAIN_DUST_SATS = BigInt(330);
|
|
1157
1055
|
var newPreimage = () => crypto.getRandomValues(new Uint8Array(32));
|
|
1158
|
-
var paymentHashOf = (preimage) =>
|
|
1159
|
-
var h160FromPaymentHash = (paymentHash) => (0, import_legacy.ripemd160)(
|
|
1056
|
+
var paymentHashOf = (preimage) => import_base3.hex.encode((0, import_sha22.sha256)(preimage));
|
|
1057
|
+
var h160FromPaymentHash = (paymentHash) => (0, import_legacy.ripemd160)(import_base3.hex.decode(paymentHash));
|
|
1160
1058
|
var L1_NETWORKS = {
|
|
1161
1059
|
bitcoin: btc.NETWORK,
|
|
1162
1060
|
testnet: btc.TEST_NETWORK,
|
|
@@ -1171,6 +1069,11 @@ function onchainHtlcScript(params, network) {
|
|
|
1171
1069
|
`refundLocktime must be a positive unix timestamp, got ${params.refundLocktime}`
|
|
1172
1070
|
);
|
|
1173
1071
|
}
|
|
1072
|
+
if (!Object.hasOwn(L1_NETWORKS, network)) {
|
|
1073
|
+
throw new Error(
|
|
1074
|
+
`unknown L1 network '${String(network)}' \u2014 expected one of ${Object.keys(L1_NETWORKS).join(", ")}`
|
|
1075
|
+
);
|
|
1076
|
+
}
|
|
1174
1077
|
const h160 = h160FromPaymentHash(params.paymentHash);
|
|
1175
1078
|
const claim = btc.Script.encode([
|
|
1176
1079
|
"SIZE",
|
|
@@ -1197,7 +1100,7 @@ function onchainHtlcScript(params, network) {
|
|
|
1197
1100
|
);
|
|
1198
1101
|
const controlBlockFor = (leaf) => {
|
|
1199
1102
|
for (const [block, script] of payment.tapLeafScript ?? []) {
|
|
1200
|
-
if (script.length - 1 === leaf.length &&
|
|
1103
|
+
if (script.length - 1 === leaf.length && import_base3.hex.encode(script.subarray(0, leaf.length)) === import_base3.hex.encode(leaf)) {
|
|
1201
1104
|
return btc.TaprootControlBlock.encode(block);
|
|
1202
1105
|
}
|
|
1203
1106
|
}
|
|
@@ -1290,13 +1193,13 @@ var buildHtlcRefund = (input) => buildLeafSpend({
|
|
|
1290
1193
|
function extractPreimage(txHex, paymentHash) {
|
|
1291
1194
|
let raw;
|
|
1292
1195
|
try {
|
|
1293
|
-
raw = btc.RawTx.decode(
|
|
1196
|
+
raw = btc.RawTx.decode(import_base3.hex.decode(txHex));
|
|
1294
1197
|
} catch {
|
|
1295
1198
|
return null;
|
|
1296
1199
|
}
|
|
1297
1200
|
for (const stack of raw.witnesses ?? []) {
|
|
1298
1201
|
for (const item of stack) {
|
|
1299
|
-
if (item.length === 32 &&
|
|
1202
|
+
if (item.length === 32 && import_base3.hex.encode((0, import_sha22.sha256)(item)) === paymentHash) return item;
|
|
1300
1203
|
}
|
|
1301
1204
|
}
|
|
1302
1205
|
return null;
|
|
@@ -1316,45 +1219,510 @@ async function awaitOnchainFill(chain, htlc, minConfirmations, options = {}) {
|
|
|
1316
1219
|
await sleep(pollMs);
|
|
1317
1220
|
}
|
|
1318
1221
|
}
|
|
1319
|
-
async function claimOnchainFill(chain, input) {
|
|
1320
|
-
const now = input.now ?? Math.floor(Date.now() / 1e3);
|
|
1321
|
-
if (input.htlc.refundLocktime - now < ONCHAIN_CLAIM_MARGIN_SECONDS) {
|
|
1322
|
-
const error = new Error(
|
|
1323
|
-
"refund leaf opens too soon to claim safely \u2014 take the covenant refund instead"
|
|
1324
|
-
);
|
|
1325
|
-
error.reason = "claim_window_closed";
|
|
1326
|
-
throw error;
|
|
1327
|
-
}
|
|
1328
|
-
const spend = await buildHtlcClaim(input);
|
|
1329
|
-
const txid = await chain.broadcast(spend.txHex);
|
|
1330
|
-
return { txid, payoutAmount: spend.payoutAmount };
|
|
1222
|
+
async function claimOnchainFill(chain, input) {
|
|
1223
|
+
const now = input.now ?? Math.floor(Date.now() / 1e3);
|
|
1224
|
+
if (input.htlc.refundLocktime - now < ONCHAIN_CLAIM_MARGIN_SECONDS) {
|
|
1225
|
+
const error = new Error(
|
|
1226
|
+
"refund leaf opens too soon to claim safely \u2014 take the covenant refund instead"
|
|
1227
|
+
);
|
|
1228
|
+
error.reason = "claim_window_closed";
|
|
1229
|
+
throw error;
|
|
1230
|
+
}
|
|
1231
|
+
const spend = await buildHtlcClaim(input);
|
|
1232
|
+
const txid = await chain.broadcast(spend.txHex);
|
|
1233
|
+
return { txid, payoutAmount: spend.payoutAmount };
|
|
1234
|
+
}
|
|
1235
|
+
async function classifyOnchainHtlc(chain, input) {
|
|
1236
|
+
const utxos = await chain.getScriptUtxos(input.htlc.pkScript);
|
|
1237
|
+
const best = utxos.sort((a, b) => b.amount > a.amount ? 1 : -1)[0];
|
|
1238
|
+
if (!best) {
|
|
1239
|
+
if (!input.funding) return { phase: "unfunded" };
|
|
1240
|
+
const spend = await chain.getSpendingTx(input.funding.txid, input.funding.vout);
|
|
1241
|
+
if (!spend) return { phase: "unfunded" };
|
|
1242
|
+
const preimage = extractPreimage(spend.txHex, input.htlc.paymentHash);
|
|
1243
|
+
const txid = btc.Transaction.fromRaw(import_base3.hex.decode(spend.txHex), {
|
|
1244
|
+
allowUnknownInputs: true,
|
|
1245
|
+
allowUnknownOutputs: true
|
|
1246
|
+
}).id;
|
|
1247
|
+
return preimage ? { phase: "claimed", txid, preimage } : { phase: "swept", txid };
|
|
1248
|
+
}
|
|
1249
|
+
if (best.confirmations < input.minConfirmations)
|
|
1250
|
+
return { phase: "awaiting_confirmations", utxo: best };
|
|
1251
|
+
const mtp = await chain.getMtp();
|
|
1252
|
+
if (mtp >= input.htlc.refundLocktime) return { phase: "refundable", utxo: best };
|
|
1253
|
+
return { phase: "claimable", utxo: best };
|
|
1254
|
+
}
|
|
1255
|
+
|
|
1256
|
+
// src/rfqCorridors.ts
|
|
1257
|
+
var LightningSendCorridor = {
|
|
1258
|
+
kind: "lightning_send",
|
|
1259
|
+
project: () => ({}),
|
|
1260
|
+
hydrate: (profile) => hydrateHashlock(profile)
|
|
1261
|
+
// No `claimSecret`, deliberately: `provisionRefundKey` mints no preimage at
|
|
1262
|
+
// all, and deriving one off the refund descriptor would fail the payment
|
|
1263
|
+
// hash check — a `hash-mismatch` on a swap that was never broken.
|
|
1264
|
+
};
|
|
1265
|
+
var LightningReceiveCorridor = {
|
|
1266
|
+
kind: "lightning_receive",
|
|
1267
|
+
// What the manager holds: the amount gate, and its own claim once it lands.
|
|
1268
|
+
// `payoutAddress` comes from the request result and never changes, so the
|
|
1269
|
+
// caller writes it once and this leaves it alone.
|
|
1270
|
+
project: (swap) => {
|
|
1271
|
+
const receive = swap;
|
|
1272
|
+
return {
|
|
1273
|
+
expectedAmount: receive.expectedAmount,
|
|
1274
|
+
...receive.claimArkTxid ? { claimArkTxid: receive.claimArkTxid } : {}
|
|
1275
|
+
};
|
|
1276
|
+
},
|
|
1277
|
+
hydrate(profile) {
|
|
1278
|
+
if (typeof profile.expectedAmount !== "number" || !Number.isFinite(profile.expectedAmount)) {
|
|
1279
|
+
throw new Error("lightning_receive record carries no expectedAmount; it cannot claim");
|
|
1280
|
+
}
|
|
1281
|
+
return {
|
|
1282
|
+
...hydrateHashlock(profile),
|
|
1283
|
+
expectedAmount: profile.expectedAmount,
|
|
1284
|
+
...profile.claimArkTxid ? { claimArkTxid: profile.claimArkTxid } : {}
|
|
1285
|
+
};
|
|
1286
|
+
},
|
|
1287
|
+
// We are the claimant here, so the preimage material on the hashlock is
|
|
1288
|
+
// ours to use.
|
|
1289
|
+
claimSecret: (profile) => ({ ...profile.signer, ...profile.hashlock }),
|
|
1290
|
+
activityTxids: (profile) => profile.claimArkTxid ? [profile.claimArkTxid] : []
|
|
1291
|
+
};
|
|
1292
|
+
function onchainSendProfile(result) {
|
|
1293
|
+
return {
|
|
1294
|
+
claimKey: import_base4.hex.encode(result.htlcParams.claimKey),
|
|
1295
|
+
refundKey: import_base4.hex.encode(result.htlcParams.refundKey),
|
|
1296
|
+
htlcLocktime: result.htlcParams.refundLocktime,
|
|
1297
|
+
network: result.l1Network,
|
|
1298
|
+
htlcAddress: result.htlc.address,
|
|
1299
|
+
minConfirmations: result.minConfirmations
|
|
1300
|
+
};
|
|
1301
|
+
}
|
|
1302
|
+
var OnchainSendCorridor = {
|
|
1303
|
+
kind: "onchain_send",
|
|
1304
|
+
// The L1 keys and the network are only in the request result — nothing on
|
|
1305
|
+
// `OnchainSendSwap` carries them — so the caller writes them once. What the
|
|
1306
|
+
// manager learns as it drives the swap is the fill and our own claim.
|
|
1307
|
+
project: (swap) => {
|
|
1308
|
+
const send = swap;
|
|
1309
|
+
return {
|
|
1310
|
+
...send.funding ? { funding: send.funding } : {},
|
|
1311
|
+
...send.claimTxid ? { claimTxid: send.claimTxid } : {}
|
|
1312
|
+
};
|
|
1313
|
+
},
|
|
1314
|
+
hydrate(profile) {
|
|
1315
|
+
const { paymentHash } = hydrateHashlock(profile);
|
|
1316
|
+
if (!profile.claimKey || !profile.refundKey) {
|
|
1317
|
+
throw new Error(
|
|
1318
|
+
"onchain_send record carries no L1 keys; its HTLC cannot be rebuilt and its refund window would pass unwatched"
|
|
1319
|
+
);
|
|
1320
|
+
}
|
|
1321
|
+
if (!Number.isInteger(profile.minConfirmations) || profile.minConfirmations < 1) {
|
|
1322
|
+
throw new Error(
|
|
1323
|
+
`onchain_send record carries no usable minConfirmations (${String(profile.minConfirmations)}); the confirmation gate cannot be checked \u2014 refusing to restore a swap that would claim an unconfirmed fill`
|
|
1324
|
+
);
|
|
1325
|
+
}
|
|
1326
|
+
const htlc = onchainHtlcScript(
|
|
1327
|
+
{
|
|
1328
|
+
// From the profile, not the covenant: the lockup commits to
|
|
1329
|
+
// `hash160(P)`, and the HTLC needs `sha256(P)`. One P
|
|
1330
|
+
// unlocks both legs, but only one of the two hashes of it
|
|
1331
|
+
// is recoverable here.
|
|
1332
|
+
paymentHash,
|
|
1333
|
+
claimKey: import_base4.hex.decode(profile.claimKey),
|
|
1334
|
+
refundKey: import_base4.hex.decode(profile.refundKey),
|
|
1335
|
+
refundLocktime: profile.htlcLocktime
|
|
1336
|
+
},
|
|
1337
|
+
profile.network
|
|
1338
|
+
);
|
|
1339
|
+
if (htlc.address !== profile.htlcAddress) {
|
|
1340
|
+
throw new Error(
|
|
1341
|
+
`onchain_send record's L1 inputs derive ${htlc.address}, but the fill was expected at ${String(profile.htlcAddress)} \u2014 these are not this swap's`
|
|
1342
|
+
);
|
|
1343
|
+
}
|
|
1344
|
+
return {
|
|
1345
|
+
paymentHash,
|
|
1346
|
+
htlc,
|
|
1347
|
+
minConfirmations: profile.minConfirmations,
|
|
1348
|
+
...profile.funding ? { funding: profile.funding } : {},
|
|
1349
|
+
...profile.claimTxid ? { claimTxid: profile.claimTxid } : {}
|
|
1350
|
+
};
|
|
1351
|
+
},
|
|
1352
|
+
// The trader claims the L1 HTLC with P, so this leg's preimage material is
|
|
1353
|
+
// ours.
|
|
1354
|
+
claimSecret: (profile) => ({ ...profile.signer, ...profile.hashlock }),
|
|
1355
|
+
// Our own L1 claim only. `funding` is the SOLVER's fill into the HTLC —
|
|
1356
|
+
// not a transaction of ours, so grouping it would claim a row this wallet
|
|
1357
|
+
// never made.
|
|
1358
|
+
activityTxids: (profile) => profile.claimTxid ? [profile.claimTxid] : []
|
|
1359
|
+
};
|
|
1360
|
+
rfqCorridorHandlers.register(LightningSendCorridor);
|
|
1361
|
+
rfqCorridorHandlers.register(LightningReceiveCorridor);
|
|
1362
|
+
rfqCorridorHandlers.register(OnchainSendCorridor);
|
|
1363
|
+
|
|
1364
|
+
// src/rfqRecord.ts
|
|
1365
|
+
var import_sdk5 = require("@arkade-os/sdk");
|
|
1366
|
+
var import_base5 = require("@scure/base");
|
|
1367
|
+
|
|
1368
|
+
// src/rfqSwapState.ts
|
|
1369
|
+
var RFQ_SWAP_TERMINAL_STATES = ["settled", "refunded", "failed"];
|
|
1370
|
+
var isRfqSwapTerminal = (state) => RFQ_SWAP_TERMINAL_STATES.includes(state);
|
|
1371
|
+
|
|
1372
|
+
// src/rfqRecord.ts
|
|
1373
|
+
var RFQ_SWAP_RETENTION_SECONDS = 30 * 24 * 60 * 60;
|
|
1374
|
+
var managerState = (swap) => ({
|
|
1375
|
+
rfqId: swap.rfqId,
|
|
1376
|
+
state: swap.state,
|
|
1377
|
+
createdAt: swap.createdAt,
|
|
1378
|
+
updatedAt: swap.updatedAt,
|
|
1379
|
+
...swap.refundArkTxid ? { refundArkTxid: swap.refundArkTxid } : {},
|
|
1380
|
+
...swap.lockupSpendArkTxids?.length ? { lockupSpendArkTxids: [...swap.lockupSpendArkTxids] } : {},
|
|
1381
|
+
...swap.failure ? { failure: swap.failure } : {},
|
|
1382
|
+
...swap.blockedReason ? { blockedReason: swap.blockedReason } : {}
|
|
1383
|
+
});
|
|
1384
|
+
function assertSameSwap(origin, swap) {
|
|
1385
|
+
if (origin.kind !== swap.kind) {
|
|
1386
|
+
throw new Error(
|
|
1387
|
+
`rfq swap record is a ${origin.kind} origin paired with a ${swap.kind} swap`
|
|
1388
|
+
);
|
|
1389
|
+
}
|
|
1390
|
+
const funded = import_base5.hex.encode(import_sdk5.ArkAddress.decode(origin.lockupAddress).pkScript);
|
|
1391
|
+
const watched = import_base5.hex.encode(swap.lockupPkScript);
|
|
1392
|
+
if (funded !== watched) {
|
|
1393
|
+
throw new Error(
|
|
1394
|
+
`rfq swap record's lockup address holds ${funded}, but the swap watches ${watched} \u2014 these are not the same swap`
|
|
1395
|
+
);
|
|
1396
|
+
}
|
|
1397
|
+
}
|
|
1398
|
+
function createRfqSwapRecord(origin, swap) {
|
|
1399
|
+
assertSameSwap(origin, swap);
|
|
1400
|
+
const handler = rfqCorridorHandlers.getOrThrow(origin.kind);
|
|
1401
|
+
return {
|
|
1402
|
+
...origin,
|
|
1403
|
+
...managerState(swap),
|
|
1404
|
+
// The caller wrote what only the request result knows; the handler adds
|
|
1405
|
+
// what the live swap already carries.
|
|
1406
|
+
profile: { ...origin.profile, ...handler.project(swap) }
|
|
1407
|
+
};
|
|
1408
|
+
}
|
|
1409
|
+
function updateRfqSwapRecord(record, swap) {
|
|
1410
|
+
assertSameSwap(record, swap);
|
|
1411
|
+
const {
|
|
1412
|
+
refundArkTxid: _refundArkTxid,
|
|
1413
|
+
lockupSpendArkTxids: _lockupSpendArkTxids,
|
|
1414
|
+
failure: _failure,
|
|
1415
|
+
blockedReason: _blockedReason,
|
|
1416
|
+
...origin
|
|
1417
|
+
} = record;
|
|
1418
|
+
const handler = rfqCorridorHandlers.getOrThrow(record.kind);
|
|
1419
|
+
return {
|
|
1420
|
+
...origin,
|
|
1421
|
+
...managerState(swap),
|
|
1422
|
+
profile: { ...record.profile, ...handler.project(swap) }
|
|
1423
|
+
};
|
|
1424
|
+
}
|
|
1425
|
+
function rfqSwapOriginOf(record) {
|
|
1426
|
+
return {
|
|
1427
|
+
kind: record.kind,
|
|
1428
|
+
lockupAddress: record.lockupAddress,
|
|
1429
|
+
profile: { ...record.profile },
|
|
1430
|
+
...record.amount !== void 0 ? { amount: record.amount } : {},
|
|
1431
|
+
...record.fundingArkTxid ? { fundingArkTxid: record.fundingArkTxid } : {}
|
|
1432
|
+
};
|
|
1433
|
+
}
|
|
1434
|
+
function lockupScript(params, lockupAddress) {
|
|
1435
|
+
const script = import_sdk5.VHTLCV2ContractHandler.createScript(params);
|
|
1436
|
+
const funded = import_sdk5.ArkAddress.decode(lockupAddress).pkScript;
|
|
1437
|
+
if (import_base5.hex.encode(funded) !== import_base5.hex.encode(script.pkScript)) {
|
|
1438
|
+
throw new Error(
|
|
1439
|
+
`rfq swap covenant params derive ${import_base5.hex.encode(script.pkScript)}, but the record's lockup address holds ${import_base5.hex.encode(funded)} \u2014 these params are not this swap's`
|
|
1440
|
+
);
|
|
1441
|
+
}
|
|
1442
|
+
return script;
|
|
1443
|
+
}
|
|
1444
|
+
function rebuildRfqSwap(record, params) {
|
|
1445
|
+
const script = lockupScript(params, record.lockupAddress);
|
|
1446
|
+
const common = {
|
|
1447
|
+
rfqId: record.rfqId,
|
|
1448
|
+
state: record.state,
|
|
1449
|
+
lockupPkScript: script.pkScript,
|
|
1450
|
+
lockup: { script, address: record.lockupAddress },
|
|
1451
|
+
// From the covenant, which binds it: the record's own copy would be a
|
|
1452
|
+
// second source for the deadline the refund is gated on.
|
|
1453
|
+
refundLocktime: Number(script.options.refundLocktime),
|
|
1454
|
+
createdAt: record.createdAt,
|
|
1455
|
+
updatedAt: record.updatedAt,
|
|
1456
|
+
...record.refundArkTxid ? { refundArkTxid: record.refundArkTxid } : {},
|
|
1457
|
+
...record.lockupSpendArkTxids?.length ? { lockupSpendArkTxids: [...record.lockupSpendArkTxids] } : {},
|
|
1458
|
+
...record.failure ? { failure: record.failure } : {},
|
|
1459
|
+
...record.blockedReason ? { blockedReason: record.blockedReason } : {}
|
|
1460
|
+
};
|
|
1461
|
+
const handler = rfqCorridorHandlers.getOrThrow(record.kind);
|
|
1462
|
+
return {
|
|
1463
|
+
...common,
|
|
1464
|
+
kind: record.kind,
|
|
1465
|
+
...handler.hydrate(record.profile, { lockup: script })
|
|
1466
|
+
};
|
|
1467
|
+
}
|
|
1468
|
+
function shouldRetainRfqSwap(record, now) {
|
|
1469
|
+
if (!isRfqSwapTerminal(record.state)) return true;
|
|
1470
|
+
return now - record.updatedAt < RFQ_SWAP_RETENTION_SECONDS;
|
|
1471
|
+
}
|
|
1472
|
+
|
|
1473
|
+
// src/restore.ts
|
|
1474
|
+
var import_base6 = require("@scure/base");
|
|
1475
|
+
var import_sdk6 = require("@arkade-os/sdk");
|
|
1476
|
+
var TXS_PER_REQUEST = 50;
|
|
1477
|
+
async function fetchParsedTxs(indexer, txids) {
|
|
1478
|
+
const parsedByTxid = /* @__PURE__ */ new Map();
|
|
1479
|
+
if (txids.length === 0) return parsedByTxid;
|
|
1480
|
+
const chunks = [];
|
|
1481
|
+
for (let i = 0; i < txids.length; i += TXS_PER_REQUEST) {
|
|
1482
|
+
chunks.push(txids.slice(i, i + TXS_PER_REQUEST));
|
|
1483
|
+
}
|
|
1484
|
+
const chunkResults = await Promise.allSettled(
|
|
1485
|
+
chunks.map(async (ids) => (await indexer.getVirtualTxs(ids)).txs)
|
|
1486
|
+
);
|
|
1487
|
+
for (const result of chunkResults) {
|
|
1488
|
+
if (result.status !== "fulfilled") continue;
|
|
1489
|
+
for (const psbt of result.value) {
|
|
1490
|
+
try {
|
|
1491
|
+
const parsed = import_sdk6.Transaction.fromPSBT(import_base6.base64.decode(psbt));
|
|
1492
|
+
parsedByTxid.set(parsed.id, parsed);
|
|
1493
|
+
} catch {
|
|
1494
|
+
}
|
|
1495
|
+
}
|
|
1496
|
+
}
|
|
1497
|
+
return parsedByTxid;
|
|
1498
|
+
}
|
|
1499
|
+
var unscannedSwapCandidates = (txs, existingIds, scanned) => txs.filter(
|
|
1500
|
+
(tx) => tx.type === "sent" && tx.redeemTxid && !existingIds.has(tx.redeemTxid) && !scanned.has(tx.redeemTxid)
|
|
1501
|
+
);
|
|
1502
|
+
function classifySpend(offer, serverPubkey, spendTx, deposit) {
|
|
1503
|
+
let leaves;
|
|
1504
|
+
try {
|
|
1505
|
+
const script = offerVtxoScript(offer, serverPubkey);
|
|
1506
|
+
if (import_base6.hex.encode(script.pkScript) !== import_base6.hex.encode(offer.swapPkScript)) return "indeterminate";
|
|
1507
|
+
leaves = {
|
|
1508
|
+
cancel: script.functionByName("cancel")?.leafScript,
|
|
1509
|
+
fulfill: script.functionByName("fulfill")?.leafScript
|
|
1510
|
+
};
|
|
1511
|
+
} catch {
|
|
1512
|
+
return "indeterminate";
|
|
1513
|
+
}
|
|
1514
|
+
for (let i = 0; i < spendTx.inputsLength; i++) {
|
|
1515
|
+
const input = spendTx.getInput(i);
|
|
1516
|
+
if (!input.txid || input.index !== deposit.vout) continue;
|
|
1517
|
+
if (import_base6.hex.encode(input.txid) !== deposit.txid) continue;
|
|
1518
|
+
for (const leaf of input.tapLeafScript ?? []) {
|
|
1519
|
+
const spent = import_base6.hex.encode((0, import_sdk6.scriptFromTapLeafScript)(leaf));
|
|
1520
|
+
if (leaves.cancel && spent === import_base6.hex.encode(leaves.cancel)) return "cancelled";
|
|
1521
|
+
if (leaves.fulfill && spent === import_base6.hex.encode(leaves.fulfill)) return "fulfilled";
|
|
1522
|
+
}
|
|
1523
|
+
}
|
|
1524
|
+
return "indeterminate";
|
|
1525
|
+
}
|
|
1526
|
+
var spendTxidsOf = (vtxo) => [vtxo.spentBy, vtxo.arkTxId].filter((id) => Boolean(id));
|
|
1527
|
+
function classifyDepositSpend(offer, serverPubkey, spendTxs, deposit) {
|
|
1528
|
+
for (const tx of spendTxs) {
|
|
1529
|
+
const kind = classifySpend(offer, serverPubkey, tx, deposit);
|
|
1530
|
+
if (kind !== "indeterminate") return kind;
|
|
1531
|
+
}
|
|
1532
|
+
return "indeterminate";
|
|
1533
|
+
}
|
|
1534
|
+
async function restoreAssetSwaps(indexer, txs, existingIds, opts) {
|
|
1535
|
+
const { serverPubkey, scanned = /* @__PURE__ */ new Set() } = opts;
|
|
1536
|
+
const candidates = unscannedSwapCandidates(txs, existingIds, scanned);
|
|
1537
|
+
if (candidates.length === 0) return { restored: [], scannedTxids: [] };
|
|
1538
|
+
const byTxid = new Map(candidates.map((tx) => [tx.redeemTxid, tx]));
|
|
1539
|
+
const parsedByTxid = await fetchParsedTxs(
|
|
1540
|
+
indexer,
|
|
1541
|
+
candidates.map((tx) => tx.redeemTxid)
|
|
1542
|
+
);
|
|
1543
|
+
const fetchedTxids = [];
|
|
1544
|
+
const found = [];
|
|
1545
|
+
for (const [txid, parsed] of parsedByTxid) {
|
|
1546
|
+
const fundingTx = byTxid.get(txid);
|
|
1547
|
+
if (!fundingTx) continue;
|
|
1548
|
+
fetchedTxids.push(txid);
|
|
1549
|
+
try {
|
|
1550
|
+
const packet = import_sdk6.Extension.fromTx(parsed).getPacketByType(OFFER_PACKET_TYPE);
|
|
1551
|
+
if (!packet) continue;
|
|
1552
|
+
const payload = packet.serialize();
|
|
1553
|
+
found.push({
|
|
1554
|
+
fundingTx,
|
|
1555
|
+
offer: decodeOffer(payload),
|
|
1556
|
+
offerHex: import_base6.hex.encode(payload)
|
|
1557
|
+
});
|
|
1558
|
+
} catch {
|
|
1559
|
+
}
|
|
1560
|
+
}
|
|
1561
|
+
if (found.length === 0) return { restored: [], scannedTxids: fetchedTxids };
|
|
1562
|
+
const scripts = [...new Set(found.map((f) => import_base6.hex.encode(f.offer.swapPkScript)))];
|
|
1563
|
+
const { vtxos } = await indexer.getVtxos({ scripts });
|
|
1564
|
+
const vtxoByScriptAndTxid = new Map(vtxos.map((v) => [`${v.script}:${v.txid}`, v]));
|
|
1565
|
+
const txByAnyId = /* @__PURE__ */ new Map();
|
|
1566
|
+
for (const tx of txs) {
|
|
1567
|
+
for (const id of [tx.boardingTxid, tx.redeemTxid, tx.roundTxid]) {
|
|
1568
|
+
if (id) txByAnyId.set(id, tx);
|
|
1569
|
+
}
|
|
1570
|
+
}
|
|
1571
|
+
const spendTxids = /* @__PURE__ */ new Set();
|
|
1572
|
+
for (const { fundingTx, offer } of found) {
|
|
1573
|
+
const vtxo = vtxoByScriptAndTxid.get(
|
|
1574
|
+
`${import_base6.hex.encode(offer.swapPkScript)}:${fundingTx.redeemTxid}`
|
|
1575
|
+
);
|
|
1576
|
+
if (vtxo?.virtualStatus.state !== "spent") continue;
|
|
1577
|
+
for (const txid of spendTxidsOf(vtxo)) spendTxids.add(txid);
|
|
1578
|
+
}
|
|
1579
|
+
const spendTxByTxid = await fetchParsedTxs(indexer, [...spendTxids]);
|
|
1580
|
+
const restored = [];
|
|
1581
|
+
const unresolved = /* @__PURE__ */ new Set();
|
|
1582
|
+
for (const { fundingTx, offer, offerHex } of found) {
|
|
1583
|
+
const swapPkScript = import_base6.hex.encode(offer.swapPkScript);
|
|
1584
|
+
const vtxo = vtxoByScriptAndTxid.get(`${swapPkScript}:${fundingTx.redeemTxid}`);
|
|
1585
|
+
if (!vtxo) {
|
|
1586
|
+
unresolved.add(fundingTx.redeemTxid);
|
|
1587
|
+
continue;
|
|
1588
|
+
}
|
|
1589
|
+
const depositRider = vtxo.assets?.length === 1 && vtxo.assets[0].amount > BigInt(0) ? vtxo.assets[0] : void 0;
|
|
1590
|
+
const fromAsset = offer.offerAsset?.toString() ?? depositRider?.assetId ?? BTC_ASSET_ID;
|
|
1591
|
+
const toAsset = offer.wantAsset?.toString() ?? BTC_ASSET_ID;
|
|
1592
|
+
const depositAmount = fromAsset === BTC_ASSET_ID ? BigInt(vtxo.value) : vtxo.assets?.find((a) => a.assetId === fromAsset)?.amount;
|
|
1593
|
+
if (depositAmount === void 0) {
|
|
1594
|
+
unresolved.add(fundingTx.redeemTxid);
|
|
1595
|
+
continue;
|
|
1596
|
+
}
|
|
1597
|
+
const fromAmount = depositAmount.toString();
|
|
1598
|
+
const state = vtxo.virtualStatus.state;
|
|
1599
|
+
const spentTxid = state === "spent" ? vtxo.arkTxId || vtxo.spentBy : void 0;
|
|
1600
|
+
let status = "pending";
|
|
1601
|
+
if (state === "swept") status = "recoverable";
|
|
1602
|
+
else if (state === "spent") {
|
|
1603
|
+
const spendTxs = spendTxidsOf(vtxo).map((id) => spendTxByTxid.get(id)).filter((tx) => tx !== void 0);
|
|
1604
|
+
const kind = classifyDepositSpend(offer, serverPubkey, spendTxs, {
|
|
1605
|
+
txid: vtxo.txid,
|
|
1606
|
+
vout: vtxo.vout
|
|
1607
|
+
});
|
|
1608
|
+
if (kind === "indeterminate") {
|
|
1609
|
+
unresolved.add(fundingTx.redeemTxid);
|
|
1610
|
+
continue;
|
|
1611
|
+
}
|
|
1612
|
+
status = kind;
|
|
1613
|
+
}
|
|
1614
|
+
restored.push({
|
|
1615
|
+
id: fundingTx.redeemTxid,
|
|
1616
|
+
fromAsset,
|
|
1617
|
+
toAsset,
|
|
1618
|
+
fromAmount,
|
|
1619
|
+
toAmount: offer.wantAmount.toString(),
|
|
1620
|
+
// ponytail(arkade-os/ts-sdk#680): empty address makes cancel fall back
|
|
1621
|
+
// to the current server key; store the funded address if server-key
|
|
1622
|
+
// rotations become real (cancelOffer now at least diagnoses the
|
|
1623
|
+
// mismatch instead of reporting a missing VTXO)
|
|
1624
|
+
swapAddress: "",
|
|
1625
|
+
swapPkScript,
|
|
1626
|
+
offerHex,
|
|
1627
|
+
fundingTxid: fundingTx.redeemTxid,
|
|
1628
|
+
spentTxid,
|
|
1629
|
+
status,
|
|
1630
|
+
createdAt: fundingTx.createdAt ? fundingTx.createdAt * 1e3 : vtxo.createdAt.getTime(),
|
|
1631
|
+
// the completion time is the caller's record of the spend, if it
|
|
1632
|
+
// has one — the psbt that classified it carries no timestamp
|
|
1633
|
+
...status === "fulfilled" && spentTxid && txByAnyId.get(spentTxid)?.createdAt ? { completedAt: txByAnyId.get(spentTxid).createdAt * 1e3 } : {}
|
|
1634
|
+
});
|
|
1635
|
+
}
|
|
1636
|
+
return { restored, scannedTxids: fetchedTxids.filter((id) => !unresolved.has(id)) };
|
|
1637
|
+
}
|
|
1638
|
+
|
|
1639
|
+
// src/watch.ts
|
|
1640
|
+
var import_base7 = require("@scure/base");
|
|
1641
|
+
var import_sdk7 = require("@arkade-os/sdk");
|
|
1642
|
+
var TERMINAL = ["fulfilled", "cancelled", "recoverable"];
|
|
1643
|
+
function spendUpdate(swap, spend) {
|
|
1644
|
+
if (TERMINAL.includes(swap.status)) return void 0;
|
|
1645
|
+
if (spend.kind === "indeterminate") return void 0;
|
|
1646
|
+
const status = spend.kind === "cancelled" ? "cancelled" : "fulfilled";
|
|
1647
|
+
return {
|
|
1648
|
+
status,
|
|
1649
|
+
spentTxid: spend.txid,
|
|
1650
|
+
// mirrors restore.ts: a completion time is a fill's, not a cancel's
|
|
1651
|
+
...status === "fulfilled" && spend.at ? { completedAt: spend.at } : {}
|
|
1652
|
+
};
|
|
1331
1653
|
}
|
|
1332
|
-
async function
|
|
1333
|
-
|
|
1334
|
-
|
|
1335
|
-
|
|
1336
|
-
|
|
1337
|
-
|
|
1338
|
-
|
|
1339
|
-
|
|
1340
|
-
|
|
1341
|
-
|
|
1342
|
-
|
|
1343
|
-
|
|
1344
|
-
|
|
1345
|
-
}
|
|
1346
|
-
|
|
1347
|
-
|
|
1348
|
-
|
|
1349
|
-
|
|
1350
|
-
|
|
1654
|
+
async function watchOfferSwaps({
|
|
1655
|
+
wallet,
|
|
1656
|
+
arkServerUrl,
|
|
1657
|
+
repository,
|
|
1658
|
+
onUpdate
|
|
1659
|
+
}) {
|
|
1660
|
+
const manager = await wallet.getContractManager();
|
|
1661
|
+
const serverPubkey = import_sdk7.ArkAddress.decode(await wallet.getAddress()).serverPubKey;
|
|
1662
|
+
const indexer = new import_sdk7.RestIndexerProvider(arkServerUrl);
|
|
1663
|
+
let queue = Promise.resolve();
|
|
1664
|
+
const enqueue = (task) => {
|
|
1665
|
+
queue = queue.then(task).catch(() => {
|
|
1666
|
+
});
|
|
1667
|
+
};
|
|
1668
|
+
const classify = async (swap, vtxo, spentTxid) => {
|
|
1669
|
+
if (swap.spentTxid === spentTxid && swap.status === "cancelling") return "cancelled";
|
|
1670
|
+
try {
|
|
1671
|
+
const candidates = spendTxidsOf(vtxo);
|
|
1672
|
+
if (candidates.length === 0) return "indeterminate";
|
|
1673
|
+
const { txs } = await indexer.getVirtualTxs(candidates);
|
|
1674
|
+
return classifyDepositSpend(
|
|
1675
|
+
decodeOffer(import_base7.hex.decode(swap.offerHex)),
|
|
1676
|
+
serverPubkey,
|
|
1677
|
+
txs.map((psbt) => import_sdk7.Transaction.fromPSBT(import_base7.base64.decode(psbt))),
|
|
1678
|
+
{ txid: vtxo.txid, vout: vtxo.vout }
|
|
1679
|
+
);
|
|
1680
|
+
} catch {
|
|
1681
|
+
return "indeterminate";
|
|
1682
|
+
}
|
|
1683
|
+
};
|
|
1684
|
+
const handleSpend = async (event) => {
|
|
1685
|
+
if (event.contract.metadata?.kind !== OFFER_CONTRACT_KIND) return;
|
|
1686
|
+
for (const vtxo of event.vtxos) {
|
|
1687
|
+
const spentTxid = vtxo.arkTxId || vtxo.spentBy;
|
|
1688
|
+
if (!spentTxid) continue;
|
|
1689
|
+
const swap = (await getAssetSwaps(repository)).find(
|
|
1690
|
+
(s) => s.fundingTxid === vtxo.txid && s.swapPkScript === event.contractScript
|
|
1691
|
+
);
|
|
1692
|
+
if (!swap) continue;
|
|
1693
|
+
const kind = await classify(swap, vtxo, spentTxid);
|
|
1694
|
+
const changes = spendUpdate(swap, { txid: spentTxid, kind, at: event.timestamp });
|
|
1695
|
+
if (!changes) continue;
|
|
1696
|
+
const { persisted, swaps } = await updateAssetSwapBestEffort(
|
|
1697
|
+
repository,
|
|
1698
|
+
swap.id,
|
|
1699
|
+
changes
|
|
1700
|
+
);
|
|
1701
|
+
if (!persisted) continue;
|
|
1702
|
+
onUpdate?.({ ...swap, ...changes });
|
|
1703
|
+
if (changes.status && RETIRABLE.includes(changes.status)) {
|
|
1704
|
+
await retireOfferContract(manager, swaps, event.contractScript);
|
|
1705
|
+
}
|
|
1706
|
+
}
|
|
1707
|
+
};
|
|
1708
|
+
const unsubscribe = manager.onContractEvent((event) => {
|
|
1709
|
+
if (event.type !== "vtxo_spent") return;
|
|
1710
|
+
enqueue(() => handleSpend(event));
|
|
1711
|
+
});
|
|
1712
|
+
return {
|
|
1713
|
+
stop: unsubscribe,
|
|
1714
|
+
idle: () => queue
|
|
1715
|
+
};
|
|
1351
1716
|
}
|
|
1352
1717
|
|
|
1353
1718
|
// src/rfq.ts
|
|
1719
|
+
var import_base10 = require("@scure/base");
|
|
1720
|
+
var import_legacy2 = require("@noble/hashes/legacy.js");
|
|
1354
1721
|
var import_sdk9 = require("@arkade-os/sdk");
|
|
1722
|
+
var import_sdk10 = require("@arkade-os/sdk");
|
|
1355
1723
|
|
|
1356
1724
|
// src/claimPacket.ts
|
|
1357
|
-
var
|
|
1725
|
+
var import_base8 = require("@scure/base");
|
|
1358
1726
|
var import_secp256k1 = require("@noble/curves/secp256k1.js");
|
|
1359
1727
|
var import_hkdf = require("@noble/hashes/hkdf.js");
|
|
1360
1728
|
var import_sha23 = require("@noble/hashes/sha2.js");
|
|
@@ -1393,15 +1761,29 @@ async function sealWithEntropy(input, ephemeralKey, nonce) {
|
|
|
1393
1761
|
packet.set(ephemeralPub, 0);
|
|
1394
1762
|
packet.set(nonce, 33);
|
|
1395
1763
|
packet.set(sealed, 45);
|
|
1396
|
-
return { ciphertext:
|
|
1764
|
+
return { ciphertext: import_base8.base64.encode(packet) };
|
|
1397
1765
|
}
|
|
1398
1766
|
|
|
1399
1767
|
// src/lockupContract.ts
|
|
1400
|
-
var
|
|
1401
|
-
var
|
|
1768
|
+
var import_base9 = require("@scure/base");
|
|
1769
|
+
var import_sdk8 = require("@arkade-os/sdk");
|
|
1402
1770
|
var SWAP_LOCKUP_CONTRACT_TYPE = "vhtlc-v2";
|
|
1403
1771
|
var SWAP_LOCKUP_CONTRACT_LABEL = "Arkade RFQ swap lockup";
|
|
1404
1772
|
var SWAP_LOCKUP_CONTRACT_KIND = "rfq-swap-lockup";
|
|
1773
|
+
var LockupContractMissing = class extends Error {
|
|
1774
|
+
/** The lockup whose row is absent. */
|
|
1775
|
+
address;
|
|
1776
|
+
/** Its pkScript hex — the key the row would have been under. */
|
|
1777
|
+
script;
|
|
1778
|
+
constructor(address, script) {
|
|
1779
|
+
super(
|
|
1780
|
+
`no contract row for lockup ${address} (script ${script}); its covenant cannot be rebuilt from this wallet's contract store`
|
|
1781
|
+
);
|
|
1782
|
+
this.name = "LockupContractMissing";
|
|
1783
|
+
this.address = address;
|
|
1784
|
+
this.script = script;
|
|
1785
|
+
}
|
|
1786
|
+
};
|
|
1405
1787
|
var LockupRegistrationFailed = class extends Error {
|
|
1406
1788
|
/** The lockup address that was never registered — never fund it: nothing
|
|
1407
1789
|
* is watching it. */
|
|
@@ -1420,8 +1802,8 @@ async function registerLockupContract(contracts, script, address) {
|
|
|
1420
1802
|
try {
|
|
1421
1803
|
await contracts.createContract({
|
|
1422
1804
|
type: SWAP_LOCKUP_CONTRACT_TYPE,
|
|
1423
|
-
params:
|
|
1424
|
-
script:
|
|
1805
|
+
params: import_sdk8.VHTLCV2ContractHandler.serializeParams(script.options),
|
|
1806
|
+
script: import_base9.hex.encode(script.pkScript),
|
|
1425
1807
|
address,
|
|
1426
1808
|
label: SWAP_LOCKUP_CONTRACT_LABEL,
|
|
1427
1809
|
metadata: { genericallySpendable: false, kind: SWAP_LOCKUP_CONTRACT_KIND }
|
|
@@ -1430,6 +1812,12 @@ async function registerLockupContract(contracts, script, address) {
|
|
|
1430
1812
|
throw new LockupRegistrationFailed(script, address, error);
|
|
1431
1813
|
}
|
|
1432
1814
|
}
|
|
1815
|
+
async function lockupContractParams(contracts, lockupAddress) {
|
|
1816
|
+
const script = import_base9.hex.encode(import_sdk8.ArkAddress.decode(lockupAddress).pkScript);
|
|
1817
|
+
const [row] = await contracts.getContracts({ script });
|
|
1818
|
+
if (!row) throw new LockupContractMissing(lockupAddress, script);
|
|
1819
|
+
return row.params;
|
|
1820
|
+
}
|
|
1433
1821
|
|
|
1434
1822
|
// src/rfq.ts
|
|
1435
1823
|
var xOnly = (key, label) => {
|
|
@@ -1441,7 +1829,7 @@ var xOnly = (key, label) => {
|
|
|
1441
1829
|
};
|
|
1442
1830
|
var solverHex = (value, field) => {
|
|
1443
1831
|
try {
|
|
1444
|
-
return
|
|
1832
|
+
return import_base10.hex.decode(value);
|
|
1445
1833
|
} catch {
|
|
1446
1834
|
throw new Error(`solver sent malformed hex for ${field}`);
|
|
1447
1835
|
}
|
|
@@ -1477,7 +1865,7 @@ var AddressMismatch = class extends Error {
|
|
|
1477
1865
|
this.quoted = quoted;
|
|
1478
1866
|
}
|
|
1479
1867
|
};
|
|
1480
|
-
var newRfqId = () =>
|
|
1868
|
+
var newRfqId = () => import_base10.hex.encode(crypto.getRandomValues(new Uint8Array(32)));
|
|
1481
1869
|
var lightningSendRequest = (input) => ({
|
|
1482
1870
|
v: 1,
|
|
1483
1871
|
type: "rfq_request",
|
|
@@ -1487,7 +1875,7 @@ var lightningSendRequest = (input) => ({
|
|
|
1487
1875
|
profile: {
|
|
1488
1876
|
invoice: input.invoice,
|
|
1489
1877
|
refund_address: input.refundAddress,
|
|
1490
|
-
client_refund_pubkey:
|
|
1878
|
+
client_refund_pubkey: import_base10.hex.encode(input.senderPubkey)
|
|
1491
1879
|
}
|
|
1492
1880
|
});
|
|
1493
1881
|
var arkadeSwapRequest = (input) => {
|
|
@@ -1734,11 +2122,11 @@ function lightningSendVtxoScript(params) {
|
|
|
1734
2122
|
type: "seconds",
|
|
1735
2123
|
value: BigInt(value)
|
|
1736
2124
|
});
|
|
1737
|
-
return new
|
|
2125
|
+
return new import_sdk9.VHTLC.ScriptV2({
|
|
1738
2126
|
sender: params.senderPubkey,
|
|
1739
2127
|
receiver: params.solverPubkey,
|
|
1740
2128
|
server: params.serverPubkey,
|
|
1741
|
-
preimageHash: (0, import_legacy2.ripemd160)(
|
|
2129
|
+
preimageHash: (0, import_legacy2.ripemd160)(import_base10.hex.decode(params.paymentHash)),
|
|
1742
2130
|
refundLocktime: BigInt(params.refundLocktime),
|
|
1743
2131
|
unilateralClaimDelay: seconds(params.claimDelay),
|
|
1744
2132
|
unilateralRefundDelay: seconds(unilateralRefundDelay(params.claimDelay)),
|
|
@@ -1757,10 +2145,10 @@ function lightningSendVtxoScript(params) {
|
|
|
1757
2145
|
}
|
|
1758
2146
|
async function requestLightningSend(wallet, arkServerUrl, transport, params) {
|
|
1759
2147
|
const rfqId = params.rfqId ?? newRfqId();
|
|
1760
|
-
const secrets = await (0,
|
|
2148
|
+
const secrets = await (0, import_sdk10.provisionRefundKey)(wallet);
|
|
1761
2149
|
const senderPubkey = secrets.pubkey;
|
|
1762
2150
|
const [info, refundAddress] = await Promise.all([
|
|
1763
|
-
new
|
|
2151
|
+
new import_sdk9.RestArkProvider(arkServerUrl).getInfo(),
|
|
1764
2152
|
wallet.getAddress()
|
|
1765
2153
|
]);
|
|
1766
2154
|
const quote = await transport.requestQuote(
|
|
@@ -1783,22 +2171,23 @@ async function requestLightningSend(wallet, arkServerUrl, transport, params) {
|
|
|
1783
2171
|
`quote from_amount ${quote.from_amount} is below the invoice amount \u2014 a negative spread is not a quote`
|
|
1784
2172
|
);
|
|
1785
2173
|
}
|
|
1786
|
-
const serverPubkey = xOnly(
|
|
1787
|
-
const network = (0,
|
|
1788
|
-
const
|
|
1789
|
-
solverPubkey: xOnly(
|
|
2174
|
+
const serverPubkey = xOnly(import_base10.hex.decode(info.signerPubkey), "ark signer key");
|
|
2175
|
+
const network = (0, import_sdk9.getNetwork)(info.network);
|
|
2176
|
+
const treeParams = {
|
|
2177
|
+
solverPubkey: xOnly(import_base10.hex.decode(quote.solver_pubkey), "solver key"),
|
|
1790
2178
|
refundLocktime: quote.refund_locktime,
|
|
1791
2179
|
serverPubkey,
|
|
1792
2180
|
paymentHash: params.invoice.paymentHash,
|
|
1793
2181
|
claimDelay: unilateralClaimDelay(Number(info.unilateralExitDelay)),
|
|
1794
2182
|
emulatorPubkey: xOnly(
|
|
1795
|
-
|
|
2183
|
+
import_base10.hex.decode((0, import_sdk9.resolveEmulatorPubkey)(network, params.emulatorPubkey)),
|
|
1796
2184
|
"emulator signer key"
|
|
1797
2185
|
),
|
|
1798
2186
|
senderPubkey,
|
|
1799
2187
|
receiverPkScript: solverHex(receiverPkScriptHex, "profile.receiver_pk_script"),
|
|
1800
|
-
refundPkScript:
|
|
1801
|
-
}
|
|
2188
|
+
refundPkScript: import_sdk9.ArkAddress.decode(refundAddress).pkScript
|
|
2189
|
+
};
|
|
2190
|
+
const script = lightningSendVtxoScript(treeParams);
|
|
1802
2191
|
const address = script.address(network.hrp, serverPubkey).encode();
|
|
1803
2192
|
verifyLockupAddress(quote, address);
|
|
1804
2193
|
assertFundable({
|
|
@@ -1818,7 +2207,8 @@ async function requestLightningSend(wallet, arkServerUrl, transport, params) {
|
|
|
1818
2207
|
script,
|
|
1819
2208
|
refundAddress,
|
|
1820
2209
|
senderPubkey,
|
|
1821
|
-
secrets
|
|
2210
|
+
secrets,
|
|
2211
|
+
treeParams
|
|
1822
2212
|
};
|
|
1823
2213
|
}
|
|
1824
2214
|
var offerTermsFromQuote = (quote, assets) => {
|
|
@@ -1837,9 +2227,9 @@ var onchainSendRequest = (input) => ({
|
|
|
1837
2227
|
amount: input.amount,
|
|
1838
2228
|
profile: {
|
|
1839
2229
|
payment_hash: input.paymentHash,
|
|
1840
|
-
payout_pubkey:
|
|
2230
|
+
payout_pubkey: import_base10.hex.encode(input.payoutPubkey),
|
|
1841
2231
|
refund_address: input.refundAddress,
|
|
1842
|
-
client_refund_pubkey:
|
|
2232
|
+
client_refund_pubkey: import_base10.hex.encode(input.senderPubkey)
|
|
1843
2233
|
}
|
|
1844
2234
|
});
|
|
1845
2235
|
var lightningReceiveRequest = (input) => ({
|
|
@@ -1852,7 +2242,7 @@ var lightningReceiveRequest = (input) => ({
|
|
|
1852
2242
|
profile: {
|
|
1853
2243
|
payment_hash: input.paymentHash,
|
|
1854
2244
|
payout_address: input.payoutAddress,
|
|
1855
|
-
payout_pubkey:
|
|
2245
|
+
payout_pubkey: import_base10.hex.encode(input.payoutPubkey),
|
|
1856
2246
|
claim_packet: input.claimPacket
|
|
1857
2247
|
}
|
|
1858
2248
|
});
|
|
@@ -1866,9 +2256,9 @@ var onchainReceiveRequest = (input) => ({
|
|
|
1866
2256
|
profile: {
|
|
1867
2257
|
payment_hash: input.paymentHash,
|
|
1868
2258
|
claim_packet: input.claimPacket,
|
|
1869
|
-
refund_pubkey:
|
|
2259
|
+
refund_pubkey: import_base10.hex.encode(input.refundPubkey),
|
|
1870
2260
|
payout_address: input.payoutAddress,
|
|
1871
|
-
payout_pubkey:
|
|
2261
|
+
payout_pubkey: import_base10.hex.encode(input.payoutPubkey)
|
|
1872
2262
|
}
|
|
1873
2263
|
});
|
|
1874
2264
|
function deriveOnchainSend(input) {
|
|
@@ -1884,7 +2274,7 @@ function deriveOnchainSend(input) {
|
|
|
1884
2274
|
throw new Error("onchain-send quote is missing a binding field");
|
|
1885
2275
|
}
|
|
1886
2276
|
const script = lightningSendVtxoScript({
|
|
1887
|
-
solverPubkey: xOnly(
|
|
2277
|
+
solverPubkey: xOnly(import_base10.hex.decode(quote.solver_pubkey), "solver key"),
|
|
1888
2278
|
refundLocktime,
|
|
1889
2279
|
serverPubkey: input.serverPubkey,
|
|
1890
2280
|
paymentHash: input.paymentHash,
|
|
@@ -1892,25 +2282,25 @@ function deriveOnchainSend(input) {
|
|
|
1892
2282
|
emulatorPubkey: input.emulatorPubkey,
|
|
1893
2283
|
senderPubkey: input.senderPubkey,
|
|
1894
2284
|
receiverPkScript: solverHex(receiverPkScriptHex, "profile.receiver_pk_script"),
|
|
1895
|
-
refundPkScript:
|
|
2285
|
+
refundPkScript: import_sdk9.ArkAddress.decode(input.refundAddress).pkScript
|
|
1896
2286
|
});
|
|
1897
2287
|
const address = script.address(input.hrp, input.serverPubkey).encode();
|
|
1898
2288
|
verifyLockupAddress(quote, address);
|
|
1899
|
-
const
|
|
1900
|
-
|
|
1901
|
-
|
|
1902
|
-
|
|
1903
|
-
|
|
1904
|
-
|
|
1905
|
-
|
|
1906
|
-
input.l1Network
|
|
1907
|
-
);
|
|
2289
|
+
const htlcParams = {
|
|
2290
|
+
paymentHash: input.paymentHash,
|
|
2291
|
+
claimKey: input.payoutPubkey,
|
|
2292
|
+
refundKey: xOnly(import_base10.hex.decode(htlcPubkey), "solver L1 htlc key"),
|
|
2293
|
+
refundLocktime: htlcLocktime
|
|
2294
|
+
};
|
|
2295
|
+
const htlc = onchainHtlcScript(htlcParams, input.l1Network);
|
|
1908
2296
|
if (htlc.address !== htlcAddress) throw new AddressMismatch(htlc.address, htlcAddress);
|
|
1909
2297
|
return {
|
|
1910
2298
|
address,
|
|
1911
2299
|
swapPkScript: script.pkScript,
|
|
1912
2300
|
script,
|
|
1913
2301
|
htlc,
|
|
2302
|
+
htlcParams,
|
|
2303
|
+
l1Network: input.l1Network,
|
|
1914
2304
|
refundLocktime,
|
|
1915
2305
|
htlcLocktime,
|
|
1916
2306
|
minConfirmations
|
|
@@ -1918,16 +2308,16 @@ function deriveOnchainSend(input) {
|
|
|
1918
2308
|
}
|
|
1919
2309
|
async function requestOnchainSend(wallet, arkServerUrl, transport, params) {
|
|
1920
2310
|
const rfqId = params.rfqId ?? newRfqId();
|
|
1921
|
-
const secrets = await (0,
|
|
2311
|
+
const secrets = await (0, import_sdk10.provisionClaimSecret)(wallet, { preimage: params.preimage });
|
|
1922
2312
|
if (secrets.mustPersistPreimage) {
|
|
1923
2313
|
console.warn(
|
|
1924
2314
|
"[swap] this swap's preimage cannot be re-derived from the seed and MUST be persisted with the record before funding"
|
|
1925
2315
|
);
|
|
1926
2316
|
}
|
|
1927
|
-
const paymentHash =
|
|
2317
|
+
const paymentHash = import_base10.hex.encode(secrets.paymentHash);
|
|
1928
2318
|
const senderPubkey = secrets.pubkey;
|
|
1929
2319
|
const [info, refundAddress] = await Promise.all([
|
|
1930
|
-
new
|
|
2320
|
+
new import_sdk9.RestArkProvider(arkServerUrl).getInfo(),
|
|
1931
2321
|
wallet.getAddress()
|
|
1932
2322
|
]);
|
|
1933
2323
|
const quote = await transport.requestQuote(
|
|
@@ -1941,14 +2331,14 @@ async function requestOnchainSend(wallet, arkServerUrl, transport, params) {
|
|
|
1941
2331
|
amountSide: params.amountSide
|
|
1942
2332
|
})
|
|
1943
2333
|
);
|
|
1944
|
-
const network = (0,
|
|
2334
|
+
const network = (0, import_sdk9.getNetwork)(info.network);
|
|
1945
2335
|
const derived = deriveOnchainSend({
|
|
1946
2336
|
quote,
|
|
1947
2337
|
paymentHash,
|
|
1948
2338
|
payoutPubkey: params.payoutPubkey,
|
|
1949
|
-
serverPubkey: xOnly(
|
|
2339
|
+
serverPubkey: xOnly(import_base10.hex.decode(info.signerPubkey), "ark signer key"),
|
|
1950
2340
|
emulatorPubkey: xOnly(
|
|
1951
|
-
|
|
2341
|
+
import_base10.hex.decode((0, import_sdk9.resolveEmulatorPubkey)(network, params.emulatorPubkey)),
|
|
1952
2342
|
"emulator signer key"
|
|
1953
2343
|
),
|
|
1954
2344
|
claimDelay: unilateralClaimDelay(Number(info.unilateralExitDelay)),
|
|
@@ -1980,6 +2370,9 @@ async function requestOnchainSend(wallet, arkServerUrl, transport, params) {
|
|
|
1980
2370
|
script: derived.script,
|
|
1981
2371
|
refundAddress,
|
|
1982
2372
|
htlc: derived.htlc,
|
|
2373
|
+
htlcParams: derived.htlcParams,
|
|
2374
|
+
l1Network: derived.l1Network,
|
|
2375
|
+
minConfirmations: derived.minConfirmations,
|
|
1983
2376
|
senderPubkey,
|
|
1984
2377
|
secrets
|
|
1985
2378
|
};
|
|
@@ -2056,11 +2449,11 @@ function receiveVtxoScript(params) {
|
|
|
2056
2449
|
type: "seconds",
|
|
2057
2450
|
value: BigInt(value)
|
|
2058
2451
|
});
|
|
2059
|
-
return new
|
|
2452
|
+
return new import_sdk9.VHTLC.ScriptV2({
|
|
2060
2453
|
sender: params.solverPubkey,
|
|
2061
2454
|
receiver: params.payoutPubkey,
|
|
2062
2455
|
server: params.serverPubkey,
|
|
2063
|
-
preimageHash: (0, import_legacy2.ripemd160)(
|
|
2456
|
+
preimageHash: (0, import_legacy2.ripemd160)(import_base10.hex.decode(params.paymentHash)),
|
|
2064
2457
|
refundLocktime: BigInt(params.refundLocktime),
|
|
2065
2458
|
unilateralClaimDelay: seconds(params.claimDelay),
|
|
2066
2459
|
unilateralRefundDelay: seconds(unilateralRefundDelay(params.claimDelay)),
|
|
@@ -2086,8 +2479,8 @@ function deriveLightningReceive(input) {
|
|
|
2086
2479
|
if (refundLocktime === void 0 || invoice === void 0 || solverRefundPkScriptHex === void 0) {
|
|
2087
2480
|
throw new Error("lightning-receive quote is missing a binding field");
|
|
2088
2481
|
}
|
|
2089
|
-
const
|
|
2090
|
-
solverPubkey: xOnly(
|
|
2482
|
+
const treeParams = {
|
|
2483
|
+
solverPubkey: xOnly(import_base10.hex.decode(quote.solver_pubkey), "solver key"),
|
|
2091
2484
|
refundLocktime,
|
|
2092
2485
|
serverPubkey: input.serverPubkey,
|
|
2093
2486
|
paymentHash: input.paymentHash,
|
|
@@ -2095,25 +2488,26 @@ function deriveLightningReceive(input) {
|
|
|
2095
2488
|
emulatorPubkey: input.emulatorPubkey,
|
|
2096
2489
|
solverRefundPkScript: solverHex(solverRefundPkScriptHex, "profile.solver_refund_pk_script"),
|
|
2097
2490
|
payoutPubkey: input.payoutPubkey,
|
|
2098
|
-
payoutPkScript:
|
|
2099
|
-
}
|
|
2491
|
+
payoutPkScript: import_sdk9.ArkAddress.decode(input.payoutAddress).pkScript
|
|
2492
|
+
};
|
|
2493
|
+
const script = receiveVtxoScript(treeParams);
|
|
2100
2494
|
const address = script.address(input.hrp, input.serverPubkey).encode();
|
|
2101
2495
|
verifyLockupAddress(quote, address);
|
|
2102
|
-
return { address, swapPkScript: script.pkScript, script, invoice, refundLocktime };
|
|
2496
|
+
return { address, swapPkScript: script.pkScript, script, invoice, refundLocktime, treeParams };
|
|
2103
2497
|
}
|
|
2104
2498
|
async function requestLightningReceive(wallet, arkServerUrl, transport, params) {
|
|
2105
2499
|
const rfqId = params.rfqId ?? newRfqId();
|
|
2106
|
-
const secrets = await (0,
|
|
2500
|
+
const secrets = await (0, import_sdk10.provisionClaimSecret)(wallet);
|
|
2107
2501
|
if (secrets.mustPersistPreimage) {
|
|
2108
2502
|
console.warn(
|
|
2109
2503
|
"[swap] this swap's preimage cannot be re-derived from the seed and MUST be persisted with the record before paying"
|
|
2110
2504
|
);
|
|
2111
2505
|
}
|
|
2112
2506
|
const preimage = secrets.preimage;
|
|
2113
|
-
const paymentHash =
|
|
2507
|
+
const paymentHash = import_base10.hex.encode(secrets.paymentHash);
|
|
2114
2508
|
const payoutPubkey = secrets.pubkey;
|
|
2115
2509
|
const [info, payoutAddress] = await Promise.all([
|
|
2116
|
-
new
|
|
2510
|
+
new import_sdk9.RestArkProvider(arkServerUrl).getInfo(),
|
|
2117
2511
|
wallet.getAddress()
|
|
2118
2512
|
]);
|
|
2119
2513
|
const claimPacket = await sealClaimPacket({
|
|
@@ -2132,15 +2526,15 @@ async function requestLightningReceive(wallet, arkServerUrl, transport, params)
|
|
|
2132
2526
|
})
|
|
2133
2527
|
);
|
|
2134
2528
|
assertQuotedAmount(quote, params.amountSide, params.amount);
|
|
2135
|
-
const network = (0,
|
|
2529
|
+
const network = (0, import_sdk9.getNetwork)(info.network);
|
|
2136
2530
|
const derived = deriveLightningReceive({
|
|
2137
2531
|
quote,
|
|
2138
2532
|
paymentHash,
|
|
2139
2533
|
payoutPubkey,
|
|
2140
2534
|
payoutAddress,
|
|
2141
|
-
serverPubkey: xOnly(
|
|
2535
|
+
serverPubkey: xOnly(import_base10.hex.decode(info.signerPubkey), "ark signer key"),
|
|
2142
2536
|
emulatorPubkey: xOnly(
|
|
2143
|
-
|
|
2537
|
+
import_base10.hex.decode((0, import_sdk9.resolveEmulatorPubkey)(network, params.emulatorPubkey)),
|
|
2144
2538
|
"emulator signer key"
|
|
2145
2539
|
),
|
|
2146
2540
|
claimDelay: unilateralClaimDelay(Number(info.unilateralExitDelay)),
|
|
@@ -2171,7 +2565,8 @@ async function requestLightningReceive(wallet, arkServerUrl, transport, params)
|
|
|
2171
2565
|
script: derived.script,
|
|
2172
2566
|
payoutAddress,
|
|
2173
2567
|
payoutPubkey,
|
|
2174
|
-
secrets
|
|
2568
|
+
secrets,
|
|
2569
|
+
treeParams: derived.treeParams
|
|
2175
2570
|
};
|
|
2176
2571
|
}
|
|
2177
2572
|
function deriveOnchainReceive(input) {
|
|
@@ -2187,7 +2582,7 @@ function deriveOnchainReceive(input) {
|
|
|
2187
2582
|
throw new Error("onchain-receive quote is missing a binding field");
|
|
2188
2583
|
}
|
|
2189
2584
|
const script = receiveVtxoScript({
|
|
2190
|
-
solverPubkey: xOnly(
|
|
2585
|
+
solverPubkey: xOnly(import_base10.hex.decode(quote.solver_pubkey), "solver key"),
|
|
2191
2586
|
refundLocktime,
|
|
2192
2587
|
serverPubkey: input.serverPubkey,
|
|
2193
2588
|
paymentHash: input.paymentHash,
|
|
@@ -2195,14 +2590,14 @@ function deriveOnchainReceive(input) {
|
|
|
2195
2590
|
emulatorPubkey: input.emulatorPubkey,
|
|
2196
2591
|
solverRefundPkScript: solverHex(solverRefundPkScriptHex, "profile.solver_refund_pk_script"),
|
|
2197
2592
|
payoutPubkey: input.payoutPubkey,
|
|
2198
|
-
payoutPkScript:
|
|
2593
|
+
payoutPkScript: import_sdk9.ArkAddress.decode(input.payoutAddress).pkScript
|
|
2199
2594
|
});
|
|
2200
2595
|
const address = script.address(input.hrp, input.serverPubkey).encode();
|
|
2201
2596
|
verifyLockupAddress(quote, address);
|
|
2202
2597
|
const htlc = onchainHtlcScript(
|
|
2203
2598
|
{
|
|
2204
2599
|
paymentHash: input.paymentHash,
|
|
2205
|
-
claimKey: xOnly(
|
|
2600
|
+
claimKey: xOnly(import_base10.hex.decode(claimPubkey), "solver L1 claim key"),
|
|
2206
2601
|
refundKey: input.refundPubkey,
|
|
2207
2602
|
refundLocktime: htlcLocktime
|
|
2208
2603
|
},
|
|
@@ -2221,17 +2616,17 @@ function deriveOnchainReceive(input) {
|
|
|
2221
2616
|
}
|
|
2222
2617
|
async function requestOnchainReceive(wallet, arkServerUrl, transport, params) {
|
|
2223
2618
|
const rfqId = params.rfqId ?? newRfqId();
|
|
2224
|
-
const secrets = await (0,
|
|
2619
|
+
const secrets = await (0, import_sdk10.provisionClaimSecret)(wallet);
|
|
2225
2620
|
if (secrets.mustPersistPreimage) {
|
|
2226
2621
|
console.warn(
|
|
2227
2622
|
"[swap] this swap's preimage cannot be re-derived from the seed and MUST be persisted with the record before funding"
|
|
2228
2623
|
);
|
|
2229
2624
|
}
|
|
2230
2625
|
const preimage = secrets.preimage;
|
|
2231
|
-
const paymentHash =
|
|
2626
|
+
const paymentHash = import_base10.hex.encode(secrets.paymentHash);
|
|
2232
2627
|
const payoutPubkey = secrets.pubkey;
|
|
2233
2628
|
const [info, payoutAddress] = await Promise.all([
|
|
2234
|
-
new
|
|
2629
|
+
new import_sdk9.RestArkProvider(arkServerUrl).getInfo(),
|
|
2235
2630
|
wallet.getAddress()
|
|
2236
2631
|
]);
|
|
2237
2632
|
const claimPacket = await sealClaimPacket({
|
|
@@ -2251,16 +2646,16 @@ async function requestOnchainReceive(wallet, arkServerUrl, transport, params) {
|
|
|
2251
2646
|
})
|
|
2252
2647
|
);
|
|
2253
2648
|
assertQuotedAmount(quote, params.amountSide, params.amount);
|
|
2254
|
-
const network = (0,
|
|
2649
|
+
const network = (0, import_sdk9.getNetwork)(info.network);
|
|
2255
2650
|
const derived = deriveOnchainReceive({
|
|
2256
2651
|
quote,
|
|
2257
2652
|
paymentHash,
|
|
2258
2653
|
payoutPubkey,
|
|
2259
2654
|
payoutAddress,
|
|
2260
2655
|
refundPubkey: params.refundPubkey,
|
|
2261
|
-
serverPubkey: xOnly(
|
|
2656
|
+
serverPubkey: xOnly(import_base10.hex.decode(info.signerPubkey), "ark signer key"),
|
|
2262
2657
|
emulatorPubkey: xOnly(
|
|
2263
|
-
|
|
2658
|
+
import_base10.hex.decode((0, import_sdk9.resolveEmulatorPubkey)(network, params.emulatorPubkey)),
|
|
2264
2659
|
"emulator signer key"
|
|
2265
2660
|
),
|
|
2266
2661
|
claimDelay: unilateralClaimDelay(Number(info.unilateralExitDelay)),
|
|
@@ -2297,15 +2692,15 @@ async function requestOnchainReceive(wallet, arkServerUrl, transport, params) {
|
|
|
2297
2692
|
}
|
|
2298
2693
|
|
|
2299
2694
|
// src/claim.ts
|
|
2300
|
-
var
|
|
2695
|
+
var import_base12 = require("@scure/base");
|
|
2301
2696
|
var import_legacy3 = require("@noble/hashes/legacy.js");
|
|
2302
2697
|
var import_sha25 = require("@noble/hashes/sha2.js");
|
|
2303
|
-
var
|
|
2698
|
+
var import_sdk12 = require("@arkade-os/sdk");
|
|
2304
2699
|
|
|
2305
2700
|
// src/refund.ts
|
|
2306
|
-
var
|
|
2701
|
+
var import_base11 = require("@scure/base");
|
|
2307
2702
|
var import_sha24 = require("@noble/hashes/sha2.js");
|
|
2308
|
-
var
|
|
2703
|
+
var import_sdk11 = require("@arkade-os/sdk");
|
|
2309
2704
|
var sleep2 = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
2310
2705
|
var isRfqTerminal = (state) => RFQ_TERMINAL_STATES.includes(state);
|
|
2311
2706
|
var RFQ_RESOLVED_STATES = ["settled", "refunded"];
|
|
@@ -2352,7 +2747,7 @@ var LockupNeedsRecoveryError = class extends Error {
|
|
|
2352
2747
|
}
|
|
2353
2748
|
};
|
|
2354
2749
|
async function findLockupVtxos(indexer, swapPkScript) {
|
|
2355
|
-
const scripts = [
|
|
2750
|
+
const scripts = [import_base11.hex.encode(swapPkScript)];
|
|
2356
2751
|
const [spendable, recoverable] = await Promise.all([
|
|
2357
2752
|
indexer.getVtxos({ scripts, spendableOnly: true }),
|
|
2358
2753
|
indexer.getVtxos({ scripts, recoverableOnly: true })
|
|
@@ -2377,28 +2772,33 @@ async function findLockupVtxos(indexer, swapPkScript) {
|
|
|
2377
2772
|
}
|
|
2378
2773
|
return out;
|
|
2379
2774
|
}
|
|
2380
|
-
var hashesTo = (candidate, paymentHash) =>
|
|
2775
|
+
var hashesTo = (candidate, paymentHash) => import_base11.hex.encode((0, import_sha24.sha256)(candidate)) === paymentHash;
|
|
2381
2776
|
var candidateWitnessItems = (tx, inputIndex) => [
|
|
2382
|
-
...(0,
|
|
2777
|
+
...(0, import_sdk11.getArkPsbtFields)(tx, inputIndex, import_sdk11.ConditionWitness).flat(),
|
|
2383
2778
|
...tx.getInput(inputIndex).finalScriptWitness ?? []
|
|
2384
2779
|
];
|
|
2385
2780
|
async function readLockupFate(indexer, input) {
|
|
2386
|
-
const { vtxos } = await indexer.getVtxos({ scripts: [
|
|
2781
|
+
const { vtxos } = await indexer.getVtxos({ scripts: [import_base11.hex.encode(input.swapPkScript)] });
|
|
2387
2782
|
const all = vtxos ?? [];
|
|
2388
2783
|
if (all.length === 0) return { fate: "unknown" };
|
|
2389
|
-
const spentBy = /* @__PURE__ */ new
|
|
2784
|
+
const spentBy = /* @__PURE__ */ new Map();
|
|
2390
2785
|
let everySpendNamed = true;
|
|
2391
2786
|
for (const vtxo of all) {
|
|
2392
2787
|
if (!vtxo.isSpent && !vtxo.spentBy && !vtxo.settledBy) return { fate: "open" };
|
|
2393
|
-
if (vtxo.spentBy)
|
|
2788
|
+
if (vtxo.spentBy)
|
|
2789
|
+
spentBy.set(vtxo.spentBy, {
|
|
2790
|
+
checkpointTxid: vtxo.spentBy,
|
|
2791
|
+
arkTxid: vtxo.arkTxId
|
|
2792
|
+
});
|
|
2394
2793
|
else everySpendNamed = false;
|
|
2395
2794
|
}
|
|
2396
|
-
const
|
|
2795
|
+
const spends = [...spentBy.values()];
|
|
2796
|
+
const { txs } = await indexer.getVirtualTxs([...spentBy.keys()]);
|
|
2397
2797
|
const observed = /* @__PURE__ */ new Set();
|
|
2398
2798
|
for (const raw of txs) {
|
|
2399
2799
|
let tx;
|
|
2400
2800
|
try {
|
|
2401
|
-
tx =
|
|
2801
|
+
tx = import_sdk11.Transaction.fromPSBT(import_base11.base64.decode(raw));
|
|
2402
2802
|
} catch {
|
|
2403
2803
|
continue;
|
|
2404
2804
|
}
|
|
@@ -2406,16 +2806,16 @@ async function readLockupFate(indexer, input) {
|
|
|
2406
2806
|
for (let i = 0; i < tx.inputsLength; i++) {
|
|
2407
2807
|
const spent = tx.getInput(i);
|
|
2408
2808
|
if (!spent.txid) continue;
|
|
2409
|
-
const txid =
|
|
2809
|
+
const txid = import_base11.hex.encode(spent.txid);
|
|
2410
2810
|
if (!all.some((vtxo) => vtxo.txid === txid && vtxo.vout === spent.index)) continue;
|
|
2411
2811
|
for (const candidate of candidateWitnessItems(tx, i)) {
|
|
2412
2812
|
if (hashesTo(candidate, input.paymentHash)) {
|
|
2413
|
-
return { fate: "claimed", preimage: candidate };
|
|
2813
|
+
return { fate: "claimed", preimage: candidate, spends };
|
|
2414
2814
|
}
|
|
2415
2815
|
}
|
|
2416
2816
|
}
|
|
2417
2817
|
}
|
|
2418
|
-
return everySpendNamed && observed.size === spentBy.size ? { fate: "returned" } : { fate: "unknown" };
|
|
2818
|
+
return everySpendNamed && observed.size === spentBy.size ? { fate: "returned", spends } : { fate: "unknown" };
|
|
2419
2819
|
}
|
|
2420
2820
|
async function pushRefundWithoutReceiver(ark, input) {
|
|
2421
2821
|
if (input.vtxos.length === 0) throw new Error("nothing to refund: no funded outputs");
|
|
@@ -2435,14 +2835,14 @@ async function pushRefundWithoutReceiver(ark, input) {
|
|
|
2435
2835
|
const info = await ark.getInfo();
|
|
2436
2836
|
let serverUnrollScript;
|
|
2437
2837
|
try {
|
|
2438
|
-
serverUnrollScript =
|
|
2838
|
+
serverUnrollScript = import_sdk11.CSVMultisigTapscript.decode(import_base11.hex.decode(info.checkpointTapscript));
|
|
2439
2839
|
} catch {
|
|
2440
2840
|
throw new Error("invalid checkpointTapscript from the Arkade server");
|
|
2441
2841
|
}
|
|
2442
2842
|
const leaf = input.script.refundWithoutReceiver();
|
|
2443
2843
|
const tapTree = input.script.encode();
|
|
2444
2844
|
const amount = input.vtxos.reduce((sum, vtxo) => sum + vtxo.value, 0);
|
|
2445
|
-
const { arkTx, checkpoints } = (0,
|
|
2845
|
+
const { arkTx, checkpoints } = (0, import_sdk11.buildOffchainTx)(
|
|
2446
2846
|
input.vtxos.map((vtxo) => ({
|
|
2447
2847
|
txid: vtxo.txid,
|
|
2448
2848
|
vout: vtxo.vout,
|
|
@@ -2455,18 +2855,18 @@ async function pushRefundWithoutReceiver(ark, input) {
|
|
|
2455
2855
|
);
|
|
2456
2856
|
const signedArkTx = await input.sender.sign(arkTx);
|
|
2457
2857
|
const submitted = await ark.submitTx(
|
|
2458
|
-
|
|
2459
|
-
checkpoints.map((c) =>
|
|
2858
|
+
import_base11.base64.encode(signedArkTx.toPSBT()),
|
|
2859
|
+
checkpoints.map((c) => import_base11.base64.encode(c.toPSBT()))
|
|
2460
2860
|
);
|
|
2461
|
-
(0,
|
|
2462
|
-
const matched = (0,
|
|
2861
|
+
(0, import_sdk11.assertSubmittedArkTxid)(submitted, signedArkTx, "refundWithoutReceiver");
|
|
2862
|
+
const matched = (0, import_sdk11.matchServerCheckpoints)(
|
|
2463
2863
|
submitted.signedCheckpointTxs,
|
|
2464
2864
|
checkpoints,
|
|
2465
2865
|
"refundWithoutReceiver"
|
|
2466
2866
|
);
|
|
2467
2867
|
const finalCheckpoints = await Promise.all(
|
|
2468
2868
|
matched.map(
|
|
2469
|
-
async ({ server }) =>
|
|
2869
|
+
async ({ server }) => import_base11.base64.encode((await input.sender.sign(server, [0])).toPSBT())
|
|
2470
2870
|
)
|
|
2471
2871
|
);
|
|
2472
2872
|
await ark.finalizeTx(submitted.arkTxid, finalCheckpoints);
|
|
@@ -2546,20 +2946,20 @@ async function pushClaim(ark, input) {
|
|
|
2546
2946
|
}
|
|
2547
2947
|
}
|
|
2548
2948
|
const committed = input.script.options.preimageHash;
|
|
2549
|
-
if (
|
|
2949
|
+
if (import_base12.hex.encode((0, import_legacy3.ripemd160)((0, import_sha25.sha256)(input.preimage))) !== import_base12.hex.encode(committed)) {
|
|
2550
2950
|
throw new Error("preimage does not match the covenant's payment hash");
|
|
2551
2951
|
}
|
|
2552
2952
|
const info = await ark.getInfo();
|
|
2553
2953
|
let serverUnrollScript;
|
|
2554
2954
|
try {
|
|
2555
|
-
serverUnrollScript =
|
|
2955
|
+
serverUnrollScript = import_sdk12.CSVMultisigTapscript.decode(import_base12.hex.decode(info.checkpointTapscript));
|
|
2556
2956
|
} catch {
|
|
2557
2957
|
throw new Error("invalid checkpointTapscript from the Arkade server");
|
|
2558
2958
|
}
|
|
2559
2959
|
const leaf = input.script.claim();
|
|
2560
2960
|
const tapTree = input.script.encode();
|
|
2561
|
-
const arkTxid = await (0,
|
|
2562
|
-
identity: (0,
|
|
2961
|
+
const arkTxid = await (0, import_sdk12.signAndSubmitOffchainTx)({
|
|
2962
|
+
identity: (0, import_sdk12.claimWithPreimageIdentity)(input.receiver, input.preimage),
|
|
2563
2963
|
provider: ark,
|
|
2564
2964
|
inputs: input.vtxos.map((vtxo) => ({
|
|
2565
2965
|
txid: vtxo.txid,
|
|
@@ -2606,7 +3006,7 @@ async function claimReceiveLockup(indexer, ark, input) {
|
|
|
2606
3006
|
}
|
|
2607
3007
|
|
|
2608
3008
|
// src/refundBlocked.ts
|
|
2609
|
-
var
|
|
3009
|
+
var import_sdk13 = require("@arkade-os/sdk");
|
|
2610
3010
|
var RefundNotLocallyPossibleError = class extends Error {
|
|
2611
3011
|
constructor(reason, message, options) {
|
|
2612
3012
|
super(message, options);
|
|
@@ -2623,16 +3023,16 @@ async function senderIdentityForSwapRecord(wallet, record) {
|
|
|
2623
3023
|
);
|
|
2624
3024
|
}
|
|
2625
3025
|
try {
|
|
2626
|
-
return await (0,
|
|
3026
|
+
return await (0, import_sdk13.contractSigner)(wallet, record.signingDescriptor);
|
|
2627
3027
|
} catch (cause) {
|
|
2628
|
-
if (cause instanceof
|
|
3028
|
+
if (cause instanceof import_sdk13.WalletCannotSignError) {
|
|
2629
3029
|
throw new RefundNotLocallyPossibleError(
|
|
2630
3030
|
"unsignable-wallet",
|
|
2631
3031
|
`this wallet holds ${record.signingDescriptor} but cannot sign with it; attach its signer`,
|
|
2632
3032
|
{ cause }
|
|
2633
3033
|
);
|
|
2634
3034
|
}
|
|
2635
|
-
if (cause instanceof
|
|
3035
|
+
if (cause instanceof import_sdk13.ForeignDescriptorError) {
|
|
2636
3036
|
throw new RefundNotLocallyPossibleError(
|
|
2637
3037
|
"foreign-descriptor",
|
|
2638
3038
|
`this wallet cannot derive ${record.signingDescriptor}; the swap was created on another wallet`,
|
|
@@ -2643,10 +3043,31 @@ async function senderIdentityForSwapRecord(wallet, record) {
|
|
|
2643
3043
|
}
|
|
2644
3044
|
}
|
|
2645
3045
|
|
|
3046
|
+
// src/arkadeRefunder.ts
|
|
3047
|
+
function arkadeRefunder(deps) {
|
|
3048
|
+
return async (swap) => {
|
|
3049
|
+
const script = swap.lockup?.script;
|
|
3050
|
+
if (!script) {
|
|
3051
|
+
throw new Error(
|
|
3052
|
+
`swap ${swap.rfqId} carries no lockup covenant, so its refund cannot be built`
|
|
3053
|
+
);
|
|
3054
|
+
}
|
|
3055
|
+
const vtxos = await findLockupVtxos(deps.indexer, swap.lockupPkScript);
|
|
3056
|
+
if (vtxos.length === 0) return null;
|
|
3057
|
+
const record = await deps.repository.getRfqSwap(swap.rfqId);
|
|
3058
|
+
if (!record) {
|
|
3059
|
+
throw new RefundNotLocallyPossibleError(
|
|
3060
|
+
"no-secrets",
|
|
3061
|
+
`no stored record for ${swap.rfqId}; the descriptor that signs its refund lives there`
|
|
3062
|
+
);
|
|
3063
|
+
}
|
|
3064
|
+
const sender = await senderIdentityForSwapRecord(deps.wallet, rfqSignerOf(record) ?? {});
|
|
3065
|
+
return pushRefundWithoutReceiver(deps.ark, { script, sender, vtxos });
|
|
3066
|
+
};
|
|
3067
|
+
}
|
|
3068
|
+
|
|
2646
3069
|
// src/swapManager.ts
|
|
2647
|
-
var
|
|
2648
|
-
var RFQ_SWAP_TERMINAL_STATES = ["settled", "refunded", "failed"];
|
|
2649
|
-
var isRfqSwapTerminal = (state) => RFQ_SWAP_TERMINAL_STATES.includes(state);
|
|
3070
|
+
var import_base13 = require("@scure/base");
|
|
2650
3071
|
function nextOnchainAction(input) {
|
|
2651
3072
|
switch (input.phase.phase) {
|
|
2652
3073
|
case "unfunded":
|
|
@@ -2662,6 +3083,17 @@ function nextOnchainAction(input) {
|
|
|
2662
3083
|
return input.htlcLocktime - input.now >= ONCHAIN_CLAIM_MARGIN_SECONDS ? "claim" : "claim_window_closed";
|
|
2663
3084
|
}
|
|
2664
3085
|
}
|
|
3086
|
+
var RfqSwapOriginRequired = class extends Error {
|
|
3087
|
+
/** The swap that could not be admitted. */
|
|
3088
|
+
rfqId;
|
|
3089
|
+
constructor(rfqId) {
|
|
3090
|
+
super(
|
|
3091
|
+
`rfq swap ${rfqId} has no stored record and no origin was supplied; pass the request-time origin as addSwap's second argument so its first record can be written`
|
|
3092
|
+
);
|
|
3093
|
+
this.name = "RfqSwapOriginRequired";
|
|
3094
|
+
this.rfqId = rfqId;
|
|
3095
|
+
}
|
|
3096
|
+
};
|
|
2665
3097
|
var notify = (listeners, call) => {
|
|
2666
3098
|
for (const listener of listeners) {
|
|
2667
3099
|
try {
|
|
@@ -2739,7 +3171,20 @@ var RfqSwapManager = class {
|
|
|
2739
3171
|
* answers instead of throwing "not found". Cleared by {@link removeSwap}. */
|
|
2740
3172
|
finished = /* @__PURE__ */ new Map();
|
|
2741
3173
|
waiters = /* @__PURE__ */ new Map();
|
|
2742
|
-
/**
|
|
3174
|
+
/**
|
|
3175
|
+
* The request-time origin of each swap the manager may have to CREATE a
|
|
3176
|
+
* record for, by rfqId.
|
|
3177
|
+
*
|
|
3178
|
+
* Needed only for a swap the store has never seen: once a record exists,
|
|
3179
|
+
* `updateRfqSwapRecord` carries the origin half through and the map is
|
|
3180
|
+
* redundant. It is kept anyway for the swap's whole life, so a record the
|
|
3181
|
+
* store loses between passes is rewritten rather than lost — and dropped
|
|
3182
|
+
* by {@link removeSwap} and by retention, which are the two places a swap
|
|
3183
|
+
* stops being this manager's business.
|
|
3184
|
+
*/
|
|
3185
|
+
origins = /* @__PURE__ */ new Map();
|
|
3186
|
+
/** Records changed during the current pass, flushed to the repository and
|
|
3187
|
+
* through `saveSwap`. */
|
|
2743
3188
|
dirty = /* @__PURE__ */ new Set();
|
|
2744
3189
|
/** Race guard: one action at a time per swap. */
|
|
2745
3190
|
inProgress = /* @__PURE__ */ new Set();
|
|
@@ -2761,7 +3206,9 @@ var RfqSwapManager = class {
|
|
|
2761
3206
|
this.actionExecutedListeners.add(config.events.onActionExecuted);
|
|
2762
3207
|
}
|
|
2763
3208
|
}
|
|
2764
|
-
/** Wire the money-moving half. Without it the manager only watches.
|
|
3209
|
+
/** Wire the money-moving half. Without it the manager only watches. The
|
|
3210
|
+
* two claims may be omitted for a consumer that drives no kind reaching
|
|
3211
|
+
* them — see {@link AvailableRfqSwapManagerCallbacks}. */
|
|
2765
3212
|
setCallbacks(callbacks) {
|
|
2766
3213
|
this.callbacks = callbacks;
|
|
2767
3214
|
}
|
|
@@ -2781,6 +3228,116 @@ var RfqSwapManager = class {
|
|
|
2781
3228
|
this.actionExecutedListeners.add(listener);
|
|
2782
3229
|
return () => this.actionExecutedListeners.delete(listener);
|
|
2783
3230
|
}
|
|
3231
|
+
/**
|
|
3232
|
+
* Rebuild every stored swap and take over monitoring them.
|
|
3233
|
+
*
|
|
3234
|
+
* The composition a consumer otherwise writes by hand, and the one place
|
|
3235
|
+
* all four pieces meet: retention decides what to keep
|
|
3236
|
+
* (`shouldRetainRfqSwap`), the lockup's contract row supplies the covenant
|
|
3237
|
+
* (`lockupContractParams`), `rebuildRfqSwap` turns a record back into a
|
|
3238
|
+
* live swap, and each rebuilt swap arrives with its own origin, so nothing
|
|
3239
|
+
* is asked of the caller.
|
|
3240
|
+
*
|
|
3241
|
+
* **Deliberately not part of {@link start}.** A consumer that wants to
|
|
3242
|
+
* look at its records — count them, show them, prune and stop — is not
|
|
3243
|
+
* forced to start driving money to do it. Call this first and `start()`
|
|
3244
|
+
* after; a manager already running polls the restored swaps at once.
|
|
3245
|
+
*
|
|
3246
|
+
* Retention runs BEFORE the rebuild, so a record past
|
|
3247
|
+
* `RFQ_SWAP_RETENTION_SECONDS` costs no contract lookup on its way to being
|
|
3248
|
+
* dropped.
|
|
3249
|
+
*
|
|
3250
|
+
* **A record that cannot be rebuilt is reported, never swallowed and never
|
|
3251
|
+
* fatal.** `rebuildRfqSwap` throws by design when the covenant params do
|
|
3252
|
+
* not derive the funded address, and `lockupContractParams` throws
|
|
3253
|
+
* `LockupContractMissing` when the wallet has no row for the lockup — both
|
|
3254
|
+
* say something true about that one record, and neither is a reason to
|
|
3255
|
+
* strand the others. They come back in {@link RfqRestoreResult.failed}.
|
|
3256
|
+
*/
|
|
3257
|
+
async restoreFromRepository(options = {}) {
|
|
3258
|
+
const repository = this.requireRepository("restoreFromRepository");
|
|
3259
|
+
const params = options.params ?? this.paramsFromContracts();
|
|
3260
|
+
const records = await repository.getAllRfqSwaps();
|
|
3261
|
+
const pruned = await this.dropRetired(repository, records);
|
|
3262
|
+
const retired = new Set(pruned);
|
|
3263
|
+
const restored = [];
|
|
3264
|
+
const failed = [];
|
|
3265
|
+
for (const record of records) {
|
|
3266
|
+
if (retired.has(record.rfqId)) continue;
|
|
3267
|
+
let swap;
|
|
3268
|
+
try {
|
|
3269
|
+
swap = rebuildRfqSwap(record, await params(record));
|
|
3270
|
+
} catch (error) {
|
|
3271
|
+
failed.push({
|
|
3272
|
+
rfqId: record.rfqId,
|
|
3273
|
+
error: error instanceof Error ? error : new Error(errorMessage(error))
|
|
3274
|
+
});
|
|
3275
|
+
continue;
|
|
3276
|
+
}
|
|
3277
|
+
this.origins.set(record.rfqId, rfqSwapOriginOf(record));
|
|
3278
|
+
if (isRfqSwapTerminal(swap.state)) this.finished.set(swap.rfqId, swap);
|
|
3279
|
+
else this.track(swap);
|
|
3280
|
+
restored.push(swap);
|
|
3281
|
+
}
|
|
3282
|
+
if (this.running) {
|
|
3283
|
+
await Promise.allSettled(
|
|
3284
|
+
restored.filter((swap) => this.monitored.has(swap.rfqId)).map((swap) => this.pollSwap(swap))
|
|
3285
|
+
);
|
|
3286
|
+
}
|
|
3287
|
+
return { restored, failed, pruned };
|
|
3288
|
+
}
|
|
3289
|
+
/**
|
|
3290
|
+
* Drop stored records that are terminal and past
|
|
3291
|
+
* `RFQ_SWAP_RETENTION_SECONDS`, and return their ids.
|
|
3292
|
+
*
|
|
3293
|
+
* `needs_counterparty` is never dropped, however old: the money is still at
|
|
3294
|
+
* the lockup and the counterparty's move still ends the swap. That rule
|
|
3295
|
+
* lives in `shouldRetainRfqSwap`, which this defers to rather than
|
|
3296
|
+
* restating.
|
|
3297
|
+
*
|
|
3298
|
+
* Public because retention is a caller's cadence, not the manager's: a
|
|
3299
|
+
* long-lived process wants it on a timer of its own, a mobile app wants it
|
|
3300
|
+
* at boot. {@link restoreFromRepository} runs it first, so the boot path
|
|
3301
|
+
* needs no separate call.
|
|
3302
|
+
*/
|
|
3303
|
+
async pruneRetiredSwaps() {
|
|
3304
|
+
const repository = this.deps.repository;
|
|
3305
|
+
if (!repository) return [];
|
|
3306
|
+
return this.dropRetired(repository, await repository.getAllRfqSwaps());
|
|
3307
|
+
}
|
|
3308
|
+
async dropRetired(repository, records) {
|
|
3309
|
+
const now = this.config.now();
|
|
3310
|
+
const dropped = [];
|
|
3311
|
+
for (const record of records) {
|
|
3312
|
+
if (shouldRetainRfqSwap(record, now)) continue;
|
|
3313
|
+
await repository.removeRfqSwap(record.rfqId);
|
|
3314
|
+
this.finished.delete(record.rfqId);
|
|
3315
|
+
if (!this.monitored.has(record.rfqId)) this.origins.delete(record.rfqId);
|
|
3316
|
+
dropped.push(record.rfqId);
|
|
3317
|
+
}
|
|
3318
|
+
return dropped;
|
|
3319
|
+
}
|
|
3320
|
+
requireRepository(method) {
|
|
3321
|
+
const repository = this.deps.repository;
|
|
3322
|
+
if (!repository) {
|
|
3323
|
+
throw new Error(
|
|
3324
|
+
`${method} needs a record store; pass one as RfqSwapManagerDeps.repository`
|
|
3325
|
+
);
|
|
3326
|
+
}
|
|
3327
|
+
return repository;
|
|
3328
|
+
}
|
|
3329
|
+
/** The default covenant source: the wallet's own contract row for each
|
|
3330
|
+
* lockup, which is where registration put it before the address could be
|
|
3331
|
+
* funded. */
|
|
3332
|
+
paramsFromContracts() {
|
|
3333
|
+
const contracts = this.deps.contracts;
|
|
3334
|
+
if (!contracts) {
|
|
3335
|
+
throw new Error(
|
|
3336
|
+
"restoreFromRepository needs the covenant parameters: wire RfqSwapManagerDeps.contracts so each lockup's contract row can be read, or pass options.params"
|
|
3337
|
+
);
|
|
3338
|
+
}
|
|
3339
|
+
return (record) => lockupContractParams(contracts, record.lockupAddress);
|
|
3340
|
+
}
|
|
2784
3341
|
/**
|
|
2785
3342
|
* Load records and begin monitoring. Runs one pass immediately — a caller
|
|
2786
3343
|
* resuming after a restart may be well past a deadline already — then
|
|
@@ -2790,8 +3347,17 @@ var RfqSwapManager = class {
|
|
|
2790
3347
|
* Calling it again while running loads the records and returns rather than
|
|
2791
3348
|
* re-arming — dropping them silently would strand a funded swap on a
|
|
2792
3349
|
* caller's harmless double-start.
|
|
3350
|
+
*
|
|
3351
|
+
* The signature is unchanged with a {@link RfqSwapManagerDeps.repository}
|
|
3352
|
+
* wired; the origins are resolved from the store instead, by the same rule
|
|
3353
|
+
* {@link addSwap} applies. A swap the store has never seen throws
|
|
3354
|
+
* {@link RfqSwapOriginRequired} — hand that one to `addSwap` with its
|
|
3355
|
+
* origin, or restore the whole set with {@link restoreFromRepository},
|
|
3356
|
+
* which needs no caller input at all. Every swap is checked before any is
|
|
3357
|
+
* tracked, so a bad one in the list does not leave a half-loaded manager.
|
|
2793
3358
|
*/
|
|
2794
3359
|
async start(swaps = []) {
|
|
3360
|
+
for (const swap of swaps) await this.admit(swap);
|
|
2795
3361
|
for (const swap of swaps) {
|
|
2796
3362
|
if (isRfqSwapTerminal(swap.state)) this.finished.set(swap.rfqId, swap);
|
|
2797
3363
|
else this.track(swap);
|
|
@@ -2823,9 +3389,22 @@ var RfqSwapManager = class {
|
|
|
2823
3389
|
this.unsubscribeContracts?.();
|
|
2824
3390
|
this.unsubscribeContracts = null;
|
|
2825
3391
|
}
|
|
2826
|
-
/**
|
|
2827
|
-
*
|
|
2828
|
-
|
|
3392
|
+
/**
|
|
3393
|
+
* Begin monitoring a swap. Polled immediately when the manager is running,
|
|
3394
|
+
* so a just-funded swap does not wait out a whole interval.
|
|
3395
|
+
*
|
|
3396
|
+
* `origin` is the request-time half a live swap cannot carry — the corridor,
|
|
3397
|
+
* the funded address, the corridor's profile, the funding txid — and it is
|
|
3398
|
+
* what lets the manager write this swap's FIRST record. Supply it whenever
|
|
3399
|
+
* a {@link RfqSwapManagerDeps.repository} is wired and the swap is new. It
|
|
3400
|
+
* may be omitted for a swap the store already holds a record for, which is
|
|
3401
|
+
* then read to confirm it; omitting it for one the store has never seen
|
|
3402
|
+
* throws {@link RfqSwapOriginRequired} rather than admitting a swap whose
|
|
3403
|
+
* record could never be written. With no repository wired the parameter is
|
|
3404
|
+
* inert.
|
|
3405
|
+
*/
|
|
3406
|
+
async addSwap(swap, origin) {
|
|
3407
|
+
await this.admit(swap, origin);
|
|
2829
3408
|
if (isRfqSwapTerminal(swap.state)) {
|
|
2830
3409
|
this.finished.set(swap.rfqId, swap);
|
|
2831
3410
|
return;
|
|
@@ -2833,6 +3412,29 @@ var RfqSwapManager = class {
|
|
|
2833
3412
|
this.track(swap);
|
|
2834
3413
|
if (this.running) await this.pollSwap(swap);
|
|
2835
3414
|
}
|
|
3415
|
+
/**
|
|
3416
|
+
* Settle where this swap's record will come from, before it is monitored.
|
|
3417
|
+
*
|
|
3418
|
+
* Three ways it can be answered, in order: the caller passed an origin, one
|
|
3419
|
+
* is already remembered from an earlier `addSwap`, or the store holds a
|
|
3420
|
+
* record — which is the origin, already written. Only the last costs a read,
|
|
3421
|
+
* and only when the first two are absent.
|
|
3422
|
+
*/
|
|
3423
|
+
async admit(swap, origin) {
|
|
3424
|
+
if (origin) {
|
|
3425
|
+
assertSameSwap(origin, swap);
|
|
3426
|
+
this.origins.set(swap.rfqId, origin);
|
|
3427
|
+
if (this.deps.repository && !isRfqSwapTerminal(swap.state)) {
|
|
3428
|
+
this.dirty.add(swap.rfqId);
|
|
3429
|
+
}
|
|
3430
|
+
return;
|
|
3431
|
+
}
|
|
3432
|
+
const repository = this.deps.repository;
|
|
3433
|
+
if (!repository || this.origins.has(swap.rfqId)) return;
|
|
3434
|
+
const stored = await repository.getRfqSwap(swap.rfqId);
|
|
3435
|
+
if (!stored) throw new RfqSwapOriginRequired(swap.rfqId);
|
|
3436
|
+
this.origins.set(swap.rfqId, rfqSwapOriginOf(stored));
|
|
3437
|
+
}
|
|
2836
3438
|
/** Forget a swap entirely, monitored or finished.
|
|
2837
3439
|
*
|
|
2838
3440
|
* Its contract row is left alone: registration is a wallet-level fact about
|
|
@@ -2850,6 +3452,7 @@ var RfqSwapManager = class {
|
|
|
2850
3452
|
}
|
|
2851
3453
|
this.waiters.delete(rfqId);
|
|
2852
3454
|
this.dirty.delete(rfqId);
|
|
3455
|
+
this.origins.delete(rfqId);
|
|
2853
3456
|
}
|
|
2854
3457
|
/** Every swap still being monitored. */
|
|
2855
3458
|
async getPendingSwaps() {
|
|
@@ -2912,7 +3515,7 @@ var RfqSwapManager = class {
|
|
|
2912
3515
|
// ── internals ────────────────────────────────────────────────────────────
|
|
2913
3516
|
track(swap) {
|
|
2914
3517
|
this.monitored.set(swap.rfqId, swap);
|
|
2915
|
-
this.byLockupScript.set(
|
|
3518
|
+
this.byLockupScript.set(import_base13.hex.encode(swap.lockupPkScript), swap);
|
|
2916
3519
|
}
|
|
2917
3520
|
/** Drops the swap from BOTH indexes. The event index is the one that stops
|
|
2918
3521
|
* a late event finding a swap that is gone; `pollSwap`'s own
|
|
@@ -2921,7 +3524,7 @@ var RfqSwapManager = class {
|
|
|
2921
3524
|
* them from silently re-driving a cancelled swap. */
|
|
2922
3525
|
untrack(rfqId) {
|
|
2923
3526
|
const swap = this.monitored.get(rfqId);
|
|
2924
|
-
if (swap) this.byLockupScript.delete(
|
|
3527
|
+
if (swap) this.byLockupScript.delete(import_base13.hex.encode(swap.lockupPkScript));
|
|
2925
3528
|
this.monitored.delete(rfqId);
|
|
2926
3529
|
this.refundRefused.delete(rfqId);
|
|
2927
3530
|
this.lastClaimError.delete(rfqId);
|
|
@@ -2983,7 +3586,7 @@ var RfqSwapManager = class {
|
|
|
2983
3586
|
if (!lockup) {
|
|
2984
3587
|
try {
|
|
2985
3588
|
const [existing] = await contracts.getContracts({
|
|
2986
|
-
script:
|
|
3589
|
+
script: import_base13.hex.encode(swap.lockupPkScript)
|
|
2987
3590
|
});
|
|
2988
3591
|
if (existing) {
|
|
2989
3592
|
this.registered.set(swap.rfqId, true);
|
|
@@ -3002,13 +3605,13 @@ var RfqSwapManager = class {
|
|
|
3002
3605
|
);
|
|
3003
3606
|
return;
|
|
3004
3607
|
}
|
|
3005
|
-
const script =
|
|
3006
|
-
if (script !==
|
|
3608
|
+
const script = import_base13.hex.encode(lockup.script.pkScript);
|
|
3609
|
+
if (script !== import_base13.hex.encode(swap.lockupPkScript)) {
|
|
3007
3610
|
this.registered.set(swap.rfqId, false);
|
|
3008
3611
|
this.emitFailed(
|
|
3009
3612
|
swap,
|
|
3010
3613
|
new Error(
|
|
3011
|
-
`swap ${swap.rfqId} lockup script ${script} does not match its lockupPkScript ${
|
|
3614
|
+
`swap ${swap.rfqId} lockup script ${script} does not match its lockupPkScript ${import_base13.hex.encode(swap.lockupPkScript)}`
|
|
3012
3615
|
)
|
|
3013
3616
|
);
|
|
3014
3617
|
return;
|
|
@@ -3027,7 +3630,7 @@ var RfqSwapManager = class {
|
|
|
3027
3630
|
* script for its whole life. Best-effort — the swap is over either way. */
|
|
3028
3631
|
retireContract(swap) {
|
|
3029
3632
|
if (!this.deps.contracts || !this.registered.get(swap.rfqId)) return;
|
|
3030
|
-
void this.deps.contracts.setContractWatchState(
|
|
3633
|
+
void this.deps.contracts.setContractWatchState(import_base13.hex.encode(swap.lockupPkScript), "retained").catch((error) => this.emitFailed(swap, error));
|
|
3031
3634
|
}
|
|
3032
3635
|
arm() {
|
|
3033
3636
|
if (!this.running) return;
|
|
@@ -3065,6 +3668,7 @@ var RfqSwapManager = class {
|
|
|
3065
3668
|
fate = { fate: "unknown" };
|
|
3066
3669
|
}
|
|
3067
3670
|
if (fate.fate === "claimed" || fate.fate === "returned") {
|
|
3671
|
+
this.stampLockupSpends(swap, fate.spends);
|
|
3068
3672
|
this.setState(swap, fate.fate === "claimed" ? "settled" : "refunded");
|
|
3069
3673
|
return;
|
|
3070
3674
|
}
|
|
@@ -3149,10 +3753,10 @@ var RfqSwapManager = class {
|
|
|
3149
3753
|
);
|
|
3150
3754
|
}
|
|
3151
3755
|
}
|
|
3152
|
-
if (!this.callbacks) {
|
|
3756
|
+
if (!this.callbacks || !this.callbacks.claimLockup) {
|
|
3153
3757
|
return this.block(
|
|
3154
3758
|
swap,
|
|
3155
|
-
"no callbacks are wired, so this wallet cannot claim the lockup"
|
|
3759
|
+
this.callbacks ? "no claimLockup callback is wired, so this wallet cannot claim the lockup" : "no callbacks are wired, so this wallet cannot claim the lockup"
|
|
3156
3760
|
);
|
|
3157
3761
|
}
|
|
3158
3762
|
this.setState(swap, "claimable");
|
|
@@ -3202,8 +3806,15 @@ var RfqSwapManager = class {
|
|
|
3202
3806
|
});
|
|
3203
3807
|
if (action === "claim" && phase.phase === "claimable") {
|
|
3204
3808
|
if (swap.claimTxid) return "continue";
|
|
3809
|
+
if (this.callbacks && !this.callbacks.claimOnchain) {
|
|
3810
|
+
this.block(
|
|
3811
|
+
swap,
|
|
3812
|
+
"no claimOnchain callback is wired, so this wallet cannot claim the L1 fill"
|
|
3813
|
+
);
|
|
3814
|
+
return "handled";
|
|
3815
|
+
}
|
|
3205
3816
|
this.setOnchainState(swap, "claimable");
|
|
3206
|
-
if (!this.config.enableAutoActions || !this.callbacks) return "handled";
|
|
3817
|
+
if (!this.config.enableAutoActions || !this.callbacks?.claimOnchain) return "handled";
|
|
3207
3818
|
try {
|
|
3208
3819
|
const { txid } = await this.callbacks.claimOnchain(swap, phase.utxo);
|
|
3209
3820
|
swap.claimTxid = txid;
|
|
@@ -3316,6 +3927,24 @@ var RfqSwapManager = class {
|
|
|
3316
3927
|
if (swap.state !== "needs_counterparty") return;
|
|
3317
3928
|
this.setState(swap, traderClaimTxid(swap) ? "claimed" : "pending");
|
|
3318
3929
|
}
|
|
3930
|
+
/**
|
|
3931
|
+
* Record which ark transactions ended the lockup.
|
|
3932
|
+
*
|
|
3933
|
+
* Only the ones the indexer actually named: `LockupSpend.arkTxid` is
|
|
3934
|
+
* optional, and a checkpoint txid is not what history correlates on — a
|
|
3935
|
+
* record carrying one would name a transaction the wallet's own activity
|
|
3936
|
+
* never shows. Fewer txids is the right failure here.
|
|
3937
|
+
*
|
|
3938
|
+
* Assigned rather than merged: the fate is one read of the whole lockup,
|
|
3939
|
+
* so it is the complete answer for this swap, and a swap only reaches a
|
|
3940
|
+
* verdict once.
|
|
3941
|
+
*/
|
|
3942
|
+
stampLockupSpends(swap, spends) {
|
|
3943
|
+
const arkTxids = spends.map((spend) => spend.arkTxid).filter((txid) => txid !== void 0);
|
|
3944
|
+
if (arkTxids.length === 0) return;
|
|
3945
|
+
swap.lockupSpendArkTxids = arkTxids;
|
|
3946
|
+
this.touch(swap);
|
|
3947
|
+
}
|
|
3319
3948
|
touch(swap) {
|
|
3320
3949
|
swap.updatedAt = this.config.now();
|
|
3321
3950
|
this.dirty.add(swap.rfqId);
|
|
@@ -3341,9 +3970,29 @@ var RfqSwapManager = class {
|
|
|
3341
3970
|
emitAction(swap, action) {
|
|
3342
3971
|
notify(this.actionExecutedListeners, (listener) => listener(swap, action));
|
|
3343
3972
|
}
|
|
3344
|
-
/**
|
|
3973
|
+
/**
|
|
3974
|
+
* Flush a changed record to every sink that is wired, and say whether all
|
|
3975
|
+
* of them took it.
|
|
3976
|
+
*
|
|
3977
|
+
* Up to two writes, in this order: the canonical `RfqSwapRecord` to
|
|
3978
|
+
* {@link RfqSwapManagerDeps.repository}, then
|
|
3979
|
+
* {@link RfqSwapManagerCallbacks.saveSwap}. Both gate — a rejection from
|
|
3980
|
+
* either leaves the record dirty and monitored, so waiters stay unsettled
|
|
3981
|
+
* and a terminal swap is not finalized until the write it claims lands.
|
|
3982
|
+
* That is exactly today's rule for `saveSwap`, applied to whichever sinks
|
|
3983
|
+
* exist; wiring the repository does not weaken it.
|
|
3984
|
+
*
|
|
3985
|
+
* The canonical write goes FIRST and a failure there skips the second.
|
|
3986
|
+
* `saveSwap` is a projection of the record, and projecting a state the
|
|
3987
|
+
* record of record has just refused would leave the secondary sink ahead
|
|
3988
|
+
* of the primary — the one ordering that survives no restart.
|
|
3989
|
+
*
|
|
3990
|
+
* With neither wired the state is process-local, which is what a manager
|
|
3991
|
+
* with no callbacks has always done.
|
|
3992
|
+
*/
|
|
3345
3993
|
async save(swap) {
|
|
3346
|
-
if (!this.
|
|
3994
|
+
if (!await this.saveRecord(swap)) return false;
|
|
3995
|
+
if (!this.callbacks?.saveSwap) return true;
|
|
3347
3996
|
try {
|
|
3348
3997
|
await this.callbacks.saveSwap(swap);
|
|
3349
3998
|
return true;
|
|
@@ -3352,6 +4001,25 @@ var RfqSwapManager = class {
|
|
|
3352
4001
|
return false;
|
|
3353
4002
|
}
|
|
3354
4003
|
}
|
|
4004
|
+
/** The canonical write. True when there is no repository to write to. */
|
|
4005
|
+
async saveRecord(swap) {
|
|
4006
|
+
const repository = this.deps.repository;
|
|
4007
|
+
if (!repository) return true;
|
|
4008
|
+
try {
|
|
4009
|
+
const stored = await repository.getRfqSwap(swap.rfqId);
|
|
4010
|
+
const record = stored ? updateRfqSwapRecord(stored, swap) : createRfqSwapRecord(this.originOrThrow(swap), swap);
|
|
4011
|
+
await repository.saveRfqSwap(record);
|
|
4012
|
+
return true;
|
|
4013
|
+
} catch (error) {
|
|
4014
|
+
this.emitFailed(swap, error);
|
|
4015
|
+
return false;
|
|
4016
|
+
}
|
|
4017
|
+
}
|
|
4018
|
+
originOrThrow(swap) {
|
|
4019
|
+
const origin = this.origins.get(swap.rfqId);
|
|
4020
|
+
if (!origin) throw new RfqSwapOriginRequired(swap.rfqId);
|
|
4021
|
+
return origin;
|
|
4022
|
+
}
|
|
3355
4023
|
/**
|
|
3356
4024
|
* Drop a terminal swap from monitoring and report it exactly once.
|
|
3357
4025
|
*
|
|
@@ -3411,6 +4079,8 @@ var errorMessage = (error) => error instanceof Error ? error.message : String(er
|
|
|
3411
4079
|
var outpointKey = (vtxo) => `${vtxo.txid}:${vtxo.vout}`;
|
|
3412
4080
|
|
|
3413
4081
|
// src/activity.ts
|
|
4082
|
+
var import_sdk14 = require("@arkade-os/sdk");
|
|
4083
|
+
var import_base14 = require("@scure/base");
|
|
3414
4084
|
var LABELS = {
|
|
3415
4085
|
lightning_send: "Lightning send",
|
|
3416
4086
|
lightning_receive: "Lightning receive",
|
|
@@ -3463,6 +4133,44 @@ function swapActivityResolver(deps) {
|
|
|
3463
4133
|
}
|
|
3464
4134
|
};
|
|
3465
4135
|
}
|
|
4136
|
+
async function rfqSwapActivityInputs(deps) {
|
|
4137
|
+
const records = await deps.repository.getAllRfqSwaps();
|
|
4138
|
+
return Promise.all(records.map((record) => activityInputOf(record, deps.indexer)));
|
|
4139
|
+
}
|
|
4140
|
+
async function activityInputOf(record, indexer) {
|
|
4141
|
+
const txids = /* @__PURE__ */ new Set();
|
|
4142
|
+
if (record.fundingArkTxid) txids.add(record.fundingArkTxid);
|
|
4143
|
+
if (record.refundArkTxid) txids.add(record.refundArkTxid);
|
|
4144
|
+
const handler = rfqCorridorHandlers.getOrThrow(record.kind);
|
|
4145
|
+
for (const txid of handler.activityTxids?.(record.profile) ?? []) txids.add(txid);
|
|
4146
|
+
for (const txid of record.lockupSpendArkTxids ?? []) txids.add(txid);
|
|
4147
|
+
const spendUnknown = isRfqSwapTerminal(record.state) && !record.refundArkTxid && !record.lockupSpendArkTxids?.length;
|
|
4148
|
+
if (indexer && (!record.fundingArkTxid || spendUnknown)) {
|
|
4149
|
+
for (const txid of await lockupTxids(indexer, record, !record.fundingArkTxid)) {
|
|
4150
|
+
txids.add(txid);
|
|
4151
|
+
}
|
|
4152
|
+
}
|
|
4153
|
+
return { rfqId: record.rfqId, kind: record.kind, state: record.state, txids: [...txids] };
|
|
4154
|
+
}
|
|
4155
|
+
async function lockupTxids(indexer, record, wantFunding) {
|
|
4156
|
+
let script;
|
|
4157
|
+
try {
|
|
4158
|
+
script = import_base14.hex.encode(import_sdk14.ArkAddress.decode(record.lockupAddress).pkScript);
|
|
4159
|
+
} catch {
|
|
4160
|
+
return [];
|
|
4161
|
+
}
|
|
4162
|
+
try {
|
|
4163
|
+
const { vtxos } = await indexer.getVtxos({ scripts: [script] });
|
|
4164
|
+
const out = [];
|
|
4165
|
+
for (const vtxo of vtxos ?? []) {
|
|
4166
|
+
if (wantFunding) out.push(vtxo.txid);
|
|
4167
|
+
if (vtxo.arkTxId) out.push(vtxo.arkTxId);
|
|
4168
|
+
}
|
|
4169
|
+
return out;
|
|
4170
|
+
} catch {
|
|
4171
|
+
return [];
|
|
4172
|
+
}
|
|
4173
|
+
}
|
|
3466
4174
|
// Annotate the CommonJS export names for ESM import in node:
|
|
3467
4175
|
0 && (module.exports = {
|
|
3468
4176
|
ARKADE_ASSET,
|
|
@@ -3475,6 +4183,7 @@ function swapActivityResolver(deps) {
|
|
|
3475
4183
|
LIGHTNING_RECEIVE_PAIR,
|
|
3476
4184
|
LIGHTNING_SEND_PAIR,
|
|
3477
4185
|
LockupAmountMismatchError,
|
|
4186
|
+
LockupContractMissing,
|
|
3478
4187
|
LockupNeedsRecoveryError,
|
|
3479
4188
|
LockupRegistrationFailed,
|
|
3480
4189
|
MAX_MIN_CONFIRMATIONS,
|
|
@@ -3492,10 +4201,12 @@ function swapActivityResolver(deps) {
|
|
|
3492
4201
|
QUOTE_OPTIONS,
|
|
3493
4202
|
REFUND_MTP_LAG_SECONDS,
|
|
3494
4203
|
RFQ_RESOLVED_STATES,
|
|
4204
|
+
RFQ_SWAP_RETENTION_SECONDS,
|
|
3495
4205
|
RFQ_SWAP_TERMINAL_STATES,
|
|
3496
4206
|
RFQ_TERMINAL_STATES,
|
|
3497
4207
|
RefundNotLocallyPossibleError,
|
|
3498
4208
|
RfqSwapManager,
|
|
4209
|
+
RfqSwapOriginRequired,
|
|
3499
4210
|
SOLO_REFUND_HEADROOM_SECONDS,
|
|
3500
4211
|
SWAP_LOCKUP_CONTRACT_KIND,
|
|
3501
4212
|
SWAP_LOCKUP_CONTRACT_LABEL,
|
|
@@ -3503,6 +4214,7 @@ function swapActivityResolver(deps) {
|
|
|
3503
4214
|
SwapRefusal,
|
|
3504
4215
|
addAssetSwap,
|
|
3505
4216
|
arkadeAssetLeg,
|
|
4217
|
+
arkadeRefunder,
|
|
3506
4218
|
arkadeSwapRequest,
|
|
3507
4219
|
assertFundable,
|
|
3508
4220
|
assertReceivable,
|
|
@@ -3518,6 +4230,7 @@ function swapActivityResolver(deps) {
|
|
|
3518
4230
|
classifyOnchainHtlc,
|
|
3519
4231
|
classifySpend,
|
|
3520
4232
|
createOffer,
|
|
4233
|
+
createRfqSwapRecord,
|
|
3521
4234
|
decodeOffer,
|
|
3522
4235
|
deriveLightningReceive,
|
|
3523
4236
|
deriveOnchainReceive,
|
|
@@ -3535,6 +4248,7 @@ function swapActivityResolver(deps) {
|
|
|
3535
4248
|
lightningReceiveRequest,
|
|
3536
4249
|
lightningSendRequest,
|
|
3537
4250
|
lightningSendVtxoScript,
|
|
4251
|
+
lockupContractParams,
|
|
3538
4252
|
makeCachedFeedFetch,
|
|
3539
4253
|
newPreimage,
|
|
3540
4254
|
newRfqId,
|
|
@@ -3543,12 +4257,14 @@ function swapActivityResolver(deps) {
|
|
|
3543
4257
|
offerVtxoScript,
|
|
3544
4258
|
onchainHtlcScript,
|
|
3545
4259
|
onchainReceiveRequest,
|
|
4260
|
+
onchainSendProfile,
|
|
3546
4261
|
onchainSendRequest,
|
|
3547
4262
|
paymentHashOf,
|
|
3548
4263
|
preimageForSwapRecord,
|
|
3549
4264
|
pushClaim,
|
|
3550
4265
|
pushRefundWithoutReceiver,
|
|
3551
4266
|
readLockupFate,
|
|
4267
|
+
rebuildRfqSwap,
|
|
3552
4268
|
receiveVtxoScript,
|
|
3553
4269
|
refundIfUnresolved,
|
|
3554
4270
|
registerLockupContract,
|
|
@@ -3559,9 +4275,15 @@ function swapActivityResolver(deps) {
|
|
|
3559
4275
|
requestOnchainSend,
|
|
3560
4276
|
restoreAssetSwaps,
|
|
3561
4277
|
retireSettledOfferContracts,
|
|
4278
|
+
rfqClaimSecretOf,
|
|
3562
4279
|
rfqPair,
|
|
4280
|
+
rfqSecretsProfile,
|
|
4281
|
+
rfqSignerOf,
|
|
4282
|
+
rfqSwapActivityInputs,
|
|
4283
|
+
rfqSwapOriginOf,
|
|
3563
4284
|
sealClaimPacket,
|
|
3564
4285
|
senderIdentityForSwapRecord,
|
|
4286
|
+
shouldRetainRfqSwap,
|
|
3565
4287
|
spendTxidsOf,
|
|
3566
4288
|
spendUpdate,
|
|
3567
4289
|
swapActivityResolver,
|
|
@@ -3572,6 +4294,7 @@ function swapActivityResolver(deps) {
|
|
|
3572
4294
|
unilateralRefundWithoutReceiverDelay,
|
|
3573
4295
|
updateAssetSwap,
|
|
3574
4296
|
updateAssetSwapBestEffort,
|
|
4297
|
+
updateRfqSwapRecord,
|
|
3575
4298
|
validatePlan,
|
|
3576
4299
|
verifyLockupAddress,
|
|
3577
4300
|
verifyReceiveInvoice,
|