UNPKG

@rsksmart/rsk-cli

Version:

CLI tool for Rootstock network using Viem

247 lines (246 loc) • 10.1 kB
import ViemProvider from "../utils/viemProvider.js"; import chalk from "chalk"; import ora from "ora"; import fs from "fs"; import inquirer from "inquirer"; import { parseEther, formatEther } from "viem"; import { walletFilePath } from "../utils/constants.js"; import { getTokenInfo, isERC20Contract } from "../utils/tokenHelper.js"; export async function transactionCommand(testnet, toAddress, value, name, tokenAddress, options) { try { 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.")); return; } const { currentWallet, wallets } = walletsData; let wallet = name ? wallets[name] : wallets[currentWallet]; if (!wallet) { console.log(chalk.red("āš ļø Wallet not found.")); return; } const provider = new ViemProvider(testnet); const publicClient = await provider.getPublicClient(); const walletClient = await provider.getWalletClient(name); const account = walletClient.account; if (!account) { console.log(chalk.red("āš ļø Failed to retrieve the account.")); return; } if (!toAddress || !value) { const transactionType = await promptTransactionType(); const txDetails = await promptTransactionDetails(transactionType, publicClient, wallet.address); toAddress = txDetails.to; value = txDetails.value; tokenAddress = txDetails.tokenAddress; options = txDetails.options; } if (!toAddress || value === undefined) { throw new Error("Recipient address and value are required"); } const gasPrice = await publicClient.getGasPrice(); const formattedGasPrice = formatEther(gasPrice); console.log(chalk.white(`⛽ Current Gas Price: ${formattedGasPrice} RBTC`)); if (tokenAddress) { await handleTokenTransfer(publicClient, walletClient, account, tokenAddress, toAddress, value, wallet.address, testnet, options); } else { await handleRBTCTransfer(publicClient, walletClient, account, toAddress, value, wallet.address, testnet, options); } } catch (error) { console.error(chalk.red("🚨 Error:"), chalk.yellow("Error during transaction, please check the transaction details.")); } } async function promptTransactionType() { const { type } = await inquirer.prompt([ { type: 'list', name: 'type', message: 'šŸ“ What type of transaction would you like to create?', choices: [ { name: 'Simple Transfer (RBTC or Token)', value: 'simple' }, { name: 'Advanced Transfer (with custom gas settings)', value: 'advanced' }, { name: 'Raw Transaction (with custom data)', value: 'raw' } ] } ]); return type; } async function promptTransactionDetails(type, publicClient, fromAddress) { const details = {}; const common = await inquirer.prompt([ { type: 'input', name: 'to', message: 'šŸŽÆ Enter recipient address:', validate: (input) => input.startsWith('0x') && input.length === 42 }, { type: 'confirm', name: 'isToken', message: 'šŸŖ™ Is this a token transfer?', default: false } ]); details.to = common.to; if (common.isToken) { const tokenDetails = await inquirer.prompt([ { type: 'input', name: 'tokenAddress', message: 'šŸ“ Enter token contract address:', validate: async (input) => { if (!input.startsWith('0x') || input.length !== 42) return false; return await isERC20Contract(publicClient, input); } } ]); details.tokenAddress = tokenDetails.tokenAddress; const { decimals } = await getTokenInfo(publicClient, details.tokenAddress, fromAddress); const { value } = await inquirer.prompt([ { type: 'input', name: 'value', message: 'šŸ’° Enter amount to transfer:', validate: (input) => !isNaN(parseFloat(input)) && parseFloat(input) > 0 } ]); details.value = parseFloat(value); } else { const { value } = await inquirer.prompt([ { type: 'input', name: 'value', message: 'šŸ’° Enter amount in RBTC:', validate: (input) => !isNaN(parseFloat(input)) && parseFloat(input) > 0 } ]); details.value = parseFloat(value); } if (type === 'advanced' || type === 'raw') { const advanced = await inquirer.prompt([ { type: 'input', name: 'gasLimit', message: '⛽ Enter gas limit (optional):', default: '' }, { type: 'input', name: 'maxFeePerGas', message: 'šŸ’° Enter max fee per gas in RBTC (optional):', default: '' }, { type: 'input', name: 'maxPriorityFeePerGas', message: 'šŸ’° Enter max priority fee per gas in RBTC (optional):', default: '' } ]); details.options = { ...(advanced.gasLimit && { gasLimit: BigInt(advanced.gasLimit) }), ...(advanced.maxFeePerGas && { maxFeePerGas: parseEther(advanced.maxFeePerGas.toString()) }), ...(advanced.maxPriorityFeePerGas && { maxPriorityFeePerGas: parseEther(advanced.maxPriorityFeePerGas.toString()) }) }; if (type === 'raw') { const { data } = await inquirer.prompt([ { type: 'input', name: 'data', message: 'šŸ“ Enter transaction data (hex):', validate: (input) => input.startsWith('0x') } ]); details.options.data = data; } } return details; } async function handleTokenTransfer(publicClient, walletClient, account, tokenAddress, toAddress, value, fromAddress, testnet, options) { const tokenInfo = await getTokenInfo(publicClient, tokenAddress, fromAddress); console.log(chalk.white('\nšŸ“„ Token Transfer Details:')); console.log(chalk.white(`Token: ${tokenInfo.name} (${tokenInfo.symbol})`)); console.log(chalk.white(`Contract: ${tokenAddress}`)); console.log(chalk.white(`From: ${fromAddress}`)); console.log(chalk.white(`To: ${toAddress}`)); console.log(chalk.white(`Amount: ${value} ${tokenInfo.symbol}`)); const spinner = ora('ā³ Simulating token transfer...').start(); try { const { request } = await publicClient.simulateContract({ account, address: tokenAddress, abi: [{ name: 'transfer', type: 'function', stateMutability: 'nonpayable', inputs: [ { name: 'recipient', type: 'address' }, { name: 'amount', type: 'uint256' } ], outputs: [{ type: 'bool' }] }], functionName: 'transfer', args: [toAddress, BigInt(value * (10 ** tokenInfo.decimals))], ...options }); spinner.succeed('āœ… Simulation successful'); const txHash = await walletClient.writeContract(request); await handleTransactionReceipt(publicClient, txHash, testnet); } catch (error) { spinner.fail('āŒ Transaction failed'); throw error; } } async function handleRBTCTransfer(publicClient, walletClient, account, toAddress, value, fromAddress, testnet, options) { console.log(chalk.white('\nšŸ“„ RBTC Transfer Details:')); console.log(chalk.white(`From: ${fromAddress}`)); console.log(chalk.white(`To: ${toAddress}`)); console.log(chalk.white(`Amount: ${value} RBTC`)); const spinner = ora('ā³ Preparing transaction...').start(); try { const txHash = await walletClient.sendTransaction({ account, to: toAddress, value: parseEther(value.toString()), ...options }); spinner.succeed('āœ… Transaction sent'); await handleTransactionReceipt(publicClient, txHash, testnet); } catch (error) { spinner.fail('āŒ Transaction failed'); throw error; } } async function handleTransactionReceipt(publicClient, txHash, testnet) { const spinner = ora('ā³ Waiting for confirmation...').start(); try { const receipt = await publicClient.waitForTransactionReceipt({ hash: txHash }); spinner.stop(); if (receipt.status === 'success') { console.log(chalk.green('\nāœ… Transaction confirmed successfully!')); console.log(chalk.white(`šŸ“¦ Block Number: ${receipt.blockNumber}`)); console.log(chalk.white(`⛽ Gas Used: ${receipt.gasUsed}`)); 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('\nāŒ Transaction failed')); } } catch (error) { spinner.fail('āŒ Transaction confirmation failed'); throw error; } }