Browse Source

Merge pull request #184 from loeck/master

Add counterValues
master
Meriadec Pillet 7 years ago
committed by GitHub
parent
commit
78dfca3000
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 10
      package.json
  2. 66
      src/actions/counterValues.js
  3. 40
      src/components/BalanceSummary/BalanceInfos.js
  4. 66
      src/components/BalanceSummary/index.js
  5. 137
      src/components/CalculateBalance.js
  6. 97
      src/components/DashboardPage/AccountCard.js
  7. 144
      src/components/DashboardPage/index.js
  8. 15
      src/components/ReceiveBox.js
  9. 2
      src/components/SelectAccount/stories.js
  10. 2
      src/components/SideBar/index.js
  11. 265
      src/components/base/Chart/index.js
  12. 7
      src/components/base/FormattedVal.js
  13. 3
      src/components/base/Pills/index.js
  14. 2
      src/components/modals/Receive.js
  15. 117
      src/helpers/btc.js
  16. 2
      src/helpers/db.js
  17. 13
      src/internals/accounts/sync.js
  18. 2
      src/internals/usb/wallet/accounts.js
  19. 3
      src/middlewares/db.js
  20. 17
      src/reducers/accounts.js
  21. 19
      src/reducers/counterValues.js
  22. 12
      src/reducers/index.js
  23. 36
      src/renderer/events.js
  24. 6
      src/renderer/init.js
  25. 6
      src/styles/global.js
  26. 6
      src/types/common.js
  27. 30
      yarn.lock

10
package.json

@ -62,7 +62,7 @@
"color": "^3.0.0", "color": "^3.0.0",
"cross-env": "^5.1.3", "cross-env": "^5.1.3",
"debug": "^3.1.0", "debug": "^3.1.0",
"downshift": "^1.28.1", "downshift": "^1.28.2",
"electron-store": "^1.3.0", "electron-store": "^1.3.0",
"electron-updater": "^2.20.1", "electron-updater": "^2.20.1",
"fuse.js": "^3.2.0", "fuse.js": "^3.2.0",
@ -75,8 +75,8 @@
"object-path": "^0.11.4", "object-path": "^0.11.4",
"qrcode": "^1.2.0", "qrcode": "^1.2.0",
"query-string": "^5.1.0", "query-string": "^5.1.0",
"raven": "^2.4.1", "raven": "^2.4.2",
"raven-js": "^3.22.3", "raven-js": "^3.22.4",
"react": "^16.2.0", "react": "^16.2.0",
"react-dom": "^16.2.0", "react-dom": "^16.2.0",
"react-i18next": "^7.4.0", "react-i18next": "^7.4.0",
@ -139,8 +139,8 @@
"js-yaml": "^3.10.0", "js-yaml": "^3.10.0",
"lint-staged": "^7.0.0", "lint-staged": "^7.0.0",
"node-loader": "^0.6.0", "node-loader": "^0.6.0",
"prettier": "^1.11.0", "prettier": "^1.11.1",
"react-hot-loader": "^4.0.0-beta.21", "react-hot-loader": "^4.0.0",
"webpack": "^3.11.0" "webpack": "^3.11.0"
} }
} }

66
src/actions/counterValues.js

@ -0,0 +1,66 @@
// @flow
import axios from 'axios'
import moment from 'moment'
import { getDefaultUnitByCoinType } from '@ledgerhq/currencies'
import get from 'lodash/get'
import db from 'helpers/db'
type InitCounterValues = () => { type: string, payload: Object }
export const initCounterValues: InitCounterValues = () => ({
type: 'UPDATE_COUNTER_VALUES',
payload: db.get('counterValues'),
})
type UpdateCounterValues = Object => { type: string, payload: Object }
export const updateCounterValues: UpdateCounterValues = payload => ({
type: 'DB:UPDATE_COUNTER_VALUES',
payload,
})
type FetchCounterValues = (?number) => (Dispatch<*>, Function) => void
export const fetchCounterValues: FetchCounterValues = coinType => (dispatch, getState) => {
const { accounts, counterValues } = getState()
let coinTypes = []
if (!coinType) {
coinTypes = [...new Set(accounts.map(a => a.coinType))]
} else {
coinTypes = [coinType]
}
const today = moment().format('YYYY-MM-DD')
const fetchCounterValuesByCoinType = coinType => {
const { code } = getDefaultUnitByCoinType(coinType)
const todayCounterValues = get(counterValues, `${code}-USD.${today}`, null)
if (todayCounterValues !== null) {
return {}
}
return axios
.get(
`https://min-api.cryptocompare.com/data/histoday?&extraParams=ledger-test&fsym=${code}&tsym=USD&allData=1`,
)
.then(({ data }) => ({
symbol: `${code}-USD`,
values: data.Data.reduce((result, d) => {
const date = moment(d.time * 1000).format('YYYY-MM-DD')
result[date] = d.close
return result
}, {}),
}))
}
Promise.all(coinTypes.map(fetchCounterValuesByCoinType)).then(result => {
const newCounterValues = result.reduce((r, v) => {
r[v.symbol] = v.values
return r
}, {})
dispatch(updateCounterValues(newCounterValues))
})
}

40
src/components/DashboardPage/BalanceInfos.js → src/components/BalanceSummary/BalanceInfos.js

@ -1,40 +1,33 @@
// @flow // @flow
import React from 'react' import React from 'react'
import { connect } from 'react-redux'
import styled from 'styled-components' import styled from 'styled-components'
import { getDefaultUnitByCoinType } from '@ledgerhq/currencies'
import type { MapStateToProps } from 'react-redux'
import { getTotalBalance } from 'reducers/accounts'
import Box from 'components/base/Box' import Box from 'components/base/Box'
import Text from 'components/base/Text' import Text from 'components/base/Text'
import FormattedVal from 'components/base/FormattedVal' import FormattedVal from 'components/base/FormattedVal'
const mapStateToProps: MapStateToProps<*, *, *> = state => ({
totalBalance: getTotalBalance(state),
})
type Props = { type Props = {
fiat: string,
since: string,
totalBalance: number, totalBalance: number,
sinceBalance: number,
} }
const Sub = styled(Text).attrs({ const Sub = styled(Text).attrs({
ff: 'Open Sans', ff: 'Open Sans',
color: 'graphite', color: 'warnGrey',
fontSize: 4, fontSize: 4,
})`` })``
function BalanceInfos(props: Props) { function BalanceInfos(props: Props) {
const { totalBalance } = props const { fiat, totalBalance, since, sinceBalance } = props
return ( return (
<Box horizontal alignItems="flex-end" flow={7}> <Box horizontal alignItems="flex-end" flow={7}>
<Box grow> <Box grow>
<FormattedVal <FormattedVal
fiat={fiat}
val={totalBalance} val={totalBalance}
unit={getDefaultUnitByCoinType(0)}
alwaysShowSign={false} alwaysShowSign={false}
showCode showCode
fontSize={8} fontSize={8}
@ -44,15 +37,26 @@ function BalanceInfos(props: Props) {
<Sub>{'Total balance'}</Sub> <Sub>{'Total balance'}</Sub>
</Box> </Box>
<Box alignItems="flex-end"> <Box alignItems="flex-end">
<FormattedVal isPercent val={9.25} alwaysShowSign fontSize={7} /> <FormattedVal
<Sub>{'since one week'}</Sub> isPercent
val={Math.floor((totalBalance - sinceBalance) / sinceBalance * 100)}
alwaysShowSign
fontSize={7}
/>
<Sub>since one {since}</Sub>
</Box> </Box>
<Box alignItems="flex-end"> <Box alignItems="flex-end">
<FormattedVal fiat="USD" alwaysShowSign showCode val={6132.23} fontSize={7} /> <FormattedVal
<Sub>{'since one week'}</Sub> fiat="USD"
alwaysShowSign
showCode
val={totalBalance - sinceBalance}
fontSize={7}
/>
<Sub>since one {since}</Sub>
</Box> </Box>
</Box> </Box>
) )
} }
export default connect(mapStateToProps)(BalanceInfos) export default BalanceInfos

66
src/components/BalanceSummary/index.js

@ -0,0 +1,66 @@
// @flow
import React, { Fragment } from 'react'
import moment from 'moment'
import { formatCurrencyUnit, getFiatUnit } from '@ledgerhq/currencies'
import type { Accounts } from 'types/common'
import { space } from 'styles/theme'
import { AreaChart } from 'components/base/Chart'
import Box, { Card } from 'components/base/Box'
import CalculateBalance from 'components/CalculateBalance'
import BalanceInfos from './BalanceInfos'
type Props = {
accounts: Accounts,
selectedTime: string,
daysCount: number,
}
const BalanceSummary = ({ accounts, selectedTime, daysCount }: Props) => (
<Card flow={3} p={0} py={6}>
<CalculateBalance
accounts={accounts}
daysCount={daysCount}
render={({ allBalances, totalBalance, sinceBalance }) => (
<Fragment>
<Box px={6}>
<BalanceInfos
fiat="USD"
totalBalance={totalBalance}
since={selectedTime}
sinceBalance={sinceBalance}
/>
</Box>
<Box ff="Open Sans" fontSize={4} color="graphite">
<AreaChart
color="#5286f7"
data={allBalances}
height={250}
id="dashboard-chart"
padding={{
top: space[6],
bottom: space[6],
left: space[6] * 2,
right: space[6],
}}
strokeWidth={2}
renderLabels={d =>
formatCurrencyUnit(getFiatUnit('USD'), d.y * 100, {
showCode: true,
})
}
renderTickX={t => moment(t).format('MMM. D')}
/>
</Box>
</Fragment>
)}
/>
</Card>
)
export default BalanceSummary

137
src/components/CalculateBalance.js

@ -0,0 +1,137 @@
// @flow
import { PureComponent } from 'react'
import { connect } from 'react-redux'
import moment from 'moment'
import type { MapStateToProps } from 'react-redux'
import type { Accounts } from 'types/common'
import { getDefaultUnitByCoinType } from '@ledgerhq/currencies'
import first from 'lodash/first'
import get from 'lodash/get'
import last from 'lodash/last'
const mapStateToProps: MapStateToProps<*, *, *> = state => ({
counterValues: state.counterValues,
})
function getAllBalances({
accounts,
counterValues,
daysCount,
}: {
accounts: Accounts,
counterValues: Object,
daysCount: number,
}) {
const getDate = date => moment(date).format('YYYY-MM-DD')
const getValue = (balance, unit, d) =>
balance / 10 ** unit.magnitude * counterValues['BTC-USD'][d]
const allBalancesByCoinType = accounts.reduce((result, account) => {
const { coinType } = account
Object.keys(account.balanceByDay).forEach(k => {
if (!result[coinType]) {
result[coinType] = {}
}
result[coinType][k] = account.balanceByDay[k] + get(result, `${coinType}.${k}`, 0)
})
return result
}, {})
const allBalances = Object.keys(allBalancesByCoinType).reduce((result, coinType) => {
const unit = getDefaultUnitByCoinType(parseInt(coinType, 10))
const balanceByDay = allBalancesByCoinType[coinType]
const balanceByDayKeys = Object.keys(balanceByDay).sort((a, b) => new Date(b) - new Date(a))
const lastDay = balanceByDayKeys[0]
const lastBalance = balanceByDay[lastDay]
let balance = lastBalance
let index = daysCount
result[lastDay] = getValue(balance, unit, lastDay)
let d = getDate(moment(lastDay).subtract(1, 'days'))
while (index !== 0) {
result[d] = getValue(balance, unit, d) + (result[d] || 0)
d = getDate(moment(d).subtract(1, 'days'))
if (balanceByDay[d]) {
balance = balanceByDay[d]
}
index--
}
return result
}, {})
return Object.keys(allBalances)
.sort()
.map(k => ({
name: k,
value: allBalances[k],
}))
}
function calculateBalance(props) {
const allBalances = getAllBalances({
accounts: props.accounts,
counterValues: props.counterValues,
daysCount: props.daysCount,
})
return {
allBalances,
totalBalance: last(allBalances).value,
sinceBalance: first(allBalances).value,
}
}
type Props = {
accounts: Accounts,
counterValues: Object,
daysCount: number,
render: Function,
}
type State = {
allBalances: Array<Object>,
totalBalance: number,
sinceBalance: number,
}
class CalculateBalance extends PureComponent<Props, State> {
state = {
...calculateBalance(this.props),
}
componentWillReceiveProps(nextProps) {
const sameAccounts = this.props.accounts === nextProps.accounts
const sameCounterValues = this.props.counterValues === nextProps.counterValues
const sameDaysCount = this.props.daysCount === nextProps.daysCount
if (!sameAccounts || !sameCounterValues || !sameDaysCount) {
this.setState({
...calculateBalance(nextProps),
})
}
}
render() {
const { render } = this.props
const { allBalances, totalBalance, sinceBalance } = this.state
return render({ allBalances, totalBalance, sinceBalance })
}
}
export default connect(mapStateToProps)(CalculateBalance)

97
src/components/DashboardPage/AccountCard.js

@ -8,51 +8,90 @@ import type { Account } from 'types/common'
import { SimpleAreaChart } from 'components/base/Chart' import { SimpleAreaChart } from 'components/base/Chart'
import Bar from 'components/base/Bar' import Bar from 'components/base/Bar'
import Box, { Card } from 'components/base/Box' import Box, { Card } from 'components/base/Box'
import CalculateBalance from 'components/CalculateBalance'
import FormattedVal from 'components/base/FormattedVal' import FormattedVal from 'components/base/FormattedVal'
const AccountCard = ({ const AccountCard = ({
account, account,
data,
onClick, onClick,
daysCount,
}: { }: {
account: Account, account: Account,
data: Array<Object>,
onClick: Function, onClick: Function,
daysCount: number,
}) => { }) => {
const Icon = getIconByCoinType(account.currency.coinType) const Icon = getIconByCoinType(account.currency.coinType)
return ( return (
<Card p={4} flow={4} flex={1} style={{ cursor: 'pointer' }} onClick={onClick}> <Card p={4} flex={1} style={{ cursor: 'pointer' }} onClick={onClick}>
<Box horizontal ff="Open Sans|SemiBold" flow={3} alignItems="center"> <Box flow={4}>
<Box alignItems="center" justifyContent="center" style={{ color: account.currency.color }}> <Box horizontal ff="Open Sans|SemiBold" flow={3} alignItems="center">
{Icon && <Icon size={20} />} <Box
</Box> alignItems="center"
<Box> justifyContent="center"
<Box style={{ textTransform: 'uppercase' }} fontSize={0} color="graphite"> style={{ color: account.currency.color }}
{account.unit.code} >
{Icon && <Icon size={20} />}
</Box> </Box>
<Box fontSize={4} color="dark"> <Box>
{account.name} <Box style={{ textTransform: 'uppercase' }} fontSize={0} color="graphite">
{account.unit.code}
</Box>
<Box fontSize={4} color="dark">
{account.name}
</Box>
</Box> </Box>
</Box> </Box>
<Bar size={1} color="fog" />
<Box justifyContent="center">
<FormattedVal
alwaysShowSign={false}
color="dark"
unit={account.unit}
showCode
val={account.balance}
style={{
lineHeight: 1,
}}
/>
</Box>
</Box> </Box>
<Bar size={2} color="fog" /> <CalculateBalance
<Box grow justifyContent="center" color="dark"> accounts={[account]}
<FormattedVal daysCount={daysCount}
alwaysShowSign={false} render={({ allBalances, totalBalance, sinceBalance }) => (
color="dark" <Box flow={4}>
unit={account.unit} <Box flow={2} horizontal>
showCode <Box justifyContent="center">
val={account.balance} <FormattedVal
/> fiat="USD"
</Box> val={totalBalance}
<SimpleAreaChart alwaysShowSign={false}
id={`account-chart-${account.id}`} showCode
color={account.currency.color} fontSize={3}
height={52} color="graphite"
data={data} />
strokeWidth={1.5} </Box>
linearGradient={[[5, 0.2], [75, 0]]} <Box grow justifyContent="center">
<FormattedVal
isPercent
val={Math.floor((totalBalance - sinceBalance) / sinceBalance * 100)}
alwaysShowSign
fontSize={3}
/>
</Box>
</Box>
<SimpleAreaChart
data={allBalances}
color={account.currency.color}
height={52}
id={`account-chart-${account.id}`}
linearGradient={[[5, 0.2], [75, 0]]}
simple
strokeWidth={1.5}
/>
</Box>
)}
/> />
</Card> </Card>
) )

144
src/components/DashboardPage/index.js

@ -8,28 +8,23 @@ import { push } from 'react-router-redux'
import chunk from 'lodash/chunk' import chunk from 'lodash/chunk'
import get from 'lodash/get' import get from 'lodash/get'
import random from 'lodash/random'
import sortBy from 'lodash/sortBy' import sortBy from 'lodash/sortBy'
import takeRight from 'lodash/takeRight'
import type { MapStateToProps } from 'react-redux' import type { MapStateToProps } from 'react-redux'
import type { Accounts, T } from 'types/common' import type { Account, Accounts, T } from 'types/common'
import { space } from 'styles/theme'
import { getVisibleAccounts } from 'reducers/accounts' import { getVisibleAccounts } from 'reducers/accounts'
import { updateOrderAccounts } from 'actions/accounts' import { updateOrderAccounts } from 'actions/accounts'
import { saveSettings } from 'actions/settings' import { saveSettings } from 'actions/settings'
import { AreaChart } from 'components/base/Chart' import BalanceSummary from 'components/BalanceSummary'
import Box, { Card } from 'components/base/Box' import Box from 'components/base/Box'
import Pills from 'components/base/Pills' import Pills from 'components/base/Pills'
import Text from 'components/base/Text' import Text from 'components/base/Text'
import TransactionsList from 'components/TransactionsList' import TransactionsList from 'components/TransactionsList'
import AccountCard from './AccountCard' import AccountCard from './AccountCard'
import BalanceInfos from './BalanceInfos'
import AccountsOrder from './AccountsOrder' import AccountsOrder from './AccountsOrder'
const mapStateToProps: MapStateToProps<*, *, *> = state => ({ const mapStateToProps: MapStateToProps<*, *, *> = state => ({
@ -49,46 +44,19 @@ type Props = {
} }
type State = { type State = {
accountsChunk: Array<any>, accountsChunk: Array<Array<Account | null>>,
allTransactions: Array<Object>, allTransactions: Array<Object>,
fakeDatas: Object,
fakeDatasMerge: Array<any>,
selectedTime: string, selectedTime: string,
} }
const ACCOUNTS_BY_LINE = 3 const ACCOUNTS_BY_LINE = 3
const ALL_TRANSACTIONS_LIMIT = 10 const ALL_TRANSACTIONS_LIMIT = 10
const TIMEOUT_REFRESH_DATAS = 5e3
const itemsTimes = [{ key: 'day' }, { key: 'week' }, { key: 'month' }, { key: 'year' }]
const generateFakeData = v => ({
index: v,
name: `Day ${v}`,
value: random(10, 100),
})
const generateFakeDatas = accounts => const itemsTimes = [
accounts.reduce((result, a) => { { key: 'week', value: 7 },
result[a.id] = [...Array(25).keys()].map(v => generateFakeData(v + 1)) { key: 'month', value: 30 },
{ key: 'year', value: 365 },
return result ]
}, {})
const mergeFakeDatas = fakeDatas =>
takeRight(
Object.keys(fakeDatas).reduce((res, k) => {
const data = fakeDatas[k]
data.forEach((d, i) => {
res[i] = {
name: d.name,
value: (res[i] ? res[i].value : 0) + d.value,
}
})
return res
}, []),
25,
)
const getAllTransactions = accounts => { const getAllTransactions = accounts => {
const allTransactions = accounts.reduce((result, account) => { const allTransactions = accounts.reduce((result, account) => {
@ -120,43 +88,25 @@ const getAccountsChunk = accounts => {
} }
class DashboardPage extends PureComponent<Props, State> { class DashboardPage extends PureComponent<Props, State> {
constructor(props) { state = {
super() accountsChunk: getAccountsChunk(this.props.accounts),
allTransactions: getAllTransactions(this.props.accounts),
const fakeDatas = generateFakeDatas(props.accounts) selectedTime: 'week',
this.state = {
accountsChunk: getAccountsChunk(props.accounts),
allTransactions: getAllTransactions(props.accounts),
fakeDatas,
fakeDatasMerge: mergeFakeDatas(fakeDatas),
selectedTime: 'day',
}
} }
componentWillMount() { componentWillMount() {
this._itemsTimes = itemsTimes.map(item => ({ this._itemsTimes = itemsTimes.map(item => ({
...item, ...item,
value: item.value,
label: this.props.t(`time:${item.key}`), label: this.props.t(`time:${item.key}`),
})) }))
} }
componentDidMount() { componentDidMount() {
this._mounted = true this._mounted = true
this.addFakeDatasOnAccounts()
} }
componentWillReceiveProps(nextProps) { componentWillReceiveProps(nextProps) {
if (Object.keys(this.state.fakeDatas).length === 0) {
const fakeDatas = generateFakeDatas(nextProps.accounts)
this.setState({
fakeDatas,
fakeDatasMerge: mergeFakeDatas(fakeDatas),
})
}
if (nextProps.accounts !== this.props.accounts) { if (nextProps.accounts !== this.props.accounts) {
this.setState({ this.setState({
accountsChunk: getAccountsChunk(nextProps.accounts), accountsChunk: getAccountsChunk(nextProps.accounts),
@ -167,46 +117,29 @@ class DashboardPage extends PureComponent<Props, State> {
componentWillUnmount() { componentWillUnmount() {
this._mounted = false this._mounted = false
clearTimeout(this._timeout)
} }
addFakeDatasOnAccounts = () => { getDaysCount() {
this._timeout = setTimeout(() => { const { selectedTime } = this.state
const { fakeDatas } = this.state
const newFakeDatas = {}
Object.keys(fakeDatas).forEach(k => {
const data = fakeDatas[k]
data.shift()
const lastIndex = data[data.length - 1]
newFakeDatas[k] = [...data, generateFakeData(lastIndex.index + 1)]
})
window.requestIdleCallback(() => { const selectedTimeItems = this._itemsTimes.find(i => i.key === selectedTime)
if (this._mounted) {
this.setState({
fakeDatas: newFakeDatas,
fakeDatasMerge: mergeFakeDatas(newFakeDatas),
})
}
this.addFakeDatasOnAccounts() return selectedTimeItems && selectedTimeItems.value ? selectedTimeItems.value : 7
})
}, TIMEOUT_REFRESH_DATAS)
} }
_timeout = undefined handleChangeSelectedTime = item =>
this.setState({
selectedTime: item.key,
})
_mounted = false _mounted = false
_itemsTimes = [] _itemsTimes = []
render() { render() {
const { push, accounts, t } = this.props const { push, accounts, t } = this.props
const { accountsChunk, allTransactions, selectedTime, fakeDatas, fakeDatasMerge } = this.state const { accountsChunk, allTransactions, selectedTime } = this.state
const daysCount = this.getDaysCount()
const totalAccounts = accounts.length const totalAccounts = accounts.length
return ( return (
@ -226,32 +159,13 @@ class DashboardPage extends PureComponent<Props, State> {
<Pills <Pills
items={this._itemsTimes} items={this._itemsTimes}
activeKey={selectedTime} activeKey={selectedTime}
onChange={item => this.setState({ selectedTime: item.key })} onChange={this.handleChangeSelectedTime}
/> />
</Box> </Box>
</Box> </Box>
{totalAccounts > 0 && ( {totalAccounts > 0 && (
<Fragment> <Fragment>
<Card flow={3} p={0} py={6}> <BalanceSummary accounts={accounts} selectedTime={selectedTime} daysCount={daysCount} />
<Box px={6}>
<BalanceInfos since={selectedTime} />
</Box>
<Box ff="Open Sans" fontSize={4} color="graphite">
<AreaChart
id="dashboard-chart"
padding={{
top: space[6],
bottom: space[6],
left: space[6] * 2,
right: space[6],
}}
color="#5286f7"
height={250}
data={fakeDatasMerge}
strokeWidth={2}
/>
</Box>
</Card>
<Box flow={4}> <Box flow={4}>
<Box horizontal alignItems="flex-end"> <Box horizontal alignItems="flex-end">
<Text color="dark" ff="Museo Sans" fontSize={6}> <Text color="dark" ff="Museo Sans" fontSize={6}>
@ -278,9 +192,9 @@ class DashboardPage extends PureComponent<Props, State> {
/> />
) : ( ) : (
<AccountCard <AccountCard
key={account.id}
account={account} account={account}
data={fakeDatas[account.id]} daysCount={daysCount}
key={account.id}
onClick={() => push(`/account/${account.id}`)} onClick={() => push(`/account/${account.id}`)}
/> />
), ),

15
src/components/ReceiveBox.js

@ -6,7 +6,7 @@ import styled from 'styled-components'
import { ipcRenderer } from 'electron' import { ipcRenderer } from 'electron'
import type { MapStateToProps } from 'react-redux' import type { MapStateToProps } from 'react-redux'
import type { Device } from 'types/common' import type { Account, Device } from 'types/common'
import { getCurrentDevice } from 'reducers/devices' import { getCurrentDevice } from 'reducers/devices'
import { sendEvent } from 'renderer/events' import { sendEvent } from 'renderer/events'
@ -54,9 +54,8 @@ const mapStateToProps: MapStateToProps<*, *, *> = state => ({
type Props = { type Props = {
currentDevice: Device | null, currentDevice: Device | null,
address: string, account: Account,
amount?: string, amount?: string,
path: string,
} }
type State = { type State = {
@ -83,7 +82,7 @@ class ReceiveBox extends PureComponent<Props, State> {
} }
componentWillReceiveProps(nextProps: Props) { componentWillReceiveProps(nextProps: Props) {
if (this.props.address !== nextProps.address) { if (this.props.account !== nextProps.account) {
this.setState({ this.setState({
...defaultState, ...defaultState,
}) })
@ -112,12 +111,12 @@ class ReceiveBox extends PureComponent<Props, State> {
} }
handleVerifyAddress = () => { handleVerifyAddress = () => {
const { currentDevice, path } = this.props const { currentDevice, account } = this.props
if (currentDevice !== null) { if (currentDevice !== null) {
sendEvent('usb', 'wallet.verifyAddress', { sendEvent('usb', 'wallet.verifyAddress', {
pathDevice: currentDevice.path, pathDevice: currentDevice.path,
path, path: `${account.rootPath}${account.path}`,
}) })
this.setState({ this.setState({
@ -127,7 +126,7 @@ class ReceiveBox extends PureComponent<Props, State> {
} }
render() { render() {
const { amount, address } = this.props const { amount, account } = this.props
const { isVerified, isDisplay } = this.state const { isVerified, isDisplay } = this.state
if (!isDisplay) { if (!isDisplay) {
@ -138,6 +137,8 @@ class ReceiveBox extends PureComponent<Props, State> {
) )
} }
const { address } = account
return ( return (
<Box flow={3}> <Box flow={3}>
<Box> <Box>

2
src/components/SelectAccount/stories.js

@ -15,11 +15,13 @@ const accounts = [...Array(20)].map(() => ({
address: chance.string(), address: chance.string(),
addresses: [], addresses: [],
balance: chance.floating({ min: 0, max: 20 }), balance: chance.floating({ min: 0, max: 20 }),
balanceByDay: {},
coinType: 0, coinType: 0,
currency: getCurrencyByCoinType(0), currency: getCurrencyByCoinType(0),
index: chance.integer({ min: 0, max: 20 }), index: chance.integer({ min: 0, max: 20 }),
name: chance.name(), name: chance.name(),
path: '', path: '',
rootPath: '',
transactions: [], transactions: [],
unit: getDefaultUnitByCoinType(0), unit: getDefaultUnitByCoinType(0),
settings: { settings: {

2
src/components/SideBar/index.js

@ -92,7 +92,7 @@ class SideBar extends PureComponent<Props> {
<Box flow={4} grow pt={1}> <Box flow={4} grow pt={1}>
<CapsSubtitle horizontal alignItems="center"> <CapsSubtitle horizontal alignItems="center">
<Box grow>{t('sidebar:accounts')}</Box> <Box grow>{t('sidebar:accounts')}</Box>
<Tooltip render={() => t('addAccount:title')} offset={[0, 1]}> <Tooltip render={() => t('addAccount:title')}>
<PlusBtn onClick={() => openModal(MODAL_ADD_ACCOUNT)}> <PlusBtn onClick={() => openModal(MODAL_ADD_ACCOUNT)}>
<IconPlus height={14} width={14} /> <IconPlus height={14} width={14} />
</PlusBtn> </PlusBtn>

265
src/components/base/Chart/index.js

@ -1,5 +1,7 @@
// @flow // @flow
/* eslint-disable react/no-multi-comp */
import React, { Fragment, PureComponent } from 'react' import React, { Fragment, PureComponent } from 'react'
import { import {
VictoryChart, VictoryChart,
@ -11,11 +13,15 @@ import {
} from 'victory' } from 'victory'
import { radii, space, colors, fontSizes } from 'styles/theme' import { radii, space, colors, fontSizes } from 'styles/theme'
import { rgba, ff } from 'styles/helpers' import { ff } from 'styles/helpers'
import Box from 'components/base/Box' import Box from 'components/base/Box'
const ANIMATION_DURATION = 600 const ANIMATION_DURATION = 600
const DEFAULT_PROPS = {
color: 'blue',
padding: 0,
}
type Props = { type Props = {
height: number, height: number,
@ -88,7 +94,7 @@ function getLinearGradient({
id: string, id: string,
color: string, color: string,
}) { }) {
return ( return linearGradient.length > 0 ? (
<svg style={{ height: 0 }}> <svg style={{ height: 0 }}>
<defs> <defs>
<linearGradient id={id} x1="0" y1="0" x2="0" y2="100%"> <linearGradient id={id} x1="0" y1="0" x2="0" y2="100%">
@ -103,20 +109,25 @@ function getLinearGradient({
</linearGradient> </linearGradient>
</defs> </defs>
</svg> </svg>
) ) : null
} }
type LinearGradient = Array<Array<*>> type LinearGradient = Array<Array<*>>
type Chart = { type GenericChart = {
id: string, id: string,
linearGradient: LinearGradient, linearGradient: LinearGradient,
strokeWidth: number, strokeWidth: number,
height: number, height: number,
padding?: Object | number, padding: Object | number,
color: string, color: string,
data: Array<Object>, data: Array<Object>,
} }
type Chart = GenericChart & {
renderLabels: Function,
renderTickX: Function,
renderTickY: Function,
}
export const SimpleAreaChart = ({ export const SimpleAreaChart = ({
linearGradient, linearGradient,
@ -126,10 +137,10 @@ export const SimpleAreaChart = ({
id, id,
padding, padding,
color, color,
}: Chart) => ( }: GenericChart) => (
<WrapperChart <WrapperChart
height={height} height={height}
render={({ width, isAnimationActive }) => ( render={({ width }) => (
<Fragment> <Fragment>
{getLinearGradient({ {getLinearGradient({
linearGradient, linearGradient,
@ -137,7 +148,9 @@ export const SimpleAreaChart = ({
color, color,
})} })}
<VictoryArea <VictoryArea
animate={isAnimationActive ? { duration: ANIMATION_DURATION } : null} domainPadding={{
y: [0, space[1]],
}}
data={data} data={data}
x="name" x="name"
y="value" y="value"
@ -158,114 +171,142 @@ export const SimpleAreaChart = ({
) )
SimpleAreaChart.defaultProps = { SimpleAreaChart.defaultProps = {
padding: 0, height: 50,
id: 'simple-chart',
linearGradient: [],
strokeWidth: 1,
...DEFAULT_PROPS,
} }
export const AreaChart = ({ const areaChartTooltip = ({ renderLabels }: { renderLabels: Function }) => (
strokeWidth, <VictoryTooltip
id, corderRadius={radii[1]}
color, pointerLength={0}
linearGradient, height={25}
padding, labelComponent={
height, <VictoryLabel
data, style={{
}: Chart) => { ...ff('Open Sans|SemiBold'),
const tickLabelsStyle = { fontSize: fontSizes[2],
fill: colors.grey, fill: colors.white,
fontSize: fontSizes[4], }}
fontFamily: 'inherit', />
fontWeight: 'inherit', }
flyoutStyle={{
fill: colors.dark,
stroke: null,
}}
width={a => space[2] * 2 + renderLabels(a).length * 5.2} // Approximatif size of char for calculate Tooltip witdh
/>
)
const AreaChartContainer = <VictoryVoronoiContainer voronoiDimension="x" />
export class AreaChart extends PureComponent<Chart> {
static defaultProps = {
height: 100,
id: 'chart',
linearGradient: [[5, 0.2], [50, 0]],
strokeWidth: 2,
renderLabels: (d: Object) => d.y,
renderTickX: (t: any) => t,
renderTickY: (t: any) => t,
...DEFAULT_PROPS,
} }
return ( render() {
<WrapperChart const {
height={height} color,
render={({ width, isAnimationActive }) => ( data,
<Fragment> height,
{getLinearGradient({ id,
linearGradient, linearGradient,
id, padding,
color, renderLabels,
})} renderTickX,
<VictoryChart renderTickY,
height={height} strokeWidth,
width={width} } = this.props
padding={padding}
containerComponent={<VictoryVoronoiContainer voronoiDimension="x" />} const tickLabelsStyle = {
> fill: colors.grey,
<VictoryAxis fontSize: fontSizes[4],
tickCount={6} fontFamily: 'inherit',
style={{ fontWeight: 'inherit',
axis: { }
stroke: colors.lightGrey,
}, return (
tickLabels: { <WrapperChart
...tickLabelsStyle, height={height}
padding: space[2], render={({ width, isAnimationActive }) => (
}, <Fragment>
}} {getLinearGradient({
/> linearGradient,
<VictoryAxis id,
dependentAxis color,
tickCount={4} })}
style={{ <VictoryChart
grid: { height={height}
stroke: colors.lightGrey, width={width}
strokeDasharray: 5, padding={padding}
}, domainPadding={{
axis: { y: [0, space[1]],
stroke: null,
},
tickLabels: {
...tickLabelsStyle,
padding: space[4],
},
}} }}
/> containerComponent={AreaChartContainer}
<VictoryArea >
animate={isAnimationActive ? { duration: ANIMATION_DURATION } : null} <VictoryAxis
data={data} tickCount={6}
x="name" tickFormat={renderTickX}
y="value" style={{
labelComponent={ axis: {
<VictoryTooltip stroke: colors.lightGrey,
corderRadius={radii[1]} },
pointerLength={0} tickLabels: {
height={25} ...tickLabelsStyle,
labelComponent={ padding: space[2],
<VictoryLabel },
style={{ }}
...ff('Open Sans|SemiBold'), />
fontSize: fontSizes[2], <VictoryAxis
fill: colors.white, dependentAxis
}} tickCount={4}
/> tickFormat={renderTickY}
} style={{
flyoutStyle={{ grid: {
fill: rgba(colors.dark, 0.8), stroke: colors.lightGrey,
strokeDasharray: 5,
},
axis: {
stroke: null, stroke: null,
}} },
width={a => space[1] * 2 + a.value.length} tickLabels: {
/> ...tickLabelsStyle,
} padding: space[4],
labels={d => d.y} },
style={{ }}
data: { />
stroke: color, <VictoryArea
fill: `url(#${id})`, animate={isAnimationActive ? { duration: ANIMATION_DURATION } : null}
strokeWidth, data={data}
}, x="name"
}} y="value"
width={width} labelComponent={areaChartTooltip({
/> renderLabels,
</VictoryChart> })}
</Fragment> labels={renderLabels}
)} style={{
/> data: {
) stroke: color,
} fill: `url(#${id})`,
strokeWidth,
AreaChart.defaultProps = { },
linearGradient: [[5, 0.2], [50, 0]], }}
padding: undefined, width={width}
/>
</VictoryChart>
</Fragment>
)}
/>
)
}
} }

7
src/components/base/FormattedVal.js

@ -23,18 +23,19 @@ type Props = {
} }
function FormattedVal(props: Props) { function FormattedVal(props: Props) {
const { val, fiat, isPercent, alwaysShowSign, showCode, ...p } = props const { fiat, isPercent, alwaysShowSign, showCode, ...p } = props
let { unit } = props let { val, unit } = props
const isNegative = val < 0 const isNegative = val < 0
let text = '' let text = ''
if (isPercent) { if (isPercent) {
text = `${alwaysShowSign ? (isNegative ? '- ' : '+ ') : ''}${val} %` text = `${alwaysShowSign ? (isNegative ? '- ' : '+ ') : ''}${isNegative ? val * -1 : val} %`
} else { } else {
if (fiat) { if (fiat) {
unit = getFiatUnit(fiat) unit = getFiatUnit(fiat)
val *= 100
} else if (!unit) { } else if (!unit) {
return '' return ''
} }

3
src/components/base/Pills/index.js

@ -8,8 +8,9 @@ import Box, { Tabbable } from 'components/base/Box'
import BoldToggle from 'components/base/BoldToggle' import BoldToggle from 'components/base/BoldToggle'
type Item = { type Item = {
key: string,
label: string, label: string,
key: string,
value?: any,
} }
type Props = { type Props = {

2
src/components/modals/Receive.js

@ -83,7 +83,7 @@ class ReceiveModal extends PureComponent<Props, State> {
onChange={this.handleChangeInput('amount')} onChange={this.handleChangeInput('amount')}
/> />
</Box> </Box>
<ReceiveBox path={account.path} amount={amount} address={account.address || ''} /> <ReceiveBox account={account} amount={amount} />
</Fragment> </Fragment>
)} )}
<Box horizontal justifyContent="center"> <Box horizontal justifyContent="center">

117
src/helpers/btc.js

@ -2,7 +2,14 @@
import ledger from 'ledger-test-library' import ledger from 'ledger-test-library'
import bitcoin from 'bitcoinjs-lib' import bitcoin from 'bitcoinjs-lib'
import groupBy from 'lodash/groupBy'
import noop from 'lodash/noop' import noop from 'lodash/noop'
import uniqBy from 'lodash/uniqBy'
import type { Transactions } from 'types/common'
const GAP_LIMIT_ADDRESSES = 20
export const networks = [ export const networks = [
{ {
@ -15,36 +22,65 @@ export const networks = [
}, },
] ]
export function computeTransaction(addresses: Array<*>) { export function computeTransaction(addresses: Array<string>) {
return (transaction: Object) => { return (t: Object) => {
const outputVal = transaction.outputs const outputVal = t.outputs
.filter(o => addresses.includes(o.address)) .filter(o => addresses.includes(o.address))
.reduce((acc, cur) => acc + cur.value, 0) .reduce((acc, cur) => acc + cur.value, 0)
const inputVal = transaction.inputs const inputVal = t.inputs
.filter(i => addresses.includes(i.address)) .filter(i => addresses.includes(i.address))
.reduce((acc, cur) => acc + cur.value, 0) .reduce((acc, cur) => acc + cur.value, 0)
const balance = outputVal - inputVal const balance = outputVal - inputVal
return { return {
...transaction, address: t.balance > 0 ? t.inputs[0].address : t.outputs[0].address,
balance, balance,
confirmations: t.confirmations,
hash: t.hash,
receivedAt: t.received_at,
} }
} }
} }
export function getBalanceByDay(transactions: Transactions) {
const txsByDate = groupBy(transactions, tx => {
const [date] = new Date(tx.receivedAt).toISOString().split('T')
return date
})
let balance = 0
return Object.keys(txsByDate)
.sort()
.reduce((result, k) => {
const txs = txsByDate[k]
balance += txs.reduce((r, v) => {
r += v.balance
return r
}, 0)
result[k] = balance
return result
}, {})
}
export async function getAccount({ export async function getAccount({
rootPath,
allAddresses = [], allAddresses = [],
allTxsHash = [],
currentIndex = 0, currentIndex = 0,
path,
hdnode, hdnode,
segwit, segwit,
network, network,
coinType, coinType,
asyncDelay = 500, asyncDelay = 250,
onProgress = noop, onProgress = noop,
}: { }: {
rootPath: string,
allAddresses?: Array<string>, allAddresses?: Array<string>,
allTxsHash?: Array<string>,
currentIndex?: number, currentIndex?: number,
path: string,
hdnode: Object, hdnode: Object,
segwit: boolean, segwit: boolean,
coinType: number, coinType: number,
@ -52,7 +88,6 @@ export async function getAccount({
asyncDelay?: number, asyncDelay?: number,
onProgress?: Function, onProgress?: Function,
}) { }) {
const gapLimit = 20
const script = segwit ? parseInt(network.scriptHash, 10) : parseInt(network.pubKeyHash, 10) const script = segwit ? parseInt(network.scriptHash, 10) : parseInt(network.pubKeyHash, 10)
let balance = 0 let balance = 0
@ -75,28 +110,26 @@ export async function getAccount({
const getPath = (type, index) => `${type === 'external' ? 0 : 1}/${index}` const getPath = (type, index) => `${type === 'external' ? 0 : 1}/${index}`
const getAddress = ({ type, index }) => ({ const getAddress = ({ type, index }) => {
type, const p = getPath(type, index)
index, return {
address: getPublicAddress({ hdnode, path: getPath(type, index), script, segwit }), type,
}) index,
path: `${rootPath}/${p}`,
address: getPublicAddress({ hdnode, path: p, script, segwit }),
}
}
const getAsyncAddress = params => const getAsyncAddress = params =>
new Promise(resolve => setTimeout(() => resolve(getAddress(params)), asyncDelay)) new Promise(resolve => setTimeout(() => resolve(getAddress(params)), asyncDelay))
const getLastAddress = (addresses, txs) => { const getLastAddress = (addresses, txs) => {
const txsAddresses = [...txs.inputs.map(tx => tx.address), ...txs.outputs.map(tx => tx.address)] const txsAddresses = [...txs.inputs.map(t => t.address), ...txs.outputs.map(t => t.address)]
const lastAddress = addresses.reverse().find(a => txsAddresses.includes(a.address)) || { return addresses.find(a => txsAddresses.includes(a.address)) || null
index: 0,
}
return {
...lastAddress,
address: getAddress({ type: 'external', index: lastAddress.index + 1 }).address,
}
} }
const nextPath = (index = 0) => const nextPath = (index = 0) =>
Array.from(new Array(gapLimit).keys()) Array.from(new Array(GAP_LIMIT_ADDRESSES).keys())
.reduce( .reduce(
(promise, v) => (promise, v) =>
promise.then(async results => { promise.then(async results => {
@ -115,41 +148,45 @@ export async function getAccount({
allAddresses = [...new Set([...allAddresses, ...listAddresses])] allAddresses = [...new Set([...allAddresses, ...listAddresses])]
const transactionsOpts = { coin_type: coinType } const transactionsOpts = { coin_type: coinType }
const txs = await ledger.getTransactions(listAddresses, transactionsOpts) let txs = await ledger.getTransactions(listAddresses, transactionsOpts)
txs = txs.filter(t => !allTxsHash.includes(t.hash)).reverse()
const hasTransactions = txs.length > 0 const hasTransactions = txs.length > 0
if (hasTransactions) { if (hasTransactions) {
const newTransactions = txs.map(computeTransaction(allAddresses)) const newTransactions = txs.map(computeTransaction(allAddresses))
const txHashs = transactions.map(t => t.hash)
balance = newTransactions
.filter(t => !txHashs.includes(t.hash))
.reduce((result, v) => result + v.balance, balance)
lastAddress = getLastAddress(addresses, txs[0]) lastAddress = getLastAddress(addresses, txs[0])
transactions = [...transactions, ...newTransactions] transactions = uniqBy([...transactions, ...newTransactions], t => t.hash)
balance = newTransactions.reduce((result, v) => result + v.balance, balance)
onProgress({ onProgress({
balance, balance,
transactions: transactions.length, transactions: transactions.length,
}) })
return nextPath(index + (gapLimit - 1)) return nextPath(index + (GAP_LIMIT_ADDRESSES - 1))
} }
const currentAddress = const { type, ...nextAddress } =
lastAddress !== null ? lastAddress : getAddress({ type: 'external', index: 0 }) lastAddress !== null
? getAddress({
type: 'external',
index: lastAddress.index + 1,
})
: getAddress({ type: 'external', index: 0 })
return { return {
address: currentAddress.address, ...nextAddress,
addresses: allAddresses, addresses: transactions.length > 0 ? allAddresses : [],
balance, balance,
index: currentAddress.index, balanceByDay: getBalanceByDay(transactions),
path: `${path}/${getPath('external', currentAddress.index + 1)}`, rootPath,
transactions: transactions.map(t => ({ transactions,
confirmations: t.confirmations,
address: t.balance > 0 ? t.inputs[0].address : t.outputs[0].address,
balance: t.balance,
hash: t.hash,
receivedAt: t.received_at,
})),
} }
}) })

2
src/helpers/db.js

@ -6,7 +6,7 @@ import get from 'lodash/get'
import { serializeAccounts, deserializeAccounts } from 'reducers/accounts' import { serializeAccounts, deserializeAccounts } from 'reducers/accounts'
type DBKey = 'settings' | 'accounts' type DBKey = 'settings' | 'accounts' | 'counterValues'
const encryptionKey = {} const encryptionKey = {}

13
src/internals/accounts/sync.js

@ -4,12 +4,15 @@ import { getAccount, getHDNode, networks } from 'helpers/btc'
const network = networks[1] const network = networks[1]
function syncAccount({ id, ...currentAccount }) { function syncAccount({ id, transactions, ...currentAccount }) {
const hdnode = getHDNode({ xpub58: id, network }) const hdnode = getHDNode({ xpub58: id, network })
return getAccount({ hdnode, network, segwit: true, ...currentAccount }).then(account => ({ const allTxsHash = transactions.map(t => t.hash)
id, return getAccount({ hdnode, network, allTxsHash, segwit: true, ...currentAccount }).then(
...account, account => ({
})) id,
...account,
}),
)
} }
export default (send: Function) => ({ export default (send: Function) => ({

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

@ -148,7 +148,7 @@ export default async ({
const hdnode = getHDNode({ xpub58, network }) const hdnode = getHDNode({ xpub58, network })
const account = await getAccount({ const account = await getAccount({
path, rootPath: path,
hdnode, hdnode,
coinType, coinType,
network, network,

3
src/middlewares/db.js

@ -15,10 +15,11 @@ export default store => next => action => {
dispatch({ type, payload: action.payload }) dispatch({ type, payload: action.payload })
const state = getState() const state = getState()
const { settings } = state const { settings, counterValues } = state
const accounts = getAccounts(state) const accounts = getAccounts(state)
db.set('settings', settings) db.set('settings', settings)
db.set('accounts', accounts) db.set('accounts', accounts)
db.set('counterValues', counterValues)
} }

17
src/reducers/accounts.js

@ -53,17 +53,18 @@ const handlers: Object = {
return existingAccount return existingAccount
} }
const { transactions, index } = account const { balance, balanceByDay, transactions } = existingAccount
const updatedAccount = { const updatedAccount = {
...existingAccount, ...existingAccount,
...account, ...account,
balance: transactions.reduce((result, v) => { balance: balance + account.balance,
result += v.balance balanceByDay: Object.keys(balanceByDay).reduce((result, k) => {
result[k] = balanceByDay[k] + (account.balanceByDay[k] || 0)
return result return result
}, 0), }, {}),
index: index || get(existingAccount, 'currentIndex', 0), index: account.index || get(existingAccount, 'index', 0),
transactions, transactions: [...transactions, ...account.transactions],
} }
return orderAccountsTransactions(updatedAccount) return orderAccountsTransactions(updatedAccount)
@ -114,11 +115,13 @@ export function serializeAccounts(accounts: Array<Object>) {
address: account.address, address: account.address,
addresses: account.addresses, addresses: account.addresses,
balance: account.balance, balance: account.balance,
balanceByDay: account.balanceByDay,
coinType: account.coinType, coinType: account.coinType,
currency: getCurrencyByCoinType(account.coinType), currency: getCurrencyByCoinType(account.coinType),
index: account.index, index: account.index,
name: account.name || `${key}`, name: account.name || `${key}`,
path: account.path, path: account.path,
rootPath: account.rootPath,
unit: account.unit || getDefaultUnitByCoinType(account.coinType), unit: account.unit || getDefaultUnitByCoinType(account.coinType),
settings: account.settings, settings: account.settings,
} }
@ -139,10 +142,12 @@ export function deserializeAccounts(accounts: Accounts) {
address: account.address, address: account.address,
addresses: account.addresses, addresses: account.addresses,
balance: account.balance, balance: account.balance,
balanceByDay: account.balanceByDay,
coinType: account.coinType, coinType: account.coinType,
index: account.index, index: account.index,
name: account.name, name: account.name,
path: account.path, path: account.path,
rootPath: account.rootPath,
transactions: account.transactions.map(({ account, ...t }) => t), transactions: account.transactions.map(({ account, ...t }) => t),
unit: account.unit, unit: account.unit,
settings: account.settings, settings: account.settings,

19
src/reducers/counterValues.js

@ -0,0 +1,19 @@
// @flow
import { handleActions } from 'redux-actions'
export type CounterValuesState = {}
const state: CounterValuesState = {}
const handlers = {
UPDATE_COUNTER_VALUES: (
state: CounterValuesState,
{ payload: counterValues }: { payload: CounterValuesState },
): CounterValuesState => ({
...state,
...counterValues,
}),
}
export default handleActions(handlers, state)

12
src/reducers/index.js

@ -5,23 +5,26 @@ import { routerReducer as router } from 'react-router-redux'
import type { LocationShape } from 'react-router' import type { LocationShape } from 'react-router'
import application from './application'
import accounts from './accounts' import accounts from './accounts'
import application from './application'
import counterValues from './counterValues'
import devices from './devices' import devices from './devices'
import modals from './modals' import modals from './modals'
import settings from './settings' import settings from './settings'
import update from './update' import update from './update'
import type { ApplicationState } from './application'
import type { AccountsState } from './accounts' import type { AccountsState } from './accounts'
import type { ApplicationState } from './application'
import type { CounterValuesState } from './counterValues'
import type { DevicesState } from './devices' import type { DevicesState } from './devices'
import type { ModalsState } from './modals' import type { ModalsState } from './modals'
import type { SettingsState } from './settings' import type { SettingsState } from './settings'
import type { UpdateState } from './update' import type { UpdateState } from './update'
export type State = { export type State = {
application: ApplicationState,
accounts: AccountsState, accounts: AccountsState,
application: ApplicationState,
counterValues: CounterValuesState,
devices: DevicesState, devices: DevicesState,
modals: ModalsState, modals: ModalsState,
router: LocationShape, router: LocationShape,
@ -30,8 +33,9 @@ export type State = {
} }
export default combineReducers({ export default combineReducers({
application,
accounts, accounts,
application,
counterValues,
devices, devices,
modals, modals,
router, router,

36
src/renderer/events.js

@ -2,8 +2,6 @@
import { ipcRenderer } from 'electron' import { ipcRenderer } from 'electron'
import objectPath from 'object-path' import objectPath from 'object-path'
import get from 'lodash/get'
import uniqBy from 'lodash/uniqBy'
import debug from 'debug' import debug from 'debug'
import type { Accounts } from 'types/common' import type { Accounts } from 'types/common'
@ -53,12 +51,13 @@ export function startSyncAccounts(accounts: Accounts) {
syncAccounts = true syncAccounts = true
sendEvent('accounts', 'sync.all', { sendEvent('accounts', 'sync.all', {
accounts: accounts.map(account => { accounts: accounts.map(account => {
const index = get(account, 'index', 0) const { id, rootPath, addresses, index, transactions } = account
const addresses = get(account, 'addresses', [])
return { return {
id: account.id, id,
allAddresses: addresses, allAddresses: addresses,
currentIndex: index, currentIndex: index,
rootPath,
transactions,
} }
}), }),
}) })
@ -86,22 +85,17 @@ export default ({ store, locked }: { store: Object, locked: boolean }) => {
success: account => { success: account => {
if (syncAccounts) { if (syncAccounts) {
const state = store.getState() const state = store.getState()
const currentAccount = getAccountById(state, account.id) || {} const currentAccount = getAccountById(state, account.id)
const currentAccountTransactions = get(currentAccount, 'transactions', [])
if (!currentAccount) {
const transactions = uniqBy( return
[...currentAccountTransactions, ...account.transactions], }
tx => tx.hash,
) const { name } = currentAccount
if (currentAccountTransactions.length !== transactions.length) { if (account.transactions.length > 0) {
d.sync(`Update account - ${currentAccount.name}`) d.sync(`Update account - ${name}`)
store.dispatch( store.dispatch(updateAccount(account))
updateAccount({
...account,
transactions,
}),
)
} }
} }
}, },

6
src/renderer/init.js

@ -11,6 +11,7 @@ import events from 'renderer/events'
import { fetchAccounts } from 'actions/accounts' import { fetchAccounts } from 'actions/accounts'
import { fetchSettings } from 'actions/settings' import { fetchSettings } from 'actions/settings'
import { initCounterValues, fetchCounterValues } from 'actions/counterValues'
import { isLocked } from 'reducers/application' import { isLocked } from 'reducers/application'
import { getLanguage } from 'reducers/settings' import { getLanguage } from 'reducers/settings'
@ -20,13 +21,15 @@ import App from 'components/App'
import 'styles/global' import 'styles/global'
// Init settings with defaults if needed // Init db with defaults if needed
db.init('settings', {}) db.init('settings', {})
db.init('counterValues', {})
const history = createHistory() const history = createHistory()
const store = createStore(history) const store = createStore(history)
const rootNode = document.getElementById('app') const rootNode = document.getElementById('app')
store.dispatch(initCounterValues())
store.dispatch(fetchSettings()) store.dispatch(fetchSettings())
const state = store.getState() || {} const state = store.getState() || {}
@ -38,6 +41,7 @@ if (!locked) {
db.init('accounts', []) db.init('accounts', [])
store.dispatch(fetchAccounts()) store.dispatch(fetchAccounts())
store.dispatch(fetchCounterValues())
} }
function r(Comp) { function r(Comp) {

6
src/styles/global.js

@ -8,7 +8,7 @@ import '@fortawesome/fontawesome-free-solid'
import '@fortawesome/fontawesome-free-regular' import '@fortawesome/fontawesome-free-regular'
import '@fortawesome/fontawesome-free-brands' import '@fortawesome/fontawesome-free-brands'
import { fontFace, rgba } from 'styles/helpers' import { fontFace } from 'styles/helpers'
import { radii, colors } from 'styles/theme' import { radii, colors } from 'styles/theme'
import reset from './reset' import reset from './reset'
@ -90,11 +90,11 @@ injectGlobal`
${reset}; ${reset};
.tippy-tooltip { .tippy-tooltip {
background-color: ${rgba(colors.dark, 0.8)}; background-color: ${colors.dark};
border-radius: ${radii[1]}px; border-radius: ${radii[1]}px;
} }
.tippy-popper .tippy-roundarrow { .tippy-popper .tippy-roundarrow {
fill: ${rgba(colors.dark, 0.8)}; fill: ${colors.dark};
} }
` `

6
src/types/common.js

@ -21,6 +21,8 @@ export type Transaction = {
confirmations: number, confirmations: number,
} }
export type Transactions = Array<Transaction>
// -------------------- Accounts // -------------------- Accounts
export type AccountSettings = { export type AccountSettings = {
@ -32,13 +34,15 @@ export type Account = {
addresses: Array<string>, addresses: Array<string>,
archived?: boolean, archived?: boolean,
balance: number, balance: number,
balanceByDay: Object,
coinType: number, coinType: number,
currency: Currency, currency: Currency,
id: string, id: string,
index: number, index: number,
name: string, name: string,
path: string, path: string,
transactions: Array<Transaction>, rootPath: string,
transactions: Transactions,
unit: Unit, unit: Unit,
settings: AccountSettings, settings: AccountSettings,
} }

30
yarn.lock

@ -3427,9 +3427,9 @@ dotenv@^5.0.1:
version "5.0.1" version "5.0.1"
resolved "https://registry.yarnpkg.com/dotenv/-/dotenv-5.0.1.tgz#a5317459bd3d79ab88cff6e44057a6a3fbb1fcef" resolved "https://registry.yarnpkg.com/dotenv/-/dotenv-5.0.1.tgz#a5317459bd3d79ab88cff6e44057a6a3fbb1fcef"
downshift@^1.28.1: downshift@^1.28.2:
version "1.28.1" version "1.28.2"
resolved "https://registry.yarnpkg.com/downshift/-/downshift-1.28.1.tgz#8e787eda4e31c8a5519dccaf0e16dd90787720a4" resolved "https://registry.yarnpkg.com/downshift/-/downshift-1.28.2.tgz#ff5b4e89ff439943a8e58890993015199604e1e6"
duplexer3@^0.1.4: duplexer3@^0.1.4:
version "0.1.4" version "0.1.4"
@ -7845,9 +7845,9 @@ preserve@^0.2.0:
version "0.2.0" version "0.2.0"
resolved "https://registry.yarnpkg.com/preserve/-/preserve-0.2.0.tgz#815ed1f6ebc65926f865b310c0713bcb3315ce4b" resolved "https://registry.yarnpkg.com/preserve/-/preserve-0.2.0.tgz#815ed1f6ebc65926f865b310c0713bcb3315ce4b"
prettier@^1.11.0: prettier@^1.11.1:
version "1.11.0" version "1.11.1"
resolved "https://registry.yarnpkg.com/prettier/-/prettier-1.11.0.tgz#c024f70cab158c993f50fc0c25ffe738cb8b0f85" resolved "https://registry.yarnpkg.com/prettier/-/prettier-1.11.1.tgz#61e43fc4cd44e68f2b0dfc2c38cd4bb0fccdcc75"
pretty-bytes@^1.0.2: pretty-bytes@^1.0.2:
version "1.0.4" version "1.0.4"
@ -8089,13 +8089,13 @@ range-parser@^1.0.3, range-parser@~1.2.0:
version "1.2.0" version "1.2.0"
resolved "https://registry.yarnpkg.com/range-parser/-/range-parser-1.2.0.tgz#f49be6b487894ddc40dcc94a322f611092e00d5e" resolved "https://registry.yarnpkg.com/range-parser/-/range-parser-1.2.0.tgz#f49be6b487894ddc40dcc94a322f611092e00d5e"
raven-js@^3.22.3: raven-js@^3.22.4:
version "3.22.3" version "3.22.4"
resolved "https://registry.yarnpkg.com/raven-js/-/raven-js-3.22.3.tgz#8330dcc102b699ffbc2f48790978b997bf4d8f75" resolved "https://registry.yarnpkg.com/raven-js/-/raven-js-3.22.4.tgz#e5ac2aef7cdbbe639eef0db04703e99b6a0bcb28"
raven@^2.4.1: raven@^2.4.2:
version "2.4.1" version "2.4.2"
resolved "https://registry.yarnpkg.com/raven/-/raven-2.4.1.tgz#7a6a6ff1c42d0a3892308f44c94273e7f88677fd" resolved "https://registry.yarnpkg.com/raven/-/raven-2.4.2.tgz#0129e2adc30788646fd530b67d08a8ce25d4f6dc"
dependencies: dependencies:
cookie "0.3.1" cookie "0.3.1"
md5 "^2.2.1" md5 "^2.2.1"
@ -8170,9 +8170,9 @@ react-fuzzy@^0.5.1:
fuse.js "^3.0.1" fuse.js "^3.0.1"
prop-types "^15.5.9" prop-types "^15.5.9"
react-hot-loader@^4.0.0-beta.21: react-hot-loader@^4.0.0:
version "4.0.0-rc.0" version "4.0.0"
resolved "https://registry.yarnpkg.com/react-hot-loader/-/react-hot-loader-4.0.0-rc.0.tgz#54d931dafeface5119c741d44ccc3e75fbb432e8" resolved "https://registry.yarnpkg.com/react-hot-loader/-/react-hot-loader-4.0.0.tgz#3452fa9bc0d0ba9dfc5b0ccfa25101ca8dbd2de2"
dependencies: dependencies:
fast-levenshtein "^2.0.6" fast-levenshtein "^2.0.6"
global "^4.3.0" global "^4.3.0"

Loading…
Cancel
Save