@rsksmart/rsk-cli
Version:
CLI tool for Rootstock network using Viem
247 lines (246 loc) ⢠10.1 kB
JavaScript
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;
}
}