你是一个区块链量化资深开发者, 以下是一个solana+jupiter 的多线程 多代理ip 套利程...

Creato il: 21 giugno 2025

Risposto usando O3 da Chat01

Domanda

你是一个区块链量化资深开发者,
以下是一个solana+jupiter 的多线程 多代理ip 套利程序,
请分析 系统运行流程,确认代码有无错误

代码:
/*****************************************************************

  • index.js —— 覆盖后 node index.js 即可运行
    *****************************************************************/
    const { v4: uuidv4 } = require('uuid'); // 文件顶部,npm i uuid
    const fetch = require('node-fetch');
    const {
    PublicKey, SystemProgram, TransactionMessage, ComputeBudgetProgram,
    VersionedTransaction, AddressLookupTableAccount, LAMPORTS_PER_SOL,
    Connection,
    } = require('@solana/web3.js');
    const bs58 = require('bs58');
    const { HttpsProxyAgent } = require('https-proxy-agent');
    const { TOKENS1000 } = require('./constants');
    const ProxyPool = require('./proxyPool');
    const { WALLET_PRIVATE_KEY } = require('./config');
    const { deserializeInstruction } = require('./transactionUtils');
    const fs = require('fs');

process.env.NODE_TLS_REJECT_UNAUTHORIZED = '0';
const keypair = (() => {
const buf = bs58.decode(WALLET_PRIVATE_KEY);
return require('@solana/web3.js').Keypair.fromSecretKey(buf);
})();

/* ---------------- 线程 & 代理池准备 ---------------- */
const THREADS = Number(process.env.THREADS) || 4;
const PROXY_STR = process.env.PROXY_POOL || '127.0.0.1:10000->10';
//const PROXY_STR = '127.0.0.1:10000->180';
const BASE_AMOUNT_SOL = 0.05;
const SLIPPAGE_BPS = 50;//第一次報價slippage
const SLIPPAGE_BPS0 = 1500;//第二次報價slippage

//500,000 lamports = 0.0005 SOL=0.075 usdt=0.5 rmb

const MIN_NET_PROFIT =20000;// 200000; //最小净利润阈值 0.2rmb

const maxAccounts=32;
/*
1200_000->SLIPPAGE_BPS:100;lamports
600_000->SLIPPAGE_BPS:50;lamports
*/
const MIN_PROFIT_FIXED =20_000;//
const COMPUTE_UNIT_FALLBACK = 300_000;//优先费
const CU_UPPER_LIMIT = 1_400_000;

function splitAvg(arr,n){const s=Math.ceil(arr.length/n);return Array.from({length:n},(,i)=>arr.slice(i*s,(i+1)*s));}
function parseProxy(str){const[hps,sz]=str.split('->');const[h,sp]=hps.split(':');return Array.from({length:Number(sz)},(
,i)=>http://${h}:${Number(sp)+i});}
const PROXY_CHUNKS = splitAvg(parseProxy(PROXY_STR), THREADS);
const TOKEN_CHUNKS = splitAvg(TOKENS1000, THREADS);

const SOLANA_RPC=['https://solana-mainnet.g.alchemy.com/v2/alqGV-y3cz0uau9dDpvohJmlf8k893tT',
'https://solana-mainnet.g.alchemy.com/v2/alcht_r9gUZZ9NEe7YO06VQ265LJqxsACIWl',
'https://solana-mainnet.g.alchemy.com/v2/aosSEDNOSBO0fsNcTISNYfj9irU59Gzz',
'https://solana-mainnet.g.alchemy.com/v2/asCWW_nE_5MottOGIJtjOIfVCm_dU6Bd',
'https://solana-mainnet.g.alchemy.com/v2/ac1w6ejg24NFG2VZmDWieAO_hW0aPw2m',
'https://solana-mainnet.core.chainstack.com/c1f70bbc35644bd95ae98c944984230a',
'https://solana-mainnet.g.alchemy.com/v2/zAXPbOHQv5uZBUD1NG_nLbzip0WKZ5bM',
'https://solana-mainnet.g.alchemy.com/v2/sgFGPAyqiBwvqiEkXHiiSFYkVaNbFOro',
'https://solana-mainnet.g.alchemy.com/v2/ZB4MhPz7qnwwAK8kF6bXBVw_qHomL6YU',
'https://solana-mainnet.g.alchemy.com/v2/BO3XZ1SKw5U2agTBN-5CW',
'https://solana-mainnet.g.alchemy.com/v2/cym50KbOd8JsNJSk2mMLlsCbJIkExt6w',
'https://solana-mainnet.core.chainstack.com/90b1a03e7d63d7dafdefe698039b7056',
'https://solana-mainnet.core.chainstack.com/8d701a8cf39221fedef455984ecd8b4f',
'https://solana-mainnet.core.chainstack.com/704d0c5ae2421c3ecac091544acb5089'
];

// 全局只做一次
let avgPriorFee = 5_000;
setInterval(async ()=>{
try{
const {microLamports} = await getAveragePriorityFee(); // RPC 拿 avg
avgPriorFee = Math.ceil(microLamports * 1.1); // +10% 安全系数
}catch{ /* 持续用旧值 / }
}, 5_000);
//
/
=============================================================

    1. 先把“全部代理”拍平成一个数组
  • =========================================================== */
    const ALL_PROXIES = PROXY_CHUNKS.flat(); // <-- 关键 ①

let latestBlockhashData = null;
setInterval(async () => {
try {
latestBlockhashData = await getLatestBlockhash1();
}
catch (e) {
console.warn('更新区块哈希失败,继续使用旧值', e);
} }, 500); // 500毫秒更新一次

// 假设 SOLANA_RPC = ['url1', 'url2', 'url3']
let rpcIdx = 0; // 全局下标

function nextRpc() {
const url = SOLANA_RPC[rpcIdx];
rpcIdx = (rpcIdx + 1) % SOLANA_RPC.length; // 末尾→0,形成循环
return url;
}
/* -------- getLatestBlockhash(按指定实现) -------- */
async function getLatestBlockhash1(){
var proxies=['http://127.0.0.1:1082'];//PROXY_CHUNKS[0];
//var proxies=PROXY_CHUNKS[1];
const proxyPool = new ProxyPool(proxies);
//const proxyPool = new ProxyPool(ALL_PROXIES);
const safeFetch = buildSafeFetch(proxyPool);

text
//const MAIN_RPC = 'https://api.mainnet-beta.solana.com'; //const MAIN_RPC=SOLANA_RPC[Math.floor(Math.random()*SOLANA_RPC.length)]; const MAIN_RPC = nextRpc();// 每调用一次就往后走一个 const res = await safeFetch(MAIN_RPC,{ method:'POST', headers:{'Content-Type':'application/json'}, body:JSON.stringify({jsonrpc:'2.0',id:1,method:'getLatestBlockhash', params:[{commitment:'confirmed'}]}), }); //start /* const responseText = await res.text();

console.log('响应内容:', responseText);
try {
const data = JSON.parse(responseText);
// 继续处理
} catch (e) {
console.error('解析JSON失败,内容:', responseText);
}
*/
//end

text
const data = await res.json(); //console.log('>>> getLatestBlockhash1:', JSON.stringify(data, null, 2)); if(data.error) throw new Error(data.error.message); if(data.result) return { blockhash:data.result.value.blockhash, lastValidBlockHeight:data.result.value.lastValidBlockHeight, }; throw new Error('No blockhash data in response');

}

async function ensureFreshBlockhash() {
// 当前块高
//const cur = await connection.getBlockHeight('finalized');
const cur = await getBlockHeight();
// 剩余 slot 小于 10,就重新拉
if (!latestBlockhashData ||
latestBlockhashData.lastValidBlockHeight - cur < 10) {
latestBlockhashData = await getLatestBlockhash1();
}
return latestBlockhashData;
}
async function getBlockHeight(){
var proxies=['http://127.0.0.1:1082'];//PROXY_CHUNKS[0];
//var proxies=PROXY_CHUNKS[1];
const proxyPool = new ProxyPool(proxies);
//const proxyPool = new ProxyPool(ALL_PROXIES);
const safeFetch = buildSafeFetch(proxyPool);

text
//const MAIN_RPC = 'https://api.mainnet-beta.solana.com'; //const MAIN_RPC=SOLANA_RPC[Math.floor(Math.random()*SOLANA_RPC.length)]; const MAIN_RPC = nextRpc();// 每调用一次就往后走一个 const res = await safeFetch(MAIN_RPC,{ method:'POST', headers:{'Content-Type':'application/json'}, body:JSON.stringify({jsonrpc:'2.0',id:1,method:'getBlockHeight', params:[{commitment:'finalized'}]}), }); //start /* const responseText = await res.text();

console.log('响应内容:', responseText);
try {
const data = JSON.parse(responseText);
// 继续处理
} catch (e) {
console.error('解析JSON失败,内容:', responseText);
}
*/
//end

text
const data = await res.json(); //console.log('>>> getLatestBlockhash1:', JSON.stringify(data, null, 2)); if(data.error) throw new Error(data.error.message); if(data.result) return data.result; throw new Error('No blockhash data in response');

}

/* ---------------- safeFetch 工厂 ---------------- */
function buildSafeFetch(pool){
return async function safeFetch(url,opt={},retry=pool.list.length){
let i=0;
while(i<retry){
const agent=pool.getAgent();
try{
const res=await fetch(url,{...opt,agent});
if(res.status>=500) throw new Error(HTTP ${res.status});
return res;
}catch(e){
//console.log(❌ 代理 ${pool.list[pool.idx]} 失败: ${e.message});
pool.next();i++;}}
//throw new Error(全部代理均失败: ${url});
throw new Error(``);

}
}
// (1) 在文件开头 (或至少在 createWorker / doWork 之前) 添加一个调试签名函数
function signWithDebug(tx, signers, contextInfo = {}) {
console.log('>>> [signWithDebug] 即将签名 transaction');
if (contextInfo) {
console.log('>>> [signWithDebug] 额外上下文:', JSON.stringify(contextInfo, null, 2));
}

// 可以先检查 transaction.message 里的 addressTableLookups / staticAccountKeys / instructions
try {
// 在这里可手动调试 transaction.serialize() 以打印每一步
// console.log('>>> [signWithDebug] serialize check: ', tx.serialize().toString('base64'));

tx.sign(signers);
console.log('>>> [signWithDebug] 签名成功');
} catch (error) {
console.error('!!! [signWithDebug] 签名报错:', error.message);
console.error('>>> error stack:', error.stack); // 打印堆栈

// 尝试查看 transaction 的关键信息
try {
console.error(
'>>> debug: transaction.message (JSON)',
JSON.stringify(tx.message, null, 2)
);
} catch (e2) {
console.error('>>> transaction.message JSON.stringify 再次异常:', e2.message);
}

// 也可以遍历一下 addressTableLookups
try {
const lookups = tx.message.addressTableLookups || [];
console.log('>>> addressTableLookups 条数:', lookups.length);
lookups.forEach((l, idx) => {
console.log(--- lookup[${idx}]:, l);
});
} catch (e3) {
console.error('>>> dump lookups 异常:', e3.message);
}

// 最后抛出原始错误,不要吞掉
throw error;
}
}

/* =================================================================
createWorker —— 每并发线程
================================================================= */
function createWorker(threadId, proxies, tokens){
const proxyPool = new ProxyPool(proxies);
const safeFetch = buildSafeFetch(proxyPool);

const MAIN_RPC = 'https://api.mainnet-beta.solana.com';
/* -------- Jito bundle工具(全部走 safeFetch) -------- /
const JITO_EP=[ 'https://mainnet.block-engine.jito.wtf/api/v1/bundles',
'https://amsterdam.mainnet.block-engine.jito.wtf/api/v1/bundles',
'https://frankfurt.mainnet.block-engine.jito.wtf/api/v1/bundles',
'https://ny.mainnet.block-engine.jito.wtf/api/v1/bundles',
'https://tokyo.mainnet.block-engine.jito.wtf/api/v1/bundles'];
const agentJito=new HttpsProxyAgent('http://127.0.0.1:1082'); // 如需改代理自行修改
/
=========================================================================

  • checkBundleStatus —— 轮转节点,直到拿到终止态
  • • 终止态:Landed / Dropped / Expired / Rejected
  • • 初始态 / 中间态:Invalid / Submitted / Processed …
  • ======================================================================= /
    /
    ================================================================
  • util:返回一个“轮转取节点”的生成器
  • ============================================================== /
    function
    roundRobin(arr) {
    let i = 0;
    while (true) {
    yield arr[i % arr.length];
    i += 1;
    }
    }

// 放在 createWorker 里,保持线程私有
const jitoUrlIter = roundRobin(JITO_EP);

/* -------- getLatestBlockhash(按指定实现) -------- */
async function getLatestBlockhash(){
const res = await safeFetch(MAIN_RPC,{
method:'POST',
headers:{'Content-Type':'application/json'},
body:JSON.stringify({jsonrpc:'2.0',id:1,method:'getLatestBlockhash',
params:[{commitment:'confirmed'}]}),
});
const data = await res.json();
if(data.error) throw new Error(data.error.message);
if(data.result) return {
blockhash:data.result.value.blockhash,
lastValidBlockHeight:data.result.value.lastValidBlockHeight,
};
throw new Error('No blockhash data in response');
}

/* -------- getAccountInfo1 & ALT 解析 -------- */
// 简单的数组 chunk 函数
function chunkArray(arr, size) {
const ret = [];
for (let i = 0; i < arr.length; i += size) {
ret.push(arr.slice(i, i + size));
}
return ret;
}

/**

  • 批量拉取 ALT table,并把原始 b64 一并返回
  • @param {string[]} keys - base58 字符串数组
  • @returns {Promise<{key: PublicKey, state: AddressLookupTableAccount, raw: string}[]>}
    */
    async function getAltAccounts(keys) {
    const uniqueKeys = Array.from(new Set(keys));
    const batches = chunkArray(uniqueKeys, 100);
    const altResults = [];

for (const batch of batches) {
const body = {
jsonrpc: '2.0',
id : 1,
method : 'getMultipleAccounts',
params : [
batch,
{ encoding: 'base64', commitment: 'confirmed' }
]
};
const res = await safeFetch(MAIN_RPC, {
method : 'POST',
headers: { 'Content-Type': 'application/json' },
body : JSON.stringify(body),
});
const json = await res.json();
if (json.error) throw new Error(json.error.message);

text
const accounts = json.result.value || []; for (let i = 0; i < batch.length; i++) { const info = accounts[i]; if (!info?.data) continue; // 取出 Base64 字符串 const b64 = Array.isArray(info.data) ? info.data[0] : info.data; const buf = Buffer.from(b64, 'base64'); try { // 反序列化出 wrapper const state = AddressLookupTableAccount.deserialize(buf); altResults.push({ key : new PublicKey(batch[i]), state, raw : b64, // <- 把原始 b64 带出来 }); } catch (e) { console.warn(`[getAltAccounts] 解析 ALT 失败: ${batch[i]}`, e.message); } }

}

return altResults;
}
/* -------- Jupiter 助手 -------- */
const JUP='https://quote-api.jup.ag/v6';
const jQuote=qs=>safeFetch(${JUP}/quote?${qs}).then(r=>r.json());
const jIx=body=>safeFetch(${JUP}/swap-instructions,{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify(body)}).then(r=>r.json());

/* -------- VersionedTx 构造工具 -------- /
function createVersionedTransaction(
instructions,
payer,
addressLookupTableAccounts,
recentBlockhash,
computeUnits,
microLamports = 0, // 新增参数
tipAmount
) {
const computeBudgetIx = ComputeBudgetProgram.setComputeUnitLimit({
units: computeUnits,
});
/
---------- CU Price(90 % 逻辑兜底) ---------- /
/

const microLamports = priorityFeeLamports !== null
? priorityFeeLamports
: Math.max(5_000, Math.floor(expectedProfit * 0.9));
*/

const priorityFeeIx = ComputeBudgetProgram.setComputeUnitPrice({
microLamports: microLamports,
});

//const tipAccountPubkey = new PublicKey("96gYZGLnJYVFmbjzopPSU6QiEV5fGqZNyN9nmNhvrZU5");
//使用多个tip账户以提高成功率
// Jito官方tip账户池
const tipAccounts = [
"96gYZGLnJYVFmbjzopPSU6QiEV5fGqZNyN9nmNhvrZU5",
"HFqU5x63VTqvQss8hp11i4wVV8bD44PvwucfZ2bU7gRe",
"Cw8CFyM9FkoMi7K7Crf6HNQqf4uEMzpKw6QNghXLvLkY",
"ADaUMid9yfUytqMBgopwjb2DTLSokTSzL1zt6iGPaS49",
"DfXygSm4jCyNCybVYYK6DwvWqjKee8pbDmJGcLWNDXjh",
"ADuUkR4vqLUMWXxW9gh6D6L8pMSawimctcNZ5pGwDcEt",
"DttWaMuVvTiduZRnguLF7jNxTgiMBZ1hyAumKUiL2KRL",
"3AVi9Tg9Uo68tJfuvoKvqKNWKkC5wPdSSdeBnizKZ6jT"
];

// 随机选择一个tip账户
const tipAccountPubkey = new PublicKey(
tipAccounts[Math.floor(Math.random() * tipAccounts.length)]
);

// 动态计算tip:利润的1-5%,但至少1000 lamports
//let tipAmount = Math.max(1000, Math.floor(expectedProfit * 0.02)); // 2%的利润作为tip

// 设置一个合理的上限,避免tip过高
//tipAmount = Math.min(tipAmount, 50000); // 最多0.05 SOL

text
const tipInstruction = SystemProgram.transfer({ fromPubkey: keypair.publicKey, toPubkey: tipAccountPubkey, lamports:tipAmount// 10000, });

const finalInstructions = [computeBudgetIx, priorityFeeIx, ...instructions, tipInstruction];

const messageV0 = new TransactionMessage({
payerKey: payer,
recentBlockhash: recentBlockhash,
instructions: finalInstructions,
}).compileToV0Message(addressLookupTableAccounts.filter(Boolean));

return new VersionedTransaction(messageV0);
}

async function jitoCall(method,params){
const url=JITO_EP[Math.floor(Math.random()*JITO_EP.length)];
const res=await safeFetch(url,{
method:'POST',agent:agentJito,
headers:{'Content-Type':'application/json'},
body:JSON.stringify({jsonrpc:'2.0',id:1,method,params})});
const json=await res.json(); if(json.error) throw new Error(json.error.message);
return json.result;
}
//const createJitoBundle = (tx)=>bs58.encode(tx.serialize());
//const sendJitoBundle = (bundle)=>jitoCall('sendBundle',[bundle]);
//const checkBundleStatus= (id)=>jitoCall('getInflightBundleStatuses',[[id]]);

/* -------- Priority Fee 取平均 -------- */

/* ============================================================

  • getAveragePriorityFee —— 纯 JSON-RPC 实现
  • ========================================================== */
    async function getAveragePriorityFee() {
    const rpcUrl = 'https://api.mainnet-beta.solana.com';

// 构造 JSON-RPC 请求体
const body = {
jsonrpc: '2.0',
id : 1,
method : 'getRecentPrioritizationFees',
params : [] // 官方文档:该方法无参数
};

// 通过线程私有 safeFetch 发送
const res = await safeFetch(rpcUrl, {
method : 'POST',
headers: { 'Content-Type': 'application/json' },
body : JSON.stringify(body),
});

const json = await res.json();

if (json.error) {
console.warn('[priorityFee] RPC 返回错误,使用默认 5000μ');
return { microLamports: 5_000 };
}

const fees = json.result || [];
if (!Array.isArray(fees) || fees.length === 0) {
console.warn('[priorityFee] 空结果,使用默认 5000μ');
return { microLamports: 5_000 };
}

// 取最近 150 个槽做平均
const recent = fees.slice(-150);
const avg = Math.ceil(
recent.reduce((sum, f) => sum + (f.prioritizationFee || 0), 0) /
recent.length
);

// 最低兜底 5000μ
const microLamports = Math.max(avg, 5_000);
return { microLamports };
}

/* ---------- Jito bundle 工具 ---------- */
async function createJitoBundle(versionedTx) {
//const bundle = bs58.encode(transaction.serialize());
//return bundle;
// 单笔交易就返回长度为 1 的数组;多笔时 push 即可
return [ bs58.encode(versionedTx.serialize()) ];
/*如果以后要一次发多笔,只需
const bundle = [
bs58.encode(tx1.serialize()),
bs58.encode(tx2.serialize()),
...
];

  */

}
function buildBundle(base58Tx) {
return {
encodedTransactions: [base58Tx],
bundleName : uuidv4(), // 方便排查
// tipTx : '<可选补充 tip 交易>'
};
}
/* -----------------------------------------------------------

  • sendJitoBundle —— 内建限流退避
  • • 默认最多 retry 5 次
  • • 收到 429 时:指数退避 (500ms * 2^n) + 随机 0-500ms
  • • 其他 HTTP≥400 直接抛错
  • --------------------------------------------------------- */
    async function sendJitoBundle(transactions, region = 'tokyo',maxRetry = 5) {
    // 1. 先序列化,再用 bs58 做 BASE-58 编码
    //const base58Tx = bs58.encode(transactions.serialize());
    //const base58Tx = bs58.encode(transactions.serialize());
    //const bundle = buildBundle(base58Tx);
    //const base64Tx = Buffer.from(transactions.serialize()).toString('base64');
    //const bundle = buildBundle(base64Tx);// buildBundle 内只做包裹

const base64Tx = Buffer.from(transactions.serialize()).toString('base64');
/*
const bundle = {
encodedTransactions : [base64Tx],
bundleName : uuidv4(),
blockhash : latestBlockhashData.blockhash,
lastValidBlockHeight : latestBlockhashData.lastValidBlockHeight
};
/
const bundle = {
bundle_id : uuidv4(), // 必须叫 bundle_id
encoded_transactions : [base64Tx], // snake_case
reference_blockhash : latestBlockhashData.blockhash,
last_valid_block_height: latestBlockhashData.lastValidBlockHeight,
tip : null // 如有打赏 tx 填 base64
};
for (let attempt = 0; attempt < maxRetry; attempt++) {
const url= jitoUrlIter.next().value;// 轮转节点
const agentJito = new HttpsProxyAgent(http://127.0.0.1:1082);
const resp = await safeFetch(url, {
method : 'POST',
agent : agentJito,
headers: { 'Content-Type': 'application/json' },
body : JSON.stringify({
jsonrpc: '2.0',
id : 1,
method : 'sendBundle',
params : [bundle]
//params : [bundle]// ← 这里是 object
//params: [transactions]
//params: [[base58Tx]]// 双层数组
/

params: [
transactions,
{
region: region // 将region包装在选项对象中
}
]
*/
}),
});

text
/* -------- 2xx 正常返回 -------- */ if (resp.ok) { const json = await resp.json(); return json.result; // bundleId } /* -------- 429 -> 退避重试 -------- */ if (resp.status === 429) { /* const base = 500 * 2 ** attempt; const jitter = Math.floor(Math.random() * 500); const wait = base + jitter; console.warn(`⚠️ 429 TooManyRequests,${wait}ms 后重试 (${attempt + 1}/${maxRetry})`); await new Promise(r => setTimeout(r, wait)); */ console.warn(`⚠️ 429 TooManyRequests @ ${url},切换节点后重试 (${attempt+1}/${maxRetry})`); // 不休眠,直接继续循环(已换节点) continue; } /* -------- 其它 4xx/5xx:打印具体错误并抛 -------- */ const errText = await resp.text(); // 读取返回体,看具体错误 throw new Error(`HTTP ${resp.status} → ${errText}`);

}
throw new Error('sendJitoBundle retry exhausted');
}
/**

  • 轮询查询 bundle 状态
  • 返回格式示例:
  • { bundle_id, status: 'Processed'|'Dropped'|'Invalid', landed_slot }
    /
    /
    *
  • 轮询查询 bundle 状态:
  • • Landed / Failed → 立即返回状态
  • • 其他状态 → 等待 500ms 后继续查询
  • @param {string} bundleId
  • @param {number} maxRetries 最多尝试次数
  • @returns {Promise<string>} 最终状态(Landed 或 Failed)
    */
    async function checkBundleStatus(bundleId, maxRetries = 60) {
    const requestBody = {
    jsonrpc: "2.0",
    id: 1,
    method: "getInflightBundleStatuses",
    params: [[bundleId]],
    };

let tries = 0;
while (tries < maxRetries) {
// 轮换 Jito 节点
const url = jitoUrlIter.next().value;
tries++;

text
try { const resp = await safeFetch(url, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(requestBody), }); // 解析 RPC 返回 const json = await resp.json(); const info = json.result?.value?.[0]; if (info && info.status) { const st = info.status; console.log(`[轮询#${tries}] bundle ${bundleId} 状态 = ${st}`); // 如果命中终态,就直接返回 if (st === "Landed" || st === "Failed") { return info; } } } catch (e) { console.warn(`[轮询#${tries}] 查询失败: ${e.message}`); // 可视情况 decide:这里也算一次尝试,或直接继续不计数 } // 500ms 后再次尝试 await new Promise(r => setTimeout(r, 500));

}

throw new Error(达到最大重试次数 ${maxRetries},仍未得到 Landed/Failed);
}
async function getIxSafeaa(quote,lookupKeys,slippageBps) {
try {
return await jIx(buildIxBody(quote, lookupKeys));
} catch (e) {
if (e.message?.includes('6001')) { // SlippageExceeded
console.warn('↪ 降级到 3% 滑点重试');
return await jIx({
...buildIxBody(quote, lookupKeys),
slippageBps: slippageBps * 2 // 3%
});
}
throw e;
}
}
async function getIxSafe12(quote, lookupKeys, slippageBps) {
try {
return await jIx(
buildIxBody(quote, prioMicroLamports, slippageBps, lookupKeys)
);
} catch (e) {
if (e.message?.includes('6001')) {
// 降级滑点重试
return await jIx({
...buildIxBody(quote, prioMicroLamports, slippageBps * 2, lookupKeys),
});
}
throw e;
}
}
// ==== 新版:把 prioMicroLamports 也作为参数传进来 ====
async function getIxSafe (
quote,
lookupKeys,
slippageBps,
prioMicroLamports // <—— 新增
) {
try {
// 第一次调用,按原始 slippage 生成指令
return await jIx(
buildIxBody(quote, prioMicroLamports, slippageBps, lookupKeys)
);
} catch (e) {
// Jupiter 自定义错误 6001 = SlippageExceeded
if (e.message?.includes('6001')) {
console.warn('↪ SlippageExceeded,自动 x2 重新请求');
return await jIx(
buildIxBody(quote, prioMicroLamports, slippageBps * 2, lookupKeys)
);
}
throw e; // 其它错误照旧抛
}
}
// ① 把滑点、ALT、maxAccounts 全塞进去
// -------------------------------------------------------------
// quote : Jupiter /quote 的原响应
// lookupKeys : 需要引用的 ALT pubkey[](≤32)
// slippageBps : 这条 quote 希望容忍的滑点
// -------------------------------------------------------------
/* ---------- 1. 先把 buildIxBody 写好(不需要 lookupKeys) ---------- */
// 修改签名,多加一个第四参:lookupKeys,默认空数组
function buildIxBody(
quote,
prioMicroLamports,
slippageBps,
lookupKeys = []
) {
return {
quoteResponse : quote,
userPublicKey : keypair.publicKey.toBase58(),
wrapAndUnwrapSol : true,
dynamicComputeUnitLimit : true,
prioritizationFeeLamports: prioMicroLamports,
slippageBps : slippageBps,
maxAccounts : maxAccounts,
// 新增:让 Jupiter 直接在指令里引用 ALT
addressLookupTableAddresses: lookupKeys,
};
}
function trimAlt(alt) {
if (alt.addresses.length < 256) return alt; // OK

// 生成一个“浅拷贝 + slice(0,255)”的新实例
return new AddressLookupTableAccount({
key : alt.key,
state : { ...alt.state, addresses: alt.addresses.slice(0, 255) }
});
}
function isMessageTooBig(message) {
// ① static keys 本身
if (message.staticAccountKeys.length >= 256) return true;

// ② 每条指令里的最大索引
return message.compiledInstructions.some(ix =>
ix.accountKeyIndexes.some(i => i >= 256)
);
}
/* ---------- helper:按 255 上限裁剪 ALT ---------- */
function trimAltSet(staticCnt, altStates) {
const out = [];
let total = staticCnt;

for (const tbl of altStates) {
const sz = tbl.addresses.length;
if (total + sz >= 256) break; // 再加就爆
out.push(tbl);
total += sz;
}
return out;
}

/* -------- 模拟交易 -------- /
/
------------------------------------------------------------------

  • simulateTransaction()
  • • 任何 RangeError/overruns 视为 “尺寸超限” 直接返回 null
  • • 调用方只要 if (!sim) return 即可跳过本轮
  • -----------------------------------------------------------------*/
    async function simulateTransaction(instr, payer, alts) {
    //const latest=await getLatestBlockhash();
    // 1. 用假 blockhash 编译一次,只为组装 message
    const msgV0 = new TransactionMessage({
    payerKey : payer,
    //recentBlockhash:latestBlockhashData.blockhash,// '11111111111111111111111111111111', // dummy
    recentBlockhash:'11111111111111111111111111111111', // dummy
    instructions : instr,
    }).compileToV0Message(alts.filter(Boolean));

// 2. 粗测 static+ALT 总量,>255 直接跳过
if (isMessageTooBig(msgV0)) return null;

// 3. serialize 可能仍触发 RangeError → 捕获并返回 null
let tx64;
try {
tx64 = Buffer
.from(new VersionedTransaction(msgV0).serialize())
.toString('base64');
} catch (e) {
if (e instanceof RangeError &&
e.message.includes('overruns Uint8Array')) {

text
dumpMessageDetail(msgV0, alts);// ← 新增 console.warn('[simulate] serialize 超限,跳过本轮'); return null; // 让 caller 直接 continue } throw e; // 其它错误照旧抛出

}

// 4. 真正 RPC simulate
const res = await safeFetch(MAIN_RPC, {
method : 'POST',
headers: { 'Content-Type':'application/json' },
body : JSON.stringify({
jsonrpc: '2.0',
id : 1,
method : 'simulateTransaction',
params : [
tx64,
{ encoding:'base64', sigVerify:false, replaceRecentBlockhash:true }
]
})
}).then(r => r.json());

return res;
}
/* ===========================================================

  • dumpMessageDetail —— 把静态 key、ALT、指令索引全部打印

  • ========================================================= */
    function dumpMessageDetail(msgV0, altStates = []) {
    try {
    const staticKeys = msgV0.staticAccountKeys.map(k => k.toBase58());

    // 汇总每张 ALT 的地址个数
    const altSummary = altStates.map((alt, idx) => ({
    idx,
    key : (alt.key || alt.addresses?.[0]).toBase58?.() ?? 'n/a',
    addrCnt : alt.addresses.length
    }));

    // 找出所有指令里最大的 accountKeyIndex
    const maxIdxEachIx = msgV0.compiledInstructions.map(ix =>
    Math.max(...ix.accountKeyIndexes)
    );

    console.warn('====== [Message Detail] ======');
    console.warn('• staticAccountKeys.len =', staticKeys.length);
    console.warn('• ALT 数量 =', altStates.length);
    console.warn(' ', JSON.stringify(altSummary, null, 2));
    console.warn('• 各指令最大索引 =', maxIdxEachIx);
    console.warn('==============================');
    } catch (dumpErr) {
    console.error('[dumpMessageDetail] 解析失败:', dumpErr.message);
    }
    }
    // 放在文件最上面,确保 fs 已经 require
    // const fs = require('fs');
    // const { AddressLookupTableAccount, PublicKey } = require('@solana/web3.js');
    async function loadAltAccountsWithCachebak(keys) {
    const CACHE_FILE = 'alt-cache.json';
    let altCache = [];

// 1. 读本地缓存
try {
altCache = JSON.parse(fs.readFileSync(CACHE_FILE, 'utf-8'));
} catch {
altCache = [];
}

// 2. 建 map
const cacheMap = new Map(altCache.map(item => [item.key, item.data]));
const uniqueKeys = Array.from(new Set(keys));
const hitKeys = uniqueKeys.filter(k => cacheMap.has(k));
const missKeys = uniqueKeys.filter(k => !cacheMap.has(k));

// 3. 命中部分,直接反序列化
/*
const cachedAlts = hitKeys.map(k => {
const buf = Buffer.from(cacheMap.get(k), 'base64');
const state = AddressLookupTableAccount.deserialize(buf);
return { key: new PublicKey(k), state };
});
/
// ------- 3. 命中缓存的部分:手动把 key 写进 state ----------
/

const cachedAlts = hitKeys.map(k => {
const buf = Buffer.from(cacheMap.get(k), 'base64');
const state = AddressLookupTableAccount.deserialize(buf);
state.key = new PublicKey(k);// ← 补写 key
return { key: new PublicKey(k), state };
});
*/
const cachedAlts = hitKeys.map(k => {
const raw = cacheMap.get(k);
if (raw === undefined) {
console.warn([loadAltAccountsWithCache] 缓存缺失 key=${k});
return null; // 或继续
}
const buf = Buffer.from(raw, 'base64');
const state = AddressLookupTableAccount.deserialize(buf);
state.key = new PublicKey(k);
return { key: new PublicKey(k), state };
}).filter(Boolean);

// 4. 未命中:批量 RPC 拉 + 更新缓存
let fetchedAlts = [];
if (missKeys.length) {
fetchedAlts = await getAltAccounts(missKeys);

text
// 不再用 state.serialize(),直接把 RPC 的原始 raw 写进缓存 for (const { key, raw } of fetchedAlts) { if (typeof raw === 'string' && raw.length) { altCache.push({ key: key.toBase58(), data: raw }); } } fs.writeFileSync(CACHE_FILE, JSON.stringify(altCache, null, 2));

}

// 5. 合并返回
return [
...cachedAlts,
// fetchedAlts 里已经是 {key,state,...}
...fetchedAlts.map(({key, state})=>({key, state}))
];
}
async function loadAltAccountsWithCache(keys) {
const CACHE_FILE = 'alt-cache.json';
let altCache = [];

// 1. 读本地缓存
try {
altCache = JSON.parse(fs.readFileSync(CACHE_FILE, 'utf-8'));
} catch {
altCache = [];
}

// 2. 建 map
const cacheMap = new Map(altCache.map(item => [item.key, item.data]));
const uniqueKeys = Array.from(new Set(keys));
const hitKeys = uniqueKeys.filter(k => cacheMap.has(k));
const missKeys = uniqueKeys.filter(k => !cacheMap.has(k));

// 3. 命中缓存 → 直接反序列化 + new AddressLookupTableAccount
const cachedAlts = hitKeys.map(k => {
const raw = cacheMap.get(k);
if (!raw) return null;

text
const buf = Buffer.from(raw, 'base64'); const state = AddressLookupTableAccount.deserialize(buf); return new AddressLookupTableAccount({ key : new PublicKey(k), state, });

}).filter(Boolean);

// 4. 未命中:RPC 拉取,写入缓存
let fetchedAlts = [];
if (missKeys.length) {
fetchedAlts = await getAltAccounts(missKeys); // 返回 { key, raw, state }

text
// 把 RPC 拿到的 raw 存盘 for (const { key, raw } of fetchedAlts) { if (typeof raw === 'string' && raw.length) { altCache.push({ key: key.toBase58(), data: raw }); } } fs.writeFileSync(CACHE_FILE, JSON.stringify(altCache, null, 2));

}

// 5. 把 RPC 获得的结果也包装成 AddressLookupTableAccount
const fetchedAltAccounts = fetchedAlts.map(({ key, state }) =>
new AddressLookupTableAccount({ key, state })
);

// 6. 合并返回
return [...cachedAlts, ...fetchedAltAccounts];
}
async function getFeeForMessageRPC(msgV0) {
const res = await safeFetch(MAIN_RPC, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
jsonrpc: '2.0',
id: 1,
method: 'getFeeForMessage',
params: [Buffer.from(msgV0.serialize()).toString('base64')],
}),
});

const data = await res.json();

//fs.writeFileSync("11.txt", JSON.stringify(data, null, 2));

if (data.error) {
throw new Error(data.error.message);
}
if (data.result && typeof data.result.value === 'number') {
//return data.result.value; // 费率(lamports)
const val = data?.result?.value;
if (typeof val === 'number') return val;
console.warn('[getFeeForMessageRPC] 返回了非数字 fee,使用兜底 5000');
return 5_000;

}
throw new Error('No fee data in response');
}

/* ============================================================

  • ===== [SIZE_FIX_START] 更严格的 ALT 尺寸控制 =====
  • 原理:
    1. 先将 ALT 按 addresses.length 从大到小排序;
    1. 带入 compileToV0Message 做一次“真实”尺寸检测;
    1. 若仍超限,循环删除“最大的那张 ALT”重试,直到:
  •   • staticKeys + ΣALT.addresses < 256 且
    
  •   • 所有 accountKeyIndex < 256
    
    1. 如果删光 ALT 仍超限,则返回空数组,转为“无 ALT”版本。
  • ========================================================== */
    function strictTrimAlt(instr, payer, originAlts,DUMMY_BH) {
    // <── 新增:空值直接返回 []
    if (!originAlts || !Array.isArray(originAlts) || originAlts.length === 0) {
    return []; // 让调用方按 “无 ALT” 继续
    }
    // ① 先确保 alt 都是有效对象
    originAlts = (originAlts || []).filter(a => a && a.addresses);

// 没有可用 ALT 直接返回 []
if (originAlts.length === 0) return [];
// ① 拷贝一份并按地址数降序排,越大的越先被剔除
const sorted = [...originAlts].sort(
(a, b) => (b.addresses.length - a.addresses.length)
);

// ② 循环迭代,直到满足 isMessageTooBig === false
while (true) {
const msg = new TransactionMessage({
payerKey : payer,
recentBlockhash: DUMMY_BH, // dummy,尺寸检测即可
instructions : instr,
}).compileToV0Message(sorted.filter(Boolean));

text
if (!isMessageTooBig(msg)) { return sorted; // ← OK,返回当前 ALT 集合 }

/*
if (sorted.length === 0) {
return []; // 全删光依旧超 → 干脆不用 ALT
}
*/
if (sorted.length === 0) return []; // ← 交由调用方判空

text
// ③ 删除当前“最大的”一张 ALT,继续循环 sorted.shift();

}
}
// ===== [SIZE_FIX_END] =====
// Jupiter 的 swapInstruction 里 已经带了一条 setComputeUnitLimit
// 先把它们剔掉
function stripCbIx(instructions=[]){
return instructions.filter(ix =>
!ix.programId.equals(ComputeBudgetProgram.programId));
}
function safeDeserialize(ixB64){
if (!ixB64) return null; // ① 彻底过滤 undefined / ''
try {
return deserializeInstruction(ixB64); // ② 正常反序列化
} catch(e){
console.warn('[deserialize] 跳过异常指令:', e.message);
return null; // ③ 任何解析异常也返回 null
}
}
/
---------------- 线程主逻辑 ---------------- /
async function doWork(){
try{
const token=tokens[Math.floor(Math.random()tokens.length)];
//const token='EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v';
const amt=BASE_AMOUNT_SOL
LAMPORTS_PER_SOL;
//增加滑点容忍度: 从 slippageBps=50 (0.5%) 增加到 slippageBps=100 (1%)
//alt賬戶:256 上限
const qIn =await jQuote(inputMint=So11111111111111111111111111111111111111112&outputMint=${token}&amount=${amt}&onlyDirectRoutes=false&maxAccounts=${maxAccounts}&slippageBps=${SLIPPAGE_BPS});
if(!qIn?.outAmount) return;
const qOut=await jQuote(inputMint=${token}&outputMint=So11111111111111111111111111111111111111112&amount=${qIn.outAmount}&onlyDirectRoutes=false&maxAccounts=${maxAccounts}&slippageBps=${SLIPPAGE_BPS0});
if(!qOut?.outAmount) return;
const profit=qOut.outAmount-amt;
//console.log([T${threadId}] token ${token} profit ${profit});
// ==== PATCH-BEGIN (放在 profit 计算完之后) ====
/
*

  • 动态滑点(bps) 0.5% ≤ slip ≤ 50%
  • 当利润越大 → 允许滑点越大;保证最小 50bps,最大 5 000bps
    /
    /

    const dynSlipInBps = Math.min(
    5_000,
    Math.max( 50, Math.ceil(0.8 * profit / qIn .outAmount * 10_000) )
    );
    const dynSlipOutBps = Math.min(
    5_000,
    Math.max( 50, Math.ceil(0.8 * profit / qOut.outAmount * 10_000) )
    );
    */
    //800 bps (=0.8 %)
    //0.8% 封顶;而且把第二跳最小值调到 100bps,不容易 6001
    const dynSlipInBps = Math.min(800, Math.max(SLIPPAGE_BPS, profit * 1e4 / qIn.outAmount ));
    const dynSlipOutBps = Math.min(1500, Math.max(SLIPPAGE_BPS0, profit * 1e4 / qOut.outAmount ));
    //console.log([T${threadId}] token ${token} profit ${profit} dynSlipInBps ${dynSlipInBps} dynSlipOutBps ${dynSlipOutBps});

// ==== PATCH-END ====
//为正-不亏本
const solProfit = profit / LAMPORTS_PER_SOL;
const usdtProfit = solProfit150;
if (qOut.outAmount>=amt) {
const now = new Date().toLocaleString();
fs.appendFileSync('不亏本.txt',[T${threadId}] [${now}] 使用 token: ${token} 利润: ${profit} lamports (${solProfit} SOL) ${usdtProfit} usdt \n);
}
if(profit<MIN_PROFIT_FIXED) return;//提高利润门槛: 从 15,000 增加到 20,000+ lamports
/
计算优先费:利润的 90%(向下取整) */
// ① 计算 90 %,单位 μ-lamports;5_000 兜底
const prioMicroLamports = Math.max(5_000, Math.floor(profit * 0.9));
let swapIn, swapOut;
let lookupKeys;
let altAcc;
let altStates;
let instr;
let altWrappers;

try
{

/*
[swapIn,swapOut]=await Promise.all([
jIx({quoteResponse:qIn,userPublicKey:keypair.publicKey.toBase58(),wrapAndUnwrapSol:true,dynamicComputeUnitLimit:true,prioritizationFeeLamports:prioMicroLamports}),
jIx({quoteResponse:qOut,userPublicKey:keypair.publicKey.toBase58(),wrapAndUnwrapSol:true,dynamicComputeUnitLimit:true,prioritizationFeeLamports:prioMicroLamports}),
]);
/
/

[swapIn, swapOut] = await Promise.all([
jIx(buildIxBody(qIn,lookupKeys,lookupKeys)),
jIx(buildIxBody(qOut,lookupKeys, lookupKeys)),
]);
/
/
---------------- 请求 swap-instructions ---------------- /
/
---------- 2. 先请求 swap-instructions ---------- /
// 注释掉原来的 jIx 调用
/

[swapIn, swapOut] = await Promise.all([
jIx(buildIxBody(qIn,prioMicroLamports, SLIPPAGE_BPS )), // 0.5 %
jIx(buildIxBody(qOut,prioMicroLamports, SLIPPAGE_BPS0)), // 1.5 %
]);
/
//第一步:不带任何 lookupKeys,获取 “推荐的” ALT keys
const [firstIn, firstOut] = await Promise.all([ getIxSafe(qIn, [], dynSlipInBps ,prioMicroLamports), getIxSafe(qOut, [], dynSlipOutBps,prioMicroLamports), ]);
if (!firstIn || !firstOut) return; // 防御
// 拿到所有推荐的 ALT 公钥
lookupKeys = [ ...firstIn.addressLookupTableAddresses, ...firstOut.addressLookupTableAddresses, ];
// —— 加载并裁剪 ALT accounts,得到 altStates
altWrappers = await loadAltAccountsWithCache(lookupKeys);
altStates = (altWrappers || []) // ← 避免 null
//.filter(w => w.addresses && w.addresses.length < 256) // ✅ 直接用 wrapper
.map(trimAlt) // 先把 256+ 的表裁成 255
.slice(0, 32); // 最多 32 张
/

let altStates = altWrappers .map(w => { w.state.key = w.key;
return w.state; }).filter(s => s.addresses.length < 256) .slice(0, 32); // (这里还可以再走 strictTrimAlt/trimAltSet 的逻辑)
*/

// —— 第二步:带上 lookupKeys,拿到真正要用的指令
[swapIn, swapOut] = await Promise.all([ getIxSafe(qIn, lookupKeys, dynSlipInBps ,prioMicroLamports), getIxSafe(qOut, lookupKeys, dynSlipOutBps,prioMicroLamports), ]);
//
// 只取核心 swapInstruction,去掉 setup/cleanup(你也可以按需保留)
//const instr = [ deserializeInstruction(swapIn.swapInstruction), deserializeInstruction(swapOut.swapInstruction), ];

// 新:带滑点重试的安全接口
/*
[swapIn, swapOut] = await Promise.all([
getIxSafe(qIn, lookupKeys, SLIPPAGE_BPS),
getIxSafe(qOut, lookupKeys, SLIPPAGE_BPS0),
]);
/
/

instr=[...swapIn.setupInstructions.map(deserializeInstruction),
deserializeInstruction(swapIn.swapInstruction),
deserializeInstruction(swapOut.swapInstruction),
deserializeInstruction(swapOut.cleanupInstruction)];
*/
const inIxs = stripCbIx([
...(swapIn.setupInstructions || []).map(safeDeserialize),
safeDeserialize(swapIn.swapInstruction),
]).filter(Boolean); // ④ 过滤 null

const outIxs = stripCbIx([
safeDeserialize(swapOut.swapInstruction),
safeDeserialize(swapOut.cleanupInstruction), // 允许 undefined
]).filter(Boolean);

instr = [...inIxs, ...outIxs]; // 绝不包含 undefined
if (!Array.isArray(instr) || instr.length === 0) return;

} catch (error) {
console.error('!!! [instr] 报错:', error.message);
console.error('>>> error stackaa:', error.stack); // 打印堆栈
}
/* ---------- 3. 收集 ALT pubkey,再去加载 ---------- /
const DUMMY_BH ='11111111111111111111111111111111'; // 32 个字符
const tmpMsg = new TransactionMessage({
payerKey: keypair.publicKey,
recentBlockhash: latestBlockhashData.blockhash,
instructions: instr,
}).compileToV0Message([]); // 先不用 ALT
const staticCnt = tmpMsg.staticAccountKeys.length;
if (staticCnt > 38 && altStates.length === 0) {
console.warn([T${threadId}] static=${staticCnt} 无 ALT,必超 1232B → skip);
return;
}
//altStates = trimAltSet(staticCnt, altStates);
/
---------- 使用严格裁剪替换旧逻辑 ---------- */
//兜底:如果 strictTrimAlt 返回 null(全部裁到不能用)就转成 []:
altStates = strictTrimAlt(instr, keypair.publicKey, altStates,latestBlockhashData.blockhash)|| [];

/* ---------- 全局 255 兜底裁剪 ---------- */
//altStates = trimAltSet(staticCntRough, altStates);

/* 若仍 >255(极端情况),干脆清空,走无 ALT 版本 */
const totalAddr = staticCnt +
altStates.reduce((n,t)=>n+t.addresses.length,0);
if (totalAddr >= 256) altStates = [];

/* 若数量不一致,打印一下方便对照 */
if (altStates.length !== altWrappers.length && altStates.length > 0) {
console.warn([ALT] ${altWrappers.length} => ${altStates.length} 有效 ALT);
}

console.log([T${threadId}] token ${token} profit ${profit} step1);

// ---------- DEBUG: 把 altStates dump 到 a.txt ----------
// ② dump 前再检查一次
try {
if (altStates.length) {
const dump = altStates.map((s, i) => ({
idx : i,
//key : s.key.toBase58(),
key: (s.key ?? new PublicKey(lookupKeys[i])).toBase58(),
addresses : s.addresses.length,
}));
/*
fs.writeFileSync(
${token}.txt,
[${new Date().toLocaleString()}] altStates(${altStates.length})\n +
JSON.stringify(dump, null, 2) + '\n\n',
{ flag: 'a' }
);
/
} else {
/

fs.writeFileSync(
${token}.txt,
[${new Date().toLocaleString()}] altStates(0)\n\n,
{ flag: 'a' }
);
*/

}
} catch (e) {
console.warn('❗ dump altStates 失败:', e.message);
}

//盈虧控制 start
// 1) 估算 CU
//let computeUnits =COMPUTE_UNIT_FALLBACK;//优先费 200_000; // fallback(约 200K CU)
//try {
const sim= await simulateTransaction(instr, keypair.publicKey, altStates);
if (!sim || !sim.result) {
// ① simulate 为空的分支
console.error(
[T${threadId}] token ${token} profit ${profit} [simulate] 空 result,完整响应 =,
JSON.stringify(sim, null, 2)
);

return; // 或者 throw,自行決定
}
// ② simulate 正常的分支
console.log(
[T${threadId}] token ${token} profit ${profit} [simulate] result =\n,
JSON.stringify(sim.result, null, 2)
);

/*
if (sim.value.err) {
throw new Error(交易模拟失败: ${JSON.stringify(sim.value.err)});
}
/
if (sim.result.value?.err) {
throw new Error(交易模擬失敗: ${JSON.stringify(sim.result.value.err)});
}
// ---------- 估算 CU ----------
// 单笔交易里
// 1) simulate 拿 unitsConsumed
const consumed = sim.result.value.unitsConsumed || 200_000;
const computeUnits = Math.ceil(consumed * 1.1);
// 2) 签名费精确
//const msgV0 = new TransactionMessage({ /
same as createVersionedTransaction 里 */ })
//.compileToV0Message(altStates);

const msgV0 = new TransactionMessage({
payerKey : keypair.publicKey,
recentBlockhash: latestBlockhashData.blockhash, // 或 latestBlockhash.blockhash
instructions : instr,
}).compileToV0Message(altStates.filter(Boolean));

const sigFee =5_000;//await getFeeForMessageRPC(msgV0);// await connection.getFeeForMessage(msgV0);//5_000
console.log([T${threadId}] profit=${profit} sigFee=${sigFee} );
// 3) 优先费
const priorityFeeCost = Math.ceil(computeUnits * avgPriorFee / 1_000_000);
// 4) tip 固定或比例
const tipLamports = Math.min(10_000, Math.max(1_000, Math.floor(priorityFeeCost * 0.1)));
// 5) 总成本=优先费+小費+签名费
const totalCost = sigFee + priorityFeeCost + tipLamports;

// ---------- microLamports (μ-lamports / CU) ----------
//let microLamports = Math.floor(profit * 0.9 * 1_000_000 / computeUnits);
//microLamports = Math.max(5_000, Math.min(microLamports, 100_000));
// 2) 优先费单价直接用 avgPriorFee
//const microLamports = avgPriorFee;
//从固定 avgPriorFee 切到“挂钩利润”的动态 μ-lamports
/*
要确保 sigFee、computeUnits、profit 已经提前算好并在当前作用域可用。
此处用 1e6 把 lamports → μ-lamports。
/
const expectedProfit = profit - sigFee;
/

const microLamports = Math.max(
5_000,
Math.min(
Math.floor(expectedProfit1e6/ computeUnits),
200_000
)
);
/
/

优先费 μ-lamports 建议直接用 avgPriorFee × 1.2
不再跟利润耦合,避免把单价抬到 20 万:
/
//const microLamports = Math.round(avgPriorFee * 1.2);
//Priority Fee 上限 200 000μ 已够高,再乘 1.2 可能会炸,建议 Math.min(avg
1.2, 200_000)
const microLamports =Math.min(avgPriorFee
1.2, 200_000)

// ③ 成本檢查
const ta=profit - totalCost;
console.log([T${threadId}] token ${token} profit ${profit} dynSlipInBps ${dynSlipInBps} dynSlipOutBps ${dynSlipOutBps} totalCost ${totalCost} 净利润: ${ta});

if (totalCost >= profit) {
console.log([T${threadId}] 跳過:profit=${profit} cost=${totalCost} 扣费后无利润);
return;
}
// 4) 盈亏判断
if (profit - totalCost < MIN_NET_PROFIT) return;
//盈虧控制 end

//} catch (e) {
//console.warn('[simulate] 获取 unitsConsumed 失败,使用默认值 200000');
//}
console.log([T${threadId}] token ${token} profit ${profit} cost=${totalCost} computeUnits ${computeUnits} step2);
try{
//const latestBlockhash = await CONNECTION.getLatestBlockhash('finalized');
//const latestBlockhash=await getLatestBlockhash();
/*
const latestBlockhash = latestBlockhashData;
if (!latestBlockhash) {
// 还没有获取到值,可以等待或者立即调用获取
// 这里可等待首次初始化完成
latestBlockhash=await getLatestBlockhash();
}
*/
console.log([T${threadId}] token ${token} profit ${profit} latestBlockhash ${latestBlockhashData.blockhash} step2);

// 6) 创建最终交易
//const { blockhash } = await ensureFreshBlockhash();
const transaction= createVersionedTransaction(
instr,
keypair.publicKey,
altStates,
latestBlockhashData.blockhash,
computeUnits,
microLamports, // 继续传给 expectedProfit,用于后面打赏
tipLamports
);
//console.log([T${threadId}] transaction ${JSON.stringify(transaction)} step2);
console.log([T${threadId}] profit ${profit} step210);
//transaction.sign([keypair]);
//signWithDebug(transaction, [keypair])
// 将这里替换:
// transaction.sign([keypair]);
signWithDebug(transaction, [keypair], {
token,
profit,
computeUnits,
priorityFee: microLamports,
altAccCount: altAcc?.length,
});

console.log([T${threadId}] profit ${profit} step211);
//const jitoBundle= await createJitoBundle(transaction);
//const jitoBundle = createJitoBundle(transaction, latestBlockhash.lastValidBlockHeight);
//console.log([T${threadId}] profit ${profit} step22);
//console.log([T${threadId}] jitoBundle ${JSON.stringify(jitoBundle)} step3);
const bundleId= await sendJitoBundle(transaction);
console.log([T${threadId}] bundleId [T${bundleId}] token ${token} profit ${profit} step4);
const bundleStatus = await checkBundleStatus(bundleId);
if (bundleStatus.status === 'Landed') {
console.log('✅ 成功落块', bundleStatus.landed_slot);
const now3 = new Date().toLocaleString();
fs.appendFileSync('落块.txt',[T${threadId}] [${now3}] 使用 token: ${token} 利润: ${profit} lamports (${solProfit} SOL) landed_slot ${bundleStatus.landed_slot} ${usdtProfit} usdt \n);

} else {
console.warn('❌ 未成功:', JSON.stringify(bundleStatus));
}
console.log('bundle: ', JSON.stringify(bundleStatus));
const now = new Date().toLocaleString();//toISOString();
fs.appendFileSync('交易.txt',[T${threadId}] [${now}] 使用 token: ${token} 利润: ${profit} lamports (${solProfit} SOL) bundle status ${JSON.stringify(bundleStatus)} bundleId ${bundleId}\n);

text
if(!bundleStatus || bundleStatus.status!=='Landed'){ throw new Error('Soory'); } console.log('========== SWAP DONE! =========='); }catch(err){ console.error('Error sending transaction:',err.message); } }catch(e){

console.error([T${threadId}] error,e.message);
console.error([T${threadId}] error stack:, e.stack); // 打印堆栈

}
}

return{doWork};
}

/* ---------------- 调度器 ---------------- */
async function main(){
console.log(启动 ${THREADS} 条逻辑线程…);
//让所有逻辑在拿到第一次 blockhash 之前不要跑
while (!latestBlockhashData || !latestBlockhashData.blockhash) {
await new Promise(r => setTimeout(r, 100));
}

const workers=Array.from({length:THREADS},(_,i)=>createWorker(i,PROXY_CHUNKS[i],TOKEN_CHUNKS[i]));
const setImmediatePromise = () => new Promise(res => setImmediate(res));
while(true){
await Promise.all(workers.map(w=>w.doWork()));
//await new Promise(r=>setTimeout(r,100));
await setImmediatePromise();

}
}
main().catch(console.error);

Pensiero

Assessing code errors

I’m reviewing the user’s code. Instead of asking for confirmation at each step, I'll summarize the key issues noticed. I could technically use a tool to parse variable names, but it might not be necessary. A manual reading shows some problems like the altAcc variable being referenced but not defined, and latestBlockhashData being used before it's set. There’s also some duplication, like getAveragePriorityFee being defined globally and in the worker. I'll just focus on these coding inconsistencies rather than running Python code.

Identifying variable issues

It looks like there’s a variable shadowing issue with computeUnits, and some variables are either declared but not used or inconsistently passed. For example, altAcc is declared but never assigned, and then signWithDebug references altAcc?.length.

Also, in the createVersionedTransaction function, there’s a mismatch where tipAmount isn't used, and microLamports is converted incorrectly. The parameter order seems off, with tipLamports being passed, even though the function signature expects microLamports first.

Reviewing potential coding issues

It looks like there's some complexity with variable shadowing and function implementations. For example, the altAcc variable is referenced but undefined, and the computeUnits variable is overshadowed. There's also a mix-up with function parameters; in the createJitoBundle function, there's a mismatch between what’s returned and expected. Additionally, problematic function definitions, like getAveragePriorityFee, are either duplicated or incorrectly scoped in different contexts. I’ll need to focus on how these function definitions may be conflicting.

Analyzing potential bugs and errors

The code has a few issues with variable scoping, especially related to function hoisting and shadowing. For example, getAveragePriorityFee is defined inside createWorker, but it's referenced globally, causing a ReferenceError when setInterval tries to access it before it’s defined. There are also concurrency issues, such as avgPriorFee being updated in different threads but not updated globally, leading to inaccurate values. Additionally, there’s misalignment with some parameters (like tipAmount and microLamports) that may cause confusion or unnoticed bugs in certain functions.

Thought for 31 seconds

Risposta

下面按 “流程梳理 → 潜在 Bug → 风险与改进建议” 的顺序给出代码审查结论。


1 . 系统整体运行流程(已简化成关键路径)

  1. 全局初始化

    • 读取 WALLET_PRIVATE_KEY 生成 keypair
    • 解析 THREADS / PROXY_POOL / TOKENS1000,切分为 PROXY_CHUNKSTOKEN_CHUNKS
    • 声明若干全局常量(滑点、最小利润、CU 上限等)。
    • 通过
      js
      setInterval(getAveragePriorityFee, 5_000) setInterval(getLatestBlockhash1, 500)
      挂 2 个定时器:
      • avgPriorFee:滚动计算网络平均 μ-lamports / CU
      • latestBlockhashData:滚动获取最新 blockhash
  2. 主线程 main()

    • 等待 latestBlockhashData 首次就绪 → 创建 THREADS 个逻辑线程 (createWorker)。
    • “while(true)+Promise.all” 调度所有线程的 doWork(),形成伪并发。
  3. 每个 worker 内部 (doWork)

    1. 随机抽 1 个目标 Token → 以 “SOL→Token→SOL” 两跳报价 (/quote)
    2. 估算毛利润 profit,并 动态调整滑点
    3. 第一次 请求 /swap-instructions 只为拿“建议的 ALT 表”;随后批量拉取 ALT → 裁剪 → 缓存
    4. 第二次 请求 /swap-instructions(带 ALT、公差、优先费)获取真指令
    5. 拼装 instrsimulateTransaction() 估 CU / 签名费
    6. 计算 priorityFee、tip、总成本;若 profit – costMIN_NET_PROFIT 则继续
    7. 调用 createVersionedTransactionsignWithDebugsendJitoBundle
    8. 轮询 checkBundleStatus,落块即记录;否则丢弃

2 . 关键错误和易错点

#位置问题描述影响
E-1全局 setInterval(getAveragePriorityFee …)getAveragePriorityFee定义写在 createWorker 内部作用域,全局无法访问,5 秒后第一次触发即 ReferenceError: getAveragePriorityFee is not defined所有 线程都会一直停留在默认 avgPriorFee = 5 000,同时刷错误日志。
E-2getLatestBlockhash1 / getBlockHeight强行写死代理 ['http://127.0.0.1:1082']忽略了传入的 PROXY_POOL;如果本机没挂 SOCKS5/HTTP 代理将直接全部失败。无法获取区块高度 / blockhash → 线程起不来。
E-3buildSafeFetch()当所有代理失效时 throw new Error('')(空字符串)。调试时看不到错误原因。难以定位网络问题。
E-4altAcc?.lengthsignWithDebugcontextInfo 中引用 altAcc,但实际从未给 altAcc 赋值。undefined?.length 恒为 undefined,属逻辑冗余,但误导调试。
E-5avgPriorFee 跨线程共享虽然是全局变量,但由于 E-1 导致其不再更新;各线程仍用过时值计算 microLamports可能高估 / 低估优先费,影响成交率或净利润。
E-6strictTrimAlt()入参 originAlts 若为 [] 时直接返回 [],但上层已过滤 altStates.length === 0 的情况;仍有可能出现两次空裁剪,日志判断混乱。极端情况下导致“静态 key>38 且无 ALT”分支失效,仍去 simulate,浪费 RPC。
E-7大量 fs.appendFileSync在逻辑线程里同步写文件会阻塞整个 Node 事件循环。QPS 上不去,且 I/O 抖动放大延迟。
E-8无限 while(true) + Promise.all(workers.map(...))若任一 worker.doWork() 抛未捕获异常,主循环会卡死(Promise.reject 未处理)。整体挂掉,需要 try-catch 包裹每个循环。
E-9反复 new HttpsProxyAgent('http://127.0.0.1:1082')sendJitoBundle 内每次调用都创建新 Agent,没有连接复用。内存 & FD 增长,在高并发时容易触达系统句柄上限。

3 . 风险点 & 优化建议

  1. 作用域 & Hoisting

    • getAveragePriorityFee 挪到文件顶层,或改 setInterval(() => workerGetAveragePriorityFee(...), 5000)
    • 避免在函数内部再定义与全局同名的方法,易踩“覆盖 / 闭包”坑。
  2. 网络层

    • getLatestBlockhash1 / getBlockHeight 也用注入的 proxyPool,并为主链 RPC 设置 回落列表
    • buildSafeFetch 建议给每次失败记录 res.statusagent.proxy,便于定位。
  3. 日志与 I/O

    • 替换为 异步 fs.appendFile 或集中收集后批量 flush。
    • 错误日志务必包含 stack 与可读消息,切勿抛空串。
  4. 并发稳态

    • 外层主循环加
      js
      try { await Promise.all(...); } catch (e) { console.error(e); }
    • 可以再加简单的 back-off,如单线程连续 3 次失败后休眠 1 s。
  5. 资源管理

    • 复用 HttpsProxyAgent:放在线程级常量,或使用 keepAlive: true 的全局 Agent.
    • 达到 CU_UPPER_LIMIT(1.4 M)时直接丢弃该交易,避免浪费 simulate。
  6. 业务逻辑

    • microLamports 已有动态计算,再乘 1.2 时用 Math.round 而非 *1.2 的浮点;同时做好 上/下限保护
    • 尽量把“利润→优先费→tip”逻辑封装成函数,减少魔数散落。

4 . 结论

  • 流程设计整体 OK:多线程 + 代理池 + 双报价 + ALT 缓存 + simulate 估 CU + Jito Bundle。
  • 阻断级 Bug:** E-1、E-2** 会直接导致程序无法进入正常套利;必须先修复。
  • 其余为 可预见的稳定性 / 性能风险,修复后能显著提高成功率与 TPS。

如果后续需要对某段具体逻辑(如 ALT 裁剪、priority fee 策略)做更细粒度重构,或者希望配合 esprima/ts-morph 做静态检查,请再告诉我!

Condividi questa Q&A