Skip to content

Contract tab: finding matching bytecode states #2680

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 3 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions lib/socket/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,9 @@ SocketMessage.AddressTxsPending |
SocketMessage.AddressTokenTransfer |
SocketMessage.AddressChangedBytecode |
SocketMessage.AddressFetchedBytecode |
SocketMessage.EthBytecodeDbLookupStarted |
SocketMessage.SmartContractWasVerified |
SocketMessage.SmartContractWasNotVerified |
SocketMessage.TokenTransfers |
SocketMessage.TokenTotalSupply |
SocketMessage.TokenInstanceMetadataFetched |
Expand Down Expand Up @@ -71,7 +73,9 @@ export namespace SocketMessage {
export type AddressTokenTransfer = SocketMessageParamsGeneric<'token_transfer', { token_transfers: Array<TokenTransfer> }>;
export type AddressChangedBytecode = SocketMessageParamsGeneric<'changed_bytecode', Record<string, never>>;
export type AddressFetchedBytecode = SocketMessageParamsGeneric<'fetched_bytecode', { fetched_bytecode: string }>;
export type EthBytecodeDbLookupStarted = SocketMessageParamsGeneric<'eth_bytecode_db_lookup_started', Record<string, never>>;
export type SmartContractWasVerified = SocketMessageParamsGeneric<'smart_contract_was_verified', Record<string, never>>;
export type SmartContractWasNotVerified = SocketMessageParamsGeneric<'smart_contract_was_not_verified', Record<string, never>>;
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>;
Expand Down
2 changes: 2 additions & 0 deletions playwright/fixtures/socketServer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<string, never>): 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<string, never>): void;
export function sendMessage(socket: WebSocket, channel: Channel, msg: 'smart_contract_was_verified', payload: Record<string, never>): void;
export function sendMessage(socket: WebSocket, channel: Channel, msg: 'smart_contract_was_not_verified', payload: Record<string, never>): void;
export function sendMessage(socket: WebSocket, channel: Channel, msg: 'token_transfer', payload: { token_transfers: Array<TokenTransfer> }): 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 {
Expand Down
4 changes: 0 additions & 4 deletions toolkit/components/AdaptiveTabs/AdaptiveTabs.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -43,10 +43,6 @@ const AdaptiveTabs = (props: Props) => {
}
}, [ defaultValue ]);

if (tabs.length === 1) {
return <div>{ tabs[0].component }</div>;
}

return (
<TabsRoot
position="relative"
Expand Down
6 changes: 5 additions & 1 deletion toolkit/components/AdaptiveTabs/AdaptiveTabsList.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,10 @@ const AdaptiveTabsList = (props: Props) => {
const activeTabIndex = tabsList.findIndex((tab) => getTabValue(tab) === activeTab) ?? 0;
useScrollToActiveTab({ activeTabIndex, listRef, tabsRefs, isMobile, isLoading });

if (tabs.length === 1 && !leftSlot && !rightSlot) {
return null;
}

return (
<TabsList
ref={ listRef }
Expand Down Expand Up @@ -134,7 +138,7 @@ const AdaptiveTabsList = (props: Props) => {
</Box>
)
}
{ tabsList.slice(0, isLoading ? 5 : Infinity).map((tab, index) => {
{ tabs.length > 1 && tabsList.slice(0, isLoading ? 5 : Infinity).map((tab, index) => {
const value = getTabValue(tab);
const ref = tabsRefs[index];

Expand Down
90 changes: 76 additions & 14 deletions ui/address/AddressContract.pw.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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('address', addressMock.contract, { pathParams: { hash } });
await mockApiResponse(
'contract',
{ ...contractInfoMock.verified, abi: [ ...contractMethodsMock.read, ...contractMethodsMock.write ] },
{ pathParams: { hash } },
);
});

test.describe('ABI functionality', () => {
test.beforeEach(async({ mockApiResponse }) => {
await mockApiResponse('address', addressMock.contract, { pathParams: { hash } });
await mockApiResponse(
'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(<AddressContract/>, { hooksConfig }, { withSocket: true });
const component = await render(<AddressContract addressData={ addressMock.contract }/>, { hooksConfig }, { withSocket: true });
const socket = await createSocket();
await socketServer.joinChannel(socket, 'addresses:' + addressMock.contract.hash.toLowerCase());

Expand All @@ -43,7 +43,7 @@ test.describe('ABI functionality', () => {
},
};
await mockEnvs(ENVS_MAP.noWalletClient);
const component = await render(<AddressContract/>, { hooksConfig }, { withSocket: true });
const component = await render(<AddressContract addressData={ addressMock.contract }/>, { hooksConfig }, { withSocket: true });
const socket = await createSocket();
await socketServer.joinChannel(socket, 'addresses:' + addressMock.contract.hash.toLowerCase());

Expand All @@ -58,7 +58,7 @@ test.describe('ABI functionality', () => {
query: { hash, tab: 'write_contract' },
},
};
const component = await render(<AddressContract/>, { hooksConfig }, { withSocket: true });
const component = await render(<AddressContract addressData={ addressMock.contract }/>, { hooksConfig }, { withSocket: true });
const socket = await createSocket();
await socketServer.joinChannel(socket, 'addresses:' + addressMock.contract.hash.toLowerCase());

Expand All @@ -80,7 +80,7 @@ test.describe('ABI functionality', () => {
};
await mockEnvs(ENVS_MAP.noWalletClient);

const component = await render(<AddressContract/>, { hooksConfig }, { withSocket: true });
const component = await render(<AddressContract addressData={ addressMock.contract }/>, { hooksConfig }, { withSocket: true });
const socket = await createSocket();
await socketServer.joinChannel(socket, 'addresses:' + addressMock.contract.hash.toLowerCase());

Expand All @@ -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('address', addressData, { pathParams: { hash } });
contractApiUrl = await mockApiResponse('contract', contractInfoMock.nonVerified, { pathParams: { hash } });
});

test('base flow', async({ render, createSocket }) => {
const hooksConfig = {
router: {
query: { hash, tab: 'contract' },
},
};
const component = await render(<AddressContract addressData={ addressData }/>, { 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(<AddressContract addressData={ addressData }/>, { 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(<AddressContract addressData={ addressData }/>, { 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();
});
});
18 changes: 0 additions & 18 deletions ui/address/AddressContract.pwstory.tsx

This file was deleted.

92 changes: 85 additions & 7 deletions ui/address/AddressContract.tsx
Original file line number Diff line number Diff line change
@@ -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 { SECOND } from 'lib/consts';
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 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<TabItemRegular>;
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<TContractAutoVerificationStatus | null>(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('address', { pathParams: { hash: addressData?.hash } }),
});
await queryClient.refetchQueries({
queryKey: getResourceKey('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 ?
<ContractAutoVerificationStatus status={ autoVerificationStatus } mode={ isMobile && contractTabs.tabs.length > 1 ? 'tooltip' : 'inline' }/> :
null;

return (
<RoutedTabs tabs={ tabs } variant="secondary" size="sm" isLoading={ isLoading }/>
<RoutedTabs
tabs={ contractTabs.tabs }
variant="secondary"
size="sm"
isLoading={ contractTabs.isLoading }
rightSlot={ rightSlot }
rightSlotProps={{ ml: contractTabs.tabs.length > 1 ? { base: 'auto', md: 6 } : 0 }}
/>
);
};

Expand Down
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
40 changes: 40 additions & 0 deletions ui/address/contract/ContractAutoVerificationStatus.tsx
Original file line number Diff line number Diff line change
@@ -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: <Spinner size="sm"/>,
},
success: {
text: 'Contract successfully verified',
leftElement: <IconSvg name="verified_slim" boxSize={ 5 } color="green.500"/>,
},
failed: {
text: 'Contract not verified automatically. Please verify manually.',
leftElement: <IconSvg name="status/warning" boxSize={ 5 } color="orange.400"/>,
},
};

export type TContractAutoVerificationStatus = keyof typeof STATUS_MAP;

interface Props {
status: TContractAutoVerificationStatus;
mode?: 'inline' | 'tooltip';
}

const ContractAutoVerificationStatus = ({ status, mode = 'inline' }: Props) => {
return (
<Tooltip content={ STATUS_MAP[status].text } disabled={ mode === 'inline' }>
<HStack gap={ 2 } whiteSpace="pre-wrap">
{ STATUS_MAP[status].leftElement }
<Box display={ mode === 'inline' ? 'inline' : 'none' } textStyle="sm">{ STATUS_MAP[status].text }</Box>
</HStack>
</Tooltip>
);
};

export default React.memo(ContractAutoVerificationStatus);
17 changes: 1 addition & 16 deletions ui/address/contract/ContractDetails.pw.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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/[email protected]/**', (route) => {
route.abort();
});
addressApiUrl = await mockApiResponse('address', addressMock.contract, { pathParams: { hash: addressMock.contract.hash } });
await mockApiResponse('address', addressMock.contract, { pathParams: { hash: addressMock.contract.hash } });
});

test.describe('full view', () => {
Expand Down Expand Up @@ -95,19 +93,6 @@ test.describe('mobile view', () => {
});
});

test('verified via lookup in eth_bytecode_db', async({ render, mockApiResponse, createSocket, page }) => {
const contractApiUrl = await mockApiResponse('contract', contractMock.nonVerified, { pathParams: { hash: addressMock.contract.hash } });
await render(<ContractDetails/>, { 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('contract', contractMock.withMultiplePaths, { pathParams: { hash: addressMock.contract.hash } });
await render(<ContractDetails/>, { hooksConfig }, { withSocket: true });
Expand Down
Loading
Loading