|
|
@ -698,7 +698,7 @@ WalletService.prototype._selectTxInputs = function(txp, cb) { |
|
|
|
|
|
|
|
var balance = self._totalizeUtxos(utxos); |
|
|
|
|
|
|
|
if (balance.totalAmount < txp.amount) |
|
|
|
if (balance.totalAmount < txp.getTotalAmount()) |
|
|
|
return cb(new ClientError('INSUFFICIENTFUNDS', 'Insufficient funds')); |
|
|
|
if ((balance.totalAmount - balance.lockedAmount) < txp.amount) |
|
|
|
return cb(new ClientError('LOCKEDFUNDS', 'Funds are locked by pending transaction proposals')); |
|
|
@ -718,7 +718,7 @@ WalletService.prototype._selectTxInputs = function(txp, cb) { |
|
|
|
total += inputs[i].satoshis; |
|
|
|
i++; |
|
|
|
|
|
|
|
if (total >= txp.amount) { |
|
|
|
if (total >= txp.getTotalAmount()) { |
|
|
|
try { |
|
|
|
txp.inputs = selected; |
|
|
|
bitcoreTx = txp.getBitcoreTx(); |
|
|
@ -782,8 +782,10 @@ WalletService.prototype._canCreateTx = function(copayerId, cb) { |
|
|
|
/** |
|
|
|
* Creates a new transaction proposal. |
|
|
|
* @param {Object} opts |
|
|
|
* @param {string} opts.toAddress - Destination address. |
|
|
|
* @param {number} opts.amount - Amount to transfer in satoshi. |
|
|
|
* @param {string} opts.type - Proposal type. |
|
|
|
* @param {string} opts.toAddress || opts.outputs[].toAddress - Destination address. |
|
|
|
* @param {number} opts.amount || opts.outputs[].amount - Amount to transfer in satoshi. |
|
|
|
* @param {string} opts.outputs[].message - A message to attach to this output. |
|
|
|
* @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.feePerKb - Optional: Use an alternative fee per KB for this TX |
|
|
@ -793,9 +795,33 @@ WalletService.prototype._canCreateTx = function(copayerId, cb) { |
|
|
|
WalletService.prototype.createTx = function(opts, cb) { |
|
|
|
var self = this; |
|
|
|
|
|
|
|
if (!Utils.checkRequired(opts, ['toAddress', 'amount', 'proposalSignature'])) |
|
|
|
if (!Utils.checkRequired(opts, ['proposalSignature'])) |
|
|
|
return cb(new ClientError('Required argument missing')); |
|
|
|
|
|
|
|
if (!Model.TxProposal.isTypeSupported(opts.type)) |
|
|
|
return cb(new ClientError('Invalid proposal type')); |
|
|
|
|
|
|
|
if (opts.type == Model.TxProposal.Types.MULTIPLEOUTPUTS) { |
|
|
|
if (!Utils.checkRequired(opts, ['outputs'])) |
|
|
|
return cb(new ClientError('Required argument missing')); |
|
|
|
var missing = false, unsupported = false; |
|
|
|
_.each(opts.outputs, function(o) { |
|
|
|
if (!Utils.checkRequired(o, ['toAddress', 'amount'])) |
|
|
|
missing = true; |
|
|
|
_.each(_.keys(o), function(key) { |
|
|
|
if (!_.contains(['toAddress', 'amount', 'message'], key)) |
|
|
|
unsupported = true; |
|
|
|
}); |
|
|
|
}); |
|
|
|
if (missing) |
|
|
|
return cb(new ClientError('Required outputs argument missing')); |
|
|
|
if (unsupported) |
|
|
|
return cb(new ClientError('Invalid outputs argument found')); |
|
|
|
} else { |
|
|
|
if (!Utils.checkRequired(opts, ['toAddress', 'amount'])) |
|
|
|
return cb(new ClientError('Required argument missing')); |
|
|
|
} |
|
|
|
|
|
|
|
var feePerKb = opts.feePerKb || 10000; |
|
|
|
if (feePerKb < WalletUtils.MIN_FEE_PER_KB || feePerKb > WalletUtils.MAX_FEE_PER_KB) |
|
|
|
return cb(new ClientError('Invalid fee per KB value')); |
|
|
@ -812,30 +838,48 @@ WalletService.prototype.createTx = function(opts, cb) { |
|
|
|
return cb(new ClientError('NOTALLOWEDTOCREATETX', 'Cannot create TX proposal during backoff time')); |
|
|
|
|
|
|
|
var copayer = wallet.getCopayer(self.copayerId); |
|
|
|
var hash = WalletUtils.getProposalHash(opts.toAddress, opts.amount, opts.message, opts.payProUrl); |
|
|
|
var proposalHeader = Model.TxProposal.create(opts).getHeader(); |
|
|
|
var hash = WalletUtils.getProposalHash.apply(WalletUtils, proposalHeader); |
|
|
|
if (!self._verifySignature(hash, opts.proposalSignature, copayer.requestPubKey)) |
|
|
|
return cb(new ClientError('Invalid proposal signature')); |
|
|
|
|
|
|
|
var toAddress; |
|
|
|
try { |
|
|
|
toAddress = new Bitcore.Address(opts.toAddress); |
|
|
|
} catch (ex) { |
|
|
|
var outputs = (opts.type == Model.TxProposal.Types.MULTIPLEOUTPUTS) |
|
|
|
? opts.outputs |
|
|
|
: [ { toAddress: opts.toAddress, amount: opts.amount } ]; |
|
|
|
var badAddress = false, |
|
|
|
badNetwork = false, |
|
|
|
badAmount = false, |
|
|
|
badDust = false; |
|
|
|
_.each(outputs, function(output) { |
|
|
|
var toAddress; |
|
|
|
try { |
|
|
|
toAddress = new Bitcore.Address(output.toAddress); |
|
|
|
} catch (ex) { |
|
|
|
badAddress = true; |
|
|
|
} |
|
|
|
if (toAddress.network != wallet.getNetworkName()) |
|
|
|
badNetwork = true; |
|
|
|
if (output.amount <= 0) |
|
|
|
badAmount = true; |
|
|
|
if (output.amount < Bitcore.Transaction.DUST_AMOUNT) |
|
|
|
badDust = true; |
|
|
|
}); |
|
|
|
if (badAddress) |
|
|
|
return cb(new ClientError('INVALIDADDRESS', 'Invalid address')); |
|
|
|
} |
|
|
|
if (toAddress.network != wallet.getNetworkName()) |
|
|
|
if (badNetwork) |
|
|
|
return cb(new ClientError('INVALIDADDRESS', 'Incorrect address network')); |
|
|
|
|
|
|
|
if (opts.amount <= 0) |
|
|
|
if (badAmount) |
|
|
|
return cb(new ClientError('Invalid amount')); |
|
|
|
|
|
|
|
if (opts.amount < Bitcore.Transaction.DUST_AMOUNT) |
|
|
|
if (badDust) |
|
|
|
return cb(new ClientError('DUSTAMOUNT', 'Amount below dust threshold')); |
|
|
|
|
|
|
|
var changeAddress = wallet.createAddress(true); |
|
|
|
|
|
|
|
var txp = Model.TxProposal.create({ |
|
|
|
type: opts.type, |
|
|
|
walletId: self.walletId, |
|
|
|
creatorId: self.copayerId, |
|
|
|
outputs: opts.outputs, |
|
|
|
toAddress: opts.toAddress, |
|
|
|
amount: opts.amount, |
|
|
|
message: opts.message, |
|
|
@ -847,6 +891,7 @@ WalletService.prototype.createTx = function(opts, cb) { |
|
|
|
requiredRejections: Math.min(wallet.m, wallet.n - wallet.m + 1), |
|
|
|
}); |
|
|
|
|
|
|
|
|
|
|
|
self._selectTxInputs(txp, function(err) { |
|
|
|
if (err) return cb(err); |
|
|
|
|
|
|
@ -859,7 +904,7 @@ WalletService.prototype.createTx = function(opts, cb) { |
|
|
|
if (err) return cb(err); |
|
|
|
|
|
|
|
self._notify('NewTxProposal', { |
|
|
|
amount: opts.amount |
|
|
|
amount: txp.getTotalAmount() |
|
|
|
}, function() { |
|
|
|
return cb(null, txp); |
|
|
|
}); |
|
|
@ -1069,7 +1114,7 @@ WalletService.prototype.broadcastTx = function(opts, cb) { |
|
|
|
self._notify('NewOutgoingTx', { |
|
|
|
txProposalId: opts.txProposalId, |
|
|
|
txid: txid, |
|
|
|
amount: txp.amount, |
|
|
|
amount: txp.getTotalAmount(), |
|
|
|
}, function() { |
|
|
|
return cb(null, txp); |
|
|
|
}); |
|
|
|