Merge pull request #256 from greggzigler/feat/multOutputs
Feat/mult outputs
This commit is contained in:
commit
e6fc53f4b2
|
@ -9,7 +9,33 @@ var Address = Bitcore.Address;
|
||||||
var TxProposalAction = require('./txproposalaction');
|
var TxProposalAction = require('./txproposalaction');
|
||||||
|
|
||||||
function TxProposal() {
|
function TxProposal() {
|
||||||
this.version = '1.0.0';
|
this.version = '1.0.1';
|
||||||
|
};
|
||||||
|
|
||||||
|
TxProposal.Types = {
|
||||||
|
SIMPLE: 'simple',
|
||||||
|
MULTIPLEOUTPUTS: 'multiple_outputs',
|
||||||
|
};
|
||||||
|
|
||||||
|
TxProposal.isTypeSupported = function(type) {
|
||||||
|
return !type || _.contains(_.values(TxProposal.Types), type);
|
||||||
|
};
|
||||||
|
|
||||||
|
TxProposal._create = {};
|
||||||
|
|
||||||
|
TxProposal._create.simple = function(txp, opts) {
|
||||||
|
txp.toAddress = opts.toAddress;
|
||||||
|
txp.amount = opts.amount;
|
||||||
|
txp.outputOrder = _.shuffle(_.range(2));
|
||||||
|
txp.network = Bitcore.Address(txp.toAddress).toObject().network;
|
||||||
|
};
|
||||||
|
|
||||||
|
TxProposal._create.undefined = TxProposal._create.simple;
|
||||||
|
|
||||||
|
TxProposal._create.multiple_outputs = function(txp, opts) {
|
||||||
|
txp.outputs = opts.outputs;
|
||||||
|
txp.outputOrder = _.shuffle(_.range(txp.outputs.length + 1));
|
||||||
|
txp.network = Bitcore.Address(txp.outputs[0].toAddress).toObject().network;
|
||||||
};
|
};
|
||||||
|
|
||||||
TxProposal.create = function(opts) {
|
TxProposal.create = function(opts) {
|
||||||
|
@ -17,13 +43,15 @@ TxProposal.create = function(opts) {
|
||||||
|
|
||||||
var x = new TxProposal();
|
var x = new TxProposal();
|
||||||
|
|
||||||
|
if (opts.version == '1.0.0' && !opts.type) {
|
||||||
|
opts.type = TxProposal.Types.SIMPLE;
|
||||||
|
}
|
||||||
|
x.type = opts.type;
|
||||||
var now = Date.now();
|
var now = Date.now();
|
||||||
x.createdOn = Math.floor(now / 1000);
|
x.createdOn = Math.floor(now / 1000);
|
||||||
x.id = _.padLeft(now, 14, '0') + Uuid.v4();
|
x.id = _.padLeft(now, 14, '0') + Uuid.v4();
|
||||||
x.walletId = opts.walletId;
|
x.walletId = opts.walletId;
|
||||||
x.creatorId = opts.creatorId;
|
x.creatorId = opts.creatorId;
|
||||||
x.toAddress = opts.toAddress;
|
|
||||||
x.amount = opts.amount;
|
|
||||||
x.message = opts.message;
|
x.message = opts.message;
|
||||||
x.payProUrl = opts.payProUrl;
|
x.payProUrl = opts.payProUrl;
|
||||||
x.proposalSignature = opts.proposalSignature;
|
x.proposalSignature = opts.proposalSignature;
|
||||||
|
@ -34,22 +62,29 @@ TxProposal.create = function(opts) {
|
||||||
x.requiredRejections = opts.requiredRejections;
|
x.requiredRejections = opts.requiredRejections;
|
||||||
x.status = 'pending';
|
x.status = 'pending';
|
||||||
x.actions = [];
|
x.actions = [];
|
||||||
x.outputOrder = _.shuffle(_.range(2));
|
|
||||||
x.fee = null;
|
x.fee = null;
|
||||||
x.network = Bitcore.Address(x.toAddress).toObject().network;
|
|
||||||
x.feePerKb = opts.feePerKb;
|
x.feePerKb = opts.feePerKb;
|
||||||
|
|
||||||
|
if (typeof TxProposal._create[x.type] == 'function') {
|
||||||
|
TxProposal._create[x.type](x, opts);
|
||||||
|
}
|
||||||
|
|
||||||
return x;
|
return x;
|
||||||
};
|
};
|
||||||
|
|
||||||
TxProposal.fromObj = function(obj) {
|
TxProposal.fromObj = function(obj) {
|
||||||
var x = new TxProposal();
|
var x = new TxProposal();
|
||||||
|
|
||||||
|
if (obj.version == '1.0.0' && !obj.type) {
|
||||||
|
obj.type = TxProposal.Types.SIMPLE;
|
||||||
|
}
|
||||||
|
x.type = obj.type;
|
||||||
x.version = obj.version;
|
x.version = obj.version;
|
||||||
x.createdOn = obj.createdOn;
|
x.createdOn = obj.createdOn;
|
||||||
x.id = obj.id;
|
x.id = obj.id;
|
||||||
x.walletId = obj.walletId;
|
x.walletId = obj.walletId;
|
||||||
x.creatorId = obj.creatorId;
|
x.creatorId = obj.creatorId;
|
||||||
|
x.outputs = obj.outputs;
|
||||||
x.toAddress = obj.toAddress;
|
x.toAddress = obj.toAddress;
|
||||||
x.amount = obj.amount;
|
x.amount = obj.amount;
|
||||||
x.message = obj.message;
|
x.message = obj.message;
|
||||||
|
@ -126,6 +161,36 @@ TxProposal.prototype.getRawTx = function() {
|
||||||
return t.uncheckedSerialize();
|
return t.uncheckedSerialize();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* getHeader
|
||||||
|
*
|
||||||
|
* @return {Array} arguments for getProposalHash wallet utility method
|
||||||
|
*/
|
||||||
|
TxProposal.prototype.getHeader = function() {
|
||||||
|
if (this.type == TxProposal.Types.MULTIPLEOUTPUTS) {
|
||||||
|
return [ {
|
||||||
|
outputs: this.outputs,
|
||||||
|
message: this.message,
|
||||||
|
payProUrl: this.payProUrl
|
||||||
|
} ];
|
||||||
|
} else {
|
||||||
|
return [ this.toAddress, this.amount, this.message, this.payProUrl ];
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* getTotalAmount
|
||||||
|
*
|
||||||
|
* @return {Number} total amount of all outputs excluding change output
|
||||||
|
*/
|
||||||
|
TxProposal.prototype.getTotalAmount = function() {
|
||||||
|
if (this.type == TxProposal.Types.MULTIPLEOUTPUTS) {
|
||||||
|
return _.map(this.outputs, function(o) { return o.amount })
|
||||||
|
.reduce(function(total, n) { return total + n; });
|
||||||
|
} else {
|
||||||
|
return this.amount;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* getActors
|
* getActors
|
||||||
|
|
|
@ -731,7 +731,7 @@ WalletService.prototype._selectTxInputs = function(txp, cb) {
|
||||||
|
|
||||||
var balance = self._totalizeUtxos(utxos);
|
var balance = self._totalizeUtxos(utxos);
|
||||||
|
|
||||||
if (balance.totalAmount < txp.amount)
|
if (balance.totalAmount < txp.getTotalAmount())
|
||||||
return cb(new ClientError('INSUFFICIENTFUNDS', 'Insufficient funds'));
|
return cb(new ClientError('INSUFFICIENTFUNDS', 'Insufficient funds'));
|
||||||
if ((balance.totalAmount - balance.lockedAmount) < txp.amount)
|
if ((balance.totalAmount - balance.lockedAmount) < txp.amount)
|
||||||
return cb(new ClientError('LOCKEDFUNDS', 'Funds are locked by pending transaction proposals'));
|
return cb(new ClientError('LOCKEDFUNDS', 'Funds are locked by pending transaction proposals'));
|
||||||
|
@ -751,7 +751,7 @@ WalletService.prototype._selectTxInputs = function(txp, cb) {
|
||||||
total += inputs[i].satoshis;
|
total += inputs[i].satoshis;
|
||||||
i++;
|
i++;
|
||||||
|
|
||||||
if (total >= txp.amount) {
|
if (total >= txp.getTotalAmount()) {
|
||||||
try {
|
try {
|
||||||
txp.setInputs(selected);
|
txp.setInputs(selected);
|
||||||
bitcoreTx = txp.getBitcoreTx();
|
bitcoreTx = txp.getBitcoreTx();
|
||||||
|
@ -814,8 +814,10 @@ WalletService.prototype._canCreateTx = function(copayerId, cb) {
|
||||||
/**
|
/**
|
||||||
* Creates a new transaction proposal.
|
* Creates a new transaction proposal.
|
||||||
* @param {Object} opts
|
* @param {Object} opts
|
||||||
* @param {string} opts.toAddress - Destination address.
|
* @param {string} opts.type - Proposal type.
|
||||||
* @param {number} opts.amount - Amount to transfer in satoshi.
|
* @param {string} opts.toAddress || opts.outputs[].toAddress - Destination address.
|
||||||
|
* @param {number} opts.amount || opts.outputs[].amount - Amount to transfer in satoshi.
|
||||||
|
* @param {string} opts.outputs[].message - A message to attach to this output.
|
||||||
* @param {string} opts.message - A message to attach to this transaction.
|
* @param {string} opts.message - A message to attach to this transaction.
|
||||||
* @param {string} opts.proposalSignature - S(toAddress|amount|message|payProUrl). Used by other copayers to verify the proposal.
|
* @param {string} opts.proposalSignature - S(toAddress|amount|message|payProUrl). Used by other copayers to verify the proposal.
|
||||||
* @param {string} opts.feePerKb - Optional: Use an alternative fee per KB for this TX
|
* @param {string} opts.feePerKb - Optional: Use an alternative fee per KB for this TX
|
||||||
|
@ -825,9 +827,39 @@ WalletService.prototype._canCreateTx = function(copayerId, cb) {
|
||||||
WalletService.prototype.createTx = function(opts, cb) {
|
WalletService.prototype.createTx = function(opts, cb) {
|
||||||
var self = this;
|
var self = this;
|
||||||
|
|
||||||
if (!Utils.checkRequired(opts, ['toAddress', 'amount', 'proposalSignature']))
|
if (!Utils.checkRequired(opts, ['proposalSignature']))
|
||||||
return cb(new ClientError('Required argument missing'));
|
return cb(new ClientError('Required argument missing'));
|
||||||
|
|
||||||
|
if (!Model.TxProposal.isTypeSupported(opts.type))
|
||||||
|
return cb(new ClientError('Invalid proposal type'));
|
||||||
|
|
||||||
|
if (opts.type == Model.TxProposal.Types.MULTIPLEOUTPUTS) {
|
||||||
|
if (!Utils.checkRequired(opts, ['outputs']))
|
||||||
|
return cb(new ClientError('Required argument missing'));
|
||||||
|
_.each(opts.outputs, function(o) {
|
||||||
|
o.valid = true;
|
||||||
|
if (!Utils.checkRequired(o, ['toAddress', 'amount'])) {
|
||||||
|
o.valid = false;
|
||||||
|
cb(new ClientError('Required outputs argument missing'));
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
_.each(_.keys(o), function(key) {
|
||||||
|
if (!_.contains(['toAddress', 'amount', 'message', 'valid'], key)) {
|
||||||
|
o.valid = false;
|
||||||
|
cb(new ClientError('Invalid outputs argument found'));
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
if (!o.valid) return false;
|
||||||
|
});
|
||||||
|
if (_.any(opts.outputs, 'valid', false)) return;
|
||||||
|
_.each(opts.outputs, function(o) { delete o.valid; });
|
||||||
|
|
||||||
|
} else {
|
||||||
|
if (!Utils.checkRequired(opts, ['toAddress', 'amount']))
|
||||||
|
return cb(new ClientError('Required argument missing'));
|
||||||
|
}
|
||||||
|
|
||||||
var feePerKb = opts.feePerKb || 10000;
|
var feePerKb = opts.feePerKb || 10000;
|
||||||
if (feePerKb < WalletUtils.MIN_FEE_PER_KB || feePerKb > WalletUtils.MAX_FEE_PER_KB)
|
if (feePerKb < WalletUtils.MIN_FEE_PER_KB || feePerKb > WalletUtils.MAX_FEE_PER_KB)
|
||||||
return cb(new ClientError('Invalid fee per KB value'));
|
return cb(new ClientError('Invalid fee per KB value'));
|
||||||
|
@ -844,30 +876,46 @@ WalletService.prototype.createTx = function(opts, cb) {
|
||||||
return cb(new ClientError('NOTALLOWEDTOCREATETX', 'Cannot create TX proposal during backoff time'));
|
return cb(new ClientError('NOTALLOWEDTOCREATETX', 'Cannot create TX proposal during backoff time'));
|
||||||
|
|
||||||
var copayer = wallet.getCopayer(self.copayerId);
|
var copayer = wallet.getCopayer(self.copayerId);
|
||||||
var hash = WalletUtils.getProposalHash(opts.toAddress, opts.amount, opts.message, opts.payProUrl);
|
var proposalHeader = Model.TxProposal.create(opts).getHeader();
|
||||||
|
var hash = WalletUtils.getProposalHash.apply(WalletUtils, proposalHeader);
|
||||||
if (!self._verifySignature(hash, opts.proposalSignature, copayer.requestPubKey))
|
if (!self._verifySignature(hash, opts.proposalSignature, copayer.requestPubKey))
|
||||||
return cb(new ClientError('Invalid proposal signature'));
|
return cb(new ClientError('Invalid proposal signature'));
|
||||||
|
|
||||||
var toAddress;
|
var outputs = (opts.type == Model.TxProposal.Types.MULTIPLEOUTPUTS)
|
||||||
|
? opts.outputs
|
||||||
|
: [ { toAddress: opts.toAddress, amount: opts.amount } ];
|
||||||
|
_.each(outputs, function(output) {
|
||||||
|
output.valid = false;
|
||||||
|
var toAddress = {};
|
||||||
try {
|
try {
|
||||||
toAddress = new Bitcore.Address(opts.toAddress);
|
toAddress = new Bitcore.Address(output.toAddress);
|
||||||
} catch (ex) {
|
} catch (ex) {
|
||||||
return cb(new ClientError('INVALIDADDRESS', 'Invalid address'));
|
cb(new ClientError('INVALIDADDRESS', 'Invalid address'));
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
if (toAddress.network != wallet.getNetworkName())
|
if (toAddress.network != wallet.getNetworkName()) {
|
||||||
return cb(new ClientError('INVALIDADDRESS', 'Incorrect address network'));
|
cb(new ClientError('INVALIDADDRESS', 'Incorrect address network'));
|
||||||
|
return false;
|
||||||
if (opts.amount <= 0)
|
}
|
||||||
return cb(new ClientError('Invalid amount'));
|
if (output.amount <= 0) {
|
||||||
|
cb(new ClientError('Invalid amount'));
|
||||||
if (opts.amount < Bitcore.Transaction.DUST_AMOUNT)
|
return false;
|
||||||
return cb(new ClientError('DUSTAMOUNT', 'Amount below dust threshold'));
|
}
|
||||||
|
if (output.amount < Bitcore.Transaction.DUST_AMOUNT) {
|
||||||
|
cb(new ClientError('DUSTAMOUNT', 'Amount below dust threshold'));
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
output.valid = true;
|
||||||
|
});
|
||||||
|
if (_.any(outputs, 'valid', false)) return;
|
||||||
|
|
||||||
var changeAddress = wallet.createAddress(true);
|
var changeAddress = wallet.createAddress(true);
|
||||||
|
|
||||||
var txp = Model.TxProposal.create({
|
var txp = Model.TxProposal.create({
|
||||||
|
type: opts.type,
|
||||||
walletId: self.walletId,
|
walletId: self.walletId,
|
||||||
creatorId: self.copayerId,
|
creatorId: self.copayerId,
|
||||||
|
outputs: opts.outputs,
|
||||||
toAddress: opts.toAddress,
|
toAddress: opts.toAddress,
|
||||||
amount: opts.amount,
|
amount: opts.amount,
|
||||||
message: opts.message,
|
message: opts.message,
|
||||||
|
@ -879,6 +927,7 @@ WalletService.prototype.createTx = function(opts, cb) {
|
||||||
requiredRejections: Math.min(wallet.m, wallet.n - wallet.m + 1),
|
requiredRejections: Math.min(wallet.m, wallet.n - wallet.m + 1),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
self._selectTxInputs(txp, function(err) {
|
self._selectTxInputs(txp, function(err) {
|
||||||
if (err) return cb(err);
|
if (err) return cb(err);
|
||||||
|
|
||||||
|
@ -891,7 +940,7 @@ WalletService.prototype.createTx = function(opts, cb) {
|
||||||
if (err) return cb(err);
|
if (err) return cb(err);
|
||||||
|
|
||||||
self._notify('NewTxProposal', {
|
self._notify('NewTxProposal', {
|
||||||
amount: opts.amount
|
amount: txp.getTotalAmount()
|
||||||
}, function() {
|
}, function() {
|
||||||
return cb(null, txp);
|
return cb(null, txp);
|
||||||
});
|
});
|
||||||
|
@ -1101,7 +1150,7 @@ WalletService.prototype.broadcastTx = function(opts, cb) {
|
||||||
self._notify('NewOutgoingTx', {
|
self._notify('NewOutgoingTx', {
|
||||||
txProposalId: opts.txProposalId,
|
txProposalId: opts.txProposalId,
|
||||||
txid: txid,
|
txid: txid,
|
||||||
amount: txp.amount,
|
amount: txp.getTotalAmount(),
|
||||||
}, function() {
|
}, function() {
|
||||||
return cb(null, txp);
|
return cb(null, txp);
|
||||||
});
|
});
|
||||||
|
|
|
@ -20,7 +20,7 @@
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"async": "^0.9.0",
|
"async": "^0.9.0",
|
||||||
"bitcore": "^0.12.9",
|
"bitcore": "^0.12.9",
|
||||||
"bitcore-wallet-utils": "0.0.16",
|
"bitcore-wallet-utils": "^0.0.17",
|
||||||
"body-parser": "^1.11.0",
|
"body-parser": "^1.11.0",
|
||||||
"coveralls": "^2.11.2",
|
"coveralls": "^2.11.2",
|
||||||
"email-validator": "^1.0.1",
|
"email-validator": "^1.0.1",
|
||||||
|
|
|
@ -201,6 +201,35 @@ helpers.createProposalOpts = function(toAddress, amount, message, signingKey, fe
|
||||||
return opts;
|
return opts;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
helpers.createProposalOptsByType = function(type, outputs, message, signingKey, feePerKb) {
|
||||||
|
var opts = {
|
||||||
|
type: type,
|
||||||
|
message: message,
|
||||||
|
proposalSignature: null,
|
||||||
|
};
|
||||||
|
if (type == Model.TxProposal.Types.MULTIPLEOUTPUTS) {
|
||||||
|
opts.outputs = [];
|
||||||
|
_.each(outputs, function(o) {
|
||||||
|
opts.outputs.push(o);
|
||||||
|
o.amount = helpers.toSatoshi(o.amount);
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
opts.toAddress = outputs[0].toAddress;
|
||||||
|
opts.amount = helpers.toSatoshi(outputs[0].amount);
|
||||||
|
}
|
||||||
|
if (feePerKb) opts.feePerKb = feePerKb;
|
||||||
|
|
||||||
|
var txp = Model.TxProposal.create(opts);
|
||||||
|
var proposalHeader = txp.getHeader();
|
||||||
|
var hash = WalletUtils.getProposalHash.apply(WalletUtils, proposalHeader);
|
||||||
|
|
||||||
|
try {
|
||||||
|
opts.proposalSignature = WalletUtils.signMessage(hash, signingKey);
|
||||||
|
} catch (ex) {}
|
||||||
|
|
||||||
|
return opts;
|
||||||
|
};
|
||||||
|
|
||||||
helpers.createAddresses = function(server, wallet, main, change, cb) {
|
helpers.createAddresses = function(server, wallet, main, change, cb) {
|
||||||
async.map(_.range(main + change), function(i, next) {
|
async.map(_.range(main + change), function(i, next) {
|
||||||
var address = wallet.createAddress(i >= main);
|
var address = wallet.createAddress(i >= main);
|
||||||
|
@ -1372,8 +1401,7 @@ describe('Wallet service', function() {
|
||||||
server.createTx(txOpts, function(err, tx) {
|
server.createTx(txOpts, function(err, tx) {
|
||||||
should.not.exist(tx);
|
should.not.exist(tx);
|
||||||
should.exist(err);
|
should.exist(err);
|
||||||
err.code.should.equal('INVALIDADDRESS');
|
// may fail due to Non-base58 character, or Checksum mismatch, or other
|
||||||
err.message.should.equal('Invalid address');
|
|
||||||
done();
|
done();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -1633,6 +1661,49 @@ describe('Wallet service', function() {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should create tx for type multiple_outputs', function(done) {
|
||||||
|
helpers.stubUtxos(server, wallet, [100, 200], function() {
|
||||||
|
var outputs = [
|
||||||
|
{ toAddress: '18PzpUFkFZE8zKWUPvfykkTxmB9oMR8qP7', amount: 75, message: 'message #1' },
|
||||||
|
{ toAddress: '18PzpUFkFZE8zKWUPvfykkTxmB9oMR8qP7', amount: 75, message: 'message #2' }
|
||||||
|
];
|
||||||
|
var txOpts = helpers.createProposalOptsByType(Model.TxProposal.Types.MULTIPLEOUTPUTS, outputs, 'some message', TestData.copayers[0].privKey_1H_0);
|
||||||
|
server.createTx(txOpts, function(err, tx) {
|
||||||
|
should.not.exist(err);
|
||||||
|
should.exist(tx);
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should fail to create tx for type multiple_outputs with invalid output argument', function(done) {
|
||||||
|
helpers.stubUtxos(server, wallet, [100, 200], function() {
|
||||||
|
var outputs = [
|
||||||
|
{ toAddress: '18PzpUFkFZE8zKWUPvfykkTxmB9oMR8qP7', amount: 80, message: 'message #1', foo: 'bar' },
|
||||||
|
{ toAddress: '18PzpUFkFZE8zKWUPvfykkTxmB9oMR8qP7', amount: 90, message: 'message #2' }
|
||||||
|
];
|
||||||
|
var txOpts = helpers.createProposalOptsByType(Model.TxProposal.Types.MULTIPLEOUTPUTS, outputs, 'some message', TestData.copayers[0].privKey_1H_0);
|
||||||
|
server.createTx(txOpts, function(err, tx) {
|
||||||
|
should.exist(err);
|
||||||
|
err.message.should.contain('Invalid outputs argument');
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should fail to create tx for unsupported proposal type', function(done) {
|
||||||
|
helpers.stubUtxos(server, wallet, [100, 200], function() {
|
||||||
|
var txOpts = helpers.createProposalOpts('18PzpUFkFZE8zKWUPvfykkTxmB9oMR8qP7', 80, 'some message', TestData.copayers[0].privKey_1H_0);
|
||||||
|
txOpts.type = 'bogus';
|
||||||
|
server.createTx(txOpts, function(err, tx) {
|
||||||
|
should.exist(err);
|
||||||
|
err.message.should.contain('Invalid proposal type');
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
it('should be able to send max amount', function(done) {
|
it('should be able to send max amount', function(done) {
|
||||||
helpers.stubUtxos(server, wallet, _.range(1, 10, 0), function() {
|
helpers.stubUtxos(server, wallet, _.range(1, 10, 0), function() {
|
||||||
server.getBalance({}, function(err, balance) {
|
server.getBalance({}, function(err, balance) {
|
||||||
|
|
|
@ -6,23 +6,45 @@ var sinon = require('sinon');
|
||||||
var should = chai.should();
|
var should = chai.should();
|
||||||
var TXP = require('../../lib/model/txproposal');
|
var TXP = require('../../lib/model/txproposal');
|
||||||
var Bitcore = require('bitcore-wallet-utils').Bitcore;
|
var Bitcore = require('bitcore-wallet-utils').Bitcore;
|
||||||
|
var WalletUtils = require('bitcore-wallet-utils');
|
||||||
|
|
||||||
describe('TXProposal', function() {
|
describe('TXProposal', function() {
|
||||||
|
|
||||||
describe('#fromObj', function() {
|
describe('#create', function() {
|
||||||
it('should create a TXP', function() {
|
it('should create a TXP', function() {
|
||||||
|
var txp = TXP.create(aTxpOpts());
|
||||||
|
should.exist(txp);
|
||||||
|
should.exist(txp.toAddress);
|
||||||
|
should.not.exist(txp.outputs);
|
||||||
|
});
|
||||||
|
it('should create a multiple-outputs TXP', function() {
|
||||||
|
var txp = TXP.create(aTxpOpts(TXP.Types.MULTIPLEOUTPUTS));
|
||||||
|
should.exist(txp);
|
||||||
|
should.not.exist(txp.toAddress);
|
||||||
|
should.exist(txp.outputs);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('#fromObj', function() {
|
||||||
|
it('should copy a TXP', function() {
|
||||||
var txp = TXP.fromObj(aTXP());
|
var txp = TXP.fromObj(aTXP());
|
||||||
should.exist(txp);
|
should.exist(txp);
|
||||||
|
txp.toAddress.should.equal(aTXP().toAddress);
|
||||||
|
});
|
||||||
|
it('should copy a multiple-outputs TXP', function() {
|
||||||
|
var txp = TXP.fromObj(aTXP(TXP.Types.MULTIPLEOUTPUTS));
|
||||||
|
should.exist(txp);
|
||||||
|
txp.outputs.should.deep.equal(aTXP(TXP.Types.MULTIPLEOUTPUTS).outputs);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
describe('#_getBitcoreTx', function() {
|
|
||||||
|
describe('#getBitcoreTx', function() {
|
||||||
it('should create a valid bitcore TX', function() {
|
it('should create a valid bitcore TX', function() {
|
||||||
var txp = TXP.fromObj(aTXP());
|
var txp = TXP.fromObj(aTXP());
|
||||||
var t = txp.getBitcoreTx();
|
var t = txp.getBitcoreTx();
|
||||||
should.exist(t);
|
should.exist(t);
|
||||||
});
|
});
|
||||||
it('should order ouputs as specified by outputOrder', function() {
|
it('should order outputs as specified by outputOrder', function() {
|
||||||
var txp = TXP.fromObj(aTXP());
|
var txp = TXP.fromObj(aTXP());
|
||||||
|
|
||||||
txp.outputOrder = [0, 1];
|
txp.outputOrder = [0, 1];
|
||||||
|
@ -33,8 +55,44 @@ describe('TXProposal', function() {
|
||||||
var t = txp.getBitcoreTx();
|
var t = txp.getBitcoreTx();
|
||||||
t.getChangeOutput().should.deep.equal(t.outputs[0]);
|
t.getChangeOutput().should.deep.equal(t.outputs[0]);
|
||||||
});
|
});
|
||||||
|
it('should create a bitcore TX with multiple outputs', function() {
|
||||||
|
var txp = TXP.fromObj(aTXP(TXP.Types.MULTIPLEOUTPUTS));
|
||||||
|
txp.outputOrder = [0, 1, 2];
|
||||||
|
var t = txp.getBitcoreTx();
|
||||||
|
t.getChangeOutput().should.deep.equal(t.outputs[2]);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('#getHeader', function() {
|
||||||
|
it('should be compatible with simple proposal legacy header', function() {
|
||||||
|
var x = TXP.fromObj(aTXP());
|
||||||
|
var proposalHeader = x.getHeader();
|
||||||
|
var pH = WalletUtils.getProposalHash.apply(WalletUtils, proposalHeader);
|
||||||
|
var uH = WalletUtils.getProposalHash(x.toAddress, x.amount, x.message, x.payProUrl);
|
||||||
|
pH.should.equal(uH);
|
||||||
|
});
|
||||||
|
it('should handle multiple-outputs', function() {
|
||||||
|
var x = TXP.fromObj(aTXP(TXP.Types.MULTIPLEOUTPUTS));
|
||||||
|
var proposalHeader = x.getHeader();
|
||||||
|
should.exist(proposalHeader);
|
||||||
|
var pH = WalletUtils.getProposalHash.apply(WalletUtils, proposalHeader);
|
||||||
|
should.exist(pH);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('#getTotalAmount', function() {
|
||||||
|
it('should be compatible with simple proposal legacy amount', function() {
|
||||||
|
var x = TXP.fromObj(aTXP());
|
||||||
|
var total = x.getTotalAmount();
|
||||||
|
total.should.equal(x.amount);
|
||||||
|
});
|
||||||
|
it('should handle multiple-outputs', function() {
|
||||||
|
var x = TXP.fromObj(aTXP(TXP.Types.MULTIPLEOUTPUTS));
|
||||||
|
var totalOutput = 0;
|
||||||
|
_.each(x.outputs, function(o) { totalOutput += o.amount });
|
||||||
|
x.getTotalAmount().should.equal(totalOutput);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
describe('#sign', function() {
|
describe('#sign', function() {
|
||||||
it('should sign 2-2', function() {
|
it('should sign 2-2', function() {
|
||||||
|
@ -86,9 +144,36 @@ var theXPriv = 'xprv9s21ZrQH143K2rMHbXTJmWTuFx6ssqn1vyRoZqPkCXYchBSkp5ey8kMJe84s
|
||||||
var theXPub = 'xpub661MyMwAqRbcFLRkhYzK8eQdoywNHJVsJCMQNDoMks5bZymuMcyDgYfnVQYq2Q9npnVmdTAthYGc3N3uxm5sEdnTpSqBc4YYTAhNnoSxCm9';
|
var theXPub = 'xpub661MyMwAqRbcFLRkhYzK8eQdoywNHJVsJCMQNDoMks5bZymuMcyDgYfnVQYq2Q9npnVmdTAthYGc3N3uxm5sEdnTpSqBc4YYTAhNnoSxCm9';
|
||||||
var theSignatures = ['3045022100896aeb8db75fec22fddb5facf791927a996eb3aee23ee6deaa15471ea46047de02204c0c33f42a9d3ff93d62738712a8c8a5ecd21b45393fdd144e7b01b5a186f1f9'];
|
var theSignatures = ['3045022100896aeb8db75fec22fddb5facf791927a996eb3aee23ee6deaa15471ea46047de02204c0c33f42a9d3ff93d62738712a8c8a5ecd21b45393fdd144e7b01b5a186f1f9'];
|
||||||
|
|
||||||
var aTXP = function() {
|
var aTxpOpts = function(type) {
|
||||||
return {
|
var opts = {
|
||||||
|
type: type,
|
||||||
|
toAddress: '18PzpUFkFZE8zKWUPvfykkTxmB9oMR8qP7',
|
||||||
|
amount: 50000000,
|
||||||
|
message: 'some message'
|
||||||
|
};
|
||||||
|
if (type == TXP.Types.MULTIPLEOUTPUTS) {
|
||||||
|
opts.outputs = [
|
||||||
|
{
|
||||||
|
toAddress: "18PzpUFkFZE8zKWUPvfykkTxmB9oMR8qP7",
|
||||||
|
amount: 10000000,
|
||||||
|
message: "first message"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
toAddress: "18PzpUFkFZE8zKWUPvfykkTxmB9oMR8qP7",
|
||||||
|
amount: 20000000,
|
||||||
|
message: "second message"
|
||||||
|
},
|
||||||
|
];
|
||||||
|
delete opts.toAddress;
|
||||||
|
delete opts.amount;
|
||||||
|
}
|
||||||
|
return opts;
|
||||||
|
};
|
||||||
|
|
||||||
|
var aTXP = function(type) {
|
||||||
|
var txp = {
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
|
"type": type,
|
||||||
"createdOn": 1423146231,
|
"createdOn": 1423146231,
|
||||||
"id": "75c34f49-1ed6-255f-e9fd-0c71ae75ed1e",
|
"id": "75c34f49-1ed6-255f-e9fd-0c71ae75ed1e",
|
||||||
"walletId": "1",
|
"walletId": "1",
|
||||||
|
@ -123,5 +208,23 @@ var aTXP = function() {
|
||||||
"status": "pending",
|
"status": "pending",
|
||||||
"actions": [],
|
"actions": [],
|
||||||
"outputOrder": [0, 1],
|
"outputOrder": [0, 1],
|
||||||
}
|
};
|
||||||
|
if (type == TXP.Types.MULTIPLEOUTPUTS) {
|
||||||
|
txp.outputs = [
|
||||||
|
{
|
||||||
|
toAddress: "18PzpUFkFZE8zKWUPvfykkTxmB9oMR8qP7",
|
||||||
|
amount: 10000000,
|
||||||
|
message: "first message"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
toAddress: "18PzpUFkFZE8zKWUPvfykkTxmB9oMR8qP7",
|
||||||
|
amount: 20000000,
|
||||||
|
message: "second message"
|
||||||
|
},
|
||||||
|
];
|
||||||
|
txp.outputOrder = [0, 1, 2];
|
||||||
|
delete txp.toAddress;
|
||||||
|
delete txp.amount;
|
||||||
|
}
|
||||||
|
return txp;
|
||||||
};
|
};
|
||||||
|
|
Loading…
Reference in New Issue