|
|
@ -1934,6 +1934,7 @@ WalletService.prototype._validateAndSanitizeTxOpts = function(wallet, opts, cb) |
|
|
|
/** |
|
|
|
* 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. |
|
|
|
* @param {Array} opts.outputs - List of outputs. |
|
|
|
* @param {string} opts.outputs[].toAddress - Destination address. |
|
|
|
* @param {number} opts.outputs[].amount - Amount to transfer in satoshi. |
|
|
@ -1973,73 +1974,90 @@ WalletService.prototype.createTx = function(opts, cb) { |
|
|
|
} |
|
|
|
}; |
|
|
|
|
|
|
|
function checkTxpAlreadyExists(txProposalId, cb) { |
|
|
|
if (!txProposalId) return cb(); |
|
|
|
|
|
|
|
self.storage.fetchTx(self.walletId, txProposalId, function(err, txp) { |
|
|
|
if (err || !txp) return cb(err); |
|
|
|
if (txp.status == 'temporary') { |
|
|
|
return cb(null, txp); |
|
|
|
} else { |
|
|
|
return cb(Errors.TX_ALREADY_EXISTS); |
|
|
|
} |
|
|
|
}); |
|
|
|
}; |
|
|
|
|
|
|
|
self._runLocked(cb, function(cb) { |
|
|
|
var wallet, txp, changeAddress; |
|
|
|
async.series([ |
|
|
|
var txp, changeAddress; |
|
|
|
self.getWallet({}, function(err, wallet) { |
|
|
|
if (err) return cb(err); |
|
|
|
if (!wallet.isComplete()) return cb(Errors.WALLET_NOT_COMPLETE); |
|
|
|
|
|
|
|
function(next) { |
|
|
|
self.getWallet({}, function(err, w) { |
|
|
|
if (err) return next(err); |
|
|
|
if (!w.isComplete()) return next(Errors.WALLET_NOT_COMPLETE); |
|
|
|
wallet = w; |
|
|
|
next(); |
|
|
|
}); |
|
|
|
}, |
|
|
|
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(); |
|
|
|
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(); |
|
|
|
}); |
|
|
|
}, |
|
|
|
function(next) { |
|
|
|
var txOpts = { |
|
|
|
id: opts.txProposalId, |
|
|
|
walletId: self.walletId, |
|
|
|
creatorId: self.copayerId, |
|
|
|
outputs: opts.outputs, |
|
|
|
message: opts.message, |
|
|
|
changeAddress: changeAddress, |
|
|
|
feePerKb: opts.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 || 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) { |
|
|
|
if (err && err !== 'break') return cb(err); |
|
|
|
return cb(null, txp); |
|
|
|
}); |
|
|
|
}, |
|
|
|
function(next) { |
|
|
|
var txOpts = { |
|
|
|
walletId: self.walletId, |
|
|
|
creatorId: self.copayerId, |
|
|
|
outputs: opts.outputs, |
|
|
|
message: opts.message, |
|
|
|
changeAddress: changeAddress, |
|
|
|
feePerKb: opts.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 || 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) { |
|
|
|
if (err) return cb(err); |
|
|
|
return cb(null, txp); |
|
|
|
}); |
|
|
|
}); |
|
|
|
}); |
|
|
|
}; |
|
|
|