bitcore-wallet-service/lib/server.js

3403 lines
103 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');
2017-08-25 12:04:14 -07:00
var Bitcore_ = {
btc: Bitcore,
bch: require('bitcore-lib-cash')
};
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) {
2016-08-08 06:06:58 -07:00
messageBroker = opts.messageBroker || new MessageBroker(opts.messageBrokerOpts);
if (messageBroker) {
messageBroker.onMessage(WalletService.handleIncomingNotification);
2015-05-04 14:23:56 -07:00
}
2016-08-08 06:06:58 -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
};
2016-08-08 06:06:58 -07:00
WalletService.handleIncomingNotification = function(notification, cb) {
cb = cb || function() {};
if (!notification || notification.type != 'NewBlock') return cb();
2016-08-10 07:14:21 -07:00
2017-08-25 12:04:14 -07:00
WalletService._clearBlockchainHeightCache(notification.data.coin, notification.data.network);
2016-08-08 06:06:58 -07:00
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 || {};
2016-07-13 12:37:08 -07:00
var version = Utils.parseVersion(opts.clientVersion);
if (version && version.agent == 'bwc') {
if (version.major == 0 || (version.major == 1 && version.minor < 2)) {
throw new ClientError(Errors.codes.UPGRADE_NEEDED, 'BWC clients < 1.2 are no longer supported.');
}
}
2015-06-29 08:20:24 -07:00
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 - (Optional) The contents of the request to be signed. Only needed if no session token is provided.
* @param {string} opts.signature - (Optional) Signature of message to be verified using one of the copayer's requestPubKeys. Only needed if no session token is provided.
* @param {string} opts.session - (Optional) A valid session token previously obtained using the #login method
2015-06-29 08:20:24 -07:00
* @param {string} opts.clientVersion - A string that identifies the client issuing the request
* @param {string} [opts.walletId] - The wallet id to use as current wallet for this request (only when copayer is support staff).
2015-02-06 12:56:51 -08:00
*/
2015-02-20 12:32:19 -08:00
WalletService.getInstanceWithAuth = function(opts, cb) {
function withSignature(cb) {
if (!checkRequired(opts, ['copayerId', 'message', 'signature'], cb)) return;
var server;
try {
server = WalletService.getInstance(opts);
} catch (ex) {
return cb(ex);
}
2015-02-06 12:56:51 -08:00
server.storage.fetchCopayerLookup(opts.copayerId, function(err, copayer) {
if (err) return cb(err);
if (!copayer) return cb(new ClientError(Errors.codes.NOT_AUTHORIZED, 'Copayer not found'));
2016-07-13 12:37:08 -07:00
if (!copayer.isSupportStaff) {
var isValid = !!server._getSigningKey(opts.message, opts.signature, copayer.requestPubKeys);
if (!isValid)
return cb(new ClientError(Errors.codes.NOT_AUTHORIZED, 'Invalid signature'));
server.walletId = copayer.walletId;
} else {
server.walletId = opts.walletId || copayer.walletId;
server.copayerIsSupportStaff = true;
}
2015-02-06 12:56:51 -08:00
server.copayerId = opts.copayerId;
return cb(null, server);
});
};
2015-02-02 10:56:53 -08:00
function withSession(cb) {
if (!checkRequired(opts, ['copayerId', 'session'], cb)) return;
var server;
try {
server = WalletService.getInstance(opts);
} catch (ex) {
return cb(ex);
}
server.storage.getSession(opts.copayerId, function(err, s) {
if (err) return cb(err);
var isValid = s && s.id == opts.session && s.isValid();
if (!isValid) return cb(new ClientError(Errors.codes.NOT_AUTHORIZED, 'Session expired'));
server.storage.fetchCopayerLookup(opts.copayerId, function(err, copayer) {
if (err) return cb(err);
if (!copayer) return cb(new ClientError(Errors.codes.NOT_AUTHORIZED, 'Copayer not found'));
server.copayerId = opts.copayerId;
server.walletId = copayer.walletId;
return cb(null, server);
});
});
};
var authFn = opts.session ? withSession : withSignature;
return authFn(cb);
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);
};
WalletService.prototype.login = function(opts, cb) {
var self = this;
var session;
async.series([
function(next) {
self.storage.getSession(self.copayerId, function(err, s) {
if (err) return next(err);
session = s;
next();
});
},
function(next) {
if (!session || !session.isValid()) {
session = Model.Session.create({
copayerId: self.copayerId,
walletId: self.walletId,
});
} else {
session.touch();
}
next();
},
function(next) {
self.storage.storeSession(session, next);
},
], function(err) {
if (err) return cb(err);
if (!session) return cb(new Error('Could not get current session for this copayer'));
return cb(null, session.id);
});
};
WalletService.prototype.logout = function(opts, cb) {
var self = this;
self.storage.removeSession(self.copayerId, cb);
};
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.
2017-08-23 13:28:35 -07:00
* @param {string} opts.coin[='btc'] - The coin for this wallet (btc, bch).
2016-06-03 09:34:12 -07:00
* @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'));
2017-08-23 13:28:35 -07:00
opts.coin = opts.coin || Defaults.COIN;
if (!Utils.checkValueInCollection(opts.coin, Constants.COINS))
return cb(new ClientError('Invalid coin'));
2015-08-28 10:54:36 -07:00
opts.network = opts.network || 'livenet';
2017-08-23 13:28:35 -07:00
if (!Utils.checkValueInCollection(opts.network, Constants.NETWORKS))
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,
2017-08-23 13:28:35 -07:00
coin: opts.coin,
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
};
/**
* Retrieves a wallet from storage.
* @param {Object} opts
* @param {string} opts.identifier - The identifier associated with the wallet (one of: walletId, address, txid).
* @returns {Object} wallet
*/
WalletService.prototype.getWalletFromIdentifier = function(opts, cb) {
var self = this;
2017-06-29 06:56:42 -07:00
if (!opts.identifier) return cb();
var walletId;
async.parallel([
2017-06-27 11:56:09 -07:00
function(done) {
self.storage.fetchWallet(opts.identifier, function(err, wallet) {
if (wallet) walletId = wallet.id;
2017-06-27 11:56:09 -07:00
return done(err);
});
},
2017-06-27 11:56:09 -07:00
function(done) {
2017-08-31 07:59:30 -07:00
self.storage.fetchAddressByCoin(Defaults.COIN, opts.identifier, function(err, address) {
if (address) walletId = address.walletId;
2017-06-27 11:56:09 -07:00
return done(err);
});
},
2017-06-27 11:56:09 -07:00
function(done) {
self.storage.fetchTxByHash(opts.identifier, function(err, tx) {
if (tx) walletId = tx.walletId;
2017-06-27 11:56:09 -07:00
return done(err);
});
},
], function(err) {
if (err) return cb(err);
if (walletId) {
return self.storage.fetchWallet(walletId, cb);
}
2017-09-08 08:46:46 -07:00
var re = /^[\da-f]+$/gi;
if (!re.test(opts.identifier)) return cb();
// Is identifier a txid form an incomming tx?
2017-08-25 12:04:14 -07:00
var coinNetworkPairs = [];
_.each(_.values(Constants.COINS), function(coin) {
_.each(_.values(Constants.NETWORKS), function(network) {
coinNetworkPairs.push({
coin: coin,
network: network
});
});
});
async.detectSeries(coinNetworkPairs, function(coinNetwork, nextCoinNetwork) {
var bc = self._getBlockchainExplorer(coinNetwork.coin, coinNetwork.network);
2017-09-08 08:46:46 -07:00
if (!bc) return nextCoinNetwork(false);
2017-06-27 11:56:09 -07:00
bc.getTransaction(opts.identifier, function(err, tx) {
2017-09-08 08:46:46 -07:00
if (err || !tx) return nextCoinNetwork(false);
var outputs = _.first(self._normalizeTxHistory(tx)).outputs;
2017-10-30 13:50:56 -07:00
var toAddresses = _.map(outputs, 'address');
async.detect(toAddresses, function(addressStr, nextAddress) {
2017-08-31 07:59:30 -07:00
self.storage.fetchAddressByCoin(coinNetwork.coin, addressStr, function(err, address) {
2017-09-08 08:46:46 -07:00
if (err || !address) return nextAddress(false);
walletId = address.walletId;
2017-09-08 08:46:46 -07:00
nextAddress(true);
});
2017-09-08 08:46:46 -07:00
}, function() {
nextCoinNetwork(!!walletId);
});
});
2017-09-08 09:06:13 -07:00
}, function() {
2017-09-08 08:46:46 -07:00
if (!walletId) return cb();
return self.storage.fetchWallet(walletId, cb);
});
});
};
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) {
2017-03-15 11:36:11 -07:00
var pub = (new Bitcore.HDPublicKey(xPubKey)).deriveChild(Constants.PATHS.REQUEST_KEY_AUTH).publicKey;
2015-10-30 11:24:47 -07:00
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
WalletService.prototype._notifyTxProposalAction = function(type, txp, extraArgs, cb) {
var self = this;
if (_.isFunction(extraArgs)) {
cb = extraArgs;
extraArgs = {};
}
var data = _.assign({
txProposalId: txp.id,
creatorId: txp.creatorId,
amount: txp.getTotalAmount(),
message: txp.message,
}, extraArgs);
self._notify(type, data, {}, cb);
};
2015-08-05 12:53:06 -07:00
WalletService.prototype._addCopayerToWallet = function(wallet, opts, cb) {
var self = this;
var copayer = Model.Copayer.create({
coin: wallet.coin,
2015-08-05 12:53:06 -07:00
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
});
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() {
if (_.isUndefined(this.parsedClientVersion)) {
2016-07-13 12:37:08 -07:00
this.parsedClientVersion = Utils.parseVersion(this.clientVersion);
2015-09-01 07:53:07 -07:00
}
return this.parsedClientVersion;
};
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.coin[='btc'] - The expected coin for this wallet (btc, bch).
2015-01-27 07:54:17 -08:00
* @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
opts.coin = opts.coin || Defaults.COIN;
if (!Utils.checkValueInCollection(opts.coin, Constants.COINS))
return cb(new ClientError('Invalid coin'));
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
if (opts.coin != wallet.coin) {
return cb(new ClientError('The wallet you are trying to join was created for a different coin'));
}
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());
},
}];
2017-10-30 13:50:56 -07:00
opts = _.pick(opts, _.map(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);
2017-08-25 12:04:14 -07:00
var bc = self._getBlockchainExplorer(latestAddresses[0].coin, latestAddresses[0].network);
2017-09-08 09:06:13 -07:00
if (!bc) return cb(new Error('Could not get blockchain explorer instance'));
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) {
var address = wallet.createAddress(false);
2015-02-04 11:18:36 -08:00
self.storage.storeAddressAndWallet(wallet, address, function(err) {
if (err) return cb(err);
self._notify('NewAddress', {
address: address.address,
}, function() {
return cb(null, address);
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._canCreateAddress(opts.ignoreMaxGap, function(err, canCreate) {
if (err) return cb(err);
if (!canCreate) return cb(Errors.MAIN_ADDRESS_GAP_REACHED);
self._runLocked(cb, function(cb) {
self.getWallet({}, function(err, wallet) {
if (err) return cb(err);
if (!wallet.isComplete()) return cb(Errors.WALLET_NOT_COMPLETE);
2016-06-03 09:34:12 -07:00
var createFn = wallet.singleAddress ? getFirstAddress : createNewAddress;
return createFn(wallet, cb);
});
2016-06-03 09:34:12 -07:00
});
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
2017-08-25 12:04:14 -07:00
WalletService.prototype._getBlockchainExplorer = function(coin, network) {
2017-06-27 11:56:09 -07:00
var opts = {};
2015-03-30 11:34:05 -07:00
2017-06-27 11:56:09 -07:00
if (this.blockchainExplorer) return this.blockchainExplorer;
2017-08-25 12:04:14 -07:00
if (this.blockchainExplorerOpts && this.blockchainExplorerOpts[coin] && this.blockchainExplorerOpts[coin][network]) {
opts = this.blockchainExplorerOpts[coin][network];
2017-06-27 11:56:09 -07:00
}
// TODO: provider should be configurable
opts.provider = 'insight';
2017-08-25 12:04:14 -07:00
opts.coin = coin;
2017-06-27 11:56:09 -07:00
opts.network = network;
opts.userAgent = WalletService.getServiceVersion();
2017-09-08 08:46:46 -07:00
var bc;
try {
bc = new BlockchainExplorer(opts);
} catch (ex) {
log.warn('Could not instantiate blockchain explorer', ex);
}
return bc;
2015-01-27 05:18:45 -08:00
};
2017-08-25 12:04:14 -07:00
WalletService.prototype._getUtxos = function(coin, 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, []);
2017-09-12 19:42:14 -07:00
var networkName = Bitcore_[coin].Address(addresses[0]).toObject().network;
2015-08-13 08:01:22 -07:00
2017-08-25 12:04:14 -07:00
var bc = self._getBlockchainExplorer(coin, networkName);
2017-09-08 09:06:13 -07:00
if (!bc) return cb(new Error('Could not get blockchain explorer instance'));
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);
});
};
2017-09-01 07:45:19 -07:00
WalletService.prototype._getUtxosForCurrentWallet = function(opts, cb) {
2015-08-13 08:01:22 -07:00
var self = this;
2015-08-13 07:00:27 -07:00
2017-09-01 07:45:19 -07:00
var opts = opts || {};
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 coin, allAddresses, allUtxos, utxoIndex, addressStrs;
async.series([
2017-08-25 12:04:14 -07:00
function(next) {
self.getWallet({}, function(err, wallet) {
if (err) return next(err);
coin = wallet.coin;
return next();
});
2017-08-25 12:04:14 -07:00
},
function(next) {
2017-09-01 07:45:19 -07:00
if (_.isArray(opts.addresses)) {
allAddresses = opts.addresses;
return next();
}
self.storage.fetchAddresses(self.walletId, function(err, addresses) {
allAddresses = addresses;
if (allAddresses.length == 0) return cb(null, []);
return next();
});
},
function(next) {
2017-10-30 13:50:56 -07:00
addressStrs = _.map(allAddresses, 'address');
if (!opts.coin) return next();
coin = opts.coin;
addressStrs = _.map(addressStrs, function(a) {
return Utils.translateAddress(a, coin);
});
next();
},
function(next) {
2017-08-25 12:04:14 -07:00
self._getUtxos(coin, 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
2017-10-30 13:50:56 -07:00
var lockedInputs = _.map(_.flatten(_.map(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);
2017-10-30 13:50:56 -07:00
var spentInputs = _.map(_.flatten(_.map(txs, 'inputs')), utxoKey);
_.each(spentInputs, function(input) {
if (utxoIndex[input]) {
utxoIndex[input].spent = true;
}
});
allUtxos = _.reject(allUtxos, {
spent: true
});
return next();
});
},
function(next) {
if (opts.coin) return 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
2017-08-25 12:04:14 -07:00
* @param {String} [opts.coin='btc'] (optional)
2015-08-13 08:01:22 -07:00
* @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 || {};
2017-09-13 20:49:44 -07:00
if (opts.coin) {
if (!Utils.checkValueInCollection(opts.coin, Constants.COINS))
return cb(new ClientError('Invalid coin'));
}
2017-08-25 12:04:14 -07:00
2015-08-13 08:01:22 -07:00
if (_.isUndefined(opts.addresses)) {
2017-09-01 07:45:19 -07:00
self._getUtxosForCurrentWallet({
coin: opts.coin
}, cb);
2015-08-13 08:01:22 -07:00
} else {
2017-09-13 20:49:44 -07:00
self._getUtxos(Utils.getAddressCoin(opts.addresses[0]), 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
WalletService.prototype._getBalanceFromAddresses = function(opts, cb, i) {
2015-02-02 12:07:18 -08:00
var self = this;
2015-01-30 12:37:30 -08:00
2017-09-01 07:45:19 -07:00
var opts = opts || {};
// This lock is to prevent server starvation on big wallets
self._runLocked(cb, function(cb) {
self._getUtxosForCurrentWallet({
coin: opts.coin,
addresses: opts.addresses
}, function(err, utxos) {
if (err) return cb(err);
2015-01-30 12:37:30 -08:00
var balance = self._totalizeUtxos(utxos);
2015-03-06 09:58:22 -08:00
// Compute balance by address
var byAddress = {};
_.each(_.indexBy(_.sortBy(utxos, 'address'), 'address'), function(value, key) {
byAddress[key] = {
address: key,
path: value.path,
amount: 0,
};
});
2015-03-06 09:58:22 -08:00
_.each(utxos, function(utxo) {
byAddress[utxo.address].amount += utxo.satoshis;
});
2015-03-06 09:58:22 -08:00
balance.byAddress = _.values(byAddress);
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) {
var self = this;
2017-10-31 16:13:37 -07:00
self.storage.fetchAddresses(self.walletId, function(err, addresses) {
if (err) return cb(err);
2017-10-31 16:13:37 -07:00
self._getBalanceFromAddresses({
coin: opts.coin,
addresses: addresses
}, function(err, balance) {
if (err) return cb(err);
2017-10-31 16:13:37 -07:00
// Update cache
var withBalance = _.map(balance.byAddress, 'address')
self.storage.storeAddressesWithBalance(self.walletId, withBalance, function(err) {
if (err) {
log.warn('Could not update wallet cache', err);
}
return cb(null, balance);
});
});
});
};
WalletService.prototype._getActiveAddresses = function(cb) {
var self = this;
2017-10-31 15:21:36 -07:00
self.storage.fetchAddressesWithBalance(self.walletId, function(err, addressesWB) {
if (err) {
log.warn('Could not fetch active addresses from cache', err);
return cb();
}
2017-10-31 15:21:36 -07:00
if (!_.isArray(addressesWB))
addressesWB = [];
2017-10-31 15:21:36 -07:00
var now = Math.floor(Date.now() / 1000);
var fromTs = now - Defaults.TWO_STEP_CREATION_HOURS * 3600;
2017-10-31 15:21:36 -07:00
self.storage.fetchNewAddresses(self.walletId, fromTs, function(err, recent) {
if (err) return cb(err);
2017-10-31 15:21:36 -07:00
var result = _.uniq(_.union(addressesWB, recent), 'address');
return cb(null, result);
});
});
};
2017-10-31 15:21:36 -07:00
WalletService.prototype._checkAndUpdateAddressCount = function(twoStepCache, cb) {
var self = this;
2017-10-31 15:21:36 -07:00
if (twoStepCache.addressCount > Defaults.TWO_STEP_BALANCE_THRESHOLD) {
log.info('Not counting addresses for '+ self.walletId);
return cb(null, true);
}
self.storage.countAddresses(self.walletId, function(err, addressCount) {
if (err) return cb(err);
if (addressCount < Defaults.TWO_STEP_BALANCE_THRESHOLD)
return cb(null, false);
twoStepCache.addressCount = addressCount;
// updates cache
self.storage.storeTwoStepCache(self.walletId, twoStepCache, function(err) {
if (err) return cb(err);
return cb(null, true);
});
});
};
2015-12-04 12:17:40 -08:00
/**
* Get wallet balance.
* @param {Object} opts
2017-09-01 11:06:58 -07:00
* @param {string} [opts.coin] - Override wallet coin (default wallet's coin).
* @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, i) {
2015-12-04 12:17:40 -08:00
var self = this;
opts = opts || {};
2017-09-01 11:06:58 -07:00
if (opts.coin) {
if (!Utils.checkValueInCollection(opts.coin, Constants.COINS))
return cb(new ClientError('Invalid coin'));
}
2017-10-31 15:21:36 -07:00
if (!opts.twoStep) {
return self._getBalanceOneStep(opts, cb);
2017-10-31 15:21:36 -07:00
}
2017-10-31 15:21:36 -07:00
self.storage.getTwoStepCache(self.walletId, function(err, twoStepCache) {
2015-12-10 06:43:47 -08:00
if (err) return cb(err);
2017-10-31 15:21:36 -07:00
twoStepCache = twoStepCache || {};
self._checkAndUpdateAddressCount(twoStepCache, function(err, needsTwoStep ) {
2015-12-10 07:36:30 -08:00
if (err) return cb(err);
2017-10-31 15:21:36 -07:00
if (!needsTwoStep) {
return self._getBalanceOneStep(opts, cb);
}
2017-10-31 15:21:36 -07:00
self._getActiveAddresses(function(err, activeAddresses) {
if (err) return cb(err);
if (!_.isArray(activeAddresses)) {
return self._getBalanceOneStep(opts, cb);
} else {
log.debug('Requesting partial balance for ' + activeAddresses.length + ' addresses');
2017-10-31 15:21:36 -07:00
self._getBalanceFromAddresses({
coin: opts.coin,
addresses: activeAddresses
}, function(err, partialBalance) {
if (err) return cb(err);
cb(null, partialBalance);
var now = Math.floor(Date.now() / 1000);
if (twoStepCache.lastEmpty > now - Defaults.TWO_STEP_INACTIVE_CLEAN_DURATION_MIN * 60 ) {
2017-11-01 07:43:47 -07:00
log.debug('Not running the FULL balance query due to TWO_STEP_INACTIVE_CLEAN_DURATION_MIN ');
2017-10-31 15:21:36 -07:00
return;
}
setTimeout(function() {
2017-11-01 07:43:47 -07:00
log.debug('Running full balance query');
2017-10-31 15:21:36 -07:00
self._getBalanceOneStep(opts, function(err, fullBalance) {
if (err) return;
if (!_.isEqual(partialBalance, fullBalance)) {
log.info('Balance in active addresses differs from final balance');
self._notify('BalanceUpdated', fullBalance, {
isGlobal: true
});
} else {
// updates cache
twoStepCache.lastEmpty = now;
// updates cache
return self.storage.storeTwoStepCache(self.walletId, twoStepCache, function(err) {
return;
});
}
});
}, 1);
return;
}, i);
2017-10-31 15:21:36 -07:00
}
});
2015-12-10 07:36:30 -08:00
});
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
2016-08-23 07:57:17 -07:00
* @param {number} opts.feeLevel[='normal'] - Optional. Specify the fee level for this TX ('priority', 'normal', 'economy', 'superEconomy') as defined in Defaults.FEE_LEVELS.
* @param {number} opts.feePerKb - Optional. Specify the fee per KB for this TX (in satoshi).
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 || {};
2017-08-29 12:33:35 -07:00
self.getWallet({}, function(err, wallet) {
if (err) return cb(err);
2016-08-23 07:57:17 -07:00
2017-08-29 12:33:35 -07:00
var feeArgs = !!opts.feeLevel + _.isNumber(opts.feePerKb);
if (feeArgs > 1)
return cb(new ClientError('Only one of feeLevel/feePerKb can be specified'));
2016-08-23 07:57:17 -07:00
2017-08-29 12:33:35 -07:00
if (feeArgs == 0) {
log.debug('No fee provided, using "normal" fee level');
opts.feeLevel = 'normal';
}
2016-08-23 07:57:17 -07:00
2017-08-29 12:33:35 -07:00
var feeLevels = Defaults.FEE_LEVELS[wallet.coin];
if (opts.feeLevel) {
if (!_.any(feeLevels, {
name: opts.feeLevel
}))
2017-10-30 13:50:56 -07:00
return cb(new ClientError('Invalid fee level. Valid values are ' + _.map(feeLevels, 'name').join(', ')));
2017-08-29 12:33:35 -07:00
}
2016-02-19 06:11:43 -08:00
2017-08-29 12:33:35 -07:00
if (_.isNumber(opts.feePerKb)) {
if (opts.feePerKb < Defaults.MIN_FEE_PER_KB || opts.feePerKb > Defaults.MAX_FEE_PER_KB)
return cb(new ClientError('Invalid fee per KB'));
}
2016-02-19 06:11:43 -08:00
2017-09-01 07:45:19 -07:00
self._getUtxosForCurrentWallet({}, function(err, utxos) {
2016-02-19 06:11:43 -08:00
if (err) return cb(err);
var info = {
size: 0,
amount: 0,
fee: 0,
2016-08-23 07:57:17 -07:00
feePerKb: 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);
2016-08-23 07:57:17 -07:00
self._getFeePerKb(wallet, opts, function(err, feePerKb) {
if (err) return cb(err);
2016-02-19 06:11:43 -08:00
2016-08-23 07:57:17 -07:00
info.feePerKb = feePerKb;
2016-03-18 12:32:11 -07:00
2016-08-23 07:57:17 -07:00
var txp = Model.TxProposal.create({
walletId: self.walletId,
2017-08-25 12:04:14 -07:00
coin: wallet.coin,
2016-08-23 07:57:17 -07:00
network: wallet.network,
walletM: wallet.m,
walletN: wallet.n,
feePerKb: feePerKb,
});
2016-03-18 12:32:11 -07:00
2016-08-23 07:57:17 -07:00
var baseTxpSize = txp.getEstimatedSize();
var baseTxpFee = baseTxpSize * txp.feePerKb / 1000.;
var sizePerInput = txp.getEstimatedSizeForSingleInput();
var feePerInput = sizePerInput * txp.feePerKb / 1000.;
2016-03-18 12:32:11 -07:00
2016-08-23 07:57:17 -07:00
var partitionedByAmount = _.partition(inputs, function(input) {
return input.satoshis > feePerInput;
});
2016-03-18 11:56:10 -07:00
2016-08-23 07:57:17 -07:00
info.utxosBelowFee = partitionedByAmount[1].length;
info.amountBelowFee = _.sum(partitionedByAmount[1], 'satoshis');
inputs = partitionedByAmount[0];
2016-03-18 11:56:10 -07:00
2016-08-23 07:57:17 -07:00
_.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');
return false;
}
txp.inputs.push(input);
});
2016-02-19 06:11:43 -08:00
2016-08-23 07:57:17 -07:00
if (_.isEmpty(txp.inputs)) return cb(null, info);
2017-05-15 06:24:15 -07:00
var fee = txp.getEstimatedFee();
var amount = _.sum(txp.inputs, 'satoshis') - fee;
if (amount < Defaults.MIN_OUTPUT_AMOUNT) return cb(null, info);
2016-08-23 07:57:17 -07:00
info.size = txp.getEstimatedSize();
2017-05-15 06:24:15 -07:00
info.fee = fee;
info.amount = amount;
2016-08-23 07:57:17 -07:00
if (opts.returnInputs) {
info.inputs = _.shuffle(txp.inputs);
}
return cb(null, info);
});
2016-02-19 06:11:43 -08:00
});
});
};
2016-03-04 12:02:05 -08:00
2017-08-25 12:04:14 -07:00
WalletService.prototype._sampleFeeLevels = function(coin, network, points, cb) {
2015-07-15 18:42:05 -07:00
var self = this;
2017-08-25 12:04:14 -07:00
var bc = self._getBlockchainExplorer(coin, network);
2017-09-08 09:06:13 -07:00
if (!bc) return cb(new Error('Could not get blockchain explorer instance'));
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
});
};
/**
* Returns fee levels for the current state of the network.
* @param {Object} opts
2017-08-25 12:04:14 -07:00
* @param {string} [opts.coin = 'btc'] - The coin to estimate fee levels from.
2015-07-15 18:42:05 -07:00
* @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 || {};
2017-08-29 12:33:35 -07:00
opts.coin = opts.coin || Defaults.COIN;
if (!Utils.checkValueInCollection(opts.coin, Constants.COINS))
return cb(new ClientError('Invalid coin'));
opts.network = opts.network || 'livenet';
if (!Utils.checkValueInCollection(opts.network, Constants.NETWORKS))
return cb(new ClientError('Invalid network'));
var feeLevels = Defaults.FEE_LEVELS[opts.coin];
2016-06-27 08:29:37 -07:00
function samplePoints() {
2017-10-30 13:50:56 -07:00
var definedPoints = _.uniq(_.map(feeLevels, 'nbBlocks'));
2016-06-27 08:29:37 -07:00
return _.uniq(_.flatten(_.map(definedPoints, function(p) {
return _.range(p, p + Defaults.FEE_LEVELS_FALLBACK + 1);
})));
};
function getFeeLevel(feeSamples, level, n, fallback) {
var result;
if (feeSamples[n] >= 0) {
result = {
nbBlocks: n,
feePerKb: feeSamples[n],
};
} else {
if (fallback > 0) {
result = getFeeLevel(feeSamples, level, n + 1, fallback - 1);
} else {
result = {
feePerKb: level.defaultValue,
nbBlocks: null,
};
}
}
return result;
};
2017-08-25 12:04:14 -07:00
self._sampleFeeLevels(opts.coin, opts.network, samplePoints(), function(err, feeSamples) {
2017-08-29 12:33:35 -07:00
var values = _.map(feeLevels, function(level) {
2015-07-27 05:00:37 -07:00
var result = {
2015-07-16 12:17:58 -07:00
level: level.name,
};
2016-06-27 08:29:37 -07:00
if (err) {
result.feePerKb = level.defaultValue;
result.nbBlocks = null;
} else {
2016-06-27 08:29:37 -07:00
var feeLevel = getFeeLevel(feeSamples, level, level.nbBlocks, Defaults.FEE_LEVELS_FALLBACK);
2017-03-01 13:51:22 -08:00
result.feePerKb = +(feeLevel.feePerKb * (level.multiplier || 1)).toFixed(0);
2016-06-27 08:29:37 -07:00
result.nbBlocks = feeLevel.nbBlocks;
}
return result;
2015-07-16 12:17:58 -07:00
});
2015-07-15 18:42:05 -07:00
2016-06-27 08:29:37 -07:00
// Ensure monotonically decreasing values
for (var i = 1; i < values.length; i++) {
values[i].feePerKb = Math.min(values[i].feePerKb, values[i - 1].feePerKb);
}
2016-03-04 12:02:05 -08:00
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 = {
2016-08-04 07:35:40 -07:00
disableIsFullySigned: true,
disableSmallFees: true,
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;
}
2017-08-25 12:04:14 -07:00
if (bitcoreError instanceof Bitcore_[txp.coin].errors.Transaction.FeeError)
return Errors.INSUFFICIENT_FUNDS_FOR_FEE;
2017-08-25 12:04:14 -07:00
if (bitcoreError instanceof Bitcore_[txp.coin].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));
2017-08-25 12:04:14 -07:00
var dustThreshold = Math.max(Defaults.MIN_OUTPUT_AMOUNT, Bitcore_[txp.coin].Transaction.DUST_AMOUNT);
2016-06-10 09:50:57 -07:00
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
2017-09-01 07:45:19 -07:00
self._getUtxosForCurrentWallet({}, function(err, utxos) {
2016-02-29 13:17:14 -08:00
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) {
2017-09-13 12:46:23 -07:00
var A = Bitcore_[wallet.coin].Address;
2017-08-25 12:04:14 -07:00
var dustThreshold = Math.max(Defaults.MIN_OUTPUT_AMOUNT, Bitcore_[wallet.coin].Transaction.DUST_AMOUNT);
2016-06-14 08:22:26 -07:00
if (_.isEmpty(opts.outputs)) return new ClientError('No outputs were specified');
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 {
2017-09-13 12:46:23 -07:00
toAddress = new A(output.toAddress);
} catch (ex) {
return Errors.INVALID_ADDRESS;
}
2017-08-25 12:04:14 -07:00
if (toAddress.network != wallet.network) {
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');
}
2016-06-14 08:22:26 -07:00
if (output.amount < dustThreshold) {
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
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-08-22 15:46:23 -07:00
var feeArgs = !!opts.feeLevel + _.isNumber(opts.feePerKb) + _.isNumber(opts.fee);
if (feeArgs > 1)
return next(new ClientError('Only one of feeLevel/feePerKb/fee can be specified'));
if (feeArgs == 0) {
log.debug('No fee provided, using "normal" fee level');
opts.feeLevel = 'normal';
}
2017-08-29 12:33:35 -07:00
var feeLevels = Defaults.FEE_LEVELS[wallet.coin];
2016-08-22 15:46:23 -07:00
if (opts.feeLevel) {
2017-08-29 12:33:35 -07:00
if (!_.any(feeLevels, {
2016-08-22 15:46:23 -07:00
name: opts.feeLevel
}))
2017-10-30 13:50:56 -07:00
return next(new ClientError('Invalid fee level. Valid values are ' + _.map(feeLevels, 'name').join(', ')));
2016-08-22 15:46:23 -07:00
}
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'));
}
2016-08-22 15:46:23 -07:00
2016-08-23 07:57:17 -07:00
if (_.isNumber(opts.fee) && _.isEmpty(opts.inputs))
2016-08-22 15:46:23 -07:00
return next(new ClientError('fee can only be set when inputs are specified'));
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))
2016-08-22 15:46:23 -07:00
return next(new ClientError('Fee is not allowed when sendMax is specified (use feeLevel/feePerKb instead)'));
self.getSendMaxInfo({
2017-08-29 12:33:35 -07:00
feePerKb: opts.feePerKb,
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();
});
},
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);
};
2016-08-23 07:57:17 -07:00
WalletService.prototype._getFeePerKb = function(wallet, opts, cb) {
var self = this;
if (_.isNumber(opts.feePerKb)) return cb(null, opts.feePerKb);
self.getFeeLevels({
2017-08-25 12:04:14 -07:00
coin: wallet.coin,
2016-08-23 07:57:17 -07:00
network: wallet.network
}, function(err, levels) {
if (err) return cb(err);
var level = _.find(levels, {
level: opts.feeLevel
});
if (!level) {
var msg = 'Could not compute fee for "' + opts.feeLevel + '" level';
log.error(msg);
return cb(new ClientError(msg));
}
return cb(null, level.feePerKb);
});
};
2015-11-26 05:50:22 -08:00
/**
* Creates a new transaction proposal.
* @param {Object} opts
* @param {string} opts.txProposalId - Optional. If provided it will be used as this TX proposal ID. Should be unique in the scope of the wallet.
2015-11-26 05:50:22 -08:00
* @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-08-22 15:46:23 -07:00
* @param {number} opts.feeLevel[='normal'] - Optional. Specify the fee level for this TX ('priority', 'normal', 'economy', 'superEconomy') as defined in Defaults.FEE_LEVELS.
* @param {number} opts.feePerKb - Optional. Specify the fee per KB for this TX (in satoshi).
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-08-11 06:54:15 -07:00
opts = opts || {};
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) {
2017-08-31 07:59:30 -07:00
self.storage.fetchAddressByWalletId(wallet.id, 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
}
}
};
function checkTxpAlreadyExists(txProposalId, cb) {
if (!txProposalId) return cb();
self.storage.fetchTx(self.walletId, txProposalId, cb);
};
2015-11-26 05:50:22 -08:00
self._runLocked(cb, function(cb) {
2016-08-22 15:46:23 -07:00
var txp, changeAddress, feePerKb;
self.getWallet({}, function(err, wallet) {
if (err) return cb(err);
if (!wallet.isComplete()) return cb(Errors.WALLET_NOT_COMPLETE);
checkTxpAlreadyExists(opts.txProposalId, function(err, txp) {
if (err) return cb(err);
if (txp) return cb(null, txp);
async.series([
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) {
if (opts.sendMax) return next();
getChangeAddress(wallet, function(err, address) {
if (err) return next(err);
changeAddress = address;
next();
});
},
2016-08-22 15:46:23 -07:00
function(next) {
2016-08-23 07:57:17 -07:00
if (_.isNumber(opts.fee) && !_.isEmpty(opts.inputs)) return next();
self._getFeePerKb(wallet, opts, function(err, fee) {
2016-08-22 15:46:23 -07:00
feePerKb = fee;
next();
});
},
function(next) {
var txOpts = {
id: opts.txProposalId,
walletId: self.walletId,
creatorId: self.copayerId,
2017-08-25 12:04:14 -07:00
coin: wallet.coin,
network: wallet.network,
outputs: opts.outputs,
message: opts.message,
changeAddress: changeAddress,
2016-08-22 16:34:22 -07:00
feeLevel: opts.feeLevel,
2016-08-22 15:46:23 -07:00
feePerKb: feePerKb,
payProUrl: opts.payProUrl,
walletM: wallet.m,
walletN: wallet.n,
excludeUnconfirmedUtxos: !!opts.excludeUnconfirmedUtxos,
validateOutputs: !opts.validateOutputs,
addressType: wallet.addressType,
customData: opts.customData,
inputs: opts.inputs,
fee: opts.inputs && !_.isNumber(opts.feePerKb) ? opts.fee : null,
noShuffleOutputs: opts.noShuffleOutputs
};
txp = Model.TxProposal.create(txOpts);
next();
},
function(next) {
self._selectTxInputs(txp, opts.utxosToExclude, next);
},
function(next) {
if (!changeAddress || wallet.singleAddress || opts.dryRun) return next();
self.storage.storeAddressAndWallet(wallet, txp.changeAddress, next);
},
function(next) {
if (opts.dryRun) return next();
self.storage.storeTx(wallet.id, txp, next);
},
], function(err) {
2016-07-26 07:09:17 -07:00
if (err) return cb(err);
return cb(null, txp);
2016-06-03 09:34:12 -07:00
});
2015-11-26 05:50:22 -08:00
});
2015-11-26 05:50:22 -08:00
});
});
};
WalletService.prototype._verifyRequestPubKey = function(requestPubKey, signature, xPubKey) {
2017-03-15 11:36:11 -07:00
var pub = (new Bitcore.HDPublicKey(xPubKey)).deriveChild(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);
2016-08-19 11:06:25 -07:00
if (!txp.isTemporary()) return cb(null, txp);
var copayer = wallet.getCopayer(self.copayerId);
2016-08-04 07:35:40 -07:00
var raw;
try {
raw = txp.getRawTx();
} catch (ex) {
return cb(ex);
}
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
2017-09-04 06:40:16 -07:00
self._getUtxosForCurrentWallet({}, function(err, utxos) {
if (err) return cb(err);
var txpInputs = _.map(txp.inputs, utxoKey);
2017-04-05 06:51:50 -07:00
var utxosIndex = _.indexBy(utxos, utxoKey);
var unavailable = _.any(txpInputs, function(i) {
2017-04-05 06:51:50 -07:00
var utxo = utxosIndex[i];
return !utxo || utxo.locked;
});
if (unavailable) return cb(Errors.UNAVAILABLE_UTXOS);
txp.status = 'pending';
self.storage.storeTx(self.walletId, txp, function(err) {
if (err) return cb(err);
self._notifyTxProposalAction('NewTxProposal', txp, 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);
}
2016-09-22 06:11:37 -07:00
self.storage.storeTxNote(note, function(err) {
if (err) return cb(err);
self.storage.fetchTxNote(self.walletId, opts.txid, cb);
});
2016-05-17 09:13:21 -07:00
});
});
};
/**
* 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._notifyTxProposalAction('TxProposalRemoved', txp, cb);
2015-04-30 16:31:45 -07:00
});
2015-02-10 11:30:58 -08:00
});
});
};
2017-08-25 12:04:14 -07:00
WalletService.prototype._broadcastRawTx = function(coin, network, raw, cb) {
var bc = this._getBlockchainExplorer(coin, network);
2017-09-08 09:06:13 -07:00
if (!bc) return cb(new Error('Could not get blockchain explorer instance'));
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
2017-08-25 12:04:14 -07:00
* @param {string} [opts.coin = 'btc'] - The coin for this transaction.
2015-08-13 12:06:22 -07:00
* @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
2017-08-25 12:04:14 -07:00
opts.coin = opts.coin || Defaults.COIN;
if (!Utils.checkValueInCollection(opts.coin, Constants.COINS))
return cb(new ClientError('Invalid coin'));
opts.network = opts.network || 'livenet';
if (!Utils.checkValueInCollection(opts.network, Constants.NETWORKS))
2015-08-13 12:06:22 -07:00
return cb(new ClientError('Invalid network'));
2017-08-25 12:04:14 -07:00
self._broadcastRawTx(opts.coin, opts.network, opts.rawTx, cb);
2015-08-13 12:06:22 -07:00
};
2015-05-28 08:51:41 -07:00
WalletService.prototype._checkTxInBlockchain = function(txp, cb) {
if (!txp.txid) return cb();
2017-08-25 12:04:14 -07:00
var bc = this._getBlockchainExplorer(txp.coin, txp.network);
2017-09-08 09:06:13 -07:00
if (!bc) return cb(new Error('Could not get blockchain explorer instance'));
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
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-08-04 07:35:40 -07:00
try {
if (!txp.sign(self.copayerId, opts.signatures, copayer.xPubKey)) {
log.warn('Error signing transaction (BAD_SIGNATURES)');
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);
return cb(Errors.BAD_SIGNATURES);
}
} catch (ex) {
log.error('Error signing transaction proposal', ex);
return cb(ex);
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) {
self._notifyTxProposalAction('TxProposalAcceptedBy', txp, {
2015-04-30 16:31:45 -07:00
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._notifyTxProposalAction('TxProposalFinallyAccepted', txp, 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 extraArgs = {
txid: txp.txid,
};
if (opts.byThirdParty) {
self._notifyTxProposalAction('NewOutgoingTxByThirdParty', txp, extraArgs);
} else {
self._notifyTxProposalAction('NewOutgoingTx', txp, extraArgs);
}
2016-07-29 06:39:01 -07:00
self.storage.softResetTxHistoryCache(self.walletId, function() {
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);
}
2017-08-25 12:04:14 -07:00
self._broadcastRawTx(wallet.coin, wallet.network, 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) {
self._notifyTxProposalAction('TxProposalRejectedBy', txp, {
2015-04-30 16:31:45 -07:00
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') {
2017-10-30 13:50:56 -07:00
var rejectedBy = _.map(_.filter(txp.actions, {
2015-06-08 14:26:33 -07:00
type: 'reject'
}), 'copayerId');
self._notifyTxProposalAction('TxProposalFinallyRejected', txp, {
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
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);
return _.map([].concat(txs), function(tx) {
2015-02-21 17:35:12 -08:00
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,
blockheight: tx.blockheight,
2015-02-21 17:35:12 -08:00
fees: parseInt((tx.fees * 1e8).toFixed(0)),
2017-04-12 06:49:28 -07:00
size: tx.size,
2015-10-28 13:46:36 -07:00
time: t,
2015-02-21 17:35:12 -08:00
inputs: inputs,
outputs: outputs,
};
});
};
2016-08-10 06:51:42 -07:00
WalletService._cachedBlockheight;
WalletService._initBlockchainHeightCache = function() {
if (WalletService._cachedBlockheight) return;
WalletService._cachedBlockheight = {
2017-08-25 12:04:14 -07:00
btc: {
livenet: {},
testnet: {}
},
bch: {
livenet: {},
testnet: {}
},
2016-08-10 06:51:42 -07:00
};
};
2017-08-25 12:04:14 -07:00
WalletService._clearBlockchainHeightCache = function(coin, network) {
2016-08-10 06:51:42 -07:00
WalletService._initBlockchainHeightCache();
2017-08-23 13:28:35 -07:00
if (!Utils.checkValueInCollection(network, Constants.NETWORKS)) {
2017-08-25 12:04:14 -07:00
log.error('Incorrect network in new block: ' + coin + '/' + network);
2016-08-10 07:14:21 -07:00
return;
}
2017-08-25 12:04:14 -07:00
WalletService._cachedBlockheight[coin][network].current = null;
2016-08-08 06:06:58 -07:00
};
2017-08-25 12:04:14 -07:00
WalletService.prototype._getBlockchainHeight = function(coin, network, cb) {
var self = this;
2016-08-08 06:06:58 -07:00
var now = Date.now();
2016-08-10 06:51:42 -07:00
WalletService._initBlockchainHeightCache();
2017-08-25 12:04:14 -07:00
var cache = WalletService._cachedBlockheight[coin][network];
2016-08-08 06:06:58 -07:00
function fetchFromBlockchain(cb) {
2017-08-25 12:04:14 -07:00
var bc = self._getBlockchainExplorer(coin, network);
2017-09-08 09:06:13 -07:00
if (!bc) return cb(new Error('Could not get blockchain explorer instance'));
2016-08-08 06:06:58 -07:00
bc.getBlockchainHeight(function(err, height) {
if (!err && height > 0) {
cache.current = height;
cache.last = height;
cache.updatedOn = now;
}
return cb(null, cache.last);
});
};
if (!cache.current || (now - cache.updatedOn) > Defaults.BLOCKHEIGHT_CACHE_TIME * 1000) {
return fetchFromBlockchain(cb);
}
return cb(null, cache.current);
};
2015-02-21 17:35:12 -08:00
/**
* 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
function decorate(wallet, 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 || wallet.singleAddress) : 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
2017-04-12 06:49:28 -07:00
if (_.isNumber(tx.size) && tx.size > 0) {
newTx.feePerKb = +(tx.fees * 1000 / tx.size).toFixed();
}
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) {
2016-10-11 07:43:39 -07:00
newTx.createdOn = proposal.createdOn;
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
});
};
2017-08-25 12:04:14 -07:00
function getNormalizedTxs(wallet, addresses, from, to, cb) {
var txs, fromCache, totalItems;
2016-07-29 04:52:36 -07:00
var useCache = addresses.length >= Defaults.HISTORY_CACHE_ADDRESS_THRESOLD;
2015-02-21 17:35:12 -08:00
async.series([
2016-07-29 04:52:36 -07:00
function(next) {
if (!useCache) return next();
self.storage.getTxHistoryCache(self.walletId, from, to, function(err, res) {
if (err) return next(err);
if (!res || !res[0]) return next();
txs = res;
fromCache = true;
2015-02-21 17:35:12 -08:00
return next()
});
},
2015-02-21 17:35:12 -08:00
function(next) {
if (txs) return next();
2017-10-30 13:50:56 -07:00
var addressStrs = _.map(addresses, 'address');
2017-08-25 12:04:14 -07:00
var bc = self._getBlockchainExplorer(wallet.coin, wallet.network);
2017-09-08 09:06:13 -07:00
if (!bc) return next(new Error('Could not get blockchain explorer instance'));
bc.getTransactions(addressStrs, from, to, function(err, rawTxs, total) {
if (err) return next(err);
2017-03-15 12:12:13 -07:00
txs = self._normalizeTxHistory(rawTxs);
totalItems = total;
return next();
});
2015-02-21 17:35:12 -08:00
},
function(next) {
if (!useCache || fromCache) return next();
2016-07-29 04:52:36 -07:00
var txsToCache = _.filter(txs, function(i) {
return i.confirmations >= Defaults.CONFIRMATIONS_TO_START_CACHING;
}).reverse();
2016-07-29 06:39:01 -07:00
if (!txsToCache.length) return next();
2016-07-29 04:52:36 -07:00
var fwdIndex = totalItems - to;
if (fwdIndex < 0) fwdIndex = 0;
self.storage.storeTxHistoryCache(self.walletId, totalItems, fwdIndex, txsToCache, next);
},
function(next) {
if (!useCache || !fromCache) return next();
if (!txs) return next();
// Fix tx confirmations for cached txs
2017-08-25 12:04:14 -07:00
self._getBlockchainHeight(wallet.coin, wallet.network, function(err, height) {
if (err || !height) return next(err);
_.each(txs, function(tx) {
if (tx.blockheight >= 0) {
2016-08-06 09:12:25 -07:00
tx.confirmations = height - tx.blockheight + 1;
}
});
next();
});
},
], function(err) {
if (err) return cb(err);
return cb(null, {
items: txs,
fromCache: fromCache
});
});
};
2016-07-29 04:52:36 -07:00
2017-04-13 08:50:00 -07:00
function tagLowFees(wallet, txs, cb) {
var unconfirmed = _.filter(txs, {
confirmations: 0
});
if (_.isEmpty(unconfirmed)) return cb();
self.getFeeLevels({
2017-08-25 12:04:14 -07:00
coin: wallet.coin,
2017-04-13 08:50:00 -07:00
network: wallet.network
}, function(err, levels) {
if (err) {
log.warn('Could not fetch fee levels', err);
} else {
var level = _.find(levels, {
level: 'superEconomy'
});
if (!level || !level.nbBlocks) {
log.debug('Cannot compute super economy fee level from blockchain');
} else {
var minFeePerKb = level.feePerKb;
_.each(unconfirmed, function(tx) {
tx.lowFees = tx.feePerKb < minFeePerKb;
});
}
}
return cb();
});
};
self.getWallet({}, function(err, wallet) {
2015-02-21 17:35:12 -08:00
if (err) return cb(err);
// 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 from = opts.skip || 0;
var to = from + opts.limit;
2015-02-21 17:35:12 -08:00
async.waterfall([
2015-02-21 17:35:12 -08:00
function(next) {
2017-08-25 12:04:14 -07:00
getNormalizedTxs(wallet, addresses, from, to, next);
},
function(txs, next) {
// Fetch all proposals in [t - 7 days, t + 1 day]
var minTs = _.min(txs.items, 'time').time - 7 * 24 * 3600;
var maxTs = _.max(txs.items, 'time').time + 1 * 24 * 3600;
async.parallel([
function(done) {
self.storage.fetchTxs(self.walletId, {
minTs: minTs,
maxTs: maxTs
}, done);
},
function(done) {
self.storage.fetchTxNotes(self.walletId, {
minTs: minTs
}, done);
},
], function(err, res) {
return next(err, {
txs: txs,
txps: res[0],
notes: res[1]
});
});
},
], function(err, res) {
if (err) return cb(err);
2015-02-21 17:35:12 -08:00
var finalTxs = decorate(wallet, res.txs.items, addresses, res.txps, res.notes);
2015-02-21 17:35:12 -08:00
2017-04-13 08:50:00 -07:00
tagLowFees(wallet, finalTxs, function(err) {
if (err)
log.warn('Failed to tag unconfirmed with low fee');
2017-04-12 06:49:28 -07:00
if (res.txs.fromCache)
log.debug("History from cache for:", self.walletId, from, to);
2015-02-21 17:35:12 -08:00
2017-04-12 06:49:28 -07:00
return cb(null, finalTxs, !!res.txs.fromCache);
});
});
2015-02-21 17:35:12 -08:00
});
});
};
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 || {};
2017-08-25 12:04:14 -07:00
function checkActivity(wallet, address, cb) {
var bc = self._getBlockchainExplorer(wallet.coin, wallet.network);
2017-09-08 09:06:13 -07:00
if (!bc) return cb(new Error('Could not get blockchain explorer instance'));
bc.getAddressActivity(address, cb);
2015-04-01 12:42:12 -07:00
};
2017-08-25 12:04:14 -07:00
function scanBranch(wallet, derivator, cb) {
var inactiveCounter = 0;
var allAddresses = [];
var gap = Defaults.SCAN_ADDRESS_GAP;
2015-04-01 12:42:12 -07:00
async.whilst(function() {
2017-11-01 12:56:32 -07:00
log.debug('Scanning addr gap:'+ inactiveCounter);
return inactiveCounter < gap;
2015-04-01 12:42:12 -07:00
}, function(next) {
var address = derivator.derive();
2017-08-25 12:04:14 -07:00
checkActivity(wallet, address.address, 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';
2015-04-01 12:42:12 -07:00
2016-07-29 06:39:01 -07:00
self.storage.clearTxHistoryCache(self.walletId, function() {
self.storage.storeWallet(wallet, function(err) {
if (err) return cb(err);
2015-04-15 06:57:10 -07:00
2016-07-29 06:39:01 -07:00
var derivators = [];
_.each([false, true], function(isChange) {
derivators.push({
derive: _.bind(wallet.createAddress, wallet, isChange),
rewind: _.bind(wallet.addressManager.rewindIndex, wallet.addressManager, isChange),
});
if (opts.includeCopayerBranches) {
_.each(wallet.copayers, function(copayer) {
if (copayer.addressManager) {
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
});
2016-07-29 06:39:01 -07:00
async.eachSeries(derivators, function(derivator, next) {
2017-08-25 12:04:14 -07:00
scanBranch(wallet, derivator, function(err, addresses) {
2016-07-29 06:39:01 -07:00
if (err) return next(err);
self.storage.storeAddressAndWallet(wallet, addresses, next);
});
2016-07-29 06:39:01 -07:00
}, 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() {
2017-11-01 12:15:35 -07:00
self.storage.storeTwoStepCache(self.walletId, {}, function(err) {
return cb(error);
});
2016-07-29 06:39:01 -07:00
});
})
});
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
};
/**
* Subscribe this copayer to the Push Notifications service using the specified token.
* @param {Object} opts
* @param {string} opts.token - The token representing the app/device.
* @param {string} [opts.packageName] - The restricted_package_name option associated with this token.
* @param {string} [opts.platform] - The platform associated with this token.
*/
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;
var sub = Model.PushNotificationSub.create({
copayerId: self.copayerId,
token: opts.token,
packageName: opts.packageName,
platform: opts.platform,
});
self.storage.storePushNotificationSub(sub, cb);
};
/**
* Unsubscribe this copayer to the Push Notifications service using the specified token.
* @param {Object} opts
* @param {string} opts.token - The token representing the app/device.
*/
WalletService.prototype.pushNotificationsUnsubscribe = function(opts, cb) {
if (!checkRequired(opts, ['token'], cb)) return;
var self = this;
self.storage.removePushNotificationSub(self.copayerId, opts.token, cb);
};
2015-04-02 07:18:39 -07:00
2017-05-15 12:09:52 -07:00
/**
* Subscribe this copayer to the specified tx to get a notification when the tx confirms.
* @param {Object} opts
* @param {string} opts.txid - The txid of the tx to be notified of.
*/
WalletService.prototype.txConfirmationSubscribe = function(opts, cb) {
if (!checkRequired(opts, ['txid'], cb)) return;
var self = this;
var sub = Model.TxConfirmationSub.create({
copayerId: self.copayerId,
2017-05-17 11:10:16 -07:00
walletId: self.walletId,
2017-05-15 12:09:52 -07:00
txid: opts.txid,
});
self.storage.storeTxConfirmationSub(sub, cb);
};
/**
* Unsubscribe this copayer to the Push Notifications service using the specified token.
* @param {Object} opts
* @param {string} opts.txid - The txid of the tx to be notified of.
*/
WalletService.prototype.txConfirmationUnsubscribe = function(opts, cb) {
if (!checkRequired(opts, ['txid'], cb)) return;
var self = this;
self.storage.removeTxConfirmationSub(self.copayerId, opts.txid, cb);
};
2015-02-20 12:32:19 -08:00
module.exports = WalletService;
2015-02-09 10:30:16 -08:00
module.exports.ClientError = ClientError;