@continuumdao/ctm-mpc-defi 0.1.3
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/LICENSE +18 -0
- package/README.md +111 -0
- package/dist/agent/catalog.cjs +484 -0
- package/dist/agent/catalog.cjs.map +1 -0
- package/dist/agent/catalog.d.cts +117 -0
- package/dist/agent/catalog.d.ts +117 -0
- package/dist/agent/catalog.js +474 -0
- package/dist/agent/catalog.js.map +1 -0
- package/dist/chains/evm/index.cjs +474 -0
- package/dist/chains/evm/index.cjs.map +1 -0
- package/dist/chains/evm/index.d.cts +62 -0
- package/dist/chains/evm/index.d.ts +62 -0
- package/dist/chains/evm/index.js +459 -0
- package/dist/chains/evm/index.js.map +1 -0
- package/dist/chains/near/index.cjs +25 -0
- package/dist/chains/near/index.cjs.map +1 -0
- package/dist/chains/near/index.d.cts +37 -0
- package/dist/chains/near/index.d.ts +37 -0
- package/dist/chains/near/index.js +20 -0
- package/dist/chains/near/index.js.map +1 -0
- package/dist/chains/solana/index.cjs +25 -0
- package/dist/chains/solana/index.cjs.map +1 -0
- package/dist/chains/solana/index.d.cts +40 -0
- package/dist/chains/solana/index.d.ts +40 -0
- package/dist/chains/solana/index.js +20 -0
- package/dist/chains/solana/index.js.map +1 -0
- package/dist/core/index.cjs +128 -0
- package/dist/core/index.cjs.map +1 -0
- package/dist/core/index.d.cts +10 -0
- package/dist/core/index.d.ts +10 -0
- package/dist/core/index.js +116 -0
- package/dist/core/index.js.map +1 -0
- package/dist/envelope-CcE5Cz_q.d.ts +35 -0
- package/dist/envelope-DYDPnrHZ.d.cts +35 -0
- package/dist/index.cjs +2481 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +15 -0
- package/dist/index.d.ts +15 -0
- package/dist/index.js +2446 -0
- package/dist/index.js.map +1 -0
- package/dist/keygen-CfNp8yKJ.d.cts +9 -0
- package/dist/keygen-DsINazx8.d.ts +9 -0
- package/dist/nodeRead-BnmSaMGO.d.cts +8 -0
- package/dist/nodeRead-BnmSaMGO.d.ts +8 -0
- package/dist/protocols/evm/curve-dao/index.cjs +869 -0
- package/dist/protocols/evm/curve-dao/index.cjs.map +1 -0
- package/dist/protocols/evm/curve-dao/index.d.cts +147 -0
- package/dist/protocols/evm/curve-dao/index.d.ts +147 -0
- package/dist/protocols/evm/curve-dao/index.js +846 -0
- package/dist/protocols/evm/curve-dao/index.js.map +1 -0
- package/dist/protocols/evm/uniswap-v4/index.cjs +1700 -0
- package/dist/protocols/evm/uniswap-v4/index.cjs.map +1 -0
- package/dist/protocols/evm/uniswap-v4/index.d.cts +323 -0
- package/dist/protocols/evm/uniswap-v4/index.d.ts +323 -0
- package/dist/protocols/evm/uniswap-v4/index.js +1659 -0
- package/dist/protocols/evm/uniswap-v4/index.js.map +1 -0
- package/dist/registry-BwZoE668.d.cts +8 -0
- package/dist/registry-oMKlO_5z.d.ts +8 -0
- package/dist/txParams-BC7ogvdR.d.cts +19 -0
- package/dist/txParams-BC7ogvdR.d.ts +19 -0
- package/dist/types-5u863Fd9.d.ts +34 -0
- package/dist/types-B8idm_gu.d.cts +34 -0
- package/dist/types-Ce2qNHai.d.cts +57 -0
- package/dist/types-Ce2qNHai.d.ts +57 -0
- package/package.json +94 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,2446 @@
|
|
|
1
|
+
import { getAddress, zeroAddress, defineChain, createPublicClient, http, formatUnits, parseGwei, serializeTransaction, keccak256, encodeFunctionData, erc20Abi, parseAbi, parseUnits, decodeFunctionData, isAddress } from 'viem';
|
|
2
|
+
|
|
3
|
+
// src/core/keygen.ts
|
|
4
|
+
function firstClientIdFromKeyGen(data) {
|
|
5
|
+
const map = data?.ClientKeys;
|
|
6
|
+
if (!map || typeof map !== "object") return null;
|
|
7
|
+
for (const v of Object.values(map)) {
|
|
8
|
+
if (typeof v === "string" && v.trim()) return v.trim();
|
|
9
|
+
}
|
|
10
|
+
return null;
|
|
11
|
+
}
|
|
12
|
+
function requirePubKeyHex(keyGen) {
|
|
13
|
+
const ph = (keyGen.pubkeyhex ?? "").trim();
|
|
14
|
+
if (!ph) throw new Error("keyGen pubKey (pubkeyhex) is required");
|
|
15
|
+
return ph;
|
|
16
|
+
}
|
|
17
|
+
function keyListFromKeyGen(keyGen) {
|
|
18
|
+
return keyGen.keylist ?? [];
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
// src/core/purpose.ts
|
|
22
|
+
function mergePurposeText(purposeText, purposeSuffix) {
|
|
23
|
+
const t = purposeText.trim();
|
|
24
|
+
const suffix = (purposeSuffix ?? "").trim();
|
|
25
|
+
if (!suffix) return t;
|
|
26
|
+
return t ? `${t}
|
|
27
|
+
|
|
28
|
+
${suffix}` : suffix;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// src/core/envelope.ts
|
|
32
|
+
function finalizeMultisign(input) {
|
|
33
|
+
const { keyGen, destinationChainID, legs } = input;
|
|
34
|
+
if (legs.length === 0) {
|
|
35
|
+
throw new Error("finalizeMultisign requires at least one leg");
|
|
36
|
+
}
|
|
37
|
+
const ph = (keyGen.pubkeyhex ?? "").trim();
|
|
38
|
+
if (!ph) throw new Error("keyGen pubKey (pubkeyhex) is required");
|
|
39
|
+
const keyList = keyGen.keylist ?? [];
|
|
40
|
+
const clientId = firstClientIdFromKeyGen(keyGen);
|
|
41
|
+
const first = legs[0];
|
|
42
|
+
const messageHashes = legs.map((l) => l.msgHash);
|
|
43
|
+
const messageRawBatch = legs.map((l) => l.msgRaw);
|
|
44
|
+
const batchMeta = legs.map((l) => ({
|
|
45
|
+
destinationAddress: l.destinationAddress,
|
|
46
|
+
signatureText: l.signatureText,
|
|
47
|
+
...l.audit
|
|
48
|
+
}));
|
|
49
|
+
const proposalTxParams = legs.map((l) => l.proposalTxParams).filter((p) => p != null && typeof p === "object");
|
|
50
|
+
const extraPayload = {
|
|
51
|
+
batchMeta,
|
|
52
|
+
...input.extraJSON ?? {}
|
|
53
|
+
};
|
|
54
|
+
const extraJSON = JSON.stringify(extraPayload);
|
|
55
|
+
const bodyForSign = {
|
|
56
|
+
keyList,
|
|
57
|
+
pubKey: ph,
|
|
58
|
+
msgHash: messageHashes[0],
|
|
59
|
+
msgRaw: first.msgRaw,
|
|
60
|
+
destinationChainID,
|
|
61
|
+
destinationAddress: input.destinationAddress ?? first.destinationAddress,
|
|
62
|
+
extraJSON,
|
|
63
|
+
signatureText: first.signatureText,
|
|
64
|
+
purpose: mergePurposeText(input.purposeText, input.purposeSuffix),
|
|
65
|
+
...first.feeSnapshot
|
|
66
|
+
};
|
|
67
|
+
if (legs.length > 1) {
|
|
68
|
+
bodyForSign.messageHashes = messageHashes;
|
|
69
|
+
bodyForSign.messageRawBatch = messageRawBatch;
|
|
70
|
+
}
|
|
71
|
+
if (proposalTxParams.length > 0) {
|
|
72
|
+
bodyForSign.proposalTxParams = proposalTxParams;
|
|
73
|
+
}
|
|
74
|
+
const valueWei = first.valueWei;
|
|
75
|
+
if (valueWei != null && valueWei > 0n) {
|
|
76
|
+
bodyForSign.value = valueWei.toString();
|
|
77
|
+
}
|
|
78
|
+
if (clientId) bodyForSign.clientId = clientId;
|
|
79
|
+
return { bodyForSign, messageToSign: JSON.stringify(bodyForSign) };
|
|
80
|
+
}
|
|
81
|
+
var coreChainCategoryModule = {
|
|
82
|
+
category: "evm",
|
|
83
|
+
finalizeMultisign
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
// src/core/registry.ts
|
|
87
|
+
var modules = [];
|
|
88
|
+
function registerProtocolModule(mod) {
|
|
89
|
+
const existing = modules.findIndex((m) => m.id === mod.id);
|
|
90
|
+
if (existing >= 0) {
|
|
91
|
+
modules[existing] = mod;
|
|
92
|
+
} else {
|
|
93
|
+
modules.push(mod);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
function getProtocolModules() {
|
|
97
|
+
return modules;
|
|
98
|
+
}
|
|
99
|
+
function getProtocolModule(id) {
|
|
100
|
+
return modules.find((m) => m.id === id);
|
|
101
|
+
}
|
|
102
|
+
function getActionsByChainCategory(category) {
|
|
103
|
+
return modules.filter((m) => m.chainCategory === category).flatMap((m) => m.actions);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// src/core/nodeRead.ts
|
|
107
|
+
function nodeFetchWithReadAuth(url, init, auth) {
|
|
108
|
+
const method = (init?.method ?? "GET").toUpperCase();
|
|
109
|
+
const headers = new Headers(init?.headers);
|
|
110
|
+
if (auth.bearerOnGet && method === "GET" && auth.jwt && auth.jwt.trim()) {
|
|
111
|
+
headers.set("Authorization", `Bearer ${auth.jwt.trim()}`);
|
|
112
|
+
}
|
|
113
|
+
return fetch(url, { ...init, headers });
|
|
114
|
+
}
|
|
115
|
+
function isEvmNativeToken(address) {
|
|
116
|
+
try {
|
|
117
|
+
return getAddress(address) === zeroAddress;
|
|
118
|
+
} catch {
|
|
119
|
+
return address.toLowerCase() === zeroAddress;
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
function matchEvmTokenKind(kind, address) {
|
|
123
|
+
if (kind === "native") return isEvmNativeToken(address);
|
|
124
|
+
if (kind === "erc20" || kind === "ctmerc20" || kind === "ctmrwa1") {
|
|
125
|
+
return !isEvmNativeToken(address);
|
|
126
|
+
}
|
|
127
|
+
return true;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// src/chains/evm/txParams.ts
|
|
131
|
+
function gasLimitFromEstimateAndChainConfig(estimatedGas, chainGasLimit) {
|
|
132
|
+
if (chainGasLimit == null || !Number.isFinite(chainGasLimit) || chainGasLimit <= 0) {
|
|
133
|
+
return estimatedGas;
|
|
134
|
+
}
|
|
135
|
+
const cfg = BigInt(Math.floor(chainGasLimit));
|
|
136
|
+
return cfg > estimatedGas ? cfg : estimatedGas;
|
|
137
|
+
}
|
|
138
|
+
function routerSwapGasLimitFromEstimate(estimatedGas, chainGasLimit) {
|
|
139
|
+
if (chainGasLimit != null && Number.isFinite(chainGasLimit) && chainGasLimit > 0) {
|
|
140
|
+
return gasLimitFromEstimateAndChainConfig(estimatedGas, chainGasLimit);
|
|
141
|
+
}
|
|
142
|
+
return (estimatedGas * 12n + 9n) / 10n;
|
|
143
|
+
}
|
|
144
|
+
function composeFeePayloadToTxParams(p, legacy) {
|
|
145
|
+
const gl = p.txGasLimit ?? p.txgaslimit;
|
|
146
|
+
if (gl == null || String(gl).trim() === "") return void 0;
|
|
147
|
+
const n = p.txNonce ?? p.txnonce;
|
|
148
|
+
let nonce = 0;
|
|
149
|
+
if (typeof n === "bigint") nonce = Number(n);
|
|
150
|
+
else if (typeof n === "number") nonce = n;
|
|
151
|
+
else if (n != null) nonce = parseInt(String(n), 10);
|
|
152
|
+
if (!Number.isFinite(nonce)) nonce = 0;
|
|
153
|
+
const gasLimit = String(gl);
|
|
154
|
+
if (legacy) {
|
|
155
|
+
const gp = p.txGasPrice ?? p.txgasprice;
|
|
156
|
+
return {
|
|
157
|
+
nonce,
|
|
158
|
+
gasLimit,
|
|
159
|
+
txType: "legacy",
|
|
160
|
+
gasPrice: gp != null ? String(gp) : "0"
|
|
161
|
+
};
|
|
162
|
+
}
|
|
163
|
+
return {
|
|
164
|
+
nonce,
|
|
165
|
+
gasLimit,
|
|
166
|
+
txType: "eip1559",
|
|
167
|
+
maxFeePerGas: String(p.txMaxFeePerGas ?? ""),
|
|
168
|
+
maxPriorityFeePerGas: String(p.txMaxPriorityFeePerGas ?? "")
|
|
169
|
+
};
|
|
170
|
+
}
|
|
171
|
+
function triggerTxParamsFromComposeBody(body) {
|
|
172
|
+
const existing = body.txParams;
|
|
173
|
+
if (existing && typeof existing === "object" && !Array.isArray(existing)) {
|
|
174
|
+
const o = existing;
|
|
175
|
+
if (String(o.gasLimit ?? "").trim() !== "") {
|
|
176
|
+
return { ...o };
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
const pb = body.proposalTxParams;
|
|
180
|
+
if (Array.isArray(pb) && pb.length > 0 && typeof pb[0] === "object" && pb[0] !== null) {
|
|
181
|
+
const first = pb[0];
|
|
182
|
+
if (String(first.gasLimit ?? "").trim() !== "") {
|
|
183
|
+
return { ...first };
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
const fromSnapshot = composeFeePayloadToTxParams(
|
|
187
|
+
body,
|
|
188
|
+
body.txMaxFeePerGas == null && body.txMaxPriorityFeePerGas == null
|
|
189
|
+
);
|
|
190
|
+
if (fromSnapshot) return fromSnapshot;
|
|
191
|
+
return {
|
|
192
|
+
nonce: 0,
|
|
193
|
+
gasLimit: "",
|
|
194
|
+
txType: "legacy",
|
|
195
|
+
gasPrice: "0"
|
|
196
|
+
};
|
|
197
|
+
}
|
|
198
|
+
function proposalTxParamsToFeeSnapshot(params) {
|
|
199
|
+
if (params.txType === "legacy") {
|
|
200
|
+
return {
|
|
201
|
+
txNonce: params.nonce,
|
|
202
|
+
txGasLimit: params.gasLimit,
|
|
203
|
+
txGasPrice: params.gasPrice ?? "0"
|
|
204
|
+
};
|
|
205
|
+
}
|
|
206
|
+
return {
|
|
207
|
+
txNonce: params.nonce,
|
|
208
|
+
txGasLimit: params.gasLimit,
|
|
209
|
+
txMaxFeePerGas: params.maxFeePerGas ?? "",
|
|
210
|
+
txMaxPriorityFeePerGas: params.maxPriorityFeePerGas ?? ""
|
|
211
|
+
};
|
|
212
|
+
}
|
|
213
|
+
async function fetchChainFeeParams(rpcUrl, chainId) {
|
|
214
|
+
const url = rpcUrl.trim();
|
|
215
|
+
if (!url) return { isEip1559: false };
|
|
216
|
+
const chainIdNum = typeof chainId === "string" ? parseInt(chainId, 10) : chainId;
|
|
217
|
+
if (Number.isNaN(chainIdNum)) return { isEip1559: false };
|
|
218
|
+
const chain = defineChain({
|
|
219
|
+
id: chainIdNum,
|
|
220
|
+
name: "Discovery",
|
|
221
|
+
nativeCurrency: { decimals: 18, name: "Ether", symbol: "ETH" },
|
|
222
|
+
rpcUrls: { default: { http: [url] } }
|
|
223
|
+
});
|
|
224
|
+
const publicClient = createPublicClient({
|
|
225
|
+
chain,
|
|
226
|
+
transport: http(url)
|
|
227
|
+
});
|
|
228
|
+
const getGasPriceGwei = async () => {
|
|
229
|
+
const gasPriceWei = await publicClient.getGasPrice();
|
|
230
|
+
return parseFloat(formatUnits(gasPriceWei, 9));
|
|
231
|
+
};
|
|
232
|
+
try {
|
|
233
|
+
const block = await publicClient.getBlock({ blockTag: "latest" });
|
|
234
|
+
const baseFeePerGas = block?.baseFeePerGas;
|
|
235
|
+
if (baseFeePerGas == null || baseFeePerGas === void 0) {
|
|
236
|
+
const gasPriceGwei2 = await getGasPriceGwei();
|
|
237
|
+
return { isEip1559: false, gasPriceGwei: gasPriceGwei2 };
|
|
238
|
+
}
|
|
239
|
+
const baseFeeGwei = parseFloat(formatUnits(baseFeePerGas, 9));
|
|
240
|
+
let priorityFeeGwei;
|
|
241
|
+
try {
|
|
242
|
+
const priorityWei = await publicClient.estimateMaxPriorityFeePerGas();
|
|
243
|
+
priorityFeeGwei = parseFloat(formatUnits(priorityWei, 9));
|
|
244
|
+
} catch {
|
|
245
|
+
}
|
|
246
|
+
const gasPriceGwei = await getGasPriceGwei();
|
|
247
|
+
return {
|
|
248
|
+
isEip1559: true,
|
|
249
|
+
baseFeeGwei,
|
|
250
|
+
priorityFeeGwei,
|
|
251
|
+
gasPriceGwei
|
|
252
|
+
};
|
|
253
|
+
} catch {
|
|
254
|
+
try {
|
|
255
|
+
const gasPriceWei = await publicClient.getGasPrice();
|
|
256
|
+
const gasPriceGwei = parseFloat(formatUnits(gasPriceWei, 9));
|
|
257
|
+
return { isEip1559: false, gasPriceGwei };
|
|
258
|
+
} catch {
|
|
259
|
+
return { isEip1559: false };
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
function finalizeEip1559Fees(maxFeePerGas, maxPriorityFeePerGas, floor, baseWei) {
|
|
264
|
+
let maxP = maxPriorityFeePerGas;
|
|
265
|
+
let maxF = maxFeePerGas;
|
|
266
|
+
if (baseWei > 0n && maxF < baseWei + maxP) {
|
|
267
|
+
maxF = baseWei + maxP + parseGwei("0.001");
|
|
268
|
+
}
|
|
269
|
+
if (maxF < maxP) {
|
|
270
|
+
maxF = baseWei > 0n ? baseWei + maxP + parseGwei("0.001") : maxP * 2n;
|
|
271
|
+
}
|
|
272
|
+
return { maxFeePerGas: maxF, maxPriorityFeePerGas: maxP };
|
|
273
|
+
}
|
|
274
|
+
function alignEip1559FeesWithLatestBase(maxFeePerGas, maxPriorityFeePerGas, latestBlockBaseFeeWei) {
|
|
275
|
+
return finalizeEip1559Fees(maxFeePerGas, maxPriorityFeePerGas, null, latestBlockBaseFeeWei);
|
|
276
|
+
}
|
|
277
|
+
function gweiToDecimalString(n) {
|
|
278
|
+
if (!Number.isFinite(n)) return "0";
|
|
279
|
+
if (n === 0) return "0";
|
|
280
|
+
const s = String(n);
|
|
281
|
+
if (s.indexOf("e") !== -1 || s.indexOf("E") !== -1) return n.toFixed(9).replace(/\.?0+$/, "") || "0";
|
|
282
|
+
return s;
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
// src/chains/evm/buildBatch.ts
|
|
286
|
+
async function buildEvmMultisignBatch(args) {
|
|
287
|
+
const { context, steps } = args;
|
|
288
|
+
const {
|
|
289
|
+
chainId,
|
|
290
|
+
rpcUrl,
|
|
291
|
+
executorAddress,
|
|
292
|
+
chainDetail,
|
|
293
|
+
useCustomGas,
|
|
294
|
+
customGasChainDetails,
|
|
295
|
+
keyGen,
|
|
296
|
+
purposeText
|
|
297
|
+
} = context;
|
|
298
|
+
if (steps.length === 0) throw new Error("buildEvmMultisignBatch requires at least one step");
|
|
299
|
+
const ch = defineChain({
|
|
300
|
+
id: chainId,
|
|
301
|
+
name: "Destination",
|
|
302
|
+
nativeCurrency: { decimals: 18, name: "Ether", symbol: "ETH" },
|
|
303
|
+
rpcUrls: { default: { http: [rpcUrl] } }
|
|
304
|
+
});
|
|
305
|
+
const publicClient = createPublicClient({ chain: ch, transport: http(rpcUrl) });
|
|
306
|
+
const feeParams = await fetchChainFeeParams(rpcUrl, chainId);
|
|
307
|
+
const legacy = Boolean(chainDetail?.legacy) || !feeParams.isEip1559;
|
|
308
|
+
const latestBaseFeeWei = !legacy ? (await publicClient.getBlock({ blockTag: "latest" })).baseFeePerGas ?? 0n : 0n;
|
|
309
|
+
const gasLimitConfig = useCustomGas && chainDetail?.gasLimit != null ? Number(chainDetail.gasLimit) : void 0;
|
|
310
|
+
const chainGasLimitRouter = chainDetail?.gasLimit != null && Number.isFinite(Number(chainDetail.gasLimit)) && Number(chainDetail.gasLimit) > 0 ? Number(chainDetail.gasLimit) : void 0;
|
|
311
|
+
const gasFeeMultiplier = useCustomGas && chainDetail?.gasMultiplier != null ? Number(chainDetail.gasMultiplier) : void 0;
|
|
312
|
+
const executor = getAddress(executorAddress);
|
|
313
|
+
const baseNonce = await publicClient.getTransactionCount({ address: executor, blockTag: "pending" });
|
|
314
|
+
const legs = [];
|
|
315
|
+
for (let i = 0; i < steps.length; i++) {
|
|
316
|
+
const step = steps[i];
|
|
317
|
+
const currentNonce = baseNonce + i;
|
|
318
|
+
let estimatedGas;
|
|
319
|
+
try {
|
|
320
|
+
estimatedGas = await publicClient.estimateGas({
|
|
321
|
+
to: step.to,
|
|
322
|
+
data: step.data,
|
|
323
|
+
value: step.value,
|
|
324
|
+
account: executor
|
|
325
|
+
});
|
|
326
|
+
} catch {
|
|
327
|
+
estimatedGas = step.fallbackGas ?? 100000n;
|
|
328
|
+
}
|
|
329
|
+
let gasLimitI;
|
|
330
|
+
if (args.resolveGasLimit) {
|
|
331
|
+
gasLimitI = await args.resolveGasLimit({ step, index: i, estimatedGas, publicClient });
|
|
332
|
+
} else if (step.routerSwap) {
|
|
333
|
+
gasLimitI = routerSwapGasLimitFromEstimate(estimatedGas, chainGasLimitRouter);
|
|
334
|
+
} else {
|
|
335
|
+
gasLimitI = useCustomGas ? gasLimitFromEstimateAndChainConfig(estimatedGas, gasLimitConfig) : estimatedGas;
|
|
336
|
+
}
|
|
337
|
+
let proposalTxParams;
|
|
338
|
+
let feeSnapshot;
|
|
339
|
+
let serialized;
|
|
340
|
+
if (legacy) {
|
|
341
|
+
let gasPriceWei = await publicClient.getGasPrice();
|
|
342
|
+
if (useCustomGas && gasFeeMultiplier != null && gasFeeMultiplier > 0) {
|
|
343
|
+
gasPriceWei = gasPriceWei * BigInt(100 + gasFeeMultiplier) / 100n;
|
|
344
|
+
}
|
|
345
|
+
if (useCustomGas && chainDetail?.gasPrice != null && chainDetail.gasPrice > 0) {
|
|
346
|
+
const configured = parseGwei(gweiToDecimalString(Number(chainDetail.gasPrice)));
|
|
347
|
+
if (configured > gasPriceWei) gasPriceWei = configured;
|
|
348
|
+
}
|
|
349
|
+
serialized = serializeTransaction({
|
|
350
|
+
type: "legacy",
|
|
351
|
+
to: step.to,
|
|
352
|
+
data: step.data,
|
|
353
|
+
value: step.value,
|
|
354
|
+
gas: gasLimitI,
|
|
355
|
+
gasPrice: gasPriceWei,
|
|
356
|
+
nonce: currentNonce,
|
|
357
|
+
chainId
|
|
358
|
+
});
|
|
359
|
+
proposalTxParams = {
|
|
360
|
+
nonce: currentNonce,
|
|
361
|
+
gasLimit: gasLimitI.toString(),
|
|
362
|
+
txType: "legacy",
|
|
363
|
+
gasPrice: gasPriceWei.toString()
|
|
364
|
+
};
|
|
365
|
+
feeSnapshot = proposalTxParamsToFeeSnapshot(proposalTxParams);
|
|
366
|
+
} else {
|
|
367
|
+
const fetchedBase = feeParams.baseFeeGwei ?? 0;
|
|
368
|
+
const fetchedPriority = feeParams.priorityFeeGwei ?? 0;
|
|
369
|
+
const configuredBase = useCustomGas && chainDetail?.baseFee != null ? Number(chainDetail.baseFee) : 0;
|
|
370
|
+
const configuredPriority = useCustomGas && chainDetail?.priorityFee != null ? Number(chainDetail.priorityFee) : 0;
|
|
371
|
+
const effectiveBaseFeeGwei = Math.max(fetchedBase, configuredBase);
|
|
372
|
+
const effectivePriorityFeeGwei = Math.max(fetchedPriority, configuredPriority);
|
|
373
|
+
const baseFeeMultiplierPct = useCustomGas && chainDetail?.baseFeeMultiplier != null ? Math.max(100, Number(chainDetail.baseFeeMultiplier)) : 100;
|
|
374
|
+
const baseComponentGwei = effectiveBaseFeeGwei * baseFeeMultiplierPct / 100;
|
|
375
|
+
const maxFeePerGasGwei = baseComponentGwei + effectivePriorityFeeGwei;
|
|
376
|
+
let maxPriorityFeePerGas = effectivePriorityFeeGwei > 0 ? parseGwei(gweiToDecimalString(effectivePriorityFeeGwei)) : parseGwei("1");
|
|
377
|
+
let maxFeePerGas = parseGwei(gweiToDecimalString(maxFeePerGasGwei));
|
|
378
|
+
if (useCustomGas && gasFeeMultiplier != null && gasFeeMultiplier > 0) {
|
|
379
|
+
maxPriorityFeePerGas = maxPriorityFeePerGas * BigInt(100 + gasFeeMultiplier) / 100n;
|
|
380
|
+
maxFeePerGas = maxFeePerGas * BigInt(100 + gasFeeMultiplier) / 100n;
|
|
381
|
+
}
|
|
382
|
+
({ maxFeePerGas, maxPriorityFeePerGas } = alignEip1559FeesWithLatestBase(
|
|
383
|
+
maxFeePerGas,
|
|
384
|
+
maxPriorityFeePerGas,
|
|
385
|
+
latestBaseFeeWei
|
|
386
|
+
));
|
|
387
|
+
serialized = serializeTransaction({
|
|
388
|
+
type: "eip1559",
|
|
389
|
+
to: step.to,
|
|
390
|
+
data: step.data,
|
|
391
|
+
value: step.value,
|
|
392
|
+
gas: gasLimitI,
|
|
393
|
+
maxFeePerGas,
|
|
394
|
+
maxPriorityFeePerGas,
|
|
395
|
+
nonce: currentNonce,
|
|
396
|
+
chainId
|
|
397
|
+
});
|
|
398
|
+
proposalTxParams = {
|
|
399
|
+
nonce: currentNonce,
|
|
400
|
+
gasLimit: gasLimitI.toString(),
|
|
401
|
+
txType: "eip1559",
|
|
402
|
+
maxFeePerGas: maxFeePerGas.toString(),
|
|
403
|
+
maxPriorityFeePerGas: maxPriorityFeePerGas.toString()
|
|
404
|
+
};
|
|
405
|
+
feeSnapshot = i === 0 ? proposalTxParamsToFeeSnapshot(proposalTxParams) : {};
|
|
406
|
+
}
|
|
407
|
+
const h = keccak256(serialized);
|
|
408
|
+
const msgHash = h.startsWith("0x") ? h.slice(2) : h;
|
|
409
|
+
const batchMetaExtra = args.buildBatchMeta({ step, index: i, gasLimit: gasLimitI });
|
|
410
|
+
legs.push({
|
|
411
|
+
msgHash,
|
|
412
|
+
msgRaw: i === 0 && args.firstMsgRawNo0x != null ? args.firstMsgRawNo0x : serialized,
|
|
413
|
+
destinationAddress: step.to,
|
|
414
|
+
signatureText: typeof batchMetaExtra.signatureText === "string" ? batchMetaExtra.signatureText : JSON.stringify(batchMetaExtra.signatureText ?? {}),
|
|
415
|
+
audit: batchMetaExtra,
|
|
416
|
+
feeSnapshot: i === 0 ? feeSnapshot : {},
|
|
417
|
+
proposalTxParams,
|
|
418
|
+
valueWei: i === 0 ? step.value : void 0
|
|
419
|
+
});
|
|
420
|
+
if (i === 0 && args.firstMsgRawNo0x != null) {
|
|
421
|
+
legs[0].msgRaw = args.firstMsgRawNo0x;
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
const extraJSON = {};
|
|
425
|
+
if (useCustomGas && customGasChainDetails && Object.keys(customGasChainDetails).length > 0) {
|
|
426
|
+
extraJSON.customGasChainDetails = customGasChainDetails;
|
|
427
|
+
}
|
|
428
|
+
const result = finalizeMultisign({
|
|
429
|
+
keyGen,
|
|
430
|
+
purposeText,
|
|
431
|
+
purposeSuffix: args.purposeSuffix,
|
|
432
|
+
destinationChainID: String(chainId),
|
|
433
|
+
destinationAddress: args.destinationAddress ?? steps[0].to,
|
|
434
|
+
legs,
|
|
435
|
+
extraJSON: Object.keys(extraJSON).length > 0 ? extraJSON : void 0
|
|
436
|
+
});
|
|
437
|
+
const pv = args.payableValueWei;
|
|
438
|
+
if (pv != null && pv > 0n) {
|
|
439
|
+
result.bodyForSign.value = pv.toString();
|
|
440
|
+
}
|
|
441
|
+
return result;
|
|
442
|
+
}
|
|
443
|
+
var evmChainCategoryModule = {
|
|
444
|
+
category: "evm",
|
|
445
|
+
finalizeMultisign,
|
|
446
|
+
buildEvmMultisignBatch
|
|
447
|
+
};
|
|
448
|
+
|
|
449
|
+
// src/chains/evm/fees/customGas.ts
|
|
450
|
+
function chainSnapshotForCustomGasExtraJSON(chainDetail) {
|
|
451
|
+
const lr = chainDetail.legacy;
|
|
452
|
+
const legacy = lr === true || typeof lr === "string" && lr.toLowerCase() === "true";
|
|
453
|
+
const push = (o, key, v) => {
|
|
454
|
+
if (v === void 0 || v === null) return;
|
|
455
|
+
if (typeof v === "string" && v.trim() === "") return;
|
|
456
|
+
o[key] = v;
|
|
457
|
+
};
|
|
458
|
+
const fields = {};
|
|
459
|
+
push(fields, "gasLimit", chainDetail.gasLimit);
|
|
460
|
+
if (legacy) {
|
|
461
|
+
push(fields, "gasMultiplier", chainDetail.gasMultiplier);
|
|
462
|
+
push(fields, "gasPrice", chainDetail.gasPrice);
|
|
463
|
+
} else {
|
|
464
|
+
push(fields, "gasMultiplier", chainDetail.gasMultiplier);
|
|
465
|
+
push(fields, "baseFee", chainDetail.baseFee);
|
|
466
|
+
push(fields, "priorityFee", chainDetail.priorityFee);
|
|
467
|
+
push(fields, "baseFeeMultiplier", chainDetail.baseFeeMultiplier);
|
|
468
|
+
}
|
|
469
|
+
push(fields, "legacy", legacy);
|
|
470
|
+
return fields;
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
// src/chains/evm/chainIdParse.ts
|
|
474
|
+
function parseEvmChainIdToNumber(chainId) {
|
|
475
|
+
if (chainId == null) return Number.NaN;
|
|
476
|
+
if (typeof chainId === "bigint") {
|
|
477
|
+
const n = Number(chainId);
|
|
478
|
+
return Number.isSafeInteger(n) && n >= 0 ? n : Number.NaN;
|
|
479
|
+
}
|
|
480
|
+
if (typeof chainId === "number") {
|
|
481
|
+
return Number.isInteger(chainId) && chainId >= 0 ? chainId : Number.NaN;
|
|
482
|
+
}
|
|
483
|
+
const t = String(chainId).trim();
|
|
484
|
+
if (!t) return Number.NaN;
|
|
485
|
+
const low = t.toLowerCase();
|
|
486
|
+
if (low.startsWith("eip155:")) {
|
|
487
|
+
const rest = t.slice("eip155:".length).trim();
|
|
488
|
+
const n = Number.parseInt(rest, 10);
|
|
489
|
+
return Number.isNaN(n) || n < 0 ? Number.NaN : n;
|
|
490
|
+
}
|
|
491
|
+
if (low.startsWith("0x")) {
|
|
492
|
+
return Number.parseInt(t, 16);
|
|
493
|
+
}
|
|
494
|
+
return Number.parseInt(t, 10);
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
// src/chains/solana/types.ts
|
|
498
|
+
var solanaChainCategoryModule = {
|
|
499
|
+
category: "solana"
|
|
500
|
+
};
|
|
501
|
+
|
|
502
|
+
// src/chains/solana/index.ts
|
|
503
|
+
var SOLANA_CHAIN_CATEGORY = "solana";
|
|
504
|
+
|
|
505
|
+
// src/chains/near/types.ts
|
|
506
|
+
var nearChainCategoryModule = {
|
|
507
|
+
category: "near"
|
|
508
|
+
};
|
|
509
|
+
|
|
510
|
+
// src/chains/near/index.ts
|
|
511
|
+
var NEAR_CHAIN_CATEGORY = "near";
|
|
512
|
+
var PERMIT2_ADDRESS = "0x000000000022D473030F116dDEE9F6B43aC78BA3";
|
|
513
|
+
var UNIVERSAL_ROUTER_SPENDER = {
|
|
514
|
+
1: "0x66a9893cc07d91d95644aedd05d03f95e1dba8af",
|
|
515
|
+
5: "0x3fC91A3afd70395Cd496C647d5a6CC9D4B2b7FAD",
|
|
516
|
+
11155111: "0x3a9d48ab9751398bbfa63ad67599bb04e4bdf98b",
|
|
517
|
+
137: "0x1095692a6237d83c6a72f3f5efedb9a670c49223",
|
|
518
|
+
80001: "0x3fC91A3afd70395Cd496C647d5a6CC9D4B2b7FAD",
|
|
519
|
+
10: "0x851116d9223fabed8e56c0e6b8ad0c31d98b3507",
|
|
520
|
+
420: "0x3fC91A3afd70395Cd496C647d5a6CC9D4B2b7FAD",
|
|
521
|
+
42161: "0xa51afafe0263b40edaef0df8781ea9aa03e381a3",
|
|
522
|
+
421613: "0x3fC91A3afd70395Cd496C647d5a6CC9D4B2b7FAD",
|
|
523
|
+
42220: "0xcb695bc5D3Aa22cAD1E6DF07801b061a05A0233A",
|
|
524
|
+
44787: "0x3fC91A3afd70395Cd496C647d5a6CC9D4B2b7FAD",
|
|
525
|
+
56: "0x1906c1d672b88cd1b9ac7593301ca990f94eae07",
|
|
526
|
+
43114: "0x94b75331ae8d42c1b61065089b7d48fe14aa73b7",
|
|
527
|
+
84531: "0xd0872d928672ae2ff74bdb2f5130ac12229cafaf",
|
|
528
|
+
8453: "0x6fF5693b99212da76ad316178a184ab56d299b43",
|
|
529
|
+
81457: "0xeabbcb3e8e415306207ef514f660a3f820025be3",
|
|
530
|
+
7777777: "0x3315ef7ca28db74abadc6c44570efdf06b04b020",
|
|
531
|
+
324: "0x28731BCC616B5f51dD52CF2e4dF0E78dD1136C06",
|
|
532
|
+
480: "0x8ac7bee993bb44dab564ea4bc9ea67bf9eb5e743",
|
|
533
|
+
1301: "0xf70536b3bcc1bd1a972dc186a2cf84cc6da6be5d",
|
|
534
|
+
130: "0xef740bf23acae26f6492b10de645d6b98dc8eaf3",
|
|
535
|
+
10143: "0x3ae6d8a282d67893e17aa70ebffb33ee5aa65893",
|
|
536
|
+
84532: "0x492e6456d9528771018deb9e87ef7750ef184104",
|
|
537
|
+
1868: "0x0e2850543f69f678257266e0907ff9a58b3f13de",
|
|
538
|
+
143: "0x0d97dc33264bfc1c226207428a79b26757fb9dc3",
|
|
539
|
+
59144: "0x661e93cca42afacb172121ef892830ca3b70f08d",
|
|
540
|
+
4217: "0x1febb76be10aaf3a1402f04e8e835f2c382f7914",
|
|
541
|
+
196: "0x5507749f2c558bb3e162c6e90c314c092e7372ff"
|
|
542
|
+
};
|
|
543
|
+
function isUniswapV4ChainSupported(chainId) {
|
|
544
|
+
if (chainId == null) return false;
|
|
545
|
+
const n = parseEvmChainIdToNumber(chainId);
|
|
546
|
+
if (Number.isNaN(n) || n < 0) return false;
|
|
547
|
+
const a = UNIVERSAL_ROUTER_SPENDER[n];
|
|
548
|
+
return typeof a === "string" && a.startsWith("0x");
|
|
549
|
+
}
|
|
550
|
+
function getUniswapUniversalRouterSpenderOrThrow(chainId) {
|
|
551
|
+
const raw = UNIVERSAL_ROUTER_SPENDER[chainId];
|
|
552
|
+
if (!raw || !raw.startsWith("0x")) {
|
|
553
|
+
throw new Error(
|
|
554
|
+
`No Uniswap Universal Router (spender) is configured for chainId ${chainId}. Add this chain to uniswap-v4/constants or use a chain supported by the Uniswap SDK.`
|
|
555
|
+
);
|
|
556
|
+
}
|
|
557
|
+
return getAddress(raw);
|
|
558
|
+
}
|
|
559
|
+
var MAX_UINT160 = (1n << 160n) - 1n;
|
|
560
|
+
var MAX_UINT48 = (1n << 48n) - 1n;
|
|
561
|
+
var UNISWAP_UNIVERSAL_ROUTER_DEFAULT_GAS_UNITS = 1500000n;
|
|
562
|
+
var UNISWAP_TRADE_BASE_DEFAULT = "https://trade-api.gateway.uniswap.org/v1";
|
|
563
|
+
var UNISWAP_UNIVERSAL_ROUTER_VERSION_DEFAULT = "2.0";
|
|
564
|
+
var UNISWAP_SWAP_DEFAULT_EXPIRY_MINUTES = 30;
|
|
565
|
+
var DEFAULT_TRADE_BASE = "https://trade-api.gateway.uniswap.org/v1";
|
|
566
|
+
var UNISWAP_QUOTE_HEADERS_BASE = {
|
|
567
|
+
"Content-Type": "application/json",
|
|
568
|
+
"User-Agent": "ctm-mpc-defi uniswapTradeQuote/1.0 (TS)"
|
|
569
|
+
};
|
|
570
|
+
function parseUniswapChainId(value) {
|
|
571
|
+
if (typeof value === "number") {
|
|
572
|
+
if (!Number.isInteger(value) || value < 0) {
|
|
573
|
+
throw new TypeError("chainId number must be a non-negative integer");
|
|
574
|
+
}
|
|
575
|
+
return value;
|
|
576
|
+
}
|
|
577
|
+
const t = value.trim();
|
|
578
|
+
if (t.toLowerCase().startsWith("0x")) {
|
|
579
|
+
return Number.parseInt(t, 16);
|
|
580
|
+
}
|
|
581
|
+
return Number.parseInt(t, 10);
|
|
582
|
+
}
|
|
583
|
+
function trimAddr(a) {
|
|
584
|
+
return a.trim();
|
|
585
|
+
}
|
|
586
|
+
function isUniswapTokenInAddressNative(tokenIn) {
|
|
587
|
+
const t = (tokenIn ?? "").toString().trim();
|
|
588
|
+
if (!t) return false;
|
|
589
|
+
try {
|
|
590
|
+
return getAddress(t) === zeroAddress;
|
|
591
|
+
} catch {
|
|
592
|
+
return t.toLowerCase() === "0x0000000000000000000000000000000000000000";
|
|
593
|
+
}
|
|
594
|
+
}
|
|
595
|
+
function isUniswapFullQuoteResponseNativeIn(stored) {
|
|
596
|
+
if (!stored) return false;
|
|
597
|
+
const q = stored.quote;
|
|
598
|
+
if (!q || typeof q !== "object" || Array.isArray(q)) return false;
|
|
599
|
+
const input = q.input;
|
|
600
|
+
if (!input) return false;
|
|
601
|
+
const raw = (input.address ?? input.token ?? "").toString().trim();
|
|
602
|
+
return isUniswapTokenInAddressNative(raw);
|
|
603
|
+
}
|
|
604
|
+
async function fetchEthereumAddressForKeyGen(managementNodeUrl, keyGenId, readAuth = { bearerOnGet: true, jwt: null }, init) {
|
|
605
|
+
const base = managementNodeUrl.trim().replace(/\/$/, "");
|
|
606
|
+
if (!base) {
|
|
607
|
+
throw new Error("managementNodeUrl is required to resolve keyGen");
|
|
608
|
+
}
|
|
609
|
+
const id = (keyGenId || "").trim();
|
|
610
|
+
if (!id) {
|
|
611
|
+
throw new Error("keyGen is required");
|
|
612
|
+
}
|
|
613
|
+
const url = `${base}/getKeyGenResultById?id=${encodeURIComponent(id)}`;
|
|
614
|
+
const res = await nodeFetchWithReadAuth(url, { ...init, method: "GET", cache: "no-store" }, readAuth);
|
|
615
|
+
if (!res.ok) {
|
|
616
|
+
const t = await res.text().catch(() => "");
|
|
617
|
+
throw new Error(
|
|
618
|
+
`getKeyGenResultById HTTP ${res.status}: ${t || res.statusText}`.trim()
|
|
619
|
+
);
|
|
620
|
+
}
|
|
621
|
+
const raw = await res.json();
|
|
622
|
+
const d = raw.Data ?? raw.data;
|
|
623
|
+
const eth = d?.ethereumaddress;
|
|
624
|
+
if (typeof eth !== "string" || !eth.trim()) {
|
|
625
|
+
throw new Error("getKeyGenResultById: ethereumaddress missing for this keyGen");
|
|
626
|
+
}
|
|
627
|
+
return eth.trim();
|
|
628
|
+
}
|
|
629
|
+
function resolveInputChainId(p) {
|
|
630
|
+
if (p.chainId !== void 0 && p.chainId !== null && String(p.chainId).trim() !== "") {
|
|
631
|
+
return p.chainId;
|
|
632
|
+
}
|
|
633
|
+
if (p.tokenInChainId !== void 0 && p.tokenInChainId !== null && String(p.tokenInChainId).trim() !== "") {
|
|
634
|
+
return p.tokenInChainId;
|
|
635
|
+
}
|
|
636
|
+
if (p.tokenOutChainId !== void 0 && p.tokenOutChainId !== null && String(p.tokenOutChainId).trim() !== "") {
|
|
637
|
+
return p.tokenOutChainId;
|
|
638
|
+
}
|
|
639
|
+
throw new Error("Set chainId, tokenInChainId, or tokenOutChainId (same-chain) for the input side");
|
|
640
|
+
}
|
|
641
|
+
function buildUniswapQuoteRequestBody(args) {
|
|
642
|
+
const cIn = parseUniswapChainId(resolveInputChainId(args));
|
|
643
|
+
const cOut = args.tokenOutChainId !== void 0 && args.tokenOutChainId !== null && String(args.tokenOutChainId).trim() !== "" ? parseUniswapChainId(args.tokenOutChainId) : cIn;
|
|
644
|
+
const body = {
|
|
645
|
+
type: args.type,
|
|
646
|
+
amount: String(args.amount).trim(),
|
|
647
|
+
tokenInChainId: cIn,
|
|
648
|
+
tokenOutChainId: cOut,
|
|
649
|
+
tokenIn: trimAddr(args.tokenIn),
|
|
650
|
+
tokenOut: trimAddr(args.tokenOut),
|
|
651
|
+
swapper: trimAddr(args.swapper)
|
|
652
|
+
};
|
|
653
|
+
if (args.slippage !== void 0 && args.slippage !== null && String(args.slippage).trim() !== "") {
|
|
654
|
+
const s = typeof args.slippage === "number" ? args.slippage : Number.parseFloat(String(args.slippage).trim());
|
|
655
|
+
if (Number.isNaN(s)) {
|
|
656
|
+
throw new TypeError("slippage must be a number or a numeric string");
|
|
657
|
+
}
|
|
658
|
+
body.slippageTolerance = s;
|
|
659
|
+
} else {
|
|
660
|
+
body.autoSlippage = "DEFAULT";
|
|
661
|
+
}
|
|
662
|
+
return body;
|
|
663
|
+
}
|
|
664
|
+
function errorMessageFromUniswapJsonBody(value) {
|
|
665
|
+
if (value == null) return null;
|
|
666
|
+
if (typeof value === "string") {
|
|
667
|
+
const t = value.trim();
|
|
668
|
+
return t || null;
|
|
669
|
+
}
|
|
670
|
+
if (typeof value !== "object") return null;
|
|
671
|
+
if (Array.isArray(value)) {
|
|
672
|
+
if (value.length === 0) return null;
|
|
673
|
+
return errorMessageFromUniswapJsonBody(value[0]);
|
|
674
|
+
}
|
|
675
|
+
const o = value;
|
|
676
|
+
if (typeof o.detail === "string" && o.detail.trim()) {
|
|
677
|
+
const base = o.detail.trim();
|
|
678
|
+
const code = typeof o.errorCode === "string" && o.errorCode.trim() ? o.errorCode.trim() : "";
|
|
679
|
+
const req = typeof o.requestId === "string" && o.requestId.trim() ? o.requestId.trim() : "";
|
|
680
|
+
if (code) {
|
|
681
|
+
return req ? `${base} (${code}) [${req}]` : `${base} (${code})`;
|
|
682
|
+
}
|
|
683
|
+
return req ? `${base} [${req}]` : base;
|
|
684
|
+
}
|
|
685
|
+
if (typeof o.errorCode === "string" && o.errorCode.trim()) {
|
|
686
|
+
return o.errorCode.trim();
|
|
687
|
+
}
|
|
688
|
+
for (const k of ["message", "detail", "description", "title", "reason"]) {
|
|
689
|
+
const v = o[k];
|
|
690
|
+
if (typeof v === "string" && v.trim()) return v.trim();
|
|
691
|
+
}
|
|
692
|
+
if (typeof o.error === "string" && o.error.trim()) return o.error.trim();
|
|
693
|
+
if (o.error != null && typeof o.error === "object") {
|
|
694
|
+
const inner = errorMessageFromUniswapJsonBody(o.error);
|
|
695
|
+
if (inner) return inner;
|
|
696
|
+
}
|
|
697
|
+
if (Array.isArray(o.errors) && o.errors.length > 0) {
|
|
698
|
+
const inner = errorMessageFromUniswapJsonBody(o.errors[0]);
|
|
699
|
+
if (inner) return inner;
|
|
700
|
+
}
|
|
701
|
+
if (typeof o.code === "string" && o.code.trim()) return o.code.trim();
|
|
702
|
+
return null;
|
|
703
|
+
}
|
|
704
|
+
function bodyLooksLikeHtml(t) {
|
|
705
|
+
const s = t.slice(0, 256).trimStart();
|
|
706
|
+
return s.startsWith("<!") || s.toLowerCase().startsWith("<html");
|
|
707
|
+
}
|
|
708
|
+
function titleFromHtml(t) {
|
|
709
|
+
const m = t.match(/<title[^>]*>([^<]+)<\/title>/i);
|
|
710
|
+
if (!m?.[1]) return null;
|
|
711
|
+
const s = m[1].replace(/\s+/g, " ").trim();
|
|
712
|
+
return s || null;
|
|
713
|
+
}
|
|
714
|
+
function messageFromUniswapHttpResponseBody(text, status, statusText = "") {
|
|
715
|
+
const raw = (text ?? "").trim();
|
|
716
|
+
const st = (statusText ?? "").trim();
|
|
717
|
+
if (!raw) {
|
|
718
|
+
if (status > 0) {
|
|
719
|
+
return st ? `HTTP ${status} (${st})` : `HTTP ${status}`;
|
|
720
|
+
}
|
|
721
|
+
return "Empty response";
|
|
722
|
+
}
|
|
723
|
+
if (bodyLooksLikeHtml(raw)) {
|
|
724
|
+
const title = titleFromHtml(raw);
|
|
725
|
+
if (title) {
|
|
726
|
+
return status > 0 ? `HTTP ${status}: ${title}` : title;
|
|
727
|
+
}
|
|
728
|
+
return status > 0 ? "HTTP " + String(status) + ": response was not JSON (HTML or error page). Check API key, token pair, and network." : "Response was not JSON (HTML or error page).";
|
|
729
|
+
}
|
|
730
|
+
const forParse = raw.replace(/^\uFEFF/, "");
|
|
731
|
+
try {
|
|
732
|
+
const j = JSON.parse(forParse);
|
|
733
|
+
if (j && typeof j === "object") {
|
|
734
|
+
const msg = errorMessageFromUniswapJsonBody(j);
|
|
735
|
+
if (msg) {
|
|
736
|
+
if (status >= 400) {
|
|
737
|
+
return st ? `${msg} (HTTP ${status} ${st})` : `${msg} (HTTP ${status})`;
|
|
738
|
+
}
|
|
739
|
+
return msg;
|
|
740
|
+
}
|
|
741
|
+
}
|
|
742
|
+
} catch {
|
|
743
|
+
}
|
|
744
|
+
const oneLine = raw.replace(/\s+/g, " ").trim();
|
|
745
|
+
const short = oneLine.length > 500 ? `${oneLine.slice(0, 500)}\u2026` : oneLine;
|
|
746
|
+
if (status > 0) {
|
|
747
|
+
return st && !short ? `HTTP ${status} (${st})` : st ? `HTTP ${status}: ${short} (${st})` : `HTTP ${status}: ${short}`;
|
|
748
|
+
}
|
|
749
|
+
return short;
|
|
750
|
+
}
|
|
751
|
+
async function uniswapTradeQuote(args) {
|
|
752
|
+
const apiKey = (args.uniswapApiKey || "").trim();
|
|
753
|
+
if (!apiKey) {
|
|
754
|
+
throw new Error("uniswapApiKey (x-api-key) is required");
|
|
755
|
+
}
|
|
756
|
+
const keyGen = (args.keyGen || "").trim();
|
|
757
|
+
let swapper;
|
|
758
|
+
if ((args.swapper || "").trim()) {
|
|
759
|
+
swapper = trimAddr(args.swapper);
|
|
760
|
+
} else {
|
|
761
|
+
if (!keyGen) {
|
|
762
|
+
throw new Error("keyGen is required when swapper is not provided");
|
|
763
|
+
}
|
|
764
|
+
const mpc = (args.managementNodeUrl || "").trim();
|
|
765
|
+
if (!mpc) {
|
|
766
|
+
throw new Error("managementNodeUrl is required when swapper is not provided (to resolve keyGen)");
|
|
767
|
+
}
|
|
768
|
+
swapper = await fetchEthereumAddressForKeyGen(
|
|
769
|
+
mpc,
|
|
770
|
+
keyGen,
|
|
771
|
+
args.nodeReadAuth ?? { bearerOnGet: true, jwt: null }
|
|
772
|
+
);
|
|
773
|
+
}
|
|
774
|
+
const base = args.baseUrl && args.baseUrl.trim() ? args.baseUrl.trim().replace(/\/$/, "") : DEFAULT_TRADE_BASE;
|
|
775
|
+
const quoteUrl = `${base}/quote`;
|
|
776
|
+
const urv = args.universalRouterVersion && args.universalRouterVersion.trim() || "2.0";
|
|
777
|
+
const body = buildUniswapQuoteRequestBody({ ...args, swapper });
|
|
778
|
+
const nativeIn = isUniswapTokenInAddressNative(args.tokenIn);
|
|
779
|
+
const headers = {
|
|
780
|
+
...UNISWAP_QUOTE_HEADERS_BASE,
|
|
781
|
+
"x-api-key": apiKey,
|
|
782
|
+
"x-universal-router-version": urv,
|
|
783
|
+
"x-permit2-disabled": args.permit2Disabled === true ? "true" : "false",
|
|
784
|
+
/** Native (0x0) as token in: Trade API needs this for classic routes and payable /swap. */
|
|
785
|
+
"x-erc20eth-enabled": nativeIn ? "true" : "false"
|
|
786
|
+
};
|
|
787
|
+
const fetchFn = args.fetchImpl ?? globalThis.fetch;
|
|
788
|
+
const res = await fetchFn(quoteUrl, {
|
|
789
|
+
method: "POST",
|
|
790
|
+
headers,
|
|
791
|
+
body: JSON.stringify(body),
|
|
792
|
+
signal: args.signal
|
|
793
|
+
});
|
|
794
|
+
const text = await res.text();
|
|
795
|
+
if (!res.ok) {
|
|
796
|
+
throw new Error(messageFromUniswapHttpResponseBody(text, res.status, res.statusText));
|
|
797
|
+
}
|
|
798
|
+
const forParse = text.replace(/^\uFEFF/, "");
|
|
799
|
+
try {
|
|
800
|
+
return JSON.parse(forParse);
|
|
801
|
+
} catch {
|
|
802
|
+
throw new Error(
|
|
803
|
+
`Trade API: ${messageFromUniswapHttpResponseBody(text, res.status, res.statusText)}`
|
|
804
|
+
);
|
|
805
|
+
}
|
|
806
|
+
}
|
|
807
|
+
function parseUniswapQuoteClassicInOut(res) {
|
|
808
|
+
const inner = res.quote;
|
|
809
|
+
if (!inner || typeof inner !== "object" || Array.isArray(inner)) return null;
|
|
810
|
+
const q = inner;
|
|
811
|
+
const input = q.input;
|
|
812
|
+
const output = q.output;
|
|
813
|
+
const inAm = input?.amount;
|
|
814
|
+
const outAm = output?.amount;
|
|
815
|
+
if (typeof inAm !== "string" || !inAm || typeof outAm !== "string" || !outAm) return null;
|
|
816
|
+
try {
|
|
817
|
+
return { inputWei: BigInt(inAm), outputWei: BigInt(outAm) };
|
|
818
|
+
} catch {
|
|
819
|
+
return null;
|
|
820
|
+
}
|
|
821
|
+
}
|
|
822
|
+
function parseOptionalGasLimitString(raw) {
|
|
823
|
+
if (raw == null) return null;
|
|
824
|
+
const s = String(raw).trim();
|
|
825
|
+
if (!s) return null;
|
|
826
|
+
try {
|
|
827
|
+
if (/^0x[0-9a-fA-F]+$/.test(s)) return BigInt(s);
|
|
828
|
+
return BigInt(s);
|
|
829
|
+
} catch {
|
|
830
|
+
return null;
|
|
831
|
+
}
|
|
832
|
+
}
|
|
833
|
+
var DEFAULT_UNIVERSAL_ROUTER_GAS_FALLBACK = UNISWAP_UNIVERSAL_ROUTER_DEFAULT_GAS_UNITS;
|
|
834
|
+
function swapTransactionDeadlineUnixFromExpiryMinutes(expiryMinutes, nowSec = Math.floor(Date.now() / 1e3)) {
|
|
835
|
+
const m = Number.isFinite(expiryMinutes) && expiryMinutes > 0 ? expiryMinutes : UNISWAP_SWAP_DEFAULT_EXPIRY_MINUTES;
|
|
836
|
+
return nowSec + Math.floor(m * 60);
|
|
837
|
+
}
|
|
838
|
+
function swapDeadlineUnixSeconds(args) {
|
|
839
|
+
const now = Math.floor(Date.now() / 1e3);
|
|
840
|
+
if (args.swapTransactionDeadlineUnix != null && Number.isFinite(args.swapTransactionDeadlineUnix) && args.swapTransactionDeadlineUnix > now) {
|
|
841
|
+
return Math.floor(args.swapTransactionDeadlineUnix);
|
|
842
|
+
}
|
|
843
|
+
return swapTransactionDeadlineUnixFromExpiryMinutes(UNISWAP_SWAP_DEFAULT_EXPIRY_MINUTES, now);
|
|
844
|
+
}
|
|
845
|
+
function getClassicQuoteFromStoredUniswapResponse(stored) {
|
|
846
|
+
if (!stored) return null;
|
|
847
|
+
const q = stored.quote;
|
|
848
|
+
return q != null && typeof q === "object" ? q : null;
|
|
849
|
+
}
|
|
850
|
+
async function uniswapCreateSwap(args) {
|
|
851
|
+
const deadline = swapDeadlineUnixSeconds(args);
|
|
852
|
+
const useProxy = args.useServerProxy !== false && typeof globalThis !== "undefined" && typeof globalThis.window !== "undefined";
|
|
853
|
+
if (useProxy) {
|
|
854
|
+
const proxyPayload = {
|
|
855
|
+
uniswapApiKey: args.uniswapApiKey,
|
|
856
|
+
fullQuoteFromPermit: args.fullQuoteFromPermit,
|
|
857
|
+
universalRouterVersion: args.universalRouterVersion,
|
|
858
|
+
baseUrl: args.baseUrl,
|
|
859
|
+
swapTransactionDeadlineUnix: deadline
|
|
860
|
+
};
|
|
861
|
+
const res2 = await (args.fetchImpl ?? globalThis.fetch)("/api/uniswap/swap", {
|
|
862
|
+
method: "POST",
|
|
863
|
+
headers: { "Content-Type": "application/json" },
|
|
864
|
+
body: JSON.stringify(proxyPayload),
|
|
865
|
+
credentials: "same-origin"
|
|
866
|
+
});
|
|
867
|
+
const text2 = await res2.text();
|
|
868
|
+
if (!res2.ok) {
|
|
869
|
+
let errMsg = res2.statusText;
|
|
870
|
+
try {
|
|
871
|
+
const j = JSON.parse(text2);
|
|
872
|
+
if (j && typeof j.error === "string" && j.error.trim()) errMsg = j.error.trim();
|
|
873
|
+
} catch {
|
|
874
|
+
if (text2.trim()) errMsg = text2.slice(0, 500);
|
|
875
|
+
}
|
|
876
|
+
throw new Error(`Uniswap swap proxy failed: ${errMsg}`);
|
|
877
|
+
}
|
|
878
|
+
const parsed2 = JSON.parse(text2);
|
|
879
|
+
if (!parsed2?.swap || typeof parsed2.swap !== "object") {
|
|
880
|
+
throw new Error("Uniswap /swap (proxy): missing `swap` in response.");
|
|
881
|
+
}
|
|
882
|
+
return parsed2;
|
|
883
|
+
}
|
|
884
|
+
const base = (args.baseUrl ?? UNISWAP_TRADE_BASE_DEFAULT).replace(/\/$/, "");
|
|
885
|
+
const url = `${base}/swap`;
|
|
886
|
+
const classic = getClassicQuoteFromStoredUniswapResponse(args.fullQuoteFromPermit);
|
|
887
|
+
if (!classic) {
|
|
888
|
+
throw new Error("Stored Uniswap quote is missing a classic `quote` object; cannot build swap calldata.");
|
|
889
|
+
}
|
|
890
|
+
const body = {
|
|
891
|
+
quote: classic,
|
|
892
|
+
deadline
|
|
893
|
+
};
|
|
894
|
+
const urv = (args.universalRouterVersion ?? UNISWAP_UNIVERSAL_ROUTER_VERSION_DEFAULT).trim() || "2.0";
|
|
895
|
+
const fn = args.fetchImpl ?? globalThis.fetch;
|
|
896
|
+
const nativeIn = isUniswapFullQuoteResponseNativeIn(args.fullQuoteFromPermit);
|
|
897
|
+
const res = await fn(url, {
|
|
898
|
+
method: "POST",
|
|
899
|
+
headers: {
|
|
900
|
+
"Content-Type": "application/json",
|
|
901
|
+
"x-api-key": args.uniswapApiKey.trim(),
|
|
902
|
+
"x-universal-router-version": urv,
|
|
903
|
+
"x-erc20eth-enabled": nativeIn ? "true" : "false",
|
|
904
|
+
"x-permit2-disabled": "true"
|
|
905
|
+
},
|
|
906
|
+
body: JSON.stringify(body)
|
|
907
|
+
});
|
|
908
|
+
const text = await res.text();
|
|
909
|
+
if (!res.ok) {
|
|
910
|
+
throw new Error(
|
|
911
|
+
`Uniswap POST /swap failed: ${messageFromUniswapHttpResponseBody(text, res.status, res.statusText)}`
|
|
912
|
+
);
|
|
913
|
+
}
|
|
914
|
+
const parsed = JSON.parse(text);
|
|
915
|
+
if (!parsed?.swap || typeof parsed.swap !== "object") {
|
|
916
|
+
throw new Error("Uniswap /swap: missing `swap` in response.");
|
|
917
|
+
}
|
|
918
|
+
return parsed;
|
|
919
|
+
}
|
|
920
|
+
var permit2ApproveRouterAbi = [
|
|
921
|
+
{
|
|
922
|
+
name: "approve",
|
|
923
|
+
type: "function",
|
|
924
|
+
stateMutability: "nonpayable",
|
|
925
|
+
inputs: [
|
|
926
|
+
{ name: "token", type: "address" },
|
|
927
|
+
{ name: "spender", type: "address" },
|
|
928
|
+
{ name: "amount", type: "uint160" },
|
|
929
|
+
{ name: "expiration", type: "uint48" }
|
|
930
|
+
],
|
|
931
|
+
outputs: []
|
|
932
|
+
}
|
|
933
|
+
];
|
|
934
|
+
var uniswapDispatchExecuteAbi = [
|
|
935
|
+
{
|
|
936
|
+
name: "execute",
|
|
937
|
+
type: "function",
|
|
938
|
+
stateMutability: "payable",
|
|
939
|
+
inputs: [
|
|
940
|
+
{ name: "router", type: "address" },
|
|
941
|
+
{ name: "token", type: "address" },
|
|
942
|
+
{ name: "amount", type: "uint256" },
|
|
943
|
+
{ name: "commands", type: "bytes" },
|
|
944
|
+
{ name: "inputs", type: "bytes[]" },
|
|
945
|
+
{ name: "deadline", type: "uint256" }
|
|
946
|
+
],
|
|
947
|
+
outputs: []
|
|
948
|
+
}
|
|
949
|
+
];
|
|
950
|
+
function parseSwapTxValueWei(swap) {
|
|
951
|
+
const raw = swap.value ?? swap.Value;
|
|
952
|
+
if (raw == null || raw === "") return 0n;
|
|
953
|
+
if (typeof raw === "bigint") return raw > 0n ? raw : 0n;
|
|
954
|
+
if (typeof raw === "number" && Number.isFinite(raw)) {
|
|
955
|
+
try {
|
|
956
|
+
return BigInt(Math.max(0, Math.trunc(raw)));
|
|
957
|
+
} catch {
|
|
958
|
+
return 0n;
|
|
959
|
+
}
|
|
960
|
+
}
|
|
961
|
+
const s = String(raw).trim();
|
|
962
|
+
if (!s) return 0n;
|
|
963
|
+
try {
|
|
964
|
+
if (/^0x[0-9a-fA-F]+$/.test(s)) return BigInt(s);
|
|
965
|
+
return BigInt(s.replace(/^0x/i, ""));
|
|
966
|
+
} catch {
|
|
967
|
+
return 0n;
|
|
968
|
+
}
|
|
969
|
+
}
|
|
970
|
+
function nativeEthInPayableValueWei(args) {
|
|
971
|
+
const fromApi = parseSwapTxValueWei(args.swap);
|
|
972
|
+
if (fromApi > 0n) return fromApi;
|
|
973
|
+
try {
|
|
974
|
+
const d = decodeFunctionData({ abi: uniswapDispatchExecuteAbi, data: args.dataHex });
|
|
975
|
+
if (d.functionName === "execute") {
|
|
976
|
+
const rawAmt = d.args[2];
|
|
977
|
+
const amount = typeof rawAmt === "bigint" ? rawAmt : BigInt(String(rawAmt));
|
|
978
|
+
if (amount > 0n) return amount;
|
|
979
|
+
}
|
|
980
|
+
} catch {
|
|
981
|
+
}
|
|
982
|
+
return args.quoteInputWei > 0n ? args.quoteInputWei : 0n;
|
|
983
|
+
}
|
|
984
|
+
function applySlippagePercentToApproveWei(baseWei, slippagePercent) {
|
|
985
|
+
if (baseWei <= 0n) return baseWei;
|
|
986
|
+
const s = slippagePercent == null ? NaN : Number(slippagePercent);
|
|
987
|
+
if (!Number.isFinite(s) || s <= 0) return baseWei;
|
|
988
|
+
const bps = Math.min(1e5, Math.round(s * 100));
|
|
989
|
+
if (bps <= 0) return baseWei;
|
|
990
|
+
return (baseWei * BigInt(1e4 + bps) + 9999n) / 10000n;
|
|
991
|
+
}
|
|
992
|
+
function permit2SpenderAndApproveWeiFromSwapCalldata(dataHex, tokenIn, quoteInputWei, chainId) {
|
|
993
|
+
const canonical = getUniswapUniversalRouterSpenderOrThrow(chainId);
|
|
994
|
+
try {
|
|
995
|
+
const d = decodeFunctionData({ abi: uniswapDispatchExecuteAbi, data: dataHex });
|
|
996
|
+
if (d.functionName !== "execute") {
|
|
997
|
+
return { permit2Spender: canonical, approveWei: quoteInputWei, calldataAmountWei: null };
|
|
998
|
+
}
|
|
999
|
+
const router = getAddress(d.args[0]);
|
|
1000
|
+
const token = getAddress(d.args[1]);
|
|
1001
|
+
const rawAmt = d.args[2];
|
|
1002
|
+
const amount = typeof rawAmt === "bigint" ? rawAmt : BigInt(String(rawAmt));
|
|
1003
|
+
if (token.toLowerCase() !== tokenIn.toLowerCase()) {
|
|
1004
|
+
throw new Error(
|
|
1005
|
+
`Swap calldata token ${token} does not match token in ${tokenIn}. Refresh quote and /swap.`
|
|
1006
|
+
);
|
|
1007
|
+
}
|
|
1008
|
+
const calldataAmountWei = amount > 0n ? amount : null;
|
|
1009
|
+
let approveWei = quoteInputWei;
|
|
1010
|
+
if (calldataAmountWei != null && calldataAmountWei > approveWei) {
|
|
1011
|
+
approveWei = calldataAmountWei;
|
|
1012
|
+
}
|
|
1013
|
+
return { permit2Spender: router, approveWei, calldataAmountWei };
|
|
1014
|
+
} catch (e) {
|
|
1015
|
+
if (e instanceof Error && e.message.includes("does not match token")) throw e;
|
|
1016
|
+
return { permit2Spender: canonical, approveWei: quoteInputWei, calldataAmountWei: null };
|
|
1017
|
+
}
|
|
1018
|
+
}
|
|
1019
|
+
async function buildEvmMultisignBodyUniswapV4NativeInOnly(args, quoteInputWei) {
|
|
1020
|
+
const ph = (args.keyGen.pubkeyhex ?? "").trim();
|
|
1021
|
+
if (!ph) throw new Error("keyGen pubKey (pubkeyhex) is required");
|
|
1022
|
+
const keyList = args.keyGen.keylist ?? [];
|
|
1023
|
+
const clientId = firstClientIdFromKeyGen(args.keyGen);
|
|
1024
|
+
const toRouter = getAddress(
|
|
1025
|
+
(args.swap.to ?? "").trim().startsWith("0x") ? args.swap.to.trim() : `0x${args.swap.to.trim()}`
|
|
1026
|
+
);
|
|
1027
|
+
const dataHex = (() => {
|
|
1028
|
+
const d = (args.swap.data ?? "0x").toString().trim();
|
|
1029
|
+
return d.startsWith("0x") ? d : `0x${d}`;
|
|
1030
|
+
})();
|
|
1031
|
+
const valueWei = nativeEthInPayableValueWei({
|
|
1032
|
+
swap: args.swap,
|
|
1033
|
+
dataHex,
|
|
1034
|
+
quoteInputWei
|
|
1035
|
+
});
|
|
1036
|
+
if (valueWei <= 0n) {
|
|
1037
|
+
throw new Error(
|
|
1038
|
+
"Native (ETH) in swap: could not determine payable value (no `swap.value`, no `execute` amount in calldata, and quote input is zero). Refresh the quote and request /swap again."
|
|
1039
|
+
);
|
|
1040
|
+
}
|
|
1041
|
+
const ch = defineChain({
|
|
1042
|
+
id: args.chainId,
|
|
1043
|
+
name: "Destination",
|
|
1044
|
+
nativeCurrency: { decimals: 18, name: "Ether", symbol: "ETH" },
|
|
1045
|
+
rpcUrls: { default: { http: [args.rpcUrl] } }
|
|
1046
|
+
});
|
|
1047
|
+
const publicClient = createPublicClient({ chain: ch, transport: http(args.rpcUrl) });
|
|
1048
|
+
const feeParams = await fetchChainFeeParams(args.rpcUrl, args.chainId);
|
|
1049
|
+
const legacy = Boolean(args.chainDetail?.legacy) || !feeParams.isEip1559;
|
|
1050
|
+
const latestBaseFeeWeiNativeIn = !legacy ? (await publicClient.getBlock({ blockTag: "latest" })).baseFeePerGas ?? 0n : 0n;
|
|
1051
|
+
const useCustomGas = args.useCustomGas;
|
|
1052
|
+
const chainGasLimitRouter = args.chainDetail?.gasLimit != null && Number.isFinite(Number(args.chainDetail.gasLimit)) && Number(args.chainDetail.gasLimit) > 0 ? Number(args.chainDetail.gasLimit) : void 0;
|
|
1053
|
+
const gasFeeMultiplier = useCustomGas && args.chainDetail?.gasMultiplier != null ? Number(args.chainDetail.gasMultiplier) : void 0;
|
|
1054
|
+
const nonce = await publicClient.getTransactionCount({ address: args.executorAddress, blockTag: "pending" });
|
|
1055
|
+
const proposalTxParamsBatch = [];
|
|
1056
|
+
const messageHashes = [];
|
|
1057
|
+
const messageRawBatch = [];
|
|
1058
|
+
const swapRecord = args.swap;
|
|
1059
|
+
const fromTradeApi = parseOptionalGasLimitString(swapRecord.gasLimit) ?? parseOptionalGasLimitString(swapRecord.gas);
|
|
1060
|
+
let gasBuildSource = "rpcEstimate";
|
|
1061
|
+
let estimateGasError;
|
|
1062
|
+
let baseGasUnits1;
|
|
1063
|
+
if (fromTradeApi != null && fromTradeApi > 0n) {
|
|
1064
|
+
baseGasUnits1 = fromTradeApi;
|
|
1065
|
+
gasBuildSource = "tradeApi";
|
|
1066
|
+
} else {
|
|
1067
|
+
try {
|
|
1068
|
+
baseGasUnits1 = await publicClient.estimateGas({
|
|
1069
|
+
to: toRouter,
|
|
1070
|
+
data: dataHex,
|
|
1071
|
+
value: valueWei,
|
|
1072
|
+
account: args.executorAddress
|
|
1073
|
+
});
|
|
1074
|
+
gasBuildSource = "rpcEstimate";
|
|
1075
|
+
} catch (e) {
|
|
1076
|
+
estimateGasError = e instanceof Error ? e.message : String(e);
|
|
1077
|
+
const minRouterGas = 500000n;
|
|
1078
|
+
if (useCustomGas) {
|
|
1079
|
+
const cfg = args.chainDetail?.gasLimit != null ? parseOptionalGasLimitString(String(args.chainDetail.gasLimit)) : null;
|
|
1080
|
+
if (cfg != null && cfg >= minRouterGas) {
|
|
1081
|
+
baseGasUnits1 = cfg;
|
|
1082
|
+
} else {
|
|
1083
|
+
baseGasUnits1 = DEFAULT_UNIVERSAL_ROUTER_GAS_FALLBACK;
|
|
1084
|
+
}
|
|
1085
|
+
} else {
|
|
1086
|
+
baseGasUnits1 = DEFAULT_UNIVERSAL_ROUTER_GAS_FALLBACK;
|
|
1087
|
+
}
|
|
1088
|
+
gasBuildSource = "estimateFailedFallback";
|
|
1089
|
+
}
|
|
1090
|
+
}
|
|
1091
|
+
const gasLimit1 = routerSwapGasLimitFromEstimate(baseGasUnits1, chainGasLimitRouter);
|
|
1092
|
+
const currentNonce0 = nonce;
|
|
1093
|
+
let firstTxFeePayload = {};
|
|
1094
|
+
if (legacy) {
|
|
1095
|
+
let gasPriceWei1 = await publicClient.getGasPrice();
|
|
1096
|
+
if (useCustomGas && gasFeeMultiplier != null && gasFeeMultiplier > 0) {
|
|
1097
|
+
gasPriceWei1 = gasPriceWei1 * BigInt(100 + gasFeeMultiplier) / 100n;
|
|
1098
|
+
}
|
|
1099
|
+
if (useCustomGas && args.chainDetail?.gasPrice != null && args.chainDetail.gasPrice > 0) {
|
|
1100
|
+
const configured = parseGwei(gweiToDecimalString(Number(args.chainDetail.gasPrice)));
|
|
1101
|
+
if (configured > gasPriceWei1) gasPriceWei1 = configured;
|
|
1102
|
+
}
|
|
1103
|
+
firstTxFeePayload = { txNonce: nonce, txGasLimit: gasLimit1.toString(), txGasPrice: gasPriceWei1.toString() };
|
|
1104
|
+
const ser0 = serializeTransaction({
|
|
1105
|
+
type: "legacy",
|
|
1106
|
+
to: toRouter,
|
|
1107
|
+
data: dataHex,
|
|
1108
|
+
value: valueWei,
|
|
1109
|
+
gas: gasLimit1,
|
|
1110
|
+
gasPrice: gasPriceWei1,
|
|
1111
|
+
nonce: currentNonce0,
|
|
1112
|
+
chainId: args.chainId
|
|
1113
|
+
});
|
|
1114
|
+
const h0 = keccak256(ser0);
|
|
1115
|
+
messageHashes.push(h0.startsWith("0x") ? h0.slice(2) : h0);
|
|
1116
|
+
messageRawBatch.push(ser0);
|
|
1117
|
+
proposalTxParamsBatch.push({
|
|
1118
|
+
nonce: currentNonce0,
|
|
1119
|
+
gasLimit: gasLimit1.toString(),
|
|
1120
|
+
txType: "legacy",
|
|
1121
|
+
gasPrice: gasPriceWei1.toString()
|
|
1122
|
+
});
|
|
1123
|
+
} else {
|
|
1124
|
+
const fetchedBase1 = feeParams.baseFeeGwei ?? 0;
|
|
1125
|
+
const fetchedPriority1 = feeParams.priorityFeeGwei ?? 0;
|
|
1126
|
+
const configuredBase1 = useCustomGas && args.chainDetail?.baseFee != null ? Number(args.chainDetail.baseFee) : 0;
|
|
1127
|
+
const configuredPriority1 = useCustomGas && args.chainDetail?.priorityFee != null ? Number(args.chainDetail.priorityFee) : 0;
|
|
1128
|
+
const effectiveBaseFeeGwei1 = Math.max(fetchedBase1, configuredBase1);
|
|
1129
|
+
const effectivePriorityFeeGwei1 = Math.max(fetchedPriority1, configuredPriority1);
|
|
1130
|
+
const baseFeeMultiplierPct1 = useCustomGas && args.chainDetail?.baseFeeMultiplier != null ? Math.max(100, Number(args.chainDetail.baseFeeMultiplier)) : 100;
|
|
1131
|
+
const baseComponentGwei1 = effectiveBaseFeeGwei1 * baseFeeMultiplierPct1 / 100;
|
|
1132
|
+
const maxFeePerGasGwei1 = baseComponentGwei1 + effectivePriorityFeeGwei1;
|
|
1133
|
+
let maxPriorityFeePerGas1 = effectivePriorityFeeGwei1 > 0 ? parseGwei(gweiToDecimalString(effectivePriorityFeeGwei1)) : parseGwei("1");
|
|
1134
|
+
let maxFeePerGas1 = parseGwei(gweiToDecimalString(maxFeePerGasGwei1));
|
|
1135
|
+
if (useCustomGas && gasFeeMultiplier != null && gasFeeMultiplier > 0) {
|
|
1136
|
+
maxPriorityFeePerGas1 = maxPriorityFeePerGas1 * BigInt(100 + gasFeeMultiplier) / 100n;
|
|
1137
|
+
maxFeePerGas1 = maxFeePerGas1 * BigInt(100 + gasFeeMultiplier) / 100n;
|
|
1138
|
+
}
|
|
1139
|
+
({ maxFeePerGas: maxFeePerGas1, maxPriorityFeePerGas: maxPriorityFeePerGas1 } = alignEip1559FeesWithLatestBase(maxFeePerGas1, maxPriorityFeePerGas1, latestBaseFeeWeiNativeIn));
|
|
1140
|
+
firstTxFeePayload = {
|
|
1141
|
+
txNonce: nonce,
|
|
1142
|
+
txGasLimit: gasLimit1.toString(),
|
|
1143
|
+
txMaxFeePerGas: maxFeePerGas1.toString(),
|
|
1144
|
+
txMaxPriorityFeePerGas: maxPriorityFeePerGas1.toString()
|
|
1145
|
+
};
|
|
1146
|
+
const ser0 = serializeTransaction({
|
|
1147
|
+
type: "eip1559",
|
|
1148
|
+
to: toRouter,
|
|
1149
|
+
data: dataHex,
|
|
1150
|
+
value: valueWei,
|
|
1151
|
+
gas: gasLimit1,
|
|
1152
|
+
maxFeePerGas: maxFeePerGas1,
|
|
1153
|
+
maxPriorityFeePerGas: maxPriorityFeePerGas1,
|
|
1154
|
+
nonce: currentNonce0,
|
|
1155
|
+
chainId: args.chainId
|
|
1156
|
+
});
|
|
1157
|
+
const h0 = keccak256(ser0);
|
|
1158
|
+
messageHashes.push(h0.startsWith("0x") ? h0.slice(2) : h0);
|
|
1159
|
+
messageRawBatch.push(ser0);
|
|
1160
|
+
proposalTxParamsBatch.push({
|
|
1161
|
+
nonce: currentNonce0,
|
|
1162
|
+
gasLimit: gasLimit1.toString(),
|
|
1163
|
+
txType: "eip1559",
|
|
1164
|
+
maxFeePerGas: maxFeePerGas1.toString(),
|
|
1165
|
+
maxPriorityFeePerGas: maxPriorityFeePerGas1.toString()
|
|
1166
|
+
});
|
|
1167
|
+
}
|
|
1168
|
+
const dataNo0x = dataHex.startsWith("0x") ? dataHex.slice(2) : dataHex;
|
|
1169
|
+
const audit = {
|
|
1170
|
+
skipPermit2Batch: true,
|
|
1171
|
+
inputKind: "native_eth",
|
|
1172
|
+
noErc20Approve: true,
|
|
1173
|
+
quoteInputWei: quoteInputWei.toString(),
|
|
1174
|
+
swapValueWei: valueWei.toString(),
|
|
1175
|
+
uniswapCreateSwap: {
|
|
1176
|
+
requestId: args.createSwapResponse.requestId,
|
|
1177
|
+
gasFee: args.createSwapResponse.gasFee,
|
|
1178
|
+
gasBuildSwap: {
|
|
1179
|
+
useCustomGas,
|
|
1180
|
+
source: gasBuildSource,
|
|
1181
|
+
baseGasUnits: baseGasUnits1.toString(),
|
|
1182
|
+
...estimateGasError != null && estimateGasError !== "" ? { estimateGasError } : {}
|
|
1183
|
+
},
|
|
1184
|
+
swap: {
|
|
1185
|
+
to: args.createSwapResponse.swap.to,
|
|
1186
|
+
value: args.createSwapResponse.swap.value,
|
|
1187
|
+
dataNibbles: (() => {
|
|
1188
|
+
const t = (args.createSwapResponse.swap.data ?? "").toString().trim();
|
|
1189
|
+
return t.startsWith("0x") ? t.length - 2 : t.length;
|
|
1190
|
+
})()
|
|
1191
|
+
}
|
|
1192
|
+
},
|
|
1193
|
+
fullQuoteFromPermitSnapshot: args.fullQuoteSnapshot,
|
|
1194
|
+
originalPurpose: args.purposeText
|
|
1195
|
+
};
|
|
1196
|
+
const batchMeta = [
|
|
1197
|
+
{
|
|
1198
|
+
destinationAddress: toRouter,
|
|
1199
|
+
signatureText: JSON.stringify({
|
|
1200
|
+
kind: "UniswapV4",
|
|
1201
|
+
name: "UniversalRouter (payable, native in)",
|
|
1202
|
+
note: "Single tx from Trade POST /swap; no ERC-20 approve. Calldata in messageHashes[0] / messageRawBatch[0]."
|
|
1203
|
+
}),
|
|
1204
|
+
evm: { type: "uniswap_v4_swap_tx", version: 1, chainId: String(args.chainId) },
|
|
1205
|
+
uniswapV4: audit
|
|
1206
|
+
}
|
|
1207
|
+
];
|
|
1208
|
+
const extraPayload = { batchMeta };
|
|
1209
|
+
if (useCustomGas) {
|
|
1210
|
+
const snap = args.customGasChainDetails;
|
|
1211
|
+
if (snap && typeof snap === "object" && !Array.isArray(snap) && Object.keys(snap).length > 0) {
|
|
1212
|
+
extraPayload.customGasChainDetails = snap;
|
|
1213
|
+
}
|
|
1214
|
+
}
|
|
1215
|
+
const extraJSON = JSON.stringify(extraPayload);
|
|
1216
|
+
const firstSigText = batchMeta[0].signatureText;
|
|
1217
|
+
const bodyForSign = {
|
|
1218
|
+
keyList,
|
|
1219
|
+
pubKey: ph,
|
|
1220
|
+
msgHash: messageHashes[0],
|
|
1221
|
+
msgRaw: dataNo0x,
|
|
1222
|
+
messageHashes,
|
|
1223
|
+
messageRawBatch,
|
|
1224
|
+
destinationChainID: String(args.chainId),
|
|
1225
|
+
destinationAddress: toRouter,
|
|
1226
|
+
extraJSON,
|
|
1227
|
+
signatureText: firstSigText,
|
|
1228
|
+
purpose: (() => {
|
|
1229
|
+
const t = args.purposeText.trim();
|
|
1230
|
+
const batchDesc = "Uniswap V4: 1-tx batch \u2014 native gas token in (no ERC-20 approve) \u2014 single payable swap (Trade /swap).";
|
|
1231
|
+
return (t ? `${t}
|
|
1232
|
+
|
|
1233
|
+
` : "") + batchDesc;
|
|
1234
|
+
})(),
|
|
1235
|
+
...firstTxFeePayload,
|
|
1236
|
+
proposalTxParams: proposalTxParamsBatch
|
|
1237
|
+
};
|
|
1238
|
+
if (valueWei > 0n) {
|
|
1239
|
+
bodyForSign.value = valueWei.toString();
|
|
1240
|
+
}
|
|
1241
|
+
if (clientId) bodyForSign.clientId = clientId;
|
|
1242
|
+
return { bodyForSign, messageToSign: JSON.stringify(bodyForSign) };
|
|
1243
|
+
}
|
|
1244
|
+
async function buildEvmMultisignBodyUniswapV4SkipPermit2Batch(args) {
|
|
1245
|
+
const ph = (args.keyGen.pubkeyhex ?? "").trim();
|
|
1246
|
+
if (!ph) throw new Error("keyGen pubKey (pubkeyhex) is required");
|
|
1247
|
+
const keyList = args.keyGen.keylist ?? [];
|
|
1248
|
+
const clientId = firstClientIdFromKeyGen(args.keyGen);
|
|
1249
|
+
const tokenIn = getAddress(args.tokenIn);
|
|
1250
|
+
const parsedInOut = parseUniswapQuoteClassicInOut(args.fullQuoteSnapshot);
|
|
1251
|
+
const quoteInputWei = parsedInOut?.inputWei;
|
|
1252
|
+
if (quoteInputWei == null || quoteInputWei <= 0n) {
|
|
1253
|
+
throw new Error(
|
|
1254
|
+
"Could not read a positive input amount from the quote. Refresh the quote and ensure the Trade API returned classic quote.input.amount."
|
|
1255
|
+
);
|
|
1256
|
+
}
|
|
1257
|
+
if (tokenIn === zeroAddress) {
|
|
1258
|
+
return buildEvmMultisignBodyUniswapV4NativeInOnly(args, quoteInputWei);
|
|
1259
|
+
}
|
|
1260
|
+
const toRouter = getAddress(
|
|
1261
|
+
(args.swap.to ?? "").trim().startsWith("0x") ? args.swap.to.trim() : `0x${args.swap.to.trim()}`
|
|
1262
|
+
);
|
|
1263
|
+
const dataHex = (() => {
|
|
1264
|
+
const d = (args.swap.data ?? "0x").toString().trim();
|
|
1265
|
+
return d.startsWith("0x") ? d : `0x${d}`;
|
|
1266
|
+
})();
|
|
1267
|
+
const canonicalUniversalRouter = getUniswapUniversalRouterSpenderOrThrow(args.chainId);
|
|
1268
|
+
const { permit2Spender, approveWei: baseApproveWei, calldataAmountWei } = permit2SpenderAndApproveWeiFromSwapCalldata(
|
|
1269
|
+
dataHex,
|
|
1270
|
+
tokenIn,
|
|
1271
|
+
quoteInputWei,
|
|
1272
|
+
args.chainId
|
|
1273
|
+
);
|
|
1274
|
+
const approveAmountWei = applySlippagePercentToApproveWei(baseApproveWei, args.slippagePercent);
|
|
1275
|
+
const usePermit2Triple = toRouter.toLowerCase() === permit2Spender.toLowerCase();
|
|
1276
|
+
if (usePermit2Triple && approveAmountWei > MAX_UINT160) {
|
|
1277
|
+
throw new Error("Approved transfer amount exceeds allowance-hub uint160 max; reduce trade size.");
|
|
1278
|
+
}
|
|
1279
|
+
let expiration48 = 0n;
|
|
1280
|
+
if (usePermit2Triple) {
|
|
1281
|
+
const deadlineSec = Math.floor(Number(args.swapDeadlineUnix));
|
|
1282
|
+
if (!Number.isFinite(deadlineSec) || deadlineSec <= 0) {
|
|
1283
|
+
throw new Error("Invalid swapDeadlineUnix for allowance-hub approve expiration.");
|
|
1284
|
+
}
|
|
1285
|
+
expiration48 = BigInt(deadlineSec);
|
|
1286
|
+
if (expiration48 > MAX_UINT48) expiration48 = MAX_UINT48;
|
|
1287
|
+
}
|
|
1288
|
+
const erc20ApproveSpender = usePermit2Triple ? PERMIT2_ADDRESS : toRouter;
|
|
1289
|
+
const approveData = encodeFunctionData({
|
|
1290
|
+
abi: erc20Abi,
|
|
1291
|
+
functionName: "approve",
|
|
1292
|
+
args: [erc20ApproveSpender, approveAmountWei]
|
|
1293
|
+
});
|
|
1294
|
+
const valueWei = (() => {
|
|
1295
|
+
const v = args.swap.value;
|
|
1296
|
+
if (v == null || v === "") return 0n;
|
|
1297
|
+
try {
|
|
1298
|
+
return BigInt(String(v).replace(/^0x/i, ""));
|
|
1299
|
+
} catch {
|
|
1300
|
+
return 0n;
|
|
1301
|
+
}
|
|
1302
|
+
})();
|
|
1303
|
+
const ch = defineChain({
|
|
1304
|
+
id: args.chainId,
|
|
1305
|
+
name: "Destination",
|
|
1306
|
+
nativeCurrency: { decimals: 18, name: "Ether", symbol: "ETH" },
|
|
1307
|
+
rpcUrls: { default: { http: [args.rpcUrl] } }
|
|
1308
|
+
});
|
|
1309
|
+
const publicClient = createPublicClient({ chain: ch, transport: http(args.rpcUrl) });
|
|
1310
|
+
const feeParams = await fetchChainFeeParams(args.rpcUrl, args.chainId);
|
|
1311
|
+
const legacy = Boolean(args.chainDetail?.legacy) || !feeParams.isEip1559;
|
|
1312
|
+
const latestBaseFeeWeiSkipBatch = !legacy ? (await publicClient.getBlock({ blockTag: "latest" })).baseFeePerGas ?? 0n : 0n;
|
|
1313
|
+
const useCustomGas = args.useCustomGas;
|
|
1314
|
+
const gasLimitConfig = useCustomGas && args.chainDetail?.gasLimit != null ? Number(args.chainDetail.gasLimit) : void 0;
|
|
1315
|
+
const chainGasLimitRouter = args.chainDetail?.gasLimit != null && Number.isFinite(Number(args.chainDetail.gasLimit)) && Number(args.chainDetail.gasLimit) > 0 ? Number(args.chainDetail.gasLimit) : void 0;
|
|
1316
|
+
const gasFeeMultiplier = useCustomGas && args.chainDetail?.gasMultiplier != null ? Number(args.chainDetail.gasMultiplier) : void 0;
|
|
1317
|
+
const nonce = await publicClient.getTransactionCount({ address: args.executorAddress, blockTag: "pending" });
|
|
1318
|
+
const proposalTxParamsBatch = [];
|
|
1319
|
+
const messageHashes = [];
|
|
1320
|
+
const messageRawBatch = [];
|
|
1321
|
+
const approveMsgRawNo0x = approveData.startsWith("0x") ? approveData.slice(2) : approveData;
|
|
1322
|
+
let firstTxFeePayload = {};
|
|
1323
|
+
const approveGas = await publicClient.estimateGas({
|
|
1324
|
+
to: tokenIn,
|
|
1325
|
+
data: approveData,
|
|
1326
|
+
value: 0n,
|
|
1327
|
+
account: args.executorAddress
|
|
1328
|
+
});
|
|
1329
|
+
const gasLimit0 = useCustomGas ? gasLimitFromEstimateAndChainConfig(approveGas, gasLimitConfig) : approveGas;
|
|
1330
|
+
const currentNonce0 = nonce;
|
|
1331
|
+
if (legacy) {
|
|
1332
|
+
let gasPriceWei = await publicClient.getGasPrice();
|
|
1333
|
+
if (useCustomGas && gasFeeMultiplier != null && gasFeeMultiplier > 0) {
|
|
1334
|
+
gasPriceWei = gasPriceWei * BigInt(100 + gasFeeMultiplier) / 100n;
|
|
1335
|
+
}
|
|
1336
|
+
if (useCustomGas && args.chainDetail?.gasPrice != null && args.chainDetail.gasPrice > 0) {
|
|
1337
|
+
const configured = parseGwei(gweiToDecimalString(Number(args.chainDetail.gasPrice)));
|
|
1338
|
+
if (configured > gasPriceWei) gasPriceWei = configured;
|
|
1339
|
+
}
|
|
1340
|
+
firstTxFeePayload = { txNonce: nonce, txGasLimit: gasLimit0.toString(), txGasPrice: gasPriceWei.toString() };
|
|
1341
|
+
const ser0 = serializeTransaction({
|
|
1342
|
+
type: "legacy",
|
|
1343
|
+
to: tokenIn,
|
|
1344
|
+
data: approveData,
|
|
1345
|
+
value: 0n,
|
|
1346
|
+
gas: gasLimit0,
|
|
1347
|
+
gasPrice: gasPriceWei,
|
|
1348
|
+
nonce: currentNonce0,
|
|
1349
|
+
chainId: args.chainId
|
|
1350
|
+
});
|
|
1351
|
+
const h0 = keccak256(ser0);
|
|
1352
|
+
messageHashes.push(h0.startsWith("0x") ? h0.slice(2) : h0);
|
|
1353
|
+
messageRawBatch.push(ser0);
|
|
1354
|
+
proposalTxParamsBatch.push({
|
|
1355
|
+
nonce: currentNonce0,
|
|
1356
|
+
gasLimit: gasLimit0.toString(),
|
|
1357
|
+
txType: "legacy",
|
|
1358
|
+
gasPrice: gasPriceWei.toString()
|
|
1359
|
+
});
|
|
1360
|
+
} else {
|
|
1361
|
+
const fetchedBase = feeParams.baseFeeGwei ?? 0;
|
|
1362
|
+
const fetchedPriority = feeParams.priorityFeeGwei ?? 0;
|
|
1363
|
+
const configuredBase = useCustomGas && args.chainDetail?.baseFee != null ? Number(args.chainDetail.baseFee) : 0;
|
|
1364
|
+
const configuredPriority = useCustomGas && args.chainDetail?.priorityFee != null ? Number(args.chainDetail.priorityFee) : 0;
|
|
1365
|
+
const effectiveBaseFeeGwei = Math.max(fetchedBase, configuredBase);
|
|
1366
|
+
const effectivePriorityFeeGwei = Math.max(fetchedPriority, configuredPriority);
|
|
1367
|
+
const baseFeeMultiplierPct = useCustomGas && args.chainDetail?.baseFeeMultiplier != null ? Math.max(100, Number(args.chainDetail.baseFeeMultiplier)) : 100;
|
|
1368
|
+
const baseComponentGwei = effectiveBaseFeeGwei * baseFeeMultiplierPct / 100;
|
|
1369
|
+
const maxFeePerGasGwei = baseComponentGwei + effectivePriorityFeeGwei;
|
|
1370
|
+
let maxPriorityFeePerGas = effectivePriorityFeeGwei > 0 ? parseGwei(gweiToDecimalString(effectivePriorityFeeGwei)) : parseGwei("1");
|
|
1371
|
+
let maxFeePerGas = parseGwei(gweiToDecimalString(maxFeePerGasGwei));
|
|
1372
|
+
if (useCustomGas && gasFeeMultiplier != null && gasFeeMultiplier > 0) {
|
|
1373
|
+
maxPriorityFeePerGas = maxPriorityFeePerGas * BigInt(100 + gasFeeMultiplier) / 100n;
|
|
1374
|
+
maxFeePerGas = maxFeePerGas * BigInt(100 + gasFeeMultiplier) / 100n;
|
|
1375
|
+
}
|
|
1376
|
+
({ maxFeePerGas, maxPriorityFeePerGas } = alignEip1559FeesWithLatestBase(
|
|
1377
|
+
maxFeePerGas,
|
|
1378
|
+
maxPriorityFeePerGas,
|
|
1379
|
+
latestBaseFeeWeiSkipBatch
|
|
1380
|
+
));
|
|
1381
|
+
firstTxFeePayload = {
|
|
1382
|
+
txNonce: nonce,
|
|
1383
|
+
txGasLimit: gasLimit0.toString(),
|
|
1384
|
+
txMaxFeePerGas: maxFeePerGas.toString(),
|
|
1385
|
+
txMaxPriorityFeePerGas: maxPriorityFeePerGas.toString()
|
|
1386
|
+
};
|
|
1387
|
+
const ser0 = serializeTransaction({
|
|
1388
|
+
type: "eip1559",
|
|
1389
|
+
to: tokenIn,
|
|
1390
|
+
data: approveData,
|
|
1391
|
+
value: 0n,
|
|
1392
|
+
gas: gasLimit0,
|
|
1393
|
+
maxFeePerGas,
|
|
1394
|
+
maxPriorityFeePerGas,
|
|
1395
|
+
nonce: currentNonce0,
|
|
1396
|
+
chainId: args.chainId
|
|
1397
|
+
});
|
|
1398
|
+
const h0 = keccak256(ser0);
|
|
1399
|
+
messageHashes.push(h0.startsWith("0x") ? h0.slice(2) : h0);
|
|
1400
|
+
messageRawBatch.push(ser0);
|
|
1401
|
+
proposalTxParamsBatch.push({
|
|
1402
|
+
nonce: currentNonce0,
|
|
1403
|
+
gasLimit: gasLimit0.toString(),
|
|
1404
|
+
txType: "eip1559",
|
|
1405
|
+
maxFeePerGas: maxFeePerGas.toString(),
|
|
1406
|
+
maxPriorityFeePerGas: maxPriorityFeePerGas.toString()
|
|
1407
|
+
});
|
|
1408
|
+
}
|
|
1409
|
+
if (usePermit2Triple) {
|
|
1410
|
+
const permit2ApproveData = encodeFunctionData({
|
|
1411
|
+
abi: permit2ApproveRouterAbi,
|
|
1412
|
+
functionName: "approve",
|
|
1413
|
+
args: [tokenIn, permit2Spender, approveAmountWei, Number(expiration48)]
|
|
1414
|
+
});
|
|
1415
|
+
const approveP2Gas = await publicClient.estimateGas({
|
|
1416
|
+
to: PERMIT2_ADDRESS,
|
|
1417
|
+
data: permit2ApproveData,
|
|
1418
|
+
value: 0n,
|
|
1419
|
+
account: args.executorAddress
|
|
1420
|
+
});
|
|
1421
|
+
const gasLimitP2 = useCustomGas ? gasLimitFromEstimateAndChainConfig(approveP2Gas, gasLimitConfig) : approveP2Gas;
|
|
1422
|
+
const currentNonceP2 = nonce + 1;
|
|
1423
|
+
if (legacy) {
|
|
1424
|
+
let gasPriceWeiP2 = await publicClient.getGasPrice();
|
|
1425
|
+
if (useCustomGas && gasFeeMultiplier != null && gasFeeMultiplier > 0) {
|
|
1426
|
+
gasPriceWeiP2 = gasPriceWeiP2 * BigInt(100 + gasFeeMultiplier) / 100n;
|
|
1427
|
+
}
|
|
1428
|
+
if (useCustomGas && args.chainDetail?.gasPrice != null && args.chainDetail.gasPrice > 0) {
|
|
1429
|
+
const configured = parseGwei(gweiToDecimalString(Number(args.chainDetail.gasPrice)));
|
|
1430
|
+
if (configured > gasPriceWeiP2) gasPriceWeiP2 = configured;
|
|
1431
|
+
}
|
|
1432
|
+
const serP2 = serializeTransaction({
|
|
1433
|
+
type: "legacy",
|
|
1434
|
+
to: PERMIT2_ADDRESS,
|
|
1435
|
+
data: permit2ApproveData,
|
|
1436
|
+
value: 0n,
|
|
1437
|
+
gas: gasLimitP2,
|
|
1438
|
+
gasPrice: gasPriceWeiP2,
|
|
1439
|
+
nonce: currentNonceP2,
|
|
1440
|
+
chainId: args.chainId
|
|
1441
|
+
});
|
|
1442
|
+
const hP2 = keccak256(serP2);
|
|
1443
|
+
messageHashes.push(hP2.startsWith("0x") ? hP2.slice(2) : hP2);
|
|
1444
|
+
messageRawBatch.push(serP2);
|
|
1445
|
+
proposalTxParamsBatch.push({
|
|
1446
|
+
nonce: currentNonceP2,
|
|
1447
|
+
gasLimit: gasLimitP2.toString(),
|
|
1448
|
+
txType: "legacy",
|
|
1449
|
+
gasPrice: gasPriceWeiP2.toString()
|
|
1450
|
+
});
|
|
1451
|
+
} else {
|
|
1452
|
+
const fetchedBaseP2 = feeParams.baseFeeGwei ?? 0;
|
|
1453
|
+
const fetchedPriorityP2 = feeParams.priorityFeeGwei ?? 0;
|
|
1454
|
+
const configuredBaseP2 = useCustomGas && args.chainDetail?.baseFee != null ? Number(args.chainDetail.baseFee) : 0;
|
|
1455
|
+
const configuredPriorityP2 = useCustomGas && args.chainDetail?.priorityFee != null ? Number(args.chainDetail.priorityFee) : 0;
|
|
1456
|
+
const effectiveBaseFeeGweiP2 = Math.max(fetchedBaseP2, configuredBaseP2);
|
|
1457
|
+
const effectivePriorityFeeGweiP2 = Math.max(fetchedPriorityP2, configuredPriorityP2);
|
|
1458
|
+
const baseFeeMultiplierPctP2 = useCustomGas && args.chainDetail?.baseFeeMultiplier != null ? Math.max(100, Number(args.chainDetail.baseFeeMultiplier)) : 100;
|
|
1459
|
+
const baseComponentGweiP2 = effectiveBaseFeeGweiP2 * baseFeeMultiplierPctP2 / 100;
|
|
1460
|
+
const maxFeePerGasGweiP2 = baseComponentGweiP2 + effectivePriorityFeeGweiP2;
|
|
1461
|
+
let maxPriorityFeePerGasP2 = effectivePriorityFeeGweiP2 > 0 ? parseGwei(gweiToDecimalString(effectivePriorityFeeGweiP2)) : parseGwei("1");
|
|
1462
|
+
let maxFeePerGasP2 = parseGwei(gweiToDecimalString(maxFeePerGasGweiP2));
|
|
1463
|
+
if (useCustomGas && gasFeeMultiplier != null && gasFeeMultiplier > 0) {
|
|
1464
|
+
maxPriorityFeePerGasP2 = maxPriorityFeePerGasP2 * BigInt(100 + gasFeeMultiplier) / 100n;
|
|
1465
|
+
maxFeePerGasP2 = maxFeePerGasP2 * BigInt(100 + gasFeeMultiplier) / 100n;
|
|
1466
|
+
}
|
|
1467
|
+
({ maxFeePerGas: maxFeePerGasP2, maxPriorityFeePerGas: maxPriorityFeePerGasP2 } = alignEip1559FeesWithLatestBase(maxFeePerGasP2, maxPriorityFeePerGasP2, latestBaseFeeWeiSkipBatch));
|
|
1468
|
+
const serP2 = serializeTransaction({
|
|
1469
|
+
type: "eip1559",
|
|
1470
|
+
to: PERMIT2_ADDRESS,
|
|
1471
|
+
data: permit2ApproveData,
|
|
1472
|
+
value: 0n,
|
|
1473
|
+
gas: gasLimitP2,
|
|
1474
|
+
maxFeePerGas: maxFeePerGasP2,
|
|
1475
|
+
maxPriorityFeePerGas: maxPriorityFeePerGasP2,
|
|
1476
|
+
nonce: currentNonceP2,
|
|
1477
|
+
chainId: args.chainId
|
|
1478
|
+
});
|
|
1479
|
+
const hP2 = keccak256(serP2);
|
|
1480
|
+
messageHashes.push(hP2.startsWith("0x") ? hP2.slice(2) : hP2);
|
|
1481
|
+
messageRawBatch.push(serP2);
|
|
1482
|
+
proposalTxParamsBatch.push({
|
|
1483
|
+
nonce: currentNonceP2,
|
|
1484
|
+
gasLimit: gasLimitP2.toString(),
|
|
1485
|
+
txType: "eip1559",
|
|
1486
|
+
maxFeePerGas: maxFeePerGasP2.toString(),
|
|
1487
|
+
maxPriorityFeePerGas: maxPriorityFeePerGasP2.toString()
|
|
1488
|
+
});
|
|
1489
|
+
}
|
|
1490
|
+
}
|
|
1491
|
+
const swapRecord = args.swap;
|
|
1492
|
+
const fromTradeApi = parseOptionalGasLimitString(swapRecord.gasLimit) ?? parseOptionalGasLimitString(swapRecord.gas);
|
|
1493
|
+
let gasBuildSource = "rpcEstimate";
|
|
1494
|
+
let estimateGasError;
|
|
1495
|
+
let baseGasUnits1;
|
|
1496
|
+
if (fromTradeApi != null && fromTradeApi > 0n) {
|
|
1497
|
+
baseGasUnits1 = fromTradeApi;
|
|
1498
|
+
gasBuildSource = "tradeApi";
|
|
1499
|
+
} else {
|
|
1500
|
+
try {
|
|
1501
|
+
baseGasUnits1 = await publicClient.estimateGas({
|
|
1502
|
+
to: toRouter,
|
|
1503
|
+
data: dataHex,
|
|
1504
|
+
value: valueWei,
|
|
1505
|
+
account: args.executorAddress
|
|
1506
|
+
});
|
|
1507
|
+
gasBuildSource = "rpcEstimate";
|
|
1508
|
+
} catch (e) {
|
|
1509
|
+
estimateGasError = e instanceof Error ? e.message : String(e);
|
|
1510
|
+
const minRouterGas = 500000n;
|
|
1511
|
+
if (useCustomGas) {
|
|
1512
|
+
const cfg = args.chainDetail?.gasLimit != null ? parseOptionalGasLimitString(String(args.chainDetail.gasLimit)) : null;
|
|
1513
|
+
if (cfg != null && cfg >= minRouterGas) {
|
|
1514
|
+
baseGasUnits1 = cfg;
|
|
1515
|
+
} else {
|
|
1516
|
+
baseGasUnits1 = DEFAULT_UNIVERSAL_ROUTER_GAS_FALLBACK;
|
|
1517
|
+
}
|
|
1518
|
+
} else {
|
|
1519
|
+
baseGasUnits1 = DEFAULT_UNIVERSAL_ROUTER_GAS_FALLBACK;
|
|
1520
|
+
}
|
|
1521
|
+
gasBuildSource = "estimateFailedFallback";
|
|
1522
|
+
}
|
|
1523
|
+
}
|
|
1524
|
+
const gasLimit1 = routerSwapGasLimitFromEstimate(baseGasUnits1, chainGasLimitRouter);
|
|
1525
|
+
const currentNonce1 = nonce + (usePermit2Triple ? 2 : 1);
|
|
1526
|
+
if (legacy) {
|
|
1527
|
+
let gasPriceWei1 = await publicClient.getGasPrice();
|
|
1528
|
+
if (useCustomGas && gasFeeMultiplier != null && gasFeeMultiplier > 0) {
|
|
1529
|
+
gasPriceWei1 = gasPriceWei1 * BigInt(100 + gasFeeMultiplier) / 100n;
|
|
1530
|
+
}
|
|
1531
|
+
if (useCustomGas && args.chainDetail?.gasPrice != null && args.chainDetail.gasPrice > 0) {
|
|
1532
|
+
const configured = parseGwei(gweiToDecimalString(Number(args.chainDetail.gasPrice)));
|
|
1533
|
+
if (configured > gasPriceWei1) gasPriceWei1 = configured;
|
|
1534
|
+
}
|
|
1535
|
+
const ser1 = serializeTransaction({
|
|
1536
|
+
type: "legacy",
|
|
1537
|
+
to: toRouter,
|
|
1538
|
+
data: dataHex,
|
|
1539
|
+
value: valueWei,
|
|
1540
|
+
gas: gasLimit1,
|
|
1541
|
+
gasPrice: gasPriceWei1,
|
|
1542
|
+
nonce: currentNonce1,
|
|
1543
|
+
chainId: args.chainId
|
|
1544
|
+
});
|
|
1545
|
+
const h1 = keccak256(ser1);
|
|
1546
|
+
messageHashes.push(h1.startsWith("0x") ? h1.slice(2) : h1);
|
|
1547
|
+
messageRawBatch.push(ser1);
|
|
1548
|
+
proposalTxParamsBatch.push({
|
|
1549
|
+
nonce: currentNonce1,
|
|
1550
|
+
gasLimit: gasLimit1.toString(),
|
|
1551
|
+
txType: "legacy",
|
|
1552
|
+
gasPrice: gasPriceWei1.toString()
|
|
1553
|
+
});
|
|
1554
|
+
} else {
|
|
1555
|
+
const fetchedBase1 = feeParams.baseFeeGwei ?? 0;
|
|
1556
|
+
const fetchedPriority1 = feeParams.priorityFeeGwei ?? 0;
|
|
1557
|
+
const configuredBase1 = useCustomGas && args.chainDetail?.baseFee != null ? Number(args.chainDetail.baseFee) : 0;
|
|
1558
|
+
const configuredPriority1 = useCustomGas && args.chainDetail?.priorityFee != null ? Number(args.chainDetail.priorityFee) : 0;
|
|
1559
|
+
const effectiveBaseFeeGwei1 = Math.max(fetchedBase1, configuredBase1);
|
|
1560
|
+
const effectivePriorityFeeGwei1 = Math.max(fetchedPriority1, configuredPriority1);
|
|
1561
|
+
const baseFeeMultiplierPct1 = useCustomGas && args.chainDetail?.baseFeeMultiplier != null ? Math.max(100, Number(args.chainDetail.baseFeeMultiplier)) : 100;
|
|
1562
|
+
const baseComponentGwei1 = effectiveBaseFeeGwei1 * baseFeeMultiplierPct1 / 100;
|
|
1563
|
+
const maxFeePerGasGwei1 = baseComponentGwei1 + effectivePriorityFeeGwei1;
|
|
1564
|
+
let maxPriorityFeePerGas1 = effectivePriorityFeeGwei1 > 0 ? parseGwei(gweiToDecimalString(effectivePriorityFeeGwei1)) : parseGwei("1");
|
|
1565
|
+
let maxFeePerGas1 = parseGwei(gweiToDecimalString(maxFeePerGasGwei1));
|
|
1566
|
+
if (useCustomGas && gasFeeMultiplier != null && gasFeeMultiplier > 0) {
|
|
1567
|
+
maxPriorityFeePerGas1 = maxPriorityFeePerGas1 * BigInt(100 + gasFeeMultiplier) / 100n;
|
|
1568
|
+
maxFeePerGas1 = maxFeePerGas1 * BigInt(100 + gasFeeMultiplier) / 100n;
|
|
1569
|
+
}
|
|
1570
|
+
({ maxFeePerGas: maxFeePerGas1, maxPriorityFeePerGas: maxPriorityFeePerGas1 } = alignEip1559FeesWithLatestBase(maxFeePerGas1, maxPriorityFeePerGas1, latestBaseFeeWeiSkipBatch));
|
|
1571
|
+
const ser1 = serializeTransaction({
|
|
1572
|
+
type: "eip1559",
|
|
1573
|
+
to: toRouter,
|
|
1574
|
+
data: dataHex,
|
|
1575
|
+
value: valueWei,
|
|
1576
|
+
gas: gasLimit1,
|
|
1577
|
+
maxFeePerGas: maxFeePerGas1,
|
|
1578
|
+
maxPriorityFeePerGas: maxPriorityFeePerGas1,
|
|
1579
|
+
nonce: currentNonce1,
|
|
1580
|
+
chainId: args.chainId
|
|
1581
|
+
});
|
|
1582
|
+
const h1 = keccak256(ser1);
|
|
1583
|
+
messageHashes.push(h1.startsWith("0x") ? h1.slice(2) : h1);
|
|
1584
|
+
messageRawBatch.push(ser1);
|
|
1585
|
+
proposalTxParamsBatch.push({
|
|
1586
|
+
nonce: currentNonce1,
|
|
1587
|
+
gasLimit: gasLimit1.toString(),
|
|
1588
|
+
txType: "eip1559",
|
|
1589
|
+
maxFeePerGas: maxFeePerGas1.toString(),
|
|
1590
|
+
maxPriorityFeePerGas: maxPriorityFeePerGas1.toString()
|
|
1591
|
+
});
|
|
1592
|
+
}
|
|
1593
|
+
const swapMsgIndex = usePermit2Triple ? 2 : 1;
|
|
1594
|
+
const audit = {
|
|
1595
|
+
skipPermit2Batch: true,
|
|
1596
|
+
approvalPath: usePermit2Triple ? "permit2_triple" : "dispatcher",
|
|
1597
|
+
approveAmount: {
|
|
1598
|
+
baseWeiBeforeSlippage: baseApproveWei.toString(),
|
|
1599
|
+
finalWei: approveAmountWei.toString(),
|
|
1600
|
+
slippagePercent: args.slippagePercent,
|
|
1601
|
+
quoteInputWei: quoteInputWei.toString(),
|
|
1602
|
+
...calldataAmountWei != null ? { swapCalldataAmountWei: calldataAmountWei.toString() } : {}
|
|
1603
|
+
},
|
|
1604
|
+
uniswapCreateSwap: {
|
|
1605
|
+
requestId: args.createSwapResponse.requestId,
|
|
1606
|
+
gasFee: args.createSwapResponse.gasFee,
|
|
1607
|
+
gasBuildSwap: {
|
|
1608
|
+
useCustomGas,
|
|
1609
|
+
source: gasBuildSource,
|
|
1610
|
+
baseGasUnits: baseGasUnits1.toString(),
|
|
1611
|
+
...estimateGasError != null && estimateGasError !== "" ? { estimateGasError } : {}
|
|
1612
|
+
},
|
|
1613
|
+
swap: {
|
|
1614
|
+
to: args.createSwapResponse.swap.to,
|
|
1615
|
+
value: args.createSwapResponse.swap.value,
|
|
1616
|
+
dataNibbles: (() => {
|
|
1617
|
+
const d = args.createSwapResponse.swap.data ?? "";
|
|
1618
|
+
const t = d.toString().trim();
|
|
1619
|
+
return t.startsWith("0x") ? t.length - 2 : t.length;
|
|
1620
|
+
})()
|
|
1621
|
+
}
|
|
1622
|
+
},
|
|
1623
|
+
fullQuoteFromPermitSnapshot: args.fullQuoteSnapshot,
|
|
1624
|
+
originalPurpose: args.purposeText,
|
|
1625
|
+
innerRouterFromCalldata: permit2Spender,
|
|
1626
|
+
swapToEqualsInnerRouter: usePermit2Triple,
|
|
1627
|
+
...usePermit2Triple ? {
|
|
1628
|
+
permit2Erc20Approve: {
|
|
1629
|
+
token: tokenIn,
|
|
1630
|
+
spender: PERMIT2_ADDRESS,
|
|
1631
|
+
amountWei: approveAmountWei.toString(),
|
|
1632
|
+
note: "ERC-20 \u2192 allowance hub before hub approve(router)."
|
|
1633
|
+
},
|
|
1634
|
+
permit2ApproveUniversalRouter: {
|
|
1635
|
+
permit2: PERMIT2_ADDRESS,
|
|
1636
|
+
token: tokenIn,
|
|
1637
|
+
spender: permit2Spender,
|
|
1638
|
+
canonicalUniversalRouterFromAppMap: canonicalUniversalRouter,
|
|
1639
|
+
spenderMatchesCanonicalMap: permit2Spender.toLowerCase() === canonicalUniversalRouter.toLowerCase(),
|
|
1640
|
+
amountWei: approveAmountWei.toString(),
|
|
1641
|
+
expiration: expiration48.toString(),
|
|
1642
|
+
note: "Allowance-hub spender must match the router in swap calldata."
|
|
1643
|
+
}
|
|
1644
|
+
} : {
|
|
1645
|
+
erc20ApproveDispatcher: {
|
|
1646
|
+
token: tokenIn,
|
|
1647
|
+
spender: toRouter,
|
|
1648
|
+
amountWei: approveAmountWei.toString(),
|
|
1649
|
+
note: "Dispatcher pulls via ERC20.transferFrom(user, universalRouter, amount); allowance must be on swap.to (see cast trace TRANSFER_FROM_FAILED when only the hub is approved)."
|
|
1650
|
+
}
|
|
1651
|
+
}
|
|
1652
|
+
};
|
|
1653
|
+
const batchMeta = [
|
|
1654
|
+
{
|
|
1655
|
+
destinationAddress: tokenIn,
|
|
1656
|
+
signatureText: JSON.stringify({
|
|
1657
|
+
kind: "UniswapV4",
|
|
1658
|
+
name: "ERC20.approve",
|
|
1659
|
+
to: usePermit2Triple ? "allowance hub" : "dispatcher(swap.to)",
|
|
1660
|
+
function: "approve(address spender, uint256 amount)",
|
|
1661
|
+
spender: erc20ApproveSpender,
|
|
1662
|
+
amountWei: approveAmountWei.toString()
|
|
1663
|
+
}),
|
|
1664
|
+
evm: {
|
|
1665
|
+
type: usePermit2Triple ? "uniswap_v4_skip_permit2_approve" : "uniswap_v4_skip_permit2_dispatcher_approve",
|
|
1666
|
+
version: 1,
|
|
1667
|
+
chainId: String(args.chainId),
|
|
1668
|
+
...usePermit2Triple ? { permit2: PERMIT2_ADDRESS } : { dispatcher: toRouter }
|
|
1669
|
+
}
|
|
1670
|
+
}
|
|
1671
|
+
];
|
|
1672
|
+
if (usePermit2Triple) {
|
|
1673
|
+
batchMeta.push({
|
|
1674
|
+
destinationAddress: PERMIT2_ADDRESS,
|
|
1675
|
+
signatureText: JSON.stringify({
|
|
1676
|
+
kind: "UniswapV4",
|
|
1677
|
+
name: "AllowanceHub.approve",
|
|
1678
|
+
function: "approve(address token, address spender, uint160 amount, uint48 expiration)",
|
|
1679
|
+
token: tokenIn,
|
|
1680
|
+
spender: permit2Spender,
|
|
1681
|
+
amountWei: approveAmountWei.toString(),
|
|
1682
|
+
expiration: expiration48.toString()
|
|
1683
|
+
}),
|
|
1684
|
+
evm: {
|
|
1685
|
+
type: "uniswap_v4_skip_permit2_permit2_approve",
|
|
1686
|
+
version: 1,
|
|
1687
|
+
chainId: String(args.chainId),
|
|
1688
|
+
permit2: PERMIT2_ADDRESS,
|
|
1689
|
+
permit2Spender
|
|
1690
|
+
}
|
|
1691
|
+
});
|
|
1692
|
+
}
|
|
1693
|
+
batchMeta.push({
|
|
1694
|
+
destinationAddress: toRouter,
|
|
1695
|
+
signatureText: JSON.stringify({
|
|
1696
|
+
kind: "UniswapV4",
|
|
1697
|
+
name: "UniversalRouter.execute",
|
|
1698
|
+
note: `Calldata from Trade API POST /swap; signed tx hash in messageHashes[${swapMsgIndex}].`
|
|
1699
|
+
}),
|
|
1700
|
+
evm: { type: "uniswap_v4_swap_tx", version: 1, chainId: String(args.chainId) },
|
|
1701
|
+
uniswapV4: audit
|
|
1702
|
+
});
|
|
1703
|
+
const extraPayload = { batchMeta };
|
|
1704
|
+
if (useCustomGas) {
|
|
1705
|
+
const snap = args.customGasChainDetails;
|
|
1706
|
+
if (snap && typeof snap === "object" && !Array.isArray(snap) && Object.keys(snap).length > 0) {
|
|
1707
|
+
extraPayload.customGasChainDetails = snap;
|
|
1708
|
+
}
|
|
1709
|
+
}
|
|
1710
|
+
const extraJSON = JSON.stringify(extraPayload);
|
|
1711
|
+
const firstSigText = batchMeta[0].signatureText;
|
|
1712
|
+
const bodyForSign = {
|
|
1713
|
+
keyList,
|
|
1714
|
+
pubKey: ph,
|
|
1715
|
+
msgHash: messageHashes[0],
|
|
1716
|
+
msgRaw: approveMsgRawNo0x,
|
|
1717
|
+
messageHashes,
|
|
1718
|
+
messageRawBatch,
|
|
1719
|
+
destinationChainID: String(args.chainId),
|
|
1720
|
+
destinationAddress: tokenIn,
|
|
1721
|
+
extraJSON,
|
|
1722
|
+
signatureText: firstSigText,
|
|
1723
|
+
purpose: (() => {
|
|
1724
|
+
const t = args.purposeText.trim();
|
|
1725
|
+
const batchDesc = usePermit2Triple ? "Uniswap V4: 3-tx batch (classic allowance) \u2014 (1) ERC-20 approve allowance hub, (2) hub approve(Universal Router), (3) swap (Trade /swap)." : "Uniswap V4: 2-tx batch (classic allowance, dispatcher) \u2014 (1) ERC-20 approve swap.to (dispatcher pulls tokens), (2) swap (Trade /swap).";
|
|
1726
|
+
return (t ? `${t}
|
|
1727
|
+
|
|
1728
|
+
` : "") + batchDesc;
|
|
1729
|
+
})(),
|
|
1730
|
+
...firstTxFeePayload,
|
|
1731
|
+
proposalTxParams: proposalTxParamsBatch
|
|
1732
|
+
};
|
|
1733
|
+
if (valueWei > 0n) {
|
|
1734
|
+
bodyForSign.value = valueWei.toString();
|
|
1735
|
+
}
|
|
1736
|
+
if (clientId) bodyForSign.clientId = clientId;
|
|
1737
|
+
return { bodyForSign, messageToSign: JSON.stringify(bodyForSign) };
|
|
1738
|
+
}
|
|
1739
|
+
|
|
1740
|
+
// src/protocols/evm/uniswap-v4/swap.ts
|
|
1741
|
+
async function swapFromQuote(args) {
|
|
1742
|
+
const deadline = args.swapTransactionDeadlineUnix ?? swapTransactionDeadlineUnixFromExpiryMinutes(30);
|
|
1743
|
+
return buildEvmMultisignBodyUniswapV4SkipPermit2Batch({
|
|
1744
|
+
keyGen: args.keyGen,
|
|
1745
|
+
chainId: args.chainId,
|
|
1746
|
+
rpcUrl: args.rpcUrl,
|
|
1747
|
+
chainDetail: args.chainDetail,
|
|
1748
|
+
useCustomGas: args.useCustomGas,
|
|
1749
|
+
customGasChainDetails: args.customGasChainDetails,
|
|
1750
|
+
tokenIn: args.tokenIn,
|
|
1751
|
+
executorAddress: args.executorAddress,
|
|
1752
|
+
swap: args.swap.swap,
|
|
1753
|
+
createSwapResponse: args.swap,
|
|
1754
|
+
fullQuoteSnapshot: args.fullQuote,
|
|
1755
|
+
purposeText: args.purposeText,
|
|
1756
|
+
swapDeadlineUnix: deadline,
|
|
1757
|
+
slippagePercent: args.slippagePercent
|
|
1758
|
+
});
|
|
1759
|
+
}
|
|
1760
|
+
async function quoteSwap(args) {
|
|
1761
|
+
return uniswapTradeQuote(args);
|
|
1762
|
+
}
|
|
1763
|
+
async function createSwap(args) {
|
|
1764
|
+
return uniswapCreateSwap(args);
|
|
1765
|
+
}
|
|
1766
|
+
async function swapExactInput(args) {
|
|
1767
|
+
const swap = await uniswapCreateSwap({
|
|
1768
|
+
uniswapApiKey: args.uniswapApiKey,
|
|
1769
|
+
fullQuoteFromPermit: args.fullQuote,
|
|
1770
|
+
baseUrl: args.baseUrl,
|
|
1771
|
+
swapTransactionDeadlineUnix: args.swapTransactionDeadlineUnix,
|
|
1772
|
+
useServerProxy: false
|
|
1773
|
+
});
|
|
1774
|
+
return swapFromQuote({ ...args, swap });
|
|
1775
|
+
}
|
|
1776
|
+
|
|
1777
|
+
// src/protocols/evm/uniswap-v4/index.ts
|
|
1778
|
+
var UNISWAP_V4_PROTOCOL_ID = "uniswap-v4";
|
|
1779
|
+
var uniswapV4ProtocolModule = {
|
|
1780
|
+
id: UNISWAP_V4_PROTOCOL_ID,
|
|
1781
|
+
chainCategory: "evm",
|
|
1782
|
+
isChainSupported(ctx) {
|
|
1783
|
+
if (ctx.chainCategory !== "evm") return false;
|
|
1784
|
+
return isUniswapV4ChainSupported(ctx.chainId);
|
|
1785
|
+
},
|
|
1786
|
+
isTokenSupported(token) {
|
|
1787
|
+
if (token.category !== "evm") return false;
|
|
1788
|
+
return token.kind === "native" || token.kind === "erc20";
|
|
1789
|
+
},
|
|
1790
|
+
actions: [
|
|
1791
|
+
{
|
|
1792
|
+
id: "uniswap-v4.swap-exact-input",
|
|
1793
|
+
protocolId: UNISWAP_V4_PROTOCOL_ID,
|
|
1794
|
+
chainCategory: "evm",
|
|
1795
|
+
description: "Swap ERC-20 or native token via Uniswap V4 Universal Router (classic allowance path)",
|
|
1796
|
+
commonParams: ["keyGen", "purposeText", "useCustomGas"],
|
|
1797
|
+
params: {
|
|
1798
|
+
tokenIn: { type: "address", required: true, description: "Input token (0x0 for native)" },
|
|
1799
|
+
tokenOut: { type: "address", required: true, description: "Output token address" },
|
|
1800
|
+
amount: { type: "string", required: true, description: "Human or wei amount per trade type" },
|
|
1801
|
+
slippagePercent: { type: "number", required: true, description: "Slippage tolerance percent" },
|
|
1802
|
+
swapTransactionDeadlineUnix: {
|
|
1803
|
+
type: "number",
|
|
1804
|
+
required: false,
|
|
1805
|
+
description: "On-chain swap deadline (unix seconds)"
|
|
1806
|
+
},
|
|
1807
|
+
uniswapApiKey: { type: "string", required: true, description: "Uniswap Trade API key" }
|
|
1808
|
+
}
|
|
1809
|
+
},
|
|
1810
|
+
{
|
|
1811
|
+
id: "uniswap-v4.quote",
|
|
1812
|
+
protocolId: UNISWAP_V4_PROTOCOL_ID,
|
|
1813
|
+
chainCategory: "evm",
|
|
1814
|
+
description: "Fetch Uniswap Trade API quote",
|
|
1815
|
+
commonParams: ["keyGen"],
|
|
1816
|
+
params: {
|
|
1817
|
+
tokenIn: { type: "address", required: true, description: "Input token" },
|
|
1818
|
+
tokenOut: { type: "address", required: true, description: "Output token" },
|
|
1819
|
+
amount: { type: "string", required: true, description: "Amount for quote" },
|
|
1820
|
+
type: { type: "EXACT_INPUT | EXACT_OUTPUT", required: true, description: "Trade type" }
|
|
1821
|
+
}
|
|
1822
|
+
}
|
|
1823
|
+
]
|
|
1824
|
+
};
|
|
1825
|
+
registerProtocolModule(uniswapV4ProtocolModule);
|
|
1826
|
+
var uniswapV4 = {
|
|
1827
|
+
quoteSwap,
|
|
1828
|
+
createSwap,
|
|
1829
|
+
swapExactInput,
|
|
1830
|
+
swapFromQuote,
|
|
1831
|
+
buildSwapMultisignBody: buildEvmMultisignBodyUniswapV4SkipPermit2Batch,
|
|
1832
|
+
quote: uniswapTradeQuote,
|
|
1833
|
+
isChainSupported: isUniswapV4ChainSupported
|
|
1834
|
+
};
|
|
1835
|
+
|
|
1836
|
+
// src/protocols/evm/curve-dao/support.ts
|
|
1837
|
+
var CURVE_FULL_NETWORK_CONSTANTS_CHAIN_IDS = /* @__PURE__ */ new Set([
|
|
1838
|
+
1,
|
|
1839
|
+
10,
|
|
1840
|
+
56,
|
|
1841
|
+
100,
|
|
1842
|
+
137,
|
|
1843
|
+
146,
|
|
1844
|
+
196,
|
|
1845
|
+
250,
|
|
1846
|
+
252,
|
|
1847
|
+
324,
|
|
1848
|
+
999,
|
|
1849
|
+
1284,
|
|
1850
|
+
2222,
|
|
1851
|
+
5e3,
|
|
1852
|
+
8453,
|
|
1853
|
+
42161,
|
|
1854
|
+
42220,
|
|
1855
|
+
43114,
|
|
1856
|
+
1313161554
|
|
1857
|
+
]);
|
|
1858
|
+
function isCurveApiChainSupported(chainId) {
|
|
1859
|
+
const n = parseEvmChainIdToNumber(chainId);
|
|
1860
|
+
if (Number.isNaN(n) || n < 0) return false;
|
|
1861
|
+
return CURVE_FULL_NETWORK_CONSTANTS_CHAIN_IDS.has(n);
|
|
1862
|
+
}
|
|
1863
|
+
var CURVE_NATIVE_PLACEHOLDER = "0xeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee";
|
|
1864
|
+
var ETH_PLACEHOLDER = CURVE_NATIVE_PLACEHOLDER;
|
|
1865
|
+
function normalizeAddr(a) {
|
|
1866
|
+
return a.toLowerCase();
|
|
1867
|
+
}
|
|
1868
|
+
function toCurveRouterTokenId(tokenAddress, wrappedNative) {
|
|
1869
|
+
const t = (tokenAddress ?? "").trim().toLowerCase();
|
|
1870
|
+
if (t === ETH_PLACEHOLDER) return ETH_PLACEHOLDER;
|
|
1871
|
+
if (!isAddress((tokenAddress ?? "").trim())) return normalizeAddr((tokenAddress ?? "").trim());
|
|
1872
|
+
try {
|
|
1873
|
+
const a = getAddress((tokenAddress ?? "").trim());
|
|
1874
|
+
if (wrappedNative && a.toLowerCase() === wrappedNative.toLowerCase()) {
|
|
1875
|
+
return ETH_PLACEHOLDER;
|
|
1876
|
+
}
|
|
1877
|
+
return a.toLowerCase();
|
|
1878
|
+
} catch {
|
|
1879
|
+
return normalizeAddr((tokenAddress ?? "").trim());
|
|
1880
|
+
}
|
|
1881
|
+
}
|
|
1882
|
+
function addEdge(adj, a, b) {
|
|
1883
|
+
if (a === b) return;
|
|
1884
|
+
if (!adj.has(a)) adj.set(a, /* @__PURE__ */ new Set());
|
|
1885
|
+
if (!adj.has(b)) adj.set(b, /* @__PURE__ */ new Set());
|
|
1886
|
+
adj.get(a).add(b);
|
|
1887
|
+
adj.get(b).add(a);
|
|
1888
|
+
}
|
|
1889
|
+
function buildCurveLiquidityGraphFromApi(curve) {
|
|
1890
|
+
const adj = /* @__PURE__ */ new Map();
|
|
1891
|
+
for (const id of curve.getPoolList()) {
|
|
1892
|
+
try {
|
|
1893
|
+
const pool = curve.getPool(id);
|
|
1894
|
+
const w = (pool.wrappedCoinAddresses ?? []).map(normalizeAddr);
|
|
1895
|
+
const u = (pool.underlyingCoinAddresses ?? []).map(normalizeAddr);
|
|
1896
|
+
const all = [.../* @__PURE__ */ new Set([...w, ...u])];
|
|
1897
|
+
for (let i = 0; i < all.length; i++) {
|
|
1898
|
+
for (let j = i + 1; j < all.length; j++) {
|
|
1899
|
+
addEdge(adj, all[i], all[j]);
|
|
1900
|
+
}
|
|
1901
|
+
}
|
|
1902
|
+
} catch {
|
|
1903
|
+
}
|
|
1904
|
+
}
|
|
1905
|
+
return adj;
|
|
1906
|
+
}
|
|
1907
|
+
function addNativeWethBridge(adj, wrappedNative) {
|
|
1908
|
+
if (!wrappedNative) return;
|
|
1909
|
+
const w = wrappedNative.toLowerCase();
|
|
1910
|
+
const a = ETH_PLACEHOLDER;
|
|
1911
|
+
const b = w;
|
|
1912
|
+
if (a === b) return;
|
|
1913
|
+
if (!adj.has(a)) adj.set(a, /* @__PURE__ */ new Set());
|
|
1914
|
+
if (!adj.has(b)) adj.set(b, /* @__PURE__ */ new Set());
|
|
1915
|
+
adj.get(a).add(b);
|
|
1916
|
+
adj.get(b).add(a);
|
|
1917
|
+
}
|
|
1918
|
+
function swappableCurveGraphNodeKeys(adj) {
|
|
1919
|
+
const out = /* @__PURE__ */ new Set();
|
|
1920
|
+
for (const [k, neigh] of adj) {
|
|
1921
|
+
if (neigh.size > 0) out.add(k);
|
|
1922
|
+
}
|
|
1923
|
+
return out;
|
|
1924
|
+
}
|
|
1925
|
+
|
|
1926
|
+
// src/protocols/evm/curve-dao/apiSession.ts
|
|
1927
|
+
async function fetchAllCurvePools(curve) {
|
|
1928
|
+
const run = (p) => p.catch(() => void 0);
|
|
1929
|
+
await Promise.all([
|
|
1930
|
+
run(curve.factory.fetchPools()),
|
|
1931
|
+
run(curve.crvUSDFactory.fetchPools()),
|
|
1932
|
+
run(curve.EYWAFactory.fetchPools()),
|
|
1933
|
+
run(curve.cryptoFactory.fetchPools()),
|
|
1934
|
+
run(curve.twocryptoFactory.fetchPools()),
|
|
1935
|
+
run(curve.tricryptoFactory.fetchPools()),
|
|
1936
|
+
run(curve.stableNgFactory.fetchPools())
|
|
1937
|
+
]);
|
|
1938
|
+
}
|
|
1939
|
+
async function loadFullCurveSessionForRpc(rpcUrl) {
|
|
1940
|
+
const url = (rpcUrl ?? "").trim();
|
|
1941
|
+
if (!url) return null;
|
|
1942
|
+
try {
|
|
1943
|
+
const { default: curve } = await import('@curvefi/api');
|
|
1944
|
+
await curve.init("JsonRpc", { url }, {});
|
|
1945
|
+
const wrapped = curve.getNetworkConstants().NATIVE_COIN?.wrappedAddress;
|
|
1946
|
+
if (!curve.hasRouter || !curve.hasRouter()) {
|
|
1947
|
+
return {
|
|
1948
|
+
curve,
|
|
1949
|
+
adj: /* @__PURE__ */ new Map(),
|
|
1950
|
+
swappableNodeKeys: /* @__PURE__ */ new Set(),
|
|
1951
|
+
wrappedNative: wrapped
|
|
1952
|
+
};
|
|
1953
|
+
}
|
|
1954
|
+
await fetchAllCurvePools(curve);
|
|
1955
|
+
const adj = buildCurveLiquidityGraphFromApi(curve);
|
|
1956
|
+
addNativeWethBridge(adj, wrapped);
|
|
1957
|
+
return {
|
|
1958
|
+
curve,
|
|
1959
|
+
adj,
|
|
1960
|
+
swappableNodeKeys: swappableCurveGraphNodeKeys(adj),
|
|
1961
|
+
wrappedNative: wrapped
|
|
1962
|
+
};
|
|
1963
|
+
} catch {
|
|
1964
|
+
return null;
|
|
1965
|
+
}
|
|
1966
|
+
}
|
|
1967
|
+
|
|
1968
|
+
// src/protocols/evm/curve-dao/executeHelpers.ts
|
|
1969
|
+
var CURVE_ROUTER_EXCHANGE_DEFAULT_GAS_UNITS = 1200000n;
|
|
1970
|
+
var ETH_PLACEHOLDER2 = "0xeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee";
|
|
1971
|
+
function txToViemStep(tx) {
|
|
1972
|
+
if (!tx.to) throw new Error("Transaction missing `to`");
|
|
1973
|
+
const to = getAddress((tx.to ?? "").trim());
|
|
1974
|
+
const raw = (tx.data ?? "0x").toString();
|
|
1975
|
+
const data = raw.startsWith("0x") ? raw : `0x${raw}`;
|
|
1976
|
+
let value = 0n;
|
|
1977
|
+
if (tx.value != null) {
|
|
1978
|
+
if (typeof tx.value === "bigint") value = tx.value;
|
|
1979
|
+
else value = BigInt(String(tx.value));
|
|
1980
|
+
}
|
|
1981
|
+
return { to, data, value };
|
|
1982
|
+
}
|
|
1983
|
+
async function buildEvmMultisignBodyCurveDaoBatch(args) {
|
|
1984
|
+
const tokenIn = getAddress(args.tokenIn);
|
|
1985
|
+
const executor = getAddress(args.executorAddress);
|
|
1986
|
+
const session = await loadFullCurveSessionForRpc(args.rpcUrl);
|
|
1987
|
+
if (!session?.curve?.router?.populateSwap) {
|
|
1988
|
+
throw new Error("Curve router is not available (load session failed or chain has no router).");
|
|
1989
|
+
}
|
|
1990
|
+
const { curve, wrappedNative } = session;
|
|
1991
|
+
const wn = wrappedNative;
|
|
1992
|
+
const tokenInId = toCurveRouterTokenId(tokenIn, wn);
|
|
1993
|
+
const outTrim = (args.tokenOut ?? "").trim();
|
|
1994
|
+
const tokenOutId = toCurveRouterTokenId(
|
|
1995
|
+
outTrim.toLowerCase() === ETH_PLACEHOLDER2 ? ETH_PLACEHOLDER2 : getAddress(outTrim),
|
|
1996
|
+
wn
|
|
1997
|
+
);
|
|
1998
|
+
const isNativeIn = tokenInId.toLowerCase() === ETH_PLACEHOLDER2;
|
|
1999
|
+
const slip = args.slippagePercent;
|
|
2000
|
+
if (!Number.isFinite(slip) || slip <= 0 || slip >= 100) {
|
|
2001
|
+
throw new Error("Slippage must be between 0 and 100 (exclusive).");
|
|
2002
|
+
}
|
|
2003
|
+
const swapPop = await curve.router.populateSwap(tokenInId, tokenOutId, args.amountHuman, slip);
|
|
2004
|
+
const swapBase = txToViemStep(swapPop);
|
|
2005
|
+
const rawTo = swapPop?.to;
|
|
2006
|
+
if (rawTo == null || String(rawTo).trim() === "") {
|
|
2007
|
+
throw new Error("Curve populateSwap did not return a destination address (router).");
|
|
2008
|
+
}
|
|
2009
|
+
const routerAddr = getAddress(String(rawTo).trim());
|
|
2010
|
+
const ch = defineChain({
|
|
2011
|
+
id: args.chainId,
|
|
2012
|
+
name: "Destination",
|
|
2013
|
+
nativeCurrency: { decimals: 18, name: "Ether", symbol: "ETH" },
|
|
2014
|
+
rpcUrls: { default: { http: [args.rpcUrl] } }
|
|
2015
|
+
});
|
|
2016
|
+
const publicClient = createPublicClient({ chain: ch, transport: http(args.rpcUrl) });
|
|
2017
|
+
const erc20AllowanceAbi = parseAbi([
|
|
2018
|
+
"function allowance(address owner, address spender) view returns (uint256)",
|
|
2019
|
+
"function decimals() view returns (uint8)"
|
|
2020
|
+
]);
|
|
2021
|
+
const erc20ApproveAbi = parseAbi(["function approve(address spender, uint256 amount) returns (bool)"]);
|
|
2022
|
+
const steps = [];
|
|
2023
|
+
if (!isNativeIn) {
|
|
2024
|
+
const decimalsN = await publicClient.readContract({
|
|
2025
|
+
address: tokenIn,
|
|
2026
|
+
abi: erc20AllowanceAbi,
|
|
2027
|
+
functionName: "decimals"
|
|
2028
|
+
});
|
|
2029
|
+
const amountWei = parseUnits(args.amountHuman, Number(decimalsN));
|
|
2030
|
+
const currentAllowance = await publicClient.readContract({
|
|
2031
|
+
address: tokenIn,
|
|
2032
|
+
abi: erc20AllowanceAbi,
|
|
2033
|
+
functionName: "allowance",
|
|
2034
|
+
args: [executor, routerAddr]
|
|
2035
|
+
});
|
|
2036
|
+
if (currentAllowance < amountWei) {
|
|
2037
|
+
if (currentAllowance > 0n) {
|
|
2038
|
+
const dataReset = encodeFunctionData({
|
|
2039
|
+
abi: erc20ApproveAbi,
|
|
2040
|
+
functionName: "approve",
|
|
2041
|
+
args: [routerAddr, 0n]
|
|
2042
|
+
});
|
|
2043
|
+
steps.push({ to: tokenIn, data: dataReset, value: 0n, kind: "approve", fallbackGas: 80000n });
|
|
2044
|
+
}
|
|
2045
|
+
const dataApprove = encodeFunctionData({
|
|
2046
|
+
abi: erc20ApproveAbi,
|
|
2047
|
+
functionName: "approve",
|
|
2048
|
+
args: [routerAddr, amountWei]
|
|
2049
|
+
});
|
|
2050
|
+
steps.push({ to: tokenIn, data: dataApprove, value: 0n, kind: "approve", fallbackGas: 80000n });
|
|
2051
|
+
}
|
|
2052
|
+
}
|
|
2053
|
+
steps.push({
|
|
2054
|
+
...swapBase,
|
|
2055
|
+
kind: "exchange",
|
|
2056
|
+
routerSwap: true,
|
|
2057
|
+
fallbackGas: CURVE_ROUTER_EXCHANGE_DEFAULT_GAS_UNITS
|
|
2058
|
+
});
|
|
2059
|
+
const chainGasLimitRouter = args.chainDetail?.gasLimit != null && Number.isFinite(Number(args.chainDetail.gasLimit)) && Number(args.chainDetail.gasLimit) > 0 ? Number(args.chainDetail.gasLimit) : void 0;
|
|
2060
|
+
const gasLimitByIndex = /* @__PURE__ */ new Map();
|
|
2061
|
+
const n = steps.length;
|
|
2062
|
+
const purposeSuffix = n === 1 ? "Curve (DAO): 1-tx \u2014 CurveRouterNG.exchange (native in or allowance already set)." : `Curve (DAO): ${n}-tx batch \u2014 ERC-20 approve Curve router, then CurveRouterNG.exchange.`;
|
|
2063
|
+
const firstDataNo0x = steps[0].data.startsWith("0x") ? steps[0].data.slice(2) : steps[0].data;
|
|
2064
|
+
const payableValueWei = steps.filter((s) => s.kind === "exchange").reduce((a, s) => a + s.value, 0n);
|
|
2065
|
+
return buildEvmMultisignBatch({
|
|
2066
|
+
context: {
|
|
2067
|
+
chainCategory: "evm",
|
|
2068
|
+
keyGen: args.keyGen,
|
|
2069
|
+
purposeText: args.purposeText,
|
|
2070
|
+
chainId: args.chainId,
|
|
2071
|
+
rpcUrl: args.rpcUrl,
|
|
2072
|
+
executorAddress: executor,
|
|
2073
|
+
chainDetail: args.chainDetail,
|
|
2074
|
+
useCustomGas: args.useCustomGas,
|
|
2075
|
+
customGasChainDetails: args.customGasChainDetails
|
|
2076
|
+
},
|
|
2077
|
+
steps,
|
|
2078
|
+
purposeSuffix,
|
|
2079
|
+
firstMsgRawNo0x: firstDataNo0x,
|
|
2080
|
+
destinationAddress: steps[0].to,
|
|
2081
|
+
payableValueWei: payableValueWei > 0n ? payableValueWei : void 0,
|
|
2082
|
+
resolveGasLimit: async ({ step, index, estimatedGas }) => {
|
|
2083
|
+
const s = step;
|
|
2084
|
+
let gasLimitI;
|
|
2085
|
+
if (s.kind === "exchange") {
|
|
2086
|
+
gasLimitI = routerSwapGasLimitFromEstimate(estimatedGas, chainGasLimitRouter);
|
|
2087
|
+
} else {
|
|
2088
|
+
gasLimitI = estimatedGas;
|
|
2089
|
+
}
|
|
2090
|
+
gasLimitByIndex.set(index, gasLimitI);
|
|
2091
|
+
return gasLimitI;
|
|
2092
|
+
},
|
|
2093
|
+
buildBatchMeta: ({ step, index, gasLimit }) => {
|
|
2094
|
+
const s = step;
|
|
2095
|
+
const gl = gasLimitByIndex.get(index) ?? gasLimit;
|
|
2096
|
+
if (s.kind === "approve") {
|
|
2097
|
+
return {
|
|
2098
|
+
signatureText: JSON.stringify({
|
|
2099
|
+
kind: "CurveDao",
|
|
2100
|
+
name: "ERC20.approve",
|
|
2101
|
+
to: "Curve router",
|
|
2102
|
+
function: "approve(address spender, uint256 amount)",
|
|
2103
|
+
spender: routerAddr,
|
|
2104
|
+
amountHuman: args.amountHuman,
|
|
2105
|
+
note: "Allowance approved for this swap amount only (not unlimited)."
|
|
2106
|
+
}),
|
|
2107
|
+
evm: { type: "curve_dao_erc20_approve", version: 1, chainId: String(args.chainId) }
|
|
2108
|
+
};
|
|
2109
|
+
}
|
|
2110
|
+
return {
|
|
2111
|
+
signatureText: JSON.stringify({
|
|
2112
|
+
kind: "CurveDao",
|
|
2113
|
+
name: "CurveRouterNG.exchange",
|
|
2114
|
+
note: "Calldata from @curvefi/api `router.populateSwap` (same on-chain as router `exchange`)."
|
|
2115
|
+
}),
|
|
2116
|
+
evm: { type: "curve_dao_router_exchange", version: 1, chainId: String(args.chainId) },
|
|
2117
|
+
curveDao: {
|
|
2118
|
+
slippagePercent: slip,
|
|
2119
|
+
amountHuman: args.amountHuman,
|
|
2120
|
+
tokenIn: tokenInId,
|
|
2121
|
+
tokenOut: tokenOutId,
|
|
2122
|
+
isNativeIn,
|
|
2123
|
+
gasBuildExchange: { baseGasUnits: gl.toString() }
|
|
2124
|
+
}
|
|
2125
|
+
};
|
|
2126
|
+
}
|
|
2127
|
+
});
|
|
2128
|
+
}
|
|
2129
|
+
|
|
2130
|
+
// src/protocols/evm/curve-dao/index.ts
|
|
2131
|
+
var CURVE_DAO_PROTOCOL_ID = "curve-dao";
|
|
2132
|
+
var curveDaoProtocolModule = {
|
|
2133
|
+
id: CURVE_DAO_PROTOCOL_ID,
|
|
2134
|
+
chainCategory: "evm",
|
|
2135
|
+
isChainSupported(ctx) {
|
|
2136
|
+
if (ctx.chainCategory !== "evm") return false;
|
|
2137
|
+
return isCurveApiChainSupported(ctx.chainId);
|
|
2138
|
+
},
|
|
2139
|
+
isTokenSupported(token) {
|
|
2140
|
+
if (token.category !== "evm") return false;
|
|
2141
|
+
return token.kind === "native" || token.kind === "erc20";
|
|
2142
|
+
},
|
|
2143
|
+
actions: [
|
|
2144
|
+
{
|
|
2145
|
+
id: "curve-dao.swap",
|
|
2146
|
+
protocolId: CURVE_DAO_PROTOCOL_ID,
|
|
2147
|
+
chainCategory: "evm",
|
|
2148
|
+
description: "Swap via Curve Router NG (optional ERC-20 approve batch)",
|
|
2149
|
+
commonParams: ["keyGen", "purposeText", "useCustomGas"],
|
|
2150
|
+
params: {
|
|
2151
|
+
tokenIn: { type: "address", required: true, description: "ERC-20 token in (native uses WETH path in UI)" },
|
|
2152
|
+
tokenOut: { type: "address", required: true, description: "Output token or 0xeeee\u2026 native placeholder" },
|
|
2153
|
+
amountHuman: { type: "string", required: true, description: "Human-readable input amount" },
|
|
2154
|
+
slippagePercent: { type: "number", required: true, description: "Slippage percent (0\u2013100 exclusive)" }
|
|
2155
|
+
}
|
|
2156
|
+
}
|
|
2157
|
+
]
|
|
2158
|
+
};
|
|
2159
|
+
registerProtocolModule(curveDaoProtocolModule);
|
|
2160
|
+
var curveDao = {
|
|
2161
|
+
buildSwapMultisignBody: buildEvmMultisignBodyCurveDaoBatch,
|
|
2162
|
+
isChainSupported: isCurveApiChainSupported,
|
|
2163
|
+
loadSession: loadFullCurveSessionForRpc
|
|
2164
|
+
};
|
|
2165
|
+
|
|
2166
|
+
// src/agent/commonParamDocs.ts
|
|
2167
|
+
var EVM_COMMON_PARAM_DOCS = {
|
|
2168
|
+
keyGen: {
|
|
2169
|
+
type: "object",
|
|
2170
|
+
required: true,
|
|
2171
|
+
description: "MPC key slice: { pubkeyhex: string (required), keylist: string[], ClientKeys?: Record<string,string> }. Used for pubKey/keyList on POST /multiSignRequest."
|
|
2172
|
+
},
|
|
2173
|
+
purposeText: {
|
|
2174
|
+
type: "string",
|
|
2175
|
+
required: true,
|
|
2176
|
+
description: "Human-readable purpose for the sign request. Stored in bodyForSign.purpose (may be appended with an automatic batch suffix)."
|
|
2177
|
+
},
|
|
2178
|
+
useCustomGas: {
|
|
2179
|
+
type: "boolean",
|
|
2180
|
+
required: true,
|
|
2181
|
+
description: "When true, apply chain gas settings from chainDetail / customGasChainDetails instead of raw RPC estimates only."
|
|
2182
|
+
},
|
|
2183
|
+
chainId: {
|
|
2184
|
+
type: "number",
|
|
2185
|
+
required: true,
|
|
2186
|
+
description: "EVM chain id (decimal). Becomes destinationChainID on the sign request."
|
|
2187
|
+
},
|
|
2188
|
+
rpcUrl: {
|
|
2189
|
+
type: "string",
|
|
2190
|
+
required: true,
|
|
2191
|
+
description: "HTTPS JSON-RPC URL for gas estimation, nonce, and allowance reads."
|
|
2192
|
+
},
|
|
2193
|
+
executorAddress: {
|
|
2194
|
+
type: "address",
|
|
2195
|
+
required: true,
|
|
2196
|
+
description: "MPC wallet address (from keyGen ethereumaddress) \u2014 tx sender for estimates and approvals."
|
|
2197
|
+
},
|
|
2198
|
+
chainDetail: {
|
|
2199
|
+
type: "object",
|
|
2200
|
+
required: true,
|
|
2201
|
+
description: "Optional gas config: { legacy?, gasLimit?, gasMultiplier?, gasPrice?, baseFee?, priorityFee?, baseFeeMultiplier? }."
|
|
2202
|
+
},
|
|
2203
|
+
customGasChainDetails: {
|
|
2204
|
+
type: "object",
|
|
2205
|
+
required: false,
|
|
2206
|
+
description: "Snapshot written to extraJSON.customGasChainDetails when useCustomGas is true."
|
|
2207
|
+
}
|
|
2208
|
+
};
|
|
2209
|
+
var MULTISIGN_OUTPUT_DOC = {
|
|
2210
|
+
description: "Unsigned mpc-auth multiSignRequest payload. The caller must sign messageToSign (MetaMask personal_sign or Ed25519) and POST { ...bodyForSign, clientSig, signedMessage: messageToSign } to /multiSignRequest.",
|
|
2211
|
+
fields: {
|
|
2212
|
+
bodyForSign: {
|
|
2213
|
+
type: "object",
|
|
2214
|
+
description: "POST body fields without clientSig: keyList, pubKey, msgHash, msgRaw, destinationChainID, purpose, extraJSON, proposalTxParams (batch), messageHashes/messageRawBatch when N>1 txs."
|
|
2215
|
+
},
|
|
2216
|
+
messageToSign: {
|
|
2217
|
+
type: "string",
|
|
2218
|
+
description: "JSON.stringify(bodyForSign) \u2014 exact string to sign before adding clientSig."
|
|
2219
|
+
}
|
|
2220
|
+
}
|
|
2221
|
+
};
|
|
2222
|
+
|
|
2223
|
+
// src/agent/mcpTools.ts
|
|
2224
|
+
function paramProperties(params, includeCommon = []) {
|
|
2225
|
+
const out = {};
|
|
2226
|
+
for (const k of includeCommon) {
|
|
2227
|
+
const d = EVM_COMMON_PARAM_DOCS[k];
|
|
2228
|
+
if (d) out[k] = { type: d.type, description: d.description };
|
|
2229
|
+
}
|
|
2230
|
+
for (const [k, d] of Object.entries(params)) {
|
|
2231
|
+
out[k] = { type: d.type, description: d.description };
|
|
2232
|
+
}
|
|
2233
|
+
return out;
|
|
2234
|
+
}
|
|
2235
|
+
function requiredKeys(params, includeCommon = []) {
|
|
2236
|
+
const req = [];
|
|
2237
|
+
for (const k of includeCommon) {
|
|
2238
|
+
if (EVM_COMMON_PARAM_DOCS[k]?.required) req.push(k);
|
|
2239
|
+
}
|
|
2240
|
+
for (const [k, d] of Object.entries(params)) {
|
|
2241
|
+
if (d.required) req.push(k);
|
|
2242
|
+
}
|
|
2243
|
+
return req;
|
|
2244
|
+
}
|
|
2245
|
+
var multisignOutputSchema = {
|
|
2246
|
+
type: "object",
|
|
2247
|
+
description: MULTISIGN_OUTPUT_DOC.description,
|
|
2248
|
+
properties: {
|
|
2249
|
+
bodyForSign: {
|
|
2250
|
+
type: "object",
|
|
2251
|
+
description: MULTISIGN_OUTPUT_DOC.fields.bodyForSign.description
|
|
2252
|
+
},
|
|
2253
|
+
messageToSign: {
|
|
2254
|
+
type: "string",
|
|
2255
|
+
description: MULTISIGN_OUTPUT_DOC.fields.messageToSign.description
|
|
2256
|
+
}
|
|
2257
|
+
},
|
|
2258
|
+
required: ["bodyForSign", "messageToSign"]
|
|
2259
|
+
};
|
|
2260
|
+
var MCP_TOOL_DEFINITIONS = [
|
|
2261
|
+
{
|
|
2262
|
+
name: "ctm_uniswap_v4_quote",
|
|
2263
|
+
actionId: "uniswap-v4.quote",
|
|
2264
|
+
protocolId: "uniswap-v4",
|
|
2265
|
+
chainCategory: "evm",
|
|
2266
|
+
description: "Fetch a Uniswap V4 Trade API quote (POST /v1/quote). Returns classic quote JSON including quote.input/output amounts and routing. Does NOT create a sign request \u2014 use ctm_uniswap_v4_create_swap and ctm_uniswap_v4_build_swap_multisign after quoting. Requires uniswapApiKey and swapper (MPC executor address) or keyGen + managementNodeUrl to resolve swapper.",
|
|
2267
|
+
prerequisites: ["Chain must be supported by Uniswap V4 (Universal Router map)."],
|
|
2268
|
+
followUp: ["ctm_uniswap_v4_create_swap", "ctm_uniswap_v4_build_swap_multisign"],
|
|
2269
|
+
handler: { importPath: "protocols/evm/uniswap-v4", exportName: "uniswapTradeQuote" },
|
|
2270
|
+
inputSchema: {
|
|
2271
|
+
type: "object",
|
|
2272
|
+
properties: paramProperties({
|
|
2273
|
+
type: { type: "string", required: true, description: "EXACT_INPUT or EXACT_OUTPUT" },
|
|
2274
|
+
amount: { type: "string", required: true, description: "Amount in token-in base units (wei string for ERC-20)" },
|
|
2275
|
+
tokenIn: { type: "address", required: true, description: "Input token; 0x0 for native ETH" },
|
|
2276
|
+
tokenOut: { type: "address", required: true, description: "Output token address" },
|
|
2277
|
+
chainId: { type: "number", required: true, description: "tokenInChainId / same-chain default" },
|
|
2278
|
+
uniswapApiKey: { type: "string", required: true, description: "Uniswap Trade API x-api-key" },
|
|
2279
|
+
swapper: { type: "address", required: false, description: "MPC executor; omit if keyGen + managementNodeUrl provided" },
|
|
2280
|
+
slippage: { type: "number", required: false, description: "Slippage percent; omit for API auto slippage" }
|
|
2281
|
+
}),
|
|
2282
|
+
required: requiredKeys({
|
|
2283
|
+
type: { type: "string", required: true, description: "" },
|
|
2284
|
+
amount: { type: "string", required: true, description: "" },
|
|
2285
|
+
tokenIn: { type: "address", required: true, description: "" },
|
|
2286
|
+
tokenOut: { type: "address", required: true, description: "" },
|
|
2287
|
+
chainId: { type: "number", required: true, description: "" },
|
|
2288
|
+
uniswapApiKey: { type: "string", required: true, description: "" }
|
|
2289
|
+
})
|
|
2290
|
+
},
|
|
2291
|
+
outputSchema: {
|
|
2292
|
+
type: "object",
|
|
2293
|
+
description: "Full Uniswap POST /quote JSON (includes nested quote object with input/output amounts)."
|
|
2294
|
+
}
|
|
2295
|
+
},
|
|
2296
|
+
{
|
|
2297
|
+
name: "ctm_uniswap_v4_create_swap",
|
|
2298
|
+
actionId: "uniswap-v4.create-swap",
|
|
2299
|
+
protocolId: "uniswap-v4",
|
|
2300
|
+
chainCategory: "evm",
|
|
2301
|
+
description: "Call Uniswap Trade API POST /v1/swap to build Universal Router calldata from a prior quote. Returns { swap: { to, data, value, gasLimit? }, requestId? }. Does NOT produce a multiSignRequest \u2014 call ctm_uniswap_v4_build_swap_multisign next.",
|
|
2302
|
+
prerequisites: ["ctm_uniswap_v4_quote output (fullQuoteFromPermit)"],
|
|
2303
|
+
followUp: ["ctm_uniswap_v4_build_swap_multisign"],
|
|
2304
|
+
handler: { importPath: "protocols/evm/uniswap-v4", exportName: "uniswapCreateSwap" },
|
|
2305
|
+
inputSchema: {
|
|
2306
|
+
type: "object",
|
|
2307
|
+
properties: paramProperties({
|
|
2308
|
+
uniswapApiKey: { type: "string", required: true, description: "Uniswap Trade API key" },
|
|
2309
|
+
fullQuoteFromPermit: { type: "object", required: true, description: "Full quote JSON from ctm_uniswap_v4_quote" },
|
|
2310
|
+
swapTransactionDeadlineUnix: {
|
|
2311
|
+
type: "number",
|
|
2312
|
+
required: false,
|
|
2313
|
+
description: "On-chain deadline unix seconds; default ~30 min from now"
|
|
2314
|
+
},
|
|
2315
|
+
useServerProxy: {
|
|
2316
|
+
type: "boolean",
|
|
2317
|
+
required: false,
|
|
2318
|
+
description: "Set false in Node/agents; true only in browser via Next API route"
|
|
2319
|
+
}
|
|
2320
|
+
}),
|
|
2321
|
+
required: requiredKeys({
|
|
2322
|
+
uniswapApiKey: { type: "string", required: true, description: "" },
|
|
2323
|
+
fullQuoteFromPermit: { type: "object", required: true, description: "" }
|
|
2324
|
+
})
|
|
2325
|
+
},
|
|
2326
|
+
outputSchema: {
|
|
2327
|
+
type: "object",
|
|
2328
|
+
description: "{ swap: TransactionRequest, requestId?, gasFee? }"
|
|
2329
|
+
}
|
|
2330
|
+
},
|
|
2331
|
+
{
|
|
2332
|
+
name: "ctm_uniswap_v4_build_swap_multisign",
|
|
2333
|
+
actionId: "uniswap-v4.swap-exact-input",
|
|
2334
|
+
protocolId: "uniswap-v4",
|
|
2335
|
+
chainCategory: "evm",
|
|
2336
|
+
description: "Build mpc-auth multiSignRequest body for a Uniswap V4 swap. May batch 1\u20133 EVM txs: ERC-20 approve(s) to Permit2/router path + Universal Router swap (or 1 tx for native-in). Estimates gas, serializes unsigned txs, sets proposalTxParams. Output must be signed and POSTed to /multiSignRequest by the caller.",
|
|
2337
|
+
prerequisites: [
|
|
2338
|
+
"ctm_uniswap_v4_create_swap output",
|
|
2339
|
+
"keyGen with pubkeyhex",
|
|
2340
|
+
"executorAddress matching MPC wallet",
|
|
2341
|
+
"RPC URL and chainDetail from node chain config"
|
|
2342
|
+
],
|
|
2343
|
+
followUp: ["Sign messageToSign", "POST /multiSignRequest with clientSig"],
|
|
2344
|
+
handler: { importPath: "protocols/evm/uniswap-v4", exportName: "buildEvmMultisignBodyUniswapV4SkipPermit2Batch" },
|
|
2345
|
+
inputSchema: {
|
|
2346
|
+
type: "object",
|
|
2347
|
+
properties: paramProperties(
|
|
2348
|
+
{
|
|
2349
|
+
tokenIn: { type: "address", required: true, description: "Token in; 0x0 for native ETH" },
|
|
2350
|
+
swap: { type: "object", required: true, description: "swap field from create_swap response" },
|
|
2351
|
+
createSwapResponse: { type: "object", required: true, description: "Full create_swap response" },
|
|
2352
|
+
fullQuoteSnapshot: { type: "object", required: true, description: "Quote JSON used for the swap" },
|
|
2353
|
+
swapDeadlineUnix: { type: "number", required: true, description: "Same deadline passed to create_swap" },
|
|
2354
|
+
slippagePercent: { type: "number", required: false, description: "Extra approve headroom for EXACT_OUTPUT" }
|
|
2355
|
+
},
|
|
2356
|
+
["keyGen", "purposeText", "useCustomGas", "chainId", "rpcUrl", "executorAddress", "chainDetail", "customGasChainDetails"]
|
|
2357
|
+
),
|
|
2358
|
+
required: requiredKeys(
|
|
2359
|
+
{
|
|
2360
|
+
tokenIn: { type: "address", required: true, description: "" },
|
|
2361
|
+
swap: { type: "object", required: true, description: "" },
|
|
2362
|
+
createSwapResponse: { type: "object", required: true, description: "" },
|
|
2363
|
+
fullQuoteSnapshot: { type: "object", required: true, description: "" },
|
|
2364
|
+
swapDeadlineUnix: { type: "number", required: true, description: "" }
|
|
2365
|
+
},
|
|
2366
|
+
["keyGen", "purposeText", "useCustomGas", "chainId", "rpcUrl", "executorAddress", "chainDetail"]
|
|
2367
|
+
)
|
|
2368
|
+
},
|
|
2369
|
+
outputSchema: multisignOutputSchema
|
|
2370
|
+
},
|
|
2371
|
+
{
|
|
2372
|
+
name: "ctm_curve_dao_build_swap_multisign",
|
|
2373
|
+
actionId: "curve-dao.swap",
|
|
2374
|
+
protocolId: "curve-dao",
|
|
2375
|
+
chainCategory: "evm",
|
|
2376
|
+
description: "Build mpc-auth multiSignRequest for a Curve Router NG swap via @curvefi/api populateSwap. Optionally batches ERC-20 approve txs when allowance is insufficient, then exchange. Requires JSON-RPC and Curve-supported chain.",
|
|
2377
|
+
prerequisites: ["keyGen", "executorAddress", "tokenIn/tokenOut/amountHuman/slippage", "RPC URL"],
|
|
2378
|
+
followUp: ["Sign messageToSign", "POST /multiSignRequest"],
|
|
2379
|
+
handler: { importPath: "protocols/evm/curve-dao", exportName: "buildEvmMultisignBodyCurveDaoBatch" },
|
|
2380
|
+
inputSchema: {
|
|
2381
|
+
type: "object",
|
|
2382
|
+
properties: paramProperties(
|
|
2383
|
+
{
|
|
2384
|
+
tokenIn: { type: "address", required: true, description: "ERC-20 sold (native in uses WETH path in UI)" },
|
|
2385
|
+
tokenOut: { type: "string", required: true, description: "Output token or 0xeeee\u2026 native placeholder" },
|
|
2386
|
+
amountHuman: { type: "string", required: true, description: "Human-readable amount of tokenIn" },
|
|
2387
|
+
slippagePercent: { type: "number", required: true, description: "Slippage 0\u2013100 exclusive" }
|
|
2388
|
+
},
|
|
2389
|
+
["keyGen", "purposeText", "useCustomGas", "chainId", "rpcUrl", "executorAddress", "chainDetail", "customGasChainDetails"]
|
|
2390
|
+
),
|
|
2391
|
+
required: requiredKeys(
|
|
2392
|
+
{
|
|
2393
|
+
tokenIn: { type: "address", required: true, description: "" },
|
|
2394
|
+
tokenOut: { type: "string", required: true, description: "" },
|
|
2395
|
+
amountHuman: { type: "string", required: true, description: "" },
|
|
2396
|
+
slippagePercent: { type: "number", required: true, description: "" }
|
|
2397
|
+
},
|
|
2398
|
+
["keyGen", "purposeText", "useCustomGas", "chainId", "rpcUrl", "executorAddress", "chainDetail"]
|
|
2399
|
+
)
|
|
2400
|
+
},
|
|
2401
|
+
outputSchema: multisignOutputSchema
|
|
2402
|
+
}
|
|
2403
|
+
];
|
|
2404
|
+
function getAgentCatalogForMcp() {
|
|
2405
|
+
return {
|
|
2406
|
+
tools: MCP_TOOL_DEFINITIONS,
|
|
2407
|
+
protocols: getProtocolModules(),
|
|
2408
|
+
commonParams: EVM_COMMON_PARAM_DOCS,
|
|
2409
|
+
multisignOutput: MULTISIGN_OUTPUT_DOC,
|
|
2410
|
+
workflow: {
|
|
2411
|
+
evmSwapTypical: [
|
|
2412
|
+
"1. Quote (protocol-specific API if needed)",
|
|
2413
|
+
"2. Build protocol calldata (e.g. create_swap)",
|
|
2414
|
+
"3. build_*_multisign \u2192 { bodyForSign, messageToSign }",
|
|
2415
|
+
"4. Sign messageToSign (MetaMask or Ed25519)",
|
|
2416
|
+
"5. POST /multiSignRequest with clientSig and signedMessage"
|
|
2417
|
+
]
|
|
2418
|
+
}
|
|
2419
|
+
};
|
|
2420
|
+
}
|
|
2421
|
+
|
|
2422
|
+
// src/agent/catalog.ts
|
|
2423
|
+
registerProtocolModule(uniswapV4ProtocolModule);
|
|
2424
|
+
registerProtocolModule(curveDaoProtocolModule);
|
|
2425
|
+
function getAgentCatalog() {
|
|
2426
|
+
return {
|
|
2427
|
+
protocols: getProtocolModules(),
|
|
2428
|
+
byCategory: {
|
|
2429
|
+
evm: getActionsByChainCategory("evm"),
|
|
2430
|
+
solana: getActionsByChainCategory("solana"),
|
|
2431
|
+
near: getActionsByChainCategory("near")
|
|
2432
|
+
},
|
|
2433
|
+
uniswapV4: uniswapV4ProtocolModule,
|
|
2434
|
+
curveDao: curveDaoProtocolModule,
|
|
2435
|
+
/** Prefer getAgentCatalogForMcp() or getMcpToolDefinitions() for MCP servers. */
|
|
2436
|
+
mcp: getAgentCatalogForMcp()
|
|
2437
|
+
};
|
|
2438
|
+
}
|
|
2439
|
+
|
|
2440
|
+
// src/index.ts
|
|
2441
|
+
registerProtocolModule(uniswapV4ProtocolModule);
|
|
2442
|
+
registerProtocolModule(curveDaoProtocolModule);
|
|
2443
|
+
|
|
2444
|
+
export { NEAR_CHAIN_CATEGORY, SOLANA_CHAIN_CATEGORY, alignEip1559FeesWithLatestBase, buildEvmMultisignBatch, chainSnapshotForCustomGasExtraJSON, composeFeePayloadToTxParams, coreChainCategoryModule, curveDao, curveDaoProtocolModule, evmChainCategoryModule, fetchChainFeeParams, finalizeMultisign, firstClientIdFromKeyGen, gasLimitFromEstimateAndChainConfig, getActionsByChainCategory, getAgentCatalog, getProtocolModule, getProtocolModules, gweiToDecimalString, isEvmNativeToken, keyListFromKeyGen, matchEvmTokenKind, mergePurposeText, nearChainCategoryModule, nodeFetchWithReadAuth, parseEvmChainIdToNumber, proposalTxParamsToFeeSnapshot, registerProtocolModule, requirePubKeyHex, routerSwapGasLimitFromEstimate, solanaChainCategoryModule, triggerTxParamsFromComposeBody, uniswapV4, uniswapV4ProtocolModule };
|
|
2445
|
+
//# sourceMappingURL=index.js.map
|
|
2446
|
+
//# sourceMappingURL=index.js.map
|