diff --git a/centrifuge-app/.env-config/.env.development b/centrifuge-app/.env-config/.env.development index 72519c9784..8bf6e9b058 100644 --- a/centrifuge-app/.env-config/.env.development +++ b/centrifuge-app/.env-config/.env.development @@ -10,7 +10,7 @@ REACT_APP_PINNING_API_URL=https://europe-central2-peak-vista-185616.cloudfunctio REACT_APP_POOL_CREATION_TYPE=immediate REACT_APP_RELAY_WSS_URL=wss://fullnode-relay.development.cntrfg.com REACT_APP_SUBQUERY_URL=https://api.subquery.network/sq/centrifuge/pools-development -REACT_APP_SUBSCAN_URL= +REACT_APP_SUBSCAN_URL=https://centrifuge.subscan.io REACT_APP_TINLAKE_NETWORK=goerli REACT_APP_INFURA_KEY=bf808e7d3d924fbeb74672d9341d0550 REACT_APP_WHITELISTED_ACCOUNTS= diff --git a/centrifuge-app/src/components/Menu/index.tsx b/centrifuge-app/src/components/Menu/index.tsx index 63770079ce..4fd89bd618 100644 --- a/centrifuge-app/src/components/Menu/index.tsx +++ b/centrifuge-app/src/components/Menu/index.tsx @@ -1,5 +1,6 @@ import { Box, + IconClock, IconGlobe, IconInvestments, IconNft, @@ -61,6 +62,13 @@ export function Menu() { )} + {showPortfolio && address && ( + + + History + + )} + {(pools.length > 0 || config.poolCreationType === 'immediate') && ( {isLarge ? ( diff --git a/centrifuge-app/src/components/PoolList.tsx b/centrifuge-app/src/components/PoolList.tsx index 745282c5c5..2e2caa05a1 100644 --- a/centrifuge-app/src/components/PoolList.tsx +++ b/centrifuge-app/src/components/PoolList.tsx @@ -90,12 +90,12 @@ export function PoolList() { ? Array(6) .fill(true) .map((_, index) => ( - + )) : filteredPools.map((pool) => ( - + ))} diff --git a/centrifuge-app/src/components/Portfolio/AssetAllocation.tsx b/centrifuge-app/src/components/Portfolio/AssetAllocation.tsx index 69ec228512..f94d6e3b7e 100644 --- a/centrifuge-app/src/components/Portfolio/AssetAllocation.tsx +++ b/centrifuge-app/src/components/Portfolio/AssetAllocation.tsx @@ -57,7 +57,7 @@ export function AssetAllocation({ address }: { address: string }) { {shares.map((cell, i) => ( - <> + {i > 0 && } - + ))} diff --git a/centrifuge-app/src/components/Portfolio/InvestedTokens.tsx b/centrifuge-app/src/components/Portfolio/InvestedTokens.tsx index 3dc3f146a0..a028f3ddf5 100644 --- a/centrifuge-app/src/components/Portfolio/InvestedTokens.tsx +++ b/centrifuge-app/src/components/Portfolio/InvestedTokens.tsx @@ -1,30 +1,117 @@ -import { useAddress, useBalances } from '@centrifuge/centrifuge-react' -import { Box, Grid, Stack, Text } from '@centrifuge/fabric' -import { useMemo, useState } from 'react' +import { Token, TokenBalance } from '@centrifuge/centrifuge-js' +import { formatBalance, useAddress, useBalances, useCentrifuge } from '@centrifuge/centrifuge-react' +import { + AnchorButton, + Box, + Button, + Grid, + IconExternalLink, + IconMinus, + IconPlus, + Shelf, + Stack, + Text, + Thumbnail, +} from '@centrifuge/fabric' +import { useMemo } from 'react' +import { useTheme } from 'styled-components' +import { Dec } from '../../utils/Decimal' import { useTinlakeBalances } from '../../utils/tinlake/useTinlakeBalances' -import { useTinlakePools } from '../../utils/tinlake/useTinlakePools' -import { usePools } from '../../utils/usePools' -import { FilterButton } from '../FilterButton' -import { SortChevrons } from '../SortChevrons' -import { sortTokens } from './sortTokens' -import { TokenListItem } from './TokenListItem' +import { usePool, usePoolMetadata, usePools } from '../../utils/usePools' +import { Column, DataTable, SortableTableHeader } from '../DataTable' +import { Eththumbnail } from '../EthThumbnail' -export const COLUMN_GAPS = '200px 140px 140px 140px' - -export type SortOptions = { - sortBy: 'position' | 'market-value' - sortDirection: 'asc' | 'desc' +type Row = { + currency: Token['currency'] + poolId: string + trancheId: string + marketValue: TokenBalance + position: TokenBalance + tokenPrice: TokenBalance + canInvestRedeem: boolean } +const columns: Column[] = [ + { + align: 'left', + header: 'Token', + cell: (token: Row) => { + return + }, + width: '2fr', + }, + { + header: 'Token price', + cell: ({ tokenPrice }: Row) => { + return ( + + {formatBalance(tokenPrice.toDecimal() || 1, 'USDT', 4)} + + ) + }, + }, + { + header: , + cell: ({ currency, position }: Row) => { + return ( + + {formatBalance(position, currency.symbol)} + + ) + }, + sortKey: 'position', + }, + { + header: , + cell: ({ marketValue }: Row) => { + return ( + + {formatBalance(marketValue, 'USDT', 4)} + + ) + }, + sortKey: 'marketValue', + }, + { + align: 'left', + header: '', // invest redeem buttons + cell: ({ canInvestRedeem, poolId }: Row) => { + const isTinlakePool = poolId.startsWith('0x') + return ( + canInvestRedeem && ( + + {isTinlakePool ? ( + + View on Tinlake + + ) : ( + <> + + + + )} + + ) + ) + }, + }, +] + // TODO: change canInvestRedeem to default to true once the drawer is implemented export const InvestedTokens = ({ canInvestRedeem = false }) => { - const [sortOptions, setSortOptions] = useState({ sortBy: 'position', sortDirection: 'desc' }) - const address = useAddress() const centBalances = useBalances(address) const { data: tinlakeBalances } = useTinlakeBalances() - - const { data: tinlakePools } = useTinlakePools() const pools = usePools() const balances = useMemo(() => { @@ -34,62 +121,49 @@ export const InvestedTokens = ({ canInvestRedeem = false }) => { ] }, [centBalances, tinlakeBalances]) - const sortedTokens = - balances.length && pools && tinlakePools - ? sortTokens( - balances, - { - centPools: pools, - tinlakePools: tinlakePools.pools, - }, - sortOptions - ) - : [] - - const handleSort = (sortOption: SortOptions['sortBy']) => { - setSortOptions((prev) => ({ - sortBy: sortOption, - sortDirection: prev.sortBy !== sortOption ? 'desc' : prev.sortDirection === 'asc' ? 'desc' : 'asc', - })) - } + const tableData = balances.map((balance) => { + const pool = pools?.find((pool) => pool.id === balance.poolId) + const tranche = pool?.tranches.find((tranche) => tranche.id === balance.trancheId) + return { + currency: balance.currency, + poolId: balance.poolId, + trancheId: balance.trancheId, + position: balance.balance, + tokenPrice: tranche?.tokenPrice || Dec(1), + marketValue: tranche?.tokenPrice ? balance.balance.toDecimal().mul(tranche?.tokenPrice.toDecimal()) : Dec(0), + canInvestRedeem, + } + }) - return sortedTokens.length ? ( + return tableData.length ? ( Portfolio - - - - - Token - - - handleSort('position')}> - Position - - - - - Token price - - - handleSort('market-value')}> - Market Value - - - - - - {balances.map((balance, index) => ( - - ))} - - + ) : null } + +const TokenWithIcon = ({ poolId, currency }: Row) => { + const pool = usePool(poolId, false) + const { data: metadata } = usePoolMetadata(pool) + const cent = useCentrifuge() + const { sizes } = useTheme() + const icon = metadata?.pool?.icon?.uri ? cent.metadata.parseMetadataUrl(metadata.pool.icon.uri) : null + return ( + + + {icon ? ( + + ) : ( + + )} + + + + {currency.name} + + + ) +} diff --git a/centrifuge-app/src/components/Portfolio/TokenListItem.tsx b/centrifuge-app/src/components/Portfolio/TokenListItem.tsx deleted file mode 100644 index 2257e7beff..0000000000 --- a/centrifuge-app/src/components/Portfolio/TokenListItem.tsx +++ /dev/null @@ -1,100 +0,0 @@ -import { AccountTokenBalance } from '@centrifuge/centrifuge-js' -import { formatBalance, useCentrifuge } from '@centrifuge/centrifuge-react' -import { - AnchorButton, - Box, - Button, - Grid, - IconExternalLink, - IconMinus, - IconPlus, - Shelf, - Text, - Thumbnail, -} from '@centrifuge/fabric' -import styled, { useTheme } from 'styled-components' -import { usePool, usePoolMetadata } from '../../utils/usePools' -import { Eththumbnail } from '../EthThumbnail' -import { Root } from '../ListItemCardStyles' -import { COLUMN_GAPS } from './InvestedTokens' - -export type TokenCardProps = AccountTokenBalance & { - canInvestRedeem?: boolean -} - -const TokenName = styled(Text)` - text-wrap: nowrap; -` - -export function TokenListItem({ balance, currency, poolId, trancheId, canInvestRedeem }: TokenCardProps) { - const { sizes } = useTheme() - const pool = usePool(poolId, false) - const { data: metadata } = usePoolMetadata(pool) - const cent = useCentrifuge() - - const isTinlakePool = poolId.startsWith('0x') - - // @ts-expect-error known typescript issue: https://github.com/microsoft/TypeScript/issues/44373 - const trancheInfo = pool?.tranches.find(({ id }) => id === trancheId) - const icon = metadata?.pool?.icon?.uri ? cent.metadata.parseMetadataUrl(metadata.pool.icon.uri) : null - - return ( - - - - - {icon ? ( - - ) : ( - - )} - - - - {currency.name} - - - - - {formatBalance(balance, currency.symbol)} - - - - {trancheInfo?.tokenPrice - ? formatBalance(trancheInfo.tokenPrice.toDecimal(), trancheInfo.currency.symbol, 4) - : '-'} - - - - {trancheInfo?.tokenPrice - ? formatBalance(balance.toDecimal().mul(trancheInfo.tokenPrice.toDecimal()), trancheInfo.currency.symbol, 4) - : '-'} - - - {canInvestRedeem && ( - - {isTinlakePool ? ( - - View on Tinlake - - ) : ( - <> - - - - )} - - )} - - - ) -} diff --git a/centrifuge-app/src/components/Portfolio/TransactionTypeChip.tsx b/centrifuge-app/src/components/Portfolio/TransactionTypeChip.tsx index e54a3a775d..8f044b099a 100644 --- a/centrifuge-app/src/components/Portfolio/TransactionTypeChip.tsx +++ b/centrifuge-app/src/components/Portfolio/TransactionTypeChip.tsx @@ -1,28 +1,23 @@ +import { BorrowerTransactionType, InvestorTransactionType } from '@centrifuge/centrifuge-js' import { StatusChip, StatusChipProps } from '@centrifuge/fabric' import * as React from 'react' -import { TransactionCardProps } from './Transactions' type TransactionTypeProps = { - type: TransactionCardProps['action'] + type: InvestorTransactionType | BorrowerTransactionType } -// @ts-expect-error const states: { - [Key in TransactionCardProps['action']]: { + [Key in InvestorTransactionType | BorrowerTransactionType]: { label: string status: StatusChipProps['status'] } } = { - PENDING_ORDER: { - label: 'Pending order', - status: 'default', - }, INVEST_ORDER_UPDATE: { - label: 'Invest order update', + label: 'Invest order placed', status: 'default', }, REDEEM_ORDER_UPDATE: { - label: 'Redeem order update', + label: 'Redeem order placed', status: 'default', }, INVEST_ORDER_CANCEL: { @@ -34,12 +29,12 @@ const states: { status: 'default', }, INVEST_EXECUTION: { - label: 'Invest execution', - status: 'default', + label: 'Invest executed', + status: 'ok', }, REDEEM_EXECUTION: { - label: 'Redeem execution', - status: 'default', + label: 'Redeem executed', + status: 'info', }, TRANSFER_IN: { label: 'Transfer in', @@ -73,6 +68,10 @@ const states: { label: 'Closed', status: 'default', }, + PRICED: { + label: 'Priced', + status: 'default', + }, } export function TransactionTypeChip({ type }: TransactionTypeProps) { diff --git a/centrifuge-app/src/components/Portfolio/Transactions.tsx b/centrifuge-app/src/components/Portfolio/Transactions.tsx index bde88a195c..972d9d7a88 100644 --- a/centrifuge-app/src/components/Portfolio/Transactions.tsx +++ b/centrifuge-app/src/components/Portfolio/Transactions.tsx @@ -1,195 +1,204 @@ -import { - BorrowerTransactionType, - CurrencyBalance, - InvestorTransactionType, - Pool, - SubqueryInvestorTransaction, -} from '@centrifuge/centrifuge-js' +import { BorrowerTransactionType, InvestorTransactionType, Token, TokenBalance } from '@centrifuge/centrifuge-js' import { formatBalance, useCentrifugeUtils } from '@centrifuge/centrifuge-react' -import { Box, Grid, IconExternalLink, Stack, Text } from '@centrifuge/fabric' +import { + AnchorButton, + Box, + IconExternalLink, + IconEye, + Pagination, + PaginationProvider, + Shelf, + Stack, + Text, + usePagination, + VisualButton, +} from '@centrifuge/fabric' import { isAddress as isValidEVMAddress } from '@ethersproject/address' import * as React from 'react' import { Link } from 'react-router-dom' +import { TransactionTypeChip } from '../../components/Portfolio/TransactionTypeChip' +import { Spinner } from '../../components/Spinner' import { formatDate } from '../../utils/date' +import { Dec } from '../../utils/Decimal' +import { getCSVDownloadUrl } from '../../utils/getCSVDownloadUrl' import { useAddress } from '../../utils/useAddress' -import { useAllTransactions, usePool, usePoolMetadata } from '../../utils/usePools' -import { TransactionTypeChip } from './TransactionTypeChip' +import { usePools, useTransactionsByAddress } from '../../utils/usePools' +import { Column, DataTable, SortableTableHeader } from '../DataTable' -export const TRANSACTION_CARD_COLUMNS = `150px 100px 250px 150px 1fr` -export const TRANSACTION_CARD_GAP = 4 - -type AddressTransactionsProps = { - count?: number -} - -type SubqueryBorrowerTransaction = any -type SubqueryOutstandingOrder = any - -const formatters = { - investorTransactions: ({ - timestamp, - type, - poolId, - hash, - tokenAmount, - tokenPrice, - currencyAmount, - trancheId, - }: Omit) => { - return { - date: new Date(timestamp).getTime(), - action: type, - amount: tokenAmount, - poolId, - hash, - trancheId, - } as TransactionCardProps - }, - borrowerTransactions: ({ timestamp, type, amount, poolId, hash }: SubqueryBorrowerTransaction) => - ({ - date: new Date(timestamp).getTime(), - action: type, - amount, - poolId, - hash, - } as TransactionCardProps), - outstandingOrders: ({ timestamp, investAmount, redeemAmount, poolId, hash, trancheId }: SubqueryOutstandingOrder) => - ({ - date: new Date(timestamp).getTime(), - action: 'PENDING_ORDER', - amount: investAmount.add(redeemAmount), - poolId, - hash, - trancheId, - } as TransactionCardProps), +type TransactionsProps = { + onlyMostRecent?: boolean + txTypes?: InvestorTransactionType[] } -export function Transactions({ count }: AddressTransactionsProps) { - const { formatAddress } = useCentrifugeUtils() - const address = useAddress() - const formattedAddress = address && isValidEVMAddress(address) ? address : formatAddress(address || '') - const allTransactions = useAllTransactions(formattedAddress) - const formattedTransactions: TransactionCardProps[] = [] - - if (allTransactions) { - const { borrowerTransactions, investorTransactions, outstandingOrders } = allTransactions - - investorTransactions.forEach((transaction) => - formattedTransactions.push(formatters.investorTransactions(transaction)) - ) - borrowerTransactions.forEach((transaction) => - formattedTransactions.push(formatters.borrowerTransactions(transaction)) - ) - outstandingOrders.forEach((transaction) => formattedTransactions.push(formatters.outstandingOrders(transaction))) - } - - const transactions = formattedTransactions.slice(0, count ?? formattedTransactions.length) +type TransactionTableData = Row[] - return !!transactions.length ? ( - - - Transaction history - - - - Action - - - Transaction date - - - Token - - - - Amount - - - - - - {transactions.map((transaction, index) => ( - - - - ))} - - - View all - - ) : null -} - -export type TransactionCardProps = { +type Row = { + action: InvestorTransactionType | BorrowerTransactionType date: number - action: InvestorTransactionType | BorrowerTransactionType | 'PENDING_ORDER' - amount: CurrencyBalance - poolId: string + tranche: Token | undefined + tranchePrice: string + amount: TokenBalance hash: string - trancheId?: string + poolId: string + trancheId: string } -export function TransactionListItem({ date, action, amount, poolId, hash, trancheId }: TransactionCardProps) { - const pool = usePool(poolId) as Pool - const { data } = usePoolMetadata(pool) - const token = trancheId ? pool.tranches.find(({ id }) => id === trancheId) : undefined - const subScanUrl = import.meta.env.REACT_APP_SUBSCAN_URL - - if (!pool || !data) { - return null - } - - return ( - - - - - - +const columns: Column[] = [ + { + align: 'left', + header: 'Action', + cell: ({ action }: Row) => , + }, + { + align: 'left', + header: , + cell: ({ date }: Row) => ( + {formatDate(date, { day: '2-digit', month: '2-digit', year: '2-digit', })} - - - - {!!token ? token.currency?.name : data.pool?.name} - - {!!token && ( - - {data?.pool?.name} - - )} - - - - - {formatBalance(amount, pool.currency.symbol)} - - - - {!!subScanUrl && !!hash && ( - ( + + {tranche?.currency.symbol} - ({tranche?.currency.name}) + + ), + }, + { + align: 'right', + header: 'Token price', + cell: ({ tranche }: Row) => ( + + {formatBalance(tranche?.tokenPrice?.toDecimal() || Dec(1), tranche?.currency.symbol, 4)} + + ), + }, + { + align: 'right', + header: , + cell: ({ amount, tranche }: Row) => ( + + {formatBalance(amount.toDecimal(), tranche?.currency.symbol || '')} + + ), + sortKey: 'amount', + }, + { + align: 'center', + header: 'View transaction', + cell: ({ hash }: Row) => { + return ( + - - )} - + + ) + }, + }, +] + +export function Transactions({ onlyMostRecent, txTypes }: TransactionsProps) { + const { formatAddress } = useCentrifugeUtils() + const address = useAddress() + const formattedAddress = address && isValidEVMAddress(address) ? address : formatAddress(address || '') + const transactions = useTransactionsByAddress(formatAddress(formattedAddress)) + const pools = usePools() + + const investorTransactions: TransactionTableData = React.useMemo(() => { + const txs = + transactions?.investorTransactions + .slice(0, onlyMostRecent ? 3 : transactions?.investorTransactions.length) + .filter((tx) => (txTypes ? txTypes?.includes(tx.type) : tx)) + .map((tx) => { + const pool = pools?.find((pool) => pool.id === tx.poolId) + const tranche = pool?.tranches.find((tranche) => tranche.id === tx.trancheId) + return { + date: new Date(tx.timestamp).getTime(), + action: tx.type, + tranche, + tranchePrice: tranche?.tokenPrice?.toDecimal().toString() || '', + amount: tx.currencyAmount, + hash: tx.hash, + poolId: tx.poolId, + trancheId: tx.trancheId, + } + }) || [] + return txs + }, [transactions?.investorTransactions, onlyMostRecent, txTypes, pools]) + + const csvData = React.useMemo(() => { + if (!investorTransactions || !investorTransactions?.length) { + return undefined + } + return investorTransactions.map((entry) => { + const pool = pools?.find((pool) => pool.id === entry.poolId) + return { + 'Transaction date': `"${formatDate(entry.date)}"`, + Action: entry.action, + Token: (pool && pool.tranches.find(({ id }) => id === entry.trancheId)?.currency.name) ?? '', + Amount: (pool && `"${formatBalance(entry.amount.toDecimal(), pool.currency.symbol)}"`) ?? '', + } + }) + }, [investorTransactions, pools]) + + const csvUrl = React.useMemo(() => csvData && getCSVDownloadUrl(csvData), [csvData]) + + const pagination = usePagination({ data: investorTransactions, pageSize: onlyMostRecent ? 3 : 15 }) + + return !!investorTransactions.length ? ( + + + Transaction history + + + + + {onlyMostRecent ? ( + + + + View all + + + + ) : ( + + {pagination.pageCount > 1 && ( + + + + )} + {csvUrl && ( + + + Export as CSV + + + )} + + )} + + + + ) : ( + ) } diff --git a/centrifuge-app/src/components/Portfolio/sortTokens.ts b/centrifuge-app/src/components/Portfolio/sortTokens.ts deleted file mode 100644 index ed12057e09..0000000000 --- a/centrifuge-app/src/components/Portfolio/sortTokens.ts +++ /dev/null @@ -1,50 +0,0 @@ -import { Pool } from '@centrifuge/centrifuge-js' -import { TinlakePool } from '../../utils/tinlake/useTinlakePools' -import { SortOptions } from './InvestedTokens' -import { TokenCardProps } from './TokenListItem' - -export const sortTokens = ( - tokens: TokenCardProps[], - pools: { - centPools: Pool[] - tinlakePools: TinlakePool[] - }, - sortOptions: SortOptions -) => { - const { sortBy, sortDirection } = sortOptions - if (sortBy === 'market-value') { - tokens.sort((trancheA, trancheB) => { - const valueA = sortMarketValue(trancheA, pools) - const valueB = sortMarketValue(trancheB, pools) - - return sortDirection === 'asc' ? valueA - valueB : valueB - valueA - }) - } - - if (sortBy === 'position' || (!sortDirection && !sortBy)) { - tokens.sort(({ balance: balanceA }, { balance: balanceB }) => - sortDirection === 'asc' - ? balanceA.toDecimal().toNumber() - balanceB.toDecimal().toNumber() - : balanceB.toDecimal().toNumber() - balanceA.toDecimal().toNumber() - ) - } - - return tokens -} - -const sortMarketValue = ( - token: TokenCardProps, - pools: { - centPools: Pool[] - tinlakePools: TinlakePool[] - } -) => { - const pool = token.poolId.startsWith('0x') - ? pools.tinlakePools?.find((p) => p.id.toLowerCase() === token.poolId.toLowerCase()) - : pools.centPools?.find((p) => p.id === token.poolId) - - // @ts-expect-error known typescript issue: https://github.com/microsoft/TypeScript/issues/44373 - const poolTranche = pool?.tranches.find(({ id }) => id === token.trancheId) - - return poolTranche?.tokenPrice ? token.balance.toDecimal().mul(poolTranche.tokenPrice.toDecimal()).toNumber() : 0 -} diff --git a/centrifuge-app/src/components/Root.tsx b/centrifuge-app/src/components/Root.tsx index 8471417875..41736f4bd8 100644 --- a/centrifuge-app/src/components/Root.tsx +++ b/centrifuge-app/src/components/Root.tsx @@ -167,7 +167,7 @@ const EmailVerified = React.lazy(() => import('../pages/Onboarding/EmailVerified const UpdateInvestorStatus = React.lazy(() => import('../pages/Onboarding/UpdateInvestorStatus')) const PoolDetailPage = React.lazy(() => import('../pages/Pool')) const PortfolioPage = React.lazy(() => import('../pages/Portfolio')) -const TransactionsPage = React.lazy(() => import('../pages/Portfolio/Transactions')) +const TransactionHistoryPage = React.lazy(() => import('../pages/Portfolio/TransactionHistory')) const TokenOverviewPage = React.lazy(() => import('../pages/Tokens')) const PrimePage = React.lazy(() => import('../pages/Prime')) const PrimeDetailPage = React.lazy(() => import('../pages/Prime/Detail')) @@ -214,8 +214,8 @@ function Routes() { - - + + diff --git a/centrifuge-app/src/pages/Portfolio/TransactionHistory.tsx b/centrifuge-app/src/pages/Portfolio/TransactionHistory.tsx new file mode 100644 index 0000000000..5247028d2f --- /dev/null +++ b/centrifuge-app/src/pages/Portfolio/TransactionHistory.tsx @@ -0,0 +1,14 @@ +import * as React from 'react' +import { LayoutBase } from '../../components/LayoutBase' +import { BasePadding } from '../../components/LayoutBase/BasePadding' +import { Transactions } from '../../components/Portfolio/Transactions' + +export default function TransactionHistoryPage() { + return ( + + + + + + ) +} diff --git a/centrifuge-app/src/pages/Portfolio/Transactions/index.tsx b/centrifuge-app/src/pages/Portfolio/Transactions/index.tsx deleted file mode 100644 index 6fac67eedc..0000000000 --- a/centrifuge-app/src/pages/Portfolio/Transactions/index.tsx +++ /dev/null @@ -1,26 +0,0 @@ -import { Box, Stack, Text } from '@centrifuge/fabric' -import * as React from 'react' -import { LayoutBase } from '../../../components/LayoutBase' -import { Transactions } from '../../../components/Portfolio/Transactions' -import { useAddress } from '../../../utils/useAddress' - -export default function TransactionsPage() { - const address = useAddress() - return ( - - - - - Transaction history - - - - {!!address ? ( - - ) : ( - You need to connect your wallet to see your transactions - )} - - - ) -} diff --git a/centrifuge-app/src/pages/Portfolio/index.tsx b/centrifuge-app/src/pages/Portfolio/index.tsx index 83ad7fc892..1c9f49e0b8 100644 --- a/centrifuge-app/src/pages/Portfolio/index.tsx +++ b/centrifuge-app/src/pages/Portfolio/index.tsx @@ -19,7 +19,7 @@ export default function PortfolioPage() { } function Portfolio() { - const address = useAddress() + const address = useAddress('substrate') const theme = useTheme() if (!address) { @@ -50,9 +50,9 @@ function Portfolio() { - + - + diff --git a/centrifuge-app/src/utils/usePools.ts b/centrifuge-app/src/utils/usePools.ts index 3761f24595..83c94782ce 100644 --- a/centrifuge-app/src/utils/usePools.ts +++ b/centrifuge-app/src/utils/usePools.ts @@ -52,10 +52,10 @@ export function useMonthlyPoolStates(poolId: string, from?: Date, to?: Date) { return result } -export function useAllTransactions(address?: string) { +export function useTransactionsByAddress(address?: string) { const [result] = useCentrifugeQuery( - ['all transactions by address', address], - (cent) => cent.pools.getAllTransactions([address!]), + ['txByAddress', address], + (cent) => cent.pools.getTransactionsByAddress([address!]), { enabled: !!address, } diff --git a/centrifuge-js/src/modules/pools.ts b/centrifuge-js/src/modules/pools.ts index 3b539108b2..8a277054c1 100644 --- a/centrifuge-js/src/modules/pools.ts +++ b/centrifuge-js/src/modules/pools.ts @@ -1,8 +1,8 @@ import { StorageKey, u32 } from '@polkadot/types' import { Codec } from '@polkadot/types-codec/types' import BN from 'bn.js' -import { combineLatest, EMPTY, expand, firstValueFrom, forkJoin, from, Observable, of, startWith } from 'rxjs' -import { combineLatestWith, filter, map, mergeMap, repeatWhen, switchMap, take, toArray } from 'rxjs/operators' +import { combineLatest, EMPTY, expand, firstValueFrom, from, Observable, of, startWith } from 'rxjs' +import { combineLatestWith, filter, map, repeatWhen, switchMap, take } from 'rxjs/operators' import { calculateOptimalSolution, SolverResult } from '..' import { Centrifuge } from '../Centrifuge' import { Account, TransactionOptions } from '../types' @@ -2003,51 +2003,17 @@ export function getPoolsModule(inst: Centrifuge) { ) } - function getAllTransactions(args: [address: string]) { + function getTransactionsByAddress(args: [address: string, count?: number, txTypes?: InvestorTransactionType[]]) { const [address] = args const $query = inst.getSubqueryObservable<{ - investorTransactions: { - nodes: { - timestamp: string - type: InvestorTransactionType - poolId: string - trancheId: string - hash: string - tokenAmount: string - tokenPrice: string - currencyAmount: string - }[] - } - borrowerTransactions: { - nodes: { - timestamp: string - type: BorrowerTransactionType - poolId: string - amount: string - hash: string - }[] - } - outstandingOrders: { - nodes: { - timestamp: string - poolId: string - trancheId: string - hash: string - redeemAmount: string - investAmount: string - tranche: { - tokenPrice: string - } - }[] - } + investorTransactions: { nodes: SubqueryInvestorTransaction[] } }>( - `query($address: String!) { - investorTransactions(filter: { - accountId: { - equalTo: $address - } - }) { + `query ($address: String) { + investorTransactions( + filter: {accountId: {equalTo: $address}} + orderBy: TIMESTAMP_DESC + ) { nodes { timestamp type @@ -2059,38 +2025,6 @@ export function getPoolsModule(inst: Centrifuge) { currencyAmount } } - - borrowerTransactions(filter: { - accountId: { - equalTo: $address - } - }) { - nodes { - timestamp - type - poolId - hash - amount - } - } - - outstandingOrders(filter: { - accountId: { - equalTo: $address - } - }) { - nodes { - timestamp - redeemAmount - investAmount - poolId - trancheId - hash - tranche { - tokenPrice - } - } - } } `, { @@ -2099,56 +2033,24 @@ export function getPoolsModule(inst: Centrifuge) { ) return $query.pipe( - mergeMap((data) => { - const investorTransactions$ = from(data?.investorTransactions.nodes || []).pipe( - mergeMap((entry) => { - return getPoolCurrency([entry.poolId]).pipe( - map((poolCurrency) => ({ - ...entry, - tokenAmount: new CurrencyBalance(entry.tokenAmount || 0, poolCurrency.decimals), - tokenPrice: new Price(entry.tokenPrice || 0), - currencyAmount: new CurrencyBalance(entry.currencyAmount || 0, poolCurrency.decimals), - trancheId: entry.trancheId.split('-')[1], - })) - ) - }), - toArray() - ) - - const borrowerTransactions$ = from(data?.borrowerTransactions.nodes || []).pipe( - mergeMap((entry) => { - return getPoolCurrency([entry.poolId]).pipe( - map((poolCurrency) => ({ - ...entry, - amount: new CurrencyBalance(entry.amount || 0, poolCurrency.decimals), - })) - ) - }), - toArray() - ) - - const outstandingOrders$ = from(data?.outstandingOrders.nodes || []).pipe( - mergeMap((entry) => { - return getPoolCurrency([entry.poolId]).pipe( - map((poolCurrency) => { - return { - ...entry, - investAmount: new CurrencyBalance(entry.investAmount || 0, poolCurrency.decimals), - redeemAmount: new CurrencyBalance(entry.redeemAmount || 0, poolCurrency.decimals), - trancheId: entry.trancheId.split('-')[1], - } - }) - ) - }), - toArray() - ) - - return forkJoin([investorTransactions$, borrowerTransactions$, outstandingOrders$]).pipe( - map(([investorTransactions, borrowerTransactions, outstandingOrders]) => { + switchMap((data) => { + const poolIds = new Set(data?.investorTransactions.nodes.map((e) => e.poolId)) ?? [] + const $poolCurrencies = Array.from(poolIds).map((poolId) => getPoolCurrency([poolId])) + return combineLatest($poolCurrencies).pipe( + map((currencies) => { + const txs = data?.investorTransactions.nodes.map((tx) => { + const currencyIndex = Array.from(poolIds).indexOf(tx.poolId) + const poolCurrency = currencies[currencyIndex] + return { + ...tx, + tokenAmount: new TokenBalance(tx.tokenAmount || 0, poolCurrency.decimals), + tokenPrice: new Price(tx.tokenPrice || 0), + currencyAmount: new CurrencyBalance(tx.currencyAmount || 0, poolCurrency.decimals), + trancheId: tx.trancheId.split('-')[1], + } + }) return { - investorTransactions, - borrowerTransactions, - outstandingOrders, + investorTransactions: txs || [], } }) ) @@ -2903,7 +2805,7 @@ export function getPoolsModule(inst: Centrifuge) { const update = updateData.toPrimitive() as any if (!update?.changes) return null const { changes, submittedAt } = update - + return { changes: { tranches: changes.tranches.noChange === null ? null : changes.tranches.newValue, @@ -3019,7 +2921,7 @@ export function getPoolsModule(inst: Centrifuge) { getNativeCurrency, getCurrencies, getDailyTrancheStates, - getAllTransactions, + getTransactionsByAddress, getDailyTVL, } } diff --git a/centrifuge-js/src/types/subquery.ts b/centrifuge-js/src/types/subquery.ts index 9015048c0b..7a340cf1cd 100644 --- a/centrifuge-js/src/types/subquery.ts +++ b/centrifuge-js/src/types/subquery.ts @@ -84,6 +84,18 @@ export type SubqueryBorrowerTransaction = { quantity: string | null } +export type SubqueryOutstandingOrder = { + timestamp: string + poolId: string + trancheId: string // poolId-trancheId + hash: string + redeemAmount: string + investAmount: string + tranche: { + tokenPrice: string + } +} + export type SubqueryEpoch = { id: string poolId: string diff --git a/fabric/src/components/Pagination/Pagination.tsx b/fabric/src/components/Pagination/Pagination.tsx index 59d14bb94f..fdbd35eb92 100644 --- a/fabric/src/components/Pagination/Pagination.tsx +++ b/fabric/src/components/Pagination/Pagination.tsx @@ -84,16 +84,13 @@ export function Pagination({ pagination }: { pagination?: PaginationState }) { return ( - goToFirst()} - disabled={!canPreviousPage} - aria-label="first page" - style={{ visibility: firstShown > 1 ? 'visible' : 'hidden' }} - > - - - - + {firstShown > 1 && ( + goToFirst()} disabled={!canPreviousPage} aria-label="first page"> + + + + + )} goToPrevious()} disabled={!canPreviousPage} aria-label="previous page"> @@ -105,7 +102,12 @@ export function Pagination({ pagination }: { pagination?: PaginationState }) { )} {pages.map((n) => ( - goToPage(n)} $active={page === n} aria-label={`Go to page ${n}`}> + goToPage(n)} + $active={page === n} + aria-label={`Go to page ${n}`} + > {n} @@ -121,16 +123,13 @@ export function Pagination({ pagination }: { pagination?: PaginationState }) { - goToLast()} - disabled={!canNextPage} - aria-label="last page" - style={{ visibility: lastShown < pageCount ? 'visible' : 'hidden' }} - > - - - - + {lastShown < pageCount && ( + goToLast()} disabled={!canNextPage} aria-label="last page"> + + + + + )} ) }