diff --git a/bin/index.ts b/bin/index.ts
index f240d42..6991d24 100644
--- a/bin/index.ts
+++ b/bin/index.ts
@@ -10,145 +10,185 @@ 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;
- address?: string;
- value?: string;
- txid?: string;
- abi?: string;
- bytecode?: string;
- args?: any;
- json?: any;
- name?: string;
- decodedArgs?: any;
+ 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
+ );
+ });
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("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("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("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("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("bridge")
+ .description("Interact with RSK bridge")
+ .option("-t, --testnet", "Deploy on the testnet")
+ .action(async (options: CommandOptions) => {
+ await bridgeCommand(!!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("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")
+ .option("-f, --file ", "Execute transactions from a file")
+ .action(async (options) => {
+ try {
+ const interactive = !!options.interactive;
+ const testnet = !!options.testnet;
+ const file = options.file;
+
+ if (interactive && file) {
+ console.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")
+ );
+ }
+ });
program.parse(process.argv);
diff --git a/package.json b/package.json
index e9962d9..ae2faaa 100644
--- a/package.json
+++ b/package.json
@@ -15,7 +15,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"
},
"keywords": [
"rootstock",
diff --git a/src/commands/batchTransfer.ts b/src/commands/batchTransfer.ts
new file mode 100644
index 0000000..18347fa
--- /dev/null
+++ b/src/commands/batchTransfer.ts
@@ -0,0 +1,193 @@
+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);
+ 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;
+ }
+
+ // Initialize wallet provider
+ 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;
+ }
+
+ // Fetch balance
+ 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`)
+ );
+
+ // Process transactions
+ 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);
+ });
+}
+
+function askQuestion(
+ rl: readline.Interface,
+ question: string
+): Promise {
+ return new Promise((resolve) => rl.question(question, resolve));
+}