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

fix: allow claims to be collected whenever there are claimable reward… #151

Merged
merged 1 commit into from
Feb 26, 2021
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
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
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think the 60 days should be put in an env var or if possible, retrieved from the subgraph. Since it looks like it will change to 30 days soon.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good point, I created #152 for that. Thanks!


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