Browse Source

Merge pull request #11 from matiu/feat/signtx

signTX
activeAddress
Ivan Socolsky 10 years ago
parent
commit
bca18b4292
  1. 11
      lib/model/copayer.js
  2. 92
      lib/model/txproposal.js
  3. 6
      lib/model/txproposalaction.js
  4. 6
      lib/model/wallet.js
  5. 104
      lib/server.js
  6. 2
      package.json
  7. 223
      test/integration.js
  8. 108
      test/txproposal.js

11
lib/model/copayer.js

@ -26,9 +26,16 @@ function Copayer(opts) {
this.addressManager = new AddressManager({ copayerIndex: opts.copayerIndex });
};
Copayer.prototype.getPublicKey = function(path) {
return HDPublicKey
.fromString(this.xPubKey)
.derive(path)
.publicKey
.toString();
};
Copayer.prototype.getSigningPubKey = function() {
if (!this.xPubKey) return null;
return HDPublicKey.fromString(this.xPubKey).derive(MESSAGE_SIGNING_PATH).publicKey.toString();
return this.getPublicKey(MESSAGE_SIGNING_PATH);
};
Copayer.fromObj = function(obj) {

92
lib/model/txproposal.js

@ -2,6 +2,7 @@
var _ = require('lodash');
var Guid = require('guid');
var Bitcore = require('bitcore');
var TxProposalAction = require('./txproposalaction');
@ -19,13 +20,14 @@ function TxProposal(opts) {
this.message = opts.message;
this.changeAddress = opts.changeAddress;
this.inputs = opts.inputs;
this.inputPaths = opts.inputPaths;
this.requiredSignatures = opts.requiredSignatures;
this.maxRejections = opts.maxRejections;
this.status = 'pending';
this.actions = [];
this.actions = {};
};
TxProposal.fromObj = function (obj) {
TxProposal.fromObj = function(obj) {
var x = new TxProposal();
x.version = obj.version;
@ -37,19 +39,21 @@ TxProposal.fromObj = function (obj) {
x.message = obj.message;
x.changeAddress = obj.changeAddress;
x.inputs = obj.inputs;
x.rawTx = obj.rawTx;
x.requiredSignatures = obj.requiredSignatures;
x.maxRejections = obj.maxRejections;
x.status = obj.status;
x.txid = obj.txid;
x.actions = _.map(obj.actions, function(action) {
return new TxProposalAction(action);
x.inputPaths = obj.inputPaths;
x.actions = obj.actions;
_.each(x.actions, function(action, copayerId) {
x.actions[copayerId] = new TxProposalAction(action);
});
return x;
};
TxProposal.prototype._updateStatus = function () {
TxProposal.prototype._updateStatus = function() {
if (this.status != 'pending') return;
if (this.isRejected()) {
@ -59,35 +63,89 @@ TxProposal.prototype._updateStatus = function () {
}
};
TxProposal.prototype.addAction = function (copayerId, type, signature) {
TxProposal.prototype._getBitcoreTx = function(n) {
var self = this;
var t = new Bitcore.Transaction();
_.each(this.inputs, function(i) {
t.from(i, i.publicKeys, self.requiredSignatures)
});
t.to(this.toAddress, this.amount)
.change(this.changeAddress);
t._updateChangeOutput();
return t;
};
TxProposal.prototype.addAction = function(copayerId, type, signatures) {
var action = new TxProposalAction({
copayerId: copayerId,
type: type,
signature: signature,
signatures: signatures,
});
this.actions.push(action);
this.actions[copayerId] = action;
this._updateStatus();
};
TxProposal.prototype.sign = function (copayerId, signature) {
this.addAction(copayerId, 'accept', signature);
// TODO: no sure we should receive xpub or a list of pubkeys (pre derived)
TxProposal.prototype.checkSignatures = function(signatures, xpub) {
var self = this;
var t = this._getBitcoreTx();
if (signatures.length != this.inputs.length)
return false;
var oks = 0,
i = 0,
x = new Bitcore.HDPublicKey(xpub);
_.each(signatures, function(signatureHex) {
var input = self.inputs[i];
try {
var signature = Bitcore.crypto.Signature.fromString(signatureHex);
var pub = x.derive(self.inputPaths[i]).publicKey;
var s = {
inputIndex: i,
signature: signature,
sigtype: Bitcore.crypto.Signature.SIGHASH_ALL,
publicKey: pub,
};
i++;
t.applySignature(s);
oks++;
} catch (e) {
// TODO only for debug now
console.log('DEBUG ONLY:',e.message); //TODO
};
});
return oks === t.inputs.length;
};
TxProposal.prototype.sign = function(copayerId, signatures) {
this.addAction(copayerId, 'accept', signatures);
};
TxProposal.prototype.reject = function (copayerId) {
TxProposal.prototype.reject = function(copayerId) {
this.addAction(copayerId, 'reject');
};
TxProposal.prototype.isAccepted = function () {
var votes = _.countBy(this.actions, 'type');
TxProposal.prototype.isAccepted = function() {
var votes = _.countBy(_.values(this.actions), 'type');
return votes['accept'] >= this.requiredSignatures;
};
TxProposal.prototype.isRejected = function () {
var votes = _.countBy(this.actions, 'type');
TxProposal.prototype.isRejected = function() {
var votes = _.countBy(_.values(this.actions), 'type');
return votes['reject'] > this.maxRejections;
};
TxProposal.prototype.setBroadcasted = function (txid) {
TxProposal.prototype.setBroadcasted = function(txid) {
this.txid = txid;
this.status = 'broadcasted';
};

6
lib/model/txproposalaction.js

@ -5,8 +5,8 @@ function TxProposalAction(opts) {
this.createdOn = Math.floor(Date.now() / 1000);
this.copayerId = opts.copayerId;
this.type = opts.type || (opts.signature ? 'accept' : 'reject');
this.signature = opts.signature;
this.type = opts.type || (opts.signatures ? 'accept' : 'reject');
this.signatures = opts.signatures;
};
TxProposalAction.fromObj = function (obj) {
@ -15,7 +15,7 @@ TxProposalAction.fromObj = function (obj) {
x.createdOn = obj.createdOn;
x.copayerId = obj.copayerId;
x.type = obj.type;
x.signature = obj.signature;
x.signatures = obj.signatures;
return x;
};

6
lib/model/wallet.js

@ -100,6 +100,12 @@ Wallet.prototype._getBitcoreNetwork = function() {
return this.isTestnet ? Bitcore.Networks.testnet : Bitcore.Networks.livenet;
};
Wallet.prototype.getPublicKey = function(copayerId, path) {
var copayer = this.getCopayer(copayerId);
return copayer.getPublicKey(path);
};
Wallet.prototype.createAddress = function(isChange) {
var path = this.addressManager.getNewAddressPath(isChange);

104
lib/server.js

@ -184,10 +184,10 @@ CopayServer.prototype.createAddress = function(opts, cb) {
self.storage.storeAddress(wallet.id, address, function(err) {
if (err) return cb(err);
self.storage.storeWallet(wallet, function(err) {
if (err) {
self.storage.removeAddress(wallet.id, address, function () {
self.storage.removeAddress(wallet.id, address, function() {
return cb(err);
});
} else {
@ -205,7 +205,7 @@ CopayServer.prototype.createAddress = function(opts, cb) {
* @param {string} opts.walletId - The wallet id.
* @returns {Address[]}
*/
CopayServer.prototype.getAddresses = function (opts, cb) {
CopayServer.prototype.getAddresses = function(opts, cb) {
var self = this;
self.storage.fetchAddresses(opts.walletId, function(err, addresses) {
@ -271,10 +271,11 @@ CopayServer.prototype._getUtxos = function(opts, cb) {
if (err) return cb(err);
if (addresses.length == 0) return cb(new ClientError('The wallet has no addresses'));
var addresses = _.pluck(addresses, 'address');
var addressStrs = _.pluck(addresses, 'address');
var addressToPath = _.indexBy(addresses, 'address'); // TODO : check performance
var bc = self._getBlockExplorer('insight', opts.network);
bc.getUnspentUtxos(addresses, function(err, utxos) {
bc.getUnspentUtxos(addressStrs, function(err, utxos) {
if (err) return cb(err);
self.getPendingTxs({
@ -290,7 +291,7 @@ CopayServer.prototype._getUtxos = function(opts, cb) {
})
.value();
var dictionary = _.reduce(utxos, function (memo, utxo) {
var dictionary = _.reduce(utxos, function(memo, utxo) {
memo[utxo.txid + '|' + utxo.vout] = utxo;
return memo;
}, {});
@ -301,6 +302,11 @@ CopayServer.prototype._getUtxos = function(opts, cb) {
}
});
// Needed for the clients to sign UTXOs
_.each(utxos, function(utxo) {
utxo.path = addressToPath[utxo.address].path;
utxo.publicKeys = addressToPath[utxo.address].publicKeys;
});
return cb(null, utxos);
});
@ -327,13 +333,13 @@ CopayServer.prototype.getBalance = function(opts, cb) {
var balance = {};
balance.totalAmount = Utils.strip(_.reduce(utxos, function(sum, utxo) {
return sum + utxo.amount;
return sum + self._inputSatoshis(utxo);
}, 0));
balance.lockedAmount = Utils.strip(_.reduce(_.filter(utxos, {
locked: true
}), function(sum, utxo) {
return sum + utxo.amount;
return sum + self._inputSatoshis(utxo);
}, 0));
return cb(null, balance);
@ -341,17 +347,9 @@ CopayServer.prototype.getBalance = function(opts, cb) {
};
CopayServer.prototype._createRawTx = function(txp) {
console.log('[server.js.320:txp:]',txp.inputs, txp.toAddress, txp.amount, txp.changeAddress); //TODO
var rawTx = new Bitcore.Transaction()
.from(txp.inputs)
.to(txp.toAddress, txp.amount)
.change(txp.changeAddress);
console.log('[server.js.324:rawTx:]',rawTx); //TODO
return rawTx;
CopayServer.prototype._inputSatoshis = function(i) {
return i.amount ? Utils.strip(i.amount * 1e8) : i.satoshis;
};
CopayServer.prototype._selectUtxos = function(txp, utxos) {
@ -362,13 +360,15 @@ CopayServer.prototype._selectUtxos = function(txp, utxos) {
while (i < inputs.length) {
selected.push(inputs[i]);
total += inputs[i].amount;
total += this._inputSatoshis(inputs[i]);
if (total >= txp.amount) {
break;
}
i++;
};
return total >= txp.amount ? selected : null;
return total >= txp.amount ? selected : null;
};
@ -422,6 +422,8 @@ CopayServer.prototype.createTx = function(opts, cb) {
return cb(new ClientError('INSUFFICIENTFUNDS', 'Insufficient funds'));
}
txp.inputPaths = _.pluck(txp.inputs, 'path');
// no need to do this now: // TODO remove this comment
//self._createRawTx(txp);
self.storage.storeTx(wallet.id, txp, function(err) {
@ -463,37 +465,56 @@ CopayServer.prototype._broadcastTx = function(rawTx, cb) {
* @param {string} opts.walletId - The wallet id.
* @param {string} opts.copayerId - The wallet id.
* @param {string} opts.txProposalId - The identifier of the transaction.
* @param {string} opts.signature - The signature of the tx for this copayer.
* @param {string} opts.signatures - The signatures of the inputs of this tx for this copayer (in apperance order)
*/
CopayServer.prototype.signTx = function(opts, cb) {
var self = this;
Utils.checkRequired(opts, ['walletId', 'copayerId', 'txProposalId', 'signature']);
Utils.checkRequired(opts, ['walletId', 'copayerId', 'txProposalId', 'signatures']);
self.fetchTx(opts.walletId, opts.txProposalId, function(err, txp) {
self.getWallet({
id: opts.walletId
}, function(err, wallet) {
if (err) return cb(err);
if (!txp) return cb(new ClientError('Transaction proposal not found'));
var action = _.find(txp.actions, {
copayerId: opts.copayerId
});
if (action) return cb(new ClientError('CVOTED', 'Copayer already voted on this transaction proposal'));
if (txp.status != 'pending') return cb(new ClientError('TXNOTPENDING', 'The transaction proposal is not pending'));
txp.sign(opts.copayerId, opts.signature);
self.storage.storeTx(opts.walletId, txp, function(err) {
self.getTx({
walletId: opts.walletId,
id: opts.txProposalId
}, function(err, txp) {
if (err) return cb(err);
if (!txp) return cb(new ClientError('Transaction proposal not found'));
var action = _.find(txp.actions, {
copayerId: opts.copayerId
});
if (action)
return cb(new ClientError('CVOTED', 'Copayer already voted on this transaction proposal'));
if (txp.status != 'pending')
return cb(new ClientError('TXNOTPENDING', 'The transaction proposal is not pending'));
if (txp.status == 'accepted');
self._broadcastTx(txp.rawTx, function(err, txid) {
var copayer = wallet.getCopayer(opts.copayerId);
if (!txp.checkSignatures(opts.signatures, copayer.xPubKey))
return cb(new ClientError('BADSIGNATURES', 'Bad signatures'));
txp.sign(opts.copayerId, opts.signatures);
self.storage.storeTx(opts.walletId, txp, function(err) {
if (err) return cb(err);
tx.setBroadcasted(txid);
self.storage.storeTx(opts.walletId, txp, function(err) {
if (err) return cb(err);
if (txp.status == 'accepted') {
self._broadcastTx(txp.rawTx, function(err, txid) {
if (err) return cb(err);
return cb();
});
tx.setBroadcasted(txid);
self.storage.storeTx(opts.walletId, txp, function(err) {
if (err) return cb(err);
return cb(null, txp);
});
});
} else {
return cb(null, txp);
}
});
});
});
@ -512,7 +533,10 @@ CopayServer.prototype.rejectTx = function(opts, cb) {
Utils.checkRequired(opts, ['walletId', 'copayerId', 'txProposalId']);
self.fetchTx(opts.walletId, opts.txProposalId, function(err, txp) {
self.getTx({
walletId: opts.walletId,
id: opts.txProposalId
}, function(err, txp) {
if (err) return cb(err);
if (!txp) return cb(new ClientError('Transaction proposal not found'));
var action = _.find(txp.actions, {

2
package.json

@ -18,7 +18,7 @@
},
"dependencies": {
"async": "^0.9.0",
"bitcore": "^0.8.6",
"bitcore": "0.9.6",
"bitcore-explorers": "^0.9.1",
"express": "^4.10.0",
"inherits": "^2.0.1",

223
test/integration.js

@ -10,6 +10,7 @@ var levelup = require('levelup');
var memdown = require('memdown');
var Bitcore = require('bitcore');
var Utils = require('../lib/utils');
var SignUtils = require('../lib/signutils');
var Storage = require('../lib/storage');
@ -43,7 +44,7 @@ var someXPubKeys = [
'xpub661MyMwAqRbcG67ioS7rz3fFg7EDQNLJ9m1etAPwBecZhL5kKAKe4JU5jCTzRcEWp28XCYA1gKh7jyficSr97gcR2pjDL5jbWua1CwTKWV4',
];
// with keyPair.priv
var someXPubKeysSignatures = [
'30440220192ae7345d980f45f908bd63ccad60ce04270d07b91f1a9d92424a07a38af85202201591f0f71dd4e79d9206d2306862e6b8375e13a62c193953d768e884b6fb5a46',
'30440220134d13139323ba16ff26471c415035679ee18b2281bf85550ccdf6a370899153022066ef56ff97091b9be7dede8e40f50a3a8aad8205f2e3d8e194f39c20f3d15c62',
@ -51,7 +52,7 @@ var someXPubKeysSignatures = [
'304402203ae5bf7fa8935b8ab2ac33724dbb191356cecb47c8371d2c9389e918a3600918022073b48705306730c8fe4ab22d5f6ed3ca3def27eb6e8c5cc8f53e23c11fa5e5ef',
'3045022100eabd2a605403b377a8db9eec57726da0309a7eb385e7e4e5273b9862046f25ef02204d18755a90580a98f45e162ae5d5dc39aa3aa708a0d79433ed259e70a832b49c',
'3045022100c282254773c65025054e18a61ee550cbf78b88fc72ef66770050815b62502d9c02206e0df528203c9201c144f865df71f5d2471668f4ed8387979fcee20f6fa121a9',
]; // with keyPair.priv
];
//Copayer signature
var aText = 'hello world';
@ -100,9 +101,20 @@ helpers.randomTXID = function() {
return Bitcore.crypto.Hash.sha256(new Buffer(Math.random() * 100000)).toString('hex');;
};
helpers.toSatoshi = function(btc) {
if (_.isArray(btc)) {
return _.map(btc, helpers.toSatoshi);
} else {
return Utils.strip(btc * 1e8);
}
};
// Amounts in satoshis
helpers.createUtxos = function(server, wallet, amounts, cb) {
var addresses = [];
async.each(amounts, function(a, next) {
server.createAddress({
walletId: wallet.id,
@ -116,17 +128,56 @@ helpers.createUtxos = function(server, wallet, amounts, cb) {
amounts = [].concat(amounts);
var i = 0;
return cb(_.map(amounts, function(amount) {
var utxos = _.map(amounts, function(amount) {
return {
txid: helpers.randomTXID(),
vout: Math.floor((Math.random() * 10) + 1),
amount: amount,
satoshis: amount,
scriptPubKey: addresses[i].getScriptPubKey(wallet.m).toBuffer().toString('hex'),
address: addresses[i++].address,
};
}));
});
var bc = sinon.stub();
bc.getUnspentUtxos = sinon.stub().callsArgWith(1, null, utxos);
server._getBlockExplorer = sinon.stub().returns(bc);
return cb();
});
};
helpers.clientSign = function(tx, xpriv, n) {
//Derive proper key to sign, for each input
var privs = [],
derived = {};
var xpriv = new Bitcore.HDPrivateKey(someXPrivKey[0]);
_.each(tx.inputs, function(i) {
if (!derived[i.path]) {
derived[i.path] = xpriv.derive(i.path).privateKey;
}
privs.push(derived[i.path]);
});
var t = new Bitcore.Transaction();
_.each(tx.inputs, function(i) {
t.from(i, i.publicKeys, n);
});
t.to(tx.toAddress, tx.amount)
.change(tx.changeAddress)
.sign(privs);
var signatures = [];
_.each(privs, function(p) {
var s = t.getSignatures(p)[0].signature.toDER().toString('hex');
signatures.push(s);
});
//
return signatures;
};
var db, storage;
var server;
@ -638,7 +689,7 @@ describe('Copay server', function() {
it('should not create address if unable to store wallet', function(done) {
helpers.createAndJoinWallet('123', 2, 2, function(err, wallet) {
var storeWalletStub = sinon.stub(server.storage, 'storeWallet');
storeWalletStub.yields('dummy error');
@ -649,9 +700,11 @@ describe('Copay server', function() {
err.should.exist;
should.not.exist(address);
server.getAddresses({ walletId: '123' }, function (err, addresses) {
server.getAddresses({
walletId: '123'
}, function(err, addresses) {
addresses.length.should.equal(0);
server.storage.storeWallet.restore();
server.createAddress({
walletId: '123',
@ -670,7 +723,7 @@ describe('Copay server', function() {
it('should not create address if unable to store addresses', function(done) {
helpers.createAndJoinWallet('123', 2, 2, function(err, wallet) {
var storeAddressStub = sinon.stub(server.storage, 'storeAddress');
storeAddressStub.yields('dummy error');
@ -681,9 +734,11 @@ describe('Copay server', function() {
err.should.exist;
should.not.exist(address);
server.getAddresses({ walletId: '123' }, function (err, addresses) {
server.getAddresses({
walletId: '123'
}, function(err, addresses) {
addresses.length.should.equal(0);
server.storage.storeAddress.restore();
server.createAddress({
walletId: '123',
@ -720,17 +775,13 @@ describe('Copay server', function() {
it('should create tx', function(done) {
helpers.createUtxos(server, wallet, [100, 200], function(utxos) {
var bc = sinon.stub();
bc.getUnspentUtxos = sinon.stub().callsArgWith(1, null, utxos);
server._getBlockExplorer = sinon.stub().returns(bc);
helpers.createUtxos(server, wallet, helpers.toSatoshi([100, 200]), function(utxos) {
var txOpts = {
copayerId: '1',
walletId: '123',
toAddress: '18PzpUFkFZE8zKWUPvfykkTxmB9oMR8qP7',
amount: 80,
amount: helpers.toSatoshi(80),
message: 'some message',
otToken: 'dummy',
requestSignature: 'dummy',
@ -750,8 +801,8 @@ describe('Copay server', function() {
walletId: '123'
}, function(err, balance) {
should.not.exist(err);
balance.totalAmount.should.equal(300);
balance.lockedAmount.should.equal(100);
balance.totalAmount.should.equal(helpers.toSatoshi(300));
balance.lockedAmount.should.equal(helpers.toSatoshi(100));
done();
});
});
@ -761,17 +812,13 @@ describe('Copay server', function() {
it('should fail to create tx when insufficient funds', function(done) {
helpers.createUtxos(server, wallet, [100], function(utxos) {
var bc = sinon.stub();
bc.getUnspentUtxos = sinon.stub().callsArgWith(1, null, utxos);
server._getBlockExplorer = sinon.stub().returns(bc);
helpers.createUtxos(server, wallet, helpers.toSatoshi([100]), function() {
var txOpts = {
copayerId: '1',
walletId: '123',
toAddress: '18PzpUFkFZE8zKWUPvfykkTxmB9oMR8qP7',
amount: 120,
amount: helpers.toSatoshi(120),
message: 'some message',
otToken: 'dummy',
requestSignature: 'dummy',
@ -790,7 +837,7 @@ describe('Copay server', function() {
}, function(err, balance) {
should.not.exist(err);
balance.lockedAmount.should.equal(0);
balance.totalAmount.should.equal(100);
balance.totalAmount.should.equal(10000000000);
done();
});
});
@ -800,17 +847,13 @@ describe('Copay server', function() {
it('should create tx when there is a pending tx and enough UTXOs', function(done) {
helpers.createUtxos(server, wallet, [10.1, 10.2, 10.3], function(utxos) {
var bc = sinon.stub();
bc.getUnspentUtxos = sinon.stub().callsArgWith(1, null, utxos);
server._getBlockExplorer = sinon.stub().returns(bc);
helpers.createUtxos(server, wallet, helpers.toSatoshi([10.1, 10.2, 10.3]), function(utxos) {
var txOpts = {
copayerId: '1',
walletId: '123',
toAddress: '18PzpUFkFZE8zKWUPvfykkTxmB9oMR8qP7',
amount: 12,
amount: helpers.toSatoshi(12),
message: 'some message',
otToken: 'dummy',
requestSignature: 'dummy',
@ -840,8 +883,8 @@ describe('Copay server', function() {
walletId: '123'
}, function(err, balance) {
should.not.exist(err);
balance.totalAmount.should.equal(30.6);
balance.lockedAmount.should.equal(30.6);
balance.totalAmount.should.equal(3060000000);
balance.lockedAmount.should.equal(3060000000);
done();
});
});
@ -851,17 +894,13 @@ describe('Copay server', function() {
});
it('should fail to create tx when there is a pending tx and not enough UTXOs', function(done) {
helpers.createUtxos(server, wallet, [10.1, 10.2, 10.3], function(utxos) {
var bc = sinon.stub();
bc.getUnspentUtxos = sinon.stub().callsArgWith(1, null, utxos);
server._getBlockExplorer = sinon.stub().returns(bc);
helpers.createUtxos(server, wallet, helpers.toSatoshi([10.1, 10.2, 10.3]), function(utxos) {
var txOpts = {
copayerId: '1',
walletId: '123',
toAddress: '18PzpUFkFZE8zKWUPvfykkTxmB9oMR8qP7',
amount: 12,
amount: helpers.toSatoshi(12),
message: 'some message',
otToken: 'dummy',
requestSignature: 'dummy',
@ -874,7 +913,7 @@ describe('Copay server', function() {
copayerId: '1',
walletId: '123',
toAddress: '18PzpUFkFZE8zKWUPvfykkTxmB9oMR8qP7',
amount: 24,
amount: helpers.toSatoshi(24),
message: 'some message 2',
otToken: 'dummy',
requestSignature: 'dummy',
@ -892,8 +931,8 @@ describe('Copay server', function() {
walletId: '123'
}, function(err, balance) {
should.not.exist(err);
balance.totalAmount.should.equal(30.6);
balance.lockedAmount.should.equal(20.3);
balance.totalAmount.should.equal(helpers.toSatoshi(30.6));
balance.lockedAmount.should.equal(helpers.toSatoshi(20.3));
done();
});
});
@ -903,4 +942,102 @@ describe('Copay server', function() {
});
});
describe('#signTx', function() {
var wallet, txid;
beforeEach(function(done) {
server = new CopayServer({
storage: storage,
});
helpers.createAndJoinWallet('123', 2, 2, function(err, w) {
wallet = w;
server.createAddress({
walletId: '123',
isChange: false,
}, function(err, address) {
helpers.createUtxos(server, wallet, helpers.toSatoshi([1, 2, 3, 4, 5, 6, 7, 8]), function(utxos) {
var txOpts = {
copayerId: '1',
walletId: '123',
toAddress: '18PzpUFkFZE8zKWUPvfykkTxmB9oMR8qP7',
amount: helpers.toSatoshi(10),
message: 'some message',
otToken: 'dummy',
requestSignature: 'dummy',
};
server.createTx(txOpts, function(err, tx) {
should.not.exist(err);
tx.should.exist;
txid = tx.id;
done();
});
});
});
});
});
it('should sign a TX with multiple inputs, different paths', function(done) {
server.getPendingTxs({
walletId: '123'
}, function(err, txs) {
var tx = txs[0];
tx.id.should.equal(txid);
var signatures = helpers.clientSign(tx, someXPrivKey[0], wallet.n);
server.signTx({
walletId: '123',
copayerId: '1',
txProposalId: txid,
signatures: signatures,
}, function(err) {
should.not.exist(err);
done();
});
});
});
it('should fail if one signature is broken', function(done) {
server.getPendingTxs({
walletId: '123'
}, function(err, txs) {
var tx = txs[0];
tx.id.should.equal(txid);
var signatures = helpers.clientSign(tx, someXPrivKey[0], wallet.n);
signatures[0]=1;
server.signTx({
walletId: '123',
copayerId: '1',
txProposalId: txid,
signatures: signatures,
}, function(err) {
err.message.should.contain('signatures');
done();
});
});
});
it('should fail on invalids signature', function(done) {
server.getPendingTxs({
walletId: '123'
}, function(err, txs) {
var tx = txs[0];
tx.id.should.equal(txid);
var signatures = ['11', '22', '33', '44'];
server.signTx({
walletId: '123',
copayerId: '1',
txProposalId: txid,
signatures: signatures,
}, function(err) {
err.message.should.contain('signatures');
done();
});
});
});
});
});

108
test/txproposal.js

@ -0,0 +1,108 @@
'use strict';
var _ = require('lodash');
var chai = require('chai');
var sinon = require('sinon');
var should = chai.should();
var TXP = require('../lib/model/txproposal');
var Bitcore = require('bitcore');
describe('TXProposal', function() {
describe('#fromObj', function() {
it('should create a TXP', function() {
var txp = TXP.fromObj(aTXP());
should.exist(txp);
});
});
describe('#_getBitcoreTx', function() {
it('should create a valid bitcore TX', function() {
var txp = TXP.fromObj(aTXP());
var t = txp._getBitcoreTx();
should.exist(t);
});
});
describe('#sign', function() {
it('should sign 2-2', function() {
var txp = TXP.fromObj(aTXP());
txp.sign('1', theSignatures);
txp.isAccepted().should.equal(false);
txp.isRejected().should.equal(false);
txp.sign('2', theSignatures);
txp.isAccepted().should.equal(true);
txp.isRejected().should.equal(false);
});
});
describe('#reject', function() {
it('should reject 2-2', function() {
var txp = TXP.fromObj(aTXP());
txp.reject('1');
txp.isAccepted().should.equal(false);
txp.isRejected().should.equal(true);
});
});
describe('#reject & #sign', function() {
it('should finally reject', function() {
var txp = TXP.fromObj(aTXP());
txp.sign('1', theSignatures);
txp.isAccepted().should.equal(false);
txp.isRejected().should.equal(false);
txp.reject('2');
txp.isAccepted().should.equal(false);
txp.isRejected().should.equal(true);
});
});
describe('#checkSignatures', function() {
it('should check signatures', function() {
var txp = TXP.fromObj(aTXP());
var xpriv = new Bitcore.HDPrivateKey(theXPriv);
var priv = xpriv.derive(txp.inputPaths[0]).privateKey;
var t = txp._getBitcoreTx();
t.sign(priv);
var s = t.getSignatures(priv)[0].signature.toDER().toString('hex');
var xpub = new Bitcore.HDPublicKey(xpriv);
var res = txp.checkSignatures([s], xpub);
res.should.equal(true);
});
});
});
var theXPriv = 'xprv9s21ZrQH143K2rMHbXTJmWTuFx6ssqn1vyRoZqPkCXYchBSkp5ey8kMJe84sxfXq5uChWH4gk94rWbXZt2opN9kg4ufKGvUM7HQSLjnoh7e';
var theSignatures = ['3045022100896aeb8db75fec22fddb5facf791927a996eb3aee23ee6deaa15471ea46047de02204c0c33f42a9d3ff93d62738712a8c8a5ecd21b45393fdd144e7b01b5a186f1f9'];
var aTXP = function() {
return {
"version": "1.0.0",
"createdOn": 1423146231,
"id": "75c34f49-1ed6-255f-e9fd-0c71ae75ed1e",
"creatorId": "1",
"toAddress": "18PzpUFkFZE8zKWUPvfykkTxmB9oMR8qP7",
"amount": 50000000,
"changeAddress": "3CauZ5JUFfmSAx2yANvCRoNXccZ3YSUjXH",
"inputs": [{
"txid": "6ee699846d2d6605f96d20c7cc8230382e5da43342adb11b499bbe73709f06ab",
"vout": 8,
"satoshis": 100000000,
"scriptPubKey": "a914a8a9648754fbda1b6c208ac9d4e252075447f36887",
"address": "3H4pNP6J4PW4NnvdrTg37VvZ7h2QWuAwtA",
"path": "m/2147483647/0/1",
"publicKeys": ["0319008ffe1b3e208f5ebed8f46495c056763f87b07930a7027a92ee477fb0cb0f", "03b5f035af8be40d0db5abb306b7754949ab39032cf99ad177691753b37d101301"]
}],
"inputPaths": ["m/2147483647/0/1"],
"requiredSignatures": 2,
"maxRejections": 0,
"status": "pending",
"actions": []
}
};
Loading…
Cancel
Save