diff --git a/centrifuge-app/src/components/PoolOverview/TransactionHistory.tsx b/centrifuge-app/src/components/PoolOverview/TransactionHistory.tsx index f4a475a23..ff6af92a9 100644 --- a/centrifuge-app/src/components/PoolOverview/TransactionHistory.tsx +++ b/centrifuge-app/src/components/PoolOverview/TransactionHistory.tsx @@ -21,6 +21,7 @@ type Row = { toAssetName?: string amount: CurrencyBalance | undefined hash: string + netFlow?: 'positive' | 'negative' | 'neutral' } const getTransactionTypeStatus = (type: string): 'default' | 'info' | 'ok' | 'warning' | 'critical' => { diff --git a/centrifuge-app/src/components/Tooltips.tsx b/centrifuge-app/src/components/Tooltips.tsx index 2b51b6bf3..e881d0cf9 100644 --- a/centrifuge-app/src/components/Tooltips.tsx +++ b/centrifuge-app/src/components/Tooltips.tsx @@ -326,6 +326,10 @@ export const tooltipText = { label: 'Available balance', body: 'Unlimited because this is a virtual accounting process.', }, + linearAccrual: { + label: 'Linear accrual', + body: 'If enabled, the price of the asset is updated continuously based on linear accrual from the latest known market price to the value at maturity.', + }, } export type TooltipsProps = { diff --git a/centrifuge-app/src/config.ts b/centrifuge-app/src/config.ts index ae8b6bab2..f4f36dac6 100644 --- a/centrifuge-app/src/config.ts +++ b/centrifuge-app/src/config.ts @@ -6,7 +6,6 @@ import assetHubLogo from '@centrifuge/fabric/assets/logos/assethub.svg' import baseLogo from '@centrifuge/fabric/assets/logos/base.svg' import celoLogo from '@centrifuge/fabric/assets/logos/celo.svg' import ethereumLogo from '@centrifuge/fabric/assets/logos/ethereum.svg' -import goerliLogo from '@centrifuge/fabric/assets/logos/goerli.svg' import sepoliaLogo from '@centrifuge/fabric/assets/logos/sepolia.png' import * as React from 'react' import { DefaultTheme } from 'styled-components' @@ -175,18 +174,6 @@ export const evmChains: EvmChains = { iconUrl: ethereumLogo, isTestnet: false, }, - 5: { - name: 'Ethereum Goerli', - nativeCurrency: { - name: 'Görli Ether', - symbol: 'görETH', - decimals: 18, - }, - blockExplorerUrl: 'https://goerli.etherscan.io/', - urls: [`https://eth-sepolia.g.alchemy.com/v2/${alchemyKey}`], - iconUrl: goerliLogo, - isTestnet: true, - }, 11155111: { name: 'Ethereum Sepolia', nativeCurrency: { name: 'Sepolia Ether', symbol: 'sepETH', decimals: 18 }, @@ -203,11 +190,11 @@ export const evmChains: EvmChains = { iconUrl: baseLogo, isTestnet: false, }, - 84531: { - name: 'Base Goerli', - nativeCurrency: { name: 'Base Goerli Ether', symbol: 'gbETH', decimals: 18 }, - blockExplorerUrl: 'https://goerli.basescan.org/', - urls: [`https://goerli.base.org`], + 84532: { + name: 'Base Sepolia', + nativeCurrency: { name: 'Base Sepolia Ether', symbol: 'sbETH', decimals: 18 }, + blockExplorerUrl: 'https://sepolia.basescan.org/', + urls: [`https://sepolia.base.org`], iconUrl: baseLogo, isTestnet: true, }, @@ -223,18 +210,6 @@ export const evmChains: EvmChains = { iconUrl: arbitrumLogo, isTestnet: false, }, - 421613: { - name: 'Arbitrum Goerli', - nativeCurrency: { - name: 'Ether', - symbol: 'ETH', - decimals: 18, - }, - blockExplorerUrl: 'https://goerli.arbiscan.io/', - urls: [`https://arbitrum-goerli.infura.io/v3/${infuraKey}`], - iconUrl: arbitrumLogo, - isTestnet: true, - }, 42220: { name: 'Celo', nativeCurrency: { diff --git a/centrifuge-app/src/pages/IssuerPool/Configuration/index.tsx b/centrifuge-app/src/pages/IssuerPool/Configuration/index.tsx index fbd506cd9..69af40ae4 100644 --- a/centrifuge-app/src/pages/IssuerPool/Configuration/index.tsx +++ b/centrifuge-app/src/pages/IssuerPool/Configuration/index.tsx @@ -5,6 +5,7 @@ import { LayoutBase } from '../../../components/LayoutBase' import { LoadBoundary } from '../../../components/LoadBoundary' import { useCanBorrow, usePoolAdmin } from '../../../utils/usePermissions' import { IssuerPoolHeader } from '../Header' +import { OnboardingSettings } from '../Investors/OnboardingSettings' import { Details } from './Details' import { EpochAndTranches } from './EpochAndTranches' import { Issuer } from './Issuer' @@ -39,6 +40,7 @@ function IssuerPoolConfiguration() { + {isPoolAdmin && } {editPoolConfig && } )} diff --git a/centrifuge-app/src/pages/IssuerPool/Investors/LiquidityPools.tsx b/centrifuge-app/src/pages/IssuerPool/Investors/LiquidityPools.tsx index 8a08822cf..c3bef6656 100644 --- a/centrifuge-app/src/pages/IssuerPool/Investors/LiquidityPools.tsx +++ b/centrifuge-app/src/pages/IssuerPool/Investors/LiquidityPools.tsx @@ -4,15 +4,26 @@ import { useCentrifugeApi, useCentrifugeTransaction, useGetExplorerUrl, + useGetNetworkIcon, useGetNetworkName, useNetworkName, } from '@centrifuge/centrifuge-react' -import { Accordion, Button, IconExternalLink, Shelf, Stack, Text } from '@centrifuge/fabric' +import { + Accordion, + Box, + Button, + Grid, + IconExternalLink, + InlineFeedback, + Shelf, + Spinner, + Stack, + Text, +} from '@centrifuge/fabric' import React from 'react' import { useParams } from 'react-router' import { combineLatest, switchMap } from 'rxjs' 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' @@ -36,30 +47,45 @@ export function LiquidityPools() { const { data: domains, refetch } = useActiveDomains(poolId) const getName = useGetNetworkName() - - const titles = { - inactive: 'Not active', - deploying: 'Action needed', - deployed: 'Active', - } + const getIcon = useGetNetworkIcon() return ( - ({ - title: ( - <> - {getName(domain.chainId)} - {titles[getDomainStatus(domain)]} - - ), - body: , - })) ?? [] - } - /> + + {domains ? ( + domains.map((domain) => ( + + + + {getName(domain.chainId)}{' '} + {getDomainStatus(domain) === 'deploying' && - Action needed} + + + ), + body: , + }, + ]} + /> + )) + ) : ( + + )} + ) } @@ -67,7 +93,6 @@ export function LiquidityPools() { function PoolDomain({ poolId, domain, refetch }: { poolId: string; domain: Domain; refetch: () => void }) { const pool = usePool(poolId) const poolAdmin = usePoolAdmin(poolId) - const getName = useGetNetworkName() const explorer = useGetExplorerUrl(domain.chainId) const api = useCentrifugeApi() @@ -82,7 +107,17 @@ function PoolDomain({ poolId, domain, refetch }: { poolId: string; domain: Domai ) ).pipe( switchMap((txs) => { - return cent.wrapSignAndSend(api, txs.length > 1 ? api.tx.utility.batchAll(txs) : txs[0], options) + return cent.wrapSignAndSend( + api, + txs.length > 1 + ? api.tx.utility.batchAll([ + api.tx.liquidityPoolsGateway.startBatchMessage({ EVM: domain.chainId }), + ...txs, + api.tx.liquidityPoolsGateway.endBatchMessage({ EVM: domain.chainId }), + ]) + : txs[0], + options + ) }) ) } @@ -99,48 +134,57 @@ function PoolDomain({ poolId, domain, refetch }: { poolId: string; domain: Domai return ( - {status === 'inactive' ? ( - - ) : status === 'deploying' ? ( - - {pool.tranches.map((t) => ( - - {domain.canTrancheBeDeployed[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)} - - - - - )) - )} {domain.hasDeployedLp && ( - )} + + {domain.currencies.length === 0 && ( + + + There is no assets setup yet for this chain. + + + )} + {domain.currencies.length > 0 && + (status === 'inactive' ? ( + + ) : status === 'deploying' ? ( + + {pool.tranches.map((t) => ( + + {domain.canTrancheBeDeployed[t.id] && ( + + )} + {domain.currencies.map((currency, i) => ( + + {domain.trancheTokens[t.id] && !domain.liquidityPools[t.id][currency.address] && ( + + )} + + ))} + + ))} + + ) : ( + pool.tranches.map((tranche) => ( + + + + )) + ))} ) } @@ -157,14 +201,14 @@ function DeployTrancheButton({ onSuccess: () => void }) { const pool = usePool(poolId) - const { execute, isLoading } = useEvmTransaction(`Deploy tranche`, (cent) => cent.liquidityPools.deployTranche, { + const { execute, isLoading } = useEvmTransaction(`Deploy token`, (cent) => cent.liquidityPools.deployTranche, { onSuccess, }) const tranche = find(pool.tranches, (t) => t.id === trancheId)! return ( ) } @@ -184,11 +228,9 @@ function DeployLPButton({ }) { const pool = usePool(poolId) - const { execute, isLoading } = useEvmTransaction( - `Deploy liquidity pool`, - (cent) => cent.liquidityPools.deployLiquidityPool, - { onSuccess } - ) + const { execute, isLoading } = useEvmTransaction(`Deploy vault`, (cent) => cent.liquidityPools.deployLiquidityPool, { + onSuccess, + }) const tranche = find(pool.tranches, (t) => t.id === trancheId)! return ( @@ -197,7 +239,7 @@ function DeployLPButton({ onClick={() => execute([domain.poolManager, poolId, trancheId, domain.currencies[currencyIndex].address])} small > - Deploy tranche/currency liquidity pool: {tranche.currency.name} / {domain.currencies[currencyIndex].name} + Deploy {tranche.currency.symbol} / {domain.currencies[currencyIndex].symbol} vault ) } diff --git a/centrifuge-app/src/pages/IssuerPool/Investors/index.tsx b/centrifuge-app/src/pages/IssuerPool/Investors/index.tsx index 71938da4d..9f1f540b4 100644 --- a/centrifuge-app/src/pages/IssuerPool/Investors/index.tsx +++ b/centrifuge-app/src/pages/IssuerPool/Investors/index.tsx @@ -5,7 +5,6 @@ import { useSuitableAccounts } from '../../../utils/usePermissions' import { IssuerPoolHeader } from '../Header' import { InvestorStatus } from './InvestorStatus' import { LiquidityPools } from './LiquidityPools' -import { OnboardingSettings } from './OnboardingSettings' export function IssuerPoolInvestorsPage() { return ( @@ -30,7 +29,6 @@ function IssuerPoolInvestors() { <> {canEditInvestors && } {isPoolAdmin && } - {isPoolAdmin && } ) } diff --git a/centrifuge-app/src/pages/Loan/PricingValues.tsx b/centrifuge-app/src/pages/Loan/PricingValues.tsx index f366dfada..7ae30fa8b 100644 --- a/centrifuge-app/src/pages/Loan/PricingValues.tsx +++ b/centrifuge-app/src/pages/Loan/PricingValues.tsx @@ -1,5 +1,6 @@ import { Loan, Pool, TinlakeLoan } from '@centrifuge/centrifuge-js' import { Card, Stack, Text } from '@centrifuge/fabric' +import { Tooltips } from '../../components/Tooltips' import { formatDate, getAge } from '../../utils/date' import { formatBalance, formatPercentage } from '../../utils/formatting' import { getLatestPrice } from '../../utils/getLatestPrice' @@ -33,12 +34,14 @@ export function PricingValues({ loan, pool }: Props) { } }) - const days = getAge(new Date(latestOraclePrice.timestamp).toISOString()) - const borrowerAssetTransactions = assetTransactions?.filter( (assetTransaction) => assetTransaction.asset.id === `${loan.poolId}-${loan.id}` ) - const latestPrice = getLatestPrice(latestOraclePrice.value, borrowerAssetTransactions, pool.currency.decimals) + const latestPrice = getLatestPrice(latestOraclePrice, borrowerAssetTransactions, pool.currency.decimals) + + const days = latestPrice.timestamp > 0 ? getAge(new Date(latestPrice.timestamp).toISOString()) : undefined + + const accruedPrice = 'currentPrice' in loan && loan.currentPrice return ( @@ -50,11 +53,21 @@ export function PricingValues({ loan, pool }: Props) { metrics={[ ...('isin' in pricing.priceId ? [{ label: 'ISIN', value: pricing.priceId.isin }] : []), { - label: `Latest price${latestOraclePrice.value.isZero() && latestPrice ? ' (settlement)' : ''}`, - value: latestPrice ? `${formatBalance(latestPrice, pool.currency.symbol, 6, 2)}` : '-', + label: `Current price${latestOraclePrice.value.isZero() && latestPrice ? ' (settlement)' : ''}`, + value: accruedPrice + ? `${formatBalance(accruedPrice || latestPrice, pool.currency.symbol, 6, 2)}` + : latestPrice + ? `${formatBalance(latestPrice.value, pool.currency.symbol, 6, 2)}` + : '-', + }, + { + label: , + value: pricing.withLinearPricing ? 'Enabled' : 'Disabled', }, - { label: 'Price last updated', value: days === '0' ? `${days} ago` : `Today` }, - ...(pricing.interestRate + ...(!pricing.withLinearPricing + ? [{ label: 'Price last updated', value: days ? `${days} ago` : `Today` }] + : [{ label: 'Last manual price update', value: days ? `${days} ago` : `Today` }]), + ...(pricing.interestRate.gtn(0) ? [ { label: 'Interest rate', diff --git a/centrifuge-app/src/utils/getLatestPrice.ts b/centrifuge-app/src/utils/getLatestPrice.ts index dbb102c8f..1fd40b230 100644 --- a/centrifuge-app/src/utils/getLatestPrice.ts +++ b/centrifuge-app/src/utils/getLatestPrice.ts @@ -1,21 +1,21 @@ import { AssetTransaction, CurrencyBalance } from '@centrifuge/centrifuge-js' export const getLatestPrice = ( - oracleValue: CurrencyBalance, + oracleValue: { value: CurrencyBalance; timestamp: number }, borrowerAssetTransactions: AssetTransaction[] | undefined, decimals: number -) => { - if (!borrowerAssetTransactions) return null +): { value: CurrencyBalance; timestamp: number } => { + if (!borrowerAssetTransactions) return { value: new CurrencyBalance(0, decimals), timestamp: 0 } - const latestSettlementPrice = borrowerAssetTransactions[borrowerAssetTransactions.length - 1]?.settlementPrice + const latestTx = borrowerAssetTransactions[borrowerAssetTransactions.length - 1] - if (latestSettlementPrice && oracleValue.isZero()) { - return new CurrencyBalance(latestSettlementPrice, decimals) + if (latestTx.settlementPrice && oracleValue.value.isZero()) { + return { value: new CurrencyBalance(latestTx.settlementPrice, decimals), timestamp: latestTx.timestamp.getTime() } } - if (oracleValue.isZero()) { - return null + if (oracleValue.value.isZero()) { + return { value: new CurrencyBalance(0, decimals), timestamp: 0 } } - return new CurrencyBalance(oracleValue, 18) + return oracleValue } diff --git a/centrifuge-js/src/modules/liquidityPools.ts b/centrifuge-js/src/modules/liquidityPools.ts index 4cb4bbd0a..c5d0b34b0 100644 --- a/centrifuge-js/src/modules/liquidityPools.ts +++ b/centrifuge-js/src/modules/liquidityPools.ts @@ -87,12 +87,14 @@ export function getLiquidityPoolsModule(inst: Centrifuge) { switchMap((rawPool) => { const pool = rawPool.toPrimitive() as any const tx = api.tx.utility.batchAll([ + api.tx.liquidityPoolsGateway.startBatchMessage({ EVM: chainId }), ...(currencyKeysToAdd?.map((key) => api.tx.liquidityPools.addCurrency(key)) ?? []), api.tx.liquidityPools.addPool(poolId, { EVM: chainId }), ...pool.tranches.ids.flatMap((trancheId: string) => [ api.tx.liquidityPools.addTranche(poolId, trancheId, { EVM: chainId }), ]), ...currencies.map((cur) => api.tx.liquidityPools.allowInvestmentCurrency(poolId, cur.key)), + api.tx.liquidityPoolsGateway.endBatchMessage({ EVM: chainId }), ]) return inst.wrapSignAndSend(api, tx, options) })