diff --git a/README.md b/README.md index 9a7838c..600a8fe 100644 --- a/README.md +++ b/README.md @@ -147,6 +147,14 @@ Use the `-t` or `--testnet` flag to check the balance on the Rootstock testnet. rsk-cli balance -t ``` +#### Wallet + +Use the `--wallet` flag to dynamically select wallet + +```bash +rsk-cli balance --wallet +``` + Output example: ``` @@ -171,6 +179,14 @@ Use the `-t` or `--testnet` flag to execute the transfer on the Rootstock testne rsk-cli transfer --testnet --address 0x0x08C4E4BdAb2473E454B8B2a4400358792786d341 --value 0.001 ``` +#### Wallet + +Use the `--wallet` flag to dynamically select wallet + +```bash +rsk-cli transfer --wallet --address 0x0x08C4E4BdAb2473E454B8B2a4400358792786d341 --value 0.001 +``` + Output example: ``` @@ -227,6 +243,12 @@ rsk-cli deploy --abi --bytecode --args < rsk-cli deploy --testnet --abi --bytecode --args ... ``` +#### Wallet + +```bash +rsk-cli deploy --wallet --abi --bytecode --args ... +``` + Output example: ``` @@ -331,6 +353,12 @@ rsk-cli bridge rsk-cli bridge --testnet ``` +#### Wallet + +```bash +rsk-cli bridge --wallet +``` + Output example: ``` @@ -342,6 +370,52 @@ Output example: šŸ”— View on Explorer: https://explorer.testnet.rootstock.io/address/0x0000000000000000000000000000000001000006 ``` +### 9. Fetch Wallet History + +The history command allows you to fetch the transaction history for a wallet on the Rootstock blockchain. This includes transactions such as ERC20, ERC721, and external transfers. You can specify whether to fetch the history from the Mainnet or Testnet by providing the appropriate flag + +#### Mainnet + +With arguments: + +```bash +rsk-cli history --apiKey --number +``` + +Without arguments: + +```bash +rsk-cli history +``` + +#### Testnet + +With arguments: + +```bash +rsk-cli history --testnet --apiKey --number +``` + +Without arguments + +```bash +rsk-cli history --testnet +``` + +Output example: + +``` +? šŸ”’ Enter Alchemy API key to fetch history: ******************************** +šŸ” Fetching transaction history on Rootstack Testnet for 0x19661D036D4e590948b9c00eef3807b88fBfA8e1 ... +āœ… Transfer: + From: 0x19661d036d4e590948b9c00eef3807b88fbfa8e1 + To: 0xb45805aead9407f5c7860ff8eccaedd4d0ab36a6 + Token: ETH + Value: 0.000003 + Tx Hash: 0xde678614cd9e20fe5891c25069afef680174456b104f31c9078eb486abd95a64 + +``` + ## Contributing We welcome contributions from the community. Please fork the repository and submit pull requests with your changes. Ensure your code adheres to the project's main objective. diff --git a/bin/index.ts b/bin/index.ts index f240d42..a22f9b8 100644 --- a/bin/index.ts +++ b/bin/index.ts @@ -9,19 +9,25 @@ 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"; +import { historyCommand } from "../src/commands/history.js"; interface CommandOptions { testnet?: boolean; - address?: string; + address?: Address; + contract?: Address; value?: string; txid?: string; abi?: string; bytecode?: string; + apiKey?: string; args?: any; json?: any; name?: string; decodedArgs?: any; + wallet?: string; + number?: string; } const orange = chalk.rgb(255, 165, 0); @@ -56,14 +62,17 @@ program .command("balance") .description("Check the balance of the saved wallet") .option("-t, --testnet", "Check the balance on the testnet") + .option("--wallet ", "Name of the wallet") + .option("-a ,--address
", "Token holder address") .action(async (options: CommandOptions) => { - await balanceCommand(!!options.testnet); + await balanceCommand(!!options.testnet, options.wallet!, options.address); }); program .command("transfer") .description("Transfer rBTC to the provided address") .option("-t, --testnet", "Transfer on the testnet") + .option("--wallet ", "Name of the wallet") .requiredOption("-a, --address
", "Recipient address") .requiredOption("-v, --value ", "Amount to transfer in rBTC") .action(async (options: CommandOptions) => { @@ -75,7 +84,8 @@ program await transferCommand( !!options.testnet, address, - parseFloat(options.value!) + parseFloat(options.value!), + options.wallet! ); } catch (error) { console.error(chalk.red("Error during transfer:"), error); @@ -100,6 +110,7 @@ program .description("Deploy a contract") .requiredOption("--abi ", "Path to the ABI file") .requiredOption("--bytecode ", "Path to the bytecode file") + .option("--wallet ", "Name of the wallet") .option("--args ", "Constructor arguments (space-separated)") .option("-t, --testnet", "Deploy on the testnet") .action(async (options: CommandOptions) => { @@ -108,7 +119,8 @@ program options.abi!, options.bytecode!, !!options.testnet, - args + args, + options.wallet! ); }); @@ -147,8 +159,19 @@ program .command("bridge") .description("Interact with RSK bridge") .option("-t, --testnet", "Deploy on the testnet") + .option("--wallet ", "Name of the wallet") + .action(async (options: CommandOptions) => { + await bridgeCommand(!!options.testnet, options.wallet!); + }); + +program + .command("history") + .description("Fetch history for current wallet") + .option("--apiKey ", "Number of transactions to fetch") + .option("-t, --testnet", "History of wallet on the testnet") .action(async (options: CommandOptions) => { - await bridgeCommand(!!options.testnet); + await historyCommand(!!options.testnet, options.apiKey!, options.number!); }); program.parse(process.argv); diff --git a/package-lock.json b/package-lock.json index 551065f..191ecab 100644 --- a/package-lock.json +++ b/package-lock.json @@ -25,7 +25,7 @@ "rsk-cli": "dist/bin/index.js" }, "devDependencies": { - "@types/bun": "*", + "@types/bun": "latest", "@types/figlet": "^1.5.8", "solc": "0.8.28", "typescript": "^5.0.0" diff --git a/package.json b/package.json index 2108ddb..2c638da 100644 --- a/package.json +++ b/package.json @@ -9,10 +9,12 @@ }, "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", - "tx-status": "pnpm run build && node dist/bin/index.js tx --testnet --txid 0x876a0a9b167889350c41930a4204e5d9acf5704a7f201447a337094189af961c4" + "tx-status": "pnpm run build && node dist/bin/index.js tx --testnet --txid 0x876a0a9b167889350c41930a4204e5d9acf5704a7f201447a337094189af961c4", + "history": "pnpm run build && node dist/bin/index.js history" }, "keywords": [ "rootstock", diff --git a/src/commands/balance.ts b/src/commands/balance.ts index ab6717d..84e553f 100644 --- a/src/commands/balance.ts +++ b/src/commands/balance.ts @@ -1,17 +1,30 @@ import ViemProvider from "../utils/viemProvider.js"; -import fs from "fs"; import chalk from "chalk"; +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"; +import fs from "fs"; import { walletFilePath } from "../utils/constants.js"; -export async function balanceCommand(testnet: boolean) { - try { - if (!fs.existsSync(walletFilePath)) { - console.log( - chalk.red("šŸš« No saved wallet found. Please create a wallet first.") - ); - return; - } +export async function balanceCommand( + testnet: boolean, + walletName: string, + holderAddress?: Address +) { + const spinner = ora(); + try { const walletsData = JSON.parse(fs.readFileSync(walletFilePath, "utf8")); if (!walletsData.currentWallet || !walletsData.wallets) { @@ -24,8 +37,20 @@ export async function balanceCommand(testnet: boolean) { } const { currentWallet, wallets } = walletsData; + let wallet = wallets[currentWallet]; + + if (walletName) { + if (!wallets[walletName]) { + console.log( + chalk.red("āš ļø Wallet with the provided name does not exist.") + ); + + return; + } else { + wallet = wallets[walletName]; + } + } - const wallet = wallets[currentWallet]; const { address } = wallet; if (!address) { @@ -33,22 +58,104 @@ export async function balanceCommand(testnet: boolean) { return; } + const targetAddress = getAddress(address); + + if (!targetAddress) { + return; + } + const provider = new ViemProvider(testnet); const client = await provider.getPublicClient(); - const balance = await client.getBalance({ address }); + const { token } = await inquirer.prompt({ + type: "list", + name: "token", + message: "Select token to check balance:", + choices: ["rBTC", ...Object.keys(TOKENS), "Custom Token"], + }); - const rbtcBalance = Number(balance) / 10 ** 18; + if (token === "rBTC") { + spinner.start(chalk.white("šŸ” Checking balance...")); + const balance = await client.getBalance({ address: targetAddress }); + const rbtcBalance = formatUnits(balance, 18); - console.log(chalk.white(`šŸ“„ Wallet Address:`), chalk.green(address)); - console.log( - chalk.white(`šŸŒ Network:`), - chalk.green(testnet ? "Rootstock Testnet" : "Rootstock Mainnet") + 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.white(`šŸ’° Current Balance:`), + chalk.green(`${rbtcBalance} RBTC`) + ); + console.log( + chalk.blue( + `šŸ”— Ensure that transactions are being conducted on the correct network.` + ) + ); + return; + } + + let tokenAddress: Address; + + 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); + } + + spinner.start(chalk.white("šŸ” Checking balance...")); + + const { balance, decimals, name, symbol } = await getTokenInfo( + client, + tokenAddress, + targetAddress ); + const formattedBalance = formatUnits(balance, decimals); + + spinner.succeed(chalk.green("Balance retrieved successfully")); + 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 +170,7 @@ export async function balanceCommand(testnet: boolean) { } else { console.error(chalk.red("šŸšØ An unknown error occurred.")); } + } finally { + spinner.stop(); } } diff --git a/src/commands/bridge.ts b/src/commands/bridge.ts index 7a1771b..d3edc63 100644 --- a/src/commands/bridge.ts +++ b/src/commands/bridge.ts @@ -12,7 +12,7 @@ type InquirerAnswers = { args?: string[]; }; -export async function bridgeCommand(testnet: boolean) { +export async function bridgeCommand(testnet: boolean, name: string) { try { const spinner = ora(); console.log( @@ -100,7 +100,7 @@ export async function bridgeCommand(testnet: boolean) { } if (selectedType === "write") { - const walletClient = await provider.getWalletClient(); + const walletClient = await provider.getWalletClient(name); const account = walletClient.account; console.log(chalk.blue(`šŸ”‘ Wallet account: ${account?.address}`)); diff --git a/src/commands/deploy.ts b/src/commands/deploy.ts index 89db662..c08045c 100644 --- a/src/commands/deploy.ts +++ b/src/commands/deploy.ts @@ -7,7 +7,8 @@ export async function deployCommand( abiPath: string, bytecodePath: string, testnet: boolean, - args: any[] = [] + args: any[] = [], + name: string ): Promise { try { console.log( @@ -16,7 +17,7 @@ export async function deployCommand( ) ); const provider = new ViemProvider(testnet); - const walletClient = await provider.getWalletClient(); + const walletClient = await provider.getWalletClient(name); if (!walletClient.account) { console.error( diff --git a/src/commands/history.ts b/src/commands/history.ts new file mode 100644 index 0000000..4d66a1c --- /dev/null +++ b/src/commands/history.ts @@ -0,0 +1,155 @@ +import chalk from "chalk"; +import fs from "fs"; +import { walletFilePath } from "../utils/constants.js"; + +export async function historyCommand( + testnet: boolean, + apiKey: string, + number: string +) { + try { + // Check if API key exists in storage or passed as argument + let storedApiKey = getApiKeyFromStorage(); + + if (apiKey && !storedApiKey) { + await writeApiKey(apiKey); + } + + if (!apiKey && !storedApiKey) { + console.log(chalk.red("šŸ”‘ Alchemy API key is missing.")); + return; + } + + const finalApiKey = apiKey || storedApiKey; + + if (!fs.existsSync(walletFilePath)) { + console.log( + chalk.red("šŸš« No saved wallet found. Please create a wallet first.") + ); + return; + } + + const walletsData = JSON.parse(fs.readFileSync(walletFilePath, "utf8")); + + if (!walletsData.currentWallet || !walletsData.wallets) { + console.log( + chalk.red( + "āš ļø No valid wallet found. Please create or import a wallet first." + ) + ); + throw new Error(); + } + + const { currentWallet, wallets } = walletsData; + const wallet = wallets[currentWallet]; + const { address: walletAddress } = wallet; + + console.log( + chalk.blue( + `šŸ” Fetching transaction history on Rootstack ${ + testnet ? "Testnet" : "Mainnet" + } for ${walletAddress} ... ` + ) + ); + + const data = JSON.stringify({ + jsonrpc: "2.0", + id: 0, + method: "alchemy_getAssetTransfers", + params: [ + { + fromBlock: "0x0", + fromAddress: walletAddress, + category: ["external", "erc20", "erc721", "erc1155"], + withMetadata: true, + }, + ], + }); + + const testnetUrl = `https://rootstock-testnet.g.alchemy.com/v2/${finalApiKey}`; + const mainnetUrl = `https://rootstock-mainnet.g.alchemy.com/v2/${finalApiKey}`; + const baseURL = testnet ? testnetUrl : mainnetUrl; + + const response = await fetch(baseURL, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: data, + }); + + if (!response.ok) { + console.error( + chalk.red(`āŒ API request failed with status: ${response.status}`) + ); + return; + } + + const result = await response.json(); + + // Handle Alchemy-specific errors + if (result.error) { + console.error( + chalk.red(`āŒ Error from Alchemy: ${result.error.message}`) + ); + return; + } + + let transfers = result.result?.transfers; + + if (!transfers || transfers.length === 0) { + console.log(chalk.yellow("āš ļø No transactions found.")); + return; + } + + if (number) { + transfers = transfers.slice(0, parseInt(number)); + } + for (const transfer of transfers) { + console.log(chalk.green(`āœ… Transfer:`)); + console.log(` From: ${transfer.from}`); + console.log(` To: ${transfer.to}`); + console.log(` Token: ${transfer.asset || "N/A"}`); + console.log(` Value: ${transfer.value || "N/A"}`); + console.log(` Tx Hash: ${transfer.hash}`); + console.log(` Time: ${new Date(transfer.metadata.blockTimestamp)}`); + } + } catch (error: any) { + console.error( + chalk.red(`šŸšØ An unknown error occurred: ${error.message || error}`) + ); + } +} + +async function writeApiKey(apiKey: string) { + try { + // Read the existing wallet file + const walletsData = JSON.parse(fs.readFileSync(walletFilePath, "utf8")); + + // Add or update the alchemyApiKey + walletsData.alchemyApiKey = apiKey; + + // Write the updated JSON back to the file + fs.writeFileSync(walletFilePath, JSON.stringify(walletsData, null, 2)); + + console.log(chalk.green(`āœ… Alchemy API key updated successfully.`)); + } catch (error: any) { + console.error( + chalk.red("āŒ Error updating Alchemy API key:"), + chalk.yellow(error.message || error) + ); + } +} + +function getApiKeyFromStorage(): string | undefined { + try { + if (fs.existsSync(walletFilePath)) { + const configData = JSON.parse(fs.readFileSync(walletFilePath, "utf8")); + return configData.alchemyApiKey; + } + return undefined; + } catch (error: any) { + console.error( + chalk.red("āŒ Error reading alchemy API key:"), + chalk.yellow(error.message || error) + ); + } +} diff --git a/src/commands/transfer.ts b/src/commands/transfer.ts index 106a2cf..81b0442 100644 --- a/src/commands/transfer.ts +++ b/src/commands/transfer.ts @@ -8,7 +8,8 @@ import { walletFilePath } from "../utils/constants.js"; export async function transferCommand( testnet: boolean, toAddress: Address, - value: number + value: number, + name?: string ) { try { if (!fs.existsSync(walletFilePath)) { @@ -31,7 +32,19 @@ export async function transferCommand( const { currentWallet, wallets } = walletsData; - const wallet = wallets[currentWallet]; + let wallet = wallets[currentWallet]; + + if (name) { + if (!wallets[name]) { + console.log( + chalk.red("āš ļø Wallet with the provided name does not exist.") + ); + + throw new Error(); + } else { + wallet = wallets[name]; + } + } const { address: walletAddress } = wallet; if (!walletAddress) { @@ -64,7 +77,7 @@ export async function transferCommand( return; } - const walletClient = await provider.getWalletClient(); + const walletClient = await provider.getWalletClient(name); const account = walletClient.account; if (!account) { diff --git a/src/commands/wallet.ts b/src/commands/wallet.ts index d5336db..4c44c10 100644 --- a/src/commands/wallet.ts +++ b/src/commands/wallet.ts @@ -5,6 +5,7 @@ import fs from "fs-extra"; import crypto from "crypto"; import { loadWallets } from "../utils/index.js"; import { walletFilePath } from "../utils/constants.js"; +import path from "path"; type InquirerAnswers = { action?: string; @@ -16,6 +17,7 @@ type InquirerAnswers = { setCurrentWallet?: boolean; confirmDelete?: boolean; newWalletName?: string; + backupPath?: string; }; export async function walletCommand() { @@ -47,6 +49,7 @@ export async function walletCommand() { "šŸ” List saved wallets", "šŸ” Switch wallet", "šŸ“ Update wallet name", + "šŸ“‚ Backup wallet data", "āŒ Delete wallet", ], }, @@ -481,6 +484,27 @@ export async function walletCommand() { writeWalletData(walletFilePath, walletsData); } + + if (action === "šŸ“‚ Backup wallet data") { + const backupPathQuestion: any = [ + { + type: "input", + name: "backupPath", + message: "šŸ’¾ Enter the path where you want to save the backup:", + }, + ]; + + const { backupPath } = await inquirer.prompt( + backupPathQuestion + ); + + if (!backupPath) { + console.log(chalk.red("āš ļø Backup path is required!")); + return; + } + + await backupCommand(backupPath); + } } catch (error: any) { console.error( chalk.red("āŒ Error creating or managing wallets:"), @@ -489,14 +513,10 @@ export async function walletCommand() { } } -async function writeWalletData(walletFilePath: string, walletsData: any) { +async function writeWalletData(filePath: string, data: any) { try { - fs.writeFileSync( - walletFilePath, - JSON.stringify(walletsData, null, 2), - "utf8" - ); - console.log(chalk.green(`šŸ’¾ Changes saved at ${walletFilePath}`)); + fs.writeFileSync(filePath, JSON.stringify(data, null, 2), "utf8"); + console.log(chalk.green(`šŸ’¾ Changes saved at ${filePath}`)); } catch (error: any) { console.error( chalk.red("āŒ Error saving wallet data:"), @@ -504,3 +524,43 @@ async function writeWalletData(walletFilePath: string, walletsData: any) { ); } } + +async function backupCommand(backupPath: string) { + try { + if (!fs.existsSync(walletFilePath)) { + console.log(chalk.red("šŸš« No saved wallet found. Please create a wallet first.")); + return; + } + + if (!backupPath) { + console.log(chalk.red("āš ļø Please provide a valid file path for backup.")); + return; + } + + let absoluteBackupPath = path.resolve(backupPath); + const backupDir = path.dirname(absoluteBackupPath); + + if (fs.existsSync(absoluteBackupPath) && fs.lstatSync(absoluteBackupPath).isDirectory()) { + absoluteBackupPath = path.join(absoluteBackupPath, 'wallet_backup.json'); + console.log(chalk.yellow(`āš ļø Provided a directory. Using default file name: wallet_backup.json`)); + } + + if (!fs.existsSync(backupDir)) { + fs.mkdirSync(backupDir, { recursive: true }); + console.log(chalk.green(`šŸ“‚ Created backup directory: ${backupDir}`)); + } + + const walletData = JSON.parse(fs.readFileSync(walletFilePath, "utf8")); + + writeWalletData(absoluteBackupPath, walletData); + console.log( + chalk.green("āœ… Wallet backup created successfully!"), + chalk.green(`\nšŸ’¾ Backup saved successfully at: ${absoluteBackupPath}`) + ); + } catch (error: any) { + console.error( + chalk.red("šŸšØ Error during wallet backup:"), + chalk.yellow(error.message) + ); + } +} \ No newline at end of file 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..2bffe18 --- /dev/null +++ b/src/utils/tokenHelper.ts @@ -0,0 +1,101 @@ +import { Address, encodeFunctionData, PublicClient, erc20Abi } from "viem"; +import { TOKENS } from "../constants/tokenAdress.js"; + +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; + } +} diff --git a/src/utils/viemProvider.ts b/src/utils/viemProvider.ts index caff679..d342ad7 100644 --- a/src/utils/viemProvider.ts +++ b/src/utils/viemProvider.ts @@ -27,8 +27,10 @@ class ViemProvider { }); } - public async getWalletClient(): Promise { - const { account } = await this.decryptPrivateKey(); + public async getWalletClient( + name: string | undefined + ): Promise { + const { account } = await this.decryptPrivateKey(name ? name : undefined); return createWalletClient({ chain: this.chain, @@ -37,7 +39,7 @@ class ViemProvider { }); } - private async decryptPrivateKey(): Promise<{ + private async decryptPrivateKey(name: string | undefined): Promise<{ account: ReturnType; }> { if (!fs.existsSync(walletFilePath)) { @@ -58,6 +60,19 @@ class ViemProvider { } const { currentWallet, wallets } = walletsData; + let wallet = wallets[currentWallet]; + + if (name) { + if (!wallets[name]) { + console.log( + chalk.red("āš ļø Wallet with the provided name does not exist.") + ); + + throw new Error(); + } else { + wallet = wallets[name]; + } + } const passwordQuestion: any = [ { @@ -70,7 +85,6 @@ class ViemProvider { const { password } = await inquirer.prompt(passwordQuestion); - const wallet = wallets[currentWallet]; const { encryptedPrivateKey, iv } = wallet; try {