@rsksmart/rsk-cli
Version:
CLI tool for Rootstock network using Viem
613 lines (612 loc) ⢠26.2 kB
JavaScript
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";
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)) {
console.log(chalk.grey("š Wallet data file found."));
const walletsDataString = loadWallets();
if (walletsDataString) {
const walletsData = JSON.parse(walletsDataString);
if (walletsData.currentWallet) {
console.log(chalk.yellow(`\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 finalPassword = await createPassword(params.isExternal ?? false, params.password);
if (!finalPassword) {
return {
error: "Error creating wallet, password required contains an error.",
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, prefixedPrivateKey);
}
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) {
const passwordQuestion = [
{
type: "password",
name: "password",
message: "š Enter a password to encrypt your wallet:",
mask: "*",
},
];
const { password } = await inquirer.prompt(passwordQuestion);
finalPassword = password;
}
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!");
console.log(chalk.white(`š Address:`), chalk.green(`${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) {
console.log(chalk.green("š Available wallets:"));
walletNames.forEach((walletName) => {
const isCurrent = walletName === existingWalletsData.currentWallet
? chalk.yellow(" (Current)")
: "";
console.log(chalk.blue(`- ${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)
console.log(chalk.red("ā No wallet selected."));
return {
error: "No wallet selected.",
success: false,
};
}
if (!existingWalletsData.wallets[prevWalletName]) {
if (!params.isExternal)
console.log(chalk.red(`ā 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)
console.log(chalk.red("ā No new wallet name provided."));
return {
error: "No new wallet name provided.",
success: false,
};
}
if (existingWalletsData.wallets[newWalletName]) {
if (!params.isExternal) {
console.log(chalk.red(`ā 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) {
console.log(chalk.green(`ā
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) {
console.log(chalk.red("ā ļø 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)
console.log(chalk.red("ā 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;
if (!_isExternal) {
console.log(chalk.rgb(255, 165, 0)(`š Wallet created successfully on Rootstock!`));
const passwordQuestion = [
{
type: "password",
name: "password",
message: "š Enter a password to encrypt your wallet:",
mask: "*",
},
];
const { password } = await inquirer.prompt(passwordQuestion);
finalPassword = password;
}
return finalPassword;
}
async function saveWalletData(existingWalletsData, isExternal, replaceCurrentWallet, finalWalletName, walletData, prefixedPrivateKey) {
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;
console.log(chalk.green("ā
Wallet set as current!"));
}
}
}
else {
existingWalletsData.currentWallet = finalWalletName;
}
existingWalletsData.wallets[finalWalletName] = walletData;
if (!isExternal) {
console.log(chalk.white(`š Address:`), chalk.green(`${chalk.bold(walletData.address)}`));
console.log(chalk.white(`š Private Key:`), chalk.green(`${chalk.bold(prefixedPrivateKey)}`));
console.log(chalk.gray("š Please save the private key in a secure location."));
writeWalletData(walletFilePath, existingWalletsData);
return;
}
return {
success: true,
message: "Wallet saved successfully",
walletsData: existingWalletsData,
};
}