UNPKG

@rsksmart/rsk-cli

Version:

CLI tool for Rootstock network using Viem

755 lines (754 loc) • 31.8 kB
import { generatePrivateKey, privateKeyToAccount } from "viem/accounts"; import chalk from "chalk"; import inquirer from "inquirer"; import fs from "fs-extra"; import crypto from "crypto"; import { loadWallets } from "../utils/index.js"; import { walletFilePath } from "../utils/constants.js"; import path from "path"; import { addressBookCommand } from "./addressbook.js"; import zxcvbn from "zxcvbn"; const CONFIG = { minLength: 6, maxLength: 128, minScore: 3, }; /** * Validates password using zxcvbn library */ function validatePassword(password) { const errors = []; if (password.length < CONFIG.minLength) { errors.push(`Password must be at least ${CONFIG.minLength} characters long`); } if (password.length > CONFIG.maxLength) { errors.push(`Password must be no more than ${CONFIG.maxLength} characters long`); } const result = zxcvbn(password); if (result.score < CONFIG.minScore) { const scoreLabels = [ "too guessable (risky password)", "very guessable (protection from throttled online attacks)", "somewhat guessable (protection from unthrottled online attacks)", "safely unguessable (moderate protection from offline attacks)", "very unguessable (strong protection from offline attacks)" ]; errors.push(`Password strength: ${scoreLabels[result.score]} - score ${result.score}/4 (minimum required: ${CONFIG.minScore}/4)`); if (result.feedback.warning) { errors.push(`āš ļø Warning: ${result.feedback.warning}`); } if (result.feedback.suggestions && result.feedback.suggestions.length > 0) { result.feedback.suggestions.forEach((suggestion) => { errors.push(`šŸ’” Suggestion: ${suggestion}`); }); } } const entropy = Math.log2(result.guesses); const crackTime = String(result.crack_times_display.offline_fast_hashing_1e10_per_second); return { isValid: errors.length === 0, errors, entropy, score: result.score, crackTime }; } export const createWalletOptions = [ "šŸ†• Create a new wallet", "šŸ”‘ Import existing wallet", "šŸ” List saved wallets", "šŸ” Switch wallet", "šŸ“ Update wallet name", "šŸ“‚ Backup wallet data", "āŒ Delete wallet", "šŸ“– Address Book", ]; function getWalletsData(params) { return params.walletsData && params.isExternal ? params.walletsData : JSON.parse(loadWallets()); } function logMessage(params, message, color = chalk.white) { if (!params.isExternal) { console.log(color(message)); } } function logError(params, message) { logMessage(params, `āŒ ${message}`, chalk.red); } function logSuccess(params, message) { logMessage(params, message, chalk.green); } function logWarning(params, message) { logMessage(params, message, chalk.yellow); } function logInfo(params, message) { logMessage(params, message, chalk.blue); } function encryptPrivateKey(privateKey, password) { const iv = crypto.randomBytes(16); const key = crypto.scryptSync(password, Uint8Array.from(iv), 32); const cipher = crypto.createCipheriv("aes-256-cbc", Uint8Array.from(key), Uint8Array.from(iv)); let encrypted = cipher.update(privateKey, "utf8", "hex"); encrypted += cipher.final("hex"); return { encryptedPrivateKey: encrypted, iv: iv.toString("hex"), }; } export async function walletCommand(params = {}) { try { if (!params.action && fs.existsSync(walletFilePath)) { logInfo(params, "šŸ“ Wallet data file found."); const walletsDataString = loadWallets(); if (walletsDataString) { const walletsData = JSON.parse(walletsDataString); if (walletsData.currentWallet) { logWarning(params, `\nšŸ”‘ Current wallet: ${walletsData.currentWallet}`); } } } if (params.action && !params.password) { return { error: "Password is required to import an existing wallet, when using in external mode.", success: false, }; } let runOption = params.action; if (!params.action) { const questions = [ { type: "list", name: "action", message: "What would you like to do?", choices: createWalletOptions, }, ]; const { action } = await inquirer.prompt(questions); runOption = action; } return await processOption({ ...params, action: runOption, }); } catch (error) { console.error(chalk.red("āŒ Error creating or managing wallets:"), chalk.yellow(error.message || error)); } } export async function processOption(params) { switch (params.action) { case "šŸ†• Create a new wallet": { const privateKey = generatePrivateKey(); const prefixedPrivateKey = `0x${privateKey.replace(/^0x/, "")}`; const account = privateKeyToAccount(prefixedPrivateKey); const existingWalletsData = getWalletsData(params); const passwordResult = await createPassword(params.isExternal ?? false, params.password); if (!passwordResult.success) { return { error: passwordResult.error || "Error creating wallet, password required contains an error.", success: false, }; } const finalPassword = passwordResult.password; if (!finalPassword) { return { error: "No valid password received.", success: false, }; } const { encryptedPrivateKey, iv } = encryptPrivateKey(prefixedPrivateKey, finalPassword); let finalWalletName = params.newWalletName; if (!finalWalletName) { const walletNameQuestion = [ { type: "input", name: "walletName", message: "šŸ–‹ļø Enter a name for your wallet:", }, ]; const { walletName } = await inquirer.prompt(walletNameQuestion); if (existingWalletsData.wallets[walletName]) { logError(params, `Wallet named ${walletName} already exists.`); return { error: `Wallet named ${walletName} already exists.`, success: false, }; } finalWalletName = walletName; } if (!finalWalletName) { logError(params, "No wallet name provided."); return { error: "No wallet name provided.", success: false, }; } const walletData = { address: account.address, encryptedPrivateKey: encryptedPrivateKey, iv: iv, }; return await saveWalletData(existingWalletsData, params.isExternal ?? false, params.replaceCurrentWallet ?? false, finalWalletName, walletData, finalPassword); } case "šŸ”‘ Import existing wallet": { const existingWalletsData = getWalletsData(params); let prefixedPrivateKey; if (!params.isExternal && !params.pk) { const inputQuestions = [ { type: "password", name: "privateKey", message: "šŸ”‘ Enter your private key:", mask: "*", }, ]; const { privateKey } = await inquirer.prompt(inputQuestions); prefixedPrivateKey = `0x${privateKey.replace(/^0x/, "")}`; } else { prefixedPrivateKey = `0x${params.pk.replace(/^0x/, "")}`; } const account = privateKeyToAccount(prefixedPrivateKey); if (Object.values(existingWalletsData.wallets).some((wallet) => wallet.address === account.address)) { logError(params, `Wallet with address ${account.address} already saved.`); return { error: `Wallet with address ${account.address} already saved.`, success: false, }; } let finalWalletName = params.newWalletName; if (!params.isExternal) { const walletNameQuestion = [ { type: "input", name: "walletName", message: "šŸ–‹ļø Enter a name for your wallet:", }, ]; const { walletName } = await inquirer.prompt(walletNameQuestion); finalWalletName = walletName; } if (!finalWalletName) { logError(params, "No wallet name provided."); return { error: "No wallet name provided.", success: false, }; } if (existingWalletsData.wallets[finalWalletName]) { logError(params, `Wallet named ${finalWalletName} already exists.`); return { error: `Wallet named ${finalWalletName} already exists.`, success: false, }; } let finalPassword = params.password; if (!params.isExternal) { logInfo(params, "šŸ” Password Requirements:"); logInfo(params, `• At least ${CONFIG.minLength} characters long`); logInfo(params, `• At most ${CONFIG.maxLength} characters long`); logInfo(params, 'Use a strong password with a mix of uppercase and lowercase letters, numbers, and special characters.'); let isValidPassword = false; while (!isValidPassword) { const passwordQuestion = [ { type: "password", name: "password", message: "šŸ”’ Enter a secure password to encrypt your wallet:", mask: "*", }, ]; const { password } = await inquirer.prompt(passwordQuestion); if (!password) { logError(params, "āŒ Password cannot be empty. Please try again."); continue; } const validation = validatePassword(password); if (validation.isValid) { logSuccess(params, "āœ… Password is secure!"); finalPassword = password; isValidPassword = true; } else { logError(params, "āŒ Password validation failed:"); validation.errors.forEach(error => { logError(params, ` ${error}`); }); logWarning(params, "Please try again with a stronger password.\n"); } } } else { if (params.password) { const validation = validatePassword(params.password); if (!validation.isValid) { const errorMessage = `Password validation failed: ${validation.errors.join("; ")}`; return { error: errorMessage, success: false, }; } } } if (!finalPassword) { logError(params, "No password provided."); return { error: "Error creating wallet, password required contains an error.", success: false, }; } const { encryptedPrivateKey, iv } = encryptPrivateKey(prefixedPrivateKey, finalPassword); if (existingWalletsData?.currentWallet) { let finalCurrentWallet = params.replaceCurrentWallet ?? false; if (!params.isExternal) { const setCurrentWalletQuestion = [ { type: "confirm", name: "setCurrentWallet", message: "šŸ” Would you like to set this as the current wallet?", default: true, }, ]; const { setCurrentWallet } = await inquirer.prompt(setCurrentWalletQuestion); finalCurrentWallet = setCurrentWallet ?? false; } if (finalCurrentWallet) { existingWalletsData.currentWallet = finalWalletName; logSuccess(params, "āœ… Wallet set as current!"); } } else { existingWalletsData.currentWallet = finalWalletName; } const walletData = { address: account.address, encryptedPrivateKey: encryptedPrivateKey, iv: iv, }; existingWalletsData.wallets[finalWalletName] = walletData; if (!params.isExternal) { logSuccess(params, "āœ… Wallet validated successfully!"); logInfo(params, "šŸ“„ Address:"); logInfo(params, `${chalk.bold(account.address)}`); writeWalletData(walletFilePath, existingWalletsData); } return { success: true, message: "Wallet imported successfully", walletsData: existingWalletsData, }; } case "šŸ” List saved wallets": { const existingWalletsData = getWalletsData(params); const walletCount = Object.keys(existingWalletsData.wallets).length; if (walletCount === 0) { logError(params, "No wallets found."); return { error: "No wallets found.", success: false, }; } logSuccess(params, `šŸ“œ Saved wallets (${walletCount}):`); Object.keys(existingWalletsData.wallets).forEach((walletName) => { logInfo(params, `- ${walletName}: ${existingWalletsData.wallets[walletName].address}`); }); if (existingWalletsData.currentWallet) { logWarning(params, `\nšŸ”‘ Current wallet: ${existingWalletsData.currentWallet}`); } return { success: true, message: "Wallets listed successfully", walletsData: existingWalletsData, }; } case "šŸ” Switch wallet": { const existingWalletsData = getWalletsData(params); const walletNames = Object.keys(existingWalletsData.wallets); const otherWallets = walletNames.filter((walletName) => walletName !== existingWalletsData.currentWallet); if (otherWallets.length === 0) { logError(params, "No other wallets available to switch to."); return { error: "No other wallets available to switch to.", success: false, }; } let finalWalletName = params.newMainWallet; if (!params.isExternal) { const walletSwitchQuestion = [ { type: "list", name: "walletName", message: "šŸ” Select the wallet you want to switch to:", choices: otherWallets, }, ]; const { walletName } = await inquirer.prompt(walletSwitchQuestion); finalWalletName = walletName; } existingWalletsData.currentWallet = finalWalletName; if (!params.isExternal) { logSuccess(params, `āœ… Successfully switched to wallet: ${finalWalletName}`); console.log(chalk.white(`šŸ“„ Address:`), chalk.green(`${chalk.bold(existingWalletsData.wallets[finalWalletName].address)}`)); writeWalletData(walletFilePath, existingWalletsData); } return { success: true, message: "Wallet switched successfully", walletsData: existingWalletsData, }; } case "āŒ Delete wallet": { const existingWalletsData = getWalletsData(params); const walletNames = Object.keys(existingWalletsData.wallets); const otherWallets = walletNames.filter((walletName) => walletName !== existingWalletsData.currentWallet); if (otherWallets.length === 0) { logError(params, "No other wallets available to delete."); return { error: "No other wallets available to delete.", success: false, }; } let deleteWalletName = params.deleteWalletName; if (!params.isExternal) { logSuccess(params, "šŸ“œ Other available wallets:"); otherWallets.forEach((walletName) => { logInfo(params, `- ${walletName}: ${existingWalletsData.wallets[walletName].address}`); }); const deleteWalletQuestion = [ { type: "list", name: "walletName", message: "āŒ Select the wallet you want to delete:", choices: otherWallets, }, ]; const { walletName } = await inquirer.prompt(deleteWalletQuestion); deleteWalletName = walletName; } if (!deleteWalletName) { logError(params, "No wallet name provided."); return { error: "No wallet name provided.", success: false, }; } if (!existingWalletsData.wallets[deleteWalletName]) { logError(params, `Wallet "${deleteWalletName}" not found.`); return { error: `Wallet "${deleteWalletName}" not found.`, success: false, }; } let confirmDelete = !!params.deleteWalletName; if (!params.isExternal) { const confirmDeleteQuestion = [ { type: "confirm", name: "confirmDelete", message: `ā—ļø Are you sure you want to delete the wallet "${deleteWalletName}"? This action cannot be undone.`, default: false, }, ]; const { confirmDelete: userConfirmDelete } = await inquirer.prompt(confirmDeleteQuestion); confirmDelete = userConfirmDelete ?? false; } if (!confirmDelete) { logWarning(params, "🚫 Wallet deletion cancelled."); return { error: "Wallet deletion cancelled.", success: false, }; } delete existingWalletsData.wallets[deleteWalletName]; if (!params.isExternal) { logError(params, `šŸ—‘ļø Wallet "${deleteWalletName}" has been deleted.`); writeWalletData(walletFilePath, existingWalletsData); } return { success: true, message: `Wallet "${deleteWalletName}" has been deleted successfully.`, walletsData: existingWalletsData, }; } case "šŸ“ Update wallet name": { const existingWalletsData = getWalletsData(params); const walletNames = Object.keys(existingWalletsData.wallets); if (walletNames.length === 0) { logError(params, "No wallets available to update."); return { error: "No wallets available to update.", success: false, }; } let prevWalletName = params.previousWallet; if (!params.isExternal) { logSuccess(params, "šŸ“œ Available wallets:"); walletNames.forEach((walletName) => { const isCurrent = walletName === existingWalletsData.currentWallet ? chalk.yellow(" (Current)") : ""; logInfo(params, `- ${walletName}: ${existingWalletsData.wallets[walletName].address}${isCurrent}`); }); const selectWalletQuestion = [ { type: "list", name: "walletName", message: "šŸ“ Select the wallet you want to update the name for:", choices: walletNames, }, ]; const { walletName } = await inquirer.prompt(selectWalletQuestion); prevWalletName = walletName; } if (!prevWalletName) { if (!params.isExternal) logError(params, "āŒ No wallet selected."); return { error: "No wallet selected.", success: false, }; } if (!existingWalletsData.wallets[prevWalletName]) { if (!params.isExternal) logError(params, `āŒ Wallet "${prevWalletName}" not found.`); return { error: `Wallet "${prevWalletName}" not found.`, success: false, }; } let newWalletName = params.newWalletName; if (!params.isExternal) { const updateNameQuestion = [ { type: "input", name: "newWalletName", message: `šŸ–‹ļø Enter the new name for the wallet "${prevWalletName}":`, }, ]; const { newWalletName: userNewWalletName } = await inquirer.prompt(updateNameQuestion); newWalletName = userNewWalletName; } if (!newWalletName) { if (!params.isExternal) logError(params, "āŒ No new wallet name provided."); return { error: "No new wallet name provided.", success: false, }; } if (existingWalletsData.wallets[newWalletName]) { if (!params.isExternal) { logError(params, `āŒ A wallet with the name "${newWalletName}" already exists.`); } return { error: `A wallet with the name "${newWalletName}" already exists.`, success: false, }; } existingWalletsData.wallets[newWalletName] = existingWalletsData.wallets[prevWalletName]; delete existingWalletsData.wallets[prevWalletName]; if (existingWalletsData.currentWallet === prevWalletName) { existingWalletsData.currentWallet = newWalletName; } if (!params.isExternal) { logSuccess(params, `āœ… Wallet name updated from "${prevWalletName}" to "${newWalletName}".`); writeWalletData(walletFilePath, existingWalletsData); } return { success: true, message: `Wallet name updated from "${prevWalletName}" to "${newWalletName}".`, walletsData: existingWalletsData, }; } case "šŸ“‚ Backup wallet data": { if (params.isExternal) { return { error: "Backup is not available in external mode.", success: false, }; } const backupPathQuestion = [ { type: "input", name: "backupPath", message: "šŸ’¾ Enter the path where you want to save the backup:", }, ]; const { backupPath } = await inquirer.prompt(backupPathQuestion); if (!backupPath) { logError(params, "āš ļø Backup path is required!"); return; } await backupCommand(backupPath); break; } case "šŸ“– Address Book": { if (params.isExternal) { return { error: "Address book is not available in external mode.", success: false, }; } await addressBookCommand(); break; } default: { if (!params.isExternal) logError(params, "āŒ Invalid option selected."); return { error: "Invalid option selected.", success: false, }; } } } export async function writeWalletData(filePath, data) { try { fs.writeFileSync(filePath, JSON.stringify(data, null, 2), "utf8"); console.log(chalk.green(`šŸ’¾ Changes saved at ${filePath}`)); } catch (error) { console.error(chalk.red("āŒ Error saving wallet data:"), chalk.yellow(error.message || error)); } } async function backupCommand(backupPath) { try { if (!fs.existsSync(walletFilePath)) { console.log(chalk.red("🚫 No saved wallet found. Please create a wallet first.")); return; } if (!backupPath) { console.log(chalk.red("āš ļø Please provide a valid file path for backup.")); return; } let absoluteBackupPath = path.resolve(backupPath); const backupDir = path.dirname(absoluteBackupPath); if (fs.existsSync(absoluteBackupPath) && fs.lstatSync(absoluteBackupPath).isDirectory()) { absoluteBackupPath = path.join(absoluteBackupPath, "wallet_backup.json"); console.log(chalk.yellow(`āš ļø Provided a directory. Using default file name: wallet_backup.json`)); } if (!fs.existsSync(backupDir)) { fs.mkdirSync(backupDir, { recursive: true }); console.log(chalk.green(`šŸ“‚ Created backup directory: ${backupDir}`)); } const walletData = JSON.parse(fs.readFileSync(walletFilePath, "utf8")); writeWalletData(absoluteBackupPath, walletData); console.log(chalk.green("āœ… Wallet backup created successfully!"), chalk.green(`\nšŸ’¾ Backup saved successfully at: ${absoluteBackupPath}`)); } catch (error) { console.error(chalk.red("🚨 Error during wallet backup:"), chalk.yellow(error.message)); } } async function createPassword(_isExternal, _password) { let finalPassword = _password; const params = { isExternal: _isExternal, password: _password, }; if (!_isExternal) { logInfo(params, "šŸ” Password Requirements:"); logInfo(params, `• At least ${CONFIG.minLength} characters long`); logInfo(params, `• At most ${CONFIG.maxLength} characters long`); logInfo(params, 'Use a strong password with a mix of uppercase and lowercase letters, numbers, and special characters.'); let isValidPassword = false; while (!isValidPassword) { const passwordQuestion = [ { type: "password", name: "password", message: "šŸ”’ Enter a secure password to encrypt your wallet:", mask: "*", }, ]; const { password } = await inquirer.prompt(passwordQuestion); if (!password) { logError(params, "āŒ Password cannot be empty. Please try again."); continue; } const validation = validatePassword(password); if (validation.isValid) { logSuccess(params, "āœ… Password is secure!"); finalPassword = password; isValidPassword = true; } else { logError(params, "āŒ Password validation failed:"); validation.errors.forEach(error => { logError(params, ` ${error}`); }); logWarning(params, "Please try again with a stronger password.\n"); } } } else { if (_password) { const validation = validatePassword(_password); if (!validation.isValid) { const errorMessage = `Password validation failed: ${validation.errors.join("; ")}`; return { success: false, error: errorMessage }; } } } if (!finalPassword) { return { success: false, error: "No password provided" }; } logSuccess(params, "šŸŽ‰ Wallet created successfully on Rootstock!"); return { success: true, password: finalPassword }; } async function saveWalletData(existingWalletsData, isExternal, replaceCurrentWallet, finalWalletName, walletData, prefixedPrivateKey) { const params = { isExternal: isExternal }; if (existingWalletsData?.currentWallet) { if (isExternal) { existingWalletsData.currentWallet = replaceCurrentWallet ? finalWalletName : existingWalletsData.currentWallet; } else { const setCurrentWalletQuestion = [ { type: "confirm", name: "setCurrentWallet", message: "šŸ” Would you like to set this as the current wallet?", default: true, }, ]; const { setCurrentWallet } = await inquirer.prompt(setCurrentWalletQuestion); if (setCurrentWallet) { existingWalletsData.currentWallet = finalWalletName; logSuccess(params, "āœ… Wallet set as current!"); } } } else { existingWalletsData.currentWallet = finalWalletName; } existingWalletsData.wallets[finalWalletName] = walletData; if (!isExternal) { logInfo(params, "šŸ“„ Address:"); logInfo(params, `${chalk.bold(walletData.address)}`); logInfo(params, "šŸ”‘ Private Key:"); logInfo(params, `${chalk.bold(prefixedPrivateKey)}`); logInfo(params, "šŸ”’ Please save the private key in a secure location."); writeWalletData(walletFilePath, existingWalletsData); return; } return { success: true, message: "Wallet saved successfully", walletsData: existingWalletsData, }; }