From fd321f5b2c65a5b45d48bc96fcf063f4323a61a1 Mon Sep 17 00:00:00 2001 From: Sophia Date: Thu, 9 May 2024 15:38:53 +0100 Subject: [PATCH 01/10] Migrate e2e to demo (#2119) * Add new demo pool config * Update action and run after successful demo deployments * Update sample env file * Fix symbol --- .github/workflows/e2e.yml | 13 +++++++------ centrifuge-app/env.sample | 13 +++++++------ centrifuge-app/tests/e2e/data/pool.json | 8 ++++---- centrifuge-app/tests/e2e/specs/invest.cy.ts | 2 +- centrifuge-app/tests/e2e/support/e2e.ts | 2 +- 5 files changed, 20 insertions(+), 18 deletions(-) diff --git a/.github/workflows/e2e.yml b/.github/workflows/e2e.yml index f09073262e..edec2fe416 100644 --- a/.github/workflows/e2e.yml +++ b/.github/workflows/e2e.yml @@ -1,9 +1,10 @@ name: Synpress tests on: - pull_request: - types: [closed] - branches: [main] + workflow_run: + workflows: ['Demo deployments (manual)'] + types: + - completed jobs: cypress-run: @@ -35,8 +36,8 @@ jobs: CYPRESS_PRIVATE_KEY: ${{secrets.CYPRESS_PRIVATE_KEY}} PRIVATE_KEY: ${{secrets.CYPRESS_PRIVATE_KEY}} NETWORK_NAME: centrifuge - RPC_URL: https://fullnode.development.cntrfg.com - CHAIN_ID: 2000 + RPC_URL: https://fullnode-apps.demo.k-f.dev + CHAIN_ID: 2090 SYMBOL: DEVEL IS_TESTNET: true DEBUG: true @@ -54,5 +55,5 @@ jobs: CYPRESS_RECORD_KEY: ${{ secrets.CYPRESS_RECORD_KEY }} CYPRESS_PRIVATE_KEY: ${{ secrets.CYPRESS_PRIVATE_KEY }} PRIVATE_KEY: ${{ secrets.CYPRESS_PRIVATE_KEY }} - CYPRESS_BASE_URL: https://app-dev.k-f.dev + CYPRESS_BASE_URL: https://app-demo.k-f.dev DISPLAY: :0.0 diff --git a/centrifuge-app/env.sample b/centrifuge-app/env.sample index 0f3d3fb466..0e89c2cea7 100644 --- a/centrifuge-app/env.sample +++ b/centrifuge-app/env.sample @@ -1,7 +1,8 @@ # Copy to .env (for synpress/e2e testing config ONLY) -PRIVATE_KEY= -NETWORK_NAME=centrifuge -RPC_URL=https://fullnode.development.cntrfg.com -CHAIN_ID=2000 -SYMBOL=DEVEL -IS_TESTNET=true \ No newline at end of file +NETWORK_NAME=centrifuge +RPC_URL=https://fullnode-apps.demo.k-f.dev +CHAIN_ID=2090 +SYMBOL=DEVEL +IS_TESTNET=true +DEBUG=true +PRIVATE_KEY= \ No newline at end of file diff --git a/centrifuge-app/tests/e2e/data/pool.json b/centrifuge-app/tests/e2e/data/pool.json index eee3cdb140..22f50186d4 100644 --- a/centrifuge-app/tests/e2e/data/pool.json +++ b/centrifuge-app/tests/e2e/data/pool.json @@ -1,19 +1,19 @@ { "name": "E2E Pool", - "poolId": "3515519799", - "poolCurrency": "USDT", + "poolId": "1101557597", + "poolCurrency": "USDC", "investAmount": "2", "poolAdmin": "0x70fC4d9C87E9e9B0751A5680b1Dd654517f02309", "tranches": [ { "name": "Junior", "symbol": "E2EJUN", - "id": "0xec6528e5b1a899fed47777b19963b1a6" + "id": "0xbf444862859556b448b9242c9cfee126" }, { "name": "Senior", "symbol": "E2ESEN", - "id": "0xc18e73516eda0e038c50f6ab075b74ec" + "id": "0x7ccd846d1116ce4a751edf9d01e484b7" } ] } diff --git a/centrifuge-app/tests/e2e/specs/invest.cy.ts b/centrifuge-app/tests/e2e/specs/invest.cy.ts index c9df5cbc03..1eb179129e 100644 --- a/centrifuge-app/tests/e2e/specs/invest.cy.ts +++ b/centrifuge-app/tests/e2e/specs/invest.cy.ts @@ -20,7 +20,7 @@ describe('Invest flows', () => { cy.get('button[type="submit"]').click() cy.confirmTransaction() }) - it('Pool Admin: Transfer USDT (fund investor)', () => { + it(`Pool Admin: Transfer ${pool.poolCurrency} (fund investor)`, () => { cy.visit('/portfolio', { failOnStatusCode: false }) cy.connectWallet() cy.switchMetamaskAccount('Investor') // switch to investor account to grab address diff --git a/centrifuge-app/tests/e2e/support/e2e.ts b/centrifuge-app/tests/e2e/support/e2e.ts index 26dc265194..a1346cbe2d 100644 --- a/centrifuge-app/tests/e2e/support/e2e.ts +++ b/centrifuge-app/tests/e2e/support/e2e.ts @@ -38,7 +38,7 @@ Cypress.Commands.add('connectWallet', (config) => { }) Cypress.Commands.add('confirmTransaction', () => { - cy.confirmMetamaskTransaction() + cy.confirmMetamaskTransaction({ gasConfig: 'market' }) cy.contains('Transaction pending').should('exist') cy.contains('Transaction pending', { timeout: 100000 }).should('not.exist') cy.contains('Transaction failed').should('not.exist') From 954ff6eb5a7c28dfb5d018172a180b4f9117c910 Mon Sep 17 00:00:00 2001 From: Onno Visser Date: Mon, 13 May 2024 19:08:17 +0200 Subject: [PATCH 02/10] Fix EVM balance (#2124) --- centrifuge-react/src/components/WalletProvider/evm/utils.ts | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/centrifuge-react/src/components/WalletProvider/evm/utils.ts b/centrifuge-react/src/components/WalletProvider/evm/utils.ts index e631fe912f..4d1ae68b16 100644 --- a/centrifuge-react/src/components/WalletProvider/evm/utils.ts +++ b/centrifuge-react/src/components/WalletProvider/evm/utils.ts @@ -79,7 +79,6 @@ export function useEvmProvider() { } export function useNativeBalance(address?: string) { - const provider = useEvmProvider() const { evm } = useWallet() const addr = address || evm.selectedAddress @@ -87,10 +86,10 @@ export function useNativeBalance(address?: string) { const query = useQuery( ['evmNativeBalance', addr, evm.chainId], async () => { - const balance = await provider!.getBalance(addr!) + const balance = await evm.getProvider(evm.chainId!).getBalance(addr!) return new CurrencyBalance(balance.toString(), 18) }, - { enabled: !!provider && !!addr } + { enabled: !!evm.chainId && !!addr } ) return query } From cb9c08e52520fe3440a9c1bb13a4f1d0b6d0f04f Mon Sep 17 00:00:00 2001 From: Onno Visser Date: Tue, 14 May 2024 09:40:52 +0200 Subject: [PATCH 03/10] Prime page fixes (#2117) --- .../src/components/Charts/CashflowsChart.tsx | 2 +- .../Charts/PoolPerformanceChart.tsx | 2 +- centrifuge-app/src/components/Charts/utils.ts | 2 +- .../Portfolio/CardPortfolioValue.tsx | 7 +- .../src/components/Portfolio/Holdings.tsx | 59 ++--- .../components/Portfolio/PortfolioValue.tsx | 1 + .../src/components/Portfolio/Transactions.tsx | 11 +- .../src/components/Portfolio/usePortfolio.ts | 211 +++++++++++++----- centrifuge-app/src/components/Resolutions.tsx | 10 +- centrifuge-app/src/pages/Prime/Detail.tsx | 2 +- centrifuge-app/src/pages/Prime/index.tsx | 108 +++++---- centrifuge-app/src/utils/usePools.ts | 2 +- centrifuge-app/src/utils/useSubquery.ts | 10 +- centrifuge-js/src/modules/pools.ts | 138 +++++++----- 14 files changed, 356 insertions(+), 209 deletions(-) diff --git a/centrifuge-app/src/components/Charts/CashflowsChart.tsx b/centrifuge-app/src/components/Charts/CashflowsChart.tsx index bb1b83b4f9..bdf281cb55 100644 --- a/centrifuge-app/src/components/Charts/CashflowsChart.tsx +++ b/centrifuge-app/src/components/Charts/CashflowsChart.tsx @@ -34,7 +34,7 @@ export const CashflowsChart = ({ poolStates, pool }: Props) => { const [range, setRange] = React.useState<(typeof rangeFilters)[number]>({ value: 'ytd', label: 'Year to date' }) const poolAge = pool.createdAt ? daysBetween(pool.createdAt, new Date()) : 0 - const rangeNumber = getRangeNumber(range.value, poolAge) + const rangeNumber = getRangeNumber(range.value, poolAge) ?? 100 const data = React.useMemo( () => diff --git a/centrifuge-app/src/components/Charts/PoolPerformanceChart.tsx b/centrifuge-app/src/components/Charts/PoolPerformanceChart.tsx index 0d3090c9b4..f8c7abf124 100644 --- a/centrifuge-app/src/components/Charts/PoolPerformanceChart.tsx +++ b/centrifuge-app/src/components/Charts/PoolPerformanceChart.tsx @@ -53,7 +53,7 @@ function PoolPerformanceChart() { }) const [range, setRange] = React.useState<(typeof rangeFilters)[number]>({ value: 'ytd', label: 'Year to date' }) - const rangeNumber = getRangeNumber(range.value, poolAge) + const rangeNumber = getRangeNumber(range.value, poolAge) ?? 100 const data: ChartData[] = React.useMemo( () => diff --git a/centrifuge-app/src/components/Charts/utils.ts b/centrifuge-app/src/components/Charts/utils.ts index b279bcc8c3..f69ef6b21d 100644 --- a/centrifuge-app/src/components/Charts/utils.ts +++ b/centrifuge-app/src/components/Charts/utils.ts @@ -15,7 +15,7 @@ export const getRangeNumber = (rangeValue: string, poolAge?: number) => { return daysSinceJanuary1 + 1 } - if (rangeValue === 'all' && poolAge) { + if (rangeValue === 'all') { return poolAge } diff --git a/centrifuge-app/src/components/Portfolio/CardPortfolioValue.tsx b/centrifuge-app/src/components/Portfolio/CardPortfolioValue.tsx index f7f47dd900..d8468b2b25 100644 --- a/centrifuge-app/src/components/Portfolio/CardPortfolioValue.tsx +++ b/centrifuge-app/src/components/Portfolio/CardPortfolioValue.tsx @@ -5,6 +5,7 @@ import { config } from '../../config' import { Dec } from '../../utils/Decimal' import { formatBalance } from '../../utils/formatting' import { useTransactionsByAddress } from '../../utils/usePools' +import { LoadBoundary } from '../LoadBoundary' import { useHoldings } from './Holdings' import { PortfolioValue } from './PortfolioValue' @@ -18,7 +19,7 @@ const rangeFilters = [ { value: '30d', label: '30 days' }, { value: '90d', label: '90 days' }, { value: 'ytd', label: 'Year to date' }, - // { value: 'all', label: 'All' }, + { value: 'all', label: 'All' }, ] as const export function CardPortfolioValue({ address }: { address?: string }) { @@ -100,7 +101,9 @@ export function CardPortfolioValue({ address }: { address?: string }) { - + + + ) : null} diff --git a/centrifuge-app/src/components/Portfolio/Holdings.tsx b/centrifuge-app/src/components/Portfolio/Holdings.tsx index daf392bafc..e145aecd0e 100644 --- a/centrifuge-app/src/components/Portfolio/Holdings.tsx +++ b/centrifuge-app/src/components/Portfolio/Holdings.tsx @@ -44,7 +44,7 @@ type Row = { marketValue: Decimal position: Decimal tokenPrice: Decimal - canInvestRedeem: boolean + showActions: boolean address?: string connectedNetwork?: string } @@ -95,7 +95,7 @@ const columns: Column[] = [ { align: 'left', header: '', // invest redeem buttons - cell: ({ canInvestRedeem, poolId, trancheId, currency, connectedNetwork }: Row) => { + cell: ({ showActions, poolId, trancheId, currency, connectedNetwork }: Row) => { const isTinlakePool = poolId.startsWith('0x') return ( @@ -109,24 +109,26 @@ const columns: Column[] = [ > View on Tinlake - ) : canInvestRedeem ? ( - - - Redeem - - - Invest - - - ) : connectedNetwork === 'Centrifuge' ? ( - - - Receive - - - Send - - + ) : showActions ? ( + trancheId ? ( + + + Redeem + + + Invest + + + ) : connectedNetwork === 'Centrifuge' ? ( + + + Receive + + + Send + + + ) : null ) : null} ) @@ -134,9 +136,10 @@ const columns: Column[] = [ }, ] -export function useHoldings(address?: string, canInvestRedeem = true) { +export function useHoldings(address?: string, showActions = true) { const { data: tinlakeBalances } = useTinlakeBalances(address && isEvmAddress(address) ? address : undefined) const centBalances = useBalances(address && isSubstrateAddress(address) ? address : undefined) + const wallet = useWallet() const tinlakePools = useTinlakePools() const portfolioTokens = usePortfolioTokens(address) @@ -147,7 +150,7 @@ export function useHoldings(address?: string, canInvestRedeem = true) { ...portfolioTokens.map((token) => ({ ...token, tokenPrice: token.tokenPrice.toDecimal() || Dec(0), - canInvestRedeem, + showActions, })), ...(tinlakeBalances?.tranches.filter((tranche) => !tranche.balance.isZero()) || []).map((balance) => { const pool = tinlakePools.data?.pools?.find((pool) => pool.id === balance.poolId) @@ -160,7 +163,7 @@ export function useHoldings(address?: string, canInvestRedeem = true) { trancheId: balance.trancheId, poolId: balance.poolId, currency: tranche.currency, - canInvestRedeem, + showActions, connectedNetwork: wallet.connectedNetworkName, } }), @@ -173,7 +176,7 @@ export function useHoldings(address?: string, canInvestRedeem = true) { trancheId: '', poolId: '', currency: currency.currency, - canInvestRedeem: false, + showActions: false, connectedNetwork: wallet.connectedNetworkName, } }), @@ -189,7 +192,7 @@ export function useHoldings(address?: string, canInvestRedeem = true) { position: currency.balance.toDecimal(), tokenPrice: Dec(1), marketValue: currency.balance.toDecimal(), - canInvestRedeem: false, + showActions: false, connectedNetwork: wallet.connectedNetworkName, } }) || []), @@ -211,7 +214,7 @@ export function useHoldings(address?: string, canInvestRedeem = true) { position: centBalances?.native.balance.toDecimal().sub(centBalances.native.locked.toDecimal()) || Dec(0), tokenPrice: CFGPrice ? Dec(CFGPrice) : Dec(0), marketValue: CFGPrice ? centBalances?.native.balance.toDecimal().mul(CFGPrice) ?? Dec(0) : Dec(0), - canInvestRedeem: false, + showActions: false, connectedNetwork: wallet.connectedNetworkName, }, ] @@ -221,7 +224,7 @@ export function useHoldings(address?: string, canInvestRedeem = true) { return tokens } -export function Holdings({ canInvestRedeem = true, address }: { canInvestRedeem?: boolean; address?: string }) { +export function Holdings({ showActions = true, address }: { showActions?: boolean; address?: string }) { const { search, pathname } = useLocation() const history = useHistory() const params = new URLSearchParams(search) @@ -233,7 +236,7 @@ export function Holdings({ canInvestRedeem = true, address }: { canInvestRedeem? const [investPoolId, investTrancheId] = openInvestDrawer?.split('-') || [] const [redeemPoolId, redeemTrancheId] = openRedeemDrawer?.split('-') || [] - const tokens = useHoldings(address, canInvestRedeem) + const tokens = useHoldings(address, showActions) return address && tokens.length ? ( <> diff --git a/centrifuge-app/src/components/Portfolio/PortfolioValue.tsx b/centrifuge-app/src/components/Portfolio/PortfolioValue.tsx index bfe17f179d..fb50806ab6 100644 --- a/centrifuge-app/src/components/Portfolio/PortfolioValue.tsx +++ b/centrifuge-app/src/components/Portfolio/PortfolioValue.tsx @@ -32,6 +32,7 @@ export function PortfolioValue({ rangeValue, address }: { rangeValue: string; ad const dailyPortfolioValue = useDailyPortfolioValue(address, rangeNumber) const getXAxisInterval = () => { + if (!rangeNumber) return dailyPortfolioValue ? Math.floor(dailyPortfolioValue.length / 10) : 45 if (rangeNumber <= 30) return 5 if (rangeNumber > 30 && rangeNumber <= 90) { return 14 diff --git a/centrifuge-app/src/components/Portfolio/Transactions.tsx b/centrifuge-app/src/components/Portfolio/Transactions.tsx index e5dc3170be..e34bdf1e38 100644 --- a/centrifuge-app/src/components/Portfolio/Transactions.tsx +++ b/centrifuge-app/src/components/Portfolio/Transactions.tsx @@ -15,7 +15,6 @@ import { import * as React from 'react' import { TransactionTypeChip } from '../../components/Portfolio/TransactionTypeChip' import { formatDate } from '../../utils/date' -import { Dec } from '../../utils/Decimal' import { getCSVDownloadUrl } from '../../utils/getCSVDownloadUrl' import { usePools, useTransactionsByAddress } from '../../utils/usePools' import { Column, DataTable, SortableTableHeader } from '../DataTable' @@ -33,7 +32,7 @@ type Row = { action: InvestorTransactionType | AssetTransactionType date: number tranche?: Token - tranchePrice: string + tranchePrice: number amount: TokenBalance hash: string pool?: Pool @@ -81,9 +80,9 @@ export function Transactions({ onlyMostRecent, narrow, txTypes, address, tranche { align: 'right', header: 'Token price', - cell: ({ tranche, pool }: Row) => ( + cell: ({ tranche, tranchePrice, pool }: Row) => ( - {formatBalance(tranche?.tokenPrice?.toDecimal() || Dec(1), pool?.currency.symbol, 4)} + {formatBalance(tranchePrice, pool?.currency.symbol, 4)} ), }, @@ -130,13 +129,13 @@ export function Transactions({ onlyMostRecent, narrow, txTypes, address, tranche date: new Date(tx.timestamp).getTime(), action: tx.type, tranche, - tranchePrice: tranche?.tokenPrice?.toDecimal().toString() || '', + tranchePrice: tx?.tokenPrice?.toFloat() ?? 1, amount: tx.currencyAmount, hash: tx.hash, poolId: tx.poolId, pool, trancheId: tx.trancheId, - } as Row + } satisfies Row }) return txs }, [transactions?.investorTransactions, onlyMostRecent, txTypes, pools, trancheId]) diff --git a/centrifuge-app/src/components/Portfolio/usePortfolio.ts b/centrifuge-app/src/components/Portfolio/usePortfolio.ts index 055a47b041..03aa66f9f3 100644 --- a/centrifuge-app/src/components/Portfolio/usePortfolio.ts +++ b/centrifuge-app/src/components/Portfolio/usePortfolio.ts @@ -1,10 +1,18 @@ -import { CurrencyBalance, CurrencyMetadata, InvestorTransactionType, Price, Token } from '@centrifuge/centrifuge-js' -import { useCentrifugeQuery } from '@centrifuge/centrifuge-react' +import { + CurrencyBalance, + CurrencyMetadata, + InvestorTransactionType, + Price, + Token, + TokenBalance, + addressToHex, +} from '@centrifuge/centrifuge-js' import BN from 'bn.js' import Decimal from 'decimal.js-light' import { useMemo } from 'react' import { Dec } from '../../utils/Decimal' import { useDailyTranchesStates, usePools, useTransactionsByAddress } from '../../utils/usePools' +import { useSubquery } from '../../utils/useSubquery' type InvestorTransaction = { currencyAmount: CurrencyBalance @@ -17,18 +25,7 @@ type InvestorTransaction = { type: InvestorTransactionType } -type TrancheSnapshot = { - blockNumber: number - timestamp: string - tokenPrice: Price - trancheId: string - tranche: { - poolId: string - trancheId: string - } -} - -export function useDailyPortfolioValue(address: string, rangeValue: number) { +export function useDailyPortfolioValue(address: string, rangeValue?: number) { const transactions = useTransactionsByAddress(address) const transactionsByTrancheId = transactions?.investorTransactions.reduce( @@ -39,29 +36,22 @@ export function useDailyPortfolioValue(address: string, rangeValue: number) { {} as Record ) - const dailyTrancheStates = useDailyTranchesStates(Object.keys(transactionsByTrancheId || {})) + const daysSinceFirstTx = transactions?.investorTransactions + ? Math.ceil( + (new Date().getTime() - new Date(transactions.investorTransactions.at(-1)!.timestamp).getTime()) / + (1000 * 3600 * 24) + ) + : 0 - const dailyTrancheStatesByTrancheId = dailyTrancheStates?.reduce((tranches, trancheSnapshots) => { - if (trancheSnapshots.length) { - const trancheId = trancheSnapshots[0].tranche.trancheId - return { - ...tranches, - [trancheId]: trancheSnapshots, - } - } + const dailyTrancheStatesByTrancheId = useDailyTranchesStates(Object.keys(transactionsByTrancheId || {})) - return tranches - }, {} as Record) + const rangeDays = (rangeValue ?? daysSinceFirstTx) + 1 return useMemo(() => { - if ( - dailyTrancheStatesByTrancheId && - transactionsByTrancheId && - Object.keys(dailyTrancheStatesByTrancheId).length === dailyTrancheStates?.length - ) { + if (dailyTrancheStatesByTrancheId && transactionsByTrancheId) { const today = new Date() - return Array(rangeValue + 1) + return Array((rangeValue ?? daysSinceFirstTx) + 1) .fill(null) .map((_, i) => i) .map((day) => { @@ -71,7 +61,7 @@ export function useDailyPortfolioValue(address: string, rangeValue: number) { ) return transactionsInDateRange.reduce((trancheValues: Decimal, transaction) => { - const priceAtDate = getPriceAtDate(dailyTrancheStatesByTrancheId, trancheId, rangeValue, day, today) + const priceAtDate = getPriceAtDate(dailyTrancheStatesByTrancheId, trancheId, rangeDays, day, today) if (!priceAtDate) return trancheValues // TODO: remove this once we have the correct price -- https://github.com/centrifuge/pools-subql/issues/76 @@ -105,11 +95,17 @@ export function useDailyPortfolioValue(address: string, rangeValue: number) { ) }) } - }, [dailyTrancheStates?.length, dailyTrancheStatesByTrancheId, rangeValue, transactionsByTrancheId]) + }, [dailyTrancheStatesByTrancheId, rangeValue, transactionsByTrancheId]) } const getPriceAtDate = ( - dailyTrancheStatesByTrancheId: Record, + dailyTrancheStatesByTrancheId: Record< + string, + { + timestamp: string + tokenPrice: Price + }[] + >, trancheId: string, rangeValue: number, day: number, @@ -128,10 +124,120 @@ const getPriceAtDate = ( } export function usePortfolio(address?: string) { - const [result] = useCentrifugeQuery(['accountPortfolio', address], (cent) => cent.pools.getPortfolio([address!]), { - enabled: !!address, - }) - return result + // const [result] = useCentrifugeQuery(['accountPortfolio', address], (cent) => cent.pools.getPortfolio([address!]), { + // enabled: !!address, + // }) + // return result + + const { data: subData } = useSubquery( + `query ($account: String!) { + account( + id: $account + ) { + trancheBalances { + nodes { + claimableCurrency + claimableTrancheTokens + pendingInvestCurrency + pendingRedeemTrancheTokens + sumClaimedCurrency + sumClaimedTrancheTokens + trancheId + poolId + tranche { + tokenPrice + } + pool { + currency { + decimals + } + } + } + } + currencyBalances { + nodes { + amount + currency { + symbol + decimals + trancheId + } + } + } + } + }`, + { + account: address && addressToHex(address), + }, + { + enabled: !!address, + } + ) + + const data = useMemo(() => { + return ( + (subData?.account as undefined | {}) && + (Object.fromEntries( + subData.account.trancheBalances.nodes.map((tranche: any) => { + const decimals = tranche.pool.currency.decimals + const tokenPrice = new Price(tranche.tranche.tokenPrice) + let freeTrancheTokens = new CurrencyBalance(0, decimals) + + const claimableCurrency = new CurrencyBalance(tranche.claimableCurrency, decimals) + const claimableTrancheTokens = new TokenBalance(tranche.claimableTrancheTokens, decimals) + const pendingInvestCurrency = new CurrencyBalance(tranche.pendingInvestCurrency, decimals) + const pendingRedeemTrancheTokens = new TokenBalance(tranche.pendingRedeemTrancheTokens, decimals) + const sumClaimedCurrency = new CurrencyBalance(tranche.sumClaimedCurrency, decimals) + const sumClaimedTrancheTokens = new TokenBalance(tranche.sumClaimedTrancheTokens, decimals) + + const currencyAmount = subData.account.currencyBalances.nodes.find( + (b: any) => b.currency.trancheId && b.currency.trancheId === tranche.trancheId + ) + if (currencyAmount) { + freeTrancheTokens = new CurrencyBalance(currencyAmount.amount, decimals) + } + + const totalTrancheTokens = new CurrencyBalance( + new BN(tranche.claimableTrancheTokens) + .add(new BN(tranche.pendingRedeemTrancheTokens)) + .add(freeTrancheTokens), + decimals + ) + + return [ + tranche.trancheId.split('-')[1], + { + claimableCurrency, + claimableTrancheTokens, + pendingInvestCurrency, + pendingRedeemTrancheTokens, + sumClaimedCurrency, + sumClaimedTrancheTokens, + totalTrancheTokens, + freeTrancheTokens, + tokenPrice, + }, + ] + }) + ) as Record< + string, + { + claimableCurrency: CurrencyBalance + claimableTrancheTokens: TokenBalance + pendingInvestCurrency: CurrencyBalance + pendingRedeemTrancheTokens: TokenBalance + sumClaimedCurrency: CurrencyBalance + sumClaimedTrancheTokens: TokenBalance + totalTrancheTokens: TokenBalance + freeTrancheTokens: TokenBalance + tokenPrice: Price + // TODO: add reservedTrancheTokens + } + >) + ) + }, [subData]) + + return data } type PortfolioToken = { @@ -161,28 +267,19 @@ export function usePortfolioTokens(address?: string) { ) if (portfolioData && trancheTokenPrices) { - return Object.keys(portfolioData)?.reduce((sum, trancheId) => { - const tranche = portfolioData[trancheId] - + return Object.entries(portfolioData).map(([trancheId, tranche]) => { const trancheTokenPrice = trancheTokenPrices[trancheId].tokenPrice || new Price(0) - const trancheTokensBalance = tranche.claimableTrancheTokens - .toDecimal() - .add(tranche.freeTrancheTokens.toDecimal()) - .add(tranche.reservedTrancheTokens.toDecimal()) - .add(tranche.pendingRedeemTrancheTokens.toDecimal()) + const trancheTokensBalance = tranche.totalTrancheTokens.toDecimal() - return [ - ...sum, - { - position: trancheTokensBalance, - marketValue: trancheTokensBalance.mul(trancheTokenPrice.toDecimal()), - tokenPrice: trancheTokenPrice, - trancheId: trancheId, - poolId: trancheTokenPrices[trancheId].poolId, - currency: trancheTokenPrices[trancheId].currency, - }, - ] + return { + position: trancheTokensBalance, + marketValue: trancheTokensBalance.mul(trancheTokenPrice.toDecimal()), + tokenPrice: trancheTokenPrice, + trancheId: trancheId, + poolId: trancheTokenPrices[trancheId].poolId, + currency: trancheTokenPrices[trancheId].currency, + } }, [] as PortfolioToken[]) } diff --git a/centrifuge-app/src/components/Resolutions.tsx b/centrifuge-app/src/components/Resolutions.tsx index e253978341..bddf431dd9 100644 --- a/centrifuge-app/src/components/Resolutions.tsx +++ b/centrifuge-app/src/components/Resolutions.tsx @@ -1,4 +1,4 @@ -import { Shelf, Stack, Text } from '@centrifuge/fabric' +import { Box, Grid, Stack, Text } from '@centrifuge/fabric' import styled from 'styled-components' import { DAO } from '../utils/useDAOConfig' import { LayoutSection } from './LayoutBase/LayoutSection' @@ -6,11 +6,9 @@ import { LayoutSection } from './LayoutBase/LayoutSection' export const Resolutions = ({ dao }: { dao: DAO }) => { return ( - + {dao.resolutions.map((blog) => ( { rel="noopener noreferrer" borderRadius="4px" > - {blog.title} + {blog.title} {new Date(blog.timestamp * 1000).toLocaleDateString('en-US', { @@ -37,7 +35,7 @@ export const Resolutions = ({ dao }: { dao: DAO }) => { ))} - + ) } diff --git a/centrifuge-app/src/pages/Prime/Detail.tsx b/centrifuge-app/src/pages/Prime/Detail.tsx index 4e633e9f45..4f34bd7e11 100644 --- a/centrifuge-app/src/pages/Prime/Detail.tsx +++ b/centrifuge-app/src/pages/Prime/Detail.tsx @@ -48,7 +48,7 @@ function PrimeDetail() { - + diff --git a/centrifuge-app/src/pages/Prime/index.tsx b/centrifuge-app/src/pages/Prime/index.tsx index 89889ed86e..6cca11c3c1 100644 --- a/centrifuge-app/src/pages/Prime/index.tsx +++ b/centrifuge-app/src/pages/Prime/index.tsx @@ -1,15 +1,15 @@ -import { Price } from '@centrifuge/centrifuge-js' -import { useCentrifuge, useCentrifugeUtils, useGetNetworkName } from '@centrifuge/centrifuge-react' +import { CurrencyBalance, addressToHex } from '@centrifuge/centrifuge-js' +import { useCentrifugeUtils, useGetNetworkName } from '@centrifuge/centrifuge-react' import { AnchorButton, Box, IconExternalLink, Shelf, Text, TextWithPlaceholder } from '@centrifuge/fabric' -import { useQuery } from 'react-query' -import { firstValueFrom } from 'rxjs' +import { BN } from 'bn.js' import { Column, DataTable, FilterableTableHeader, SortableTableHeader } from '../../components/DataTable' import { LayoutBase } from '../../components/LayoutBase' import { LayoutSection } from '../../components/LayoutBase/LayoutSection' import { formatDate } from '../../utils/date' -import { formatBalance, formatPercentage } from '../../utils/formatting' +import { formatBalance } from '../../utils/formatting' import { DAO, useDAOConfig } from '../../utils/useDAOConfig' import { useFilters } from '../../utils/useFilters' +import { usePools } from '../../utils/usePools' import { useSubquery } from '../../utils/useSubquery' export default function PrimePage() { @@ -53,23 +53,19 @@ type Row = DAO & { value?: number; profit?: number; networkName: string; firstIn function DaoPortfoliosTable() { const utils = useCentrifugeUtils() - const cent = useCentrifuge() const getNetworkName = useGetNetworkName() const { data: daoData } = useDAOConfig() + const pools = usePools() const daos = daoData?.map((dao) => ({ ...dao, - address: utils.formatAddress( - typeof dao.network === 'number' ? utils.evmToSubstrateAddress(dao.address, dao.network) : dao.address - ), + centAddress: + typeof dao.network === 'number' + ? utils.evmToSubstrateAddress(dao.address, dao.network) + : addressToHex(dao.address), })) || [] - // TODO: Update to use new portfolio Runtime API - const { data, isLoading: isPortfoliosLoading } = useQuery(['daoPortfolios', daos.map((dao) => dao.address)], () => - Promise.all(daos.map((dao) => firstValueFrom(cent.pools.getBalances([dao.address])))) - ) - const { data: subData, isLoading: isSubqueryLoading } = useSubquery( `query ($accounts: [String!]) { accounts( @@ -77,7 +73,7 @@ function DaoPortfoliosTable() { ) { nodes { id - investorTransactions { + investorTransactions(orderBy: TIMESTAMP_ASC, first: 1) { nodes { timestamp tokenPrice @@ -87,37 +83,68 @@ function DaoPortfoliosTable() { } } } + trancheBalances { + nodes { + claimableCurrency + claimableTrancheTokens + pendingInvestCurrency + pendingRedeemTrancheTokens + sumClaimedCurrency + sumClaimedTrancheTokens + trancheId + poolId + } + } + currencyBalances { + nodes { + amount + currency { + symbol + decimals + trancheId + } + } + } } } }`, { - accounts: daos.map((dao) => dao.address), + accounts: daos.map((dao) => dao.centAddress), } ) const mapped: Row[] = daos.map((dao, i) => { - const investTxs = subData?.accounts.nodes.find((n: any) => n.id === dao.address)?.investorTransactions.nodes - const trancheBalances = - data?.[i].tranches && Object.fromEntries(data[i].tranches.map((t) => [t.trancheId, t.balance.toFloat()])) - const yields = - trancheBalances && - Object.keys(trancheBalances).map((tid) => { - const firstTx = investTxs?.find((tx: any) => tx.tranche.trancheId === tid) - const initialTokenPrice = firstTx && new Price(firstTx.tokenPrice).toFloat() - const tokenPrice = firstTx && new Price(firstTx.tranche.tokenPrice).toFloat() - const profit = tokenPrice / initialTokenPrice - 1 - return [tid, profit] as const - }) - const totalValue = trancheBalances && Object.values(trancheBalances)?.reduce((acc, balance) => acc + balance, 0) - const weightedYield = - yields && - totalValue && - yields.reduce((acc, [tid, profit]) => acc + profit * trancheBalances![tid], 0) / totalValue + const account = subData?.accounts.nodes.find((n: any) => n.id === dao.centAddress) + const investTxs = account?.investorTransactions.nodes + const trancheBalances = !!account + ? Object.fromEntries( + account.trancheBalances.nodes.map((tranche: any) => { + const pool = pools?.find((p) => p.id === tranche.poolId) + const decimals = pool?.currency.decimals ?? 18 + const tokenPrice = pool?.tranches.find((t) => tranche.trancheId.endsWith(t.id))?.tokenPrice?.toFloat() ?? 1 + let balance = new CurrencyBalance( + new BN(tranche.claimableTrancheTokens).add(new BN(tranche.pendingRedeemTrancheTokens)), + decimals + ).toFloat() + + const subqueryCurrency = account?.currencyBalances.nodes.find( + (b: any) => b.currency.trancheId && b.currency.trancheId === tranche.trancheId + ) + if (subqueryCurrency) { + balance += new CurrencyBalance(subqueryCurrency.amount, decimals).toFloat() + } + return [tranche.trancheId.split('-')[1], { balance, tokenPrice }] + }) + ) + : {} + const totalValue = Object.values(trancheBalances)?.reduce( + (acc, { balance, tokenPrice }) => acc + balance * tokenPrice, + 0 + ) return { ...dao, value: totalValue ?? 0, - profit: weightedYield ? weightedYield * 100 : 0, networkName: getNetworkName(dao.network), firstInvestment: investTxs?.[0] && new Date(investTxs[0].timestamp), } @@ -147,26 +174,17 @@ function DaoPortfoliosTable() { options={Object.fromEntries(uniqueNetworks.map((chain) => [chain, getNetworkName(chain)]))} /> ), - cell: (row: Row) => {row.network}, + cell: (row: Row) => {getNetworkName(row.network)}, }, { header: , cell: (row: Row) => ( - + {row.value != null && formatBalance(row.value, 'USD')} ), sortKey: 'value', }, - { - header: , - cell: (row: Row) => ( - - {row.profit != null && formatPercentage(row.profit)} - - ), - sortKey: 'profit', - }, { align: 'left', header: 'First investment', diff --git a/centrifuge-app/src/utils/usePools.ts b/centrifuge-app/src/utils/usePools.ts index 2904757ebf..0b14e1866a 100644 --- a/centrifuge-app/src/utils/usePools.ts +++ b/centrifuge-app/src/utils/usePools.ts @@ -157,7 +157,7 @@ export function useDailyPoolStates(poolId: string, from?: Date, to?: Date, suspe export function useDailyTranchesStates(trancheIds: string[]) { const [result] = useCentrifugeQuery( ['dailyTrancheStates', { trancheIds }], - (cent) => combineLatest(trancheIds.map((tid) => cent.pools.getDailyTrancheStates([tid]))), + (cent) => cent.pools.getDailyTrancheStates([{ trancheIds }]), { suspense: true, enabled: !!trancheIds?.length, diff --git a/centrifuge-app/src/utils/useSubquery.ts b/centrifuge-app/src/utils/useSubquery.ts index ed5eaf5fb5..a4c9ced589 100644 --- a/centrifuge-app/src/utils/useSubquery.ts +++ b/centrifuge-app/src/utils/useSubquery.ts @@ -1,10 +1,12 @@ import { useCentrifuge } from '@centrifuge/centrifuge-react' -import { useQuery } from 'react-query' +import { UseQueryOptions, useQuery } from 'react-query' import { firstValueFrom } from 'rxjs' -export function useSubquery(query: string, variables?: object) { +export function useSubquery(query: string, variables?: object, options?: Omit) { const cent = useCentrifuge() - return useQuery(['subquery', query, variables], () => - firstValueFrom(cent.getSubqueryObservable(query, variables, false)) + return useQuery( + ['subquery', query, variables], + () => firstValueFrom(cent.getSubqueryObservable(query, variables, false)) as any, + options as any ) } diff --git a/centrifuge-js/src/modules/pools.ts b/centrifuge-js/src/modules/pools.ts index 4a98f47c52..63d53e14b8 100644 --- a/centrifuge-js/src/modules/pools.ts +++ b/centrifuge-js/src/modules/pools.ts @@ -5,7 +5,7 @@ import { Codec } from '@polkadot/types-codec/types' import { blake2AsHex } from '@polkadot/util-crypto/blake2' import BN from 'bn.js' import { EMPTY, Observable, combineLatest, expand, firstValueFrom, forkJoin, from, of, startWith } from 'rxjs' -import { combineLatestWith, filter, map, repeatWhen, switchMap, take } from 'rxjs/operators' +import { combineLatestWith, filter, map, repeatWhen, switchMap, take, takeLast } from 'rxjs/operators' import { SolverResult, calculateOptimalSolution } from '..' import { Centrifuge } from '../Centrifuge' import { Account, TransactionOptions } from '../types' @@ -2200,22 +2200,38 @@ export function getPoolsModule(inst: Centrifuge) { ) } - function getTrancheSnapshotsWithCursor(poolId: string, endCursor: string | null, from?: Date, to?: Date) { + function getTrancheSnapshotsWithCursor( + filterBy: { poolId: string } | { trancheIds: string[] }, + endCursor: string | null, + from?: Date, + to?: Date + ) { + const filter: any = { + timestamp: { + greaterThan: from ? from.toISOString() : getDateYearsFromNow(-10).toISOString(), + lessThan: to ? to.toISOString() : getDateYearsFromNow(10).toISOString(), + }, + } + if ('poolId' in filterBy) { + filter.tranche = { poolId: { equalTo: filterBy.poolId } } + } else if ('trancheIds' in filterBy) { + filter.tranche = { trancheId: { in: filterBy.trancheIds } } + } + return inst.getSubqueryObservable<{ trancheSnapshots: { nodes: SubqueryTrancheSnapshot[]; pageInfo: { hasNextPage: boolean; endCursor: string } } }>( - `query($poolId: String!, $from: Datetime!, $to: Datetime!, $trancheCursor: Cursor) { + `query($filter: TrancheSnapshotFilter, $trancheCursor: Cursor) { trancheSnapshots( orderBy: BLOCK_NUMBER_ASC, - filter: { - id: { startsWith: $poolId }, - timestamp: { greaterThan: $from, lessThan: $to } - } + filter: $filter after: $trancheCursor ) { nodes { - id - trancheId + tranche { + poolId + trancheId + } timestamp tokenSupply tokenPrice @@ -2232,14 +2248,65 @@ export function getPoolsModule(inst: Centrifuge) { } `, { - poolId, - from: from ? from.toISOString() : getDateYearsFromNow(-10).toISOString(), - to: to ? to.toISOString() : getDateYearsFromNow(10).toISOString(), + filter, trancheCursor: endCursor, } ) } + function getDailyTrancheStates( + args: [filter: { poolId: string } | { trancheIds: string[] }, from?: Date, to?: Date] + ) { + const [filter, from, to] = args + + return of({ trancheSnapshots: [], endCursor: null, hasNextPage: true }).pipe( + expand(({ trancheSnapshots, endCursor, hasNextPage }) => { + if (!hasNextPage) return EMPTY + return getTrancheSnapshotsWithCursor(filter, endCursor, from, to).pipe( + map( + ( + response: { + trancheSnapshots: { + nodes: SubqueryTrancheSnapshot[] + pageInfo: { hasNextPage: boolean; endCursor: string } + } + } | null + ) => { + if (response?.trancheSnapshots) { + const { endCursor, hasNextPage } = response.trancheSnapshots.pageInfo + + return { + endCursor, + hasNextPage, + trancheSnapshots: [...trancheSnapshots, ...response.trancheSnapshots.nodes], + } + } + return {} + } + ) + ) + }), + takeLast(1), + map(({ trancheSnapshots }) => { + const trancheStates: Record = {} + trancheSnapshots?.forEach((state) => { + const tid = state.tranche.trancheId + const entry = { + timestamp: state.timestamp, + tokenPrice: new Price(state.tokenPrice), + pool: state.tranche.poolId, + } + if (trancheStates[tid]) { + trancheStates[tid].push(entry) + } else { + trancheStates[tid] = [entry] + } + }) + return trancheStates + }) + ) + } + function getDailyPoolStates(args: [poolId: string, from?: Date, to?: Date]) { const [poolId, from, to] = args @@ -2275,7 +2342,7 @@ export function getPoolsModule(inst: Centrifuge) { of({ trancheSnapshots: [], endCursor: null, hasNextPage: true }).pipe( expand(({ trancheSnapshots, endCursor, hasNextPage }) => { if (!hasNextPage) return EMPTY - return getTrancheSnapshotsWithCursor(poolId, endCursor, from, to).pipe( + return getTrancheSnapshotsWithCursor({ poolId }, endCursor, from, to).pipe( map( ( response: { @@ -2305,7 +2372,7 @@ export function getPoolsModule(inst: Centrifuge) { map(([{ poolSnapshots }, { trancheSnapshots }, poolCurrency]) => { const trancheStates: Record = {} trancheSnapshots?.forEach((state) => { - const tid = state.trancheId.split('-')[1] + const tid = state.tranche.trancheId const entry = { timestamp: state.timestamp, tokenPrice: new Price(state.tokenPrice) } if (trancheStates[tid]) { trancheStates[tid].push(entry) @@ -2328,7 +2395,7 @@ export function getPoolsModule(inst: Centrifuge) { const tranches: { [trancheId: string]: DailyTrancheState } = {} trancheSnapshotsToday?.forEach((tranche) => { - const tid = tranche.trancheId.split('-')[1] + const tid = tranche.tranche.trancheId tranches[tid] = { id: tranche.trancheId, price: tranche.tokenPrice ? new Price(tranche.tokenPrice) : null, @@ -2420,47 +2487,6 @@ export function getPoolsModule(inst: Centrifuge) { ) } - function getDailyTrancheStates(args: [trancheId: string]) { - const [trancheId] = args - const $query = inst.getSubqueryObservable<{ trancheSnapshots: { nodes: SubqueryTrancheSnapshot[] } }>( - `query($trancheId: String!) { - trancheSnapshots( - orderBy: BLOCK_NUMBER_DESC, - filter: { - trancheId: { includes: $trancheId }, - }) { - nodes { - tokenPrice - blockNumber - timestamp - trancheId - tranche { - poolId - trancheId - } - } - } - } - `, - { - trancheId, - } - ) - return $query.pipe( - map((data) => { - if (!data) { - return [] - } - return data.trancheSnapshots.nodes.reverse().map((state) => { - return { - ...state, - tokenPrice: new Price(state.tokenPrice), - } - }) - }) - ) - } - function getMonthlyPoolStates(args: [poolId: string, from?: Date, to?: Date]) { return getDailyPoolStates(args).pipe( map(({ poolStates }) => { From 5fc75396a8cd415c9d8ecebcd0b03cab07cfc72d Mon Sep 17 00:00:00 2001 From: Onno Visser Date: Tue, 14 May 2024 10:15:20 +0200 Subject: [PATCH 04/10] Prime: hide 0 CFG balance (#2125) --- centrifuge-app/src/components/Portfolio/Holdings.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/centrifuge-app/src/components/Portfolio/Holdings.tsx b/centrifuge-app/src/components/Portfolio/Holdings.tsx index e145aecd0e..87fe05059c 100644 --- a/centrifuge-app/src/components/Portfolio/Holdings.tsx +++ b/centrifuge-app/src/components/Portfolio/Holdings.tsx @@ -196,7 +196,7 @@ export function useHoldings(address?: string, showActions = true) { connectedNetwork: wallet.connectedNetworkName, } }) || []), - ...(wallet.connectedNetworkName === 'Centrifuge' + ...((wallet.connectedNetworkName === 'Centrifuge' && showActions) || centBalances?.native.balance.gtn(0) ? [ { currency: { From e5d29bf96a3c60d3b68de42f7bcbcf66058589f5 Mon Sep 17 00:00:00 2001 From: JP Date: Tue, 14 May 2024 17:01:18 -0500 Subject: [PATCH 05/10] fix: text alignment (#2128) --- .../src/components/PoolOverview/TrancheTokenCards.tsx | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/centrifuge-app/src/components/PoolOverview/TrancheTokenCards.tsx b/centrifuge-app/src/components/PoolOverview/TrancheTokenCards.tsx index e29b414f34..64fe0223bd 100644 --- a/centrifuge-app/src/components/PoolOverview/TrancheTokenCards.tsx +++ b/centrifuge-app/src/components/PoolOverview/TrancheTokenCards.tsx @@ -100,9 +100,11 @@ const TrancheTokenCard = ({ )} - - Token price - + + + Token price + + {formatBalance(trancheToken.tokenPrice || 0, poolCurrencySymbol, 5, 5)} From ba568a3d3053fdbb52307b4164db22c62abb4f95 Mon Sep 17 00:00:00 2001 From: Sophia Date: Thu, 16 May 2024 15:50:09 +0100 Subject: [PATCH 06/10] Nav management overview + table updates (#2122) * Update layout so that button isn't hidden under wallet component * Show nav-management menu when toggle is enabled * Move asset table to new file * Add basic layout for nav summary and overview table * Move hook to own file * Add rest of calcs * Fix summary * Remove collection id from createPool since it's not used * Fix change it asset valuation calcs * Prefill price for assets and add row clickable * Remove last of collectionId * Remove unused variable * Make input editable * Add reserve row to nav table * Fix total calc * Hide input on reserve row and update reserve logo * Fix link on reserve click --- .../Charts/PoolPerformanceChart.tsx | 1 - .../src/components/Menu/NavManagementMenu.tsx | 8 +- .../src/pages/IssuerCreatePool/index.tsx | 5 +- centrifuge-app/src/pages/MultisigApproval.tsx | 65 ++- .../NavManagement/NavManagementAssetTable.tsx | 291 ++++++++++++ .../src/pages/NavManagement/Overview.tsx | 441 +++++------------- centrifuge-app/src/utils/useCreatePoolFee.ts | 5 +- .../utils/usePoolsForWhichAccountIsFeeder.tsx | 56 +++ centrifuge-js/src/modules/pools.ts | 7 +- 9 files changed, 517 insertions(+), 362 deletions(-) create mode 100644 centrifuge-app/src/pages/NavManagement/NavManagementAssetTable.tsx create mode 100644 centrifuge-app/src/utils/usePoolsForWhichAccountIsFeeder.tsx diff --git a/centrifuge-app/src/components/Charts/PoolPerformanceChart.tsx b/centrifuge-app/src/components/Charts/PoolPerformanceChart.tsx index f8c7abf124..4230f7e431 100644 --- a/centrifuge-app/src/components/Charts/PoolPerformanceChart.tsx +++ b/centrifuge-app/src/components/Charts/PoolPerformanceChart.tsx @@ -70,7 +70,6 @@ function PoolPerformanceChart() { // querying chain for more accurate data, since data for today from subquery is not necessarily up to date const todayAssetValue = pool?.nav.total.toDecimal().toNumber() || 0 - const todayReserve = pool?.reserve.total.toDecimal().toNumber() || 0 const chartData = data.slice(-rangeNumber) diff --git a/centrifuge-app/src/components/Menu/NavManagementMenu.tsx b/centrifuge-app/src/components/Menu/NavManagementMenu.tsx index d5c54ce90a..3fb96ee2e6 100644 --- a/centrifuge-app/src/components/Menu/NavManagementMenu.tsx +++ b/centrifuge-app/src/components/Menu/NavManagementMenu.tsx @@ -2,7 +2,7 @@ import { Box, IconChevronDown, IconChevronRight, IconMonitor, Menu, MenuItemGrou import * as React from 'react' import { useRouteMatch } from 'react-router' import { useTheme } from 'styled-components' -import { usePoolsForWhichAccountIsFeeder } from '../../pages/NavManagement/Overview' +import { usePoolsForWhichAccountIsFeeder } from '../../utils/usePoolsForWhichAccountIsFeeder' import { PoolLink } from './PoolLink' import { Toggle } from './Toggle' @@ -22,7 +22,7 @@ export function NavManagementMenu({ stacked }: NavManagementMenuProps) { return ( allowedPools && - allowedPools?.length > 1 && ( + allowedPools?.length >= 1 && ( {open && ( - + NAV management - {!stacked && (open ? : )} + {!stacked && (open ? : )} { - const [transferToMultisig, aoProxy, adminProxy, , , , , , { adminMultisig }] = args + const [transferToMultisig, aoProxy, adminProxy, , , , , { adminMultisig }] = args const multisigAddr = adminMultisig && createKeyMulti(adminMultisig.signers, adminMultisig.threshold) const poolArgs = args.slice(2) as any return combineLatest([ @@ -403,7 +402,6 @@ function CreatePoolForm() { const currency = currencies.find((c) => c.symbol === values.currency)! const poolId = await centrifuge.pools.getAvailablePoolId() - const collectionId = await centrifuge.nfts.getAvailableCollectionId() if (!values.poolIcon || !values.executiveSummary) { return } @@ -479,7 +477,6 @@ function CreatePoolForm() { aoProxy, adminProxy, poolId, - collectionId, tranches, currency.key, CurrencyBalance.fromFloat(values.maxReserve, currency.decimals), diff --git a/centrifuge-app/src/pages/MultisigApproval.tsx b/centrifuge-app/src/pages/MultisigApproval.tsx index b66e51b769..96e9beea71 100644 --- a/centrifuge-app/src/pages/MultisigApproval.tsx +++ b/centrifuge-app/src/pages/MultisigApproval.tsx @@ -46,11 +46,35 @@ export default function MultisigApprovalPage() { const suitableAccount = accounts?.find((acc) => multisig.signers.includes(acc.address)) return ( - + + + {isCallDataNeeded && !callString && ( + <> + setCallFormInput(e.target.value)} + /> + {callInputError && ( + + Calldata doesn't match hash + + )} + + )} + {callString && ( +
+ + Call details + + + {callString} + +
+ )} + {selectedAddress && !multisig.signers.includes(selectedAddress) ? ( selected account not signer to multisig {suitableAccount && ( @@ -63,35 +87,8 @@ export default function MultisigApprovalPage() { - ) - } - > - - {isCallDataNeeded && !callString && ( - <> - setCallFormInput(e.target.value)} - /> - {callInputError && ( - - Calldata doesn't match hash - - )} - - )} - {callString && ( -
- - Call details - - - {callString} - -
- )} + )} +
) diff --git a/centrifuge-app/src/pages/NavManagement/NavManagementAssetTable.tsx b/centrifuge-app/src/pages/NavManagement/NavManagementAssetTable.tsx new file mode 100644 index 0000000000..2fdd41b066 --- /dev/null +++ b/centrifuge-app/src/pages/NavManagement/NavManagementAssetTable.tsx @@ -0,0 +1,291 @@ +import { ActiveLoan, CreatedLoan, CurrencyBalance, ExternalLoan } from '@centrifuge/centrifuge-js' +import { useCentrifugeApi, useCentrifugeQuery, useCentrifugeTransaction } from '@centrifuge/centrifuge-react' +import { Box, Button, CurrencyInput, Drawer, IconDownload, Shelf, Text, Thumbnail } from '@centrifuge/fabric' +import { Field, FieldProps, FormikProvider, useFormik } from 'formik' +import * as React from 'react' +import daiLogo from '../../assets/images/dai-logo.svg' +import usdcLogo from '../../assets/images/usdc-logo.svg' +import { ButtonGroup } from '../../components/ButtonGroup' +import { DataCol, DataRow, DataTable } from '../../components/DataTable' +import { LayoutSection } from '../../components/LayoutBase/LayoutSection' +import { AssetName } from '../../components/LoanList' +import { formatDate } from '../../utils/date' +import { formatBalance } from '../../utils/formatting' +import { usePool } from '../../utils/usePools' +import { usePoolsForWhichAccountIsFeeder } from '../../utils/usePoolsForWhichAccountIsFeeder' +import { settlementPrice } from '../../utils/validation' +import { isCashLoan, isExternalLoan } from '../Loan/utils' + +type FormValues = { + feed: { + formIndex: number + id: string + oldValue: number + value: number | '' + Isin: string + quantity: number + maturity: string + currentPrice: number + }[] + closeEpoch: boolean +} +type Row = FormValues['feed'][0] | ActiveLoan | CreatedLoan + +export function NavManagementAssetTable({ poolId }: { poolId: string }) { + const allowedPools = usePoolsForWhichAccountIsFeeder() + const isFeeder = !!allowedPools?.find((p) => p.id === poolId) + const [isEditing, setIsEditing] = React.useState(false) + const [isConfirming, setIsConfirming] = React.useState(false) + + const pool = usePool(poolId, false) + const [allLoans] = useCentrifugeQuery(['loans', poolId], (cent) => cent.pools.getLoans([poolId]), { + enabled: !!poolId && !!pool, + }) + + const externalLoans = React.useMemo( + () => (allLoans?.filter((l) => isExternalLoan(l) && l.status !== 'Closed') as ExternalLoan[]) ?? [], + [allLoans] + ) + const cashLoans = + (allLoans?.filter((l) => isCashLoan(l) && l.status !== 'Closed') as (CreatedLoan | ActiveLoan)[]) ?? [] + const api = useCentrifugeApi() + + const reserveRow = [ + { + id: 'reserve', + Isin: 'Reserve', + quantity: 1, + currentPrice: 0, + value: pool?.reserve.total.toDecimal().toNumber(), + formIndex: -1, + maturity: '', + oldValue: '', + }, + ] + + const { execute, isLoading } = useCentrifugeTransaction( + 'Set oracle prices', + (cent) => (args: [values: FormValues], options) => { + const [values] = args + const batch = [ + ...values.feed + .filter((f) => typeof f.value === 'number' && !Number.isNaN(f.value)) + .map((f) => api.tx.oraclePriceFeed.feed({ Isin: f.Isin }, CurrencyBalance.fromFloat(f.value, 18))), + api.tx.oraclePriceCollection.updateCollection(poolId), + api.tx.loans.updatePortfolioValuation(poolId), + ] + if (values.closeEpoch) { + batch.push(api.tx.poolSystem.closeEpoch(poolId)) + } + const tx = api.tx.utility.batchAll(batch) + return cent.wrapSignAndSend(api, tx, options) + } + ) + + const initialValues = React.useMemo( + () => ({ + feed: + externalLoans?.map((l, i) => { + let latestOraclePrice = l.pricing.oracle[0] + l.pricing.oracle.forEach((price) => { + if (price.timestamp > latestOraclePrice.timestamp) { + latestOraclePrice = price + } + }) + return { + formIndex: i, + id: l.id, + oldValue: latestOraclePrice.value.toFloat(), + value: '' as const, + Isin: l.pricing.Isin, + quantity: l.pricing.outstandingQuantity.toFloat(), + maturity: formatDate(l.pricing.maturityDate), + currentPrice: l.status === 'Active' ? l?.currentPrice.toDecimal().toNumber() : 0, + } + }) ?? [], + closeEpoch: false, + }), + [externalLoans] + ) + + const form = useFormik({ + initialValues, + onSubmit(values, actions) { + execute([values]) + actions.setSubmitting(false) + }, + }) + + React.useEffect(() => { + if (isEditing && !isLoading) return + form.resetForm() + form.setValues(initialValues, false) + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [initialValues, isEditing, isLoading]) + + const poolReserve = pool?.reserve.total.toDecimal().toNumber() || 0 + const newNavExternal = form.values.feed.reduce( + (acc, cur) => acc + cur.quantity * (isEditing ? cur.currentPrice : cur.value || cur.oldValue), + 0 + ) + const newNavCash = cashLoans.reduce((acc, cur) => acc + cur.outstandingDebt.toFloat(), 0) + const newNav = newNavExternal + newNavCash + poolReserve + const isTinlakePool = poolId.startsWith('0x') + + const columns = [ + { + align: 'left', + header: 'Asset', + cell: (row: Row) => + 'oldValue' in row ? ( + + {row.id === 'reserve' ? ( + + + + ) : ( + + )} + + {row.Isin} + + + ) : ( + + + + ), + }, + { + align: 'left', + header: 'Maturity date', + cell: (row: Row) => ('oldValue' in row ? row.maturity : ''), + }, + { + align: 'right', + header: 'Quantity', + cell: (row: Row) => + row.id !== 'reserve' ? formatBalance('oldValue' in row ? row.quantity : row.outstandingDebt) : '', + }, + { + align: 'right', + header: 'Asset price', + cell: (row: Row) => + row.id !== 'reserve' ? formatBalance('oldValue' in row ? row.oldValue : 1, pool?.currency.symbol, 8) : '', + }, + { + align: 'right', + header: 'New price', + cell: (row: Row) => { + return 'oldValue' in row && row.id !== 'reserve' ? ( + + {({ field, meta, form }: FieldProps) => ( + form.setFieldValue(`feed.${row.formIndex}.value`, value)} + value={row.currentPrice} + onClick={(e) => e.preventDefault()} + /> + )} + + ) : ( + '' + ) + }, + }, + { + align: 'right', + header: 'Value', + cell: (row: Row) => + 'oldValue' in row + ? formatBalance(row.quantity * (row.value || row.oldValue), pool?.currency.symbol) + : formatBalance(row.outstandingDebt, pool?.currency.symbol), + }, + ] + + if (!isEditing) { + columns.splice(4, 1) + } + + return ( + <> + + setIsConfirming(false)}> + + + + + + + + + + ) : ( + + + + + ) + } + > + + row.id !== 'reserve' ? `/issuer/${pool?.id}/assets/${row.id}` : `/nav-management/${pool?.id}` + } + footer={ + + + + Total + + + + + + {isEditing && } + + + {formatBalance(newNav, pool?.currency.symbol)} + + + + } + /> + + + + ) +} diff --git a/centrifuge-app/src/pages/NavManagement/Overview.tsx b/centrifuge-app/src/pages/NavManagement/Overview.tsx index 7451646715..77d691ea68 100644 --- a/centrifuge-app/src/pages/NavManagement/Overview.tsx +++ b/centrifuge-app/src/pages/NavManagement/Overview.tsx @@ -1,28 +1,15 @@ -import { ActiveLoan, addressToHex, CreatedLoan, CurrencyBalance, ExternalLoan } from '@centrifuge/centrifuge-js' -import { - useAddress, - useCentrifugeApi, - useCentrifugeQuery, - useCentrifugeTransaction, -} from '@centrifuge/centrifuge-react' -import { Button, CurrencyInput, Drawer, IconDownload, Shelf, Text, Thumbnail } from '@centrifuge/fabric' -import { Field, FieldProps, FormikProvider, useFormik } from 'formik' -import * as React from 'react' import { useParams } from 'react-router' -import { map } from 'rxjs' -import { ButtonGroup } from '../../components/ButtonGroup' -import { DataCol, DataRow, DataTable } from '../../components/DataTable' import { LayoutBase } from '../../components/LayoutBase' -import { LayoutSection } from '../../components/LayoutBase/LayoutSection' -import { AssetName } from '../../components/LoanList' import { PageSummary } from '../../components/PageSummary' import { Tooltips } from '../../components/Tooltips' -import { isSubstrateAddress } from '../../utils/address' -import { formatDate } from '../../utils/date' import { formatBalance } from '../../utils/formatting' -import { usePool, usePoolMetadata, usePools } from '../../utils/usePools' -import { settlementPrice } from '../../utils/validation' -import { isCashLoan, isExternalLoan } from '../Loan/utils' +import { useDailyPoolStates, usePool } from '../../utils/usePools' + +import { CurrencyBalance } from '@centrifuge/centrifuge-js' +import { Box, Divider, IconClockForward, Shelf, Stack, Text } from '@centrifuge/fabric' +import { BN } from 'bn.js' +import React from 'react' +import { NavManagementAssetTable } from './NavManagementAssetTable' import { NavManagementHeader } from './NavManagementHeader' export default function NavManagementOverviewPage() { @@ -30,311 +17,137 @@ export default function NavManagementOverviewPage() { return ( - + + + ) } -type FormValues = { - feed: { - formIndex: number - id: string - oldValue: number - value: number | '' - Isin: string - quantity: number - maturity: string - }[] - closeEpoch: boolean -} -type Row = FormValues['feed'][0] | ActiveLoan | CreatedLoan - -function NavManagement({ poolId }: { poolId: string }) { - const allowedPools = usePoolsForWhichAccountIsFeeder() - const isFeeder = !!allowedPools?.find((p) => p.id === poolId) - const [isEditing, setIsEditing] = React.useState(false) - const [isConfirming, setIsConfirming] = React.useState(false) - - const pool = usePool(poolId, false) - const [allLoans] = useCentrifugeQuery(['loans', poolId], (cent) => cent.pools.getLoans([poolId]), { - enabled: !!poolId && !!pool, - }) - - const externalLoans = React.useMemo( - () => (allLoans?.filter((l) => isExternalLoan(l) && l.status !== 'Closed') as ExternalLoan[]) ?? [], - [allLoans] - ) - const cashLoans = - (allLoans?.filter((l) => isCashLoan(l) && l.status !== 'Closed') as (CreatedLoan | ActiveLoan)[]) ?? [] - const api = useCentrifugeApi() - - const { execute, isLoading } = useCentrifugeTransaction( - 'Set oracle prices', - (cent) => (args: [values: FormValues], options) => { - const [values] = args - const batch = [ - ...values.feed - .filter((f) => typeof f.value === 'number' && !Number.isNaN(f.value)) - .map((f) => api.tx.oraclePriceFeed.feed({ Isin: f.Isin }, CurrencyBalance.fromFloat(f.value, 18))), - api.tx.oraclePriceCollection.updateCollection(poolId), - api.tx.loans.updatePortfolioValuation(poolId), - ] - if (values.closeEpoch) { - batch.push(api.tx.poolSystem.closeEpoch(poolId)) - } - const tx = api.tx.utility.batchAll(batch) - return cent.wrapSignAndSend(api, tx, options) - } - ) - - const initialValues = React.useMemo( - () => ({ - feed: - externalLoans?.map((l, i) => { - let latestOraclePrice = l.pricing.oracle[0] - l.pricing.oracle.forEach((price) => { - if (price.timestamp > latestOraclePrice.timestamp) { - latestOraclePrice = price - } - }) - return { - formIndex: i, - id: l.id, - oldValue: latestOraclePrice.value.toFloat(), - value: '' as const, - Isin: l.pricing.Isin, - quantity: l.pricing.outstandingQuantity.toFloat(), - maturity: formatDate(l.pricing.maturityDate), - } - }) ?? [], - closeEpoch: false, - }), - [externalLoans] - ) - - const form = useFormik({ - initialValues, - onSubmit(values, actions) { - execute([values]) - actions.setSubmitting(false) - }, - }) - - React.useEffect(() => { - if (isEditing && !isLoading) return - form.resetForm() - form.setValues(initialValues, false) - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [initialValues, isEditing]) - - const newNavExternal = form.values.feed.reduce((acc, cur) => acc + cur.quantity * (cur.value || cur.oldValue), 0) - const newNavCash = cashLoans.reduce((acc, cur) => acc + cur.outstandingDebt.toFloat(), 0) - const newNav = newNavExternal + newNavCash - - const columns = [ - { - align: 'left', - header: 'Asset', - cell: (row: Row) => - 'oldValue' in row ? ( - - - - {row.Isin} - - - ) : ( - - - - ), - }, - { - align: 'left', - header: 'Maturity date', - cell: (row: Row) => ('oldValue' in row ? row.maturity : ''), - }, - { - align: 'right', - header: 'Quantity', - cell: (row: Row) => formatBalance('oldValue' in row ? row.quantity : row.outstandingDebt), - }, - { - align: 'right', - header: 'Asset price', - cell: (row: Row) => formatBalance('oldValue' in row ? row.oldValue : 1, pool?.currency.symbol, 8), - }, - { - align: 'right', - header: 'New price', - cell: (row: Row) => - 'oldValue' in row - ? (console.log('row.formIndex', row.formIndex), - ( - - {({ field, meta, form }: FieldProps) => ( - form.setFieldValue(`feed.${row.formIndex}.value`, value)} - /> - )} - - )) - : '', - }, - { - align: 'right', - header: 'Value', - cell: (row: Row) => - 'oldValue' in row - ? formatBalance(row.quantity * (row.value || row.oldValue), pool?.currency.symbol) - : formatBalance(row.outstandingDebt, pool?.currency.symbol), - }, - ] +export const NavManagementPageSummary = ({ poolId }: { poolId: string }) => { + const pool = usePool(poolId) + const today = new Date() + today.setHours(0, 0, 0, 0) + const dailyPoolStates = useDailyPoolStates(poolId, new Date(pool.createdAt || today), today) + const investments = + pool && + dailyPoolStates?.poolStates?.reduce( + (acc, state) => + state && state?.sumInvestedAmountByPeriod ? acc.add(new BN(state.sumInvestedAmountByPeriod)) : new BN(0), + new BN(0) + ) - if (!isEditing) { - columns.splice(4, 1) - } + const redemptions = + pool && + dailyPoolStates?.poolStates?.reduce( + (acc, state) => + state && state?.sumRedeemedAmountByPeriod ? acc.add(new BN(state.sumRedeemedAmountByPeriod)) : new BN(0), + new BN(0) + ) return ( - - , - value: formatBalance(pool?.nav.latest ?? 0, pool?.currency.symbol), - }, - ]} - /> - setIsConfirming(false)}> - - - - - - - - - - ) : ( - - - - - ) - } - > - - - - Total - - - - - - {isEditing && } - - - {formatBalance(newNav, pool?.currency.symbol)} - - - - } - /> - - + , + value: formatBalance(pool?.nav.total ?? 0, pool?.currency.symbol, 2), + }, + { + label: 'Investments', + value: formatBalance( + new CurrencyBalance(investments ?? 0, pool?.currency.decimals || 18), + pool?.currency.symbol, + 2 + ), + }, + { + label: 'Redemptions', + value: formatBalance(new CurrencyBalance(redemptions ?? 0, pool.currency.decimals), pool?.currency.symbol, 2), + }, + ]} + /> ) } -function PoolName({ poolId }: { poolId: string }) { +export const NavOverviewCard = ({ poolId }: { poolId: string }) => { const pool = usePool(poolId) - const { data: metadata } = usePoolMetadata(pool) - return metadata?.pool?.name || poolId -} - -function usePoolFeeders() { - const api = useCentrifugeApi() - const [storedInfo] = useCentrifugeQuery(['oracleCollectionInfos'], () => - api.query.oraclePriceCollection.collectionInfo.entries().pipe( - map((data) => { - const poolsByFeeder: Record = {} - const feedersByPool: Record = {} - data.forEach(([keys, value]) => { - const poolId = (keys.toHuman() as string[])[0].replace(/\D/g, '') - const info = value.toPrimitive() as any - const feeders = info.feeders - .filter((f: any) => !!f.system.signed) - .map((f: any) => addressToHex(f.system.signed)) as string[] - - feeders.forEach((feeder) => { - if (poolsByFeeder[feeder]) { - poolsByFeeder[feeder].push(poolId) - } else { - poolsByFeeder[feeder] = [poolId] - } - }) - - feedersByPool[poolId] = { - valueLifetime: info.valueLifetime as number, - minFeeders: info.minFeeders as number, - feeders, - } - }) - - return { - poolsByFeeder, - feedersByPool, - } - }) + const today = new Date() + today.setHours(0, 0, 0, 0) + const { poolStates: dailyPoolStates } = + useDailyPoolStates(poolId, new Date(new Date(pool.createdAt || today)), today) || {} + + const pendingFees = React.useMemo(() => { + return new CurrencyBalance( + pool?.poolFees?.map((f) => f.amounts.pending).reduce((acc, f) => acc.add(f), new BN(0)) ?? new BN(0), + pool.currency.decimals ) + }, [pool.poolFees, pool.currency.decimals]) + + const changeInValuation = React.useMemo(() => { + const lastUpdated = pool?.nav.lastUpdated || new Date() + const lastUpdatedSumBorrowedAmountByPeriod = dailyPoolStates?.find( + (state) => state.timestamp >= lastUpdated + )?.sumBorrowedAmountByPeriod + const todaySumBorrowedAmountByPeriod = dailyPoolStates?.[0].sumBorrowedAmountByPeriod + return lastUpdatedSumBorrowedAmountByPeriod && todaySumBorrowedAmountByPeriod + ? new BN(todaySumBorrowedAmountByPeriod).sub(new BN(lastUpdatedSumBorrowedAmountByPeriod)) + : new BN(0) + }, [dailyPoolStates]) + + const pendingNav = React.useMemo(() => { + return dailyPoolStates && dailyPoolStates?.length + ? new BN(dailyPoolStates.reverse()[0].portfolioValuation).add(pool.reserve.total) + : new BN(0) + }, [dailyPoolStates, pool.reserve.total]) + return ( + + + + + Current NAV + + {formatBalance(pool?.nav.total, pool.currency.displayName, 2)} + + + + + Change in asset valuation + + + {formatBalance( + changeInValuation ? new CurrencyBalance(changeInValuation, pool.currency.decimals) : 0, + pool.currency.displayName, + 2 + )} + + + + + Pending fees + + + -{formatBalance(pendingFees, pool.currency.displayName, 2)} + + + + + + + + Pending NAV + + + + {formatBalance(new CurrencyBalance(pendingNav, pool.currency.decimals), pool.currency.displayName, 2)} + + + + ) - - return { - poolsByFeeder: storedInfo?.poolsByFeeder ?? {}, - feedersByPool: storedInfo?.feedersByPool ?? {}, - } -} - -export function usePoolsForWhichAccountIsFeeder(address?: string) { - const defaultAddress = useAddress('substrate') - address ??= defaultAddress - const { poolsByFeeder } = usePoolFeeders() - const poolIds = (address && isSubstrateAddress(address) && poolsByFeeder[address]) || [] - return usePools()?.filter((p) => poolIds.includes(p.id)) } diff --git a/centrifuge-app/src/utils/useCreatePoolFee.ts b/centrifuge-app/src/utils/useCreatePoolFee.ts index 0dc88bbb51..1611eb26bc 100644 --- a/centrifuge-app/src/utils/useCreatePoolFee.ts +++ b/centrifuge-app/src/utils/useCreatePoolFee.ts @@ -2,7 +2,7 @@ import Centrifuge, { CurrencyBalance, Perquintill, PoolMetadataInput, Rate } fro import { useCentrifuge, useCentrifugeConsts, useWallet } from '@centrifuge/centrifuge-react' import BN from 'bn.js' import * as React from 'react' -import { combineLatest, map, of, Subject, switchMap } from 'rxjs' +import { Subject, combineLatest, map, of, switchMap } from 'rxjs' import { config } from '../config' import { useCurrencies } from './useCurrencies' @@ -115,12 +115,11 @@ export function useCreatePoolFee(formValues: Pick + api.query.oraclePriceCollection.collectionInfo.entries().pipe( + map((data) => { + const poolsByFeeder: Record = {} + const feedersByPool: Record = {} + data.forEach(([keys, value]) => { + const poolId = (keys.toHuman() as string[])[0].replace(/\D/g, '') + const info = value.toPrimitive() as any + const feeders = info.feeders + .filter((f: any) => !!f.system.signed) + .map((f: any) => addressToHex(f.system.signed)) as string[] + + feeders.forEach((feeder) => { + if (poolsByFeeder[feeder]) { + poolsByFeeder[feeder].push(poolId) + } else { + poolsByFeeder[feeder] = [poolId] + } + }) + + feedersByPool[poolId] = { + valueLifetime: info.valueLifetime as number, + minFeeders: info.minFeeders as number, + feeders, + } + }) + + return { + poolsByFeeder, + feedersByPool, + } + }) + ) + ) + + return { + poolsByFeeder: storedInfo?.poolsByFeeder ?? {}, + feedersByPool: storedInfo?.feedersByPool ?? {}, + } +} + +export function usePoolsForWhichAccountIsFeeder(address?: string) { + const defaultAddress = useAddress('substrate') + address ??= defaultAddress + const { poolsByFeeder } = usePoolFeeders() + const poolIds = (address && isSubstrateAddress(address) && poolsByFeeder[address]) || [] + return usePools()?.filter((p) => poolIds.includes(p.id)) +} diff --git a/centrifuge-js/src/modules/pools.ts b/centrifuge-js/src/modules/pools.ts index 63d53e14b8..2980c20404 100644 --- a/centrifuge-js/src/modules/pools.ts +++ b/centrifuge-js/src/modules/pools.ts @@ -508,6 +508,7 @@ export type ActiveLoan = { outstandingPrincipal: CurrencyBalance outstandingInterest: CurrencyBalance presentValue: CurrencyBalance + currentPrice: CurrencyBalance // may not actually be set yet, this is what the price should be } // transformed type for UI @@ -890,7 +891,6 @@ export function getPoolsModule(inst: Centrifuge) { args: [ admin: string, poolId: string, - collectionId: string, tranches: TrancheInput[], currency: CurrencyKey, maxReserve: BN, @@ -899,7 +899,7 @@ export function getPoolsModule(inst: Centrifuge) { ], options?: TransactionOptions ) { - const [admin, poolId, , tranches, currency, maxReserve, metadata, fees] = args + const [admin, poolId, tranches, currency, maxReserve, metadata, fees] = args const trancheInput = tranches.map((t, i) => ({ trancheType: t.interestRatePerSec ? { @@ -3171,6 +3171,7 @@ export function getPoolsModule(inst: Centrifuge) { presentValue: CurrencyBalance outstandingPrincipal: CurrencyBalance outstandingInterest: CurrencyBalance + currentPrice: CurrencyBalance } > = {} @@ -3180,6 +3181,7 @@ export function getPoolsModule(inst: Centrifuge) { presentValue: new CurrencyBalance(data.presentValue, currency.decimals), outstandingPrincipal: new CurrencyBalance(data.outstandingPrincipal, currency.decimals), outstandingInterest: new CurrencyBalance(data.outstandingInterest, currency.decimals), + currentPrice: new CurrencyBalance(data.currentPrice ?? 0, currency.decimals), } }) @@ -3331,6 +3333,7 @@ export function getPoolsModule(inst: Centrifuge) { outstandingPrincipal: portfolio.outstandingPrincipal, outstandingInterest: portfolio.outstandingInterest, presentValue: portfolio.presentValue, + currentPrice: portfolio.currentPrice, } } ) From 9ea024998e9ba50ada685161f601335b6f0a89f8 Mon Sep 17 00:00:00 2001 From: Sophia Date: Thu, 16 May 2024 20:48:53 +0100 Subject: [PATCH 07/10] Add pure proxy to onboarding on prod (#2136) --- onboarding-api/env-vars/production.env | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/onboarding-api/env-vars/production.env b/onboarding-api/env-vars/production.env index 4b1dac63da..445e64e4de 100644 --- a/onboarding-api/env-vars/production.env +++ b/onboarding-api/env-vars/production.env @@ -1,5 +1,5 @@ REDIRECT_URL=https://app.centrifuge.io -MEMBERLIST_ADMIN_PURE_PROXY='' +MEMBERLIST_ADMIN_PURE_PROXY=4g9fEMyQQkFSRsNrvJTyiqEyrD42xZzWCxeEPoweXmbtAaPP COLLATOR_WSS_URL=wss://fullnode.parachain.centrifuge.io RELAY_WSS_URL=wss://rpc.polkadot.io INFURA_KEY=bf808e7d3d924fbeb74672d9341d0550 From 1fe41c7c8816fdedd97c5a2b1a20e3a5359e3c0b Mon Sep 17 00:00:00 2001 From: Onno Visser Date: Thu, 16 May 2024 21:54:31 +0200 Subject: [PATCH 08/10] fix transfer debt (#2134) Co-authored-by: Sophia --- centrifuge-app/src/pages/Loan/TransferDebtForm.tsx | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/centrifuge-app/src/pages/Loan/TransferDebtForm.tsx b/centrifuge-app/src/pages/Loan/TransferDebtForm.tsx index ce3e9fed4a..acf23acfb0 100644 --- a/centrifuge-app/src/pages/Loan/TransferDebtForm.tsx +++ b/centrifuge-app/src/pages/Loan/TransferDebtForm.tsx @@ -77,12 +77,12 @@ export function TransferDebtForm({ loan }: { loan: LoanType }) { borrowAmount = borrow.amount } - const outstandingPrincipal = selectedLoan.totalBorrowed.sub(selectedLoan.repaid.principal) - let principal: BN = new BN(borrowAmount) - let interest = new BN(0) - if (principal.gt(outstandingPrincipal)) { - interest = principal.sub(outstandingPrincipal) - principal = outstandingPrincipal + const { outstandingInterest } = selectedLoan + let interest = new BN(borrowAmount) + let principal = new BN(0) + if (interest.gt(outstandingInterest)) { + interest = outstandingInterest + principal = interest.sub(outstandingInterest) } let repay: any = { principal, interest } if (isExternalLoan(selectedLoan)) { From 28fb556335a1e64149ef32be7ad62cf70b1b4323 Mon Sep 17 00:00:00 2001 From: Onno Visser Date: Fri, 17 May 2024 11:10:23 +0200 Subject: [PATCH 09/10] Invest: disable native currency balance check (#2137) --- .../src/components/InvestRedeem/InvestForm.tsx | 11 +---------- 1 file changed, 1 insertion(+), 10 deletions(-) diff --git a/centrifuge-app/src/components/InvestRedeem/InvestForm.tsx b/centrifuge-app/src/components/InvestRedeem/InvestForm.tsx index ea0fb02c7b..71219243b9 100644 --- a/centrifuge-app/src/components/InvestRedeem/InvestForm.tsx +++ b/centrifuge-app/src/components/InvestRedeem/InvestForm.tsx @@ -85,8 +85,6 @@ export function InvestForm({ autoFocus, investLabel = 'Invest' }: InvestFormProp const formRef = React.useRef(null) useFocusInvalidInput(form, formRef) - const nativeBalanceTooLow = state.nativeBalance.eq(0) - const inputAmountCoveredByCapacity = inputToDecimal(form.values.amount).lessThanOrEqualTo(state.capacity ?? 0) const isPending = @@ -109,11 +107,6 @@ export function InvestForm({ autoFocus, investLabel = 'Invest' }: InvestFormProp {state.statusMessage && {state.statusMessage}} - {nativeBalanceTooLow && ( - - {state.nativeCurrency && `${state.nativeCurrency.symbol} balance is too low.`} - - )} {!state.collectType || claimDismissed ? ( <> @@ -203,9 +196,7 @@ export function InvestForm({ autoFocus, investLabel = 'Invest' }: InvestFormProp loading={isInvesting} loadingMessage={loadingMessage} disabled={ - state.isPoolBusy || - nativeBalanceTooLow || - (state.poolCurrency?.symbol.toLowerCase().includes('lp') && hasPendingOrder) + state.isPoolBusy || (state.poolCurrency?.symbol.toLowerCase().includes('lp') && hasPendingOrder) } > {investLabel} From 0fc10340e5088e258ea43b6659434c8b3ecd514f Mon Sep 17 00:00:00 2001 From: Onno Visser Date: Fri, 17 May 2024 16:19:51 +0200 Subject: [PATCH 10/10] Remove Prime debug flag (#2138) --- .../src/components/DebugFlags/config.ts | 6 ------ centrifuge-app/src/components/Menu/index.tsx | 16 +++++++--------- 2 files changed, 7 insertions(+), 15 deletions(-) diff --git a/centrifuge-app/src/components/DebugFlags/config.ts b/centrifuge-app/src/components/DebugFlags/config.ts index 774e5fd2c2..02c1e56420 100644 --- a/centrifuge-app/src/components/DebugFlags/config.ts +++ b/centrifuge-app/src/components/DebugFlags/config.ts @@ -48,7 +48,6 @@ export type Key = | 'convertAddress' | 'showTestNets' | 'showSwaps' - | 'showPrime' | 'showOracle' | 'poolCreationType' | 'podAdminSeed' @@ -134,11 +133,6 @@ export const flagsConfig: Record = { default: false, type: 'checkbox', }, - showPrime: { - alwaysShow: true, - default: false, - type: 'checkbox', - }, showSwaps: { default: false, type: 'checkbox', diff --git a/centrifuge-app/src/components/Menu/index.tsx b/centrifuge-app/src/components/Menu/index.tsx index 39464e9062..ed5de6620d 100644 --- a/centrifuge-app/src/components/Menu/index.tsx +++ b/centrifuge-app/src/components/Menu/index.tsx @@ -28,7 +28,7 @@ export function Menu() { const pools = usePoolsThatAnyConnectedAddressHasPermissionsFor() || [] const isLarge = useIsAboveBreakpoint('L') const address = useAddress('substrate') - const { showSwaps, showPrime, showOracle } = useDebugFlags() + const { showSwaps, showOracle } = useDebugFlags() const transactions = useTransactionsByAddress(address) return ( @@ -63,14 +63,12 @@ export function Menu() {
)} - {showPrime && ( - - - - Prime - - - )} + + + + Prime + +