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__ ...@@ -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_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_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_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_AD_ADBUTLER_ON=__PLACEHOLDER_FOR_NEXT_PUBLIC_AD_ADBUTLER_ON__
NEXT_PUBLIC_GRAPHIQL_TRANSACTION=__PLACEHOLDER_FOR_NEXT_PUBLIC_GRAPHIQL_TRANSACTION__ NEXT_PUBLIC_GRAPHIQL_TRANSACTION=__PLACEHOLDER_FOR_NEXT_PUBLIC_GRAPHIQL_TRANSACTION__
......
...@@ -9,6 +9,7 @@ jobs: ...@@ -9,6 +9,7 @@ jobs:
lint: lint:
name: ESLint name: ESLint
runs-on: ubuntu-latest runs-on: ubuntu-latest
if: "!contains(github.event.pull_request.labels.*.name, 'WIP')"
steps: steps:
- name: Checkout repo - name: Checkout repo
uses: actions/checkout@v3 uses: actions/checkout@v3
...@@ -30,6 +31,7 @@ jobs: ...@@ -30,6 +31,7 @@ jobs:
type_check: type_check:
name: TypeScript name: TypeScript
runs-on: ubuntu-latest runs-on: ubuntu-latest
if: "!contains(github.event.pull_request.labels.*.name, 'WIP')"
steps: steps:
- name: Checkout repo - name: Checkout repo
uses: actions/checkout@v3 uses: actions/checkout@v3
......
This diff is collapsed.
This diff is collapsed.
...@@ -5,7 +5,7 @@ blockscout: ...@@ -5,7 +5,7 @@ blockscout:
app: blockscout app: blockscout
enabled: true enabled: true
image: 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: replicas:
app: 1 app: 1
# init container # init container
...@@ -83,9 +83,9 @@ blockscout: ...@@ -83,9 +83,9 @@ blockscout:
_default: prod _default: prod
ECTO_USE_SSL: ECTO_USE_SSL:
_default: 'false' _default: 'false'
ENABLE_RUST_VERIFICATION_SERVICE: MICROSERVICE_SC_VERIFIER_ENABLED:
_default: 'true' _default: 'true'
RUST_VERIFICATION_SERVICE_URL: MICROSERVICE_SC_VERIFIER_URL:
_default: http://sc-verifier-svc:8043 _default: http://sc-verifier-svc:8043
ACCOUNT_ENABLED: ACCOUNT_ENABLED:
_default: 'true' _default: 'true'
......
...@@ -108,9 +108,9 @@ blockscout: ...@@ -108,9 +108,9 @@ blockscout:
_default: 'true' _default: 'true'
CHAIN_ID: CHAIN_ID:
_default: 5 _default: 5
ENABLE_RUST_VERIFICATION_SERVICE: MICROSERVICE_SC_VERIFIER_ENABLED:
_default: 'true' _default: 'true'
RUST_VERIFICATION_SERVICE_URL: MICROSERVICE_SC_VERIFIER_URL:
_default: http://eth-bytecode-db-svc.eth-bytecode-db-testing.svc.cluster.local:80 _default: http://eth-bytecode-db-svc.eth-bytecode-db-testing.svc.cluster.local:80
INDEXER_MEMORY_LIMIT: INDEXER_MEMORY_LIMIT:
_default: 5 _default: 5
...@@ -311,9 +311,9 @@ frontend: ...@@ -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'}}]" _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 # network config
NEXT_PUBLIC_NETWORK_NAME: NEXT_PUBLIC_NETWORK_NAME:
_default: Ethereum _default: Göerli
NEXT_PUBLIC_NETWORK_SHORT_NAME: NEXT_PUBLIC_NETWORK_SHORT_NAME:
_default: Goerli _default: Göerli
NEXT_PUBLIC_NETWORK_ASSETS_PATHNAME: NEXT_PUBLIC_NETWORK_ASSETS_PATHNAME:
_default: ethereum _default: ethereum
NEXT_PUBLIC_NETWORK_TYPE: NEXT_PUBLIC_NETWORK_TYPE:
......
...@@ -39,6 +39,7 @@ frontend: ...@@ -39,6 +39,7 @@ frontend:
- "/csv-export" - "/csv-export"
- "/verified-contracts" - "/verified-contracts"
- "/graphiql" - "/graphiql"
- "/login"
resources: resources:
limits: limits:
...@@ -67,9 +68,9 @@ frontend: ...@@ -67,9 +68,9 @@ frontend:
NEXT_PUBLIC_FOOTER_STAKING_LINK: NEXT_PUBLIC_FOOTER_STAKING_LINK:
_default: https://duneanalytics.com/maxaleks/xdai-staking _default: https://duneanalytics.com/maxaleks/xdai-staking
NEXT_PUBLIC_NETWORK_NAME: NEXT_PUBLIC_NETWORK_NAME:
_default: Ethereum _default: Göerli
NEXT_PUBLIC_NETWORK_SHORT_NAME: NEXT_PUBLIC_NETWORK_SHORT_NAME:
_default: Goerli _default: Göerli
NEXT_PUBLIC_NETWORK_ASSETS_PATHNAME: NEXT_PUBLIC_NETWORK_ASSETS_PATHNAME:
_default: ethereum _default: ethereum
NEXT_PUBLIC_NETWORK_TYPE: NEXT_PUBLIC_NETWORK_TYPE:
......
This diff is collapsed.
import React from 'react'; 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 type { Params as FetchParams } from 'lib/hooks/useFetch';
import useFetch from 'lib/hooks/useFetch'; import useFetch from 'lib/hooks/useFetch';
...@@ -27,7 +27,7 @@ export default function useApiFetch() { ...@@ -27,7 +27,7 @@ export default function useApiFetch() {
url, url,
{ {
credentials: 'include', credentials: 'include',
...(resource.endpoint && appConfig.host === 'localhost' ? { ...(resource.endpoint && isNeedProxy() ? {
headers: { headers: {
'x-endpoint': resource.endpoint, '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 { export default function getErrorStatusCode(error: Error | undefined): number | undefined {
return ( const cause = getErrorCause(error);
error && 'cause' in error && return cause && 'status' in cause && typeof cause.status === 'number' ? cause.status : undefined;
typeof error.cause === 'object' && error.cause !== null &&
'status' in error.cause && typeof error.cause.status === 'number' &&
error.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'; ...@@ -3,6 +3,7 @@ import type { Channel } from 'phoenix';
import type { AddressCoinBalanceHistoryItem } from 'types/api/address'; import type { AddressCoinBalanceHistoryItem } from 'types/api/address';
import type { NewBlockSocketResponse } from 'types/api/block'; import type { NewBlockSocketResponse } from 'types/api/block';
import type { SmartContractVerificationResponse } from 'types/api/contract'; import type { SmartContractVerificationResponse } from 'types/api/contract';
import type { RawTracesResponse } from 'types/api/rawTrace';
import type { TokenTransfer } from 'types/api/tokenTransfer'; import type { TokenTransfer } from 'types/api/tokenTransfer';
import type { Transaction } from 'types/api/transaction'; import type { Transaction } from 'types/api/transaction';
...@@ -10,6 +11,7 @@ export type SocketMessageParams = SocketMessage.NewBlock | ...@@ -10,6 +11,7 @@ export type SocketMessageParams = SocketMessage.NewBlock |
SocketMessage.BlocksIndexStatus | SocketMessage.BlocksIndexStatus |
SocketMessage.InternalTxsIndexStatus | SocketMessage.InternalTxsIndexStatus |
SocketMessage.TxStatusUpdate | SocketMessage.TxStatusUpdate |
SocketMessage.TxRawTrace |
SocketMessage.NewTx | SocketMessage.NewTx |
SocketMessage.NewPendingTx | SocketMessage.NewPendingTx |
SocketMessage.AddressBalance | SocketMessage.AddressBalance |
...@@ -19,7 +21,9 @@ SocketMessage.AddressCoinBalance | ...@@ -19,7 +21,9 @@ SocketMessage.AddressCoinBalance |
SocketMessage.AddressTxs | SocketMessage.AddressTxs |
SocketMessage.AddressTxsPending | SocketMessage.AddressTxsPending |
SocketMessage.AddressTokenTransfer | SocketMessage.AddressTokenTransfer |
SocketMessage.AddressChangedBytecode |
SocketMessage.TokenTransfers | SocketMessage.TokenTransfers |
SocketMessage.TokenTotalSupply |
SocketMessage.ContractVerification | SocketMessage.ContractVerification |
SocketMessage.Unknown; SocketMessage.Unknown;
...@@ -35,6 +39,7 @@ export namespace SocketMessage { ...@@ -35,6 +39,7 @@ export namespace SocketMessage {
export type BlocksIndexStatus = SocketMessageParamsGeneric<'block_index_status', {finished: boolean; ratio: string}>; 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 InternalTxsIndexStatus = SocketMessageParamsGeneric<'internal_txs_index_status', {finished: boolean; ratio: string}>;
export type TxStatusUpdate = SocketMessageParamsGeneric<'collated', NewBlockSocketResponse>; export type TxStatusUpdate = SocketMessageParamsGeneric<'collated', NewBlockSocketResponse>;
export type TxRawTrace = SocketMessageParamsGeneric<'raw_trace', RawTracesResponse>;
export type NewTx = SocketMessageParamsGeneric<'transaction', { transaction: number }>; export type NewTx = SocketMessageParamsGeneric<'transaction', { transaction: number }>;
export type NewPendingTx = SocketMessageParamsGeneric<'pending_transaction', { pending_transaction: number }>; export type NewPendingTx = SocketMessageParamsGeneric<'pending_transaction', { pending_transaction: number }>;
export type AddressBalance = SocketMessageParamsGeneric<'balance', { balance: string; block_number: number; exchange_rate: string }>; export type AddressBalance = SocketMessageParamsGeneric<'balance', { balance: string; block_number: number; exchange_rate: string }>;
...@@ -45,7 +50,9 @@ export namespace SocketMessage { ...@@ -45,7 +50,9 @@ export namespace SocketMessage {
export type AddressTxs = SocketMessageParamsGeneric<'transaction', { transaction: Transaction }>; export type AddressTxs = SocketMessageParamsGeneric<'transaction', { transaction: Transaction }>;
export type AddressTxsPending = SocketMessageParamsGeneric<'pending_transaction', { transaction: Transaction }>; export type AddressTxsPending = SocketMessageParamsGeneric<'pending_transaction', { transaction: Transaction }>;
export type AddressTokenTransfer = SocketMessageParamsGeneric<'token_transfer', { token_transfer: TokenTransfer }>; 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 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 ContractVerification = SocketMessageParamsGeneric<'verification_result', SmartContractVerificationResponse>;
export type Unknown = SocketMessageParamsGeneric<undefined, unknown>; export type Unknown = SocketMessageParamsGeneric<undefined, unknown>;
} }
...@@ -85,7 +85,7 @@ export const erc721: TokenTransfer = { ...@@ -85,7 +85,7 @@ export const erc721: TokenTransfer = {
method: 'updateSmartAsset', method: 'updateSmartAsset',
}; };
export const erc1155: TokenTransfer = { export const erc1155A: TokenTransfer = {
from: { from: {
hash: '0x0000000000000000000000000000000000000000', hash: '0x0000000000000000000000000000000000000000',
implementation_name: null, implementation_name: null,
...@@ -128,26 +128,44 @@ export const erc1155: TokenTransfer = { ...@@ -128,26 +128,44 @@ export const erc1155: TokenTransfer = {
log_index: '1', log_index: '1',
}; };
export const erc1155multiple: TokenTransfer = { export const erc1155B: TokenTransfer = {
...erc1155, ...erc1155A,
token: { token: {
...erc1155.token, ...erc1155A.token,
name: 'SastanaNFT', name: 'SastanaNFT',
symbol: 'ipfs://QmUpFUfVKDCWeZQk5pvDFUxnpQP9N6eLSHhNUy49T1JVtY', symbol: 'ipfs://QmUpFUfVKDCWeZQk5pvDFUxnpQP9N6eLSHhNUy49T1JVtY',
}, },
total: [ total: { token_id: '12345678', value: '100000000000000000000', decimals: null },
{ token_id: '12345678', value: '100000000000000000000', decimals: null }, };
{ token_id: '483200961027732618117991942553110860267520', value: '200000000000000000000', decimals: null },
{ token_id: '456', value: '42', 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 = { export const mixTokens: TokenTransferResponse = {
items: [ items: [
erc20, erc20,
erc721, erc721,
erc1155, erc1155A,
erc1155multiple, erc1155B,
erc1155C,
erc1155D,
], ],
next_page_params: null, next_page_params: null,
}; };
...@@ -100,8 +100,10 @@ export const withTokenTransfer: Transaction = { ...@@ -100,8 +100,10 @@ export const withTokenTransfer: Transaction = {
token_transfers: [ token_transfers: [
tokenTransferMock.erc20, tokenTransferMock.erc20,
tokenTransferMock.erc721, tokenTransferMock.erc721,
tokenTransferMock.erc1155, tokenTransferMock.erc1155A,
tokenTransferMock.erc1155multiple, tokenTransferMock.erc1155B,
tokenTransferMock.erc1155C,
tokenTransferMock.erc1155D,
], ],
tx_types: [ tx_types: [
'token_transfer', 'token_transfer',
......
...@@ -16,6 +16,7 @@ ...@@ -16,6 +16,7 @@
"build": "next build", "build": "next build",
"build:docker": "docker build --build-arg GIT_COMMIT_SHA=$(git rev-parse HEAD) -t blockscout ./", "build:docker": "docker build --build-arg GIT_COMMIT_SHA=$(git rev-parse HEAD) -t blockscout ./",
"start": "next start", "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", "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": "./node_modules/.bin/eslint . --ext .js,.jsx,.ts,.tsx",
"lint:eslint:fix": "./node_modules/.bin/eslint . --ext .js,.jsx,.ts,.tsx --fix", "lint:eslint:fix": "./node_modules/.bin/eslint . --ext .js,.jsx,.ts,.tsx --fix",
......
...@@ -17,4 +17,4 @@ const ApiKeysPage: NextPage = () => { ...@@ -17,4 +17,4 @@ const ApiKeysPage: NextPage = () => {
export default ApiKeysPage; export default ApiKeysPage;
export { getServerSideProps } from 'lib/next/getServerSideProps'; export { getServerSideProps } from 'lib/next/account/getServerSideProps';
...@@ -17,4 +17,4 @@ const CustomAbiPage: NextPage = () => { ...@@ -17,4 +17,4 @@ const CustomAbiPage: NextPage = () => {
export default CustomAbiPage; export default CustomAbiPage;
export { getServerSideProps } from 'lib/next/getServerSideProps'; export { getServerSideProps } from 'lib/next/account/getServerSideProps';
...@@ -17,4 +17,4 @@ const PublicTagsPage: NextPage = () => { ...@@ -17,4 +17,4 @@ const PublicTagsPage: NextPage = () => {
export default PublicTagsPage; export default PublicTagsPage;
export { getServerSideProps } from 'lib/next/getServerSideProps'; export { getServerSideProps } from 'lib/next/account/getServerSideProps';
...@@ -17,4 +17,4 @@ const AddressTagsPage: NextPage = () => { ...@@ -17,4 +17,4 @@ const AddressTagsPage: NextPage = () => {
export default AddressTagsPage; export default AddressTagsPage;
export { getServerSideProps } from 'lib/next/getServerSideProps'; export { getServerSideProps } from 'lib/next/account/getServerSideProps';
...@@ -17,4 +17,4 @@ const VerifiedAddressesPage: NextPage = () => { ...@@ -17,4 +17,4 @@ const VerifiedAddressesPage: NextPage = () => {
export default VerifiedAddressesPage; 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 = () => { ...@@ -19,4 +19,4 @@ const WatchListPage: NextPage = () => {
export default WatchListPage; export default WatchListPage;
export { getServerSideProps } from 'lib/next/getServerSideProps'; export { getServerSideProps } from 'lib/next/account/getServerSideProps';
...@@ -15,4 +15,4 @@ const MyProfilePage: NextPage = () => { ...@@ -15,4 +15,4 @@ const MyProfilePage: NextPage = () => {
export default MyProfilePage; export default MyProfilePage;
export { getServerSideProps } from 'lib/next/getServerSideProps'; export { getServerSideProps } from 'lib/next/account/getServerSideProps';
...@@ -5,6 +5,7 @@ import React from 'react'; ...@@ -5,6 +5,7 @@ import React from 'react';
import getSeo from 'lib/next/block/getSeo'; import getSeo from 'lib/next/block/getSeo';
import Block from 'ui/pages/Block'; import Block from 'ui/pages/Block';
import Page from 'ui/shared/Page/Page';
const BlockPage: NextPage<RoutedQuery<'/block/[height]'>> = ({ height }: RoutedQuery<'/block/[height]'>) => { const BlockPage: NextPage<RoutedQuery<'/block/[height]'>> = ({ height }: RoutedQuery<'/block/[height]'>) => {
const { title, description } = getSeo({ height }); const { title, description } = getSeo({ height });
...@@ -14,7 +15,9 @@ const BlockPage: NextPage<RoutedQuery<'/block/[height]'>> = ({ height }: RoutedQ ...@@ -14,7 +15,9 @@ const BlockPage: NextPage<RoutedQuery<'/block/[height]'>> = ({ height }: RoutedQ
<title>{ title }</title> <title>{ title }</title>
<meta name="description" content={ description }/> <meta name="description" content={ description }/>
</Head> </Head>
<Block/> <Page>
<Block/>
</Page>
</> </>
); );
}; };
......
...@@ -61,6 +61,8 @@ export function sendMessage(socket: WebSocket, channel: Channel, msg: 'transacti ...@@ -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: '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: '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: '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 { export function sendMessage(socket: WebSocket, channel: Channel, msg: string, payload: unknown): void {
socket.send(JSON.stringify([ socket.send(JSON.stringify([
...channel, ...channel,
......
...@@ -166,6 +166,10 @@ export interface VerifiedAddress { ...@@ -166,6 +166,10 @@ export interface VerifiedAddress {
chainId: string; chainId: string;
contractAddress: string; contractAddress: string;
verifiedDate: string; verifiedDate: string;
metadata: {
tokenName: string | null;
tokenSymbol: string | null;
};
} }
export interface VerifiedAddressResponse { export interface VerifiedAddressResponse {
......
...@@ -124,6 +124,7 @@ export interface SmartContractVerificationConfigRaw { ...@@ -124,6 +124,7 @@ export interface SmartContractVerificationConfigRaw {
verification_options: Array<string>; verification_options: Array<string>;
vyper_compiler_versions: Array<string>; vyper_compiler_versions: Array<string>;
vyper_evm_versions: Array<string>; vyper_evm_versions: Array<string>;
is_rust_verifier_microservice_enabled: boolean;
} }
export interface SmartContractVerificationConfig extends SmartContractVerificationConfigRaw { export interface SmartContractVerificationConfig extends SmartContractVerificationConfigRaw {
......
...@@ -19,8 +19,6 @@ export interface TokenCounters { ...@@ -19,8 +19,6 @@ export interface TokenCounters {
transfers_count: string; transfers_count: string;
} }
export type TokenInfoGeneric<Type extends TokenType> = Omit<TokenInfo, 'type'> & { type: Type };
export interface TokenHolders { export interface TokenHolders {
items: Array<TokenHolder>; items: Array<TokenHolder>;
next_page_params: TokenHoldersPagination; next_page_params: TokenHoldersPagination;
......
import type { AddressParam } from './addressParams'; import type { AddressParam } from './addressParams';
import type { TokenInfoGeneric, TokenType } from './token'; import type { TokenInfo, TokenType } from './token';
export type Erc20TotalPayload = { export type Erc20TotalPayload = {
decimals: string | null; decimals: string | null;
...@@ -18,20 +18,20 @@ export type Erc1155TotalPayload = { ...@@ -18,20 +18,20 @@ export type Erc1155TotalPayload = {
export type TokenTransfer = ( export type TokenTransfer = (
{ {
token: TokenInfoGeneric<'ERC-20'>; token: TokenInfo<'ERC-20'>;
total: Erc20TotalPayload; total: Erc20TotalPayload;
} | } |
{ {
token: TokenInfoGeneric<'ERC-721'>; token: TokenInfo<'ERC-721'>;
total: Erc721TotalPayload; total: Erc721TotalPayload;
} | } |
{ {
token: TokenInfoGeneric<'ERC-1155'>; token: TokenInfo<'ERC-1155'>;
total: Erc1155TotalPayload | Array<Erc1155TotalPayload>; total: Erc1155TotalPayload;
} }
) & TokenTransferBase ) & TokenTransferBase
export type TokenTotal = Erc20TotalPayload | Erc721TotalPayload | Erc1155TotalPayload | Array<Erc1155TotalPayload>; export type TokenTotal = Erc20TotalPayload | Erc721TotalPayload | Erc1155TotalPayload;
interface TokenTransferBase { interface TokenTransferBase {
type: 'token_transfer' | 'token_burning' | 'token_spawning' | 'token_minting'; type: 'token_transfer' | 'token_burning' | 'token_spawning' | 'token_minting';
......
...@@ -79,7 +79,7 @@ const AddressBlocksValidated = ({ scrollRef }: Props) => { ...@@ -79,7 +79,7 @@ const AddressBlocksValidated = ({ scrollRef }: Props) => {
<Th width="17%">Block</Th> <Th width="17%">Block</Th>
<Th width="17%">Age</Th> <Th width="17%">Age</Th>
<Th width="16%">Txn</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> <Th width="25%" isNumeric>Reward { appConfig.network.currency.symbol }</Th>
</Tr> </Tr>
</Thead> </Thead>
......
...@@ -2,7 +2,7 @@ import { Box } from '@chakra-ui/react'; ...@@ -2,7 +2,7 @@ import { Box } from '@chakra-ui/react';
import { test, expect } from '@playwright/experimental-ct-react'; import { test, expect } from '@playwright/experimental-ct-react';
import React from 'react'; import React from 'react';
import { erc1155 } from 'mocks/tokens/tokenTransfer'; import { erc1155A } from 'mocks/tokens/tokenTransfer';
import TestApp from 'playwright/TestApp'; import TestApp from 'playwright/TestApp';
import buildApiUrl from 'playwright/utils/buildApiUrl'; import buildApiUrl from 'playwright/utils/buildApiUrl';
...@@ -20,7 +20,7 @@ const hooksConfig = { ...@@ -20,7 +20,7 @@ const hooksConfig = {
test('with token filter and pagination +@mobile', async({ mount, page }) => { test('with token filter and pagination +@mobile', async({ mount, page }) => {
await page.route(API_URL, (route) => route.fulfill({ await page.route(API_URL, (route) => route.fulfill({
status: 200, 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( const component = await mount(
...@@ -37,7 +37,7 @@ test('with token filter and pagination +@mobile', async({ mount, page }) => { ...@@ -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 }) => { test('with token filter and no pagination +@mobile', async({ mount, page }) => {
await page.route(API_URL, (route) => route.fulfill({ await page.route(API_URL, (route) => route.fulfill({
status: 200, status: 200,
body: JSON.stringify({ items: [ erc1155 ] }), body: JSON.stringify({ items: [ erc1155A ] }),
})); }));
const component = await mount( const component = await mount(
......
...@@ -26,7 +26,6 @@ import HashStringShorten from 'ui/shared/HashStringShorten'; ...@@ -26,7 +26,6 @@ import HashStringShorten from 'ui/shared/HashStringShorten';
import Pagination from 'ui/shared/Pagination'; import Pagination from 'ui/shared/Pagination';
import SocketNewItemsNotice from 'ui/shared/SocketNewItemsNotice'; import SocketNewItemsNotice from 'ui/shared/SocketNewItemsNotice';
import TokenLogo from 'ui/shared/TokenLogo'; import TokenLogo from 'ui/shared/TokenLogo';
import { flattenTotal } from 'ui/shared/TokenTransfer/helpers';
import TokenTransferFilter from 'ui/shared/TokenTransfer/TokenTransferFilter'; import TokenTransferFilter from 'ui/shared/TokenTransfer/TokenTransferFilter';
import TokenTransferList from 'ui/shared/TokenTransfer/TokenTransferList'; import TokenTransferList from 'ui/shared/TokenTransfer/TokenTransferList';
import TokenTransferTable from 'ui/shared/TokenTransfer/TokenTransferTable'; import TokenTransferTable from 'ui/shared/TokenTransfer/TokenTransferTable';
...@@ -161,12 +160,11 @@ const AddressTokenTransfers = ({ scrollRef }: {scrollRef?: React.RefObject<HTMLD ...@@ -161,12 +160,11 @@ const AddressTokenTransfers = ({ scrollRef }: {scrollRef?: React.RefObject<HTMLD
const numActiveFilters = (filters.type?.length || 0) + (filters.filter ? 1 : 0); const numActiveFilters = (filters.type?.length || 0) + (filters.filter ? 1 : 0);
const isActionBarHidden = !tokenFilter && !numActiveFilters && !data?.items.length && !currentAddress; const isActionBarHidden = !tokenFilter && !numActiveFilters && !data?.items.length && !currentAddress;
const items = data?.items?.reduce(flattenTotal, []); const content = data?.items ? (
const content = items ? (
<> <>
<Hide below="lg" ssr={ false }> <Hide below="lg" ssr={ false }>
<TokenTransferTable <TokenTransferTable
data={ items } data={ data?.items }
baseAddress={ currentAddress } baseAddress={ currentAddress }
showTxInfo showTxInfo
top={ isActionBarHidden ? 0 : 80 } top={ isActionBarHidden ? 0 : 80 }
...@@ -187,7 +185,7 @@ const AddressTokenTransfers = ({ scrollRef }: {scrollRef?: React.RefObject<HTMLD ...@@ -187,7 +185,7 @@ const AddressTokenTransfers = ({ scrollRef }: {scrollRef?: React.RefObject<HTMLD
/> />
) } ) }
<TokenTransferList <TokenTransferList
data={ items } data={ data?.items }
baseAddress={ currentAddress } baseAddress={ currentAddress }
showTxInfo showTxInfo
enableTimeIncrement enableTimeIncrement
......
import { test, expect } from '@playwright/experimental-ct-react'; import { test as base, expect } from '@playwright/experimental-ct-react';
import React from 'react'; import React from 'react';
import * as contractMock from 'mocks/contract/info'; import * as contractMock from 'mocks/contract/info';
import * as socketServer from 'playwright/fixtures/socketServer';
import TestApp from 'playwright/TestApp'; import TestApp from 'playwright/TestApp';
import buildApiUrl from 'playwright/utils/buildApiUrl'; import buildApiUrl from 'playwright/utils/buildApiUrl';
...@@ -15,6 +16,14 @@ const hooksConfig = { ...@@ -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 }) => { test('verified with changed byte code +@mobile +@dark-mode', async({ mount, page }) => {
await page.route(CONTRACT_API_URL, (route) => route.fulfill({ await page.route(CONTRACT_API_URL, (route) => route.fulfill({
status: 200, status: 200,
...@@ -24,11 +33,32 @@ test('verified with changed byte code +@mobile +@dark-mode', async({ mount, page ...@@ -24,11 +33,32 @@ test('verified with changed byte code +@mobile +@dark-mode', async({ mount, page
const component = await mount( const component = await mount(
<TestApp> <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 }/> <ContractCode addressHash={ addressHash }/>
</TestApp>, </TestApp>,
{ hooksConfig }, { hooksConfig },
); );
const socket = await createSocket();
const channel = await socketServer.joinChannel(socket, 'addresses:' + addressHash.toLowerCase());
socketServer.sendMessage(socket, channel, 'changed_bytecode', {});
await expect(component).toHaveScreenshot(); await expect(component).toHaveScreenshot();
}); });
...@@ -41,7 +71,7 @@ test('verified with multiple sources +@mobile', async({ mount, page }) => { ...@@ -41,7 +71,7 @@ test('verified with multiple sources +@mobile', async({ mount, page }) => {
await mount( await mount(
<TestApp> <TestApp>
<ContractCode addressHash={ addressHash }/> <ContractCode addressHash={ addressHash } noSocket/>
</TestApp>, </TestApp>,
{ hooksConfig }, { hooksConfig },
); );
...@@ -60,7 +90,7 @@ test('verified via sourcify', async({ mount, page }) => { ...@@ -60,7 +90,7 @@ test('verified via sourcify', async({ mount, page }) => {
await mount( await mount(
<TestApp> <TestApp>
<ContractCode addressHash={ addressHash }/> <ContractCode addressHash={ addressHash } noSocket/>
</TestApp>, </TestApp>,
{ hooksConfig }, { hooksConfig },
); );
...@@ -77,7 +107,7 @@ test('self destructed', async({ mount, page }) => { ...@@ -77,7 +107,7 @@ test('self destructed', async({ mount, page }) => {
await mount( await mount(
<TestApp> <TestApp>
<ContractCode addressHash={ addressHash }/> <ContractCode addressHash={ addressHash } noSocket/>
</TestApp>, </TestApp>,
{ hooksConfig }, { hooksConfig },
); );
...@@ -95,7 +125,7 @@ test('with twin address alert +@mobile', async({ mount, page }) => { ...@@ -95,7 +125,7 @@ test('with twin address alert +@mobile', async({ mount, page }) => {
const component = await mount( const component = await mount(
<TestApp> <TestApp>
<ContractCode addressHash={ addressHash }/> <ContractCode addressHash={ addressHash } noSocket/>
</TestApp>, </TestApp>,
{ hooksConfig }, { hooksConfig },
); );
...@@ -112,7 +142,7 @@ test('with proxy address alert +@mobile', async({ mount, page }) => { ...@@ -112,7 +142,7 @@ test('with proxy address alert +@mobile', async({ mount, page }) => {
const component = await mount( const component = await mount(
<TestApp> <TestApp>
<ContractCode addressHash={ addressHash }/> <ContractCode addressHash={ addressHash } noSocket/>
</TestApp>, </TestApp>,
{ hooksConfig }, { hooksConfig },
); );
...@@ -129,7 +159,7 @@ test('non verified', async({ mount, page }) => { ...@@ -129,7 +159,7 @@ test('non verified', async({ mount, page }) => {
const component = await mount( const component = await mount(
<TestApp> <TestApp>
<ContractCode addressHash={ addressHash }/> <ContractCode addressHash={ addressHash } noSocket/>
</TestApp>, </TestApp>,
{ hooksConfig }, { hooksConfig },
); );
......
...@@ -2,8 +2,12 @@ import { Flex, Skeleton, Button, Grid, GridItem, Text, Alert, Link, chakra, Box ...@@ -2,8 +2,12 @@ import { Flex, Skeleton, Button, Grid, GridItem, Text, Alert, Link, chakra, Box
import { route } from 'nextjs-routes'; import { route } from 'nextjs-routes';
import React from 'react'; import React from 'react';
import type { SocketMessage } from 'lib/socket/types';
import useApiQuery from 'lib/api/useApiQuery'; import useApiQuery from 'lib/api/useApiQuery';
import dayjs from 'lib/date/dayjs'; 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 Address from 'ui/shared/address/Address';
import AddressIcon from 'ui/shared/address/AddressIcon'; import AddressIcon from 'ui/shared/address/AddressIcon';
import AddressLink from 'ui/shared/address/AddressLink'; import AddressLink from 'ui/shared/address/AddressLink';
...@@ -16,6 +20,8 @@ import ContractSourceCode from './ContractSourceCode'; ...@@ -16,6 +20,8 @@ import ContractSourceCode from './ContractSourceCode';
type Props = { type Props = {
addressHash?: string; addressHash?: string;
// prop for pw tests only
noSocket?: boolean;
} }
const InfoItem = chakra(({ label, value, className }: { label: string; value: string; className?: string }) => ( 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 ...@@ -25,15 +31,33 @@ const InfoItem = chakra(({ label, value, className }: { label: string; value: st
</GridItem> </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', { const { data, isLoading, isError } = useApiQuery('contract', {
pathParams: { hash: addressHash }, pathParams: { hash: addressHash },
queryOptions: { queryOptions: {
enabled: Boolean(addressHash), enabled: Boolean(addressHash) && (noSocket || isSocketOpen),
refetchOnMount: false, 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) { if (isError) {
return <DataFetchAlert/>; return <DataFetchAlert/>;
} }
...@@ -117,7 +141,7 @@ const ContractCode = ({ addressHash }: Props) => { ...@@ -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> } { data.sourcify_repo_url && <LinkExternal href={ data.sourcify_repo_url } fontSize="md">View contract in Sourcify repository</LinkExternal> }
</Alert> </Alert>
) } ) }
{ data.is_changed_bytecode && ( { (data.is_changed_bytecode || isChangedBytecodeSocket) && (
<Alert status="warning"> <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. Warning! Contract bytecode has been changed and does not match the verified one. Therefore, interaction with this smart contract may be risky.
</Alert> </Alert>
......
...@@ -11,6 +11,8 @@ import AddressVerificationStepAddress from './steps/AddressVerificationStepAddre ...@@ -11,6 +11,8 @@ import AddressVerificationStepAddress from './steps/AddressVerificationStepAddre
import AddressVerificationStepSignature from './steps/AddressVerificationStepSignature'; import AddressVerificationStepSignature from './steps/AddressVerificationStepSignature';
import AddressVerificationStepSuccess from './steps/AddressVerificationStepSuccess'; import AddressVerificationStepSuccess from './steps/AddressVerificationStepSuccess';
type StateData = AddressVerificationFormFirstStepFields & AddressCheckStatusSuccess & { isToken?: boolean };
interface Props { interface Props {
isOpen: boolean; isOpen: boolean;
onClose: () => void; onClose: () => void;
...@@ -22,7 +24,7 @@ interface Props { ...@@ -22,7 +24,7 @@ interface Props {
const AddressVerificationModal = ({ defaultAddress, isOpen, onClose, onSubmit, onAddTokenInfoClick, onShowListClick }: Props) => { const AddressVerificationModal = ({ defaultAddress, isOpen, onClose, onSubmit, onAddTokenInfoClick, onShowListClick }: Props) => {
const [ stepIndex, setStepIndex ] = React.useState(0); 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) => { const handleGoToSecondStep = React.useCallback((firstStepResult: typeof data) => {
setData(firstStepResult); setData(firstStepResult);
...@@ -32,6 +34,7 @@ const AddressVerificationModal = ({ defaultAddress, isOpen, onClose, onSubmit, o ...@@ -32,6 +34,7 @@ const AddressVerificationModal = ({ defaultAddress, isOpen, onClose, onSubmit, o
const handleGoToThirdStep = React.useCallback((address: VerifiedAddress) => { const handleGoToThirdStep = React.useCallback((address: VerifiedAddress) => {
onSubmit(address); onSubmit(address);
setStepIndex((prev) => prev + 1); setStepIndex((prev) => prev + 1);
setData((prev) => ({ ...prev, isToken: Boolean(address.metadata.tokenName) }));
}, [ onSubmit ]); }, [ onSubmit ]);
const handleGoToPrevStep = React.useCallback(() => { const handleGoToPrevStep = React.useCallback(() => {
...@@ -60,7 +63,14 @@ const AddressVerificationModal = ({ defaultAddress, isOpen, onClose, onSubmit, o ...@@ -60,7 +63,14 @@ const AddressVerificationModal = ({ defaultAddress, isOpen, onClose, onSubmit, o
}, },
{ {
title: 'Congrats! Address is verified.', 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]; const step = steps[stepIndex];
......
...@@ -108,7 +108,7 @@ const AddressVerificationStepAddress = ({ defaultAddress, onContinue }: Props) = ...@@ -108,7 +108,7 @@ const AddressVerificationStepAddress = ({ defaultAddress, onContinue }: Props) =
</Button> </Button>
<Box> <Box>
<span>Contact </span> <span>Contact </span>
<Link>support@blockscout.com</Link> <Link href="mailto:help@blockscout.com">help@blockscout.com</Link>
</Box> </Box>
</Flex> </Flex>
</form> </form>
......
...@@ -133,7 +133,7 @@ const AddressVerificationStepSignature = ({ address, signingMessage, contractCre ...@@ -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 = (() => { const rootError = (() => {
switch (formState.errors.root?.type) { switch (formState.errors.root?.type) {
...@@ -215,6 +215,10 @@ const AddressVerificationStepSignature = ({ address, signingMessage, contractCre ...@@ -215,6 +215,10 @@ const AddressVerificationStepSignature = ({ address, signingMessage, contractCre
</Flex> </Flex>
<Flex alignItems="center" mt={ 8 } columnGap={ 5 }> <Flex alignItems="center" mt={ 8 } columnGap={ 5 }>
{ button } { button }
<Box>
<span>Contact </span>
<Link href="mailto:help@blockscout.com">help@blockscout.com</Link>
</Box>
</Flex> </Flex>
</form> </form>
); );
......
...@@ -4,24 +4,28 @@ import React from 'react'; ...@@ -4,24 +4,28 @@ import React from 'react';
interface Props { interface Props {
onShowListClick: () => void; onShowListClick: () => void;
onAddTokenInfoClick: () => void; onAddTokenInfoClick: () => void;
isToken?: boolean;
address: string;
} }
const AddressVerificationStepSuccess = ({ onAddTokenInfoClick, onShowListClick }: Props) => { const AddressVerificationStepSuccess = ({ onAddTokenInfoClick, onShowListClick, isToken, address }: Props) => {
return ( return (
<Box> <Box>
<Alert status="success" flexWrap="wrap" whiteSpace="pre-wrap" wordBreak="break-word" mb={ 3 } display="inline-block"> <Alert status="success" flexWrap="wrap" whiteSpace="pre-wrap" wordBreak="break-word" mb={ 3 } display="inline-block">
<span>The address ownership for </span> <span>The address ownership for </span>
<chakra.span fontWeight={ 700 }>0xaba7161a7fb69c88e16ed9f455ce62b791ee4d03</chakra.span> <chakra.span fontWeight={ 700 }>{ address }</chakra.span>
<span> is verified.</span> <span> is verified.</span>
</Alert> </Alert>
<p>You may now submit the “Add token information” request</p> <p>You may now submit the “Add token information” request</p>
<Flex alignItems="center" mt={ 8 } columnGap={ 5 } flexWrap="wrap" rowGap={ 5 }> <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 View my verified addresses
</Button> </Button>
<Button size="lg" onClick={ onAddTokenInfoClick }> { isToken && (
Add token information <Button size="lg" onClick={ onAddTokenInfoClick }>
</Button> Add token information
</Button>
) }
</Flex> </Flex>
</Box> </Box>
); );
......
import type { VerifiedAddress } from 'types/api/account';
export interface AddressVerificationFormFirstStepFields { export interface AddressVerificationFormFirstStepFields {
address: string; address: string;
} }
...@@ -34,12 +36,7 @@ export interface AddressVerificationResponseError { ...@@ -34,12 +36,7 @@ export interface AddressVerificationResponseError {
export type AddressValidationResponseSuccess = { export type AddressValidationResponseSuccess = {
status: 'SUCCESS'; status: 'SUCCESS';
result: { result: {
verifiedAddress: { verifiedAddress: VerifiedAddress;
chainId: string;
contractAddress: string;
userId: string;
verifiedDate: string;
};
}; };
} | } |
{ {
......
...@@ -141,7 +141,7 @@ const BlockDetails = ({ query }: Props) => { ...@@ -141,7 +141,7 @@ const BlockDetails = ({ query }: Props) => {
{ /* api doesn't return the block processing time yet */ } { /* api doesn't return the block processing time yet */ }
{ /* <Text>{ dayjs.duration(block.minedIn, 'second').humanize(true) }</Text> */ } { /* <Text>{ dayjs.duration(block.minedIn, 'second').humanize(true) }</Text> */ }
</DetailsInfoItem> </DetailsInfoItem>
{ !totalReward.isEqualTo(ZERO) && ( { !appConfig.L2.isL2Network && !totalReward.isEqualTo(ZERO) && (
<DetailsInfoItem <DetailsInfoItem
title="Block reward" title="Block reward"
hint={ hint={
......
...@@ -77,20 +77,24 @@ const BlocksListItem = ({ data, isPending, enableTimeIncrement }: Props) => { ...@@ -77,20 +77,24 @@ const BlocksListItem = ({ data, isPending, enableTimeIncrement }: Props) => {
) } ) }
</Flex> </Flex>
</Box> </Box>
<Flex columnGap={ 2 }> { !appConfig.L2.isL2Network && (
<Text fontWeight={ 500 }>Reward { appConfig.network.currency.symbol }</Text> <Flex columnGap={ 2 }>
<Text variant="secondary">{ totalReward.toFixed() }</Text> <Text fontWeight={ 500 }>Reward { appConfig.network.currency.symbol }</Text>
</Flex> <Text variant="secondary">{ totalReward.toFixed() }</Text>
<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> </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> </ListItemMobile>
); );
}; };
......
...@@ -24,11 +24,11 @@ const BlocksTable = ({ data, top, page }: Props) => { ...@@ -24,11 +24,11 @@ const BlocksTable = ({ data, top, page }: Props) => {
<Tr> <Tr>
<Th width="125px">Block</Th> <Th width="125px">Block</Th>
<Th width="120px">Size, bytes</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="64px" isNumeric>Txn</Th>
<Th width="35%">Gas used</Th> <Th width={ appConfig.L2.isL2Network ? '63%' : '35%' }>Gas used</Th>
<Th width="22%">Reward { appConfig.network.currency.symbol }</Th> { !appConfig.L2.isL2Network && <Th width="22%">Reward { appConfig.network.currency.symbol }</Th> }
<Th width="22%">Burnt fees { appConfig.network.currency.symbol }</Th> { !appConfig.L2.isL2Network && <Th width="22%">Burnt fees { appConfig.network.currency.symbol }</Th> }
</Tr> </Tr>
</Thead> </Thead>
<Tbody> <Tbody>
......
...@@ -6,6 +6,7 @@ import React from 'react'; ...@@ -6,6 +6,7 @@ import React from 'react';
import type { Block } from 'types/api/block'; import type { Block } from 'types/api/block';
import appConfig from 'configs/app/config';
import flameIcon from 'icons/flame.svg'; import flameIcon from 'icons/flame.svg';
import getBlockTotalReward from 'lib/block/getBlockTotalReward'; import getBlockTotalReward from 'lib/block/getBlockTotalReward';
import { WEI } from 'lib/consts'; import { WEI } from 'lib/consts';
...@@ -28,7 +29,7 @@ const BlocksTableItem = ({ data, isPending, enableTimeIncrement }: Props) => { ...@@ -28,7 +29,7 @@ const BlocksTableItem = ({ data, isPending, enableTimeIncrement }: Props) => {
const txFees = BigNumber(data.tx_fees || 0); const txFees = BigNumber(data.tx_fees || 0);
const separatorColor = useColorModeValue('gray.200', 'gray.700'); const separatorColor = useColorModeValue('gray.200', 'gray.700');
const burntFeesIconColor = useColorModeValue('gray.500', 'inherit');
return ( return (
<Tr <Tr
as={ motion.tr } as={ motion.tr }
...@@ -63,34 +64,38 @@ const BlocksTableItem = ({ data, isPending, enableTimeIncrement }: Props) => { ...@@ -63,34 +64,38 @@ const BlocksTableItem = ({ data, isPending, enableTimeIncrement }: Props) => {
</LinkInternal> </LinkInternal>
) : data.tx_count } ) : data.tx_count }
</Td> </Td>
<Td fontSize="sm"> { !appConfig.L2.isL2Network && (
<Box>{ BigNumber(data.gas_used || 0).toFormat() }</Box> <Td fontSize="sm">
<Flex mt={ 2 }> <Box>{ BigNumber(data.gas_used || 0).toFormat() }</Box>
<Tooltip label="Gas Used %"> <Flex mt={ 2 }>
<Box> <Tooltip label="Gas Used %">
<Utilization colorScheme="gray" value={ BigNumber(data.gas_used || 0).dividedBy(BigNumber(data.gas_limit)).toNumber() }/> <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> </Box>
</Tooltip> </Tooltip>
{ data.gas_target_percentage && ( </Td>
<> ) }
<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>
</Tr> </Tr>
); );
}; };
......
...@@ -16,6 +16,7 @@ const hooksConfig = { ...@@ -16,6 +16,7 @@ const hooksConfig = {
const hash = '0x2F99338637F027CFB7494E46B49987457beCC6E3'; const hash = '0x2F99338637F027CFB7494E46B49987457beCC6E3';
const formConfig: SmartContractVerificationConfig = { const formConfig: SmartContractVerificationConfig = {
is_rust_verifier_microservice_enabled: true,
solidity_compiler_versions: [ solidity_compiler_versions: [
'v0.8.17+commit.8df45f5f', 'v0.8.17+commit.8df45f5f',
'v0.8.16+commit.07a7930e', 'v0.8.16+commit.07a7930e',
......
...@@ -22,15 +22,6 @@ import ContractVerificationVyperContract from './methods/ContractVerificationVyp ...@@ -22,15 +22,6 @@ import ContractVerificationVyperContract from './methods/ContractVerificationVyp
import ContractVerificationVyperMultiPartFile from './methods/ContractVerificationVyperMultiPartFile'; import ContractVerificationVyperMultiPartFile from './methods/ContractVerificationVyperMultiPartFile';
import { prepareRequestBody, formatSocketErrors, getDefaultValues } from './utils'; 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 { interface Props {
method?: SmartContractVerificationMethod; method?: SmartContractVerificationMethod;
config: SmartContractVerificationConfig; config: SmartContractVerificationConfig;
...@@ -122,8 +113,18 @@ const ContractVerificationForm = ({ method: methodFromQuery, config, hash }: Pro ...@@ -122,8 +113,18 @@ const ContractVerificationForm = ({ method: methodFromQuery, config, hash }: Pro
handler: handleNewSocketMessage, 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 method = watch('method');
const content = METHOD_COMPONENTS[method?.value] || null; const content = methods[method?.value] || null;
const methodValue = method?.value; const methodValue = method?.value;
useUpdateEffect(() => { useUpdateEffect(() => {
......
...@@ -11,9 +11,10 @@ import ContractVerificationFormRow from '../ContractVerificationFormRow'; ...@@ -11,9 +11,10 @@ import ContractVerificationFormRow from '../ContractVerificationFormRow';
interface Props { interface Props {
hint?: string; hint?: string;
isReadOnly?: boolean;
} }
const ContractVerificationFieldName = ({ hint }: Props) => { const ContractVerificationFieldName = ({ hint, isReadOnly }: Props) => {
const { formState, control } = useFormContext<FormFields>(); const { formState, control } = useFormContext<FormFields>();
const renderControl = React.useCallback(({ field }: {field: ControllerRenderProps<FormFields, 'name'>}) => { const renderControl = React.useCallback(({ field }: {field: ControllerRenderProps<FormFields, 'name'>}) => {
...@@ -26,13 +27,13 @@ const ContractVerificationFieldName = ({ hint }: Props) => { ...@@ -26,13 +27,13 @@ const ContractVerificationFieldName = ({ hint }: Props) => {
required required
isInvalid={ Boolean(error) } isInvalid={ Boolean(error) }
maxLength={ 255 } maxLength={ 255 }
isDisabled={ formState.isSubmitting } isDisabled={ formState.isSubmitting || isReadOnly }
autoComplete="off" autoComplete="off"
/> />
<InputPlaceholder text="Contract name" error={ error }/> <InputPlaceholder text="Contract name" error={ error }/>
</FormControl> </FormControl>
); );
}, [ formState.errors, formState.isSubmitting ]); }, [ formState.errors, formState.isSubmitting, isReadOnly ]);
return ( return (
<ContractVerificationFormRow> <ContractVerificationFormRow>
......
import React from 'react'; import React from 'react';
import type { SmartContractVerificationConfig } from 'types/api/contract';
import ContractVerificationMethod from '../ContractVerificationMethod'; import ContractVerificationMethod from '../ContractVerificationMethod';
import ContractVerificationFieldAutodetectArgs from '../fields/ContractVerificationFieldAutodetectArgs'; import ContractVerificationFieldAutodetectArgs from '../fields/ContractVerificationFieldAutodetectArgs';
import ContractVerificationFieldCode from '../fields/ContractVerificationFieldCode'; import ContractVerificationFieldCode from '../fields/ContractVerificationFieldCode';
...@@ -10,16 +12,16 @@ import ContractVerificationFieldLibraries from '../fields/ContractVerificationFi ...@@ -10,16 +12,16 @@ import ContractVerificationFieldLibraries from '../fields/ContractVerificationFi
import ContractVerificationFieldName from '../fields/ContractVerificationFieldName'; import ContractVerificationFieldName from '../fields/ContractVerificationFieldName';
import ContractVerificationFieldOptimization from '../fields/ContractVerificationFieldOptimization'; import ContractVerificationFieldOptimization from '../fields/ContractVerificationFieldOptimization';
const ContractVerificationFlattenSourceCode = () => { const ContractVerificationFlattenSourceCode = ({ config }: { config: SmartContractVerificationConfig }) => {
return ( return (
<ContractVerificationMethod title="Contract verification via Solidity (flattened source code)"> <ContractVerificationMethod title="Contract verification via Solidity (flattened source code)">
<ContractVerificationFieldName/> { !config?.is_rust_verifier_microservice_enabled && <ContractVerificationFieldName/> }
<ContractVerificationFieldIsYul/> { config?.is_rust_verifier_microservice_enabled && <ContractVerificationFieldIsYul/> }
<ContractVerificationFieldCompiler/> <ContractVerificationFieldCompiler/>
<ContractVerificationFieldEvmVersion/> <ContractVerificationFieldEvmVersion/>
<ContractVerificationFieldOptimization/> <ContractVerificationFieldOptimization/>
<ContractVerificationFieldCode/> <ContractVerificationFieldCode/>
<ContractVerificationFieldAutodetectArgs/> { !config?.is_rust_verifier_microservice_enabled && <ContractVerificationFieldAutodetectArgs/> }
<ContractVerificationFieldLibraries/> <ContractVerificationFieldLibraries/>
</ContractVerificationMethod> </ContractVerificationMethod>
); );
......
import React from 'react'; import React from 'react';
import type { SmartContractVerificationConfig } from 'types/api/contract';
import ContractVerificationMethod from '../ContractVerificationMethod'; import ContractVerificationMethod from '../ContractVerificationMethod';
import ContractVerificationFieldCode from '../fields/ContractVerificationFieldCode'; import ContractVerificationFieldCode from '../fields/ContractVerificationFieldCode';
import ContractVerificationFieldCompiler from '../fields/ContractVerificationFieldCompiler'; import ContractVerificationFieldCompiler from '../fields/ContractVerificationFieldCompiler';
import ContractVerificationFieldConstructorArgs from '../fields/ContractVerificationFieldConstructorArgs'; import ContractVerificationFieldConstructorArgs from '../fields/ContractVerificationFieldConstructorArgs';
import ContractVerificationFieldEvmVersion from '../fields/ContractVerificationFieldEvmVersion';
import ContractVerificationFieldName from '../fields/ContractVerificationFieldName'; import ContractVerificationFieldName from '../fields/ContractVerificationFieldName';
const ContractVerificationVyperContract = () => { const ContractVerificationVyperContract = ({ config }: { config: SmartContractVerificationConfig }) => {
return ( return (
<ContractVerificationMethod title="Contract verification via Vyper (contract)"> <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/> <ContractVerificationFieldCompiler isVyper/>
{ config?.is_rust_verifier_microservice_enabled && <ContractVerificationFieldEvmVersion isVyper/> }
<ContractVerificationFieldCode isVyper/> <ContractVerificationFieldCode isVyper/>
<ContractVerificationFieldConstructorArgs/> { !config?.is_rust_verifier_microservice_enabled && <ContractVerificationFieldConstructorArgs/> }
</ContractVerificationMethod> </ContractVerificationMethod>
); );
}; };
......
...@@ -14,7 +14,7 @@ interface MethodOption { ...@@ -14,7 +14,7 @@ interface MethodOption {
export interface FormFieldsFlattenSourceCode { export interface FormFieldsFlattenSourceCode {
method: MethodOption; method: MethodOption;
is_yul: boolean; is_yul: boolean;
name: string; name: string | undefined;
compiler: Option | null; compiler: Option | null;
evm_version: Option | null; evm_version: Option | null;
is_optimization_enabled: boolean; is_optimization_enabled: boolean;
...@@ -53,9 +53,10 @@ export interface FormFieldsMultiPartFile { ...@@ -53,9 +53,10 @@ export interface FormFieldsMultiPartFile {
export interface FormFieldsVyperContract { export interface FormFieldsVyperContract {
method: MethodOption; method: MethodOption;
name: string; name: string;
evm_version: Option | null;
compiler: Option | null; compiler: Option | null;
code: string; code: string;
constructor_args: string; constructor_args: string | undefined;
} }
export interface FormFieldsVyperMultiPartFile { export interface FormFieldsVyperMultiPartFile {
......
...@@ -84,8 +84,9 @@ export const DEFAULT_VALUES: Record<SmartContractVerificationMethod, FormFields> ...@@ -84,8 +84,9 @@ export const DEFAULT_VALUES: Record<SmartContractVerificationMethod, FormFields>
value: 'vyper-code' as const, value: 'vyper-code' as const,
label: METHOD_LABELS['vyper-code'], label: METHOD_LABELS['vyper-code'],
}, },
name: '', name: 'Vyper_contract',
compiler: null, compiler: null,
evm_version: null,
code: '', code: '',
constructor_args: '', constructor_args: '',
}, },
...@@ -113,6 +114,13 @@ export function getDefaultValues(method: SmartContractVerificationMethod, config ...@@ -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; return defaultValues;
} }
...@@ -145,7 +153,7 @@ export function prepareRequestBody(data: FormFields): FetchParams['body'] { ...@@ -145,7 +153,7 @@ export function prepareRequestBody(data: FormFields): FetchParams['body'] {
is_optimization_enabled: _data.is_optimization_enabled, is_optimization_enabled: _data.is_optimization_enabled,
is_yul_contract: _data.is_yul, is_yul_contract: _data.is_yul,
optimization_runs: _data.optimization_runs, optimization_runs: _data.optimization_runs,
contract_name: _data.name, contract_name: _data.name || undefined,
libraries: reduceLibrariesArray(_data.libraries), libraries: reduceLibrariesArray(_data.libraries),
evm_version: _data.evm_version?.value, evm_version: _data.evm_version?.value,
autodetect_constructor_args: _data.autodetect_constructor_args, autodetect_constructor_args: _data.autodetect_constructor_args,
...@@ -196,6 +204,7 @@ export function prepareRequestBody(data: FormFields): FetchParams['body'] { ...@@ -196,6 +204,7 @@ export function prepareRequestBody(data: FormFields): FetchParams['body'] {
return { return {
compiler_version: _data.compiler?.value, compiler_version: _data.compiler?.value,
evm_version: _data.evm_version?.value,
source_code: _data.code, source_code: _data.code,
contract_name: _data.name, contract_name: _data.name,
constructor_args: _data.constructor_args, constructor_args: _data.constructor_args,
......
...@@ -7,6 +7,7 @@ import React from 'react'; ...@@ -7,6 +7,7 @@ import React from 'react';
import type { SocketMessage } from 'lib/socket/types'; import type { SocketMessage } from 'lib/socket/types';
import type { Block } from 'types/api/block'; import type { Block } from 'types/api/block';
import appConfig from 'configs/app/config';
import useApiQuery, { getResourceKey } from 'lib/api/useApiQuery'; import useApiQuery, { getResourceKey } from 'lib/api/useApiQuery';
import useIsMobile from 'lib/hooks/useIsMobile'; import useIsMobile from 'lib/hooks/useIsMobile';
import { nbsp } from 'lib/html-entities'; import { nbsp } from 'lib/html-entities';
...@@ -17,12 +18,20 @@ import LinkInternal from 'ui/shared/LinkInternal'; ...@@ -17,12 +18,20 @@ import LinkInternal from 'ui/shared/LinkInternal';
import LatestBlocksItem from './LatestBlocksItem'; import LatestBlocksItem from './LatestBlocksItem';
import LatestBlocksItemSkeleton from './LatestBlocksItemSkeleton'; import LatestBlocksItemSkeleton from './LatestBlocksItemSkeleton';
const BLOCK_HEIGHT = 166; const BLOCK_HEIGHT_L1 = 166;
const BLOCK_HEIGHT_L2 = 112;
const BLOCK_MARGIN = 12; const BLOCK_MARGIN = 12;
const LatestBlocks = () => { const LatestBlocks = () => {
const blockHeight = appConfig.L2.isL2Network ? BLOCK_HEIGHT_L2 : BLOCK_HEIGHT_L1;
const isMobile = useIsMobile(); 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 { data, isLoading, isError } = useApiQuery('homepage_blocks');
const queryClient = useQueryClient(); const queryClient = useQueryClient();
...@@ -60,7 +69,7 @@ const LatestBlocks = () => { ...@@ -60,7 +69,7 @@ const LatestBlocks = () => {
<VStack <VStack
spacing={ `${ BLOCK_MARGIN }px` } spacing={ `${ BLOCK_MARGIN }px` }
mb={ 6 } mb={ 6 }
height={ `${ BLOCK_HEIGHT * blocksMaxCount + BLOCK_MARGIN * (blocksMaxCount - 1) }px` } height={ `${ blockHeight * blocksMaxCount + BLOCK_MARGIN * (blocksMaxCount - 1) }px` }
overflow="hidden" overflow="hidden"
> >
{ Array.from(Array(blocksMaxCount)).map((item, index) => <LatestBlocksItemSkeleton key={ index }/>) } { Array.from(Array(blocksMaxCount)).map((item, index) => <LatestBlocksItemSkeleton key={ index }/>) }
...@@ -92,9 +101,9 @@ const LatestBlocks = () => { ...@@ -92,9 +101,9 @@ const LatestBlocks = () => {
</Text> </Text>
</Box> </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 } > <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> </AnimatePresence>
</VStack> </VStack>
<Flex justifyContent="center"> <Flex justifyContent="center">
......
...@@ -13,6 +13,7 @@ import React from 'react'; ...@@ -13,6 +13,7 @@ import React from 'react';
import type { Block } from 'types/api/block'; import type { Block } from 'types/api/block';
import appConfig from 'configs/app/config';
import blockIcon from 'icons/block.svg'; import blockIcon from 'icons/block.svg';
import getBlockTotalReward from 'lib/block/getBlockTotalReward'; import getBlockTotalReward from 'lib/block/getBlockTotalReward';
import BlockTimestamp from 'ui/blocks/BlockTimestamp'; import BlockTimestamp from 'ui/blocks/BlockTimestamp';
...@@ -56,11 +57,14 @@ const LatestBlocksItem = ({ block, h }: Props) => { ...@@ -56,11 +57,14 @@ const LatestBlocksItem = ({ block, h }: Props) => {
<Grid gridGap={ 2 } templateColumns="auto minmax(0, 1fr)" fontSize="sm"> <Grid gridGap={ 2 } templateColumns="auto minmax(0, 1fr)" fontSize="sm">
<GridItem>Txn</GridItem> <GridItem>Txn</GridItem>
<GridItem><Text variant="secondary">{ block.tx_count }</Text></GridItem> <GridItem><Text variant="secondary">{ block.tx_count }</Text></GridItem>
{ /* */ } { !appConfig.L2.isL2Network && (
<GridItem>Reward</GridItem> <>
<GridItem><Text variant="secondary">{ totalReward.toFixed() }</Text></GridItem> <GridItem>Reward</GridItem>
<GridItem>Miner</GridItem> <GridItem><Text variant="secondary">{ totalReward.toFixed() }</Text></GridItem>
<GridItem><AddressLink type="address" alias={ block.miner.name } hash={ block.miner.hash } truncation="constant" maxW="100%"/></GridItem> <GridItem>Miner</GridItem>
<GridItem><AddressLink type="address" alias={ block.miner.name } hash={ block.miner.hash } truncation="constant" maxW="100%"/></GridItem>
</>
) }
</Grid> </Grid>
</Box> </Box>
); );
......
...@@ -8,6 +8,8 @@ import { ...@@ -8,6 +8,8 @@ import {
} from '@chakra-ui/react'; } from '@chakra-ui/react';
import React from 'react'; import React from 'react';
import appConfig from 'configs/app/config';
const LatestBlocksItemSkeleton = () => { const LatestBlocksItemSkeleton = () => {
return ( return (
<Box <Box
...@@ -27,10 +29,14 @@ const LatestBlocksItemSkeleton = () => { ...@@ -27,10 +29,14 @@ const LatestBlocksItemSkeleton = () => {
<Grid gridGap={ 2 } templateColumns="auto minmax(0, 1fr)" fontSize="sm"> <Grid gridGap={ 2 } templateColumns="auto minmax(0, 1fr)" fontSize="sm">
<GridItem><Skeleton w="30px" h="15px"/></GridItem> <GridItem><Skeleton w="30px" h="15px"/></GridItem>
<GridItem><Skeleton w="93px" h="15px"/></GridItem> <GridItem><Skeleton w="93px" h="15px"/></GridItem>
<GridItem><Skeleton w="30px" h="15px"/></GridItem> { !appConfig.L2.isL2Network && (
<GridItem><Skeleton w="93px" h="15px"/></GridItem> <>
<GridItem><Skeleton w="30px" h="15px"/></GridItem> <GridItem><Skeleton w="30px" h="15px"/></GridItem>
<GridItem><Skeleton w="93px" 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> </Grid>
</Box> </Box>
); );
......
...@@ -11,7 +11,6 @@ import useQueryWithPages from 'lib/hooks/useQueryWithPages'; ...@@ -11,7 +11,6 @@ import useQueryWithPages from 'lib/hooks/useQueryWithPages';
import getQueryParamString from 'lib/router/getQueryParamString'; import getQueryParamString from 'lib/router/getQueryParamString';
import BlockDetails from 'ui/block/BlockDetails'; import BlockDetails from 'ui/block/BlockDetails';
import TextAd from 'ui/shared/ad/TextAd'; import TextAd from 'ui/shared/ad/TextAd';
import Page from 'ui/shared/Page/Page';
import PageTitle from 'ui/shared/Page/PageTitle'; import PageTitle from 'ui/shared/Page/PageTitle';
import Pagination from 'ui/shared/Pagination'; import Pagination from 'ui/shared/Pagination';
import RoutedTabs from 'ui/shared/RoutedTabs/RoutedTabs'; import RoutedTabs from 'ui/shared/RoutedTabs/RoutedTabs';
...@@ -39,7 +38,7 @@ const BlockPageContent = () => { ...@@ -39,7 +38,7 @@ const BlockPageContent = () => {
resourceName: 'block_txs', resourceName: 'block_txs',
pathParams: { height }, pathParams: { height },
options: { options: {
enabled: Boolean(height && tab === 'txs'), enabled: Boolean(blockQuery.data?.height && tab === 'txs'),
}, },
}); });
...@@ -47,6 +46,10 @@ const BlockPageContent = () => { ...@@ -47,6 +46,10 @@ const BlockPageContent = () => {
throw new Error('Block not found', { cause: { status: 404 } }); 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(() => ([ const tabs: Array<RoutedTab> = React.useMemo(() => ([
{ id: 'index', title: 'Details', component: <BlockDetails query={ blockQuery }/> }, { id: 'index', title: 'Details', component: <BlockDetails query={ blockQuery }/> },
{ id: 'txs', title: 'Transactions', component: <TxsContent query={ blockTxsQuery } showBlockInfo={ false } showSocketInfo={ false }/> }, { id: 'txs', title: 'Transactions', component: <TxsContent query={ blockTxsQuery } showBlockInfo={ false } showSocketInfo={ false }/> },
...@@ -68,7 +71,7 @@ const BlockPageContent = () => { ...@@ -68,7 +71,7 @@ const BlockPageContent = () => {
}, [ appProps.referrer ]); }, [ appProps.referrer ]);
return ( return (
<Page> <>
{ blockQuery.isLoading ? <Skeleton h={{ base: 12, lg: 6 }} mb={ 6 } w="100%" maxW="680px"/> : <TextAd mb={ 6 }/> } { blockQuery.isLoading ? <Skeleton h={{ base: 12, lg: 6 }} mb={ 6 } w="100%" maxW="680px"/> : <TextAd mb={ 6 }/> }
{ blockQuery.isLoading ? ( { blockQuery.isLoading ? (
<Skeleton h={ 10 } w="300px" mb={ 6 }/> <Skeleton h={ 10 } w="300px" mb={ 6 }/>
...@@ -84,7 +87,7 @@ const BlockPageContent = () => { ...@@ -84,7 +87,7 @@ const BlockPageContent = () => {
rightSlot={ hasPagination ? <Pagination { ...blockTxsQuery.pagination }/> : null } rightSlot={ hasPagination ? <Pagination { ...blockTxsQuery.pagination }/> : null }
stickyEnabled={ hasPagination } 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 React from 'react';
import { token as contract } from 'mocks/address/address'; import { token as contract } from 'mocks/address/address';
import { tokenInfo, tokenCounters } from 'mocks/tokens/tokenInfo'; import { tokenInfo, tokenCounters } from 'mocks/tokens/tokenInfo';
import * as socketServer from 'playwright/fixtures/socketServer';
import TestApp from 'playwright/TestApp'; import TestApp from 'playwright/TestApp';
import buildApiUrl from 'playwright/utils/buildApiUrl'; import buildApiUrl from 'playwright/utils/buildApiUrl';
import insertAdPlaceholder from 'playwright/utils/insertAdPlaceholder'; import insertAdPlaceholder from 'playwright/utils/insertAdPlaceholder';
...@@ -20,8 +21,15 @@ const hooksConfig = { ...@@ -20,8 +21,15 @@ const hooksConfig = {
}, },
}; };
// FIXME: idk why mobile test doesn't work (it's ok locally) const test = base.extend<socketServer.SocketServerFixture>({
test('base view +@mobile +@dark-mode', async({ mount, page }) => { 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({ await page.route('https://request-global.czilladx.com/serve/native.php?z=19260bf627546ab7242', (route) => route.fulfill({
status: 200, status: 200,
body: '', body: '',
...@@ -43,15 +51,41 @@ test('base view +@mobile +@dark-mode', async({ mount, page }) => { ...@@ -43,15 +51,41 @@ test('base view +@mobile +@dark-mode', async({ mount, page }) => {
status: 200, status: 200,
body: JSON.stringify({}), body: JSON.stringify({}),
})); }));
});
test('base view', async({ mount, page, createSocket }) => {
const component = await mount( const component = await mount(
<TestApp> <TestApp withSocket>
<Token/> <Token/>
</TestApp>, </TestApp>,
{ hooksConfig }, { 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 insertAdPlaceholder(page);
await expect(component.locator('main')).toHaveScreenshot(); 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 { Skeleton, Box, Flex, SkeletonCircle, Icon, Tag } from '@chakra-ui/react';
import { useQueryClient } from '@tanstack/react-query';
import { useRouter } from 'next/router'; import { useRouter } from 'next/router';
import React, { useEffect } from 'react'; 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 type { RoutedTab } from 'ui/shared/RoutedTabs/types';
import appConfig from 'configs/app/config'; import appConfig from 'configs/app/config';
import iconSuccess from 'icons/status/success.svg'; 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 { useAppContext } from 'lib/appContext';
import useContractTabs from 'lib/hooks/useContractTabs'; import useContractTabs from 'lib/hooks/useContractTabs';
import useIsMobile from 'lib/hooks/useIsMobile'; import useIsMobile from 'lib/hooks/useIsMobile';
import useQueryWithPages from 'lib/hooks/useQueryWithPages'; 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 trimTokenSymbol from 'lib/token/trimTokenSymbol';
import AddressContract from 'ui/address/AddressContract'; import AddressContract from 'ui/address/AddressContract';
import TextAd from 'ui/shared/ad/TextAd'; import TextAd from 'ui/shared/ad/TextAd';
...@@ -30,6 +35,8 @@ import TokenTransfer from 'ui/token/TokenTransfer/TokenTransfer'; ...@@ -30,6 +35,8 @@ import TokenTransfer from 'ui/token/TokenTransfer/TokenTransfer';
export type TokenTabs = 'token_transfers' | 'holders' | 'inventory'; export type TokenTabs = 'token_transfers' | 'holders' | 'inventory';
const TokenPageContent = () => { const TokenPageContent = () => {
const [ isSocketOpen, setIsSocketOpen ] = React.useState(false);
const [ totalSupplySocket, setTotalSupplySocket ] = React.useState<number>();
const router = useRouter(); const router = useRouter();
const isMobile = useIsMobile(); const isMobile = useIsMobile();
...@@ -39,9 +46,44 @@ const TokenPageContent = () => { ...@@ -39,9 +46,44 @@ const TokenPageContent = () => {
const hashString = router.query.hash?.toString(); const hashString = router.query.hash?.toString();
const queryClient = useQueryClient();
const tokenQuery = useApiQuery('token', { const tokenQuery = useApiQuery('token', {
pathParams: { hash: hashString }, 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(() => { 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 { useQueryClient } from '@tanstack/react-query';
import { useRouter } from 'next/router'; import { useRouter } from 'next/router';
import React from 'react'; import React from 'react';
...@@ -182,6 +182,11 @@ const VerifiedAddresses = () => { ...@@ -182,6 +182,11 @@ const VerifiedAddresses = () => {
<chakra.div mt={ 5 }> <chakra.div mt={ 5 }>
Once these steps are complete, click the Add address button below to get started. Once these steps are complete, click the Add address button below to get started.
</chakra.div> </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> </AccountPageDescription>
<DataListDisplay <DataListDisplay
isLoading={ addressesQuery.isLoading || applicationsQuery.isLoading } 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'; ...@@ -2,9 +2,11 @@ import { Flex } from '@chakra-ui/react';
import React from 'react'; import React from 'react';
import getErrorStatusCode from 'lib/errors/getErrorStatusCode'; import getErrorStatusCode from 'lib/errors/getErrorStatusCode';
import getResourceErrorPayload from 'lib/errors/getResourceErrorPayload';
import useAdblockDetect from 'lib/hooks/useAdblockDetect'; import useAdblockDetect from 'lib/hooks/useAdblockDetect';
import useGetCsrfToken from 'lib/hooks/useGetCsrfToken'; import useGetCsrfToken from 'lib/hooks/useGetCsrfToken';
import AppError from 'ui/shared/AppError/AppError'; import AppError from 'ui/shared/AppError/AppError';
import AppErrorBlockConsensus from 'ui/shared/AppError/AppErrorBlockConsensus';
import ErrorBoundary from 'ui/shared/ErrorBoundary'; import ErrorBoundary from 'ui/shared/ErrorBoundary';
import ErrorInvalidTxHash from 'ui/shared/ErrorInvalidTxHash'; import ErrorInvalidTxHash from 'ui/shared/ErrorInvalidTxHash';
import PageContent from 'ui/shared/Page/PageContent'; import PageContent from 'ui/shared/Page/PageContent';
...@@ -31,15 +33,27 @@ const Page = ({ ...@@ -31,15 +33,27 @@ const Page = ({
const renderErrorScreen = React.useCallback((error?: Error) => { const renderErrorScreen = React.useCallback((error?: Error) => {
const statusCode = getErrorStatusCode(error) || 500; 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 isInvalidTxHash = error?.message.includes('Invalid tx hash');
const isBlockConsensus = messageInPayload?.includes('Block lost consensus');
if (isInvalidTxHash) {
return <PageContent isHomePage={ isHomePage }><ErrorInvalidTxHash/></PageContent>;
}
if (wrapChildren) { if (isBlockConsensus) {
const content = isInvalidTxHash ? <ErrorInvalidTxHash/> : <AppError statusCode={ statusCode } mt="50px"/>; const hash = resourceErrorPayload && 'hash' in resourceErrorPayload && typeof resourceErrorPayload.hash === 'string' ?
return <PageContent isHomePage={ isHomePage }>{ content }</PageContent>; resourceErrorPayload.hash :
undefined;
return <PageContent isHomePage={ isHomePage }><AppErrorBlockConsensus hash={ hash } mt="50px"/></PageContent>;
} }
return isInvalidTxHash ? <ErrorInvalidTxHash/> : <AppError statusCode={ statusCode }/>; return <PageContent isHomePage={ isHomePage }><AppError statusCode={ statusCode } mt="50px"/></PageContent>;
}, [ isHomePage, wrapChildren ]); }, [ isHomePage ]);
const renderedChildren = wrapChildren ? ( const renderedChildren = wrapChildren ? (
<PageContent isHomePage={ isHomePage }>{ children }</PageContent> <PageContent isHomePage={ isHomePage }>{ children }</PageContent>
......
...@@ -5,11 +5,8 @@ import React from 'react'; ...@@ -5,11 +5,8 @@ import React from 'react';
import * as tokenTransferMock from 'mocks/tokens/tokenTransfer'; import * as tokenTransferMock from 'mocks/tokens/tokenTransfer';
import TestApp from 'playwright/TestApp'; import TestApp from 'playwright/TestApp';
import { flattenTotal } from './helpers';
import TokenTransferList from './TokenTransferList'; import TokenTransferList from './TokenTransferList';
const flattenData = tokenTransferMock.mixTokens.items.reduce(flattenTotal, []);
test.use({ viewport: devices['iPhone 13 Pro'].viewport }); test.use({ viewport: devices['iPhone 13 Pro'].viewport });
test('without tx info', async({ mount }) => { test('without tx info', async({ mount }) => {
...@@ -17,7 +14,7 @@ test('without tx info', async({ mount }) => { ...@@ -17,7 +14,7 @@ test('without tx info', async({ mount }) => {
<TestApp> <TestApp>
<Box h={{ base: '134px', lg: 6 }}/> <Box h={{ base: '134px', lg: 6 }}/>
<TokenTransferList <TokenTransferList
data={ flattenData } data={ tokenTransferMock.mixTokens.items }
showTxInfo={ false } showTxInfo={ false }
/> />
</TestApp>, </TestApp>,
...@@ -31,7 +28,7 @@ test('with tx info', async({ mount }) => { ...@@ -31,7 +28,7 @@ test('with tx info', async({ mount }) => {
<TestApp> <TestApp>
<Box h={{ base: '134px', lg: 6 }}/> <Box h={{ base: '134px', lg: 6 }}/>
<TokenTransferList <TokenTransferList
data={ flattenData } data={ tokenTransferMock.mixTokens.items }
showTxInfo={ true } showTxInfo={ true }
/> />
</TestApp>, </TestApp>,
......
...@@ -5,17 +5,14 @@ import React from 'react'; ...@@ -5,17 +5,14 @@ import React from 'react';
import * as tokenTransferMock from 'mocks/tokens/tokenTransfer'; import * as tokenTransferMock from 'mocks/tokens/tokenTransfer';
import TestApp from 'playwright/TestApp'; import TestApp from 'playwright/TestApp';
import { flattenTotal } from './helpers';
import TokenTransferTable from './TokenTransferTable'; import TokenTransferTable from './TokenTransferTable';
const flattenData = tokenTransferMock.mixTokens.items.reduce(flattenTotal, []);
test('without tx info', async({ mount }) => { test('without tx info', async({ mount }) => {
const component = await mount( const component = await mount(
<TestApp> <TestApp>
<Box h={{ base: '134px', lg: 6 }}/> <Box h={{ base: '134px', lg: 6 }}/>
<TokenTransferTable <TokenTransferTable
data={ flattenData } data={ tokenTransferMock.mixTokens.items }
top={ 0 } top={ 0 }
showTxInfo={ false } showTxInfo={ false }
/> />
...@@ -30,7 +27,7 @@ test('with tx info', async({ mount }) => { ...@@ -30,7 +27,7 @@ test('with tx info', async({ mount }) => {
<TestApp> <TestApp>
<Box h={{ base: '134px', lg: 6 }}/> <Box h={{ base: '134px', lg: 6 }}/>
<TokenTransferTable <TokenTransferTable
data={ flattenData } data={ tokenTransferMock.mixTokens.items }
top={ 0 } top={ 0 }
showTxInfo={ true } showTxInfo={ true }
/> />
......
import type { TokenTransfer } from 'types/api/tokenTransfer'; 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']) => { export const getTokenTransferTypeText = (type: TokenTransfer['type']) => {
switch (type) { switch (type) {
case 'token_minting': case 'token_minting':
......
...@@ -274,6 +274,7 @@ const ChartWidget = ({ items, title, description, isLoading, className, isError, ...@@ -274,6 +274,7 @@ const ChartWidget = ({ items, title, description, isLoading, className, isError,
title={ title } title={ title }
description={ description } description={ description }
onClose={ clearFullscreenChart } onClose={ clearFullscreenChart }
units={ units }
/> />
) } ) }
</> </>
......
...@@ -13,6 +13,7 @@ type Props = { ...@@ -13,6 +13,7 @@ type Props = {
description?: string; description?: string;
items: Array<TimeChartItem>; items: Array<TimeChartItem>;
onClose: () => void; onClose: () => void;
units?: string;
} }
const FullscreenChartModal = ({ const FullscreenChartModal = ({
...@@ -20,6 +21,7 @@ const FullscreenChartModal = ({ ...@@ -20,6 +21,7 @@ const FullscreenChartModal = ({
title, title,
description, description,
items, items,
units,
onClose, onClose,
}: Props) => { }: Props) => {
const [ isZoomResetInitial, setIsZoomResetInitial ] = React.useState(true); const [ isZoomResetInitial, setIsZoomResetInitial ] = React.useState(true);
...@@ -94,6 +96,7 @@ const FullscreenChartModal = ({ ...@@ -94,6 +96,7 @@ const FullscreenChartModal = ({
margin={{ bottom: 60 }} margin={{ bottom: 60 }}
isEnlarged isEnlarged
items={ items } items={ items }
units={ units }
onZoom={ handleZoom } onZoom={ handleZoom }
isZoomResetInitial={ isZoomResetInitial } isZoomResetInitial={ isZoomResetInitial }
title={ title } title={ title }
......
...@@ -64,7 +64,12 @@ test('erc1155 +@mobile', async({ mount }) => { ...@@ -64,7 +64,12 @@ test('erc1155 +@mobile', async({ mount }) => {
// @ts-ignore: // @ts-ignore:
transfersQuery={{ transfersQuery={{
data: { data: {
items: [ tokenTransferMock.erc1155, tokenTransferMock.erc1155multiple ], items: [
tokenTransferMock.erc1155A,
tokenTransferMock.erc1155B,
tokenTransferMock.erc1155C,
tokenTransferMock.erc1155D,
],
next_page_params: null, next_page_params: null,
}, },
isPaginationVisible: true, isPaginationVisible: true,
......
...@@ -15,7 +15,6 @@ import DataListDisplay from 'ui/shared/DataListDisplay'; ...@@ -15,7 +15,6 @@ import DataListDisplay from 'ui/shared/DataListDisplay';
import Pagination from 'ui/shared/Pagination'; import Pagination from 'ui/shared/Pagination';
import type { Props as PaginationProps } from 'ui/shared/Pagination'; import type { Props as PaginationProps } from 'ui/shared/Pagination';
import SocketNewItemsNotice from 'ui/shared/SocketNewItemsNotice'; import SocketNewItemsNotice from 'ui/shared/SocketNewItemsNotice';
import { flattenTotal } from 'ui/shared/TokenTransfer/helpers';
import TokenTransferList from 'ui/token/TokenTransfer/TokenTransferList'; import TokenTransferList from 'ui/token/TokenTransfer/TokenTransferList';
import TokenTransferTable from 'ui/token/TokenTransfer/TokenTransferTable'; import TokenTransferTable from 'ui/token/TokenTransfer/TokenTransferTable';
...@@ -59,14 +58,12 @@ const TokenTransfer = ({ transfersQuery, tokenId }: Props) => { ...@@ -59,14 +58,12 @@ const TokenTransfer = ({ transfersQuery, tokenId }: Props) => {
handler: handleNewTransfersMessage, handler: handleNewTransfersMessage,
}); });
const items = data?.items?.reduce(flattenTotal, []); const content = data?.items ? (
const content = items ? (
<> <>
<Hide below="lg" ssr={ false }> <Hide below="lg" ssr={ false }>
<TokenTransferTable <TokenTransferTable
data={ items } data={ data?.items }
top={ isPaginationVisible ? 80 : 0 } top={ isPaginationVisible ? 80 : 0 }
showSocketInfo={ pagination.page === 1 } showSocketInfo={ pagination.page === 1 }
socketInfoAlert={ socketAlert } socketInfoAlert={ socketAlert }
...@@ -84,7 +81,7 @@ const TokenTransfer = ({ transfersQuery, tokenId }: Props) => { ...@@ -84,7 +81,7 @@ const TokenTransfer = ({ transfersQuery, tokenId }: Props) => {
borderBottomRadius={ 0 } borderBottomRadius={ 0 }
/> />
) } ) }
<TokenTransferList data={ items } tokenId={ tokenId }/> <TokenTransferList data={ data?.items } tokenId={ tokenId }/>
</Show> </Show>
</> </>
) : null; ) : null;
......
...@@ -304,7 +304,7 @@ const TxDetails = () => { ...@@ -304,7 +304,7 @@ const TxDetails = () => {
) } ) }
</DetailsInfoItem> </DetailsInfoItem>
) } ) }
{ data.tx_burnt_fee && ( { data.tx_burnt_fee && !appConfig.L2.isL2Network && (
<DetailsInfoItem <DetailsInfoItem
title="Burnt fees" title="Burnt fees"
hint={ `Amount of ${ appConfig.network.currency.symbol } burned for this transaction. Equals Block Base Fee per Gas * Gas Used` } 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'; ...@@ -2,9 +2,14 @@ import { Flex, Skeleton } from '@chakra-ui/react';
import { useRouter } from 'next/router'; import { useRouter } from 'next/router';
import React from 'react'; 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 useApiQuery from 'lib/api/useApiQuery';
import { SECOND } from 'lib/consts'; import { SECOND } from 'lib/consts';
import getQueryParamString from 'lib/router/getQueryParamString'; 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 DataFetchAlert from 'ui/shared/DataFetchAlert';
import RawDataSnippet from 'ui/shared/RawDataSnippet'; import RawDataSnippet from 'ui/shared/RawDataSnippet';
import TxPendingAlert from 'ui/tx/TxPendingAlert'; import TxPendingAlert from 'ui/tx/TxPendingAlert';
...@@ -12,6 +17,8 @@ import TxSocketAlert from 'ui/tx/TxSocketAlert'; ...@@ -12,6 +17,8 @@ import TxSocketAlert from 'ui/tx/TxSocketAlert';
import useFetchTxInfo from 'ui/tx/useFetchTxInfo'; import useFetchTxInfo from 'ui/tx/useFetchTxInfo';
const TxRawTrace = () => { const TxRawTrace = () => {
const [ isSocketOpen, setIsSocketOpen ] = React.useState(false);
const [ rawTraces, setRawTraces ] = React.useState<RawTracesResponse>();
const router = useRouter(); const router = useRouter();
const hash = getQueryParamString(router.query.hash); const hash = getQueryParamString(router.query.hash);
...@@ -19,10 +26,25 @@ const TxRawTrace = () => { ...@@ -19,10 +26,25 @@ const TxRawTrace = () => {
const { data, isLoading, isError } = useApiQuery('tx_raw_trace', { const { data, isLoading, isError } = useApiQuery('tx_raw_trace', {
pathParams: { hash }, pathParams: { hash },
queryOptions: { 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) { if (!txInfo.isLoading && !txInfo.isError && !txInfo.data.status) {
return txInfo.socketStatus ? <TxSocketAlert status={ txInfo.socketStatus }/> : <TxPendingAlert/>; return txInfo.socketStatus ? <TxSocketAlert status={ txInfo.socketStatus }/> : <TxPendingAlert/>;
} }
...@@ -42,11 +64,13 @@ const TxRawTrace = () => { ...@@ -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>; 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 }/>; return <RawDataSnippet data={ text }/>;
}; };
......
...@@ -13,7 +13,6 @@ import ActionBar from 'ui/shared/ActionBar'; ...@@ -13,7 +13,6 @@ import ActionBar from 'ui/shared/ActionBar';
import DataFetchAlert from 'ui/shared/DataFetchAlert'; import DataFetchAlert from 'ui/shared/DataFetchAlert';
import DataListDisplay from 'ui/shared/DataListDisplay'; import DataListDisplay from 'ui/shared/DataListDisplay';
import Pagination from 'ui/shared/Pagination'; import Pagination from 'ui/shared/Pagination';
import { flattenTotal } from 'ui/shared/TokenTransfer/helpers';
import TokenTransferFilter from 'ui/shared/TokenTransfer/TokenTransferFilter'; import TokenTransferFilter from 'ui/shared/TokenTransfer/TokenTransferFilter';
import TokenTransferList from 'ui/shared/TokenTransfer/TokenTransferList'; import TokenTransferList from 'ui/shared/TokenTransfer/TokenTransferList';
import TokenTransferTable from 'ui/shared/TokenTransfer/TokenTransferTable'; import TokenTransferTable from 'ui/shared/TokenTransfer/TokenTransferTable';
...@@ -55,15 +54,13 @@ const TxTokenTransfer = () => { ...@@ -55,15 +54,13 @@ const TxTokenTransfer = () => {
const numActiveFilters = typeFilter.length; const numActiveFilters = typeFilter.length;
const isActionBarHidden = !numActiveFilters && !tokenTransferQuery.data?.items.length; const isActionBarHidden = !numActiveFilters && !tokenTransferQuery.data?.items.length;
const items = tokenTransferQuery.data?.items?.reduce(flattenTotal, []); const content = tokenTransferQuery.data?.items ? (
const content = items ? (
<> <>
<Hide below="lg" ssr={ false }> <Hide below="lg" ssr={ false }>
<TokenTransferTable data={ items } top={ isActionBarHidden ? 0 : 80 }/> <TokenTransferTable data={ tokenTransferQuery.data?.items } top={ isActionBarHidden ? 0 : 80 }/>
</Hide> </Hide>
<Show below="lg" ssr={ false }> <Show below="lg" ssr={ false }>
<TokenTransferList data={ items }/> <TokenTransferList data={ tokenTransferQuery.data?.items }/>
</Show> </Show>
</> </>
) : null; ) : null;
......
...@@ -11,25 +11,25 @@ import CurrencyValue from 'ui/shared/CurrencyValue'; ...@@ -11,25 +11,25 @@ import CurrencyValue from 'ui/shared/CurrencyValue';
import TokenSnippet from 'ui/shared/TokenSnippet/TokenSnippet'; import TokenSnippet from 'ui/shared/TokenSnippet/TokenSnippet';
import NftTokenTransferSnippet from 'ui/tx/NftTokenTransferSnippet'; import NftTokenTransferSnippet from 'ui/tx/NftTokenTransferSnippet';
type Props = TTokenTransfer; interface Props {
data: TTokenTransfer;
}
const TxDetailsTokenTransfer = ({ token, total, to, from }: Props) => { const TxDetailsTokenTransfer = ({ data }: Props) => {
const isColumnLayout = token.type === 'ERC-1155' && Array.isArray(total);
const content = (() => { const content = (() => {
switch (token.type) { switch (data.token.type) {
case 'ERC-20': { case 'ERC-20': {
const payload = total as Erc20TotalPayload; const total = data.total as Erc20TotalPayload;
return ( return (
<Flex flexWrap="wrap" columnGap={ 3 } rowGap={ 2 }> <Flex flexWrap="wrap" columnGap={ 3 } rowGap={ 2 }>
<Text fontWeight={ 500 } as="span">For:{ space } <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> </Text>
<TokenSnippet <TokenSnippet
symbol={ trimTokenSymbol(token.symbol) } symbol={ trimTokenSymbol(data.token.symbol) }
hash={ token.address } hash={ data.token.address }
name={ token.name } name={ data.token.name }
w="auto" w="auto"
flexGrow="1" flexGrow="1"
columnGap={ 1 } columnGap={ 1 }
...@@ -40,47 +40,46 @@ const TxDetailsTokenTransfer = ({ token, total, to, from }: Props) => { ...@@ -40,47 +40,46 @@ const TxDetailsTokenTransfer = ({ token, total, to, from }: Props) => {
} }
case 'ERC-721': { case 'ERC-721': {
const payload = total as Erc721TotalPayload; const total = data.total as Erc721TotalPayload;
return ( return (
<NftTokenTransferSnippet <NftTokenTransferSnippet
name={ token.name } name={ data.token.name }
tokenId={ payload.token_id } tokenId={ total.token_id }
value="1" value="1"
hash={ token.address } hash={ data.token.address }
symbol={ trimTokenSymbol(token.symbol) } symbol={ trimTokenSymbol(data.token.symbol) }
/> />
); );
} }
case 'ERC-1155': { case 'ERC-1155': {
const payload = total as Erc1155TotalPayload | Array<Erc1155TotalPayload>; const total = data.total as Erc1155TotalPayload;
const items = Array.isArray(payload) ? payload : [ payload ]; return (
return items.map((item) => (
<NftTokenTransferSnippet <NftTokenTransferSnippet
name={ token.name } name={ data.token.name }
key={ item.token_id } key={ total.token_id }
tokenId={ item.token_id } tokenId={ total.token_id }
value={ item.value } value={ total.value }
hash={ token.address } hash={ data.token.address }
symbol={ trimTokenSymbol(token.symbol) } symbol={ trimTokenSymbol(data.token.symbol) }
/> />
)); );
} }
} }
})(); })();
return ( return (
<Flex <Flex
alignItems={ isColumnLayout ? 'flex-start' : 'center' } alignItems="center"
flexWrap="wrap" flexWrap="wrap"
columnGap={ 3 } columnGap={ 3 }
rowGap={ 3 } rowGap={ 3 }
flexDir={ isColumnLayout ? 'column' : 'row' } flexDir="row"
> >
<Flex alignItems="center"> <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"/> <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>
<Flex flexDir="column" rowGap={ 5 }> <Flex flexDir="column" rowGap={ 5 }>
{ content } { content }
......
...@@ -7,7 +7,6 @@ import type { TokenTransfer } from 'types/api/tokenTransfer'; ...@@ -7,7 +7,6 @@ import type { TokenTransfer } from 'types/api/tokenTransfer';
import tokenIcon from 'icons/token.svg'; import tokenIcon from 'icons/token.svg';
import DetailsInfoItem from 'ui/shared/DetailsInfoItem'; import DetailsInfoItem from 'ui/shared/DetailsInfoItem';
import LinkInternal from 'ui/shared/LinkInternal'; import LinkInternal from 'ui/shared/LinkInternal';
import { flattenTotal } from 'ui/shared/TokenTransfer/helpers';
import TxDetailsTokenTransfer from './TxDetailsTokenTransfer'; import TxDetailsTokenTransfer from './TxDetailsTokenTransfer';
...@@ -27,10 +26,9 @@ const VISIBLE_ITEMS_NUM = 3; ...@@ -27,10 +26,9 @@ const VISIBLE_ITEMS_NUM = 3;
const TxDetailsTokenTransfers = ({ data, txHash }: Props) => { const TxDetailsTokenTransfers = ({ data, txHash }: Props) => {
const viewAllUrl = route({ pathname: '/tx/[hash]', query: { hash: txHash, tab: 'token_transfers' } }); const viewAllUrl = route({ pathname: '/tx/[hash]', query: { hash: txHash, tab: 'token_transfers' } });
const formattedData = data.reduce(flattenTotal, []);
const transferGroups = TOKEN_TRANSFERS_TYPES.map((group) => ({ const transferGroups = TOKEN_TRANSFERS_TYPES.map((group) => ({
...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); const showViewAllLink = transferGroups.some(({ items }) => items.length > VISIBLE_ITEMS_NUM);
...@@ -54,7 +52,7 @@ const TxDetailsTokenTransfers = ({ data, txHash }: Props) => { ...@@ -54,7 +52,7 @@ const TxDetailsTokenTransfers = ({ data, txHash }: Props) => {
rowGap={ 5 } rowGap={ 5 }
w="100%" 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> </Flex>
</DetailsInfoItem> </DetailsInfoItem>
); );
......
...@@ -34,29 +34,33 @@ const VerifiedAddressesListItem = ({ item, application, onAdd, onEdit }: Props) ...@@ -34,29 +34,33 @@ const VerifiedAddressesListItem = ({ item, application, onAdd, onEdit }: Props)
<AddressSnippet address={{ hash: item.contractAddress, is_contract: true, implementation_name: null }}/> <AddressSnippet address={{ hash: item.contractAddress, is_contract: true, implementation_name: null }}/>
</ListItemMobileGrid.Value> </ListItemMobileGrid.Value>
<ListItemMobileGrid.Label>Token Info</ListItemMobileGrid.Label> { item.metadata.tokenName && (
<ListItemMobileGrid.Value py={ application ? '3px' : '5px' } display="flex" alignItems="center"> <>
{ application ? ( <ListItemMobileGrid.Label>Token Info</ListItemMobileGrid.Label>
<> <ListItemMobileGrid.Value py={ application ? '3px' : '5px' } display="flex" alignItems="center">
<VerifiedAddressesTokenSnippet application={ application }/> { application ? (
<Tooltip label="Edit"> <>
<IconButton <VerifiedAddressesTokenSnippet application={ application } name={ item.metadata.tokenName }/>
aria-label="edit" <Tooltip label="Edit">
variant="simple" <IconButton
boxSize={ 5 } aria-label="edit"
borderRadius="none" variant="simple"
flexShrink={ 0 } boxSize={ 5 }
onClick={ handleEditClick } borderRadius="none"
icon={ <Icon as={ editIcon }/> } flexShrink={ 0 }
/> onClick={ handleEditClick }
</Tooltip> icon={ <Icon as={ editIcon }/> }
</> />
) : ( </Tooltip>
<Link onClick={ handleAddClick }>Add details</Link> </>
) } ) : (
</ListItemMobileGrid.Value> <Link onClick={ handleAddClick }>Add details</Link>
) }
</ListItemMobileGrid.Value>
</>
) }
{ application && ( { item.metadata.tokenName && application && (
<> <>
<ListItemMobileGrid.Label>Status</ListItemMobileGrid.Label> <ListItemMobileGrid.Label>Status</ListItemMobileGrid.Label>
<ListItemMobileGrid.Value> <ListItemMobileGrid.Value>
...@@ -65,7 +69,7 @@ const VerifiedAddressesListItem = ({ item, application, onAdd, onEdit }: Props) ...@@ -65,7 +69,7 @@ const VerifiedAddressesListItem = ({ item, application, onAdd, onEdit }: Props)
</> </>
) } ) }
{ application && ( { item.metadata.tokenName && application && (
<> <>
<ListItemMobileGrid.Label>Date</ListItemMobileGrid.Label> <ListItemMobileGrid.Label>Date</ListItemMobileGrid.Label>
<ListItemMobileGrid.Value> <ListItemMobileGrid.Value>
......
...@@ -27,33 +27,43 @@ const VerifiedAddressesTableItem = ({ item, application, onAdd, onEdit }: Props) ...@@ -27,33 +27,43 @@ const VerifiedAddressesTableItem = ({ item, application, onAdd, onEdit }: Props)
onEdit(item.contractAddress); onEdit(item.contractAddress);
}, [ item, onEdit ]); }, [ 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 ( return (
<Tr> <Tr>
<Td> <Td>
<AddressSnippet address={{ hash: item.contractAddress, is_contract: true, implementation_name: null }}/> <AddressSnippet address={{ hash: item.contractAddress, is_contract: true, implementation_name: null }}/>
</Td> </Td>
<Td fontSize="sm" verticalAlign="middle"> <Td fontSize="sm" verticalAlign="middle">
{ application ? ( { tokenInfo }
<VerifiedAddressesTokenSnippet application={ application }/> </Td>
) : ( <Td>
<Link onClick={ handleAddClick }>Add details</Link> { 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>
<Td>{ application ? ( <Td fontSize="sm"><VerifiedAddressesStatus status={ item.metadata.tokenName ? application?.status : undefined }/></Td>
<Tooltip label="Edit"> <Td fontSize="sm" color="text_secondary">{ item.metadata.tokenName ? dayjs(application?.updatedAt).format('MMM DD, YYYY') : null }</Td>
<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>
</Tr> </Tr>
); );
}; };
......
...@@ -8,9 +8,10 @@ import TokenLogoPlaceholder from 'ui/shared/TokenLogoPlaceholder'; ...@@ -8,9 +8,10 @@ import TokenLogoPlaceholder from 'ui/shared/TokenLogoPlaceholder';
interface Props { interface Props {
application: TokenInfoApplication; application: TokenInfoApplication;
name: string;
} }
const VerifiedAddressesTokenSnippet = ({ application }: Props) => { const VerifiedAddressesTokenSnippet = ({ application, name }: Props) => {
return ( return (
<Flex alignItems="center" columnGap={ 2 } w="100%"> <Flex alignItems="center" columnGap={ 2 } w="100%">
<Image <Image
...@@ -23,7 +24,7 @@ const VerifiedAddressesTokenSnippet = ({ application }: Props) => { ...@@ -23,7 +24,7 @@ const VerifiedAddressesTokenSnippet = ({ application }: Props) => {
/> />
<AddressLink <AddressLink
hash={ application.tokenAddress } hash={ application.tokenAddress }
alias={ application.projectName } alias={ name }
type="token" type="token"
isDisabled={ application.status === 'IN_PROCESS' } isDisabled={ application.status === 'IN_PROCESS' }
fontWeight={ 500 } 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