diff --git a/CHANGELOG.md b/CHANGELOG.md index f7ade61d5..014150387 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -40,6 +40,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Features - [#966](https://github.com/alleslabs/celatone-frontend/pull/966) Support lite version for proposal details +- [#958](https://github.com/alleslabs/celatone-frontend/pull/958) Support lite version for block index page - [#951](https://github.com/alleslabs/celatone-frontend/pull/951) Support contract details lite version with LCD endpoint - [#961](https://github.com/alleslabs/celatone-frontend/pull/961) Add and refactor proposal related lcd endpoints - [#952](https://github.com/alleslabs/celatone-frontend/pull/952) Support module details page lite version with LCD endpoint @@ -73,6 +74,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Improvements +- [#967](https://github.com/alleslabs/celatone-frontend/pull/967) Utilize consensus address for better consistency - [#963](https://github.com/alleslabs/celatone-frontend/pull/963) Add separator between token in tx message - [#959](https://github.com/alleslabs/celatone-frontend/pull/959) Update @cosmos-kit/react to v2.15.0 and friends - [#947](https://github.com/alleslabs/celatone-frontend/pull/947) Add zod type for sign mode infos diff --git a/src/lib/app-provider/env.ts b/src/lib/app-provider/env.ts index 49f3e4ed3..f4e9ee920 100644 --- a/src/lib/app-provider/env.ts +++ b/src/lib/app-provider/env.ts @@ -19,6 +19,7 @@ export enum CELATONE_QUERY_KEYS { // BLOCK BLOCKS = "CELATONE_QUERY_BLOCKS", BLOCK_DATA = "CELATONE_QUERY_BLOCK_DATA", + BLOCK_DATA_LCD = "CELATONE_QUERY_BLOCK_DATA_LCD", BLOCK_LATEST_HEIGHT_LCD = "CELATONE_QUERY_BLOCK_LATEST_HEIGHT_LCD", // CODE GQL CODES = "CELATONE_QUERY_CODES", diff --git a/src/lib/components/table/transactions/TransactionsTable.tsx b/src/lib/components/table/transactions/TransactionsTable.tsx index cca5fad8b..75d16b7e8 100644 --- a/src/lib/components/table/transactions/TransactionsTable.tsx +++ b/src/lib/components/table/transactions/TransactionsTable.tsx @@ -11,6 +11,7 @@ interface TransactionsTableProps { transactions: Option; isLoading: boolean; emptyState: JSX.Element; + showSuccess?: boolean; showRelations: boolean; showTimestamp?: boolean; showAction?: boolean; @@ -20,6 +21,7 @@ export const TransactionsTable = ({ transactions, isLoading, emptyState, + showSuccess = true, showRelations, showTimestamp = true, showAction = false, @@ -29,11 +31,17 @@ export const TransactionsTable = ({ if (isLoading) return ; if (!transactions?.length) return emptyState; - const templateColumns = `32px 190px 48px minmax(380px, 1fr) ${ - showRelations ? "90px " : "" - }max(180px) ${showTimestamp ? "max(228px) " : ""}${ - showAction ? "100px " : "" - }`; + const columns: string[] = [ + "32px", + "190px", + ...(showSuccess ? ["48px"] : []), + "minmax(380px, 1fr)", + ...(showRelations ? ["90px"] : []), + "max(180px)", + ...(showTimestamp ? ["max(228px)"] : []), + ...(showAction ? ["100px"] : []), + ]; + const templateColumns: string = columns.join(" "); return isMobile ? ( @@ -41,6 +49,7 @@ export const TransactionsTable = ({ @@ -50,6 +59,7 @@ export const TransactionsTable = ({ Transaction Hash - + {showSuccess && } Messages {showRelations && Relations} Sender diff --git a/src/lib/components/table/transactions/TransactionsTableMobileCard.tsx b/src/lib/components/table/transactions/TransactionsTableMobileCard.tsx index 3056121b7..dafbfd17c 100644 --- a/src/lib/components/table/transactions/TransactionsTableMobileCard.tsx +++ b/src/lib/components/table/transactions/TransactionsTableMobileCard.tsx @@ -13,11 +13,13 @@ import { RelationChip } from "./RelationChip"; interface TransactionsTableMobileCardProps { transaction: Transaction; + showSuccess: boolean; showRelations: boolean; showTimestamp: boolean; } export const TransactionsTableMobileCard = ({ transaction, + showSuccess, showRelations, showTimestamp, }: TransactionsTableMobileCardProps) => { @@ -33,10 +35,14 @@ export const TransactionsTableMobileCard = ({ topContent={ <> - {transaction.success ? ( - - ) : ( - + {showSuccess && ( + <> + {transaction.success ? ( + + ) : ( + + )} + )} - - {transaction.success ? ( - - ) : ( - - )} - + {showSuccess && ( + + {transaction.success ? ( + + ) : ( + + )} + + )} @@ -85,14 +89,20 @@ export const TransactionsTableRow = ({ /> {showTimestamp && ( - - - {formatUTC(transaction.created)} - - {`(${dateFromNow(transaction.created)})`} - - - + <> + {transaction.created ? ( + + + {formatUTC(transaction.created)} + + {`(${dateFromNow(transaction.created)})`} + + + + ) : ( + N/A + )} + )} {showAction && ( diff --git a/src/lib/hooks/useSingleMessageProps.ts b/src/lib/hooks/useSingleMessageProps.ts index 1fc7ec91a..337906eed 100644 --- a/src/lib/hooks/useSingleMessageProps.ts +++ b/src/lib/hooks/useSingleMessageProps.ts @@ -1,7 +1,7 @@ import router from "next/router"; import type { GetAddressTypeByLengthFn } from "lib/app-provider"; -import { useGetAddressTypeByLength } from "lib/app-provider"; +import { useGetAddressTypeByLength, useTierConfig } from "lib/app-provider"; import type { SingleMsgProps } from "lib/components/action-msg/SingleMsg"; import type { LinkType } from "lib/components/ExplorerLink"; import { useContractStore } from "lib/providers/store"; @@ -55,6 +55,7 @@ const instantiateSingleMsgProps = ( isInstantiate2: boolean, getAddressTypeByLength: GetAddressTypeByLengthFn ) => { + // TODO: need to handle undefined case const detail = messages[0].detail as DetailInstantiate; // TODO - revisit, instantiate detail response when query from contract transaction table doesn't contain contract addr const contractAddress = @@ -598,11 +599,16 @@ export const useSingleActionMsgProps = ( messages: Message[], singleMsg: Option ): SingleMsgProps => { + const isFullTier = useTierConfig() === "full"; const { getContractLocalInfo } = useContractStore(); const { data: assetInfos } = useAssetInfos({ withPrices: false }); const { data: movePoolInfos } = useMovePoolInfos({ withPrices: false }); const getAddressTypeByLength = useGetAddressTypeByLength(); + // HACK: to prevent the error when message.detail is undefined + // TODO: revist and support custom message detail on lite tier later + if (!isFullTier) return otherMessageSingleMsgProps(isSuccess, messages, type); + switch (type) { case "MsgExecuteContract": return executeSingleMsgProps( diff --git a/src/lib/pages/block-details/components/BlockInfo.tsx b/src/lib/pages/block-details/components/BlockInfo.tsx index 950457186..9482d7bd6 100644 --- a/src/lib/pages/block-details/components/BlockInfo.tsx +++ b/src/lib/pages/block-details/components/BlockInfo.tsx @@ -1,6 +1,6 @@ import { Box, Flex, Heading } from "@chakra-ui/react"; -import { useCelatoneApp } from "lib/app-provider"; +import { useCelatoneApp, useTierConfig } from "lib/app-provider"; import { LabelText } from "lib/components/LabelText"; import { ValidatorBadge } from "lib/components/ValidatorBadge"; import type { BlockData } from "lib/types"; @@ -12,6 +12,7 @@ interface BlockInfoProps { export const BlockInfo = ({ blockData }: BlockInfoProps) => { const { currentChainId } = useCelatoneApp(); + const isFullTier = useTierConfig() === "full"; return ( @@ -19,25 +20,43 @@ export const BlockInfo = ({ blockData }: BlockInfoProps) => { Block Info - + {isFullTier ? ( + + + + {currentChainId} + + + {`${blockData.gasUsed ? formatInteger(blockData.gasUsed) : 0} / ${ + blockData.gasLimit ? formatInteger(blockData.gasLimit) : 0 + }`} + + + + + + + ) : ( {currentChainId} - - {`${blockData.gasUsed ? formatInteger(blockData.gasUsed) : 0} / ${ - blockData.gasLimit ? formatInteger(blockData.gasLimit) : 0 - }`} + + - - - - + )} ); }; diff --git a/src/lib/pages/block-details/components/BlockTxsTable.tsx b/src/lib/pages/block-details/components/BlockTxsTableFull.tsx similarity index 95% rename from src/lib/pages/block-details/components/BlockTxsTable.tsx rename to src/lib/pages/block-details/components/BlockTxsTableFull.tsx index 3e766899c..811fbc4a0 100644 --- a/src/lib/pages/block-details/components/BlockTxsTable.tsx +++ b/src/lib/pages/block-details/components/BlockTxsTableFull.tsx @@ -10,7 +10,7 @@ interface BlockTxsTableProps { height: number; } -export const BlockTxsTable = ({ height }: BlockTxsTableProps) => { +export const BlockTxsTableFull = ({ height }: BlockTxsTableProps) => { const { pagesQuantity, setTotalData, @@ -43,6 +43,7 @@ export const BlockTxsTable = ({ height }: BlockTxsTableProps) => { withBorder /> } + showSuccess showRelations={false} showTimestamp={false} /> diff --git a/src/lib/pages/block-details/components/BlockTxsTableLite.tsx b/src/lib/pages/block-details/components/BlockTxsTableLite.tsx new file mode 100644 index 000000000..0d30829d6 --- /dev/null +++ b/src/lib/pages/block-details/components/BlockTxsTableLite.tsx @@ -0,0 +1,32 @@ +import { EmptyState } from "lib/components/state"; +import { TableTitle, TransactionsTable } from "lib/components/table"; +import { useBlockDataLcd } from "lib/services/block"; + +interface BlockTxsTableProps { + height: number; +} + +export const BlockTxsTableLite = ({ height }: BlockTxsTableProps) => { + const { data, isLoading } = useBlockDataLcd(height); + const total = data?.transactions.length; + + return ( + <> + + + } + showSuccess={false} + showRelations={false} + showTimestamp={false} + /> + + ); +}; diff --git a/src/lib/pages/block-details/components/InvalidBlock.tsx b/src/lib/pages/block-details/components/InvalidBlock.tsx new file mode 100644 index 000000000..3cb41cd2a --- /dev/null +++ b/src/lib/pages/block-details/components/InvalidBlock.tsx @@ -0,0 +1,3 @@ +import { InvalidState } from "lib/components/state"; + +export const InvalidBlock = () => ; diff --git a/src/lib/pages/block-details/components/index.ts b/src/lib/pages/block-details/components/index.ts index 0f62f4ffe..0c0b2da0c 100644 --- a/src/lib/pages/block-details/components/index.ts +++ b/src/lib/pages/block-details/components/index.ts @@ -1,3 +1,4 @@ export * from "./BlockDetailsTop"; export * from "./BlockInfo"; -export * from "./BlockTxsTable"; +export * from "./BlockTxsTableLite"; +export * from "./BlockTxsTableFull"; diff --git a/src/lib/pages/block-details/data.ts b/src/lib/pages/block-details/data.ts new file mode 100644 index 000000000..c360a8b48 --- /dev/null +++ b/src/lib/pages/block-details/data.ts @@ -0,0 +1,44 @@ +import { useMemo } from "react"; + +import { useBlockDataLcd } from "lib/services/block"; +import { useValidatorsLcd } from "lib/services/validator"; +import type { BlockData, Nullable, Option, Validator } from "lib/types"; + +export const useBlockDataWithValidatorLcd = ( + height: number +): { + data: Option; + isLoading: boolean; +} => { + const { data: blockData, isLoading: isLoadingBlockData } = + useBlockDataLcd(height); + const { data: validators, isLoading: isLoadingValidators } = + useValidatorsLcd(); + + const proposer = useMemo>(() => { + if (!blockData || !validators) return null; + + const found = validators.find( + (validator) => + validator.consensusAddress === blockData.proposerConsensusAddress + ); + + return found + ? { + validatorAddress: found.validatorAddress, + moniker: found.moniker, + identity: found.identity, + } + : null; + }, [blockData, validators]); + + return { + data: blockData + ? { + ...blockData.block, + proposer, + } + : undefined, + isLoading: isLoadingBlockData || isLoadingValidators, + }; +}; diff --git a/src/lib/pages/block-details/full.tsx b/src/lib/pages/block-details/full.tsx new file mode 100644 index 000000000..564aca523 --- /dev/null +++ b/src/lib/pages/block-details/full.tsx @@ -0,0 +1,32 @@ +import { Breadcrumb } from "lib/components/Breadcrumb"; +import { Loading } from "lib/components/Loading"; +import { UserDocsLink } from "lib/components/UserDocsLink"; +import { useBlockData } from "lib/services/block"; + +import { BlockDetailsTop, BlockInfo, BlockTxsTableFull } from "./components"; +import { InvalidBlock } from "./components/InvalidBlock"; + +export const BlockDetailsFull = ({ height }: { height: number }) => { + const { data: blockData, isLoading } = useBlockData(height); + + if (isLoading) return ; + if (!blockData) return ; + return ( + <> + + + + + + + ); +}; diff --git a/src/lib/pages/block-details/index.tsx b/src/lib/pages/block-details/index.tsx index 606544673..33aada463 100644 --- a/src/lib/pages/block-details/index.tsx +++ b/src/lib/pages/block-details/index.tsx @@ -2,44 +2,25 @@ import { useRouter } from "next/router"; import { useEffect } from "react"; import { AmpEvent, track } from "lib/amplitude"; -import { Breadcrumb } from "lib/components/Breadcrumb"; -import { Loading } from "lib/components/Loading"; +import { useTierConfig } from "lib/app-provider"; import PageContainer from "lib/components/PageContainer"; -import { InvalidState } from "lib/components/state"; -import { UserDocsLink } from "lib/components/UserDocsLink"; -import { useBlockData } from "lib/services/block"; -import { BlockDetailsTop, BlockInfo, BlockTxsTable } from "./components"; +import { InvalidBlock } from "./components/InvalidBlock"; +import { BlockDetailsFull } from "./full"; +import { BlockDetailsLite } from "./lite"; import { zBlockDetailQueryParams } from "./types"; -const InvalidBlock = () => ; - interface BlockDetailsBodyProps { height: number; } const BlockDetailsBody = ({ height }: BlockDetailsBodyProps) => { - const { data: blockData, isLoading } = useBlockData(height); + const isFullTier = useTierConfig() === "full"; - if (isLoading) return ; - if (!blockData) return ; - return ( - <> - - - - - - + return isFullTier ? ( + + ) : ( + ); }; diff --git a/src/lib/pages/block-details/lite.tsx b/src/lib/pages/block-details/lite.tsx new file mode 100644 index 000000000..a647d15a9 --- /dev/null +++ b/src/lib/pages/block-details/lite.tsx @@ -0,0 +1,46 @@ +import { Breadcrumb } from "lib/components/Breadcrumb"; +import { Loading } from "lib/components/Loading"; +import { EmptyState } from "lib/components/state"; +import { UserDocsLink } from "lib/components/UserDocsLink"; +import { useLatestBlockLcd } from "lib/services/block"; + +import { BlockDetailsTop, BlockInfo, BlockTxsTableLite } from "./components"; +import { InvalidBlock } from "./components/InvalidBlock"; +import { useBlockDataWithValidatorLcd } from "./data"; + +export const BlockDetailsLite = ({ height }: { height: number }) => { + const { data, isLoading } = useBlockDataWithValidatorLcd(height); + const { data: latestHeight, isLoading: isLatestHeightLoading } = + useLatestBlockLcd(); + + if (isLoading || isLatestHeightLoading) return ; + if (latestHeight && latestHeight > height && !data) + return ( + + ); + if (!data) return ; + return ( + <> + + + + + + + ); +}; diff --git a/src/lib/pages/past-txs/lite.tsx b/src/lib/pages/past-txs/lite.tsx index cd40343de..af235862b 100644 --- a/src/lib/pages/past-txs/lite.tsx +++ b/src/lib/pages/past-txs/lite.tsx @@ -45,6 +45,7 @@ export const PastTxsLite = () => { pageSize, offset, { + enabled: !!address, onSuccess: ({ total }) => setTotalData(total), } ); diff --git a/src/lib/services/block/index.ts b/src/lib/services/block/index.ts index 0c15751b9..60d663dbf 100644 --- a/src/lib/services/block/index.ts +++ b/src/lib/services/block/index.ts @@ -4,13 +4,18 @@ import type { UseQueryOptions } from "@tanstack/react-query"; import { CELATONE_QUERY_KEYS, useBaseApiRoute, + useCurrentChain, useLcdEndpoint, } from "lib/app-provider"; import type { BlocksResponse } from "lib/services/types"; -import type { BlockData } from "lib/types"; +import type { BlockData, ConsensusAddr, Transaction } from "lib/types"; +import { + convertAccountPubkeyToAccountAddress, + convertRawConsensusAddrToConsensusAddr, +} from "lib/utils"; import { getBlockData, getBlocks } from "./api"; -import { getLatestBlockLcd } from "./lcd"; +import { getBlockDataLcd, getLatestBlockLcd } from "./lcd"; export const useBlocks = ( limit: number, @@ -39,6 +44,40 @@ export const useBlockData = (height: number, enabled = true) => { ); }; +export const useBlockDataLcd = (height: number) => { + const endpoint = useLcdEndpoint(); + const { + chain: { bech32_prefix: prefix }, + } = useCurrentChain(); + + return useQuery<{ + block: BlockData; + proposerConsensusAddress: ConsensusAddr; + transactions: Transaction[]; + }>( + [CELATONE_QUERY_KEYS.BLOCK_DATA_LCD, endpoint, height], + async () => { + const { rawProposerConsensusAddress, transactions, ...rest } = + await getBlockDataLcd(endpoint, height); + return { + ...rest, + proposerConsensusAddress: convertRawConsensusAddrToConsensusAddr( + rawProposerConsensusAddress, + prefix + ), + transactions: transactions.map((tx) => ({ + ...tx, + sender: convertAccountPubkeyToAccountAddress(tx.signerPubkey, prefix), + })), + }; + }, + { + retry: false, + refetchOnWindowFocus: false, + } + ); +}; + export const useLatestBlockLcd = () => { const endpoint = useLcdEndpoint(); diff --git a/src/lib/services/block/lcd.ts b/src/lib/services/block/lcd.ts index 9c08cb7bb..ac4cf322d 100644 --- a/src/lib/services/block/lcd.ts +++ b/src/lib/services/block/lcd.ts @@ -1,9 +1,16 @@ import axios from "axios"; -import { zBlockLcd } from "../types"; +import { zBlockDataResponseLcd, zBlockLcd } from "../types"; import { parseWithError } from "lib/utils"; export const getLatestBlockLcd = async (endpoint: string) => axios .get(`${endpoint}/cosmos/base/tendermint/v1beta1/blocks/latest`) .then(({ data }) => parseWithError(zBlockLcd, data).block.header.height); + +export const getBlockDataLcd = async (endpoint: string, height: number) => + axios + .get( + `${endpoint}/cosmos/tx/v1beta1/txs/block/${encodeURIComponent(height)}` + ) + .then(({ data }) => parseWithError(zBlockDataResponseLcd, data)); diff --git a/src/lib/services/tx/api.ts b/src/lib/services/tx/api.ts index 2d513feb0..932a92ab1 100644 --- a/src/lib/services/tx/api.ts +++ b/src/lib/services/tx/api.ts @@ -6,7 +6,7 @@ import { zTxByHashResponseLcd, zTxsCountResponse, zTxsResponse, -} from "../types/tx"; +} from "../types"; import type { BechAddr, Option, TxFilters } from "lib/types"; import { camelToSnake, parseWithError } from "lib/utils"; diff --git a/src/lib/services/tx/index.ts b/src/lib/services/tx/index.ts index e4744935e..a3bf557cf 100644 --- a/src/lib/services/tx/index.ts +++ b/src/lib/services/tx/index.ts @@ -6,13 +6,13 @@ import type { AccountTxsResponse, BlockTxsResponse, TxData, - TxsByAddressResponseLcd, TxsResponse, -} from "../types/tx"; +} from "../types"; import { CELATONE_QUERY_KEYS, useBaseApiRoute, useCelatoneApp, + useCurrentChain, useInitia, useLcdEndpoint, useMoveConfig, @@ -25,9 +25,15 @@ import type { BechAddr20, BechAddr32, Option, + Transaction, TxFilters, } from "lib/types"; -import { extractTxLogs, isTxHash, snakeToCamel } from "lib/utils"; +import { + convertAccountPubkeyToAccountAddress, + extractTxLogs, + isTxHash, + snakeToCamel, +} from "lib/utils"; import { getTxData, @@ -238,11 +244,31 @@ export const useTxsByContractAddressLcd = ( address: BechAddr32, limit: number, offset: number, - options: UseQueryOptions = {} + options: UseQueryOptions = {} ) => { const endpoint = useLcdEndpoint(); + const { + chain: { bech32_prefix: prefix }, + } = useCurrentChain(); - return useQuery( + const queryfn = useCallback( + () => + getTxsByContractAddressLcd(endpoint, address, limit, offset).then( + (txs) => ({ + items: txs.items.map((tx) => ({ + ...tx, + sender: convertAccountPubkeyToAccountAddress( + tx.signerPubkey, + prefix + ), + })), + total: txs.total, + }) + ), + [address, endpoint, limit, offset, prefix] + ); + + return useQuery( [ CELATONE_QUERY_KEYS.TXS_BY_CONTRACT_ADDRESS_LCD, endpoint, @@ -250,7 +276,7 @@ export const useTxsByContractAddressLcd = ( limit, offset, ], - async () => getTxsByContractAddressLcd(endpoint, address, limit, offset), + queryfn, { retry: 1, refetchOnWindowFocus: false, ...options } ); }; @@ -284,34 +310,56 @@ export const useTxsByAddressLcd = ( search: string, limit: number, offset: number, - options: Pick, "onSuccess"> = {} + options: UseQueryOptions = {} ) => { const endpoint = useLcdEndpoint(); const { validateContractAddress } = useValidateAddress(); + const { + chain: { bech32_prefix: prefix }, + } = useCurrentChain(); - const queryfn = useCallback(() => { - if (isTxHash(search)) return getTxsByHashLcd(endpoint, search); + const queryfn = useCallback(async () => { + const txs = await (async () => { + if (isTxHash(search)) return getTxsByHashLcd(endpoint, search); - if (!validateContractAddress(search)) - return getTxsByContractAddressLcd( - endpoint, - search as BechAddr32, - offset, - limit - ); + if (!validateContractAddress(search)) + return getTxsByContractAddressLcd( + endpoint, + search as BechAddr32, + limit, + offset + ); - if (!address) throw new Error("address is undefined (useTxsByAddressLcd)"); - return getTxsByAccountAddressLcd(endpoint, address, limit, offset); - }, [address, endpoint, limit, offset, search, validateContractAddress]); + if (!address) + throw new Error("address is undefined (useTxsByAddressLcd)"); + return getTxsByAccountAddressLcd(endpoint, address, limit, offset); + })(); - return useQuery( + return { + items: txs.items.map((tx) => ({ + ...tx, + sender: convertAccountPubkeyToAccountAddress(tx.signerPubkey, prefix), + })), + total: txs.total, + }; + }, [ + address, + endpoint, + limit, + offset, + prefix, + search, + validateContractAddress, + ]); + + return useQuery( [ CELATONE_QUERY_KEYS.TXS_BY_ADDRESS_LCD, endpoint, address, search, - offset, limit, + offset, ], queryfn, { ...options, retry: 1, refetchOnWindowFocus: false } diff --git a/src/lib/services/types/block.ts b/src/lib/services/types/block.ts index f95cce17b..3e7926dde 100644 --- a/src/lib/services/types/block.ts +++ b/src/lib/services/types/block.ts @@ -1,8 +1,22 @@ import { z } from "zod"; -import type { Block, BlockData, Validator } from "lib/types"; -import { zUtcDate, zValidatorAddr } from "lib/types"; -import { parseTxHash, snakeToCamel } from "lib/utils"; +import type { + Block, + BlockData, + Message, + TransactionWithSignerPubkey, + Validator, +} from "lib/types"; +import { + ActionMsgType, + MsgFurtherAction, + zPagination, + zUtcDate, + zValidatorAddr, +} from "lib/types"; +import { createTxHash, parseTxHash, snakeToCamel } from "lib/utils"; + +import { zTx } from "./tx"; const zNullableValidator = z.nullable( z @@ -59,14 +73,76 @@ export const zBlockDataResponse = z gasLimit: val.gas_limit, })); -export const zBlockLcd = z - .object({ - block: z.object({ - header: z.object({ - chain_id: z.string(), - height: z.coerce.number(), - // TODO: Fill in the rest of the block fields - }), +export const zBlockLcd = z.object({ + block: z.object({ + header: z.object({ + chain_id: z.string(), + height: z.coerce.number(), + time: zUtcDate, + proposer_address: z.string(), + }), + data: z.object({ + txs: z.array(z.string()), }), + }), + block_id: z.object({ + hash: z.string(), + }), +}); + +export const zBlockDataResponseLcd = zBlockLcd + .extend({ + txs: z.array(zTx), + pagination: zPagination, }) - .transform(snakeToCamel); + .transform(snakeToCamel) + .transform<{ + block: BlockData; + rawProposerConsensusAddress: string; + transactions: TransactionWithSignerPubkey[]; + }>((val) => { + // 1. Create Tx Hashes + const txHashes = val.block.data.txs.map(createTxHash); + + // 2. Parse Tx to Transaction + const transactions = val.txs.map((tx, idx) => { + const txBody = tx.body; + + const messages = txBody.messages.map((msg) => ({ + log: undefined, + type: msg["@type"], + })); + + return { + hash: txHashes[idx], + messages, + signerPubkey: tx.authInfo.signerInfos[0].publicKey, + isSigner: true, + height: val.block.header.height, + created: val.block.header.time, + success: false, // NOTE: Hidden in Lite Tier + // TODO: implement below later + actionMsgType: ActionMsgType.OTHER_ACTION_MSG, + furtherAction: MsgFurtherAction.NONE, + isIbc: false, + isOpinit: false, + isInstantiate: false, + }; + }); + + // 3. Create Block Data + const block: BlockData = { + hash: Buffer.from(val.blockId.hash, "base64").toString("hex"), + height: val.block.header.height, + timestamp: val.block.header.time, + proposer: null, // NOTE: Will be filled in the next step + gasLimit: undefined, + gasUsed: undefined, + }; + + return { + block, + rawProposerConsensusAddress: val.block.header.proposerAddress, + transactions, + }; + }); diff --git a/src/lib/services/types/tx.ts b/src/lib/services/types/tx.ts index 09dac6406..024cb9e92 100644 --- a/src/lib/services/types/tx.ts +++ b/src/lib/services/types/tx.ts @@ -7,13 +7,18 @@ import type { } from "cosmjs-types/cosmos/tx/v1beta1/tx"; import { z } from "zod"; -import type { BechAddr, Message, Transaction } from "lib/types"; +import type { + Message, + Transaction, + TransactionWithSignerPubkey, +} from "lib/types"; import { ActionMsgType, MsgFurtherAction, zBechAddr, zCoin, zMessageResponse, + zPubkey, zUint8Schema, zUtcDate, } from "lib/types"; @@ -52,10 +57,7 @@ zModeInfo = z.object({ const zSignerInfo = z .object({ - public_key: z.object({ - "@type": z.string(), - key: z.string(), - }), + public_key: zPubkey, mode_info: zModeInfo.optional(), sequence: z.string(), }) @@ -88,9 +90,8 @@ const zTxBody = z .transform(snakeToCamel); export type TxBody = z.infer; -const zTx = z +export const zTx = z .object({ - "@type": z.string(), body: zTxBody, auth_info: zAuthInfo, signatures: z.array(z.string()), @@ -145,14 +146,9 @@ export interface TxData extends TxResponse { isTxFailed: boolean; } -export const zTxsResponseItemFromLcd = zTxResponse.transform( - (val) => { +export const zTxsResponseItemFromLcd = + zTxResponse.transform((val) => { const txBody = val.tx.body; - const message = txBody.messages[0]; - const sender = (message?.sender || - message?.signer || - message?.fromAddress || - "") as BechAddr; const logs = extractTxLogs(val); @@ -164,7 +160,7 @@ export const zTxsResponseItemFromLcd = zTxResponse.transform( return { hash: val.txhash, messages, - sender, + signerPubkey: val.tx.authInfo.signerInfos[0].publicKey, isSigner: true, height: Number(val.height), created: val.timestamp, @@ -176,8 +172,7 @@ export const zTxsResponseItemFromLcd = zTxResponse.transform( isOpinit: false, isInstantiate: false, }; - } -); + }); export const zTxsByAddressResponseLcd = z .object({ diff --git a/src/lib/services/types/validator.ts b/src/lib/services/types/validator.ts index 5bac97b7b..078dcf04d 100644 --- a/src/lib/services/types/validator.ts +++ b/src/lib/services/types/validator.ts @@ -1,12 +1,13 @@ import { z } from "zod"; -import type { ValidatorData } from "lib/types"; +import type { ConsensusPubkey, ValidatorData } from "lib/types"; import { BlockVote, SlashingEvent, zBechAddr, zBig, zCoin, + zConsensusPubkey, zPagination, zProposalStatus, zProposalType, @@ -20,10 +21,7 @@ import { parseTxHash, snakeToCamel, valoperToAddr } from "lib/utils"; const zValidatorInfoLcd = z .object({ operator_address: zValidatorAddr, - consensus_pubkey: z.object({ - "@type": z.string(), - key: z.string(), - }), + consensus_pubkey: zConsensusPubkey, jailed: z.boolean(), status: z.enum([ "BOND_STATUS_BONDED", @@ -51,7 +49,11 @@ const zValidatorInfoLcd = z }), min_self_delegation: z.string(), }) - .transform((val) => ({ + .transform< + Omit & { + consensusPubkey: ConsensusPubkey; + } + >((val) => ({ rank: null, validatorAddress: val.operator_address, accountAddress: valoperToAddr(val.operator_address), @@ -63,7 +65,9 @@ const zValidatorInfoLcd = z isActive: val.status === "BOND_STATUS_BONDED", votingPower: val.tokens, website: val.description.website, + consensusPubkey: val.consensus_pubkey, })); +export type ValidatorInfoLcd = z.infer; export const zValidatorResponseLcd = z.object({ validator: zValidatorInfoLcd, diff --git a/src/lib/services/validator/index.ts b/src/lib/services/validator/index.ts index e8aac8f03..8bcce06a3 100644 --- a/src/lib/services/validator/index.ts +++ b/src/lib/services/validator/index.ts @@ -29,6 +29,7 @@ import type { ValidatorAddr, ValidatorData, } from "lib/types"; +import { convertConsensusPubkeyToConsensusAddr } from "lib/utils"; import { getHistoricalPowers, @@ -83,10 +84,22 @@ export const useValidators = ( export const useValidatorsLcd = (enabled = true) => { const endpoint = useLcdEndpoint(); + const { + chain: { bech32_prefix: prefix }, + } = useCurrentChain(); return useQuery( [CELATONE_QUERY_KEYS.VALIDATORS_LCD, endpoint], - async () => getValidatorsLcd(endpoint), + async () => { + const res = await getValidatorsLcd(endpoint); + return res.map((val) => ({ + ...val, + consensusAddress: convertConsensusPubkeyToConsensusAddr( + val.consensusPubkey, + prefix + ), + })); + }, { enabled, retry: 1, @@ -117,10 +130,22 @@ export const useValidatorDataLcd = ( enabled = true ) => { const endpoint = useLcdEndpoint(); + const { + chain: { bech32_prefix: prefix }, + } = useCurrentChain(); return useQuery( [CELATONE_QUERY_KEYS.VALIDATOR_DATA_LCD, endpoint, validatorAddr], - async () => getValidatorDataLcd(endpoint, validatorAddr), + async () => { + const res = await getValidatorDataLcd(endpoint, validatorAddr); + return { + ...res, + consensusAddress: convertConsensusPubkeyToConsensusAddr( + res.consensusPubkey, + prefix + ), + }; + }, { enabled: enabled && Boolean(validatorAddr), retry: 1, diff --git a/src/lib/services/validator/lcd.ts b/src/lib/services/validator/lcd.ts index cc0f722c4..81486ce59 100644 --- a/src/lib/services/validator/lcd.ts +++ b/src/lib/services/validator/lcd.ts @@ -6,18 +6,21 @@ import { getEpochProvisionsLcd, getMintParamsLcd, } from "../staking/lcd"; -import type { ValidatorDelegatorsResponse } from "lib/services/types"; +import type { + ValidatorDelegatorsResponse, + ValidatorInfoLcd, +} from "lib/services/types"; import { zValidatorDelegatorsResponse, zValidatorResponseLcd, zValidatorsResponseLcd, } from "lib/services/types"; import { big } from "lib/types"; -import type { Nullable, ValidatorAddr, ValidatorData } from "lib/types"; +import type { Nullable, ValidatorAddr } from "lib/types"; import { parseWithError } from "lib/utils"; export const getValidatorsLcd = async (endpoint: string) => { - const result: ValidatorData[] = []; + const result: ValidatorInfoLcd[] = []; const fetchFn = async (paginationKey: Nullable) => { const res = await axios diff --git a/src/lib/types/account.ts b/src/lib/types/account.ts index e24662efb..c1095046e 100644 --- a/src/lib/types/account.ts +++ b/src/lib/types/account.ts @@ -1,3 +1,5 @@ +import { z } from "zod"; + export enum AccountType { BaseAccount = "BaseAccount", InterchainAccount = "InterchainAccount", @@ -10,3 +12,10 @@ export enum AccountType { PermanentLockedAccount = "PermanentLockedAccount", BaseVestingAccount = "BaseVestingAccount", } + +export const zPubkey = z.object({ + "@type": z.string(), + key: z.string(), +}); + +export type Pubkey = z.infer; diff --git a/src/lib/types/addrs.ts b/src/lib/types/addrs.ts index 5aadd7871..be2e0bb3c 100644 --- a/src/lib/types/addrs.ts +++ b/src/lib/types/addrs.ts @@ -34,3 +34,6 @@ export type Addr = z.infer; export const zValidatorAddr = z.string().brand("ValidatorAddr"); export type ValidatorAddr = z.infer; + +export const zConsensusAddr = z.string().brand("ConsensusAddr"); +export type ConsensusAddr = z.infer; diff --git a/src/lib/types/tx/transaction.ts b/src/lib/types/tx/transaction.ts index 60085740b..e076850b0 100644 --- a/src/lib/types/tx/transaction.ts +++ b/src/lib/types/tx/transaction.ts @@ -1,7 +1,7 @@ import type { Log } from "@cosmjs/stargate/build/logs"; import { z } from "zod"; -import type { BechAddr, Option } from "lib/types"; +import type { BechAddr, Option, Pubkey } from "lib/types"; export enum ActionMsgType { SINGLE_ACTION_MSG = "SINGLE_ACTION_MSG", @@ -41,6 +41,10 @@ export interface Transaction { isOpinit: boolean; } +export type TransactionWithSignerPubkey = Omit & { + signerPubkey: Pubkey; +}; + /* Filter for INITIA */ export interface InitiaTxFilters { isOpinit: boolean; diff --git a/src/lib/types/validator.ts b/src/lib/types/validator.ts index a34c041bc..25fcabad6 100644 --- a/src/lib/types/validator.ts +++ b/src/lib/types/validator.ts @@ -3,10 +3,11 @@ import { z } from "zod"; import { snakeToCamel } from "lib/utils/formatter/snakeToCamel"; import { formatUrl } from "lib/utils/formatter/url"; -import { zBechAddr20, zValidatorAddr } from "./addrs"; +import { zPubkey } from "./account"; +import { zBechAddr20, zConsensusAddr, zValidatorAddr } from "./addrs"; import { zBig } from "./big"; -import { zRatio } from "./currency"; import type { Ratio } from "./currency"; +import { zRatio } from "./currency"; export const zValidator = z .object({ @@ -26,6 +27,7 @@ export const zValidatorData = z rank: z.number().nullable(), validator_address: zValidatorAddr, account_address: zBechAddr20, + consensus_address: zConsensusAddr, identity: z.string(), moniker: z.string(), details: z.string(), @@ -42,6 +44,9 @@ export const zValidatorData = z })); export type ValidatorData = z.infer; +export const zConsensusPubkey = zPubkey; +export type ConsensusPubkey = z.infer; + export enum BlockVote { PROPOSE = "PROPOSE", VOTE = "VOTE", diff --git a/src/lib/utils/address.ts b/src/lib/utils/address.ts index 2828fa933..9e9b64579 100644 --- a/src/lib/utils/address.ts +++ b/src/lib/utils/address.ts @@ -1,7 +1,15 @@ -import { fromBech32, fromHex, toBech32, toHex } from "@cosmjs/encoding"; +import { Ripemd160, sha256 } from "@cosmjs/crypto"; +import { + fromBase64, + fromBech32, + fromHex, + toBech32, + toHex, +} from "@cosmjs/encoding"; import type { AddressReturnType } from "lib/app-provider"; -import type { BechAddr, HexAddr, Option } from "lib/types"; +import { zBechAddr20 } from "lib/types"; +import type { BechAddr, HexAddr, Option, Pubkey } from "lib/types"; import { sha256Hex } from "./sha256"; @@ -41,3 +49,22 @@ export const hexToBech32Address = ( const strip = padHexAddress(hexAddr, length).slice(2); return toBech32(prefix, fromHex(strip)) as BechAddr; }; + +export const convertAccountPubkeyToAccountAddress = ( + accountPubkey: Pubkey, + prefix: string +) => { + if (accountPubkey["@type"] === "/cosmos.crypto.ed25519.PubKey") { + const pubkey = fromBase64(accountPubkey.key); + const data = fromHex(toHex(sha256(pubkey)).slice(0, 40)); + return zBechAddr20.parse(toBech32(prefix, data)); + } + + if (accountPubkey["@type"] === "/cosmos.crypto.secp256k1.PubKey") { + const pubkey = fromBase64(accountPubkey.key); + const data = new Ripemd160().update(sha256(pubkey)).digest(); + return zBechAddr20.parse(toBech32(prefix, data)); + } + + return zBechAddr20.parse(""); +}; diff --git a/src/lib/utils/index.ts b/src/lib/utils/index.ts index 7afd8c2dd..16c7296fd 100644 --- a/src/lib/utils/index.ts +++ b/src/lib/utils/index.ts @@ -1,4 +1,5 @@ export * from "./address"; +export * from "./array"; export * from "./assetValue"; export * from "./base64"; export * from "./bech32"; @@ -36,6 +37,6 @@ export * from "./truncate"; export * from "./tx"; export * from "./txHash"; export * from "./validate"; +export * from "./validator"; export * from "./window"; export * from "./zod"; -export * from "./array"; diff --git a/src/lib/utils/tx/__test__/createTxHash.test.ts b/src/lib/utils/tx/__test__/createTxHash.test.ts new file mode 100644 index 000000000..14c9c9fc2 --- /dev/null +++ b/src/lib/utils/tx/__test__/createTxHash.test.ts @@ -0,0 +1,13 @@ +import { createTxHash } from "../createTxHash"; + +describe("createTxHash", () => { + test("success", () => { + expect( + createTxHash( + "CpUCCpICChovaW5pdGlhLm1vdmUudjEuTXNnRXhlY3V0ZRLzAQoraW5pdDFwZ2d3bTZua3F6djdqbWdmOGtzZnc5Mzdza2Ezc3NjODRuOTIyMBIqMHhFNjA2MDgxMTVDRjE2QzhBN0UwNzkwMjEwQzBBNjcyMTVCN0JFQzA5GhdleHRlcm5hbF9pbnRlbnRfbWFuYWdlciIsdmVyaWZ5X2Nsb3NlX3Bvc2l0aW9uX3RyYW5zZmVyX2ludGVudF9maWxsZWQyJSRhYThjNjg0Ni04OGJjLTRmYjAtODYxZS01ZDgyZWYwYjQ4NjYyIPTuyaaQLdM3TWJy5evGnv+8yN+DKDxzy/fGo97NVMUBMgjaVuUAAAAAABJaClIKRgofL2Nvc21vcy5jcnlwdG8uc2VjcDI1NmsxLlB1YktleRIjCiECDycOMnG0mAWpG2gBCPgq1NBZ263AGLCMGzX3OuQQZi8SBAoCCAEYu7wCEgQQqYgUGkBGZNRBO0VYqen0E4GTpdFZ5tJVdjPlFQ8cHhbzZYmAlRUiaMOaQLhQaLhmVdoMpy7HlN/4BWSPHn923pgi3xvq" + ) + ).toEqual( + "5BCB87ACD981F366183B3624CE552294012216167DDCEBA5BE9717969AA48249" + ); + }); +}); diff --git a/src/lib/utils/tx/__test__/extractTxLogs.example.ts b/src/lib/utils/tx/__test__/extractTxLogs.example.ts index cd2e74ee8..f7e003afa 100644 --- a/src/lib/utils/tx/__test__/extractTxLogs.example.ts +++ b/src/lib/utils/tx/__test__/extractTxLogs.example.ts @@ -271,7 +271,6 @@ export const fromLogs: TestCase = { '[{"events":[{"type":"message","attributes":[{"key":"action","value":"/cosmwasm.wasm.v1.MsgMigrateContract"},{"key":"module","value":"wasm"},{"key":"sender","value":"osmo18rf2vketuhfvrw0n986mghms33ahm884wsrfsj"}]},{"type":"migrate","attributes":[{"key":"code_id","value":"17"},{"key":"_contract_address","value":"osmo1cvtzwsj8lam9at8vfgxfnveyzjne8eecqjnsh6k3jvt3l7ve6zms3wtpd0"}]}]}]', timestamp: parseDate("2023-06-29T06:09:47Z"), tx: { - "@type": "/cosmos.tx.v1beta1.Tx", authInfo: { fee: { amount: [ @@ -464,7 +463,6 @@ export const fromLogsTxFailed: TestCase = { "failed to execute message; message index: 0: was (1000uosmo), need (400000000uosmo): minimum deposit is too small", timestamp: parseDate("2022-09-06T09:03:15Z"), tx: { - "@type": "/cosmos.tx.v1beta1.Tx", authInfo: { fee: { amount: [ @@ -750,7 +748,6 @@ export const fromEvents: TestCase = { rawLog: "", timestamp: parseDate("2024-01-16T16:51:20Z"), tx: { - "@type": "/cosmos.tx.v1beta1.Tx", authInfo: { fee: { amount: [ @@ -1023,7 +1020,6 @@ export const fromEventsTxFailed: TestCase = { "failed to execute message; message index: 0: VM failure: status OUT_OF_GAS of type Execution, location=0000000000000000000000002ab506311ffe3aaf8871f84a7ba8a685e025dbba::ed25519, function=1, code_offset=12", timestamp: parseDate("2024-01-26T07:05:00Z"), tx: { - "@type": "/cosmos.tx.v1beta1.Tx", authInfo: { fee: { amount: [ diff --git a/src/lib/utils/tx/createTxHash.ts b/src/lib/utils/tx/createTxHash.ts new file mode 100644 index 000000000..fed502543 --- /dev/null +++ b/src/lib/utils/tx/createTxHash.ts @@ -0,0 +1,5 @@ +import { sha256 } from "@cosmjs/crypto"; +import { toHex } from "@cosmjs/encoding"; + +export const createTxHash = (txRaw: string) => + toHex(sha256(Buffer.from(txRaw, "base64"))).toUpperCase(); diff --git a/src/lib/utils/tx/index.ts b/src/lib/utils/tx/index.ts index 4fc6d9e62..7b63d2246 100644 --- a/src/lib/utils/tx/index.ts +++ b/src/lib/utils/tx/index.ts @@ -1,4 +1,5 @@ export * from "./composeMsg"; +export * from "./createTxHash"; export * from "./extractTxDetails"; export * from "./extractTxLogs"; export * from "./findAttr"; diff --git a/src/lib/utils/validator.ts b/src/lib/utils/validator.ts new file mode 100644 index 000000000..30784749d --- /dev/null +++ b/src/lib/utils/validator.ts @@ -0,0 +1,32 @@ +import { Ripemd160, sha256 } from "@cosmjs/crypto"; +import { fromBase64, fromHex, toBech32, toHex } from "@cosmjs/encoding"; + +import type { ConsensusPubkey } from "lib/types"; +import { zConsensusAddr } from "lib/types"; + +export const convertRawConsensusAddrToConsensusAddr = ( + rawConsensusAddr: string, + prefix: string +) => { + const data = fromBase64(rawConsensusAddr); + return zConsensusAddr.parse(toBech32(`${prefix}valcons`, data)); +}; + +export const convertConsensusPubkeyToConsensusAddr = ( + consensusPubkey: ConsensusPubkey, + prefix: string +) => { + if (consensusPubkey["@type"] === "/cosmos.crypto.ed25519.PubKey") { + const pubkey = fromBase64(consensusPubkey.key); + const data = fromHex(toHex(sha256(pubkey)).slice(0, 40)); + return zConsensusAddr.parse(toBech32(`${prefix}valcons`, data)); + } + + if (consensusPubkey["@type"] === "/cosmos.crypto.secp256k1.PubKey") { + const pubkey = fromBase64(consensusPubkey.key); + const data = new Ripemd160().update(sha256(pubkey)).digest(); + return zConsensusAddr.parse(toBech32(`${prefix}valcons`, data)); + } + + return zConsensusAddr.parse(""); +};