From 7e42031d19bc40dceaf331f0b762173781441db4 Mon Sep 17 00:00:00 2001 From: Logan Nguyen Date: Thu, 7 Sep 2023 12:14:15 -0500 Subject: [PATCH] dapp-feat: QueryTokenPermission feature complete (#318) (#377) Signed-off-by: Logan Nguyen Signed-off-by: Mo Shaikjee --- .../components/TokenPermissionInfoModal.tsx | 129 +++++++ .../components/TransactionResultTable.tsx | 31 +- .../methods/handleSanitizeFormInputs.tsx | 24 +- .../token-query-contract/methods/index.tsx | 7 +- .../methods/querySpecificToken/index.tsx | 4 +- .../methods/queryTokenPermission/index.tsx | 319 ++++++++++++++++++ 6 files changed, 498 insertions(+), 16 deletions(-) create mode 100644 system-contract-dapp-playground/src/components/contract-interaction/hts/shared/components/TokenPermissionInfoModal.tsx create mode 100644 system-contract-dapp-playground/src/components/contract-interaction/hts/token-query-contract/methods/queryTokenPermission/index.tsx diff --git a/system-contract-dapp-playground/src/components/contract-interaction/hts/shared/components/TokenPermissionInfoModal.tsx b/system-contract-dapp-playground/src/components/contract-interaction/hts/shared/components/TokenPermissionInfoModal.tsx new file mode 100644 index 000000000..0efba989a --- /dev/null +++ b/system-contract-dapp-playground/src/components/contract-interaction/hts/shared/components/TokenPermissionInfoModal.tsx @@ -0,0 +1,129 @@ +/*- + * + * Hedera Smart Contracts + * + * Copyright (C) 2023 Hedera Hashgraph, LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +import { Dispatch, SetStateAction } from 'react'; +import { convertCalmelCaseFunctionName } from '@/utils/common/helpers'; +import { + Tr, + Th, + Td, + Modal, + Table, + Tbody, + ModalBody, + ModalFooter, + ModalHeader, + ModalOverlay, + ModalContent, + TableContainer, + ModalCloseButton, +} from '@chakra-ui/react'; + +interface PageProps { + APIMethods: 'ALLOWANCE' | 'GET_APPROVED' | 'IS_APPROVAL'; + tokenInfo: any; + eventMaps: any; + isOpen: boolean; + onClose: () => void; + initialParamValues?: any; + setTokenInfo?: Dispatch; + setParamValues?: Dispatch; + setIsSuccessful?: Dispatch>; +} + +const TokenPermissionInfoModal = ({ + APIMethods, + tokenInfo, + isOpen, + onClose, + setTokenInfo, + eventMaps, + setParamValues, + setIsSuccessful, + initialParamValues, +}: PageProps) => { + return ( + { + onClose(); + if (setTokenInfo) setTokenInfo({}); + if (setIsSuccessful) setIsSuccessful(false); + if (setParamValues) setParamValues(initialParamValues); + }} + isCentered + size={'3xl'} + > + + + {/* title */} + Token Information + + + {/* break line */} +
+ + {/* body */} + + + + + + + {APIMethods === 'GET_APPROVED' && ( + + )} + {APIMethods === 'IS_APPROVAL' && ( + + )} + {APIMethods === 'ALLOWANCE' && } + + +
{convertCalmelCaseFunctionName(eventMaps[APIMethods])} + {`${tokenInfo.slice(0, 6)}...${tokenInfo.slice(-6)}`} + + {JSON.stringify(tokenInfo === 1).toUpperCase()} + {tokenInfo}
+
+
+ + {/* footer */} + + + +
+
+ ); +}; + +export default TokenPermissionInfoModal; diff --git a/system-contract-dapp-playground/src/components/contract-interaction/hts/shared/components/TransactionResultTable.tsx b/system-contract-dapp-playground/src/components/contract-interaction/hts/shared/components/TransactionResultTable.tsx index dc1bebc10..0a2b46db3 100644 --- a/system-contract-dapp-playground/src/components/contract-interaction/hts/shared/components/TransactionResultTable.tsx +++ b/system-contract-dapp-playground/src/components/contract-interaction/hts/shared/components/TransactionResultTable.tsx @@ -57,13 +57,14 @@ interface TransactionResultTablePageProps { setTransactionResults: Dispatch>; setKeyTypeFromTxResult?: Dispatch>; API: - | 'TokenCreate' - | 'TokenMint' - | 'TokenAssociate' | 'GrantKYC' + | 'TokenMint' + | 'TokenCreate' | 'QueryValidity' + | 'TokenAssociate' + | 'QuerySpecificInfo' | 'QueryTokenGeneralInfo' - | 'QuerySpecificInfo'; + | 'QueryTokenPermission'; } export const TransactionResultTable = ({ @@ -92,6 +93,7 @@ export const TransactionResultTable = ({ case 'TokenMint': case 'QuerySpecificInfo': case 'QueryTokenGeneralInfo': + case 'QueryTokenPermission': beginingHashIndex = 8; endingHashIndex = -4; break; @@ -120,10 +122,14 @@ export const TransactionResultTable = ({ )} {API === 'GrantKYC' && KYCed Account} {API === 'QueryValidity' && Valid Token} - {(API === 'QueryTokenGeneralInfo' || API === 'QuerySpecificInfo') && ( + {(API === 'QueryTokenGeneralInfo' || + API === 'QuerySpecificInfo' || + API === 'QueryTokenPermission') && ( Token Info )} - {(API === 'QueryTokenGeneralInfo' || API === 'QuerySpecificInfo') && ( + {(API === 'QueryTokenGeneralInfo' || + API === 'QuerySpecificInfo' || + API === 'QueryTokenPermission') && ( API called )} @@ -211,7 +217,8 @@ export const TransactionResultTable = ({ API === 'GrantKYC' || API === 'QueryValidity' || API === 'QueryTokenGeneralInfo' || - API === 'QuerySpecificInfo') && ( + API === 'QuerySpecificInfo' || + API === 'QueryTokenPermission') && ( {transactionResult.tokenAddress ? (
@@ -436,10 +443,12 @@ export const TransactionResultTable = ({ )} {/* query - token info */} - {(API === 'QueryTokenGeneralInfo' || API === 'QuerySpecificInfo') && ( + {(API === 'QueryTokenGeneralInfo' || + API === 'QuerySpecificInfo' || + API === 'QueryTokenPermission') && (
- {transactionResult.tokenInfo ? ( + {typeof transactionResult.tokenInfo !== 'undefined' ? (
{ onOpen!(); @@ -468,7 +477,9 @@ export const TransactionResultTable = ({ )} {/* query - API called */} - {(API === 'QueryTokenGeneralInfo' || API === 'QuerySpecificInfo') && ( + {(API === 'QueryTokenGeneralInfo' || + API === 'QuerySpecificInfo' || + API === 'QueryTokenPermission') && ( {transactionResult.APICalled ? ( <> diff --git a/system-contract-dapp-playground/src/components/contract-interaction/hts/shared/methods/handleSanitizeFormInputs.tsx b/system-contract-dapp-playground/src/components/contract-interaction/hts/shared/methods/handleSanitizeFormInputs.tsx index 69587df12..3d12a7c05 100644 --- a/system-contract-dapp-playground/src/components/contract-interaction/hts/shared/methods/handleSanitizeFormInputs.tsx +++ b/system-contract-dapp-playground/src/components/contract-interaction/hts/shared/methods/handleSanitizeFormInputs.tsx @@ -33,6 +33,8 @@ interface ParamsProps { maxSupply?: string; initSupply?: string; serialNumber?: string; + ownerAddress?: string; + spenderAddress?: string; withCustomFee?: boolean; accountAddress?: string; feeTokenAddress?: string; @@ -60,7 +62,11 @@ interface ParamsProps { | 'WIPE_NON_FUNGIBLE' | 'BURN' | 'DELETE' - | 'QueryTokenInfo'; + | 'QueryTokenInfo' + | 'QueryTokenInfo' + | 'ALLOWANCE' + | 'GET_APPROVED' + | 'IS_APPROVAL'; } /** @dev handle sanitizing Hedera token form inputs */ export const handleSanitizeHederaFormInputs = ({ @@ -75,8 +81,10 @@ export const handleSanitizeHederaFormInputs = ({ feeValue, maxSupply, initSupply, + ownerAddress, serialNumber, withCustomFee, + spenderAddress, accountAddress, feeTokenAddress, autoRenewPeriod, @@ -281,6 +289,20 @@ export const handleSanitizeHederaFormInputs = ({ if (!isAddress(hederaTokenAddress)) { sanitizeErr = 'Invalid token address'; } + } else if (API === 'ALLOWANCE' || API === 'IS_APPROVAL') { + if (!isAddress(hederaTokenAddress)) { + sanitizeErr = 'Invalid token address'; + } else if (!isAddress(ownerAddress)) { + sanitizeErr = 'Invalid owner address'; + } else if (!isAddress(spenderAddress)) { + sanitizeErr = 'Invalid spender address'; + } + } else if (API === 'GET_APPROVED') { + if (!isAddress(hederaTokenAddress)) { + sanitizeErr = 'Invalid token address'; + } else if (serialNumber === '' || Number(serialNumber) < 0) { + sanitizeErr = 'Invalid serial number'; + } } return sanitizeErr; diff --git a/system-contract-dapp-playground/src/components/contract-interaction/hts/token-query-contract/methods/index.tsx b/system-contract-dapp-playground/src/components/contract-interaction/hts/token-query-contract/methods/index.tsx index e2aac10b1..cc95fd125 100644 --- a/system-contract-dapp-playground/src/components/contract-interaction/hts/token-query-contract/methods/index.tsx +++ b/system-contract-dapp-playground/src/components/contract-interaction/hts/token-query-contract/methods/index.tsx @@ -22,6 +22,7 @@ import { Contract } from 'ethers'; import QueryTokenValidity from './queryTokenValidity'; import QueryTokenSpecificInfomation from './querySpecificToken'; import QueryTokenGeneralInfomation from './queryTokenGeneralInfo'; +import QueryTokenPermissionInfomation from './queryTokenPermission'; interface PageProps { method: string; @@ -32,12 +33,12 @@ const HederaTokenQueryMethods = ({ baseContract, method }: PageProps) => { return ( <> {method === 'tokenRelation' && <>tokenRelation} - {method === 'tokenRelation' && <>tokenRelation} - {method === 'tokenPermission' && <>tokenPermission} - {method === 'tokenPermission' && <>tokenPermission} {method === 'tokenValidity' && } {method === 'generalInfo' && } {method === 'specificInfo' && } + {method === 'tokenPermission' && ( + + )} ); }; diff --git a/system-contract-dapp-playground/src/components/contract-interaction/hts/token-query-contract/methods/querySpecificToken/index.tsx b/system-contract-dapp-playground/src/components/contract-interaction/hts/token-query-contract/methods/querySpecificToken/index.tsx index cf8ed76df..204593711 100644 --- a/system-contract-dapp-playground/src/components/contract-interaction/hts/token-query-contract/methods/querySpecificToken/index.tsx +++ b/system-contract-dapp-playground/src/components/contract-interaction/hts/token-query-contract/methods/querySpecificToken/index.tsx @@ -70,10 +70,10 @@ const QueryTokenSpecificInfomation = ({ baseContract }: PageProps) => { const [showTokenInfo, setShowTokenInfo] = useState(false); const hederaNetwork = JSON.parse(Cookies.get('_network') as string); const [currentTransactionPage, setCurrentTransactionPage] = useState(1); - const [APIMethods, setAPIMethods] = useState('DEFAULT_FREEZE_STATUS'); const [tokenInfoFromTxResult, setTokenInfoFromTxResult] = useState(); const [keyType, setKeyType] = useState('ADMIN'); const [tokenAddressFromTxResult, setTokenAddressFromTxResult] = useState(''); + const [APIMethods, setAPIMethods] = useState('DEFAULT_FREEZE_STATUS'); const [transactionResults, setTransactionResults] = useState([]); const transactionResultStorageKey = 'HEDERA.HTS.TOKEN-QUERY.TOKEN-SPECIFIC-INFO-RESULTS'; const [APIMethodsFromTxResult, setAPIMethodsFromTxResult] = @@ -148,7 +148,7 @@ const QueryTokenSpecificInfomation = ({ baseContract }: PageProps) => { setParamValues((prev: any) => ({ ...prev, [param]: e.target.value })); }; - /** @dev handle invoking the API to interact with smart contract and update token relation */ + /** @dev handle invoking the API to interact with smart contract and query token info */ const handleQuerySpecificInfo = async (API: API_NAMES) => { // sanitize params if (!isAddress(paramValues.hederaTokenAddress)) { diff --git a/system-contract-dapp-playground/src/components/contract-interaction/hts/token-query-contract/methods/queryTokenPermission/index.tsx b/system-contract-dapp-playground/src/components/contract-interaction/hts/token-query-contract/methods/queryTokenPermission/index.tsx new file mode 100644 index 000000000..b94ad3398 --- /dev/null +++ b/system-contract-dapp-playground/src/components/contract-interaction/hts/token-query-contract/methods/queryTokenPermission/index.tsx @@ -0,0 +1,319 @@ +/*- + * + * Hedera Smart Contracts + * + * Copyright (C) 2023 Hedera Hashgraph, LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +import Cookies from 'js-cookie'; +import { Contract } from 'ethers'; +import { useEffect, useMemo, useState } from 'react'; +import { useDisclosure, useToast } from '@chakra-ui/react'; +import { CommonErrorToast } from '@/components/toast/CommonToast'; +import { TransactionResult } from '@/types/contract-interactions/HTS'; +import { handleAPIErrors } from '../../../shared/methods/handleAPIErrors'; +import { TRANSACTION_PAGE_SIZE } from '../../../shared/states/commonStates'; +import { usePaginatedTxResults } from '../../../shared/hooks/usePaginatedTxResults'; +import { queryTokenPermissionInformation } from '@/api/hedera/tokenQuery-interactions'; +import TokenPermissionInfoModal from '../../../shared/components/TokenPermissionInfoModal'; +import { TransactionResultTable } from '../../../shared/components/TransactionResultTable'; +import { handleSanitizeHederaFormInputs } from '../../../shared/methods/handleSanitizeFormInputs'; +import { useUpdateTransactionResultsToLocalStorage } from '../../../shared/hooks/useUpdateLocalStorage'; +import { htsQueryTokenPermissionParamFields } from '@/utils/contract-interactions/HTS/token-query/constant'; +import { handleRetrievingTransactionResultsFromLocalStorage } from '../../../shared/methods/handleRetrievingTransactionResultsFromLocalStorage'; +import { + SharedExecuteButton, + SharedFormButton, + SharedFormInputField, +} from '../../../shared/components/ParamInputForm'; + +interface PageProps { + baseContract: Contract; +} + +type API_NAMES = 'ALLOWANCE' | 'GET_APPROVED' | 'IS_APPROVAL'; +type EVENT_NAMES = 'Approved' | 'AllowanceValue' | 'ApprovedAddress'; + +const QueryTokenPermissionInfomation = ({ baseContract }: PageProps) => { + // general states + const toaster = useToast(); + const [isLoading, setIsLoading] = useState(false); + const [tokenInfo, setTokenInfo] = useState(); + const { isOpen, onOpen, onClose } = useDisclosure(); + const [isSuccessful, setIsSuccessful] = useState(false); + const [showTokenInfo, setShowTokenInfo] = useState(false); + const hederaNetwork = JSON.parse(Cookies.get('_network') as string); + const [APIMethods, setAPIMethods] = useState('ALLOWANCE'); + const [currentTransactionPage, setCurrentTransactionPage] = useState(1); + const [tokenInfoFromTxResult, setTokenInfoFromTxResult] = useState(); + const [transactionResults, setTransactionResults] = useState([]); + const transactionResultStorageKey = 'HEDERA.HTS.TOKEN-QUERY.TOKEN-PERMISSION-INFO-RESULTS'; + const [APIMethodsFromTxResult, setAPIMethodsFromTxResult] = useState('ALLOWANCE'); + const initialParamValues = { + hederaTokenAddress: '', + ownerAddress: '', + spenderAddress: '', + serialNumber: '', + }; + const [paramValues, setParamValues] = useState(initialParamValues); + + const tokenCommonFields = useMemo(() => { + switch (APIMethods) { + case 'ALLOWANCE': + case 'IS_APPROVAL': + return ['hederaTokenAddress', 'ownerAddress', 'spenderAddress']; + case 'GET_APPROVED': + return ['hederaTokenAddress', 'serialNumber']; + } + }, [APIMethods]); + + const APIButtonTitles: { API: API_NAMES; apiSwitchTitle: string; executeTitle: any }[] = [ + { + API: 'ALLOWANCE', + apiSwitchTitle: 'Allowance', + executeTitle: 'Query Allowance Value', + }, + { + API: 'GET_APPROVED', + apiSwitchTitle: 'Get Approved', + executeTitle: 'Query Approved Address', + }, + { + API: 'IS_APPROVAL', + apiSwitchTitle: 'Is Approval', + executeTitle: 'Query Approval Status', + }, + ]; + + // prepare events map + const eventMaps: Record = { + IS_APPROVAL: 'Approved', + ALLOWANCE: 'AllowanceValue', + GET_APPROVED: 'ApprovedAddress', + }; + + /** @dev retrieve token creation results from localStorage to maintain data on re-renders */ + useEffect(() => { + handleRetrievingTransactionResultsFromLocalStorage( + toaster, + transactionResultStorageKey, + setCurrentTransactionPage, + setTransactionResults + ); + }, [toaster]); + + // declare a paginatedTransactionResults + const paginatedTransactionResults = usePaginatedTxResults( + currentTransactionPage, + transactionResults + ); + + /** @dev handle form inputs on change */ + const handleInputOnChange = (e: any, param: string) => { + setParamValues((prev: any) => ({ ...prev, [param]: e.target.value })); + }; + + /** @dev handle invoking the API to interact with smart contract and query token info */ + const handleQueryTokenPermissionInfo = async (API: API_NAMES) => { + // destructuring param values + const { hederaTokenAddress, ownerAddress, spenderAddress, serialNumber } = paramValues; + + // sanitize params + const sanitizeErr = handleSanitizeHederaFormInputs({ + API, + hederaTokenAddress, + ownerAddress, + serialNumber, + spenderAddress, + }); + + // toast error if any param is invalid + if (sanitizeErr) { + CommonErrorToast({ toaster, title: 'Invalid parameters', description: sanitizeErr }); + return; + } + + // turn is loading on + setIsLoading(true); + + // invoking method API + const tokenInfoResult = await queryTokenPermissionInformation( + baseContract, + API, + hederaTokenAddress, + ownerAddress, + spenderAddress, + serialNumber + ); + + // turn is loading off + setIsLoading(false); + + // handle err + if (tokenInfoResult.err) { + handleAPIErrors({ + toaster, + APICalled: API, + setTransactionResults, + err: tokenInfoResult.err, + transactionHash: tokenInfoResult.transactionHash, + tokenAddress: paramValues.hederaTokenAddress, + }); + return; + } else { + // handle successful + let cachedTokenInfo = null as any; + if (API === 'GET_APPROVED') { + cachedTokenInfo = `0x${tokenInfoResult[eventMaps[API]].slice(-40)}`; + setTokenInfo(cachedTokenInfo); + } else { + cachedTokenInfo = Number(tokenInfoResult[eventMaps[API]]); + setTokenInfo(cachedTokenInfo); + } + + // udpate transaction result + setTransactionResults((prev) => [ + ...prev, + { + APICalled: API, + status: 'success', + tokenInfo: cachedTokenInfo, + tokenAddress: paramValues.hederaTokenAddress, + txHash: tokenInfoResult.transactionHash as string, + }, + ]); + + // turn on successful + setIsSuccessful(true); + + // open modal + onOpen(); + } + }; + + /** @dev listen to change event on transactionResults state => load to localStorage */ + useUpdateTransactionResultsToLocalStorage(transactionResults, transactionResultStorageKey); + + return ( +
+ {/* Query token form */} +
+ {/* API methods */} +
+ {APIButtonTitles.map((APIButton) => ( +
+ setAPIMethods(APIButton.API)} + explanation={''} + /> +
+ ))} +
+ + {/* hederaTokenAddress & targetApprovedAddress */} + {tokenCommonFields.map((param) => { + return ( +
+ +
+ ); + })} + + {/* Execute buttons */} + {APIButtonTitles.map((APIButton) => { + if (APIMethods === APIButton.API) { + return ( +
+ handleQueryTokenPermissionInfo(APIButton.API)} + buttonTitle={APIButton.executeTitle} + /> +
+ ); + } + })} +
+ + {/* transaction results table */} + {transactionResults.length > 0 && ( + + )} + + {/* token info popup - after done calling the API */} + {isSuccessful && ( + + )} + + {/* token info popup - by clicking on the `Token Info` from transaction Result table*/} + {showTokenInfo && !isSuccessful && ( + + )} +
+ ); +}; + +export default QueryTokenPermissionInfomation;