From c99bedbc58c5e90d50a93dbe5ac8d57aabd3b884 Mon Sep 17 00:00:00 2001 From: Sjors Provoost Date: Mon, 27 Mar 2017 17:15:07 +0200 Subject: [PATCH] refactor(Labels): do not use KV store --- src/blockchain-wallet.js | 22 +- src/hd-account.js | 25 +-- src/labels.js | 246 +++++--------------- src/wallet.js | 14 +- tests/blockchain_wallet_spec.js | 7 +- tests/hdaccount_spec.js | 50 +---- tests/labels_spec.js | 386 ++------------------------------ 7 files changed, 103 insertions(+), 647 deletions(-) diff --git a/src/blockchain-wallet.js b/src/blockchain-wallet.js index 1cb2f4212..5e52c27ee 100644 --- a/src/blockchain-wallet.js +++ b/src/blockchain-wallet.js @@ -833,6 +833,9 @@ Wallet.prototype.metadata = function (typeId) { return Metadata.fromMasterHDNode(masterhdnode, typeId); }; +// This sets: +// * wallet.external (buy-sell, KV Store type 3) +// * wallet.labels (not yet using KV Store) Wallet.prototype.loadMetadata = function (optionalPayloads, magicHashes) { optionalPayloads = optionalPayloads || {}; @@ -857,21 +860,8 @@ Wallet.prototype.loadMetadata = function (optionalPayloads, magicHashes) { }; var fetchLabels = function () { - var success = function (labels) { - self._labels = labels; - }; - - var error = function (message) { - console.warn('wallet.labels not set:', message); - self._labels = null; - return Promise.resolve(); - }; - - if (optionalPayloads.labels) { - return Labels.fromJSON(this, optionalPayloads.labels, magicHashes.labels, MyWallet.syncWallet).then(success); - } else { - return Labels.fetch(this, MyWallet.syncWallet).then(success).catch(error); - } + this._labels = new Labels(this, MyWallet.syncWallet); + return Promise.resolve(); }; let promises = []; @@ -881,7 +871,7 @@ Wallet.prototype.loadMetadata = function (optionalPayloads, magicHashes) { promises.push(fetchExternal.call(this)); } - // Falls back to read-only based on wallet payload if metadata is disabled + // Labels currently don't use the KV Store, so this should never fail. promises.push(fetchLabels.call(this)); return Promise.all(promises); diff --git a/src/hd-account.js b/src/hd-account.js index c6fb59192..734130838 100644 --- a/src/hd-account.js +++ b/src/hd-account.js @@ -21,10 +21,7 @@ function HDAccount (object) { this._xpub = obj.xpub; this._network = obj.network || Bitcoin.networks.bitcoin; - // Prevent deleting address_labels field when saving wallet: - // * backwards compatibility with mobile (we'll keep one entry for the highest label) - // * if user has 2nd password enabled and doesn't enter it during migration step - this._address_labels_backup = obj.address_labels; + this._address_labels = obj.address_labels; // computed properties this._keyRing = new KeyRing(obj.xpub, obj.cache); @@ -94,10 +91,10 @@ Object.defineProperties(HDAccount.prototype, { configurable: false, get: function () { let maxLabeledReceiveIndex = null; - if (MyWallet.wallet.labels) { + if (MyWallet.wallet.labels) { // May not be set yet maxLabeledReceiveIndex = MyWallet.wallet.labels.maxLabeledReceiveIndex(this.index); - } else if (this._address_labels_backup && this._address_labels_backup.length) { - maxLabeledReceiveIndex = this._address_labels_backup[this._address_labels_backup.length - 1].index; + } else if (this._address_labels && this._address_labels.length) { + maxLabeledReceiveIndex = this._address_labels[this._address_labels.length - 1].index; } return Math.max( this.lastUsedReceiveIndex === null ? -1 : this.lastUsedReceiveIndex, @@ -211,24 +208,12 @@ HDAccount.factory = function (o) { // JSON SERIALIZER HDAccount.prototype.toJSON = function () { - let labelsJSON = this._address_labels_backup; - - // Hold on to the backup until labels are saved in KV store. - if (MyWallet.wallet.labels && !MyWallet.wallet.labels.readOnly && !MyWallet.wallet.labels.dirty) { - // Use placeholder entry to prevent address reuse: - labelsJSON = []; - let max = MyWallet.wallet.labels.maxLabeledReceiveIndex(this._index); - if (max > -1) { - labelsJSON.push({index: max, label: ''}); - } - } - var hdaccount = { label: this._label, archived: this._archived, xpriv: this._xpriv, xpub: this._xpub, - address_labels: labelsJSON, + address_labels: this._address_labels, cache: this._keyRing }; diff --git a/src/labels.js b/src/labels.js index 3ceeea596..d86658452 100644 --- a/src/labels.js +++ b/src/labels.js @@ -1,64 +1,44 @@ -var Metadata = require('./metadata'); var Helpers = require('./helpers'); var AddressHD = require('./address-hd'); var BlockchainAPI = require('./api'); var assert = require('assert'); -var METADATA_TYPE_LABELS = 4; - class Labels { - constructor (metadata, wallet, object, syncWallet) { + constructor (wallet, syncWallet) { assert(syncWallet instanceof Function, 'syncWallet function required'); - this._readOnly = false; // Default this._wallet = wallet; - this._metadata = metadata; - // Required for initial migration and whenever placeholder needs an update this._syncWallet = syncWallet; - this._walletNeedsSync = false; - - this._before = JSON.stringify(object); - - object = this.migrateIfNeeded(object); - - this.init(object); - - this.save(); // Only saves if migration changed something - } - - get readOnly () { - return this._readOnly || !this._wallet.isMetadataReady; - } - get dirty () { - return this._before !== JSON.stringify(this); + this.init(); } - get version () { - return this._version; - } - - init (object) { - this._version = object.version; + init () { this._accounts = []; - for (let accountObject of object.accounts) { - let accountIndex = object.accounts.indexOf(accountObject); - let hdAccount = this._wallet.hdwallet.accounts[accountIndex]; + + for (let hdAccount of this._wallet.hdwallet.accounts) { + let accountIndex = hdAccount.index; let receiveIndex = hdAccount.receiveIndex; let addresses = []; - for (let addressObject of accountObject) { - if (addressObject === null) { - addresses.push(null); // Placeholder, will be replaced below - } else { - let addressIndex = accountObject.indexOf(addressObject); - addresses.push(new AddressHD(addressObject, hdAccount, addressIndex)); + let maxLabeledReceiveIndex = -1; + + for (let addressLabel of hdAccount._address_labels) { + if (addressLabel.index > maxLabeledReceiveIndex) { + maxLabeledReceiveIndex = addressLabel.index; } + addresses[addressLabel.index] = new AddressHD( + { + label: addressLabel.label + }, + hdAccount, + addressLabel.index + ); } - // Add null entries up to the current receive index - for (let i = 0; i < receiveIndex; i++) { + // Add null entries up to the current (labeled) receive index + for (let i = 0; i < Math.max(receiveIndex, maxLabeledReceiveIndex); i++) { if (!addresses[i]) { addresses[i] = new AddressHD(null, hdAccount, @@ -69,10 +49,18 @@ class Labels { } } - static initMetadata (wallet) { - return Metadata.fromMasterHDNode(wallet._metadataHDNode, METADATA_TYPE_LABELS); + get readOnly () { + return false; + } + + syncWallet () { + const syncWallet = () => { + this._syncWallet(() => Promise.resolve(), (e) => Promise.reject(e)); + }; + return Promise.resolve().then(syncWallet); } + // For debugging only, not used to save. toJSON () { return { version: this.version, @@ -89,135 +77,13 @@ class Labels { }; } - static fromJSON (wallet, json, magicHash, syncWallet) { - var success = (payload) => { - return new Labels(metadata, wallet, payload, syncWallet); - }; - var metadata = Labels.initMetadata(wallet); - return metadata.fromObject(JSON.parse(json), magicHash).then(success); - } - - static fetch (wallet, syncWallet) { - var metadata = wallet.isMetadataReady ? Labels.initMetadata(wallet) : null; - - var fetchSuccess = function (payload) { - return new Labels(metadata, wallet, payload, syncWallet); - }; - - var fetchFailed = function (e) { - // Metadata service is down or unreachable. - return Promise.reject(e); - }; - - if (wallet.isMetadataReady) { - return metadata.fetch().then(fetchSuccess).catch(fetchFailed); - } else { - return Promise.resolve(null).then(fetchSuccess); - } - } - - save () { - if (!this.dirty) { - return Promise.resolve(); - } - if (this.readOnly) { - console.info('Labels KV store is read-only, not saving'); - return Promise.resolve(); - } - let promise; - if (!this._metadata.existsOnServer) { - promise = this._metadata.create(this); - } else { - promise = this._metadata.update(this); - } - return promise.then(() => { - this._before = JSON.stringify(this); - if (this._walletNeedsSync) { - console.info('Sync MyWallet address label placeholder'); - this._syncWallet(() => { - this._walletNeedsSync = false; - }); - } - }); - } - - wipe () { - this._metadata.update(null).then(() => { console.log('Wipe complete. Reload browser.'); }); - } - - migrateIfNeeded (object) { - let major, minor, patch; - - if (object && object.version) { - [major, minor, patch] = object.version.split('.').map(n => parseInt(n, 10)); - } - - // First time, migrate from wallet payload if needed - if (object === null || Helpers.isEmptyObject(object)) { - object = { - version: '1.0.0', - accounts: [] - }; - - if (this._wallet.hdwallet.accounts[0]._address_labels_backup) { - console.info('Migrate address labels from wallet to KV-Store v1.0.0'); - - for (let account of this._wallet.hdwallet.accounts) { - let labels = []; - for (let label of account._address_labels_backup || []) { - labels[label.index] = {label: label.label}; - } - // Set undefined entries to null: - for (let i = 0; i < labels.length; i++) { - if (!labels[i]) { - labels[i] = null; - } - } - object.accounts.push(labels); - } - - if (!this.readOnly) { - this._walletNeedsSync = true; - } - } else { - // This is a new wallet, create placeholders for each account: - object.accounts = this._wallet.hdwallet.accounts.map(() => []); - } - } else if (major > 1) { - // Payload contains unsuppored new major version, abort: - throw new Error('LABELS_UNSUPPORTED_MAJOR_VERSION'); - } else if (major === 1 && minor > 0) { - // New minor version can safely be used in read-only mode: - this._readOnly = true; - } else if (major === 1 && minor === 0 && patch > 0) { - // New patch version can safely to be used. - } - - // Run (future) migration scripts: - // switch (object.version) { - // case '1.0.0': - // // Migrate from 1.0.1 to e.g. 1.0.2 or 1.1.0: - // // ... - // object.version = '1.0.1'; - // // falls through - // case '1.0.1': - // // Migrate from 1.0.1 to e.g. 1.0.2 or 1.1.0: - // // ... - // object.version = '1.0.2'; - // // falls through - // default: - // } - return object; - } - // Goes through all labeled addresses and checks which ones have transactions. // This result will be cached in the future. Although we obtain the balance, // this is an implementation detail and we don't save it. checkIfUsed (accountIndex) { assert(Helpers.isPositiveInteger(accountIndex), 'specify accountIndex'); let labeledAddresses = this.all(accountIndex).filter((a) => a !== null); - let addresses = labeledAddresses.filter((a) => a.label).map((a) => a.address); - + let addresses = labeledAddresses.filter((a) => a.label !== null).map((a) => a.address); if (addresses.length === 0) return Promise.resolve(); return BlockchainAPI.getBalances(addresses).then((data) => { @@ -265,9 +131,9 @@ class Labels { // returns Int or null maxLabeledReceiveIndex (accountIndex) { - let labeledAddresses = this._getAccount(accountIndex).filter(a => a && a.label); + let labeledAddresses = this._getAccount(accountIndex).filter(a => a && a.label !== null); if (labeledAddresses.length === 0) return null; - const indexOf = this._getAccount(accountIndex).indexOf(labeledAddresses[labeledAddresses.length - 1]); + let indexOf = labeledAddresses[labeledAddresses.length - 1].index; return indexOf > -1 ? indexOf : null; } @@ -296,8 +162,6 @@ class Labels { assert(Helpers.isString(label), 'specify label'); assert(Helpers.isPositiveInteger(maxGap) && maxGap <= 20, 'Max gap must be less than 20'); - if (this.readOnly) return Promise.reject('KV_LABELS_READ_ONLY'); - let receiveIndex = this._wallet.hdwallet.accounts[accountIndex].receiveIndex; let lastUsedReceiveIndex = this._wallet.hdwallet.accounts[accountIndex].lastUsedReceiveIndex; @@ -313,9 +177,16 @@ class Labels { addr.label = label; addr.used = false; - this._walletNeedsSync = true; + // Update wallet: + let labels = this._wallet.hdwallet.accounts[accountIndex]._address_labels; + + let labelEntry = { + index: receiveIndex, + label: label + }; + labels.push(labelEntry); - return this.save().then(() => { + return this.syncWallet().then(() => { return addr; }); } @@ -327,8 +198,6 @@ class Labels { (address.constructor && address.constructor.name === 'AddressHD'), 'address should be AddressHD instance or Int'); - if (this.readOnly) return Promise.reject('KV_LABELS_READ_ONLY'); - let receiveIndex; if (Helpers.isPositiveInteger(address)) { @@ -339,6 +208,8 @@ class Labels { assert(Helpers.isPositiveInteger(receiveIndex), 'Address not found'); } + console.log('receiveIndex: ', receiveIndex); + if (!Helpers.isValidLabel(label)) { return Promise.reject('NOT_ALPHANUMERIC'); } @@ -347,20 +218,24 @@ class Labels { return Promise.resolve(); } - let maxLabeledReceiveIndexBefore = this.maxLabeledReceiveIndex(accountIndex); - address.label = label; - if (receiveIndex > maxLabeledReceiveIndexBefore) { - this._walletNeedsSync = true; + let labels = this._wallet.hdwallet.accounts[accountIndex]._address_labels; + + // Update in wallet: + let labelEntry = labels.find((label) => label.index === receiveIndex); + + if (!labelEntry) { + labelEntry = {index: receiveIndex}; + labels.push(labelEntry); } - return this.save(); + labelEntry.label = label; + + return this.syncWallet(); } removeLabel (accountIndex, address) { - if (this.readOnly) return Promise.reject('KV_LABELS_READ_ONLY'); - assert(Helpers.isPositiveInteger(accountIndex), 'Account index required'); assert( @@ -377,15 +252,14 @@ class Labels { assert(Helpers.isPositiveInteger(addressIndex), 'Address not found'); } - let maxLabeledReceiveIndexBefore = this.maxLabeledReceiveIndex(accountIndex); - address.label = null; - if (addressIndex === maxLabeledReceiveIndexBefore) { - this._walletNeedsSync = true; - } + // Remove from wallet: + let labels = this._wallet.hdwallet.accounts[accountIndex]._address_labels; + let labelEntry = labels.find((label) => label.index === addressIndex); + labels.splice(labels.indexOf(labelEntry), 1); - return this.save(); + return this.syncWallet(); } // options: diff --git a/src/wallet.js b/src/wallet.js index 1f947cf53..5e61cdc4c 100644 --- a/src/wallet.js +++ b/src/wallet.js @@ -193,33 +193,25 @@ MyWallet.makePairingCode = function (success, error) { } }; -MyWallet.loginFromJSON = function (stringWallet, stringExternal, magicHashHexExternal, stringLabels, magicHashHexLabels, password) { +MyWallet.loginFromJSON = function (stringWallet, stringExternal, magicHashHexExternal, password) { assert(stringWallet, 'Wallet JSON required'); // If metadata service returned 404, do not pass in a string. var externalJSON = null; - var labelsJSON = null; if (stringExternal) { assert(magicHashHexExternal, 'Magic hash for external required'); externalJSON = JSON.parse(stringExternal); } - if (stringLabels) { - assert(magicHashHexLabels, 'Magic hash for labels required'); - labelsJSON = JSON.parse(stringLabels); - } - var walletJSON = JSON.parse(stringWallet); MyWallet.wallet = new Wallet(walletJSON); WalletStore.unsafeSetPassword(password); MyWallet.wallet.loadMetaData({ - external: externalJSON, - labels: labelsJSON + external: externalJSON }, { - external: magicHashHexExternal ? Buffer.from(magicHashHexExternal, 'hex') : null, - labels: magicHashHexExternal ? Buffer.from(magicHashHexLabels, 'hex') : null + external: magicHashHexExternal ? Buffer.from(magicHashHexExternal, 'hex') : null }); setIsInitialized(); return true; diff --git a/tests/blockchain_wallet_spec.js b/tests/blockchain_wallet_spec.js index ff53fabba..5e3df76bf 100644 --- a/tests/blockchain_wallet_spec.js +++ b/tests/blockchain_wallet_spec.js @@ -79,10 +79,8 @@ describe('Blockchain-Wallet', () => { } }; - Labels = { - fetch: () => { - return Promise.resolve({mock: 'labels'}); - } + Labels = () => { + return {mock: 'labels'}; }; External = { @@ -444,6 +442,7 @@ describe('Blockchain-Wallet', () => { }; wallet.loadMetadata().then(checks).then(done); }); + it('should set external', (done) => { let checks = () => { expect(wallet.external).toEqual({mock: 'external'}); diff --git a/tests/hdaccount_spec.js b/tests/hdaccount_spec.js index 7809169eb..e19afe5bd 100644 --- a/tests/hdaccount_spec.js +++ b/tests/hdaccount_spec.js @@ -131,46 +131,8 @@ describe('HDAccount', () => { expect(json1).toEqual(json2); }); - describe('labeled_addresses placeholder', () => { - it('should be set if KV store entry is saved', () => { - maxLabeledReceiveIndex = 3; - expect(account.toJSON()).toEqual(jasmine.objectContaining({ - address_labels: [{ - index: maxLabeledReceiveIndex, - label: '' - }] - })); - }); - - it('should be empty array if there are no labels', () => { - maxLabeledReceiveIndex = -1; - expect(account.toJSON()).toEqual(jasmine.objectContaining({ - address_labels: [] - })); - }); - + describe('labeled_addresses', () => { it('should resave original if KV store is read-only', () => { - MyWallet.wallet.labels.readOnly = true; - expect(account.toJSON()).toEqual(jasmine.objectContaining({ - address_labels: [{ - index: 3, - label: 'Hello' - }] - })); - }); - - it('should resave original if KV store is unsaved', () => { - MyWallet.wallet.labels.dirty = true; - expect(account.toJSON()).toEqual(jasmine.objectContaining({ - address_labels: [{ - index: 3, - label: 'Hello' - }] - })); - }); - - it('should resave original if KV store doesn\'t work', () => { - MyWallet.wallet.labels = null; expect(account.toJSON()).toEqual(jasmine.objectContaining({ address_labels: [{ index: 3, @@ -288,21 +250,13 @@ describe('HDAccount', () => { expect(account.receiveIndex).toEqual(4); }); - it('receiveIndex falls back to legacy labels if KV store doesn\'t work', () => { + it('receiveIndex falls back to legacy labels if Labels doesn\'t work', () => { maxLabeledReceiveIndex = 3; MyWallet.wallet.labels = null; account.lastUsedReceiveIndex = 2; expect(account.receiveIndex).toEqual(4); }); - it('receiveIndex ignores labeled index if nothing works', () => { - maxLabeledReceiveIndex = 3; - MyWallet.wallet.labels = null; - account.lastUsedReceiveIndex = 2; - account._address_labels_backup = undefined; - expect(account.receiveIndex).toEqual(3); - }); - it('changeIndex must be a number', () => { let invalid = () => { account.changeIndex = '1'; }; let valid = () => { account.changeIndex = 1; }; diff --git a/tests/labels_spec.js b/tests/labels_spec.js index 4d2887062..33e219054 100644 --- a/tests/labels_spec.js +++ b/tests/labels_spec.js @@ -1,26 +1,6 @@ let proxyquire = require('proxyquireify')(require); describe('Labels', () => { - const latestVersion = '1.0.0'; - - const defaultInitialPayload = { - version: latestVersion, - accounts: [[]] - }; - - let mockPayload; - - let Metadata = { - fromMasterHDNode (n, masterhdnode) { - return { - create () {}, - fetch () { - return Promise.resolve(mockPayload); - } - }; - } - }; - let AddressHD = (obj) => { return { constructor: { @@ -38,7 +18,6 @@ describe('Labels', () => { }; let stubs = { - './metadata': Metadata, './address-hd': AddressHD }; @@ -49,101 +28,45 @@ describe('Labels', () => { let l; beforeEach(() => { - mockPayload = { - version: latestVersion, - accounts: [[null, {label: 'Hello'}]] - }; - wallet = { hdwallet: { accounts: [{ index: 0, receiveAddressAtIndex: (index) => `0-${index}`, receiveIndex: 2, - lastUsedReceiveIndex: 1 + lastUsedReceiveIndex: 1, + _address_labels: [{index: 1, label: 'Hello'}] }] - }, - isMetadataReady: true + } }; }); describe('class', () => { - let metadata = {}; - - // Includes helper method init(), but not migrateIfNeeded() + // Includes helper method init() describe('new Labels()', () => { - beforeEach(() => { - spyOn(Labels.prototype, 'migrateIfNeeded').and.callFake((object) => { - if (object && object.version === '0.1.0') { - object.version = latestVersion; - } - return object; - }); - spyOn(Labels.prototype, 'save').and.returnValue(true); + it('should create a Labels object', () => { + l = new Labels(wallet, () => {}); + expect(l.constructor.name).toEqual('Labels'); }); it('should require syncWallet function', () => { expect(() => { - l = new Labels(metadata, wallet, mockPayload); + l = new Labels(wallet); }).toThrow(); }); - - it('should transform an Object to Labels', () => { - l = new Labels(metadata, wallet, mockPayload, () => {}); - expect(l.constructor.name).toEqual('Labels'); - }); - - it('should deserialize the version', () => { - l = new Labels(metadata, wallet, mockPayload, () => {}); - expect(l.version).toEqual(latestVersion); - }); - - it('should not deserialize non-expected fields', () => { - mockPayload.non_expected_field = 'I am an intruder'; - l = new Labels(metadata, wallet, mockPayload, () => {}); - expect(l._non_expected_field).toBeUndefined(); - }); - - it('should call save if migration changes anything', () => { - mockPayload.version = '0.1.0'; - l = new Labels(metadata, wallet, mockPayload, () => {}); - expect(Labels.prototype.save).toHaveBeenCalled(); - }); - }); - - // Includes helper function initMetadata() - describe('fetch()', () => { - }); - - // Includes helper function initMetadata() - describe('fromJSON()', () => { }); }); describe('instance', () => { - let metadata; beforeEach(() => { - metadata = { - existsOnServer: true, - create: () => Promise.resolve(), - update: () => Promise.resolve() - }; - spyOn(Labels.prototype, 'migrateIfNeeded').and.callFake((object) => object); - l = new Labels(metadata, wallet, mockPayload, () => {}); + l = new Labels(wallet, (success) => { success(); }); }); describe('toJSON', () => { beforeEach(() => { }); - it('should store version', () => { - let json = JSON.stringify(l); - expect(JSON.parse(json)).toEqual( - jasmine.objectContaining({version: latestVersion} - )); - }); - - it('should store labels', () => { + it('should serialize labels', () => { let json = JSON.stringify(l); let res = JSON.parse(json); expect(res.accounts[0].length).toEqual(2); @@ -162,249 +85,13 @@ describe('Labels', () => { let json = JSON.stringify(l, null, 2); let obj = JSON.parse(json); - expect(obj.version).toBeDefined(); expect(obj.rarefield).not.toBeDefined(); }); }); describe('readOnly', () => { - it('should be true when _readOnly is true', () => { - l._readOnly = true; - expect(l.readOnly).toEqual(true); - }); - - it('should be true if KV store doesn\'t work', () => { - l._readOnly = false; - l._wallet.isMetadataReady = false; - expect(l.readOnly).toEqual(true); - }); - }); - - describe('dirty', () => { - it('should be true if something changed', () => { - expect(l.dirty).toEqual(false); - l._accounts.push([]); - expect(l.dirty).toEqual(true); - }); - }); - - describe('save()', () => { - beforeEach(() => { - spyOn(metadata, 'create').and.callThrough(); - spyOn(metadata, 'update').and.callThrough(); - l._accounts.push([]); // Create a change to mark object dirty - }); - - it('should create a new metadata entry the first time', (done) => { - l._metadata.existsOnServer = false; - const checks = () => { - expect(l._metadata.create).toHaveBeenCalled(); - }; - let promise = l.save().then(checks); - expect(promise).toBeResolved(done); - }); - - it('should update existing metadata entry', (done) => { - const checks = () => { - expect(l._metadata.update).toHaveBeenCalled(); - }; - let promise = l.save().then(checks); - expect(promise).toBeResolved(done); - }); - - it('should not save if read-only', (done) => { - l._readOnly = true; - const checks = () => { - expect(l._metadata.update).not.toHaveBeenCalled(); - }; - let promise = l.save().then(checks); - expect(promise).toBeResolved(done); - }); - - it('should not save if not dirty', (done) => { - l._accounts.pop(); // Undo change, so object is not dirty - const checks = () => { - expect(l._metadata.update).not.toHaveBeenCalled(); - }; - let promise = l.save().then(checks); - expect(promise).toBeResolved(done); - }); - - it('should result in !dirty', (done) => { - const checks = () => { - expect(l.dirty).toEqual(false); - }; - let promise = l.save().then(checks); - expect(promise).toBeResolved(done); - }); - - it('should remain dirty if read-only', (done) => { - l._readOnly = true; - - const checks = () => { - expect(l.dirty).toEqual(true); - }; - let promise = l.save().then(checks); - expect(promise).toBeResolved(done); - }); - - it('should sync MyWallet if needed', (done) => { - spyOn(l, '_syncWallet').and.callFake((success) => success()); - - l._walletNeedsSync = true; - const checks = () => { - expect(l._syncWallet).toHaveBeenCalled(); - expect(l._walletNeedsSync).toEqual(false); - }; - let promise = l.save().then(checks); - expect(promise).toBeResolved(done); - }); - - it('should not sync MyWallet if not needed', (done) => { - spyOn(l, '_syncWallet').and.callFake((success) => success()); - - expect(l._walletNeedsSync).toEqual(false); - - const checks = () => { - expect(l._syncWallet).not.toHaveBeenCalled(); - }; - let promise = l.save().then(checks); - expect(promise).toBeResolved(done); - }); - }); - - describe('migrateIfNeeded()', () => { - beforeEach(() => { - Labels.prototype.migrateIfNeeded.and.callThrough(); // Remove spy - }); - - describe('first time', () => { - it('should create an initial payload', () => { - let res = l.migrateIfNeeded(null); - expect(res).toEqual(defaultInitialPayload); - }); - - it('should also replace empty object with initial payload', () => { - let res = l.migrateIfNeeded({}); - expect(res).toEqual(defaultInitialPayload); - }); - - describe('wallet without existing labels', () => { - beforeEach(() => { - l._wallet.hdwallet.accounts = [{address_labels_backup: undefined}]; - }); - - it('should create a placeholder for each account', () => { - l._wallet.hdwallet.accounts.push([]); - let res = l.migrateIfNeeded(null); - expect(res.accounts.length).toEqual(2); - }); - - it('should not trigger a wallet sync', () => { - l.migrateIfNeeded(null); - expect(l._walletNeedsSync).toEqual(false); - }); - }); - - describe('wallet with existing labels', () => { - beforeEach(() => { - l._wallet.hdwallet.accounts = [{_address_labels_backup: [ - {index: 1, label: 'Hello'} - ]}]; - }); - - it('should import labels', () => { - let res = l.migrateIfNeeded(null); - expect(res).toEqual(mockPayload); - }); - - it('should create a placeholder for each account', () => { - l._wallet.hdwallet.accounts.push([]); - let res = l.migrateIfNeeded({}); - expect(res.accounts.length).toEqual(2); - }); - - it('should trigger a wallet sync', () => { - l.migrateIfNeeded(null); - expect(l._walletNeedsSync).toEqual(true); - }); - - it('should not trigger a wallet sync if read-only', () => { - l._readOnly = true; - l.migrateIfNeeded(null); - expect(l._walletNeedsSync).toEqual(false); - }); - }); - }); - - describe('no version change', () => { - it('should not change anything', () => { - let res = l.migrateIfNeeded(mockPayload); - expect(res).toEqual(mockPayload); - }); - }); - - // describe('version 1.0.0', () => { - // it('should upgrade to 1.1.0', () => { - // let oldPayload = { - // version: '1.0.0', - // accounts: [[null, {label: 'Hello'}]] - // }; - // let res = l.migrateIfNeeded(oldPayload); - // expect(res).toEqual(mockPayload); - // }); - // }); - - describe('unrecognized new major version', () => { - it('should throw', () => { - let v = latestVersion.split('.').map((i) => parseInt(i)); - v[0] += 1; - let newVersion = v.join('.'); - expect(() => { l.migrateIfNeeded({version: newVersion}); }).toThrow( - Error('LABELS_UNSUPPORTED_MAJOR_VERSION') - ); - }); - }); - - describe('unrecognized new minor version', () => { - it('should switch to readOnly', () => { - let v = latestVersion.split('.').map((i) => parseInt(i)); - v[1] += 1; - let newVersion = v.join('.'); - l.migrateIfNeeded({version: newVersion}); - expect(l.readOnly).toEqual(true); - }); - }); - - describe('unrecognized new patch version', () => { - let res; - let mockPayLoadNewVersion; - - beforeEach(() => { - let v = latestVersion.split('.').map((i) => parseInt(i)); - v[2] += 1; - mockPayLoadNewVersion = JSON.parse(JSON.stringify(mockPayload)); - mockPayLoadNewVersion.version = v.join('.'); - - res = l.migrateIfNeeded(mockPayLoadNewVersion); - }); - - it('should not modify the payload', () => { - expect(res).toEqual(mockPayLoadNewVersion); - }); - - it('should not switch to read-only', () => { - expect(l.readOnly).toEqual(false); - }); - - it('should downgrade the payload if saved', (done) => { - const checks = () => { - expect(l.version).toEqual(latestVersion); - }; - - let promise = l.save().then(checks); - expect(promise).toBeResolved(done); - }); + it('should be false', () => { + expect(l.readOnly).toEqual(false); }); }); @@ -468,57 +155,32 @@ describe('Labels', () => { }); describe('setLabel()', () => { - it('should call save()', () => { - spyOn(l, 'save'); + it('should call syncWallet()', () => { + spyOn(l, 'syncWallet'); l.setLabel(0, 1, 'Updated Label'); - expect(l.save).toHaveBeenCalled(); + expect(l.syncWallet).toHaveBeenCalled(); }); - it('should not call save() if label is unchanged', () => { - spyOn(l, 'save'); + it('should not call syncWallet() if label is unchanged', () => { + spyOn(l, 'syncWallet'); l.setLabel(0, 1, 'Hello'); - expect(l.save).not.toHaveBeenCalled(); - }); - - it('should normally not sync MyWallet', () => { - l.setLabel(0, 1, 'Updated Label'); - expect(l._walletNeedsSync).toEqual(false); - }); - - it('should sync MyWallet if highest labeled index', () => { - l.setLabel(0, 2, 'New Label'); - expect(l._walletNeedsSync).toEqual(true); + expect(l.syncWallet).not.toHaveBeenCalled(); }); }); describe('addLabel()', () => { - it('should call save()', () => { - spyOn(l, 'save').and.callThrough(); + it('should call syncWallet()', () => { + spyOn(l, 'syncWallet').and.callThrough(); l.addLabel(0, 15, 'New Label'); - expect(l.save).toHaveBeenCalled(); - }); - - it('should sync MyWallet', () => { - l.addLabel(0, 15, 'New Label'); - expect(l._walletNeedsSync).toEqual(true); + expect(l.syncWallet).toHaveBeenCalled(); }); }); describe('removeLabel()', () => { - it('should call save()', () => { - spyOn(l, 'save'); + it('should call syncWallet()', () => { + spyOn(l, 'syncWallet'); l.removeLabel(0, 1); - expect(l.save).toHaveBeenCalled(); - }); - - it('should normally not sync MyWallet', () => { - l.removeLabel(0, 0); - expect(l._walletNeedsSync).toEqual(false); - }); - - it('should sync MyWallet if highest labeled index', () => { - l.removeLabel(0, 1, 'New Label'); - expect(l._walletNeedsSync).toEqual(true); + expect(l.syncWallet).toHaveBeenCalled(); }); });