ethereumos
Version:
CLI for interacting with the Ethereum OS
525 lines (509 loc) • 27.6 kB
JavaScript
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();