Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Agent: replicate rate conversion strategy from #1177 #1200

Merged
merged 3 commits into from
Jul 14, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions apps/agent/app/src/components/BalanceToken.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import BN from 'bn.js'
import { GU, Help, formatTokenAmount, textStyle, useTheme } from '@aragon/ui'
import { useNetwork } from '@aragon/api-react'
import { tokenIconUrl } from '../lib/icon-utils'

function BalanceToken({
address,
amount,
Expand All @@ -15,13 +16,15 @@ function BalanceToken({
}) {
const theme = useTheme()
const network = useNetwork()

const amountFormatted = formatTokenAmount(amount, decimals, {
digits: decimals,
})
const amountFormattedRounded = formatTokenAmount(amount, decimals, {
digits: 3,
})
const amountWasRounded = amountFormatted !== amountFormattedRounded

return (
<div css="display: inline-block">
<div
Expand Down
62 changes: 6 additions & 56 deletions apps/agent/app/src/components/Balances.js
Original file line number Diff line number Diff line change
@@ -1,58 +1,9 @@
import React, { useEffect, useMemo, useState } from 'react'
import React, { useMemo } from 'react'
import BN from 'bn.js'
import { Box, GU, textStyle, useTheme, useLayout } from '@aragon/ui'
import BalanceToken from './BalanceToken'

const CONVERT_API_RETRY_DELAY = 2000

function convertRatesUrl(symbolsQuery) {
return `https://min-api.cryptocompare.com/data/price?fsym=USD&tsyms=${symbolsQuery}`
}

function useConvertRates(symbols) {
const [rates, setRates] = useState({})

const symbolsQuery = symbols.join(',')

useEffect(() => {
let cancelled = false
let retryTimer = null

const update = async () => {
if (!symbolsQuery) {
setRates({})
return
}

try {
const response = await fetch(convertRatesUrl(symbolsQuery))
const rates = await response.json()
if (!cancelled) {
setRates(rates)
}
} catch (err) {
// The !cancelled check is needed in case:
// 1. The fetch() request is ongoing.
// 2. The component gets unmounted.
// 3. An error gets thrown.
//
// Assuming the fetch() request keeps throwing, it would create new
// requests even though the useEffect() got cancelled.
if (!cancelled) {
retryTimer = setTimeout(update, CONVERT_API_RETRY_DELAY)
}
}
}
update()

return () => {
cancelled = true
clearTimeout(retryTimer)
}
}, [symbolsQuery])

return rates
}
import { getConvertedAmount } from '../lib/conversion-utils'
import { useConvertRates } from './useConvertRates'

// Prepare the balances for the BalanceToken component
function useBalanceItems(balances) {
Expand All @@ -67,14 +18,13 @@ function useBalanceItems(balances) {
address,
amount,
convertedAmount: convertRates[symbol]
? amount.divn(convertRates[symbol])
: new BN(-1),
? getConvertedAmount(amount, convertRates[symbol])
: new BN('-1'),
decimals,
symbol,
verified,
}))
}, [balances, convertRates])

return balanceItems
}

Expand All @@ -89,7 +39,7 @@ function Balances({ balances }) {
<Box heading="Token Balances" padding={0}>
<div
css={`
padding: ${(layoutName === 'small' ? 1 : 2) * GU}px;
padding: ${(compact ? 1 : 2) * GU}px;
`}
>
<div
Expand Down
61 changes: 61 additions & 0 deletions apps/agent/app/src/components/useConvertRates.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
import { useEffect, useState, useRef } from 'react'

const CONVERT_API_RETRY_DELAY = 2 * 1000
const CONVERT_API_RETRY_DELAY_MAX = 60 * 1000

function convertRatesUrl(symbolsQuery) {
return `https://min-api.cryptocompare.com/data/price?fsym=USD&tsyms=${symbolsQuery}`
}

export function useConvertRates(symbols) {
const [rates, setRates] = useState({})
const retryDelay = useRef(CONVERT_API_RETRY_DELAY)

const symbolsQuery = symbols.join(',')

useEffect(() => {
let cancelled = false
let retryTimer = null

const update = async () => {
if (!symbolsQuery) {
setRates({})
return
}

try {
const response = await fetch(convertRatesUrl(symbolsQuery))
const rates = await response.json()
if (!cancelled) {
setRates(rates)
retryDelay.current = CONVERT_API_RETRY_DELAY
}
} catch (err) {
// The !cancelled check is needed in case:
// 1. The fetch() request is ongoing.
// 2. The component gets unmounted.
// 3. An error gets thrown.
//
// Assuming the fetch() request keeps throwing, it would create new
// requests even though the useEffect() got cancelled.
if (!cancelled) {
// Add more delay after every failed attempt
retryDelay.current = Math.min(
CONVERT_API_RETRY_DELAY_MAX,
retryDelay.current * 1.2
)
retryTimer = setTimeout(update, retryDelay.current)
}
}
}
update()

return () => {
cancelled = true
clearTimeout(retryTimer)
retryDelay.current = CONVERT_API_RETRY_DELAY
}
}, [symbolsQuery])

return rates
}
16 changes: 16 additions & 0 deletions apps/agent/app/src/lib/conversion-utils.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import BN from 'bn.js'

export function getConvertedAmount(amount, convertRate) {
const [whole = '', dec = ''] = convertRate.toString().split('.')
// Remove any trailing zeros from the decimal part
const parsedDec = dec.replace(/0*$/, '')
// Construct the final rate, and remove any leading zeros
const rate = `${whole}${parsedDec}`.replace(/^0*/, '')

// Number of decimals to shift the amount of the token passed in,
// resulting from converting the rate to a number without any decimal
// places
const carryAmount = new BN(parsedDec.length.toString())

return amount.mul(new BN('10').pow(carryAmount)).div(new BN(rate))
}
29 changes: 29 additions & 0 deletions apps/agent/app/src/lib/conversion-utils.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import BN from 'bn.js'
import { getConvertedAmount } from './conversion-utils'

const ONE_ETH = new BN('10').pow(new BN('18'))

describe('getConvertedAmount tests', () => {
test('Converts amounts correctly', () => {
expect(getConvertedAmount(new BN('1'), 1).toString()).toEqual('1')
expect(getConvertedAmount(new BN(ONE_ETH), 1).toString()).toEqual(
ONE_ETH.toString()
)
expect(getConvertedAmount(new BN('1'), 0.5).toString()).toEqual('2')
expect(getConvertedAmount(new BN('1'), 0.25).toString()).toEqual('4')
expect(getConvertedAmount(new BN('1'), 0.125).toString()).toEqual('8')

expect(getConvertedAmount(new BN('100'), 50).toString()).toEqual('2')
// This is the exact case that broke the previous implementation,
// which is AAVE's amount of WBTC + the exchange rate at a certain
// hour on 2020-06-24
expect(
getConvertedAmount(new BN('1145054'), 0.00009248).toString()
).toEqual('12381639273')
})

test('Throws on invalid inputs', () => {
expect(() => getConvertedAmount(new BN('1'), 0)).toThrow()
expect(() => getConvertedAmount('1000', 0)).toThrow()
})
})