Skip to content

Commit b33d42a

Browse files
authored
Create index.js
1 parent 71b4de0 commit b33d42a

File tree

1 file changed

+357
-0
lines changed

1 file changed

+357
-0
lines changed

index.js

Lines changed: 357 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,357 @@
1+
const { ethers } = require("ethers");
2+
const dotenv = require("dotenv");
3+
const fs = require("fs");
4+
const path = require("path");
5+
const chalk = require("chalk");
6+
7+
dotenv.config();
8+
9+
// === CONFIG ===
10+
// List of public RPCs for Celo.
11+
const RPCS = [
12+
"https://rpc.ankr.com/celo",
13+
"https://celo.drpc.org",
14+
"https://forno.celo.org",
15+
"https://1rpc.io/celo"
16+
];
17+
const GAS_LIMIT = 21000;
18+
const keysFile = "key.txt";
19+
let lastKey = null;
20+
21+
// --- Load keys from file ---
22+
const PRIVATE_KEYS = fs.readFileSync(keysFile, "utf-8")
23+
.split("\n")
24+
.map(line => line.trim())
25+
.filter(line => line.length > 0);
26+
27+
// --- Wallet Persona Management ---
28+
const personaFile = "personas.json";
29+
let walletProfiles = {};
30+
let lastPersonaSave = 0;
31+
const PERSONA_SAVE_DEBOUNCE_MS = 5_000; // coalesce multiple writes into one
32+
33+
function loadPersonas() {
34+
if (fs.existsSync(personaFile)) {
35+
try {
36+
walletProfiles = JSON.parse(fs.readFileSync(personaFile, "utf-8"));
37+
console.log(chalk.cyan("🎭 Loaded existing personas"));
38+
} catch (e) {
39+
console.error(chalk.bgRed.white.bold("❌ Error parsing personas.json, starting fresh."));
40+
walletProfiles = {};
41+
}
42+
}
43+
}
44+
45+
// Debounced save to avoid frequent blocking disk writes
46+
function savePersonas() {
47+
try {
48+
const now = Date.now();
49+
if (now - lastPersonaSave < PERSONA_SAVE_DEBOUNCE_MS) {
50+
// schedule a final write shortly
51+
setTimeout(() => {
52+
try { fs.writeFileSync(personaFile, JSON.stringify(walletProfiles, null, 2)); lastPersonaSave = Date.now(); }
53+
catch (e) { console.error("failed saving personas:", e.message); }
54+
}, PERSONA_SAVE_DEBOUNCE_MS);
55+
return;
56+
}
57+
fs.writeFileSync(personaFile, JSON.stringify(walletProfiles, null, 2));
58+
lastPersonaSave = now;
59+
} catch (e) {
60+
console.error("failed saving personas:", e.message);
61+
}
62+
}
63+
64+
function ensurePersona(wallet) {
65+
if (!walletProfiles[wallet.address]) {
66+
walletProfiles[wallet.address] = {
67+
idleBias: Math.random() * 0.25,
68+
pingBias: Math.random() * 0.25,
69+
minAmount: 0.00005 + Math.random() * 0.0001,
70+
maxAmount: 0.015 + Math.random() * 0.005,
71+
// NEW TRAITS
72+
activeHours: [6 + Math.floor(Math.random() * 6), 22], // e.g. 06:00-22:00 UTC
73+
cooldownAfterFail: 60 + Math.floor(Math.random() * 180), // 1-4 min
74+
avgWait: 60 + Math.floor(Math.random() * 120), // base wait time 1-3 min
75+
retryBias: Math.random() * 0.5, // 0-50% chance to retry
76+
// dynamic per-wallet nonce retirement
77+
maxNonce: 520 + Math.floor(Math.random() * 100),
78+
// failure tracking
79+
failCount: 0,
80+
lastFailAt: null
81+
};
82+
savePersonas();
83+
}
84+
return walletProfiles[wallet.address];
85+
}
86+
87+
// --- Dynamic Log File Management (Daily Rotation) ---
88+
// This function returns the log file path for the current day.
89+
function getLogFile() {
90+
const today = new Date().toISOString().split("T")[0]; // YYYY-MM-DD format
91+
return path.join(__dirname, `tx_log_${today}.csv`);
92+
}
93+
94+
// This function initializes the log file with a header if it doesn't exist.
95+
function initLogFile() {
96+
const logFile = getLogFile();
97+
if (!fs.existsSync(logFile)) {
98+
fs.writeFileSync(
99+
logFile,
100+
"timestamp,wallet,tx_hash,nonce,gas_used,gas_price_gwei,fee_celo,status,action\n"
101+
);
102+
}
103+
return logFile;
104+
}
105+
106+
// --- Tx Log Buffer ---
107+
let txBuffer = [];
108+
const FLUSH_INTERVAL = 300 * 1000;
109+
function bufferTxLog(entry) {
110+
txBuffer.push(entry);
111+
}
112+
function flushTxLog() {
113+
if (txBuffer.length === 0) return;
114+
const logFile = initLogFile();
115+
fs.appendFileSync(logFile, txBuffer.join("\n") + "\n");
116+
console.log(chalk.gray(`📝 Flushed ${txBuffer.length} tx logs to disk`));
117+
txBuffer = [];
118+
}
119+
// Periodic flusher
120+
setInterval(flushTxLog, FLUSH_INTERVAL);
121+
122+
// --- Pick random key (with small chance of reusing last key) ---
123+
// Provides a more "human-like" key selection by sometimes re-using the last key.
124+
function pickRandomKey() {
125+
if (lastKey && Math.random() < 0.2) return lastKey;
126+
const idx = Math.floor(Math.random() * PRIVATE_KEYS.length);
127+
lastKey = PRIVATE_KEYS[idx];
128+
return lastKey;
129+
}
130+
131+
// --- Provider without proxy ---
132+
function getProvider(rpcUrl) {
133+
const network = {
134+
chainId: 42220,
135+
name: "celo"
136+
};
137+
return new ethers.JsonRpcProvider(rpcUrl, network);
138+
}
139+
140+
/**
141+
* Attempts to connect to an RPC endpoint.
142+
* @returns {Promise<{provider: ethers.JsonRpcProvider, url: string}|null>} The working provider and its URL, or null if all fail.
143+
*/
144+
async function tryProviders() {
145+
console.log(chalk.hex("#00FFFF").bold("🔍 Searching for a working RPC endpoint..."));
146+
for (const url of RPCS) {
147+
try {
148+
const provider = getProvider(url);
149+
const network = await Promise.race([
150+
provider.getNetwork(),
151+
new Promise((_, reject) => setTimeout(() => reject(new Error("Timeout")), 5000))
152+
]);
153+
console.log(chalk.hex("#00FF7F").bold(`✅ Connected: ${url}, Chain ID: ${network.chainId}`));
154+
return { provider, url };
155+
} catch (e) {
156+
console.log(chalk.hex("#FF5555").bold(`❌ Failed to connect to ${url}: ${e.message}`));
157+
}
158+
}
159+
return null;
160+
}
161+
162+
/**
163+
* Iterates through the list of RPCs and returns the first one that successfully connects.
164+
* @returns {Promise<{provider: ethers.JsonRpcProvider, url: string}>} The working provider and its URL.
165+
*/
166+
async function getWorkingProvider() {
167+
return await tryProviders();
168+
}
169+
170+
function randomDelay(minSec, maxSec) {
171+
const ms = (Math.floor(Math.random() * (maxSec - minSec + 1)) + minSec) * 1000;
172+
return new Promise(resolve => setTimeout(resolve, ms));
173+
}
174+
175+
function isWithinActiveHours(profile) {
176+
const nowUTC = new Date().getUTCHours();
177+
return nowUTC >= profile.activeHours[0] && nowUTC <= profile.activeHours[1];
178+
}
179+
180+
async function sendTx(wallet, provider, profile, url) {
181+
try {
182+
if (Math.random() < profile.idleBias) {
183+
console.log(chalk.hex("#808080").italic("\n😴 Persona idle mode, skipping this cycle..."));
184+
return;
185+
}
186+
187+
const balance = await provider.getBalance(wallet.address);
188+
await randomDelay(2, 4);
189+
190+
const walletNonce = await provider.getTransactionCount(wallet.address);
191+
await randomDelay(2, 4); // Another short delay
192+
193+
const maxNonce = profile.maxNonce;
194+
if (walletNonce >= maxNonce) {
195+
console.log(chalk.bgYellow.black.bold(`\n🟡 Wallet: ${wallet.address} nonce ${walletNonce} >= ${maxNonce}. Skipping.`));
196+
return;
197+
}
198+
199+
console.log(chalk.hex("#1E90FF").bold.underline(`\n🎲 Wallet: ${wallet.address}`));
200+
console.log(chalk.hex("#1E90FF").bold(`Using RPC: ${url}`));
201+
console.log(chalk.hex("#FFD700").bold(`Balance: ${ethers.formatEther(balance)} CELO`));
202+
console.log(chalk.hex("#FFD700").bold(`Nonce: ${walletNonce}`));
203+
204+
if (balance < ethers.parseEther("0.01")) {
205+
console.log(chalk.hex("#FFA500").bold("⚠️ Not enough balance, skipping..."));
206+
return;
207+
}
208+
209+
// === Decide action: normal send vs ping ===
210+
let action = "normal";
211+
let value;
212+
if (Math.random() < profile.pingBias) {
213+
action = "ping";
214+
value = 0n; // 0 CELO, just burns gas
215+
} else {
216+
const amount = profile.minAmount + Math.random() * (profile.maxAmount - profile.minAmount);
217+
value = ethers.parseEther(amount.toFixed(6));
218+
}
219+
220+
const tx = await wallet.sendTransaction({
221+
to: wallet.address,
222+
value: value,
223+
gasLimit: GAS_LIMIT
224+
});
225+
226+
console.log(chalk.hex("#7FFF00").bold(`✅ Sent tx: ${tx.hash}`));
227+
// Safely log the gas price, accounting for EIP-1559 transactions.
228+
const gasPrice = tx.gasPrice ?? tx.maxFeePerGas ?? 0n;
229+
if (gasPrice > 0n) {
230+
console.log(chalk.hex("#FF69B4").bold(`⛽ Gas Price (RPC): ${ethers.formatUnits(gasPrice, "gwei")} gwei`));
231+
}
232+
console.log(chalk.dim(`Explorer link: https://celoscan.io/tx/${tx.hash}`));
233+
234+
// Initialize variables outside of the try block to ensure scope
235+
let status = "pending", gasUsed = "", feeCELO = "", gasPriceGwei = "", txNonce = tx.nonce;
236+
237+
try {
238+
// Use Promise.race with a timeout that resolves to null
239+
const receipt = await Promise.race([
240+
tx.wait(),
241+
new Promise(resolve => setTimeout(() => resolve(null), 30000)) // returns null on timeout
242+
]);
243+
244+
if (receipt) {
245+
// Only access receipt properties if it's not null/undefined
246+
status = "confirmed";
247+
// Safely handle potentially missing gasPrice or gasUsed
248+
const gasPriceUsed = receipt?.effectiveGasPrice ?? receipt?.gasPrice ?? 0n;
249+
gasUsed = (receipt?.gasUsed ?? 0n).toString();
250+
gasPriceGwei = ethers.formatUnits(gasPriceUsed, "gwei");
251+
feeCELO = ethers.formatEther(gasPriceUsed * (receipt?.gasUsed ?? 0n));
252+
253+
console.log(chalk.bgGreen.white.bold("🟢 Confirmed!"));
254+
console.log(`   Nonce: ${txNonce}`);
255+
console.log(chalk.hex("#ADFF2F").bold(`   Gas Used: ${gasUsed}`));
256+
console.log(chalk.hex("#FFB6C1").bold(`   Gas Price: ${gasPriceGwei} gwei`));
257+
console.log(chalk.hex("#FFD700").bold(`   Fee Paid: ${feeCELO} CELO`));
258+
} else {
259+
console.log(chalk.bgYellow.white.bold("🟡 No confirmation in 30s, moving on..."));
260+
status = "timeout";
261+
}
262+
} catch (err) {
263+
console.error(chalk.bgRed.white.bold("❌ Error fetching receipt:", err.message));
264+
status = "error";
265+
}
266+
267+
// === Buffer to CSV (daily rotation) ===
268+
const line = [
269+
new Date().toISOString(),
270+
wallet.address,
271+
tx.hash,
272+
txNonce,
273+
gasUsed,
274+
gasPriceGwei,
275+
feeCELO,
276+
status,
277+
action
278+
].join(",");
279+
bufferTxLog(line);
280+
281+
} catch (err) {
282+
console.error(chalk.bgRed.white.bold("❌ Error in sendTx:", err.message));
283+
throw err; // Re-throw to be caught by safeSendTx for cooldown logic
284+
}
285+
}
286+
287+
async function safeSendTx(wallet, provider, profile, url) {
288+
try {
289+
await sendTx(wallet, provider, profile, url);
290+
} catch (err) {
291+
console.log(chalk.hex("#FFA500").bold("⚠️ Transaction failed. Checking persona retry bias..."));
292+
293+
// NEW LOGIC: Check retry bias and apply cooldown
294+
if (Math.random() > profile.retryBias) {
295+
console.log(chalk.hex("#FF8C00")(`⏸ Persona ${wallet.address} cooling down after fail...`));
296+
// Add a small jitter to the cooldown period
297+
const cooldownSec = profile.cooldownAfterFail + Math.floor(Math.random() * 60);
298+
await randomDelay(cooldownSec, cooldownSec);
299+
return;
300+
}
301+
302+
console.log(chalk.hex("#FFA500").bold("⚠️ Retrying after error..."));
303+
await randomDelay(5, 10);
304+
try { await sendTx(wallet, provider, profile, url); } catch (retryErr) {
305+
console.error(chalk.bgRed.white.bold("❌ Error on retry:", retryErr.message));
306+
}
307+
}
308+
}
309+
310+
async function loop() {
311+
loadPersonas(); // Load personas at the start of the loop
312+
while (true) {
313+
// === NEW LOGIC: Retry loop for RPC connection ===
314+
let provider = null;
315+
let url = null;
316+
while (!provider) {
317+
const providerResult = await getWorkingProvider();
318+
if (providerResult) {
319+
provider = providerResult.provider;
320+
url = providerResult.url;
321+
} else {
322+
console.log(chalk.hex("#FF8C00").bold("🚫 All RPCs failed to connect. Retrying in 10 seconds..."));
323+
await randomDelay(10, 15);
324+
}
325+
}
326+
// === END NEW LOGIC ===
327+
328+
const key = pickRandomKey();
329+
const wallet = new ethers.Wallet(key, provider);
330+
const profile = ensurePersona(wallet);
331+
332+
// Check if the wallet is within its active hours
333+
if (!isWithinActiveHours(profile)) {
334+
const sleepSec = 600 + Math.floor(Math.random() * 600); // 10–20 min idle
335+
console.log(chalk.gray(`🛌 Wallet ${wallet.address} is outside active hours, sleeping ${sleepSec}s`));
336+
await randomDelay(sleepSec, sleepSec);
337+
continue;
338+
}
339+
340+
// Execute the transaction logic, including retries
341+
await safeSendTx(wallet, provider, profile, url);
342+
343+
// NEW LOGIC: Use persona's avgWait for the wait loop
344+
let waitSec = Math.floor(profile.avgWait * (0.8 + Math.random() * 0.4));
345+
346+
console.log(chalk.hex("#00CED1").italic.bold(`⏳ Waiting ${waitSec}s before next tx...`));
347+
await randomDelay(waitSec, waitSec);
348+
}
349+
}
350+
351+
loop();
352+
353+
// ensure flush/persona save on termination/unhandled
354+
process.on("SIGINT", () => { console.log("SIGINT"); flushTxLog(); savePersonas(); process.exit(); });
355+
process.on("SIGTERM", () => { console.log("SIGTERM"); flushTxLog(); savePersonas(); process.exit(); });
356+
process.on("exit", () => { flushTxLog(); savePersonas(); });
357+
process.on("unhandledRejection", (r) => { console.error("unhandledRejection:", r); flushTxLog(); savePersonas(); });

0 commit comments

Comments
 (0)