From f0a916abf820b8bd9d7fbe600e6fe10b87f94998 Mon Sep 17 00:00:00 2001 From: Andriy-Shymkiv Date: Sun, 25 Feb 2024 19:46:29 +0200 Subject: [PATCH 01/10] Add MY_TOKEN screen to AppScreen enum --- src/store/appSlice.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/store/appSlice.ts b/src/store/appSlice.ts index 8719a26..9c2edbd 100644 --- a/src/store/appSlice.ts +++ b/src/store/appSlice.ts @@ -10,6 +10,7 @@ export enum AppScreen { SWAP_TOKENS = 'SWAP_TOKENS', SELECT_YOU_PAY_TOKEN = 'SELECT_YOU_PAY_TOKEN', SELECT_YOU_RECEIVE_TOKEN = 'SELECT_YOU_RECEIVE_TOKEN', + MY_TOKEN = 'MY_TOKEN', } export interface AppState { From 005e755aa7e8209ab69c38aaaa00d5f60f863163 Mon Sep 17 00:00:00 2001 From: Andriy-Shymkiv Date: Sun, 25 Feb 2024 19:47:42 +0200 Subject: [PATCH 02/10] Add contract abi --- src/abis/mytoken.json | 330 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 330 insertions(+) create mode 100644 src/abis/mytoken.json diff --git a/src/abis/mytoken.json b/src/abis/mytoken.json new file mode 100644 index 0000000..619dda5 --- /dev/null +++ b/src/abis/mytoken.json @@ -0,0 +1,330 @@ +[ + { + "inputs": [{ "internalType": "address[]", "name": "admins", "type": "address[]" }], + "stateMutability": "nonpayable", + "type": "constructor" + }, + { "inputs": [], "name": "AccessControlBadConfirmation", "type": "error" }, + { + "inputs": [ + { "internalType": "address", "name": "account", "type": "address" }, + { "internalType": "bytes32", "name": "neededRole", "type": "bytes32" } + ], + "name": "AccessControlUnauthorizedAccount", + "type": "error" + }, + { + "inputs": [{ "internalType": "address", "name": "_address", "type": "address" }], + "name": "AlreadyBlackListed", + "type": "error" + }, + { + "inputs": [{ "internalType": "address", "name": "_address", "type": "address" }], + "name": "BlackListed", + "type": "error" + }, + { + "inputs": [{ "internalType": "address", "name": "_address", "type": "address" }], + "name": "CannotBlackListAdmin", + "type": "error" + }, + { + "inputs": [ + { "internalType": "address", "name": "spender", "type": "address" }, + { "internalType": "uint256", "name": "allowance", "type": "uint256" }, + { "internalType": "uint256", "name": "needed", "type": "uint256" } + ], + "name": "ERC20InsufficientAllowance", + "type": "error" + }, + { + "inputs": [ + { "internalType": "address", "name": "sender", "type": "address" }, + { "internalType": "uint256", "name": "balance", "type": "uint256" }, + { "internalType": "uint256", "name": "needed", "type": "uint256" } + ], + "name": "ERC20InsufficientBalance", + "type": "error" + }, + { + "inputs": [{ "internalType": "address", "name": "approver", "type": "address" }], + "name": "ERC20InvalidApprover", + "type": "error" + }, + { + "inputs": [{ "internalType": "address", "name": "receiver", "type": "address" }], + "name": "ERC20InvalidReceiver", + "type": "error" + }, + { + "inputs": [{ "internalType": "address", "name": "sender", "type": "address" }], + "name": "ERC20InvalidSender", + "type": "error" + }, + { + "inputs": [{ "internalType": "address", "name": "spender", "type": "address" }], + "name": "ERC20InvalidSpender", + "type": "error" + }, + { + "inputs": [{ "internalType": "address", "name": "_address", "type": "address" }], + "name": "NotBlackListed", + "type": "error" + }, + { "inputs": [], "name": "RecipientIsBlackListed", "type": "error" }, + { "inputs": [], "name": "SenderIsBlackListed", "type": "error" }, + { + "anonymous": false, + "inputs": [{ "indexed": true, "internalType": "address", "name": "_address", "type": "address" }], + "name": "AddedToBlackList", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { "indexed": true, "internalType": "address", "name": "owner", "type": "address" }, + { "indexed": true, "internalType": "address", "name": "spender", "type": "address" }, + { "indexed": false, "internalType": "uint256", "name": "value", "type": "uint256" } + ], + "name": "Approval", + "type": "event" + }, + { + "anonymous": false, + "inputs": [{ "indexed": true, "internalType": "address", "name": "_address", "type": "address" }], + "name": "RemovedFromBlackList", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { "indexed": true, "internalType": "bytes32", "name": "role", "type": "bytes32" }, + { "indexed": true, "internalType": "bytes32", "name": "previousAdminRole", "type": "bytes32" }, + { "indexed": true, "internalType": "bytes32", "name": "newAdminRole", "type": "bytes32" } + ], + "name": "RoleAdminChanged", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { "indexed": true, "internalType": "bytes32", "name": "role", "type": "bytes32" }, + { "indexed": true, "internalType": "address", "name": "account", "type": "address" }, + { "indexed": true, "internalType": "address", "name": "sender", "type": "address" } + ], + "name": "RoleGranted", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { "indexed": true, "internalType": "bytes32", "name": "role", "type": "bytes32" }, + { "indexed": true, "internalType": "address", "name": "account", "type": "address" }, + { "indexed": true, "internalType": "address", "name": "sender", "type": "address" } + ], + "name": "RoleRevoked", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { "indexed": true, "internalType": "address", "name": "from", "type": "address" }, + { "indexed": true, "internalType": "address", "name": "to", "type": "address" }, + { "indexed": false, "internalType": "uint256", "name": "value", "type": "uint256" } + ], + "name": "Transfer", + "type": "event" + }, + { + "inputs": [], + "name": "ADMIN_ROLE", + "outputs": [{ "internalType": "bytes32", "name": "", "type": "bytes32" }], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "DEFAULT_ADMIN_ROLE", + "outputs": [{ "internalType": "bytes32", "name": "", "type": "bytes32" }], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [{ "internalType": "address[]", "name": "_addresses", "type": "address[]" }], + "name": "addToBlackList", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { "internalType": "address", "name": "owner", "type": "address" }, + { "internalType": "address", "name": "spender", "type": "address" } + ], + "name": "allowance", + "outputs": [{ "internalType": "uint256", "name": "", "type": "uint256" }], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { "internalType": "address", "name": "spender", "type": "address" }, + { "internalType": "uint256", "name": "value", "type": "uint256" } + ], + "name": "approve", + "outputs": [{ "internalType": "bool", "name": "", "type": "bool" }], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [{ "internalType": "address", "name": "account", "type": "address" }], + "name": "balanceOf", + "outputs": [{ "internalType": "uint256", "name": "", "type": "uint256" }], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [{ "internalType": "address", "name": "", "type": "address" }], + "name": "blackList", + "outputs": [{ "internalType": "bool", "name": "", "type": "bool" }], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [{ "internalType": "uint256", "name": "value", "type": "uint256" }], + "name": "burn", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { "internalType": "address", "name": "account", "type": "address" }, + { "internalType": "uint256", "name": "amount", "type": "uint256" } + ], + "name": "burnFrom", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "decimals", + "outputs": [{ "internalType": "uint8", "name": "", "type": "uint8" }], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [{ "internalType": "bytes32", "name": "role", "type": "bytes32" }], + "name": "getRoleAdmin", + "outputs": [{ "internalType": "bytes32", "name": "", "type": "bytes32" }], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { "internalType": "bytes32", "name": "role", "type": "bytes32" }, + { "internalType": "address", "name": "account", "type": "address" } + ], + "name": "grantRole", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { "internalType": "bytes32", "name": "role", "type": "bytes32" }, + { "internalType": "address", "name": "account", "type": "address" } + ], + "name": "hasRole", + "outputs": [{ "internalType": "bool", "name": "", "type": "bool" }], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { "internalType": "address", "name": "to", "type": "address" }, + { "internalType": "uint256", "name": "amount", "type": "uint256" } + ], + "name": "mint", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "name", + "outputs": [{ "internalType": "string", "name": "", "type": "string" }], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [{ "internalType": "address[]", "name": "_addresses", "type": "address[]" }], + "name": "removeFromBlackList", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { "internalType": "bytes32", "name": "role", "type": "bytes32" }, + { "internalType": "address", "name": "callerConfirmation", "type": "address" } + ], + "name": "renounceRole", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { "internalType": "bytes32", "name": "role", "type": "bytes32" }, + { "internalType": "address", "name": "account", "type": "address" } + ], + "name": "revokeRole", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [{ "internalType": "bytes4", "name": "interfaceId", "type": "bytes4" }], + "name": "supportsInterface", + "outputs": [{ "internalType": "bool", "name": "", "type": "bool" }], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "symbol", + "outputs": [{ "internalType": "string", "name": "", "type": "string" }], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "totalSupply", + "outputs": [{ "internalType": "uint256", "name": "", "type": "uint256" }], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { "internalType": "address", "name": "to", "type": "address" }, + { "internalType": "uint256", "name": "value", "type": "uint256" } + ], + "name": "transfer", + "outputs": [{ "internalType": "bool", "name": "", "type": "bool" }], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { "internalType": "address", "name": "from", "type": "address" }, + { "internalType": "address", "name": "to", "type": "address" }, + { "internalType": "uint256", "name": "value", "type": "uint256" } + ], + "name": "transferFrom", + "outputs": [{ "internalType": "bool", "name": "", "type": "bool" }], + "stateMutability": "nonpayable", + "type": "function" + } +] From 413c22ac3637dbb80d184631a64d1293c2979284 Mon Sep 17 00:00:00 2001 From: Andriy-Shymkiv Date: Sun, 25 Feb 2024 20:49:19 +0200 Subject: [PATCH 03/10] Add MyTokenScreen component --- src/App.tsx | 9 +-- src/components/MyTokenScreen.tsx | 24 +++++++ src/components/SwapTokensScreen.tsx | 69 ++++++++++++++------- src/components/UnsupportedNetworkScreen.tsx | 2 +- src/hooks/useMyToken.ts | 51 +++++++++++++++ src/utils/constants.ts | 2 + 6 files changed, 127 insertions(+), 30 deletions(-) create mode 100644 src/components/MyTokenScreen.tsx create mode 100644 src/hooks/useMyToken.ts diff --git a/src/App.tsx b/src/App.tsx index b671458..5ec7e3d 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -7,9 +7,7 @@ import { SelectTokenScreen } from './components/SelectTokenScreen'; import { SwapTokensScreen } from './components/SwapTokensScreen'; import { AppScreen, setAppScreen } from './store/appSlice'; import { useAppDispatch, useAppSelector } from './store/hooks'; -import { useIsNetworkSupported } from '~/hooks/useSwitchNetwork'; -import { ChainId } from './walletActions/types'; -import { UnsupportedNetworkScreen } from './components/UnsupportedNetworkScreen'; +import { MyTokenScreen } from './components/MyTokenScreen'; const StyledWidgetWrapper = styled(Card, { name: 'StyledWidgetWrapper', @@ -41,7 +39,6 @@ export const App: React.FC = (): JSX.Element => { const { screen } = useAppSelector(({ app }) => app); const { account } = useWeb3React(); const dispatch = useAppDispatch(); - const isNetworkSupported = useIsNetworkSupported(); useEffect(() => { if (account) { @@ -54,9 +51,6 @@ export const App: React.FC = (): JSX.Element => { const showCloseButton = screen !== AppScreen.INITIAL && screen !== AppScreen.SWAP_TOKENS && screen !== AppScreen.CHOOSE_WALLET; - if (!isNetworkSupported) { - return ; - } return ( {showCloseButton && ( @@ -69,6 +63,7 @@ export const App: React.FC = (): JSX.Element => { {(screen === AppScreen.SELECT_YOU_PAY_TOKEN || screen === AppScreen.SELECT_YOU_RECEIVE_TOKEN) && ( )} + {screen === AppScreen.MY_TOKEN && } ); }; diff --git a/src/components/MyTokenScreen.tsx b/src/components/MyTokenScreen.tsx new file mode 100644 index 0000000..643d716 --- /dev/null +++ b/src/components/MyTokenScreen.tsx @@ -0,0 +1,24 @@ +import { Box, Typography } from '@mui/material'; +import { useWeb3React } from '@web3-react/core'; +import { useIsBlackListed } from '~/hooks/useMyToken'; + +export const MyTokenScreen = (): JSX.Element => { + const { account } = useWeb3React(); + const { data: isBlackListed } = useIsBlackListed(account); + return ( + + theme.palette.info.main}> + {'Black list status:'} + {' '} + {isBlackListed ? ( + theme.palette.error.main}> + {'Whoops, you are blacklisted and can only read from the contract'} + + ) : ( + theme.palette.success.main}> + {'Very good, you are not blacklisted (yet)'} + + )} + + ); +}; diff --git a/src/components/SwapTokensScreen.tsx b/src/components/SwapTokensScreen.tsx index 4b97546..7c14d84 100644 --- a/src/components/SwapTokensScreen.tsx +++ b/src/components/SwapTokensScreen.tsx @@ -11,6 +11,9 @@ import { SelectChain } from './SelectChain'; import { useSwapCondition } from '~/hooks/useSwapCondition'; import { useRequestApprove } from '~/hooks/useRequestApprove'; import { useCreateSwap } from '~/hooks/useCreateSwap'; +import { useIsNetworkSupported } from '~/hooks/useSwitchNetwork'; +import { ChainId } from '~/walletActions/types'; +import { UnsupportedNetworkScreen } from './UnsupportedNetworkScreen'; const StyledDisconnectButton = styled(Button, { name: 'StyledDisconnectButton', @@ -18,10 +21,17 @@ const StyledDisconnectButton = styled(Button, { textTransform: 'none', backgroundColor: theme.palette.error.main, })); +const StyledGoToMyTokenButton = styled(Button, { + name: 'StyledGoToMyTokenButton', +})(({ theme }) => ({ + textTransform: 'none', + backgroundColor: theme.palette.success.main, +})); export const SwapTokensScreen: React.FC = (): JSX.Element => { const { account, connector } = useWeb3React(); const dispatch = useAppDispatch(); + const isNetworkSupported = useIsNetworkSupported(); const { isEnoughBalance, isEnoughAllowance } = useSwapCondition(); const { mutate: requestApprove, isLoading: isRequestApproveLoading } = useRequestApprove(); const { mutate: createSwap, isLoading: isCreateSwapLoading } = useCreateSwap(); @@ -35,6 +45,10 @@ export const SwapTokensScreen: React.FC = (): JSX.Element => { connector.resetState(); }, [connector, dispatch]); + const handleGoToMyToken = useCallback(() => { + dispatch(setAppScreen(AppScreen.MY_TOKEN)); + }, [dispatch]); + const onClick = useCallback((): void => { if (!isEnoughBalance) { return; @@ -55,30 +69,41 @@ export const SwapTokensScreen: React.FC = (): JSX.Element => { <> {`Connected to ${getEllipsisString(account)}`} - - {'Disconnect'} - + + + {'Go to MyToken'} + + + {'Disconnect'} + + - - - - - + {!isNetworkSupported ? ( + + + + ) : ( + + + + + - - - {buttonLabel} - {isLoader && } - - - + + + {buttonLabel} + {isLoader && } + + + + )} ); }; diff --git a/src/components/UnsupportedNetworkScreen.tsx b/src/components/UnsupportedNetworkScreen.tsx index 2a5f19d..e697870 100644 --- a/src/components/UnsupportedNetworkScreen.tsx +++ b/src/components/UnsupportedNetworkScreen.tsx @@ -27,7 +27,7 @@ export const UnsupportedNetworkScreen = ({ return ( + {`Current network is not supported, click`}{' '} {'here'} diff --git a/src/hooks/useMyToken.ts b/src/hooks/useMyToken.ts new file mode 100644 index 0000000..5db1a7f --- /dev/null +++ b/src/hooks/useMyToken.ts @@ -0,0 +1,51 @@ +import { Web3Provider } from '@ethersproject/providers'; +import { UseQueryResult, useQuery } from '@tanstack/react-query'; +import { useWeb3React } from '@web3-react/core'; +import { Contract } from 'ethers'; +import { useState, useEffect } from 'react'; +import myTokenAbi from '~/abis/mytoken.json'; +import { MY_TOKEN } from '~/utils/constants'; + +export function useMyToken(): { + contract: Contract | null; +} { + const [contract, setContract] = useState(null); + + const { provider, account } = useWeb3React(); + + useEffect(() => { + if (!provider || !account) { + return; + } + + const contract = new Contract( + MY_TOKEN, + myTokenAbi, + // todo: investigate why the fuck it is TS error here + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + provider.getSigner(), + ); + setContract(contract); + }, [provider, account]); + + return { + contract, + }; +} + +export function useIsBlackListed(address?: string): UseQueryResult { + const { contract } = useMyToken(); + console.log('contract', contract); + + return useQuery( + ['isBlackListed'], + () => { + console.log('contract', contract); + return contract?.blackList?.(address); + }, + { + enabled: !!contract && !!address, + }, + ); +} diff --git a/src/utils/constants.ts b/src/utils/constants.ts index 141c587..68a82f0 100644 --- a/src/utils/constants.ts +++ b/src/utils/constants.ts @@ -88,4 +88,6 @@ export const UNLIMITED_ALLOWANCE_IN_BASE_UNITS = export const SWAP_SPENDER = '0xb7742b7cf4d590de1f2bded0139537fea8f00710'; +export const MY_TOKEN = '0x732Cb99100EC7f93F49BEf9d90ede40EED53b67D'; + export const SNACKBAR_DURATION = 5000; From 77d2a6434a76bc8d289b7584d90e9d981e74fb5e Mon Sep 17 00:00:00 2001 From: Andriy-Shymkiv Date: Sun, 25 Feb 2024 22:00:39 +0200 Subject: [PATCH 04/10] Add Tooltip to Go to MyToken button and refactor useMyToken hook --- src/components/MyTokenScreen.tsx | 102 ++++++++++++++++++++++---- src/components/SwapTokensScreen.tsx | 19 +++-- src/hooks/useMyToken.ts | 109 +++++++++++++++++++++++----- 3 files changed, 192 insertions(+), 38 deletions(-) diff --git a/src/components/MyTokenScreen.tsx b/src/components/MyTokenScreen.tsx index 643d716..f363d1e 100644 --- a/src/components/MyTokenScreen.tsx +++ b/src/components/MyTokenScreen.tsx @@ -1,24 +1,94 @@ -import { Box, Typography } from '@mui/material'; +import { Box, Button, CircularProgress, Typography, styled } from '@mui/material'; import { useWeb3React } from '@web3-react/core'; -import { useIsBlackListed } from '~/hooks/useMyToken'; +import { useMyTokenDetails, useBalanceOf, useIsBlackListed, useAllowance } from '~/hooks/useMyToken'; +import { formatUnits } from 'ethers'; +import { getEllipsisString } from '~/helpers/utils'; + +const StyledTxButton = styled(Button)(({ theme }) => ({ + color: theme.palette.text.primary, + backgroundColor: theme.palette.success.main, + textTransform: 'none', + '&:hover': { + backgroundColor: theme.palette.success.dark, + }, +})); export const MyTokenScreen = (): JSX.Element => { const { account } = useWeb3React(); - const { data: isBlackListed } = useIsBlackListed(account); + const { data: details, isLoading: isDetailsLoading } = useMyTokenDetails(); + const { data: isBlackListed, isLoading: isBlackListedLoading } = useIsBlackListed(account); + const { data: balanceOf, isLoading: isBalanceOfLoading } = useBalanceOf(account); + const { data: allowance, isLoading: isAllowanceLoading } = useAllowance(account); return ( - - theme.palette.info.main}> - {'Black list status:'} - {' '} - {isBlackListed ? ( - theme.palette.error.main}> - {'Whoops, you are blacklisted and can only read from the contract'} - - ) : ( - theme.palette.success.main}> - {'Very good, you are not blacklisted (yet)'} - - )} + + + Basic details: + {isDetailsLoading ? ( + + ) : ( + Object.entries(details || {}).map(([key, value]) => ( + + theme.palette.info.main}> + {key}: + {' '} + + {key === 'totalSupply' ? formatUnits(value || 0, 18) : value.toString()} + + + )) + )} + + + {getEllipsisString(account)}: + + theme.palette.info.main}> + {'blackList:'} + {' '} + {isBlackListedLoading ? ( + + ) : isBlackListed ? ( + theme.palette.error.main}> + {'true'} + + ) : ( + theme.palette.success.main}> + {'false'} + + )} + + + theme.palette.info.main}> + {'balanceOf:'} + {' '} + {isBalanceOfLoading ? ( + + ) : ( + + {formatUnits(balanceOf || 0, 18)} + + )} + + + theme.palette.info.main}> + {'allowance:'} + {' '} + {isAllowanceLoading ? ( + + ) : ( + + {formatUnits(allowance || 0, 18)} + + )} + + + + {'transfer'} + + + {'transferFrom'} + + + ); }; diff --git a/src/components/SwapTokensScreen.tsx b/src/components/SwapTokensScreen.tsx index 7c14d84..7077795 100644 --- a/src/components/SwapTokensScreen.tsx +++ b/src/components/SwapTokensScreen.tsx @@ -1,4 +1,4 @@ -import { Box, Button, styled, Typography, CircularProgress } from '@mui/material'; +import { Box, Button, styled, Typography, CircularProgress, Tooltip } from '@mui/material'; import { useWeb3React } from '@web3-react/core'; import { useCallback } from 'react'; import { getEllipsisString } from '~/helpers/utils'; @@ -29,7 +29,7 @@ const StyledGoToMyTokenButton = styled(Button, { })); export const SwapTokensScreen: React.FC = (): JSX.Element => { - const { account, connector } = useWeb3React(); + const { account, connector, chainId } = useWeb3React(); const dispatch = useAppDispatch(); const isNetworkSupported = useIsNetworkSupported(); const { isEnoughBalance, isEnoughAllowance } = useSwapCondition(); @@ -70,9 +70,18 @@ export const SwapTokensScreen: React.FC = (): JSX.Element => { {`Connected to ${getEllipsisString(account)}`} - - {'Go to MyToken'} - + + + + {'Go to MyToken'} + + + + {'Disconnect'} diff --git a/src/hooks/useMyToken.ts b/src/hooks/useMyToken.ts index 5db1a7f..d1620be 100644 --- a/src/hooks/useMyToken.ts +++ b/src/hooks/useMyToken.ts @@ -1,23 +1,20 @@ import { Web3Provider } from '@ethersproject/providers'; -import { UseQueryResult, useQuery } from '@tanstack/react-query'; +import { UseMutationResult, UseQueryResult, useMutation, useQuery } from '@tanstack/react-query'; import { useWeb3React } from '@web3-react/core'; import { Contract } from 'ethers'; import { useState, useEffect } from 'react'; import myTokenAbi from '~/abis/mytoken.json'; import { MY_TOKEN } from '~/utils/constants'; +import { useSnackbar } from './useSnackbar'; -export function useMyToken(): { - contract: Contract | null; -} { - const [contract, setContract] = useState(null); - +export function useMyToken(): Contract | null { + const [myToken, setMyToken] = useState(null); const { provider, account } = useWeb3React(); useEffect(() => { if (!provider || !account) { return; } - const contract = new Contract( MY_TOKEN, myTokenAbi, @@ -26,26 +23,104 @@ export function useMyToken(): { // @ts-ignore provider.getSigner(), ); - setContract(contract); + setMyToken(contract); }, [provider, account]); - return { - contract, - }; + // const handleTx = async (): Promise => { + // // Example: Transfer tokens + // if (contract && account) { + // // await contract.mint?.(account, 0.009 * 10 ** 18); + // // const bl = await contract.blackList?.(account); + // // console.log('bl', bl); + // // setIsBlackListed(bl); + // await contract.transfer?.('0x9e08D72501C1ccE2916AaC582D5536f414fD8A1b', 0.009 * 10 ** 18); + // } + // }; + + // const addToBlackList = async (address: string): Promise => { + // if (contract && account) { + // await contract.addToBlackList?.([address]); + // } + // }; + return myToken; +} + +export function useMyTokenDetails(): UseQueryResult< + { + name: string; + symbol: string; + decimals: number; + totalSupply: bigint; + }, + Error +> { + const myToken = useMyToken(); + + return useQuery( + ['myTokenDetails'], + async () => ({ + name: await myToken?.name?.(), + symbol: await myToken?.symbol?.(), + decimals: await myToken?.decimals?.(), + totalSupply: await myToken?.totalSupply?.(), + }), + { + enabled: !!myToken, + staleTime: Infinity, + cacheTime: Infinity, + }, + ); } export function useIsBlackListed(address?: string): UseQueryResult { - const { contract } = useMyToken(); - console.log('contract', contract); + const myToken = useMyToken(); + + return useQuery( + ['isBlackListed', address], + () => { + return myToken?.blackList?.(address); + }, + { enabled: !!myToken && !!address }, + ); +} +export function useBalanceOf(address?: string): UseQueryResult { + const myToken = useMyToken(); return useQuery( - ['isBlackListed'], + ['balanceOf', address], () => { - console.log('contract', contract); - return contract?.blackList?.(address); + return myToken?.balanceOf?.(address); + }, + { enabled: !!myToken && !!address }, + ); +} + +export function useAllowance(address?: string): UseQueryResult { + const myToken = useMyToken(); + return useQuery( + ['allowance', address], + () => { + return myToken?.allowance?.(address, address); + }, + { enabled: !!myToken && !!address }, + ); +} + +export function useTransfer(): UseMutationResult { + const myToken = useMyToken(); + const { showSnackbar } = useSnackbar(); + return useMutation( + ['transfer'], + async ({ to, amount }) => { + await myToken?.transfer?.(to, amount); }, { - enabled: !!contract && !!address, + onError: () => { + showSnackbar({ + message: 'Error transferring tokens', + severity: 'error', + }); + }, }, ); } From b97f40e61043074964e7fb8d681a0504f83a8971 Mon Sep 17 00:00:00 2001 From: Andriy-Shymkiv Date: Sun, 25 Feb 2024 23:08:42 +0200 Subject: [PATCH 05/10] Add Formik dependency, define SEPOLIA_CHAIN_ID, and update SwapTokensScreen --- package.json | 1 + pnpm-lock.yaml | 40 +++++++ src/components/MyTokenScreen.tsx | 160 ++++++++++++++++++++++++++-- src/components/SwapTokensScreen.tsx | 14 ++- src/hooks/useMyToken.ts | 54 ++++++---- src/utils/constants.ts | 1 + 6 files changed, 238 insertions(+), 32 deletions(-) diff --git a/package.json b/package.json index 2f1b168..88a587b 100644 --- a/package.json +++ b/package.json @@ -32,6 +32,7 @@ "bignumber.js": "^9.1.2", "ethers": "^6.6.2", "events": "^3.3.0", + "formik": "^2.4.5", "react": "^18.2.0", "react-dom": "^18.2.0", "react-number-format": "^5.2.2", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 7169dab..67eff3b 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -71,6 +71,9 @@ dependencies: events: specifier: ^3.3.0 version: 3.3.0 + formik: + specifier: ^2.4.5 + version: 2.4.5(react@18.2.0) react: specifier: ^18.2.0 version: 18.2.0 @@ -2213,6 +2216,11 @@ packages: resolution: {integrity: sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==} dev: true + /deepmerge@2.2.1: + resolution: {integrity: sha512-R9hc1Xa/NOBi9WRVUWg19rl1UB7Tt4kuPd+thNJgFZoxXsTz7ncaPaeIm+40oSGuP33DfMb4sZt1QIGiJzC4EA==} + engines: {node: '>=0.10.0'} + dev: false + /delay@5.0.0: resolution: {integrity: sha512-ReEBKkIfe4ya47wlPYf/gu5ib6yUG0/Aez0JQZQz94kiWtRQvZIQbTiehsnwHvLSWJnQdhVeqYue7Id1dKr0qw==} engines: {node: '>=10'} @@ -2640,6 +2648,22 @@ packages: mime-types: 2.1.35 dev: false + /formik@2.4.5(react@18.2.0): + resolution: {integrity: sha512-Gxlht0TD3vVdzMDHwkiNZqJ7Mvg77xQNfmBRrNtvzcHZs72TJppSTDKHpImCMJZwcWPBJ8jSQQ95GJzXFf1nAQ==} + peerDependencies: + react: '>=16.8.0' + dependencies: + '@types/hoist-non-react-statics': 3.3.1 + deepmerge: 2.2.1 + hoist-non-react-statics: 3.3.2 + lodash: 4.17.21 + lodash-es: 4.17.21 + react: 18.2.0 + react-fast-compare: 2.0.4 + tiny-warning: 1.0.3 + tslib: 2.6.0 + dev: false + /fs.realpath@1.0.0: resolution: {integrity: sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==} dev: true @@ -3030,10 +3054,18 @@ packages: p-locate: 5.0.0 dev: true + /lodash-es@4.17.21: + resolution: {integrity: sha512-mKnC+QJ9pWVzv+C4/U3rRsHapFfHvQFoFB92e52xeyGMcX6/OlIl78je1u8vePzYZSkkogMPJ2yjxxsb89cxyw==} + dev: false + /lodash.merge@4.6.2: resolution: {integrity: sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==} dev: true + /lodash@4.17.21: + resolution: {integrity: sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==} + dev: false + /loose-envify@1.4.0: resolution: {integrity: sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==} hasBin: true @@ -3343,6 +3375,10 @@ packages: scheduler: 0.23.0 dev: false + /react-fast-compare@2.0.4: + resolution: {integrity: sha512-suNP+J1VU1MWFKcyt7RtjiSWUjvidmQSlqu+eHslq+342xCbGTYmC0mEhPCOHxlW0CywylOC1u2DFAT+bv4dBw==} + dev: false + /react-is@16.13.1: resolution: {integrity: sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==} dev: false @@ -3699,6 +3735,10 @@ packages: resolution: {integrity: sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg==} dev: false + /tiny-warning@1.0.3: + resolution: {integrity: sha512-lBN9zLN/oAf68o3zNXYrdCt1kP8WsiGW8Oo2ka41b2IM5JL/S1CTyX1rW0mb/zSuJun0ZUrDxx4sqvYS2FWzPA==} + dev: false + /to-fast-properties@2.0.0: resolution: {integrity: sha512-/OaKK0xYrs3DmxRYqL/yDc+FxFUVYhDlXMhRmv3z915w2HF1tnN1omB354j8VUGO/hbRzyD6Y3sA7v7GS/ceog==} engines: {node: '>=4'} diff --git a/src/components/MyTokenScreen.tsx b/src/components/MyTokenScreen.tsx index f363d1e..8353087 100644 --- a/src/components/MyTokenScreen.tsx +++ b/src/components/MyTokenScreen.tsx @@ -1,8 +1,18 @@ -import { Box, Button, CircularProgress, Typography, styled } from '@mui/material'; +import { Box, Button, CircularProgress, TextField, Typography, styled } from '@mui/material'; import { useWeb3React } from '@web3-react/core'; -import { useMyTokenDetails, useBalanceOf, useIsBlackListed, useAllowance } from '~/hooks/useMyToken'; -import { formatUnits } from 'ethers'; +import { + useMyTokenDetails, + useBalanceOf, + useIsAdmin, + useIsBlackListed, + useAllowance, + useTransfer, + useManageBlackList, +} from '~/hooks/useMyToken'; +import { formatUnits, parseUnits, isAddress } from 'ethers'; import { getEllipsisString } from '~/helpers/utils'; +import { Form, Formik } from 'formik'; +import { useSnackbar } from '~/hooks/useSnackbar'; const StyledTxButton = styled(Button)(({ theme }) => ({ color: theme.palette.text.primary, @@ -15,10 +25,15 @@ const StyledTxButton = styled(Button)(({ theme }) => ({ export const MyTokenScreen = (): JSX.Element => { const { account } = useWeb3React(); + const { showSnackbar } = useSnackbar(); const { data: details, isLoading: isDetailsLoading } = useMyTokenDetails(); + const { data: isAdmin, isLoading: isAdminLoading } = useIsAdmin(account); const { data: isBlackListed, isLoading: isBlackListedLoading } = useIsBlackListed(account); const { data: balanceOf, isLoading: isBalanceOfLoading } = useBalanceOf(account); const { data: allowance, isLoading: isAllowanceLoading } = useAllowance(account); + const { mutate: transfer } = useTransfer(); + const { mutate: manageBlackList } = useManageBlackList(); + return ( @@ -40,6 +55,22 @@ export const MyTokenScreen = (): JSX.Element => { {getEllipsisString(account)}: + + theme.palette.info.main}> + {'hasRole(ADMIN_ROLE):'} + {' '} + {isAdminLoading ? ( + + ) : isAdmin ? ( + theme.palette.success.main}> + {'true'} + + ) : ( + theme.palette.info.main}> + {'false'} + + )} + theme.palette.info.main}> {'blackList:'} @@ -80,13 +111,122 @@ export const MyTokenScreen = (): JSX.Element => { )} - - - {'transfer'} - - - {'transferFrom'} - + + { + if (!isAddress(account)) { + showSnackbar({ + message: 'Invalid address', + severity: 'error', + }); + } else { + manageBlackList({ + action: 'add', + account, + }); + } + }} + > + {({ values, handleChange, handleSubmit }) => ( +
+ + + {'addToBlackList'} + + + + + +
+ )} +
+ + { + if (!isAddress(account)) { + showSnackbar({ + message: 'Invalid address', + severity: 'error', + }); + } else { + manageBlackList({ + action: 'remove', + account, + }); + } + }} + > + {({ values, handleChange, handleSubmit }) => ( +
+ + + {'removeFromBlackList'} + + + + + +
+ )} +
+ + { + if (!isAddress(to) || !amount) { + showSnackbar({ + message: 'Invalid address or amount', + severity: 'error', + }); + } else { + transfer({ + to, + amount: parseUnits(amount, 18), + }); + } + }} + > + {({ values, handleChange, handleSubmit }) => ( +
+ + + {'transfer'} + + + + + + +
+ )} +
diff --git a/src/components/SwapTokensScreen.tsx b/src/components/SwapTokensScreen.tsx index 7077795..5201095 100644 --- a/src/components/SwapTokensScreen.tsx +++ b/src/components/SwapTokensScreen.tsx @@ -14,6 +14,7 @@ import { useCreateSwap } from '~/hooks/useCreateSwap'; import { useIsNetworkSupported } from '~/hooks/useSwitchNetwork'; import { ChainId } from '~/walletActions/types'; import { UnsupportedNetworkScreen } from './UnsupportedNetworkScreen'; +import { SEPOLIA_CHAIN_ID } from '~/utils/constants'; const StyledDisconnectButton = styled(Button, { name: 'StyledDisconnectButton', @@ -27,6 +28,11 @@ const StyledGoToMyTokenButton = styled(Button, { textTransform: 'none', backgroundColor: theme.palette.success.main, })); +const StyledTooltipWrapper = styled(Box, { + name: 'StyledTooltipWrapper', +})({ + cursor: 'pointer', +}); export const SwapTokensScreen: React.FC = (): JSX.Element => { const { account, connector, chainId } = useWeb3React(); @@ -70,16 +76,16 @@ export const SwapTokensScreen: React.FC = (): JSX.Element => { {`Connected to ${getEllipsisString(account)}`} - - + + {'Go to MyToken'} - + diff --git a/src/hooks/useMyToken.ts b/src/hooks/useMyToken.ts index d1620be..1ca96be 100644 --- a/src/hooks/useMyToken.ts +++ b/src/hooks/useMyToken.ts @@ -26,22 +26,6 @@ export function useMyToken(): Contract | null { setMyToken(contract); }, [provider, account]); - // const handleTx = async (): Promise => { - // // Example: Transfer tokens - // if (contract && account) { - // // await contract.mint?.(account, 0.009 * 10 ** 18); - // // const bl = await contract.blackList?.(account); - // // console.log('bl', bl); - // // setIsBlackListed(bl); - // await contract.transfer?.('0x9e08D72501C1ccE2916AaC582D5536f414fD8A1b', 0.009 * 10 ** 18); - // } - // }; - - // const addToBlackList = async (address: string): Promise => { - // if (contract && account) { - // await contract.addToBlackList?.([address]); - // } - // }; return myToken; } @@ -72,6 +56,18 @@ export function useMyTokenDetails(): UseQueryResult< ); } +export function useIsAdmin(address?: string): UseQueryResult { + const myToken = useMyToken(); + + return useQuery( + ['isAdmin', address], + () => { + return myToken?.hasRole?.(myToken?.ADMIN_ROLE?.(), address); + }, + { enabled: !!myToken && !!address }, + ); +} + export function useIsBlackListed(address?: string): UseQueryResult { const myToken = useMyToken(); @@ -106,13 +102,33 @@ export function useAllowance(address?: string): UseQueryResult { ); } -export function useTransfer(): UseMutationResult { +export function useManageBlackList(): UseMutationResult { + const myToken = useMyToken(); + const { showSnackbar } = useSnackbar(); + return useMutation( + ['manageBlackList'], + async ({ action, account }) => { + const executable = action === 'add' ? 'addToBlackList' : 'removeFromBlackList'; + return myToken?.[executable]?.([account]); + }, + { + onError: () => { + showSnackbar({ + message: 'Error managing black list, maybe you are not allowed to do this', + severity: 'error', + }); + }, + }, + ); +} + +export function useTransfer(): UseMutationResult { const myToken = useMyToken(); const { showSnackbar } = useSnackbar(); return useMutation( ['transfer'], async ({ to, amount }) => { - await myToken?.transfer?.(to, amount); + return myToken?.transfer?.(to, amount); }, { onError: () => { @@ -124,3 +140,5 @@ export function useTransfer(): UseMutationResult = { logoURI: USDC_TOKEN_LOGO_URL, }, }; +export const SEPOLIA_CHAIN_ID = 11155111; // testnet chain id export const MULTI_CALL_ADDRESS: Record = { [ChainId.MAINNET]: '0x5ba1e12693dc8f9c48aad8770482f4739beed696', From 18d253ddedd5bae2d88d7c06deb13e1d5218423b Mon Sep 17 00:00:00 2001 From: Andriy-Shymkiv Date: Sun, 25 Feb 2024 23:35:19 +0200 Subject: [PATCH 06/10] Add useMint and useBurnFrom hooks --- src/hooks/useMyToken.ts | 36 ++++++++++++++++++++++++++++++++++++ 1 file changed, 36 insertions(+) diff --git a/src/hooks/useMyToken.ts b/src/hooks/useMyToken.ts index 1ca96be..12519d0 100644 --- a/src/hooks/useMyToken.ts +++ b/src/hooks/useMyToken.ts @@ -141,4 +141,40 @@ export function useTransfer(): UseMutationResult { + const myToken = useMyToken(); + const { showSnackbar } = useSnackbar(); + return useMutation( + ['mint'], + async ({ to, amount }) => { + return myToken?.mint?.(to, amount); + }, + { + onError: () => { + showSnackbar({ + message: 'Error minting tokens', + severity: 'error', + }); + }, + }, + ); +} +export function useBurnFrom(): UseMutationResult { + const myToken = useMyToken(); + const { showSnackbar } = useSnackbar(); + return useMutation( + ['burnFrom'], + async ({ from, amount }) => { + return myToken?.burnFrom?.(from, amount); + }, + { + onError: () => { + showSnackbar({ + message: 'Error burning tokens', + severity: 'error', + }); + }, + }, + ); +} From 50d40378beb1c6cbe9df73d94d5e043122da4292 Mon Sep 17 00:00:00 2001 From: Andriy-Shymkiv Date: Mon, 26 Feb 2024 12:28:23 +0200 Subject: [PATCH 07/10] Update widget width and add mint and burn functionality --- src/App.tsx | 4 +- src/components/MyTokenScreen.tsx | 95 +++++++++++++++++++++++++++++++- src/hooks/useMyToken.ts | 2 +- 3 files changed, 95 insertions(+), 6 deletions(-) diff --git a/src/App.tsx b/src/App.tsx index 5ec7e3d..4dab507 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -15,8 +15,8 @@ const StyledWidgetWrapper = styled(Card, { display: 'flex', flexDirection: 'column', // TODO: make responsive - height: 440, - width: 440, + // height: 440, + width: 520, padding: theme.spacing(10), boxShadow: theme.shadows[8], position: 'relative', diff --git a/src/components/MyTokenScreen.tsx b/src/components/MyTokenScreen.tsx index 8353087..2fc124d 100644 --- a/src/components/MyTokenScreen.tsx +++ b/src/components/MyTokenScreen.tsx @@ -8,6 +8,8 @@ import { useAllowance, useTransfer, useManageBlackList, + useMint, + useBurnFrom, } from '~/hooks/useMyToken'; import { formatUnits, parseUnits, isAddress } from 'ethers'; import { getEllipsisString } from '~/helpers/utils'; @@ -32,6 +34,8 @@ export const MyTokenScreen = (): JSX.Element => { const { data: balanceOf, isLoading: isBalanceOfLoading } = useBalanceOf(account); const { data: allowance, isLoading: isAllowanceLoading } = useAllowance(account); const { mutate: transfer } = useTransfer(); + const { mutate: mint } = useMint(); + const { mutate: burnFrom } = useBurnFrom(); const { mutate: manageBlackList } = useManageBlackList(); return ( @@ -140,7 +144,7 @@ export const MyTokenScreen = (): JSX.Element => { @@ -178,7 +182,7 @@ export const MyTokenScreen = (): JSX.Element => { @@ -214,7 +218,92 @@ export const MyTokenScreen = (): JSX.Element => { {'transfer'} - + + + + + + )} + + + { + if (!isAddress(to) || !amount) { + showSnackbar({ + message: 'Invalid address or amount', + severity: 'error', + }); + } else { + mint({ + to, + amount: parseUnits(amount, 18), + }); + } + }} + > + {({ values, handleChange, handleSubmit }) => ( +
+ + + {'mint'} + + + + + + +
+ )} +
+ { + if (!isAddress(from) || !amount) { + showSnackbar({ + message: 'Invalid address or amount', + severity: 'error', + }); + } else { + burnFrom({ + from, + amount: parseUnits(amount, 18), + }); + } + }} + > + {({ values, handleChange, handleSubmit }) => ( +
+ + + {'burnFrom'} + + + Date: Mon, 26 Feb 2024 14:51:53 +0200 Subject: [PATCH 08/10] Add new components and hooks, update MyTokenScreen --- src/components/MyTokenScreen.tsx | 82 +++++++++++++++++++++++++++-- src/hooks/useMyToken.ts | 3 +- src/hooks/useTransactionHistory.tsx | 34 ++++++++++++ src/utils/constants.ts | 11 ++++ src/utils/env.ts | 6 +++ 5 files changed, 132 insertions(+), 4 deletions(-) create mode 100644 src/hooks/useTransactionHistory.tsx create mode 100644 src/utils/env.ts diff --git a/src/components/MyTokenScreen.tsx b/src/components/MyTokenScreen.tsx index 2fc124d..900f68a 100644 --- a/src/components/MyTokenScreen.tsx +++ b/src/components/MyTokenScreen.tsx @@ -1,4 +1,19 @@ -import { Box, Button, CircularProgress, TextField, Typography, styled } from '@mui/material'; +import { + Box, + Button, + CircularProgress, + Modal, + Paper, + Table, + TableBody, + TableCell, + TableContainer, + TableHead, + TableRow, + TextField, + Typography, + styled, +} from '@mui/material'; import { useWeb3React } from '@web3-react/core'; import { useMyTokenDetails, @@ -15,8 +30,12 @@ import { formatUnits, parseUnits, isAddress } from 'ethers'; import { getEllipsisString } from '~/helpers/utils'; import { Form, Formik } from 'formik'; import { useSnackbar } from '~/hooks/useSnackbar'; +import { useTransactionHistory } from '~/hooks/useTransactionHistory'; +import { useState } from 'react'; -const StyledTxButton = styled(Button)(({ theme }) => ({ +const StyledTxButton = styled(Button, { + name: 'StyledTxButton', +})(({ theme }) => ({ color: theme.palette.text.primary, backgroundColor: theme.palette.success.main, textTransform: 'none', @@ -24,9 +43,22 @@ const StyledTxButton = styled(Button)(({ theme }) => ({ backgroundColor: theme.palette.success.dark, }, })); +const StyledBox = styled(Box, { + name: 'StyledBox', +})(({ theme }) => ({ + position: 'absolute', + top: '50%', + left: '50%', + transform: 'translate(-50%, -50%)', + padding: theme.spacing(4), + boxShadow: theme.shadows[5], + borderRadius: theme.shape.borderRadius, + backgroundColor: theme.palette.background.paper, +})); +// this component will be deleted from the codebase after pdp export const MyTokenScreen = (): JSX.Element => { - const { account } = useWeb3React(); + const { account, chainId } = useWeb3React(); const { showSnackbar } = useSnackbar(); const { data: details, isLoading: isDetailsLoading } = useMyTokenDetails(); const { data: isAdmin, isLoading: isAdminLoading } = useIsAdmin(account); @@ -37,6 +69,9 @@ export const MyTokenScreen = (): JSX.Element => { const { mutate: mint } = useMint(); const { mutate: burnFrom } = useBurnFrom(); const { mutate: manageBlackList } = useManageBlackList(); + const { data: txHistory } = useTransactionHistory(account, chainId); + + const [isModalOpen, setIsModalOpen] = useState(false); return ( @@ -116,6 +151,47 @@ export const MyTokenScreen = (): JSX.Element => { )} + { + setIsModalOpen(true); + }} + > + show last 5 transactions + + { + setIsModalOpen(false); + }} + > + + last 5 transactions from etherscan: + + + + + tx hash + blockNumber + from + to + + + + {txHistory?.result.slice(0, 5).map((tx) => ( + + + {getEllipsisString(tx.hash)} + + {getEllipsisString(tx.blockNumber?.toString())} + {getEllipsisString(tx.from)} + {getEllipsisString(tx.to ?? '')} + + ))} + +
+
+
+
(null); const { provider, account } = useWeb3React(); @@ -18,9 +19,9 @@ export function useMyToken(): Contract | null { const contract = new Contract( MY_TOKEN, myTokenAbi, - // why ??? // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-ignore + // todo: need to manually fix type mismatch between ethers and web3 - react provider.getSigner(), ); setMyToken(contract); diff --git a/src/hooks/useTransactionHistory.tsx b/src/hooks/useTransactionHistory.tsx new file mode 100644 index 0000000..623dd82 --- /dev/null +++ b/src/hooks/useTransactionHistory.tsx @@ -0,0 +1,34 @@ +import { UseQueryResult, useQuery } from '@tanstack/react-query'; +import { ETHERSCAN_API_URL, ONE_MINUTE } from '../utils/constants'; +import { ChainId } from '~/walletActions/types'; +import axios from 'axios'; +import ENV from '~/utils/env'; +import { TransactionResponse } from 'ethers'; +export function useTransactionHistory( + address?: string, + chainId?: ChainId | number, +): UseQueryResult< + { + status: string; + message: string; + // todo: define correct type for tx from etherscan + result: TransactionResponse[]; + }, + Error +> { + return useQuery( + ['transactionHistory', address, chainId], + async () => { + const baseUrl = ETHERSCAN_API_URL[chainId as ChainId | number]; + const apiKey = ENV.ETHERSCAN_API_KEY; + const url = `${baseUrl}/api?module=account&action=txlist&address=${address}&sort=desc&apikey=${apiKey}`; + const { data } = await axios.get(url); + return data; + }, + { + enabled: !!address && !!chainId, + staleTime: ONE_MINUTE, + cacheTime: ONE_MINUTE, + }, + ); +} diff --git a/src/utils/constants.ts b/src/utils/constants.ts index c000853..135d9fd 100644 --- a/src/utils/constants.ts +++ b/src/utils/constants.ts @@ -74,6 +74,17 @@ export const USDC_TOKEN: Record = { }; export const SEPOLIA_CHAIN_ID = 11155111; // testnet chain id +export const ETHERSCAN_API_URL: Record = { + [ChainId.MAINNET]: 'https://api.etherscan.io', + [ChainId.BSC]: 'https://api.bscscan.com', + [ChainId.POLYGON]: 'https://api.polygonscan.com', + [ChainId.ARBITRUM]: 'https://api.arbiscan.io', + [ChainId.AVALANCHE]: 'https://api.cchain.explorer.avax.network', + [ChainId.FANTOM]: 'https://api.ftmscan.com', + [ChainId.OPTIMISM]: 'https://api-optimistic.etherscan.io', + [SEPOLIA_CHAIN_ID]: 'https://api-sepolia.etherscan.io', +}; + export const MULTI_CALL_ADDRESS: Record = { [ChainId.MAINNET]: '0x5ba1e12693dc8f9c48aad8770482f4739beed696', [ChainId.BSC]: '0xC50F4c1E81c873B2204D7eFf7069Ffec6Fbe136D', diff --git a/src/utils/env.ts b/src/utils/env.ts new file mode 100644 index 0000000..a9702a4 --- /dev/null +++ b/src/utils/env.ts @@ -0,0 +1,6 @@ +// todo: add validation +const ENV = { + ETHERSCAN_API_KEY: import.meta.env.VITE_ETHERSCAN_API_KEY, +}; + +export default ENV; From 1ed9e04e10dd4138929bcb96d0ffa1ad0ba31b74 Mon Sep 17 00:00:00 2001 From: Andriy-Shymkiv Date: Mon, 26 Feb 2024 15:03:20 +0200 Subject: [PATCH 09/10] Fix array slicing in MyTokenScreen component --- src/components/MyTokenScreen.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/MyTokenScreen.tsx b/src/components/MyTokenScreen.tsx index 900f68a..ba92597 100644 --- a/src/components/MyTokenScreen.tsx +++ b/src/components/MyTokenScreen.tsx @@ -177,7 +177,7 @@ export const MyTokenScreen = (): JSX.Element => { - {txHistory?.result.slice(0, 5).map((tx) => ( + {txHistory?.result?.slice(0, 5).map((tx) => ( {getEllipsisString(tx.hash)} From 1e1e5efefba034a7695c9fedde9f45b078e8de80 Mon Sep 17 00:00:00 2001 From: Andriy-Shymkiv Date: Thu, 7 Mar 2024 18:37:47 +0200 Subject: [PATCH 10/10] Fix type mismatch between ethers and @web3-react in useMyToken.ts --- src/hooks/useMyToken.ts | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/src/hooks/useMyToken.ts b/src/hooks/useMyToken.ts index afe9d10..9c71145 100644 --- a/src/hooks/useMyToken.ts +++ b/src/hooks/useMyToken.ts @@ -21,7 +21,7 @@ export function useMyToken(): Contract | null { myTokenAbi, // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-ignore - // todo: need to manually fix type mismatch between ethers and web3 - react + // todo: need to fix type mismatch between ethers and @web3-react provider.getSigner(), ); setMyToken(contract); @@ -43,12 +43,15 @@ export function useMyTokenDetails(): UseQueryResult< return useQuery( ['myTokenDetails'], - async () => ({ - name: await myToken?.name?.(), - symbol: await myToken?.symbol?.(), - decimals: await myToken?.decimals?.(), - totalSupply: await myToken?.totalSupply?.(), - }), + async () => { + const [name, symbol, decimals, totalSupply] = await Promise.all([ + myToken?.name?.(), + myToken?.symbol?.(), + myToken?.decimals?.(), + myToken?.totalSupply?.(), + ]); + return { name, symbol, decimals, totalSupply }; + }, { enabled: !!myToken, staleTime: Infinity,