diff --git a/README.md b/README.md index a0ec869..99423cc 100644 --- a/README.md +++ b/README.md @@ -287,7 +287,306 @@ The transfer command supports the following options: > 4. Enough RBTC to cover gas fees > 5. Appropriate gas parameters for your transaction type -### 4. Check Transaction Status +### 4. Simulate Transaction (Dry-Run) + +The `simulate` command allows you to simulate transactions without executing them, providing a safe way to test transactions and estimate costs before actually sending them. This command supports simulation of both RBTC and ERC20 token transfers on either mainnet or testnet. + +#### Mainnet + +```bash +# Simulate RBTC transfer +rsk-cli simulate --address 0xRecipientAddress --value 0.001 + +# Simulate ERC20 token transfer +rsk-cli simulate --token 0xTokenAddress --address 0xRecipientAddress --value 0.1 +``` + +#### Testnet + +```bash +# Simulate RBTC transfer on testnet +rsk-cli simulate --testnet --address 0x1234567890123456789012345678901234567890 --value 0.001 + +# Simulate ERC20 token transfer on testnet +rsk-cli simulate --testnet --token 0x19f64674d8a5b4e652319f5e239efd3bc969a1fe --address 0x1234567890123456789012345678901234567890 --value 0.1 +``` + +#### Dynamic Wallet Selection + +```bash +rsk-cli simulate --wallet --testnet --address 0x1234567890123456789012345678901234567890 --value 0.001 +``` + +#### Advanced Simulation Options + +```bash +# With custom gas settings +rsk-cli simulate --testnet --address 0x1234567890123456789012345678901234567890 --value 0.001 --gas-limit 50000 --gas-price 0.00001 + +# With custom transaction data +rsk-cli simulate --testnet --address 0x1234567890123456789012345678901234567890 --value 0.001 --data "0x1234abcd" +``` + +Output example for RBTC simulation: + +``` +šŸ” Starting transaction simulation... +šŸ“„ From: 0x3C9614C9C8a4E966d3166857702fA8E8dC6eCd0f +šŸŽÆ To: 0x1234567890123456789012345678901234567890 +šŸ’° Amount: 0.001 +🌐 Network: Rootstock Testnet + +šŸ“„ RBTC Transfer: + Amount: 0.001 RBTC +ā³ Simulating transaction... +āœ… Simulation completed successfully! + +šŸ“Š Simulation Results: +━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ +šŸ“„ Transaction Details: + From: 0x3C9614C9C8a4E966d3166857702fA8E8dC6eCd0f + To: 0x1234567890123456789012345678901234567890 + Value: 0.001 RBTC + Network: Rootstock Testnet + +⛽ Gas Information: + Gas Estimate: 21000 + Gas Price: 0.000000000025804944 RBTC + Total Gas Cost: 0.000000541903824 RBTC + +šŸ” Simulation Details: + Block Number: 6890128 + Timestamp: 2025-10-03T17:27:26.397Z + Nonce: 0 + Chain ID: 31 +━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ +āœ… This transaction would succeed if executed. +šŸ’” Use the actual transfer command to execute this transaction. +``` + +Output example for ERC20 token simulation: + +``` +šŸ” Starting transaction simulation... +šŸ“„ From: 0x3C9614C9C8a4E966d3166857702fA8E8dC6eCd0f +šŸŽÆ To: 0x1234567890123456789012345678901234567890 +šŸ’° Amount: 0.1 +🌐 Network: Rootstock Testnet + +šŸ“„ Token Information: + Name: tRIF Token + Symbol: tRIF + Contract: 0x19f64674d8a5b4e652319f5e239efd3bc969a1fe + Amount: 0.1 tRIF + +šŸ“Š Simulation Results: +━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ +šŸ“„ Transaction Details: + From: 0x3C9614C9C8a4E966d3166857702fA8E8dC6eCd0f + To: 0x1234567890123456789012345678901234567890 + Value: 0.1 tRIF + Network: Rootstock Testnet + +šŸŖ™ Token Information: + Name: tRIF Token + Symbol: tRIF + Contract: 0x19f64674d8a5b4e652319f5e239efd3bc969a1fe + +⛽ Gas Information: + Gas Estimate: 65000 + Gas Price: 0.000000000025804944 RBTC + Total Gas Cost: 0.00167732136 RBTC + +šŸ” Simulation Details: + Block Number: 6890128 + Timestamp: 2025-10-03T17:27:26.397Z + Nonce: 0 + Chain ID: 31 +━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ +āœ… This transaction would succeed if executed. +šŸ’” Use the actual transfer command to execute this transaction. +``` + +#### Available Options + +The simulate command supports the following options: + +- `-t, --testnet`: Simulate on Rootstock testnet network +- `--wallet `: Select a specific wallet to use +- `-a, --address
`: Recipient address +- `--token
`: ERC20 token contract address (for token transfers) +- `--value `: Amount to transfer +- `--gas-limit `: Custom gas limit for the transaction +- `--gas-price `: Custom gas price in RBTC +- `--data `: Custom transaction data (hexadecimal format) + +> **ā„¹ļø Info:** +> +> The simulate command provides a safe way to test transactions without spending real gas or executing actual transfers. It helps you: +> +> - **Validate transactions** before execution +> - **Estimate gas costs** accurately +> - **Check transaction feasibility** +> - **Avoid costly mistakes** by testing first +> - **Plan transaction costs** in advance +> +> If a simulation fails, it indicates the transaction would also fail if executed, helping you identify and fix issues beforehand. + +### 5. Gas Price Optimization + +The `gas` command allows you to get current gas prices and optimization recommendations for the Rootstock blockchain. This command helps you choose the optimal gas price for your transactions, saving money while ensuring timely confirmation. + +#### Mainnet + +```bash +# Get current gas prices +rsk-cli gas + +# Get gas prices with cost estimates +rsk-cli gas --estimate + +# Get fast gas recommendation +rsk-cli gas --speed fast +``` + +#### Testnet + +```bash +# Get current gas prices on testnet +rsk-cli gas --testnet + +# Get gas prices with cost estimates on testnet +rsk-cli gas --testnet --estimate + +# Get slow gas recommendation on testnet +rsk-cli gas --testnet --speed slow +``` + +#### Gas Speed Options + +```bash +# Slow gas (cheapest, 2-5 min confirmation) +rsk-cli gas --speed slow + +# Standard gas (recommended, 30-60 sec confirmation) +rsk-cli gas --speed standard + +# Fast gas (most expensive, 10-30 sec confirmation) +rsk-cli gas --speed fast +``` + +Output example for mainnet: + +``` +⛽ Fetching current gas prices... +🌐 Network: Rootstock Mainnet +ā³ Getting gas price data... +āœ… Gas price data retrieved + +⛽ Current Gas Prices: +━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ +🌐 Network Information: + Network: Rootstock Mainnet + Block Number: 8066921 + +šŸ’° Gas Price Tiers: + 🐌 Slow: 0.00000000002085248 RBTC (0.02 gwei) - 2-5 min + šŸš€ Standard: 0.0000000000260656 RBTC (0.03 gwei) - 30-60 sec + ⚔ Fast: 0.00000000003127872 RBTC (0.03 gwei) - 10-30 sec + +šŸ’” Recommendation: + Use standard gas price (0.0000000000260656 RBTC) for most transactions +━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ +šŸ’” Tips: + • Use slow gas for non-urgent transactions to save money + • Use standard gas for most transactions (recommended) + • Use fast gas only when you need quick confirmation + • Use --estimate flag to see cost estimates for different transaction types +``` + +Output example with cost estimates: + +``` +⛽ Fetching current gas prices... +🌐 Network: Rootstock Testnet +ā³ Getting gas price data... +āœ… Gas price data retrieved + +⛽ Current Gas Prices: +━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ +🌐 Network Information: + Network: Rootstock Testnet + Block Number: 6894887 + +šŸ’° Gas Price Tiers: + 🐌 Slow: 0.000000000020429341 RBTC (0.02 gwei) - 2-5 min + šŸš€ Standard: 0.000000000025536677 RBTC (0.03 gwei) - 30-60 sec + ⚔ Fast: 0.000000000030644012 RBTC (0.03 gwei) - 10-30 sec + +šŸ“Š Gas Cost Estimates: + 1. Simple RBTC Transfer + Gas Limit: 21000 + Estimated Cost: 0.000000536270217 RBTC + Recommendation: Standard gas price recommended + 2. ERC20 Token Transfer + Gas Limit: 65000 + Estimated Cost: 0.000001659884005 RBTC + Recommendation: Standard gas price recommended + 3. Contract Interaction + Gas Limit: 100000 + Estimated Cost: 0.0000025536677 RBTC + Recommendation: Standard gas price recommended + +šŸ’” Recommendation: + Use standard gas price (0.000000000025536677 RBTC) for most transactions +━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ +šŸ’” Tips: + • Use slow gas for non-urgent transactions to save money + • Use standard gas for most transactions (recommended) + • Use fast gas only when you need quick confirmation + • Use --estimate flag to see cost estimates for different transaction types +``` + +#### Available Options + +The gas command supports the following options: + +- `-t, --testnet`: Get gas prices for Rootstock testnet +- `--speed `: Gas speed preference (slow|standard|fast) +- `--estimate`: Show cost estimates for different transaction types + +#### Gas Price Tiers + +The command displays three gas price tiers: + +- **🐌 Slow**: 80% of current price - Cheapest option, 2-5 minute confirmation +- **šŸš€ Standard**: Current network price - Recommended for most transactions, 30-60 second confirmation +- **⚔ Fast**: 120% of current price - Fastest option, 10-30 second confirmation + +#### Cost Estimates + +When using the `--estimate` flag, the command provides cost estimates for: + +- **Simple RBTC Transfer**: 21,000 gas units +- **ERC20 Token Transfer**: 65,000 gas units +- **Contract Interaction**: 100,000 gas units + +> **ā„¹ļø Info:** +> +> The gas command helps you optimize transaction costs by: +> +> - **Real-time Prices**: Shows current network gas prices +> - **Cost Optimization**: Helps choose the right gas price for your needs +> - **Time Estimates**: Provides confirmation time estimates for each tier +> - **Cost Estimates**: Shows estimated costs for different transaction types +> - **Smart Recommendations**: Suggests optimal gas prices based on your preferences +> +> This command is particularly useful when combined with the simulate command: +> 1. Use `rsk-cli gas` to check current prices +> 2. Use `rsk-cli simulate` to test your transaction +> 3. Execute your transaction with optimal gas settings + +### 6. Check Transaction Status The `tx` command allows you to check the status of a specific transaction on the Rootstock blockchain by providing the transaction ID. You can check the status on either the mainnet or testnet using the appropriate flags. @@ -312,7 +611,7 @@ Output example: šŸ”— Ensure that transactions are being conducted on the correct network. ``` -### 5. Deploy Smart Contract +### 7. Deploy Smart Contract The deploy command allows you to deploy a smart contract on the Rootstock blockchain. This command supports deployment on both the mainnet and testnet. @@ -349,7 +648,7 @@ Output example: šŸ”— View on Explorer: https://explorer.testnet.rootstock.io/address/0xf922e98776686ae39119bc3ea224f54bd0500d3f ``` -### 6. Verify Smart Contract +### 7. Verify Smart Contract The verify command allows you to verify a smart contract on the Rootstock blockchain using JSON Standard Input via Rootstock Explorer API. This command supports contract verification on both the mainnet and testnet. @@ -393,7 +692,7 @@ Output example: šŸ”— View on Explorer: https://explorer.testnet.rootstock.io/address/0x5E6Fad85585E857A76368dD0962D3B0CCf48Eb21 ``` -### 7. Interact with verified smart contracts +### 8. Interact with verified smart contracts The contract command allows you to interact with a smart contract on the Rootstock blockchain. This command lists all available read functions of a verified smart contract and allows you to call them. Write functions are excluded to ensure no state-changing operations are performed accidentally. @@ -422,7 +721,7 @@ Output example: šŸ”— View on Explorer: https://explorer.testnet.rootstock.io/address/0x15c41c730b86d9a598bf00da2d27d963b6dd2318 ``` -### 8. Interact with RSK bridge contract +### 9. Interact with RSK bridge contract The bridge command allows you to interact with the RSK bridge contract on the Rootstock blockchain. This command lists all allowed read and write functions of the RSK bridge contract and allows you to call them. @@ -455,7 +754,7 @@ Output example: šŸ”— View on Explorer: https://explorer.testnet.rootstock.io/address/0x0000000000000000000000000000000001000006 ``` -### 9. Fetch Wallet History +### 10. Fetch Wallet History The history command allows you to fetch the transaction history for a wallet on the Rootstock blockchain. This includes transactions such as ERC20, ERC721, and external transfers. You can specify whether to fetch the history from the Mainnet or Testnet by providing the appropriate flag. For this command to work, make sure to have an Alchemy API key you can get from [Alchemy Dashboard](https://dashboard.alchemy.com/). @@ -501,7 +800,7 @@ Output example: Time: Tue Nov 12 2024 11:46:32 GMT+0700 (Indochina Time) ``` -### 9. Fetch Wallet History +### 11. Batch Transfer The batch-transfer command allows you to send multiple transactions in one batch. This feature supports both interactive mode (manual input) and file-based batch processing, enabling you to transfer rBTC to multiple addresses in a single operation. diff --git a/bin/index.ts b/bin/index.ts index 2c7e104..6bd3bee 100644 --- a/bin/index.ts +++ b/bin/index.ts @@ -15,6 +15,8 @@ import { batchTransferCommand } from "../src/commands/batchTransfer.js"; import { historyCommand } from "../src/commands/history.js"; import { selectAddress } from "../src/commands/selectAddress.js"; import { transactionCommand } from "../src/commands/transaction.js"; +import { simulateCommand } from "../src/commands/simulate.js"; +import { gasCommand } from "../src/commands/gas.js"; import { parseEther } from "viem"; interface CommandOptions { @@ -38,6 +40,8 @@ interface CommandOptions { gasLimit?: string; gasPrice?: string; data?: string; + speed?: string; + estimate?: boolean; } const orange = chalk.rgb(255, 165, 0); @@ -272,4 +276,74 @@ program } }); +program + .command("simulate") + .description("Simulate a transaction without executing it (dry-run)") + .option("-t, --testnet", "Simulate on testnet") + .option("--wallet ", "Name of the wallet") + .option("-a, --address
", "Recipient address") + .option("--token
", "ERC20 token contract address (optional, for token transfers)") + .option("--value ", "Amount to transfer") + .option("--gas-limit ", "Custom gas limit") + .option("--gas-price ", "Custom gas price in RBTC") + .option("--data ", "Custom transaction data (hex)") + .action(async (options: CommandOptions) => { + try { + if (!options.value) { + throw new Error("Value is required for simulation."); + } + + const value = parseFloat(options.value); + + if (isNaN(value) || value <= 0) { + throw new Error("Invalid value specified for simulation."); + } + + const address = options.address + ? (`0x${options.address.replace(/^0x/, "")}` as `0x${string}`) + : await selectAddress(); + + const txOptions = { + ...(options.gasLimit && { gasLimit: BigInt(options.gasLimit) }), + ...(options.gasPrice && { gasPrice: parseEther(options.gasPrice.toString()) }), + ...(options.data && { data: options.data as `0x${string}` }) + }; + + await simulateCommand({ + testnet: !!options.testnet, + toAddress: address, + value: value, + name: options.wallet!, + tokenAddress: options.token as `0x${string}` | undefined, + txOptions: txOptions, + }); + } catch (error: any) { + console.error( + chalk.red("Error during simulation:"), + error.message || error + ); + } + }); + +program + .command("gas") + .description("Get current gas prices and optimization recommendations") + .option("-t, --testnet", "Get gas prices on testnet") + .option("--speed ", "Gas speed preference: slow|standard|fast") + .option("--estimate", "Estimate gas cost for a simple transfer") + .action(async (options: CommandOptions) => { + try { + await gasCommand({ + testnet: !!options.testnet, + speed: options.speed as 'slow' | 'standard' | 'fast' | undefined, + estimate: !!options.estimate, + }); + } catch (error: any) { + console.error( + chalk.red("Error getting gas information:"), + error.message || error + ); + } + }); + program.parse(process.argv); diff --git a/src/commands/gas.ts b/src/commands/gas.ts new file mode 100644 index 0000000..a58643c --- /dev/null +++ b/src/commands/gas.ts @@ -0,0 +1,185 @@ +import ViemProvider from "../utils/viemProvider.js"; +import chalk from "chalk"; +import ora from "ora"; +import { GasOptions, GasResult, GasPriceInfo, GasEstimate } from "../utils/types.js"; +import { formatEther, parseEther } from "viem"; + +export async function gasCommand(options: GasOptions): Promise { + try { + const provider = new ViemProvider(options.testnet); + const publicClient = await provider.getPublicClient(); + + console.log(chalk.blue("⛽ Fetching current gas prices...")); + console.log(chalk.white(`🌐 Network: ${options.testnet ? 'Rootstock Testnet' : 'Rootstock Mainnet'}`)); + + const spinner = ora('ā³ Getting gas price data...').start(); + + try { + // Get current gas price and block information + const [gasPrice, blockNumber] = await Promise.all([ + publicClient.getGasPrice(), + publicClient.getBlockNumber() + ]); + + spinner.succeed('āœ… Gas price data retrieved'); + + // Calculate different gas price tiers + const gasPrices = calculateGasPriceTiers(gasPrice); + + // Get gas estimates if requested + let estimates: GasEstimate[] = []; + if (options.estimate) { + estimates = await getGasEstimates(publicClient, gasPrices); + } + + // Get recommendation + const recommendation = getGasRecommendation(gasPrices, options.speed); + + // Display results + displayGasInfo(gasPrices, estimates, recommendation, options, blockNumber.toString()); + + return { + success: true, + data: { + gasPrices, + estimates: options.estimate ? estimates : undefined, + network: options.testnet ? 'Rootstock Testnet' : 'Rootstock Mainnet', + blockNumber: blockNumber.toString(), + recommendation + } + }; + + } catch (error: any) { + spinner.fail('āŒ Failed to get gas price data'); + throw error; + } + + } catch (error: any) { + console.error(chalk.red("🚨 Error getting gas information:"), error.message || error); + return { success: false, error: error.message || "Unknown error" }; + } +} + +function calculateGasPriceTiers(currentGasPrice: bigint): GasPriceInfo { + const currentPriceWei = currentGasPrice; + const currentPriceEther = formatEther(currentPriceWei); + + // Calculate different tiers based on current price + const slowPrice = currentPriceWei * BigInt(80) / BigInt(100); // 80% of current + const standardPrice = currentPriceWei; // Current price + const fastPrice = currentPriceWei * BigInt(120) / BigInt(100); // 120% of current + + return { + slow: { + price: formatEther(slowPrice), + time: "2-5 min", + gwei: formatGwei(slowPrice) + }, + standard: { + price: formatEther(standardPrice), + time: "30-60 sec", + gwei: formatGwei(standardPrice) + }, + fast: { + price: formatEther(fastPrice), + time: "10-30 sec", + gwei: formatGwei(fastPrice) + } + }; +} + +function formatGwei(priceWei: bigint): string { + // Convert wei to gwei (1 gwei = 10^9 wei) + const gwei = Number(priceWei) / 1e9; + return gwei.toFixed(2); +} + +async function getGasEstimates(publicClient: any, gasPrices: GasPriceInfo): Promise { + const estimates: GasEstimate[] = []; + + // Simple RBTC transfer estimate + const simpleTransferGas = BigInt(21000); + estimates.push({ + transactionType: "Simple RBTC Transfer", + gasLimit: simpleTransferGas.toString(), + estimatedCost: formatEther(parseEther(gasPrices.standard.price) * simpleTransferGas), + recommendation: "Standard gas price recommended" + }); + + // ERC20 token transfer estimate + const tokenTransferGas = BigInt(65000); + estimates.push({ + transactionType: "ERC20 Token Transfer", + gasLimit: tokenTransferGas.toString(), + estimatedCost: formatEther(parseEther(gasPrices.standard.price) * tokenTransferGas), + recommendation: "Standard gas price recommended" + }); + + // Contract interaction estimate + const contractInteractionGas = BigInt(100000); + estimates.push({ + transactionType: "Contract Interaction", + gasLimit: contractInteractionGas.toString(), + estimatedCost: formatEther(parseEther(gasPrices.standard.price) * contractInteractionGas), + recommendation: "Standard gas price recommended" + }); + + return estimates; +} + +function getGasRecommendation(gasPrices: GasPriceInfo, preferredSpeed?: string): string { + if (preferredSpeed) { + switch (preferredSpeed) { + case 'slow': + return `Use slow gas price (${gasPrices.slow.price} RBTC) for cost savings`; + case 'fast': + return `Use fast gas price (${gasPrices.fast.price} RBTC) for quick confirmation`; + case 'standard': + return `Use standard gas price (${gasPrices.standard.price} RBTC) for balanced speed and cost`; + default: + return `Use standard gas price (${gasPrices.standard.price} RBTC) for most transactions`; + } + } + + return `Use standard gas price (${gasPrices.standard.price} RBTC) for most transactions`; +} + +function displayGasInfo( + gasPrices: GasPriceInfo, + estimates: GasEstimate[], + recommendation: string, + options: GasOptions, + blockNumber: string +) { + console.log(chalk.green('\n⛽ Current Gas Prices:')); + console.log(chalk.white('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━')); + + console.log(chalk.cyan('🌐 Network Information:')); + console.log(chalk.white(` Network: ${options.testnet ? 'Rootstock Testnet' : 'Rootstock Mainnet'}`)); + console.log(chalk.white(` Block Number: ${blockNumber}`)); + + console.log(chalk.cyan('\nšŸ’° Gas Price Tiers:')); + console.log(chalk.white(` 🐌 Slow: ${gasPrices.slow.price} RBTC (${gasPrices.slow.gwei} gwei) - ${gasPrices.slow.time}`)); + console.log(chalk.white(` šŸš€ Standard: ${gasPrices.standard.price} RBTC (${gasPrices.standard.gwei} gwei) - ${gasPrices.standard.time}`)); + console.log(chalk.white(` ⚔ Fast: ${gasPrices.fast.price} RBTC (${gasPrices.fast.gwei} gwei) - ${gasPrices.fast.time}`)); + + if (options.estimate && estimates.length > 0) { + console.log(chalk.cyan('\nšŸ“Š Gas Cost Estimates:')); + estimates.forEach((estimate, index) => { + console.log(chalk.white(` ${index + 1}. ${estimate.transactionType}`)); + console.log(chalk.white(` Gas Limit: ${estimate.gasLimit}`)); + console.log(chalk.white(` Estimated Cost: ${estimate.estimatedCost} RBTC`)); + console.log(chalk.white(` Recommendation: ${estimate.recommendation}`)); + }); + } + + console.log(chalk.cyan('\nšŸ’” Recommendation:')); + console.log(chalk.yellow(` ${recommendation}`)); + + console.log(chalk.white('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━')); + console.log(chalk.blue('šŸ’” Tips:')); + console.log(chalk.white(' • Use slow gas for non-urgent transactions to save money')); + console.log(chalk.white(' • Use standard gas for most transactions (recommended)')); + console.log(chalk.white(' • Use fast gas only when you need quick confirmation')); + console.log(chalk.white(' • Use --estimate flag to see cost estimates for different transaction types')); +} diff --git a/src/commands/simulate.ts b/src/commands/simulate.ts new file mode 100644 index 0000000..bab85d0 --- /dev/null +++ b/src/commands/simulate.ts @@ -0,0 +1,229 @@ +import ViemProvider from "../utils/viemProvider.js"; +import chalk from "chalk"; +import ora from "ora"; +import fs from "fs"; +import { Address, parseEther, formatEther } from "viem"; +import { walletFilePath } from "../utils/constants.js"; +import { getTokenInfo, isERC20Contract } from "../utils/tokenHelper.js"; +import { SimulationOptions, SimulationResult, SimulationData } from "../utils/types.js"; + +export async function simulateCommand(options: SimulationOptions): Promise { + try { + if (!fs.existsSync(walletFilePath)) { + console.log(chalk.red("🚫 No saved wallet found. Please create a wallet first.")); + return { success: false, error: "No wallet found" }; + } + + 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 { success: false, error: "No valid wallet found" }; + } + + const { currentWallet, wallets } = walletsData; + let wallet = options.name ? wallets[options.name] : wallets[currentWallet]; + if (!wallet) { + console.log(chalk.red("āš ļø Wallet not found.")); + return { success: false, error: "Wallet not found" }; + } + + const provider = new ViemProvider(options.testnet); + const publicClient = await provider.getPublicClient(); + const walletClient = await provider.getWalletClient(options.name); + const account = walletClient.account; + + if (!account) { + console.log(chalk.red("āš ļø Failed to retrieve the account.")); + return { success: false, error: "Failed to retrieve account" }; + } + + console.log(chalk.blue("šŸ” Starting transaction simulation...")); + console.log(chalk.white(`šŸ“„ From: ${wallet.address}`)); + console.log(chalk.white(`šŸŽÆ To: ${options.toAddress}`)); + console.log(chalk.white(`šŸ’° Amount: ${options.value}`)); + console.log(chalk.white(`🌐 Network: ${options.testnet ? 'Rootstock Testnet' : 'Rootstock Mainnet'}`)); + + const spinner = ora('ā³ Simulating transaction...').start(); + + try { + let simulationData: SimulationData; + + if (options.tokenAddress) { + // ERC20 Token Transfer Simulation + simulationData = await simulateTokenTransfer( + publicClient, + account, + options.tokenAddress, + options.toAddress, + options.value, + wallet.address, + options.testnet, + options.txOptions + ); + } else { + // RBTC Transfer Simulation + simulationData = await simulateRBTCTransfer( + publicClient, + account, + options.toAddress, + options.value, + wallet.address, + options.testnet, + options.txOptions + ); + } + + spinner.succeed('āœ… Simulation completed successfully!'); + + // Display simulation results + displaySimulationResults(simulationData); + + return { success: true, data: simulationData }; + } catch (error: any) { + spinner.fail('āŒ Simulation failed'); + console.log(chalk.red(`🚨 Simulation Error: ${error.message}`)); + return { success: false, error: error.message }; + } + } catch (error: any) { + console.error(chalk.red("🚨 Error during simulation:"), error.message || error); + return { success: false, error: error.message || "Unknown error" }; + } +} + +async function simulateTokenTransfer( + publicClient: any, + account: any, + tokenAddress: Address, + toAddress: Address, + value: number, + fromAddress: Address, + testnet: boolean, + txOptions?: any +): Promise { + // Get token information + const tokenInfo = await getTokenInfo(publicClient, tokenAddress, fromAddress); + + console.log(chalk.white(`\nšŸ“„ Token Information:`)); + console.log(chalk.white(` Name: ${tokenInfo.name}`)); + console.log(chalk.white(` Symbol: ${tokenInfo.symbol}`)); + console.log(chalk.white(` Contract: ${tokenAddress}`)); + console.log(chalk.white(` Amount: ${value} ${tokenInfo.symbol}`)); + + // Simulate the token transfer + 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))], + ...txOptions + }); + + // Get current gas price + const gasPrice = await publicClient.getGasPrice(); + const gasEstimate = request.gas || BigInt(65000); // Default for ERC20 transfers + + return { + from: fromAddress, + to: toAddress, + value: `${value} ${tokenInfo.symbol}`, + gasEstimate: gasEstimate.toString(), + gasPrice: formatEther(gasPrice), + totalCost: formatEther(BigInt(gasPrice) * gasEstimate), + network: testnet ? 'Rootstock Testnet' : 'Rootstock Mainnet', + tokenInfo: { + name: tokenInfo.name, + symbol: tokenInfo.symbol, + decimals: tokenInfo.decimals, + contract: tokenAddress + }, + simulationDetails: { + blockNumber: (await publicClient.getBlockNumber()).toString(), + timestamp: new Date().toISOString(), + nonce: Number(await publicClient.getTransactionCount({ address: fromAddress })), + chainId: testnet ? 31 : 30 + } + }; +} + +async function simulateRBTCTransfer( + publicClient: any, + account: any, + toAddress: Address, + value: number, + fromAddress: Address, + testnet: boolean, + txOptions?: any +): Promise { + console.log(chalk.white(`\nšŸ“„ RBTC Transfer:`)); + console.log(chalk.white(` Amount: ${value} RBTC`)); + + // Simulate the RBTC transfer by estimating gas + const gasEstimate = await publicClient.estimateGas({ + account, + to: toAddress, + value: parseEther(value.toString()), + ...txOptions + }); + + // Get current gas price + const gasPrice = await publicClient.getGasPrice(); + + return { + from: fromAddress, + to: toAddress, + value: `${value} RBTC`, + gasEstimate: gasEstimate.toString(), + gasPrice: formatEther(gasPrice), + totalCost: formatEther(BigInt(gasPrice) * gasEstimate), + network: testnet ? 'Rootstock Testnet' : 'Rootstock Mainnet', + simulationDetails: { + blockNumber: (await publicClient.getBlockNumber()).toString(), + timestamp: new Date().toISOString(), + nonce: Number(await publicClient.getTransactionCount({ address: fromAddress })), + chainId: testnet ? 31 : 30 + } + }; +} + +function displaySimulationResults(data: SimulationData) { + console.log(chalk.green('\nšŸ“Š Simulation Results:')); + console.log(chalk.white('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━')); + + console.log(chalk.cyan('šŸ“„ Transaction Details:')); + console.log(chalk.white(` From: ${data.from}`)); + console.log(chalk.white(` To: ${data.to}`)); + console.log(chalk.white(` Value: ${data.value}`)); + console.log(chalk.white(` Network: ${data.network}`)); + + if (data.tokenInfo) { + console.log(chalk.cyan('\nšŸŖ™ Token Information:')); + console.log(chalk.white(` Name: ${data.tokenInfo.name}`)); + console.log(chalk.white(` Symbol: ${data.tokenInfo.symbol}`)); + console.log(chalk.white(` Contract: ${data.tokenInfo.contract}`)); + } + + console.log(chalk.cyan('\n⛽ Gas Information:')); + console.log(chalk.white(` Gas Estimate: ${data.gasEstimate}`)); + console.log(chalk.white(` Gas Price: ${data.gasPrice} RBTC`)); + console.log(chalk.white(` Total Gas Cost: ${data.totalCost} RBTC`)); + + console.log(chalk.cyan('\nšŸ” Simulation Details:')); + console.log(chalk.white(` Block Number: ${data.simulationDetails.blockNumber}`)); + console.log(chalk.white(` Timestamp: ${data.simulationDetails.timestamp}`)); + console.log(chalk.white(` Nonce: ${data.simulationDetails.nonce}`)); + console.log(chalk.white(` Chain ID: ${data.simulationDetails.chainId}`)); + + console.log(chalk.white('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━')); + console.log(chalk.green('āœ… This transaction would succeed if executed.')); + console.log(chalk.yellow('šŸ’” Use the actual transfer command to execute this transaction.')); +} diff --git a/src/utils/types.ts b/src/utils/types.ts index 7386b2c..0b47bac 100644 --- a/src/utils/types.ts +++ b/src/utils/types.ts @@ -85,4 +85,88 @@ export type VerificationRequest = { sources: string; settings: any; constructorArguments?: any[]; +}; + +export type SimulationResult = { + success: boolean; + data?: SimulationData; + error?: string; +}; + +export type SimulationData = { + from: string; + to: string; + value: string; + gasEstimate: string; + gasPrice: string; + totalCost: string; + network: string; + tokenInfo?: { + name: string; + symbol: string; + decimals: number; + contract: string; + }; + simulationDetails: { + blockNumber: string; + timestamp: string; + nonce: number; + chainId: number; + }; +}; + +export type SimulationOptions = { + testnet: boolean; + toAddress: `0x${string}`; + value: number; + name?: string; + tokenAddress?: `0x${string}`; + txOptions?: { + gasLimit?: bigint; + gasPrice?: bigint; + data?: `0x${string}`; + }; +}; + +export type GasPriceInfo = { + slow: { + price: string; + time: string; + gwei: string; + }; + standard: { + price: string; + time: string; + gwei: string; + }; + fast: { + price: string; + time: string; + gwei: string; + }; +}; + +export type GasEstimate = { + transactionType: string; + gasLimit: string; + estimatedCost: string; + recommendation: string; +}; + +export type GasResult = { + success: boolean; + data?: { + gasPrices: GasPriceInfo; + estimates?: GasEstimate[]; + network: string; + blockNumber: string; + recommendation: string; + }; + error?: string; +}; + +export type GasOptions = { + testnet: boolean; + speed?: 'slow' | 'standard' | 'fast'; + estimate?: boolean; }; \ No newline at end of file