From 4777512f670b18d208ac2c086130366cc1e89370 Mon Sep 17 00:00:00 2001 From: Sophia Date: Tue, 22 Aug 2023 11:40:18 -0400 Subject: [PATCH 001/202] OnboardingAPI: liquidity pool authentication and remark signing (#1539) * Add button to onboard to junior * Add chain id to auth token * Add chain id types * Add base network behind debug flag * Fix error with liquidity stakes * Implement remark signing for base networks * Fix types * Hide onboarding button for not yet supported networks * Add metadata to extended chain info for centrfuge chains * Localize countdown hook to prevent rerenders * Remove extra chars * Remove avalanche and check for evm on cent chain in api * Fix error from throwing in invest redeem component --- .../src/components/DebugFlags/config.ts | 4 +- .../components/InvestRedeem/InvestRedeem.tsx | 12 ++-- .../InvestRedeemCentrifugeProvider.tsx | 6 +- .../InvestRedeem/InvestRedeemProvider.tsx | 6 ++ .../LiquidityRewardsClaimer.tsx | 8 ++- .../LiquidityRewardsProvider.tsx | 4 +- .../src/components/LiquidityRewards/types.ts | 1 - .../src/components/OnboardingAuthProvider.tsx | 16 +++-- centrifuge-app/src/components/Root.tsx | 24 +++---- centrifuge-app/src/config.ts | 3 +- .../queries/useSignAndSendDocuments.ts | 4 +- .../pages/Onboarding/queries/useSignRemark.ts | 11 ++- .../src/pages/Pool/Overview/index.tsx | 8 ++- centrifuge-js/src/modules/rewards.ts | 9 ++- .../WalletProvider/ConnectionGuard.tsx | 5 +- .../WalletProvider/WalletDialog.tsx | 18 +++-- .../WalletProvider/WalletProvider.tsx | 6 +- .../components/WalletProvider/evm/chains.ts | 18 ++--- .../controllers/auth/authenticateWallet.ts | 61 +---------------- onboarding-api/src/database/index.ts | 1 + onboarding-api/src/middleware/verifyAuth.ts | 6 +- .../annotateAgreementAndSignAsInvestor.ts | 7 +- .../src/utils/networks/centrifuge.ts | 38 ++++++++++- onboarding-api/src/utils/networks/evm.ts | 68 +++++++++++++++++++ .../src/utils/networks/networkSwitch.ts | 27 ++++---- onboarding-api/src/utils/networks/tinlake.ts | 48 ++++--------- onboarding-api/src/utils/types.d.ts | 1 + 27 files changed, 232 insertions(+), 188 deletions(-) create mode 100644 onboarding-api/src/utils/networks/evm.ts diff --git a/centrifuge-app/src/components/DebugFlags/config.ts b/centrifuge-app/src/components/DebugFlags/config.ts index 99d41dc24f..866e55c54c 100644 --- a/centrifuge-app/src/components/DebugFlags/config.ts +++ b/centrifuge-app/src/components/DebugFlags/config.ts @@ -34,7 +34,7 @@ export type Key = | 'evmAddress' | 'batchMintNFTs' | 'persistDebugFlags' - | 'showAvalanche' + | 'showBase' | 'showUnusedFlags' | 'allowInvestBelowMin' | 'alternativeTheme' @@ -68,7 +68,7 @@ export const flagsConfig: Record = { default: false, alwaysShow: true, }, - showAvalanche: { + showBase: { type: 'checkbox', default: false, alwaysShow: true, diff --git a/centrifuge-app/src/components/InvestRedeem/InvestRedeem.tsx b/centrifuge-app/src/components/InvestRedeem/InvestRedeem.tsx index ebfbc91ad3..caf1a0e0ac 100644 --- a/centrifuge-app/src/components/InvestRedeem/InvestRedeem.tsx +++ b/centrifuge-app/src/components/InvestRedeem/InvestRedeem.tsx @@ -26,7 +26,7 @@ import css from '@styled-system/css' import Decimal from 'decimal.js-light' import { Field, FieldProps, Form, FormikErrors, FormikProvider, useFormik } from 'formik' import * as React from 'react' -import { useHistory } from 'react-router-dom' +import { useHistory, useParams } from 'react-router-dom' import styled from 'styled-components' import { Dec } from '../../utils/Decimal' import { formatBalance, roundDown } from '../../utils/formatting' @@ -310,19 +310,19 @@ function InvestRedeemInner({ view, setView, setTrancheId, networks }: InnerProps return null } -const OnboardingButton = ({ networks }: { networks: Network[] | undefined }) => { +const OnboardingButton = ({ networks }: { networks: Network[] | undefined; trancheId?: string }) => { const { showWallets, showNetworks, connectedType } = useWallet() const { state } = useInvestRedeem() - const pool = usePool(state.poolId) + const { pid: poolId } = useParams<{ pid: string }>() + const pool = usePool(poolId) const { data: metadata } = usePoolMetadata(pool) const isTinlakePool = pool.id.startsWith('0x') + const history = useHistory() const trancheName = state.trancheId.split('-')[1] === '0' ? 'junior' : 'senior' - const centPoolInvestStatus = metadata?.onboarding?.tranches?.[state.trancheId].openForOnboarding ? 'open' : 'closed' + const centPoolInvestStatus = metadata?.onboarding?.tranches?.[state?.trancheId]?.openForOnboarding ? 'open' : 'closed' const investStatus = isTinlakePool ? metadata?.pool?.newInvestmentsStatus?.[trancheName] : centPoolInvestStatus - const history = useHistory() - const getOnboardingButtonText = () => { if (connectedType) { if (investStatus === 'request') { diff --git a/centrifuge-app/src/components/InvestRedeem/InvestRedeemCentrifugeProvider.tsx b/centrifuge-app/src/components/InvestRedeem/InvestRedeemCentrifugeProvider.tsx index 3fa9789489..449cf540ee 100644 --- a/centrifuge-app/src/components/InvestRedeem/InvestRedeemCentrifugeProvider.tsx +++ b/centrifuge-app/src/components/InvestRedeem/InvestRedeemCentrifugeProvider.tsx @@ -23,9 +23,7 @@ export function InvestRedeemCentrifugeProvider({ poolId, trancheId, children }: const tranche = pool.tranches.find((t) => t.id === trancheId) const { data: metadata, isLoading: isMetadataLoading } = usePoolMetadata(pool) const trancheMeta = metadata?.tranches?.[trancheId] - const { - state: { combinedStakes }, - } = useLiquidityRewards() + const { state: liquidityState } = useLiquidityRewards() if (!tranche) throw new Error(`Token not found. Pool id: ${poolId}, token id: ${trancheId}`) @@ -35,7 +33,7 @@ export function InvestRedeemCentrifugeProvider({ poolId, trancheId, children }: const price = tranche.tokenPrice?.toDecimal() ?? Dec(1) const investToCollect = order?.payoutTokenAmount.toDecimal() ?? Dec(0) const pendingRedeem = order?.remainingRedeemToken.toDecimal() ?? Dec(0) - const stakedAmount = combinedStakes ?? Dec(0) + const stakedAmount = liquidityState?.combinedStakes ?? Dec(0) const combinedBalance = trancheBalance.add(investToCollect).add(pendingRedeem).add(stakedAmount) const investmentValue = combinedBalance.mul(price) const poolCurBalance = diff --git a/centrifuge-app/src/components/InvestRedeem/InvestRedeemProvider.tsx b/centrifuge-app/src/components/InvestRedeem/InvestRedeemProvider.tsx index 0d3f293db0..beac16627d 100644 --- a/centrifuge-app/src/components/InvestRedeem/InvestRedeemProvider.tsx +++ b/centrifuge-app/src/components/InvestRedeem/InvestRedeemProvider.tsx @@ -1,3 +1,4 @@ +import { useWallet } from '@centrifuge/centrifuge-react' import * as React from 'react' import { InvestRedeemCentrifugeProvider } from './InvestRedeemCentrifugeProvider' import { InvestRedeemTinlakeProvider } from './InvestRedeemTinlakeProvider' @@ -13,6 +14,11 @@ export function useInvestRedeem() { export function InvestRedeemProvider(props: Props) { const isTinlakePool = props.poolId.startsWith('0x') + const { connectedNetwork } = useWallet() + if (connectedNetwork && [1, 5, 8453, 84531].includes(connectedNetwork as any)) { + return null + } + const Comp = isTinlakePool ? InvestRedeemTinlakeProvider : InvestRedeemCentrifugeProvider return diff --git a/centrifuge-app/src/components/LiquidityRewards/LiquidityRewardsClaimer.tsx b/centrifuge-app/src/components/LiquidityRewards/LiquidityRewardsClaimer.tsx index d8d50248b0..2cd6a2f5bd 100644 --- a/centrifuge-app/src/components/LiquidityRewards/LiquidityRewardsClaimer.tsx +++ b/centrifuge-app/src/components/LiquidityRewards/LiquidityRewardsClaimer.tsx @@ -2,13 +2,15 @@ import { Box, Button, Card, Shelf, Stack, Text } from '@centrifuge/fabric' import * as React from 'react' import { Dec } from '../../utils/Decimal' import { formatBalance, formatBalanceAbbreviated } from '../../utils/formatting' +import { useClaimCountdown } from './hooks' import { useLiquidityRewards } from './LiquidityRewardsContext' export function LiquidityRewardsClaimer() { const { - state: { countdown, rewards, canClaim, isLoading, nativeCurrency }, + state: { rewards, canClaim, isLoading, nativeCurrency }, actions: { claim }, } = useLiquidityRewards() + const claimCountdown = useClaimCountdown() const rewardsAmount = rewards && !rewards?.isZero() @@ -36,9 +38,9 @@ export function LiquidityRewardsClaimer() { - {!!countdown && ( + {!!claimCountdown && ( - New rounds of rewards will be available in {countdown} + New rounds of rewards will be available in {claimCountdown} )} diff --git a/centrifuge-app/src/components/LiquidityRewards/LiquidityRewardsProvider.tsx b/centrifuge-app/src/components/LiquidityRewards/LiquidityRewardsProvider.tsx index 74ed6db15d..909001841f 100644 --- a/centrifuge-app/src/components/LiquidityRewards/LiquidityRewardsProvider.tsx +++ b/centrifuge-app/src/components/LiquidityRewards/LiquidityRewardsProvider.tsx @@ -5,7 +5,7 @@ import * as React from 'react' import { Dec } from '../../utils/Decimal' import { useAddress } from '../../utils/useAddress' import { usePendingCollect, usePool } from '../../utils/usePools' -import { useAccountStakes, useClaimCountdown, useComputeLiquidityRewards, useRewardCurrencyGroup } from './hooks' +import { useAccountStakes, useComputeLiquidityRewards, useRewardCurrencyGroup } from './hooks' import { LiquidityRewardsContext } from './LiquidityRewardsContext' import { LiquidityRewardsActions, LiquidityRewardsProviderProps, LiquidityRewardsState } from './types' @@ -22,7 +22,6 @@ function Provider({ poolId, trancheId, children }: LiquidityRewardsProviderProps const stakes = useAccountStakes(address, poolId, trancheId) const rewards = useComputeLiquidityRewards(address, poolId, trancheId) const balances = useBalances(address) - const countdown = useClaimCountdown() const rewardCurrencyGroup = useRewardCurrencyGroup(poolId, trancheId) const trancheBalance = @@ -52,7 +51,6 @@ function Provider({ poolId, trancheId, children }: LiquidityRewardsProviderProps const state: LiquidityRewardsState = { tranche, - countdown, rewards, stakeableAmount, combinedStakes, diff --git a/centrifuge-app/src/components/LiquidityRewards/types.ts b/centrifuge-app/src/components/LiquidityRewards/types.ts index d0f951b1d1..d3e7eeb4d2 100644 --- a/centrifuge-app/src/components/LiquidityRewards/types.ts +++ b/centrifuge-app/src/components/LiquidityRewards/types.ts @@ -9,7 +9,6 @@ export type LiquidityRewardsProviderProps = { export type LiquidityRewardsState = { tranche: Token | undefined - countdown: ClaimCountDown | null rewards: Decimal | null | undefined stakeableAmount: Decimal | null combinedStakes: Decimal | null diff --git a/centrifuge-app/src/components/OnboardingAuthProvider.tsx b/centrifuge-app/src/components/OnboardingAuthProvider.tsx index b06c6a15b1..53cfb23100 100644 --- a/centrifuge-app/src/components/OnboardingAuthProvider.tsx +++ b/centrifuge-app/src/components/OnboardingAuthProvider.tsx @@ -16,7 +16,7 @@ const AUTHORIZED_ONBOARDING_PROXY_TYPES = ['Any', 'Invest', 'NonTransfer', 'NonP export function OnboardingAuthProvider({ children }: { children: React.ReactNode }) { const { substrate: { selectedWallet, selectedProxies, selectedAccount, evmChainId }, - evm: { selectedAddress }, + evm: { selectedAddress, ...evm }, isEvmOnSubstrate, } = useWallet() const cent = useCentrifuge() @@ -51,9 +51,9 @@ export function OnboardingAuthProvider({ children }: { children: React.ReactNode if (selectedAccount?.address && selectedWallet?.signer) { await loginWithSubstrate(selectedAccount?.address, selectedWallet.signer, cent, proxy) } else if (isEvmOnSubstrate && selectedAddress && provider?.getSigner()) { - await loginWithEvm(selectedAddress, provider.getSigner(), evmChainId) + await loginWithEvm(selectedAddress, provider.getSigner(), evmChainId, isEvmOnSubstrate) } else if (selectedAddress && provider?.getSigner()) { - await loginWithEvm(selectedAddress, provider.getSigner()) + await loginWithEvm(selectedAddress, provider.getSigner(), evm.chainId) } throw new Error('network not supported') } catch { @@ -175,13 +175,14 @@ const loginWithSubstrate = async (hexAddress: string, signer: Wallet['signer'], const { token, payload } = await cent.auth.generateJw3t(address, signer) if (token) { + const centChainId = await cent.getChainId() const authTokenRes = await fetch(`${import.meta.env.REACT_APP_ONBOARDING_API_URL}/authenticateWallet`, { method: 'POST', headers: { 'Content-Type': 'application/json', }, credentials: 'include', - body: JSON.stringify({ jw3t: token, nonce, network: 'substrate' }), + body: JSON.stringify({ jw3t: token, nonce, network: 'substrate', chainId: centChainId }), }) if (authTokenRes.status !== 200) { throw new Error('Failed to authenticate wallet') @@ -196,7 +197,7 @@ const loginWithSubstrate = async (hexAddress: string, signer: Wallet['signer'], } } -const loginWithEvm = async (address: string, signer: any, evmChainId?: number) => { +const loginWithEvm = async (address: string, signer: any, evmChainId?: number, isEvmOnSubstrate?: boolean) => { const nonceRes = await fetch(`${import.meta.env.REACT_APP_ONBOARDING_API_URL}/nonce`, { method: 'POST', headers: { @@ -216,7 +217,7 @@ Please sign to authenticate your wallet URI: ${origin} Version: 1 -Chain ID: ${evmChainId ? evmChainId : import.meta.env.REACT_APP_TINLAKE_NETWORK === 'mainnet' ? 1 : 5 /* goerli */} +Chain ID: ${evmChainId || 1} Nonce: ${nonce} Issued At: ${new Date().toISOString()}` @@ -232,7 +233,8 @@ Issued At: ${new Date().toISOString()}` signature: signedMessage, address, nonce, - network: evmChainId ? 'evmOnSubstrate' : 'evm', + network: isEvmOnSubstrate ? 'evmOnSubstrate' : 'evm', + chainId: evmChainId || 1, }), }) if (tokenRes.status !== 200) { diff --git a/centrifuge-app/src/components/Root.tsx b/centrifuge-app/src/components/Root.tsx index 4faae3a7ed..b13f54116b 100644 --- a/centrifuge-app/src/components/Root.tsx +++ b/centrifuge-app/src/components/Root.tsx @@ -83,9 +83,9 @@ const evmChains: EvmChains = urls: [`https://mainnet.infura.io/v3/${infuraKey}`], iconUrl: ethereumLogo, }, - 43114: { - urls: ['https://api.avax.network/ext/bc/C/rpc'], - iconUrl: 'https://cryptologos.cc/logos/avalanche-avax-logo.svg?v=013', + 8453: { + urls: ['https://mainnet.base.org'], + iconUrl: 'https://docs.base.org/img/logo_dark.svg', }, } : { @@ -97,20 +97,20 @@ const evmChains: EvmChains = urls: [`https://goerli.infura.io/v3/${infuraKey}`], iconUrl: goerliLogo, }, - 43114: { - urls: ['https://api.avax.network/ext/bc/C/rpc'], - iconUrl: 'https://cryptologos.cc/logos/avalanche-avax-logo.svg?v=013', + 8453: { + urls: ['https://mainnet.base.org'], + iconUrl: 'https://docs.base.org/img/logo.svg', }, - 43113: { - urls: ['https://api.avax-test.network/ext/bc/C/rpc'], - iconUrl: 'https://cryptologos.cc/logos/avalanche-avax-logo.svg?v=013', + 84531: { + urls: ['https://goerli.base.org'], + iconUrl: 'https://docs.base.org/img/logo.svg', }, } export function Root() { const [isThemeToggled, setIsThemeToggled] = React.useState(!!initialFlagsState.alternativeTheme) const [showAdvancedAccounts, setShowAdvancedAccounts] = React.useState(!!initialFlagsState.showAdvancedAccounts) - const [showAvalanche, setShowAvalanche] = React.useState(!!initialFlagsState.showAvalanche) + const [showBase, setShowBase] = React.useState(!!initialFlagsState.showBase) return ( <> @@ -137,7 +137,7 @@ export function Root() { subscanUrl={import.meta.env.REACT_APP_SUBSCAN_URL} walletConnectId={import.meta.env.REACT_APP_WALLETCONNECT_ID} showAdvancedAccounts={showAdvancedAccounts} - showAvalanche={showAvalanche} + showBase={showBase} > @@ -145,7 +145,7 @@ export function Root() { onChange={(state) => { setIsThemeToggled(!!state.alternativeTheme) setShowAdvancedAccounts(!!state.showAdvancedAccounts) - setShowAvalanche(!!state.showAvalanche) + setShowBase(!!state.showBase) }} > diff --git a/centrifuge-app/src/config.ts b/centrifuge-app/src/config.ts index ae6e22bee2..d5b9f2d954 100644 --- a/centrifuge-app/src/config.ts +++ b/centrifuge-app/src/config.ts @@ -125,7 +125,6 @@ const goerliConfig = { poolRegistryAddress: '0x5ba1e12693dc8f9c48aad8770482f4739beed696', tinlakeUrl: 'https://goerli.staging.tinlake.cntrfg.com/', poolsHash: 'QmQe9NTiVJnVcb4srw6sBpHefhYieubR7v3J8ZriULQ8vB', // TODO: add registry to config and fetch poolHash - remarkerAddress: '0x6E395641087a4938861d7ada05411e3146175F58', blockExplorerUrl: 'https://goerli.etherscan.io', } const mainnetConfig = { @@ -133,13 +132,13 @@ const mainnetConfig = { poolRegistryAddress: '0x5ba1e12693dc8f9c48aad8770482f4739beed696', tinlakeUrl: 'https://tinlake.centrifuge.io', poolsHash: 'QmNvauf8E6TkUiyF1ZgtYtntHz335tCswKp2uhBH1fiui1', // TODO: add registry to config and fetch poolHash - remarkerAddress: '0x075f37451e7a4877f083aa070dd47a6969af2ced', blockExplorerUrl: 'https://etherscan.io', } export const ethConfig = { network: ethNetwork, multicallContractAddress: '0x5ba1e12693dc8f9c48aad8770482f4739beed696', // Same for all networks + remarkerAddress: '0x3E39db43035981c2C31F7Ffa4392f25231bE4477', // Same for all networks ...(ethNetwork === 'goerli' ? goerliConfig : mainnetConfig), } diff --git a/centrifuge-app/src/pages/Onboarding/queries/useSignAndSendDocuments.ts b/centrifuge-app/src/pages/Onboarding/queries/useSignAndSendDocuments.ts index 31822fc48b..fd48989066 100644 --- a/centrifuge-app/src/pages/Onboarding/queries/useSignAndSendDocuments.ts +++ b/centrifuge-app/src/pages/Onboarding/queries/useSignAndSendDocuments.ts @@ -1,4 +1,4 @@ -import { useTransactions } from '@centrifuge/centrifuge-react' +import { Network, useTransactions } from '@centrifuge/centrifuge-react' import { useMutation } from 'react-query' import { useOnboardingAuth } from '../../../components/OnboardingAuthProvider' import { OnboardingPool, useOnboarding } from '../../../components/OnboardingProvider' @@ -14,7 +14,7 @@ export const useSignAndSendDocuments = () => { const trancheId = pool.trancheId const mutation = useMutation( - async (transactionInfo: { txHash: string; blockNumber: string }) => { + async (transactionInfo: { txHash: string; blockNumber: string; chainId: Network }) => { addOrUpdateTransaction({ id: txIdSendDocs, title: `Send documents to issuers`, diff --git a/centrifuge-app/src/pages/Onboarding/queries/useSignRemark.ts b/centrifuge-app/src/pages/Onboarding/queries/useSignRemark.ts index 0f450276cb..08f3f7b7d4 100644 --- a/centrifuge-app/src/pages/Onboarding/queries/useSignRemark.ts +++ b/centrifuge-app/src/pages/Onboarding/queries/useSignRemark.ts @@ -1,4 +1,5 @@ import { + Network, useBalances, useCentrifuge, useCentrifugeTransaction, @@ -24,6 +25,7 @@ export const useSignRemark = ( txHash: string blockNumber: string isEvmOnSubstrate?: boolean + chainId: Network }, unknown > @@ -37,6 +39,7 @@ export const useSignRemark = ( connectedType, isEvmOnSubstrate, substrate: { selectedAddress, selectedAccount }, + connectedNetwork, } = useWallet() const [expectedTxFee, setExpectedTxFee] = React.useState(Dec(0)) const balances = useBalances(selectedAddress || '') @@ -59,7 +62,12 @@ export const useSignRemark = ( // @ts-expect-error blockNumber = result.blockNumber.toString() } - await sendDocumentsToIssuer({ txHash, blockNumber, isEvmOnSubstrate }) + await sendDocumentsToIssuer({ + txHash, + blockNumber, + isEvmOnSubstrate, + chainId: connectedNetwork || 'centrifuge', + }) setIsSubstrateTxLoading(false) } catch (e) { setIsSubstrateTxLoading(false) @@ -157,6 +165,7 @@ export const useSignRemark = ( await sendDocumentsToIssuer({ txHash: result.hash, blockNumber: finalizedTx.blockNumber.toString(), + chainId: connectedNetwork || 'centrifuge', }) updateTransaction(txId, () => ({ status: 'succeeded', diff --git a/centrifuge-app/src/pages/Pool/Overview/index.tsx b/centrifuge-app/src/pages/Pool/Overview/index.tsx index 08d01cc010..90fa740d60 100644 --- a/centrifuge-app/src/pages/Pool/Overview/index.tsx +++ b/centrifuge-app/src/pages/Pool/Overview/index.tsx @@ -1,4 +1,4 @@ -import { useWallet } from '@centrifuge/centrifuge-react' +import { Network, 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' @@ -63,13 +63,17 @@ 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', 84531] as Network[] return ( ) diff --git a/centrifuge-js/src/modules/rewards.ts b/centrifuge-js/src/modules/rewards.ts index 90e6278fb7..c1b68ca663 100644 --- a/centrifuge-js/src/modules/rewards.ts +++ b/centrifuge-js/src/modules/rewards.ts @@ -126,11 +126,14 @@ export function getRewardsModule(inst: Centrifuge) { } function getAccountStakes(args: [address: Account, poolId: string, trancheId: string]) { - const [address, poolId, trancheId] = args + const [addressEvm, poolId, trancheId] = args const { getPoolCurrency } = inst.pools - return inst.getApi().pipe( - switchMap((api) => api.query.liquidityRewardsBase.stakeAccount(address, { Tranche: [poolId, trancheId] })), + combineLatestWith(inst.getChainId()), + switchMap(([api, chainId]) => { + const address = inst.utils.evmToSubstrateAddress(addressEvm.toString(), chainId) + return api.query.liquidityRewardsBase.stakeAccount(address, { Tranche: [poolId, trancheId] }) + }), combineLatestWith(getPoolCurrency([poolId])), map(([data, currency]) => { const { stake, pendingStake, rewardTally, lastCurrencyMovement } = data.toPrimitive() as { diff --git a/centrifuge-react/src/components/WalletProvider/ConnectionGuard.tsx b/centrifuge-react/src/components/WalletProvider/ConnectionGuard.tsx index fb42993c62..e5d00f68f1 100644 --- a/centrifuge-react/src/components/WalletProvider/ConnectionGuard.tsx +++ b/centrifuge-react/src/components/WalletProvider/ConnectionGuard.tsx @@ -15,12 +15,11 @@ export function ConnectionGuard({ networks, children, body = 'Unsupported networ isEvmOnSubstrate, connectedType, connectedNetwork, - evm: { chains, selectedWallet }, + evm: { selectedWallet }, substrate: { evmChainId }, showWallets, connect, } = useWallet() - const getName = useGetNetworkName() if (!connectedNetwork) { @@ -65,7 +64,7 @@ export function ConnectionGuard({ networks, children, body = 'Unsupported networ {networks.map((network) => ( { state.close() switchNetwork(network) diff --git a/centrifuge-react/src/components/WalletProvider/WalletDialog.tsx b/centrifuge-react/src/components/WalletProvider/WalletDialog.tsx index f03a107e09..2a2a4282b1 100644 --- a/centrifuge-react/src/components/WalletProvider/WalletDialog.tsx +++ b/centrifuge-react/src/components/WalletProvider/WalletDialog.tsx @@ -29,7 +29,7 @@ import { useCentEvmChainId, useWallet, wallets } from './WalletProvider' type Props = { evmChains: EvmChains showAdvancedAccounts?: boolean - showAvalanche?: boolean + showBase?: boolean } const title = { @@ -38,15 +38,13 @@ const title = { accounts: 'Choose account', } -export function WalletDialog({ evmChains: allEvmChains, showAdvancedAccounts, showAvalanche }: Props) { - const evmChains = showAvalanche - ? allEvmChains - : Object.keys(allEvmChains) - .filter((chain) => !['43114', '43113'].includes(chain)) - .reduce((obj, key) => { - obj[key] = allEvmChains[key] - return obj - }, {}) +export function WalletDialog({ evmChains: allEvmChains, showAdvancedAccounts, showBase }: Props) { + const evmChains = Object.keys(allEvmChains) + .filter((chain) => (!showBase ? !['8453', '84531'].includes(chain) : true)) + .reduce((obj, key) => { + obj[key] = allEvmChains[key] + return obj + }, {}) as EvmChains const ctx = useWallet() const centEvmChainId = useCentEvmChainId() const { diff --git a/centrifuge-react/src/components/WalletProvider/WalletProvider.tsx b/centrifuge-react/src/components/WalletProvider/WalletProvider.tsx index 9fb0b8dccc..b6c8376f51 100644 --- a/centrifuge-react/src/components/WalletProvider/WalletProvider.tsx +++ b/centrifuge-react/src/components/WalletProvider/WalletProvider.tsx @@ -122,7 +122,7 @@ type WalletProviderProps = { walletConnectId?: string subscanUrl?: string showAdvancedAccounts?: boolean - showAvalanche?: boolean + showBase?: boolean } let cachedEvmConnectors: EvmConnectorMeta[] | undefined = undefined @@ -138,7 +138,7 @@ export function WalletProvider({ walletConnectId, subscanUrl, showAdvancedAccounts, - showAvalanche, + showBase, }: WalletProviderProps) { if (!evmChainsProp[1]?.urls[0]) throw new Error('Mainnet should be defined in EVM Chains') @@ -468,7 +468,7 @@ export function WalletProvider({ return ( {children} - + ) } diff --git a/centrifuge-react/src/components/WalletProvider/evm/chains.ts b/centrifuge-react/src/components/WalletProvider/evm/chains.ts index d02b5558c0..4904303903 100644 --- a/centrifuge-react/src/components/WalletProvider/evm/chains.ts +++ b/centrifuge-react/src/components/WalletProvider/evm/chains.ts @@ -12,7 +12,7 @@ type ExtendedChainInformation = BasicChainInformation & { } export type EvmChains = - | { [chainId in 1 | 5 | 43113 | 43114]?: BasicChainInformation } + | { [chainId in 1 | 5 | 8453 | 84531]?: BasicChainInformation } | { [chainId: number]: ExtendedChainInformation } export function getAddChainParameters(chains: EvmChains, chainId: number): AddEthereumChainParameter | number { @@ -53,15 +53,15 @@ const chainExtendedInfo = { nativeCurrency: { name: 'Görli Ether', symbol: 'görETH', decimals: 18 }, blockExplorerUrl: 'https://goerli.etherscan.io/', }, - 43113: { - name: 'Fuji', - nativeCurrency: { name: 'Avalanche', symbol: 'AVAX', decimals: 18 }, - blockExplorerUrl: 'https://testnet.snowtrace.io/', + 8453: { + name: 'Base', + nativeCurrency: { name: 'Base', symbol: 'bETH', decimals: 18 }, + blockExplorerUrl: 'https://basescan.org/', }, - 43114: { - name: 'Avalanche', - nativeCurrency: { name: 'Avalanche', symbol: 'AVAX', decimals: 18 }, - blockExplorerUrl: 'https://snowtrace.io/', + 84531: { + name: 'Base Goerli', + nativeCurrency: { name: 'Base Goerli', symbol: 'gbETH', decimals: 18 }, + blockExplorerUrl: 'https://goerli.basescan.org/', }, } diff --git a/onboarding-api/src/controllers/auth/authenticateWallet.ts b/onboarding-api/src/controllers/auth/authenticateWallet.ts index 48e4e9d9e3..c0acabe4fa 100644 --- a/onboarding-api/src/controllers/auth/authenticateWallet.ts +++ b/onboarding-api/src/controllers/auth/authenticateWallet.ts @@ -1,11 +1,9 @@ import { isAddress } from '@polkadot/util-crypto' import { Request, Response } from 'express' import * as jwt from 'jsonwebtoken' -import { SiweMessage } from 'siwe' import { InferType, object, string, StringSchema } from 'yup' import { SupportedNetworks } from '../../database' -import { HttpError, reportHttpError } from '../../utils/httpError' -import { getCentrifuge } from '../../utils/networks/centrifuge' +import { reportHttpError } from '../../utils/httpError' import { NetworkSwitch } from '../../utils/networks/networkSwitch' import { validateInput } from '../../utils/validateInput' @@ -32,6 +30,7 @@ const verifyWalletInput = object({ }), nonce: string().required(), network: string().oneOf(['evm', 'substrate', 'evmOnSubstrate']) as StringSchema, + chainId: string().required(), }) export const authenticateWalletController = async ( @@ -52,59 +51,3 @@ export const authenticateWalletController = async ( return res.status(error.code).send({ error: error.message, e }) } } - -const AUTHORIZED_ONBOARDING_PROXY_TYPES = ['Any', 'Invest', 'NonTransfer', 'NonProxy'] -export async function verifySubstrateWallet(req: Request, res: Response): Promise { - const { jw3t: token, nonce } = req.body - const { verified, payload } = await await getCentrifuge().auth.verify(token!) - - const onBehalfOf = payload?.on_behalf_of - const address = payload.address - - const cookieNonce = req.signedCookies[`onboarding-auth-${address.toLowerCase()}`] - if (!cookieNonce || cookieNonce !== nonce) { - throw new HttpError(400, 'Invalid nonce') - } - - res.clearCookie(`onboarding-auth-${address.toLowerCase()}`) - - if (verified && onBehalfOf) { - const isVerifiedProxy = await getCentrifuge().auth.verifyProxy( - address, - onBehalfOf, - AUTHORIZED_ONBOARDING_PROXY_TYPES - ) - if (isVerifiedProxy.verified) { - req.wallet.address = address - } else if (verified && !onBehalfOf) { - req.wallet.address = address - } else { - throw new Error() - } - } - return { - address, - network: payload.network || 'substrate', - } -} - -export async function verifyEthWallet(req: Request, res: Response): Promise { - const { message, signature, address, nonce, network } = req.body - - if (!isAddress(address)) { - throw new HttpError(400, 'Invalid address') - } - - const cookieNonce = req.signedCookies[`onboarding-auth-${address.toLowerCase()}`] - - if (!cookieNonce || cookieNonce !== nonce) { - throw new HttpError(400, 'Invalid nonce') - } - - const decodedMessage = await new SiweMessage(message).verify({ signature }) - res.clearCookie(`onboarding-auth-${address.toLowerCase()}`) - return { - address: decodedMessage.data.address, - network, - } -} diff --git a/onboarding-api/src/database/index.ts b/onboarding-api/src/database/index.ts index ba0d7d64f7..ddd201f758 100644 --- a/onboarding-api/src/database/index.ts +++ b/onboarding-api/src/database/index.ts @@ -34,6 +34,7 @@ export const transactionInfoSchema = object({ txHash: string().required(), blockNumber: string().required(), isEvmOnSubstrate: bool().optional(), + chainId: string().required(), }) export type TransactionInfo = InferType diff --git a/onboarding-api/src/middleware/verifyAuth.ts b/onboarding-api/src/middleware/verifyAuth.ts index 735c85a844..b7bbf78922 100644 --- a/onboarding-api/src/middleware/verifyAuth.ts +++ b/onboarding-api/src/middleware/verifyAuth.ts @@ -10,18 +10,18 @@ export const verifyAuth = async (req: Request, _res: Response, next: NextFunctio throw new Error('Unauthorized') } const token = authorization.split(' ')[1] - const { address, network, aud } = (await jwt.verify(token, process.env.JWT_SECRET)) as Request['wallet'] & + const { address, network, chainId, aud } = (await jwt.verify(token, process.env.JWT_SECRET)) as Request['wallet'] & jwt.JwtPayload if (!address || aud !== req.get('origin')) { throw new Error('Unauthorized') } if ( (network.includes('evm') && !isAddress(address)) || - (network === 'substrate' && !(await getValidSubstrateAddress({ address, network }))) + (network === 'substrate' && !(await getValidSubstrateAddress({ address, network, chainId }))) ) { throw new Error('Unauthorized') } - req.wallet = { address, network } + req.wallet = { address, network, chainId } next() } catch (e) { throw new Error('Unauthorized') diff --git a/onboarding-api/src/utils/annotateAgreementAndSignAsInvestor.ts b/onboarding-api/src/utils/annotateAgreementAndSignAsInvestor.ts index 6ab92f7793..4a407ce9ea 100644 --- a/onboarding-api/src/utils/annotateAgreementAndSignAsInvestor.ts +++ b/onboarding-api/src/utils/annotateAgreementAndSignAsInvestor.ts @@ -36,8 +36,9 @@ export const annotateAgreementAndSignAsInvestor = async ({ const unsignedAgreementUrl = metadata?.onboarding?.tranches?.[trancheId]?.agreement?.uri ? centrifuge.metadata.parseMetadataUrl(metadata?.onboarding?.tranches?.[trancheId]?.agreement?.uri) - : wallet.network === 'substrate' || wallet.network === 'evmOnSubstrate' - ? centrifuge.metadata.parseMetadataUrl(GENERIC_SUBSCRIPTION_AGREEMENT) + : !pool.id.startsWith('0x') + ? // TODO: remove generic and don't allow onboarding if agreement is not uploaded + centrifuge.metadata.parseMetadataUrl(GENERIC_SUBSCRIPTION_AGREEMENT) : null // tinlake pools that are closed for onboarding don't have agreements in their metadata @@ -113,7 +114,7 @@ Agreement hash: ${unsignedAgreementUrl}`, }) // all tinlake agreements require the executive summary to be appended - if (wallet.network === 'evm') { + if (pool.id.startsWith('0x')) { const execSummaryRes = await fetch(metadata.pool.links.executiveSummary.uri) const execSummary = Buffer.from(await execSummaryRes.arrayBuffer()) const execSummaryPdf = await PDFDocument.load(execSummary) diff --git a/onboarding-api/src/utils/networks/centrifuge.ts b/onboarding-api/src/utils/networks/centrifuge.ts index 4780def035..ee3074b362 100644 --- a/onboarding-api/src/utils/networks/centrifuge.ts +++ b/onboarding-api/src/utils/networks/centrifuge.ts @@ -1,7 +1,7 @@ import Centrifuge, { CurrencyBalance, evmToSubstrateAddress } from '@centrifuge/centrifuge-js' import { Keyring } from '@polkadot/keyring' import { cryptoWaitReady, encodeAddress } from '@polkadot/util-crypto' -import { Request } from 'express' +import { Request, Response } from 'express' import { combineLatest, combineLatestWith, firstValueFrom, lastValueFrom, switchMap, take, takeWhile } from 'rxjs' import { InferType } from 'yup' import { signAndSendDocumentsInput } from '../../controllers/emails/signAndSendDocuments' @@ -188,3 +188,39 @@ export const getValidSubstrateAddress = async (wallet: Request['wallet']) => { throw new HttpError(400, 'Invalid substrate address') } } + +const AUTHORIZED_ONBOARDING_PROXY_TYPES = ['Any', 'Invest', 'NonTransfer', 'NonProxy'] +export async function verifySubstrateWallet(req: Request, res: Response): Promise { + const { jw3t: token, nonce } = req.body + const { verified, payload } = await getCentrifuge().auth.verify(token!) + + const onBehalfOf = payload?.on_behalf_of + const address = payload.address + + const cookieNonce = req.signedCookies[`onboarding-auth-${address.toLowerCase()}`] + if (!cookieNonce || cookieNonce !== nonce) { + throw new HttpError(400, 'Invalid nonce') + } + + res.clearCookie(`onboarding-auth-${address.toLowerCase()}`) + + if (verified && onBehalfOf) { + const isVerifiedProxy = await getCentrifuge().auth.verifyProxy( + address, + onBehalfOf, + AUTHORIZED_ONBOARDING_PROXY_TYPES + ) + if (isVerifiedProxy.verified) { + req.wallet.address = address + } else if (verified && !onBehalfOf) { + req.wallet.address = address + } else { + throw new Error() + } + } + return { + address, + network: payload.network || 'substrate', + chainId: payload.chainId, + } +} diff --git a/onboarding-api/src/utils/networks/evm.ts b/onboarding-api/src/utils/networks/evm.ts new file mode 100644 index 0000000000..65e82c91fe --- /dev/null +++ b/onboarding-api/src/utils/networks/evm.ts @@ -0,0 +1,68 @@ +import { isAddress } from '@ethersproject/address' +import { Contract } from '@ethersproject/contracts' +import { InfuraProvider, JsonRpcProvider, Provider } from '@ethersproject/providers' +import { Request, Response } from 'express' +import { SiweMessage } from 'siwe' +import { InferType } from 'yup' +import { signAndSendDocumentsInput } from '../../controllers/emails/signAndSendDocuments' +import { HttpError } from '../httpError' +import RemarkerAbi from './abi/Remarker.abi.json' + +const getEvmProvider = (chainId: number | string, isEvmOnCentChain?: boolean): Provider => { + if (isEvmOnCentChain) { + return new InfuraProvider(chainId, process.env.INFURA_KEY) + } + switch (chainId.toString()) { + case '1': // eth mainnet + case '5': // goerli + return new InfuraProvider(chainId, process.env.INFURA_KEY) + case '8453': // base mainnet + return new JsonRpcProvider('https://mainnet.base.org') + case '84531': // base goerli + return new JsonRpcProvider('https://goerli.base.org') + default: + throw new HttpError(404, `Unsupported chainId ${chainId}`) + } +} + +export const validateEvmRemark = async ( + wallet: Request['wallet'], + transactionInfo: InferType['transactionInfo'], + expectedRemark: string +) => { + const provider = getEvmProvider(transactionInfo.chainId, transactionInfo?.isEvmOnSubstrate) + const remarkerAddress = '0x3E39db43035981c2C31F7Ffa4392f25231bE4477' + const contract = new Contract(remarkerAddress, RemarkerAbi).connect(provider) + const filteredEvents = await contract.queryFilter( + 'Remarked', + Number(transactionInfo.blockNumber), + Number(transactionInfo.blockNumber) + ) + + const [sender, actualRemark] = filteredEvents.flatMap((ev) => ev.args?.map((arg) => arg.toString())) + if (actualRemark !== expectedRemark || sender !== wallet.address) { + throw new HttpError(400, 'Invalid remark') + } +} + +export async function verifyEvmWallet(req: Request, res: Response): Promise { + const { message, signature, address, nonce, network, chainId } = req.body + + if (!isAddress(address)) { + throw new HttpError(400, 'Invalid address') + } + + const cookieNonce = req.signedCookies[`onboarding-auth-${address.toLowerCase()}`] + + if (!cookieNonce || cookieNonce !== nonce) { + throw new HttpError(400, 'Invalid nonce') + } + + const decodedMessage = await new SiweMessage(message).verify({ signature }) + res.clearCookie(`onboarding-auth-${address.toLowerCase()}`) + return { + address: decodedMessage.data.address, + network, + chainId, + } +} diff --git a/onboarding-api/src/utils/networks/networkSwitch.ts b/onboarding-api/src/utils/networks/networkSwitch.ts index 2065207398..73a9d14976 100644 --- a/onboarding-api/src/utils/networks/networkSwitch.ts +++ b/onboarding-api/src/utils/networks/networkSwitch.ts @@ -1,11 +1,16 @@ import { Request, Response } from 'express' import { InferType } from 'yup' -import { verifyEthWallet, verifySubstrateWallet } from '../../controllers/auth/authenticateWallet' import { signAndSendDocumentsInput } from '../../controllers/emails/signAndSendDocuments' import { SupportedNetworks } from '../../database' import { HttpError } from '../httpError' -import { addCentInvestorToMemberList, getCentPoolById, validateSubstrateRemark } from './centrifuge' -import { addTinlakeInvestorToMemberList, getTinlakePoolById, validateEvmRemark } from './tinlake' +import { + addCentInvestorToMemberList, + getCentPoolById, + validateSubstrateRemark, + verifySubstrateWallet, +} from './centrifuge' +import { validateEvmRemark, verifyEvmWallet } from './evm' +import { addTinlakeInvestorToMemberList, getTinlakePoolById } from './tinlake' export class NetworkSwitch { network: SupportedNetworks @@ -17,9 +22,9 @@ export class NetworkSwitch { if (this.network === 'substrate') { return verifySubstrateWallet(req, res) } else if (this.network === 'evm' || this.network === 'evmOnSubstrate') { - return verifyEthWallet(req, res) + return verifyEvmWallet(req, res) } - throw new Error('Unsupported network') + throw new HttpError(404, 'Unsupported network') } validateRemark = ( @@ -36,20 +41,16 @@ export class NetworkSwitch { } addInvestorToMemberList = async (wallet: Request['wallet'], poolId: string, trancheId: string) => { - if (this.network === 'evmOnSubstrate' || this.network === 'substrate') { - return addCentInvestorToMemberList(wallet, poolId, trancheId) - } else if (this.network === 'evm') { + if (this.network === 'evm' && poolId.startsWith('0x')) { return addTinlakeInvestorToMemberList(wallet, poolId, trancheId) } - throw new HttpError(404, 'Unsupported network') + return addCentInvestorToMemberList(wallet, poolId, trancheId) } getPoolById = async (poolId: string) => { - if (this.network === 'evmOnSubstrate' || this.network === 'substrate') { - return getCentPoolById(poolId) - } else if (this.network === 'evm') { + if (this.network === 'evm' && poolId.startsWith('0x')) { return getTinlakePoolById(poolId) } - throw new HttpError(404, 'Unsupported network') + return getCentPoolById(poolId) } } diff --git a/onboarding-api/src/utils/networks/tinlake.ts b/onboarding-api/src/utils/networks/tinlake.ts index 6f0c052492..dba446dc06 100644 --- a/onboarding-api/src/utils/networks/tinlake.ts +++ b/onboarding-api/src/utils/networks/tinlake.ts @@ -3,11 +3,8 @@ import { InfuraProvider } from '@ethersproject/providers' import { Wallet } from '@ethersproject/wallet' import { Request } from 'express' import { lastValueFrom } from 'rxjs' -import { InferType } from 'yup' -import { signAndSendDocumentsInput } from '../../controllers/emails/signAndSendDocuments' import { HttpError, reportHttpError } from '../httpError' import MemberListAdminAbi from './abi/MemberListAdmin.abi.json' -import RemarkerAbi from './abi/Remarker.abi.json' import { getCentrifuge } from './centrifuge' export interface LaunchingPool extends BasePool {} @@ -96,34 +93,32 @@ interface ActivePool extends BasePool { } } +function parsePoolsMetadata(poolsMetadata): { active: ActivePool[] } { + const launching = poolsMetadata.filter((p): p is LaunchingPool => !!p.metadata?.isLaunching) + const active = poolsMetadata.filter( + (p): p is ActivePool => !!('addresses' in p && p.addresses.ROOT_CONTRACT && !launching?.includes(p)) + ) + return { active } +} + const goerliConfig = { - remarkerAddress: '0x6E395641087a4938861d7ada05411e3146175F58', poolsHash: 'QmQe9NTiVJnVcb4srw6sBpHefhYieubR7v3J8ZriULQ8vB', // TODO: add registry to config and fetch poolHash memberListAddress: '0xaEcFA11fE9601c1B960661d7083A08A5df7c1947', } const mainnetConfig = { - remarkerAddress: '0x075f37451e7a4877f083aa070dd47a6969af2ced', poolsHash: 'QmNvauf8E6TkUiyF1ZgtYtntHz335tCswKp2uhBH1fiui1', // TODO: add registry to config and fetch poolHash memberListAddress: '0xB7e70B77f6386Ffa5F55DDCb53D87A0Fb5a2f53b', } -export const getEthConfig = () => ({ +export const getTinlakeConfig = () => ({ network: process.env.EVM_NETWORK, multicallContractAddress: '0x5ba1e12693dc8f9c48aad8770482f4739beed696', // Same for all networks signerPrivateKey: process.env.EVM_MEMBERLIST_ADMIN_PRIVATE_KEY, ...(process.env.EVM_NETWORK === 'goerli' ? goerliConfig : mainnetConfig), }) -function parsePoolsMetadata(poolsMetadata): { active: ActivePool[] } { - const launching = poolsMetadata.filter((p): p is LaunchingPool => !!p.metadata?.isLaunching) - const active = poolsMetadata.filter( - (p): p is ActivePool => !!('addresses' in p && p.addresses.ROOT_CONTRACT && !launching?.includes(p)) - ) - return { active } -} - export const getTinlakePoolById = async (poolId: string) => { - const uri = getEthConfig().poolsHash + const uri = getTinlakeConfig().poolsHash const data = (await lastValueFrom(getCentrifuge().metadata.getMetadata(uri))) as PoolMetadataDetails const pools = parsePoolsMetadata(Object.values(data)) const poolData = pools.active.find((p) => p.addresses.ROOT_CONTRACT === poolId) @@ -189,30 +184,11 @@ export const getTinlakePoolById = async (poolId: string) => { } } -export const validateEvmRemark = async ( - wallet: Request['wallet'], - transactionInfo: InferType['transactionInfo'], - expectedRemark: string -) => { - const provider = new InfuraProvider(process.env.EVM_NETWORK, process.env.INFURA_KEY) - const contract = new Contract(getEthConfig().remarkerAddress, RemarkerAbi).connect(provider) - const filteredEvents = await contract.queryFilter( - 'Remarked', - Number(transactionInfo.blockNumber), - Number(transactionInfo.blockNumber) - ) - - const [sender, actualRemark] = filteredEvents.flatMap((ev) => ev.args?.map((arg) => arg.toString())) - if (actualRemark !== expectedRemark || sender !== wallet.address) { - throw new HttpError(400, 'Invalid remark') - } -} - export const addTinlakeInvestorToMemberList = async (wallet: Request['wallet'], poolId: string, trancheId: string) => { try { const pool = await getTinlakePoolById(poolId) - const provider = new InfuraProvider(process.env.EVM_NETWORK, process.env.INFURA_KEY) - const ethConfig = getEthConfig() + const provider = new InfuraProvider(wallet.chainId, process.env.INFURA_KEY) + const ethConfig = getTinlakeConfig() const signer = new Wallet(ethConfig.signerPrivateKey).connect(provider) const memberAdminContract = new Contract(ethConfig.memberListAddress, MemberListAdminAbi, signer) const memberlistAddress = trancheId.endsWith('1') diff --git a/onboarding-api/src/utils/types.d.ts b/onboarding-api/src/utils/types.d.ts index d5b2dc891d..0545496549 100644 --- a/onboarding-api/src/utils/types.d.ts +++ b/onboarding-api/src/utils/types.d.ts @@ -16,6 +16,7 @@ declare global { wallet: { address: string network: SupportedNetworks + chainId: string } } } From 6fce1a568c2e9c5dec95ad7ab03f800778dc4b5e Mon Sep 17 00:00:00 2001 From: Sophia Date: Thu, 24 Aug 2023 09:52:38 -0400 Subject: [PATCH 002/202] OnboardingAPI: Implement faucet for remark signing evm on cent chain (#1547) * Implement remark faucet for evm on cent chain * Fix import * Clean up chain id types --- .../pages/Onboarding/queries/useSignRemark.ts | 29 +++++++++++++------ .../controllers/auth/authenticateWallet.ts | 4 +-- onboarding-api/src/database/index.ts | 4 +-- .../src/utils/networks/centrifuge.ts | 11 +++---- onboarding-api/src/utils/networks/evm.ts | 12 ++++---- onboarding-api/src/utils/types.d.ts | 2 +- 6 files changed, 37 insertions(+), 25 deletions(-) diff --git a/centrifuge-app/src/pages/Onboarding/queries/useSignRemark.ts b/centrifuge-app/src/pages/Onboarding/queries/useSignRemark.ts index 08f3f7b7d4..092d143cd4 100644 --- a/centrifuge-app/src/pages/Onboarding/queries/useSignRemark.ts +++ b/centrifuge-app/src/pages/Onboarding/queries/useSignRemark.ts @@ -7,6 +7,7 @@ import { useTransactions, useWallet, } from '@centrifuge/centrifuge-react' +import { useNativeBalance } from '@centrifuge/centrifuge-react/dist/components/WalletProvider/evm/utils' import { Contract } from '@ethersproject/contracts' import React, { useEffect } from 'react' import { UseMutateFunction } from 'react-query' @@ -39,10 +40,12 @@ export const useSignRemark = ( connectedType, isEvmOnSubstrate, substrate: { selectedAddress, selectedAccount }, + evm: { selectedAddress: evmSelectedAddress, chainId: evmChainId }, connectedNetwork, } = useWallet() const [expectedTxFee, setExpectedTxFee] = React.useState(Dec(0)) - const balances = useBalances(selectedAddress || '') + const balances = useBalances(selectedAddress || undefined) + const { data: evmBalance } = useNativeBalance() const { authToken } = useOnboardingAuth() const [account] = useSuitableAccounts({ actingAddress: [selectedAddress || ''] }) @@ -79,7 +82,7 @@ export const useSignRemark = ( const txIdSignRemark = Math.random().toString(36).substr(2) addOrUpdateTransaction({ id: txIdSignRemark, - title: `Get ${balances?.native.currency.symbol}`, + title: `Get ${balances?.native.currency.symbol || 'CFG'}`, status: 'pending', args: [], }) @@ -95,16 +98,16 @@ export const useSignRemark = ( if (response.status !== 201) { addOrUpdateTransaction({ id: txIdSignRemark, - title: `Get ${balances?.native.currency.symbol}`, + title: `Get ${balances?.native.currency.symbol || 'CFG'}`, status: 'failed', args: [], }) setIsSubstrateTxLoading(false) - throw new Error('Insufficient funds') + throw new Error('Unable to get balance for signing') } else { addOrUpdateTransaction({ id: txIdSignRemark, - title: `Get ${balances?.native.currency.symbol}`, + title: `Get ${balances?.native.currency.symbol || 'CFG'}`, status: 'succeeded', args: [], }) @@ -113,7 +116,10 @@ export const useSignRemark = ( const signSubstrateRemark = async (args: [message: string]) => { setIsSubstrateTxLoading(true) - if (!isEvmOnSubstrate && balances?.native.balance?.toDecimal().lt(expectedTxFee.mul(1.1))) { + if (balances?.native.balance?.toDecimal().lt(expectedTxFee.mul(1.1))) { + await getBalanceForSigning() + } + if (isEvmOnSubstrate && evmBalance?.toDecimal().lt(expectedTxFee.mul(1.1))) { await getBalanceForSigning() } substrateMutation.execute(args, { account }) @@ -121,15 +127,20 @@ export const useSignRemark = ( useEffect(() => { const executePaymentInfo = async () => { - if (selectedAccount && selectedAccount.signer) { - const api = await centrifuge.connect(selectedAccount.address, selectedAccount.signer as any) + if ((selectedAccount && selectedAccount.signer) || (isEvmOnSubstrate && evmSelectedAddress)) { + const address = + isEvmOnSubstrate && evmSelectedAddress + ? centrifuge.utils.evmToSubstrateAddress(evmSelectedAddress, evmChainId!) + : selectedAccount?.address + const signer = selectedAccount?.signer || (await evmProvider?.getSigner()) + const api = await centrifuge.connect(address!, signer as any) const paymentInfo = await lastValueFrom( api.remark.signRemark( [ `I hereby sign the subscription agreement of pool [POOL_ID] and tranche [TRANCHE_ID]: [IPFS_HASH_OF_TEMPLATE]`, ], { - paymentInfo: selectedAccount.address, + paymentInfo: address!, } ) ) diff --git a/onboarding-api/src/controllers/auth/authenticateWallet.ts b/onboarding-api/src/controllers/auth/authenticateWallet.ts index c0acabe4fa..34ec8c1169 100644 --- a/onboarding-api/src/controllers/auth/authenticateWallet.ts +++ b/onboarding-api/src/controllers/auth/authenticateWallet.ts @@ -1,7 +1,7 @@ import { isAddress } from '@polkadot/util-crypto' import { Request, Response } from 'express' import * as jwt from 'jsonwebtoken' -import { InferType, object, string, StringSchema } from 'yup' +import { InferType, number, object, string, StringSchema } from 'yup' import { SupportedNetworks } from '../../database' import { reportHttpError } from '../../utils/httpError' import { NetworkSwitch } from '../../utils/networks/networkSwitch' @@ -30,7 +30,7 @@ const verifyWalletInput = object({ }), nonce: string().required(), network: string().oneOf(['evm', 'substrate', 'evmOnSubstrate']) as StringSchema, - chainId: string().required(), + chainId: number().required(), }) export const authenticateWalletController = async ( diff --git a/onboarding-api/src/database/index.ts b/onboarding-api/src/database/index.ts index ddd201f758..adb63287f1 100644 --- a/onboarding-api/src/database/index.ts +++ b/onboarding-api/src/database/index.ts @@ -2,7 +2,7 @@ import { Firestore } from '@google-cloud/firestore' import { Storage } from '@google-cloud/storage' import * as dotenv from 'dotenv' import { Request } from 'express' -import { array, bool, date, InferType, lazy, mixed, object, string, StringSchema } from 'yup' +import { array, bool, date, InferType, lazy, mixed, number, object, string, StringSchema } from 'yup' import { HttpError } from '../utils/httpError' import { Subset } from '../utils/types' @@ -34,7 +34,7 @@ export const transactionInfoSchema = object({ txHash: string().required(), blockNumber: string().required(), isEvmOnSubstrate: bool().optional(), - chainId: string().required(), + chainId: number().required(), }) export type TransactionInfo = InferType diff --git a/onboarding-api/src/utils/networks/centrifuge.ts b/onboarding-api/src/utils/networks/centrifuge.ts index ee3074b362..354bcb46e1 100644 --- a/onboarding-api/src/utils/networks/centrifuge.ts +++ b/onboarding-api/src/utils/networks/centrifuge.ts @@ -131,12 +131,13 @@ export const validateSubstrateRemark = async ( } export const checkBalanceBeforeSigningRemark = async (wallet: Request['wallet']) => { + const address = await getValidSubstrateAddress(wallet) const signer = await getSigner() const $api = getCentrifuge().getApi() const $paymentInfo = $api - .pipe(switchMap((api) => api.tx.system.remarkWithEvent('Signing for pool').paymentInfo(wallet.address))) + .pipe(switchMap((api) => api.tx.system.remarkWithEvent('Signing for pool').paymentInfo(address))) .pipe(take(1)) - const $nativeBalance = $api.pipe(switchMap((api) => api.query.system.account(wallet.address))).pipe(take(1)) + const $nativeBalance = $api.pipe(switchMap((api) => api.query.system.account(address))).pipe(take(1)) const tx = await lastValueFrom( combineLatest([$api, $paymentInfo, $nativeBalance]).pipe( switchMap(([api, paymentInfo, nativeBalance]) => { @@ -151,7 +152,7 @@ export const checkBalanceBeforeSigningRemark = async (wallet: Request['wallet']) } // add 10% buffer to the transaction fee - const submittable = api.tx.tokens.transfer({ Id: wallet.address }, 'Native', txFee.add(txFee.muln(1.1))) + const submittable = api.tx.tokens.transfer({ Id: address }, 'Native', txFee.add(txFee.muln(1.1))) return submittable.signAndSend(signer) }), takeWhile(({ events, isFinalized }) => { @@ -159,10 +160,10 @@ export const checkBalanceBeforeSigningRemark = async (wallet: Request['wallet']) events.forEach(({ event }) => { const result = event.data[0]?.toHuman() if (event.method === 'ProxyExecuted' && result === 'Ok') { - console.log(`Executed proxy for transfer`, { walletAddress: wallet.address, result }) + console.log(`Executed proxy for transfer`, { walletAddress: address, result }) } if (event.method === 'ExtrinsicFailed') { - console.log(`Extrinsic failed`, { walletAddress: wallet.address, result }) + console.log(`Extrinsic failed`, { walletAddress: address, result }) throw new HttpError(400, 'Extrinsic failed') } }) diff --git a/onboarding-api/src/utils/networks/evm.ts b/onboarding-api/src/utils/networks/evm.ts index 65e82c91fe..7463fc7b60 100644 --- a/onboarding-api/src/utils/networks/evm.ts +++ b/onboarding-api/src/utils/networks/evm.ts @@ -8,17 +8,17 @@ import { signAndSendDocumentsInput } from '../../controllers/emails/signAndSendD import { HttpError } from '../httpError' import RemarkerAbi from './abi/Remarker.abi.json' -const getEvmProvider = (chainId: number | string, isEvmOnCentChain?: boolean): Provider => { +const getEvmProvider = (chainId: number, isEvmOnCentChain?: boolean): Provider => { if (isEvmOnCentChain) { return new InfuraProvider(chainId, process.env.INFURA_KEY) } - switch (chainId.toString()) { - case '1': // eth mainnet - case '5': // goerli + switch (chainId) { + case 1: // eth mainnet + case 5: // goerli return new InfuraProvider(chainId, process.env.INFURA_KEY) - case '8453': // base mainnet + case 8453: // base mainnet return new JsonRpcProvider('https://mainnet.base.org') - case '84531': // base goerli + case 84531: // base goerli return new JsonRpcProvider('https://goerli.base.org') default: throw new HttpError(404, `Unsupported chainId ${chainId}`) diff --git a/onboarding-api/src/utils/types.d.ts b/onboarding-api/src/utils/types.d.ts index 0545496549..6d6ad4b3e9 100644 --- a/onboarding-api/src/utils/types.d.ts +++ b/onboarding-api/src/utils/types.d.ts @@ -16,7 +16,7 @@ declare global { wallet: { address: string network: SupportedNetworks - chainId: string + chainId: number } } } From e5beac5a96e37fc649d00b3ac09f681e6199fddb Mon Sep 17 00:00:00 2001 From: Sophia Date: Thu, 24 Aug 2023 11:49:18 -0400 Subject: [PATCH 003/202] OnboardingAPI: updateMember for liquidity pools (#1549) * Add exrtinsic to update member in liquidity pools * Use hex address for extrinsics * Remove proxy --- .../src/utils/networks/centrifuge.ts | 27 ++++++++++++++----- 1 file changed, 21 insertions(+), 6 deletions(-) diff --git a/onboarding-api/src/utils/networks/centrifuge.ts b/onboarding-api/src/utils/networks/centrifuge.ts index 354bcb46e1..7c7d31ff49 100644 --- a/onboarding-api/src/utils/networks/centrifuge.ts +++ b/onboarding-api/src/utils/networks/centrifuge.ts @@ -61,6 +61,9 @@ export const addCentInvestorToMemberList = async (wallet: Request['wallet'], poo { Pool: poolId }, { PoolRole: { TrancheInvestor: [trancheId, OneHundredYearsFromNow] } } ) + const proxiedSubmittable = api.tx.proxy.proxy(pureProxyAddress, undefined, submittable) + const batchSubmittable = [proxiedSubmittable] + // give the investor PODReadAccess if they issuer enabled it if (!hasPodReadAccess && metadata?.onboarding?.podReadAccess) { const podSubmittable = api.tx.permissions.add( { PoolRole: 'InvestorAdmin' }, @@ -68,13 +71,22 @@ export const addCentInvestorToMemberList = async (wallet: Request['wallet'], poo { Pool: poolId }, { PoolRole: 'PODReadAccess' } ) - const proxiedSubmittable = api.tx.proxy.proxy(pureProxyAddress, undefined, submittable) const proxiedPodSubmittable = api.tx.proxy.proxy(pureProxyAddress, undefined, podSubmittable) - const batchSubmittable = api.tx.utility.batchAll([proxiedPodSubmittable, proxiedSubmittable]) - return batchSubmittable.signAndSend(signer) + batchSubmittable.push(proxiedPodSubmittable) } - const proxiedSubmittable = api.tx.proxy.proxy(pureProxyAddress, undefined, submittable) - return proxiedSubmittable.signAndSend(signer) + // add investor to liquidity pools if they are investing on any domain other than centrifuge + if (wallet.network === 'evm') { + const updateMemberSubmittable = api.tx.connectors.updateMember( + poolId, + trancheId, + { + EVM: [wallet.chainId, wallet.address], + }, + OneHundredYearsFromNow + ) + batchSubmittable.push(updateMemberSubmittable) + } + return api.tx.utility.batchAll(batchSubmittable).signAndSend(signer) }), combineLatestWith(api), takeWhile(([{ events, isFinalized }, api]) => { @@ -181,7 +193,10 @@ export const getValidSubstrateAddress = async (wallet: Request['wallet']) => { const centChainId = await cent.getChainId() if (wallet.network === 'evmOnSubstrate') { const chainId = await firstValueFrom(cent.getApi().pipe(switchMap((api) => api.query.evmChainId.chainId()))) - return encodeAddress(evmToSubstrateAddress(wallet.address, Number(chainId.toString())), centChainId) + return evmToSubstrateAddress(wallet.address, Number(chainId.toString())) + } + if (wallet.network === 'evm') { + return evmToSubstrateAddress(wallet.address, wallet.chainId) } const validAddress = encodeAddress(wallet.address, centChainId) return validAddress From eb46ec3994c1cb2ba635a46ff8f1c0f382b6894f Mon Sep 17 00:00:00 2001 From: Sophia Date: Fri, 25 Aug 2023 13:55:01 -0400 Subject: [PATCH 004/202] Update centrifuge app docs (#1551) --- .github/README.md | 20 ++++------------ centrifuge-app/README.md | 49 ++++++++++++++++++++++++++-------------- 2 files changed, 37 insertions(+), 32 deletions(-) diff --git a/.github/README.md b/.github/README.md index 48f154588c..eb00f95835 100644 --- a/.github/README.md +++ b/.github/README.md @@ -1,15 +1,4 @@ -# Monorepo for the Centrifuge applications. - -## Setup - -Make sure you have installed Yarn and NVM. - -1. Use Node v14.15.1: `nvm use` -2. Install dependencies: `yarn install` -3. Install `husky`: `yarn postinstall` -4. Add `.env` files with the right environment variables to each project. - -It's also recommended to run Prettier automatically in your editor, e.g. using [this VS Code plugin](https://marketplace.visualstudio.com/items?itemName=esbenp.prettier-vscode). +# Monorepo for the Centrifuge applications ## Preparing Envs (e.g when the dev chain data is reset) @@ -24,9 +13,10 @@ It's also recommended to run Prettier automatically in your editor, e.g. using [ Setup pure proxy to sign transactions (whitelisting & transfer tokens). -1. Run `/initProxies` to create the pure proxy, fund it, and give it sufficient permissions -2. Copy the resulting pure proxy address and add it to the env varibles: `MEMBERLIST_ADMIN_PURE_PROXY` (onboarding-api) and `REACT_APP_MEMBERLIST_ADMIN_PURE_PROXY` (centrifuge-app) -3. Enable onboarding for each new pool under /issuer//investors +1. Use sudo in polkadot UI to give Alice enough currency to distribute (tokens.setBalance()). For currencyId select ForeignAsset and submit the transacton once with ForeignAsset 1 and once with ForeignAsset 2 +2. Run `/initProxies` to create the pure proxy, fund it, and give it sufficient permissions +3. Copy the resulting pure proxy address and add it to the env varibles: `MEMBERLIST_ADMIN_PURE_PROXY` (onboarding-api) and `REACT_APP_MEMBERLIST_ADMIN_PURE_PROXY` (centrifuge-app) +4. Enable onboarding for each new pool under /issuer//investors ### Asset Originator POD Access diff --git a/centrifuge-app/README.md b/centrifuge-app/README.md index bfd4431dab..a69c22e00a 100644 --- a/centrifuge-app/README.md +++ b/centrifuge-app/README.md @@ -2,35 +2,50 @@ ## Data and UI Architecture -- `centrifuge-js`: fetch data from the chain or subquery. -- `fabric`: all design system elements (run storybook in fabric to see everything available). +UI -## Commands +- `centrifuge-js`: library to interact with the Centrifuge chain and subquery +- `fabric`: design system elements and components +- `centrifuge-react`: reusable React component and hooks (wallets, queries, transactions) -#### `yarn start` +Cloud functions -Running `yarn start` will start the following processes: -Start a development server that watches the different workspace modules and the react app (using Vite) +- `onboarding-api`: KYC/KYB and investor whitelisting +- `faucet-api`: dev chain faucet +- `pinning-api`: pin documents to Pinata (IPFS) -#### `yarn start:deps` -It will start a development mode on the dependencies (`fabric` & `centrifuge-js`), to allow HMR to work when making changes +Indexing -#### `yarn build` or `yarn build --mode $ENV` or `yarn build immutable` +- [pools-subql](https://github.com/centrifuge/pools-subql): subquery to index pools and assets -Build all dependencies, functions, and app with libraries. +## Development -## Other useful information +### Prerequisites + +- node v16 +- yarn -This app uses [`vite`](https://vitejs.dev/guide/) but serve, build and bundle. +### Setup -To reference env variables in code please use the vite standard `import.meta.env.ENV_VARIABLE`. +1. copy [.env.development](./.env-config/env.development) to `.env.development.local` +2. Install modules: + ```bash + $ yarn + ``` +3. Start the development server: + ```bash + $ yarn start + ``` +4. Open [http://localhost:3000](http://localhost:3000) in your browser + +## Other useful information -Check the Vite configuration file to find where we keep env file. Vite automatically grabs the right file when building with the `--mode` flag. [More info here](https://vitejs.dev/guide/env-and-mode.html) +This app uses [`vite`](https://vitejs.dev/guide/) to serve, build and bundle. -> in Netlify functions you still need to reference env variables with `process.env` +To reference env variables in code please use the viste standard `import.meta.env.ENV_VARIABLE`. ## Deployments -Up-to-date info in k-f's Knowledge Base: +Up-to-date info in k-f's Knowledge Base: -https://centrifuge.hackmd.io/MFsnRldyQSa4cadx11OtVg?view#Environments-amp-Deployments \ No newline at end of file +https://centrifuge.hackmd.io/MFsnRldyQSa4cadx11OtVg?view#Environments-amp-Deployments From 41088a19fca7acfe8a9fd9cb0cfb9d9d2f3b9c45 Mon Sep 17 00:00:00 2001 From: Hornebom Date: Tue, 29 Aug 2023 14:30:16 +0200 Subject: [PATCH 005/202] Pools Overview redesign (#1526) --- centrifuge-app/.env-config/.env.altair | 3 +- centrifuge-app/.env-config/.env.catalyst | 1 + centrifuge-app/.env-config/.env.demo | 1 + centrifuge-app/.env-config/.env.development | 1 + centrifuge-app/.env-config/.env.example | 1 + centrifuge-app/.env-config/.env.production | 1 + .../src/components/CardTotalValueLocked.tsx | 92 +++++++++ .../components/Charts/TotalValueLocked.tsx | 104 ++++++++++ centrifuge-app/src/components/GlobalStyle.tsx | 10 + .../src/components/LayoutBase/BaseSection.tsx | 15 ++ .../src/components/LayoutBase/config.ts | 9 + .../src/components/LayoutBase/index.tsx | 61 ++++++ .../src/components/LayoutBase/styles.tsx | 157 +++++++++++++++ .../LiquidityRewardsProvider.tsx | 2 +- .../src/components/LiquidityRewards/hooks.ts | 11 +- .../src/components/LogoLink-deprecated.tsx | 19 ++ centrifuge-app/src/components/LogoLink.tsx | 10 +- .../Menu-deprecated/GovernanceMenu.tsx | 138 +++++++++++++ .../components/Menu-deprecated/IssuerMenu.tsx | 69 +++++++ .../components/Menu-deprecated/PageLink.tsx | 26 +++ .../components/Menu-deprecated/PoolLink.tsx | 36 ++++ .../src/components/Menu-deprecated/Toggle.tsx | 11 ++ .../src/components/Menu-deprecated/index.tsx | 85 ++++++++ .../src/components/Menu-deprecated/styles.ts | 44 +++++ .../src/components/Menu/GovernanceMenu.tsx | 20 +- .../src/components/Menu/IssuerMenu.tsx | 14 +- centrifuge-app/src/components/Menu/index.tsx | 10 +- centrifuge-app/src/components/Menu/styles.ts | 4 +- centrifuge-app/src/components/MenuSwitch.tsx | 46 ++--- .../src/components/PageWithSideBar.tsx | 4 +- centrifuge-app/src/components/PoolCard.tsx | 128 ------------ .../src/components/PoolCard/PoolStatus.tsx | 14 ++ .../src/components/PoolCard/index.tsx | 95 +++++++++ .../src/components/PoolCard/styles.tsx | 36 ++++ .../src/components/PoolFilter/FilterMenu.tsx | 119 ++++++++++++ .../src/components/PoolFilter/SortButton.tsx | 108 +++++++++++ .../src/components/PoolFilter/config.ts | 30 +++ .../src/components/PoolFilter/index.tsx | 50 +++++ .../src/components/PoolFilter/styles.ts | 26 +++ .../src/components/PoolFilter/types.ts | 6 + .../src/components/PoolFilter/utils.ts | 54 ++++++ centrifuge-app/src/components/PoolList.tsx | 33 ++-- .../src/components/PoolsTokensShared.tsx | 43 +++++ .../src/components/PortfolioCta/Cubes.tsx | 70 +++++++ .../src/components/PortfolioCta/index.tsx | 106 ++++++++++ centrifuge-app/src/components/SideDrawer.tsx | 63 ++++++ centrifuge-app/src/components/Tooltips.tsx | 2 +- centrifuge-app/src/pages/Pools.tsx | 155 +++++++++------ centrifuge-app/src/pages/Tokens.tsx | 182 +++++++----------- centrifuge-app/src/utils/formatting.ts | 7 +- .../utils/tinlake/fetchFromTinlakeSubgraph.ts | 20 ++ .../utils/tinlake/getTinlakeSubgraphTVL.ts | 40 ++++ .../src/utils/tinlake/useTinlakePools.ts | 2 +- centrifuge-app/src/utils/useMetadata.ts | 2 +- centrifuge-app/src/utils/usePools.ts | 8 + centrifuge-js/src/modules/pools.ts | 81 +++++++- centrifuge-js/src/modules/rewards.ts | 22 ++- fabric/src/components/Checkbox/index.tsx | 76 +++++--- fabric/src/components/Popover/index.tsx | 6 +- .../src/components/SideNavigation/index.tsx | 45 +++++ fabric/src/components/StatusChip/index.tsx | 2 + fabric/src/icon-svg/icon-filter.svg | 5 + fabric/src/index.ts | 1 + fabric/src/theme/tokens/baseTheme.ts | 1 + fabric/src/theme/types.ts | 2 +- 65 files changed, 2226 insertions(+), 419 deletions(-) create mode 100644 centrifuge-app/src/components/CardTotalValueLocked.tsx create mode 100644 centrifuge-app/src/components/Charts/TotalValueLocked.tsx create mode 100644 centrifuge-app/src/components/LayoutBase/BaseSection.tsx create mode 100644 centrifuge-app/src/components/LayoutBase/config.ts create mode 100644 centrifuge-app/src/components/LayoutBase/index.tsx create mode 100644 centrifuge-app/src/components/LayoutBase/styles.tsx create mode 100644 centrifuge-app/src/components/LogoLink-deprecated.tsx create mode 100644 centrifuge-app/src/components/Menu-deprecated/GovernanceMenu.tsx create mode 100644 centrifuge-app/src/components/Menu-deprecated/IssuerMenu.tsx create mode 100644 centrifuge-app/src/components/Menu-deprecated/PageLink.tsx create mode 100644 centrifuge-app/src/components/Menu-deprecated/PoolLink.tsx create mode 100644 centrifuge-app/src/components/Menu-deprecated/Toggle.tsx create mode 100644 centrifuge-app/src/components/Menu-deprecated/index.tsx create mode 100644 centrifuge-app/src/components/Menu-deprecated/styles.ts delete mode 100644 centrifuge-app/src/components/PoolCard.tsx create mode 100644 centrifuge-app/src/components/PoolCard/PoolStatus.tsx create mode 100644 centrifuge-app/src/components/PoolCard/index.tsx create mode 100644 centrifuge-app/src/components/PoolCard/styles.tsx create mode 100644 centrifuge-app/src/components/PoolFilter/FilterMenu.tsx create mode 100644 centrifuge-app/src/components/PoolFilter/SortButton.tsx create mode 100644 centrifuge-app/src/components/PoolFilter/config.ts create mode 100644 centrifuge-app/src/components/PoolFilter/index.tsx create mode 100644 centrifuge-app/src/components/PoolFilter/styles.ts create mode 100644 centrifuge-app/src/components/PoolFilter/types.ts create mode 100644 centrifuge-app/src/components/PoolFilter/utils.ts create mode 100644 centrifuge-app/src/components/PoolsTokensShared.tsx create mode 100644 centrifuge-app/src/components/PortfolioCta/Cubes.tsx create mode 100644 centrifuge-app/src/components/PortfolioCta/index.tsx create mode 100644 centrifuge-app/src/components/SideDrawer.tsx create mode 100644 centrifuge-app/src/utils/tinlake/fetchFromTinlakeSubgraph.ts create mode 100644 centrifuge-app/src/utils/tinlake/getTinlakeSubgraphTVL.ts create mode 100644 fabric/src/components/SideNavigation/index.tsx create mode 100644 fabric/src/icon-svg/icon-filter.svg diff --git a/centrifuge-app/.env-config/.env.altair b/centrifuge-app/.env-config/.env.altair index a45415d108..af289426e9 100644 --- a/centrifuge-app/.env-config/.env.altair +++ b/centrifuge-app/.env-config/.env.altair @@ -15,5 +15,6 @@ REACT_APP_TINLAKE_NETWORK=goerli REACT_APP_INFURA_KEY=bf808e7d3d924fbeb74672d9341d0550 REACT_APP_WHITELISTED_ACCOUNTS= 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=kALJqPUHFzDR2VkoQYWefPQyzjGzKznNny2smXGQpSf3aMw19 +REACT_APP_WALLETCONNECT_ID=c32fa79350803519804a67fcab0b742a +REACT_APP_TINLAKE_SUBGRAPH_URL=https://graph.centrifuge.io/tinlake diff --git a/centrifuge-app/.env-config/.env.catalyst b/centrifuge-app/.env-config/.env.catalyst index 904670ff09..aa45a5272c 100644 --- a/centrifuge-app/.env-config/.env.catalyst +++ b/centrifuge-app/.env-config/.env.catalyst @@ -17,3 +17,4 @@ REACT_APP_WHITELISTED_ACCOUNTS= REACT_APP_REWARDS_TREE_URL=https://storage.googleapis.com/rad-rewards-trees-kovan-staging/latest.json REACT_APP_MEMBERLIST_ADMIN_PURE_PROXY=4bo2vNkwZtr2PuqppWwqya6dPC8MzxqZ4kgnAoTZyKo9Kxq8 REACT_APP_WALLETCONNECT_ID=c32fa79350803519804a67fcab0b742a +REACT_APP_TINLAKE_SUBGRAPH_URL=https://graph.centrifuge.io/tinlake diff --git a/centrifuge-app/.env-config/.env.demo b/centrifuge-app/.env-config/.env.demo index 2df090c581..db85519d73 100644 --- a/centrifuge-app/.env-config/.env.demo +++ b/centrifuge-app/.env-config/.env.demo @@ -17,3 +17,4 @@ REACT_APP_NETWORK=centrifuge REACT_APP_REWARDS_TREE_URL=https://storage.googleapis.com/rad-rewards-trees-kovan-staging/latest.json REACT_APP_MEMBERLIST_ADMIN_PURE_PROXY=kALwmJutBq95s41U9fWnoApCUgvPqPGTh1GSmFnQh5f9fWo93 REACT_APP_WALLETCONNECT_ID=c32fa79350803519804a67fcab0b742a +REACT_APP_TINLAKE_SUBGRAPH_URL=https://graph.centrifuge.io/tinlake diff --git a/centrifuge-app/.env-config/.env.development b/centrifuge-app/.env-config/.env.development index 83a78f8a75..a1a9a0e836 100644 --- a/centrifuge-app/.env-config/.env.development +++ b/centrifuge-app/.env-config/.env.development @@ -14,6 +14,7 @@ REACT_APP_SUBSCAN_URL= REACT_APP_TINLAKE_NETWORK=goerli REACT_APP_INFURA_KEY=bf808e7d3d924fbeb74672d9341d0550 REACT_APP_WHITELISTED_ACCOUNTS= +REACT_APP_TINLAKE_SUBGRAPH_URL=https://graph.centrifuge.io/tinlake 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=kAKfp33p1SHRq6d1BMtGndP7Cek6pH6oZKKUoA7wJXRUqf6FY diff --git a/centrifuge-app/.env-config/.env.example b/centrifuge-app/.env-config/.env.example index 2b26c1d0e5..a6caf14422 100644 --- a/centrifuge-app/.env-config/.env.example +++ b/centrifuge-app/.env-config/.env.example @@ -17,3 +17,4 @@ REACT_APP_WHITELISTED_ACCOUNTS='' REACT_APP_REWARDS_TREE_URL=https://storage.googleapis.com/rad-rewards-trees-kovan-staging/latest.json REACT_APP_MEMBERLIST_ADMIN_PURE_PROXY=kALJqPUHFzDR2VkoQYWefPQyzjGzKznNny2smXGQpSf3aMw19 REACT_APP_WALLETCONNECT_ID=c32fa79350803519804a67fcab0b742a +REACT_APP_TINLAKE_SUBGRAPH_URL=https://graph.centrifuge.io/tinlake diff --git a/centrifuge-app/.env-config/.env.production b/centrifuge-app/.env-config/.env.production index b127592e39..a9c9ccc116 100644 --- a/centrifuge-app/.env-config/.env.production +++ b/centrifuge-app/.env-config/.env.production @@ -17,3 +17,4 @@ REACT_APP_WHITELISTED_ACCOUNTS='' REACT_APP_REWARDS_TREE_URL=https://storage.googleapis.com/rad-rewards-trees-mainnet-production/latest.json REACT_APP_MEMBERLIST_ADMIN_PURE_PROXY=kALJqPUHFzDR2VkoQYWefPQyzjGzKznNny2smXGQpSf3aMw19 REACT_APP_WALLETCONNECT_ID=c32fa79350803519804a67fcab0b742a +REACT_APP_TINLAKE_SUBGRAPH_URL=https://graph.centrifuge.io/tinlake diff --git a/centrifuge-app/src/components/CardTotalValueLocked.tsx b/centrifuge-app/src/components/CardTotalValueLocked.tsx new file mode 100644 index 0000000000..c794c62fba --- /dev/null +++ b/centrifuge-app/src/components/CardTotalValueLocked.tsx @@ -0,0 +1,92 @@ +import { Box, Stack, Text, TextWithPlaceholder, Tooltip } from '@centrifuge/fabric' +import * as React from 'react' +import { useTheme } from 'styled-components' +import { config } from '../config' +import { formatDate } from '../utils/date' +import { Dec } from '../utils/Decimal' +import { formatBalance } from '../utils/formatting' +import { useListedPools } from '../utils/useListedPools' +import { DataPoint, TotalValueLocked } from './Charts/TotalValueLocked' +import { tooltipText } from './Tooltips' + +export function CardTotalValueLocked() { + const { colors } = useTheme() + const [hovered, setHovered] = React.useState(undefined) + const [, listedTokens] = useListedPools() + + const chartHeight = 100 + const balanceProps = { + as: 'strong', + fontSize: [28, 32], + } + const headingProps = { + as: 'h2', + variant: 'heading3', + } + + const totalValueLocked = React.useMemo(() => { + return ( + listedTokens + ?.map((tranche) => ({ + valueLocked: tranche.totalIssuance + .toDecimal() + .mul(tranche.tokenPrice?.toDecimal() ?? Dec(0)) + .toNumber(), + })) + .reduce((prev, curr) => prev.add(curr.valueLocked), Dec(0)) ?? Dec(0) + ) + }, [listedTokens]) + + return ( + + + {hovered ? ( + <> + + TVL on{' '} + + + {formatBalance(Dec(hovered?.tvl || 0), config.baseCurrency)} + + ) : ( + <> + + {tooltipText.tvl.label} + + + {formatBalance(Dec(totalValueLocked || 0), config.baseCurrency)} + + + )} + + + + + + + ) +} diff --git a/centrifuge-app/src/components/Charts/TotalValueLocked.tsx b/centrifuge-app/src/components/Charts/TotalValueLocked.tsx new file mode 100644 index 0000000000..dabe29c187 --- /dev/null +++ b/centrifuge-app/src/components/Charts/TotalValueLocked.tsx @@ -0,0 +1,104 @@ +import Decimal from 'decimal.js-light' +import * as React from 'react' +import { useQuery } from 'react-query' +import { Area, AreaChart, ResponsiveContainer, Tooltip } from 'recharts' +import { getTinlakeSubgraphTVL } from '../../utils/tinlake/getTinlakeSubgraphTVL' +import { useDailyTVL } from '../../utils/usePools' + +export type DataPoint = { + dateInMilliseconds: number + tvl: Decimal +} + +type TotalValueLockedProps = { + chainTVL: Decimal + setHovered: (entry: DataPoint | undefined) => void +} + +export function TotalValueLocked({ chainTVL, setHovered }: TotalValueLockedProps) { + const centrifugeTVL = useDailyTVL() + const tinlakeTVL = useDailyTinlakeTVL() + const chartColor = '#ff8c00' + + const chartData = React.useMemo(() => { + if (!tinlakeTVL || !centrifugeTVL) { + return [] + } + + const currentTVL = chainTVL + ? { + dateInMilliseconds: new Date().setHours(0, 0, 0, 0), + tvl: chainTVL, + } + : undefined + + return getMergedData([...tinlakeTVL, ...centrifugeTVL], currentTVL) + }, [tinlakeTVL, centrifugeTVL, chainTVL]) + + return ( + + { + if (val?.activePayload && val?.activePayload.length > 0) { + setHovered(val.activePayload[0].payload) + } + }} + onMouseLeave={() => { + setHovered(undefined) + }} + > + + + + + + + + } /> + + + ) +} + +function useDailyTinlakeTVL() { + const { data } = useQuery('use daily tinlake tvl', getTinlakeSubgraphTVL, { + staleTime: Infinity, + suspense: true, + }) + + return data +} + +function getMergedData(combined: DataPoint[], current?: DataPoint) { + const mergedMap = new Map() + + combined.forEach((entry) => { + const { dateInMilliseconds, tvl } = entry + + if (mergedMap.has(dateInMilliseconds)) { + mergedMap.set(dateInMilliseconds, mergedMap.get(dateInMilliseconds).add(tvl)) + } else { + mergedMap.set(dateInMilliseconds, tvl) + } + }) + + if (current) { + mergedMap.set(current.dateInMilliseconds, current.tvl) + } + + const merged = Array.from(mergedMap, ([dateInMilliseconds, tvl]) => ({ dateInMilliseconds, tvl })) + .sort((a, b) => a.dateInMilliseconds - b.dateInMilliseconds) + .map((entry) => ({ ...entry, tvl: entry.tvl.toNumber() })) + + return merged +} diff --git a/centrifuge-app/src/components/GlobalStyle.tsx b/centrifuge-app/src/components/GlobalStyle.tsx index ebf029e575..18f1aa1aa0 100644 --- a/centrifuge-app/src/components/GlobalStyle.tsx +++ b/centrifuge-app/src/components/GlobalStyle.tsx @@ -27,4 +27,14 @@ export const GlobalStyle = createGlobalStyle` ul { list-style: none; } + + .visually-hidden { + clip: rect(0 0 0 0); + clip-path: inset(50%); + height: 1px; + overflow: hidden; + position: absolute; + white-space: nowrap; + width: 1px; + } ` diff --git a/centrifuge-app/src/components/LayoutBase/BaseSection.tsx b/centrifuge-app/src/components/LayoutBase/BaseSection.tsx new file mode 100644 index 0000000000..ab523309f7 --- /dev/null +++ b/centrifuge-app/src/components/LayoutBase/BaseSection.tsx @@ -0,0 +1,15 @@ +import { Box, BoxProps } from '@centrifuge/fabric' +import * as React from 'react' +import { config } from './config' + +type BaseSectionProps = BoxProps & { + children: React.ReactNode +} + +export function BaseSection({ children, ...boxProps }: BaseSectionProps) { + return ( + + {children} + + ) +} diff --git a/centrifuge-app/src/components/LayoutBase/config.ts b/centrifuge-app/src/components/LayoutBase/config.ts new file mode 100644 index 0000000000..dcb172ee3c --- /dev/null +++ b/centrifuge-app/src/components/LayoutBase/config.ts @@ -0,0 +1,9 @@ +export const config = { + HEADER_HEIGHT: 60, + TOOLBAR_HEIGHT: 50, + LAYOUT_MAX_WIDTH: 1800, + SIDEBAR_WIDTH: 80, + SIDEBAR_WIDTH_EXTENDED: 220, + PADDING_MAIN: [2, 2, 3, 3, 5], + WALLET_WIDTH: [200, 264], +} diff --git a/centrifuge-app/src/components/LayoutBase/index.tsx b/centrifuge-app/src/components/LayoutBase/index.tsx new file mode 100644 index 0000000000..03ca4abf07 --- /dev/null +++ b/centrifuge-app/src/components/LayoutBase/index.tsx @@ -0,0 +1,61 @@ +import { WalletMenu } from '@centrifuge/centrifuge-react' +import * as React from 'react' +import { Footer } from '../Footer' +import { LoadBoundary } from '../LoadBoundary' +import { LogoLink } from '../LogoLink' +import { Menu } from '../Menu' +import { OnboardingStatus } from '../OnboardingStatus' +import { SideDrawerProps } from '../SideDrawer' +import { config } from './config' +import { + FooterContainer, + HeaderBackground, + Inner, + LogoContainer, + MainContainer, + Root, + ToolbarContainer, + WalletContainer, + WalletInner, + WalletPositioner, +} from './styles' + +type LayoutBaseProps = { + children?: React.ReactNode + sideDrawer?: React.ReactElement +} + +export function LayoutBase({ children, sideDrawer }: LayoutBaseProps) { + return ( + + + + + + + + + + + + ]} /> + + + + + + + + + + {children} + + + +