diff --git a/CHANGELOG.md b/CHANGELOG.md index 4d5bab402..e8f6fa427 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -39,6 +39,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Features +- [#832](https://github.com/alleslabs/celatone-frontend/pull/832) Add bonded token changes delegation related transactions - [#831](https://github.com/alleslabs/celatone-frontend/pull/831) Update zod schema to apply with new validator delegation related txs api spec - [#828](https://github.com/alleslabs/celatone-frontend/pull/828) Add validator detail voting power overview section with data from APIs - [#819](https://github.com/alleslabs/celatone-frontend/pull/819) Add validator detail overview section with data from APIs diff --git a/src/lib/pages/validator-details/components/bonded-token-changes/index.tsx b/src/lib/pages/validator-details/components/bonded-token-changes/index.tsx index 5cf7f8200..e5d6c62c5 100644 --- a/src/lib/pages/validator-details/components/bonded-token-changes/index.tsx +++ b/src/lib/pages/validator-details/components/bonded-token-changes/index.tsx @@ -1,6 +1,14 @@ -import { Flex } from "@chakra-ui/react"; +import { Box, Flex } from "@chakra-ui/react"; -import { RelatedTransactionTable } from "../tables/RelatedTransactionsTable"; +import { + RelatedTransactionsMobileCard, + RelatedTransactionTable, +} from "../tables"; +import { useMobile } from "lib/app-provider"; +import { Pagination } from "lib/components/pagination"; +import { usePaginator } from "lib/components/pagination/usePaginator"; +import { TableTitle } from "lib/components/table"; +import { useValidatorDelegationRelatedTxs } from "lib/services/validatorService"; import type { AssetInfos, Option, ValidatorAddr } from "lib/types"; import { VotingPowerChart } from "./VotingPowerChart"; @@ -15,13 +23,82 @@ export const BondedTokenChanges = ({ validatorAddress, singleStakingDenom, assetInfos, -}: BondedTokenChangesProps) => ( - - - - -); +}: BondedTokenChangesProps) => { + const isMobile = useMobile(); + + const { + pagesQuantity, + setTotalData, + currentPage, + setCurrentPage, + pageSize, + setPageSize, + offset, + } = usePaginator({ + initialState: { + pageSize: 10, + currentPage: 1, + isDisabled: false, + }, + }); + + const { data, isLoading } = useValidatorDelegationRelatedTxs( + validatorAddress, + pageSize, + offset, + { + onSuccess: ({ total }) => setTotalData(total), + } + ); + + const tableHeaderId = "relatedTransactionTableHeader"; + + return ( + + + + {isMobile ? ( + + ) : ( + <> + + + + )} + {!!data?.total && data.total > 10 && ( + { + const size = Number(e.target.value); + setPageSize(size); + setCurrentPage(1); + }} + /> + )} + + + ); +}; diff --git a/src/lib/pages/validator-details/components/tables/RelatedTransactionsTable.tsx b/src/lib/pages/validator-details/components/tables/RelatedTransactionsTable.tsx deleted file mode 100644 index 4183138bd..000000000 --- a/src/lib/pages/validator-details/components/tables/RelatedTransactionsTable.tsx +++ /dev/null @@ -1,21 +0,0 @@ -import { Flex, Heading, Text } from "@chakra-ui/react"; - -import { useMobile } from "lib/app-provider"; - -export const RelatedTransactionTable = () => { - const isMobile = useMobile(); - return isMobile ? ( - mobile table - ) : ( - - - Delegation-Related Transactions - - - Shows transactions relevant to changes in delegated tokens, excluding - any token reduction due to slashing. - - table - - ); -}; diff --git a/src/lib/pages/validator-details/components/tables/index.ts b/src/lib/pages/validator-details/components/tables/index.ts index 1ce862e1b..112205369 100644 --- a/src/lib/pages/validator-details/components/tables/index.ts +++ b/src/lib/pages/validator-details/components/tables/index.ts @@ -1,3 +1,3 @@ export * from "./ProposedBlocksTable"; -export * from "./RelatedTransactionsTable"; +export * from "./related-transactions"; export * from "./VotedProposalsTable"; diff --git a/src/lib/pages/validator-details/components/tables/related-transactions/RelatedTransactionsBondedTokenChanges.tsx b/src/lib/pages/validator-details/components/tables/related-transactions/RelatedTransactionsBondedTokenChanges.tsx new file mode 100644 index 000000000..141b3ae4c --- /dev/null +++ b/src/lib/pages/validator-details/components/tables/related-transactions/RelatedTransactionsBondedTokenChanges.tsx @@ -0,0 +1,71 @@ +import { Box, Flex, Text } from "@chakra-ui/react"; +import type { BigSource } from "big.js"; + +import { TokenImageRender } from "lib/components/token"; +import { getUndefinedTokenIcon } from "lib/pages/pools/utils"; +import type { AssetInfos, Coin, Option, USD } from "lib/types"; +import { + coinToTokenWithValue, + formatPrice, + formatUTokenWithPrecision, +} from "lib/utils"; + +interface RelatedTransactionsBondedTokenChangesProps { + txHash: string; + token: Coin; + assetInfos: Option; +} + +export const RelatedTransactionsBondedTokenChanges = ({ + txHash, + token, + assetInfos, +}: RelatedTransactionsBondedTokenChangesProps) => { + const tokenWithValue = coinToTokenWithValue( + token?.denom, + token?.amount, + assetInfos + ); + + const formattedAmount = formatUTokenWithPrecision( + tokenWithValue.amount, + tokenWithValue.precision ?? 0, + false, + 2, + true + ); + + const isPositiveAmount = tokenWithValue.amount.gte(0); + + return ( + + + + + {formattedAmount} + {" "} + {tokenWithValue.symbol} + + + ({formatPrice(tokenWithValue.value?.abs() as USD)}) + + + + + ); +}; diff --git a/src/lib/pages/validator-details/components/tables/related-transactions/RelatedTransactionsMobileCard.tsx b/src/lib/pages/validator-details/components/tables/related-transactions/RelatedTransactionsMobileCard.tsx new file mode 100644 index 000000000..b2c8e7719 --- /dev/null +++ b/src/lib/pages/validator-details/components/tables/related-transactions/RelatedTransactionsMobileCard.tsx @@ -0,0 +1,96 @@ +import { Badge, Box, Flex, Grid, GridItem, Text } from "@chakra-ui/react"; + +import { ExplorerLink } from "lib/components/ExplorerLink"; +import { Loading } from "lib/components/Loading"; +import { MobileCardTemplate, MobileTableContainer } from "lib/components/table"; +import type { ValidatorDelegationRelatedTxsResponseItem } from "lib/services/validator"; +import type { AssetInfos, Option } from "lib/types"; +import { dateFromNow, extractMsgType, formatUTC } from "lib/utils"; + +import { RelatedTransactionsBondedTokenChanges } from "./RelatedTransactionsBondedTokenChanges"; + +interface RelatedTransactionsMobileCardProps { + delegationRelatedTxs: Option; + isLoading: boolean; + assetInfos: Option; +} + +export const RelatedTransactionsMobileCard = ({ + delegationRelatedTxs, + isLoading, + assetInfos, +}: RelatedTransactionsMobileCardProps) => { + if (isLoading) return ; + + return ( + + {delegationRelatedTxs?.map((delegationRelatedTx) => ( + + + + + Transaction Hash + + + {delegationRelatedTx.messages.length > 1 && ( + + {delegationRelatedTx.messages.length} + + )} + + + + Sender + + + + + + + Action + + + {delegationRelatedTx.messages.length > 1 + ? `${delegationRelatedTx.messages.length} Messages` + : extractMsgType(delegationRelatedTx.messages[0].type)} + + + + + Bonded Token Changes + + {delegationRelatedTx.tokens.map((token) => ( + + ))} + + + } + bottomContent={ + + + {formatUTC(delegationRelatedTx.timestamp)} + + + {`(${dateFromNow(delegationRelatedTx.timestamp)})`} + + + } + /> + ))} + + ); +}; diff --git a/src/lib/pages/validator-details/components/tables/related-transactions/RelatedTransactionsTable.tsx b/src/lib/pages/validator-details/components/tables/related-transactions/RelatedTransactionsTable.tsx new file mode 100644 index 000000000..322dc666b --- /dev/null +++ b/src/lib/pages/validator-details/components/tables/related-transactions/RelatedTransactionsTable.tsx @@ -0,0 +1,37 @@ +import { Loading } from "lib/components/Loading"; +import { TableContainer } from "lib/components/table"; +import type { ValidatorDelegationRelatedTxsResponseItem } from "lib/services/validator"; +import type { AssetInfos, Option } from "lib/types"; + +import { RelatedTransactionsTableHeader } from "./RelatedTransactionsTableHeader"; +import { RelatedTransactionsTableRow } from "./RelatedTransactionsTableRow"; + +interface RelatedTransactionTableProps { + delegationRelatedTxs: Option; + isLoading: boolean; + assetInfos: Option; +} + +export const RelatedTransactionTable = ({ + delegationRelatedTxs = [], + isLoading, + assetInfos, +}: RelatedTransactionTableProps) => { + if (isLoading) return ; + + const templateColumns = "max(180px) max(180px) max(180px) 1fr max(280px)"; + + return ( + + + {delegationRelatedTxs.map((delegationRelatedTx) => ( + + ))} + + ); +}; diff --git a/src/lib/pages/validator-details/components/tables/related-transactions/RelatedTransactionsTableHeader.tsx b/src/lib/pages/validator-details/components/tables/related-transactions/RelatedTransactionsTableHeader.tsx new file mode 100644 index 000000000..b8becd2b2 --- /dev/null +++ b/src/lib/pages/validator-details/components/tables/related-transactions/RelatedTransactionsTableHeader.tsx @@ -0,0 +1,21 @@ +import { Grid } from "@chakra-ui/react"; + +import { TableHeader } from "lib/components/table"; + +interface RelatedTransactionsTableHeaderProps { + templateColumns: string; +} + +export const RelatedTransactionsTableHeader = ({ + templateColumns, +}: RelatedTransactionsTableHeaderProps) => ( + + Transaction Hash + Sender + Action + + Bonded Token Changes + + Timestamp + +); diff --git a/src/lib/pages/validator-details/components/tables/related-transactions/RelatedTransactionsTableRow.tsx b/src/lib/pages/validator-details/components/tables/related-transactions/RelatedTransactionsTableRow.tsx new file mode 100644 index 000000000..01bfa5933 --- /dev/null +++ b/src/lib/pages/validator-details/components/tables/related-transactions/RelatedTransactionsTableRow.tsx @@ -0,0 +1,77 @@ +import { Badge, Box, Grid, Text } from "@chakra-ui/react"; + +import { ExplorerLink } from "lib/components/ExplorerLink"; +import { TableRow } from "lib/components/table"; +import type { ValidatorDelegationRelatedTxsResponseItem } from "lib/services/validator"; +import type { AssetInfos, Option } from "lib/types"; +import { dateFromNow, extractMsgType, formatUTC } from "lib/utils"; + +import { RelatedTransactionsBondedTokenChanges } from "./RelatedTransactionsBondedTokenChanges"; + +interface RelatedTransactionsTableRowProps { + delegationRelatedTx: ValidatorDelegationRelatedTxsResponseItem; + templateColumns: string; + assetInfos: Option; +} + +export const RelatedTransactionsTableRow = ({ + delegationRelatedTx, + templateColumns, + assetInfos, +}: RelatedTransactionsTableRowProps) => { + return ( + + + + {delegationRelatedTx.messages.length > 1 && ( + + {delegationRelatedTx.messages.length} + + )} + + + + + + + {delegationRelatedTx.messages.length > 1 + ? `${delegationRelatedTx.messages.length} Messages` + : extractMsgType(delegationRelatedTx.messages[0].type)} + + + + {delegationRelatedTx.tokens.map((token) => ( + + ))} + + + + + {formatUTC(delegationRelatedTx.timestamp)} + + + {`(${dateFromNow(delegationRelatedTx.timestamp)})`} + + + + + ); +}; diff --git a/src/lib/pages/validator-details/components/tables/related-transactions/index.ts b/src/lib/pages/validator-details/components/tables/related-transactions/index.ts new file mode 100644 index 000000000..510db6bb2 --- /dev/null +++ b/src/lib/pages/validator-details/components/tables/related-transactions/index.ts @@ -0,0 +1,2 @@ +export * from "./RelatedTransactionsTable"; +export * from "./RelatedTransactionsMobileCard"; diff --git a/src/lib/services/validator.ts b/src/lib/services/validator.ts index 37a2b8a22..fb014df1f 100644 --- a/src/lib/services/validator.ts +++ b/src/lib/services/validator.ts @@ -12,7 +12,12 @@ import { zValidatorAddr, zValidatorData, } from "lib/types"; -import { parseWithError, removeSpecialChars, snakeToCamel } from "lib/utils"; +import { + parseTxHash, + parseWithError, + removeSpecialChars, + snakeToCamel, +} from "lib/utils"; import { zBlocksResponse } from "./block"; import { zProposal } from "./proposal"; @@ -214,20 +219,26 @@ export const getValidatorUptime = async ( }) .then(({ data }) => parseWithError(zValidatorUptimeResponse, data)); -const zValidatorDelegationRelatedTxsResponseMessage = z.object({ - type: z.string(), -}); - const zValidatorDelegationRelatedTxsResponseItem = z .object({ tx_hash: z.string(), height: z.number().positive(), tokens: zCoin.array(), timestamp: zUtcDate, - messages: z.array(zValidatorDelegationRelatedTxsResponseMessage), + messages: z.array( + z.object({ + type: z.string(), + }) + ), sender: zValidatorAddr, }) - .transform(snakeToCamel); + .transform((val) => ({ + ...snakeToCamel(val), + txHash: parseTxHash(val.tx_hash), + })); +export type ValidatorDelegationRelatedTxsResponseItem = z.infer< + typeof zValidatorDelegationRelatedTxsResponseItem +>; const zValidatorDelegationRelatedTxsResponse = z.object({ items: z.array(zValidatorDelegationRelatedTxsResponseItem), diff --git a/src/lib/services/validatorService.ts b/src/lib/services/validatorService.ts index c390dbea3..2ee12ce50 100644 --- a/src/lib/services/validatorService.ts +++ b/src/lib/services/validatorService.ts @@ -168,7 +168,11 @@ export const useValidatorUptime = ( export const useValidatorDelegationRelatedTxs = ( validatorAddress: ValidatorAddr, limit: number, - offset: number + offset: number, + options: Pick< + UseQueryOptions, + "onSuccess" + > = {} ) => { const endpoint = useBaseApiRoute("validators"); @@ -177,6 +181,8 @@ export const useValidatorDelegationRelatedTxs = ( CELATONE_QUERY_KEYS.VALIDATOR_DELEGATION_RELATED_TXS, endpoint, validatorAddress, + limit, + offset, ], async () => getValidatorDelegationRelatedTxs( @@ -185,7 +191,7 @@ export const useValidatorDelegationRelatedTxs = ( limit, offset ), - { retry: 1 } + { retry: 1, ...options } ); }; diff --git a/src/lib/types/asset.ts b/src/lib/types/asset.ts index 6cfe6690b..0d649602b 100644 --- a/src/lib/types/asset.ts +++ b/src/lib/types/asset.ts @@ -7,6 +7,7 @@ export const zCoin = z.object({ denom: z.string(), amount: z.string(), }); +export type Coin = z.infer; export const zAssetInfo = z.object({ coingecko: z.string(), diff --git a/src/lib/utils/formatter/token.ts b/src/lib/utils/formatter/token.ts index e6eb779b0..01b02271c 100644 --- a/src/lib/utils/formatter/token.ts +++ b/src/lib/utils/formatter/token.ts @@ -53,7 +53,7 @@ export const toToken = ( ): Token => { try { const value = big(uAmount).div(big(10).pow(precision)); - return (value.gte(0) ? value : big(0)) as Token; + return value as Token; } catch { return big(0) as Token; } @@ -68,18 +68,23 @@ export const formatUTokenWithPrecision = ( amount: U>, precision: number, isSuffix = true, - decimalPoints?: number + decimalPoints?: number, + allowNegativeValue = false ): string => { const token = toToken(amount, precision); + if (isSuffix) { if (token.gte(B)) return `${d2Formatter(token.div(B), "0.00")}B`; if (token.gte(M)) return `${d2Formatter(token.div(M), "0.00")}M`; if (token.gte(K)) return `${d2Formatter(token, "0.00")}`; } - const lowestThreshold = big(10).pow(-(decimalPoints ?? precision)); - if (!token.eq(0) && token.lt(lowestThreshold)) { - return `<${lowestThreshold.toFixed()}`; + if (!allowNegativeValue) { + const lowestThreshold = big(10).pow(-(decimalPoints ?? precision)); + + if (!token.eq(0) && token.lt(lowestThreshold)) { + return `<${lowestThreshold.toFixed()}`; + } } return formatDecimal({