icewallet
Version:
Cold storage enabled command line bitcoin wallet based on bitpay's bitcore
225 lines (203 loc) • 7.67 kB
text/typescript
var bitcore = require('bitcore-lib');
import async = require('async');
import {InsightService} from '../Services/InsightService';
import {PublicWalletInfo} from '../Models/PublicWalletInfo'
import * as BM from '../Models/BitcoreModels';
import WalletService from './WalletService';
import {Deserialize, Serialize} from 'cerialize'
class PublicWalletService extends WalletService {
insightService:InsightService;
changeAddresses:BM.AddressInfo[];
externalAddresses:BM.AddressInfo[];
walletInfo:PublicWalletInfo;
constructor(info: PublicWalletInfo, password:string)
{
super(info, password);
this.insightService = new InsightService('https://insight.bitpay.com/api/');
this.changeAddresses= [];
this.externalAddresses = [];
}
static openWallet(password:string, encryptedInfo:string, callback:(err:any, info:string, wallet:PublicWalletService) => void){
this.cryptoService.decrypt(password, encryptedInfo, (err, decrypted) => {
if (err){
return callback(err,null,null);
}
else if (decrypted.startsWith('xpub')){
let error = 'This version of Icewallet is not compatable with your wallet\n';
error += 'Please create a new wallet with your public key\n';
error += decrypted + '\n';
error += 'Then use the "Next Unused Address Indexes" option to update your private wallet';
return callback(error,null,null);
}
try {
var json = JSON.parse(decrypted);
var walletInfo:PublicWalletInfo = Deserialize(json, PublicWalletInfo)
var wallet = new PublicWalletService(walletInfo, password);
}
catch(err){
return callback('Could not create wallet, check your xpub or password',decrypted,null);
}
return callback(null, decrypted, wallet);
});
}
public get balance() : number {
var changeBalance = 0;
var externalBalance = 0;
if (this.changeAddresses.length > 0){
changeBalance = this.changeAddresses.map((addr) => addr.balanceSat).reduce((p,c) => c + p);
}
if (this.externalAddresses.length > 0){
externalBalance = this.externalAddresses.map((addr) => addr.balanceSat).reduce((p,c) => c + p);
}
return changeBalance + externalBalance;
}
getAddressInfo(index:number, change:boolean, callback:(error: any, info:BM.AddressInfo) => void){
var address = this.address(index,change);
this.insightService.getAddressInfo(address, (err, resp, body) => {
if (err){
return callback(err, null);
}
return callback(null, JSON.parse(body));
})
}
switchAccount(accountName:string, callback:(err:any) => void){
this.selectedAccount = this.walletInfo.accounts.find(account => account.name == accountName);
console.log('updating wallet account...');
return this.update(callback);
}
// not used atm
getTransactions(change:boolean, callback: (error: any, transactions: BM.Transaction[]) => void){
var startingAddress = 0;
var addrs = this.addressRange(startingAddress,startingAddress + 19, change);
var transactions: BM.Transaction[] = []
var self = this;
function combine(err:any,resp:any,body:any){
if (err){
return callback(err,null);
}
var transactionBatch:BM.Transaction[] = JSON.parse(body).items;
// combine them
transactionBatch.forEach((utxo) => transactions.push(utxo));
// if its still returning results
if (transactionBatch.length > 0){
//increment the starting address
startingAddress += 20;
addrs = self.addressRange(startingAddress, startingAddress + 19, change);
// call the service again and repeat
self.insightService.getTransactions(addrs, combine);
}
else {
return callback(err,transactions);
}
}
this.insightService.getTransactions(addrs, combine);
}
getAddresses(change:boolean, callback:(error:any, addrs:BM.AddressInfo[]) => void){
// max number of concurrent requests
var maxConcurrency = 3;
// when to stop looking for transactions
var maxUnusedAddresses = 5;
// setup initial variables
var index = 0;
var emptyAddressCount = 0;
var addresses:BM.AddressInfo[] = [];
var errors:any[] = [];
function taskCallback(error:any){
if (error){
return errors.push(error);
}
}
//create the queue with concurrency
var q = async.queue<any>((task, callback) => {
this.getAddressInfo(task.index, change, (err, address) => {
if(err){
return callback(err)
}
if(address.txApperances == 0){
emptyAddressCount++
}
else {
addresses[task.index] = address;
}
// kick off a new task with the next index
if(emptyAddressCount < maxUnusedAddresses){
q.push({index:index}, taskCallback);
}
index++;
return callback();
})
}, maxConcurrency)
// kick off initial tasks
for(index = 0; index < maxConcurrency; index++){
q.push({index:index}, taskCallback)
}
// setup the final callback
q.drain = () => {
return callback(errors.length > 0 ? errors : null, addresses);
}
}
update(callback:(error:any, wallet:PublicWalletService) => void){
async.series<BM.AddressInfo[]>([
(cb) => this.getAddresses(false, cb),
(cb) => this.getAddresses(true, cb),
],(err, results) => {
if (err){
return callback(err,null)
}
this.externalAddresses = results[0];
this.changeAddresses = results[1];
return callback(null, this);
})
}
createTransaction(to:string, amount:number, callback:(err:any,serializedTransaction:string) => void){
var addrs:string[] = [];
var total:number = 0;
var standardFee = 15000;
var addrsWithBalance = this.externalAddresses.concat(this.changeAddresses)
.filter((addr) => addr.balanceSat > 0);
addrsWithBalance.forEach((addr) => {
if (total < amount + standardFee){
addrs.push(addr.addrStr);
total += addr.balanceSat;
}
})
if(total < amount + standardFee){
return callback('you dont have enough coins for this transaction plus a fee', null);
}
this.insightService.getUtxos(addrs, (err, utxos) => {
if (err){
return callback(err, null);
}
var utxosForTransaction = utxos.map((utxo) => {
return {
address: utxo.address,
txId: utxo.txid,
outputIndex: utxo.vout,
script: utxo.scriptPubKey,
satoshis: utxo.satoshis
}
})
var transaction = new bitcore.Transaction()
.from(utxosForTransaction) // Feed information about what unspent outputs one can use
.to(to, amount) // Add an output with the given amount of satoshis
return callback(null, JSON.stringify(transaction.toObject()));
})
}
broadcastTransaction(serializedTransaction:string, callback:(err:any, txid:any) => void){
var transaction = new bitcore.Transaction(JSON.parse(serializedTransaction));
this.insightService.broadcastTransaction(transaction.serialize(),callback);
}
exportInfo(callback:(err:any, encryptedInfo:string) => void){
// derive the encryption key
PublicWalletService.cryptoService.deriveKey(this.password, (err,key) => {
if(err){
return callback(err,null);
}
var serialized = Serialize(this.walletInfo);
var stringified = JSON.stringify(serialized);
let encrypted = PublicWalletService.cryptoService.encrypt(key, stringified);
return callback(null, encrypted);
});
}
}
export {PublicWalletService}