Browse Source

Refactor the data structures used to store accounts. Update storage behavior.

master
meriadec 7 years ago
parent
commit
cc013bad5e
No known key found for this signature in database GPG Key ID: 1D2FC2305E2CB399
  1. 29
      src/actions/accounts.js
  2. 2
      src/actions/settings.js
  3. 6
      src/components/AccountPage.js
  4. 60
      src/components/DashboardPage.js
  5. 2
      src/components/SelectAccount/index.js
  6. 8
      src/components/SideBar/index.js
  7. 2
      src/components/TopBar.js
  8. 16
      src/components/modals/AddAccount/index.js
  9. 14
      src/components/modals/SettingsAccount.js
  10. 38
      src/helpers/db.js
  11. 6
      src/internals/usb/wallet/accounts.js
  12. 4
      src/middlewares/db.js
  13. 136
      src/reducers/accounts.js
  14. 8
      src/renderer/events.js
  15. 6
      src/renderer/index.js
  16. 2
      src/types/common.js

29
src/actions/accounts.js

@ -1,9 +1,5 @@
// @flow
import { createAction } from 'redux-actions'
import type { Dispatch } from 'redux'
import db from 'helpers/db'
import type { Account } from 'types/common'
@ -14,24 +10,17 @@ export const addAccount: AddAccount = payload => ({
payload,
})
export type EditAccount = Account => { type: string, payload: Account }
export const editAccount: AddAccount = payload => ({
type: 'DB:EDIT_ACCOUNT',
export type UpdateAccount = Account => { type: string, payload: Account }
export const updateAccount: AddAccount = payload => ({
type: 'DB:UPDATE_ACCOUNT',
payload,
})
type FetchAccounts = () => { type: string }
export const fetchAccounts: FetchAccounts = () => ({
type: 'FETCH_ACCOUNTS',
payload: db('accounts'),
})
const setAccountData = createAction('DB:SET_ACCOUNT_DATA', (accountID, data) => ({
accountID,
data,
}))
export const syncAccount: Function = account => async (dispatch: Dispatch<*>) => {
const { id, ...data } = account
dispatch(setAccountData(id, data))
export const fetchAccounts: FetchAccounts = () => {
const payload = db.get('accounts')
return {
type: 'SET_ACCOUNTS',
payload,
}
}

2
src/actions/settings.js

@ -11,7 +11,7 @@ export const saveSettings: SaveSettings = payload => ({
})
export const fetchSettings: Function = () => dispatch => {
const settings = db('settings')
const settings = db.get('settings')
if (Object.keys(settings).length === 0) {
return
}

6
src/components/AccountPage.js

@ -4,6 +4,7 @@ import React, { PureComponent, Fragment } from 'react'
import { compose } from 'redux'
import { connect } from 'react-redux'
import { translate } from 'react-i18next'
import { Redirect } from 'react-router'
import { MODAL_SEND, MODAL_RECEIVE, MODAL_SETTINGS_ACCOUNT } from 'constants'
@ -42,6 +43,11 @@ class AccountPage extends PureComponent<Props> {
render() {
const { account, accountData, openModal, t } = this.props
// Don't even throw if we jumped in wrong account route
if (!account) {
return <Redirect to="/" />
}
return (
<Box flow={3}>
<Box horizontal>

60
src/components/DashboardPage.js

@ -40,7 +40,7 @@ type Props = {
type State = {
tab: number,
datas: Object,
fakeDatas: Array<any>,
}
const ACCOUNTS_BY_LINE = 3
@ -52,7 +52,7 @@ const itemsTimes = [
{ key: 'year', name: 'Last year' },
]
const generateData = v => ({
const generateFakeData = v => ({
name: `Day ${v}`,
value: random(10, 100),
})
@ -60,11 +60,11 @@ const generateData = v => ({
class DashboardPage extends PureComponent<Props, State> {
state = {
tab: 0,
datas: this.generateDatas(),
fakeDatas: this.generateFakeDatas(),
}
componentDidMount() {
this.addDatasOnAccounts()
this.addFakeDatasOnAccounts()
}
componentWillUnmount() {
@ -74,42 +74,33 @@ class DashboardPage extends PureComponent<Props, State> {
getAccountsChunk() {
const { accounts } = this.props
const listAccounts = Object.values(accounts)
// create shallow copy of accounts, to be mutated
const listAccounts = [...accounts]
while (listAccounts.length % ACCOUNTS_BY_LINE !== 0) listAccounts.push(null)
return chunk(listAccounts, ACCOUNTS_BY_LINE)
}
generateDatas() {
generateFakeDatas() {
const { accounts } = this.props
return Object.keys(accounts).reduce((result, key) => {
result[key] = [...Array(25).keys()].map(v => generateData(v + 1))
return result
}, {})
return accounts.map(() => [...Array(25).keys()].map(v => generateFakeData(v + 1)))
}
addDatasOnAccounts = () => {
addFakeDatasOnAccounts = () => {
this._timeout = setTimeout(() => {
const { accounts } = this.props
this.setState(prev => ({
datas: {
...Object.keys(accounts).reduce((result, key) => {
if (result[key]) {
const nextIndex = result[key].length
result[key][nextIndex] = generateData(nextIndex)
}
return result
}, prev.datas),
},
fakeDatas: accounts.reduce((res, acc, i) => {
if (res[i]) {
const nextIndex = res[i].length
res[i][nextIndex] = generateFakeData(nextIndex)
}
return res
}, prev.fakeDatas),
}))
this.addDatasOnAccounts()
this.addFakeDatasOnAccounts()
}, TIMEOUT_REFRESH_DATAS)
}
@ -119,9 +110,9 @@ class DashboardPage extends PureComponent<Props, State> {
render() {
const { totalBalance, push, accounts } = this.props
const { tab, datas } = this.state
const { tab, fakeDatas } = this.state
const totalAccounts = Object.keys(accounts).length
const totalAccounts = accounts.length
return (
<Box flow={4}>
@ -171,17 +162,14 @@ class DashboardPage extends PureComponent<Props, State> {
<AreaChart
height={250}
data={takeRight(
Object.keys(datas).reduce((result, key) => {
const data = datas[key]
fakeDatas.reduce((res, data) => {
data.forEach((d, i) => {
result[i] = {
res[i] = {
name: d.name,
value: (result[i] ? result[i].value : 0) + d.value,
value: (res[i] ? res[i].value : 0) + d.value,
}
})
return result
return res
}, []),
25,
)}
@ -216,7 +204,7 @@ class DashboardPage extends PureComponent<Props, State> {
<Box grow align="center" justify="center">
{account.data && formatBTC(account.data.balance)}
</Box>
<BarChart height={100} data={takeRight(datas[account.id], 25)} />
<BarChart height={100} data={takeRight(fakeDatas[j], 25)} />
</Card>
),
)}

2
src/components/SelectAccount/index.js

@ -18,7 +18,7 @@ import Box from 'components/base/Box'
import Text from 'components/base/Text'
const mapStateToProps: MapStateToProps<*, *, *> = state => ({
accounts: Object.entries(getVisibleAccounts(state)).map(([, account]: [string, any]) => account),
accounts: getVisibleAccounts(state),
})
const renderItem = item => (

8
src/components/SideBar/index.js

@ -111,11 +111,11 @@ class SideBar extends PureComponent<Props> {
</PlusBtn>
</CapsSubtitle>
<GrowScroll pb={2} px={2} flow={1}>
{Object.entries(accounts).map(([id, account]: [string, any]) => (
{accounts.map(account => (
<Item
linkTo={`/account/${id}`}
desc={formatBTC(account.data.balance)}
key={id}
linkTo={`/account/${account.id}`}
desc={formatBTC(account.data ? account.data.balance : 0)}
key={account.id}
icon={{ iconName: 'btc', prefix: 'fab' }}
>
{account.name}

2
src/components/TopBar.js

@ -32,7 +32,7 @@ const Container = styled(Box).attrs({
`
const mapStateToProps: MapStateToProps<*, *, *> = state => ({
hasAccounts: Object.keys(getAccounts(state)).length > 0,
hasAccounts: getAccounts(state).length > 0,
hasPassword: hasPassword(state),
})

16
src/components/modals/AddAccount/index.js

@ -5,6 +5,7 @@ import { connect } from 'react-redux'
import { compose } from 'redux'
import { translate } from 'react-i18next'
import { ipcRenderer } from 'electron'
import differenceBy from 'lodash/differenceBy'
import { MODAL_ADD_ACCOUNT } from 'constants'
@ -74,7 +75,7 @@ const Steps = {
</Box>
),
listAccounts: (props: Object) => {
const accounts = Object.entries(props.accounts).map(([, account]: [string, any]) => account)
const { accounts } = props
const emptyAccounts = accounts.filter(account => account.transactions.length === 0)
const existingAccounts = accounts.filter(account => account.transactions.length > 0)
const canCreateAccount = props.canCreateAccount && emptyAccounts.length === 1
@ -110,7 +111,7 @@ type Props = {
type State = {
inputValue: InputValue,
step: Step,
accounts: Object,
accounts: Accounts,
progress: null | Object,
}
@ -129,7 +130,7 @@ const defaultState = {
inputValue: {
wallet: '',
},
accounts: {},
accounts: [],
progress: null,
step: 'chooseWallet',
}
@ -146,12 +147,7 @@ class AddAccountModal extends PureComponent<Props, State> {
componentWillReceiveProps(nextProps) {
if (nextProps.accounts) {
this.setState(prev => ({
accounts: Object.keys(prev.accounts).reduce((result, value) => {
if (!nextProps.accounts[value]) {
result[value] = prev.accounts[value]
}
return result
}, {}),
accounts: differenceBy(prev.accounts, nextProps.accounts, 'id'),
}))
}
}
@ -183,7 +179,7 @@ class AddAccountModal extends PureComponent<Props, State> {
sendEvent('usb', 'wallet.getAccounts', {
path: currentDevice.path,
wallet: inputValue.wallet,
currentAccounts: Object.keys(accounts),
currentAccounts: accounts.map(acc => acc.id),
})
}

14
src/components/modals/SettingsAccount.js

@ -9,7 +9,7 @@ import { MODAL_SETTINGS_ACCOUNT } from 'constants'
import type { Account } from 'types/common'
import { editAccount } from 'actions/accounts'
import { updateAccount } from 'actions/accounts'
import { setDataModal, closeModal } from 'reducers/modals'
import Box from 'components/base/Box'
@ -27,14 +27,14 @@ type State = {
type Props = {
closeModal: Function,
editAccount: Function,
updateAccount: Function,
setDataModal: Function,
push: Function,
}
const mapDispatchToProps = {
closeModal,
editAccount,
updateAccount,
setDataModal,
push,
}
@ -91,9 +91,9 @@ class SettingsAccount extends PureComponent<Props, State> {
handleSubmitName = (account: Account) => (e: SyntheticEvent<HTMLFormElement>) => {
e.preventDefault()
const { editAccount, setDataModal } = this.props
const { updateAccount, setDataModal } = this.props
editAccount(account)
updateAccount(account)
setDataModal(MODAL_SETTINGS_ACCOUNT, { account })
this.setState({
@ -102,9 +102,9 @@ class SettingsAccount extends PureComponent<Props, State> {
}
handleArchiveAccount = (account: Account) => () => {
const { push, closeModal, editAccount } = this.props
const { push, closeModal, updateAccount } = this.props
editAccount({
updateAccount({
...account,
archived: true,
})

38
src/helpers/db.js

@ -2,23 +2,37 @@ import Store from 'electron-store'
const encryptionKey = {}
const store = type =>
const store = key =>
new Store({
name: type,
defaults: {},
encryptionKey: encryptionKey[type],
name: key,
defaults: {
data: null,
},
encryptionKey: encryptionKey[key],
})
export function setEncryptionKey(type, value) {
encryptionKey[type] = value
export function setEncryptionKey(key, value) {
encryptionKey[key] = value
}
export default (type, values) => {
const db = store(type)
export default {
// If the db doesn't exists for that key, init it, with the default value provided
init: (key, defaults) => {
const db = store(key)
const data = db.get('data')
if (!data) {
db.set('data', defaults)
}
},
if (values) {
db.store = values
}
get: key => {
const db = store(key)
return db.get('data')
},
return db.store
set: (key, val) => {
const db = store(key)
db.set('data', val)
return db.get('data')
},
}

6
src/internals/usb/wallet/accounts.js

@ -105,7 +105,7 @@ export default async ({
return encodeBase58Check(xpub)
}
const getAllAccounts = async (currentAccount = 0, accounts = {}) => {
const getAllAccounts = async (currentAccount = 0, accounts = []) => {
const xpub58 = await getXpub58ByAccount({ account: currentAccount, network })
if (currentAccounts.includes(xpub58)) {
@ -122,10 +122,10 @@ export default async ({
const hasTransactions = account.transactions.length > 0
accounts[xpub58] = {
accounts.push({
id: xpub58,
...account,
}
})
if (hasTransactions) {
return getAllAccounts(currentAccount + 1, accounts)

4
src/middlewares/db.js

@ -17,6 +17,6 @@ export default store => next => action => {
const state = getState()
const { settings } = state
db('settings', settings)
db('accounts', getAccounts(state))
db.set('settings', settings)
db.set('accounts', getAccounts(state))
}

136
src/reducers/accounts.js

@ -12,64 +12,85 @@ import type { Account, Accounts, AccountData } from 'types/common'
export type AccountsState = Accounts
const state: AccountsState = {}
const state: AccountsState = []
function getAccount(account: Account) {
function orderAccountsTransactions(account: Account) {
const transactions = get(account.data, 'transactions', [])
transactions.sort((a, b) => new Date(b.received_at) - new Date(a.received_at))
return {
...account,
data: {
...(account.data || {}),
...account.data,
transactions,
},
}
}
const defaultAccountData: AccountData = {
address: '',
balance: 0,
currentIndex: 0,
transactions: [],
}
const handlers: Object = {
ADD_ACCOUNT: (state: AccountsState, { payload: account }: { payload: Account }) => ({
...state,
[account.id]: getAccount(account),
}),
EDIT_ACCOUNT: (state: AccountsState, { payload: account }: { payload: Account }) => ({
...state,
[account.id]: {
...state[account.id],
...getAccount(account),
},
}),
FETCH_ACCOUNTS: (state: AccountsState, { payload: accounts }: { payload: Accounts }) => accounts,
SET_ACCOUNT_DATA: (
SET_ACCOUNTS: (
state: AccountsState,
{ payload: { accountID, data } }: { payload: { accountID: string, data: AccountData } },
): AccountsState => {
const account = state[accountID]
const { data: accountData } = account
const transactions = uniqBy(
[...get(accountData, 'transactions', []), ...data.transactions],
tx => tx.hash,
)
const currentIndex = data.currentIndex ? data.currentIndex : get(accountData, 'currentIndex', 0)
account.data = {
...accountData,
...data,
balance: transactions.reduce((result, v) => {
result += v.balance
return result
}, 0),
currentIndex,
transactions,
}
{ payload: accounts }: { payload: Accounts },
): AccountsState => accounts,
return {
...state,
[accountID]: getAccount(account),
}
ADD_ACCOUNT: (
state: AccountsState,
{ payload: account }: { payload: Account },
): AccountsState => {
account = orderAccountsTransactions({
...account,
data: {
...defaultAccountData,
...account.data,
},
})
return [...state, account]
},
UPDATE_ACCOUNT: (
state: AccountsState,
{ payload: account }: { payload: Account },
): AccountsState =>
state.map(existingAccount => {
if (existingAccount.id !== account.id) {
return existingAccount
}
const existingData = get(existingAccount, 'data', {})
const data = get(account, 'data', {})
const transactions = uniqBy(
[...get(existingData, 'transactions', []), ...get(data, 'transactions', [])],
tx => tx.hash,
)
const currentIndex = data.currentIndex
? data.currentIndex
: get(existingData, 'currentIndex', 0)
const updatedAccount = {
...existingAccount,
...account,
data: {
...existingData,
...data,
balance: transactions.reduce((result, v) => {
result += v.balance
return result
}, 0),
currentIndex,
transactions,
},
}
return orderAccountsTransactions(updatedAccount)
}),
}
// Selectors
@ -78,35 +99,24 @@ export function getTotalBalance(state: { accounts: AccountsState }) {
return reduce(
state.accounts,
(result, account) => {
result += account.data.balance
result += get(account, 'data.balance', 0)
return result
},
0,
)
}
export function getAccounts(state: { accounts: AccountsState }) {
return Object.keys(state.accounts).reduce((result, key) => {
result[key] = getAccount(state.accounts[key])
return result
}, {})
export function getAccounts(state: { accounts: AccountsState }): Array<Account> {
return state.accounts
}
export function getVisibleAccounts(state: { accounts: AccountsState }) {
const accounts = getAccounts(state)
return Object.keys(accounts).reduce((result, key) => {
const account = accounts[key]
if (account.archived !== true) {
result[key] = account
}
return result
}, {})
export function getVisibleAccounts(state: { accounts: AccountsState }): Array<Account> {
return getAccounts(state).filter(account => account.archived !== true)
}
export function getAccountById(state: { accounts: AccountsState }, id: string) {
return getAccounts(state)[id]
export function getAccountById(state: { accounts: AccountsState }, id: string): Account | null {
const account = getAccounts(state).find(account => account.id === id)
return account || null
}
export function getAccountData(state: State, id: string): AccountData | null {
@ -114,7 +124,7 @@ export function getAccountData(state: State, id: string): AccountData | null {
}
export function canCreateAccount(state: State): boolean {
return every(getAccounts(state), a => a.data.transactions.length > 0)
return every(getAccounts(state), a => get(a, 'data.transactions.length', 0) > 0)
}
export default handleActions(handlers, state)

8
src/renderer/events.js

@ -10,7 +10,7 @@ import type { Accounts } from 'types/common'
import { CHECK_UPDATE_TIMEOUT, SYNC_ACCOUNT_TIMEOUT } from 'constants'
import { updateDevices, addDevice, removeDevice } from 'actions/devices'
import { syncAccount } from 'actions/accounts'
import { updateAccount } from 'actions/accounts'
import { setUpdateStatus } from 'reducers/update'
import { getAccountData, getAccounts } from 'reducers/accounts'
@ -41,11 +41,11 @@ export function sendSyncEvent(channel: string, msgType: string, data: any): any
export function startSyncAccounts(accounts: Accounts) {
syncAccounts = true
sendEvent('accounts', 'sync.all', {
accounts: Object.entries(accounts).map(([id, account]: [string, any]) => {
accounts: accounts.map(account => {
const currentIndex = get(account, 'data.currentIndex', 0)
const allAddresses = get(account, 'data.allAddresses', [])
return {
id,
id: account.id,
allAddresses,
currentIndex,
}
@ -75,7 +75,7 @@ export default ({ store, locked }: { store: Object, locked: boolean }) => {
)
if (currentAccountData.transactions.length !== transactions.length) {
store.dispatch(syncAccount(account))
store.dispatch(updateAccount(account))
}
}
},

6
src/renderer/index.js

@ -16,6 +16,8 @@ import { fetchSettings } from 'actions/settings'
import { isLocked } from 'reducers/application'
import { getLanguage } from 'reducers/settings'
import db from 'helpers/db'
import App from 'components/App'
import 'styles/global'
@ -25,6 +27,10 @@ if (__PROD__ && __SENTRY_URL__) {
window.addEventListener('unhandledrejection', event => Raven.captureException(event.reason))
}
// init db with defaults if needed
db.init('accounts', [])
db.init('settings', {})
const history = createHistory()
const store = createStore(history)
const rootNode = document.getElementById('app')

2
src/types/common.js

@ -33,7 +33,7 @@ export type Account = {
data?: AccountData,
}
export type Accounts = { [_: string]: Account }
export type Accounts = Array<Account>
// -------------------- Settings

Loading…
Cancel
Save