|
|
@ -12,6 +12,10 @@ var EmailValidator = require('email-validator'); |
|
|
|
var Stringify = require('json-stable-stringify'); |
|
|
|
|
|
|
|
var Bitcore = require('bitcore-lib'); |
|
|
|
var Bitcore_ = { |
|
|
|
btc: Bitcore, |
|
|
|
bch: require('bitcore-lib-cash') |
|
|
|
}; |
|
|
|
|
|
|
|
var Common = require('./common'); |
|
|
|
var Utils = Common.Utils; |
|
|
@ -158,7 +162,7 @@ WalletService.handleIncomingNotification = function(notification, cb) { |
|
|
|
|
|
|
|
if (!notification || notification.type != 'NewBlock') return cb(); |
|
|
|
|
|
|
|
WalletService._clearBlockchainHeightCache(notification.data.network); |
|
|
|
WalletService._clearBlockchainHeightCache(notification.data.coin, notification.data.network); |
|
|
|
return cb(); |
|
|
|
}; |
|
|
|
|
|
|
@ -441,10 +445,19 @@ WalletService.prototype.getWalletFromIdentifier = function(opts, cb) { |
|
|
|
} |
|
|
|
|
|
|
|
// Is identifier a txid form an incomming tx?
|
|
|
|
async.detectSeries(_.values(Constants.NETWORKS), function(network, nextNetwork) { |
|
|
|
var bc = self._getBlockchainExplorer(network); |
|
|
|
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); |
|
|
|
bc.getTransaction(opts.identifier, function(err, tx) { |
|
|
|
if (err || !tx) return nextNetwork(err, false); |
|
|
|
if (err || !tx) return nextCoinNetwork(err, false); |
|
|
|
var outputs = _.first(self._normalizeTxHistory(tx)).outputs; |
|
|
|
var toAddresses = _.pluck(outputs, 'address'); |
|
|
|
async.detect(toAddresses, function(addressStr, nextAddress) { |
|
|
@ -454,7 +467,7 @@ WalletService.prototype.getWalletFromIdentifier = function(opts, cb) { |
|
|
|
nextAddress(null, true); |
|
|
|
}); |
|
|
|
}, function(err) { |
|
|
|
nextNetwork(err, !!walletId); |
|
|
|
nextCoinNetwork(err, !!walletId); |
|
|
|
}); |
|
|
|
}); |
|
|
|
}, function(err) { |
|
|
@ -915,7 +928,7 @@ WalletService.prototype._canCreateAddress = function(ignoreMaxGap, cb) { |
|
|
|
hasActivity: true |
|
|
|
})) return cb(null, true); |
|
|
|
|
|
|
|
var bc = self._getBlockchainExplorer(latestAddresses[0].network); |
|
|
|
var bc = self._getBlockchainExplorer(latestAddresses[0].coin, latestAddresses[0].network); |
|
|
|
var activityFound = false; |
|
|
|
var i = latestAddresses.length; |
|
|
|
async.whilst(function() { |
|
|
@ -1035,27 +1048,28 @@ WalletService.prototype.verifyMessageSignature = function(opts, cb) { |
|
|
|
}; |
|
|
|
|
|
|
|
|
|
|
|
WalletService.prototype._getBlockchainExplorer = function(network) { |
|
|
|
WalletService.prototype._getBlockchainExplorer = function(coin, network) { |
|
|
|
var opts = {}; |
|
|
|
|
|
|
|
if (this.blockchainExplorer) return this.blockchainExplorer; |
|
|
|
if (this.blockchainExplorerOpts && this.blockchainExplorerOpts[network]) { |
|
|
|
opts = this.blockchainExplorerOpts[network]; |
|
|
|
if (this.blockchainExplorerOpts && this.blockchainExplorerOpts[coin] && this.blockchainExplorerOpts[coin][network]) { |
|
|
|
opts = this.blockchainExplorerOpts[coin][network]; |
|
|
|
} |
|
|
|
// TODO: provider should be configurable
|
|
|
|
opts.provider = 'insight'; |
|
|
|
opts.coin = coin; |
|
|
|
opts.network = network; |
|
|
|
opts.userAgent = WalletService.getServiceVersion(); |
|
|
|
return new BlockchainExplorer(opts); |
|
|
|
}; |
|
|
|
|
|
|
|
WalletService.prototype._getUtxos = function(addresses, cb) { |
|
|
|
WalletService.prototype._getUtxos = function(coin, addresses, cb) { |
|
|
|
var self = this; |
|
|
|
|
|
|
|
if (addresses.length == 0) return cb(null, []); |
|
|
|
var networkName = Bitcore.Address(addresses[0]).toObject().network; |
|
|
|
|
|
|
|
var bc = self._getBlockchainExplorer(networkName); |
|
|
|
var bc = self._getBlockchainExplorer(coin, networkName); |
|
|
|
bc.getUtxos(addresses, function(err, utxos) { |
|
|
|
if (err) return cb(err); |
|
|
|
|
|
|
@ -1079,10 +1093,16 @@ WalletService.prototype._getUtxosForCurrentWallet = function(addresses, cb) { |
|
|
|
return utxo.txid + '|' + utxo.vout |
|
|
|
}; |
|
|
|
|
|
|
|
var allAddresses, allUtxos, utxoIndex; |
|
|
|
var coin, allAddresses, allUtxos, utxoIndex; |
|
|
|
|
|
|
|
async.series([ |
|
|
|
|
|
|
|
function(next) { |
|
|
|
self.getWallet({}, function(err, wallet) { |
|
|
|
coin = wallet.coin; |
|
|
|
return next(); |
|
|
|
}); |
|
|
|
}, |
|
|
|
function(next) { |
|
|
|
if (_.isArray(addresses)) { |
|
|
|
allAddresses = addresses; |
|
|
@ -1097,7 +1117,7 @@ WalletService.prototype._getUtxosForCurrentWallet = function(addresses, cb) { |
|
|
|
if (allAddresses.length == 0) return cb(null, []); |
|
|
|
|
|
|
|
var addressStrs = _.pluck(allAddresses, 'address'); |
|
|
|
self._getUtxos(addressStrs, function(err, utxos) { |
|
|
|
self._getUtxos(coin, addressStrs, function(err, utxos) { |
|
|
|
if (err) return next(err); |
|
|
|
|
|
|
|
if (utxos.length == 0) return cb(null, []); |
|
|
@ -1159,6 +1179,7 @@ WalletService.prototype._getUtxosForCurrentWallet = function(addresses, cb) { |
|
|
|
/** |
|
|
|
* Returns list of UTXOs |
|
|
|
* @param {Object} opts |
|
|
|
* @param {String} [opts.coin='btc'] (optional) |
|
|
|
* @param {Array} opts.addresses (optional) - List of addresses from where to fetch UTXOs. |
|
|
|
* @returns {Array} utxos - List of UTXOs. |
|
|
|
*/ |
|
|
@ -1167,10 +1188,14 @@ WalletService.prototype.getUtxos = function(opts, cb) { |
|
|
|
|
|
|
|
opts = opts || {}; |
|
|
|
|
|
|
|
opts.coin = opts.coin || Defaults.COIN; |
|
|
|
if (!Utils.checkValueInCollection(opts.coin, Constants.COINS)) |
|
|
|
return cb(new ClientError('Invalid coin')); |
|
|
|
|
|
|
|
if (_.isUndefined(opts.addresses)) { |
|
|
|
self._getUtxosForCurrentWallet(null, cb); |
|
|
|
} else { |
|
|
|
self._getUtxos(opts.addresses, cb); |
|
|
|
self._getUtxos(opts.coin, opts.addresses, cb); |
|
|
|
} |
|
|
|
}; |
|
|
|
|
|
|
@ -1392,6 +1417,7 @@ WalletService.prototype.getSendMaxInfo = function(opts, cb) { |
|
|
|
|
|
|
|
var txp = Model.TxProposal.create({ |
|
|
|
walletId: self.walletId, |
|
|
|
coin: wallet.coin, |
|
|
|
network: wallet.network, |
|
|
|
walletM: wallet.m, |
|
|
|
walletN: wallet.n, |
|
|
@ -1442,10 +1468,10 @@ WalletService.prototype.getSendMaxInfo = function(opts, cb) { |
|
|
|
}); |
|
|
|
}; |
|
|
|
|
|
|
|
WalletService.prototype._sampleFeeLevels = function(network, points, cb) { |
|
|
|
WalletService.prototype._sampleFeeLevels = function(coin, network, points, cb) { |
|
|
|
var self = this; |
|
|
|
|
|
|
|
var bc = self._getBlockchainExplorer(network); |
|
|
|
var bc = self._getBlockchainExplorer(coin, network); |
|
|
|
bc.estimateFee(points, function(err, result) { |
|
|
|
if (err) { |
|
|
|
log.error('Error estimating fee', err); |
|
|
@ -1473,6 +1499,7 @@ WalletService.prototype._sampleFeeLevels = function(network, points, cb) { |
|
|
|
/** |
|
|
|
* Returns fee levels for the current state of the network. |
|
|
|
* @param {Object} opts |
|
|
|
* @param {string} [opts.coin = 'btc'] - The coin to estimate fee levels from. |
|
|
|
* @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. |
|
|
|
*/ |
|
|
@ -1509,11 +1536,15 @@ WalletService.prototype.getFeeLevels = function(opts, cb) { |
|
|
|
return result; |
|
|
|
}; |
|
|
|
|
|
|
|
var network = opts.network || 'livenet'; |
|
|
|
if (network != 'livenet' && network != 'testnet') |
|
|
|
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')); |
|
|
|
|
|
|
|
self._sampleFeeLevels(network, samplePoints(), function(err, feeSamples) { |
|
|
|
self._sampleFeeLevels(opts.coin, opts.network, samplePoints(), function(err, feeSamples) { |
|
|
|
var values = _.map(Defaults.FEE_LEVELS, function(level) { |
|
|
|
var result = { |
|
|
|
level: level.name, |
|
|
@ -1565,10 +1596,10 @@ WalletService.prototype._checkTx = function(txp) { |
|
|
|
return ex; |
|
|
|
} |
|
|
|
|
|
|
|
if (bitcoreError instanceof Bitcore.errors.Transaction.FeeError) |
|
|
|
if (bitcoreError instanceof Bitcore_[txp.coin].errors.Transaction.FeeError) |
|
|
|
return Errors.INSUFFICIENT_FUNDS_FOR_FEE; |
|
|
|
|
|
|
|
if (bitcoreError instanceof Bitcore.errors.Transaction.DustOutputs) |
|
|
|
if (bitcoreError instanceof Bitcore_[txp.coin].errors.Transaction.DustOutputs) |
|
|
|
return Errors.DUST_AMOUNT; |
|
|
|
return bitcoreError; |
|
|
|
}; |
|
|
@ -1697,7 +1728,7 @@ WalletService.prototype._selectTxInputs = function(txp, utxosToExclude, cb) { |
|
|
|
var changeAmount = Math.round(total - txpAmount - fee); |
|
|
|
log.debug('Tx change: ', Utils.formatAmountInBtc(changeAmount)); |
|
|
|
|
|
|
|
var dustThreshold = Math.max(Defaults.MIN_OUTPUT_AMOUNT, Bitcore.Transaction.DUST_AMOUNT); |
|
|
|
var dustThreshold = Math.max(Defaults.MIN_OUTPUT_AMOUNT, Bitcore_[txp.coin].Transaction.DUST_AMOUNT); |
|
|
|
if (changeAmount > 0 && changeAmount <= dustThreshold) { |
|
|
|
log.debug('Change below dust threshold (' + Utils.formatAmountInBtc(dustThreshold) + '). Incrementing fee to remove change.'); |
|
|
|
// Remove dust change by incrementing fee
|
|
|
@ -1850,7 +1881,7 @@ WalletService.prototype._canCreateTx = function(cb) { |
|
|
|
}; |
|
|
|
|
|
|
|
WalletService.prototype._validateOutputs = function(opts, wallet, cb) { |
|
|
|
var dustThreshold = Math.max(Defaults.MIN_OUTPUT_AMOUNT, Bitcore.Transaction.DUST_AMOUNT); |
|
|
|
var dustThreshold = Math.max(Defaults.MIN_OUTPUT_AMOUNT, Bitcore_[wallet.coin].Transaction.DUST_AMOUNT); |
|
|
|
|
|
|
|
if (_.isEmpty(opts.outputs)) return new ClientError('No outputs were specified'); |
|
|
|
|
|
|
@ -1868,7 +1899,7 @@ WalletService.prototype._validateOutputs = function(opts, wallet, cb) { |
|
|
|
} catch (ex) { |
|
|
|
return Errors.INVALID_ADDRESS; |
|
|
|
} |
|
|
|
if (toAddress.network != wallet.getNetworkName()) { |
|
|
|
if (toAddress.network != wallet.network) { |
|
|
|
return Errors.INCORRECT_ADDRESS_NETWORK; |
|
|
|
} |
|
|
|
|
|
|
@ -1957,6 +1988,7 @@ WalletService.prototype._getFeePerKb = function(wallet, opts, cb) { |
|
|
|
|
|
|
|
if (_.isNumber(opts.feePerKb)) return cb(null, opts.feePerKb); |
|
|
|
self.getFeeLevels({ |
|
|
|
coin: wallet.coin, |
|
|
|
network: wallet.network |
|
|
|
}, function(err, levels) { |
|
|
|
if (err) return cb(err); |
|
|
@ -2066,6 +2098,8 @@ WalletService.prototype.createTx = function(opts, cb) { |
|
|
|
id: opts.txProposalId, |
|
|
|
walletId: self.walletId, |
|
|
|
creatorId: self.copayerId, |
|
|
|
coin: wallet.coin, |
|
|
|
network: wallet.network, |
|
|
|
outputs: opts.outputs, |
|
|
|
message: opts.message, |
|
|
|
changeAddress: changeAddress, |
|
|
@ -2330,8 +2364,8 @@ WalletService.prototype.removePendingTx = function(opts, cb) { |
|
|
|
}); |
|
|
|
}; |
|
|
|
|
|
|
|
WalletService.prototype._broadcastRawTx = function(network, raw, cb) { |
|
|
|
var bc = this._getBlockchainExplorer(network); |
|
|
|
WalletService.prototype._broadcastRawTx = function(coin, network, raw, cb) { |
|
|
|
var bc = this._getBlockchainExplorer(coin, network); |
|
|
|
bc.broadcast(raw, function(err, txid) { |
|
|
|
if (err) return cb(err); |
|
|
|
return cb(null, txid); |
|
|
@ -2341,6 +2375,7 @@ WalletService.prototype._broadcastRawTx = function(network, raw, cb) { |
|
|
|
/** |
|
|
|
* Broadcast a raw transaction. |
|
|
|
* @param {Object} opts |
|
|
|
* @param {string} [opts.coin = 'btc'] - The coin for this transaction. |
|
|
|
* @param {string} [opts.network = 'livenet'] - The Bitcoin network for this transaction. |
|
|
|
* @param {string} opts.rawTx - Raw tx data. |
|
|
|
*/ |
|
|
@ -2349,17 +2384,21 @@ WalletService.prototype.broadcastRawTx = function(opts, cb) { |
|
|
|
|
|
|
|
if (!checkRequired(opts, ['network', 'rawTx'], cb)) return; |
|
|
|
|
|
|
|
var network = opts.network || 'livenet'; |
|
|
|
if (network != 'livenet' && network != 'testnet') |
|
|
|
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')); |
|
|
|
|
|
|
|
self._broadcastRawTx(network, opts.rawTx, cb); |
|
|
|
self._broadcastRawTx(opts.coin, opts.network, opts.rawTx, cb); |
|
|
|
}; |
|
|
|
|
|
|
|
|
|
|
|
WalletService.prototype._checkTxInBlockchain = function(txp, cb) { |
|
|
|
if (!txp.txid) return cb(); |
|
|
|
var bc = this._getBlockchainExplorer(txp.getNetworkName()); |
|
|
|
var bc = this._getBlockchainExplorer(txp.coin, txp.network); |
|
|
|
bc.getTransaction(txp.txid, function(err, tx) { |
|
|
|
if (err) return cb(err); |
|
|
|
return cb(null, !!tx); |
|
|
@ -2487,7 +2526,7 @@ WalletService.prototype.broadcastTx = function(opts, cb) { |
|
|
|
} catch (ex) { |
|
|
|
return cb(ex); |
|
|
|
} |
|
|
|
self._broadcastRawTx(txp.getNetworkName(), raw, function(err, txid) { |
|
|
|
self._broadcastRawTx(wallet.coin, wallet.network, raw, function(err, txid) { |
|
|
|
if (err) { |
|
|
|
var broadcastErr = err; |
|
|
|
// Check if tx already in blockchain
|
|
|
@ -2693,29 +2732,35 @@ WalletService._cachedBlockheight; |
|
|
|
WalletService._initBlockchainHeightCache = function() { |
|
|
|
if (WalletService._cachedBlockheight) return; |
|
|
|
WalletService._cachedBlockheight = { |
|
|
|
livenet: {}, |
|
|
|
testnet: {} |
|
|
|
btc: { |
|
|
|
livenet: {}, |
|
|
|
testnet: {} |
|
|
|
}, |
|
|
|
bch: { |
|
|
|
livenet: {}, |
|
|
|
testnet: {} |
|
|
|
}, |
|
|
|
}; |
|
|
|
}; |
|
|
|
|
|
|
|
WalletService._clearBlockchainHeightCache = function(network) { |
|
|
|
WalletService._clearBlockchainHeightCache = function(coin, network) { |
|
|
|
WalletService._initBlockchainHeightCache(); |
|
|
|
if (!Utils.checkValueInCollection(network, Constants.NETWORKS)) { |
|
|
|
log.error('Incorrect network in new block: ' + network); |
|
|
|
log.error('Incorrect network in new block: ' + coin + '/' + network); |
|
|
|
return; |
|
|
|
} |
|
|
|
WalletService._cachedBlockheight[network].current = null; |
|
|
|
WalletService._cachedBlockheight[coin][network].current = null; |
|
|
|
}; |
|
|
|
|
|
|
|
WalletService.prototype._getBlockchainHeight = function(network, cb) { |
|
|
|
WalletService.prototype._getBlockchainHeight = function(coin, network, cb) { |
|
|
|
var self = this; |
|
|
|
|
|
|
|
var now = Date.now(); |
|
|
|
WalletService._initBlockchainHeightCache(); |
|
|
|
var cache = WalletService._cachedBlockheight[network]; |
|
|
|
var cache = WalletService._cachedBlockheight[coin][network]; |
|
|
|
|
|
|
|
function fetchFromBlockchain(cb) { |
|
|
|
var bc = self._getBlockchainExplorer(network); |
|
|
|
var bc = self._getBlockchainExplorer(coin, network); |
|
|
|
bc.getBlockchainHeight(function(err, height) { |
|
|
|
if (!err && height > 0) { |
|
|
|
cache.current = height; |
|
|
@ -2883,10 +2928,9 @@ WalletService.prototype.getTxHistory = function(opts, cb) { |
|
|
|
}); |
|
|
|
}; |
|
|
|
|
|
|
|
function getNormalizedTxs(addresses, from, to, cb) { |
|
|
|
function getNormalizedTxs(wallet, addresses, from, to, cb) { |
|
|
|
var txs, fromCache, totalItems; |
|
|
|
var useCache = addresses.length >= Defaults.HISTORY_CACHE_ADDRESS_THRESOLD; |
|
|
|
var network = Bitcore.Address(addresses[0].address).toObject().network; |
|
|
|
|
|
|
|
async.series([ |
|
|
|
|
|
|
@ -2907,7 +2951,7 @@ WalletService.prototype.getTxHistory = function(opts, cb) { |
|
|
|
if (txs) return next(); |
|
|
|
|
|
|
|
var addressStrs = _.pluck(addresses, 'address'); |
|
|
|
var bc = self._getBlockchainExplorer(network); |
|
|
|
var bc = self._getBlockchainExplorer(wallet.coin, wallet.network); |
|
|
|
bc.getTransactions(addressStrs, from, to, function(err, rawTxs, total) { |
|
|
|
if (err) return next(err); |
|
|
|
|
|
|
@ -2934,7 +2978,7 @@ WalletService.prototype.getTxHistory = function(opts, cb) { |
|
|
|
if (!txs) return next(); |
|
|
|
|
|
|
|
// Fix tx confirmations for cached txs
|
|
|
|
self._getBlockchainHeight(network, function(err, height) { |
|
|
|
self._getBlockchainHeight(wallet.coin, wallet.network, function(err, height) { |
|
|
|
if (err || !height) return next(err); |
|
|
|
_.each(txs, function(tx) { |
|
|
|
if (tx.blockheight >= 0) { |
|
|
@ -2961,6 +3005,7 @@ WalletService.prototype.getTxHistory = function(opts, cb) { |
|
|
|
if (_.isEmpty(unconfirmed)) return cb(); |
|
|
|
|
|
|
|
self.getFeeLevels({ |
|
|
|
coin: wallet.coin, |
|
|
|
network: wallet.network |
|
|
|
}, function(err, levels) { |
|
|
|
if (err) { |
|
|
@ -2997,7 +3042,7 @@ WalletService.prototype.getTxHistory = function(opts, cb) { |
|
|
|
async.waterfall([ |
|
|
|
|
|
|
|
function(next) { |
|
|
|
getNormalizedTxs(addresses, from, to, next); |
|
|
|
getNormalizedTxs(wallet, addresses, from, to, next); |
|
|
|
}, |
|
|
|
function(txs, next) { |
|
|
|
// Fetch all proposals in [t - 7 days, t + 1 day]
|
|
|
@ -3056,12 +3101,12 @@ WalletService.prototype.scan = function(opts, cb) { |
|
|
|
|
|
|
|
opts = opts || {}; |
|
|
|
|
|
|
|
function checkActivity(address, network, cb) { |
|
|
|
var bc = self._getBlockchainExplorer(network); |
|
|
|
function checkActivity(wallet, address, cb) { |
|
|
|
var bc = self._getBlockchainExplorer(wallet.coin, wallet.network); |
|
|
|
bc.getAddressActivity(address, cb); |
|
|
|
}; |
|
|
|
|
|
|
|
function scanBranch(derivator, cb) { |
|
|
|
function scanBranch(wallet, derivator, cb) { |
|
|
|
var inactiveCounter = 0; |
|
|
|
var allAddresses = []; |
|
|
|
var gap = Defaults.SCAN_ADDRESS_GAP; |
|
|
@ -3070,7 +3115,7 @@ WalletService.prototype.scan = function(opts, cb) { |
|
|
|
return inactiveCounter < gap; |
|
|
|
}, function(next) { |
|
|
|
var address = derivator.derive(); |
|
|
|
checkActivity(address.address, address.network, function(err, activity) { |
|
|
|
checkActivity(wallet, address.address, function(err, activity) { |
|
|
|
if (err) return next(err); |
|
|
|
|
|
|
|
allAddresses.push(address); |
|
|
@ -3114,7 +3159,7 @@ WalletService.prototype.scan = function(opts, cb) { |
|
|
|
}); |
|
|
|
|
|
|
|
async.eachSeries(derivators, function(derivator, next) { |
|
|
|
scanBranch(derivator, function(err, addresses) { |
|
|
|
scanBranch(wallet, derivator, function(err, addresses) { |
|
|
|
if (err) return next(err); |
|
|
|
self.storage.storeAddressAndWallet(wallet, addresses, next); |
|
|
|
}); |
|
|
|