diff --git a/bin/index.ts b/bin/index.ts index f240d42..00a026a 100644 --- a/bin/index.ts +++ b/bin/index.ts @@ -9,11 +9,12 @@ import chalk from "chalk"; import { deployCommand } from "../src/commands/deploy.js"; import { verifyCommand } from "../src/commands/verify.js"; import { ReadContract } from "../src/commands/contract.js"; +import { Address } from "viem"; import { bridgeCommand } from "../src/commands/bridge.js"; - interface CommandOptions { testnet?: boolean; - address?: string; + address?: Address; + contract?: Address; value?: string; txid?: string; abi?: string; @@ -56,8 +57,9 @@ program .command("balance") .description("Check the balance of the saved wallet") .option("-t, --testnet", "Check the balance on the testnet") + .option("-a ,--address
", "Token holder address") .action(async (options: CommandOptions) => { - await balanceCommand(!!options.testnet); + await balanceCommand(!!options.testnet, options.address); }); program diff --git a/package.json b/package.json index 2108ddb..657e88d 100644 --- a/package.json +++ b/package.json @@ -9,6 +9,7 @@ }, "scripts": { "build": "tsc", + "dev": "tsc -w", "wallet": "pnpm run build && node dist/bin/index.js wallet", "balance": "pnpm run build && node dist/bin/index.js balance", "transfer": "pnpm run build && node dist/bin/index.js transfer --testnet --address 0xa5f45f5bddefC810C48aCC1D5CdA5e5a4c6BC59E --value 0.001", diff --git a/src/commands/balance.ts b/src/commands/balance.ts index ab6717d..cb5edd9 100644 --- a/src/commands/balance.ts +++ b/src/commands/balance.ts @@ -1,54 +1,124 @@ import ViemProvider from "../utils/viemProvider.js"; -import fs from "fs"; import chalk from "chalk"; -import { walletFilePath } from "../utils/constants.js"; +import inquirer from "inquirer"; +import { + getTokenInfo, + isERC20Contract, + resolveTokenAddress, +} from "../utils/tokenHelper.js"; +import ora from "ora"; +import { + getAddress, + isValidContract, + validateAndFormatAddress, +} from "../utils/index.js"; +import { Address, formatUnits } from "viem"; +import { TOKENS } from "../constants/tokenAdress.js"; + +export async function balanceCommand( + testnet: boolean, + holderAddress?: Address +) { + const spinner = ora(); -export async function balanceCommand(testnet: boolean) { try { - if (!fs.existsSync(walletFilePath)) { - console.log( - chalk.red("🚫 No saved wallet found. Please create a wallet first.") - ); + const targetAddress = getAddress(holderAddress); + + if (!targetAddress) { return; } + const provider = new ViemProvider(testnet); + const client = await provider.getPublicClient(); - const walletsData = JSON.parse(fs.readFileSync(walletFilePath, "utf8")); + const { token } = await inquirer.prompt({ + type: "list", + name: "token", + message: "Select token to check balance:", + choices: ["rBTC", ...Object.keys(TOKENS), "Custom Token"], + }); - if (!walletsData.currentWallet || !walletsData.wallets) { + if (token === "rBTC") { + spinner.start(chalk.white("🔍 Checking balance...")); + const balance = await client.getBalance({ address: targetAddress }); + const rbtcBalance = formatUnits(balance, 18); + + spinner.succeed(chalk.green("Balance retrieved successfully")); + + console.log( + chalk.white(`📄 Wallet Address:`), + chalk.green(targetAddress) + ); + console.log( + chalk.white(`🌐 Network:`), + chalk.green(testnet ? "Rootstock Testnet" : "Rootstock Mainnet") + ); console.log( - chalk.red( - "⚠️ No valid wallet found. Please create or import a wallet first." + chalk.white(`💰 Current Balance:`), + chalk.green(`${rbtcBalance} RBTC`) + ); + console.log( + chalk.blue( + `🔗 Ensure that transactions are being conducted on the correct network.` ) ); - throw new Error(); + return; } - const { currentWallet, wallets } = walletsData; + let tokenAddress: Address; - const wallet = wallets[currentWallet]; - const { address } = wallet; - - if (!address) { - console.log(chalk.red("⚠️ No valid address found in the saved wallet.")); - return; + if (token === "Custom Token") { + spinner.stop(); + const { address } = await inquirer.prompt({ + type: "input", + name: "address", + message: "Enter the token address:", + validate: async (input: string) => { + try { + const address = input as Address; + const formattedContractAddress = validateAndFormatAddress(address); + if (!formattedContractAddress) { + console.log(chalk.red()); + return "🚫 Invalid contract address"; + } + if (!(await isValidContract(client, formattedContractAddress))) { + return "🚫 Invalid contract address or contract not found"; + } + if (!(await isERC20Contract(client, formattedContractAddress))) { + return "🚫 Invalid contract address, only ERC20 tokens are supported"; + } + return true; + } catch { + return false; + } + }, + }); + tokenAddress = address.toLowerCase() as Address; + } else { + tokenAddress = resolveTokenAddress(token, testnet); } - const provider = new ViemProvider(testnet); - const client = await provider.getPublicClient(); + spinner.start(chalk.white("🔍 Checking balance...")); - const balance = await client.getBalance({ address }); + const { balance, decimals, name, symbol } = await getTokenInfo( + client, + tokenAddress, + targetAddress + ); + const formattedBalance = formatUnits(balance, decimals); - const rbtcBalance = Number(balance) / 10 ** 18; + spinner.succeed(chalk.green("Balance retrieved successfully")); - console.log(chalk.white(`📄 Wallet Address:`), chalk.green(address)); console.log( - chalk.white(`🌐 Network:`), - chalk.green(testnet ? "Rootstock Testnet" : "Rootstock Mainnet") - ); - console.log( - chalk.white(`💰 Current Balance:`), - chalk.green(`${rbtcBalance} RBTC`) + chalk.white(`📄 Token Information: + Name: ${chalk.green(name)} + Contract: ${chalk.green(tokenAddress)} + 👤 Holder Address: ${chalk.green(targetAddress)} + 💰 Balance: ${chalk.green(`${formattedBalance} ${symbol}`)} + 🌐 Network: ${chalk.green( + testnet ? "Rootstock Testnet" : "Rootstock Mainnet" + )}`) ); + console.log( chalk.blue( `🔗 Ensure that transactions are being conducted on the correct network.` @@ -63,5 +133,7 @@ export async function balanceCommand(testnet: boolean) { } else { console.error(chalk.red("🚨 An unknown error occurred.")); } + } finally { + spinner.stop(); } } diff --git a/src/constants/tokenAdress.ts b/src/constants/tokenAdress.ts new file mode 100644 index 0000000..d3d803d --- /dev/null +++ b/src/constants/tokenAdress.ts @@ -0,0 +1,16 @@ +import { Address } from "viem"; + +export const TOKENS: Record> = { + RIF: { + mainnet: "0x2acc95758f8b5F583470ba265eb685a8f45fc9d5", + testnet: "0x19f64674d8a5b4e652319f5e239efd3bc969a1fe", + }, + USDRIF: { + mainnet: "0x3A15461d8ae0f0fb5fa2629e9da7D66a794a6e37", + testnet: "0xd1b0d1bc03491f49b9aea967ddd07b37f7327e63", + }, + DoC: { + mainnet: "0xe700691da7B9851f2f35f8b8182c69c53ccad9db", + testnet: "0xd37a3e5874be2dc6c732ad21c008a1e4032a6040", + }, +}; diff --git a/src/utils/index.ts b/src/utils/index.ts index 9485d60..0265808 100644 --- a/src/utils/index.ts +++ b/src/utils/index.ts @@ -1,14 +1,61 @@ +import { Address, isAddress, PublicClient } from "viem"; +import chalk from "chalk"; +import fs from "fs"; import { ALLOWED_BRIDGE_METHODS, METHOD_TYPES, walletFilePath, } from "./constants.js"; -import fs from "fs"; export function wait(ms: number): Promise { return new Promise((resolve) => setTimeout(resolve, ms)); } +export function validateAndFormatAddress(address: string): Address | undefined { + if (!address) return undefined; + + const formattedAddress = address.toLowerCase(); + if (!isAddress(formattedAddress)) { + console.log(chalk.red("🚫 Invalid address")); + return undefined; + } + return formattedAddress as Address; +} + +export async function isValidContract( + client: PublicClient, + address: Address +): Promise { + try { + const code = await client.getBytecode({ address }); + return code !== undefined && code !== "0x"; + } catch (error) { + return false; + } +} + +export function getAddress(address?: Address): Address | undefined { + if (address) { + return validateAndFormatAddress(address); + } + + if (!fs.existsSync(walletFilePath)) { + console.log(chalk.red("🚫 No saved wallet found")); + return undefined; + } + + try { + const { currentWallet, wallets } = JSON.parse( + fs.readFileSync(walletFilePath, "utf8") + ); + const savedAddress = wallets[currentWallet].address; + return validateAndFormatAddress(savedAddress); + } catch (error) { + console.log(chalk.red("⚠️ Invalid wallet data")); + return undefined; + } +} + export function loadWallets(): string { if (fs.existsSync(walletFilePath)) { const walletsData = fs.readFileSync(walletFilePath, "utf8"); diff --git a/src/utils/tokenHelper.ts b/src/utils/tokenHelper.ts new file mode 100644 index 0000000..429e729 --- /dev/null +++ b/src/utils/tokenHelper.ts @@ -0,0 +1,101 @@ +import { Address, encodeFunctionData, PublicClient, erc20Abi } from "viem"; +import { TOKENS } from "../constants/tokenAdress"; + +export function resolveTokenAddress(token: string, testnet: boolean): Address { + return TOKENS[token][ + testnet ? "testnet" : "mainnet" + ].toLowerCase() as Address; +} +export async function getTokenInfo( + client: PublicClient, + tokenAddress: Address, + holderAddress: Address +): Promise<{ + balance: bigint; + decimals: number; + name: string; + symbol: string; +}> { + const [balance, decimals, name, symbol] = await Promise.all([ + client.readContract({ + address: tokenAddress, + abi: erc20Abi, + functionName: "balanceOf", + args: [holderAddress], + }), + client.readContract({ + address: tokenAddress, + abi: erc20Abi, + functionName: "decimals", + }), + client.readContract({ + address: tokenAddress, + abi: erc20Abi, + functionName: "name", + }) as Promise, + client.readContract({ + address: tokenAddress, + abi: erc20Abi, + functionName: "symbol", + }) as Promise, + ]); + + return { + balance: balance as bigint, + decimals: decimals as number, + name: name, + symbol: symbol, + }; +} + +export async function isERC20Contract( + client: PublicClient, + address: Address +): Promise { + try { + const checks = await Promise.all([ + client + .call({ + to: address, + data: encodeFunctionData({ + abi: erc20Abi, + functionName: "totalSupply", + }), + }) + .then(() => true) + .catch(() => false), + client + .call({ + to: address, + data: encodeFunctionData({ + abi: erc20Abi, + functionName: "decimals", + }), + }) + .then(() => true) + .catch(() => false), + + client + .call({ + to: address, + data: encodeFunctionData({ + abi: erc20Abi, + functionName: "symbol", + }), + }) + .then(() => true) + .catch(() => false), + ]); + + const isERC20 = checks.every((check) => check === true); + + if (!isERC20) { + return false; + } + + return true; + } catch (error) { + console.error("Error checking ERC20 contract:", error); + return false; + } +}