Commit d27e6a4b authored by tom's avatar tom

Merge branch 'feat/verified-tokens' into token/verified-info

parents 706b0fd5 4ea22c48
......@@ -39,7 +39,7 @@ NEXT_PUBLIC_HOMEPAGE_CHARTS=__PLACEHOLDER_FOR_NEXT_PUBLIC_HOMEPAGE_CHARTS__
NEXT_PUBLIC_HOMEPAGE_PLATE_GRADIENT=__PLACEHOLDER_FOR_NEXT_PUBLIC_HOMEPAGE_PLATE_GRADIENT__
NEXT_PUBLIC_HOMEPAGE_SHOW_GAS_TRACKER=__PLACEHOLDER_FOR_NEXT_PUBLIC_HOMEPAGE_SHOW_GAS_TRACKER__
NEXT_PUBLIC_HOMEPAGE_SHOW_AVG_BLOCK_TIME=__PLACEHOLDER_FOR_NEXT_PUBLIC_HOMEPAGE_SHOW_AVG_BLOCK_TIME__
NEXT_PUBLIC_AD_DOMAIN_WITH_AD=__PLACEHOLDER_FOR_NEXT_PUBLIC_DOMAIN_WITH_AD__
NEXT_PUBLIC_AD_DOMAIN_WITH_AD=__PLACEHOLDER_FOR_NEXT_PUBLIC_AD_DOMAIN_WITH_AD__
NEXT_PUBLIC_AD_ADBUTLER_ON=__PLACEHOLDER_FOR_NEXT_PUBLIC_AD_ADBUTLER_ON__
NEXT_PUBLIC_GRAPHIQL_TRANSACTION=__PLACEHOLDER_FOR_NEXT_PUBLIC_GRAPHIQL_TRANSACTION__
......
......@@ -9,6 +9,7 @@ jobs:
lint:
name: ESLint
runs-on: ubuntu-latest
if: "!contains(github.event.pull_request.labels.*.name, 'WIP')"
steps:
- name: Checkout repo
uses: actions/checkout@v3
......@@ -30,6 +31,7 @@ jobs:
type_check:
name: TypeScript
runs-on: ubuntu-latest
if: "!contains(github.event.pull_request.labels.*.name, 'WIP')"
steps:
- name: Checkout repo
uses: actions/checkout@v3
......
This diff is collapsed.
This diff is collapsed.
......@@ -5,7 +5,7 @@ blockscout:
app: blockscout
enabled: true
image:
_default: &image blockscout/blockscout-optimism-l2-advanced:5.1.2-prerelease-7a3279b9
_default: &image blockscout/blockscout-optimism-l2-advanced:5.1.2-prerelease-465ba09e
replicas:
app: 1
# init container
......@@ -83,9 +83,9 @@ blockscout:
_default: prod
ECTO_USE_SSL:
_default: 'false'
ENABLE_RUST_VERIFICATION_SERVICE:
MICROSERVICE_SC_VERIFIER_ENABLED:
_default: 'true'
RUST_VERIFICATION_SERVICE_URL:
MICROSERVICE_SC_VERIFIER_URL:
_default: http://sc-verifier-svc:8043
ACCOUNT_ENABLED:
_default: 'true'
......
......@@ -108,9 +108,9 @@ blockscout:
_default: 'true'
CHAIN_ID:
_default: 5
ENABLE_RUST_VERIFICATION_SERVICE:
MICROSERVICE_SC_VERIFIER_ENABLED:
_default: 'true'
RUST_VERIFICATION_SERVICE_URL:
MICROSERVICE_SC_VERIFIER_URL:
_default: http://eth-bytecode-db-svc.eth-bytecode-db-testing.svc.cluster.local:80
INDEXER_MEMORY_LIMIT:
_default: 5
......@@ -311,9 +311,9 @@ frontend:
_default: "[{'title':'Anyblock','baseUrl':'https://explorer.anyblock.tools','paths':{'tx':'/ethereum/ethereum/goerli/transaction','address':'/ethereum/ethereum/goerli/address'}},{'title':'Etherscan','baseUrl':'https://goerli.etherscan.io/','paths':{'tx':'/tx','address':'/address'}}]"
# network config
NEXT_PUBLIC_NETWORK_NAME:
_default: Ethereum
_default: Göerli
NEXT_PUBLIC_NETWORK_SHORT_NAME:
_default: Goerli
_default: Göerli
NEXT_PUBLIC_NETWORK_ASSETS_PATHNAME:
_default: ethereum
NEXT_PUBLIC_NETWORK_TYPE:
......
......@@ -39,6 +39,7 @@ frontend:
- "/csv-export"
- "/verified-contracts"
- "/graphiql"
- "/login"
resources:
limits:
......@@ -67,9 +68,9 @@ frontend:
NEXT_PUBLIC_FOOTER_STAKING_LINK:
_default: https://duneanalytics.com/maxaleks/xdai-staking
NEXT_PUBLIC_NETWORK_NAME:
_default: Ethereum
_default: Göerli
NEXT_PUBLIC_NETWORK_SHORT_NAME:
_default: Goerli
_default: Göerli
NEXT_PUBLIC_NETWORK_ASSETS_PATHNAME:
_default: ethereum
NEXT_PUBLIC_NETWORK_TYPE:
......
This diff is collapsed.
import React from 'react';
import appConfig from 'configs/app/config';
import isNeedProxy from 'lib/api/isNeedProxy';
import type { Params as FetchParams } from 'lib/hooks/useFetch';
import useFetch from 'lib/hooks/useFetch';
......@@ -27,7 +27,7 @@ export default function useApiFetch() {
url,
{
credentials: 'include',
...(resource.endpoint && appConfig.host === 'localhost' ? {
...(resource.endpoint && isNeedProxy() ? {
headers: {
'x-endpoint': resource.endpoint,
},
......
export default function getErrorCause(error: Error | undefined): Record<string, unknown> | undefined {
return (
error && 'cause' in error &&
typeof error.cause === 'object' && error.cause !== null &&
error.cause as Record<string, unknown>
) ||
undefined;
}
import getErrorCause from './getErrorCause';
export default function getErrorStatusCode(error: Error | undefined): number | undefined {
return (
error && 'cause' in error &&
typeof error.cause === 'object' && error.cause !== null &&
'status' in error.cause && typeof error.cause.status === 'number' &&
error.cause.status
) ||
undefined;
const cause = getErrorCause(error);
return cause && 'status' in cause && typeof cause.status === 'number' ? cause.status : undefined;
}
import type { ResourceError } from 'lib/api/resources';
import getErrorCause from './getErrorCause';
export default function getResourceErrorPayload<Payload = Record<string, unknown>>(error: Error | undefined): ResourceError<Payload>['payload'] | undefined {
const cause = getErrorCause(error);
return cause && 'payload' in cause ? cause.payload as ResourceError<Payload>['payload'] : undefined;
}
import appConfig from 'configs/app/config';
import { getServerSideProps as base } from '../getServerSideProps';
export const getServerSideProps: typeof base = async(...args) => {
if (!appConfig.isAccountSupported) {
return {
notFound: true,
};
}
return base(...args);
};
export const getServerSidePropsForVerifiedAddresses: typeof base = async(...args) => {
if (!appConfig.isAccountSupported || !appConfig.adminServiceApi.endpoint || !appConfig.contractInfoApi.endpoint) {
return {
notFound: true,
};
}
return base(...args);
};
......@@ -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>;
}
......@@ -85,7 +85,7 @@ export const erc721: TokenTransfer = {
method: 'updateSmartAsset',
};
export const erc1155: TokenTransfer = {
export const erc1155A: TokenTransfer = {
from: {
hash: '0x0000000000000000000000000000000000000000',
implementation_name: null,
......@@ -128,26 +128,44 @@ export const erc1155: TokenTransfer = {
log_index: '1',
};
export const erc1155multiple: TokenTransfer = {
...erc1155,
export const erc1155B: TokenTransfer = {
...erc1155A,
token: {
...erc1155.token,
...erc1155A.token,
name: 'SastanaNFT',
symbol: 'ipfs://QmUpFUfVKDCWeZQk5pvDFUxnpQP9N6eLSHhNUy49T1JVtY',
},
total: [
{ token_id: '12345678', value: '100000000000000000000', decimals: null },
{ token_id: '483200961027732618117991942553110860267520', value: '200000000000000000000', decimals: null },
{ token_id: '456', value: '42', decimals: null },
],
total: { token_id: '12345678', value: '100000000000000000000', decimals: null },
};
export const erc1155C: TokenTransfer = {
...erc1155A,
token: {
...erc1155A.token,
name: 'SastanaNFT',
symbol: 'ipfs://QmUpFUfVKDCWeZQk5pvDFUxnpQP9N6eLSHhNUy49T1JVtY',
},
total: { token_id: '483200961027732618117991942553110860267520', value: '200000000000000000000', decimals: null },
};
export const erc1155D: TokenTransfer = {
...erc1155A,
token: {
...erc1155A.token,
name: 'SastanaNFT',
symbol: 'ipfs://QmUpFUfVKDCWeZQk5pvDFUxnpQP9N6eLSHhNUy49T1JVtY',
},
total: { token_id: '456', value: '42', decimals: null },
};
export const mixTokens: TokenTransferResponse = {
items: [
erc20,
erc721,
erc1155,
erc1155multiple,
erc1155A,
erc1155B,
erc1155C,
erc1155D,
],
next_page_params: null,
};
......@@ -100,8 +100,10 @@ export const withTokenTransfer: Transaction = {
token_transfers: [
tokenTransferMock.erc20,
tokenTransferMock.erc721,
tokenTransferMock.erc1155,
tokenTransferMock.erc1155multiple,
tokenTransferMock.erc1155A,
tokenTransferMock.erc1155B,
tokenTransferMock.erc1155C,
tokenTransferMock.erc1155D,
],
tx_types: [
'token_transfer',
......
......@@ -16,6 +16,7 @@
"build": "next build",
"build:docker": "docker build --build-arg GIT_COMMIT_SHA=$(git rev-parse HEAD) -t blockscout ./",
"start": "next start",
"start:docker:local": "docker run -p 3000:3000 --env-file .env.local blockscout",
"start:docker:poa_core": "docker run -p 3000:3000 --env-file ./configs/envs/.env.common --env-file ./configs/envs/.env.poa_core --env-file ./configs/envs/.env.secrets blockscout",
"lint:eslint": "./node_modules/.bin/eslint . --ext .js,.jsx,.ts,.tsx",
"lint:eslint:fix": "./node_modules/.bin/eslint . --ext .js,.jsx,.ts,.tsx --fix",
......
......@@ -17,4 +17,4 @@ const ApiKeysPage: NextPage = () => {
export default ApiKeysPage;
export { getServerSideProps } from 'lib/next/getServerSideProps';
export { getServerSideProps } from 'lib/next/account/getServerSideProps';
......@@ -17,4 +17,4 @@ const CustomAbiPage: NextPage = () => {
export default CustomAbiPage;
export { getServerSideProps } from 'lib/next/getServerSideProps';
export { getServerSideProps } from 'lib/next/account/getServerSideProps';
......@@ -17,4 +17,4 @@ const PublicTagsPage: NextPage = () => {
export default PublicTagsPage;
export { getServerSideProps } from 'lib/next/getServerSideProps';
export { getServerSideProps } from 'lib/next/account/getServerSideProps';
......@@ -17,4 +17,4 @@ const AddressTagsPage: NextPage = () => {
export default AddressTagsPage;
export { getServerSideProps } from 'lib/next/getServerSideProps';
export { getServerSideProps } from 'lib/next/account/getServerSideProps';
......@@ -17,4 +17,4 @@ const VerifiedAddressesPage: NextPage = () => {
export default VerifiedAddressesPage;
export { getServerSideProps } from 'lib/next/getServerSideProps';
export { getServerSidePropsForVerifiedAddresses as getServerSideProps } from 'lib/next/account/getServerSideProps';
......@@ -19,4 +19,4 @@ const WatchListPage: NextPage = () => {
export default WatchListPage;
export { getServerSideProps } from 'lib/next/getServerSideProps';
export { getServerSideProps } from 'lib/next/account/getServerSideProps';
......@@ -15,4 +15,4 @@ const MyProfilePage: NextPage = () => {
export default MyProfilePage;
export { getServerSideProps } from 'lib/next/getServerSideProps';
export { getServerSideProps } from 'lib/next/account/getServerSideProps';
......@@ -5,6 +5,7 @@ import React from 'react';
import getSeo from 'lib/next/block/getSeo';
import Block from 'ui/pages/Block';
import Page from 'ui/shared/Page/Page';
const BlockPage: NextPage<RoutedQuery<'/block/[height]'>> = ({ height }: RoutedQuery<'/block/[height]'>) => {
const { title, description } = getSeo({ height });
......@@ -14,7 +15,9 @@ const BlockPage: NextPage<RoutedQuery<'/block/[height]'>> = ({ height }: RoutedQ
<title>{ title }</title>
<meta name="description" content={ description }/>
</Head>
<Block/>
<Page>
<Block/>
</Page>
</>
);
};
......
......@@ -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,
......
......@@ -166,6 +166,10 @@ export interface VerifiedAddress {
chainId: string;
contractAddress: string;
verifiedDate: string;
metadata: {
tokenName: string | null;
tokenSymbol: string | null;
};
}
export interface VerifiedAddressResponse {
......
......@@ -124,6 +124,7 @@ export interface SmartContractVerificationConfigRaw {
verification_options: Array<string>;
vyper_compiler_versions: Array<string>;
vyper_evm_versions: Array<string>;
is_rust_verifier_microservice_enabled: boolean;
}
export interface SmartContractVerificationConfig extends SmartContractVerificationConfigRaw {
......
......@@ -19,8 +19,6 @@ export interface TokenCounters {
transfers_count: string;
}
export type TokenInfoGeneric<Type extends TokenType> = Omit<TokenInfo, 'type'> & { type: Type };
export interface TokenHolders {
items: Array<TokenHolder>;
next_page_params: TokenHoldersPagination;
......
import type { AddressParam } from './addressParams';
import type { TokenInfoGeneric, TokenType } from './token';
import type { TokenInfo, TokenType } from './token';
export type Erc20TotalPayload = {
decimals: string | null;
......@@ -18,20 +18,20 @@ export type Erc1155TotalPayload = {
export type TokenTransfer = (
{
token: TokenInfoGeneric<'ERC-20'>;
token: TokenInfo<'ERC-20'>;
total: Erc20TotalPayload;
} |
{
token: TokenInfoGeneric<'ERC-721'>;
token: TokenInfo<'ERC-721'>;
total: Erc721TotalPayload;
} |
{
token: TokenInfoGeneric<'ERC-1155'>;
total: Erc1155TotalPayload | Array<Erc1155TotalPayload>;
token: TokenInfo<'ERC-1155'>;
total: Erc1155TotalPayload;
}
) & TokenTransferBase
export type TokenTotal = Erc20TotalPayload | Erc721TotalPayload | Erc1155TotalPayload | Array<Erc1155TotalPayload>;
export type TokenTotal = Erc20TotalPayload | Erc721TotalPayload | Erc1155TotalPayload;
interface TokenTransferBase {
type: 'token_transfer' | 'token_burning' | 'token_spawning' | 'token_minting';
......
......@@ -79,7 +79,7 @@ const AddressBlocksValidated = ({ scrollRef }: Props) => {
<Th width="17%">Block</Th>
<Th width="17%">Age</Th>
<Th width="16%">Txn</Th>
<Th width="25%">GasUsed</Th>
<Th width="25%">Gas used</Th>
<Th width="25%" isNumeric>Reward { appConfig.network.currency.symbol }</Th>
</Tr>
</Thead>
......
......@@ -2,7 +2,7 @@ import { Box } from '@chakra-ui/react';
import { test, expect } from '@playwright/experimental-ct-react';
import React from 'react';
import { erc1155 } from 'mocks/tokens/tokenTransfer';
import { erc1155A } from 'mocks/tokens/tokenTransfer';
import TestApp from 'playwright/TestApp';
import buildApiUrl from 'playwright/utils/buildApiUrl';
......@@ -20,7 +20,7 @@ const hooksConfig = {
test('with token filter and pagination +@mobile', async({ mount, page }) => {
await page.route(API_URL, (route) => route.fulfill({
status: 200,
body: JSON.stringify({ items: [ erc1155 ], next_page_params: { block_number: 1 } }),
body: JSON.stringify({ items: [ erc1155A ], next_page_params: { block_number: 1 } }),
}));
const component = await mount(
......@@ -37,7 +37,7 @@ test('with token filter and pagination +@mobile', async({ mount, page }) => {
test('with token filter and no pagination +@mobile', async({ mount, page }) => {
await page.route(API_URL, (route) => route.fulfill({
status: 200,
body: JSON.stringify({ items: [ erc1155 ] }),
body: JSON.stringify({ items: [ erc1155A ] }),
}));
const component = await mount(
......
......@@ -26,7 +26,6 @@ import HashStringShorten from 'ui/shared/HashStringShorten';
import Pagination from 'ui/shared/Pagination';
import SocketNewItemsNotice from 'ui/shared/SocketNewItemsNotice';
import TokenLogo from 'ui/shared/TokenLogo';
import { flattenTotal } from 'ui/shared/TokenTransfer/helpers';
import TokenTransferFilter from 'ui/shared/TokenTransfer/TokenTransferFilter';
import TokenTransferList from 'ui/shared/TokenTransfer/TokenTransferList';
import TokenTransferTable from 'ui/shared/TokenTransfer/TokenTransferTable';
......@@ -161,12 +160,11 @@ const AddressTokenTransfers = ({ scrollRef }: {scrollRef?: React.RefObject<HTMLD
const numActiveFilters = (filters.type?.length || 0) + (filters.filter ? 1 : 0);
const isActionBarHidden = !tokenFilter && !numActiveFilters && !data?.items.length && !currentAddress;
const items = data?.items?.reduce(flattenTotal, []);
const content = items ? (
const content = data?.items ? (
<>
<Hide below="lg" ssr={ false }>
<TokenTransferTable
data={ items }
data={ data?.items }
baseAddress={ currentAddress }
showTxInfo
top={ isActionBarHidden ? 0 : 80 }
......@@ -187,7 +185,7 @@ const AddressTokenTransfers = ({ scrollRef }: {scrollRef?: React.RefObject<HTMLD
/>
) }
<TokenTransferList
data={ items }
data={ data?.items }
baseAddress={ currentAddress }
showTxInfo
enableTimeIncrement
......
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>
......
......@@ -11,6 +11,8 @@ import AddressVerificationStepAddress from './steps/AddressVerificationStepAddre
import AddressVerificationStepSignature from './steps/AddressVerificationStepSignature';
import AddressVerificationStepSuccess from './steps/AddressVerificationStepSuccess';
type StateData = AddressVerificationFormFirstStepFields & AddressCheckStatusSuccess & { isToken?: boolean };
interface Props {
isOpen: boolean;
onClose: () => void;
......@@ -22,7 +24,7 @@ interface Props {
const AddressVerificationModal = ({ defaultAddress, isOpen, onClose, onSubmit, onAddTokenInfoClick, onShowListClick }: Props) => {
const [ stepIndex, setStepIndex ] = React.useState(0);
const [ data, setData ] = React.useState<AddressVerificationFormFirstStepFields & AddressCheckStatusSuccess>({ address: '', signingMessage: '' });
const [ data, setData ] = React.useState<StateData>({ address: '', signingMessage: '' });
const handleGoToSecondStep = React.useCallback((firstStepResult: typeof data) => {
setData(firstStepResult);
......@@ -32,6 +34,7 @@ const AddressVerificationModal = ({ defaultAddress, isOpen, onClose, onSubmit, o
const handleGoToThirdStep = React.useCallback((address: VerifiedAddress) => {
onSubmit(address);
setStepIndex((prev) => prev + 1);
setData((prev) => ({ ...prev, isToken: Boolean(address.metadata.tokenName) }));
}, [ onSubmit ]);
const handleGoToPrevStep = React.useCallback(() => {
......@@ -60,7 +63,14 @@ const AddressVerificationModal = ({ defaultAddress, isOpen, onClose, onSubmit, o
},
{
title: 'Congrats! Address is verified.',
content: <AddressVerificationStepSuccess onShowListClick={ onShowListClick } onAddTokenInfoClick={ handleAddTokenInfoClick }/>,
content: (
<AddressVerificationStepSuccess
onShowListClick={ onShowListClick }
onAddTokenInfoClick={ handleAddTokenInfoClick }
isToken={ data.isToken }
address={ data.address }
/>
),
},
];
const step = steps[stepIndex];
......
......@@ -108,7 +108,7 @@ const AddressVerificationStepAddress = ({ defaultAddress, onContinue }: Props) =
</Button>
<Box>
<span>Contact </span>
<Link>support@blockscout.com</Link>
<Link href="mailto:help@blockscout.com">help@blockscout.com</Link>
</Box>
</Flex>
</form>
......
......@@ -133,7 +133,7 @@ const AddressVerificationStepSignature = ({ address, signingMessage, contractCre
);
})();
const contactUsLink = <Link>contact us</Link>;
const contactUsLink = <span>contact us <Link href="mailto:help@blockscout.com">help@blockscout.com</Link></span>;
const rootError = (() => {
switch (formState.errors.root?.type) {
......@@ -215,6 +215,10 @@ const AddressVerificationStepSignature = ({ address, signingMessage, contractCre
</Flex>
<Flex alignItems="center" mt={ 8 } columnGap={ 5 }>
{ button }
<Box>
<span>Contact </span>
<Link href="mailto:help@blockscout.com">help@blockscout.com</Link>
</Box>
</Flex>
</form>
);
......
......@@ -4,24 +4,28 @@ import React from 'react';
interface Props {
onShowListClick: () => void;
onAddTokenInfoClick: () => void;
isToken?: boolean;
address: string;
}
const AddressVerificationStepSuccess = ({ onAddTokenInfoClick, onShowListClick }: Props) => {
const AddressVerificationStepSuccess = ({ onAddTokenInfoClick, onShowListClick, isToken, address }: Props) => {
return (
<Box>
<Alert status="success" flexWrap="wrap" whiteSpace="pre-wrap" wordBreak="break-word" mb={ 3 } display="inline-block">
<span>The address ownership for </span>
<chakra.span fontWeight={ 700 }>0xaba7161a7fb69c88e16ed9f455ce62b791ee4d03</chakra.span>
<chakra.span fontWeight={ 700 }>{ address }</chakra.span>
<span> is verified.</span>
</Alert>
<p>You may now submit the “Add token information” request</p>
<Flex alignItems="center" mt={ 8 } columnGap={ 5 } flexWrap="wrap" rowGap={ 5 }>
<Button size="lg" variant="outline" onClick={ onShowListClick }>
<Button size="lg" variant={ isToken ? 'outline' : 'solid' } onClick={ onShowListClick }>
View my verified addresses
</Button>
<Button size="lg" onClick={ onAddTokenInfoClick }>
Add token information
</Button>
{ isToken && (
<Button size="lg" onClick={ onAddTokenInfoClick }>
Add token information
</Button>
) }
</Flex>
</Box>
);
......
import type { VerifiedAddress } from 'types/api/account';
export interface AddressVerificationFormFirstStepFields {
address: string;
}
......@@ -34,12 +36,7 @@ export interface AddressVerificationResponseError {
export type AddressValidationResponseSuccess = {
status: 'SUCCESS';
result: {
verifiedAddress: {
chainId: string;
contractAddress: string;
userId: string;
verifiedDate: string;
};
verifiedAddress: VerifiedAddress;
};
} |
{
......
......@@ -141,7 +141,7 @@ const BlockDetails = ({ query }: Props) => {
{ /* api doesn't return the block processing time yet */ }
{ /* <Text>{ dayjs.duration(block.minedIn, 'second').humanize(true) }</Text> */ }
</DetailsInfoItem>
{ !totalReward.isEqualTo(ZERO) && (
{ !appConfig.L2.isL2Network && !totalReward.isEqualTo(ZERO) && (
<DetailsInfoItem
title="Block reward"
hint={
......
......@@ -77,20 +77,24 @@ const BlocksListItem = ({ data, isPending, enableTimeIncrement }: Props) => {
) }
</Flex>
</Box>
<Flex columnGap={ 2 }>
<Text fontWeight={ 500 }>Reward { appConfig.network.currency.symbol }</Text>
<Text variant="secondary">{ totalReward.toFixed() }</Text>
</Flex>
<Box>
<Text fontWeight={ 500 }>Burnt fees</Text>
<Flex columnGap={ 4 } mt={ 2 }>
<Flex>
<Icon as={ flameIcon } boxSize={ 5 } color="gray.500"/>
<Text variant="secondary" ml={ 1 }>{ burntFees.div(WEI).toFixed() }</Text>
</Flex>
<Utilization ml={ 4 } value={ burntFees.div(txFees).toNumber() }/>
{ !appConfig.L2.isL2Network && (
<Flex columnGap={ 2 }>
<Text fontWeight={ 500 }>Reward { appConfig.network.currency.symbol }</Text>
<Text variant="secondary">{ totalReward.toFixed() }</Text>
</Flex>
</Box>
) }
{ !appConfig.L2.isL2Network && (
<Box>
<Text fontWeight={ 500 }>Burnt fees</Text>
<Flex columnGap={ 4 } mt={ 2 }>
<Flex>
<Icon as={ flameIcon } boxSize={ 5 } color="gray.500"/>
<Text variant="secondary" ml={ 1 }>{ burntFees.div(WEI).toFixed() }</Text>
</Flex>
<Utilization ml={ 4 } value={ burntFees.div(txFees).toNumber() }/>
</Flex>
</Box>
) }
</ListItemMobile>
);
};
......
......@@ -24,11 +24,11 @@ const BlocksTable = ({ data, top, page }: Props) => {
<Tr>
<Th width="125px">Block</Th>
<Th width="120px">Size, bytes</Th>
<Th width="21%" minW="144px">{ capitalize(getNetworkValidatorTitle()) }</Th>
<Th width={ appConfig.L2.isL2Network ? '37%' : '21%' } minW="144px">{ capitalize(getNetworkValidatorTitle()) }</Th>
<Th width="64px" isNumeric>Txn</Th>
<Th width="35%">Gas used</Th>
<Th width="22%">Reward { appConfig.network.currency.symbol }</Th>
<Th width="22%">Burnt fees { appConfig.network.currency.symbol }</Th>
<Th width={ appConfig.L2.isL2Network ? '63%' : '35%' }>Gas used</Th>
{ !appConfig.L2.isL2Network && <Th width="22%">Reward { appConfig.network.currency.symbol }</Th> }
{ !appConfig.L2.isL2Network && <Th width="22%">Burnt fees { appConfig.network.currency.symbol }</Th> }
</Tr>
</Thead>
<Tbody>
......
......@@ -6,6 +6,7 @@ import React from 'react';
import type { Block } from 'types/api/block';
import appConfig from 'configs/app/config';
import flameIcon from 'icons/flame.svg';
import getBlockTotalReward from 'lib/block/getBlockTotalReward';
import { WEI } from 'lib/consts';
......@@ -28,7 +29,7 @@ const BlocksTableItem = ({ data, isPending, enableTimeIncrement }: Props) => {
const txFees = BigNumber(data.tx_fees || 0);
const separatorColor = useColorModeValue('gray.200', 'gray.700');
const burntFeesIconColor = useColorModeValue('gray.500', 'inherit');
return (
<Tr
as={ motion.tr }
......@@ -63,34 +64,38 @@ const BlocksTableItem = ({ data, isPending, enableTimeIncrement }: Props) => {
</LinkInternal>
) : data.tx_count }
</Td>
<Td fontSize="sm">
<Box>{ BigNumber(data.gas_used || 0).toFormat() }</Box>
<Flex mt={ 2 }>
<Tooltip label="Gas Used %">
<Box>
<Utilization colorScheme="gray" value={ BigNumber(data.gas_used || 0).dividedBy(BigNumber(data.gas_limit)).toNumber() }/>
{ !appConfig.L2.isL2Network && (
<Td fontSize="sm">
<Box>{ BigNumber(data.gas_used || 0).toFormat() }</Box>
<Flex mt={ 2 }>
<Tooltip label="Gas Used %">
<Box>
<Utilization colorScheme="gray" value={ BigNumber(data.gas_used || 0).dividedBy(BigNumber(data.gas_limit)).toNumber() }/>
</Box>
</Tooltip>
{ data.gas_target_percentage && (
<>
<TextSeparator color={ separatorColor } mx={ 1 }/>
<GasUsedToTargetRatio value={ data.gas_target_percentage }/>
</>
) }
</Flex>
</Td>
) }
<Td fontSize="sm">{ totalReward.toFixed(8) }</Td>
{ !appConfig.L2.isL2Network && (
<Td fontSize="sm">
<Flex alignItems="center" columnGap={ 1 }>
<Icon as={ flameIcon } boxSize={ 5 } color={ burntFeesIconColor }/>
{ burntFees.dividedBy(WEI).toFixed(8) }
</Flex>
<Tooltip label="Burnt fees / Txn fees * 100%">
<Box w="min-content">
<Utilization mt={ 2 } value={ burntFees.div(txFees).toNumber() }/>
</Box>
</Tooltip>
{ data.gas_target_percentage && (
<>
<TextSeparator color={ separatorColor } mx={ 1 }/>
<GasUsedToTargetRatio value={ data.gas_target_percentage }/>
</>
) }
</Flex>
</Td>
<Td fontSize="sm">{ totalReward.toFixed(8) }</Td>
<Td fontSize="sm">
<Flex alignItems="center" columnGap={ 1 }>
<Icon as={ flameIcon } boxSize={ 5 } color={ useColorModeValue('gray.500', 'inherit') }/>
{ burntFees.dividedBy(WEI).toFixed(8) }
</Flex>
<Tooltip label="Burnt fees / Txn fees * 100%">
<Box w="min-content">
<Utilization mt={ 2 } value={ burntFees.div(txFees).toNumber() }/>
</Box>
</Tooltip>
</Td>
</Td>
) }
</Tr>
);
};
......
......@@ -16,6 +16,7 @@ const hooksConfig = {
const hash = '0x2F99338637F027CFB7494E46B49987457beCC6E3';
const formConfig: SmartContractVerificationConfig = {
is_rust_verifier_microservice_enabled: true,
solidity_compiler_versions: [
'v0.8.17+commit.8df45f5f',
'v0.8.16+commit.07a7930e',
......
......@@ -22,15 +22,6 @@ import ContractVerificationVyperContract from './methods/ContractVerificationVyp
import ContractVerificationVyperMultiPartFile from './methods/ContractVerificationVyperMultiPartFile';
import { prepareRequestBody, formatSocketErrors, getDefaultValues } from './utils';
const METHOD_COMPONENTS = {
'flattened-code': <ContractVerificationFlattenSourceCode/>,
'standard-input': <ContractVerificationStandardInput/>,
sourcify: <ContractVerificationSourcify/>,
'multi-part': <ContractVerificationMultiPartFile/>,
'vyper-code': <ContractVerificationVyperContract/>,
'vyper-multi-part': <ContractVerificationVyperMultiPartFile/>,
};
interface Props {
method?: SmartContractVerificationMethod;
config: SmartContractVerificationConfig;
......@@ -122,8 +113,18 @@ const ContractVerificationForm = ({ method: methodFromQuery, config, hash }: Pro
handler: handleNewSocketMessage,
});
const methods = React.useMemo(() => {
return {
'flattened-code': <ContractVerificationFlattenSourceCode config={ config }/>,
'standard-input': <ContractVerificationStandardInput/>,
sourcify: <ContractVerificationSourcify/>,
'multi-part': <ContractVerificationMultiPartFile/>,
'vyper-code': <ContractVerificationVyperContract config={ config }/>,
'vyper-multi-part': <ContractVerificationVyperMultiPartFile/>,
};
}, [ config ]);
const method = watch('method');
const content = METHOD_COMPONENTS[method?.value] || null;
const content = methods[method?.value] || null;
const methodValue = method?.value;
useUpdateEffect(() => {
......
......@@ -11,9 +11,10 @@ import ContractVerificationFormRow from '../ContractVerificationFormRow';
interface Props {
hint?: string;
isReadOnly?: boolean;
}
const ContractVerificationFieldName = ({ hint }: Props) => {
const ContractVerificationFieldName = ({ hint, isReadOnly }: Props) => {
const { formState, control } = useFormContext<FormFields>();
const renderControl = React.useCallback(({ field }: {field: ControllerRenderProps<FormFields, 'name'>}) => {
......@@ -26,13 +27,13 @@ const ContractVerificationFieldName = ({ hint }: Props) => {
required
isInvalid={ Boolean(error) }
maxLength={ 255 }
isDisabled={ formState.isSubmitting }
isDisabled={ formState.isSubmitting || isReadOnly }
autoComplete="off"
/>
<InputPlaceholder text="Contract name" error={ error }/>
</FormControl>
);
}, [ formState.errors, formState.isSubmitting ]);
}, [ formState.errors, formState.isSubmitting, isReadOnly ]);
return (
<ContractVerificationFormRow>
......
import React from 'react';
import type { SmartContractVerificationConfig } from 'types/api/contract';
import ContractVerificationMethod from '../ContractVerificationMethod';
import ContractVerificationFieldAutodetectArgs from '../fields/ContractVerificationFieldAutodetectArgs';
import ContractVerificationFieldCode from '../fields/ContractVerificationFieldCode';
......@@ -10,16 +12,16 @@ import ContractVerificationFieldLibraries from '../fields/ContractVerificationFi
import ContractVerificationFieldName from '../fields/ContractVerificationFieldName';
import ContractVerificationFieldOptimization from '../fields/ContractVerificationFieldOptimization';
const ContractVerificationFlattenSourceCode = () => {
const ContractVerificationFlattenSourceCode = ({ config }: { config: SmartContractVerificationConfig }) => {
return (
<ContractVerificationMethod title="Contract verification via Solidity (flattened source code)">
<ContractVerificationFieldName/>
<ContractVerificationFieldIsYul/>
{ !config?.is_rust_verifier_microservice_enabled && <ContractVerificationFieldName/> }
{ config?.is_rust_verifier_microservice_enabled && <ContractVerificationFieldIsYul/> }
<ContractVerificationFieldCompiler/>
<ContractVerificationFieldEvmVersion/>
<ContractVerificationFieldOptimization/>
<ContractVerificationFieldCode/>
<ContractVerificationFieldAutodetectArgs/>
{ !config?.is_rust_verifier_microservice_enabled && <ContractVerificationFieldAutodetectArgs/> }
<ContractVerificationFieldLibraries/>
</ContractVerificationMethod>
);
......
import React from 'react';
import type { SmartContractVerificationConfig } from 'types/api/contract';
import ContractVerificationMethod from '../ContractVerificationMethod';
import ContractVerificationFieldCode from '../fields/ContractVerificationFieldCode';
import ContractVerificationFieldCompiler from '../fields/ContractVerificationFieldCompiler';
import ContractVerificationFieldConstructorArgs from '../fields/ContractVerificationFieldConstructorArgs';
import ContractVerificationFieldEvmVersion from '../fields/ContractVerificationFieldEvmVersion';
import ContractVerificationFieldName from '../fields/ContractVerificationFieldName';
const ContractVerificationVyperContract = () => {
const ContractVerificationVyperContract = ({ config }: { config: SmartContractVerificationConfig }) => {
return (
<ContractVerificationMethod title="Contract verification via Vyper (contract)">
<ContractVerificationFieldName hint="Must match the name specified in the code."/>
<ContractVerificationFieldName hint="Must match the name specified in the code." isReadOnly/>
<ContractVerificationFieldCompiler isVyper/>
{ config?.is_rust_verifier_microservice_enabled && <ContractVerificationFieldEvmVersion isVyper/> }
<ContractVerificationFieldCode isVyper/>
<ContractVerificationFieldConstructorArgs/>
{ !config?.is_rust_verifier_microservice_enabled && <ContractVerificationFieldConstructorArgs/> }
</ContractVerificationMethod>
);
};
......
......@@ -14,7 +14,7 @@ interface MethodOption {
export interface FormFieldsFlattenSourceCode {
method: MethodOption;
is_yul: boolean;
name: string;
name: string | undefined;
compiler: Option | null;
evm_version: Option | null;
is_optimization_enabled: boolean;
......@@ -53,9 +53,10 @@ export interface FormFieldsMultiPartFile {
export interface FormFieldsVyperContract {
method: MethodOption;
name: string;
evm_version: Option | null;
compiler: Option | null;
code: string;
constructor_args: string;
constructor_args: string | undefined;
}
export interface FormFieldsVyperMultiPartFile {
......
......@@ -84,8 +84,9 @@ export const DEFAULT_VALUES: Record<SmartContractVerificationMethod, FormFields>
value: 'vyper-code' as const,
label: METHOD_LABELS['vyper-code'],
},
name: '',
name: 'Vyper_contract',
compiler: null,
evm_version: null,
code: '',
constructor_args: '',
},
......@@ -113,6 +114,13 @@ export function getDefaultValues(method: SmartContractVerificationMethod, config
}
}
if (config.is_rust_verifier_microservice_enabled) {
if (method === 'flattened-code') {
'name' in defaultValues && (defaultValues.name = undefined);
'autodetect_constructor_args' in defaultValues && (defaultValues.autodetect_constructor_args = false);
}
}
return defaultValues;
}
......@@ -145,7 +153,7 @@ export function prepareRequestBody(data: FormFields): FetchParams['body'] {
is_optimization_enabled: _data.is_optimization_enabled,
is_yul_contract: _data.is_yul,
optimization_runs: _data.optimization_runs,
contract_name: _data.name,
contract_name: _data.name || undefined,
libraries: reduceLibrariesArray(_data.libraries),
evm_version: _data.evm_version?.value,
autodetect_constructor_args: _data.autodetect_constructor_args,
......@@ -196,6 +204,7 @@ export function prepareRequestBody(data: FormFields): FetchParams['body'] {
return {
compiler_version: _data.compiler?.value,
evm_version: _data.evm_version?.value,
source_code: _data.code,
contract_name: _data.name,
constructor_args: _data.constructor_args,
......
......@@ -7,6 +7,7 @@ import React from 'react';
import type { SocketMessage } from 'lib/socket/types';
import type { Block } from 'types/api/block';
import appConfig from 'configs/app/config';
import useApiQuery, { getResourceKey } from 'lib/api/useApiQuery';
import useIsMobile from 'lib/hooks/useIsMobile';
import { nbsp } from 'lib/html-entities';
......@@ -17,12 +18,20 @@ import LinkInternal from 'ui/shared/LinkInternal';
import LatestBlocksItem from './LatestBlocksItem';
import LatestBlocksItemSkeleton from './LatestBlocksItemSkeleton';
const BLOCK_HEIGHT = 166;
const BLOCK_HEIGHT_L1 = 166;
const BLOCK_HEIGHT_L2 = 112;
const BLOCK_MARGIN = 12;
const LatestBlocks = () => {
const blockHeight = appConfig.L2.isL2Network ? BLOCK_HEIGHT_L2 : BLOCK_HEIGHT_L1;
const isMobile = useIsMobile();
const blocksMaxCount = isMobile ? 2 : 3;
// const blocksMaxCount = isMobile ? 2 : 3;
let blocksMaxCount: number;
if (appConfig.L2.isL2Network) {
blocksMaxCount = isMobile ? 4 : 5;
} else {
blocksMaxCount = isMobile ? 2 : 3;
}
const { data, isLoading, isError } = useApiQuery('homepage_blocks');
const queryClient = useQueryClient();
......@@ -60,7 +69,7 @@ const LatestBlocks = () => {
<VStack
spacing={ `${ BLOCK_MARGIN }px` }
mb={ 6 }
height={ `${ BLOCK_HEIGHT * blocksMaxCount + BLOCK_MARGIN * (blocksMaxCount - 1) }px` }
height={ `${ blockHeight * blocksMaxCount + BLOCK_MARGIN * (blocksMaxCount - 1) }px` }
overflow="hidden"
>
{ Array.from(Array(blocksMaxCount)).map((item, index) => <LatestBlocksItemSkeleton key={ index }/>) }
......@@ -92,9 +101,9 @@ const LatestBlocks = () => {
</Text>
</Box>
) }
<VStack spacing={ `${ BLOCK_MARGIN }px` } mb={ 4 } height={ `${ BLOCK_HEIGHT * blocksCount + BLOCK_MARGIN * (blocksCount - 1) }px` } overflow="hidden">
<VStack spacing={ `${ BLOCK_MARGIN }px` } mb={ 4 } height={ `${ blockHeight * blocksCount + BLOCK_MARGIN * (blocksCount - 1) }px` } overflow="hidden">
<AnimatePresence initial={ false } >
{ dataToShow.map((block => <LatestBlocksItem key={ block.height } block={ block } h={ BLOCK_HEIGHT }/>)) }
{ dataToShow.map((block => <LatestBlocksItem key={ block.height } block={ block } h={ blockHeight }/>)) }
</AnimatePresence>
</VStack>
<Flex justifyContent="center">
......
......@@ -13,6 +13,7 @@ import React from 'react';
import type { Block } from 'types/api/block';
import appConfig from 'configs/app/config';
import blockIcon from 'icons/block.svg';
import getBlockTotalReward from 'lib/block/getBlockTotalReward';
import BlockTimestamp from 'ui/blocks/BlockTimestamp';
......@@ -56,11 +57,14 @@ const LatestBlocksItem = ({ block, h }: Props) => {
<Grid gridGap={ 2 } templateColumns="auto minmax(0, 1fr)" fontSize="sm">
<GridItem>Txn</GridItem>
<GridItem><Text variant="secondary">{ block.tx_count }</Text></GridItem>
{ /* */ }
<GridItem>Reward</GridItem>
<GridItem><Text variant="secondary">{ totalReward.toFixed() }</Text></GridItem>
<GridItem>Miner</GridItem>
<GridItem><AddressLink type="address" alias={ block.miner.name } hash={ block.miner.hash } truncation="constant" maxW="100%"/></GridItem>
{ !appConfig.L2.isL2Network && (
<>
<GridItem>Reward</GridItem>
<GridItem><Text variant="secondary">{ totalReward.toFixed() }</Text></GridItem>
<GridItem>Miner</GridItem>
<GridItem><AddressLink type="address" alias={ block.miner.name } hash={ block.miner.hash } truncation="constant" maxW="100%"/></GridItem>
</>
) }
</Grid>
</Box>
);
......
......@@ -8,6 +8,8 @@ import {
} from '@chakra-ui/react';
import React from 'react';
import appConfig from 'configs/app/config';
const LatestBlocksItemSkeleton = () => {
return (
<Box
......@@ -27,10 +29,14 @@ const LatestBlocksItemSkeleton = () => {
<Grid gridGap={ 2 } templateColumns="auto minmax(0, 1fr)" fontSize="sm">
<GridItem><Skeleton w="30px" h="15px"/></GridItem>
<GridItem><Skeleton w="93px" h="15px"/></GridItem>
<GridItem><Skeleton w="30px" h="15px"/></GridItem>
<GridItem><Skeleton w="93px" h="15px"/></GridItem>
<GridItem><Skeleton w="30px" h="15px"/></GridItem>
<GridItem><Skeleton w="93px" h="15px"/></GridItem>
{ !appConfig.L2.isL2Network && (
<>
<GridItem><Skeleton w="30px" h="15px"/></GridItem>
<GridItem><Skeleton w="93px" h="15px"/></GridItem>
<GridItem><Skeleton w="30px" h="15px"/></GridItem>
<GridItem><Skeleton w="93px" h="15px"/></GridItem>
</>
) }
</Grid>
</Box>
);
......
......@@ -11,7 +11,6 @@ import useQueryWithPages from 'lib/hooks/useQueryWithPages';
import getQueryParamString from 'lib/router/getQueryParamString';
import BlockDetails from 'ui/block/BlockDetails';
import TextAd from 'ui/shared/ad/TextAd';
import Page from 'ui/shared/Page/Page';
import PageTitle from 'ui/shared/Page/PageTitle';
import Pagination from 'ui/shared/Pagination';
import RoutedTabs from 'ui/shared/RoutedTabs/RoutedTabs';
......@@ -39,7 +38,7 @@ const BlockPageContent = () => {
resourceName: 'block_txs',
pathParams: { height },
options: {
enabled: Boolean(height && tab === 'txs'),
enabled: Boolean(blockQuery.data?.height && tab === 'txs'),
},
});
......@@ -47,6 +46,10 @@ const BlockPageContent = () => {
throw new Error('Block not found', { cause: { status: 404 } });
}
if (blockQuery.isError) {
throw new Error(undefined, { cause: blockQuery.error });
}
const tabs: Array<RoutedTab> = React.useMemo(() => ([
{ id: 'index', title: 'Details', component: <BlockDetails query={ blockQuery }/> },
{ id: 'txs', title: 'Transactions', component: <TxsContent query={ blockTxsQuery } showBlockInfo={ false } showSocketInfo={ false }/> },
......@@ -68,7 +71,7 @@ const BlockPageContent = () => {
}, [ appProps.referrer ]);
return (
<Page>
<>
{ blockQuery.isLoading ? <Skeleton h={{ base: 12, lg: 6 }} mb={ 6 } w="100%" maxW="680px"/> : <TextAd mb={ 6 }/> }
{ blockQuery.isLoading ? (
<Skeleton h={ 10 } w="300px" mb={ 6 }/>
......@@ -84,7 +87,7 @@ const BlockPageContent = () => {
rightSlot={ hasPagination ? <Pagination { ...blockTxsQuery.pagination }/> : null }
stickyEnabled={ hasPagination }
/>
</Page>
</>
);
};
......
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 appConfig from 'configs/app/config';
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';
......@@ -30,6 +35,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();
......@@ -39,9 +46,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(() => {
......
import { OrderedList, ListItem, chakra, Button, useDisclosure, Show, Hide, Skeleton, Box } from '@chakra-ui/react';
import { OrderedList, ListItem, chakra, Button, useDisclosure, Show, Hide, Skeleton, Box, Link } from '@chakra-ui/react';
import { useQueryClient } from '@tanstack/react-query';
import { useRouter } from 'next/router';
import React from 'react';
......@@ -182,6 +182,11 @@ const VerifiedAddresses = () => {
<chakra.div mt={ 5 }>
Once these steps are complete, click the Add address button below to get started.
</chakra.div>
<chakra.div>
<span>Need help? Contact admin team at </span>
<Link href="mailto:help@blockscout.com">help@blockscout.com</Link>
<span> for assistance!</span>
</chakra.div>
</AccountPageDescription>
<DataListDisplay
isLoading={ addressesQuery.isLoading || applicationsQuery.isLoading }
......
import { Box, Button, Heading, Icon, chakra } from '@chakra-ui/react';
import { route } from 'nextjs-routes';
import React from 'react';
import icon404 from 'icons/error-pages/404.svg';
interface Props {
hash?: string;
className?: string;
}
const AppErrorBlockConsensus = ({ hash, className }: Props) => {
return (
<Box className={ className }>
<Icon as={ icon404 } width="200px" height="auto"/>
<Heading mt={ 8 } size="2xl" fontFamily="body">Block removed due to chain reorganization</Heading>
<Button
mt={ 8 }
size="lg"
variant="outline"
as="a"
href={ hash ? route({ pathname: '/block/[height]', query: { height: hash } }) : route({ pathname: '/' }) }
>
{ hash ? 'View reorg' : 'Back to home' }
</Button>
</Box>
);
};
export default chakra(AppErrorBlockConsensus);
......@@ -2,9 +2,11 @@ import { Flex } from '@chakra-ui/react';
import React from 'react';
import getErrorStatusCode from 'lib/errors/getErrorStatusCode';
import getResourceErrorPayload from 'lib/errors/getResourceErrorPayload';
import useAdblockDetect from 'lib/hooks/useAdblockDetect';
import useGetCsrfToken from 'lib/hooks/useGetCsrfToken';
import AppError from 'ui/shared/AppError/AppError';
import AppErrorBlockConsensus from 'ui/shared/AppError/AppErrorBlockConsensus';
import ErrorBoundary from 'ui/shared/ErrorBoundary';
import ErrorInvalidTxHash from 'ui/shared/ErrorInvalidTxHash';
import PageContent from 'ui/shared/Page/PageContent';
......@@ -31,15 +33,27 @@ const Page = ({
const renderErrorScreen = React.useCallback((error?: Error) => {
const statusCode = getErrorStatusCode(error) || 500;
const resourceErrorPayload = getResourceErrorPayload(error);
const messageInPayload = resourceErrorPayload && 'message' in resourceErrorPayload && typeof resourceErrorPayload.message === 'string' ?
resourceErrorPayload.message :
undefined;
const isInvalidTxHash = error?.message.includes('Invalid tx hash');
const isBlockConsensus = messageInPayload?.includes('Block lost consensus');
if (isInvalidTxHash) {
return <PageContent isHomePage={ isHomePage }><ErrorInvalidTxHash/></PageContent>;
}
if (wrapChildren) {
const content = isInvalidTxHash ? <ErrorInvalidTxHash/> : <AppError statusCode={ statusCode } mt="50px"/>;
return <PageContent isHomePage={ isHomePage }>{ content }</PageContent>;
if (isBlockConsensus) {
const hash = resourceErrorPayload && 'hash' in resourceErrorPayload && typeof resourceErrorPayload.hash === 'string' ?
resourceErrorPayload.hash :
undefined;
return <PageContent isHomePage={ isHomePage }><AppErrorBlockConsensus hash={ hash } mt="50px"/></PageContent>;
}
return isInvalidTxHash ? <ErrorInvalidTxHash/> : <AppError statusCode={ statusCode }/>;
}, [ isHomePage, wrapChildren ]);
return <PageContent isHomePage={ isHomePage }><AppError statusCode={ statusCode } mt="50px"/></PageContent>;
}, [ isHomePage ]);
const renderedChildren = wrapChildren ? (
<PageContent isHomePage={ isHomePage }>{ children }</PageContent>
......
......@@ -5,11 +5,8 @@ import React from 'react';
import * as tokenTransferMock from 'mocks/tokens/tokenTransfer';
import TestApp from 'playwright/TestApp';
import { flattenTotal } from './helpers';
import TokenTransferList from './TokenTransferList';
const flattenData = tokenTransferMock.mixTokens.items.reduce(flattenTotal, []);
test.use({ viewport: devices['iPhone 13 Pro'].viewport });
test('without tx info', async({ mount }) => {
......@@ -17,7 +14,7 @@ test('without tx info', async({ mount }) => {
<TestApp>
<Box h={{ base: '134px', lg: 6 }}/>
<TokenTransferList
data={ flattenData }
data={ tokenTransferMock.mixTokens.items }
showTxInfo={ false }
/>
</TestApp>,
......@@ -31,7 +28,7 @@ test('with tx info', async({ mount }) => {
<TestApp>
<Box h={{ base: '134px', lg: 6 }}/>
<TokenTransferList
data={ flattenData }
data={ tokenTransferMock.mixTokens.items }
showTxInfo={ true }
/>
</TestApp>,
......
......@@ -5,17 +5,14 @@ import React from 'react';
import * as tokenTransferMock from 'mocks/tokens/tokenTransfer';
import TestApp from 'playwright/TestApp';
import { flattenTotal } from './helpers';
import TokenTransferTable from './TokenTransferTable';
const flattenData = tokenTransferMock.mixTokens.items.reduce(flattenTotal, []);
test('without tx info', async({ mount }) => {
const component = await mount(
<TestApp>
<Box h={{ base: '134px', lg: 6 }}/>
<TokenTransferTable
data={ flattenData }
data={ tokenTransferMock.mixTokens.items }
top={ 0 }
showTxInfo={ false }
/>
......@@ -30,7 +27,7 @@ test('with tx info', async({ mount }) => {
<TestApp>
<Box h={{ base: '134px', lg: 6 }}/>
<TokenTransferTable
data={ flattenData }
data={ tokenTransferMock.mixTokens.items }
top={ 0 }
showTxInfo={ true }
/>
......
import type { TokenTransfer } from 'types/api/tokenTransfer';
export const flattenTotal = (result: Array<TokenTransfer>, item: TokenTransfer): Array<TokenTransfer> => {
if (Array.isArray(item.total)) {
item.total.forEach((total) => {
result.push({ ...item, total });
});
} else {
result.push(item);
}
return result;
};
export const getTokenTransferTypeText = (type: TokenTransfer['type']) => {
switch (type) {
case 'token_minting':
......
......@@ -274,6 +274,7 @@ const ChartWidget = ({ items, title, description, isLoading, className, isError,
title={ title }
description={ description }
onClose={ clearFullscreenChart }
units={ units }
/>
) }
</>
......
......@@ -13,6 +13,7 @@ type Props = {
description?: string;
items: Array<TimeChartItem>;
onClose: () => void;
units?: string;
}
const FullscreenChartModal = ({
......@@ -20,6 +21,7 @@ const FullscreenChartModal = ({
title,
description,
items,
units,
onClose,
}: Props) => {
const [ isZoomResetInitial, setIsZoomResetInitial ] = React.useState(true);
......@@ -94,6 +96,7 @@ const FullscreenChartModal = ({
margin={{ bottom: 60 }}
isEnlarged
items={ items }
units={ units }
onZoom={ handleZoom }
isZoomResetInitial={ isZoomResetInitial }
title={ title }
......
......@@ -64,7 +64,12 @@ test('erc1155 +@mobile', async({ mount }) => {
// @ts-ignore:
transfersQuery={{
data: {
items: [ tokenTransferMock.erc1155, tokenTransferMock.erc1155multiple ],
items: [
tokenTransferMock.erc1155A,
tokenTransferMock.erc1155B,
tokenTransferMock.erc1155C,
tokenTransferMock.erc1155D,
],
next_page_params: null,
},
isPaginationVisible: true,
......
......@@ -15,7 +15,6 @@ import DataListDisplay from 'ui/shared/DataListDisplay';
import Pagination from 'ui/shared/Pagination';
import type { Props as PaginationProps } from 'ui/shared/Pagination';
import SocketNewItemsNotice from 'ui/shared/SocketNewItemsNotice';
import { flattenTotal } from 'ui/shared/TokenTransfer/helpers';
import TokenTransferList from 'ui/token/TokenTransfer/TokenTransferList';
import TokenTransferTable from 'ui/token/TokenTransfer/TokenTransferTable';
......@@ -59,14 +58,12 @@ const TokenTransfer = ({ transfersQuery, tokenId }: Props) => {
handler: handleNewTransfersMessage,
});
const items = data?.items?.reduce(flattenTotal, []);
const content = items ? (
const content = data?.items ? (
<>
<Hide below="lg" ssr={ false }>
<TokenTransferTable
data={ items }
data={ data?.items }
top={ isPaginationVisible ? 80 : 0 }
showSocketInfo={ pagination.page === 1 }
socketInfoAlert={ socketAlert }
......@@ -84,7 +81,7 @@ const TokenTransfer = ({ transfersQuery, tokenId }: Props) => {
borderBottomRadius={ 0 }
/>
) }
<TokenTransferList data={ items } tokenId={ tokenId }/>
<TokenTransferList data={ data?.items } tokenId={ tokenId }/>
</Show>
</>
) : null;
......
......@@ -304,7 +304,7 @@ const TxDetails = () => {
) }
</DetailsInfoItem>
) }
{ data.tx_burnt_fee && (
{ data.tx_burnt_fee && !appConfig.L2.isL2Network && (
<DetailsInfoItem
title="Burnt fees"
hint={ `Amount of ${ appConfig.network.currency.symbol } burned for this transaction. Equals Block Base Fee per Gas * Gas Used` }
......
......@@ -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 }/>;
};
......
......@@ -13,7 +13,6 @@ import ActionBar from 'ui/shared/ActionBar';
import DataFetchAlert from 'ui/shared/DataFetchAlert';
import DataListDisplay from 'ui/shared/DataListDisplay';
import Pagination from 'ui/shared/Pagination';
import { flattenTotal } from 'ui/shared/TokenTransfer/helpers';
import TokenTransferFilter from 'ui/shared/TokenTransfer/TokenTransferFilter';
import TokenTransferList from 'ui/shared/TokenTransfer/TokenTransferList';
import TokenTransferTable from 'ui/shared/TokenTransfer/TokenTransferTable';
......@@ -55,15 +54,13 @@ const TxTokenTransfer = () => {
const numActiveFilters = typeFilter.length;
const isActionBarHidden = !numActiveFilters && !tokenTransferQuery.data?.items.length;
const items = tokenTransferQuery.data?.items?.reduce(flattenTotal, []);
const content = items ? (
const content = tokenTransferQuery.data?.items ? (
<>
<Hide below="lg" ssr={ false }>
<TokenTransferTable data={ items } top={ isActionBarHidden ? 0 : 80 }/>
<TokenTransferTable data={ tokenTransferQuery.data?.items } top={ isActionBarHidden ? 0 : 80 }/>
</Hide>
<Show below="lg" ssr={ false }>
<TokenTransferList data={ items }/>
<TokenTransferList data={ tokenTransferQuery.data?.items }/>
</Show>
</>
) : null;
......
......@@ -11,25 +11,25 @@ import CurrencyValue from 'ui/shared/CurrencyValue';
import TokenSnippet from 'ui/shared/TokenSnippet/TokenSnippet';
import NftTokenTransferSnippet from 'ui/tx/NftTokenTransferSnippet';
type Props = TTokenTransfer;
interface Props {
data: TTokenTransfer;
}
const TxDetailsTokenTransfer = ({ token, total, to, from }: Props) => {
const isColumnLayout = token.type === 'ERC-1155' && Array.isArray(total);
const TxDetailsTokenTransfer = ({ data }: Props) => {
const content = (() => {
switch (token.type) {
switch (data.token.type) {
case 'ERC-20': {
const payload = total as Erc20TotalPayload;
const total = data.total as Erc20TotalPayload;
return (
<Flex flexWrap="wrap" columnGap={ 3 } rowGap={ 2 }>
<Text fontWeight={ 500 } as="span">For:{ space }
<CurrencyValue value={ payload.value } exchangeRate={ token.exchange_rate } fontWeight={ 600 } decimals={ payload.decimals }/>
<CurrencyValue value={ total.value } exchangeRate={ data.token.exchange_rate } fontWeight={ 600 } decimals={ total.decimals }/>
</Text>
<TokenSnippet
symbol={ trimTokenSymbol(token.symbol) }
hash={ token.address }
name={ token.name }
symbol={ trimTokenSymbol(data.token.symbol) }
hash={ data.token.address }
name={ data.token.name }
w="auto"
flexGrow="1"
columnGap={ 1 }
......@@ -40,47 +40,46 @@ const TxDetailsTokenTransfer = ({ token, total, to, from }: Props) => {
}
case 'ERC-721': {
const payload = total as Erc721TotalPayload;
const total = data.total as Erc721TotalPayload;
return (
<NftTokenTransferSnippet
name={ token.name }
tokenId={ payload.token_id }
name={ data.token.name }
tokenId={ total.token_id }
value="1"
hash={ token.address }
symbol={ trimTokenSymbol(token.symbol) }
hash={ data.token.address }
symbol={ trimTokenSymbol(data.token.symbol) }
/>
);
}
case 'ERC-1155': {
const payload = total as Erc1155TotalPayload | Array<Erc1155TotalPayload>;
const items = Array.isArray(payload) ? payload : [ payload ];
return items.map((item) => (
const total = data.total as Erc1155TotalPayload;
return (
<NftTokenTransferSnippet
name={ token.name }
key={ item.token_id }
tokenId={ item.token_id }
value={ item.value }
hash={ token.address }
symbol={ trimTokenSymbol(token.symbol) }
name={ data.token.name }
key={ total.token_id }
tokenId={ total.token_id }
value={ total.value }
hash={ data.token.address }
symbol={ trimTokenSymbol(data.token.symbol) }
/>
));
);
}
}
})();
return (
<Flex
alignItems={ isColumnLayout ? 'flex-start' : 'center' }
alignItems="center"
flexWrap="wrap"
columnGap={ 3 }
rowGap={ 3 }
flexDir={ isColumnLayout ? 'column' : 'row' }
flexDir="row"
>
<Flex alignItems="center">
<AddressLink type="address" fontWeight="500" hash={ from.hash } truncation="constant"/>
<AddressLink type="address" fontWeight="500" hash={ data.from.hash } truncation="constant"/>
<Icon as={ rightArrowIcon } boxSize={ 6 } mx={ 2 } color="gray.500"/>
<AddressLink type="address" fontWeight="500" hash={ to.hash } truncation="constant"/>
<AddressLink type="address" fontWeight="500" hash={ data.to.hash } truncation="constant"/>
</Flex>
<Flex flexDir="column" rowGap={ 5 }>
{ content }
......
......@@ -7,7 +7,6 @@ import type { TokenTransfer } from 'types/api/tokenTransfer';
import tokenIcon from 'icons/token.svg';
import DetailsInfoItem from 'ui/shared/DetailsInfoItem';
import LinkInternal from 'ui/shared/LinkInternal';
import { flattenTotal } from 'ui/shared/TokenTransfer/helpers';
import TxDetailsTokenTransfer from './TxDetailsTokenTransfer';
......@@ -27,10 +26,9 @@ const VISIBLE_ITEMS_NUM = 3;
const TxDetailsTokenTransfers = ({ data, txHash }: Props) => {
const viewAllUrl = route({ pathname: '/tx/[hash]', query: { hash: txHash, tab: 'token_transfers' } });
const formattedData = data.reduce(flattenTotal, []);
const transferGroups = TOKEN_TRANSFERS_TYPES.map((group) => ({
...group,
items: formattedData?.filter((token) => token.type === group.type) || [],
items: data?.filter((token) => token.type === group.type) || [],
}));
const showViewAllLink = transferGroups.some(({ items }) => items.length > VISIBLE_ITEMS_NUM);
......@@ -54,7 +52,7 @@ const TxDetailsTokenTransfers = ({ data, txHash }: Props) => {
rowGap={ 5 }
w="100%"
>
{ items.slice(0, VISIBLE_ITEMS_NUM).map((item, index) => <TxDetailsTokenTransfer key={ index } { ...item }/>) }
{ items.slice(0, VISIBLE_ITEMS_NUM).map((item, index) => <TxDetailsTokenTransfer key={ index } data={ item }/>) }
</Flex>
</DetailsInfoItem>
);
......
......@@ -34,29 +34,33 @@ const VerifiedAddressesListItem = ({ item, application, onAdd, onEdit }: Props)
<AddressSnippet address={{ hash: item.contractAddress, is_contract: true, implementation_name: null }}/>
</ListItemMobileGrid.Value>
<ListItemMobileGrid.Label>Token Info</ListItemMobileGrid.Label>
<ListItemMobileGrid.Value py={ application ? '3px' : '5px' } display="flex" alignItems="center">
{ application ? (
<>
<VerifiedAddressesTokenSnippet application={ application }/>
<Tooltip label="Edit">
<IconButton
aria-label="edit"
variant="simple"
boxSize={ 5 }
borderRadius="none"
flexShrink={ 0 }
onClick={ handleEditClick }
icon={ <Icon as={ editIcon }/> }
/>
</Tooltip>
</>
) : (
<Link onClick={ handleAddClick }>Add details</Link>
) }
</ListItemMobileGrid.Value>
{ item.metadata.tokenName && (
<>
<ListItemMobileGrid.Label>Token Info</ListItemMobileGrid.Label>
<ListItemMobileGrid.Value py={ application ? '3px' : '5px' } display="flex" alignItems="center">
{ application ? (
<>
<VerifiedAddressesTokenSnippet application={ application } name={ item.metadata.tokenName }/>
<Tooltip label="Edit">
<IconButton
aria-label="edit"
variant="simple"
boxSize={ 5 }
borderRadius="none"
flexShrink={ 0 }
onClick={ handleEditClick }
icon={ <Icon as={ editIcon }/> }
/>
</Tooltip>
</>
) : (
<Link onClick={ handleAddClick }>Add details</Link>
) }
</ListItemMobileGrid.Value>
</>
) }
{ application && (
{ item.metadata.tokenName && application && (
<>
<ListItemMobileGrid.Label>Status</ListItemMobileGrid.Label>
<ListItemMobileGrid.Value>
......@@ -65,7 +69,7 @@ const VerifiedAddressesListItem = ({ item, application, onAdd, onEdit }: Props)
</>
) }
{ application && (
{ item.metadata.tokenName && application && (
<>
<ListItemMobileGrid.Label>Date</ListItemMobileGrid.Label>
<ListItemMobileGrid.Value>
......
......@@ -27,33 +27,43 @@ const VerifiedAddressesTableItem = ({ item, application, onAdd, onEdit }: Props)
onEdit(item.contractAddress);
}, [ item, onEdit ]);
const tokenInfo = (() => {
if (!item.metadata.tokenName) {
return <span>Not a token</span>;
}
if (!application) {
return <Link onClick={ handleAddClick }>Add details</Link>;
}
return <VerifiedAddressesTokenSnippet application={ application } name={ item.metadata.tokenName }/>;
})();
return (
<Tr>
<Td>
<AddressSnippet address={{ hash: item.contractAddress, is_contract: true, implementation_name: null }}/>
</Td>
<Td fontSize="sm" verticalAlign="middle">
{ application ? (
<VerifiedAddressesTokenSnippet application={ application }/>
) : (
<Link onClick={ handleAddClick }>Add details</Link>
) }
{ tokenInfo }
</Td>
<Td>
{ item.metadata.tokenName && application ? (
<Tooltip label="Edit">
<IconButton
aria-label="edit"
variant="simple"
boxSize={ 5 }
borderRadius="none"
flexShrink={ 0 }
onClick={ handleEditClick }
icon={ <Icon as={ editIcon }/> }
/>
</Tooltip>
) : null }
</Td>
<Td>{ application ? (
<Tooltip label="Edit">
<IconButton
aria-label="edit"
variant="simple"
boxSize={ 5 }
borderRadius="none"
flexShrink={ 0 }
onClick={ handleEditClick }
icon={ <Icon as={ editIcon }/> }
/>
</Tooltip>
) : null }</Td>
<Td fontSize="sm"><VerifiedAddressesStatus status={ application?.status }/></Td>
<Td fontSize="sm" color="text_secondary">{ dayjs(application?.updatedAt).format('MMM DD, YYYY') }</Td>
<Td fontSize="sm"><VerifiedAddressesStatus status={ item.metadata.tokenName ? application?.status : undefined }/></Td>
<Td fontSize="sm" color="text_secondary">{ item.metadata.tokenName ? dayjs(application?.updatedAt).format('MMM DD, YYYY') : null }</Td>
</Tr>
);
};
......
......@@ -8,9 +8,10 @@ import TokenLogoPlaceholder from 'ui/shared/TokenLogoPlaceholder';
interface Props {
application: TokenInfoApplication;
name: string;
}
const VerifiedAddressesTokenSnippet = ({ application }: Props) => {
const VerifiedAddressesTokenSnippet = ({ application, name }: Props) => {
return (
<Flex alignItems="center" columnGap={ 2 } w="100%">
<Image
......@@ -23,7 +24,7 @@ const VerifiedAddressesTokenSnippet = ({ application }: Props) => {
/>
<AddressLink
hash={ application.tokenAddress }
alias={ application.projectName }
alias={ name }
type="token"
isDisabled={ application.status === 'IN_PROCESS' }
fontWeight={ 500 }
......
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