diff --git a/lib/api/resources.ts b/lib/api/resources.ts index e1127d886e..4229430c18 100644 --- a/lib/api/resources.ts +++ b/lib/api/resources.ts @@ -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 : diff --git a/lib/hooks/useContractTabs.tsx b/lib/hooks/useContractTabs.tsx index 24667f69a1..b6408ee7dc 100644 --- a/lib/hooks/useContractTabs.tsx +++ b/lib/hooks/useContractTabs.tsx @@ -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 ? + : + , + }, ].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, + ]); } diff --git a/stubs/contract.ts b/stubs/contract.ts index 67219f8a5b..e821440284 100644 --- a/stubs/contract.ts +++ b/stubs/contract.ts @@ -1,9 +1,9 @@ -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, + }, + ], +}; diff --git a/types/api/contract.ts b/types/api/contract.ts index f2e1ad61d0..f1e937d866 100644 --- a/types/api/contract.ts +++ b/types/api/contract.ts @@ -143,3 +143,19 @@ export type SmartContractSecurityAuditSubmission = { 'audit_publish_date': string; 'comment'?: string; } + +// MUD SYSTEM + +export interface SmartContractMudSystemsResponse { + items: Array; +} + +export interface SmartContractMudSystemItem { + address: string; + name: string; +} + +export interface SmartContractMudSystemInfo { + name: string; + abi: Abi; +} diff --git a/ui/address/contract/methods/ContractAbi.tsx b/ui/address/contract/methods/ContractAbi.tsx index 19e91a1bd1..c3502ccf1e 100644 --- a/ui/address/contract/methods/ContractAbi.tsx +++ b/ui/address/contract/methods/ContractAbi.tsx @@ -12,9 +12,10 @@ interface Props { abi: Array; addressHash: string; tab: string; + sourceAddress?: string; } -const ContractAbi = ({ abi, addressHash, tab }: Props) => { +const ContractAbi = ({ abi, addressHash, sourceAddress, tab }: Props) => { const [ expandedSections, setExpandedSections ] = React.useState>(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 } /> diff --git a/ui/address/contract/methods/ContractAbiItem.tsx b/ui/address/contract/methods/ContractAbiItem.tsx index 9796b12d47..2e97d3d385 100644 --- a/ui/address/contract/methods/ContractAbiItem.tsx +++ b/ui/address/contract/methods/ContractAbiItem.tsx @@ -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(); diff --git a/ui/address/contract/methods/ContractImplementationAddress.tsx b/ui/address/contract/methods/ContractImplementationAddress.tsx deleted file mode 100644 index 6eabc1e86c..0000000000 --- a/ui/address/contract/methods/ContractImplementationAddress.tsx +++ /dev/null @@ -1,66 +0,0 @@ -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'; - -interface Props { - selectedItem: AddressImplementation; - onItemSelect: (event: React.ChangeEvent) => void; - implementations: Array; - isLoading?: boolean; -} - -const ContractImplementationAddress = ({ selectedItem, onItemSelect, implementations, isLoading }: Props) => { - - if (isLoading) { - return ; - } - - if (implementations.length === 0) { - return null; - } - - if (implementations.length === 1) { - return ( - - Implementation address: - - - ); - } - - return ( - - Implementation address: - - - - - ); -}; - -export default React.memo(ContractImplementationAddress); diff --git a/ui/address/contract/methods/ContractMethods.tsx b/ui/address/contract/methods/ContractMethods.tsx index eb7e00ee65..4c13085d98 100644 --- a/ui/address/contract/methods/ContractMethods.tsx +++ b/ui/address/contract/methods/ContractMethods.tsx @@ -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 No public { type } functions were found for this contract.; + const typeText = type === 'all' ? '' : type; + return No public { typeText } functions were found for this contract.; } - return ; + return ; }; export default React.memo(ContractMethods); diff --git a/ui/address/contract/methods/ContractMethodsMudSystem.tsx b/ui/address/contract/methods/ContractMethodsMudSystem.tsx new file mode 100644 index 0000000000..ff8ef75c2b --- /dev/null +++ b/ui/address/contract/methods/ContractMethodsMudSystem.tsx @@ -0,0 +1,68 @@ +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; +} + +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 No MUD System found for this contract.; + } + + const abi = systemInfoQuery.data?.abi?.filter(isMethod).map(enrichWithMethodId) || []; + + return ( + + + + + + ); +}; + +export default React.memo(ContractMethodsMudSystem); diff --git a/ui/address/contract/methods/ContractMethodsProxy.tsx b/ui/address/contract/methods/ContractMethodsProxy.tsx index a6b0337f85..a6f627ea15 100644 --- a/ui/address/contract/methods/ContractMethodsProxy.tsx +++ b/ui/address/contract/methods/ContractMethodsProxy.tsx @@ -1,15 +1,17 @@ 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) => { - 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 ( - diff --git a/ui/address/contract/methods/ContractSourceAddressSelector.tsx b/ui/address/contract/methods/ContractSourceAddressSelector.tsx new file mode 100644 index 0000000000..f8a2853f5f --- /dev/null +++ b/ui/address/contract/methods/ContractSourceAddressSelector.tsx @@ -0,0 +1,79 @@ +import { Flex, Select, Skeleton } from '@chakra-ui/react'; +import React from 'react'; + +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 { + label: string; + selectedItem: Item; + onItemSelect: (item: Item) => void; + items: Array; + isLoading?: boolean; +} + +const ContractSourceAddressSelector = ({ selectedItem, onItemSelect, items, isLoading, label }: Props) => { + + const handleItemSelect = React.useCallback((event: React.ChangeEvent) => { + const nextOption = items.find(({ address }) => address === event.target.value); + if (nextOption) { + onItemSelect(nextOption); + } + }, [ items, onItemSelect ]); + + if (isLoading) { + return ; + } + + if (items.length === 0) { + return null; + } + + if (items.length === 1) { + return ( + + { label } + + + ); + } + + return ( + + { label } + + + + + + + ); +}; + +export default React.memo(ContractSourceAddressSelector); diff --git a/ui/address/contract/methods/__screenshots__/ContractMethodsProxy.pw.tsx_default_with-multiple-implementations-mobile-1.png b/ui/address/contract/methods/__screenshots__/ContractMethodsProxy.pw.tsx_default_with-multiple-implementations-mobile-1.png index 5fc0d17994..6bacbfb3b2 100644 Binary files a/ui/address/contract/methods/__screenshots__/ContractMethodsProxy.pw.tsx_default_with-multiple-implementations-mobile-1.png and b/ui/address/contract/methods/__screenshots__/ContractMethodsProxy.pw.tsx_default_with-multiple-implementations-mobile-1.png differ diff --git a/ui/address/contract/methods/__screenshots__/ContractMethodsProxy.pw.tsx_default_with-one-implementation-mobile-1.png b/ui/address/contract/methods/__screenshots__/ContractMethodsProxy.pw.tsx_default_with-one-implementation-mobile-1.png index d5dcadbc39..33e3c21c93 100644 Binary files a/ui/address/contract/methods/__screenshots__/ContractMethodsProxy.pw.tsx_default_with-one-implementation-mobile-1.png and b/ui/address/contract/methods/__screenshots__/ContractMethodsProxy.pw.tsx_default_with-one-implementation-mobile-1.png differ diff --git a/ui/address/contract/methods/__screenshots__/ContractMethodsProxy.pw.tsx_mobile_with-multiple-implementations-mobile-1.png b/ui/address/contract/methods/__screenshots__/ContractMethodsProxy.pw.tsx_mobile_with-multiple-implementations-mobile-1.png index ce31564e4e..88d93ec490 100644 Binary files a/ui/address/contract/methods/__screenshots__/ContractMethodsProxy.pw.tsx_mobile_with-multiple-implementations-mobile-1.png and b/ui/address/contract/methods/__screenshots__/ContractMethodsProxy.pw.tsx_mobile_with-multiple-implementations-mobile-1.png differ diff --git a/ui/address/contract/methods/__screenshots__/ContractMethodsProxy.pw.tsx_mobile_with-one-implementation-mobile-1.png b/ui/address/contract/methods/__screenshots__/ContractMethodsProxy.pw.tsx_mobile_with-one-implementation-mobile-1.png index a0ea56b320..d4b2df625c 100644 Binary files a/ui/address/contract/methods/__screenshots__/ContractMethodsProxy.pw.tsx_mobile_with-one-implementation-mobile-1.png and b/ui/address/contract/methods/__screenshots__/ContractMethodsProxy.pw.tsx_mobile_with-one-implementation-mobile-1.png differ diff --git a/ui/address/contract/methods/types.ts b/ui/address/contract/methods/types.ts index 1882b77a9f..985ab9571e 100644 --- a/ui/address/contract/methods/types.ts +++ b/ui/address/contract/methods/types.ts @@ -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'; diff --git a/ui/address/contract/methods/utils.ts b/ui/address/contract/methods/utils.ts index a7f2716929..58b523fc63 100644 --- a/ui/address/contract/methods/utils.ts +++ b/ui/address/contract/methods/utils.ts @@ -1,8 +1,8 @@ -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; } +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, write: abi .filter(isWriteMethod) - .map((method) => { - - if (method.type !== 'function') { - return method; - } - - return { - ...method, - ...enrichWithMethodId(method), - }; - }), + .map(enrichWithMethodId) as Array, }; } diff --git a/ui/pages/Address.tsx b/ui/pages/Address.tsx index 96b8708000..191f77e607 100644 --- a/ui/pages/Address.tsx +++ b/ui/pages/Address.tsx @@ -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 = React.useMemo(() => { return [