bitcore-wallet-service/lib/server.js

2982 lines
90 KiB
JavaScript
Raw Normal View History

2015-01-27 05:18:45 -08:00
'use strict';
2016-01-28 13:11:53 -08:00
2015-01-27 05:18:45 -08:00
var _ = require('lodash');
var $ = require('preconditions').singleton();
var async = require('async');
var log = require('npmlog');
var config = require('../config');
2015-01-27 05:18:45 -08:00
log.debug = log.verbose;
2015-04-18 02:55:24 -07:00
log.disableColor();
var EmailValidator = require('email-validator');
2015-10-30 11:24:47 -07:00
var Stringify = require('json-stable-stringify');
2015-02-02 10:29:14 -08:00
2015-10-30 11:24:47 -07:00
var Bitcore = require('bitcore-lib');
2015-01-27 05:18:45 -08:00
var Common = require('./common');
var Utils = Common.Utils;
var Constants = Common.Constants;
var Defaults = Common.Defaults;
2015-07-31 08:16:18 -07:00
var ClientError = require('./errors/clienterror');
var Errors = require('./errors/errordefinitions');
2015-04-07 13:02:08 -07:00
var Lock = require('./lock');
2015-01-27 05:18:45 -08:00
var Storage = require('./storage');
2015-05-06 06:00:09 -07:00
var MessageBroker = require('./messagebroker');
2015-03-30 16:16:51 -07:00
var BlockchainExplorer = require('./blockchainexplorer');
2016-01-11 12:46:36 -08:00
var FiatRateService = require('./fiatrateservice');
2015-01-27 11:40:21 -08:00
var request = require('request');
2015-04-27 11:38:33 -07:00
var Model = require('./model');
var Wallet = Model.Wallet;
2015-01-27 05:18:45 -08:00
2015-02-06 12:56:51 -08:00
var initialized = false;
2015-04-28 20:34:18 -07:00
var lock;
var storage;
var blockchainExplorer;
var blockchainExplorerOpts;
2015-05-06 06:00:09 -07:00
var messageBroker;
2016-01-11 12:46:36 -08:00
var fiatRateService;
2015-10-19 07:18:26 -07:00
var serviceVersion;
2015-02-06 12:56:51 -08:00
2015-01-27 07:54:17 -08:00
/**
2015-03-04 09:04:34 -08:00
* Creates an instance of the Bitcore Wallet Service.
2015-01-27 07:54:17 -08:00
* @constructor
2015-02-06 12:56:51 -08:00
*/
2015-02-20 12:32:19 -08:00
function WalletService() {
2015-02-20 12:23:42 -08:00
if (!initialized)
2015-02-19 12:38:48 -08:00
throw new Error('Server not initialized');
2015-04-07 13:02:08 -07:00
this.lock = lock;
2015-02-06 12:56:51 -08:00
this.storage = storage;
2015-03-30 16:16:51 -07:00
this.blockchainExplorer = blockchainExplorer;
2015-04-15 06:59:25 -07:00
this.blockchainExplorerOpts = blockchainExplorerOpts;
2015-05-06 06:00:09 -07:00
this.messageBroker = messageBroker;
2016-01-11 12:46:36 -08:00
this.fiatRateService = fiatRateService;
2015-02-12 05:26:13 -08:00
this.notifyTicker = 0;
2015-10-19 07:18:26 -07:00
};
2016-05-20 06:12:41 -07:00
function checkRequired(obj, args, cb) {
var missing = Utils.getMissingFields(obj, args);
if (_.isEmpty(missing)) return true;
if (_.isFunction(cb))
cb(new ClientError('Required argument ' + _.first(missing) + ' missing.'));
2016-05-20 06:12:41 -07:00
return false;
};
2015-08-13 13:24:49 -07:00
2015-10-19 08:32:29 -07:00
/**
* Gets the current version of BWS
*/
WalletService.getServiceVersion = function() {
if (!serviceVersion)
serviceVersion = 'bws-' + require('../package').version;
return serviceVersion;
};
2015-02-06 12:56:51 -08:00
/**
* Initializes global settings for all instances.
2015-01-28 05:52:45 -08:00
* @param {Object} opts
* @param {Storage} [opts.storage] - The storage provider.
2015-03-30 16:16:51 -07:00
* @param {Storage} [opts.blockchainExplorer] - The blockchainExporer provider.
2015-04-21 10:43:35 -07:00
* @param {Callback} cb
2015-01-27 07:54:17 -08:00
*/
2015-04-21 10:43:35 -07:00
WalletService.initialize = function(opts, cb) {
$.shouldBeFunction(cb);
2015-02-02 12:07:18 -08:00
opts = opts || {};
2015-04-07 13:02:08 -07:00
lock = opts.lock || new Lock(opts.lockOpts);
2015-03-30 16:16:51 -07:00
blockchainExplorer = opts.blockchainExplorer;
2015-04-15 06:59:25 -07:00
blockchainExplorerOpts = opts.blockchainExplorerOpts;
if (opts.request)
request = opts.request;
2015-04-21 10:43:35 -07:00
2015-05-04 14:23:56 -07:00
function initStorage(cb) {
if (opts.storage) {
storage = opts.storage;
return cb();
2015-04-28 20:34:18 -07:00
} else {
var newStorage = new Storage();
newStorage.connect(opts.storageOpts, function(err) {
if (err) return cb(err);
storage = newStorage;
return cb();
});
2015-05-04 14:23:56 -07:00
}
2015-04-28 20:34:18 -07:00
};
2015-05-06 06:00:09 -07:00
function initMessageBroker(cb) {
if (opts.messageBroker) {
messageBroker = opts.messageBroker;
} else {
messageBroker = new MessageBroker(opts.messageBrokerOpts);
2015-05-04 14:23:56 -07:00
}
2015-05-05 09:04:29 -07:00
return cb();
2015-05-04 14:23:56 -07:00
};
2016-01-11 12:46:36 -08:00
function initFiatRateService(cb) {
if (opts.fiatRateService) {
fiatRateService = opts.fiatRateService;
return cb();
} else {
var newFiatRateService = new FiatRateService();
var opts2 = opts.fiatRateServiceOpts || {};
opts2.storage = storage;
newFiatRateService.init(opts2, function(err) {
if (err) return cb(err);
fiatRateService = newFiatRateService;
return cb();
});
}
};
2015-05-04 14:23:56 -07:00
async.series([
function(next) {
initStorage(next);
},
function(next) {
2015-05-06 06:00:09 -07:00
initMessageBroker(next);
2015-05-04 14:23:56 -07:00
},
2016-01-11 12:46:36 -08:00
function(next) {
initFiatRateService(next);
},
2015-05-04 14:23:56 -07:00
], function(err) {
if (err) {
log.error('Could not initialize', err);
throw err;
}
initialized = true;
return cb();
});
2015-04-21 10:43:35 -07:00
};
WalletService.shutDown = function(cb) {
2015-04-23 08:25:36 -07:00
if (!initialized) return cb();
storage.disconnect(function(err) {
if (err) return cb(err);
initialized = false;
return cb();
});
2015-01-27 05:18:45 -08:00
};
2015-06-29 08:20:24 -07:00
/**
* Gets an instance of the server without authentication.
* @param {Object} opts
* @param {string} opts.clientVersion - A string that identifies the client issuing the request
*/
WalletService.getInstance = function(opts) {
opts = opts || {};
var server = new WalletService();
server._setClientVersion(opts.clientVersion);
2015-06-29 08:20:24 -07:00
return server;
2015-02-09 10:30:16 -08:00
};
2015-02-06 12:56:51 -08:00
/**
* Gets an instance of the server after authenticating the copayer.
* @param {Object} opts
* @param {string} opts.copayerId - The copayer id making the request.
* @param {string} opts.message - The contents of the request to be signed.
2015-08-04 17:05:26 -07:00
* @param {string} opts.signature - Signature of message to be verified using one of the copayer's requestPubKeys
2015-06-29 08:20:24 -07:00
* @param {string} opts.clientVersion - A string that identifies the client issuing the request
2015-02-06 12:56:51 -08:00
*/
2015-02-20 12:32:19 -08:00
WalletService.getInstanceWithAuth = function(opts, cb) {
2016-05-20 06:12:41 -07:00
if (!checkRequired(opts, ['copayerId', 'message', 'signature'], cb)) return;
2015-02-06 12:56:51 -08:00
2015-02-20 12:32:19 -08:00
var server = new WalletService();
2015-02-07 08:13:29 -08:00
server.storage.fetchCopayerLookup(opts.copayerId, function(err, copayer) {
2015-02-06 12:56:51 -08:00
if (err) return cb(err);
2015-08-03 12:11:09 -07:00
if (!copayer) return cb(new ClientError(Errors.codes.NOT_AUTHORIZED, 'Copayer not found'));
2015-02-06 12:56:51 -08:00
2015-08-05 12:53:06 -07:00
var isValid = !!server._getSigningKey(opts.message, opts.signature, copayer.requestPubKeys);
2015-02-21 14:29:42 -08:00
if (!isValid)
2015-08-03 12:11:09 -07:00
return cb(new ClientError(Errors.codes.NOT_AUTHORIZED, 'Invalid signature'));
2015-02-02 10:56:53 -08:00
2015-02-06 12:56:51 -08:00
server.copayerId = opts.copayerId;
server.walletId = copayer.walletId;
server._setClientVersion(opts.clientVersion);
2015-02-06 12:56:51 -08:00
return cb(null, server);
});
2015-02-02 10:56:53 -08:00
};
2015-01-27 05:18:45 -08:00
2015-04-08 11:18:28 -07:00
WalletService.prototype._runLocked = function(cb, task) {
$.checkState(this.walletId);
this.lock.runLocked(this.walletId, cb, task);
};
2015-02-06 12:56:51 -08:00
2015-01-27 07:54:17 -08:00
/**
* Creates a new wallet.
2015-01-27 11:40:21 -08:00
* @param {Object} opts
2015-01-27 07:54:17 -08:00
* @param {string} opts.id - The wallet id.
* @param {string} opts.name - The wallet name.
* @param {number} opts.m - Required copayers.
* @param {number} opts.n - Total copayers.
* @param {string} opts.pubKey - Public key to verify copayers joining have access to the wallet secret.
2016-06-03 09:34:12 -07:00
* @param {string} opts.singleAddress[=false] - The wallet will only ever have one address.
* @param {string} opts.network[='livenet'] - The Bitcoin network for this wallet.
* @param {string} opts.supportBIP44AndP2PKH[=true] - Client supports BIP44 & P2PKH for new wallets.
2015-01-27 07:54:17 -08:00
*/
2015-02-20 12:32:19 -08:00
WalletService.prototype.createWallet = function(opts, cb) {
2015-02-02 15:13:13 -08:00
var self = this,
pubKey;
2015-01-27 05:18:45 -08:00
2016-05-20 06:12:41 -07:00
if (!checkRequired(opts, ['name', 'm', 'n', 'pubKey'], cb)) return;
2015-02-07 08:13:29 -08:00
2015-02-08 08:16:41 -08:00
if (_.isEmpty(opts.name)) return cb(new ClientError('Invalid wallet name'));
2015-02-07 08:13:29 -08:00
if (!Wallet.verifyCopayerLimits(opts.m, opts.n))
return cb(new ClientError('Invalid combination of required copayers / total copayers'));
2015-08-28 10:54:36 -07:00
opts.network = opts.network || 'livenet';
if (!_.contains(['livenet', 'testnet'], opts.network))
2015-02-07 08:13:29 -08:00
return cb(new ClientError('Invalid network'));
2015-02-02 10:29:14 -08:00
2015-09-10 10:53:33 -07:00
opts.supportBIP44AndP2PKH = _.isBoolean(opts.supportBIP44AndP2PKH) ? opts.supportBIP44AndP2PKH : true;
2015-10-30 11:24:47 -07:00
var derivationStrategy = opts.supportBIP44AndP2PKH ? Constants.DERIVATION_STRATEGIES.BIP44 : Constants.DERIVATION_STRATEGIES.BIP45;
var addressType = (opts.n == 1 && opts.supportBIP44AndP2PKH) ? Constants.SCRIPT_TYPES.P2PKH : Constants.SCRIPT_TYPES.P2SH;
2015-08-28 10:54:36 -07:00
2015-01-31 14:56:50 -08:00
try {
2015-10-30 11:24:47 -07:00
pubKey = new Bitcore.PublicKey.fromString(opts.pubKey);
2015-02-24 05:36:14 -08:00
} catch (ex) {
return cb(new ClientError('Invalid public key'));
2015-01-31 14:56:50 -08:00
};
2015-01-30 06:58:28 -08:00
2015-03-31 13:28:01 -07:00
var newWallet;
async.series([
function(acb) {
if (!opts.id)
return acb();
2015-02-07 08:13:29 -08:00
2015-03-31 13:28:01 -07:00
self.storage.fetchWallet(opts.id, function(err, wallet) {
2015-08-03 12:11:09 -07:00
if (wallet) return acb(Errors.WALLET_ALREADY_EXISTS);
2015-03-31 13:28:01 -07:00
return acb(err);
});
},
function(acb) {
var wallet = Wallet.create({
2015-08-28 10:54:36 -07:00
id: opts.id,
2015-03-31 13:28:01 -07:00
name: opts.name,
m: opts.m,
n: opts.n,
2015-08-28 10:54:36 -07:00
network: opts.network,
2015-03-31 13:28:01 -07:00
pubKey: pubKey.toString(),
2016-06-03 09:34:12 -07:00
singleAddress: !!opts.singleAddress,
2015-09-04 17:05:39 -07:00
derivationStrategy: derivationStrategy,
2015-09-04 20:50:51 -07:00
addressType: addressType,
2015-03-31 13:28:01 -07:00
});
self.storage.storeWallet(wallet, function(err) {
2015-08-28 10:54:36 -07:00
log.debug('Wallet created', wallet.id, opts.network);
2015-03-31 13:28:01 -07:00
newWallet = wallet;
return acb(err);
});
}
], function(err) {
return cb(err, newWallet ? newWallet.id : null);
2015-02-02 12:07:18 -08:00
});
2015-01-27 05:18:45 -08:00
};
2015-01-27 07:54:17 -08:00
/**
* Retrieves a wallet from storage.
2015-01-27 11:40:21 -08:00
* @param {Object} opts
2015-02-02 06:55:03 -08:00
* @returns {Object} wallet
2015-01-27 07:54:17 -08:00
*/
2015-02-20 12:32:19 -08:00
WalletService.prototype.getWallet = function(opts, cb) {
2015-02-02 12:07:18 -08:00
var self = this;
2015-01-27 05:18:45 -08:00
2015-02-06 12:56:51 -08:00
self.storage.fetchWallet(self.walletId, function(err, wallet) {
2015-02-02 12:07:18 -08:00
if (err) return cb(err);
2015-08-05 06:41:03 -07:00
if (!wallet) return cb(Errors.WALLET_NOT_FOUND);
2015-02-02 12:07:18 -08:00
return cb(null, wallet);
});
2015-01-27 05:18:45 -08:00
};
2015-08-18 13:56:46 -07:00
/**
* Retrieves wallet status.
* @param {Object} opts
* @param {Object} opts.twoStep[=false] - Optional: use 2-step balance computation for improved performance
2015-08-18 14:22:43 -07:00
* @param {Object} opts.includeExtendedInfo - Include PKR info & address managers for wallet & copayers
2015-08-18 13:56:46 -07:00
* @returns {Object} status
*/
WalletService.prototype.getStatus = function(opts, cb) {
var self = this;
2015-08-18 14:22:43 -07:00
opts = opts || {};
2015-08-18 13:56:46 -07:00
var status = {};
async.parallel([
function(next) {
self.getWallet({}, function(err, wallet) {
if (err) return next(err);
2015-08-18 14:22:43 -07:00
var walletExtendedKeys = ['publicKeyRing', 'pubKey', 'addressManager'];
2015-08-25 12:12:47 -07:00
var copayerExtendedKeys = ['xPubKey', 'requestPubKey', 'signature', 'addressManager', 'customData'];
2015-08-18 14:22:43 -07:00
2015-08-25 12:12:47 -07:00
wallet.copayers = _.map(wallet.copayers, function(copayer) {
if (copayer.id == self.copayerId) return copayer;
return _.omit(copayer, 'customData');
});
2015-08-18 14:22:43 -07:00
if (!opts.includeExtendedInfo) {
wallet = _.omit(wallet, walletExtendedKeys);
wallet.copayers = _.map(wallet.copayers, function(copayer) {
return _.omit(copayer, copayerExtendedKeys);
});
}
2015-08-18 13:56:46 -07:00
status.wallet = wallet;
next();
});
},
function(next) {
self.getBalance(opts, function(err, balance) {
2015-08-18 13:56:46 -07:00
if (err) return next(err);
status.balance = balance;
next();
});
},
function(next) {
self.getPendingTxs({}, function(err, pendingTxps) {
if (err) return next(err);
status.pendingTxps = pendingTxps;
next();
});
},
function(next) {
self.getPreferences({}, function(err, preferences) {
if (err) return next(err);
status.preferences = preferences;
next();
});
},
], function(err) {
if (err) return cb(err);
return cb(null, status);
});
};
2015-10-30 11:24:47 -07:00
/*
2015-02-01 06:41:16 -08:00
* Verifies a signature
* @param text
* @param signature
2015-08-04 17:05:26 -07:00
* @param pubKeys
2015-02-01 06:41:16 -08:00
*/
2015-08-04 17:05:26 -07:00
WalletService.prototype._verifySignature = function(text, signature, pubkey) {
2015-10-30 11:24:47 -07:00
return Utils.verifyMessage(text, signature, pubkey);
2015-08-04 17:05:26 -07:00
};
2015-10-30 11:24:47 -07:00
/*
* Verifies a request public key
* @param requestPubKey
* @param signature
* @param xPubKey
*/
WalletService.prototype._verifyRequestPubKey = function(requestPubKey, signature, xPubKey) {
var pub = (new Bitcore.HDPublicKey(xPubKey)).derive(Constants.PATHS.REQUEST_KEY_AUTH).publicKey;
return Utils.verifyMessage(requestPubKey, signature, pub.toString());
};
2015-08-04 17:05:26 -07:00
/*
* Verifies signature againt a collection of pubkeys
* @param text
* @param signature
* @param pubKeys
*/
2015-08-05 12:53:06 -07:00
WalletService.prototype._getSigningKey = function(text, signature, pubKeys) {
2015-08-04 17:05:26 -07:00
var self = this;
2015-08-05 12:53:06 -07:00
return _.find(pubKeys, function(item) {
2015-08-04 17:05:26 -07:00
return self._verifySignature(text, signature, item.key);
});
2015-02-01 06:41:16 -08:00
};
2015-02-11 11:00:16 -08:00
/**
2015-02-11 18:11:30 -08:00
* _notify
2015-02-11 11:00:16 -08:00
*
2015-04-02 07:57:47 -07:00
* @param {String} type
* @param {Object} data
2015-05-07 10:16:24 -07:00
* @param {Object} opts
* @param {Boolean} opts.isGlobal - If true, the notification is not issued on behalf of any particular copayer (defaults to false)
2015-02-11 11:00:16 -08:00
*/
2015-05-07 10:16:24 -07:00
WalletService.prototype._notify = function(type, data, opts, cb) {
2015-02-11 10:42:49 -08:00
var self = this;
2015-05-07 10:16:24 -07:00
if (_.isFunction(opts)) {
cb = opts;
opts = {};
}
opts = opts || {};
2015-02-12 11:42:32 -08:00
log.debug('Notification', type, data);
2015-04-30 10:50:48 -07:00
cb = cb || function() {};
2015-02-11 18:11:30 -08:00
var walletId = self.walletId || data.walletId;
var copayerId = self.copayerId || data.copayerId;
2015-02-11 18:11:30 -08:00
$.checkState(walletId);
2015-04-28 08:49:43 -07:00
var notification = Model.Notification.create({
2015-02-11 10:42:49 -08:00
type: type,
data: data,
2015-02-12 05:26:13 -08:00
ticker: this.notifyTicker++,
2015-05-07 10:16:24 -07:00
creatorId: opts.isGlobal ? null : copayerId,
2015-03-30 07:24:33 -07:00
walletId: walletId,
2015-02-11 10:42:49 -08:00
});
2015-04-28 20:34:18 -07:00
this.storage.storeNotification(walletId, notification, function() {
self.messageBroker.send(notification);
2015-05-29 06:30:59 -07:00
return cb();
2015-02-11 10:42:49 -08:00
});
};
2015-04-28 08:49:43 -07:00
2015-08-05 12:53:06 -07:00
WalletService.prototype._addCopayerToWallet = function(wallet, opts, cb) {
var self = this;
var copayer = Model.Copayer.create({
name: opts.name,
copayerIndex: wallet.copayers.length,
xPubKey: opts.xPubKey,
requestPubKey: opts.requestPubKey,
signature: opts.copayerSignature,
2015-08-25 12:12:47 -07:00
customData: opts.customData,
2015-09-04 17:05:39 -07:00
derivationStrategy: wallet.derivationStrategy,
2015-08-05 12:53:06 -07:00
});
self.storage.fetchCopayerLookup(copayer.id, function(err, res) {
if (err) return cb(err);
if (res) return cb(Errors.COPAYER_REGISTERED);
2015-09-17 07:39:23 -07:00
if (opts.dryRun) return cb(null, {
copayerId: null,
wallet: wallet
});
2015-08-05 12:53:06 -07:00
wallet.addCopayer(copayer);
self.storage.storeWalletAndUpdateCopayersLookup(wallet, function(err) {
if (err) return cb(err);
async.series([
function(next) {
self._notify('NewCopayer', {
walletId: opts.walletId,
copayerId: copayer.id,
copayerName: copayer.name,
}, next);
},
function(next) {
if (wallet.isComplete() && wallet.isShared()) {
self._notify('WalletComplete', {
walletId: opts.walletId,
}, {
isGlobal: true
}, next);
} else {
next();
}
},
], function() {
return cb(null, {
copayerId: copayer.id,
wallet: wallet
});
});
});
});
};
WalletService.prototype._addKeyToCopayer = function(wallet, copayer, opts, cb) {
var self = this;
2015-08-10 11:07:20 -07:00
wallet.addCopayerRequestKey(copayer.copayerId, opts.requestPubKey, opts.signature, opts.restrictions, opts.name);
2015-08-05 12:53:06 -07:00
self.storage.storeWalletAndUpdateCopayersLookup(wallet, function(err) {
if (err) return cb(err);
return cb(null, {
copayerId: copayer.id,
wallet: wallet
});
});
};
/**
* Adds access to a given copayer
*
* @param {Object} opts
* @param {string} opts.copayerId - The copayer id
* @param {string} opts.requestPubKey - Public Key used to check requests from this copayer.
* @param {string} opts.copayerSignature - S(requestPubKey). Used by other copayers to verify the that the copayer is himself (signed with REQUEST_KEY_AUTH)
* @param {string} opts.restrictions
* - cannotProposeTXs
* - cannotXXX TODO
2015-08-10 11:07:20 -07:00
* @param {string} opts.name (name for the new access)
2015-08-05 12:53:06 -07:00
*/
WalletService.prototype.addAccess = function(opts, cb) {
var self = this;
2016-05-20 06:12:41 -07:00
if (!checkRequired(opts, ['copayerId', 'requestPubKey', 'signature'], cb)) return;
2015-08-05 12:53:06 -07:00
self.storage.fetchCopayerLookup(opts.copayerId, function(err, copayer) {
if (err) return cb(err);
if (!copayer) return cb(Errors.NOT_AUTHORIZED);
self.storage.fetchWallet(copayer.walletId, function(err, wallet) {
if (err) return cb(err);
if (!wallet) return cb(Errors.NOT_AUTHORIZED);
var xPubKey = _.find(wallet.copayers, {
id: opts.copayerId
}).xPubKey;
2015-08-10 11:07:20 -07:00
2015-10-30 11:24:47 -07:00
if (!self._verifyRequestPubKey(opts.requestPubKey, opts.signature, xPubKey)) {
2015-08-05 12:53:06 -07:00
return cb(Errors.NOT_AUTHORIZED);
}
if (copayer.requestPubKeys.length > Defaults.MAX_KEYS)
2015-08-14 08:51:48 -07:00
return cb(Errors.TOO_MANY_KEYS);
2015-08-05 12:53:06 -07:00
self._addKeyToCopayer(wallet, copayer, opts, cb);
});
});
};
WalletService.prototype._setClientVersion = function(version) {
delete this.parsedClientVersion;
this.clientVersion = version;
};
2015-09-01 07:53:07 -07:00
WalletService.prototype._parseClientVersion = function() {
function parse(version) {
var v = {};
if (!version) return null;
var x = version.split('-');
if (x.length != 2) {
v.agent = version;
return v;
}
v.agent = _.contains(['bwc', 'bws'], x[0]) ? 'bwc' : x[0];
x = x[1].split('.');
v.major = parseInt(x[0]);
v.minor = parseInt(x[1]);
v.patch = parseInt(x[2]);
return v;
};
if (_.isUndefined(this.parsedClientVersion)) {
this.parsedClientVersion = parse(this.clientVersion);
}
return this.parsedClientVersion;
};
WalletService.prototype._clientSupportsTXPv2 = function() {
var version = this._parseClientVersion();
if (!version) return false;
if (version.agent != 'bwc') return true; // Asume 3rd party clients are up-to-date
if (version.major == 0 && version.minor == 0) return false;
return true;
};
WalletService.prototype._clientSupportsTXPv3 = function() {
var version = this._parseClientVersion();
if (!version) return false;
if (version.agent != 'bwc') return true; // Asume 3rd party clients are up-to-date
if (version.major < 2) return false;
return true;
};
WalletService.prototype._clientSupportsPayProRefund = function() {
var version = this._parseClientVersion();
if (!version) return false;
if (version.agent != 'bwc') return true;
if (version.major < 1 || (version.major == 1 && version.minor < 2)) return false;
return true;
};
2015-10-30 11:24:47 -07:00
WalletService._getCopayerHash = function(name, xPubKey, requestPubKey) {
return [name, xPubKey, requestPubKey].join('|');
};
2015-01-27 07:54:17 -08:00
/**
* Joins a wallet in creation.
2015-01-27 11:40:21 -08:00
* @param {Object} opts
2015-01-27 07:54:17 -08:00
* @param {string} opts.walletId - The wallet id.
* @param {string} opts.name - The copayer name.
2015-03-09 14:11:25 -07:00
* @param {string} opts.xPubKey - Extended Public Key for this copayer.
* @param {string} opts.requestPubKey - Public Key used to check requests from this copayer.
2015-08-25 12:12:47 -07:00
* @param {string} opts.copayerSignature - S(name|xPubKey|requestPubKey). Used by other copayers to verify that the copayer joining knows the wallet secret.
* @param {string} opts.customData - (optional) Custom data for this copayer.
2016-02-25 09:47:03 -08:00
* @param {string} opts.dryRun[=false] - (optional) Simulate the action but do not change server state.
2015-09-10 10:53:33 -07:00
* @param {string} [opts.supportBIP44AndP2PKH = true] - Client supports BIP44 & P2PKH for joining wallets.
2015-01-27 07:54:17 -08:00
*/
2015-02-20 12:32:19 -08:00
WalletService.prototype.joinWallet = function(opts, cb) {
2015-02-02 12:07:18 -08:00
var self = this;
2015-01-27 05:18:45 -08:00
2016-05-20 06:12:41 -07:00
if (!checkRequired(opts, ['walletId', 'name', 'xPubKey', 'requestPubKey', 'copayerSignature'], cb)) return;
2015-02-02 10:29:14 -08:00
2015-02-12 11:42:32 -08:00
if (_.isEmpty(opts.name))
2015-02-12 05:26:13 -08:00
return cb(new ClientError('Invalid copayer name'));
2015-02-08 08:36:19 -08:00
2015-12-01 06:25:59 -08:00
try {
Bitcore.HDPublicKey(opts.xPubKey);
} catch (ex) {
return cb(new ClientError('Invalid extended public key'));
}
2015-09-10 10:53:33 -07:00
opts.supportBIP44AndP2PKH = _.isBoolean(opts.supportBIP44AndP2PKH) ? opts.supportBIP44AndP2PKH : true;
2015-04-08 11:18:28 -07:00
self.walletId = opts.walletId;
self._runLocked(cb, function(cb) {
2015-02-06 12:56:51 -08:00
self.storage.fetchWallet(opts.walletId, function(err, wallet) {
2015-02-02 12:07:18 -08:00
if (err) return cb(err);
2015-08-05 06:41:03 -07:00
if (!wallet) return cb(Errors.WALLET_NOT_FOUND);
2015-02-01 06:41:16 -08:00
2015-09-07 13:18:32 -07:00
if (opts.supportBIP44AndP2PKH) {
// New client trying to join legacy wallet
2015-10-30 11:24:47 -07:00
if (wallet.derivationStrategy == Constants.DERIVATION_STRATEGIES.BIP45) {
2015-09-07 13:18:32 -07:00
return cb(new ClientError('The wallet you are trying to join was created with an older version of the client app.'));
}
} else {
// Legacy client trying to join new wallet
2015-10-30 11:24:47 -07:00
if (wallet.derivationStrategy == Constants.DERIVATION_STRATEGIES.BIP44) {
2015-09-07 13:18:32 -07:00
return cb(new ClientError(Errors.codes.UPGRADE_NEEDED, 'To join this wallet you need to upgrade your client app.'));
}
}
2015-10-30 11:24:47 -07:00
var hash = WalletService._getCopayerHash(opts.name, opts.xPubKey, opts.requestPubKey);
2015-03-10 07:23:23 -07:00
if (!self._verifySignature(hash, opts.copayerSignature, wallet.pubKey)) {
2015-02-04 08:31:02 -08:00
return cb(new ClientError());
2015-02-01 06:41:16 -08:00
}
2015-02-02 06:55:03 -08:00
if (_.find(wallet.copayers, {
xPubKey: opts.xPubKey
})) return cb(Errors.COPAYER_IN_WALLET);
2015-02-17 12:36:45 -08:00
2015-09-07 14:52:07 -07:00
if (wallet.copayers.length == wallet.n) return cb(Errors.WALLET_FULL);
2015-08-05 12:53:06 -07:00
self._addCopayerToWallet(wallet, opts, cb);
2015-02-02 12:07:18 -08:00
});
});
2015-01-27 05:18:45 -08:00
};
2015-04-27 11:38:33 -07:00
/**
* Save copayer preferences for the current wallet/copayer pair.
* @param {Object} opts
* @param {string} opts.email - Email address for notifications.
* @param {string} opts.language - Language used for notifications.
* @param {string} opts.unit - Bitcoin unit used to format amounts in notifications.
2015-04-27 11:38:33 -07:00
*/
WalletService.prototype.savePreferences = function(opts, cb) {
var self = this;
opts = opts || {};
var preferences = [{
name: 'email',
isValid: function(value) {
return EmailValidator.validate(value);
},
}, {
name: 'language',
isValid: function(value) {
return _.isString(value) && value.length == 2;
},
}, {
name: 'unit',
isValid: function(value) {
return _.isString(value) && _.contains(['btc', 'bit'], value.toLowerCase());
},
}];
2015-06-29 04:57:53 -07:00
opts = _.pick(opts, _.pluck(preferences, 'name'));
try {
_.each(preferences, function(preference) {
var value = opts[preference.name];
if (!value) return;
if (!preference.isValid(value)) {
throw 'Invalid ' + preference.name;
return false;
}
});
} catch (ex) {
return cb(new ClientError(ex));
2015-05-11 07:46:28 -07:00
}
2015-04-27 11:38:33 -07:00
self._runLocked(cb, function(cb) {
self.storage.fetchPreferences(self.walletId, self.copayerId, function(err, oldPref) {
if (err) return cb(err);
var newPref = Model.Preferences.create({
walletId: self.walletId,
copayerId: self.copayerId,
});
var preferences = Model.Preferences.fromObj(_.defaults(newPref, opts, oldPref));
self.storage.storePreferences(preferences, function(err) {
return cb(err);
});
2015-04-27 11:38:33 -07:00
});
});
};
/**
* Retrieves a preferences for the current wallet/copayer pair.
* @param {Object} opts
* @returns {Object} preferences
*/
WalletService.prototype.getPreferences = function(opts, cb) {
var self = this;
self.storage.fetchPreferences(self.walletId, self.copayerId, function(err, preferences) {
if (err) return cb(err);
return cb(null, preferences || {});
});
};
WalletService.prototype._canCreateAddress = function(ignoreMaxGap, cb) {
var self = this;
if (ignoreMaxGap) return cb(null, true);
2015-10-27 12:22:42 -07:00
self.storage.fetchAddresses(self.walletId, function(err, addresses) {
if (err) return cb(err);
var latestAddresses = _.takeRight(_.reject(addresses, {
isChange: true
}), Defaults.MAX_MAIN_ADDRESS_GAP);
if (latestAddresses.length < Defaults.MAX_MAIN_ADDRESS_GAP || _.any(latestAddresses, {
hasActivity: true
})) return cb(null, true);
var bc = self._getBlockchainExplorer(latestAddresses[0].network);
2015-10-28 09:23:13 -07:00
var activityFound = false;
var i = latestAddresses.length;
async.whilst(function() {
return i > 0 && !activityFound;
}, function(next) {
bc.getAddressActivity(latestAddresses[--i].address, function(err, res) {
if (err) return next(err);
activityFound = !!res;
return next();
});
}, function(err) {
if (err) return cb(err);
if (!activityFound) return cb(null, false);
var address = latestAddresses[i];
address.hasActivity = true;
2015-10-29 12:35:30 -07:00
self.storage.storeAddress(address, function(err) {
2015-10-28 09:23:13 -07:00
return cb(err, true);
});
});
});
};
2015-04-27 11:38:33 -07:00
2015-01-27 11:40:21 -08:00
/**
* Creates a new address.
* @param {Object} opts
* @param {Boolean} [opts.ignoreMaxGap=false] - Ignore constraint of maximum number of consecutive addresses without activity
2015-02-02 06:55:03 -08:00
* @returns {Address} address
2015-01-27 11:40:21 -08:00
*/
2015-02-20 12:32:19 -08:00
WalletService.prototype.createAddress = function(opts, cb) {
2015-02-02 12:07:18 -08:00
var self = this;
opts = opts || {};
2016-06-03 09:34:12 -07:00
function createNewAddress(wallet, cb) {
self._canCreateAddress(opts.ignoreMaxGap, function(err, canCreate) {
2015-02-02 12:07:18 -08:00
if (err) return cb(err);
2016-06-03 09:34:12 -07:00
if (!canCreate) return cb(Errors.MAIN_ADDRESS_GAP_REACHED);
2015-02-02 15:13:13 -08:00
2016-06-03 09:34:12 -07:00
var address = wallet.createAddress(false);
2015-02-04 11:18:36 -08:00
2016-06-03 09:34:12 -07:00
self.storage.storeAddressAndWallet(wallet, address, function(err) {
if (err) return cb(err);
2016-06-03 09:34:12 -07:00
self._notify('NewAddress', {
address: address.address,
}, function() {
return cb(null, address);
2015-03-30 08:45:43 -07:00
});
2015-02-02 12:07:18 -08:00
});
});
2016-06-03 09:34:12 -07:00
};
function getFirstAddress(wallet, cb) {
self.storage.fetchAddresses(self.walletId, function(err, addresses) {
if (err) return cb(err);
if (!_.isEmpty(addresses)) return cb(null, _.first(addresses))
return createNewAddress(wallet, cb);
});
};
self._runLocked(cb, function(cb) {
self.getWallet({}, function(err, wallet) {
if (err) return cb(err);
if (!wallet.isComplete()) return cb(Errors.WALLET_NOT_COMPLETE);
var createFn = wallet.singleAddress ? getFirstAddress : createNewAddress;
return createFn(wallet, cb);
});
2015-02-02 12:07:18 -08:00
});
2015-01-27 05:18:45 -08:00
};
2015-02-03 12:32:40 -08:00
/**
* Get all addresses.
* @param {Object} opts
2015-10-29 11:17:39 -07:00
* @param {Numeric} opts.limit (optional) - Limit the resultset. Return all addresses by default.
* @param {Boolean} [opts.reverse=false] (optional) - Reverse the order of returned addresses.
2015-02-03 12:32:40 -08:00
* @returns {Address[]}
*/
2015-02-22 08:04:23 -08:00
WalletService.prototype.getMainAddresses = function(opts, cb) {
2015-02-03 12:32:40 -08:00
var self = this;
2015-10-29 11:17:39 -07:00
opts = opts || {};
2015-02-06 12:56:51 -08:00
self.storage.fetchAddresses(self.walletId, function(err, addresses) {
2015-02-03 12:32:40 -08:00
if (err) return cb(err);
var onlyMain = _.reject(addresses, {
isChange: true
});
2015-10-29 11:17:39 -07:00
if (opts.reverse) onlyMain.reverse();
if (opts.limit > 0) onlyMain = _.take(onlyMain, opts.limit);
return cb(null, onlyMain);
2015-02-03 12:32:40 -08:00
});
};
2015-01-28 07:06:34 -08:00
/**
* Verifies that a given message was actually sent by an authorized copayer.
* @param {Object} opts
* @param {string} opts.message - The message to verify.
* @param {string} opts.signature - The signature of message to verify.
* @returns {truthy} The result of the verification.
*/
2015-02-20 12:32:19 -08:00
WalletService.prototype.verifyMessageSignature = function(opts, cb) {
2015-02-02 12:07:18 -08:00
var self = this;
2015-01-28 07:06:34 -08:00
2016-05-20 06:12:41 -07:00
if (!checkRequired(opts, ['message', 'signature'], cb)) return;
2015-02-02 10:29:14 -08:00
2015-02-06 12:56:51 -08:00
self.getWallet({}, function(err, wallet) {
2015-02-02 12:07:18 -08:00
if (err) return cb(err);
2015-01-28 09:21:09 -08:00
2015-02-06 12:56:51 -08:00
var copayer = wallet.getCopayer(self.copayerId);
2015-01-28 07:06:34 -08:00
2015-08-05 12:53:06 -07:00
var isValid = !!self._getSigningKey(opts.message, opts.signature, copayer.requestPubKeys);
2015-02-02 12:07:18 -08:00
return cb(null, isValid);
});
2015-01-28 07:06:34 -08:00
};
2015-01-27 05:18:45 -08:00
2015-07-15 18:42:05 -07:00
WalletService.prototype._getBlockchainExplorer = function(network) {
2015-03-30 16:16:51 -07:00
if (!this.blockchainExplorer) {
2015-04-17 14:25:41 -07:00
var opts = {};
2015-04-15 06:59:25 -07:00
if (this.blockchainExplorerOpts && this.blockchainExplorerOpts[network]) {
2015-04-17 14:25:41 -07:00
opts = this.blockchainExplorerOpts[network];
2015-04-15 06:59:25 -07:00
}
2015-07-15 18:42:05 -07:00
// TODO: provider should be configurable
opts.provider = 'insight';
2015-04-15 06:59:25 -07:00
opts.network = network;
2016-04-07 10:52:23 -07:00
opts.userAgent = WalletService.getServiceVersion();
2015-04-15 06:59:25 -07:00
this.blockchainExplorer = new BlockchainExplorer(opts);
2015-02-02 12:07:18 -08:00
}
2015-03-30 11:34:05 -07:00
2015-03-30 16:16:51 -07:00
return this.blockchainExplorer;
2015-01-27 05:18:45 -08:00
};
2015-12-04 12:17:40 -08:00
WalletService.prototype._getUtxos = function(addresses, cb) {
2015-02-02 12:07:18 -08:00
var self = this;
2015-08-13 08:01:22 -07:00
if (addresses.length == 0) return cb(null, []);
var networkName = Bitcore.Address(addresses[0]).toObject().network;
var bc = self._getBlockchainExplorer(networkName);
bc.getUtxos(addresses, function(err, utxos) {
2015-08-13 08:01:22 -07:00
if (err) return cb(err);
var utxos = _.map(utxos, function(utxo) {
var u = _.pick(utxo, ['txid', 'vout', 'address', 'scriptPubKey', 'amount', 'satoshis', 'confirmations']);
u.confirmations = u.confirmations || 0;
u.locked = false;
u.satoshis = _.isNumber(u.satoshis) ? +u.satoshis : Utils.strip(u.amount * 1e8);
2015-08-13 08:01:22 -07:00
delete u.amount;
return u;
});
return cb(null, utxos);
});
};
WalletService.prototype._getUtxosForCurrentWallet = function(addresses, cb) {
2015-08-13 08:01:22 -07:00
var self = this;
2015-08-13 07:00:27 -07:00
2015-07-20 09:44:39 -07:00
function utxoKey(utxo) {
return utxo.txid + '|' + utxo.vout
};
2015-02-02 12:07:18 -08:00
var allAddresses, allUtxos, utxoIndex;
async.series([
2015-02-02 12:07:18 -08:00
function(next) {
if (_.isArray(addresses)) {
allAddresses = addresses;
return next();
}
self.storage.fetchAddresses(self.walletId, function(err, addresses) {
allAddresses = addresses;
return next();
});
},
function(next) {
if (allAddresses.length == 0) return cb(null, []);
2015-08-02 15:48:18 -07:00
var addressStrs = _.pluck(allAddresses, 'address');
2015-12-04 12:17:40 -08:00
self._getUtxos(addressStrs, function(err, utxos) {
if (err) return next(err);
2016-06-03 16:59:54 -07:00
if (utxos.length == 0) return cb(null, []);
allUtxos = utxos;
utxoIndex = _.indexBy(allUtxos, utxoKey);
return next();
});
},
function(next) {
self.getPendingTxs({}, function(err, txps) {
if (err) return next(err);
2015-02-02 12:07:18 -08:00
var lockedInputs = _.map(_.flatten(_.pluck(txps, 'inputs')), utxoKey);
_.each(lockedInputs, function(input) {
if (utxoIndex[input]) {
utxoIndex[input].locked = true;
}
});
return next();
2015-02-02 12:07:18 -08:00
});
},
function(next) {
2016-05-24 07:25:54 -07:00
var now = Math.floor(Date.now() / 1000);
// Fetch latest broadcasted txs and remove any spent inputs from the
// list of UTXOs returned by the block explorer. This counteracts any out-of-sync
// effects between broadcasting a tx and getting the list of UTXOs.
// This is especially true in the case of having multiple instances of the block explorer.
self.storage.fetchBroadcastedTxs(self.walletId, {
2016-05-24 07:25:54 -07:00
minTs: now - 24 * 3600,
limit: 100
}, function(err, txs) {
if (err) return next(err);
var spentInputs = _.map(_.flatten(_.pluck(txs, 'inputs')), utxoKey);
_.each(spentInputs, function(input) {
if (utxoIndex[input]) {
utxoIndex[input].spent = true;
}
});
allUtxos = _.reject(allUtxos, {
spent: true
});
return next();
});
},
function(next) {
// Needed for the clients to sign UTXOs
var addressToPath = _.indexBy(allAddresses, 'address');
_.each(allUtxos, function(utxo) {
utxo.path = addressToPath[utxo.address].path;
utxo.publicKeys = addressToPath[utxo.address].publicKeys;
});
return next();
},
], function(err) {
return cb(err, allUtxos);
});
2015-01-27 05:18:45 -08:00
};
2015-08-13 08:01:22 -07:00
/**
* Returns list of UTXOs
* @param {Object} opts
* @param {Array} opts.addresses (optional) - List of addresses from where to fetch UTXOs.
* @returns {Array} utxos - List of UTXOs.
*/
WalletService.prototype.getUtxos = function(opts, cb) {
var self = this;
opts = opts || {};
if (_.isUndefined(opts.addresses)) {
self._getUtxosForCurrentWallet(null, cb);
2015-08-13 08:01:22 -07:00
} else {
2015-12-04 12:17:40 -08:00
self._getUtxos(opts.addresses, cb);
2015-08-13 08:01:22 -07:00
}
};
WalletService.prototype._totalizeUtxos = function(utxos) {
2015-07-20 08:45:12 -07:00
var balance = {
totalAmount: _.sum(utxos, 'satoshis'),
lockedAmount: _.sum(_.filter(utxos, 'locked'), 'satoshis'),
totalConfirmedAmount: _.sum(_.filter(utxos, 'confirmations'), 'satoshis'),
lockedConfirmedAmount: _.sum(_.filter(_.filter(utxos, 'locked'), 'confirmations'), 'satoshis'),
2015-07-20 08:45:12 -07:00
};
balance.availableAmount = balance.totalAmount - balance.lockedAmount;
balance.availableConfirmedAmount = balance.totalConfirmedAmount - balance.lockedConfirmedAmount;
return balance;
};
2015-01-30 12:37:30 -08:00
2015-07-27 08:19:27 -07:00
WalletService.prototype._computeBytesToSendMax = function(utxos, cb) {
2015-06-18 09:31:53 -07:00
var self = this;
2015-12-22 12:13:50 -08:00
var size = {
all: 0,
confirmed: 0
};
2015-07-20 09:44:39 -07:00
var unlockedUtxos = _.reject(utxos, 'locked');
2015-12-22 12:13:50 -08:00
if (_.isEmpty(unlockedUtxos)) return cb(null, size);
2015-06-18 09:31:53 -07:00
self.getWallet({}, function(err, wallet) {
if (err) return cb(err);
2015-11-26 05:19:12 -08:00
var txp = Model.TxProposalLegacy.create({
2015-07-27 08:19:27 -07:00
walletId: self.walletId,
requiredSignatures: wallet.m,
walletN: wallet.n,
});
2015-12-22 11:53:25 -08:00
2015-07-27 08:19:27 -07:00
txp.inputs = unlockedUtxos;
2015-12-22 11:53:25 -08:00
size.all = txp.getEstimatedSize();
2015-07-27 08:19:27 -07:00
2015-12-22 11:53:25 -08:00
txp.inputs = _.filter(unlockedUtxos, 'confirmations');
size.confirmed = txp.getEstimatedSize();
2015-07-27 08:19:27 -07:00
return cb(null, size);
2015-06-18 09:31:53 -07:00
});
};
2015-12-04 12:17:40 -08:00
WalletService.prototype._getBalanceFromAddresses = function(addresses, cb) {
2015-02-02 12:07:18 -08:00
var self = this;
2015-01-30 12:37:30 -08:00
2015-12-04 12:17:40 -08:00
self._getUtxosForCurrentWallet(addresses, function(err, utxos) {
2015-02-02 12:07:18 -08:00
if (err) return cb(err);
2015-12-04 12:17:40 -08:00
var balance = self._totalizeUtxos(utxos);
2015-01-30 12:37:30 -08:00
2015-03-06 09:58:22 -08:00
// Compute balance by address
var byAddress = {};
2016-02-18 07:00:36 -08:00
_.each(_.indexBy(_.sortBy(utxos, 'address'), 'address'), function(value, key) {
2015-03-06 09:58:22 -08:00
byAddress[key] = {
address: key,
path: value.path,
amount: 0,
};
});
_.each(utxos, function(utxo) {
byAddress[utxo.address].amount += utxo.satoshis;
});
balance.byAddress = _.values(byAddress);
2015-07-27 08:38:12 -07:00
self._computeBytesToSendMax(utxos, function(err, size) {
2015-06-18 09:31:53 -07:00
if (err) {
2015-12-04 10:12:43 -08:00
log.error('Could not compute size of send max transaction', err);
}
2015-12-22 11:53:25 -08:00
balance.totalBytesToSendMax = _.isNumber(size.all) ? size.all : null;
balance.totalBytesToSendConfirmedMax = _.isNumber(size.confirmed) ? size.confirmed : null;
return cb(null, balance);
});
2015-02-02 12:07:18 -08:00
});
2015-01-30 12:37:30 -08:00
};
WalletService.prototype._getBalanceOneStep = function(opts, cb) {
2015-12-04 12:17:40 -08:00
var self = this;
self.storage.fetchAddresses(self.walletId, function(err, addresses) {
if (err) return cb(err);
self._getBalanceFromAddresses(addresses, function(err, balance) {
if (err) return cb(err);
// Update cache
async.series([
function(next) {
self.storage.cleanActiveAddresses(self.walletId, next);
},
function(next) {
var active = _.pluck(balance.byAddress, 'address')
self.storage.storeActiveAddresses(self.walletId, active, next);
},
], function(err) {
2015-12-09 10:09:47 -08:00
if (err) {
log.warn('Could not update wallet cache', err);
}
return cb(null, balance);
});
2015-12-04 12:17:40 -08:00
});
});
};
2015-12-10 06:43:47 -08:00
WalletService.prototype._getActiveAddresses = function(cb) {
var self = this;
self.storage.fetchActiveAddresses(self.walletId, function(err, active) {
if (err) {
log.warn('Could not fetch active addresses from cache', err);
return cb();
2015-12-10 06:43:47 -08:00
}
if (!_.isArray(active)) return cb();
2015-12-10 06:43:47 -08:00
self.storage.fetchAddresses(self.walletId, function(err, allAddresses) {
if (err) return cb(err);
var now = Math.floor(Date.now() / 1000);
var recent = _.pluck(_.filter(allAddresses, function(address) {
return address.createdOn > (now - 24 * 3600);
}), 'address');
var result = _.union(active, recent);
var index = _.indexBy(allAddresses, 'address');
result = _.compact(_.map(result, function(r) {
2015-12-10 06:43:47 -08:00
return index[r];
}));
2015-12-10 06:43:47 -08:00
return cb(null, result);
});
});
};
2015-12-04 12:17:40 -08:00
/**
* Get wallet balance.
* @param {Object} opts
* @param {Boolean} opts.twoStep[=false] - Optional - Use 2 step balance computation for improved performance
2015-12-04 12:17:40 -08:00
* @returns {Object} balance - Total amount & locked amount.
*/
WalletService.prototype.getBalance = function(opts, cb) {
2015-12-04 12:17:40 -08:00
var self = this;
opts = opts || {};
if (!opts.twoStep)
return self._getBalanceOneStep(opts, cb);
2015-12-10 07:36:30 -08:00
self.storage.countAddresses(self.walletId, function(err, nbAddresses) {
2015-12-10 06:43:47 -08:00
if (err) return cb(err);
2015-12-10 07:36:30 -08:00
if (nbAddresses < Defaults.TWO_STEP_BALANCE_THRESHOLD) {
return self._getBalanceOneStep(opts, cb);
2015-12-09 10:09:47 -08:00
}
2015-12-10 07:36:30 -08:00
self._getActiveAddresses(function(err, activeAddresses) {
if (err) return cb(err);
if (!_.isArray(activeAddresses)) {
return self._getBalanceOneStep(opts, cb);
2015-12-10 07:36:30 -08:00
} else {
2015-12-14 12:33:04 -08:00
log.debug('Requesting partial balance for ' + activeAddresses.length + ' out of ' + nbAddresses + ' addresses');
2015-12-10 07:36:30 -08:00
self._getBalanceFromAddresses(activeAddresses, function(err, partialBalance) {
if (err) return cb(err);
cb(null, partialBalance);
setTimeout(function() {
self._getBalanceOneStep(opts, function(err, fullBalance) {
2015-12-10 07:36:30 -08:00
if (err) return;
if (!_.isEqual(partialBalance, fullBalance)) {
2016-02-18 07:00:36 -08:00
log.info('Balance in active addresses differs from final balance');
self._notify('BalanceUpdated', fullBalance, {
isGlobal: true
});
2015-12-10 07:36:30 -08:00
}
});
}, 1);
return;
});
}
});
2015-12-04 12:17:40 -08:00
});
};
2016-02-19 06:11:43 -08:00
/**
* Return info needed to send all funds in the wallet
* @param {Object} opts
* @param {string} opts.feePerKb - The fee per KB used to compute the TX.
2016-02-25 06:27:30 -08:00
* @param {string} opts.excludeUnconfirmedUtxos[=false] - Optional. Do not use UTXOs of unconfirmed transactions as inputs
* @param {string} opts.returnInputs[=false] - Optional. Return the list of UTXOs that would be included in the tx.
2016-02-19 06:11:43 -08:00
* @returns {Object} sendMaxInfo
*/
WalletService.prototype.getSendMaxInfo = function(opts, cb) {
var self = this;
opts = opts || {};
2016-05-20 06:12:41 -07:00
if (!checkRequired(opts, ['feePerKb'], cb)) return;
2016-02-19 06:11:43 -08:00
self.getWallet({}, function(err, wallet) {
if (err) return cb(err);
self._getUtxosForCurrentWallet(null, function(err, utxos) {
if (err) return cb(err);
var info = {
size: 0,
amount: 0,
fee: 0,
2016-02-24 11:42:45 -08:00
inputs: [],
2016-03-18 12:32:11 -07:00
utxosBelowFee: 0,
amountBelowFee: 0,
utxosAboveMaxSize: 0,
amountAboveMaxSize: 0,
2016-02-19 06:11:43 -08:00
};
var inputs = _.reject(utxos, 'locked');
2016-02-25 06:27:30 -08:00
if (!!opts.excludeUnconfirmedUtxos) {
2016-02-23 12:28:59 -08:00
inputs = _.filter(inputs, 'confirmations');
2016-02-19 06:11:43 -08:00
}
2016-03-18 12:32:11 -07:00
inputs = _.sortBy(inputs, function(input) {
return -input.satoshis;
});
2016-02-19 06:11:43 -08:00
if (_.isEmpty(inputs)) return cb(null, info);
var txp = Model.TxProposal.create({
walletId: self.walletId,
network: wallet.network,
walletM: wallet.m,
walletN: wallet.n,
feePerKb: opts.feePerKb,
});
2016-03-18 12:32:11 -07:00
var baseTxpSize = txp.getEstimatedSize();
var baseTxpFee = baseTxpSize * txp.feePerKb / 1000.;
var sizePerInput = txp.getEstimatedSizeForSingleInput();
var feePerInput = sizePerInput * txp.feePerKb / 1000.;
var partitionedByAmount = _.partition(inputs, function(input) {
return input.satoshis > feePerInput;
});
info.utxosBelowFee = partitionedByAmount[1].length;
info.amountBelowFee = _.sum(partitionedByAmount[1], 'satoshis');
inputs = partitionedByAmount[0];
_.each(inputs, function(input, i) {
var sizeInKb = (baseTxpSize + (i + 1) * sizePerInput) / 1000.;
if (sizeInKb > Defaults.MAX_TX_SIZE_IN_KB) {
info.utxosAboveMaxSize = inputs.length - i;
info.amountAboveMaxSize = _.sum(_.slice(inputs, i), 'satoshis');
2016-02-23 12:28:59 -08:00
return false;
}
2016-03-18 12:32:11 -07:00
txp.inputs.push(input);
2016-02-19 06:11:43 -08:00
});
2016-03-18 11:56:10 -07:00
if (_.isEmpty(txp.inputs)) return cb(null, info);
2016-02-19 06:11:43 -08:00
info.size = txp.getEstimatedSize();
info.fee = txp.getEstimatedFee();
info.amount = _.sum(txp.inputs, 'satoshis') - info.fee;
2016-03-16 12:42:39 -07:00
if (opts.returnInputs) {
2016-03-16 12:46:11 -07:00
info.inputs = _.shuffle(txp.inputs);
2016-03-16 12:42:39 -07:00
}
2016-02-19 06:11:43 -08:00
return cb(null, info);
});
});
};
2016-03-04 12:02:05 -08:00
2015-07-15 18:42:05 -07:00
WalletService.prototype._sampleFeeLevels = function(network, points, cb) {
var self = this;
2015-07-15 18:44:34 -07:00
var bc = self._getBlockchainExplorer(network);
2015-08-12 14:39:19 -07:00
bc.estimateFee(points, function(err, result) {
if (err) {
log.error('Error estimating fee', err);
return cb(err);
}
2016-02-23 11:00:34 -08:00
var failed = [];
2015-08-12 14:39:19 -07:00
var levels = _.zipObject(_.map(points, function(p) {
var feePerKb = _.isObject(result) ? +result[p] : -1;
2016-02-23 11:00:34 -08:00
if (feePerKb < 0)
failed.push(p);
return [p, Utils.strip(feePerKb * 1e8)];
2015-08-12 14:39:19 -07:00
}));
2016-02-23 11:00:34 -08:00
if (failed.length) {
var logger = network == 'livenet' ? log.warn : log.debug;
2016-03-04 12:02:05 -08:00
logger('Could not compute fee estimation in ' + network + ': ' + failed.join(', ') + ' blocks.');
2016-02-23 11:00:34 -08:00
}
2015-08-12 14:39:19 -07:00
return cb(null, levels);
2015-07-15 18:42:05 -07:00
});
};
2016-03-04 12:02:05 -08:00
WalletService._feeLevelCache = {};
2015-07-15 18:42:05 -07:00
/**
* Returns fee levels for the current state of the network.
* @param {Object} opts
* @param {string} [opts.network = 'livenet'] - The Bitcoin network to estimate fee levels from.
* @returns {Object} feeLevels - A list of fee levels & associated amount per kB in satoshi.
*/
WalletService.prototype.getFeeLevels = function(opts, cb) {
var self = this;
opts = opts || {};
var network = opts.network || 'livenet';
if (network != 'livenet' && network != 'testnet')
return cb(new ClientError('Invalid network'));
2016-03-04 12:02:05 -08:00
var cache = WalletService._feeLevelCache[network] || {};
var levels = Defaults.FEE_LEVELS;
2015-07-15 18:42:05 -07:00
var samplePoints = _.uniq(_.pluck(levels, 'nbBlocks'));
self._sampleFeeLevels(network, samplePoints, function(err, feeSamples) {
2015-07-16 12:17:58 -07:00
var values = _.map(levels, function(level) {
2015-07-27 05:00:37 -07:00
var result = {
2015-07-16 12:17:58 -07:00
level: level.name,
};
if (err || feeSamples[level.nbBlocks] < 0) {
2016-03-04 12:02:05 -08:00
if (cache[level.nbBlocks] >= 0) {
result.feePerKb = cache[level.nbBlocks];
result.nbBlocks = level.nbBlocks;
} else {
result.feePerKb = level.defaultValue;
result.nbBlocks = null;
}
} else {
result.feePerKb = feeSamples[level.nbBlocks];
result.nbBlocks = level.nbBlocks;
}
return result;
2015-07-16 12:17:58 -07:00
});
2015-07-15 18:42:05 -07:00
2016-03-04 12:02:05 -08:00
var obtainedValues = _.zipObject(_.map(_.reject(values, {
nbBlocks: null
}), function(v) {
return [v.nbBlocks, v.feePerKb];
}));
WalletService._feeLevelCache[network] = _.assign(cache, obtainedValues);
2015-07-15 18:42:05 -07:00
return cb(null, values);
});
};
2016-03-08 10:24:58 -08:00
WalletService.prototype._estimateFee = function(txp) {
txp.estimateFee();
};
WalletService.prototype._checkTx = function(txp) {
var bitcoreError;
var serializationOpts = {
disableIsFullySigned: true
};
if (!_.startsWith(txp.version, '1.')) {
serializationOpts.disableSmallFees = true;
serializationOpts.disableLargeFees = true;
}
2016-03-04 12:10:48 -08:00
if (txp.getEstimatedSize() / 1000 > Defaults.MAX_TX_SIZE_IN_KB)
return Errors.TX_MAX_SIZE_EXCEEDED;
try {
var bitcoreTx = txp.getBitcoreTx();
bitcoreError = bitcoreTx.getSerializationError(serializationOpts);
if (!bitcoreError) {
txp.fee = bitcoreTx.getFee();
}
} catch (ex) {
log.error('Error building Bitcore transaction', ex);
return ex;
}
if (bitcoreError instanceof Bitcore.errors.Transaction.FeeError)
return Errors.INSUFFICIENT_FUNDS_FOR_FEE;
if (bitcoreError instanceof Bitcore.errors.Transaction.DustOutputs)
return Errors.DUST_AMOUNT;
return bitcoreError;
};
WalletService.prototype._selectTxInputs = function(txp, utxosToExclude, cb) {
2015-03-11 11:04:42 -07:00
var self = this;
2016-02-29 13:17:14 -08:00
//todo: check inputs are ours and have enough value
if (txp.inputs && !_.isEmpty(txp.inputs)) {
if (!_.isNumber(txp.fee))
self._estimateFee(txp);
2016-03-08 10:24:58 -08:00
return cb(self._checkTx(txp));
2016-02-29 13:17:14 -08:00
}
var txpAmount = txp.getTotalAmount();
var baseTxpSize = txp.getEstimatedSize();
var baseTxpFee = baseTxpSize * txp.feePerKb / 1000.;
var sizePerInput = txp.getEstimatedSizeForSingleInput();
var feePerInput = sizePerInput * txp.feePerKb / 1000.;
2016-03-07 10:44:40 -08:00
function sanitizeUtxos(utxos) {
2016-02-29 13:17:14 -08:00
var excludeIndex = _.reduce(utxosToExclude, function(res, val) {
res[val] = val;
return res;
}, {});
return _.filter(utxos, function(utxo) {
if (utxo.locked) return false;
if (utxo.satoshis <= feePerInput) return false;
if (txp.excludeUnconfirmedUtxos && !utxo.confirmations) return false;
2016-03-07 10:44:40 -08:00
if (excludeIndex[utxo.txid + ":" + utxo.vout]) return false;
return true;
});
};
2016-02-29 13:17:14 -08:00
function partitionUtxos(utxos) {
return _.groupBy(utxos, function(utxo) {
if (utxo.confirmations == 0) return '0'
if (utxo.confirmations < 6) return '<6';
return '6+';
});
};
2016-03-07 07:30:10 -08:00
function select(utxos, cb) {
2016-03-04 12:10:48 -08:00
var totalValueInUtxos = _.sum(utxos, 'satoshis');
var netValueInUtxos = totalValueInUtxos - baseTxpFee - (utxos.length * feePerInput);
2016-03-02 11:44:38 -08:00
if (totalValueInUtxos < txpAmount) {
2016-03-07 08:00:53 -08:00
log.debug('Total value in all utxos (' + Utils.formatAmountInBtc(totalValueInUtxos) + ') is insufficient to cover for txp amount (' + Utils.formatAmountInBtc(txpAmount) + ')');
2016-03-07 07:30:10 -08:00
return cb(Errors.INSUFFICIENT_FUNDS);
2016-03-04 12:10:48 -08:00
}
if (netValueInUtxos < txpAmount) {
2016-03-07 08:00:53 -08:00
log.debug('Value after fees in all utxos (' + Utils.formatAmountInBtc(netValueInUtxos) + ') is insufficient to cover for txp amount (' + Utils.formatAmountInBtc(txpAmount) + ')');
2016-03-07 07:30:10 -08:00
return cb(Errors.INSUFFICIENT_FUNDS_FOR_FEE);
2016-03-02 11:44:38 -08:00
}
2016-02-29 13:17:14 -08:00
2016-03-03 05:15:42 -08:00
var bigInputThreshold = txpAmount * Defaults.UTXO_SELECTION_MAX_SINGLE_UTXO_FACTOR + (baseTxpFee + feePerInput);
2016-03-07 08:00:53 -08:00
log.debug('Big input threshold ' + Utils.formatAmountInBtc(bigInputThreshold));
2016-03-02 12:33:44 -08:00
2016-02-29 13:17:14 -08:00
var partitions = _.partition(utxos, function(utxo) {
2016-03-02 12:33:44 -08:00
return utxo.satoshis > bigInputThreshold;
2016-02-29 13:17:14 -08:00
});
var bigInputs = _.sortBy(partitions[0], 'satoshis');
var smallInputs = _.sortBy(partitions[1], function(utxo) {
return -utxo.satoshis;
});
2016-03-07 08:00:53 -08:00
log.debug('Considering ' + bigInputs.length + ' big inputs (' + Utils.formatUtxos(bigInputs) + ')');
log.debug('Considering ' + smallInputs.length + ' small inputs (' + Utils.formatUtxos(smallInputs) + ')');
2016-02-29 13:17:14 -08:00
2016-03-02 12:33:44 -08:00
var total = 0;
2016-03-11 06:13:29 -08:00
var netTotal = -baseTxpFee;
2016-03-02 12:33:44 -08:00
var selected = [];
2016-03-08 10:24:58 -08:00
var fee;
2016-03-04 12:10:48 -08:00
var error;
2016-02-29 13:17:14 -08:00
2016-03-02 06:26:07 -08:00
_.each(smallInputs, function(input, i) {
2016-03-07 08:00:53 -08:00
log.debug('Input #' + i + ': ' + Utils.formatUtxos(input));
2016-03-02 06:26:07 -08:00
2016-03-08 10:24:58 -08:00
var netInputAmount = input.satoshis - feePerInput;
log.debug('The input contributes ' + Utils.formatAmountInBtc(netInputAmount));
2016-03-02 06:26:07 -08:00
2016-02-29 13:17:14 -08:00
selected.push(input);
2016-03-08 10:24:58 -08:00
total += input.satoshis;
netTotal += netInputAmount;
2016-03-02 06:26:07 -08:00
var txpSize = baseTxpSize + selected.length * sizePerInput;
2016-03-08 10:24:58 -08:00
fee = Math.round(baseTxpFee + selected.length * feePerInput);
2016-03-01 12:40:07 -08:00
2016-03-08 10:24:58 -08:00
log.debug('Tx size: ' + Utils.formatSize(txpSize) + ', Tx fee: ' + Utils.formatAmountInBtc(fee));
2016-03-04 12:10:48 -08:00
2016-03-08 10:24:58 -08:00
var feeVsAmountRatio = fee / txpAmount;
var amountVsUtxoRatio = netInputAmount / txpAmount;
2016-03-01 12:40:07 -08:00
2016-03-09 05:45:37 -08:00
log.debug('Fee/Tx amount: ' + Utils.formatRatio(feeVsAmountRatio) + ' (max: ' + Utils.formatRatio(Defaults.UTXO_SELECTION_MAX_FEE_VS_TX_AMOUNT_FACTOR) + ')');
log.debug('Tx amount/Input amount:' + Utils.formatRatio(amountVsUtxoRatio) + ' (min: ' + Utils.formatRatio(Defaults.UTXO_SELECTION_MIN_TX_AMOUNT_VS_UTXO_FACTOR) + ')');
2016-03-01 12:40:07 -08:00
2016-03-04 12:10:48 -08:00
if (txpSize / 1000. > Defaults.MAX_TX_SIZE_IN_KB) {
2016-03-07 08:00:53 -08:00
log.debug('Breaking because tx size (' + Utils.formatSize(txpSize) + ') is too big (max: ' + Utils.formatSize(Defaults.MAX_TX_SIZE_IN_KB * 1000.) + ')');
2016-03-04 12:10:48 -08:00
error = Errors.TX_MAX_SIZE_EXCEEDED;
return false;
}
2016-03-02 06:26:07 -08:00
if (!_.isEmpty(bigInputs)) {
if (amountVsUtxoRatio < Defaults.UTXO_SELECTION_MIN_TX_AMOUNT_VS_UTXO_FACTOR) {
log.debug('Breaking because utxo is too small compared to tx amount');
2016-03-02 06:26:07 -08:00
return false;
}
2016-03-01 12:40:07 -08:00
2016-03-09 05:45:37 -08:00
if (feeVsAmountRatio > Defaults.UTXO_SELECTION_MAX_FEE_VS_TX_AMOUNT_FACTOR) {
var feeVsSingleInputFeeRatio = fee / (baseTxpFee + feePerInput);
log.debug('Fee/Single-input fee: ' + Utils.formatRatio(feeVsSingleInputFeeRatio) + ' (max: ' + Utils.formatRatio(Defaults.UTXO_SELECTION_MAX_FEE_VS_SINGLE_UTXO_FEE_FACTOR) + ')' + ' loses wrt single-input tx: ' + Utils.formatAmountInBtc((selected.length - 1) * feePerInput));
if (feeVsSingleInputFeeRatio > Defaults.UTXO_SELECTION_MAX_FEE_VS_SINGLE_UTXO_FEE_FACTOR) {
log.debug('Breaking because fee is too significant compared to tx amount and it is too expensive compared to using single input');
return false;
}
2016-03-02 06:26:07 -08:00
}
}
2016-03-01 12:40:07 -08:00
2016-03-08 10:24:58 -08:00
log.debug('Cumuled total so far: ' + Utils.formatAmountInBtc(total) + ', Net total so far: ' + Utils.formatAmountInBtc(netTotal));
if (netTotal >= txpAmount) {
var changeAmount = Math.round(total - txpAmount - fee);
log.debug('Tx change: ', Utils.formatAmountInBtc(changeAmount));
2016-06-10 09:50:57 -07:00
var dustThreshold = Math.max(Defaults.MIN_OUTPUT_AMOUNT, Bitcore.Transaction.DUST_AMOUNT);
if (changeAmount > 0 && changeAmount <= dustThreshold) {
log.debug('Change below dust threshold (' + Utils.formatAmountInBtc(dustThreshold) + '). Incrementing fee to remove change.');
// Remove dust change by incrementing fee
2016-03-08 10:24:58 -08:00
fee += changeAmount;
}
return false;
}
2016-02-29 13:17:14 -08:00
});
2016-03-08 10:24:58 -08:00
if (netTotal < txpAmount) {
log.debug('Could not reach Txp total (' + Utils.formatAmountInBtc(txpAmount) + '), still missing: ' + Utils.formatAmountInBtc(txpAmount - netTotal));
2016-02-29 13:17:14 -08:00
selected = [];
if (!_.isEmpty(bigInputs)) {
2016-03-02 06:26:07 -08:00
var input = _.first(bigInputs);
2016-03-08 10:24:58 -08:00
log.debug('Using big input: ', Utils.formatUtxos(input));
2016-03-02 06:26:07 -08:00
total = input.satoshis;
2016-03-08 10:24:58 -08:00
fee = Math.round(baseTxpFee + feePerInput);
netTotal = total - fee;
2016-03-02 06:26:07 -08:00
selected = [input];
2016-02-29 13:17:14 -08:00
}
}
2016-03-02 06:26:07 -08:00
2016-03-04 12:10:48 -08:00
if (_.isEmpty(selected)) {
log.debug('Could not find enough funds within this utxo subset');
2016-03-07 07:30:10 -08:00
return cb(error || Errors.INSUFFICIENT_FUNDS_FOR_FEE);
2016-03-02 06:26:07 -08:00
}
2016-03-08 10:24:58 -08:00
return cb(null, selected, fee);
2016-02-29 13:17:14 -08:00
};
2016-03-07 08:00:53 -08:00
log.debug('Selecting inputs for a ' + Utils.formatAmountInBtc(txp.getTotalAmount()) + ' txp');
2016-03-02 12:33:44 -08:00
2016-02-29 13:17:14 -08:00
self._getUtxosForCurrentWallet(null, function(err, utxos) {
if (err) return cb(err);
var totalAmount;
var availableAmount;
var balance = self._totalizeUtxos(utxos);
if (txp.excludeUnconfirmedUtxos) {
totalAmount = balance.totalConfirmedAmount;
availableAmount = balance.availableConfirmedAmount;
} else {
totalAmount = balance.totalAmount;
availableAmount = balance.availableAmount;
}
if (totalAmount < txp.getTotalAmount()) return cb(Errors.INSUFFICIENT_FUNDS);
if (availableAmount < txp.getTotalAmount()) return cb(Errors.LOCKED_FUNDS);
utxos = sanitizeUtxos(utxos);
2016-02-29 13:17:14 -08:00
2016-03-07 08:00:53 -08:00
log.debug('Considering ' + utxos.length + ' utxos (' + Utils.formatUtxos(utxos) + ')');
2016-03-02 12:33:44 -08:00
2016-03-07 07:30:10 -08:00
var groups = [6, 1];
if (!txp.excludeUnconfirmedUtxos) groups.push(0);
2016-02-29 13:17:14 -08:00
var inputs = [];
2016-03-08 10:24:58 -08:00
var fee;
2016-03-07 07:30:10 -08:00
var selectionError;
var i = 0;
2016-02-29 13:17:14 -08:00
var lastGroupLength;
2016-03-07 07:30:10 -08:00
async.whilst(function() {
return i < groups.length && _.isEmpty(inputs);
}, function(next) {
var group = groups[i++];
2016-02-29 13:17:14 -08:00
var candidateUtxos = _.filter(utxos, function(utxo) {
return utxo.confirmations >= group;
});
2016-03-02 12:33:44 -08:00
log.debug('Group >= ' + group);
2016-02-29 13:17:14 -08:00
// If this group does not have any new elements, skip it
2016-03-02 12:33:44 -08:00
if (lastGroupLength === candidateUtxos.length) {
log.debug('This group is identical to the one already explored');
2016-03-07 07:30:10 -08:00
return next();
2016-03-02 12:33:44 -08:00
}
2016-02-29 13:17:14 -08:00
2016-03-07 08:00:53 -08:00
log.debug('Candidate utxos: ' + Utils.formatUtxos(candidateUtxos));
2016-03-02 12:33:44 -08:00
lastGroupLength = candidateUtxos.length;
2016-02-29 13:17:14 -08:00
2016-03-08 10:24:58 -08:00
select(candidateUtxos, function(err, selectedInputs, selectedFee) {
2016-03-07 07:30:10 -08:00
if (err) {
log.debug('No inputs selected on this group: ', err);
selectionError = err;
return next();
}
2016-02-29 13:17:14 -08:00
2016-03-07 07:30:10 -08:00
selectionError = null;
2016-03-08 10:24:58 -08:00
inputs = selectedInputs;
fee = selectedFee;
2016-02-29 13:17:14 -08:00
2016-03-07 08:00:53 -08:00
log.debug('Selected inputs from this group: ' + Utils.formatUtxos(inputs));
2016-03-08 10:24:58 -08:00
log.debug('Fee for this selection: ' + Utils.formatAmountInBtc(fee));
2016-03-07 07:30:10 -08:00
return next();
});
}, function(err) {
if (err) return cb(err);
if (selectionError || _.isEmpty(inputs)) return cb(selectionError || new Error('Could not select tx inputs'));
2016-02-29 13:17:14 -08:00
2016-03-08 10:47:31 -08:00
txp.setInputs(_.shuffle(inputs));
2016-03-08 10:24:58 -08:00
txp.fee = fee;
2016-02-29 13:17:14 -08:00
2016-03-08 10:24:58 -08:00
var err = self._checkTx(txp);
2016-03-04 12:10:48 -08:00
2016-03-07 07:30:10 -08:00
if (!err) {
2016-04-28 16:00:02 -07:00
var change = _.sum(txp.inputs, 'satoshis') - _.sum(txp.outputs, 'amount') - txp.fee;
log.debug('Successfully built transaction. Total fees: ' + Utils.formatAmountInBtc(txp.fee) + ', total change: ' + Utils.formatAmountInBtc(change));
2016-03-07 07:30:10 -08:00
} else {
log.warn('Error building transaction', err);
}
2016-03-03 05:15:42 -08:00
2016-03-07 07:30:10 -08:00
return cb(err);
});
2016-02-29 13:17:14 -08:00
});
};
2015-11-26 05:50:22 -08:00
WalletService.prototype._canCreateTx = function(cb) {
2015-06-12 12:05:33 -07:00
var self = this;
2015-11-26 05:50:22 -08:00
self.storage.fetchLastTxs(self.walletId, self.copayerId, 5 + Defaults.BACKOFF_OFFSET, function(err, txs) {
2015-06-12 12:05:33 -07:00
if (err) return cb(err);
if (!txs.length)
2015-06-12 12:05:33 -07:00
return cb(null, true);
var lastRejections = _.takeWhile(txs, {
status: 'rejected'
});
2015-06-12 12:05:33 -07:00
var exceededRejections = lastRejections.length - Defaults.BACKOFF_OFFSET;
if (exceededRejections <= 0)
2015-06-12 12:05:33 -07:00
return cb(null, true);
2015-06-12 12:05:33 -07:00
var lastTxTs = txs[0].createdOn;
var now = Math.floor(Date.now() / 1000);
var timeSinceLastRejection = now - lastTxTs;
var backoffTime = Defaults.BACKOFF_TIME;
2015-06-12 12:05:33 -07:00
if (timeSinceLastRejection <= backoffTime)
log.debug('Not allowing to create TX: timeSinceLastRejection/backoffTime', timeSinceLastRejection, backoffTime);
return cb(null, timeSinceLastRejection > backoffTime);
});
};
2016-05-20 06:12:41 -07:00
WalletService.prototype._validateOutputs = function(opts, wallet, cb) {
for (var i = 0; i < opts.outputs.length; i++) {
var output = opts.outputs[i];
output.valid = false;
if (!checkRequired(output, ['toAddress', 'amount'])) {
return new ClientError('Argument missing in output #' + (i + 1) + '.');
}
var toAddress = {};
try {
toAddress = new Bitcore.Address(output.toAddress);
} catch (ex) {
return Errors.INVALID_ADDRESS;
}
if (toAddress.network != wallet.getNetworkName()) {
return Errors.INCORRECT_ADDRESS_NETWORK;
}
2016-02-25 06:27:30 -08:00
if (!_.isNumber(output.amount) || _.isNaN(output.amount) || output.amount <= 0) {
return new ClientError('Invalid amount');
}
if (output.amount < Bitcore.Transaction.DUST_AMOUNT) {
return Errors.DUST_AMOUNT;
}
2016-02-25 06:27:30 -08:00
output.valid = true;
}
return null;
};
2015-06-12 12:05:33 -07:00
2015-10-30 11:24:47 -07:00
WalletService._getProposalHash = function(proposalHeader) {
function getOldHash(toAddress, amount, message, payProUrl) {
return [toAddress, amount, (message || ''), (payProUrl || '')].join('|');
};
// For backwards compatibility
if (arguments.length > 1) {
return getOldHash.apply(this, arguments);
}
return Stringify(proposalHeader);
};
2015-01-27 07:54:17 -08:00
/**
* Creates a new transaction proposal.
2015-01-27 11:40:21 -08:00
* @param {Object} opts
* @param {string} opts.type - Proposal type.
* @param {string} opts.toAddress || opts.outputs[].toAddress - Destination address.
* @param {number} opts.amount || opts.outputs[].amount - Amount to transfer in satoshi.
* @param {string} opts.outputs[].message - A message to attach to this output.
2015-01-27 07:54:17 -08:00
* @param {string} opts.message - A message to attach to this transaction.
2015-03-25 12:44:47 -07:00
* @param {string} opts.proposalSignature - S(toAddress|amount|message|payProUrl). Used by other copayers to verify the proposal.
* @param {string} opts.inputs - Optional. Inputs for this TX
2015-06-11 11:26:34 -07:00
* @param {string} opts.feePerKb - Optional: Use an alternative fee per KB for this TX
* @param {string} opts.payProUrl - Optional: Paypro URL for peers to verify TX
* @param {string} opts.excludeUnconfirmedUtxos - Optional: Do not use UTXOs of unconfirmed transactions as inputs
2015-02-02 06:55:03 -08:00
* @returns {TxProposal} Transaction proposal.
2015-01-27 07:54:17 -08:00
*/
2015-11-26 07:52:51 -08:00
WalletService.prototype.createTxLegacy = function(opts, cb) {
2015-02-02 12:07:18 -08:00
var self = this;
2015-01-27 05:18:45 -08:00
2015-06-25 07:43:47 -07:00
if (!opts.outputs) {
2015-06-25 08:53:53 -07:00
opts.outputs = _.pick(opts, ['amount', 'toAddress']);
2015-06-25 07:43:47 -07:00
}
opts.outputs = [].concat(opts.outputs);
2016-05-20 06:12:41 -07:00
if (!checkRequired(opts, ['outputs', 'proposalSignature'], cb)) return;
2015-02-02 10:29:14 -08:00
2015-11-26 05:19:12 -08:00
var type = opts.type || Model.TxProposalLegacy.Types.SIMPLE;
if (!Model.TxProposalLegacy.isTypeSupported(type))
return cb(new ClientError('Invalid proposal type'));
var feePerKb = opts.feePerKb || Defaults.DEFAULT_FEE_PER_KB;
if (feePerKb < Defaults.MIN_FEE_PER_KB || feePerKb > Defaults.MAX_FEE_PER_KB)
2015-06-11 11:26:34 -07:00
return cb(new ClientError('Invalid fee per KB value'));
2015-04-08 11:18:28 -07:00
self._runLocked(cb, function(cb) {
2015-02-08 13:29:58 -08:00
self.getWallet({}, function(err, wallet) {
2015-02-02 12:07:18 -08:00
if (err) return cb(err);
2015-08-05 06:44:09 -07:00
if (!wallet.isComplete()) return cb(Errors.WALLET_NOT_COMPLETE);
2015-02-24 05:36:14 -08:00
2016-06-03 09:34:12 -07:00
if (wallet.singleAddress) return cb(new ClientError('Not compatible with single-address wallets'));
if (opts.payProUrl) {
if (wallet.addressType == Constants.SCRIPT_TYPES.P2PKH && !self._clientSupportsPayProRefund()) {
return cb(new ClientError(Errors.codes.UPGRADE_NEEDED, 'To sign this spend proposal you need to upgrade your client app.'));
}
}
2015-06-25 08:53:53 -07:00
var copayer = wallet.getCopayer(self.copayerId);
var hash;
2015-11-26 05:19:12 -08:00
if (!opts.type || opts.type == Model.TxProposalLegacy.Types.SIMPLE) {
2015-10-30 11:24:47 -07:00
hash = WalletService._getProposalHash(opts.toAddress, opts.amount, opts.message, opts.payProUrl);
2015-06-25 08:53:53 -07:00
} else {
// should match bwc api _computeProposalSignature
2015-06-25 08:53:53 -07:00
var header = {
outputs: _.map(opts.outputs, function(output) {
return _.pick(output, ['toAddress', 'amount', 'message']);
}),
message: opts.message,
payProUrl: opts.payProUrl
};
2015-10-30 11:24:47 -07:00
hash = WalletService._getProposalHash(header)
2015-06-25 08:53:53 -07:00
}
2015-08-05 12:53:06 -07:00
var signingKey = self._getSigningKey(hash, opts.proposalSignature, copayer.requestPubKeys)
if (!signingKey)
2015-06-25 08:53:53 -07:00
return cb(new ClientError('Invalid proposal signature'));
2015-11-26 05:50:22 -08:00
self._canCreateTx(function(err, canCreate) {
2015-06-12 12:05:33 -07:00
if (err) return cb(err);
2015-08-03 12:11:09 -07:00
if (!canCreate) return cb(Errors.TX_CANNOT_CREATE);
2015-11-26 05:19:12 -08:00
if (type != Model.TxProposalLegacy.Types.EXTERNAL) {
2016-05-20 06:12:41 -07:00
var validationError = self._validateOutputs(opts, wallet, cb);
if (validationError) {
return cb(validationError);
}
}
2015-02-16 10:00:41 -08:00
2015-08-05 12:53:06 -07:00
var txOpts = {
2015-06-25 08:53:53 -07:00
type: type,
2015-06-12 12:05:33 -07:00
walletId: self.walletId,
creatorId: self.copayerId,
outputs: opts.outputs,
inputs: opts.inputs,
2015-06-12 12:05:33 -07:00
toAddress: opts.toAddress,
amount: opts.amount,
message: opts.message,
proposalSignature: opts.proposalSignature,
2015-06-25 08:53:53 -07:00
changeAddress: wallet.createAddress(true),
2015-06-11 11:26:34 -07:00
feePerKb: feePerKb,
2015-06-12 12:05:33 -07:00
payProUrl: opts.payProUrl,
requiredSignatures: wallet.m,
requiredRejections: Math.min(wallet.m, wallet.n - wallet.m + 1),
2015-07-27 05:00:37 -07:00
walletN: wallet.n,
excludeUnconfirmedUtxos: !!opts.excludeUnconfirmedUtxos,
2015-09-05 14:49:43 -07:00
addressType: wallet.addressType,
2015-11-11 06:59:34 -08:00
derivationStrategy: wallet.derivationStrategy,
2015-09-07 13:18:32 -07:00
customData: opts.customData,
2015-08-05 12:53:06 -07:00
};
if (signingKey.selfSigned) {
txOpts.proposalSignaturePubKey = signingKey.key;
txOpts.proposalSignaturePubKeySig = signingKey.signature;
}
2015-11-26 05:19:12 -08:00
var txp = Model.TxProposalLegacy.create(txOpts);
2015-02-08 13:29:58 -08:00
if (!self._clientSupportsTXPv2()) {
2015-07-29 13:45:25 -07:00
txp.version = '1.0.1';
}
2016-03-07 08:00:53 -08:00
self._selectTxInputs(txp, opts.utxosToExclude, function(err) {
2015-02-08 13:29:58 -08:00
if (err) return cb(err);
2015-01-28 07:06:34 -08:00
2015-06-12 12:05:33 -07:00
$.checkState(txp.inputs);
2015-06-25 07:43:47 -07:00
self.storage.storeAddressAndWallet(wallet, txp.changeAddress, function(err) {
2015-02-08 13:29:58 -08:00
if (err) return cb(err);
2015-06-12 12:05:33 -07:00
self.storage.storeTx(wallet.id, txp, function(err) {
if (err) return cb(err);
self._notify('NewTxProposal', {
amount: txp.getTotalAmount()
2015-06-12 12:05:33 -07:00
}, function() {
return cb(null, txp);
});
2015-02-11 18:11:30 -08:00
});
2015-02-08 15:46:02 -08:00
});
2015-02-08 13:29:58 -08:00
});
2015-02-02 12:07:18 -08:00
});
});
});
2015-02-02 15:13:13 -08:00
};
2015-01-28 07:06:34 -08:00
WalletService.prototype._validateAndSanitizeTxOpts = function(wallet, opts, cb) {
2015-11-26 05:50:22 -08:00
var self = this;
async.series([
2015-11-26 05:50:22 -08:00
function(next) {
2016-05-20 06:12:41 -07:00
if (!checkRequired(opts, ['outputs'], next)) return;
next();
},
function(next) {
// feePerKb is required unless inputs & fee are specified
if (!_.isNumber(opts.feePerKb) && !(opts.inputs && _.isNumber(opts.fee)))
return next(new ClientError('Required argument missing'));
if (_.isNumber(opts.feePerKb)) {
if (opts.feePerKb < Defaults.MIN_FEE_PER_KB || opts.feePerKb > Defaults.MAX_FEE_PER_KB)
return next(new ClientError('Invalid fee per KB'));
}
next();
},
2016-06-03 09:34:12 -07:00
function(next) {
if (wallet.singleAddress && opts.changeAddress) return next(new ClientError('Cannot specify change address on single-address wallet'));
next();
},
function(next) {
if (!opts.sendMax) return next();
if (!_.isArray(opts.outputs) || opts.outputs.length > 1) {
return next(new ClientError('Only one output allowed when sendMax is specified'));
}
if (_.isNumber(opts.outputs[0].amount))
return next(new ClientError('Amount is not allowed when sendMax is specified'));
if (_.isNumber(opts.fee))
return next(new ClientError('Fee is not allowed when sendMax is specified (use feePerKb instead)'));
self.getSendMaxInfo({
feePerKb: opts.feePerKb || Defaults.DEFAULT_FEE_PER_KB,
excludeUnconfirmedUtxos: !!opts.excludeUnconfirmedUtxos,
returnInputs: true,
}, function(err, info) {
if (err) return next(err);
opts.outputs[0].amount = info.amount;
opts.inputs = info.inputs;
return next();
});
},
2016-06-10 09:50:57 -07:00
function(next) {
var dustThreshold = Math.max(Defaults.MIN_OUTPUT_AMOUNT, Bitcore.Transaction.DUST_AMOUNT);
if (_.any(opts.outputs, function(output) {
return output.amount < dustThreshold;
})) {
return next(Errors.DUST_AMOUNT);
}
next();
},
function(next) {
if (opts.validateOutputs === false) return next();
2016-05-20 06:12:41 -07:00
var validationError = self._validateOutputs(opts, wallet, next);
if (validationError) {
return next(validationError);
}
next();
},
], cb);
};
2015-11-26 05:50:22 -08:00
/**
* Creates a new transaction proposal.
* @param {Object} opts
* @param {Array} opts.outputs - List of outputs.
* @param {string} opts.outputs[].toAddress - Destination address.
* @param {number} opts.outputs[].amount - Amount to transfer in satoshi.
* @param {string} opts.outputs[].message - A message to attach to this output.
* @param {string} opts.message - A message to attach to this transaction.
2016-05-20 08:27:45 -07:00
* @param {number} opts.feePerKb - Use an alternative fee per KB for this TX.
2016-06-03 09:34:12 -07:00
* @param {string} opts.changeAddress - Optional. Use this address as the change address for the tx. The address should belong to the wallet. In the case of singleAddress wallets, the first main address will be used.
2016-05-20 08:27:45 -07:00
* @param {Boolean} opts.sendMax - Optional. Send maximum amount of funds that make sense under the specified fee/feePerKb conditions. (defaults to false).
2015-11-26 05:50:22 -08:00
* @param {string} opts.payProUrl - Optional. Paypro URL for peers to verify TX
2016-05-20 08:27:45 -07:00
* @param {Boolean} opts.excludeUnconfirmedUtxos[=false] - Optional. Do not use UTXOs of unconfirmed transactions as inputs
* @param {Boolean} opts.validateOutputs[=true] - Optional. Perform validation on outputs.
* @param {Boolean} opts.dryRun[=false] - Optional. Simulate the action but do not change server state.
* @param {Array} opts.inputs - Optional. Inputs for this TX
2016-03-11 09:22:54 -08:00
* @param {number} opts.fee - Optional. Use an fixed fee for this TX (only when opts.inputs is specified)
2016-04-19 09:55:05 -07:00
* @param {Boolean} opts.noShuffleOutputs - Optional. If set, TX outputs won't be shuffled. Defaults to false
2015-11-26 05:50:22 -08:00
* @returns {TxProposal} Transaction proposal.
*/
2015-11-26 07:52:51 -08:00
WalletService.prototype.createTx = function(opts, cb) {
2015-11-26 05:50:22 -08:00
var self = this;
2016-06-03 09:34:12 -07:00
function getChangeAddress(wallet, cb) {
if (wallet.singleAddress) {
self.storage.fetchAddresses(self.walletId, function(err, addresses) {
if (err) return cb(err);
if (_.isEmpty(addresses)) return cb(new ClientError('The wallet has no addresses'));
return cb(null, _.first(addresses));
});
} else {
if (opts.changeAddress) {
self.storage.fetchAddress(opts.changeAddress, function(err, address) {
if (err) return cb(Errors.INVALID_CHANGE_ADDRESS);
return cb(null, address);
2016-06-03 09:34:12 -07:00
});
} else {
return cb(null, wallet.createAddress(true));
2016-06-03 09:34:12 -07:00
}
}
};
2015-11-26 05:50:22 -08:00
self._runLocked(cb, function(cb) {
var wallet, txp, changeAddress;
async.series([
2015-11-26 05:50:22 -08:00
function(next) {
self.getWallet({}, function(err, w) {
if (err) return next(err);
if (!w.isComplete()) return next(Errors.WALLET_NOT_COMPLETE);
wallet = w;
next();
});
},
function(next) {
self._validateAndSanitizeTxOpts(wallet, opts, next);
},
function(next) {
self._canCreateTx(function(err, canCreate) {
if (err) return next(err);
if (!canCreate) return next(Errors.TX_CANNOT_CREATE);
next();
});
},
function(next) {
2016-05-20 08:27:45 -07:00
if (opts.sendMax) return next();
2016-06-03 09:34:12 -07:00
getChangeAddress(wallet, function(err, address) {
if (err) return next(err);
changeAddress = address;
next();
});
2016-05-20 08:27:45 -07:00
},
function(next) {
2015-11-26 05:50:22 -08:00
var txOpts = {
walletId: self.walletId,
creatorId: self.copayerId,
outputs: opts.outputs,
2015-11-26 05:50:22 -08:00
message: opts.message,
changeAddress: changeAddress,
feePerKb: opts.feePerKb,
2015-11-26 05:50:22 -08:00
payProUrl: opts.payProUrl,
walletM: wallet.m,
2015-11-26 05:50:22 -08:00
walletN: wallet.n,
excludeUnconfirmedUtxos: !!opts.excludeUnconfirmedUtxos,
validateOutputs: !opts.validateOutputs,
2015-11-26 05:50:22 -08:00
addressType: wallet.addressType,
customData: opts.customData,
inputs: opts.inputs,
fee: opts.inputs && !_.isNumber(opts.feePerKb) ? opts.fee : null,
noShuffleOutputs: opts.noShuffleOutputs
2015-11-26 05:50:22 -08:00
};
txp = Model.TxProposal.create(txOpts);
next();
},
function(next) {
self._selectTxInputs(txp, opts.utxosToExclude, next);
},
function(next) {
2016-02-25 09:47:03 -08:00
if (!changeAddress || opts.dryRun) return next();
self.storage.storeAddressAndWallet(wallet, txp.changeAddress, next);
},
function(next) {
2016-02-25 09:47:03 -08:00
if (opts.dryRun) return next();
self.storage.storeTx(wallet.id, txp, next);
},
], function(err) {
if (err) return cb(err);
return cb(null, txp);
2015-11-26 05:50:22 -08:00
});
});
};
WalletService.prototype._verifyRequestPubKey = function(requestPubKey, signature, xPubKey) {
var pub = (new Bitcore.HDPublicKey(xPubKey)).derive(Constants.PATHS.REQUEST_KEY_AUTH).publicKey;
return Utils.verifyMessage(requestPubKey, signature, pub.toString());
};
2015-11-26 06:28:07 -08:00
/**
2015-12-08 07:01:49 -08:00
* Publish an already created tx proposal so inputs are locked and other copayers in the wallet can see it.
2015-11-26 06:28:07 -08:00
* @param {Object} opts
* @param {string} opts.txProposalId - The tx id.
* @param {string} opts.proposalSignature - S(raw tx). Used by other copayers to verify the proposal.
*/
2015-12-08 07:01:49 -08:00
WalletService.prototype.publishTx = function(opts, cb) {
2015-11-26 06:28:07 -08:00
var self = this;
function utxoKey(utxo) {
return utxo.txid + '|' + utxo.vout
};
2016-05-20 06:12:41 -07:00
if (!checkRequired(opts, ['txProposalId', 'proposalSignature'], cb)) return;
2015-11-26 06:28:07 -08:00
self._runLocked(cb, function(cb) {
self.getWallet({}, function(err, wallet) {
2015-11-26 06:28:07 -08:00
if (err) return cb(err);
2015-11-26 09:05:52 -08:00
self.storage.fetchTx(self.walletId, opts.txProposalId, function(err, txp) {
2015-11-26 06:28:07 -08:00
if (err) return cb(err);
if (!txp) return cb(Errors.TX_NOT_FOUND);
if (!txp.isTemporary()) return cb();
var copayer = wallet.getCopayer(self.copayerId);
var raw = txp.getRawTx();
var signingKey = self._getSigningKey(raw, opts.proposalSignature, copayer.requestPubKeys);
if (!signingKey) {
return cb(new ClientError('Invalid proposal signature'));
}
// Save signature info for other copayers to check
txp.proposalSignature = opts.proposalSignature;
if (signingKey.selfSigned) {
txp.proposalSignaturePubKey = signingKey.key;
txp.proposalSignaturePubKeySig = signingKey.signature;
}
// Verify UTXOs are still available
self.getUtxos({}, function(err, utxos) {
if (err) return cb(err);
var txpInputs = _.map(txp.inputs, utxoKey);
var lockedUtxoIndex = _.indexBy(_.filter(utxos, 'locked'), utxoKey);
var unavailable = _.any(txpInputs, function(i) {
return lockedUtxoIndex[i];
});
if (unavailable) return cb(Errors.UNAVAILABLE_UTXOS);
txp.status = 'pending';
self.storage.storeTx(self.walletId, txp, function(err) {
if (err) return cb(err);
self._notify('NewTxProposal', {
amount: txp.getTotalAmount()
}, function() {
return cb(null, txp);
});
});
});
2015-11-26 06:28:07 -08:00
});
});
});
};
2015-11-26 05:50:22 -08:00
2015-02-04 10:45:08 -08:00
/**
* Retrieves a tx from storage.
* @param {Object} opts
2015-02-26 05:41:55 -08:00
* @param {string} opts.txProposalId - The tx id.
2015-02-04 10:45:08 -08:00
* @returns {Object} txProposal
*/
2015-02-20 12:32:19 -08:00
WalletService.prototype.getTx = function(opts, cb) {
2015-02-04 10:45:08 -08:00
var self = this;
2015-02-26 05:41:55 -08:00
self.storage.fetchTx(self.walletId, opts.txProposalId, function(err, txp) {
2015-02-04 10:45:08 -08:00
if (err) return cb(err);
2015-08-05 06:48:36 -07:00
if (!txp) return cb(Errors.TX_NOT_FOUND);
2016-05-17 09:13:21 -07:00
2016-05-17 11:32:06 -07:00
if (!txp.txid) return cb(null, txp);
2016-05-17 09:13:21 -07:00
self.storage.fetchTxNote(self.walletId, txp.txid, function(err, note) {
if (err) {
log.warn('Error fetching tx note for ' + txp.txid);
}
txp.note = note;
return cb(null, txp);
});
2015-02-04 10:45:08 -08:00
});
};
2016-05-17 09:13:21 -07:00
/**
* Edit note associated to a txid.
* @param {Object} opts
* @param {string} opts.txid - The txid of the tx on the blockchain.
* @param {string} opts.body - The contents of the note.
*/
WalletService.prototype.editTxNote = function(opts, cb) {
var self = this;
2016-05-23 05:59:13 -07:00
if (!checkRequired(opts, 'txid', cb)) return;
2016-05-17 09:13:21 -07:00
self._runLocked(cb, function(cb) {
self.storage.fetchTxNote(self.walletId, opts.txid, function(err, note) {
if (err) return cb(err);
if (!note) {
note = Model.TxNote.create({
walletId: self.walletId,
txid: opts.txid,
copayerId: self.copayerId,
body: opts.body,
});
} else {
note.edit(opts.body, self.copayerId);
}
self.storage.storeTxNote(note, cb);
});
});
};
/**
* Get tx notes.
* @param {Object} opts
* @param {string} opts.txid - The txid associated with the note.
*/
WalletService.prototype.getTxNote = function(opts, cb) {
var self = this;
2016-05-23 05:59:13 -07:00
if (!checkRequired(opts, 'txid', cb)) return;
2016-05-17 09:13:21 -07:00
self.storage.fetchTxNote(self.walletId, opts.txid, cb);
};
2015-02-09 13:07:15 -08:00
2016-05-23 05:55:14 -07:00
/**
* Get tx notes.
* @param {Object} opts
* @param {string} opts.minTs[=0] - The start date used to filter notes.
*/
WalletService.prototype.getTxNotes = function(opts, cb) {
var self = this;
opts = opts || {};
self.storage.fetchTxNotes(self.walletId, opts, cb);
};
2015-02-09 13:07:15 -08:00
/**
2015-02-10 11:11:44 -08:00
* removeWallet
*
* @param opts
* @param cb
* @return {undefined}
2015-02-09 13:07:15 -08:00
*/
2015-02-20 12:32:19 -08:00
WalletService.prototype.removeWallet = function(opts, cb) {
2015-02-10 11:11:44 -08:00
var self = this;
2015-04-08 11:18:28 -07:00
self._runLocked(cb, function(cb) {
2015-02-10 11:11:44 -08:00
self.storage.removeWallet(self.walletId, cb);
});
2015-02-09 13:07:15 -08:00
};
2015-06-11 14:38:42 -07:00
WalletService.prototype.getRemainingDeleteLockTime = function(txp) {
var now = Math.floor(Date.now() / 1000);
var lockTimeRemaining = txp.createdOn + Defaults.DELETE_LOCKTIME - now;
2015-06-11 14:38:42 -07:00
if (lockTimeRemaining < 0)
return 0;
// not the creator? need to wait
if (txp.creatorId !== this.copayerId)
return lockTimeRemaining;
// has other approvers? need to wait
var approvers = txp.getApprovers();
if (approvers.length > 1 || (approvers.length == 1 && approvers[0] !== this.copayerId))
return lockTimeRemaining;
2015-06-12 06:06:15 -07:00
2015-06-11 14:38:42 -07:00
return 0;
};
2015-02-10 11:30:58 -08:00
/**
* removePendingTx
*
* @param opts
2015-02-15 08:03:48 -08:00
* @param {string} opts.txProposalId - The tx id.
2015-02-10 11:30:58 -08:00
* @return {undefined}
*/
2015-02-20 12:32:19 -08:00
WalletService.prototype.removePendingTx = function(opts, cb) {
2015-02-10 11:30:58 -08:00
var self = this;
2016-05-20 06:12:41 -07:00
if (!checkRequired(opts, ['txProposalId'], cb)) return;
2015-02-10 11:30:58 -08:00
2015-04-08 11:18:28 -07:00
self._runLocked(cb, function(cb) {
2015-02-10 11:30:58 -08:00
2015-02-11 07:05:21 -08:00
self.getTx({
2015-02-26 05:41:55 -08:00
txProposalId: opts.txProposalId,
2015-02-11 07:05:21 -08:00
}, function(err, txp) {
2015-02-10 11:30:58 -08:00
if (err) return cb(err);
2015-08-03 12:11:09 -07:00
if (!txp.isPending()) return cb(Errors.TX_NOT_PENDING);
2015-02-10 13:04:50 -08:00
2015-06-11 14:38:42 -07:00
var deleteLockTime = self.getRemainingDeleteLockTime(txp);
2015-08-03 12:11:09 -07:00
if (deleteLockTime > 0) return cb(Errors.TX_CANNOT_REMOVE);
2015-07-31 08:16:18 -07:00
2015-06-12 06:06:15 -07:00
self.storage.removeTx(self.walletId, txp.id, function() {
self._notify('TxProposalRemoved', {}, cb);
2015-04-30 16:31:45 -07:00
});
2015-02-10 11:30:58 -08:00
});
});
};
2015-08-13 12:06:22 -07:00
WalletService.prototype._broadcastRawTx = function(network, raw, cb) {
var bc = this._getBlockchainExplorer(network);
2015-02-05 12:22:38 -08:00
bc.broadcast(raw, function(err, txid) {
2015-08-02 15:48:18 -07:00
if (err) return cb(err);
2015-05-15 07:25:54 -07:00
return cb(null, txid);
});
2015-01-28 08:28:18 -08:00
};
2015-08-13 12:06:22 -07:00
/**
* Broadcast a raw transaction.
* @param {Object} opts
* @param {string} [opts.network = 'livenet'] - The Bitcoin network for this transaction.
* @param {string} opts.rawTx - Raw tx data.
*/
WalletService.prototype.broadcastRawTx = function(opts, cb) {
var self = this;
2016-05-20 06:12:41 -07:00
if (!checkRequired(opts, ['network', 'rawTx'], cb)) return;
2015-08-13 12:06:22 -07:00
var network = opts.network || 'livenet';
if (network != 'livenet' && network != 'testnet')
return cb(new ClientError('Invalid network'));
self._broadcastRawTx(network, opts.rawTx, cb);
};
2015-05-28 08:51:41 -07:00
WalletService.prototype._checkTxInBlockchain = function(txp, cb) {
if (!txp.txid) return cb();
2015-07-15 18:42:05 -07:00
var bc = this._getBlockchainExplorer(txp.getNetworkName());
bc.getTransaction(txp.txid, function(err, tx) {
2015-08-02 15:48:18 -07:00
if (err) return cb(err);
return cb(null, !!tx);
2015-05-28 08:51:41 -07:00
})
};
2015-01-28 07:06:34 -08:00
/**
* Sign a transaction proposal.
* @param {Object} opts
2015-01-28 08:28:18 -08:00
* @param {string} opts.txProposalId - The identifier of the transaction.
2015-02-04 11:18:36 -08:00
* @param {string} opts.signatures - The signatures of the inputs of this tx for this copayer (in apperance order)
2015-01-28 07:06:34 -08:00
*/
2015-02-20 12:32:19 -08:00
WalletService.prototype.signTx = function(opts, cb) {
2015-02-02 12:07:18 -08:00
var self = this;
2015-01-28 07:06:34 -08:00
2016-05-20 06:12:41 -07:00
if (!checkRequired(opts, ['txProposalId', 'signatures'], cb)) return;
2015-02-02 10:29:14 -08:00
2015-02-06 12:56:51 -08:00
self.getWallet({}, function(err, wallet) {
2015-02-02 12:07:18 -08:00
if (err) return cb(err);
2015-01-27 05:18:45 -08:00
2015-02-05 10:50:18 -08:00
self.getTx({
2015-02-26 05:41:55 -08:00
txProposalId: opts.txProposalId
2015-02-05 10:50:18 -08:00
}, function(err, txp) {
2015-02-02 12:07:18 -08:00
if (err) return cb(err);
2015-02-10 11:30:58 -08:00
if (!self._clientSupportsTXPv2()) {
if (!_.startsWith(txp.version, '1.')) {
2015-08-11 12:25:29 -07:00
return cb(new ClientError(Errors.codes.UPGRADE_NEEDED, 'To sign this spend proposal you need to upgrade your client app.'));
}
}
2015-02-05 10:50:18 -08:00
var action = _.find(txp.actions, {
2015-02-10 11:30:58 -08:00
copayerId: self.copayerId
2015-02-05 10:50:18 -08:00
});
2015-08-03 12:11:09 -07:00
if (action) return cb(Errors.COPAYER_VOTED);
if (!txp.isPending()) return cb(Errors.TX_NOT_PENDING);
2015-01-28 11:40:07 -08:00
2015-02-06 12:56:51 -08:00
var copayer = wallet.getCopayer(self.copayerId);
2015-01-27 05:18:45 -08:00
2016-04-27 08:45:00 -07:00
if (!txp.sign(self.copayerId, opts.signatures, copayer.xPubKey)) {
log.warn('Error signing transaction (BAD_SIGNATURES)');
2016-04-27 08:57:05 -07:00
log.warn('Wallet id:', self.walletId);
log.warn('Copayer id:', self.copayerId);
log.warn('Client version:', self.clientVersion);
log.warn('Arguments:', JSON.stringify(opts));
log.warn('Transaction proposal:', JSON.stringify(txp));
var raw = txp.getBitcoreTx().uncheckedSerialize();
log.warn('Raw tx:', raw);
2015-08-03 12:11:09 -07:00
return cb(Errors.BAD_SIGNATURES);
2016-04-27 08:45:00 -07:00
}
2015-02-05 10:50:18 -08:00
2015-02-06 12:56:51 -08:00
self.storage.storeTx(self.walletId, txp, function(err) {
2015-02-05 10:50:18 -08:00
if (err) return cb(err);
2015-05-14 08:48:19 -07:00
async.series([
2015-04-30 16:31:45 -07:00
2015-05-14 08:48:19 -07:00
function(next) {
2015-04-30 16:31:45 -07:00
self._notify('TxProposalAcceptedBy', {
txProposalId: opts.txProposalId,
copayerId: self.copayerId,
2015-05-14 08:48:19 -07:00
}, next);
2015-04-30 16:31:45 -07:00
},
2015-05-14 08:48:19 -07:00
function(next) {
2015-04-30 16:31:45 -07:00
if (txp.isAccepted()) {
self._notify('TxProposalFinallyAccepted', {
txProposalId: opts.txProposalId,
2015-05-14 08:48:19 -07:00
}, next);
2015-04-30 16:31:45 -07:00
} else {
2015-05-14 08:48:19 -07:00
next();
2015-04-30 16:31:45 -07:00
}
},
], function() {
return cb(null, txp);
2015-02-11 18:11:30 -08:00
});
2015-02-05 10:50:18 -08:00
});
2015-02-02 12:07:18 -08:00
});
});
2015-02-02 06:55:03 -08:00
};
2015-01-27 05:18:45 -08:00
WalletService.prototype._processBroadcast = function(txp, opts, cb) {
var self = this;
$.checkState(txp.txid);
opts = opts || {};
txp.setBroadcasted();
self.storage.storeTx(self.walletId, txp, function(err) {
if (err) return cb(err);
var args = {
txProposalId: txp.id,
txid: txp.txid,
amount: txp.getTotalAmount(),
};
if (opts.byThirdParty) {
self._notify('NewOutgoingTxByThirdParty', args);
} else {
self._notify('NewOutgoingTx', args);
}
return cb(err, txp);
});
};
2015-02-15 13:52:48 -08:00
/**
* Broadcast a transaction proposal.
* @param {Object} opts
* @param {string} opts.txProposalId - The identifier of the transaction.
*/
2015-02-20 12:32:19 -08:00
WalletService.prototype.broadcastTx = function(opts, cb) {
2015-02-15 13:52:48 -08:00
var self = this;
2016-05-20 06:12:41 -07:00
if (!checkRequired(opts, ['txProposalId'], cb)) return;
2015-02-15 13:52:48 -08:00
self.getWallet({}, function(err, wallet) {
if (err) return cb(err);
self.getTx({
2015-02-26 05:41:55 -08:00
txProposalId: opts.txProposalId
2015-02-15 13:52:48 -08:00
}, function(err, txp) {
if (err) return cb(err);
2015-08-03 12:11:09 -07:00
if (txp.status == 'broadcasted') return cb(Errors.TX_ALREADY_BROADCASTED);
if (txp.status != 'accepted') return cb(Errors.TX_NOT_ACCEPTED);
2015-02-15 13:52:48 -08:00
2015-08-13 12:06:22 -07:00
var raw;
try {
raw = txp.getRawTx();
} catch (ex) {
return cb(ex);
}
self._broadcastRawTx(txp.getNetworkName(), raw, function(err, txid) {
2015-05-28 08:51:41 -07:00
if (err) {
var broadcastErr = err;
// Check if tx already in blockchain
self._checkTxInBlockchain(txp, function(err, isInBlockchain) {
2015-05-28 08:51:41 -07:00
if (err) return cb(err);
if (!isInBlockchain) return cb(broadcastErr);
2015-02-15 13:52:48 -08:00
self._processBroadcast(txp, {
byThirdParty: true
}, cb);
2015-02-15 13:52:48 -08:00
});
2015-05-28 08:51:41 -07:00
} else {
self._processBroadcast(txp, {
byThirdParty: false
}, function(err) {
if (err) return cb(err);
return cb(null, txp);
});
2015-05-28 08:51:41 -07:00
}
2015-02-15 13:52:48 -08:00
});
});
});
};
2015-01-29 09:57:26 -08:00
/**
* Reject a transaction proposal.
* @param {Object} opts
* @param {string} opts.txProposalId - The identifier of the transaction.
2015-02-02 10:29:14 -08:00
* @param {string} [opts.reason] - A message to other copayers explaining the rejection.
2015-01-29 09:57:26 -08:00
*/
2015-02-20 12:32:19 -08:00
WalletService.prototype.rejectTx = function(opts, cb) {
2015-02-02 12:07:18 -08:00
var self = this;
2015-01-29 09:57:26 -08:00
2016-05-20 06:12:41 -07:00
if (!checkRequired(opts, ['txProposalId'], cb)) return;
2015-02-02 10:29:14 -08:00
2015-02-04 11:27:36 -08:00
self.getTx({
2015-02-26 05:41:55 -08:00
txProposalId: opts.txProposalId
2015-02-04 11:27:36 -08:00
}, function(err, txp) {
2015-02-02 12:07:18 -08:00
if (err) return cb(err);
2015-02-11 06:27:52 -08:00
2015-02-02 15:13:13 -08:00
var action = _.find(txp.actions, {
2015-02-06 12:56:51 -08:00
copayerId: self.copayerId
2015-02-02 15:13:13 -08:00
});
2015-02-11 18:11:30 -08:00
2015-08-03 12:11:09 -07:00
if (action) return cb(Errors.COPAYER_VOTED);
if (txp.status != 'pending') return cb(Errors.TX_NOT_PENDING);
2015-01-29 09:57:26 -08:00
2015-02-15 10:46:29 -08:00
txp.reject(self.copayerId, opts.reason);
2015-01-29 09:57:26 -08:00
2015-02-06 12:56:51 -08:00
self.storage.storeTx(self.walletId, txp, function(err) {
2015-02-02 12:07:18 -08:00
if (err) return cb(err);
2015-01-29 09:57:26 -08:00
2015-05-14 08:48:19 -07:00
async.series([
2015-02-11 18:11:30 -08:00
2015-05-14 08:48:19 -07:00
function(next) {
2015-04-30 16:31:45 -07:00
self._notify('TxProposalRejectedBy', {
txProposalId: opts.txProposalId,
copayerId: self.copayerId,
2015-05-14 08:48:19 -07:00
}, next);
2015-04-30 16:31:45 -07:00
},
2015-05-14 08:48:19 -07:00
function(next) {
2015-04-30 16:31:45 -07:00
if (txp.status == 'rejected') {
2015-06-08 14:26:33 -07:00
var rejectedBy = _.pluck(_.filter(txp.actions, {
type: 'reject'
}), 'copayerId');
2015-04-30 16:31:45 -07:00
self._notify('TxProposalFinallyRejected', {
txProposalId: opts.txProposalId,
2015-06-08 14:26:33 -07:00
rejectedBy: rejectedBy,
2015-05-14 08:48:19 -07:00
}, next);
2015-04-30 16:31:45 -07:00
} else {
2015-05-14 08:48:19 -07:00
next();
2015-04-30 16:31:45 -07:00
}
},
], function() {
return cb(null, txp);
});
2015-02-02 12:07:18 -08:00
});
});
2015-01-29 09:57:26 -08:00
};
2015-01-28 07:06:34 -08:00
/**
2015-02-21 17:35:12 -08:00
* Retrieves pending transaction proposals.
2015-01-28 07:06:34 -08:00
* @param {Object} opts
2015-02-02 06:55:03 -08:00
* @returns {TxProposal[]} Transaction proposal.
2015-01-28 07:06:34 -08:00
*/
2015-02-20 12:32:19 -08:00
WalletService.prototype.getPendingTxs = function(opts, cb) {
2015-02-02 12:07:18 -08:00
var self = this;
2015-01-27 05:18:45 -08:00
2015-02-06 12:51:21 -08:00
self.storage.fetchPendingTxs(self.walletId, function(err, txps) {
2015-02-02 12:07:18 -08:00
if (err) return cb(err);
2015-01-28 12:40:37 -08:00
var v3Txps = _.any(txps, function(txp) {
return txp.version >= 3;
});
if (v3Txps && !self._clientSupportsTXPv3()) {
return cb(new ClientError(Errors.codes.UPGRADE_NEEDED, 'To view some of the pending proposals you need to upgrade your client app.'));
}
2015-06-12 06:06:15 -07:00
_.each(txps, function(txp) {
2015-06-11 14:38:42 -07:00
txp.deleteLockTime = self.getRemainingDeleteLockTime(txp);
});
async.each(txps, function(txp, next) {
if (txp.status != 'accepted') return next();
self._checkTxInBlockchain(txp, function(err, isInBlockchain) {
if (err || !isInBlockchain) return next(err);
self._processBroadcast(txp, {
byThirdParty: true
}, next);
});
}, function(err) {
return cb(err, _.reject(txps, function(txp) {
return txp.status == 'broadcasted';
}));
});
2015-02-02 12:07:18 -08:00
});
2015-01-27 05:18:45 -08:00
};
2015-02-06 23:09:45 -08:00
/**
2015-02-21 17:35:12 -08:00
* Retrieves all transaction proposals in the range (maxTs-minTs)
2015-02-12 05:26:13 -08:00
* Times are in UNIX EPOCH
*
2015-02-06 23:09:45 -08:00
* @param {Object} opts.minTs (defaults to 0)
* @param {Object} opts.maxTs (defaults to now)
* @param {Object} opts.limit
2015-04-20 15:45:45 -07:00
* @returns {TxProposal[]} Transaction proposals, newer first
2015-02-06 23:09:45 -08:00
*/
2015-02-20 12:32:19 -08:00
WalletService.prototype.getTxs = function(opts, cb) {
2015-02-06 23:09:45 -08:00
var self = this;
self.storage.fetchTxs(self.walletId, opts, function(err, txps) {
if (err) return cb(err);
return cb(null, txps);
});
};
2015-10-15 11:14:29 -07:00
/**
* Retrieves notifications after a specific id or from a given ts (whichever is more recent).
*
* @param {Object} opts
* @param {Object} opts.notificationId (optional)
* @param {Object} opts.minTs (optional) - default 0.
2015-10-15 11:14:29 -07:00
* @returns {Notification[]} Notifications
*/
WalletService.prototype.getNotifications = function(opts, cb) {
2015-10-15 11:14:29 -07:00
var self = this;
opts = opts || {};
self.getWallet({}, function(err, wallet) {
2015-10-15 11:14:29 -07:00
if (err) return cb(err);
async.map([wallet.network, self.walletId], function(walletId, next) {
self.storage.fetchNotifications(walletId, opts.notificationId, opts.minTs || 0, next);
}, function(err, res) {
if (err) return cb(err);
var notifications = _.sortBy(_.map(_.flatten(res), function(n) {
n.walletId = self.walletId;
return n;
}), 'id');
return cb(null, notifications);
});
2015-10-15 11:14:29 -07:00
});
};
2015-02-11 18:13:19 -08:00
2015-02-21 17:35:12 -08:00
WalletService.prototype._normalizeTxHistory = function(txs) {
2015-07-20 12:10:08 -07:00
var now = Math.floor(Date.now() / 1000);
2015-02-21 17:35:12 -08:00
return _.map(txs, function(tx) {
var inputs = _.map(tx.vin, function(item) {
return {
address: item.addr,
amount: item.valueSat,
}
});
var outputs = _.map(tx.vout, function(item) {
var itemAddr;
// If classic multisig, ignore
2015-07-02 08:09:43 -07:00
if (item.scriptPubKey && _.isArray(item.scriptPubKey.addresses) && item.scriptPubKey.addresses.length == 1) {
2015-02-21 17:35:12 -08:00
itemAddr = item.scriptPubKey.addresses[0];
}
return {
address: itemAddr,
amount: parseInt((item.value * 1e8).toFixed(0)),
}
});
2015-10-28 14:34:05 -07:00
var t = tx.blocktime; // blocktime
2015-10-28 13:46:36 -07:00
if (!t || _.isNaN(t)) t = tx.firstSeenTs;
if (!t || _.isNaN(t)) t = now;
2015-02-21 17:35:12 -08:00
return {
txid: tx.txid,
confirmations: tx.confirmations,
fees: parseInt((tx.fees * 1e8).toFixed(0)),
2015-10-28 13:46:36 -07:00
time: t,
2015-02-21 17:35:12 -08:00
inputs: inputs,
outputs: outputs,
};
});
};
/**
* Retrieves all transactions (incoming & outgoing)
2015-02-21 17:35:12 -08:00
* Times are in UNIX EPOCH
*
2015-03-17 15:46:01 -07:00
* @param {Object} opts
* @param {Number} opts.skip (defaults to 0)
2015-03-17 15:46:01 -07:00
* @param {Number} opts.limit
* @param {Number} opts.includeExtendedInfo[=false] - Include all inputs/outputs for every tx.
2015-02-21 17:35:12 -08:00
* @returns {TxProposal[]} Transaction proposals, first newer
*/
WalletService.prototype.getTxHistory = function(opts, cb) {
var self = this;
2015-12-04 06:13:34 -08:00
opts = opts || {};
2016-02-17 11:45:56 -08:00
opts.limit = (_.isUndefined(opts.limit) ? Defaults.HISTORY_LIMIT : opts.limit);
if (opts.limit > Defaults.HISTORY_LIMIT)
2015-12-14 11:41:06 -08:00
return cb(Errors.HISTORY_LIMIT_EXCEEDED);
2015-12-04 06:13:34 -08:00
2016-05-17 11:32:06 -07:00
function decorate(txs, addresses, proposals, notes) {
var indexedAddresses = _.indexBy(addresses, 'address');
var indexedProposals = _.indexBy(proposals, 'txid');
2016-05-17 11:32:06 -07:00
var indexedNotes = _.indexBy(notes, 'txid');
2015-02-21 17:35:12 -08:00
function sum(items, isMine, isChange) {
var filter = {};
if (_.isBoolean(isMine)) filter.isMine = isMine;
if (_.isBoolean(isChange)) filter.isChange = isChange;
2015-07-20 09:44:39 -07:00
return _.sum(_.filter(items, filter), 'amount');
2015-02-21 17:35:12 -08:00
};
function classify(items) {
return _.map(items, function(item) {
2015-02-21 17:35:12 -08:00
var address = indexedAddresses[item.address];
return {
address: item.address,
amount: item.amount,
isMine: !!address,
isChange: address ? address.isChange : false,
}
2015-02-21 17:35:12 -08:00
});
};
return _.map(txs, function(tx) {
var amountIn, amountOut, amountOutChange;
var amount, action, addressTo;
var inputs, outputs;
2015-07-15 05:57:45 -07:00
if (tx.outputs.length || tx.inputs.length) {
inputs = classify(tx.inputs);
outputs = classify(tx.outputs);
2015-07-15 05:57:45 -07:00
amountIn = sum(inputs, true);
amountOut = sum(outputs, true, false);
amountOutChange = sum(outputs, true, true);
if (amountIn == (amountOut + amountOutChange + (amountIn > 0 ? tx.fees : 0))) {
amount = amountOut;
action = 'moved';
} else {
amount = amountIn - amountOut - amountOutChange - (amountIn > 0 ? tx.fees : 0);
action = amount > 0 ? 'sent' : 'received';
}
amount = Math.abs(amount);
2016-03-01 09:54:02 -08:00
if (action == 'sent' || action == 'moved') {
2015-07-15 05:57:45 -07:00
var firstExternalOutput = _.find(outputs, {
isMine: false
});
addressTo = firstExternalOutput ? firstExternalOutput.address : 'N/A';
};
2015-02-21 17:35:12 -08:00
} else {
2015-07-15 05:57:45 -07:00
action = 'invalid';
amount = 0;
2015-02-21 17:35:12 -08:00
}
function formatOutput(o) {
2015-07-29 13:45:25 -07:00
return {
amount: o.amount,
address: o.address
}
};
var newTx = {
txid: tx.txid,
action: action,
amount: amount,
fees: tx.fees,
2015-07-20 12:10:08 -07:00
time: tx.time,
addressTo: addressTo,
confirmations: tx.confirmations,
};
2015-02-21 17:35:12 -08:00
if (opts.includeExtendedInfo) {
newTx.inputs = _.map(inputs, function(input) {
return _.pick(input, 'address', 'amount', 'isMine');
});
newTx.outputs = _.map(outputs, function(output) {
return _.pick(output, 'address', 'amount', 'isMine');
});
} else {
outputs = _.filter(outputs, {
isChange: false
});
if (action == 'received') {
outputs = _.filter(outputs, {
isMine: true
});
}
newTx.outputs = _.map(outputs, formatOutput);
}
2015-02-21 17:35:12 -08:00
var proposal = indexedProposals[tx.txid];
if (proposal) {
newTx.proposalId = proposal.id;
newTx.proposalType = proposal.type;
newTx.creatorName = proposal.creatorName;
newTx.message = proposal.message;
newTx.actions = _.map(proposal.actions, function(action) {
2015-02-22 18:26:21 -08:00
return _.pick(action, ['createdOn', 'type', 'copayerId', 'copayerName', 'comment']);
});
_.each(newTx.outputs, function(output) {
2015-07-29 13:45:25 -07:00
var query = {
toAddress: output.address,
amount: output.amount
};
var txpOut = _.find(proposal.outputs, query);
output.message = txpOut ? txpOut.message : null;
});
2016-01-13 08:11:47 -08:00
newTx.customData = proposal.customData;
// newTx.sentTs = proposal.sentTs;
// newTx.merchant = proposal.merchant;
//newTx.paymentAckMemo = proposal.paymentAckMemo;
2015-02-21 17:35:12 -08:00
}
2016-05-17 11:32:06 -07:00
var note = indexedNotes[tx.txid];
if (note) {
newTx.note = _.pick(note, ['body', 'editedBy', 'editedByName', 'editedOn']);
}
return newTx;
2015-02-21 17:35:12 -08:00
});
};
// Get addresses for this wallet
self.storage.fetchAddresses(self.walletId, function(err, addresses) {
if (err) return cb(err);
if (addresses.length == 0) return cb(null, []);
var addressStrs = _.pluck(addresses, 'address');
var networkName = Bitcore.Address(addressStrs[0]).toObject().network;
2015-07-15 18:42:05 -07:00
var bc = self._getBlockchainExplorer(networkName);
2015-02-21 17:35:12 -08:00
async.parallel([
function(next) {
2016-05-17 11:32:06 -07:00
self.storage.fetchTxs(self.walletId, {}, next);
2015-02-21 17:35:12 -08:00
},
function(next) {
2015-07-13 13:32:12 -07:00
var from = opts.skip || 0;
2015-12-04 06:13:34 -08:00
var to = from + opts.limit;
2015-07-13 13:32:12 -07:00
bc.getTransactions(addressStrs, from, to, function(err, txs) {
2015-08-02 15:48:18 -07:00
if (err) return cb(err);
2015-02-21 17:35:12 -08:00
next(null, self._normalizeTxHistory(txs));
});
},
2016-05-17 11:32:06 -07:00
function(next) {
2016-05-23 05:55:14 -07:00
self.storage.fetchTxNotes(self.walletId, {}, next);
2016-05-17 11:32:06 -07:00
},
2015-02-21 17:35:12 -08:00
], function(err, res) {
if (err) return cb(err);
var proposals = res[0];
var txs = res[1];
2016-05-17 11:32:06 -07:00
var notes = res[2];
2015-02-21 17:35:12 -08:00
2016-05-17 11:32:06 -07:00
txs = decorate(txs, addresses, proposals, notes);
2015-02-21 17:35:12 -08:00
return cb(null, txs);
});
});
};
2015-02-11 18:13:19 -08:00
2015-04-01 12:42:12 -07:00
/**
* Scan the blockchain looking for addresses having some activity
*
* @param {Object} opts
* @param {Boolean} opts.includeCopayerBranches (defaults to false)
*/
WalletService.prototype.scan = function(opts, cb) {
2015-04-01 12:42:12 -07:00
var self = this;
opts = opts || {};
function checkActivity(address, network, cb) {
var bc = self._getBlockchainExplorer(network);
bc.getAddressActivity(address, cb);
2015-04-01 12:42:12 -07:00
};
2015-04-01 13:48:54 -07:00
function scanBranch(derivator, cb) {
var inactiveCounter = 0;
var allAddresses = [];
var gap = Defaults.SCAN_ADDRESS_GAP;
2015-04-01 12:42:12 -07:00
async.whilst(function() {
return inactiveCounter < gap;
2015-04-01 12:42:12 -07:00
}, function(next) {
var address = derivator.derive();
checkActivity(address.address, address.network, function(err, activity) {
2015-04-01 12:42:12 -07:00
if (err) return next(err);
2015-05-15 07:25:54 -07:00
allAddresses.push(address);
inactiveCounter = activity ? 0 : inactiveCounter + 1;
next();
2015-04-01 12:42:12 -07:00
});
}, function(err) {
derivator.rewind(gap);
return cb(err, _.dropRight(allAddresses, gap));
});
2015-04-01 12:42:12 -07:00
};
2015-04-08 11:18:28 -07:00
self._runLocked(cb, function(cb) {
2015-04-01 12:42:12 -07:00
self.getWallet({}, function(err, wallet) {
if (err) return cb(err);
2015-08-05 06:44:09 -07:00
if (!wallet.isComplete()) return cb(Errors.WALLET_NOT_COMPLETE);
2015-04-01 12:42:12 -07:00
2015-04-15 06:57:10 -07:00
wallet.scanStatus = 'running';
self.storage.storeWallet(wallet, function(err) {
if (err) return cb(err);
2015-04-01 12:42:12 -07:00
2015-04-15 06:57:10 -07:00
var derivators = [];
_.each([false, true], function(isChange) {
2015-04-17 14:25:41 -07:00
derivators.push({
derive: _.bind(wallet.createAddress, wallet, isChange),
rewind: _.bind(wallet.addressManager.rewindIndex, wallet.addressManager, isChange),
});
2015-04-15 06:57:10 -07:00
if (opts.includeCopayerBranches) {
_.each(wallet.copayers, function(copayer) {
if (copayer.addressManager) {
2015-08-27 13:14:33 -07:00
derivators.push({
derive: _.bind(copayer.createAddress, copayer, wallet, isChange),
rewind: _.bind(copayer.addressManager.rewindIndex, copayer.addressManager, isChange),
});
}
2015-04-15 06:57:10 -07:00
});
}
});
async.eachSeries(derivators, function(derivator, next) {
scanBranch(derivator, function(err, addresses) {
if (err) return next(err);
self.storage.storeAddressAndWallet(wallet, addresses, next);
});
}, function(error) {
self.storage.fetchWallet(wallet.id, function(err, wallet) {
if (err) return cb(err);
wallet.scanStatus = error ? 'error' : 'success';
self.storage.storeWallet(wallet, function() {
return cb(error);
});
})
2015-04-01 12:42:12 -07:00
});
2015-04-15 06:57:10 -07:00
});
2015-04-01 12:42:12 -07:00
});
});
2015-04-02 07:18:39 -07:00
};
/**
* Start a scan process.
*
* @param {Object} opts
* @param {Boolean} opts.includeCopayerBranches (defaults to false)
*/
WalletService.prototype.startScan = function(opts, cb) {
var self = this;
function scanFinished(err) {
2015-04-14 11:41:27 -07:00
var data = {
2015-04-15 06:57:10 -07:00
result: err ? 'error' : 'success',
2015-04-14 11:41:27 -07:00
};
if (err) data.error = err;
2015-05-07 10:16:24 -07:00
self._notify('ScanFinished', data, {
isGlobal: true
});
2015-04-02 07:18:39 -07:00
};
2015-04-03 14:49:08 -07:00
self.getWallet({}, function(err, wallet) {
if (err) return cb(err);
2015-08-05 06:44:09 -07:00
if (!wallet.isComplete()) return cb(Errors.WALLET_NOT_COMPLETE);
2015-04-02 07:18:39 -07:00
2015-04-15 06:57:10 -07:00
setTimeout(function() {
self.scan(opts, scanFinished);
}, 100);
2015-04-15 06:57:10 -07:00
return cb(null, {
started: true
});
2015-04-02 07:18:39 -07:00
});
};
2016-01-11 12:10:24 -08:00
/**
* Returns exchange rate for the specified currency & timestamp.
* @param {Object} opts
* @param {string} opts.code - Currency ISO code.
* @param {Date} [opts.ts] - A timestamp to base the rate on (default Date.now()).
* @param {String} [opts.provider] - A provider of exchange rates (default 'BitPay').
* @returns {Object} rates - The exchange rate.
*/
2016-01-11 12:46:36 -08:00
WalletService.prototype.getFiatRate = function(opts, cb) {
2016-01-11 12:10:24 -08:00
var self = this;
2016-05-20 06:12:41 -07:00
if (!checkRequired(opts, ['code'], cb)) return;
2016-01-11 12:10:24 -08:00
2016-01-11 12:46:36 -08:00
self.fiatRateService.getRate(opts, function(err, rate) {
if (err) return cb(err);
return cb(null, rate);
});
2016-01-11 12:10:24 -08:00
};
WalletService.prototype.pushNotificationsSubscribe = function(opts, cb) {
2016-05-20 06:12:41 -07:00
if (!checkRequired(opts, ['token'], cb)) return;
2016-01-18 05:26:37 -08:00
var self = this;
2016-01-18 11:28:11 -08:00
opts.user = self.walletId + '$' + self.copayerId;
request({
url: config.pushNotificationsOpts.pushServerUrl + '/subscribe',
method: 'POST',
json: true,
body: opts
}, function(err, response) {
return cb(err, response);
});
};
2016-01-18 12:37:40 -08:00
WalletService.prototype.pushNotificationsUnsubscribe = function(cb) {
var self = this;
request({
url: config.pushNotificationsOpts.pushServerUrl + '/unsubscribe',
method: 'POST',
json: true,
body: {
2016-01-18 11:28:11 -08:00
user: self.walletId + '$' + self.copayerId
}
}, function(err, response) {
return cb(err, response);
});
};
2015-04-02 07:18:39 -07:00
2015-02-20 12:32:19 -08:00
module.exports = WalletService;
2015-02-09 10:30:16 -08:00
module.exports.ClientError = ClientError;