Beginner2 hours
CLI Wallet
Build a command-line wallet for Beatoz
CLI Wallet Toy Project
In this project, you'll build a command-line wallet using Node.js and the @beatoz/web3 SDK.
Learning Objectives
- Create and manage accounts
- Encrypt and store private keys
- Check account balances
- Transfer tokens
Project Structure
beatoz-cli-wallet/
├── package.json
├── src/
│ ├── index.js # Main entry
│ ├── commands/
│ │ ├── account.js # Account commands
│ │ ├── balance.js # Balance check
│ │ └── transfer.js # Transfer
│ ├── utils/
│ │ ├── crypto.js # Encryption
│ │ └── storage.js # Storage
│ └── config.js # Configuration
├── .env
└── README.md
Getting Started
1. Create Project
mkdir beatoz-cli-wallet
cd beatoz-cli-wallet
npm init -y
2. Install Dependencies
npm install @beatoz/web3 commander inquirer chalk dotenv
npm install --save-dev @types/node
Core Code
package.json
{
"name": "beatoz-cli-wallet",
"version": "1.0.0",
"type": "module",
"bin": {
"beatoz-wallet": "./src/index.js"
},
"scripts": {
"start": "node src/index.js",
"wallet": "node src/index.js"
},
"dependencies": {
"@beatoz/web3": "^1.0.0",
"commander": "^11.0.0",
"inquirer": "^9.0.0",
"chalk": "^5.0.0",
"dotenv": "^16.0.0"
}
}
src/config.js
import dotenv from 'dotenv';
dotenv.config();
export const config = {
networks: {
testnet: {
name: 'Beatoz Testnet',
rpc: 'https://rpc-testnet0.beatoz.io',
chainId: '0xbea701',
},
},
defaultNetwork: process.env.NETWORK || 'testnet',
walletPath: process.env.WALLET_PATH || './.wallet',
};
src/index.js
#!/usr/bin/env node
import { Command } from 'commander';
import chalk from 'chalk';
import { accountCommands } from './commands/account.js';
import { balanceCommands } from './commands/balance.js';
import { transferCommands } from './commands/transfer.js';
const program = new Command();
program
.name('beatoz-wallet')
.description('CLI wallet for Beatoz blockchain')
.version('1.0.0');
// Account commands
program
.command('create')
.description('Create a new account')
.action(accountCommands.create);
program
.command('import')
.description('Import account from private key')
.action(accountCommands.importKey);
program
.command('list')
.description('List all accounts')
.action(accountCommands.list);
program
.command('export <address>')
.description('Export private key')
.action(accountCommands.exportKey);
// Balance commands
program
.command('balance [address]')
.description('Check account balance')
.action(balanceCommands.check);
// Transfer commands
program
.command('send')
.description('Send BTZ to another address')
.option('-t, --to <address>', 'Recipient address')
.option('-a, --amount <amount>', 'Amount to send')
.option('-f, --from <address>', 'Sender address')
.action(transferCommands.send);
// Network info
program
.command('network')
.description('Show network information')
.action(async () => {
const { Web3 } = await import('@beatoz/web3');
const { config } = await import('./config.js');
const network = config.networks[config.defaultNetwork];
const web3 = new Web3(network.rpc);
try {
const status = await web3.beatoz.status();
console.log(chalk.cyan('\n📡 Network Information\n'));
console.log(` Network: ${chalk.green(network.name)}`);
console.log(` Chain ID: ${chalk.yellow(status.node_info.network)}`);
console.log(` Latest Block: ${chalk.white(status.sync_info.latest_block_height)}`);
console.log(` RPC: ${chalk.gray(network.rpc)}\n`);
} catch (error) {
console.error(chalk.red('Failed to connect to network'));
}
});
program.parse();
src/commands/account.js
import { Web3 } from '@beatoz/web3';
import inquirer from 'inquirer';
import chalk from 'chalk';
import crypto from 'crypto';
import fs from 'fs';
import { config } from '../config.js';
const WALLET_FILE = config.walletPath + '/accounts.json';
function ensureWalletDir() {
if (!fs.existsSync(config.walletPath)) {
fs.mkdirSync(config.walletPath, { recursive: true });
}
}
function loadAccounts() {
ensureWalletDir();
if (!fs.existsSync(WALLET_FILE)) {
return [];
}
return JSON.parse(fs.readFileSync(WALLET_FILE, 'utf-8'));
}
function saveAccounts(accounts) {
ensureWalletDir();
fs.writeFileSync(WALLET_FILE, JSON.stringify(accounts, null, 2));
}
function encrypt(text, password) {
const iv = crypto.randomBytes(16);
const key = crypto.scryptSync(password, 'salt', 32);
const cipher = crypto.createCipheriv('aes-256-cbc', key, iv);
let encrypted = cipher.update(text, 'utf8', 'hex');
encrypted += cipher.final('hex');
return iv.toString('hex') + ':' + encrypted;
}
function decrypt(encrypted, password) {
const [ivHex, encryptedText] = encrypted.split(':');
const iv = Buffer.from(ivHex, 'hex');
const key = crypto.scryptSync(password, 'salt', 32);
const decipher = crypto.createDecipheriv('aes-256-cbc', key, iv);
let decrypted = decipher.update(encryptedText, 'hex', 'utf8');
decrypted += decipher.final('utf8');
return decrypted;
}
export const accountCommands = {
async create() {
const network = config.networks[config.defaultNetwork];
const web3 = new Web3(network.rpc);
const { password, confirmPassword, name } = await inquirer.prompt([
{
type: 'password',
name: 'password',
message: 'Enter password to encrypt the account:',
mask: '*',
},
{
type: 'password',
name: 'confirmPassword',
message: 'Confirm password:',
mask: '*',
},
{
type: 'input',
name: 'name',
message: 'Account name (optional):',
default: 'Account',
},
]);
if (password !== confirmPassword) {
console.log(chalk.red('Passwords do not match!'));
return;
}
const account = web3.beatoz.accounts.create();
const encryptedKey = encrypt(account.privateKey, password);
const accounts = loadAccounts();
accounts.push({
name: name || `Account ${accounts.length + 1}`,
address: account.address,
encryptedKey,
createdAt: new Date().toISOString(),
});
saveAccounts(accounts);
console.log(chalk.green('\n✅ Account created successfully!\n'));
console.log(` Name: ${chalk.cyan(name)}`);
console.log(` Address: ${chalk.yellow(account.address)}\n`);
console.log(chalk.gray(' Your private key is encrypted and stored locally.\n'));
},
async importKey() {
const { privateKey, password, name } = await inquirer.prompt([
{
type: 'password',
name: 'privateKey',
message: 'Enter private key:',
mask: '*',
},
{
type: 'password',
name: 'password',
message: 'Enter password to encrypt:',
mask: '*',
},
{
type: 'input',
name: 'name',
message: 'Account name:',
default: 'Imported Account',
},
]);
const network = config.networks[config.defaultNetwork];
const web3 = new Web3(network.rpc);
try {
const account = web3.beatoz.accounts.privateKeyToAccount(privateKey);
const encryptedKey = encrypt(privateKey, password);
const accounts = loadAccounts();
accounts.push({
name,
address: account.address,
encryptedKey,
createdAt: new Date().toISOString(),
});
saveAccounts(accounts);
console.log(chalk.green('\n✅ Account imported successfully!\n'));
console.log(` Address: ${chalk.yellow(account.address)}\n`);
} catch (error) {
console.log(chalk.red('Invalid private key'));
}
},
async list() {
const accounts = loadAccounts();
if (accounts.length === 0) {
console.log(chalk.yellow('\nNo accounts found. Create one with: beatoz-wallet create\n'));
return;
}
console.log(chalk.cyan('\n📋 Your Accounts\n'));
accounts.forEach((acc, index) => {
console.log(` ${index + 1}. ${chalk.white(acc.name)}`);
console.log(` ${chalk.yellow(acc.address)}`);
console.log(` Created: ${chalk.gray(new Date(acc.createdAt).toLocaleDateString())}\n`);
});
},
async exportKey(address) {
const accounts = loadAccounts();
const account = accounts.find(a => a.address.toLowerCase() === address.toLowerCase());
if (!account) {
console.log(chalk.red('Account not found'));
return;
}
const { password, confirm } = await inquirer.prompt([
{
type: 'password',
name: 'password',
message: 'Enter password:',
mask: '*',
},
{
type: 'confirm',
name: 'confirm',
message: chalk.red('⚠️ This will display your private key. Continue?'),
default: false,
},
]);
if (!confirm) return;
try {
const privateKey = decrypt(account.encryptedKey, password);
console.log(chalk.yellow(`\nPrivate Key: ${privateKey}\n`));
console.log(chalk.red('⚠️ Never share this key with anyone!\n'));
} catch (error) {
console.log(chalk.red('Incorrect password'));
}
},
};
src/commands/balance.js
import { Web3 } from '@beatoz/web3';
import chalk from 'chalk';
import { config } from '../config.js';
import fs from 'fs';
const WALLET_FILE = config.walletPath + '/accounts.json';
function loadAccounts() {
if (!fs.existsSync(WALLET_FILE)) return [];
return JSON.parse(fs.readFileSync(WALLET_FILE, 'utf-8'));
}
export const balanceCommands = {
async check(address) {
const network = config.networks[config.defaultNetwork];
const web3 = new Web3(network.rpc);
// If no address, show all accounts
if (!address) {
const accounts = loadAccounts();
if (accounts.length === 0) {
console.log(chalk.yellow('No accounts found'));
return;
}
console.log(chalk.cyan('\n💰 Account Balances\n'));
for (const acc of accounts) {
try {
const account = await web3.beatoz.getAccount(acc.address);
const balance = web3.utils.fromFons(account.value.balance, 'beatoz');
console.log(` ${chalk.white(acc.name)}`);
console.log(` ${chalk.gray(acc.address)}`);
console.log(` Balance: ${chalk.green(balance)} BTZ\n`);
} catch (error) {
console.log(` ${chalk.white(acc.name)}`);
console.log(` ${chalk.gray(acc.address)}`);
console.log(` Balance: ${chalk.green('0')} BTZ\n`);
}
}
} else {
// Show specific address balance
try {
const account = await web3.beatoz.getAccount(address);
const balance = web3.utils.fromFons(account.value.balance, 'beatoz');
console.log(chalk.cyan('\n💰 Account Balance\n'));
console.log(` Address: ${chalk.gray(address)}`);
console.log(` Balance: ${chalk.green(balance)} BTZ\n`);
} catch (error) {
console.log(chalk.cyan('\n💰 Account Balance\n'));
console.log(` Address: ${chalk.gray(address)}`);
console.log(` Balance: ${chalk.green('0')} BTZ\n`);
}
}
},
};
src/commands/transfer.js
import { Web3, TrxProtoBuilder } from '@beatoz/web3';
import inquirer from 'inquirer';
import chalk from 'chalk';
import crypto from 'crypto';
import fs from 'fs';
import { config } from '../config.js';
const WALLET_FILE = config.walletPath + '/accounts.json';
function loadAccounts() {
if (!fs.existsSync(WALLET_FILE)) return [];
return JSON.parse(fs.readFileSync(WALLET_FILE, 'utf-8'));
}
function decrypt(encrypted, password) {
const [ivHex, encryptedText] = encrypted.split(':');
const iv = Buffer.from(ivHex, 'hex');
const key = crypto.scryptSync(password, 'salt', 32);
const decipher = crypto.createDecipheriv('aes-256-cbc', key, iv);
let decrypted = decipher.update(encryptedText, 'hex', 'utf8');
decrypted += decipher.final('utf8');
return decrypted;
}
export const transferCommands = {
async send(options) {
const accounts = loadAccounts();
if (accounts.length === 0) {
console.log(chalk.yellow('No accounts found. Create one first.'));
return;
}
const network = config.networks[config.defaultNetwork];
const web3 = new Web3(network.rpc);
// Interactive prompts
const answers = await inquirer.prompt([
{
type: 'list',
name: 'fromAccount',
message: 'Select sender account:',
choices: accounts.map(a => ({
name: `${a.name} (${a.address.slice(0, 10)}...)`,
value: a,
})),
when: !options.from,
},
{
type: 'input',
name: 'to',
message: 'Recipient address:',
when: !options.to,
validate: (input) => /^(0x)?[0-9a-fA-F]{40}$/.test(input) || 'Invalid address',
},
{
type: 'input',
name: 'amount',
message: 'Amount (BTZ):',
when: !options.amount,
validate: (input) => !isNaN(parseFloat(input)) || 'Invalid amount',
},
{
type: 'password',
name: 'password',
message: 'Enter password:',
mask: '*',
},
]);
const fromAccount = answers.fromAccount ||
accounts.find(a => a.address.toLowerCase() === options.from?.toLowerCase());
const toAddress = options.to || answers.to;
const amount = options.amount || answers.amount;
if (!fromAccount) {
console.log(chalk.red('Sender account not found'));
return;
}
try {
// Decrypt private key
const privateKey = decrypt(fromAccount.encryptedKey, answers.password);
const sender = web3.beatoz.accounts.privateKeyToAccount(privateKey);
// Get account info
const accountInfo = await web3.beatoz.getAccount(sender.address);
const status = await web3.beatoz.status();
const balance = web3.utils.fromFons(accountInfo.value.balance, 'beatoz');
if (parseFloat(balance) < parseFloat(amount)) {
console.log(chalk.red(`Insufficient balance: ${balance} BTZ`));
return;
}
// Build transaction
const tx = TrxProtoBuilder.buildTransferTrxProto({
from: sender.address,
to: toAddress,
nonce: accountInfo.value.nonce,
gas: 100000,
gasPrice: '48000000000',
amount: web3.utils.toFons(amount, 'beatoz'),
});
// Sign and broadcast
TrxProtoBuilder.signTrxProto(tx, sender, status.node_info.network);
console.log(chalk.cyan('\n📤 Sending transaction...\n'));
const result = await web3.beatoz.broadcastTxCommit(tx);
if ((result.check_tx?.code ?? 0) !== 0 || (result.deliver_tx?.code ?? 0) !== 0) {
console.log(chalk.red(`Transaction failed: ${result.check_tx?.log || result.deliver_tx?.log}`));
return;
}
console.log(chalk.green('✅ Transaction sent!\n'));
console.log(` From: ${chalk.gray(sender.address)}`);
console.log(` To: ${chalk.gray(toAddress)}`);
console.log(` Amount: ${chalk.yellow(amount)} BTZ`);
console.log(` TX Hash: ${chalk.cyan(result.hash)}\n`);
} catch (error) {
if (error.message.includes('decipher')) {
console.log(chalk.red('Incorrect password'));
} else {
console.log(chalk.red(`Error: ${error.message}`));
}
}
},
};
Usage Examples
# Check network information
node src/index.js network
# Create new account
node src/index.js create
# Import account from private key
node src/index.js import
# List all accounts
node src/index.js list
# Check balance
node src/index.js balance
# Check specific address balance
node src/index.js balance 0x...
# Send BTZ
node src/index.js send
# Send with options
node src/index.js send -t 0x... -a 1.5
CLI Wallet Complete! 🎉
