commit
93a8d65932
|
@ -350,6 +350,19 @@ ExpressApp.prototype.start = function(opts, cb) {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
router.get('/v1/sendmaxinfo/', function(req, res) {
|
||||||
|
getServerWithAuth(req, res, function(server) {
|
||||||
|
var opts = {};
|
||||||
|
opts.feePerKb = +req.query.feePerKb;
|
||||||
|
if (req.query.excludeUnconfirmedUtxos == '1') opts.excludeUnconfirmedUtxos = true;
|
||||||
|
if (req.query.returnInputs == '1') opts.returnInputs = true;
|
||||||
|
server.getSendMaxInfo(opts, function(err, info) {
|
||||||
|
if (err) return returnError(err, res, req);
|
||||||
|
res.json(info);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
router.get('/v1/utxos/', function(req, res) {
|
router.get('/v1/utxos/', function(req, res) {
|
||||||
var opts = {};
|
var opts = {};
|
||||||
var addresses = req.query.addresses;
|
var addresses = req.query.addresses;
|
||||||
|
|
|
@ -164,7 +164,7 @@ TxProposal.prototype._buildTx = function() {
|
||||||
var totalInputs = _.sum(self.inputs, 'satoshis');
|
var totalInputs = _.sum(self.inputs, 'satoshis');
|
||||||
var totalOutputs = _.sum(self.outputs, 'satoshis');
|
var totalOutputs = _.sum(self.outputs, 'satoshis');
|
||||||
|
|
||||||
if (totalInputs - totalOutputs - self.fee > 0) {
|
if (totalInputs - totalOutputs - self.fee > 0 && self.changeAddress) {
|
||||||
t.change(self.changeAddress.address);
|
t.change(self.changeAddress.address);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -252,10 +252,14 @@ TxProposal.prototype.getEstimatedSize = function() {
|
||||||
return parseInt((size * (1 + safetyMargin)).toFixed(0));
|
return parseInt((size * (1 + safetyMargin)).toFixed(0));
|
||||||
};
|
};
|
||||||
|
|
||||||
TxProposal.prototype.estimateFee = function() {
|
TxProposal.prototype.getEstimatedFee = function() {
|
||||||
$.checkState(_.isNumber(this.feePerKb));
|
$.checkState(_.isNumber(this.feePerKb));
|
||||||
var fee = this.feePerKb * this.getEstimatedSize() / 1000;
|
var fee = this.feePerKb * this.getEstimatedSize() / 1000;
|
||||||
this.fee = parseInt(fee.toFixed(0));
|
return parseInt(fee.toFixed(0));
|
||||||
|
};
|
||||||
|
|
||||||
|
TxProposal.prototype.estimateFee = function() {
|
||||||
|
this.fee = this.getEstimatedFee();
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
208
lib/server.js
208
lib/server.js
|
@ -606,6 +606,7 @@ WalletService._getCopayerHash = function(name, xPubKey, requestPubKey) {
|
||||||
* @param {string} opts.requestPubKey - Public Key used to check requests from this copayer.
|
* @param {string} opts.requestPubKey - Public Key used to check requests from this copayer.
|
||||||
* @param {string} opts.copayerSignature - S(name|xPubKey|requestPubKey). Used by other copayers to verify that the copayer joining knows the wallet secret.
|
* @param {string} opts.copayerSignature - S(name|xPubKey|requestPubKey). Used by other copayers to verify that the copayer joining knows the wallet secret.
|
||||||
* @param {string} opts.customData - (optional) Custom data for this copayer.
|
* @param {string} opts.customData - (optional) Custom data for this copayer.
|
||||||
|
* @param {string} opts.dryRun[=false] - (optional) Simulate the action but do not change server state.
|
||||||
* @param {string} [opts.supportBIP44AndP2PKH = true] - Client supports BIP44 & P2PKH for joining wallets.
|
* @param {string} [opts.supportBIP44AndP2PKH = true] - Client supports BIP44 & P2PKH for joining wallets.
|
||||||
*/
|
*/
|
||||||
WalletService.prototype.joinWallet = function(opts, cb) {
|
WalletService.prototype.joinWallet = function(opts, cb) {
|
||||||
|
@ -1148,6 +1149,75 @@ WalletService.prototype.getBalance = function(opts, cb) {
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return info needed to send all funds in the wallet
|
||||||
|
* @param {Object} opts
|
||||||
|
* @param {string} opts.feePerKb - The fee per KB used to compute the TX.
|
||||||
|
* @param {string} opts.excludeUnconfirmedUtxos[=false] - Optional. Do not use UTXOs of unconfirmed transactions as inputs
|
||||||
|
* @param {string} opts.returnInputs[=false] - Optional. Return the list of UTXOs that would be included in the tx.
|
||||||
|
* @returns {Object} sendMaxInfo
|
||||||
|
*/
|
||||||
|
WalletService.prototype.getSendMaxInfo = function(opts, cb) {
|
||||||
|
var self = this;
|
||||||
|
|
||||||
|
|
||||||
|
opts = opts || {};
|
||||||
|
|
||||||
|
if (!Utils.checkRequired(opts, ['feePerKb']))
|
||||||
|
return cb(new ClientError('Required argument missing'));
|
||||||
|
|
||||||
|
self.getWallet({}, function(err, wallet) {
|
||||||
|
if (err) return cb(err);
|
||||||
|
|
||||||
|
self._getUtxosForCurrentWallet(null, function(err, utxos) {
|
||||||
|
if (err) return cb(err);
|
||||||
|
|
||||||
|
var info = {
|
||||||
|
size: 0,
|
||||||
|
amount: 0,
|
||||||
|
fee: 0,
|
||||||
|
inputs: [],
|
||||||
|
};
|
||||||
|
|
||||||
|
var inputs = _.reject(utxos, 'locked');
|
||||||
|
if (!!opts.excludeUnconfirmedUtxos) {
|
||||||
|
inputs = _.filter(inputs, 'confirmations');
|
||||||
|
}
|
||||||
|
inputs = _.sortBy(inputs, 'satoshis');
|
||||||
|
|
||||||
|
if (_.isEmpty(inputs)) return cb(null, info);
|
||||||
|
|
||||||
|
var txp = Model.TxProposal.create({
|
||||||
|
walletId: self.walletId,
|
||||||
|
network: wallet.network,
|
||||||
|
walletM: wallet.m,
|
||||||
|
walletN: wallet.n,
|
||||||
|
feePerKb: opts.feePerKb,
|
||||||
|
});
|
||||||
|
|
||||||
|
var lastFee = txp.getEstimatedFee();
|
||||||
|
_.eachRight(inputs, function(input) {
|
||||||
|
txp.inputs.push(input);
|
||||||
|
var fee = txp.getEstimatedFee();
|
||||||
|
var sizeInKb = txp.getEstimatedSize() / 1000.;
|
||||||
|
if (fee - lastFee > input.satoshis || sizeInKb > Defaults.MAX_TX_SIZE_IN_KB) {
|
||||||
|
txp.inputs.pop();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
lastFee = fee;
|
||||||
|
});
|
||||||
|
info.size = txp.getEstimatedSize();
|
||||||
|
info.fee = txp.getEstimatedFee();
|
||||||
|
info.amount = _.sum(txp.inputs, 'satoshis') - info.fee;
|
||||||
|
if (opts.returnInputs) {
|
||||||
|
info.inputs = _.shuffle(txp.inputs);
|
||||||
|
}
|
||||||
|
|
||||||
|
return cb(null, info);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
WalletService.prototype._sampleFeeLevels = function(network, points, cb) {
|
WalletService.prototype._sampleFeeLevels = function(network, points, cb) {
|
||||||
var self = this;
|
var self = this;
|
||||||
|
@ -1559,12 +1629,14 @@ WalletService.prototype._validateOutputs = function(opts, wallet) {
|
||||||
if (toAddress.network != wallet.getNetworkName()) {
|
if (toAddress.network != wallet.getNetworkName()) {
|
||||||
return Errors.INCORRECT_ADDRESS_NETWORK;
|
return Errors.INCORRECT_ADDRESS_NETWORK;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!_.isNumber(output.amount) || _.isNaN(output.amount) || output.amount <= 0) {
|
if (!_.isNumber(output.amount) || _.isNaN(output.amount) || output.amount <= 0) {
|
||||||
return new ClientError('Invalid amount');
|
return new ClientError('Invalid amount');
|
||||||
}
|
}
|
||||||
if (output.amount < Bitcore.Transaction.DUST_AMOUNT) {
|
if (output.amount < Bitcore.Transaction.DUST_AMOUNT) {
|
||||||
return Errors.DUST_AMOUNT;
|
return Errors.DUST_AMOUNT;
|
||||||
}
|
}
|
||||||
|
|
||||||
output.valid = true;
|
output.valid = true;
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
|
@ -1716,6 +1788,59 @@ WalletService.prototype.createTxLegacy = function(opts, cb) {
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
WalletService.prototype._validateAndSanitizeTxOpts = function(wallet, opts, cb) {
|
||||||
|
var self = this;
|
||||||
|
|
||||||
|
async.series([
|
||||||
|
|
||||||
|
function(next) {
|
||||||
|
if (!Utils.checkRequired(opts, ['outputs']))
|
||||||
|
return next(new ClientError('Required argument missing'));
|
||||||
|
next();
|
||||||
|
},
|
||||||
|
function(next) {
|
||||||
|
// feePerKb is required unless inputs & fee are specified
|
||||||
|
if (!_.isNumber(opts.feePerKb) && !(opts.inputs && _.isNumber(opts.fee)))
|
||||||
|
return next(new ClientError('Required argument missing'));
|
||||||
|
|
||||||
|
if (_.isNumber(opts.feePerKb)) {
|
||||||
|
if (opts.feePerKb < Defaults.MIN_FEE_PER_KB || opts.feePerKb > Defaults.MAX_FEE_PER_KB)
|
||||||
|
return next(new ClientError('Invalid fee per KB'));
|
||||||
|
}
|
||||||
|
next();
|
||||||
|
},
|
||||||
|
function(next) {
|
||||||
|
if (!opts.sendMax) return next();
|
||||||
|
if (!_.isArray(opts.outputs) || opts.outputs.length > 1) {
|
||||||
|
return next(new ClientError('Only one output allowed when sendMax is specified'));
|
||||||
|
}
|
||||||
|
if (_.isNumber(opts.outputs[0].amount))
|
||||||
|
return next(new ClientError('Amount is not allowed when sendMax is specified'));
|
||||||
|
if (_.isNumber(opts.fee))
|
||||||
|
return next(new ClientError('Fee is not allowed when sendMax is specified (use feePerKb instead)'));
|
||||||
|
|
||||||
|
self.getSendMaxInfo({
|
||||||
|
feePerKb: opts.feePerKb || Defaults.DEFAULT_FEE_PER_KB,
|
||||||
|
excludeUnconfirmedUtxos: !!opts.excludeUnconfirmedUtxos,
|
||||||
|
returnInputs: true,
|
||||||
|
}, function(err, info) {
|
||||||
|
if (err) return next(err);
|
||||||
|
opts.outputs[0].amount = info.amount;
|
||||||
|
opts.inputs = info.inputs;
|
||||||
|
return next();
|
||||||
|
});
|
||||||
|
},
|
||||||
|
function(next) {
|
||||||
|
if (opts.validateOutputs === false) return next();
|
||||||
|
var validationError = self._validateOutputs(opts, wallet);
|
||||||
|
if (validationError) {
|
||||||
|
return next(validationError);
|
||||||
|
}
|
||||||
|
next();
|
||||||
|
},
|
||||||
|
], cb);
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Creates a new transaction proposal.
|
* Creates a new transaction proposal.
|
||||||
* @param {Object} opts
|
* @param {Object} opts
|
||||||
|
@ -1724,51 +1849,52 @@ WalletService.prototype.createTxLegacy = function(opts, cb) {
|
||||||
* @param {number} opts.outputs[].amount - Amount to transfer in satoshi.
|
* @param {number} opts.outputs[].amount - Amount to transfer in satoshi.
|
||||||
* @param {string} opts.outputs[].message - A message to attach to this output.
|
* @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.message - A message to attach to this transaction.
|
||||||
* @param {number} opts.feePerKb - The fee per kB to use for this TX.
|
* @param {string} opts.feePerKb - Use an alternative fee per KB for this TX.
|
||||||
|
* @param {string} opts.sendMax - Optional. Send maximum amount of funds that make sense under the specified fee/feePerKb conditions. (defaults to false).
|
||||||
* @param {string} opts.payProUrl - Optional. Paypro URL for peers to verify TX
|
* @param {string} opts.payProUrl - Optional. Paypro URL for peers to verify TX
|
||||||
* @param {string} opts.excludeUnconfirmedUtxos[=false] - Optional. Do not use UTXOs of unconfirmed transactions as inputs
|
* @param {string} opts.excludeUnconfirmedUtxos[=false] - Optional. Do not use UTXOs of unconfirmed transactions as inputs
|
||||||
* @param {string} opts.validateOutputs[=true] - Optional. Perform validation on outputs.
|
* @param {string} opts.validateOutputs[=true] - Optional. Perform validation on outputs.
|
||||||
|
* @param {string} opts.dryRun[=false] - Optional. Simulate the action but do not change server state.
|
||||||
* @param {Array} opts.inputs - Optional. Inputs for this TX
|
* @param {Array} opts.inputs - Optional. Inputs for this TX
|
||||||
* @param {number} opts.fee - Optional. The fee to use for this TX (used only when opts.inputs is specified).
|
* @param {number} opts.fee - Optional. Use an fixed fee for this TX (only when opts.inputs is specified)
|
||||||
* @returns {TxProposal} Transaction proposal.
|
* @returns {TxProposal} Transaction proposal.
|
||||||
*/
|
*/
|
||||||
WalletService.prototype.createTx = function(opts, cb) {
|
WalletService.prototype.createTx = function(opts, cb) {
|
||||||
var self = this;
|
var self = this;
|
||||||
|
|
||||||
if (!Utils.checkRequired(opts, ['outputs']))
|
|
||||||
return cb(new ClientError('Required argument missing'));
|
|
||||||
|
|
||||||
// feePerKb is required unless inputs & fee are specified
|
|
||||||
if (!_.isNumber(opts.feePerKb) && !(opts.inputs && _.isNumber(opts.fee)))
|
|
||||||
return cb(new ClientError('Required argument missing'));
|
|
||||||
|
|
||||||
if (_.isNumber(opts.feePerKb)) {
|
|
||||||
if (opts.feePerKb < Defaults.MIN_FEE_PER_KB || opts.feePerKb > Defaults.MAX_FEE_PER_KB)
|
|
||||||
return cb(new ClientError('Invalid fee per KB'));
|
|
||||||
}
|
|
||||||
|
|
||||||
self._runLocked(cb, function(cb) {
|
self._runLocked(cb, function(cb) {
|
||||||
self.getWallet({}, function(err, wallet) {
|
|
||||||
if (err) return cb(err);
|
|
||||||
if (!wallet.isComplete()) return cb(Errors.WALLET_NOT_COMPLETE);
|
|
||||||
|
|
||||||
|
var wallet, txp, changeAddress;
|
||||||
|
async.series([
|
||||||
|
|
||||||
|
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) {
|
self._canCreateTx(function(err, canCreate) {
|
||||||
if (err) return cb(err);
|
if (err) return next(err);
|
||||||
if (!canCreate) return cb(Errors.TX_CANNOT_CREATE);
|
if (!canCreate) return next(Errors.TX_CANNOT_CREATE);
|
||||||
|
next();
|
||||||
if (opts.validateOutputs !== false) {
|
});
|
||||||
var validationError = self._validateOutputs(opts, wallet);
|
},
|
||||||
if (validationError) {
|
function(next) {
|
||||||
return cb(validationError);
|
if (!opts.sendMax) {
|
||||||
|
changeAddress = wallet.createAddress(true);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
var txOpts = {
|
var txOpts = {
|
||||||
walletId: self.walletId,
|
walletId: self.walletId,
|
||||||
creatorId: self.copayerId,
|
creatorId: self.copayerId,
|
||||||
outputs: opts.outputs,
|
outputs: opts.outputs,
|
||||||
message: opts.message,
|
message: opts.message,
|
||||||
changeAddress: wallet.createAddress(true),
|
changeAddress: changeAddress,
|
||||||
feePerKb: opts.feePerKb,
|
feePerKb: opts.feePerKb,
|
||||||
payProUrl: opts.payProUrl,
|
payProUrl: opts.payProUrl,
|
||||||
walletM: wallet.m,
|
walletM: wallet.m,
|
||||||
|
@ -1781,23 +1907,25 @@ WalletService.prototype.createTx = function(opts, cb) {
|
||||||
fee: opts.inputs && !_.isNumber(opts.feePerKb) ? opts.fee : null,
|
fee: opts.inputs && !_.isNumber(opts.feePerKb) ? opts.fee : null,
|
||||||
};
|
};
|
||||||
|
|
||||||
var txp = Model.TxProposal.create(txOpts);
|
txp = Model.TxProposal.create(txOpts);
|
||||||
|
next();
|
||||||
self._selectTxInputs(txp, opts.utxosToExclude, function(err) {
|
},
|
||||||
if (err) return cb(err);
|
function(next) {
|
||||||
|
self._selectTxInputs(txp, opts.utxosToExclude, next);
|
||||||
self.storage.storeAddressAndWallet(wallet, txp.changeAddress, function(err) {
|
},
|
||||||
if (err) return cb(err);
|
function(next) {
|
||||||
|
if (!changeAddress || opts.dryRun) return next();
|
||||||
self.storage.storeTx(wallet.id, txp, function(err) {
|
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);
|
if (err) return cb(err);
|
||||||
return cb(null, txp);
|
return cb(null, txp);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
};
|
};
|
||||||
|
|
||||||
WalletService.prototype._verifyRequestPubKey = function(requestPubKey, signature, xPubKey) {
|
WalletService.prototype._verifyRequestPubKey = function(requestPubKey, signature, xPubKey) {
|
||||||
|
|
|
@ -113,6 +113,38 @@ describe('ExpressApp', function() {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('/v1/sendmaxinfo', function(done) {
|
||||||
|
var server = {
|
||||||
|
getSendMaxInfo: sinon.stub().callsArgWith(1, null, {
|
||||||
|
amount: 123
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
var TestExpressApp = proxyquire('../lib/expressapp', {
|
||||||
|
'./server': {
|
||||||
|
initialize: sinon.stub().callsArg(1),
|
||||||
|
getInstanceWithAuth: sinon.stub().callsArgWith(1, null, server),
|
||||||
|
}
|
||||||
|
});
|
||||||
|
start(TestExpressApp, function() {
|
||||||
|
var requestOptions = {
|
||||||
|
url: testHost + ':' + testPort + config.basePath + '/v1/sendmaxinfo?feePerKb=10000&returnInputs=1',
|
||||||
|
headers: {
|
||||||
|
'x-identity': 'identity',
|
||||||
|
'x-signature': 'signature'
|
||||||
|
}
|
||||||
|
};
|
||||||
|
request(requestOptions, function(err, res, body) {
|
||||||
|
should.not.exist(err);
|
||||||
|
res.statusCode.should.equal(200);
|
||||||
|
var args = server.getSendMaxInfo.getCalls()[0].args[0];
|
||||||
|
args.feePerKb.should.equal(10000);
|
||||||
|
args.returnInputs.should.be.true;
|
||||||
|
JSON.parse(body).amount.should.equal(123);
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
describe('Balance', function() {
|
describe('Balance', function() {
|
||||||
it('should handle cache argument', function(done) {
|
it('should handle cache argument', function(done) {
|
||||||
var server = {
|
var server = {
|
||||||
|
|
|
@ -2841,6 +2841,32 @@ describe('Wallet service', function() {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should not be able to publish a temporary tx proposal created in a dry run', function(done) {
|
||||||
|
helpers.stubUtxos(server, wallet, [1, 2], function() {
|
||||||
|
var txOpts = {
|
||||||
|
outputs: [{
|
||||||
|
toAddress: '18PzpUFkFZE8zKWUPvfykkTxmB9oMR8qP7',
|
||||||
|
amount: 0.8 * 1e8,
|
||||||
|
}],
|
||||||
|
feePerKb: 100e2,
|
||||||
|
dryRun: true,
|
||||||
|
};
|
||||||
|
server.createTx(txOpts, function(err, txp) {
|
||||||
|
should.not.exist(err);
|
||||||
|
should.exist(txp);
|
||||||
|
var publishOpts = helpers.getProposalSignatureOpts(txp, TestData.copayers[0].privKey_1H_0);
|
||||||
|
server.publishTx(publishOpts, function(err) {
|
||||||
|
should.exist(err);
|
||||||
|
err.code.should.equal('TX_NOT_FOUND');
|
||||||
|
server.getPendingTxs({}, function(err, txs) {
|
||||||
|
should.not.exist(err);
|
||||||
|
txs.length.should.equal(0);
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
it('should delay NewTxProposal notification until published', function(done) {
|
it('should delay NewTxProposal notification until published', function(done) {
|
||||||
helpers.stubUtxos(server, wallet, [1, 2], function() {
|
helpers.stubUtxos(server, wallet, [1, 2], function() {
|
||||||
var txOpts = {
|
var txOpts = {
|
||||||
|
@ -3113,6 +3139,33 @@ describe('Wallet service', function() {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should be able to send max funds', function(done) {
|
||||||
|
helpers.stubUtxos(server, wallet, [1, 2], function() {
|
||||||
|
var txOpts = {
|
||||||
|
outputs: [{
|
||||||
|
toAddress: '18PzpUFkFZE8zKWUPvfykkTxmB9oMR8qP7',
|
||||||
|
amount: null,
|
||||||
|
}],
|
||||||
|
feePerKb: 10000,
|
||||||
|
sendMax: true,
|
||||||
|
};
|
||||||
|
server.createTx(txOpts, function(err, tx) {
|
||||||
|
should.not.exist(err);
|
||||||
|
should.exist(tx);
|
||||||
|
should.not.exist(tx.changeAddress);
|
||||||
|
tx.amount.should.equal(3e8 - tx.fee);
|
||||||
|
|
||||||
|
var t = tx.getBitcoreTx();
|
||||||
|
t.getFee().should.equal(tx.fee);
|
||||||
|
should.not.exist(t.getChangeOutput());
|
||||||
|
t.toObject().inputs.length.should.equal(tx.inputs.length);
|
||||||
|
t.toObject().outputs[0].satoshis.should.equal(tx.amount);
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('Backoff time', function(done) {
|
describe('Backoff time', function(done) {
|
||||||
|
@ -3284,7 +3337,6 @@ describe('Wallet service', function() {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should select smaller utxos if within fee constraints', function(done) {
|
it('should select smaller utxos if within fee constraints', function(done) {
|
||||||
helpers.stubUtxos(server, wallet, [1, '800bit', '800bit', '800bit'], function() {
|
helpers.stubUtxos(server, wallet, [1, '800bit', '800bit', '800bit'], function() {
|
||||||
var txOpts = {
|
var txOpts = {
|
||||||
|
@ -3563,6 +3615,20 @@ describe('Wallet service', function() {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('#createTx backoff time', function() {
|
||||||
|
var server, wallet, txid;
|
||||||
|
|
||||||
|
beforeEach(function(done) {
|
||||||
|
helpers.createAndJoinWallet(2, 2, function(s, w) {
|
||||||
|
server = s;
|
||||||
|
wallet = w;
|
||||||
|
helpers.stubUtxos(server, wallet, _.range(2, 6), function() {
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
});
|
||||||
it('should ignore small utxos if fee is higher', function(done) {
|
it('should ignore small utxos if fee is higher', function(done) {
|
||||||
helpers.stubUtxos(server, wallet, [].concat(_.times(10, function() {
|
helpers.stubUtxos(server, wallet, [].concat(_.times(10, function() {
|
||||||
return '30bit';
|
return '30bit';
|
||||||
|
@ -3600,6 +3666,176 @@ describe('Wallet service', function() {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('#getSendMaxInfo', function() {
|
||||||
|
var server, wallet;
|
||||||
|
beforeEach(function(done) {
|
||||||
|
helpers.createAndJoinWallet(2, 3, function(s, w) {
|
||||||
|
server = s;
|
||||||
|
wallet = w;
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
function sendTx(info, cb) {
|
||||||
|
var txOpts = {
|
||||||
|
outputs: [{
|
||||||
|
toAddress: '18PzpUFkFZE8zKWUPvfykkTxmB9oMR8qP7',
|
||||||
|
amount: info.amount,
|
||||||
|
}],
|
||||||
|
inputs: info.inputs,
|
||||||
|
fee: info.fee,
|
||||||
|
};
|
||||||
|
server.createTx(txOpts, function(err, tx) {
|
||||||
|
should.not.exist(err);
|
||||||
|
should.exist(tx);
|
||||||
|
var t = tx.getBitcoreTx();
|
||||||
|
t.toObject().inputs.length.should.equal(info.inputs.length);
|
||||||
|
t.getFee().should.equal(info.fee);
|
||||||
|
should.not.exist(t.getChangeOutput());
|
||||||
|
return cb();
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
it('should be able to get send max info on empty wallet', function(done) {
|
||||||
|
server.getSendMaxInfo({
|
||||||
|
feePerKb: 10000,
|
||||||
|
returnInputs: true,
|
||||||
|
}, function(err, info) {
|
||||||
|
should.not.exist(err);
|
||||||
|
should.exist(info);
|
||||||
|
info.size.should.equal(0);
|
||||||
|
info.amount.should.equal(0);
|
||||||
|
info.fee.should.equal(0);
|
||||||
|
info.inputs.should.be.empty;
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
it('should correctly get send max info', function(done) {
|
||||||
|
helpers.stubUtxos(server, wallet, [0.1, 0.2, 0.3, 0.4], function() {
|
||||||
|
server.getSendMaxInfo({
|
||||||
|
feePerKb: 10000,
|
||||||
|
returnInputs: true,
|
||||||
|
}, function(err, info) {
|
||||||
|
should.not.exist(err);
|
||||||
|
should.exist(info);
|
||||||
|
info.inputs.length.should.equal(4);
|
||||||
|
info.size.should.equal(1304);
|
||||||
|
info.fee.should.equal(info.size * 10000 / 1000.);
|
||||||
|
info.amount.should.equal(1e8 - info.fee);
|
||||||
|
sendTx(info, done);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
it('should return inputs in random order', function(done) {
|
||||||
|
// NOTE: this test has a chance of failing of 1 in 1'073'741'824 :P
|
||||||
|
helpers.stubUtxos(server, wallet, _.range(1, 31), function(utxos) {
|
||||||
|
server.getSendMaxInfo({
|
||||||
|
feePerKb: 100e2,
|
||||||
|
returnInputs: true
|
||||||
|
}, function(err, info) {
|
||||||
|
should.not.exist(err);
|
||||||
|
should.exist(info);
|
||||||
|
var amounts = _.pluck(info.inputs, 'satoshis');
|
||||||
|
amounts.length.should.equal(30);
|
||||||
|
_.all(amounts, function(amount, i) {
|
||||||
|
if (i == 0) return true;
|
||||||
|
return amount < amounts[i - 1];
|
||||||
|
}).should.be.false;
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should exclude unconfirmed inputs', function(done) {
|
||||||
|
helpers.stubUtxos(server, wallet, ['u0.1', 0.2, 0.3, 0.4], function() {
|
||||||
|
server.getSendMaxInfo({
|
||||||
|
feePerKb: 10000,
|
||||||
|
excludeUnconfirmedUtxos: true,
|
||||||
|
returnInputs: true,
|
||||||
|
}, function(err, info) {
|
||||||
|
should.not.exist(err);
|
||||||
|
should.exist(info);
|
||||||
|
info.inputs.length.should.equal(3);
|
||||||
|
info.size.should.equal(1002);
|
||||||
|
info.fee.should.equal(info.size * 10000 / 1000.);
|
||||||
|
info.amount.should.equal(0.9e8 - info.fee);
|
||||||
|
sendTx(info, done);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
it('should exclude locked inputs', function(done) {
|
||||||
|
helpers.stubUtxos(server, wallet, ['u0.1', 0.1, 0.1, 0.1], function() {
|
||||||
|
var txOpts = {
|
||||||
|
outputs: [{
|
||||||
|
toAddress: '18PzpUFkFZE8zKWUPvfykkTxmB9oMR8qP7',
|
||||||
|
amount: 0.09e8,
|
||||||
|
}],
|
||||||
|
feePerKb: 100e2,
|
||||||
|
};
|
||||||
|
helpers.createAndPublishTx(server, txOpts, TestData.copayers[0].privKey_1H_0, function(tx) {
|
||||||
|
should.exist(tx);
|
||||||
|
server.getSendMaxInfo({
|
||||||
|
feePerKb: 10000,
|
||||||
|
excludeUnconfirmedUtxos: true,
|
||||||
|
returnInputs: true,
|
||||||
|
}, function(err, info) {
|
||||||
|
should.not.exist(err);
|
||||||
|
should.exist(info);
|
||||||
|
info.inputs.length.should.equal(2);
|
||||||
|
info.size.should.equal(700);
|
||||||
|
info.fee.should.equal(info.size * 10000 / 1000.);
|
||||||
|
info.amount.should.equal(0.2e8 - info.fee);
|
||||||
|
sendTx(info, done);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
it('should ignore utxos not contributing to total amount (below their cost in fee)', function(done) {
|
||||||
|
helpers.stubUtxos(server, wallet, ['u0.1', 0.2, 0.3, 0.4, 0.000001, 0.0002, 0.0003], function() {
|
||||||
|
server.getSendMaxInfo({
|
||||||
|
feePerKb: 0.001e8,
|
||||||
|
returnInputs: true,
|
||||||
|
}, function(err, info) {
|
||||||
|
should.not.exist(err);
|
||||||
|
should.exist(info);
|
||||||
|
info.inputs.length.should.equal(4);
|
||||||
|
info.size.should.equal(1304);
|
||||||
|
info.fee.should.equal(info.size * 0.001e8 / 1000.);
|
||||||
|
info.amount.should.equal(1e8 - info.fee);
|
||||||
|
server.getSendMaxInfo({
|
||||||
|
feePerKb: 0.0001e8,
|
||||||
|
returnInputs: true,
|
||||||
|
}, function(err, info) {
|
||||||
|
should.not.exist(err);
|
||||||
|
should.exist(info);
|
||||||
|
info.inputs.length.should.equal(6);
|
||||||
|
info.size.should.equal(1907);
|
||||||
|
info.fee.should.equal(info.size * 0.0001e8 / 1000.);
|
||||||
|
info.amount.should.equal(1.0005e8 - info.fee);
|
||||||
|
sendTx(info, done);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
it('should not go beyond max tx size', function(done) {
|
||||||
|
var _oldDefault = Defaults.MAX_TX_SIZE_IN_KB;
|
||||||
|
Defaults.MAX_TX_SIZE_IN_KB = 2;
|
||||||
|
helpers.stubUtxos(server, wallet, _.range(1, 10, 0), function() {
|
||||||
|
server.getSendMaxInfo({
|
||||||
|
feePerKb: 10000,
|
||||||
|
returnInputs: true,
|
||||||
|
}, function(err, info) {
|
||||||
|
should.not.exist(err);
|
||||||
|
should.exist(info);
|
||||||
|
info.size.should.be.below(2000);
|
||||||
|
info.inputs.length.should.be.below(9);
|
||||||
|
Defaults.MAX_TX_SIZE_IN_KB = _oldDefault;
|
||||||
|
sendTx(info, done);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
})
|
||||||
|
|
||||||
describe('#rejectTx', function() {
|
describe('#rejectTx', function() {
|
||||||
var server, wallet, txid;
|
var server, wallet, txid;
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue