diff --git a/centrifuge-app/src/components/Portfolio/Holdings.tsx b/centrifuge-app/src/components/Portfolio/Holdings.tsx
index 8ea2f99b75..8c2d462982 100644
--- a/centrifuge-app/src/components/Portfolio/Holdings.tsx
+++ b/centrifuge-app/src/components/Portfolio/Holdings.tsx
@@ -26,7 +26,7 @@ import { Tooltips } from '../Tooltips'
import { TransferTokensDrawer } from './TransferTokensDrawer'
import { usePortfolioTokens } from './usePortfolio'
-type Row = {
+export type Holding = {
currency: Token['currency']
poolId: string
trancheId: string
@@ -42,13 +42,13 @@ const columns: Column[] = [
{
align: 'left',
header: 'Token',
- cell: (token: Row) => {
+ cell: (token: Holding) => {
return
},
},
{
header: ,
- cell: ({ tokenPrice }: Row) => {
+ cell: ({ tokenPrice }: Holding) => {
return (
{formatBalance(tokenPrice || 1, 'USD', 4)}
@@ -59,7 +59,7 @@ const columns: Column[] = [
},
{
header: ,
- cell: ({ currency, position }: Row) => {
+ cell: ({ currency, position }: Holding) => {
return (
{formatBalanceAbbreviated(position || 0, currency?.symbol, 2)}
@@ -71,7 +71,7 @@ const columns: Column[] = [
},
{
header: ,
- cell: ({ marketValue }: Row) => {
+ cell: ({ marketValue }: Holding) => {
return (
{formatBalanceAbbreviated(marketValue || 0, 'USD', 2)}
@@ -82,14 +82,21 @@ const columns: Column[] = [
align: 'left',
},
{
- align: 'left',
+ align: 'right',
header: '', // invest redeem buttons
- cell: ({ showActions, poolId, trancheId, currency, connectedNetwork }: Row) => {
+ width: 'max-content',
+ cell: ({ showActions, poolId, trancheId, currency, connectedNetwork }: Holding) => {
return (
{showActions ? (
trancheId ? (
+
+ Receive
+
+
+ Send
+
Redeem
@@ -127,7 +134,7 @@ export function useHoldings(address?: string, chainId?: number, showActions = tr
const currencies = usePoolCurrencies()
const CFGPrice = useCFGTokenPrice()
- const tokens: Row[] = [
+ const tokens: Holding[] = [
...portfolioTokens.map((token) => ({
...token,
tokenPrice: token.tokenPrice.toDecimal() || Dec(0),
@@ -179,7 +186,7 @@ export function useHoldings(address?: string, chainId?: number, showActions = tr
connectedNetwork: wallet.connectedNetworkName,
}
}) || []),
- ...((wallet.connectedNetworkName === 'Centrifuge' && showActions) || centBalances?.native.balance.gtn(0)
+ ...((wallet.connectedNetwork === 'centrifuge' && showActions) || centBalances?.native.balance.gtn(0)
? [
{
currency: {
@@ -239,7 +246,6 @@ export function Holdings({
defaultView={openRedeemDrawer ? 'redeem' : 'invest'}
/>
navigate(pathname, { replace: true })}
/>
@@ -254,7 +260,7 @@ export function Holdings({
)
}
-const TokenWithIcon = ({ poolId, currency }: Row) => {
+const TokenWithIcon = ({ poolId, currency }: Holding) => {
const pool = usePool(poolId, false)
const { data: metadata } = usePoolMetadata(pool)
const cent = useCentrifuge()
diff --git a/centrifuge-app/src/components/Portfolio/TransferTokensDrawer.tsx b/centrifuge-app/src/components/Portfolio/TransferTokensDrawer.tsx
index 032b65d797..8651c8f440 100644
--- a/centrifuge-app/src/components/Portfolio/TransferTokensDrawer.tsx
+++ b/centrifuge-app/src/components/Portfolio/TransferTokensDrawer.tsx
@@ -1,9 +1,12 @@
import { CurrencyBalance } from '@centrifuge/centrifuge-js'
import {
- useBalances,
+ getChainInfo,
useCentEvmChainId,
+ useCentrifuge,
+ useCentrifugeConsts,
useCentrifugeTransaction,
useCentrifugeUtils,
+ useWallet,
} from '@centrifuge/centrifuge-react'
import {
AddressInput,
@@ -14,210 +17,315 @@ import {
Drawer,
IconCheckCircle,
IconCopy,
+ Select,
Shelf,
Stack,
Tabs,
TabsItem,
Text,
} from '@centrifuge/fabric'
-import { isAddress as isSubstrateAddress } from '@polkadot/util-crypto'
+import { isAddress } from '@polkadot/util-crypto'
+import BN from 'bn.js'
import Decimal from 'decimal.js-light'
import { isAddress as isEvmAddress } from 'ethers'
import { Field, FieldProps, Form, FormikProvider, useFormik } from 'formik'
-import React, { useMemo } from 'react'
+import React, { useEffect } from 'react'
import { useQuery } from 'react-query'
import { useLocation, useMatch, useNavigate } from 'react-router'
import styled from 'styled-components'
import centrifugeLogo from '../../assets/images/logoCentrifuge.svg'
+import { useInvestorStatus } from '../../pages/IssuerPool/Investors/InvestorStatus'
import { Dec } from '../../utils/Decimal'
import { copyToClipboard } from '../../utils/copyToClipboard'
import { formatBalance, formatBalanceAbbreviated } from '../../utils/formatting'
+import { useEvmTransaction } from '../../utils/tinlake/useEvmTransaction'
+import { useAddress } from '../../utils/useAddress'
import { useCFGTokenPrice, useDailyCFGPrice } from '../../utils/useCFGTokenPrice'
-import { useTransactionFeeEstimate } from '../../utils/useTransactionFeeEstimate'
+import { useActiveDomains, useLiquidityPools } from '../../utils/useLiquidityPools'
+import { combine, max, positiveNumber, required } from '../../utils/validation'
import { truncate } from '../../utils/web3'
import { FilterOptions, PriceChart } from '../Charts/PriceChart'
import { LabelValueStack } from '../LabelValueStack'
+import { LoadBoundary } from '../LoadBoundary'
+import { Spinner } from '../Spinner'
import { Tooltips } from '../Tooltips'
+import { Holding, useHoldings } from './Holdings'
type TransferTokensProps = {
- address: string
onClose: () => void
isOpen: boolean
}
-export const TransferTokensDrawer = ({ address, onClose, isOpen }: TransferTokensProps) => {
- const centBalances = useBalances(address)
- const CFGPrice = useCFGTokenPrice()
+export function TransferTokensDrawer({ onClose, isOpen }: TransferTokensProps) {
+ return (
+
+
+
+
+
+ )
+}
+
+function TransferTokensDrawerInner() {
+ const address = useAddress()
+ const consts = useCentrifugeConsts()
+ const tokens = useHoldings(address)
const isPortfolioPage = useMatch('/portfolio')
+
const { search } = useLocation()
const navigate = useNavigate()
const params = new URLSearchParams(search)
- const transferCurrencySymbol = params.get('receive') || params.get('send')
- const isNativeTransfer = transferCurrencySymbol?.toLowerCase() === centBalances?.native.currency.symbol.toLowerCase()
- const currency = useMemo(() => {
- if (isNativeTransfer && centBalances?.native) {
- return {
- ...centBalances.native,
- balance: new CurrencyBalance(
- centBalances?.native.balance.sub(centBalances.native.locked),
- centBalances.native.currency.decimals
- ),
- }
+ const transferKey = params.get('receive') || params.get('send') || ''
+ const isSend = !!params.get('send')
+ const isNativeTransfer = transferKey.toLowerCase() === consts.chainSymbol.toLowerCase()
+
+ function getHolding() {
+ if (!transferKey) return null
+
+ if (transferKey?.includes('.')) {
+ const [poolId, trancheId] = transferKey.split('.')
+ return tokens?.find((token) => token.poolId === poolId && token.trancheId === trancheId)
+ } else {
+ return tokens?.find((token) => token.currency.symbol === transferKey)
}
- return centBalances?.currencies.find((token) => token.currency.symbol === transferCurrencySymbol)
- }, [centBalances, isNativeTransfer, transferCurrencySymbol])
+ }
- const tokenPrice = isNativeTransfer ? CFGPrice : 1
+ const holding = getHolding()
- return (
-
-
-
- {transferCurrencySymbol || 'CFG'} Holdings
-
-
-
-
-
- ) : (
- 'Price'
- )
+ return holding ? (
+
+
+ {holding?.currency.symbol} Holdings
+
+
+
+
+ : 'Price'}
+ value={formatBalance(holding?.tokenPrice || 0, 'USD', 4)}
+ />
+
+ {isPortfolioPage && (
+
+
+ navigate({
+ search: index === 0 ? `send=${transferKey}` : `receive=${transferKey}`,
+ })
}
- value={formatBalance(tokenPrice || 0, 'USD', 4)}
- />
-
- {isPortfolioPage && (
-
-
- navigate({
- search: index === 0 ? `send=${transferCurrencySymbol}` : `receive=${transferCurrencySymbol}`,
- })
- }
- >
- Send
- Receive
-
- {params.get('send') ? (
-
- ) : (
-
- )}
-
- )}
- {isNativeTransfer && (
-
-
- Price
-
-
-
-
-
- )}
-
-
+ >
+ Send
+ Receive
+
+ {isSend ? (
+
+ ) : (
+
+ )}
+
+ )}
+ {isNativeTransfer && (
+
+
+ Price
+
+
+
+
+
+ )}
+
+ ) : (
+
)
}
-type SendReceiveProps = {
- address: string
- currency?: {
- balance: CurrencyBalance
- currency: { symbol: string; decimals: number; key: string | { ForeignAsset: number } }
- }
+type SendProps = {
+ holding: Holding
isNativeTransfer?: boolean
}
-const SendToken = ({ address, currency, isNativeTransfer }: SendReceiveProps) => {
+const SendToken = ({ holding, isNativeTransfer }: SendProps) => {
+ const address = useAddress()
+ const cent = useCentrifuge()
+ const { data: domains } = useActiveDomains(holding.poolId)
+ const activeDomains = domains?.filter((domain) => domain.hasDeployedLp) ?? []
+ const {
+ connectedNetwork,
+ isEvmOnSubstrate,
+ evm: { chains, chainId: connectedEvmChainId, getProvider },
+ } = useWallet()
+
const utils = useCentrifugeUtils()
- const chainId = useCentEvmChainId()
+ const centEvmChainId = useCentEvmChainId()
const { execute: transfer, isLoading } = useCentrifugeTransaction(
- `Send ${currency?.currency.symbol || 'CFG'}`,
+ `Send ${holding.currency.symbol}`,
(cent) => cent.tokens.transfer,
{
onSuccess: () => form.resetForm(),
}
)
-
- const { txFee, execute: estimatedTxFee } = useTransactionFeeEstimate((cent) => cent.tokens.transfer)
- useQuery(
- ['paymentInfo', address],
- async () => {
- if (!currency) return
- await estimatedTxFee([
- address,
- currency?.currency.key,
- CurrencyBalance.fromFloat(currency.balance.toDecimal(), currency?.currency.decimals),
- ])
- },
+ const { execute: evmTransfer, isLoading: evmIsLoading } = useEvmTransaction(
+ `Send ${holding.currency.symbol}`,
+ (cent) => cent.liquidityPools.transferTrancheTokens,
{
- enabled: !!address,
+ onSuccess: () => {
+ form.resetForm()
+ refetchAllowance()
+ },
}
)
- const form = useFormik<{ amount: Decimal | undefined; recipientAddress: string; isDisclaimerAgreed: boolean }>({
+ const form = useFormik<{
+ amount: Decimal | number | ''
+ chain: number | ''
+ recipientAddress: string
+ isDisclaimerAgreed: boolean
+ }>({
initialValues: {
- amount: undefined,
+ amount: '',
+ chain: '',
recipientAddress: '',
isDisclaimerAgreed: false,
},
validate(values) {
const errors: Partial<{ amount: string; recipientAddress: string; isDisclaimerAgreed: string }> = {}
+ const { chain, recipientAddress } = values
+ const validator = chain ? isEvmAddress : isAddress
+ const validAddress = validator(recipientAddress) ? recipientAddress : undefined
+ if (!validAddress) {
+ errors.recipientAddress = 'Invalid address'
+ } else if (!allowedTranches.includes(holding.trancheId)) {
+ errors.recipientAddress = 'Recipient is not allowed to receive this token'
+ }
if (!values.isDisclaimerAgreed && values.recipientAddress.startsWith('0x') && isNativeTransfer) {
errors.isDisclaimerAgreed = 'Please read and accept the above'
}
- if (values.amount && Dec(values.amount).gt(currency?.balance.toDecimal() || Dec(0))) {
- errors.amount = 'Amount exceeds wallet balance'
- }
- if (!values.amount || Dec(values.amount).lte(0)) {
- errors.amount = 'Amount must be greater than 0'
- }
- if (!(isSubstrateAddress(values.recipientAddress) || isEvmAddress(values.recipientAddress))) {
- errors.recipientAddress = 'Invalid address format'
- }
-
return errors
},
onSubmit: (values, actions) => {
- if (typeof values.amount === 'undefined') {
- actions.setErrors({ amount: 'Amount must be greater than 0' })
- } else if (!currency) {
- actions.setErrors({ amount: 'Invalid currency' })
- } else {
- if (isEvmAddress(values.recipientAddress)) {
- values.recipientAddress = utils.evmToSubstrateAddress(values.recipientAddress, chainId || 2031)
- }
+ let { recipientAddress, chain } = values
+ if (isEvmAddress(recipientAddress) && chain === '') {
+ recipientAddress = utils.evmToSubstrateAddress(recipientAddress, centEvmChainId!)
+ }
+ if (connectedNetwork === 'centrifuge' || isEvmOnSubstrate) {
transfer([
- values.recipientAddress,
- currency?.currency.key,
- CurrencyBalance.fromFloat(values.amount.toString(), currency?.currency.decimals),
+ recipientAddress,
+ holding.currency.key,
+ CurrencyBalance.fromFloat(values.amount.toString(), holding.currency.decimals),
+ chain === '' ? undefined : { evm: chain },
])
+ } else {
+ if (!liquidityPools?.[0]) return
+ const amount = CurrencyBalance.fromFloat(values.amount || 0, holding.currency.decimals)
+ const send = () =>
+ evmTransfer([
+ recipientAddress,
+ amount,
+ liquidityPools[0].lpAddress,
+ liquidityPools[0].trancheTokenAddress,
+ connectedEvmChainId!,
+ chain === '' ? 'centrifuge' : { evm: chain },
+ ])
+ if (isEvmAndNeedsApprove) {
+ executeApprove([
+ send,
+ liquidityPools[0].trancheTokenAddress,
+ CurrencyBalance.fromFloat(values.amount || 0, holding.currency.decimals),
+ connectedEvmChainId!,
+ ])
+ } else {
+ send()
+ }
}
actions.setSubmitting(false)
},
})
+ const { data: liquidityPools } = useLiquidityPools(
+ holding.poolId,
+ holding.trancheId,
+ !isEvmOnSubstrate ? connectedEvmChainId ?? -1 : -1 // typeof form.values.chain === 'number' ? form.values.chain :
+ )
+
+ const { allowedTranches } = useInvestorStatus(
+ holding.poolId,
+ form.values.recipientAddress,
+ form.values.chain || 'centrifuge'
+ )
+
+ const { data: allowanceData, refetch: refetchAllowance } = useQuery(
+ ['allowance', liquidityPools?.[0]?.trancheTokenAddress, connectedEvmChainId, address],
+ () =>
+ cent.liquidityPools.getCentrifugeRouterAllowance(
+ [liquidityPools![0].trancheTokenAddress!, address!, connectedEvmChainId!],
+ {
+ rpcProvider: getProvider(connectedEvmChainId!),
+ }
+ ),
+ {
+ enabled:
+ !!liquidityPools?.[0]?.trancheTokenAddress && !!connectedEvmChainId && !!address && isEvmAddress(address),
+ }
+ )
+
+ const isEvmAndNeedsApprove =
+ !!liquidityPools?.[0]?.trancheTokenAddress &&
+ allowanceData &&
+ allowanceData?.allowance?.toDecimal().lt(Dec(form.values.amount || 0))
+
+ const { execute: executeApprove, isLoading: isApproving } = useEvmTransaction(
+ `Send ${holding.currency.symbol}`,
+ (cent) =>
+ ([, ...args]: [cb: () => void, currencyAddress: string, amount: BN, chainId: number], options) =>
+ cent.liquidityPools.approveForCurrency(args, options),
+ {
+ onSuccess: ([cb]) => {
+ cb()
+ },
+ }
+ )
+
+ useEffect(() => {
+ form.validateForm()
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ }, [allowedTranches])
+
return (