Skip to content

Commit

Permalink
fix: allow claims to be collected whenever there are claimable reward…
Browse files Browse the repository at this point in the history
…s on the last link, improve debug box (#151)
  • Loading branch information
philipstanislaus committed Feb 26, 2021
1 parent 3656f72 commit e9394af
Show file tree
Hide file tree
Showing 4 changed files with 99 additions and 48 deletions.
14 changes: 12 additions & 2 deletions tinlake-ui/containers/ClaimRewards/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -60,8 +60,7 @@ const ClaimRewards: React.FC<Props> = ({ activeLink }: Props) => {
return <Box pad="medium">Loading claimable rewards...</Box>
}

const unclaimed =
activeLink.claimable !== null && activeLink.claimed !== null ? activeLink.claimable.sub(activeLink.claimed) : null
const unclaimed = calcUnclaimed(activeLink)

return (
<>
Expand Down Expand Up @@ -168,3 +167,14 @@ const ClaimRewards: React.FC<Props> = ({ activeLink }: Props) => {
}

export default ClaimRewards

function calcUnclaimed(link: UserRewardsLink): null | BN {
if (!link.claimable || !link.claimed) {
return null
}
const unclaimed = link.claimable.sub(link.claimed)
if (unclaimed.ltn(0)) {
return new BN(0)
}
return unclaimed
}
120 changes: 84 additions & 36 deletions tinlake-ui/containers/UserRewards/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@ import { useRouter } from 'next/router'
import * as React from 'react'
import { useDispatch, useSelector } from 'react-redux'
import styled from 'styled-components'
import Alert from '../../components/Alert'
import { LoadingValue } from '../../components/LoadingValue'
import NumberDisplay from '../../components/NumberDisplay'
import PageTitle from '../../components/PageTitle'
Expand All @@ -14,7 +13,7 @@ import { AuthState, ensureAuthed } from '../../ducks/auth'
import { CentChainWalletState } from '../../ducks/centChainWallet'
import { PortfolioState } from '../../ducks/portfolio'
import { maybeLoadRewards, RewardsState } from '../../ducks/rewards'
import { maybeLoadUserRewards, UserRewardsLink, UserRewardsState } from '../../ducks/userRewards'
import { maybeLoadUserRewards, UserRewardsData, UserRewardsLink, UserRewardsState } from '../../ducks/userRewards'
import { accountIdToCentChainAddr } from '../../services/centChain/accountIdToCentChainAddr'
import { addThousandsSeparators } from '../../utils/addThousandsSeparators'
import { shortAddr } from '../../utils/shortAddr'
Expand Down Expand Up @@ -150,43 +149,54 @@ const UserRewards: React.FC<Props> = ({ tinlake }: Props) => {
</>
))}

{debug && (
<Card margin={{ bottom: 'large' }} background="neutral-3">
<Box pad="medium">
<h3>Debug:</h3>
<ul>
<li>Non-zero investment since: {data?.nonZeroInvestmentSince?.toString() || 'null'}</li>
<li>
Total earned rewards:{' '}
{data ? `${toPrecision(baseToDisplay(data?.totalEarnedRewards, 18), 4)} RAD` : 'null'}
</li>
<li>
Unlinked rewards:{' '}
{data ? `${toPrecision(baseToDisplay(data?.unlinkedRewards, 18), 4)} RAD` : 'null'}
</li>
{data?.links.map((c, i) => (
<li key={c.centAccountID}>
Link {i + 1} {i === data.links.length - 1 && '(Active)'}
<ul>
<li>Centrifuge Chain Address: {accountIdToCentChainAddr(c.centAccountID)}</li>
<li>Centrifuge Chain Account ID: {c.centAccountID}</li>
<li>Earned (from Subgraph): {toPrecision(baseToDisplay(c.earned, 18), 4)} RAD</li>
<li>
Claimable (from GCP):{' '}
{c.claimable ? `${toPrecision(baseToDisplay(c.claimable, 18), 4)} RAD` : `[loading...]`}
</li>
<li>
Claimed (from Centrifuge Chain):{' '}
{c.claimed ? `${toPrecision(baseToDisplay(c.claimed, 18), 4)} RAD` : `[loading...]`}
</li>
</ul>
</li>
))}
</ul>
</Box>
</Card>
)}

{ethAddr && data?.links && data.links.length > 0 && (
<Card>
<Box direction="row" pad={{ horizontal: 'medium', top: 'medium', bottom: 'medium' }}>
<Box flex={true}>
<Head>Claim Your RAD Rewards</Head>

{debug && (
<Alert type="info">
<h3>Debug:</h3>
<ul>
{data.links.map((c, i) => (
<li key={c.centAccountID}>
Link {i + 1} {i === data.links.length - 1 && '(Active)'}
<ul>
<li>Centrifuge Chain Address: {shortAddr(accountIdToCentChainAddr(c.centAccountID))}</li>
<li>Centrifuge Chain Account ID: {shortAddr(c.centAccountID)}</li>
<li>Earned (from Subgraph): {toPrecision(baseToDisplay(c.earned, 18), 4)} RAD</li>
<li>
Claimable (from GCP):{' '}
{c.claimable ? `${toPrecision(baseToDisplay(c.claimable, 18), 4)} RAD` : `[loading...]`}
</li>
<li>
Claimed (from Centrifuge Chain):{' '}
{c.claimed ? `${toPrecision(baseToDisplay(c.claimed, 18), 4)} RAD` : `[loading...]`}
</li>
</ul>
</li>
))}
</ul>
</Alert>
)}

{!data?.claimable && comebackDate(data?.nonZeroInvestmentSince)}
{comebackDate(data?.nonZeroInvestmentSince)}
</Box>
<RewardRecipients recipients={data?.links} />
</Box>
{data?.claimable && <ClaimRewards activeLink={data.links[data.links.length - 1]} />}
{showClaimStripe(data) && <ClaimRewards activeLink={data.links[data.links.length - 1]} />}
</Card>
)}
</Box>
Expand Down Expand Up @@ -228,31 +238,69 @@ const UserRewards: React.FC<Props> = ({ tinlake }: Props) => {

export default UserRewards

const days = 24 * 60 * 60
const day = 24 * 60 * 60
const minNonZeroDays = 61

function comebackDate(nonZero: BN | null | undefined) {
function comebackDate(nonZero: BN | null | undefined): null | string {
if (!nonZero || nonZero.isZero()) {
return 'You can not yet claim your rewards, please come back after investing in a Tinlake pool and waiting for 60 days.'
}

const start = nonZero
const startDate = new Date(start.toNumber() * 1000).toLocaleDateString()
const target = start.addn(61 * days)
const target = start.addn(minNonZeroDays * day)
const targetDate = new Date(target.toNumber() * 1000).toLocaleDateString()
const diff = target
.sub(new BN(Date.now() / 1000))
.divn(1 * days)
.divn(1 * day)
.addn(1)
.toString()

// if not in the future
if (diff.lten(0)) {
return null
}

return (
`You cannot claim your RAD rewards yet. RAD rewards can only be claimed after a minimum investment period of 60 ` +
`days. Your first eligible investment was made ${startDate}. Please come back in ${
diff === '1' ? '1 day' : `${diff} days`
diff.eqn(1) ? '1 day' : `${diff.toString()} days`
} on ${targetDate} to claim your RAD rewards.`
)
}

function showClaimStripe(data: UserRewardsData | null): boolean {
if (data === null) {
return false
}

const { links } = data

// `false` if there are no links
if (links.length === 0) {
return false
}

const lastLink = links[links.length - 1]

// `true` if last link has positive total rewards. That might still imply that there are no unclaimed rewards, but the
// claim stripe handles that.
if (lastLink.earned.gtn(0)) {
return true
}

// `true` if last link has positive claimable rewards. Claimable could be positive even if earned is zero in cases where one Centrifuge Chain account receives rewards from multiple Ethereum accounts.
if (lastLink.claimable?.gtn(0)) {
return true
}

// `true` if last link has positive claimed rewards. Claimed could be positive even if earned is zero in cases where one Centrifuge Chain account receives rewards from multiple Ethereum accounts.
if (lastLink.claimed?.gtn(0)) {
return true
}

return false
}

const Card = ({ children, ...rest }: React.PropsWithChildren<any>) => (
<Box width="100%" pad="none" elevation="small" round="xsmall" background="white" {...rest}>
{children}
Expand Down
10 changes: 3 additions & 7 deletions tinlake-ui/ducks/userRewards.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ export interface UserRewardsState {
/**
* Process to earn and claim rewards:
* 1. User earns rewards on Ethereum for any investments on that Ethereum account `totalEarnedRewards`
* 2. After holding a non zero investements for 60 days, those rewards become `claimable`
* 2. After holding a non zero investements for 60 days, those rewards become claimable
* 3. To claim rewards, user needs to link a Cent Chain account to the Ethereum account. If there is none, any rewards
* are in `unlinkedRewards`. If there is a link, rewards will be fully assigned to the (last) linked Cent Chain
* account.
Expand All @@ -50,14 +50,10 @@ export interface UserRewardsData {
* be a timestamp (in seconds).
*/
nonZeroInvestmentSince: BN | null
/**
* From subgraph. Determines whether investment was long enough on Ethereum yet for rewards to be claimable.
*/
claimable: boolean
/**
* From subgraph. Those are rewards that have not been linked to a Cent Chain account on Ethereum. They can be linked
* at any time. If claimable is true, they will be immediately assigned to a linked Cent Chain account. If claimable
* is false, they will remain unlinked until they become claimable.
* at any time. If they are claimable, they will be immediately assigned to a linked Cent Chain account. If they are
* not claimable, they will remain unlinked until they become claimable.
*/
unlinkedRewards: BN
/**
Expand Down
3 changes: 0 additions & 3 deletions tinlake-ui/services/apollo/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -331,7 +331,6 @@ class Apollo {
centAddress
rewardsAccumulated
}
claimable
linkableRewards
totalRewards
nonZeroBalanceSince
Expand All @@ -346,7 +345,6 @@ class Apollo {

const transformed: UserRewardsData = {
nonZeroInvestmentSince: null,
claimable: false,
totalEarnedRewards: new BN(0),
unlinkedRewards: new BN(0),
links: [],
Expand All @@ -356,7 +354,6 @@ class Apollo {
if (rewardBalance) {
transformed.nonZeroInvestmentSince =
rewardBalance.nonZeroBalanceSince && new BN(rewardBalance.nonZeroBalanceSince)
transformed.claimable = rewardBalance.claimable
transformed.totalEarnedRewards = new BN(new Decimal(rewardBalance.totalRewards).toFixed(0))
transformed.unlinkedRewards = new BN(new Decimal(rewardBalance.linkableRewards).toFixed(0))
transformed.links = (rewardBalance.links as any[]).map((link: any) => ({
Expand Down

0 comments on commit e9394af

Please sign in to comment.