diff --git a/centrifuge-app/.env-config/.env.development b/centrifuge-app/.env-config/.env.development index 8bf6e9b058..271aa0cd10 100644 --- a/centrifuge-app/.env-config/.env.development +++ b/centrifuge-app/.env-config/.env.development @@ -17,4 +17,4 @@ REACT_APP_WHITELISTED_ACCOUNTS= REACT_APP_TINLAKE_SUBGRAPH_URL=https://api.goldsky.com/api/public/project_clhi43ef5g4rw49zwftsvd2ks/subgraphs/main/1.0.2/gn REACT_APP_REWARDS_TREE_URL=https://storage.googleapis.com/rad-rewards-trees-kovan-staging/latest.json REACT_APP_WALLETCONNECT_ID=c32fa79350803519804a67fcab0b742a -REACT_APP_MEMBERLIST_ADMIN_PURE_PROXY=kAJ27w29x7gHM75xajP2yXVLjVBaKmmUTxHwgRuCoAcWaoEiz +REACT_APP_MEMBERLIST_ADMIN_PURE_PROXY=kAJ27w29x7gHM75xajP2yXVLjVBaKmmUTxHwgRuCoAcWaoEiz \ No newline at end of file diff --git a/centrifuge-app/src/components/DebugFlags/components/ConvertAddress.tsx b/centrifuge-app/src/components/DebugFlags/components/ConvertAddress.tsx new file mode 100644 index 0000000000..477295fbac --- /dev/null +++ b/centrifuge-app/src/components/DebugFlags/components/ConvertAddress.tsx @@ -0,0 +1,214 @@ +import { addressToHex } from '@centrifuge/centrifuge-js' +import { + getChainInfo, + truncateAddress, + useCentrifugeApi, + useCentrifugeUtils, + useWallet, +} from '@centrifuge/centrifuge-react' +import { Dialog, Grid, Select, Stack, Text, TextInput } from '@centrifuge/fabric' +import { isAddress as isEvmAddress } from '@ethersproject/address' +import { isAddress as isSubstrateAddress } from '@polkadot/util-crypto' +import * as React from 'react' +import { useQuery } from 'react-query' +import { firstValueFrom } from 'rxjs' +import { copyToClipboard } from '../../../utils/copyToClipboard' + +enum LocationType { + Parachain = 'Parachain', + Relaychain = 'Relaychain', + Native = 'Native', + EVM = 'EVM', +} + +export function ConvertAddress() { + const [open, setOpen] = React.useState(false) + const { + evm: { accounts: evmAccounts, chains }, + substrate: { accounts: substrateAccounts }, + } = useWallet() + + const [address, setAddress] = React.useState('') + const [locationType, setLocationType] = React.useState(LocationType.Parachain) + const [locationDetail, setLocationDetail] = React.useState('') + const utils = useCentrifugeUtils() + const api = useCentrifugeApi() + const addressListId = React.useId() + const locationListId = React.useId() + const isValidAddress = + locationType === LocationType.EVM + ? isEvmAddress(address) + : locationType === LocationType.Parachain + ? isSubstrateAddress(address) || isEvmAddress(address) + : !isEvmAddress(address) && isSubstrateAddress(address) + + function getMultilocation() { + if (!isValidAddress) return null + switch (locationType) { + case LocationType.Parachain: + if (!locationDetail) return null + return { + parents: 1, + interior: { + X2: [ + { + Parachain: locationDetail, + }, + isEvmAddress(address) + ? { + AccountKey20: { + network: null, + key: address, + }, + } + : { + AccountId32: { + id: addressToHex(address), + }, + }, + ], + }, + } + case LocationType.Relaychain: + return { + parents: 1, + interior: { + X1: { + AccountId32: { + id: addressToHex(address), + }, + }, + }, + } + case LocationType.Native: + return {} + case LocationType.EVM: + if (!locationDetail) return null + return { + parents: 0, + interior: { + X1: { + AccountKey20: { + network: { Ethereum: { chainId: locationDetail } }, + key: address, + }, + }, + }, + } + } + } + + const multilocation = getMultilocation() + + const { data: convertedAddress } = useQuery( + [address, locationType, locationDetail], + async () => { + if (locationType === LocationType.Native) return address + const addr = await firstValueFrom(api.call.accountConversionApi.conversionOf(multilocation)) + return addr.toHuman() as string + }, + { + enabled: !!address && !!multilocation && isValidAddress, + staleTime: Infinity, + } + ) + + return ( + <> + + setOpen(false)}> + + + + {evmAccounts?.map((addr) => ( + + ))} + {substrateAccounts?.map((acc) => ( + + ))} + + ({ - label: tranche.currency.symbol ?? '', - value: tranche.id, - })) - .reverse()} - value={state.trancheId} - onChange={(event) => setTrancheId(event.target.value as any)} - /> - )} - {connectedType && state.isDataLoading ? ( - - ) : state.isAllowedToInvest ? ( + return ( + <> + + + + + + {connectedType && ( <> - {canOnlyInvest ? ( - - ) : actualView === 'start' ? ( - <> - {state.order && - (!state.order.payoutTokenAmount.isZero() ? ( - - ) : !state.order.payoutCurrencyAmount.isZero() ? ( - - ) : null)} - - - - - - - - - - - - ) : actualView === 'invest' ? ( - setView('start')} autoFocus /> - ) : ( - setView('start')} autoFocus /> - )} + + {formatBalance(state.investmentValue, state.poolCurrency?.symbol, 2, 0)} + + + {formatBalance(state.trancheBalanceWithPending, state.trancheCurrency?.symbol, 2, 0)} + - ) : ( - // TODO: Show whether onboarding is in progress - - - {metadata?.pool?.issuer?.name} tokens are available to U.S. and Non-U.S. persons. U.S. persons must be - verified “accredited investors”.{' '} - - Learn more - - - - - - )} + + + + {pool.tranches.length > 1 && ( + ({ + value: chainId, + label: `${chainId} - ${getChainInfo(chains, Number(chainId)).name}`, + })), + ]} + onChange={(e) => { + setChain(e.target.value as any) + }} + /> + )} {address && !validAddress ? ( @@ -97,7 +135,7 @@ export const InvestorStatus: React.FC = () => { ) : null) )} - {pool?.tranches && validAddress && permissions && ( + {pool?.tranches && centAddress && permissions && ( { { align: 'left', header: 'Investment', - cell: (row: Token) => , + cell: (row: Token) => , }, { header: '', diff --git a/centrifuge-app/src/pages/IssuerPool/Investors/LiquidityPools.tsx b/centrifuge-app/src/pages/IssuerPool/Investors/LiquidityPools.tsx new file mode 100644 index 0000000000..b7d3e0d439 --- /dev/null +++ b/centrifuge-app/src/pages/IssuerPool/Investors/LiquidityPools.tsx @@ -0,0 +1,185 @@ +import { + ConnectionGuard, + useCentrifugeTransaction, + useGetExplorerUrl, + useGetNetworkName, + useNetworkName, +} from '@centrifuge/centrifuge-react' +import { Accordion, Button, IconExternalLink, Shelf, Stack, Text } from '@centrifuge/fabric' +import React from 'react' +import { useParams } from 'react-router' +import { PageSection } from '../../../components/PageSection' +import { AnchorTextLink } from '../../../components/TextLink' +import { find } from '../../../utils/helpers' +import { useEvmTransaction } from '../../../utils/tinlake/useEvmTransaction' +import { Domain, useActiveDomains } from '../../../utils/useLiquidityPools' +import { useSuitableAccounts } from '../../../utils/usePermissions' +import { usePool } from '../../../utils/usePools' + +function getDomainStatus(domain: Domain) { + if (!domain.isActive) { + return 'inactive' + } + if (Object.values(domain.liquidityPools).every((t) => Object.values(t).every((p) => !!p))) { + return 'deployed' + } + return 'deploying' +} + +export function LiquidityPools() { + const { pid: poolId } = useParams<{ pid: string }>() + const { data: domains, refetch } = useActiveDomains(poolId) + const getName = useGetNetworkName() + + const titles = { + inactive: 'Not active', + deploying: 'Action needed', + deployed: 'Active', + } + + return ( + + ({ + title: ( + <> + {getName(domain.chainId)} - {titles[getDomainStatus(domain)]} + + ), + body: , + })) ?? [] + } + /> + + ) +} + +function PoolDomain({ poolId, domain, refetch }: { poolId: string; domain: Domain; refetch: () => void }) { + const pool = usePool(poolId) + const getName = useGetNetworkName() + const explorer = useGetExplorerUrl(domain.chainId) + + const status = getDomainStatus(domain) + + return ( + + {status === 'inactive' ? ( + + ) : status === 'deploying' ? ( + + {pool.tranches.map((t) => ( + + {domain.undeployedTranches[t.id] && ( + + )} + {domain.currencies.map((currency, i) => ( + + {domain.trancheTokens[t.id] && !domain.liquidityPools[t.id][currency.address] && ( + + )} + + ))} + + ))} + + ) : ( + pool.tranches.map((tranche) => ( + + + + See {tranche.currency.name} token on {getName(domain.chainId)} + + + + + )) + )} + + ) +} + +function DeployTrancheButton({ + poolId, + trancheId, + domain, + onSuccess, +}: { + poolId: string + trancheId: string + domain: Domain + onSuccess: () => void +}) { + const pool = usePool(poolId) + const { execute, isLoading } = useEvmTransaction(`Deploy tranche`, (cent) => cent.liquidityPools.deployTranche, { + onSuccess, + }) + const tranche = find(pool.tranches, (t) => t.id === trancheId)! + + return ( + + ) +} + +function DeployLPButton({ + poolId, + trancheId, + currencyIndex, + domain, + onSuccess, +}: { + poolId: string + trancheId: string + domain: Domain + currencyIndex: number + onSuccess: () => void +}) { + const pool = usePool(poolId) + + const { execute, isLoading } = useEvmTransaction( + `Deploy liquidity pool`, + (cent) => cent.liquidityPools.deployLiquidityPool, + { onSuccess } + ) + const tranche = find(pool.tranches, (t) => t.id === trancheId)! + + return ( + + ) +} + +function EnableButton({ poolId, domain }: { poolId: string; domain: Domain }) { + const [account] = useSuitableAccounts({ poolId, poolRole: ['PoolAdmin'] }) + const name = useNetworkName(domain.chainId) + const { execute, isLoading } = useCentrifugeTransaction( + `Enable ${name}`, + (cent) => cent.liquidityPools.enablePoolOnDomain + ) + + const currenciesToAdd = domain.currencies + .filter((cur) => domain.currencyNeedsAdding[cur.address]) + .map((cur) => cur.key) + + return ( + + ) +} diff --git a/centrifuge-app/src/pages/IssuerPool/Investors/index.tsx b/centrifuge-app/src/pages/IssuerPool/Investors/index.tsx index 0573531a1f..bf1bc96549 100644 --- a/centrifuge-app/src/pages/IssuerPool/Investors/index.tsx +++ b/centrifuge-app/src/pages/IssuerPool/Investors/index.tsx @@ -6,6 +6,7 @@ import { PendingMultisigs } from '../../../components/PendingMultisigs' import { useSuitableAccounts } from '../../../utils/usePermissions' import { IssuerPoolHeader } from '../Header' import { InvestorStatus } from './InvestorStatus' +import { LiquidityPools } from './LiquidityPools' import { OnboardingSettings } from './OnboardingSettings' export function IssuerPoolInvestorsPage() { @@ -28,6 +29,7 @@ function IssuerPoolInvestors() { return ( <> {canEditInvestors && } + {isPoolAdmin && } {isPoolAdmin && } ) diff --git a/centrifuge-app/src/pages/Pool/Overview/index.tsx b/centrifuge-app/src/pages/Pool/Overview/index.tsx index d7f8f5f8e3..bdabd3700e 100644 --- a/centrifuge-app/src/pages/Pool/Overview/index.tsx +++ b/centrifuge-app/src/pages/Pool/Overview/index.tsx @@ -1,4 +1,4 @@ -import { Network, useWallet } from '@centrifuge/centrifuge-react' +import { useWallet } from '@centrifuge/centrifuge-react' import { Button, Shelf, Stack, Text, TextWithPlaceholder } from '@centrifuge/fabric' import * as React from 'react' import { useLocation, useParams } from 'react-router' @@ -13,7 +13,6 @@ import { PageWithSideBar } from '../../../components/PageWithSideBar' import { PoolToken } from '../../../components/PoolToken' import { Spinner } from '../../../components/Spinner' import { Tooltips } from '../../../components/Tooltips' -import { ethConfig } from '../../../config' import { formatDate, getAge } from '../../../utils/date' import { Dec } from '../../../utils/Decimal' import { formatBalance, formatBalanceAbbreviated, formatPercentage } from '../../../utils/formatting' @@ -63,19 +62,8 @@ export function PoolDetailSideBar({ investRef?: ActionsRef }) { const { pid: poolId } = useParams<{ pid: string }>() - const isTinlakePool = poolId.startsWith('0x') - const tinlakeNetworks = [ethConfig.network === 'goerli' ? 5 : 1] as Network[] - // TODO: fetch supported networks from centrifuge chain - const centrifugeNetworks = ['centrifuge'] as Network[] - return ( - + ) } diff --git a/centrifuge-app/src/pages/Pool/index.tsx b/centrifuge-app/src/pages/Pool/index.tsx index deb7abeabb..5484d6b7c9 100644 --- a/centrifuge-app/src/pages/Pool/index.tsx +++ b/centrifuge-app/src/pages/Pool/index.tsx @@ -1,26 +1,12 @@ -import { useWallet } from '@centrifuge/centrifuge-react' import * as React from 'react' -import { Route, Switch, useParams, useRouteMatch } from 'react-router' -import { ethConfig } from '../../config' +import { Route, Switch, useRouteMatch } from 'react-router' import { PoolDetailAssetsTab } from './Assets' import { PoolDetailLiquidityTab } from './Liquidity' import { PoolDetailOverviewTab } from './Overview' import { PoolDetailReportingTab } from './Reporting' export default function PoolDetailPage() { - const { pid } = useParams<{ pid: string }>() - const isTinlakePool = pid.startsWith('0x') - const { setScopedNetworks } = useWallet() const { path } = useRouteMatch() - - React.useEffect(() => { - setScopedNetworks(isTinlakePool ? [ethConfig.network === 'goerli' ? 5 : 1] : ['centrifuge']) - - return () => setScopedNetworks(null) - - // eslint-disable-next-line react-hooks/exhaustive-deps - }, []) - return ( diff --git a/centrifuge-app/src/pages/Swaps.tsx b/centrifuge-app/src/pages/Swaps.tsx new file mode 100644 index 0000000000..6af2528592 --- /dev/null +++ b/centrifuge-app/src/pages/Swaps.tsx @@ -0,0 +1,12 @@ +import { LayoutBase, LayoutMain } from '../components/LayoutBase' +import { Orders } from '../components/Swaps/Orders' + +export function SwapsPage() { + return ( + + + + + + ) +} diff --git a/centrifuge-app/src/utils/tinlake/useEvmTransaction.ts b/centrifuge-app/src/utils/tinlake/useEvmTransaction.ts index c75de85be6..8f2330b588 100644 --- a/centrifuge-app/src/utils/tinlake/useEvmTransaction.ts +++ b/centrifuge-app/src/utils/tinlake/useEvmTransaction.ts @@ -16,7 +16,7 @@ export function useEvmTransaction>( transactionCallback: ( centrifuge: Centrifuge ) => (args: T, options?: TransactionRequest) => Observable, - options: { onSuccess?: (args: T, result: any) => void; onError?: (error: any) => void } = {} + options: { onSuccess?: (args: T, result: any) => void; onError?: (error: any) => void; chainId?: number } = {} ) { const { addOrUpdateTransaction, updateTransaction } = useTransactions() const { showWallets, evm, walletDialog } = useWallet() @@ -64,7 +64,7 @@ export function useEvmTransaction>( if (!selectedAddress || !provider) { pendingTransaction.current = { id, args, options } - showWallets(1) + showWallets(chainId ?? 1) } else { doTransaction(id, args, options) } diff --git a/centrifuge-app/src/utils/useAddress.ts b/centrifuge-app/src/utils/useAddress.ts index a42f811866..c77dc573b8 100644 --- a/centrifuge-app/src/utils/useAddress.ts +++ b/centrifuge-app/src/utils/useAddress.ts @@ -3,11 +3,10 @@ import { useDebugFlags } from '../components/DebugFlags' export function useAddress(typeOverride?: 'substrate' | 'evm') { const { address: debugSubstrateAddress, evmAddress: debugEvmAddress } = useDebugFlags() - const { connectedType } = useWallet() const address = useWalletAddress(typeOverride) const { isEvmOnSubstrate } = useWallet() const debugAddress = - typeOverride === 'evm' || (connectedType === 'evm' && !isEvmOnSubstrate) ? debugEvmAddress : debugSubstrateAddress + typeOverride === 'evm' || (!typeOverride && !isEvmOnSubstrate) ? debugEvmAddress : debugSubstrateAddress return (debugAddress as string) || address } diff --git a/centrifuge-app/src/utils/useLiquidityPools.ts b/centrifuge-app/src/utils/useLiquidityPools.ts new file mode 100644 index 0000000000..cb23db97a7 --- /dev/null +++ b/centrifuge-app/src/utils/useLiquidityPools.ts @@ -0,0 +1,136 @@ +import Centrifuge from '@centrifuge/centrifuge-js' +import { useCentrifuge, useCentrifugeQuery, useWallet } from '@centrifuge/centrifuge-react' +import { useQuery } from 'react-query' + +export function useDomainRouters(suspense?: boolean) { + const [data] = useCentrifugeQuery(['domainRouters'], (cent) => cent.liquidityPools.getDomainRouters(), { suspense }) + + return data +} + +export type Domain = (ReturnType extends Promise ? T : never) & { + chainId: number + managerAddress: string +} + +export function useActiveDomains(poolId: string, suspense?: boolean) { + const { + evm: { getProvider }, + } = useWallet() + const cent = useCentrifuge() + const routers = useDomainRouters(suspense) + const query = useQuery( + ['activeDomains', poolId, routers?.length], + async () => { + const results = await Promise.allSettled( + routers!.map(async (r) => { + const rpcProvider = getProvider(r.chainId) + const manager = await cent.liquidityPools.getManagerFromRouter([r.router], { + rpcProvider, + }) + const pool = await cent.liquidityPools.getPool([r.chainId, manager, poolId], { rpcProvider }) + return [manager, pool] as const + }) + ) + return results + .map((result, i) => { + if (result.status === 'rejected') { + console.error(result.reason) + return null as never + } + const [manager, pool] = result.value + const router = routers![i] + const domain: Domain = { + ...pool, + chainId: router.chainId, + managerAddress: manager, + } + return domain + }) + .filter(Boolean) + }, + { + enabled: !!routers?.length && !poolId.startsWith('0x'), + staleTime: Infinity, + suspense, + } + ) + + return query +} + +export function useLiquidityPools(poolId: string, trancheId: string) { + const { + evm: { chainId, getProvider }, + } = useWallet() + const cent = useCentrifuge() + const { data: domains } = useActiveDomains(poolId) + const managerAddress = domains?.find((d) => d.chainId === chainId)?.managerAddress + + const query = useQuery( + ['lps', poolId, trancheId, chainId], + () => + cent.liquidityPools.getLiquidityPools([managerAddress!, poolId, trancheId, chainId!], { + rpcProvider: getProvider(chainId!), + }), + { + enabled: !!managerAddress, + staleTime: Infinity, + } + ) + + return query +} + +export function useLPEvents(poolId: string, trancheId: string, lpAddress?: string) { + const { + evm: { chainId, getProvider, selectedAddress }, + } = useWallet() + const cent = useCentrifuge() + const { data: lps } = useLiquidityPools(poolId, trancheId) + const lp = lps?.find((l) => l.lpAddress === lpAddress) + + const query = useQuery( + ['lpDepositedEvents', chainId, lp?.lpAddress, selectedAddress], + () => + cent.liquidityPools.getRecentLPEvents([lp!.lpAddress, selectedAddress!], { + rpcProvider: getProvider(chainId!), + }), + + { + enabled: !!lp && !!selectedAddress, + } + ) + return query +} + +export function useLiquidityPoolInvestment(poolId: string, trancheId: string, lpIndex?: number) { + const { + evm: { chainId, getProvider, selectedAddress }, + } = useWallet() + const cent = useCentrifuge() + const { data: domains } = useActiveDomains(poolId) + const managerAddress = domains?.find((d) => d.chainId === chainId)?.managerAddress + + const { data: lps } = useLiquidityPools(poolId, trancheId) + const lp = lps?.[lpIndex ?? 0] + + const query = useQuery( + ['lpInvestment', chainId, lp?.lpAddress, selectedAddress], + async () => ({ + ...(await cent.liquidityPools.getLiquidityPoolInvestment( + [selectedAddress!, managerAddress!, lp!.lpAddress, lp!.currencyAddress], + { + rpcProvider: getProvider(chainId!), + } + )), + ...lp!, + }), + + { + enabled: !!lp && !!selectedAddress, + } + ) + + return query +} diff --git a/centrifuge-app/src/utils/usePermissions.ts b/centrifuge-app/src/utils/usePermissions.tsx similarity index 90% rename from centrifuge-app/src/utils/usePermissions.ts rename to centrifuge-app/src/utils/usePermissions.tsx index f961aa7eba..2c450f5e53 100644 --- a/centrifuge-app/src/utils/usePermissions.ts +++ b/centrifuge-app/src/utils/usePermissions.tsx @@ -6,8 +6,15 @@ import { isSameAddress, PoolRoles, } from '@centrifuge/centrifuge-js' -import { useCentrifugeQuery, useWallet } from '@centrifuge/centrifuge-react' -import { useMemo } from 'react' +import { + CombinedSubstrateAccount, + truncateAddress, + useCentrifugeQuery, + useCentrifugeUtils, + useWallet, +} from '@centrifuge/centrifuge-react' +import { Select } from '@centrifuge/fabric' +import * as React from 'react' import { combineLatest, filter, map, repeatWhen, switchMap } from 'rxjs' import { diffPermissions } from '../pages/IssuerPool/Configuration/Admins' import { useCollections } from './useCollections' @@ -106,6 +113,28 @@ type SuitableConfig = { proxyType?: string[] | ((accountProxyTypes: string[]) => boolean) } +export function useSuitableAccountPicker(config: SuitableConfig) { + const accounts = useSuitableAccounts(config) + const [account, setAccount] = React.useState(accounts[0]) + const utils = useCentrifugeUtils() + + const pickerElement = + accounts?.length > 1 ? ( +