UNPKG

icewallet

Version:

Cold storage enabled command line bitcoin wallet based on bitpay's bitcore

378 lines (360 loc) 12.5 kB
import fs = require('fs'); let qrcode = require('qrcode-terminal'); let unit = require('bitcore-lib').Unit; let intl = require('intl'); import PrivateWalletService from '../Services/PrivateWalletService'; import IceWallet from './IceWallet'; import {PrivateWalletInfo} from '../Models/PrivateWalletInfo'; import TransactionInfo from '../Models/transactionInfo'; import inquirer = require('inquirer'); export default class IceWalletPrivate extends IceWallet { wallet:PrivateWalletService; loadWalletFromInfo(callback:(err:any,wallet:PrivateWalletService) => void) { inquirer.prompt({ name:'password', type:'password', message:'enter your password to open the wallet', }) .then((answers:any) => { let password = answers['password'].toString(); console.log('loading and decrypting wallet info from' + this.pathToWalletInfo); console.log('this might take a minute'); fs.readFile(this.pathToWalletInfo, 'hex', (err, data) => { if (err){ return callback(err,null); } PrivateWalletService.openWallet(password, data, (err, info, wallet) => { if(err == 'SEED_MISSING'){ return this.verifySeed(password, info, callback); } else if (err){ return callback(err, null); } return callback(err, wallet); }) }) }) } verifySeed(password:string, info:PrivateWalletInfo, callback:(err:any, wallet:PrivateWalletService) => void){ inquirer.prompt([{ name:'seed', message:'the seed is not stored in the info please enter it now to open the wallet', }]) .then((answers:any) => { console.log('verifying seed...') PrivateWalletService.seedWallet(password, info, answers['seed'].toString(), callback); }) } createNewWallet(callback:(err:any,wallet:PrivateWalletService) => void){ inquirer.prompt([ { name:'password1', type:'password', message:'create a password', validate:(password) => {if(!password) return 'Password required'; else return true} }, { name:'password2', type:'password', message:'retype password', validate:(password) => {if(!password) return 'Password required'; else return true} }]) .then((passwords:any) => { if(passwords['password1'] != passwords['password2']){ return callback('Passwords dont match', null); } let password = passwords['password1']; inquirer.prompt([ { name:'seed', message:'Please type the BIP39 Mnemonic seed for the new wallet, or leave blank for random', default:null, }, { name:'exportSeed', message:'Do you want to export the seed with the wallet info, (exports are always encrypted)?', type:'confirm', }, ]) .then((answers:any) => { var info = new PrivateWalletInfo(answers['seed'].toString(), Boolean(answers['exportSeed'])); try { var wallet = new PrivateWalletService(info, password.toString()); } catch (err){ return callback(err,null); } console.log('sucessfully created wallet'); return callback(null, wallet); }) }) } addAccount(callback:(err:any) => void){ inquirer.prompt([ { name:'name', message:'Give a name for this account, be descriptive', }, { name:'index', message:'what is the BIP32 derivation index for this account', validate:(externalIndex) => {if(!Number.isInteger(Number(externalIndex))) return 'Must be an integer'; else return true} }, { name:'externalIndex', message:'What is the starting external address index', default:0, validate:(externalIndex) => {if(!Number.isInteger(Number(externalIndex))) return 'Must be an integer'; else return true} }, { name:'changeIndex', message:'What is the starting change address index', default:0, validate:(changeIndex) => {if(!Number.isInteger(Number(changeIndex))) return 'Must be an integer'; else return true} }, ]) .then((answers:any) => { this.wallet.walletInfo.addAccount(answers['name'], Number(answers['index']), Number(answers['changeIndex']), Number(answers['externalIndex'])) return callback(null); }) } displayAccountMenu(){ class Choices { [key: string]: string; deposit = 'Deposit'; sign = 'Sign Transaction'; showUsed = 'Show Used Addresses'; exportAddresses = 'Export Addresses'; showSeed = 'Show seed'; showXpub = 'Show Account Public Key'; changeUsedAddresses = 'Update Used Address Indexes'; backToMain = 'Back To Main Menu'; saveAndQuit = 'Save and Quit (dont quit any other way)'; } let choices = new Choices(); console.log('----------' + this.wallet.selectedAccount.name + '----------'); inquirer.prompt([ { name:'choice', type:'list', message:'Choose an option', choices: Object.keys(choices).map<string>(choice => choices[choice]) }, { name:'fee', message:'enter your desired fee in bits', when: (answers) => { return answers['choice'] == choices.sign }, validate:(fee) => {if(!Number.isInteger(Number(fee))) return 'Must be an integer'; else return true} }]) .then((answers:any) => { let choice:string = answers['choice']; let fee = Number(unit.fromBits(answers['fee']).satoshis); let done = (err:any) => { if (err){ console.log(err); } if (choice != choices.saveAndQuit){ this.displayAccountMenu(); } } switch(choice){ case choices.deposit: this.deposit(done); break; case choices.sign: this.sign(fee, done); break; case choices.showUsed: this.printAddresses(); done(null); break; case choices.exportAddresses: this.exportAddresses(done); break; case choices.changeUsedAddresses: this.changeUsedAddresses(done); break; case choices.showSeed: console.log(this.wallet.walletInfo.seed); done(null); break; case choices.showXpub: qrcode.generate(this.wallet.selectedAccount.xpub); console.log(this.wallet.selectedAccount.xpub); this.displayAccountMenu(); break; case choices.backToMain: this.displayMainMenu(); break; case choices.saveAndQuit: this.saveAndQuit(done); break; default: this.displayAccountMenu(); } }) } deposit(callback:(err:any) => void){ var newAddress = this.wallet.getDepositAddress(); console.log('Send coins to:' + newAddress); qrcode.generate(newAddress); inquirer.prompt( { name:'choice', message:'Did the transaction complete?', type:'confirm' }) .then((answers:any) => { if(answers['choice']){ console.log('good') this.wallet.incrementExternalIndex(); } else if(answers['choice']){ console.log('try again'); } return callback(null); }) } verifyTransaction(transaction:TransactionInfo, fee:number, callback:(err:any) => void){ console.log('Please verify this transaction'); for(let address in transaction.outputTotals){ console.log('Send: ' + unit.fromSatoshis(transaction.outputTotals[address]).bits.toLocaleString() + ' bits'); console.log('To: ' + address); } console.log('Fee: ' + unit.fromSatoshis(fee).bits.toLocaleString() + ' bits'); inquirer.prompt({ name:'complete', type:'confirm', message:'answer y/n', }) .then((answers:any) => { let complete:boolean = answers['complete']; if(complete){ return callback(null); } else { return callback('Fix issues and try again'); } }); } sign(fee:number, callback:(err:any) => void){ inquirer.prompt([ { name:'import', message:'type the import path (path to unsigned transaction)', when: (answers) => { return (!this.pathToUnsignedTransaction) }, }, { name:'export', message:'type the export path', when: (answers) => { return (!this.pathToSignedTransaction) }, }]) .then((answers:any) => { var outputPath:string = this.pathToSignedTransaction || answers['export']; var importPath:string = this.pathToUnsignedTransaction || answers['import']; fs.readFile(importPath || answers['import'], 'utf8', (err, serialized) => { if(err){ return callback(err); } var transactionInfo = this.wallet.parseTransaction(serialized); this.verifyTransaction(transactionInfo, fee, (err) => { if (err){ return callback(err); } try { // add the fee, change script and sign it var signed = this.wallet.completeTransaction(serialized, fee); } catch(err){ return callback(err); } // export the signed transaction fs.writeFile(outputPath, signed, (err) => { if(err){ return callback(err); } // update the change index count this.wallet.incrementChangeIndex(); console.log('transaction successfully signed and written to: ' + outputPath); return callback(null); }) }) }) } ) } printAddresses(){ console.log('change: '); this.wallet.addressRange(0, this.wallet.nextChangeIndex - 1, true).forEach((address) => { console.log('\t' + address); }) console.log('external: '); this.wallet.addressRange(0, this.wallet.nextExternalIndex - 1, false).forEach((address) => { console.log('\t' + address); }) } exportAddresses(callback:(err:any) => void){ inquirer.prompt([ { name:'count', message:'How many addresses?', validate:(fee) => {if(!Number.isInteger(Number(fee))) return 'Must be an integer'; else return true} }, { name:'burn', message:'Mark these as used? (may cause issues updating public wallet if you dont use them then deposit to this account again)', type:'confirm', }, { name:'path', message:'Type the export path', }, ]) .then((answers:any) => { let count = Number(answers['count']); let burn = Boolean(answers['burn']); let path:string = answers['path']; let starting = this.wallet.nextExternalIndex; let ending = starting + count - 1; var addresses = this.wallet.addressRange(starting, ending, false) fs.writeFile(path, JSON.stringify(addresses), (err) => { addresses.forEach((address) => { console.log(address); }) if(burn){ this.wallet.nextExternalIndex += count; } console.log('Adress list saved to ' + path); callback(null); }) }) } changeUsedAddresses(callback:(err:any) => void){ inquirer.prompt([ { name:'externalIndex', message:'How many external addresses have been used', default:0, validate:(externalIndex) => {if(!Number.isInteger(Number(externalIndex))) return 'Must be an integer'; else return true} }, { name:'changeIndex', message:'How many change addresses have been used', default:0, validate:(changeIndex) => {if(!Number.isInteger(Number(changeIndex))) return 'Must be an integer'; else return true} }, ]) .then((answers:any) => { this.wallet.nextExternalIndex = Number(answers['externalIndex']); this.wallet.nextChangeIndex = Number(answers['changeIndex']); console.log('sucessfully updated wallet'); return callback(null); }) } }