Skip to content
This repository has been archived by the owner on Jun 30, 2022. It is now read-only.

Commit

Permalink
Merge pull request #18 from celo-org/aaronmgdr/loading
Browse files Browse the repository at this point in the history
better data loading
  • Loading branch information
aaronmgdr authored Mar 15, 2021
2 parents a9f6b8b + 62fe573 commit 31cf909
Show file tree
Hide file tree
Showing 12 changed files with 177 additions and 59 deletions.
11 changes: 10 additions & 1 deletion next.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -39,12 +39,21 @@ module.exports = {
},
],
},
{
source: '/api/holdings/:kind',
headers: [
{
key: "Cache-Control",
value: "public; max-age=30, stale-while-revalidate=360",
},
],
},
{
source: '/api/:any',
headers: [
{
key: "Cache-Control",
value: "public; max-age=10, stale-while-revalidate=20",
value: "public; max-age=5, stale-while-revalidate=20",
},
],
},
Expand Down
14 changes: 9 additions & 5 deletions src/components/Amount.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { css } from '@emotion/react'
import { BreakPoints } from 'src/components/styles'
import colors from './colors'
import { loadingStyle } from './loadingKeyframes'
interface AmountProps {
label: string
units: number
Expand All @@ -20,7 +21,7 @@ export default function Amount({iconSrc, label, units, gridArea, context, value,
{iconSrc && <img width={30} height={30} src={iconSrc} css={iconStyle} alt="" />}
<p id={id} css={labelCss}><abbr css={abbrCSS} title={context}>{label}</abbr></p>
</div>
<span aria-labelledby={id} css={css(!loading && value ? secondaryNumberStyle : numberStyle, loading && notShowing )}>{display}</span>
<span aria-labelledby={id} css={css(!loading && value ? secondaryNumberStyle : numberStyle, loading && amountLoadingStyle )}>{display}</span>
<DollarDisplay css={primaryNumberStyle} value={value} loading={loading} label={label} />
</div>
)
Expand All @@ -36,7 +37,7 @@ interface DollarDisplayProps {
export function DollarDisplay({value, loading, label, className}: DollarDisplayProps) {
const displayValue = value && Math.round(value).toLocaleString()

return <span className={className} aria-label={`Value of ${label} in USD`} css={css(dollarValueStyle, loading && notShowing)}>
return <span className={className} aria-label={`Value of ${label} in USD`} css={css(dollarValueStyle, loading && amountLoadingStyle)}>
{loading ? " ": !!value && `$${displayValue}`}
</span>
}
Expand All @@ -46,11 +47,14 @@ const abbrCSS = css({
cursor: "help"
})

const notShowing = css({
opacity: 0,
minHeight: "1.15em"
const amountLoadingStyle = css(loadingStyle, {
backgroundColor: colors.gray,
borderTopRightRadius: 6,
color: "transparent",
minHeight: "1.1em",
})


const labelCss = css({
textAlign: "left",
alignItems: "flex-end",
Expand Down
44 changes: 14 additions & 30 deletions src/components/Holdings.tsx
Original file line number Diff line number Diff line change
@@ -1,36 +1,16 @@

import { css } from '@emotion/react'
import useSWR from "swr"
import Amount, { DollarDisplay } from 'src/components/Amount'
import Heading from 'src/components/Heading'
import { BreakPoints } from 'src/components/styles'
import PieChart,{ChartData} from 'src/components/PieChart'
import { HoldingsApi} from "src/service/holdings"
import Head from 'next/head'
import { Tokens} from "src/service/Data"
import { fetcher } from 'src/utils/fetcher'
import { skipZeros } from 'src/utils/skipZeros'
import { Updated } from 'src/components/Updated'
import Section from 'src/components/Section'
import { sumTotalHoldings } from './sumTotalHoldings'

const initalToken = {
value: 0,
units: 0,
hasError: false,
token: "CELO" as Tokens,
updated: 0
}

const INITAL_DATA: HoldingsApi = {
celo: {
custody: initalToken,
unfrozen: initalToken,
frozen: initalToken
},
otherAssets: []
}

import useHoldings from 'src/hooks/useHoldings'

export function sumCeloTotal(holdings: HoldingsApi) {
const { custody, frozen, unfrozen } = holdings.celo
Expand Down Expand Up @@ -70,34 +50,36 @@ function findOldestValueUpdatedAt(data?: HoldingsApi): number {
}

export default function Holdings() {
const {data} = useSWR<HoldingsApi>("/api/holdings", fetcher, {initialData: INITAL_DATA, revalidateOnMount: true})
const {data} = useHoldings()
const percentages = getPercents(data)
const isLoading = data.otherAssets.length === 0
const celo = data.celo
const isLoadingCelo = data.celo.frozen.updated === 0 || data.celo.unfrozen.updated === 0
const isLoadingOther = !data.otherAssets.findIndex((coin) => coin.updated === 0)
const oldestUpdate = findOldestValueUpdatedAt(data)
const celo = data.celo
return (
<Section
title={'Current Reserve Holdings'}
subHeading={
<>
<DollarDisplay loading={isLoading} label="Liquidity" value={sumTotalHoldings(data)} />
<DollarDisplay loading={isLoadingCelo || isLoadingOther} label="Liquidity" value={sumTotalHoldings(data)} />
<Updated date={oldestUpdate} />
</>
}
>
<Head>
<link rel="preload" href="/api/holdings" as="fetch" crossOrigin="anonymous"/>
<link rel="preload" href="/api/holdings/celo" as="fetch" crossOrigin="anonymous"/>
<link rel="preload" href="/api/holdings/other" as="fetch" crossOrigin="anonymous"/>
</Head>
<div css={rootStyle}>
<Heading title="CELO" gridArea="celo" />
<Amount iconSrc={"/assets/tokens/CELO.svg"} context="Funds frozen in on-chain Reserve contract" loading={isLoading} label="Frozen" units={celo.frozen.units} value={celo.frozen.value} gridArea="frozen" />
<Amount iconSrc={"/assets/tokens/CELO.svg"} context="Funds in on-chain Reserve contract and in custody" loading={isLoading} label="Unfrozen" units={celo.unfrozen.units + celo.custody.units} value={celo.unfrozen.value + celo.custody.value} gridArea="unfrozen" />
<Amount iconSrc={"/assets/tokens/CELO.svg"} context="Funds frozen in on-chain Reserve contract" loading={isLoadingCelo} label="Frozen" units={celo.frozen.units} value={celo.frozen.value} gridArea="frozen" />
<Amount iconSrc={"/assets/tokens/CELO.svg"} context="Funds in on-chain Reserve contract and in custody" loading={isLoadingCelo} label="Unfrozen" units={celo.unfrozen.units + celo.custody.units} value={celo.unfrozen.value + celo.custody.value} gridArea="unfrozen" />
<Heading title="Non-CELO Crypto Assets" gridArea="crypto" marginTop={30} />
{data?.otherAssets?.filter(skipZeros)?.map(asset => (
<Amount key={asset.token} loading={isLoading} label={asset.token} units={asset.units} value={asset.value} gridArea={""} />
<Amount key={asset.token} loading={isLoadingOther} label={asset.token} units={asset.units} value={asset.value} gridArea={""} />
))}
</div>
<PieChart label={"Current Composition"} slices={percentages} isLoading={isLoading} />
<PieChart label={"Current Composition"} slices={percentages} isLoading={isLoadingCelo || isLoadingOther} />
</Section>
)
}
Expand All @@ -106,12 +88,14 @@ const rootStyle = css({
display: 'grid',
gridColumnGap: 20,
gridRowGap: 12,
gridAutoColumns: "1fr 1fr 1fr",
gridTemplateAreas: `"celo celo celo"
"frozen unfrozen unfrozen"
"crypto crypto crypto"
"btc eth dai"
`,
[BreakPoints.tablet]: {
gridAutoColumns: "1fr",
gridTemplateAreas: `"celo"
"frozen"
"unfrozen"
Expand Down
3 changes: 2 additions & 1 deletion src/components/PieChart.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import {Fragment} from "react"
import { css } from '@emotion/react'
import colors from 'src/components/colors'
import { loadingStyle } from './loadingKeyframes'


export const INITAL_TARGET: ChartData[] = [
Expand Down Expand Up @@ -51,7 +52,7 @@ export default function PieChart({slices,label,showFinePrint, isLoading}: Props)
*Crypto Assets with low volatility. Candidates are decentralised stablecoins e.g. DAI
</small>}
</figcaption>
<div css={pieStyle}>
<div css={css(pieStyle, isLoading && loadingStyle)}>
<svg viewBox="-25 -25 50 40" transform="rotate(-90)" width="100%" height="100%">
{dataWithOffsets.map(({ percent, offset, token }) => {
return (
Expand Down
4 changes: 2 additions & 2 deletions src/components/Ratios.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,15 @@ import { css } from '@emotion/react'
import useSWR from "swr"
import Amount from 'src/components/Amount'
import { BreakPoints } from 'src/components/styles'
import { HoldingsApi } from "src/service/holdings"
import StableValueTokensAPI from 'src/interfaces/stable-value-tokens'
import { fetcher } from "src/utils/fetcher"
import { sumLiquidHoldings } from './sumLiquidHoldings'
import { sumTotalHoldings } from './sumTotalHoldings'
import useHoldings from 'src/hooks/useHoldings'

export function Ratios() {
const stables = useSWR<StableValueTokensAPI>("/api/stable-value-tokens", fetcher)
const holdings = useSWR<HoldingsApi>("/api/holdings", fetcher)
const holdings = useHoldings()
const isLoading = !holdings.data || !stables.data

const outstanding = stables.data?.totalStableValueInUSD || 1
Expand Down
19 changes: 19 additions & 0 deletions src/components/loadingKeyframes.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import { css, keyframes } from '@emotion/react'

const loadingKeyframes = keyframes`
from {
opacity: 0.15
}
to {opacity: 0.40}
`
export const loadingStyle = css({
opacity: 0,
animationDirection: "alternate-reverse",
animationDuration: '1.4s',
animationDelay: "20ms",
animationFillMode: "none",
animationIterationCount: 'infinite',
animationTimingFunction: "ease-in-out",
animationName: loadingKeyframes
})
42 changes: 42 additions & 0 deletions src/hooks/useHoldings.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import useSWR from "swr"
import { Tokens} from "src/service/Data"
import { fetcher } from 'src/utils/fetcher'
import { HoldingsApi} from "src/service/holdings"

const initalToken = {
value: NaN,
units: NaN,
hasError: false,
token: "CELO" as Tokens,
updated: 0
}

const initalOtherToken = {
value: NaN,
units: NaN,
hasError: false,
token: "BTC",
updated: 0
} as const

const INITAL_DATA: HoldingsApi = {
celo: {
custody: initalToken,
unfrozen: initalToken,
frozen: initalToken
},
otherAssets: [
initalOtherToken,
{...initalOtherToken, token: "ETH" },
{...initalOtherToken, token: "DAI"}
]
}


export default function useHoldings(): {data: HoldingsApi, error: any} {
const celoHoldings = useSWR<Pick<HoldingsApi, 'celo'>>("/api/holdings/celo", fetcher, {initialData: {celo: INITAL_DATA.celo}, revalidateOnMount: true})
const otherHoldings = useSWR<Pick<HoldingsApi,'otherAssets'>>("/api/holdings/other", fetcher, {initialData: {otherAssets: INITAL_DATA.otherAssets}, revalidateOnMount: true})
const error = celoHoldings.error || otherHoldings.error
const data: HoldingsApi = {...celoHoldings.data, ...otherHoldings.data}
return {data, error}
}
26 changes: 26 additions & 0 deletions src/pages/api/holdings/[kind].ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import { NextApiRequest, NextApiResponse } from 'next'
import getHoldings, {getHoldingsCelo, getHoldingsOther} from "src/service/holdings"

export default async function(req: NextApiRequest, res: NextApiResponse) {
try {
if (req.method === 'GET') {
const start = Date.now()
if (req.query.kind === 'celo') {
const holdings = await getHoldingsCelo()
res.setHeader("Server-Timing", `ms;dur=${Date.now() - start}`)
res.json(holdings)
} else if (req.query.kind === 'other') {
const holdings = await getHoldingsOther()
res.setHeader("Server-Timing", `ms;dur=${Date.now() - start}`)
res.json(holdings)
} else {
res.status(404)
}

} else {
res.status(405)
}
} catch (error) {
res.status(error.statusCode || 500).json({ message: error.message || 'unknownError' })
}
}
File renamed without changes.
67 changes: 50 additions & 17 deletions src/service/holdings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { getFrozenBalance, getInCustodyBalance, getUnFrozenBalance } from 'src/p
import * as etherscan from 'src/providers/Etherscan'
import * as ethplorer from 'src/providers/Ethplorerer'
import duel, { Duel, sumMerge } from './duel'
import getRates from "./rates"
import getRates, { celoPrice } from "./rates"
import {refresh, getOrSave} from "src/service/cache"
import { MINUTE } from "src/utils/TIME"
import { TokenModel, Tokens } from './Data'
Expand Down Expand Up @@ -92,47 +92,80 @@ export interface HoldingsApi {
otherAssets: TokenModel[]
}

export default async function getHoldings(): Promise<HoldingsApi> {
const [rates, btcHeld, ethHeld, daiHeld, celoCustodied, frozen, unfrozen] = await Promise.all([
getRates(),
btcBalance(),
ethBalance(),
daiBalance(),
export async function getHoldingsCelo() {
const [celoRate, celoCustodied, frozen, unfrozen] = await Promise.all([
celoPrice(),
celoCustodiedBalance(),
celoFrozenBalance(),
celoUnfrozenBalance(),
])

const otherAssets: TokenModel[] = [
toToken("BTC", btcHeld, rates.btc),
toToken("ETH", ethHeld, rates.eth),
toToken("DAI", daiHeld),
]

return {celo: toCeloShape(frozen, celoRate, unfrozen, celoCustodied)}
}

function toCeloShape(frozen: ProviderSource, celoRate: Duel, unfrozen: ProviderSource, celoCustodied: ProviderSource) {
return {
celo: {
frozen: {
token: "CELO",
units: frozen.value,
value: frozen.value * rates.celo.value,
value: frozen.value * celoRate.value,
hasError: frozen.hasError,
updated: frozen.time
},
unfrozen: {
token: "CELO",
units: unfrozen.value,
value: unfrozen.value * rates.celo.value,
value: unfrozen.value * celoRate.value,
hasError: unfrozen.hasError,
updated: unfrozen.time
},
custody: {
token: "CELO",
units: celoCustodied.value,
value: celoCustodied.value * rates.celo.value,
value: celoCustodied.value * celoRate.value,
hasError: celoCustodied.hasError,
updated: celoCustodied.time
}
},
} as const
}

export async function getHoldingsOther() {
const [rates, btcHeld, ethHeld, daiHeld] = await Promise.all([
getRates(),
btcBalance(),
ethBalance(),
daiBalance(),
])

const otherAssets: TokenModel[] = [
toToken("BTC", btcHeld, rates.btc),
toToken("ETH", ethHeld, rates.eth),
toToken("DAI", daiHeld),
]

return {otherAssets}
}

export default async function getHoldings(): Promise<HoldingsApi> {
const [rates, btcHeld, ethHeld, daiHeld, celoCustodied, frozen, unfrozen] = await Promise.all([
getRates(),
btcBalance(),
ethBalance(),
daiBalance(),
celoCustodiedBalance(),
celoFrozenBalance(),
celoUnfrozenBalance(),
])

const otherAssets: TokenModel[] = [
toToken("BTC", btcHeld, rates.btc),
toToken("ETH", ethHeld, rates.eth),
toToken("DAI", daiHeld),
]

return {
celo: toCeloShape(frozen, rates.celo, unfrozen, celoCustodied),
otherAssets
}
}
Expand Down
Loading

0 comments on commit 31cf909

Please sign in to comment.