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 [