@peachprojects/aggregator-sdk 0.0.0-experimental-20260324105259
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 +275 -0
- package/dist/index.cjs +4 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.ts +681 -0
- package/dist/index.mjs +1098 -0
- package/dist/index.mjs.map +1 -0
- package/package.json +70 -0
package/dist/index.mjs
ADDED
|
@@ -0,0 +1,1098 @@
|
|
|
1
|
+
import { ethers as h } from "ethers";
|
|
2
|
+
const X = 50, y = 10000n, T = 1200, D = 6e4, x = [50, 100, 200, 400, 800, 1200], V = "0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE";
|
|
3
|
+
function O(m) {
|
|
4
|
+
return m.toLowerCase() === V.toLowerCase();
|
|
5
|
+
}
|
|
6
|
+
const tt = 4001;
|
|
7
|
+
var g = /* @__PURE__ */ ((m) => (m.PancakeV2 = "PancakeV2", m.PancakeV3 = "PancakeV3", m.UniswapV3 = "UniswapV3", m.Dodo = "Dodo", m.Thena = "Thena", m))(g || {});
|
|
8
|
+
const et = {
|
|
9
|
+
chainId: 56,
|
|
10
|
+
rpcUrl: "https://bsc-dataseed.binance.org",
|
|
11
|
+
weth: "0xbb4CdB9CBd36B01bD1cBaEBF2De08d9173bc095c",
|
|
12
|
+
// WBNB
|
|
13
|
+
adapters: []
|
|
14
|
+
}, nt = {
|
|
15
|
+
chainId: 97,
|
|
16
|
+
rpcUrl: "https://bsc-testnet-rpc.publicnode.com",
|
|
17
|
+
weth: "0xae13d989daC2f0dEbFf460aC112a837C89BAa7cd",
|
|
18
|
+
// WBNB Testnet
|
|
19
|
+
adapters: []
|
|
20
|
+
};
|
|
21
|
+
class $ extends Error {
|
|
22
|
+
constructor(t, e, n) {
|
|
23
|
+
super(t), this.name = "ExecuteTimeoutError", this.stage = e, this.txHash = n;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
const k = {
|
|
27
|
+
/** Default route search depth */
|
|
28
|
+
depth: 3,
|
|
29
|
+
/** Default trade split count */
|
|
30
|
+
splitCount: 20,
|
|
31
|
+
/** Default DEX providers */
|
|
32
|
+
providers: ["PANCAKEV2", "PANCAKEV3", "UNISWAPV3", "DODO", "THENA"],
|
|
33
|
+
/** Default client version for V3 API */
|
|
34
|
+
clientVersion: 1001500
|
|
35
|
+
}, M = 1e4;
|
|
36
|
+
class U {
|
|
37
|
+
constructor(t) {
|
|
38
|
+
if (!t.baseUrl)
|
|
39
|
+
throw new Error("ApiClient requires a baseUrl");
|
|
40
|
+
this.baseUrl = t.baseUrl, this.timeout = t.timeout || M;
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Find optimal routes via API
|
|
44
|
+
*/
|
|
45
|
+
async findRoutes(t) {
|
|
46
|
+
const {
|
|
47
|
+
from: e,
|
|
48
|
+
target: n,
|
|
49
|
+
amount: o,
|
|
50
|
+
byAmountIn: s = !0,
|
|
51
|
+
depth: r = k.depth,
|
|
52
|
+
splitCount: a = k.splitCount,
|
|
53
|
+
providers: i = k.providers
|
|
54
|
+
} = t, c = new URLSearchParams({
|
|
55
|
+
from: e,
|
|
56
|
+
target: n,
|
|
57
|
+
amount: o.toString(),
|
|
58
|
+
by_amount_in: s.toString(),
|
|
59
|
+
depth: r.toString(),
|
|
60
|
+
split_count: a.toString(),
|
|
61
|
+
providers: i.join(","),
|
|
62
|
+
v: k.clientVersion.toString()
|
|
63
|
+
}), u = `${this.baseUrl}/router/find_routes?${c}`, d = new AbortController(), l = setTimeout(() => d.abort(), this.timeout);
|
|
64
|
+
try {
|
|
65
|
+
const f = await fetch(u, {
|
|
66
|
+
method: "GET",
|
|
67
|
+
headers: {
|
|
68
|
+
Accept: "application/json"
|
|
69
|
+
},
|
|
70
|
+
signal: d.signal
|
|
71
|
+
});
|
|
72
|
+
if (clearTimeout(l), !f.ok)
|
|
73
|
+
throw new w(
|
|
74
|
+
`API request failed: ${f.status} ${f.statusText}`,
|
|
75
|
+
f.status
|
|
76
|
+
);
|
|
77
|
+
const v = await f.json();
|
|
78
|
+
if (v.code !== 200)
|
|
79
|
+
throw new w(v.msg || "Route not found", v.code);
|
|
80
|
+
if (!v.data || !v.data.paths || v.data.paths.length === 0)
|
|
81
|
+
throw new w("No routes found", 404);
|
|
82
|
+
return v.data;
|
|
83
|
+
} catch (f) {
|
|
84
|
+
throw clearTimeout(l), f instanceof w ? f : f instanceof Error ? f.name === "AbortError" ? new w("API request timeout", 408) : new w(`API request failed: ${f.message}`, 0) : new w("Unknown API error", 0);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
/**
|
|
88
|
+
* Get service status including available providers
|
|
89
|
+
*/
|
|
90
|
+
async getStatus() {
|
|
91
|
+
const t = `${this.baseUrl}/router/status`, e = new AbortController(), n = setTimeout(() => e.abort(), this.timeout);
|
|
92
|
+
try {
|
|
93
|
+
const o = await fetch(t, {
|
|
94
|
+
method: "GET",
|
|
95
|
+
headers: {
|
|
96
|
+
Accept: "application/json"
|
|
97
|
+
},
|
|
98
|
+
signal: e.signal
|
|
99
|
+
});
|
|
100
|
+
if (clearTimeout(n), !o.ok)
|
|
101
|
+
throw new w(
|
|
102
|
+
`API request failed: ${o.status} ${o.statusText}`,
|
|
103
|
+
o.status
|
|
104
|
+
);
|
|
105
|
+
const s = await o.json();
|
|
106
|
+
if (s.code !== 200)
|
|
107
|
+
throw new w(s.msg || "Failed to get status", s.code);
|
|
108
|
+
return s.data;
|
|
109
|
+
} catch (o) {
|
|
110
|
+
throw clearTimeout(n), o instanceof w ? o : o instanceof Error ? o.name === "AbortError" ? new w("API request timeout", 408) : new w(`API request failed: ${o.message}`, 0) : new w("Unknown API error", 0);
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
/**
|
|
114
|
+
* Get list of available providers
|
|
115
|
+
*/
|
|
116
|
+
async getAvailableProviders() {
|
|
117
|
+
return (await this.getStatus()).providers;
|
|
118
|
+
}
|
|
119
|
+
/**
|
|
120
|
+
* Update API base URL
|
|
121
|
+
*/
|
|
122
|
+
setBaseUrl(t) {
|
|
123
|
+
this.baseUrl = t;
|
|
124
|
+
}
|
|
125
|
+
/**
|
|
126
|
+
* Get current API base URL
|
|
127
|
+
*/
|
|
128
|
+
getBaseUrl() {
|
|
129
|
+
return this.baseUrl;
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
class w extends Error {
|
|
133
|
+
constructor(t, e) {
|
|
134
|
+
super(t), this.name = "ApiError", this.code = e;
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
async function b(m, t = D) {
|
|
138
|
+
if (t <= 0)
|
|
139
|
+
return m;
|
|
140
|
+
let e;
|
|
141
|
+
try {
|
|
142
|
+
return await Promise.race([
|
|
143
|
+
m,
|
|
144
|
+
new Promise((n, o) => {
|
|
145
|
+
e = setTimeout(() => {
|
|
146
|
+
o(
|
|
147
|
+
new $(
|
|
148
|
+
`Wallet did not settle sendTransaction within ${t}ms.`,
|
|
149
|
+
"wallet_send"
|
|
150
|
+
)
|
|
151
|
+
);
|
|
152
|
+
}, t);
|
|
153
|
+
})
|
|
154
|
+
]);
|
|
155
|
+
} finally {
|
|
156
|
+
e && clearTimeout(e);
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
const B = [
|
|
160
|
+
"function swap((address srcToken, address dstToken, uint256 amountIn, uint256 amountOutMin, (address adapter, address pool, address tokenIn, address tokenOut, uint256 amountIn, bytes extraData)[] steps, address[] intermediateTokens, uint256 deadline, bytes32 quoteId, uint256 expectAmountOut) params) external returns (uint256 amountOut)",
|
|
161
|
+
"function swapETH((address srcToken, address dstToken, uint256 amountIn, uint256 amountOutMin, (address adapter, address pool, address tokenIn, address tokenOut, uint256 amountIn, bytes extraData)[] steps, address[] intermediateTokens, uint256 deadline, bytes32 quoteId, uint256 expectAmountOut) params) external payable returns (uint256 amountOut)",
|
|
162
|
+
"function isAdapterRegistered(address adapter) external view returns (bool)",
|
|
163
|
+
"function WETH() external view returns (address)"
|
|
164
|
+
], E = [
|
|
165
|
+
"function approve(address spender, uint256 amount) external returns (bool)",
|
|
166
|
+
"function allowance(address owner, address spender) external view returns (uint256)",
|
|
167
|
+
"function balanceOf(address account) external view returns (uint256)",
|
|
168
|
+
"function decimals() external view returns (uint8)",
|
|
169
|
+
"function symbol() external view returns (string)"
|
|
170
|
+
], R = {
|
|
171
|
+
PANCAKEV3: g.PancakeV3,
|
|
172
|
+
PANCAKEV2: g.PancakeV2,
|
|
173
|
+
UNISWAPV3: g.UniswapV3,
|
|
174
|
+
DODO: g.Dodo,
|
|
175
|
+
THENA: g.Thena
|
|
176
|
+
};
|
|
177
|
+
class ot {
|
|
178
|
+
constructor(t, e, n) {
|
|
179
|
+
this.config = t, this.provider = e || new h.JsonRpcProvider(t.rpcUrl), this.routerContract = new h.Contract(
|
|
180
|
+
t.routerAddress || h.ZeroAddress,
|
|
181
|
+
B,
|
|
182
|
+
this.provider
|
|
183
|
+
), this.apiClient = n?.api ? new U(n.api) : null;
|
|
184
|
+
}
|
|
185
|
+
/**
|
|
186
|
+
* Get the effective router address for a quote.
|
|
187
|
+
*/
|
|
188
|
+
getRouterAddress(t) {
|
|
189
|
+
const e = t.routerAddress || this.config.routerAddress;
|
|
190
|
+
if (!e || e === h.ZeroAddress)
|
|
191
|
+
throw new Error("No router address available. Provide routerAddress in config or use API-based getQuote.");
|
|
192
|
+
return e;
|
|
193
|
+
}
|
|
194
|
+
/**
|
|
195
|
+
* Apply slippage to swap params, returning a new SwapParams with adjusted amountOutMin
|
|
196
|
+
*/
|
|
197
|
+
applySlippage(t, e) {
|
|
198
|
+
if (e < 0 || e > 1e4)
|
|
199
|
+
throw new Error("slippageBps must be between 0 and 10000");
|
|
200
|
+
const n = t.amountOutMin * (y - BigInt(e)) / y;
|
|
201
|
+
return { ...t, amountOutMin: n };
|
|
202
|
+
}
|
|
203
|
+
/**
|
|
204
|
+
* Build transaction requests for an approval (if needed) and the swap itself.
|
|
205
|
+
*/
|
|
206
|
+
async swap(t, e, n) {
|
|
207
|
+
const o = this.getRouterAddress(t), { tx: s, method: r } = this.buildSwapTransactionRequest(t, n);
|
|
208
|
+
let a;
|
|
209
|
+
return t.srcNative || (a = await this.buildApprovalRequest(
|
|
210
|
+
t.srcToken,
|
|
211
|
+
e,
|
|
212
|
+
t.amountIn,
|
|
213
|
+
o,
|
|
214
|
+
n
|
|
215
|
+
)), {
|
|
216
|
+
routerAddress: o,
|
|
217
|
+
method: r,
|
|
218
|
+
tx: s,
|
|
219
|
+
approval: a
|
|
220
|
+
};
|
|
221
|
+
}
|
|
222
|
+
/**
|
|
223
|
+
* Execute swap using the legacy signer-managed flow.
|
|
224
|
+
*
|
|
225
|
+
* @deprecated Prefer swap(), then send the returned tx request with your wallet/client.
|
|
226
|
+
*/
|
|
227
|
+
async execute(t, e, n) {
|
|
228
|
+
const o = await e.getAddress(), s = await this.swap(t, o, n);
|
|
229
|
+
try {
|
|
230
|
+
return s.approval && await (await this.sendTransactionWithTimeout(
|
|
231
|
+
e,
|
|
232
|
+
s.approval.tx,
|
|
233
|
+
n
|
|
234
|
+
)).wait(), await this.sendTransactionWithTimeout(e, s.tx, n);
|
|
235
|
+
} catch (r) {
|
|
236
|
+
const a = r instanceof Error ? r.message : String(r), i = /estimateGas/i.test(a), c = /missing revert data/i.test(a) || a.includes("reason=null") && a.includes("data=null");
|
|
237
|
+
if (i && (c || /reason=null|data=null/.test(a))) {
|
|
238
|
+
const u = "Transaction reverted during gas estimation and the RPC did not return a revert reason. Try: 1) Get a fresh quote and confirm immediately 2) Switch network or RPC 3) Increase slippage.", d = new Error(`${u} (estimateGas/missing revert data)`);
|
|
239
|
+
throw d.cause = r, d;
|
|
240
|
+
}
|
|
241
|
+
throw r;
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
/**
|
|
245
|
+
* Encode parameters to contract format
|
|
246
|
+
*/
|
|
247
|
+
encodeParams(t) {
|
|
248
|
+
return {
|
|
249
|
+
srcToken: t.srcToken,
|
|
250
|
+
dstToken: t.dstToken,
|
|
251
|
+
amountIn: t.amountIn,
|
|
252
|
+
amountOutMin: t.amountOutMin,
|
|
253
|
+
steps: t.steps.map((e) => ({
|
|
254
|
+
adapter: e.adapter,
|
|
255
|
+
pool: e.pool,
|
|
256
|
+
tokenIn: e.tokenIn,
|
|
257
|
+
tokenOut: e.tokenOut,
|
|
258
|
+
amountIn: e.amountIn,
|
|
259
|
+
extraData: e.extraData
|
|
260
|
+
})),
|
|
261
|
+
intermediateTokens: t.intermediateTokens,
|
|
262
|
+
deadline: t.deadline,
|
|
263
|
+
quoteId: t.quoteId,
|
|
264
|
+
expectAmountOut: t.expectAmountOut
|
|
265
|
+
};
|
|
266
|
+
}
|
|
267
|
+
getProtocolForProvider(t) {
|
|
268
|
+
if (t in R)
|
|
269
|
+
return R[t];
|
|
270
|
+
throw new Error(`Unsupported provider: ${t}`);
|
|
271
|
+
}
|
|
272
|
+
/**
|
|
273
|
+
* Encode swap calldata for the Peach Aggregator contract
|
|
274
|
+
* Useful for simulation or building custom transactions
|
|
275
|
+
*
|
|
276
|
+
* @param quote - Quote object from getQuote
|
|
277
|
+
* @param slippageBps - Slippage tolerance in basis points (e.g. 50 = 0.5%). Required.
|
|
278
|
+
* @returns Encoded calldata and transaction info (to address, value)
|
|
279
|
+
*/
|
|
280
|
+
encodeSwapCalldata(t, e) {
|
|
281
|
+
const n = t.routerAddress ?? this.config.routerAddress ?? this.routerContract.target, o = this.applySlippage(t.params, e), s = t.srcNative === !0, r = this.encodeParams(o);
|
|
282
|
+
if (s) {
|
|
283
|
+
const a = this.routerContract.interface.encodeFunctionData("swapETH", [r]);
|
|
284
|
+
return {
|
|
285
|
+
to: n,
|
|
286
|
+
data: a,
|
|
287
|
+
value: t.amountIn,
|
|
288
|
+
method: "swapETH"
|
|
289
|
+
};
|
|
290
|
+
} else {
|
|
291
|
+
const a = this.routerContract.interface.encodeFunctionData("swap", [r]);
|
|
292
|
+
return {
|
|
293
|
+
to: n,
|
|
294
|
+
data: a,
|
|
295
|
+
value: 0n,
|
|
296
|
+
method: "swap"
|
|
297
|
+
};
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
buildSwapTransactionRequest(t, e) {
|
|
301
|
+
const { to: n, data: o, value: s, method: r } = this.encodeSwapCalldata(t, e.slippageBps);
|
|
302
|
+
return {
|
|
303
|
+
method: r,
|
|
304
|
+
tx: this.applyTxOverrides({ to: n, data: o, value: s }, e, !0)
|
|
305
|
+
};
|
|
306
|
+
}
|
|
307
|
+
async buildApprovalRequest(t, e, n, o, s) {
|
|
308
|
+
const r = await this.getAllowance(t, e, o);
|
|
309
|
+
if (!(r >= n))
|
|
310
|
+
return {
|
|
311
|
+
token: t,
|
|
312
|
+
owner: e,
|
|
313
|
+
spender: o,
|
|
314
|
+
currentAllowance: r,
|
|
315
|
+
requiredAmount: n,
|
|
316
|
+
approveAmount: h.MaxUint256,
|
|
317
|
+
tx: this.buildApprovalTransactionRequest(t, o, s)
|
|
318
|
+
};
|
|
319
|
+
}
|
|
320
|
+
buildApprovalTransactionRequest(t, e, n) {
|
|
321
|
+
const s = new h.Interface(E).encodeFunctionData("approve", [e, h.MaxUint256]);
|
|
322
|
+
return this.applyTxOverrides({ to: t, data: s, value: 0n }, n, !1);
|
|
323
|
+
}
|
|
324
|
+
async getAllowance(t, e, n) {
|
|
325
|
+
return new h.Contract(t, E, this.provider).allowance(e, n);
|
|
326
|
+
}
|
|
327
|
+
applyTxOverrides(t, e, n) {
|
|
328
|
+
const o = { ...t };
|
|
329
|
+
return e.gasPrice && (o.gasPrice = e.gasPrice), n && e.gasLimit && (o.gasLimit = e.gasLimit), o;
|
|
330
|
+
}
|
|
331
|
+
async sendTransactionWithTimeout(t, e, n) {
|
|
332
|
+
const o = n.timeoutMs ?? D;
|
|
333
|
+
if (o <= 0)
|
|
334
|
+
return t.sendTransaction(e);
|
|
335
|
+
const s = t;
|
|
336
|
+
if (typeof s.sendUncheckedTransaction == "function" && s.provider) {
|
|
337
|
+
const r = await b(
|
|
338
|
+
s.sendUncheckedTransaction(e),
|
|
339
|
+
o
|
|
340
|
+
), a = await this.waitForTransactionResponse(
|
|
341
|
+
s.provider,
|
|
342
|
+
r,
|
|
343
|
+
o,
|
|
344
|
+
n.transactionResponsePollingIntervalsMs
|
|
345
|
+
);
|
|
346
|
+
if (a.response)
|
|
347
|
+
return a.response;
|
|
348
|
+
const i = a.rpcErrors > 0 ? `${a.rpcErrors} transient provider error(s) and ${a.nullResponses} null response(s)` : `${a.nullResponses} null response(s)`;
|
|
349
|
+
throw new $(
|
|
350
|
+
`Transaction was broadcast but provider did not return TransactionResponse within ${o}ms (${i}).`,
|
|
351
|
+
"provider_index",
|
|
352
|
+
r
|
|
353
|
+
);
|
|
354
|
+
}
|
|
355
|
+
return b(t.sendTransaction(e), o);
|
|
356
|
+
}
|
|
357
|
+
async waitForTransactionResponse(t, e, n, o = x) {
|
|
358
|
+
const s = Date.now() + n;
|
|
359
|
+
let r = 0, a = 0, i = 0;
|
|
360
|
+
for (; Date.now() < s; ) {
|
|
361
|
+
try {
|
|
362
|
+
const u = await t.getTransaction(e);
|
|
363
|
+
if (u)
|
|
364
|
+
return { response: u, nullResponses: a, rpcErrors: i };
|
|
365
|
+
a++;
|
|
366
|
+
} catch {
|
|
367
|
+
i++;
|
|
368
|
+
}
|
|
369
|
+
const c = this.getNextPollingDelay(o, r);
|
|
370
|
+
r++, await this.delay(Math.min(c, Math.max(25, s - Date.now())));
|
|
371
|
+
}
|
|
372
|
+
return { response: null, nullResponses: a, rpcErrors: i };
|
|
373
|
+
}
|
|
374
|
+
async delay(t) {
|
|
375
|
+
t <= 0 || await new Promise((e) => {
|
|
376
|
+
setTimeout(e, t);
|
|
377
|
+
});
|
|
378
|
+
}
|
|
379
|
+
getNextPollingDelay(t, e) {
|
|
380
|
+
return t.length === 0 ? x.at(-1) ?? 1200 : t[Math.min(e, t.length - 1)] ?? 1200;
|
|
381
|
+
}
|
|
382
|
+
/**
|
|
383
|
+
* Get token metadata and, optionally, the balance for a specific owner.
|
|
384
|
+
*/
|
|
385
|
+
async getTokenInfo(t, e) {
|
|
386
|
+
const n = new h.Contract(t, E, this.provider), [o, s, r] = await Promise.all([
|
|
387
|
+
n.symbol(),
|
|
388
|
+
n.decimals(),
|
|
389
|
+
e ? n.balanceOf(e) : Promise.resolve(void 0)
|
|
390
|
+
]);
|
|
391
|
+
return r === void 0 ? { symbol: o, decimals: s } : { symbol: o, decimals: s, balance: r };
|
|
392
|
+
}
|
|
393
|
+
/**
|
|
394
|
+
* Get user token balance
|
|
395
|
+
*/
|
|
396
|
+
async getBalance(t, e) {
|
|
397
|
+
return new h.Contract(t, E, this.provider).balanceOf(e);
|
|
398
|
+
}
|
|
399
|
+
/**
|
|
400
|
+
* Get quote via API
|
|
401
|
+
* @throws Error if API client is not configured
|
|
402
|
+
*/
|
|
403
|
+
async getQuote(t) {
|
|
404
|
+
const { srcToken: e, dstToken: n, amountIn: o, options: s = {} } = t, {
|
|
405
|
+
byAmountIn: r = !0,
|
|
406
|
+
depth: a = k.depth,
|
|
407
|
+
splitCount: i = k.splitCount,
|
|
408
|
+
providers: c = k.providers,
|
|
409
|
+
deadlineSeconds: u = T
|
|
410
|
+
} = s;
|
|
411
|
+
if (!this.apiClient)
|
|
412
|
+
throw new Error("API client is not configured. getQuote() requires PeachClientOptions.api.");
|
|
413
|
+
const d = O(e), l = O(n), f = d ? this.config.weth : e, v = l ? this.config.weth : n, I = await this.apiClient.findRoutes({
|
|
414
|
+
from: f,
|
|
415
|
+
target: v,
|
|
416
|
+
amount: o,
|
|
417
|
+
byAmountIn: r,
|
|
418
|
+
depth: a,
|
|
419
|
+
splitCount: i,
|
|
420
|
+
providers: c
|
|
421
|
+
});
|
|
422
|
+
return this.buildQuoteFromApi(I, f, v, u, c, d, l);
|
|
423
|
+
}
|
|
424
|
+
/**
|
|
425
|
+
* Filter paths by allowed providers, removing paths with disallowed providers
|
|
426
|
+
* and cascade-removing orphaned paths that depend on removed paths.
|
|
427
|
+
*/
|
|
428
|
+
filterPathsByProviders(t, e, n, o) {
|
|
429
|
+
const s = new Set(e.map((l) => l.toUpperCase()));
|
|
430
|
+
let r = t.filter((l) => s.has(l.provider.toUpperCase()));
|
|
431
|
+
if (r.length === t.length)
|
|
432
|
+
return r;
|
|
433
|
+
const a = n.toLowerCase(), i = o.toLowerCase(), c = /* @__PURE__ */ new Set();
|
|
434
|
+
c.add(a);
|
|
435
|
+
let u = !0;
|
|
436
|
+
for (; u; ) {
|
|
437
|
+
u = !1;
|
|
438
|
+
for (const l of r)
|
|
439
|
+
c.has(l.token_in.toLowerCase()) && !c.has(l.token_out.toLowerCase()) && (c.add(l.token_out.toLowerCase()), u = !0);
|
|
440
|
+
}
|
|
441
|
+
const d = /* @__PURE__ */ new Set();
|
|
442
|
+
for (d.add(i), u = !0; u; ) {
|
|
443
|
+
u = !1;
|
|
444
|
+
for (const l of r)
|
|
445
|
+
d.has(l.token_out.toLowerCase()) && !d.has(l.token_in.toLowerCase()) && (d.add(l.token_in.toLowerCase()), u = !0);
|
|
446
|
+
}
|
|
447
|
+
return r.length, r = r.filter(
|
|
448
|
+
(l) => c.has(l.token_in.toLowerCase()) && d.has(l.token_out.toLowerCase())
|
|
449
|
+
), r;
|
|
450
|
+
}
|
|
451
|
+
/**
|
|
452
|
+
* Build Quote from route data (e.g. from JSON file or API response).
|
|
453
|
+
* Use this to simulate a swap from a pre-computed route without calling the API.
|
|
454
|
+
*
|
|
455
|
+
* @param data - Route data with paths, amount_in, amount_out, contracts, gas
|
|
456
|
+
* @param srcToken - Source token address (first path token_in)
|
|
457
|
+
* @param dstToken - Destination token address (last path token_out)
|
|
458
|
+
* @param deadlineSeconds - Optional deadline in seconds from now (default: 20 min)
|
|
459
|
+
*/
|
|
460
|
+
buildQuoteFromRouteData(t, e, n, o, s) {
|
|
461
|
+
const r = this.buildQuoteFromRouteDataInternal(
|
|
462
|
+
t,
|
|
463
|
+
e,
|
|
464
|
+
n,
|
|
465
|
+
o ?? T,
|
|
466
|
+
void 0
|
|
467
|
+
);
|
|
468
|
+
return s?.srcNative && (r.srcNative = !0), s?.dstNative && (r.dstNative = !0), r;
|
|
469
|
+
}
|
|
470
|
+
/**
|
|
471
|
+
* Build Quote from API response
|
|
472
|
+
*/
|
|
473
|
+
buildQuoteFromApi(t, e, n, o, s, r, a) {
|
|
474
|
+
const i = this.buildQuoteFromRouteDataInternal(
|
|
475
|
+
t,
|
|
476
|
+
e,
|
|
477
|
+
n,
|
|
478
|
+
o,
|
|
479
|
+
s
|
|
480
|
+
);
|
|
481
|
+
return r && (i.srcNative = !0), a && (i.dstNative = !0), i;
|
|
482
|
+
}
|
|
483
|
+
buildQuoteFromRouteDataInternal(t, e, n, o, s) {
|
|
484
|
+
const r = e.toLowerCase();
|
|
485
|
+
t.paths.length;
|
|
486
|
+
let a = t.paths.filter((p) => !(!(p.token_in.toLowerCase() === r) && BigInt(p.amount_in) === 0n && BigInt(p.amount_out) === 0n));
|
|
487
|
+
if (a.length === 0)
|
|
488
|
+
throw new w("All route paths have zero amounts", 4001);
|
|
489
|
+
if (s && s.length > 0 && (a = this.filterPathsByProviders(a, s, e, n), a.length === 0))
|
|
490
|
+
throw new w("No valid route paths remaining after provider filtering", 4001);
|
|
491
|
+
const i = n.toLowerCase();
|
|
492
|
+
let c = 0n, u = 0n;
|
|
493
|
+
for (const p of a)
|
|
494
|
+
p.token_in.toLowerCase() === r && (c += BigInt(p.amount_in)), p.token_out.toLowerCase() === i && (u += BigInt(p.amount_out));
|
|
495
|
+
const d = BigInt(
|
|
496
|
+
Math.floor(Date.now() / 1e3) + o
|
|
497
|
+
), l = /* @__PURE__ */ new Map();
|
|
498
|
+
for (const p of a) {
|
|
499
|
+
const A = p.token_in.toLowerCase();
|
|
500
|
+
l.set(A, (l.get(A) ?? 0) + 1);
|
|
501
|
+
}
|
|
502
|
+
const f = /* @__PURE__ */ new Map(), v = a.map((p) => {
|
|
503
|
+
const A = p.token_in.toLowerCase(), _ = (f.get(A) ?? 0) + 1;
|
|
504
|
+
f.set(A, _);
|
|
505
|
+
const S = l.get(A), L = S > 1 && _ < S, N = A === r;
|
|
506
|
+
return {
|
|
507
|
+
adapter: p.adapter,
|
|
508
|
+
pool: p.pool,
|
|
509
|
+
tokenIn: p.token_in,
|
|
510
|
+
tokenOut: p.token_out,
|
|
511
|
+
amountIn: N || L ? BigInt(p.amount_in) : 0n,
|
|
512
|
+
extraData: p.extra_data || "0x"
|
|
513
|
+
};
|
|
514
|
+
}), I = /* @__PURE__ */ new Set();
|
|
515
|
+
for (const p of a)
|
|
516
|
+
p.token_out.toLowerCase() !== i && I.add(p.token_out);
|
|
517
|
+
const P = Array.from(I), C = {
|
|
518
|
+
srcToken: e,
|
|
519
|
+
dstToken: n,
|
|
520
|
+
amountIn: c,
|
|
521
|
+
amountOutMin: u,
|
|
522
|
+
steps: v,
|
|
523
|
+
intermediateTokens: P,
|
|
524
|
+
deadline: d,
|
|
525
|
+
quoteId: t.request_id ? h.id(t.request_id).slice(0, 66) : h.ZeroHash,
|
|
526
|
+
expectAmountOut: u
|
|
527
|
+
}, F = {
|
|
528
|
+
routes: [
|
|
529
|
+
{
|
|
530
|
+
steps: a.map((p) => ({
|
|
531
|
+
pool: {
|
|
532
|
+
address: p.pool,
|
|
533
|
+
token0: p.token_in,
|
|
534
|
+
token1: p.token_out,
|
|
535
|
+
protocol: this.getProtocolForProvider(p.provider),
|
|
536
|
+
fee: p.fee_rate ? Math.round(parseFloat(p.fee_rate) * 1e6) : void 0
|
|
537
|
+
},
|
|
538
|
+
tokenIn: p.token_in,
|
|
539
|
+
tokenOut: p.token_out,
|
|
540
|
+
amountIn: BigInt(p.amount_in),
|
|
541
|
+
amountOut: BigInt(p.amount_out)
|
|
542
|
+
})),
|
|
543
|
+
amountIn: c,
|
|
544
|
+
amountOut: u,
|
|
545
|
+
gasEstimate: BigInt(t.gas)
|
|
546
|
+
}
|
|
547
|
+
],
|
|
548
|
+
percentages: [1e4],
|
|
549
|
+
totalAmountIn: c,
|
|
550
|
+
totalAmountOut: u,
|
|
551
|
+
totalGasEstimate: BigInt(t.gas)
|
|
552
|
+
};
|
|
553
|
+
if (!t.contracts?.router)
|
|
554
|
+
throw new w("API response missing contracts.router address", 4002);
|
|
555
|
+
return {
|
|
556
|
+
srcToken: e,
|
|
557
|
+
dstToken: n,
|
|
558
|
+
amountIn: c,
|
|
559
|
+
amountOut: u,
|
|
560
|
+
priceImpact: parseFloat(t.deviation_ratio || "0"),
|
|
561
|
+
route: F,
|
|
562
|
+
params: C,
|
|
563
|
+
gasEstimate: BigInt(t.gas),
|
|
564
|
+
routerAddress: t.contracts?.router
|
|
565
|
+
};
|
|
566
|
+
}
|
|
567
|
+
/**
|
|
568
|
+
* Get available providers from the API
|
|
569
|
+
* @throws Error if API client is not configured
|
|
570
|
+
*/
|
|
571
|
+
async getAvailableProviders() {
|
|
572
|
+
if (!this.apiClient)
|
|
573
|
+
throw new Error("API client is not configured. getAvailableProviders() requires PeachClientOptions.api.");
|
|
574
|
+
return this.apiClient.getAvailableProviders();
|
|
575
|
+
}
|
|
576
|
+
/**
|
|
577
|
+
* Simulate swap via eth_call (no gas, no state change)
|
|
578
|
+
* Useful for testing and verifying quote accuracy
|
|
579
|
+
*
|
|
580
|
+
* @param quote - Quote object from getQuote
|
|
581
|
+
* @param slippageBps - Slippage tolerance in basis points (e.g. 50 = 0.5%). Required.
|
|
582
|
+
* @param fromAddress - Optional caller address for simulation (default: zero address)
|
|
583
|
+
* @param stateOverrides - Optional state overrides for ERC20 balance/allowance
|
|
584
|
+
* @returns Simulated amountOut and method used
|
|
585
|
+
*/
|
|
586
|
+
async simulate(t, e, n, o) {
|
|
587
|
+
const s = n || h.ZeroAddress, { to: r, data: a, value: i, method: c } = this.encodeSwapCalldata(t, e);
|
|
588
|
+
if (console.log("[PeachClient] simulate using router:", r), o) {
|
|
589
|
+
const u = this.getJsonRpcProviderForStateOverrides(), d = i > 0n ? "0x" + i.toString(16) : void 0, l = await u.send("eth_call", [
|
|
590
|
+
{ from: s, to: r, data: a, value: d },
|
|
591
|
+
"latest",
|
|
592
|
+
o
|
|
593
|
+
]), [f] = this.routerContract.interface.decodeFunctionResult(c, l);
|
|
594
|
+
return { amountOut: f, method: c };
|
|
595
|
+
} else {
|
|
596
|
+
const u = await this.provider.call({
|
|
597
|
+
from: s,
|
|
598
|
+
to: r,
|
|
599
|
+
data: a,
|
|
600
|
+
value: i > 0n ? i : void 0
|
|
601
|
+
}), [d] = this.routerContract.interface.decodeFunctionResult(c, u);
|
|
602
|
+
return { amountOut: d, method: c };
|
|
603
|
+
}
|
|
604
|
+
}
|
|
605
|
+
getJsonRpcProviderForStateOverrides() {
|
|
606
|
+
if (typeof this.provider.send != "function")
|
|
607
|
+
throw new Error(
|
|
608
|
+
"stateOverrides require a JsonRpcProvider-compatible provider with send(method, params)."
|
|
609
|
+
);
|
|
610
|
+
return this.provider;
|
|
611
|
+
}
|
|
612
|
+
/**
|
|
613
|
+
* Format simulate error with human-readable details
|
|
614
|
+
*/
|
|
615
|
+
formatSimulateError(t, e, n, o) {
|
|
616
|
+
const s = t instanceof Error ? t : new Error(String(t)), r = t;
|
|
617
|
+
let a = "unknown";
|
|
618
|
+
if (r.reason && typeof r.reason == "string")
|
|
619
|
+
a = r.reason;
|
|
620
|
+
else if (r.revert && typeof r.revert == "object") {
|
|
621
|
+
const d = r.revert;
|
|
622
|
+
d.args && Array.isArray(d.args) && (a = d.args.join(", "));
|
|
623
|
+
} else s.message && (a = s.message);
|
|
624
|
+
const i = e.params.steps.map(
|
|
625
|
+
(d, l) => ` Step ${l}: ${d.tokenIn.slice(0, 10)}→${d.tokenOut.slice(0, 10)} via adapter ${d.adapter.slice(0, 10)} pool ${d.pool.slice(0, 10)}`
|
|
626
|
+
).join(`
|
|
627
|
+
`), c = [
|
|
628
|
+
`Simulate ${n} failed: ${a}`,
|
|
629
|
+
` Route: ${e.srcToken} → ${e.dstToken}`,
|
|
630
|
+
` AmountIn: ${e.amountIn}`,
|
|
631
|
+
` AmountOutMin: ${e.params.amountOutMin}`,
|
|
632
|
+
` Router: ${e.routerAddress}`,
|
|
633
|
+
` Caller: ${o}`,
|
|
634
|
+
` Steps (${e.params.steps.length}):`,
|
|
635
|
+
i
|
|
636
|
+
].join(`
|
|
637
|
+
`), u = new Error(c);
|
|
638
|
+
return u.cause = s, u.reason = a, u;
|
|
639
|
+
}
|
|
640
|
+
/**
|
|
641
|
+
* Find which step in the route causes the same revert as the full route (e.g. MUL_ERROR).
|
|
642
|
+
* Simulates with steps [0..1], [0..2], ... and returns the first step whose revert
|
|
643
|
+
* matches fullRouteError. Ignores steps that revert with a different reason (e.g. unknown custom error).
|
|
644
|
+
*
|
|
645
|
+
* @param quote - Full quote from getQuote (the one that fails when simulated)
|
|
646
|
+
* @param slippageBps - Same as for simulate
|
|
647
|
+
* @param fromAddress - Same as for simulate
|
|
648
|
+
* @param stateOverrides - Same as for simulate (use when simulating ERC20 sell with arbitrary address)
|
|
649
|
+
* @param fullRouteError - The error from simulating the full route. Required so we match by revert reason (e.g. "MUL_ERROR"); only the step that produces the same reason is returned.
|
|
650
|
+
* @returns The step index and step details whose revert matches fullRouteError, or null if none match or full route succeeds
|
|
651
|
+
*/
|
|
652
|
+
async findFailingStep(t, e, n, o, s) {
|
|
653
|
+
const r = t.params.steps;
|
|
654
|
+
if (!r.length) return null;
|
|
655
|
+
const a = s != null ? this.normalizeRevertReason(s) : void 0;
|
|
656
|
+
for (let i = 1; i <= r.length; i++) {
|
|
657
|
+
const c = this.quoteWithFirstNSteps(t, i);
|
|
658
|
+
try {
|
|
659
|
+
await this.simulate(c, e, n, o);
|
|
660
|
+
} catch (u) {
|
|
661
|
+
const d = this.normalizeRevertReason(u), l = u?.message ?? u?.reason ?? u?.shortMessage;
|
|
662
|
+
if (a != null ? d === a : !0)
|
|
663
|
+
return {
|
|
664
|
+
stepIndex: i - 1,
|
|
665
|
+
step: r[i - 1],
|
|
666
|
+
error: u,
|
|
667
|
+
revertMessage: typeof l == "string" ? l : void 0,
|
|
668
|
+
fullRouteRevertMessage: a ?? void 0
|
|
669
|
+
};
|
|
670
|
+
}
|
|
671
|
+
}
|
|
672
|
+
return null;
|
|
673
|
+
}
|
|
674
|
+
/** Extract a comparable revert reason (e.g. "MUL_ERROR") from an error for findFailingStep matching. */
|
|
675
|
+
normalizeRevertReason(t) {
|
|
676
|
+
if (t == null) return;
|
|
677
|
+
const e = t;
|
|
678
|
+
if (typeof e.reason == "string" && e.reason.length > 0) return e.reason;
|
|
679
|
+
const n = e.shortMessage ?? e.message;
|
|
680
|
+
if (typeof n != "string") return;
|
|
681
|
+
const o = n.match(/reason="([^"]+)"/);
|
|
682
|
+
if (o) return o[1];
|
|
683
|
+
const s = n.match(/reverted:\s*"([^"]+)"/);
|
|
684
|
+
if (s) return s[1];
|
|
685
|
+
const r = n.match(/execution reverted:\s*"([^"]+)"/);
|
|
686
|
+
if (r) return r[1];
|
|
687
|
+
}
|
|
688
|
+
/** Build a quote that only includes the first stepCount steps (for findFailingStep). */
|
|
689
|
+
quoteWithFirstNSteps(t, e) {
|
|
690
|
+
const n = t.params.steps.slice(0, e), o = t.dstToken.toLowerCase(), s = /* @__PURE__ */ new Set(), r = [];
|
|
691
|
+
for (const a of n) {
|
|
692
|
+
const i = a.tokenOut.toLowerCase();
|
|
693
|
+
i !== o && !s.has(i) && (s.add(i), r.push(a.tokenOut));
|
|
694
|
+
}
|
|
695
|
+
return {
|
|
696
|
+
...t,
|
|
697
|
+
params: {
|
|
698
|
+
...t.params,
|
|
699
|
+
steps: n,
|
|
700
|
+
intermediateTokens: r
|
|
701
|
+
}
|
|
702
|
+
};
|
|
703
|
+
}
|
|
704
|
+
/**
|
|
705
|
+
* Build state overrides for ERC20 token balance and allowance.
|
|
706
|
+
* Useful for simulating swaps without actual on-chain token balance/approval.
|
|
707
|
+
*
|
|
708
|
+
* Automatically covers multiple storage slot layouts (slots 0-2 for balance,
|
|
709
|
+
* slots 0-7 for allowance) to handle OZ ERC20 (slot 0/1), Ownable+ERC20 (slot 1/2),
|
|
710
|
+
* and other common BSC token implementations.
|
|
711
|
+
*
|
|
712
|
+
* WBNB (native wrap) is skipped automatically — swapETH wraps msg.value
|
|
713
|
+
* internally so no ERC20 approval from the sender is needed.
|
|
714
|
+
*
|
|
715
|
+
* @param tokenAddress - ERC20 token address (WBNB returns empty overrides)
|
|
716
|
+
* @param owner - Address that needs the balance and allowance
|
|
717
|
+
* @param routerAddress - Router address (used as fallback spender)
|
|
718
|
+
* @param balance - Balance to inject (default: 1M tokens with 18 decimals)
|
|
719
|
+
* @param spenderAddress - Spender to approve (default: routerAddress). Pass quote.routerAddress when simulating API quotes.
|
|
720
|
+
*/
|
|
721
|
+
buildStateOverrides(t, e, n, o, s, r) {
|
|
722
|
+
if (r?.isNative || O(t))
|
|
723
|
+
return {};
|
|
724
|
+
if (!n || n === h.ZeroAddress)
|
|
725
|
+
throw new Error("buildStateOverrides requires a non-zero routerAddress.");
|
|
726
|
+
const a = h.AbiCoder.defaultAbiCoder(), i = o || h.parseUnits("1000000", 18), c = s ?? n, u = h.zeroPadValue(h.toBeHex(i), 32), d = h.zeroPadValue(h.toBeHex(h.MaxUint256), 32), l = {}, f = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 50, 51, 52, 100, 101, 102];
|
|
727
|
+
for (const v of f) {
|
|
728
|
+
const I = h.keccak256(a.encode(["address", "uint256"], [e, v]));
|
|
729
|
+
l[I] = u;
|
|
730
|
+
const P = h.keccak256(a.encode(["address", "uint256"], [e, v])), C = h.keccak256(a.encode(["address", "bytes32"], [c, P]));
|
|
731
|
+
l[C] = d;
|
|
732
|
+
}
|
|
733
|
+
return {
|
|
734
|
+
[t.toLowerCase()]: { stateDiff: l }
|
|
735
|
+
};
|
|
736
|
+
}
|
|
737
|
+
}
|
|
738
|
+
const q = [
|
|
739
|
+
"function token0() external view returns (address)",
|
|
740
|
+
"function token1() external view returns (address)",
|
|
741
|
+
"function getReserves() external view returns (uint112 reserve0, uint112 reserve1, uint32 blockTimestampLast)"
|
|
742
|
+
], H = [
|
|
743
|
+
"function token0() external view returns (address)",
|
|
744
|
+
"function token1() external view returns (address)",
|
|
745
|
+
"function fee() external view returns (uint24)",
|
|
746
|
+
"function liquidity() external view returns (uint128)",
|
|
747
|
+
"function slot0() external view returns (uint160 sqrtPriceX96, int24 tick, uint16 observationIndex, uint16 observationCardinality, uint16 observationCardinalityNext, uint32 feeProtocol, bool unlocked)"
|
|
748
|
+
], W = "0xcA143Ce32Fe78f1f7019d7d551a6402fC5350c73", G = [
|
|
749
|
+
"function getPair(address tokenA, address tokenB) external view returns (address pair)"
|
|
750
|
+
], K = "0x0BFbCF9fa4f9C56B0F40a671Ad40E0805A091865", j = [
|
|
751
|
+
"function getPool(address tokenA, address tokenB, uint24 fee) external view returns (address pool)"
|
|
752
|
+
], Q = [100, 500, 2500, 1e4], Z = 60000n, z = 120000n, J = 100000n;
|
|
753
|
+
class rt {
|
|
754
|
+
constructor(t, e) {
|
|
755
|
+
this.poolCache = /* @__PURE__ */ new Map(), this.provider = t, this.config = e, this.v2Factory = new h.Contract(
|
|
756
|
+
W,
|
|
757
|
+
G,
|
|
758
|
+
t
|
|
759
|
+
), this.v3Factory = new h.Contract(
|
|
760
|
+
K,
|
|
761
|
+
j,
|
|
762
|
+
t
|
|
763
|
+
);
|
|
764
|
+
}
|
|
765
|
+
/**
|
|
766
|
+
* Discover optimal route
|
|
767
|
+
*/
|
|
768
|
+
async findBestRoute(t, e, n) {
|
|
769
|
+
const o = await this.discoverPools(t, e);
|
|
770
|
+
if (o.length === 0)
|
|
771
|
+
throw new Error(`No pools found for ${t} -> ${e}`);
|
|
772
|
+
const r = (await this.calculateRoutes(
|
|
773
|
+
o,
|
|
774
|
+
t,
|
|
775
|
+
e,
|
|
776
|
+
n
|
|
777
|
+
)).reduce(
|
|
778
|
+
(a, i) => i.amountOut > a.amountOut ? i : a
|
|
779
|
+
);
|
|
780
|
+
return {
|
|
781
|
+
routes: [r],
|
|
782
|
+
percentages: [1e4],
|
|
783
|
+
// 100%
|
|
784
|
+
totalAmountIn: n,
|
|
785
|
+
totalAmountOut: r.amountOut,
|
|
786
|
+
totalGasEstimate: r.gasEstimate
|
|
787
|
+
};
|
|
788
|
+
}
|
|
789
|
+
/**
|
|
790
|
+
* Discover available pools (direct path)
|
|
791
|
+
*/
|
|
792
|
+
async discoverPools(t, e) {
|
|
793
|
+
const n = [];
|
|
794
|
+
try {
|
|
795
|
+
const o = await this.findV2Pool(t, e);
|
|
796
|
+
o && n.push(o);
|
|
797
|
+
} catch {
|
|
798
|
+
}
|
|
799
|
+
for (const o of Q)
|
|
800
|
+
try {
|
|
801
|
+
const s = await this.findV3Pool(t, e, o);
|
|
802
|
+
s && n.push(s);
|
|
803
|
+
} catch {
|
|
804
|
+
}
|
|
805
|
+
return n;
|
|
806
|
+
}
|
|
807
|
+
/**
|
|
808
|
+
* Find V2 pool
|
|
809
|
+
*/
|
|
810
|
+
async findV2Pool(t, e) {
|
|
811
|
+
const n = `v2-${t}-${e}`;
|
|
812
|
+
if (this.poolCache.has(n))
|
|
813
|
+
return this.poolCache.get(n);
|
|
814
|
+
const o = await this.v2Factory.getPair(t, e);
|
|
815
|
+
if (o === h.ZeroAddress)
|
|
816
|
+
return null;
|
|
817
|
+
const s = new h.Contract(o, q, this.provider), [r, a, i] = await Promise.all([
|
|
818
|
+
s.token0(),
|
|
819
|
+
s.token1(),
|
|
820
|
+
s.getReserves()
|
|
821
|
+
]), c = {
|
|
822
|
+
address: o,
|
|
823
|
+
token0: r,
|
|
824
|
+
token1: a,
|
|
825
|
+
protocol: g.PancakeV2,
|
|
826
|
+
reserve0: i.reserve0,
|
|
827
|
+
reserve1: i.reserve1
|
|
828
|
+
};
|
|
829
|
+
return this.poolCache.set(n, c), c;
|
|
830
|
+
}
|
|
831
|
+
/**
|
|
832
|
+
* Find V3 pool
|
|
833
|
+
*/
|
|
834
|
+
async findV3Pool(t, e, n) {
|
|
835
|
+
const o = `v3-${t}-${e}-${n}`;
|
|
836
|
+
if (this.poolCache.has(o))
|
|
837
|
+
return this.poolCache.get(o);
|
|
838
|
+
const s = await this.v3Factory.getPool(t, e, n);
|
|
839
|
+
if (s === h.ZeroAddress)
|
|
840
|
+
return null;
|
|
841
|
+
const r = new h.Contract(s, H, this.provider), [a, i, c] = await Promise.all([
|
|
842
|
+
r.token0(),
|
|
843
|
+
r.token1(),
|
|
844
|
+
r.liquidity()
|
|
845
|
+
]);
|
|
846
|
+
if (c === 0n)
|
|
847
|
+
return null;
|
|
848
|
+
const u = {
|
|
849
|
+
address: s,
|
|
850
|
+
token0: a,
|
|
851
|
+
token1: i,
|
|
852
|
+
protocol: g.PancakeV3,
|
|
853
|
+
fee: n,
|
|
854
|
+
liquidity: c
|
|
855
|
+
};
|
|
856
|
+
return this.poolCache.set(o, u), u;
|
|
857
|
+
}
|
|
858
|
+
/**
|
|
859
|
+
* Calculate route quotes
|
|
860
|
+
*/
|
|
861
|
+
async calculateRoutes(t, e, n, o) {
|
|
862
|
+
const s = [];
|
|
863
|
+
for (const r of t)
|
|
864
|
+
try {
|
|
865
|
+
const a = await this.getAmountOut(
|
|
866
|
+
r,
|
|
867
|
+
e,
|
|
868
|
+
n,
|
|
869
|
+
o
|
|
870
|
+
);
|
|
871
|
+
a > 0n && s.push({
|
|
872
|
+
steps: [
|
|
873
|
+
{
|
|
874
|
+
pool: r,
|
|
875
|
+
tokenIn: e,
|
|
876
|
+
tokenOut: n,
|
|
877
|
+
amountIn: o,
|
|
878
|
+
amountOut: a
|
|
879
|
+
}
|
|
880
|
+
],
|
|
881
|
+
amountIn: o,
|
|
882
|
+
amountOut: a,
|
|
883
|
+
gasEstimate: r.protocol === g.PancakeV2 ? Z : r.protocol === g.Dodo ? J : z
|
|
884
|
+
});
|
|
885
|
+
} catch {
|
|
886
|
+
}
|
|
887
|
+
return s;
|
|
888
|
+
}
|
|
889
|
+
/**
|
|
890
|
+
* Get output for a single pool
|
|
891
|
+
*/
|
|
892
|
+
async getAmountOut(t, e, n, o) {
|
|
893
|
+
if (t.protocol === g.PancakeV2)
|
|
894
|
+
return this.getV2AmountOut(t, e, o);
|
|
895
|
+
if (t.protocol === g.Dodo)
|
|
896
|
+
throw new Error("DODO amount out not supported in local route discovery");
|
|
897
|
+
return this.estimateV3AmountOut(t, e, o);
|
|
898
|
+
}
|
|
899
|
+
/**
|
|
900
|
+
* V2 output calculation
|
|
901
|
+
*/
|
|
902
|
+
getV2AmountOut(t, e, n) {
|
|
903
|
+
const o = e.toLowerCase() === t.token0.toLowerCase(), [s, r] = o ? [t.reserve0, t.reserve1] : [t.reserve1, t.reserve0], a = n * 9975n, i = a * r, c = s * 10000n + a;
|
|
904
|
+
return i / c;
|
|
905
|
+
}
|
|
906
|
+
/**
|
|
907
|
+
* V3 output estimation (simplified)
|
|
908
|
+
*/
|
|
909
|
+
estimateV3AmountOut(t, e, n) {
|
|
910
|
+
const s = 1000000n - BigInt(t.fee || 2500);
|
|
911
|
+
return n * s / 1000000n;
|
|
912
|
+
}
|
|
913
|
+
/**
|
|
914
|
+
* Clear cache
|
|
915
|
+
*/
|
|
916
|
+
clearCache() {
|
|
917
|
+
this.poolCache.clear();
|
|
918
|
+
}
|
|
919
|
+
}
|
|
920
|
+
class st {
|
|
921
|
+
constructor(t) {
|
|
922
|
+
this.adapters = t;
|
|
923
|
+
}
|
|
924
|
+
/**
|
|
925
|
+
* Build SwapParams from split route
|
|
926
|
+
*/
|
|
927
|
+
build(t, e, n, o, s = T) {
|
|
928
|
+
const r = this.flattenRoutes(t), a = this.mergeIdenticalPools(r), i = this.topologicalSort(a, e), c = this.convertToSwapSteps(i), u = this.extractIntermediates(
|
|
929
|
+
i,
|
|
930
|
+
e,
|
|
931
|
+
n
|
|
932
|
+
), d = BigInt(Math.floor(Date.now() / 1e3) + s);
|
|
933
|
+
return {
|
|
934
|
+
srcToken: e,
|
|
935
|
+
dstToken: n,
|
|
936
|
+
amountIn: t.totalAmountIn,
|
|
937
|
+
amountOutMin: o,
|
|
938
|
+
steps: c,
|
|
939
|
+
intermediateTokens: u,
|
|
940
|
+
deadline: d,
|
|
941
|
+
quoteId: h.ZeroHash,
|
|
942
|
+
expectAmountOut: t.totalAmountOut
|
|
943
|
+
};
|
|
944
|
+
}
|
|
945
|
+
/**
|
|
946
|
+
* Flatten split route
|
|
947
|
+
*/
|
|
948
|
+
flattenRoutes(t) {
|
|
949
|
+
const e = [];
|
|
950
|
+
for (let n = 0; n < t.routes.length; n++) {
|
|
951
|
+
const o = t.routes[n], s = t.percentages[n], r = t.totalAmountIn * BigInt(s) / y;
|
|
952
|
+
for (let a = 0; a < o.steps.length; a++) {
|
|
953
|
+
const i = o.steps[a];
|
|
954
|
+
e.push({
|
|
955
|
+
pool: i.pool,
|
|
956
|
+
tokenIn: i.tokenIn,
|
|
957
|
+
tokenOut: i.tokenOut,
|
|
958
|
+
// Only first step has fixed amount, subsequent steps depend on previous output
|
|
959
|
+
amountIn: a === 0 ? r : 0n,
|
|
960
|
+
routeIndex: n,
|
|
961
|
+
stepIndex: a
|
|
962
|
+
});
|
|
963
|
+
}
|
|
964
|
+
}
|
|
965
|
+
return e;
|
|
966
|
+
}
|
|
967
|
+
/**
|
|
968
|
+
* Merge identical pools
|
|
969
|
+
* Key: pool + tokenIn + tokenOut
|
|
970
|
+
*/
|
|
971
|
+
mergeIdenticalPools(t) {
|
|
972
|
+
const e = /* @__PURE__ */ new Map(), n = [];
|
|
973
|
+
for (const o of t) {
|
|
974
|
+
const s = `${o.pool.address}-${o.tokenIn}-${o.tokenOut}`;
|
|
975
|
+
if (e.has(s)) {
|
|
976
|
+
const r = e.get(s);
|
|
977
|
+
o.amountIn > 0n && r.amountIn > 0n && (r.amountIn = 0n);
|
|
978
|
+
} else {
|
|
979
|
+
const r = { ...o };
|
|
980
|
+
e.set(s, r), n.push(r);
|
|
981
|
+
}
|
|
982
|
+
}
|
|
983
|
+
for (const o of n) {
|
|
984
|
+
const s = `${o.pool.address}-${o.tokenIn}-${o.tokenOut}`;
|
|
985
|
+
t.filter(
|
|
986
|
+
(a) => `${a.pool.address}-${a.tokenIn}-${a.tokenOut}` === s
|
|
987
|
+
).length > 1 && (o.amountIn = 0n);
|
|
988
|
+
}
|
|
989
|
+
return n;
|
|
990
|
+
}
|
|
991
|
+
/**
|
|
992
|
+
* Topological sort (Kahn's Algorithm)
|
|
993
|
+
*/
|
|
994
|
+
topologicalSort(t, e) {
|
|
995
|
+
const n = /* @__PURE__ */ new Map(), o = /* @__PURE__ */ new Map(), s = /* @__PURE__ */ new Map();
|
|
996
|
+
for (const i of t) {
|
|
997
|
+
const c = this.stepKey(i);
|
|
998
|
+
o.set(c, i), n.set(c, 0), s.set(c, []);
|
|
999
|
+
}
|
|
1000
|
+
for (const i of t) {
|
|
1001
|
+
const c = this.stepKey(i);
|
|
1002
|
+
if (i.tokenIn !== e) {
|
|
1003
|
+
for (const u of t)
|
|
1004
|
+
if (u.tokenOut === i.tokenIn) {
|
|
1005
|
+
const d = this.stepKey(u);
|
|
1006
|
+
d !== c && (s.get(d).push(c), n.set(c, (n.get(c) || 0) + 1));
|
|
1007
|
+
}
|
|
1008
|
+
}
|
|
1009
|
+
}
|
|
1010
|
+
const r = [];
|
|
1011
|
+
for (const [i, c] of n)
|
|
1012
|
+
c === 0 && r.push(i);
|
|
1013
|
+
const a = [];
|
|
1014
|
+
for (; r.length > 0; ) {
|
|
1015
|
+
const i = r.shift(), c = o.get(i);
|
|
1016
|
+
a.push(c);
|
|
1017
|
+
for (const u of s.get(i) || []) {
|
|
1018
|
+
const d = (n.get(u) || 0) - 1;
|
|
1019
|
+
n.set(u, d), d === 0 && r.push(u);
|
|
1020
|
+
}
|
|
1021
|
+
}
|
|
1022
|
+
if (a.length !== t.length)
|
|
1023
|
+
throw new Error("Circular dependency detected in route");
|
|
1024
|
+
return a;
|
|
1025
|
+
}
|
|
1026
|
+
/**
|
|
1027
|
+
* Convert to contract SwapStep format
|
|
1028
|
+
*/
|
|
1029
|
+
convertToSwapSteps(t) {
|
|
1030
|
+
return t.map((e) => {
|
|
1031
|
+
const n = this.adapters.get(e.pool.protocol);
|
|
1032
|
+
if (!n)
|
|
1033
|
+
throw new Error(`No adapter for protocol: ${e.pool.protocol}`);
|
|
1034
|
+
return {
|
|
1035
|
+
adapter: n,
|
|
1036
|
+
pool: e.pool.address,
|
|
1037
|
+
tokenIn: e.tokenIn,
|
|
1038
|
+
tokenOut: e.tokenOut,
|
|
1039
|
+
amountIn: e.amountIn,
|
|
1040
|
+
extraData: this.encodeExtraData(e.pool)
|
|
1041
|
+
};
|
|
1042
|
+
});
|
|
1043
|
+
}
|
|
1044
|
+
/**
|
|
1045
|
+
* Encode protocol-specific parameters
|
|
1046
|
+
*/
|
|
1047
|
+
encodeExtraData(t) {
|
|
1048
|
+
switch (t.protocol) {
|
|
1049
|
+
case g.PancakeV2:
|
|
1050
|
+
return "0x";
|
|
1051
|
+
// V2 doesn't need extra parameters
|
|
1052
|
+
case g.PancakeV3:
|
|
1053
|
+
case g.UniswapV3:
|
|
1054
|
+
return "0x";
|
|
1055
|
+
case g.Dodo:
|
|
1056
|
+
return "0x";
|
|
1057
|
+
default:
|
|
1058
|
+
return "0x";
|
|
1059
|
+
}
|
|
1060
|
+
}
|
|
1061
|
+
/**
|
|
1062
|
+
* Extract intermediate tokens
|
|
1063
|
+
*/
|
|
1064
|
+
extractIntermediates(t, e, n) {
|
|
1065
|
+
const o = /* @__PURE__ */ new Set();
|
|
1066
|
+
for (const s of t)
|
|
1067
|
+
s.tokenOut !== n && o.add(s.tokenOut);
|
|
1068
|
+
return o.delete(e), o.delete(n), Array.from(o);
|
|
1069
|
+
}
|
|
1070
|
+
/**
|
|
1071
|
+
* Generate unique step key
|
|
1072
|
+
*/
|
|
1073
|
+
stepKey(t) {
|
|
1074
|
+
return `${t.pool.address}-${t.tokenIn}-${t.tokenOut}`;
|
|
1075
|
+
}
|
|
1076
|
+
}
|
|
1077
|
+
export {
|
|
1078
|
+
k as API_DEFAULTS,
|
|
1079
|
+
U as ApiClient,
|
|
1080
|
+
w as ApiError,
|
|
1081
|
+
y as BPS_DENOMINATOR,
|
|
1082
|
+
et as BSC_MAINNET_CONFIG,
|
|
1083
|
+
nt as BSC_TESTNET_CONFIG,
|
|
1084
|
+
T as DEFAULT_DEADLINE_SECONDS,
|
|
1085
|
+
D as DEFAULT_EXECUTE_TIMEOUT_MS,
|
|
1086
|
+
X as DEFAULT_SLIPPAGE_BPS,
|
|
1087
|
+
x as DEFAULT_TRANSACTION_RESPONSE_POLL_INTERVALS_MS,
|
|
1088
|
+
tt as ERR_ZERO_AMOUNT_PATHS,
|
|
1089
|
+
$ as ExecuteTimeoutError,
|
|
1090
|
+
V as NATIVE_TOKEN_ADDRESS,
|
|
1091
|
+
ot as PeachClient,
|
|
1092
|
+
g as ProtocolType,
|
|
1093
|
+
rt as RouteDiscovery,
|
|
1094
|
+
st as SwapBuilder,
|
|
1095
|
+
O as isNativeTokenAddress,
|
|
1096
|
+
b as withWalletSendTimeout
|
|
1097
|
+
};
|
|
1098
|
+
//# sourceMappingURL=index.mjs.map
|