From badf937c0d1ead8ea9a9404e85d52787f090167b Mon Sep 17 00:00:00 2001 From: JP Date: Thu, 22 Feb 2024 11:27:27 -0600 Subject: [PATCH] feat: transaction history table (#1973) * feat: transaction history table * wire up download button * use prod env temporarily * fix amount formatting * use asset name, link asset, right align --- centrifuge-app/.env-config/.env.development | 30 +-- .../PoolOverview/TransactionHistory.tsx | 188 +++++++++++++++++- .../components/Report/AssetTransactions.tsx | 6 +- centrifuge-app/src/components/Root.tsx | 6 +- .../src/pages/Loan/PricingValues.tsx | 2 +- .../src/pages/Pool/Overview/index.tsx | 6 +- centrifuge-app/src/pages/PoolTransactions.tsx | 29 +++ centrifuge-app/src/utils/usePools.ts | 2 +- centrifuge-js/src/modules/pools.ts | 56 ++++-- centrifuge-js/src/types/subquery.ts | 9 +- 10 files changed, 282 insertions(+), 52 deletions(-) create mode 100644 centrifuge-app/src/pages/PoolTransactions.tsx diff --git a/centrifuge-app/.env-config/.env.development b/centrifuge-app/.env-config/.env.development index 57881c1a10..d96792a54c 100644 --- a/centrifuge-app/.env-config/.env.development +++ b/centrifuge-app/.env-config/.env.development @@ -1,21 +1,21 @@ -REACT_APP_COLLATOR_WSS_URL=wss://fullnode.development.cntrfg.com -REACT_APP_DEFAULT_NODE_URL=https://pod-development.k-f.dev +REACT_APP_COLLATOR_WSS_URL=wss://fullnode.parachain.centrifuge.io +REACT_APP_DEFAULT_NODE_URL='' REACT_APP_DEFAULT_UNLIST_POOLS=false -REACT_APP_FAUCET_URL=https://europe-central2-peak-vista-185616.cloudfunctions.net/faucet-api-dev +REACT_APP_FAUCET_URL= REACT_APP_IPFS_GATEWAY=https://centrifuge.mypinata.cloud/ -REACT_APP_IS_DEMO=false +REACT_APP_IS_DEMO= REACT_APP_NETWORK=centrifuge -REACT_APP_ONBOARDING_API_URL=https://europe-central2-peak-vista-185616.cloudfunctions.net/onboarding-api-dev -REACT_APP_PINNING_API_URL=https://europe-central2-peak-vista-185616.cloudfunctions.net/pinning-api-dev -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_ONBOARDING_API_URL=https://europe-central2-centrifuge-production-x.cloudfunctions.net/onboarding-api-production +REACT_APP_PINNING_API_URL=https://europe-central2-centrifuge-production-x.cloudfunctions.net/pinning-api-production +REACT_APP_POOL_CREATION_TYPE=propose +REACT_APP_RELAY_WSS_URL=wss://rpc.polkadot.io +REACT_APP_SUBQUERY_URL=https://api.subquery.network/sq/centrifuge/pools REACT_APP_SUBSCAN_URL=https://centrifuge.subscan.io -REACT_APP_TINLAKE_NETWORK=goerli +REACT_APP_TINLAKE_NETWORK=mainnet REACT_APP_INFURA_KEY=bf808e7d3d924fbeb74672d9341d0550 -REACT_APP_ONFINALITY_KEY=0e1c049f-d876-4e77-a45f-b5afdf5739b2 -REACT_APP_WHITELISTED_ACCOUNTS= -REACT_APP_TINLAKE_SUBGRAPH_URL=https://api.goldsky.com/api/public/project_clhi43ef5g4rw49zwftsvd2ks/subgraphs/main/prod/gn -REACT_APP_REWARDS_TREE_URL=https://storage.googleapis.com/rad-rewards-trees-kovan-staging/latest.json +REACT_APP_ONFINALITY_KEY=7e8caebc-b052-402d-87a4-e990b67ed612 +REACT_APP_WHITELISTED_ACCOUNTS='' +REACT_APP_REWARDS_TREE_URL=https://storage.googleapis.com/rad-rewards-trees-mainnet-production/latest.json +REACT_APP_MEMBERLIST_ADMIN_PURE_PROXY=kALJqPUHFzDR2VkoQYWefPQyzjGzKznNny2smXGQpSf3aMw19 REACT_APP_WALLETCONNECT_ID=c32fa79350803519804a67fcab0b742a -REACT_APP_MEMBERLIST_ADMIN_PURE_PROXY=kAJ27w29x7gHM75xajP2yXVLjVBaKmmUTxHwgRuCoAcWaoEiz +REACT_APP_TINLAKE_SUBGRAPH_URL=https://api.goldsky.com/api/public/project_clhi43ef5g4rw49zwftsvd2ks/subgraphs/main/prod/gn diff --git a/centrifuge-app/src/components/PoolOverview/TransactionHistory.tsx b/centrifuge-app/src/components/PoolOverview/TransactionHistory.tsx index d5032b88f4..6db48b71dc 100644 --- a/centrifuge-app/src/components/PoolOverview/TransactionHistory.tsx +++ b/centrifuge-app/src/components/PoolOverview/TransactionHistory.tsx @@ -1,9 +1,187 @@ -import { Card } from '@centrifuge/fabric' +import { AssetTransaction, CurrencyBalance } from '@centrifuge/centrifuge-js' +import { AnchorButton, IconDownload, IconExternalLink, Shelf, Stack, StatusChip, Text } from '@centrifuge/fabric' +import BN from 'bn.js' +import { nftMetadataSchema } from '../../schemas' +import { formatDate } from '../../utils/date' +import { formatBalance } from '../../utils/formatting' +import { getCSVDownloadUrl } from '../../utils/getCSVDownloadUrl' +import { useMetadataMulti } from '../../utils/useMetadata' +import { useAssetTransactions } from '../../utils/usePools' +import { DataTable, SortableTableHeader } from '../DataTable' +import { AnchorTextLink } from '../TextLink' + +type Row = { + type: string + transactionDate: string + assetId: string + amount: CurrencyBalance | undefined + hash: string + assetName: string +} + +const getTransactionTypeStatus = (type: string) => { + if (type === 'Principal payment' || type === 'Repaid') return 'warning' + if (type === 'Interest') return 'ok' + return 'default' +} + +export const columns = [ + { + align: 'left', + header: 'Type', + cell: ({ type }: Row) => {type}, + }, + { + align: 'left', + header: , + cell: ({ transactionDate }: Row) => ( + + {formatDate(transactionDate)} + + ), + sortKey: 'transactionDate', + }, + { + align: 'left', + header: 'Asset name', + cell: ({ assetId, assetName }: Row) => { + const [poolId, id] = assetId.split('-') + return ( + + {assetName} + + ) + }, + }, + { + align: 'right', + header: , + cell: ({ amount }: Row) => ( + + {amount ? formatBalance(amount, 'USD', 2, 2) : ''} + + ), + sortKey: 'amount', + }, + { + align: 'right', + header: 'View transaction', + cell: ({ hash }: Row) => { + return ( + + + + ) + }, + }, +] + +export const TransactionHistory = ({ poolId, preview = true }: { poolId: string; preview?: boolean }) => { + const transactions = useAssetTransactions(poolId, new Date(0)) + + const assetMetadata = useMetadataMulti( + [...new Set(transactions?.map((transaction) => transaction.asset.metadata))] || [], + nftMetadataSchema + ) + + const getLabelAndAmount = (transaction: AssetTransaction) => { + if (transaction.type === 'BORROWED') { + return { + label: 'Purchase', + amount: transaction.amount, + } + } + if (transaction.type === 'REPAID' && !new BN(transaction.interestAmount || 0).isZero()) { + return { + label: 'Interest', + amount: transaction.interestAmount, + } + } + + return { + label: 'Principal payment', + amount: transaction.principalAmount, + } + } + + const csvData = transactions + ?.filter( + (transaction) => transaction.type !== 'CREATED' && transaction.type !== 'CLOSED' && transaction.type !== 'PRICED' + ) + .map((transaction) => { + const { label, amount } = getLabelAndAmount(transaction) + const [, id] = transaction.asset.id.split('-') + return { + Type: label, + 'Transaction Date': `"${formatDate(transaction.timestamp, { + year: 'numeric', + month: 'long', + day: 'numeric', + hour: 'numeric', + minute: 'numeric', + second: 'numeric', + timeZoneName: 'short', + })}"`, + 'Asset Name': assetMetadata[Number(id) - 1]?.data?.name || '-', + Amount: amount ? `"${formatBalance(amount, 'USD', 2, 2)}"` : '-', + Transaction: `${import.meta.env.REACT_APP_SUBSCAN_URL}/extrinsic/${transaction.hash}`, + } + }) + + const csvUrl = csvData?.length ? getCSVDownloadUrl(csvData) : '' + + const tableData = + transactions + ?.filter( + (transaction) => + transaction.type !== 'CREATED' && transaction.type !== 'CLOSED' && transaction.type !== 'PRICED' + ) + .sort((a, b) => (a.timestamp > b.timestamp ? -1 : 1)) + .slice(0, preview ? 8 : Infinity) + .map((transaction) => { + const [, id] = transaction.asset.id.split('-') + const { label, amount } = getLabelAndAmount(transaction) + return { + type: label, + transactionDate: transaction.timestamp, + assetId: transaction.asset.id, + assetName: assetMetadata[Number(id) - 1]?.data?.name, + amount, + hash: transaction.hash, + } + }) || [] -export const TransactionHistory = () => { return ( - - Transaction History - + + + + Transaction history + + {transactions?.length && ( + + Download + + )} + + + {transactions?.length! > 8 && preview && ( + + View all + + )} + ) } diff --git a/centrifuge-app/src/components/Report/AssetTransactions.tsx b/centrifuge-app/src/components/Report/AssetTransactions.tsx index 83cc9a11e9..155133082b 100644 --- a/centrifuge-app/src/components/Report/AssetTransactions.tsx +++ b/centrifuge-app/src/components/Report/AssetTransactions.tsx @@ -7,9 +7,9 @@ import { getCSVDownloadUrl } from '../../utils/getCSVDownloadUrl' import { useAssetTransactions } from '../../utils/usePools' import { DataTable } from '../DataTable' import { Spinner } from '../Spinner' -import type { TableDataRow } from './index' import { ReportContext } from './ReportContext' import { UserFeedback } from './UserFeedback' +import type { TableDataRow } from './index' import { formatAssetTransactionType } from './utils' export function AssetTransactions({ pool }: { pool: Pool }) { @@ -26,7 +26,7 @@ export function AssetTransactions({ pool }: { pool: Pool }) { return transactions?.map((tx) => ({ name: '', value: [ - tx.assetId.split('-').at(-1)!, + tx.asset.id.split('-').at(-1)!, tx.epochId.split('-').at(-1)!, formatDate(tx.timestamp.toString()), formatAssetTransactionType(tx.type), @@ -77,4 +77,4 @@ export function AssetTransactions({ pool }: { pool: Pool }) { ) : ( ) -} \ No newline at end of file +} diff --git a/centrifuge-app/src/components/Root.tsx b/centrifuge-app/src/components/Root.tsx index 30ad40e09e..84f5fc6528 100644 --- a/centrifuge-app/src/components/Root.tsx +++ b/centrifuge-app/src/components/Root.tsx @@ -5,11 +5,11 @@ import { TransactionToasts, WalletProvider, } from '@centrifuge/centrifuge-react' -import { FabricProvider, GlobalStyle as FabricGlobalStyle } from '@centrifuge/fabric' +import { GlobalStyle as FabricGlobalStyle, FabricProvider } from '@centrifuge/fabric' import * as React from 'react' import { HelmetProvider } from 'react-helmet-async' import { QueryClient, QueryClientProvider } from 'react-query' -import { BrowserRouter as Router, LinkProps, matchPath, Redirect, Route, RouteProps, Switch } from 'react-router-dom' +import { LinkProps, Redirect, Route, RouteProps, BrowserRouter as Router, Switch, matchPath } from 'react-router-dom' import { config, evmChains } from '../config' import PoolsPage from '../pages/Pools' import { pinToApi } from '../utils/pinToApi' @@ -131,6 +131,7 @@ const TransactionHistoryPage = React.lazy(() => import('../pages/Portfolio/Trans const TokenOverviewPage = React.lazy(() => import('../pages/Tokens')) const PrimePage = React.lazy(() => import('../pages/Prime')) const PrimeDetailPage = React.lazy(() => import('../pages/Prime/Detail')) +const PoolTransactionsPage = React.lazy(() => import('../pages/PoolTransactions')) const routes: RouteProps[] = [ { path: '/nfts/collection/:cid/object/mint', component: MintNFTPage }, @@ -144,6 +145,7 @@ const routes: RouteProps[] = [ { path: '/issuer/:pid', component: IssuerPoolPage }, { path: '/pools/:pid/assets/:aid', component: LoanPage }, { path: '/pools/tokens', component: TokenOverviewPage }, + { path: '/pools/:pid/transactions', component: PoolTransactionsPage }, { path: '/pools/:pid', component: PoolDetailPage }, { path: '/pools', component: PoolsPage }, { path: '/history/:address', component: TransactionHistoryPage }, diff --git a/centrifuge-app/src/pages/Loan/PricingValues.tsx b/centrifuge-app/src/pages/Loan/PricingValues.tsx index 345156f792..1937941b41 100644 --- a/centrifuge-app/src/pages/Loan/PricingValues.tsx +++ b/centrifuge-app/src/pages/Loan/PricingValues.tsx @@ -27,7 +27,7 @@ export function PricingValues({ loan, pool }: Props) { const days = getAge(new Date(pricing.oracle.timestamp).toISOString()) const borrowerAssetTransactions = assetTransactions?.filter( - (assetTransaction) => assetTransaction.loanId === `${loan.poolId}-${loan.id}` + (assetTransaction) => assetTransaction.asset.id === `${loan.poolId}-${loan.id}` ) const latestPrice = getLatestPrice(pricing.oracle.value, borrowerAssetTransactions, pool.currency.decimals) diff --git a/centrifuge-app/src/pages/Pool/Overview/index.tsx b/centrifuge-app/src/pages/Pool/Overview/index.tsx index 47dbc37a12..f69e21ed91 100644 --- a/centrifuge-app/src/pages/Pool/Overview/index.tsx +++ b/centrifuge-app/src/pages/Pool/Overview/index.tsx @@ -1,5 +1,5 @@ import { CurrencyBalance, Price } from '@centrifuge/centrifuge-js' -import { Box, Button, Grid, TextWithPlaceholder } from '@centrifuge/fabric' +import { Box, Button, Card, Grid, TextWithPlaceholder } from '@centrifuge/fabric' import Decimal from 'decimal.js-light' import * as React from 'react' import { useParams } from 'react-router' @@ -163,7 +163,9 @@ export function PoolDetailOverview() { }> - + + + diff --git a/centrifuge-app/src/pages/PoolTransactions.tsx b/centrifuge-app/src/pages/PoolTransactions.tsx new file mode 100644 index 0000000000..bb860855ef --- /dev/null +++ b/centrifuge-app/src/pages/PoolTransactions.tsx @@ -0,0 +1,29 @@ +import { Box, Stack, Text } from '@centrifuge/fabric' +import { useParams } from 'react-router' +import { LayoutBase } from '../components/LayoutBase' +import { LayoutSection } from '../components/LayoutBase/LayoutSection' +import { TransactionHistory } from '../components/PoolOverview/TransactionHistory' +import { usePool, usePoolMetadata } from '../utils/usePools' + +const PoolTransactions = () => { + const { pid: poolId } = useParams<{ pid: string }>() + const pool = usePool(poolId) + const { data: metadata } = usePoolMetadata(pool) + + return ( + + + + + {metadata?.pool?.name} + + + + + + + + ) +} + +export default PoolTransactions diff --git a/centrifuge-app/src/utils/usePools.ts b/centrifuge-app/src/utils/usePools.ts index f54997748b..7a98a0ba7c 100644 --- a/centrifuge-app/src/utils/usePools.ts +++ b/centrifuge-app/src/utils/usePools.ts @@ -117,7 +117,7 @@ export function useBorrowerAssetTransactions(poolId: string, assetId: string, fr return assetTransactions.pipe( map((transactions: AssetTransaction[]) => - transactions.filter((transaction) => transaction.assetId.split('-')[1] === assetId) + transactions.filter((transaction) => transaction.asset.id.split('-')[1] === assetId) ) ) }, diff --git a/centrifuge-js/src/modules/pools.ts b/centrifuge-js/src/modules/pools.ts index e9bf8c6fe9..8383c4ccb7 100644 --- a/centrifuge-js/src/modules/pools.ts +++ b/centrifuge-js/src/modules/pools.ts @@ -10,22 +10,22 @@ import { SolverResult, calculateOptimalSolution } from '..' import { Centrifuge } from '../Centrifuge' import { Account, TransactionOptions } from '../types' import { - AssetTransactionType, - InvestorTransactionType, - SubqueryAssetTransaction, - SubqueryCurrencyBalances, - SubqueryInvestorTransaction, - SubqueryPoolSnapshot, - SubqueryTrancheBalances, - SubqueryTrancheSnapshot, + AssetTransactionType, + InvestorTransactionType, + SubqueryAssetTransaction, + SubqueryCurrencyBalances, + SubqueryInvestorTransaction, + SubqueryPoolSnapshot, + SubqueryTrancheBalances, + SubqueryTrancheSnapshot, } from '../types/subquery' import { - addressToHex, - computeTrancheId, - getDateMonthsFromNow, - getDateYearsFromNow, - getRandomUint, - isSameAddress, + addressToHex, + computeTrancheId, + getDateMonthsFromNow, + getDateYearsFromNow, + getRandomUint, + isSameAddress, } from '../utils' import { CurrencyBalance, Perquintill, Price, Rate, TokenBalance } from '../utils/BN' import { Dec } from '../utils/Decimal' @@ -731,11 +731,17 @@ export type AssetTransaction = { poolId: string accountId: string epochId: string - loanId: string type: AssetTransactionType amount: CurrencyBalance | undefined settlementPrice: string | null quantity: string | null + principalAmount: CurrencyBalance | undefined + interestAmount: CurrencyBalance | undefined + hash: string + asset: { + id: string + metadata: string + } } type Holder = { @@ -2521,13 +2527,19 @@ export function getPoolsModule(inst: Centrifuge) { timestamp: { greaterThan: $from, lessThan: $to }, }) { nodes { - assetId + principalAmount + interestAmount epochId type timestamp amount settlementPrice quantity + hash + asset { + id + metadata + } } } } @@ -2546,6 +2558,8 @@ export function getPoolsModule(inst: Centrifuge) { return data!.assetTransactions.nodes.map((tx) => ({ ...tx, amount: tx.amount ? new CurrencyBalance(tx.amount, currency.decimals) : undefined, + principalAmount: tx.principalAmount ? new CurrencyBalance(tx.principalAmount, currency.decimals) : undefined, + interestAmount: tx.interestAmount ? new CurrencyBalance(tx.interestAmount, currency.decimals) : undefined, timestamp: new Date(`${tx.timestamp}+00:00`), })) as unknown as AssetTransaction[] }) @@ -2933,11 +2947,11 @@ export function getPoolsModule(inst: Centrifuge) { } > = {} // oracles.forEach(() => { - // const { timestamp, value } = oracle[1].toPrimitive() as any - // oraclePrices[(oracle[0].toHuman() as any)[0].Isin] = { - // timestamp, - // value: new CurrencyBalance(value, currency.decimals), - // } + // const { timestamp, value } = oracle[1].toPrimitive() as any + // oraclePrices[(oracle[0].toHuman() as any)[0].Isin] = { + // timestamp, + // value: new CurrencyBalance(value, currency.decimals), + // } // }) const activeLoansPortfolio: Record< diff --git a/centrifuge-js/src/types/subquery.ts b/centrifuge-js/src/types/subquery.ts index 4cc7707737..75be6a2871 100644 --- a/centrifuge-js/src/types/subquery.ts +++ b/centrifuge-js/src/types/subquery.ts @@ -83,11 +83,16 @@ export type SubqueryAssetTransaction = { poolId: string accountId: string epochId: string - assetId: string type: AssetTransactionType - amount?: number | null + amount: CurrencyBalance | undefined + principalAmount: CurrencyBalance | undefined + interestAmount: CurrencyBalance | undefined settlementPrice: string | null quantity: string | null + asset: { + id: string + metadata: string + } } export type SubqueryTrancheBalances = {