Browse Source

Merge pull request #153 from loeck/master

Flat AccountData, create serialize/deserialize
master
Meriadec Pillet 7 years ago
committed by GitHub
parent
commit
d95346df94
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 3
      .eslintrc
  2. 5
      src/actions/accounts.js
  3. 61
      src/components/AccountPage.js
  4. 16
      src/components/DashboardPage/AccountCard.js
  5. 4
      src/components/DashboardPage/index.js
  6. 6
      src/components/SelectAccount/index.js
  7. 15
      src/components/SelectAccount/stories.js
  8. 2
      src/components/SideBar/index.js
  9. 41
      src/components/TransactionsList/index.js
  10. 6
      src/components/TransactionsList/stories.js
  11. 6
      src/components/modals/AddAccount/index.js
  12. 31
      src/components/modals/Receive.js
  13. 2
      src/components/modals/SettingsAccount.js
  14. 11
      src/helpers/btc.js
  15. 88
      src/helpers/db.js
  16. 4
      src/middlewares/db.js
  17. 63
      src/reducers/accounts.js
  18. 18
      src/renderer/events.js
  19. 24
      src/types/common.js

3
.eslintrc

@ -16,6 +16,7 @@
},
"rules": {
"camelcase": 0,
"global-require": 0,
"import/no-extraneous-dependencies": 0,
"import/no-named-as-default": 0,
"import/prefer-default-export": 0,
@ -28,8 +29,8 @@
"no-return-assign": 0,
"no-shadow": 0,
"no-underscore-dangle": 0,
"no-use-before-define": 0,
"no-void": 0,
"global-require": 0,
"react/forbid-prop-types": 0,
"react/jsx-curly-brace-presence": 0,
"react/jsx-filename-extension": 0,

5
src/actions/accounts.js

@ -1,8 +1,9 @@
// @flow
import db from 'helpers/db'
import sortBy from 'lodash/sortBy'
import db from 'helpers/db'
import type { Dispatch } from 'redux'
import type { Account } from 'types/common'
@ -13,7 +14,7 @@ function sortAccounts(accounts, orderAccounts) {
const accountsSorted = sortBy(accounts, a => {
if (order === 'balance') {
return a.data.balance
return a.balance
}
return a[order]

61
src/components/AccountPage.js

@ -1,6 +1,6 @@
// @flow
import React, { PureComponent, Fragment } from 'react'
import React, { PureComponent } from 'react'
import { compose } from 'redux'
import { connect } from 'react-redux'
import { translate } from 'react-i18next'
@ -9,11 +9,11 @@ import { Redirect } from 'react-router'
import { MODAL_SEND, MODAL_RECEIVE, MODAL_SETTINGS_ACCOUNT } from 'constants'
import type { MapStateToProps } from 'react-redux'
import type { T, Account, AccountData } from 'types/common'
import type { T, Account } from 'types/common'
import { formatBTC } from 'helpers/format'
import { getAccountById, getAccountData } from 'reducers/accounts'
import { getAccountById } from 'reducers/accounts'
import { openModal } from 'reducers/modals'
import Box, { Card } from 'components/base/Box'
@ -26,29 +26,20 @@ import TransactionsList from 'components/TransactionsList'
type Props = {
t: T,
account: Account,
accountData: AccountData,
openModal: Function,
}
const mapStateToProps: MapStateToProps<*, *, *> = (state, props) => ({
account: getAccountById(state, props.match.params.id),
accountData: getAccountData(state, props.match.params.id),
})
const mapDispatchToProps = {
openModal,
}
function enrichTransactionsWithAccount(transactions, account) {
return transactions.map(t => ({
...t,
account,
}))
}
class AccountPage extends PureComponent<Props> {
render() {
const { account, accountData, openModal, t } = this.props
const { account, openModal, t } = this.props
// Don't even throw if we jumped in wrong account route
if (!account) {
@ -86,33 +77,27 @@ class AccountPage extends PureComponent<Props> {
/>
</Box>
</Box>
{accountData && (
<Fragment>
<Box horizontal flow={3}>
<Box grow>
<Card
title={t('AccountPage.balance')}
style={{ height: 435 }}
alignItems="center"
justifyContent="center"
>
<Text fontSize={5}>{formatBTC(accountData.balance)}</Text>
</Card>
</Box>
<Box horizontal flow={3}>
<Box grow>
<Card
title={t('AccountPage.balance')}
style={{ height: 435 }}
alignItems="center"
justifyContent="center"
>
<Text fontSize={5}>{formatBTC(account.balance)}</Text>
</Card>
</Box>
<Box style={{ width: 300 }}>
<Card title={t('AccountPage.receive')} flow={3}>
<ReceiveBox path={accountData.path} address={accountData.address} />
</Card>
</Box>
</Box>
<Card p={0} px={4} title={t('AccountPage.lastOperations')}>
<TransactionsList
transactions={enrichTransactionsWithAccount(accountData.transactions, account)}
/>
<Box style={{ width: 300 }}>
<Card title={t('AccountPage.receive')} flow={3}>
<ReceiveBox path={account.path} address={account.address} />
</Card>
</Fragment>
)}
</Box>
</Box>
<Card p={0} px={4} title={t('AccountPage.lastOperations')}>
<TransactionsList transactions={account.transactions} />
</Card>
</Box>
)
}

16
src/components/DashboardPage/AccountCard.js

@ -37,15 +37,13 @@ const AccountCard = ({
</Box>
<Bar size={1} color="argile" />
<Box grow justifyContent="center" color="dark">
{account.data && (
<FormattedVal
alwaysShowSign={false}
color="dark"
unit={account.unit}
showCode
val={account.data.balance}
/>
)}
<FormattedVal
alwaysShowSign={false}
color="dark"
unit={account.unit}
showCode
val={account.balance}
/>
</Box>
<AreaChart
tiny

4
src/components/DashboardPage/index.js

@ -90,7 +90,7 @@ const mergeFakeDatas = fakeDatas =>
const getAllTransactions = accounts => {
const allTransactions = accounts.reduce((result, account) => {
const transactions = get(account, 'data.transactions', [])
const transactions = get(account, 'transactions', [])
result = [
...result,
@ -103,7 +103,7 @@ const getAllTransactions = accounts => {
return result
}, [])
return sortBy(allTransactions, t => t.received_at)
return sortBy(allTransactions, t => t.receivedAt)
.reverse()
.slice(0, ALL_TRANSACTIONS_LIMIT)
}

6
src/components/SelectAccount/index.js

@ -7,7 +7,7 @@ import { translate } from 'react-i18next'
import noop from 'lodash/noop'
import type { MapStateToProps } from 'react-redux'
import type { T, Account } from 'types/common'
import type { T, Accounts, Account } from 'types/common'
import { formatBTC } from 'helpers/format'
@ -30,14 +30,14 @@ const renderItem = item => (
</Box>
<Box>
<Text color="mouse" fontSize={0}>
{formatBTC(item.data.balance)}
{formatBTC(item.balance)}
</Text>
</Box>
</Box>
)
type Props = {
accounts: Array<Account>,
accounts: Accounts,
onChange?: () => Account | void,
value?: Account | null,
t: T,

15
src/components/SelectAccount/stories.js

@ -12,17 +12,16 @@ const stories = storiesOf('Components/SelectAccount', module)
const accounts = [...Array(20)].map(() => ({
id: chance.string(),
name: chance.name(),
address: chance.string(),
addresses: [],
balance: chance.floating({ min: 0, max: 20 }),
coinType: 0,
currency: getCurrencyByCoinType(0),
index: chance.integer({ min: 0, max: 20 }),
name: chance.name(),
path: '',
transactions: [],
unit: getDefaultUnitByCoinType(0),
data: {
address: chance.string(),
balance: chance.floating({ min: 0, max: 20 }),
currentIndex: chance.integer({ min: 0, max: 20 }),
path: '',
transactions: [],
},
}))
type State = {

2
src/components/SideBar/index.js

@ -110,7 +110,7 @@ class SideBar extends PureComponent<Props> {
color="warmGrey"
unit={account.unit}
showCode
val={account.data ? account.data.balance : 0}
val={account.balance || 0}
/>
}
iconActiveColor={account.currency.color}

41
src/components/TransactionsList/index.js

@ -73,11 +73,13 @@ const Cell = styled(Box).attrs({
const Transaction = ({
onAccountClick,
tx,
withAccounts,
}: {
onAccountClick?: Function,
tx: TransactionType,
withAccounts?: boolean,
}) => {
const time = moment(tx.received_at)
const time = moment(tx.receivedAt)
const Icon = getIconByCoinType(get(tx, 'account.currency.coinType'))
return (
<TransactionRaw>
@ -87,22 +89,23 @@ const Transaction = ({
<Hour>{time.format('HH:mm')}</Hour>
</Box>
</Cell>
{tx.account && (
<Cell
size={ACCOUNT_COL_SIZE}
horizontal
flow={2}
style={{ cursor: 'pointer' }}
onClick={() => onAccountClick && onAccountClick(tx.account)}
>
<Box alignItems="center" justifyContent="center" style={{ color: '#fcb653' }}>
{Icon && <Icon size={16} />}
</Box>
<Box ff="Open Sans|SemiBold" fontSize={4} color="dark">
{tx.account.name}
</Box>
</Cell>
)}
{withAccounts &&
tx.account && (
<Cell
size={ACCOUNT_COL_SIZE}
horizontal
flow={2}
style={{ cursor: 'pointer' }}
onClick={() => onAccountClick && onAccountClick(tx.account)}
>
<Box alignItems="center" justifyContent="center" style={{ color: '#fcb653' }}>
{Icon && <Icon size={16} />}
</Box>
<Box ff="Open Sans|SemiBold" fontSize={4} color="dark">
{tx.account.name}
</Box>
</Cell>
)}
<Cell
grow
shrink
@ -117,7 +120,7 @@ const Transaction = ({
{tx.balance > 0 ? 'From' : 'To'}
</Box>
<Box color="dark" ff="Open Sans" fontSize={3}>
{tx.balance > 0 ? get(tx, 'inputs.0.address') : get(tx, 'outputs.0.address')}
{tx.address}
</Box>
</Cell>
<Cell size={AMOUNT_COL_SIZE} justifyContent="flex-end">
@ -135,6 +138,7 @@ const Transaction = ({
Transaction.defaultProps = {
onAccountClick: noop,
withAccounts: false,
}
type Props = {
@ -181,6 +185,7 @@ class TransactionsList extends Component<Props> {
{transactions.map(t => (
<Transaction
key={`{${t.hash}-${t.account ? t.account.id : ''}`}
withAccounts={withAccounts}
onAccountClick={onAccountClick}
tx={t}
/>

6
src/components/TransactionsList/stories.js

@ -9,14 +9,16 @@ const stories = storiesOf('Components/TransactionsList', module)
const transactions = [
{
address: '5c6ea1716520c7d6e038d36a3223faced3c',
hash: '5c6ea1716520c7d6e038d36a3223faced3c4b8f7ffb69d9fb5bd527d562fdb62',
balance: 130000000,
received_at: '2018-01-09T16:03:52Z',
receivedAt: '2018-01-09T16:03:52Z',
},
{
address: '27416a48caab90fab053b507b8b6b9d4',
hash: '27416a48caab90fab053b507b8b6b9d48fba75421d3bfdbae4b85f64024bc9c4',
balance: 65000000,
received_at: '2018-01-09T16:02:40Z',
receivedAt: '2018-01-09T16:02:40Z',
},
]

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

@ -260,7 +260,7 @@ class AddAccountModal extends PureComponent<Props, State> {
...defaultState,
})
addAccount = ({ id, name, ...data }) => {
addAccount = account => {
const { currency } = this.state
const { addAccount } = this.props
@ -269,12 +269,10 @@ class AddAccountModal extends PureComponent<Props, State> {
}
addAccount({
id,
name,
...account,
coinType: currency.coinType,
currency,
unit: getDefaultUnitByCoinType(currency.coinType),
data,
})
}

31
src/components/modals/Receive.js

@ -1,6 +1,6 @@
// @flow
import React, { PureComponent, Fragment } from 'react'
import React, { PureComponent } from 'react'
import { translate } from 'react-i18next'
import get from 'lodash/get'
@ -62,7 +62,6 @@ class ReceiveModal extends PureComponent<Props, State> {
onHide={this.handleHide}
render={({ data, onClose }) => {
const account = this.getAccount(data)
const accountData = get(account, 'data', {})
return (
<ModalBody onClose={onClose} flow={3}>
@ -73,24 +72,16 @@ class ReceiveModal extends PureComponent<Props, State> {
<Label>Account</Label>
<SelectAccount value={account} onChange={this.handleChangeInput('account')} />
</Box>
{accountData && (
<Fragment>
<Box flow={1}>
<Label>Request amount</Label>
<Input
type="number"
min={0}
max={accountData.balance / 1e8}
onChange={this.handleChangeInput('amount')}
/>
</Box>
<ReceiveBox
path={accountData.path}
amount={amount}
address={accountData.address || ''}
/>
</Fragment>
)}
<Box flow={1}>
<Label>Request amount</Label>
<Input
type="number"
min={0}
max={account.balance / 1e8}
onChange={this.handleChangeInput('amount')}
/>
</Box>
<ReceiveBox path={account.path} amount={amount} address={account.address || ''} />
<Box horizontal justifyContent="center">
<Button primary onClick={onClose}>
Close

2
src/components/modals/SettingsAccount.js

@ -48,7 +48,7 @@ const defaultState = {
}
function hasNoTransactions(account: Account) {
return get(account, 'data.transactions.length', 0) === 0
return get(account, 'transactions.length', 0) === 0
}
class SettingsAccount extends PureComponent<Props, State> {

11
src/helpers/btc.js

@ -133,11 +133,16 @@ export async function getAccount({
return {
address: currentAddress.address,
allAddresses,
addresses: allAddresses,
balance,
currentIndex: currentAddress.index,
index: currentAddress.index,
path: `${path}/${getPath('external', currentAddress.index + 1)}`,
transactions,
transactions: transactions.map(t => ({
address: t.balance > 0 ? t.inputs[0].address : t.outputs[0].address,
balance: t.balance,
hash: t.hash,
receivedAt: t.received_at,
})),
}
})

88
src/helpers/db.js

@ -1,7 +1,15 @@
// @flow
import Store from 'electron-store'
import set from 'lodash/set'
import get from 'lodash/get'
import { getCurrencyByCoinType } from '@ledgerhq/currencies'
import type { Accounts } from 'types/common'
type DBKey = 'settings' | 'accounts'
const encryptionKey = {}
const store = key =>
@ -13,13 +21,61 @@ const store = key =>
encryptionKey: encryptionKey[key],
})
export function setEncryptionKey(key, value) {
export function setEncryptionKey(key: DBKey, value?: string) {
encryptionKey[key] = value
}
export function serializeAccounts(accounts: Accounts) {
return accounts.map(account => ({
id: account.id,
address: account.address,
addresses: account.addresses,
balance: account.balance,
coinType: account.coinType,
currency: getCurrencyByCoinType(account.coinType),
index: account.index,
name: account.name,
path: account.path,
unit: account.unit,
transactions: account.transactions.map(t => ({
...t,
account,
})),
}))
}
export function deserializeAccounts(accounts: Accounts) {
return accounts.map(account => ({
id: account.id,
address: account.address,
addresses: account.addresses,
balance: account.balance,
coinType: account.coinType,
index: account.index,
name: account.name,
path: account.path,
transactions: account.transactions.map(({ account, ...t }) => t),
unit: account.unit,
}))
}
function middleware(type, key, data: any) {
if (key === 'accounts') {
if (type === 'get') {
data = serializeAccounts(data)
}
if (type === 'set') {
data = deserializeAccounts(data)
}
}
return data
}
export default {
// If the db doesn't exists for that key, init it, with the default value provided
init: (key, defaults) => {
init: (key: DBKey, defaults: any) => {
const db = store(key)
const data = db.get('data')
if (!data) {
@ -27,28 +83,40 @@ export default {
}
},
get: (key, defaults) => {
get: (key: DBKey, defaults: any): any => {
const db = store(key)
return db.get('data', defaults)
const data = db.get('data', defaults)
return middleware('get', key, data)
},
set: (key, val) => {
set: (key: DBKey, val: any) => {
const db = store(key)
val = middleware('set', key, val)
db.set('data', val)
return db.get('data')
return val
},
getIn: (key, path, defaultValue) => {
getIn: (key: DBKey, path: string, defaultValue: any) => {
const db = store(key)
const data = db.get('data')
let data = db.get('data')
data = middleware('get', key, data)
return get(data, path, defaultValue)
},
setIn: (key, path, val) => {
setIn: (key: DBKey, path: string, val: any) => {
const db = store(key)
const data = db.get('data')
val = middleware('set', key, val)
set(data, path, val)
db.set('data', data)
return db.get('data')
return val
},
}

4
src/middlewares/db.js

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

63
src/reducers/accounts.js

@ -7,32 +7,21 @@ import get from 'lodash/get'
import reduce from 'lodash/reduce'
import type { State } from 'reducers'
import type { Account, Accounts, AccountData } from 'types/common'
import type { Account, Accounts } from 'types/common'
export type AccountsState = Accounts
const state: AccountsState = []
function orderAccountsTransactions(account: Account) {
const transactions = get(account.data, 'transactions', [])
transactions.sort((a, b) => new Date(b.received_at) - new Date(a.received_at))
const { transactions } = account
transactions.sort((a, b) => new Date(b.receivedAt) - new Date(a.receivedAt))
return {
...account,
data: {
...account.data,
transactions,
},
transactions,
}
}
const defaultAccountData: AccountData = {
address: '',
balance: 0,
currentIndex: 0,
path: '',
transactions: [],
}
const handlers: Object = {
SET_ACCOUNTS: (
state: AccountsState,
@ -42,16 +31,7 @@ const handlers: Object = {
ADD_ACCOUNT: (
state: AccountsState,
{ payload: account }: { payload: Account },
): AccountsState => {
account = orderAccountsTransactions({
...account,
data: {
...defaultAccountData,
...account.data,
},
})
return [...state, account]
},
): AccountsState => [...state, orderAccountsTransactions(account)],
UPDATE_ACCOUNT: (
state: AccountsState,
@ -62,28 +42,17 @@ const handlers: Object = {
return existingAccount
}
const existingData = get(existingAccount, 'data', {})
const data = get(account, 'data', {})
const transactions = get(data, 'transactions', [])
const currentIndex = data.currentIndex
? data.currentIndex
: get(existingData, 'currentIndex', 0)
const { transactions, index } = account
const updatedAccount = {
...existingAccount,
...account,
data: {
...existingData,
...data,
balance: transactions.reduce((result, v) => {
result += v.balance
return result
}, 0),
currentIndex,
transactions,
},
balance: transactions.reduce((result, v) => {
result += v.balance
return result
}, 0),
index: index || get(existingAccount, 'currentIndex', 0),
transactions,
}
return orderAccountsTransactions(updatedAccount)
@ -99,7 +68,7 @@ export function getTotalBalance(state: { accounts: AccountsState }) {
return reduce(
state.accounts,
(result, account) => {
result += get(account, 'data.balance', 0)
result += get(account, 'balance', 0)
return result
},
0,
@ -123,12 +92,8 @@ export function getAccountById(state: { accounts: AccountsState }, id: string):
return account || null
}
export function getAccountData(state: State, id: string): AccountData | null {
return get(getAccountById(state, id), 'data', null)
}
export function canCreateAccount(state: State): boolean {
return every(getAccounts(state), a => get(a, 'data.transactions.length', 0) > 0)
return every(getAccounts(state), a => get(a, 'transactions.length', 0) > 0)
}
export default handleActions(handlers, state)

18
src/renderer/events.js

@ -13,7 +13,7 @@ import { CHECK_UPDATE_TIMEOUT, SYNC_ACCOUNT_TIMEOUT } from 'constants'
import { updateDevices, addDevice, removeDevice } from 'actions/devices'
import { updateAccount } from 'actions/accounts'
import { setUpdateStatus } from 'reducers/update'
import { getAccountData, getAccounts, getAccountById } from 'reducers/accounts'
import { getAccounts, getAccountById } from 'reducers/accounts'
import { isLocked } from 'reducers/application'
import i18n from 'renderer/i18n'
@ -53,12 +53,12 @@ export function startSyncAccounts(accounts: Accounts) {
syncAccounts = true
sendEvent('accounts', 'sync.all', {
accounts: accounts.map(account => {
const currentIndex = get(account, 'data.currentIndex', 0)
const allAddresses = get(account, 'data.allAddresses', [])
const index = get(account, 'index', 0)
const addresses = get(account, 'addresses', [])
return {
id: account.id,
allAddresses,
currentIndex,
allAddresses: addresses,
currentIndex: index,
}
}),
})
@ -87,8 +87,7 @@ export default ({ store, locked }: { store: Object, locked: boolean }) => {
if (syncAccounts) {
const state = store.getState()
const currentAccount = getAccountById(state, account.id) || {}
const currentAccountData = getAccountData(state, account.id) || {}
const currentAccountTransactions = get(currentAccountData, 'transactions', [])
const currentAccountTransactions = get(currentAccount, 'transactions', [])
const transactions = uniqBy(
[...currentAccountTransactions, ...account.transactions],
@ -100,10 +99,7 @@ export default ({ store, locked }: { store: Object, locked: boolean }) => {
store.dispatch(
updateAccount({
...account,
data: {
...account.data,
transactions,
},
transactions,
}),
)
}

24
src/types/common.js

@ -13,29 +13,27 @@ export type Devices = Array<Device>
// -------------------- Transactions
export type Transaction = {
account?: Object,
account?: Account,
address: string,
balance: number,
hash: string,
received_at: string,
receivedAt: string,
}
// -------------------- Accounts
export type AccountData = {
address: string,
balance: number,
currentIndex: number,
path: string,
transactions: Array<Transaction>,
}
export type Account = {
address: string,
addresses: Array<string>,
archived?: boolean,
data?: AccountData,
id: string,
name: string,
balance: number,
coinType: number,
currency: Currency,
id: string,
index: number,
name: string,
path: string,
transactions: Array<Transaction>,
unit: Unit,
}

Loading…
Cancel
Save