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">
+
+
+
+
+ )}
)
}