diff --git a/TODO.txt b/TODO.txt index d7f06ef..9600e23 100644 --- a/TODO.txt +++ b/TODO.txt @@ -1,5 +1,8 @@ - When creating a wallet, the id should be generated by the server and returned to the client to be used as part of the wallet secret. -- Copayer id should be auto-generated. - Check not blank & length < 100 for both wallet.name & copayer.name - Proposal with spent input should be tagged as invalid or removed - Cron job to broadcast accepted txps that failed to broadcast (we may need to track broadcast attempts for this). + + + +- check parameters for KEY at storage diff --git a/lib/model/copayer.js b/lib/model/copayer.js index 7056645..605816a 100644 --- a/lib/model/copayer.js +++ b/lib/model/copayer.js @@ -5,7 +5,7 @@ var util = require('util'); var Bitcore = require('bitcore'); var HDPublicKey = Bitcore.HDPublicKey; - +var Uuid = require('uuid'); var AddressManager = require('./addressmanager'); @@ -16,13 +16,14 @@ function Copayer(opts) { opts = opts || {}; opts.copayerIndex = opts.copayerIndex || 0; + this.id = Uuid.v4(); this.version = VERSION; this.createdOn = Math.floor(Date.now() / 1000); - this.id = opts.id; this.name = opts.name; this.xPubKey = opts.xPubKey; this.xPubKeySignature = opts.xPubKeySignature; // So third parties can check independently - this.signingPubKey = this.getSigningPubKey(); + if (this.xPubKey) + this.signingPubKey = this.getSigningPubKey(); this.addressManager = new AddressManager({ copayerIndex: opts.copayerIndex }); }; diff --git a/lib/model/txproposal.js b/lib/model/txproposal.js index 2f157b7..3a9d277 100644 --- a/lib/model/txproposal.js +++ b/lib/model/txproposal.js @@ -1,7 +1,7 @@ 'use strict'; var _ = require('lodash'); -var Guid = require('guid'); +var Uuid = require('uuid'); var Bitcore = require('bitcore'); var Address = Bitcore.Address; @@ -14,7 +14,7 @@ function TxProposal(opts) { this.version = VERSION; this.createdOn = Math.floor(Date.now() / 1000); - this.id = Guid.raw(); + this.id = ('000000000000' + this.createdOn).slice(-12) + Uuid.v4(); this.creatorId = opts.creatorId; this.toAddress = opts.toAddress; this.amount = opts.amount; diff --git a/lib/model/wallet.js b/lib/model/wallet.js index f65978d..8bf50d2 100644 --- a/lib/model/wallet.js +++ b/lib/model/wallet.js @@ -5,6 +5,7 @@ var util = require('util'); var Bitcore = require('bitcore'); var BitcoreAddress = Bitcore.Address; +var Uuid = require('uuid'); var Address = require('./address'); var Copayer = require('./copayer'); @@ -17,7 +18,7 @@ function Wallet(opts) { this.version = VERSION; this.createdOn = Math.floor(Date.now() / 1000); - this.id = opts.id; + this.id = Uuid.v4(); this.name = opts.name; this.m = opts.m; this.n = opts.n; @@ -71,7 +72,7 @@ Wallet.fromObj = function(obj) { x.status = obj.status; x.publicKeyRing = obj.publicKeyRing; x.copayers = _.map(obj.copayers, function(copayer) { - return new Copayer(copayer); + return Copayer.fromObj(copayer); }); x.pubKey = obj.pubKey; x.isTestnet = obj.isTestnet; diff --git a/lib/server.js b/lib/server.js index 9180a2d..5a1c49e 100644 --- a/lib/server.js +++ b/lib/server.js @@ -43,7 +43,7 @@ function CopayServer() { * @param {Object} opts * @param {Storage} [opts.storage] - The storage provider. */ -CopayServer.initialize = function (opts) { +CopayServer.initialize = function(opts) { opts = opts || {}; storage = opts.storage ||  new Storage(); initialized = true; @@ -56,12 +56,12 @@ CopayServer.initialize = function (opts) { * @param {string} opts.message - The contents of the request to be signed. * @param {string} opts.signature - Signature of message to be verified using the copayer's signingPubKey. */ -CopayServer.getInstanceWithAuth = function (opts, cb) { +CopayServer.getInstanceWithAuth = function(opts, cb) { Utils.checkRequired(opts, ['copayerId', 'message', 'signature']); var server = new CopayServer(); - server.storage.fetchCopayerLookup(opts.copayerId, function (err, copayer) { + server.storage.fetchCopayerLookup(opts.copayerId, function(err, copayer) { if (err) return cb(err); if (!copayer) return cb('Copayer not found'); @@ -89,10 +89,14 @@ CopayServer.prototype.createWallet = function(opts, cb) { var self = this, pubKey; - Utils.checkRequired(opts, ['id', 'name', 'm', 'n', 'pubKey']); - if (!Wallet.verifyCopayerLimits(opts.m, opts.n)) return cb(new ClientError('Invalid combination of required copayers / total copayers')); + Utils.checkRequired(opts, ['name', 'm', 'n', 'pubKey']); + + if (!Wallet.verifyCopayerLimits(opts.m, opts.n)) + return cb(new ClientError('Invalid combination of required copayers / total copayers')); + var network = opts.network || 'livenet'; - if (network != 'livenet' && network != 'testnet') return cb(new ClientError('Invalid network')); + if (network != 'livenet' && network != 'testnet') + return cb(new ClientError('Invalid network')); try { pubKey = new PublicKey.fromString(opts.pubKey); @@ -100,20 +104,16 @@ CopayServer.prototype.createWallet = function(opts, cb) { return cb(e.toString()); }; - self.storage.fetchWallet(opts.id, function(err, wallet) { - if (err) return cb(err); - if (wallet) return cb(new ClientError('WEXISTS', 'Wallet already exists')); - - var wallet = new Wallet({ - id: opts.id, - name: opts.name, - m: opts.m, - n: opts.n, - network: opts.network || 'livenet', - pubKey: pubKey, - }); + var wallet = new Wallet({ + name: opts.name, + m: opts.m, + n: opts.n, + network: opts.network || 'livenet', + pubKey: pubKey, + }); - self.storage.storeWallet(wallet, cb); + self.storage.storeWallet(wallet, function(err) { + return cb(err,wallet.id); }); }; @@ -147,7 +147,6 @@ CopayServer.prototype._verifySignature = function(text, signature, pubKey) { * Joins a wallet in creation. * @param {Object} opts * @param {string} opts.walletId - The wallet id. - * @param {string} opts.id - The copayer id. * @param {string} opts.name - The copayer name. * @param {number} opts.xPubKey - Extended Public Key for this copayer. * @param {number} opts.xPubKeySignature - Signature of xPubKey using the wallet pubKey. @@ -155,7 +154,7 @@ CopayServer.prototype._verifySignature = function(text, signature, pubKey) { CopayServer.prototype.joinWallet = function(opts, cb) { var self = this; - Utils.checkRequired(opts, ['walletId', 'id', 'name', 'xPubKey', 'xPubKeySignature']); + Utils.checkRequired(opts, ['walletId', 'name', 'xPubKey', 'xPubKeySignature']); Utils.runLocked(opts.walletId, cb, function(cb) { self.storage.fetchWallet(opts.walletId, function(err, wallet) { @@ -172,7 +171,6 @@ CopayServer.prototype.joinWallet = function(opts, cb) { if (wallet.copayers.length == wallet.n) return cb(new ClientError('WFULL', 'Wallet full')); var copayer = new Copayer({ - id: opts.id, name: opts.name, xPubKey: opts.xPubKey, xPubKeySignature: opts.xPubKeySignature, @@ -180,9 +178,8 @@ CopayServer.prototype.joinWallet = function(opts, cb) { }); wallet.addCopayer(copayer); - self.storage.storeWalletAndUpdateCopayersLookup(wallet, function(err) { - return cb(err); + return cb(err, copayer.id); }); }); }); diff --git a/lib/storage.js b/lib/storage.js index 512a9b1..275d4a6 100644 --- a/lib/storage.js +++ b/lib/storage.js @@ -29,7 +29,6 @@ var opKeyTs = function(key) { }; - var KEY = { WALLET: function(id) { return 'wallet!' + id; @@ -40,11 +39,8 @@ var KEY = { TXP: function(walletId, txProposalId) { return 'txp!' + walletId + opKey(txProposalId); }, - TXP_BY_TS: function(walletId, ts, txProposalId) { - return 'txp-ts!' + walletId + opKeyTs(ts) + opKey(txProposalId); - }, - PENDING_TXP_BY_TS: function(walletId, ts, txProposalId) { - return 'pending-txp-ts!' + walletId + opKey(ts) + opKey(txProposalId); + PENDING_TXP: function(walletId, txProposalId) { + return 'pending-txp-ts!' + walletId + opKey(txProposalId); }, ADDRESS: function(walletId, address) { return 'address!' + walletId + opKey(address); @@ -108,7 +104,7 @@ Storage.prototype.fetchTx = function(walletId, txProposalId, cb) { Storage.prototype.fetchPendingTxs = function(walletId, cb) { var txs = []; - var key = KEY.PENDING_TXP_BY_TS(walletId); + var key = KEY.PENDING_TXP(walletId); this.db.createReadStream({ gte: key, lt: key + '~' @@ -136,12 +132,12 @@ Storage.prototype.fetchPendingTxs = function(walletId, cb) { Storage.prototype.fetchTxs = function(walletId, opts, cb) { var txs = []; opts = opts || {}; - opts.limit = opts.limit || -1; - opts.minTs = opts.minTs || 0; - opts.maxTs = opts.maxTs || MAX_TS; + opts.limit = _.isNumber(opts.limit) ? parseInt(opts.limit) : -1; + opts.minTs = _.isNumber(opts.minTs) ? ('000000000000' + parseInt(opts.minTs)).slice(-12) : 0; + opts.maxTs = _.isNumber(opts.maxTs) ? ('000000000000' + parseInt(opts.maxTs)).slice(-12) : MAX_TS; - var key = KEY.TXP_BY_TS(walletId, opts.minTs); - var endkey = KEY.TXP_BY_TS(walletId, opts.maxTs); + var key = KEY.TXP(walletId, opts.minTs); + var endkey = KEY.TXP(walletId, opts.maxTs); this.db.createReadStream({ gt: key, @@ -169,22 +165,18 @@ Storage.prototype.storeTx = function(walletId, txp, cb) { type: 'put', key: KEY.TXP(walletId, txp.id), value: txp, - }, { - type: 'put', - key: KEY.TXP_BY_TS(walletId, txp.createdOn, txp.id), - value: txp, }]; if (txp.isPending()) { ops.push({ type: 'put', - key: KEY.PENDING_TXP_BY_TS(walletId, txp.createdOn, txp.id), + key: KEY.PENDING_TXP(walletId, txp.id), value: txp, }); } else { ops.push({ type: 'del', - key: KEY.PENDING_TXP_BY_TS(walletId, txp.createdOn, txp.id), + key: KEY.PENDING_TXP(walletId, txp.id), }); } this.db.batch(ops, cb); diff --git a/package.json b/package.json index e7f86a7..30ce41f 100644 --- a/package.json +++ b/package.json @@ -18,7 +18,7 @@ }, "dependencies": { "async": "^0.9.0", - "bitcore": "0.9.6", + "bitcore": "*", "bitcore-explorers": "^0.9.1", "express": "^4.10.0", "inherits": "^2.0.1", @@ -27,7 +27,7 @@ "lodash": "^2.4.1", "npmlog": "^0.1.1", "preconditions": "^1.0.7", - "guid":"*" + "uuid":"*" }, "devDependencies": { "chai": "^1.9.1", diff --git a/test/integration.js b/test/integration.js index b34e98b..479fa93 100644 --- a/test/integration.js +++ b/test/integration.js @@ -73,36 +73,36 @@ helpers.getAuthServer = function(copayerId, cb) { }); }; -helpers.createAndJoinWallet = function(id, m, n, cb) { +helpers.createAndJoinWallet = function(m, n, cb) { var server = new CopayServer(); + var copayerIds = []; var walletOpts = { - id: id, - name: id + ' wallet', + name: 'a wallet', m: m, n: n, pubKey: keyPair.pub, }; - server.createWallet(walletOpts, function(err) { + server.createWallet(walletOpts, function(err, walletId) { if (err) return cb(err); async.each(_.range(1, n + 1), function(i, cb) { var copayerOpts = { - walletId: id, - id: '' + i, + walletId: walletId, name: 'copayer ' + i, xPubKey: someXPubKeys[i - 1], xPubKeySignature: someXPubKeysSignatures[i - 1], }; - server.joinWallet(copayerOpts, function(err) { + server.joinWallet(copayerOpts, function(err, copayerId) { + copayerIds.push(copayerId); return cb(err); }); }, function(err) { if (err) return new Error('Could not generate wallet'); - helpers.getAuthServer('1', function(s) { + helpers.getAuthServer(copayerIds[0], function(s) { s.getWallet({}, function(err, w) { cb(s, w); }); @@ -236,24 +236,24 @@ describe('Copay server', function() { it('should create and store wallet', function(done) { var opts = { - id: '123', name: 'my wallet', m: 2, n: 3, pubKey: aPubKey, }; - server.createWallet(opts, function(err) { + server.createWallet(opts, function(err, walletId) { should.not.exist(err); - server.storage.fetchWallet('123', function(err, wallet) { + server.storage.fetchWallet(walletId, function(err, wallet) { should.not.exist(err); - wallet.id.should.equal('123'); + wallet.id.should.equal(walletId); wallet.name.should.equal('my wallet'); done(); }); }); }); - it('should fail to recreate existing wallet', function(done) { + // non sense with server generated UUIDs + it.skip('should fail to recreate existing wallet', function(done) { var opts = { id: '123', name: 'my wallet', @@ -322,31 +322,29 @@ describe('Copay server', function() { it('should join existing wallet', function(done) { var walletOpts = { - id: '123', name: 'my wallet', m: 2, n: 3, pubKey: keyPair.pub, }; - server.createWallet(walletOpts, function(err) { + server.createWallet(walletOpts, function(err, walletId) { should.not.exist(err); var copayerOpts = { - walletId: '123', - id: '999', + walletId: walletId, name: 'me', xPubKey: aXPubKey, xPubKeySignature: aXPubKeySignature, }; - server.joinWallet(copayerOpts, function(err) { + server.joinWallet(copayerOpts, function(err, copayerId) { should.not.exist(err); - helpers.getAuthServer('999', function(server) { + helpers.getAuthServer(copayerId, function(server) { server.getWallet({}, function(err, wallet) { - wallet.id.should.equal('123'); + wallet.id.should.equal(walletId); wallet.copayers.length.should.equal(1); var copayer = wallet.copayers[0]; - copayer.id.should.equal('999'); copayer.name.should.equal('me'); + copayer.id.should.equal(copayerId); done(); }); }); @@ -355,59 +353,47 @@ describe('Copay server', function() { }); it('should fail to join non-existent wallet', function(done) { - var walletOpts = { - id: '123', - name: 'my wallet', - m: 2, - n: 3, - pubKey: aPubKey, + var copayerOpts = { + walletId: '234', + name: 'me', + xPubKey: 'dummy', + xPubKeySignature: 'dummy', }; - server.createWallet(walletOpts, function(err) { - should.not.exist(err); - var copayerOpts = { - walletId: '234', - id: '999', - name: 'me', - xPubKey: 'dummy', - xPubKeySignature: 'dummy', - }; - server.joinWallet(copayerOpts, function(err) { - should.exist(err); - done(); - }); + server.joinWallet(copayerOpts, function(err) { + should.exist(err); + done(); }); }); it('should fail to join full wallet', function(done) { var walletOpts = { - id: '123', name: 'my wallet', m: 1, n: 1, pubKey: keyPair.pub, }; - server.createWallet(walletOpts, function(err) { + server.createWallet(walletOpts, function(err,walletId) { should.not.exist(err); var copayer1Opts = { - walletId: '123', + walletId: walletId, id: '111', name: 'me', xPubKey: someXPubKeys[0], xPubKeySignature: someXPubKeysSignatures[0], }; var copayer2Opts = { - walletId: '123', + walletId: walletId, id: '222', name: 'me 2', xPubKey: someXPubKeys[1], xPubKeySignature: someXPubKeysSignatures[1], }; - server.joinWallet(copayer1Opts, function(err) { + server.joinWallet(copayer1Opts, function(err, copayer1Id) { should.not.exist(err); - helpers.getAuthServer('111', function(server) { + helpers.getAuthServer(copayer1Id, function(server) { server.getWallet({}, function(err, wallet) { wallet.status.should.equal('complete'); - server.joinWallet(copayer2Opts, function(err) { + server.joinWallet(copayer2Opts, function(err, copayer2Id) { should.exist(err); err.code.should.equal('WFULL'); err.message.should.equal('Wallet full'); @@ -421,16 +407,15 @@ describe('Copay server', function() { it('should fail to re-join wallet', function(done) { var walletOpts = { - id: '123', name: 'my wallet', m: 1, n: 1, pubKey: keyPair.pub, }; - server.createWallet(walletOpts, function(err) { + server.createWallet(walletOpts, function(err, walletId) { should.not.exist(err); var copayerOpts = { - walletId: '123', + walletId: walletId, id: '111', name: 'me', xPubKey: someXPubKeys[0], @@ -457,11 +442,10 @@ describe('Copay server', function() { n: 1, pubKey: aPubKey, }; - server.createWallet(walletOpts, function(err) { + server.createWallet(walletOpts, function(err, walletId) { should.not.exist(err); var copayerOpts = { - walletId: '123', - id: '111', + walletId: walletId, name: 'me', xPubKey: someXPubKeys[0], xPubKeySignature: 'bad sign', @@ -507,11 +491,10 @@ describe('Copay server', function() { n: 1, pubKey: aPubKey, }; - server.createWallet(walletOpts, function(err) { + server.createWallet(walletOpts, function(err, walletId) { should.not.exist(err); var copayerOpts = { - walletId: '123', - id: '111', + walletId: walletId, name: 'me', xPubKey: someXPubKeys[0], xPubKeySignature: someXPubKeysSignatures[0], @@ -524,7 +507,7 @@ describe('Copay server', function() { }); it('should set pkr and status = complete on last copayer joining (2-3)', function(done) { - helpers.createAndJoinWallet('123', 2, 3, function(server) { + helpers.createAndJoinWallet(2, 3, function(server) { server.getWallet({}, function(err, wallet) { should.not.exist(err); wallet.status.should.equal('complete'); @@ -539,7 +522,7 @@ describe('Copay server', function() { describe('#verifyMessageSignature', function() { var server, wallet; beforeEach(function(done) { - helpers.createAndJoinWallet('123', 2, 2, function(s, w) { + helpers.createAndJoinWallet(2, 2, function(s, w) { server = s; wallet = w; done(); @@ -563,7 +546,7 @@ describe('Copay server', function() { message: aText, signature: aTextSignature, }; - helpers.getAuthServer('2', function (server) { + helpers.getAuthServer(wallet.copayers[1].id, function(server) { server.verifyMessageSignature(opts, function(err, isValid) { should.not.exist(err); isValid.should.be.false; @@ -576,7 +559,7 @@ describe('Copay server', function() { describe('#createAddress', function() { var server, wallet; beforeEach(function(done) { - helpers.createAndJoinWallet('123', 2, 2, function(s, w) { + helpers.createAndJoinWallet(2, 2, function(s, w) { server = s; wallet = w; done(); @@ -608,8 +591,7 @@ describe('Copay server', function() { }); }); - it.skip('should fail to create address when wallet is not complete', function(done) { - }); + it.skip('should fail to create address when wallet is not complete', function(done) {}); it('should create many addresses on simultaneous requests', function(done) { async.map(_.range(10), function(i, cb) { @@ -684,7 +666,7 @@ describe('Copay server', function() { describe('#createTx', function() { var server, wallet; beforeEach(function(done) { - helpers.createAndJoinWallet('123', 2, 2, function(s, w) { + helpers.createAndJoinWallet(2, 2, function(s, w) { server = s; wallet = w; server.createAddress({ @@ -726,11 +708,7 @@ describe('Copay server', function() { }); }); - it.skip('should fail to create tx when wallet is not complete', function(done) { - }); - - it.skip('should fail to create tx when wallet is not complete', function(done) { - }); + it.skip('should fail to create tx when wallet is not complete', function(done) {}); it('should fail to create tx when insufficient funds', function(done) { helpers.createUtxos(server, wallet, helpers.toSatoshi([100]), function(utxos) { @@ -845,7 +823,7 @@ describe('Copay server', function() { var server, wallet, txid; beforeEach(function(done) { - helpers.createAndJoinWallet('123', 2, 2, function(s, w) { + helpers.createAndJoinWallet(2, 2, function(s, w) { server = s; wallet = w; server.createAddress({ @@ -904,7 +882,7 @@ describe('Copay server', function() { }); }); }); - it('should fail on invalid signature', function(done) { + it('should fail on invalid signature', function(done) { server.getPendingTxs({}, function(err, txs) { var tx = txs[0]; tx.id.should.equal(txid); @@ -927,7 +905,7 @@ describe('Copay server', function() { var server, wallet, utxos; beforeEach(function(done) { - helpers.createAndJoinWallet('123', 1, 1, function(s, w) { + helpers.createAndJoinWallet(1, 1, function(s, w) { server = s; wallet = w; server.createAddress({ @@ -1016,26 +994,26 @@ describe('Copay server', function() { }); describe('Multisignature wallet', function() { - it.skip('all copayers should see pending proposal created by one copayer', function (done) { - }); + it.skip('all copayers should see pending proposal created by one copayer', function(done) {}); - it.skip('tx proposals should not be broadcast until quorum is reached', function (done) { - }); + it.skip('tx proposals should not be broadcast until quorum is reached', function(done) {}); - it.skip('tx proposals should accept as many rejections as possible without finally rejecting', function (done) { - }); + it.skip('tx proposals should accept as many rejections as possible without finally rejecting', function(done) {}); - it.skip('proposal creator should be able to delete proposal if there are no other signatures', function (done) { - }); + it.skip('proposal creator should be able to delete proposal if there are no other signatures', function(done) {}); }); describe('#getTxs', function() { var server, wallet, clock; beforeEach(function(done) { + if (server) + return done(); + + this.timeout(5000); console.log('\tCreating TXS...'); clock = sinon.useFakeTimers(); - helpers.createAndJoinWallet('123', 1, 1, function(s, w) { + helpers.createAndJoinWallet(1, 1, function(s, w) { server = s; wallet = w; server.createAddress({ @@ -1052,8 +1030,9 @@ describe('Copay server', function() { server.createTx(txOpts, function(err, tx) { next(); }); - }, - done + }, function(err) { + return done(err); + } ); }); }); @@ -1069,8 +1048,8 @@ describe('Copay server', function() { limit: 8 }, function(err, txps) { should.not.exist(err); - var times = _.pluck(txps,'createdOn'); - times.should.deep.equal([90,80,70,60]); + var times = _.pluck(txps, 'createdOn'); + times.should.deep.equal([90, 80, 70, 60]); done(); }); }); @@ -1081,8 +1060,8 @@ describe('Copay server', function() { limit: 5 }, function(err, txps) { should.not.exist(err); - var times = _.pluck(txps,'createdOn'); - times.should.deep.equal([50,40,30,20,10]); + var times = _.pluck(txps, 'createdOn'); + times.should.deep.equal([50, 40, 30, 20, 10]); done(); }); }); @@ -1092,18 +1071,17 @@ describe('Copay server', function() { limit: 4 }, function(err, txps) { should.not.exist(err); - var times = _.pluck(txps,'createdOn'); - times.should.deep.equal([90,80,70,60]); + var times = _.pluck(txps, 'createdOn'); + times.should.deep.equal([90, 80, 70, 60]); done(); }); }); it('should pull all txs', function(done) { - server.getTxs({ - }, function(err, txps) { + server.getTxs({}, function(err, txps) { should.not.exist(err); - var times = _.pluck(txps,'createdOn'); - times.should.deep.equal([90,80,70,60,50,40,30,20,10]); + var times = _.pluck(txps, 'createdOn'); + times.should.deep.equal([90, 80, 70, 60, 50, 40, 30, 20, 10]); done(); }); }); @@ -1115,8 +1093,8 @@ describe('Copay server', function() { maxTs: 70, }, function(err, txps) { should.not.exist(err); - var times = _.pluck(txps,'createdOn'); - times.should.deep.equal([70,60,50]); + var times = _.pluck(txps, 'createdOn'); + times.should.deep.equal([70, 60, 50]); done(); }); });