Skip to content

Commit

Permalink
Agent: replicate rate conversion strategy from aragon#1177 (aragon#1200)
Browse files Browse the repository at this point in the history
* Agent: replicate rate conversion strategy from aragon#1177

* Remove unused parameter

* Remove redundant return on map :)
  • Loading branch information
Evalir authored Jul 14, 2020
1 parent 09f358f commit ef46458
Show file tree
Hide file tree
Showing 5 changed files with 115 additions and 56 deletions.
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()
})
})

0 comments on commit ef46458

Please sign in to comment.