Skip to content

Commit

Permalink
Claim RAD UI (#93) (#114)
Browse files Browse the repository at this point in the history
* Add refund utilities for tinlake.js tests, add helper for tx success

* Use refund mechanism in all tests

* Add claim rad functionality to tinlake.js

* Use actual gas price for refunding ETH after tinlake.js tests

* Update claim RAD ABI

* Fix yield data fetching

* Implement reward claims UI

* Rename claim RAD contract name

* Fix buggy permission check

* Add Centrifuge Chain wallet, add functionality to set Centrifuge Chain address link on Ethereum

* Fix validation

* Improve flow

Signed-off-by: Philip Stanislaus <6912756+philipstanislaus@users.noreply.github.com>

* Fix compilation errors

* Update field names

* Add display of claimed rewards on Centrifuge Chain

* Add fetching of rad rewards tree from GCP

* Refactor user rewards to make them simpler

* Add reward collection on Centrifuge Chain

* Improve flow and copy for demo of RAD rewards claim

* Fix spacing and other bugs

* Fix claims

* Add explanation if collected/uncollected is higher than earned

* Add rad reward claim proofs

* Fix hash calculation

* Remove system rewards

* Update reward claim design

* Fix reward collection

* Simplify state

* Add user current investments and daily rewards

* Add system rewards

* Fix rewards collected display

* Add RAD icon to collect stripe

* Remove console.logs

* Update copy

* Show data for non-investors

* Update copy

* Update copy

* More copy updates

* Add hook to detect installed/injected polkadot browser extension

* Fix bug caused by checksum casing of Ethereum address

* Add rewards to dashboard

* Fix loading indicators

* Design and copy changes

* Remove Total Financed to Date

* Small layout fixes

* Add reload page after polkadot wallet extension install

* Automatically connect polkadot wallet

* Add title to rewards page

* Replace orange with white RAD rewards box on dashboard

* Rename "address" with "account" on rewards UI

* Change data types for rewards from strings to BNs/Decimals

* Add start investing step for investors without earned rewards before link account step

* Add success message to account linking and poll status

* Change cent chain link address selection to dropdown

* Copy changes

* Redesign system rewards

* Fix bug that led to wrong allocation of claimed rewards

* Fix bug if nonZeroBalanceSince is null

* Fix padding

* Improve pool title visuals

* Copy and layout changes

* Remove unused router

* Add claim rewards entry to wallet

* Copy changes

* Fix compilation error

* Fix bug that caused linked accounts without investments not showing up in rewards page

* Add extrinsic hash to claim UI

* Fix decimal issues

* Add thousands separators to RAD reward claims

* Rename tinlake get cent chain account functions

* Improve missing env variable errors

Co-authored-by: Jeroen Offerijns <jeroen@offerijns.com>

Co-authored-by: Jeroen Offerijns <jeroen@offerijns.com>
  • Loading branch information
philipstanislaus and hieronx authored Jan 19, 2021
1 parent a6e90d8 commit 543bbf8
Show file tree
Hide file tree
Showing 63 changed files with 2,802 additions and 147 deletions.
5 changes: 4 additions & 1 deletion tinlake-ui/.env.dev-example
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,9 @@ INVESTOR_REQUEST_EMAIL=ask@centrifuge.io
NEXT_PUBLIC_IPFS_GATEWAY=https://cloudflare-ipfs.com/ipfs/
NEXT_PUBLIC_POOL_REGISTRY=0x8FE85CeAe6157C1dfcDD1c5ec99361c9722d97de
NEXT_PUBLIC_ONBOARD_API_HOST=http://localhost:3100/
NEXT_PUBLIC_CENTRIFUGE_CHAIN_URL=wss://fullnode.amber.centrifuge.io
NEXT_PUBLIC_CLAIM_RAD_CONTRACT_ADDRESS=0x297237e17F327f8e5C8dEd78b15761A7D513353b
NEXT_PUBLIC_MULTICALL_CONTRACT_ADDRESS=0x2cc8688c5f75e365aaeeb4ea8d6a480405a48d2a
NEXT_PUBLIC_REWARDS_TREE_URL=https://storage.googleapis.com/rad-rewards-trees/latest.json
#NEXT_PUBLIC_POOLS_IPFS_HASH_OVERRIDE
NEXT_PUBLIC_INFURA_KEY=bf808e7d3d924fbeb74672d9341d0550
NEXT_PUBLIC_INFURA_KEY=bf808e7d3d924fbeb74672d9341d0550
3 changes: 3 additions & 0 deletions tinlake-ui/.env.prod-example
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,9 @@ INVESTOR_REQUEST_EMAIL=ask@centrifuge.io
NEXT_PUBLIC_IPFS_GATEWAY=https://cloudflare-ipfs.com/ipfs/
NEXT_PUBLIC_POOL_REGISTRY=0xddf1c516cf87126c6c610b52fd8d609e67fb6033
NEXT_PUBLIC_ONBOARD_API_HOST=http://localhost:3100/
NEXT_PUBLIC_CENTRIFUGE_CHAIN_URL=wss://fullnode.centrifuge.io
NEXT_PUBLIC_CLAIM_RAD_CONTRACT_ADDRESS=0x297237e17F327f8e5C8dEd78b15761A7D513353b
NEXT_PUBLIC_MULTICALL_CONTRACT_ADDRESS=0xeefba1e63905ef1d7acba5a8513c70307c1ce441
NEXT_PUBLIC_REWARDS_TREE_URL=https://storage.googleapis.com/rad-rewards-trees/latest.json
#NEXT_PUBLIC_POOLS_IPFS_HASH_OVERRIDE
NEXT_PUBLIC_INFURA_KEY=bf808e7d3d924fbeb74672d9341d0550
13 changes: 8 additions & 5 deletions tinlake-ui/components/Header/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@ import { AuthState, clear, ensureAuthed } from '../../ducks/auth'
import { OnboardingState } from '../../ducks/onboarding'
import { loadPortfolio, PortfolioState, TokenBalance } from '../../ducks/portfolio'
import { selectWalletTransactions, TransactionState } from '../../ducks/transactions'
import { load } from '../../ducks/userRewards'
import { WalletRewards } from '../WalletRewards'
import { addThousandsSeparators } from '../../utils/addThousandsSeparators'
import { getAddressLink } from '../../utils/etherscanLinkGenerator'
import { toPrecision } from '../../utils/toPrecision'
Expand Down Expand Up @@ -174,17 +176,18 @@ const Header: React.FC<Props> = (props: Props) => {
</Portfolio>
)}
<div style={{ flex: '0 0 auto', paddingLeft: 16, borderLeft: '1px solid #D8D8D8' }}>
{!auth?.address && <Button onClick={connectAccount} label="Connect" />}
{auth?.address && (
{!address && <Button onClick={connectAccount} label="Connect" />}
{address && (
<Web3Wallet
address={auth?.address}
address={address}
providerName={providerName}
networkName={network}
onDisconnect={clear}
transactions={selectWalletTransactions(transactions)}
getAddressLink={getAddressLink}
style={{ padding: 0 }}
kycStatus={onboarding.data?.kyc?.status === 'verified' ? 'verified' : 'none'}
extension={<WalletRewards address={address} />}
/>
)}
</div>
Expand All @@ -194,6 +197,8 @@ const Header: React.FC<Props> = (props: Props) => {
)
}

export default connect((state) => state, { ensureAuthed, clear, load })(withRouter(Header))

const Portfolio = styled(Box)`
cursor: pointer;
`
Expand All @@ -219,5 +224,3 @@ const Desc = styled.div`
font-size: 10px;
color: #bbb;
`

export default connect((state) => state, { ensureAuthed, clear })(withRouter(Header))
33 changes: 24 additions & 9 deletions tinlake-ui/components/LoadingValue/index.tsx
Original file line number Diff line number Diff line change
@@ -1,14 +1,25 @@
import * as React from 'react'
import styled from 'styled-components'

interface LoadingValueProps {
interface PropsWithChildren {
done: boolean
children: React.ReactNode
height?: number
maxWidth?: number
alignRight?: boolean
}

interface PropsWithRender {
done: boolean
height?: number
maxWidth?: number
alignRight?: boolean
render: () => React.ReactNode
}

const Wrapper = styled.div<{ width?: number; verticalMargin?: number; alignRight?: boolean }>`
display: inline-block;
vertical-align: middle;
background: #eee;
border-radius: 6px;
width: ${(props) => (props.width ? `${props.width}px` : '80px')};
Expand All @@ -19,19 +30,15 @@ const Wrapper = styled.div<{ width?: number; verticalMargin?: number; alignRight
: `2px 0 2px ${props.alignRight ? 'auto' : '0'}`};
`

export const LoadingValue = (props: LoadingValueProps) => {
const randomWidth = () => {
return 60 + Math.round(Math.random() * 60)
}

const [width, setWidth] = React.useState(randomWidth())
export const LoadingValue = (props: PropsWithChildren | PropsWithRender) => {
const [width, setWidth] = React.useState(randomWidth(props.maxWidth))

React.useEffect(() => {
setWidth(randomWidth())
!props.done && setWidth(randomWidth(props.maxWidth))
}, [props.done])

return props.done ? (
<>{props.children}</>
<>{isPropsWithRender(props) ? props.render() : props.children}</>
) : (
<Wrapper
width={width}
Expand All @@ -42,3 +49,11 @@ export const LoadingValue = (props: LoadingValueProps) => {
</Wrapper>
)
}

const randomWidth = (max: number = 120) => {
return Math.round(max / 2 + Math.random() * (max / 2))
}

function isPropsWithRender(props: PropsWithChildren | PropsWithRender): props is PropsWithRender {
return (props as PropsWithRender).render !== undefined
}
3 changes: 0 additions & 3 deletions tinlake-ui/components/PageTitle/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,6 @@ const Wrapper = styled.div`

const BackLink = styled.div`
margin: 14px 20px 0 18px;
> svg {
width: 18px;
height: 18px;
Expand Down Expand Up @@ -114,11 +113,9 @@ const PageName = styled.h1`
font-size: 18px;
font-weight: bold;
margin: 0;
a {
color: #000;
text-decoration: none;
&:hover {
color: rgb(39, 98, 255);
}
Expand Down
43 changes: 31 additions & 12 deletions tinlake-ui/components/PoolsMetrics/index.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
import { baseToDisplay } from '@centrifuge/tinlake-js'
import { Box } from 'grommet'
import { Box, Button } from 'grommet'
import { useRouter } from 'next/router'
import * as React from 'react'
import { useSelector } from 'react-redux'
import { useDispatch, useSelector } from 'react-redux'
import { Area, AreaChart, ResponsiveContainer, Tooltip } from 'recharts'
import { PoolsDailyData, PoolsData } from '../../ducks/pools'
import { maybeLoadRewards, RewardsState } from '../../ducks/rewards'
import { dateToYMD } from '../../utils/date'
import { dynamicPrecision } from '../../utils/toDynamicPrecision'
import NumberDisplay from '../NumberDisplay'
import { Cont, Label, TokenLogo, Unit, Value } from './styles'

Expand All @@ -20,6 +22,16 @@ const PoolsMetrics: React.FC<Props> = (props: Props) => {
const [hoveredPoolValue, setHoveredPoolValue] = React.useState<number | undefined>(undefined)
const [hoveredDay, setHoveredDay] = React.useState<number | undefined>(undefined)

const rewards = useSelector<any, RewardsState>((state: any) => state.rewards)
const dispatch = useDispatch()
React.useEffect(() => {
dispatch(maybeLoadRewards())
}, [])

const totalRewardsEarned = baseToDisplay(rewards.data?.toDateRewardAggregateValue || '0', 18)

const goToRewards = () => router.push('/rewards')

return (
<>
{router.query.showAll && (
Expand Down Expand Up @@ -103,21 +115,28 @@ const PoolsMetrics: React.FC<Props> = (props: Props) => {
</Box>
</Box>
<Box
width="256px"
pad="medium"
width="430px"
elevation="small"
round="xsmall"
background="white"
margin={{ horizontal: '16px' }}
direction="row"
pad="medium"
justify="center"
>
<Cont style={{ marginTop: '8px' }}>
<TokenLogo src={`/static/dai.svg`} />
<Value>
<NumberDisplay value={baseToDisplay(props.pools.totalFinancedCurrency, 18)} precision={0} />
</Value>{' '}
<Unit>DAI</Unit>
</Cont>
<Label>Total Financed to Date</Label>
<Box>
<Cont style={{ marginTop: '8px' }}>
<TokenLogo src={`/static/rad.svg`} />
<Value>
<NumberDisplay value={totalRewardsEarned} precision={dynamicPrecision(totalRewardsEarned)} />
</Value>{' '}
<Unit>RAD</Unit>
</Cont>
<Label>Total Rewards Earned</Label>
</Box>
<Box margin={{ left: 'medium' }} justify="center">
<Button label="Claim Rewards" primary onClick={goToRewards} color="#FCBA59" />
</Box>
</Box>
</>
)
Expand Down
66 changes: 66 additions & 0 deletions tinlake-ui/components/WalletRewards/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
import { baseToDisplay } from '@centrifuge/tinlake-js'
import { Box, Button } from 'grommet'
import { useRouter } from 'next/router'
import React from 'react'
import { useDispatch, useSelector } from 'react-redux'
import styled from 'styled-components'
import { load, UserRewardsState } from '../../ducks/userRewards'
import { addThousandsSeparators } from '../../utils/addThousandsSeparators'
import { toDynamicPrecision } from '../../utils/toDynamicPrecision'

export const WalletRewards = ({ address }: { address: string }) => {
const dispatch = useDispatch()
const userRewards = useSelector<any, UserRewardsState>((state: any) => state.userRewards)
React.useEffect(() => {
if (address) {
dispatch(load(address))
}
}, [address])

const router = useRouter()

if (!userRewards.data) {
return null
}

return (
<Cont direction="row" pad={{ vertical: '10px', horizontal: '16px' }}>
<TokenLogo src="/static/rad-black.svg" />
<Box>
<Label>Your rewards</Label>
<Number>
{addThousandsSeparators(toDynamicPrecision(baseToDisplay(userRewards.data?.totalEarnedRewards || '0', 18)))}{' '}
RAD
</Number>
</Box>
<Button secondary onClick={() => router.push('/rewards')} label="Claim RAD" margin={{ left: 'auto' }} />
</Cont>
)
}

const Cont = styled(Box)`
background: #fcba59;
border-radius: 0 0 8px 8px;
`

const TokenLogo = styled.img`
margin: 0 14px 0 0;
width: 24px;
height: 24px;
position: relative;
top: 12px;
`

const Label = styled.div`
font-size: 10px;
font-weight: 500;
height: 14px;
line-height: 14px;
`

const Number = styled.div`
font-size: 20px;
font-weight: 500;
height: 32px;
line-height: 32px;
`
9 changes: 2 additions & 7 deletions tinlake-ui/components/WithTinlake/index.tsx
Original file line number Diff line number Diff line change
@@ -1,16 +1,11 @@
import { ITinlake } from '@centrifuge/tinlake-js'
import { ContractAddresses, ITinlake } from '@centrifuge/tinlake-js'
import * as React from 'react'
import { initTinlake } from '../../services/tinlake'

interface Props {
render: (tinlake: ITinlake) => React.ReactElement
version?: 2 | 3
addresses?: {
ROOT_CONTRACT: string
ACTIONS: string
PROXY_REGISTRY: string
COLLATERAL_NFT: string
}
addresses?: ContractAddresses
contractConfig?: {
JUNIOR_OPERATOR: 'ALLOWANCE_OPERATOR'
SENIOR_OPERATOR: 'ALLOWANCE_OPERATOR' | 'PROPORTIONAL_OPERATOR'
Expand Down
21 changes: 19 additions & 2 deletions tinlake-ui/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,9 @@ interface Config {
onboardAPIHost: string
featureFlagNewOnboardingPools: string[]
enableErrorLogging: boolean
centrifugeChainUrl: string
claimRADContractAddress: string
rewardsTreeUrl: string
multicallContractAddress: string
}

Expand Down Expand Up @@ -298,11 +301,11 @@ const config: Config = {
.validateSync(networkUrlToName(process.env.NEXT_PUBLIC_RPC_URL || '')),
portisApiKey: yup
.string()
.required()
.required('NEXT_PUBLIC_PORTIS_KEY is required')
.validateSync(process.env.NEXT_PUBLIC_PORTIS_KEY),
infuraKey: yup
.string()
.required()
.required('NEXT_PUBLIC_INFURA_KEY is required')
.validateSync(process.env.NEXT_PUBLIC_INFURA_KEY),
gasLimit: yup
.number()
Expand All @@ -312,6 +315,20 @@ const config: Config = {
.string()
.required('NEXT_PUBLIC_ONBOARD_API_HOST is required')
.validateSync(process.env.NEXT_PUBLIC_ONBOARD_API_HOST),
centrifugeChainUrl: yup
.string()
.required('NEXT_PUBLIC_CENTRIFUGE_CHAIN_URL is required')
.validateSync(process.env.NEXT_PUBLIC_CENTRIFUGE_CHAIN_URL),
claimRADContractAddress: yup
.string()
.length(42)
.matches(/0x[0-9a-fA-F]{40}/)
.required('NEXT_PUBLIC_CLAIM_RAD_CONTRACT_ADDRESS is required')
.validateSync(process.env.NEXT_PUBLIC_CLAIM_RAD_CONTRACT_ADDRESS),
rewardsTreeUrl: yup
.string()
.required('NEXT_PUBLIC_REWARDS_TREE_URL is required')
.validateSync(process.env.NEXT_PUBLIC_REWARDS_TREE_URL),
enableErrorLogging: yup.boolean().validateSync(false),
// Loading a comma-separated string as a string array using yup proved hard/impossible
featureFlagNewOnboardingPools: process.env.NEXT_PUBLIC_FEATURE_FLAG_NEW_ONBOARDING?.split(',') || [],
Expand Down
Loading

0 comments on commit 543bbf8

Please sign in to comment.