From b3181070605457f2da0d81a4f0b3a01381bd55dc Mon Sep 17 00:00:00 2001 From: songwongtp <16089160+songwongtp@users.noreply.github.com> Date: Mon, 13 Nov 2023 18:25:50 +0700 Subject: [PATCH 1/4] fix: saved account type check --- CHANGELOG.md | 1 + src/lib/app-provider/env.ts | 1 + src/lib/app-provider/hooks/useAddress.ts | 89 ++++++++------- .../components/abi/args-form/field/index.tsx | 24 ++-- src/lib/components/forms/ControllerInput.tsx | 4 + .../modal/account/SaveNewAccount.tsx | 107 ++++++++++-------- .../modal/account/ToContractButton.tsx | 34 ++++++ src/lib/gql/fragment-masking.ts | 5 +- src/lib/gql/gql.ts | 8 ++ src/lib/gql/graphql.ts | 63 +++++++++++ src/lib/gql/index.ts | 2 +- src/lib/hooks/useFormatAddresses.ts | 22 ++-- src/lib/pages/account-details/index.tsx | 40 +++---- src/lib/query/account.ts | 8 ++ src/lib/services/accountService.ts | 43 ++++++- src/lib/types/account.ts | 11 ++ src/lib/types/index.ts | 1 + 17 files changed, 321 insertions(+), 142 deletions(-) create mode 100644 src/lib/components/modal/account/ToContractButton.tsx create mode 100644 src/lib/types/account.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index ed58d1c06..f6deae92e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -71,6 +71,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Improvements +- [#625](https://github.com/alleslabs/celatone-frontend/pull/625) Fix abi empty vector serialization - [#605](https://github.com/alleslabs/celatone-frontend/pull/605) Apply singleton class style to Amplitude structure - [#591](https://github.com/alleslabs/celatone-frontend/pull/591) Bump initia.js due to decimal serialization and dynamic buffer - [#564](https://github.com/alleslabs/celatone-frontend/pull/564) Handle multi bond denoms data in the delegation section diff --git a/src/lib/app-provider/env.ts b/src/lib/app-provider/env.ts index 6539517ea..0c73f1f68 100644 --- a/src/lib/app-provider/env.ts +++ b/src/lib/app-provider/env.ts @@ -16,6 +16,7 @@ export enum CELATONE_QUERY_KEYS { // ACCOUNT ACCOUNT_BALANCES_INFO = "CELATONE_QUERY_ACCOUNT_BALANCES_INFO", ACCOUNT_ID = "CELATONE_QUERY_ACCOUNT_ID", + ACCOUNT_TYPE = "CELATONE_QUERY_ACCOUNT_TYPE", // ASSET ASSET_INFOS = "CELATONE_QUERY_ASSET_INFOS", ASSET_INFO_LIST = "CELATONE_QUERY_ASSET_INFO_LIST", diff --git a/src/lib/app-provider/hooks/useAddress.ts b/src/lib/app-provider/hooks/useAddress.ts index f4d7d0a37..4f5ad05cd 100644 --- a/src/lib/app-provider/hooks/useAddress.ts +++ b/src/lib/app-provider/hooks/useAddress.ts @@ -2,8 +2,9 @@ import { fromBech32 } from "@cosmjs/encoding"; import { useCallback, useMemo } from "react"; import type { Option } from "lib/types"; -import { isHexWalletAddress, isHexModuleAddress } from "lib/utils"; +import { isHexModuleAddress, isHexWalletAddress } from "lib/utils"; +import { useMoveConfig } from "./useConfig"; import { useCurrentChain } from "./useCurrentChain"; import { useExampleAddresses } from "./useExampleAddresses"; @@ -103,47 +104,55 @@ export const useValidateAddress = () => { chain: { bech32_prefix: bech32Prefix }, } = useCurrentChain(); const getAddressTypeByLength = useGetAddressTypeByLength(); + const move = useMoveConfig({ shouldRedirect: false }); + + const validateContractAddress = useCallback( + (address: string) => + validateAddress( + bech32Prefix, + address, + "contract_address", + getAddressTypeByLength + ), + [bech32Prefix, getAddressTypeByLength] + ); + const validateUserAddress = useCallback( + (address: string) => + validateAddress( + bech32Prefix, + address, + "user_address", + getAddressTypeByLength + ), + [bech32Prefix, getAddressTypeByLength] + ); + const validateValidatorAddress = useCallback( + (address: string) => + validateAddress( + bech32Prefix, + address, + "validator_address", + getAddressTypeByLength + ), + [bech32Prefix, getAddressTypeByLength] + ); return { - validateContractAddress: useCallback( - (address: string) => - validateAddress( - bech32Prefix, - address, - "contract_address", - getAddressTypeByLength - ), - [bech32Prefix, getAddressTypeByLength] - ), - validateUserAddress: useCallback( - (address: string) => - validateAddress( - bech32Prefix, - address, - "user_address", - getAddressTypeByLength - ), - [bech32Prefix, getAddressTypeByLength] - ), - validateValidatorAddress: useCallback( - (address: string) => - validateAddress( - bech32Prefix, - address, - "validator_address", - getAddressTypeByLength - ), - [bech32Prefix, getAddressTypeByLength] - ), - validateHexWalletAddress: useCallback( - (address: string) => - !address.startsWith(bech32Prefix) && isHexWalletAddress(address), - [bech32Prefix] - ), - validateHexModuleAddress: useCallback( - (address: string) => - !address.startsWith(bech32Prefix) && isHexModuleAddress(address), - [bech32Prefix] + validateContractAddress, + validateUserAddress, + validateValidatorAddress, + isSomeValidAddress: useCallback( + (address: string) => { + const errUser = validateUserAddress(address); + const errContract = validateContractAddress(address); + const isHex = isHexWalletAddress(address); + const isHexModule = isHexModuleAddress(address); + + return ( + !errUser || !errContract || (move.enabled && (isHex || isHexModule)) + ); + }, + [move.enabled, validateContractAddress, validateUserAddress] ), }; }; diff --git a/src/lib/components/abi/args-form/field/index.tsx b/src/lib/components/abi/args-form/field/index.tsx index cff3e12f7..ce2360045 100644 --- a/src/lib/components/abi/args-form/field/index.tsx +++ b/src/lib/components/abi/args-form/field/index.tsx @@ -13,6 +13,7 @@ import { type Control, useController } from "react-hook-form"; import { useValidateAddress } from "lib/app-provider"; import type { AbiFormData } from "lib/types"; +import { isHexModuleAddress, isHexWalletAddress } from "lib/utils"; import { ArgFieldWidget } from "./ArgFieldWidget"; import { OBJECT_TYPE, STRING_TYPE } from "./constants"; @@ -30,31 +31,20 @@ export const ArgFieldTemplate = ({ control, }: ArgFieldTemplateProps) => { const [isEditted, setIsEditted] = useState(false); - const { - validateUserAddress, - validateContractAddress, - validateHexWalletAddress, - validateHexModuleAddress, - } = useValidateAddress(); + const { validateUserAddress, validateContractAddress } = useValidateAddress(); const isValidArgAddress = useCallback( (input: string) => validateUserAddress(input) === null || validateContractAddress(input) === null || - validateHexWalletAddress(input) || - validateHexModuleAddress(input), - [ - validateContractAddress, - validateHexModuleAddress, - validateHexWalletAddress, - validateUserAddress, - ] + isHexWalletAddress(input) || + isHexModuleAddress(input), + [validateContractAddress, validateUserAddress] ); const isValidArgObject = useCallback( (input: string) => - validateContractAddress(input) === null || - validateHexModuleAddress(input), - [validateContractAddress, validateHexModuleAddress] + validateContractAddress(input) === null || isHexModuleAddress(input), + [validateContractAddress] ); const isOptional = param.startsWith("0x1::option::Option"); diff --git a/src/lib/components/forms/ControllerInput.tsx b/src/lib/components/forms/ControllerInput.tsx index 75fd4f95a..4eb559ec2 100644 --- a/src/lib/components/forms/ControllerInput.tsx +++ b/src/lib/components/forms/ControllerInput.tsx @@ -45,6 +45,8 @@ export const ControllerInput = ({ rules = {}, status, maxLength, + autoFocus, + cursor, helperAction, ...componentProps }: ControllerInputProps) => { @@ -94,6 +96,8 @@ export const ControllerInput = ({ value={watcher} onChange={field.onChange} maxLength={maxLength} + autoFocus={autoFocus} + cursor={cursor} /> {status && getStatusIcon(status.state)} diff --git a/src/lib/components/modal/account/SaveNewAccount.tsx b/src/lib/components/modal/account/SaveNewAccount.tsx index 07b8b4ee1..0eb09f28c 100644 --- a/src/lib/components/modal/account/SaveNewAccount.tsx +++ b/src/lib/components/modal/account/SaveNewAccount.tsx @@ -9,14 +9,16 @@ import { useExampleAddresses, useMoveConfig, useValidateAddress, + useWasmConfig, } from "lib/app-provider"; import type { FormStatus } from "lib/components/forms"; import { ControllerInput, ControllerTextarea } from "lib/components/forms"; import { useGetMaxLengthError, useHandleAccountSave } from "lib/hooks"; import { useAccountStore } from "lib/providers/store"; +import { useAccountType } from "lib/services/accountService"; import type { Addr } from "lib/types"; -import { SavedAccountModalHeader } from "./SavedAccountModalHeader"; +import { ToContractButton } from "./ToContractButton"; export interface SaveAccountDetail { address: Addr; @@ -24,6 +26,11 @@ export interface SaveAccountDetail { description: string; } +const statusSuccess: FormStatus = { + state: "success", + message: "Valid Address", +}; + interface SaveNewAccountModalProps { buttonProps: ButtonProps; accountAddress?: Addr; @@ -37,26 +44,23 @@ export function SaveNewAccountModal({ publicName, publicDescription, }: SaveNewAccountModalProps) { - const { user: exampleUserAddress } = useExampleAddresses(); - const { - validateUserAddress, - validateContractAddress, - validateHexWalletAddress, - validateHexModuleAddress, - } = useValidateAddress(); const { constants } = useCelatoneApp(); + const { user: exampleUserAddress } = useExampleAddresses(); + const { isSomeValidAddress } = useValidateAddress(); const move = useMoveConfig({ shouldRedirect: false }); + const wasm = useWasmConfig({ shouldRedirect: false }); const getMaxLengthError = useGetMaxLengthError(); const { isAccountSaved } = useAccountStore(); + const defaultAddress = accountAddress ?? ("" as Addr); const defaultValues: SaveAccountDetail = useMemo(() => { return { - address: accountAddress ?? ("" as Addr), + address: defaultAddress, name: publicName ?? "", description: publicDescription ?? "", }; - }, [accountAddress, publicName, publicDescription]); + }, [defaultAddress, publicDescription, publicName]); const { control, @@ -69,6 +73,7 @@ export function SaveNewAccountModal({ }); const [status, setStatus] = useState({ state: "init" }); + const [isContract, setIsContract] = useState(false); const addressState = watch("address"); const nameState = watch("name"); @@ -76,10 +81,32 @@ export function SaveNewAccountModal({ const resetForm = (resetAddress = true) => { reset({ ...defaultValues, - address: resetAddress ? "" : addressState, + address: resetAddress ? defaultAddress : addressState, }); }; + const { refetch } = useAccountType( + addressState as Addr, + false, + (type) => { + if (type !== "ContractAccount") setStatus(statusSuccess); + else { + setStatus({ + state: "error", + message: "Cannot save contract", + }); + setIsContract(true); + } + }, + (err) => { + resetForm(false); + setStatus({ + state: "error", + message: err.message, + }); + } + ); + useEffect(() => { if (addressState.trim().length === 0) { setStatus({ @@ -89,6 +116,7 @@ export function SaveNewAccountModal({ setStatus({ state: "loading", }); + setIsContract(false); if (isAccountSaved(addressState)) { setStatus({ state: "error", @@ -96,25 +124,13 @@ export function SaveNewAccountModal({ }); } else { const timeoutId = setTimeout(() => { - const errUser = validateUserAddress(addressState); - const errContract = validateContractAddress(addressState); - const isHex = validateHexWalletAddress(addressState); - const isHexModule = validateHexModuleAddress(addressState); - - if ( - (!move.enabled && errUser && errContract) || - (move.enabled && errUser && errContract && !isHex && !isHexModule) - ) + if (!isSomeValidAddress(addressState)) setStatus({ state: "error", - message: errUser, - }); - // TODO validate contract type online - else - setStatus({ - state: "success", - message: "Valid Address", + message: "Invalid Address", }); + else if (wasm.enabled) refetch(); + else setStatus(statusSuccess); }, 1000); return () => clearTimeout(timeoutId); } @@ -123,11 +139,10 @@ export function SaveNewAccountModal({ }, [ addressState, isAccountSaved, - validateUserAddress, - validateContractAddress, - validateHexWalletAddress, - validateHexModuleAddress, + isSomeValidAddress, + refetch, move.enabled, + wasm.enabled, ]); const handleSave = useHandleAccountSave({ @@ -151,25 +166,24 @@ export function SaveNewAccountModal({ disabledMain={ status.state !== "success" || !!errors.name || !!errors.description } - headerContent={ - accountAddress && - } otherBtnTitle="Cancel" buttonRemark="Saved accounts are stored locally on your device." > - {!accountAddress && ( - - )} + } + /> { + const router = useRouter(); + const navigate = useInternalNavigate(); + + const isSavedAccounts = router.pathname === "/[network]/saved-accounts"; + return ( + + navigate( + isSavedAccounts + ? { pathname: "/contract-lists/saved-contracts" } + : { + pathname: "/contracts/[contractAddress]", + query: { contractAddress: router.query.accountAddress }, + } + ) + } + > + {isSavedAccounts ? "To Saved Contracts" : "To Contract Details"} + + ); +}; diff --git a/src/lib/gql/fragment-masking.ts b/src/lib/gql/fragment-masking.ts index c6c2ca9cf..24ea25266 100644 --- a/src/lib/gql/fragment-masking.ts +++ b/src/lib/gql/fragment-masking.ts @@ -1,5 +1,6 @@ -/* eslint-disable */ -import { +/* eslint-disable @typescript-eslint/no-unused-vars */ +/* eslint-disable @typescript-eslint/no-explicit-any */ +import type { ResultOf, TypedDocumentNode as DocumentNode, } from "@graphql-typed-document-node/core"; diff --git a/src/lib/gql/gql.ts b/src/lib/gql/gql.ts index fcb2974ca..3ad01976b 100644 --- a/src/lib/gql/gql.ts +++ b/src/lib/gql/gql.ts @@ -15,6 +15,8 @@ import { TypedDocumentNode as DocumentNode } from "@graphql-typed-document-node/ const documents = { "\n query getAccountIdByAddressQueryDocument($address: String!) {\n accounts_by_pk(address: $address) {\n id\n }\n }\n": types.GetAccountIdByAddressQueryDocumentDocument, + "\n query getAccountTypeByAddressQueryDocument($address: String!) {\n accounts_by_pk(address: $address) {\n type\n }\n }\n": + types.GetAccountTypeByAddressQueryDocumentDocument, "\n query getBlockTimestampByHeightQuery($height: Int!) {\n blocks_by_pk(height: $height) {\n timestamp\n }\n }\n": types.GetBlockTimestampByHeightQueryDocument, "\n query getBlockListQuery($limit: Int!, $offset: Int!) {\n blocks(limit: $limit, offset: $offset, order_by: { height: desc }) {\n hash\n height\n timestamp\n transactions_aggregate {\n aggregate {\n count\n }\n }\n validator {\n moniker\n operator_address\n identity\n }\n }\n }\n": @@ -143,6 +145,12 @@ export function graphql(source: string): unknown; export function graphql( source: "\n query getAccountIdByAddressQueryDocument($address: String!) {\n accounts_by_pk(address: $address) {\n id\n }\n }\n" ): (typeof documents)["\n query getAccountIdByAddressQueryDocument($address: String!) {\n accounts_by_pk(address: $address) {\n id\n }\n }\n"]; +/** + * The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients. + */ +export function graphql( + source: "\n query getAccountTypeByAddressQueryDocument($address: String!) {\n accounts_by_pk(address: $address) {\n type\n }\n }\n" +): (typeof documents)["\n query getAccountTypeByAddressQueryDocument($address: String!) {\n accounts_by_pk(address: $address) {\n type\n }\n }\n"]; /** * The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients. */ diff --git a/src/lib/gql/graphql.ts b/src/lib/gql/graphql.ts index fa91b7875..f82b77014 100644 --- a/src/lib/gql/graphql.ts +++ b/src/lib/gql/graphql.ts @@ -14834,6 +14834,15 @@ export type GetAccountIdByAddressQueryDocumentQuery = { accounts_by_pk?: { __typename?: "accounts"; id: number } | null; }; +export type GetAccountTypeByAddressQueryDocumentQueryVariables = Exact<{ + address: Scalars["String"]; +}>; + +export type GetAccountTypeByAddressQueryDocumentQuery = { + __typename?: "query_root"; + accounts_by_pk?: { __typename?: "accounts"; type?: any | null } | null; +}; + export type GetBlockTimestampByHeightQueryQueryVariables = Exact<{ height: Scalars["Int"]; }>; @@ -15884,6 +15893,60 @@ export const GetAccountIdByAddressQueryDocumentDocument = { GetAccountIdByAddressQueryDocumentQuery, GetAccountIdByAddressQueryDocumentQueryVariables >; +export const GetAccountTypeByAddressQueryDocumentDocument = { + kind: "Document", + definitions: [ + { + kind: "OperationDefinition", + operation: "query", + name: { kind: "Name", value: "getAccountTypeByAddressQueryDocument" }, + variableDefinitions: [ + { + kind: "VariableDefinition", + variable: { + kind: "Variable", + name: { kind: "Name", value: "address" }, + }, + type: { + kind: "NonNullType", + type: { + kind: "NamedType", + name: { kind: "Name", value: "String" }, + }, + }, + }, + ], + selectionSet: { + kind: "SelectionSet", + selections: [ + { + kind: "Field", + name: { kind: "Name", value: "accounts_by_pk" }, + arguments: [ + { + kind: "Argument", + name: { kind: "Name", value: "address" }, + value: { + kind: "Variable", + name: { kind: "Name", value: "address" }, + }, + }, + ], + selectionSet: { + kind: "SelectionSet", + selections: [ + { kind: "Field", name: { kind: "Name", value: "type" } }, + ], + }, + }, + ], + }, + }, + ], +} as unknown as DocumentNode< + GetAccountTypeByAddressQueryDocumentQuery, + GetAccountTypeByAddressQueryDocumentQueryVariables +>; export const GetBlockTimestampByHeightQueryDocument = { kind: "Document", definitions: [ diff --git a/src/lib/gql/index.ts b/src/lib/gql/index.ts index b3c7f85b8..0ea4a91cf 100644 --- a/src/lib/gql/index.ts +++ b/src/lib/gql/index.ts @@ -1,2 +1,2 @@ -export * from "./gql"; export * from "./fragment-masking"; +export * from "./gql"; diff --git a/src/lib/hooks/useFormatAddresses.ts b/src/lib/hooks/useFormatAddresses.ts index 5fe55401e..2336fe2c4 100644 --- a/src/lib/hooks/useFormatAddresses.ts +++ b/src/lib/hooks/useFormatAddresses.ts @@ -1,23 +1,26 @@ import { useCallback } from "react"; -import { useConvertHexAddress, useValidateAddress } from "lib/app-provider"; +import { useConvertHexAddress } from "lib/app-provider"; import type { HexAddr, HumanAddr } from "lib/types"; -import { bech32AddressToHex, unpadHexAddress } from "lib/utils"; +import { + bech32AddressToHex, + isHexModuleAddress, + isHexWalletAddress, + unpadHexAddress, +} from "lib/utils"; export const useFormatAddresses = () => { const { convertHexWalletAddress, convertHexModuleAddress } = useConvertHexAddress(); - const { validateHexWalletAddress, validateHexModuleAddress } = - useValidateAddress(); return useCallback( (address: string) => { - if (validateHexWalletAddress(address)) + if (isHexWalletAddress(address)) return { address: convertHexWalletAddress(address as HexAddr), hex: unpadHexAddress(address as HexAddr), }; - if (validateHexModuleAddress(address)) + if (isHexModuleAddress(address)) return { address: convertHexModuleAddress(address as HexAddr), hex: unpadHexAddress(address as HexAddr), @@ -27,11 +30,6 @@ export const useFormatAddresses = () => { hex: unpadHexAddress(bech32AddressToHex(address as HumanAddr)), }; }, - [ - convertHexModuleAddress, - convertHexWalletAddress, - validateHexModuleAddress, - validateHexWalletAddress, - ] + [convertHexModuleAddress, convertHexWalletAddress] ); }; diff --git a/src/lib/pages/account-details/index.tsx b/src/lib/pages/account-details/index.tsx index b96e75d58..5d3160d61 100644 --- a/src/lib/pages/account-details/index.tsx +++ b/src/lib/pages/account-details/index.tsx @@ -33,13 +33,8 @@ import { usePublicProjectByAccountAddress, usePublicProjectBySlug, } from "lib/services/publicProjectService"; -import type { HexAddr, HumanAddr, MoveAccountAddr, Option } from "lib/types"; -import { - getFirstQueryParam, - isHexWalletAddress, - isHexModuleAddress, - truncate, -} from "lib/utils"; +import type { Addr, HexAddr, HumanAddr, Option } from "lib/types"; +import { getFirstQueryParam, truncate } from "lib/utils"; import { AccountHeader } from "./components/AccountHeader"; import { AssetsSection } from "./components/asset"; @@ -71,8 +66,8 @@ enum TabIndex { } export interface AccountDetailsBodyProps { - hexAddress: HexAddr; - accountAddress: HumanAddr; + accountAddressParam: string; + tab: TabIndex; } const getAddressOnPath = (hexAddress: HexAddr, accountAddress: HumanAddr) => @@ -81,21 +76,23 @@ const getAddressOnPath = (hexAddress: HexAddr, accountAddress: HumanAddr) => const InvalidAccount = () => ; const AccountDetailsBody = ({ - hexAddress, - accountAddress, + accountAddressParam, + tab, }: AccountDetailsBodyProps) => { const { chainConfig: { extra: { disableDelegation }, }, } = useCelatoneApp(); + const formatAddresses = useFormatAddresses(); const wasm = useWasmConfig({ shouldRedirect: false }); const move = useMoveConfig({ shouldRedirect: false }); // const nft = useNftConfig({ shouldRedirect: false }); const navigate = useInternalNavigate(); const router = useRouter(); - // TODO: remove assertion later - const tab = getFirstQueryParam(router.query.tab) as TabIndex; + + const { address: accountAddress, hex: hexAddress } = + formatAddresses(accountAddressParam); const { data: publicInfo } = usePublicProjectByAccountAddress(accountAddress); const { data: publicInfoBySlug } = usePublicProjectBySlug(publicInfo?.slug); const { data: accountId } = useAccountId(accountAddress); @@ -446,13 +443,12 @@ const AccountDetailsBody = ({ const AccountDetails = () => { const router = useRouter(); - const { validateUserAddress, validateContractAddress } = useValidateAddress(); - const formatAddresses = useFormatAddresses(); + const { isSomeValidAddress } = useValidateAddress(); + // TODO: change to `Addr` for correctness (i.e. interchain account) const accountAddressParam = getFirstQueryParam( router.query.accountAddress - ).toLowerCase() as MoveAccountAddr; - const { address, hex } = formatAddresses(accountAddressParam); + ).toLowerCase() as Addr; // TODO: fix assertion later const tab = getFirstQueryParam(router.query.tab) as TabIndex; @@ -462,13 +458,13 @@ const AccountDetails = () => { return ( - {!isHexWalletAddress(accountAddressParam) && - !isHexModuleAddress(accountAddressParam) && - validateUserAddress(accountAddressParam) && - validateContractAddress(accountAddressParam) ? ( + {!isSomeValidAddress(accountAddressParam) ? ( ) : ( - + )} ); diff --git a/src/lib/query/account.ts b/src/lib/query/account.ts index 1d945ff89..94e73885a 100644 --- a/src/lib/query/account.ts +++ b/src/lib/query/account.ts @@ -7,3 +7,11 @@ export const getAccountIdByAddressQueryDocument = graphql(` } } `); + +export const getAccountTypeByAddressQueryDocument = graphql(` + query getAccountTypeByAddressQueryDocument($address: String!) { + accounts_by_pk(address: $address) { + type + } + } +`); diff --git a/src/lib/services/accountService.ts b/src/lib/services/accountService.ts index 66fa7e702..5c154d5a2 100644 --- a/src/lib/services/accountService.ts +++ b/src/lib/services/accountService.ts @@ -7,8 +7,11 @@ import { useBaseApiRoute, CELATONE_QUERY_KEYS, } from "lib/app-provider"; -import { getAccountIdByAddressQueryDocument } from "lib/query"; -import type { Addr, Balance, Nullable, Option } from "lib/types"; +import { + getAccountIdByAddressQueryDocument, + getAccountTypeByAddressQueryDocument, +} from "lib/query"; +import type { AccountType, Addr, Balance, Nullable, Option } from "lib/types"; import { getAccountBalanceInfo } from "./account"; @@ -49,3 +52,39 @@ export const useAccountId = ( } ); }; + +export const useAccountType = ( + walletAddress: Option, + enabled: boolean, + onSuccess?: (type: AccountType) => void, + onError?: (err: Error) => void +): UseQueryResult => { + const { indexerGraphClient } = useCelatoneApp(); + + const queryFn = useCallback(async () => { + if (!walletAddress) + throw new Error( + "Error fetching account type: failed to retrieve address." + ); + return indexerGraphClient + .request(getAccountTypeByAddressQueryDocument, { + address: walletAddress, + }) + .then( + ({ accounts_by_pk }) => + (accounts_by_pk?.type ?? "BaseAccount") as AccountType + ); + }, [indexerGraphClient, walletAddress]); + + return useQuery( + [CELATONE_QUERY_KEYS.ACCOUNT_TYPE, indexerGraphClient, walletAddress], + queryFn, + { + enabled: enabled && Boolean(walletAddress), + retry: 1, + refetchOnWindowFocus: false, + onSuccess, + onError, + } + ); +}; diff --git a/src/lib/types/account.ts b/src/lib/types/account.ts new file mode 100644 index 000000000..3f412428c --- /dev/null +++ b/src/lib/types/account.ts @@ -0,0 +1,11 @@ +export type AccountType = + | "BaseAccount" + | "InterchainAccount" + | "ModuleAccount" + | "ContinuousVestingAccount" + | "DelayedVestingAccount" + | "ClawbackVestingAccount" + | "ContractAccount" + | "PeriodicVestingAccount" + | "PermanentLockedAccount" + | "BaseVestingAccount"; diff --git a/src/lib/types/index.ts b/src/lib/types/index.ts index 90d56faab..bb50c6593 100644 --- a/src/lib/types/index.ts +++ b/src/lib/types/index.ts @@ -1,6 +1,7 @@ export * from "./currency"; export * from "./move"; export * from "./tx"; +export * from "./account"; export * from "./addrs"; export * from "./asset"; export * from "./block"; From 3197d808cbb7bf7e8e03a5bbecb687e788eb2714 Mon Sep 17 00:00:00 2001 From: songwongtp <16089160+songwongtp@users.noreply.github.com> Date: Tue, 14 Nov 2023 12:00:37 +0700 Subject: [PATCH 2/4] fix: wording --- src/lib/components/modal/account/SaveNewAccount.tsx | 2 +- src/lib/components/modal/account/ToContractButton.tsx | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/lib/components/modal/account/SaveNewAccount.tsx b/src/lib/components/modal/account/SaveNewAccount.tsx index 0eb09f28c..5ebb3ef1e 100644 --- a/src/lib/components/modal/account/SaveNewAccount.tsx +++ b/src/lib/components/modal/account/SaveNewAccount.tsx @@ -93,7 +93,7 @@ export function SaveNewAccountModal({ else { setStatus({ state: "error", - message: "Cannot save contract", + message: "You need to save contract through Contract Details.", }); setIsContract(true); } diff --git a/src/lib/components/modal/account/ToContractButton.tsx b/src/lib/components/modal/account/ToContractButton.tsx index b9831dfcc..833c942ba 100644 --- a/src/lib/components/modal/account/ToContractButton.tsx +++ b/src/lib/components/modal/account/ToContractButton.tsx @@ -28,7 +28,7 @@ export const ToContractButton = () => { ) } > - {isSavedAccounts ? "To Saved Contracts" : "To Contract Details"} + Go to {isSavedAccounts ? "Saved Contracts" : "Contract Details"} ); }; From 568c4aef3f26e23d20304eb0dc567a140f133521 Mon Sep 17 00:00:00 2001 From: songwongtp <16089160+songwongtp@users.noreply.github.com> Date: Tue, 14 Nov 2023 15:04:16 +0700 Subject: [PATCH 3/4] fix: comment --- src/lib/app-provider/hooks/useAddress.ts | 1 - .../modal/account/SaveNewAccount.tsx | 19 ++++++++------- src/lib/pages/account-details/index.tsx | 1 - src/lib/services/accountService.ts | 13 +++++------ src/lib/types/account.ts | 23 ++++++++++--------- 5 files changed, 27 insertions(+), 30 deletions(-) diff --git a/src/lib/app-provider/hooks/useAddress.ts b/src/lib/app-provider/hooks/useAddress.ts index 4f5ad05cd..47af439c8 100644 --- a/src/lib/app-provider/hooks/useAddress.ts +++ b/src/lib/app-provider/hooks/useAddress.ts @@ -98,7 +98,6 @@ export const useGetAddressType = () => { ); }; -// TODO: refactor export const useValidateAddress = () => { const { chain: { bech32_prefix: bech32Prefix }, diff --git a/src/lib/components/modal/account/SaveNewAccount.tsx b/src/lib/components/modal/account/SaveNewAccount.tsx index 5ebb3ef1e..ffa47c5ef 100644 --- a/src/lib/components/modal/account/SaveNewAccount.tsx +++ b/src/lib/components/modal/account/SaveNewAccount.tsx @@ -16,7 +16,7 @@ import { ControllerInput, ControllerTextarea } from "lib/components/forms"; import { useGetMaxLengthError, useHandleAccountSave } from "lib/hooks"; import { useAccountStore } from "lib/providers/store"; import { useAccountType } from "lib/services/accountService"; -import type { Addr } from "lib/types"; +import { AccountType, type Addr } from "lib/types"; import { ToContractButton } from "./ToContractButton"; @@ -85,27 +85,26 @@ export function SaveNewAccountModal({ }); }; - const { refetch } = useAccountType( - addressState as Addr, - false, - (type) => { - if (type !== "ContractAccount") setStatus(statusSuccess); + const { refetch } = useAccountType(addressState as Addr, { + enabled: false, + onSuccess: (type) => { + if (type !== AccountType.ContractAccount) setStatus(statusSuccess); else { setStatus({ state: "error", - message: "You need to save contract through Contract Details.", + message: "You need to save contract through Contract page.", }); setIsContract(true); } }, - (err) => { + onError: (err) => { resetForm(false); setStatus({ state: "error", message: err.message, }); - } - ); + }, + }); useEffect(() => { if (addressState.trim().length === 0) { diff --git a/src/lib/pages/account-details/index.tsx b/src/lib/pages/account-details/index.tsx index 5d3160d61..4542e684f 100644 --- a/src/lib/pages/account-details/index.tsx +++ b/src/lib/pages/account-details/index.tsx @@ -445,7 +445,6 @@ const AccountDetails = () => { const router = useRouter(); const { isSomeValidAddress } = useValidateAddress(); - // TODO: change to `Addr` for correctness (i.e. interchain account) const accountAddressParam = getFirstQueryParam( router.query.accountAddress ).toLowerCase() as Addr; diff --git a/src/lib/services/accountService.ts b/src/lib/services/accountService.ts index 5c154d5a2..1be2a1740 100644 --- a/src/lib/services/accountService.ts +++ b/src/lib/services/accountService.ts @@ -1,4 +1,4 @@ -import type { UseQueryResult } from "@tanstack/react-query"; +import type { UseQueryOptions, UseQueryResult } from "@tanstack/react-query"; import { useQuery } from "@tanstack/react-query"; import { useCallback } from "react"; @@ -55,9 +55,10 @@ export const useAccountId = ( export const useAccountType = ( walletAddress: Option, - enabled: boolean, - onSuccess?: (type: AccountType) => void, - onError?: (err: Error) => void + options: Pick< + UseQueryOptions, + "enabled" | "onSuccess" | "onError" + > = {} ): UseQueryResult => { const { indexerGraphClient } = useCelatoneApp(); @@ -80,11 +81,9 @@ export const useAccountType = ( [CELATONE_QUERY_KEYS.ACCOUNT_TYPE, indexerGraphClient, walletAddress], queryFn, { - enabled: enabled && Boolean(walletAddress), + ...options, retry: 1, refetchOnWindowFocus: false, - onSuccess, - onError, } ); }; diff --git a/src/lib/types/account.ts b/src/lib/types/account.ts index 3f412428c..e24662efb 100644 --- a/src/lib/types/account.ts +++ b/src/lib/types/account.ts @@ -1,11 +1,12 @@ -export type AccountType = - | "BaseAccount" - | "InterchainAccount" - | "ModuleAccount" - | "ContinuousVestingAccount" - | "DelayedVestingAccount" - | "ClawbackVestingAccount" - | "ContractAccount" - | "PeriodicVestingAccount" - | "PermanentLockedAccount" - | "BaseVestingAccount"; +export enum AccountType { + BaseAccount = "BaseAccount", + InterchainAccount = "InterchainAccount", + ModuleAccount = "ModuleAccount", + ContinuousVestingAccount = "ContinuousVestingAccount", + DelayedVestingAccount = "DelayedVestingAccount", + ClawbackVestingAccount = "ClawbackVestingAccount", + ContractAccount = "ContractAccount", + PeriodicVestingAccount = "PeriodicVestingAccount", + PermanentLockedAccount = "PermanentLockedAccount", + BaseVestingAccount = "BaseVestingAccount", +} From e084e089f1b6bc2d681d21140d5ab0402e0d4c90 Mon Sep 17 00:00:00 2001 From: songwongtp <16089160+songwongtp@users.noreply.github.com> Date: Tue, 14 Nov 2023 15:19:16 +0700 Subject: [PATCH 4/4] fix: as comment --- .../components/modal/account/EditSavedAccount.tsx | 1 + src/lib/components/modal/account/SaveNewAccount.tsx | 10 +++++++--- .../components/modal/account/ToContractButton.tsx | 13 +++++++++---- 3 files changed, 17 insertions(+), 7 deletions(-) diff --git a/src/lib/components/modal/account/EditSavedAccount.tsx b/src/lib/components/modal/account/EditSavedAccount.tsx index 3f6334c88..68903b873 100644 --- a/src/lib/components/modal/account/EditSavedAccount.tsx +++ b/src/lib/components/modal/account/EditSavedAccount.tsx @@ -94,6 +94,7 @@ export const EditSavedAccountModal = ({ error={ errors.name && getMaxLengthError(nameState.length, "account_name") } + autoFocus /> ({ state: "init" }); - const [isContract, setIsContract] = useState(false); + const [isContract, setIsContract] = useState(false); const addressState = watch("address"); const nameState = watch("name"); @@ -85,7 +85,7 @@ export function SaveNewAccountModal({ }); }; - const { refetch } = useAccountType(addressState as Addr, { + const { refetch } = useAccountType(addressState, { enabled: false, onSuccess: (type) => { if (type !== AccountType.ContractAccount) setStatus(statusSuccess); @@ -181,7 +181,11 @@ export function SaveNewAccountModal({ autoFocus={!accountAddress} isReadOnly={!!accountAddress} cursor={accountAddress ? "not-allowed" : "pointer"} - helperAction={isContract && } + helperAction={ + isContract && ( + + ) + } /> { +interface ToContractButtonProps { + isAccountPrefilled: boolean; +} + +export const ToContractButton = ({ + isAccountPrefilled, +}: ToContractButtonProps) => { const router = useRouter(); const navigate = useInternalNavigate(); - const isSavedAccounts = router.pathname === "/[network]/saved-accounts"; return ( { minW={16} onClick={() => navigate( - isSavedAccounts + isAccountPrefilled ? { pathname: "/contract-lists/saved-contracts" } : { pathname: "/contracts/[contractAddress]", @@ -28,7 +33,7 @@ export const ToContractButton = () => { ) } > - Go to {isSavedAccounts ? "Saved Contracts" : "Contract Details"} + Go to {isAccountPrefilled ? "Saved Contracts" : "Contract Details"} ); };