├── seed.txt ├── README.md ├── config.json ├── package.json └── index.js /seed.txt: -------------------------------------------------------------------------------- 1 | Pharse1.. 2 | Pharse2.. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # TuskyAutoBot-NTE 2 | Full Tutorial Join https://t.me/NTExhaust 3 | -------------------------------------------------------------------------------- /config.json: -------------------------------------------------------------------------------- 1 | { 2 | "uploadConfig": { 3 | "uploadCount": 5 4 | } 5 | } -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "NT Exhaust - Tusky Testnet Auto Upload ", 3 | "version": "1.0.0", 4 | "main": "index.js", 5 | "type": "module", 6 | "scripts": { 7 | "start": "node index.js" 8 | }, 9 | "keywords": [], 10 | "author": "VinzSenzoo", 11 | "license": "ISC", 12 | "description": "", 13 | "dependencies": { 14 | "axios": "^1.8.4", 15 | "blessed": "^0.1.81", 16 | "@mysten/sui.js": "^0.54.1", 17 | "chalk": "^5.4.1", 18 | "figlet": "^1.8.1", 19 | "https-proxy-agent": "^7.0.6", 20 | "socks-proxy-agent": "^8.0.5", 21 | "uuid": "^11.1.0" 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | import blessed from "blessed"; 2 | import chalk from "chalk"; 3 | import figlet from "figlet"; 4 | import { SuiClient, getFullnodeUrl } from "@mysten/sui.js/client"; 5 | import { Ed25519Keypair } from "@mysten/sui.js/keypairs/ed25519"; 6 | import fs from "fs"; 7 | import axios from "axios"; 8 | import { SocksProxyAgent } from "socks-proxy-agent"; 9 | import { HttpsProxyAgent } from "https-proxy-agent"; 10 | 11 | const API_BASE_URL = "https://dev-api.tusky.io"; 12 | const CONFIG_FILE = "config.json"; 13 | const SEED_FILE = "seed.txt"; 14 | 15 | let walletInfo = { 16 | address: "N/A", 17 | activeAccount: "N/A", 18 | cycleCount: 0, 19 | nextCycle: "N/A" 20 | }; 21 | let transactionLogs = []; 22 | let activityRunning = false; 23 | let isCycleRunning = false; 24 | let shouldStop = false; 25 | let keypairs = []; 26 | let proxies = []; 27 | let selectedWalletIndex = 0; 28 | let loadingSpinner = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"]; 29 | const borderBlinkColors = ["cyan", "blue", "magenta", "red", "yellow", "green"]; 30 | let borderBlinkIndex = 0; 31 | let blinkCounter = 0; 32 | let spinnerIndex = 0; 33 | let isHeaderRendered = false; 34 | let activeProcesses = 0; 35 | let accountTokens = {}; 36 | let uploadConfig = { uploadCount: 3 }; 37 | let cycleTimeout = null; 38 | 39 | const photoAdjectives = [ 40 | "sunset", "ocean", "mountain", "forest", "sky", "river", "cloud", "dawn", "twilight", "horizon", 41 | "serene", "vibrant", "misty", "golden", "crimson", "azure", "emerald", "sapphire", "radiant", "tranquil" 42 | ]; 43 | const photoNouns = [ 44 | "moment", "view", "breeze", "scape", "light", "vibe", "dream", "path", "glow", "wave", 45 | "scene", "horizon", "peak", "valley", "shore", "canopy", "mist", "dusk", "dawn", "twilight" 46 | ]; 47 | const photoVerbs = [ 48 | "capture", "reflect", "illuminate", "glisten", "shine", "glow", "sparkle", "drift", "flow", "rise", 49 | "set", "dance", "whisper", "embrace", "bathe", "kiss", "caress", "paint", "etch", "frame" 50 | ]; 51 | 52 | function getRandomString(length = 5) { 53 | const characters = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"; 54 | let result = ""; 55 | for (let i = 0; i < length; i++) { 56 | result += characters.charAt(Math.floor(Math.random() * characters.length)); 57 | } 58 | return result; 59 | } 60 | 61 | function getFilename() { 62 | const adjective = photoAdjectives[Math.floor(Math.random() * photoAdjectives.length)]; 63 | const noun = photoNouns[Math.floor(Math.random() * photoNouns.length)]; 64 | const verb = photoVerbs[Math.floor(Math.random() * photoVerbs.length)]; 65 | const randomString = getRandomString(); 66 | return `${adjective}_${noun}_${verb}_${randomString}.jpg`; 67 | } 68 | 69 | function loadConfig() { 70 | try { 71 | if (fs.existsSync(CONFIG_FILE)) { 72 | const data = fs.readFileSync(CONFIG_FILE, "utf8"); 73 | const config = JSON.parse(data); 74 | uploadConfig = { ...uploadConfig, ...config.uploadConfig }; 75 | addLog("Loaded config file.", "success"); 76 | } else { 77 | addLog("No config file found, using default settings.", "info"); 78 | } 79 | } catch (error) { 80 | addLog(`Failed to load config: ${error.message}`, "error"); 81 | } 82 | } 83 | 84 | function saveConfig() { 85 | try { 86 | fs.writeFileSync(CONFIG_FILE, JSON.stringify({ uploadConfig }, null, 2)); 87 | addLog("Configuration saved successfully.", "success"); 88 | } catch (error) { 89 | addLog(`Failed to save config: ${error.message}`, "error"); 90 | } 91 | } 92 | 93 | process.on("unhandledRejection", (reason, promise) => { 94 | addLog(`Unhandled Rejection at: ${promise}, reason: ${reason}`, "error"); 95 | }); 96 | 97 | process.on("uncaughtException", (error) => { 98 | addLog(`Uncaught Exception: ${error.message}`, "error"); 99 | process.exit(1); 100 | }); 101 | 102 | function getShortAddress(address) { 103 | return address ? address.slice(0, 6) + "..." + address.slice(-4) : "N/A"; 104 | } 105 | 106 | function getShortId(id) { 107 | return id ? id.slice(0, 8) + "..." : "N/A"; 108 | } 109 | 110 | function addLog(message, type = "info") { 111 | const timestamp = new Date().toLocaleTimeString("id-ID", { timeZone: "Asia/Jakarta" }); 112 | let coloredMessage; 113 | switch (type) { 114 | case "error": 115 | coloredMessage = chalk.redBright(message); 116 | break; 117 | case "success": 118 | coloredMessage = chalk.greenBright(message); 119 | break; 120 | case "wait": 121 | coloredMessage = chalk.yellowBright(message); 122 | break; 123 | case "info": 124 | coloredMessage = chalk.whiteBright(message); 125 | break; 126 | case "delay": 127 | coloredMessage = chalk.cyanBright(message); 128 | break; 129 | default: 130 | coloredMessage = chalk.white(message); 131 | } 132 | const logMessage = `[${timestamp}] ${coloredMessage}`; 133 | transactionLogs.push(logMessage); 134 | if (transactionLogs.length > 100) { 135 | transactionLogs.shift(); 136 | } 137 | updateLogs(); 138 | } 139 | 140 | function clearTransactionLogs() { 141 | transactionLogs = []; 142 | logBox.setContent(''); 143 | logBox.scrollTo(0); 144 | addLog("Transaction logs cleared.", "success"); 145 | } 146 | 147 | async function sleep(ms) { 148 | if (shouldStop) { 149 | return; 150 | } 151 | activeProcesses++; 152 | try { 153 | await new Promise((resolve) => { 154 | const timeout = setTimeout(() => { 155 | resolve(); 156 | }, ms); 157 | const checkStop = setInterval(() => { 158 | if (shouldStop) { 159 | clearTimeout(timeout); 160 | clearInterval(checkStop); 161 | resolve(); 162 | } 163 | }, 100); 164 | }); 165 | } finally { 166 | activeProcesses = Math.max(0, activeProcesses - 1); 167 | } 168 | } 169 | 170 | function getRandomDelay(min, max) { 171 | return Math.floor(Math.random() * (max - min + 1)) + min; 172 | } 173 | 174 | function loadSeedPhrases() { 175 | try { 176 | const data = fs.readFileSync(SEED_FILE, "utf8"); 177 | const seeds = data.split("\n").map(seed => seed.trim()).filter(seed => seed.split(" ").length >= 12); 178 | keypairs = seeds.map(seed => { 179 | try { 180 | return Ed25519Keypair.deriveKeypair(seed); 181 | } catch (error) { 182 | addLog(`Invalid seed phrase: ${seed.slice(0, 10)}...`, "error"); 183 | return null; 184 | } 185 | }).filter(kp => kp !== null); 186 | if (keypairs.length === 0) throw new Error("No valid seed phrases in seed.txt"); 187 | addLog(`Loaded ${keypairs.length} seed phrases from seed.txt`, "success"); 188 | } catch (error) { 189 | addLog(`Failed to load seed phrases: ${error.message}`, "error"); 190 | keypairs = []; 191 | } 192 | } 193 | 194 | function loadProxies() { 195 | try { 196 | const data = fs.readFileSync("proxy.txt", "utf8"); 197 | proxies = data.split("\n").map(proxy => proxy.trim()).filter(proxy => proxy); 198 | if (proxies.length === 0) throw new Error("No proxies found in proxy.txt"); 199 | addLog(`Loaded ${proxies.length} proxies from proxy.txt`, "success"); 200 | } catch (error) { 201 | addLog(`No proxy.txt found or failed to load, running without proxies: ${error.message}`, "warn"); 202 | proxies = []; 203 | } 204 | } 205 | 206 | function createAgent(proxyUrl) { 207 | if (!proxyUrl) return null; 208 | if (proxyUrl.startsWith("socks")) { 209 | return new SocksProxyAgent(proxyUrl); 210 | } else if (proxyUrl.startsWith("http") || proxyUrl.startsWith("https")) { 211 | return new HttpsProxyAgent(proxyUrl); 212 | } 213 | throw new Error(`Unsupported proxy protocol: ${proxyUrl}`); 214 | } 215 | 216 | async function getClientWithProxy(proxyUrl) { 217 | try { 218 | const agent = createAgent(proxyUrl); 219 | const client = new SuiClient({ 220 | url: RPC_URL, 221 | transport: agent ? { agent } : undefined 222 | }); 223 | await client.getChainIdentifier(); 224 | return client; 225 | } catch (error) { 226 | addLog(`Failed to initialize client with proxy: ${error.message}`, "error"); 227 | return new SuiClient({ url: RPC_URL }); 228 | } 229 | } 230 | 231 | function getHeaders(token = null) { 232 | const headers = { 233 | "accept": "application/json, text/plain, */*", 234 | "accept-encoding": "gzip, deflate, br", 235 | "accept-language": "en-US,en;q=0.9,id;q=0.8", 236 | "client-name": "Tusky-App/dev", 237 | "content-type": "application/json", 238 | "origin": "https://testnet.app.tusky.io", 239 | "priority": "u=1, i", 240 | "referer": "https://testnet.app.tusky.io/", 241 | "sdk-version": "Tusky-SDK/0.31.0", 242 | "sec-ch-ua": '"Chromium";v="134", "Not:A-Brand";v="24", "Google Chrome";v="134"', 243 | "sec-ch-ua-mobile": "?0", 244 | "sec-ch-ua-platform": '"Windows"', 245 | "sec-fetch-dest": "empty", 246 | "sec-fetch-mode": "cors", 247 | "sec-fetch-site": "same-site", 248 | "user-agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/134.0.0.0 Safari/537.36" 249 | }; 250 | if (token) { 251 | headers["authorization"] = `Bearer ${token}`; 252 | } 253 | return headers; 254 | } 255 | 256 | async function makeApiRequest(method, url, data, proxyUrl, customHeaders = {}, maxRetries = 3, retryDelay = 2000) { 257 | activeProcesses++; 258 | try { 259 | for (let attempt = 1; attempt <= maxRetries; attempt++) { 260 | try { 261 | const agent = createAgent(proxyUrl); 262 | const headers = { ...customHeaders }; 263 | const config = { 264 | method, 265 | url, 266 | data, 267 | headers, 268 | ...(agent ? { httpsAgent: agent, httpAgent: agent } : {}), 269 | timeout: 10000 270 | }; 271 | const response = await axios(config); 272 | return response.data; 273 | } catch (error) { 274 | let errorMessage = `Attempt ${attempt}/${maxRetries} failed for API request to ${url}`; 275 | if (error.response) errorMessage += `: HTTP ${error.response.status} - ${JSON.stringify(error.response.data || error.response.statusText)}`; 276 | else if (error.request) errorMessage += `: No response received`; 277 | else errorMessage += `: ${error.message}`; 278 | addLog(errorMessage, "error"); 279 | if (attempt < maxRetries) { 280 | addLog(`Retrying API request in ${retryDelay/1000} seconds...`, "wait"); 281 | await sleep(retryDelay); 282 | } 283 | } 284 | } 285 | throw new Error(`Failed to make API request to ${url} after ${maxRetries} attempts`); 286 | } finally { 287 | activeProcesses = Math.max(0, activeProcesses - 1); 288 | } 289 | } 290 | 291 | async function listFilesInVault(vaultId, parentId, proxyUrl, token) { 292 | try { 293 | const filesUrl = `${API_BASE_URL}/files?vaultId=${vaultId}&parentId=${parentId}&limit=80`; 294 | const response = await makeApiRequest("get", filesUrl, null, proxyUrl, getHeaders(token)); 295 | return response.items || []; 296 | } catch (error) { 297 | addLog(`Failed to list files in vault ${getShortId(vaultId)}: ${error.message}`, "error"); 298 | return []; 299 | } 300 | } 301 | 302 | async function getFolderId(vaultId, proxyUrl, token) { 303 | try { 304 | const foldersUrl = `${API_BASE_URL}/folders?vaultId=${vaultId}&parentId=${vaultId}&limit=80`; 305 | const response = await makeApiRequest("get", foldersUrl, null, proxyUrl, getHeaders(token)); 306 | if (response.items && response.items.length > 0) { 307 | return response.items[0].id; 308 | } 309 | return vaultId; 310 | } catch (error) { 311 | addLog(`Failed to fetch folders for vault ${getShortId(vaultId)}: ${error.message}`, "error"); 312 | return vaultId; 313 | } 314 | } 315 | 316 | function generateUploadMetadata(vaultId, parentId, filename, fileType, imageSize) { 317 | const metadata = { 318 | vaultId: Buffer.from(vaultId).toString("base64"), 319 | parentId: Buffer.from(parentId).toString("base64"), 320 | name: filename, 321 | type: Buffer.from(fileType).toString("base64"), 322 | filetype: Buffer.from(fileType).toString("base64"), 323 | filename: Buffer.from(filename).toString("base64"), 324 | numberOfChunks: Buffer.from("1").toString("base64"), 325 | chunkSize: Buffer.from(imageSize.toString()).toString("base64") 326 | }; 327 | return Object.entries(metadata).map(([key, value]) => `${key} ${value}`).join(","); 328 | } 329 | 330 | async function updateWalletData() { 331 | const walletDataPromises = keypairs.map(async (keypair, i) => { 332 | try { 333 | const address = keypair.getPublicKey().toSuiAddress(); 334 | const formattedEntry = `${i === selectedWalletIndex ? "→ " : " "}${getShortAddress(address)}`; 335 | if (i === selectedWalletIndex) { 336 | walletInfo.address = address; 337 | walletInfo.activeAccount = getShortAddress(address); 338 | } 339 | return formattedEntry; 340 | } catch (error) { 341 | addLog(`Failed to fetch wallet data for account #${i + 1}: ${error.message}`, "error"); 342 | return `${i === selectedWalletIndex ? "→ " : " "}N/A`; 343 | } 344 | }); 345 | const walletData = await Promise.all(walletDataPromises); 346 | addLog("Wallet data updated.", "success"); 347 | return walletData; 348 | } 349 | 350 | async function loginAccount(keypair, proxyUrl) { 351 | if (shouldStop) { 352 | return false; 353 | } 354 | try { 355 | const address = keypair.getPublicKey().toSuiAddress(); 356 | const challengeUrl = `${API_BASE_URL}/auth/create-challenge`; 357 | const challengePayload = { address: address }; 358 | const challengeResponse = await makeApiRequest("post", challengeUrl, challengePayload, proxyUrl, getHeaders()); 359 | 360 | if (!challengeResponse || !challengeResponse.nonce) { 361 | throw new Error("Invalid challenge response: No nonce received"); 362 | } 363 | const nonce = challengeResponse.nonce; 364 | 365 | const message = `tusky:connect:${nonce}`; 366 | const messageBytes = new TextEncoder().encode(message); 367 | const signatureObj = await keypair.signPersonalMessage(messageBytes); 368 | const signature = signatureObj.signature; 369 | 370 | const verifyUrl = `${API_BASE_URL}/auth/verify-challenge`; 371 | const verifyPayload = { 372 | address: address, 373 | signature: signature 374 | }; 375 | const verifyResponse = await makeApiRequest("post", verifyUrl, verifyPayload, proxyUrl, getHeaders()); 376 | if (!verifyResponse.idToken) { 377 | throw new Error("No idToken received in verify response"); 378 | } 379 | const idToken = verifyResponse.idToken; 380 | accountTokens[address] = idToken; 381 | addLog(`Account ${getShortAddress(address)}: Logged in successfully.`, "success"); 382 | return true; 383 | } catch (error) { 384 | addLog(`Account ${getShortAddress(keypair.getPublicKey().toSuiAddress())}: Login error: ${error.message}`, "error"); 385 | return false; 386 | } 387 | } 388 | 389 | async function generateRandomImage() { 390 | const randomSeed = Math.floor(Math.random() * 100000); 391 | const imageUrl = `https://picsum.photos/seed/${randomSeed}/500/500`; 392 | const imageResponse = await axios.get(imageUrl, { responseType: 'arraybuffer' }); 393 | return imageResponse.data; 394 | } 395 | 396 | async function autoUpload() { 397 | if (keypairs.length === 0) { 398 | addLog("No valid seed phrases found.", "error"); 399 | return; 400 | } 401 | addLog(`Starting Auto Upload for ${keypairs.length} accounts with ${uploadConfig.uploadCount} uploads each.`, "info"); 402 | activityRunning = true; 403 | isCycleRunning = true; 404 | shouldStop = false; 405 | activeProcesses = 0; 406 | updateMenu(); 407 | try { 408 | for (let accountIndex = 0; accountIndex < keypairs.length && !shouldStop; accountIndex++) { 409 | selectedWalletIndex = accountIndex; 410 | const proxyUrl = proxies[accountIndex % proxies.length] || null; 411 | const proxyInfo = proxyUrl ? `using proxy ${proxyUrl}` : "no proxy"; 412 | const keypair = keypairs[accountIndex]; 413 | const address = keypair.getPublicKey().toSuiAddress(); 414 | addLog(`Processing account ${accountIndex + 1}: ${getShortAddress(address)} (${proxyInfo}).`, "info"); 415 | await updateWallets(); 416 | 417 | const loginSuccess = await loginAccount(keypair, proxyUrl); 418 | if (!loginSuccess) { 419 | addLog(`Account ${accountIndex + 1}: Skipping upload due to login failure.`, "error"); 420 | continue; 421 | } 422 | 423 | let usedVaultIds = []; 424 | for (let i = 0; i < uploadConfig.uploadCount && !shouldStop; i++) { 425 | try { 426 | const vaultsUrl = `${API_BASE_URL}/vaults?status=active&limit=1000`; 427 | const vaultsResponse = await makeApiRequest("get", vaultsUrl, null, proxyUrl, getHeaders(accountTokens[address])); 428 | const vaults = vaultsResponse.items.filter(vault => vault.encrypted === false); 429 | if (!vaults || vaults.length === 0) { 430 | addLog(`Account ${accountIndex + 1}: No unencrypted vaults available.`, "error"); 431 | break; 432 | } 433 | addLog(`Account ${accountIndex + 1}: Found ${vaults.length} unencrypted vaults.`, "info"); 434 | 435 | let availableVaults = vaults.filter(vault => !usedVaultIds.includes(vault.id)); 436 | if (availableVaults.length === 0) { 437 | usedVaultIds = []; 438 | availableVaults = vaults; 439 | } 440 | 441 | const randomVault = availableVaults[Math.floor(Math.random() * availableVaults.length)]; 442 | const vaultId = randomVault.id; 443 | const vaultName = randomVault.name || getShortId(vaultId); 444 | usedVaultIds.push(vaultId); 445 | const parentId = await getFolderId(vaultId, proxyUrl, accountTokens[address]); 446 | addLog(`Account ${accountIndex + 1}: Selected Vault ${vaultName} For image ${i + 1}.`, "info"); 447 | 448 | const imageBuffer = await generateRandomImage(); 449 | const filename = getFilename(); 450 | const fileType = "image/jpeg"; 451 | 452 | const uploadUrl = `${API_BASE_URL}/uploads`; 453 | const uploadMetadata = generateUploadMetadata(vaultId, parentId, filename, fileType, imageBuffer.length); 454 | const uploadHeaders = { 455 | ...getHeaders(accountTokens[address]), 456 | "content-type": "application/offset+octet-stream", 457 | "tus-resumable": "1.0.0", 458 | "upload-length": imageBuffer.length.toString(), 459 | "upload-metadata": uploadMetadata, 460 | "accept": "*/*", 461 | "content-length": imageBuffer.length.toString() 462 | }; 463 | const uploadResponse = await makeApiRequest("post", uploadUrl, imageBuffer, proxyUrl, uploadHeaders); 464 | if (uploadResponse.uploadId) { 465 | addLog(`Account ${accountIndex + 1}: Image ${i + 1} uploaded successfully. ID: ${getShortId(uploadResponse.uploadId)}`, "success"); 466 | await sleep(2000); 467 | const files = await listFilesInVault(vaultId, parentId, proxyUrl, accountTokens[address]); 468 | if (files.some(file => file.name === filename)) { 469 | addLog(`Account ${accountIndex + 1}: Image ${i + 1} confirmed in vault ${vaultName} with name ${filename}`, "success"); 470 | } else { 471 | addLog(`Account ${accountIndex + 1}: Image ${i + 1} not found in vault ${vaultName}.`, "error"); 472 | } 473 | } else { 474 | addLog(`Account ${accountIndex + 1}: Image ${i + 1} upload failed. No uploadId received.`, "error"); 475 | } 476 | 477 | if (i < uploadConfig.uploadCount - 1 && !shouldStop) { 478 | const delay = getRandomDelay(4000, 10000); 479 | addLog(`Account ${accountIndex + 1}: Waiting ${(delay/1000).toFixed(2)} seconds before next upload...`, "delay"); 480 | await sleep(delay); 481 | } 482 | } catch (error) { 483 | addLog(`Account ${accountIndex + 1}: Image ${i + 1} upload error: ${error.message}`, "error"); 484 | } 485 | } 486 | 487 | if (accountIndex < keypairs.length - 1 && !shouldStop) { 488 | addLog(`Waiting 5 seconds before next account...`, "delay"); 489 | await sleep(5000); 490 | } 491 | } 492 | if (!shouldStop && activeProcesses <= 0) { 493 | addLog("All accounts processed. Waiting 24 hours for next cycle.", "success"); 494 | cycleTimeout = setTimeout(autoUpload, 24 * 60 * 60 * 1000); 495 | activityRunning = false; 496 | isCycleRunning = true; 497 | updateStatus(); 498 | safeRender(); 499 | } 500 | } catch (error) { 501 | addLog(`Auto upload failed: ${error.message}`, "error"); 502 | activityRunning = false; 503 | isCycleRunning = false; 504 | shouldStop = false; 505 | if (cycleTimeout) { 506 | clearTimeout(cycleTimeout); 507 | cycleTimeout = null; 508 | } 509 | updateMenu(); 510 | updateStatus(); 511 | safeRender(); 512 | } finally { 513 | if (shouldStop && activeProcesses <= 0) { 514 | activityRunning = false; 515 | isCycleRunning = false; 516 | shouldStop = false; 517 | if (cycleTimeout) { 518 | clearTimeout(cycleTimeout); 519 | cycleTimeout = null; 520 | } 521 | addLog("Auto upload stopped successfully.", "success"); 522 | updateMenu(); 523 | updateStatus(); 524 | safeRender(); 525 | } 526 | } 527 | } 528 | 529 | const screen = blessed.screen({ 530 | smartCSR: true, 531 | title: "TUSKY AUTO UPLOAD BOT", 532 | autoPadding: true, 533 | fullUnicode: true, 534 | mouse: true, 535 | ignoreLocked: ["C-c", "q", "escape"] 536 | }); 537 | 538 | const headerBox = blessed.box({ 539 | top: 0, 540 | left: "center", 541 | width: "100%", 542 | height: 6, 543 | tags: true, 544 | style: { fg: "yellow", bg: "default" } 545 | }); 546 | 547 | const statusBox = blessed.box({ 548 | left: 0, 549 | top: 6, 550 | width: "100%", 551 | height: 3, 552 | tags: true, 553 | border: { type: "line", fg: "cyan" }, 554 | style: { fg: "white", bg: "default", border: { fg: "cyan" } }, 555 | content: "Status: Initializing...", 556 | padding: { left: 1, right: 1, top: 0, bottom: 0 }, 557 | label: chalk.cyan(" Status "), 558 | wrap: true 559 | }); 560 | 561 | const walletBox = blessed.list({ 562 | label: " Wallet Information", 563 | top: 9, 564 | left: 0, 565 | width: "40%", 566 | height: "35%", 567 | border: { type: "line", fg: "cyan" }, 568 | style: { border: { fg: "cyan" }, fg: "white", bg: "default", item: { fg: "white" } }, 569 | scrollable: true, 570 | scrollbar: { bg: "cyan", fg: "black" }, 571 | padding: { left: 1, right: 1, top: 0, bottom: 0 }, 572 | tags: true, 573 | keys: true, 574 | vi: true, 575 | mouse: true, 576 | content: "Loading wallet data..." 577 | }); 578 | 579 | const logBox = blessed.log({ 580 | label: " Transaction Logs", 581 | top: 9, 582 | left: "41%", 583 | width: "60%", 584 | height: "100%-9", 585 | border: { type: "line" }, 586 | scrollable: true, 587 | alwaysScroll: true, 588 | mouse: true, 589 | tags: true, 590 | scrollbar: { ch: "│", style: { bg: "cyan", fg: "white" }, track: { bg: "gray" } }, 591 | scrollback: 100, 592 | smoothScroll: true, 593 | style: { border: { fg: "magenta" }, bg: "default", fg: "white" }, 594 | padding: { left: 1, right: 1, top: 0, bottom: 0 }, 595 | wrap: true, 596 | focusable: true, 597 | keys: true 598 | }); 599 | 600 | const menuBox = blessed.list({ 601 | label: " Menu ", 602 | top: "44%", 603 | left: 0, 604 | width: "40%", 605 | height: "56%", 606 | keys: true, 607 | vi: true, 608 | mouse: true, 609 | border: { type: "line" }, 610 | style: { fg: "white", bg: "default", border: { fg: "red" }, selected: { bg: "magenta", fg: "black" }, item: { fg: "white" } }, 611 | items: ["Start Auto Upload", "Set Manual Config", "Clear Logs", "Refresh", "Exit"], 612 | padding: { left: 1, top: 1 } 613 | }); 614 | 615 | const uploadConfigSubMenu = blessed.list({ 616 | label: " Manual Config Options ", 617 | top: "44%", 618 | left: 0, 619 | width: "40%", 620 | height: "56%", 621 | keys: true, 622 | vi: true, 623 | mouse: true, 624 | border: { type: "line" }, 625 | style: { 626 | fg: "white", 627 | bg: "default", 628 | border: { fg: "blue" }, 629 | selected: { bg: "blue", fg: "black" }, 630 | item: { fg: "white" } 631 | }, 632 | items: ["Set Upload Count", "Back to Main Menu"], 633 | padding: { left: 1, top: 1 }, 634 | hidden: true 635 | }); 636 | 637 | const configForm = blessed.form({ 638 | label: " Enter Config Value ", 639 | top: "center", 640 | left: "center", 641 | width: "30%", 642 | height: "40%", 643 | keys: true, 644 | mouse: true, 645 | border: { type: "line" }, 646 | style: { 647 | fg: "white", 648 | bg: "default", 649 | border: { fg: "blue" } 650 | }, 651 | padding: { left: 1, top: 1 }, 652 | hidden: true 653 | }); 654 | 655 | const configLabel = blessed.text({ 656 | parent: configForm, 657 | top: 0, 658 | left: 1, 659 | content: "Upload Count (1-10):", 660 | style: { fg: "white" } 661 | }); 662 | 663 | const configInput = blessed.textbox({ 664 | parent: configForm, 665 | top: 1, 666 | left: 1, 667 | width: "90%", 668 | height: 3, 669 | inputOnFocus: true, 670 | border: { type: "line" }, 671 | style: { 672 | fg: "white", 673 | bg: "default", 674 | border: { fg: "white" }, 675 | focus: { border: { fg: "green" } } 676 | } 677 | }); 678 | 679 | const configSubmitButton = blessed.button({ 680 | parent: configForm, 681 | top: 5, 682 | left: "center", 683 | width: 10, 684 | height: 3, 685 | content: "Submit", 686 | align: "center", 687 | border: { type: "line" }, 688 | clickable: true, 689 | keys: true, 690 | style: { 691 | fg: "white", 692 | bg: "blue", 693 | border: { fg: "white" }, 694 | hover: { bg: "green" }, 695 | focus: { bg: "green", border: { fg: "yellow" } } 696 | } 697 | }); 698 | 699 | screen.append(headerBox); 700 | screen.append(statusBox); 701 | screen.append(walletBox); 702 | screen.append(logBox); 703 | screen.append(menuBox); 704 | screen.append(uploadConfigSubMenu); 705 | screen.append(configForm); 706 | 707 | let renderQueue = []; 708 | let isRendering = false; 709 | function safeRender() { 710 | renderQueue.push(true); 711 | if (isRendering) return; 712 | isRendering = true; 713 | setTimeout(() => { 714 | try { 715 | if (!isHeaderRendered) { 716 | figlet.text("NT EXHAUST", { font: "ANSI Shadow" }, (err, data) => { 717 | if (!err) headerBox.setContent(`{center}{bold}{cyan-fg}${data}{/cyan-fg}{/bold}{/center}`); 718 | isHeaderRendered = true; 719 | }); 720 | } 721 | screen.render(); 722 | } catch (error) { 723 | addLog(`UI render error: ${error.message}`, "error"); 724 | } 725 | renderQueue.shift(); 726 | isRendering = false; 727 | if (renderQueue.length > 0) safeRender(); 728 | }, 100); 729 | } 730 | 731 | function adjustLayout() { 732 | const screenHeight = screen.height || 24; 733 | const screenWidth = screen.width || 80; 734 | 735 | headerBox.height = Math.max(6, Math.floor(screenHeight * 0.15)); 736 | statusBox.top = headerBox.height; 737 | statusBox.height = Math.max(3, Math.floor(screenHeight * 0.07)); 738 | 739 | walletBox.top = headerBox.height + statusBox.height; 740 | walletBox.width = Math.floor(screenWidth * 0.4); 741 | walletBox.height = Math.floor(screenHeight * 0.35); 742 | 743 | logBox.top = headerBox.height + statusBox.height; 744 | logBox.left = Math.floor(screenWidth * 0.41); 745 | logBox.width = Math.floor(screenWidth * 0.6); 746 | logBox.height = screenHeight - (headerBox.height + statusBox.height); 747 | 748 | menuBox.top = headerBox.height + statusBox.height + walletBox.height; 749 | menuBox.width = Math.floor(screenWidth * 0.4); 750 | menuBox.height = screenHeight - (headerBox.height + statusBox.height + walletBox.height); 751 | 752 | uploadConfigSubMenu.top = menuBox.top; 753 | uploadConfigSubMenu.width = menuBox.width; 754 | uploadConfigSubMenu.height = menuBox.height; 755 | uploadConfigSubMenu.left = menuBox.left; 756 | configForm.width = Math.floor(screenWidth * 0.3); 757 | configForm.height = Math.floor(screenHeight * 0.4); 758 | 759 | safeRender(); 760 | } 761 | 762 | function updateStatus() { 763 | const isProcessing = activityRunning || isCycleRunning; 764 | const status = activityRunning 765 | ? `${loadingSpinner[spinnerIndex]} ${chalk.yellowBright("Running")}` 766 | : isCycleRunning 767 | ? `${loadingSpinner[spinnerIndex]} ${chalk.yellowBright("Waiting for next cycle")}` 768 | : chalk.green("Idle"); 769 | const statusText = `Status: ${status} | Active Account: ${walletInfo.activeAccount} | Total Accounts: ${keypairs.length} | Uploads per Account: ${uploadConfig.uploadCount}`; 770 | try { 771 | statusBox.setContent(statusText); 772 | } catch (error) { 773 | addLog(`Status update error: ${error.message}`, "error"); 774 | } 775 | if (isProcessing) { 776 | if (blinkCounter % 1 === 0) { 777 | statusBox.style.border.fg = borderBlinkColors[borderBlinkIndex]; 778 | borderBlinkIndex = (borderBlinkIndex + 1) % borderBlinkColors.length; 779 | } 780 | blinkCounter++; 781 | } else { 782 | statusBox.style.border.fg = "cyan"; 783 | } 784 | spinnerIndex = (spinnerIndex + 1) % loadingSpinner.length; 785 | safeRender(); 786 | } 787 | 788 | async function updateWallets() { 789 | const walletData = await updateWalletData(); 790 | const header = `${chalk.bold.cyan("Address")}`; 791 | const separator = chalk.gray("-".repeat(30)); 792 | try { 793 | walletBox.setItems([header, separator, ...walletData]); 794 | walletBox.select(2 + selectedWalletIndex); 795 | } catch (error) { 796 | addLog(`Wallet update error: ${error.message}`, "error"); 797 | } 798 | safeRender(); 799 | } 800 | 801 | function updateLogs() { 802 | try { 803 | logBox.add(transactionLogs[transactionLogs.length - 1] || chalk.gray("No logs available.")); 804 | safeRender(); 805 | } catch (error) { 806 | addLog(`Log update failed: ${error.message}`, "error"); 807 | } 808 | } 809 | 810 | function updateMenu() { 811 | const menuItems = isCycleRunning 812 | ? ["Stop Auto Upload", "Set Manual Config", "Clear Logs", "Refresh", "Exit"] 813 | : ["Start Auto Upload", "Set Manual Config", "Clear Logs", "Refresh", "Exit"]; 814 | try { 815 | menuBox.setItems(menuItems); 816 | menuBox.select(0); 817 | } catch (error) { 818 | addLog(`Menu update error: ${error.message}`, "error"); 819 | } 820 | safeRender(); 821 | } 822 | 823 | logBox.key(["up"], () => { 824 | if (screen.focused === logBox) { 825 | logBox.scroll(-1); 826 | safeRender(); 827 | } 828 | }); 829 | 830 | logBox.key(["down"], () => { 831 | if (screen.focused === logBox) { 832 | logBox.scroll(1); 833 | safeRender(); 834 | } 835 | }); 836 | 837 | logBox.on("click", () => { 838 | screen.focusPush(logBox); 839 | logBox.style.border.fg = "yellow"; 840 | menuBox.style.border.fg = "red"; 841 | uploadConfigSubMenu.style.border.fg = "blue"; 842 | safeRender(); 843 | }); 844 | 845 | logBox.on("blur", () => { 846 | logBox.style.border.fg = "magenta"; 847 | safeRender(); 848 | }); 849 | 850 | menuBox.on("select", async item => { 851 | const action = item.getText(); 852 | switch (action) { 853 | case "Start Auto Upload": 854 | if (isCycleRunning) { 855 | addLog("Cycle is still running. Stop the current cycle first.", "error"); 856 | } else { 857 | await autoUpload(); 858 | } 859 | break; 860 | case "Stop Auto Upload": 861 | shouldStop = true; 862 | addLog("Stopping auto upload... Please wait for ongoing processes to complete.", "info"); 863 | if (cycleTimeout) { 864 | clearTimeout(cycleTimeout); 865 | cycleTimeout = null; 866 | activityRunning = false; 867 | isCycleRunning = false; 868 | shouldStop = false; 869 | addLog("Auto upload stopped successfully.", "success"); 870 | updateMenu(); 871 | updateStatus(); 872 | safeRender(); 873 | } 874 | break; 875 | case "Set Manual Config": 876 | menuBox.hide(); 877 | uploadConfigSubMenu.show(); 878 | setTimeout(() => { 879 | if (uploadConfigSubMenu.visible) { 880 | screen.focusPush(uploadConfigSubMenu); 881 | uploadConfigSubMenu.style.border.fg = "yellow"; 882 | logBox.style.border.fg = "magenta"; 883 | safeRender(); 884 | } 885 | }, 100); 886 | break; 887 | case "Clear Logs": 888 | clearTransactionLogs(); 889 | break; 890 | case "Refresh": 891 | await updateWallets(); 892 | addLog("Data refreshed.", "success"); 893 | break; 894 | case "Exit": 895 | clearInterval(statusInterval); 896 | if (cycleTimeout) { 897 | clearTimeout(cycleTimeout); 898 | cycleTimeout = null; 899 | } 900 | process.exit(0); 901 | } 902 | safeRender(); 903 | }); 904 | 905 | uploadConfigSubMenu.on("select", (item) => { 906 | const action = item.getText(); 907 | switch (action) { 908 | case "Set Upload Count": 909 | configForm.configType = "uploadCount"; 910 | configForm.setLabel(" Enter Upload Count "); 911 | configLabel.setContent("Upload Count (1-100):"); 912 | configInput.setValue(uploadConfig.uploadCount.toString()); 913 | configForm.show(); 914 | setTimeout(() => { 915 | if (configForm.visible) { 916 | screen.focusPush(configInput); 917 | safeRender(); 918 | } 919 | }, 100); 920 | break; 921 | case "Back to Main Menu": 922 | uploadConfigSubMenu.hide(); 923 | menuBox.show(); 924 | setTimeout(() => { 925 | if (menuBox.visible) { 926 | screen.focusPush(menuBox); 927 | menuBox.style.border.fg = "cyan"; 928 | uploadConfigSubMenu.style.border.fg = "blue"; 929 | logBox.style.border.fg = "magenta"; 930 | safeRender(); 931 | } 932 | }, 100); 933 | break; 934 | } 935 | }); 936 | 937 | configForm.on("submit", () => { 938 | const inputValue = configInput.getValue().trim(); 939 | let value; 940 | try { 941 | value = parseInt(inputValue, 10); 942 | if (isNaN(value) || value < 1 || value > 100) { 943 | addLog("Invalid upload count. Please enter a number between 1 and 100.", "error"); 944 | configInput.setValue(""); 945 | screen.focusPush(configInput); 946 | safeRender(); 947 | return; 948 | } 949 | } catch (error) { 950 | addLog(`Invalid format: ${error.message}`, "error"); 951 | configInput.setValue(""); 952 | screen.focusPush(configInput); 953 | safeRender(); 954 | return; 955 | } 956 | 957 | if (configForm.configType === "uploadCount") { 958 | uploadConfig.uploadCount = value; 959 | addLog(`Upload Count set to ${uploadConfig.uploadCount}`, "success"); 960 | saveConfig(); 961 | updateStatus(); 962 | } 963 | 964 | configForm.hide(); 965 | uploadConfigSubMenu.show(); 966 | setTimeout(() => { 967 | if (uploadConfigSubMenu.visible) { 968 | screen.focusPush(uploadConfigSubMenu); 969 | uploadConfigSubMenu.style.border.fg = "yellow"; 970 | logBox.style.border.fg = "magenta"; 971 | safeRender(); 972 | } 973 | }, 100); 974 | }); 975 | 976 | configInput.on("submit", () => { 977 | configForm.submit(); 978 | }); 979 | 980 | configSubmitButton.on("press", () => { 981 | configForm.submit(); 982 | }); 983 | 984 | configSubmitButton.on("click", () => { 985 | configForm.submit(); 986 | }); 987 | 988 | configForm.key(["escape"], () => { 989 | configForm.hide(); 990 | uploadConfigSubMenu.show(); 991 | setTimeout(() => { 992 | if (uploadConfigSubMenu.visible) { 993 | screen.focusPush(uploadConfigSubMenu); 994 | uploadConfigSubMenu.style.border.fg = "yellow"; 995 | logBox.style.border.fg = "magenta"; 996 | safeRender(); 997 | } 998 | }, 100); 999 | }); 1000 | 1001 | uploadConfigSubMenu.key(["escape"], () => { 1002 | uploadConfigSubMenu.hide(); 1003 | menuBox.show(); 1004 | setTimeout(() => { 1005 | if (menuBox.visible) { 1006 | screen.focusPush(menuBox); 1007 | menuBox.style.border.fg = "cyan"; 1008 | uploadConfigSubMenu.style.border.fg = "blue"; 1009 | logBox.style.border.fg = "magenta"; 1010 | safeRender(); 1011 | } 1012 | }, 100); 1013 | }); 1014 | 1015 | const statusInterval = setInterval(() => { 1016 | updateStatus(); 1017 | safeRender(); 1018 | }, 100); 1019 | 1020 | screen.key(["escape", "q", "C-c"], () => { 1021 | addLog("Exiting application", "info"); 1022 | clearInterval(statusInterval); 1023 | if (cycleTimeout) { 1024 | clearTimeout(cycleTimeout); 1025 | cycleTimeout = null; 1026 | } 1027 | process.exit(0); 1028 | }); 1029 | 1030 | async function initialize() { 1031 | loadConfig(); 1032 | loadSeedPhrases(); 1033 | loadProxies(); 1034 | await updateWallets(); 1035 | updateStatus(); 1036 | updateLogs(); 1037 | updateMenu(); 1038 | adjustLayout(); 1039 | setTimeout(() => { 1040 | menuBox.show(); 1041 | menuBox.focus(); 1042 | menuBox.select(0); 1043 | screen.render(); 1044 | }, 100); 1045 | safeRender(); 1046 | } 1047 | 1048 | setTimeout(() => { 1049 | adjustLayout(); 1050 | screen.on("resize", adjustLayout); 1051 | }, 100); 1052 | 1053 | initialize(); --------------------------------------------------------------------------------