diff --git a/CHANGELOG.md b/CHANGELOG.md index 48bde5dd8..7c8f5c69d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -39,6 +39,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Features +- [#833](https://github.com/alleslabs/celatone-frontend/pull/833) Add link to transactions page to current bonded token component +- [#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 - [#835](https://github.com/alleslabs/celatone-frontend/pull/835) Add amp view in json - block and proposal details - [#823](https://github.com/alleslabs/celatone-frontend/pull/823) Add validator uptime and penalty events diff --git a/src/lib/pages/validator-details/components/bonded-token-changes/VotingPowerChart.tsx b/src/lib/pages/validator-details/components/bonded-token-changes/VotingPowerChart.tsx index 69f785ca9..ab4beeb3c 100644 --- a/src/lib/pages/validator-details/components/bonded-token-changes/VotingPowerChart.tsx +++ b/src/lib/pages/validator-details/components/bonded-token-changes/VotingPowerChart.tsx @@ -1,8 +1,11 @@ -import { Box, Flex, Heading, Text } from "@chakra-ui/react"; +import { Box, Button, Flex, Heading, Text } from "@chakra-ui/react"; import type { BigSource } from "big.js"; import type { ScriptableContext, TooltipModel } from "chart.js"; +import { TabIndex } from "../../types"; +import { useInternalNavigate } from "lib/app-provider"; import { LineChart } from "lib/components/chart/LineChart"; +import { CustomIcon } from "lib/components/icon"; import { Loading } from "lib/components/Loading"; import { ErrorFetching } from "lib/components/state"; import { useValidatorHistoricalPowers } from "lib/services/validatorService"; @@ -17,13 +20,17 @@ interface VotingPowerChartProps { validatorAddress: ValidatorAddr; singleStakingDenom: Option; assetInfos: Option; + isOverview?: boolean; } export const VotingPowerChart = ({ validatorAddress, singleStakingDenom, assetInfos, + isOverview, }: VotingPowerChartProps) => { + const navigate = useInternalNavigate(); + const { data: historicalPowers, isLoading } = useValidatorHistoricalPowers(validatorAddress); @@ -115,27 +122,50 @@ export const VotingPowerChart = ({ rounded={8} w="100%" > - - - {singleStakingDenom - ? "Current Bonded Token" - : "Current Voting Powers"} - - - {currentPrice} {currency} - - - = 0 ? "success.main" : "error.main"} + + + + {singleStakingDenom + ? "Current Bonded Token" + : "Current Voting Powers"} + + + {currentPrice} {currency} + + + = 0 ? "success.main" : "error.main"} + > + {diffInLast24Hr >= 0 + ? `+${handleFormatValue(diffInLast24Hr)}` + : `-${handleFormatValue(-diffInLast24Hr)}`} + {" "} + {currency} in last 24 hr + + + {isOverview && ( + + )} ( - - - - -); +}: 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..1de032901 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 "./VotedProposalsTable"; +export * from "./related-transactions"; 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..c3e40b0d7 --- /dev/null +++ b/src/lib/pages/validator-details/components/tables/related-transactions/RelatedTransactionsBondedTokenChanges.tsx @@ -0,0 +1,66 @@ +import { Box, Flex, Text } from "@chakra-ui/react"; +import type { BigSource } from "big.js"; +import type Big from "big.js"; + +import { TokenImageRender } from "lib/components/token"; +import { getUndefinedTokenIcon } from "lib/pages/pools/utils"; +import type { AssetInfos, Coin, Option, Token, U, USD } from "lib/types"; +import { + coinToTokenWithValue, + formatPrice, + formatUTokenWithPrecision, + getTokenLabel, +} from "lib/utils"; + +interface RelatedTransactionsBondedTokenChangesProps { + txHash: string; + coin: Coin; + assetInfos: Option; +} + +export const RelatedTransactionsBondedTokenChanges = ({ + txHash, + coin, + assetInfos, +}: RelatedTransactionsBondedTokenChangesProps) => { + const token = coinToTokenWithValue(coin.denom, coin.amount, assetInfos); + const isPositiveAmount = token.amount.gte(0); + const formattedAmount = `${isPositiveAmount ? "+" : "-"}${formatUTokenWithPrecision(token.amount.abs() as U>, token.precision ?? 0, false, 2)}`; + + return ( + + + + + {formattedAmount} + + {getTokenLabel(token.denom, token.symbol)} + + + + {token.value + ? `(${formatPrice(token.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..4dfae0042 --- /dev/null +++ b/src/lib/pages/validator-details/components/tables/related-transactions/RelatedTransactionsMobileCard.tsx @@ -0,0 +1,89 @@ +import { Badge, Box, Flex, Grid, GridItem, Text } from "@chakra-ui/react"; + +import { ExplorerLink } from "lib/components/ExplorerLink"; +import { MobileCardTemplate } 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 { + delegationRelatedTx: ValidatorDelegationRelatedTxsResponseItem; + assetInfos: Option; + onRowSelect: (txHash: string) => void; +} + +export const RelatedTransactionsMobileCard = ({ + delegationRelatedTx, + assetInfos, + onRowSelect, +}: RelatedTransactionsMobileCardProps) => ( + + + + + 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)})`} + + + } + onClick={() => onRowSelect(delegationRelatedTx.txHash)} + /> +); 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..4a15f52e9 --- /dev/null +++ b/src/lib/pages/validator-details/components/tables/related-transactions/RelatedTransactionsTableRow.tsx @@ -0,0 +1,79 @@ +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; + onRowSelect: (txHash: string) => void; + assetInfos: Option; +} + +export const RelatedTransactionsTableRow = ({ + delegationRelatedTx, + templateColumns, + onRowSelect, + assetInfos, +}: RelatedTransactionsTableRowProps) => ( + onRowSelect(delegationRelatedTx.txHash)} + > + + + {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.tsx b/src/lib/pages/validator-details/components/tables/related-transactions/index.tsx new file mode 100644 index 000000000..0db818db4 --- /dev/null +++ b/src/lib/pages/validator-details/components/tables/related-transactions/index.tsx @@ -0,0 +1,62 @@ +import { useInternalNavigate, useMobile } from "lib/app-provider"; +import { Loading } from "lib/components/Loading"; +import { ErrorFetching } from "lib/components/state"; +import { MobileTableContainer, TableContainer } from "lib/components/table"; +import type { ValidatorDelegationRelatedTxsResponseItem } from "lib/services/validator"; +import type { AssetInfos, Option } from "lib/types"; + +import { RelatedTransactionsMobileCard } from "./RelatedTransactionsMobileCard"; +import { RelatedTransactionsTableHeader } from "./RelatedTransactionsTableHeader"; +import { RelatedTransactionsTableRow } from "./RelatedTransactionsTableRow"; + +interface RelatedTransactionTableProps { + delegationRelatedTxs: Option; + isLoading: boolean; + assetInfos: Option; +} + +export const RelatedTransactionTable = ({ + delegationRelatedTxs, + isLoading, + assetInfos, +}: RelatedTransactionTableProps) => { + const isMobile = useMobile(); + const navigate = useInternalNavigate(); + const onRowSelect = (txHash: string) => + navigate({ + pathname: "/txs/[txHash]", + query: { txHash: txHash.toUpperCase() }, + }); + + if (isLoading) return ; + if (!delegationRelatedTxs) + return ; + + const templateColumns = "max(180px) max(180px) max(180px) 1fr max(280px)"; + + return isMobile ? ( + + {delegationRelatedTxs.map((delegationRelatedTx) => ( + + ))} + + ) : ( + + + {delegationRelatedTxs.map((delegationRelatedTx) => ( + + ))} + + ); +}; diff --git a/src/lib/pages/validator-details/components/validator-overview/index.tsx b/src/lib/pages/validator-details/components/validator-overview/index.tsx index b4f2f60c2..be0d26e18 100644 --- a/src/lib/pages/validator-details/components/validator-overview/index.tsx +++ b/src/lib/pages/validator-details/components/validator-overview/index.tsx @@ -76,6 +76,7 @@ export const ValidatorOverview = ({ validatorAddress={validatorAddress} singleStakingDenom={singleStakingDenom} assetInfos={assetInfos} + isOverview /> )} diff --git a/src/lib/services/validator.ts b/src/lib/services/validator.ts index 8f95454d5..7bde78eb0 100644 --- a/src/lib/services/validator.ts +++ b/src/lib/services/validator.ts @@ -6,13 +6,18 @@ import { CURR_THEME } from "env"; import type { Option, StakingShare, Validator, ValidatorAddr } from "lib/types"; import { BlockVote, + zBechAddr, zBig, zCoin, zUtcDate, - 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"; @@ -223,13 +228,21 @@ export const getValidatorUptime = async ( const zValidatorDelegationRelatedTxsResponseItem = z .object({ - tx_hash: z.string(), + tx_hash: z.string().transform(parseTxHash), height: z.number().positive(), tokens: zCoin.array(), timestamp: zUtcDate, - validator_address: zValidatorAddr, + messages: z.array( + z.object({ + type: z.string(), + }) + ), + sender: zBechAddr, }) .transform(snakeToCamel); +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 3f23b8d2c..cc3042072 100644 --- a/src/lib/services/validatorService.ts +++ b/src/lib/services/validatorService.ts @@ -169,7 +169,11 @@ export const useValidatorUptime = ( export const useValidatorDelegationRelatedTxs = ( validatorAddress: ValidatorAddr, limit: number, - offset: number + offset: number, + options: Pick< + UseQueryOptions, + "onSuccess" + > = {} ) => { const endpoint = useBaseApiRoute("validators"); @@ -178,6 +182,8 @@ export const useValidatorDelegationRelatedTxs = ( CELATONE_QUERY_KEYS.VALIDATOR_DELEGATION_RELATED_TXS, endpoint, validatorAddress, + limit, + offset, ], async () => getValidatorDelegationRelatedTxs( @@ -186,7 +192,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.test.ts b/src/lib/utils/formatter/token.test.ts index f28f53228..e941a9cac 100644 --- a/src/lib/utils/formatter/token.test.ts +++ b/src/lib/utils/formatter/token.test.ts @@ -155,9 +155,6 @@ describe("toToken", () => { test("NaN", () => { expect(toToken(NaN as U>, 6)).toEqual(big(0) as Token); }); - test("negative number", () => { - expect(toToken(-1 as U>, 6)).toEqual(big(0) as Token); - }); }); test("more than 1", () => { expect(toToken(12345678 as U>, 6)).toEqual( diff --git a/src/lib/utils/formatter/token.ts b/src/lib/utils/formatter/token.ts index e6eb779b0..78c383f38 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; } @@ -71,6 +71,7 @@ export const formatUTokenWithPrecision = ( decimalPoints?: number ): 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`; @@ -78,6 +79,7 @@ export const formatUTokenWithPrecision = ( } const lowestThreshold = big(10).pow(-(decimalPoints ?? precision)); + if (!token.eq(0) && token.lt(lowestThreshold)) { return `<${lowestThreshold.toFixed()}`; }