diff --git a/system-contract-dapp-playground/__tests__/hedera/erc721-interactions/index.test.ts b/system-contract-dapp-playground/__tests__/hedera/erc721-interactions/index.test.ts new file mode 100644 index 000000000..c3e7f7ff4 --- /dev/null +++ b/system-contract-dapp-playground/__tests__/hedera/erc721-interactions/index.test.ts @@ -0,0 +1,352 @@ +/*- + * + * 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 { + erc721Mint, + erc721OwnerOf, + erc721TokenURI, + erc721Transfers, + erc721BalanceOf, + erc721TokenApprove, + erc721TokenApproval, + getERC721TokenInformation, +} from '@/api/hedera/erc721-interactions'; +import { Contract } from 'ethers'; +import { MOCK_TX_HASH } from '../../utils/common/constants'; + +describe('ERC721 test suite', () => { + const tokenID = 369; + const approvalStatus = true; + const expectedSymbol = 'TKN'; + const expectedBalance = '120'; + const expectedName = 'TokenName'; + const expectedTokenURI = 'ipfs://bafyreih7a5ds4th3o'; + const recipient = '0x34810E139b451e0a4c67d5743E956Ac8990842A8'; + const tokenOwner = '0xCC07a8243578590d55c5708D7fB453245350Cc2A'; + const spenderAddress = '0x05FbA803Be258049A27B820088bab1cAD2058871'; + const operatorAddress = '0x0851072d7bB726305032Eff23CB8fd22eB74c85B'; + + const waitMockedObject = { + wait: jest.fn().mockResolvedValue({ + hash: MOCK_TX_HASH, + }), + }; + + // Mock baseContract object + const baseContract = { + name: jest.fn().mockResolvedValue(expectedName), + symbol: jest.fn().mockResolvedValue(expectedSymbol), + tokenURI: jest.fn().mockResolvedValue(expectedTokenURI), + mint: jest.fn().mockResolvedValue(waitMockedObject), + balanceOf: jest.fn().mockResolvedValue(expectedBalance), + ownerOf: jest.fn().mockResolvedValue(tokenOwner), + approve: jest.fn().mockResolvedValue(waitMockedObject), + getApproved: jest.fn().mockResolvedValue(spenderAddress), + setApprovalForAll: jest.fn().mockResolvedValue(waitMockedObject), + isApprovedForAll: jest.fn().mockResolvedValue(approvalStatus), + transferFrom: jest.fn().mockResolvedValue(waitMockedObject), + ['safeTransferFrom(address,address,uint256,bytes)']: jest.fn().mockResolvedValue(waitMockedObject), + }; + + describe('getERC721TokenInformation', () => { + it('should execute name()', async () => { + const res = await getERC721TokenInformation(baseContract as unknown as Contract, 'name'); + + // assertion + expect(res.err).toBeNull; + expect(res.name).toBe(expectedName); + expect(getERC721TokenInformation).toBeCalled; + }); + it('should execute symbol()', async () => { + const res = await getERC721TokenInformation(baseContract as unknown as Contract, 'symbol'); + + // assertion + expect(res.err).toBeNull; + expect(res.symbol).toBe(expectedSymbol); + expect(getERC721TokenInformation).toBeCalled; + }); + }); + + describe('erc721TokenURI', () => { + it('should execute erc721TokenURI()', async () => { + const res = await erc721TokenURI(baseContract as unknown as Contract, tokenID); + + // assertion + expect(res.err).toBeNull; + expect(erc721TokenURI).toBeCalled; + expect(res.tokenURI).toBe(expectedTokenURI); + }); + + it('should execute erc721TokenURI() and return an error if the tokenID is invalid', async () => { + const res = await erc721TokenURI(baseContract as unknown as Contract, -3); + + // assertion + expect(res.tokenURI).toBeNull; + expect(erc721TokenURI).toBeCalled; + expect(res.err).toBe('Invalid token amount'); + }); + }); + + describe('erc721Mint', () => { + it('should execute erc721Mint', async () => { + const res = await erc721Mint(baseContract as unknown as Contract, recipient, tokenID); + + // assertion + expect(res.err).toBeNull; + expect(erc721Mint).toBeCalled; + expect(res.txHash).toBe(MOCK_TX_HASH); + }); + + it('should execute erc721Mint and return error if recipientAddress is invalid', async () => { + const res = await erc721Mint(baseContract as unknown as Contract, '0xabc', tokenID); + + // assertion + expect(res.err).toBe('Invalid recipient address'); + expect(erc721Mint).toBeCalled; + expect(res.txHash).toBeNull; + }); + + it('should execute erc721Mint and return error if tokenID is invalid', async () => { + const res = await erc721Mint(baseContract as unknown as Contract, recipient, -3); + + // assertion + expect(res.err).toBe('Invalid token amount'); + expect(erc721Mint).toBeCalled; + expect(res.txHash).toBeNull; + }); + }); + + describe('erc721BalanceOf', () => { + it('should execute erc721BalanceOf', async () => { + const res = await erc721BalanceOf(baseContract as unknown as Contract, tokenOwner); + + // assertion + expect(res.err).toBeNull; + expect(erc721BalanceOf).toBeCalled; + expect(res.balanceOfRes).toBe(expectedBalance); + }); + + it('should execute erc721BalanceOf and return error if recipientAddress is invalid', async () => { + const res = await erc721BalanceOf(baseContract as unknown as Contract, '0xabc'); + + // assertion + expect(res.err).toBe('Invalid account address'); + expect(erc721BalanceOf).toBeCalled; + expect(res.txHash).toBeNull; + }); + }); + + describe('erc721OwnerOf', () => { + it('should execute erc721OwnerOf', async () => { + const res = await erc721OwnerOf(baseContract as unknown as Contract, tokenID); + + // assertion + expect(res.err).toBeNull; + expect(erc721OwnerOf).toBeCalled; + expect(res.ownerOfRes).toBe(tokenOwner); + }); + }); + + describe('erc721TokenApprove', () => { + it('should execute erc721TokenApprove with method === "APPROVE" and return a txHash', async () => { + const res = await erc721TokenApprove( + baseContract as unknown as Contract, + 'APPROVE', + spenderAddress, + tokenID + ); + + // assertion + expect(res.err).toBeNull; + expect(res.txHash).toBe(MOCK_TX_HASH); + expect(erc721TokenApprove).toBeCalled; + }); + + it('should execute erc721TokenApprove with method === "GET_APPROVE" and return an approved account', async () => { + const res = await erc721TokenApprove( + baseContract as unknown as Contract, + 'GET_APPROVE', + spenderAddress, + tokenID + ); + + // assertion + expect(res.err).toBeNull; + expect(res.approvedAccountRes).toBe(spenderAddress); + expect(erc721TokenApprove).toBeCalled; + }); + + it('should execute erc721TokenApprove and return an error if the spender address is invalid', async () => { + const res = await erc721TokenApprove(baseContract as unknown as Contract, 'APPROVE', '0xabc', tokenID); + + // assertion + expect(res.txHash).toBeNul; + expect(erc721TokenApprove).toBeCalled; + expect(res.approvedAccountRes).toBeNul; + expect(res.err).toBe('Invalid account address'); + }); + }); + + describe('erc721TokenApproval', () => { + it('should execute erc721TokenApproval with method === "SET_APPROVAL" and return a txHash ', async () => { + const res = await erc721TokenApproval( + baseContract as unknown as Contract, + 'SET_APPROVAL', + tokenOwner, + operatorAddress, + approvalStatus + ); + + // assertion + expect(res.err).toBeNull; + expect(res.txHash).toBe(MOCK_TX_HASH); + expect(erc721TokenApproval).toBeCalled; + }); + + it('should execute erc721TokenApproval with method === "IS_APPROVAL" and return the approval status', async () => { + const res = await erc721TokenApproval( + baseContract as unknown as Contract, + 'IS_APPROVAL', + tokenOwner, + operatorAddress, + approvalStatus + ); + + // assertion + expect(res.err).toBeNull; + expect(erc721TokenApproval).toBeCalled; + expect(res.approvalStatusRes).toBe(approvalStatus); + }); + + it('should execute erc721TokenApproval and return error if tokenOwner is invalid', async () => { + const res = await erc721TokenApproval( + baseContract as unknown as Contract, + 'IS_APPROVAL', + '0xabc', + operatorAddress, + approvalStatus + ); + + // assertion + expect(res.txHash).toBeNull; + expect(res.approvalStatusRes).toBeNull; + expect(erc721TokenApproval).toBeCalled; + expect(res.err).toBe('Invalid owner address'); + }); + + it('should execute erc721TokenApproval and return error if operatorAddress is invalid', async () => { + const res = await erc721TokenApproval( + baseContract as unknown as Contract, + 'IS_APPROVAL', + tokenOwner, + '0xabc', + approvalStatus + ); + + // assertion + expect(res.txHash).toBeNull; + expect(res.approvalStatusRes).toBeNull; + expect(erc721TokenApproval).toBeCalled; + expect(res.err).toBe('Invalid operator address'); + }); + }); + + describe('erc721Transfers', () => { + it('should execute erc721Transfers with method === "TRANSFER_FROM" and return a txHash ', async () => { + const res = await erc721Transfers( + baseContract as unknown as Contract, + 'TRANSFER_FROM', + tokenOwner, + recipient, + tokenID, + '' + ); + + // assertion + expect(res.err).toBeNull; + expect(res.txHash).toBe(MOCK_TX_HASH); + expect(erc721Transfers).toBeCalled; + }); + + it('should execute erc721Transfers with method === "SAFE_TRANSFER_FROM" and return a txHash ', async () => { + const res = await erc721Transfers( + baseContract as unknown as Contract, + 'SAFE_TRANSFER_FROM', + tokenOwner, + recipient, + tokenID, + '' + ); + + // assertion + expect(res.err).toBeNull; + expect(res.txHash).toBe(MOCK_TX_HASH); + expect(erc721Transfers).toBeCalled; + }); + + it('should execute erc721Transfers and return an error if senderAddress is invalid ', async () => { + const res = await erc721Transfers( + baseContract as unknown as Contract, + 'SAFE_TRANSFER_FROM', + '0xabc', + recipient, + tokenID, + '' + ); + + // assertion + expect(res.txHash).toBeNull; + expect(erc721Transfers).toBeCalled; + expect(res.err).toBe('Invalid sender address'); + }); + + it('should execute erc721Transfers and return an error if recipientAddress is invalid ', async () => { + const res = await erc721Transfers( + baseContract as unknown as Contract, + 'SAFE_TRANSFER_FROM', + tokenOwner, + '0xabc', + tokenID, + '' + ); + + // assertion + expect(res.txHash).toBeNull; + expect(erc721Transfers).toBeCalled; + expect(res.err).toBe('Invalid recipient address'); + }); + + it('should execute erc721Transfers and return an error if tokenID is invalid ', async () => { + const res = await erc721Transfers( + baseContract as unknown as Contract, + 'SAFE_TRANSFER_FROM', + tokenOwner, + recipient, + -3, + '' + ); + + // assertion + expect(res.txHash).toBeNull; + expect(erc721Transfers).toBeCalled; + expect(res.err).toBe('Invalid tokenId'); + }); + }); +}); diff --git a/system-contract-dapp-playground/__tests__/hedera/ihrc-interactions/index.test.ts b/system-contract-dapp-playground/__tests__/hedera/ihrc-interactions/index.test.ts index 6f6173082..a1845b36c 100644 --- a/system-contract-dapp-playground/__tests__/hedera/ihrc-interactions/index.test.ts +++ b/system-contract-dapp-playground/__tests__/hedera/ihrc-interactions/index.test.ts @@ -38,7 +38,7 @@ jest.mock('ethers', () => { }; }); -describe.only('handleIHR719CAPIs test suite', () => { +describe('handleIHR719CAPIs test suite', () => { it("should execute handleIHRCAPI() with API === 'ASSOCIATE' and return a success response code and a transaction hash", async () => { const txRes = await handleIHRC719APIs( 'ASSOCIATE', diff --git a/system-contract-dapp-playground/src/api/hedera/erc20-interactions/index.ts b/system-contract-dapp-playground/src/api/hedera/erc20-interactions/index.ts index d764aab1d..a08922bce 100644 --- a/system-contract-dapp-playground/src/api/hedera/erc20-interactions/index.ts +++ b/system-contract-dapp-playground/src/api/hedera/erc20-interactions/index.ts @@ -29,12 +29,12 @@ import { Contract, isAddress } from 'ethers'; * * @param method: 'name' | 'symbol' | 'totalSupply' | 'decimals' * - * @return Promise + * @return Promise */ export const getERC20TokenInformation = async ( baseContract: Contract, method: 'name' | 'symbol' | 'totalSupply' | 'decimals' -): Promise => { +): Promise => { try { switch (method) { case 'name': @@ -61,13 +61,13 @@ export const getERC20TokenInformation = async ( * * @param tokenAmount: number * - * @return Promise + * @return Promise */ export const erc20Mint = async ( baseContract: Contract, recipientAddress: string, tokenAmount: number -): Promise => { +): Promise => { if (!isAddress(recipientAddress)) { return { err: 'Invalid recipient address' }; } else if (tokenAmount <= 0) { @@ -90,12 +90,12 @@ export const erc20Mint = async ( * * @param accountAddress: address * - * @return Promise + * @return Promise */ export const balanceOf = async ( baseContract: Contract, accountAddress: string -): Promise => { +): Promise => { if (!isAddress(accountAddress)) { return { err: 'Invalid account address' }; } @@ -129,7 +129,7 @@ export const balanceOf = async ( * * @param amount?: number * - * @return Promise + * @return Promise */ export const handleErc20TokenPermissions = async ( baseContract: Contract, @@ -137,7 +137,7 @@ export const handleErc20TokenPermissions = async ( spenderAddress: string, ownerAddress?: string, amount?: number -): Promise => { +): Promise => { // sanitize params if (ownerAddress && !isAddress(ownerAddress)) { return { err: 'Invalid owner address' }; @@ -172,7 +172,7 @@ export const handleErc20TokenPermissions = async ( }; /** - * @dev handle executing APIs relate to Token Transfer + * @dev handle executing APIs relate to Token Transfer * * @dev transfer() moves amount tokens from the caller’s account to `recipient`. * @@ -188,7 +188,7 @@ export const handleErc20TokenPermissions = async ( * * @param tokenOwnerAddress?: address * - * @return Promise + * @return Promise */ export const erc20Transfers = async ( baseContract: Contract, @@ -196,7 +196,7 @@ export const erc20Transfers = async ( recipientAddress: string, amount: number, tokenOwnerAddress?: string -): Promise => { +): Promise => { if (method === 'transferFrom' && !isAddress(tokenOwnerAddress)) { return { err: 'Invalid token owner address' }; } else if (!isAddress(recipientAddress)) { @@ -206,9 +206,7 @@ export const erc20Transfers = async ( try { switch (method) { case 'transfer': - const transferReceipt = await ( - await baseContract.transfer(recipientAddress, amount) - ).wait(); + const transferReceipt = await (await baseContract.transfer(recipientAddress, amount)).wait(); return { transferRes: true, txHash: transferReceipt.hash }; case 'transferFrom': const transferFromReceipt = await ( diff --git a/system-contract-dapp-playground/src/api/hedera/erc721-interactions/index.ts b/system-contract-dapp-playground/src/api/hedera/erc721-interactions/index.ts new file mode 100644 index 000000000..c0eecb2d4 --- /dev/null +++ b/system-contract-dapp-playground/src/api/hedera/erc721-interactions/index.ts @@ -0,0 +1,303 @@ +/*- + * + * 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, ethers, isAddress } from 'ethers'; + +/** + * @dev get token information + * + * @notice execute name(), symbol() + * + * @param baseContract: Contract + * + * @param method: 'name' | 'symbol' + * + * @return Promise + */ +export const getERC721TokenInformation = async ( + baseContract: Contract, + method: 'name' | 'symbol' +): Promise => { + try { + switch (method) { + case 'name': + return { name: await baseContract.name() }; + case 'symbol': + return { symbol: await baseContract.symbol() }; + } + } catch (err) { + console.error(err); + return { err }; + } +}; + +/** + * @dev get token URI of the tokenId token + * + * @param baseContract: Contract + * + * @param tokenId: number + * + * @return Promise + */ +export const erc721TokenURI = async ( + baseContract: Contract, + tokenId: number +): Promise => { + if (tokenId < 0) { + return { err: 'Invalid token amount' }; + } + + try { + return { tokenURI: (await baseContract.tokenURI(tokenId)).toString() }; + } catch (err) { + console.error(err); + return { err }; + } +}; + +/** + * @dev mints erc721 tokens + * + * @param baseContract: Contract + * + * @param recipientAddress: address + * + * @param tokenId: number + * + * @return Promise + */ +export const erc721Mint = async ( + baseContract: Contract, + recipientAddress: string, + tokenId: number +): Promise => { + if (!isAddress(recipientAddress)) { + return { err: 'Invalid recipient address' }; + } else if (tokenId < 0) { + return { err: 'Invalid token amount' }; + } + + try { + const txReceipt = await (await baseContract.mint(recipientAddress, tokenId)).wait(); + return { txHash: txReceipt.hash }; + } catch (err) { + console.error(err); + return { err }; + } +}; + +/** + * @dev get token balance owned by `accountAddress` + * + * @param baseContract: Contract + * + * @param accountAddress: address + * + * @return Promise + */ +export const erc721BalanceOf = async ( + baseContract: Contract, + accountAddress: string +): Promise => { + if (!isAddress(accountAddress)) { + return { err: 'Invalid account address' }; + } + + try { + return { balanceOfRes: (await baseContract.balanceOf(accountAddress)).toString() }; + } catch (err) { + console.error(err); + return { err }; + } +}; + +/** + * @dev gets the token owner of the `tokenId` token + * + * @param baseContract: Contract + * + * @param tokenId: number + * + * @return Promise + */ +export const erc721OwnerOf = async ( + baseContract: Contract, + tokenId: number +): Promise => { + try { + return { ownerOfRes: (await baseContract.ownerOf(tokenId)).toString() }; + } catch (err) { + console.error(err); + return { err }; + } +}; + +/** + * @dev integrates ERC721.approve() + * + * @dev integrates ERC721.getApproved() + * + * @param baseContract: Contract + * + * @param method: 'APPROVE' | 'GET_APPROVE' + * + * @param spenderAddress: string + * + * @param tokenId: number + * + * @return Promise + */ +export const erc721TokenApprove = async ( + baseContract: Contract, + method: 'APPROVE' | 'GET_APPROVE', + spenderAddress: string, + tokenId: number +): Promise => { + if (method === 'APPROVE' && !isAddress(spenderAddress)) { + return { err: 'Invalid account address' }; + } + + try { + switch (method) { + case 'APPROVE': + const approveReceipt = await (await baseContract.approve(spenderAddress, tokenId)).wait(); + return { txHash: approveReceipt.hash }; + case 'GET_APPROVE': + return { approvedAccountRes: await baseContract.getApproved(tokenId) }; + } + } catch (err) { + console.error(err); + return { err }; + } +}; + +/** + * @dev integrates ERC721.setApprovalForAll() + * + * @dev integrates ERC721.isApprovedForAll() + * + * @param baseContract: Contract + * + * @param method: 'SET_APPROVAL' | 'IS_APPROVAL' + * + * @param ownerAddress: string + * + * @param operatorAddress: string + * + * @param approvalStatus: boolean + * + * @return Promise + */ +export const erc721TokenApproval = async ( + baseContract: Contract, + method: 'SET_APPROVAL' | 'IS_APPROVAL', + ownerAddress: string, + operatorAddress: string, + approvalStatus: boolean +): Promise => { + if (method === 'IS_APPROVAL' && !isAddress(ownerAddress)) { + return { err: 'Invalid owner address' }; + } else if (!isAddress(operatorAddress)) { + return { err: 'Invalid operator address' }; + } + + try { + switch (method) { + case 'SET_APPROVAL': + const approveReceipt = await ( + await baseContract.setApprovalForAll(operatorAddress, approvalStatus) + ).wait(); + return { txHash: approveReceipt.hash }; + case 'IS_APPROVAL': + return { + approvalStatusRes: await baseContract.isApprovedForAll(ownerAddress, operatorAddress), + }; + } + } catch (err) { + console.error(err); + return { err }; + } +}; + +/** + * @dev handle executing APIs relate to Token Transfer + * + * @dev integrates ERC721.transferFrom() + * + * @dev integrates ERC721.safeTransferFrom() + * + * @param baseContract: Contract + * + * @param method: "TRANSFER_FROM" | "SAFE_TRANSFER_FROM" + * + * @param senderAddress: string + * + * @param recipientAddress: string + * + * @param tokenId: number + * + * @param data: string + * + * @return Promise + */ +export const erc721Transfers = async ( + baseContract: Contract, + method: 'TRANSFER_FROM' | 'SAFE_TRANSFER_FROM', + senderAddress: string, + recipientAddress: string, + tokenId: number, + data: string +): Promise => { + if (!isAddress(senderAddress)) { + return { err: 'Invalid sender address' }; + } else if (!isAddress(recipientAddress)) { + return { err: 'Invalid recipient address' }; + } else if (tokenId < 0) { + return { err: 'Invalid tokenId' }; + } + + try { + switch (method) { + case 'TRANSFER_FROM': + const transferReceipt = await ( + await baseContract.transferFrom(senderAddress, recipientAddress, tokenId) + ).wait(); + return { txHash: transferReceipt.hash }; + + case 'SAFE_TRANSFER_FROM': + // Typed function signature to specify the safeTransferFrom function + const safeTransferFunctionSignature = 'safeTransferFrom(address,address,uint256,bytes)'; + + const safeTransferReceipt = await ( + await baseContract[safeTransferFunctionSignature]( + senderAddress, + recipientAddress, + tokenId, + ethers.toUtf8Bytes(data) + ) + ).wait(); + return { txHash: safeTransferReceipt.hash }; + } + } catch (err: any) { + console.error(err); + return { err, txHash: err.receipt && err.receipt.hash }; + } +}; diff --git a/system-contract-dapp-playground/src/components/contract-interaction/erc/erc-20/methods/BalanceOf.tsx b/system-contract-dapp-playground/src/components/contract-interaction/erc/erc-20/methods/balance-of/index.tsx similarity index 88% rename from system-contract-dapp-playground/src/components/contract-interaction/erc/erc-20/methods/BalanceOf.tsx rename to system-contract-dapp-playground/src/components/contract-interaction/erc/erc-20/methods/balance-of/index.tsx index fa304a91a..9324e6b31 100644 --- a/system-contract-dapp-playground/src/components/contract-interaction/erc/erc-20/methods/BalanceOf.tsx +++ b/system-contract-dapp-playground/src/components/contract-interaction/erc/erc-20/methods/balance-of/index.tsx @@ -23,25 +23,29 @@ import { BiCopy } from 'react-icons/bi'; import { Contract, isAddress } from 'ethers'; import { AiOutlineMinus } from 'react-icons/ai'; import { IoRefreshOutline } from 'react-icons/io5'; -import { ReactNode, useEffect, useState } from 'react'; +import { ReactNode, useCallback, useEffect, useState } from 'react'; import { balanceOf } from '@/api/hedera/erc20-interactions'; import { getBalancesFromLocalStorage } from '@/api/localStorage'; import { CommonErrorToast } from '@/components/toast/CommonToast'; import HederaCommonTextField from '@/components/common/HederaCommonTextField'; -import { HEDERA_BRANDING_COLORS, HEDERA_CHAKRA_INPUT_BOX_SIZES } from '@/utils/common/constants'; import { - Popover, - PopoverContent, - PopoverTrigger, - Table, - TableContainer, - Tbody, + HEDERA_BRANDING_COLORS, + HEDERA_CHAKRA_INPUT_BOX_SIZES, + HEDERA_TRANSACTION_RESULT_STORAGE_KEYS, +} from '@/utils/common/constants'; +import { Td, Th, + Tr, + Table, Thead, + Tbody, Tooltip, - Tr, + Popover, useToast, + PopoverContent, + PopoverTrigger, + TableContainer, } from '@chakra-ui/react'; interface PageProps { @@ -54,13 +58,12 @@ const BalanceOf = ({ baseContract }: PageProps) => { const [accountAddress, setAccountAddress] = useState(''); const [balancesMap, setBalancesMap] = useState(new Map()); const [balancesRactNodes, setBalancesReactNodes] = useState([]); - const transactionResultStorageKey = 'HEDERA.EIP.ERC-20.BALANCE-OF-RESULTS.READONLY'; + const transactionResultStorageKey = HEDERA_TRANSACTION_RESULT_STORAGE_KEYS['ERC20-RESULT']['BALANCE-OF']; /** @dev retrieve balances from localStorage to maintain data on re-renders */ useEffect(() => { - const { storageBalances, err: localStorageBalanceErr } = getBalancesFromLocalStorage( - transactionResultStorageKey - ); + const { storageBalances, err: localStorageBalanceErr } = + getBalancesFromLocalStorage(transactionResultStorageKey); // handle err if (localStorageBalanceErr) { CommonErrorToast({ @@ -75,7 +78,7 @@ const BalanceOf = ({ baseContract }: PageProps) => { if (storageBalances) { setBalancesMap(storageBalances); } - }, [toaster]); + }, [toaster, transactionResultStorageKey]); /** @dev copy content to clipboard */ const copyWalletAddress = (content: string) => { @@ -83,15 +86,18 @@ const BalanceOf = ({ baseContract }: PageProps) => { }; /** @dev handle remove record */ - const handleRemoveRecord = (addr: string) => { - setBalancesMap((prev) => { - prev.delete(addr); - if (prev.size === 0) { - localStorage.removeItem(transactionResultStorageKey); - } - return new Map(prev); - }); - }; + const handleRemoveRecord = useCallback( + (addr: string) => { + setBalancesMap((prev) => { + prev.delete(addr); + if (prev.size === 0) { + localStorage.removeItem(transactionResultStorageKey); + } + return new Map(prev); + }); + }, + [transactionResultStorageKey] + ); /** @dev handle executing balance of */ const handleExecuteBalanceOf = async () => { @@ -199,12 +205,9 @@ const BalanceOf = ({ baseContract }: PageProps) => { //// update local storage if (balancesMap.size > 0) { - localStorage.setItem( - transactionResultStorageKey, - JSON.stringify(Object.fromEntries(balancesMap)) - ); + localStorage.setItem(transactionResultStorageKey, JSON.stringify(Object.fromEntries(balancesMap))); } - }, [balancesMap, baseContract, toaster]); + }, [balancesMap, baseContract, toaster, handleRemoveRecord, transactionResultStorageKey]); return (
diff --git a/system-contract-dapp-playground/src/components/contract-interaction/erc/erc-20/methods/index.tsx b/system-contract-dapp-playground/src/components/contract-interaction/erc/erc-20/methods/index.tsx index 80106d380..d413f3645 100644 --- a/system-contract-dapp-playground/src/components/contract-interaction/erc/erc-20/methods/index.tsx +++ b/system-contract-dapp-playground/src/components/contract-interaction/erc/erc-20/methods/index.tsx @@ -18,12 +18,12 @@ * */ -import Mint from './Mint'; +import Mint from './mint'; import { Contract } from 'ethers'; -import Transfer from './Transfer'; -import BalanceOf from './BalanceOf'; -import TokenPermission from './TokenPermissions'; -import TokenInformation from './TokenInformation'; +import BalanceOf from './balance-of'; +import Transfer from './token-transfer'; +import TokenPermission from './token-permission'; +import TokenInformation from './token-information'; interface PageProps { method: string; diff --git a/system-contract-dapp-playground/src/components/contract-interaction/erc/erc-20/methods/Mint.tsx b/system-contract-dapp-playground/src/components/contract-interaction/erc/erc-20/methods/mint/index.tsx similarity index 95% rename from system-contract-dapp-playground/src/components/contract-interaction/erc/erc-20/methods/Mint.tsx rename to system-contract-dapp-playground/src/components/contract-interaction/erc/erc-20/methods/mint/index.tsx index 94a6effd5..b0f948c88 100644 --- a/system-contract-dapp-playground/src/components/contract-interaction/erc/erc-20/methods/Mint.tsx +++ b/system-contract-dapp-playground/src/components/contract-interaction/erc/erc-20/methods/mint/index.tsx @@ -26,6 +26,7 @@ import { CommonErrorToast } from '@/components/toast/CommonToast'; import MultiLineMethod from '@/components/common/MultiLineMethod'; import { TransactionResult } from '@/types/contract-interactions/HTS'; import { mintParamFields } from '@/utils/contract-interactions/erc/constant'; +import { HEDERA_TRANSACTION_RESULT_STORAGE_KEYS } from '@/utils/common/constants'; import { handleAPIErrors } from '@/components/contract-interaction/hts/shared/methods/handleAPIErrors'; import { useUpdateTransactionResultsToLocalStorage } from '@/components/contract-interaction/hts/shared/hooks/useUpdateLocalStorage'; @@ -37,8 +38,8 @@ const Mint = ({ baseContract }: PageProps) => { const toaster = useToast(); const [isLoading, setIsLoading] = useState(false); const [isSuccessful, setIsSuccessful] = useState(false); - const transactionResultStorageKey = 'HEDERA.EIP.ERC-20.TOKEN-MINT-RESULTS'; const [transactionResults, setTransactionResults] = useState([]); + const transactionResultStorageKey = HEDERA_TRANSACTION_RESULT_STORAGE_KEYS['ERC20-RESULT']['TOKEN-MINT']; const [mintParams, setMintParams] = useState({ recipient: '', amount: '', diff --git a/system-contract-dapp-playground/src/components/contract-interaction/erc/erc-20/methods/TokenInformation.tsx b/system-contract-dapp-playground/src/components/contract-interaction/erc/erc-20/methods/token-information/index.tsx similarity index 100% rename from system-contract-dapp-playground/src/components/contract-interaction/erc/erc-20/methods/TokenInformation.tsx rename to system-contract-dapp-playground/src/components/contract-interaction/erc/erc-20/methods/token-information/index.tsx diff --git a/system-contract-dapp-playground/src/components/contract-interaction/erc/erc-20/methods/TokenPermissions.tsx b/system-contract-dapp-playground/src/components/contract-interaction/erc/erc-20/methods/token-permission/index.tsx similarity index 91% rename from system-contract-dapp-playground/src/components/contract-interaction/erc/erc-20/methods/TokenPermissions.tsx rename to system-contract-dapp-playground/src/components/contract-interaction/erc/erc-20/methods/token-permission/index.tsx index 29f642701..1eea272c8 100644 --- a/system-contract-dapp-playground/src/components/contract-interaction/erc/erc-20/methods/TokenPermissions.tsx +++ b/system-contract-dapp-playground/src/components/contract-interaction/erc/erc-20/methods/token-permission/index.tsx @@ -24,7 +24,7 @@ import { BiCopy } from 'react-icons/bi'; import { AiOutlineMinus } from 'react-icons/ai'; import { IoRefreshOutline } from 'react-icons/io5'; import { CommonErrorToast } from '@/components/toast/CommonToast'; -import { HEDERA_BRANDING_COLORS } from '@/utils/common/constants'; +import { HEDERA_BRANDING_COLORS, HEDERA_TRANSACTION_RESULT_STORAGE_KEYS } from '@/utils/common/constants'; import MultiLineMethod from '@/components/common/MultiLineMethod'; import { Dispatch, SetStateAction, useEffect, useState } from 'react'; import { getArrayTypedValuesFromLocalStorage } from '@/api/localStorage'; @@ -67,9 +67,10 @@ type Allowance = { const TokenPermission = ({ baseContract }: PageProps) => { const toaster = useToast(); const [allowances, setAllowances] = useState([]); - const allowanceStorageKey = 'HEDERA.EIP.ERC-20.ALLOWANCES-RESULTS.READONLY'; - const transactionResultStorageKey = 'HEDERA.EIP.ERC-20.TOKEN-PERMISSIONS-RESULTS'; const [transactionResults, setTransactionResults] = useState([]); + const transactionResultStorageKey = + HEDERA_TRANSACTION_RESULT_STORAGE_KEYS['ERC20-RESULT']['TOKEN-PERMISSION']; + const allowanceStorageKey = HEDERA_TRANSACTION_RESULT_STORAGE_KEYS['ERC20-RESULT']['ALLOWANCES-RESULT']; const [successStatus, setSuccessStatus] = useState({ approve: false, increaseAllowance: false, @@ -130,7 +131,7 @@ const TokenPermission = ({ baseContract }: PageProps) => { undefined, setTransactionResults ); - }, [toaster]); + }, [toaster, transactionResultStorageKey]); /** @dev retrieve allowances from localStorage to maintain data on re-renders */ useEffect(() => { @@ -150,7 +151,7 @@ const TokenPermission = ({ baseContract }: PageProps) => { if (storageResult) { setAllowances(storageResult as Allowance[]); } - }, [toaster]); + }, [toaster, allowanceStorageKey]); /** * @dev handle execute methods @@ -224,10 +225,7 @@ const TokenPermission = ({ baseContract }: PageProps) => { amount: Number(tokenPermissionRes.allowanceRes!), }; const newAllowances = allowances.map((allowance) => { - if ( - allowance.owner === allowanceObj.owner && - allowance.spender === allowanceObj.spender - ) { + if (allowance.owner === allowanceObj.owner && allowance.spender === allowanceObj.spender) { allowance.amount = Number(tokenPermissionRes.allowanceRes!); duplicated = true; } @@ -273,15 +271,11 @@ const TokenPermission = ({ baseContract }: PageProps) => { if (allowances.length > 0) { localStorage.setItem(allowanceStorageKey, JSON.stringify(allowances)); } - }, [allowances]); + }, [allowances, allowanceStorageKey]); // toast executing successful useEffect(() => { - if ( - successStatus.approve || - successStatus.increaseAllowance || - successStatus.decreaseAllowance - ) { + if (successStatus.approve || successStatus.increaseAllowance || successStatus.decreaseAllowance) { let title = ''; if (successStatus.approve) { title = 'Approve successful 🎉'; @@ -331,11 +325,7 @@ const TokenPermission = ({ baseContract }: PageProps) => { setParams={setIncreaseAllowanceParams} isLoading={methodState.increaseAllowance.isLoading} handleExecute={() => - handleExecutingMethods( - 'increaseAllowance', - increaseAllowanceParams, - setIncreaseAllowanceParams - ) + handleExecutingMethods('increaseAllowance', increaseAllowanceParams, setIncreaseAllowanceParams) } explanation="Atomically increases the allowance granted to spender by the caller." /> @@ -348,11 +338,7 @@ const TokenPermission = ({ baseContract }: PageProps) => { setParams={setDecreaseAllowanceParams} isLoading={methodState.decreaseAllowance.isLoading} handleExecute={() => - handleExecutingMethods( - 'decreaseAllowance', - decreaseAllowanceParams, - setDecreaseAllowanceParams - ) + handleExecutingMethods('decreaseAllowance', decreaseAllowanceParams, setDecreaseAllowanceParams) } explanation="Atomically decreases the allowance granted to spender by the caller." /> @@ -367,9 +353,7 @@ const TokenPermission = ({ baseContract }: PageProps) => { widthSize="w-[360px]" setParams={setAllowanceParams} isLoading={methodState.allowance.isLoading} - handleExecute={() => - handleExecutingMethods('allowance', allowanceParams, setAllowanceParams) - } + handleExecute={() => handleExecutingMethods('allowance', allowanceParams, setAllowanceParams)} explanation="Returns the remaining number of tokens that `spender` will be allowed to spend on behalf of `owner` through `transferFrom` function." />
@@ -447,10 +431,7 @@ const TokenPermission = ({ baseContract }: PageProps) => { }; return ( - copyWalletAddress(allowance.owner)} - className="cursor-pointer" - > + copyWalletAddress(allowance.owner)} className="cursor-pointer">
@@ -463,16 +444,11 @@ const TokenPermission = ({ baseContract }: PageProps) => {
-
- Copied -
+
Copied
- copyWalletAddress(allowance.spender)} - className="cursor-pointer" - > + copyWalletAddress(allowance.spender)} className="cursor-pointer">
@@ -485,9 +461,7 @@ const TokenPermission = ({ baseContract }: PageProps) => {
-
- Copied -
+
Copied
diff --git a/system-contract-dapp-playground/src/components/contract-interaction/erc/erc-20/methods/Transfer.tsx b/system-contract-dapp-playground/src/components/contract-interaction/erc/erc-20/methods/token-transfer/index.tsx similarity index 93% rename from system-contract-dapp-playground/src/components/contract-interaction/erc/erc-20/methods/Transfer.tsx rename to system-contract-dapp-playground/src/components/contract-interaction/erc/erc-20/methods/token-transfer/index.tsx index de9cc5e1a..46a4e81e8 100644 --- a/system-contract-dapp-playground/src/components/contract-interaction/erc/erc-20/methods/Transfer.tsx +++ b/system-contract-dapp-playground/src/components/contract-interaction/erc/erc-20/methods/token-transfer/index.tsx @@ -25,15 +25,13 @@ import { erc20Transfers } from '@/api/hedera/erc20-interactions'; import { CommonErrorToast } from '@/components/toast/CommonToast'; import MultiLineMethod from '@/components/common/MultiLineMethod'; import { Dispatch, SetStateAction, useEffect, useState } from 'react'; -import { - transferParamFields, - transferFromParamFields, -} from '@/utils/contract-interactions/erc/constant'; -import { handleRetrievingTransactionResultsFromLocalStorage } from '@/components/contract-interaction/hts/shared/methods/handleRetrievingTransactionResultsFromLocalStorage'; import { TransactionResult } from '@/types/contract-interactions/HTS'; -import { handleAPIErrors } from '@/components/contract-interaction/hts/shared/methods/handleAPIErrors'; import { convertCalmelCaseFunctionName } from '@/utils/common/helpers'; +import { HEDERA_TRANSACTION_RESULT_STORAGE_KEYS } from '@/utils/common/constants'; +import { handleAPIErrors } from '@/components/contract-interaction/hts/shared/methods/handleAPIErrors'; +import { transferParamFields, transferFromParamFields } from '@/utils/contract-interactions/erc/constant'; import { useUpdateTransactionResultsToLocalStorage } from '@/components/contract-interaction/hts/shared/hooks/useUpdateLocalStorage'; +import { handleRetrievingTransactionResultsFromLocalStorage } from '@/components/contract-interaction/hts/shared/methods/handleRetrievingTransactionResultsFromLocalStorage'; interface PageProps { baseContract: Contract; @@ -41,7 +39,8 @@ interface PageProps { const Transfer = ({ baseContract }: PageProps) => { const toaster = useToast(); - const transactionResultStorageKey = 'HEDERA.EIP.ERC-20.TOKEN-TRANSFER-RESULTS'; + const transactionResultStorageKey = + HEDERA_TRANSACTION_RESULT_STORAGE_KEYS['ERC20-RESULT']['TOKEN-TRANSFER']; const [transactionResults, setTransactionResults] = useState([]); const [transferParams, setTransferParams] = useState({ @@ -74,7 +73,7 @@ const Transfer = ({ baseContract }: PageProps) => { undefined, setTransactionResults ); - }, [toaster]); + }, [toaster, transactionResultStorageKey]); /** @dev handle execute methods */ const handleExecutingMethods = async ( @@ -140,9 +139,7 @@ const Transfer = ({ baseContract }: PageProps) => { status: 'success', transactionTimeStamp: Date.now(), txHash: tokenTransferRes.txHash as string, - transactionType: `ERC20-${convertCalmelCaseFunctionName(method) - .toUpperCase() - .replace(' ', '-')}`, + transactionType: `ERC20-${convertCalmelCaseFunctionName(method).toUpperCase().replace(' ', '-')}`, }, ]); } @@ -188,9 +185,7 @@ const Transfer = ({ baseContract }: PageProps) => { widthSize="w-[360px]" setParams={setTransferParams} isLoading={methodState.transfer.isLoading} - handleExecute={() => - handleExecutingMethods('transfer', transferParams, setTransferParams) - } + handleExecute={() => handleExecutingMethods('transfer', transferParams, setTransferParams)} explanation="Moves `amount` tokens from the caller’s account to `recipient`." /> diff --git a/system-contract-dapp-playground/src/types/contract-interactions/erc/index.d.ts b/system-contract-dapp-playground/src/types/contract-interactions/erc/index.d.ts index 696727113..894a087b9 100644 --- a/system-contract-dapp-playground/src/types/contract-interactions/erc/index.d.ts +++ b/system-contract-dapp-playground/src/types/contract-interactions/erc/index.d.ts @@ -19,20 +19,24 @@ */ /** - * @dev an interface for the results returned back from interacting with ERC20Mock smart contract + * @dev an interface for the results returned back from interacting with ERC20Mock & ERC721Mock smart contract */ -interface ERC20MockSmartContractResult { +interface ERCSmartContractResult { name?: string; symbol?: string; txHash?: string; decimals?: string; + tokenURI?: string; mintRes?: boolean; + ownerOfRes?: string; totalSupply?: string; balanceOfRes?: string; approveRes?: boolean; allowanceRes?: string; transferRes?: boolean; transferFromRes?: boolean; + approvalStatusRes?: boolean; + approvedAccountRes?: string; increaseAllowanceRes?: boolean; decreaseAllowanceRes?: boolean; err?: any; diff --git a/system-contract-dapp-playground/src/utils/common/constants.ts b/system-contract-dapp-playground/src/utils/common/constants.ts index b44ba41cb..0190ea09e 100644 --- a/system-contract-dapp-playground/src/utils/common/constants.ts +++ b/system-contract-dapp-playground/src/utils/common/constants.ts @@ -321,8 +321,13 @@ export const HEDERA_SHARED_PARAM_INPUT_FIELDS = { /** * @notice a shared object stores all transaction result storage keys */ -const prepareTransactionResultStorageKey = (contractKey: string, methodKey: string, resultKey: string) => { - return `HEDERA.${contractKey}.${methodKey}.${resultKey}-RESULTS`; +const prepareTransactionResultStorageKey = ( + contractKey: string, + methodKey: string, + resultKey: string, + readonly?: boolean +) => { + return `HEDERA.${contractKey}.${methodKey}.${resultKey}-RESULTS${readonly ? `.READONLY` : ``}`; }; export const HEDERA_TRANSACTION_RESULT_STORAGE_KEYS = { 'TOKEN-CREATE': { @@ -353,4 +358,11 @@ export const HEDERA_TRANSACTION_RESULT_STORAGE_KEYS = { 'MULTIPLE-TOKENS': prepareTransactionResultStorageKey('HTS', 'TOKEN-TRANSFER', 'MULTIPLE-TOKENS'), }, 'IHRC719-RESULTS': `HEDERA.IHRC719.IHRC719-RESULTS`, + 'ERC20-RESULT': { + 'TOKEN-MINT': prepareTransactionResultStorageKey('EIP', 'ERC-20', 'TOKEN-MINT'), + 'BALANCE-OF': prepareTransactionResultStorageKey('EIP', 'ERC-20', 'BALANCE-OF', true), + 'TOKEN-TRANSFER': prepareTransactionResultStorageKey('EIP', 'ERC-20', 'TOKEN-TRANSFER'), + 'TOKEN-PERMISSION': prepareTransactionResultStorageKey('EIP', 'ERC-20', 'TOKEN-PERMISSION'), + 'ALLOWANCES-RESULT': prepareTransactionResultStorageKey('EIP', 'ERC-20', 'ALLOWANCES', true), + }, };