From a8f0401e8e017d7e6d85fffd98e1eac6206df286 Mon Sep 17 00:00:00 2001 From: Matias Alejo Garcia Date: Fri, 28 Nov 2014 18:43:22 -0300 Subject: [PATCH] simplified and cached address derivation --- js/controllers/receive.js | 13 +- js/models/PublicKeyRing.js | 265 ++++++++++++++++--------------------- js/models/Wallet.js | 96 +++++++------- test/PublicKeyRing.js | 59 +++++---- test/Wallet.js | 184 +++++++++++++------------ test/performance.js | 75 ----------- 6 files changed, 304 insertions(+), 388 deletions(-) delete mode 100644 test/performance.js diff --git a/js/controllers/receive.js b/js/controllers/receive.js index a701c0392..a3a34e49c 100644 --- a/js/controllers/receive.js +++ b/js/controllers/receive.js @@ -13,13 +13,12 @@ angular.module('copayApp.controllers').controller('ReceiveController', var w = $rootScope.wallet; $scope.loading = true; $scope.isNewAddr = false; - w.generateAddress(null, function() { - $timeout(function() { - controllerUtils.updateAddressList(); - $scope.loading = false; - $scope.isNewAddr = true; - }, 1); - }); + w.generateAddress(null); + $timeout(function() { + controllerUtils.updateAddressList(); + $scope.loading = false; + $scope.isNewAddr = true; + }, 1); }; $scope.openAddressModal = function(address) { diff --git a/js/models/PublicKeyRing.js b/js/models/PublicKeyRing.js index 0b8e628f8..c12220ff4 100644 --- a/js/models/PublicKeyRing.js +++ b/js/models/PublicKeyRing.js @@ -42,10 +42,21 @@ function PublicKeyRing(opts) { this.publicKeysCache = {}; this.nicknameFor = opts.nicknameFor || {}; this.copayerIds = []; - this.addressToPath = {}; + this.resetCache(); }; +PublicKeyRing.prototype.resetCache = function() { + this.cache = {}; + this.cache.addressToPath = {}; + this.cache.receiveAddresses = []; + this.cache.changeAddresses = []; + + // Non persistent cache + this._isChange = {}; +}; + + /** * @desc Returns an object with only the keys needed to rebuild a PublicKeyRing * @@ -99,9 +110,9 @@ PublicKeyRing.fromObj = function(opts) { pkr.addCopayer(opts.copayersExtPubKeys[k]); } - if (opts._cache){ + if (opts.cache) { log.debug('PublicKeyRing: Using address cache'); - pkr._cacheAddressMap = opts._cache; + pkr.cache = opts.cache; } return pkr; @@ -129,7 +140,7 @@ PublicKeyRing.prototype.toObj = function() { return b.extendedPublicKeyString(); }), nicknameFor: this.nicknameFor, - _cache: this._cacheAddressMap + cache: this.cache, }; }; @@ -300,20 +311,12 @@ PublicKeyRing.prototype.addCopayer = function(newHexaExtendedPublicKey, nickname PublicKeyRing.prototype.getPubKeys = function(index, isChange, copayerIndex) { this._checkKeys(); + log.warn('Slow pubkey derivation...'); var path = HDPath.Branch(index, isChange, copayerIndex); - var pubKeys = this.publicKeysCache[path]; - if (!pubKeys) { - pubKeys = _.map(this.copayersHK, function(hdKey) { + var pubKeys = _.map(this.copayersHK, function(hdKey) { return hdKey.derive(path).eckey.public; - }); - this.publicKeysCache[path] = pubKeys.map(function(pk) { - return pk.toString('hex'); - }); - } else { - pubKeys = pubKeys.map(function(s) { - return new Buffer(s, 'hex'); - }); - } + }); + return pubKeys; }; @@ -347,31 +350,24 @@ PublicKeyRing.prototype.getRedeemScript = function(index, isChange, copayerIndex * @param {number} copayerIndex - the index of the copayer that requested the derivation * @returns {bitcore.Address} */ -PublicKeyRing.prototype.getAddress = function(index, isChange, id) { +PublicKeyRing.prototype._getAddress = function(index, isChange, id) { var copayerIndex = this.getCosigner(id); - if (!this._cachedAddress(index, isChange, id)) { + var path = HDPath.FullBranch(index, isChange, copayerIndex); -console.log('[PublicKeyRing.js.338] CACHE MISS'); //TODO - var script = this.getRedeemScript(index, isChange, copayerIndex); - var address = Address.fromScript(script, this.network.name); - this.addressToPath[address.toString()] = HDPath.FullBranch(index, isChange, copayerIndex); - this._cacheAddress(index, isChange, copayerIndex, address); - } - return this._cachedAddress(index, isChange, copayerIndex); + log.info('Generating Address:', index, isChange, copayerIndex); + var script = this.getRedeemScript(index, isChange, copayerIndex); + var address = Address.fromScript(script, this.network.name).toString(); + + this._cacheAddress(address, path, isChange); + return address; }; -PublicKeyRing.prototype._cacheAddress = function(index, isChange, copayerIndex, address) { - var changeIndex = isChange ? 1 : 0; - if (!this._cacheAddressMap) this._cacheAddressMap = {}; - if (!this._cacheAddressMap[index]) this._cacheAddressMap[index] = {}; - if (!this._cacheAddressMap[index][changeIndex]) this._cacheAddressMap[index][changeIndex] = {}; - this._cacheAddressMap[index][changeIndex][copayerIndex] = address; -}; -PublicKeyRing.prototype._cachedAddress = function(index, isChange, copayerIndex) { - var changeIndex = isChange ? 1 : 0; - if (!this._cacheAddressMap) return undefined; - if (!this._cacheAddressMap[index]) return undefined; - if (!this._cacheAddressMap[index][changeIndex]) return undefined; - return this._cacheAddressMap[index][changeIndex][copayerIndex]; + +PublicKeyRing.prototype._cacheAddress = function(address, path, isChange) { + this.cache.addressToPath[address] = path; + if (isChange) + this.cache.changeAddresses.push(address); + else + this.cache.receiveAddresses.push(address); }; /** @@ -401,26 +397,12 @@ PublicKeyRing.prototype.getHDParams = function(id) { * @return {HDPath} */ PublicKeyRing.prototype.pathForAddress = function(address) { - var path = this.addressToPath[address]; + this._checkAndRebuildCache(); + var path = this.cache.addressToPath[address]; if (!path) throw new Error('Couldn\'t find path for address ' + address); return path; }; -/** - * @desc - * Get the hexadecimal representation of a P2SH script - * - * @param {number} index - index to use when generating the address - * @param {boolean} isChange - generate a change address or a receive addres - * @param {number|string} pubkey - index of the copayer, or his public key - * @returns {string} hexadecimal encoded P2SH hash - */ -PublicKeyRing.prototype.getScriptPubKeyHex = function(index, isChange, pubkey) { - var copayerIndex = this.getCosigner(pubkey); - var addr = this.getAddress(index, isChange, copayerIndex); - return Script.createP2SH(addr.payload()).getBuffer().toString('hex'); -}; - /** * @desc * Generates a new address and updates the last index used @@ -433,26 +415,44 @@ PublicKeyRing.prototype.getScriptPubKeyHex = function(index, isChange, pubkey) { */ PublicKeyRing.prototype.generateAddress = function(isChange, pubkey) { isChange = !!isChange; - var HDParams = this.getHDParams(pubkey); - var index = isChange ? HDParams.getChangeIndex() : HDParams.getReceiveIndex(); - var ret = this.getAddress(index, isChange, HDParams.copayerIndex); - HDParams.increment(isChange); + var hdParams = this.getHDParams(pubkey); + var index = isChange ? hdParams.getChangeIndex() : hdParams.getReceiveIndex(); + var ret = this._getAddress(index, isChange, hdParams.copayerIndex); + hdParams.increment(isChange); return ret; }; /** - * @desc - * Retrieve the addresses from a getAddressInfo return object + * @desc Is an address is from this wallet? * - * {@see PublicKeyRing#getAddressInfo} - * @returns {string[]} the result of retrieving the addresses from calling + * @param {string} address + * @return {boolean} */ -PublicKeyRing.prototype.getAddresses = function(opts) { - return this.getAddressesInfo(opts).map(function(info) { - return info.address; - }); +PublicKeyRing.prototype.addressIsOwn = function(address) { + return !!this.cache.addressToPath[address]; }; +/** + * @desc Is an address is a change address? + * + * @param {string} address + * @return {boolean} + */ +PublicKeyRing.prototype.addressIsChange = function(address) { + this._checkAndRebuildCache(); + + if (!this.cache.addressToPath[address]) + return null; + + //Memoization Only, never stored. + if (_.isUndefined(this._isChange[address])) { + this._isChange[address] = _.indexOf(this.cache.changeAddresses, address) >= 0; + } + return !!this._isChange[address]; +}; + + + /** * @desc * Maps a copayer's public key to his index in the keyring @@ -478,79 +478,46 @@ PublicKeyRing.prototype.getCosigner = function(pubKey) { return index; }; + + +PublicKeyRing.prototype.buildAddressCache = function() { + var ret = []; + var self = this; + + log.info('Rebuilding Address Cache...this will take a while'); + _.each(this.indexes, function(index) { + for (var i = 0; i < index.receiveIndex; i++) { + self._getAddress(i, false, index.copayerIndex); + } + for (var i = 0; i < index.changeIndex; i++) { + self._getAddress(i, true, index.copayerIndex); + } + }); + log.info('...done!'); +}; + + +PublicKeyRing.prototype._checkAndRebuildCache = function(opts) { + // If cache exists, it has to be updated + if (_.isEmpty(this.cache.addressToPath)) { + this.buildAddressCache(); + } +}; + + /** * @desc * Gets information about addresses for a copayer * - * @see PublicKeyRing#getAddressesInfoForIndex * @param {Object} opts - * @param {string|number} pubkey - the pubkey or index of a copayer in the ring * @returns {AddressInfo[]} */ -PublicKeyRing.prototype.getAddressesInfo = function(opts, pubkey) { - -console.log('[PublicKeyRing.js.474] STARTED'); //TODO - var ret = []; - var self = this; - var copayerIndex = pubkey && this.getCosigner(pubkey); -console.log('[PublicKeyRing.js.478:copayerIndex:]',copayerIndex); //TODO - this.indexes.forEach(function(index) { -console.log('[PublicKeyRing.js.479:index:]',index); //TODO - ret = ret.concat(self.getAddressesInfoForIndex(index, opts, copayerIndex)); - }); -console.log('[PublicKeyRing.js.474] END'); //TODO +PublicKeyRing.prototype.getAddresses = function() { + this._checkAndRebuildCache(); + var ret = this.cache.receiveAddresses.concat(this.cache.changeAddresses); return ret; }; -/** - * @typedef AddressInfo - * @property {bitcore.Address} address - the address generated - * @property {string} addressStr - the base58 encoded address - * @property {boolean} isChange - true if it's a change address - * @property {boolean} owned - true if it's an address generated by a copayer - */ -/** - * @desc - * Retrieves info about addresses generated by a copayer - * - * @param {HDParams} index - HDParams of the copayer - * @param {Object} opts - * @param {boolean} opts.excludeChange - don't append information about change addresses - * @param {boolean} opts.excludeMain - don't append information about receive addresses - * @param {string|number|undefined} copayerIndex - copayer index, pubkey, or undefined to fetch info - * about shared addresses - * @return {AddressInfo[]} a list of AddressInfo - */ -PublicKeyRing.prototype.getAddressesInfoForIndex = function(index, opts, copayerIndex) { - opts = opts || {}; - var isOwned = index.copayerIndex === HDPath.SHARED_INDEX || index.copayerIndex === copayerIndex; - var ret = []; - var appendAddressInfo = function(address, isChange) { - ret.push({ - address: address, - addressStr: address.toString(), - isChange: isChange, - owned: isOwned - }); - -console.log('[PublicKeyRing.js.518] Appending address'); //TODO - }; - - console.log('[PublicKeyRing.js.519] getAddressesInfoForIndex'); //TODO - for (var i = 0; !opts.excludeChange && i < index.changeIndex; i++) { - appendAddressInfo(this.getAddress(i, true, index.copayerIndex), true); - } - -console.log('[PublicKeyRing.js.526]'); //TODO - for (var i = 0; !opts.excludeMain && i < index.receiveIndex; i++) { - appendAddressInfo(this.getAddress(i, false, index.copayerIndex), false); - } - - -console.log('[PublicKeyRing.js.534] CACHE IS' , this._cacheAddressMap); //TODO - - return ret; -}; /** * @desc @@ -722,25 +689,6 @@ PublicKeyRing.prototype._mergePubkeys = function(inPKR) { return hasChanged; }; -/** - * @desc - * Merges this public key ring with another one, optionally ignoring the - * wallet id - * - * @param {PublicKeyRing} inPkr - * @param {boolean} ignoreId - * @return {boolean} true if the internal state has changed - */ -PublicKeyRing.prototype.merge = function(inPKR, ignoreId) { - this._checkInPKR(inPKR, ignoreId); - - var hasChanged = false; - hasChanged |= this.mergeIndexes(inPKR.indexes); - hasChanged |= this._mergePubkeys(inPKR); - - return !!hasChanged; -}; - /** * @desc @@ -763,4 +711,25 @@ PublicKeyRing.prototype.mergeIndexes = function(indexes) { } +/** + * @desc + * Merges this public key ring with another one, optionally ignoring the + * wallet id + * + * @param {PublicKeyRing} inPkr + * @param {boolean} ignoreId + * @return {boolean} true if the internal state has changed + */ +PublicKeyRing.prototype.merge = function(inPKR, ignoreId) { + this._checkInPKR(inPKR, ignoreId); + + var hasChanged = false; + hasChanged |= this.mergeIndexes(inPKR.indexes); + hasChanged |= this._mergePubkeys(inPKR); + + return !!hasChanged; +}; + + + module.exports = PublicKeyRing; diff --git a/js/models/Wallet.js b/js/models/Wallet.js index 5cc85bdee..f9def93a7 100644 --- a/js/models/Wallet.js +++ b/js/models/Wallet.js @@ -887,7 +887,6 @@ Wallet.prototype._lockIncomming = function() { Wallet.prototype._setBlockchainListeners = function() { -console.log('[Wallet.js.889] address'); //TODO var self = this; self.blockchain.removeAllListeners(); self.subscribeToAddresses(); @@ -902,14 +901,12 @@ console.log('[Wallet.js.889] address'); //TODO log.debug('Wallet:' + self.id + 'blockchain disconnect event'); self.emitAndKeepAlive('insightError'); }); + self.blockchain.on('tx', function(tx) { log.debug('Wallet:' + self.id + ' blockchain tx event'); - var addresses = self.getAddressesInfo(); - var addr = _.findWhere(addresses, { - addressStr: tx.address - }); - if (addr) { - self.emitAndKeepAlive('tx', tx.address, addr.isChange); + var addresses = self.getAddresses(); + if (_.indexOf(addresses,tx.address)>=0) { + self.emitAndKeepAlive('tx', tx.address, self.addressIsChange(tx.address)); } }); @@ -1055,6 +1052,7 @@ Wallet.prototype.toObj = function() { settings: this.settings, networkNonce: this.network.getHexNonce(), //yours networkNonces: this.network.getHexNonces(), //copayers + publicKeyRing: this.publicKeyRing.toObj(), txProposals: this.txProposals.toObj(), privateKey: this.privateKey ? this.privateKey.toObj() : undefined, addressBook: this.addressBook, @@ -1376,21 +1374,15 @@ Wallet.prototype._doGenerateAddress = function(isChange) { return this.publicKeyRing.generateAddress(isChange, this.publicKey); }; -/** - * @callback addressCallback - * @param {string} addr - all the addresses of the wallet - */ /** * @desc Generate a new address * @param {boolean} isChange - whether to generate a change address or a receive address - * @param {addressCallback} cb * @return {string[]} a list of all the addresses generated so far for the wallet */ -Wallet.prototype.generateAddress = function(isChange, cb) { +Wallet.prototype.generateAddress = function(isChange) { var addr = this._doGenerateAddress(isChange); this.sendIndexes(); this._newAddresses(); - if (cb) return cb(addr); return addr; }; @@ -1844,12 +1836,21 @@ Wallet.prototype.parsePaymentRequest = function(options, rawData) { */ Wallet.prototype._getPayProRefundOutputs = function(txp) { var pkr = this.publicKeyRing; - var index = pkr.getHDParams(this.publicKey); var amount = +txp.merchant.total.toString(10); var output = new PayPro.Output(); - var script = pkr.getScriptPubKeyHex(index.changeIndex, true, this.pubkey); - output.set('script', new Buffer(script, 'hex')); + var opts = JSON.parse(txp.builder.vanilla.opts); + if (!opts.remainderOut) { + log.warn('no remainder set. Not setting refund in PayPro'); + return; + } +console.log('[Wallet.js.1842:builder:]',txp.builder.vanilla.opts); //TODO + var addrStr = opts.remainderOut.address; + var addr = new bitcore.Address(addrStr); + var script = bitcore.Script.createP2SH(addr.payload()).getBuffer(); + log.debug('PayPro refund address set to:' + addrStr); + + output.set('script', script); output.set('amount', amount); return [output]; }; @@ -1867,7 +1868,6 @@ Wallet.prototype.createPayProPayment = function(txp) { var tx = txp.builder.build(); var txBuf = tx.serialize(); - var refund_outputs = this._getPayProRefundOutputs(txp); // We send this to the serve after receiving a PaymentRequest var pay = new PayPro(); @@ -1878,9 +1878,11 @@ Wallet.prototype.createPayProPayment = function(txp) { merchant_data = new Buffer(merchant_data, 'hex'); pay.set('merchant_data', merchant_data); } - pay.set('transactions', [txBuf]); - pay.set('refund_to', refund_outputs); + + var refund_outputs = this._getPayProRefundOutputs(txp); + if (refund_outputs) + pay.set('refund_to', refund_outputs); // Unused for now // options.memo = ''; @@ -2002,28 +2004,32 @@ Wallet.prototype.getAddressesStr = function(opts) { Wallet.prototype.subscribeToAddresses = function() { if (!this.publicKeyRing.isComplete()) return; -console.log('[Wallet.js.2002:subscribeToAddresses:]'); //TODO - var addrInfo = this.publicKeyRing.getAddressesInfo(); - this.blockchain.subscribe(_.pluck(addrInfo, 'addressStr')); - log.debug('Subscribed to ' + addrInfo.length + ' addresses'); + var addresses = this.publicKeyRing.getAddresses(); + this.blockchain.subscribe(addresses); + log.debug('Subscribed to ' + addresses.length + ' addresses'); }; -/** - * @desc Alias for {@link PublicKeyRing#getAddressesInfo} - */ -Wallet.prototype.getAddressesInfo = function(opts) { - return this.publicKeyRing.getAddressesInfo(opts, this.publicKey); -}; /** * @desc Returns true if a given address was generated by deriving our master public key * @return {boolean} */ Wallet.prototype.addressIsOwn = function(addrStr) { - return !!this.publicKeyRing.addressToPath[addrStr]; + return this.publicKeyRing.addressIsOwn(addrStr); }; +/** + * @desc Returns true if a given address is a change address (remainder) + * @param addrStr + * @return {boolean} + */ +Wallet.prototype.addressIsChange = function(addrStr) { + return this.publicKeyRing.addressIsChange(addrStr); +}; + + + /** * Estimate a tx fee in satoshis given its input count * (only used when spending all wallet funds) @@ -2110,7 +2116,7 @@ Wallet.prototype.maxRejectCount = function() { // TODO: Can we add cache to getUnspent? Wallet.prototype.getUnspent = function(cb) { var self = this; - this.blockchain.getUnspent(this.getAddressesStr(), function(err, unspentList) { + this.blockchain.getUnspent(this.getAddresses(), function(err, unspentList) { if (err) { return cb(err); @@ -2372,7 +2378,8 @@ Wallet.prototype.deriveAddresses = function(index, amount, isChange, copayerInde var ret = new Array(amount); for (var i = 0; i < amount; i++) { - ret[i] = this.publicKeyRing.getAddress(index + i, isChange, copayerIndex).toString(); + // TODO + ret[i] = this.publicKeyRing._getAddress(index + i, isChange, copayerIndex).toString(); } return ret; }; @@ -2534,7 +2541,7 @@ Wallet.prototype.getTransactionHistory = function(opts, cb) { } opts = opts || {}; - var addresses = self.getAddressesInfo(); + var addresses = self.getAddresses(); var proposals = self.txProposals.txps; var satToUnit = 1 / self.settings.unitToSatoshi; @@ -2544,33 +2551,26 @@ Wallet.prototype.getTransactionHistory = function(opts, cb) { function extractInsOuts(tx) { // Inputs var inputs = _.map(tx.vin, function(item) { - var addr = _.findWhere(addresses, { - addressStr: item.addr - }); return { type: 'in', - address: addr ? addr.addressStr : item.addr, - isMine: !_.isUndefined(addr), - isChange: addr ? !!addr.isChange : false, + address: item.addr, + isMine: self.addressIsOwn(item.addr), + isChange: self.addressIsChange(item.addr), amountSat: item.valueSat, } }); var outputs = _.map(tx.vout, function(item) { - var addr; var itemAddr; // If classic multisig, ignore if (item.scriptPubKey && item.scriptPubKey.addresses.length == 1) { itemAddr = item.scriptPubKey.addresses[0]; - addr = _.findWhere(addresses, { - addressStr: itemAddr, - }); } return { type: 'out', - address: addr ? addr.addressStr : itemAddr, - isMine: !_.isUndefined(addr), - isChange: addr ? !!addr.isChange : false, + address: itemAddr, + isMine: self.addressIsOwn(itemAddr), + isChange: self.addressIsChange(itemAddr), label: self.addressBook[itemAddr] ? self.addressBook[itemAddr].label : undefined, amountSat: parseInt((item.value * bitcore.util.COIN).toFixed(0)), } @@ -2608,6 +2608,8 @@ Wallet.prototype.getTransactionHistory = function(opts, cb) { var fees = parseInt((tx.fees * bitcore.util.COIN).toFixed(0)); var amount; + + if (amountIn == (amountOut + amountOutChange + (amountIn > 0 ? fees : 0))) { tx.action = 'moved'; amount = amountOut; diff --git a/test/PublicKeyRing.js b/test/PublicKeyRing.js index 321b81ffe..ca0a8d648 100644 --- a/test/PublicKeyRing.js +++ b/test/PublicKeyRing.js @@ -136,7 +136,8 @@ describe('PublicKeyRing model', function() { [true, false].forEach(function(isChange) { for (var i = 0; i < 2; i++) { - var a = w.generateAddress(isChange, k.pub); + var aStr = w.generateAddress(isChange, k.pub); + var a= new bitcore.Address(aStr); a.isValid().should.equal(true); a.isScript().should.equal(true); a.network().name.should.equal('livenet'); @@ -152,27 +153,32 @@ describe('PublicKeyRing model', function() { var setup = getCachedW(); var pubkeyring = setup.w; - var address = pubkeyring.getAddress(3, false, 4); + var address = pubkeyring._getAddress(3, false, 4); - (pubkeyring._cacheAddressMap[3][0][4]).should.equal(address); + pubkeyring.cache.addressToPath[address].should.equal("m/45'/4/0/3"); + _.indexOf(pubkeyring.cache.receiveAddresses,address).should.be.above(0); + _.indexOf(pubkeyring.cache.changeAddresses,address).should.be.equal(-1); }); - it('getAddress cache hit doesn\'t alter state', function() { - var setup = getCachedW(); - var pubkeyring = setup.w; - var spySave; - - pubkeyring.getAddress(3, false, 4); - - spySave = sinon.stub(pubkeyring, '_cacheAddress'); - spySave.onFirstCall().throws(new Error()); - - pubkeyring.getAddress(3, false, 4); - - spySave.restore(); + it('should generate one address by default', function() { + var k = createW(); + var w = k.w; + var a = w.getAddresses(); + a.length.should.equal(1); }); - it('should return PublicKeyRing addresses', function() { + it('should generate one address by default', function() { + var k = createW(); + var w = k.w; + + var a = w.getAddresses(); + a.length.should.equal(1); + a = w.getAddresses(); + a.length.should.equal(1); + }); + + + it('should generate 4+1 addresses', function() { var k = createW(); var w = k.w; @@ -184,15 +190,16 @@ describe('PublicKeyRing model', function() { w.generateAddress(isChange, k.pub); } }); + }); - var as = w.getAddressesInfo(); - as.length.should.equal(5); // include pre-generated shared one - for (var j in as) { - var a = as[j]; - a.address.isValid().should.equal(true); - a.addressStr.should.equal(a.address.toString()); - a.isChange.should.equal([false, true, true, false, false][j]); - } + it('should check isChange 4+1 addresses', function() { + var k = createW(); + var w = k.w; + var a = w.getAddresses(); + _.each(a, function(a, j) { + var addr = new bitcore.Address(a); + w.addressIsChange(a).should.equal([false, true, true, false, false][j]); + }); }); @@ -405,7 +412,7 @@ describe('PublicKeyRing model', function() { (function() { PublicKeyRing.fromObj(pkr); - }).should.throw('bad data format: Did you use .toObj()?'); + }).should.throw('format'); }); diff --git a/test/Wallet.js b/test/Wallet.js index bbef0644d..db1c437b6 100644 --- a/test/Wallet.js +++ b/test/Wallet.js @@ -8,6 +8,7 @@ var Transaction = bitcore.Transaction; var Address = bitcore.Address; var PayPro = bitcore.PayPro; var Buffer = bitcore.Buffer; +var Script = bitcore.Script; function assertObjectEqual(a, b) { @@ -163,17 +164,13 @@ describe('Wallet model', function() { should.exist(w.addressBook); }); - it('should provide some basic features', function(done) { + it('should provide some basic features', function() { var opts = {}; var w = cachedCreateW(); addCopayers(w); w.publicKeyRing.generateAddress(false, w.publicKey); w.publicKeyRing.isComplete().should.equal(true); - w.generateAddress(true).isValid().should.equal(true); - w.generateAddress(true, function(addr) { - addr.isValid().should.equal(true); - done(); - }); + (new bitcore.Address(w.generateAddress(true))).isValid().should.equal(true); }); var unspentTest = [{ @@ -224,14 +221,18 @@ describe('Wallet model', function() { return w; }; + var unSpentTestFromWallet = function(w, addrStr) { + + unspentTest[0].address = addrStr; + var a = new bitcore.Address(addrStr); + unspentTest[0].scriptPubKey = Script.createP2SH(a.payload()).getBuffer().toString('hex'); + }; + it('#create, fail for network', function() { var w = cachedCreateW2(); - - unspentTest[0].address = w.publicKeyRing.getAddress(1, true).toString(); - unspentTest[0].scriptPubKey = w.publicKeyRing.getScriptPubKeyHex(1, true); - + unSpentTestFromWallet(unspentTest[0], w.publicKeyRing.generateAddress(true)); var f = function() { w._createTxProposal( '15q6HKjWHAksHcH91JW23BJEuzZgFwydBt', @@ -240,15 +241,14 @@ describe('Wallet model', function() { unspentTest ); }; - f.should.throw(Error); + f.should.throw('networkname'); }); it('#create, check builder opts', function() { var w = cachedCreateW2(); - unspentTest[0].address = w.publicKeyRing.getAddress(1, true, w.publicKey).toString(); - unspentTest[0].scriptPubKey = w.publicKeyRing.getScriptPubKeyHex(1, true, w.publicKey); + unSpentTestFromWallet(unspentTest[0], w.publicKeyRing.generateAddress(true)); var txp = w._createTxProposal( 'mgGJEugdPnvhmRuFdbdQcFfoFLc1XXeB79', '123456789', @@ -264,9 +264,7 @@ describe('Wallet model', function() { it('#create, 1 sign', function() { var w = cachedCreateW2(); - - unspentTest[0].address = w.publicKeyRing.getAddress(1, true, w.publicKey).toString(); - unspentTest[0].scriptPubKey = w.publicKeyRing.getScriptPubKeyHex(1, true, w.publicKey); + unSpentTestFromWallet(unspentTest[0], w.publicKeyRing.generateAddress(true)); var txp = w._createTxProposal( 'mgGJEugdPnvhmRuFdbdQcFfoFLc1XXeB79', @@ -288,9 +286,7 @@ describe('Wallet model', function() { var w = cachedCreateW2(); var comment = 'This is a comment'; - - unspentTest[0].address = w.publicKeyRing.getAddress(1, true, w.publicKey).toString(); - unspentTest[0].scriptPubKey = w.publicKeyRing.getScriptPubKeyHex(1, true, w.publicKey); + unSpentTestFromWallet(unspentTest[0], w.publicKeyRing.generateAddress(true)); var txp = w._createTxProposal( 'mgGJEugdPnvhmRuFdbdQcFfoFLc1XXeB79', @@ -308,9 +304,7 @@ describe('Wallet model', function() { var w = cachedCreateW2(); var comment = 'Lorem ipsum dolor sit amet, suas euismod vis te, velit deleniti vix an. Pri ex suscipit similique, inermis per'; - - unspentTest[0].address = w.publicKeyRing.getAddress(1, true, w.publicKey).toString(); - unspentTest[0].scriptPubKey = w.publicKeyRing.getScriptPubKeyHex(1, true, w.publicKey); + unSpentTestFromWallet(unspentTest[0], w.publicKeyRing.generateAddress(true)); (function() { w._createTxProposal( @@ -340,8 +334,7 @@ describe('Wallet model', function() { var ts = Date.now(); for (var isChange = false; !isChange; isChange = true) { for (var index = 0; index < 3; index++) { - unspentTest[0].address = w.publicKeyRing.getAddress(index, isChange, w.publicKey).toString(); - unspentTest[0].scriptPubKey = w.publicKeyRing.getScriptPubKeyHex(index, isChange, w.publicKey); + unSpentTestFromWallet(unspentTest[0], w.publicKeyRing.generateAddress(true)); var txp = w._createTxProposal( 'mgGJEugdPnvhmRuFdbdQcFfoFLc1XXeB79', '123456789', @@ -910,7 +903,7 @@ describe('Wallet model', function() { w.issueTx(ntxid, function(err, txid, status) { should.not.exist(err); - txp.getSent().should.be.above(now-1); + txp.getSent().should.be.above(now - 1); txp.sentTxid.should.be.equal(txid); txid.should.equal(1234); status.should.equal(Wallet.TX_BROADCASTED); @@ -1170,16 +1163,14 @@ describe('Wallet model', function() { describe('#subscribeToAddresses', function() { it('should subscribe successfully', function() { var w = cachedCreateW2(); - var addr1 = w.getAddresses()[0].toString(); var addr2 = w.generateAddress().toString(); var addr3 = w.generateAddress(true).toString(); - chai.expect(w.getAddresses().length).to.equal(3); w.blockchain.subscribe = sinon.spy(); w.subscribeToAddresses(); w.blockchain.subscribe.calledOnce.should.equal(true); var arg = w.blockchain.subscribe.getCall(0).args[0]; - chai.expect(_.difference(arg, [addr1, addr2, addr3]).length).to.equal(0); + _.intersection(arg, [addr2, addr3]).length.should.be.equal(2); }); }); @@ -1967,27 +1958,40 @@ describe('Wallet model', function() { - it('should emit notification when tx received', function(done) { + it('should emit notification when tx received', function() { var w = cachedCreateW2(); + + var addr1 = w.generateAddress(false); + sinon.stub(w,'subscribeToAddresses'); + w.blockchain.removeAllListeners = sinon.stub(); - var spy = sinon.spy(w, 'emit'); + w.blockchain.on = sinon.stub(); - w.generateAddress(false, function(addr1) { - w.generateAddress(true, function(addr2) { - w.blockchain.on = sinon.stub().withArgs('tx').yields({ - address: addr1.toString(), - }); - w._setBlockchainListeners(); - spy.calledWith('tx', addr1.toString(), false).should.be.true; - - w.blockchain.on = sinon.stub().withArgs('tx').yields({ - address: addr2.toString(), - }); - w._setBlockchainListeners(); - spy.calledWith('tx', addr2.toString(), true).should.be.true; - done(); - }); + w.blockchain.on.withArgs('tx').yields({ + address: addr1, }); + + var spy = sinon.spy(w, 'emit'); + w._setBlockchainListeners(); + spy.calledWith('tx', addr1, false).should.equal(true); + }); + + it('should emit notification when tx received (change addr)', function() { + var w = cachedCreateW2(); + + var addr1 = w.generateAddress(true); + sinon.stub(w,'subscribeToAddresses'); + + w.blockchain.removeAllListeners = sinon.stub(); + w.blockchain.on = sinon.stub(); + + w.blockchain.on.withArgs('tx').yields({ + address: addr1, + }); + + var spy = sinon.spy(w, 'emit'); + w._setBlockchainListeners(); + spy.calledWith('tx', addr1, true).should.equal(true); }); describe('#fromObj / #toObj', function() { @@ -2041,13 +2045,28 @@ describe('Wallet model', function() { should.exist(w.txProposals.toObj); should.exist(w.privateKey.toObj); - assertObjectEqual(w.toObj(), JSON.parse(o2)); + var obj = w.toObj(); + + // remove data from new versions + delete obj.publicKeyRing['cache']; + + assertObjectEqual(obj, JSON.parse(o2)); }); }); describe('#getTransactionHistory', function() { + var w; + beforeEach(function() { + w = cachedCreateW2(); + }); + afterEach(function() { + if (w.publicKeyRing.addressIsOwn.restore) + w.publicKeyRing.addressIsOwn.restore(); + if (w.publicKeyRing.addressIsChange.restore) + w.publicKeyRing.addressIsChange.restore(); + }); + it('should return list of txs', function(done) { - var w = cachedCreateW2(); var txs = [{ vin: [{ addr: 'addr_in_1', @@ -2092,11 +2111,17 @@ describe('Wallet model', function() { items: txs, totalItems: txs.length, }); - w.getAddressesInfo = sinon.stub().returns([{ - addressStr: 'addr_in_1' - }, { - addressStr: 'addr_out_2' - }]); + + sinon.stub(w,'getAddresses').returns([ 'addr_in_1', 'addr_out_2' ]); + var s = sinon.stub(w.publicKeyRing,'addressIsOwn'); + s.withArgs('addr_in_1').returns(true); + s.withArgs('addr_in_2').returns(false); + s.withArgs('addr_out_2').returns(true); + + + var s2 = sinon.stub(w.publicKeyRing,'addressIsChange'); + s2.withArgs('addr_out_1').returns(false); + s2.withArgs('addr_out_2').returns(false); w.getTransactionHistory(function(err, res) { res.should.exist; @@ -2113,7 +2138,6 @@ describe('Wallet model', function() { }); }); it('should return paginated list of txs', function(done) { - var w = cachedCreateW2(); var txs = [{ txid: 'id1', vin: [{ @@ -2161,11 +2185,6 @@ describe('Wallet model', function() { items: txs.slice(2, 3), totalItems: txs.length, }); - w.getAddressesInfo = sinon.stub().returns([{ - addressStr: 'addr_in_1' - }, { - addressStr: 'addr_out_2' - }]); w.getTransactionHistory({ currentPage: 2, @@ -2182,17 +2201,11 @@ describe('Wallet model', function() { }); }); it('should paginate empty list', function(done) { - var w = cachedCreateW2(); var txs = []; w.blockchain.getTransactions = sinon.stub().yields(null, { items: txs, totalItems: txs.length, }); - w.getAddressesInfo = sinon.stub().returns([{ - addressStr: 'addr_in_1' - }, { - addressStr: 'addr_out_2' - }]); w.getTransactionHistory({ currentPage: 2, @@ -2207,7 +2220,6 @@ describe('Wallet model', function() { }); }); it('should compute sent amount correctly', function(done) { - var w = cachedCreateW2(); var txs = [{ vin: [{ addr: 'addr_in_1', @@ -2234,14 +2246,17 @@ describe('Wallet model', function() { items: txs, totalItems: txs.length, }); - w.getAddressesInfo = sinon.stub().returns([{ - addressStr: 'addr_in_1' - }, { - addressStr: 'addr_in_2' - }, { - addressStr: 'change', - isChange: true, - }]); + + + sinon.stub(w,'getAddresses').returns([ 'addr_in_1', 'addr_in_2', 'change']); + var s = sinon.stub(w.publicKeyRing,'addressIsOwn'); + s.withArgs('addr_in_1').returns(true); + s.withArgs('addr_in_2').returns(true); + s.withArgs('change').returns(true); + + var s2 = sinon.stub(w.publicKeyRing,'addressIsChange'); + s2.withArgs('addr_out_2').returns(false); + s2.withArgs('change').returns(true); w.getTransactionHistory(function(err, res) { res.should.exist; @@ -2253,7 +2268,6 @@ describe('Wallet model', function() { }); }); it('should compute moved amount correctly', function(done) { - var w = cachedCreateW2(); var txs = [{ vin: [{ addr: 'addr_1', @@ -2280,14 +2294,16 @@ describe('Wallet model', function() { items: txs, totalItems: txs.length, }); - w.getAddressesInfo = sinon.stub().returns([{ - addressStr: 'addr_1' - }, { - addressStr: 'addr_2' - }, { - addressStr: 'change', - isChange: true, - }]); + + sinon.stub(w,'getAddresses').returns([ 'addr_in_1', 'addr_in_2', 'change']); + var s = sinon.stub(w.publicKeyRing,'addressIsOwn'); + s.withArgs('addr_1').returns(true); + s.withArgs('addr_2').returns(true); + s.withArgs('change').returns(true); + + var s2 = sinon.stub(w.publicKeyRing,'addressIsChange'); + s2.withArgs('addr_1').returns(false); + s2.withArgs('change').returns(true); w.getTransactionHistory(function(err, res) { res.should.exist; @@ -2299,7 +2315,6 @@ describe('Wallet model', function() { }); }); it('should assign label when address in address book', function(done) { - var w = cachedCreateW2(); var txs = [{ vin: [{ addr: 'addr_in_1', @@ -2349,7 +2364,6 @@ describe('Wallet model', function() { }); }); it('should assign comment from tx proposal if found', function(done) { - var w = cachedCreateW2(); var txs = [{ txid: 'id1', vin: [{ @@ -2517,7 +2531,7 @@ describe('Wallet model', function() { // DATA - var o = '{"opts":{"id":"dbfe10c3fae71cea", "spendUnconfirmed":1,"requiredCopayers":3,"totalCopayers":5,"version":"0.0.5","networkName":"testnet"},"networkNonce":"0000000000000001","networkNonces":[],"publicKeyRing":{"walletId":"dbfe10c3fae71cea","networkName":"testnet","requiredCopayers":3,"totalCopayers":5,"indexes":[{"copayerIndex":2,"changeIndex":0,"receiveIndex":0}],"copayersExtPubKeys":["tpubD6NzVbkrYhZ4YGK8ZhZ8WVeBXNAAoTYjjpw9twCPiNGrGQYFktP3iVQkKmZNiFnUcAFMJRxJVJF6Nq9MDv2kiRceExJaHFbxUCGUiRhmy97","tpubD6NzVbkrYhZ4YKGDJkzWdQsQV3AcFemaQKiwNhV4RL8FHnBFvinidGdQtP8RKj3h34E65RkdtxjrggZYqsEwJ8RhhN2zz9VrjLnrnwbXYNc","tpubD6NzVbkrYhZ4YkDiewjb32Pp3Sz9WK2jpp37KnL7RCrHAyPpnLfgdfRnTdpn6DTWmPS7niywfgWiT42aJb1J6CjWVNmkgsMCxuw7j9DaGKB","tpubD6NzVbkrYhZ4XEtUAz4UUTWbprewbLTaMhR8NUvSJUEAh4Sidxr6rRPFdqqVRR73btKf13wUjds2i8vVCNo8sbKrAnyoTr3o5Y6QSbboQjk","tpubD6NzVbkrYhZ4Yj9AAt6xUVuGPVd8jXCrEE6V2wp7U3PFh8jYYvVad31b4VUXEYXzSnkco4fktu8r4icBsB2t3pCR3WnhVLedY2hxGcPFLKD"],"nicknameFor":{}},"txProposals":{"txps":[],"walletId":"dbfe10c3fae71cea","networkName":"testnet"},"privateKey":{"extendedPrivateKeyString":"tprv8ZgxMBicQKsPeoHLg3tY75z4xLeEe8MqAXLNcRA6J6UTRvHV8VZTXznt9eoTmSk1fwSrwZtMhY3XkNsceJ14h6sCXHSWinRqMSSbY8tfhHi","networkName":"testnet"},"addressBook":{},"settings":{"unitName":"BTC","unitToSatoshi":100000000,"unitDecimals":8,"alternativeName":"Argentine Peso","alternativeIsoCode":"ARS"}}'; + var o = '{"opts":{"id":"dbfe10c3fae71cea", "spendUnconfirmed":1,"requiredCopayers":3,"totalCopayers":5,"version":"0.0.5","networkName":"testnet"},"networkNonce":"0000000000000001","networkNonces":[],"publicKeyRing":{ "cache": { "addressToPath": {}, "changeAddresses": [], "receiveAddresses": [] }, "walletId":"dbfe10c3fae71cea","networkName":"testnet","requiredCopayers":3,"totalCopayers":5,"indexes":[{"copayerIndex":2,"changeIndex":0,"receiveIndex":0}],"copayersExtPubKeys":["tpubD6NzVbkrYhZ4YGK8ZhZ8WVeBXNAAoTYjjpw9twCPiNGrGQYFktP3iVQkKmZNiFnUcAFMJRxJVJF6Nq9MDv2kiRceExJaHFbxUCGUiRhmy97","tpubD6NzVbkrYhZ4YKGDJkzWdQsQV3AcFemaQKiwNhV4RL8FHnBFvinidGdQtP8RKj3h34E65RkdtxjrggZYqsEwJ8RhhN2zz9VrjLnrnwbXYNc","tpubD6NzVbkrYhZ4YkDiewjb32Pp3Sz9WK2jpp37KnL7RCrHAyPpnLfgdfRnTdpn6DTWmPS7niywfgWiT42aJb1J6CjWVNmkgsMCxuw7j9DaGKB","tpubD6NzVbkrYhZ4XEtUAz4UUTWbprewbLTaMhR8NUvSJUEAh4Sidxr6rRPFdqqVRR73btKf13wUjds2i8vVCNo8sbKrAnyoTr3o5Y6QSbboQjk","tpubD6NzVbkrYhZ4Yj9AAt6xUVuGPVd8jXCrEE6V2wp7U3PFh8jYYvVad31b4VUXEYXzSnkco4fktu8r4icBsB2t3pCR3WnhVLedY2hxGcPFLKD"],"nicknameFor":{}},"txProposals":{"txps":[],"walletId":"dbfe10c3fae71cea","networkName":"testnet"},"privateKey":{"extendedPrivateKeyString":"tprv8ZgxMBicQKsPeoHLg3tY75z4xLeEe8MqAXLNcRA6J6UTRvHV8VZTXznt9eoTmSk1fwSrwZtMhY3XkNsceJ14h6sCXHSWinRqMSSbY8tfhHi","networkName":"testnet"},"addressBook":{},"settings":{"unitName":"BTC","unitToSatoshi":100000000,"unitDecimals":8,"alternativeName":"Argentine Peso","alternativeIsoCode":"ARS"}}'; }); diff --git a/test/performance.js b/test/performance.js deleted file mode 100644 index 8f218e24a..000000000 --- a/test/performance.js +++ /dev/null @@ -1,75 +0,0 @@ -'use strict'; - -var PrivateKey = copay.PrivateKey; -var PublicKeyRing = copay.PublicKeyRing; - -var getNewEpk = function() { - return new PrivateKey({ - networkName: 'livenet', - }) - .deriveBIP45Branch() - .extendedPublicKeyString(); -} - - -describe('Performance tests', function() { - describe('PrivateKey', function() { - it('should optimize BIP32 private key gen time with cache', function() { - var k1 = new PrivateKey(); - var generateN = 25; - var generated = []; - var start1 = new Date().getTime(); - for (var i = 0; i < generateN; i++) { - var k = JSON.stringify(k1.get(i, false).storeObj()); - generated.push(k); - } - var delta1 = new Date().getTime() - start1; - var start2 = new Date().getTime(); - for (var i = 0; i < generateN; i++) { - var k = JSON.stringify(k1.get(i, false).storeObj()); - generated[i].should.equal(k); - } - var delta2 = new Date().getTime() - start2; - delta2.should.be.below(delta1); - }); - }); - describe('PublicKeyRing', function() { - var maxN = 7; - for (var n = 1; n < maxN; n++) { - for (var m = 1; m <= n; m++) { - if ((m === 3 && n === 5) || - (m === 2 && n === 3)) { - var M = m; - var N = n; - (function(M, N) { - it('should optimize BIP32 publickey gen time with cache for ' + M + '-of-' + N, function() { - var pkr1 = new PublicKeyRing({ - totalCopayers: N, - requiredCopayers: M - }); - for (var i = 0; i < N; i++) { - pkr1.addCopayer(getNewEpk()); // add new random ext public key - } - var generateN = 5; - var generated = []; - var start1 = new Date().getTime(); - for (var i = 0; i < generateN; i++) { - var pubKeys = JSON.stringify(pkr1.getPubKeys(i, false)); - generated.push(pubKeys); - } - var delta1 = new Date().getTime() - start1; - var start2 = new Date().getTime(); - for (var i = 0; i < generateN; i++) { - var pubKeys = JSON.stringify(pkr1.getPubKeys(i, false)); - generated[i].should.equal(pubKeys); - } - var delta2 = new Date().getTime() - start2; - delta2.should.be.below(delta1); - }); - })(M, N); - } - } - } - - }); -});