Commit b5d6ec96 authored by tom goriunov's avatar tom goriunov Committed by GitHub

Support custom MUD system ABI (#2296)

* MUD system tab placeholder

* implement MUD system selector and fix proxy contract read/write

* rollback changes of target contract address
parent 9337b034
......@@ -67,6 +67,8 @@ import type {
SmartContract,
SmartContractVerificationConfigRaw,
SmartContractSecurityAudits,
SmartContractMudSystemsResponse,
SmartContractMudSystemInfo,
} from 'types/api/contract';
import type { VerifiedContractsResponse, VerifiedContractsFilters, VerifiedContractsCounters } from 'types/api/contracts';
import type {
......@@ -767,6 +769,16 @@ export const RESOURCES = {
pathParams: [ 'hash' as const, 'table_id' as const, 'record_id' as const ],
},
contract_mud_systems: {
path: '/api/v2/mud/worlds/:hash/systems',
pathParams: [ 'hash' as const ],
},
contract_mud_system_info: {
path: '/api/v2/mud/worlds/:hash/systems/:system_address',
pathParams: [ 'hash' as const, 'system_address' as const ],
},
// arbitrum L2
arbitrum_l2_messages: {
path: '/api/v2/arbitrum/messages/:direction',
......@@ -1195,6 +1207,8 @@ Q extends 'address_mud_tables' ? AddressMudTables :
Q extends 'address_mud_tables_count' ? number :
Q extends 'address_mud_records' ? AddressMudRecords :
Q extends 'address_mud_record' ? AddressMudRecord :
Q extends 'contract_mud_systems' ? SmartContractMudSystemsResponse :
Q extends 'contract_mud_system_info' ? SmartContractMudSystemInfo :
Q extends 'address_epoch_rewards' ? AddressEpochRewardsResponse :
Q extends 'withdrawals' ? WithdrawalsResponse :
Q extends 'withdrawals_counters' ? WithdrawalsCounters :
......
......@@ -10,9 +10,11 @@ import useSocketChannel from 'lib/socket/useSocketChannel';
import * as stubs from 'stubs/contract';
import ContractCode from 'ui/address/contract/ContractCode';
import ContractMethodsCustom from 'ui/address/contract/methods/ContractMethodsCustom';
import ContractMethodsMudSystem from 'ui/address/contract/methods/ContractMethodsMudSystem';
import ContractMethodsProxy from 'ui/address/contract/methods/ContractMethodsProxy';
import ContractMethodsRegular from 'ui/address/contract/methods/ContractMethodsRegular';
import { divideAbiIntoMethodTypes } from 'ui/address/contract/methods/utils';
import ContentLoader from 'ui/shared/ContentLoader';
const CONTRACT_TAB_IDS = [
'contract_code',
......@@ -24,6 +26,7 @@ const CONTRACT_TAB_IDS = [
'write_contract_rpc',
'write_proxy',
'write_custom_methods',
'mud_system',
] as const;
interface ContractTab {
......@@ -37,7 +40,7 @@ interface ReturnType {
isLoading: boolean;
}
export default function useContractTabs(data: Address | undefined, isPlaceholderData: boolean): ReturnType {
export default function useContractTabs(data: Address | undefined, isPlaceholderData: boolean, hasMudTab?: boolean): ReturnType {
const [ isQueryEnabled, setIsQueryEnabled ] = React.useState(false);
const router = useRouter();
......@@ -65,6 +68,15 @@ export default function useContractTabs(data: Address | undefined, isPlaceholder
},
});
const mudSystemsQuery = useApiQuery('contract_mud_systems', {
pathParams: { hash: data?.hash },
queryOptions: {
enabled: isEnabled && isQueryEnabled && hasMudTab,
refetchOnMount: false,
placeholderData: stubs.MUD_SYSTEMS,
},
});
const channel = useSocketChannel({
topic: `addresses:${ data?.hash?.toLowerCase() }`,
isDisabled: !isEnabled,
......@@ -136,8 +148,26 @@ export default function useContractTabs(data: Address | undefined, isPlaceholder
/>
),
},
hasMudTab && {
id: 'mud_system' as const,
title: 'MUD System',
component: mudSystemsQuery.isPlaceholderData ?
<ContentLoader/> :
<ContractMethodsMudSystem items={ mudSystemsQuery.data?.items ?? [] }/>,
},
].filter(Boolean),
isLoading: contractQuery.isPlaceholderData,
};
}, [ contractQuery, channel, data?.hash, verifiedImplementations, methods.read, methods.write, methodsCustomAbi.read, methodsCustomAbi.write ]);
}, [
contractQuery,
channel,
data?.hash,
methods.read,
methods.write,
methodsCustomAbi.read,
methodsCustomAbi.write,
verifiedImplementations,
mudSystemsQuery,
hasMudTab,
]);
}
import type { SmartContract } from 'types/api/contract';
import type { SmartContract, SmartContractMudSystemsResponse } from 'types/api/contract';
import type { VerifiedContract, VerifiedContractsCounters } from 'types/api/contracts';
import type { SolidityScanReport } from 'lib/solidityScan/schema';
import { ADDRESS_PARAMS } from './addressParams';
import { ADDRESS_PARAMS, ADDRESS_HASH } from './addressParams';
export const CONTRACT_CODE_UNVERIFIED = {
creation_bytecode: '0x60806040526e',
......@@ -98,3 +98,12 @@ export const SOLIDITY_SCAN_REPORT: SolidityScanReport = {
scanner_reference_url: 'https://solidityscan.com/quickscan/0xc1EF7811FF2ebFB74F80ed7423f2AdAA37454be2/blockscout/eth-goerli?ref=blockscout',
},
};
export const MUD_SYSTEMS: SmartContractMudSystemsResponse = {
items: [
{
name: 'sy.AccessManagement',
address: ADDRESS_HASH,
},
],
};
......@@ -143,3 +143,19 @@ export type SmartContractSecurityAuditSubmission = {
'audit_publish_date': string;
'comment'?: string;
}
// MUD SYSTEM
export interface SmartContractMudSystemsResponse {
items: Array<SmartContractMudSystemItem>;
}
export interface SmartContractMudSystemItem {
address: string;
name: string;
}
export interface SmartContractMudSystemInfo {
name: string;
abi: Abi;
}
......@@ -12,9 +12,10 @@ interface Props {
abi: Array<SmartContractMethod>;
addressHash: string;
tab: string;
sourceAddress?: string;
}
const ContractAbi = ({ abi, addressHash, tab }: Props) => {
const ContractAbi = ({ abi, addressHash, sourceAddress, tab }: Props) => {
const [ expandedSections, setExpandedSections ] = React.useState<Array<number>>(abi.length === 1 ? [ 0 ] : []);
const [ id, setId ] = React.useState(0);
......@@ -61,6 +62,7 @@ const ContractAbi = ({ abi, addressHash, tab }: Props) => {
id={ id }
index={ index }
addressHash={ addressHash }
sourceAddress={ sourceAddress }
tab={ tab }
onSubmit={ handleFormSubmit }
/>
......
......@@ -19,11 +19,12 @@ interface Props {
index: number;
id: number;
addressHash: string;
sourceAddress?: string;
tab: string;
onSubmit: FormSubmitHandler;
}
const ContractAbiItem = ({ data, index, id, addressHash, tab, onSubmit }: Props) => {
const ContractAbiItem = ({ data, index, id, addressHash, sourceAddress, tab, onSubmit }: Props) => {
const [ attempt, setAttempt ] = React.useState(0);
const url = React.useMemo(() => {
......@@ -36,10 +37,11 @@ const ContractAbiItem = ({ data, index, id, addressHash, tab, onSubmit }: Props)
query: {
hash: addressHash ?? '',
tab,
...(sourceAddress ? { source_address: sourceAddress } : {}),
},
hash: data.method_id,
});
}, [ addressHash, data, tab ]);
}, [ addressHash, data, tab, sourceAddress ]);
const handleCopyLinkClick = React.useCallback((event: React.MouseEvent) => {
event.stopPropagation();
......
......@@ -14,9 +14,10 @@ interface Props {
isLoading?: boolean;
isError?: boolean;
type: MethodType;
sourceAddress?: string;
}
const ContractMethods = ({ abi, isLoading, isError, type }: Props) => {
const ContractMethods = ({ abi, isLoading, isError, type, sourceAddress }: Props) => {
const router = useRouter();
......@@ -32,10 +33,11 @@ const ContractMethods = ({ abi, isLoading, isError, type }: Props) => {
}
if (abi.length === 0) {
return <span>No public { type } functions were found for this contract.</span>;
const typeText = type === 'all' ? '' : type;
return <span>No public { typeText } functions were found for this contract.</span>;
}
return <ContractAbi abi={ abi } tab={ tab } addressHash={ addressHash }/>;
return <ContractAbi abi={ abi } tab={ tab } addressHash={ addressHash } sourceAddress={ sourceAddress }/>;
};
export default React.memo(ContractMethods);
import { Box } from '@chakra-ui/react';
import { useRouter } from 'next/router';
import React from 'react';
import type { SmartContractMudSystemItem } from 'types/api/contract';
import useApiQuery from 'lib/api/useApiQuery';
import getQueryParamString from 'lib/router/getQueryParamString';
import ContractConnectWallet from './ContractConnectWallet';
import ContractMethods from './ContractMethods';
import type { Item } from './ContractSourceAddressSelector';
import ContractSourceAddressSelector from './ContractSourceAddressSelector';
import { enrichWithMethodId, isMethod } from './utils';
interface Props {
items: Array<SmartContractMudSystemItem>;
}
const ContractMethodsMudSystem = ({ items }: Props) => {
const router = useRouter();
const addressHash = getQueryParamString(router.query.hash);
const contractAddress = getQueryParamString(router.query.source_address);
const [ selectedItem, setSelectedItem ] = React.useState(items.find((item) => item.address === contractAddress) || items[0]);
const systemInfoQuery = useApiQuery('contract_mud_system_info', {
pathParams: { hash: addressHash, system_address: selectedItem.address },
queryOptions: {
enabled: Boolean(selectedItem?.address),
refetchOnMount: false,
},
});
const handleItemSelect = React.useCallback((item: Item) => {
setSelectedItem(item as SmartContractMudSystemItem);
}, []);
if (items.length === 0) {
return <span>No MUD System found for this contract.</span>;
}
const abi = systemInfoQuery.data?.abi?.filter(isMethod).map(enrichWithMethodId) || [];
return (
<Box>
<ContractConnectWallet/>
<ContractSourceAddressSelector
items={ items }
selectedItem={ selectedItem }
onItemSelect={ handleItemSelect }
label="System address"
/>
<ContractMethods
key={ selectedItem.address }
abi={ abi }
isLoading={ systemInfoQuery.isPending }
isError={ systemInfoQuery.isError }
sourceAddress={ selectedItem.address }
type="all"
/>
</Box>
);
};
export default React.memo(ContractMethodsMudSystem);
import { Box } from '@chakra-ui/react';
import { useRouter } from 'next/router';
import React from 'react';
import type { MethodType } from './types';
import type { AddressImplementation } from 'types/api/addressParams';
import useApiQuery from 'lib/api/useApiQuery';
import getQueryParamString from 'lib/router/getQueryParamString';
import ContractConnectWallet from './ContractConnectWallet';
import ContractImplementationAddress from './ContractImplementationAddress';
import ContractMethods from './ContractMethods';
import { isReadMethod, isWriteMethod } from './utils';
import ContractSourceAddressSelector from './ContractSourceAddressSelector';
import { enrichWithMethodId, isReadMethod, isWriteMethod } from './utils';
interface Props {
type: MethodType;
......@@ -18,8 +20,10 @@ interface Props {
}
const ContractMethodsProxy = ({ type, implementations, isLoading: isInitialLoading }: Props) => {
const router = useRouter();
const contractAddress = getQueryParamString(router.query.source_address);
const [ selectedItem, setSelectedItem ] = React.useState(implementations[0]);
const [ selectedItem, setSelectedItem ] = React.useState(implementations.find((item) => item.address === contractAddress) || implementations[0]);
const contractQuery = useApiQuery('contract', {
pathParams: { hash: selectedItem.address },
......@@ -29,29 +33,24 @@ const ContractMethodsProxy = ({ type, implementations, isLoading: isInitialLoadi
},
});
const handleItemSelect = React.useCallback((event: React.ChangeEvent<HTMLSelectElement>) => {
const nextOption = implementations.find(({ address }) => address === event.target.value);
if (nextOption) {
setSelectedItem(nextOption);
}
}, [ implementations ]);
const abi = contractQuery.data?.abi?.filter(type === 'read' ? isReadMethod : isWriteMethod) || [];
const abi = contractQuery.data?.abi?.filter(type === 'read' ? isReadMethod : isWriteMethod).map(enrichWithMethodId) || [];
return (
<Box>
<ContractConnectWallet isLoading={ isInitialLoading }/>
<ContractImplementationAddress
implementations={ implementations }
<ContractSourceAddressSelector
items={ implementations }
selectedItem={ selectedItem }
onItemSelect={ handleItemSelect }
onItemSelect={ setSelectedItem }
isLoading={ isInitialLoading }
label="Implementation address"
/>
<ContractMethods
key={ selectedItem.address }
abi={ abi }
isLoading={ isInitialLoading || contractQuery.isPending }
isError={ contractQuery.isError }
sourceAddress={ selectedItem.address }
type={ type }
/>
</Box>
......
import { Flex, Select, Skeleton } from '@chakra-ui/react';
import React from 'react';
import type { AddressImplementation } from 'types/api/addressParams';
import { route } from 'nextjs-routes';
import CopyToClipboard from 'ui/shared/CopyToClipboard';
import AddressEntity from 'ui/shared/entities/address/AddressEntity';
import LinkNewTab from 'ui/shared/links/LinkNewTab';
export interface Item {
address: string;
name?: string | null | undefined;
}
interface Props {
selectedItem: AddressImplementation;
onItemSelect: (event: React.ChangeEvent<HTMLSelectElement>) => void;
implementations: Array<AddressImplementation>;
label: string;
selectedItem: Item;
onItemSelect: (item: Item) => void;
items: Array<Item>;
isLoading?: boolean;
}
const ContractImplementationAddress = ({ selectedItem, onItemSelect, implementations, isLoading }: Props) => {
const ContractSourceAddressSelector = ({ selectedItem, onItemSelect, items, isLoading, label }: Props) => {
const handleItemSelect = React.useCallback((event: React.ChangeEvent<HTMLSelectElement>) => {
const nextOption = items.find(({ address }) => address === event.target.value);
if (nextOption) {
onItemSelect(nextOption);
}
}, [ items, onItemSelect ]);
if (isLoading) {
return <Skeleton mb={ 6 } h={ 6 } w={{ base: '300px', lg: '500px' }}/>;
}
if (implementations.length === 0) {
if (items.length === 0) {
return null;
}
if (implementations.length === 1) {
if (items.length === 1) {
return (
<Flex mb={ 6 } flexWrap="wrap" columnGap={ 2 } rowGap={ 2 }>
<span>Implementation address:</span>
<Flex mb={ 6 } flexWrap="wrap" columnGap={ 3 } rowGap={ 2 }>
<span>{ label }</span>
<AddressEntity
address={{ hash: implementations[0].address, is_contract: true, is_verified: true }}
address={{ hash: items[0].address, is_contract: true, is_verified: true }}
/>
</Flex>
);
}
return (
<Flex mb={ 6 } flexWrap="wrap" columnGap={ 2 } rowGap={ 2 } alignItems="center">
<span>Implementation address:</span>
<Flex mb={ 6 } flexWrap="wrap" columnGap={ 3 } rowGap={ 2 } alignItems="center">
<span>{ label }</span>
<Select
size="xs"
value={ selectedItem.address }
onChange={ onItemSelect }
onChange={ handleItemSelect }
w="auto"
fontWeight={ 600 }
borderRadius="base"
>
{ implementations.map((implementation) => (
<option key={ implementation.address } value={ implementation.address }>
{ implementation.name }
{ items.map((item) => (
<option key={ item.address } value={ item.address }>
{ item.name }
</option>
)) }
</Select>
<CopyToClipboard text={ selectedItem.address } ml={ 1 }/>
<LinkNewTab
label="Open contract details page in new tab"
href={ route({ pathname: '/address/[hash]', query: { hash: selectedItem.address, tab: 'contract' } }) }
/>
<Flex columnGap={ 2 } alignItems="center">
<CopyToClipboard text={ selectedItem.address } ml={ 0 }/>
<LinkNewTab
label="Open contract details page in new tab"
href={ route({ pathname: '/address/[hash]', query: { hash: selectedItem.address, tab: 'contract' } }) }
/>
</Flex>
</Flex>
);
};
export default React.memo(ContractImplementationAddress);
export default React.memo(ContractSourceAddressSelector);
......@@ -2,7 +2,7 @@ import type { AbiFunction, AbiFallback, AbiReceive } from 'abitype';
export type ContractAbiItemInput = AbiFunction['inputs'][number] & { fieldType?: 'native_coin' };
export type MethodType = 'read' | 'write';
export type MethodType = 'read' | 'write' | 'all';
export type MethodCallStrategy = 'read' | 'write' | 'simulate';
export type ResultViewMode = 'preview' | 'result';
......
import type { Abi } from 'abitype';
import type { Abi, AbiFallback, AbiReceive } from 'abitype';
import type { AbiFunction } from 'viem';
import { toFunctionSelector } from 'viem';
import type { SmartContractMethodCustomFields, SmartContractMethodRead, SmartContractMethodWrite } from './types';
import type { SmartContractMethod, SmartContractMethodRead, SmartContractMethodWrite } from './types';
export const getNativeCoinValue = (value: unknown) => {
if (typeof value !== 'string') {
......@@ -17,6 +17,9 @@ interface DividedAbi {
write: Array<SmartContractMethodWrite>;
}
export const isMethod = (method: Abi[number]): method is SmartContractMethod =>
(method.type === 'function' || method.type === 'fallback' || method.type === 'receive');
export const isReadMethod = (method: Abi[number]): method is SmartContractMethodRead =>
method.type === 'function' && (
method.constant || method.stateMutability === 'view' || method.stateMutability === 'pure'
......@@ -26,13 +29,19 @@ export const isWriteMethod = (method: Abi[number]): method is SmartContractMetho
(method.type === 'function' || method.type === 'fallback' || method.type === 'receive') &&
!isReadMethod(method);
const enrichWithMethodId = (method: AbiFunction): SmartContractMethodCustomFields => {
export const enrichWithMethodId = (method: AbiFunction | AbiFallback | AbiReceive): SmartContractMethod => {
if (method.type !== 'function') {
return method;
}
try {
return {
...method,
method_id: toFunctionSelector(method).slice(2),
};
} catch (error) {
return {
...method,
is_invalid: true,
};
}
......@@ -42,22 +51,9 @@ export function divideAbiIntoMethodTypes(abi: Abi): DividedAbi {
return {
read: abi
.filter(isReadMethod)
.map((method) => ({
...method,
...enrichWithMethodId(method),
})),
.map(enrichWithMethodId) as Array<SmartContractMethodRead>,
write: abi
.filter(isWriteMethod)
.map((method) => {
if (method.type !== 'function') {
return method;
}
return {
...method,
...enrichWithMethodId(method),
};
}),
.map(enrichWithMethodId) as Array<SmartContractMethodWrite>,
};
}
......@@ -138,7 +138,11 @@ const AddressPageContent = () => {
const isSafeAddress = useIsSafeAddress(!addressQuery.isPlaceholderData && addressQuery.data?.is_contract ? hash : undefined);
const safeIconColor = useColorModeValue('black', 'white');
const contractTabs = useContractTabs(addressQuery.data, addressQuery.isPlaceholderData);
const contractTabs = useContractTabs(
addressQuery.data,
config.features.mudFramework.isEnabled ? (mudTablesCountQuery.isPlaceholderData || addressQuery.isPlaceholderData) : addressQuery.isPlaceholderData,
Boolean(config.features.mudFramework.isEnabled && mudTablesCountQuery.data && mudTablesCountQuery.data > 0),
);
const tabs: Array<RoutedTab> = React.useMemo(() => {
return [
......
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