@nano/wallet
Version:
Enterprise Nano Wallet — Browser, Node.js & CLI. Zero external dependencies.
449 lines (402 loc) • 14 kB
JavaScript
#!/usr/bin/env node
// @nano/wallet CLI
// Usage: nano-wallet <command> [options]
const nano = require('../nano.js');
const fs = require('fs');
const path = require('path');
const args = process.argv.slice(2);
const command = args[0];
const DEFAULT_WALLET = path.join(process.cwd(), 'nano-wallet.dat');
// ── Helpers ──────────────────────────────────────────────
function usage() {
console.log(`
@nano/wallet CLI — Enterprise Nano Wallet
Usage: nano-wallet <command> [options]
Commands:
generate Generate new wallet and save locally
address Print wallet address and nano.to receive link
receive Receive all pending blocks (pockets funds)
send <to> <amount> Send Nano (signs, publishes, confirms <1s)
change_rep [rep_address] Change representative (auto-picks if omitted)
balance Get balance for wallet address
faucet [address] Claim 0.001 free test NANO from the faucet
export [format] Export wallet (seed, mnemonic, nault)
account_info Get account info from RPC
rpc <action> [key=value ...] Raw RPC call to rpc.nano.to
convert <amount> <from> <to> Convert units (NANO/RAW)
encrypt <file> <password> Encrypt a file with AES-256
decrypt <file> <password> Decrypt an AES-256 encrypted file
sign <block_json> <private_key> Sign a block
pow <hash> Generate Proof of Work
Export Formats:
export seed Export wallet seed (default)
export mnemonic Export mnemonic recovery phrase
export nault <password> Export as Nault.cc import URL
Options:
--secret <password> Wallet password (encrypts/decrypts wallet file)
--wallet <file> Wallet file path (default: ./nano-wallet.dat)
--node <url> RPC endpoint (default: https://rpc.nano.to)
--key <api_key> RPC API key for rpc.nano.to
--json Output raw JSON
--help Show this help message
Environment Variables:
NANO_SECRET Wallet password (alternative to --secret)
NANO_WALLET Wallet file path (alternative to --wallet)
NANO_RPC RPC endpoint (default: https://rpc.nano.to)
NANO_RPC_KEY API key for rpc.nano.to
Quick Start:
nano-wallet generate --secret mypassword
nano-wallet faucet --secret mypassword
nano-wallet receive --secret mypassword
nano-wallet send nano_1to... 0.001 --secret mypassword
With Environment Variables:
export NANO_SECRET=mypassword
nano-wallet generate
nano-wallet receive
nano-wallet send nano_1to... 0.001
nano-wallet balance
`);
}
function getFlag(name) {
const idx = args.indexOf('--' + name);
if (idx === -1) return null;
return args[idx + 1] || true;
}
function hasFlag(name) {
return args.includes('--' + name);
}
// Get positional args (skipping flags and their values)
function getPositional() {
const positional = [];
for (let i = 1; i < args.length; i++) {
if (args[i].startsWith('--')) { i++; continue; }
positional.push(args[i]);
}
return positional;
}
function jsonOut(data) {
console.log(JSON.stringify(data, null, 2));
}
function setupRpc() {
const node = getFlag('node') || process.env.NANO_RPC || 'https://rpc.nano.to';
const key = getFlag('key') || process.env.NANO_RPC_KEY || '';
nano.endpoint = node;
nano.rpc_key = key;
}
function getSecret() {
return getFlag('secret') || process.env.NANO_SECRET || null;
}
function getWalletPath() {
return getFlag('wallet') || process.env.NANO_WALLET || DEFAULT_WALLET;
}
function loadWallet() {
const secret = getSecret();
const walletPath = getWalletPath();
if (!secret) {
console.error('Error: wallet password required. Use --secret <password> or set NANO_SECRET');
process.exit(1);
}
if (!fs.existsSync(walletPath)) {
console.error(`Error: wallet file not found at ${walletPath}`);
console.error('Run "nano-wallet generate --secret <password>" first.');
process.exit(1);
}
nano.offline({ database: walletPath, secret, node: nano.endpoint });
return nano.wallets;
}
// ── Commands ─────────────────────────────────────────────
async function main() {
if (!command || command === '--help' || command === '-h' || command === 'help') {
return usage();
}
setupRpc();
switch (command) {
case 'generate': {
const secret = getSecret();
const walletPath = getWalletPath();
if (!secret) {
console.error('Error: password required to encrypt wallet. Use --secret <password> or set NANO_SECRET');
process.exit(1);
}
if (fs.existsSync(walletPath)) {
console.error(`Error: wallet already exists at ${walletPath}`);
console.error('Delete it first or use --wallet <path> to save elsewhere.');
process.exit(1);
}
// Generate, encrypt, and save in one step
nano.offline({ database: walletPath, secret, node: nano.endpoint });
// Retrieve the full wallet data (including mnemonic) for display
const walletData = nano.wallet();
console.log(`Wallet saved to ${walletPath}`);
console.log(`Address: ${walletData.accounts[0].address}`);
console.log(`Mnemonic: ${walletData.mnemonic}`);
console.log('');
console.log('IMPORTANT: Save your mnemonic phrase. It is the only way to recover your wallet.');
if (hasFlag('json')) {
jsonOut({
mnemonic: walletData.mnemonic,
seed: walletData.seed,
accounts: walletData.accounts.map(a => ({
index: a.accountIndex,
address: a.address,
private: a.private,
public: a.public
}))
});
}
break;
}
case 'balance': {
const pos = getPositional();
let address = pos[0];
if (!address) {
const wallets = loadWallet();
address = wallets[0].address;
}
const res = await nano.rpc({ action: 'account_balance', account: address });
if (res.error) return console.error('Error:', res.error);
res.balance_nano = nano.convert(res.balance, 'RAW', 'NANO');
res.receivable_nano = nano.convert(res.receivable || res.pending || '0', 'RAW', 'NANO');
jsonOut(res);
break;
}
case 'account_info': {
const pos = getPositional();
let address = pos[0];
if (!address) {
const wallets = loadWallet();
address = wallets[0].address;
}
const res = await nano.rpc({ action: 'account_info', account: address, representative: 'true' });
jsonOut(res);
break;
}
case 'rpc': {
const pos = getPositional();
const action = pos[0];
if (!action) return console.error('Error: action required. Usage: nano-wallet rpc <action> [key=value ...]');
const body = { action };
for (let i = 1; i < pos.length; i++) {
const [key, ...rest] = pos[i].split('=');
if (rest.length) body[key] = rest.join('=');
}
const res = await nano.rpc(body);
jsonOut(res);
break;
}
case 'convert': {
const pos = getPositional();
const amount = pos[0];
const from = pos[1];
const to = pos[2];
if (!amount || !from || !to) return console.error('Usage: nano-wallet convert <amount> <from> <to>\nExample: nano-wallet convert 1.5 NANO RAW');
try {
const result = nano.convert(amount, from, to);
console.log(result);
} catch (e) {
console.error('Error:', e.message);
}
break;
}
case 'send': {
const pos = getPositional();
const to = pos[0];
const amount = pos[1];
if (!to || !amount) return console.error('Usage: nano-wallet send <to> <amount>');
const wallets = loadWallet();
const from = wallets[0].address;
try {
const res = await nano.send({ from, to, amount });
if (!res || !res.length) return console.error('Error: send returned no results');
for (const block of res) {
console.log(`Sent ${block.amount} NANO`);
console.log(` to: ${block.to}`);
console.log(` from: ${block.from}`);
console.log(` hash: ${block.hash}`);
console.log(` view: ${block.browser}`);
}
if (hasFlag('json')) jsonOut(res);
} catch (e) {
console.error('Error:', e.message);
process.exit(1);
}
break;
}
case 'receive': {
const wallets = loadWallet();
try {
const blocks = await nano.receive();
if (!blocks || !blocks.length) {
console.log('No pending blocks to receive.');
break;
}
console.log(`Received ${blocks.length} block(s):`);
for (const block of blocks) {
console.log(` ${block.amount_nano} NANO — hash: ${block.hash}`);
}
if (hasFlag('json')) jsonOut(blocks);
} catch (e) {
console.error('Error:', e.message);
process.exit(1);
}
break;
}
case 'change_rep': {
const wallets = loadWallet();
const pos = getPositional();
const rep = pos[0] || undefined;
try {
const res = await nano.change_rep({ rep });
console.log(`Representative changed!`);
console.log(` rep: ${res.representative}`);
console.log(` account: ${res.account}`);
if (res.hash) console.log(` hash: ${res.hash}`);
if (res.browser) console.log(` view: ${res.browser}`);
if (hasFlag('json')) jsonOut(res);
} catch (e) {
console.error('Error:', e.message);
process.exit(1);
}
break;
}
case 'address': {
const wallets = loadWallet();
const address = wallets[0].address;
console.log(`Address: ${address}`);
console.log(`Receive: https://nano.to/${address}`);
if (hasFlag('json')) jsonOut({ address, receive_url: `https://nano.to/${address}` });
break;
}
case 'faucet': {
const pos = getPositional();
let address = pos[0];
if (!address) {
const wallets = loadWallet();
address = wallets[0].address;
}
if (!address) return console.error('Error: No address. Provide one or generate a wallet first.');
try {
const res = await nano.faucet(address);
if (res.error) return console.error('Error:', res.error);
console.log(`Faucet claimed! ${res.claim?.amount || '0.001'} NANO is on the way.`);
if (res.claim?.tx_hash) console.log(` hash: ${res.claim.tx_hash}`);
if (res.claim?.nano_address) console.log(` to: ${res.claim.nano_address}`);
if (hasFlag('json')) jsonOut(res);
} catch (e) {
console.error('Error:', e.message);
process.exit(1);
}
break;
}
case 'export': {
const wallets = loadWallet();
const pos = getPositional();
const format = (pos[0] || 'seed').toLowerCase();
const walletData = nano.wallet();
if (format === 'seed') {
if (!walletData.seed) {
console.error('Error: no seed found in wallet (wallet may have been imported without a seed).');
process.exit(1);
}
if (hasFlag('json')) {
jsonOut({ seed: walletData.seed });
} else {
console.log(walletData.seed);
}
} else if (format === 'mnemonic' || format === 'nemonic') {
if (!walletData.mnemonic) {
console.error('Error: no mnemonic found in wallet (wallet may have been imported from a seed).');
process.exit(1);
}
if (hasFlag('json')) {
jsonOut({ mnemonic: walletData.mnemonic });
} else {
console.log(walletData.mnemonic);
}
} else if (format === 'nault') {
const naultPassword = pos[1] || getFlag('password');
if (!naultPassword) {
console.error('Usage: nano-wallet export nault <password>');
console.error('A password is required to encrypt the wallet for Nault.');
process.exit(1);
}
const url = nano.nault(naultPassword);
if (url instanceof Error) {
console.error('Error:', url.message);
process.exit(1);
}
if (hasFlag('json')) {
jsonOut({ url, format: 'nault' });
} else {
console.log(url);
}
} else {
console.error(`Unknown export format: ${format}`);
console.error('Available formats: seed, mnemonic, nault');
process.exit(1);
}
break;
}
case 'encrypt': {
const pos = getPositional();
const file = pos[0];
const password = pos[1];
if (!file || !password) return console.error('Usage: nano-wallet encrypt <file> <password>');
try {
const content = fs.readFileSync(file, 'utf8');
console.log(nano.encrypt(content, password));
} catch (e) {
console.error('Error:', e.message);
}
break;
}
case 'decrypt': {
const pos = getPositional();
const file = pos[0];
const password = pos[1];
if (!file || !password) return console.error('Usage: nano-wallet decrypt <file> <password>');
try {
const content = fs.readFileSync(file, 'utf8');
const decrypted = nano.decrypt(content, password);
console.log(typeof decrypted === 'string' ? decrypted : JSON.stringify(decrypted, null, 2));
} catch (e) {
console.error('Error:', e.message);
}
break;
}
case 'sign': {
const pos = getPositional();
const blockJson = pos[0];
const privateKey = pos[1];
if (!blockJson || !privateKey) return console.error('Usage: nano-wallet sign <block_json> <private_key>');
try {
const block = JSON.parse(blockJson);
const signed = nano.sign(block, privateKey);
jsonOut(signed);
} catch (e) {
console.error('Error:', e.message);
}
break;
}
case 'pow': {
const pos = getPositional();
const hash = pos[0];
if (!hash) return console.error('Usage: nano-wallet pow <hash>');
try {
const work = await nano.pow({ frontier: hash });
console.log(work);
} catch (e) {
console.error('Error:', e.message);
}
break;
}
default:
console.error(`Unknown command: ${command}`);
usage();
process.exit(1);
}
}
main().catch(err => {
console.error('Error:', err.message);
process.exit(1);
});