Merge pull request #572 from isocolsky/feat/fee-level
Allow specifying feeLevel on txp creation
This commit is contained in:
commit
db34c557bc
|
@ -46,6 +46,7 @@ TxProposal.create = function(opts) {
|
||||||
x.requiredRejections = Math.min(x.walletM, x.walletN - x.walletM + 1),
|
x.requiredRejections = Math.min(x.walletM, x.walletN - x.walletM + 1),
|
||||||
x.status = 'temporary';
|
x.status = 'temporary';
|
||||||
x.actions = [];
|
x.actions = [];
|
||||||
|
x.feeLevel = opts.feeLevel;
|
||||||
x.feePerKb = opts.feePerKb;
|
x.feePerKb = opts.feePerKb;
|
||||||
x.excludeUnconfirmedUtxos = opts.excludeUnconfirmedUtxos;
|
x.excludeUnconfirmedUtxos = opts.excludeUnconfirmedUtxos;
|
||||||
|
|
||||||
|
@ -98,6 +99,7 @@ TxProposal.fromObj = function(obj) {
|
||||||
});
|
});
|
||||||
x.outputOrder = obj.outputOrder;
|
x.outputOrder = obj.outputOrder;
|
||||||
x.fee = obj.fee;
|
x.fee = obj.fee;
|
||||||
|
x.feeLevel = obj.feeLevel;
|
||||||
x.feePerKb = obj.feePerKb;
|
x.feePerKb = obj.feePerKb;
|
||||||
x.excludeUnconfirmedUtxos = obj.excludeUnconfirmedUtxos;
|
x.excludeUnconfirmedUtxos = obj.excludeUnconfirmedUtxos;
|
||||||
x.addressType = obj.addressType;
|
x.addressType = obj.addressType;
|
||||||
|
|
|
@ -1683,14 +1683,30 @@ WalletService.prototype._validateAndSanitizeTxOpts = function(wallet, opts, cb)
|
||||||
async.series([
|
async.series([
|
||||||
|
|
||||||
function(next) {
|
function(next) {
|
||||||
// feePerKb is required unless inputs & fee are specified
|
var feeArgs = !!opts.feeLevel + _.isNumber(opts.feePerKb) + _.isNumber(opts.fee);
|
||||||
if (!_.isNumber(opts.feePerKb) && !(opts.inputs && _.isNumber(opts.fee)))
|
if (feeArgs > 1)
|
||||||
return next(new ClientError('Required argument missing'));
|
return next(new ClientError('Only one of feeLevel/feePerKb/fee can be specified'));
|
||||||
|
|
||||||
|
if (feeArgs == 0) {
|
||||||
|
log.debug('No fee provided, using "normal" fee level');
|
||||||
|
opts.feeLevel = 'normal';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (opts.feeLevel) {
|
||||||
|
if (!_.any(Defaults.FEE_LEVELS, {
|
||||||
|
name: opts.feeLevel
|
||||||
|
}))
|
||||||
|
return next(new ClientError('Invalid fee level. Valid values are ' + _.pluck(Defaults.FEE_LEVELS, 'name').join(', ')));
|
||||||
|
}
|
||||||
|
|
||||||
if (_.isNumber(opts.feePerKb)) {
|
if (_.isNumber(opts.feePerKb)) {
|
||||||
if (opts.feePerKb < Defaults.MIN_FEE_PER_KB || opts.feePerKb > Defaults.MAX_FEE_PER_KB)
|
if (opts.feePerKb < Defaults.MIN_FEE_PER_KB || opts.feePerKb > Defaults.MAX_FEE_PER_KB)
|
||||||
return next(new ClientError('Invalid fee per KB'));
|
return next(new ClientError('Invalid fee per KB'));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (_.isNumber(opts.fee) && !opts.inputs)
|
||||||
|
return next(new ClientError('fee can only be set when inputs are specified'));
|
||||||
|
|
||||||
next();
|
next();
|
||||||
},
|
},
|
||||||
function(next) {
|
function(next) {
|
||||||
|
@ -1705,7 +1721,7 @@ WalletService.prototype._validateAndSanitizeTxOpts = function(wallet, opts, cb)
|
||||||
if (_.isNumber(opts.outputs[0].amount))
|
if (_.isNumber(opts.outputs[0].amount))
|
||||||
return next(new ClientError('Amount is not allowed when sendMax is specified'));
|
return next(new ClientError('Amount is not allowed when sendMax is specified'));
|
||||||
if (_.isNumber(opts.fee))
|
if (_.isNumber(opts.fee))
|
||||||
return next(new ClientError('Fee is not allowed when sendMax is specified (use feePerKb instead)'));
|
return next(new ClientError('Fee is not allowed when sendMax is specified (use feeLevel/feePerKb instead)'));
|
||||||
|
|
||||||
self.getSendMaxInfo({
|
self.getSendMaxInfo({
|
||||||
feePerKb: opts.feePerKb || Defaults.DEFAULT_FEE_PER_KB,
|
feePerKb: opts.feePerKb || Defaults.DEFAULT_FEE_PER_KB,
|
||||||
|
@ -1738,7 +1754,8 @@ WalletService.prototype._validateAndSanitizeTxOpts = function(wallet, 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 - Use an alternative fee per KB for this TX.
|
* @param {number} opts.feeLevel[='normal'] - Optional. Specify the fee level for this TX ('priority', 'normal', 'economy', 'superEconomy') as defined in Defaults.FEE_LEVELS.
|
||||||
|
* @param {number} opts.feePerKb - Optional. Specify the fee per KB for this TX (in satoshi).
|
||||||
* @param {string} opts.changeAddress - Optional. Use this address as the change address for the tx. The address should belong to the wallet. In the case of singleAddress wallets, the first main address will be used.
|
* @param {string} opts.changeAddress - Optional. Use this address as the change address for the tx. The address should belong to the wallet. In the case of singleAddress wallets, the first main address will be used.
|
||||||
* @param {Boolean} opts.sendMax - Optional. Send maximum amount of funds that make sense under the specified fee/feePerKb conditions. (defaults to false).
|
* @param {Boolean} 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
|
||||||
|
@ -1774,13 +1791,33 @@ WalletService.prototype.createTx = function(opts, cb) {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
|
function getFeePerKb(wallet, cb) {
|
||||||
|
if (opts.inputs && _.isNumber(opts.fee)) return cb();
|
||||||
|
if (_.isNumber(opts.feePerKb)) return cb(null, opts.feePerKb);
|
||||||
|
self.getFeeLevels({
|
||||||
|
network: wallet.network
|
||||||
|
}, function(err, levels) {
|
||||||
|
if (err) return cb(err);
|
||||||
|
var level = _.find(levels, {
|
||||||
|
level: opts.feeLevel
|
||||||
|
});
|
||||||
|
if (!level) {
|
||||||
|
var msg = 'Could not compute fee for "' + opts.feeLevel + '" level';
|
||||||
|
log.error(msg);
|
||||||
|
return cb(new ClientError(msg));
|
||||||
|
}
|
||||||
|
return cb(null, level.feePerKb);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
function checkTxpAlreadyExists(txProposalId, cb) {
|
function checkTxpAlreadyExists(txProposalId, cb) {
|
||||||
if (!txProposalId) return cb();
|
if (!txProposalId) return cb();
|
||||||
self.storage.fetchTx(self.walletId, txProposalId, cb);
|
self.storage.fetchTx(self.walletId, txProposalId, cb);
|
||||||
};
|
};
|
||||||
|
|
||||||
self._runLocked(cb, function(cb) {
|
self._runLocked(cb, function(cb) {
|
||||||
var txp, changeAddress;
|
var txp, changeAddress, feePerKb;
|
||||||
self.getWallet({}, function(err, wallet) {
|
self.getWallet({}, function(err, wallet) {
|
||||||
if (err) return cb(err);
|
if (err) return cb(err);
|
||||||
if (!wallet.isComplete()) return cb(Errors.WALLET_NOT_COMPLETE);
|
if (!wallet.isComplete()) return cb(Errors.WALLET_NOT_COMPLETE);
|
||||||
|
@ -1809,6 +1846,12 @@ WalletService.prototype.createTx = function(opts, cb) {
|
||||||
next();
|
next();
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
function(next) {
|
||||||
|
getFeePerKb(wallet, function(err, fee) {
|
||||||
|
feePerKb = fee;
|
||||||
|
next();
|
||||||
|
});
|
||||||
|
},
|
||||||
function(next) {
|
function(next) {
|
||||||
var txOpts = {
|
var txOpts = {
|
||||||
id: opts.txProposalId,
|
id: opts.txProposalId,
|
||||||
|
@ -1817,7 +1860,8 @@ WalletService.prototype.createTx = function(opts, cb) {
|
||||||
outputs: opts.outputs,
|
outputs: opts.outputs,
|
||||||
message: opts.message,
|
message: opts.message,
|
||||||
changeAddress: changeAddress,
|
changeAddress: changeAddress,
|
||||||
feePerKb: opts.feePerKb,
|
feeLevel: opts.feeLevel,
|
||||||
|
feePerKb: feePerKb,
|
||||||
payProUrl: opts.payProUrl,
|
payProUrl: opts.payProUrl,
|
||||||
walletM: wallet.m,
|
walletM: wallet.m,
|
||||||
walletN: wallet.n,
|
walletN: wallet.n,
|
||||||
|
@ -1853,7 +1897,6 @@ WalletService.prototype.createTx = function(opts, cb) {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
WalletService.prototype._verifyRequestPubKey = function(requestPubKey, signature, xPubKey) {
|
WalletService.prototype._verifyRequestPubKey = function(requestPubKey, signature, xPubKey) {
|
||||||
var pub = (new Bitcore.HDPublicKey(xPubKey)).derive(Constants.PATHS.REQUEST_KEY_AUTH).publicKey;
|
var pub = (new Bitcore.HDPublicKey(xPubKey)).derive(Constants.PATHS.REQUEST_KEY_AUTH).publicKey;
|
||||||
return Utils.verifyMessage(requestPubKey, signature, pub.toString());
|
return Utils.verifyMessage(requestPubKey, signature, pub.toString());
|
||||||
|
|
|
@ -2286,6 +2286,7 @@ describe('Wallet service', function() {
|
||||||
tx.isTemporary().should.equal.true;
|
tx.isTemporary().should.equal.true;
|
||||||
tx.amount.should.equal(helpers.toSatoshi(0.8));
|
tx.amount.should.equal(helpers.toSatoshi(0.8));
|
||||||
tx.feePerKb.should.equal(123e2);
|
tx.feePerKb.should.equal(123e2);
|
||||||
|
should.not.exist(tx.feeLevel);
|
||||||
server.getPendingTxs({}, function(err, txs) {
|
server.getPendingTxs({}, function(err, txs) {
|
||||||
should.not.exist(err);
|
should.not.exist(err);
|
||||||
txs.should.be.empty;
|
txs.should.be.empty;
|
||||||
|
@ -2294,6 +2295,8 @@ describe('Wallet service', function() {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('Validations', function() {
|
||||||
it('should fail to create a tx without outputs', function(done) {
|
it('should fail to create a tx without outputs', function(done) {
|
||||||
helpers.stubUtxos(server, wallet, [1, 2], function() {
|
helpers.stubUtxos(server, wallet, [1, 2], function() {
|
||||||
var txOpts = {
|
var txOpts = {
|
||||||
|
@ -2308,6 +2311,144 @@ describe('Wallet service', function() {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
it('should fail to create tx for invalid address', function(done) {
|
||||||
|
helpers.stubUtxos(server, wallet, 1, function() {
|
||||||
|
var txOpts = {
|
||||||
|
outputs: [{
|
||||||
|
toAddress: 'invalid address',
|
||||||
|
amount: 0.5e8
|
||||||
|
}],
|
||||||
|
feePerKb: 100e2,
|
||||||
|
};
|
||||||
|
server.createTx(txOpts, function(err, tx) {
|
||||||
|
should.exist(err);
|
||||||
|
should.not.exist(tx);
|
||||||
|
// may fail due to Non-base58 character, or Checksum mismatch, or other
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
it('should fail to create tx for address of different network', function(done) {
|
||||||
|
helpers.stubUtxos(server, wallet, 1, function() {
|
||||||
|
var txOpts = {
|
||||||
|
outputs: [{
|
||||||
|
toAddress: 'myE38JHdxmQcTJGP1ZiX4BiGhDxMJDvLJD',
|
||||||
|
amount: 0.5e8
|
||||||
|
}],
|
||||||
|
feePerKb: 100e2,
|
||||||
|
};
|
||||||
|
server.createTx(txOpts, function(err, tx) {
|
||||||
|
should.not.exist(tx);
|
||||||
|
should.exist(err);
|
||||||
|
err.code.should.equal('INCORRECT_ADDRESS_NETWORK');
|
||||||
|
err.message.should.equal('Incorrect address network');
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
it('should fail to create tx for invalid amount', function(done) {
|
||||||
|
var txOpts = {
|
||||||
|
outputs: [{
|
||||||
|
toAddress: '18PzpUFkFZE8zKWUPvfykkTxmB9oMR8qP7',
|
||||||
|
amount: 0,
|
||||||
|
}],
|
||||||
|
feePerKb: 100e2,
|
||||||
|
};
|
||||||
|
server.createTx(txOpts, function(err, tx) {
|
||||||
|
should.not.exist(tx);
|
||||||
|
should.exist(err);
|
||||||
|
err.message.should.equal('Invalid amount');
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
it('should fail to specify both feeLevel & feePerKb', function(done) {
|
||||||
|
helpers.stubUtxos(server, wallet, 2, function() {
|
||||||
|
var txOpts = {
|
||||||
|
outputs: [{
|
||||||
|
toAddress: '18PzpUFkFZE8zKWUPvfykkTxmB9oMR8qP7',
|
||||||
|
amount: 1e8,
|
||||||
|
}],
|
||||||
|
feeLevel: 'normal',
|
||||||
|
feePerKb: 123e2,
|
||||||
|
};
|
||||||
|
server.createTx(txOpts, function(err, txp) {
|
||||||
|
should.exist(err);
|
||||||
|
should.not.exist(txp);
|
||||||
|
err.toString().should.contain('Only one of feeLevel/feePerKb');
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
it('should be able to create tx with inputs argument', function(done) {
|
||||||
|
helpers.stubUtxos(server, wallet, [1, 3, 2], function(utxos) {
|
||||||
|
server.getUtxos({}, function(err, utxos) {
|
||||||
|
should.not.exist(err);
|
||||||
|
var inputs = [utxos[0], utxos[2]];
|
||||||
|
var txOpts = {
|
||||||
|
outputs: [{
|
||||||
|
toAddress: '18PzpUFkFZE8zKWUPvfykkTxmB9oMR8qP7',
|
||||||
|
amount: 2.5e8,
|
||||||
|
}],
|
||||||
|
feePerKb: 100e2,
|
||||||
|
inputs: inputs,
|
||||||
|
};
|
||||||
|
server.createTx(txOpts, function(err, tx) {
|
||||||
|
should.not.exist(err);
|
||||||
|
should.exist(tx);
|
||||||
|
tx.inputs.length.should.equal(2);
|
||||||
|
var txids = _.pluck(tx.inputs, 'txid');
|
||||||
|
txids.should.contain(utxos[0].txid);
|
||||||
|
txids.should.contain(utxos[2].txid);
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
it('should be able to specify change address', function(done) {
|
||||||
|
helpers.stubUtxos(server, wallet, [1, 2], function(utxos) {
|
||||||
|
var txOpts = {
|
||||||
|
outputs: [{
|
||||||
|
toAddress: '18PzpUFkFZE8zKWUPvfykkTxmB9oMR8qP7',
|
||||||
|
amount: 0.8e8,
|
||||||
|
}],
|
||||||
|
feePerKb: 100e2,
|
||||||
|
changeAddress: utxos[0].address,
|
||||||
|
};
|
||||||
|
server.createTx(txOpts, function(err, tx) {
|
||||||
|
should.not.exist(err);
|
||||||
|
should.exist(tx);
|
||||||
|
var t = tx.getBitcoreTx();
|
||||||
|
t.getChangeOutput().script.toAddress().toString().should.equal(txOpts.changeAddress);
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
it('should be able to specify inputs & absolute fee', function(done) {
|
||||||
|
helpers.stubUtxos(server, wallet, [1, 2], function(utxos) {
|
||||||
|
var txOpts = {
|
||||||
|
outputs: [{
|
||||||
|
toAddress: '18PzpUFkFZE8zKWUPvfykkTxmB9oMR8qP7',
|
||||||
|
amount: 0.8e8,
|
||||||
|
}],
|
||||||
|
inputs: utxos,
|
||||||
|
fee: 1000e2,
|
||||||
|
};
|
||||||
|
server.createTx(txOpts, function(err, tx) {
|
||||||
|
should.not.exist(err);
|
||||||
|
should.exist(tx);
|
||||||
|
tx.amount.should.equal(helpers.toSatoshi(0.8));
|
||||||
|
should.not.exist(tx.feePerKb);
|
||||||
|
tx.fee.should.equal(1000e2);
|
||||||
|
var t = tx.getBitcoreTx();
|
||||||
|
t.getFee().should.equal(1000e2);
|
||||||
|
t.getChangeOutput().satoshis.should.equal(3e8 - 0.8e8 - 1000e2);
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Foreign ID', function() {
|
||||||
it('should create a tx with foreign ID', function(done) {
|
it('should create a tx with foreign ID', function(done) {
|
||||||
helpers.stubUtxos(server, wallet, 2, function() {
|
helpers.stubUtxos(server, wallet, 2, function() {
|
||||||
var txOpts = {
|
var txOpts = {
|
||||||
|
@ -2387,6 +2528,9 @@ describe('Wallet service', function() {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Publishing', function() {
|
||||||
it('should be able to publish a temporary tx proposal', function(done) {
|
it('should be able to publish a temporary tx proposal', function(done) {
|
||||||
helpers.stubUtxos(server, wallet, [1, 2], function() {
|
helpers.stubUtxos(server, wallet, [1, 2], function() {
|
||||||
var txOpts = {
|
var txOpts = {
|
||||||
|
@ -2440,288 +2584,6 @@ describe('Wallet service', function() {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
it('should generate new change address for each created tx', function(done) {
|
|
||||||
helpers.stubUtxos(server, wallet, [1, 2], function() {
|
|
||||||
var txOpts = {
|
|
||||||
outputs: [{
|
|
||||||
toAddress: '18PzpUFkFZE8zKWUPvfykkTxmB9oMR8qP7',
|
|
||||||
amount: 0.8e8,
|
|
||||||
}],
|
|
||||||
feePerKb: 100e2,
|
|
||||||
};
|
|
||||||
server.createTx(txOpts, function(err, tx1) {
|
|
||||||
should.not.exist(err);
|
|
||||||
should.exist(tx1);
|
|
||||||
server.createTx(txOpts, function(err, tx2) {
|
|
||||||
should.not.exist(err);
|
|
||||||
should.exist(tx2);
|
|
||||||
tx1.changeAddress.address.should.not.equal(tx2.changeAddress.address);
|
|
||||||
done();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
it('should support creating a tx with no change address', function(done) {
|
|
||||||
helpers.stubUtxos(server, wallet, [1, 2], function() {
|
|
||||||
var max = 3e8 - 7000; // Fees for this tx at 100bits/kB = 7000 sat
|
|
||||||
var txOpts = {
|
|
||||||
outputs: [{
|
|
||||||
toAddress: '18PzpUFkFZE8zKWUPvfykkTxmB9oMR8qP7',
|
|
||||||
amount: max,
|
|
||||||
}],
|
|
||||||
feePerKb: 100e2,
|
|
||||||
};
|
|
||||||
server.createTx(txOpts, function(err, txp) {
|
|
||||||
should.not.exist(err);
|
|
||||||
should.exist(txp);
|
|
||||||
var t = txp.getBitcoreTx().toObject();
|
|
||||||
t.outputs.length.should.equal(1);
|
|
||||||
t.outputs[0].satoshis.should.equal(max);
|
|
||||||
done();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
it('should fail gracefully if unable to reach the blockchain', function(done) {
|
|
||||||
blockchainExplorer.getUtxos = sinon.stub().callsArgWith(1, 'dummy error');
|
|
||||||
server.createAddress({}, function(err, address) {
|
|
||||||
should.not.exist(err);
|
|
||||||
var txOpts = {
|
|
||||||
outputs: [{
|
|
||||||
toAddress: '18PzpUFkFZE8zKWUPvfykkTxmB9oMR8qP7',
|
|
||||||
amount: 1e8
|
|
||||||
}],
|
|
||||||
feePerKb: 100e2,
|
|
||||||
};
|
|
||||||
server.createTx(txOpts, function(err, tx) {
|
|
||||||
should.exist(err);
|
|
||||||
err.toString().should.equal('dummy error');
|
|
||||||
done();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
it('should fail to create tx for invalid address', function(done) {
|
|
||||||
helpers.stubUtxos(server, wallet, 1, function() {
|
|
||||||
var txOpts = {
|
|
||||||
outputs: [{
|
|
||||||
toAddress: 'invalid address',
|
|
||||||
amount: 0.5e8
|
|
||||||
}],
|
|
||||||
feePerKb: 100e2,
|
|
||||||
};
|
|
||||||
server.createTx(txOpts, function(err, tx) {
|
|
||||||
should.exist(err);
|
|
||||||
should.not.exist(tx);
|
|
||||||
// may fail due to Non-base58 character, or Checksum mismatch, or other
|
|
||||||
done();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
it('should fail to create tx for address of different network', function(done) {
|
|
||||||
helpers.stubUtxos(server, wallet, 1, function() {
|
|
||||||
var txOpts = {
|
|
||||||
outputs: [{
|
|
||||||
toAddress: 'myE38JHdxmQcTJGP1ZiX4BiGhDxMJDvLJD',
|
|
||||||
amount: 0.5e8
|
|
||||||
}],
|
|
||||||
feePerKb: 100e2,
|
|
||||||
};
|
|
||||||
server.createTx(txOpts, function(err, tx) {
|
|
||||||
should.not.exist(tx);
|
|
||||||
should.exist(err);
|
|
||||||
err.code.should.equal('INCORRECT_ADDRESS_NETWORK');
|
|
||||||
err.message.should.equal('Incorrect address network');
|
|
||||||
done();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
it('should fail to create tx for invalid amount', function(done) {
|
|
||||||
var txOpts = {
|
|
||||||
outputs: [{
|
|
||||||
toAddress: '18PzpUFkFZE8zKWUPvfykkTxmB9oMR8qP7',
|
|
||||||
amount: 0,
|
|
||||||
}],
|
|
||||||
feePerKb: 100e2,
|
|
||||||
};
|
|
||||||
server.createTx(txOpts, function(err, tx) {
|
|
||||||
should.not.exist(tx);
|
|
||||||
should.exist(err);
|
|
||||||
err.message.should.equal('Invalid amount');
|
|
||||||
done();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
it('should fail to create a tx exceeding max size in kb', function(done) {
|
|
||||||
var _oldDefault = Defaults.MAX_TX_SIZE_IN_KB;
|
|
||||||
Defaults.MAX_TX_SIZE_IN_KB = 1;
|
|
||||||
helpers.stubUtxos(server, wallet, _.range(1, 10, 0), function() {
|
|
||||||
var txOpts = {
|
|
||||||
outputs: [{
|
|
||||||
toAddress: '18PzpUFkFZE8zKWUPvfykkTxmB9oMR8qP7',
|
|
||||||
amount: 8e8,
|
|
||||||
}],
|
|
||||||
feePerKb: 100e2,
|
|
||||||
};
|
|
||||||
server.createTx(txOpts, function(err, tx) {
|
|
||||||
should.exist(err);
|
|
||||||
err.code.should.equal('TX_MAX_SIZE_EXCEEDED');
|
|
||||||
Defaults.MAX_TX_SIZE_IN_KB = _oldDefault;
|
|
||||||
done();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
it('should fail with different error for insufficient funds and locked funds', function(done) {
|
|
||||||
helpers.stubUtxos(server, wallet, [1, 1], function() {
|
|
||||||
var txOpts = {
|
|
||||||
outputs: [{
|
|
||||||
toAddress: '18PzpUFkFZE8zKWUPvfykkTxmB9oMR8qP7',
|
|
||||||
amount: 1.1e8,
|
|
||||||
}],
|
|
||||||
feePerKb: 100e2,
|
|
||||||
};
|
|
||||||
helpers.createAndPublishTx(server, txOpts, TestData.copayers[0].privKey_1H_0, function(tx) {
|
|
||||||
server.getBalance({}, function(err, balance) {
|
|
||||||
should.not.exist(err);
|
|
||||||
balance.totalAmount.should.equal(2e8);
|
|
||||||
balance.lockedAmount.should.equal(2e8);
|
|
||||||
txOpts.outputs[0].amount = 0.8e8;
|
|
||||||
server.createTx(txOpts, function(err, tx) {
|
|
||||||
should.exist(err);
|
|
||||||
err.code.should.equal('LOCKED_FUNDS');
|
|
||||||
err.message.should.equal('Funds are locked by pending transaction proposals');
|
|
||||||
done();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
it('should create tx with 0 change output', function(done) {
|
|
||||||
helpers.stubUtxos(server, wallet, 1, function() {
|
|
||||||
var fee = 4100; // The exact fee of the resulting tx
|
|
||||||
var amount = 1e8 - fee;
|
|
||||||
|
|
||||||
var txOpts = {
|
|
||||||
outputs: [{
|
|
||||||
toAddress: '18PzpUFkFZE8zKWUPvfykkTxmB9oMR8qP7',
|
|
||||||
amount: amount,
|
|
||||||
}],
|
|
||||||
feePerKb: 100e2,
|
|
||||||
};
|
|
||||||
server.createTx(txOpts, function(err, tx) {
|
|
||||||
should.not.exist(err);
|
|
||||||
should.exist(tx);
|
|
||||||
var bitcoreTx = tx.getBitcoreTx();
|
|
||||||
bitcoreTx.outputs.length.should.equal(1);
|
|
||||||
bitcoreTx.outputs[0].satoshis.should.equal(tx.amount);
|
|
||||||
done();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
it('should fail gracefully when bitcore throws exception on raw tx creation', function(done) {
|
|
||||||
helpers.stubUtxos(server, wallet, 1, function() {
|
|
||||||
var bitcoreStub = sinon.stub(Bitcore, 'Transaction');
|
|
||||||
bitcoreStub.throws({
|
|
||||||
name: 'dummy',
|
|
||||||
message: 'dummy exception'
|
|
||||||
});
|
|
||||||
var txOpts = {
|
|
||||||
outputs: [{
|
|
||||||
toAddress: '18PzpUFkFZE8zKWUPvfykkTxmB9oMR8qP7',
|
|
||||||
amount: 0.5e8,
|
|
||||||
}],
|
|
||||||
feePerKb: 100e2,
|
|
||||||
};
|
|
||||||
server.createTx(txOpts, function(err, tx) {
|
|
||||||
should.exist(err);
|
|
||||||
err.message.should.equal('dummy exception');
|
|
||||||
bitcoreStub.restore();
|
|
||||||
done();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
it('should create tx when there is a pending tx and enough UTXOs', function(done) {
|
|
||||||
helpers.stubUtxos(server, wallet, [1.1, 1.2, 1.3], function() {
|
|
||||||
var txOpts = {
|
|
||||||
outputs: [{
|
|
||||||
toAddress: '18PzpUFkFZE8zKWUPvfykkTxmB9oMR8qP7',
|
|
||||||
amount: 1.5e8,
|
|
||||||
}],
|
|
||||||
feePerKb: 100e2,
|
|
||||||
};
|
|
||||||
helpers.createAndPublishTx(server, txOpts, TestData.copayers[0].privKey_1H_0, function(tx) {
|
|
||||||
should.exist(tx);
|
|
||||||
txOpts.outputs[0].amount = 0.8e8;
|
|
||||||
helpers.createAndPublishTx(server, txOpts, TestData.copayers[0].privKey_1H_0, function(tx) {
|
|
||||||
should.exist(tx);
|
|
||||||
server.getPendingTxs({}, function(err, txs) {
|
|
||||||
should.not.exist(err);
|
|
||||||
txs.length.should.equal(2);
|
|
||||||
server.getBalance({}, function(err, balance) {
|
|
||||||
should.not.exist(err);
|
|
||||||
balance.totalAmount.should.equal(3.6e8);
|
|
||||||
balance.lockedAmount.should.equal(3.6e8);
|
|
||||||
done();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
it('should fail to create tx when there is a pending tx and not enough UTXOs', function(done) {
|
|
||||||
helpers.stubUtxos(server, wallet, [1.1, 1.2, 1.3], function() {
|
|
||||||
var txOpts = {
|
|
||||||
outputs: [{
|
|
||||||
toAddress: '18PzpUFkFZE8zKWUPvfykkTxmB9oMR8qP7',
|
|
||||||
amount: 1.5e8,
|
|
||||||
}],
|
|
||||||
feePerKb: 100e2,
|
|
||||||
};
|
|
||||||
helpers.createAndPublishTx(server, txOpts, TestData.copayers[0].privKey_1H_0, function(tx) {
|
|
||||||
should.exist(tx);
|
|
||||||
txOpts.outputs[0].amount = 1.8e8;
|
|
||||||
server.createTx(txOpts, function(err, tx) {
|
|
||||||
err.code.should.equal('LOCKED_FUNDS');
|
|
||||||
should.not.exist(tx);
|
|
||||||
server.getPendingTxs({}, function(err, txs) {
|
|
||||||
should.not.exist(err);
|
|
||||||
txs.length.should.equal(1);
|
|
||||||
server.getBalance({}, function(err, balance) {
|
|
||||||
should.not.exist(err);
|
|
||||||
balance.totalAmount.should.equal(3.6e8);
|
|
||||||
var amountInputs = _.sum(txs[0].inputs, 'satoshis');
|
|
||||||
balance.lockedAmount.should.equal(amountInputs);
|
|
||||||
balance.lockedAmount.should.be.below(balance.totalAmount);
|
|
||||||
balance.availableAmount.should.equal(balance.totalAmount - balance.lockedAmount);
|
|
||||||
done();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
it('should be able to create tx with inputs argument', function(done) {
|
|
||||||
helpers.stubUtxos(server, wallet, [1, 3, 2], function(utxos) {
|
|
||||||
server.getUtxos({}, function(err, utxos) {
|
|
||||||
should.not.exist(err);
|
|
||||||
var inputs = [utxos[0], utxos[2]];
|
|
||||||
var txOpts = {
|
|
||||||
outputs: [{
|
|
||||||
toAddress: '18PzpUFkFZE8zKWUPvfykkTxmB9oMR8qP7',
|
|
||||||
amount: 2.5e8,
|
|
||||||
}],
|
|
||||||
feePerKb: 100e2,
|
|
||||||
inputs: inputs,
|
|
||||||
};
|
|
||||||
server.createTx(txOpts, function(err, tx) {
|
|
||||||
should.not.exist(err);
|
|
||||||
should.exist(tx);
|
|
||||||
tx.inputs.length.should.equal(2);
|
|
||||||
var txids = _.pluck(tx.inputs, 'txid');
|
|
||||||
txids.should.contain(utxos[0].txid);
|
|
||||||
txids.should.contain(utxos[2].txid);
|
|
||||||
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 = {
|
||||||
|
@ -2815,55 +2677,6 @@ describe('Wallet service', function() {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
it('should accept a tx proposal signed with a custom key', function(done) {
|
|
||||||
var reqPrivKey = new Bitcore.PrivateKey();
|
|
||||||
var reqPubKey = reqPrivKey.toPublicKey().toString();
|
|
||||||
|
|
||||||
var xPrivKey = TestData.copayers[0].xPrivKey_44H_0H_0H;
|
|
||||||
|
|
||||||
var accessOpts = {
|
|
||||||
copayerId: TestData.copayers[0].id44,
|
|
||||||
requestPubKey: reqPubKey,
|
|
||||||
signature: helpers.signRequestPubKey(reqPubKey, xPrivKey),
|
|
||||||
};
|
|
||||||
|
|
||||||
server.addAccess(accessOpts, function(err) {
|
|
||||||
should.not.exist(err);
|
|
||||||
|
|
||||||
helpers.stubUtxos(server, wallet, [1, 2], function() {
|
|
||||||
var txOpts = {
|
|
||||||
outputs: [{
|
|
||||||
toAddress: '18PzpUFkFZE8zKWUPvfykkTxmB9oMR8qP7',
|
|
||||||
amount: 0.8 * 1e8,
|
|
||||||
}],
|
|
||||||
message: 'some message',
|
|
||||||
feePerKb: 100e2,
|
|
||||||
};
|
|
||||||
server.createTx(txOpts, function(err, txp) {
|
|
||||||
should.not.exist(err);
|
|
||||||
should.exist(txp);
|
|
||||||
|
|
||||||
var publishOpts = {
|
|
||||||
txProposalId: txp.id,
|
|
||||||
proposalSignature: helpers.signMessage(txp.getRawTx(), reqPrivKey),
|
|
||||||
}
|
|
||||||
|
|
||||||
server.publishTx(publishOpts, function(err) {
|
|
||||||
should.not.exist(err);
|
|
||||||
server.getTx({
|
|
||||||
txProposalId: txp.id
|
|
||||||
}, function(err, x) {
|
|
||||||
should.not.exist(err);
|
|
||||||
x.proposalSignature.should.equal(publishOpts.proposalSignature);
|
|
||||||
x.proposalSignaturePubKey.should.equal(accessOpts.requestPubKey);
|
|
||||||
x.proposalSignaturePubKeySig.should.equal(accessOpts.signature);
|
|
||||||
done();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
it('should fail to publish a temporary tx proposal if utxos are unavailable', function(done) {
|
it('should fail to publish a temporary tx proposal if utxos are unavailable', function(done) {
|
||||||
var txp1, txp2;
|
var txp1, txp2;
|
||||||
var txOpts = {
|
var txOpts = {
|
||||||
|
@ -2932,48 +2745,347 @@ describe('Wallet service', function() {
|
||||||
done();
|
done();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
it('should be able to specify change address', function(done) {
|
});
|
||||||
helpers.stubUtxos(server, wallet, [1, 2], function(utxos) {
|
|
||||||
|
describe('Fee levels', function() {
|
||||||
|
it('should create a tx specifying feeLevel', function(done) {
|
||||||
|
helpers.stubFeeLevels({
|
||||||
|
1: 400e2,
|
||||||
|
2: 200e2,
|
||||||
|
6: 180e2,
|
||||||
|
24: 90e2,
|
||||||
|
});
|
||||||
|
helpers.stubUtxos(server, wallet, 2, function() {
|
||||||
|
var txOpts = {
|
||||||
|
outputs: [{
|
||||||
|
toAddress: '18PzpUFkFZE8zKWUPvfykkTxmB9oMR8qP7',
|
||||||
|
amount: 1e8,
|
||||||
|
}],
|
||||||
|
feeLevel: 'economy',
|
||||||
|
};
|
||||||
|
server.createTx(txOpts, function(err, txp) {
|
||||||
|
should.not.exist(err);
|
||||||
|
should.exist(txp);
|
||||||
|
txp.feePerKb.should.equal(180e2);
|
||||||
|
txp.feeLevel.should.equal('economy');
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
it('should fail if the specified fee level does not exist', function(done) {
|
||||||
|
helpers.stubUtxos(server, wallet, 2, function() {
|
||||||
|
var txOpts = {
|
||||||
|
outputs: [{
|
||||||
|
toAddress: '18PzpUFkFZE8zKWUPvfykkTxmB9oMR8qP7',
|
||||||
|
amount: 1e8,
|
||||||
|
}],
|
||||||
|
feeLevel: 'madeUpLevel',
|
||||||
|
};
|
||||||
|
server.createTx(txOpts, function(err, txp) {
|
||||||
|
should.exist(err);
|
||||||
|
should.not.exist(txp);
|
||||||
|
err.toString().should.contain('Invalid fee level');
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
it('should assume "normal" fee level if no feeLevel and no feePerKb/fee is specified', function(done) {
|
||||||
|
helpers.stubFeeLevels({
|
||||||
|
1: 400e2,
|
||||||
|
2: 200e2,
|
||||||
|
6: 180e2,
|
||||||
|
24: 90e2,
|
||||||
|
});
|
||||||
|
helpers.stubUtxos(server, wallet, 2, function() {
|
||||||
|
var txOpts = {
|
||||||
|
outputs: [{
|
||||||
|
toAddress: '18PzpUFkFZE8zKWUPvfykkTxmB9oMR8qP7',
|
||||||
|
amount: 1e8,
|
||||||
|
}],
|
||||||
|
};
|
||||||
|
server.createTx(txOpts, function(err, txp) {
|
||||||
|
should.not.exist(err);
|
||||||
|
should.exist(txp);
|
||||||
|
txp.feePerKb.should.equal(200e2);
|
||||||
|
txp.feeLevel.should.equal('normal');
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
it('should generate new change address for each created tx', function(done) {
|
||||||
|
helpers.stubUtxos(server, wallet, [1, 2], function() {
|
||||||
var txOpts = {
|
var txOpts = {
|
||||||
outputs: [{
|
outputs: [{
|
||||||
toAddress: '18PzpUFkFZE8zKWUPvfykkTxmB9oMR8qP7',
|
toAddress: '18PzpUFkFZE8zKWUPvfykkTxmB9oMR8qP7',
|
||||||
amount: 0.8e8,
|
amount: 0.8e8,
|
||||||
}],
|
}],
|
||||||
feePerKb: 100e2,
|
feePerKb: 100e2,
|
||||||
changeAddress: utxos[0].address,
|
|
||||||
};
|
};
|
||||||
server.createTx(txOpts, function(err, tx) {
|
server.createTx(txOpts, function(err, tx1) {
|
||||||
should.not.exist(err);
|
should.not.exist(err);
|
||||||
should.exist(tx);
|
should.exist(tx1);
|
||||||
var t = tx.getBitcoreTx();
|
server.createTx(txOpts, function(err, tx2) {
|
||||||
t.getChangeOutput().script.toAddress().toString().should.equal(txOpts.changeAddress);
|
should.not.exist(err);
|
||||||
|
should.exist(tx2);
|
||||||
|
tx1.changeAddress.address.should.not.equal(tx2.changeAddress.address);
|
||||||
done();
|
done();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
it('should be able to specify inputs & absolute fee', function(done) {
|
});
|
||||||
helpers.stubUtxos(server, wallet, [1, 2], function(utxos) {
|
it('should support creating a tx with no change address', function(done) {
|
||||||
|
helpers.stubUtxos(server, wallet, [1, 2], function() {
|
||||||
|
var max = 3e8 - 7000; // Fees for this tx at 100bits/kB = 7000 sat
|
||||||
var txOpts = {
|
var txOpts = {
|
||||||
outputs: [{
|
outputs: [{
|
||||||
toAddress: '18PzpUFkFZE8zKWUPvfykkTxmB9oMR8qP7',
|
toAddress: '18PzpUFkFZE8zKWUPvfykkTxmB9oMR8qP7',
|
||||||
amount: 0.8e8,
|
amount: max,
|
||||||
}],
|
}],
|
||||||
inputs: utxos,
|
feePerKb: 100e2,
|
||||||
fee: 1000e2,
|
};
|
||||||
|
server.createTx(txOpts, function(err, txp) {
|
||||||
|
should.not.exist(err);
|
||||||
|
should.exist(txp);
|
||||||
|
var t = txp.getBitcoreTx().toObject();
|
||||||
|
t.outputs.length.should.equal(1);
|
||||||
|
t.outputs[0].satoshis.should.equal(max);
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
it('should fail gracefully if unable to reach the blockchain', function(done) {
|
||||||
|
blockchainExplorer.getUtxos = sinon.stub().callsArgWith(1, 'dummy error');
|
||||||
|
server.createAddress({}, function(err, address) {
|
||||||
|
should.not.exist(err);
|
||||||
|
var txOpts = {
|
||||||
|
outputs: [{
|
||||||
|
toAddress: '18PzpUFkFZE8zKWUPvfykkTxmB9oMR8qP7',
|
||||||
|
amount: 1e8
|
||||||
|
}],
|
||||||
|
feePerKb: 100e2,
|
||||||
|
};
|
||||||
|
server.createTx(txOpts, function(err, tx) {
|
||||||
|
should.exist(err);
|
||||||
|
err.toString().should.equal('dummy error');
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
it('should fail gracefully when bitcore throws exception on raw tx creation', function(done) {
|
||||||
|
helpers.stubUtxos(server, wallet, 1, function() {
|
||||||
|
var bitcoreStub = sinon.stub(Bitcore, 'Transaction');
|
||||||
|
bitcoreStub.throws({
|
||||||
|
name: 'dummy',
|
||||||
|
message: 'dummy exception'
|
||||||
|
});
|
||||||
|
var txOpts = {
|
||||||
|
outputs: [{
|
||||||
|
toAddress: '18PzpUFkFZE8zKWUPvfykkTxmB9oMR8qP7',
|
||||||
|
amount: 0.5e8,
|
||||||
|
}],
|
||||||
|
feePerKb: 100e2,
|
||||||
|
};
|
||||||
|
server.createTx(txOpts, function(err, tx) {
|
||||||
|
should.exist(err);
|
||||||
|
err.message.should.equal('dummy exception');
|
||||||
|
bitcoreStub.restore();
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
it('should fail to create a tx exceeding max size in kb', function(done) {
|
||||||
|
var _oldDefault = Defaults.MAX_TX_SIZE_IN_KB;
|
||||||
|
Defaults.MAX_TX_SIZE_IN_KB = 1;
|
||||||
|
helpers.stubUtxos(server, wallet, _.range(1, 10, 0), function() {
|
||||||
|
var txOpts = {
|
||||||
|
outputs: [{
|
||||||
|
toAddress: '18PzpUFkFZE8zKWUPvfykkTxmB9oMR8qP7',
|
||||||
|
amount: 8e8,
|
||||||
|
}],
|
||||||
|
feePerKb: 100e2,
|
||||||
|
};
|
||||||
|
server.createTx(txOpts, function(err, tx) {
|
||||||
|
should.exist(err);
|
||||||
|
err.code.should.equal('TX_MAX_SIZE_EXCEEDED');
|
||||||
|
Defaults.MAX_TX_SIZE_IN_KB = _oldDefault;
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
it('should fail with different error for insufficient funds and locked funds', function(done) {
|
||||||
|
helpers.stubUtxos(server, wallet, [1, 1], function() {
|
||||||
|
var txOpts = {
|
||||||
|
outputs: [{
|
||||||
|
toAddress: '18PzpUFkFZE8zKWUPvfykkTxmB9oMR8qP7',
|
||||||
|
amount: 1.1e8,
|
||||||
|
}],
|
||||||
|
feePerKb: 100e2,
|
||||||
|
};
|
||||||
|
helpers.createAndPublishTx(server, txOpts, TestData.copayers[0].privKey_1H_0, function(tx) {
|
||||||
|
server.getBalance({}, function(err, balance) {
|
||||||
|
should.not.exist(err);
|
||||||
|
balance.totalAmount.should.equal(2e8);
|
||||||
|
balance.lockedAmount.should.equal(2e8);
|
||||||
|
txOpts.outputs[0].amount = 0.8e8;
|
||||||
|
server.createTx(txOpts, function(err, tx) {
|
||||||
|
should.exist(err);
|
||||||
|
err.code.should.equal('LOCKED_FUNDS');
|
||||||
|
err.message.should.equal('Funds are locked by pending transaction proposals');
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
it('should fail to create tx for dust amount in outputs', function(done) {
|
||||||
|
helpers.stubUtxos(server, wallet, 1, function() {
|
||||||
|
var txOpts = {
|
||||||
|
outputs: [{
|
||||||
|
toAddress: '18PzpUFkFZE8zKWUPvfykkTxmB9oMR8qP7',
|
||||||
|
amount: 20e2,
|
||||||
|
}],
|
||||||
|
feePerKb: 100e2,
|
||||||
|
};
|
||||||
|
server.createTx(txOpts, function(err, tx) {
|
||||||
|
should.exist(err);
|
||||||
|
err.code.should.equal('DUST_AMOUNT');
|
||||||
|
err.message.should.equal('Amount below dust threshold');
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
it('should create tx with 0 change output', function(done) {
|
||||||
|
helpers.stubUtxos(server, wallet, 1, function() {
|
||||||
|
var fee = 4100; // The exact fee of the resulting tx
|
||||||
|
var amount = 1e8 - fee;
|
||||||
|
|
||||||
|
var txOpts = {
|
||||||
|
outputs: [{
|
||||||
|
toAddress: '18PzpUFkFZE8zKWUPvfykkTxmB9oMR8qP7',
|
||||||
|
amount: amount,
|
||||||
|
}],
|
||||||
|
feePerKb: 100e2,
|
||||||
};
|
};
|
||||||
server.createTx(txOpts, function(err, tx) {
|
server.createTx(txOpts, function(err, tx) {
|
||||||
should.not.exist(err);
|
should.not.exist(err);
|
||||||
should.exist(tx);
|
should.exist(tx);
|
||||||
tx.amount.should.equal(helpers.toSatoshi(0.8));
|
var bitcoreTx = tx.getBitcoreTx();
|
||||||
should.not.exist(tx.feePerKb);
|
bitcoreTx.outputs.length.should.equal(1);
|
||||||
tx.fee.should.equal(1000e2);
|
bitcoreTx.outputs[0].satoshis.should.equal(tx.amount);
|
||||||
var t = tx.getBitcoreTx();
|
|
||||||
t.getFee().should.equal(1000e2);
|
|
||||||
t.getChangeOutput().satoshis.should.equal(3e8 - 0.8e8 - 1000e2);
|
|
||||||
done();
|
done();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
it('should create tx when there is a pending tx and enough UTXOs', function(done) {
|
||||||
|
helpers.stubUtxos(server, wallet, [1.1, 1.2, 1.3], function() {
|
||||||
|
var txOpts = {
|
||||||
|
outputs: [{
|
||||||
|
toAddress: '18PzpUFkFZE8zKWUPvfykkTxmB9oMR8qP7',
|
||||||
|
amount: 1.5e8,
|
||||||
|
}],
|
||||||
|
feePerKb: 100e2,
|
||||||
|
};
|
||||||
|
helpers.createAndPublishTx(server, txOpts, TestData.copayers[0].privKey_1H_0, function(tx) {
|
||||||
|
should.exist(tx);
|
||||||
|
txOpts.outputs[0].amount = 0.8e8;
|
||||||
|
helpers.createAndPublishTx(server, txOpts, TestData.copayers[0].privKey_1H_0, function(tx) {
|
||||||
|
should.exist(tx);
|
||||||
|
server.getPendingTxs({}, function(err, txs) {
|
||||||
|
should.not.exist(err);
|
||||||
|
txs.length.should.equal(2);
|
||||||
|
server.getBalance({}, function(err, balance) {
|
||||||
|
should.not.exist(err);
|
||||||
|
balance.totalAmount.should.equal(3.6e8);
|
||||||
|
balance.lockedAmount.should.equal(3.6e8);
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
it('should fail to create tx when there is a pending tx and not enough UTXOs', function(done) {
|
||||||
|
helpers.stubUtxos(server, wallet, [1.1, 1.2, 1.3], function() {
|
||||||
|
var txOpts = {
|
||||||
|
outputs: [{
|
||||||
|
toAddress: '18PzpUFkFZE8zKWUPvfykkTxmB9oMR8qP7',
|
||||||
|
amount: 1.5e8,
|
||||||
|
}],
|
||||||
|
feePerKb: 100e2,
|
||||||
|
};
|
||||||
|
helpers.createAndPublishTx(server, txOpts, TestData.copayers[0].privKey_1H_0, function(tx) {
|
||||||
|
should.exist(tx);
|
||||||
|
txOpts.outputs[0].amount = 1.8e8;
|
||||||
|
server.createTx(txOpts, function(err, tx) {
|
||||||
|
err.code.should.equal('LOCKED_FUNDS');
|
||||||
|
should.not.exist(tx);
|
||||||
|
server.getPendingTxs({}, function(err, txs) {
|
||||||
|
should.not.exist(err);
|
||||||
|
txs.length.should.equal(1);
|
||||||
|
server.getBalance({}, function(err, balance) {
|
||||||
|
should.not.exist(err);
|
||||||
|
balance.totalAmount.should.equal(3.6e8);
|
||||||
|
var amountInputs = _.sum(txs[0].inputs, 'satoshis');
|
||||||
|
balance.lockedAmount.should.equal(amountInputs);
|
||||||
|
balance.lockedAmount.should.be.below(balance.totalAmount);
|
||||||
|
balance.availableAmount.should.equal(balance.totalAmount - balance.lockedAmount);
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
it('should accept a tx proposal signed with a custom key', function(done) {
|
||||||
|
var reqPrivKey = new Bitcore.PrivateKey();
|
||||||
|
var reqPubKey = reqPrivKey.toPublicKey().toString();
|
||||||
|
|
||||||
|
var xPrivKey = TestData.copayers[0].xPrivKey_44H_0H_0H;
|
||||||
|
|
||||||
|
var accessOpts = {
|
||||||
|
copayerId: TestData.copayers[0].id44,
|
||||||
|
requestPubKey: reqPubKey,
|
||||||
|
signature: helpers.signRequestPubKey(reqPubKey, xPrivKey),
|
||||||
|
};
|
||||||
|
|
||||||
|
server.addAccess(accessOpts, function(err) {
|
||||||
|
should.not.exist(err);
|
||||||
|
|
||||||
|
helpers.stubUtxos(server, wallet, [1, 2], function() {
|
||||||
|
var txOpts = {
|
||||||
|
outputs: [{
|
||||||
|
toAddress: '18PzpUFkFZE8zKWUPvfykkTxmB9oMR8qP7',
|
||||||
|
amount: 0.8 * 1e8,
|
||||||
|
}],
|
||||||
|
message: 'some message',
|
||||||
|
feePerKb: 100e2,
|
||||||
|
};
|
||||||
|
server.createTx(txOpts, function(err, txp) {
|
||||||
|
should.not.exist(err);
|
||||||
|
should.exist(txp);
|
||||||
|
|
||||||
|
var publishOpts = {
|
||||||
|
txProposalId: txp.id,
|
||||||
|
proposalSignature: helpers.signMessage(txp.getRawTx(), reqPrivKey),
|
||||||
|
}
|
||||||
|
|
||||||
|
server.publishTx(publishOpts, function(err) {
|
||||||
|
should.not.exist(err);
|
||||||
|
server.getTx({
|
||||||
|
txProposalId: txp.id
|
||||||
|
}, function(err, x) {
|
||||||
|
should.not.exist(err);
|
||||||
|
x.proposalSignature.should.equal(publishOpts.proposalSignature);
|
||||||
|
x.proposalSignaturePubKey.should.equal(accessOpts.requestPubKey);
|
||||||
|
x.proposalSignaturePubKeySig.should.equal(accessOpts.signature);
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
it('should be able to send max funds', function(done) {
|
it('should be able to send max funds', function(done) {
|
||||||
helpers.stubUtxos(server, wallet, [1, 2], function() {
|
helpers.stubUtxos(server, wallet, [1, 2], function() {
|
||||||
var txOpts = {
|
var txOpts = {
|
||||||
|
@ -3033,23 +3145,6 @@ describe('Wallet service', function() {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
it('should fail to create tx for dust amount in outputs', function(done) {
|
|
||||||
helpers.stubUtxos(server, wallet, 1, function() {
|
|
||||||
var txOpts = {
|
|
||||||
outputs: [{
|
|
||||||
toAddress: '18PzpUFkFZE8zKWUPvfykkTxmB9oMR8qP7',
|
|
||||||
amount: 20e2,
|
|
||||||
}],
|
|
||||||
feePerKb: 100e2,
|
|
||||||
};
|
|
||||||
server.createTx(txOpts, function(err, tx) {
|
|
||||||
should.exist(err);
|
|
||||||
err.code.should.equal('DUST_AMOUNT');
|
|
||||||
err.message.should.equal('Amount below dust threshold');
|
|
||||||
done();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('Backoff time', function(done) {
|
describe('Backoff time', function(done) {
|
||||||
|
|
Loading…
Reference in New Issue