diff --git a/app/browser/api/ledger.js b/app/browser/api/ledger.js index 352a28fe14c..235033a818c 100644 --- a/app/browser/api/ledger.js +++ b/app/browser/api/ledger.js @@ -73,6 +73,7 @@ let balanceTimeoutId = false let runTimeoutId let promotionTimeoutId let togglePromotionTimeoutId +let verifiedTimeoutId = false // Database let v2RulesetDB @@ -149,12 +150,24 @@ const paymentPresent = (state, tabId, present) => { if (!balanceTimeoutId) { module.exports.getBalance(state) } + + getPublisherTimestamp(true) } else if (balanceTimeoutId) { clearTimeout(balanceTimeoutId) balanceTimeoutId = false } } +const getPublisherTimestamp = (updateList) => { + client.publisherTimestamp((err, result) => { + if (err) { + console.error('Error while retrieving publisher timestamp', err.toString()) + return + } + appActions.onPublisherTimestamp(result.timestamp, updateList) + }) +} + const addFoundClosed = (state) => { if (balanceTimeoutId) { clearTimeout(balanceTimeoutId) @@ -185,13 +198,8 @@ const onBootStateFile = (state) => { try { clientprep() client = ledgerClient(null, underscore.extend({roundtrip: roundtrip}, clientOptions), null) - client.publisherTimestamp((err, result) => { - if (err) { - console.error('Error while retrieving publisher timestamp', err.toString()) - return - } - appActions.onPublisherTimestamp(result.timestamp) - }) + + getPublisherTimestamp() } catch (ex) { state = ledgerState.resetInfo(state) bootP = false @@ -540,21 +548,26 @@ const inspectP = (db, path, publisher, property, key, callback) => { // TODO rename function name const verifiedP = (state, publisherKey, callback) => { - client.publisherInfo(publisherKey, (err, result) => { + const clientCallback = (err, result) => { if (err) { console.error(`Error verifying publisher ${publisherKey}: `, err.toString()) return } if (callback) { - let data = false - if (result && result.properties && result.properties.verified) { - data = result.properties.verified + if (result) { + callback(null, result) + } else { + callback(err, {}) } - - callback(null, data) } - }) + } + + if (Array.isArray(publisherKey)) { + client.publishersInfo(publisherKey, clientCallback) + } else { + client.publisherInfo(publisherKey, clientCallback) + } if (process.env.NODE_ENV === 'test') { ['brianbondy.com', 'clifton.io'].forEach((key) => { @@ -689,30 +702,50 @@ const saveVisit = (state, publisherKey, options) => { }) state = ledgerState.setPublisher(state, publisherKey, synopsis.publishers[publisherKey]) state = updatePublisherInfo(state) - state = checkVerifiedStatus(state, publisherKey) + state = module.exports.checkVerifiedStatus(state, publisherKey) return state } -const checkVerifiedStatus = (state, publisherKey) => { - if (publisherKey == null) { +const checkVerifiedStatus = (state, publisherKeys, publisherTimestamp) => { + if (publisherKeys == null) { return state } - const lastUpdate = parseInt(ledgerState.getLedgerValue(state, 'publisherTimestamp')) - const lastPublisherUpdate = parseInt(ledgerState.getPublisherOption(state, publisherKey, 'verifiedTimestamp') || 0) + if (!Array.isArray(publisherKeys)) { + publisherKeys = [publisherKeys] + } + + const checkKeys = [] + const lastUpdate = parseInt(publisherTimestamp || ledgerState.getLedgerValue(state, 'publisherTimestamp')) - if (lastUpdate > lastPublisherUpdate) { - state = module.exports.verifiedP(state, publisherKey, (error, result) => { - if (!error) { - appActions.onPublisherOptionUpdate(publisherKey, 'verified', result) - appActions.onPublisherOptionUpdate(publisherKey, 'verifiedTimestamp', lastUpdate) - savePublisherOption(publisherKey, 'verified', result) - savePublisherOption(publisherKey, 'verifiedTimestamp', lastUpdate) - } - }) + publisherKeys.forEach(key => { + const lastPublisherUpdate = parseInt(ledgerState.getPublisherOption(state, key, 'verifiedTimestamp') || 0) + + if (lastUpdate > lastPublisherUpdate) { + checkKeys.push(key) + } + }) + + if (checkKeys.length === 0) { + return state } + state = module.exports.verifiedP(state, checkKeys, (error, result) => { + if (!error) { + const publisherKey = result.publisher + + if (result && result.properties && result.properties) { + const verified = !!result.properties.verified + appActions.onPublisherOptionUpdate(publisherKey, 'verified', verified) + savePublisherOption(publisherKey, 'verified', verified) + } + + appActions.onPublisherOptionUpdate(publisherKey, 'verifiedTimestamp', lastUpdate) + savePublisherOption(publisherKey, 'verifiedTimestamp', lastUpdate) + } + }) + return state } @@ -1743,12 +1776,18 @@ const initialize = (state, paymentsEnabled) => { ledgerNotifications.init() + if (verifiedTimeoutId) { + clearInterval(verifiedTimeoutId) + } + if (!paymentsEnabled) { client = null newClient = false return ledgerState.resetInfo(state, true) } + verifiedTimeoutId = setInterval(getPublisherTimestamp, 1 * ledgerUtil.milliseconds.hour) + if (client) { return state } @@ -1834,13 +1873,8 @@ const onInitRead = (state, parsedData) => { client = ledgerClient(parsedData.personaId, underscore.extend(parsedData.options, {roundtrip: roundtrip}, options), parsedData) - client.publisherTimestamp((err, result) => { - if (err) { - console.error('Error while retrieving publisher timestamp', err.toString()) - return - } - appActions.onPublisherTimestamp(result.timestamp) - }) + + getPublisherTimestamp(true) // Scenario: User enables Payments, disables it, waits 30+ days, then // enables it again -> reconcileStamp is in the past. @@ -2268,13 +2302,7 @@ const transitionWalletToBat = () => { })) appActions.onBitcoinToBatTransitioned() ledgerNotifications.showBraveWalletUpdated() - client.publisherTimestamp((err, result) => { - if (err) { - console.error('Error while retrieving publisher timestamp', err.toString()) - return - } - appActions.onPublisherTimestamp(result.timestamp) - }) + getPublisherTimestamp() } }) } catch (ex) { @@ -2504,6 +2532,21 @@ const onPromotionResponse = (state, status) => { return state } +const onPublisherTimestamp = (state, oldTimestamp, newTimestamp) => { + if (oldTimestamp === newTimestamp) { + return + } + + const publishers = ledgerState.getPublishers(state) + if (publishers.isEmpty()) { + return + } + + publishers.forEach((publisher, key) => { + module.exports.checkVerifiedStatus(state, key, newTimestamp) + }) +} + const getMethods = () => { const publicMethods = { backupKeys, @@ -2546,7 +2589,9 @@ const getMethods = () => { claimPromotion, onPromotionResponse, getBalance, - getPromotion + getPromotion, + onPublisherTimestamp, + checkVerifiedStatus } let privateMethods = {} @@ -2580,7 +2625,6 @@ const getMethods = () => { }, getCurrentMediaKey: (key) => currentMediaKey, synopsisNormalizer, - checkVerifiedStatus, roundtrip, observeTransactions, onWalletRecovery, diff --git a/app/browser/reducers/ledgerReducer.js b/app/browser/reducers/ledgerReducer.js index abd114e4f6e..f444e0eb6ba 100644 --- a/app/browser/reducers/ledgerReducer.js +++ b/app/browser/reducers/ledgerReducer.js @@ -402,7 +402,11 @@ const ledgerReducer = (state, action, immutableAction) => { } case appConstants.APP_ON_PUBLISHER_TIMESTAMP: { + const oldValue = ledgerState.getLedgerValue(state, 'publisherTimestamp') state = ledgerState.setLedgerValue(state, 'publisherTimestamp', action.get('timestamp')) + if (action.get('updateList')) { + ledgerApi.onPublisherTimestamp(state, oldValue, action.get('timestamp')) + } break } case appConstants.APP_SAVE_LEDGER_PROMOTION: diff --git a/docs/state.md b/docs/state.md index abdbe7a0085..45496f6488c 100644 --- a/docs/state.md +++ b/docs/state.md @@ -186,19 +186,19 @@ AppStore }, info: { addresses: { - BAT: string, - BTC: string, - CARD_ID: string, - ETH: string, + BAT: string, + BTC: string, + CARD_ID: string, + ETH: string, LTC: string }, balance: number, // confirmed balance in BAT.toFixed(2) bravery: { - days: number, + days: number, fee: { amount: number, currency: string - }, + }, setting: string }, converted: string, @@ -210,9 +210,9 @@ AppStore paymentId: string, probi: number, rates:{ - BTC: string, - ETH: number, - EUR: number, + BTC: string, + ETH: number, + EUR: number, USD: number }, reconcileFrequency: number // duration between each reconciliation in days @@ -265,7 +265,7 @@ AppStore options: { persist: boolean, style: string - } + } }, panel: { optInMarkup: { @@ -288,7 +288,7 @@ AppStore options: { persist: boolean, style: string - } + } }, panel: { disclaimer: string, @@ -313,7 +313,7 @@ AppStore options: { persist: boolean, style: string - } + } }, panel: { disclaimer: string, @@ -348,7 +348,7 @@ AppStore options: { exclude: boolean, verified: boolean, - verifiedTimestamp: number, // timestamp of the last change + verifiedTimestamp: number, // timestamp of the last change stickyP: boolean }, pinPercentage: number, diff --git a/js/actions/appActions.js b/js/actions/appActions.js index 7fd4ee522c5..f01b08a0942 100644 --- a/js/actions/appActions.js +++ b/js/actions/appActions.js @@ -1826,10 +1826,11 @@ const appActions = { }) }, - onPublisherTimestamp: function (timestamp) { + onPublisherTimestamp: function (timestamp, updateList) { dispatch({ actionType: appConstants.APP_ON_PUBLISHER_TIMESTAMP, - timestamp + timestamp, + updateList }) }, diff --git a/package-lock.json b/package-lock.json index f1760cb6097..8bb8a215e71 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1640,9 +1640,9 @@ } }, "bat-client": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/bat-client/-/bat-client-2.0.4.tgz", - "integrity": "sha512-nWzEKxWnsNcY9cao3A0NAtCx93u17n7MoztqwvBtP/WG0/mKawO+bm3kaBUUmzcVDvkqUt5745jN1jR7bL5EeA==", + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/bat-client/-/bat-client-2.0.5.tgz", + "integrity": "sha512-gYF6dpJ1LQnVeJksDE1mtm+3UVZgqKuCibdGHSKAbYuJfMMWxdZHZYkAfzb1Dwk6RUa0MdeIpXe7V5B2c3pYPg==", "requires": { "@ambassify/backoff-strategies": "1.0.0", "bat-balance": "1.0.4", @@ -5499,61 +5499,6 @@ } } }, - "electron-download": { - "version": "github:brave/electron-download#409b65caff14edeef1daa36a7445ba6334658d7c", - "dev": true, - "requires": { - "debug": "2.6.9", - "home-path": "1.0.5", - "minimist": "1.2.0", - "mkdirp": "0.5.1", - "mv": "2.1.1", - "nugget": "1.6.2", - "path-exists": "1.0.0", - "rc": "1.2.1" - }, - "dependencies": { - "minimist": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz", - "integrity": "sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ=", - "dev": true - }, - "nugget": { - "version": "1.6.2", - "resolved": "https://registry.npmjs.org/nugget/-/nugget-1.6.2.tgz", - "integrity": "sha1-iMpuA7pXBqmRc/XaCQJZPWvK4Qc=", - "dev": true, - "requires": { - "debug": "2.6.9", - "minimist": "1.2.0", - "pretty-bytes": "1.0.4", - "progress-stream": "1.2.0", - "request": "2.82.0", - "single-line-log": "0.4.1", - "throttleit": "0.0.2" - } - }, - "path-exists": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-1.0.0.tgz", - "integrity": "sha1-1aiZjrce83p0w06w2eum6HjuoIE=", - "dev": true - }, - "single-line-log": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/single-line-log/-/single-line-log-0.4.1.tgz", - "integrity": "sha1-h6VWSfdJ14PsDc2AToFA2Yc8fO4=", - "dev": true - }, - "throttleit": { - "version": "0.0.2", - "resolved": "https://registry.npmjs.org/throttleit/-/throttleit-0.0.2.tgz", - "integrity": "sha1-z+34jmDADdlpe2H90qg0OptoDq8=", - "dev": true - } - } - }, "electron-download-tf": { "version": "4.3.1", "resolved": "https://registry.npmjs.org/electron-download-tf/-/electron-download-tf-4.3.1.tgz", @@ -5739,6 +5684,20 @@ "integrity": "sha1-HUixB9ghJqLz4hHC6iX4A7pVGyE=", "dev": true }, + "electron-download": { + "version": "github:brave/electron-download#409b65caff14edeef1daa36a7445ba6334658d7c", + "dev": true, + "requires": { + "debug": "2.6.9", + "home-path": "1.0.5", + "minimist": "1.2.0", + "mkdirp": "0.5.1", + "mv": "2.1.1", + "nugget": "1.6.2", + "path-exists": "1.0.0", + "rc": "1.2.1" + } + }, "electron-osx-sign": { "version": "0.3.2", "resolved": "https://registry.npmjs.org/electron-osx-sign/-/electron-osx-sign-0.3.2.tgz", @@ -5788,6 +5747,27 @@ "integrity": "sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ=", "dev": true }, + "nugget": { + "version": "1.6.2", + "resolved": "https://registry.npmjs.org/nugget/-/nugget-1.6.2.tgz", + "integrity": "sha1-iMpuA7pXBqmRc/XaCQJZPWvK4Qc=", + "dev": true, + "requires": { + "debug": "2.6.9", + "minimist": "1.2.0", + "pretty-bytes": "1.0.4", + "progress-stream": "1.2.0", + "request": "2.82.0", + "single-line-log": "0.4.1", + "throttleit": "0.0.2" + } + }, + "path-exists": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-1.0.0.tgz", + "integrity": "sha1-1aiZjrce83p0w06w2eum6HjuoIE=", + "dev": true + }, "plist": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/plist/-/plist-1.2.0.tgz", @@ -5800,6 +5780,18 @@ "xmldom": "0.1.27" } }, + "single-line-log": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/single-line-log/-/single-line-log-0.4.1.tgz", + "integrity": "sha1-h6VWSfdJ14PsDc2AToFA2Yc8fO4=", + "dev": true + }, + "throttleit": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/throttleit/-/throttleit-0.0.2.tgz", + "integrity": "sha1-z+34jmDADdlpe2H90qg0OptoDq8=", + "dev": true + }, "xmlbuilder": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-4.0.0.tgz", diff --git a/package.json b/package.json index 46b94552088..68031b83c1d 100644 --- a/package.json +++ b/package.json @@ -88,7 +88,7 @@ "aphrodite": "1.1.0", "async": "^2.0.1", "bat-balance": "^1.0.3", - "bat-client": "^2.0.4", + "bat-client": "^2.0.5", "bat-publisher": "^2.0.3", "bignumber.js": "^4.0.4", "bloodhound-js": "brave/bloodhound", diff --git a/test/unit/app/browser/api/ledgerTest.js b/test/unit/app/browser/api/ledgerTest.js index 5182e2598a4..7ec92b42d18 100644 --- a/test/unit/app/browser/api/ledgerTest.js +++ b/test/unit/app/browser/api/ledgerTest.js @@ -47,6 +47,7 @@ describe('ledger api unit tests', function () { let ledgersetPromotionSpy let ledgergetPromotionSpy let ledgerSetTimeUntilReconcile + let onPublisherOptionUpdate const defaultAppState = Immutable.fromJS({ cache: { @@ -91,6 +92,7 @@ describe('ledger api unit tests', function () { onLedgerCallbackSpy = sinon.spy(appActions, 'onLedgerCallback') onBitcoinToBatBeginTransitionSpy = sinon.spy(appActions, 'onBitcoinToBatBeginTransition') onChangeSettingSpy = sinon.spy(appActions, 'changeSetting') + onPublisherOptionUpdate = sinon.spy(appActions, 'onPublisherOptionUpdate') // default to tab state which should be tracked tabState = tabState.setIn(['navigationState', 'activeEntry'], { @@ -503,18 +505,43 @@ describe('ledger api unit tests', function () { describe('checkVerifiedStatus', function () { let verifiedPSpy + let returnValue = false before(function () { + onPublisherOptionUpdate.reset() verifiedPSpy = sinon.spy(ledgerApi, 'verifiedP') + ledgerApi.setClient({ - publisherInfo: function () { - return false + publisherInfo: function (publisherKey, callback) { + callback(null, { + publisher: 'test.io', + properties: { + verified: returnValue + } + }) + }, + publishersInfo: function (publisherKey, callback) { + publisherKey.forEach(key => { + callback(null, { + publisher: key, + properties: { + verified: returnValue + } + }) + }) } }) }) + afterEach(function () { + returnValue = false + verifiedPSpy.reset() + onPublisherOptionUpdate.reset() + }) + after(function () { verifiedPSpy.restore() + onPublisherOptionUpdate.restore() ledgerApi.setClient(undefined) }) @@ -527,22 +554,54 @@ describe('ledger api unit tests', function () { it('only update if timestamp is older then current', function () { const newState = defaultAppState .setIn(['ledger', 'publisherTimestamp'], 20) - .setIn(['ledger', 'synopsis', 'publishers', 'clifton.io', 'options', 'verifiedTimestamp'], 20) - const result = ledgerApi.checkVerifiedStatus(newState, 'clifton.io') + .setIn(['ledger', 'synopsis', 'publishers', 'test.io', 'options', 'verifiedTimestamp'], 20) + const result = ledgerApi.checkVerifiedStatus(newState, 'test.io') assert.deepEqual(result.toJS(), newState.toJS()) assert(verifiedPSpy.notCalled) }) it('update when timestamp is older', function () { + returnValue = true const newState = defaultAppState .setIn(['ledger', 'publisherTimestamp'], 20) - .setIn(['ledger', 'synopsis', 'publishers', 'clifton.io', 'options', 'verifiedTimestamp'], 10) + .setIn(['ledger', 'synopsis', 'publishers', 'test.io', 'options', 'verifiedTimestamp'], 10) - const expectedState = newState - .setIn(['ledger', 'synopsis', 'publishers', 'clifton.io', 'options', 'verified'], true) - const result = ledgerApi.checkVerifiedStatus(newState, 'clifton.io') - assert.deepEqual(result.toJS(), expectedState.toJS()) + const result = ledgerApi.checkVerifiedStatus(newState, 'test.io') + assert.deepEqual(result.toJS(), newState.toJS()) assert(verifiedPSpy.calledOnce) + assert(onPublisherOptionUpdate.withArgs('test.io', 'verified', true).calledOnce) + }) + + it('change publisher verified status from true to false', function () { + const newState = defaultAppState + .setIn(['ledger', 'publisherTimestamp'], 20) + .setIn(['ledger', 'synopsis', 'publishers', 'test.io', 'options', 'verifiedTimestamp'], 10) + .setIn(['ledger', 'synopsis', 'publishers', 'test.io', 'options', 'verified'], true) + + const result = ledgerApi.checkVerifiedStatus(newState, 'test.io') + assert.deepEqual(result.toJS(), newState.toJS()) + assert(verifiedPSpy.calledOnce) + assert(onPublisherOptionUpdate.withArgs('test.io', 'verified', false).calledOnce) + }) + + it('handle multiple publishers', function () { + const newState = defaultAppState + .setIn(['ledger', 'publisherTimestamp'], 20) + .setIn(['ledger', 'synopsis', 'publishers', 'test.io', 'options', 'verifiedTimestamp'], 10) + .setIn(['ledger', 'synopsis', 'publishers', 'test1.io', 'options', 'verifiedTimestamp'], 15) + .setIn(['ledger', 'synopsis', 'publishers', 'test2.io', 'options', 'verifiedTimestamp'], 20) + .setIn(['ledger', 'synopsis', 'publishers', 'test3.io', 'options', 'verifiedTimestamp'], 30) + + const result = ledgerApi.checkVerifiedStatus(newState, [ + 'test1.io', + 'test2.io', + 'test3.io', + 'test.io' + ]) + assert.deepEqual(result.toJS(), newState.toJS()) + assert(verifiedPSpy.withArgs(sinon.match.any, ['test1.io', 'test.io'], sinon.match.any).calledOnce) + assert.deepEqual(onPublisherOptionUpdate.getCall(0).args, ['test1.io', 'verified', false]) + assert.deepEqual(onPublisherOptionUpdate.getCall(2).args, ['test.io', 'verified', false]) }) }) @@ -1715,4 +1774,60 @@ describe('ledger api unit tests', function () { }) }) }) + + describe('onPublisherTimestamp', function () { + let checkVerifiedStatusSpy + + const stateWithData = defaultAppState + .setIn(['ledger', 'synopsis', 'publishers', 'clifton.io'], Immutable.fromJS({ + visits: 1 + })) + + before(function () { + checkVerifiedStatusSpy = sinon.spy(ledgerApi, 'checkVerifiedStatus') + ledgerApi.setClient({ + publisherInfo: function () { + return false + }, + publishersInfo: function () { + return false + } + }) + }) + + afterEach(function () { + checkVerifiedStatusSpy.reset() + }) + + after(function () { + checkVerifiedStatusSpy.restore() + ledgerApi.setClient(undefined) + }) + + it('publisher timestamp is the same', function () { + ledgerApi.onPublisherTimestamp(defaultAppState, 10, 10) + assert(checkVerifiedStatusSpy.notCalled) + }) + + it('publisher list is empty', function () { + ledgerApi.onPublisherTimestamp(defaultAppState, 10, 20) + assert(checkVerifiedStatusSpy.notCalled) + }) + + it('check publishers', function () { + ledgerApi.onPublisherTimestamp(stateWithData, 10, 20) + assert(checkVerifiedStatusSpy.withArgs(sinon.match.any, 'clifton.io', 20).calledOnce) + }) + + it('check multiple publishers', function () { + const multiple = stateWithData + .setIn(['ledger', 'synopsis', 'publishers', 'brave.com'], Immutable.fromJS({ + visits: 1 + })) + ledgerApi.onPublisherTimestamp(multiple, 10, 20) + + assert.equal(checkVerifiedStatusSpy.getCall(0).args[1], 'clifton.io') + assert.equal(checkVerifiedStatusSpy.getCall(1).args[1], 'brave.com') + }) + }) })