@arkade-os/swap 0.0.7 → 0.0.9
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 +276 -12
- package/dist/{chunk-ZDTRQZE2.js → chunk-2FEMUOIH.js} +83 -57
- package/dist/{chunk-WGRU2DBF.js → chunk-6ZUS47GA.js} +15 -1
- package/dist/index.cjs +1155 -443
- package/dist/index.d.cts +293 -1140
- package/dist/index.d.ts +293 -1140
- package/dist/index.js +761 -87
- package/dist/nostr.cjs +1 -0
- 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-DEHLtD9l.d.cts +1862 -0
- package/dist/repository-DIAr5XYk.d.ts +1862 -0
- package/dist/{rfq-DfT9dAss.d.cts → rfq-hbzhTWHT.d.cts} +71 -3
- package/dist/{rfq-DfT9dAss.d.ts → rfq-hbzhTWHT.d.ts} +71 -3
- package/package.json +3 -2
- package/dist/repository-BwnZ8N62.d.cts +0 -236
- package/dist/repository-BwnZ8N62.d.ts +0 -236
package/dist/index.cjs
CHANGED
|
@@ -39,7 +39,9 @@ __export(index_exports, {
|
|
|
39
39
|
LIGHTNING_BTC: () => LIGHTNING_BTC,
|
|
40
40
|
LIGHTNING_RECEIVE_PAIR: () => LIGHTNING_RECEIVE_PAIR,
|
|
41
41
|
LIGHTNING_SEND_PAIR: () => LIGHTNING_SEND_PAIR,
|
|
42
|
+
LOCKTIME_THRESHOLD: () => LOCKTIME_THRESHOLD,
|
|
42
43
|
LockupAmountMismatchError: () => LockupAmountMismatchError,
|
|
44
|
+
LockupContractMissing: () => LockupContractMissing,
|
|
43
45
|
LockupNeedsRecoveryError: () => LockupNeedsRecoveryError,
|
|
44
46
|
LockupRegistrationFailed: () => LockupRegistrationFailed,
|
|
45
47
|
MAX_MIN_CONFIRMATIONS: () => MAX_MIN_CONFIRMATIONS,
|
|
@@ -57,10 +59,12 @@ __export(index_exports, {
|
|
|
57
59
|
QUOTE_OPTIONS: () => QUOTE_OPTIONS,
|
|
58
60
|
REFUND_MTP_LAG_SECONDS: () => REFUND_MTP_LAG_SECONDS,
|
|
59
61
|
RFQ_RESOLVED_STATES: () => RFQ_RESOLVED_STATES,
|
|
62
|
+
RFQ_SWAP_RETENTION_SECONDS: () => RFQ_SWAP_RETENTION_SECONDS,
|
|
60
63
|
RFQ_SWAP_TERMINAL_STATES: () => RFQ_SWAP_TERMINAL_STATES,
|
|
61
64
|
RFQ_TERMINAL_STATES: () => RFQ_TERMINAL_STATES,
|
|
62
65
|
RefundNotLocallyPossibleError: () => RefundNotLocallyPossibleError,
|
|
63
66
|
RfqSwapManager: () => RfqSwapManager,
|
|
67
|
+
RfqSwapOriginRequired: () => RfqSwapOriginRequired,
|
|
64
68
|
SOLO_REFUND_HEADROOM_SECONDS: () => SOLO_REFUND_HEADROOM_SECONDS,
|
|
65
69
|
SWAP_LOCKUP_CONTRACT_KIND: () => SWAP_LOCKUP_CONTRACT_KIND,
|
|
66
70
|
SWAP_LOCKUP_CONTRACT_LABEL: () => SWAP_LOCKUP_CONTRACT_LABEL,
|
|
@@ -68,6 +72,7 @@ __export(index_exports, {
|
|
|
68
72
|
SwapRefusal: () => SwapRefusal,
|
|
69
73
|
addAssetSwap: () => addAssetSwap,
|
|
70
74
|
arkadeAssetLeg: () => arkadeAssetLeg,
|
|
75
|
+
arkadeRefunder: () => arkadeRefunder,
|
|
71
76
|
arkadeSwapRequest: () => arkadeSwapRequest,
|
|
72
77
|
assertFundable: () => assertFundable,
|
|
73
78
|
assertReceivable: () => assertReceivable,
|
|
@@ -83,6 +88,7 @@ __export(index_exports, {
|
|
|
83
88
|
classifyOnchainHtlc: () => classifyOnchainHtlc,
|
|
84
89
|
classifySpend: () => classifySpend,
|
|
85
90
|
createOffer: () => createOffer,
|
|
91
|
+
createRfqSwapRecord: () => createRfqSwapRecord,
|
|
86
92
|
decodeOffer: () => decodeOffer,
|
|
87
93
|
deriveLightningReceive: () => deriveLightningReceive,
|
|
88
94
|
deriveOnchainReceive: () => deriveOnchainReceive,
|
|
@@ -100,6 +106,7 @@ __export(index_exports, {
|
|
|
100
106
|
lightningReceiveRequest: () => lightningReceiveRequest,
|
|
101
107
|
lightningSendRequest: () => lightningSendRequest,
|
|
102
108
|
lightningSendVtxoScript: () => lightningSendVtxoScript,
|
|
109
|
+
lockupContractParams: () => lockupContractParams,
|
|
103
110
|
makeCachedFeedFetch: () => makeCachedFeedFetch,
|
|
104
111
|
newPreimage: () => newPreimage,
|
|
105
112
|
newRfqId: () => newRfqId,
|
|
@@ -108,12 +115,14 @@ __export(index_exports, {
|
|
|
108
115
|
offerVtxoScript: () => offerVtxoScript,
|
|
109
116
|
onchainHtlcScript: () => onchainHtlcScript,
|
|
110
117
|
onchainReceiveRequest: () => onchainReceiveRequest,
|
|
118
|
+
onchainSendProfile: () => onchainSendProfile,
|
|
111
119
|
onchainSendRequest: () => onchainSendRequest,
|
|
112
120
|
paymentHashOf: () => paymentHashOf,
|
|
113
121
|
preimageForSwapRecord: () => preimageForSwapRecord,
|
|
114
122
|
pushClaim: () => pushClaim,
|
|
115
123
|
pushRefundWithoutReceiver: () => pushRefundWithoutReceiver,
|
|
116
124
|
readLockupFate: () => readLockupFate,
|
|
125
|
+
rebuildRfqSwap: () => rebuildRfqSwap,
|
|
117
126
|
receiveVtxoScript: () => receiveVtxoScript,
|
|
118
127
|
refundIfUnresolved: () => refundIfUnresolved,
|
|
119
128
|
registerLockupContract: () => registerLockupContract,
|
|
@@ -124,9 +133,15 @@ __export(index_exports, {
|
|
|
124
133
|
requestOnchainSend: () => requestOnchainSend,
|
|
125
134
|
restoreAssetSwaps: () => restoreAssetSwaps,
|
|
126
135
|
retireSettledOfferContracts: () => retireSettledOfferContracts,
|
|
136
|
+
rfqClaimSecretOf: () => rfqClaimSecretOf,
|
|
127
137
|
rfqPair: () => rfqPair,
|
|
138
|
+
rfqSecretsProfile: () => rfqSecretsProfile,
|
|
139
|
+
rfqSignerOf: () => rfqSignerOf,
|
|
140
|
+
rfqSwapActivityInputs: () => rfqSwapActivityInputs,
|
|
141
|
+
rfqSwapOriginOf: () => rfqSwapOriginOf,
|
|
128
142
|
sealClaimPacket: () => sealClaimPacket,
|
|
129
143
|
senderIdentityForSwapRecord: () => senderIdentityForSwapRecord,
|
|
144
|
+
shouldRetainRfqSwap: () => shouldRetainRfqSwap,
|
|
130
145
|
spendTxidsOf: () => spendTxidsOf,
|
|
131
146
|
spendUpdate: () => spendUpdate,
|
|
132
147
|
swapActivityResolver: () => swapActivityResolver,
|
|
@@ -137,6 +152,7 @@ __export(index_exports, {
|
|
|
137
152
|
unilateralRefundWithoutReceiverDelay: () => unilateralRefundWithoutReceiverDelay,
|
|
138
153
|
updateAssetSwap: () => updateAssetSwap,
|
|
139
154
|
updateAssetSwapBestEffort: () => updateAssetSwapBestEffort,
|
|
155
|
+
updateRfqSwapRecord: () => updateRfqSwapRecord,
|
|
140
156
|
validatePlan: () => validatePlan,
|
|
141
157
|
verifyLockupAddress: () => verifyLockupAddress,
|
|
142
158
|
verifyReceiveInvoice: () => verifyReceiveInvoice,
|
|
@@ -762,8 +778,9 @@ var validatePlan = (plan, giveBalance, dust) => {
|
|
|
762
778
|
// src/repository.ts
|
|
763
779
|
var marketsCacheKey = (network, registry) => `arkade-intents-markets-${network}-${registry}`;
|
|
764
780
|
var InMemoryAssetSwapRepository = class {
|
|
765
|
-
version =
|
|
781
|
+
version = 4;
|
|
766
782
|
swaps = /* @__PURE__ */ new Map();
|
|
783
|
+
rfqSwaps = /* @__PURE__ */ new Map();
|
|
767
784
|
scanned = /* @__PURE__ */ new Set();
|
|
768
785
|
markets = /* @__PURE__ */ new Map();
|
|
769
786
|
async saveSwap(swap) {
|
|
@@ -772,6 +789,18 @@ var InMemoryAssetSwapRepository = class {
|
|
|
772
789
|
async getAllSwaps() {
|
|
773
790
|
return [...this.swaps.values()];
|
|
774
791
|
}
|
|
792
|
+
async saveRfqSwap(record) {
|
|
793
|
+
this.rfqSwaps.set(record.rfqId, record);
|
|
794
|
+
}
|
|
795
|
+
async getRfqSwap(rfqId) {
|
|
796
|
+
return this.rfqSwaps.get(rfqId);
|
|
797
|
+
}
|
|
798
|
+
async getAllRfqSwaps() {
|
|
799
|
+
return [...this.rfqSwaps.values()];
|
|
800
|
+
}
|
|
801
|
+
async removeRfqSwap(rfqId) {
|
|
802
|
+
this.rfqSwaps.delete(rfqId);
|
|
803
|
+
}
|
|
775
804
|
async getScannedTxids() {
|
|
776
805
|
return new Set(this.scanned);
|
|
777
806
|
}
|
|
@@ -786,6 +815,7 @@ var InMemoryAssetSwapRepository = class {
|
|
|
786
815
|
}
|
|
787
816
|
async clear() {
|
|
788
817
|
this.swaps.clear();
|
|
818
|
+
this.rfqSwaps.clear();
|
|
789
819
|
this.scanned.clear();
|
|
790
820
|
this.markets.clear();
|
|
791
821
|
}
|
|
@@ -796,58 +826,44 @@ var InMemoryAssetSwapRepository = class {
|
|
|
796
826
|
// src/indexedDbRepository.ts
|
|
797
827
|
var import_sdk4 = require("@arkade-os/sdk");
|
|
798
828
|
var DEFAULT_DB_NAME = "arkade-intents";
|
|
799
|
-
var DB_VERSION =
|
|
829
|
+
var DB_VERSION = 2;
|
|
800
830
|
var STORE_SWAPS = "swaps";
|
|
831
|
+
var STORE_RFQ_SWAPS = "rfqSwaps";
|
|
801
832
|
var STORE_SCANNED = "scannedTxids";
|
|
802
833
|
var STORE_MARKETS = "markets";
|
|
803
834
|
var STORES = [
|
|
804
835
|
[STORE_SWAPS, { keyPath: "id" }],
|
|
836
|
+
// v2. Separate from `swaps` rather than sharing it: the two record types
|
|
837
|
+
// have different keys and no consumer wants them interleaved.
|
|
838
|
+
[STORE_RFQ_SWAPS, { keyPath: "rfqId" }],
|
|
805
839
|
[STORE_SCANNED],
|
|
806
840
|
[STORE_MARKETS]
|
|
807
841
|
];
|
|
808
|
-
function initDatabase(db) {
|
|
842
|
+
function initDatabase(db, oldVersion, transaction) {
|
|
843
|
+
void oldVersion;
|
|
844
|
+
void transaction;
|
|
809
845
|
for (const [name, options] of STORES) {
|
|
810
846
|
if (!db.objectStoreNames.contains(name)) db.createObjectStore(name, options);
|
|
811
847
|
}
|
|
812
848
|
}
|
|
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
849
|
var IndexedDbAssetSwapRepository = class {
|
|
850
|
+
version = 4;
|
|
851
|
+
connection;
|
|
823
852
|
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;
|
|
853
|
+
this.connection = (0, import_sdk4.createManagedConnection)(dbName, DB_VERSION, initDatabase);
|
|
854
|
+
}
|
|
834
855
|
ensureDb() {
|
|
835
|
-
return this.
|
|
836
|
-
(err) => {
|
|
837
|
-
this.dbPromise = null;
|
|
838
|
-
throw err;
|
|
839
|
-
}
|
|
840
|
-
);
|
|
856
|
+
return this.connection.get();
|
|
841
857
|
}
|
|
842
858
|
async readStore(name) {
|
|
843
859
|
return (await this.ensureDb()).transaction([name], "readonly").objectStore(name);
|
|
844
860
|
}
|
|
845
861
|
/** Every write in one place, so none of them can forget to await the
|
|
846
862
|
* commit. Requests need no individual await: a failed one aborts the
|
|
847
|
-
* transaction, which `
|
|
863
|
+
* transaction, which `awaitTransaction` reports. */
|
|
848
864
|
async write(name, apply) {
|
|
849
865
|
const tx = (await this.ensureDb()).transaction([name], "readwrite");
|
|
850
|
-
const done =
|
|
866
|
+
const done = (0, import_sdk4.awaitTransaction)(tx);
|
|
851
867
|
apply(tx.objectStore(name));
|
|
852
868
|
await done;
|
|
853
869
|
}
|
|
@@ -857,10 +873,26 @@ var IndexedDbAssetSwapRepository = class {
|
|
|
857
873
|
});
|
|
858
874
|
}
|
|
859
875
|
async getAllSwaps() {
|
|
860
|
-
return
|
|
876
|
+
return (0, import_sdk4.promisifyRequest)((await this.readStore(STORE_SWAPS)).getAll());
|
|
877
|
+
}
|
|
878
|
+
async saveRfqSwap(record) {
|
|
879
|
+
await this.write(STORE_RFQ_SWAPS, (store) => {
|
|
880
|
+
store.put(record);
|
|
881
|
+
});
|
|
882
|
+
}
|
|
883
|
+
async getRfqSwap(rfqId) {
|
|
884
|
+
return (0, import_sdk4.promisifyRequest)((await this.readStore(STORE_RFQ_SWAPS)).get(rfqId));
|
|
885
|
+
}
|
|
886
|
+
async getAllRfqSwaps() {
|
|
887
|
+
return (0, import_sdk4.promisifyRequest)((await this.readStore(STORE_RFQ_SWAPS)).getAll());
|
|
888
|
+
}
|
|
889
|
+
async removeRfqSwap(rfqId) {
|
|
890
|
+
await this.write(STORE_RFQ_SWAPS, (store) => {
|
|
891
|
+
store.delete(rfqId);
|
|
892
|
+
});
|
|
861
893
|
}
|
|
862
894
|
async getScannedTxids() {
|
|
863
|
-
const keys = await
|
|
895
|
+
const keys = await (0, import_sdk4.promisifyRequest)((await this.readStore(STORE_SCANNED)).getAllKeys());
|
|
864
896
|
return new Set(keys);
|
|
865
897
|
}
|
|
866
898
|
async markTxidsScanned(txids) {
|
|
@@ -870,7 +902,7 @@ var IndexedDbAssetSwapRepository = class {
|
|
|
870
902
|
}
|
|
871
903
|
async getCachedMarkets(network, registry) {
|
|
872
904
|
const store = await this.readStore(STORE_MARKETS);
|
|
873
|
-
return
|
|
905
|
+
return (0, import_sdk4.promisifyRequest)(store.get(marketsCacheKey(network, registry)));
|
|
874
906
|
}
|
|
875
907
|
async saveCachedMarkets(network, registry, entry) {
|
|
876
908
|
await this.write(STORE_MARKETS, (store) => {
|
|
@@ -883,280 +915,148 @@ var IndexedDbAssetSwapRepository = class {
|
|
|
883
915
|
async clear() {
|
|
884
916
|
const stores = STORES.map(([name]) => name);
|
|
885
917
|
const tx = (await this.ensureDb()).transaction(stores, "readwrite");
|
|
886
|
-
const done =
|
|
918
|
+
const done = (0, import_sdk4.awaitTransaction)(tx);
|
|
887
919
|
for (const name of stores) tx.objectStore(name).clear();
|
|
888
920
|
await done;
|
|
889
921
|
}
|
|
890
922
|
async [Symbol.asyncDispose]() {
|
|
891
|
-
|
|
892
|
-
await (0, import_sdk4.closeDatabase)(this.dbName);
|
|
893
|
-
this.dbPromise = null;
|
|
923
|
+
await this.connection[Symbol.asyncDispose]();
|
|
894
924
|
}
|
|
895
925
|
};
|
|
896
926
|
|
|
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
|
-
}
|
|
927
|
+
// src/rfqCorridors.ts
|
|
928
|
+
var import_base4 = require("@scure/base");
|
|
929
|
+
|
|
930
|
+
// src/rfqCorridor.ts
|
|
931
|
+
var RfqCorridorRegistry = class {
|
|
932
|
+
handlers = /* @__PURE__ */ new Map();
|
|
933
|
+
register(handler) {
|
|
934
|
+
if (this.handlers.has(handler.kind)) {
|
|
935
|
+
throw new Error(
|
|
936
|
+
`RFQ corridor handler for kind '${handler.kind}' is already registered`
|
|
937
|
+
);
|
|
919
938
|
}
|
|
939
|
+
this.handlers.set(handler.kind, handler);
|
|
920
940
|
}
|
|
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
|
-
}
|
|
941
|
+
/** Takes a bare `string`, deliberately, where {@link RfqCorridorHandler.kind}
|
|
942
|
+
* is the manager's union: a lookup key comes off a record a backend handed
|
|
943
|
+
* back, and that it typechecks as a `kind` is a claim about the type, not
|
|
944
|
+
* about what was stored. Narrowing this would read as a check already made. */
|
|
945
|
+
get(kind) {
|
|
946
|
+
return this.handlers.get(kind);
|
|
947
947
|
}
|
|
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 {
|
|
948
|
+
/**
|
|
949
|
+
* The handler for a kind, or a loud failure.
|
|
950
|
+
*
|
|
951
|
+
* A record whose corridor is not registered cannot be restored, and
|
|
952
|
+
* guessing would monitor a swap with no idea how to drive it. Names the
|
|
953
|
+
* registered kinds so a missing `register()` call is obvious.
|
|
954
|
+
*/
|
|
955
|
+
getOrThrow(kind) {
|
|
956
|
+
const handler = this.get(kind);
|
|
957
|
+
if (!handler) {
|
|
958
|
+
throw new Error(
|
|
959
|
+
`no RFQ corridor handler registered for kind '${kind}'; registered: ${this.registeredKinds().join(", ") || "none"}`
|
|
960
|
+
);
|
|
983
961
|
}
|
|
962
|
+
return handler;
|
|
984
963
|
}
|
|
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
|
-
}
|
|
964
|
+
has(kind) {
|
|
965
|
+
return this.handlers.has(kind);
|
|
994
966
|
}
|
|
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);
|
|
967
|
+
registeredKinds() {
|
|
968
|
+
return [...this.handlers.keys()];
|
|
1002
969
|
}
|
|
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
|
-
});
|
|
970
|
+
/** Test seam, mirroring the contract registry's. */
|
|
971
|
+
unregister(kind) {
|
|
972
|
+
return this.handlers.delete(kind);
|
|
1059
973
|
}
|
|
1060
|
-
|
|
1061
|
-
|
|
974
|
+
};
|
|
975
|
+
var rfqCorridorHandlers = new RfqCorridorRegistry();
|
|
1062
976
|
|
|
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";
|
|
977
|
+
// src/rfqProfileParts.ts
|
|
978
|
+
var rfqSecretsProfile = (secrets, paymentHash) => {
|
|
979
|
+
const { signingDescriptor, ...preimage } = swapSecretsToRecord(secrets);
|
|
1071
980
|
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 } : {}
|
|
981
|
+
signer: { signingDescriptor },
|
|
982
|
+
...paymentHash ? { hashlock: { paymentHash, ...preimage } } : {}
|
|
1076
983
|
};
|
|
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
|
|
984
|
+
};
|
|
985
|
+
var parseHex32 = (value, field) => {
|
|
986
|
+
if (typeof value !== "string" || !/^[0-9a-fA-F]{64}$/.test(value)) {
|
|
987
|
+
throw new Error(`${field} must be 32 bytes of hex, got ${JSON.stringify(value)}`);
|
|
988
|
+
}
|
|
989
|
+
return value.toLowerCase();
|
|
990
|
+
};
|
|
991
|
+
var parseSigner = (value) => {
|
|
992
|
+
const signingDescriptor = value?.signingDescriptor;
|
|
993
|
+
if (typeof signingDescriptor !== "string" || signingDescriptor.length === 0) {
|
|
994
|
+
throw new Error(
|
|
995
|
+
`rfq profile.signer carries no signingDescriptor (got ${JSON.stringify(signingDescriptor)})`
|
|
996
|
+
);
|
|
997
|
+
}
|
|
998
|
+
return { signingDescriptor };
|
|
999
|
+
};
|
|
1000
|
+
var parseHashlock = (value) => {
|
|
1001
|
+
const raw = value ?? {};
|
|
1002
|
+
const hashlock = {
|
|
1003
|
+
paymentHash: parseHex32(raw.paymentHash, "rfq profile.hashlock.paymentHash")
|
|
1139
1004
|
};
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
|
|
1005
|
+
if (raw.preimageHex !== void 0) {
|
|
1006
|
+
hashlock.preimageHex = parseHex32(raw.preimageHex, "rfq profile.hashlock.preimageHex");
|
|
1007
|
+
}
|
|
1008
|
+
if (raw.preimageSaltHex !== void 0) {
|
|
1009
|
+
hashlock.preimageSaltHex = parseHex32(
|
|
1010
|
+
raw.preimageSaltHex,
|
|
1011
|
+
"rfq profile.hashlock.preimageSaltHex"
|
|
1012
|
+
);
|
|
1013
|
+
}
|
|
1014
|
+
return hashlock;
|
|
1015
|
+
};
|
|
1016
|
+
var hydrateHashlock = (profile) => {
|
|
1017
|
+
try {
|
|
1018
|
+
return { paymentHash: parseHashlock(profile.hashlock).paymentHash };
|
|
1019
|
+
} catch (cause) {
|
|
1020
|
+
throw new Error(
|
|
1021
|
+
`rfq record carries no usable hashlock; it cannot verify a preimage: ${String(cause)}`,
|
|
1022
|
+
{ cause }
|
|
1023
|
+
);
|
|
1024
|
+
}
|
|
1025
|
+
};
|
|
1026
|
+
var rfqSignerOf = (record) => {
|
|
1027
|
+
const signer = record.profile.signer;
|
|
1028
|
+
if (signer === void 0) return void 0;
|
|
1029
|
+
return parseSigner(signer);
|
|
1030
|
+
};
|
|
1031
|
+
var rfqClaimSecretOf = (record) => {
|
|
1032
|
+
const handler = rfqCorridorHandlers.getOrThrow(record.kind);
|
|
1033
|
+
if (!handler.claimSecret) return void 0;
|
|
1034
|
+
try {
|
|
1035
|
+
const claim = handler.claimSecret(record.profile);
|
|
1036
|
+
return { ...parseSigner(claim), ...parseHashlock(claim) };
|
|
1037
|
+
} catch (cause) {
|
|
1038
|
+
throw new PreimageNotRecoverableError(
|
|
1039
|
+
"malformed-record",
|
|
1040
|
+
`this ${record.kind} record's claim secret is unreadable: ${String(cause)}`,
|
|
1041
|
+
{ cause }
|
|
1042
|
+
);
|
|
1043
|
+
}
|
|
1044
|
+
};
|
|
1146
1045
|
|
|
1147
1046
|
// src/onchainHtlc.ts
|
|
1148
|
-
var
|
|
1047
|
+
var import_base3 = require("@scure/base");
|
|
1149
1048
|
var import_sha22 = require("@noble/hashes/sha2.js");
|
|
1150
1049
|
var import_legacy = require("@noble/hashes/legacy.js");
|
|
1151
1050
|
var btc = __toESM(require("@scure/btc-signer"), 1);
|
|
1152
1051
|
var ONCHAIN_ORDER_MARGIN_SECONDS = 2 * 60 * 60;
|
|
1153
1052
|
var ONCHAIN_CLAIM_MARGIN_SECONDS = 90 * 60;
|
|
1154
1053
|
var MAX_MIN_CONFIRMATIONS = 6;
|
|
1054
|
+
var LOCKTIME_THRESHOLD = 5e8;
|
|
1155
1055
|
var ONCHAIN_SECONDS_PER_BLOCK = 600;
|
|
1156
1056
|
var ONCHAIN_DUST_SATS = BigInt(330);
|
|
1157
1057
|
var newPreimage = () => crypto.getRandomValues(new Uint8Array(32));
|
|
1158
|
-
var paymentHashOf = (preimage) =>
|
|
1159
|
-
var h160FromPaymentHash = (paymentHash) => (0, import_legacy.ripemd160)(
|
|
1058
|
+
var paymentHashOf = (preimage) => import_base3.hex.encode((0, import_sha22.sha256)(preimage));
|
|
1059
|
+
var h160FromPaymentHash = (paymentHash) => (0, import_legacy.ripemd160)(import_base3.hex.decode(paymentHash));
|
|
1160
1060
|
var L1_NETWORKS = {
|
|
1161
1061
|
bitcoin: btc.NETWORK,
|
|
1162
1062
|
testnet: btc.TEST_NETWORK,
|
|
@@ -1171,6 +1071,16 @@ function onchainHtlcScript(params, network) {
|
|
|
1171
1071
|
`refundLocktime must be a positive unix timestamp, got ${params.refundLocktime}`
|
|
1172
1072
|
);
|
|
1173
1073
|
}
|
|
1074
|
+
if (params.refundLocktime < LOCKTIME_THRESHOLD) {
|
|
1075
|
+
throw new Error(
|
|
1076
|
+
`refundLocktime ${params.refundLocktime} is below LOCKTIME_THRESHOLD (${LOCKTIME_THRESHOLD}) and would be interpreted as a block height`
|
|
1077
|
+
);
|
|
1078
|
+
}
|
|
1079
|
+
if (!Object.hasOwn(L1_NETWORKS, network)) {
|
|
1080
|
+
throw new Error(
|
|
1081
|
+
`unknown L1 network '${String(network)}' \u2014 expected one of ${Object.keys(L1_NETWORKS).join(", ")}`
|
|
1082
|
+
);
|
|
1083
|
+
}
|
|
1174
1084
|
const h160 = h160FromPaymentHash(params.paymentHash);
|
|
1175
1085
|
const claim = btc.Script.encode([
|
|
1176
1086
|
"SIZE",
|
|
@@ -1197,7 +1107,7 @@ function onchainHtlcScript(params, network) {
|
|
|
1197
1107
|
);
|
|
1198
1108
|
const controlBlockFor = (leaf) => {
|
|
1199
1109
|
for (const [block, script] of payment.tapLeafScript ?? []) {
|
|
1200
|
-
if (script.length - 1 === leaf.length &&
|
|
1110
|
+
if (script.length - 1 === leaf.length && import_base3.hex.encode(script.subarray(0, leaf.length)) === import_base3.hex.encode(leaf)) {
|
|
1201
1111
|
return btc.TaprootControlBlock.encode(block);
|
|
1202
1112
|
}
|
|
1203
1113
|
}
|
|
@@ -1290,13 +1200,13 @@ var buildHtlcRefund = (input) => buildLeafSpend({
|
|
|
1290
1200
|
function extractPreimage(txHex, paymentHash) {
|
|
1291
1201
|
let raw;
|
|
1292
1202
|
try {
|
|
1293
|
-
raw = btc.RawTx.decode(
|
|
1203
|
+
raw = btc.RawTx.decode(import_base3.hex.decode(txHex));
|
|
1294
1204
|
} catch {
|
|
1295
1205
|
return null;
|
|
1296
1206
|
}
|
|
1297
1207
|
for (const stack of raw.witnesses ?? []) {
|
|
1298
1208
|
for (const item of stack) {
|
|
1299
|
-
if (item.length === 32 &&
|
|
1209
|
+
if (item.length === 32 && import_base3.hex.encode((0, import_sha22.sha256)(item)) === paymentHash) return item;
|
|
1300
1210
|
}
|
|
1301
1211
|
}
|
|
1302
1212
|
return null;
|
|
@@ -1325,36 +1235,502 @@ async function claimOnchainFill(chain, input) {
|
|
|
1325
1235
|
error.reason = "claim_window_closed";
|
|
1326
1236
|
throw error;
|
|
1327
1237
|
}
|
|
1328
|
-
const spend = await buildHtlcClaim(input);
|
|
1329
|
-
const txid = await chain.broadcast(spend.txHex);
|
|
1330
|
-
return { txid, payoutAmount: spend.payoutAmount };
|
|
1238
|
+
const spend = await buildHtlcClaim(input);
|
|
1239
|
+
const txid = await chain.broadcast(spend.txHex);
|
|
1240
|
+
return { txid, payoutAmount: spend.payoutAmount };
|
|
1241
|
+
}
|
|
1242
|
+
async function classifyOnchainHtlc(chain, input) {
|
|
1243
|
+
const utxos = await chain.getScriptUtxos(input.htlc.pkScript);
|
|
1244
|
+
const best = utxos.sort((a, b) => b.amount > a.amount ? 1 : -1)[0];
|
|
1245
|
+
if (!best) {
|
|
1246
|
+
if (!input.funding) return { phase: "unfunded" };
|
|
1247
|
+
const spend = await chain.getSpendingTx(input.funding.txid, input.funding.vout);
|
|
1248
|
+
if (!spend) return { phase: "unfunded" };
|
|
1249
|
+
const preimage = extractPreimage(spend.txHex, input.htlc.paymentHash);
|
|
1250
|
+
const txid = btc.Transaction.fromRaw(import_base3.hex.decode(spend.txHex), {
|
|
1251
|
+
allowUnknownInputs: true,
|
|
1252
|
+
allowUnknownOutputs: true
|
|
1253
|
+
}).id;
|
|
1254
|
+
return preimage ? { phase: "claimed", txid, preimage } : { phase: "swept", txid };
|
|
1255
|
+
}
|
|
1256
|
+
if (best.confirmations < input.minConfirmations)
|
|
1257
|
+
return { phase: "awaiting_confirmations", utxo: best };
|
|
1258
|
+
const mtp = await chain.getMtp();
|
|
1259
|
+
if (mtp >= input.htlc.refundLocktime) return { phase: "refundable", utxo: best };
|
|
1260
|
+
return { phase: "claimable", utxo: best };
|
|
1261
|
+
}
|
|
1262
|
+
|
|
1263
|
+
// src/rfqCorridors.ts
|
|
1264
|
+
var LightningSendCorridor = {
|
|
1265
|
+
kind: "lightning_send",
|
|
1266
|
+
project: () => ({}),
|
|
1267
|
+
hydrate: (profile) => hydrateHashlock(profile)
|
|
1268
|
+
// No `claimSecret`, deliberately: `provisionRefundKey` mints no preimage at
|
|
1269
|
+
// all, and deriving one off the refund descriptor would fail the payment
|
|
1270
|
+
// hash check — a `hash-mismatch` on a swap that was never broken.
|
|
1271
|
+
};
|
|
1272
|
+
var LightningReceiveCorridor = {
|
|
1273
|
+
kind: "lightning_receive",
|
|
1274
|
+
// What the manager holds: the amount gate, and its own claim once it lands.
|
|
1275
|
+
// `payoutAddress` comes from the request result and never changes, so the
|
|
1276
|
+
// caller writes it once and this leaves it alone.
|
|
1277
|
+
project: (swap) => {
|
|
1278
|
+
const receive = swap;
|
|
1279
|
+
return {
|
|
1280
|
+
expectedAmount: receive.expectedAmount,
|
|
1281
|
+
...receive.claimArkTxid ? { claimArkTxid: receive.claimArkTxid } : {}
|
|
1282
|
+
};
|
|
1283
|
+
},
|
|
1284
|
+
hydrate(profile) {
|
|
1285
|
+
if (typeof profile.expectedAmount !== "number" || !Number.isFinite(profile.expectedAmount)) {
|
|
1286
|
+
throw new Error("lightning_receive record carries no expectedAmount; it cannot claim");
|
|
1287
|
+
}
|
|
1288
|
+
return {
|
|
1289
|
+
...hydrateHashlock(profile),
|
|
1290
|
+
expectedAmount: profile.expectedAmount,
|
|
1291
|
+
...profile.claimArkTxid ? { claimArkTxid: profile.claimArkTxid } : {}
|
|
1292
|
+
};
|
|
1293
|
+
},
|
|
1294
|
+
// We are the claimant here, so the preimage material on the hashlock is
|
|
1295
|
+
// ours to use.
|
|
1296
|
+
claimSecret: (profile) => ({ ...profile.signer, ...profile.hashlock }),
|
|
1297
|
+
activityTxids: (profile) => profile.claimArkTxid ? [profile.claimArkTxid] : []
|
|
1298
|
+
};
|
|
1299
|
+
function onchainSendProfile(result) {
|
|
1300
|
+
return {
|
|
1301
|
+
claimKey: import_base4.hex.encode(result.htlcParams.claimKey),
|
|
1302
|
+
refundKey: import_base4.hex.encode(result.htlcParams.refundKey),
|
|
1303
|
+
htlcLocktime: result.htlcParams.refundLocktime,
|
|
1304
|
+
network: result.l1Network,
|
|
1305
|
+
htlcAddress: result.htlc.address,
|
|
1306
|
+
minConfirmations: result.minConfirmations
|
|
1307
|
+
};
|
|
1308
|
+
}
|
|
1309
|
+
var OnchainSendCorridor = {
|
|
1310
|
+
kind: "onchain_send",
|
|
1311
|
+
// The L1 keys and the network are only in the request result — nothing on
|
|
1312
|
+
// `OnchainSendSwap` carries them — so the caller writes them once. What the
|
|
1313
|
+
// manager learns as it drives the swap is the fill and our own claim.
|
|
1314
|
+
project: (swap) => {
|
|
1315
|
+
const send = swap;
|
|
1316
|
+
return {
|
|
1317
|
+
...send.funding ? { funding: send.funding } : {},
|
|
1318
|
+
...send.claimTxid ? { claimTxid: send.claimTxid } : {}
|
|
1319
|
+
};
|
|
1320
|
+
},
|
|
1321
|
+
hydrate(profile) {
|
|
1322
|
+
const { paymentHash } = hydrateHashlock(profile);
|
|
1323
|
+
if (!profile.claimKey || !profile.refundKey) {
|
|
1324
|
+
throw new Error(
|
|
1325
|
+
"onchain_send record carries no L1 keys; its HTLC cannot be rebuilt and its refund window would pass unwatched"
|
|
1326
|
+
);
|
|
1327
|
+
}
|
|
1328
|
+
if (!Number.isInteger(profile.minConfirmations) || profile.minConfirmations < 1) {
|
|
1329
|
+
throw new Error(
|
|
1330
|
+
`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`
|
|
1331
|
+
);
|
|
1332
|
+
}
|
|
1333
|
+
const htlc = onchainHtlcScript(
|
|
1334
|
+
{
|
|
1335
|
+
// From the profile, not the covenant: the lockup commits to
|
|
1336
|
+
// `hash160(P)`, and the HTLC needs `sha256(P)`. One P
|
|
1337
|
+
// unlocks both legs, but only one of the two hashes of it
|
|
1338
|
+
// is recoverable here.
|
|
1339
|
+
paymentHash,
|
|
1340
|
+
claimKey: import_base4.hex.decode(profile.claimKey),
|
|
1341
|
+
refundKey: import_base4.hex.decode(profile.refundKey),
|
|
1342
|
+
refundLocktime: profile.htlcLocktime
|
|
1343
|
+
},
|
|
1344
|
+
profile.network
|
|
1345
|
+
);
|
|
1346
|
+
if (htlc.address !== profile.htlcAddress) {
|
|
1347
|
+
throw new Error(
|
|
1348
|
+
`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`
|
|
1349
|
+
);
|
|
1350
|
+
}
|
|
1351
|
+
return {
|
|
1352
|
+
paymentHash,
|
|
1353
|
+
htlc,
|
|
1354
|
+
minConfirmations: profile.minConfirmations,
|
|
1355
|
+
...profile.funding ? { funding: profile.funding } : {},
|
|
1356
|
+
...profile.claimTxid ? { claimTxid: profile.claimTxid } : {}
|
|
1357
|
+
};
|
|
1358
|
+
},
|
|
1359
|
+
// The trader claims the L1 HTLC with P, so this leg's preimage material is
|
|
1360
|
+
// ours.
|
|
1361
|
+
claimSecret: (profile) => ({ ...profile.signer, ...profile.hashlock }),
|
|
1362
|
+
// Our own L1 claim only. `funding` is the SOLVER's fill into the HTLC —
|
|
1363
|
+
// not a transaction of ours, so grouping it would claim a row this wallet
|
|
1364
|
+
// never made.
|
|
1365
|
+
activityTxids: (profile) => profile.claimTxid ? [profile.claimTxid] : []
|
|
1366
|
+
};
|
|
1367
|
+
rfqCorridorHandlers.register(LightningSendCorridor);
|
|
1368
|
+
rfqCorridorHandlers.register(LightningReceiveCorridor);
|
|
1369
|
+
rfqCorridorHandlers.register(OnchainSendCorridor);
|
|
1370
|
+
|
|
1371
|
+
// src/rfqRecord.ts
|
|
1372
|
+
var import_sdk5 = require("@arkade-os/sdk");
|
|
1373
|
+
var import_base5 = require("@scure/base");
|
|
1374
|
+
|
|
1375
|
+
// src/rfqSwapState.ts
|
|
1376
|
+
var RFQ_SWAP_TERMINAL_STATES = ["settled", "refunded", "failed"];
|
|
1377
|
+
var isRfqSwapTerminal = (state) => RFQ_SWAP_TERMINAL_STATES.includes(state);
|
|
1378
|
+
|
|
1379
|
+
// src/rfqRecord.ts
|
|
1380
|
+
var RFQ_SWAP_RETENTION_SECONDS = 30 * 24 * 60 * 60;
|
|
1381
|
+
var managerState = (swap) => ({
|
|
1382
|
+
rfqId: swap.rfqId,
|
|
1383
|
+
state: swap.state,
|
|
1384
|
+
createdAt: swap.createdAt,
|
|
1385
|
+
updatedAt: swap.updatedAt,
|
|
1386
|
+
...swap.refundArkTxid ? { refundArkTxid: swap.refundArkTxid } : {},
|
|
1387
|
+
...swap.lockupSpendArkTxids?.length ? { lockupSpendArkTxids: [...swap.lockupSpendArkTxids] } : {},
|
|
1388
|
+
...swap.failure ? { failure: swap.failure } : {},
|
|
1389
|
+
...swap.blockedReason ? { blockedReason: swap.blockedReason } : {}
|
|
1390
|
+
});
|
|
1391
|
+
function assertSameSwap(origin, swap) {
|
|
1392
|
+
if (origin.kind !== swap.kind) {
|
|
1393
|
+
throw new Error(
|
|
1394
|
+
`rfq swap record is a ${origin.kind} origin paired with a ${swap.kind} swap`
|
|
1395
|
+
);
|
|
1396
|
+
}
|
|
1397
|
+
const funded = import_base5.hex.encode(import_sdk5.ArkAddress.decode(origin.lockupAddress).pkScript);
|
|
1398
|
+
const watched = import_base5.hex.encode(swap.lockupPkScript);
|
|
1399
|
+
if (funded !== watched) {
|
|
1400
|
+
throw new Error(
|
|
1401
|
+
`rfq swap record's lockup address holds ${funded}, but the swap watches ${watched} \u2014 these are not the same swap`
|
|
1402
|
+
);
|
|
1403
|
+
}
|
|
1404
|
+
}
|
|
1405
|
+
function createRfqSwapRecord(origin, swap) {
|
|
1406
|
+
assertSameSwap(origin, swap);
|
|
1407
|
+
const handler = rfqCorridorHandlers.getOrThrow(origin.kind);
|
|
1408
|
+
return {
|
|
1409
|
+
...origin,
|
|
1410
|
+
...managerState(swap),
|
|
1411
|
+
// The caller wrote what only the request result knows; the handler adds
|
|
1412
|
+
// what the live swap already carries.
|
|
1413
|
+
profile: { ...origin.profile, ...handler.project(swap) }
|
|
1414
|
+
};
|
|
1415
|
+
}
|
|
1416
|
+
function updateRfqSwapRecord(record, swap) {
|
|
1417
|
+
assertSameSwap(record, swap);
|
|
1418
|
+
const {
|
|
1419
|
+
refundArkTxid: _refundArkTxid,
|
|
1420
|
+
lockupSpendArkTxids: _lockupSpendArkTxids,
|
|
1421
|
+
failure: _failure,
|
|
1422
|
+
blockedReason: _blockedReason,
|
|
1423
|
+
...origin
|
|
1424
|
+
} = record;
|
|
1425
|
+
const handler = rfqCorridorHandlers.getOrThrow(record.kind);
|
|
1426
|
+
return {
|
|
1427
|
+
...origin,
|
|
1428
|
+
...managerState(swap),
|
|
1429
|
+
profile: { ...record.profile, ...handler.project(swap) }
|
|
1430
|
+
};
|
|
1431
|
+
}
|
|
1432
|
+
function rfqSwapOriginOf(record) {
|
|
1433
|
+
return {
|
|
1434
|
+
kind: record.kind,
|
|
1435
|
+
lockupAddress: record.lockupAddress,
|
|
1436
|
+
profile: { ...record.profile },
|
|
1437
|
+
...record.amount !== void 0 ? { amount: record.amount } : {},
|
|
1438
|
+
...record.fundingArkTxid ? { fundingArkTxid: record.fundingArkTxid } : {}
|
|
1439
|
+
};
|
|
1440
|
+
}
|
|
1441
|
+
function lockupScript(params, lockupAddress) {
|
|
1442
|
+
const script = import_sdk5.VHTLCV2ContractHandler.createScript(params);
|
|
1443
|
+
const funded = import_sdk5.ArkAddress.decode(lockupAddress).pkScript;
|
|
1444
|
+
if (import_base5.hex.encode(funded) !== import_base5.hex.encode(script.pkScript)) {
|
|
1445
|
+
throw new Error(
|
|
1446
|
+
`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`
|
|
1447
|
+
);
|
|
1448
|
+
}
|
|
1449
|
+
return script;
|
|
1450
|
+
}
|
|
1451
|
+
function rebuildRfqSwap(record, params) {
|
|
1452
|
+
const script = lockupScript(params, record.lockupAddress);
|
|
1453
|
+
const common = {
|
|
1454
|
+
rfqId: record.rfqId,
|
|
1455
|
+
state: record.state,
|
|
1456
|
+
lockupPkScript: script.pkScript,
|
|
1457
|
+
lockup: { script, address: record.lockupAddress },
|
|
1458
|
+
// From the covenant, which binds it: the record's own copy would be a
|
|
1459
|
+
// second source for the deadline the refund is gated on.
|
|
1460
|
+
refundLocktime: Number(script.options.refundLocktime),
|
|
1461
|
+
createdAt: record.createdAt,
|
|
1462
|
+
updatedAt: record.updatedAt,
|
|
1463
|
+
...record.refundArkTxid ? { refundArkTxid: record.refundArkTxid } : {},
|
|
1464
|
+
...record.lockupSpendArkTxids?.length ? { lockupSpendArkTxids: [...record.lockupSpendArkTxids] } : {},
|
|
1465
|
+
...record.failure ? { failure: record.failure } : {},
|
|
1466
|
+
...record.blockedReason ? { blockedReason: record.blockedReason } : {}
|
|
1467
|
+
};
|
|
1468
|
+
const handler = rfqCorridorHandlers.getOrThrow(record.kind);
|
|
1469
|
+
return {
|
|
1470
|
+
...common,
|
|
1471
|
+
kind: record.kind,
|
|
1472
|
+
...handler.hydrate(record.profile, { lockup: script })
|
|
1473
|
+
};
|
|
1474
|
+
}
|
|
1475
|
+
function shouldRetainRfqSwap(record, now) {
|
|
1476
|
+
if (!isRfqSwapTerminal(record.state)) return true;
|
|
1477
|
+
return now - record.updatedAt < RFQ_SWAP_RETENTION_SECONDS;
|
|
1478
|
+
}
|
|
1479
|
+
|
|
1480
|
+
// src/restore.ts
|
|
1481
|
+
var import_base6 = require("@scure/base");
|
|
1482
|
+
var import_sdk6 = require("@arkade-os/sdk");
|
|
1483
|
+
var TXS_PER_REQUEST = 50;
|
|
1484
|
+
async function fetchParsedTxs(indexer, txids) {
|
|
1485
|
+
const parsedByTxid = /* @__PURE__ */ new Map();
|
|
1486
|
+
if (txids.length === 0) return parsedByTxid;
|
|
1487
|
+
const chunks = [];
|
|
1488
|
+
for (let i = 0; i < txids.length; i += TXS_PER_REQUEST) {
|
|
1489
|
+
chunks.push(txids.slice(i, i + TXS_PER_REQUEST));
|
|
1490
|
+
}
|
|
1491
|
+
const chunkResults = await Promise.allSettled(
|
|
1492
|
+
chunks.map(async (ids) => (await indexer.getVirtualTxs(ids)).txs)
|
|
1493
|
+
);
|
|
1494
|
+
for (const result of chunkResults) {
|
|
1495
|
+
if (result.status !== "fulfilled") continue;
|
|
1496
|
+
for (const psbt of result.value) {
|
|
1497
|
+
try {
|
|
1498
|
+
const parsed = import_sdk6.Transaction.fromPSBT(import_base6.base64.decode(psbt));
|
|
1499
|
+
parsedByTxid.set(parsed.id, parsed);
|
|
1500
|
+
} catch {
|
|
1501
|
+
}
|
|
1502
|
+
}
|
|
1503
|
+
}
|
|
1504
|
+
return parsedByTxid;
|
|
1505
|
+
}
|
|
1506
|
+
var unscannedSwapCandidates = (txs, existingIds, scanned) => txs.filter(
|
|
1507
|
+
(tx) => tx.type === "sent" && tx.redeemTxid && !existingIds.has(tx.redeemTxid) && !scanned.has(tx.redeemTxid)
|
|
1508
|
+
);
|
|
1509
|
+
function classifySpend(offer, serverPubkey, spendTx, deposit) {
|
|
1510
|
+
let leaves;
|
|
1511
|
+
try {
|
|
1512
|
+
const script = offerVtxoScript(offer, serverPubkey);
|
|
1513
|
+
if (import_base6.hex.encode(script.pkScript) !== import_base6.hex.encode(offer.swapPkScript)) return "indeterminate";
|
|
1514
|
+
leaves = {
|
|
1515
|
+
cancel: script.functionByName("cancel")?.leafScript,
|
|
1516
|
+
fulfill: script.functionByName("fulfill")?.leafScript
|
|
1517
|
+
};
|
|
1518
|
+
} catch {
|
|
1519
|
+
return "indeterminate";
|
|
1520
|
+
}
|
|
1521
|
+
for (let i = 0; i < spendTx.inputsLength; i++) {
|
|
1522
|
+
const input = spendTx.getInput(i);
|
|
1523
|
+
if (!input.txid || input.index !== deposit.vout) continue;
|
|
1524
|
+
if (import_base6.hex.encode(input.txid) !== deposit.txid) continue;
|
|
1525
|
+
for (const leaf of input.tapLeafScript ?? []) {
|
|
1526
|
+
const spent = import_base6.hex.encode((0, import_sdk6.scriptFromTapLeafScript)(leaf));
|
|
1527
|
+
if (leaves.cancel && spent === import_base6.hex.encode(leaves.cancel)) return "cancelled";
|
|
1528
|
+
if (leaves.fulfill && spent === import_base6.hex.encode(leaves.fulfill)) return "fulfilled";
|
|
1529
|
+
}
|
|
1530
|
+
}
|
|
1531
|
+
return "indeterminate";
|
|
1532
|
+
}
|
|
1533
|
+
var spendTxidsOf = (vtxo) => [vtxo.spentBy, vtxo.arkTxId].filter((id) => Boolean(id));
|
|
1534
|
+
function classifyDepositSpend(offer, serverPubkey, spendTxs, deposit) {
|
|
1535
|
+
for (const tx of spendTxs) {
|
|
1536
|
+
const kind = classifySpend(offer, serverPubkey, tx, deposit);
|
|
1537
|
+
if (kind !== "indeterminate") return kind;
|
|
1538
|
+
}
|
|
1539
|
+
return "indeterminate";
|
|
1540
|
+
}
|
|
1541
|
+
async function restoreAssetSwaps(indexer, txs, existingIds, opts) {
|
|
1542
|
+
const { serverPubkey, scanned = /* @__PURE__ */ new Set() } = opts;
|
|
1543
|
+
const candidates = unscannedSwapCandidates(txs, existingIds, scanned);
|
|
1544
|
+
if (candidates.length === 0) return { restored: [], scannedTxids: [] };
|
|
1545
|
+
const byTxid = new Map(candidates.map((tx) => [tx.redeemTxid, tx]));
|
|
1546
|
+
const parsedByTxid = await fetchParsedTxs(
|
|
1547
|
+
indexer,
|
|
1548
|
+
candidates.map((tx) => tx.redeemTxid)
|
|
1549
|
+
);
|
|
1550
|
+
const fetchedTxids = [];
|
|
1551
|
+
const found = [];
|
|
1552
|
+
for (const [txid, parsed] of parsedByTxid) {
|
|
1553
|
+
const fundingTx = byTxid.get(txid);
|
|
1554
|
+
if (!fundingTx) continue;
|
|
1555
|
+
fetchedTxids.push(txid);
|
|
1556
|
+
try {
|
|
1557
|
+
const packet = import_sdk6.Extension.fromTx(parsed).getPacketByType(OFFER_PACKET_TYPE);
|
|
1558
|
+
if (!packet) continue;
|
|
1559
|
+
const payload = packet.serialize();
|
|
1560
|
+
found.push({
|
|
1561
|
+
fundingTx,
|
|
1562
|
+
offer: decodeOffer(payload),
|
|
1563
|
+
offerHex: import_base6.hex.encode(payload)
|
|
1564
|
+
});
|
|
1565
|
+
} catch {
|
|
1566
|
+
}
|
|
1567
|
+
}
|
|
1568
|
+
if (found.length === 0) return { restored: [], scannedTxids: fetchedTxids };
|
|
1569
|
+
const scripts = [...new Set(found.map((f) => import_base6.hex.encode(f.offer.swapPkScript)))];
|
|
1570
|
+
const { vtxos } = await indexer.getVtxos({ scripts });
|
|
1571
|
+
const vtxoByScriptAndTxid = new Map(vtxos.map((v) => [`${v.script}:${v.txid}`, v]));
|
|
1572
|
+
const txByAnyId = /* @__PURE__ */ new Map();
|
|
1573
|
+
for (const tx of txs) {
|
|
1574
|
+
for (const id of [tx.boardingTxid, tx.redeemTxid, tx.roundTxid]) {
|
|
1575
|
+
if (id) txByAnyId.set(id, tx);
|
|
1576
|
+
}
|
|
1577
|
+
}
|
|
1578
|
+
const spendTxids = /* @__PURE__ */ new Set();
|
|
1579
|
+
for (const { fundingTx, offer } of found) {
|
|
1580
|
+
const vtxo = vtxoByScriptAndTxid.get(
|
|
1581
|
+
`${import_base6.hex.encode(offer.swapPkScript)}:${fundingTx.redeemTxid}`
|
|
1582
|
+
);
|
|
1583
|
+
if (vtxo?.virtualStatus.state !== "spent") continue;
|
|
1584
|
+
for (const txid of spendTxidsOf(vtxo)) spendTxids.add(txid);
|
|
1585
|
+
}
|
|
1586
|
+
const spendTxByTxid = await fetchParsedTxs(indexer, [...spendTxids]);
|
|
1587
|
+
const restored = [];
|
|
1588
|
+
const unresolved = /* @__PURE__ */ new Set();
|
|
1589
|
+
for (const { fundingTx, offer, offerHex } of found) {
|
|
1590
|
+
const swapPkScript = import_base6.hex.encode(offer.swapPkScript);
|
|
1591
|
+
const vtxo = vtxoByScriptAndTxid.get(`${swapPkScript}:${fundingTx.redeemTxid}`);
|
|
1592
|
+
if (!vtxo) {
|
|
1593
|
+
unresolved.add(fundingTx.redeemTxid);
|
|
1594
|
+
continue;
|
|
1595
|
+
}
|
|
1596
|
+
const depositRider = vtxo.assets?.length === 1 && vtxo.assets[0].amount > BigInt(0) ? vtxo.assets[0] : void 0;
|
|
1597
|
+
const fromAsset = offer.offerAsset?.toString() ?? depositRider?.assetId ?? BTC_ASSET_ID;
|
|
1598
|
+
const toAsset = offer.wantAsset?.toString() ?? BTC_ASSET_ID;
|
|
1599
|
+
const depositAmount = fromAsset === BTC_ASSET_ID ? BigInt(vtxo.value) : vtxo.assets?.find((a) => a.assetId === fromAsset)?.amount;
|
|
1600
|
+
if (depositAmount === void 0) {
|
|
1601
|
+
unresolved.add(fundingTx.redeemTxid);
|
|
1602
|
+
continue;
|
|
1603
|
+
}
|
|
1604
|
+
const fromAmount = depositAmount.toString();
|
|
1605
|
+
const state = vtxo.virtualStatus.state;
|
|
1606
|
+
const spentTxid = state === "spent" ? vtxo.arkTxId || vtxo.spentBy : void 0;
|
|
1607
|
+
let status = "pending";
|
|
1608
|
+
if (state === "swept") status = "recoverable";
|
|
1609
|
+
else if (state === "spent") {
|
|
1610
|
+
const spendTxs = spendTxidsOf(vtxo).map((id) => spendTxByTxid.get(id)).filter((tx) => tx !== void 0);
|
|
1611
|
+
const kind = classifyDepositSpend(offer, serverPubkey, spendTxs, {
|
|
1612
|
+
txid: vtxo.txid,
|
|
1613
|
+
vout: vtxo.vout
|
|
1614
|
+
});
|
|
1615
|
+
if (kind === "indeterminate") {
|
|
1616
|
+
unresolved.add(fundingTx.redeemTxid);
|
|
1617
|
+
continue;
|
|
1618
|
+
}
|
|
1619
|
+
status = kind;
|
|
1620
|
+
}
|
|
1621
|
+
restored.push({
|
|
1622
|
+
id: fundingTx.redeemTxid,
|
|
1623
|
+
fromAsset,
|
|
1624
|
+
toAsset,
|
|
1625
|
+
fromAmount,
|
|
1626
|
+
toAmount: offer.wantAmount.toString(),
|
|
1627
|
+
// ponytail(arkade-os/ts-sdk#680): empty address makes cancel fall back
|
|
1628
|
+
// to the current server key; store the funded address if server-key
|
|
1629
|
+
// rotations become real (cancelOffer now at least diagnoses the
|
|
1630
|
+
// mismatch instead of reporting a missing VTXO)
|
|
1631
|
+
swapAddress: "",
|
|
1632
|
+
swapPkScript,
|
|
1633
|
+
offerHex,
|
|
1634
|
+
fundingTxid: fundingTx.redeemTxid,
|
|
1635
|
+
spentTxid,
|
|
1636
|
+
status,
|
|
1637
|
+
createdAt: fundingTx.createdAt ? fundingTx.createdAt * 1e3 : vtxo.createdAt.getTime(),
|
|
1638
|
+
// the completion time is the caller's record of the spend, if it
|
|
1639
|
+
// has one — the psbt that classified it carries no timestamp
|
|
1640
|
+
...status === "fulfilled" && spentTxid && txByAnyId.get(spentTxid)?.createdAt ? { completedAt: txByAnyId.get(spentTxid).createdAt * 1e3 } : {}
|
|
1641
|
+
});
|
|
1642
|
+
}
|
|
1643
|
+
return { restored, scannedTxids: fetchedTxids.filter((id) => !unresolved.has(id)) };
|
|
1331
1644
|
}
|
|
1332
|
-
|
|
1333
|
-
|
|
1334
|
-
|
|
1335
|
-
|
|
1336
|
-
|
|
1337
|
-
|
|
1338
|
-
|
|
1339
|
-
|
|
1340
|
-
|
|
1341
|
-
|
|
1342
|
-
|
|
1343
|
-
|
|
1344
|
-
|
|
1345
|
-
|
|
1346
|
-
|
|
1347
|
-
|
|
1348
|
-
|
|
1349
|
-
|
|
1350
|
-
|
|
1645
|
+
|
|
1646
|
+
// src/watch.ts
|
|
1647
|
+
var import_base7 = require("@scure/base");
|
|
1648
|
+
var import_sdk7 = require("@arkade-os/sdk");
|
|
1649
|
+
var TERMINAL = ["fulfilled", "cancelled", "recoverable"];
|
|
1650
|
+
function spendUpdate(swap, spend) {
|
|
1651
|
+
if (TERMINAL.includes(swap.status)) return void 0;
|
|
1652
|
+
if (spend.kind === "indeterminate") return void 0;
|
|
1653
|
+
const status = spend.kind === "cancelled" ? "cancelled" : "fulfilled";
|
|
1654
|
+
return {
|
|
1655
|
+
status,
|
|
1656
|
+
spentTxid: spend.txid,
|
|
1657
|
+
// mirrors restore.ts: a completion time is a fill's, not a cancel's
|
|
1658
|
+
...status === "fulfilled" && spend.at ? { completedAt: spend.at } : {}
|
|
1659
|
+
};
|
|
1660
|
+
}
|
|
1661
|
+
async function watchOfferSwaps({
|
|
1662
|
+
wallet,
|
|
1663
|
+
arkServerUrl,
|
|
1664
|
+
repository,
|
|
1665
|
+
onUpdate
|
|
1666
|
+
}) {
|
|
1667
|
+
const manager = await wallet.getContractManager();
|
|
1668
|
+
const serverPubkey = import_sdk7.ArkAddress.decode(await wallet.getAddress()).serverPubKey;
|
|
1669
|
+
const indexer = new import_sdk7.RestIndexerProvider(arkServerUrl);
|
|
1670
|
+
let queue = Promise.resolve();
|
|
1671
|
+
const enqueue = (task) => {
|
|
1672
|
+
queue = queue.then(task).catch(() => {
|
|
1673
|
+
});
|
|
1674
|
+
};
|
|
1675
|
+
const classify = async (swap, vtxo, spentTxid) => {
|
|
1676
|
+
if (swap.spentTxid === spentTxid && swap.status === "cancelling") return "cancelled";
|
|
1677
|
+
try {
|
|
1678
|
+
const candidates = spendTxidsOf(vtxo);
|
|
1679
|
+
if (candidates.length === 0) return "indeterminate";
|
|
1680
|
+
const { txs } = await indexer.getVirtualTxs(candidates);
|
|
1681
|
+
return classifyDepositSpend(
|
|
1682
|
+
decodeOffer(import_base7.hex.decode(swap.offerHex)),
|
|
1683
|
+
serverPubkey,
|
|
1684
|
+
txs.map((psbt) => import_sdk7.Transaction.fromPSBT(import_base7.base64.decode(psbt))),
|
|
1685
|
+
{ txid: vtxo.txid, vout: vtxo.vout }
|
|
1686
|
+
);
|
|
1687
|
+
} catch {
|
|
1688
|
+
return "indeterminate";
|
|
1689
|
+
}
|
|
1690
|
+
};
|
|
1691
|
+
const handleSpend = async (event) => {
|
|
1692
|
+
if (event.contract.metadata?.kind !== OFFER_CONTRACT_KIND) return;
|
|
1693
|
+
for (const vtxo of event.vtxos) {
|
|
1694
|
+
const spentTxid = vtxo.arkTxId || vtxo.spentBy;
|
|
1695
|
+
if (!spentTxid) continue;
|
|
1696
|
+
const swap = (await getAssetSwaps(repository)).find(
|
|
1697
|
+
(s) => s.fundingTxid === vtxo.txid && s.swapPkScript === event.contractScript
|
|
1698
|
+
);
|
|
1699
|
+
if (!swap) continue;
|
|
1700
|
+
const kind = await classify(swap, vtxo, spentTxid);
|
|
1701
|
+
const changes = spendUpdate(swap, { txid: spentTxid, kind, at: event.timestamp });
|
|
1702
|
+
if (!changes) continue;
|
|
1703
|
+
const { persisted, swaps } = await updateAssetSwapBestEffort(
|
|
1704
|
+
repository,
|
|
1705
|
+
swap.id,
|
|
1706
|
+
changes
|
|
1707
|
+
);
|
|
1708
|
+
if (!persisted) continue;
|
|
1709
|
+
onUpdate?.({ ...swap, ...changes });
|
|
1710
|
+
if (changes.status && RETIRABLE.includes(changes.status)) {
|
|
1711
|
+
await retireOfferContract(manager, swaps, event.contractScript);
|
|
1712
|
+
}
|
|
1713
|
+
}
|
|
1714
|
+
};
|
|
1715
|
+
const unsubscribe = manager.onContractEvent((event) => {
|
|
1716
|
+
if (event.type !== "vtxo_spent") return;
|
|
1717
|
+
enqueue(() => handleSpend(event));
|
|
1718
|
+
});
|
|
1719
|
+
return {
|
|
1720
|
+
stop: unsubscribe,
|
|
1721
|
+
idle: () => queue
|
|
1722
|
+
};
|
|
1351
1723
|
}
|
|
1352
1724
|
|
|
1353
1725
|
// src/rfq.ts
|
|
1726
|
+
var import_base10 = require("@scure/base");
|
|
1727
|
+
var import_legacy2 = require("@noble/hashes/legacy.js");
|
|
1354
1728
|
var import_sdk9 = require("@arkade-os/sdk");
|
|
1729
|
+
var import_sdk10 = require("@arkade-os/sdk");
|
|
1355
1730
|
|
|
1356
1731
|
// src/claimPacket.ts
|
|
1357
|
-
var
|
|
1732
|
+
var import_base8 = require("@scure/base");
|
|
1733
|
+
var import_aes = require("@noble/ciphers/aes.js");
|
|
1358
1734
|
var import_secp256k1 = require("@noble/curves/secp256k1.js");
|
|
1359
1735
|
var import_hkdf = require("@noble/hashes/hkdf.js");
|
|
1360
1736
|
var import_sha23 = require("@noble/hashes/sha2.js");
|
|
@@ -1375,33 +1751,34 @@ async function sealWithEntropy(input, ephemeralKey, nonce) {
|
|
|
1375
1751
|
const sharedX = import_secp256k1.secp256k1.getSharedSecret(ephemeralKey, input.covclaimdPubkey, true).subarray(1);
|
|
1376
1752
|
const key = (0, import_hkdf.hkdf)(import_sha23.sha256, sharedX, ephemeralPub, HKDF_INFO, 32);
|
|
1377
1753
|
if (nonce.length !== 12) throw new Error("nonce must be 12 bytes");
|
|
1378
|
-
const
|
|
1379
|
-
"encrypt"
|
|
1380
|
-
]);
|
|
1381
|
-
const sealed = new Uint8Array(
|
|
1382
|
-
await crypto.subtle.encrypt(
|
|
1383
|
-
{
|
|
1384
|
-
name: "AES-GCM",
|
|
1385
|
-
iv: nonce,
|
|
1386
|
-
additionalData: ephemeralPub
|
|
1387
|
-
},
|
|
1388
|
-
aesKey,
|
|
1389
|
-
input.preimage
|
|
1390
|
-
)
|
|
1391
|
-
);
|
|
1754
|
+
const sealed = (0, import_aes.gcm)(key, nonce, ephemeralPub).encrypt(input.preimage);
|
|
1392
1755
|
const packet = new Uint8Array(33 + 12 + sealed.length);
|
|
1393
1756
|
packet.set(ephemeralPub, 0);
|
|
1394
1757
|
packet.set(nonce, 33);
|
|
1395
1758
|
packet.set(sealed, 45);
|
|
1396
|
-
return { ciphertext:
|
|
1759
|
+
return { ciphertext: import_base8.base64.encode(packet) };
|
|
1397
1760
|
}
|
|
1398
1761
|
|
|
1399
1762
|
// src/lockupContract.ts
|
|
1400
|
-
var
|
|
1401
|
-
var
|
|
1763
|
+
var import_base9 = require("@scure/base");
|
|
1764
|
+
var import_sdk8 = require("@arkade-os/sdk");
|
|
1402
1765
|
var SWAP_LOCKUP_CONTRACT_TYPE = "vhtlc-v2";
|
|
1403
1766
|
var SWAP_LOCKUP_CONTRACT_LABEL = "Arkade RFQ swap lockup";
|
|
1404
1767
|
var SWAP_LOCKUP_CONTRACT_KIND = "rfq-swap-lockup";
|
|
1768
|
+
var LockupContractMissing = class extends Error {
|
|
1769
|
+
/** The lockup whose row is absent. */
|
|
1770
|
+
address;
|
|
1771
|
+
/** Its pkScript hex — the key the row would have been under. */
|
|
1772
|
+
script;
|
|
1773
|
+
constructor(address, script) {
|
|
1774
|
+
super(
|
|
1775
|
+
`no contract row for lockup ${address} (script ${script}); its covenant cannot be rebuilt from this wallet's contract store`
|
|
1776
|
+
);
|
|
1777
|
+
this.name = "LockupContractMissing";
|
|
1778
|
+
this.address = address;
|
|
1779
|
+
this.script = script;
|
|
1780
|
+
}
|
|
1781
|
+
};
|
|
1405
1782
|
var LockupRegistrationFailed = class extends Error {
|
|
1406
1783
|
/** The lockup address that was never registered — never fund it: nothing
|
|
1407
1784
|
* is watching it. */
|
|
@@ -1420,8 +1797,8 @@ async function registerLockupContract(contracts, script, address) {
|
|
|
1420
1797
|
try {
|
|
1421
1798
|
await contracts.createContract({
|
|
1422
1799
|
type: SWAP_LOCKUP_CONTRACT_TYPE,
|
|
1423
|
-
params:
|
|
1424
|
-
script:
|
|
1800
|
+
params: import_sdk8.VHTLCV2ContractHandler.serializeParams(script.options),
|
|
1801
|
+
script: import_base9.hex.encode(script.pkScript),
|
|
1425
1802
|
address,
|
|
1426
1803
|
label: SWAP_LOCKUP_CONTRACT_LABEL,
|
|
1427
1804
|
metadata: { genericallySpendable: false, kind: SWAP_LOCKUP_CONTRACT_KIND }
|
|
@@ -1430,18 +1807,17 @@ async function registerLockupContract(contracts, script, address) {
|
|
|
1430
1807
|
throw new LockupRegistrationFailed(script, address, error);
|
|
1431
1808
|
}
|
|
1432
1809
|
}
|
|
1810
|
+
async function lockupContractParams(contracts, lockupAddress) {
|
|
1811
|
+
const script = import_base9.hex.encode(import_sdk8.ArkAddress.decode(lockupAddress).pkScript);
|
|
1812
|
+
const [row] = await contracts.getContracts({ script });
|
|
1813
|
+
if (!row) throw new LockupContractMissing(lockupAddress, script);
|
|
1814
|
+
return row.params;
|
|
1815
|
+
}
|
|
1433
1816
|
|
|
1434
1817
|
// src/rfq.ts
|
|
1435
|
-
var xOnly = (key, label) => {
|
|
1436
|
-
if (key.length === 32) return key;
|
|
1437
|
-
if (key.length !== 33 || key[0] !== 2 && key[0] !== 3) {
|
|
1438
|
-
throw new Error(`${label} is not a compressed or x-only public key`);
|
|
1439
|
-
}
|
|
1440
|
-
return key.slice(1);
|
|
1441
|
-
};
|
|
1442
1818
|
var solverHex = (value, field) => {
|
|
1443
1819
|
try {
|
|
1444
|
-
return
|
|
1820
|
+
return import_base10.hex.decode(value);
|
|
1445
1821
|
} catch {
|
|
1446
1822
|
throw new Error(`solver sent malformed hex for ${field}`);
|
|
1447
1823
|
}
|
|
@@ -1477,7 +1853,7 @@ var AddressMismatch = class extends Error {
|
|
|
1477
1853
|
this.quoted = quoted;
|
|
1478
1854
|
}
|
|
1479
1855
|
};
|
|
1480
|
-
var newRfqId = () =>
|
|
1856
|
+
var newRfqId = () => import_base10.hex.encode(crypto.getRandomValues(new Uint8Array(32)));
|
|
1481
1857
|
var lightningSendRequest = (input) => ({
|
|
1482
1858
|
v: 1,
|
|
1483
1859
|
type: "rfq_request",
|
|
@@ -1487,7 +1863,7 @@ var lightningSendRequest = (input) => ({
|
|
|
1487
1863
|
profile: {
|
|
1488
1864
|
invoice: input.invoice,
|
|
1489
1865
|
refund_address: input.refundAddress,
|
|
1490
|
-
client_refund_pubkey:
|
|
1866
|
+
client_refund_pubkey: import_base10.hex.encode(input.senderPubkey)
|
|
1491
1867
|
}
|
|
1492
1868
|
});
|
|
1493
1869
|
var arkadeSwapRequest = (input) => {
|
|
@@ -1734,11 +2110,11 @@ function lightningSendVtxoScript(params) {
|
|
|
1734
2110
|
type: "seconds",
|
|
1735
2111
|
value: BigInt(value)
|
|
1736
2112
|
});
|
|
1737
|
-
return new
|
|
2113
|
+
return new import_sdk9.VHTLC.ScriptV2({
|
|
1738
2114
|
sender: params.senderPubkey,
|
|
1739
2115
|
receiver: params.solverPubkey,
|
|
1740
2116
|
server: params.serverPubkey,
|
|
1741
|
-
preimageHash: (0, import_legacy2.ripemd160)(
|
|
2117
|
+
preimageHash: (0, import_legacy2.ripemd160)(import_base10.hex.decode(params.paymentHash)),
|
|
1742
2118
|
refundLocktime: BigInt(params.refundLocktime),
|
|
1743
2119
|
unilateralClaimDelay: seconds(params.claimDelay),
|
|
1744
2120
|
unilateralRefundDelay: seconds(unilateralRefundDelay(params.claimDelay)),
|
|
@@ -1757,10 +2133,10 @@ function lightningSendVtxoScript(params) {
|
|
|
1757
2133
|
}
|
|
1758
2134
|
async function requestLightningSend(wallet, arkServerUrl, transport, params) {
|
|
1759
2135
|
const rfqId = params.rfqId ?? newRfqId();
|
|
1760
|
-
const secrets = await (0,
|
|
2136
|
+
const secrets = await (0, import_sdk10.provisionRefundKey)(wallet);
|
|
1761
2137
|
const senderPubkey = secrets.pubkey;
|
|
1762
2138
|
const [info, refundAddress] = await Promise.all([
|
|
1763
|
-
new
|
|
2139
|
+
new import_sdk9.RestArkProvider(arkServerUrl).getInfo(),
|
|
1764
2140
|
wallet.getAddress()
|
|
1765
2141
|
]);
|
|
1766
2142
|
const quote = await transport.requestQuote(
|
|
@@ -1783,22 +2159,23 @@ async function requestLightningSend(wallet, arkServerUrl, transport, params) {
|
|
|
1783
2159
|
`quote from_amount ${quote.from_amount} is below the invoice amount \u2014 a negative spread is not a quote`
|
|
1784
2160
|
);
|
|
1785
2161
|
}
|
|
1786
|
-
const serverPubkey =
|
|
1787
|
-
const network = (0,
|
|
1788
|
-
const
|
|
1789
|
-
solverPubkey:
|
|
2162
|
+
const serverPubkey = (0, import_sdk9.toXOnly)(import_base10.hex.decode(info.signerPubkey), "ark signer key");
|
|
2163
|
+
const network = (0, import_sdk9.getNetwork)(info.network);
|
|
2164
|
+
const treeParams = {
|
|
2165
|
+
solverPubkey: (0, import_sdk9.toXOnly)(import_base10.hex.decode(quote.solver_pubkey), "solver key"),
|
|
1790
2166
|
refundLocktime: quote.refund_locktime,
|
|
1791
2167
|
serverPubkey,
|
|
1792
2168
|
paymentHash: params.invoice.paymentHash,
|
|
1793
2169
|
claimDelay: unilateralClaimDelay(Number(info.unilateralExitDelay)),
|
|
1794
|
-
emulatorPubkey:
|
|
1795
|
-
|
|
2170
|
+
emulatorPubkey: (0, import_sdk9.toXOnly)(
|
|
2171
|
+
import_base10.hex.decode((0, import_sdk9.resolveEmulatorPubkey)(network, params.emulatorPubkey)),
|
|
1796
2172
|
"emulator signer key"
|
|
1797
2173
|
),
|
|
1798
2174
|
senderPubkey,
|
|
1799
2175
|
receiverPkScript: solverHex(receiverPkScriptHex, "profile.receiver_pk_script"),
|
|
1800
|
-
refundPkScript:
|
|
1801
|
-
}
|
|
2176
|
+
refundPkScript: import_sdk9.ArkAddress.decode(refundAddress).pkScript
|
|
2177
|
+
};
|
|
2178
|
+
const script = lightningSendVtxoScript(treeParams);
|
|
1802
2179
|
const address = script.address(network.hrp, serverPubkey).encode();
|
|
1803
2180
|
verifyLockupAddress(quote, address);
|
|
1804
2181
|
assertFundable({
|
|
@@ -1818,7 +2195,8 @@ async function requestLightningSend(wallet, arkServerUrl, transport, params) {
|
|
|
1818
2195
|
script,
|
|
1819
2196
|
refundAddress,
|
|
1820
2197
|
senderPubkey,
|
|
1821
|
-
secrets
|
|
2198
|
+
secrets,
|
|
2199
|
+
treeParams
|
|
1822
2200
|
};
|
|
1823
2201
|
}
|
|
1824
2202
|
var offerTermsFromQuote = (quote, assets) => {
|
|
@@ -1837,9 +2215,9 @@ var onchainSendRequest = (input) => ({
|
|
|
1837
2215
|
amount: input.amount,
|
|
1838
2216
|
profile: {
|
|
1839
2217
|
payment_hash: input.paymentHash,
|
|
1840
|
-
payout_pubkey:
|
|
2218
|
+
payout_pubkey: import_base10.hex.encode(input.payoutPubkey),
|
|
1841
2219
|
refund_address: input.refundAddress,
|
|
1842
|
-
client_refund_pubkey:
|
|
2220
|
+
client_refund_pubkey: import_base10.hex.encode(input.senderPubkey)
|
|
1843
2221
|
}
|
|
1844
2222
|
});
|
|
1845
2223
|
var lightningReceiveRequest = (input) => ({
|
|
@@ -1852,7 +2230,7 @@ var lightningReceiveRequest = (input) => ({
|
|
|
1852
2230
|
profile: {
|
|
1853
2231
|
payment_hash: input.paymentHash,
|
|
1854
2232
|
payout_address: input.payoutAddress,
|
|
1855
|
-
payout_pubkey:
|
|
2233
|
+
payout_pubkey: import_base10.hex.encode(input.payoutPubkey),
|
|
1856
2234
|
claim_packet: input.claimPacket
|
|
1857
2235
|
}
|
|
1858
2236
|
});
|
|
@@ -1866,9 +2244,9 @@ var onchainReceiveRequest = (input) => ({
|
|
|
1866
2244
|
profile: {
|
|
1867
2245
|
payment_hash: input.paymentHash,
|
|
1868
2246
|
claim_packet: input.claimPacket,
|
|
1869
|
-
refund_pubkey:
|
|
2247
|
+
refund_pubkey: import_base10.hex.encode(input.refundPubkey),
|
|
1870
2248
|
payout_address: input.payoutAddress,
|
|
1871
|
-
payout_pubkey:
|
|
2249
|
+
payout_pubkey: import_base10.hex.encode(input.payoutPubkey)
|
|
1872
2250
|
}
|
|
1873
2251
|
});
|
|
1874
2252
|
function deriveOnchainSend(input) {
|
|
@@ -1884,7 +2262,7 @@ function deriveOnchainSend(input) {
|
|
|
1884
2262
|
throw new Error("onchain-send quote is missing a binding field");
|
|
1885
2263
|
}
|
|
1886
2264
|
const script = lightningSendVtxoScript({
|
|
1887
|
-
solverPubkey:
|
|
2265
|
+
solverPubkey: (0, import_sdk9.toXOnly)(import_base10.hex.decode(quote.solver_pubkey), "solver key"),
|
|
1888
2266
|
refundLocktime,
|
|
1889
2267
|
serverPubkey: input.serverPubkey,
|
|
1890
2268
|
paymentHash: input.paymentHash,
|
|
@@ -1892,25 +2270,25 @@ function deriveOnchainSend(input) {
|
|
|
1892
2270
|
emulatorPubkey: input.emulatorPubkey,
|
|
1893
2271
|
senderPubkey: input.senderPubkey,
|
|
1894
2272
|
receiverPkScript: solverHex(receiverPkScriptHex, "profile.receiver_pk_script"),
|
|
1895
|
-
refundPkScript:
|
|
2273
|
+
refundPkScript: import_sdk9.ArkAddress.decode(input.refundAddress).pkScript
|
|
1896
2274
|
});
|
|
1897
2275
|
const address = script.address(input.hrp, input.serverPubkey).encode();
|
|
1898
2276
|
verifyLockupAddress(quote, address);
|
|
1899
|
-
const
|
|
1900
|
-
|
|
1901
|
-
|
|
1902
|
-
|
|
1903
|
-
|
|
1904
|
-
|
|
1905
|
-
|
|
1906
|
-
input.l1Network
|
|
1907
|
-
);
|
|
2277
|
+
const htlcParams = {
|
|
2278
|
+
paymentHash: input.paymentHash,
|
|
2279
|
+
claimKey: input.payoutPubkey,
|
|
2280
|
+
refundKey: (0, import_sdk9.toXOnly)(import_base10.hex.decode(htlcPubkey), "solver L1 htlc key"),
|
|
2281
|
+
refundLocktime: htlcLocktime
|
|
2282
|
+
};
|
|
2283
|
+
const htlc = onchainHtlcScript(htlcParams, input.l1Network);
|
|
1908
2284
|
if (htlc.address !== htlcAddress) throw new AddressMismatch(htlc.address, htlcAddress);
|
|
1909
2285
|
return {
|
|
1910
2286
|
address,
|
|
1911
2287
|
swapPkScript: script.pkScript,
|
|
1912
2288
|
script,
|
|
1913
2289
|
htlc,
|
|
2290
|
+
htlcParams,
|
|
2291
|
+
l1Network: input.l1Network,
|
|
1914
2292
|
refundLocktime,
|
|
1915
2293
|
htlcLocktime,
|
|
1916
2294
|
minConfirmations
|
|
@@ -1918,16 +2296,16 @@ function deriveOnchainSend(input) {
|
|
|
1918
2296
|
}
|
|
1919
2297
|
async function requestOnchainSend(wallet, arkServerUrl, transport, params) {
|
|
1920
2298
|
const rfqId = params.rfqId ?? newRfqId();
|
|
1921
|
-
const secrets = await (0,
|
|
2299
|
+
const secrets = await (0, import_sdk10.provisionClaimSecret)(wallet, { preimage: params.preimage });
|
|
1922
2300
|
if (secrets.mustPersistPreimage) {
|
|
1923
2301
|
console.warn(
|
|
1924
2302
|
"[swap] this swap's preimage cannot be re-derived from the seed and MUST be persisted with the record before funding"
|
|
1925
2303
|
);
|
|
1926
2304
|
}
|
|
1927
|
-
const paymentHash =
|
|
2305
|
+
const paymentHash = import_base10.hex.encode(secrets.paymentHash);
|
|
1928
2306
|
const senderPubkey = secrets.pubkey;
|
|
1929
2307
|
const [info, refundAddress] = await Promise.all([
|
|
1930
|
-
new
|
|
2308
|
+
new import_sdk9.RestArkProvider(arkServerUrl).getInfo(),
|
|
1931
2309
|
wallet.getAddress()
|
|
1932
2310
|
]);
|
|
1933
2311
|
const quote = await transport.requestQuote(
|
|
@@ -1941,14 +2319,14 @@ async function requestOnchainSend(wallet, arkServerUrl, transport, params) {
|
|
|
1941
2319
|
amountSide: params.amountSide
|
|
1942
2320
|
})
|
|
1943
2321
|
);
|
|
1944
|
-
const network = (0,
|
|
2322
|
+
const network = (0, import_sdk9.getNetwork)(info.network);
|
|
1945
2323
|
const derived = deriveOnchainSend({
|
|
1946
2324
|
quote,
|
|
1947
2325
|
paymentHash,
|
|
1948
2326
|
payoutPubkey: params.payoutPubkey,
|
|
1949
|
-
serverPubkey:
|
|
1950
|
-
emulatorPubkey:
|
|
1951
|
-
|
|
2327
|
+
serverPubkey: (0, import_sdk9.toXOnly)(import_base10.hex.decode(info.signerPubkey), "ark signer key"),
|
|
2328
|
+
emulatorPubkey: (0, import_sdk9.toXOnly)(
|
|
2329
|
+
import_base10.hex.decode((0, import_sdk9.resolveEmulatorPubkey)(network, params.emulatorPubkey)),
|
|
1952
2330
|
"emulator signer key"
|
|
1953
2331
|
),
|
|
1954
2332
|
claimDelay: unilateralClaimDelay(Number(info.unilateralExitDelay)),
|
|
@@ -1980,6 +2358,9 @@ async function requestOnchainSend(wallet, arkServerUrl, transport, params) {
|
|
|
1980
2358
|
script: derived.script,
|
|
1981
2359
|
refundAddress,
|
|
1982
2360
|
htlc: derived.htlc,
|
|
2361
|
+
htlcParams: derived.htlcParams,
|
|
2362
|
+
l1Network: derived.l1Network,
|
|
2363
|
+
minConfirmations: derived.minConfirmations,
|
|
1983
2364
|
senderPubkey,
|
|
1984
2365
|
secrets
|
|
1985
2366
|
};
|
|
@@ -2056,11 +2437,11 @@ function receiveVtxoScript(params) {
|
|
|
2056
2437
|
type: "seconds",
|
|
2057
2438
|
value: BigInt(value)
|
|
2058
2439
|
});
|
|
2059
|
-
return new
|
|
2440
|
+
return new import_sdk9.VHTLC.ScriptV2({
|
|
2060
2441
|
sender: params.solverPubkey,
|
|
2061
2442
|
receiver: params.payoutPubkey,
|
|
2062
2443
|
server: params.serverPubkey,
|
|
2063
|
-
preimageHash: (0, import_legacy2.ripemd160)(
|
|
2444
|
+
preimageHash: (0, import_legacy2.ripemd160)(import_base10.hex.decode(params.paymentHash)),
|
|
2064
2445
|
refundLocktime: BigInt(params.refundLocktime),
|
|
2065
2446
|
unilateralClaimDelay: seconds(params.claimDelay),
|
|
2066
2447
|
unilateralRefundDelay: seconds(unilateralRefundDelay(params.claimDelay)),
|
|
@@ -2086,8 +2467,8 @@ function deriveLightningReceive(input) {
|
|
|
2086
2467
|
if (refundLocktime === void 0 || invoice === void 0 || solverRefundPkScriptHex === void 0) {
|
|
2087
2468
|
throw new Error("lightning-receive quote is missing a binding field");
|
|
2088
2469
|
}
|
|
2089
|
-
const
|
|
2090
|
-
solverPubkey:
|
|
2470
|
+
const treeParams = {
|
|
2471
|
+
solverPubkey: (0, import_sdk9.toXOnly)(import_base10.hex.decode(quote.solver_pubkey), "solver key"),
|
|
2091
2472
|
refundLocktime,
|
|
2092
2473
|
serverPubkey: input.serverPubkey,
|
|
2093
2474
|
paymentHash: input.paymentHash,
|
|
@@ -2095,25 +2476,26 @@ function deriveLightningReceive(input) {
|
|
|
2095
2476
|
emulatorPubkey: input.emulatorPubkey,
|
|
2096
2477
|
solverRefundPkScript: solverHex(solverRefundPkScriptHex, "profile.solver_refund_pk_script"),
|
|
2097
2478
|
payoutPubkey: input.payoutPubkey,
|
|
2098
|
-
payoutPkScript:
|
|
2099
|
-
}
|
|
2479
|
+
payoutPkScript: import_sdk9.ArkAddress.decode(input.payoutAddress).pkScript
|
|
2480
|
+
};
|
|
2481
|
+
const script = receiveVtxoScript(treeParams);
|
|
2100
2482
|
const address = script.address(input.hrp, input.serverPubkey).encode();
|
|
2101
2483
|
verifyLockupAddress(quote, address);
|
|
2102
|
-
return { address, swapPkScript: script.pkScript, script, invoice, refundLocktime };
|
|
2484
|
+
return { address, swapPkScript: script.pkScript, script, invoice, refundLocktime, treeParams };
|
|
2103
2485
|
}
|
|
2104
2486
|
async function requestLightningReceive(wallet, arkServerUrl, transport, params) {
|
|
2105
2487
|
const rfqId = params.rfqId ?? newRfqId();
|
|
2106
|
-
const secrets = await (0,
|
|
2488
|
+
const secrets = await (0, import_sdk10.provisionClaimSecret)(wallet);
|
|
2107
2489
|
if (secrets.mustPersistPreimage) {
|
|
2108
2490
|
console.warn(
|
|
2109
2491
|
"[swap] this swap's preimage cannot be re-derived from the seed and MUST be persisted with the record before paying"
|
|
2110
2492
|
);
|
|
2111
2493
|
}
|
|
2112
2494
|
const preimage = secrets.preimage;
|
|
2113
|
-
const paymentHash =
|
|
2495
|
+
const paymentHash = import_base10.hex.encode(secrets.paymentHash);
|
|
2114
2496
|
const payoutPubkey = secrets.pubkey;
|
|
2115
2497
|
const [info, payoutAddress] = await Promise.all([
|
|
2116
|
-
new
|
|
2498
|
+
new import_sdk9.RestArkProvider(arkServerUrl).getInfo(),
|
|
2117
2499
|
wallet.getAddress()
|
|
2118
2500
|
]);
|
|
2119
2501
|
const claimPacket = await sealClaimPacket({
|
|
@@ -2132,15 +2514,15 @@ async function requestLightningReceive(wallet, arkServerUrl, transport, params)
|
|
|
2132
2514
|
})
|
|
2133
2515
|
);
|
|
2134
2516
|
assertQuotedAmount(quote, params.amountSide, params.amount);
|
|
2135
|
-
const network = (0,
|
|
2517
|
+
const network = (0, import_sdk9.getNetwork)(info.network);
|
|
2136
2518
|
const derived = deriveLightningReceive({
|
|
2137
2519
|
quote,
|
|
2138
2520
|
paymentHash,
|
|
2139
2521
|
payoutPubkey,
|
|
2140
2522
|
payoutAddress,
|
|
2141
|
-
serverPubkey:
|
|
2142
|
-
emulatorPubkey:
|
|
2143
|
-
|
|
2523
|
+
serverPubkey: (0, import_sdk9.toXOnly)(import_base10.hex.decode(info.signerPubkey), "ark signer key"),
|
|
2524
|
+
emulatorPubkey: (0, import_sdk9.toXOnly)(
|
|
2525
|
+
import_base10.hex.decode((0, import_sdk9.resolveEmulatorPubkey)(network, params.emulatorPubkey)),
|
|
2144
2526
|
"emulator signer key"
|
|
2145
2527
|
),
|
|
2146
2528
|
claimDelay: unilateralClaimDelay(Number(info.unilateralExitDelay)),
|
|
@@ -2171,7 +2553,8 @@ async function requestLightningReceive(wallet, arkServerUrl, transport, params)
|
|
|
2171
2553
|
script: derived.script,
|
|
2172
2554
|
payoutAddress,
|
|
2173
2555
|
payoutPubkey,
|
|
2174
|
-
secrets
|
|
2556
|
+
secrets,
|
|
2557
|
+
treeParams: derived.treeParams
|
|
2175
2558
|
};
|
|
2176
2559
|
}
|
|
2177
2560
|
function deriveOnchainReceive(input) {
|
|
@@ -2187,7 +2570,7 @@ function deriveOnchainReceive(input) {
|
|
|
2187
2570
|
throw new Error("onchain-receive quote is missing a binding field");
|
|
2188
2571
|
}
|
|
2189
2572
|
const script = receiveVtxoScript({
|
|
2190
|
-
solverPubkey:
|
|
2573
|
+
solverPubkey: (0, import_sdk9.toXOnly)(import_base10.hex.decode(quote.solver_pubkey), "solver key"),
|
|
2191
2574
|
refundLocktime,
|
|
2192
2575
|
serverPubkey: input.serverPubkey,
|
|
2193
2576
|
paymentHash: input.paymentHash,
|
|
@@ -2195,14 +2578,14 @@ function deriveOnchainReceive(input) {
|
|
|
2195
2578
|
emulatorPubkey: input.emulatorPubkey,
|
|
2196
2579
|
solverRefundPkScript: solverHex(solverRefundPkScriptHex, "profile.solver_refund_pk_script"),
|
|
2197
2580
|
payoutPubkey: input.payoutPubkey,
|
|
2198
|
-
payoutPkScript:
|
|
2581
|
+
payoutPkScript: import_sdk9.ArkAddress.decode(input.payoutAddress).pkScript
|
|
2199
2582
|
});
|
|
2200
2583
|
const address = script.address(input.hrp, input.serverPubkey).encode();
|
|
2201
2584
|
verifyLockupAddress(quote, address);
|
|
2202
2585
|
const htlc = onchainHtlcScript(
|
|
2203
2586
|
{
|
|
2204
2587
|
paymentHash: input.paymentHash,
|
|
2205
|
-
claimKey:
|
|
2588
|
+
claimKey: (0, import_sdk9.toXOnly)(import_base10.hex.decode(claimPubkey), "solver L1 claim key"),
|
|
2206
2589
|
refundKey: input.refundPubkey,
|
|
2207
2590
|
refundLocktime: htlcLocktime
|
|
2208
2591
|
},
|
|
@@ -2221,17 +2604,17 @@ function deriveOnchainReceive(input) {
|
|
|
2221
2604
|
}
|
|
2222
2605
|
async function requestOnchainReceive(wallet, arkServerUrl, transport, params) {
|
|
2223
2606
|
const rfqId = params.rfqId ?? newRfqId();
|
|
2224
|
-
const secrets = await (0,
|
|
2607
|
+
const secrets = await (0, import_sdk10.provisionClaimSecret)(wallet);
|
|
2225
2608
|
if (secrets.mustPersistPreimage) {
|
|
2226
2609
|
console.warn(
|
|
2227
2610
|
"[swap] this swap's preimage cannot be re-derived from the seed and MUST be persisted with the record before funding"
|
|
2228
2611
|
);
|
|
2229
2612
|
}
|
|
2230
2613
|
const preimage = secrets.preimage;
|
|
2231
|
-
const paymentHash =
|
|
2614
|
+
const paymentHash = import_base10.hex.encode(secrets.paymentHash);
|
|
2232
2615
|
const payoutPubkey = secrets.pubkey;
|
|
2233
2616
|
const [info, payoutAddress] = await Promise.all([
|
|
2234
|
-
new
|
|
2617
|
+
new import_sdk9.RestArkProvider(arkServerUrl).getInfo(),
|
|
2235
2618
|
wallet.getAddress()
|
|
2236
2619
|
]);
|
|
2237
2620
|
const claimPacket = await sealClaimPacket({
|
|
@@ -2251,16 +2634,16 @@ async function requestOnchainReceive(wallet, arkServerUrl, transport, params) {
|
|
|
2251
2634
|
})
|
|
2252
2635
|
);
|
|
2253
2636
|
assertQuotedAmount(quote, params.amountSide, params.amount);
|
|
2254
|
-
const network = (0,
|
|
2637
|
+
const network = (0, import_sdk9.getNetwork)(info.network);
|
|
2255
2638
|
const derived = deriveOnchainReceive({
|
|
2256
2639
|
quote,
|
|
2257
2640
|
paymentHash,
|
|
2258
2641
|
payoutPubkey,
|
|
2259
2642
|
payoutAddress,
|
|
2260
2643
|
refundPubkey: params.refundPubkey,
|
|
2261
|
-
serverPubkey:
|
|
2262
|
-
emulatorPubkey:
|
|
2263
|
-
|
|
2644
|
+
serverPubkey: (0, import_sdk9.toXOnly)(import_base10.hex.decode(info.signerPubkey), "ark signer key"),
|
|
2645
|
+
emulatorPubkey: (0, import_sdk9.toXOnly)(
|
|
2646
|
+
import_base10.hex.decode((0, import_sdk9.resolveEmulatorPubkey)(network, params.emulatorPubkey)),
|
|
2264
2647
|
"emulator signer key"
|
|
2265
2648
|
),
|
|
2266
2649
|
claimDelay: unilateralClaimDelay(Number(info.unilateralExitDelay)),
|
|
@@ -2297,15 +2680,15 @@ async function requestOnchainReceive(wallet, arkServerUrl, transport, params) {
|
|
|
2297
2680
|
}
|
|
2298
2681
|
|
|
2299
2682
|
// src/claim.ts
|
|
2300
|
-
var
|
|
2683
|
+
var import_base12 = require("@scure/base");
|
|
2301
2684
|
var import_legacy3 = require("@noble/hashes/legacy.js");
|
|
2302
2685
|
var import_sha25 = require("@noble/hashes/sha2.js");
|
|
2303
|
-
var
|
|
2686
|
+
var import_sdk12 = require("@arkade-os/sdk");
|
|
2304
2687
|
|
|
2305
2688
|
// src/refund.ts
|
|
2306
|
-
var
|
|
2689
|
+
var import_base11 = require("@scure/base");
|
|
2307
2690
|
var import_sha24 = require("@noble/hashes/sha2.js");
|
|
2308
|
-
var
|
|
2691
|
+
var import_sdk11 = require("@arkade-os/sdk");
|
|
2309
2692
|
var sleep2 = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
2310
2693
|
var isRfqTerminal = (state) => RFQ_TERMINAL_STATES.includes(state);
|
|
2311
2694
|
var RFQ_RESOLVED_STATES = ["settled", "refunded"];
|
|
@@ -2352,7 +2735,7 @@ var LockupNeedsRecoveryError = class extends Error {
|
|
|
2352
2735
|
}
|
|
2353
2736
|
};
|
|
2354
2737
|
async function findLockupVtxos(indexer, swapPkScript) {
|
|
2355
|
-
const scripts = [
|
|
2738
|
+
const scripts = [import_base11.hex.encode(swapPkScript)];
|
|
2356
2739
|
const [spendable, recoverable] = await Promise.all([
|
|
2357
2740
|
indexer.getVtxos({ scripts, spendableOnly: true }),
|
|
2358
2741
|
indexer.getVtxos({ scripts, recoverableOnly: true })
|
|
@@ -2377,28 +2760,33 @@ async function findLockupVtxos(indexer, swapPkScript) {
|
|
|
2377
2760
|
}
|
|
2378
2761
|
return out;
|
|
2379
2762
|
}
|
|
2380
|
-
var hashesTo = (candidate, paymentHash) =>
|
|
2763
|
+
var hashesTo = (candidate, paymentHash) => import_base11.hex.encode((0, import_sha24.sha256)(candidate)) === paymentHash;
|
|
2381
2764
|
var candidateWitnessItems = (tx, inputIndex) => [
|
|
2382
|
-
...(0,
|
|
2765
|
+
...(0, import_sdk11.getArkPsbtFields)(tx, inputIndex, import_sdk11.ConditionWitness).flat(),
|
|
2383
2766
|
...tx.getInput(inputIndex).finalScriptWitness ?? []
|
|
2384
2767
|
];
|
|
2385
2768
|
async function readLockupFate(indexer, input) {
|
|
2386
|
-
const { vtxos } = await indexer.getVtxos({ scripts: [
|
|
2769
|
+
const { vtxos } = await indexer.getVtxos({ scripts: [import_base11.hex.encode(input.swapPkScript)] });
|
|
2387
2770
|
const all = vtxos ?? [];
|
|
2388
2771
|
if (all.length === 0) return { fate: "unknown" };
|
|
2389
|
-
const spentBy = /* @__PURE__ */ new
|
|
2772
|
+
const spentBy = /* @__PURE__ */ new Map();
|
|
2390
2773
|
let everySpendNamed = true;
|
|
2391
2774
|
for (const vtxo of all) {
|
|
2392
2775
|
if (!vtxo.isSpent && !vtxo.spentBy && !vtxo.settledBy) return { fate: "open" };
|
|
2393
|
-
if (vtxo.spentBy)
|
|
2776
|
+
if (vtxo.spentBy)
|
|
2777
|
+
spentBy.set(vtxo.spentBy, {
|
|
2778
|
+
checkpointTxid: vtxo.spentBy,
|
|
2779
|
+
arkTxid: vtxo.arkTxId
|
|
2780
|
+
});
|
|
2394
2781
|
else everySpendNamed = false;
|
|
2395
2782
|
}
|
|
2396
|
-
const
|
|
2783
|
+
const spends = [...spentBy.values()];
|
|
2784
|
+
const { txs } = await indexer.getVirtualTxs([...spentBy.keys()]);
|
|
2397
2785
|
const observed = /* @__PURE__ */ new Set();
|
|
2398
2786
|
for (const raw of txs) {
|
|
2399
2787
|
let tx;
|
|
2400
2788
|
try {
|
|
2401
|
-
tx =
|
|
2789
|
+
tx = import_sdk11.Transaction.fromPSBT(import_base11.base64.decode(raw));
|
|
2402
2790
|
} catch {
|
|
2403
2791
|
continue;
|
|
2404
2792
|
}
|
|
@@ -2406,16 +2794,16 @@ async function readLockupFate(indexer, input) {
|
|
|
2406
2794
|
for (let i = 0; i < tx.inputsLength; i++) {
|
|
2407
2795
|
const spent = tx.getInput(i);
|
|
2408
2796
|
if (!spent.txid) continue;
|
|
2409
|
-
const txid =
|
|
2797
|
+
const txid = import_base11.hex.encode(spent.txid);
|
|
2410
2798
|
if (!all.some((vtxo) => vtxo.txid === txid && vtxo.vout === spent.index)) continue;
|
|
2411
2799
|
for (const candidate of candidateWitnessItems(tx, i)) {
|
|
2412
2800
|
if (hashesTo(candidate, input.paymentHash)) {
|
|
2413
|
-
return { fate: "claimed", preimage: candidate };
|
|
2801
|
+
return { fate: "claimed", preimage: candidate, spends };
|
|
2414
2802
|
}
|
|
2415
2803
|
}
|
|
2416
2804
|
}
|
|
2417
2805
|
}
|
|
2418
|
-
return everySpendNamed && observed.size === spentBy.size ? { fate: "returned" } : { fate: "unknown" };
|
|
2806
|
+
return everySpendNamed && observed.size === spentBy.size ? { fate: "returned", spends } : { fate: "unknown" };
|
|
2419
2807
|
}
|
|
2420
2808
|
async function pushRefundWithoutReceiver(ark, input) {
|
|
2421
2809
|
if (input.vtxos.length === 0) throw new Error("nothing to refund: no funded outputs");
|
|
@@ -2435,14 +2823,14 @@ async function pushRefundWithoutReceiver(ark, input) {
|
|
|
2435
2823
|
const info = await ark.getInfo();
|
|
2436
2824
|
let serverUnrollScript;
|
|
2437
2825
|
try {
|
|
2438
|
-
serverUnrollScript =
|
|
2826
|
+
serverUnrollScript = import_sdk11.CSVMultisigTapscript.decode(import_base11.hex.decode(info.checkpointTapscript));
|
|
2439
2827
|
} catch {
|
|
2440
2828
|
throw new Error("invalid checkpointTapscript from the Arkade server");
|
|
2441
2829
|
}
|
|
2442
2830
|
const leaf = input.script.refundWithoutReceiver();
|
|
2443
2831
|
const tapTree = input.script.encode();
|
|
2444
2832
|
const amount = input.vtxos.reduce((sum, vtxo) => sum + vtxo.value, 0);
|
|
2445
|
-
const { arkTx, checkpoints } = (0,
|
|
2833
|
+
const { arkTx, checkpoints } = (0, import_sdk11.buildOffchainTx)(
|
|
2446
2834
|
input.vtxos.map((vtxo) => ({
|
|
2447
2835
|
txid: vtxo.txid,
|
|
2448
2836
|
vout: vtxo.vout,
|
|
@@ -2455,18 +2843,18 @@ async function pushRefundWithoutReceiver(ark, input) {
|
|
|
2455
2843
|
);
|
|
2456
2844
|
const signedArkTx = await input.sender.sign(arkTx);
|
|
2457
2845
|
const submitted = await ark.submitTx(
|
|
2458
|
-
|
|
2459
|
-
checkpoints.map((c) =>
|
|
2846
|
+
import_base11.base64.encode(signedArkTx.toPSBT()),
|
|
2847
|
+
checkpoints.map((c) => import_base11.base64.encode(c.toPSBT()))
|
|
2460
2848
|
);
|
|
2461
|
-
(0,
|
|
2462
|
-
const matched = (0,
|
|
2849
|
+
(0, import_sdk11.assertSubmittedArkTxid)(submitted, signedArkTx, "refundWithoutReceiver");
|
|
2850
|
+
const matched = (0, import_sdk11.matchServerCheckpoints)(
|
|
2463
2851
|
submitted.signedCheckpointTxs,
|
|
2464
2852
|
checkpoints,
|
|
2465
2853
|
"refundWithoutReceiver"
|
|
2466
2854
|
);
|
|
2467
2855
|
const finalCheckpoints = await Promise.all(
|
|
2468
2856
|
matched.map(
|
|
2469
|
-
async ({ server }) =>
|
|
2857
|
+
async ({ server }) => import_base11.base64.encode((await input.sender.sign(server, [0])).toPSBT())
|
|
2470
2858
|
)
|
|
2471
2859
|
);
|
|
2472
2860
|
await ark.finalizeTx(submitted.arkTxid, finalCheckpoints);
|
|
@@ -2546,20 +2934,20 @@ async function pushClaim(ark, input) {
|
|
|
2546
2934
|
}
|
|
2547
2935
|
}
|
|
2548
2936
|
const committed = input.script.options.preimageHash;
|
|
2549
|
-
if (
|
|
2937
|
+
if (import_base12.hex.encode((0, import_legacy3.ripemd160)((0, import_sha25.sha256)(input.preimage))) !== import_base12.hex.encode(committed)) {
|
|
2550
2938
|
throw new Error("preimage does not match the covenant's payment hash");
|
|
2551
2939
|
}
|
|
2552
2940
|
const info = await ark.getInfo();
|
|
2553
2941
|
let serverUnrollScript;
|
|
2554
2942
|
try {
|
|
2555
|
-
serverUnrollScript =
|
|
2943
|
+
serverUnrollScript = import_sdk12.CSVMultisigTapscript.decode(import_base12.hex.decode(info.checkpointTapscript));
|
|
2556
2944
|
} catch {
|
|
2557
2945
|
throw new Error("invalid checkpointTapscript from the Arkade server");
|
|
2558
2946
|
}
|
|
2559
2947
|
const leaf = input.script.claim();
|
|
2560
2948
|
const tapTree = input.script.encode();
|
|
2561
|
-
const arkTxid = await (0,
|
|
2562
|
-
identity: (0,
|
|
2949
|
+
const arkTxid = await (0, import_sdk12.signAndSubmitOffchainTx)({
|
|
2950
|
+
identity: (0, import_sdk12.claimWithPreimageIdentity)(input.receiver, input.preimage),
|
|
2563
2951
|
provider: ark,
|
|
2564
2952
|
inputs: input.vtxos.map((vtxo) => ({
|
|
2565
2953
|
txid: vtxo.txid,
|
|
@@ -2606,7 +2994,7 @@ async function claimReceiveLockup(indexer, ark, input) {
|
|
|
2606
2994
|
}
|
|
2607
2995
|
|
|
2608
2996
|
// src/refundBlocked.ts
|
|
2609
|
-
var
|
|
2997
|
+
var import_sdk13 = require("@arkade-os/sdk");
|
|
2610
2998
|
var RefundNotLocallyPossibleError = class extends Error {
|
|
2611
2999
|
constructor(reason, message, options) {
|
|
2612
3000
|
super(message, options);
|
|
@@ -2623,16 +3011,16 @@ async function senderIdentityForSwapRecord(wallet, record) {
|
|
|
2623
3011
|
);
|
|
2624
3012
|
}
|
|
2625
3013
|
try {
|
|
2626
|
-
return await (0,
|
|
3014
|
+
return await (0, import_sdk13.contractSigner)(wallet, record.signingDescriptor);
|
|
2627
3015
|
} catch (cause) {
|
|
2628
|
-
if (cause instanceof
|
|
3016
|
+
if (cause instanceof import_sdk13.WalletCannotSignError) {
|
|
2629
3017
|
throw new RefundNotLocallyPossibleError(
|
|
2630
3018
|
"unsignable-wallet",
|
|
2631
3019
|
`this wallet holds ${record.signingDescriptor} but cannot sign with it; attach its signer`,
|
|
2632
3020
|
{ cause }
|
|
2633
3021
|
);
|
|
2634
3022
|
}
|
|
2635
|
-
if (cause instanceof
|
|
3023
|
+
if (cause instanceof import_sdk13.ForeignDescriptorError) {
|
|
2636
3024
|
throw new RefundNotLocallyPossibleError(
|
|
2637
3025
|
"foreign-descriptor",
|
|
2638
3026
|
`this wallet cannot derive ${record.signingDescriptor}; the swap was created on another wallet`,
|
|
@@ -2643,10 +3031,31 @@ async function senderIdentityForSwapRecord(wallet, record) {
|
|
|
2643
3031
|
}
|
|
2644
3032
|
}
|
|
2645
3033
|
|
|
3034
|
+
// src/arkadeRefunder.ts
|
|
3035
|
+
function arkadeRefunder(deps) {
|
|
3036
|
+
return async (swap) => {
|
|
3037
|
+
const script = swap.lockup?.script;
|
|
3038
|
+
if (!script) {
|
|
3039
|
+
throw new Error(
|
|
3040
|
+
`swap ${swap.rfqId} carries no lockup covenant, so its refund cannot be built`
|
|
3041
|
+
);
|
|
3042
|
+
}
|
|
3043
|
+
const vtxos = await findLockupVtxos(deps.indexer, swap.lockupPkScript);
|
|
3044
|
+
if (vtxos.length === 0) return null;
|
|
3045
|
+
const record = await deps.repository.getRfqSwap(swap.rfqId);
|
|
3046
|
+
if (!record) {
|
|
3047
|
+
throw new RefundNotLocallyPossibleError(
|
|
3048
|
+
"no-secrets",
|
|
3049
|
+
`no stored record for ${swap.rfqId}; the descriptor that signs its refund lives there`
|
|
3050
|
+
);
|
|
3051
|
+
}
|
|
3052
|
+
const sender = await senderIdentityForSwapRecord(deps.wallet, rfqSignerOf(record) ?? {});
|
|
3053
|
+
return pushRefundWithoutReceiver(deps.ark, { script, sender, vtxos });
|
|
3054
|
+
};
|
|
3055
|
+
}
|
|
3056
|
+
|
|
2646
3057
|
// src/swapManager.ts
|
|
2647
|
-
var
|
|
2648
|
-
var RFQ_SWAP_TERMINAL_STATES = ["settled", "refunded", "failed"];
|
|
2649
|
-
var isRfqSwapTerminal = (state) => RFQ_SWAP_TERMINAL_STATES.includes(state);
|
|
3058
|
+
var import_base13 = require("@scure/base");
|
|
2650
3059
|
function nextOnchainAction(input) {
|
|
2651
3060
|
switch (input.phase.phase) {
|
|
2652
3061
|
case "unfunded":
|
|
@@ -2662,6 +3071,17 @@ function nextOnchainAction(input) {
|
|
|
2662
3071
|
return input.htlcLocktime - input.now >= ONCHAIN_CLAIM_MARGIN_SECONDS ? "claim" : "claim_window_closed";
|
|
2663
3072
|
}
|
|
2664
3073
|
}
|
|
3074
|
+
var RfqSwapOriginRequired = class extends Error {
|
|
3075
|
+
/** The swap that could not be admitted. */
|
|
3076
|
+
rfqId;
|
|
3077
|
+
constructor(rfqId) {
|
|
3078
|
+
super(
|
|
3079
|
+
`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`
|
|
3080
|
+
);
|
|
3081
|
+
this.name = "RfqSwapOriginRequired";
|
|
3082
|
+
this.rfqId = rfqId;
|
|
3083
|
+
}
|
|
3084
|
+
};
|
|
2665
3085
|
var notify = (listeners, call) => {
|
|
2666
3086
|
for (const listener of listeners) {
|
|
2667
3087
|
try {
|
|
@@ -2739,7 +3159,20 @@ var RfqSwapManager = class {
|
|
|
2739
3159
|
* answers instead of throwing "not found". Cleared by {@link removeSwap}. */
|
|
2740
3160
|
finished = /* @__PURE__ */ new Map();
|
|
2741
3161
|
waiters = /* @__PURE__ */ new Map();
|
|
2742
|
-
/**
|
|
3162
|
+
/**
|
|
3163
|
+
* The request-time origin of each swap the manager may have to CREATE a
|
|
3164
|
+
* record for, by rfqId.
|
|
3165
|
+
*
|
|
3166
|
+
* Needed only for a swap the store has never seen: once a record exists,
|
|
3167
|
+
* `updateRfqSwapRecord` carries the origin half through and the map is
|
|
3168
|
+
* redundant. It is kept anyway for the swap's whole life, so a record the
|
|
3169
|
+
* store loses between passes is rewritten rather than lost — and dropped
|
|
3170
|
+
* by {@link removeSwap} and by retention, which are the two places a swap
|
|
3171
|
+
* stops being this manager's business.
|
|
3172
|
+
*/
|
|
3173
|
+
origins = /* @__PURE__ */ new Map();
|
|
3174
|
+
/** Records changed during the current pass, flushed to the repository and
|
|
3175
|
+
* through `saveSwap`. */
|
|
2743
3176
|
dirty = /* @__PURE__ */ new Set();
|
|
2744
3177
|
/** Race guard: one action at a time per swap. */
|
|
2745
3178
|
inProgress = /* @__PURE__ */ new Set();
|
|
@@ -2761,7 +3194,9 @@ var RfqSwapManager = class {
|
|
|
2761
3194
|
this.actionExecutedListeners.add(config.events.onActionExecuted);
|
|
2762
3195
|
}
|
|
2763
3196
|
}
|
|
2764
|
-
/** Wire the money-moving half. Without it the manager only watches.
|
|
3197
|
+
/** Wire the money-moving half. Without it the manager only watches. The
|
|
3198
|
+
* two claims may be omitted for a consumer that drives no kind reaching
|
|
3199
|
+
* them — see {@link AvailableRfqSwapManagerCallbacks}. */
|
|
2765
3200
|
setCallbacks(callbacks) {
|
|
2766
3201
|
this.callbacks = callbacks;
|
|
2767
3202
|
}
|
|
@@ -2781,6 +3216,116 @@ var RfqSwapManager = class {
|
|
|
2781
3216
|
this.actionExecutedListeners.add(listener);
|
|
2782
3217
|
return () => this.actionExecutedListeners.delete(listener);
|
|
2783
3218
|
}
|
|
3219
|
+
/**
|
|
3220
|
+
* Rebuild every stored swap and take over monitoring them.
|
|
3221
|
+
*
|
|
3222
|
+
* The composition a consumer otherwise writes by hand, and the one place
|
|
3223
|
+
* all four pieces meet: retention decides what to keep
|
|
3224
|
+
* (`shouldRetainRfqSwap`), the lockup's contract row supplies the covenant
|
|
3225
|
+
* (`lockupContractParams`), `rebuildRfqSwap` turns a record back into a
|
|
3226
|
+
* live swap, and each rebuilt swap arrives with its own origin, so nothing
|
|
3227
|
+
* is asked of the caller.
|
|
3228
|
+
*
|
|
3229
|
+
* **Deliberately not part of {@link start}.** A consumer that wants to
|
|
3230
|
+
* look at its records — count them, show them, prune and stop — is not
|
|
3231
|
+
* forced to start driving money to do it. Call this first and `start()`
|
|
3232
|
+
* after; a manager already running polls the restored swaps at once.
|
|
3233
|
+
*
|
|
3234
|
+
* Retention runs BEFORE the rebuild, so a record past
|
|
3235
|
+
* `RFQ_SWAP_RETENTION_SECONDS` costs no contract lookup on its way to being
|
|
3236
|
+
* dropped.
|
|
3237
|
+
*
|
|
3238
|
+
* **A record that cannot be rebuilt is reported, never swallowed and never
|
|
3239
|
+
* fatal.** `rebuildRfqSwap` throws by design when the covenant params do
|
|
3240
|
+
* not derive the funded address, and `lockupContractParams` throws
|
|
3241
|
+
* `LockupContractMissing` when the wallet has no row for the lockup — both
|
|
3242
|
+
* say something true about that one record, and neither is a reason to
|
|
3243
|
+
* strand the others. They come back in {@link RfqRestoreResult.failed}.
|
|
3244
|
+
*/
|
|
3245
|
+
async restoreFromRepository(options = {}) {
|
|
3246
|
+
const repository = this.requireRepository("restoreFromRepository");
|
|
3247
|
+
const params = options.params ?? this.paramsFromContracts();
|
|
3248
|
+
const records = await repository.getAllRfqSwaps();
|
|
3249
|
+
const pruned = await this.dropRetired(repository, records);
|
|
3250
|
+
const retired = new Set(pruned);
|
|
3251
|
+
const restored = [];
|
|
3252
|
+
const failed = [];
|
|
3253
|
+
for (const record of records) {
|
|
3254
|
+
if (retired.has(record.rfqId)) continue;
|
|
3255
|
+
let swap;
|
|
3256
|
+
try {
|
|
3257
|
+
swap = rebuildRfqSwap(record, await params(record));
|
|
3258
|
+
} catch (error) {
|
|
3259
|
+
failed.push({
|
|
3260
|
+
rfqId: record.rfqId,
|
|
3261
|
+
error: error instanceof Error ? error : new Error(errorMessage(error))
|
|
3262
|
+
});
|
|
3263
|
+
continue;
|
|
3264
|
+
}
|
|
3265
|
+
this.origins.set(record.rfqId, rfqSwapOriginOf(record));
|
|
3266
|
+
if (isRfqSwapTerminal(swap.state)) this.finished.set(swap.rfqId, swap);
|
|
3267
|
+
else this.track(swap);
|
|
3268
|
+
restored.push(swap);
|
|
3269
|
+
}
|
|
3270
|
+
if (this.running) {
|
|
3271
|
+
await Promise.allSettled(
|
|
3272
|
+
restored.filter((swap) => this.monitored.has(swap.rfqId)).map((swap) => this.pollSwap(swap))
|
|
3273
|
+
);
|
|
3274
|
+
}
|
|
3275
|
+
return { restored, failed, pruned };
|
|
3276
|
+
}
|
|
3277
|
+
/**
|
|
3278
|
+
* Drop stored records that are terminal and past
|
|
3279
|
+
* `RFQ_SWAP_RETENTION_SECONDS`, and return their ids.
|
|
3280
|
+
*
|
|
3281
|
+
* `needs_counterparty` is never dropped, however old: the money is still at
|
|
3282
|
+
* the lockup and the counterparty's move still ends the swap. That rule
|
|
3283
|
+
* lives in `shouldRetainRfqSwap`, which this defers to rather than
|
|
3284
|
+
* restating.
|
|
3285
|
+
*
|
|
3286
|
+
* Public because retention is a caller's cadence, not the manager's: a
|
|
3287
|
+
* long-lived process wants it on a timer of its own, a mobile app wants it
|
|
3288
|
+
* at boot. {@link restoreFromRepository} runs it first, so the boot path
|
|
3289
|
+
* needs no separate call.
|
|
3290
|
+
*/
|
|
3291
|
+
async pruneRetiredSwaps() {
|
|
3292
|
+
const repository = this.deps.repository;
|
|
3293
|
+
if (!repository) return [];
|
|
3294
|
+
return this.dropRetired(repository, await repository.getAllRfqSwaps());
|
|
3295
|
+
}
|
|
3296
|
+
async dropRetired(repository, records) {
|
|
3297
|
+
const now = this.config.now();
|
|
3298
|
+
const dropped = [];
|
|
3299
|
+
for (const record of records) {
|
|
3300
|
+
if (shouldRetainRfqSwap(record, now)) continue;
|
|
3301
|
+
await repository.removeRfqSwap(record.rfqId);
|
|
3302
|
+
this.finished.delete(record.rfqId);
|
|
3303
|
+
if (!this.monitored.has(record.rfqId)) this.origins.delete(record.rfqId);
|
|
3304
|
+
dropped.push(record.rfqId);
|
|
3305
|
+
}
|
|
3306
|
+
return dropped;
|
|
3307
|
+
}
|
|
3308
|
+
requireRepository(method) {
|
|
3309
|
+
const repository = this.deps.repository;
|
|
3310
|
+
if (!repository) {
|
|
3311
|
+
throw new Error(
|
|
3312
|
+
`${method} needs a record store; pass one as RfqSwapManagerDeps.repository`
|
|
3313
|
+
);
|
|
3314
|
+
}
|
|
3315
|
+
return repository;
|
|
3316
|
+
}
|
|
3317
|
+
/** The default covenant source: the wallet's own contract row for each
|
|
3318
|
+
* lockup, which is where registration put it before the address could be
|
|
3319
|
+
* funded. */
|
|
3320
|
+
paramsFromContracts() {
|
|
3321
|
+
const contracts = this.deps.contracts;
|
|
3322
|
+
if (!contracts) {
|
|
3323
|
+
throw new Error(
|
|
3324
|
+
"restoreFromRepository needs the covenant parameters: wire RfqSwapManagerDeps.contracts so each lockup's contract row can be read, or pass options.params"
|
|
3325
|
+
);
|
|
3326
|
+
}
|
|
3327
|
+
return (record) => lockupContractParams(contracts, record.lockupAddress);
|
|
3328
|
+
}
|
|
2784
3329
|
/**
|
|
2785
3330
|
* Load records and begin monitoring. Runs one pass immediately — a caller
|
|
2786
3331
|
* resuming after a restart may be well past a deadline already — then
|
|
@@ -2790,8 +3335,17 @@ var RfqSwapManager = class {
|
|
|
2790
3335
|
* Calling it again while running loads the records and returns rather than
|
|
2791
3336
|
* re-arming — dropping them silently would strand a funded swap on a
|
|
2792
3337
|
* caller's harmless double-start.
|
|
3338
|
+
*
|
|
3339
|
+
* The signature is unchanged with a {@link RfqSwapManagerDeps.repository}
|
|
3340
|
+
* wired; the origins are resolved from the store instead, by the same rule
|
|
3341
|
+
* {@link addSwap} applies. A swap the store has never seen throws
|
|
3342
|
+
* {@link RfqSwapOriginRequired} — hand that one to `addSwap` with its
|
|
3343
|
+
* origin, or restore the whole set with {@link restoreFromRepository},
|
|
3344
|
+
* which needs no caller input at all. Every swap is checked before any is
|
|
3345
|
+
* tracked, so a bad one in the list does not leave a half-loaded manager.
|
|
2793
3346
|
*/
|
|
2794
3347
|
async start(swaps = []) {
|
|
3348
|
+
for (const swap of swaps) await this.admit(swap);
|
|
2795
3349
|
for (const swap of swaps) {
|
|
2796
3350
|
if (isRfqSwapTerminal(swap.state)) this.finished.set(swap.rfqId, swap);
|
|
2797
3351
|
else this.track(swap);
|
|
@@ -2823,9 +3377,22 @@ var RfqSwapManager = class {
|
|
|
2823
3377
|
this.unsubscribeContracts?.();
|
|
2824
3378
|
this.unsubscribeContracts = null;
|
|
2825
3379
|
}
|
|
2826
|
-
/**
|
|
2827
|
-
*
|
|
2828
|
-
|
|
3380
|
+
/**
|
|
3381
|
+
* Begin monitoring a swap. Polled immediately when the manager is running,
|
|
3382
|
+
* so a just-funded swap does not wait out a whole interval.
|
|
3383
|
+
*
|
|
3384
|
+
* `origin` is the request-time half a live swap cannot carry — the corridor,
|
|
3385
|
+
* the funded address, the corridor's profile, the funding txid — and it is
|
|
3386
|
+
* what lets the manager write this swap's FIRST record. Supply it whenever
|
|
3387
|
+
* a {@link RfqSwapManagerDeps.repository} is wired and the swap is new. It
|
|
3388
|
+
* may be omitted for a swap the store already holds a record for, which is
|
|
3389
|
+
* then read to confirm it; omitting it for one the store has never seen
|
|
3390
|
+
* throws {@link RfqSwapOriginRequired} rather than admitting a swap whose
|
|
3391
|
+
* record could never be written. With no repository wired the parameter is
|
|
3392
|
+
* inert.
|
|
3393
|
+
*/
|
|
3394
|
+
async addSwap(swap, origin) {
|
|
3395
|
+
await this.admit(swap, origin);
|
|
2829
3396
|
if (isRfqSwapTerminal(swap.state)) {
|
|
2830
3397
|
this.finished.set(swap.rfqId, swap);
|
|
2831
3398
|
return;
|
|
@@ -2833,6 +3400,29 @@ var RfqSwapManager = class {
|
|
|
2833
3400
|
this.track(swap);
|
|
2834
3401
|
if (this.running) await this.pollSwap(swap);
|
|
2835
3402
|
}
|
|
3403
|
+
/**
|
|
3404
|
+
* Settle where this swap's record will come from, before it is monitored.
|
|
3405
|
+
*
|
|
3406
|
+
* Three ways it can be answered, in order: the caller passed an origin, one
|
|
3407
|
+
* is already remembered from an earlier `addSwap`, or the store holds a
|
|
3408
|
+
* record — which is the origin, already written. Only the last costs a read,
|
|
3409
|
+
* and only when the first two are absent.
|
|
3410
|
+
*/
|
|
3411
|
+
async admit(swap, origin) {
|
|
3412
|
+
if (origin) {
|
|
3413
|
+
assertSameSwap(origin, swap);
|
|
3414
|
+
this.origins.set(swap.rfqId, origin);
|
|
3415
|
+
if (this.deps.repository && !isRfqSwapTerminal(swap.state)) {
|
|
3416
|
+
this.dirty.add(swap.rfqId);
|
|
3417
|
+
}
|
|
3418
|
+
return;
|
|
3419
|
+
}
|
|
3420
|
+
const repository = this.deps.repository;
|
|
3421
|
+
if (!repository || this.origins.has(swap.rfqId)) return;
|
|
3422
|
+
const stored = await repository.getRfqSwap(swap.rfqId);
|
|
3423
|
+
if (!stored) throw new RfqSwapOriginRequired(swap.rfqId);
|
|
3424
|
+
this.origins.set(swap.rfqId, rfqSwapOriginOf(stored));
|
|
3425
|
+
}
|
|
2836
3426
|
/** Forget a swap entirely, monitored or finished.
|
|
2837
3427
|
*
|
|
2838
3428
|
* Its contract row is left alone: registration is a wallet-level fact about
|
|
@@ -2850,6 +3440,7 @@ var RfqSwapManager = class {
|
|
|
2850
3440
|
}
|
|
2851
3441
|
this.waiters.delete(rfqId);
|
|
2852
3442
|
this.dirty.delete(rfqId);
|
|
3443
|
+
this.origins.delete(rfqId);
|
|
2853
3444
|
}
|
|
2854
3445
|
/** Every swap still being monitored. */
|
|
2855
3446
|
async getPendingSwaps() {
|
|
@@ -2912,7 +3503,7 @@ var RfqSwapManager = class {
|
|
|
2912
3503
|
// ── internals ────────────────────────────────────────────────────────────
|
|
2913
3504
|
track(swap) {
|
|
2914
3505
|
this.monitored.set(swap.rfqId, swap);
|
|
2915
|
-
this.byLockupScript.set(
|
|
3506
|
+
this.byLockupScript.set(import_base13.hex.encode(swap.lockupPkScript), swap);
|
|
2916
3507
|
}
|
|
2917
3508
|
/** Drops the swap from BOTH indexes. The event index is the one that stops
|
|
2918
3509
|
* a late event finding a swap that is gone; `pollSwap`'s own
|
|
@@ -2921,7 +3512,7 @@ var RfqSwapManager = class {
|
|
|
2921
3512
|
* them from silently re-driving a cancelled swap. */
|
|
2922
3513
|
untrack(rfqId) {
|
|
2923
3514
|
const swap = this.monitored.get(rfqId);
|
|
2924
|
-
if (swap) this.byLockupScript.delete(
|
|
3515
|
+
if (swap) this.byLockupScript.delete(import_base13.hex.encode(swap.lockupPkScript));
|
|
2925
3516
|
this.monitored.delete(rfqId);
|
|
2926
3517
|
this.refundRefused.delete(rfqId);
|
|
2927
3518
|
this.lastClaimError.delete(rfqId);
|
|
@@ -2983,7 +3574,7 @@ var RfqSwapManager = class {
|
|
|
2983
3574
|
if (!lockup) {
|
|
2984
3575
|
try {
|
|
2985
3576
|
const [existing] = await contracts.getContracts({
|
|
2986
|
-
script:
|
|
3577
|
+
script: import_base13.hex.encode(swap.lockupPkScript)
|
|
2987
3578
|
});
|
|
2988
3579
|
if (existing) {
|
|
2989
3580
|
this.registered.set(swap.rfqId, true);
|
|
@@ -3002,13 +3593,13 @@ var RfqSwapManager = class {
|
|
|
3002
3593
|
);
|
|
3003
3594
|
return;
|
|
3004
3595
|
}
|
|
3005
|
-
const script =
|
|
3006
|
-
if (script !==
|
|
3596
|
+
const script = import_base13.hex.encode(lockup.script.pkScript);
|
|
3597
|
+
if (script !== import_base13.hex.encode(swap.lockupPkScript)) {
|
|
3007
3598
|
this.registered.set(swap.rfqId, false);
|
|
3008
3599
|
this.emitFailed(
|
|
3009
3600
|
swap,
|
|
3010
3601
|
new Error(
|
|
3011
|
-
`swap ${swap.rfqId} lockup script ${script} does not match its lockupPkScript ${
|
|
3602
|
+
`swap ${swap.rfqId} lockup script ${script} does not match its lockupPkScript ${import_base13.hex.encode(swap.lockupPkScript)}`
|
|
3012
3603
|
)
|
|
3013
3604
|
);
|
|
3014
3605
|
return;
|
|
@@ -3027,7 +3618,7 @@ var RfqSwapManager = class {
|
|
|
3027
3618
|
* script for its whole life. Best-effort — the swap is over either way. */
|
|
3028
3619
|
retireContract(swap) {
|
|
3029
3620
|
if (!this.deps.contracts || !this.registered.get(swap.rfqId)) return;
|
|
3030
|
-
void this.deps.contracts.setContractWatchState(
|
|
3621
|
+
void this.deps.contracts.setContractWatchState(import_base13.hex.encode(swap.lockupPkScript), "retained").catch((error) => this.emitFailed(swap, error));
|
|
3031
3622
|
}
|
|
3032
3623
|
arm() {
|
|
3033
3624
|
if (!this.running) return;
|
|
@@ -3065,6 +3656,7 @@ var RfqSwapManager = class {
|
|
|
3065
3656
|
fate = { fate: "unknown" };
|
|
3066
3657
|
}
|
|
3067
3658
|
if (fate.fate === "claimed" || fate.fate === "returned") {
|
|
3659
|
+
this.stampLockupSpends(swap, fate.spends);
|
|
3068
3660
|
this.setState(swap, fate.fate === "claimed" ? "settled" : "refunded");
|
|
3069
3661
|
return;
|
|
3070
3662
|
}
|
|
@@ -3149,10 +3741,10 @@ var RfqSwapManager = class {
|
|
|
3149
3741
|
);
|
|
3150
3742
|
}
|
|
3151
3743
|
}
|
|
3152
|
-
if (!this.callbacks) {
|
|
3744
|
+
if (!this.callbacks || !this.callbacks.claimLockup) {
|
|
3153
3745
|
return this.block(
|
|
3154
3746
|
swap,
|
|
3155
|
-
"no callbacks are wired, so this wallet cannot claim the lockup"
|
|
3747
|
+
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
3748
|
);
|
|
3157
3749
|
}
|
|
3158
3750
|
this.setState(swap, "claimable");
|
|
@@ -3202,8 +3794,15 @@ var RfqSwapManager = class {
|
|
|
3202
3794
|
});
|
|
3203
3795
|
if (action === "claim" && phase.phase === "claimable") {
|
|
3204
3796
|
if (swap.claimTxid) return "continue";
|
|
3797
|
+
if (this.callbacks && !this.callbacks.claimOnchain) {
|
|
3798
|
+
this.block(
|
|
3799
|
+
swap,
|
|
3800
|
+
"no claimOnchain callback is wired, so this wallet cannot claim the L1 fill"
|
|
3801
|
+
);
|
|
3802
|
+
return "handled";
|
|
3803
|
+
}
|
|
3205
3804
|
this.setOnchainState(swap, "claimable");
|
|
3206
|
-
if (!this.config.enableAutoActions || !this.callbacks) return "handled";
|
|
3805
|
+
if (!this.config.enableAutoActions || !this.callbacks?.claimOnchain) return "handled";
|
|
3207
3806
|
try {
|
|
3208
3807
|
const { txid } = await this.callbacks.claimOnchain(swap, phase.utxo);
|
|
3209
3808
|
swap.claimTxid = txid;
|
|
@@ -3316,6 +3915,24 @@ var RfqSwapManager = class {
|
|
|
3316
3915
|
if (swap.state !== "needs_counterparty") return;
|
|
3317
3916
|
this.setState(swap, traderClaimTxid(swap) ? "claimed" : "pending");
|
|
3318
3917
|
}
|
|
3918
|
+
/**
|
|
3919
|
+
* Record which ark transactions ended the lockup.
|
|
3920
|
+
*
|
|
3921
|
+
* Only the ones the indexer actually named: `LockupSpend.arkTxid` is
|
|
3922
|
+
* optional, and a checkpoint txid is not what history correlates on — a
|
|
3923
|
+
* record carrying one would name a transaction the wallet's own activity
|
|
3924
|
+
* never shows. Fewer txids is the right failure here.
|
|
3925
|
+
*
|
|
3926
|
+
* Assigned rather than merged: the fate is one read of the whole lockup,
|
|
3927
|
+
* so it is the complete answer for this swap, and a swap only reaches a
|
|
3928
|
+
* verdict once.
|
|
3929
|
+
*/
|
|
3930
|
+
stampLockupSpends(swap, spends) {
|
|
3931
|
+
const arkTxids = spends.map((spend) => spend.arkTxid).filter((txid) => txid !== void 0);
|
|
3932
|
+
if (arkTxids.length === 0) return;
|
|
3933
|
+
swap.lockupSpendArkTxids = arkTxids;
|
|
3934
|
+
this.touch(swap);
|
|
3935
|
+
}
|
|
3319
3936
|
touch(swap) {
|
|
3320
3937
|
swap.updatedAt = this.config.now();
|
|
3321
3938
|
this.dirty.add(swap.rfqId);
|
|
@@ -3341,9 +3958,29 @@ var RfqSwapManager = class {
|
|
|
3341
3958
|
emitAction(swap, action) {
|
|
3342
3959
|
notify(this.actionExecutedListeners, (listener) => listener(swap, action));
|
|
3343
3960
|
}
|
|
3344
|
-
/**
|
|
3961
|
+
/**
|
|
3962
|
+
* Flush a changed record to every sink that is wired, and say whether all
|
|
3963
|
+
* of them took it.
|
|
3964
|
+
*
|
|
3965
|
+
* Up to two writes, in this order: the canonical `RfqSwapRecord` to
|
|
3966
|
+
* {@link RfqSwapManagerDeps.repository}, then
|
|
3967
|
+
* {@link RfqSwapManagerCallbacks.saveSwap}. Both gate — a rejection from
|
|
3968
|
+
* either leaves the record dirty and monitored, so waiters stay unsettled
|
|
3969
|
+
* and a terminal swap is not finalized until the write it claims lands.
|
|
3970
|
+
* That is exactly today's rule for `saveSwap`, applied to whichever sinks
|
|
3971
|
+
* exist; wiring the repository does not weaken it.
|
|
3972
|
+
*
|
|
3973
|
+
* The canonical write goes FIRST and a failure there skips the second.
|
|
3974
|
+
* `saveSwap` is a projection of the record, and projecting a state the
|
|
3975
|
+
* record of record has just refused would leave the secondary sink ahead
|
|
3976
|
+
* of the primary — the one ordering that survives no restart.
|
|
3977
|
+
*
|
|
3978
|
+
* With neither wired the state is process-local, which is what a manager
|
|
3979
|
+
* with no callbacks has always done.
|
|
3980
|
+
*/
|
|
3345
3981
|
async save(swap) {
|
|
3346
|
-
if (!this.
|
|
3982
|
+
if (!await this.saveRecord(swap)) return false;
|
|
3983
|
+
if (!this.callbacks?.saveSwap) return true;
|
|
3347
3984
|
try {
|
|
3348
3985
|
await this.callbacks.saveSwap(swap);
|
|
3349
3986
|
return true;
|
|
@@ -3352,6 +3989,25 @@ var RfqSwapManager = class {
|
|
|
3352
3989
|
return false;
|
|
3353
3990
|
}
|
|
3354
3991
|
}
|
|
3992
|
+
/** The canonical write. True when there is no repository to write to. */
|
|
3993
|
+
async saveRecord(swap) {
|
|
3994
|
+
const repository = this.deps.repository;
|
|
3995
|
+
if (!repository) return true;
|
|
3996
|
+
try {
|
|
3997
|
+
const stored = await repository.getRfqSwap(swap.rfqId);
|
|
3998
|
+
const record = stored ? updateRfqSwapRecord(stored, swap) : createRfqSwapRecord(this.originOrThrow(swap), swap);
|
|
3999
|
+
await repository.saveRfqSwap(record);
|
|
4000
|
+
return true;
|
|
4001
|
+
} catch (error) {
|
|
4002
|
+
this.emitFailed(swap, error);
|
|
4003
|
+
return false;
|
|
4004
|
+
}
|
|
4005
|
+
}
|
|
4006
|
+
originOrThrow(swap) {
|
|
4007
|
+
const origin = this.origins.get(swap.rfqId);
|
|
4008
|
+
if (!origin) throw new RfqSwapOriginRequired(swap.rfqId);
|
|
4009
|
+
return origin;
|
|
4010
|
+
}
|
|
3355
4011
|
/**
|
|
3356
4012
|
* Drop a terminal swap from monitoring and report it exactly once.
|
|
3357
4013
|
*
|
|
@@ -3411,6 +4067,8 @@ var errorMessage = (error) => error instanceof Error ? error.message : String(er
|
|
|
3411
4067
|
var outpointKey = (vtxo) => `${vtxo.txid}:${vtxo.vout}`;
|
|
3412
4068
|
|
|
3413
4069
|
// src/activity.ts
|
|
4070
|
+
var import_sdk14 = require("@arkade-os/sdk");
|
|
4071
|
+
var import_base14 = require("@scure/base");
|
|
3414
4072
|
var LABELS = {
|
|
3415
4073
|
lightning_send: "Lightning send",
|
|
3416
4074
|
lightning_receive: "Lightning receive",
|
|
@@ -3463,6 +4121,44 @@ function swapActivityResolver(deps) {
|
|
|
3463
4121
|
}
|
|
3464
4122
|
};
|
|
3465
4123
|
}
|
|
4124
|
+
async function rfqSwapActivityInputs(deps) {
|
|
4125
|
+
const records = await deps.repository.getAllRfqSwaps();
|
|
4126
|
+
return Promise.all(records.map((record) => activityInputOf(record, deps.indexer)));
|
|
4127
|
+
}
|
|
4128
|
+
async function activityInputOf(record, indexer) {
|
|
4129
|
+
const txids = /* @__PURE__ */ new Set();
|
|
4130
|
+
if (record.fundingArkTxid) txids.add(record.fundingArkTxid);
|
|
4131
|
+
if (record.refundArkTxid) txids.add(record.refundArkTxid);
|
|
4132
|
+
const handler = rfqCorridorHandlers.getOrThrow(record.kind);
|
|
4133
|
+
for (const txid of handler.activityTxids?.(record.profile) ?? []) txids.add(txid);
|
|
4134
|
+
for (const txid of record.lockupSpendArkTxids ?? []) txids.add(txid);
|
|
4135
|
+
const spendUnknown = isRfqSwapTerminal(record.state) && !record.refundArkTxid && !record.lockupSpendArkTxids?.length;
|
|
4136
|
+
if (indexer && (!record.fundingArkTxid || spendUnknown)) {
|
|
4137
|
+
for (const txid of await lockupTxids(indexer, record, !record.fundingArkTxid)) {
|
|
4138
|
+
txids.add(txid);
|
|
4139
|
+
}
|
|
4140
|
+
}
|
|
4141
|
+
return { rfqId: record.rfqId, kind: record.kind, state: record.state, txids: [...txids] };
|
|
4142
|
+
}
|
|
4143
|
+
async function lockupTxids(indexer, record, wantFunding) {
|
|
4144
|
+
let script;
|
|
4145
|
+
try {
|
|
4146
|
+
script = import_base14.hex.encode(import_sdk14.ArkAddress.decode(record.lockupAddress).pkScript);
|
|
4147
|
+
} catch {
|
|
4148
|
+
return [];
|
|
4149
|
+
}
|
|
4150
|
+
try {
|
|
4151
|
+
const { vtxos } = await indexer.getVtxos({ scripts: [script] });
|
|
4152
|
+
const out = [];
|
|
4153
|
+
for (const vtxo of vtxos ?? []) {
|
|
4154
|
+
if (wantFunding) out.push(vtxo.txid);
|
|
4155
|
+
if (vtxo.arkTxId) out.push(vtxo.arkTxId);
|
|
4156
|
+
}
|
|
4157
|
+
return out;
|
|
4158
|
+
} catch {
|
|
4159
|
+
return [];
|
|
4160
|
+
}
|
|
4161
|
+
}
|
|
3466
4162
|
// Annotate the CommonJS export names for ESM import in node:
|
|
3467
4163
|
0 && (module.exports = {
|
|
3468
4164
|
ARKADE_ASSET,
|
|
@@ -3474,7 +4170,9 @@ function swapActivityResolver(deps) {
|
|
|
3474
4170
|
LIGHTNING_BTC,
|
|
3475
4171
|
LIGHTNING_RECEIVE_PAIR,
|
|
3476
4172
|
LIGHTNING_SEND_PAIR,
|
|
4173
|
+
LOCKTIME_THRESHOLD,
|
|
3477
4174
|
LockupAmountMismatchError,
|
|
4175
|
+
LockupContractMissing,
|
|
3478
4176
|
LockupNeedsRecoveryError,
|
|
3479
4177
|
LockupRegistrationFailed,
|
|
3480
4178
|
MAX_MIN_CONFIRMATIONS,
|
|
@@ -3492,10 +4190,12 @@ function swapActivityResolver(deps) {
|
|
|
3492
4190
|
QUOTE_OPTIONS,
|
|
3493
4191
|
REFUND_MTP_LAG_SECONDS,
|
|
3494
4192
|
RFQ_RESOLVED_STATES,
|
|
4193
|
+
RFQ_SWAP_RETENTION_SECONDS,
|
|
3495
4194
|
RFQ_SWAP_TERMINAL_STATES,
|
|
3496
4195
|
RFQ_TERMINAL_STATES,
|
|
3497
4196
|
RefundNotLocallyPossibleError,
|
|
3498
4197
|
RfqSwapManager,
|
|
4198
|
+
RfqSwapOriginRequired,
|
|
3499
4199
|
SOLO_REFUND_HEADROOM_SECONDS,
|
|
3500
4200
|
SWAP_LOCKUP_CONTRACT_KIND,
|
|
3501
4201
|
SWAP_LOCKUP_CONTRACT_LABEL,
|
|
@@ -3503,6 +4203,7 @@ function swapActivityResolver(deps) {
|
|
|
3503
4203
|
SwapRefusal,
|
|
3504
4204
|
addAssetSwap,
|
|
3505
4205
|
arkadeAssetLeg,
|
|
4206
|
+
arkadeRefunder,
|
|
3506
4207
|
arkadeSwapRequest,
|
|
3507
4208
|
assertFundable,
|
|
3508
4209
|
assertReceivable,
|
|
@@ -3518,6 +4219,7 @@ function swapActivityResolver(deps) {
|
|
|
3518
4219
|
classifyOnchainHtlc,
|
|
3519
4220
|
classifySpend,
|
|
3520
4221
|
createOffer,
|
|
4222
|
+
createRfqSwapRecord,
|
|
3521
4223
|
decodeOffer,
|
|
3522
4224
|
deriveLightningReceive,
|
|
3523
4225
|
deriveOnchainReceive,
|
|
@@ -3535,6 +4237,7 @@ function swapActivityResolver(deps) {
|
|
|
3535
4237
|
lightningReceiveRequest,
|
|
3536
4238
|
lightningSendRequest,
|
|
3537
4239
|
lightningSendVtxoScript,
|
|
4240
|
+
lockupContractParams,
|
|
3538
4241
|
makeCachedFeedFetch,
|
|
3539
4242
|
newPreimage,
|
|
3540
4243
|
newRfqId,
|
|
@@ -3543,12 +4246,14 @@ function swapActivityResolver(deps) {
|
|
|
3543
4246
|
offerVtxoScript,
|
|
3544
4247
|
onchainHtlcScript,
|
|
3545
4248
|
onchainReceiveRequest,
|
|
4249
|
+
onchainSendProfile,
|
|
3546
4250
|
onchainSendRequest,
|
|
3547
4251
|
paymentHashOf,
|
|
3548
4252
|
preimageForSwapRecord,
|
|
3549
4253
|
pushClaim,
|
|
3550
4254
|
pushRefundWithoutReceiver,
|
|
3551
4255
|
readLockupFate,
|
|
4256
|
+
rebuildRfqSwap,
|
|
3552
4257
|
receiveVtxoScript,
|
|
3553
4258
|
refundIfUnresolved,
|
|
3554
4259
|
registerLockupContract,
|
|
@@ -3559,9 +4264,15 @@ function swapActivityResolver(deps) {
|
|
|
3559
4264
|
requestOnchainSend,
|
|
3560
4265
|
restoreAssetSwaps,
|
|
3561
4266
|
retireSettledOfferContracts,
|
|
4267
|
+
rfqClaimSecretOf,
|
|
3562
4268
|
rfqPair,
|
|
4269
|
+
rfqSecretsProfile,
|
|
4270
|
+
rfqSignerOf,
|
|
4271
|
+
rfqSwapActivityInputs,
|
|
4272
|
+
rfqSwapOriginOf,
|
|
3563
4273
|
sealClaimPacket,
|
|
3564
4274
|
senderIdentityForSwapRecord,
|
|
4275
|
+
shouldRetainRfqSwap,
|
|
3565
4276
|
spendTxidsOf,
|
|
3566
4277
|
spendUpdate,
|
|
3567
4278
|
swapActivityResolver,
|
|
@@ -3572,6 +4283,7 @@ function swapActivityResolver(deps) {
|
|
|
3572
4283
|
unilateralRefundWithoutReceiverDelay,
|
|
3573
4284
|
updateAssetSwap,
|
|
3574
4285
|
updateAssetSwapBestEffort,
|
|
4286
|
+
updateRfqSwapRecord,
|
|
3575
4287
|
validatePlan,
|
|
3576
4288
|
verifyLockupAddress,
|
|
3577
4289
|
verifyReceiveInvoice,
|