UNPKG

mnee-cli

Version:
713 lines (711 loc) 27.5 kB
#!/usr/bin/env node import { Command } from 'commander'; import inquirer from 'inquirer'; import crypto from 'crypto'; import { PrivateKey } from '@bsv/sdk'; import { decryptPrivateKey, encryptPrivateKey } from './utils/crypto.js'; import { getActiveWallet, getAllWallets, saveWallets, setActiveWallet, getWalletByAddress, setPrivateKey, deletePrivateKey, getPrivateKey, clearActiveWallet, getLegacyWallet, deleteLegacyWallet, } from './utils/keytar.js'; import { getVersion, singleLineLogger } from './utils/helper.js'; import Mnee from 'mnee'; import { readTxHistoryCache, writeTxHistoryCache, clearTxHistoryCache } from './utils/cache.js'; const getMneeInstance = (environment, apiKey) => { return new Mnee({ environment, apiKey }); }; const safePrompt = async (questions) => { try { return await inquirer.prompt(questions); } catch { console.log('\n❌ Operation cancelled by user.'); process.exit(1); } }; const program = new Command(); if (!process.argv.slice(2).length) { console.log(` __ __ __ __ ________ ________ ______ __ ______ / \\ / |/ \\ / |/ |/ | / \\ / | / | $$ \\ /$$ |$$ \\ $$ |$$$$$$$$/ $$$$$$$$/ /$$$$$$ |$$ | $$$$$$/ $$$ \\ /$$$ |$$$ \\$$ |$$ |__ $$ |__ $$ | $$/ $$ | $$ | $$$$ /$$$$ |$$$$ $$ |$$ | $$ | $$ | $$ | $$ | $$ $$ $$/$$ |$$ $$ $$ |$$$$$/ $$$$$/ $$ | __ $$ | $$ | $$ |$$$/ $$ |$$ |$$$$ |$$ |_____ $$ |_____ $$ \\__/ |$$ |_____ _$$ |_ $$ | $/ $$ |$$ | $$$ |$$ |$$ | $$ $$/ $$ |/ $$ | $$/ $$/ $$/ $$/ $$$$$$$$/ $$$$$$$$/ $$$$$$/ $$$$$$$$/ $$$$$$/ `); } program.name('mnee').description('CLI for interacting with MNEE tokens').version(getVersion()); // Add error handling for the main program program.exitOverride((err) => { if (err.code === 'commander.help') { process.exit(0); } process.exit(err.exitCode); }); program .command('create') .description('Generate a new wallet and store keys securely') .action(async () => { try { const existingWallets = await getAllWallets(); const { environment } = await safePrompt([ { type: 'list', name: 'environment', message: 'Select wallet environment:', choices: [ { name: 'Production', value: 'production' }, { name: 'Sandbox', value: 'sandbox' }, ], default: 'production', }, ]); const { walletName } = await safePrompt([ { type: 'input', name: 'walletName', message: `Enter a name for your ${environment} wallet:`, default: `${environment}-wallet-${Date.now()}`, validate: (input) => { const validation = validateWalletName(input); if (!validation.isValid) { return validation.error || 'Invalid wallet name'; } if (existingWallets.some((w) => w.name === input)) { return `A wallet with name "${input}" already exists`; } return true; }, }, ]); const entropy = crypto.randomBytes(32); const privateKey = PrivateKey.fromString(entropy.toString('hex')); const address = privateKey.toAddress(); const { password, confirmPassword } = await safePrompt([ { type: 'password', name: 'password', message: 'Set a password for your wallet:', mask: '*', }, { type: 'password', name: 'confirmPassword', message: 'Confirm your password:', mask: '*', }, ]); if (password !== confirmPassword) { console.error('❌ Passwords do not match. Try again.'); return; } const encryptedKey = encryptPrivateKey(privateKey.toString(), password); const wallets = await getAllWallets(); const newWallet = { address, environment, name: walletName, isActive: wallets.length === 0, }; wallets.push(newWallet); await saveWallets(wallets); await setPrivateKey(address, encryptedKey); if (newWallet.isActive) { await setActiveWallet(newWallet); } console.log('\n✅ Wallet created successfully!'); console.log(`\nName: ${walletName}`); console.log(`Environment: ${environment}`); console.log(`Address: ${address}\n`); if (newWallet.isActive) { console.log('This wallet is now your active wallet.'); } else { console.log(`To use this wallet, run: mnee use ${walletName}`); } } catch (error) { console.error('\n❌ Error creating wallet:', error); } }); program .command('address') .description('Retrieve your wallet address') .action(async () => { const activeWallet = await getActiveWallet(); if (!activeWallet) { console.error('❌ No active wallet found. Run `mnee create` first or `mnee use <wallet-name>` to select a wallet.'); return; } console.log(`\nActive Wallet: ${activeWallet.name} (${activeWallet.environment})`); console.log(`Address: ${activeWallet.address}\n`); }); program .command('balance') .description('Get the balance of the wallet') .action(async () => { const activeWallet = await getActiveWallet(); if (!activeWallet) { console.error('❌ No active wallet found. Run `mnee create` first or `mnee use <wallet-name>` to select a wallet.'); return; } singleLineLogger.start(`Fetching balance for ${activeWallet.name} (${activeWallet.environment})...`); const mneeInstance = getMneeInstance(activeWallet.environment); const { decimalAmount } = await mneeInstance.balance(activeWallet.address); singleLineLogger.done(`\n$${decimalAmount} MNEE\n`); }); program .command('history') .description('Get the history of the wallet') .option('-u, --unconfirmed', 'Show unconfirmed transactions') .option('-f, --fresh', 'Clear cache and fetch fresh history from the beginning') .action(async (options) => { const activeWallet = await getActiveWallet(); if (!activeWallet) { console.error('❌ No active wallet found. Run `mnee create` first or `mnee use <wallet-name>` to select a wallet.'); return; } singleLineLogger.start(`Fetching history for ${activeWallet.name} (${activeWallet.environment})...`); const mneeInstance = getMneeInstance(activeWallet.environment); let nextScore = undefined; let hasMore = true; let history = []; let attempts = 0; const maxAttempts = 20; // Safety limit to prevent infinite loops if (options.fresh) { console.log('Fresh mode: Clearing cache and fetching from the beginning...'); clearTxHistoryCache(activeWallet); } else { const cachedData = readTxHistoryCache(activeWallet); if (cachedData) { history = cachedData.history; nextScore = cachedData.nextScore; // If nextScore is 0, we have all history if (nextScore === 0) { console.log(JSON.stringify(history, null, 2)); singleLineLogger.done(`\nHistory fetched successfully from cache!\n`); return; } } } while (hasMore && attempts < maxAttempts) { const { history: newHistory, nextScore: newNextScore } = await mneeInstance.recentTxHistory(activeWallet.address, nextScore, 100); if (newNextScore === nextScore && newNextScore !== undefined) break; history.push(...newHistory); nextScore = newNextScore; hasMore = nextScore !== 0 && nextScore !== undefined; attempts++; } if (attempts >= maxAttempts) { console.log('Reached maximum number of attempts. Some history may be missing.'); } writeTxHistoryCache(activeWallet, history, nextScore || 0); if (options.unconfirmed) { const unconfirmedHistory = history.filter((tx) => tx.status === 'unconfirmed'); console.log(JSON.stringify(unconfirmedHistory, null, 2)); } else { console.log(JSON.stringify(history, null, 2)); } singleLineLogger.done(`\n${history.length} transactions fetched successfully!\n`); }); program .command('transfer') .description('Transfer MNEE to another address') .action(async () => { try { const activeWallet = await getActiveWallet(); if (!activeWallet) { console.error('❌ No active wallet found. Run `mnee create` first or `mnee use <wallet-name>` to select a wallet.'); return; } const { amount, toAddress } = await safePrompt([ { type: 'input', name: 'amount', message: 'Enter the amount to transfer:', }, { type: 'input', name: 'toAddress', message: "Enter the recipient's address:", }, ]); const { password } = await safePrompt([ { type: 'password', name: 'password', message: 'Enter your wallet password:', mask: '*', }, ]); const encryptedKey = await getPrivateKey(activeWallet.address); if (!encryptedKey) { console.error('❌ Private key not found for this wallet.'); return; } const privateKeyHex = decryptPrivateKey(encryptedKey, password); if (!privateKeyHex) { console.error('❌ Incorrect password! Decryption failed.'); return; } const privateKey = PrivateKey.fromString(privateKeyHex); const request = [{ address: toAddress, amount: parseFloat(amount) }]; singleLineLogger.start(`Transferring MNEE from ${activeWallet.name} (${activeWallet.environment})...`); const mneeInstance = getMneeInstance(activeWallet.environment); const { txid, error } = await mneeInstance.transfer(request, privateKey.toWif()); if (!txid) { singleLineLogger.done(`❌ Transfer failed. ${error ? error : 'Please try again.'}`); return; } singleLineLogger.done(`\n✅ Transfer successful! TXID:\n${txid}\n`); } catch (error) { console.log('\n❌ Operation interrupted.'); process.exit(1); } }); program .command('export') .description('Decrypt and retrieve your private key in WIF format') .action(async () => { try { const activeWallet = await getActiveWallet(); if (!activeWallet) { console.error('❌ No active wallet found. Run `mnee create` first or `mnee use <wallet-name>` to select a wallet.'); return; } const { password } = await safePrompt([ { type: 'password', name: 'password', message: 'Enter your wallet password:', mask: '*', }, ]); const encryptedKey = await getPrivateKey(activeWallet.address); if (!encryptedKey) { console.error('❌ Private key not found for this wallet.'); return; } const { confirm } = await safePrompt([ { type: 'confirm', name: 'confirm', message: 'You are about to expose your private key. Continue?', default: false, }, ]); if (!confirm) { console.log('🚫 Operation cancelled.'); return; } const privateKeyHex = decryptPrivateKey(encryptedKey, password); if (!privateKeyHex) { console.error('❌ Incorrect password! Decryption failed.'); return; } const privateKey = PrivateKey.fromString(privateKeyHex); const wif = privateKey.toWif(); console.log(`\nWallet Name: ${activeWallet.name}`); console.log(`Environment: ${activeWallet.environment}`); console.log(`Wallet Address:\n${activeWallet.address}`); console.log(`\nWIF Private Key:\n${wif}`); console.log('\n🚨 Keep this key SAFE! Never share it with anyone.\n'); } catch (error) { console.error('\n❌ Error exporting private key:', error); } }); program .command('delete') .description('Delete a wallet') .argument('<walletName>', 'Name of the wallet to delete (defaults to active wallet)') .action(async (walletName) => { try { const wallets = await getAllWallets(); const activeWallet = await getActiveWallet(); if (wallets.length === 0) { console.error('❌ No wallets found.'); return; } if (!walletName && activeWallet) { walletName = activeWallet.name; } if (!walletName) { console.error('❌ No wallet specified and no active wallet found.'); return; } const wallet = wallets.find((w) => w.name === walletName); if (!wallet) { console.error(`❌ Wallet "${walletName}" not found.`); return; } const { confirm } = await safePrompt([ { type: 'confirm', name: 'confirm', message: `Are you sure you want to delete wallet "${walletName}"? This action cannot be undone.`, default: false, }, ]); if (!confirm) { console.log('🚫 Operation cancelled.'); return; } const encryptedKey = await getPrivateKey(wallet.address); if (!encryptedKey) { console.error('❌ Private key not found for this wallet.'); return; } const { password } = await safePrompt([ { type: 'password', name: 'password', message: 'Enter your wallet password to confirm deletion:', mask: '*', }, ]); let decryptedKey = null; try { decryptedKey = decryptPrivateKey(encryptedKey, password); } catch (error) { console.error('❌ Incorrect password! Deletion cancelled.'); return; } if (!decryptedKey) { console.error('❌ Password verification failed. Deletion cancelled.'); return; } const updatedWallets = wallets.filter((w) => w.name !== walletName); if (wallet.isActive) { if (updatedWallets.length > 0) { updatedWallets[0].isActive = true; await setActiveWallet(updatedWallets[0]); console.log(`\n✅ Active wallet switched to: ${updatedWallets[0].name}`); } else { await clearActiveWallet(); console.log('\nℹ️ No active wallet set. Create a new wallet with `mnee create`.'); } } // Delete the wallet's private key first await deletePrivateKey(wallet.address); // Then update the wallets list await saveWallets(updatedWallets); console.log(`\n🗑️ Wallet "${walletName}" deleted successfully!`); } catch (error) { console.error('\n❌ Error deleting wallet:', error); } }); program .command('list') .description('List all your wallets and optionally switch to a different wallet') .action(async () => { try { const wallets = await getAllWallets(); const activeWallet = await getActiveWallet(); if (wallets.length === 0) { console.log('\n❌ No wallets found. Run `mnee create` to create a wallet.'); return; } console.log('\nYour Wallets:'); console.log('-------------'); wallets.forEach((wallet, index) => { const activeIndicator = wallet.isActive ? ' (Active) ✅' : ''; const truncatedAddress = `${wallet.address.slice(0, 8)}...${wallet.address.slice(-8)}`; console.log(`${index + 1}. ${wallet.name}${activeIndicator}`); console.log(` Environment: ${wallet.environment}`); console.log(` Address: ${truncatedAddress}`); console.log(''); }); if (activeWallet) { console.log(`Current active wallet: ${activeWallet.name} (${activeWallet.environment})`); } const { wantToSwitch } = await safePrompt([ { type: 'confirm', name: 'wantToSwitch', message: 'Would you like to switch to a different wallet?', default: false, }, ]); if (wantToSwitch) { const { selectedWallet } = await safePrompt([ { type: 'list', name: 'selectedWallet', message: 'Select a wallet to switch to:', choices: wallets.map((wallet) => ({ name: `${wallet.name} | ${wallet.environment} | ${wallet.address.slice(0, 5)}...${wallet.address.slice(-4)}`, value: wallet.name, })), }, ]); const wallet = wallets.find((w) => w.name === selectedWallet); if (wallet) { wallets.forEach((w) => { w.isActive = w.name === selectedWallet; }); await saveWallets(wallets); await setActiveWallet(wallet); console.log(`\n✅ Switched to wallet: ${wallet.name}`); console.log(`Environment: ${wallet.environment}`); console.log(`Address: ${wallet.address}`); } } } catch (error) { console.error('\n❌ Error listing wallets:', error); } }); program .command('rename') .description('Rename a wallet') .argument('<oldName>', 'Current name of the wallet') .argument('<newName>', 'New name for the wallet') .action(async (oldName, newName) => { try { const validation = validateWalletName(newName); if (!validation.isValid) { console.error(`❌ ${validation.error}`); return; } const wallets = await getAllWallets(); if (wallets.length === 0) { console.error('❌ No wallets found. Run `mnee create` to create a wallet.'); return; } const wallet = wallets.find((w) => w.name === oldName); if (!wallet) { console.error(`❌ Wallet "${oldName}" not found.`); console.log('Run `mnee list` to see your available wallets.'); return; } if (wallets.some((w) => w.name === newName)) { console.error(`❌ A wallet with name "${newName}" already exists.`); return; } wallet.name = newName; await saveWallets(wallets); const activeWallet = await getActiveWallet(); if (activeWallet && activeWallet.name === oldName) { await setActiveWallet(wallet); } console.log(`\n✅ Wallet renamed from "${oldName}" to "${newName}"`); if (wallet.isActive) { console.log('This is your active wallet.'); } } catch (error) { console.error('\n❌ Error renaming wallet:', error); } }); program .command('import') .description('Import an existing wallet using a WIF private key') .action(async () => { try { const existingWallets = await getAllWallets(); const { environment } = await safePrompt([ { type: 'list', name: 'environment', message: 'Select wallet environment:', choices: [ { name: 'Production', value: 'production' }, { name: 'Sandbox', value: 'sandbox' }, ], default: 'production', }, ]); const { wifKey } = await safePrompt([ { type: 'password', name: 'wifKey', message: 'Enter your WIF private key:', mask: '*', }, ]); let privateKey; try { privateKey = PrivateKey.fromWif(wifKey); } catch (error) { console.error('❌ Invalid WIF key. Please check and try again.'); return; } const address = privateKey.toAddress(); // Check if wallet with this address already exists const existingWallet = await getWalletByAddress(address); if (existingWallet) { console.error(`\n❌ A wallet with address ${address} already exists.`); console.log(`\nTo use this wallet, run: mnee use ${existingWallet.name}`); return; } const { walletName } = await safePrompt([ { type: 'input', name: 'walletName', message: `Enter a name for your ${environment} wallet:`, default: `${environment}-wallet-${Date.now()}`, validate: (input) => { const validation = validateWalletName(input); if (!validation.isValid) { return validation.error || 'Invalid wallet name'; } if (existingWallets.some((w) => w.name === input)) { return `A wallet with name "${input}" already exists`; } return true; }, }, ]); const { password, confirmPassword } = await safePrompt([ { type: 'password', name: 'password', message: 'Set a password to encrypt your wallet:', mask: '*', }, { type: 'password', name: 'confirmPassword', message: 'Confirm your password:', mask: '*', }, ]); if (password !== confirmPassword) { console.error('❌ Passwords do not match. Try again.'); return; } const encryptedKey = encryptPrivateKey(privateKey.toString(), password); const newWallet = { address, environment, name: walletName, isActive: true, }; // Deactivate all other wallets existingWallets.forEach((wallet) => { wallet.isActive = false; }); existingWallets.push(newWallet); await saveWallets(existingWallets); await setPrivateKey(address, encryptedKey); await setActiveWallet(newWallet); console.log('\n✅ Wallet imported successfully!'); console.log(`\nName: ${walletName}`); console.log(`Environment: ${environment}`); console.log(`Address: ${address}\n`); console.log('This wallet is now your active wallet.'); } catch (error) { console.error('\n❌ Error importing wallet:', error); } }); const migrateOldWallets = async () => { try { const { address: oldAddress, privateKey: oldEncryptedKey } = await getLegacyWallet(); if (!oldAddress || !oldEncryptedKey) { return; } const existingWallets = await getAllWallets(); const addresses = existingWallets.map((w) => w.address.trim()); console.log('👍 Legacy wallet found:', oldAddress); console.log('🔍 Checking for existing wallets...'); console.log('📦 Existing wallet addresses:', addresses); const alreadyMigrated = addresses.includes(oldAddress.trim()); if (alreadyMigrated) { console.log('ℹ️ Legacy wallet already exists in new format. Skipping migration.'); await deleteLegacyWallet(); return; } const { confirm } = await inquirer.prompt([ { type: 'confirm', name: 'confirm', message: `A legacy wallet (${oldAddress}) was found. Do you want to migrate it?`, default: true, }, ]); if (!confirm) { console.log('🚫 Migration cancelled.'); return; } const { password } = await inquirer.prompt([ { type: 'password', name: 'password', message: 'Enter your wallet password to decrypt legacy wallet:', mask: '*', }, ]); const decryptedKey = decryptPrivateKey(oldEncryptedKey, password); if (!decryptedKey) { console.error('❌ Failed to decrypt old private key. Migration aborted.'); return; } const reEncryptedKey = encryptPrivateKey(decryptedKey, password); // Use unique name if "legacy-wallet" is taken let baseName = 'legacy-wallet'; let name = baseName; let suffix = 1; while (existingWallets.some((w) => w.name === name)) { name = `${baseName}-${suffix++}`; } const newWallet = { address: oldAddress, environment: 'production', name, isActive: existingWallets.length === 0, // Only auto-activate if no other wallets }; const updatedWallets = [...existingWallets, newWallet]; await saveWallets(updatedWallets); if (newWallet.isActive) { await setActiveWallet(newWallet); } await setPrivateKey(oldAddress, reEncryptedKey); await deleteLegacyWallet(); console.log(`✅ Migration complete! Wallet added as "${name}".`); if (newWallet.isActive) { console.log('This wallet is now your active wallet.'); } else { console.log(`To use it, run: mnee use ${name}`); } } catch (error) { console.error('\n❌ Error during wallet migration:', error); } }; const validateWalletName = (name) => { if (!name || name.trim() === '') { return { isValid: false, error: 'Wallet name cannot be empty' }; } if (name.includes(' ')) { return { isValid: false, error: 'Wallet name cannot contain spaces' }; } if (!/^[a-zA-Z0-9-_]+$/.test(name)) { return { isValid: false, error: 'Wallet name can only contain letters, numbers, hyphens, and underscores', }; } if (name.length < 1 || name.length > 50) { return { isValid: false, error: 'Wallet name must be between 1 and 50 characters', }; } return { isValid: true }; }; await migrateOldWallets(); program.parse(process.argv); process.on('SIGINT', () => { console.log('\n👋 Exiting program gracefully...'); process.exit(0); });