From bcb8a4ce6840f5d3198c2577fbf9e8fbab704ff1 Mon Sep 17 00:00:00 2001 From: Matt Huggins Date: Tue, 30 Jan 2018 01:08:59 -0600 Subject: [PATCH] Replaced balance, claim, & transaction history actions/reducers with new actions architecture (#611) * Added actions for fetching asset & token balances * Removed unused prop mapping from app store * Added balance actions and HOC's to replaced most of loadWalletData function * Reset stores on logout * Renamed accountActions to authActions * Removed unused deprecated functions * Fixed fetching state from action that has not been fetched * Added claims actions and account actions * Replaced load wallet claim sync with new action data * Replaced load wallet transactions sync with new action data * Fixed tokens argument being passed to account request action --- __tests__/components/Claim.test.js | 178 +++----- __tests__/components/LoginNep2.test.js | 2 +- .../components/TransactionHistory.test.js | 81 ++-- __tests__/components/WalletInfo.test.js | 143 ++----- .../__snapshots__/Claim.test.js.snap | 382 ++++++++++++++---- .../TransactionHistory.test.js.snap | 7 +- .../__snapshots__/WalletInfo.test.js.snap | 8 +- __tests__/modules/wallet.test.js | 64 --- __tests__/store/reducers.test.js | 2 - __tests__/testHelpers.js | 15 + app/actions/accountActions.js | 72 +--- app/actions/authActions.js | 69 ++++ app/actions/balancesActions.js | 47 +++ app/actions/claimsActions.js | 25 ++ app/actions/transactionHistoryActions.js | 24 ++ app/containers/App/Header/Logout/index.js | 2 +- app/containers/App/Header/index.js | 4 +- app/containers/App/index.js | 19 +- app/containers/Claim/Claim.jsx | 14 +- app/containers/Claim/index.js | 19 +- app/containers/Dashboard/Dashboard.jsx | 24 +- app/containers/Dashboard/index.js | 48 ++- app/containers/LoginLedgerNanoS/index.js | 2 +- app/containers/LoginLocalStorage/index.js | 2 +- app/containers/LoginNep2/index.js | 2 +- app/containers/LoginPrivateKey/index.js | 2 +- .../TransactionHistory/TransactionHistory.jsx | 18 +- app/containers/TransactionHistory/index.js | 37 +- app/containers/WalletInfo/WalletInfo.jsx | 27 +- app/containers/WalletInfo/index.js | 37 +- app/core/deprecated.js | 49 +-- app/core/networks.js | 5 + app/core/wallet.js | 21 +- app/hocs/api/withProgressProp.js | 4 +- app/hocs/api/withReload.js | 56 +-- app/hocs/api/withReset.js | 7 + app/hocs/api/withResponsiveAction.js | 65 +++ app/hocs/auth/withLoginRedirect.js | 7 +- app/hocs/auth/withLogoutRedirect.js | 7 +- app/hocs/auth/withLogoutReset.js | 27 ++ app/hocs/auth/withRedirect.js | 6 +- app/hocs/helpers/didLogin.js | 6 + app/hocs/helpers/didLogout.js | 6 + .../{withAccountData.js => withAuthData.js} | 10 +- app/hocs/withFailureNotification.js | 12 +- app/hocs/withSuccessNotification.js | 51 +++ app/modules/claim.js | 56 +-- app/modules/index.js | 4 - app/modules/sale.js | 5 +- app/modules/transactions.js | 64 +-- app/modules/wallet.js | 207 ---------- app/util/Logs.js | 2 +- package.json | 1 + 53 files changed, 1004 insertions(+), 1050 deletions(-) delete mode 100644 __tests__/modules/wallet.test.js create mode 100644 __tests__/testHelpers.js create mode 100644 app/actions/authActions.js create mode 100644 app/actions/balancesActions.js create mode 100644 app/actions/claimsActions.js create mode 100644 app/actions/transactionHistoryActions.js create mode 100644 app/hocs/api/withReset.js create mode 100644 app/hocs/api/withResponsiveAction.js create mode 100644 app/hocs/auth/withLogoutReset.js create mode 100644 app/hocs/helpers/didLogin.js create mode 100644 app/hocs/helpers/didLogout.js rename app/hocs/{withAccountData.js => withAuthData.js} (58%) create mode 100644 app/hocs/withSuccessNotification.js delete mode 100644 app/modules/wallet.js diff --git a/__tests__/components/Claim.test.js b/__tests__/components/Claim.test.js index 475e8cef0..bc2d103c2 100644 --- a/__tests__/components/Claim.test.js +++ b/__tests__/components/Claim.test.js @@ -1,17 +1,17 @@ import React from 'react' import * as neonjs from 'neon-js' -import { cloneDeep } from 'lodash' -import { Provider } from 'react-redux' -import configureStore from 'redux-mock-store' -import thunk from 'redux-thunk' -import { shallow, mount } from 'enzyme' +import { merge } from 'lodash' +import { mount } from 'enzyme' +import { createStore, provideStore } from '../testHelpers' import Claim from '../../app/containers/Claim' import { setClaimRequest, disableClaim } from '../../app/modules/claim' -import { SHOW_NOTIFICATION, HIDE_NOTIFICATIONS, DEFAULT_POSITION } from '../../app/modules/notifications' +import { SHOW_NOTIFICATION } from '../../app/modules/notifications' import { NOTIFICATION_LEVELS, MAIN_NETWORK_ID } from '../../app/core/constants' import { LOADED } from '../../app/values/state' +jest.useFakeTimers() + const initialState = { api: { NETWORK: { @@ -19,152 +19,92 @@ const initialState = { state: LOADED, data: MAIN_NETWORK_ID }, - SETTINGS: { + CLAIMS: { batch: false, state: LOADED, - data: {} + data: { + total: '0.01490723' + } + }, + BALANCES: { + batch: false, + state: LOADED, + data: { + NEO: '10', + GAS: '10.00000000' + } } }, claim: { - claimAmount: 10, claimRequest: false, - claimWasUpdated: false, disableClaimButton: false - }, - account: { - wif: 'wif', - address: 'address' - }, - wallet: { - NEO: '1' } } -const setup = (state = initialState, shallowRender = true) => { - const store = configureStore([thunk])(state) - - let wrapper - if (shallowRender) { - wrapper = shallow() - } else { - wrapper = mount( - - - - ) - } - - return { - store, - wrapper - } +const simulateSendAsset = (result) => { + return jest.fn(() => { + return new Promise((resolve, reject) => { + resolve({ result }) + }) + }) } describe('Claim', () => { test('should render without crashing', () => { - const { wrapper } = setup() + const store = createStore(initialState) + const wrapper = mount(provideStore(, store)) expect(wrapper).toMatchSnapshot() }) - test('should render claim GAS button when claim button is not disabled', () => { - const { wrapper } = setup() - expect(wrapper.dive()).toMatchSnapshot() + test('should render claim GAS button as enabled', () => { + const store = createStore(initialState) + const wrapper = mount(provideStore(, store)) + expect(wrapper).toMatchSnapshot() }) - test('should not render claim GAS button when claim button is disabled', () => { - const newState = cloneDeep(initialState) - newState.claim.disableClaimButton = true - const { wrapper } = setup(newState) - expect(wrapper.dive()).toMatchSnapshot() + test('should render claim GAS button as disabled', () => { + const state = merge({}, initialState, { claim: { disableClaimButton: true } }) + const store = createStore(state) + const wrapper = mount(provideStore(, store)) + expect(wrapper).toMatchSnapshot() }) - describe('when do GAS claim button is clicked', () => { - test('should dispatch transaction failure event', async () => { - const { wrapper, store } = setup() - neonjs.api.neonDB.doSendAsset = jest.fn(() => { - return new Promise((resolve, reject) => { - resolve({ result: undefined }) - }) - }) - wrapper.dive().find('#claim').simulate('click') + describe('when claim GAS button is clicked', () => { + test('should dispatch transaction failure event', async (done) => { + const store = createStore(initialState) + const wrapper = mount(provideStore(, store)) + neonjs.api.neonDB.doSendAsset = simulateSendAsset(false) + wrapper.find('button#claim').simulate('click') + jest.runAllTimers() await Promise.resolve().then().then().then() + const actions = store.getActions() - expect(actions.length).toEqual(4) - expect(actions[0]).toEqual({ - type: HIDE_NOTIFICATIONS, - payload: expect.objectContaining({ - dismissible: true, - position: DEFAULT_POSITION - }) - }) - expect(actions[1]).toEqual({ - type: SHOW_NOTIFICATION, - payload: expect.objectContaining({ - message: 'Sending NEO to Yourself...', - level: NOTIFICATION_LEVELS.INFO - }) - }) - expect(actions[2]).toEqual({ - type: HIDE_NOTIFICATIONS, - payload: expect.objectContaining({ - dismissible: true, - position: DEFAULT_POSITION - }) - }) - expect(actions[3]).toEqual({ + + expect(actions).toContainEqual(expect.objectContaining({ type: SHOW_NOTIFICATION, payload: expect.objectContaining({ - message: 'Transaction failed!', - level: NOTIFICATION_LEVELS.ERROR + level: NOTIFICATION_LEVELS.ERROR, + message: 'Transaction failed!' }) - }) + })) + done() }) - test('should dispatch transaction waiting, set claim request and disable claim event', async () => { - const { wrapper, store } = setup() - neonjs.api.neonDB.doSendAsset = jest.fn(() => { - return new Promise((resolve, reject) => { - resolve({ result: true }) - }) - }) - - wrapper.dive().find('#claim').simulate('click') + test('should dispatch transaction waiting, set claim request, and disable claim events', async (done) => { + const store = createStore(initialState) + const wrapper = mount(provideStore(, store)) + neonjs.api.neonDB.doSendAsset = simulateSendAsset(true) + wrapper.find('button#claim').simulate('click') + jest.runAllTimers() await Promise.resolve().then().then().then() + const actions = store.getActions() - expect(actions.length).toEqual(6) - expect(actions[0]).toEqual({ - type: HIDE_NOTIFICATIONS, - payload: expect.objectContaining({ - dismissible: true, - position: DEFAULT_POSITION - }) - }) - expect(actions[1]).toEqual({ - type: SHOW_NOTIFICATION, - payload: expect.objectContaining({ - message: 'Sending NEO to Yourself...', - level: NOTIFICATION_LEVELS.INFO - }) - }) - expect(actions[2]).toEqual({ - type: HIDE_NOTIFICATIONS, - payload: expect.objectContaining({ - dismissible: true, - position: DEFAULT_POSITION - }) - }) - expect(actions[3]).toEqual({ - type: SHOW_NOTIFICATION, - payload: expect.objectContaining({ - message: 'Waiting for transaction to clear...', - level: NOTIFICATION_LEVELS.INFO - }) - }) - expect(actions[4]).toEqual(setClaimRequest(true)) - expect(actions[5]).toEqual(disableClaim(true)) + expect(actions).toContainEqual(setClaimRequest(true)) + expect(actions).toContainEqual(disableClaim(true)) + done() }) }) }) diff --git a/__tests__/components/LoginNep2.test.js b/__tests__/components/LoginNep2.test.js index 4f0f246c8..7c68bef2d 100644 --- a/__tests__/components/LoginNep2.test.js +++ b/__tests__/components/LoginNep2.test.js @@ -84,6 +84,6 @@ describe('LoginNep2', () => { const actions = store.getActions() expect(actions.length).toEqual(1) - expect(actions[0].type).toEqual('ACCOUNT/REQ/REQUEST') + expect(actions[0].type).toEqual('AUTH/REQ/REQUEST') }) }) diff --git a/__tests__/components/TransactionHistory.test.js b/__tests__/components/TransactionHistory.test.js index 0937d66d9..b484fd38f 100644 --- a/__tests__/components/TransactionHistory.test.js +++ b/__tests__/components/TransactionHistory.test.js @@ -3,15 +3,18 @@ import configureStore from 'redux-mock-store' import { Provider } from 'react-redux' import thunk from 'redux-thunk' import { shallow, mount } from 'enzyme' +import { merge } from 'lodash' import TransactionHistory from '../../app/containers/TransactionHistory' -import { setTransactionHistory } from '../../app/modules/wallet' -import { setIsLoadingTransaction } from '../../app/modules/transactions' -import { MAIN_NETWORK_ID } from '../../app/core/constants' +import { MAIN_NETWORK_ID, EXPLORERS } from '../../app/core/constants' import { LOADED } from '../../app/values/state' const initialState = { api: { + AUTH: { + address: 'AWy7RNBVr9vDadRMK9p7i7Z1tL7GrLAxoh', + wif: 'L4SLRcPgqNMAMwM3nFSxnh36f1v5omjPg3Ewy1tg2PnEon8AcHou' + }, NETWORK: { batch: false, state: LOADED, @@ -20,38 +23,30 @@ const initialState = { SETTINGS: { batch: false, state: LOADED, - data: {} + data: { + blockExplorer: EXPLORERS.NEO_TRACKER + } + }, + TRANSACTION_HISTORY: { + batch: false, + state: LOADED, + data: [] } - }, - account: { - loggedIn: true, - wif: 'L4SLRcPgqNMAMwM3nFSxnh36f1v5omjPg3Ewy1tg2PnEon8AcHou', - address: 'AWy7RNBVr9vDadRMK9p7i7Z1tL7GrLAxoh' - }, - wallet: { - transactions: [] - }, - transactions: { - isLoadingTransactions: false } } -const transactions = { - wallet: { - transactions: [ - { - NEO: '50', - GAS: '0.00000000', - txid: '76938979' - }, - { - NEO: '0', - GAS: '0.40000000', - txid: '76938980' - } - ] +const transactions = [ + { + NEO: '50', + GAS: '0.00000000', + txid: '76938979' + }, + { + NEO: '0', + GAS: '0.40000000', + txid: '76938980' } -} +] const setup = (state = initialState, shallowRender = true) => { const store = configureStore([thunk])(state) @@ -74,22 +69,12 @@ const setup = (state = initialState, shallowRender = true) => { } describe('TransactionHistory', () => { - test('renders without crashing', (done) => { + test('renders without crashing', () => { const { wrapper } = setup() expect(wrapper).toMatchSnapshot() - done() - }) - - test('calls syncTransactionHistory after rendering', async () => { - const { store } = setup(initialState, false) - await Promise.resolve('Pause').then().then().then() - const actions = store.getActions() - expect(actions[0]).toEqual(setIsLoadingTransaction(true)) - expect(actions[1]).toEqual(setIsLoadingTransaction(false)) - expect(actions[2]).toEqual(setTransactionHistory(initialState.wallet.transactions)) }) - test('correctly renders no transaction history', (done) => { + test('correctly renders no transaction history', () => { const { wrapper } = setup(initialState, false) const columnHeader = wrapper.find('#columnHeader') @@ -97,19 +82,19 @@ describe('TransactionHistory', () => { const transactionList = wrapper.find('#transactionList') expect(transactionList.children().length).toEqual(0) - done() }) - test('correctly renders with NEO and GAS transaction history', (done) => { - const transactionState = Object.assign({}, initialState, transactions) + test('correctly renders with NEO and GAS transaction history', () => { + const transactionState = merge({}, initialState, { + api: { TRANSACTION_HISTORY: { data: transactions } } + }) const { wrapper } = setup(transactionState, false) const transactionList = wrapper.find('#transactionList') expect(transactionList.children().length).toEqual(2) - expect(transactionList.childAt(0).find('.txid').first().text()).toEqual(transactions.wallet.transactions[0].txid) - expect(transactionList.childAt(1).find('.txid').first().text()).toEqual(transactions.wallet.transactions[1].txid) + expect(transactionList.childAt(0).find('.txid').first().text()).toEqual(transactions[0].txid) + expect(transactionList.childAt(1).find('.txid').first().text()).toEqual(transactions[1].txid) expect(transactionList.childAt(0).find('.amountNEO').text()).toEqual('50 NEO') expect(transactionList.childAt(1).find('.amountGAS').text()).toEqual('0.40000000 GAS') - done() }) }) diff --git a/__tests__/components/WalletInfo.test.js b/__tests__/components/WalletInfo.test.js index 4830bfb47..1613c3981 100644 --- a/__tests__/components/WalletInfo.test.js +++ b/__tests__/components/WalletInfo.test.js @@ -1,23 +1,14 @@ import React from 'react' import * as neonjs from 'neon-js' -import { Provider } from 'react-redux' -import configureStore from 'redux-mock-store' -import thunk from 'redux-thunk' import { merge } from 'lodash' import { mount, shallow } from 'enzyme' -import { - SET_TRANSACTION_HISTORY, - SET_BALANCE -} from '../../app/modules/wallet' +import { createStore, provideStore, provideState } from '../testHelpers' +import WalletInfo from '../../app/containers/WalletInfo' import { SHOW_NOTIFICATION } from '../../app/modules/notifications' -import { LOADING_TRANSACTIONS } from '../../app/modules/transactions' - -import { DEFAULT_CURRENCY_CODE, MAIN_NETWORK_ID } from '../../app/core/constants' +import { NOTIFICATION_LEVELS, DEFAULT_CURRENCY_CODE, MAIN_NETWORK_ID } from '../../app/core/constants' import { LOADED } from '../../app/values/state' -import WalletInfo from '../../app/containers/WalletInfo' - // TODO research how to move the axios mock code which is repeated in NetworkSwitch to a helper or config file import axios from 'axios' import MockAdapter from 'axios-mock-adapter' @@ -28,17 +19,12 @@ axiosMock .onGet('http://testnet-api.wallet.cityofzion.io/v2/version') .reply(200, { version }) axiosMock - .onGet('https://api.coinmarketcap.com/v1/ticker/NEO/?convert=USD') - .reply(200, [{ price_usd: 24.5 }]) -axiosMock - .onGet('https://api.coinmarketcap.com/v1/ticker/GAS/?convert=USD') - .reply(200, [{ price_usd: 18.2 }]) + .onGet('https://api.coinmarketcap.com/v1/ticker/?limit=0&convert=USD') + .reply(200, [{ symbol: 'NEO', price_usd: 24.5 }, { symbol: 'GAS', price_usd: 18.2 }]) jest.mock('electron', () => ({ app: { - getPath: () => { - return 'C:\\tmp\\mock_path' - } + getPath: () => 'C:\\tmp\\mock_path' } })) jest.useFakeTimers() @@ -54,7 +40,7 @@ const initialState = { state: LOADED, data: MAIN_NETWORK_ID }, - ACCOUNT: { + AUTH: { batch: false, state: LOADED, data: { @@ -65,7 +51,8 @@ const initialState = { batch: false, state: LOADED, data: { - currency: DEFAULT_CURRENCY_CODE + currency: DEFAULT_CURRENCY_CODE, + tokens: [] } }, PRICES: { @@ -75,47 +62,38 @@ const initialState = { NEO: 25.48, GAS: 18.1 } + }, + BALANCES: { + batch: false, + state: LOADED, + data: { + NEO: '100001', + GAS: '1000.0001601' + } + }, + CLAIMS: { + batch: false, + state: LOADED, + data: { + total: '0.5' + } } }, - wallet: { - NEO: '100001', - GAS: '1000.0001601', - tokenBalances: [] - }, claim: { - claimAmount: 0.5 - } -} - -const setup = (state = initialState, shallowRender = true) => { - const store = configureStore([thunk])(state) - - let wrapper - if (shallowRender) { - wrapper = shallow() - } else { - wrapper = mount( - - - - ) - } - - return { - store, - wrapper + claimRequest: false, + disableClaimButton: false } } describe('WalletInfo', () => { - test('renders without crashing', done => { - const { wrapper } = setup() + test('renders without crashing', () => { + const store = createStore(initialState) + const wrapper = shallow() expect(wrapper).toMatchSnapshot() - done() }) - test('correctly renders data from state', done => { - const { wrapper } = setup(initialState, false) + test('correctly renders data from state', () => { + const wrapper = mount(provideState(, initialState)) const neoWalletValue = wrapper.find('.neoWalletValue') const gasWalletValue = wrapper.find('.gasWalletValue') @@ -133,39 +111,28 @@ describe('WalletInfo', () => { expect(neoField.text()).toEqual('100,001') // TODO: Test the GAS tooltip value, this is testing the display value, truncated to 4 decimals expect(gasField.text()).toEqual('1,000.0002') - done() }) - test('refreshBalance is getting called on click', async () => { - const { wrapper, store } = setup(initialState, false) + test('account data refreshes when refresh button is clicked', () => { + const store = createStore(initialState) + const wrapper = mount(provideStore(, store)) wrapper.find('.refreshBalance').simulate('click') - await Promise.resolve('Pause') - .then() - .then() - .then() - jest.runAllTimers() - - const action = store.getActions().find((action) => action.type === SET_BALANCE) - - expect(action).toEqual({ - type: SET_BALANCE, - payload: { - NEO: '1', - GAS: '1' - } - }) + expect(store.getActions()).toContainEqual(expect.objectContaining({ + type: 'BATCH/REQUEST', + meta: expect.objectContaining({ id: 'ACCOUNT' }) + })) }) - test('correctly renders data from state with non-default currency', done => { + test('correctly renders data from state with non-default currency', () => { const testState = merge(initialState, { api: { SETTINGS: { data: { currency: 'eur' } }, PRICES: { data: { NEO: 1.11, GAS: 0.55 } } } }) - const { wrapper } = setup(testState, false) + const wrapper = mount(provideState(, testState)) const neoWalletValue = wrapper.find('.neoWalletValue') const gasWalletValue = wrapper.find('.gasWalletValue') @@ -178,35 +145,5 @@ describe('WalletInfo', () => { expect(neoWalletValue.text()).toEqual(`€${expectedNeoWalletValue} EUR`) expect(gasWalletValue.text()).toEqual(`€${expectedGasWalletValue} EUR`) expect(walletValue.text()).toEqual(`Total €${expectedWalletValue} EUR`) - - done() - }) - - test('network error is shown with connectivity error', async () => { - neonjs.api.neonDB.getBalance = jest.fn(() => { - return new Promise((resolve, reject) => { - reject(new Error()) - }) - }) - const { wrapper, store } = setup(initialState, false) - wrapper.find('.refreshBalance').simulate('click') - - jest.runAllTimers() - await Promise.resolve('Pause') - .then() - .then() - .then() - .then() - - const actions = store.getActions() - const notifications = [] - actions.forEach(action => { - if (action.type === SHOW_NOTIFICATION) { - notifications.push(action) - } - }) - - // let's make sure the last notification show was an error. - expect(notifications.pop().payload.level).toEqual('error') }) }) diff --git a/__tests__/components/__snapshots__/Claim.test.js.snap b/__tests__/components/__snapshots__/Claim.test.js.snap index b5db1b722..06d54bca7 100644 --- a/__tests__/components/__snapshots__/Claim.test.js.snap +++ b/__tests__/components/__snapshots__/Claim.test.js.snap @@ -1,58 +1,229 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`Claim should not render claim GAS button when claim button is disabled 1`] = ` -
- - - -
+ +
+ + +
+ + +
+
+
+
+
+ + + `; -exports[`Claim should render claim GAS button when claim button is not disabled 1`] = ` -
- - - -
+ +
+ + +
+ + +
+
+
+
+
+ + + `; exports[`Claim should render without crashing 1`] = ` - +> + + + +
+ + +
+ + +
+
+
+
+
+
+
+ `; diff --git a/__tests__/components/__snapshots__/TransactionHistory.test.js.snap b/__tests__/components/__snapshots__/TransactionHistory.test.js.snap index ba3722f18..0b113cd90 100644 --- a/__tests__/components/__snapshots__/TransactionHistory.test.js.snap +++ b/__tests__/components/__snapshots__/TransactionHistory.test.js.snap @@ -1,8 +1,9 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP exports[`TransactionHistory renders without crashing 1`] = ` - `; diff --git a/__tests__/components/__snapshots__/WalletInfo.test.js.snap b/__tests__/components/__snapshots__/WalletInfo.test.js.snap index 3053e933b..0ed6cad63 100644 --- a/__tests__/components/__snapshots__/WalletInfo.test.js.snap +++ b/__tests__/components/__snapshots__/WalletInfo.test.js.snap @@ -1,10 +1,7 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP exports[`WalletInfo renders without crashing 1`] = ` - `; diff --git a/__tests__/modules/wallet.test.js b/__tests__/modules/wallet.test.js deleted file mode 100644 index cb3d3fc71..000000000 --- a/__tests__/modules/wallet.test.js +++ /dev/null @@ -1,64 +0,0 @@ -import walletReducer, { - setBalance, - setTransactionHistory, - SET_BALANCE, - SET_TRANSACTION_HISTORY -} from '../../app/modules/wallet' - -describe('wallet module tests', () => { - const NEO = '1' - const GAS = '1' - const initialState = { - loaded: false, - NEO: '0', - GAS: '0', - tokenBalances: [], - transactions: [] - } - - describe('setBalance tests', () => { - const expectedAction = { - type: SET_BALANCE, - payload: { NEO, GAS } - } - - test('setBalance action works', () => { - expect(setBalance(NEO, GAS)).toEqual(expectedAction) - }) - - test('setBalance reducer should return the initial state', () => { - expect(walletReducer(undefined, {})).toEqual(initialState) - }) - - test('wallet reducer should handle SET_BALANCE', () => { - const expectedState = { - ...initialState, - NEO, - GAS - } - expect(walletReducer(undefined, expectedAction)).toEqual(expectedState) - }) - }) - - describe('setTransactionHistory tests', () => { - const transactions = ['random array', 'of any items', 'for this test'] - const expectedAction = { - type: SET_TRANSACTION_HISTORY, - payload: { - transactions - } - } - - test('setTransactionHistory action works', () => { - expect(setTransactionHistory(transactions)).toEqual(expectedAction) - }) - - test('wallet reducer should handle SET_TRANSACTION_HISTORY', () => { - const expectedState = { - ...initialState, - transactions - } - expect(walletReducer(undefined, expectedAction)).toEqual(expectedState) - }) - }) -}) diff --git a/__tests__/store/reducers.test.js b/__tests__/store/reducers.test.js index c41d24a28..480352fd7 100644 --- a/__tests__/store/reducers.test.js +++ b/__tests__/store/reducers.test.js @@ -6,8 +6,6 @@ describe('root reducer', () => { api: expect.any(Object), addressBook: expect.any(Object), generateWallet: expect.any(Object), - wallet: expect.any(Object), - transactions: expect.any(Object), notifications: expect.any(Object), claim: expect.any(Object), modal: expect.any(Object) diff --git a/__tests__/testHelpers.js b/__tests__/testHelpers.js new file mode 100644 index 000000000..6aaa16f05 --- /dev/null +++ b/__tests__/testHelpers.js @@ -0,0 +1,15 @@ +import React from 'react' +import thunk from 'redux-thunk' +import configureStore from 'redux-mock-store' +import { Provider } from 'react-redux' + +export const createStore = configureStore([thunk]) + +export const provideState = (node, initialState = {}) => { + const store = createStore(initialState) + return provideStore(node, store) +} + +export const provideStore = (node, store) => { + return {node} +} diff --git a/app/actions/accountActions.js b/app/actions/accountActions.js index ca933c504..2e297951c 100644 --- a/app/actions/accountActions.js +++ b/app/actions/accountActions.js @@ -1,69 +1,13 @@ // @flow -import { wallet } from 'neon-js' -import { noop } from 'lodash' - -import createRequestActions from '../util/api/createRequestActions' -import { upgradeNEP6AddAddresses } from '../core/account' -import { validatePassphraseLength } from '../core/wallet' - -type WifLoginProps = { - wif: string -} - -type LedgerLoginProps = { - publicKey: string, - signingFunction: Function -} - -type Nep2LoginProps = { - passphrase: string, - encryptedWIF: string -} - -type AccountType = ?{ - address: string, - wif?: string, - publicKey?: string, - signingFunction?: Function, - isHardwareLogin: boolean -} +import createBatchActions from '../util/api/createBatchActions' +import balancesActions from './balancesActions' +import claimsActions from './claimsActions' +import transactionHistoryActions from './transactionHistoryActions' export const ID = 'ACCOUNT' -export const wifLoginActions = createRequestActions(ID, ({ wif }: WifLoginProps) => (state: Object): AccountType => { - const account = new wallet.Account(wif) - - return { wif, address: account.address, isHardwareLogin: false } -}) - -export const nep2LoginActions = createRequestActions(ID, ({ passphrase, encryptedWIF }: Nep2LoginProps) => async (state: Object): Promise => { - if (!validatePassphraseLength(passphrase)) { - throw new Error('Passphrase too short') - } - - if (!wallet.isNEP2(encryptedWIF)) { - throw new Error('That is not a valid encrypted key') - } - - const wif = wallet.decrypt(encryptedWIF, passphrase) - const account = new wallet.Account(wif) - - await upgradeNEP6AddAddresses(encryptedWIF, wif) - - return { wif, address: account.address, isHardwareLogin: false } +export default createBatchActions(ID, { + balances: balancesActions, + claims: claimsActions, + transactions: transactionHistoryActions }) - -export const ledgerLoginActions = createRequestActions(ID, ({ publicKey, signingFunction }: LedgerLoginProps) => (state: Object): AccountType => { - const publicKeyEncoded = wallet.getPublicKeyEncoded(publicKey) - const account = new wallet.Account(publicKeyEncoded) - - return { address: account.address, publicKey, signingFunction, isHardwareLogin: true } -}) - -export const logoutActions = createRequestActions(ID, () => (state: Object): AccountType => { - return null -}) - -// TODO: Better way to expose action data than to make a faux function? One idea is to change -// `withData` to accept the `ID` exported from this file instead of a generated action. -export default createRequestActions(ID, () => (state: Object) => noop) diff --git a/app/actions/authActions.js b/app/actions/authActions.js new file mode 100644 index 000000000..36d7cb20c --- /dev/null +++ b/app/actions/authActions.js @@ -0,0 +1,69 @@ +// @flow +import { wallet } from 'neon-js' +import { noop } from 'lodash' + +import createRequestActions from '../util/api/createRequestActions' +import { upgradeNEP6AddAddresses } from '../core/account' +import { validatePassphraseLength } from '../core/wallet' + +type WifLoginProps = { + wif: string +} + +type LedgerLoginProps = { + publicKey: string, + signingFunction: Function +} + +type Nep2LoginProps = { + passphrase: string, + encryptedWIF: string +} + +type AccountType = ?{ + address: string, + wif?: string, + publicKey?: string, + signingFunction?: Function, + isHardwareLogin: boolean +} + +export const ID = 'AUTH' + +export const wifLoginActions = createRequestActions(ID, ({ wif }: WifLoginProps) => (state: Object): AccountType => { + const account = new wallet.Account(wif) + + return { wif, address: account.address, isHardwareLogin: false } +}) + +export const nep2LoginActions = createRequestActions(ID, ({ passphrase, encryptedWIF }: Nep2LoginProps) => async (state: Object): Promise => { + if (!validatePassphraseLength(passphrase)) { + throw new Error('Passphrase too short') + } + + if (!wallet.isNEP2(encryptedWIF)) { + throw new Error('That is not a valid encrypted key') + } + + const wif = wallet.decrypt(encryptedWIF, passphrase) + const account = new wallet.Account(wif) + + await upgradeNEP6AddAddresses(encryptedWIF, wif) + + return { wif, address: account.address, isHardwareLogin: false } +}) + +export const ledgerLoginActions = createRequestActions(ID, ({ publicKey, signingFunction }: LedgerLoginProps) => (state: Object): AccountType => { + const publicKeyEncoded = wallet.getPublicKeyEncoded(publicKey) + const account = new wallet.Account(publicKeyEncoded) + + return { address: account.address, publicKey, signingFunction, isHardwareLogin: true } +}) + +export const logoutActions = createRequestActions(ID, () => (state: Object): AccountType => { + return null +}) + +// TODO: Better way to expose action data than to make a faux function? One idea is to change +// `withData` to accept the `ID` exported from this file instead of a generated action. +export default createRequestActions(ID, () => (state: Object) => noop) diff --git a/app/actions/balancesActions.js b/app/actions/balancesActions.js new file mode 100644 index 000000000..cd147d0ce --- /dev/null +++ b/app/actions/balancesActions.js @@ -0,0 +1,47 @@ +// @flow +import { api } from 'neon-js' +import { extend } from 'lodash' + +import createRequestActions from '../util/api/createRequestActions' +import { toBigNumber } from '../core/math' +import { ASSETS } from '../core/constants' +import { COIN_DECIMAL_LENGTH } from '../core/formatters' + +type Props = { + net: string, + address: string, + tokens: Array +} + +export const ID = 'BALANCES' + +async function getBalances ({ net, address, tokens }: Props) { + const endpoint = await api.neonDB.getRPCEndpoint(net) + + // token balances + const promises = tokens.map(async (token) => { + const { scriptHash } = token + const response = await api.nep5.getToken(endpoint, scriptHash, address) + const balance = toBigNumber(response.balance || 0).round(response.decimals).toString() + + return { + [scriptHash]: { ...response, balance } + } + }) + + // asset balances + promises.push((async () => { + const assetBalances = await api.neonDB.getBalance(net, address) + + return { + [ASSETS.NEO]: toBigNumber(assetBalances.NEO.balance).toString(), + [ASSETS.GAS]: toBigNumber(assetBalances.GAS.balance).round(COIN_DECIMAL_LENGTH).toString() + } + })()) + + return extend({}, ...await Promise.all(promises)) +} + +export default createRequestActions(ID, ({ net, address, tokens }: Props = {}) => async (state: Object) => { + return getBalances({ net, address, tokens }) +}) diff --git a/app/actions/claimsActions.js b/app/actions/claimsActions.js new file mode 100644 index 000000000..dd9346af9 --- /dev/null +++ b/app/actions/claimsActions.js @@ -0,0 +1,25 @@ +// @flow +import { api } from 'neon-js' + +import createRequestActions from '../util/api/createRequestActions' +import { toBigNumber } from '../core/math' +import { COIN_DECIMAL_LENGTH } from '../core/formatters' + +type Props = { + net: string, + address: string +} + +const toDecimal = (intValue) => toBigNumber(intValue).div(10 ** COIN_DECIMAL_LENGTH).toString() + +export const ID = 'CLAIMS' + +export default createRequestActions(ID, ({ net, address }: Props = {}) => async (state: Object): Promise => { + const result = await api.neonDB.getClaims(net, address) + + return { + total: toDecimal(result.total_claim + result.total_unspent_claim), + available: toDecimal(result.total_claim), + unavailable: toDecimal(result.total_unspent_claim) + } +}) diff --git a/app/actions/transactionHistoryActions.js b/app/actions/transactionHistoryActions.js new file mode 100644 index 000000000..e8f03bfa0 --- /dev/null +++ b/app/actions/transactionHistoryActions.js @@ -0,0 +1,24 @@ +// @flow +import { api } from 'neon-js' + +import createRequestActions from '../util/api/createRequestActions' +import { toBigNumber } from '../core/math' +import { toFixedDecimals, COIN_DECIMAL_LENGTH } from '../core/formatters' +import { ASSETS } from '../core/constants' + +type Props = { + net: string, + address: string +} + +export const ID = 'TRANSACTION_HISTORY' + +export default createRequestActions(ID, ({ net, address }: Props = {}) => async (state: Object) => { + const transactions = await api.neonDB.getTransactionHistory(net, address) + + return transactions.map(({ NEO, GAS, txid }: TransactionHistoryType) => ({ + txid, + [ASSETS.NEO]: toFixedDecimals(NEO, 0), + [ASSETS.GAS]: toBigNumber(GAS).round(COIN_DECIMAL_LENGTH).toString() + })) +}) diff --git a/app/containers/App/Header/Logout/index.js b/app/containers/App/Header/Logout/index.js index e06352c3e..c16be9112 100644 --- a/app/containers/App/Header/Logout/index.js +++ b/app/containers/App/Header/Logout/index.js @@ -1,7 +1,7 @@ // @flow import Logout from './Logout' import withActions from '../../../../hocs/api/withActions' -import { logoutActions } from '../../../../actions/accountActions' +import { logoutActions } from '../../../../actions/authActions' type Props = { logout: Function diff --git a/app/containers/App/Header/index.js b/app/containers/App/Header/index.js index 022cf55ff..6eb677001 100644 --- a/app/containers/App/Header/index.js +++ b/app/containers/App/Header/index.js @@ -3,7 +3,7 @@ import { compose } from 'recompose' import Header from './Header' import withData from '../../../hocs/api/withData' -import withAccountData from '../../../hocs/withAccountData' +import withAuthData from '../../../hocs/withAuthData' import withCurrencyData from '../../../hocs/withCurrencyData' import pricesActions from '../../../actions/pricesActions' @@ -14,6 +14,6 @@ const mapPricesDataToProps = ({ NEO, GAS }) => ({ export default compose( withData(pricesActions, mapPricesDataToProps), - withAccountData(), + withAuthData(), withCurrencyData('currencyCode') )(Header) diff --git a/app/containers/App/index.js b/app/containers/App/index.js index 9e8d8e697..20fee1934 100644 --- a/app/containers/App/index.js +++ b/app/containers/App/index.js @@ -3,16 +3,18 @@ import { connect } from 'react-redux' import { bindActionCreators } from 'redux' import { compose } from 'recompose' -import withData from '../../hocs/api/withData' +import appActions from '../../actions/appActions' +import authActions from '../../actions/authActions' +import accountActions from '../../actions/accountActions' +import networkActions from '../../actions/networkActions' import withFetch from '../../hocs/api/withFetch' import withReload from '../../hocs/api/withReload' import withProgressComponents from '../../hocs/api/withProgressComponents' import withLoginRedirect from '../../hocs/auth/withLoginRedirect' import withLogoutRedirect from '../../hocs/auth/withLogoutRedirect' -import appActions from '../../actions/appActions' -import alreadyLoaded from '../../hocs/api/progressStrategies/alreadyLoadedStrategy' +import withLogoutReset from '../../hocs/auth/withLogoutReset' import withNetworkData from '../../hocs/withNetworkData' -import networkActions from '../../actions/networkActions' +import alreadyLoaded from '../../hocs/api/progressStrategies/alreadyLoadedStrategy' import { checkVersion } from '../../modules/metadata' import { showErrorNotification } from '../../modules/notifications' import { LOADING, FAILED } from '../../values/state' @@ -28,8 +30,6 @@ const actionCreators = { const mapDispatchToProps = dispatch => bindActionCreators(actionCreators, dispatch) -const mapAppDataToProps = ({ height, settings }) => ({ height, settings }) - export default compose( // Old way of fetching data, need to refactor this out... connect(null, mapDispatchToProps), @@ -47,7 +47,6 @@ export default compose( // Fetch application data based upon the selected network. Reload data when the network changes. withFetch(appActions), - withData(appActions, mapAppDataToProps), withReload(appActions, ['networkId']), withProgressComponents(appActions, { [LOADING]: Loading, @@ -58,5 +57,9 @@ export default compose( // Navigate to the home or dashboard when the user logs in or out. withLoginRedirect, - withLogoutRedirect + withLogoutRedirect, + + // Remove stale data from store on logout + withLogoutReset(authActions), + withLogoutReset(accountActions) )(App) diff --git a/app/containers/Claim/Claim.jsx b/app/containers/Claim/Claim.jsx index 9c9dd79d1..d8ea6ed8d 100644 --- a/app/containers/Claim/Claim.jsx +++ b/app/containers/Claim/Claim.jsx @@ -4,6 +4,7 @@ import React, { Component } from 'react' import Button from '../../components/Button' import Tooltip from '../../components/Tooltip' import { formatGAS } from '../../core/formatters' +import { toBigNumber } from '../../core/math' type Props = { doClaimNotify: Function, @@ -11,14 +12,13 @@ type Props = { doGasClaim: Function, claimRequest: boolean, disableClaimButton: boolean, - claimWasUpdated: boolean, - claimAmount: number, + claimAmount: string, } export default class Claim extends Component { - componentDidUpdate () { - const { claimRequest, claimWasUpdated, doClaimNotify, setClaimRequest } = this.props - if (claimRequest && claimWasUpdated) { + componentDidUpdate (prevProps: Props) { + const { claimRequest, doClaimNotify, setClaimRequest } = this.props + if (claimRequest && !prevProps.claimRequest) { setClaimRequest(false) doClaimNotify() } @@ -26,7 +26,7 @@ export default class Claim extends Component { render () { const { claimAmount, disableClaimButton, doGasClaim } = this.props - const shouldDisableButton = disableClaimButton || claimAmount === 0 + const shouldDisableButton = disableClaimButton || toBigNumber(claimAmount).eq(0) const formattedAmount = formatGAS(claimAmount) return (
@@ -34,7 +34,7 @@ export default class Claim extends Component { title='You can claim GAS once every 5 minutes' disabled={!disableClaimButton} > - diff --git a/app/containers/Claim/index.js b/app/containers/Claim/index.js index 8bfe89d19..3cdf26352 100644 --- a/app/containers/Claim/index.js +++ b/app/containers/Claim/index.js @@ -1,23 +1,21 @@ // @flow import { connect } from 'react-redux' import { bindActionCreators } from 'redux' +import { compose } from 'recompose' +import Claim from './Claim' +import claimsActions from '../../actions/claimsActions' +import withData from '../../hocs/api/withData' import { setClaimRequest, doGasClaim, doClaimNotify, - getClaimAmount, getClaimRequest, - getClaimWasUpdated, getDisableClaimButton } from '../../modules/claim' -import Claim from './Claim' - const mapStateToProps = (state: Object) => ({ - claimAmount: getClaimAmount(state), claimRequest: getClaimRequest(state), - claimWasUpdated: getClaimWasUpdated(state), disableClaimButton: getDisableClaimButton(state) }) @@ -29,4 +27,11 @@ const actionCreators = { const mapDispatchToProps = dispatch => bindActionCreators(actionCreators, dispatch) -export default connect(mapStateToProps, mapDispatchToProps)(Claim) +const mapClaimsDataToProps = (claims) => ({ + claimAmount: claims.total +}) + +export default compose( + connect(mapStateToProps, mapDispatchToProps), + withData(claimsActions, mapClaimsDataToProps) +)(Claim) diff --git a/app/containers/Dashboard/Dashboard.jsx b/app/containers/Dashboard/Dashboard.jsx index 35ca16ef7..5e0a5bfec 100644 --- a/app/containers/Dashboard/Dashboard.jsx +++ b/app/containers/Dashboard/Dashboard.jsx @@ -5,8 +5,6 @@ import classNames from 'classnames' import TransactionHistory from '../TransactionHistory' import WalletInfo from '../WalletInfo' -import Loader from '../../components/Loader' - import { log } from '../../util/Logs' import { MODAL_TYPES } from '../../core/constants' @@ -24,25 +22,26 @@ type Props = { NEO: string, GAS: string, tokenBalances: Array, - loaded: boolean, networkId: string, loadWalletData: Function, } const REFRESH_INTERVAL_MS = 30000 -let walletDataInterval export default class Dashboard extends Component { + walletDataInterval: ?number + componentDidMount () { const { loadWalletData, net, address } = this.props - // only logging public information here - log(net, 'LOGIN', address, {}) - loadWalletData() - walletDataInterval = setInterval(loadWalletData, REFRESH_INTERVAL_MS) + + log(net, 'LOGIN', address) // only logging public information here + this.walletDataInterval = setInterval(loadWalletData, REFRESH_INTERVAL_MS) } componentWillUnmount () { - clearInterval(walletDataInterval) + if (this.walletDataInterval) { + clearInterval(this.walletDataInterval) + } } componentWillReceiveProps (nextProps: Props) { @@ -59,14 +58,9 @@ export default class Dashboard extends Component { NEO, GAS, tokenBalances, - sendTransaction, - loaded + sendTransaction } = this.props - if (!loaded) { - return - } - return (
diff --git a/app/containers/Dashboard/index.js b/app/containers/Dashboard/index.js index 8dbcacd21..3ae7292e5 100644 --- a/app/containers/Dashboard/index.js +++ b/app/containers/Dashboard/index.js @@ -2,35 +2,59 @@ import { connect, type MapStateToProps } from 'react-redux' import { bindActionCreators } from 'redux' import { compose } from 'recompose' +import { omit } from 'lodash' +import Loader from '../../components/Loader' +import accountActions from '../../actions/accountActions' +import withData from '../../hocs/api/withData' +import withFetch from '../../hocs/api/withFetch' +import withReload from '../../hocs/api/withReload' +import withActions from '../../hocs/api/withActions' +import withProgressComponents from '../../hocs/api/withProgressComponents' import withNetworkData from '../../hocs/withNetworkData' -import withAccountData from '../../hocs/withAccountData' +import withAuthData from '../../hocs/withAuthData' +import withFilteredTokensData from '../../hocs/withFilteredTokensData' +import alreadyLoaded from '../../hocs/api/progressStrategies/alreadyLoadedStrategy' import { getNotifications } from '../../modules/notifications' -import { getNEO, getGAS, getTokenBalances, getIsLoaded, loadWalletData } from '../../modules/wallet' import { showModal } from '../../modules/modal' import { sendTransaction } from '../../modules/transactions' +import { LOADING } from '../../values/state' import Dashboard from './Dashboard' const mapStateToProps: MapStateToProps<*, *, *> = (state: Object) => ({ - notification: getNotifications(state), - NEO: getNEO(state), - GAS: getGAS(state), - tokenBalances: getTokenBalances(state), - loaded: getIsLoaded(state) + notification: getNotifications(state) +}) + +const mapBalanceDataToProps = (balances) => ({ + NEO: balances.NEO, + GAS: balances.GAS, + tokenBalances: omit(balances, 'NEO', 'GAS') }) const actionCreators = { showModal, - sendTransaction, - loadWalletData + sendTransaction } -const mapDispatchToProps = dispatch => - bindActionCreators(actionCreators, dispatch) +const mapDispatchToProps = dispatch => bindActionCreators(actionCreators, dispatch) + +const mapAccountActionsToProps = (actions, props) => ({ + loadWalletData: () => actions.request({ net: props.net, address: props.address, tokens: props.tokens }) +}) export default compose( connect(mapStateToProps, mapDispatchToProps), withNetworkData(), - withAccountData() + withAuthData(), + withFilteredTokensData(), + withFetch(accountActions), + withProgressComponents(accountActions, { + [LOADING]: Loader + }, { + strategy: alreadyLoaded + }), + withData(accountActions, mapBalanceDataToProps), + withReload(accountActions, ['networkId']), + withActions(accountActions, mapAccountActionsToProps) )(Dashboard) diff --git a/app/containers/LoginLedgerNanoS/index.js b/app/containers/LoginLedgerNanoS/index.js index 93efeb9a5..8bd2bf2c4 100644 --- a/app/containers/LoginLedgerNanoS/index.js +++ b/app/containers/LoginLedgerNanoS/index.js @@ -7,7 +7,7 @@ import withError from '../../hocs/api/withError' import withFetch from '../../hocs/api/withFetch' import withActions from '../../hocs/api/withActions' import ledgerActions from '../../actions/ledgerActions' -import { ledgerLoginActions } from '../../actions/accountActions' +import { ledgerLoginActions } from '../../actions/authActions' const mapLedgerActionsToProps = (actions) => ({ connect: () => ledgerActions.request() diff --git a/app/containers/LoginLocalStorage/index.js b/app/containers/LoginLocalStorage/index.js index 869421f92..87554151b 100644 --- a/app/containers/LoginLocalStorage/index.js +++ b/app/containers/LoginLocalStorage/index.js @@ -5,7 +5,7 @@ import withData from '../../hocs/api/withData' import withActions from '../../hocs/api/withActions' import withFailureNotification from '../../hocs/withFailureNotification' import accountsActions from '../../actions/accountsActions' -import { nep2LoginActions } from '../../actions/accountActions' +import { nep2LoginActions } from '../../actions/authActions' import LoginLocalStorage from './LoginLocalStorage' diff --git a/app/containers/LoginNep2/index.js b/app/containers/LoginNep2/index.js index 28a3158be..979d952e1 100644 --- a/app/containers/LoginNep2/index.js +++ b/app/containers/LoginNep2/index.js @@ -4,7 +4,7 @@ import { compose } from 'recompose' import LoginNep2 from './LoginNep2' import withActions from '../../hocs/api/withActions' import withFailureNotification from '../../hocs/withFailureNotification' -import { nep2LoginActions } from '../../actions/accountActions' +import { nep2LoginActions } from '../../actions/authActions' const mapActionsToProps = (actions) => ({ loginNep2: (passphrase, encryptedWIF) => actions.request({ passphrase, encryptedWIF }) diff --git a/app/containers/LoginPrivateKey/index.js b/app/containers/LoginPrivateKey/index.js index 4aba62535..9066dc6c2 100644 --- a/app/containers/LoginPrivateKey/index.js +++ b/app/containers/LoginPrivateKey/index.js @@ -4,7 +4,7 @@ import { compose } from 'recompose' import LoginPrivateKey from './LoginPrivateKey' import withActions from '../../hocs/api/withActions' import withFailureNotification from '../../hocs/withFailureNotification' -import { wifLoginActions } from '../../actions/accountActions' +import { wifLoginActions } from '../../actions/authActions' const mapActionsToProps = (actions) => ({ loginWithPrivateKey: (wif) => actions.request({ wif }) diff --git a/app/containers/TransactionHistory/TransactionHistory.jsx b/app/containers/TransactionHistory/TransactionHistory.jsx index 2428acc7b..5565ec05b 100644 --- a/app/containers/TransactionHistory/TransactionHistory.jsx +++ b/app/containers/TransactionHistory/TransactionHistory.jsx @@ -8,29 +8,23 @@ import Transactions from './Transactions' import styles from './TransactionHistory.scss' type Props = { - address: string, - net: NetworkType, transactions: Array, - syncTransactionHistory: Function, - isLoadingTransactions: boolean + loading: boolean } export default class TransactionHistory extends Component { - componentDidMount () { - const { net, address, syncTransactionHistory } = this.props - syncTransactionHistory(net, address) - } - render () { - const { transactions, isLoadingTransactions } = this.props + const { transactions, loading } = this.props return (
- Transaction History {isLoadingTransactions && } + Transaction History {loading && }
- + {!loading && ( + + )}
) } diff --git a/app/containers/TransactionHistory/index.js b/app/containers/TransactionHistory/index.js index e0386af08..7359dfa64 100644 --- a/app/containers/TransactionHistory/index.js +++ b/app/containers/TransactionHistory/index.js @@ -1,27 +1,30 @@ // @flow -import { connect, type MapStateToProps } from 'react-redux' -import { bindActionCreators } from 'redux' -import { compose } from 'recompose' +import { compose, mapProps } from 'recompose' +import { omit } from 'lodash' -import withNetworkData from '../../hocs/withNetworkData' -import withAccountData from '../../hocs/withAccountData' -import { syncTransactionHistory, getIsLoadingTransactions } from '../../modules/transactions' -import { getTransactions } from '../../modules/wallet' import TransactionHistory from './TransactionHistory' +import transactionHistoryActions from '../../actions/transactionHistoryActions' +import withFetch from '../../hocs/api/withFetch' +import withData from '../../hocs/api/withData' +import withProgressProp from '../../hocs/api/withProgressProp' +import withNetworkData from '../../hocs/withNetworkData' +import withAuthData from '../../hocs/withAuthData' +import { LOADING } from '../../values/state' -const mapStateToProps: MapStateToProps<*, *, *> = (state: Object) => ({ - transactions: getTransactions(state), - isLoadingTransactions: getIsLoadingTransactions(state) +const mapTransactionsDataToProps = (transactions) => ({ + transactions }) -const actionCreators = { - syncTransactionHistory -} - -const mapDispatchToProps = dispatch => bindActionCreators(actionCreators, dispatch) +const mapLoadingProp = (props) => ({ + ...omit(props, 'progress'), + loading: props.progress === LOADING +}) export default compose( - connect(mapStateToProps, mapDispatchToProps), withNetworkData(), - withAccountData() + withAuthData(), + withFetch(transactionHistoryActions), + withData(transactionHistoryActions, mapTransactionsDataToProps), + withProgressProp(transactionHistoryActions, { propName: 'progress' }), + mapProps(mapLoadingProp) )(TransactionHistory) diff --git a/app/containers/WalletInfo/WalletInfo.jsx b/app/containers/WalletInfo/WalletInfo.jsx index 826dc4b62..caaa0c639 100644 --- a/app/containers/WalletInfo/WalletInfo.jsx +++ b/app/containers/WalletInfo/WalletInfo.jsx @@ -27,8 +27,6 @@ type Props = { tokenBalances: Array, loadWalletData: Function, currencyCode: string, - showSuccessNotification: Function, - showErrorNotification: Function, showModal: Function, oldParticipateInSale: Function, participateInSale: Function, @@ -39,25 +37,6 @@ type Props = { } export default class WalletInfo extends Component { - refreshBalance = () => { - const { - showSuccessNotification, - showErrorNotification, - loadWalletData - } = this.props - loadWalletData() - .then(response => { - showSuccessNotification({ - message: 'Received latest blockchain information.' - }) - }) - .catch(e => { - showErrorNotification({ - message: 'Failed to retrieve blockchain information' - }) - }) - } - render () { const { address, @@ -117,7 +96,7 @@ export default class WalletInfo extends Component { {formatFiat(totalValue)} {displayCurrencyCode}
{ networks, networkId, setUserGeneratedTokens, - onSave: () => { - loadWalletData(false) - } + onSave: loadWalletData }) }, oldParticipateInSale, diff --git a/app/containers/WalletInfo/index.js b/app/containers/WalletInfo/index.js index f579507ee..fd23423dc 100644 --- a/app/containers/WalletInfo/index.js +++ b/app/containers/WalletInfo/index.js @@ -2,38 +2,42 @@ import { connect, type MapStateToProps } from 'react-redux' import { bindActionCreators } from 'redux' import { compose } from 'recompose' +import { values, omit } from 'lodash' +import accountActions from '../../actions/accountActions' import pricesActions from '../../actions/pricesActions' +import balancesActions from '../../actions/balancesActions' import withData from '../../hocs/api/withData' import withActions from '../../hocs/api/withActions' import withNetworkData from '../../hocs/withNetworkData' -import withAccountData from '../../hocs/withAccountData' +import withAuthData from '../../hocs/withAuthData' import withCurrencyData from '../../hocs/withCurrencyData' +import withFilteredTokensData from '../../hocs/withFilteredTokensData' +import withSuccessNotification from '../../hocs/withSuccessNotification' +import withFailureNotification from '../../hocs/withFailureNotification' import { updateSettingsActions } from '../../actions/settingsActions' import { getNetworks } from '../../core/networks' -import { showErrorNotification, showSuccessNotification } from '../../modules/notifications' -import { loadWalletData, getNEO, getGAS, getTokenBalances } from '../../modules/wallet' import { showModal } from '../../modules/modal' import { participateInSale, oldParticipateInSale } from '../../modules/sale' import WalletInfo from './WalletInfo' const mapStateToProps: MapStateToProps<*, *, *> = (state: Object) => ({ - NEO: getNEO(state), - GAS: getGAS(state), - tokenBalances: getTokenBalances(state), networks: getNetworks() }) +const mapBalanceDataToProps = (balances) => ({ + NEO: balances.NEO, + GAS: balances.GAS, + tokenBalances: values(omit(balances, 'NEO', 'GAS')) +}) + const mapPricesDataToProps = ({ NEO, GAS }) => ({ neoPrice: NEO, gasPrice: GAS }) const actionCreators = { - loadWalletData, - showErrorNotification, - showSuccessNotification, showModal, participateInSale, oldParticipateInSale @@ -41,15 +45,24 @@ const actionCreators = { const mapDispatchToProps = dispatch => bindActionCreators(actionCreators, dispatch) -const mapActionsToProps = (actions) => ({ +const mapSettingsActionsToProps = (actions) => ({ setUserGeneratedTokens: (tokens) => actions.request({ tokens }) }) +const mapAccountActionsToProps = (actions, props) => ({ + loadWalletData: () => actions.request({ net: props.net, address: props.address, tokens: props.tokens }) +}) + export default compose( connect(mapStateToProps, mapDispatchToProps), withData(pricesActions, mapPricesDataToProps), + withData(balancesActions, mapBalanceDataToProps), withNetworkData(), - withAccountData(), + withAuthData(), withCurrencyData('currencyCode'), - withActions(updateSettingsActions, mapActionsToProps) + withFilteredTokensData(), + withActions(updateSettingsActions, mapSettingsActionsToProps), + withActions(accountActions, mapAccountActionsToProps), + withSuccessNotification(accountActions, 'Received latest blockchain information.'), + withFailureNotification(accountActions, 'Failed to retrieve blockchain information.') )(WalletInfo) diff --git a/app/core/deprecated.js b/app/core/deprecated.js index 13005ee47..589ad9e75 100644 --- a/app/core/deprecated.js +++ b/app/core/deprecated.js @@ -1,12 +1,11 @@ // @flow -import { get } from 'lodash' +import { get, omit } from 'lodash' -import blockHeightActions from '../actions/blockHeightActions' -import pricesActions from '../actions/pricesActions' -import { ID as ACCOUNT_ID } from '../actions/accountActions' +import { ID as AUTH_ID } from '../actions/authActions' +import { ID as BALANCES_ID } from '../actions/balancesActions' import { ID as NETWORK_ID } from '../actions/networkActions' -import { ID as SETTINGS_ID } from '../actions/settingsActions' -import { getNetworks } from '../core/networks' +import { ASSETS } from '../core/constants' +import { findNetwork } from '../core/networks' const PREFIX = 'api' @@ -20,53 +19,45 @@ export const getNetworkId = (state: Object) => { } export const getNetwork = (state: Object) => { - const selectedNetworkId = getNetworkId(state) - const networks = getNetworks() - const networkItem = networks.find(({ id, value }) => id === selectedNetworkId) || networks[0] - - return networkItem.network + return getNetworkById(getNetworkId(state)) } export const getNetworkById = (networkId: string) => { - const networks = getNetworks() - const networkItem = networks.find(({ id, value }) => id === networkId) || networks[0] - return networkItem.network + return findNetwork(networkId).network } export const getAddress = (state: Object) => { - return get(state, `${PREFIX}.${ACCOUNT_ID}.data.address`) + return get(state, `${PREFIX}.${AUTH_ID}.data.address`) } export const getWIF = (state: Object) => { - return get(state, `${PREFIX}.${ACCOUNT_ID}.data.wif`) + return get(state, `${PREFIX}.${AUTH_ID}.data.wif`) } export const getSigningFunction = (state: Object) => { - return get(state, `${PREFIX}.${ACCOUNT_ID}.data.signingFunction`) + return get(state, `${PREFIX}.${AUTH_ID}.data.signingFunction`) } export const getPublicKey = (state: Object) => { - return get(state, `${PREFIX}.${ACCOUNT_ID}.data.publicKey`) + return get(state, `${PREFIX}.${AUTH_ID}.data.publicKey`) } export const getIsHardwareLogin = (state: Object) => { - return get(state, `${PREFIX}.${ACCOUNT_ID}.data.isHardwareLogin`) + return get(state, `${PREFIX}.${AUTH_ID}.data.isHardwareLogin`) } -export const getCurrency = (state: Object) => { - return get(state, `${PREFIX}.${SETTINGS_ID}.data.currency`) +export const getNEO = (state: Object): string => { + return getBalances(state)[ASSETS.NEO] } -export const getTokensForNetwork = (state: Object) => { - const selectedNetworkId = getNetworkId(state) - const allTokens = get(state, `${PREFIX}.${SETTINGS_ID}.data.tokens`) - return allTokens.filter(({ networkId }) => networkId === selectedNetworkId) +export const getGAS = (state: Object): string => { + return getBalances(state)[ASSETS.GAS] } -export const syncBlockHeight = (state: Object) => { - return blockHeightActions.request({ networkId: getNetworkId(state) }) +export const getTokenBalances = (state: Object): Object => { + return omit(getBalances(state), ASSETS.NEO, ASSETS.GAS) } -export const getMarketPrices = (state: Object) => { - return pricesActions.request({ currency: getCurrency(state) }) +export const getBalances = (state: Object) => { + return get(state, `${PREFIX}.${BALANCES_ID}.data`) } diff --git a/app/core/networks.js b/app/core/networks.js index 3c499bd55..ea3813274 100644 --- a/app/core/networks.js +++ b/app/core/networks.js @@ -16,3 +16,8 @@ export const getNetworks = () => ([ network: 'TestNet' } ]) + +export const findNetwork = (networkId: string): NetworkItemType => { + const networks = getNetworks() + return networks.find(({ id }) => id === networkId) || networks[0] +} diff --git a/app/core/wallet.js b/app/core/wallet.js index b26294f13..d90080c2f 100644 --- a/app/core/wallet.js +++ b/app/core/wallet.js @@ -1,5 +1,6 @@ // @flow import { wallet } from 'neon-js' +import { extend } from 'lodash' import { ASSETS } from './constants' import { toBigNumber } from './math' @@ -16,23 +17,9 @@ export const obtainBalance = (balances: Object, symbol: SymbolType) => { return balances[symbol] || 0 } -export const getTokenBalancesMap = (tokenBalances: Array) => - tokenBalances.reduce( - (tokenBalance, { symbol, balance }: TokenBalanceType) => { - tokenBalance[symbol] = balance - return tokenBalance - }, - {} - ) - -export const getTokenScriptHashMap = (tokenBalances: Array) => - tokenBalances.reduce( - (tokenBalance, { symbol, scriptHash }: TokenBalanceType) => { - tokenBalance[symbol] = scriptHash - return tokenBalance - }, - {} - ) +export const getTokenBalancesMap = (tokenBalances: Array) => { + return extend({}, ...tokenBalances.map(({ symbol, balance }) => ({ [symbol]: balance }))) +} export const validateTransactionBeforeSending = ( balance: number, diff --git a/app/hocs/api/withProgressProp.js b/app/hocs/api/withProgressProp.js index 312ec5066..149579b50 100644 --- a/app/hocs/api/withProgressProp.js +++ b/app/hocs/api/withProgressProp.js @@ -5,7 +5,7 @@ import { compose, setDisplayName, wrapDisplayName } from 'recompose' import defaultStrategy from './progressStrategies/defaultStrategy' import { type Actions, type ActionState } from '../../values/api' -import { type ProgressState } from '../../values/state' +import { INITIAL, type ProgressState } from '../../values/state' const PROGRESS_PROP: string = '__progress__' @@ -28,7 +28,7 @@ export default function withProgressProp ( if (!actionState) { return [] } else if (actionState.batch) { - return map(actionState.mapping, (key) => get(state, `${prefix}.${key}`)) + return map(actionState.mapping, (key) => get(state, `${prefix}.${key}`, INITIAL)) } else { return castArray(actionState) } diff --git a/app/hocs/api/withReload.js b/app/hocs/api/withReload.js index 2c40d9471..90d3f94a2 100644 --- a/app/hocs/api/withReload.js +++ b/app/hocs/api/withReload.js @@ -1,57 +1,7 @@ // @flow -import React from 'react' -import { compose, setDisplayName, wrapDisplayName } from 'recompose' -import { isEqual, some, castArray } from 'lodash' - -import withActions from './withActions' -import withoutProps from '../withoutProps' import { type Actions } from '../../values/api' +import withResponsiveAction, { type ShouldPerform } from './withResponsiveAction' -type Props = { - [key: string]: Function -} - -type ShouldReload = Array | string | Function - -function createShouldReload (shouldReloadDefinition: ShouldReload) { - if (typeof shouldReloadDefinition === 'function') { - return shouldReloadDefinition - } else { - const shouldReloadProps = castArray(shouldReloadDefinition) - - return (prevProps: Object, props: Object): boolean => { - return some(shouldReloadProps, (key) => !isEqual(prevProps[key], props[key])) - } - } -} - -export default function withReload (actions: Actions, shouldReloadDefinition: ShouldReload, propName: string = 'performAction'): Function { - const shouldReload = createShouldReload(shouldReloadDefinition) - - const mapActionsToProps = (actions: Actions, props: Object): Object => ({ - [propName]: (...args: Array) => actions.request(...args) - }) - - return (Component: Class>) => { - const WrappedComponent = withoutProps(propName)(Component) - - class ComponentWithReload extends React.Component { - static displayName = 'ComponentWithFetch' - - componentDidUpdate = (prevProps) => { - if (shouldReload(prevProps, this.props)) { - this.props[propName](this.props) - } - } - - render = () => { - return - } - } - - return compose( - withActions(actions, mapActionsToProps), - setDisplayName(wrapDisplayName(Component, 'withReload')) - )(ComponentWithReload) - } +export default function withReload (actions: Actions, shouldReload: ShouldPerform, options: Object = {}): Function { + return withResponsiveAction(actions, 'request', shouldReload, options) } diff --git a/app/hocs/api/withReset.js b/app/hocs/api/withReset.js new file mode 100644 index 000000000..974b2f0d0 --- /dev/null +++ b/app/hocs/api/withReset.js @@ -0,0 +1,7 @@ +// @flow +import { type Actions } from '../../values/api' +import withResponsiveAction, { type ShouldPerform } from './withResponsiveAction' + +export default function withReset (actions: Actions, shouldReload: ShouldPerform, options: Object = {}): Function { + return withResponsiveAction(actions, 'reset', shouldReload, options) +} diff --git a/app/hocs/api/withResponsiveAction.js b/app/hocs/api/withResponsiveAction.js new file mode 100644 index 000000000..3a14bfa87 --- /dev/null +++ b/app/hocs/api/withResponsiveAction.js @@ -0,0 +1,65 @@ +// @flow +import React from 'react' +import { wrapDisplayName } from 'recompose' +import { isEqual, some, castArray } from 'lodash' + +import withActions from './withActions' +import withoutProps from '../withoutProps' +import { type Actions } from '../../values/api' + +type Props = { + [key: string]: Function +} + +type ActionName = 'request' | 'retry' | 'cancel' | 'reset' + +type Options = { + propName?: string +} + +export type ShouldPerform = Array | string | Function + +function createShouldPerform (shouldPerformDefinition: ShouldPerform) { + if (typeof shouldPerformDefinition === 'function') { + return shouldPerformDefinition + } else { + const shouldPerformProps = castArray(shouldPerformDefinition) + + return (prevProps: Object, props: Object): boolean => { + return some(shouldPerformProps, (key) => !isEqual(prevProps[key], props[key])) + } + } +} + +export default function withResponsiveAction ( + actions: Actions, + actionName: ActionName, + shouldPerformDefinition: ShouldPerform, + { propName = 'performAction' }: Options = {} +): Function { + const shouldPerform = createShouldPerform(shouldPerformDefinition) + + const mapActionsToProps = (actions: Actions, props: Object): Object => ({ + [propName]: (...args: Array) => actions[actionName](...args) + }) + + return (Component: Class>) => { + const WrappedComponent = withoutProps(propName)(Component) + + class ComponentWithResponsiveAction extends React.Component { + static displayName = wrapDisplayName(Component, 'withResponsiveAction') + + componentDidUpdate = (prevProps) => { + if (shouldPerform(prevProps, this.props)) { + this.props[propName](this.props) + } + } + + render = () => { + return + } + } + + return withActions(actions, mapActionsToProps)(ComponentWithResponsiveAction) + } +} diff --git a/app/hocs/auth/withLoginRedirect.js b/app/hocs/auth/withLoginRedirect.js index 99d994f5c..c88994943 100644 --- a/app/hocs/auth/withLoginRedirect.js +++ b/app/hocs/auth/withLoginRedirect.js @@ -1,9 +1,6 @@ // @flow -import { wallet } from 'neon-js' - import withRedirect from './withRedirect' +import didLogin from '../helpers/didLogin' import { ROUTES } from '../../core/constants' -export default withRedirect(ROUTES.DASHBOARD, (oldAddress, newAddress) => { - return !oldAddress && wallet.isAddress(newAddress) -}) +export default withRedirect(ROUTES.DASHBOARD, didLogin) diff --git a/app/hocs/auth/withLogoutRedirect.js b/app/hocs/auth/withLogoutRedirect.js index 0cda88c6d..e1b6aaa03 100644 --- a/app/hocs/auth/withLogoutRedirect.js +++ b/app/hocs/auth/withLogoutRedirect.js @@ -1,9 +1,6 @@ // @flow -import { wallet } from 'neon-js' - import withRedirect from './withRedirect' +import didLogout from '../helpers/didLogout' import { ROUTES } from '../../core/constants' -export default withRedirect(ROUTES.HOME, (oldAddress, newAddress) => { - return wallet.isAddress(oldAddress) && !newAddress -}) +export default withRedirect(ROUTES.HOME, didLogout) diff --git a/app/hocs/auth/withLogoutReset.js b/app/hocs/auth/withLogoutReset.js new file mode 100644 index 000000000..267851862 --- /dev/null +++ b/app/hocs/auth/withLogoutReset.js @@ -0,0 +1,27 @@ +// @flow +import { compose } from 'recompose' + +import withData from '../api/withData' +import withReset from '../api/withReset' +import withoutProps from '../withoutProps' +import didLogout from '../helpers/didLogout' +import authActions from '../../actions/authActions' +import { type Actions } from '../../values/api' + +type Options = { + propName?: string +} + +export default function withLogoutReset (actions: Actions, { propName = '__address__' }: Options = {}) { + const mapAuthDataToProps = (account) => ({ + [propName]: account && account.address + }) + + const shouldReset = (oldProps, newProps) => didLogout(oldProps[propName], newProps[propName]) + + return compose( + withData(authActions, mapAuthDataToProps), + withReset(actions, shouldReset), + withoutProps(propName) + ) +} diff --git a/app/hocs/auth/withRedirect.js b/app/hocs/auth/withRedirect.js index 2f12ff7d6..3fae7cef4 100644 --- a/app/hocs/auth/withRedirect.js +++ b/app/hocs/auth/withRedirect.js @@ -5,7 +5,7 @@ import { compose } from 'recompose' import { withRouter } from 'react-router-dom' import withData from '../api/withData' -import accountActions from '../../actions/accountActions' +import authActions from '../../actions/authActions' type Props = { [key: string]: string, @@ -23,7 +23,7 @@ export default function withRedirect ( strategy: Function, { propName = '__address__' }: Options = {} ) { - const mapAccountDataToProps = (account) => ({ + const mapAuthDataToProps = (account) => ({ [propName]: account && account.address }) @@ -43,7 +43,7 @@ export default function withRedirect ( return compose( withRouter, - withData(accountActions, mapAccountDataToProps) + withData(authActions, mapAuthDataToProps) )(WrappedComponent) } } diff --git a/app/hocs/helpers/didLogin.js b/app/hocs/helpers/didLogin.js new file mode 100644 index 000000000..7467e20f8 --- /dev/null +++ b/app/hocs/helpers/didLogin.js @@ -0,0 +1,6 @@ +// @flow +import { wallet } from 'neon-js' + +export default function didLogin (oldAddress: ?string, newAddress: ?string) { + return !oldAddress && wallet.isAddress(newAddress) +} diff --git a/app/hocs/helpers/didLogout.js b/app/hocs/helpers/didLogout.js new file mode 100644 index 000000000..9765bfb1d --- /dev/null +++ b/app/hocs/helpers/didLogout.js @@ -0,0 +1,6 @@ +// @flow +import { wallet } from 'neon-js' + +export default function didLogout (oldAddress: ?string, newAddress: ?string) { + return wallet.isAddress(oldAddress) && !newAddress +} diff --git a/app/hocs/withAccountData.js b/app/hocs/withAuthData.js similarity index 58% rename from app/hocs/withAccountData.js rename to app/hocs/withAuthData.js index 24eacf9eb..86dea4d17 100644 --- a/app/hocs/withAccountData.js +++ b/app/hocs/withAuthData.js @@ -2,7 +2,7 @@ import { isEmpty, zipObject, mapValues, invert } from 'lodash' import withData from './api/withData' -import accountActions from '../actions/accountActions' +import authActions from '../actions/authActions' type Mapping = { address?: string, @@ -14,10 +14,10 @@ type Mapping = { const keys: Array = ['address', 'publicKey', 'wif', 'signingFunction'] const defaultMapping: Mapping = zipObject(keys, keys) -export default function withAccountData (mapping: Mapping = defaultMapping) { - return withData(accountActions, (account) => { - if (!isEmpty(account)) { - return mapValues(invert(mapping), (field) => account[field]) +export default function withAuthData (mapping: Mapping = defaultMapping) { + return withData(authActions, (auth) => { + if (!isEmpty(auth)) { + return mapValues(invert(mapping), (field) => auth[field]) } else { return {} } diff --git a/app/hocs/withFailureNotification.js b/app/hocs/withFailureNotification.js index fb227568b..50ef1463c 100644 --- a/app/hocs/withFailureNotification.js +++ b/app/hocs/withFailureNotification.js @@ -2,7 +2,7 @@ import React from 'react' import { connect } from 'react-redux' import { compose } from 'recompose' -import { omit } from 'lodash' +import { omit, isFunction } from 'lodash' import withError from './api/withError' import withProgressProp from './api/withProgressProp' @@ -16,12 +16,18 @@ type Props = { __showErrorNotification__: Function } +type Message = string | Function + const ERROR_PROP = '__error__' const PROGRESS_PROP = '__progress__' const NOTIFICATION_PROP = '__showErrorNotification__' -export default function withFailureNotifications (actions: Actions) { - const mapErrorToProps = (error) => ({ [ERROR_PROP]: error }) +const defaultMessage = (error) => error + +export default function withFailureNotification (actions: Actions, message: Message = defaultMessage) { + const mapErrorToProps = (error: Error) => ({ + [ERROR_PROP]: isFunction(message) ? message(error) : message + }) const mapDisptchToProps = (dispatch, ownProps) => ({ [NOTIFICATION_PROP]: (...args) => dispatch(showErrorNotification(...args)) diff --git a/app/hocs/withSuccessNotification.js b/app/hocs/withSuccessNotification.js new file mode 100644 index 000000000..7faaeabfa --- /dev/null +++ b/app/hocs/withSuccessNotification.js @@ -0,0 +1,51 @@ +// @flow +import React from 'react' +import { connect } from 'react-redux' +import { compose } from 'recompose' +import { omit, isFunction } from 'lodash' + +import withProgressProp from './api/withProgressProp' +import { type Actions } from '../values/api' +import { LOADED, type ProgressState } from '../values/state' +import { showSuccessNotification } from '../modules/notifications' + +type Props = { + __progress__: ProgressState, + __showSuccessNotification__: Function +} + +type Message = string | Function + +const PROGRESS_PROP = '__progress__' +const NOTIFICATION_PROP = '__showSuccessNotification__' + +export default function withSuccessNotification (actions: Actions, message: Message) { + const mapDisptchToProps = (dispatch, ownProps) => ({ + [NOTIFICATION_PROP]: (...args) => dispatch(showSuccessNotification(...args)) + }) + + return (Component: Class>): Class> => { + const progressChangedToLoaded = (prevProps: Props, nextProps: Props) => { + return prevProps[PROGRESS_PROP] !== LOADED && nextProps[PROGRESS_PROP] === LOADED + } + + class LoadedNotifier extends React.Component { + componentWillReceiveProps (nextProps) { + if (progressChangedToLoaded(this.props, nextProps)) { + const showSuccessNotification = nextProps[NOTIFICATION_PROP] + showSuccessNotification({ message: isFunction(message) ? message(nextProps) : message }) + } + } + + render () { + const passDownProps = omit(this.props, PROGRESS_PROP, NOTIFICATION_PROP) + return + } + } + + return compose( + connect(null, mapDisptchToProps), + withProgressProp(actions, { propName: PROGRESS_PROP }) + )(LoadedNotifier) + } +} diff --git a/app/modules/claim.js b/app/modules/claim.js index 859d6601d..c19e403bd 100644 --- a/app/modules/claim.js +++ b/app/modules/claim.js @@ -1,7 +1,6 @@ // @flow import { api } from 'neon-js' -import { getNEO } from './wallet' import { showErrorNotification, showSuccessNotification, @@ -13,32 +12,21 @@ import { getAddress, getSigningFunction, getPublicKey, - getIsHardwareLogin + getIsHardwareLogin, + getNEO } from '../core/deprecated' import { ASSETS } from '../core/constants' import asyncWrap from '../core/asyncHelper' import { FIVE_MINUTES_MS } from '../core/time' import { log } from '../util/Logs' -import { toBigNumber, toNumber } from '../core/math' -import { COIN_DECIMAL_LENGTH } from '../core/formatters' +import { toNumber } from '../core/math' // Constants -export const SET_CLAIM = 'SET_CLAIM' export const SET_CLAIM_REQUEST = 'SET_CLAIM_REQUEST' export const DISABLE_CLAIM = 'DISABLE_CLAIM' // Actions -export function setClaim (claimAvailable: number, claimUnavailable: number) { - return { - type: SET_CLAIM, - payload: { - claimAvailable, - claimUnavailable - } - } -} - export function setClaimRequest (claimRequest: boolean) { return { type: SET_CLAIM_REQUEST, @@ -53,15 +41,6 @@ export function disableClaim (disableClaimButton: boolean) { } } -export const syncAvailableClaim = (net: NetworkType, address: string) => async ( - dispatch: DispatchType -) => { - const [_err, result] = await asyncWrap(api.neonDB.getClaims(net, address)) // eslint-disable-line - const available = parseInt(result.total_claim) - const unavailable = parseInt(result.total_unspent_claim) - return dispatch(setClaim(available, unavailable)) -} - export const doClaimNotify = () => async ( dispatch: DispatchType, getState: GetStateType @@ -174,21 +153,10 @@ export const doGasClaim = () => async ( // State Getters export const getClaimRequest = (state: Object) => state.claim.claimRequest -export const getClaimAmount = (state: Object) => state.claim.claimAmount -export const getClaimAvailable = (state: Object) => state.claim.claimAvailable -export const getClaimUnavailable = (state: Object) => - state.claim.claimUnavailable -export const getClaimWasUpdated = (state: Object) => - state.claim.claimWasUpdated -export const getDisableClaimButton = (state: Object) => - state.claim.disableClaimButton +export const getDisableClaimButton = (state: Object) => state.claim.disableClaimButton const initialState = { claimRequest: false, - claimAmount: 0, - claimAvailable: 0, - claimUnavailable: 0, - claimWasUpdated: false, disableClaimButton: false } @@ -200,22 +168,6 @@ export default (state: Object = initialState, action: ReduxAction) => { ...state, claimRequest } - case SET_CLAIM: - const { claimAvailable, claimUnavailable } = action.payload - let claimWasUpdated = false - if ( - claimAvailable > state.claimAvailable && - state.claimRequest === true - ) { - claimWasUpdated = true - } - return { - ...state, - claimAmount: toBigNumber(claimAvailable + claimUnavailable).div(10 ** COIN_DECIMAL_LENGTH).toNumber(), - claimAvailable, - claimUnavailable, - claimWasUpdated - } case DISABLE_CLAIM: const { disableClaimButton } = action.payload return { diff --git a/app/modules/index.js b/app/modules/index.js index a62257730..44dd27af6 100644 --- a/app/modules/index.js +++ b/app/modules/index.js @@ -2,8 +2,6 @@ import { combineReducers } from 'redux' import api from './api' import generateWallet from './generateWallet' -import transactions from './transactions' -import wallet from './wallet' import claim from './claim' import notifications from './notifications' import modal from './modal' @@ -12,8 +10,6 @@ import addressBook from './addressBook' export default combineReducers({ api, generateWallet, - wallet, - transactions, claim, notifications, modal, diff --git a/app/modules/sale.js b/app/modules/sale.js index 550d32bc7..9d592238d 100644 --- a/app/modules/sale.js +++ b/app/modules/sale.js @@ -9,9 +9,10 @@ import { getAddress, getIsHardwareLogin, getSigningFunction, - getPublicKey + getPublicKey, + getNEO, + getGAS } from '../core/deprecated' -import { getNEO, getGAS } from './wallet' import { toNumber } from '../core/math' import asyncWrap from '../core/asyncHelper' import { ASSETS } from '../core/constants' diff --git a/app/modules/transactions.js b/app/modules/transactions.js index c187ff08e..1a04bd223 100644 --- a/app/modules/transactions.js +++ b/app/modules/transactions.js @@ -3,7 +3,6 @@ import { api, sc, u, wallet } from 'neon-js' import { flatMap, keyBy } from 'lodash' -import { setTransactionHistory, getBalances, getTokenBalances } from './wallet' import { showErrorNotification, showInfoNotification, @@ -15,51 +14,17 @@ import { getPublicKey, getSigningFunction, getAddress, - getIsHardwareLogin + getIsHardwareLogin, + getBalances, + getTokenBalances } from '../core/deprecated' import { isToken, validateTransactionsBeforeSending } from '../core/wallet' import { ASSETS } from '../core/constants' import asyncWrap from '../core/asyncHelper' -import { toNumber, toBigNumber } from '../core/math' -import { toFixedDecimals, COIN_DECIMAL_LENGTH } from '../core/formatters' +import { toNumber } from '../core/math' import { log } from '../util/Logs' -// Constants -export const LOADING_TRANSACTIONS = 'LOADING_TRANSACTIONS' - -export const setIsLoadingTransaction = (isLoading: boolean) => ({ - type: LOADING_TRANSACTIONS, - payload: { - isLoadingTransactions: isLoading - } -}) - -export const syncTransactionHistory = ( - net: NetworkType, - address: string -) => async (dispatch: DispatchType) => { - dispatch(setIsLoadingTransaction(true)) - const [err, transactions] = await asyncWrap( - api.neonDB.getTransactionHistory(net, address) - ) - if (!err && transactions) { - const txs = transactions.map( - ({ NEO, GAS, txid, block_index }: TransactionHistoryType) => ({ - txid, - [ASSETS.NEO]: toFixedDecimals(NEO, 0), - [ASSETS.GAS]: toBigNumber(GAS) - .round(COIN_DECIMAL_LENGTH) - .toString() - }) - ) - dispatch(setIsLoadingTransaction(false)) - dispatch(setTransactionHistory(txs)) - } else { - dispatch(setIsLoadingTransaction(false)) - } -} - const extractTokens = (sendEntries: Array) => { return sendEntries.filter(({ symbol }) => isToken(symbol)) } @@ -218,24 +183,3 @@ export const sendTransaction = (sendEntries: Array) => async ( ) } } - -// state getters -export const getIsLoadingTransactions = (state: Object) => - state.transactions.isLoadingTransactions - -const initialState = { - isLoadingTransactions: false -} - -export default (state: Object = initialState, action: ReduxAction) => { - switch (action.type) { - case LOADING_TRANSACTIONS: - const { isLoadingTransactions } = action.payload - return { - ...state, - isLoadingTransactions - } - default: - return state - } -} diff --git a/app/modules/wallet.js b/app/modules/wallet.js deleted file mode 100644 index 319f7e437..000000000 --- a/app/modules/wallet.js +++ /dev/null @@ -1,207 +0,0 @@ -// @flow -import { api } from 'neon-js' -import { isNil } from 'lodash' - -import { syncTransactionHistory } from './transactions' -import { syncAvailableClaim } from './claim' -import { showErrorNotification } from './notifications' - -import { ASSETS } from '../core/constants' -import { getAddress, getNetwork, getTokensForNetwork, getMarketPrices, syncBlockHeight } from '../core/deprecated' -import asyncWrap from '../core/asyncHelper' -import { getTokenBalancesMap } from '../core/wallet' -import { COIN_DECIMAL_LENGTH } from '../core/formatters' -import { toBigNumber } from '../core/math' - -// Constants -export const SET_BALANCE = 'SET_BALANCE' -export const SET_TRANSACTION_HISTORY = 'SET_TRANSACTION_HISTORY' -export const SET_TOKENS_BALANCE = 'SET_TOKENS_BALANCE' -export const SET_IS_LOADED = 'SET_IS_LOADED' - -export const setIsLoaded = (loaded: boolean) => ({ - type: SET_IS_LOADED, - payload: { - loaded - } -}) - -// Actions -export function setBalance (NEO: string, GAS: string) { - return { - type: SET_BALANCE, - payload: { NEO, GAS } - } -} - -export function setTransactionHistory (transactions: Array) { - return { - type: SET_TRANSACTION_HISTORY, - payload: { transactions } - } -} - -export function setTokenBalances (tokenBalances: Array) { - return { - type: SET_TOKENS_BALANCE, - payload: { tokenBalances } - } -} - -export const retrieveBalance = (net: NetworkType, address: string) => async ( - dispatch: DispatchType -) => { - // If API dies, still display balance - ignore _err - const [_err, resultBalance] = await asyncWrap( - api.neonDB.getBalance(net, address) - ) // eslint-disable-line - if (_err) { - return dispatch( - showErrorNotification({ - message: `Could not retrieve NEO/GAS balance`, - stack: true - }) - ) - } else { - return dispatch( - setBalance( - String(resultBalance.NEO.balance), - toBigNumber(resultBalance.GAS.balance) - .round(COIN_DECIMAL_LENGTH) - .toString() - ) - ) - } -} - -export const loadWalletData = (silent: boolean = true) => async ( - dispatch: DispatchType, - getState: GetStateType -) => { - const state = getState() - const net = getNetwork(state) - const address = getAddress(state) - - if (!silent) { - dispatch(setIsLoaded(false)) - } - dispatch(syncTransactionHistory(net, address)) - dispatch(syncAvailableClaim(net, address)) - dispatch(syncBlockHeight(state)) - dispatch(getMarketPrices(state)) - await Promise.all([ - dispatch(retrieveTokenBalances()), - dispatch(retrieveBalance(net, address)) - ]) - dispatch(setIsLoaded(true)) - return true -} - -export const retrieveTokenBalances = () => async ( - dispatch: DispatchType, - getState: GetStateType -) => { - const state = getState() - const net = getNetwork(state) - const address = getAddress(state) - - const tokens = getTokensForNetwork(state) - const tokenBalances = [] - - for (const token of tokens) { - const { scriptHash } = token - - try { - const [rpcError, tokenRpcEndpoint] = await asyncWrap( - api.neonDB.getRPCEndpoint(net) - ) - const [tokenError, tokenResults] = await asyncWrap( - api.nep5.getToken(tokenRpcEndpoint, scriptHash, address) - ) - - if (!rpcError && !tokenError) { - tokenBalances.push({ - ...tokenResults, - balance: - isNil(tokenResults.balance) - ? '0' - : toBigNumber(tokenResults.balance) - .round(COIN_DECIMAL_LENGTH) - .toString(), - scriptHash - }) - } - } catch (e) { - console.error(e) - } - } - return dispatch(setTokenBalances(tokenBalances)) -} - -// state getters -export const getNEO = (state: Object): string => state.wallet.NEO -export const getGAS = (state: Object): string => state.wallet.GAS -export const getTransactions = (state: Object) => state.wallet.transactions -export const getTokenBalances = (state: Object) => state.wallet.tokenBalances -export const getIsLoaded = (state: Object) => state.wallet.loaded - -export const getBalances = (state: Object) => { - const neoBalance = getNEO(state) - const gasBalance = getGAS(state) - const tokenBalances = getTokenBalances(state) - const tokenBalancesMap = getTokenBalancesMap(tokenBalances) - - return { - [ASSETS.NEO]: neoBalance, - [ASSETS.GAS]: gasBalance, - ...tokenBalancesMap - } -} - -type State = { - loaded: boolean, - NEO: string, - GAS: string, - transactions: Array, - tokenBalances: Array -} - -const initialState = { - loaded: false, - NEO: '0', - GAS: '0', - transactions: [], - tokenBalances: [] -} - -export default (state: State = initialState, action: ReduxAction) => { - switch (action.type) { - case SET_BALANCE: - const { NEO, GAS } = action.payload - return { - ...state, - NEO, - GAS - } - case SET_TRANSACTION_HISTORY: - const { transactions } = action.payload - return { - ...state, - transactions - } - case SET_TOKENS_BALANCE: - const { tokenBalances } = action.payload - return { - ...state, - tokenBalances - } - case SET_IS_LOADED: - const { loaded } = action.payload - return { - ...state, - loaded - } - default: - return state - } -} diff --git a/app/util/Logs.js b/app/util/Logs.js index 882ba7c2e..911c01194 100644 --- a/app/util/Logs.js +++ b/app/util/Logs.js @@ -5,7 +5,7 @@ import { version } from '../../package.json' let sessionCount = 0 -export const log = (net: NetworkType, type: string, address: string, data: Object) => { +export const log = (net: NetworkType, type: string, address: string, data: Object = {}) => { const apiEndpoint = api.neonDB.getAPIEndpoint(net) axios.post(apiEndpoint + '/v2/log', { type: type, diff --git a/package.json b/package.json index b662632a9..baf67abce 100644 --- a/package.json +++ b/package.json @@ -177,6 +177,7 @@ "\\.(css|less|sass|scss)$": "identity-obj-proxy" }, "testPathIgnorePatterns": [ + "__tests__/testHelpers.js", "__tests__/setupTests.js" ], "unmockedModulePathPatterns": [