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

Centrifuge App: Fix Tinlake rewards claiming #1519

Merged
merged 3 commits into from
Aug 7, 2023
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
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { Pool, TokenBalance } from '@centrifuge/centrifuge-js'
import { useBalances, useCentrifugeTransaction } from '@centrifuge/centrifuge-react'
import { useBalances, useCentrifugeConsts, useCentrifugeTransaction } from '@centrifuge/centrifuge-react'
import Decimal from 'decimal.js-light'
import * as React from 'react'
import { Dec } from '../../utils/Decimal'
Expand All @@ -16,6 +16,7 @@ export function LiquidityRewardsProvider(props: LiquidityRewardsProviderProps) {

function Provider({ poolId, trancheId, children }: LiquidityRewardsProviderProps) {
const pool = usePool(poolId) as Pool
const consts = useCentrifugeConsts()
const address = useAddress()
const order = usePendingCollect(poolId, trancheId, address)
const stakes = useAccountStakes(address, poolId, trancheId)
Expand Down Expand Up @@ -59,7 +60,10 @@ function Provider({ poolId, trancheId, children }: LiquidityRewardsProviderProps
canStake,
canUnstake,
canClaim,
nativeCurrency: balances?.native.currency,
nativeCurrency: {
symbol: consts.chainSymbol,
decimals: consts.chainDecimals,
},
isLoading: {
claim: claim.isLoading,
stake: stake.isLoading,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,19 +1,20 @@
import { TransactionStatus, useBalances, useCentrifugeTransaction } from '@centrifuge/centrifuge-react'
import { CurrencyBalance } from '@centrifuge/centrifuge-js'
import { useCentrifugeConsts, useCentrifugeTransaction } from '@centrifuge/centrifuge-react'
import { Box, Button, Card, Shelf, Stack, Text } from '@centrifuge/fabric'
import BN from 'bn.js'
import * as React from 'react'
import { formatBalance } from '../../utils/formatting'
import { TinlakeTranche, useTinlakePortfolio } from '../../utils/tinlake/useTinlakePortfolio'
import { useRewardClaims, useTinlakeRewards, useUserRewards } from '../../utils/tinlake/useTinlakeRewards'
import { useAddress } from '../../utils/useAddress'
import { calculateCFGRewards, calcUnclaimed, createBufferProofFromClaim, createTree, newClaim } from './utils'
import { calcUnclaimed, createBufferProofFromClaim, createTree, newClaim } from './utils'

export function ClaimTinlakeRewards() {
const { data } = useUserRewards()
const consts = useCentrifugeConsts()
const { data: claims } = useRewardClaims()
const { data: rewards } = useTinlakeRewards()
const portfolio = useTinlakePortfolio()
const address = useAddress()
const balances = useBalances(address)
const centAddress = useAddress('substrate')

const portfolioValue = portfolio.data?.totalValue
const portfolioTinValue = portfolio.data?.tokenBalances
Expand All @@ -24,15 +25,12 @@ export function ClaimTinlakeRewards() {
.reduce((sum, tb) => tb.value.add(sum), new BN(0))
const activeLink = data?.links.at(-1)

const [status, setStatus] = React.useState<null | TransactionStatus>(null)
const [error, setError] = React.useState<null | string>(null)

const { execute } = useCentrifugeTransaction('Claim CFG rewards', (cent) => cent.tinlake.claimCFGRewards)

const claim = async () => {
setStatus('pending')
setError(null)
const { execute, lastCreatedTransaction } = useCentrifugeTransaction(
'Claim CFG rewards',
(cent) => cent.tinlake.claimCFGRewards
)

function claim() {
if (!claims) {
throw new Error('claims must exist to claim')
}
Expand All @@ -46,14 +44,7 @@ export function ClaimTinlakeRewards() {
const tree = createTree(claims.map((c) => newClaim(c)))
const proof = createBufferProofFromClaim(tree, newClaim(claim))

try {
await execute([claim.accountID, claim.balance, proof])
setStatus('succeeded')
} catch (e) {
console.log('error "ClaimTinlakeRewards" execute:', e)
setStatus('failed')
setError((e as Error).toString())
}
execute([claim.accountID, claim.balance, proof])
}

if (!activeLink) {
Expand All @@ -74,57 +65,73 @@ export function ClaimTinlakeRewards() {

const dailyRewards =
rewards?.dropRewardRate && rewards?.tinRewardRate && portfolioValue
? calculateCFGRewards(
rewards.dropRewardRate
?.mul(portfolioDropValue?.toString() || 0)
.add(rewards.tinRewardRate?.mul(portfolioTinValue?.toString() || 0))
.toFixed(0) || '0'
? formatBalance(
new CurrencyBalance(
rewards.dropRewardRate
?.mul(portfolioDropValue?.toString() || 0)
.add(rewards.tinRewardRate?.mul(portfolioTinValue?.toString() || 0))
.toFixed(0) || '0',
18
)
)
: null

const status = lastCreatedTransaction?.status
return (
<Stack as={Card} p={2} pb={4} gap={2}>
{status === 'failed' && error && (
<Stack as={Card} p={2} pb={2} gap={2}>
{status === 'failed' && (
<Text as="strong" variant="heading3" color="statusCritical">
Error claiming rewards.
</Text>
)}

{unclaimed && !unclaimed?.isZero() ? (
<>
<Stack gap={1}>
{!centAddress && <Text variant="body2">Connect Polkadot wallet to claim</Text>}
<Shelf justifyContent="space-between">
<Stack>
<Text as="span" variant="body3">
Claimable rewards
</Text>
<Text as="strong" variant="heading3">
{calculateCFGRewards(unclaimed || '0')} CFG
{formatBalance(unclaimed || '0')} CFG
</Text>
</Stack>

<Button
disabled={(!!unclaimed && unclaimed.isZero()) || status === 'unconfirmed' || status === 'pending'}
disabled={!!unclaimed && unclaimed.isZero()}
loading={status === 'unconfirmed' || status === 'pending'}
onClick={claim}
small
>
{status === 'unconfirmed' || status === 'pending' ? 'Claiming...' : 'Claim'}
{status === 'unconfirmed' || status === 'pending'
? 'Claiming...'
: centAddress
? 'Claim'
: 'Connect & Claim'}
</Button>
</Shelf>
</>
</Stack>
) : (
<>
<Stack>
<Text as="span" variant="body3">
You claimed
</Text>
<Text as="strong" variant="heading3">
{calculateCFGRewards((data?.links || []).reduce((p, l) => p.add(l.claimed || new BN(0)), new BN(0)))} CFG
{formatBalance(
new CurrencyBalance(
(data?.links || []).reduce((p, l) => p.add(l.claimed || new BN(0)), new BN(0)),
18
)
)}{' '}
CFG
</Text>
</Stack>

{dailyRewards && (
<Text as="span" variant="body3" color="textSecondary">
Stay invested to continue earning {dailyRewards} {balances?.native.currency.symbol || 'CFG'} daily.
Stay invested to continue earning {dailyRewards} {consts.chainSymbol || 'CFG'} daily.
</Text>
)}
</>
Expand Down
58 changes: 58 additions & 0 deletions centrifuge-app/src/components/TinlakeRewards/utils.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
import { Claim } from './types'
import { createBufferProofFromClaim, createProofFromClaim, createTree, hashLeaf, verifyProof } from './utils'
const BN = require('bn.js')

describe('Tinlake Rewards Proofs', () => {
it('should be able to create proofs correctly', () => {
const claim0: Claim = {
accountID: '0xe0505d9eb1fd7c06c1396c655bb78448e4b469812a9c2a2bfed1089bb21c5b47',
balance: new BN('100000000000000000000', 10), // 100 CFG
}
const claim1: Claim = {
accountID: '0x085ef2a683e8b2bd4db4be08ba0617ed19989c38a6afd9265b4517944647b112',
balance: new BN('201000000000000000000', 10),
}
const claim2: Claim = {
accountID: '0x02c451256605c7bcec9dcc4e0fd7aaa589672e4ff8a433bf5630dd9d40685502',
balance: new BN('301000000000000000000', 10),
}
const claim3: Claim = {
accountID: '0xaaf166d4a5771686aedf556b66a484d16864b2aeed2bcf8e4f29ab2ec5041c3a',
balance: new BN('401000000000000000000', 10),
}
const claim4: Claim = {
accountID: '0x94489d88cd8e43777a2bd74a189354afdb96a8f9fe52f1a556378fb04721230a',
balance: new BN('501000000000000000000', 10),
}
const claim5: Claim = {
accountID: '0x5c84a8d5a2d754eae104115c769b385c9334fba7084cf761fbd0d688b795e903',
balance: new BN('601000000000000000000', 10),
}
const claim6: Claim = {
accountID: '0x0a2cec550b92cc3f1de2e15edc085dc47bb8d7c3978457c61add02e3e1eebb76',
balance: new BN('701000000000000000000', 10),
}
const claim7: Claim = {
accountID: '0x5692727ec3ceacea7553a7787304fcb66dee75cc52ff7959ab53634ed5e14f5c',
balance: new BN('801000000000000000000', 10),
}
const claim8: Claim = {
accountID: '0xc622f88f92f1d2c15293a8b1e7733b81198cbc99ed131c58761abf2ad365f72b',
balance: new BN('901000000000000000000', 10),
}
const claim9: Claim = {
accountID: '0xd6c50e8575dd364b5cbdde6610356e9a02a3ee0c42f7f867cce4ca9d36f1d87b',
balance: new BN('1001000000000000000000', 10),
}
const claims: Claim[] = [claim0, claim1, claim2, claim3, claim4, claim5, claim6, claim7, claim8, claim9]
const tree = createTree(claims)

const leaf = hashLeaf(claim0.accountID, claim0.balance)
const proof = createProofFromClaim(tree, claim0)
expect(proof.length).toEqual(4)
const proofArray = createBufferProofFromClaim(tree, claim0)
expect(proofArray.length).toEqual(4)
expect(tree.getHexRoot()).toEqual('0xb86441971a590bb28da204c422f8f90e5bdbe4eed7149c489be23b534f8eff6b')
expect(verifyProof(tree, leaf, proof)).toBeTruthy()
})
})
53 changes: 15 additions & 38 deletions centrifuge-app/src/components/TinlakeRewards/utils.ts
Original file line number Diff line number Diff line change
@@ -1,46 +1,20 @@
import { addThousandsSeparators, toPrecision } from '@centrifuge/centrifuge-js'
import { blake2AsU8a } from '@polkadot/util-crypto/blake2'
import { CurrencyBalance } from '@centrifuge/centrifuge-js'
import { bnToU8a, hexToU8a, u8aConcat } from '@polkadot/util'
import { blake2AsHex } from '@polkadot/util-crypto/blake2'
import BN from 'bn.js'
import Decimal from 'decimal.js-light'
import { MerkleTree } from 'merkletreejs'
import { UserRewardsLink } from '../../utils/tinlake/types'
import { Claim, Proof } from './types'

export function calculateCFGRewards(amount: string | BN) {
return addThousandsSeparators(toDynamicPrecision(baseToDisplay(amount, 18)))
}

export function calcUnclaimed(link: UserRewardsLink): null | BN {
export function calcUnclaimed(link: UserRewardsLink) {
if (!link.claimable || !link.claimed) {
return null
}
const unclaimed = link.claimable.sub(link.claimed)
if (unclaimed.ltn(0)) {
return new BN(0)
return new CurrencyBalance(0, 18)
}
return unclaimed
}

function baseToDisplay(base: string | BN, decimals: number) {
let baseStr = typeof base === 'string' ? base : base.toString()
const neg = baseStr.includes('-')

baseStr = baseStr.replace(/-/g, '')

const a = baseStr.slice(0, -decimals) || '0'
const b = baseStr.slice(-decimals).padStart(decimals, '0')

const res = `${a}.${b}`

return neg ? `-${res}` : res
}

function dynamicPrecision(num: string) {
return new Decimal(num).lt(10) ? 4 : 0
}

function toDynamicPrecision(num: string) {
return toPrecision(num, dynamicPrecision(num))
return new CurrencyBalance(unclaimed, 18)
}

export function newClaim({ accountID, balance }: { accountID: string; balance: string }): Claim {
Expand All @@ -61,22 +35,25 @@ export function createTree(claims: Claim[]): MerkleTree {
}

function hashBlake2b(bytes: string | Uint8Array) {
return Buffer.from(blake2AsU8a(bytes))
return blake2AsHex(bytes)
}

function hashLeaf(accountID: string, balance: BN): Buffer {
// @ts-expect-error
return hashBlake2b(u8aConcat(hexToU8a(accountID), bnToU8a(balance, 128, true)))
export function hashLeaf(accountID: string, balance: BN) {
return hashBlake2b(u8aConcat(hexToU8a(accountID), bnToU8a(balance, { bitLength: 128 })))
}

function createProofs(tree: MerkleTree, leaf: Buffer): Proof[] {
function createProofs(tree: MerkleTree, leaf: Buffer | string) {
return tree.getProof(leaf)
}

function createProofFromClaim(tree: MerkleTree, claim: Claim): Proof[] {
export function createProofFromClaim(tree: MerkleTree, claim: Claim) {
return createProofs(tree, hashLeaf(claim.accountID, claim.balance))
}

export function createBufferProofFromClaim(tree: MerkleTree, claim: Claim): Buffer[] {
return createProofFromClaim(tree, claim).map((x) => x.data)
}

export function verifyProof(tree: MerkleTree, leaf: Buffer | string, proof: Proof[]): boolean {
return tree.verify(proof, leaf, tree.getRoot())
}
8 changes: 4 additions & 4 deletions centrifuge-app/src/utils/tinlake/useTinlakePortfolio.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,15 @@
import { CurrencyBalance } from '@centrifuge/centrifuge-js'
import { useWallet } from '@centrifuge/centrifuge-react'
import { BigNumber } from '@ethersproject/bignumber'
import BN from 'bn.js'
import { useQuery } from 'react-query'
import { useAddress } from '../useAddress'
import { Call, multicall } from './multicall'
import { IpfsPools, TokenResult } from './types'
import { useIpfsPools } from './useTinlakePools'

export function useTinlakePortfolio() {
const ipfsPools = useIpfsPools(false)
const { evm } = useWallet()
const { selectedAddress: address } = evm
const address = useAddress('evm')

const query = useQuery(['portfolio', address], () => getTinlakePortfolio(ipfsPools!, address!), {
enabled: !!address && !!ipfsPools,
Expand All @@ -24,7 +24,7 @@
}

async function getTinlakePortfolio(ipfsPools: IpfsPools, address: string) {
const toBN = (val: BN) => new CurrencyBalance(val, 18)
const toBN = (val: BigNumber) => new CurrencyBalance(val.toString(), 18)

const calls: Call[] = ipfsPools.active.flatMap((pool) => [
{
Expand Down Expand Up @@ -83,7 +83,7 @@

const tokenBalances = Object.entries(updatesPerToken).map(([tokenId, tokenResult]) => {
let tranche = TinlakeTranche.senior
ipfsPools.active.flatMap((pool) => {

Check warning on line 86 in centrifuge-app/src/utils/tinlake/useTinlakePortfolio.ts

View workflow job for this annotation

GitHub Actions / build-app

Array.prototype.flatMap() expects a return value from arrow function
if (tokenId === pool.addresses.JUNIOR_TOKEN) {
tranche = TinlakeTranche.junior
}
Expand Down
4 changes: 2 additions & 2 deletions centrifuge-app/src/utils/tinlake/useTinlakeRewards.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ async function getTinlakeUserRewards(ethAddr: string) {

if (response?.ok) {
const { data } = await response.json()
rewardBalances = data
;({ rewardBalances } = data)
} else {
throw new Error(`Error occurred while fetching user rewards for user ${ethAddr}`)
}
Expand All @@ -55,7 +55,7 @@ async function getTinlakeUserRewards(ethAddr: string) {
: null
transformed.totalEarnedRewards = new CurrencyBalance(new Decimal(rewardBalance.totalRewards).toFixed(0), 18)
transformed.unlinkedRewards = new CurrencyBalance(new Decimal(rewardBalance.linkableRewards).toFixed(0), 18)
transformed.links = (rewardBalance.links as any[]).map((link: any) => ({
transformed.links = rewardBalance.links.map((link) => ({
centAccountID: link.centAddress,
earned: new CurrencyBalance(new Decimal(link.rewardsAccumulated).toFixed(0), 18),
}))
Expand Down
2 changes: 1 addition & 1 deletion centrifuge-app/vite.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ export default defineConfig({
plugins: ['babel-plugin-styled-components'],
},
}),
// The Coinbase and WalletConnect connectors rely on node globals
// Coinbase, WalletConnect and MerkletreeJS rely on node globals
nodePolyfills(),
splitVendorChunkPlugin(),
],
Expand Down
Loading