Gaëtan Renaudeau
7 years ago
49 changed files with 654 additions and 752 deletions
@ -1,15 +1,18 @@ |
|||
import { genStoreState } from '@ledgerhq/live-common/lib/countervalues/mock' |
|||
import { |
|||
getCryptoCurrencyById, |
|||
getFiatCurrencyByTicker, |
|||
} from '@ledgerhq/live-common/lib/helpers/currencies' |
|||
|
|||
export default { |
|||
counterValues: { |
|||
BTC: { |
|||
USD: { |
|||
'2018-01-09': 0.00795978, |
|||
'2018-03-29': 0.007106619999999999, |
|||
'2018-03-30': 0.0068537599999999995, |
|||
'2018-03-31': 0.00694377, |
|||
'2018-04-01': 0.00683584, |
|||
'2018-04-02': 0.007061689999999999, |
|||
latest: 0.00706156, |
|||
}, |
|||
countervalues: genStoreState([ |
|||
{ |
|||
from: getCryptoCurrencyById('bitcoin'), |
|||
to: getFiatCurrencyByTicker('USD'), |
|||
exchange: 'KRAKEN', |
|||
dateFrom: new Date(2015, 1, 1), |
|||
dateTo: new Date(), |
|||
rate: d => 0.007 + 0.003 * Math.max(0, (d / 1e12 + Math.sin(d / 1e9)) / 2), |
|||
}, |
|||
}, |
|||
]), |
|||
} |
|||
|
@ -1,43 +0,0 @@ |
|||
// @flow
|
|||
|
|||
import type { Dispatch } from 'redux' |
|||
import { fetchHistodayRates } from '@ledgerhq/live-common/lib/api/countervalue' |
|||
import type { CryptoCurrency, Currency } from '@ledgerhq/live-common/lib/types' |
|||
|
|||
import { counterValueCurrencySelector } from 'reducers/settings' |
|||
import { currenciesSelector } from 'reducers/accounts' |
|||
import db from 'helpers/db' |
|||
import type { State } from 'reducers' |
|||
|
|||
export type InitCounterValues = () => { type: string, payload: Object } |
|||
export const initCounterValues: InitCounterValues = () => ({ |
|||
type: 'UPDATE_COUNTER_VALUES', |
|||
payload: db.get('counterValues'), |
|||
}) |
|||
|
|||
export type UpdateCounterValues = Object => { type: string, payload: Object } |
|||
export const updateCounterValues: UpdateCounterValues = payload => ({ |
|||
type: 'DB:UPDATE_COUNTER_VALUES', |
|||
payload, |
|||
}) |
|||
|
|||
function cryptoCurrenciesToCurrencies(currencies: CryptoCurrency[]): Currency[] { |
|||
// $FlowFixMe this function is just to fix flow types. array contravariant issue.
|
|||
return currencies |
|||
} |
|||
|
|||
export type FetchCounterValues = ( |
|||
currencies?: Currency[], |
|||
) => (Dispatch<*>, () => State) => Promise<any> |
|||
|
|||
export const fetchCounterValues: FetchCounterValues = currencies => async (dispatch, getState) => { |
|||
const state = getState() |
|||
const currency = counterValueCurrencySelector(state) |
|||
if (!currency) return |
|||
if (!currencies) { |
|||
// TODO this should be default, there is no need to provide the currencies in parameter
|
|||
currencies = cryptoCurrenciesToCurrencies(currenciesSelector(state)) |
|||
} |
|||
const counterValues = await fetchHistodayRates(currencies, currency) |
|||
dispatch(updateCounterValues(counterValues)) |
|||
} |
@ -1,63 +0,0 @@ |
|||
// @flow
|
|||
|
|||
import React from 'react' |
|||
|
|||
import render from '__mocks__/render' |
|||
import CounterValue from '..' |
|||
|
|||
describe('components', () => { |
|||
describe('CounterValue', () => { |
|||
it('basic', () => { |
|||
const state = { counterValues: { BTC: { USD: { latest: 10e2 } } } } |
|||
const component = <CounterValue ticker="BTC" value={1} /> |
|||
const tree = render(component, state) |
|||
expect(tree).toMatchSnapshot() |
|||
}) |
|||
|
|||
it('specifying ticker different from default', () => { |
|||
const state = { counterValues: { LOL: { USD: { latest: 5e2 } } } } |
|||
const component = <CounterValue ticker="LOL" value={1} /> |
|||
const tree = render(component, state) |
|||
expect(tree).toMatchSnapshot() |
|||
}) |
|||
|
|||
it('using countervalue different from default', () => { |
|||
const state = { |
|||
counterValues: { BTC: { EUR: { latest: 42 } } }, |
|||
settings: { |
|||
counterValue: 'EUR', |
|||
}, |
|||
} |
|||
const component = <CounterValue ticker="BTC" value={1} /> |
|||
const tree = render(component, state) |
|||
expect(tree).toMatchSnapshot() |
|||
}) |
|||
|
|||
it('without countervalues populated', () => { |
|||
const state = { counterValues: {} } |
|||
const component = <CounterValue ticker="BTC" value={1} /> |
|||
const tree = render(component, state) |
|||
expect(tree).toMatchSnapshot() |
|||
}) |
|||
|
|||
it('with time travel whith date in countervalues', () => { |
|||
const state = { counterValues: { BTC: { USD: { '2018-01-01': 20e2 } } } } |
|||
|
|||
const date = new Date('2018-01-01') |
|||
const component = <CounterValue ticker="BTC" value={1} date={date} /> |
|||
const tree = render(component, state) |
|||
expect(tree).toMatchSnapshot() |
|||
}) |
|||
|
|||
it('with time travel whith date not in countervalues', () => { |
|||
const state = { counterValues: { BTC: { USD: { '2018-01-01': 20e2 } } } } |
|||
const date = new Date('2018-01-02') |
|||
const component = <CounterValue ticker="BTC" value={1} date={date} /> |
|||
const tree = render(component, state) |
|||
|
|||
// TODO: actually it returns 0 when date is not in countervalues
|
|||
// do we want to use closest past value instead?
|
|||
expect(tree).toMatchSnapshot() |
|||
}) |
|||
}) |
|||
}) |
@ -1,55 +0,0 @@ |
|||
// Jest Snapshot v1, https://goo.gl/fbAQLP |
|||
|
|||
exports[`components CounterValue basic 1`] = ` |
|||
<div |
|||
className="k45ou1-0 iqaJGf e345n3-0 bJwMKx" |
|||
color="#66be54" |
|||
> |
|||
+ USD 10.00 |
|||
</div> |
|||
`; |
|||
|
|||
exports[`components CounterValue specifying ticker different from default 1`] = ` |
|||
<div |
|||
className="k45ou1-0 iqaJGf e345n3-0 bJwMKx" |
|||
color="#66be54" |
|||
> |
|||
+ USD 5.00 |
|||
</div> |
|||
`; |
|||
|
|||
exports[`components CounterValue using countervalue different from default 1`] = ` |
|||
<div |
|||
className="k45ou1-0 iqaJGf e345n3-0 huzgmt" |
|||
color={undefined} |
|||
> |
|||
+ EUR 0.42 |
|||
</div> |
|||
`; |
|||
|
|||
exports[`components CounterValue with time travel whith date in countervalues 1`] = ` |
|||
<div |
|||
className="k45ou1-0 iqaJGf e345n3-0 bJwMKx" |
|||
color="#66be54" |
|||
> |
|||
+ USD 20.00 |
|||
</div> |
|||
`; |
|||
|
|||
exports[`components CounterValue with time travel whith date not in countervalues 1`] = ` |
|||
<div |
|||
className="k45ou1-0 iqaJGf e345n3-0 bJwMKx" |
|||
color="#66be54" |
|||
> |
|||
+ USD 0.00 |
|||
</div> |
|||
`; |
|||
|
|||
exports[`components CounterValue without countervalues populated 1`] = ` |
|||
<div |
|||
className="k45ou1-0 iqaJGf e345n3-0 bJwMKx" |
|||
color="#66be54" |
|||
> |
|||
+ USD 0.00 |
|||
</div> |
|||
`; |
@ -0,0 +1,17 @@ |
|||
// @flow
|
|||
import React, { PureComponent } from 'react' |
|||
import FormattedVal from 'components/base/FormattedVal' |
|||
|
|||
class DeltaChange extends PureComponent<{ |
|||
from: number, |
|||
to: number, |
|||
}> { |
|||
render() { |
|||
const { from, to, ...rest } = this.props |
|||
const val = from ? Math.floor((to - from) / from * 100) : 0 |
|||
// TODO in future, we also want to diverge rendering when the % is way too high (this can easily happen)
|
|||
return <FormattedVal isPercent val={val} {...rest} /> |
|||
} |
|||
} |
|||
|
|||
export default DeltaChange |
@ -0,0 +1,110 @@ |
|||
// @flow
|
|||
import React, { Component } from 'react' |
|||
import type { Currency } from '@ledgerhq/live-common/lib/types' |
|||
import type { Exchange } from '@ledgerhq/live-common/lib/countervalues/types' |
|||
import Select from 'components/base/Select' |
|||
import Box from 'components/base/Box' |
|||
import Text from 'components/base/Text' |
|||
import CounterValues from 'helpers/countervalues' |
|||
|
|||
const renderItem = ex => ( |
|||
<Box grow horizontal alignItems="center" flow={2}> |
|||
<Text ff="Open Sans|SemiBold" color="dark" fontSize={4}> |
|||
{ex.name} |
|||
</Text> |
|||
</Box> |
|||
) |
|||
|
|||
class ExchangeSelect extends Component< |
|||
{ |
|||
from: Currency, |
|||
to: Currency, |
|||
exchangeId: string, |
|||
onChange: (?Exchange) => void, |
|||
style?: *, |
|||
}, |
|||
{ |
|||
prevFromTo: string, |
|||
exchanges: ?(Exchange[]), |
|||
error: ?Error, |
|||
}, |
|||
> { |
|||
static getDerivedStateFromProps(nextProps: *, prevState: *) { |
|||
const fromTo = `${nextProps.from.ticker}/${nextProps.to.ticker}` |
|||
if (fromTo !== prevState.prevFromTo) { |
|||
return { |
|||
prevFromTo: fromTo, |
|||
exchanges: null, |
|||
} |
|||
} |
|||
return null |
|||
} |
|||
|
|||
state = { |
|||
prevFromTo: '', // eslint-disable-line
|
|||
exchanges: null, |
|||
error: null, |
|||
} |
|||
|
|||
componentDidMount() { |
|||
this._load() |
|||
} |
|||
|
|||
componentDidUpdate() { |
|||
if (this.state.exchanges === null) { |
|||
this._load() |
|||
} |
|||
} |
|||
|
|||
componentWillUnmount() { |
|||
this._unmounted = true |
|||
} |
|||
_unmounted = false |
|||
|
|||
_loadId = 0 |
|||
async _load() { |
|||
this._loadId++ |
|||
if (this._unmounted) return |
|||
this.setState({ exchanges: [] }) |
|||
const { _loadId } = this |
|||
const { from, to } = this.props |
|||
try { |
|||
const exchanges = await CounterValues.fetchExchangesForPair(from, to) |
|||
if (!this._unmounted && this._loadId === _loadId) { |
|||
this.setState({ exchanges }) |
|||
} |
|||
} catch (error) { |
|||
console.error(error) |
|||
if (!this._unmounted && this._loadId === _loadId) { |
|||
this.setState({ error }) |
|||
} |
|||
} |
|||
} |
|||
|
|||
render() { |
|||
const { onChange, exchangeId, style } = this.props |
|||
const { exchanges, error } = this.state |
|||
return exchanges && exchanges.length > 0 ? ( |
|||
<Select |
|||
style={style} |
|||
value={exchanges.find(e => e.id === exchangeId)} |
|||
renderSelected={renderItem} |
|||
renderItem={renderItem} |
|||
keyProp="id" |
|||
items={exchanges} |
|||
fontSize={4} |
|||
onChange={onChange} |
|||
/> |
|||
) : error ? ( |
|||
<Text ff="Open Sans|SemiBold" color="dark" fontSize={4}> |
|||
Failed to load. |
|||
</Text> |
|||
) : ( |
|||
<Text ff="Open Sans|SemiBold" color="dark" fontSize={4}> |
|||
Loading... |
|||
</Text> |
|||
) |
|||
} |
|||
} |
|||
|
|||
export default ExchangeSelect |
@ -1,166 +0,0 @@ |
|||
// @flow
|
|||
|
|||
import moment from 'moment' |
|||
import get from 'lodash/get' |
|||
import type { Account } from '@ledgerhq/live-common/lib/types' |
|||
|
|||
import find from 'lodash/find' |
|||
import first from 'lodash/first' |
|||
import isUndefined from 'lodash/isUndefined' |
|||
import last from 'lodash/last' |
|||
|
|||
type DateInterval = { |
|||
start: string, |
|||
end: string, |
|||
} |
|||
|
|||
type BalanceHistoryDay = { |
|||
date: string, |
|||
balance: number, |
|||
} |
|||
|
|||
type CalculateBalance = { |
|||
accounts: Account[], |
|||
counterValue: string, |
|||
counterValues: Object, |
|||
daysCount: number, |
|||
} |
|||
|
|||
// Map the given date interval
|
|||
// iteratee is given day, index, and currently constructed array
|
|||
// (exactly like Array.map)
|
|||
function mapInterval(iv: DateInterval, cb: Function) { |
|||
const res = [] |
|||
let startDate = moment(iv.start) |
|||
let i = 0 |
|||
const endDate = moment(iv.end) |
|||
res.push(cb(startDate.format('YYYY-MM-DD'), i, res)) |
|||
while (!startDate.isSame(endDate, 'day')) { |
|||
startDate = startDate.add(1, 'day') |
|||
res.push(cb(startDate.format('YYYY-MM-DD'), ++i, res)) |
|||
} |
|||
return res |
|||
} |
|||
|
|||
function getBalanceAtIntervalStart(account: Account, interval: DateInterval): number | null { |
|||
const target = moment(interval.start) |
|||
let res = 0 |
|||
for (const i in account.balanceByDay) { |
|||
if (account.balanceByDay.hasOwnProperty(i)) { |
|||
const d = moment(i) |
|||
if (!d.isBefore(target, 'day')) { |
|||
break |
|||
} |
|||
res = account.balanceByDay[i] || 0 |
|||
} |
|||
} |
|||
return res |
|||
} |
|||
|
|||
export function getBalanceHistoryForAccount({ |
|||
account, |
|||
counterValue, |
|||
counterValues, |
|||
interval, |
|||
}: { |
|||
counterValue: string, |
|||
account: Account, |
|||
counterValues: Object, |
|||
interval: DateInterval, |
|||
}): Array<BalanceHistoryDay> { |
|||
const todayDate = moment().format('YYYY-MM-DD') |
|||
const { unit } = account |
|||
const counterVals = get(counterValues, `${unit.code}.${counterValue}`) |
|||
let lastBalance = getBalanceAtIntervalStart(account, interval) |
|||
return mapInterval(interval, date => { |
|||
let balance = 0 |
|||
|
|||
if (!counterVals) { |
|||
return { balance, date } |
|||
} |
|||
|
|||
const isToday = date === todayDate |
|||
const counterVal = isToday |
|||
? counterVals.latest || counterVals[date] || 0 |
|||
: counterVals[date] || 0 |
|||
|
|||
// if we don't have data on account balance for that day,
|
|||
// we take the prev day
|
|||
if (isUndefined(account.balanceByDay[date])) { |
|||
balance = lastBalance === null ? 0 : lastBalance * counterVal |
|||
} else { |
|||
const b = account.balanceByDay[date] |
|||
lastBalance = b |
|||
balance = b * counterVal |
|||
} |
|||
|
|||
if (isNaN(balance)) { |
|||
console.warn(`This should not happen. Cant calculate balance for ${date}`) // eslint-disable-line no-console
|
|||
return { date, balance: 0 } |
|||
} |
|||
|
|||
return { date, balance } |
|||
}) |
|||
} |
|||
|
|||
export function getBalanceHistoryForAccounts({ |
|||
accounts, |
|||
counterValue, |
|||
counterValues, |
|||
interval, |
|||
}: { |
|||
counterValue: string, |
|||
accounts: Account[], |
|||
counterValues: Object, |
|||
interval: DateInterval, |
|||
}): Array<BalanceHistoryDay> { |
|||
// calculate balance history for each account on the given interval
|
|||
const balances = accounts.map(account => |
|||
getBalanceHistoryForAccount({ |
|||
counterValue, |
|||
account, |
|||
counterValues, |
|||
interval, |
|||
}), |
|||
) |
|||
|
|||
// if more than one account, addition all balances, day by day
|
|||
// and returns a big summed up array
|
|||
return balances.length > 1 |
|||
? balances[0].map((item, i) => { |
|||
let b = item.balance |
|||
for (let j = 1; j < balances.length; j++) { |
|||
b += balances[j][i].balance |
|||
} |
|||
return { ...item, balance: b } |
|||
}) |
|||
: balances.length > 0 |
|||
? balances[0] |
|||
: [] |
|||
} |
|||
|
|||
export default function calculateBalance(props: CalculateBalance) { |
|||
const interval = { |
|||
start: moment() |
|||
.subtract(props.daysCount, 'days') |
|||
.format('YYYY-MM-DD'), |
|||
end: moment().format('YYYY-MM-DD'), |
|||
} |
|||
|
|||
const allBalances = getBalanceHistoryForAccounts({ |
|||
counterValue: props.counterValue, |
|||
accounts: props.accounts, |
|||
counterValues: props.counterValues, |
|||
interval, |
|||
}).map(e => ({ date: e.date, value: e.balance })) |
|||
|
|||
const firstNonEmptyDay = find(allBalances, e => e.value) |
|||
const refBalance = firstNonEmptyDay ? firstNonEmptyDay.value : 0 |
|||
|
|||
return { |
|||
allBalances, |
|||
totalBalance: last(allBalances).value, |
|||
sinceBalance: first(allBalances).value, |
|||
refBalance, |
|||
} |
|||
} |
@ -0,0 +1,49 @@ |
|||
// @flow
|
|||
|
|||
import { createSelector } from 'reselect' |
|||
import createCounterValues from '@ledgerhq/live-common/lib/countervalues' |
|||
import { setExchangePairsAction } from 'actions/settings' |
|||
import { currenciesSelector } from 'reducers/accounts' |
|||
import { counterValueCurrencySelector, currencySettingsSelector } from 'reducers/settings' |
|||
|
|||
const pairsSelector = createSelector( |
|||
currenciesSelector, |
|||
counterValueCurrencySelector, |
|||
state => state, |
|||
(currencies, counterValueCurrency, state) => |
|||
currencies.map(currency => ({ |
|||
from: currency, |
|||
to: counterValueCurrency, |
|||
exchange: currencySettingsSelector(state, currency).exchange, |
|||
})), |
|||
) |
|||
|
|||
const addExtraPollingHooks = (schedulePoll, cancelPoll) => { |
|||
// TODO hook to net info of Electron ? retrieving network should trigger a poll
|
|||
|
|||
// provide a basic mecanism to stop polling when you leave the tab
|
|||
// & immediately poll when you come back.
|
|||
function onWindowBlur() { |
|||
cancelPoll() |
|||
} |
|||
function onWindowFocus() { |
|||
schedulePoll(1000) |
|||
} |
|||
window.addEventListener('blur', onWindowBlur) |
|||
window.addEventListener('focus', onWindowFocus) |
|||
|
|||
return () => { |
|||
window.removeEventListener('blur', onWindowBlur) |
|||
window.removeEventListener('focus', onWindowFocus) |
|||
} |
|||
} |
|||
|
|||
const CounterValues = createCounterValues({ |
|||
getAPIBaseURL: () => 'https://ledger-countervalue-poc.herokuapp.com', |
|||
storeSelector: state => state.countervalues, |
|||
pairsSelector, |
|||
setExchangePairsAction, |
|||
addExtraPollingHooks, |
|||
}) |
|||
|
|||
export default CounterValues |
@ -1,14 +0,0 @@ |
|||
// @flow
|
|||
|
|||
import { fetchCurrentRates } from '@ledgerhq/live-common/lib/api/countervalue' |
|||
|
|||
type SendFunction = (type: string, data: *) => void |
|||
|
|||
export default async (send: SendFunction, { counterValue, currencies }: Object) => { |
|||
try { |
|||
const data = await fetchCurrentRates(currencies, counterValue) |
|||
send('counterValues.update', data) |
|||
} catch (err) { |
|||
console.error(err) // eslint-disable-line no-console
|
|||
} |
|||
} |
@ -1,41 +1,22 @@ |
|||
// @flow
|
|||
|
|||
import { handleActions } from 'redux-actions' |
|||
import merge from 'lodash/merge' |
|||
import get from 'lodash/get' |
|||
import { |
|||
makeCalculateCounterValue, |
|||
makeReverseCounterValue, |
|||
formatCounterValueDay, |
|||
} from '@ledgerhq/live-common/lib/helpers/countervalue' |
|||
import CounterValues from 'helpers/countervalues' |
|||
|
|||
import type { CalculateCounterValue } from '@ledgerhq/live-common/lib/types' |
|||
import type { Currency } from '@ledgerhq/live-common/lib/types' |
|||
import type { State } from 'reducers' |
|||
|
|||
export type CounterValuesState = {} |
|||
const state: CounterValuesState = {} |
|||
|
|||
const handlers = { |
|||
UPDATE_COUNTER_VALUES: (state, { payload: counterValues }) => merge({ ...state }, counterValues), |
|||
} |
|||
|
|||
const getPairHistory = state => (coinTicker, fiat) => { |
|||
const byDate = get(state, `counterValues.${coinTicker}.${fiat}`) |
|||
return date => { |
|||
if (!byDate) { |
|||
return 0 |
|||
} |
|||
if (!date) { |
|||
return byDate.latest || 0 |
|||
} |
|||
return byDate[formatCounterValueDay(date)] || 0 |
|||
} |
|||
} |
|||
|
|||
export const calculateCounterValueSelector = (state: State): CalculateCounterValue => |
|||
makeCalculateCounterValue(getPairHistory(state)) |
|||
|
|||
export const reverseCounterValueSelector = (state: State): CalculateCounterValue => |
|||
makeReverseCounterValue(getPairHistory(state)) |
|||
|
|||
export default handleActions(handlers, state) |
|||
// FIXME DEPRECATED approach. we will move to use calculateSelector everywhere.. it's just easier to process for now.
|
|||
|
|||
export const calculateCounterValueSelector = (state: State) => ( |
|||
from: Currency, |
|||
to: Currency, |
|||
exchange: string, |
|||
) => (value: number, date?: Date, disableRounding?: boolean): ?number => |
|||
CounterValues.calculateSelector(state, { from, to, exchange, value, date, disableRounding }) |
|||
|
|||
export const reverseCounterValueSelector = (state: State) => ( |
|||
from: Currency, |
|||
to: Currency, |
|||
exchange: string, |
|||
) => (value: number, date?: Date, disableRounding?: boolean): ?number => |
|||
CounterValues.reverseSelector(state, { from, to, exchange, value, date, disableRounding }) |
|||
|
Loading…
Reference in new issue