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" ) ;
1+ import { ethers } from "ethers" ;
2+ import dotenv from "dotenv" ;
3+ import * as fs from "fs/promises" ;
4+ import path from "path" ;
5+ import chalk from "chalk" ;
6+ import { HttpsProxyAgent } from "https-proxy-agent" ;
7+ import { SocksProxyAgent } from "socks-proxy-agent" ;
8+ import { fileURLToPath } from "url" ;
69
710dotenv . config ( ) ;
811
12+ // Define __dirname equivalent for ES Modules
13+ const __filename = fileURLToPath ( import . meta. url ) ;
14+ const __dirname = path . dirname ( __filename ) ;
15+
916// === CONFIG ===
1017// List of public RPCs for Celo.
1118const RPCS = [
@@ -18,49 +25,94 @@ const GAS_LIMIT = 21000;
1825const keysFile = "key.txt" ;
1926let lastKey = null ;
2027
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 ) ;
28+ // --- Key loader (inline, no extra files) ---
29+ let PRIVATE_KEYS = [ ] ;
30+ let ALL_WALLETS = [ ] ;
2631
27- // Pre-calculate all wallet addresses to easily check if all are inactive
28- const ALL_WALLETS = PRIVATE_KEYS . map ( key => {
29- const wallet = new ethers . Wallet ( key ) ;
30- return wallet . address ;
31- } ) ;
32+ // normalize to 0x-prefixed hex string
33+ function normalizeKey ( k ) {
34+ if ( ! k ) return null ;
35+ k = String ( k ) . trim ( ) ;
36+ if ( ! k ) return null ;
37+ return k . startsWith ( "0x" ) ? k : "0x" + k ;
38+ }
39+
40+ // Validate by constructing an ethers.Wallet
41+ function isValidPrivateKey ( k ) {
42+ try {
43+ new ethers . Wallet ( k ) ;
44+ return true ;
45+ } catch {
46+ return false ;
47+ }
48+ }
49+
50+ // Load keys: primary from env PRIVATE_KEYS (comma separated), fallback to keysFile (one-per-line)
51+ async function loadKeysInline ( { allowFileFallback = true } = { } ) {
52+ // 1) env var (preferred)
53+ const env = process . env . PRIVATE_KEYS ;
54+ if ( env && env . trim ( ) . length > 0 ) {
55+ PRIVATE_KEYS = env . split ( "," ) . map ( normalizeKey ) . filter ( Boolean ) ;
56+ } else if ( allowFileFallback ) {
57+ // 2) fallback to keysFile (async fs from fs/promises is used in this file)
58+ try {
59+ const raw = await fs . readFile ( keysFile , "utf-8" ) ;
60+ PRIVATE_KEYS = raw . split ( / \r ? \n / ) . map ( normalizeKey ) . filter ( Boolean ) ;
61+ } catch ( err ) {
62+ // leave PRIVATE_KEYS empty if file not found / unreadable
63+ PRIVATE_KEYS = [ ] ;
64+ }
65+ }
66+
67+ // validate & compute addresses
68+ const valid = [ ] ;
69+ const addrs = [ ] ;
70+ for ( const k of PRIVATE_KEYS ) {
71+ if ( ! k ) continue ;
72+ if ( ! isValidPrivateKey ( k ) ) {
73+ console . warn ( "Ignored invalid private key:" , k ) ;
74+ continue ;
75+ }
76+ valid . push ( k ) ;
77+ addrs . push ( new ethers . Wallet ( k ) . address ) ;
78+ }
79+ PRIVATE_KEYS = valid ;
80+ ALL_WALLETS = addrs ;
81+ }
3282
3383// --- Wallet Persona Management ---
3484const personaFile = "personas.json" ;
3585let walletProfiles = { } ;
3686let lastPersonaSave = 0 ;
3787const PERSONA_SAVE_DEBOUNCE_MS = 5_000 ; // coalesce multiple writes into one
3888
39- function loadPersonas ( ) {
40- if ( fs . existsSync ( personaFile ) ) {
41- try {
42- walletProfiles = JSON . parse ( fs . readFileSync ( personaFile , "utf-8" ) ) ;
43- console . log ( chalk . cyan ( "🎭 Loaded existing personas" ) ) ;
44- } catch ( e ) {
89+ async function loadPersonas ( ) {
90+ try {
91+ const data = await fs . readFile ( personaFile , "utf-8" ) ;
92+ walletProfiles = JSON . parse ( data ) ;
93+ console . log ( chalk . cyan ( "🎭 Loaded existing personas" ) ) ;
94+ } catch ( e ) {
95+ if ( e . code === 'ENOENT' ) {
96+ console . log ( chalk . yellow ( "🎭 Personas file not found, starting fresh." ) ) ;
97+ } else {
4598 console . error ( chalk . bgRed . white . bold ( "❌ Error parsing personas.json, starting fresh." ) ) ;
46- walletProfiles = { } ;
4799 }
100+ walletProfiles = { } ;
48101 }
49102}
50103
51104// Debounced save to avoid frequent blocking disk writes
52- function savePersonas ( ) {
105+ async function savePersonas ( ) {
53106 try {
54107 const now = Date . now ( ) ;
55108 if ( now - lastPersonaSave < PERSONA_SAVE_DEBOUNCE_MS ) {
56- // schedule a final write shortly
57109 setTimeout ( ( ) => {
58- try { fs . writeFileSync ( personaFile , JSON . stringify ( walletProfiles , null , 2 ) ) ; lastPersonaSave = Date . now ( ) ; }
110+ try { fs . writeFile ( personaFile , JSON . stringify ( walletProfiles , null , 2 ) ) ; lastPersonaSave = Date . now ( ) ; }
59111 catch ( e ) { console . error ( "failed saving personas:" , e . message ) ; }
60112 } , PERSONA_SAVE_DEBOUNCE_MS ) ;
61113 return ;
62114 }
63- fs . writeFileSync ( personaFile , JSON . stringify ( walletProfiles , null , 2 ) ) ;
115+ await fs . writeFile ( personaFile , JSON . stringify ( walletProfiles , null , 2 ) ) ;
64116 lastPersonaSave = now ;
65117 } catch ( e ) {
66118 console . error ( "failed saving personas:" , e . message ) ;
@@ -74,13 +126,12 @@ function ensurePersona(wallet) {
74126 pingBias : Math . random ( ) * 0.25 ,
75127 minAmount : 0.00005 + Math . random ( ) * 0.0001 ,
76128 maxAmount : 0.015 + Math . random ( ) * 0.005 ,
77- // NEW TRAITS
78- activeHours : [ 6 + Math . floor ( Math . random ( ) * 6 ) , 22 ] , // e.g. 06:00-22:00 UTC
79- cooldownAfterFail : 60 + Math . floor ( Math . random ( ) * 180 ) , // 1-4 min
80- avgWait : 60 + Math . floor ( Math . random ( ) * 41 ) , // base wait time 1-1.6 min
129+ activeHours : [ 2 + Math . floor ( Math . random ( ) * 4 ) , 22 + Math . floor ( Math . random ( ) * 3 ) ] , // 2-5 to 22-24 UTC
130+ cooldownAfterFail : 60 + Math . floor ( Math . random ( ) * 120 ) , // 1-3 min
131+ avgWait : 30 + Math . floor ( Math . random ( ) * 40 ) , // base wait time 30-70 sec
81132 retryBias : Math . random ( ) * 0.5 , // 0-50% chance to retry
82133 // dynamic per-wallet nonce retirement
83- maxNonce : 520 + Math . floor ( Math . random ( ) * 100 ) ,
134+ maxNonce : 520 + Math . floor ( Math . random ( ) * 80 ) ,
84135 // failure tracking
85136 failCount : 0 ,
86137 lastFailAt : null
@@ -94,21 +145,24 @@ function ensurePersona(wallet) {
94145const inactiveFile = "inactive.json" ;
95146let inactiveWallets = new Set ( ) ;
96147
97- function loadInactive ( ) {
98- if ( fs . existsSync ( inactiveFile ) ) {
99- try {
100- inactiveWallets = new Set ( JSON . parse ( fs . readFileSync ( inactiveFile , "utf-8" ) ) ) ;
101- console . log ( chalk . gray ( `📂 Loaded ${ inactiveWallets . size } inactive wallets` ) ) ;
102- } catch ( e ) {
148+ async function loadInactive ( ) {
149+ try {
150+ const data = await fs . readFile ( inactiveFile , "utf-8" ) ;
151+ inactiveWallets = new Set ( JSON . parse ( data ) ) ;
152+ console . log ( chalk . gray ( `📂 Loaded ${ inactiveWallets . size } inactive wallets` ) ) ;
153+ } catch ( e ) {
154+ if ( e . code === 'ENOENT' ) {
155+ console . log ( chalk . yellow ( "📂 Inactive file not found, starting empty." ) ) ;
156+ } else {
103157 console . error ( "❌ Failed parsing inactive.json, starting empty" ) ;
104- inactiveWallets = new Set ( ) ;
105158 }
159+ inactiveWallets = new Set ( ) ;
106160 }
107161}
108162
109- function saveInactive ( ) {
163+ async function saveInactive ( ) {
110164 try {
111- fs . writeFileSync ( inactiveFile , JSON . stringify ( [ ...inactiveWallets ] , null , 2 ) ) ;
165+ await fs . writeFile ( inactiveFile , JSON . stringify ( [ ...inactiveWallets ] , null , 2 ) ) ;
112166 } catch ( e ) {
113167 console . error ( "❌ Failed saving inactive.json:" , e . message ) ;
114168 }
@@ -121,11 +175,13 @@ function getLogFile() {
121175 return path . join ( __dirname , `tx_log_${ today } .csv` ) ;
122176}
123177
124- // This function initializes the log file with a header if it doesn't exist.
125- function initLogFile ( ) {
178+ async function initLogFile ( ) {
126179 const logFile = getLogFile ( ) ;
127- if ( ! fs . existsSync ( logFile ) ) {
128- fs . writeFileSync (
180+ try {
181+ await fs . access ( logFile ) ;
182+ } catch ( e ) {
183+ // File doesn't exist, create it with a header
184+ await fs . writeFile (
129185 logFile ,
130186 "timestamp,wallet,tx_hash,nonce,gas_used,gas_price_gwei,fee_celo,status,action\n"
131187 ) ;
@@ -139,10 +195,10 @@ const FLUSH_INTERVAL = 300 * 1000;
139195function bufferTxLog ( entry ) {
140196 txBuffer . push ( entry ) ;
141197}
142- function flushTxLog ( ) {
198+ async function flushTxLog ( ) {
143199 if ( txBuffer . length === 0 ) return ;
144- const logFile = initLogFile ( ) ;
145- fs . appendFileSync ( logFile , txBuffer . join ( "\n" ) + "\n" ) ;
200+ const logFile = await initLogFile ( ) ;
201+ await fs . appendFile ( logFile , txBuffer . join ( "\n" ) + "\n" ) ;
146202 console . log ( chalk . gray ( `📝 Flushed ${ txBuffer . length } tx logs to disk` ) ) ;
147203 txBuffer = [ ] ;
148204}
@@ -158,13 +214,46 @@ function pickRandomKey() {
158214 return lastKey ;
159215}
160216
161- // --- Provider without proxy ---
162- function getProvider ( rpcUrl ) {
217+ // --- Proxy Variables ---
218+ let proxies = [ ] ;
219+
220+ // --- Proxy Functions ---
221+ async function loadProxies ( ) {
222+ try {
223+ const fileContent = await fs . readFile ( "proxy.txt" , "utf8" ) ;
224+ proxies = fileContent . split ( "\n" ) . map ( proxy => proxy . trim ( ) ) . filter ( proxy => proxy ) ;
225+ if ( proxies . length === 0 ) {
226+ console . log ( chalk . cyan ( `[${ new Date ( ) . toISOString ( ) } ] ⟐ No proxy found in proxy.txt. Running without proxy.` ) ) ;
227+ } else {
228+ console . log ( chalk . green ( `[${ new Date ( ) . toISOString ( ) } ] ✔ Loaded ${ proxies . length } proxies from proxy.txt` ) ) ;
229+ }
230+ } catch ( error ) {
231+ if ( error . code === 'ENOENT' ) {
232+ console . log ( chalk . cyan ( `[${ new Date ( ) . toISOString ( ) } ] ⟐ No proxy.txt found, running without proxy.` ) ) ;
233+ } else {
234+ console . log ( chalk . red ( `[${ new Date ( ) . toISOString ( ) } ] ✖ Failed to load proxy: ${ error . message } ` ) ) ;
235+ }
236+ proxies = [ ] ;
237+ }
238+ }
239+
240+ function createAgent ( proxyUrl ) {
241+ if ( ! proxyUrl ) return null ;
242+ if ( proxyUrl . startsWith ( "socks" ) ) {
243+ return new SocksProxyAgent ( proxyUrl ) ;
244+ } else {
245+ return new HttpsProxyAgent ( proxyUrl ) ;
246+ }
247+ }
248+
249+ // --- Provider with proxy support ---
250+ function getProvider ( rpcUrl , agent ) {
163251 const network = {
164252 chainId : 42220 ,
165253 name : "celo"
166254 } ;
167- return new ethers . JsonRpcProvider ( rpcUrl , network ) ;
255+ const providerOptions = agent ? { fetchOptions : { agent } } : { } ;
256+ return new ethers . JsonRpcProvider ( rpcUrl , network , providerOptions ) ;
168257}
169258
170259/**
@@ -175,7 +264,10 @@ async function tryProviders() {
175264 console . log ( chalk . hex ( "#00FFFF" ) . bold ( "🔍 Searching for a working RPC endpoint..." ) ) ;
176265 for ( const url of RPCS ) {
177266 try {
178- const provider = getProvider ( url ) ;
267+ const proxyUrl = proxies . length > 0 ? proxies [ Math . floor ( Math . random ( ) * proxies . length ) ] : null ;
268+ const agent = createAgent ( proxyUrl ) ;
269+
270+ const provider = getProvider ( url , agent ) ;
179271 const network = await Promise . race ( [
180272 provider . getNetwork ( ) ,
181273 new Promise ( ( _ , reject ) => setTimeout ( ( ) => reject ( new Error ( "Timeout" ) ) , 5000 ) )
@@ -357,7 +449,7 @@ async function safeSendTx(wallet, provider, profile, url) {
357449}
358450
359451// === NEW: Refresh inactive wallets every 30 minutes ===
360- setInterval ( ( ) => {
452+ setInterval ( async ( ) => {
361453 console . log ( chalk . cyan ( "🔄 Refreshing inactive wallets..." ) ) ;
362454 for ( const addr of [ ...inactiveWallets ] ) {
363455 const profile = walletProfiles [ addr ] ;
@@ -366,17 +458,32 @@ setInterval(() => {
366458 console . log ( chalk . green ( `🌅 Wallet ${ addr } is now inside active hours` ) ) ;
367459 }
368460 }
369- saveInactive ( ) ;
461+ await saveInactive ( ) ;
370462} , 30 * 60 * 1000 ) ;
371463
372- async function loop ( ) {
373- loadPersonas ( ) ;
374- loadInactive ( ) ;
464+ async function main ( ) {
465+ // Load keys (env primary, file fallback)
466+ await loadKeysInline ( { allowFileFallback : true } ) ;
467+
468+ if ( PRIVATE_KEYS . length === 0 ) {
469+ console . error ( chalk . bgRed . white . bold ( `❌ No valid private keys found (env or ${ keysFile } ). Make sure PRIVATE_KEYS env var or ${ keysFile } exists and contains keys.` ) ) ;
470+ process . exit ( 1 ) ;
471+ }
472+
473+
474+ await loadPersonas ( ) ;
475+ await loadInactive ( ) ;
476+ await loadProxies ( ) ;
477+
478+ // === NEW LOGIC: Initial log file creation before the loop starts ===
479+ await initLogFile ( ) ;
480+
375481 while ( true ) {
376482 // === NEW LOGIC: Check if all wallets are inactive and sleep if so ===
377483 if ( inactiveWallets . size >= ALL_WALLETS . length ) {
378484 console . log ( chalk . yellow ( "😴 All wallets are currently inactive. Sleeping for 5 minutes..." ) ) ;
379- await randomDelay ( 300 , 300 ) ; // 5 minutes
485+ await randomDelay ( 240 , 360 ) ; // 5 minutes
486+ continue ;
380487 }
381488
382489 // === NEW LOGIC: Retry loop for RPC connection ===
@@ -400,7 +507,7 @@ async function loop() {
400507
401508 // === NEW: Main loop modification ===
402509 if ( ! checkActive ( wallet , profile ) ) {
403- await randomDelay ( 10 , 15 ) ; // just a short pause
510+ await randomDelay ( 10 , 15 ) ;
404511 continue ;
405512 }
406513
@@ -415,10 +522,29 @@ async function loop() {
415522 }
416523}
417524
418- loop ( ) ;
525+ main ( ) ;
419526
420527// ensure flush/persona save on termination/unhandled
421- process . on ( "SIGINT" , ( ) => { console . log ( "SIGINT" ) ; flushTxLog ( ) ; savePersonas ( ) ; process . exit ( ) ; } ) ;
422- process . on ( "SIGTERM" , ( ) => { console . log ( "SIGTERM" ) ; flushTxLog ( ) ; savePersonas ( ) ; process . exit ( ) ; } ) ;
423- process . on ( "exit" , ( ) => { flushTxLog ( ) ; savePersonas ( ) ; } ) ;
424- process . on ( "unhandledRejection" , ( r ) => { console . error ( "unhandledRejection:" , r ) ; flushTxLog ( ) ; savePersonas ( ) ; } ) ;
528+ process . on ( "SIGINT" , async ( ) => {
529+ console . log ( "SIGINT received, shutting down gracefully..." ) ;
530+ await flushTxLog ( ) ;
531+ await savePersonas ( ) ;
532+ process . exit ( ) ;
533+ } ) ;
534+ process . on ( "SIGTERM" , async ( ) => {
535+ console . log ( "SIGTERM received, shutting down gracefully..." ) ;
536+ await flushTxLog ( ) ;
537+ await savePersonas ( ) ;
538+ process . exit ( ) ;
539+ } ) ;
540+ process . on ( "exit" , async ( ) => {
541+ console . log ( "Exiting, flushing final logs..." ) ;
542+ await flushTxLog ( ) ;
543+ await savePersonas ( ) ;
544+ } ) ;
545+ process . on ( "unhandledRejection" , async ( r ) => {
546+ console . error ( "unhandledRejection:" , r ) ;
547+ await flushTxLog ( ) ;
548+ await savePersonas ( ) ;
549+ process . exit ( 1 ) ;
550+ } ) ;
0 commit comments