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({