icewallet
Version:
Cold storage enabled command line bitcoin wallet based on bitpay's bitcore
167 lines (148 loc) • 5.61 kB
text/typescript
var bitcore = require('bitcore-lib');
var Mnemonic = require('bitcore-mnemonic');
import {Deserialize, Serialize} from 'cerialize'
import {PrivateWalletInfo} from '../Models/PrivateWalletInfo'
import TransactionInfo from '../Models/TransactionInfo'
import WalletService from './WalletService'
import async = require('async');
export default class PrivateWalletService extends WalletService {
walletHdPrivKey : any;
walletInfo:PrivateWalletInfo;
get accountHdPrivKey():any {
return this.walletHdPrivKey.derive("m/44'/0'").derive(this.selectedAccount.index,true);
}
get nextChangeIndex():number {
return this.selectedAccount.nextChangeIndex;
}
set nextChangeIndex(value:number) {
this.selectedAccount.nextChangeIndex = value;
}
get nextExternalIndex():number {
return this.selectedAccount.nextExternalIndex;
}
set nextExternalIndex(value:number) {
this.selectedAccount.nextExternalIndex = value;
}
switchAccount(accountName:string, callback:(err:any) => void){
this.selectedAccount = this.walletInfo.accounts.find(account => account.name == accountName);
return callback(null);
}
static openWallet(password:string, encryptedInfo:string, callback:(err:any, info:PrivateWalletInfo, wallet:PrivateWalletService) => void){
this.cryptoService.decrypt(password, encryptedInfo, (err, decrypted) => {
if (err){
return callback(err,null,null);
}
try {
var json = JSON.parse(decrypted);
var walletInfo:PrivateWalletInfo = Deserialize(json, PrivateWalletInfo);
}
catch(err){
return callback('cannot open wallet, make sure your password is correct',null,null)
}
if(walletInfo.seed == null){
return callback('SEED_MISSING', walletInfo, null);
}
else {
var wallet = new PrivateWalletService(walletInfo, password);
return callback(null, walletInfo, wallet);
}
});
}
static seedWallet(password:string, info:PrivateWalletInfo, seed:string, callback:(err:any,wallet:PrivateWalletService) => void){
this.cryptoService.verifyHash(info.seedHash, seed, (err, matched) => {
if (err){
return callback (err, null)
}
if(!matched){
return callback('seed entered does not match hash', null);
}
//matched
info.seed = seed;
var wallet = new PrivateWalletService(info, password);
return callback(null, wallet)
});
}
constructor(walletInfo:PrivateWalletInfo, password:string){
if (!walletInfo.seed){
walletInfo.seed = new Mnemonic().toString();
}
let walletHdPrivKey = (new Mnemonic(walletInfo.seed)).toHDPrivateKey();
super(walletInfo, password);
this.walletHdPrivKey = walletHdPrivKey;
}
hdPrivateKey(index:number, change:boolean):any{
var chain:number = change ? 1 : 0;
return this.accountHdPrivKey.derive(chain).derive(index);
}
privateKeyRange(start:number, end:number, change:boolean):string[]{
var keys:string[] = [];
for (var i:number = start; i <= end; i++){
keys.push(this.hdPrivateKey(i,change).privateKey.toString());
}
return keys;
}
getDepositAddress():string{
return this.address(this.nextExternalIndex, false);
}
incrementChangeIndex(){
this.nextChangeIndex += 1;
}
incrementExternalIndex(){
this.nextExternalIndex += 1;
}
exportInfo(callback:(err:any, encryptedInfo:string) => void){
// we can derive the key and hash the seed in parallel
async.parallel<string>({
// derive the encryption key
cryptoKey: (cb) => PrivateWalletService.cryptoService.deriveKey(this.password, cb),
// dont hash the seed if its already computed or being exported plain text
seedHash: (cb) => {
if(this.walletInfo.seedHash || this.walletInfo.exportSeed){
return cb(null)
}
else{
PrivateWalletService.cryptoService.hash(this.walletInfo.seed, (err, hash) => {
if(err){
return cb(err);
}
this.walletInfo.seedHash = hash;
return cb(null, hash);
});
}
}
},
(err,results) => {
if(err){
return callback(err,null);
}
var serialized = Serialize(this.walletInfo);
var stringified = JSON.stringify(serialized);
let encrypted = PrivateWalletService.cryptoService.encrypt(results['cryptoKey'], stringified);
return callback(null, encrypted);
});
}
parseTransaction(serializedTransaction:string):TransactionInfo{
var transaction = new bitcore.Transaction(JSON.parse(serializedTransaction));
var info = new TransactionInfo();
info.outputTotals = {};
transaction.outputs.forEach((output:any) => {
info.outputTotals[output._script.toAddress().toString()] = output._satoshis;
})
return info;
}
completeTransaction(serializedTransaction:string, fee:number):string{
var transaction = new bitcore.Transaction(JSON.parse(serializedTransaction));
var changePrivateKeys = this.privateKeyRange(0, this.nextChangeIndex - 1, true);
var externalPrivateKeys = this.privateKeyRange(0, this.nextExternalIndex - 1, false);
transaction
.change(this.address(this.nextChangeIndex, true))
.fee(fee)
.sign(externalPrivateKeys.concat(changePrivateKeys));
// this performs some checks
transaction.serialize();
if (!transaction.isFullySigned()){
throw 'transaction is not fully signed, check yourself before you wreck yourself';
}
return JSON.stringify(transaction.toObject());
}
}