diff --git a/lib/socket/types.ts b/lib/socket/types.ts index 5674dd1449..1a611706bd 100644 --- a/lib/socket/types.ts +++ b/lib/socket/types.ts @@ -32,7 +32,9 @@ SocketMessage.AddressTxsPending | SocketMessage.AddressTokenTransfer | SocketMessage.AddressChangedBytecode | SocketMessage.AddressFetchedBytecode | +SocketMessage.EthBytecodeDbLookupStarted | SocketMessage.SmartContractWasVerified | +SocketMessage.SmartContractWasNotVerified | SocketMessage.TokenTransfers | SocketMessage.TokenTotalSupply | SocketMessage.TokenInstanceMetadataFetched | @@ -71,7 +73,9 @@ export namespace SocketMessage { export type AddressTokenTransfer = SocketMessageParamsGeneric<'token_transfer', { token_transfers: Array }>; export type AddressChangedBytecode = SocketMessageParamsGeneric<'changed_bytecode', Record>; export type AddressFetchedBytecode = SocketMessageParamsGeneric<'fetched_bytecode', { fetched_bytecode: string }>; + export type EthBytecodeDbLookupStarted = SocketMessageParamsGeneric<'eth_bytecode_db_lookup_started', Record>; export type SmartContractWasVerified = SocketMessageParamsGeneric<'smart_contract_was_verified', Record>; + export type SmartContractWasNotVerified = SocketMessageParamsGeneric<'smart_contract_was_not_verified', Record>; export type TokenTransfers = SocketMessageParamsGeneric<'token_transfer', { token_transfer: number }>; export type TokenTotalSupply = SocketMessageParamsGeneric<'total_supply', { total_supply: number }>; export type TokenInstanceMetadataFetched = SocketMessageParamsGeneric<'fetched_token_instance_metadata', TokenInstanceMetadataSocketMessage>; diff --git a/playwright/fixtures/socketServer.ts b/playwright/fixtures/socketServer.ts index 19e94f878a..58278726ff 100644 --- a/playwright/fixtures/socketServer.ts +++ b/playwright/fixtures/socketServer.ts @@ -73,7 +73,9 @@ export function sendMessage(socket: WebSocket, channel: Channel, msg: 'verificat export function sendMessage(socket: WebSocket, channel: Channel, msg: 'total_supply', payload: { total_supply: number }): void; export function sendMessage(socket: WebSocket, channel: Channel, msg: 'changed_bytecode', payload: Record): void; export function sendMessage(socket: WebSocket, channel: Channel, msg: 'fetched_bytecode', payload: { fetched_bytecode: string }): void; +export function sendMessage(socket: WebSocket, channel: Channel, msg: 'eth_bytecode_db_lookup_started', payload: Record): void; export function sendMessage(socket: WebSocket, channel: Channel, msg: 'smart_contract_was_verified', payload: Record): void; +export function sendMessage(socket: WebSocket, channel: Channel, msg: 'smart_contract_was_not_verified', payload: Record): void; export function sendMessage(socket: WebSocket, channel: Channel, msg: 'token_transfer', payload: { token_transfers: Array }): void; export function sendMessage(socket: WebSocket, channel: Channel, msg: 'fetched_token_instance_metadata', payload: TokenInstanceMetadataSocketMessage): void; export function sendMessage(socket: WebSocket, channel: Channel, msg: string, payload: unknown): void { diff --git a/public/static/merits/activity_pass.svg b/public/static/merits/activity_pass.svg index 5a07fce84c..8e84d0fbc2 100644 --- a/public/static/merits/activity_pass.svg +++ b/public/static/merits/activity_pass.svg @@ -8,17 +8,17 @@ - + - + - + - + @@ -30,55 +30,55 @@ - + - + - + - + - + - + - + - - + + - - + + - + - + - + - - + + - - + + - + - + diff --git a/public/static/merits/cells.svg b/public/static/merits/cells.svg index 85adb070c9..0966422555 100644 --- a/public/static/merits/cells.svg +++ b/public/static/merits/cells.svg @@ -1,5 +1,5 @@ - + @@ -7,7 +7,7 @@ - + @@ -28,115 +28,115 @@ - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + diff --git a/public/static/merits/cells_dark.svg b/public/static/merits/cells_dark.svg index 35c24ec262..74dfb63c7f 100644 --- a/public/static/merits/cells_dark.svg +++ b/public/static/merits/cells_dark.svg @@ -1,5 +1,5 @@ - + @@ -7,7 +7,7 @@ - + @@ -28,115 +28,115 @@ - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + diff --git a/toolkit/components/AdaptiveTabs/AdaptiveTabs.tsx b/toolkit/components/AdaptiveTabs/AdaptiveTabs.tsx index 5cce45715a..62815f1a04 100644 --- a/toolkit/components/AdaptiveTabs/AdaptiveTabs.tsx +++ b/toolkit/components/AdaptiveTabs/AdaptiveTabs.tsx @@ -42,10 +42,6 @@ const AdaptiveTabs = (props: Props) => { } }, [ defaultValue ]); - if (tabs.length === 1) { - return
{ tabs[0].component }
; - } - return ( { const activeTabIndex = tabsList.findIndex((tab) => getTabValue(tab) === activeTab) ?? 0; useScrollToActiveTab({ activeTabIndex, listRef, tabsRefs, isMobile, isLoading }); + if (tabs.length === 1 && !leftSlot && !rightSlot) { + return null; + } + const isReady = !isLoading && tabsCut !== undefined; return ( @@ -133,7 +137,7 @@ const AdaptiveTabsList = (props: Props) => { ) } - { tabsList.map((tab, index) => { + { tabs.length > 1 && tabsList.map((tab, index) => { const value = getTabValue(tab); const ref = tabsRefs[index]; diff --git a/ui/address/AddressContract.pw.tsx b/ui/address/AddressContract.pw.tsx index 965a3dadcf..bfbb321ec1 100644 --- a/ui/address/AddressContract.pw.tsx +++ b/ui/address/AddressContract.pw.tsx @@ -7,27 +7,27 @@ 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'; +import AddressContract from './AddressContract'; const hash = addressMock.contract.hash; -test.beforeEach(async({ mockApiResponse }) => { - await mockApiResponse('general:address', addressMock.contract, { pathParams: { hash } }); - await mockApiResponse( - 'general:contract', - { ...contractInfoMock.verified, abi: [ ...contractMethodsMock.read, ...contractMethodsMock.write ] }, - { pathParams: { hash } }, - ); -}); - test.describe('ABI functionality', () => { + test.beforeEach(async({ mockApiResponse }) => { + await mockApiResponse('general:address', addressMock.contract, { pathParams: { hash } }); + await mockApiResponse( + 'general:contract', + { ...contractInfoMock.verified, abi: [ ...contractMethodsMock.read, ...contractMethodsMock.write ] }, + { pathParams: { hash } }, + ); + }); + test('read', async({ render, createSocket }) => { const hooksConfig = { router: { query: { hash, tab: 'read_contract' }, }, }; - const component = await render(, { hooksConfig }, { withSocket: true }); + const component = await render(, { hooksConfig }, { withSocket: true }); const socket = await createSocket(); await socketServer.joinChannel(socket, 'addresses:' + addressMock.contract.hash.toLowerCase()); @@ -43,7 +43,7 @@ test.describe('ABI functionality', () => { }, }; await mockEnvs(ENVS_MAP.noWalletClient); - const component = await render(, { hooksConfig }, { withSocket: true }); + const component = await render(, { hooksConfig }, { withSocket: true }); const socket = await createSocket(); await socketServer.joinChannel(socket, 'addresses:' + addressMock.contract.hash.toLowerCase()); @@ -58,7 +58,7 @@ test.describe('ABI functionality', () => { query: { hash, tab: 'write_contract' }, }, }; - const component = await render(, { hooksConfig }, { withSocket: true }); + const component = await render(, { hooksConfig }, { withSocket: true }); const socket = await createSocket(); await socketServer.joinChannel(socket, 'addresses:' + addressMock.contract.hash.toLowerCase()); @@ -80,7 +80,7 @@ test.describe('ABI functionality', () => { }; await mockEnvs(ENVS_MAP.noWalletClient); - const component = await render(, { hooksConfig }, { withSocket: true }); + const component = await render(, { hooksConfig }, { withSocket: true }); const socket = await createSocket(); await socketServer.joinChannel(socket, 'addresses:' + addressMock.contract.hash.toLowerCase()); @@ -94,3 +94,65 @@ test.describe('ABI functionality', () => { await expect(component.getByLabel('5.').getByRole('button', { name: 'Write' })).toBeDisabled(); }); }); + +test.describe('auto verification status', () => { + const addressData = { ...addressMock.contract, is_verified: false, implementations: [] }; + let contractApiUrl: string; + + test.beforeEach(async({ mockApiResponse }) => { + await mockApiResponse('general:address', addressData, { pathParams: { hash } }); + contractApiUrl = await mockApiResponse('general:contract', contractInfoMock.nonVerified, { pathParams: { hash } }); + }); + + test('base flow', async({ render, createSocket }) => { + const hooksConfig = { + router: { + query: { hash, tab: 'contract' }, + }, + }; + const component = await render(, { hooksConfig }, { withSocket: true }); + + const socket = await createSocket(); + const channel = await socketServer.joinChannel(socket, 'addresses:' + addressData.hash.toLowerCase()); + + socketServer.sendMessage(socket, channel, 'eth_bytecode_db_lookup_started', { }); + const tabs = component.getByRole('tablist').first(); + await expect(tabs).toHaveScreenshot(); + }); + + test('after verification will refetch contract data', async({ page, render, createSocket }) => { + const hooksConfig = { + router: { + query: { hash, tab: 'contract' }, + }, + }; + await render(, { hooksConfig }, { withSocket: true }); + + const socket = await createSocket(); + const channel = await socketServer.joinChannel(socket, 'addresses:' + addressData.hash.toLowerCase()); + + socketServer.sendMessage(socket, channel, 'smart_contract_was_verified', { }); + + const contractRequest = await page.waitForRequest(contractApiUrl); + expect(contractRequest).toBeTruthy(); + }); + + test('with one tab', async({ render, createSocket, mockEnvs }) => { + const hooksConfig = { + router: { + query: { hash, tab: 'contract' }, + }, + }; + await mockEnvs([ + [ 'NEXT_PUBLIC_IS_ACCOUNT_SUPPORTED', 'false' ], + ]); + const component = await render(, { hooksConfig }, { withSocket: true }); + + const socket = await createSocket(); + const channel = await socketServer.joinChannel(socket, 'addresses:' + addressData.hash.toLowerCase()); + + socketServer.sendMessage(socket, channel, 'smart_contract_was_not_verified', { }); + const tabs = component.getByRole('tablist').first(); + await expect(tabs).toHaveScreenshot(); + }); +}); diff --git a/ui/address/AddressContract.pwstory.tsx b/ui/address/AddressContract.pwstory.tsx deleted file mode 100644 index 9b6638a728..0000000000 --- a/ui/address/AddressContract.pwstory.tsx +++ /dev/null @@ -1,18 +0,0 @@ -import { useRouter } from 'next/router'; -import React from 'react'; - -import useApiQuery from 'lib/api/useApiQuery'; -import getQueryParamString from 'lib/router/getQueryParamString'; -import useContractTabs from 'ui/address/contract/useContractTabs'; - -import AddressContract from './AddressContract'; - -const AddressContractPwStory = () => { - const router = useRouter(); - const hash = getQueryParamString(router.query.hash); - const addressQuery = useApiQuery('general:address', { pathParams: { hash } }); - const { tabs } = useContractTabs(addressQuery.data, false); - return ; -}; - -export default AddressContractPwStory; diff --git a/ui/address/AddressContract.tsx b/ui/address/AddressContract.tsx index 84c3337e8b..ea36c561f5 100644 --- a/ui/address/AddressContract.tsx +++ b/ui/address/AddressContract.tsx @@ -1,22 +1,100 @@ +import { useQueryClient } from '@tanstack/react-query'; +import { useRouter } from 'next/router'; import React from 'react'; -import type { TabItemRegular } from 'toolkit/components/AdaptiveTabs/types'; +import type { SocketMessage } from 'lib/socket/types'; +import type { Address } from 'types/api/address'; +import { getResourceKey } from 'lib/api/useApiQuery'; +import delay from 'lib/delay'; +import useIsMobile from 'lib/hooks/useIsMobile'; +import getQueryParamString from 'lib/router/getQueryParamString'; +import useSocketChannel from 'lib/socket/useSocketChannel'; +import useSocketMessage from 'lib/socket/useSocketMessage'; import RoutedTabs from 'toolkit/components/RoutedTabs/RoutedTabs'; +import { SECOND } from 'toolkit/utils/consts'; + +import type { TContractAutoVerificationStatus } from './contract/ContractAutoVerificationStatus'; +import ContractAutoVerificationStatus from './contract/ContractAutoVerificationStatus'; +import useContractTabs from './contract/useContractTabs'; +import { CONTRACT_TAB_IDS } from './contract/utils'; interface Props { - tabs: Array; - isLoading: boolean; - shouldRender?: boolean; + addressData: Address | undefined; + isLoading?: boolean; + hasMudTab?: boolean; } -const AddressContract = ({ tabs, isLoading, shouldRender }: Props) => { - if (!shouldRender) { +const AddressContract = ({ addressData, isLoading = false, hasMudTab }: Props) => { + const [ isQueryEnabled, setIsQueryEnabled ] = React.useState(false); + const [ autoVerificationStatus, setAutoVerificationStatus ] = React.useState(null); + + const router = useRouter(); + const queryClient = useQueryClient(); + const isMobile = useIsMobile(); + const enableQuery = React.useCallback(() => { + setIsQueryEnabled(true); + }, []); + + const tab = getQueryParamString(router.query.tab); + const isSocketEnabled = Boolean(addressData?.hash) && addressData?.is_contract && !isLoading && CONTRACT_TAB_IDS.concat('contract' as never).includes(tab); + + const channel = useSocketChannel({ + topic: `addresses:${ addressData?.hash?.toLowerCase() }`, + isDisabled: !isSocketEnabled, + onJoin: enableQuery, + onSocketError: enableQuery, + }); + + const contractTabs = useContractTabs({ + addressData, + isEnabled: isQueryEnabled, + hasMudTab, + channel, + }); + + const handleLookupStartedMessage: SocketMessage.EthBytecodeDbLookupStarted['handler'] = React.useCallback(() => { + setAutoVerificationStatus('pending'); + }, []); + + const handleContractWasVerifiedMessage: SocketMessage.SmartContractWasVerified['handler'] = React.useCallback(async() => { + setAutoVerificationStatus('success'); + await queryClient.refetchQueries({ + queryKey: getResourceKey('general:address', { pathParams: { hash: addressData?.hash } }), + }); + await queryClient.refetchQueries({ + queryKey: getResourceKey('general:contract', { pathParams: { hash: addressData?.hash } }), + }); + setAutoVerificationStatus(null); + }, [ addressData?.hash, queryClient ]); + + const handleContractWasNotVerifiedMessage: SocketMessage.SmartContractWasNotVerified['handler'] = React.useCallback(async() => { + setAutoVerificationStatus('failed'); + await delay(10 * SECOND); + setAutoVerificationStatus(null); + }, []); + + useSocketMessage({ channel, event: 'eth_bytecode_db_lookup_started', handler: handleLookupStartedMessage }); + useSocketMessage({ channel, event: 'smart_contract_was_verified', handler: handleContractWasVerifiedMessage }); + useSocketMessage({ channel, event: 'smart_contract_was_not_verified', handler: handleContractWasNotVerifiedMessage }); + + if (isLoading) { return null; } + const rightSlot = autoVerificationStatus ? + 1 ? 'tooltip' : 'inline' }/> : + null; + return ( - + 1 ? { base: 'auto', md: 6 } : 0 }} + /> ); }; diff --git a/ui/address/__screenshots__/AddressContract.pw.tsx_default_auto-verification-status-base-flow-1.png b/ui/address/__screenshots__/AddressContract.pw.tsx_default_auto-verification-status-base-flow-1.png new file mode 100644 index 0000000000..f5aa9f944d Binary files /dev/null and b/ui/address/__screenshots__/AddressContract.pw.tsx_default_auto-verification-status-base-flow-1.png differ diff --git a/ui/address/__screenshots__/AddressContract.pw.tsx_default_auto-verification-status-with-one-tab-1.png b/ui/address/__screenshots__/AddressContract.pw.tsx_default_auto-verification-status-with-one-tab-1.png new file mode 100644 index 0000000000..aa8af788c3 Binary files /dev/null and b/ui/address/__screenshots__/AddressContract.pw.tsx_default_auto-verification-status-with-one-tab-1.png differ diff --git a/ui/address/contract/ContractAutoVerificationStatus.tsx b/ui/address/contract/ContractAutoVerificationStatus.tsx new file mode 100644 index 0000000000..7f1db01a66 --- /dev/null +++ b/ui/address/contract/ContractAutoVerificationStatus.tsx @@ -0,0 +1,40 @@ +import { Box, HStack, Spinner } from '@chakra-ui/react'; +import React from 'react'; + +import { Tooltip } from 'toolkit/chakra/tooltip'; +import IconSvg from 'ui/shared/IconSvg'; + +const STATUS_MAP = { + pending: { + text: 'Checking contract verification', + leftElement: , + }, + success: { + text: 'Contract successfully verified', + leftElement: , + }, + failed: { + text: 'Contract not verified automatically. Please verify manually.', + leftElement: , + }, +}; + +export type TContractAutoVerificationStatus = keyof typeof STATUS_MAP; + +interface Props { + status: TContractAutoVerificationStatus; + mode?: 'inline' | 'tooltip'; +} + +const ContractAutoVerificationStatus = ({ status, mode = 'inline' }: Props) => { + return ( + + + { STATUS_MAP[status].leftElement } + { STATUS_MAP[status].text } + + + ); +}; + +export default React.memo(ContractAutoVerificationStatus); diff --git a/ui/address/contract/ContractDetails.pw.tsx b/ui/address/contract/ContractDetails.pw.tsx index 19a1cdaee4..30347337bc 100644 --- a/ui/address/contract/ContractDetails.pw.tsx +++ b/ui/address/contract/ContractDetails.pw.tsx @@ -18,13 +18,11 @@ const hooksConfig = { // test cases which use socket cannot run in parallel since the socket server always run on the same port test.describe.configure({ mode: 'serial' }); -let addressApiUrl: string; - test.beforeEach(async({ mockApiResponse, page }) => { await page.route('https://cdn.jsdelivr.net/npm/monaco-editor@0.33.0/**', (route) => { route.abort(); }); - addressApiUrl = await mockApiResponse('general:address', addressMock.contract, { pathParams: { hash: addressMock.contract.hash } }); + await mockApiResponse('general:address', addressMock.contract, { pathParams: { hash: addressMock.contract.hash } }); }); test.describe('full view', () => { @@ -103,19 +101,6 @@ test.describe('mobile view', () => { }); }); -test('verified via lookup in eth_bytecode_db', async({ render, mockApiResponse, createSocket, page }) => { - const contractApiUrl = await mockApiResponse('general:contract', contractMock.nonVerified, { pathParams: { hash: addressMock.contract.hash } }); - await render(, { hooksConfig }, { withSocket: true }); - - const socket = await createSocket(); - const channel = await socketServer.joinChannel(socket, `addresses:${ addressMock.contract.hash.toLowerCase() }`); - await page.waitForResponse(contractApiUrl); - socketServer.sendMessage(socket, channel, 'smart_contract_was_verified', {}); - const request = await page.waitForRequest(addressApiUrl); - - expect(request).toBeTruthy(); -}); - test('verified with multiple sources', async({ render, page, mockApiResponse, createSocket }) => { await mockApiResponse('general:contract', contractMock.withMultiplePaths, { pathParams: { hash: addressMock.contract.hash } }); await render(, { hooksConfig }, { withSocket: true }); diff --git a/ui/address/contract/ContractDetails.tsx b/ui/address/contract/ContractDetails.tsx index 05c85263f1..314ba43189 100644 --- a/ui/address/contract/ContractDetails.tsx +++ b/ui/address/contract/ContractDetails.tsx @@ -1,19 +1,16 @@ import { Box } from '@chakra-ui/react'; import type { UseQueryResult } from '@tanstack/react-query'; -import { useQueryClient } from '@tanstack/react-query'; import { useRouter } from 'next/router'; import type { Channel } from 'phoenix'; import React from 'react'; -import type { SocketMessage } from 'lib/socket/types'; import type { Address } from 'types/api/address'; import type { AddressImplementation } from 'types/api/addressParams'; import type { SmartContract } from 'types/api/contract'; import type { ResourceError } from 'lib/api/resources'; -import useApiQuery, { getResourceKey } from 'lib/api/useApiQuery'; +import useApiQuery from 'lib/api/useApiQuery'; import getQueryParamString from 'lib/router/getQueryParamString'; -import useSocketMessage from 'lib/socket/useSocketMessage'; import * as stubs from 'stubs/contract'; import RoutedTabs from 'toolkit/components/RoutedTabs/RoutedTabs'; import DataFetchAlert from 'ui/shared/DataFetchAlert'; @@ -36,8 +33,6 @@ const ContractDetails = ({ addressData, channel, mainContractQuery }: Props) => const router = useRouter(); const sourceAddress = getQueryParamString(router.query.source_address); - const queryClient = useQueryClient(); - const sourceItems: Array = React.useMemo(() => { const currentAddressDefaultName = addressData?.proxy_type === 'eip7702' ? 'Current address' : 'Current contract'; const currentAddressItem = { address_hash: addressData.hash, name: addressData?.name || currentAddressDefaultName }; @@ -56,30 +51,15 @@ const ContractDetails = ({ addressData, channel, mainContractQuery }: Props) => const contractQuery = useApiQuery('general:contract', { pathParams: { hash: selectedItem?.address_hash }, queryOptions: { - enabled: Boolean(selectedItem?.address_hash && !mainContractQuery.isPlaceholderData), + enabled: Boolean(selectedItem?.address_hash && !mainContractQuery.isPlaceholderData && selectedItem.address_hash !== addressData.hash), refetchOnMount: false, placeholderData: addressData?.is_verified ? stubs.CONTRACT_CODE_VERIFIED : stubs.CONTRACT_CODE_UNVERIFIED, }, }); - const { data, isPlaceholderData, isError } = contractQuery; + const { data, isPlaceholderData, isError } = selectedItem.address_hash !== addressData.hash ? contractQuery : mainContractQuery; const tabs = useContractDetailsTabs({ data, isLoading: isPlaceholderData, addressData, sourceAddress: selectedItem.address_hash }); - const handleContractWasVerifiedMessage: SocketMessage.SmartContractWasVerified['handler'] = React.useCallback(() => { - queryClient.refetchQueries({ - queryKey: getResourceKey('general:address', { pathParams: { hash: addressData.hash } }), - }); - queryClient.refetchQueries({ - queryKey: getResourceKey('general:contract', { pathParams: { hash: addressData.hash } }), - }); - }, [ addressData.hash, queryClient ]); - - useSocketMessage({ - channel, - event: 'smart_contract_was_verified', - handler: handleContractWasVerifiedMessage, - }); - if (isError) { return ; } diff --git a/ui/address/contract/__screenshots__/ContractDetails.pw.tsx_default_verified-with-multiple-sources-2.png b/ui/address/contract/__screenshots__/ContractDetails.pw.tsx_default_verified-with-multiple-sources-2.png index 28408db572..8addc38247 100644 Binary files a/ui/address/contract/__screenshots__/ContractDetails.pw.tsx_default_verified-with-multiple-sources-2.png and b/ui/address/contract/__screenshots__/ContractDetails.pw.tsx_default_verified-with-multiple-sources-2.png differ diff --git a/ui/address/contract/specs/ContractDetails.tsx b/ui/address/contract/specs/ContractDetails.tsx index 9bbec706b1..9e683c6ea4 100644 --- a/ui/address/contract/specs/ContractDetails.tsx +++ b/ui/address/contract/specs/ContractDetails.tsx @@ -2,6 +2,7 @@ import { useRouter } from 'next/router'; import useApiQuery from 'lib/api/useApiQuery'; import getQueryParamString from 'lib/router/getQueryParamString'; +import useSocketChannel from 'lib/socket/useSocketChannel'; import useContractTabs from '../useContractTabs'; @@ -9,7 +10,12 @@ const ContractDetails = () => { const router = useRouter(); const hash = getQueryParamString(router.query.hash); const addressQuery = useApiQuery('general:address', { pathParams: { hash } }); - const { tabs } = useContractTabs(addressQuery.data, false); + const channel = useSocketChannel({ + topic: `addresses:${ hash?.toLowerCase() }`, + isDisabled: !addressQuery.data, + }); + + const { tabs } = useContractTabs({ addressData: addressQuery.data, isEnabled: true, channel }); const content = tabs.find(({ id }) => id === 'contract_code')?.component; return content ?? null; }; diff --git a/ui/address/contract/useContractTabs.tsx b/ui/address/contract/useContractTabs.tsx index 4c5102c951..fb9d9505d4 100644 --- a/ui/address/contract/useContractTabs.tsx +++ b/ui/address/contract/useContractTabs.tsx @@ -1,12 +1,10 @@ -import { useRouter } from 'next/router'; +import type { Channel } from 'phoenix'; import React from 'react'; import type { Address } from 'types/api/address'; import config from 'configs/app'; import useApiQuery from 'lib/api/useApiQuery'; -import getQueryParamString from 'lib/router/getQueryParamString'; -import useSocketChannel from 'lib/socket/useSocketChannel'; import * as stubs from 'stubs/contract'; import ContractDetails from 'ui/address/contract/ContractDetails'; import ContractMethodsCustom from 'ui/address/contract/methods/ContractMethodsCustom'; @@ -16,7 +14,7 @@ import ContractMethodsRegular from 'ui/address/contract/methods/ContractMethodsR import ContentLoader from 'ui/shared/ContentLoader'; import type { CONTRACT_MAIN_TAB_IDS } from './utils'; -import { CONTRACT_DETAILS_TAB_IDS, CONTRACT_TAB_IDS } from './utils'; +import { CONTRACT_DETAILS_TAB_IDS } from './utils'; interface ContractTab { id: typeof CONTRACT_MAIN_TAB_IDS[number] | Array; @@ -30,54 +28,43 @@ interface ReturnType { isLoading: boolean; } -export default function useContractTabs(data: Address | undefined, isPlaceholderData: boolean, hasMudTab: boolean = false): ReturnType { - const [ isQueryEnabled, setIsQueryEnabled ] = React.useState(false); - - const router = useRouter(); - const tab = getQueryParamString(router.query.tab); - - const isEnabled = Boolean(data?.hash) && data?.is_contract && !isPlaceholderData && CONTRACT_TAB_IDS.concat('contract' as never).includes(tab); - - const enableQuery = React.useCallback(() => { - setIsQueryEnabled(true); - }, []); +interface Props { + addressData: Address | undefined; + isEnabled: boolean; + hasMudTab?: boolean; + channel: Channel | undefined; +} +export default function useContractTabs({ addressData, isEnabled, hasMudTab, channel }: Props): ReturnType { const contractQuery = useApiQuery('general:contract', { - pathParams: { hash: data?.hash }, + pathParams: { hash: addressData?.hash }, queryOptions: { - enabled: isEnabled && isQueryEnabled, + enabled: isEnabled, refetchOnMount: false, - placeholderData: data?.is_verified ? stubs.CONTRACT_CODE_VERIFIED : stubs.CONTRACT_CODE_UNVERIFIED, + placeholderData: addressData?.is_verified ? stubs.CONTRACT_CODE_VERIFIED : stubs.CONTRACT_CODE_UNVERIFIED, }, }); const mudSystemsQuery = useApiQuery('general:mud_systems', { - pathParams: { hash: data?.hash }, + pathParams: { hash: addressData?.hash }, queryOptions: { - enabled: isEnabled && isQueryEnabled && hasMudTab, + enabled: isEnabled && hasMudTab, refetchOnMount: false, placeholderData: stubs.MUD_SYSTEMS, }, }); - const channel = useSocketChannel({ - topic: `addresses:${ data?.hash?.toLowerCase() }`, - isDisabled: !isEnabled, - onJoin: enableQuery, - onSocketError: enableQuery, - }); - const verifiedImplementations = React.useMemo(() => { - return data?.implementations?.filter(({ name, address_hash: addressHash }) => name && addressHash && addressHash !== data?.hash) || []; - }, [ data?.hash, data?.implementations ]); + return addressData?.implementations?.filter(({ name, address_hash: addressHash }) => name && addressHash && addressHash !== addressData?.hash) || []; + }, [ addressData?.hash, addressData?.implementations ]); return React.useMemo(() => { return { tabs: [ - data && { + addressData && { id: 'contract_code' as const, title: 'Code', - component: , + component: , subTabs: CONTRACT_DETAILS_TAB_IDS as unknown as Array, }, contractQuery.data?.abi && { @@ -92,7 +79,7 @@ export default function useContractTabs(data: Address | undefined, isPlaceholder ), }, @@ -112,7 +99,7 @@ export default function useContractTabs(data: Address | undefined, isPlaceholder isLoading: contractQuery.isPlaceholderData, }; }, [ - data, + addressData, contractQuery, channel, verifiedImplementations, diff --git a/ui/pages/Address.tsx b/ui/pages/Address.tsx index fb68240dda..dd42f5f023 100644 --- a/ui/pages/Address.tsx +++ b/ui/pages/Address.tsx @@ -34,7 +34,6 @@ import AddressTokenTransfers from 'ui/address/AddressTokenTransfers'; import AddressTxs from 'ui/address/AddressTxs'; import AddressUserOps from 'ui/address/AddressUserOps'; import AddressWithdrawals from 'ui/address/AddressWithdrawals'; -import useContractTabs from 'ui/address/contract/useContractTabs'; import { CONTRACT_TAB_IDS } from 'ui/address/contract/utils'; import AddressFavoriteButton from 'ui/address/details/AddressFavoriteButton'; import AddressMetadataAlert from 'ui/address/details/AddressMetadataAlert'; @@ -144,12 +143,6 @@ const AddressPageContent = () => { const xStarQuery = useFetchXStarScore({ hash }); - 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 [ { @@ -175,9 +168,9 @@ const AddressPageContent = () => { }, component: ( 0) } /> ), subTabs: CONTRACT_TAB_IDS, @@ -266,7 +259,6 @@ const AddressPageContent = () => { ].filter(Boolean); }, [ addressQuery, - contractTabs, addressTabsCountersQuery.data, userOpsAccountQuery.data, isTabsLoading, diff --git a/ui/pages/Token.tsx b/ui/pages/Token.tsx index ba9b0edfdf..d4cc5ca5b9 100644 --- a/ui/pages/Token.tsx +++ b/ui/pages/Token.tsx @@ -23,7 +23,6 @@ import { generateListStub } from 'stubs/utils'; import RoutedTabs from 'toolkit/components/RoutedTabs/RoutedTabs'; import AddressContract from 'ui/address/AddressContract'; import AddressCsvExportLink from 'ui/address/AddressCsvExportLink'; -import useContractTabs from 'ui/address/contract/useContractTabs'; import { CONTRACT_TAB_IDS } from 'ui/address/contract/utils'; import TextAd from 'ui/shared/ad/TextAd'; import IconSvg from 'ui/shared/IconSvg'; @@ -160,7 +159,6 @@ const TokenPageContent = () => { }); const isLoading = tokenQuery.isPlaceholderData || addressQuery.isPlaceholderData; - const contractTabs = useContractTabs(addressQuery.data, addressQuery.isPlaceholderData); const tabs: Array = [ hasInventoryTab ? { @@ -192,7 +190,7 @@ const TokenPageContent = () => { return 'Contract'; }, - component: , + component: , subTabs: CONTRACT_TAB_IDS, } : undefined, ].filter(Boolean);