Commit 8b429238 authored by tom goriunov's avatar tom goriunov Committed by GitHub

Contract interaction improvements (#1875)

* add contract method id tag

* add form submit type

* refactor types for SmartConstract and make new component for Contract ABI methods

* move contract method form inside ABI folder

* handle form submit and display result

* clean-up

* change copied text

* handle case when blockchain interaction is not configured

* tests and screenshots update

* fix bug wit WEI checkbox
parent 825a3e7f
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;
import type { import type {
SmartContractQueryMethodReadError, SmartContractQueryMethodError,
SmartContractQueryMethodReadSuccess, SmartContractQueryMethodSuccess,
SmartContractReadMethod, SmartContractReadMethod,
SmartContractWriteMethod, SmartContractWriteMethod,
} from 'types/api/contract'; } from 'types/api/contract';
...@@ -94,7 +94,7 @@ export const read: Array<SmartContractReadMethod> = [ ...@@ -94,7 +94,7 @@ export const read: Array<SmartContractReadMethod> = [
}, },
]; ];
export const readResultSuccess: SmartContractQueryMethodReadSuccess = { export const readResultSuccess: SmartContractQueryMethodSuccess = {
is_error: false, is_error: false,
result: { result: {
names: [ 'amount' ], names: [ 'amount' ],
...@@ -104,7 +104,7 @@ export const readResultSuccess: SmartContractQueryMethodReadSuccess = { ...@@ -104,7 +104,7 @@ export const readResultSuccess: SmartContractQueryMethodReadSuccess = {
}, },
}; };
export const readResultError: SmartContractQueryMethodReadError = { export const readResultError: SmartContractQueryMethodError = {
is_error: true, is_error: true,
result: { result: {
message: 'Some shit happened', message: 'Some shit happened',
......
...@@ -18,6 +18,7 @@ import theme from 'theme'; ...@@ -18,6 +18,7 @@ import theme from 'theme';
export type Props = { export type Props = {
children: React.ReactNode; children: React.ReactNode;
withSocket?: boolean; withSocket?: boolean;
withWalletClient?: boolean;
appContext?: { appContext?: {
pageProps: PageProps; pageProps: PageProps;
}; };
...@@ -47,7 +48,20 @@ const wagmiConfig = createConfig({ ...@@ -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 (
<WagmiProvider config={ wagmiConfig }>
{ children }
</WagmiProvider>
);
}
// 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({ const [ queryClient ] = React.useState(() => new QueryClient({
defaultOptions: { defaultOptions: {
queries: { queries: {
...@@ -63,9 +77,9 @@ const TestApp = ({ children, withSocket, appContext = defaultAppContext }: Props ...@@ -63,9 +77,9 @@ const TestApp = ({ children, withSocket, appContext = defaultAppContext }: Props
<SocketProvider url={ withSocket ? `ws://${ config.app.host }:${ app.socketPort }` : undefined }> <SocketProvider url={ withSocket ? `ws://${ config.app.host }:${ app.socketPort }` : undefined }>
<AppContextProvider { ...appContext }> <AppContextProvider { ...appContext }>
<GrowthBookProvider> <GrowthBookProvider>
<WagmiProvider config={ wagmiConfig }> <WalletClientProvider withWalletClient={ withWalletClient }>
{ children } { children }
</WagmiProvider> </WalletClientProvider>
</GrowthBookProvider> </GrowthBookProvider>
</AppContextProvider> </AppContextProvider>
</SocketProvider> </SocketProvider>
......
...@@ -37,4 +37,7 @@ export const ENVS_MAP: Record<string, Array<[string, string]>> = { ...@@ -37,4 +37,7 @@ export const ENVS_MAP: Record<string, Array<[string, string]>> = {
blockHiddenFields: [ blockHiddenFields: [
[ 'NEXT_PUBLIC_VIEWS_BLOCK_HIDDEN_FIELDS', '["burnt_fees", "total_reward", "nonce"]' ], [ 'NEXT_PUBLIC_VIEWS_BLOCK_HIDDEN_FIELDS', '["burnt_fees", "total_reward", "nonce"]' ],
], ],
noWalletClient: [
[ 'NEXT_PUBLIC_WALLET_CONNECT_PROJECT_ID', '' ],
],
}; };
import type { Abi, AbiType } from 'abitype'; import type { Abi, AbiType, AbiFallback, AbiFunction, AbiReceive } from 'abitype';
export type SmartContractMethodArgType = AbiType; export type SmartContractMethodArgType = AbiType;
export type SmartContractMethodStateMutability = 'view' | 'nonpayable' | 'payable'; export type SmartContractMethodStateMutability = 'view' | 'nonpayable' | 'payable';
...@@ -78,49 +78,19 @@ export interface SmartContractExternalLibrary { ...@@ -78,49 +78,19 @@ export interface SmartContractExternalLibrary {
name: string; name: string;
} }
export interface SmartContractMethodBase { export type SmartContractMethodOutputValue = string | boolean | object;
inputs: Array<SmartContractMethodInput>; export type SmartContractMethodOutput = AbiFunction['outputs'][number] & { value?: SmartContractMethodOutputValue };
outputs?: Array<SmartContractMethodOutput>; export type SmartContractMethodBase = Omit<AbiFunction, 'outputs'> & {
constant: boolean;
name: string;
stateMutability: SmartContractMethodStateMutability;
type: 'function';
payable: boolean;
error?: string;
method_id: string; method_id: string;
} outputs: Array<SmartContractMethodOutput>;
constant?: boolean;
error?: string;
};
export type SmartContractReadMethod = SmartContractMethodBase; export type SmartContractReadMethod = SmartContractMethodBase;
export type SmartContractWriteMethod = SmartContractMethodBase | AbiFallback | AbiReceive;
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 SmartContractMethod = SmartContractReadMethod | SmartContractWriteMethod; export type SmartContractMethod = SmartContractReadMethod | SmartContractWriteMethod;
export interface SmartContractMethodInput { export interface SmartContractQueryMethodSuccess {
internalType?: string; // there could be any string, e.g "enum MyEnum"
name: string;
type: SmartContractMethodArgType;
components?: Array<SmartContractMethodInput>;
fieldType?: 'native_coin';
}
export interface SmartContractMethodOutput extends SmartContractMethodInput {
value?: string | boolean | object;
}
export interface SmartContractQueryMethodReadSuccess {
is_error: false; is_error: false;
result: { result: {
names: Array<string | [ string, Array<string> ]>; names: Array<string | [ string, Array<string> ]>;
...@@ -131,7 +101,7 @@ export interface SmartContractQueryMethodReadSuccess { ...@@ -131,7 +101,7 @@ export interface SmartContractQueryMethodReadSuccess {
}; };
} }
export interface SmartContractQueryMethodReadError { export interface SmartContractQueryMethodError {
is_error: true; is_error: true;
result: { result: {
code: number; code: number;
...@@ -147,7 +117,7 @@ export interface SmartContractQueryMethodReadError { ...@@ -147,7 +117,7 @@ export interface SmartContractQueryMethodReadError {
}; };
} }
export type SmartContractQueryMethodRead = SmartContractQueryMethodReadSuccess | SmartContractQueryMethodReadError; export type SmartContractQueryMethod = SmartContractQueryMethodSuccess | SmartContractQueryMethodError;
// VERIFICATION // VERIFICATION
......
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(<AddressContract/>, { 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(<AddressContract/>, { 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(<AddressContract/>, { 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(<AddressContract/>, { 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();
});
});
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 <AddressContract tabs={ tabs } shouldRender={ true } isLoading={ false }/>;
};
export default AddressContractPwStory;
...@@ -3,7 +3,6 @@ import React from 'react'; ...@@ -3,7 +3,6 @@ import React from 'react';
import type { RoutedSubTab } from 'ui/shared/Tabs/types'; import type { RoutedSubTab } from 'ui/shared/Tabs/types';
import RoutedTabs from 'ui/shared/Tabs/RoutedTabs'; import RoutedTabs from 'ui/shared/Tabs/RoutedTabs';
import Web3ModalProvider from 'ui/shared/Web3ModalProvider';
interface Props { interface Props {
tabs: Array<RoutedSubTab>; tabs: Array<RoutedSubTab>;
...@@ -16,21 +15,12 @@ const TAB_LIST_PROPS = { ...@@ -16,21 +15,12 @@ const TAB_LIST_PROPS = {
}; };
const AddressContract = ({ tabs, isLoading, shouldRender }: Props) => { const AddressContract = ({ tabs, isLoading, shouldRender }: Props) => {
const fallback = React.useCallback(() => {
const noProviderTabs = tabs.filter(({ id }) => id === 'contract_code' || id.startsWith('read_'));
return (
<RoutedTabs tabs={ noProviderTabs } variant="outline" colorScheme="gray" size="sm" tabListProps={ TAB_LIST_PROPS } isLoading={ isLoading }/>
);
}, [ isLoading, tabs ]);
if (!shouldRender) { if (!shouldRender) {
return null; return null;
} }
return ( return (
<Web3ModalProvider fallback={ fallback }> <RoutedTabs tabs={ tabs } variant="outline" colorScheme="gray" size="sm" tabListProps={ TAB_LIST_PROPS } isLoading={ isLoading }/>
<RoutedTabs tabs={ tabs } variant="outline" colorScheme="gray" size="sm" tabListProps={ TAB_LIST_PROPS } isLoading={ isLoading }/>
</Web3ModalProvider>
); );
}; };
......
import { Accordion, Box, Flex, Link } from '@chakra-ui/react'; import { Accordion, Box, Flex, Link } from '@chakra-ui/react';
import _range from 'lodash/range'; import _range from 'lodash/range';
import React from 'react'; 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<T extends SmartContractMethod> { interface Props {
data: Array<T>; data: TContractAbi;
addressHash?: string; addressHash: string;
renderItemContent: (item: T, index: number, id: number) => React.ReactNode;
tab: string; tab: string;
methodType: MethodType;
} }
const ContractMethodsAccordion = <T extends SmartContractMethod>({ data, addressHash, renderItemContent, tab }: Props<T>) => { const ContractAbi = ({ data, addressHash, tab, methodType }: Props) => {
const [ expandedSections, setExpandedSections ] = React.useState<Array<number>>(data.length === 1 ? [ 0 ] : []); const [ expandedSections, setExpandedSections ] = React.useState<Array<number>>(data.length === 1 ? [ 0 ] : []);
const [ id, setId ] = React.useState(0); const [ id, setId ] = React.useState(0);
React.useEffect(() => { useScrollToMethod(data, setExpandedSections);
const hash = window.location.hash.replace('#', '');
if (!hash) { const handleFormSubmit = useFormSubmit({ addressHash, tab });
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 handleAccordionStateChange = React.useCallback((newValue: Array<number>) => { const handleAccordionStateChange = React.useCallback((newValue: Array<number>) => {
setExpandedSections(newValue); setExpandedSections(newValue);
...@@ -73,14 +60,15 @@ const ContractMethodsAccordion = <T extends SmartContractMethod>({ data, address ...@@ -73,14 +60,15 @@ const ContractMethodsAccordion = <T extends SmartContractMethod>({ data, address
</Flex> </Flex>
<Accordion allowMultiple position="relative" onChange={ handleAccordionStateChange } index={ expandedSections }> <Accordion allowMultiple position="relative" onChange={ handleAccordionStateChange } index={ expandedSections }>
{ data.map((item, index) => ( { data.map((item, index) => (
<ContractMethodsAccordionItem <ContractAbiItem
key={ index } key={ index }
data={ item } data={ item }
id={ id } id={ id }
index={ index } index={ index }
addressHash={ addressHash } addressHash={ addressHash }
renderContent={ renderItemContent as (item: SmartContractMethod, index: number, id: number) => React.ReactNode }
tab={ tab } tab={ tab }
onSubmit={ handleFormSubmit }
methodType={ methodType }
/> />
)) } )) }
</Accordion> </Accordion>
...@@ -88,4 +76,4 @@ const ContractMethodsAccordion = <T extends SmartContractMethod>({ data, address ...@@ -88,4 +76,4 @@ const ContractMethodsAccordion = <T extends SmartContractMethod>({ data, address
); );
}; };
export default React.memo(ContractMethodsAccordion) as typeof ContractMethodsAccordion; export default React.memo(ContractAbi);
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 React from 'react';
import { Element } from 'react-scroll'; 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 { route } from 'nextjs-routes';
import config from 'configs/app'; 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 Hint from 'ui/shared/Hint';
import IconSvg from 'ui/shared/IconSvg'; import IconSvg from 'ui/shared/IconSvg';
interface Props<T extends SmartContractMethod> { import ContractAbiItemConstant from './ContractAbiItemConstant';
data: T; import ContractMethodForm from './form/ContractMethodForm';
import { getElementName } from './useScrollToMethod';
interface Props {
data: TContractAbiItem;
index: number; index: number;
id: number; id: number;
addressHash?: string; addressHash: string;
renderContent: (item: T, index: number, id: number) => React.ReactNode;
tab: string; tab: string;
onSubmit: FormSubmitHandler;
methodType: MethodType;
} }
const ContractMethodsAccordionItem = <T extends SmartContractMethod>({ data, index, id, addressHash, renderContent, tab }: Props<T>) => { const ContractAbiItem = ({ data, index, id, addressHash, tab, onSubmit, methodType }: Props) => {
const url = React.useMemo(() => { const url = React.useMemo(() => {
if (!('method_id' in data)) { if (!('method_id' in data)) {
return ''; return '';
...@@ -43,11 +50,40 @@ const ContractMethodsAccordionItem = <T extends SmartContractMethod>({ data, ind ...@@ -43,11 +50,40 @@ const ContractMethodsAccordionItem = <T extends SmartContractMethod>({ data, ind
onCopy(); onCopy();
}, [ onCopy ]); }, [ onCopy ]);
const handleCopyMethodIdClick = React.useCallback((event: React.MouseEvent) => {
event.stopPropagation();
}, []);
const content = (() => {
if ('error' in data && data.error) {
return <Alert status="error" fontSize="sm" wordBreak="break-word">{ data.error }</Alert>;
}
const hasConstantOutputs = 'outputs' in data && data.outputs.some(({ value }) => value !== undefined && value !== null);
if (hasConstantOutputs) {
return (
<Flex flexDir="column" rowGap={ 1 }>
{ data.outputs.map((output, index) => <ContractAbiItemConstant key={ index } data={ output }/>) }
</Flex>
);
}
return (
<ContractMethodForm
key={ id + '_' + index }
data={ data }
onSubmit={ onSubmit }
methodType={ methodType }
/>
);
})();
return ( return (
<AccordionItem as="section" _first={{ borderTopWidth: 0 }} _last={{ borderBottomWidth: 0 }}> <AccordionItem as="section" _first={{ borderTopWidth: 0 }} _last={{ borderBottomWidth: 0 }}>
{ ({ isExpanded }) => ( { ({ isExpanded }) => (
<> <>
<Element as="h2" name={ 'method_id' in data ? `method_${ data.method_id }` : '' }> <Element as="h2" name={ 'method_id' in data ? getElementName(data.method_id) : '' }>
<AccordionButton px={ 0 } py={ 3 } _hover={{ bgColor: 'inherit' }} wordBreak="break-all" textAlign="left" as="div" cursor="pointer"> <AccordionButton px={ 0 } py={ 3 } _hover={{ bgColor: 'inherit' }} wordBreak="break-all" textAlign="left" as="div" cursor="pointer">
{ 'method_id' in data && ( { 'method_id' in data && (
<Tooltip label={ hasCopied ? 'Copied!' : 'Copy link' } isOpen={ isOpen || hasCopied } onClose={ onClose }> <Tooltip label={ hasCopied ? 'Copied!' : 'Copy link' } isOpen={ isOpen || hasCopied } onClose={ onClose }>
...@@ -85,11 +121,17 @@ const ContractMethodsAccordionItem = <T extends SmartContractMethod>({ data, ind ...@@ -85,11 +121,17 @@ const ContractMethodsAccordionItem = <T extends SmartContractMethod>({ data, ind
the contract cannot receive Ether through regular transactions and throws an exception.` the contract cannot receive Ether through regular transactions and throws an exception.`
}/> }/>
) } ) }
{ 'method_id' in data && (
<>
<Tag>{ data.method_id }</Tag>
<CopyToClipboard text={ `${ data.name } (${ data.method_id })` } onClick={ handleCopyMethodIdClick }/>
</>
) }
<AccordionIcon transform={ isExpanded ? 'rotate(0deg)' : 'rotate(-90deg)' } color="gray.500"/> <AccordionIcon transform={ isExpanded ? 'rotate(0deg)' : 'rotate(-90deg)' } color="gray.500"/>
</AccordionButton> </AccordionButton>
</Element> </Element>
<AccordionPanel pb={ 4 } pr={ 0 } pl="28px" w="calc(100% - 6px)"> <AccordionPanel pb={ 4 } pr={ 0 } pl="28px" w="calc(100% - 6px)">
{ renderContent(data, index, id) } { content }
</AccordionPanel> </AccordionPanel>
</> </>
) } ) }
...@@ -97,4 +139,4 @@ const ContractMethodsAccordionItem = <T extends SmartContractMethod>({ data, ind ...@@ -97,4 +139,4 @@ const ContractMethodsAccordionItem = <T extends SmartContractMethod>({ data, ind
); );
}; };
export default React.memo(ContractMethodsAccordionItem); export default React.memo(ContractAbiItem);
...@@ -4,12 +4,14 @@ import type { ChangeEvent } from 'react'; ...@@ -4,12 +4,14 @@ import type { ChangeEvent } from 'react';
import React from 'react'; import React from 'react';
import { getAddress } from 'viem'; import { getAddress } from 'viem';
import type { SmartContractMethodOutput } from 'types/api/contract'; import type { ContractAbiItemOutput } from './types';
import { WEI } from 'lib/consts'; import { WEI } from 'lib/consts';
import { currencyUnits } from 'lib/units'; import { currencyUnits } from 'lib/units';
import AddressEntity from 'ui/shared/entities/address/AddressEntity'; import AddressEntity from 'ui/shared/entities/address/AddressEntity';
import { matchInt } from './form/utils';
function castValueToString(value: number | string | boolean | object | bigint | undefined): string { function castValueToString(value: number | string | boolean | object | bigint | undefined): string {
switch (typeof value) { switch (typeof value) {
case 'string': case 'string':
...@@ -28,13 +30,15 @@ function castValueToString(value: number | string | boolean | object | bigint | ...@@ -28,13 +30,15 @@ function castValueToString(value: number | string | boolean | object | bigint |
} }
interface Props { interface Props {
data: SmartContractMethodOutput; data: ContractAbiItemOutput;
} }
const ContractMethodStatic = ({ data }: Props) => { const ContractAbiItemConstant = ({ data }: Props) => {
const [ value, setValue ] = React.useState<string>(castValueToString(data.value)); const [ value, setValue ] = React.useState<string>(castValueToString(data.value));
const [ label, setLabel ] = React.useState(currencyUnits.wei.toUpperCase()); const [ label, setLabel ] = React.useState(currencyUnits.wei.toUpperCase());
const intMatch = matchInt(data.type);
const handleCheckboxChange = React.useCallback((event: ChangeEvent<HTMLInputElement>) => { const handleCheckboxChange = React.useCallback((event: ChangeEvent<HTMLInputElement>) => {
const initialValue = castValueToString(data.value); const initialValue = castValueToString(data.value);
...@@ -63,9 +67,9 @@ const ContractMethodStatic = ({ data }: Props) => { ...@@ -63,9 +67,9 @@ const ContractMethodStatic = ({ data }: Props) => {
return ( return (
<Flex flexDir={{ base: 'column', lg: 'row' }} columnGap={ 2 } rowGap={ 2 }> <Flex flexDir={{ base: 'column', lg: 'row' }} columnGap={ 2 } rowGap={ 2 }>
{ content } { content }
{ (data.type.includes('int256') || data.type.includes('int128')) && <Checkbox onChange={ handleCheckboxChange }>{ label }</Checkbox> } { Number(intMatch?.power) >= 128 && <Checkbox onChange={ handleCheckboxChange }>{ label }</Checkbox> }
</Flex> </Flex>
); );
}; };
export default ContractMethodStatic; export default ContractAbiItemConstant;
...@@ -3,7 +3,7 @@ import React from 'react'; ...@@ -3,7 +3,7 @@ import React from 'react';
import { useController, useFormContext } from 'react-hook-form'; import { useController, useFormContext } from 'react-hook-form';
import { NumericFormat } from 'react-number-format'; import { NumericFormat } from 'react-number-format';
import type { SmartContractMethodInput } from 'types/api/contract'; import type { ContractAbiItemInput } from '../types';
import ClearButton from 'ui/shared/ClearButton'; import ClearButton from 'ui/shared/ClearButton';
...@@ -14,7 +14,7 @@ import useValidateField from './useValidateField'; ...@@ -14,7 +14,7 @@ import useValidateField from './useValidateField';
import { matchInt } from './utils'; import { matchInt } from './utils';
interface Props { interface Props {
data: SmartContractMethodInput; data: ContractAbiItemInput;
hideLabel?: boolean; hideLabel?: boolean;
path: string; path: string;
className?: string; className?: string;
......
...@@ -2,7 +2,7 @@ import { Flex } from '@chakra-ui/react'; ...@@ -2,7 +2,7 @@ import { Flex } from '@chakra-ui/react';
import React from 'react'; import React from 'react';
import { useFormContext } from 'react-hook-form'; import { useFormContext } from 'react-hook-form';
import type { SmartContractMethodInput } from 'types/api/contract'; import type { ContractAbiItemInput } from '../types';
import ContractMethodArrayButton from './ContractMethodArrayButton'; import ContractMethodArrayButton from './ContractMethodArrayButton';
import type { Props as AccordionProps } from './ContractMethodFieldAccordion'; import type { Props as AccordionProps } from './ContractMethodFieldAccordion';
...@@ -13,7 +13,7 @@ import ContractMethodFieldLabel from './ContractMethodFieldLabel'; ...@@ -13,7 +13,7 @@ import ContractMethodFieldLabel from './ContractMethodFieldLabel';
import { getFieldLabel, matchArray, transformDataForArrayItem } from './utils'; import { getFieldLabel, matchArray, transformDataForArrayItem } from './utils';
interface Props extends Pick<AccordionProps, 'onAddClick' | 'onRemoveClick' | 'index'> { interface Props extends Pick<AccordionProps, 'onAddClick' | 'onRemoveClick' | 'index'> {
data: SmartContractMethodInput; data: ContractAbiItemInput;
level: number; level: number;
basePath: string; basePath: string;
isDisabled: boolean; isDisabled: boolean;
......
import React from 'react'; import React from 'react';
import { useFormContext } from 'react-hook-form'; 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 type { Props as AccordionProps } from './ContractMethodFieldAccordion';
import ContractMethodFieldAccordion from './ContractMethodFieldAccordion'; import ContractMethodFieldAccordion from './ContractMethodFieldAccordion';
...@@ -10,7 +10,7 @@ import ContractMethodFieldInputArray from './ContractMethodFieldInputArray'; ...@@ -10,7 +10,7 @@ import ContractMethodFieldInputArray from './ContractMethodFieldInputArray';
import { getFieldLabel, matchArray } from './utils'; import { getFieldLabel, matchArray } from './utils';
interface Props extends Pick<AccordionProps, 'onAddClick' | 'onRemoveClick' | 'index'> { interface Props extends Pick<AccordionProps, 'onAddClick' | 'onRemoveClick' | 'index'> {
data: SmartContractMethodInput; data: ContractAbiItemInput;
basePath: string; basePath: string;
level: number; level: number;
isDisabled: boolean; isDisabled: boolean;
...@@ -21,6 +21,10 @@ const ContractMethodFieldInputTuple = ({ data, basePath, level, isDisabled, ...a ...@@ -21,6 +21,10 @@ const ContractMethodFieldInputTuple = ({ data, basePath, level, isDisabled, ...a
const fieldsWithErrors = Object.keys(errors); const fieldsWithErrors = Object.keys(errors);
const isInvalid = fieldsWithErrors.some((field) => field.startsWith(basePath)); const isInvalid = fieldsWithErrors.some((field) => field.startsWith(basePath));
if (!('components' in data)) {
return null;
}
return ( return (
<ContractMethodFieldAccordion <ContractMethodFieldAccordion
{ ...accordionProps } { ...accordionProps }
...@@ -29,7 +33,7 @@ const ContractMethodFieldInputTuple = ({ data, basePath, level, isDisabled, ...a ...@@ -29,7 +33,7 @@ const ContractMethodFieldInputTuple = ({ data, basePath, level, isDisabled, ...a
isInvalid={ isInvalid } isInvalid={ isInvalid }
> >
{ data.components?.map((component, index) => { { data.components?.map((component, index) => {
if (component.components && component.type === 'tuple') { if ('components' in component && component.type === 'tuple') {
return ( return (
<ContractMethodFieldInputTuple <ContractMethodFieldInputTuple
key={ index } key={ index }
......
import { Box, useColorModeValue } from '@chakra-ui/react'; import { Box, useColorModeValue } from '@chakra-ui/react';
import React from 'react'; import React from 'react';
import type { SmartContractMethodInput } from 'types/api/contract'; import type { ContractAbiItemInput } from '../types';
import { getFieldLabel } from './utils'; import { getFieldLabel } from './utils';
interface Props { interface Props {
data: SmartContractMethodInput; data: ContractAbiItemInput;
isOptional?: boolean; isOptional?: boolean;
level: number; level: number;
} }
......
import { test, expect } from '@playwright/experimental-ct-react'; import { test, expect } from '@playwright/experimental-ct-react';
import React from 'react'; import React from 'react';
import type { SmartContractWriteMethod } from 'types/api/contract'; import type { ContractAbiItem } from '../types';
import TestApp from 'playwright/TestApp'; import TestApp from 'playwright/TestApp';
import ContractMethodForm from './ContractMethodForm'; import ContractMethodForm from './ContractMethodForm';
const onSubmit = () => Promise.resolve({ hash: '0x0000' as `0x${ string }` }); const onSubmit = () => Promise.resolve({ source: 'wallet_client' as const, result: { hash: '0x0000' as `0x${ string }` } });
const resultComponent = () => null;
const data: SmartContractWriteMethod = { const data: ContractAbiItem = {
inputs: [ inputs: [
// TUPLE // TUPLE
{ {
...@@ -102,10 +101,9 @@ test('base view +@mobile +@dark-mode', async({ mount }) => { ...@@ -102,10 +101,9 @@ test('base view +@mobile +@dark-mode', async({ mount }) => {
const component = await mount( const component = await mount(
<TestApp> <TestApp>
<ContractMethodForm<SmartContractWriteMethod> <ContractMethodForm
data={ data } data={ data }
onSubmit={ onSubmit } onSubmit={ onSubmit }
resultComponent={ resultComponent }
methodType="write" methodType="write"
/> />
</TestApp>, </TestApp>,
......
import { Box, Button, Flex, chakra } from '@chakra-ui/react'; import { Box, Button, Flex, Tooltip, chakra } from '@chakra-ui/react';
import _mapValues from 'lodash/mapValues'; import _mapValues from 'lodash/mapValues';
import React from 'react'; import React from 'react';
import type { SubmitHandler } from 'react-hook-form'; import type { SubmitHandler } from 'react-hook-form';
import { useForm, FormProvider } from 'react-hook-form'; import { useForm, FormProvider } from 'react-hook-form';
import type { AbiFunction } from 'viem';
import type { ContractMethodCallResult } from '../types'; import type { FormSubmitHandler, FormSubmitResult, MethodCallStrategy, MethodType, ContractAbiItem } from '../types';
import type { ResultComponentProps } from './types';
import type { SmartContractMethod, SmartContractMethodInput } from 'types/api/contract';
import config from 'configs/app'; import config from 'configs/app';
import * as mixpanel from 'lib/mixpanel/index'; import * as mixpanel from 'lib/mixpanel/index';
...@@ -15,30 +14,38 @@ import ContractMethodFieldAccordion from './ContractMethodFieldAccordion'; ...@@ -15,30 +14,38 @@ import ContractMethodFieldAccordion from './ContractMethodFieldAccordion';
import ContractMethodFieldInput from './ContractMethodFieldInput'; import ContractMethodFieldInput from './ContractMethodFieldInput';
import ContractMethodFieldInputArray from './ContractMethodFieldInputArray'; import ContractMethodFieldInputArray from './ContractMethodFieldInputArray';
import ContractMethodFieldInputTuple from './ContractMethodFieldInputTuple'; import ContractMethodFieldInputTuple from './ContractMethodFieldInputTuple';
import ContractMethodFormOutputs from './ContractMethodFormOutputs'; import ContractMethodOutputs from './ContractMethodOutputs';
import ContractMethodResult from './ContractMethodResult';
import { getFieldLabel, matchArray, transformFormDataToMethodArgs } from './utils'; import { getFieldLabel, matchArray, transformFormDataToMethodArgs } from './utils';
import type { ContractMethodFormFields } from './utils'; import type { ContractMethodFormFields } from './utils';
interface Props<T extends SmartContractMethod> { interface Props {
data: T; data: ContractAbiItem;
onSubmit: (data: T, args: Array<unknown>) => Promise<ContractMethodCallResult<T>>; onSubmit: FormSubmitHandler;
resultComponent: (props: ResultComponentProps<T>) => JSX.Element | null; methodType: MethodType;
methodType: 'read' | 'write';
} }
const ContractMethodForm = <T extends SmartContractMethod>({ data, onSubmit, resultComponent: ResultComponent, methodType }: Props<T>) => { const ContractMethodForm = ({ data, onSubmit, methodType }: Props) => {
const [ result, setResult ] = React.useState<ContractMethodCallResult<T>>(); const [ result, setResult ] = React.useState<FormSubmitResult>();
const [ isLoading, setLoading ] = React.useState(false); const [ isLoading, setLoading ] = React.useState(false);
const [ callStrategy, setCallStrategy ] = React.useState<MethodCallStrategy>();
const callStrategyRef = React.useRef(callStrategy);
const formApi = useForm<ContractMethodFormFields>({ const formApi = useForm<ContractMethodFormFields>({
mode: 'all', mode: 'all',
shouldUnregister: true, 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<ContractMethodFormFields> = React.useCallback(async(formData) => { const onFormSubmit: SubmitHandler<ContractMethodFormFields> = React.useCallback(async(formData) => {
// The API used for reading from contracts expects all values to be strings. // The API used for reading from contracts expects all values to be strings.
const formattedData = methodType === 'read' ? const formattedData = callStrategyRef.current === 'api' ?
_mapValues(formData, (value) => value !== undefined ? String(value) : undefined) : _mapValues(formData, (value) => value !== undefined ? String(value) : undefined) :
formData; formData;
const args = transformFormDataToMethodArgs(formattedData); const args = transformFormDataToMethodArgs(formattedData);
...@@ -46,12 +53,15 @@ const ContractMethodForm = <T extends SmartContractMethod>({ data, onSubmit, res ...@@ -46,12 +53,15 @@ const ContractMethodForm = <T extends SmartContractMethod>({ data, onSubmit, res
setResult(undefined); setResult(undefined);
setLoading(true); setLoading(true);
onSubmit(data, args) onSubmit(data, args, callStrategyRef.current)
.then((result) => { .then((result) => {
setResult(result); setResult(result);
}) })
.catch((error) => { .catch((error) => {
setResult(error?.error || error?.data || (error?.reason && { message: error.reason }) || error); setResult({
source: callStrategyRef.current ?? 'wallet_client',
result: error?.error || error?.data || (error?.reason && { message: error.reason }) || error,
});
setLoading(false); setLoading(false);
}) })
.finally(() => { .finally(() => {
...@@ -70,9 +80,9 @@ const ContractMethodForm = <T extends SmartContractMethod>({ data, onSubmit, res ...@@ -70,9 +80,9 @@ const ContractMethodForm = <T extends SmartContractMethod>({ data, onSubmit, res
result && setResult(undefined); result && setResult(undefined);
}, [ result ]); }, [ result ]);
const inputs: Array<SmartContractMethodInput> = React.useMemo(() => { const inputs: AbiFunction['inputs'] = React.useMemo(() => {
return [ return [
...('inputs' in data ? data.inputs : []), ...('inputs' in data && data.inputs ? data.inputs : []),
...('stateMutability' in data && data.stateMutability === 'payable' ? [ { ...('stateMutability' in data && data.stateMutability === 'payable' ? [ {
name: `Send native ${ config.chain.currency.symbol || 'coin' }`, name: `Send native ${ config.chain.currency.symbol || 'coin' }`,
type: 'uint256' as const, type: 'uint256' as const,
...@@ -84,6 +94,28 @@ const ContractMethodForm = <T extends SmartContractMethod>({ data, onSubmit, res ...@@ -84,6 +94,28 @@ const ContractMethodForm = <T extends SmartContractMethod>({ data, onSubmit, res
const outputs = 'outputs' in data && data.outputs ? data.outputs : []; 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 ( return (
<Box> <Box>
<FormProvider { ...formApi }> <FormProvider { ...formApi }>
...@@ -94,12 +126,18 @@ const ContractMethodForm = <T extends SmartContractMethod>({ data, onSubmit, res ...@@ -94,12 +126,18 @@ const ContractMethodForm = <T extends SmartContractMethod>({ data, onSubmit, res
> >
<Flex flexDir="column" rowGap={ 3 } mb={ 6 } _empty={{ display: 'none' }}> <Flex flexDir="column" rowGap={ 3 } mb={ 6 } _empty={{ display: 'none' }}>
{ inputs.map((input, index) => { { inputs.map((input, index) => {
if (input.components && input.type === 'tuple') { const props = {
return <ContractMethodFieldInputTuple key={ index } data={ input } basePath={ `${ index }` } level={ 0 } isDisabled={ isLoading }/>; data: input,
basePath: `${ index }`,
isDisabled: isLoading,
level: 0,
};
if ('components' in input && input.components && input.type === 'tuple') {
return <ContractMethodFieldInputTuple key={ index } { ...props }/>;
} }
const arrayMatch = matchArray(input.type); const arrayMatch = matchArray(input.type);
if (arrayMatch) { if (arrayMatch) {
if (arrayMatch.isNested) { if (arrayMatch.isNested) {
const fieldsWithErrors = Object.keys(formApi.formState.errors); const fieldsWithErrors = Object.keys(formApi.formState.errors);
...@@ -112,33 +150,57 @@ const ContractMethodForm = <T extends SmartContractMethod>({ data, onSubmit, res ...@@ -112,33 +150,57 @@ const ContractMethodForm = <T extends SmartContractMethod>({ data, onSubmit, res
label={ getFieldLabel(input) } label={ getFieldLabel(input) }
isInvalid={ isInvalid } isInvalid={ isInvalid }
> >
<ContractMethodFieldInputArray data={ input } basePath={ `${ index }` } level={ 0 } isDisabled={ isLoading }/> <ContractMethodFieldInputArray { ...props }/>
</ContractMethodFieldAccordion> </ContractMethodFieldAccordion>
); );
} }
return <ContractMethodFieldInputArray key={ index } data={ input } basePath={ `${ index }` } level={ 0 } isDisabled={ isLoading }/>; return <ContractMethodFieldInputArray key={ index } { ...props }/>;
} }
return <ContractMethodFieldInput key={ index } data={ input } path={ `${ index }` } isDisabled={ isLoading } level={ 0 }/>; return <ContractMethodFieldInput key={ index } { ...props } path={ `${ index }` }/>;
}) } }) }
</Flex> </Flex>
<Button { callStrategies.secondary && (
isLoading={ isLoading } <Button
loadingText={ methodType === 'write' ? 'Write' : 'Read' } isLoading={ callStrategy === callStrategies.secondary && isLoading }
variant="outline" isDisabled={ isLoading }
size="sm" onClick={ handleButtonClick }
flexShrink={ 0 } loadingText="Simulate"
width="min-content" variant="outline"
px={ 4 } size="sm"
type="submit" flexShrink={ 0 }
> width="min-content"
{ methodType === 'write' ? 'Write' : 'Read' } px={ 4 }
</Button> mr={ 3 }
type="submit"
data-call-strategy={ callStrategies.secondary }
>
Simulate
</Button>
) }
<Tooltip label={ !callStrategies.primary ? noWalletClientText : undefined } maxW="300px">
<Button
isLoading={ callStrategy === callStrategies.primary && isLoading }
isDisabled={ isLoading || !callStrategies.primary }
onClick={ handleButtonClick }
loadingText={ methodType === 'write' ? 'Write' : 'Read' }
variant="outline"
size="sm"
flexShrink={ 0 }
width="min-content"
px={ 4 }
type="submit"
data-call-strategy={ callStrategies.primary }
>
{ methodType === 'write' ? 'Write' : 'Read' }
</Button>
</Tooltip>
</chakra.form> </chakra.form>
</FormProvider> </FormProvider>
{ methodType === 'read' && <ContractMethodFormOutputs data={ outputs }/> } { 'outputs' in data && Boolean(data.outputs?.length) && <ContractMethodOutputs data={ outputs }/> }
{ result && <ResultComponent item={ data } result={ result } onSettle={ handleTxSettle }/> } { result && <ContractMethodResult abiItem={ data } result={ result } onSettle={ handleTxSettle }/> }
</Box> </Box>
); );
}; };
......
import { Flex, chakra } from '@chakra-ui/react'; import { Flex, chakra } from '@chakra-ui/react';
import React from 'react'; import React from 'react';
import type { AbiFunction } from 'viem';
import type { SmartContractMethodOutput } from 'types/api/contract';
import IconSvg from 'ui/shared/IconSvg'; import IconSvg from 'ui/shared/IconSvg';
interface Props { interface Props {
data: Array<SmartContractMethodOutput>; data: AbiFunction['outputs'];
} }
const ContractMethodFormOutputs = ({ data }: Props) => { const ContractMethodOutputs = ({ data }: Props) => {
if (data.length === 0) { if (data.length === 0) {
return null; return null;
} }
...@@ -32,4 +31,4 @@ const ContractMethodFormOutputs = ({ data }: Props) => { ...@@ -32,4 +31,4 @@ const ContractMethodFormOutputs = ({ data }: Props) => {
); );
}; };
export default React.memo(ContractMethodFormOutputs); export default React.memo(ContractMethodOutputs);
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 <ContractMethodResultApi item={ abiItem } result={ result.result } onSettle={ onSettle }/>;
case 'wallet_client':
return <ContractMethodResultWalletClient result={ result.result } onSettle={ onSettle }/>;
default: {
return null;
}
}
};
export default React.memo(ContractMethodResult);
import { test, expect } from '@playwright/experimental-ct-react';
import React from 'react'; import React from 'react';
import type { ContractMethodReadResult } from './types'; import type { FormSubmitResultApi } from '../types';
import * as contractMethodsMock from 'mocks/contract/methods'; 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 item = contractMethodsMock.read[0];
const onSettle = () => Promise.resolve(); const onSettle = () => Promise.resolve();
test.use({ viewport: { width: 500, height: 500 } }); test.use({ viewport: { width: 500, height: 500 } });
test('default error', async({ mount }) => { test('default error', async({ render }) => {
const result: ContractMethodReadResult = { const result: FormSubmitResultApi['result'] = {
is_error: true, is_error: true,
result: { result: {
error: 'I am an error', error: 'I am an error',
}, },
}; };
const component = await mount( const component = await render(<ContractMethodResultApi item={ item } onSettle={ onSettle } result={ result }/>);
<TestApp>
<ContractReadResult item={ item } onSettle={ onSettle } result={ result }/>
</TestApp>,
);
await expect(component).toHaveScreenshot(); await expect(component).toHaveScreenshot();
}); });
test('error with code', async({ mount }) => { test('error with code', async({ render }) => {
const result: ContractMethodReadResult = { const result: FormSubmitResultApi['result'] = {
is_error: true, is_error: true,
result: { result: {
message: 'I am an error', message: 'I am an error',
code: -32017, code: -32017,
}, },
}; };
const component = await mount( const component = await render(<ContractMethodResultApi item={ item } onSettle={ onSettle } result={ result }/>);
<TestApp>
<ContractReadResult item={ item } onSettle={ onSettle } result={ result }/>
</TestApp>,
);
await expect(component).toHaveScreenshot(); await expect(component).toHaveScreenshot();
}); });
test('raw error', async({ mount }) => { test('raw error', async({ render }) => {
const result: ContractMethodReadResult = { const result: FormSubmitResultApi['result'] = {
is_error: true, is_error: true,
result: { result: {
raw: '49276d20616c7761797320726576657274696e67207769746820616e206572726f72', raw: '49276d20616c7761797320726576657274696e67207769746820616e206572726f72',
}, },
}; };
const component = await mount( const component = await render(<ContractMethodResultApi item={ item } onSettle={ onSettle } result={ result }/>);
<TestApp>
<ContractReadResult item={ item } onSettle={ onSettle } result={ result }/>
</TestApp>,
);
await expect(component).toHaveScreenshot(); await expect(component).toHaveScreenshot();
}); });
test('complex error', async({ mount }) => { test('complex error', async({ render }) => {
const result: ContractMethodReadResult = { const result: FormSubmitResultApi['result'] = {
is_error: true, is_error: true,
result: { result: {
method_call: 'SomeCustomError(address addr, uint256 balance)', method_call: 'SomeCustomError(address addr, uint256 balance)',
...@@ -74,34 +61,26 @@ test('complex error', async({ mount }) => { ...@@ -74,34 +61,26 @@ test('complex error', async({ mount }) => {
], ],
}, },
}; };
const component = await mount( const component = await render(<ContractMethodResultApi item={ item } onSettle={ onSettle } result={ result }/>);
<TestApp>
<ContractReadResult item={ item } onSettle={ onSettle } result={ result }/>
</TestApp>,
);
await expect(component).toHaveScreenshot(); await expect(component).toHaveScreenshot();
}); });
test('success', async({ mount }) => { test('success', async({ render }) => {
const result: ContractMethodReadResult = { const result: FormSubmitResultApi['result'] = {
is_error: false, is_error: false,
result: { result: {
names: [ 'address' ], names: [ 'address' ],
output: [ { type: 'address', value: '0x0000000000000000000000000000000000000000' } ], output: [ { type: 'address', value: '0x0000000000000000000000000000000000000000' } ],
}, },
}; };
const component = await mount( const component = await render(<ContractMethodResultApi item={ item } onSettle={ onSettle } result={ result }/>);
<TestApp>
<ContractReadResult item={ item } onSettle={ onSettle } result={ result }/>
</TestApp>,
);
await expect(component).toHaveScreenshot(); await expect(component).toHaveScreenshot();
}); });
test('complex success', async({ mount }) => { test('complex success', async({ render }) => {
const result: ContractMethodReadResult = { const result: FormSubmitResultApi['result'] = {
is_error: false, is_error: false,
result: { result: {
names: [ names: [
...@@ -122,11 +101,7 @@ test('complex success', async({ mount }) => { ...@@ -122,11 +101,7 @@ test('complex success', async({ mount }) => {
], ],
}, },
}; };
const component = await mount( const component = await render(<ContractMethodResultApi item={ item } onSettle={ onSettle } result={ result }/>);
<TestApp>
<ContractReadResult item={ item } onSettle={ onSettle } result={ result }/>
</TestApp>,
);
await expect(component).toHaveScreenshot(); await expect(component).toHaveScreenshot();
}); });
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 <ContractMethodResultApiError>{ result.statusText }</ContractMethodResultApiError>;
}
if (result instanceof Error) {
return <ContractMethodResultApiError>{ result.message }</ContractMethodResultApiError>;
}
if (result.is_error) {
if ('error' in result.result) {
return <ContractMethodResultApiError>{ result.result.error }</ContractMethodResultApiError>;
}
if ('message' in result.result) {
return <ContractMethodResultApiError>[{ result.result.code }] { result.result.message }</ContractMethodResultApiError>;
}
if ('raw' in result.result) {
return <ContractMethodResultApiError>{ `Revert reason: ${ hexToUtf8(result.result.raw) }` }</ContractMethodResultApiError>;
}
if ('method_id' in result.result) {
return <ContractMethodResultApiError>{ JSON.stringify(result.result, undefined, 2) }</ContractMethodResultApiError>;
}
return <ContractMethodResultApiError>Something went wrong.</ContractMethodResultApiError>;
}
return (
<Box mt={ 3 } p={ 4 } borderRadius="md" bgColor={ resultBgColor } fontSize="sm" whiteSpace="break-spaces" wordBreak="break-all">
<p>
[ <chakra.span fontWeight={ 500 }>{ 'name' in item ? item.name : '' }</chakra.span> method response ]
</p>
<p>[</p>
{ result.result.output.map((output, index) => <ContractMethodResultApiItem key={ index } output={ output } name={ result.result.names[index] }/>) }
<p>]</p>
</Box>
);
};
export default React.memo(ContractMethodResultApi);
import { Alert } from '@chakra-ui/react';
import React from 'react';
interface Props {
children: React.ReactNode;
}
const ContractMethodResultApiError = ({ children }: Props) => {
return (
<Alert status="error" mt={ 3 } p={ 4 } borderRadius="md" fontSize="sm" wordBreak="break-word" whiteSpace="pre-wrap">
{ children }
</Alert>
);
};
export default React.memo(ContractMethodResultApiError);
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 (
<>
<p>
<chakra.span fontWeight={ 500 }> { structName }</chakra.span>
<span> ({ output.type }) :</span>
</p>
{ argNames.map((argName, argIndex) => {
return (
<p key={ argName }>
<chakra.span fontWeight={ 500 }> { argName }</chakra.span>
<span>{ argTypes?.[argIndex] ? ` (${ argTypes[argIndex] })` : '' } : { String(output.value[argIndex]) }</span>
</p>
);
}) }
</>
);
}
return (
<p>
<span> </span>
{ name && <chakra.span fontWeight={ 500 }>{ name } </chakra.span> }
<span>({ output.type }) : { String(output.value) }</span>
</p>
);
};
export default React.memo(ContractMethodResultApiItem);
import { test, expect } from '@playwright/experimental-ct-react';
import React from '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 = { const props = {
txInfo: { txInfo: {
status: 'pending' as const, status: 'pending' as const,
error: null, error: null,
}, } as PropsDumb['txInfo'],
result: { result: {
hash: '0x363574E6C5C71c343d7348093D84320c76d5Dd29' as `0x${ string }`, hash: '0x363574E6C5C71c343d7348093D84320c76d5Dd29' as `0x${ string }`,
}, },
onSettle: () => {}, onSettle: () => {},
}; };
const component = await mount( const component = await render(<ContractMethodResultWalletClientDumb { ...props }/>);
<TestApp>
<ContractWriteResultDumb { ...props }/>
</TestApp>,
);
await expect(component).toHaveScreenshot(); await expect(component).toHaveScreenshot();
}); });
test('success', async({ mount }) => { test('success', async({ render }) => {
const props = { const props = {
txInfo: { txInfo: {
status: 'success' as const, status: 'success' as const,
error: null, error: null,
}, } as PropsDumb['txInfo'],
result: { result: {
hash: '0x363574E6C5C71c343d7348093D84320c76d5Dd29' as `0x${ string }`, hash: '0x363574E6C5C71c343d7348093D84320c76d5Dd29' as `0x${ string }`,
}, },
onSettle: () => {}, onSettle: () => {},
}; };
const component = await mount( const component = await render(<ContractMethodResultWalletClientDumb { ...props }/>);
<TestApp>
<ContractWriteResultDumb { ...props }/>
</TestApp>,
);
await expect(component).toHaveScreenshot(); await expect(component).toHaveScreenshot();
}); });
test('error +@mobile', async({ mount }) => { test('error +@mobile', async({ render }) => {
const props = { const props = {
txInfo: { txInfo: {
status: 'error' as const, status: 'error' as const,
...@@ -55,39 +45,29 @@ test('error +@mobile', async({ mount }) => { ...@@ -55,39 +45,29 @@ test('error +@mobile', async({ mount }) => {
// eslint-disable-next-line max-len // 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 ]', 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 Error,
}, } as PropsDumb['txInfo'],
result: { result: {
hash: '0x363574E6C5C71c343d7348093D84320c76d5Dd29' as `0x${ string }`, hash: '0x363574E6C5C71c343d7348093D84320c76d5Dd29' as `0x${ string }`,
}, },
onSettle: () => {}, onSettle: () => {},
}; };
const component = await mount( const component = await render(<ContractMethodResultWalletClientDumb { ...props }/>);
<TestApp>
<ContractWriteResultDumb { ...props }/>
</TestApp>,
);
await expect(component).toHaveScreenshot(); await expect(component).toHaveScreenshot();
}); });
test('error in result', async({ mount }) => { test('error in result', async({ render }) => {
const props = { const props = {
txInfo: { txInfo: {
status: 'idle' as const, status: 'idle' as const,
error: null, error: null,
}, } as unknown as PropsDumb['txInfo'],
result: { result: {
message: 'wallet is not connected', message: 'wallet is not connected',
} as Error, } as Error,
onSettle: () => {}, onSettle: () => {},
}; };
const component = await mount( const component = await render(<ContractMethodResultWalletClientDumb { ...props }/>);
<TestApp>
<ContractWriteResultDumb { ...props }/>
</TestApp>,
);
await expect(component).toHaveScreenshot(); await expect(component).toHaveScreenshot();
}); });
import { Box, chakra, Spinner } from '@chakra-ui/react'; import { chakra, Spinner, Box } from '@chakra-ui/react';
import React from '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 { route } from 'nextjs-routes';
import LinkInternal from 'ui/shared/LinkInternal'; import LinkInternal from 'ui/shared/LinkInternal';
interface Props { interface Props {
result: ContractMethodWriteResult; result: FormSubmitResultWalletClient['result'];
onSettle: () => void; 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 <ContractMethodResultWalletClientDumb result={ result } onSettle={ onSettle } txInfo={ txInfo }/>;
};
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; const txHash = result && 'hash' in result ? result.hash : undefined;
React.useEffect(() => { React.useEffect(() => {
...@@ -93,4 +106,4 @@ const ContractWriteResultDumb = ({ result, onSettle, txInfo }: Props) => { ...@@ -93,4 +106,4 @@ const ContractWriteResultDumb = ({ result, onSettle, txInfo }: Props) => {
); );
}; };
export default React.memo(ContractWriteResultDumb); export default React.memo(ContractMethodResultWalletClient);
import React from 'react'; import React from 'react';
import type { SmartContractMethodArgType } from 'types/api/contract';
import type { MatchInt } from './utils'; import type { MatchInt } from './utils';
interface Params { interface Params {
argType: SmartContractMethodArgType; argType: string;
argTypeMatchInt: MatchInt | null; argTypeMatchInt: MatchInt | null;
} }
......
import React from 'react'; import React from 'react';
import { getAddress, isAddress, isHex } from 'viem'; import { getAddress, isAddress, isHex } from 'viem';
import type { SmartContractMethodArgType } from 'types/api/contract';
import type { MatchInt } from './utils'; import type { MatchInt } from './utils';
import { BYTES_REGEXP } from './utils'; import { BYTES_REGEXP } from './utils';
interface Params { interface Params {
argType: SmartContractMethodArgType; argType: string;
argTypeMatchInt: MatchInt | null; argTypeMatchInt: MatchInt | null;
isOptional: boolean; isOptional: boolean;
} }
......
import _set from 'lodash/set'; import _set from 'lodash/set';
import type { SmartContractMethodArgType, SmartContractMethodInput } from 'types/api/contract'; import type { ContractAbiItemInput } from '../types';
export type ContractMethodFormFields = Record<string, string | boolean | undefined>; export type ContractMethodFormFields = Record<string, string | boolean | undefined>;
...@@ -11,22 +11,22 @@ export const BYTES_REGEXP = /^bytes(\d+)?$/i; ...@@ -11,22 +11,22 @@ export const BYTES_REGEXP = /^bytes(\d+)?$/i;
export const ARRAY_REGEXP = /^(.*)\[(\d*)\]$/; export const ARRAY_REGEXP = /^(.*)\[(\d*)\]$/;
export interface MatchArray { export interface MatchArray {
itemType: SmartContractMethodArgType; itemType: string;
size: number; size: number;
isNested: boolean; isNested: boolean;
} }
export const matchArray = (argType: SmartContractMethodArgType): MatchArray | null => { export const matchArray = (argType: string): MatchArray | null => {
const match = argType.match(ARRAY_REGEXP); const match = argType.match(ARRAY_REGEXP);
if (!match) { if (!match) {
return null; return null;
} }
const [ , itemType, size ] = match; const [ , itemType, size ] = match;
const isNested = Boolean(matchArray(itemType as SmartContractMethodArgType)); const isNested = Boolean(matchArray(itemType));
return { return {
itemType: itemType as SmartContractMethodArgType, itemType,
size: size ? Number(size) : Infinity, size: size ? Number(size) : Infinity,
isNested, isNested,
}; };
...@@ -39,7 +39,7 @@ export interface MatchInt { ...@@ -39,7 +39,7 @@ export interface MatchInt {
max: bigint; max: bigint;
} }
export const matchInt = (argType: SmartContractMethodArgType): MatchInt | null => { export const matchInt = (argType: string): MatchInt | null => {
const match = argType.match(INT_REGEXP); const match = argType.match(INT_REGEXP);
if (!match) { if (!match) {
return null; return null;
...@@ -51,9 +51,9 @@ export const matchInt = (argType: SmartContractMethodArgType): MatchInt | null = ...@@ -51,9 +51,9 @@ export const matchInt = (argType: SmartContractMethodArgType): MatchInt | null =
return { isUnsigned: Boolean(isUnsigned), power, min, max }; 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 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 childrenInternalType = arrayMatchInternalType?.itemType.replaceAll('struct ', '');
const postfix = childrenInternalType ? ' ' + childrenInternalType : ''; const postfix = childrenInternalType ? ' ' + childrenInternalType : '';
...@@ -97,7 +97,7 @@ function filterOurEmptyItems(array: Array<unknown>): Array<unknown> { ...@@ -97,7 +97,7 @@ function filterOurEmptyItems(array: Array<unknown>): Array<unknown> {
.filter((item) => item !== undefined); .filter((item) => item !== undefined);
} }
export function getFieldLabel(input: SmartContractMethodInput, isRequired?: boolean) { export function getFieldLabel(input: ContractAbiItemInput, isRequired?: boolean) {
const name = input.name || input.internalType || '<unnamed argument>'; const name = input.name || input.internalType || '<unnamed argument>';
return `${ name } (${ input.type })${ isRequired ? '*' : '' }`; return `${ name } (${ input.type })${ isRequired ? '*' : '' }`;
} }
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<ContractAbiItem>;
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<unknown>, submitType: MethodCallStrategy | undefined) => Promise<FormSubmitResult>;
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<unknown>;
isProxy: boolean;
isCustomAbi: boolean;
addressHash: string;
}
export default function useCallMethodApi(): (params: Params) => Promise<FormSubmitResult> {
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 ]);
}
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<unknown>;
addressHash: string;
}
export default function useCallMethodWalletClient(): (params: Params) => Promise<FormSubmitResult> {
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 ]);
}
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;
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<number>) => 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 ]);
}
export const getNativeCoinValue = (value: unknown) => {
if (typeof value !== 'string') {
return BigInt(0);
}
return BigInt(value);
};
import { Alert, Flex } from '@chakra-ui/react';
import { useRouter } from 'next/router'; import { useRouter } from 'next/router';
import React from 'react'; import React from 'react';
import type { SmartContractReadMethod, SmartContractQueryMethodRead } from 'types/api/contract'; import config from 'configs/app';
import useApiFetch from 'lib/api/useApiFetch';
import useApiQuery from 'lib/api/useApiQuery'; import useApiQuery from 'lib/api/useApiQuery';
import getQueryParamString from 'lib/router/getQueryParamString'; 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 ContentLoader from 'ui/shared/ContentLoader';
import DataFetchAlert from 'ui/shared/DataFetchAlert'; import DataFetchAlert from 'ui/shared/DataFetchAlert';
import ContractAbi from './ABI/ContractAbi';
import ContractConnectWallet from './ContractConnectWallet'; import ContractConnectWallet from './ContractConnectWallet';
import ContractCustomAbiAlert from './ContractCustomAbiAlert'; import ContractCustomAbiAlert from './ContractCustomAbiAlert';
import ContractImplementationAddress from './ContractImplementationAddress'; import ContractImplementationAddress from './ContractImplementationAddress';
import ContractMethodConstant from './ContractMethodConstant';
import ContractReadResult from './ContractReadResult';
import ContractMethodForm from './methodForm/ContractMethodForm';
import useWatchAccount from './useWatchAccount';
interface Props { interface Props {
isLoading?: boolean; isLoading?: boolean;
} }
const ContractRead = ({ isLoading }: Props) => { const ContractRead = ({ isLoading }: Props) => {
const apiFetch = useApiFetch(); const { address } = useAccount();
const account = useWatchAccount();
const router = useRouter(); const router = useRouter();
const tab = getQueryParamString(router.query.tab); const tab = getQueryParamString(router.query.tab);
...@@ -37,55 +30,13 @@ const ContractRead = ({ isLoading }: Props) => { ...@@ -37,55 +30,13 @@ const ContractRead = ({ isLoading }: Props) => {
pathParams: { hash: addressHash }, pathParams: { hash: addressHash },
queryParams: { queryParams: {
is_custom_abi: isCustomAbi ? 'true' : 'false', is_custom_abi: isCustomAbi ? 'true' : 'false',
from: account?.address, from: address,
}, },
queryOptions: { queryOptions: {
enabled: !isLoading, enabled: !isLoading,
}, },
}); });
const handleMethodFormSubmit = React.useCallback(async(item: SmartContractReadMethod, args: Array<unknown>) => {
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 <Alert status="error" fontSize="sm" wordBreak="break-word">{ item.error }</Alert>;
}
if (item.outputs?.some(({ value }) => value !== undefined && value !== null)) {
return (
<Flex flexDir="column" rowGap={ 1 }>
{ item.outputs.map((output, index) => <ContractMethodConstant key={ index } data={ output }/>) }
</Flex>
);
}
return (
<ContractMethodForm
key={ id + '_' + index }
data={ item }
onSubmit={ handleMethodFormSubmit }
resultComponent={ ContractReadResult }
methodType="read"
/>
);
}, [ handleMethodFormSubmit ]);
if (isError) { if (isError) {
return <DataFetchAlert/>; return <DataFetchAlert/>;
} }
...@@ -101,9 +52,9 @@ const ContractRead = ({ isLoading }: Props) => { ...@@ -101,9 +52,9 @@ const ContractRead = ({ isLoading }: Props) => {
return ( return (
<> <>
{ isCustomAbi && <ContractCustomAbiAlert/> } { isCustomAbi && <ContractCustomAbiAlert/> }
{ account && <ContractConnectWallet/> } { config.features.blockchainInteraction.isEnabled && <ContractConnectWallet/> }
{ isProxy && <ContractImplementationAddress hash={ addressHash }/> } { isProxy && <ContractImplementationAddress hash={ addressHash }/> }
<ContractMethodsAccordion data={ data } addressHash={ addressHash } renderItemContent={ renderItemContent } tab={ tab }/> <ContractAbi data={ data } addressHash={ addressHash } tab={ tab } methodType="read"/>
</> </>
); );
}; };
......
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 (
<Alert status="error" mt={ 3 } p={ 4 } borderRadius="md" fontSize="sm" wordBreak="break-word" whiteSpace="pre-wrap">
{ children }
</Alert>
);
};
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 (
<>
<p>
<chakra.span fontWeight={ 500 }> { structName }</chakra.span>
<span> ({ output.type }) :</span>
</p>
{ argNames.map((argName, argIndex) => {
return (
<p key={ argName }>
<chakra.span fontWeight={ 500 }> { argName }</chakra.span>
<span>{ argTypes?.[argIndex] ? ` (${ argTypes[argIndex] })` : '' } : { String(output.value[argIndex]) }</span>
</p>
);
}) }
</>
);
}
return (
<p>
<span> </span>
{ name && <chakra.span fontWeight={ 500 }>{ name } </chakra.span> }
<span>({ output.type }) : { String(output.value) }</span>
</p>
);
};
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 <ContractReadResultError>{ result.statusText }</ContractReadResultError>;
}
if (result.is_error) {
if ('error' in result.result) {
return <ContractReadResultError>{ result.result.error }</ContractReadResultError>;
}
if ('message' in result.result) {
return <ContractReadResultError>[{ result.result.code }] { result.result.message }</ContractReadResultError>;
}
if ('raw' in result.result) {
return <ContractReadResultError>{ `Revert reason: ${ hexToUtf8(result.result.raw) }` }</ContractReadResultError>;
}
if ('method_id' in result.result) {
return <ContractReadResultError>{ JSON.stringify(result.result, undefined, 2) }</ContractReadResultError>;
}
return <ContractReadResultError>Something went wrong.</ContractReadResultError>;
}
return (
<Box mt={ 3 } p={ 4 } borderRadius="md" bgColor={ resultBgColor } fontSize="sm" whiteSpace="break-spaces" wordBreak="break-all">
<p>
[ <chakra.span fontWeight={ 500 }>{ 'name' in item ? item.name : '' }</chakra.span> method response ]
</p>
<p>[</p>
{ result.result.output.map((output, index) => <ContractReadResultItem key={ index } output={ output } name={ result.result.names[index] }/>) }
<p>]</p>
</Box>
);
};
export default React.memo(ContractReadResult);
import { useRouter } from 'next/router'; import { useRouter } from 'next/router';
import React from 'react'; import React from 'react';
import { useAccount, useWalletClient, useSwitchChain } from 'wagmi';
import type { SmartContractWriteMethod } from 'types/api/contract';
import config from 'configs/app'; import config from 'configs/app';
import useApiQuery from 'lib/api/useApiQuery'; import useApiQuery from 'lib/api/useApiQuery';
import getQueryParamString from 'lib/router/getQueryParamString'; import getQueryParamString from 'lib/router/getQueryParamString';
import ContractMethodsAccordion from 'ui/address/contract/ContractMethodsAccordion';
import ContentLoader from 'ui/shared/ContentLoader'; import ContentLoader from 'ui/shared/ContentLoader';
import DataFetchAlert from 'ui/shared/DataFetchAlert'; import DataFetchAlert from 'ui/shared/DataFetchAlert';
import ContractAbi from './ABI/ContractAbi';
import ContractConnectWallet from './ContractConnectWallet'; import ContractConnectWallet from './ContractConnectWallet';
import ContractCustomAbiAlert from './ContractCustomAbiAlert'; import ContractCustomAbiAlert from './ContractCustomAbiAlert';
import ContractImplementationAddress from './ContractImplementationAddress'; import ContractImplementationAddress from './ContractImplementationAddress';
import ContractWriteResult from './ContractWriteResult';
import ContractMethodForm from './methodForm/ContractMethodForm';
import useContractAbi from './useContractAbi';
import { getNativeCoinValue, prepareAbi } from './utils';
interface Props { interface Props {
isLoading?: boolean; isLoading?: boolean;
} }
const ContractWrite = ({ isLoading }: Props) => { const ContractWrite = ({ isLoading }: Props) => {
const { data: walletClient } = useWalletClient();
const { isConnected, chainId } = useAccount();
const { switchChainAsync } = useSwitchChain();
const router = useRouter(); const router = useRouter();
const tab = getQueryParamString(router.query.tab); const tab = getQueryParamString(router.query.tab);
...@@ -46,63 +35,6 @@ const ContractWrite = ({ isLoading }: Props) => { ...@@ -46,63 +35,6 @@ const ContractWrite = ({ isLoading }: Props) => {
}, },
}); });
const contractAbi = useContractAbi({ addressHash, isProxy, isCustomAbi });
const handleMethodFormSubmit = React.useCallback(async(item: SmartContractWriteMethod, args: Array<unknown>) => {
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 (
<ContractMethodForm
key={ id + '_' + index }
data={ item }
onSubmit={ handleMethodFormSubmit }
resultComponent={ ContractWriteResult }
methodType="write"
/>
);
}, [ handleMethodFormSubmit ]);
if (isError) { if (isError) {
return <DataFetchAlert/>; return <DataFetchAlert/>;
} }
...@@ -118,9 +50,9 @@ const ContractWrite = ({ isLoading }: Props) => { ...@@ -118,9 +50,9 @@ const ContractWrite = ({ isLoading }: Props) => {
return ( return (
<> <>
{ isCustomAbi && <ContractCustomAbiAlert/> } { isCustomAbi && <ContractCustomAbiAlert/> }
<ContractConnectWallet/> { config.features.blockchainInteraction.isEnabled && <ContractConnectWallet/> }
{ isProxy && <ContractImplementationAddress hash={ addressHash }/> } { isProxy && <ContractImplementationAddress hash={ addressHash }/> }
<ContractMethodsAccordion data={ data } addressHash={ addressHash } renderItemContent={ renderItemContent } tab={ tab }/> <ContractAbi data={ data } addressHash={ addressHash } tab={ tab } methodType="write"/>
</> </>
); );
}; };
......
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<SmartContractWriteMethod>) => {
const txHash = result && 'hash' in result ? result.hash as `0x${ string }` : undefined;
const txInfo = useWaitForTransactionReceipt({
hash: txHash,
});
return <ContractWriteResultDumb result={ result as ContractMethodWriteResult } onSettle={ onSettle } txInfo={ txInfo }/>;
};
export default React.memo(ContractWriteResult) as typeof ContractWriteResult;
import type { ContractMethodCallResult } from '../types';
import type { SmartContractMethod } from 'types/api/contract';
export interface ResultComponentProps<T extends SmartContractMethod> {
item: T;
result: ContractMethodCallResult<T>;
onSettle: () => void;
}
import type { SmartContractQueryMethodRead, SmartContractMethod, SmartContractReadMethod } from 'types/api/contract';
import type { ResourceError } from 'lib/api/resources';
export type MethodFormFields = Record<string, string | Array<string>>;
export type MethodFormFieldsFormatted = Record<string, MethodArgType>;
export type MethodArgType = string | boolean | Array<MethodArgType>;
export type ContractMethodReadResult = SmartContractQueryMethodRead | ResourceError;
export type ContractMethodWriteResult = Error | { hash: `0x${ string }` | undefined } | undefined;
export type ContractMethodCallResult<T extends SmartContractMethod> =
T extends SmartContractReadMethod ? ContractMethodReadResult : ContractMethodWriteResult;
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<Address>(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 ]);
}
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;
}
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]);
});
});
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;
}
...@@ -7,9 +7,10 @@ export interface Props { ...@@ -7,9 +7,10 @@ export interface Props {
text: string; text: string;
className?: string; className?: string;
isLoading?: boolean; 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 { hasCopied, onCopy } = useClipboard(text, 1000);
const [ copied, setCopied ] = useState(false); const [ copied, setCopied ] = useState(false);
// have to implement controlled tooltip because of the issue - https://github.com/chakra-ui/chakra-ui/issues/7107 // 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) => { ...@@ -24,6 +25,11 @@ const CopyToClipboard = ({ text, className, isLoading }: Props) => {
} }
}, [ hasCopied ]); }, [ hasCopied ]);
const handleClick = React.useCallback((event: React.MouseEvent) => {
onCopy();
onClick?.(event);
}, [ onClick, onCopy ]);
if (isLoading) { if (isLoading) {
return <Skeleton boxSize={ 5 } className={ className } borderRadius="sm" flexShrink={ 0 } ml={ 2 } display="inline-block"/>; return <Skeleton boxSize={ 5 } className={ className } borderRadius="sm" flexShrink={ 0 } ml={ 2 } display="inline-block"/>;
} }
...@@ -39,7 +45,7 @@ const CopyToClipboard = ({ text, className, isLoading }: Props) => { ...@@ -39,7 +45,7 @@ const CopyToClipboard = ({ text, className, isLoading }: Props) => {
variant="simple" variant="simple"
display="inline-block" display="inline-block"
flexShrink={ 0 } flexShrink={ 0 }
onClick={ onCopy } onClick={ handleClick }
className={ className } className={ className }
onMouseEnter={ onOpen } onMouseEnter={ onOpen }
onMouseLeave={ onClose } onMouseLeave={ onClose }
......
...@@ -52,8 +52,11 @@ const TabsWithScroll = ({ ...@@ -52,8 +52,11 @@ const TabsWithScroll = ({
}, [ tabs ]); }, [ tabs ]);
const handleTabChange = React.useCallback((index: number) => { const handleTabChange = React.useCallback((index: number) => {
if (isLoading) {
return;
}
onTabChange ? onTabChange(index) : setActiveTabIndex(index); onTabChange ? onTabChange(index) : setActiveTabIndex(index);
}, [ onTabChange ]); }, [ isLoading, onTabChange ]);
useEffect(() => { useEffect(() => {
if (defaultTabIndex !== undefined) { if (defaultTabIndex !== undefined) {
......
import { Flex, chakra } from '@chakra-ui/react'; import { Flex, chakra } from '@chakra-ui/react';
import dynamic from 'next/dynamic'; import dynamic from 'next/dynamic';
import React from 'react'; import React from 'react';
import { useAccount } from 'wagmi';
import useIsMobile from 'lib/hooks/useIsMobile'; import useIsMobile from 'lib/hooks/useIsMobile';
import useAccount from 'lib/web3/useAccount';
import Web3ModalProvider from '../Web3ModalProvider';
const GetitAdPlugin = dynamic(() => import('getit-sdk').then(module => module.GetitAdPlugin), { ssr: false }); const GetitAdPlugin = dynamic(() => import('getit-sdk').then(module => module.GetitAdPlugin), { ssr: false });
const GETIT_API_KEY = 'ZmGXVvwYUAW4yXL8RzWQHNKmpSyQmt3TDXsXUxqFqXPdoaiSSFyca3BOyunDcWdyOwTkX3UVVQel28qbjoOoWPxYVpPdNzbUNkAHyFyJX7Lk9TVcPDZKTQmwHlSMzO3a'; const GETIT_API_KEY = 'ZmGXVvwYUAW4yXL8RzWQHNKmpSyQmt3TDXsXUxqFqXPdoaiSSFyca3BOyunDcWdyOwTkX3UVVQel28qbjoOoWPxYVpPdNzbUNkAHyFyJX7Lk9TVcPDZKTQmwHlSMzO3a';
const GetitBannerContent = ({ address, className }: { address?: string; className?: string }) => { const GetitBanner = ({ className }: { className?: string }) => {
const isMobile = Boolean(useIsMobile()); const isMobile = Boolean(useIsMobile());
const { address } = useAccount();
return ( return (
<Flex className={ className } h="90px"> <Flex className={ className } h="90px">
...@@ -27,22 +26,4 @@ const GetitBannerContent = ({ address, className }: { address?: string; classNam ...@@ -27,22 +26,4 @@ const GetitBannerContent = ({ address, className }: { address?: string; classNam
); );
}; };
const GetitBannerWithWalletAddress = ({ className }: { className?: string }) => {
const { address } = useAccount();
return <GetitBannerContent address={ address } className={ className }/>;
};
const GetitBanner = ({ className }: { className?: string }) => {
const fallback = React.useCallback(() => {
return <GetitBannerContent className={ className }/>;
}, [ className ]);
return (
<Web3ModalProvider fallback={ fallback }>
<GetitBannerWithWalletAddress className={ className }/>
</Web3ModalProvider>
);
};
export default chakra(GetitBanner); export default chakra(GetitBanner);
...@@ -2,15 +2,22 @@ import { Flex, chakra } from '@chakra-ui/react'; ...@@ -2,15 +2,22 @@ import { Flex, chakra } from '@chakra-ui/react';
import { Banner, setWalletAddresses } from '@hypelab/sdk-react'; import { Banner, setWalletAddresses } from '@hypelab/sdk-react';
import Script from 'next/script'; import Script from 'next/script';
import React from 'react'; import React from 'react';
import { useAccount } from 'wagmi';
import Web3ModalProvider from '../Web3ModalProvider'; import useAccount from 'lib/web3/useAccount';
import { hypeInit } from './hypeBannerScript'; import { hypeInit } from './hypeBannerScript';
const DESKTOP_BANNER_SLUG = 'b1559fc3e7'; const DESKTOP_BANNER_SLUG = 'b1559fc3e7';
const MOBILE_BANNER_SLUG = '668ed80a9e'; 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 ( return (
<> <>
...@@ -28,28 +35,4 @@ const HypeBannerContent = ({ className }: { className?: string }) => { ...@@ -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 <HypeBannerContent className={ className }/>;
};
const HypeBanner = ({ className }: { className?: string }) => {
const fallback = React.useCallback(() => {
return <HypeBannerContent className={ className }/>;
}, [ className ]);
return (
<Web3ModalProvider fallback={ fallback }>
<HypeBannerWithWalletAddress className={ className }/>
</Web3ModalProvider>
);
};
export default chakra(HypeBanner); export default chakra(HypeBanner);
import { Divider, Flex, Skeleton, VStack } from '@chakra-ui/react'; import { Divider, Flex, Skeleton, VStack } from '@chakra-ui/react';
import React from 'react'; import React from 'react';
import Tag from 'ui/shared/chakra/Tag';
interface Props { interface Props {
methodId: string; methodId: string;
methodCall: string; methodCall: string;
isLoading?: boolean; 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 ( return (
<Flex <Flex
columnGap={ 5 } columnGap={ 5 }
...@@ -19,7 +21,7 @@ const Item = ({ label, text, isLoading }: { label: string; text: string; isLoadi ...@@ -19,7 +21,7 @@ const Item = ({ label, text, isLoading }: { label: string; text: string; isLoadi
<Skeleton fontWeight={ 600 } w={{ base: 'auto', lg: '80px' }} flexShrink={ 0 } isLoaded={ !isLoading }> <Skeleton fontWeight={ 600 } w={{ base: 'auto', lg: '80px' }} flexShrink={ 0 } isLoaded={ !isLoading }>
{ label } { label }
</Skeleton > </Skeleton >
<Skeleton isLoaded={ !isLoading } whiteSpace="pre-wrap">{ text }</Skeleton> { children }
</Flex> </Flex>
); );
}; };
...@@ -32,8 +34,12 @@ const LogDecodedInputDataHeader = ({ methodId, methodCall, isLoading }: Props) = ...@@ -32,8 +34,12 @@ const LogDecodedInputDataHeader = ({ methodId, methodCall, isLoading }: Props) =
fontSize="sm" fontSize="sm"
lineHeight={ 5 } lineHeight={ 5 }
> >
<Item label="Method id" text={ methodId } isLoading={ isLoading }/> <Item label="Method id" isLoading={ isLoading }>
<Item label="Call" text={ methodCall } isLoading={ isLoading }/> <Tag isLoading={ isLoading }>{ methodId }</Tag>
</Item>
<Item label="Call" isLoading={ isLoading }>
<Skeleton isLoaded={ !isLoading } whiteSpace="pre-wrap">{ methodCall }</Skeleton>
</Item>
</VStack> </VStack>
); );
}; };
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment