UNPKG

@rsksmart/rsk-cli

Version:

CLI tool for Rootstock network using Viem

239 lines (238 loc) 8.73 kB
import fs from "fs"; import chalk from "chalk"; import ora from "ora"; import readline from "readline"; import ViemProvider from "../utils/viemProvider.js"; import { resolveRNSToAddress, isRNSDomain } from "../utils/rnsHelper.js"; import { validateAndFormatAddressRSK } from "../utils/index.js"; function logMessage(params, message, color = chalk.white) { if (!params.isExternal) { console.log(color(message)); } } function logSuccess(params, message) { logMessage(params, message, chalk.green); } function logError(params, message) { logMessage(params, `❌ ${message}`, chalk.red); } function logInfo(params, message) { logMessage(params, message, chalk.blue); } function startSpinner(params, spinner, message) { if (!params.isExternal) { spinner.start(message); } } function stopSpinner(params, spinner) { if (!params.isExternal) { spinner.stop(); } } export async function batchTransferCommand(params) { try { const batchData = await getBatchData(params); if (batchData.length === 0) { const errorMessage = "No transactions file provided. Exiting..."; logError(params, errorMessage); return { error: errorMessage, success: false, }; } const provider = new ViemProvider(params.testnet); let walletClient; if (params.isExternal) { if (!params.name || !params.password || !params.walletsData) { const errorMessage = "Wallet name, password and wallets data are required."; logError(params, errorMessage); return { error: errorMessage, success: false, }; } walletClient = await provider.getWalletClientExternal(params.walletsData, params.name, params.password, provider); } else { walletClient = await provider.getWalletClient(); } if (!walletClient) { const errorMessage = "Failed to get wallet client."; logError(params, errorMessage); return { error: errorMessage, success: false, }; } const account = walletClient.account; if (!account) { const errorMessage = "Failed to retrieve wallet account. Exiting..."; logError(params, errorMessage); return { error: errorMessage, success: false, }; } const publicClient = await provider.getPublicClient(); const balance = await publicClient.getBalance({ address: account.address, }); const rbtcBalance = Number(balance) / 10 ** 18; logMessage(params, `📄 Wallet Address: ${account.address}`); logMessage(params, `💰 Current Balance: ${rbtcBalance} RBTC`); for (const { to, value } of batchData) { if (rbtcBalance < value) { const errorMessage = `Insufficient balance to transfer ${value} RBTC.`; logError(params, errorMessage); return { error: errorMessage, success: false, }; } let recipientAddress; if (params.resolveRNS && isRNSDomain(to)) { logMessage(params, `🔍 Resolving RNS domain: ${to}`); const resolved = await resolveRNSToAddress({ name: to, testnet: params.testnet, isExternal: params.isExternal }); if (!resolved) { logError(params, `Failed to resolve RNS domain: ${to}. Skipping transaction.`); continue; } const formatted = validateAndFormatAddressRSK(resolved, params.testnet); if (!formatted) { logError(params, `Resolved address is invalid for: ${to}. Skipping transaction.`); continue; } recipientAddress = formatted; } else { recipientAddress = validateAddress(to, params.testnet); } const txHash = await walletClient.sendTransaction({ account, chain: provider.chain, to: recipientAddress, value: BigInt(Math.floor(value * 10 ** 18)), }); logMessage(params, `🔄 Transaction initiated. TxHash: ${txHash}`); const spinner = params.isExternal ? ora({ isEnabled: false }) : ora(); startSpinner(params, spinner, "⏳ Waiting for confirmation..."); const receipt = await publicClient.waitForTransactionReceipt({ hash: txHash, }); stopSpinner(params, spinner); if (receipt.status === "success") { logSuccess(params, "✅ Transaction confirmed successfully!"); logInfo(params, `📦 Block Number: ${receipt.blockNumber}`); logInfo(params, `⛽ Gas Used: ${receipt.gasUsed.toString()}`); return { success: true, data: { transactionHash: txHash, blockNumber: receipt.blockNumber, gasUsed: receipt.gasUsed.toString(), }, }; } else { logError(params, "❌ Transaction failed."); return { success: false, data: { transactionHash: txHash, blockNumber: receipt.blockNumber, gasUsed: receipt.gasUsed.toString(), }, }; } } } catch (error) { const errorMessage = `🚨 Error during batch transfer: ${error.message || "Unknown error"}`; logError(params, errorMessage); return { error: errorMessage, success: false, }; } } async function promptForTransactions(allowRNS = false) { const rl = readline.createInterface({ input: process.stdin, output: process.stdout, }); const transactions = []; while (true) { const prompt = allowRNS ? "Enter address or RNS domain (e.g., alice.rsk): " : "Enter address: "; const input = await askQuestion(rl, prompt); let to; if (allowRNS && isRNSDomain(input)) { to = input; } else { try { to = validateAddress(input); } catch (error) { console.log(chalk.red("⚠️ Invalid address. Please try again.")); continue; } } 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, testnet) { const formatted = validateAndFormatAddressRSK(address, !!testnet); if (!formatted) { throw new Error(`Invalid address: ${address}`); } return formatted; } async function getBatchData(params) { let batchData; if (params.isExternal) { if (!params.batchData || params.batchData.length <= 0) { return []; } return params.batchData; } else { if (params.interactive) { return await promptForTransactions(params.resolveRNS); } else if (params.filePath) { if (!fs.existsSync(params.filePath)) { console.log(chalk.red("🚫 Batch file not found. Please provide a valid file.")); return []; } const fileContent = JSON.parse(fs.readFileSync(params.filePath, "utf8")); batchData = fileContent.map((tx) => ({ to: tx.to, value: tx.value, })); return batchData; } else { console.log(chalk.yellow("⚠️ No transactions file provided nor interactive mode enabled. Exiting...")); return []; } } } function askQuestion(rl, question) { return new Promise((resolve) => rl.question(question, resolve)); }