UNPKG

@rsksmart/rsk-cli

Version:

CLI tool for Rootstock network using Viem

339 lines (338 loc) 13.6 kB
#!/usr/bin/env node import { Command } from "commander"; import { walletCommand } from "../src/commands/wallet.js"; import { balanceCommand } from "../src/commands/balance.js"; import { transferCommand } from "../src/commands/transfer.js"; import { txCommand } from "../src/commands/tx.js"; import figlet from "figlet"; import chalk from "chalk"; 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"; import { historyCommand } from "../src/commands/history.js"; import { selectAddress } from "../src/commands/selectAddress.js"; import { resolveCommand } from "../src/commands/resolve.js"; import { configCommand } from "../src/commands/config.js"; import { transactionCommand } from "../src/commands/transaction.js"; import { monitorCommand, listMonitoringSessions, stopMonitoringSession } from "../src/commands/monitor.js"; import { parseEther } from "viem"; import { resolveRNSToAddress } from "../src/utils/rnsHelper.js"; import { validateAndFormatAddressRSK } from "../src/utils/index.js"; const orange = chalk.rgb(255, 165, 0); console.log(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.4.0", "-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(); }); program .command("balance") .description("Check the balance of the saved wallet") .option("-t, --testnet", "Check the balance on the testnet") .option("--wallet <wallet>", "Name of the wallet") .option("-a ,--address <address>", "Token holder address") .option("--rns <domain>", "Token holder RNS domain (e.g., alice.rsk)") .action(async (options) => { let holderAddress = options.address; if (options.rns) { const resolvedAddress = await resolveRNSToAddress({ name: options.rns, testnet: !!options.testnet, isExternal: false }); if (!resolvedAddress) { throw new Error(`Failed to resolve RNS domain: ${options.rns}`); } holderAddress = resolvedAddress; } await balanceCommand({ testnet: options.testnet, walletName: options.wallet, address: holderAddress, }); }); program .command("transfer") .description("Transfer RBTC or ERC20 tokens to the provided address") .option("-t, --testnet", "Transfer on the testnet") .option("--wallet <wallet>", "Name of the wallet") .option("-a, --address <address>", "Recipient address") .option("--rns <domain>", "Recipient RNS domain (e.g., alice.rsk)") .option("--token <address>", "ERC20 token contract address (optional, for token transfers)") .option("--value <value>", "Amount to transfer") .option("-i, --interactive", "Execute interactively and input transactions") .option("--gas-limit <limit>", "Custom gas limit") .option("--gas-price <price>", "Custom gas price in RBTC") .option("--data <data>", "Custom transaction data (hex)") .action(async (options) => { try { if (options.interactive) { await batchTransferCommand({ testnet: !!options.testnet, interactive: true, }); return; } if (!options.value) { throw new Error("Value is required for the transfer."); } const value = parseFloat(options.value); if (isNaN(value) || value <= 0) { throw new Error("Invalid value specified for transfer."); } let address; if (options.rns) { const resolvedAddress = await resolveRNSToAddress({ name: options.rns, testnet: !!options.testnet, isExternal: false }); if (!resolvedAddress) { throw new Error(`Failed to resolve RNS domain: ${options.rns}`); } const formatted = validateAndFormatAddressRSK(resolvedAddress, !!options.testnet); if (!formatted) { throw new Error(`Invalid resolved address for domain: ${options.rns}`); } address = formatted; } else if (options.address) { const formatted = validateAndFormatAddressRSK(String(options.address), !!options.testnet); if (!formatted) { throw new Error("Invalid recipient address"); } address = formatted; } else { address = await selectAddress(); } const txOptions = { ...(options.gasLimit && { gasLimit: BigInt(options.gasLimit) }), ...(options.gasPrice && { gasPrice: parseEther(options.gasPrice.toString()) }), ...(options.data && { data: options.data }) }; await transferCommand({ testnet: !!options.testnet, toAddress: address, value: value, name: options.wallet, tokenAddress: options.token, }); } catch (error) { console.error(chalk.red("Error during transfer:"), error.message || error); } }); program .command("tx") .description("Check the status of a transaction") .option("-t, --testnet", "Check the transaction status on the testnet") .requiredOption("-i, --txid <txid>", "Transaction ID") .option("--monitor", "Keep monitoring the transaction until confirmation") .option("--confirmations <number>", "Required confirmations for monitoring (default: 12)") .action(async (options) => { const formattedTxId = options.txid.startsWith("0x") ? options.txid : `0x${options.txid}`; await txCommand({ testnet: !!options.testnet, txid: formattedTxId, isExternal: false, monitor: !!options.monitor, confirmations: options.confirmations ? parseInt(options.confirmations.toString()) : undefined, }); }); program .command("deploy") .description("Deploy a contract") .requiredOption("--abi <path>", "Path to the ABI file") .requiredOption("--bytecode <path>", "Path to the bytecode file") .option("--wallet <wallet>", "Name of the wallet") .option("--args <args...>", "Constructor arguments (space-separated)") .option("-t, --testnet", "Deploy on the testnet") .action(async (options) => { const args = options.args || []; await deployCommand({ abiPath: options.abi, bytecodePath: options.bytecode, testnet: options.testnet, args: args, name: options.wallet, }); }); program .command("verify") .description("Verify a contract") .requiredOption("--json <path>", "Path to the JSON Standard Input") .requiredOption("--name <name>", "Name of the contract") .requiredOption("-a, --address <address>", "Address of the deployed contract") .option("-t, --testnet", "Deploy on the testnet") .option("--decodedArgs <args...>", "Decoded Constructor arguments (space-separated)") .action(async (options) => { const args = options.decodedArgs || []; await verifyCommand({ jsonPath: options.json, address: options.address, name: options.name, testnet: options.testnet === undefined ? undefined : !!options.testnet, args: args, }); }); program .command("contract") .description("Interact with a contract") .requiredOption("-a, --address <address>", "Address of a verified contract") .option("-t, --testnet", "Deploy on the testnet") .action(async (options) => { await ReadContract({ address: options.address, testnet: !!options.testnet, }); }); program .command("bridge") .description("Interact with RSK bridge") .option("-t, --testnet", "Deploy on the testnet") .option("--wallet <wallet>", "Name of the wallet") .action(async (options) => { await bridgeCommand({ testnet: options.testnet === undefined ? undefined : !!options.testnet, name: options.wallet, }); }); program .command("history") .description("Fetch history for current wallet") .option("--apiKey <apiKey", "Alchemy API key") .option("--number <number>", "Number of transactions to fetch") .option("-t, --testnet", "History of wallet on the testnet") .action(async (options) => { await historyCommand({ testnet: !!options.testnet, apiKey: options.apiKey, number: options.number, }); }); program .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 <path>", "Execute transactions from a file") .option("--rns", "Enable RNS domain resolution for recipient addresses") .action(async (options) => { try { const interactive = !!options.interactive; const testnet = !!options.testnet; const file = options.file; const resolveRNS = !!options.rns; if (interactive && file) { console.error(chalk.red("🚨 Cannot use both interactive mode and file input simultaneously.")); return; } await batchTransferCommand({ filePath: file, testnet: testnet, interactive: interactive, resolveRNS: resolveRNS, }); } catch (error) { console.error(chalk.red("🚨 Error during batch transfer:"), chalk.yellow(error.message || "Unknown error")); } }); program .command("resolve <name>") .description("Resolve RNS names to addresses or reverse lookup addresses to names") .option("-t, --testnet", "Use testnet (currently mainnet only)") .option("-r, --reverse", "Reverse lookup: address to name") .action(async (name, options) => { await resolveCommand({ name, testnet: !!options.testnet, reverse: !!options.reverse }); }); program .command("config") .description("Manage CLI configuration settings") .action(async () => { await configCommand(); }); program .command("transaction") .description("Create and send transactions (simple, advanced, or raw)") .option("-t, --testnet", "Execute on the testnet") .option("--wallet <wallet>", "Name of the wallet") .option("-a, --address <address>", "Recipient address") .option("--token <address>", "ERC20 token contract address (optional, for token transfers)") .option("--value <value>", "Amount to transfer") .option("--gas-limit <limit>", "Custom gas limit") .option("--gas-price <price>", "Custom gas price in RBTC") .option("--data <data>", "Custom transaction data (hex)") .action(async (options) => { try { await transactionCommand(options.testnet, options.address, options.value ? parseFloat(options.value) : undefined, options.wallet, options.token, { ...(options.gasLimit && { gasLimit: BigInt(options.gasLimit) }), ...(options.gasPrice && { gasPrice: parseEther(options.gasPrice.toString()) }), ...(options.data && { data: options.data }) }); } catch (error) { console.error(chalk.red("Error during transaction:"), error.message || error); } }); program .command("monitor") .description("Monitor addresses or transactions with real-time updates") .option("-t, --testnet", "Monitor on the testnet") .option("-a, --address <address>", "Address to monitor") .option("--tx <txid>", "Transaction ID to monitor") .option("--confirmations <number>", "Required confirmations for transaction monitoring (default: 12)") .option("--balance", "Monitor address balance changes") .option("--transactions", "Monitor address transaction history") .option("--list", "List active monitoring sessions") .option("--stop <sessionId>", "Stop a specific monitoring session") .action(async (options) => { try { if (options.list) { await listMonitoringSessions(!!options.testnet); return; } if (options.stop) { await stopMonitoringSession(options.stop, !!options.testnet); return; } const address = options.address ? `0x${options.address.replace(/^0x/, "")}` : undefined; const tx = options.tx ? (options.tx.startsWith("0x") ? options.tx : `0x${options.tx}`) : undefined; await monitorCommand({ testnet: !!options.testnet, address: address, monitorBalance: options.balance !== false, monitorTransactions: !!options.transactions, tx, confirmations: options.confirmations ? parseInt(options.confirmations.toString()) : undefined, isExternal: false }); } catch (error) { console.error(chalk.red("Error during monitoring:"), error.message || error); } }); program.parse(process.argv);