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