diff --git a/CHANGELOG.md b/CHANGELOG.md index 9c2e635e9..b8be846d5 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 +- [#1009](https://github.com/alleslabs/celatone-frontend/pull/1009) Add Sequencer for account details page +- [#1006](https://github.com/alleslabs/celatone-frontend/pull/1006) Add Sequencer for past txs +- [#994](https://github.com/alleslabs/celatone-frontend/pull/994) Add Sequencer, Mesa tier and TierSwitcher component - [#1005](https://github.com/alleslabs/celatone-frontend/pull/1005) Support recent blocks, block details in sequencer tier ### Improvements diff --git a/src/lib/app-provider/env.ts b/src/lib/app-provider/env.ts index b6f2207ab..4d94d752e 100644 --- a/src/lib/app-provider/env.ts +++ b/src/lib/app-provider/env.ts @@ -100,6 +100,7 @@ export enum CELATONE_QUERY_KEYS { TXS_BY_ADDRESS = "CELATONE_QUERY_TXS_BY_ADDRESS", TXS_COUNT_BY_ADDRESS = "CELATONE_QUERY_TXS_COUNT_BY_ADDRESS", TXS_BY_ADDRESS_LCD = "CELATONE_QUERY_TXS_BY_ADDRESS_LCD", + TXS_BY_ADDRESS_SEQUENCER = "CELATONE_QUERY_TXS_BY_ADDRESS_SEQUENCER", TXS_BY_ACCOUNT_ADDRESS_LCD = "CELATONE_QUERY_TXS_BY_ACCOUNT_ADDRESS_LCD", TXS_BY_CONTRACT_ADDRESS_LCD = "CELATONE_QUERY_TXS_BY_CONTRACT_ADDRESS_LCD", TXS = "CELATONE_QUERY_TXS", diff --git a/src/lib/components/action-msg/SingleMsg.tsx b/src/lib/components/action-msg/SingleMsg.tsx index 6c83c3f20..3d9a84859 100644 --- a/src/lib/components/action-msg/SingleMsg.tsx +++ b/src/lib/components/action-msg/SingleMsg.tsx @@ -48,7 +48,15 @@ export const SingleMsg = ({ token={token} // TODO: add `ampCopierSection` later /> - {index < tokens.length - 2 ? , : and} + {tokens.length > 1 && ( + <> + {index < tokens.length - 2 ? ( + , + ) : ( + and + )} + + )} ))} {/* Tags */} diff --git a/src/lib/pages/account-details/components/tables/txs/Full.tsx b/src/lib/pages/account-details/components/tables/txs/full.tsx similarity index 100% rename from src/lib/pages/account-details/components/tables/txs/Full.tsx rename to src/lib/pages/account-details/components/tables/txs/full.tsx diff --git a/src/lib/pages/account-details/components/tables/txs/index.tsx b/src/lib/pages/account-details/components/tables/txs/index.tsx index 8d9b59f06..dce33e3b3 100644 --- a/src/lib/pages/account-details/components/tables/txs/index.tsx +++ b/src/lib/pages/account-details/components/tables/txs/index.tsx @@ -1,12 +1,14 @@ import { TierSwitcher } from "lib/components/TierSwitcher"; -import { TxsTableFull } from "./Full"; -import { TxsTableLite } from "./Lite"; +import { TxsTableFull } from "./full"; +import { TxsTableLite } from "./lite"; +import { TxsTableSequencer } from "./sequencer"; import type { TxsTableProps } from "./types"; export const TxsTable = (props: TxsTableProps) => ( } + sequencer={} lite={} /> ); diff --git a/src/lib/pages/account-details/components/tables/txs/Lite.tsx b/src/lib/pages/account-details/components/tables/txs/lite.tsx similarity index 100% rename from src/lib/pages/account-details/components/tables/txs/Lite.tsx rename to src/lib/pages/account-details/components/tables/txs/lite.tsx diff --git a/src/lib/pages/account-details/components/tables/txs/sequencer.tsx b/src/lib/pages/account-details/components/tables/txs/sequencer.tsx new file mode 100644 index 000000000..10b5b4e3f --- /dev/null +++ b/src/lib/pages/account-details/components/tables/txs/sequencer.tsx @@ -0,0 +1,83 @@ +import { Box, Flex } from "@chakra-ui/react"; + +import { useMobile, useTierConfig } from "lib/app-provider"; +import { LoadNext } from "lib/components/LoadNext"; +import { EmptyState, ErrorFetching } from "lib/components/state"; +import { + MobileTitle, + TableTitle, + TransactionsTable, + ViewMore, +} from "lib/components/table"; +import { useTxsByAddressSequencer } from "lib/services/tx"; +import type { BechAddr20 } from "lib/types"; + +import type { TxsTableProps } from "./types"; + +export const TxsTableSequencer = ({ address, onViewMore }: TxsTableProps) => { + const isMobile = useMobile(); + const { isFullTier } = useTierConfig(); + + const { + data, + error, + fetchNextPage, + hasNextPage, + isLoading, + isFetchingNextPage, + } = useTxsByAddressSequencer( + address as BechAddr20, + undefined, + onViewMore ? 5 : 10 + ); + + const isMobileOverview = isMobile && !!onViewMore; + + return ( + + {isMobileOverview ? ( + + ) : ( + + + {!isMobileOverview && ( + + ) : ( + + ) + } + showRelations + /> + )} + {hasNextPage && ( + <> + {onViewMore ? ( + + ) : ( + + )} + + )} + + )} + + ); +}; diff --git a/src/lib/pages/past-txs/index.tsx b/src/lib/pages/past-txs/index.tsx index 9c366a582..6ee1e4c45 100644 --- a/src/lib/pages/past-txs/index.tsx +++ b/src/lib/pages/past-txs/index.tsx @@ -6,6 +6,7 @@ import { TierSwitcher } from "lib/components/TierSwitcher"; import { PastTxsFull } from "./full"; import { PastTxsLite } from "./lite"; +import { PastTxsSequencer } from "./sequencer"; const PastTxs = () => { const router = useRouter(); @@ -14,7 +15,13 @@ const PastTxs = () => { if (router.isReady) track(AmpEvent.TO_PAST_TXS); }, [router.isReady]); - return } lite={} />; + return ( + } + sequencer={} + lite={} + /> + ); }; export default PastTxs; diff --git a/src/lib/pages/past-txs/lite.tsx b/src/lib/pages/past-txs/lite.tsx index ab7a1cbf0..1bf4e3a05 100644 --- a/src/lib/pages/past-txs/lite.tsx +++ b/src/lib/pages/past-txs/lite.tsx @@ -2,7 +2,7 @@ import { Flex, Heading } from "@chakra-ui/react"; import type { ChangeEvent } from "react"; import { useEffect, useState } from "react"; -import { useCurrentChain, useWasmConfig } from "lib/app-provider"; +import { useCurrentChain } from "lib/app-provider"; import InputWithIcon from "lib/components/InputWithIcon"; import PageContainer from "lib/components/PageContainer"; import { Pagination } from "lib/components/pagination"; @@ -16,22 +16,18 @@ import { useTxsByAddressLcd } from "lib/services/tx"; interface PastTxsLiteTransactionsTableWithWalletEmptyStateProps { search: string; - isWasmEnabled: boolean; error: unknown; } const PastTxsLiteTransactionsTableWithWalletEmptyState = ({ search, - isWasmEnabled, error, }: PastTxsLiteTransactionsTableWithWalletEmptyStateProps) => { if (search.trim().length > 0) return ( ); @@ -48,7 +44,6 @@ const PastTxsLiteTransactionsTableWithWalletEmptyState = ({ }; export const PastTxsLite = () => { - const isWasmEnabled = useWasmConfig({ shouldRedirect: false }).enabled; const { address, chain: { chain_id: chainId }, @@ -113,9 +108,7 @@ export const PastTxsLite = () => { { emptyState={ } diff --git a/src/lib/pages/past-txs/sequencer.tsx b/src/lib/pages/past-txs/sequencer.tsx new file mode 100644 index 000000000..07c75b62c --- /dev/null +++ b/src/lib/pages/past-txs/sequencer.tsx @@ -0,0 +1,111 @@ +import { Flex, Heading } from "@chakra-ui/react"; +import { useEffect, useState } from "react"; + +import { useCurrentChain } from "lib/app-provider"; +import InputWithIcon from "lib/components/InputWithIcon"; +import { LoadNext } from "lib/components/LoadNext"; +import PageContainer from "lib/components/PageContainer"; +import { CelatoneSeo } from "lib/components/Seo"; +import { EmptyState, ErrorFetching } from "lib/components/state"; +import { TransactionsTableWithWallet } from "lib/components/table"; +import { UserDocsLink } from "lib/components/UserDocsLink"; +import { useDebounce } from "lib/hooks"; +import { useTxsByAddressSequencer } from "lib/services/tx"; + +interface PastTxsSequencerTransactionsTableWithWalletEmptyStateProps { + search: string; + error: unknown; +} + +const PastTxsSequencerTransactionsTableWithWalletEmptyState = ({ + search, + error, +}: PastTxsSequencerTransactionsTableWithWalletEmptyStateProps) => { + if (search.trim().length > 0) + return ( + + ); + + if (error) return ; + + return ( + + ); +}; + +export const PastTxsSequencer = () => { + const { + address, + chain: { chain_id: chainId }, + } = useCurrentChain(); + + const [search, setSearch] = useState(""); + const debouncedSearch = useDebounce(search); + + const { + data, + error, + fetchNextPage, + hasNextPage, + isLoading, + isFetchingNextPage, + } = useTxsByAddressSequencer(address, debouncedSearch); + + useEffect(() => { + setSearch(""); + }, [chainId, address]); + + return ( + + + + + Past Transactions + + + + + setSearch(e.target.value)} + size={{ base: "md", md: "lg" }} + amptrackSection="past-txs-search" + /> + + + } + showActions={false} + showRelations + /> + {hasNextPage && ( + + )} + + ); +}; diff --git a/src/lib/services/tx/index.ts b/src/lib/services/tx/index.ts index 0501092c1..9f0894958 100644 --- a/src/lib/services/tx/index.ts +++ b/src/lib/services/tx/index.ts @@ -17,7 +17,6 @@ import { useLcdEndpoint, useMoveConfig, useTierConfig, - useValidateAddress, useWasmConfig, } from "lib/app-provider"; import { createQueryFnWithTimeout } from "lib/query-utils"; @@ -49,7 +48,11 @@ import { getTxsByContractAddressLcd, getTxsByHashLcd, } from "./lcd"; -import { getTxsByBlockHeightSequencer } from "./sequencer"; +import { + getTxsByAccountAddressSequencer, + getTxsByBlockHeightSequencer, + getTxsByHashSequencer, +} from "./sequencer"; export const useTxData = ( txHash: Option, @@ -291,23 +294,33 @@ export const useTxsByAddressLcd = ( options: UseQueryOptions = {} ) => { const endpoint = useLcdEndpoint(); - const { validateContractAddress } = useValidateAddress(); const { chain: { bech32_prefix: prefix }, } = useCurrentChain(); + // eslint-disable-next-line sonarjs/cognitive-complexity const queryfn = useCallback(async () => { const txs = await (async () => { - if (search && isTxHash(search)) return getTxsByHashLcd(endpoint, search); + if (search && isTxHash(search)) { + const txsByHash = await getTxsByHashLcd(endpoint, search); - if (search && !validateContractAddress(search)) - return getTxsByContractAddressLcd( - endpoint, - search as BechAddr32, - limit, - offset + if (txsByHash.total === 0) + throw new Error("transaction not found (getTxsByHashLcd)"); + + const tx = txsByHash.items[0]; + const sender = convertAccountPubkeyToAccountAddress( + tx.signerPubkey, + prefix ); + if (address === sender) return txsByHash; + + throw new Error("address is not equal to sender (getTxsByHashLcd)"); + } + + if (search && !isTxHash(search)) + throw new Error("search is not a tx hash (useTxsByAddressLcd)"); + if (!address) throw new Error("address is undefined (useTxsByAddressLcd)"); return getTxsByAccountAddressLcd(endpoint, address, limit, offset); @@ -320,15 +333,7 @@ export const useTxsByAddressLcd = ( })), total: txs.total, }; - }, [ - address, - endpoint, - limit, - offset, - prefix, - search, - validateContractAddress, - ]); + }, [address, endpoint, limit, offset, prefix, search]); return useQuery( [ @@ -344,6 +349,96 @@ export const useTxsByAddressLcd = ( ); }; +export const useTxsByAddressSequencer = ( + address: Option, + search: Option, + limit = 10 +) => { + const endpoint = useLcdEndpoint(); + const { + chain: { bech32_prefix: prefix }, + } = useCurrentChain(); + + const queryfn = useCallback( + // eslint-disable-next-line sonarjs/cognitive-complexity + async (pageParam: Option) => { + return (async () => { + if (search && isTxHash(search)) { + const txsByHash = await getTxsByHashSequencer(endpoint, search); + + if (txsByHash.pagination.total === 0) + throw new Error("transaction not found (getTxsByHashSequencer)"); + + const tx = txsByHash.items[0]; + const sender = convertAccountPubkeyToAccountAddress( + tx.signerPubkey, + prefix + ); + + if (address === sender) return txsByHash; + + const findAddressFromEvents = tx.events?.some((event) => + event.attributes.some((attr) => attr.value === address) + ); + + if (findAddressFromEvents) return txsByHash; + + throw new Error( + "transaction is not related (useTxsByAddressSequncer)" + ); + } + + if (search && !isTxHash(search)) + throw new Error("search is not a tx hash (useTxsByAddressSequncer)"); + + if (!address) + throw new Error("address is undefined (useTxsByAddressSequncer)"); + + return getTxsByAccountAddressSequencer( + endpoint, + address, + pageParam, + limit + ); + })(); + }, + [address, endpoint, prefix, search, limit] + ); + + const { data, ...rest } = useInfiniteQuery( + [ + CELATONE_QUERY_KEYS.TXS_BY_ADDRESS_SEQUENCER, + endpoint, + address, + search, + limit, + ], + ({ pageParam }) => queryfn(pageParam), + { + getNextPageParam: (lastPage) => lastPage.pagination.nextKey ?? undefined, + refetchOnWindowFocus: false, + } + ); + + return { + ...rest, + data: data?.pages.flatMap((page) => + page.items.map((item) => { + const sender = convertAccountPubkeyToAccountAddress( + item.signerPubkey, + prefix + ); + + return { + ...item, + sender, + isSigner: sender === address, + }; + }) + ), + }; +}; + export const useTxsByBlockHeightSequencer = (height: number) => { const endpoint = useLcdEndpoint(); const { diff --git a/src/lib/services/tx/lcd.ts b/src/lib/services/tx/lcd.ts index ca5b19a47..4a75b2d83 100644 --- a/src/lib/services/tx/lcd.ts +++ b/src/lib/services/tx/lcd.ts @@ -20,21 +20,19 @@ export const getTxsByHashLcd = async (endpoint: string, txHash: string) => export const getTxsByContractAddressLcd = async ( endpoint: string, - address: BechAddr32, + contractAddress: BechAddr32, limit: number, offset: number ) => axios - .get( - `${endpoint}/cosmos/tx/v1beta1/txs?events=wasm._contract_address=%27${encodeURI(address)}%27`, - { - params: { - order_by: 2, - limit, - page: offset / limit + 1, - }, - } - ) + .get(`${endpoint}/cosmos/tx/v1beta1/txs`, { + params: { + order_by: 2, + limit, + page: offset / limit + 1, + events: `wasm._contract_address='${encodeURI(contractAddress)}'`, + }, + }) .then(({ data }) => parseWithError(zTxsByAddressResponseLcd, data)); export const getTxsByAccountAddressLcd = async ( diff --git a/src/lib/services/tx/sequencer.ts b/src/lib/services/tx/sequencer.ts index 31bfbf7e1..12990fec7 100644 --- a/src/lib/services/tx/sequencer.ts +++ b/src/lib/services/tx/sequencer.ts @@ -1,9 +1,34 @@ import axios from "axios"; -import { zBlockTxsResponseSequencer } from "../types"; -import type { Option } from "lib/types"; +import { + zBlockTxsResponseSequencer, + zTxsByAddressResponseSequencer, + zTxsByHashResponseSequencer, +} from "../types"; +import type { BechAddr20, Option } from "lib/types"; import { parseWithError } from "lib/utils"; +export const getTxsByAccountAddressSequencer = async ( + endpoint: string, + address: BechAddr20, + paginationKey: Option, + limit = 10 +) => + axios + .get(`${endpoint}/indexer/tx/v1/txs/by_account/${encodeURI(address)}`, { + params: { + "pagination.limit": limit, + "pagination.reverse": true, + "pagination.key": paginationKey, + }, + }) + .then(({ data }) => parseWithError(zTxsByAddressResponseSequencer, data)); + +export const getTxsByHashSequencer = async (endpoint: string, txHash: string) => + axios + .get(`${endpoint}/indexer/tx/v1/txs/${encodeURI(txHash)}`) + .then(({ data }) => parseWithError(zTxsByHashResponseSequencer, data)); + export const getTxsByBlockHeightSequencer = async ( endpoint: string, height: number, diff --git a/src/lib/services/types/tx.ts b/src/lib/services/types/tx.ts index 2a8dbea36..d1984d52c 100644 --- a/src/lib/services/types/tx.ts +++ b/src/lib/services/types/tx.ts @@ -175,6 +175,7 @@ export const zTxsResponseItemFromLcd = isIbc: false, isOpinit: false, isInstantiate: false, + events: val.events, }; }); @@ -189,6 +190,19 @@ export const zTxsByAddressResponseLcd = z })); export type TxsByAddressResponseLcd = z.infer; +export const zTxsByAddressResponseSequencer = z + .object({ + txs: z.array(zTxsResponseItemFromLcd), + pagination: zPagination, + }) + .transform((val) => ({ + items: val.txs, + pagination: val.pagination, + })); +export type TxsByAddressResponseSequencer = z.infer< + typeof zTxsByAddressResponseSequencer +>; + export const zTxsByHashResponseLcd = z .object({ tx_response: zTxsResponseItemFromLcd, @@ -198,6 +212,21 @@ export const zTxsByHashResponseLcd = z total: 1, })); +export const zTxsByHashResponseSequencer = z + .object({ + tx: zTxsResponseItemFromLcd, + }) + .transform((val) => ({ + items: [val.tx], + pagination: { + total: val.tx ? 1 : 0, + nextKey: null, + }, + })); +export type TxsByHashResponseSequencer = z.infer< + typeof zTxsByHashResponseSequencer +>; + export const zTxByHashResponseLcd = z .object({ tx_response: zTxResponse, diff --git a/src/lib/services/types/wasm/contract.ts b/src/lib/services/types/wasm/contract.ts index fe7a8fea7..ac1fbaa0f 100644 --- a/src/lib/services/types/wasm/contract.ts +++ b/src/lib/services/types/wasm/contract.ts @@ -222,14 +222,16 @@ export const zInstantiatedContractsLcd = z export const zContractCw2InfoLcd = z .object({ - data: z.string(), + data: z.string().nullable(), }) - .transform((val) => JSON.parse(decode(val.data))) + .transform((val) => (val.data ? JSON.parse(decode(val.data)) : null)) .pipe( - z.object({ - contract: z.string(), - version: z.string(), - }) + z + .object({ + contract: z.string(), + version: z.string(), + }) + .nullable() ); export type ContractCw2InfoLcd = z.infer; diff --git a/src/lib/types/tx/transaction.ts b/src/lib/types/tx/transaction.ts index f40801df4..4a5b9e5a6 100644 --- a/src/lib/types/tx/transaction.ts +++ b/src/lib/types/tx/transaction.ts @@ -1,6 +1,7 @@ import type { Log } from "@cosmjs/stargate/build/logs"; import { z } from "zod"; +import type { Event } from "lib/services/types"; import type { BechAddr, Option, Pubkey } from "lib/types"; export enum ActionMsgType { @@ -39,6 +40,7 @@ export interface Transaction { isIbc: boolean; isInstantiate: boolean; isOpinit: boolean; + events?: Event[]; } export type TransactionWithSignerPubkey = Omit & {