four-flap-meme-sdk 4.3.6 → 4.3.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,10 +1,11 @@
1
1
  /**
2
2
  * XLayer Portal 操作 — 查询器(query)
3
3
  */
4
- import { Interface, Contract, JsonRpcProvider } from 'ethers';
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
- // ✅ 禁用 ENS: XLayer 不支持 ENS,显式设置 ensAddress: undefined 避免解析错误
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 { JsonRpcProvider, Transaction, ethers } from 'ethers';
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 = new JsonRpcProvider(rpcUrl, {
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;
@@ -24,6 +24,30 @@ describe('FourClient private API rate-limit fallback', () => {
24
24
  expect(nonce).toBe('nonce-1');
25
25
  expect(calls.some((url) => url.includes('four.meme'))).toBe(true);
26
26
  });
27
+ it('retries generateNonce on bscfourapi after custom baseUrl -1013', async () => {
28
+ const calls = [];
29
+ vi.stubGlobal('fetch', vi.fn(async (input) => {
30
+ const url = String(input);
31
+ calls.push(url);
32
+ if (url.includes('example.com')) {
33
+ return new Response(JSON.stringify({ code: -1013, msg: 'Too many requests, banned for 15 minutes, remain 493s' }), { status: 429 });
34
+ }
35
+ if (url.includes('bscfourapi.emit.tools')) {
36
+ return new Response(JSON.stringify({ code: 0, data: 'nonce-proxy' }));
37
+ }
38
+ return new Response(JSON.stringify({ code: -1013, msg: 'Too many requests, banned for 15 minutes' }), {
39
+ status: 429,
40
+ });
41
+ }));
42
+ const nonce = await new FourClient({ baseUrl: 'https://example.com/four-meme-api' }).generateNonce({
43
+ accountAddress: `0x${'11'.repeat(20)}`,
44
+ verifyType: 'LOGIN',
45
+ networkCode: 'BSC',
46
+ });
47
+ expect(nonce).toBe('nonce-proxy');
48
+ expect(calls.some((url) => url.includes('example.com'))).toBe(true);
49
+ expect(calls.some((url) => url.includes('bscfourapi.emit.tools'))).toBe(true);
50
+ });
27
51
  it('retries generateNonce when the proxy returns HTML', async () => {
28
52
  vi.stubGlobal('fetch', vi.fn(async (input) => {
29
53
  const url = String(input);
@@ -109,6 +109,8 @@ async function hashBlob(blob) {
109
109
  }
110
110
  /** Four.meme 限流:-1038 每日上传配额,-1013 15 分钟封禁 */
111
111
  const RATE_LIMIT_CODES = new Set([-1038, -1013]);
112
+ /** Cloudflare Worker 代理(独立出口 IP,主线路被封时换这条) */
113
+ const FOUR_MEME_PROXY_API = 'https://bscfourapi.emit.tools';
112
114
  /**
113
115
  * Four.meme 官方 API。nonce/login 浏览器可直连;create/upload 需 `meme-web-access`,
114
116
  * 官方 CORS 不放行该头,浏览器应走自有代理。
@@ -119,17 +121,11 @@ export class FourClient {
119
121
  uploadUrls;
120
122
  constructor(cfg) {
121
123
  // 默认使用 Cloudflare Workers 代理,已配置 CORS,浏览器可直接使用
122
- this.baseUrl = cfg?.baseUrl || 'https://bscfourapi.emit.tools';
123
- // 上传端点列表:主 URL + 用户自定义备用 URL + 官方 API(兜底)
124
- const fallbacks = cfg?.uploadFallbackUrls || [];
124
+ this.baseUrl = cfg?.baseUrl || FOUR_MEME_PROXY_API;
125
+ // 主 URL → 自定义备用 → emit 代理(换 IP)→ 官方直连(Node 无 CORS)
125
126
  this.uploadUrls = [
126
- this.baseUrl,
127
- ...fallbacks,
128
- // ✅ 官方 API 始终作为最后兜底(Node.js 环境无 CORS 限制,走用户自己 IP)
129
- FOUR_MEME_OFFICIAL_API,
127
+ ...new Set([this.baseUrl, ...(cfg?.uploadFallbackUrls || []), FOUR_MEME_PROXY_API, FOUR_MEME_OFFICIAL_API]),
130
128
  ];
131
- // 去重(如果用户 baseUrl 就是官方地址,避免重复请求)
132
- this.uploadUrls = [...new Set(this.uploadUrls)];
133
129
  }
134
130
  attachApiCode(err, code) {
135
131
  const n = Number(code);
@@ -287,7 +283,7 @@ export class FourClient {
287
283
  });
288
284
  }
289
285
  async getPublicConfig() {
290
- const urls = [...new Set([this.baseUrl, FOUR_MEME_OFFICIAL_API])];
286
+ const urls = this.uploadUrls;
291
287
  let last;
292
288
  for (const url of urls) {
293
289
  try {
@@ -344,7 +340,7 @@ export class FourClient {
344
340
  return (j.data ?? j);
345
341
  }
346
342
  async searchTokenTemplates() {
347
- const urls = [...new Set([this.baseUrl, FOUR_MEME_OFFICIAL_API])];
343
+ const urls = this.uploadUrls;
348
344
  let last = [];
349
345
  for (const url of urls) {
350
346
  try {
@@ -374,7 +370,7 @@ export class FourClient {
374
370
  return list.filter((t) => String(t.status || '').toUpperCase() === 'PUBLISHED');
375
371
  }
376
372
  async getTokenTemplateConfig(templateId) {
377
- const urls = [...new Set([this.baseUrl, FOUR_MEME_OFFICIAL_API])];
373
+ const urls = this.uploadUrls;
378
374
  let last = [];
379
375
  for (const url of urls) {
380
376
  try {
@@ -20,9 +20,10 @@
20
20
  * });
21
21
  * ```
22
22
  */
23
- import { JsonRpcProvider, Transaction, formatEther } from 'ethers';
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 = new JsonRpcProvider(rpcUrl, {
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,7 +3,7 @@
3
3
  *
4
4
  * 只读查询授权额度、批量查询(Multicall3)、智能路由检查等。
5
5
  */
6
- import { JsonRpcProvider } from 'ethers';
6
+ import { type JsonRpcProvider } from 'ethers';
7
7
  import { type SellApprovalChain } from './helpers.js';
8
8
  /**
9
9
  * 检查代币授权状态 - Four.meme V1(只读,不发送交易)
@@ -3,13 +3,14 @@
3
3
  *
4
4
  * 只读查询授权额度、批量查询(Multicall3)、智能路由检查等。
5
5
  */
6
- import { Contract, JsonRpcProvider } from 'ethers';
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 = new JsonRpcProvider(rpcUrl);
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 = new JsonRpcProvider(rpcUrl);
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 = new JsonRpcProvider(rpcUrl);
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 = new JsonRpcProvider(rpcUrl);
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 tokenInLower = tokenIn.toLowerCase();
38
- const tokenOutLower = tokenOut.toLowerCase();
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
- const quotePromises = paths.map(async (path) => {
52
- try {
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
- if (best) {
72
- return { amountOut: best.amountOut, path: best.path };
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 { ethers, Wallet } from 'ethers';
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 new ethers.JsonRpcProvider(rpcUrl, {
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
- // ✅ 方案 C:使用缓存的 Provider
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 && quoteToken ? new Contract(quoteToken, ERC20_ABI, provider) : null;
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 && quoteToken
36
- ? getTokenToNativeQuote(provider, quoteToken, baseProfitWei, chain, 'v2')
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, routerAddress).catch(() => 0n)))
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: quoteToken,
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, routerAddress);
106
- const useDYORSwap = isDYORSwap(chain, routerAddress);
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: routerAddress,
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
- // ✅ 方案 C:使用缓存的 Provider
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(tokenAddress, ERC20_ABI, provider);
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, routerAddress).catch(() => 0n))),
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', [routerAddress, ethers.MaxUint256]);
75
+ const approveData = tokenContract.interface.encodeFunctionData('approve', [router, ethers.MaxUint256]);
73
76
  const approveTx = await wallets[i].signTransaction({
74
- to: tokenAddress,
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, tokenAddress, totalSellAmount, chain, 'v2', undefined, routerAddress);
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, tokenAddress, quoteToken, totalSellAmount, chain, routerAddress);
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, quoteToken, step1.amountOut, chain, 'v2');
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
- // 构建路径和 Router
120
- const outputToken = useNativeOutput ? wrappedNative : quoteToken;
121
- const path = [tokenAddress, outputToken];
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: routerAddress,
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 = new ethers.JsonRpcProvider(rpcUrl, { chainId, name: chain });
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 { JsonRpcProvider } from 'ethers';
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 = new JsonRpcProvider(rpcUrl, { chainId: resolvedChainId, name: chain });
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, JsonRpcProvider } from '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: new JsonRpcProvider(config.customRpcUrl, { chainId, name: chain.toLowerCase(), ensAddress: undefined }),
19
+ provider: createProvider(config.customRpcUrl, chainId, chain.toLowerCase()),
19
20
  };
20
21
  }
21
22
  const merkle = new MerkleClient({
@@ -63,4 +63,13 @@ describe('flapV8TaxBps', () => {
63
63
  sellTaxRate: 500n,
64
64
  }))).toBe(500);
65
65
  });
66
+ it('keeps graduated flap 1 percent tax', () => {
67
+ expect(flapV8TaxBps(v8({
68
+ status: TokenStatus.DEX,
69
+ pool: '0x17Ae5fb34fE0198Ecac25f210B25bA67A2d6a746',
70
+ quoteTokenAddress: '0xbe9D156892E55e7154BcD3cB0FEA677F9D3103E1',
71
+ buyTaxRate: 100n,
72
+ sellTaxRate: 100n,
73
+ }))).toBe(100);
74
+ });
66
75
  });
@@ -342,13 +342,15 @@ export async function inspectTokenLP(token, opts) {
342
342
  if (opts.flapChain === 'BSC') {
343
343
  const stV8 = await flap.getTokenV8Safe(token);
344
344
  taxRate = flapV8TaxBps(stV8);
345
+ // V8 已把「无池残留 100」收成 0;毕业盘真实 1%(100 bps)要留下
346
+ if (taxRate > 0)
347
+ result.taxRate = taxRate;
345
348
  }
346
349
  else {
347
350
  const stV7 = await flap.getTokenV7(token);
348
351
  taxRate = Number(stV7.taxRate ?? 0n);
349
- }
350
- if (taxRate > 0 && taxRate !== 100) {
351
- result.taxRate = taxRate;
352
+ if (taxRate > 0 && taxRate !== 100)
353
+ result.taxRate = taxRate;
352
354
  }
353
355
  }
354
356
  catch {
@@ -1,43 +1,28 @@
1
1
  /**
2
- * Provider 工厂函数 - 统一创建 JsonRpcProvider 并禁用 ENS
2
+ * Provider 工厂 — 真正禁用 ENS
3
3
  *
4
- * 目的:
5
- * 1. 避免 "network does not support ENS" 错误
6
- * 2. 统一 Provider 创建逻辑
7
- * 3. 提升性能 (跳过 ENS 解析)
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 (自动禁用 ENS)
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 工厂函数 - 统一创建 JsonRpcProvider 并禁用 ENS
2
+ * Provider 工厂 — 真正禁用 ENS
3
3
  *
4
- * 目的:
5
- * 1. 避免 "network does not support ENS" 错误
6
- * 2. 统一 Provider 创建逻辑
7
- * 3. 提升性能 (跳过 ENS 解析)
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 (自动禁用 ENS)
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 networkConfig = chainId
22
- ? {
23
- chainId,
24
- name: chainName || `chain-${chainId}`,
25
- ensAddress: undefined, // ✅ 禁用 ENS 解析
26
- }
27
- : undefined;
28
- // 构建批处理选项
29
- const batchOptions = options
30
- ? {
31
- batchMaxCount: options.batchMaxCount ?? 20,
32
- batchStallTime: options.batchStallTime ?? 30,
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
- else if (batchOptions) {
43
- return new JsonRpcProvider(rpcUrl, undefined, batchOptions);
36
+ if (network) {
37
+ return new NoEnsJsonRpcProvider(rpcUrl, network);
44
38
  }
45
- else {
46
- return new JsonRpcProvider(rpcUrl);
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 new JsonRpcProvider(rpcUrl, chainId, {
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
+ });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "four-flap-meme-sdk",
3
- "version": "4.3.6",
3
+ "version": "4.3.8",
4
4
  "description": "SDK for Flap bonding curve and four.meme TokenManager",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",