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) {
|
||||
var opts = {};
|
||||
var addresses = req.query.addresses;
|
||||
|
|
|
@ -164,7 +164,7 @@ TxProposal.prototype._buildTx = function() {
|
|||
var totalInputs = _.sum(self.inputs, '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);
|
||||
}
|
||||
|
||||
|
@ -252,10 +252,14 @@ TxProposal.prototype.getEstimatedSize = function() {
|
|||
return parseInt((size * (1 + safetyMargin)).toFixed(0));
|
||||
};
|
||||
|
||||
TxProposal.prototype.estimateFee = function() {
|
||||
TxProposal.prototype.getEstimatedFee = function() {
|
||||
$.checkState(_.isNumber(this.feePerKb));
|
||||
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();
|
||||
};
|
||||
|
||||
/**
|
||||
|
|
212
lib/server.js
212
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.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.dryRun[=false] - (optional) Simulate the action but do not change server state.
|
||||
* @param {string} [opts.supportBIP44AndP2PKH = true] - Client supports BIP44 & P2PKH for joining wallets.
|
||||
*/
|
||||
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) {
|
||||
var self = this;
|
||||
|
@ -1559,12 +1629,14 @@ WalletService.prototype._validateOutputs = function(opts, wallet) {
|
|||
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;
|
||||
|
@ -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.
|
||||
* @param {Object} opts
|
||||
|
@ -1724,51 +1849,52 @@ WalletService.prototype.createTxLegacy = function(opts, cb) {
|
|||
* @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.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.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.dryRun[=false] - Optional. Simulate the action but do not change server state.
|
||||
* @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.
|
||||
*/
|
||||
WalletService.prototype.createTx = function(opts, cb) {
|
||||
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.getWallet({}, function(err, wallet) {
|
||||
if (err) return cb(err);
|
||||
if (!wallet.isComplete()) return cb(Errors.WALLET_NOT_COMPLETE);
|
||||
|
||||
self._canCreateTx(function(err, canCreate) {
|
||||
if (err) return cb(err);
|
||||
if (!canCreate) return cb(Errors.TX_CANNOT_CREATE);
|
||||
var wallet, txp, changeAddress;
|
||||
async.series([
|
||||
|
||||
if (opts.validateOutputs !== false) {
|
||||
var validationError = self._validateOutputs(opts, wallet);
|
||||
if (validationError) {
|
||||
return cb(validationError);
|
||||
}
|
||||
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) {
|
||||
changeAddress = wallet.createAddress(true);
|
||||
}
|
||||
|
||||
var txOpts = {
|
||||
walletId: self.walletId,
|
||||
creatorId: self.copayerId,
|
||||
outputs: opts.outputs,
|
||||
message: opts.message,
|
||||
changeAddress: wallet.createAddress(true),
|
||||
changeAddress: changeAddress,
|
||||
feePerKb: opts.feePerKb,
|
||||
payProUrl: opts.payProUrl,
|
||||
walletM: wallet.m,
|
||||
|
@ -1781,21 +1907,23 @@ WalletService.prototype.createTx = function(opts, cb) {
|
|||
fee: opts.inputs && !_.isNumber(opts.feePerKb) ? opts.fee : null,
|
||||
};
|
||||
|
||||
var txp = Model.TxProposal.create(txOpts);
|
||||
|
||||
self._selectTxInputs(txp, opts.utxosToExclude, function(err) {
|
||||
if (err) return cb(err);
|
||||
|
||||
self.storage.storeAddressAndWallet(wallet, txp.changeAddress, function(err) {
|
||||
if (err) return cb(err);
|
||||
|
||||
self.storage.storeTx(wallet.id, txp, function(err) {
|
||||
if (err) return cb(err);
|
||||
return cb(null, txp);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
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);
|
||||
});
|
||||
});
|
||||
};
|
||||
|
|
|
@ -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() {
|
||||
it('should handle cache argument', function(done) {
|
||||
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) {
|
||||
helpers.stubUtxos(server, wallet, [1, 2], function() {
|
||||
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) {
|
||||
|
@ -3284,7 +3337,6 @@ describe('Wallet service', function() {
|
|||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('should select smaller utxos if within fee constraints', function(done) {
|
||||
helpers.stubUtxos(server, wallet, [1, '800bit', '800bit', '800bit'], function() {
|
||||
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) {
|
||||
helpers.stubUtxos(server, wallet, [].concat(_.times(10, function() {
|
||||
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() {
|
||||
var server, wallet, txid;
|
||||
|
||||
|
|
Loading…
Reference in New Issue