From 9055c526b3c55a1c4a76781a7d68863850fe5c70 Mon Sep 17 00:00:00 2001 From: Chandra Bose Date: Tue, 3 Dec 2024 20:39:23 +0530 Subject: [PATCH 1/3] feat: batch transfer --- bin/index.ts | 16 ++++++ package.json | 3 +- src/commands/batchTransfer.ts | 99 +++++++++++++++++++++++++++++++++++ 3 files changed, 117 insertions(+), 1 deletion(-) create mode 100644 src/commands/batchTransfer.ts diff --git a/bin/index.ts b/bin/index.ts index f240d42..1e6f337 100644 --- a/bin/index.ts +++ b/bin/index.ts @@ -10,6 +10,7 @@ import { deployCommand } from "../src/commands/deploy.js"; import { verifyCommand } from "../src/commands/verify.js"; import { ReadContract } from "../src/commands/contract.js"; import { bridgeCommand } from "../src/commands/bridge.js"; +import { batchTransferCommand } from "../src/commands/batchTransfer.js"; interface CommandOptions { testnet?: boolean; @@ -22,6 +23,7 @@ interface CommandOptions { json?: any; name?: string; decodedArgs?: any; + file?: string; } const orange = chalk.rgb(255, 165, 0); @@ -130,6 +132,7 @@ program options.address!, options.name!, !!options.testnet, + args ); }); @@ -151,4 +154,17 @@ program await bridgeCommand(!!options.testnet); }); +program + .command("batch-transfer") + .description("Execute batch transactions from a file") + .requiredOption("-f, --file ", "Path to the batch file") + .option("-t, --testnet", "Execute on the testnet") + .action(async (options: CommandOptions) => { + try { + await batchTransferCommand(options.file!, !!options.testnet); + } catch (error) { + console.error(chalk.red("Error during batch processing:"), error); + } + }); + program.parse(process.argv); diff --git a/package.json b/package.json index f16f391..4ad22cf 100644 --- a/package.json +++ b/package.json @@ -12,7 +12,8 @@ "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", + "batch-transfer": "pnpm run build && node dist/bin/index.js batch-transfer --testnet --file" }, "keywords": [ "rootstock", diff --git a/src/commands/batchTransfer.ts b/src/commands/batchTransfer.ts new file mode 100644 index 0000000..c48059e --- /dev/null +++ b/src/commands/batchTransfer.ts @@ -0,0 +1,99 @@ +import fs from "fs"; +import chalk from "chalk"; +import ora from "ora"; +import { walletFilePath } from "../utils/constants.js"; +import ViemProvider from "../utils/viemProvider.js"; + +export async function batchTransferCommand(filePath: string, testnet: boolean) { + try { + if (!fs.existsSync(filePath)) { + console.log(chalk.red("🚫 Batch file not found. Please provide a valid file.")); + return; + } + + const batchData = JSON.parse(fs.readFileSync(filePath, "utf8")); + + if (!Array.isArray(batchData) || batchData.length === 0) { + console.log(chalk.red("⚠️ Invalid batch data. Please check the file content.")); + return; + } + + 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; + + if (!walletAddress) { + console.log(chalk.red("⚠️ No valid address found in the saved wallet.")); + return; + } + + const provider = new ViemProvider(testnet); + const publicClient = await provider.getPublicClient(); + const balance = await publicClient.getBalance({ address: walletAddress }); + + const rbtcBalance = Number(balance) / 10 ** 18; + console.log(chalk.white(`📄 Wallet Address:`), chalk.green(walletAddress)); + console.log(chalk.white(`💰 Current Balance:`), chalk.green(`${rbtcBalance} RBTC`)); + + for (const transfer of batchData) { + const { to, value } = transfer; + if (!to || !value) { + console.log(chalk.red("⚠️ Invalid transaction data in batch. Skipping...")); + continue; + } + + if (rbtcBalance < value) { + console.log(chalk.red(`🚫 Insufficient balance to transfer ${value} RBTC.`)); + break; + } + + const walletClient = await provider.getWalletClient(); + const account = walletClient.account; + if (!account) { + console.log(chalk.red("⚠️ Failed to retrieve the account. Skipping this transaction.")); + continue; + } + + const txHash = await walletClient.sendTransaction({ + account: account, + chain: provider.chain, + to: to, + value: BigInt(Math.floor(value * 10 ** 18)), + }); + + console.log(chalk.white(`🔄 Transaction initiated. TxHash:`), chalk.green(txHash)); + + const spinner = ora("⏳ Waiting for confirmation...").start(); + + const receipt = await publicClient.waitForTransactionReceipt({ hash: txHash }); + spinner.stop(); + + if (receipt.status === "success") { + console.log(chalk.green("✅ Transaction confirmed successfully!")); + console.log(chalk.white(`📦 Block Number:`), chalk.green(receipt.blockNumber)); + console.log(chalk.white(`⛽ Gas Used:`), chalk.green(receipt.gasUsed.toString())); + + const explorerUrl = testnet + ? `https://explorer.testnet.rootstock.io/tx/${txHash}` + : `https://explorer.rootstock.io/tx/${txHash}`; + console.log(chalk.white(`🔗 View on Explorer:`), chalk.dim(`${explorerUrl}`)); + } else { + console.log(chalk.red("❌ Transaction failed.")); + } + } + } catch (error: any) { + console.error(chalk.red("🚨 Error during batch transfer:"), chalk.yellow(error.message || "Unknown error")); + } +} From 575f6e818473b44414f9b53bcf17692ec64c999b Mon Sep 17 00:00:00 2001 From: Chandra Bose Date: Wed, 15 Jan 2025 23:19:37 +0530 Subject: [PATCH 2/3] chore: fix ux --- bin/index.ts | 265 ++++++++++++++++++---------------- package.json | 102 ++++++------- src/commands/batchTransfer.ts | 258 +++++++++++++++++++++------------ 3 files changed, 355 insertions(+), 270 deletions(-) diff --git a/bin/index.ts b/bin/index.ts index 1e6f337..bbc38f7 100644 --- a/bin/index.ts +++ b/bin/index.ts @@ -13,158 +13,173 @@ import { bridgeCommand } from "../src/commands/bridge.js"; import { batchTransferCommand } from "../src/commands/batchTransfer.js"; interface CommandOptions { - testnet?: boolean; - address?: string; - value?: string; - txid?: string; - abi?: string; - bytecode?: string; - args?: any; - json?: any; - name?: string; - decodedArgs?: any; - file?: string; + testnet?: boolean; + address?: string; + value?: string; + txid?: string; + abi?: string; + bytecode?: string; + args?: any; + json?: any; + name?: string; + decodedArgs?: any; + file?: string; + interactive?: boolean; } const orange = chalk.rgb(255, 165, 0); console.log( - orange( - figlet.textSync("Rootstock", { - font: "3D-ASCII", - horizontalLayout: "fitted", - verticalLayout: "fitted", - }) - ) + orange( + figlet.textSync("Rootstock", { + font: "3D-ASCII", + horizontalLayout: "fitted", + verticalLayout: "fitted", + }) + ) ); const program = new Command(); program - .name("rsk-cli") - .description("CLI tool for interacting with Rootstock blockchain") - .version("1.0.4", "-v, --version", "Display the current version"); + .name("rsk-cli") + .description("CLI tool for interacting with Rootstock blockchain") + .version("1.0.4", "-v, --version", "Display the current version"); program - .command("wallet") - .description( - "Manage your wallet: create a new one, use an existing wallet, or import a custom wallet" - ) - .action(async () => { - await walletCommand(); - }); + .command("wallet") + .description( + "Manage your wallet: create a new one, use an existing wallet, or import a custom wallet" + ) + .action(async () => { + await walletCommand(); + }); program - .command("balance") - .description("Check the balance of the saved wallet") - .option("-t, --testnet", "Check the balance on the testnet") - .action(async (options: CommandOptions) => { - await balanceCommand(!!options.testnet); - }); + .command("balance") + .description("Check the balance of the saved wallet") + .option("-t, --testnet", "Check the balance on the testnet") + .action(async (options: CommandOptions) => { + await balanceCommand(!!options.testnet); + }); program - .command("transfer") - .description("Transfer rBTC to the provided address") - .option("-t, --testnet", "Transfer on the testnet") - .requiredOption("-a, --address
", "Recipient address") - .requiredOption("-v, --value ", "Amount to transfer in rBTC") - .action(async (options: CommandOptions) => { - try { - const address = `0x${options.address!.replace( - /^0x/, - "" - )}` as `0x${string}`; - await transferCommand( - !!options.testnet, - address, - parseFloat(options.value!) - ); - } catch (error) { - console.error(chalk.red("Error during transfer:"), error); - } - }); + .command("transfer") + .description("Transfer rBTC to the provided address") + .option("-t, --testnet", "Transfer on the testnet") + .requiredOption("-a, --address
", "Recipient address") + .requiredOption("-v, --value ", "Amount to transfer in rBTC") + .action(async (options: CommandOptions) => { + try { + const address = `0x${options.address!.replace( + /^0x/, + "" + )}` as `0x${string}`; + await transferCommand( + !!options.testnet, + address, + parseFloat(options.value!) + ); + } catch (error) { + console.error(chalk.red("Error during transfer:"), error); + } + }); program - .command("tx") - .description("Check the status of a transaction") - .option("-t, --testnet", "Check the transaction status on the testnet") - .requiredOption("-i, --txid ", "Transaction ID") - .action(async (options: CommandOptions) => { - const formattedTxId = options.txid!.startsWith("0x") - ? options.txid - : `0x${options.txid}`; - - await txCommand(!!options.testnet, formattedTxId as `0x${string}`); - }); + .command("tx") + .description("Check the status of a transaction") + .option("-t, --testnet", "Check the transaction status on the testnet") + .requiredOption("-i, --txid ", "Transaction ID") + .action(async (options: CommandOptions) => { + const formattedTxId = options.txid!.startsWith("0x") + ? options.txid + : `0x${options.txid}`; + + await txCommand(!!options.testnet, formattedTxId as `0x${string}`); + }); program - .command("deploy") - .description("Deploy a contract") - .requiredOption("--abi ", "Path to the ABI file") - .requiredOption("--bytecode ", "Path to the bytecode file") - .option("--args ", "Constructor arguments (space-separated)") - .option("-t, --testnet", "Deploy on the testnet") - .action(async (options: CommandOptions) => { - const args = options.args || []; - await deployCommand( - options.abi!, - options.bytecode!, - !!options.testnet, - args - ); - }); + .command("deploy") + .description("Deploy a contract") + .requiredOption("--abi ", "Path to the ABI file") + .requiredOption("--bytecode ", "Path to the bytecode file") + .option("--args ", "Constructor arguments (space-separated)") + .option("-t, --testnet", "Deploy on the testnet") + .action(async (options: CommandOptions) => { + const args = options.args || []; + await deployCommand( + options.abi!, + options.bytecode!, + !!options.testnet, + args + ); + }); program - .command("verify") - .description("Verify a contract") - .requiredOption("--json ", "Path to the JSON Standard Input") - .requiredOption("--name ", "Name of the contract") - .requiredOption("-a, --address
", "Address of the deployed contract") - .option("-t, --testnet", "Deploy on the testnet") - .option( - "-da, --decodedArgs ", - "Decoded Constructor arguments (space-separated)" - ) - .action(async (options: CommandOptions) => { - const args = options.decodedArgs || []; - await verifyCommand( - options.json!, - options.address!, - options.name!, - !!options.testnet, - - args - ); - }); + .command("verify") + .description("Verify a contract") + .requiredOption("--json ", "Path to the JSON Standard Input") + .requiredOption("--name ", "Name of the contract") + .requiredOption( + "-a, --address
", + "Address of the deployed contract" + ) + .option("-t, --testnet", "Deploy on the testnet") + .option( + "-da, --decodedArgs ", + "Decoded Constructor arguments (space-separated)" + ) + .action(async (options: CommandOptions) => { + const args = options.decodedArgs || []; + await verifyCommand( + options.json!, + options.address!, + options.name!, + !!options.testnet, + + args + ); + }); program - .command("contract") - .description("Interact with a contract") - .requiredOption("-a, --address
", "Address of a verified contract") - .option("-t, --testnet", "Deploy on the testnet") - .action(async (options: CommandOptions) => { - await ReadContract(options.address! as `0x${string}`, !!options.testnet); - }); + .command("contract") + .description("Interact with a contract") + .requiredOption("-a, --address
", "Address of a verified contract") + .option("-t, --testnet", "Deploy on the testnet") + .action(async (options: CommandOptions) => { + await ReadContract( + options.address! as `0x${string}`, + !!options.testnet + ); + }); program - .command("bridge") - .description("Interact with RSK bridge") - .option("-t, --testnet", "Deploy on the testnet") - .action(async (options: CommandOptions) => { - await bridgeCommand(!!options.testnet); - }); + .command("bridge") + .description("Interact with RSK bridge") + .option("-t, --testnet", "Deploy on the testnet") + .action(async (options: CommandOptions) => { + await bridgeCommand(!!options.testnet); + }); program - .command("batch-transfer") - .description("Execute batch transactions from a file") - .requiredOption("-f, --file ", "Path to the batch file") - .option("-t, --testnet", "Execute on the testnet") - .action(async (options: CommandOptions) => { - try { - await batchTransferCommand(options.file!, !!options.testnet); - } catch (error) { - console.error(chalk.red("Error during batch processing:"), error); - } - }); + .command("batch-transfer") + .description("Execute batch transactions interactively or from stdin") + .option("-i, --interactive", "Execute interactively and input transactions") + .option("-t, --testnet", "Execute on the testnet") + .action(async (options: CommandOptions) => { + try { + const interactive = !!options.interactive; + const testnet = !!options.testnet; + + await batchTransferCommand(testnet, interactive); + } catch (error) { + console.error( + console.error( + chalk.red("Error during batch processing:"), + error + ) + ); + } + }); program.parse(process.argv); diff --git a/package.json b/package.json index 4ad22cf..7983753 100644 --- a/package.json +++ b/package.json @@ -1,53 +1,53 @@ { - "name": "@rsksmart/rsk-cli", - "version": "1.0.5", - "description": "CLI tool for Rootstock network using Viem", - "repository": "git@github.com:rsksmart/rsk-cli.git", - "main": "./dist/bin/index.js", - "bin": { - "rsk-cli": "./dist/bin/index.js" - }, - "scripts": { - "build": "tsc", - "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", - "batch-transfer": "pnpm run build && node dist/bin/index.js batch-transfer --testnet --file" - }, - "keywords": [ - "rootstock", - "blockchain", - "cli", - "ethereum", - "viem" - ], - "author": "rookiecol", - "license": "MIT", - "type": "module", - "files": [ - "dist/" - ], - "devDependencies": { - "@types/bun": "latest", - "@types/figlet": "^1.5.8", - "solc": "0.8.20", - "typescript": "^5.0.0" - }, - "peerDependencies": { - "typescript": "^5.0.0" - }, - "dependencies": { - "@openzeppelin/contracts": "^5.0.2", - "@rsksmart/rsk-precompiled-abis": "^6.0.0-ARROWHEAD", - "@types/fs-extra": "^11.0.4", - "chalk": "^5.3.0", - "cli-table3": "^0.6.5", - "commander": "^12.1.0", - "figlet": "^1.7.0", - "fs-extra": "^11.2.0", - "inquirer": "^10.1.8", - "ora": "^8.0.1", - "viem": "^2.19.4" - } + "name": "@rsksmart/rsk-cli", + "version": "1.0.5", + "description": "CLI tool for Rootstock network using Viem", + "repository": "git@github.com:rsksmart/rsk-cli.git", + "main": "./dist/bin/index.js", + "bin": { + "rsk-cli": "./dist/bin/index.js" + }, + "scripts": { + "build": "tsc", + "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", + "batch-transfer": "pnpm run build && node dist/bin/index.js batch-transfer --testnet" + }, + "keywords": [ + "rootstock", + "blockchain", + "cli", + "ethereum", + "viem" + ], + "author": "rookiecol", + "license": "MIT", + "type": "module", + "files": [ + "dist/" + ], + "devDependencies": { + "@types/bun": "latest", + "@types/figlet": "^1.5.8", + "solc": "0.8.20", + "typescript": "^5.0.0" + }, + "peerDependencies": { + "typescript": "^5.0.0" + }, + "dependencies": { + "@openzeppelin/contracts": "^5.0.2", + "@rsksmart/rsk-precompiled-abis": "^6.0.0-ARROWHEAD", + "@types/fs-extra": "^11.0.4", + "chalk": "^5.3.0", + "cli-table3": "^0.6.5", + "commander": "^12.1.0", + "figlet": "^1.7.0", + "fs-extra": "^11.2.0", + "inquirer": "^10.1.8", + "ora": "^8.0.1", + "viem": "^2.19.4" + } } diff --git a/src/commands/batchTransfer.ts b/src/commands/batchTransfer.ts index c48059e..3c5ca95 100644 --- a/src/commands/batchTransfer.ts +++ b/src/commands/batchTransfer.ts @@ -1,99 +1,169 @@ -import fs from "fs"; import chalk from "chalk"; import ora from "ora"; -import { walletFilePath } from "../utils/constants.js"; +import readline from "readline"; import ViemProvider from "../utils/viemProvider.js"; +import { Address } from "viem"; + +export async function batchTransferCommand( + testnet: boolean = false, + interactive: boolean = false +) { + try { + let batchData: { to: Address; value: number }[] = []; + + if (interactive) { + batchData = await promptForTransactions(); + } else { + const stdin = await readStdin(); + if (stdin) { + const jsonData = JSON.parse(stdin); + if (!Array.isArray(jsonData.transactions)) { + console.log( + chalk.red("🚫 Invalid JSON format for transactions.") + ); + return; + } + batchData = jsonData.transactions.map((tx: any) => ({ + to: validateAddress(tx.address), + value: tx.amount, + })); + } + } + + if (batchData.length === 0) { + console.log(chalk.red("⚠️ No transactions provided. Exiting...")); + return; + } + + const provider = new ViemProvider(testnet); + const walletClient = await provider.getWalletClient(); + const account = walletClient.account; + + if (!account) { + console.log( + chalk.red("🚫 Failed to retrieve wallet account. Exiting...") + ); + return; + } + + const publicClient = await provider.getPublicClient(); + const balance = await publicClient.getBalance({ + address: account.address, + }); + const rbtcBalance = Number(balance) / 10 ** 18; + + console.log( + chalk.white(`📄 Wallet Address:`), + chalk.green(account.address) + ); + console.log( + chalk.white(`💰 Current Balance:`), + chalk.green(`${rbtcBalance} RBTC`) + ); + + for (const { to, value } of batchData) { + if (rbtcBalance < value) { + console.log( + chalk.red( + `🚫 Insufficient balance to transfer ${value} RBTC.` + ) + ); + break; + } + + const txHash = await walletClient.sendTransaction({ + account, + chain: provider.chain, + to, + value: BigInt(Math.floor(value * 10 ** 18)), + }); + + console.log( + chalk.white(`🔄 Transaction initiated. TxHash:`), + chalk.green(txHash) + ); + + const spinner = ora("⏳ Waiting for confirmation...").start(); + + const receipt = await publicClient.waitForTransactionReceipt({ + hash: txHash, + }); + spinner.stop(); + + if (receipt.status === "success") { + console.log( + chalk.green("✅ Transaction confirmed successfully!") + ); + console.log( + chalk.white(`📦 Block Number:`), + chalk.green(receipt.blockNumber) + ); + console.log( + chalk.white(`⛽ Gas Used:`), + chalk.green(receipt.gasUsed.toString()) + ); + } else { + console.log(chalk.red("❌ Transaction failed.")); + } + } + } catch (error: any) { + console.error( + chalk.red("🚨 Error during batch transfer:"), + chalk.yellow(error.message || "Unknown error") + ); + } +} + +async function promptForTransactions() { + const rl = readline.createInterface({ + input: process.stdin, + output: process.stdout, + }); + + const transactions: { to: Address; value: number }[] = []; + + while (true) { + const to = validateAddress(await askQuestion(rl, "Enter address: ")); + const value = parseFloat(await askQuestion(rl, "Enter amount: ")); + + if (isNaN(value)) { + console.log(chalk.red("⚠️ Invalid amount. Please try again.")); + continue; + } + + transactions.push({ to, value }); + + const addAnother = await askQuestion( + rl, + "Add another transaction? (y/n): " + ); + if (addAnother.toLowerCase() !== "y") break; + } + + rl.close(); + return transactions; +} + +function validateAddress(address: string): Address { + if (!/^0x[a-fA-F0-9]{40}$/.test(address)) { + throw new Error(`Invalid Ethereum address: ${address}`); + } + return address as Address; +} + +async function readStdin(): Promise { + return new Promise((resolve, reject) => { + let data = ""; + process.stdin.setEncoding("utf8"); + process.stdin.on("data", (chunk) => (data += chunk)); + process.stdin.on("end", () => resolve(data)); + process.stdin.on("error", reject); + }); +} -export async function batchTransferCommand(filePath: string, testnet: boolean) { - try { - if (!fs.existsSync(filePath)) { - console.log(chalk.red("🚫 Batch file not found. Please provide a valid file.")); - return; - } - - const batchData = JSON.parse(fs.readFileSync(filePath, "utf8")); - - if (!Array.isArray(batchData) || batchData.length === 0) { - console.log(chalk.red("⚠️ Invalid batch data. Please check the file content.")); - return; - } - - 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; - - if (!walletAddress) { - console.log(chalk.red("⚠️ No valid address found in the saved wallet.")); - return; - } - - const provider = new ViemProvider(testnet); - const publicClient = await provider.getPublicClient(); - const balance = await publicClient.getBalance({ address: walletAddress }); - - const rbtcBalance = Number(balance) / 10 ** 18; - console.log(chalk.white(`📄 Wallet Address:`), chalk.green(walletAddress)); - console.log(chalk.white(`💰 Current Balance:`), chalk.green(`${rbtcBalance} RBTC`)); - - for (const transfer of batchData) { - const { to, value } = transfer; - if (!to || !value) { - console.log(chalk.red("⚠️ Invalid transaction data in batch. Skipping...")); - continue; - } - - if (rbtcBalance < value) { - console.log(chalk.red(`🚫 Insufficient balance to transfer ${value} RBTC.`)); - break; - } - - const walletClient = await provider.getWalletClient(); - const account = walletClient.account; - if (!account) { - console.log(chalk.red("⚠️ Failed to retrieve the account. Skipping this transaction.")); - continue; - } - - const txHash = await walletClient.sendTransaction({ - account: account, - chain: provider.chain, - to: to, - value: BigInt(Math.floor(value * 10 ** 18)), - }); - - console.log(chalk.white(`🔄 Transaction initiated. TxHash:`), chalk.green(txHash)); - - const spinner = ora("⏳ Waiting for confirmation...").start(); - - const receipt = await publicClient.waitForTransactionReceipt({ hash: txHash }); - spinner.stop(); - - if (receipt.status === "success") { - console.log(chalk.green("✅ Transaction confirmed successfully!")); - console.log(chalk.white(`📦 Block Number:`), chalk.green(receipt.blockNumber)); - console.log(chalk.white(`⛽ Gas Used:`), chalk.green(receipt.gasUsed.toString())); - - const explorerUrl = testnet - ? `https://explorer.testnet.rootstock.io/tx/${txHash}` - : `https://explorer.rootstock.io/tx/${txHash}`; - console.log(chalk.white(`🔗 View on Explorer:`), chalk.dim(`${explorerUrl}`)); - } else { - console.log(chalk.red("❌ Transaction failed.")); - } - } - } catch (error: any) { - console.error(chalk.red("🚨 Error during batch transfer:"), chalk.yellow(error.message || "Unknown error")); - } +function askQuestion( + rl: readline.Interface, + question: string +): Promise { + return new Promise((resolve) => rl.question(question, resolve)); } From a2bc1496d481c03ea9bc11c1e3476216a6cc1962 Mon Sep 17 00:00:00 2001 From: Chandra Bose Date: Wed, 15 Jan 2025 23:45:59 +0530 Subject: [PATCH 3/3] feat: enhance batch transfer with file input support --- bin/index.ts | 23 ++++++++++++++++------- package.json | 2 +- src/commands/batchTransfer.ts | 24 ++++++++++++++++++++++++ 3 files changed, 41 insertions(+), 8 deletions(-) diff --git a/bin/index.ts b/bin/index.ts index bbc38f7..6991d24 100644 --- a/bin/index.ts +++ b/bin/index.ts @@ -166,18 +166,27 @@ program .description("Execute batch transactions interactively or from stdin") .option("-i, --interactive", "Execute interactively and input transactions") .option("-t, --testnet", "Execute on the testnet") - .action(async (options: CommandOptions) => { + .option("-f, --file ", "Execute transactions from a file") + .action(async (options) => { try { const interactive = !!options.interactive; const testnet = !!options.testnet; + const file = options.file; - await batchTransferCommand(testnet, interactive); - } catch (error) { - console.error( + if (interactive && file) { console.error( - chalk.red("Error during batch processing:"), - error - ) + chalk.red( + "🚨 Cannot use both interactive mode and file input simultaneously." + ) + ); + return; + } + + await batchTransferCommand(file, testnet, interactive); + } catch (error: any) { + console.error( + chalk.red("🚨 Error during batch transfer:"), + chalk.yellow(error.message || "Unknown error") ); } }); diff --git a/package.json b/package.json index 7983753..24c645f 100644 --- a/package.json +++ b/package.json @@ -13,7 +13,7 @@ "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", - "batch-transfer": "pnpm run build && node dist/bin/index.js batch-transfer --testnet" + "batch-transfer": "pnpm run build && node dist/bin/index.js batch-transfer" }, "keywords": [ "rootstock", diff --git a/src/commands/batchTransfer.ts b/src/commands/batchTransfer.ts index 3c5ca95..18347fa 100644 --- a/src/commands/batchTransfer.ts +++ b/src/commands/batchTransfer.ts @@ -1,19 +1,40 @@ +import fs from "fs"; import chalk from "chalk"; import ora from "ora"; import readline from "readline"; +import { walletFilePath } from "../utils/constants.js"; import ViemProvider from "../utils/viemProvider.js"; import { Address } from "viem"; export async function batchTransferCommand( + filePath?: string, testnet: boolean = false, interactive: boolean = false ) { try { let batchData: { to: Address; value: number }[] = []; + // Handle input mode if (interactive) { + // Interactive input batchData = await promptForTransactions(); + } else if (filePath) { + // File input + if (!fs.existsSync(filePath)) { + console.log( + chalk.red( + "🚫 Batch file not found. Please provide a valid file." + ) + ); + return; + } + const fileContent = JSON.parse(fs.readFileSync(filePath, "utf8")); + batchData = fileContent.transactions.map((tx: any) => ({ + to: validateAddress(tx.address), + value: tx.amount, + })); } else { + // JSON input via stdin const stdin = await readStdin(); if (stdin) { const jsonData = JSON.parse(stdin); @@ -35,6 +56,7 @@ export async function batchTransferCommand( return; } + // Initialize wallet provider const provider = new ViemProvider(testnet); const walletClient = await provider.getWalletClient(); const account = walletClient.account; @@ -46,6 +68,7 @@ export async function batchTransferCommand( return; } + // Fetch balance const publicClient = await provider.getPublicClient(); const balance = await publicClient.getBalance({ address: account.address, @@ -61,6 +84,7 @@ export async function batchTransferCommand( chalk.green(`${rbtcBalance} RBTC`) ); + // Process transactions for (const { to, value } of batchData) { if (rbtcBalance < value) { console.log(