diff --git a/system-contract-dapp-playground/__tests__/hedera/hts-interactions/token-transfer-contract/index.test.ts b/system-contract-dapp-playground/__tests__/hedera/hts-interactions/token-transfer-contract/index.test.ts index a45f9ae6e..bf874e21c 100644 --- a/system-contract-dapp-playground/__tests__/hedera/hts-interactions/token-transfer-contract/index.test.ts +++ b/system-contract-dapp-playground/__tests__/hedera/hts-interactions/token-transfer-contract/index.test.ts @@ -33,6 +33,7 @@ import { Contract } from 'ethers'; describe('TokenTransferContract test suite', () => { const responseCode = 22; + const gasLimit = 1000000; const invalidSender = '0xabc'; const senderA = '0xDd7fCb7c2ee96A79B1e201d25F5E43d6a0cED5e6'; const senderB = '0x0851072d7bB726305032Eff23CB8fd22eB74c85B'; @@ -82,23 +83,26 @@ describe('TokenTransferContract test suite', () => { // prepare tokenTransferList: IHederaTokenServiceTokenTransferList const nftTransfers = [ { - senderAcocuntID: senderA, + senderAccountID: senderA, receiverAccountID: receiverA, serialNumber: 3, isApproval: false, }, ]; - const tokenTransferList: IHederaTokenServiceTokenTransferList = { - token: hederaTokenAddress, - transfers, - nftTransfers, - }; + const tokenTransferList: IHederaTokenServiceTokenTransferList[] = [ + { + token: hederaTokenAddress, + transfers, + nftTransfers, + }, + ]; it('should execute transferCrypto then return a successful response code', async () => { const txRes = await transferCrypto( baseContract as unknown as Contract, transferList, - tokenTransferList + tokenTransferList, + gasLimit ); expect(txRes.err).toBeNull; diff --git a/system-contract-dapp-playground/src/api/hedera/tokenTransfer-interactions/index.ts b/system-contract-dapp-playground/src/api/hedera/tokenTransfer-interactions/index.ts index bf08d972c..ea07482fb 100644 --- a/system-contract-dapp-playground/src/api/hedera/tokenTransfer-interactions/index.ts +++ b/system-contract-dapp-playground/src/api/hedera/tokenTransfer-interactions/index.ts @@ -35,18 +35,23 @@ import { handleContractResponse } from '@/utils/contract-interactions/HTS/helper * * @param transferList: IHederaTokenServiceTransferList * - * @param tokenTransferList: IHederaTokenServiceTokenTransferList + * @param tokenTransferList: IHederaTokenServiceTokenTransferList[] + * + * @param gasLimit: number * * @return Promise */ export const transferCrypto = async ( baseContract: Contract, transferList: IHederaTokenServiceTransferList, - tokenTransferList: IHederaTokenServiceTokenTransferList + tokenTransferList: IHederaTokenServiceTokenTransferList[], + gasLimit: number ): Promise => { // invoking contract methods try { - const tx = await baseContract.cryptoTransferPublic(transferList, tokenTransferList); + const tx = await baseContract.cryptoTransferPublic(transferList, tokenTransferList, { + gasLimit, + }); return await handleContractResponse(tx); } catch (err: any) { diff --git a/system-contract-dapp-playground/src/components/contract-interaction/hts/shared/components/CryptoTransferInputFields.tsx b/system-contract-dapp-playground/src/components/contract-interaction/hts/shared/components/CryptoTransferInputFields.tsx new file mode 100644 index 000000000..77e92caa8 --- /dev/null +++ b/system-contract-dapp-playground/src/components/contract-interaction/hts/shared/components/CryptoTransferInputFields.tsx @@ -0,0 +1,127 @@ +/*- + * + * 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 { Tooltip, Select } from '@chakra-ui/react'; +import { SharedFormInputField, SharedRemoveFieldsButton } from './ParamInputForm'; +import { htsCryptoTransferParamFields } from '@/utils/contract-interactions/HTS/token-transfer/paramFieldConstant'; + +interface PageProps { + mode: 'CRYPTO' | 'TOKEN'; + paramValue: any; + masterTokenParam?: any; + handleModifyRecords: any; + setCryptoTransferParamValues?: any; + handleTokenTransferInputOnChange?: any; + handleCryptoTransferInputOnChange?: any; +} + +const CryptoTransferInputFields = ({ + mode, + paramValue, + masterTokenParam, + handleModifyRecords, + setCryptoTransferParamValues, + handleTokenTransferInputOnChange, + handleCryptoTransferInputOnChange, +}: PageProps) => { + return ( +
+ {/* account ID & amount */} + {(['accountID', 'amount'] as ('accountID' | 'amount')[]).map((paramKey) => ( + { + if (mode === 'TOKEN') { + handleTokenTransferInputOnChange( + e, + masterTokenParam.fieldKey, + paramValue.fieldKey, + 'transfers', + paramKey + ); + } else { + handleCryptoTransferInputOnChange( + e, + paramKey, + paramValue.fieldKey, + setCryptoTransferParamValues + ); + } + }} + /> + ))} + + {/* isApproval A*/} +
+ + + +
+ + {/* delete key button */} +
+ +
+
+ ); +}; + +export default CryptoTransferInputFields; diff --git a/system-contract-dapp-playground/src/components/contract-interaction/hts/shared/components/ParamInputForm.tsx b/system-contract-dapp-playground/src/components/contract-interaction/hts/shared/components/ParamInputForm.tsx index d14afe919..a2548d5d2 100644 --- a/system-contract-dapp-playground/src/components/contract-interaction/hts/shared/components/ParamInputForm.tsx +++ b/system-contract-dapp-playground/src/components/contract-interaction/hts/shared/components/ParamInputForm.tsx @@ -28,6 +28,7 @@ import { HEDERA_BRANDING_COLORS } from '@/utils/common/constants'; interface SharedFormInputFieldPageProps { param: string; paramKey: string; + setFieldKey?: any; paramType: string; paramSize: string; paramValue: string; @@ -37,7 +38,7 @@ interface SharedFormInputFieldPageProps { paramClassName: string; paramFocusColor: string; paramPlaceholder: string; - handleInputOnChange: (e: any, param: string, fieldKeyToSet?: string) => void; + handleInputOnChange: (e: any, param: string, fieldKeyToSet?: string, setFieldKey?: any) => void; } export const SharedFormInputField = ({ @@ -48,6 +49,7 @@ export const SharedFormInputField = ({ paramSize, paramValue, explanation, + setFieldKey, fieldKeyToSet, paramClassName, paramFocusColor, @@ -60,7 +62,7 @@ export const SharedFormInputField = ({ value={paramValue} disabled={isDisable} type={paramType} - onChange={(e) => handleInputOnChange(e, param, fieldKeyToSet)} + onChange={(e) => handleInputOnChange(e, param, fieldKeyToSet, setFieldKey)} placeholder={paramPlaceholder} size={paramSize} focusBorderColor={paramFocusColor} @@ -147,6 +149,7 @@ export const SharedExecuteButton = ({ /** @dev shared remove fields button */ interface SharedRemoveFieldButtonPageProps { fieldKey: string; + setFieldKey?: any; handleModifyTokenAddresses: any; } export const SharedRemoveFieldsButton = ({ 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 43568578a..8b9ce8148 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 @@ -61,11 +61,13 @@ interface TransactionResultTablePageProps { | 'TokenMint' | 'TokenCreate' | 'QueryValidity' + | 'CryptoTransfer' | 'TokenAssociate' + | 'QueryTokenStatus' | 'QuerySpecificInfo' - | 'QueryTokenGeneralInfo' + | 'QueryTokenRelation' | 'QueryTokenPermission' - | 'QueryTokenStatus'; + | 'QueryTokenGeneralInfo'; } export const TransactionResultTable = ({ @@ -120,7 +122,9 @@ export const TransactionResultTable = ({ Status Tx hash - Token address + {API !== 'CryptoTransfer' && ( + Token address + )} {API === 'TokenMint' && Recipient} {API === 'TokenAssociate' && ( Associated Account @@ -189,11 +193,14 @@ export const TransactionResultTable = ({
- {/* {withTokenAddress ? ( */} -

- {transactionResult.txHash.slice(0, beginingHashIndex)}... - {transactionResult.txHash.slice(endingHashIndex)} -

+ {API === 'CryptoTransfer' ? ( + transactionResult.txHash + ) : ( +

+ {transactionResult.txHash.slice(0, beginingHashIndex)}... + {transactionResult.txHash.slice(endingHashIndex)} +

+ )}
diff --git a/system-contract-dapp-playground/src/components/contract-interaction/hts/shared/hooks/useToastSuccessful.tsx b/system-contract-dapp-playground/src/components/contract-interaction/hts/shared/hooks/useToastSuccessful.tsx index f31635302..dabeaff21 100644 --- a/system-contract-dapp-playground/src/components/contract-interaction/hts/shared/hooks/useToastSuccessful.tsx +++ b/system-contract-dapp-playground/src/components/contract-interaction/hts/shared/hooks/useToastSuccessful.tsx @@ -31,11 +31,13 @@ interface HookProps { setMetadata?: any; toastTitle: string; isSuccessful: boolean; - resetParamValues: any; + resetParamValues?: any; setTokenAddresses?: any; toastDescription?: string; - setParamValues: Dispatch; + setParamValues?: Dispatch; initialTokenAddressesValues?: any; + setTokenTransferParamValues?: any; + setCryptoTransferParamValues?: any; initialKeyValues?: CommonKeyObject[]; transactionResults: TransactionResult[]; setIsSuccessful: Dispatch>; @@ -63,7 +65,9 @@ export const useToastSuccessful = ({ setKeyTypesToShow, transactionResults, setCurrentTransactionPage, + setTokenTransferParamValues, initialTokenAddressesValues, + setCryptoTransferParamValues, }: HookProps) => { useEffect(() => { if (isSuccessful) { @@ -78,9 +82,11 @@ export const useToastSuccessful = ({ setIsSuccessful(false); if (setKeys) setKeys([]); if (setMetadata) setMetadata([]); - setParamValues(resetParamValues); if (setWithCustomFee) setWithCustomFee(false); + if (setParamValues) setParamValues(resetParamValues); if (initialKeyValues && setKeys) setKeys(initialKeyValues); + if (setTokenTransferParamValues) setTokenTransferParamValues([]); + if (setCryptoTransferParamValues) setCryptoTransferParamValues([]); if (setKeyTypesToShow) setKeyTypesToShow(new Set(HederaTokenKeyTypes)); if (setTokenAddresses) setTokenAddresses([initialTokenAddressesValues]); if (setChosenKeys) setChosenKeys(new Set()); @@ -105,6 +111,8 @@ export const useToastSuccessful = ({ setTokenAddresses, transactionResults.length, setCurrentTransactionPage, + setTokenTransferParamValues, initialTokenAddressesValues, + setCryptoTransferParamValues, ]); }; diff --git a/system-contract-dapp-playground/src/components/contract-interaction/hts/token-transfer-contract/method/index.tsx b/system-contract-dapp-playground/src/components/contract-interaction/hts/token-transfer-contract/method/index.tsx new file mode 100644 index 000000000..7a20854a4 --- /dev/null +++ b/system-contract-dapp-playground/src/components/contract-interaction/hts/token-transfer-contract/method/index.tsx @@ -0,0 +1,41 @@ +/*- + * + * 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 { Contract } from 'ethers'; +import CryptoTransfer from './transferCrypto'; +import { NetworkName } from '@/types/common'; + +interface PageProps { + method: string; + baseContract: Contract; +} + +const HederaTokenTransferMethods = ({ baseContract, method }: PageProps) => { + return ( + <> + {method === 'transferFrom' && <>{method}} + {method === 'transferToken' && <>{method}} + {method === 'transferTokens' && <>{method}} + {method === 'crypto' && } + + ); +}; + +export default HederaTokenTransferMethods; diff --git a/system-contract-dapp-playground/src/components/contract-interaction/hts/token-transfer-contract/method/transferCrypto/CryptoTransferForm.tsx b/system-contract-dapp-playground/src/components/contract-interaction/hts/token-transfer-contract/method/transferCrypto/CryptoTransferForm.tsx new file mode 100644 index 000000000..d40b5e92d --- /dev/null +++ b/system-contract-dapp-playground/src/components/contract-interaction/hts/token-transfer-contract/method/transferCrypto/CryptoTransferForm.tsx @@ -0,0 +1,92 @@ +/*- + * + * 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 { + CryptoTransferParam, + generateInitialCryptoTransferParamValues, +} from './helpers/generateInitialValues'; +import { Dispatch, SetStateAction } from 'react'; +import CryptoTransferInputFields from '../../../shared/components/CryptoTransferInputFields'; + +interface PageProps { + handleModifyTransferRecords: any; + handleCryptoTransferInputOnChange: any; + cryptoTransferParamValues: CryptoTransferParam[]; + setCryptoTransferParamValues: Dispatch>; +} + +const CryptoTransferForm = ({ + cryptoTransferParamValues, + handleModifyTransferRecords, + setCryptoTransferParamValues, + handleCryptoTransferInputOnChange, +}: PageProps) => { + return ( +
+ {/* title */} +
+
+
CRYPTO TRANSFER
+
+
+ + {/* Add more crypto records */} +
+ +
+ + {/* accountID && amount && isApproval */} +
+ {cryptoTransferParamValues.map((paramValue) => ( + + handleModifyTransferRecords( + 'CRYPTO', + 'REMOVE', + setCryptoTransferParamValues, + paramValue.fieldKey + ) + } + handleCryptoTransferInputOnChange={handleCryptoTransferInputOnChange} + setCryptoTransferParamValues={setCryptoTransferParamValues} + /> + ))} +
+
+ ); +}; + +export default CryptoTransferForm; diff --git a/system-contract-dapp-playground/src/components/contract-interaction/hts/token-transfer-contract/method/transferCrypto/TokenTransferForm.tsx b/system-contract-dapp-playground/src/components/contract-interaction/hts/token-transfer-contract/method/transferCrypto/TokenTransferForm.tsx new file mode 100644 index 000000000..2de68ff37 --- /dev/null +++ b/system-contract-dapp-playground/src/components/contract-interaction/hts/token-transfer-contract/method/transferCrypto/TokenTransferForm.tsx @@ -0,0 +1,310 @@ +/*- + * + * 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 { Tooltip, Select } from '@chakra-ui/react'; +import CryptoTransferInputFields from '../../../shared/components/CryptoTransferInputFields'; +import { htsCryptoTransferParamFields } from '@/utils/contract-interactions/HTS/token-transfer/paramFieldConstant'; +import { + SharedFormInputField, + SharedFormButton, + SharedRemoveFieldsButton, +} from '../../../shared/components/ParamInputForm'; +import { + TokenTransferParam, + generateInitialFungibleTokenTransferParamValues, + generateInitialNonFungibleTokenTransferParamValues, +} from './helpers/generateInitialValues'; + +interface PapgeProps { + handleModifyTransferRecords: any; + handleTokenTransferInputOnChange: any; + handleModifyMasterTokenTransferRecords: any; + tokenTransferParamValues: TokenTransferParam[]; + setTokenTransferParamValues: Dispatch>; +} + +const TokenTransferForm = ({ + tokenTransferParamValues, + setTokenTransferParamValues, + handleModifyTransferRecords, + handleTokenTransferInputOnChange, + handleModifyMasterTokenTransferRecords, +}: PapgeProps) => { + return ( +
+ {/* Token Transfer */} +
+ {/* title */} +
+
+
TOKEN TRANSFER
+
+
+ + {/* Add more master token transfer records */} + + + {/* TOKEN TRANSFER RECORDS */} +
+ {tokenTransferParamValues.map((masterTokenParam, index) => ( +
+
+
+ {tokenTransferParamValues.length > 1 && ( +
{`RECORD #${ + index + 1 + }`}
+ )} + + {/* Hedera Token address */} +
+ + setTokenTransferParamValues((prev) => + prev.map((param) => { + if (param.fieldKey === masterTokenParam.fieldKey) { + param.fieldValue.token = e.target.value; + } + return param; + }) + ) + } + /> +
+ + {/* Token Type */} +
+ {/* FUNGILE */} + + setTokenTransferParamValues((prev) => + prev.map((param) => { + if (param.fieldKey === masterTokenParam.fieldKey) { + param.fieldValue.tokenType = 'FUNGIBLE'; + } + return param; + }) + ) + } + explanation={''} + /> + + {/* NON_FUNGIBLE */} + + setTokenTransferParamValues((prev) => + prev.map((param) => { + if (param.fieldKey === masterTokenParam.fieldKey) { + param.fieldValue.tokenType = 'NON_FUNGIBLE'; + } + return param; + }) + ) + } + explanation={''} + /> +
+ + {/* Add more transfer pairs */} + + + {/* accountID && amount && isApprovalA */} + {masterTokenParam.fieldValue.tokenType === 'FUNGIBLE' && ( +
+ {/* {fungibleTokenTransferParamValues.map((paramValue) => ( */} + {masterTokenParam.fieldValue.transfers.map((paramValue) => ( + { + handleModifyTransferRecords( + 'TOKEN', + 'REMOVE', + setTokenTransferParamValues, + paramValue.fieldKey, + undefined, + 'transfers', + masterTokenParam.fieldKey + ); + }} + masterTokenParam={masterTokenParam} + mode={'TOKEN'} + /> + ))} +
+ )} + + {/* senderAccountID && receiverAccountID*/} + {masterTokenParam.fieldValue.tokenType === 'NON_FUNGIBLE' && ( +
+ {masterTokenParam.fieldValue.nftTransfers.map((paramValue) => ( +
+ {( + ['senderAccountID', 'receiverAccountID', 'serialNumber'] as ( + | 'senderAccountID' + | 'receiverAccountID' + | 'serialNumber' + )[] + ).map((paramKey) => ( + + handleTokenTransferInputOnChange( + e, + masterTokenParam.fieldKey, + paramValue.fieldKey, + 'nftTransfers', + paramKey + ) + } + /> + ))} + + {/* isApproval B*/} +
+ + + +
+ + {/* delete key button */} +
+ { + handleModifyTransferRecords( + 'TOKEN', + 'REMOVE', + setTokenTransferParamValues, + paramValue.fieldKey, + undefined, + 'nftTransfers', + masterTokenParam.fieldKey + ); + }} + /> +
+
+ ))} +
+ )} +
+ + {/* delete key button */} +
+ { + handleModifyMasterTokenTransferRecords('REMOVE', masterTokenParam.fieldKey); + }} + /> +
+
+
+
+ ))} +
+
+
+ ); +}; + +export default TokenTransferForm; diff --git a/system-contract-dapp-playground/src/components/contract-interaction/hts/token-transfer-contract/method/transferCrypto/helpers/generateInitialValues.ts b/system-contract-dapp-playground/src/components/contract-interaction/hts/token-transfer-contract/method/transferCrypto/helpers/generateInitialValues.ts new file mode 100644 index 000000000..ee1ae49c0 --- /dev/null +++ b/system-contract-dapp-playground/src/components/contract-interaction/hts/token-transfer-contract/method/transferCrypto/helpers/generateInitialValues.ts @@ -0,0 +1,97 @@ +/*- + * + * 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 { generatedRandomUniqueKey } from '@/utils/common/helpers'; + +export interface CryptoTransferParam { + fieldKey: string; + fieldValue: { + accountID: string; + amount: string; + isApprovalA: boolean; + }; +} + +export interface NonFungibleTokenTransferParam { + fieldKey: string; + fieldValue: { + senderAccountID: string; + receiverAccountID: string; + serialNumber: string; + isApprovalB: boolean; + }; +} + +export interface TokenTransferParam { + fieldKey: string; + fieldValue: { + token: string; + transfers: CryptoTransferParam[]; + tokenType: 'FUNGIBLE' | 'NON_FUNGIBLE'; + nftTransfers: NonFungibleTokenTransferParam[]; + }; +} + +export const generateInitialCryptoTransferParamValues = (): CryptoTransferParam => { + return { + fieldKey: generatedRandomUniqueKey(9), + fieldValue: { + accountID: '', + amount: '', + isApprovalA: false, + }, + }; +}; + +export const generateInitialFungibleTokenTransferParamValues = (): CryptoTransferParam => { + return { + fieldKey: generatedRandomUniqueKey(9), + fieldValue: { + accountID: '', + amount: '', + isApprovalA: false, + }, + }; +}; + +export const generateInitialNonFungibleTokenTransferParamValues = + (): NonFungibleTokenTransferParam => { + return { + fieldKey: generatedRandomUniqueKey(9), + fieldValue: { + senderAccountID: '', + receiverAccountID: '', + serialNumber: '', + isApprovalB: false, + }, + }; + }; + +export const generateInitialTokenTransferParamValues = (): TokenTransferParam => { + return { + fieldKey: generatedRandomUniqueKey(9), + fieldValue: { + token: '', + transfers: [], + nftTransfers: [], + tokenType: 'FUNGIBLE', + }, + }; +}; diff --git a/system-contract-dapp-playground/src/components/contract-interaction/hts/token-transfer-contract/method/transferCrypto/helpers/prepareCryptoTransferValues.ts b/system-contract-dapp-playground/src/components/contract-interaction/hts/token-transfer-contract/method/transferCrypto/helpers/prepareCryptoTransferValues.ts new file mode 100644 index 000000000..41c818e86 --- /dev/null +++ b/system-contract-dapp-playground/src/components/contract-interaction/hts/token-transfer-contract/method/transferCrypto/helpers/prepareCryptoTransferValues.ts @@ -0,0 +1,121 @@ +/*- + * + * 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 { + IHederaTokenServiceNftTransfer, + IHederaTokenServiceAccountAmount, + IHederaTokenServiceTokenTransferList, +} from '@/types/contract-interactions/HTS'; +import { CryptoTransferParam, TokenTransferParam } from './generateInitialValues'; + +interface CryptoTransferPageProps { + contractCaller: string; + cryptoTransferParamValues: CryptoTransferParam[]; +} + +export const prepareCryptoTransferList = ({ + contractCaller, + cryptoTransferParamValues, +}: CryptoTransferPageProps) => { + // prepare total amount + const amountArray = cryptoTransferParamValues.map((prev) => Number(prev.fieldValue.amount)); + const amountTotal = amountArray.reduce((sum, curVal) => sum + curVal, 0); + + let cryptoTransfers: IHederaTokenServiceAccountAmount[] = [ + { + accountID: contractCaller, + amount: amountTotal * -1, + isApproval: false, + }, + ]; + + cryptoTransferParamValues.forEach((prev) => { + cryptoTransfers.push({ + accountID: prev.fieldValue.accountID, + amount: Number(prev.fieldValue.amount), + isApproval: prev.fieldValue.isApprovalA, + }); + }); + + return { + transfers: cryptoTransfers, + }; +}; + +interface TokenTransferPageProps { + tokenTransferParamValues: TokenTransferParam[]; + contractCaller: string; +} + +export const prepareTokenTransferList = ({ + tokenTransferParamValues, + contractCaller, +}: TokenTransferPageProps) => { + let tokenTransferList: IHederaTokenServiceTokenTransferList[] = []; + tokenTransferParamValues.forEach((tokenTransferParamValue) => { + if (tokenTransferParamValue.fieldValue.tokenType === 'FUNGIBLE') { + // prepare total amount + let amountsArray = [] as number[]; + tokenTransferParamValue.fieldValue.transfers.forEach((transfer) => { + amountsArray.push(Number(transfer.fieldValue.amount)); + }); + + const amountsTotal = amountsArray.reduce((sum, curVal) => sum + curVal, 0); + + let tokenTransfers = [ + { + accountID: contractCaller, + amount: amountsTotal * -1, + isApproval: false, + }, + ]; + + tokenTransferParamValue.fieldValue.transfers.forEach((transfer) => { + tokenTransfers.push({ + accountID: transfer.fieldValue.accountID, + amount: Number(transfer.fieldValue.amount), + isApproval: transfer.fieldValue.isApprovalA, + }); + }); + + tokenTransferList.push({ + token: tokenTransferParamValue.fieldValue.token, + transfers: tokenTransfers, + nftTransfers: [], + }); + } else { + let nftTransfers = [] as IHederaTokenServiceNftTransfer[]; + tokenTransferParamValue.fieldValue.nftTransfers.forEach((transfer) => { + nftTransfers.push({ + senderAccountID: transfer.fieldValue.senderAccountID, + receiverAccountID: transfer.fieldValue.receiverAccountID, + serialNumber: Number(transfer.fieldValue.serialNumber), + isApproval: transfer.fieldValue.isApprovalB, + }); + }); + tokenTransferList.push({ + token: tokenTransferParamValue.fieldValue.token, + transfers: [], + nftTransfers, + }); + } + }); + return tokenTransferList; +}; diff --git a/system-contract-dapp-playground/src/components/contract-interaction/hts/token-transfer-contract/method/transferCrypto/index.tsx b/system-contract-dapp-playground/src/components/contract-interaction/hts/token-transfer-contract/method/transferCrypto/index.tsx new file mode 100644 index 000000000..9628e89f7 --- /dev/null +++ b/system-contract-dapp-playground/src/components/contract-interaction/hts/token-transfer-contract/method/transferCrypto/index.tsx @@ -0,0 +1,346 @@ +/*- + * + * 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, useState } from 'react'; +import { useToast } from '@chakra-ui/react'; +import TokenTransferForm from './TokenTransferForm'; +import CryptoTransferForm from './CryptoTransferForm'; +import { transferCrypto } from '@/api/hedera/tokenTransfer-interactions'; +import { handleAPIErrors } from '../../../shared/methods/handleAPIErrors'; +import { TRANSACTION_PAGE_SIZE } from '../../../shared/states/commonStates'; +import { useToastSuccessful } from '../../../shared/hooks/useToastSuccessful'; +import { usePaginatedTxResults } from '../../../shared/hooks/usePaginatedTxResults'; +import { SharedExecuteButtonWithFee } from '../../../shared/components/ParamInputForm'; +import { TransactionResultTable } from '../../../shared/components/TransactionResultTable'; +import { useUpdateTransactionResultsToLocalStorage } from '../../../shared/hooks/useUpdateLocalStorage'; +import { handleRetrievingTransactionResultsFromLocalStorage } from '../../../shared/methods/handleRetrievingTransactionResultsFromLocalStorage'; +import { + IHederaTokenServiceTokenTransferList, + IHederaTokenServiceTransferList, + TransactionResult, +} from '@/types/contract-interactions/HTS'; +import { + CryptoTransferParam, + TokenTransferParam, + generateInitialTokenTransferParamValues, +} from './helpers/generateInitialValues'; +import { + prepareCryptoTransferList, + prepareTokenTransferList, +} from './helpers/prepareCryptoTransferValues'; + +interface PageProps { + baseContract: Contract; +} + +const CryptoTransfer = ({ baseContract }: PageProps) => { + // general states + const toaster = useToast(); + const [gasLimit, setGasLimit] = useState(''); + const [isLoading, setIsLoading] = useState(false); + const [isSuccessful, setIsSuccessful] = useState(false); + const hederaNetwork = JSON.parse(Cookies.get('_network') as string); + const [currentTransactionPage, setCurrentTransactionPage] = useState(1); + const transactionResultStorageKey = 'HEDERA.HTS.TOKEN-TRANSFER.CRYPTO-TRANSFER'; + const contractCaller = JSON.parse(Cookies.get('_connectedAccounts') as string)[0]; + const [transactionResults, setTransactionResults] = useState([]); + const [tokenTransferParamValues, setTokenTransferParamValues] = useState( + [] + ); + const [cryptoTransferParamValues, setCryptoTransferParamValues] = useState( + [] + ); + + /** @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 for tokenTransferParamValue */ + const handleTokenTransferInputOnChange = ( + e: any, + masterFieldKey: string, + fieldKey: string, + transfersType: 'transfers' | 'nftTransfers', + param: + | 'accountID' + | 'amount' + | 'isApprovalA' + | 'senderAccountID' + | 'receiverAccountID' + | 'serialNumber' + | 'isApprovalB' + ) => { + setTokenTransferParamValues((prev) => + prev.map((masterParam) => { + if (masterParam.fieldKey === masterFieldKey) { + (masterParam.fieldValue as any)[transfersType] = (masterParam.fieldValue as any)[ + transfersType + ].map((transfer: any) => { + if (transfer.fieldKey === fieldKey) { + const value = e.target.value; + if (param === 'isApprovalA' || param === 'isApprovalB') { + if (value === '') { + (transfer.fieldValue as any)[param] = false; + } else { + (transfer.fieldValue as any)[param] = JSON.parse(value); + } + } else { + (transfer.fieldValue as any)[param] = e.target.value; + } + } + return transfer; + }); + } + return masterParam; + }) + ); + }; + + /** @dev handle crypto transfer inputs on change */ + const handleCryptoTransferInputOnChange = ( + e: any, + param: + | 'accountID' + | 'amount' + | 'isApprovalA' + | 'senderAccountID' + | 'receiverAccountID' + | 'serialNumber' + | 'isApprovalB', + fieldKey?: string, + setFieldKey?: any + ) => { + const value = e.target.value; + setFieldKey((prev: any) => + prev.map((field: any) => { + if (field.fieldKey === fieldKey) { + if (param === 'isApprovalA' || param === 'isApprovalB') { + if (value === '') { + field.fieldValue[param] = false; + } else { + field.fieldValue[param] = JSON.parse(e.target.value); + } + } else { + field.fieldValue[param] = value; + } + } + return field; + }) + ); + }; + + /** @dev handle modifying transfer records */ + const handleModifyTransferRecords = ( + transferType: 'TOKEN' | 'CRYPTO', + type: 'ADD' | 'REMOVE', + setFieldKey: any, + removingFieldKey?: string, + initialParamValues?: any, + transferListType?: 'transfers' | 'nftTransfers', + masterFieldKey?: string + ) => { + switch (type) { + case 'ADD': + if (transferType === 'CRYPTO') { + setFieldKey((prev: any) => [...prev, initialParamValues]); + } else { + setFieldKey((prev: any) => + prev.map((masterParam: any) => { + if (masterParam.fieldKey === masterFieldKey) { + masterParam.fieldValue[transferListType!].push(initialParamValues); + } + return masterParam; + }) + ); + } + break; + case 'REMOVE': + if (transferType === 'CRYPTO') { + setFieldKey((prev: any) => + prev.filter((field: any) => field.fieldKey !== removingFieldKey) + ); + } else { + setTokenTransferParamValues((prev) => + prev.map((masterParam) => { + if (masterParam.fieldKey === masterFieldKey) { + (masterParam.fieldValue as any)[transferListType!] = ( + masterParam.fieldValue as any + )[transferListType!].filter((field: any) => field.fieldKey !== removingFieldKey); + } + return masterParam; + }) + ); + } + } + }; + + /** + * @dev handle modify master token transfer record + */ + const handleModifyMasterTokenTransferRecords = ( + type: 'ADD' | 'REMOVE', + removingFieldKey?: string + ) => { + switch (type) { + case 'ADD': + setTokenTransferParamValues((prev) => [...prev, generateInitialTokenTransferParamValues()]); + break; + case 'REMOVE': + setTokenTransferParamValues((prev) => + prev.filter((field) => field.fieldKey !== removingFieldKey) + ); + break; + } + }; + + /** @dev handle invoking the API to interact with smart contract and transfer cryptos */ + const handleTransferCrypto = async () => { + let transferList: IHederaTokenServiceTransferList = { transfers: [] }; + let tokenTransferList: IHederaTokenServiceTokenTransferList[] = []; + + // prepare crypto transfer values + if (cryptoTransferParamValues.length > 0) { + transferList = prepareCryptoTransferList({ contractCaller, cryptoTransferParamValues }); + } + + // prepare token transfer values + if (tokenTransferParamValues.length > 0) { + tokenTransferList = prepareTokenTransferList({ tokenTransferParamValues, contractCaller }); + } + + // turn on isLoading + setIsLoading(true); + + // invoke transferCrypto() + const { result, transactionHash, err } = await transferCrypto( + baseContract, + transferList, + tokenTransferList, + Number(gasLimit) + ); + + // turn off isLoading + setIsLoading(false); + + // handle err + if (err || !result) { + handleAPIErrors({ + err, + toaster, + transactionHash, + setTransactionResults, + }); + return; + } else { + // handle succesfull + setTransactionResults((prev) => [ + ...prev, + { + status: 'success', + txHash: transactionHash as string, + }, + ]); + + setIsSuccessful(true); + } + }; + + /** @dev listen to change event on transactionResults state => load to localStorage */ + useUpdateTransactionResultsToLocalStorage(transactionResults, transactionResultStorageKey); + + /** @dev toast successful */ + useToastSuccessful({ + toaster, + isSuccessful, + setIsSuccessful, + transactionResults, + setCurrentTransactionPage, + setTokenTransferParamValues, + setCryptoTransferParamValues, + toastTitle: 'Token update successful', + }); + + return ( +
+ {/* Transfer form */} +
+ {/* Crypto Transfer */} + + + {/* Token Transfer */} + + + {/* Execute button */} + setGasLimit(e.target.value)} + explanation={'Gas limit for the transaction'} + handleInvokingAPIMethod={handleTransferCrypto} + /> +
+ + {/* transaction results table */} + {transactionResults.length > 0 && ( + + )} +
+ ); +}; + +export default CryptoTransfer; diff --git a/system-contract-dapp-playground/src/components/contract-interaction/index.tsx b/system-contract-dapp-playground/src/components/contract-interaction/index.tsx index a3bba2340..df9659f2f 100644 --- a/system-contract-dapp-playground/src/components/contract-interaction/index.tsx +++ b/system-contract-dapp-playground/src/components/contract-interaction/index.tsx @@ -50,6 +50,7 @@ import { PopoverTrigger, Tooltip, } from '@chakra-ui/react'; +import HederaTokenTransferMethods from './hts/token-transfer-contract/method'; interface PageProps { contract: HederaContractAsset; @@ -218,136 +219,160 @@ const ContractInteraction = ({ contract }: PageProps) => { className="overflow-x-scroll overflow-y-hidden no-scrollbar bg-secondary rounded-tl-xl rounded-tr-xl" > {contract.methods.map((method, index) => { - return ( - - {convertCalmelCaseFunctionName(method)} - - ); + if ( + contract.name === 'TokenTransferContract' && + method === 'crypto' && + network !== 'localnet' + ) { + return null; + } else { + return ( + + {convertCalmelCaseFunctionName(method)} + + ); + } })} {/* Tab body */} {contract.methods.map((method) => { - return ( - - {/* Contract information */} -
-
-

Hedera contract ID:

-
-
navigator.clipboard.writeText(contractId)} - > - - -
- -

{contractId}

-
-
-
- -
- Copied -
-
-
-
- - + {/* Contract information */} +
+
+

Hedera contract ID:

+
+
navigator.clipboard.writeText(contractId)} > - - - -
-
-
-

Contract deployed to:

-
-
navigator.clipboard.writeText(contractAddress)} - > - - -
- -

{contractAddress}

-
-
-
- -
- Copied -
-
-
+ + +
+ +

{contractId}

+
+
+
+ +
+ Copied +
+
+
+
+ + + + +
- - +
+

Contract deployed to:

+
+
navigator.clipboard.writeText(contractAddress)} + > + + +
+ +

{contractAddress}

+
+
+
+ +
+ Copied +
+
+
+
+ - - - + + + + +
-
-
+
- {/* Contract methods */} -
- {/* HTS Token Create */} - {contract.name === 'TokenCreateCustomContract' && ( - - )} + {/* Contract methods */} +
+ {/* HTS Token Create */} + {contract.name === 'TokenCreateCustomContract' && ( + + )} - {/* HTS Token Management*/} - {contract.name === 'TokenManagementContract' && ( - - )} + {/* HTS Token Management*/} + {contract.name === 'TokenManagementContract' && ( + + )} - {/* HTS Token Query*/} - {contract.name === 'TokenQueryContract' && ( - - )} + {/* HTS Token Query*/} + {contract.name === 'TokenQueryContract' && ( + + )} - {/* ERC-20 */} - {contract.name === 'ERC20Mock' && ( - - )} -
- - ); + {/* HTS Token Transfer*/} + {contract.name === 'TokenTransferContract' && ( + + )} + + {/* ERC-20 */} + {contract.name === 'ERC20Mock' && ( + + )} +
+ + ); + } })} diff --git a/system-contract-dapp-playground/src/types/contract-interactions/HTS/index.d.ts b/system-contract-dapp-playground/src/types/contract-interactions/HTS/index.d.ts index 9e8e3e04a..136d76080 100644 --- a/system-contract-dapp-playground/src/types/contract-interactions/HTS/index.d.ts +++ b/system-contract-dapp-playground/src/types/contract-interactions/HTS/index.d.ts @@ -355,7 +355,7 @@ interface IHederaTokenServiceNonFungibleTokenInfo { interface IHederaTokenServiceAccountAmount { accountID: string; amount: number; - isApproval: boolean; + isApproval?: boolean; } /** @@ -372,7 +372,7 @@ interface IHederaTokenServiceAccountAmount { * @see https://github.com/hashgraph/hedera-smart-contracts/blob/main/contracts/hts-precompile/IHederaTokenService.sol#L34 */ interface IHederaTokenServiceNftTransfer { - senderAcocuntID: string; + senderAccountID: string; receiverAccountID: string; serialNumber: number; isApproval: boolean; diff --git a/system-contract-dapp-playground/src/utils/common/constants.ts b/system-contract-dapp-playground/src/utils/common/constants.ts index 652660bbf..7c2a3900c 100644 --- a/system-contract-dapp-playground/src/utils/common/constants.ts +++ b/system-contract-dapp-playground/src/utils/common/constants.ts @@ -231,18 +231,7 @@ export const HEDERA_SMART_CONTRACTS_ASSETS = { contractABI: TokenTransferContract.abi, contractBytecode: TokenTransferContract.bytecode, githubUrl: `${HEDERA_SMART_CONTRACT_OFFICIAL_GITHUB_URL}/blob/main/contracts/hts-precompile/examples/token-transfer/TokenTransferContract.sol`, - methods: [ - 'cryptoTransferPublic', - 'transferTokensPublic', - 'transferNFTsPublic', - 'transferTokenPublic', - 'transferNFTPublic', - 'transferFromPublic', - 'transferFromNFTPublic', - 'setApprovalForAllPublic', - 'approvePublic', - 'approveNFTPublic', - ], + methods: ['crypto', 'transferToken', 'transferTokens', 'transferFrom'], }, ], TOKEN_ASSOCIATION: { diff --git a/system-contract-dapp-playground/src/utils/contract-interactions/HTS/token-transfer/paramFieldConstant.ts b/system-contract-dapp-playground/src/utils/contract-interactions/HTS/token-transfer/paramFieldConstant.ts new file mode 100644 index 000000000..53aadef1e --- /dev/null +++ b/system-contract-dapp-playground/src/utils/contract-interactions/HTS/token-transfer/paramFieldConstant.ts @@ -0,0 +1,88 @@ +/*- + * + * 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. + * + */ + +/** @notice an object holding information for the queryTokenInfo's input fields */ +export const htsCryptoTransferParamFields = { + accountID: { + inputType: 'text', + inputPlaceholder: 'Account ID...', + inputSize: 'md', + inputFocusBorderColor: '#A98DF4', + inputClassname: 'w-full border-white/30', + paramKey: 'accountID', + explanation: 'represents the accountID that sends/receives cryptocurrency or tokens', + }, + amount: { + inputType: 'number', + inputPlaceholder: 'Amount...', + inputSize: 'md', + inputFocusBorderColor: '#A98DF4', + inputClassname: 'w-full border-white/30', + paramKey: 'amount', + explanation: + 'represents the the amount of tinybars (for Crypto transfers) or in the lowest denomination (for Token transfers) that the account sends(negative) or receives(positive)', + }, + isApprovalA: { + paramKey: 'isApprovalA', + explanation: + 'If true then the transfer is expected to be an approved allowance and the accountID is expected to be the owner. The default is false (omitted).', + }, + hederaTokenAddress: { + inputType: 'text', + inputPlaceholder: 'Token address...', + inputSize: 'md', + inputFocusBorderColor: '#A98DF4', + inputClassname: 'w-full border-white/30', + paramKey: 'hederaTokenAddress', + explanation: 'represents the Hedera Token address', + }, + senderAccountID: { + inputType: 'text', + inputPlaceholder: 'Sender ID...', + inputSize: 'md', + inputFocusBorderColor: '#A98DF4', + inputClassname: 'w-full border-white/30', + paramKey: 'senderAccountID', + explanation: 'represents the accountID of the sender', + }, + receiverAccountID: { + inputType: 'text', + inputPlaceholder: 'Receiver ID...', + inputSize: 'md', + inputFocusBorderColor: '#A98DF4', + inputClassname: 'w-full border-white/30', + paramKey: 'receiverAccountID', + explanation: 'represents the accountID of the receiver', + }, + serialNumber: { + inputType: 'text', + inputPlaceholder: 'Serial number...', + inputSize: 'md', + inputFocusBorderColor: '#A98DF4', + inputClassname: 'w-full border-white/30', + paramKey: 'serialNumber', + explanation: 'represents the serial number of the NFT', + }, + isApprovalB: { + paramKey: 'isApprovalB', + explanation: + 'If true then the transfer is expected to be an approved allowance and the senderAccountID is expected to be the owner. The default is false (omitted).', + }, +};