你是一个区块链量化资深开发者, 以下是一个solana+jupiter 的多线程 多代理ip 套利程...
생성일: 2025년 6월 21일
생성일: 2025년 6월 21일
你是一个区块链量化资深开发者,
以下是一个solana+jupiter 的多线程 多代理ip 套利程序,
请分析 系统运行流程,确认代码有无错误
代码:
/*****************************************************************
node index.js
即可运行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);
//
/ =============================================================
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
textconst 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
textconst 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'); // 如需改代理自行修改
/ =========================================================================
// 放在 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;
}
/**
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);
textconst 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
textconst 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 取平均 -------- */
/* ============================================================
// 构造 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 交易>'
};
}
/* -----------------------------------------------------------
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');
}
/**
let tries = 0;
while (tries < maxRetries) {
// 轮换 Jito 节点
const url = jitoUrlIter.next().value;
tries++;
texttry { 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;
}
/* -------- 模拟交易 -------- /
/ ------------------------------------------------------------------
if (!sim) return
即可跳过本轮// 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')) {
textdumpMessageDetail(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;
textconst 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');
}
/* ============================================================
• staticKeys + ΣALT.addresses < 256 且
• 所有 accountKeyIndex < 256
// 没有可用 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));
textif (!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_SOLLAMPORTS_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 计算完之后) ====
/*
[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(avg1.2, 200_000)
const microLamports =Math.min(avgPriorFee1.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
);
textif(!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);
下面按 “流程梳理 → 潜在 Bug → 风险与改进建议” 的顺序给出代码审查结论。
全局初始化
WALLET_PRIVATE_KEY
生成 keypair
。THREADS / PROXY_POOL / TOKENS1000
,切分为 PROXY_CHUNKS
与 TOKEN_CHUNKS
。挂 2 个定时器:jssetInterval(getAveragePriorityFee, 5_000) setInterval(getLatestBlockhash1, 500)
avgPriorFee
:滚动计算网络平均 μ-lamports / CUlatestBlockhashData
:滚动获取最新 blockhash主线程 main()
latestBlockhashData
首次就绪 → 创建 THREADS
个逻辑线程 (createWorker
)。doWork()
,形成伪并发。每个 worker
内部 (doWork
)
/quote
)profit
,并 动态调整滑点/swap-instructions
只为拿“建议的 ALT 表”;随后批量拉取 ALT → 裁剪 → 缓存/swap-instructions
(带 ALT、公差、优先费)获取真指令instr
→ simulateTransaction()
估 CU / 签名费priorityFee
、tip、总成本;若 profit – cost
≥ MIN_NET_PROFIT
则继续createVersionedTransaction
→ signWithDebug
→ sendJitoBundle
checkBundleStatus
,落块即记录;否则丢弃# | 位置 | 问题描述 | 影响 |
---|---|---|---|
E-1 | 全局 setInterval(getAveragePriorityFee …) | getAveragePriorityFee 的定义写在 createWorker 内部作用域,全局无法访问,5 秒后第一次触发即 ReferenceError: getAveragePriorityFee is not defined 。 | 所有 线程都会一直停留在默认 avgPriorFee = 5 000 ,同时刷错误日志。 |
E-2 | getLatestBlockhash1 / getBlockHeight | 强行写死代理 ['http://127.0.0.1:1082'] ,忽略了传入的 PROXY_POOL ;如果本机没挂 SOCKS5/HTTP 代理将直接全部失败。 | 无法获取区块高度 / blockhash → 线程起不来。 |
E-3 | buildSafeFetch() | 当所有代理失效时 throw new Error('') (空字符串)。调试时看不到错误原因。 | 难以定位网络问题。 |
E-4 | altAcc?.length | 在 signWithDebug 的 contextInfo 中引用 altAcc ,但实际从未给 altAcc 赋值。 | undefined?.length 恒为 undefined ,属逻辑冗余,但误导调试。 |
E-5 | avgPriorFee 跨线程共享 | 虽然是全局变量,但由于 E-1 导致其不再更新;各线程仍用过时值计算 microLamports 。 | 可能高估 / 低估优先费,影响成交率或净利润。 |
E-6 | strictTrimAlt() | 入参 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 增长,在高并发时容易触达系统句柄上限。 |
作用域 & Hoisting
getAveragePriorityFee
挪到文件顶层,或改 setInterval(() => workerGetAveragePriorityFee(...), 5000)
。网络层
getLatestBlockhash1 / getBlockHeight
也用注入的 proxyPool
,并为主链 RPC 设置 回落列表。buildSafeFetch
建议给每次失败记录 res.status
与 agent.proxy
,便于定位。日志与 I/O
fs.appendFile
或集中收集后批量 flush。stack
与可读消息,切勿抛空串。并发稳态
jstry { await Promise.all(...); } catch (e) { console.error(e); }
资源管理
HttpsProxyAgent
:放在线程级常量,或使用 keepAlive: true
的全局 Agent
.CU_UPPER_LIMIT
(1.4 M)时直接丢弃该交易,避免浪费 simulate。业务逻辑
microLamports
已有动态计算,再乘 1.2
时用 Math.round
而非 *1.2
的浮点;同时做好 上/下限保护。如果后续需要对某段具体逻辑(如 ALT 裁剪、priority fee 策略)做更细粒度重构,或者希望配合
esprima
/ts-morph
做静态检查,请再告诉我!