four-flap-meme-sdk 4.3.7 → 4.3.9
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/chains/xlayer/eoa/query.js +3 -3
- package/dist/core/clients/blockrazor.js +3 -5
- package/dist/core/clients/four-client-fallback.test.js +81 -0
- package/dist/core/clients/four.d.ts +8 -0
- package/dist/core/clients/four.js +74 -31
- package/dist/core/clients/merkle.js +3 -5
- package/dist/core/erc20/allowances.d.ts +1 -1
- package/dist/core/erc20/allowances.js +6 -5
- package/dist/core/erc20/helpers.d.ts +2 -0
- package/dist/core/erc20/helpers.js +9 -4
- package/dist/core/erc20/helpers.test.d.ts +1 -0
- package/dist/core/erc20/helpers.test.js +10 -0
- package/dist/core/quote/quote-helpers.d.ts +2 -0
- package/dist/core/quote/quote-helpers.js +90 -34
- package/dist/core/quote/quote-helpers.test.d.ts +1 -0
- package/dist/core/quote/quote-helpers.test.js +21 -0
- package/dist/core/sign-context-helpers.js +3 -6
- package/dist/domains/dex/direct-router-v2-buy.js +18 -18
- package/dist/domains/dex/direct-router-v2-sell.js +16 -15
- package/dist/domains/flap/portal-bundle-merkle/core-helpers.js +2 -1
- package/dist/domains/flap/portal-bundle-merkle/pancake-proxy/helpers.js +2 -2
- package/dist/domains/flap/portal-bundle-merkle/private/helpers.js +3 -2
- package/dist/utils/provider-factory.d.ts +9 -24
- package/dist/utils/provider-factory.js +34 -60
- package/dist/utils/provider-factory.test.d.ts +1 -0
- package/dist/utils/provider-factory.test.js +14 -0
- package/package.json +1 -1
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* XLayer Portal 操作 — 查询器(query)
|
|
3
3
|
*/
|
|
4
|
-
import { Interface, Contract
|
|
4
|
+
import { Interface, Contract } from 'ethers';
|
|
5
5
|
import { FLAP_PORTAL, PORTAL_ABI, ERC20_ABI, MULTICALL3, DEFAULT_RPC_URL, XLAYER_CHAIN_ID, } from './constants.js';
|
|
6
6
|
import { mapWithConcurrency } from '../../../utils/concurrency.js';
|
|
7
7
|
import { erc20Iface } from './encoders.js';
|
|
8
|
+
import { createProvider } from '../../../utils/provider-factory.js';
|
|
8
9
|
/**
|
|
9
10
|
* Portal 查询器
|
|
10
11
|
*
|
|
@@ -21,8 +22,7 @@ export class PortalQuery {
|
|
|
21
22
|
constructor(config = {}) {
|
|
22
23
|
const rpcUrl = config.rpcUrl ?? DEFAULT_RPC_URL;
|
|
23
24
|
const chainId = config.chainId ?? XLAYER_CHAIN_ID;
|
|
24
|
-
|
|
25
|
-
this.provider = new JsonRpcProvider(rpcUrl, { chainId, name: 'xlayer', ensAddress: undefined }, {
|
|
25
|
+
this.provider = createProvider(rpcUrl, chainId, 'xlayer', {
|
|
26
26
|
batchMaxCount: 20,
|
|
27
27
|
batchStallTime: 30,
|
|
28
28
|
});
|
|
@@ -25,9 +25,10 @@
|
|
|
25
25
|
* });
|
|
26
26
|
* ```
|
|
27
27
|
*/
|
|
28
|
-
import {
|
|
28
|
+
import { Transaction, ethers } from 'ethers';
|
|
29
29
|
import { GAS_LIMITS, CHAINS, BLOCKRAZOR_BUILDER_EOA } from '../constants/index.js';
|
|
30
30
|
import { getErrorMessageFromUnknown } from '../index.js';
|
|
31
|
+
import { createProvider } from '../../utils/provider-factory.js';
|
|
31
32
|
export { BLOCKRAZOR_BUILDER_EOA } from '../constants/index.js';
|
|
32
33
|
// ============================================================================
|
|
33
34
|
// 常量
|
|
@@ -63,10 +64,7 @@ export class BlockRazorClient {
|
|
|
63
64
|
this.apiKey = config.apiKey;
|
|
64
65
|
// 普通 RPC(用于查询)
|
|
65
66
|
const rpcUrl = config.customRpcUrl || 'https://bsc-dataseed.binance.org';
|
|
66
|
-
this.provider =
|
|
67
|
-
chainId: this.chainId,
|
|
68
|
-
name: 'bsc',
|
|
69
|
-
});
|
|
67
|
+
this.provider = createProvider(rpcUrl, this.chainId, 'bsc');
|
|
70
68
|
// Builder RPC URL(用于发送 Bundle)
|
|
71
69
|
// ✅ 正确的端点: https://rpc.blockrazor.builders
|
|
72
70
|
this.builderUrl = config.builderRpcUrl || BLOCKRAZOR_RPC_ENDPOINTS.BSC;
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { afterEach, describe, expect, it, vi } from 'vitest';
|
|
2
2
|
import { FourClient } from './four.js';
|
|
3
|
+
import { FOUR_BNB_RAISED_TOKEN_FALLBACK } from '../../domains/four/raised-token.js';
|
|
3
4
|
describe('FourClient private API rate-limit fallback', () => {
|
|
4
5
|
afterEach(() => {
|
|
5
6
|
vi.unstubAllGlobals();
|
|
@@ -65,4 +66,84 @@ describe('FourClient private API rate-limit fallback', () => {
|
|
|
65
66
|
networkCode: 'BSC',
|
|
66
67
|
})).resolves.toBe('nonce-2');
|
|
67
68
|
});
|
|
69
|
+
it('browser hits official four.meme first so the request uses the user IP', async () => {
|
|
70
|
+
vi.stubGlobal('window', globalThis);
|
|
71
|
+
const calls = [];
|
|
72
|
+
vi.stubGlobal('fetch', vi.fn(async (input) => {
|
|
73
|
+
calls.push(String(input));
|
|
74
|
+
return new Response(JSON.stringify({ code: 0, data: 'nonce-user-ip' }));
|
|
75
|
+
}));
|
|
76
|
+
await expect(new FourClient({ baseUrl: 'https://bscfourapi.emit.tools' }).generateNonce({
|
|
77
|
+
accountAddress: `0x${'11'.repeat(20)}`,
|
|
78
|
+
verifyType: 'LOGIN',
|
|
79
|
+
networkCode: 'BSC',
|
|
80
|
+
})).resolves.toBe('nonce-user-ip');
|
|
81
|
+
expect(calls[0]).toContain('four.meme/meme-api');
|
|
82
|
+
});
|
|
83
|
+
it('browser createToken sends CORS-allowed Authorization to four.meme', async () => {
|
|
84
|
+
vi.stubGlobal('window', globalThis);
|
|
85
|
+
const headers = [];
|
|
86
|
+
vi.stubGlobal('fetch', vi.fn(async (input, init) => {
|
|
87
|
+
if (!String(input).includes('four.meme')) {
|
|
88
|
+
return new Response(JSON.stringify({ code: -1013, msg: 'Too many requests' }), { status: 429 });
|
|
89
|
+
}
|
|
90
|
+
headers.push((init?.headers || {}));
|
|
91
|
+
return new Response(JSON.stringify({ code: 0, data: { createArg: '0x1', signature: '0x2' } }));
|
|
92
|
+
}));
|
|
93
|
+
await expect(new FourClient({ baseUrl: 'https://bscfourapi.emit.tools' }).createToken('access-token', createReq())).resolves.toEqual({ createArg: '0x1', signature: '0x2' });
|
|
94
|
+
expect(headers[0]?.Authorization).toBe('access-token');
|
|
95
|
+
expect(headers[0]?.['meme-web-access']).toBeUndefined();
|
|
96
|
+
});
|
|
97
|
+
it('retries proxy with meme-web-access if official Authorization is treated as not login', async () => {
|
|
98
|
+
vi.stubGlobal('window', globalThis);
|
|
99
|
+
const seen = [];
|
|
100
|
+
vi.stubGlobal('fetch', vi.fn(async (input, init) => {
|
|
101
|
+
const url = String(input);
|
|
102
|
+
const hdrs = (init?.headers || {});
|
|
103
|
+
seen.push({ url, access: hdrs['meme-web-access'], authorization: hdrs.Authorization });
|
|
104
|
+
if (url.includes('four.meme')) {
|
|
105
|
+
return new Response(JSON.stringify({ code: -1004, msg: 'User not login' }));
|
|
106
|
+
}
|
|
107
|
+
return new Response(JSON.stringify({ code: 0, data: { createArg: '0x9', signature: '0xa' } }));
|
|
108
|
+
}));
|
|
109
|
+
await expect(new FourClient({ baseUrl: 'https://bscfourapi.emit.tools' }).createToken('access-token', createReq())).resolves.toEqual({ createArg: '0x9', signature: '0xa' });
|
|
110
|
+
expect(seen[0]?.url).toContain('four.meme');
|
|
111
|
+
expect(seen[0]?.authorization).toBe('access-token');
|
|
112
|
+
expect(seen.some((item) => item.url.includes('bscfourapi') && item.access === 'access-token')).toBe(true);
|
|
113
|
+
});
|
|
114
|
+
it('keeps proxy -1013 instead of Failed to fetch when browser CORS-upload to four.meme still fails', async () => {
|
|
115
|
+
vi.stubGlobal('window', globalThis);
|
|
116
|
+
const calls = [];
|
|
117
|
+
vi.stubGlobal('fetch', vi.fn(async (input) => {
|
|
118
|
+
const url = String(input);
|
|
119
|
+
calls.push(url);
|
|
120
|
+
if (url.includes('four.meme')) {
|
|
121
|
+
throw new TypeError('Failed to fetch');
|
|
122
|
+
}
|
|
123
|
+
return new Response(JSON.stringify({ code: -1013, msg: 'Too many requests, banned for 15 minutes, remain 120s' }), { status: 429 });
|
|
124
|
+
}));
|
|
125
|
+
await expect(new FourClient({ baseUrl: 'https://bscfourapi.emit.tools' }).createToken('access', createReq())).rejects.toThrow(/-1013|Too many requests/);
|
|
126
|
+
expect(calls.some((url) => url.includes('four.meme'))).toBe(true);
|
|
127
|
+
});
|
|
68
128
|
});
|
|
129
|
+
function createReq() {
|
|
130
|
+
return {
|
|
131
|
+
name: 't',
|
|
132
|
+
shortName: 't',
|
|
133
|
+
desc: '',
|
|
134
|
+
imgUrl: 'https://example.com/a.png',
|
|
135
|
+
launchTime: 1,
|
|
136
|
+
label: 'Meme',
|
|
137
|
+
preSale: '0',
|
|
138
|
+
onlyMPC: false,
|
|
139
|
+
lpTradingFee: 0.0025,
|
|
140
|
+
symbol: 'BNB',
|
|
141
|
+
totalSupply: 1_000_000_000,
|
|
142
|
+
raisedAmount: 18,
|
|
143
|
+
saleRate: 0.8,
|
|
144
|
+
reserveRate: 0,
|
|
145
|
+
funGroup: false,
|
|
146
|
+
clickFun: false,
|
|
147
|
+
raisedToken: FOUR_BNB_RAISED_TOKEN_FALLBACK,
|
|
148
|
+
};
|
|
149
|
+
}
|
|
@@ -112,6 +112,13 @@ export declare class FourClient {
|
|
|
112
112
|
private uploadUrls;
|
|
113
113
|
constructor(cfg?: FourConfig);
|
|
114
114
|
private attachApiCode;
|
|
115
|
+
private isBrowserEnv;
|
|
116
|
+
private extractAccessToken;
|
|
117
|
+
/** 浏览器直连官网只用 CORS 放行的 Authorization,避免 meme-web-access 被预检拦成 Failed to fetch。 */
|
|
118
|
+
private requestHeaders;
|
|
119
|
+
/** 浏览器直连 four.meme 若仍被 CORS 拦成 Failed to fetch,回退到更早的限流错误。 */
|
|
120
|
+
private preferRateLimitError;
|
|
121
|
+
private shouldRetryPrivateApi;
|
|
115
122
|
private postPrivateJson;
|
|
116
123
|
generateNonce(req: GenerateNonceReq): Promise<string>;
|
|
117
124
|
loginDex(body: LoginReq): Promise<string>;
|
|
@@ -134,6 +141,7 @@ export declare class FourClient {
|
|
|
134
141
|
*/
|
|
135
142
|
private isCorsOrNetworkError;
|
|
136
143
|
private isInvalidJsonError;
|
|
144
|
+
private isNotLoginError;
|
|
137
145
|
private getFilenameFromBlob;
|
|
138
146
|
createToken(accessToken: string, req: CreateTokenReq): Promise<CreateTokenResp>;
|
|
139
147
|
getPublicConfig(): Promise<FourPublicConfig>;
|
|
@@ -112,20 +112,20 @@ const RATE_LIMIT_CODES = new Set([-1038, -1013]);
|
|
|
112
112
|
/** Cloudflare Worker 代理(独立出口 IP,主线路被封时换这条) */
|
|
113
113
|
const FOUR_MEME_PROXY_API = 'https://bscfourapi.emit.tools';
|
|
114
114
|
/**
|
|
115
|
-
* Four.meme 官方 API
|
|
116
|
-
*
|
|
115
|
+
* Four.meme 官方 API。浏览器走这条才是用户自己的 IP,不会被共用代理连坐封禁。
|
|
116
|
+
* 官网 CORS 允许 Authorization,不允许 meme-web-access;浏览器直连时改用前者。
|
|
117
117
|
*/
|
|
118
118
|
const FOUR_MEME_OFFICIAL_API = 'https://four.meme/meme-api';
|
|
119
119
|
export class FourClient {
|
|
120
120
|
baseUrl;
|
|
121
121
|
uploadUrls;
|
|
122
122
|
constructor(cfg) {
|
|
123
|
-
// 默认使用 Cloudflare Workers 代理,已配置 CORS,浏览器可直接使用
|
|
124
123
|
this.baseUrl = cfg?.baseUrl || FOUR_MEME_PROXY_API;
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
124
|
+
const urls = [this.baseUrl, ...(cfg?.uploadFallbackUrls || []), FOUR_MEME_PROXY_API, FOUR_MEME_OFFICIAL_API];
|
|
125
|
+
// 浏览器先打官网(用户 IP);Node 仍先走调用方传入的代理
|
|
126
|
+
this.uploadUrls = this.isBrowserEnv()
|
|
127
|
+
? [...new Set([FOUR_MEME_OFFICIAL_API, ...urls])]
|
|
128
|
+
: [...new Set(urls)];
|
|
129
129
|
}
|
|
130
130
|
attachApiCode(err, code) {
|
|
131
131
|
const n = Number(code);
|
|
@@ -133,14 +133,61 @@ export class FourClient {
|
|
|
133
133
|
err.apiCode = n;
|
|
134
134
|
return err;
|
|
135
135
|
}
|
|
136
|
+
isBrowserEnv() {
|
|
137
|
+
return typeof window !== 'undefined';
|
|
138
|
+
}
|
|
139
|
+
extractAccessToken(headers) {
|
|
140
|
+
if (!headers)
|
|
141
|
+
return undefined;
|
|
142
|
+
for (const [key, value] of Object.entries(headers)) {
|
|
143
|
+
const k = key.toLowerCase();
|
|
144
|
+
if (k === 'meme-web-access' || k === 'authorization')
|
|
145
|
+
return value;
|
|
146
|
+
}
|
|
147
|
+
return undefined;
|
|
148
|
+
}
|
|
149
|
+
/** 浏览器直连官网只用 CORS 放行的 Authorization,避免 meme-web-access 被预检拦成 Failed to fetch。 */
|
|
150
|
+
requestHeaders(baseUrl, headers) {
|
|
151
|
+
const next = { ...(headers || {}) };
|
|
152
|
+
const token = this.extractAccessToken(next);
|
|
153
|
+
for (const key of Object.keys(next)) {
|
|
154
|
+
if (key.toLowerCase() === 'meme-web-access')
|
|
155
|
+
delete next[key];
|
|
156
|
+
}
|
|
157
|
+
if (!token)
|
|
158
|
+
return next;
|
|
159
|
+
const raw = token.replace(/^Bearer\s+/i, '');
|
|
160
|
+
if (this.isBrowserEnv() && baseUrl === FOUR_MEME_OFFICIAL_API) {
|
|
161
|
+
next.Authorization = raw;
|
|
162
|
+
return next;
|
|
163
|
+
}
|
|
164
|
+
next['meme-web-access'] = raw;
|
|
165
|
+
delete next.Authorization;
|
|
166
|
+
return next;
|
|
167
|
+
}
|
|
168
|
+
/** 浏览器直连 four.meme 若仍被 CORS 拦成 Failed to fetch,回退到更早的限流错误。 */
|
|
169
|
+
preferRateLimitError(lastError, rateLimitError, fallback) {
|
|
170
|
+
if (rateLimitError && (!lastError || this.isCorsOrNetworkError(lastError)))
|
|
171
|
+
return rateLimitError;
|
|
172
|
+
return lastError ?? rateLimitError ?? new Error(fallback);
|
|
173
|
+
}
|
|
174
|
+
shouldRetryPrivateApi(err, baseUrl, canRetry) {
|
|
175
|
+
if (!canRetry)
|
|
176
|
+
return false;
|
|
177
|
+
return (this.isRateLimitError(err) ||
|
|
178
|
+
this.isCorsOrNetworkError(err) ||
|
|
179
|
+
this.isInvalidJsonError(err) ||
|
|
180
|
+
(this.isNotLoginError(err) && baseUrl === FOUR_MEME_OFFICIAL_API));
|
|
181
|
+
}
|
|
136
182
|
async postPrivateJson(path, failLabel, init) {
|
|
137
183
|
let lastError = null;
|
|
184
|
+
let rateLimitError = null;
|
|
138
185
|
for (let i = 0; i < this.uploadUrls.length; i++) {
|
|
139
186
|
const baseUrl = this.uploadUrls[i];
|
|
140
187
|
try {
|
|
141
188
|
const r = await fetch(`${baseUrl}${path}`, {
|
|
142
189
|
method: 'POST',
|
|
143
|
-
headers: { 'Content-Type': 'application/json', ...(init.headers
|
|
190
|
+
headers: { 'Content-Type': 'application/json', ...this.requestHeaders(baseUrl, init.headers) },
|
|
144
191
|
body: init.body,
|
|
145
192
|
});
|
|
146
193
|
const j = (await r.json());
|
|
@@ -152,14 +199,14 @@ export class FourClient {
|
|
|
152
199
|
catch (e) {
|
|
153
200
|
const err = e instanceof Error ? e : new Error(getErrorMessageFromUnknown(e));
|
|
154
201
|
lastError = err;
|
|
155
|
-
|
|
156
|
-
|
|
202
|
+
if (this.isRateLimitError(err))
|
|
203
|
+
rateLimitError = err;
|
|
204
|
+
if (this.shouldRetryPrivateApi(err, baseUrl, i < this.uploadUrls.length - 1))
|
|
157
205
|
continue;
|
|
158
|
-
}
|
|
159
206
|
break;
|
|
160
207
|
}
|
|
161
208
|
}
|
|
162
|
-
throw lastError
|
|
209
|
+
throw this.preferRateLimitError(lastError, rateLimitError, failLabel);
|
|
163
210
|
}
|
|
164
211
|
async generateNonce(req) {
|
|
165
212
|
return this.postPrivateJson('/v1/private/user/nonce/generate', 'generateNonce failed', {
|
|
@@ -188,40 +235,30 @@ export class FourClient {
|
|
|
188
235
|
const finalFilename = filename || this.getFilenameFromBlob(file);
|
|
189
236
|
let lastError = null;
|
|
190
237
|
// ✅ 依次尝试所有上传端点(代理 → 自定义备用 → 官方直连兜底)
|
|
238
|
+
let rateLimitError = null;
|
|
191
239
|
for (let i = 0; i < this.uploadUrls.length; i++) {
|
|
192
240
|
const baseUrl = this.uploadUrls[i];
|
|
193
|
-
const isOfficial = baseUrl === FOUR_MEME_OFFICIAL_API;
|
|
194
241
|
try {
|
|
195
242
|
const imgUrl = await this.doUpload(baseUrl, accessToken, file, finalFilename);
|
|
196
|
-
// 上传成功,写入持久化缓存(localStorage)
|
|
197
243
|
setCachedUrl(hash, imgUrl);
|
|
198
244
|
return imgUrl;
|
|
199
245
|
}
|
|
200
246
|
catch (e) {
|
|
201
247
|
const err = e instanceof Error ? e : new Error(getErrorMessageFromUnknown(e));
|
|
202
248
|
lastError = err;
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
}
|
|
207
|
-
if (this.isCorsOrNetworkError(err) && canRetry) {
|
|
208
|
-
// CORS/网络错误(常见于浏览器直连官方 API)→ 跳过,继续尝试下一个
|
|
209
|
-
console.warn(`[FourClient] 端点 ${baseUrl} 请求失败(${isOfficial ? 'CORS限制' : '网络错误'}),跳过`);
|
|
249
|
+
if (this.isRateLimitError(err))
|
|
250
|
+
rateLimitError = err;
|
|
251
|
+
if (this.shouldRetryPrivateApi(err, baseUrl, i < this.uploadUrls.length - 1))
|
|
210
252
|
continue;
|
|
211
|
-
}
|
|
212
|
-
// 无法重试,跳出
|
|
213
253
|
break;
|
|
214
254
|
}
|
|
215
255
|
}
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
if (isRateLimit) {
|
|
256
|
+
const chosen = this.preferRateLimitError(lastError, rateLimitError, 'upload failed: all endpoints exhausted');
|
|
257
|
+
if (this.isRateLimitError(chosen)) {
|
|
219
258
|
throw new Error(`图片上传失败:Four.meme 限流(每日配额或 15 分钟封禁)。请稍后再试。` +
|
|
220
|
-
`\n原始错误: ${getErrorMessageFromUnknown(
|
|
259
|
+
`\n原始错误: ${getErrorMessageFromUnknown(chosen)}`);
|
|
221
260
|
}
|
|
222
|
-
|
|
223
|
-
throw lastError;
|
|
224
|
-
throw new Error('upload failed: all endpoints exhausted');
|
|
261
|
+
throw chosen;
|
|
225
262
|
}
|
|
226
263
|
/**
|
|
227
264
|
* 执行单次上传请求
|
|
@@ -231,7 +268,7 @@ export class FourClient {
|
|
|
231
268
|
form.append('file', file, filename);
|
|
232
269
|
const r = await fetch(`${baseUrl}/v1/private/token/upload`, {
|
|
233
270
|
method: 'POST',
|
|
234
|
-
headers: { 'meme-web-access': accessToken },
|
|
271
|
+
headers: this.requestHeaders(baseUrl, { 'meme-web-access': accessToken }),
|
|
235
272
|
body: form,
|
|
236
273
|
});
|
|
237
274
|
const j = await r.json();
|
|
@@ -262,6 +299,12 @@ export class FourClient {
|
|
|
262
299
|
isInvalidJsonError(e) {
|
|
263
300
|
return e instanceof SyntaxError || /unexpected token|not valid json/i.test(getErrorMessageFromUnknown(e));
|
|
264
301
|
}
|
|
302
|
+
isNotLoginError(e) {
|
|
303
|
+
const code = Number(e.apiCode);
|
|
304
|
+
if (code === -1004)
|
|
305
|
+
return true;
|
|
306
|
+
return /user not login/i.test(getErrorMessageFromUnknown(e));
|
|
307
|
+
}
|
|
265
308
|
getFilenameFromBlob(blob) {
|
|
266
309
|
const mimeToExt = {
|
|
267
310
|
'image/jpeg': 'jpg',
|
|
@@ -20,9 +20,10 @@
|
|
|
20
20
|
* });
|
|
21
21
|
* ```
|
|
22
22
|
*/
|
|
23
|
-
import {
|
|
23
|
+
import { Transaction, formatEther } from 'ethers';
|
|
24
24
|
import { CHAINS } from '../constants/index.js';
|
|
25
25
|
import { getErrorMessageFromUnknown } from '../index.js';
|
|
26
|
+
import { createProvider } from '../../utils/provider-factory.js';
|
|
26
27
|
/**
|
|
27
28
|
* Merkle 客户端
|
|
28
29
|
*/
|
|
@@ -43,10 +44,7 @@ export class MerkleClient {
|
|
|
43
44
|
? CHAINS.BSC.rpcUrls.backup // bsc-dataseed1
|
|
44
45
|
: CHAINS.ETHEREUM.rpcUrls.default;
|
|
45
46
|
const rpcUrl = config.customRpcUrl || defaultRpc;
|
|
46
|
-
this.provider =
|
|
47
|
-
chainId: this.chainId,
|
|
48
|
-
name: this.chainName,
|
|
49
|
-
});
|
|
47
|
+
this.provider = createProvider(rpcUrl, this.chainId, this.chainName);
|
|
50
48
|
}
|
|
51
49
|
/**
|
|
52
50
|
* 获取 Provider
|
|
@@ -3,13 +3,14 @@
|
|
|
3
3
|
*
|
|
4
4
|
* 只读查询授权额度、批量查询(Multicall3)、智能路由检查等。
|
|
5
5
|
*/
|
|
6
|
-
import { Contract
|
|
6
|
+
import { Contract } from 'ethers';
|
|
7
|
+
import { createProvider } from '../../utils/provider-factory.js';
|
|
7
8
|
import { ADDRESSES, getMulticall3Address } from '../constants/index.js';
|
|
8
9
|
import { ERC20_ABI, MULTICALL3_ABI } from '../../abis/common.js';
|
|
9
10
|
import { getErrorMessageFromUnknown } from '../foundation.js';
|
|
10
11
|
import { validateContractAddress, resolveSpenderAddress, FOUR_MEME_V1_PROXY, FOUR_MEME_V2_PROXY, } from './helpers.js';
|
|
11
12
|
async function checkAllowanceForSpender(rpcUrl, token, owner, amount, spender, spenderLabel) {
|
|
12
|
-
const provider =
|
|
13
|
+
const provider = createProvider(rpcUrl);
|
|
13
14
|
await validateContractAddress(provider, token, 'Token');
|
|
14
15
|
await validateContractAddress(provider, spender, spenderLabel);
|
|
15
16
|
const erc20 = new Contract(token, ERC20_ABI, provider);
|
|
@@ -76,7 +77,7 @@ export async function checkFlapSellApprovalBatch(chain, rpcUrl, token, owners, r
|
|
|
76
77
|
MORPH: ADDRESSES.MORPH.FlapPortal,
|
|
77
78
|
MONAD: ADDRESSES.MONAD.FlapPortal,
|
|
78
79
|
};
|
|
79
|
-
const provider =
|
|
80
|
+
const provider = createProvider(rpcUrl);
|
|
80
81
|
// ✅ 并行验证 token 和代理合约地址
|
|
81
82
|
await Promise.all([
|
|
82
83
|
validateContractAddress(provider, token, 'Token'),
|
|
@@ -151,7 +152,7 @@ export async function checkAllowance(chain, platform, rpcUrl, tokenAddress, owne
|
|
|
151
152
|
* @returns 当前授权额度(bigint)
|
|
152
153
|
*/
|
|
153
154
|
export async function checkAllowanceRaw(rpcUrl, tokenAddress, ownerAddress, spenderAddress) {
|
|
154
|
-
const provider =
|
|
155
|
+
const provider = createProvider(rpcUrl);
|
|
155
156
|
// ✅ 规范化地址(转为小写,避免 checksum 错误)
|
|
156
157
|
const normalizedToken = tokenAddress.toLowerCase();
|
|
157
158
|
const normalizedOwner = ownerAddress.toLowerCase();
|
|
@@ -193,7 +194,7 @@ export async function checkAllowanceBatch(chain, platform, rpcUrl, tokenAddress,
|
|
|
193
194
|
* @returns 每个地址的授权额度数组
|
|
194
195
|
*/
|
|
195
196
|
export async function checkAllowanceBatchRaw(rpcUrl, tokenAddress, ownerAddresses, spenderAddress) {
|
|
196
|
-
const provider =
|
|
197
|
+
const provider = createProvider(rpcUrl);
|
|
197
198
|
// ✅ 规范化地址(转为小写,避免 checksum 错误)
|
|
198
199
|
const normalizedToken = tokenAddress.toLowerCase();
|
|
199
200
|
const normalizedOwners = ownerAddresses.map((addr) => addr.toLowerCase());
|
|
@@ -5,6 +5,8 @@
|
|
|
5
5
|
* 此文件不会被 barrel 对外导出。
|
|
6
6
|
*/
|
|
7
7
|
import { JsonRpcProvider } from 'ethers';
|
|
8
|
+
/** 只接受 0x + 40 hex。禁止给符号(如 SPCXB)补 0x 再去 getCode/ENS。 */
|
|
9
|
+
export declare function requireEvmAddress(value: string | undefined, label: string): string;
|
|
8
10
|
export declare const MAX_UINT256: bigint;
|
|
9
11
|
export declare const MAX_APPROVAL_THRESHOLD: bigint;
|
|
10
12
|
/**
|
|
@@ -4,8 +4,16 @@
|
|
|
4
4
|
* 包含地址校验、最大授权常量、Sell 代理地址解析、spender 智能路由解析等内部辅助。
|
|
5
5
|
* 此文件不会被 barrel 对外导出。
|
|
6
6
|
*/
|
|
7
|
+
import { getAddress, isAddress } from 'ethers';
|
|
7
8
|
import { ADDRESSES, ZERO_ADDRESS } from '../constants/index.js';
|
|
8
9
|
import { getErrorMessageFromUnknown } from '../foundation.js';
|
|
10
|
+
/** 只接受 0x + 40 hex。禁止给符号(如 SPCXB)补 0x 再去 getCode/ENS。 */
|
|
11
|
+
export function requireEvmAddress(value, label) {
|
|
12
|
+
const trimmed = String(value ?? '').trim();
|
|
13
|
+
if (isAddress(trimmed))
|
|
14
|
+
return getAddress(trimmed);
|
|
15
|
+
throw new Error(`${label} 不是有效的 EVM 地址: ${trimmed || '(空)'}`);
|
|
16
|
+
}
|
|
9
17
|
// ============================================================================
|
|
10
18
|
// ✅ Max approval(与 BSC 策略一致:阈值判断,避免频繁重复授权)
|
|
11
19
|
// ============================================================================
|
|
@@ -15,11 +23,8 @@ export const MAX_APPROVAL_THRESHOLD = MAX_UINT256 / 2n;
|
|
|
15
23
|
* 验证合约地址是否有效(是否部署了代码)
|
|
16
24
|
*/
|
|
17
25
|
export async function validateContractAddress(provider, address, label) {
|
|
26
|
+
const normalizedAddress = requireEvmAddress(address, label);
|
|
18
27
|
try {
|
|
19
|
-
// ✅ 先规范化地址(转为 checksum 格式),避免 ethers v6 的严格校验报错
|
|
20
|
-
const normalizedAddress = address.toLowerCase().startsWith('0x')
|
|
21
|
-
? address.toLowerCase()
|
|
22
|
-
: `0x${address.toLowerCase()}`;
|
|
23
28
|
const code = await provider.getCode(normalizedAddress);
|
|
24
29
|
if (code === '0x' || code.length <= 2) {
|
|
25
30
|
throw new Error(`❌ ${label} 地址无效或未部署合约: ${address}`);
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
import { requireEvmAddress } from './helpers.js';
|
|
3
|
+
describe('requireEvmAddress', () => {
|
|
4
|
+
it('accepts SPCXB checksum address', () => {
|
|
5
|
+
expect(requireEvmAddress('0xbe9D156892E55e7154BcD3cB0FEA677F9D3103E1', '计价代币')).toBe('0xbe9D156892E55e7154BcD3cB0FEA677F9D3103E1');
|
|
6
|
+
});
|
|
7
|
+
it('rejects ticker symbols that used to trigger ENS', () => {
|
|
8
|
+
expect(() => requireEvmAddress('SPCXB', '计价代币')).toThrow('计价代币 不是有效的 EVM 地址: SPCXB');
|
|
9
|
+
});
|
|
10
|
+
});
|
|
@@ -10,6 +10,7 @@ import { JsonRpcProvider } from 'ethers';
|
|
|
10
10
|
import { QUOTE_CONFIG, QUOTE_V3_FEE_TIERS } from '../constants/quote.js';
|
|
11
11
|
import type { QuoteParams, QuoteResult } from './types.js';
|
|
12
12
|
export { QUOTE_CONFIG, QUOTE_V3_FEE_TIERS };
|
|
13
|
+
export declare function buildV2QuotePaths(tokenIn: string, tokenOut: string, hops: readonly string[]): string[][];
|
|
13
14
|
/**
|
|
14
15
|
* V2 报价(直接路径 + 多跳路径)
|
|
15
16
|
* ✅ 优化:并行尝试直接路径和多跳路径
|
|
@@ -22,6 +23,7 @@ export { QUOTE_CONFIG, QUOTE_V3_FEE_TIERS };
|
|
|
22
23
|
* @returns 报价结果
|
|
23
24
|
*/
|
|
24
25
|
export declare function quoteV2(provider: JsonRpcProvider, tokenIn: string, tokenOut: string, amountIn: bigint, chain: string, routerOverride?: string): Promise<QuoteResult>;
|
|
26
|
+
export declare function resolveQuotedV2Path(provider: JsonRpcProvider, tokenIn: string, tokenOut: string, amountIn: bigint, chain: string, routerOverride?: string): Promise<string[]>;
|
|
25
27
|
/**
|
|
26
28
|
* V3 报价(多费率尝试 + 多跳路径)
|
|
27
29
|
* ✅ 优化:并行尝试多个费率
|
|
@@ -12,6 +12,78 @@ import { calculateBundleSwapRoundTripProfit } from '../constants/profit.js';
|
|
|
12
12
|
import { QUOTE_CONFIG, QUOTE_V3_FEE_TIERS, getQuoteChainKey } from '../constants/quote.js';
|
|
13
13
|
const V2_ROUTER_ABI = V2_ROUTER_QUOTE_ABI;
|
|
14
14
|
export { QUOTE_CONFIG, QUOTE_V3_FEE_TIERS };
|
|
15
|
+
/** BSC 股票币/额外计价,仅内部 hop;不进 QUOTE_CONFIG,避免改公共类型 */
|
|
16
|
+
const BSC_V2_HOP_TOKENS = [
|
|
17
|
+
'0x6CEd5c6d3F913b48D59Fa07AbBfe9060C409087C', // ANTFUN
|
|
18
|
+
'0x8d0D000Ee44948FC98c9B98A4FA4921476f08B0d', // USD1
|
|
19
|
+
'0xcE24439F2D9C6a2289F741120FE202248B666666', // U
|
|
20
|
+
'0x000Ae314E2A2172a039B26378814C252734f556A', // ASTER
|
|
21
|
+
'0x61a10E8556BEd032eA176330e7F17D6a12a10000', // UUSD
|
|
22
|
+
'0x924fa68a0FC644485b8df8AbfA0A41C2e7744444', // 币安人生
|
|
23
|
+
'0x7130d2A12B9BCbFAe4f2634d864A1Ee1Ce3Ead9c', // BTCB
|
|
24
|
+
'0x570A5D26f7765Ecb712C0924E4De545B89fD43dF', // SOL
|
|
25
|
+
'0x2170Ed0880ac9A755fd29B2688956BD959F933F8', // ETH
|
|
26
|
+
'0xbe9D156892E55e7154BcD3cB0FEA677F9D3103E1', // SPCXB
|
|
27
|
+
'0xCA750eF65f295BBECd685Abf54e82CAf297BDB61', // SKHYB
|
|
28
|
+
'0x7138b48df7D98D7e3cc221BfE7192D0a178182D8', // SPYB
|
|
29
|
+
'0x21cAef8A43163Eea865baeE23b9C2E327696A3bf', // XAUT
|
|
30
|
+
'0x205812CdBed920aFf76C6580abD681a46D11efc7', // QQQB
|
|
31
|
+
'0x02Fca66C1D1aFB4E2A7884261eB00F63598a7436', // NVDAB
|
|
32
|
+
'0x431a3BEE82E2ca41e49895CbECE5bB0F76A89b7A', // AAPLB
|
|
33
|
+
'0x5b1910eAaD6450E50f816082Aa078C41F10C292f', // TSLAB
|
|
34
|
+
'0x80106cb3EAD06659A5ad19DF39D9b4733863B9b0', // MSFTB
|
|
35
|
+
'0x3F53De71c126BdaBAe20f9cD64848d317f6C3238', // GOOGLB
|
|
36
|
+
'0xA394dCEa3fd3847fD793afBFd163E2e3858B7c65', // HOODB
|
|
37
|
+
'0x4eF9d3062c7F6ebA4AAE4990c5036598C6eff4ec', // BABAB
|
|
38
|
+
'0x46cEeFDa28Dd7207059ed19B0acdc026955bb15C', // GMEB
|
|
39
|
+
'0xD6829Ea836b6FA224d099D40E54B31262f874631', // NFLXB
|
|
40
|
+
'0xE87afb3076AeB0f9B14E368DE8145ae6a2826A14', // MSTRB
|
|
41
|
+
'0xF2ec508422174Ee564de98187db9359D318AFB6b', // DJTB
|
|
42
|
+
'0xE28Cd11C99AF2df76bb8aDA4Cd0ef3904378280F', // SOXSB
|
|
43
|
+
'0xd97d097a89113fa59b76c572E5b2Eb647E8eefaf', // SOXLB
|
|
44
|
+
'0x5fd86da9B05abE396fe9d02a4A213A7c00556503', // MRNAB
|
|
45
|
+
'0x4af1D41cd9dD950dcA43984b43aaA2A8702714Ac', // FLNCB
|
|
46
|
+
'0x9b8E987e6fEc8Cf1380C4dcA7071e2C7853AEEA1', // FXIon
|
|
47
|
+
'0x3548Da95a9eFFE481e8604664d75e95821e557F5', // BMNRB
|
|
48
|
+
'0x91fc7371d6dE682A1e8CFcB4EB7dA693312A03a4', // BILIon
|
|
49
|
+
];
|
|
50
|
+
export function buildV2QuotePaths(tokenIn, tokenOut, hops) {
|
|
51
|
+
const tokenInLower = tokenIn.toLowerCase();
|
|
52
|
+
const tokenOutLower = tokenOut.toLowerCase();
|
|
53
|
+
const paths = [[tokenIn, tokenOut]];
|
|
54
|
+
for (const hop of hops) {
|
|
55
|
+
const hopLower = hop.toLowerCase();
|
|
56
|
+
if (tokenInLower === hopLower || tokenOutLower === hopLower)
|
|
57
|
+
continue;
|
|
58
|
+
paths.push([tokenIn, hop, tokenOut]);
|
|
59
|
+
}
|
|
60
|
+
return paths;
|
|
61
|
+
}
|
|
62
|
+
function hopTokensForChain(chainUpper) {
|
|
63
|
+
return chainUpper === 'BSC' ? BSC_V2_HOP_TOKENS : [];
|
|
64
|
+
}
|
|
65
|
+
async function bestAmountsOut(router, amountIn, paths) {
|
|
66
|
+
const quotePromises = paths.map(async (path) => {
|
|
67
|
+
try {
|
|
68
|
+
const amounts = await router.getAmountsOut(amountIn, path);
|
|
69
|
+
const amountOut = amounts[amounts.length - 1];
|
|
70
|
+
return { amountOut, path, success: amountOut > 0n };
|
|
71
|
+
}
|
|
72
|
+
catch {
|
|
73
|
+
return { amountOut: 0n, path, success: false };
|
|
74
|
+
}
|
|
75
|
+
});
|
|
76
|
+
const results = await Promise.all(quotePromises);
|
|
77
|
+
let best;
|
|
78
|
+
for (const result of results) {
|
|
79
|
+
if (!result.success)
|
|
80
|
+
continue;
|
|
81
|
+
if (!best || result.amountOut > best.amountOut) {
|
|
82
|
+
best = { amountOut: result.amountOut, path: result.path };
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
return best;
|
|
86
|
+
}
|
|
15
87
|
/**
|
|
16
88
|
* V2 报价(直接路径 + 多跳路径)
|
|
17
89
|
* ✅ 优化:并行尝试直接路径和多跳路径
|
|
@@ -34,45 +106,29 @@ export async function quoteV2(provider, tokenIn, tokenOut, amountIn, chain, rout
|
|
|
34
106
|
}
|
|
35
107
|
const routerAddress = routerOverride || config.v2Router;
|
|
36
108
|
const router = new Contract(routerAddress, V2_ROUTER_ABI, provider);
|
|
37
|
-
const
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
const paths = [];
|
|
41
|
-
// 直接路径
|
|
42
|
-
paths.push([tokenIn, tokenOut]);
|
|
43
|
-
// 多跳路径(通过稳定币)
|
|
44
|
-
for (const stableCoin of config.stableCoins) {
|
|
45
|
-
const stableLower = stableCoin.toLowerCase();
|
|
46
|
-
if (tokenInLower !== stableLower && tokenOutLower !== stableLower) {
|
|
47
|
-
paths.push([tokenIn, stableCoin, tokenOut]);
|
|
48
|
-
}
|
|
109
|
+
const stableBest = await bestAmountsOut(router, amountIn, buildV2QuotePaths(tokenIn, tokenOut, config.stableCoins));
|
|
110
|
+
if (stableBest) {
|
|
111
|
+
return { amountOut: stableBest.amountOut, path: stableBest.path };
|
|
49
112
|
}
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
const amounts = await router.getAmountsOut(amountIn, path);
|
|
54
|
-
const amountOut = amounts[amounts.length - 1];
|
|
55
|
-
return { amountOut, path, success: amountOut > 0n };
|
|
56
|
-
}
|
|
57
|
-
catch {
|
|
58
|
-
return { amountOut: 0n, path, success: false };
|
|
59
|
-
}
|
|
60
|
-
});
|
|
61
|
-
const results = await Promise.all(quotePromises);
|
|
62
|
-
// 取报价最高的路径:薄直连池(如 ANTFUN/WBNB)会先成功但少报,利润看起来不像万六
|
|
63
|
-
let best;
|
|
64
|
-
for (const result of results) {
|
|
65
|
-
if (!result.success)
|
|
66
|
-
continue;
|
|
67
|
-
if (!best || result.amountOut > best.amountOut) {
|
|
68
|
-
best = { amountOut: result.amountOut, path: result.path };
|
|
69
|
-
}
|
|
113
|
+
const extraHops = hopTokensForChain(chainUpper);
|
|
114
|
+
if (extraHops.length === 0) {
|
|
115
|
+
return { amountOut: 0n };
|
|
70
116
|
}
|
|
71
|
-
|
|
72
|
-
|
|
117
|
+
const hopOnly = buildV2QuotePaths(tokenIn, tokenOut, extraHops).filter((path) => path.length >= 3);
|
|
118
|
+
const hopBest = await bestAmountsOut(router, amountIn, hopOnly);
|
|
119
|
+
if (hopBest) {
|
|
120
|
+
return { amountOut: hopBest.amountOut, path: hopBest.path };
|
|
73
121
|
}
|
|
74
122
|
return { amountOut: 0n };
|
|
75
123
|
}
|
|
124
|
+
export async function resolveQuotedV2Path(provider, tokenIn, tokenOut, amountIn, chain, routerOverride) {
|
|
125
|
+
if (amountIn <= 0n)
|
|
126
|
+
return [tokenIn, tokenOut];
|
|
127
|
+
const quoted = await quoteV2(provider, tokenIn, tokenOut, amountIn, chain, routerOverride);
|
|
128
|
+
if (quoted.path && quoted.path.length >= 2)
|
|
129
|
+
return quoted.path;
|
|
130
|
+
return [tokenIn, tokenOut];
|
|
131
|
+
}
|
|
76
132
|
// ============================================================================
|
|
77
133
|
// V3 报价
|
|
78
134
|
// ============================================================================
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
import { buildV2QuotePaths } from './quote-helpers.js';
|
|
3
|
+
const WBNB = '0xbb4CdB9CBd36B01bD1cBaEBF2De08d9173bc095c';
|
|
4
|
+
const SPCXB = '0xbe9D156892E55e7154BcD3cB0FEA677F9D3103E1';
|
|
5
|
+
const TOKEN = '0x7Db2F0c15f74aB0Dc0D4f49650dD60C45FFB7777';
|
|
6
|
+
const USDT = '0x55d398326f99059fF775485246999027B3197955';
|
|
7
|
+
describe('buildV2QuotePaths', () => {
|
|
8
|
+
it('keeps direct path and hops through SPCXB for native stock-token buys', () => {
|
|
9
|
+
const paths = buildV2QuotePaths(WBNB, TOKEN, [USDT, SPCXB]);
|
|
10
|
+
expect(paths[0]).toEqual([WBNB, TOKEN]);
|
|
11
|
+
expect(paths).toContainEqual([WBNB, SPCXB, TOKEN]);
|
|
12
|
+
expect(paths).toContainEqual([WBNB, USDT, TOKEN]);
|
|
13
|
+
});
|
|
14
|
+
it('does not hop through the input token itself', () => {
|
|
15
|
+
const paths = buildV2QuotePaths(SPCXB, TOKEN, [SPCXB, USDT]);
|
|
16
|
+
expect(paths).toEqual([
|
|
17
|
+
[SPCXB, TOKEN],
|
|
18
|
+
[SPCXB, USDT, TOKEN],
|
|
19
|
+
]);
|
|
20
|
+
});
|
|
21
|
+
});
|
|
@@ -1,15 +1,12 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { Wallet } from 'ethers';
|
|
2
2
|
import { BLOCKRAZOR_BUILDER_EOA, CHAINS, getChainName } from './constants/index.js';
|
|
3
3
|
import { getTxType } from './gas/bundle-gas.js';
|
|
4
|
+
import { createProvider } from '../utils/provider-factory.js';
|
|
4
5
|
export function resolveChainName(chainIdNum) {
|
|
5
6
|
return getChainName(chainIdNum);
|
|
6
7
|
}
|
|
7
8
|
export function createSignProvider(rpcUrl, chainIdNum, chainName) {
|
|
8
|
-
return
|
|
9
|
-
chainId: chainIdNum,
|
|
10
|
-
name: chainName ?? resolveChainName(chainIdNum),
|
|
11
|
-
ensAddress: undefined,
|
|
12
|
-
});
|
|
9
|
+
return createProvider(rpcUrl, chainIdNum, chainName ?? resolveChainName(chainIdNum));
|
|
13
10
|
}
|
|
14
11
|
export function createSignWallet(fromPrivateKey, provider) {
|
|
15
12
|
return new Wallet(fromPrivateKey, provider);
|
|
@@ -7,7 +7,8 @@ import { getGasLimit } from '../../core/gas/bundle-gas.js';
|
|
|
7
7
|
import { NonceManager, buildProfitHopTransactions, PROFIT_HOP_COUNT, buildGasFields, } from '../../utils/bundle-helpers.js';
|
|
8
8
|
import { getProfitRecipient, CHAINS, GAS_LIMITS } from '../../core/constants/index.js';
|
|
9
9
|
import { ERC20_ABI } from '../../abis/common.js';
|
|
10
|
-
import { getTokenToNativeQuote } from '../../core/quote/quote-helpers.js';
|
|
10
|
+
import { getTokenToNativeQuote, resolveQuotedV2Path } from '../../core/quote/quote-helpers.js';
|
|
11
|
+
import { requireEvmAddress } from '../../core/erc20/helpers.js';
|
|
11
12
|
import { CHAIN_IDS, DIRECT_ROUTERS, SWAP_ROUTER02_V2_ABI, V2_ROUTER_ABI, buildBribeTransaction, buildProfitTransactionWithHops, calculateProfitAmount, encodeDYORMulticall, encodeDYORRefundETH, encodeDYORSwapExactTokensForTokens, findMaxFlowIndex, filterEniNativeBuyIndices, getBribeAmount, getCachedProvider, getDeadline, getGasPrice, getWrappedNative, isDYORSwap, isNativeToken, isSwapRouter02, } from './direct-router-helpers.js';
|
|
12
13
|
export async function directV2BatchBuy(params) {
|
|
13
14
|
const { chain, privateKeys, buyAmounts, tokenAddress, routerAddress, quoteToken, quoteTokenDecimals = 18, startNonces, config, } = params;
|
|
@@ -15,29 +16,33 @@ export async function directV2BatchBuy(params) {
|
|
|
15
16
|
throw new Error('privateKeys 和 buyAmounts 长度必须相同');
|
|
16
17
|
}
|
|
17
18
|
const chainId = config.chainId || CHAIN_IDS[chain.toUpperCase()] || CHAINS.BSC.chainId;
|
|
18
|
-
|
|
19
|
+
const tokenOut = requireEvmAddress(tokenAddress, '目标代币');
|
|
20
|
+
const router = requireEvmAddress(routerAddress, 'Router');
|
|
19
21
|
const provider = getCachedProvider(config.rpcUrl, chainId, chain);
|
|
20
22
|
const useNative = isNativeToken(quoteToken);
|
|
21
23
|
const wrappedNative = getWrappedNative(chain);
|
|
24
|
+
const inputToken = useNative ? wrappedNative : requireEvmAddress(quoteToken, '计价代币');
|
|
22
25
|
// 创建钱包
|
|
23
26
|
const wallets = privateKeys.map((pk) => new Wallet(pk, provider));
|
|
24
27
|
// ✅ 预先计算所有金额(同步操作,无 RPC)
|
|
25
28
|
const flowAmounts = buyAmounts.map((amount) => ethers.parseUnits(amount, quoteTokenDecimals));
|
|
26
29
|
const totalFlowWei = flowAmounts.reduce((sum, amt) => sum + amt, 0n);
|
|
27
30
|
const baseProfitWei = calculateProfitAmount(totalFlowWei);
|
|
31
|
+
const probeAmount = flowAmounts.find((amt) => amt > 0n) ?? 0n;
|
|
28
32
|
// ✅ 方案 B:并行获取 nonces、gasPrice、ERC20 报价 以及(非原生代币时)授权额度
|
|
29
|
-
const quoteTokenContract = !useNative
|
|
30
|
-
const [nonces, gasPrice, nativeProfitWei, buyAllowances] = await Promise.all([
|
|
33
|
+
const quoteTokenContract = !useNative ? new Contract(inputToken, ERC20_ABI, provider) : null;
|
|
34
|
+
const [nonces, gasPrice, nativeProfitWei, buyAllowances, quotedPath] = await Promise.all([
|
|
31
35
|
startNonces && startNonces.length === wallets.length
|
|
32
36
|
? Promise.resolve(startNonces)
|
|
33
37
|
: new NonceManager(provider).getNextNoncesForWallets(wallets),
|
|
34
38
|
getGasPrice(provider, config),
|
|
35
|
-
!useNative && baseProfitWei > 0n
|
|
36
|
-
? getTokenToNativeQuote(provider,
|
|
39
|
+
!useNative && baseProfitWei > 0n
|
|
40
|
+
? getTokenToNativeQuote(provider, inputToken, baseProfitWei, chain, 'v2', undefined, router)
|
|
37
41
|
: Promise.resolve(baseProfitWei),
|
|
38
42
|
quoteTokenContract
|
|
39
|
-
? Promise.all(wallets.map((w) => quoteTokenContract.allowance(w.address,
|
|
43
|
+
? Promise.all(wallets.map((w) => quoteTokenContract.allowance(w.address, router).catch(() => 0n)))
|
|
40
44
|
: Promise.resolve(wallets.map(() => ethers.MaxUint256)),
|
|
45
|
+
resolveQuotedV2Path(provider, inputToken, tokenOut, probeAmount, chain, router),
|
|
41
46
|
]);
|
|
42
47
|
// 确定最终利润金额(ENI 链额外补偿利润转账的 gas 成本)
|
|
43
48
|
let profitWei = nativeProfitWei > 0n ? nativeProfitWei : 0n;
|
|
@@ -80,12 +85,9 @@ export async function directV2BatchBuy(params) {
|
|
|
80
85
|
if (activeFlowAmounts[i] <= 0n)
|
|
81
86
|
continue;
|
|
82
87
|
if (activeBuyAllowances[i] < activeFlowAmounts[i]) {
|
|
83
|
-
const approveData = quoteTokenContract.interface.encodeFunctionData('approve', [
|
|
84
|
-
routerAddress,
|
|
85
|
-
ethers.MaxUint256,
|
|
86
|
-
]);
|
|
88
|
+
const approveData = quoteTokenContract.interface.encodeFunctionData('approve', [router, ethers.MaxUint256]);
|
|
87
89
|
const approveTx = await activeWallets[i].signTransaction({
|
|
88
|
-
to:
|
|
90
|
+
to: inputToken,
|
|
89
91
|
data: approveData,
|
|
90
92
|
value: 0n,
|
|
91
93
|
nonce: activeNonces[i],
|
|
@@ -98,12 +100,10 @@ export async function directV2BatchBuy(params) {
|
|
|
98
100
|
}
|
|
99
101
|
}
|
|
100
102
|
}
|
|
101
|
-
|
|
102
|
-
const inputToken = useNative ? wrappedNative : quoteToken;
|
|
103
|
-
const path = [inputToken, tokenAddress];
|
|
103
|
+
const path = quotedPath.length >= 2 ? quotedPath : [inputToken, tokenOut];
|
|
104
104
|
// 判断 Router 类型
|
|
105
|
-
const useSwapRouter02 = isSwapRouter02(chain,
|
|
106
|
-
const useDYORSwap = isDYORSwap(chain,
|
|
105
|
+
const useSwapRouter02 = isSwapRouter02(chain, router);
|
|
106
|
+
const useDYORSwap = isDYORSwap(chain, router);
|
|
107
107
|
const routerIface = useSwapRouter02
|
|
108
108
|
? new ethers.Interface(SWAP_ROUTER02_V2_ABI)
|
|
109
109
|
: new ethers.Interface(V2_ROUTER_ABI);
|
|
@@ -171,7 +171,7 @@ export async function directV2BatchBuy(params) {
|
|
|
171
171
|
const { txData, txValue } = buildTxData(wallet, activeFlowAmounts[i]);
|
|
172
172
|
signPromises.push(wallet
|
|
173
173
|
.signTransaction({
|
|
174
|
-
to:
|
|
174
|
+
to: router,
|
|
175
175
|
data: txData,
|
|
176
176
|
value: txValue,
|
|
177
177
|
nonce: activeNonces[i] + nonceOffsets[i],
|
|
@@ -8,7 +8,8 @@ import { NonceManager, buildProfitHopTransactions, PROFIT_HOP_COUNT, buildGasFie
|
|
|
8
8
|
import { getProfitRecipient, CHAINS, GAS_LIMITS } from '../../core/constants/index.js';
|
|
9
9
|
import { getErrorMessageFromUnknown } from '../../core/index.js';
|
|
10
10
|
import { ERC20_ABI } from '../../abis/common.js';
|
|
11
|
-
import { getTokenToNativeQuote, quoteV2 } from '../../core/quote/quote-helpers.js';
|
|
11
|
+
import { getTokenToNativeQuote, quoteV2, resolveQuotedV2Path } from '../../core/quote/quote-helpers.js';
|
|
12
|
+
import { requireEvmAddress } from '../../core/erc20/helpers.js';
|
|
12
13
|
import { CHAIN_IDS, DIRECT_ROUTERS, SWAP_ROUTER02_V2_ABI, V2_ROUTER_ABI, buildBribeTransaction, buildProfitTransactionWithHops, calculateProfitAmount, encodeDYORMulticall, encodeDYORSwapExactTokensForTokens, encodeDYORUnwrapWETH9, findMaxFlowIndex, getBribeAmount, getCachedProvider, getDeadline, getGasPrice, getWrappedNative, isDYORSwap, isNativeToken, isSwapRouter02, truncateDecimals, } from './direct-router-helpers.js';
|
|
13
14
|
/**
|
|
14
15
|
* V2 批量卖出(直接调用 Router)
|
|
@@ -16,13 +17,15 @@ import { CHAIN_IDS, DIRECT_ROUTERS, SWAP_ROUTER02_V2_ABI, V2_ROUTER_ABI, buildBr
|
|
|
16
17
|
export async function directV2BatchSell(params) {
|
|
17
18
|
const { chain, privateKeys, sellPercentages, sellAmounts, tokenAddress, tokenDecimals: inputDecimals, routerAddress, quoteToken, startNonces, config, } = params;
|
|
18
19
|
const chainId = config.chainId || CHAIN_IDS[chain.toUpperCase()] || CHAINS.BSC.chainId;
|
|
19
|
-
|
|
20
|
+
const tokenIn = requireEvmAddress(tokenAddress, '目标代币');
|
|
21
|
+
const router = requireEvmAddress(routerAddress, 'Router');
|
|
20
22
|
const provider = getCachedProvider(config.rpcUrl, chainId, chain);
|
|
21
23
|
const useNativeOutput = isNativeToken(quoteToken);
|
|
22
24
|
const wrappedNative = getWrappedNative(chain);
|
|
25
|
+
const outputToken = useNativeOutput ? wrappedNative : requireEvmAddress(quoteToken, '计价代币');
|
|
23
26
|
// 创建钱包
|
|
24
27
|
const wallets = privateKeys.map((pk) => new Wallet(pk, provider));
|
|
25
|
-
const tokenContract = new Contract(
|
|
28
|
+
const tokenContract = new Contract(tokenIn, ERC20_ABI, provider);
|
|
26
29
|
// ✅ 性能优化:并行获取所有独立的 RPC 数据(包括授权额度)
|
|
27
30
|
const [fetchedDecimals, balances, allowances, nonces, gasPrice] = await Promise.all([
|
|
28
31
|
inputDecimals !== undefined
|
|
@@ -32,7 +35,7 @@ export async function directV2BatchSell(params) {
|
|
|
32
35
|
.then((d) => Number(d))
|
|
33
36
|
.catch(() => 18),
|
|
34
37
|
Promise.all(wallets.map((w) => tokenContract.balanceOf(w.address))),
|
|
35
|
-
Promise.all(wallets.map((w) => tokenContract.allowance(w.address,
|
|
38
|
+
Promise.all(wallets.map((w) => tokenContract.allowance(w.address, router).catch(() => 0n))),
|
|
36
39
|
startNonces && startNonces.length === wallets.length
|
|
37
40
|
? Promise.resolve(startNonces)
|
|
38
41
|
: new NonceManager(provider).getNextNoncesForWallets(wallets),
|
|
@@ -69,9 +72,9 @@ export async function directV2BatchSell(params) {
|
|
|
69
72
|
if (sellAmountsWei[i] <= 0n)
|
|
70
73
|
continue;
|
|
71
74
|
if (allowances[i] < sellAmountsWei[i]) {
|
|
72
|
-
const approveData = tokenContract.interface.encodeFunctionData('approve', [
|
|
75
|
+
const approveData = tokenContract.interface.encodeFunctionData('approve', [router, ethers.MaxUint256]);
|
|
73
76
|
const approveTx = await wallets[i].signTransaction({
|
|
74
|
-
to:
|
|
77
|
+
to: tokenIn,
|
|
75
78
|
data: approveData,
|
|
76
79
|
value: 0n,
|
|
77
80
|
nonce: nonces[i],
|
|
@@ -89,7 +92,7 @@ export async function directV2BatchSell(params) {
|
|
|
89
92
|
try {
|
|
90
93
|
if (useNativeOutput) {
|
|
91
94
|
// 卖出代币 → 得到 BNB:先获取 V2 报价,再计算利润(✅ 使用当前 Router 报价,IROSwap 池需用 IROSwap Router)
|
|
92
|
-
const estimatedBNBOut = await getTokenToNativeQuote(provider,
|
|
95
|
+
const estimatedBNBOut = await getTokenToNativeQuote(provider, tokenIn, totalSellAmount, chain, 'v2', undefined, router);
|
|
93
96
|
if (estimatedBNBOut <= 0n)
|
|
94
97
|
return 0n;
|
|
95
98
|
const profit = calculateProfitAmount(estimatedBNBOut);
|
|
@@ -100,10 +103,10 @@ export async function directV2BatchSell(params) {
|
|
|
100
103
|
// IROSwap 等 DEX 可能只有 token/USDT 池,无 USDT/WBNB,直接 token→WBNB 会失败
|
|
101
104
|
// 1) 用当前 Router 报价 token→quoteToken(如 VOYA→USDT)
|
|
102
105
|
// 2) 用默认 PancakeSwap 报价 quoteToken→WBNB(USDT/WBNB 在 Pancake 上存在)
|
|
103
|
-
const step1 = await quoteV2(provider,
|
|
106
|
+
const step1 = await quoteV2(provider, tokenIn, outputToken, totalSellAmount, chain, router);
|
|
104
107
|
if (!step1.amountOut || step1.amountOut <= 0n)
|
|
105
108
|
return 0n;
|
|
106
|
-
const estimatedBNBOut = await getTokenToNativeQuote(provider,
|
|
109
|
+
const estimatedBNBOut = await getTokenToNativeQuote(provider, outputToken, step1.amountOut, chain, 'v2');
|
|
107
110
|
if (estimatedBNBOut <= 0n)
|
|
108
111
|
return 0n;
|
|
109
112
|
const profit = calculateProfitAmount(estimatedBNBOut);
|
|
@@ -116,11 +119,9 @@ export async function directV2BatchSell(params) {
|
|
|
116
119
|
return 0n;
|
|
117
120
|
}
|
|
118
121
|
})();
|
|
119
|
-
|
|
120
|
-
const
|
|
121
|
-
const
|
|
122
|
-
const useSwapRouter02 = isSwapRouter02(chain, routerAddress);
|
|
123
|
-
const useDYORSwap = isDYORSwap(chain, routerAddress);
|
|
122
|
+
const path = await resolveQuotedV2Path(provider, tokenIn, outputToken, totalSellAmount, chain, router);
|
|
123
|
+
const useSwapRouter02 = isSwapRouter02(chain, router);
|
|
124
|
+
const useDYORSwap = isDYORSwap(chain, router);
|
|
124
125
|
const routerIface = useSwapRouter02
|
|
125
126
|
? new ethers.Interface(SWAP_ROUTER02_V2_ABI)
|
|
126
127
|
: new ethers.Interface(V2_ROUTER_ABI);
|
|
@@ -186,7 +187,7 @@ export async function directV2BatchSell(params) {
|
|
|
186
187
|
return;
|
|
187
188
|
signPromises.push(wallet
|
|
188
189
|
.signTransaction({
|
|
189
|
-
to:
|
|
190
|
+
to: router,
|
|
190
191
|
data: buildSellTxData(wallet, sellAmountsWei[i]),
|
|
191
192
|
value: 0n,
|
|
192
193
|
nonce: nonces[i] + nonceOffsets[i],
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { ethers, Contract, Wallet, Interface, } from 'ethers';
|
|
2
|
+
import { createProvider } from '../../../utils/provider-factory.js';
|
|
2
3
|
import { ADDRESSES, applySlippageBps, } from '../../../core/constants/index.js';
|
|
3
4
|
import { MULTICALL3_ABI, V2_ROUTER_QUOTE_ABI, } from '../../../core/shared-abis/common.js';
|
|
4
5
|
import { getGasLimit, } from '../../../core/gas/bundle-gas.js';
|
|
@@ -81,7 +82,7 @@ export function createChainContext(chain, rpcUrl) {
|
|
|
81
82
|
return { chainId, provider: cached.provider };
|
|
82
83
|
}
|
|
83
84
|
// ✅ 创建新 Provider 并缓存
|
|
84
|
-
const provider =
|
|
85
|
+
const provider = createProvider(rpcUrl, chainId, chain);
|
|
85
86
|
providerCache.set(cacheKey, { provider, expireAt: now + PROVIDER_CACHE_TTL_MS });
|
|
86
87
|
return { chainId, provider };
|
|
87
88
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { createProvider } from '../../../../utils/provider-factory.js';
|
|
2
2
|
import { getTokenDecimals as fetchTokenDecimals } from '../../../../utils/swap-helpers.js';
|
|
3
3
|
import { ADDRESSES, ZERO_ADDRESS } from '../../../../core/constants/index.js';
|
|
4
4
|
import { CHAIN_ID_MAP } from '../config.js';
|
|
@@ -15,7 +15,7 @@ export function getCachedProvider(chain, rpcUrl, chainId) {
|
|
|
15
15
|
return cached.provider;
|
|
16
16
|
}
|
|
17
17
|
const resolvedChainId = chainId ?? (CHAIN_ID_MAP[chain] || 56);
|
|
18
|
-
const provider =
|
|
18
|
+
const provider = createProvider(rpcUrl, resolvedChainId, chain);
|
|
19
19
|
providerCache.set(cacheKey, { provider, expireAt: now + PROVIDER_CACHE_TTL_MS });
|
|
20
20
|
return provider;
|
|
21
21
|
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
|
-
import { ethers
|
|
1
|
+
import { ethers } from 'ethers';
|
|
2
2
|
import { MerkleClient } from '../../../../core/clients/merkle.js';
|
|
3
|
+
import { createProvider } from '../../../../utils/provider-factory.js';
|
|
3
4
|
import { ZERO_ADDRESS, CHAINS, applySlippageBps } from '../../../../core/constants/index.js';
|
|
4
5
|
import { CHAIN_ID_MAP, getTxType, calculateProfit, } from '../config.js';
|
|
5
6
|
const DEFAULT_GAS_LIMIT = 800000;
|
|
@@ -15,7 +16,7 @@ export function createMerkleContext(chain, config) {
|
|
|
15
16
|
return {
|
|
16
17
|
chainId,
|
|
17
18
|
// ✅ 禁用 ENS: 避免 "network does not support ENS" 错误
|
|
18
|
-
provider:
|
|
19
|
+
provider: createProvider(config.customRpcUrl, chainId, chain.toLowerCase()),
|
|
19
20
|
};
|
|
20
21
|
}
|
|
21
22
|
const merkle = new MerkleClient({
|
|
@@ -1,43 +1,28 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Provider
|
|
2
|
+
* Provider 工厂 — 真正禁用 ENS
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
4
|
+
* ethers v6 的 `{ ensAddress: undefined }` 不会去掉 EnsPlugin。
|
|
5
|
+
* 自定义 Network(例如 name: "BSC")没有 ENS 时,任何非 0x 地址
|
|
6
|
+
* (如计价符号 "SPCXB")都会在 getEnsAddress 抛
|
|
7
|
+
* "network does not support ENS"。
|
|
8
8
|
*/
|
|
9
9
|
import { JsonRpcProvider } from 'ethers';
|
|
10
|
+
export declare class NoEnsJsonRpcProvider extends JsonRpcProvider {
|
|
11
|
+
resolveName(name: string): Promise<null | string>;
|
|
12
|
+
}
|
|
10
13
|
/**
|
|
11
|
-
* 创建 JsonRpcProvider
|
|
12
|
-
*
|
|
13
|
-
* @param rpcUrl RPC URL
|
|
14
|
-
* @param chainId 链 ID (可选)
|
|
15
|
-
* @param chainName 链名称 (可选,用于日志)
|
|
16
|
-
* @param options 额外选项
|
|
17
|
-
* @returns JsonRpcProvider 实例
|
|
14
|
+
* 创建 JsonRpcProvider(拒绝把符号/乱码当 ENS 解析)
|
|
18
15
|
*/
|
|
19
16
|
export declare function createProvider(rpcUrl: string, chainId?: number, chainName?: string, options?: {
|
|
20
17
|
batchMaxCount?: number;
|
|
21
18
|
batchStallTime?: number;
|
|
22
19
|
}): JsonRpcProvider;
|
|
23
20
|
export declare function createCachedProvider(rpcUrl: string, chainId: number, chainName?: string): JsonRpcProvider;
|
|
24
|
-
/**
|
|
25
|
-
* 清除 Provider 缓存
|
|
26
|
-
*/
|
|
27
21
|
export declare function clearProviderCache(): void;
|
|
28
22
|
/**
|
|
29
23
|
* ENI 专用 Provider(staticNetwork,适合批量 RPC)
|
|
30
24
|
*/
|
|
31
25
|
export declare function createEniProvider(rpcUrl: string, chainId?: number): JsonRpcProvider;
|
|
32
|
-
/**
|
|
33
|
-
* XLayer 专用 Provider 创建函数
|
|
34
|
-
*/
|
|
35
26
|
export declare function createXLayerProvider(rpcUrl: string): JsonRpcProvider;
|
|
36
|
-
/**
|
|
37
|
-
* BSC 专用 Provider 创建函数
|
|
38
|
-
*/
|
|
39
27
|
export declare function createBscProvider(rpcUrl: string): JsonRpcProvider;
|
|
40
|
-
/**
|
|
41
|
-
* Monad 专用 Provider 创建函数
|
|
42
|
-
*/
|
|
43
28
|
export declare function createMonadProvider(rpcUrl: string): JsonRpcProvider;
|
|
@@ -1,59 +1,46 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Provider
|
|
2
|
+
* Provider 工厂 — 真正禁用 ENS
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
4
|
+
* ethers v6 的 `{ ensAddress: undefined }` 不会去掉 EnsPlugin。
|
|
5
|
+
* 自定义 Network(例如 name: "BSC")没有 ENS 时,任何非 0x 地址
|
|
6
|
+
* (如计价符号 "SPCXB")都会在 getEnsAddress 抛
|
|
7
|
+
* "network does not support ENS"。
|
|
8
8
|
*/
|
|
9
|
-
import { JsonRpcProvider } from 'ethers';
|
|
9
|
+
import { JsonRpcProvider, Network, getAddress, isAddress } from 'ethers';
|
|
10
|
+
export class NoEnsJsonRpcProvider extends JsonRpcProvider {
|
|
11
|
+
async resolveName(name) {
|
|
12
|
+
const trimmed = String(name ?? '').trim();
|
|
13
|
+
if (isAddress(trimmed))
|
|
14
|
+
return getAddress(trimmed);
|
|
15
|
+
throw new Error(`无效的合约地址: ${trimmed || '(空)'}(当前链不支持 ENS 域名解析)`);
|
|
16
|
+
}
|
|
17
|
+
}
|
|
10
18
|
/**
|
|
11
|
-
* 创建 JsonRpcProvider
|
|
12
|
-
*
|
|
13
|
-
* @param rpcUrl RPC URL
|
|
14
|
-
* @param chainId 链 ID (可选)
|
|
15
|
-
* @param chainName 链名称 (可选,用于日志)
|
|
16
|
-
* @param options 额外选项
|
|
17
|
-
* @returns JsonRpcProvider 实例
|
|
19
|
+
* 创建 JsonRpcProvider(拒绝把符号/乱码当 ENS 解析)
|
|
18
20
|
*/
|
|
19
21
|
export function createProvider(rpcUrl, chainId, chainName, options) {
|
|
20
|
-
|
|
21
|
-
const
|
|
22
|
-
? {
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
}
|
|
34
|
-
: undefined;
|
|
35
|
-
// 创建 Provider
|
|
36
|
-
if (networkConfig && batchOptions) {
|
|
37
|
-
return new JsonRpcProvider(rpcUrl, networkConfig, batchOptions);
|
|
38
|
-
}
|
|
39
|
-
else if (networkConfig) {
|
|
40
|
-
return new JsonRpcProvider(rpcUrl, networkConfig);
|
|
22
|
+
const network = chainId != null ? new Network(chainName || `chain-${chainId}`, chainId) : undefined;
|
|
23
|
+
const providerOptions = {
|
|
24
|
+
...(network ? { staticNetwork: network } : {}),
|
|
25
|
+
...(options
|
|
26
|
+
? {
|
|
27
|
+
batchMaxCount: options.batchMaxCount ?? 20,
|
|
28
|
+
batchStallTime: options.batchStallTime ?? 30,
|
|
29
|
+
}
|
|
30
|
+
: {}),
|
|
31
|
+
};
|
|
32
|
+
const hasOptions = Boolean(network) || Boolean(options);
|
|
33
|
+
if (network && hasOptions) {
|
|
34
|
+
return new NoEnsJsonRpcProvider(rpcUrl, network, providerOptions);
|
|
41
35
|
}
|
|
42
|
-
|
|
43
|
-
return new
|
|
36
|
+
if (network) {
|
|
37
|
+
return new NoEnsJsonRpcProvider(rpcUrl, network);
|
|
44
38
|
}
|
|
45
|
-
|
|
46
|
-
return new
|
|
39
|
+
if (hasOptions) {
|
|
40
|
+
return new NoEnsJsonRpcProvider(rpcUrl, undefined, providerOptions);
|
|
47
41
|
}
|
|
42
|
+
return new NoEnsJsonRpcProvider(rpcUrl);
|
|
48
43
|
}
|
|
49
|
-
/**
|
|
50
|
-
* 创建带缓存的 Provider
|
|
51
|
-
*
|
|
52
|
-
* @param rpcUrl RPC URL
|
|
53
|
-
* @param chainId 链 ID
|
|
54
|
-
* @param chainName 链名称
|
|
55
|
-
* @returns JsonRpcProvider 实例
|
|
56
|
-
*/
|
|
57
44
|
const providerCache = new Map();
|
|
58
45
|
export function createCachedProvider(rpcUrl, chainId, chainName) {
|
|
59
46
|
const key = `${rpcUrl}:${chainId}`;
|
|
@@ -66,9 +53,6 @@ export function createCachedProvider(rpcUrl, chainId, chainName) {
|
|
|
66
53
|
}
|
|
67
54
|
return providerCache.get(key);
|
|
68
55
|
}
|
|
69
|
-
/**
|
|
70
|
-
* 清除 Provider 缓存
|
|
71
|
-
*/
|
|
72
56
|
export function clearProviderCache() {
|
|
73
57
|
providerCache.clear();
|
|
74
58
|
}
|
|
@@ -76,33 +60,23 @@ export function clearProviderCache() {
|
|
|
76
60
|
* ENI 专用 Provider(staticNetwork,适合批量 RPC)
|
|
77
61
|
*/
|
|
78
62
|
export function createEniProvider(rpcUrl, chainId = 173) {
|
|
79
|
-
return
|
|
80
|
-
staticNetwork: true,
|
|
63
|
+
return createProvider(rpcUrl, chainId, 'eni', {
|
|
81
64
|
batchMaxCount: 20,
|
|
82
65
|
batchStallTime: 30,
|
|
83
66
|
});
|
|
84
67
|
}
|
|
85
|
-
/**
|
|
86
|
-
* XLayer 专用 Provider 创建函数
|
|
87
|
-
*/
|
|
88
68
|
export function createXLayerProvider(rpcUrl) {
|
|
89
69
|
return createProvider(rpcUrl, 196, 'xlayer', {
|
|
90
70
|
batchMaxCount: 20,
|
|
91
71
|
batchStallTime: 30,
|
|
92
72
|
});
|
|
93
73
|
}
|
|
94
|
-
/**
|
|
95
|
-
* BSC 专用 Provider 创建函数
|
|
96
|
-
*/
|
|
97
74
|
export function createBscProvider(rpcUrl) {
|
|
98
75
|
return createProvider(rpcUrl, 56, 'bsc', {
|
|
99
76
|
batchMaxCount: 20,
|
|
100
77
|
batchStallTime: 30,
|
|
101
78
|
});
|
|
102
79
|
}
|
|
103
|
-
/**
|
|
104
|
-
* Monad 专用 Provider 创建函数
|
|
105
|
-
*/
|
|
106
80
|
export function createMonadProvider(rpcUrl) {
|
|
107
81
|
return createProvider(rpcUrl, 143, 'monad', {
|
|
108
82
|
batchMaxCount: 20,
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
import { Network } from 'ethers';
|
|
3
|
+
import { NoEnsJsonRpcProvider } from './provider-factory.js';
|
|
4
|
+
describe('NoEnsJsonRpcProvider', () => {
|
|
5
|
+
const provider = new NoEnsJsonRpcProvider('http://127.0.0.1:1', new Network('BSC', 56), {
|
|
6
|
+
staticNetwork: true,
|
|
7
|
+
});
|
|
8
|
+
it('resolves real EVM addresses', async () => {
|
|
9
|
+
await expect(provider.resolveName('0xbe9D156892E55e7154BcD3cB0FEA677F9D3103E1')).resolves.toBe('0xbe9D156892E55e7154BcD3cB0FEA677F9D3103E1');
|
|
10
|
+
});
|
|
11
|
+
it('rejects ticker symbols instead of ENS', async () => {
|
|
12
|
+
await expect(provider.resolveName('SPCXB')).rejects.toThrow(/无效的合约地址: SPCXB/);
|
|
13
|
+
});
|
|
14
|
+
});
|