From d6733de12d6e9ebb3d9f5a266d05e154a121753a Mon Sep 17 00:00:00 2001 From: Matias Alejo Garcia Date: Mon, 2 Feb 2015 09:21:29 -0300 Subject: [PATCH 1/7] add indexes --- lib/model/copayer.js | 7 +------ lib/model/wallet.js | 3 +++ 2 files changed, 4 insertions(+), 6 deletions(-) diff --git a/lib/model/copayer.js b/lib/model/copayer.js index 9596534..0f0abe0 100644 --- a/lib/model/copayer.js +++ b/lib/model/copayer.js @@ -16,9 +16,7 @@ function Copayer(opts) { this.name = opts.name; this.xPubKey = opts.xPubKey; this.xPubKeySignature = opts.xPubKeySignature; // So third parties can check independently - if (opts.xPubKey) { - this.signingPubKey = this.getSigningPubKey(); - } + this.signingPubKey = opts.signingPubKey || this.getSigningPubKey(); }; Copayer.prototype.getSigningPubKey = function () { @@ -29,15 +27,12 @@ Copayer.prototype.getSigningPubKey = function () { Copayer.fromObj = function (obj) { var x = new Copayer(); - x.version = obj.version; x.createdOn = obj.createdOn; x.id = obj.id; x.name = obj.name; x.xPubKey = obj.xPubKey; x.xPubKeySignature = obj.xPubKeySignature; x.signingPubKey = obj.signingPubKey; - - return x; }; diff --git a/lib/model/wallet.js b/lib/model/wallet.js index 56e3592..f09dc48 100644 --- a/lib/model/wallet.js +++ b/lib/model/wallet.js @@ -19,6 +19,9 @@ function Wallet(opts) { this.addressIndex = 0; this.copayers = []; this.pubKey = opts.pubKey; + + this.receiveAddressIndex = 0; + this.changeAddressIndex = 0; }; /* For compressed keys, m*73 + n*34 <= 496 */ From 7b68c14fb7f96370338e50e926d59fb40fdd027e Mon Sep 17 00:00:00 2001 From: Matias Alejo Garcia Date: Mon, 2 Feb 2015 09:36:55 -0300 Subject: [PATCH 2/7] add indexes to copayer --- lib/model/copayer.js | 18 ++++++++++++++++++ lib/server.js | 1 + test/integration.js | 3 +++ 3 files changed, 22 insertions(+) diff --git a/lib/model/copayer.js b/lib/model/copayer.js index 0f0abe0..0c1c7e9 100644 --- a/lib/model/copayer.js +++ b/lib/model/copayer.js @@ -24,6 +24,24 @@ Copayer.prototype.getSigningPubKey = function () { return HDPublicKey.fromString(this.xPubKey).derive(MESSAGE_SIGNING_PATH).publicKey.toString(); }; +Copayer.prototype.addAddress = function (isChange) { + if (isChange) { + this.changeAddressIndex++; + } else { + this.receiveAddressIndex++; + } +}; + + +Copayer.prototype.getCurrentAddressPath = function (isChange) { + return +}; + +Copayer.prototype.getNewAddressPath = function (isChange) { + this.addAddress(isChange); + return this.currentAddressPath(isChange); +}; + Copayer.fromObj = function (obj) { var x = new Copayer(); diff --git a/lib/server.js b/lib/server.js index 06c95dd..05f7618 100644 --- a/lib/server.js +++ b/lib/server.js @@ -139,6 +139,7 @@ CopayServer.prototype._verifySignature = function (text, signature, pubKey) { name: opts.name, xPubKey: opts.xPubKey, xPubKeySignature: opts.xPubKeySignature, + copayerIndex: wallet.copayers.length, }); wallet.addCopayer(copayer); diff --git a/test/integration.js b/test/integration.js index a88657e..cec51a5 100644 --- a/test/integration.js +++ b/test/integration.js @@ -471,6 +471,9 @@ describe('Copay server', function() { should.not.exist(err); wallet.status.should.equal('complete'); wallet.publicKeyRing.length.should.equal(3); + _.each([0,1,2], function(i) { + wallet.copayers[i].copayerIndex.should.equal(i); + }); done(); }); }); From 6227bb1e5f531b2374f5c743e96e84979437d259 Mon Sep 17 00:00:00 2001 From: Matias Alejo Garcia Date: Mon, 2 Feb 2015 11:55:03 -0300 Subject: [PATCH 3/7] fix indexes, inheritance --- lib/model/addressable.js | 34 ++++++++++++ lib/model/copayer.js | 13 ++++- lib/model/hdpath.js | 116 +++++++++++++++++++++++++++++++++++++++ lib/model/wallet.js | 11 ++-- lib/server.js | 44 +++++++-------- test/hdpath.js | 71 ++++++++++++++++++++++++ test/integration.js | 22 +++++++- 7 files changed, 277 insertions(+), 34 deletions(-) create mode 100644 lib/model/addressable.js create mode 100644 lib/model/hdpath.js create mode 100644 test/hdpath.js diff --git a/lib/model/addressable.js b/lib/model/addressable.js new file mode 100644 index 0000000..b62c1a4 --- /dev/null +++ b/lib/model/addressable.js @@ -0,0 +1,34 @@ +var _ = require('lodash'); +var HDPath = require('./hdpath'); + +function Addressable (opts) { + this.receiveAddressIndex = 0; + this.changeAddressIndex = 0; + this.copayerIndex = ( opts && _.isNumber(opts.copayerIndex)) ? opts.copayerIndex : HDPath.SHARED_INDEX; +}; + + +Addressable.prototype.fromObj = function (obj) { + this.receiveAddressIndex = obj.receiveAddressIndex; + this.changeAddressIndex = obj.changeAddressIndex; + this.copayerIndex = obj.copayerIndex; +}; + +Addressable.prototype.addAddress = function (isChange) { + if (isChange) { + this.changeAddressIndex++; + } else { + this.receiveAddressIndex++; + } +}; + +Addressable.prototype.getCurrentAddressPath = function (isChange) { + return HDPath.Branch(isChange ? this.changeAddressIndex : this.receiveAddressIndex, isChange, this.copayerIndex); +}; + +Addressable.prototype.getNewAddressPath = function (isChange) { + this.addAddress(isChange); + return this.currentAddressPath(isChange); +}; + +module.exports = Addressable; diff --git a/lib/model/copayer.js b/lib/model/copayer.js index 0c1c7e9..c642dcb 100644 --- a/lib/model/copayer.js +++ b/lib/model/copayer.js @@ -1,13 +1,19 @@ 'use strict'; var _ = require('lodash'); +var util = require('util'); + var Bitcore = require('bitcore'); var HDPublicKey = Bitcore.HDPublicKey; +var Addressable = require('./Addressable'); + + var VERSION = '1.0.0'; var MESSAGE_SIGNING_PATH = "m/1/0"; function Copayer(opts) { + Copayer.super_.apply(this, arguments); opts = opts || {}; this.version = VERSION; @@ -19,6 +25,8 @@ function Copayer(opts) { this.signingPubKey = opts.signingPubKey || this.getSigningPubKey(); }; +util.inherits(Copayer, Addressable); + Copayer.prototype.getSigningPubKey = function () { if (!this.xPubKey) return null; return HDPublicKey.fromString(this.xPubKey).derive(MESSAGE_SIGNING_PATH).publicKey.toString(); @@ -32,7 +40,6 @@ Copayer.prototype.addAddress = function (isChange) { } }; - Copayer.prototype.getCurrentAddressPath = function (isChange) { return }; @@ -51,7 +58,9 @@ Copayer.fromObj = function (obj) { x.xPubKey = obj.xPubKey; x.xPubKeySignature = obj.xPubKeySignature; x.signingPubKey = obj.signingPubKey; -}; + Wallet.super_.prototype.fromObj.apply(this, [obj]); + return x; +}; module.exports = Copayer; diff --git a/lib/model/hdpath.js b/lib/model/hdpath.js new file mode 100644 index 0000000..7a03fa8 --- /dev/null +++ b/lib/model/hdpath.js @@ -0,0 +1,116 @@ +'use strict'; + +// 90.2% typed (by google's closure-compiler account) + +var preconditions = require('preconditions').singleton(); +var _ = require('lodash'); + +/** + * @namespace + * @desc + * HDPath contains helper functions to handle BIP32 branches as + * Copay uses them. + * Based on https://github.com/maraoz/bips/blob/master/bip-NNNN.mediawiki + *
+ * m / purpose' / copayerIndex / change:boolean / addressIndex
+ * 
+ */ +var HDPath = {}; + +/** + * @desc Copay's BIP45 purpose code + * @const + * @type number + */ +HDPath.PURPOSE = 45; + +/** + * @desc Maximum number for non-hardened values (BIP32) + * @const + * @type number + */ +HDPath.MAX_NON_HARDENED = 0x80000000 - 1; + +/** + * @desc Shared Index: used for creating addresses for no particular purpose + * @const + * @type number + */ +HDPath.SHARED_INDEX = HDPath.MAX_NON_HARDENED - 0; + +/** + * @desc ??? + * @const + * @type number + */ +HDPath.ID_INDEX = HDPath.MAX_NON_HARDENED - 1; + +/** + * @desc BIP45 prefix for COPAY + * @const + * @type string + */ +HDPath.BIP45_PUBLIC_PREFIX = 'm/' + HDPath.PURPOSE + '\''; + +/** + * @desc Retrieve a string to be used with bitcore representing a Copay branch + * @param {number} addressIndex - the last value of the HD derivation + * @param {boolean} isChange - whether this is a change address or a receive + * @param {number} copayerIndex - the index of the copayer in the pubkeyring + * @return {string} - the path for the HD derivation + */ +HDPath.Branch = function(addressIndex, isChange, copayerIndex) { + preconditions.checkArgument(_.isNumber(addressIndex)); + preconditions.checkArgument(_.isBoolean(isChange)); + + var ret = 'm/' + + (typeof copayerIndex !== 'undefined' ? copayerIndex : HDPath.SHARED_INDEX) + '/' + + (isChange ? 1 : 0) + '/' + + addressIndex; + return ret; +}; + +/** + * @desc ??? + * @param {number} addressIndex - the last value of the HD derivation + * @param {boolean} isChange - whether this is a change address or a receive + * @param {number} copayerIndex - the index of the copayer in the pubkeyring + * @return {string} - the path for the HD derivation + */ +HDPath.FullBranch = function(addressIndex, isChange, copayerIndex) { + preconditions.checkArgument(_.isNumber(addressIndex)); + preconditions.checkArgument(_.isBoolean(isChange)); + + var sub = HDPath.Branch(addressIndex, isChange, copayerIndex); + sub = sub.substring(2); + return HDPath.BIP45_PUBLIC_PREFIX + '/' + sub; +}; + +/** + * @desc + * Decompose a string and retrieve its arguments as if it where a Copay address. + * @param {string} path - the HD path + * @returns {Object} an object with three keys: addressIndex, isChange, and + * copayerIndex + */ +HDPath.indexesForPath = function(path) { + preconditions.checkArgument(_.isString(path)); + + var s = path.split('/'); + return { + isChange: s[3] === '1', + addressIndex: parseInt(s[4], 10), + copayerIndex: parseInt(s[2], 10) + }; +}; + +/** + * @desc The ID for a shared branch + */ +HDPath.IdFullBranch = HDPath.FullBranch(0, false, HDPath.ID_INDEX); +/** + * @desc Partial ID for a shared branch + */ +HDPath.IdBranch = HDPath.Branch(0, false, HDPath.ID_INDEX); + +module.exports = HDPath; diff --git a/lib/model/wallet.js b/lib/model/wallet.js index f09dc48..358419a 100644 --- a/lib/model/wallet.js +++ b/lib/model/wallet.js @@ -1,11 +1,15 @@ 'use strict'; var _ = require('lodash'); +var util = require('util'); var Copayer = require('./copayer'); +var Addressable = require('./Addressable'); + var VERSION = '1.0.0'; function Wallet(opts) { + Wallet.super_.apply(this, arguments); opts = opts || {}; this.version = VERSION; @@ -19,9 +23,6 @@ function Wallet(opts) { this.addressIndex = 0; this.copayers = []; this.pubKey = opts.pubKey; - - this.receiveAddressIndex = 0; - this.changeAddressIndex = 0; }; /* For compressed keys, m*73 + n*34 <= 496 */ @@ -39,6 +40,7 @@ Wallet.COPAYER_PAIR_LIMITS = { 11: 1, 12: 1, }; +util.inherits(Wallet, Addressable); /** * Get the maximum allowed number of required copayers. @@ -57,7 +59,6 @@ Wallet.verifyCopayerLimits = function (m, n) { Wallet.fromObj = function (obj) { var x = new Wallet(); - x.version = obj.version; x.createdOn = obj.createdOn; x.id = obj.id; x.name = obj.name; @@ -65,12 +66,12 @@ Wallet.fromObj = function (obj) { x.n = obj.n; x.status = obj.status; x.publicKeyRing = obj.publicKeyRing; - x.addressIndex = obj.addressIndex; x.copayers = _.map(obj.copayers, function (copayer) { return new Copayer(copayer); }); x.pubKey = obj.pubKey; + Wallet.super_.prototype.fromObj.apply(this, [obj]); return x; }; diff --git a/lib/server.js b/lib/server.js index 05f7618..1e2c3e0 100644 --- a/lib/server.js +++ b/lib/server.js @@ -75,10 +75,10 @@ CopayServer.prototype.createWallet = function (opts, cb) { name: opts.name, m: opts.m, n: opts.n, - network: network, + network: opts.network || 'livenet', pubKey: pubKey, }); - + self.storage.storeWallet(wallet, cb); }); }; @@ -87,7 +87,7 @@ CopayServer.prototype.createWallet = function (opts, cb) { * Retrieves a wallet from storage. * @param {Object} opts * @param {string} opts.id - The wallet id. - * @returns {Object} wallet + * @returns {Object} wallet */ CopayServer.prototype.getWallet = function (opts, cb) { var self = this; @@ -106,8 +106,8 @@ CopayServer.prototype.createWallet = function (opts, cb) { * @param signature * @param pubKey */ -CopayServer.prototype._verifySignature = function (text, signature, pubKey) { - return SignUtils.verify( text, signature, pubKey); +CopayServer.prototype._verifySignature = function(text, signature, pubKey) { + return SignUtils.verify(text, signature, pubKey); }; /** @@ -132,8 +132,11 @@ CopayServer.prototype._verifySignature = function (text, signature, pubKey) { return cb('Bad request'); } - if (_.find(wallet.copayers, { xPubKey: opts.xPubKey })) return cb('Copayer already in wallet'); + if (_.find(wallet.copayers, { + xPubKey: opts.xPubKey + })) return cb('Copayer already in wallet'); if (wallet.copayers.length == wallet.n) return cb('Wallet full'); + var copayer = new Copayer({ id: opts.id, name: opts.name, @@ -145,18 +148,12 @@ CopayServer.prototype._verifySignature = function (text, signature, pubKey) { wallet.addCopayer(copayer); self.storage.storeWallet(wallet, function (err) { - if (err) return cb(err); - - return cb(); + return cb(err); }); }); }); }; -CopayServer.prototype._doCreateAddress = function (pkr, index, isChange) { - throw 'not implemented'; -}; - /** * * TODO: How this is going to be authenticated? @@ -165,7 +162,7 @@ CopayServer.prototype._doCreateAddress = function (pkr, index, isChange) { * @param {Object} opts * @param {string} opts.walletId - The wallet id. * @param {truthy} opts.isChange - Indicates whether this is a regular address or a change address. - * @returns {Address} address + * @returns {Address} address */ CopayServer.prototype.createAddress = function (opts, cb) { var self = this; @@ -175,13 +172,12 @@ CopayServer.prototype._doCreateAddress = function (pkr, index, isChange) { Utils.runLocked(opts.walletId, cb, function (cb) { self.getWallet({ id: opts.walletId }, function (err, wallet) { if (err) return cb(err); - var index = wallet.addressIndex++; - self.storage.storeWallet(wallet, function (err) { + self.storage.storeWallet(wallet, function(err) { if (err) return cb(err); var address = self._doCreateAddress(wallet.publicKeyRing, index, opts.isChange); - self.storage.storeAddress(opts.walletId, address, function (err) { + self.storage.storeAddress(opts.walletId, address, function(err) { if (err) return cb(err); return cb(null, address); @@ -268,7 +264,6 @@ CopayServer.prototype._getUtxos = function (opts, cb) { dictionary[input].locked = true; } }); - return cb(null, utxos); }); }); @@ -280,7 +275,7 @@ CopayServer.prototype._getUtxos = function (opts, cb) { * Creates a new transaction proposal. * @param {Object} opts * @param {string} opts.walletId - The wallet id. - * @returns {Object} balance - Total amount & locked amount. + * @returns {Object} balance - Total amount & locked amount. */ CopayServer.prototype.getBalance = function (opts, cb) { var self = this; @@ -299,7 +294,7 @@ CopayServer.prototype._getUtxos = function (opts, cb) { }; -CopayServer.prototype._createRawTx = function (txp) { +CopayServer.prototype._createRawTx = function(txp) { var rawTx = new Bitcore.Transaction() .from(tx.inputs) .to(txp.toAddress, txp.amount) @@ -308,7 +303,7 @@ CopayServer.prototype._createRawTx = function (txp) { return rawTx; }; -CopayServer.prototype._selectUtxos = function (txp, utxos) { +CopayServer.prototype._selectUtxos = function(txp, utxos) { var i = 0; var total = 0; var selected = []; @@ -333,7 +328,7 @@ CopayServer.prototype._selectUtxos = function (txp, utxos) { * @param {string} opts.toAddress - Destination address. * @param {number} opts.amount - Amount to transfer in satoshi. * @param {string} opts.message - A message to attach to this transaction. - * @returns {TxProposal} Transaction proposal. + * @returns {TxProposal} Transaction proposal. */ CopayServer.prototype.createTx = function (opts, cb) { var self = this; @@ -414,7 +409,7 @@ CopayServer.prototype.signTx = function (opts, cb) { }); }); }); -}; +}; /** * Reject a transaction proposal. @@ -450,7 +445,7 @@ CopayServer.prototype.rejectTx = function (opts, cb) { * Retrieves all pending transaction proposals. * @param {Object} opts * @param {string} opts.walletId - The wallet id. - * @returns {TxProposal[]} Transaction proposal. + * @returns {TxProposal[]} Transaction proposal. */ CopayServer.prototype.getPendingTxs = function (opts, cb) { var self = this; @@ -461,7 +456,6 @@ CopayServer.prototype.getPendingTxs = function (opts, cb) { if (err) return cb(err); var pending = _.filter(txps, { status: 'pending' }); - return cb(null, pending); }); }; diff --git a/test/hdpath.js b/test/hdpath.js new file mode 100644 index 0000000..ba74469 --- /dev/null +++ b/test/hdpath.js @@ -0,0 +1,71 @@ +'use strict'; + +var HDPath = require('../lib/model/hdpath'); + +describe('HDPath model', function() { + it('should have the correct constants', function() { + HDPath.MAX_NON_HARDENED.should.equal(Math.pow(2, 31) - 1); + HDPath.SHARED_INDEX.should.equal(HDPath.MAX_NON_HARDENED); + HDPath.ID_INDEX.should.equal(HDPath.SHARED_INDEX - 1); + HDPath.IdFullBranch.should.equal('m/45\'/2147483646/0/0'); + }); + + it('should get the correct branches', function() { + // shared branch (no cosigner index specified) + HDPath.FullBranch(0, false).should.equal('m/45\'/2147483647/0/0'); + + // copayer 0, address 0, external address (receiving) + HDPath.FullBranch(0, false, 0).should.equal('m/45\'/0/0/0'); + + // copayer 0, address 10, external address (receiving) + HDPath.FullBranch(0, false, 10).should.equal('m/45\'/10/0/0'); + + // copayer 0, address 0, internal address (change) + HDPath.FullBranch(0, true, 0).should.equal('m/45\'/0/1/0'); + + // copayer 0, address 10, internal address (change) + HDPath.FullBranch(10, true, 0).should.equal('m/45\'/0/1/10'); + + // copayer 7, address 10, internal address (change) + HDPath.FullBranch(10, true, 7).should.equal('m/45\'/7/1/10'); + }); + + [ + ['m/45\'/0/0/0', { + index: 0, + isChange: false + }], + ['m/45\'/0/0/1', { + index: 1, + isChange: false + }], + ['m/45\'/0/0/2', { + index: 2, + isChange: false + }], + ['m/45\'/0/1/0', { + index: 0, + isChange: true + }], + ['m/45\'/0/1/1', { + index: 1, + isChange: true + }], + ['m/45\'/0/1/2', { + index: 2, + isChange: true + }], + ['m/45\'/0/0/900', { + index: 900, + isChange: false + }], + ].forEach(function(datum) { + var path = datum[0]; + var result = datum[1]; + it('should get the correct indexes for path ' + path, function() { + var i = HDPath.indexesForPath(path); + i.addressIndex.should.equal(result.index); + i.isChange.should.equal(result.isChange); + }); + }); +}); diff --git a/test/integration.js b/test/integration.js index cec51a5..131a50f 100644 --- a/test/integration.js +++ b/test/integration.js @@ -69,6 +69,7 @@ helpers.createAndJoinWallet = function(id, m, n, cb) { if (err) return cb(err); async.each(_.range(1, n + 1), function(i, cb) { + var copayerOpts = { walletId: id, id: '' + i, @@ -76,11 +77,13 @@ helpers.createAndJoinWallet = function(id, m, n, cb) { xPubKey: someXPubKeys[i - 1], xPubKeySignature: someXPubKeysSignatures[i - 1], }; + server.joinWallet(copayerOpts, function(err) { return cb(err); }); }, function(err) { if (err) return cb(err); + server.getWallet({ id: id, includeCopayers: true @@ -461,7 +464,20 @@ describe('Copay server', function() { }); }); + it('should set index in 1-1 wallet creation.', function(done) { + helpers.createAndJoinWallet('123', 1, 1, function(err, wallet) { + wallet.receiveAddressIndex.should.equal(0); + wallet.changeAddressIndex.should.equal(0); + wallet.copayerIndex.should.equal(0x80000000 - 1); + var copayer = wallet.copayers[0]; + copayer.receiveAddressIndex.should.equal(0); + copayer.changeAddressIndex.should.equal(0); + copayer.copayerIndex.should.equal(0); + done(); + }); + }); + it('should set pkr and status = complete on last copayer joining', function(done) { helpers.createAndJoinWallet('123', 2, 3, function(err, wallet) { @@ -472,7 +488,10 @@ describe('Copay server', function() { wallet.status.should.equal('complete'); wallet.publicKeyRing.length.should.equal(3); _.each([0,1,2], function(i) { - wallet.copayers[i].copayerIndex.should.equal(i); + var copayer = wallet.copayers[i]; + copayer.receiveAddressIndex.should.equal(0); + copayer.changeAddressIndex.should.equal(0); + copayer.copayerIndex.should.equal(i); }); done(); }); @@ -481,7 +500,6 @@ describe('Copay server', function() { }); - describe('#verifyMessageSignature', function() { beforeEach(function() { server = new CopayServer({ From 0bccc864fb912f44542893125d48d8137c54e757 Mon Sep 17 00:00:00 2001 From: Matias Alejo Garcia Date: Mon, 2 Feb 2015 16:16:14 -0300 Subject: [PATCH 4/7] create address + test in wallet --- lib/model/addressable.js | 2 +- lib/model/copayer.js | 13 +++++++------ lib/model/wallet.js | 28 ++++++++++++++++++++++++++++ lib/server.js | 9 +++++++-- test/integration.js | 2 +- 5 files changed, 44 insertions(+), 10 deletions(-) diff --git a/lib/model/addressable.js b/lib/model/addressable.js index b62c1a4..b67d470 100644 --- a/lib/model/addressable.js +++ b/lib/model/addressable.js @@ -23,7 +23,7 @@ Addressable.prototype.addAddress = function (isChange) { }; Addressable.prototype.getCurrentAddressPath = function (isChange) { - return HDPath.Branch(isChange ? this.changeAddressIndex : this.receiveAddressIndex, isChange, this.copayerIndex); + return HDPath.FullBranch(isChange ? this.changeAddressIndex : this.receiveAddressIndex, isChange, this.copayerIndex); }; Addressable.prototype.getNewAddressPath = function (isChange) { diff --git a/lib/model/copayer.js b/lib/model/copayer.js index c642dcb..2ba89f2 100644 --- a/lib/model/copayer.js +++ b/lib/model/copayer.js @@ -13,15 +13,16 @@ var VERSION = '1.0.0'; var MESSAGE_SIGNING_PATH = "m/1/0"; function Copayer(opts) { - Copayer.super_.apply(this, arguments); opts = opts || {}; + opts.copayerIndex = opts.copayerIndex || 0; + Copayer.super_.apply(this, [opts]); 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.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 = opts.signingPubKey || this.getSigningPubKey(); }; diff --git a/lib/model/wallet.js b/lib/model/wallet.js index 358419a..40193f0 100644 --- a/lib/model/wallet.js +++ b/lib/model/wallet.js @@ -3,6 +3,10 @@ var _ = require('lodash'); var util = require('util'); +var Bitcore = require('bitcore'); +var BitcoreAddress = Bitcore.Address; + +var Address = require('./address'); var Copayer = require('./copayer'); var Addressable = require('./Addressable'); @@ -23,6 +27,7 @@ function Wallet(opts) { this.addressIndex = 0; this.copayers = []; this.pubKey = opts.pubKey; + this.isTestnet = false; }; /* For compressed keys, m*73 + n*34 <= 496 */ @@ -70,6 +75,7 @@ Wallet.fromObj = function (obj) { return new Copayer(copayer); }); x.pubKey = obj.pubKey; + x.isTestnet = obj.isTestnet; Wallet.super_.prototype.fromObj.apply(this, [obj]); return x; @@ -88,4 +94,26 @@ Wallet.prototype.getCopayer = function (copayerId) { return _.find(this.copayers, { id: copayerId }); }; + +Wallet.prototype._getBitcoreNetwork = function () { + return this.isTestnet ? Bitcore.Networks.testnet : Bitcore.Networks.livenet; +}; + + +Wallet.prototype.createAddress = function (path) { + + var publicKeys = _.map(this.copayers, function(copayer) { + var xpub = new Bitcore.HDPublicKey(copayer.xPubKey); + return xpub.derive(path).publicKey; + }); + + var bitcoreAddress = BitcoreAddress.createMultisig(publicKeys, this.m, this._getBitcoreNetwork()); + + return new Address({ + address: bitcoreAddress.toString(), + path: path, + }); +}; + + module.exports = Wallet; diff --git a/lib/server.js b/lib/server.js index 1e2c3e0..3fb6ad7 100644 --- a/lib/server.js +++ b/lib/server.js @@ -166,17 +166,22 @@ CopayServer.prototype._verifySignature = function(text, signature, pubKey) { */ CopayServer.prototype.createAddress = function (opts, cb) { var self = this; + var isChange = opts.isChange; Utils.checkRequired(opts, ['walletId', 'isChange']); Utils.runLocked(opts.walletId, cb, function (cb) { self.getWallet({ id: opts.walletId }, function (err, wallet) { if (err) return cb(err); - var index = wallet.addressIndex++; + + var copayer = wallet.copayers[0]; // TODO: Assign copayer from authentication. + + var path = copayer.getNewAddressPath(isChange); self.storage.storeWallet(wallet, function(err) { if (err) return cb(err); - var address = self._doCreateAddress(wallet.publicKeyRing, index, opts.isChange); + var address = wallet.createAddress(path); + self.storage.storeAddress(opts.walletId, address, function(err) { if (err) return cb(err); diff --git a/test/integration.js b/test/integration.js index 131a50f..de32579 100644 --- a/test/integration.js +++ b/test/integration.js @@ -479,7 +479,7 @@ describe('Copay server', function() { }); - it('should set pkr and status = complete on last copayer joining', function(done) { + it('should set pkr and status = complete on last copayer joining (2-3)', function(done) { helpers.createAndJoinWallet('123', 2, 3, function(err, wallet) { server.getWallet({ id: '123' From 090faf62a20ddfdf1c0c7697573bbb4a6d9e54eb Mon Sep 17 00:00:00 2001 From: Matias Alejo Garcia Date: Mon, 2 Feb 2015 16:16:28 -0300 Subject: [PATCH 5/7] add models tests --- test/copayer.js | 28 +++++++++++++++ test/wallet.js | 91 +++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 119 insertions(+) create mode 100644 test/copayer.js create mode 100644 test/wallet.js diff --git a/test/copayer.js b/test/copayer.js new file mode 100644 index 0000000..a38e22a --- /dev/null +++ b/test/copayer.js @@ -0,0 +1,28 @@ +'use strict'; + +var _ = require('lodash'); +var chai = require('chai'); +var sinon = require('sinon'); +var should = chai.should(); +var Copayer = require('../lib/model/copayer'); + + +describe('Copayer', function() { + + describe('#getCurrentAddressPath', function() { + it('return a valid BIP32 path for defaut copayer Index', function() { + var c = new Copayer(); + c.getCurrentAddressPath(false).should.equal('m/45\'/0/0/0'); + c.getCurrentAddressPath(true).should.equal('m/45\'/0/1/0'); + }); + + it('return a valid BIP32 path for given index', function() { + var c = new Copayer({ + copayerIndex: 4 + }); + c.getCurrentAddressPath(false).should.equal('m/45\'/4/0/0'); + c.getCurrentAddressPath(true).should.equal('m/45\'/4/1/0'); + }); + }); + +}); diff --git a/test/wallet.js b/test/wallet.js new file mode 100644 index 0000000..7db2925 --- /dev/null +++ b/test/wallet.js @@ -0,0 +1,91 @@ +'use strict'; + +var _ = require('lodash'); +var chai = require('chai'); +var sinon = require('sinon'); +var should = chai.should(); +var Wallet = require('../lib/model/wallet'); + + +describe('Wallet', function() { + + describe('#fromObj', function() { + it('read a wallet', function() { + var w = Wallet.fromObj(testWallet); + w.status.should.equal('complete'); + }); + }); + describe('#createAddress', function() { + it('create an address', function() { + var w = Wallet.fromObj(testWallet); + var a = w.createAddress('m/1/1'); + a.address.should.equal('32HG4C9tWMhWoDoTHFvjmbV5sUJMjWs4vL'); + a.path.should.equal('m/1/1'); + a.createdOn.should.be.above(1); + }); + }); + describe('#getCurrentAddressPath', function() { + it('return a valid BIP32 path for defaut wallet Index', function() { + var w = new Wallet(); + w.getCurrentAddressPath(false).should.equal('m/45\'/2147483647/0/0'); + w.getCurrentAddressPath(true).should.equal('m/45\'/2147483647/1/0'); + }); + + + }); + +}); + + +var testWallet = { + receiveAddressIndex: 0, + changeAddressIndex: 0, + copayerIndex: 2147483647, + createdOn: 1422904188, + id: '123', + name: '123 wallet', + m: 2, + n: 3, + status: 'complete', + publicKeyRing: ['xpub661MyMwAqRbcFLRkhYzK8eQdoywNHJVsJCMQNDoMks5bZymuMcyDgYfnVQYq2Q9npnVmdTAthYGc3N3uxm5sEdnTpSqBc4YYTAhNnoSxCm9', + 'xpub661MyMwAqRbcEzHgVwwxoXksq21rRNsJsn7AFy4VD4PzsEmjjWwsyEiTjsdQviXbqZ5yHVWJR8zFUDgUKkq4R97su3UyNo36Z8hSaCPrv6o', + 'xpub661MyMwAqRbcFXUfkjfSaRwxJbAPpzNUvTiNFjgZwDJ8sZuhyodkP24L4LvsrgThYAAwKkVVSSmL7Ts7o9EHEHPB3EE89roAra7njoSeiMd' + ], + copayers: [{ + receiveAddressIndex: 0, + changeAddressIndex: 0, + copayerIndex: 0, + createdOn: 1422904189, + id: '1', + name: 'copayer 1', + xPubKey: 'xpub661MyMwAqRbcFLRkhYzK8eQdoywNHJVsJCMQNDoMks5bZymuMcyDgYfnVQYq2Q9npnVmdTAthYGc3N3uxm5sEdnTpSqBc4YYTAhNnoSxCm9', + xPubKeySignature: '30440220192ae7345d980f45f908bd63ccad60ce04270d07b91f1a9d92424a07a38af85202201591f0f71dd4e79d9206d2306862e6b8375e13a62c193953d768e884b6fb5a46', + version: '1.0.0', + signingPubKey: '03814ac7decf64321a3c6967bfb746112fdb5b583531cd512cc3787eaf578947dc' + }, { + receiveAddressIndex: 0, + changeAddressIndex: 0, + copayerIndex: 1, + createdOn: 1422904189, + id: '2', + name: 'copayer 2', + xPubKey: 'xpub661MyMwAqRbcEzHgVwwxoXksq21rRNsJsn7AFy4VD4PzsEmjjWwsyEiTjsdQviXbqZ5yHVWJR8zFUDgUKkq4R97su3UyNo36Z8hSaCPrv6o', + xPubKeySignature: '30440220134d13139323ba16ff26471c415035679ee18b2281bf85550ccdf6a370899153022066ef56ff97091b9be7dede8e40f50a3a8aad8205f2e3d8e194f39c20f3d15c62', + version: '1.0.0', + signingPubKey: '03fc086d2bd8b6507b1909b24c198c946e68775d745492ea4ca70adfce7be92a60' + }, { + receiveAddressIndex: 0, + changeAddressIndex: 0, + copayerIndex: 2, + createdOn: 1422904189, + id: '3', + name: 'copayer 3', + xPubKey: 'xpub661MyMwAqRbcFXUfkjfSaRwxJbAPpzNUvTiNFjgZwDJ8sZuhyodkP24L4LvsrgThYAAwKkVVSSmL7Ts7o9EHEHPB3EE89roAra7njoSeiMd', + xPubKeySignature: '304402207a4e7067d823a98fa634f9c9d991b8c42cd0f82da24f686992acf96cdeb5e387022021ceba729bf763fc8e4277f6851fc2b856a82a22b35f20d2eeb23d99c5f5a41c', + version: '1.0.0', + signingPubKey: '0246c30040eda1e36e02629ae8cd2a845fcfa947239c4c703f7ea7550d39cfb43a' + }], + version: '1.0.0', + pubKey: '{"x":"6092daeed8ecb2212869395770e956ffc9bf453f803e700f64ffa70c97a00d80","y":"ba5e7082351115af6f8a9eb218979c7ed1f8aa94214f627ae624ab00048b8650","compressed":true}', + isTestnet: false +}; From b5fbd55d893f8e5c41c6df25dc502c128624d188 Mon Sep 17 00:00:00 2001 From: Matias Alejo Garcia Date: Mon, 2 Feb 2015 16:32:13 -0300 Subject: [PATCH 6/7] getAddress working! --- lib/model/addressable.js | 4 ++-- lib/server.js | 3 ++- test/copayer.js | 8 ++++---- test/integration.js | 27 ++++++++++++++++++++------- test/wallet.js | 4 ++-- 5 files changed, 30 insertions(+), 16 deletions(-) diff --git a/lib/model/addressable.js b/lib/model/addressable.js index b67d470..1c9600f 100644 --- a/lib/model/addressable.js +++ b/lib/model/addressable.js @@ -23,12 +23,12 @@ Addressable.prototype.addAddress = function (isChange) { }; Addressable.prototype.getCurrentAddressPath = function (isChange) { - return HDPath.FullBranch(isChange ? this.changeAddressIndex : this.receiveAddressIndex, isChange, this.copayerIndex); + return HDPath.Branch(isChange ? this.changeAddressIndex : this.receiveAddressIndex, isChange, this.copayerIndex); }; Addressable.prototype.getNewAddressPath = function (isChange) { this.addAddress(isChange); - return this.currentAddressPath(isChange); + return this.getCurrentAddressPath(isChange); }; module.exports = Addressable; diff --git a/lib/server.js b/lib/server.js index 3fb6ad7..8a7c484 100644 --- a/lib/server.js +++ b/lib/server.js @@ -166,7 +166,7 @@ CopayServer.prototype._verifySignature = function(text, signature, pubKey) { */ CopayServer.prototype.createAddress = function (opts, cb) { var self = this; - var isChange = opts.isChange; + var isChange = opts.isChange || false; Utils.checkRequired(opts, ['walletId', 'isChange']); @@ -177,6 +177,7 @@ CopayServer.prototype._verifySignature = function(text, signature, pubKey) { var copayer = wallet.copayers[0]; // TODO: Assign copayer from authentication. var path = copayer.getNewAddressPath(isChange); + self.storage.storeWallet(wallet, function(err) { if (err) return cb(err); diff --git a/test/copayer.js b/test/copayer.js index a38e22a..792fd57 100644 --- a/test/copayer.js +++ b/test/copayer.js @@ -12,16 +12,16 @@ describe('Copayer', function() { describe('#getCurrentAddressPath', function() { it('return a valid BIP32 path for defaut copayer Index', function() { var c = new Copayer(); - c.getCurrentAddressPath(false).should.equal('m/45\'/0/0/0'); - c.getCurrentAddressPath(true).should.equal('m/45\'/0/1/0'); + c.getCurrentAddressPath(false).should.equal('m/0/0/0'); + c.getCurrentAddressPath(true).should.equal('m/0/1/0'); }); it('return a valid BIP32 path for given index', function() { var c = new Copayer({ copayerIndex: 4 }); - c.getCurrentAddressPath(false).should.equal('m/45\'/4/0/0'); - c.getCurrentAddressPath(true).should.equal('m/45\'/4/1/0'); + c.getCurrentAddressPath(false).should.equal('m/4/0/0'); + c.getCurrentAddressPath(true).should.equal('m/4/1/0'); }); }); diff --git a/test/integration.js b/test/integration.js index de32579..c002305 100644 --- a/test/integration.js +++ b/test/integration.js @@ -546,11 +546,7 @@ describe('Copay server', function() { }); }); - it('should create address', function(done) { - server._doCreateAddress = sinon.stub().returns(new Address({ - address: 'addr1', - path: 'path1', - })); + it('should create main address', function(done) { helpers.createAndJoinWallet('123', 2, 2, function(err, wallet) { server.createAddress({ walletId: '123', @@ -558,12 +554,29 @@ describe('Copay server', function() { }, function(err, address) { should.not.exist(err); address.should.exist; - address.address.should.equal('addr1'); - address.path.should.equal('path1'); + address.address.should.equal('3BPfHzwq5j72TBYtYv3Uggk3vyHFHX3QpA'); + address.path.should.equal('m/0/0/1'); + done(); + }); + }); + }); + + + it('should create change address', function(done) { + helpers.createAndJoinWallet('123', 2, 2, function(err, wallet) { + server.createAddress({ + walletId: '123', + isChange: true, + }, function(err, address) { + should.not.exist(err); + address.should.exist; + address.address.should.equal('39Dzj5mBJWvzH7bDfmYzXDvTbZS5HdQ4a4'); + address.path.should.equal('m/0/1/1'); done(); }); }); }); + }); describe('#createTx', function() { diff --git a/test/wallet.js b/test/wallet.js index 7db2925..886cacc 100644 --- a/test/wallet.js +++ b/test/wallet.js @@ -27,8 +27,8 @@ describe('Wallet', function() { describe('#getCurrentAddressPath', function() { it('return a valid BIP32 path for defaut wallet Index', function() { var w = new Wallet(); - w.getCurrentAddressPath(false).should.equal('m/45\'/2147483647/0/0'); - w.getCurrentAddressPath(true).should.equal('m/45\'/2147483647/1/0'); + w.getCurrentAddressPath(false).should.equal('m/2147483647/0/0'); + w.getCurrentAddressPath(true).should.equal('m/2147483647/1/0'); }); From e8687e052786f2e9f6cd29cfe8b6af85a15f635b Mon Sep 17 00:00:00 2001 From: Matias Alejo Garcia Date: Mon, 2 Feb 2015 17:24:56 -0300 Subject: [PATCH 7/7] fix merge --- lib/model/copayer.js | 17 ----------------- 1 file changed, 17 deletions(-) diff --git a/lib/model/copayer.js b/lib/model/copayer.js index 2ba89f2..f70e648 100644 --- a/lib/model/copayer.js +++ b/lib/model/copayer.js @@ -33,23 +33,6 @@ Copayer.prototype.getSigningPubKey = function () { return HDPublicKey.fromString(this.xPubKey).derive(MESSAGE_SIGNING_PATH).publicKey.toString(); }; -Copayer.prototype.addAddress = function (isChange) { - if (isChange) { - this.changeAddressIndex++; - } else { - this.receiveAddressIndex++; - } -}; - -Copayer.prototype.getCurrentAddressPath = function (isChange) { - return -}; - -Copayer.prototype.getNewAddressPath = function (isChange) { - this.addAddress(isChange); - return this.currentAddressPath(isChange); -}; - Copayer.fromObj = function (obj) { var x = new Copayer();