|
|
@ -13,7 +13,9 @@ var PublicKey = Bitcore.PublicKey; |
|
|
|
var HDPublicKey = Bitcore.HDPublicKey; |
|
|
|
var Address = Bitcore.Address; |
|
|
|
|
|
|
|
var ClientError = require('./clienterror'); |
|
|
|
var ClientError = require('./errors/clienterror'); |
|
|
|
var Errors = require('./errors/errordefinitions'); |
|
|
|
|
|
|
|
var Utils = require('./utils'); |
|
|
|
var Lock = require('./lock'); |
|
|
|
var Storage = require('./storage'); |
|
|
@ -159,11 +161,11 @@ WalletService.getInstanceWithAuth = function(opts, cb) { |
|
|
|
var server = new WalletService(); |
|
|
|
server.storage.fetchCopayerLookup(opts.copayerId, function(err, copayer) { |
|
|
|
if (err) return cb(err); |
|
|
|
if (!copayer) return cb(new ClientError('NOTAUTHORIZED', 'Copayer not found')); |
|
|
|
if (!copayer) return cb(new ClientError(Errors.codes.NOTAUTHORIZED, 'Copayer not found')); |
|
|
|
|
|
|
|
var isValid = server._verifySignature(opts.message, opts.signature, copayer.requestPubKey); |
|
|
|
if (!isValid) |
|
|
|
return cb(new ClientError('NOTAUTHORIZED', 'Invalid signature')); |
|
|
|
return cb(new ClientError(Errors.codes.NOTAUTHORIZED, 'Invalid signature')); |
|
|
|
|
|
|
|
server.copayerId = opts.copayerId; |
|
|
|
server.walletId = copayer.walletId; |
|
|
@ -217,7 +219,7 @@ WalletService.prototype.createWallet = function(opts, cb) { |
|
|
|
return acb(); |
|
|
|
|
|
|
|
self.storage.fetchWallet(opts.id, function(err, wallet) { |
|
|
|
if (wallet) return acb(new ClientError('WEXISTS', 'Wallet already exists')); |
|
|
|
if (wallet) return acb(Errors.WEXISTS); |
|
|
|
return acb(err); |
|
|
|
}); |
|
|
|
}, |
|
|
@ -295,10 +297,10 @@ WalletService.prototype.replaceTemporaryRequestKey = function(opts, cb) { |
|
|
|
$.checkState(oldCopayerData); |
|
|
|
|
|
|
|
if (oldCopayerData.xPubKey !== opts.xPubKey || !oldCopayerData.isTemporaryRequestKey) |
|
|
|
return cb(new ClientError('CDATAMISMATCH', 'Copayer data mismatch')); |
|
|
|
return cb(Errors.CDATAMISMATCH); |
|
|
|
|
|
|
|
if (wallet.copayers.length != wallet.n) |
|
|
|
return cb(new ClientError('WNOTFULL', 'Replace only works on full wallets')); |
|
|
|
return cb(Errors.WNOTFULL); |
|
|
|
|
|
|
|
wallet.updateCopayerRequestKey(self.copayerId, opts.requestPubKey, opts.copayerSignature); |
|
|
|
|
|
|
@ -404,10 +406,9 @@ WalletService.prototype.joinWallet = function(opts, cb) { |
|
|
|
|
|
|
|
if (_.find(wallet.copayers, { |
|
|
|
xPubKey: opts.xPubKey |
|
|
|
})) return cb(new ClientError('CINWALLET', 'Copayer already in wallet')); |
|
|
|
})) return cb(Errors.CINWALLET); |
|
|
|
|
|
|
|
if (wallet.copayers.length == wallet.n) |
|
|
|
return cb(new ClientError('WFULL', 'Wallet full')); |
|
|
|
if (wallet.copayers.length == wallet.n) return cb(Errors.WFULL); |
|
|
|
|
|
|
|
var copayer = Model.Copayer.create({ |
|
|
|
name: opts.name, |
|
|
@ -420,14 +421,12 @@ WalletService.prototype.joinWallet = function(opts, cb) { |
|
|
|
|
|
|
|
self.storage.fetchCopayerLookup(copayer.id, function(err, res) { |
|
|
|
if (err) return cb(err); |
|
|
|
if (res) |
|
|
|
return cb(new ClientError('CREGISTERED', 'Copayer ID already registered on server')); |
|
|
|
if (res) return cb(Errors.CREGISTERED); |
|
|
|
|
|
|
|
wallet.addCopayer(copayer); |
|
|
|
self.storage.storeWalletAndUpdateCopayersLookup(wallet, function(err) { |
|
|
|
if (err) return cb(err); |
|
|
|
|
|
|
|
|
|
|
|
async.series([ |
|
|
|
|
|
|
|
function(next) { |
|
|
@ -643,7 +642,7 @@ WalletService.prototype.getUtxos = function(cb) { |
|
|
|
bc.getUnspentUtxos(addressStrs, function(err, inutxos) { |
|
|
|
if (err) { |
|
|
|
log.error('Could not fetch unspent outputs', err); |
|
|
|
return cb(new ClientError('BLOCKCHAINERROR', 'Could not fetch unspent outputs')); |
|
|
|
return cb(new ClientError(Errors.codes.BLOCKCHAINERROR, 'Could not fetch unspent outputs')); |
|
|
|
} |
|
|
|
var utxos = _.map(inutxos, function(utxo) { |
|
|
|
var u = _.pick(utxo, ['txid', 'vout', 'address', 'scriptPubKey', 'amount', 'satoshis', 'confirmations']); |
|
|
@ -848,10 +847,8 @@ WalletService.prototype._selectTxInputs = function(txp, cb) { |
|
|
|
availableAmount = balance.availableAmount; |
|
|
|
} |
|
|
|
|
|
|
|
if (totalAmount < txp.getTotalAmount()) |
|
|
|
return cb(new ClientError('INSUFFICIENTFUNDS', 'Insufficient funds')); |
|
|
|
if (availableAmount < txp.amount) |
|
|
|
return cb(new ClientError('LOCKEDFUNDS', 'Funds are locked by pending transaction proposals')); |
|
|
|
if (totalAmount < txp.getTotalAmount()) return cb(Errors.INSUFFICIENTFUNDS); |
|
|
|
if (availableAmount < txp.amount) return cb(Errors.LOCKEDFUNDS); |
|
|
|
|
|
|
|
// Prepare UTXOs list
|
|
|
|
utxos = _.reject(utxos, 'locked'); |
|
|
@ -895,12 +892,11 @@ WalletService.prototype._selectTxInputs = function(txp, cb) { |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
if (bitcoreError instanceof Bitcore.errors.Transaction.FeeError) { |
|
|
|
return cb(new ClientError('INSUFFICIENTFUNDSFORFEE', 'Insufficient funds for fee')); |
|
|
|
} |
|
|
|
if (bitcoreError instanceof Bitcore.errors.Transaction.DustOutputs) { |
|
|
|
return cb(new ClientError('DUSTAMOUNT', 'Amount below dust threshold')); |
|
|
|
} |
|
|
|
if (bitcoreError instanceof Bitcore.errors.Transaction.FeeError) |
|
|
|
return cb(Errors.INSUFFICIENTFUNDSFORFEE); |
|
|
|
|
|
|
|
if (bitcoreError instanceof Bitcore.errors.Transaction.DustOutputs) |
|
|
|
return cb(Errors.DUSTAMOUNT); |
|
|
|
|
|
|
|
return cb(bitcoreError || new Error('Could not select tx inputs')); |
|
|
|
}); |
|
|
@ -1007,8 +1003,7 @@ WalletService.prototype.createTx = function(opts, cb) { |
|
|
|
|
|
|
|
self._canCreateTx(self.copayerId, function(err, canCreate) { |
|
|
|
if (err) return cb(err); |
|
|
|
if (!canCreate) |
|
|
|
return cb(new ClientError('NOTALLOWEDTOCREATETX', 'Cannot create TX proposal during backoff time')); |
|
|
|
if (!canCreate) return cb(Errors.NOTALLOWEDTOCREATETX); |
|
|
|
|
|
|
|
_.each(opts.outputs, function(output) { |
|
|
|
output.valid = false; |
|
|
@ -1016,11 +1011,11 @@ WalletService.prototype.createTx = function(opts, cb) { |
|
|
|
try { |
|
|
|
toAddress = new Bitcore.Address(output.toAddress); |
|
|
|
} catch (ex) { |
|
|
|
cb(new ClientError('INVALIDADDRESS', 'Invalid address')); |
|
|
|
cb(new ClientError(Errors.codes.INVALIDADDRESS, 'Invalid address')); |
|
|
|
return false; |
|
|
|
} |
|
|
|
if (toAddress.network != wallet.getNetworkName()) { |
|
|
|
cb(new ClientError('INVALIDADDRESS', 'Incorrect address network')); |
|
|
|
cb(new ClientError(Errors.codes.INVALIDADDRESS, 'Incorrect address network')); |
|
|
|
return false; |
|
|
|
} |
|
|
|
if (!_.isNumber(output.amount) || _.isNaN(output.amount) || output.amount <= 0) { |
|
|
@ -1028,7 +1023,7 @@ WalletService.prototype.createTx = function(opts, cb) { |
|
|
|
return false; |
|
|
|
} |
|
|
|
if (output.amount < Bitcore.Transaction.DUST_AMOUNT) { |
|
|
|
cb(new ClientError('DUSTAMOUNT', 'Amount below dust threshold')); |
|
|
|
cb(Errors.DUSTAMOUNT); |
|
|
|
return false; |
|
|
|
} |
|
|
|
output.valid = true; |
|
|
@ -1155,14 +1150,11 @@ WalletService.prototype.removePendingTx = function(opts, cb) { |
|
|
|
}, function(err, txp) { |
|
|
|
if (err) return cb(err); |
|
|
|
|
|
|
|
if (!txp.isPending()) |
|
|
|
return cb(new ClientError('TXNOTPENDING', 'Transaction proposal not pending')); |
|
|
|
|
|
|
|
if (!txp.isPending()) return cb(Errors.TXNOTPENDING); |
|
|
|
|
|
|
|
var deleteLockTime = self.getRemainingDeleteLockTime(txp); |
|
|
|
if (deleteLockTime > 0) { |
|
|
|
return cb(new ClientError('TXCANNOTREMOVE', 'Cannot remove this tx proposal during locktime')); |
|
|
|
} |
|
|
|
if (deleteLockTime > 0) return cb(Errors.TXCANNOTREMOVE); |
|
|
|
|
|
|
|
self.storage.removeTx(self.walletId, txp.id, function() { |
|
|
|
self._notify('TxProposalRemoved', {}, cb); |
|
|
|
}); |
|
|
@ -1182,7 +1174,7 @@ WalletService.prototype._broadcastTx = function(txp, cb) { |
|
|
|
bc.broadcast(raw, function(err, txid) { |
|
|
|
if (err) { |
|
|
|
log.error('Could not broadcast transaction', err); |
|
|
|
return cb(new ClientError('BLOCKCHAINERROR', 'Could not broadcast transaction')); |
|
|
|
return cb(new ClientError(Errors.codes.BLOCKCHAINERROR, 'Could not broadcast transaction')); |
|
|
|
} |
|
|
|
return cb(null, txid); |
|
|
|
}) |
|
|
@ -1194,7 +1186,7 @@ WalletService.prototype._checkTxInBlockchain = function(txp, cb) { |
|
|
|
bc.getTransaction(tx.id, function(err, tx) { |
|
|
|
if (err) { |
|
|
|
log.error('Could not get transaction info', err); |
|
|
|
return cb(new ClientError('BLOCKCHAINERROR', 'Could not get transaction info')); |
|
|
|
return cb(new ClientError(Errors.codes.BLOCKCHAINERROR, 'Could not get transaction info')); |
|
|
|
} |
|
|
|
return cb(null, tx); |
|
|
|
}) |
|
|
@ -1223,15 +1215,13 @@ WalletService.prototype.signTx = function(opts, cb) { |
|
|
|
var action = _.find(txp.actions, { |
|
|
|
copayerId: self.copayerId |
|
|
|
}); |
|
|
|
if (action) |
|
|
|
return cb(new ClientError('CVOTED', 'Copayer already voted on this transaction proposal')); |
|
|
|
if (!txp.isPending()) |
|
|
|
return cb(new ClientError('TXNOTPENDING', 'The transaction proposal is not pending')); |
|
|
|
if (action) return cb(Errors.CVOTED); |
|
|
|
if (!txp.isPending()) return cb(Errors.TXNOTPENDING); |
|
|
|
|
|
|
|
var copayer = wallet.getCopayer(self.copayerId); |
|
|
|
|
|
|
|
if (!txp.sign(self.copayerId, opts.signatures, copayer.xPubKey)) |
|
|
|
return cb(new ClientError('BADSIGNATURES', 'Bad signatures')); |
|
|
|
return cb(Errors.BADSIGNATURES); |
|
|
|
|
|
|
|
self.storage.storeTx(self.walletId, txp, function(err) { |
|
|
|
if (err) return cb(err); |
|
|
@ -1296,11 +1286,8 @@ WalletService.prototype.broadcastTx = function(opts, cb) { |
|
|
|
}, function(err, txp) { |
|
|
|
if (err) return cb(err); |
|
|
|
|
|
|
|
if (txp.status == 'broadcasted') |
|
|
|
return cb(new ClientError('TXALREADYBROADCASTED', 'The transaction proposal is already broadcasted')); |
|
|
|
|
|
|
|
if (txp.status != 'accepted') |
|
|
|
return cb(new ClientError('TXNOTACCEPTED', 'The transaction proposal is not accepted')); |
|
|
|
if (txp.status == 'broadcasted') return cb(Errors.TXALREADYBROADCASTED); |
|
|
|
if (txp.status != 'accepted') return cb(Errors.TXNOTACCEPTED); |
|
|
|
|
|
|
|
self._broadcastTx(txp, function(err, txid) { |
|
|
|
if (err) { |
|
|
@ -1340,11 +1327,9 @@ WalletService.prototype.rejectTx = function(opts, cb) { |
|
|
|
var action = _.find(txp.actions, { |
|
|
|
copayerId: self.copayerId |
|
|
|
}); |
|
|
|
if (action) |
|
|
|
return cb(new ClientError('CVOTED', 'Copayer already voted on this transaction proposal')); |
|
|
|
|
|
|
|
if (txp.status != 'pending') |
|
|
|
return cb(new ClientError('TXNOTPENDING', 'The transaction proposal is not pending')); |
|
|
|
if (action) return cb(Errors.CVOTED); |
|
|
|
if (txp.status != 'pending') return cb(Errors.TXNOTPENDING); |
|
|
|
|
|
|
|
txp.reject(self.copayerId, opts.reason); |
|
|
|
|
|
|
@ -1396,7 +1381,7 @@ WalletService.prototype.getPendingTxs = function(opts, cb) { |
|
|
|
return _.startsWith(txp.version, '1.'); |
|
|
|
}); |
|
|
|
|
|
|
|
if (!allLegacy) return cb(new ClientError('UPGRADENEEDED', 'Some spend proposals were created using a newer version of the client app')); |
|
|
|
if (!allLegacy) return cb(new ClientError(Errors.codes.UPGRADENEEDED, 'Some spend proposals were created using a newer version of the client app')); |
|
|
|
} |
|
|
|
|
|
|
|
_.each(txps, function(txp) { |
|
|
@ -1617,7 +1602,7 @@ WalletService.prototype.getTxHistory = function(opts, cb) { |
|
|
|
bc.getTransactions(addressStrs, from, to, function(err, txs) { |
|
|
|
if (err) { |
|
|
|
log.error('Could not fetch transactions', err); |
|
|
|
return next(new ClientError('BLOCKCHAINERROR', 'Could not fetch transactions')); |
|
|
|
return next(new ClientError(Errors.codes.BLOCKCHAINERROR, 'Could not fetch transactions')); |
|
|
|
} |
|
|
|
next(null, self._normalizeTxHistory(txs)); |
|
|
|
}); |
|
|
@ -1673,7 +1658,7 @@ WalletService.prototype.scan = function(opts, cb) { |
|
|
|
checkActivity(_.pluck(addresses, 'address'), networkName, function(err, thereIsActivity) { |
|
|
|
if (err) { |
|
|
|
log.error('Could not check address activity', err); |
|
|
|
return next(new ClientError('BLOCKCHAINERROR', 'Could not check address activity')); |
|
|
|
return next(new ClientError(Errors.codes.BLOCKCHAINERROR, 'Could not check address activity')); |
|
|
|
} |
|
|
|
|
|
|
|
activity = thereIsActivity; |
|
|
|