diff --git a/lib/web3/useAccount.ts b/lib/web3/useAccount.ts new file mode 100644 index 0000000000..f3dfcd48c8 --- /dev/null +++ b/lib/web3/useAccount.ts @@ -0,0 +1,23 @@ +import type { UseAccountReturnType } from 'wagmi'; +import { useAccount } from 'wagmi'; + +import config from 'configs/app'; + +function useAccountFallback(): UseAccountReturnType { + return { + address: undefined, + addresses: undefined, + chain: undefined, + chainId: undefined, + connector: undefined, + isConnected: false, + isConnecting: false, + isDisconnected: true, + isReconnecting: false, + status: 'disconnected', + }; +} + +const hook = config.features.blockchainInteraction.isEnabled ? useAccount : useAccountFallback; + +export default hook; diff --git a/mocks/contract/methods.ts b/mocks/contract/methods.ts index 6c1bdf367e..61ee20d666 100644 --- a/mocks/contract/methods.ts +++ b/mocks/contract/methods.ts @@ -1,6 +1,6 @@ import type { - SmartContractQueryMethodReadError, - SmartContractQueryMethodReadSuccess, + SmartContractQueryMethodError, + SmartContractQueryMethodSuccess, SmartContractReadMethod, SmartContractWriteMethod, } from 'types/api/contract'; @@ -94,7 +94,7 @@ export const read: Array = [ }, ]; -export const readResultSuccess: SmartContractQueryMethodReadSuccess = { +export const readResultSuccess: SmartContractQueryMethodSuccess = { is_error: false, result: { names: [ 'amount' ], @@ -104,7 +104,7 @@ export const readResultSuccess: SmartContractQueryMethodReadSuccess = { }, }; -export const readResultError: SmartContractQueryMethodReadError = { +export const readResultError: SmartContractQueryMethodError = { is_error: true, result: { message: 'Some shit happened', diff --git a/playwright/TestApp.tsx b/playwright/TestApp.tsx index d8ef52ca2c..ca65a6e0c6 100644 --- a/playwright/TestApp.tsx +++ b/playwright/TestApp.tsx @@ -18,6 +18,7 @@ import theme from 'theme'; export type Props = { children: React.ReactNode; withSocket?: boolean; + withWalletClient?: boolean; appContext?: { pageProps: PageProps; }; @@ -47,7 +48,20 @@ const wagmiConfig = createConfig({ }, }); -const TestApp = ({ children, withSocket, appContext = defaultAppContext }: Props) => { +const WalletClientProvider = ({ children, withWalletClient }: { children: React.ReactNode; withWalletClient?: boolean }) => { + if (withWalletClient) { + return ( + + { children } + + ); + } + + // eslint-disable-next-line react/jsx-no-useless-fragment + return <>{ children }; +}; + +const TestApp = ({ children, withSocket, withWalletClient = true, appContext = defaultAppContext }: Props) => { const [ queryClient ] = React.useState(() => new QueryClient({ defaultOptions: { queries: { @@ -63,9 +77,9 @@ const TestApp = ({ children, withSocket, appContext = defaultAppContext }: Props - + { children } - + diff --git a/playwright/fixtures/mockEnvs.ts b/playwright/fixtures/mockEnvs.ts index 69353ead37..23d7606403 100644 --- a/playwright/fixtures/mockEnvs.ts +++ b/playwright/fixtures/mockEnvs.ts @@ -37,4 +37,7 @@ export const ENVS_MAP: Record> = { blockHiddenFields: [ [ 'NEXT_PUBLIC_VIEWS_BLOCK_HIDDEN_FIELDS', '["burnt_fees", "total_reward", "nonce"]' ], ], + noWalletClient: [ + [ 'NEXT_PUBLIC_WALLET_CONNECT_PROJECT_ID', '' ], + ], }; diff --git a/types/api/contract.ts b/types/api/contract.ts index 22893140d5..50f4098d27 100644 --- a/types/api/contract.ts +++ b/types/api/contract.ts @@ -1,4 +1,4 @@ -import type { Abi, AbiType } from 'abitype'; +import type { Abi, AbiType, AbiFallback, AbiFunction, AbiReceive } from 'abitype'; export type SmartContractMethodArgType = AbiType; export type SmartContractMethodStateMutability = 'view' | 'nonpayable' | 'payable'; @@ -78,49 +78,19 @@ export interface SmartContractExternalLibrary { name: string; } -export interface SmartContractMethodBase { - inputs: Array; - outputs?: Array; - constant: boolean; - name: string; - stateMutability: SmartContractMethodStateMutability; - type: 'function'; - payable: boolean; - error?: string; +export type SmartContractMethodOutputValue = string | boolean | object; +export type SmartContractMethodOutput = AbiFunction['outputs'][number] & { value?: SmartContractMethodOutputValue }; +export type SmartContractMethodBase = Omit & { method_id: string; -} - + outputs: Array; + constant?: boolean; + error?: string; +}; export type SmartContractReadMethod = SmartContractMethodBase; - -export interface SmartContractWriteFallback { - payable?: true; - stateMutability: 'payable'; - type: 'fallback'; -} - -export interface SmartContractWriteReceive { - payable?: true; - stateMutability: 'payable'; - type: 'receive'; -} - -export type SmartContractWriteMethod = SmartContractMethodBase | SmartContractWriteFallback | SmartContractWriteReceive; - +export type SmartContractWriteMethod = SmartContractMethodBase | AbiFallback | AbiReceive; export type SmartContractMethod = SmartContractReadMethod | SmartContractWriteMethod; -export interface SmartContractMethodInput { - internalType?: string; // there could be any string, e.g "enum MyEnum" - name: string; - type: SmartContractMethodArgType; - components?: Array; - fieldType?: 'native_coin'; -} - -export interface SmartContractMethodOutput extends SmartContractMethodInput { - value?: string | boolean | object; -} - -export interface SmartContractQueryMethodReadSuccess { +export interface SmartContractQueryMethodSuccess { is_error: false; result: { names: Array ]>; @@ -131,7 +101,7 @@ export interface SmartContractQueryMethodReadSuccess { }; } -export interface SmartContractQueryMethodReadError { +export interface SmartContractQueryMethodError { is_error: true; result: { code: number; @@ -147,7 +117,7 @@ export interface SmartContractQueryMethodReadError { }; } -export type SmartContractQueryMethodRead = SmartContractQueryMethodReadSuccess | SmartContractQueryMethodReadError; +export type SmartContractQueryMethod = SmartContractQueryMethodSuccess | SmartContractQueryMethodError; // VERIFICATION diff --git a/ui/address/AddressContract.pw.tsx b/ui/address/AddressContract.pw.tsx new file mode 100644 index 0000000000..603df54668 --- /dev/null +++ b/ui/address/AddressContract.pw.tsx @@ -0,0 +1,94 @@ +import React from 'react'; + +import * as addressMock from 'mocks/address/address'; +import * as contractInfoMock from 'mocks/contract/info'; +import * as contractMethodsMock from 'mocks/contract/methods'; +import { ENVS_MAP } from 'playwright/fixtures/mockEnvs'; +import * as socketServer from 'playwright/fixtures/socketServer'; +import { test, expect } from 'playwright/lib'; + +import AddressContract from './AddressContract.pwstory'; + +const hash = addressMock.contract.hash; + +test.beforeEach(async({ mockApiResponse }) => { + await mockApiResponse('address', addressMock.contract, { pathParams: { hash } }); + await mockApiResponse('contract', contractInfoMock.verified, { pathParams: { hash } }); + await mockApiResponse('contract_methods_read', contractMethodsMock.read, { pathParams: { hash }, queryParams: { is_custom_abi: 'false' } }); + await mockApiResponse('contract_methods_write', contractMethodsMock.write, { pathParams: { hash }, queryParams: { is_custom_abi: 'false' } }); +}); + +test.describe('ABI functionality', () => { + test('read', async({ render, createSocket }) => { + const hooksConfig = { + router: { + query: { hash, tab: 'read_contract' }, + }, + }; + const component = await render(, { hooksConfig }, { withSocket: true }); + const socket = await createSocket(); + await socketServer.joinChannel(socket, 'addresses:' + addressMock.contract.hash.toLowerCase()); + + await expect(component.getByRole('button', { name: 'Connect wallet' })).toBeVisible(); + await component.getByText('FLASHLOAN_PREMIUM_TOTAL').click(); + await expect(component.getByRole('button', { name: 'Read' })).toBeVisible(); + }); + + test('read, no wallet client', async({ render, createSocket, mockEnvs }) => { + const hooksConfig = { + router: { + query: { hash, tab: 'read_contract' }, + }, + }; + await mockEnvs(ENVS_MAP.noWalletClient); + const component = await render(, { hooksConfig }, { withSocket: true, withWalletClient: false }); + const socket = await createSocket(); + await socketServer.joinChannel(socket, 'addresses:' + addressMock.contract.hash.toLowerCase()); + + await expect(component.getByRole('button', { name: 'Connect wallet' })).toBeHidden(); + await component.getByText('FLASHLOAN_PREMIUM_TOTAL').click(); + await expect(component.getByRole('button', { name: 'Read' })).toBeVisible(); + }); + + test('write', async({ render, createSocket }) => { + const hooksConfig = { + router: { + query: { hash, tab: 'write_contract' }, + }, + }; + const component = await render(, { hooksConfig }, { withSocket: true }); + const socket = await createSocket(); + await socketServer.joinChannel(socket, 'addresses:' + addressMock.contract.hash.toLowerCase()); + + await expect(component.getByRole('button', { name: 'Connect wallet' })).toBeVisible(); + await component.getByText('setReserveInterestRateStrategyAddress').click(); + await expect(component.getByLabel('2.').getByRole('button', { name: 'Simulate' })).toBeEnabled(); + await expect(component.getByLabel('2.').getByRole('button', { name: 'Write' })).toBeEnabled(); + + await component.getByText('pause').click(); + await expect(component.getByLabel('5.').getByRole('button', { name: 'Simulate' })).toBeHidden(); + await expect(component.getByLabel('5.').getByRole('button', { name: 'Write' })).toBeEnabled(); + }); + + test('write, no wallet client', async({ render, createSocket, mockEnvs }) => { + const hooksConfig = { + router: { + query: { hash, tab: 'write_contract' }, + }, + }; + await mockEnvs(ENVS_MAP.noWalletClient); + + const component = await render(, { hooksConfig }, { withSocket: true, withWalletClient: false }); + const socket = await createSocket(); + await socketServer.joinChannel(socket, 'addresses:' + addressMock.contract.hash.toLowerCase()); + + await expect(component.getByRole('button', { name: 'Connect wallet' })).toBeHidden(); + await component.getByText('setReserveInterestRateStrategyAddress').click(); + await expect(component.getByLabel('2.').getByRole('button', { name: 'Simulate' })).toBeEnabled(); + await expect(component.getByLabel('2.').getByRole('button', { name: 'Write' })).toBeDisabled(); + + await component.getByText('pause').click(); + await expect(component.getByLabel('5.').getByRole('button', { name: 'Simulate' })).toBeHidden(); + await expect(component.getByLabel('5.').getByRole('button', { name: 'Write' })).toBeDisabled(); + }); +}); diff --git a/ui/address/AddressContract.pwstory.tsx b/ui/address/AddressContract.pwstory.tsx new file mode 100644 index 0000000000..c558dd9ca2 --- /dev/null +++ b/ui/address/AddressContract.pwstory.tsx @@ -0,0 +1,18 @@ +import { useRouter } from 'next/router'; +import React from 'react'; + +import useApiQuery from 'lib/api/useApiQuery'; +import useContractTabs from 'lib/hooks/useContractTabs'; +import getQueryParamString from 'lib/router/getQueryParamString'; + +import AddressContract from './AddressContract'; + +const AddressContractPwStory = () => { + const router = useRouter(); + const hash = getQueryParamString(router.query.hash); + const addressQuery = useApiQuery('address', { pathParams: { hash } }); + const { tabs } = useContractTabs(addressQuery.data, false); + return ; +}; + +export default AddressContractPwStory; diff --git a/ui/address/AddressContract.tsx b/ui/address/AddressContract.tsx index 5d8a6dd8d0..a349532a7a 100644 --- a/ui/address/AddressContract.tsx +++ b/ui/address/AddressContract.tsx @@ -3,7 +3,6 @@ import React from 'react'; import type { RoutedSubTab } from 'ui/shared/Tabs/types'; import RoutedTabs from 'ui/shared/Tabs/RoutedTabs'; -import Web3ModalProvider from 'ui/shared/Web3ModalProvider'; interface Props { tabs: Array; @@ -16,21 +15,12 @@ const TAB_LIST_PROPS = { }; const AddressContract = ({ tabs, isLoading, shouldRender }: Props) => { - const fallback = React.useCallback(() => { - const noProviderTabs = tabs.filter(({ id }) => id === 'contract_code' || id.startsWith('read_')); - return ( - - ); - }, [ isLoading, tabs ]); - if (!shouldRender) { return null; } return ( - - - + ); }; diff --git a/ui/address/contract/ContractMethodsAccordion.tsx b/ui/address/contract/ABI/ContractAbi.tsx similarity index 58% rename from ui/address/contract/ContractMethodsAccordion.tsx rename to ui/address/contract/ABI/ContractAbi.tsx index b1e4c4bf6b..6b71c46c2d 100644 --- a/ui/address/contract/ContractMethodsAccordion.tsx +++ b/ui/address/contract/ABI/ContractAbi.tsx @@ -1,40 +1,27 @@ import { Accordion, Box, Flex, Link } from '@chakra-ui/react'; import _range from 'lodash/range'; import React from 'react'; -import { scroller } from 'react-scroll'; -import type { SmartContractMethod } from 'types/api/contract'; +import type { MethodType, ContractAbi as TContractAbi } from './types'; -import ContractMethodsAccordionItem from './ContractMethodsAccordionItem'; +import ContractAbiItem from './ContractAbiItem'; +import useFormSubmit from './useFormSubmit'; +import useScrollToMethod from './useScrollToMethod'; -interface Props { - data: Array; - addressHash?: string; - renderItemContent: (item: T, index: number, id: number) => React.ReactNode; +interface Props { + data: TContractAbi; + addressHash: string; tab: string; + methodType: MethodType; } -const ContractMethodsAccordion = ({ data, addressHash, renderItemContent, tab }: Props) => { +const ContractAbi = ({ data, addressHash, tab, methodType }: Props) => { const [ expandedSections, setExpandedSections ] = React.useState>(data.length === 1 ? [ 0 ] : []); const [ id, setId ] = React.useState(0); - React.useEffect(() => { - const hash = window.location.hash.replace('#', ''); + useScrollToMethod(data, setExpandedSections); - if (!hash) { - return; - } - - const index = data.findIndex((item) => 'method_id' in item && item.method_id === hash); - if (index > -1) { - scroller.scrollTo(`method_${ hash }`, { - duration: 500, - smooth: true, - offset: -100, - }); - setExpandedSections([ index ]); - } - }, [ data ]); + const handleFormSubmit = useFormSubmit({ addressHash, tab }); const handleAccordionStateChange = React.useCallback((newValue: Array) => { setExpandedSections(newValue); @@ -73,14 +60,15 @@ const ContractMethodsAccordion = ({ data, address { data.map((item, index) => ( - React.ReactNode } tab={ tab } + onSubmit={ handleFormSubmit } + methodType={ methodType } /> )) } @@ -88,4 +76,4 @@ const ContractMethodsAccordion = ({ data, address ); }; -export default React.memo(ContractMethodsAccordion) as typeof ContractMethodsAccordion; +export default React.memo(ContractAbi); diff --git a/ui/address/contract/ContractMethodsAccordionItem.tsx b/ui/address/contract/ABI/ContractAbiItem.tsx similarity index 64% rename from ui/address/contract/ContractMethodsAccordionItem.tsx rename to ui/address/contract/ABI/ContractAbiItem.tsx index b30c0d998b..9aa3fc6889 100644 --- a/ui/address/contract/ContractMethodsAccordionItem.tsx +++ b/ui/address/contract/ABI/ContractAbiItem.tsx @@ -1,25 +1,32 @@ -import { AccordionButton, AccordionIcon, AccordionItem, AccordionPanel, Box, Tooltip, useClipboard, useDisclosure } from '@chakra-ui/react'; +import { AccordionButton, AccordionIcon, AccordionItem, AccordionPanel, Alert, Box, Flex, Tooltip, useClipboard, useDisclosure } from '@chakra-ui/react'; import React from 'react'; import { Element } from 'react-scroll'; -import type { SmartContractMethod } from 'types/api/contract'; +import type { FormSubmitHandler, MethodType, ContractAbiItem as TContractAbiItem } from './types'; import { route } from 'nextjs-routes'; import config from 'configs/app'; +import Tag from 'ui/shared/chakra/Tag'; +import CopyToClipboard from 'ui/shared/CopyToClipboard'; import Hint from 'ui/shared/Hint'; import IconSvg from 'ui/shared/IconSvg'; -interface Props { - data: T; +import ContractAbiItemConstant from './ContractAbiItemConstant'; +import ContractMethodForm from './form/ContractMethodForm'; +import { getElementName } from './useScrollToMethod'; + +interface Props { + data: TContractAbiItem; index: number; id: number; - addressHash?: string; - renderContent: (item: T, index: number, id: number) => React.ReactNode; + addressHash: string; tab: string; + onSubmit: FormSubmitHandler; + methodType: MethodType; } -const ContractMethodsAccordionItem = ({ data, index, id, addressHash, renderContent, tab }: Props) => { +const ContractAbiItem = ({ data, index, id, addressHash, tab, onSubmit, methodType }: Props) => { const url = React.useMemo(() => { if (!('method_id' in data)) { return ''; @@ -43,11 +50,40 @@ const ContractMethodsAccordionItem = ({ data, ind onCopy(); }, [ onCopy ]); + const handleCopyMethodIdClick = React.useCallback((event: React.MouseEvent) => { + event.stopPropagation(); + }, []); + + const content = (() => { + if ('error' in data && data.error) { + return { data.error }; + } + + const hasConstantOutputs = 'outputs' in data && data.outputs.some(({ value }) => value !== undefined && value !== null); + + if (hasConstantOutputs) { + return ( + + { data.outputs.map((output, index) => ) } + + ); + } + + return ( + + ); + })(); + return ( { ({ isExpanded }) => ( <> - + { 'method_id' in data && ( @@ -85,11 +121,17 @@ const ContractMethodsAccordionItem = ({ data, ind the contract cannot receive Ether through regular transactions and throws an exception.` }/> ) } + { 'method_id' in data && ( + <> + { data.method_id } + + + ) } - { renderContent(data, index, id) } + { content } ) } @@ -97,4 +139,4 @@ const ContractMethodsAccordionItem = ({ data, ind ); }; -export default React.memo(ContractMethodsAccordionItem); +export default React.memo(ContractAbiItem); diff --git a/ui/address/contract/ContractMethodConstant.tsx b/ui/address/contract/ABI/ContractAbiItemConstant.tsx similarity index 83% rename from ui/address/contract/ContractMethodConstant.tsx rename to ui/address/contract/ABI/ContractAbiItemConstant.tsx index 016668e89b..1e2c3f213c 100644 --- a/ui/address/contract/ContractMethodConstant.tsx +++ b/ui/address/contract/ABI/ContractAbiItemConstant.tsx @@ -4,12 +4,14 @@ import type { ChangeEvent } from 'react'; import React from 'react'; import { getAddress } from 'viem'; -import type { SmartContractMethodOutput } from 'types/api/contract'; +import type { ContractAbiItemOutput } from './types'; import { WEI } from 'lib/consts'; import { currencyUnits } from 'lib/units'; import AddressEntity from 'ui/shared/entities/address/AddressEntity'; +import { matchInt } from './form/utils'; + function castValueToString(value: number | string | boolean | object | bigint | undefined): string { switch (typeof value) { case 'string': @@ -28,13 +30,15 @@ function castValueToString(value: number | string | boolean | object | bigint | } interface Props { - data: SmartContractMethodOutput; + data: ContractAbiItemOutput; } -const ContractMethodStatic = ({ data }: Props) => { +const ContractAbiItemConstant = ({ data }: Props) => { const [ value, setValue ] = React.useState(castValueToString(data.value)); const [ label, setLabel ] = React.useState(currencyUnits.wei.toUpperCase()); + const intMatch = matchInt(data.type); + const handleCheckboxChange = React.useCallback((event: ChangeEvent) => { const initialValue = castValueToString(data.value); @@ -63,9 +67,9 @@ const ContractMethodStatic = ({ data }: Props) => { return ( { content } - { (data.type.includes('int256') || data.type.includes('int128')) && { label } } + { Number(intMatch?.power) >= 128 && { label } } ); }; -export default ContractMethodStatic; +export default ContractAbiItemConstant; diff --git a/ui/address/contract/methodForm/ContractMethodArrayButton.tsx b/ui/address/contract/ABI/form/ContractMethodArrayButton.tsx similarity index 100% rename from ui/address/contract/methodForm/ContractMethodArrayButton.tsx rename to ui/address/contract/ABI/form/ContractMethodArrayButton.tsx diff --git a/ui/address/contract/methodForm/ContractMethodFieldAccordion.tsx b/ui/address/contract/ABI/form/ContractMethodFieldAccordion.tsx similarity index 100% rename from ui/address/contract/methodForm/ContractMethodFieldAccordion.tsx rename to ui/address/contract/ABI/form/ContractMethodFieldAccordion.tsx diff --git a/ui/address/contract/methodForm/ContractMethodFieldInput.tsx b/ui/address/contract/ABI/form/ContractMethodFieldInput.tsx similarity index 97% rename from ui/address/contract/methodForm/ContractMethodFieldInput.tsx rename to ui/address/contract/ABI/form/ContractMethodFieldInput.tsx index 542506c953..cbe244b210 100644 --- a/ui/address/contract/methodForm/ContractMethodFieldInput.tsx +++ b/ui/address/contract/ABI/form/ContractMethodFieldInput.tsx @@ -3,7 +3,7 @@ import React from 'react'; import { useController, useFormContext } from 'react-hook-form'; import { NumericFormat } from 'react-number-format'; -import type { SmartContractMethodInput } from 'types/api/contract'; +import type { ContractAbiItemInput } from '../types'; import ClearButton from 'ui/shared/ClearButton'; @@ -14,7 +14,7 @@ import useValidateField from './useValidateField'; import { matchInt } from './utils'; interface Props { - data: SmartContractMethodInput; + data: ContractAbiItemInput; hideLabel?: boolean; path: string; className?: string; diff --git a/ui/address/contract/methodForm/ContractMethodFieldInputArray.tsx b/ui/address/contract/ABI/form/ContractMethodFieldInputArray.tsx similarity index 98% rename from ui/address/contract/methodForm/ContractMethodFieldInputArray.tsx rename to ui/address/contract/ABI/form/ContractMethodFieldInputArray.tsx index a2a18f185c..add681e90b 100644 --- a/ui/address/contract/methodForm/ContractMethodFieldInputArray.tsx +++ b/ui/address/contract/ABI/form/ContractMethodFieldInputArray.tsx @@ -2,7 +2,7 @@ import { Flex } from '@chakra-ui/react'; import React from 'react'; import { useFormContext } from 'react-hook-form'; -import type { SmartContractMethodInput } from 'types/api/contract'; +import type { ContractAbiItemInput } from '../types'; import ContractMethodArrayButton from './ContractMethodArrayButton'; import type { Props as AccordionProps } from './ContractMethodFieldAccordion'; @@ -13,7 +13,7 @@ import ContractMethodFieldLabel from './ContractMethodFieldLabel'; import { getFieldLabel, matchArray, transformDataForArrayItem } from './utils'; interface Props extends Pick { - data: SmartContractMethodInput; + data: ContractAbiItemInput; level: number; basePath: string; isDisabled: boolean; diff --git a/ui/address/contract/methodForm/ContractMethodFieldInputTuple.tsx b/ui/address/contract/ABI/form/ContractMethodFieldInputTuple.tsx similarity index 91% rename from ui/address/contract/methodForm/ContractMethodFieldInputTuple.tsx rename to ui/address/contract/ABI/form/ContractMethodFieldInputTuple.tsx index b621459000..8c0426e473 100644 --- a/ui/address/contract/methodForm/ContractMethodFieldInputTuple.tsx +++ b/ui/address/contract/ABI/form/ContractMethodFieldInputTuple.tsx @@ -1,7 +1,7 @@ import React from 'react'; import { useFormContext } from 'react-hook-form'; -import type { SmartContractMethodInput } from 'types/api/contract'; +import type { ContractAbiItemInput } from '../types'; import type { Props as AccordionProps } from './ContractMethodFieldAccordion'; import ContractMethodFieldAccordion from './ContractMethodFieldAccordion'; @@ -10,7 +10,7 @@ import ContractMethodFieldInputArray from './ContractMethodFieldInputArray'; import { getFieldLabel, matchArray } from './utils'; interface Props extends Pick { - data: SmartContractMethodInput; + data: ContractAbiItemInput; basePath: string; level: number; isDisabled: boolean; @@ -21,6 +21,10 @@ const ContractMethodFieldInputTuple = ({ data, basePath, level, isDisabled, ...a const fieldsWithErrors = Object.keys(errors); const isInvalid = fieldsWithErrors.some((field) => field.startsWith(basePath)); + if (!('components' in data)) { + return null; + } + return ( { data.components?.map((component, index) => { - if (component.components && component.type === 'tuple') { + if ('components' in component && component.type === 'tuple') { return ( Promise.resolve({ hash: '0x0000' as `0x${ string }` }); -const resultComponent = () => null; +const onSubmit = () => Promise.resolve({ source: 'wallet_client' as const, result: { hash: '0x0000' as `0x${ string }` } }); -const data: SmartContractWriteMethod = { +const data: ContractAbiItem = { inputs: [ // TUPLE { @@ -102,10 +101,9 @@ test('base view +@mobile +@dark-mode', async({ mount }) => { const component = await mount( - + , diff --git a/ui/address/contract/ABI/form/ContractMethodForm.tsx b/ui/address/contract/ABI/form/ContractMethodForm.tsx new file mode 100644 index 0000000000..cab71b2320 --- /dev/null +++ b/ui/address/contract/ABI/form/ContractMethodForm.tsx @@ -0,0 +1,208 @@ +import { Box, Button, Flex, Tooltip, chakra } from '@chakra-ui/react'; +import _mapValues from 'lodash/mapValues'; +import React from 'react'; +import type { SubmitHandler } from 'react-hook-form'; +import { useForm, FormProvider } from 'react-hook-form'; +import type { AbiFunction } from 'viem'; + +import type { FormSubmitHandler, FormSubmitResult, MethodCallStrategy, MethodType, ContractAbiItem } from '../types'; + +import config from 'configs/app'; +import * as mixpanel from 'lib/mixpanel/index'; + +import ContractMethodFieldAccordion from './ContractMethodFieldAccordion'; +import ContractMethodFieldInput from './ContractMethodFieldInput'; +import ContractMethodFieldInputArray from './ContractMethodFieldInputArray'; +import ContractMethodFieldInputTuple from './ContractMethodFieldInputTuple'; +import ContractMethodOutputs from './ContractMethodOutputs'; +import ContractMethodResult from './ContractMethodResult'; +import { getFieldLabel, matchArray, transformFormDataToMethodArgs } from './utils'; +import type { ContractMethodFormFields } from './utils'; + +interface Props { + data: ContractAbiItem; + onSubmit: FormSubmitHandler; + methodType: MethodType; +} + +const ContractMethodForm = ({ data, onSubmit, methodType }: Props) => { + + const [ result, setResult ] = React.useState(); + const [ isLoading, setLoading ] = React.useState(false); + const [ callStrategy, setCallStrategy ] = React.useState(); + const callStrategyRef = React.useRef(callStrategy); + + const formApi = useForm({ + mode: 'all', + shouldUnregister: true, + }); + + const handleButtonClick = React.useCallback((event: React.MouseEvent) => { + const callStrategy = event?.currentTarget.getAttribute('data-call-strategy'); + setCallStrategy(callStrategy as MethodCallStrategy); + callStrategyRef.current = callStrategy as MethodCallStrategy; + }, []); + + const onFormSubmit: SubmitHandler = React.useCallback(async(formData) => { + // The API used for reading from contracts expects all values to be strings. + const formattedData = callStrategyRef.current === 'api' ? + _mapValues(formData, (value) => value !== undefined ? String(value) : undefined) : + formData; + const args = transformFormDataToMethodArgs(formattedData); + + setResult(undefined); + setLoading(true); + + onSubmit(data, args, callStrategyRef.current) + .then((result) => { + setResult(result); + }) + .catch((error) => { + setResult({ + source: callStrategyRef.current ?? 'wallet_client', + result: error?.error || error?.data || (error?.reason && { message: error.reason }) || error, + }); + setLoading(false); + }) + .finally(() => { + mixpanel.logEvent(mixpanel.EventTypes.CONTRACT_INTERACTION, { + 'Method type': methodType === 'write' ? 'Write' : 'Read', + 'Method name': 'name' in data ? data.name : 'Fallback', + }); + }); + }, [ data, methodType, onSubmit ]); + + const handleTxSettle = React.useCallback(() => { + setLoading(false); + }, []); + + const handleFormChange = React.useCallback(() => { + result && setResult(undefined); + }, [ result ]); + + const inputs: AbiFunction['inputs'] = React.useMemo(() => { + return [ + ...('inputs' in data && data.inputs ? data.inputs : []), + ...('stateMutability' in data && data.stateMutability === 'payable' ? [ { + name: `Send native ${ config.chain.currency.symbol || 'coin' }`, + type: 'uint256' as const, + internalType: 'uint256' as const, + fieldType: 'native_coin' as const, + } ] : []), + ]; + }, [ data ]); + + const outputs = 'outputs' in data && data.outputs ? data.outputs : []; + + const callStrategies = (() => { + switch (methodType) { + case 'read': { + return { primary: 'api', secondary: undefined }; + } + + case 'write': { + return { + primary: config.features.blockchainInteraction.isEnabled ? 'wallet_client' : undefined, + secondary: 'outputs' in data && Boolean(data.outputs?.length) ? 'api' : undefined, + }; + } + + default: { + return { primary: undefined, secondary: undefined }; + } + } + })(); + + // eslint-disable-next-line max-len + const noWalletClientText = 'Blockchain interaction is not available at the moment since WalletConnect is not configured for this application. Please contact the service maintainer to make necessary changes in the service configuration.'; + + return ( + + + + + { inputs.map((input, index) => { + const props = { + data: input, + basePath: `${ index }`, + isDisabled: isLoading, + level: 0, + }; + + if ('components' in input && input.components && input.type === 'tuple') { + return ; + } + + const arrayMatch = matchArray(input.type); + if (arrayMatch) { + if (arrayMatch.isNested) { + const fieldsWithErrors = Object.keys(formApi.formState.errors); + const isInvalid = fieldsWithErrors.some((field) => field.startsWith(index + ':')); + + return ( + + + + ); + + } + + return ; + } + + return ; + }) } + + { callStrategies.secondary && ( + + ) } + + + + + + { 'outputs' in data && Boolean(data.outputs?.length) && } + { result && } + + ); +}; + +export default React.memo(ContractMethodForm) as typeof ContractMethodForm; diff --git a/ui/address/contract/methodForm/ContractMethodMultiplyButton.tsx b/ui/address/contract/ABI/form/ContractMethodMultiplyButton.tsx similarity index 100% rename from ui/address/contract/methodForm/ContractMethodMultiplyButton.tsx rename to ui/address/contract/ABI/form/ContractMethodMultiplyButton.tsx diff --git a/ui/address/contract/methodForm/ContractMethodFormOutputs.tsx b/ui/address/contract/ABI/form/ContractMethodOutputs.tsx similarity index 75% rename from ui/address/contract/methodForm/ContractMethodFormOutputs.tsx rename to ui/address/contract/ABI/form/ContractMethodOutputs.tsx index 9876266514..f145cd3f20 100644 --- a/ui/address/contract/methodForm/ContractMethodFormOutputs.tsx +++ b/ui/address/contract/ABI/form/ContractMethodOutputs.tsx @@ -1,15 +1,14 @@ import { Flex, chakra } from '@chakra-ui/react'; import React from 'react'; - -import type { SmartContractMethodOutput } from 'types/api/contract'; +import type { AbiFunction } from 'viem'; import IconSvg from 'ui/shared/IconSvg'; interface Props { - data: Array; + data: AbiFunction['outputs']; } -const ContractMethodFormOutputs = ({ data }: Props) => { +const ContractMethodOutputs = ({ data }: Props) => { if (data.length === 0) { return null; } @@ -32,4 +31,4 @@ const ContractMethodFormOutputs = ({ data }: Props) => { ); }; -export default React.memo(ContractMethodFormOutputs); +export default React.memo(ContractMethodOutputs); diff --git a/ui/address/contract/ABI/form/ContractMethodResult.tsx b/ui/address/contract/ABI/form/ContractMethodResult.tsx new file mode 100644 index 0000000000..654d7236c3 --- /dev/null +++ b/ui/address/contract/ABI/form/ContractMethodResult.tsx @@ -0,0 +1,29 @@ +import React from 'react'; + +import type { FormSubmitResult, ContractAbiItem } from '../types'; + +import ContractMethodResultApi from './ContractMethodResultApi'; +import ContractMethodResultWalletClient from './ContractMethodResultWalletClient'; + +interface Props { + abiItem: ContractAbiItem; + result: FormSubmitResult; + onSettle: () => void; +} + +const ContractMethodResult = ({ result, abiItem, onSettle }: Props) => { + + switch (result.source) { + case 'api': + return ; + + case 'wallet_client': + return ; + + default: { + return null; + } + } +}; + +export default React.memo(ContractMethodResult); diff --git a/ui/address/contract/ContractReadResult.pw.tsx b/ui/address/contract/ABI/form/ContractMethodResultApi.pw.tsx similarity index 52% rename from ui/address/contract/ContractReadResult.pw.tsx rename to ui/address/contract/ABI/form/ContractMethodResultApi.pw.tsx index 35b4991a0c..beeb18b30f 100644 --- a/ui/address/contract/ContractReadResult.pw.tsx +++ b/ui/address/contract/ABI/form/ContractMethodResultApi.pw.tsx @@ -1,69 +1,56 @@ -import { test, expect } from '@playwright/experimental-ct-react'; import React from 'react'; -import type { ContractMethodReadResult } from './types'; +import type { FormSubmitResultApi } from '../types'; import * as contractMethodsMock from 'mocks/contract/methods'; -import TestApp from 'playwright/TestApp'; +import { test, expect } from 'playwright/lib'; -import ContractReadResult from './ContractReadResult'; +import ContractMethodResultApi from './ContractMethodResultApi'; const item = contractMethodsMock.read[0]; const onSettle = () => Promise.resolve(); test.use({ viewport: { width: 500, height: 500 } }); -test('default error', async({ mount }) => { - const result: ContractMethodReadResult = { +test('default error', async({ render }) => { + const result: FormSubmitResultApi['result'] = { is_error: true, result: { error: 'I am an error', }, }; - const component = await mount( - - - , - ); + const component = await render(); await expect(component).toHaveScreenshot(); }); -test('error with code', async({ mount }) => { - const result: ContractMethodReadResult = { +test('error with code', async({ render }) => { + const result: FormSubmitResultApi['result'] = { is_error: true, result: { message: 'I am an error', code: -32017, }, }; - const component = await mount( - - - , - ); + const component = await render(); await expect(component).toHaveScreenshot(); }); -test('raw error', async({ mount }) => { - const result: ContractMethodReadResult = { +test('raw error', async({ render }) => { + const result: FormSubmitResultApi['result'] = { is_error: true, result: { raw: '49276d20616c7761797320726576657274696e67207769746820616e206572726f72', }, }; - const component = await mount( - - - , - ); + const component = await render(); await expect(component).toHaveScreenshot(); }); -test('complex error', async({ mount }) => { - const result: ContractMethodReadResult = { +test('complex error', async({ render }) => { + const result: FormSubmitResultApi['result'] = { is_error: true, result: { method_call: 'SomeCustomError(address addr, uint256 balance)', @@ -74,34 +61,26 @@ test('complex error', async({ mount }) => { ], }, }; - const component = await mount( - - - , - ); + const component = await render(); await expect(component).toHaveScreenshot(); }); -test('success', async({ mount }) => { - const result: ContractMethodReadResult = { +test('success', async({ render }) => { + const result: FormSubmitResultApi['result'] = { is_error: false, result: { names: [ 'address' ], output: [ { type: 'address', value: '0x0000000000000000000000000000000000000000' } ], }, }; - const component = await mount( - - - , - ); + const component = await render(); await expect(component).toHaveScreenshot(); }); -test('complex success', async({ mount }) => { - const result: ContractMethodReadResult = { +test('complex success', async({ render }) => { + const result: FormSubmitResultApi['result'] = { is_error: false, result: { names: [ @@ -122,11 +101,7 @@ test('complex success', async({ mount }) => { ], }, }; - const component = await mount( - - - , - ); + const component = await render(); await expect(component).toHaveScreenshot(); }); diff --git a/ui/address/contract/ABI/form/ContractMethodResultApi.tsx b/ui/address/contract/ABI/form/ContractMethodResultApi.tsx new file mode 100644 index 0000000000..ba8e5fd928 --- /dev/null +++ b/ui/address/contract/ABI/form/ContractMethodResultApi.tsx @@ -0,0 +1,64 @@ +import { Box, chakra, useColorModeValue } from '@chakra-ui/react'; +import React from 'react'; + +import type { ContractAbiItem, FormSubmitResultApi } from '../types'; + +import hexToUtf8 from 'lib/hexToUtf8'; + +import ContractMethodResultApiError from './ContractMethodResultApiError'; +import ContractMethodResultApiItem from './ContractMethodResultApiItem'; + +interface Props { + item: ContractAbiItem; + result: FormSubmitResultApi['result']; + onSettle: () => void; +} + +const ContractMethodResultApi = ({ item, result, onSettle }: Props) => { + const resultBgColor = useColorModeValue('blackAlpha.50', 'whiteAlpha.50'); + + React.useEffect(() => { + onSettle(); + }, [ onSettle ]); + + if ('status' in result) { + return { result.statusText }; + } + + if (result instanceof Error) { + return { result.message }; + } + + if (result.is_error) { + if ('error' in result.result) { + return { result.result.error }; + } + + if ('message' in result.result) { + return [{ result.result.code }] { result.result.message }; + } + + if ('raw' in result.result) { + return { `Revert reason: ${ hexToUtf8(result.result.raw) }` }; + } + + if ('method_id' in result.result) { + return { JSON.stringify(result.result, undefined, 2) }; + } + + return Something went wrong.; + } + + return ( + +

+ [ { 'name' in item ? item.name : '' } method response ] +

+

[

+ { result.result.output.map((output, index) => ) } +

]

+
+ ); +}; + +export default React.memo(ContractMethodResultApi); diff --git a/ui/address/contract/ABI/form/ContractMethodResultApiError.tsx b/ui/address/contract/ABI/form/ContractMethodResultApiError.tsx new file mode 100644 index 0000000000..bbcfacf407 --- /dev/null +++ b/ui/address/contract/ABI/form/ContractMethodResultApiError.tsx @@ -0,0 +1,16 @@ +import { Alert } from '@chakra-ui/react'; +import React from 'react'; + +interface Props { + children: React.ReactNode; +} + +const ContractMethodResultApiError = ({ children }: Props) => { + return ( + + { children } + + ); +}; + +export default React.memo(ContractMethodResultApiError); diff --git a/ui/address/contract/ABI/form/ContractMethodResultApiItem.tsx b/ui/address/contract/ABI/form/ContractMethodResultApiItem.tsx new file mode 100644 index 0000000000..e1d0bacaf6 --- /dev/null +++ b/ui/address/contract/ABI/form/ContractMethodResultApiItem.tsx @@ -0,0 +1,45 @@ +import { chakra } from '@chakra-ui/react'; +import React from 'react'; + +import type { SmartContractQueryMethodSuccess } from 'types/api/contract'; + +const TUPLE_TYPE_REGEX = /\[(.+)\]/; + +interface Props { + output: SmartContractQueryMethodSuccess['result']['output'][0]; + name: SmartContractQueryMethodSuccess['result']['names'][0]; +} + +const ContractMethodResultApiItem = ({ output, name }: Props) => { + if (Array.isArray(name)) { + const [ structName, argNames ] = name; + const argTypes = output.type.match(TUPLE_TYPE_REGEX)?.[1].split(','); + + return ( + <> +

+ { structName } + ({ output.type }) : +

+ { argNames.map((argName, argIndex) => { + return ( +

+ { argName } + { argTypes?.[argIndex] ? ` (${ argTypes[argIndex] })` : '' } : { String(output.value[argIndex]) } +

+ ); + }) } + + ); + } + + return ( +

+ + { name && { name } } + ({ output.type }) : { String(output.value) } +

+ ); +}; + +export default React.memo(ContractMethodResultApiItem); diff --git a/ui/address/contract/ContractWriteResultDumb.pw.tsx b/ui/address/contract/ABI/form/ContractMethodResultWalletClient.pw.tsx similarity index 59% rename from ui/address/contract/ContractWriteResultDumb.pw.tsx rename to ui/address/contract/ABI/form/ContractMethodResultWalletClient.pw.tsx index e916ab90c6..f8bb6b6c99 100644 --- a/ui/address/contract/ContractWriteResultDumb.pw.tsx +++ b/ui/address/contract/ABI/form/ContractMethodResultWalletClient.pw.tsx @@ -1,53 +1,43 @@ -import { test, expect } from '@playwright/experimental-ct-react'; import React from 'react'; -import TestApp from 'playwright/TestApp'; +import { test, expect } from 'playwright/lib'; -import ContractWriteResultDumb from './ContractWriteResultDumb'; +import type { PropsDumb } from './ContractMethodResultWalletClient'; +import { ContractMethodResultWalletClientDumb } from './ContractMethodResultWalletClient'; -test('loading', async({ mount }) => { +test('loading', async({ render }) => { const props = { txInfo: { status: 'pending' as const, error: null, - }, + } as PropsDumb['txInfo'], result: { hash: '0x363574E6C5C71c343d7348093D84320c76d5Dd29' as `0x${ string }`, }, onSettle: () => {}, }; - const component = await mount( - - - , - ); - + const component = await render(); await expect(component).toHaveScreenshot(); }); -test('success', async({ mount }) => { +test('success', async({ render }) => { const props = { txInfo: { status: 'success' as const, error: null, - }, + } as PropsDumb['txInfo'], result: { hash: '0x363574E6C5C71c343d7348093D84320c76d5Dd29' as `0x${ string }`, }, onSettle: () => {}, }; - const component = await mount( - - - , - ); - + const component = await render(); await expect(component).toHaveScreenshot(); }); -test('error +@mobile', async({ mount }) => { +test('error +@mobile', async({ render }) => { const props = { txInfo: { status: 'error' as const, @@ -55,39 +45,29 @@ test('error +@mobile', async({ mount }) => { // eslint-disable-next-line max-len message: 'missing revert data in call exception; Transaction reverted without a reason string [ See: https://links.ethers.org/v5-errors-CALL_EXCEPTION ]', } as Error, - }, + } as PropsDumb['txInfo'], result: { hash: '0x363574E6C5C71c343d7348093D84320c76d5Dd29' as `0x${ string }`, }, onSettle: () => {}, }; - const component = await mount( - - - , - ); - + const component = await render(); await expect(component).toHaveScreenshot(); }); -test('error in result', async({ mount }) => { +test('error in result', async({ render }) => { const props = { txInfo: { status: 'idle' as const, error: null, - }, + } as unknown as PropsDumb['txInfo'], result: { message: 'wallet is not connected', } as Error, onSettle: () => {}, }; - const component = await mount( - - - , - ); - + const component = await render(); await expect(component).toHaveScreenshot(); }); diff --git a/ui/address/contract/ContractWriteResultDumb.tsx b/ui/address/contract/ABI/form/ContractMethodResultWalletClient.tsx similarity index 65% rename from ui/address/contract/ContractWriteResultDumb.tsx rename to ui/address/contract/ABI/form/ContractMethodResultWalletClient.tsx index 3b898fb570..371b914205 100644 --- a/ui/address/contract/ContractWriteResultDumb.tsx +++ b/ui/address/contract/ABI/form/ContractMethodResultWalletClient.tsx @@ -1,22 +1,35 @@ -import { Box, chakra, Spinner } from '@chakra-ui/react'; +import { chakra, Spinner, Box } from '@chakra-ui/react'; import React from 'react'; +import type { UseWaitForTransactionReceiptReturnType } from 'wagmi'; +import { useWaitForTransactionReceipt } from 'wagmi'; -import type { ContractMethodWriteResult } from './types'; +import type { FormSubmitResultWalletClient } from '../types'; import { route } from 'nextjs-routes'; import LinkInternal from 'ui/shared/LinkInternal'; interface Props { - result: ContractMethodWriteResult; + result: FormSubmitResultWalletClient['result']; onSettle: () => void; - txInfo: { - status: 'loading' | 'success' | 'error' | 'idle' | 'pending'; - error: Error | null; - }; } -const ContractWriteResultDumb = ({ result, onSettle, txInfo }: Props) => { +const ContractMethodResultWalletClient = ({ result, onSettle }: Props) => { + const txHash = result && 'hash' in result ? result.hash as `0x${ string }` : undefined; + const txInfo = useWaitForTransactionReceipt({ + hash: txHash, + }); + + return ; +}; + +export interface PropsDumb { + result: FormSubmitResultWalletClient['result']; + onSettle: () => void; + txInfo: UseWaitForTransactionReceiptReturnType; +} + +export const ContractMethodResultWalletClientDumb = ({ result, onSettle, txInfo }: PropsDumb) => { const txHash = result && 'hash' in result ? result.hash : undefined; React.useEffect(() => { @@ -93,4 +106,4 @@ const ContractWriteResultDumb = ({ result, onSettle, txInfo }: Props) => { ); }; -export default React.memo(ContractWriteResultDumb); +export default React.memo(ContractMethodResultWalletClient); diff --git a/ui/address/contract/methodForm/__screenshots__/ContractMethodForm.pw.tsx_dark-color-mode_base-view-mobile-dark-mode-1.png b/ui/address/contract/ABI/form/__screenshots__/ContractMethodForm.pw.tsx_dark-color-mode_base-view-mobile-dark-mode-1.png similarity index 91% rename from ui/address/contract/methodForm/__screenshots__/ContractMethodForm.pw.tsx_dark-color-mode_base-view-mobile-dark-mode-1.png rename to ui/address/contract/ABI/form/__screenshots__/ContractMethodForm.pw.tsx_dark-color-mode_base-view-mobile-dark-mode-1.png index 947ab4a899..85a1ffa319 100644 Binary files a/ui/address/contract/methodForm/__screenshots__/ContractMethodForm.pw.tsx_dark-color-mode_base-view-mobile-dark-mode-1.png and b/ui/address/contract/ABI/form/__screenshots__/ContractMethodForm.pw.tsx_dark-color-mode_base-view-mobile-dark-mode-1.png differ diff --git a/ui/address/contract/methodForm/__screenshots__/ContractMethodForm.pw.tsx_default_base-view-mobile-dark-mode-1.png b/ui/address/contract/ABI/form/__screenshots__/ContractMethodForm.pw.tsx_default_base-view-mobile-dark-mode-1.png similarity index 87% rename from ui/address/contract/methodForm/__screenshots__/ContractMethodForm.pw.tsx_default_base-view-mobile-dark-mode-1.png rename to ui/address/contract/ABI/form/__screenshots__/ContractMethodForm.pw.tsx_default_base-view-mobile-dark-mode-1.png index b600b4167c..89230b3a16 100644 Binary files a/ui/address/contract/methodForm/__screenshots__/ContractMethodForm.pw.tsx_default_base-view-mobile-dark-mode-1.png and b/ui/address/contract/ABI/form/__screenshots__/ContractMethodForm.pw.tsx_default_base-view-mobile-dark-mode-1.png differ diff --git a/ui/address/contract/methodForm/__screenshots__/ContractMethodForm.pw.tsx_mobile_base-view-mobile-dark-mode-1.png b/ui/address/contract/ABI/form/__screenshots__/ContractMethodForm.pw.tsx_mobile_base-view-mobile-dark-mode-1.png similarity index 86% rename from ui/address/contract/methodForm/__screenshots__/ContractMethodForm.pw.tsx_mobile_base-view-mobile-dark-mode-1.png rename to ui/address/contract/ABI/form/__screenshots__/ContractMethodForm.pw.tsx_mobile_base-view-mobile-dark-mode-1.png index 1cf1fca155..8aba610638 100644 Binary files a/ui/address/contract/methodForm/__screenshots__/ContractMethodForm.pw.tsx_mobile_base-view-mobile-dark-mode-1.png and b/ui/address/contract/ABI/form/__screenshots__/ContractMethodForm.pw.tsx_mobile_base-view-mobile-dark-mode-1.png differ diff --git a/ui/address/contract/ABI/form/__screenshots__/ContractMethodResultApi.pw.tsx_default_complex-error-1.png b/ui/address/contract/ABI/form/__screenshots__/ContractMethodResultApi.pw.tsx_default_complex-error-1.png new file mode 100644 index 0000000000..b96ceac2e2 Binary files /dev/null and b/ui/address/contract/ABI/form/__screenshots__/ContractMethodResultApi.pw.tsx_default_complex-error-1.png differ diff --git a/ui/address/contract/ABI/form/__screenshots__/ContractMethodResultApi.pw.tsx_default_complex-success-1.png b/ui/address/contract/ABI/form/__screenshots__/ContractMethodResultApi.pw.tsx_default_complex-success-1.png new file mode 100644 index 0000000000..d5902ddf0e Binary files /dev/null and b/ui/address/contract/ABI/form/__screenshots__/ContractMethodResultApi.pw.tsx_default_complex-success-1.png differ diff --git a/ui/address/contract/ABI/form/__screenshots__/ContractMethodResultApi.pw.tsx_default_default-error-1.png b/ui/address/contract/ABI/form/__screenshots__/ContractMethodResultApi.pw.tsx_default_default-error-1.png new file mode 100644 index 0000000000..cd23d7452d Binary files /dev/null and b/ui/address/contract/ABI/form/__screenshots__/ContractMethodResultApi.pw.tsx_default_default-error-1.png differ diff --git a/ui/address/contract/ABI/form/__screenshots__/ContractMethodResultApi.pw.tsx_default_error-with-code-1.png b/ui/address/contract/ABI/form/__screenshots__/ContractMethodResultApi.pw.tsx_default_error-with-code-1.png new file mode 100644 index 0000000000..e5d2fcf25c Binary files /dev/null and b/ui/address/contract/ABI/form/__screenshots__/ContractMethodResultApi.pw.tsx_default_error-with-code-1.png differ diff --git a/ui/address/contract/ABI/form/__screenshots__/ContractMethodResultApi.pw.tsx_default_raw-error-1.png b/ui/address/contract/ABI/form/__screenshots__/ContractMethodResultApi.pw.tsx_default_raw-error-1.png new file mode 100644 index 0000000000..241bb2f7e3 Binary files /dev/null and b/ui/address/contract/ABI/form/__screenshots__/ContractMethodResultApi.pw.tsx_default_raw-error-1.png differ diff --git a/ui/address/contract/ABI/form/__screenshots__/ContractMethodResultApi.pw.tsx_default_success-1.png b/ui/address/contract/ABI/form/__screenshots__/ContractMethodResultApi.pw.tsx_default_success-1.png new file mode 100644 index 0000000000..ab0f9ab157 Binary files /dev/null and b/ui/address/contract/ABI/form/__screenshots__/ContractMethodResultApi.pw.tsx_default_success-1.png differ diff --git a/ui/address/contract/ABI/form/__screenshots__/ContractMethodResultWalletClient.pw.tsx_default_error-in-result-1.png b/ui/address/contract/ABI/form/__screenshots__/ContractMethodResultWalletClient.pw.tsx_default_error-in-result-1.png new file mode 100644 index 0000000000..0d50122e57 Binary files /dev/null and b/ui/address/contract/ABI/form/__screenshots__/ContractMethodResultWalletClient.pw.tsx_default_error-in-result-1.png differ diff --git a/ui/address/contract/ABI/form/__screenshots__/ContractMethodResultWalletClient.pw.tsx_default_error-mobile-1.png b/ui/address/contract/ABI/form/__screenshots__/ContractMethodResultWalletClient.pw.tsx_default_error-mobile-1.png new file mode 100644 index 0000000000..79e30aeaa0 Binary files /dev/null and b/ui/address/contract/ABI/form/__screenshots__/ContractMethodResultWalletClient.pw.tsx_default_error-mobile-1.png differ diff --git a/ui/address/contract/ABI/form/__screenshots__/ContractMethodResultWalletClient.pw.tsx_default_loading-1.png b/ui/address/contract/ABI/form/__screenshots__/ContractMethodResultWalletClient.pw.tsx_default_loading-1.png new file mode 100644 index 0000000000..d80cc80e2f Binary files /dev/null and b/ui/address/contract/ABI/form/__screenshots__/ContractMethodResultWalletClient.pw.tsx_default_loading-1.png differ diff --git a/ui/address/contract/ABI/form/__screenshots__/ContractMethodResultWalletClient.pw.tsx_default_success-1.png b/ui/address/contract/ABI/form/__screenshots__/ContractMethodResultWalletClient.pw.tsx_default_success-1.png new file mode 100644 index 0000000000..4bd45c7a0b Binary files /dev/null and b/ui/address/contract/ABI/form/__screenshots__/ContractMethodResultWalletClient.pw.tsx_default_success-1.png differ diff --git a/ui/address/contract/__screenshots__/ContractWriteResultDumb.pw.tsx_mobile_error-mobile-1.png b/ui/address/contract/ABI/form/__screenshots__/ContractMethodResultWalletClient.pw.tsx_mobile_error-mobile-1.png similarity index 100% rename from ui/address/contract/__screenshots__/ContractWriteResultDumb.pw.tsx_mobile_error-mobile-1.png rename to ui/address/contract/ABI/form/__screenshots__/ContractMethodResultWalletClient.pw.tsx_mobile_error-mobile-1.png diff --git a/ui/address/contract/methodForm/useFormatFieldValue.tsx b/ui/address/contract/ABI/form/useFormatFieldValue.tsx similarity index 90% rename from ui/address/contract/methodForm/useFormatFieldValue.tsx rename to ui/address/contract/ABI/form/useFormatFieldValue.tsx index 2a54da6ab0..70bd8270fe 100644 --- a/ui/address/contract/methodForm/useFormatFieldValue.tsx +++ b/ui/address/contract/ABI/form/useFormatFieldValue.tsx @@ -1,11 +1,9 @@ import React from 'react'; -import type { SmartContractMethodArgType } from 'types/api/contract'; - import type { MatchInt } from './utils'; interface Params { - argType: SmartContractMethodArgType; + argType: string; argTypeMatchInt: MatchInt | null; } diff --git a/ui/address/contract/methodForm/useValidateField.tsx b/ui/address/contract/ABI/form/useValidateField.tsx similarity index 95% rename from ui/address/contract/methodForm/useValidateField.tsx rename to ui/address/contract/ABI/form/useValidateField.tsx index 60300c6121..de9d506f31 100644 --- a/ui/address/contract/methodForm/useValidateField.tsx +++ b/ui/address/contract/ABI/form/useValidateField.tsx @@ -1,13 +1,11 @@ import React from 'react'; import { getAddress, isAddress, isHex } from 'viem'; -import type { SmartContractMethodArgType } from 'types/api/contract'; - import type { MatchInt } from './utils'; import { BYTES_REGEXP } from './utils'; interface Params { - argType: SmartContractMethodArgType; + argType: string; argTypeMatchInt: MatchInt | null; isOptional: boolean; } diff --git a/ui/address/contract/methodForm/utils.test.ts b/ui/address/contract/ABI/form/utils.test.ts similarity index 100% rename from ui/address/contract/methodForm/utils.test.ts rename to ui/address/contract/ABI/form/utils.test.ts diff --git a/ui/address/contract/methodForm/utils.ts b/ui/address/contract/ABI/form/utils.ts similarity index 79% rename from ui/address/contract/methodForm/utils.ts rename to ui/address/contract/ABI/form/utils.ts index a0a136134e..55abd78f1c 100644 --- a/ui/address/contract/methodForm/utils.ts +++ b/ui/address/contract/ABI/form/utils.ts @@ -1,6 +1,6 @@ import _set from 'lodash/set'; -import type { SmartContractMethodArgType, SmartContractMethodInput } from 'types/api/contract'; +import type { ContractAbiItemInput } from '../types'; export type ContractMethodFormFields = Record; @@ -11,22 +11,22 @@ export const BYTES_REGEXP = /^bytes(\d+)?$/i; export const ARRAY_REGEXP = /^(.*)\[(\d*)\]$/; export interface MatchArray { - itemType: SmartContractMethodArgType; + itemType: string; size: number; isNested: boolean; } -export const matchArray = (argType: SmartContractMethodArgType): MatchArray | null => { +export const matchArray = (argType: string): MatchArray | null => { const match = argType.match(ARRAY_REGEXP); if (!match) { return null; } const [ , itemType, size ] = match; - const isNested = Boolean(matchArray(itemType as SmartContractMethodArgType)); + const isNested = Boolean(matchArray(itemType)); return { - itemType: itemType as SmartContractMethodArgType, + itemType, size: size ? Number(size) : Infinity, isNested, }; @@ -39,7 +39,7 @@ export interface MatchInt { max: bigint; } -export const matchInt = (argType: SmartContractMethodArgType): MatchInt | null => { +export const matchInt = (argType: string): MatchInt | null => { const match = argType.match(INT_REGEXP); if (!match) { return null; @@ -51,9 +51,9 @@ export const matchInt = (argType: SmartContractMethodArgType): MatchInt | null = return { isUnsigned: Boolean(isUnsigned), power, min, max }; }; -export const transformDataForArrayItem = (data: SmartContractMethodInput, index: number): SmartContractMethodInput => { +export const transformDataForArrayItem = (data: ContractAbiItemInput, index: number): ContractAbiItemInput => { const arrayMatchType = matchArray(data.type); - const arrayMatchInternalType = data.internalType ? matchArray(data.internalType as SmartContractMethodArgType) : null; + const arrayMatchInternalType = data.internalType ? matchArray(data.internalType) : null; const childrenInternalType = arrayMatchInternalType?.itemType.replaceAll('struct ', ''); const postfix = childrenInternalType ? ' ' + childrenInternalType : ''; @@ -97,7 +97,7 @@ function filterOurEmptyItems(array: Array): Array { .filter((item) => item !== undefined); } -export function getFieldLabel(input: SmartContractMethodInput, isRequired?: boolean) { +export function getFieldLabel(input: ContractAbiItemInput, isRequired?: boolean) { const name = input.name || input.internalType || ''; return `${ name } (${ input.type })${ isRequired ? '*' : '' }`; } diff --git a/ui/address/contract/ABI/types.ts b/ui/address/contract/ABI/types.ts new file mode 100644 index 0000000000..7268edee30 --- /dev/null +++ b/ui/address/contract/ABI/types.ts @@ -0,0 +1,25 @@ +import type { AbiFunction } from 'abitype'; + +import type { SmartContractMethod, SmartContractMethodOutput, SmartContractQueryMethod } from 'types/api/contract'; + +import type { ResourceError } from 'lib/api/resources'; + +export type ContractAbiItemInput = AbiFunction['inputs'][number] & { fieldType?: 'native_coin' }; +export type ContractAbiItemOutput = SmartContractMethodOutput; +export type ContractAbiItem = SmartContractMethod; +export type ContractAbi = Array; + +export type MethodType = 'read' | 'write'; +export type MethodCallStrategy = 'api' | 'wallet_client'; + +export interface FormSubmitResultApi { + source: 'api'; + result: SmartContractQueryMethod | ResourceError | Error; +} +export interface FormSubmitResultWalletClient { + source: 'wallet_client'; + result: Error | { hash: `0x${ string }` | undefined } | undefined; +} +export type FormSubmitResult = FormSubmitResultApi | FormSubmitResultWalletClient; + +export type FormSubmitHandler = (item: ContractAbiItem, args: Array, submitType: MethodCallStrategy | undefined) => Promise; diff --git a/ui/address/contract/ABI/useCallMethodApi.ts b/ui/address/contract/ABI/useCallMethodApi.ts new file mode 100644 index 0000000000..80345a222d --- /dev/null +++ b/ui/address/contract/ABI/useCallMethodApi.ts @@ -0,0 +1,51 @@ +import React from 'react'; + +import type { FormSubmitResult } from './types'; +import type { SmartContractQueryMethod } from 'types/api/contract'; + +import type { ResourceError } from 'lib/api/resources'; +import useApiFetch from 'lib/api/useApiFetch'; +import useAccount from 'lib/web3/useAccount'; + +interface Params { + methodId: string; + args: Array; + isProxy: boolean; + isCustomAbi: boolean; + addressHash: string; +} + +export default function useCallMethodApi(): (params: Params) => Promise { + const apiFetch = useApiFetch(); + const { address } = useAccount(); + + return React.useCallback(async({ addressHash, isCustomAbi, isProxy, args, methodId }) => { + try { + const response = await apiFetch<'contract_method_query', SmartContractQueryMethod>('contract_method_query', { + pathParams: { hash: addressHash }, + queryParams: { + is_custom_abi: isCustomAbi ? 'true' : 'false', + }, + fetchParams: { + method: 'POST', + body: { + args, + method_id: methodId, + contract_type: isProxy ? 'proxy' : 'regular', + from: address, + }, + }, + }); + + return { + source: 'api', + result: response, + }; + } catch (error) { + return { + source: 'api', + result: error as (Error | ResourceError), + }; + } + }, [ address, apiFetch ]); +} diff --git a/ui/address/contract/ABI/useCallMethodWalletClient.ts b/ui/address/contract/ABI/useCallMethodWalletClient.ts new file mode 100644 index 0000000000..1ec49b0d32 --- /dev/null +++ b/ui/address/contract/ABI/useCallMethodWalletClient.ts @@ -0,0 +1,70 @@ +import React from 'react'; +import type { Abi } from 'viem'; +import { useAccount, useWalletClient, useSwitchChain } from 'wagmi'; + +import type { ContractAbiItem, FormSubmitResult } from './types'; + +import config from 'configs/app'; + +import { getNativeCoinValue } from './utils'; + +interface Params { + item: ContractAbiItem; + args: Array; + addressHash: string; +} + +export default function useCallMethodWalletClient(): (params: Params) => Promise { + const { data: walletClient } = useWalletClient(); + const { isConnected, chainId } = useAccount(); + const { switchChainAsync } = useSwitchChain(); + + return React.useCallback(async({ args, item, addressHash }) => { + if (!isConnected) { + throw new Error('Wallet is not connected'); + } + + if (!walletClient) { + throw new Error('Wallet Client is not defined'); + } + + if (chainId && String(chainId) !== config.chain.id) { + await switchChainAsync?.({ chainId: Number(config.chain.id) }); + } + + if (item.type === 'receive' || item.type === 'fallback') { + const value = getNativeCoinValue(args[0]); + const hash = await walletClient.sendTransaction({ + to: addressHash as `0x${ string }` | undefined, + value, + }); + return { source: 'wallet_client', result: { hash } }; + } + + const methodName = item.name; + + if (!methodName) { + throw new Error('Method name is not defined'); + } + + const _args = args.slice(0, item.inputs.length); + const value = getNativeCoinValue(args[item.inputs.length]); + + const hash = await walletClient.writeContract({ + args: _args, + // Here we provide the ABI as an array containing only one item from the submitted form. + // This is a workaround for the issue with the "viem" library. + // It lacks a "method_id" field to uniquely identify the correct method and instead attempts to find a method based on its name. + // But the name is not unique in the contract ABI and this behavior in the "viem" could result in calling the wrong method. + // See related issues: + // - https://github.com/blockscout/frontend/issues/1032, + // - https://github.com/blockscout/frontend/issues/1327 + abi: [ item ] as Abi, + functionName: methodName, + address: addressHash as `0x${ string }`, + value, + }); + + return { source: 'wallet_client', result: { hash } }; + }, [ chainId, isConnected, switchChainAsync, walletClient ]); +} diff --git a/ui/address/contract/ABI/useFormSubmit.ts b/ui/address/contract/ABI/useFormSubmit.ts new file mode 100644 index 0000000000..a401955658 --- /dev/null +++ b/ui/address/contract/ABI/useFormSubmit.ts @@ -0,0 +1,72 @@ +import React from 'react'; + +import type { FormSubmitHandler } from './types'; + +import config from 'configs/app'; + +import useCallMethodApi from './useCallMethodApi'; +import useCallMethodWalletClient from './useCallMethodWalletClient'; + +interface Params { + tab: string; + addressHash: string; +} + +function useFormSubmit({ tab, addressHash }: Params): FormSubmitHandler { + const callMethodApi = useCallMethodApi(); + const callMethodWalletClient = useCallMethodWalletClient(); + + return React.useCallback(async(item, args, strategy) => { + switch (strategy) { + case 'api': { + if (!('method_id' in item)) { + throw new Error('Method ID is not defined'); + } + return callMethodApi({ + args, + methodId: item.method_id, + addressHash, + isCustomAbi: tab === 'read_custom_methods' || tab === 'write_custom_methods', + isProxy: tab === 'read_proxy' || tab === 'write_proxy', + }); + } + case 'wallet_client': { + return callMethodWalletClient({ args, item, addressHash }); + } + + default: { + throw new Error(`Unknown call strategy "${ strategy }"`); + } + } + }, [ addressHash, callMethodApi, callMethodWalletClient, tab ]); +} + +function useFormSubmitFallback({ tab, addressHash }: Params): FormSubmitHandler { + const callMethodApi = useCallMethodApi(); + + return React.useCallback(async(item, args, strategy) => { + switch (strategy) { + case 'api': { + if (!('method_id' in item)) { + throw new Error('Method ID is not defined'); + } + return callMethodApi({ + args, + methodId: item.method_id, + addressHash, + isCustomAbi: tab === 'read_custom_methods' || tab === 'write_custom_methods', + isProxy: tab === 'read_proxy' || tab === 'write_proxy', + }); + } + + default: { + throw new Error(`Unknown call strategy "${ strategy }"`); + } + } + + }, [ addressHash, callMethodApi, tab ]); +} + +const hook = config.features.blockchainInteraction.isEnabled ? useFormSubmit : useFormSubmitFallback; + +export default hook; diff --git a/ui/address/contract/ABI/useScrollToMethod.ts b/ui/address/contract/ABI/useScrollToMethod.ts new file mode 100644 index 0000000000..ddeac03d5d --- /dev/null +++ b/ui/address/contract/ABI/useScrollToMethod.ts @@ -0,0 +1,26 @@ +import React from 'react'; +import { scroller } from 'react-scroll'; + +import type { ContractAbi } from './types'; + +export const getElementName = (id: string) => `method_${ id }`; + +export default function useScrollToMethod(data: ContractAbi, onScroll: (indices: Array) => void) { + React.useEffect(() => { + const id = window.location.hash.replace('#', ''); + + if (!id) { + return; + } + + const index = data.findIndex((item) => 'method_id' in item && item.method_id === id); + if (index > -1) { + scroller.scrollTo(getElementName(id), { + duration: 500, + smooth: true, + offset: -100, + }); + onScroll([ index ]); + } + }, [ data, onScroll ]); +} diff --git a/ui/address/contract/ABI/utils.ts b/ui/address/contract/ABI/utils.ts new file mode 100644 index 0000000000..46c9fb2c8a --- /dev/null +++ b/ui/address/contract/ABI/utils.ts @@ -0,0 +1,7 @@ +export const getNativeCoinValue = (value: unknown) => { + if (typeof value !== 'string') { + return BigInt(0); + } + + return BigInt(value); +}; diff --git a/ui/address/contract/ContractRead.tsx b/ui/address/contract/ContractRead.tsx index 268b268d43..9bd819025c 100644 --- a/ui/address/contract/ContractRead.tsx +++ b/ui/address/contract/ContractRead.tsx @@ -1,31 +1,24 @@ -import { Alert, Flex } from '@chakra-ui/react'; import { useRouter } from 'next/router'; import React from 'react'; -import type { SmartContractReadMethod, SmartContractQueryMethodRead } from 'types/api/contract'; - -import useApiFetch from 'lib/api/useApiFetch'; +import config from 'configs/app'; import useApiQuery from 'lib/api/useApiQuery'; import getQueryParamString from 'lib/router/getQueryParamString'; -import ContractMethodsAccordion from 'ui/address/contract/ContractMethodsAccordion'; +import useAccount from 'lib/web3/useAccount'; import ContentLoader from 'ui/shared/ContentLoader'; import DataFetchAlert from 'ui/shared/DataFetchAlert'; +import ContractAbi from './ABI/ContractAbi'; import ContractConnectWallet from './ContractConnectWallet'; import ContractCustomAbiAlert from './ContractCustomAbiAlert'; import ContractImplementationAddress from './ContractImplementationAddress'; -import ContractMethodConstant from './ContractMethodConstant'; -import ContractReadResult from './ContractReadResult'; -import ContractMethodForm from './methodForm/ContractMethodForm'; -import useWatchAccount from './useWatchAccount'; interface Props { isLoading?: boolean; } const ContractRead = ({ isLoading }: Props) => { - const apiFetch = useApiFetch(); - const account = useWatchAccount(); + const { address } = useAccount(); const router = useRouter(); const tab = getQueryParamString(router.query.tab); @@ -37,55 +30,13 @@ const ContractRead = ({ isLoading }: Props) => { pathParams: { hash: addressHash }, queryParams: { is_custom_abi: isCustomAbi ? 'true' : 'false', - from: account?.address, + from: address, }, queryOptions: { enabled: !isLoading, }, }); - const handleMethodFormSubmit = React.useCallback(async(item: SmartContractReadMethod, args: Array) => { - return apiFetch<'contract_method_query', SmartContractQueryMethodRead>('contract_method_query', { - pathParams: { hash: addressHash }, - queryParams: { - is_custom_abi: isCustomAbi ? 'true' : 'false', - }, - fetchParams: { - method: 'POST', - body: { - args, - method_id: item.method_id, - contract_type: isProxy ? 'proxy' : 'regular', - from: account?.address, - }, - }, - }); - }, [ account?.address, addressHash, apiFetch, isCustomAbi, isProxy ]); - - const renderItemContent = React.useCallback((item: SmartContractReadMethod, index: number, id: number) => { - if (item.error) { - return { item.error }; - } - - if (item.outputs?.some(({ value }) => value !== undefined && value !== null)) { - return ( - - { item.outputs.map((output, index) => ) } - - ); - } - - return ( - - ); - }, [ handleMethodFormSubmit ]); - if (isError) { return ; } @@ -101,9 +52,9 @@ const ContractRead = ({ isLoading }: Props) => { return ( <> { isCustomAbi && } - { account && } + { config.features.blockchainInteraction.isEnabled && } { isProxy && } - + ); }; diff --git a/ui/address/contract/ContractReadResult.tsx b/ui/address/contract/ContractReadResult.tsx deleted file mode 100644 index 28f701eb9b..0000000000 --- a/ui/address/contract/ContractReadResult.tsx +++ /dev/null @@ -1,105 +0,0 @@ -import { Alert, Box, chakra, useColorModeValue } from '@chakra-ui/react'; -import React from 'react'; - -import type { ContractMethodReadResult } from './types'; -import type { SmartContractQueryMethodReadSuccess, SmartContractReadMethod } from 'types/api/contract'; - -import hexToUtf8 from 'lib/hexToUtf8'; - -const TUPLE_TYPE_REGEX = /\[(.+)\]/; - -const ContractReadResultError = ({ children }: {children: React.ReactNode}) => { - return ( - - { children } - - ); -}; - -interface ItemProps { - output: SmartContractQueryMethodReadSuccess['result']['output'][0]; - name: SmartContractQueryMethodReadSuccess['result']['names'][0]; -} - -const ContractReadResultItem = ({ output, name }: ItemProps) => { - if (Array.isArray(name)) { - const [ structName, argNames ] = name; - const argTypes = output.type.match(TUPLE_TYPE_REGEX)?.[1].split(','); - - return ( - <> -

- { structName } - ({ output.type }) : -

- { argNames.map((argName, argIndex) => { - return ( -

- { argName } - { argTypes?.[argIndex] ? ` (${ argTypes[argIndex] })` : '' } : { String(output.value[argIndex]) } -

- ); - }) } - - ); - } - - return ( -

- - { name && { name } } - ({ output.type }) : { String(output.value) } -

- ); -}; - -interface Props { - item: SmartContractReadMethod; - result: ContractMethodReadResult; - onSettle: () => void; -} - -const ContractReadResult = ({ item, result, onSettle }: Props) => { - const resultBgColor = useColorModeValue('blackAlpha.50', 'whiteAlpha.50'); - - React.useEffect(() => { - onSettle(); - }, [ onSettle ]); - - if ('status' in result) { - return { result.statusText }; - } - - if (result.is_error) { - if ('error' in result.result) { - return { result.result.error }; - } - - if ('message' in result.result) { - return [{ result.result.code }] { result.result.message }; - } - - if ('raw' in result.result) { - return { `Revert reason: ${ hexToUtf8(result.result.raw) }` }; - } - - if ('method_id' in result.result) { - return { JSON.stringify(result.result, undefined, 2) }; - } - - return Something went wrong.; - } - - return ( - -

- [ { 'name' in item ? item.name : '' } method response ] -

-

[

- { result.result.output.map((output, index) => ) } -

]

-
- ); -}; - -export default React.memo(ContractReadResult); diff --git a/ui/address/contract/ContractWrite.tsx b/ui/address/contract/ContractWrite.tsx index 378aae7c25..b79d8e0a02 100644 --- a/ui/address/contract/ContractWrite.tsx +++ b/ui/address/contract/ContractWrite.tsx @@ -1,33 +1,22 @@ import { useRouter } from 'next/router'; import React from 'react'; -import { useAccount, useWalletClient, useSwitchChain } from 'wagmi'; - -import type { SmartContractWriteMethod } from 'types/api/contract'; import config from 'configs/app'; import useApiQuery from 'lib/api/useApiQuery'; import getQueryParamString from 'lib/router/getQueryParamString'; -import ContractMethodsAccordion from 'ui/address/contract/ContractMethodsAccordion'; import ContentLoader from 'ui/shared/ContentLoader'; import DataFetchAlert from 'ui/shared/DataFetchAlert'; +import ContractAbi from './ABI/ContractAbi'; import ContractConnectWallet from './ContractConnectWallet'; import ContractCustomAbiAlert from './ContractCustomAbiAlert'; import ContractImplementationAddress from './ContractImplementationAddress'; -import ContractWriteResult from './ContractWriteResult'; -import ContractMethodForm from './methodForm/ContractMethodForm'; -import useContractAbi from './useContractAbi'; -import { getNativeCoinValue, prepareAbi } from './utils'; interface Props { isLoading?: boolean; } const ContractWrite = ({ isLoading }: Props) => { - const { data: walletClient } = useWalletClient(); - const { isConnected, chainId } = useAccount(); - const { switchChainAsync } = useSwitchChain(); - const router = useRouter(); const tab = getQueryParamString(router.query.tab); @@ -46,63 +35,6 @@ const ContractWrite = ({ isLoading }: Props) => { }, }); - const contractAbi = useContractAbi({ addressHash, isProxy, isCustomAbi }); - - const handleMethodFormSubmit = React.useCallback(async(item: SmartContractWriteMethod, args: Array) => { - if (!isConnected) { - throw new Error('Wallet is not connected'); - } - - if (chainId && String(chainId) !== config.chain.id) { - await switchChainAsync?.({ chainId: Number(config.chain.id) }); - } - - if (!contractAbi) { - throw new Error('Something went wrong. Try again later.'); - } - - if (item.type === 'receive' || item.type === 'fallback') { - const value = getNativeCoinValue(args[0]); - const hash = await walletClient?.sendTransaction({ - to: addressHash as `0x${ string }` | undefined, - value, - }); - return { hash }; - } - - const methodName = item.name; - - if (!methodName) { - throw new Error('Method name is not defined'); - } - - const _args = args.slice(0, item.inputs.length); - const value = getNativeCoinValue(args[item.inputs.length]); - const abi = prepareAbi(contractAbi, item); - - const hash = await walletClient?.writeContract({ - args: _args, - abi, - functionName: methodName, - address: addressHash as `0x${ string }`, - value, - }); - - return { hash }; - }, [ isConnected, chainId, contractAbi, walletClient, addressHash, switchChainAsync ]); - - const renderItemContent = React.useCallback((item: SmartContractWriteMethod, index: number, id: number) => { - return ( - - ); - }, [ handleMethodFormSubmit ]); - if (isError) { return ; } @@ -118,9 +50,9 @@ const ContractWrite = ({ isLoading }: Props) => { return ( <> { isCustomAbi && } - + { config.features.blockchainInteraction.isEnabled && } { isProxy && } - + ); }; diff --git a/ui/address/contract/ContractWriteResult.tsx b/ui/address/contract/ContractWriteResult.tsx deleted file mode 100644 index 3cc755d131..0000000000 --- a/ui/address/contract/ContractWriteResult.tsx +++ /dev/null @@ -1,19 +0,0 @@ -import React from 'react'; -import { useWaitForTransactionReceipt } from 'wagmi'; - -import type { ResultComponentProps } from './methodForm/types'; -import type { ContractMethodWriteResult } from './types'; -import type { SmartContractWriteMethod } from 'types/api/contract'; - -import ContractWriteResultDumb from './ContractWriteResultDumb'; - -const ContractWriteResult = ({ result, onSettle }: ResultComponentProps) => { - const txHash = result && 'hash' in result ? result.hash as `0x${ string }` : undefined; - const txInfo = useWaitForTransactionReceipt({ - hash: txHash, - }); - - return ; -}; - -export default React.memo(ContractWriteResult) as typeof ContractWriteResult; diff --git a/ui/address/contract/__screenshots__/ContractRead.pw.tsx_dark-color-mode_base-view-mobile-dark-mode-1.png b/ui/address/contract/__screenshots__/ContractRead.pw.tsx_dark-color-mode_base-view-mobile-dark-mode-1.png index f19ba84553..9ca2b3c467 100644 Binary files a/ui/address/contract/__screenshots__/ContractRead.pw.tsx_dark-color-mode_base-view-mobile-dark-mode-1.png and b/ui/address/contract/__screenshots__/ContractRead.pw.tsx_dark-color-mode_base-view-mobile-dark-mode-1.png differ diff --git a/ui/address/contract/__screenshots__/ContractRead.pw.tsx_dark-color-mode_base-view-mobile-dark-mode-2.png b/ui/address/contract/__screenshots__/ContractRead.pw.tsx_dark-color-mode_base-view-mobile-dark-mode-2.png index 9402cd11fc..3fb74dce71 100644 Binary files a/ui/address/contract/__screenshots__/ContractRead.pw.tsx_dark-color-mode_base-view-mobile-dark-mode-2.png and b/ui/address/contract/__screenshots__/ContractRead.pw.tsx_dark-color-mode_base-view-mobile-dark-mode-2.png differ diff --git a/ui/address/contract/__screenshots__/ContractRead.pw.tsx_default_base-view-mobile-dark-mode-1.png b/ui/address/contract/__screenshots__/ContractRead.pw.tsx_default_base-view-mobile-dark-mode-1.png index 82900c2f57..fe2ffcfcba 100644 Binary files a/ui/address/contract/__screenshots__/ContractRead.pw.tsx_default_base-view-mobile-dark-mode-1.png and b/ui/address/contract/__screenshots__/ContractRead.pw.tsx_default_base-view-mobile-dark-mode-1.png differ diff --git a/ui/address/contract/__screenshots__/ContractRead.pw.tsx_default_base-view-mobile-dark-mode-2.png b/ui/address/contract/__screenshots__/ContractRead.pw.tsx_default_base-view-mobile-dark-mode-2.png index 635406e6f5..171aa0aad1 100644 Binary files a/ui/address/contract/__screenshots__/ContractRead.pw.tsx_default_base-view-mobile-dark-mode-2.png and b/ui/address/contract/__screenshots__/ContractRead.pw.tsx_default_base-view-mobile-dark-mode-2.png differ diff --git a/ui/address/contract/__screenshots__/ContractRead.pw.tsx_mobile_base-view-mobile-dark-mode-1.png b/ui/address/contract/__screenshots__/ContractRead.pw.tsx_mobile_base-view-mobile-dark-mode-1.png index 930ca02a9e..3d31997d9d 100644 Binary files a/ui/address/contract/__screenshots__/ContractRead.pw.tsx_mobile_base-view-mobile-dark-mode-1.png and b/ui/address/contract/__screenshots__/ContractRead.pw.tsx_mobile_base-view-mobile-dark-mode-1.png differ diff --git a/ui/address/contract/__screenshots__/ContractRead.pw.tsx_mobile_base-view-mobile-dark-mode-2.png b/ui/address/contract/__screenshots__/ContractRead.pw.tsx_mobile_base-view-mobile-dark-mode-2.png index b5d534a0d0..77c8176a15 100644 Binary files a/ui/address/contract/__screenshots__/ContractRead.pw.tsx_mobile_base-view-mobile-dark-mode-2.png and b/ui/address/contract/__screenshots__/ContractRead.pw.tsx_mobile_base-view-mobile-dark-mode-2.png differ diff --git a/ui/address/contract/__screenshots__/ContractReadResult.pw.tsx_default_complex-error-1.png b/ui/address/contract/__screenshots__/ContractReadResult.pw.tsx_default_complex-error-1.png deleted file mode 100644 index 80741a58e9..0000000000 Binary files a/ui/address/contract/__screenshots__/ContractReadResult.pw.tsx_default_complex-error-1.png and /dev/null differ diff --git a/ui/address/contract/__screenshots__/ContractReadResult.pw.tsx_default_complex-success-1.png b/ui/address/contract/__screenshots__/ContractReadResult.pw.tsx_default_complex-success-1.png deleted file mode 100644 index 695b1df2c6..0000000000 Binary files a/ui/address/contract/__screenshots__/ContractReadResult.pw.tsx_default_complex-success-1.png and /dev/null differ diff --git a/ui/address/contract/__screenshots__/ContractReadResult.pw.tsx_default_default-error-1.png b/ui/address/contract/__screenshots__/ContractReadResult.pw.tsx_default_default-error-1.png deleted file mode 100644 index 31c2a142c1..0000000000 Binary files a/ui/address/contract/__screenshots__/ContractReadResult.pw.tsx_default_default-error-1.png and /dev/null differ diff --git a/ui/address/contract/__screenshots__/ContractReadResult.pw.tsx_default_error-with-code-1.png b/ui/address/contract/__screenshots__/ContractReadResult.pw.tsx_default_error-with-code-1.png deleted file mode 100644 index 680fbde860..0000000000 Binary files a/ui/address/contract/__screenshots__/ContractReadResult.pw.tsx_default_error-with-code-1.png and /dev/null differ diff --git a/ui/address/contract/__screenshots__/ContractReadResult.pw.tsx_default_raw-error-1.png b/ui/address/contract/__screenshots__/ContractReadResult.pw.tsx_default_raw-error-1.png deleted file mode 100644 index dfbf8524ab..0000000000 Binary files a/ui/address/contract/__screenshots__/ContractReadResult.pw.tsx_default_raw-error-1.png and /dev/null differ diff --git a/ui/address/contract/__screenshots__/ContractReadResult.pw.tsx_default_success-1.png b/ui/address/contract/__screenshots__/ContractReadResult.pw.tsx_default_success-1.png deleted file mode 100644 index 0010723b55..0000000000 Binary files a/ui/address/contract/__screenshots__/ContractReadResult.pw.tsx_default_success-1.png and /dev/null differ diff --git a/ui/address/contract/__screenshots__/ContractWrite.pw.tsx_default_base-view-mobile-1.png b/ui/address/contract/__screenshots__/ContractWrite.pw.tsx_default_base-view-mobile-1.png index e58e9c4755..19741ab173 100644 Binary files a/ui/address/contract/__screenshots__/ContractWrite.pw.tsx_default_base-view-mobile-1.png and b/ui/address/contract/__screenshots__/ContractWrite.pw.tsx_default_base-view-mobile-1.png differ diff --git a/ui/address/contract/__screenshots__/ContractWrite.pw.tsx_mobile_base-view-mobile-1.png b/ui/address/contract/__screenshots__/ContractWrite.pw.tsx_mobile_base-view-mobile-1.png index c9a99a0bfb..20cf83a691 100644 Binary files a/ui/address/contract/__screenshots__/ContractWrite.pw.tsx_mobile_base-view-mobile-1.png and b/ui/address/contract/__screenshots__/ContractWrite.pw.tsx_mobile_base-view-mobile-1.png differ diff --git a/ui/address/contract/__screenshots__/ContractWriteResultDumb.pw.tsx_default_error-in-result-1.png b/ui/address/contract/__screenshots__/ContractWriteResultDumb.pw.tsx_default_error-in-result-1.png deleted file mode 100644 index 30a20b5510..0000000000 Binary files a/ui/address/contract/__screenshots__/ContractWriteResultDumb.pw.tsx_default_error-in-result-1.png and /dev/null differ diff --git a/ui/address/contract/__screenshots__/ContractWriteResultDumb.pw.tsx_default_error-mobile-1.png b/ui/address/contract/__screenshots__/ContractWriteResultDumb.pw.tsx_default_error-mobile-1.png deleted file mode 100644 index e9ec6c1578..0000000000 Binary files a/ui/address/contract/__screenshots__/ContractWriteResultDumb.pw.tsx_default_error-mobile-1.png and /dev/null differ diff --git a/ui/address/contract/__screenshots__/ContractWriteResultDumb.pw.tsx_default_loading-1.png b/ui/address/contract/__screenshots__/ContractWriteResultDumb.pw.tsx_default_loading-1.png deleted file mode 100644 index 986269100c..0000000000 Binary files a/ui/address/contract/__screenshots__/ContractWriteResultDumb.pw.tsx_default_loading-1.png and /dev/null differ diff --git a/ui/address/contract/__screenshots__/ContractWriteResultDumb.pw.tsx_default_success-1.png b/ui/address/contract/__screenshots__/ContractWriteResultDumb.pw.tsx_default_success-1.png deleted file mode 100644 index 49697a5f42..0000000000 Binary files a/ui/address/contract/__screenshots__/ContractWriteResultDumb.pw.tsx_default_success-1.png and /dev/null differ diff --git a/ui/address/contract/methodForm/ContractMethodForm.tsx b/ui/address/contract/methodForm/ContractMethodForm.tsx deleted file mode 100644 index 7523e6782d..0000000000 --- a/ui/address/contract/methodForm/ContractMethodForm.tsx +++ /dev/null @@ -1,146 +0,0 @@ -import { Box, Button, Flex, chakra } from '@chakra-ui/react'; -import _mapValues from 'lodash/mapValues'; -import React from 'react'; -import type { SubmitHandler } from 'react-hook-form'; -import { useForm, FormProvider } from 'react-hook-form'; - -import type { ContractMethodCallResult } from '../types'; -import type { ResultComponentProps } from './types'; -import type { SmartContractMethod, SmartContractMethodInput } from 'types/api/contract'; - -import config from 'configs/app'; -import * as mixpanel from 'lib/mixpanel/index'; - -import ContractMethodFieldAccordion from './ContractMethodFieldAccordion'; -import ContractMethodFieldInput from './ContractMethodFieldInput'; -import ContractMethodFieldInputArray from './ContractMethodFieldInputArray'; -import ContractMethodFieldInputTuple from './ContractMethodFieldInputTuple'; -import ContractMethodFormOutputs from './ContractMethodFormOutputs'; -import { getFieldLabel, matchArray, transformFormDataToMethodArgs } from './utils'; -import type { ContractMethodFormFields } from './utils'; - -interface Props { - data: T; - onSubmit: (data: T, args: Array) => Promise>; - resultComponent: (props: ResultComponentProps) => JSX.Element | null; - methodType: 'read' | 'write'; -} - -const ContractMethodForm = ({ data, onSubmit, resultComponent: ResultComponent, methodType }: Props) => { - - const [ result, setResult ] = React.useState>(); - const [ isLoading, setLoading ] = React.useState(false); - - const formApi = useForm({ - mode: 'all', - shouldUnregister: true, - }); - - const onFormSubmit: SubmitHandler = React.useCallback(async(formData) => { - // The API used for reading from contracts expects all values to be strings. - const formattedData = methodType === 'read' ? - _mapValues(formData, (value) => value !== undefined ? String(value) : undefined) : - formData; - const args = transformFormDataToMethodArgs(formattedData); - - setResult(undefined); - setLoading(true); - - onSubmit(data, args) - .then((result) => { - setResult(result); - }) - .catch((error) => { - setResult(error?.error || error?.data || (error?.reason && { message: error.reason }) || error); - setLoading(false); - }) - .finally(() => { - mixpanel.logEvent(mixpanel.EventTypes.CONTRACT_INTERACTION, { - 'Method type': methodType === 'write' ? 'Write' : 'Read', - 'Method name': 'name' in data ? data.name : 'Fallback', - }); - }); - }, [ data, methodType, onSubmit ]); - - const handleTxSettle = React.useCallback(() => { - setLoading(false); - }, []); - - const handleFormChange = React.useCallback(() => { - result && setResult(undefined); - }, [ result ]); - - const inputs: Array = React.useMemo(() => { - return [ - ...('inputs' in data ? data.inputs : []), - ...('stateMutability' in data && data.stateMutability === 'payable' ? [ { - name: `Send native ${ config.chain.currency.symbol || 'coin' }`, - type: 'uint256' as const, - internalType: 'uint256' as const, - fieldType: 'native_coin' as const, - } ] : []), - ]; - }, [ data ]); - - const outputs = 'outputs' in data && data.outputs ? data.outputs : []; - - return ( - - - - - { inputs.map((input, index) => { - if (input.components && input.type === 'tuple') { - return ; - } - - const arrayMatch = matchArray(input.type); - - if (arrayMatch) { - if (arrayMatch.isNested) { - const fieldsWithErrors = Object.keys(formApi.formState.errors); - const isInvalid = fieldsWithErrors.some((field) => field.startsWith(index + ':')); - - return ( - - - - ); - } - - return ; - } - - return ; - }) } - - - - - { methodType === 'read' && } - { result && } - - ); -}; - -export default React.memo(ContractMethodForm) as typeof ContractMethodForm; diff --git a/ui/address/contract/methodForm/types.ts b/ui/address/contract/methodForm/types.ts deleted file mode 100644 index 845d6d3621..0000000000 --- a/ui/address/contract/methodForm/types.ts +++ /dev/null @@ -1,8 +0,0 @@ -import type { ContractMethodCallResult } from '../types'; -import type { SmartContractMethod } from 'types/api/contract'; - -export interface ResultComponentProps { - item: T; - result: ContractMethodCallResult; - onSettle: () => void; -} diff --git a/ui/address/contract/types.ts b/ui/address/contract/types.ts deleted file mode 100644 index 26753dcbf1..0000000000 --- a/ui/address/contract/types.ts +++ /dev/null @@ -1,15 +0,0 @@ -import type { SmartContractQueryMethodRead, SmartContractMethod, SmartContractReadMethod } from 'types/api/contract'; - -import type { ResourceError } from 'lib/api/resources'; - -export type MethodFormFields = Record>; -export type MethodFormFieldsFormatted = Record; - -export type MethodArgType = string | boolean | Array; - -export type ContractMethodReadResult = SmartContractQueryMethodRead | ResourceError; - -export type ContractMethodWriteResult = Error | { hash: `0x${ string }` | undefined } | undefined; - -export type ContractMethodCallResult = - T extends SmartContractReadMethod ? ContractMethodReadResult : ContractMethodWriteResult; diff --git a/ui/address/contract/useContractAbi.tsx b/ui/address/contract/useContractAbi.tsx deleted file mode 100644 index e9d9ff8e16..0000000000 --- a/ui/address/contract/useContractAbi.tsx +++ /dev/null @@ -1,58 +0,0 @@ -import { useQueryClient } from '@tanstack/react-query'; -import type { Abi } from 'abitype'; -import React from 'react'; - -import type { Address } from 'types/api/address'; - -import useApiQuery, { getResourceKey } from 'lib/api/useApiQuery'; - -interface Params { - addressHash?: string; - isProxy?: boolean; - isCustomAbi?: boolean; -} - -export default function useContractAbi({ addressHash, isProxy, isCustomAbi }: Params): Abi | undefined { - const queryClient = useQueryClient(); - - const { data: contractInfo } = useApiQuery('contract', { - pathParams: { hash: addressHash }, - queryOptions: { - enabled: Boolean(addressHash), - refetchOnMount: false, - }, - }); - - const addressInfo = queryClient.getQueryData
(getResourceKey('address', { - pathParams: { hash: addressHash }, - })); - - const { data: proxyInfo } = useApiQuery('contract', { - pathParams: { hash: addressInfo?.implementation_address || '' }, - queryOptions: { - enabled: Boolean(addressInfo?.implementation_address), - refetchOnMount: false, - }, - }); - - const { data: customInfo } = useApiQuery('contract_methods_write', { - pathParams: { hash: addressHash }, - queryParams: { is_custom_abi: 'true' }, - queryOptions: { - enabled: Boolean(contractInfo?.has_custom_methods_write), - refetchOnMount: false, - }, - }); - - return React.useMemo(() => { - if (isProxy) { - return proxyInfo?.abi ?? undefined; - } - - if (isCustomAbi) { - return customInfo as Abi; - } - - return contractInfo?.abi ?? undefined; - }, [ contractInfo?.abi, customInfo, isCustomAbi, isProxy, proxyInfo?.abi ]); -} diff --git a/ui/address/contract/useWatchAccount.tsx b/ui/address/contract/useWatchAccount.tsx deleted file mode 100644 index ac19b2f55e..0000000000 --- a/ui/address/contract/useWatchAccount.tsx +++ /dev/null @@ -1,31 +0,0 @@ -import { watchAccount, getAccount } from '@wagmi/core'; -import React from 'react'; -import type { Config } from 'wagmi'; -import { useConfig } from 'wagmi'; - -export function getWalletAccount(config: Config) { - try { - return getAccount(config); - } catch (error) { - return null; - } -} - -export default function useWatchAccount() { - const config = useConfig(); - const [ account, setAccount ] = React.useState(getWalletAccount(config)); - - React.useEffect(() => { - if (!account) { - return; - } - - return watchAccount(config, { - onChange(account) { - setAccount(account); - }, - }); - }, [ account, config ]); - - return account; -} diff --git a/ui/address/contract/utils.test.ts b/ui/address/contract/utils.test.ts deleted file mode 100644 index 47e12d3b67..0000000000 --- a/ui/address/contract/utils.test.ts +++ /dev/null @@ -1,101 +0,0 @@ -import { prepareAbi } from './utils'; - -describe('function prepareAbi()', () => { - const commonAbi = [ - { - inputs: [ - { internalType: 'address', name: '_pool', type: 'address' }, - { internalType: 'address', name: '_token', type: 'address' }, - { internalType: 'uint256', name: '_denominator', type: 'uint256' }, - ], - stateMutability: 'nonpayable' as const, - type: 'constructor' as const, - }, - { - anonymous: false, - inputs: [ - { indexed: false, internalType: 'uint256[]', name: 'indices', type: 'uint256[]' }, - ], - name: 'CompleteDirectDepositBatch', - type: 'event' as const, - }, - { - inputs: [ - { internalType: 'address', name: '_fallbackUser', type: 'address' }, - { internalType: 'string', name: '_zkAddress', type: 'string' }, - ], - name: 'directNativeDeposit', - outputs: [ - { internalType: 'uint256', name: '', type: 'uint256' }, - ], - stateMutability: 'payable' as const, - type: 'function' as const, - }, - ]; - - const method = { - inputs: [ - { internalType: 'address' as const, name: '_fallbackUser', type: 'address' as const }, - { internalType: 'string' as const, name: '_zkAddress', type: 'string' as const }, - ], - name: 'directNativeDeposit', - outputs: [ - { internalType: 'uint256' as const, name: '', type: 'uint256' as const }, - ], - stateMutability: 'payable' as const, - type: 'function' as const, - constant: false, - payable: true, - method_id: '0x2e0e2d3e', - }; - - it('if there is only one method with provided name, does nothing', () => { - const abi = prepareAbi(commonAbi, method); - expect(abi).toHaveLength(commonAbi.length); - }); - - it('if there are two or more methods with the same name and inputs length, filters out those which input types are not matched', () => { - const abi = prepareAbi([ - ...commonAbi, - { - inputs: [ - { internalType: 'address', name: '_fallbackUser', type: 'address' }, - { internalType: 'bytes', name: '_rawZkAddress', type: 'bytes' }, - ], - name: 'directNativeDeposit', - outputs: [ - { internalType: 'uint256', name: '', type: 'uint256' }, - ], - stateMutability: 'payable', - type: 'function', - }, - ], method); - - expect(abi).toHaveLength(commonAbi.length); - - const item = abi.find((item) => 'name' in item ? item.name === method.name : false); - expect(item).toEqual(commonAbi[2]); - }); - - it('if there are two or more methods with the same name and different inputs length, filters out those which inputs are not matched', () => { - const abi = prepareAbi([ - ...commonAbi, - { - inputs: [ - { internalType: 'address', name: '_fallbackUser', type: 'address' }, - ], - name: 'directNativeDeposit', - outputs: [ - { internalType: 'uint256', name: '', type: 'uint256' }, - ], - stateMutability: 'payable', - type: 'function', - }, - ], method); - - expect(abi).toHaveLength(commonAbi.length); - - const item = abi.find((item) => 'name' in item ? item.name === method.name : false); - expect(item).toEqual(commonAbi[2]); - }); -}); diff --git a/ui/address/contract/utils.ts b/ui/address/contract/utils.ts deleted file mode 100644 index 8fa04e839c..0000000000 --- a/ui/address/contract/utils.ts +++ /dev/null @@ -1,40 +0,0 @@ -import type { Abi } from 'abitype'; - -import type { SmartContractWriteMethod } from 'types/api/contract'; - -export const getNativeCoinValue = (value: unknown) => { - if (typeof value !== 'string') { - return BigInt(0); - } - - return BigInt(value); -}; - -export function prepareAbi(abi: Abi, item: SmartContractWriteMethod): Abi { - if ('name' in item) { - const hasMethodsWithSameName = abi.filter((abiItem) => 'name' in abiItem ? abiItem.name === item.name : false).length > 1; - - if (hasMethodsWithSameName) { - return abi.filter((abiItem) => { - if (!('name' in abiItem)) { - return true; - } - - if (abiItem.name !== item.name) { - return true; - } - - if (abiItem.inputs.length !== item.inputs.length) { - return false; - } - - return abiItem.inputs.every(({ name, type }) => { - const itemInput = item.inputs.find((input) => input.name === name); - return Boolean(itemInput) && itemInput?.type === type; - }); - }); - } - } - - return abi; -} diff --git a/ui/shared/CopyToClipboard.tsx b/ui/shared/CopyToClipboard.tsx index 000678cd90..c3b67545de 100644 --- a/ui/shared/CopyToClipboard.tsx +++ b/ui/shared/CopyToClipboard.tsx @@ -7,9 +7,10 @@ export interface Props { text: string; className?: string; isLoading?: boolean; + onClick?: (event: React.MouseEvent) => void; } -const CopyToClipboard = ({ text, className, isLoading }: Props) => { +const CopyToClipboard = ({ text, className, isLoading, onClick }: Props) => { const { hasCopied, onCopy } = useClipboard(text, 1000); const [ copied, setCopied ] = useState(false); // have to implement controlled tooltip because of the issue - https://github.com/chakra-ui/chakra-ui/issues/7107 @@ -24,6 +25,11 @@ const CopyToClipboard = ({ text, className, isLoading }: Props) => { } }, [ hasCopied ]); + const handleClick = React.useCallback((event: React.MouseEvent) => { + onCopy(); + onClick?.(event); + }, [ onClick, onCopy ]); + if (isLoading) { return ; } @@ -39,7 +45,7 @@ const CopyToClipboard = ({ text, className, isLoading }: Props) => { variant="simple" display="inline-block" flexShrink={ 0 } - onClick={ onCopy } + onClick={ handleClick } className={ className } onMouseEnter={ onOpen } onMouseLeave={ onClose } diff --git a/ui/shared/Tabs/TabsWithScroll.tsx b/ui/shared/Tabs/TabsWithScroll.tsx index 967b0f9740..efbde5723d 100644 --- a/ui/shared/Tabs/TabsWithScroll.tsx +++ b/ui/shared/Tabs/TabsWithScroll.tsx @@ -52,8 +52,11 @@ const TabsWithScroll = ({ }, [ tabs ]); const handleTabChange = React.useCallback((index: number) => { + if (isLoading) { + return; + } onTabChange ? onTabChange(index) : setActiveTabIndex(index); - }, [ onTabChange ]); + }, [ isLoading, onTabChange ]); useEffect(() => { if (defaultTabIndex !== undefined) { diff --git a/ui/shared/ad/GetitBanner.tsx b/ui/shared/ad/GetitBanner.tsx index 140a720b3b..3f4537e376 100644 --- a/ui/shared/ad/GetitBanner.tsx +++ b/ui/shared/ad/GetitBanner.tsx @@ -1,18 +1,17 @@ import { Flex, chakra } from '@chakra-ui/react'; import dynamic from 'next/dynamic'; import React from 'react'; -import { useAccount } from 'wagmi'; import useIsMobile from 'lib/hooks/useIsMobile'; - -import Web3ModalProvider from '../Web3ModalProvider'; +import useAccount from 'lib/web3/useAccount'; const GetitAdPlugin = dynamic(() => import('getit-sdk').then(module => module.GetitAdPlugin), { ssr: false }); const GETIT_API_KEY = 'ZmGXVvwYUAW4yXL8RzWQHNKmpSyQmt3TDXsXUxqFqXPdoaiSSFyca3BOyunDcWdyOwTkX3UVVQel28qbjoOoWPxYVpPdNzbUNkAHyFyJX7Lk9TVcPDZKTQmwHlSMzO3a'; -const GetitBannerContent = ({ address, className }: { address?: string; className?: string }) => { +const GetitBanner = ({ className }: { className?: string }) => { const isMobile = Boolean(useIsMobile()); + const { address } = useAccount(); return ( @@ -27,22 +26,4 @@ const GetitBannerContent = ({ address, className }: { address?: string; classNam ); }; -const GetitBannerWithWalletAddress = ({ className }: { className?: string }) => { - const { address } = useAccount(); - - return ; -}; - -const GetitBanner = ({ className }: { className?: string }) => { - const fallback = React.useCallback(() => { - return ; - }, [ className ]); - - return ( - - - - ); -}; - export default chakra(GetitBanner); diff --git a/ui/shared/ad/HypeBanner.tsx b/ui/shared/ad/HypeBanner.tsx index 7934ddc867..adcdeae993 100644 --- a/ui/shared/ad/HypeBanner.tsx +++ b/ui/shared/ad/HypeBanner.tsx @@ -2,15 +2,22 @@ import { Flex, chakra } from '@chakra-ui/react'; import { Banner, setWalletAddresses } from '@hypelab/sdk-react'; import Script from 'next/script'; import React from 'react'; -import { useAccount } from 'wagmi'; -import Web3ModalProvider from '../Web3ModalProvider'; +import useAccount from 'lib/web3/useAccount'; + import { hypeInit } from './hypeBannerScript'; const DESKTOP_BANNER_SLUG = 'b1559fc3e7'; const MOBILE_BANNER_SLUG = '668ed80a9e'; -const HypeBannerContent = ({ className }: { className?: string }) => { +const HypeBanner = ({ className }: { className?: string }) => { + const { address } = useAccount(); + + React.useEffect(() => { + if (address) { + setWalletAddresses([ address ]); + } + }, [ address ]); return ( <> @@ -28,28 +35,4 @@ const HypeBannerContent = ({ className }: { className?: string }) => { ); }; -const HypeBannerWithWalletAddress = ({ className }: { className?: string }) => { - const { address } = useAccount(); - React.useEffect(() => { - if (address) { - setWalletAddresses([ address ]); - } - }, [ address ]); - - return ; -}; - -const HypeBanner = ({ className }: { className?: string }) => { - - const fallback = React.useCallback(() => { - return ; - }, [ className ]); - - return ( - - - - ); -}; - export default chakra(HypeBanner); diff --git a/ui/shared/logs/LogDecodedInputDataHeader.tsx b/ui/shared/logs/LogDecodedInputDataHeader.tsx index 653382b18b..ec1680cce9 100644 --- a/ui/shared/logs/LogDecodedInputDataHeader.tsx +++ b/ui/shared/logs/LogDecodedInputDataHeader.tsx @@ -1,13 +1,15 @@ import { Divider, Flex, Skeleton, VStack } from '@chakra-ui/react'; import React from 'react'; +import Tag from 'ui/shared/chakra/Tag'; + interface Props { methodId: string; methodCall: string; isLoading?: boolean; } -const Item = ({ label, text, isLoading }: { label: string; text: string; isLoading?: boolean}) => { +const Item = ({ label, children, isLoading }: { label: string; children: React.ReactNode; isLoading?: boolean}) => { return ( { label } - { text } + { children } ); }; @@ -32,8 +34,12 @@ const LogDecodedInputDataHeader = ({ methodId, methodCall, isLoading }: Props) = fontSize="sm" lineHeight={ 5 } > - - + + { methodId } + + + { methodCall } + ); }; diff --git a/ui/shared/logs/__screenshots__/LogDecodedInputData.pw.tsx_dark-color-mode_with-indexed-fields-mobile-dark-mode-1.png b/ui/shared/logs/__screenshots__/LogDecodedInputData.pw.tsx_dark-color-mode_with-indexed-fields-mobile-dark-mode-1.png index 98428228b9..5dff66795a 100644 Binary files a/ui/shared/logs/__screenshots__/LogDecodedInputData.pw.tsx_dark-color-mode_with-indexed-fields-mobile-dark-mode-1.png and b/ui/shared/logs/__screenshots__/LogDecodedInputData.pw.tsx_dark-color-mode_with-indexed-fields-mobile-dark-mode-1.png differ diff --git a/ui/shared/logs/__screenshots__/LogDecodedInputData.pw.tsx_default_with-indexed-fields-mobile-dark-mode-1.png b/ui/shared/logs/__screenshots__/LogDecodedInputData.pw.tsx_default_with-indexed-fields-mobile-dark-mode-1.png index 0e81521fd8..3c02866fc0 100644 Binary files a/ui/shared/logs/__screenshots__/LogDecodedInputData.pw.tsx_default_with-indexed-fields-mobile-dark-mode-1.png and b/ui/shared/logs/__screenshots__/LogDecodedInputData.pw.tsx_default_with-indexed-fields-mobile-dark-mode-1.png differ diff --git a/ui/shared/logs/__screenshots__/LogDecodedInputData.pw.tsx_default_without-indexed-fields-mobile-1.png b/ui/shared/logs/__screenshots__/LogDecodedInputData.pw.tsx_default_without-indexed-fields-mobile-1.png index 5b5efd2b32..5b743f9c47 100644 Binary files a/ui/shared/logs/__screenshots__/LogDecodedInputData.pw.tsx_default_without-indexed-fields-mobile-1.png and b/ui/shared/logs/__screenshots__/LogDecodedInputData.pw.tsx_default_without-indexed-fields-mobile-1.png differ diff --git a/ui/shared/logs/__screenshots__/LogDecodedInputData.pw.tsx_mobile_with-indexed-fields-mobile-dark-mode-1.png b/ui/shared/logs/__screenshots__/LogDecodedInputData.pw.tsx_mobile_with-indexed-fields-mobile-dark-mode-1.png index 173ef0fc4a..a406c70799 100644 Binary files a/ui/shared/logs/__screenshots__/LogDecodedInputData.pw.tsx_mobile_with-indexed-fields-mobile-dark-mode-1.png and b/ui/shared/logs/__screenshots__/LogDecodedInputData.pw.tsx_mobile_with-indexed-fields-mobile-dark-mode-1.png differ diff --git a/ui/shared/logs/__screenshots__/LogDecodedInputData.pw.tsx_mobile_without-indexed-fields-mobile-1.png b/ui/shared/logs/__screenshots__/LogDecodedInputData.pw.tsx_mobile_without-indexed-fields-mobile-1.png index 7bde892cd2..6f9b592c45 100644 Binary files a/ui/shared/logs/__screenshots__/LogDecodedInputData.pw.tsx_mobile_without-indexed-fields-mobile-1.png and b/ui/shared/logs/__screenshots__/LogDecodedInputData.pw.tsx_mobile_without-indexed-fields-mobile-1.png differ diff --git a/ui/shared/logs/__screenshots__/LogItem.pw.tsx_dark-color-mode_with-decoded-input-data-mobile-dark-mode-1.png b/ui/shared/logs/__screenshots__/LogItem.pw.tsx_dark-color-mode_with-decoded-input-data-mobile-dark-mode-1.png index 3902449e9a..29e17c619e 100644 Binary files a/ui/shared/logs/__screenshots__/LogItem.pw.tsx_dark-color-mode_with-decoded-input-data-mobile-dark-mode-1.png and b/ui/shared/logs/__screenshots__/LogItem.pw.tsx_dark-color-mode_with-decoded-input-data-mobile-dark-mode-1.png differ diff --git a/ui/shared/logs/__screenshots__/LogItem.pw.tsx_default_with-decoded-input-data-mobile-dark-mode-1.png b/ui/shared/logs/__screenshots__/LogItem.pw.tsx_default_with-decoded-input-data-mobile-dark-mode-1.png index 3063b71f5d..0e1e563737 100644 Binary files a/ui/shared/logs/__screenshots__/LogItem.pw.tsx_default_with-decoded-input-data-mobile-dark-mode-1.png and b/ui/shared/logs/__screenshots__/LogItem.pw.tsx_default_with-decoded-input-data-mobile-dark-mode-1.png differ diff --git a/ui/shared/logs/__screenshots__/LogItem.pw.tsx_mobile_with-decoded-input-data-mobile-dark-mode-1.png b/ui/shared/logs/__screenshots__/LogItem.pw.tsx_mobile_with-decoded-input-data-mobile-dark-mode-1.png index 3834fa7b4f..7ae98dec50 100644 Binary files a/ui/shared/logs/__screenshots__/LogItem.pw.tsx_mobile_with-decoded-input-data-mobile-dark-mode-1.png and b/ui/shared/logs/__screenshots__/LogItem.pw.tsx_mobile_with-decoded-input-data-mobile-dark-mode-1.png differ diff --git a/ui/tx/details/__screenshots__/TxInfo.pw.tsx_dark-color-mode_between-addresses-mobile-dark-mode-1.png b/ui/tx/details/__screenshots__/TxInfo.pw.tsx_dark-color-mode_between-addresses-mobile-dark-mode-1.png index e62d5116ce..b5e623d053 100644 Binary files a/ui/tx/details/__screenshots__/TxInfo.pw.tsx_dark-color-mode_between-addresses-mobile-dark-mode-1.png and b/ui/tx/details/__screenshots__/TxInfo.pw.tsx_dark-color-mode_between-addresses-mobile-dark-mode-1.png differ diff --git a/ui/tx/details/__screenshots__/TxInfo.pw.tsx_default_between-addresses-mobile-dark-mode-1.png b/ui/tx/details/__screenshots__/TxInfo.pw.tsx_default_between-addresses-mobile-dark-mode-1.png index 9dcf3e54e0..5e9f01bc8b 100644 Binary files a/ui/tx/details/__screenshots__/TxInfo.pw.tsx_default_between-addresses-mobile-dark-mode-1.png and b/ui/tx/details/__screenshots__/TxInfo.pw.tsx_default_between-addresses-mobile-dark-mode-1.png differ diff --git a/ui/tx/details/__screenshots__/TxInfo.pw.tsx_default_with-blob-1.png b/ui/tx/details/__screenshots__/TxInfo.pw.tsx_default_with-blob-1.png index 1ce20160f1..a2f39211a5 100644 Binary files a/ui/tx/details/__screenshots__/TxInfo.pw.tsx_default_with-blob-1.png and b/ui/tx/details/__screenshots__/TxInfo.pw.tsx_default_with-blob-1.png differ diff --git a/ui/tx/details/__screenshots__/TxInfo.pw.tsx_default_with-decoded-revert-reason-1.png b/ui/tx/details/__screenshots__/TxInfo.pw.tsx_default_with-decoded-revert-reason-1.png index cf66bc0911..c4bd37fd61 100644 Binary files a/ui/tx/details/__screenshots__/TxInfo.pw.tsx_default_with-decoded-revert-reason-1.png and b/ui/tx/details/__screenshots__/TxInfo.pw.tsx_default_with-decoded-revert-reason-1.png differ diff --git a/ui/tx/details/__screenshots__/TxInfo.pw.tsx_mobile_between-addresses-mobile-dark-mode-1.png b/ui/tx/details/__screenshots__/TxInfo.pw.tsx_mobile_between-addresses-mobile-dark-mode-1.png index dfcad5c049..5eff7e6a3e 100644 Binary files a/ui/tx/details/__screenshots__/TxInfo.pw.tsx_mobile_between-addresses-mobile-dark-mode-1.png and b/ui/tx/details/__screenshots__/TxInfo.pw.tsx_mobile_between-addresses-mobile-dark-mode-1.png differ