Commit 37c529bf authored by Igor Stuev's avatar Igor Stuev Committed by GitHub

Merge pull request #729 from blockscout/new-sockets-2

New sockets
parents 2d8cd224 3a2c71fd
......@@ -3,6 +3,7 @@ import type { Channel } from 'phoenix';
import type { AddressCoinBalanceHistoryItem } from 'types/api/address';
import type { NewBlockSocketResponse } from 'types/api/block';
import type { SmartContractVerificationResponse } from 'types/api/contract';
import type { RawTracesResponse } from 'types/api/rawTrace';
import type { TokenTransfer } from 'types/api/tokenTransfer';
import type { Transaction } from 'types/api/transaction';
......@@ -10,6 +11,7 @@ export type SocketMessageParams = SocketMessage.NewBlock |
SocketMessage.BlocksIndexStatus |
SocketMessage.InternalTxsIndexStatus |
SocketMessage.TxStatusUpdate |
SocketMessage.TxRawTrace |
SocketMessage.NewTx |
SocketMessage.NewPendingTx |
SocketMessage.AddressBalance |
......@@ -19,7 +21,9 @@ SocketMessage.AddressCoinBalance |
SocketMessage.AddressTxs |
SocketMessage.AddressTxsPending |
SocketMessage.AddressTokenTransfer |
SocketMessage.AddressChangedBytecode |
SocketMessage.TokenTransfers |
SocketMessage.TokenTotalSupply |
SocketMessage.ContractVerification |
SocketMessage.Unknown;
......@@ -35,6 +39,7 @@ export namespace SocketMessage {
export type BlocksIndexStatus = SocketMessageParamsGeneric<'block_index_status', {finished: boolean; ratio: string}>;
export type InternalTxsIndexStatus = SocketMessageParamsGeneric<'internal_txs_index_status', {finished: boolean; ratio: string}>;
export type TxStatusUpdate = SocketMessageParamsGeneric<'collated', NewBlockSocketResponse>;
export type TxRawTrace = SocketMessageParamsGeneric<'raw_trace', RawTracesResponse>;
export type NewTx = SocketMessageParamsGeneric<'transaction', { transaction: number }>;
export type NewPendingTx = SocketMessageParamsGeneric<'pending_transaction', { pending_transaction: number }>;
export type AddressBalance = SocketMessageParamsGeneric<'balance', { balance: string; block_number: number; exchange_rate: string }>;
......@@ -45,7 +50,9 @@ export namespace SocketMessage {
export type AddressTxs = SocketMessageParamsGeneric<'transaction', { transaction: Transaction }>;
export type AddressTxsPending = SocketMessageParamsGeneric<'pending_transaction', { transaction: Transaction }>;
export type AddressTokenTransfer = SocketMessageParamsGeneric<'token_transfer', { token_transfer: TokenTransfer }>;
export type AddressChangedBytecode = SocketMessageParamsGeneric<'changed_bytecode', Record<string, never>>;
export type TokenTransfers = SocketMessageParamsGeneric<'token_transfer', {token_transfer: number }>;
export type TokenTotalSupply = SocketMessageParamsGeneric<'total_supply', {total_supply: number }>;
export type ContractVerification = SocketMessageParamsGeneric<'verification_result', SmartContractVerificationResponse>;
export type Unknown = SocketMessageParamsGeneric<undefined, unknown>;
}
......@@ -61,6 +61,8 @@ export function sendMessage(socket: WebSocket, channel: Channel, msg: 'transacti
export function sendMessage(socket: WebSocket, channel: Channel, msg: 'pending_transaction', payload: { pending_transaction: number }): void;
export function sendMessage(socket: WebSocket, channel: Channel, msg: 'new_block', payload: NewBlockSocketResponse): void;
export function sendMessage(socket: WebSocket, channel: Channel, msg: 'verification_result', payload: SmartContractVerificationResponse): void;
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: string, payload: unknown): void {
socket.send(JSON.stringify([
...channel,
......
import { test, expect } from '@playwright/experimental-ct-react';
import { test as base, expect } from '@playwright/experimental-ct-react';
import React from 'react';
import * as contractMock from 'mocks/contract/info';
import * as socketServer from 'playwright/fixtures/socketServer';
import TestApp from 'playwright/TestApp';
import buildApiUrl from 'playwright/utils/buildApiUrl';
......@@ -15,6 +16,14 @@ const hooksConfig = {
},
};
const test = base.extend<socketServer.SocketServerFixture>({
createSocket: socketServer.createSocket,
});
// FIXME
// test cases which use socket cannot run in parallel since the socket server always run on the same port
test.describe.configure({ mode: 'serial' });
test('verified with changed byte code +@mobile +@dark-mode', async({ mount, page }) => {
await page.route(CONTRACT_API_URL, (route) => route.fulfill({
status: 200,
......@@ -24,11 +33,32 @@ test('verified with changed byte code +@mobile +@dark-mode', async({ mount, page
const component = await mount(
<TestApp>
<ContractCode addressHash={ addressHash } noSocket/>
</TestApp>,
{ hooksConfig },
);
await expect(component).toHaveScreenshot();
});
test('verified with changed byte code socket', async({ mount, page, createSocket }) => {
await page.route(CONTRACT_API_URL, (route) => route.fulfill({
status: 200,
body: JSON.stringify(contractMock.verified),
}));
await page.route('https://cdn.jsdelivr.net/npm/monaco-editor@0.33.0/**', (route) => route.abort());
const component = await mount(
<TestApp withSocket>
<ContractCode addressHash={ addressHash }/>
</TestApp>,
{ hooksConfig },
);
const socket = await createSocket();
const channel = await socketServer.joinChannel(socket, 'addresses:' + addressHash.toLowerCase());
socketServer.sendMessage(socket, channel, 'changed_bytecode', {});
await expect(component).toHaveScreenshot();
});
......@@ -41,7 +71,7 @@ test('verified with multiple sources +@mobile', async({ mount, page }) => {
await mount(
<TestApp>
<ContractCode addressHash={ addressHash }/>
<ContractCode addressHash={ addressHash } noSocket/>
</TestApp>,
{ hooksConfig },
);
......@@ -60,7 +90,7 @@ test('verified via sourcify', async({ mount, page }) => {
await mount(
<TestApp>
<ContractCode addressHash={ addressHash }/>
<ContractCode addressHash={ addressHash } noSocket/>
</TestApp>,
{ hooksConfig },
);
......@@ -77,7 +107,7 @@ test('self destructed', async({ mount, page }) => {
await mount(
<TestApp>
<ContractCode addressHash={ addressHash }/>
<ContractCode addressHash={ addressHash } noSocket/>
</TestApp>,
{ hooksConfig },
);
......@@ -95,7 +125,7 @@ test('with twin address alert +@mobile', async({ mount, page }) => {
const component = await mount(
<TestApp>
<ContractCode addressHash={ addressHash }/>
<ContractCode addressHash={ addressHash } noSocket/>
</TestApp>,
{ hooksConfig },
);
......@@ -112,7 +142,7 @@ test('with proxy address alert +@mobile', async({ mount, page }) => {
const component = await mount(
<TestApp>
<ContractCode addressHash={ addressHash }/>
<ContractCode addressHash={ addressHash } noSocket/>
</TestApp>,
{ hooksConfig },
);
......@@ -129,7 +159,7 @@ test('non verified', async({ mount, page }) => {
const component = await mount(
<TestApp>
<ContractCode addressHash={ addressHash }/>
<ContractCode addressHash={ addressHash } noSocket/>
</TestApp>,
{ hooksConfig },
);
......
......@@ -2,8 +2,12 @@ import { Flex, Skeleton, Button, Grid, GridItem, Text, Alert, Link, chakra, Box
import { route } from 'nextjs-routes';
import React from 'react';
import type { SocketMessage } from 'lib/socket/types';
import useApiQuery from 'lib/api/useApiQuery';
import dayjs from 'lib/date/dayjs';
import useSocketChannel from 'lib/socket/useSocketChannel';
import useSocketMessage from 'lib/socket/useSocketMessage';
import Address from 'ui/shared/address/Address';
import AddressIcon from 'ui/shared/address/AddressIcon';
import AddressLink from 'ui/shared/address/AddressLink';
......@@ -16,6 +20,8 @@ import ContractSourceCode from './ContractSourceCode';
type Props = {
addressHash?: string;
// prop for pw tests only
noSocket?: boolean;
}
const InfoItem = chakra(({ label, value, className }: { label: string; value: string; className?: string }) => (
......@@ -25,15 +31,33 @@ const InfoItem = chakra(({ label, value, className }: { label: string; value: st
</GridItem>
));
const ContractCode = ({ addressHash }: Props) => {
const ContractCode = ({ addressHash, noSocket }: Props) => {
const [ isSocketOpen, setIsSocketOpen ] = React.useState(false);
const [ isChangedBytecodeSocket, setIsChangedBytecodeSocket ] = React.useState<boolean>();
const { data, isLoading, isError } = useApiQuery('contract', {
pathParams: { hash: addressHash },
queryOptions: {
enabled: Boolean(addressHash),
enabled: Boolean(addressHash) && (noSocket || isSocketOpen),
refetchOnMount: false,
},
});
const handleChangedBytecodeMessage: SocketMessage.AddressChangedBytecode['handler'] = React.useCallback(() => {
setIsChangedBytecodeSocket(true);
}, [ ]);
const channel = useSocketChannel({
topic: `addresses:${ addressHash?.toLowerCase() }`,
isDisabled: !addressHash,
onJoin: () => setIsSocketOpen(true),
});
useSocketMessage({
channel,
event: 'changed_bytecode',
handler: handleChangedBytecodeMessage,
});
if (isError) {
return <DataFetchAlert/>;
}
......@@ -117,7 +141,7 @@ const ContractCode = ({ addressHash }: Props) => {
{ data.sourcify_repo_url && <LinkExternal href={ data.sourcify_repo_url } fontSize="md">View contract in Sourcify repository</LinkExternal> }
</Alert>
) }
{ data.is_changed_bytecode && (
{ (data.is_changed_bytecode || isChangedBytecodeSocket) && (
<Alert status="warning">
Warning! Contract bytecode has been changed and does not match the verified one. Therefore, interaction with this smart contract may be risky.
</Alert>
......
import { test, expect } from '@playwright/experimental-ct-react';
import { test as base, expect, devices } from '@playwright/experimental-ct-react';
import React from 'react';
import { token as contract } from 'mocks/address/address';
import { tokenInfo, tokenCounters } from 'mocks/tokens/tokenInfo';
import * as socketServer from 'playwright/fixtures/socketServer';
import TestApp from 'playwright/TestApp';
import buildApiUrl from 'playwright/utils/buildApiUrl';
import insertAdPlaceholder from 'playwright/utils/insertAdPlaceholder';
......@@ -20,8 +21,15 @@ const hooksConfig = {
},
};
// FIXME: idk why mobile test doesn't work (it's ok locally)
test('base view +@mobile +@dark-mode', async({ mount, page }) => {
const test = base.extend<socketServer.SocketServerFixture>({
createSocket: socketServer.createSocket,
});
// FIXME
// test cases which use socket cannot run in parallel since the socket server always run on the same port
test.describe.configure({ mode: 'serial' });
test.beforeEach(async({ page }) => {
await page.route('https://request-global.czilladx.com/serve/native.php?z=19260bf627546ab7242', (route) => route.fulfill({
status: 200,
body: '',
......@@ -43,15 +51,41 @@ test('base view +@mobile +@dark-mode', async({ mount, page }) => {
status: 200,
body: JSON.stringify({}),
}));
});
test('base view', async({ mount, page, createSocket }) => {
const component = await mount(
<TestApp>
<TestApp withSocket>
<Token/>
</TestApp>,
{ hooksConfig },
);
const socket = await createSocket();
const channel = await socketServer.joinChannel(socket, 'tokens:1');
socketServer.sendMessage(socket, channel, 'total_supply', { total_supply: 10 ** 20 });
await insertAdPlaceholder(page);
await expect(component.locator('main')).toHaveScreenshot();
});
test.describe('mobile', () => {
test.use({ viewport: devices['iPhone 13 Pro'].viewport });
test('base view', async({ mount, page, createSocket }) => {
const component = await mount(
<TestApp withSocket>
<Token/>
</TestApp>,
{ hooksConfig },
);
const socket = await createSocket();
const channel = await socketServer.joinChannel(socket, 'tokens:1');
socketServer.sendMessage(socket, channel, 'total_supply', { total_supply: 10 ** 20 });
await insertAdPlaceholder(page);
await expect(component.locator('main')).toHaveScreenshot();
});
});
import { Skeleton, Box, Flex, SkeletonCircle, Icon, Tag } from '@chakra-ui/react';
import { useQueryClient } from '@tanstack/react-query';
import { useRouter } from 'next/router';
import React, { useEffect } from 'react';
import type { SocketMessage } from 'lib/socket/types';
import type { TokenInfo } from 'types/api/token';
import type { RoutedTab } from 'ui/shared/RoutedTabs/types';
import iconSuccess from 'icons/status/success.svg';
import useApiQuery from 'lib/api/useApiQuery';
import useApiQuery, { getResourceKey } from 'lib/api/useApiQuery';
import { useAppContext } from 'lib/appContext';
import useContractTabs from 'lib/hooks/useContractTabs';
import useIsMobile from 'lib/hooks/useIsMobile';
import useQueryWithPages from 'lib/hooks/useQueryWithPages';
import useSocketChannel from 'lib/socket/useSocketChannel';
import useSocketMessage from 'lib/socket/useSocketMessage';
import trimTokenSymbol from 'lib/token/trimTokenSymbol';
import AddressContract from 'ui/address/AddressContract';
import TextAd from 'ui/shared/ad/TextAd';
......@@ -29,6 +34,8 @@ import TokenTransfer from 'ui/token/TokenTransfer/TokenTransfer';
export type TokenTabs = 'token_transfers' | 'holders' | 'inventory';
const TokenPageContent = () => {
const [ isSocketOpen, setIsSocketOpen ] = React.useState(false);
const [ totalSupplySocket, setTotalSupplySocket ] = React.useState<number>();
const router = useRouter();
const isMobile = useIsMobile();
......@@ -40,9 +47,44 @@ const TokenPageContent = () => {
const hashString = router.query.hash?.toString();
const queryClient = useQueryClient();
const tokenQuery = useApiQuery('token', {
pathParams: { hash: hashString },
queryOptions: { enabled: Boolean(router.query.hash) },
queryOptions: { enabled: isSocketOpen && Boolean(router.query.hash) },
});
React.useEffect(() => {
if (tokenQuery.data && totalSupplySocket) {
queryClient.setQueryData(getResourceKey('token', { pathParams: { hash: hashString } }), (prevData: TokenInfo | undefined) => {
if (prevData) {
return { ...prevData, total_supply: totalSupplySocket.toString() };
}
});
}
}, [ tokenQuery.data, totalSupplySocket, hashString, queryClient ]);
const handleTotalSupplyMessage: SocketMessage.TokenTotalSupply['handler'] = React.useCallback((payload) => {
const prevData = queryClient.getQueryData(getResourceKey('token', { pathParams: { hash: hashString } }));
if (!prevData) {
setTotalSupplySocket(payload.total_supply);
}
queryClient.setQueryData(getResourceKey('token', { pathParams: { hash: hashString } }), (prevData: TokenInfo | undefined) => {
if (prevData) {
return { ...prevData, total_supply: payload.total_supply.toString() };
}
});
}, [ queryClient, hashString ]);
const channel = useSocketChannel({
topic: `tokens:${ hashString?.toLowerCase() }`,
isDisabled: !hashString,
onJoin: () => setIsSocketOpen(true),
});
useSocketMessage({
channel,
event: 'total_supply',
handler: handleTotalSupplyMessage,
});
useEffect(() => {
......
......@@ -2,9 +2,14 @@ import { Flex, Skeleton } from '@chakra-ui/react';
import { useRouter } from 'next/router';
import React from 'react';
import type { SocketMessage } from 'lib/socket/types';
import type { RawTracesResponse } from 'types/api/rawTrace';
import useApiQuery from 'lib/api/useApiQuery';
import { SECOND } from 'lib/consts';
import getQueryParamString from 'lib/router/getQueryParamString';
import useSocketChannel from 'lib/socket/useSocketChannel';
import useSocketMessage from 'lib/socket/useSocketMessage';
import DataFetchAlert from 'ui/shared/DataFetchAlert';
import RawDataSnippet from 'ui/shared/RawDataSnippet';
import TxPendingAlert from 'ui/tx/TxPendingAlert';
......@@ -12,6 +17,8 @@ import TxSocketAlert from 'ui/tx/TxSocketAlert';
import useFetchTxInfo from 'ui/tx/useFetchTxInfo';
const TxRawTrace = () => {
const [ isSocketOpen, setIsSocketOpen ] = React.useState(false);
const [ rawTraces, setRawTraces ] = React.useState<RawTracesResponse>();
const router = useRouter();
const hash = getQueryParamString(router.query.hash);
......@@ -19,10 +26,25 @@ const TxRawTrace = () => {
const { data, isLoading, isError } = useApiQuery('tx_raw_trace', {
pathParams: { hash },
queryOptions: {
enabled: Boolean(hash) && Boolean(txInfo.data?.status),
enabled: Boolean(hash) && Boolean(txInfo.data?.status) && isSocketOpen,
},
});
const handleRawTraceMessage: SocketMessage.TxRawTrace['handler'] = React.useCallback((payload) => {
setRawTraces(payload);
}, [ ]);
const channel = useSocketChannel({
topic: `transactions:${ hash }`,
isDisabled: !hash || !txInfo.data?.status,
onJoin: () => setIsSocketOpen(true),
});
useSocketMessage({
channel,
event: 'raw_trace',
handler: handleRawTraceMessage,
});
if (!txInfo.isLoading && !txInfo.isError && !txInfo.data.status) {
return txInfo.socketStatus ? <TxSocketAlert status={ txInfo.socketStatus }/> : <TxPendingAlert/>;
}
......@@ -42,11 +64,13 @@ const TxRawTrace = () => {
);
}
if (data.length === 0) {
const dataToDisplay = rawTraces ? rawTraces : data;
if (dataToDisplay.length === 0) {
return <span>No trace entries found.</span>;
}
const text = JSON.stringify(data, undefined, 4);
const text = JSON.stringify(dataToDisplay, undefined, 4);
return <RawDataSnippet data={ text }/>;
};
......
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