@rhea-finance/cross-chain-aggregation-dex 0.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +653 -0
- package/dist/index.d.mts +465 -0
- package/dist/index.d.ts +465 -0
- package/dist/index.js +1295 -0
- package/dist/index.js.map +1 -0
- package/dist/index.mjs +1275 -0
- package/dist/index.mjs.map +1 -0
- package/package.json +67 -0
package/dist/index.mjs
ADDED
|
@@ -0,0 +1,1275 @@
|
|
|
1
|
+
import Big2 from 'big.js';
|
|
2
|
+
|
|
3
|
+
// src/types/index.ts
|
|
4
|
+
function requiresRecipient(params) {
|
|
5
|
+
return "sender" in params && "recipient" in params;
|
|
6
|
+
}
|
|
7
|
+
function requiresRecipientInExecute(params) {
|
|
8
|
+
return "sender" in params && "receiveUser" in params;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
// src/utils/logger.ts
|
|
12
|
+
var LOG_LEVELS = {
|
|
13
|
+
silent: 0,
|
|
14
|
+
error: 1,
|
|
15
|
+
warn: 2,
|
|
16
|
+
info: 3,
|
|
17
|
+
debug: 4
|
|
18
|
+
};
|
|
19
|
+
function getLogLevel() {
|
|
20
|
+
if (typeof process !== "undefined" && process.env?.LOG_LEVEL) {
|
|
21
|
+
const level = process.env.LOG_LEVEL.toLowerCase();
|
|
22
|
+
if (LOG_LEVELS[level] !== void 0) {
|
|
23
|
+
return level;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
return typeof process !== "undefined" && process.env?.NODE_ENV === "production" ? "warn" : "debug";
|
|
27
|
+
}
|
|
28
|
+
var currentLogLevel = getLogLevel();
|
|
29
|
+
var currentLevelValue = LOG_LEVELS[currentLogLevel];
|
|
30
|
+
function shouldLog(level) {
|
|
31
|
+
return LOG_LEVELS[level] <= currentLevelValue;
|
|
32
|
+
}
|
|
33
|
+
var logger = {
|
|
34
|
+
debug: (...args) => {
|
|
35
|
+
if (shouldLog("debug")) {
|
|
36
|
+
console.debug(...args);
|
|
37
|
+
}
|
|
38
|
+
},
|
|
39
|
+
info: (...args) => {
|
|
40
|
+
if (shouldLog("info")) {
|
|
41
|
+
console.info(...args);
|
|
42
|
+
}
|
|
43
|
+
},
|
|
44
|
+
warn: (...args) => {
|
|
45
|
+
if (shouldLog("warn")) {
|
|
46
|
+
console.warn(...args);
|
|
47
|
+
}
|
|
48
|
+
},
|
|
49
|
+
error: (...args) => {
|
|
50
|
+
if (shouldLog("error")) {
|
|
51
|
+
console.error(...args);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
// src/utils/index.ts
|
|
57
|
+
var bluechipTokensConfig = null;
|
|
58
|
+
function setBluechipTokensConfig(config) {
|
|
59
|
+
bluechipTokensConfig = config;
|
|
60
|
+
}
|
|
61
|
+
function getBluechipTokensConfig() {
|
|
62
|
+
if (!bluechipTokensConfig) {
|
|
63
|
+
logger.warn(
|
|
64
|
+
"getBluechipTokensConfig - Bluechip tokens config not set, returning empty config"
|
|
65
|
+
);
|
|
66
|
+
return {};
|
|
67
|
+
}
|
|
68
|
+
return bluechipTokensConfig;
|
|
69
|
+
}
|
|
70
|
+
function normalizeTokenId(tokenId, wrapNearContractId = "wrap.near") {
|
|
71
|
+
if (!tokenId) {
|
|
72
|
+
logger.error("normalizeTokenId - Empty tokenId:", tokenId);
|
|
73
|
+
return "";
|
|
74
|
+
}
|
|
75
|
+
let normalized = tokenId.replace(/^nep141:/, "");
|
|
76
|
+
if (normalized === "near") {
|
|
77
|
+
normalized = wrapNearContractId;
|
|
78
|
+
}
|
|
79
|
+
if (!normalized) {
|
|
80
|
+
logger.error("normalizeTokenId - Result is empty:", {
|
|
81
|
+
tokenId,
|
|
82
|
+
normalized
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
return normalized;
|
|
86
|
+
}
|
|
87
|
+
function isNearIntentsSupportedToken(token, bluechipTokens) {
|
|
88
|
+
if (!token?.symbol || !token?.address) {
|
|
89
|
+
return false;
|
|
90
|
+
}
|
|
91
|
+
const config = bluechipTokens || getBluechipTokensConfig();
|
|
92
|
+
const normalizedSymbol = token.symbol.toUpperCase();
|
|
93
|
+
const symbolKey = normalizedSymbol === "NEAR" || normalizedSymbol === "WNEAR" ? "NEAR" : normalizedSymbol;
|
|
94
|
+
const tokenConfig = config[symbolKey];
|
|
95
|
+
if (!tokenConfig) {
|
|
96
|
+
return false;
|
|
97
|
+
}
|
|
98
|
+
const normalizeAddress = (addr) => addr.replace(/^nep141:/, "").toLowerCase();
|
|
99
|
+
const tokenAddress = normalizeAddress(token.address);
|
|
100
|
+
const configAddress = normalizeAddress(tokenConfig.address || "");
|
|
101
|
+
const configAssetId = tokenConfig.assetId ? normalizeAddress(tokenConfig.assetId) : "";
|
|
102
|
+
return tokenAddress === configAddress || tokenAddress === configAssetId;
|
|
103
|
+
}
|
|
104
|
+
function findBestBluechipToken(bluechipTokens, wrapNearContractId = "wrap.near") {
|
|
105
|
+
const preferredTokens = [];
|
|
106
|
+
if (bluechipTokens.USDT?.address) {
|
|
107
|
+
preferredTokens.push({
|
|
108
|
+
address: bluechipTokens.USDT.address,
|
|
109
|
+
symbol: "USDT",
|
|
110
|
+
decimals: bluechipTokens.USDT.decimals || 6,
|
|
111
|
+
chain: "near"
|
|
112
|
+
});
|
|
113
|
+
}
|
|
114
|
+
if (bluechipTokens.USDC?.address) {
|
|
115
|
+
preferredTokens.push({
|
|
116
|
+
address: bluechipTokens.USDC.address,
|
|
117
|
+
symbol: "USDC",
|
|
118
|
+
decimals: bluechipTokens.USDC.decimals || 6,
|
|
119
|
+
chain: "near"
|
|
120
|
+
});
|
|
121
|
+
}
|
|
122
|
+
if (bluechipTokens.NEAR?.address) {
|
|
123
|
+
preferredTokens.push({
|
|
124
|
+
address: bluechipTokens.NEAR.address,
|
|
125
|
+
symbol: "wNEAR",
|
|
126
|
+
decimals: bluechipTokens.NEAR.decimals || 24,
|
|
127
|
+
chain: "near"
|
|
128
|
+
});
|
|
129
|
+
}
|
|
130
|
+
if (preferredTokens.length === 0) {
|
|
131
|
+
logger.warn(
|
|
132
|
+
"findBestBluechipToken - No preferred tokens found, using wrap.near"
|
|
133
|
+
);
|
|
134
|
+
return {
|
|
135
|
+
address: wrapNearContractId,
|
|
136
|
+
symbol: "wNEAR",
|
|
137
|
+
decimals: 24,
|
|
138
|
+
chain: "near"
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
logger.debug("findBestBluechipToken - Selected token:", preferredTokens[0]);
|
|
142
|
+
return preferredTokens[0];
|
|
143
|
+
}
|
|
144
|
+
function convertSlippageToBasisPoints(slippage) {
|
|
145
|
+
if (slippage >= 1) {
|
|
146
|
+
return Math.round(slippage);
|
|
147
|
+
}
|
|
148
|
+
if (slippage > 0 && slippage < 0.01) {
|
|
149
|
+
return Math.round(slippage * 1e4);
|
|
150
|
+
}
|
|
151
|
+
if (slippage >= 0.01 && slippage < 1) {
|
|
152
|
+
return Math.round(slippage * 100);
|
|
153
|
+
}
|
|
154
|
+
return Math.round(slippage);
|
|
155
|
+
}
|
|
156
|
+
function normalizeDestinationAsset(assetId, wrapNearContractId = "wrap.near") {
|
|
157
|
+
if (!assetId) return assetId;
|
|
158
|
+
if (assetId.startsWith("nep141:") || assetId.startsWith("nep245:")) {
|
|
159
|
+
return assetId;
|
|
160
|
+
}
|
|
161
|
+
if (assetId === "near" || assetId === "nep141:near") {
|
|
162
|
+
return `nep141:${wrapNearContractId}`;
|
|
163
|
+
}
|
|
164
|
+
if (assetId.includes(".")) {
|
|
165
|
+
return `nep141:${normalizeTokenId(assetId, wrapNearContractId)}`;
|
|
166
|
+
}
|
|
167
|
+
return assetId;
|
|
168
|
+
}
|
|
169
|
+
function formatGasToTgas(gasInYoctoNEAR) {
|
|
170
|
+
if (!gasInYoctoNEAR) return "0";
|
|
171
|
+
const gasStr = String(gasInYoctoNEAR);
|
|
172
|
+
if (/[eE]/.test(gasStr)) {
|
|
173
|
+
const match = gasStr.match(/^([+-]?\d*\.?\d+)[eE]([+-]?\d+)$/);
|
|
174
|
+
if (match) {
|
|
175
|
+
const base = match[1];
|
|
176
|
+
const exponent = parseInt(match[2], 10);
|
|
177
|
+
const [intPart, fracPart = ""] = base.split(".");
|
|
178
|
+
if (exponent > 0) {
|
|
179
|
+
const newIntPart = intPart + fracPart;
|
|
180
|
+
const zerosToAdd = exponent - fracPart.length;
|
|
181
|
+
if (zerosToAdd > 0) {
|
|
182
|
+
return (newIntPart + "0".repeat(zerosToAdd)).replace(/^0+/, "") || "0";
|
|
183
|
+
} else {
|
|
184
|
+
const pointPos = intPart.length + exponent;
|
|
185
|
+
return (newIntPart.slice(0, pointPos) + "." + newIntPart.slice(pointPos)).replace(/\.?0+$/, "");
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
try {
|
|
191
|
+
const gasBigInt = BigInt(gasStr.split(".")[0]);
|
|
192
|
+
const tgasBigInt = gasBigInt / BigInt("1000000000000");
|
|
193
|
+
return tgasBigInt.toString();
|
|
194
|
+
} catch (error) {
|
|
195
|
+
logger.error("formatGasToTgas - Error formatting gas:", {
|
|
196
|
+
gasInYoctoNEAR,
|
|
197
|
+
error
|
|
198
|
+
});
|
|
199
|
+
return "0";
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
function formatGasString(gas) {
|
|
203
|
+
if (typeof gas === "bigint") {
|
|
204
|
+
return gas.toString();
|
|
205
|
+
}
|
|
206
|
+
const gasStr = String(gas);
|
|
207
|
+
if (!/[eE]/.test(gasStr) && !gasStr.includes(".")) {
|
|
208
|
+
return gasStr;
|
|
209
|
+
}
|
|
210
|
+
if (/[eE]/.test(gasStr)) {
|
|
211
|
+
const match = gasStr.match(/^([+-]?\d*\.?\d+)[eE]([+-]?\d+)$/);
|
|
212
|
+
if (match) {
|
|
213
|
+
const base = match[1];
|
|
214
|
+
const exponent = parseInt(match[2], 10);
|
|
215
|
+
const [intPart, fracPart = ""] = base.split(".");
|
|
216
|
+
if (exponent > 0) {
|
|
217
|
+
const newIntPart = intPart + fracPart;
|
|
218
|
+
const zerosToAdd = exponent - fracPart.length;
|
|
219
|
+
if (zerosToAdd > 0) {
|
|
220
|
+
return (newIntPart + "0".repeat(zerosToAdd)).replace(/^0+/, "") || "0";
|
|
221
|
+
} else {
|
|
222
|
+
const pointPos = intPart.length + exponent;
|
|
223
|
+
const result = newIntPart.slice(0, pointPos) + "." + newIntPart.slice(pointPos);
|
|
224
|
+
return result.replace(/\.?0+$/, "").replace(/\.$/, "");
|
|
225
|
+
}
|
|
226
|
+
} else if (exponent < 0) {
|
|
227
|
+
const absExp = Math.abs(exponent);
|
|
228
|
+
const zerosToAdd = absExp - intPart.length;
|
|
229
|
+
if (zerosToAdd > 0) {
|
|
230
|
+
return "0." + "0".repeat(zerosToAdd - 1) + intPart.replace(/^-/, "") + fracPart;
|
|
231
|
+
} else {
|
|
232
|
+
const pointPos = intPart.length - absExp;
|
|
233
|
+
return intPart.slice(0, pointPos) + "." + intPart.slice(pointPos) + fracPart;
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
if (gasStr.includes(".")) {
|
|
239
|
+
const [intPart, fracPart = ""] = gasStr.split(".");
|
|
240
|
+
return intPart + fracPart;
|
|
241
|
+
}
|
|
242
|
+
return gasStr;
|
|
243
|
+
}
|
|
244
|
+
var NearSmartRouter = class {
|
|
245
|
+
constructor(config) {
|
|
246
|
+
this.findPathAdapter = config.findPathAdapter;
|
|
247
|
+
this.nearChainAdapter = config.nearChainAdapter;
|
|
248
|
+
this.configAdapter = config.configAdapter;
|
|
249
|
+
this.wrapNearContractId = this.configAdapter.getWrapNearContractId();
|
|
250
|
+
this.refExchangeId = this.configAdapter.getRefExchangeId();
|
|
251
|
+
this.tokenStorageDepositRead = this.configAdapter.getTokenStorageDepositRead?.() || "1250000000000000000000";
|
|
252
|
+
}
|
|
253
|
+
/**
|
|
254
|
+
* Get a swap quote (normalizes token ids and queries FindPath for routes).
|
|
255
|
+
*/
|
|
256
|
+
async quote(params) {
|
|
257
|
+
try {
|
|
258
|
+
const {
|
|
259
|
+
tokenIn,
|
|
260
|
+
tokenOut,
|
|
261
|
+
amountIn,
|
|
262
|
+
slippage,
|
|
263
|
+
swapType: _swapType = "EXACT_INPUT"
|
|
264
|
+
// Currently not used, reserved for future use
|
|
265
|
+
} = params;
|
|
266
|
+
if (!tokenIn?.address || !tokenOut?.address) {
|
|
267
|
+
return {
|
|
268
|
+
success: false,
|
|
269
|
+
tokenIn: params.tokenIn,
|
|
270
|
+
tokenOut: params.tokenOut,
|
|
271
|
+
amountIn: params.amountIn,
|
|
272
|
+
amountOut: "0",
|
|
273
|
+
minAmountOut: "0",
|
|
274
|
+
routes: [],
|
|
275
|
+
error: "Missing token address"
|
|
276
|
+
};
|
|
277
|
+
}
|
|
278
|
+
const normalizedTokenIn = normalizeTokenId(
|
|
279
|
+
tokenIn.address,
|
|
280
|
+
this.wrapNearContractId
|
|
281
|
+
);
|
|
282
|
+
const normalizedTokenOut = normalizeTokenId(
|
|
283
|
+
tokenOut.address,
|
|
284
|
+
this.wrapNearContractId
|
|
285
|
+
);
|
|
286
|
+
if (!normalizedTokenIn || !normalizedTokenOut) {
|
|
287
|
+
logger.error("SmartRouter quote - Invalid token addresses:", {
|
|
288
|
+
tokenIn: {
|
|
289
|
+
original: tokenIn.address,
|
|
290
|
+
normalized: normalizedTokenIn
|
|
291
|
+
},
|
|
292
|
+
tokenOut: {
|
|
293
|
+
original: tokenOut.address,
|
|
294
|
+
normalized: normalizedTokenOut
|
|
295
|
+
}
|
|
296
|
+
});
|
|
297
|
+
return {
|
|
298
|
+
success: false,
|
|
299
|
+
tokenIn: params.tokenIn,
|
|
300
|
+
tokenOut: params.tokenOut,
|
|
301
|
+
amountIn: params.amountIn,
|
|
302
|
+
amountOut: "0",
|
|
303
|
+
minAmountOut: "0",
|
|
304
|
+
routes: [],
|
|
305
|
+
error: `Invalid token address: tokenIn=${normalizedTokenIn || "empty"}, tokenOut=${normalizedTokenOut || "empty"}`
|
|
306
|
+
};
|
|
307
|
+
}
|
|
308
|
+
const slippageBps = convertSlippageToBasisPoints(slippage);
|
|
309
|
+
const slippageDecimalForApi = slippageBps / 1e4;
|
|
310
|
+
logger.debug("SmartRouter quote - Calling findPath:", {
|
|
311
|
+
tokenIn: normalizedTokenIn,
|
|
312
|
+
tokenOut: normalizedTokenOut,
|
|
313
|
+
amountIn,
|
|
314
|
+
slippage: slippageDecimalForApi,
|
|
315
|
+
slippageBps
|
|
316
|
+
});
|
|
317
|
+
const response = await this.findPathAdapter.findPath({
|
|
318
|
+
tokenIn: normalizedTokenIn,
|
|
319
|
+
tokenOut: normalizedTokenOut,
|
|
320
|
+
amountIn: String(amountIn),
|
|
321
|
+
slippage: slippageDecimalForApi,
|
|
322
|
+
supportLedger: false
|
|
323
|
+
});
|
|
324
|
+
logger.debug("SmartRouter quote - findPath response:", {
|
|
325
|
+
result_code: response?.result_code,
|
|
326
|
+
result_msg: response?.result_msg || response?.result_message,
|
|
327
|
+
hasRoutes: !!response?.result_data?.routes?.length
|
|
328
|
+
});
|
|
329
|
+
if (response?.result_code !== 0 || !response?.result_data?.routes?.length) {
|
|
330
|
+
return {
|
|
331
|
+
success: false,
|
|
332
|
+
tokenIn,
|
|
333
|
+
tokenOut,
|
|
334
|
+
amountIn,
|
|
335
|
+
amountOut: "0",
|
|
336
|
+
minAmountOut: "0",
|
|
337
|
+
routes: [],
|
|
338
|
+
error: response?.result_msg || response?.result_message || "No route found"
|
|
339
|
+
};
|
|
340
|
+
}
|
|
341
|
+
const { routes: serverRoutes, amount_out } = response.result_data;
|
|
342
|
+
const slippageDecimal = new Big2(slippageBps).div(1e4);
|
|
343
|
+
const routes = serverRoutes.map((route) => ({
|
|
344
|
+
pools: route.pools.map((pool) => ({
|
|
345
|
+
pool_id: Number(pool.pool_id),
|
|
346
|
+
token_in: pool.token_in || normalizedTokenIn,
|
|
347
|
+
token_out: pool.token_out || normalizedTokenOut,
|
|
348
|
+
amount_in: pool.amount_in,
|
|
349
|
+
amount_out: pool.amount_out,
|
|
350
|
+
fee: pool.fee
|
|
351
|
+
})),
|
|
352
|
+
amountIn,
|
|
353
|
+
amountOut: route.amount_out || amount_out || "0"
|
|
354
|
+
}));
|
|
355
|
+
const amountOut = new Big2(amount_out || 0);
|
|
356
|
+
const minAmountOut = amountOut.mul(new Big2(1).minus(slippageDecimal)).toFixed(0, Big2.roundDown);
|
|
357
|
+
return {
|
|
358
|
+
success: true,
|
|
359
|
+
tokenIn,
|
|
360
|
+
tokenOut,
|
|
361
|
+
amountIn,
|
|
362
|
+
amountOut: amountOut.toFixed(0),
|
|
363
|
+
minAmountOut,
|
|
364
|
+
routes,
|
|
365
|
+
// Save raw serverRoutes data for executeSwap
|
|
366
|
+
rawRoutes: serverRoutes
|
|
367
|
+
};
|
|
368
|
+
} catch (error) {
|
|
369
|
+
return {
|
|
370
|
+
success: false,
|
|
371
|
+
tokenIn: params.tokenIn,
|
|
372
|
+
tokenOut: params.tokenOut,
|
|
373
|
+
amountIn: params.amountIn,
|
|
374
|
+
amountOut: "0",
|
|
375
|
+
minAmountOut: "0",
|
|
376
|
+
routes: [],
|
|
377
|
+
error: error?.message || "Quote failed"
|
|
378
|
+
};
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
/**
|
|
382
|
+
* Execute a swap: optionally adds `storage_deposit` for the recipient, then calls REF via `ft_transfer_call`.
|
|
383
|
+
*/
|
|
384
|
+
async executeSwap(params) {
|
|
385
|
+
try {
|
|
386
|
+
const { quote, recipient, depositAddress } = params;
|
|
387
|
+
if (!quote.success || !quote.routes.length) {
|
|
388
|
+
return {
|
|
389
|
+
success: false,
|
|
390
|
+
error: "Invalid quote"
|
|
391
|
+
};
|
|
392
|
+
}
|
|
393
|
+
const swapActions = [];
|
|
394
|
+
const routesToUse = quote.rawRoutes || quote.routes;
|
|
395
|
+
routesToUse.forEach((route) => {
|
|
396
|
+
const pools = route.pools || [];
|
|
397
|
+
pools.forEach((pool) => {
|
|
398
|
+
const poolCopy = { ...pool };
|
|
399
|
+
if (+(poolCopy?.amount_in || 0) == 0) {
|
|
400
|
+
delete poolCopy.amount_in;
|
|
401
|
+
}
|
|
402
|
+
poolCopy.pool_id = Number(poolCopy.pool_id);
|
|
403
|
+
swapActions.push(poolCopy);
|
|
404
|
+
});
|
|
405
|
+
});
|
|
406
|
+
if (!swapActions.length) {
|
|
407
|
+
return {
|
|
408
|
+
success: false,
|
|
409
|
+
error: "No swap actions"
|
|
410
|
+
};
|
|
411
|
+
}
|
|
412
|
+
const finalRecipient = depositAddress || recipient;
|
|
413
|
+
const transactions = [];
|
|
414
|
+
if (finalRecipient && quote.tokenOut?.address) {
|
|
415
|
+
let isRegistered = false;
|
|
416
|
+
try {
|
|
417
|
+
const storageBalance = await this.nearChainAdapter.view({
|
|
418
|
+
contractId: quote.tokenOut.address,
|
|
419
|
+
methodName: "storage_balance_of",
|
|
420
|
+
args: {
|
|
421
|
+
account_id: finalRecipient
|
|
422
|
+
}
|
|
423
|
+
});
|
|
424
|
+
isRegistered = !!storageBalance;
|
|
425
|
+
} catch (err) {
|
|
426
|
+
isRegistered = false;
|
|
427
|
+
}
|
|
428
|
+
if (!isRegistered) {
|
|
429
|
+
logger.debug("SmartRouter - Registering recipient account:", {
|
|
430
|
+
contractId: quote.tokenOut.address,
|
|
431
|
+
accountId: finalRecipient
|
|
432
|
+
});
|
|
433
|
+
transactions.push({
|
|
434
|
+
contractId: quote.tokenOut.address,
|
|
435
|
+
methodName: "storage_deposit",
|
|
436
|
+
args: {
|
|
437
|
+
account_id: finalRecipient,
|
|
438
|
+
registration_only: true
|
|
439
|
+
},
|
|
440
|
+
gas: "50",
|
|
441
|
+
expandDeposit: this.tokenStorageDepositRead
|
|
442
|
+
});
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
const swapMsg = {
|
|
446
|
+
force: 0,
|
|
447
|
+
actions: swapActions,
|
|
448
|
+
skip_unwrap_near: false
|
|
449
|
+
};
|
|
450
|
+
if (finalRecipient) {
|
|
451
|
+
swapMsg.swap_out_recipient = finalRecipient;
|
|
452
|
+
}
|
|
453
|
+
logger.debug("SmartRouter - Executing swap:", {
|
|
454
|
+
contractId: quote.tokenIn.address,
|
|
455
|
+
receiver_id: this.refExchangeId,
|
|
456
|
+
amount: quote.amountIn,
|
|
457
|
+
swapMsg,
|
|
458
|
+
swapActionsCount: swapActions.length,
|
|
459
|
+
recipient: finalRecipient,
|
|
460
|
+
tokenOut: quote.tokenOut?.address
|
|
461
|
+
});
|
|
462
|
+
transactions.push({
|
|
463
|
+
contractId: quote.tokenIn.address,
|
|
464
|
+
methodName: "ft_transfer_call",
|
|
465
|
+
args: {
|
|
466
|
+
receiver_id: this.refExchangeId,
|
|
467
|
+
amount: quote.amountIn,
|
|
468
|
+
msg: JSON.stringify(swapMsg)
|
|
469
|
+
},
|
|
470
|
+
gas: "250",
|
|
471
|
+
// NEP-141 requires attaching 1 yoctoNEAR for certain calls.
|
|
472
|
+
expandDeposit: "1"
|
|
473
|
+
});
|
|
474
|
+
const result = await this.nearChainAdapter.call({
|
|
475
|
+
transactions
|
|
476
|
+
});
|
|
477
|
+
if (result.status === "success") {
|
|
478
|
+
return {
|
|
479
|
+
success: true,
|
|
480
|
+
txHash: result.txHash,
|
|
481
|
+
txHashArray: result.txHashArr || (result.txHash ? [result.txHash] : [])
|
|
482
|
+
};
|
|
483
|
+
} else {
|
|
484
|
+
return {
|
|
485
|
+
success: false,
|
|
486
|
+
error: result.message || "Execute swap failed"
|
|
487
|
+
};
|
|
488
|
+
}
|
|
489
|
+
} catch (error) {
|
|
490
|
+
return {
|
|
491
|
+
success: false,
|
|
492
|
+
error: error?.message || "Execute swap failed"
|
|
493
|
+
};
|
|
494
|
+
}
|
|
495
|
+
}
|
|
496
|
+
/**
|
|
497
|
+
* Get Router capabilities
|
|
498
|
+
*/
|
|
499
|
+
getCapabilities() {
|
|
500
|
+
return {
|
|
501
|
+
requiresRecipient: false,
|
|
502
|
+
requiresFinalizeQuote: false,
|
|
503
|
+
requiresComplexRegistration: false,
|
|
504
|
+
supportedChain: "near"
|
|
505
|
+
};
|
|
506
|
+
}
|
|
507
|
+
/**
|
|
508
|
+
* Get supported chain
|
|
509
|
+
*/
|
|
510
|
+
getSupportedChain() {
|
|
511
|
+
return "near";
|
|
512
|
+
}
|
|
513
|
+
};
|
|
514
|
+
var AggregateDexRouter = class {
|
|
515
|
+
constructor(config) {
|
|
516
|
+
this.NEW_ACCOUNT_STORAGE_COST = "1250000000000000000000";
|
|
517
|
+
// 0.00125 NEAR in yoctoNEAR
|
|
518
|
+
this.ONE_YOCTO_NEAR = "1";
|
|
519
|
+
this.swapMultiDexPathAdapter = config.swapMultiDexPathAdapter;
|
|
520
|
+
this.nearChainAdapter = config.nearChainAdapter;
|
|
521
|
+
this.configAdapter = config.configAdapter;
|
|
522
|
+
this.aggregateDexContractId = this.configAdapter.getAggregateDexContractId?.() || "";
|
|
523
|
+
this.wrapNearContractId = this.configAdapter.getWrapNearContractId();
|
|
524
|
+
if (!this.aggregateDexContractId) {
|
|
525
|
+
logger.error(
|
|
526
|
+
"AggregateDexRouter - AGGREGATE_DEX_CONTRACT_ID not configured"
|
|
527
|
+
);
|
|
528
|
+
}
|
|
529
|
+
}
|
|
530
|
+
/**
|
|
531
|
+
* Get Router capabilities
|
|
532
|
+
*/
|
|
533
|
+
getCapabilities() {
|
|
534
|
+
return {
|
|
535
|
+
requiresRecipient: true,
|
|
536
|
+
requiresFinalizeQuote: false,
|
|
537
|
+
requiresComplexRegistration: true,
|
|
538
|
+
supportedChain: "near"
|
|
539
|
+
};
|
|
540
|
+
}
|
|
541
|
+
getSupportedChain() {
|
|
542
|
+
return "near";
|
|
543
|
+
}
|
|
544
|
+
/**
|
|
545
|
+
* Get a swap quote from V2 Router API
|
|
546
|
+
*/
|
|
547
|
+
async quote(params) {
|
|
548
|
+
try {
|
|
549
|
+
if (!requiresRecipient(params)) {
|
|
550
|
+
return {
|
|
551
|
+
success: false,
|
|
552
|
+
tokenIn: params.tokenIn,
|
|
553
|
+
tokenOut: params.tokenOut,
|
|
554
|
+
amountIn: params.amountIn,
|
|
555
|
+
amountOut: "0",
|
|
556
|
+
minAmountOut: "0",
|
|
557
|
+
routes: [],
|
|
558
|
+
error: "V2 Router requires sender and recipient parameters"
|
|
559
|
+
};
|
|
560
|
+
}
|
|
561
|
+
const { tokenIn, tokenOut, amountIn, slippage, sender, recipient } = params;
|
|
562
|
+
if (!sender || !recipient) {
|
|
563
|
+
logger.error("AggregateDexRouter quote - Missing sender or recipient:", {
|
|
564
|
+
sender,
|
|
565
|
+
recipient
|
|
566
|
+
});
|
|
567
|
+
return {
|
|
568
|
+
success: false,
|
|
569
|
+
tokenIn: params.tokenIn,
|
|
570
|
+
tokenOut: params.tokenOut,
|
|
571
|
+
amountIn: params.amountIn,
|
|
572
|
+
amountOut: "0",
|
|
573
|
+
minAmountOut: "0",
|
|
574
|
+
routes: [],
|
|
575
|
+
error: `V2 Router requires non-empty sender and recipient. Got sender="${sender}", recipient="${recipient}"`
|
|
576
|
+
};
|
|
577
|
+
}
|
|
578
|
+
if (!tokenIn?.address || !tokenOut?.address) {
|
|
579
|
+
return {
|
|
580
|
+
success: false,
|
|
581
|
+
tokenIn: params.tokenIn,
|
|
582
|
+
tokenOut: params.tokenOut,
|
|
583
|
+
amountIn: params.amountIn,
|
|
584
|
+
amountOut: "0",
|
|
585
|
+
minAmountOut: "0",
|
|
586
|
+
routes: [],
|
|
587
|
+
error: "Missing token address"
|
|
588
|
+
};
|
|
589
|
+
}
|
|
590
|
+
const normalizedTokenIn = normalizeTokenId(
|
|
591
|
+
tokenIn.address,
|
|
592
|
+
this.wrapNearContractId
|
|
593
|
+
);
|
|
594
|
+
const normalizedTokenOut = normalizeTokenId(
|
|
595
|
+
tokenOut.address,
|
|
596
|
+
this.wrapNearContractId
|
|
597
|
+
);
|
|
598
|
+
if (!normalizedTokenIn || !normalizedTokenOut) {
|
|
599
|
+
logger.error("AggregateDexRouter quote - Invalid token addresses:", {
|
|
600
|
+
tokenIn: {
|
|
601
|
+
original: tokenIn.address,
|
|
602
|
+
normalized: normalizedTokenIn
|
|
603
|
+
},
|
|
604
|
+
tokenOut: {
|
|
605
|
+
original: tokenOut.address,
|
|
606
|
+
normalized: normalizedTokenOut
|
|
607
|
+
}
|
|
608
|
+
});
|
|
609
|
+
return {
|
|
610
|
+
success: false,
|
|
611
|
+
tokenIn: params.tokenIn,
|
|
612
|
+
tokenOut: params.tokenOut,
|
|
613
|
+
amountIn: params.amountIn,
|
|
614
|
+
amountOut: "0",
|
|
615
|
+
minAmountOut: "0",
|
|
616
|
+
routes: [],
|
|
617
|
+
error: `Invalid token address: tokenIn=${normalizedTokenIn || "empty"}, tokenOut=${normalizedTokenOut || "empty"}`
|
|
618
|
+
};
|
|
619
|
+
}
|
|
620
|
+
const slippageBps = convertSlippageToBasisPoints(slippage);
|
|
621
|
+
const slippageDecimalForApi = slippageBps / 1e4;
|
|
622
|
+
logger.debug("AggregateDexRouter quote - Calling swapMultiDexPath:", {
|
|
623
|
+
tokenIn: normalizedTokenIn,
|
|
624
|
+
tokenOut: normalizedTokenOut,
|
|
625
|
+
amountIn,
|
|
626
|
+
slippage: slippageDecimalForApi,
|
|
627
|
+
sender,
|
|
628
|
+
recipient
|
|
629
|
+
});
|
|
630
|
+
const response = await this.swapMultiDexPathAdapter.swapMultiDexPath({
|
|
631
|
+
amountIn: String(amountIn),
|
|
632
|
+
tokenIn: normalizedTokenIn,
|
|
633
|
+
tokenOut: normalizedTokenOut,
|
|
634
|
+
slippage: slippageDecimalForApi,
|
|
635
|
+
pathDeep: 2,
|
|
636
|
+
user: sender,
|
|
637
|
+
receiveUser: recipient
|
|
638
|
+
});
|
|
639
|
+
logger.debug("AggregateDexRouter quote - swapMultiDexPath response:", {
|
|
640
|
+
result_code: response?.result_code,
|
|
641
|
+
result_message: response?.result_message,
|
|
642
|
+
hasData: !!response?.result_data
|
|
643
|
+
});
|
|
644
|
+
if (response.result_code !== 0 || !response.result_data) {
|
|
645
|
+
return {
|
|
646
|
+
success: false,
|
|
647
|
+
tokenIn,
|
|
648
|
+
tokenOut,
|
|
649
|
+
amountIn,
|
|
650
|
+
amountOut: "0",
|
|
651
|
+
minAmountOut: "0",
|
|
652
|
+
routes: [],
|
|
653
|
+
error: response.result_message || "V2 Router API call failed"
|
|
654
|
+
};
|
|
655
|
+
}
|
|
656
|
+
const {
|
|
657
|
+
amount_in,
|
|
658
|
+
amount_out,
|
|
659
|
+
min_amount_out,
|
|
660
|
+
msg,
|
|
661
|
+
signature,
|
|
662
|
+
tokens,
|
|
663
|
+
dexs
|
|
664
|
+
} = response.result_data;
|
|
665
|
+
return {
|
|
666
|
+
success: true,
|
|
667
|
+
tokenIn,
|
|
668
|
+
tokenOut,
|
|
669
|
+
amountIn: amount_in || amountIn,
|
|
670
|
+
amountOut: amount_out || "0",
|
|
671
|
+
minAmountOut: min_amount_out || "0",
|
|
672
|
+
routes: [],
|
|
673
|
+
routerMsg: msg,
|
|
674
|
+
signature,
|
|
675
|
+
tokens: tokens || [],
|
|
676
|
+
dexs: dexs || [],
|
|
677
|
+
recipient,
|
|
678
|
+
slippage
|
|
679
|
+
};
|
|
680
|
+
} catch (error) {
|
|
681
|
+
logger.error("AggregateDexRouter quote - Error:", error);
|
|
682
|
+
return {
|
|
683
|
+
success: false,
|
|
684
|
+
tokenIn: params.tokenIn,
|
|
685
|
+
tokenOut: params.tokenOut,
|
|
686
|
+
amountIn: params.amountIn,
|
|
687
|
+
amountOut: "0",
|
|
688
|
+
minAmountOut: "0",
|
|
689
|
+
routes: [],
|
|
690
|
+
error: error?.message || "Quote failed"
|
|
691
|
+
};
|
|
692
|
+
}
|
|
693
|
+
}
|
|
694
|
+
/**
|
|
695
|
+
* Finalize quote with depositAddress (deprecated)
|
|
696
|
+
*
|
|
697
|
+
* @deprecated No longer needed. executeSwap automatically fetches final quote using receiveUser (depositAddress).
|
|
698
|
+
* Kept for interface compatibility only.
|
|
699
|
+
*/
|
|
700
|
+
async finalizeQuote(params, depositAddress) {
|
|
701
|
+
if (!requiresRecipient(params)) {
|
|
702
|
+
throw new Error("V2 Router requires recipient parameters");
|
|
703
|
+
}
|
|
704
|
+
return await this.quote({
|
|
705
|
+
...params,
|
|
706
|
+
recipient: depositAddress
|
|
707
|
+
});
|
|
708
|
+
}
|
|
709
|
+
/**
|
|
710
|
+
* Execute swap with V2 Router
|
|
711
|
+
* Automatically fetches final quote using receiveUser (depositAddress) to ensure correct routerMsg and signature.
|
|
712
|
+
*/
|
|
713
|
+
async executeSwap(params) {
|
|
714
|
+
try {
|
|
715
|
+
if (!requiresRecipientInExecute(params)) {
|
|
716
|
+
return {
|
|
717
|
+
success: false,
|
|
718
|
+
error: "V2 Router requires sender and receiveUser parameters"
|
|
719
|
+
};
|
|
720
|
+
}
|
|
721
|
+
const { quote, sender, receiveUser } = params;
|
|
722
|
+
if (!quote.success) {
|
|
723
|
+
return {
|
|
724
|
+
success: false,
|
|
725
|
+
error: "Invalid quote"
|
|
726
|
+
};
|
|
727
|
+
}
|
|
728
|
+
if (!receiveUser || receiveUser.trim() === "") {
|
|
729
|
+
return {
|
|
730
|
+
success: false,
|
|
731
|
+
error: "receiveUser (depositAddress) is required"
|
|
732
|
+
};
|
|
733
|
+
}
|
|
734
|
+
if (receiveUser.startsWith("0x") && receiveUser.length === 42) {
|
|
735
|
+
return {
|
|
736
|
+
success: false,
|
|
737
|
+
error: `receiveUser appears to be an EVM address (${receiveUser}). For NEAR chain swaps, depositAddress must be a NEAR account (64 hex chars or .near format)`
|
|
738
|
+
};
|
|
739
|
+
}
|
|
740
|
+
logger.debug("AggregateDexRouter - executeSwap params:", {
|
|
741
|
+
sender,
|
|
742
|
+
receiveUser,
|
|
743
|
+
tokenIn: quote.tokenIn.address,
|
|
744
|
+
tokenOut: quote.tokenOut.address,
|
|
745
|
+
amountIn: quote.amountIn,
|
|
746
|
+
tokens: quote.tokens,
|
|
747
|
+
dexs: quote.dexs
|
|
748
|
+
});
|
|
749
|
+
const slippage = quote.slippage || 5e-3;
|
|
750
|
+
const finalQuoteParams = {
|
|
751
|
+
tokenIn: quote.tokenIn,
|
|
752
|
+
tokenOut: quote.tokenOut,
|
|
753
|
+
amountIn: quote.amountIn,
|
|
754
|
+
slippage,
|
|
755
|
+
sender,
|
|
756
|
+
recipient: receiveUser
|
|
757
|
+
};
|
|
758
|
+
let finalQuote;
|
|
759
|
+
try {
|
|
760
|
+
finalQuote = await this.quote(finalQuoteParams);
|
|
761
|
+
if (!finalQuote.success) {
|
|
762
|
+
return {
|
|
763
|
+
success: false,
|
|
764
|
+
error: `Failed to fetch quote with receiveUser="${receiveUser}": ${finalQuote.error}`
|
|
765
|
+
};
|
|
766
|
+
}
|
|
767
|
+
} catch (error) {
|
|
768
|
+
logger.error("AggregateDexRouter - Failed to fetch quote with receiveUser:", error);
|
|
769
|
+
return {
|
|
770
|
+
success: false,
|
|
771
|
+
error: `Failed to fetch quote with receiveUser="${receiveUser}": ${error?.message || "Unknown error"}`
|
|
772
|
+
};
|
|
773
|
+
}
|
|
774
|
+
const routerMsg = finalQuote.routerMsg;
|
|
775
|
+
const signature = finalQuote.signature;
|
|
776
|
+
if (!routerMsg || !signature) {
|
|
777
|
+
return {
|
|
778
|
+
success: false,
|
|
779
|
+
error: `Quote fetched with receiveUser="${receiveUser}" is missing routerMsg or signature.`
|
|
780
|
+
};
|
|
781
|
+
}
|
|
782
|
+
logger.debug("AggregateDexRouter - Successfully fetched final quote:", {
|
|
783
|
+
receiveUser,
|
|
784
|
+
quoteRecipient: finalQuote.recipient,
|
|
785
|
+
routerMsgLength: routerMsg.length,
|
|
786
|
+
signatureLength: signature.length
|
|
787
|
+
});
|
|
788
|
+
const tokens = finalQuote.tokens || [];
|
|
789
|
+
const dexs = finalQuote.dexs || [];
|
|
790
|
+
const transactions = [];
|
|
791
|
+
const getStorageBalance = async (tokenId, accountId) => {
|
|
792
|
+
try {
|
|
793
|
+
return await this.nearChainAdapter.view({
|
|
794
|
+
contractId: tokenId,
|
|
795
|
+
methodName: "storage_balance_of",
|
|
796
|
+
args: { account_id: accountId }
|
|
797
|
+
});
|
|
798
|
+
} catch (error) {
|
|
799
|
+
return null;
|
|
800
|
+
}
|
|
801
|
+
};
|
|
802
|
+
const isNativeNear = finalQuote.tokenIn.symbol === "NEAR" || finalQuote.tokenIn.address === "near" || !finalQuote.tokenIn.address && finalQuote.tokenIn.symbol === "NEAR";
|
|
803
|
+
if (isNativeNear) {
|
|
804
|
+
const wrapNearStorageBalance = await getStorageBalance(
|
|
805
|
+
this.wrapNearContractId,
|
|
806
|
+
sender
|
|
807
|
+
).catch(() => null);
|
|
808
|
+
if (!wrapNearStorageBalance) {
|
|
809
|
+
transactions.push({
|
|
810
|
+
contractId: this.wrapNearContractId,
|
|
811
|
+
methodName: "storage_deposit",
|
|
812
|
+
args: {
|
|
813
|
+
account_id: sender,
|
|
814
|
+
registration_only: true
|
|
815
|
+
},
|
|
816
|
+
gas: "50000000000000",
|
|
817
|
+
expandDeposit: this.NEW_ACCOUNT_STORAGE_COST
|
|
818
|
+
});
|
|
819
|
+
}
|
|
820
|
+
transactions.push({
|
|
821
|
+
contractId: this.wrapNearContractId,
|
|
822
|
+
methodName: "near_deposit",
|
|
823
|
+
args: {},
|
|
824
|
+
gas: "50000000000000",
|
|
825
|
+
expandDeposit: finalQuote.amountIn
|
|
826
|
+
});
|
|
827
|
+
}
|
|
828
|
+
const tokensToCheck = dexs.length > 1 ? tokens : [finalQuote.tokenOut.address];
|
|
829
|
+
const tokenStorageBalances = await Promise.all(
|
|
830
|
+
tokensToCheck.map(
|
|
831
|
+
(tokenId) => getStorageBalance(tokenId, sender).catch(() => null)
|
|
832
|
+
)
|
|
833
|
+
);
|
|
834
|
+
tokensToCheck.forEach((tokenId, index) => {
|
|
835
|
+
if (!tokenStorageBalances[index]) {
|
|
836
|
+
transactions.push({
|
|
837
|
+
contractId: tokenId,
|
|
838
|
+
methodName: "storage_deposit",
|
|
839
|
+
args: {
|
|
840
|
+
account_id: sender,
|
|
841
|
+
registration_only: true
|
|
842
|
+
},
|
|
843
|
+
gas: "50000000000000",
|
|
844
|
+
expandDeposit: this.NEW_ACCOUNT_STORAGE_COST
|
|
845
|
+
});
|
|
846
|
+
}
|
|
847
|
+
});
|
|
848
|
+
if (receiveUser && receiveUser !== sender) {
|
|
849
|
+
logger.debug("AggregateDexRouter - Checking receiveUser registration in tokenOut:", {
|
|
850
|
+
receiveUser,
|
|
851
|
+
tokenOut: finalQuote.tokenOut.address,
|
|
852
|
+
tokenOutSymbol: finalQuote.tokenOut.symbol
|
|
853
|
+
});
|
|
854
|
+
const receiveUserStorageBalance = await getStorageBalance(
|
|
855
|
+
finalQuote.tokenOut.address,
|
|
856
|
+
receiveUser
|
|
857
|
+
).catch((error) => {
|
|
858
|
+
logger.warn("AggregateDexRouter - Failed to check receiveUser storage balance:", {
|
|
859
|
+
receiveUser,
|
|
860
|
+
tokenOut: finalQuote.tokenOut.address,
|
|
861
|
+
error: error?.message
|
|
862
|
+
});
|
|
863
|
+
return null;
|
|
864
|
+
});
|
|
865
|
+
if (!receiveUserStorageBalance) {
|
|
866
|
+
logger.debug("AggregateDexRouter - receiveUser not registered in tokenOut, adding registration transaction:", {
|
|
867
|
+
receiveUser,
|
|
868
|
+
tokenOut: finalQuote.tokenOut.address,
|
|
869
|
+
tokenOutSymbol: finalQuote.tokenOut.symbol,
|
|
870
|
+
storageCost: this.NEW_ACCOUNT_STORAGE_COST
|
|
871
|
+
});
|
|
872
|
+
transactions.push({
|
|
873
|
+
contractId: finalQuote.tokenOut.address,
|
|
874
|
+
methodName: "storage_deposit",
|
|
875
|
+
args: {
|
|
876
|
+
account_id: receiveUser,
|
|
877
|
+
registration_only: true
|
|
878
|
+
},
|
|
879
|
+
gas: "50000000000000",
|
|
880
|
+
expandDeposit: this.NEW_ACCOUNT_STORAGE_COST
|
|
881
|
+
});
|
|
882
|
+
}
|
|
883
|
+
}
|
|
884
|
+
const aggregateDexStorageBalances = await Promise.all(
|
|
885
|
+
tokens.map(
|
|
886
|
+
(tokenId) => getStorageBalance(tokenId, this.aggregateDexContractId).catch(
|
|
887
|
+
() => null
|
|
888
|
+
)
|
|
889
|
+
)
|
|
890
|
+
);
|
|
891
|
+
tokens.forEach((tokenId, index) => {
|
|
892
|
+
if (!aggregateDexStorageBalances[index]) {
|
|
893
|
+
transactions.push({
|
|
894
|
+
contractId: tokenId,
|
|
895
|
+
methodName: "storage_deposit",
|
|
896
|
+
args: {
|
|
897
|
+
account_id: this.aggregateDexContractId,
|
|
898
|
+
registration_only: true
|
|
899
|
+
},
|
|
900
|
+
gas: "50000000000000",
|
|
901
|
+
expandDeposit: this.NEW_ACCOUNT_STORAGE_COST
|
|
902
|
+
});
|
|
903
|
+
}
|
|
904
|
+
});
|
|
905
|
+
if (tokens.length > 0) {
|
|
906
|
+
const registeredStatus = await this.queryUserTokensRegistered({
|
|
907
|
+
user: sender,
|
|
908
|
+
tokens
|
|
909
|
+
});
|
|
910
|
+
const unregisteredTokens = tokens.filter(
|
|
911
|
+
(_, index) => !registeredStatus[index]
|
|
912
|
+
);
|
|
913
|
+
if (unregisteredTokens.length > 0) {
|
|
914
|
+
const depositPerToken = new Big2("0.005").mul(
|
|
915
|
+
new Big2("1000000000000000000000000")
|
|
916
|
+
);
|
|
917
|
+
const totalDeposit2 = depositPerToken.mul(unregisteredTokens.length);
|
|
918
|
+
transactions.push({
|
|
919
|
+
contractId: this.aggregateDexContractId,
|
|
920
|
+
methodName: "tokens_storage_deposit",
|
|
921
|
+
args: {
|
|
922
|
+
user: sender,
|
|
923
|
+
tokens: unregisteredTokens
|
|
924
|
+
},
|
|
925
|
+
gas: "30000000000000",
|
|
926
|
+
expandDeposit: totalDeposit2.toFixed(0)
|
|
927
|
+
});
|
|
928
|
+
}
|
|
929
|
+
}
|
|
930
|
+
const msgString = JSON.stringify({
|
|
931
|
+
msg: routerMsg,
|
|
932
|
+
signature
|
|
933
|
+
});
|
|
934
|
+
transactions.push({
|
|
935
|
+
contractId: finalQuote.tokenIn.address,
|
|
936
|
+
methodName: "ft_transfer_call",
|
|
937
|
+
args: {
|
|
938
|
+
receiver_id: this.aggregateDexContractId,
|
|
939
|
+
amount: finalQuote.amountIn,
|
|
940
|
+
msg: msgString
|
|
941
|
+
},
|
|
942
|
+
gas: "300000000000000",
|
|
943
|
+
expandDeposit: this.ONE_YOCTO_NEAR
|
|
944
|
+
});
|
|
945
|
+
const totalDeposit = transactions.reduce((sum, tx) => {
|
|
946
|
+
if (tx.expandDeposit) {
|
|
947
|
+
return sum.plus(tx.expandDeposit);
|
|
948
|
+
}
|
|
949
|
+
return sum;
|
|
950
|
+
}, new Big2(0));
|
|
951
|
+
logger.debug("AggregateDexRouter - Executing swap (following mature codebase logic):", {
|
|
952
|
+
contractId: finalQuote.tokenIn.address,
|
|
953
|
+
receiver_id: this.aggregateDexContractId,
|
|
954
|
+
amount: finalQuote.amountIn,
|
|
955
|
+
transactionsCount: transactions.length,
|
|
956
|
+
sender,
|
|
957
|
+
receiveUser,
|
|
958
|
+
tokens: tokens.length,
|
|
959
|
+
dexs: dexs.length,
|
|
960
|
+
totalDepositYocto: totalDeposit.toFixed(0),
|
|
961
|
+
totalDepositNEAR: totalDeposit.div(new Big2("1000000000000000000000000")).toFixed(6),
|
|
962
|
+
transactions: transactions.map((tx, idx) => ({
|
|
963
|
+
index: idx,
|
|
964
|
+
contractId: tx.contractId,
|
|
965
|
+
methodName: tx.methodName,
|
|
966
|
+
expandDeposit: tx.expandDeposit,
|
|
967
|
+
expandDepositNEAR: tx.expandDeposit ? new Big2(tx.expandDeposit).div(new Big2("1000000000000000000000000")).toFixed(6) : "0"
|
|
968
|
+
}))
|
|
969
|
+
});
|
|
970
|
+
const result = await this.nearChainAdapter.call({
|
|
971
|
+
transactions
|
|
972
|
+
});
|
|
973
|
+
if (result.status === "success") {
|
|
974
|
+
return {
|
|
975
|
+
success: true,
|
|
976
|
+
txHash: result.txHash,
|
|
977
|
+
txHashArray: result.txHashArr || (result.txHash ? [result.txHash] : [])
|
|
978
|
+
};
|
|
979
|
+
} else {
|
|
980
|
+
return {
|
|
981
|
+
success: false,
|
|
982
|
+
error: result.message || "Execute swap failed"
|
|
983
|
+
};
|
|
984
|
+
}
|
|
985
|
+
} catch (error) {
|
|
986
|
+
logger.error("AggregateDexRouter executeSwap - Error:", error);
|
|
987
|
+
return {
|
|
988
|
+
success: false,
|
|
989
|
+
error: error?.message || "Execute swap failed"
|
|
990
|
+
};
|
|
991
|
+
}
|
|
992
|
+
}
|
|
993
|
+
/**
|
|
994
|
+
* Query user token registration status in AGGREGATE_DEX contract
|
|
995
|
+
*/
|
|
996
|
+
async queryUserTokensRegistered({
|
|
997
|
+
user,
|
|
998
|
+
tokens
|
|
999
|
+
}) {
|
|
1000
|
+
try {
|
|
1001
|
+
return await this.nearChainAdapter.view({
|
|
1002
|
+
contractId: this.aggregateDexContractId,
|
|
1003
|
+
methodName: "query_user_tokens_registered",
|
|
1004
|
+
args: {
|
|
1005
|
+
user,
|
|
1006
|
+
tokens
|
|
1007
|
+
}
|
|
1008
|
+
});
|
|
1009
|
+
} catch (error) {
|
|
1010
|
+
logger.error(
|
|
1011
|
+
"AggregateDexRouter - Failed to query user tokens registered:",
|
|
1012
|
+
error
|
|
1013
|
+
);
|
|
1014
|
+
return tokens.map(() => false);
|
|
1015
|
+
}
|
|
1016
|
+
}
|
|
1017
|
+
};
|
|
1018
|
+
async function completeQuote(params, config) {
|
|
1019
|
+
const {
|
|
1020
|
+
sourceToken,
|
|
1021
|
+
targetToken,
|
|
1022
|
+
sourceChain,
|
|
1023
|
+
targetChain: _targetChain,
|
|
1024
|
+
// Reserved for future use
|
|
1025
|
+
amountIn,
|
|
1026
|
+
slippage,
|
|
1027
|
+
recipient,
|
|
1028
|
+
refundTo
|
|
1029
|
+
} = params;
|
|
1030
|
+
const {
|
|
1031
|
+
intentsQuotationAdapter,
|
|
1032
|
+
dexRouters,
|
|
1033
|
+
dexRouter,
|
|
1034
|
+
bluechipTokens,
|
|
1035
|
+
configAdapter,
|
|
1036
|
+
currentUserAddress
|
|
1037
|
+
} = config;
|
|
1038
|
+
const wrapNearContractId = configAdapter.getWrapNearContractId();
|
|
1039
|
+
const routers = dexRouters || (dexRouter ? [dexRouter] : []);
|
|
1040
|
+
if (routers.length === 0) {
|
|
1041
|
+
throw new Error("At least one DEX router is required");
|
|
1042
|
+
}
|
|
1043
|
+
const userAddress = currentUserAddress || recipient;
|
|
1044
|
+
if (!userAddress) {
|
|
1045
|
+
throw new Error("currentUserAddress or recipient is required for V2 Router");
|
|
1046
|
+
}
|
|
1047
|
+
if (!sourceToken?.address) {
|
|
1048
|
+
throw new Error("Source token address is required");
|
|
1049
|
+
}
|
|
1050
|
+
if (!targetToken?.address) {
|
|
1051
|
+
throw new Error("Target token address is required");
|
|
1052
|
+
}
|
|
1053
|
+
const needsPreSwap = sourceChain === "near" && !isNearIntentsSupportedToken(sourceToken, bluechipTokens);
|
|
1054
|
+
const bluechipToken = findBestBluechipToken(
|
|
1055
|
+
bluechipTokens,
|
|
1056
|
+
wrapNearContractId
|
|
1057
|
+
);
|
|
1058
|
+
if (!bluechipToken?.address) {
|
|
1059
|
+
logger.error("DEX Aggregator - Failed to find bluechip token:", {
|
|
1060
|
+
bluechipToken,
|
|
1061
|
+
bluechipTokens
|
|
1062
|
+
});
|
|
1063
|
+
throw new Error("Failed to find bluechip token address");
|
|
1064
|
+
}
|
|
1065
|
+
logger.debug("DEX Aggregator - Using bluechip token:", {
|
|
1066
|
+
address: bluechipToken.address,
|
|
1067
|
+
symbol: bluechipToken.symbol,
|
|
1068
|
+
decimals: bluechipToken.decimals
|
|
1069
|
+
});
|
|
1070
|
+
let preSwapQuote = null;
|
|
1071
|
+
let bestRouter = null;
|
|
1072
|
+
if (needsPreSwap) {
|
|
1073
|
+
if (!sourceToken?.address) {
|
|
1074
|
+
throw new Error("Source token address is required");
|
|
1075
|
+
}
|
|
1076
|
+
logger.debug("DEX Aggregator - Pre-swap quote params:", {
|
|
1077
|
+
tokenIn: {
|
|
1078
|
+
address: sourceToken.address,
|
|
1079
|
+
symbol: sourceToken.symbol
|
|
1080
|
+
},
|
|
1081
|
+
tokenOut: {
|
|
1082
|
+
address: bluechipToken.address,
|
|
1083
|
+
symbol: bluechipToken.symbol
|
|
1084
|
+
},
|
|
1085
|
+
amountIn,
|
|
1086
|
+
slippage,
|
|
1087
|
+
routersCount: routers.length,
|
|
1088
|
+
userAddress
|
|
1089
|
+
});
|
|
1090
|
+
const quotes = await Promise.allSettled(
|
|
1091
|
+
routers.map((router) => {
|
|
1092
|
+
const capabilities = router.getCapabilities();
|
|
1093
|
+
const quoteParams = capabilities.requiresRecipient ? {
|
|
1094
|
+
tokenIn: sourceToken,
|
|
1095
|
+
tokenOut: bluechipToken,
|
|
1096
|
+
amountIn,
|
|
1097
|
+
slippage,
|
|
1098
|
+
swapType: "EXACT_INPUT",
|
|
1099
|
+
sender: userAddress,
|
|
1100
|
+
recipient: userAddress
|
|
1101
|
+
} : {
|
|
1102
|
+
tokenIn: sourceToken,
|
|
1103
|
+
tokenOut: bluechipToken,
|
|
1104
|
+
amountIn,
|
|
1105
|
+
slippage,
|
|
1106
|
+
swapType: "EXACT_INPUT"
|
|
1107
|
+
};
|
|
1108
|
+
return router.quote(quoteParams);
|
|
1109
|
+
})
|
|
1110
|
+
);
|
|
1111
|
+
const validQuotes = quotes.filter(
|
|
1112
|
+
(r) => r.status === "fulfilled" && r.value.success
|
|
1113
|
+
).map((r) => r.value);
|
|
1114
|
+
if (validQuotes.length === 0) {
|
|
1115
|
+
const errors = quotes.map((r, index) => {
|
|
1116
|
+
if (r.status === "rejected") {
|
|
1117
|
+
return `Router ${index}: ${r.reason}`;
|
|
1118
|
+
}
|
|
1119
|
+
if (r.status === "fulfilled" && !r.value.success) {
|
|
1120
|
+
return `Router ${index}: ${r.value.error}`;
|
|
1121
|
+
}
|
|
1122
|
+
return null;
|
|
1123
|
+
}).filter(Boolean);
|
|
1124
|
+
logger.error("DEX Aggregator - All router quotes failed:", errors);
|
|
1125
|
+
throw new Error(
|
|
1126
|
+
`All router quotes failed: ${errors.join("; ")}`
|
|
1127
|
+
);
|
|
1128
|
+
}
|
|
1129
|
+
const bestQuote = validQuotes.reduce((best, current) => {
|
|
1130
|
+
const bestAmount = new Big2(best.amountOut);
|
|
1131
|
+
const currentAmount = new Big2(current.amountOut);
|
|
1132
|
+
return currentAmount.gt(bestAmount) ? current : best;
|
|
1133
|
+
});
|
|
1134
|
+
const bestQuoteIndex = validQuotes.indexOf(bestQuote);
|
|
1135
|
+
bestRouter = routers[bestQuoteIndex];
|
|
1136
|
+
preSwapQuote = bestQuote;
|
|
1137
|
+
logger.debug("DEX Aggregator - Selected best router:", {
|
|
1138
|
+
routerIndex: bestQuoteIndex,
|
|
1139
|
+
amountOut: bestQuote.amountOut,
|
|
1140
|
+
routerType: bestRouter.getCapabilities().requiresRecipient ? "V2 (Recipient)" : "V1 (Simple)"
|
|
1141
|
+
});
|
|
1142
|
+
const preSwapAmountOut = preSwapQuote.amountOut;
|
|
1143
|
+
if (!preSwapAmountOut || new Big2(preSwapAmountOut).lte(0)) {
|
|
1144
|
+
logger.error("DEX Aggregator - Pre-swap amountOut is invalid:", {
|
|
1145
|
+
amountOut: preSwapAmountOut,
|
|
1146
|
+
tokenIn: sourceToken,
|
|
1147
|
+
tokenOut: bluechipToken
|
|
1148
|
+
});
|
|
1149
|
+
throw new Error(
|
|
1150
|
+
"Pre-swap returned invalid amount: amount is too small or zero"
|
|
1151
|
+
);
|
|
1152
|
+
}
|
|
1153
|
+
logger.debug("DEX Aggregator - Pre-swap quote success:", {
|
|
1154
|
+
amountOut: preSwapAmountOut,
|
|
1155
|
+
tokenOut: bluechipToken.symbol,
|
|
1156
|
+
decimals: bluechipToken.decimals
|
|
1157
|
+
});
|
|
1158
|
+
}
|
|
1159
|
+
let normalizedSourceAsset;
|
|
1160
|
+
if (needsPreSwap) {
|
|
1161
|
+
const bluechipKey = bluechipToken.symbol?.toUpperCase() === "WNEAR" ? "NEAR" : bluechipToken.symbol?.toUpperCase();
|
|
1162
|
+
const bluechipTokenConfig = bluechipKey && bluechipTokens[bluechipKey] || void 0;
|
|
1163
|
+
if (bluechipTokenConfig?.assetId) {
|
|
1164
|
+
normalizedSourceAsset = bluechipTokenConfig.assetId;
|
|
1165
|
+
logger.debug("Using bluechip token assetId for NearIntents:", {
|
|
1166
|
+
symbol: bluechipToken.symbol,
|
|
1167
|
+
assetId: normalizedSourceAsset,
|
|
1168
|
+
contractAddress: bluechipToken.address
|
|
1169
|
+
});
|
|
1170
|
+
} else {
|
|
1171
|
+
normalizedSourceAsset = `nep141:${bluechipToken.address}`;
|
|
1172
|
+
logger.warn(
|
|
1173
|
+
"Bluechip token assetId not found, using contractAddress with prefix:",
|
|
1174
|
+
{
|
|
1175
|
+
symbol: bluechipToken.symbol,
|
|
1176
|
+
normalizedSourceAsset
|
|
1177
|
+
}
|
|
1178
|
+
);
|
|
1179
|
+
}
|
|
1180
|
+
} else {
|
|
1181
|
+
if (sourceToken.symbol) {
|
|
1182
|
+
const sourceKey = sourceToken.symbol.toUpperCase();
|
|
1183
|
+
const sourceTokenConfig = bluechipTokens[sourceKey];
|
|
1184
|
+
if (sourceTokenConfig?.assetId) {
|
|
1185
|
+
normalizedSourceAsset = sourceTokenConfig.assetId;
|
|
1186
|
+
} else {
|
|
1187
|
+
normalizedSourceAsset = normalizeTokenId(
|
|
1188
|
+
sourceToken.address,
|
|
1189
|
+
wrapNearContractId
|
|
1190
|
+
);
|
|
1191
|
+
if (!normalizedSourceAsset.startsWith("nep141:")) {
|
|
1192
|
+
normalizedSourceAsset = `nep141:${normalizedSourceAsset}`;
|
|
1193
|
+
}
|
|
1194
|
+
}
|
|
1195
|
+
} else {
|
|
1196
|
+
normalizedSourceAsset = normalizeTokenId(
|
|
1197
|
+
sourceToken.address,
|
|
1198
|
+
wrapNearContractId
|
|
1199
|
+
);
|
|
1200
|
+
if (!normalizedSourceAsset.startsWith("nep141:")) {
|
|
1201
|
+
normalizedSourceAsset = `nep141:${normalizedSourceAsset}`;
|
|
1202
|
+
}
|
|
1203
|
+
}
|
|
1204
|
+
}
|
|
1205
|
+
let normalizedTargetAsset = targetToken.address;
|
|
1206
|
+
if (normalizedTargetAsset && !normalizedTargetAsset.startsWith("nep141:") && !normalizedTargetAsset.startsWith("nep245:") && normalizedTargetAsset.includes(".")) {
|
|
1207
|
+
normalizedTargetAsset = `nep141:${normalizeTokenId(
|
|
1208
|
+
normalizedTargetAsset,
|
|
1209
|
+
wrapNearContractId
|
|
1210
|
+
)}`;
|
|
1211
|
+
}
|
|
1212
|
+
normalizedTargetAsset = normalizeDestinationAsset(normalizedTargetAsset, wrapNearContractId) || normalizedTargetAsset;
|
|
1213
|
+
const slippageBps = convertSlippageToBasisPoints(slippage);
|
|
1214
|
+
const intentsAmount = needsPreSwap ? preSwapQuote.amountOut : amountIn;
|
|
1215
|
+
logger.debug("DEX Aggregator - Calling NearIntents quotation:", {
|
|
1216
|
+
originAsset: normalizedSourceAsset,
|
|
1217
|
+
destinationAsset: normalizedTargetAsset,
|
|
1218
|
+
amount: intentsAmount,
|
|
1219
|
+
needsPreSwap,
|
|
1220
|
+
preSwapAmountOut: needsPreSwap ? preSwapQuote.amountOut : void 0
|
|
1221
|
+
});
|
|
1222
|
+
const swapTypeForIntents = needsPreSwap ? "FLEX_INPUT" : void 0;
|
|
1223
|
+
logger.debug("DEX Aggregator - swapType for NearIntents:", {
|
|
1224
|
+
needsPreSwap,
|
|
1225
|
+
swapType: swapTypeForIntents || "EXACT_INPUT (default)"
|
|
1226
|
+
});
|
|
1227
|
+
const intentsQuote = await intentsQuotationAdapter.quote({
|
|
1228
|
+
originAsset: normalizedSourceAsset,
|
|
1229
|
+
destinationAsset: normalizedTargetAsset,
|
|
1230
|
+
amount: intentsAmount,
|
|
1231
|
+
refundTo: refundTo || recipient,
|
|
1232
|
+
recipient,
|
|
1233
|
+
slippageTolerance: slippageBps,
|
|
1234
|
+
swapType: swapTypeForIntents
|
|
1235
|
+
});
|
|
1236
|
+
logger.debug("DEX Aggregator - NearIntents quotation result:", {
|
|
1237
|
+
quoteStatus: intentsQuote.quoteStatus,
|
|
1238
|
+
message: intentsQuote.message,
|
|
1239
|
+
hasDepositAddress: !!intentsQuote.quoteSuccessResult?.quote?.depositAddress
|
|
1240
|
+
});
|
|
1241
|
+
if (intentsQuote.quoteStatus !== "success") {
|
|
1242
|
+
const errorMessage = intentsQuote.message || "Unknown error";
|
|
1243
|
+
logger.error("DEX Aggregator - NearIntents quote failed:", {
|
|
1244
|
+
error: errorMessage,
|
|
1245
|
+
originAsset: normalizedSourceAsset,
|
|
1246
|
+
destinationAsset: normalizedTargetAsset,
|
|
1247
|
+
amount: intentsAmount,
|
|
1248
|
+
needsPreSwap,
|
|
1249
|
+
preSwapAmountOut: needsPreSwap ? preSwapQuote.amountOut : void 0
|
|
1250
|
+
});
|
|
1251
|
+
throw new Error(`Intents quote failed: ${errorMessage}`);
|
|
1252
|
+
}
|
|
1253
|
+
const depositAddress = intentsQuote.quoteSuccessResult?.quote?.depositAddress || "";
|
|
1254
|
+
if (!depositAddress) {
|
|
1255
|
+
throw new Error("Deposit address not found in intents quote");
|
|
1256
|
+
}
|
|
1257
|
+
const finalQuote = preSwapQuote;
|
|
1258
|
+
return {
|
|
1259
|
+
intents: {
|
|
1260
|
+
quote: intentsQuote,
|
|
1261
|
+
depositAddress
|
|
1262
|
+
},
|
|
1263
|
+
preSwap: needsPreSwap && finalQuote && bestRouter ? {
|
|
1264
|
+
quote: finalQuote,
|
|
1265
|
+
tokenIn: sourceToken,
|
|
1266
|
+
tokenOut: bluechipToken,
|
|
1267
|
+
executor: bestRouter
|
|
1268
|
+
} : void 0,
|
|
1269
|
+
finalAmountOut: intentsQuote.quoteSuccessResult?.quote?.amountOut || "0"
|
|
1270
|
+
};
|
|
1271
|
+
}
|
|
1272
|
+
|
|
1273
|
+
export { AggregateDexRouter, NearSmartRouter, completeQuote, convertSlippageToBasisPoints, findBestBluechipToken, formatGasString, formatGasToTgas, getBluechipTokensConfig, isNearIntentsSupportedToken, logger, normalizeDestinationAsset, normalizeTokenId, requiresRecipient, requiresRecipientInExecute, setBluechipTokensConfig };
|
|
1274
|
+
//# sourceMappingURL=index.mjs.map
|
|
1275
|
+
//# sourceMappingURL=index.mjs.map
|