UNPKG

ethereumos

Version:

CLI for interacting with the Ethereum OS

525 lines (509 loc) 27.6 kB
#!/usr/bin/env node const { ethers } = require('ethers'); const yargs = require('yargs/yargs'); const { hideBin } = require('yargs/helpers'); const fs = require('fs'); const path = require('path'); const os = require('os'); const ora = require('ora').default; const axios = require('axios'); const chalk = require('chalk'); const Table = require('cli-table3'); const inquirer = require('inquirer'); const logSymbols = require('log-symbols'); const terminalLink = require('terminal-link'); // Banner ASCII const BANNER = ` ${chalk.magenta.bold(`┏━┳━━┳┓┏┓┏━┳━━┓ ┃┳┻┓┏┫┗┛┃┃┃┃━━┫ ┃┻┓┃┃┃┏┓┃┃┃┣━━┃ ┗━┛┗┛┗┛┗┛┗━┻━━┛`)} `; // Configuration file path const configDir = path.join(os.homedir(), '.ethos'); const configFile = path.join(configDir, 'config.json'); // --- KONSTANTA UNTUK UNISWAP --- const UNISWAP_ROUTER_ADDRESS = '0x7a250d5630B4cF539739dF2C5dAcb4c659F2488D'; const WETH_ADDRESS = '0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2'; const UNISWAP_ROUTER_ABI = [ {"inputs":[{"internalType":"uint256","name":"amountIn","type":"uint256"},{"internalType":"address[]","name":"path","type":"address[]"}],"name":"getAmountsOut","outputs":[{"internalType":"uint256[]","name":"amounts","type":"uint256[]"}],"stateMutability":"view","type":"function"}, {"inputs":[{"internalType":"uint256","name":"amountOutMin","type":"uint256"},{"internalType":"address[]","name":"path","type":"address[]"},{"internalType":"address","name":"to","type":"address"},{"internalType":"uint256","name":"deadline","type":"uint256"}],"name":"swapExactETHForTokensSupportingFeeOnTransferTokens","outputs":[],"stateMutability":"payable","type":"function"}, {"inputs":[{"internalType":"uint256","name":"amountIn","type":"uint256"},{"internalType":"uint256","name":"amountOutMin","type":"uint256"},{"internalType":"address[]","name":"path","type":"address[]"},{"internalType":"address","name":"to","type":"address"},{"internalType":"uint256","name":"deadline","type":"uint256"}],"name":"swapExactTokensForETHSupportingFeeOnTransferTokens","outputs":[],"stateMutability":"nonpayable","type":"function"} ]; // Default RPC endpoints const defaultRpcEndpoints = [ 'https://sparkling-black-pallet.quiknode.pro/a198397e9392091f1feb56498007b6cc283da705', 'https://eth.merkle.io', 'https://eth-mainnet.g.alchemy.com/v2/_MuNnwoMNfcQXBiEzCP-_Zqi1jce-nuf', 'https://mainnet.infura.io/v3/78ae05ac10914baca2abdd9702545967', ]; // Fungsi untuk menunggu input sebelum keluar async function pressAnyKeyToExit(statusCode = 0) { console.log(chalk.gray('\nPress any key to exit...')); process.stdin.setRawMode(true); process.stdin.resume(); return new Promise(resolve => process.stdin.once('data', () => { process.stdin.setRawMode(false); process.stdin.pause(); resolve(); })).then(() => process.exit(statusCode)); } // Load configuration function loadConfig() { try { if (fs.existsSync(configFile)) { return JSON.parse(fs.readFileSync(configFile, 'utf8')); } return null; } catch (error) { console.log(`${logSymbols.error} ${chalk.red(`Error reading config file: ${error.message}`)}`); return null; } } // Prompt user for configuration async function promptConfig() { console.log(chalk.blue('Please provide configuration for Ethos CLI')); console.log(`${chalk.yellow.bold(`${logSymbols.warning} WARNING:`)} ${chalk.yellow('Keep your private key secure and do not share it!\n')}`); if (!process.stdin.isTTY) { console.log(`${logSymbols.error} ${chalk.red('Interactive prompts not supported.')}`); process.exit(1); } const answers = await inquirer.prompt([ { type: 'password', name: 'privateKey', message: 'Enter your private key:', mask: '*', validate: (input) => { try { new ethers.Wallet(input); return true; } catch { return 'Invalid private key format.'; } } }, { type: 'list', name: 'selectedRpc', message: 'Select an RPC endpoint:', choices: defaultRpcEndpoints } ]); return { privateKey: answers.privateKey, rpcEndpoints: defaultRpcEndpoints, selectedRpc: answers.selectedRpc }; } // Get configuration async function getConfig() { let config = loadConfig(); if (!config || !config.privateKey || !config.selectedRpc) { console.log(`${logSymbols.info} ${chalk.yellow('Config missing or incomplete, prompting for configuration...')}`); config = await promptConfig(); const spinner = ora(chalk.blue('Saving configuration...')).start(); if (!fs.existsSync(configDir)) fs.mkdirSync(configDir, { recursive: true }); fs.writeFileSync(configFile, JSON.stringify(config, null, 2)); spinner.succeed(chalk.green('Configuration saved!')); console.log(`${chalk.yellow.bold(`${logSymbols.warning} WARNING:`)} ${chalk.yellow('Do not share your config file or private key!')}`); } return config; } // Prompt user to select an RPC async function promptRpcSelection(endpoints) { console.log(`${logSymbols.warning} ${chalk.yellow('RPC connection failed.')}`); const { selectedRpc } = await inquirer.prompt([{ type: 'list', name: 'selectedRpc', message: 'Please select another RPC:', choices: endpoints }]); return selectedRpc; } // Get provider with fallback async function getProvider(config) { let endpoints = config.rpcEndpoints || defaultRpcEndpoints; let selectedRpc = config.selectedRpc || endpoints[0]; let provider; const spinner = ora(chalk.blue(`Connecting to RPC: ${chalk.cyan(selectedRpc)}`)).start(); try { provider = new ethers.JsonRpcProvider(selectedRpc); await provider.getNetwork(); spinner.succeed(chalk.green(`Connected to network via ${chalk.cyan(selectedRpc)}`)); return provider; } catch (error) { spinner.fail(chalk.red(`Failed to connect to ${chalk.cyan(selectedRpc)}.`)); selectedRpc = await promptRpcSelection(endpoints); const spinnerRetry = ora(chalk.blue(`Retrying with ${chalk.cyan(selectedRpc)}`)).start(); try { provider = new ethers.JsonRpcProvider(selectedRpc); await provider.getNetwork(); spinnerRetry.succeed(chalk.green(`Connected to network via ${chalk.cyan(selectedRpc)}`)); config.selectedRpc = selectedRpc; fs.writeFileSync(configFile, JSON.stringify(config, null, 2)); return provider; } catch (retryError) { spinnerRetry.fail(chalk.red(`Failed to connect to ${selectedRpc}.`)); } } console.log(`${logSymbols.error} ${chalk.red('Could not connect to any RPC endpoint.')}`); process.exit(1); } // Get wallet async function getWallet() { const config = await getConfig(); const provider = await getProvider(config); try { const wallet = new ethers.Wallet(config.privateKey, provider); console.log(`${logSymbols.info} ${chalk.blue(`Using account: ${chalk.cyan(wallet.address)}`)}\n`); return { wallet, provider, config }; } catch (error) { console.log(`${logSymbols.error} ${chalk.red(`Invalid private key: ${error.message}`)}`); if (provider) provider.destroy(); process.exit(1); } } // Timeout function for transaction confirmation async function waitWithTimeout(promise, timeoutMs, errorMessage) { const timeout = new Promise((_, reject) => { setTimeout(() => reject(new Error(errorMessage)), timeoutMs); }); return Promise.race([promise, timeout]); } // Centralized function to handle transaction errors function handleTransactionError(error, spinner, tx) { let simpleMessage; switch (error.code) { case 'INSUFFICIENT_FUNDS': simpleMessage = 'Insufficient Funds. Your wallet does not have enough ETH for gas fees.'; break; case 'NONCE_EXPIRED': simpleMessage = 'Nonce has expired. Please try again.'; break; case 'REPLACEMENT_UNDERPRICED': simpleMessage = 'Replacement transaction underpriced.'; break; default: simpleMessage = error.reason || error.message; break; } if (spinner) spinner.fail(chalk.red(simpleMessage)); else console.log(`${logSymbols.error} ${chalk.red(simpleMessage)}`); if (tx && tx.hash) { const link = terminalLink('Check on Etherscan', `https://etherscan.io/tx/${tx.hash}`); console.log(chalk.yellow(`Debug: ${link}`)); } } // Configure contract const contractAddress = '0x8164B40840418C77A68F6f9EEdB5202b36d8b288'; const contractABI = [ {"inputs":[{"internalType":"address","name":"account","type":"address"}],"name":"balanceOf","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"}, {"inputs":[{"internalType":"address","name":"owner","type":"address"},{"internalType":"address","name":"spender","type":"address"}],"name":"allowance","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"}, {"inputs":[{"internalType":"address","name":"spender","type":"address"},{"internalType":"uint256","name":"amount","type":"uint256"}],"name":"approve","outputs":[{"internalType":"bool","name":"","type":"bool"}],"stateMutability":"nonpayable","type":"function"}, {"inputs":[],"name":"apy","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"}, {"inputs":[],"name":"additionalAPY","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"}, {"inputs":[],"name":"totalGM","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"}, {"inputs":[{"internalType":"address","name":"account","type":"address"}],"name":"sayGM","outputs":[],"stateMutability":"nonpayable","type":"function"}, {"inputs":[{"internalType":"address","name":"_user","type":"address"}],"name":"claimPonziTokens","outputs":[],"stateMutability":"nonpayable","type":"function"} ]; // Display banner and then run yargs console.log(BANNER); yargs(hideBin(process.argv)) .command( 'init', 'Initialize ethos configuration', () => {}, async () => { try { await getConfig(); await pressAnyKeyToExit(); } catch(error) { console.log(`${logSymbols.error} ${chalk.red(`Initialization failed: ${error.message}`)}`); await pressAnyKeyToExit(1); } } ) .command( 'gm', 'Say GM to Ethereum OS', () => {}, async () => { let spinner, tx = null, provider; try { const { wallet, provider: p } = await getWallet(); provider = p; spinner = ora(chalk.blue('Preparing to say GM...')).start(); const contract = new ethers.Contract(contractAddress, contractABI, wallet); const nonce = await provider.getTransactionCount(wallet.address, 'pending'); const feeData = await provider.getFeeData(); spinner.text = chalk.blue(`Using nonce: ${nonce}. Sending transaction...`); tx = await contract.sayGM(wallet.address, { nonce, gasLimit: 200000, maxFeePerGas: feeData.maxFeePerGas, maxPriorityFeePerGas: feeData.maxPriorityFeePerGas, }); spinner.succeed(`${logSymbols.success} ${chalk.green(`Transaction sent: ${chalk.cyan(tx.hash)}`)}`); const receiptSpinner = ora(chalk.blue('Waiting for confirmation...')).start(); await waitWithTimeout(tx.wait(), 120000, 'Transaction confirmation timed out'); receiptSpinner.succeed(`${logSymbols.success} ${chalk.green(`Transaction confirmed!`)}\n`); console.log(`${logSymbols.success} ${chalk.green.bold(`GM successfully said!`)}`); await pressAnyKeyToExit(); } catch (error) { handleTransactionError(error, spinner, tx); await pressAnyKeyToExit(1); } finally { if (provider) provider.destroy(); } } ) .command( 'claim', 'Claim $AIR tokens', () => {}, async () => { let spinner, tx = null, provider; try { const { wallet, provider: p } = await getWallet(); provider = p; spinner = ora(chalk.blue('Preparing to claim $AIR tokens...')).start(); const contract = new ethers.Contract(contractAddress, contractABI, wallet); const nonce = await provider.getTransactionCount(wallet.address, 'pending'); const feeData = await provider.getFeeData(); spinner.text = chalk.blue(`Using nonce: ${nonce}. Sending transaction...`); tx = await contract.claimPonziTokens(wallet.address, { nonce, gasLimit: 200000, maxFeePerGas: feeData.maxFeePerGas, maxPriorityFeePerGas: feeData.maxPriorityFeePerGas, }); spinner.succeed(`${logSymbols.success} ${chalk.green(`Transaction sent: ${chalk.cyan(tx.hash)}`)}`); const receiptSpinner = ora(chalk.blue('Waiting for confirmation...')).start(); await waitWithTimeout(tx.wait(), 120000, 'Transaction confirmation timed out'); receiptSpinner.succeed(`${logSymbols.success} ${chalk.green(`Transaction confirmed!`)}\n`); console.log(`${logSymbols.success} ${chalk.green.bold(`Claim transaction successful!`)}`); await pressAnyKeyToExit(); } catch (error) { handleTransactionError(error, spinner, tx); await pressAnyKeyToExit(1); } finally { if (provider) provider.destroy(); } } ) .command( 'buy <amount>', 'Buy $AIR token with ETH', (yargs) => { yargs .positional('amount', { describe: 'Amount of ETH to spend', type: 'string' }) .option('slippage', { describe: 'Slippage tolerance percentage', type: 'number', default: 1 }) .option('y', { describe: 'Skip confirmation', type: 'boolean', alias: 'yes' }); }, async (argv) => { let spinner, tx = null, provider; try { const { wallet, provider: p } = await getWallet(); provider = p; const routerContract = new ethers.Contract(UNISWAP_ROUTER_ADDRESS, UNISWAP_ROUTER_ABI, wallet); const amountInETH = ethers.parseEther(argv.amount); spinner = ora(chalk.blue('Estimating buy price...')).start(); const amountsOut = await routerContract.getAmountsOut(amountInETH, [WETH_ADDRESS, contractAddress]); const amountOutMin = amountsOut[1] - (amountsOut[1] * BigInt(Math.floor(argv.slippage * 100))) / 10000n; spinner.stop(); if (!argv.y) { const { confirm } = await inquirer.prompt([{ type: 'confirm', name: 'confirm', message: `You will spend ${chalk.yellow(argv.amount + ' ETH')} to buy at least ${chalk.cyan(ethers.formatEther(amountOutMin) + ' $AIR')}. Proceed?` }]); if (!confirm) { console.log(chalk.red('\nBuy cancelled.')); await pressAnyKeyToExit(); return; } } spinner.start(chalk.blue('Sending buy transaction...')); tx = await routerContract.swapExactETHForTokensSupportingFeeOnTransferTokens( amountOutMin, [WETH_ADDRESS, contractAddress], wallet.address, Math.floor(Date.now() / 1000) + 60 * 10, { value: amountInETH } ); spinner.text = chalk.blue('Waiting for confirmation...'); await tx.wait(); spinner.succeed(chalk.green.bold('Buy transaction successful!')); const link = terminalLink('View on Etherscan', `https://etherscan.io/tx/${tx.hash}`); console.log(link); await pressAnyKeyToExit(); } catch (error) { handleTransactionError(error, spinner, tx); await pressAnyKeyToExit(1); } finally { if (provider) provider.destroy(); } } ) .command( 'sell [amount]', 'Sell $AIR token for ETH', (yargs) => { yargs .positional('amount', { describe: 'Amount of $AIR to sell. If omitted, sells all.', type: 'string'}) .option('percent', { describe: 'Sell a percentage of your balance', type: 'number' }) .option('usd', { describe: 'Sell an equivalent of USD value', type: 'number' }) .option('slippage', { describe: 'Slippage tolerance percentage', type: 'number', default: 1 }) .option('y', { describe: 'Skip confirmation', type: 'boolean', alias: 'yes' }) .conflicts('amount', ['percent', 'usd']) .conflicts('percent', ['amount', 'usd']) .conflicts('usd', ['amount', 'percent']); }, async (argv) => { let spinner, sellTx = null, provider; try { const { wallet, provider: p } = await getWallet(); provider = p; spinner = ora(chalk.blue('Preparing sell transaction...')).start(); const routerContract = new ethers.Contract(UNISWAP_ROUTER_ADDRESS, UNISWAP_ROUTER_ABI, wallet); const airContract = new ethers.Contract(contractAddress, contractABI, wallet); const userBalance = await airContract.balanceOf(wallet.address); let amountToSell; if (argv.amount) { amountToSell = ethers.parseEther(argv.amount); } else if (argv.percent) { amountToSell = (userBalance * BigInt(argv.percent)) / 100n; } else if (argv.usd) { spinner.text = chalk.blue('Fetching token price for USD conversion...'); const moralisApiKey = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJub25jZSI6ImU3MGVkMWZlLTU3ZGUtNDJiMi1iZDNmLTJkZTIxZGE2NjE2MiIsIm9yZ0lkIjoiNDYzMzE5IiwidXNlcklkIjoiNDc2NjYxIiwidHlwZUlkIjoiODA3NmM4YTctZmM3NS00NmJiLWE4ZjEtOWEwOWQ5ZjBjZTBmIiwidHlwZSI6IlBST0pFQ1QiLCJpYXQiOjE3NTQzNjM2NDAsImV4cCI6NDkxMDEyMzY0MH0.BWMs4y8osukciuEa8lBKrM5fJpd4d8FlZQpklz0zm5w'; const priceApiUrl = `https://deep-index.moralis.io/api/v2.2/erc20/${contractAddress}/price?chain=eth`; const priceResponse = await axios.get(priceApiUrl, { headers: { 'accept': 'application/json', 'X-API-Key': moralisApiKey } }); const pricePerAir = priceResponse.data.usdPrice; if (!pricePerAir || pricePerAir === 0) { spinner.fail(chalk.red('Could not fetch a valid token price.')); await pressAnyKeyToExit(1); return; } const airAmountForUsd = argv.usd / pricePerAir; amountToSell = ethers.parseEther(airAmountForUsd.toFixed(18)); } else { amountToSell = userBalance; } if (amountToSell > userBalance) { spinner.fail(chalk.red('Amount to sell exceeds your balance.')); await pressAnyKeyToExit(1); return; } if (amountToSell === 0n) { spinner.fail(chalk.red('Amount to sell is zero.')); await pressAnyKeyToExit(1); return; } spinner.text = chalk.blue('Estimating sell price...'); const amountsOut = await routerContract.getAmountsOut(amountToSell, [contractAddress, WETH_ADDRESS]); const amountOutMin = amountsOut[1] - (amountsOut[1] * BigInt(Math.floor(argv.slippage * 100))) / 10000n; spinner.stop(); if (!argv.y) { const { confirm } = await inquirer.prompt([{ type: 'confirm', name: 'confirm', message: `You will sell ${chalk.yellow(ethers.formatEther(amountToSell) + ' $AIR')} for at least ${chalk.cyan(ethers.formatEther(amountOutMin) + ' ETH')}. Proceed?` }]); if (!confirm) { console.log(chalk.red('\nSell cancelled.')); await pressAnyKeyToExit(); return; } } spinner.start(chalk.blue('Checking allowance...')); const allowance = await airContract.allowance(wallet.address, UNISWAP_ROUTER_ADDRESS); if (allowance < amountToSell) { spinner.text = chalk.blue('Sending approve transaction...'); const approveTx = await airContract.approve(UNISWAP_ROUTER_ADDRESS, ethers.MaxUint256); spinner.text = chalk.blue('Waiting for approval confirmation...'); await approveTx.wait(); spinner.succeed(chalk.green('Approval successful!')); } else { spinner.succeed(chalk.green('Sufficient allowance found.')); } spinner.start(chalk.blue('Sending sell transaction...')); sellTx = await routerContract.swapExactTokensForETHSupportingFeeOnTransferTokens( amountToSell, amountOutMin, [contractAddress, WETH_ADDRESS], wallet.address, Math.floor(Date.now() / 1000) + 60 * 10 ); spinner.text = chalk.blue('Waiting for confirmation...'); await sellTx.wait(); spinner.succeed(chalk.green.bold('Sell transaction successful!')); const link = terminalLink('View on Etherscan', `https://etherscan.io/tx/${sellTx.hash}`); console.log(link); await pressAnyKeyToExit(); } catch (error) { handleTransactionError(error, spinner, sellTx); await pressAnyKeyToExit(1); } finally { if (provider) provider.destroy(); } } ) .command( 'rpc', 'Manage RPC endpoints', (yargs) => { yargs .option('select', { describe: 'Select an RPC by index', type: 'number' }) .option('add', { describe: 'Add a custom RPC URL', type: 'string' }); }, async (argv) => { let config = loadConfig() || {}; if (!config.rpcEndpoints) config.rpcEndpoints = [...defaultRpcEndpoints]; if (!config.selectedRpc) config.selectedRpc = defaultRpcEndpoints[0]; try { if (argv.select !== undefined) { if (argv.select < 0 || argv.select >= config.rpcEndpoints.length) { console.log(`${logSymbols.error} ${chalk.red(`Invalid index. Choose between 0 and ${config.rpcEndpoints.length - 1}.`)}`); process.exit(1); } config.selectedRpc = config.rpcEndpoints[argv.select]; fs.writeFileSync(configFile, JSON.stringify(config, null, 2)); console.log(`${logSymbols.success} ${chalk.green(`Selected RPC: ${chalk.cyan(config.selectedRpc)}`)}`); } else if (argv.add) { if (!config.rpcEndpoints.includes(argv.add)) { config.rpcEndpoints.push(argv.add); config.selectedRpc = argv.add; fs.writeFileSync(configFile, JSON.stringify(config, null, 2)); console.log(`${logSymbols.success} ${chalk.green(`Added and selected custom RPC: ${chalk.cyan(argv.add)}`)}`); } else { console.log(`${logSymbols.warning} ${chalk.yellow('RPC already exists.')}`); } } else { const table = new Table({ head: [chalk.cyan.bold('INDEX'), chalk.cyan.bold('STATUS'), chalk.cyan.bold('RPC URL')], colWidths: [8, 11, 45] }); config.rpcEndpoints.forEach((url, index) => { const status = url === config.selectedRpc ? chalk.green('✔ Active') : ''; table.push([chalk.yellow(index), status, url]); }); console.log(table.toString()); console.log(chalk.blue(`\n${logSymbols.info} To select an RPC, use: ${chalk.yellow('ethos rpc --select <INDEX>')}`)); console.log(chalk.blue(`${logSymbols.info} To add a new RPC, use: ${chalk.yellow('ethos rpc --add <URL>')}`)); } } catch (error) { console.log(`${logSymbols.error} ${chalk.red(`Error managing RPC: ${error.message}`)}`); } await pressAnyKeyToExit(); } ) .command( 'stats', 'Display Ethereum OS stats', () => {}, async () => { let spinner, provider; try { const { wallet, provider: p } = await getWallet(); provider = p; spinner = ora(chalk.blue('Fetching Ethereum OS stats...')).start(); const contract = new ethers.Contract(contractAddress, contractABI, provider); const moralisApiKey = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJub25jZSI6ImU3MGVkMWZlLTU3ZGUtNDJiMi1iZDNmLTJkZTIxZGE2NjE2MiIsIm9yZ0lkIjoiNDYzMzE5IiwidXNlcklkIjoiNDc2NjYxIiwidHlwZUlkIjoiODA3NmM4YTctZmM3NS00NmJiLWE4ZjEtOWEwOWQ5ZjBjZTBmIiwidHlwZSI6IlBST0pFQ1QiLCJpYXQiOjE3NTQzNjM2NDAsImV4cCI6NDkxMDEyMzY0MH0.BWMs4y8osukciuEa8lBKrM5fJpd4d8FlZQpklz0zm5w'; const priceApiUrl = `https://deep-index.moralis.io/api/v2.2/erc20/${contractAddress}/price?chain=eth`; const [totalGM, baseApy, additionalApy, balance, priceResponse] = await Promise.all([ contract.totalGM(), contract.apy(), contract.additionalAPY(), contract.balanceOf(wallet.address), axios.get(priceApiUrl, { headers: { 'accept': 'application/json', 'X-API-Key': moralisApiKey } }) ]); const priceData = priceResponse.data; spinner.succeed(chalk.green("Stats fetched successfully")); const balanceInEther = parseFloat(ethers.formatUnits(balance, 18)); const balanceInUsd = balanceInEther * priceData.usdPrice; const table = new Table({ head: [chalk.cyan.bold('STATS'), chalk.cyan.bold('VALUE')], colWidths: [22, 36], chars: { 'top': '═' , 'top-mid': '╤' , 'top-left': '╔' , 'top-right': '╗', 'bottom': '═' , 'bottom-mid': '╧' , 'bottom-left': '╚' , 'bottom-right': '╝', 'left': '║' , 'left-mid': '╟' , 'mid': '─' , 'mid-mid': '┼', 'right': '║' , 'right-mid': '╢' , 'middle': '│' } }); table.push( { [chalk.blue('Total GM')]: chalk.yellow(totalGM.toString()) }, { [chalk.blue('Current APY')]: chalk.yellow(`${(Number(baseApy) + Number(additionalApy)) / 100}%/day`) }, { [chalk.blue('Your Balance')]: chalk.yellow(`${balanceInEther.toFixed(4)} $AIR`) }, { [chalk.blue('Balance Value')]: chalk.green(`$${balanceInUsd.toFixed(2)} USD`) }, { [chalk.blue('$AIR Price')]: chalk.yellow(`${Number(priceData.usdPrice).toFixed(4)} USD on ${priceData.exchangeName}`) } ); console.log(`\n${table.toString()}\n`); await pressAnyKeyToExit(); } catch (error) { if(spinner) spinner.fail(chalk.red(`Error fetching stats: ${error.reason || error.message}\n`)); else console.log(`${logSymbols.error} ${chalk.red(`Error fetching stats: ${error.reason || error.message}\n`)}`); await pressAnyKeyToExit(1); } finally { if (provider) provider.destroy(); } } ) .command( 'config', 'Displays the current configuration.', () => {}, async () => { console.log(`${logSymbols.info} ${chalk.blue('Current configuration path:')} ${chalk.cyan(configFile)}`); const config = loadConfig(); if (config) { const displayConfig = { ...config, privateKey: '********' }; console.log(chalk.green(JSON.stringify(displayConfig, null, 2))); } else { console.log(`${logSymbols.warning} ${chalk.yellow('No configuration file found. Run "ethos init" to create one.')}`); } await pressAnyKeyToExit(); } ) .demandCommand(1, chalk.red.bold('Please provide a command. Use --help to see available commands.')) .help() .alias('h', 'help') .wrap(null) .parse();