|
|
@ -827,7 +827,7 @@ WalletService.prototype._getUtxosForAddresses = function(addresses, cb) { |
|
|
|
var u = _.pick(utxo, ['txid', 'vout', 'address', 'scriptPubKey', 'amount', 'satoshis', 'confirmations']); |
|
|
|
u.confirmations = u.confirmations || 0; |
|
|
|
u.locked = false; |
|
|
|
u.satoshis = u.satoshis ? +u.satoshis : Utils.strip(u.amount * 1e8); |
|
|
|
u.satoshis = _.isNumber(u.satoshis) ? +u.satoshis : Utils.strip(u.amount * 1e8); |
|
|
|
delete u.amount; |
|
|
|
return u; |
|
|
|
}); |
|
|
@ -1026,8 +1026,43 @@ WalletService.prototype.getFeeLevels = function(opts, cb) { |
|
|
|
}); |
|
|
|
}; |
|
|
|
|
|
|
|
WalletService.prototype._checkTxAndEstimateFee = function(txp) { |
|
|
|
var bitcoreError; |
|
|
|
|
|
|
|
var serializationOpts = { |
|
|
|
disableIsFullySigned: true |
|
|
|
}; |
|
|
|
if (!_.startsWith(txp.version, '1.')) { |
|
|
|
serializationOpts.disableSmallFees = true; |
|
|
|
serializationOpts.disableLargeFees = true; |
|
|
|
} |
|
|
|
|
|
|
|
try { |
|
|
|
txp.estimateFee(); |
|
|
|
var bitcoreTx = txp.getBitcoreTx(); |
|
|
|
bitcoreError = bitcoreTx.getSerializationError(serializationOpts); |
|
|
|
if (!bitcoreError) { |
|
|
|
txp.fee = bitcoreTx.getFee(); |
|
|
|
} |
|
|
|
} catch (ex) { |
|
|
|
log.error('Error building Bitcore transaction', ex); |
|
|
|
return ex; |
|
|
|
} |
|
|
|
|
|
|
|
if (bitcoreError instanceof Bitcore.errors.Transaction.FeeError) |
|
|
|
return Errors.INSUFFICIENT_FUNDS_FOR_FEE; |
|
|
|
|
|
|
|
if (bitcoreError instanceof Bitcore.errors.Transaction.DustOutputs) |
|
|
|
return Errors.DUST_AMOUNT; |
|
|
|
return bitcoreError; |
|
|
|
}; |
|
|
|
|
|
|
|
WalletService.prototype._selectTxInputs = function(txp, utxosToExclude, cb) { |
|
|
|
var self = this; |
|
|
|
//todo: check inputs are ours and has enough value
|
|
|
|
if (txp.inputs && txp.inputs.length > 0) { |
|
|
|
return cb(self._checkTxAndEstimateFee(txp)); |
|
|
|
} |
|
|
|
|
|
|
|
function sortUtxos(utxos) { |
|
|
|
var list = _.map(utxos, function(utxo) { |
|
|
@ -1086,13 +1121,6 @@ WalletService.prototype._selectTxInputs = function(txp, utxosToExclude, cb) { |
|
|
|
var inputs = sortUtxos(utxos); |
|
|
|
|
|
|
|
var bitcoreTx, bitcoreError; |
|
|
|
var serializationOpts = { |
|
|
|
disableIsFullySigned: true, |
|
|
|
}; |
|
|
|
if (!_.startsWith(txp.version, '1.')) { |
|
|
|
serializationOpts.disableSmallFees = true; |
|
|
|
serializationOpts.disableLargeFees = true; |
|
|
|
} |
|
|
|
|
|
|
|
while (i < inputs.length) { |
|
|
|
selected.push(inputs[i]); |
|
|
@ -1100,28 +1128,14 @@ WalletService.prototype._selectTxInputs = function(txp, utxosToExclude, cb) { |
|
|
|
i++; |
|
|
|
|
|
|
|
if (total >= txp.getTotalAmount()) { |
|
|
|
try { |
|
|
|
txp.setInputs(selected); |
|
|
|
txp.estimateFee(); |
|
|
|
bitcoreTx = txp.getBitcoreTx(); |
|
|
|
bitcoreError = bitcoreTx.getSerializationError(serializationOpts); |
|
|
|
if (!bitcoreError) { |
|
|
|
txp.fee = bitcoreTx.getFee(); |
|
|
|
return cb(); |
|
|
|
} |
|
|
|
} catch (ex) { |
|
|
|
log.error('Error building Bitcore transaction', ex); |
|
|
|
return cb(ex); |
|
|
|
txp.setInputs(selected); |
|
|
|
bitcoreError = self._checkTxAndEstimateFee(txp); |
|
|
|
if (!bitcoreError) { |
|
|
|
return cb(); |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
if (bitcoreError instanceof Bitcore.errors.Transaction.FeeError) |
|
|
|
return cb(Errors.INSUFFICIENT_FUNDS_FOR_FEE); |
|
|
|
|
|
|
|
if (bitcoreError instanceof Bitcore.errors.Transaction.DustOutputs) |
|
|
|
return cb(Errors.DUST_AMOUNT); |
|
|
|
|
|
|
|
return cb(bitcoreError || new Error('Could not select tx inputs')); |
|
|
|
}); |
|
|
|
}; |
|
|
@ -1155,6 +1169,34 @@ WalletService.prototype._canCreateTx = function(copayerId, cb) { |
|
|
|
}); |
|
|
|
}; |
|
|
|
|
|
|
|
WalletService.prototype._validateOutputs = function(opts, wallet) { |
|
|
|
for (var i = 0; i < opts.outputs.length; i++) { |
|
|
|
var output = opts.outputs[i]; |
|
|
|
output.valid = false; |
|
|
|
|
|
|
|
if (!Utils.checkRequired(output, ['toAddress', 'amount'])) { |
|
|
|
return new ClientError('Required outputs argument missing'); |
|
|
|
} |
|
|
|
|
|
|
|
var toAddress = {}; |
|
|
|
try { |
|
|
|
toAddress = new Bitcore.Address(output.toAddress); |
|
|
|
} catch (ex) { |
|
|
|
return Errors.INVALID_ADDRESS; |
|
|
|
} |
|
|
|
if (toAddress.network != wallet.getNetworkName()) { |
|
|
|
return Errors.INCORRECT_ADDRESS_NETWORK; |
|
|
|
} |
|
|
|
if (!_.isNumber(output.amount) || _.isNaN(output.amount) || output.amount <= 0) { |
|
|
|
return new ClientError('Invalid amount'); |
|
|
|
} |
|
|
|
if (output.amount < Bitcore.Transaction.DUST_AMOUNT) { |
|
|
|
return Errors.DUST_AMOUNT; |
|
|
|
} |
|
|
|
output.valid = true; |
|
|
|
} |
|
|
|
return null; |
|
|
|
}; |
|
|
|
|
|
|
|
WalletService._getProposalHash = function(proposalHeader) { |
|
|
|
function getOldHash(toAddress, amount, message, payProUrl) { |
|
|
@ -1178,6 +1220,7 @@ WalletService._getProposalHash = function(proposalHeader) { |
|
|
|
* @param {string} opts.outputs[].message - A message to attach to this output. |
|
|
|
* @param {string} opts.message - A message to attach to this transaction. |
|
|
|
* @param {string} opts.proposalSignature - S(toAddress|amount|message|payProUrl). Used by other copayers to verify the proposal. |
|
|
|
* @param {string} opts.inputs - Optional. Inputs for this TX |
|
|
|
* @param {string} opts.feePerKb - Optional: Use an alternative fee per KB for this TX |
|
|
|
* @param {string} opts.payProUrl - Optional: Paypro URL for peers to verify TX |
|
|
|
* @param {string} opts.excludeUnconfirmedUtxos - Optional: Do not use UTXOs of unconfirmed transactions as inputs |
|
|
@ -1198,17 +1241,6 @@ WalletService.prototype.createTx = function(opts, cb) { |
|
|
|
if (!Model.TxProposal.isTypeSupported(type)) |
|
|
|
return cb(new ClientError('Invalid proposal type')); |
|
|
|
|
|
|
|
_.each(opts.outputs, function(output) { |
|
|
|
if (!Utils.checkRequired(output, ['toAddress', 'amount'])) { |
|
|
|
output.valid = false; |
|
|
|
cb(new ClientError('Required outputs argument missing')); |
|
|
|
return false; |
|
|
|
} |
|
|
|
}); |
|
|
|
if (_.any(opts.outputs, { |
|
|
|
valid: false |
|
|
|
})) return; |
|
|
|
|
|
|
|
var feePerKb = opts.feePerKb || Defaults.DEFAULT_FEE_PER_KB; |
|
|
|
if (feePerKb < Defaults.MIN_FEE_PER_KB || feePerKb > Defaults.MAX_FEE_PER_KB) |
|
|
|
return cb(new ClientError('Invalid fee per KB value')); |
|
|
@ -1242,38 +1274,19 @@ WalletService.prototype.createTx = function(opts, cb) { |
|
|
|
if (err) return cb(err); |
|
|
|
if (!canCreate) return cb(Errors.TX_CANNOT_CREATE); |
|
|
|
|
|
|
|
_.each(opts.outputs, function(output) { |
|
|
|
output.valid = false; |
|
|
|
var toAddress = {}; |
|
|
|
try { |
|
|
|
toAddress = new Bitcore.Address(output.toAddress); |
|
|
|
} catch (ex) { |
|
|
|
cb(Errors.INVALID_ADDRESS); |
|
|
|
return false; |
|
|
|
} |
|
|
|
if (toAddress.network != wallet.getNetworkName()) { |
|
|
|
cb(Errors.INCORRECT_ADDRESS_NETWORK); |
|
|
|
return false; |
|
|
|
if (type != Model.TxProposal.Types.EXTERNAL) { |
|
|
|
var validationError = self._validateOutputs(opts, wallet); |
|
|
|
if (validationError) { |
|
|
|
return cb(validationError); |
|
|
|
} |
|
|
|
if (!_.isNumber(output.amount) || _.isNaN(output.amount) || output.amount <= 0) { |
|
|
|
cb(new ClientError('Invalid amount')); |
|
|
|
return false; |
|
|
|
} |
|
|
|
if (output.amount < Bitcore.Transaction.DUST_AMOUNT) { |
|
|
|
cb(Errors.DUST_AMOUNT); |
|
|
|
return false; |
|
|
|
} |
|
|
|
output.valid = true; |
|
|
|
}); |
|
|
|
if (_.any(opts.outputs, { |
|
|
|
valid: false |
|
|
|
})) return; |
|
|
|
} |
|
|
|
|
|
|
|
var txOpts = { |
|
|
|
type: type, |
|
|
|
walletId: self.walletId, |
|
|
|
creatorId: self.copayerId, |
|
|
|
outputs: opts.outputs, |
|
|
|
inputs: opts.inputs, |
|
|
|
toAddress: opts.toAddress, |
|
|
|
amount: opts.amount, |
|
|
|
message: opts.message, |
|
|
|