From 1e011e7600d86b959b396f28977c3ba712bb4b30 Mon Sep 17 00:00:00 2001 From: bkioshn Date: Fri, 20 Jan 2023 15:51:12 +0700 Subject: [PATCH 01/17] fix: refactor pastTxs page, add new messages, change filter action section --- .../components/action-msg/ActionMessages.tsx | 36 ++ src/lib/components/action-msg/SingleMsg.tsx | 2 +- src/lib/components/button/RedoButton.tsx | 35 ++ src/lib/components/button/ResendButton.tsx | 33 + src/lib/components/table/MsgDetail.tsx | 77 ++- src/lib/hooks/useSingleMessageProps.ts | 134 +++- .../tables/transactions/TxsTableRow.tsx | 42 +- .../pages/past-txs/components/FalseState.tsx | 35 -- .../past-txs/components/FilterSelection.tsx | 219 +++++++ .../components/FurtherActionButton.tsx | 29 + .../pages/past-txs/components/PastTxRow.tsx | 106 ++++ .../pages/past-txs/components/PastTxTable.tsx | 580 ------------------ .../past-txs/components/PastTxsContent.tsx | 89 +++ .../components/PastTxsTableHeader.tsx | 21 + src/lib/pages/past-txs/hooks/useRedo.ts | 9 +- src/lib/pages/past-txs/hooks/useResend.ts | 67 ++ src/lib/pages/past-txs/index.tsx | 366 +++-------- src/lib/pages/past-txs/query/generateWhere.ts | 82 +++ src/lib/pages/past-txs/query/graphqlQuery.ts | 225 ++----- src/lib/pages/past-txs/query/useTxQuery.ts | 310 ++++++---- src/lib/types/tx/transaction.ts | 46 +- src/lib/utils/extractActionValue.ts | 22 + src/lib/utils/index.ts | 2 + src/lib/utils/msgFurtherAction.ts | 22 + 24 files changed, 1298 insertions(+), 1291 deletions(-) create mode 100644 src/lib/components/action-msg/ActionMessages.tsx create mode 100644 src/lib/components/button/RedoButton.tsx create mode 100644 src/lib/components/button/ResendButton.tsx delete mode 100644 src/lib/pages/past-txs/components/FalseState.tsx create mode 100644 src/lib/pages/past-txs/components/FilterSelection.tsx create mode 100644 src/lib/pages/past-txs/components/FurtherActionButton.tsx create mode 100644 src/lib/pages/past-txs/components/PastTxRow.tsx delete mode 100644 src/lib/pages/past-txs/components/PastTxTable.tsx create mode 100644 src/lib/pages/past-txs/components/PastTxsContent.tsx create mode 100644 src/lib/pages/past-txs/components/PastTxsTableHeader.tsx create mode 100644 src/lib/pages/past-txs/hooks/useResend.ts create mode 100644 src/lib/pages/past-txs/query/generateWhere.ts create mode 100644 src/lib/utils/extractActionValue.ts create mode 100644 src/lib/utils/msgFurtherAction.ts diff --git a/src/lib/components/action-msg/ActionMessages.tsx b/src/lib/components/action-msg/ActionMessages.tsx new file mode 100644 index 000000000..1f20a1684 --- /dev/null +++ b/src/lib/components/action-msg/ActionMessages.tsx @@ -0,0 +1,36 @@ +import type { AllTransaction, PastTransaction } from "lib/types"; +import { ActionMsgType } from "lib/types"; +import { extractMsgType } from "lib/utils"; + +import { MultipleActionsMsg } from "./MultipleActionsMsg"; +import { SingleActionMsg } from "./SingleActionMsg"; +import { SingleMsg } from "./SingleMsg"; + +export const RenderActionsMessages = ({ + transaction, +}: { + transaction: AllTransaction | PastTransaction; +}) => { + if (transaction.actionMsgType === ActionMsgType.SINGLE_ACTION_MSG) { + return ( + + ); + } + if (transaction.actionMsgType === ActionMsgType.MULTIPLE_ACTION_MSG) { + return ; + } + return ( + + ); +}; diff --git a/src/lib/components/action-msg/SingleMsg.tsx b/src/lib/components/action-msg/SingleMsg.tsx index 6c0416f39..9080b9175 100644 --- a/src/lib/components/action-msg/SingleMsg.tsx +++ b/src/lib/components/action-msg/SingleMsg.tsx @@ -58,7 +58,7 @@ export const SingleMsg = ({ +{length - tags.length} )} {/* Length */} - {!tags && length && {length}} + {!tags && length && {length}} {/* Text2 */} {text2} {/* Link */} diff --git a/src/lib/components/button/RedoButton.tsx b/src/lib/components/button/RedoButton.tsx new file mode 100644 index 000000000..2d1adf455 --- /dev/null +++ b/src/lib/components/button/RedoButton.tsx @@ -0,0 +1,35 @@ +import { Button } from "@chakra-ui/react"; +import { useWallet } from "@cosmos-kit/react"; +import { BsArrowCounterclockwise } from "react-icons/bs"; + +import { useRedo } from "lib/pages/past-txs/hooks/useRedo"; +import type { Message } from "lib/types"; +import { extractMsgType } from "lib/utils"; + +interface RedoButtonProps { + message: Message; +} + +export const RedoButton = ({ message }: RedoButtonProps) => { + const onClickRedo = useRedo(); + const { currentChainName } = useWallet(); + + return ( + + ); +}; diff --git a/src/lib/components/button/ResendButton.tsx b/src/lib/components/button/ResendButton.tsx new file mode 100644 index 000000000..de78c4154 --- /dev/null +++ b/src/lib/components/button/ResendButton.tsx @@ -0,0 +1,33 @@ +import { Button } from "@chakra-ui/react"; +import { useState } from "react"; + +import { FailedModal } from "lib/pages/instantiate/component"; +import { useResend } from "lib/pages/past-txs/hooks/useResend"; +import type { Message } from "lib/types"; + +interface ResendButtonProps { + messages: Message[]; +} +export const ResendButton = ({ messages }: ResendButtonProps) => { + const onClickResend = useResend(); + const [error, setError] = useState(""); + + const [isButtonLoading, setIsButtonLoading] = useState(false); + + return ( + <> + + {error && setError("")} />} + + ); +}; diff --git a/src/lib/components/table/MsgDetail.tsx b/src/lib/components/table/MsgDetail.tsx index b5746023d..c2178e69c 100644 --- a/src/lib/components/table/MsgDetail.tsx +++ b/src/lib/components/table/MsgDetail.tsx @@ -1,5 +1,10 @@ +import { SlideFade } from "@chakra-ui/react"; +import { useState } from "react"; + import { SingleActionMsg } from "../action-msg/SingleActionMsg"; import { AccordionStepperItem } from "lib/components/AccordionStepperItem"; +import { RedoButton } from "lib/components/button/RedoButton"; +import { ResendButton } from "lib/components/button/ResendButton"; import type { Message } from "lib/types"; import { extractMsgType } from "lib/utils"; @@ -7,27 +12,55 @@ import { TableRow } from "./tableComponents"; interface MsgDetailProps { message: Message; + allowFurtherAction: boolean; +} + +interface RenderButtonProps { + message: Message; } -export const MsgDetail = ({ message }: MsgDetailProps) => ( - - - - -); +const RenderButton = ({ message }: RenderButtonProps) => { + if ( + extractMsgType(message.type) === "MsgExecuteContract" || + extractMsgType(message.type) === "MsgInstantiateContract" + ) + return ; + + if (extractMsgType(message.type) === "MsgSend") + return ; + + return null; +}; + +export const MsgDetail = ({ message, allowFurtherAction }: MsgDetailProps) => { + const [showButton, setShowButton] = useState(false); + return ( + setShowButton(true)} + onMouseLeave={() => setShowButton(false)} + > + + + {allowFurtherAction && ( + + + + )} + + ); +}; diff --git a/src/lib/hooks/useSingleMessageProps.ts b/src/lib/hooks/useSingleMessageProps.ts index 1fbd97825..9419eb140 100644 --- a/src/lib/hooks/useSingleMessageProps.ts +++ b/src/lib/hooks/useSingleMessageProps.ts @@ -36,21 +36,23 @@ import { getExecuteMsgTags } from "lib/utils/executeTags"; const instantiateSingleMsgProps = ( isSuccess: boolean, messages: Message[], - getContractLocalInfo: (contractAddress: string) => Option + getContractLocalInfo: (contractAddress: string) => Option, + isInstantiate2: boolean ) => { const detail = messages[0].detail as DetailInstantiate; const contractLocalInfo = getContractLocalInfo(detail.contractAddress); + const type = isInstantiate2 ? "Instantiate2" : "Instantiate"; if (messages.length > 1) { return isSuccess ? { - type: "Instantiate", + type, length: messages.length, text2: "contracts", } : { type: "Failed", - text1: "to instantiate", + text1: `to ${type}`, length: messages.length, text2: "contracts", }; @@ -58,7 +60,7 @@ const instantiateSingleMsgProps = ( return isSuccess ? { - type: "Instantiate", + type, text1: "contract", link1: { type: "contract_address" as LinkType, @@ -73,7 +75,7 @@ const instantiateSingleMsgProps = ( } : { type: "Failed", - text1: "to instantiate contract from Code ID", + text1: `to ${type} contract from Code ID`, link1: { type: "code_id" as LinkType, value: detail.codeId.toString(), @@ -85,9 +87,13 @@ const instantiateSingleMsgProps = ( * Returns messages variations for MsgExecuteContract. * * @remarks - * More than 1 address: Execute [length] messages + * - More than 1 msg: + * With same contract addr: Execute [length] messages on [name \\ contract address] + * With different contract addr: Execute [length] messages * Only 1 address: Execute [msg1] [msg2] [+number] on [name || contract address] - * Fail with more than 1 msg: Failed to execute [length] messages + * Fail with more than 1 msg: + * With same contract addr: Failed to execute [length] messages on [name \\ contract address] + * With diff contract addr: Failed to execute [length] messages * Fail with 1 msg: Failed to execute message from [name || contract address] * * @param isSuccess - boolean of whether tx is succeed or not @@ -106,23 +112,48 @@ const executeSingleMsgProps = ( const detail = messages[0].detail as DetailExecute; const contractLocalInfo = getContractLocalInfo(detail.contract); - if ( - messages.some((msg) => { - const msgDetail = msg.detail as DetailExecute; - return msgDetail.contract !== detail.contract; - }) - ) { + if (messages.length > 1) { + if ( + messages.some((msg) => { + const msgDetail = msg.detail as DetailExecute; + return msgDetail.contract !== detail.contract; + }) + ) { + return isSuccess + ? { + type: "Execute", + length: messages.length, + text2: "messages", + } + : { + type: "Failed", + // eslint-disable-next-line sonarjs/no-duplicate-string + text1: "to execute", + length: messages.length, + text2: "messages", + }; + } return isSuccess ? { type: "Execute", length: messages.length, - text2: "messages", + text2: "messages on", + link2: { + type: "contract_address" as LinkType, + value: contractLocalInfo?.name || detail.contract, + copyValue: detail.contract, + }, } : { type: "Failed", text1: "to execute", length: messages.length, - text2: "messages", + text2: "messages on", + link2: { + type: "contract_address" as LinkType, + value: contractLocalInfo?.name || detail.contract, + copyValue: detail.contract, + }, }; } @@ -152,9 +183,13 @@ const executeSingleMsgProps = ( * Returns messages variations for MsgSend. * * @remarks - * More than 1 msg: Send assets to [length] addresses + * More than 1 msg: + * With same address: Send assets to [name \\ contract address/ user address] + * With different address: Send assets to [length] addresses * Only 1 msg: Send [amount] [denom] to [contract address / user address] - * Fail with more than 1 msg: Failed to send assets to [length] addresses + * Fail with more than 1 msg: + * With same address: Failed to send assets to [name \\ contract address/ user address] + * With diff address: Failed to send assets to [length] addresses * Fail with 1 msg: Failed to send assets to [contract address / user address] * * @param isSuccess - boolean of whether tx is succeed or not @@ -166,22 +201,58 @@ const executeSingleMsgProps = ( const sendSingleMsgProps = ( isSuccess: boolean, messages: Message[], - chainName: string + chainName: string, + getContractLocalInfo: (contractAddress: string) => Option ) => { const detail = messages[0].detail as DetailSend; + const contractLocalInfo = getContractLocalInfo(detail.toAddress); if (messages.length > 1) { + if ( + messages.some((msg) => { + const msgDetail = msg.detail as DetailExecute; + return msgDetail.contract !== detail.toAddress; + }) + ) { + return isSuccess + ? { + type: "Send ", + text1: "assets to", + length: messages.length, + text2: "addresses", + } + : { + type: "Failed", + // eslint-disable-next-line sonarjs/no-duplicate-string + text1: "to send assets to", + length: messages.length, + text2: "addresses", + }; + } return isSuccess ? { - type: "Send assets to", - length: messages.length, - text: "addresses", + type: "Send", + text1: "assets to", + link2: { + type: getAddressTypeByLength( + chainName, + detail.toAddress + ) as LinkType, + value: contractLocalInfo?.name || detail.toAddress, + copyValue: detail.toAddress, + }, } : { type: "Failed", text1: "to send assets to", - length: messages.length, - text2: "addresses", + link2: { + type: "contract_address" as LinkType, + value: getAddressTypeByLength( + chainName, + detail.toAddress + ) as LinkType, + copyValue: detail.toAddress, + }, }; } return isSuccess @@ -490,14 +561,27 @@ export const useSingleActionMsgProps = ( getContractLocalInfo ); case "MsgSend": - return sendSingleMsgProps(isSuccess, messages, currentChainName); + return sendSingleMsgProps( + isSuccess, + messages, + currentChainName, + getContractLocalInfo + ); case "MsgMigrateContract": return migrateSingleMsgProps(isSuccess, messages, getContractLocalInfo); case "MsgInstantiateContract": return instantiateSingleMsgProps( isSuccess, messages, - getContractLocalInfo + getContractLocalInfo, + false + ); + case "MsgInstantiateContract2": + return instantiateSingleMsgProps( + isSuccess, + messages, + getContractLocalInfo, + true ); case "MsgUpdateAdmin": return updateAdminSingleMsgProps( diff --git a/src/lib/pages/contract-details/components/tables/transactions/TxsTableRow.tsx b/src/lib/pages/contract-details/components/tables/transactions/TxsTableRow.tsx index 33f8bf0ed..ff79e69e0 100644 --- a/src/lib/pages/contract-details/components/tables/transactions/TxsTableRow.tsx +++ b/src/lib/pages/contract-details/components/tables/transactions/TxsTableRow.tsx @@ -10,44 +10,12 @@ import { import { useEffect, useState } from "react"; import { MdCheck, MdClose, MdKeyboardArrowDown } from "react-icons/md"; -import { MultipleActionsMsg } from "lib/components/action-msg/MultipleActionsMsg"; -import { SingleActionMsg } from "lib/components/action-msg/SingleActionMsg"; -import { SingleMsg } from "lib/components/action-msg/SingleMsg"; +import { RenderActionsMessages } from "lib/components/action-msg/ActionMessages"; import { ExplorerLink } from "lib/components/ExplorerLink"; import { TableRow } from "lib/components/table"; import { MsgDetail } from "lib/components/table/MsgDetail"; import type { AllTransaction } from "lib/types"; -import { ActionMsgType } from "lib/types"; -import { dateFromNow, extractMsgType, formatUTC } from "lib/utils"; - -const RenderActionsMessages = ({ - transaction, -}: { - transaction: AllTransaction; -}) => { - if (transaction.actionMsgType === ActionMsgType.SINGLE_ACTION_MSG) { - return ( - - ); - } - if (transaction.actionMsgType === ActionMsgType.MULTIPLE_ACTION_MSG) { - return ; - } - return ( - - ); -}; +import { dateFromNow, formatUTC } from "lib/utils"; interface TxsTableRowProps { transaction: AllTransaction; @@ -133,7 +101,11 @@ export const TxsTableRow = ({ {isAccordion && ( )} diff --git a/src/lib/pages/past-txs/components/FalseState.tsx b/src/lib/pages/past-txs/components/FalseState.tsx deleted file mode 100644 index 92ca2c4d3..000000000 --- a/src/lib/pages/past-txs/components/FalseState.tsx +++ /dev/null @@ -1,35 +0,0 @@ -import { Flex, Icon, Text } from "@chakra-ui/react"; -import { MdSearch, MdSearchOff } from "react-icons/md"; - -interface FalseStateProps { - icon: string; - text1: string; - text2: string; -} -export const FalseState = ({ icon, text1, text2 }: FalseStateProps) => { - return ( - - - - {text1} - - - - {text2} - - - ); -}; diff --git a/src/lib/pages/past-txs/components/FilterSelection.tsx b/src/lib/pages/past-txs/components/FilterSelection.tsx new file mode 100644 index 000000000..ddc66f6c5 --- /dev/null +++ b/src/lib/pages/past-txs/components/FilterSelection.tsx @@ -0,0 +1,219 @@ +import type { InputProps, LayoutProps } from "@chakra-ui/react"; +import { + FormControl, + FormHelperText, + Box, + FormLabel, + Tag, + Flex, + Input, + List, + ListItem, + Icon, + Text, + useOutsideClick, +} from "@chakra-ui/react"; +import { matchSorter } from "match-sorter"; +import { observer } from "mobx-react-lite"; +import type { CSSProperties } from "react"; +import { useState, useRef, forwardRef } from "react"; +import { MdCheck, MdClose } from "react-icons/md"; + +import { displayActionValue } from "lib/utils/extractActionValue"; +import mergeRefs from "lib/utils/mergeRefs"; + +export interface FilterSelectionProps extends InputProps { + placeholder?: string; + result: string[]; + setResult: (option: string, bool: boolean) => void; + helperText?: string; + labelBgColor?: string; + label?: string; + boxWidth?: LayoutProps["width"]; + boxHeight?: LayoutProps["height"]; +} + +const listItemProps: CSSProperties = { + borderRadius: "4px", + margin: "4px 0px", + padding: "8px", + cursor: "pointer", +}; + +const tagItemProps: CSSProperties = { + borderRadius: "24px", + cursor: "pointer", + whiteSpace: "nowrap", + alignItems: "center", + display: "flex", + textTransform: "none", + gap: "4px", + marginRight: "8px", +}; + +// TODO - Refactor this along with TagSelection +export const FilterSelection = observer( + forwardRef( + ( + { + result, + setResult, + placeholder, + helperText, + labelBgColor = "background.main", + label = "Filter by Actions", + boxWidth = "full", + boxHeight = "56px", + ...rest + }: FilterSelectionProps, + ref + ) => { + const options = [ + "isUpload", + "isInstantiate", + "isExecute", + "isSend", + "isIbc", + "isMigrate", + "isClearAdmin", + "isUpdateAdmin", + ]; + + const [partialResult, setPartialResult] = useState([]); + const [displayOptions, setDisplayOptions] = useState(false); + const inputRef = useRef(null); + const boxRef = useRef(null); + + const filterOptions = (value: string) => { + setDisplayOptions(true); + setPartialResult(value ? matchSorter(options, value) : options); + }; + + const isOptionSelected = (option: string) => + result.some((selectedOption) => selectedOption === option); + + const selectOption = (option: string) => { + if (isOptionSelected(option)) { + setResult(option, false); + } else { + setResult(option, true); + } + }; + + useOutsideClick({ + ref: boxRef, + handler: () => setDisplayOptions(false), + }); + + return ( + + + + {result.length > 0 && ( + + {[...result].reverse().map((option) => ( + selectOption(option)} + key={option} + > + + {displayActionValue(option)} + + + + ))} + + )} + + filterOptions(e.currentTarget.value)} + onFocus={() => { + setPartialResult(options); + setDisplayOptions(true); + }} + ref={mergeRefs([inputRef, ref])} + maxLength={36} + style={{ border: "0" }} + {...rest} + /> + + {label} + + + + {helperText} + + + {displayOptions && ( + + {/* option selection section */} + {partialResult.map((option) => ( + selectOption(option)} + > + + {displayActionValue(option)} + + {isOptionSelected(option) && ( + + )} + + + ))} + + )} + + + ); + } + ) +); diff --git a/src/lib/pages/past-txs/components/FurtherActionButton.tsx b/src/lib/pages/past-txs/components/FurtherActionButton.tsx new file mode 100644 index 000000000..c3c61fb75 --- /dev/null +++ b/src/lib/pages/past-txs/components/FurtherActionButton.tsx @@ -0,0 +1,29 @@ +import { RedoButton } from "lib/components/button/RedoButton"; +import { ResendButton } from "lib/components/button/ResendButton"; +import type { PastTransaction } from "lib/types"; +import { MsgFurtherAction } from "lib/types"; + +interface FurtherActionButtonProps { + transaction: PastTransaction; +} + +/** + * Render redo button, resend button, or nothing. + * + * @remarks + * Redo occurs for transaction that has only 1 message + * + */ +export const FurtherActionButton = ({ + transaction, +}: FurtherActionButtonProps) => { + if (transaction.furtherAction === MsgFurtherAction.RESEND) { + return ; + } + + if (transaction.furtherAction === MsgFurtherAction.REDO) { + return ; + } + + return null; +}; diff --git a/src/lib/pages/past-txs/components/PastTxRow.tsx b/src/lib/pages/past-txs/components/PastTxRow.tsx new file mode 100644 index 000000000..be0df6900 --- /dev/null +++ b/src/lib/pages/past-txs/components/PastTxRow.tsx @@ -0,0 +1,106 @@ +import { + Box, + Flex, + Grid, + Icon, + Tag, + Text, + useDisclosure, +} from "@chakra-ui/react"; +import { useEffect, useState } from "react"; +import { MdCheck, MdClose, MdKeyboardArrowDown } from "react-icons/md"; + +import { RenderActionsMessages } from "lib/components/action-msg/ActionMessages"; +import { ExplorerLink } from "lib/components/ExplorerLink"; +import { TableRow } from "lib/components/table"; +import { MsgDetail } from "lib/components/table/MsgDetail"; +import type { PastTransaction } from "lib/types"; +import { dateFromNow, formatUTC } from "lib/utils"; + +import { FurtherActionButton } from "./FurtherActionButton"; + +interface PastTxRowProps { + transaction: PastTransaction; + templateColumnsStyle: string; +} + +export const PastTxRow = ({ + transaction, + templateColumnsStyle, +}: PastTxRowProps) => { + const { isOpen, onToggle } = useDisclosure(); + const [isAccordion, setIsAccordion] = useState(false); + + useEffect(() => { + if (transaction.messages.length > 1) setIsAccordion(true); + }, [transaction.messages]); + + return ( + + + + + + + + + + + + {transaction.isIbc && ( + + IBC + + )} + + + + + + {formatUTC(transaction.created)} + + {`(${dateFromNow(transaction.created)})`} + + + + + + + + + {isAccordion && ( + + )} + + + {isAccordion && ( + + )} + + ); +}; diff --git a/src/lib/pages/past-txs/components/PastTxTable.tsx b/src/lib/pages/past-txs/components/PastTxTable.tsx deleted file mode 100644 index 4b3a28eb9..000000000 --- a/src/lib/pages/past-txs/components/PastTxTable.tsx +++ /dev/null @@ -1,580 +0,0 @@ -import { - Td, - Tr, - Flex, - Tag, - Text, - useDisclosure, - Collapse, - Button, - Icon, -} from "@chakra-ui/react"; -import type { EncodeObject } from "@cosmjs/proto-signing"; -import { useWallet } from "@cosmos-kit/react"; -import dayjs from "dayjs"; -import { useCallback, useMemo, useState } from "react"; -import type { MouseEvent } from "react"; -import { BsArrowCounterclockwise } from "react-icons/bs"; -import { MdCheck, MdClose, MdKeyboardArrowDown } from "react-icons/md"; - -import { useRedo } from "../hooks/useRedo"; -import { useFabricateFee, useSimulateFee } from "lib/app-provider"; -import { useResendTx } from "lib/app-provider/tx/resend"; -import type { SingleMsgProps } from "lib/components/action-msg/SingleMsg"; -import { SingleMsg } from "lib/components/action-msg/SingleMsg"; -import { ExplorerLink } from "lib/components/ExplorerLink"; -import { useContractStore } from "lib/hooks"; -import { FailedModal } from "lib/pages/instantiate/component"; -import { useTxBroadcast } from "lib/providers/tx-broadcast"; -import type { - DetailExecute, - DetailInstantiate, - DetailSend, - DetailUpload, - Token, - Transaction, - U, -} from "lib/types"; -import { - camelToSnake, - encode, - formatUDenom, - formatUToken, - extractMsgType, -} from "lib/utils"; - -import { MsgDetail } from "./MsgDetail"; -import type { MultipleMsgProps } from "./MultipleMsg"; -import { MultipleMsg } from "./MultipleMsg"; - -interface PastTxTableProps { - element: Transaction; -} - -const PastTxTable = ({ element }: PastTxTableProps) => { - const onClickRedo = useRedo(); - const { isOpen, onToggle } = useDisclosure(); - const [isAccordion, setIsAccordion] = useState(false); - const [button, setButton] = useState<"redo" | "resend" | "">(""); - const [isButtonLoading, setIsButtonLoading] = useState(false); - const [error, setError] = useState(""); - const { currentChainName } = useWallet(); - - const { getContractLocalInfo } = useContractStore(); - - const extractMessage = useCallback((data: Transaction) => { - const uploadMsgs: DetailUpload[] = []; - const executeMsgs: DetailExecute[] = []; - const instantiateMsgs: DetailInstantiate[] = []; - const sendMsgs: DetailSend[] = []; - setButton(""); - data.messages.forEach((msgs) => { - const type = extractMsgType(msgs.type); - // Case where msg does not failed - if (Object.keys(msgs.detail).length) { - switch (type) { - case "MsgInstantiateContract": { - const detailInstantiate = msgs.detail as DetailInstantiate; - instantiateMsgs.push(detailInstantiate); - break; - } - - case "MsgStoreCode": { - const detailUpload = msgs.detail as DetailUpload; - uploadMsgs.push(detailUpload); - break; - } - - case "MsgExecuteContract": { - const detailExecute = msgs.detail as DetailExecute; - executeMsgs.push(detailExecute); - break; - } - - case "MsgSend": { - const detailSend = msgs.detail as DetailSend; - sendMsgs.push(detailSend); - break; - } - default: - break; - } - } - }); - - return { uploadMsgs, executeMsgs, instantiateMsgs, sendMsgs } as const; - }, []); - - // TODO - Refactor - const renderUpload = useCallback( - (uploadMsgs: Array) => { - // Multiple Upload/Store code Msgs - if (uploadMsgs.length > 1) { - const multipleMsgProps: MultipleMsgProps = element.success - ? { - type: "Upload", - length: uploadMsgs.length, - text: "Wasm files", - } - : { - type: "Failed to upload", - length: uploadMsgs.length, - text: "Wasm files", - }; - return ; - } - - // Only 1 Upload/Store code Msg - const singleMsgProps: SingleMsgProps = element.success - ? { - type: "Upload", - text1: "Wasm to Code ID", - link1: { - type: "code_id", - value: uploadMsgs[0].id.toString(), - }, - } - : { - type: "Failed", - text1: "to upload Wasm file", - }; - return ; - }, - [element.success] - ); - - // TODO - Refactor - const renderInstantiate = useCallback( - (instantiateMsgs: Array) => { - // Multiple Instantiate Msgs - if (instantiateMsgs.length > 1) { - setIsAccordion(true); - setButton("resend"); - if (!element.success) { - setButton(""); - - return ( - - ); - } - return ( - - ); - } - setButton("redo"); - const contractLocalInfo = getContractLocalInfo( - instantiateMsgs[0].contractAddress - ); - // Only 1 Instantiate Msgs - const singleMsgProps: SingleMsgProps = element.success - ? { - type: "Instantiate", - text1: "contract", - link1: { - type: "contract_address", - value: - contractLocalInfo?.name || instantiateMsgs[0].contractAddress, - copyValue: instantiateMsgs[0].contractAddress, - }, - text3: "from Code ID", - link2: { - type: "code_id", - value: instantiateMsgs[0].codeId.toString(), - }, - } - : { - type: "Failed", - text1: "to instantiate contract from Code ID", - link1: { - type: "code_id", - value: instantiateMsgs[0].codeId.toString(), - }, - }; - return ; - }, - [element.success, getContractLocalInfo] - ); - - // TODO - Refactor - const renderExecute = useCallback( - (executeMsgs: Array) => { - const tags = [Object.keys(executeMsgs[0].msg)[0]]; - const contractAddress = executeMsgs[0].contract; - if (executeMsgs.length > 1) { - tags.push(Object.keys(executeMsgs[1].msg)[0]); - } - - setIsAccordion(true); - setButton("resend"); - // Multiple Execute msgs - if (executeMsgs.some((msg) => msg.contract !== contractAddress)) { - if (!element.success) { - setButton(""); - return ( - - ); - } - - return ( - - ); - } - if (executeMsgs.length === 1) { - setButton("redo"); - setIsAccordion(false); - } - - const contractLocalInfo = getContractLocalInfo(executeMsgs[0].contract); - - // Only 1 Execute Msg - const singleMsgProps: SingleMsgProps = element.success - ? { - type: "Execute", - tags, - length: executeMsgs.length, - text2: "on", - link1: { - type: "contract_address", - value: contractLocalInfo?.name || executeMsgs[0].contract, - copyValue: executeMsgs[0].contract, - }, - } - : { - type: "Failed", - text1: "to execute message from", - link1: { - type: "contract_address", - value: contractLocalInfo?.name || executeMsgs[0].contract, - copyValue: executeMsgs[0].contract, - }, - }; - return ; - }, - [element.success, getContractLocalInfo] - ); - - // TODO - Refactor - const renderSend = useCallback( - (sendMsgs: Array) => { - setButton("resend"); - // Multiple Send msgs - if (sendMsgs.length > 1) { - setIsAccordion(true); - if (!element.success) { - setButton(""); - return ( - - ); - } - return ( - - ); - } - - // Only 1 Send msg -> Resend - if (!element.success) { - setButton(""); - return ; - } - const coins = sendMsgs[0].amount.map( - (amount) => - `${formatUToken(amount.amount as U)} ${formatUDenom( - amount.denom - )}` - ); - return ( - - ); - }, - [element.success] - ); - - // TODO - Have to refator the way to display text - // eslint-disable-next-line complexity - const displayInfo = useMemo(() => { - // Reset accordion display - setIsAccordion(false); - const { uploadMsgs, executeMsgs, instantiateMsgs, sendMsgs } = - extractMessage(element); - if ( - uploadMsgs.length !== 0 && - executeMsgs.length === 0 && - instantiateMsgs.length === 0 && - sendMsgs.length === 0 - ) { - return renderUpload(uploadMsgs); - } - if ( - instantiateMsgs.length !== 0 && - uploadMsgs.length === 0 && - executeMsgs.length === 0 && - sendMsgs.length === 0 - ) { - return renderInstantiate(instantiateMsgs); - } - if ( - executeMsgs.length !== 0 && - instantiateMsgs.length === 0 && - uploadMsgs.length === 0 && - sendMsgs.length === 0 - ) { - return renderExecute(executeMsgs); - } - if ( - sendMsgs.length !== 0 && - executeMsgs.length === 0 && - instantiateMsgs.length === 0 && - uploadMsgs.length === 0 - ) { - return renderSend(sendMsgs); - } - // Combine all - setIsAccordion(true); - // Disable resend when transaction contains upload - if (uploadMsgs.length === 0 && element.success) { - setButton("resend"); - } - - return ( - <> - {/* Execute */} - {executeMsgs.length !== 0 && ( - <> - Execute {executeMsgs.length} - - )}{" "} - {/* Instantiate */} - {instantiateMsgs.length !== 0 && ( - <> - Instantiate {instantiateMsgs.length} - - )}{" "} - {/* Upload */} - {uploadMsgs.length !== 0 && ( - <> - Upload {uploadMsgs.length} - - )}{" "} - {/* Send */} - {sendMsgs.length !== 0 && ( - <> - Send {sendMsgs.length} - - )} - - ); - }, [ - element, - extractMessage, - renderExecute, - renderInstantiate, - renderSend, - renderUpload, - ]); - - const hideBorder = isAccordion && isOpen ? "none" : ""; - - const fabricateFee = useFabricateFee(); - const { simulate } = useSimulateFee(); - const resendTx = useResendTx(); - const { broadcast } = useTxBroadcast(); - - // TODO - Redundant, refactor - const onClickResend = useCallback( - async (e: MouseEvent) => { - e.stopPropagation(); - - setIsButtonLoading(true); - const messages = [] as EncodeObject[]; - element.messages.forEach((msg) => { - if (msg.msg.msg) { - messages.push({ - typeUrl: msg.type, - value: { - ...msg.msg, - msg: encode(JSON.stringify(camelToSnake(msg.msg.msg))), - }, - }); - } else { - messages.push({ - typeUrl: msg.type, - value: { - ...msg.msg, - }, - }); - } - }); - try { - const estimatedGasUsed = await simulate(messages); - let fee; - if (estimatedGasUsed) { - fee = fabricateFee(estimatedGasUsed); - } - const stream = await resendTx({ - onTxSucceed: () => {}, - estimatedFee: fee, - messages, - }); - if (stream) broadcast(stream); - setIsButtonLoading(false); - } catch (err) { - setError((err as Error).message); - setIsButtonLoading(false); - } - - return null; - }, - [element.messages, simulate, resendTx, broadcast, fabricateFee] - ); - - const renderTimestamp = () => { - const localDate = element.block.timestamp.concat("Z"); - return ( - - - {dayjs(localDate).utc().format("MMM DD, YYYY, h:mm:ss A [UTC]")} - - - ({dayjs(localDate).fromNow()}) - - - ); - }; - - return ( - <> - - - - - - - - {element.success ? ( - - ) : ( - - )} - - - - {displayInfo} - {element.isIbc && ( - - IBC - - )} - - - {renderTimestamp()} - - - {button === "redo" && ( - - )} - {button === "resend" && ( - - )} - - - - {isAccordion && ( - - )} - - - {isAccordion && ( - - - - - {element.messages.map((item, index) => ( - - ))} - - - - - )} - {error && setError("")} />} - - ); -}; - -export default PastTxTable; diff --git a/src/lib/pages/past-txs/components/PastTxsContent.tsx b/src/lib/pages/past-txs/components/PastTxsContent.tsx new file mode 100644 index 000000000..1fb17ebc0 --- /dev/null +++ b/src/lib/pages/past-txs/components/PastTxsContent.tsx @@ -0,0 +1,89 @@ +import { Flex } from "@chakra-ui/react"; +import { useWallet } from "@cosmos-kit/react"; +import { MdSearch, MdSearchOff } from "react-icons/md"; + +import { Loading } from "lib/components/Loading"; +import { DisconnectedState } from "lib/components/state/DisconnectedState"; +import { EmptyState } from "lib/components/state/EmptyState"; +import type { PastTransaction, Option } from "lib/types"; + +import { PastTxRow } from "./PastTxRow"; +import { PastTxsTableHeader } from "./PastTxsTableHeader"; + +interface PastTxsContentProps { + isLoading: boolean; + txDataError: unknown; + input: string; + filterSelected: string[]; + txData: Option; +} +export const PastTxsContent = ({ + isLoading, + txDataError, + input, + filterSelected, + txData, +}: PastTxsContentProps) => { + const { address } = useWallet(); + + const templateColumnsStyle = + "180px 70px minmax(300px, 1fr) max(300px) max(100px) max(70px)"; + + if (!address) { + return ( + + + + ); + } + + if (isLoading) { + return ; + } + + if ( + txDataError || + ((input !== "" || filterSelected.length !== 0) && !txData?.length) + ) { + return ( + + + + ); + } + + if (!txData?.length) { + return ( + + + + ); + } + return ( + + + {txData.map((transaction) => ( + + ))} + + ); +}; diff --git a/src/lib/pages/past-txs/components/PastTxsTableHeader.tsx b/src/lib/pages/past-txs/components/PastTxsTableHeader.tsx new file mode 100644 index 000000000..6f7d0e89f --- /dev/null +++ b/src/lib/pages/past-txs/components/PastTxsTableHeader.tsx @@ -0,0 +1,21 @@ +import type { GridProps } from "@chakra-ui/react"; +import { Grid } from "@chakra-ui/react"; + +import { TableHeader } from "lib/components/table"; + +export const PastTxsTableHeader = ({ + templateColumns, +}: { + templateColumns: GridProps["templateColumns"]; +}) => { + return ( + + Tx Hash + + Messages + Timestamp + + + + ); +}; diff --git a/src/lib/pages/past-txs/hooks/useRedo.ts b/src/lib/pages/past-txs/hooks/useRedo.ts index 61a211729..36c92e82d 100644 --- a/src/lib/pages/past-txs/hooks/useRedo.ts +++ b/src/lib/pages/past-txs/hooks/useRedo.ts @@ -1,7 +1,7 @@ import { useCallback } from "react"; import { useInternalNavigate } from "lib/app-provider"; -import type { Msg } from "lib/types"; +import type { Msg, Option } from "lib/types"; import { encode, camelToSnake } from "lib/utils"; export const useRedo = () => { @@ -10,7 +10,7 @@ export const useRedo = () => { return useCallback( ( e: React.MouseEvent, - type: string | undefined, + type: Option, msg: Msg, chainName: string ) => { @@ -22,7 +22,10 @@ export const useRedo = () => { pathname: "/execute", query: { chainName, contract: msg.contract, msg: encodeMsg }, }); - } else if (type === "MsgInstantiateContract") { + } else if ( + type === "MsgInstantiateContract" || + type === "MsgInstantiateContract2" + ) { const encodeMsg = encode(JSON.stringify(camelToSnake(msg))); navigate({ pathname: "/instantiate", diff --git a/src/lib/pages/past-txs/hooks/useResend.ts b/src/lib/pages/past-txs/hooks/useResend.ts new file mode 100644 index 000000000..eb84b6905 --- /dev/null +++ b/src/lib/pages/past-txs/hooks/useResend.ts @@ -0,0 +1,67 @@ +import type { EncodeObject } from "@cosmjs/proto-signing"; +import { useCallback } from "react"; + +import { useFabricateFee, useResendTx, useSimulateFee } from "lib/app-provider"; +import { useTxBroadcast } from "lib/providers/tx-broadcast"; +import type { Message } from "lib/types"; +import { camelToSnake, encode } from "lib/utils"; + +export const useResend = () => { + const fabricateFee = useFabricateFee(); + const { simulate } = useSimulateFee(); + const resendTx = useResendTx(); + const { broadcast } = useTxBroadcast(); + + return useCallback( + ( + e: React.MouseEvent, + messagesList: Message[], + setIsButtonLoading: (isButtonLoading: boolean) => void, + setError: (err: string) => void + ) => { + (async () => { + e.stopPropagation(); + setIsButtonLoading(true); + const messages = [] as EncodeObject[]; + messagesList.forEach((msg) => { + if (msg.msg.msg) { + messages.push({ + typeUrl: msg.type, + value: { + ...msg.msg, + msg: encode(JSON.stringify(camelToSnake(msg.msg.msg))), + }, + }); + } else { + messages.push({ + typeUrl: msg.type, + value: { + ...msg.msg, + }, + }); + } + }); + try { + const estimatedGasUsed = await simulate(messages); + let fee; + if (estimatedGasUsed) { + fee = fabricateFee(estimatedGasUsed); + } + const stream = await resendTx({ + onTxSucceed: () => {}, + estimatedFee: fee, + messages, + }); + if (stream) broadcast(stream); + setIsButtonLoading(false); + } catch (err) { + setError((err as Error).message); + setIsButtonLoading(false); + } + + return null; + })(); + }, + [broadcast, fabricateFee, resendTx, simulate] + ); +}; diff --git a/src/lib/pages/past-txs/index.tsx b/src/lib/pages/past-txs/index.tsx index 6f8571494..64c33bed1 100644 --- a/src/lib/pages/past-txs/index.tsx +++ b/src/lib/pages/past-txs/index.tsx @@ -1,47 +1,52 @@ import { - Button, + Box, Flex, Heading, + Icon, Input, - Text, InputGroup, InputRightElement, - Icon, - TableContainer, - Table, - Thead, - Tr, - Th, - Tbody, - Box, } from "@chakra-ui/react"; import { useWallet } from "@cosmos-kit/react"; import type { ChangeEvent } from "react"; -import { useMemo, useState, useEffect, useCallback } from "react"; +import { useMemo } from "react"; +import { useForm } from "react-hook-form"; import { MdSearch } from "react-icons/md"; -import { Loading } from "lib/components/Loading"; import { Pagination } from "lib/components/pagination"; import { usePaginator } from "lib/components/pagination/usePaginator"; -import { DisconnectedState } from "lib/components/state/DisconnectedState"; -import type { Transaction } from "lib/types/tx/transaction"; -import { FalseState } from "./components/FalseState"; -import PastTxTable from "./components/PastTxTable"; -import { useTxQuery } from "./query/useTxQuery"; +import { FilterSelection } from "./components/FilterSelection"; +import { PastTxsContent } from "./components/PastTxsContent"; +import { useTxQuery, useTxQueryCount } from "./query/useTxQuery"; const PastTxs = () => { - const [input, setInput] = useState(""); + const { address } = useWallet(); - // TODO Combine state - const [uploadButton, setUploadButton] = useState(false); - const [instantiateButton, setInstantiateButton] = useState(false); - const [executeButton, setExecuteButton] = useState(false); - const [ibcButton, setIbcButton] = useState(false); - const [sendButton, setSendButton] = useState(false); + const { watch, setValue } = useForm({ + defaultValues: { + input: "", + filters: { + isExecute: false, + isInstantiate: false, + isUpload: false, + isIbc: false, + isSend: false, + isMigrate: false, + isUpdateAdmin: false, + isClearAdmin: false, + }, + }, + mode: "all", + }); - const [data, setData] = useState(); - const [totalData, setTotalData] = useState(0); + const pastTxsState = watch(); + + const { data: countTxs = 0 } = useTxQueryCount( + address, + pastTxsState.input, + pastTxsState.filters + ); const { pagesQuantity, @@ -51,7 +56,7 @@ const PastTxs = () => { setPageSize, offset, } = usePaginator({ - total: totalData, + total: countTxs, initialState: { pageSize: 10, currentPage: 1, @@ -59,294 +64,89 @@ const PastTxs = () => { }, }); - const { address } = useWallet(); const { data: txData, - refetch: refetchTxData, error: txDataError, - isLoading: isTxDataLoading, + isLoading, } = useTxQuery( address, - executeButton, - instantiateButton, - uploadButton, - ibcButton, - sendButton, - input, + pastTxsState.input, + pastTxsState.filters, pageSize, offset ); + const setFilter = (filter: string, bool: boolean) => { + setValue("filters", { ...pastTxsState.filters, [filter]: bool }); + }; - // TODO Refactor useEffect - // Set data and total number of data when new data is passed - useEffect(() => { - if (txData) { - setData(txData.transactions); - setTotalData(txData.count); - } - }, [txData]); - - // Refetch data - useEffect(() => { - refetchTxData(); - }, [ - uploadButton, - instantiateButton, - executeButton, - ibcButton, - sendButton, - address, - pageSize, - offset, - refetchTxData, - ]); - - // When buttons are pressed, move back to the first page - useEffect(() => { - setCurrentPage(1); - }, [ - executeButton, - instantiateButton, - ibcButton, - sendButton, - uploadButton, - pageSize, - setCurrentPage, - ]); + const onPageChange = (nextPage: number) => { + setCurrentPage(nextPage); + }; - // Auto search when text input is filled after a period of time - useEffect(() => { - const timeoutId = setTimeout(() => { - refetchTxData(); - }, 600); - // Move back to the first page + const onPageSizeChange = (e: ChangeEvent) => { + const size = Number(e.target.value); + setPageSize(size); setCurrentPage(1); - return () => clearTimeout(timeoutId); - }, [input, refetchTxData, setCurrentPage]); - - // Display each row in table - const displayRow = useMemo(() => { - const displayComponents: JSX.Element[] = []; - if (data) { - data.forEach((element) => { - displayComponents.push( - - ); - }); - } - return displayComponents; - }, [data]); - - // Set input field - const onChangeSearch = (value: string) => { - setInput(value); }; - // Page change - const onPageChange = useCallback( - (nextPage: number) => { - setCurrentPage(nextPage); - }, - [setCurrentPage] - ); - - // Page Sizing - const onPageSizeChange = useCallback( - (e: ChangeEvent) => { - const size = Number(e.target.value); - setPageSize(size); - }, - [setPageSize] - ); - - // Determine which component should be shown - const displayContent = useMemo(() => { - const isSomeButtonPressed = - executeButton || - instantiateButton || - uploadButton || - sendButton || - ibcButton; - if (!address) { - return ( - - - - ); - } - // Loading state - if (isTxDataLoading) { - return ; - } - // No data found - if ( - (data?.length === 0 && (input !== "" || isSomeButtonPressed)) || - txDataError - ) { - return ( - - ); - } - - // No input data to search - if (data?.length === 0) { - return ( - - ); - } - - // Data found, display table - return ( - <> - - - - - - - - - - {displayRow} -
- Tx Hash - - - Messages - - Timestamp - - -
-
- - - ); - }, [ - address, - currentPage, - data?.length, - displayRow, - executeButton, - onPageChange, - onPageSizeChange, - ibcButton, - input, - instantiateButton, - isTxDataLoading, - offset, - pageSize, - pagesQuantity, - sendButton, - totalData, - txDataError, - uploadButton, - ]); + const filterSelected = useMemo(() => { + const filters: string[] = []; + Object.keys(pastTxsState.filters).forEach((key) => { + if (pastTxsState.filters[key as keyof typeof pastTxsState.filters]) { + filters.push(key); + } + }); + return filters; + }, [pastTxsState]); return ( - + Past Transactions + - + onChangeSearch(e.target.value)} + value={pastTxsState.input} + onChange={(e) => setValue("input", e.target.value)} placeholder="Search with transaction hash or contract address" focusBorderColor="primary.main" h="full" /> - + + - - - - Filter by actions - - - - - - - - - - - - {displayContent} - + + {countTxs > 10 && ( + + )} ); }; diff --git a/src/lib/pages/past-txs/query/generateWhere.ts b/src/lib/pages/past-txs/query/generateWhere.ts new file mode 100644 index 000000000..205d796c0 --- /dev/null +++ b/src/lib/pages/past-txs/query/generateWhere.ts @@ -0,0 +1,82 @@ +import type { Filters } from "lib/types"; + +/** + * Generate action filter for where clause used in graphql. Only return action that is true + * + * @example + * is_send: {_eq: true}, is_execute: {_eq: true} + * + */ +export const actionsFilter = (filters: Filters) => { + const actions = { + isExecute: "is_execute", + isInstantiate: "is_instantiate", + isUpload: "is_store_code", + isIbc: "is_ibc", + isSend: "is_send", + isMigrate: "is_migrate", + isUpdateAdmin: "is_update_admin", + isClearAdmin: "is_clear_admin", + }; + + let filter = ""; + Object.keys(filters).forEach((key) => { + if (filters[key as keyof typeof filters]) { + filter += `${actions[key as keyof Filters]}: {_eq: true },`; + } + }); + + return filter; +}; + +interface GenerateWhereForContractTx { + userAddress: string; + contractAddress: string; + filters: Filters; +} + +/** + * @remark + * For contract_transactions table + * + */ +export const generateWhereForContractTx = ({ + userAddress, + contractAddress, + filters, +}: GenerateWhereForContractTx) => { + const actionFilter = actionsFilter(filters); + return `{ + transaction: { + account: { address: { _eq: "${userAddress}" } }, + ${actionFilter !== "" ? `${actionFilter},` : ""} + } + ${ + contractAddress && `contract: { address: { _eq: "${contractAddress}" } },` + } + }`; +}; + +interface GenerateWhereForTx { + userAddress: string; + txHash?: string; + filters: Filters; +} + +/** + * @remark + * For transactions table + * + */ +export const generateWhereForTx = ({ + userAddress, + txHash, + filters, +}: GenerateWhereForTx) => { + const actionFilter = actionsFilter(filters); + return ` { + account: { address: { _eq: "${userAddress}" } }, + ${actionFilter !== "" ? `${actionFilter},` : ""} + ${txHash && `hash: {_eq: "\\\\x${txHash}"}, `} + }`; +}; diff --git a/src/lib/pages/past-txs/query/graphqlQuery.ts b/src/lib/pages/past-txs/query/graphqlQuery.ts index f04fc7d92..4b7d7e38b 100644 --- a/src/lib/pages/past-txs/query/graphqlQuery.ts +++ b/src/lib/pages/past-txs/query/graphqlQuery.ts @@ -1,180 +1,85 @@ import { gql } from "graphql-request"; -// Handle normal case where msg of none 4 main types are included -export const queryShowallFromTxs = (search: string) => { - const hash = `hash: {_eq: "\\\\x${search}"} `; +export const queryTransactionsFromTxs = (where: string) => { return gql` - query QueryShowAllFromTxs($userAddr: String = "", $pageSize: Int!, $offset: Int!) { - transactions( - where: { - account: { address: { _eq: $userAddr } }, - ${search !== "" ? `${hash}` : ""}, - _and: { - _or: [ - { is_execute: { _eq: true } } - { is_instantiate: { _eq: true } } - { - _and: [ - { is_ibc: { _eq: true} } - { - _or: [ - { is_execute: { _neq: false } } - { is_instantiate: { _neq: false } } - { is_store_code: { _neq: false } } - { is_send: { _neq: false } } - ] - } - ] - } - { is_store_code: { _eq: true } } - { is_send: { _eq: true } } - ] - } - - } - limit: $pageSize - offset: $offset - order_by: { block_height: desc } - ) { - hash - is_send - is_execute - is_ibc - is_instantiate - is_store_code - messages - success - block { + query QueryTransactionsFromTxs($pageSize: Int!, $offset: Int!) { + transactions( + where: ${where} + limit: $pageSize + offset: $offset + order_by: {block: {timestamp: desc}} + ) { + hash + is_send + is_execute + is_ibc + is_instantiate + is_store_code + is_clear_admin + is_migrate + is_update_admin + messages + success + block { timestamp - } - } - transactions_aggregate( - where: { - account: { - address: { _eq: $userAddr }, - transactions: { - _and: { - _or: [ - { is_execute: { _eq: true } } - { is_instantiate: { _eq: true } } - { is_store_code: { _eq: true } } - { - _and: [ - { is_ibc: { _eq: true} } - { - _or: [ - { is_execute: { _neq: false } } - { is_instantiate: { _neq: false } } - { is_store_code: { _neq: false } } - { is_send: { _neq: false } } - ] - } - ] - } - { is_send: { _eq: true } } - ] - } - } - } - ${search !== "" ? `${hash}` : ""}, - } - ) { - aggregate { - count - } - } - } - `; + } + } + + }`; }; -// Handle the case where action buttons are pressed -export const queryWithActionsFromTxs = ( - search: string, - actionsFilter: string -) => { - const hash = `hash: {_eq: "\\\\x${search}"} `; +export const queryTransactionsCountFromTxs = (where: string) => { return gql` - query QueryWithActionsFromTxs($userAddr: String!, $pageSize: Int!, $offset: Int!) { - transactions( - where: { account: { address: { _eq: $userAddr } }, - ${search !== "" ? `${hash}` : ""}, - ${actionsFilter} }, - limit: $pageSize, - offset: $offset, - order_by: { block_height: desc } - ) { - hash - is_send - is_execute - is_ibc - is_instantiate - is_store_code - messages - success - block { - timestamp - } + query QueryTransactionsCountFromTxs { + transactions_aggregate( + where: ${where} + ) { + aggregate { + count } - transactions_aggregate( - where: { - account: { - address: { _eq: $userAddr }, - } - ${actionsFilter} - ${search !== "" ? `${hash}` : ""}, - } - ) { - aggregate { - count - } - } - } - `; + } + }`; }; // Handle the case where contract address is searched -export const queryAddrFromContracts = (actionsFilter: string) => { +export const queryTransactionFromContractTxs = (where: string) => { return gql` - query QueryAddrFromContracts($userAddr: String!, $contractAddress: String!, $pageSize: Int!, $offset: Int!) { - contract_transactions( - where: { - transaction: { - account: { address: { _eq: $userAddr } }, - ${actionsFilter !== "" ? `${actionsFilter},` : ""} - } - contract: { address: { _eq: $contractAddress } } - } - limit: $pageSize, - offset: $offset, - order_by: { transaction: { block_height: desc } } - ) { - transaction { - hash - is_send - is_execute - is_ibc - is_instantiate - is_store_code - messages - success - block { - timestamp - } + query QueryTransactionFromContractsTxs($pageSize: Int!, $offset: Int!) { + contract_transactions( + where: ${where}, + limit: $pageSize, + offset: $offset, + order_by: { transaction: {block: {timestamp: desc}} } + ) { + transaction { + hash + is_send + is_execute + is_ibc + is_instantiate + is_store_code + is_clear_admin + is_migrate + is_update_admin + messages + success + block { + timestamp } } - contract_transactions_aggregate( - where: { - transaction: { - account: { address: { _eq: $userAddr } }, - ${actionsFilter !== "" ? `${actionsFilter},` : ""} - } - contract: { address: { _eq: $contractAddress } } - } + } + }`; +}; + +export const queryTransactionCountFromContractTxs = (where: string) => { + return gql` + query QueryTransactionCountFromContractsTxs { + contract_transactions_aggregate( + where: ${where} ) { aggregate { count } } - } - `; + }`; }; diff --git a/src/lib/pages/past-txs/query/useTxQuery.ts b/src/lib/pages/past-txs/query/useTxQuery.ts index 69728842a..0c2856feb 100644 --- a/src/lib/pages/past-txs/query/useTxQuery.ts +++ b/src/lib/pages/past-txs/query/useTxQuery.ts @@ -1,155 +1,207 @@ -import { useWallet } from "@cosmos-kit/react"; import { useQuery } from "@tanstack/react-query"; -import { request } from "graphql-request"; +import { useCallback } from "react"; -import { OSMOSIS_TESTNET_GQL_ENDPOINT } from "lib/env"; -import type { Transaction } from "lib/types/tx/transaction"; -import { snakeToCamel } from "lib/utils/formatter"; +import { indexerGraphClient } from "lib/data/graphql"; +import { useGetAddressType } from "lib/hooks"; +import type { Filters, Message, Option } from "lib/types"; +import { + getActionMsgType, + parseDateDefault, + parseTxHash, + snakeToCamel, + getMsgFurtherAction, +} from "lib/utils"; import { - queryAddrFromContracts, - queryShowallFromTxs, - queryWithActionsFromTxs, + actionsFilter, + generateWhereForContractTx, + generateWhereForTx, +} from "./generateWhere"; +import { + queryTransactionCountFromContractTxs, + queryTransactionFromContractTxs, + queryTransactionsCountFromTxs, + queryTransactionsFromTxs, } from "./graphqlQuery"; -interface Action { - execute: boolean; - instantiate: boolean; - upload: boolean; - ibc: boolean; - send: boolean; -} - -interface Response { - transactions: Array; - count: number; +interface GraphqlTransactionsResponse { + hash: string; + isSend: boolean; + isExecute: boolean; + isIbc: boolean; + isInstantiate: boolean; + isStoreCode: boolean; + isClearAdmin: boolean; + isMigrate: boolean; + isUpdateAdmin: boolean; + messages: Message[]; + success: boolean; + block: { + timestamp: string; + }; } -const actions = { - execute: "is_execute", - instantiate: "is_instantiate", - upload: "is_store_code", - ibc: "is_ibc", - send: "is_send", -}; - export const useTxQuery = ( - userAddr: string | undefined, - execute: boolean, - instantiate: boolean, - upload: boolean, - ibc: boolean, - send: boolean, + userAddress: Option, search: string, + filters: Filters, pageSize: number, offset: number ) => { - const { currentChainName } = useWallet(); + const getAddressType = useGetAddressType(); + // Filter when action buttons are pressed - const actionsFilter = () => { - const actionsObj = { - execute, - instantiate, - upload, - send, - ibc, - }; + const queryFn = useCallback(async () => { + if (!userAddress) return undefined; - let filter = ""; - Object.keys(actionsObj).forEach((key) => { - if (actionsObj[key as keyof typeof actionsObj]) { - // Remove message that contain only ibc - if (key === "ibc") { - filter += ` - - _and: [ - { is_ibc: { _eq: true} } - { - _or: [ - { is_execute: { _neq: false } } - { is_instantiate: { _neq: false } } - { is_store_code: { _neq: false } } - { is_send: { _neq: false } } - ] - } - ] - - `; - } else { - filter += `${actions[key as keyof Action]}: {_eq: true}`; - filter += `,`; - } - } + // Search with contract address -> query from contract transaction table + if (getAddressType(search) === "contract_address") { + const where = generateWhereForContractTx({ + userAddress, + contractAddress: search, + filters, + }); + return indexerGraphClient + .request(queryTransactionFromContractTxs(where), { + pageSize, + offset, + }) + .then(({ contract_transactions }) => { + const contractTransactionsToCamel = snakeToCamel( + contract_transactions + ) as { transaction: GraphqlTransactionsResponse }[]; + return contractTransactionsToCamel.map( + (contractTx: { transaction: GraphqlTransactionsResponse }) => ({ + hash: parseTxHash(contractTx.transaction.hash), + messages: snakeToCamel( + contractTx.transaction.messages + ) as Message[], + created: parseDateDefault( + contractTx.transaction.block?.timestamp + ), + success: contractTx.transaction.success, + actionMsgType: getActionMsgType([ + contractTx.transaction.isExecute, + contractTx.transaction.isInstantiate, + contractTx.transaction.isSend, + contractTx.transaction.isStoreCode, + contractTx.transaction.isMigrate, + contractTx.transaction.isUpdateAdmin, + contractTx.transaction.isClearAdmin, + ]), + furtherAction: getMsgFurtherAction( + contractTx.transaction.messages.length, + { + isExecute: contractTx.transaction.isExecute, + isInstantiate: contractTx.transaction.isInstantiate, + isSend: contractTx.transaction.isSend, + isUpload: contractTx.transaction.isStoreCode, + isMigrate: contractTx.transaction.isMigrate, + isUpdateAdmin: contractTx.transaction.isUpdateAdmin, + isClearAdmin: contractTx.transaction.isClearAdmin, + isIbc: contractTx.transaction.isIbc, + } + ), + isIbc: contractTx.transaction.isIbc, + }) + ); + }); + } + + const where = generateWhereForTx({ + userAddress, + txHash: search, + filters, }); + return indexerGraphClient + .request(queryTransactionsFromTxs(where), { + pageSize, + offset, + }) + .then(({ transactions }) => { + const transactionsToCamel = snakeToCamel( + transactions + ) as GraphqlTransactionsResponse[]; + return transactionsToCamel.map((transaction) => { + return { + hash: parseTxHash(transaction.hash), + messages: snakeToCamel(transaction.messages) as Message[], + created: parseDateDefault(transaction.block?.timestamp), + success: transaction.success, + actionMsgType: getActionMsgType([ + transaction.isExecute, + transaction.isInstantiate, + transaction.isSend, + transaction.isStoreCode, + transaction.isMigrate, + transaction.isUpdateAdmin, + transaction.isClearAdmin, + ]), + furtherAction: getMsgFurtherAction(transaction.messages.length, { + isExecute: transaction.isExecute, + isInstantiate: transaction.isInstantiate, + isSend: transaction.isSend, + isUpload: transaction.isStoreCode, + isMigrate: transaction.isMigrate, + isUpdateAdmin: transaction.isUpdateAdmin, + isClearAdmin: transaction.isClearAdmin, + isIbc: transaction.isIbc, + }), + isIbc: transaction.isIbc, + }; + }); + }); + }, [filters, getAddressType, offset, pageSize, search, userAddress]); - // Remove last , - filter = filter.substring(0, filter.length - 1); - return filter; - }; + return useQuery({ + queryKey: [ + "past-transaction", + userAddress, + search, + filters, + offset, + pageSize, + ], + queryFn, + }); +}; - const queryFn = async (): Promise => { - // Determine endpoint - let endpoint = ""; - if (currentChainName === "osmosistestnet") { - endpoint = OSMOSIS_TESTNET_GQL_ENDPOINT; - } - if (endpoint === "" || userAddr === "") { - return { transactions: [], count: 0 } as Response; - } +export const useTxQueryCount = ( + userAddress: Option, + search: string, + filters: Filters +) => { + const getAddressType = useGetAddressType(); - // Tx hash and no search -> query from transactions table - if (search.length === 64 || search.length === 0) { - const isShowAll = !execute && !instantiate && !upload && !ibc && !send; - // Show all 4 main action types - let response; - if (isShowAll) { - response = await request(endpoint, queryShowallFromTxs(search), { - userAddr, - pageSize, - offset, - }); - // When buttons are pressed - } else { - response = await request( - endpoint, - queryWithActionsFromTxs(search, actionsFilter()), - { - userAddr, - pageSize, - offset, - } - ); - } - return snakeToCamel({ - transactions: response.transactions, - count: response.transactions_aggregate?.aggregate?.count, - }) as Response; + const queryFn = useCallback(async () => { + if (!userAddress) return undefined; - // Contract address -> query from contracts table - } - if (search.length === 63) { - const response = await request( - endpoint, - queryAddrFromContracts(actionsFilter()), - { - userAddr, + if (getAddressType(search) === "contract_address") { + return indexerGraphClient + .request(queryTransactionCountFromContractTxs(actionsFilter(filters)), { + userAddress, contractAddress: search, - pageSize, - offset, - } - ); - return snakeToCamel({ - transactions: response.contract_transactions.map( - (tx: { transaction: Transaction }) => tx.transaction - ), - count: response.contract_transactions_aggregate?.aggregate?.count, - }) as Response; + }) + .then( + ({ contract_transactions_aggregate }) => + contract_transactions_aggregate.aggregate.count + ); } - return { transactions: [], count: 0 } as Response; - }; + + const where = generateWhereForTx({ + userAddress, + txHash: search, + filters, + }); + return indexerGraphClient + .request(queryTransactionsCountFromTxs(where)) + .then( + ({ transactions_aggregate }) => transactions_aggregate.aggregate.count + ); + }, [filters, getAddressType, search, userAddress]); + return useQuery({ - queryKey: ["transaction", userAddr, search], + queryKey: ["past-transaction-count", userAddress, search, filters], queryFn, - refetchInterval: 2000, }); }; diff --git a/src/lib/types/tx/transaction.ts b/src/lib/types/tx/transaction.ts index 186741839..80681d7fd 100644 --- a/src/lib/types/tx/transaction.ts +++ b/src/lib/types/tx/transaction.ts @@ -10,22 +10,26 @@ import type { DetailUpload, } from "./msg"; -export interface Transaction { +export enum ActionMsgType { + SINGLE_ACTION_MSG = "SINGLE_ACTION_MSG", + MULTIPLE_ACTION_MSG = "MULTIPLE_ACTION_MSG", + OTHER_ACTION_MSG = "OTHER_ACTION_MSG", +} + +export enum MsgFurtherAction { + REDO = "REDO", + RESEND = "RESEND", + NONE = "NONE", +} + +export interface PastTransaction { hash: string; - isSend?: boolean; - isExecute?: boolean; - isInstantiate?: boolean; - isStoreCode?: boolean; - isIbc?: boolean; messages: Message[]; + created: Date; success: boolean; - account?: { - address: string; - }; - block: { - height?: number; - timestamp: string; - }; + actionMsgType: ActionMsgType; + furtherAction: MsgFurtherAction; + isIbc: boolean; } export interface Message { @@ -61,12 +65,18 @@ export interface ExecuteTransaction { success: boolean; } -export enum ActionMsgType { - SINGLE_ACTION_MSG = "SINGLE_ACTION_MSG", - MULTIPLE_ACTION_MSG = "MULTIPLE_ACTION_MSG", - OTHER_ACTION_MSG = "OTHER_ACTION_MSG", -} export interface AllTransaction extends ExecuteTransaction { actionMsgType: ActionMsgType; isIbc: boolean; } + +export interface Filters { + isExecute: boolean; + isInstantiate: boolean; + isUpload: boolean; + isIbc: boolean; + isSend: boolean; + isMigrate: boolean; + isUpdateAdmin: boolean; + isClearAdmin: boolean; +} diff --git a/src/lib/utils/extractActionValue.ts b/src/lib/utils/extractActionValue.ts new file mode 100644 index 000000000..da238839c --- /dev/null +++ b/src/lib/utils/extractActionValue.ts @@ -0,0 +1,22 @@ +export const displayActionValue = (isActionName: string) => { + switch (isActionName) { + case "isUpload": + return "Upload"; + case "isInstantiate": + return "Instantiate"; + case "isExecute": + return "Execute"; + case "isSend": + return "Send"; + case "isIbc": + return "IBC"; + case "isMigrate": + return "Migrate"; + case "isClearAdmin": + return "Clear Admin"; + case "isUpdateAdmin": + return "Update Admin"; + default: + return ""; + } +}; diff --git a/src/lib/utils/index.ts b/src/lib/utils/index.ts index c4c0280d6..77c956e79 100644 --- a/src/lib/utils/index.ts +++ b/src/lib/utils/index.ts @@ -18,3 +18,5 @@ export * from "./option"; export * from "./description"; export * from "./tags"; export * from "./countMessages"; +export * from "./msgFurtherAction"; +export * from "./extractActionValue"; diff --git a/src/lib/utils/msgFurtherAction.ts b/src/lib/utils/msgFurtherAction.ts new file mode 100644 index 000000000..828a0bb59 --- /dev/null +++ b/src/lib/utils/msgFurtherAction.ts @@ -0,0 +1,22 @@ +import type { Filters } from "lib/types"; +import { MsgFurtherAction } from "lib/types"; + +export const getMsgFurtherAction = (length: number, filters: Filters) => { + // Redo: instantiate, execute, check length === 1 + if (length === 1 && (filters.isExecute || filters.isInstantiate)) { + return MsgFurtherAction.REDO; + } + // Resend: messages with execute, instantiate, or send + if ( + !filters.isClearAdmin && + !filters.isUpload && + !filters.isIbc && + !filters.isClearAdmin && + !filters.isMigrate && + !filters.isUpdateAdmin && + (filters.isExecute || filters.isInstantiate || filters.isSend) + ) { + return MsgFurtherAction.RESEND; + } + return MsgFurtherAction.NONE; +}; From 468583bb70bbca8426c98b5661fe497e346bb7e5 Mon Sep 17 00:00:00 2001 From: bkioshn Date: Fri, 20 Jan 2023 16:07:16 +0700 Subject: [PATCH 02/17] fix: pagination problem when search, table size, input autocomplete --- src/lib/pages/past-txs/components/FilterSelection.tsx | 1 + src/lib/pages/past-txs/components/PastTxsContent.tsx | 2 +- src/lib/pages/past-txs/index.tsx | 7 ++++++- 3 files changed, 8 insertions(+), 2 deletions(-) diff --git a/src/lib/pages/past-txs/components/FilterSelection.tsx b/src/lib/pages/past-txs/components/FilterSelection.tsx index ddc66f6c5..b45a95592 100644 --- a/src/lib/pages/past-txs/components/FilterSelection.tsx +++ b/src/lib/pages/past-txs/components/FilterSelection.tsx @@ -141,6 +141,7 @@ export const FilterSelection = observer( )} { pageSize, offset ); + const setFilter = (filter: string, bool: boolean) => { setValue("filters", { ...pastTxsState.filters, [filter]: bool }); }; @@ -99,6 +100,10 @@ const PastTxs = () => { return filters; }, [pastTxsState]); + useEffect(() => { + setCurrentPage(1); + }, [pastTxsState.filters, pastTxsState.input, setCurrentPage]); + return ( From 48c4ee4a3e305acae0ac4dd18bb8f3dcc958c599 Mon Sep 17 00:00:00 2001 From: bkioshn Date: Fri, 20 Jan 2023 17:12:09 +0700 Subject: [PATCH 03/17] chore: add changelog --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9f43cf949..33458fb79 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 +- [#112](https://github.com/alleslabs/celatone-frontend/pull/112) Refactor past transactions page, support new messages including Migration, Instantiate2, Update Admin, Clear Admin, and change filter actions to dropdown selection - [#72](https://github.com/alleslabs/celatone-frontend/pull/72) Fix general wording and grammar - [#110](https://github.com/alleslabs/celatone-frontend/pull/110) Fix proposal detail rendering - [#109](https://github.com/alleslabs/celatone-frontend/pull/109) Fix incorrect rendering of zero value badges From a4eb4952e47ff09eeb4ac45c67c8c0c701d990c4 Mon Sep 17 00:00:00 2001 From: bkioshn Date: Fri, 20 Jan 2023 22:02:42 +0700 Subject: [PATCH 04/17] fix: updateAdminMsg to support contract address for new admin --- src/lib/hooks/useSingleMessageProps.ts | 15 +++++++++------ src/lib/types/tx/msg.ts | 2 +- 2 files changed, 10 insertions(+), 7 deletions(-) diff --git a/src/lib/hooks/useSingleMessageProps.ts b/src/lib/hooks/useSingleMessageProps.ts index 9419eb140..181135032 100644 --- a/src/lib/hooks/useSingleMessageProps.ts +++ b/src/lib/hooks/useSingleMessageProps.ts @@ -341,9 +341,9 @@ const migrateSingleMsgProps = ( * * @remarks * More than 1 msg: Update [length] admins - * Only 1 msg: Update admin on [name || contract address] to [user address] + * Only 1 msg: Update admin on [name || contract address] to [user address || contract address] * Fail with more than 1 msg: Failed to update [length] admins - * Fail with 1 msg: Failed to update admin on [name || contract address] to [user address] + * Fail with 1 msg: Failed to update admin on [name || contract address] to [user address || contract address] * * @param isSuccess - boolean of whether tx is succeed or not @@ -355,10 +355,12 @@ const migrateSingleMsgProps = ( const updateAdminSingleMsgProps = ( isSuccess: boolean, messages: Message[], + chainName: string, getContractLocalInfo: (contractAddress: string) => Option ) => { const detail = messages[0].detail as DetailUpdateAdmin; const contractLocalInfo = getContractLocalInfo(detail.contract); + const adminLocalInfo = getContractLocalInfo(detail.newAdmin); if (messages.length > 1) { return isSuccess @@ -385,8 +387,8 @@ const updateAdminSingleMsgProps = ( }, text3: "to", link2: { - type: "user_address" as LinkType, - value: detail.newAdmin, + type: getAddressTypeByLength(chainName, detail.newAdmin) as LinkType, + value: adminLocalInfo?.name || detail.newAdmin, }, } : { @@ -399,8 +401,8 @@ const updateAdminSingleMsgProps = ( }, text3: "to", link2: { - type: "user_address" as LinkType, - value: detail.newAdmin, + type: getAddressTypeByLength(chainName, detail.newAdmin) as LinkType, + value: adminLocalInfo?.name || detail.newAdmin, }, }; }; @@ -587,6 +589,7 @@ export const useSingleActionMsgProps = ( return updateAdminSingleMsgProps( isSuccess, messages, + currentChainName, getContractLocalInfo ); case "MsgClearAdmin": diff --git a/src/lib/types/tx/msg.ts b/src/lib/types/tx/msg.ts index 8b2f5e491..ca169628d 100644 --- a/src/lib/types/tx/msg.ts +++ b/src/lib/types/tx/msg.ts @@ -78,7 +78,7 @@ export interface DetailClearAdmin { export interface DetailUpdateAdmin { contract: ContractAddr; - newAdmin: HumanAddr; + newAdmin: HumanAddr | ContractAddr; sender: HumanAddr; } From a64422b2ebc30758a21211d6d6fa40c6586f57a5 Mon Sep 17 00:00:00 2001 From: bkioshn Date: Mon, 23 Jan 2023 12:19:49 +0700 Subject: [PATCH 05/17] fix: throw err in query fnc, change file name, fix style --- .../table/{MsgDetail.tsx => AccordionTx.tsx} | 7 +- .../tables/transactions/TxsTableRow.tsx | 4 +- .../pages/past-txs/components/MsgDetail.tsx | 297 ------------------ .../pages/past-txs/components/PastTxRow.tsx | 4 +- .../past-txs/components/PastTxsContent.tsx | 2 +- src/lib/pages/past-txs/query/useTxQuery.ts | 4 +- 6 files changed, 12 insertions(+), 306 deletions(-) rename src/lib/components/table/{MsgDetail.tsx => AccordionTx.tsx} (93%) delete mode 100644 src/lib/pages/past-txs/components/MsgDetail.tsx diff --git a/src/lib/components/table/MsgDetail.tsx b/src/lib/components/table/AccordionTx.tsx similarity index 93% rename from src/lib/components/table/MsgDetail.tsx rename to src/lib/components/table/AccordionTx.tsx index c2178e69c..356c1cbdd 100644 --- a/src/lib/components/table/MsgDetail.tsx +++ b/src/lib/components/table/AccordionTx.tsx @@ -10,7 +10,7 @@ import { extractMsgType } from "lib/utils"; import { TableRow } from "./tableComponents"; -interface MsgDetailProps { +interface AccordionTxProps { message: Message; allowFurtherAction: boolean; } @@ -32,7 +32,10 @@ const RenderButton = ({ message }: RenderButtonProps) => { return null; }; -export const MsgDetail = ({ message, allowFurtherAction }: MsgDetailProps) => { +export const AccordionTx = ({ + message, + allowFurtherAction, +}: AccordionTxProps) => { const [showButton, setShowButton] = useState(false); return (