Commit 869f08e5 authored by tom's avatar tom

Merge branch 'main' of github.com:blockscout/frontend into address-csv-export

parents 36bebecb 42f60f7a
...@@ -51,3 +51,4 @@ NEXT_PUBLIC_API_PORT=__PLACEHOLDER_FOR_NEXT_PUBLIC_API_PORT__ ...@@ -51,3 +51,4 @@ NEXT_PUBLIC_API_PORT=__PLACEHOLDER_FOR_NEXT_PUBLIC_API_PORT__
# external services config # external services config
NEXT_PUBLIC_SENTRY_DSN=__PLACEHOLDER_FOR_NEXT_PUBLIC_SENTRY_DSN__ NEXT_PUBLIC_SENTRY_DSN=__PLACEHOLDER_FOR_NEXT_PUBLIC_SENTRY_DSN__
NEXT_PUBLIC_AUTH0_CLIENT_ID=__PLACEHOLDER_FOR_NEXT_PUBLIC_AUTH0_CLIENT_ID__ NEXT_PUBLIC_AUTH0_CLIENT_ID=__PLACEHOLDER_FOR_NEXT_PUBLIC_AUTH0_CLIENT_ID__
NEXT_PUBLIC_WALLET_CONNECT_PROJECT_ID=__PLACEHOLDER_FOR_NEXT_PUBLIC_WALLET_CONNECT_PROJECT_ID__
...@@ -6,6 +6,7 @@ WORKDIR /app ...@@ -6,6 +6,7 @@ WORKDIR /app
# Install dependencies based on the preferred package manager # Install dependencies based on the preferred package manager
COPY package.json yarn.lock ./ COPY package.json yarn.lock ./
RUN apk add git
RUN yarn --frozen-lockfile RUN yarn --frozen-lockfile
# Rebuild the source code only when needed # Rebuild the source code only when needed
......
...@@ -125,6 +125,7 @@ The app instance could be customized by passing following variables to NodeJS en ...@@ -125,6 +125,7 @@ The app instance could be customized by passing following variables to NodeJS en
| NEXT_PUBLIC_SENTRY_DSN | `string` *(optional)* | Client key for your Sentry.io app | `<secret>` | | NEXT_PUBLIC_SENTRY_DSN | `string` *(optional)* | Client key for your Sentry.io app | `<secret>` |
| SENTRY_CSP_REPORT_URI | `string` *(optional)* | URL for sending CSP-reports to your Sentry.io app | `<secret>` | | SENTRY_CSP_REPORT_URI | `string` *(optional)* | URL for sending CSP-reports to your Sentry.io app | `<secret>` |
| NEXT_PUBLIC_AUTH0_CLIENT_ID | `string` *(optional)* | Client id for [Auth0](https://auth0.com/) provider | `<secret>` | | NEXT_PUBLIC_AUTH0_CLIENT_ID | `string` *(optional)* | Client id for [Auth0](https://auth0.com/) provider | `<secret>` |
| NEXT_PUBLIC_WALLET_CONNECT_PROJECT_ID | `string` | Project id for [WalletConnect](https://docs.walletconnect.com/2.0/web3modal/react/installation#obtain-project-id) integration | `<secret>` |
### Marketplace app configuration properties ### Marketplace app configuration properties
......
...@@ -115,6 +115,9 @@ const config = Object.freeze({ ...@@ -115,6 +115,9 @@ const config = Object.freeze({
showGasTracker: getEnvValue(process.env.NEXT_PUBLIC_HOMEPAGE_SHOW_GAS_TRACKER) === 'false' ? false : true, showGasTracker: getEnvValue(process.env.NEXT_PUBLIC_HOMEPAGE_SHOW_GAS_TRACKER) === 'false' ? false : true,
showAvgBlockTime: getEnvValue(process.env.NEXT_PUBLIC_HOMEPAGE_SHOW_AVG_BLOCK_TIME) === 'false' ? false : true, showAvgBlockTime: getEnvValue(process.env.NEXT_PUBLIC_HOMEPAGE_SHOW_AVG_BLOCK_TIME) === 'false' ? false : true,
}, },
walletConnect: {
projectId: getEnvValue(process.env.NEXT_PUBLIC_WALLET_CONNECT_PROJECT_ID),
},
}); });
export default config; export default config;
...@@ -3,11 +3,11 @@ NEXT_PUBLIC_FEATURED_NETWORKS=[{'title':'Ethereum','url':'https://blockscout.com ...@@ -3,11 +3,11 @@ NEXT_PUBLIC_FEATURED_NETWORKS=[{'title':'Ethereum','url':'https://blockscout.com
NEXT_PUBLIC_NETWORK_EXPLORERS=[{'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'}}] NEXT_PUBLIC_NETWORK_EXPLORERS=[{'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=Ethereum NEXT_PUBLIC_NETWORK_NAME=Goerli
NEXT_PUBLIC_NETWORK_SHORT_NAME=Goerli NEXT_PUBLIC_NETWORK_SHORT_NAME=Goerli
NEXT_PUBLIC_NETWORK_ASSETS_PATHNAME=ethereum NEXT_PUBLIC_NETWORK_ASSETS_PATHNAME=ethereum
NEXT_PUBLIC_NETWORK_TYPE=goerli NEXT_PUBLIC_NETWORK_TYPE=goerli
NEXT_PUBLIC_NETWORK_ID=420 NEXT_PUBLIC_NETWORK_ID=5
NEXT_PUBLIC_NETWORK_CURRENCY_NAME=Ether NEXT_PUBLIC_NETWORK_CURRENCY_NAME=Ether
NEXT_PUBLIC_NETWORK_CURRENCY_SYMBOL=ETH NEXT_PUBLIC_NETWORK_CURRENCY_SYMBOL=ETH
NEXT_PUBLIC_NETWORK_CURRENCY_DECIMALS=18 NEXT_PUBLIC_NETWORK_CURRENCY_DECIMALS=18
......
...@@ -61,6 +61,10 @@ const oldUrls = [ ...@@ -61,6 +61,10 @@ const oldUrls = [
oldPath: '/address/:id/validations', oldPath: '/address/:id/validations',
newPath: `${ PATHS.address_index }?tab=blocks_validated`, newPath: `${ PATHS.address_index }?tab=blocks_validated`,
}, },
{
oldPath: '/address/:id/tokens/:hash/token-transfers',
newPath: `${ PATHS.address_index }?tab=token_transfers&token=:hash`,
},
]; ];
async function redirects() { async function redirects() {
......
...@@ -473,6 +473,8 @@ frontend: ...@@ -473,6 +473,8 @@ frontend:
# - "/address" # - "/address"
- "/stats" - "/stats"
- "/search-results" - "/search-results"
- "/tokens"
- "/accounts"
resources: resources:
limits: limits:
memory: memory:
......
...@@ -322,6 +322,8 @@ frontend: ...@@ -322,6 +322,8 @@ frontend:
- "/stats" - "/stats"
- "/search-results" - "/search-results"
- "/token" - "/token"
- "/tokens"
- "/accounts"
resources: resources:
limits: limits:
......
...@@ -14,6 +14,7 @@ import type { ...@@ -14,6 +14,7 @@ import type {
AddressTokensFilter, AddressTokensFilter,
AddressTokensResponse, AddressTokensResponse,
} from 'types/api/address'; } from 'types/api/address';
import type { AddressesResponse } from 'types/api/addresses';
import type { BlocksResponse, BlockTransactionsResponse, Block, BlockFilters } from 'types/api/block'; import type { BlocksResponse, BlockTransactionsResponse, Block, BlockFilters } from 'types/api/block';
import type { ChartMarketResponse, ChartTransactionResponse } from 'types/api/charts'; import type { ChartMarketResponse, ChartTransactionResponse } from 'types/api/charts';
import type { SmartContract, SmartContractReadMethod, SmartContractWriteMethod } from 'types/api/contract'; import type { SmartContract, SmartContractReadMethod, SmartContractWriteMethod } from 'types/api/contract';
...@@ -24,6 +25,7 @@ import type { RawTracesResponse } from 'types/api/rawTrace'; ...@@ -24,6 +25,7 @@ import type { RawTracesResponse } from 'types/api/rawTrace';
import type { SearchResult, SearchResultFilters } from 'types/api/search'; import type { SearchResult, SearchResultFilters } from 'types/api/search';
import type { Counters, StatsCharts, StatsChart, HomeStats } from 'types/api/stats'; import type { Counters, StatsCharts, StatsChart, HomeStats } from 'types/api/stats';
import type { TokenCounters, TokenInfo, TokenHolders } from 'types/api/tokenInfo'; import type { TokenCounters, TokenInfo, TokenHolders } from 'types/api/tokenInfo';
import type { TokensResponse, TokensFilters } from 'types/api/tokens';
import type { TokenTransferResponse, TokenTransferFilters } from 'types/api/tokenTransfer'; import type { TokenTransferResponse, TokenTransferFilters } from 'types/api/tokenTransfer';
import type { TransactionsResponseValidated, TransactionsResponsePending, Transaction } from 'types/api/transaction'; import type { TransactionsResponseValidated, TransactionsResponsePending, Transaction } from 'types/api/transaction';
import type { TTxsFilters } from 'types/api/txsFilters'; import type { TTxsFilters } from 'types/api/txsFilters';
...@@ -127,6 +129,13 @@ export const RESOURCES = { ...@@ -127,6 +129,13 @@ export const RESOURCES = {
path: '/api/v2/transactions/:id/raw-trace', path: '/api/v2/transactions/:id/raw-trace',
}, },
// ADDRESSES
addresses: {
path: '/api/v2/addresses/',
paginationFields: [ 'fetched_coin_balance' as const, 'hash' as const, 'items_count' as const ],
filterFields: [ ],
},
// ADDRESS // ADDRESS
address: { address: {
path: '/api/v2/addresses/:id', path: '/api/v2/addresses/:id',
...@@ -150,7 +159,7 @@ export const RESOURCES = { ...@@ -150,7 +159,7 @@ export const RESOURCES = {
address_token_transfers: { address_token_transfers: {
path: '/api/v2/addresses/:id/token-transfers', path: '/api/v2/addresses/:id/token-transfers',
paginationFields: [ 'block_number' as const, 'items_count' as const, 'index' as const, 'transaction_index' as const ], paginationFields: [ 'block_number' as const, 'items_count' as const, 'index' as const, 'transaction_index' as const ],
filterFields: [ 'filter' as const, 'type' as const ], filterFields: [ 'filter' as const, 'type' as const, 'token' as const ],
}, },
address_blocks_validated: { address_blocks_validated: {
path: '/api/v2/addresses/:id/blocks-validated', path: '/api/v2/addresses/:id/blocks-validated',
...@@ -208,6 +217,16 @@ export const RESOURCES = { ...@@ -208,6 +217,16 @@ export const RESOURCES = {
paginationFields: [ 'items_count' as const, 'value' as const ], paginationFields: [ 'items_count' as const, 'value' as const ],
filterFields: [], filterFields: [],
}, },
token_transfers: {
path: '/api/v2/tokens/:hash/transfers',
paginationFields: [ 'block_number' as const, 'items_count' as const, 'index' as const ],
filterFields: [],
},
tokens: {
path: '/api/v2/tokens',
paginationFields: [ 'holder_count' as const, 'items_count' as const, 'name' as const ],
filterFields: [ 'filter' as const, 'type' as const ],
},
// HOMEPAGE // HOMEPAGE
homepage_stats: { homepage_stats: {
...@@ -275,10 +294,11 @@ export type ResourceErrorAccount<T> = ResourceError<{ errors: T }> ...@@ -275,10 +294,11 @@ export type ResourceErrorAccount<T> = ResourceError<{ errors: T }>
export type PaginatedResources = 'blocks' | 'block_txs' | export type PaginatedResources = 'blocks' | 'block_txs' |
'txs_validated' | 'txs_pending' | 'txs_validated' | 'txs_pending' |
'tx_internal_txs' | 'tx_logs' | 'tx_token_transfers' | 'tx_internal_txs' | 'tx_logs' | 'tx_token_transfers' |
'addresses' |
'address_txs' | 'address_internal_txs' | 'address_token_transfers' | 'address_blocks_validated' | 'address_coin_balance' | 'address_txs' | 'address_internal_txs' | 'address_token_transfers' | 'address_blocks_validated' | 'address_coin_balance' |
'search' | 'search' |
'address_logs' | 'address_tokens' | 'address_logs' | 'address_tokens' |
'token_holders'; 'token_transfers' | 'token_holders' | 'tokens';
export type PaginatedResponse<Q extends PaginatedResources> = ResourcePayload<Q>; export type PaginatedResponse<Q extends PaginatedResources> = ResourcePayload<Q>;
...@@ -310,6 +330,7 @@ Q extends 'tx_internal_txs' ? InternalTransactionsResponse : ...@@ -310,6 +330,7 @@ Q extends 'tx_internal_txs' ? InternalTransactionsResponse :
Q extends 'tx_logs' ? LogsResponseTx : Q extends 'tx_logs' ? LogsResponseTx :
Q extends 'tx_token_transfers' ? TokenTransferResponse : Q extends 'tx_token_transfers' ? TokenTransferResponse :
Q extends 'tx_raw_trace' ? RawTracesResponse : Q extends 'tx_raw_trace' ? RawTracesResponse :
Q extends 'addresses' ? AddressesResponse :
Q extends 'address' ? Address : Q extends 'address' ? Address :
Q extends 'address_counters' ? AddressCounters : Q extends 'address_counters' ? AddressCounters :
Q extends 'address_token_balances' ? Array<AddressTokenBalance> : Q extends 'address_token_balances' ? Array<AddressTokenBalance> :
...@@ -323,7 +344,9 @@ Q extends 'address_logs' ? LogsResponseAddress : ...@@ -323,7 +344,9 @@ Q extends 'address_logs' ? LogsResponseAddress :
Q extends 'address_tokens' ? AddressTokensResponse : Q extends 'address_tokens' ? AddressTokensResponse :
Q extends 'token' ? TokenInfo : Q extends 'token' ? TokenInfo :
Q extends 'token_counters' ? TokenCounters : Q extends 'token_counters' ? TokenCounters :
Q extends 'token_transfers' ? TokenTransferResponse :
Q extends 'token_holders' ? TokenHolders : Q extends 'token_holders' ? TokenHolders :
Q extends 'tokens' ? TokensResponse :
Q extends 'search' ? SearchResult : Q extends 'search' ? SearchResult :
Q extends 'contract' ? SmartContract : Q extends 'contract' ? SmartContract :
Q extends 'contract_methods_read' ? Array<SmartContractReadMethod> : Q extends 'contract_methods_read' ? Array<SmartContractReadMethod> :
...@@ -338,9 +361,11 @@ export type PaginationFilters<Q extends PaginatedResources> = ...@@ -338,9 +361,11 @@ export type PaginationFilters<Q extends PaginatedResources> =
Q extends 'blocks' ? BlockFilters : Q extends 'blocks' ? BlockFilters :
Q extends 'txs_validated' | 'txs_pending' ? TTxsFilters : Q extends 'txs_validated' | 'txs_pending' ? TTxsFilters :
Q extends 'tx_token_transfers' ? TokenTransferFilters : Q extends 'tx_token_transfers' ? TokenTransferFilters :
Q extends 'token_transfers' ? TokenTransferFilters :
Q extends 'address_txs' | 'address_internal_txs' ? AddressTxsFilters : Q extends 'address_txs' | 'address_internal_txs' ? AddressTxsFilters :
Q extends 'address_token_transfers' ? AddressTokenTransferFilters : Q extends 'address_token_transfers' ? AddressTokenTransferFilters :
Q extends 'address_tokens' ? AddressTokensFilter : Q extends 'address_tokens' ? AddressTokensFilter :
Q extends 'search' ? SearchResultFilters : Q extends 'search' ? SearchResultFilters :
Q extends 'tokens' ? TokensFilters :
never; never;
/* eslint-enable @typescript-eslint/indent */ /* eslint-enable @typescript-eslint/indent */
...@@ -7,6 +7,7 @@ export enum NAMES { ...@@ -7,6 +7,7 @@ export enum NAMES {
API_TOKEN='_explorer_key', API_TOKEN='_explorer_key',
TXS_SORT='txs_sort', TXS_SORT='txs_sort',
COLOR_MODE='chakra-ui-color-mode', COLOR_MODE='chakra-ui-color-mode',
INDEXING_ALERT='indexing_alert',
} }
export function get(name?: NAMES | undefined | null, serverCookie?: string) { export function get(name?: NAMES | undefined | null, serverCookie?: string) {
......
...@@ -68,8 +68,16 @@ function makePolicyMap() { ...@@ -68,8 +68,16 @@ function makePolicyMap() {
appConfig.api.socket, appConfig.api.socket,
appConfig.statsApi.endpoint, appConfig.statsApi.endpoint,
// chain RPC server
appConfig.network.rpcUrl,
// ad // ad
'request-global.czilladx.com', 'request-global.czilladx.com',
// walletconnect
'*.walletconnect.com',
'wss://*.bridge.walletconnect.org',
'wss://www.walletlink.org',
], ],
'script-src': [ 'script-src': [
...@@ -130,6 +138,9 @@ function makePolicyMap() { ...@@ -130,6 +138,9 @@ function makePolicyMap() {
// ad // ad
'servedbyadbutler.com', 'servedbyadbutler.com',
'cdn.coinzilla.io', 'cdn.coinzilla.io',
// walletconnect
'*.walletconnect.com',
], ],
'font-src': [ 'font-src': [
......
...@@ -12,6 +12,7 @@ import publicTagIcon from 'icons/publictags.svg'; ...@@ -12,6 +12,7 @@ import publicTagIcon from 'icons/publictags.svg';
import statsIcon from 'icons/stats.svg'; import statsIcon from 'icons/stats.svg';
import tokensIcon from 'icons/token.svg'; import tokensIcon from 'icons/token.svg';
import transactionsIcon from 'icons/transactions.svg'; import transactionsIcon from 'icons/transactions.svg';
import walletIcon from 'icons/wallet.svg';
import watchlistIcon from 'icons/watchlist.svg'; import watchlistIcon from 'icons/watchlist.svg';
import link from 'lib/link/link'; import link from 'lib/link/link';
import useCurrentRoute from 'lib/link/useCurrentRoute'; import useCurrentRoute from 'lib/link/useCurrentRoute';
...@@ -27,6 +28,7 @@ export default function useNavItems() { ...@@ -27,6 +28,7 @@ export default function useNavItems() {
{ text: 'Blocks', url: link('blocks'), icon: blocksIcon, isActive: currentRoute.startsWith('block'), isNewUi: false }, { text: 'Blocks', url: link('blocks'), icon: blocksIcon, isActive: currentRoute.startsWith('block'), isNewUi: false },
{ text: 'Transactions', url: link('txs'), icon: transactionsIcon, isActive: currentRoute.startsWith('tx'), isNewUi: false }, { text: 'Transactions', url: link('txs'), icon: transactionsIcon, isActive: currentRoute.startsWith('tx'), isNewUi: false },
{ text: 'Tokens', url: link('tokens'), icon: tokensIcon, isActive: currentRoute === 'tokens', isNewUi: false }, { text: 'Tokens', url: link('tokens'), icon: tokensIcon, isActive: currentRoute === 'tokens', isNewUi: false },
{ text: 'Accounts', url: link('accounts'), icon: walletIcon, isActive: currentRoute === 'accounts', isNewUi: false },
isMarketplaceFilled ? isMarketplaceFilled ?
{ text: 'Apps', url: link('apps'), icon: appsIcon, isActive: currentRoute === 'apps', isNewUi: true } : null, { text: 'Apps', url: link('apps'), icon: appsIcon, isActive: currentRoute === 'apps', isNewUi: true } : null,
{ text: 'Charts & stats', url: link('stats'), icon: statsIcon, isActive: currentRoute === 'stats', isNewUi: false }, { text: 'Charts & stats', url: link('stats'), icon: statsIcon, isActive: currentRoute === 'stats', isNewUi: false },
......
...@@ -15,6 +15,7 @@ ...@@ -15,6 +15,7 @@
"token_instance_item": "/token/:hash/instance/:id", "token_instance_item": "/token/:hash/instance/:id",
"address_index": "/address/:id", "address_index": "/address/:id",
"address_contract_verification": "/address/:id/contract_verifications/new", "address_contract_verification": "/address/:id/contract_verifications/new",
"accounts": "/accounts",
"apps": "/apps", "apps": "/apps",
"app_index": "/apps/:id", "app_index": "/apps/:id",
"search_results": "/search-results", "search_results": "/search-results",
......
...@@ -77,6 +77,12 @@ export const ROUTES = { ...@@ -77,6 +77,12 @@ export const ROUTES = {
crossNetworkNavigation: true, crossNetworkNavigation: true,
}, },
// ACCOUNTS
accounts: {
pattern: PATHS.accounts,
crossNetworkNavigation: true,
},
// APPS // APPS
apps: { apps: {
pattern: PATHS.apps, pattern: PATHS.apps,
......
...@@ -18,6 +18,7 @@ SocketMessage.AddressCoinBalance | ...@@ -18,6 +18,7 @@ SocketMessage.AddressCoinBalance |
SocketMessage.AddressTxs | SocketMessage.AddressTxs |
SocketMessage.AddressTxsPending | SocketMessage.AddressTxsPending |
SocketMessage.AddressTokenTransfer | SocketMessage.AddressTokenTransfer |
SocketMessage.TokenTransfers |
SocketMessage.Unknown; SocketMessage.Unknown;
interface SocketMessageParamsGeneric<Event extends string | undefined, Payload extends object | unknown> { interface SocketMessageParamsGeneric<Event extends string | undefined, Payload extends object | unknown> {
...@@ -42,5 +43,6 @@ export namespace SocketMessage { ...@@ -42,5 +43,6 @@ 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 TokenTransfers = SocketMessageParamsGeneric<'token_transfer', {token_transfer: number }>
export type Unknown = SocketMessageParamsGeneric<undefined, unknown>; export type Unknown = SocketMessageParamsGeneric<undefined, unknown>;
} }
import type { TokenType } from 'types/api/tokenInfo';
const TOKEN_TYPE: Array<{ title: string; id: TokenType }> = [
{ title: 'ERC-20', id: 'ERC-20' },
{ title: 'ERC-721', id: 'ERC-721' },
{ title: 'ERC-1155', id: 'ERC-1155' },
];
export default TOKEN_TYPE;
import type { Address } from 'types/api/address'; import type { Address } from 'types/api/address';
import type { AddressParam } from 'types/api/addressParams'; import type { AddressParam } from 'types/api/addressParams';
import { publicTag, privateTag, watchlistName } from 'mocks/address/tag';
import { tokenInfo } from 'mocks/tokens/tokenInfo'; import { tokenInfo } from 'mocks/tokens/tokenInfo';
export const hash = '0xd789a607CEac2f0E14867de4EB15b15C9FFB5859';
export const withName: AddressParam = { export const withName: AddressParam = {
hash: '0xd789a607CEac2f0E14867de4EB15b15C9FFB5859', hash: hash,
implementation_name: null, implementation_name: null,
is_contract: true, is_contract: true,
is_verified: null, is_verified: null,
...@@ -15,7 +18,7 @@ export const withName: AddressParam = { ...@@ -15,7 +18,7 @@ export const withName: AddressParam = {
}; };
export const withoutName: AddressParam = { export const withoutName: AddressParam = {
hash: '0xd789a607CEac2f0E14867de4EB15b15C9FFB5859', hash: hash,
implementation_name: null, implementation_name: null,
is_contract: true, is_contract: true,
is_verified: null, is_verified: null,
...@@ -25,8 +28,8 @@ export const withoutName: AddressParam = { ...@@ -25,8 +28,8 @@ export const withoutName: AddressParam = {
public_tags: [], public_tags: [],
}; };
export const withToken: Address = { export const token: Address = {
hash: '0xd789a607CEac2f0E14867de4EB15b15C9FFB5859', hash: hash,
implementation_name: null, implementation_name: null,
is_contract: true, is_contract: true,
is_verified: false, is_verified: false,
...@@ -37,8 +40,8 @@ export const withToken: Address = { ...@@ -37,8 +40,8 @@ export const withToken: Address = {
token: tokenInfo, token: tokenInfo,
block_number_balance_updated_at: 8201413, block_number_balance_updated_at: 8201413,
coin_balance: '1', coin_balance: '1',
creation_tx_hash: null, creation_tx_hash: '0xc38cf7377bf72d6436f63c37b01b24d032101f20ec1849286dc703c712f10c98',
creator_address_hash: null, creator_address_hash: '0x34A9c688512ebdB575e82C50c9803F6ba2916E72',
exchange_rate: null, exchange_rate: null,
implementation_address: null, implementation_address: null,
has_custom_methods_read: false, has_custom_methods_read: false,
...@@ -49,7 +52,65 @@ export const withToken: Address = { ...@@ -49,7 +52,65 @@ export const withToken: Address = {
has_methods_read_proxy: false, has_methods_read_proxy: false,
has_methods_write: false, has_methods_write: false,
has_methods_write_proxy: false, has_methods_write_proxy: false,
has_token_transfers: false, has_token_transfers: true,
has_tokens: true, has_tokens: true,
has_validated_blocks: false, has_validated_blocks: false,
}; };
export const contract: Address = {
block_number_balance_updated_at: 30811263,
coin_balance: '27826501896887194214322205',
creation_tx_hash: '0xf2aff6501b632604c39978b47d309813d8a1bcca721864bbe86abf59704f195e',
creator_address_hash: '0x803ad3F50b9e1fF68615e8B053A186e1be288943',
exchange_rate: '0.04311',
has_custom_methods_read: false,
has_custom_methods_write: false,
has_decompiled_code: false,
has_logs: true,
has_methods_read: true,
has_methods_read_proxy: true,
has_methods_write: true,
has_methods_write_proxy: true,
has_token_transfers: false,
has_tokens: false,
has_validated_blocks: false,
hash: hash,
implementation_address: '0x2F4F4A52295940C576417d29F22EEb92B440eC89',
implementation_name: 'HomeBridge',
is_contract: true,
is_verified: true,
name: 'EternalStorageProxy',
private_tags: [ publicTag ],
public_tags: [ privateTag ],
token: null,
watchlist_names: [ watchlistName ],
};
export const validator: Address = {
block_number_balance_updated_at: 30811932,
coin_balance: '22910462800601256910890',
creation_tx_hash: null,
creator_address_hash: null,
exchange_rate: '0.00432018',
has_custom_methods_read: false,
has_custom_methods_write: false,
has_decompiled_code: false,
has_logs: false,
has_methods_read: false,
has_methods_read_proxy: false,
has_methods_write: false,
has_methods_write_proxy: false,
has_token_transfers: false,
has_tokens: false,
has_validated_blocks: true,
hash: hash,
implementation_address: null,
implementation_name: null,
is_contract: false,
is_verified: false,
name: 'Kiryl Ihnatsyeu',
private_tags: [],
public_tags: [],
token: null,
watchlist_names: [],
};
import type { AddressCounters } from 'types/api/address';
export const forContract: AddressCounters = {
gas_usage_count: '319340525',
token_transfers_count: '0',
transactions_count: '5462',
validations_count: '0',
};
export const forToken: AddressCounters = {
gas_usage_count: '247479698',
token_transfers_count: '1',
transactions_count: '8474',
validations_count: '0',
};
export const forValidator: AddressCounters = {
gas_usage_count: '91675762951',
token_transfers_count: '0',
transactions_count: '820802',
validations_count: '1726416',
};
import type { AddressTokenBalance } from 'types/api/address'; import type { AddressTokenBalance } from 'types/api/address';
import * as tokens from 'mocks/tokens/tokenInfo';
export const erc20a: AddressTokenBalance = { export const erc20a: AddressTokenBalance = {
token: { token: tokens.tokenInfoERC20a,
address: '0xb2a90505dc6680a7a695f7975d0d32EeF610f456',
decimals: '18',
exchange_rate: null,
holders: '23',
name: 'hyfi.token',
symbol: 'HyFi',
total_supply: '369000000000000000000000000',
type: 'ERC-20',
},
token_id: null, token_id: null,
value: '1169320000000000000000000', value: '1169320000000000000000000',
}; };
export const erc20b: AddressTokenBalance = { export const erc20b: AddressTokenBalance = {
token: { token: tokens.tokenInfoERC20b,
address: '0xc1116c98ba622a6218433fF90a2E40DEa482d7A7',
decimals: '6',
exchange_rate: '0.982',
holders: '17',
name: 'USD Coin',
symbol: 'USDC',
total_supply: '900000000000000000000000000',
type: 'ERC-20',
},
token_id: null, token_id: null,
value: '872500000000', value: '872500000000',
}; };
export const erc20c: AddressTokenBalance = { export const erc20c: AddressTokenBalance = {
token: { token: tokens.tokenInfoERC20c,
address: '0xc1116c98ba622a6218433fF90a2E40DEa482d7A7',
decimals: '18',
exchange_rate: '1328.89',
holders: '17',
name: 'Ethereum',
symbol: 'ETH',
total_supply: '1000000000000000000000000',
type: 'ERC-20',
},
token_id: null, token_id: null,
value: '9852000000000000000000', value: '9852000000000000000000',
}; };
export const erc20d: AddressTokenBalance = { export const erc20d: AddressTokenBalance = {
token: { token: tokens.tokenInfoERC20d,
address: '0xCc7bb2D219A0FC08033E130629C2B854b7bA9195',
decimals: '18',
exchange_rate: null,
holders: '102625',
name: 'Zeta',
symbol: 'ZETA',
total_supply: '2100000000000000000000000000',
type: 'ERC-20',
},
token_id: null, token_id: null,
value: '39000000000000000000', value: '39000000000000000000',
}; };
export const erc721a: AddressTokenBalance = { export const erc721a: AddressTokenBalance = {
token: { token: tokens.tokenInfoERC721a,
address: '0xDe7cAc71E072FCBd4453E5FB3558C2684d1F88A0',
decimals: null,
exchange_rate: null,
holders: '7',
name: 'HyFi Athena',
symbol: 'HYFI_ATHENA',
total_supply: '105',
type: 'ERC-721',
},
token_id: null, token_id: null,
value: '51', value: '51',
}; };
export const erc721b: AddressTokenBalance = { export const erc721b: AddressTokenBalance = {
token: { token: tokens.tokenInfoERC721b,
address: '0xA8d5C7beEA8C9bB57f5fBa35fB638BF45550b11F',
decimals: null,
exchange_rate: null,
holders: '2',
name: 'World Of Women Galaxy',
symbol: 'WOWG',
total_supply: null,
type: 'ERC-721',
},
token_id: null, token_id: null,
value: '1', value: '1',
}; };
export const erc721c: AddressTokenBalance = { export const erc721c: AddressTokenBalance = {
token: { token: tokens.tokenInfoERC721c,
address: '0x47646F1d7dc4Dd2Db5a41D092e2Cf966e27A4992',
decimals: null,
exchange_rate: null,
holders: '12',
name: 'Puma',
symbol: 'PUMA',
total_supply: null,
type: 'ERC-721',
},
token_id: null, token_id: null,
value: '5', value: '5',
}; };
export const erc1155a: AddressTokenBalance = { export const erc1155a: AddressTokenBalance = {
token: { token: tokens.tokenInfoERC1155a,
address: '0x4b333DEd10c7ca855EA2C8D4D90A0a8b73788c8e',
decimals: null,
exchange_rate: null,
holders: '22',
name: 'HyFi Membership',
symbol: 'HYFI_MEMBERSHIP',
total_supply: '482',
type: 'ERC-1155',
},
token_id: '42', token_id: '42',
value: '24', value: '24',
}; };
export const erc1155b: AddressTokenBalance = { export const erc1155b: AddressTokenBalance = {
token: { token: tokens.tokenInfoERC1155b,
address: '0xf4b71b179132ad457f6bcae2a55efa9e4b26eefc',
decimals: null,
exchange_rate: null,
holders: '100',
name: 'WinkyVerse Collections',
symbol: 'WVC',
total_supply: '4943',
type: 'ERC-1155',
},
token_id: '100010000000001', token_id: '100010000000001',
value: '11', value: '11',
}; };
export const erc1155withoutName: AddressTokenBalance = { export const erc1155withoutName: AddressTokenBalance = {
token: { token: tokens.tokenInfoERC1155WithoutName,
address: '0x4b333DEd10c7ca855EA2C8D4D90A0a8b73788c8e',
decimals: null,
exchange_rate: null,
holders: '22',
name: null,
symbol: null,
total_supply: '482',
type: 'ERC-1155',
},
token_id: '64532245', token_id: '64532245',
value: '42', value: '42',
}; };
......
/* eslint-disable max-len */
import type { SmartContract } from 'types/api/contract';
export const verified: Partial<SmartContract> = {
abi: [ { anonymous: false, inputs: [ { indexed: true, internalType: 'address', name: 'src', type: 'address' }, { indexed: true, internalType: 'address', name: 'guy', type: 'address' }, { indexed: false, internalType: 'uint256', name: 'wad', type: 'uint256' } ], name: 'Approval', type: 'event' } ],
can_be_visualized_via_sol2uml: true,
compiler_version: 'v0.5.16+commit.9c3226ce',
constructor_args: 'constructor_args',
creation_bytecode: 'creation_bytecode',
deployed_bytecode: 'deployed_bytecode',
compiler_settings: 'compiler_settings',
evm_version: 'default',
is_verified: true,
name: 'WPOA',
optimization_enabled: true,
optimization_runs: 1500,
source_code: 'source_code',
verified_at: '2021-08-03T10:40:41.679421Z',
decoded_constructor_args: [
[ '0xc59615da2da226613b1c78f0c6676cac497910bc', { internalType: 'address', name: '_token', type: 'address' } ],
[ '1800', { internalType: 'uint256', name: '_duration', type: 'uint256' } ],
[ '900000000', { internalType: 'uint256', name: '_totalSupply', type: 'uint256' } ],
],
external_libraries: [
{ address_hash: '0xa62744BeE8646e237441CDbfdedD3458861748A8', name: 'Sol' },
{ address_hash: '0xa62744BeE8646e237441CDbfdedD3458861748A8', name: 'math' },
],
};
export const withMultiplePaths: Partial<SmartContract> = {
...verified,
file_path: './simple_storage.sol',
additional_sources: [
{
file_path: 'contracts/1_Storage.sol',
source_code: '// SPDX-License-Identifier: GPL-3.0 \n pragma solidity >=0.7.0 <0.9.0; \n contract Storage {\n //2112313123; \nuint256 number; \n function store(uint256 num) public {\nnumber = num;\n}\n function retrieve() public view returns (uint256)\n {\nreturn number;\n}\n}',
},
],
};
export const verifiedViaSourcify: Partial<SmartContract> = {
...verified,
is_verified_via_sourcify: true,
is_fully_verified: false,
is_partially_verified: true,
sourcify_repo_url: 'https://repo.sourcify.dev/contracts//full_match/99/0x51891596E158b2857e5356DC847e2D15dFbCF2d0/',
};
export const withTwinAddress: Partial<SmartContract> = {
...verified,
is_verified: false,
verified_twin_address_hash: '0xa62744bee8646e237441cdbfdedd3458861748a8',
};
export const withProxyAddress: Partial<SmartContract> = {
...verified,
is_verified: false,
verified_twin_address_hash: '0xa62744bee8646e237441cdbfdedd3458861748a8',
minimal_proxy_address_hash: '0xa62744bee8646e237441cdbfdedd3458861748a8',
};
export const selfDestructed: Partial<SmartContract> = {
...verified,
is_self_destructed: true,
};
export const withChangedByteCode: Partial<SmartContract> = {
...verified,
is_changed_bytecode: true,
};
export const nonVerified: Partial<SmartContract> = {
is_verified: false,
creation_bytecode: 'creation_bytecode',
deployed_bytecode: 'deployed_bytecode',
};
import type {
SmartContractQueryMethodReadError,
SmartContractQueryMethodReadSuccess,
SmartContractReadMethod,
SmartContractWriteMethod,
} from 'types/api/contract';
export const read: Array<SmartContractReadMethod> = [
{
constant: true,
inputs: [
{ internalType: 'address', name: '', type: 'address' },
],
method_id: '70a08231',
name: 'balanceOf',
outputs: [
{ internalType: 'uint256', name: '', type: 'uint256', value: '' },
],
payable: false,
stateMutability: 'view',
type: 'function',
},
{
constant: true,
inputs: [],
method_id: '06fdde03',
name: 'name',
outputs: [
{ internalType: 'string', name: '', type: 'string', value: 'Wrapped POA' },
],
payable: false,
stateMutability: 'view',
type: 'function',
},
{
constant: true,
inputs: [],
method_id: '18160ddd',
name: 'totalSupply',
outputs: [
{ internalType: 'uint256', name: '', type: 'uint256', value: '139905710421584994690047413' },
],
payable: false,
stateMutability: 'view',
type: 'function',
},
{
constant: true,
error: '(-32015) VM execution error. (revert)',
inputs: [],
method_id: 'df0ad3de',
name: 'upgradeabilityAdmin',
outputs: [
{ name: '', type: 'address', value: '' },
],
payable: false,
stateMutability: 'view',
type: 'function',
},
{
constant: true,
inputs: [],
method_id: '165ec2e2',
name: 'arianeeWhitelist',
outputs: [
{
name: '',
type: 'address',
value: '0xd3eee7f8e8021db24825c3457d5479f2b57f40ef',
},
],
payable: false,
stateMutability: 'view',
type: 'function',
},
];
export const readResultSuccess: SmartContractQueryMethodReadSuccess = {
is_error: false,
result: {
names: [ 'uint256' ],
output: [
{ type: 'uint256', value: '42' },
],
},
};
export const readResultError: SmartContractQueryMethodReadError = {
is_error: true,
result: {
message: 'Some shit happened',
code: -32017,
},
};
export const write: Array<SmartContractWriteMethod> = [
{
payable: true,
stateMutability: 'payable',
type: 'fallback',
},
{
constant: false,
inputs: [
{ internalType: 'address', name: 'guy', type: 'address' },
{ internalType: 'uint256', name: 'wad', type: 'uint256' },
],
name: 'approve',
outputs: [
{ internalType: 'bool', name: '', type: 'bool' },
],
payable: false,
stateMutability: 'nonpayable',
type: 'function',
},
{
constant: false,
inputs: [
{ internalType: 'address', name: 'src', type: 'address' },
{ internalType: 'address', name: 'dst', type: 'address' },
],
name: 'transferFrom',
outputs: [
{ internalType: 'bool', name: '', type: 'bool' },
],
payable: true,
stateMutability: 'payable',
type: 'function',
},
{
stateMutability: 'payable',
type: 'receive',
},
{
constant: false,
inputs: [],
name: 'pause',
outputs: [],
payable: false,
stateMutability: 'nonpayable',
type: 'function',
},
{
constant: false,
inputs: [
{ name: '_from', type: 'address' },
{ name: '_to', type: 'address' },
{ name: '_tokenId', type: 'uint256' },
{ name: '_data', type: 'bytes' },
],
name: 'safeTransferFrom',
outputs: [],
payable: false,
stateMutability: 'nonpayable',
type: 'function',
},
{
constant: false,
inputs: [
{ name: '_tokenId', type: 'uint256' },
{ name: '_hash', type: 'bytes32' },
{ name: '_keepRequestToken', type: 'bool' },
{ name: '_newOwner', type: 'address' },
{ name: '_signature', type: 'bytes' },
],
name: 'requestToken',
outputs: [
{ name: 'reward', type: 'uint256' },
],
payable: false,
stateMutability: 'nonpayable',
type: 'function',
},
{
constant: false,
inputs: [
{ name: '_tokenId', type: 'uint256' },
{ name: '_imprint', type: 'bytes32' },
{ name: '_uri', type: 'string' },
{ name: '_initialKey', type: 'address' },
{ name: '_tokenRecoveryTimestamp', type: 'uint256' },
{ name: '_initialKeyIsRequestKey', type: 'bool' },
],
name: 'hydrateToken',
outputs: [
{ name: '', type: 'uint256' },
],
payable: false,
stateMutability: 'nonpayable',
type: 'function',
},
];
...@@ -15,3 +15,113 @@ export const tokenCounters: TokenCounters = { ...@@ -15,3 +15,113 @@ export const tokenCounters: TokenCounters = {
token_holders_count: '8838883', token_holders_count: '8838883',
transfers_count: '88282281', transfers_count: '88282281',
}; };
export const tokenInfoERC20a: TokenInfo = {
address: '0xb2a90505dc6680a7a695f7975d0d32EeF610f456',
decimals: '18',
exchange_rate: null,
holders: '23',
name: 'hyfi.token',
symbol: 'HyFi',
total_supply: '369000000000000000000000000',
type: 'ERC-20',
};
export const tokenInfoERC20b: TokenInfo = {
address: '0xc1116c98ba622a6218433fF90a2E40DEa482d7A7',
decimals: '6',
exchange_rate: '0.982',
holders: '17',
name: 'USD Coin',
symbol: 'USDC',
total_supply: '900000000000000000000000000',
type: 'ERC-20',
};
export const tokenInfoERC20c: TokenInfo = {
address: '0xc1116c98ba622a6218433fF90a2E40DEa482d7A7',
decimals: '18',
exchange_rate: '1328.89',
holders: '17',
name: 'Ethereum',
symbol: 'ETH',
total_supply: '1000000000000000000000000',
type: 'ERC-20',
};
export const tokenInfoERC20d: TokenInfo = {
address: '0xCc7bb2D219A0FC08033E130629C2B854b7bA9195',
decimals: '18',
exchange_rate: null,
holders: '102625',
name: 'Zeta',
symbol: 'ZETA',
total_supply: '2100000000000000000000000000',
type: 'ERC-20',
};
export const tokenInfoERC721a: TokenInfo = {
address: '0xDe7cAc71E072FCBd4453E5FB3558C2684d1F88A0',
decimals: null,
exchange_rate: null,
holders: '7',
name: 'HyFi Athena',
symbol: 'HYFI_ATHENA',
total_supply: '105',
type: 'ERC-721',
};
export const tokenInfoERC721b: TokenInfo = {
address: '0xA8d5C7beEA8C9bB57f5fBa35fB638BF45550b11F',
decimals: null,
exchange_rate: null,
holders: '2',
name: 'World Of Women Galaxy',
symbol: 'WOWG',
total_supply: null,
type: 'ERC-721',
};
export const tokenInfoERC721c: TokenInfo = {
address: '0x47646F1d7dc4Dd2Db5a41D092e2Cf966e27A4992',
decimals: null,
exchange_rate: null,
holders: '12',
name: 'Puma',
symbol: 'PUMA',
total_supply: null,
type: 'ERC-721',
};
export const tokenInfoERC1155a: TokenInfo = {
address: '0x4b333DEd10c7ca855EA2C8D4D90A0a8b73788c8e',
decimals: null,
exchange_rate: null,
holders: '22',
name: 'HyFi Membership',
symbol: 'HYFI_MEMBERSHIP',
total_supply: '482',
type: 'ERC-1155',
};
export const tokenInfoERC1155b: TokenInfo = {
address: '0xf4b71b179132ad457f6bcae2a55efa9e4b26eefc',
decimals: null,
exchange_rate: null,
holders: '100',
name: 'WinkyVerse Collections',
symbol: 'WVC',
total_supply: '4943',
type: 'ERC-1155',
};
export const tokenInfoERC1155WithoutName: TokenInfo = {
address: '0x4b333DEd10c7ca855EA2C8D4D90A0a8b73788c8e',
decimals: null,
exchange_rate: null,
holders: '22',
name: null,
symbol: null,
total_supply: '482',
type: 'ERC-1155',
};
...@@ -40,6 +40,7 @@ export const erc20: TokenTransfer = { ...@@ -40,6 +40,7 @@ export const erc20: TokenTransfer = {
timestamp: '2022-10-10T14:34:30.000000Z', timestamp: '2022-10-10T14:34:30.000000Z',
block_hash: '1', block_hash: '1',
log_index: '1', log_index: '1',
method: 'updateSmartAsset',
}; };
export const erc721: TokenTransfer = { export const erc721: TokenTransfer = {
...@@ -81,6 +82,7 @@ export const erc721: TokenTransfer = { ...@@ -81,6 +82,7 @@ export const erc721: TokenTransfer = {
timestamp: '2022-10-10T14:34:30.000000Z', timestamp: '2022-10-10T14:34:30.000000Z',
block_hash: '1', block_hash: '1',
log_index: '1', log_index: '1',
method: 'updateSmartAsset',
}; };
export const erc1155: TokenTransfer = { export const erc1155: TokenTransfer = {
......
...@@ -21,7 +21,7 @@ ...@@ -21,7 +21,7 @@
"lint:tsc": "./node_modules/.bin/tsc -p ./tsconfig.json", "lint:tsc": "./node_modules/.bin/tsc -p ./tsconfig.json",
"prepare": "husky install", "prepare": "husky install",
"format-svg": "./node_modules/.bin/svgo -r ./icons", "format-svg": "./node_modules/.bin/svgo -r ./icons",
"test:pw": "./playwright/make-envs-script.sh && playwright test -c playwright-ct.config.ts", "test:pw": "./playwright/make-envs-script.sh && NODE_OPTIONS=\"--max-old-space-size=4096\" playwright test -c playwright-ct.config.ts",
"test:pw:local": "export NODE_PATH=$(pwd)/node_modules && yarn test:pw", "test:pw:local": "export NODE_PATH=$(pwd)/node_modules && yarn test:pw",
"test:pw:docker": "docker run --rm --network host -v $(pwd):/work/ -w /work/ -it mcr.microsoft.com/playwright:v1.28.0-focal ./playwright/run-tests.sh", "test:pw:docker": "docker run --rm --network host -v $(pwd):/work/ -w /work/ -it mcr.microsoft.com/playwright:v1.28.0-focal ./playwright/run-tests.sh",
"test:jest": "jest", "test:jest": "jest",
...@@ -40,6 +40,8 @@ ...@@ -40,6 +40,8 @@
"@tanstack/react-query-devtools": "^4.0.10", "@tanstack/react-query-devtools": "^4.0.10",
"@types/papaparse": "^5.3.5", "@types/papaparse": "^5.3.5",
"@types/react-scroll": "^1.8.4", "@types/react-scroll": "^1.8.4",
"@web3modal/ethereum": "^2.0.0-rc.2",
"@web3modal/react": "^2.0.0-rc.2",
"ace-builds": "^1.14.0", "ace-builds": "^1.14.0",
"bignumber.js": "^9.1.0", "bignumber.js": "^9.1.0",
"d3": "^7.6.1", "d3": "^7.6.1",
...@@ -64,7 +66,8 @@ ...@@ -64,7 +66,8 @@
"react-identicons": "^1.2.5", "react-identicons": "^1.2.5",
"react-jazzicon": "^1.0.4", "react-jazzicon": "^1.0.4",
"react-scroll": "^1.8.7", "react-scroll": "^1.8.7",
"use-font-face-observer": "^1.2.1" "use-font-face-observer": "^1.2.1",
"wagmi": "^0.10.6"
}, },
"devDependencies": { "devDependencies": {
"@playwright/experimental-ct-react": "^1.28.0", "@playwright/experimental-ct-react": "^1.28.0",
......
...@@ -50,7 +50,7 @@ const CustomErrorComponent = (props: Props) => { ...@@ -50,7 +50,7 @@ const CustomErrorComponent = (props: Props) => {
); );
} }
const colorModeCookie = cookies.getFromCookieString(props.cookies, cookies.NAMES.COLOR_MODE); const colorModeCookie = cookies.getFromCookieString(props.cookies || '', cookies.NAMES.COLOR_MODE);
return <NextErrorComponent statusCode={ props.statusCode } withDarkMode={ colorModeCookie === 'dark' }/>; return <NextErrorComponent statusCode={ props.statusCode } withDarkMode={ colorModeCookie === 'dark' }/>;
}; };
......
import type { NextPage } from 'next';
import Head from 'next/head';
import React from 'react';
import getNetworkTitle from 'lib/networks/getNetworkTitle';
import Accounts from 'ui/pages/Accounts';
const AccountsPage: NextPage = () => {
const title = `Top Accounts - ${ getNetworkTitle() }`;
return (
<>
<Head><title>{ title }</title></Head>
<Accounts/>
</>
);
};
export default AccountsPage;
export { getServerSideProps } from 'lib/next/getServerSideProps';
import type { NextPage } from 'next';
import Head from 'next/head';
import React from 'react';
import getNetworkTitle from 'lib/networks/getNetworkTitle';
import Tokens from 'ui/pages/Tokens';
const TokensPage: NextPage = () => {
const title = getNetworkTitle();
return (
<>
<Head>
<title>{ title }</title>
</Head>
<Tokens/>
</>
);
};
export default TokensPage;
export { getServerSideProps } from 'lib/next/getServerSideProps';
...@@ -51,6 +51,12 @@ const config: PlaywrightTestConfig = { ...@@ -51,6 +51,12 @@ const config: PlaywrightTestConfig = {
exportAsDefault: true, exportAsDefault: true,
}), }),
], ],
build: {
// it actually frees some memory that vite needs a lot
// https://github.com/storybookjs/builder-vite/issues/409#issuecomment-1152848986
sourcemap: false,
minify: false,
},
}, },
}, },
......
import { ChakraProvider } from '@chakra-ui/react'; import { ChakraProvider } from '@chakra-ui/react';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { providers } from 'ethers';
import React from 'react'; import React from 'react';
import { createClient, WagmiConfig } from 'wagmi';
import { mainnet } from 'wagmi/chains';
import { MockConnector } from 'wagmi/connectors/mock';
import { AppContextProvider } from 'lib/appContext'; import { AppContextProvider } from 'lib/appContext';
import type { Props as PageProps } from 'lib/next/getServerSideProps'; import type { Props as PageProps } from 'lib/next/getServerSideProps';
...@@ -23,6 +27,29 @@ const defaultAppContext = { ...@@ -23,6 +27,29 @@ const defaultAppContext = {
}, },
}; };
// >>> Web3 stuff
const provider = new providers.JsonRpcProvider(
'http://localhost:8545',
{
name: 'POA',
chainId: 99,
},
);
const connector = new MockConnector({
chains: [ mainnet ],
options: {
signer: provider.getSigner(),
},
});
const wagmiClient = createClient({
autoConnect: true,
connectors: [ connector ],
provider,
});
// <<<<
const TestApp = ({ children, withSocket, appContext = defaultAppContext }: Props) => { const TestApp = ({ children, withSocket, appContext = defaultAppContext }: Props) => {
const [ queryClient ] = React.useState(() => new QueryClient({ const [ queryClient ] = React.useState(() => new QueryClient({
defaultOptions: { defaultOptions: {
...@@ -38,7 +65,9 @@ const TestApp = ({ children, withSocket, appContext = defaultAppContext }: Props ...@@ -38,7 +65,9 @@ const TestApp = ({ children, withSocket, appContext = defaultAppContext }: Props
<QueryClientProvider client={ queryClient }> <QueryClientProvider client={ queryClient }>
<SocketProvider url={ withSocket ? `ws://localhost:${ PORT }` : undefined }> <SocketProvider url={ withSocket ? `ws://localhost:${ PORT }` : undefined }>
<AppContextProvider { ...appContext }> <AppContextProvider { ...appContext }>
{ children } <WagmiConfig client={ wagmiClient }>
{ children }
</WagmiConfig>
</AppContextProvider> </AppContextProvider>
</SocketProvider> </SocketProvider>
</QueryClientProvider> </QueryClientProvider>
......
...@@ -70,15 +70,15 @@ const variantOutline = defineStyle((props) => { ...@@ -70,15 +70,15 @@ const variantOutline = defineStyle((props) => {
borderColor, borderColor,
bg: 'transparent', bg: 'transparent',
_hover: { _hover: {
color: 'blue.400', color: 'link_hovered',
borderColor: 'blue.400', borderColor: 'link_hovered',
bg: 'transparent', bg: 'transparent',
_active: { _active: {
bg: props.isActive ? activeBg : 'transparent', bg: props.isActive ? activeBg : 'transparent',
borderColor: props.isActive ? activeBg : 'blue.400', borderColor: props.isActive ? activeBg : 'link_hovered',
color: props.isActive ? activeColor : 'blue.400', color: props.isActive ? activeColor : 'link_hovered',
p: { p: {
color: 'blue.400', color: 'link_hovered',
}, },
}, },
_disabled: { _disabled: {
...@@ -86,7 +86,7 @@ const variantOutline = defineStyle((props) => { ...@@ -86,7 +86,7 @@ const variantOutline = defineStyle((props) => {
borderColor, borderColor,
}, },
p: { p: {
color: 'blue.400', color: 'link_hovered',
}, },
}, },
_disabled: { _disabled: {
...@@ -147,7 +147,7 @@ const variantSubtle = defineStyle((props) => { ...@@ -147,7 +147,7 @@ const variantSubtle = defineStyle((props) => {
bg: mode('blackAlpha.200', 'whiteAlpha.200')(props), bg: mode('blackAlpha.200', 'whiteAlpha.200')(props),
color: mode('blackAlpha.800', 'whiteAlpha.800')(props), color: mode('blackAlpha.800', 'whiteAlpha.800')(props),
_hover: { _hover: {
color: 'blue.400', color: 'link_hovered',
_disabled: { _disabled: {
color: mode('blackAlpha.800', 'whiteAlpha.800')(props), color: mode('blackAlpha.800', 'whiteAlpha.800')(props),
bg: mode('blackAlpha.200', 'whiteAlpha.200')(props), bg: mode('blackAlpha.200', 'whiteAlpha.200')(props),
...@@ -160,7 +160,7 @@ const variantSubtle = defineStyle((props) => { ...@@ -160,7 +160,7 @@ const variantSubtle = defineStyle((props) => {
bg: `${ c }.100`, bg: `${ c }.100`,
color: `${ c }.600`, color: `${ c }.600`,
_hover: { _hover: {
color: 'blue.400', color: 'link_hovered',
}, },
}; };
}); });
......
...@@ -8,9 +8,9 @@ const baseStyle = defineStyle(getDefaultTransitionProps()); ...@@ -8,9 +8,9 @@ const baseStyle = defineStyle(getDefaultTransitionProps());
const variantPrimary = defineStyle((props) => { const variantPrimary = defineStyle((props) => {
return { return {
color: mode('blue.600', 'blue.300')(props), color: 'link',
_hover: { _hover: {
color: mode('blue.400', 'blue.200')(props), color: 'link_hovered',
textDecorationStyle: props.textDecorationStyle || 'solid', textDecorationStyle: props.textDecorationStyle || 'solid',
}, },
}; };
......
...@@ -51,7 +51,7 @@ const baseStyleCloseButton = defineStyle((props) => { ...@@ -51,7 +51,7 @@ const baseStyleCloseButton = defineStyle((props) => {
height: 10, height: 10,
width: 10, width: 10,
color: mode('gray.700', 'gray.500')(props), color: mode('gray.700', 'gray.500')(props),
_hover: { color: 'blue.400' }, _hover: { color: 'link_hovered' },
_active: { bg: 'none' }, _active: { bg: 'none' },
}; };
}); });
......
...@@ -23,7 +23,7 @@ const variantSimple = definePartsStyle((props) => { ...@@ -23,7 +23,7 @@ const variantSimple = definePartsStyle((props) => {
...transitionProps, ...transitionProps,
}, },
td: { td: {
borderColor: mode('blackAlpha.200', 'whiteAlpha.200')(props), borderColor: 'divider',
...transitionProps, ...transitionProps,
}, },
}; };
......
...@@ -22,7 +22,7 @@ const variantSoftRounded = definePartsStyle((props) => { ...@@ -22,7 +22,7 @@ const variantSoftRounded = definePartsStyle((props) => {
}, },
}, },
_hover: { _hover: {
color: 'blue.400', color: 'link_hovered',
}, },
}, },
}; };
......
...@@ -10,10 +10,6 @@ const variantSecondary = defineStyle((props) => ({ ...@@ -10,10 +10,6 @@ const variantSecondary = defineStyle((props) => ({
color: mode('gray.500', 'gray.400')(props), color: mode('gray.500', 'gray.400')(props),
})); }));
const variantAlternate = defineStyle((props) => ({
color: mode('blue.600', 'blue.300')(props),
}));
const variantInherit = { const variantInherit = {
color: 'inherit', color: 'inherit',
}; };
...@@ -21,7 +17,6 @@ const variantInherit = { ...@@ -21,7 +17,6 @@ const variantInherit = {
const variants: Record<string, SystemStyleInterpolation> = { const variants: Record<string, SystemStyleInterpolation> = {
primary: variantPrimary, primary: variantPrimary,
secondary: variantSecondary, secondary: variantSecondary,
alternate: variantAlternate,
inherit: variantInherit, inherit: variantInherit,
}; };
......
const semanticTokens = {
colors: {
divider: {
'default': 'blackAlpha.200',
_dark: 'whiteAlpha.200',
},
link: {
'default': 'blue.600',
_dark: 'blue.300',
},
link_hovered: {
'default': 'blue.400',
},
},
};
export default semanticTokens;
...@@ -5,6 +5,7 @@ import config from './config'; ...@@ -5,6 +5,7 @@ import config from './config';
import borders from './foundations/borders'; import borders from './foundations/borders';
import breakpoints from './foundations/breakpoints'; import breakpoints from './foundations/breakpoints';
import colors from './foundations/colors'; import colors from './foundations/colors';
import semanticTokens from './foundations/semanticTokens';
import transition from './foundations/transition'; import transition from './foundations/transition';
import typography from './foundations/typography'; import typography from './foundations/typography';
import zIndices from './foundations/zIndices'; import zIndices from './foundations/zIndices';
...@@ -22,6 +23,7 @@ const overrides = { ...@@ -22,6 +23,7 @@ const overrides = {
breakpoints, breakpoints,
transition, transition,
zIndices, zIndices,
semanticTokens,
}; };
export default extendTheme(overrides); export default extendTheme(overrides);
...@@ -30,6 +30,10 @@ export default function getOutlinedFieldStyles(props: StyleFunctionProps) { ...@@ -30,6 +30,10 @@ export default function getOutlinedFieldStyles(props: StyleFunctionProps) {
_hover: { _hover: {
borderColor: 'transparent', borderColor: 'transparent',
}, },
':-webkit-autofill': {
// background color for disabled input which value was selected from browser autocomplete popup
'-webkit-box-shadow': `0 0 0px 1000px ${ mode('rgba(16, 17, 18, 0.08)', 'rgba(255, 255, 255, 0.08)')(props) } inset`,
},
}, },
_invalid: { _invalid: {
borderColor: getColor(theme, errorColor), borderColor: getColor(theme, errorColor),
......
...@@ -81,8 +81,9 @@ export interface AddressTokenTransferResponse { ...@@ -81,8 +81,9 @@ export interface AddressTokenTransferResponse {
} }
export type AddressTokenTransferFilters = { export type AddressTokenTransferFilters = {
filter: AddressFromToFilter; filter?: AddressFromToFilter;
type: Array<TokenType>; type?: Array<TokenType>;
token?: string;
} }
export type AddressTokensFilter = { export type AddressTokensFilter = {
......
import type { AddressParam } from './addressParams';
export type AddressesItem = AddressParam &{ tx_count: string; coin_balance: string }
export type AddressesResponse = {
items: Array<AddressesItem>;
next_page_params: {
fetched_coin_balance: string;
hash: string;
items_count: number;
};
total_supply: string;
}
import type { Abi } from 'abitype';
export type SmartContractMethodArgType = 'address' | 'uint256' | 'bool' | 'string' | 'bytes' | 'bytes32';
export type SmartContractMethodStateMutability = 'view' | 'nonpayable' | 'payable';
export interface SmartContract { export interface SmartContract {
deployed_bytecode: string | null; deployed_bytecode: string | null;
creation_bytecode: string | null; creation_bytecode: string | null;
is_self_destructed: boolean; is_self_destructed: boolean;
abi: Array<Record<string, unknown>> | null; abi: Abi | null;
compiler_version: string | null; compiler_version: string | null;
evm_version: string | null; evm_version: string | null;
optimization_enabled: boolean | null; optimization_enabled: boolean | null;
...@@ -10,8 +15,38 @@ export interface SmartContract { ...@@ -10,8 +15,38 @@ export interface SmartContract {
name: string | null; name: string | null;
verified_at: string | null; verified_at: string | null;
is_verified: boolean | null; is_verified: boolean | null;
is_changed_bytecode: boolean | null;
// sourcify info >>>
is_verified_via_sourcify: boolean | null;
is_fully_verified: boolean | null;
is_partially_verified: boolean | null;
sourcify_repo_url: string | null;
// <<<<
source_code: string | null; source_code: string | null;
constructor_args: string | null;
decoded_constructor_args: Array<SmartContractDecodedConstructorArg> | null;
can_be_visualized_via_sol2uml: boolean | null; can_be_visualized_via_sol2uml: boolean | null;
is_vyper_contract: boolean | null;
file_path: string;
additional_sources: Array<{ file_path: string; source_code: string }>;
external_libraries: Array<SmartContractExternalLibrary> | null;
compiler_settings: unknown;
verified_twin_address_hash: string | null;
minimal_proxy_address_hash: string | null;
}
export type SmartContractDecodedConstructorArg = [
string,
{
internalType: SmartContractMethodArgType;
name: string;
type: SmartContractMethodArgType;
}
]
export interface SmartContractExternalLibrary {
address_hash: string;
name: string;
} }
export interface SmartContractMethodBase { export interface SmartContractMethodBase {
...@@ -19,9 +54,10 @@ export interface SmartContractMethodBase { ...@@ -19,9 +54,10 @@ export interface SmartContractMethodBase {
outputs: Array<SmartContractMethodOutput>; outputs: Array<SmartContractMethodOutput>;
constant: boolean; constant: boolean;
name: string; name: string;
stateMutability: string; stateMutability: SmartContractMethodStateMutability;
type: 'function'; type: 'function';
payable: boolean; payable: boolean;
error?: string;
} }
export interface SmartContractReadMethod extends SmartContractMethodBase { export interface SmartContractReadMethod extends SmartContractMethodBase {
...@@ -29,21 +65,51 @@ export interface SmartContractReadMethod extends SmartContractMethodBase { ...@@ -29,21 +65,51 @@ export interface SmartContractReadMethod extends SmartContractMethodBase {
} }
export interface SmartContractWriteFallback { export interface SmartContractWriteFallback {
payable: true; payable?: true;
stateMutability: 'payable'; stateMutability: 'payable';
type: 'fallback'; type: 'fallback';
} }
export type SmartContractWriteMethod = SmartContractMethodBase | SmartContractWriteFallback; export interface SmartContractWriteReceive {
payable?: true;
stateMutability: 'payable';
type: 'receive';
}
export type SmartContractWriteMethod = SmartContractMethodBase | SmartContractWriteFallback | SmartContractWriteReceive;
export type SmartContractMethod = SmartContractReadMethod | SmartContractWriteMethod; export type SmartContractMethod = SmartContractReadMethod | SmartContractWriteMethod;
export interface SmartContractMethodInput { export interface SmartContractMethodInput {
internalType: string; internalType?: SmartContractMethodArgType;
name: string; name: string;
type: string; type: SmartContractMethodArgType;
} }
export interface SmartContractMethodOutput extends SmartContractMethodInput { export interface SmartContractMethodOutput extends SmartContractMethodInput {
value?: string; value?: string;
} }
export interface SmartContractQueryMethodReadSuccess {
is_error: false;
result: {
names: Array<string>;
output: Array<{
type: string;
value: string;
}>;
};
}
export interface SmartContractQueryMethodReadError {
is_error: true;
result: {
code: number;
message: string;
raw?: string;
} | {
error: string;
};
}
export type SmartContractQueryMethodRead = SmartContractQueryMethodReadSuccess | SmartContractQueryMethodReadError;
...@@ -39,6 +39,7 @@ interface TokenTransferBase { ...@@ -39,6 +39,7 @@ interface TokenTransferBase {
timestamp: string; timestamp: string;
block_hash: string; block_hash: string;
log_index: string; log_index: string;
method?: string;
} }
export type TokenTransferPagination = { export type TokenTransferPagination = {
......
import type { TokenInfo, TokenType } from './tokenInfo';
export type TokensResponse = {
items: Array<TokenInfo>;
next_page_params: {
holder_count: number;
items_count: number;
name: string;
};
}
export type TokensFilters = { filter: string; type: Array<TokenType> | undefined };
import { useColorModeValue, useToken } from '@chakra-ui/react';
import {
EthereumClient,
modalConnectors,
walletConnectProvider,
} from '@web3modal/ethereum';
import { Web3Modal } from '@web3modal/react';
import React from 'react'; import React from 'react';
import type { Chain } from 'wagmi';
import { configureChains, createClient, WagmiConfig } from 'wagmi';
import type { RoutedSubTab } from 'ui/shared/RoutedTabs/types'; import type { RoutedSubTab } from 'ui/shared/RoutedTabs/types';
import appConfig from 'configs/app/config';
import { ContractContextProvider } from 'ui/address/contract/context';
import RoutedTabs from 'ui/shared/RoutedTabs/RoutedTabs'; import RoutedTabs from 'ui/shared/RoutedTabs/RoutedTabs';
interface Props { interface Props {
tabs: Array<RoutedSubTab>; tabs: Array<RoutedSubTab>;
} }
export const currentChain: Chain = {
id: Number(appConfig.network.id),
name: appConfig.network.name || '',
network: appConfig.network.name || '',
nativeCurrency: {
decimals: appConfig.network.currency.decimals,
name: appConfig.network.currency.name || '',
symbol: appConfig.network.currency.symbol || '',
},
rpcUrls: {
'default': {
http: [ appConfig.network.rpcUrl || '' ],
},
},
blockExplorers: {
'default': {
name: 'Blockscout',
url: appConfig.baseUrl,
},
},
};
const chains = [ currentChain ];
const { provider } = configureChains(chains, [
walletConnectProvider({ projectId: appConfig.walletConnect.projectId || '' }),
]);
const wagmiClient = createClient({
autoConnect: true,
connectors: modalConnectors({ appName: 'web3Modal', chains }),
provider,
});
const ethereumClient = new EthereumClient(wagmiClient, chains);
const TAB_LIST_PROPS = {
columnGap: 3,
};
const AddressContract = ({ tabs }: Props) => { const AddressContract = ({ tabs }: Props) => {
return <RoutedTabs tabs={ tabs } variant="outline" colorScheme="gray" size="sm" tabListProps={{ columnGap: 3 }}/>; const modalZIndex = useToken<string>('zIndices', 'modal');
return (
<WagmiConfig client={ wagmiClient }>
<ContractContextProvider>
<RoutedTabs tabs={ tabs } variant="outline" colorScheme="gray" size="sm" tabListProps={ TAB_LIST_PROPS }/>
</ContractContextProvider>
<Web3Modal
projectId={ appConfig.walletConnect.projectId }
ethereumClient={ ethereumClient }
themeZIndex={ Number(modalZIndex) }
themeMode={ useColorModeValue('light', 'dark') }
themeBackground="themeColor"
/>
</WagmiConfig>
);
}; };
export default React.memo(AddressContract); export default React.memo(AddressContract);
import type { MetaMaskInpageProvider } from '@metamask/providers';
import { test, expect } from '@playwright/experimental-ct-react';
import type { UseQueryResult } from '@tanstack/react-query';
import React from 'react';
import type { Address } from 'types/api/address';
import * as addressMock from 'mocks/address/address';
import * as countersMock from 'mocks/address/counters';
import * as tokenBalanceMock from 'mocks/address/tokenBalance';
import TestApp from 'playwright/TestApp';
import buildApiUrl from 'playwright/utils/buildApiUrl';
import AddressDetails from './AddressDetails';
import MockAddressPage from './testUtils/MockAddressPage';
const ADDRESS_HASH = addressMock.hash;
const API_URL_ADDRESS = buildApiUrl('address', { id: ADDRESS_HASH });
const API_URL_COUNTERS = buildApiUrl('address_counters', { id: ADDRESS_HASH });
const API_URL_TOKEN_BALANCES = buildApiUrl('address_token_balances', { id: ADDRESS_HASH });
const hooksConfig = {
router: {
query: { id: ADDRESS_HASH },
},
};
test('contract +@mobile', async({ mount, page }) => {
await page.route(API_URL_ADDRESS, (route) => route.fulfill({
status: 200,
body: JSON.stringify(addressMock.contract),
}));
await page.route(API_URL_COUNTERS, (route) => route.fulfill({
status: 200,
body: JSON.stringify(countersMock.forContract),
}));
const component = await mount(
<TestApp>
<AddressDetails addressQuery={{ data: addressMock.contract } as UseQueryResult<Address, unknown>}/>
</TestApp>,
{ hooksConfig },
);
await expect(component).toHaveScreenshot();
});
test('token', async({ mount, page }) => {
await page.route(API_URL_ADDRESS, (route) => route.fulfill({
status: 200,
body: JSON.stringify(addressMock.token),
}));
await page.route(API_URL_COUNTERS, (route) => route.fulfill({
status: 200,
body: JSON.stringify(countersMock.forToken),
}));
await page.route(API_URL_TOKEN_BALANCES, (route) => route.fulfill({
status: 200,
body: JSON.stringify(tokenBalanceMock.baseList),
}));
await page.evaluate(() => {
window.ethereum = { } as MetaMaskInpageProvider;
});
const component = await mount(
<TestApp>
<MockAddressPage>
<AddressDetails addressQuery={{ data: addressMock.token } as UseQueryResult<Address, unknown>}/>
</MockAddressPage>
</TestApp>,
{ hooksConfig },
);
await expect(component).toHaveScreenshot();
});
test('validator +@mobile', async({ mount, page }) => {
await page.route(API_URL_ADDRESS, (route) => route.fulfill({
status: 200,
body: JSON.stringify(addressMock.validator),
}));
await page.route(API_URL_COUNTERS, (route) => route.fulfill({
status: 200,
body: JSON.stringify(countersMock.forValidator),
}));
const component = await mount(
<TestApp>
<AddressDetails addressQuery={{ data: addressMock.validator } as UseQueryResult<Address, unknown>}/>
</TestApp>,
{ hooksConfig },
);
await expect(component).toHaveScreenshot();
});
...@@ -18,6 +18,7 @@ import DataFetchAlert from 'ui/shared/DataFetchAlert'; ...@@ -18,6 +18,7 @@ import DataFetchAlert from 'ui/shared/DataFetchAlert';
import DetailsInfoItem from 'ui/shared/DetailsInfoItem'; import DetailsInfoItem from 'ui/shared/DetailsInfoItem';
import ExternalLink from 'ui/shared/ExternalLink'; import ExternalLink from 'ui/shared/ExternalLink';
import HashStringShorten from 'ui/shared/HashStringShorten'; import HashStringShorten from 'ui/shared/HashStringShorten';
import HashStringShortenDynamic from 'ui/shared/HashStringShortenDynamic';
import AddressAddToMetaMask from './details/AddressAddToMetaMask'; import AddressAddToMetaMask from './details/AddressAddToMetaMask';
import AddressBalance from './details/AddressBalance'; import AddressBalance from './details/AddressBalance';
...@@ -99,11 +100,23 @@ const AddressDetails = ({ addressQuery, scrollRef }: Props) => { ...@@ -99,11 +100,23 @@ const AddressDetails = ({ addressQuery, scrollRef }: Props) => {
title="Creator" title="Creator"
hint="Transaction and address of creation." hint="Transaction and address of creation."
> >
<AddressLink hash={ addressQuery.data.creator_address_hash } truncation="constant"/> <AddressLink type="address" hash={ addressQuery.data.creator_address_hash } truncation="constant"/>
<Text whiteSpace="pre"> at </Text> <Text whiteSpace="pre"> at txn </Text>
<AddressLink hash={ addressQuery.data.creation_tx_hash } type="transaction" truncation="constant"/> <AddressLink hash={ addressQuery.data.creation_tx_hash } type="transaction" truncation="constant"/>
</DetailsInfoItem> </DetailsInfoItem>
) } ) }
{ addressQuery.data.is_contract && addressQuery.data.implementation_address && (
<DetailsInfoItem
title="Implementation"
hint="Implementation address of the proxy contract."
columnGap={ 1 }
>
<Link href={ link('address_index', { id: addressQuery.data.implementation_address }) }>{ addressQuery.data.implementation_name }</Link>
<Text variant="secondary" overflow="hidden">
<HashStringShortenDynamic hash={ `(${ addressQuery.data.implementation_address })` }/>
</Text>
</DetailsInfoItem>
) }
<AddressBalance data={ addressQuery.data }/> <AddressBalance data={ addressQuery.data }/>
{ addressQuery.data.has_tokens && ( { addressQuery.data.has_tokens && (
<DetailsInfoItem <DetailsInfoItem
......
import { Box } from '@chakra-ui/react';
import { test, expect } from '@playwright/experimental-ct-react';
import React from 'react';
import { erc1155 } from 'mocks/tokens/tokenTransfer';
import TestApp from 'playwright/TestApp';
import buildApiUrl from 'playwright/utils/buildApiUrl';
import AddressTokenTransfers from './AddressTokenTransfers';
const API_URL = buildApiUrl('address_token_transfers', { id: '0xd789a607CEac2f0E14867de4EB15b15C9FFB5859' }) +
'?token=0x1189a607CEac2f0E14867de4EB15b15C9FFB5859';
const hooksConfig = {
router: {
query: { id: '0xd789a607CEac2f0E14867de4EB15b15C9FFB5859', token: '0x1189a607CEac2f0E14867de4EB15b15C9FFB5859' },
},
};
test('with token filter and pagination +@mobile', async({ mount, page }) => {
await page.route(API_URL, (route) => route.fulfill({
status: 200,
body: JSON.stringify({ items: [ erc1155 ], next_page_params: { block_number: 1 } }),
}));
const component = await mount(
<TestApp>
<Box h={{ base: '134px', lg: 6 }}/>
<AddressTokenTransfers/>
</TestApp>,
{ hooksConfig },
);
await expect(component).toHaveScreenshot();
});
test('with token filter and no pagination +@mobile', async({ mount, page }) => {
await page.route(API_URL, (route) => route.fulfill({
status: 200,
body: JSON.stringify({ items: [ erc1155 ] }),
}));
const component = await mount(
<TestApp>
<Box h={{ base: '134px', lg: 6 }}/>
<AddressTokenTransfers/>
</TestApp>,
{ hooksConfig },
);
await expect(component).toHaveScreenshot();
});
import { Hide, Show, Text } from '@chakra-ui/react'; import { Flex, Hide, Icon, Show, Text, Tooltip, useColorModeValue } 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';
...@@ -9,13 +9,16 @@ import type { AddressFromToFilter, AddressTokenTransferResponse } from 'types/ap ...@@ -9,13 +9,16 @@ import type { AddressFromToFilter, AddressTokenTransferResponse } from 'types/ap
import type { TokenType } from 'types/api/tokenInfo'; import type { TokenType } from 'types/api/tokenInfo';
import type { TokenTransfer } from 'types/api/tokenTransfer'; import type { TokenTransfer } from 'types/api/tokenTransfer';
import crossIcon from 'icons/cross.svg';
import { getResourceKey } from 'lib/api/useApiQuery'; import { getResourceKey } from 'lib/api/useApiQuery';
import getFilterValueFromQuery from 'lib/getFilterValueFromQuery'; import getFilterValueFromQuery from 'lib/getFilterValueFromQuery';
import getFilterValuesFromQuery from 'lib/getFilterValuesFromQuery'; import getFilterValuesFromQuery from 'lib/getFilterValuesFromQuery';
import useIsMobile from 'lib/hooks/useIsMobile';
import useQueryWithPages from 'lib/hooks/useQueryWithPages'; import useQueryWithPages from 'lib/hooks/useQueryWithPages';
import { apos } from 'lib/html-entities'; import { apos } from 'lib/html-entities';
import useSocketChannel from 'lib/socket/useSocketChannel'; import useSocketChannel from 'lib/socket/useSocketChannel';
import useSocketMessage from 'lib/socket/useSocketMessage'; import useSocketMessage from 'lib/socket/useSocketMessage';
import TOKEN_TYPE from 'lib/token/tokenTypes';
import EmptySearchResult from 'ui/apps/EmptySearchResult'; import EmptySearchResult from 'ui/apps/EmptySearchResult';
import ActionBar from 'ui/shared/ActionBar'; import ActionBar from 'ui/shared/ActionBar';
import DataFetchAlert from 'ui/shared/DataFetchAlert'; import DataFetchAlert from 'ui/shared/DataFetchAlert';
...@@ -23,7 +26,8 @@ import Pagination from 'ui/shared/Pagination'; ...@@ -23,7 +26,8 @@ import Pagination from 'ui/shared/Pagination';
import SkeletonList from 'ui/shared/skeletons/SkeletonList'; import SkeletonList from 'ui/shared/skeletons/SkeletonList';
import SkeletonTable from 'ui/shared/skeletons/SkeletonTable'; import SkeletonTable from 'ui/shared/skeletons/SkeletonTable';
import SocketNewItemsNotice from 'ui/shared/SocketNewItemsNotice'; import SocketNewItemsNotice from 'ui/shared/SocketNewItemsNotice';
import { TOKEN_TYPE, flattenTotal } from 'ui/shared/TokenTransfer/helpers'; 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';
...@@ -63,20 +67,26 @@ const matchFilters = (filters: Filters, tokenTransfer: TokenTransfer, address?: ...@@ -63,20 +67,26 @@ const matchFilters = (filters: Filters, tokenTransfer: TokenTransfer, address?:
const AddressTokenTransfers = ({ scrollRef }: {scrollRef?: React.RefObject<HTMLDivElement>}) => { const AddressTokenTransfers = ({ scrollRef }: {scrollRef?: React.RefObject<HTMLDivElement>}) => {
const router = useRouter(); const router = useRouter();
const queryClient = useQueryClient(); const queryClient = useQueryClient();
const isMobile = useIsMobile();
const currentAddress = router.query.id?.toString(); const currentAddress = router.query.id?.toString();
const [ socketAlert, setSocketAlert ] = React.useState(''); const [ socketAlert, setSocketAlert ] = React.useState('');
const [ newItemsCount, setNewItemsCount ] = React.useState(0); const [ newItemsCount, setNewItemsCount ] = React.useState(0);
const tokenFilter = router.query.token ? router.query.token.toString() : undefined;
const [ filters, setFilters ] = React.useState<Filters>( const [ filters, setFilters ] = React.useState<Filters>(
{ type: getTokenFilterValue(router.query.type) || [], filter: getAddressFilterValue(router.query.filter) }, {
type: getTokenFilterValue(router.query.type) || [],
filter: getAddressFilterValue(router.query.filter),
},
); );
const { isError, isLoading, data, pagination, onFilterChange, isPaginationVisible } = useQueryWithPages({ const { isError, isLoading, data, pagination, onFilterChange, isPaginationVisible } = useQueryWithPages({
resourceName: 'address_token_transfers', resourceName: 'address_token_transfers',
pathParams: { id: currentAddress }, pathParams: { id: currentAddress },
filters: filters, filters: tokenFilter ? { token: tokenFilter } : filters,
scrollRef, scrollRef,
}); });
...@@ -91,6 +101,13 @@ const AddressTokenTransfers = ({ scrollRef }: {scrollRef?: React.RefObject<HTMLD ...@@ -91,6 +101,13 @@ const AddressTokenTransfers = ({ scrollRef }: {scrollRef?: React.RefObject<HTMLD
setFilters((prevState) => ({ ...prevState, filter: filterVal })); setFilters((prevState) => ({ ...prevState, filter: filterVal }));
}, [ filters, onFilterChange ]); }, [ filters, onFilterChange ]);
const resetTokenFilter = React.useCallback(() => {
onFilterChange({});
}, [ onFilterChange ]);
const resetTokenIconColor = useColorModeValue('blue.600', 'blue.300');
const resetTokenIconHoverColor = useColorModeValue('blue.400', 'blue.200');
const handleNewSocketMessage: SocketMessage.AddressTokenTransfer['handler'] = (payload) => { const handleNewSocketMessage: SocketMessage.AddressTokenTransfer['handler'] = (payload) => {
setSocketAlert(''); setSocketAlert('');
...@@ -133,7 +150,7 @@ const AddressTokenTransfers = ({ scrollRef }: {scrollRef?: React.RefObject<HTMLD ...@@ -133,7 +150,7 @@ const AddressTokenTransfers = ({ scrollRef }: {scrollRef?: React.RefObject<HTMLD
topic: `addresses:${ (router.query.id as string).toLowerCase() }`, topic: `addresses:${ (router.query.id as string).toLowerCase() }`,
onSocketClose: handleSocketClose, onSocketClose: handleSocketClose,
onSocketError: handleSocketError, onSocketError: handleSocketError,
isDisabled: pagination.page !== 1, isDisabled: pagination.page !== 1 || Boolean(tokenFilter),
}); });
useSocketMessage({ useSocketMessage({
...@@ -143,7 +160,7 @@ const AddressTokenTransfers = ({ scrollRef }: {scrollRef?: React.RefObject<HTMLD ...@@ -143,7 +160,7 @@ 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 = !numActiveFilters && !data?.items.length; const isActionBarHidden = !tokenFilter && !numActiveFilters && !data?.items.length;
const content = (() => { const content = (() => {
if (isLoading) { if (isLoading) {
...@@ -181,13 +198,13 @@ const AddressTokenTransfers = ({ scrollRef }: {scrollRef?: React.RefObject<HTMLD ...@@ -181,13 +198,13 @@ const AddressTokenTransfers = ({ scrollRef }: {scrollRef?: React.RefObject<HTMLD
showTxInfo showTxInfo
top={ 80 } top={ 80 }
enableTimeIncrement enableTimeIncrement
showSocketInfo={ pagination.page === 1 } showSocketInfo={ pagination.page === 1 && !tokenFilter }
socketInfoAlert={ socketAlert } socketInfoAlert={ socketAlert }
socketInfoNum={ newItemsCount } socketInfoNum={ newItemsCount }
/> />
</Hide> </Hide>
<Show below="lg"> <Show below="lg">
{ pagination.page === 1 && ( { pagination.page === 1 && !tokenFilter && (
<SocketNewItemsNotice <SocketNewItemsNotice
url={ window.location.href } url={ window.location.href }
num={ newItemsCount } num={ newItemsCount }
...@@ -207,18 +224,43 @@ const AddressTokenTransfers = ({ scrollRef }: {scrollRef?: React.RefObject<HTMLD ...@@ -207,18 +224,43 @@ const AddressTokenTransfers = ({ scrollRef }: {scrollRef?: React.RefObject<HTMLD
); );
})(); })();
const tokenFilterComponent = tokenFilter && (
<Flex alignItems="center" py={ 1 } flexWrap="wrap" mb={{ base: isPaginationVisible ? 6 : 3, lg: 0 }}>
Filtered by token
<TokenLogo hash={ tokenFilter } boxSize={ 6 } mx={ 2 }/>
{ isMobile ? tokenFilter.slice(0, 4) + '...' + tokenFilter.slice(-4) : tokenFilter }
<Tooltip label="Reset filter">
<Flex>
<Icon
as={ crossIcon }
boxSize={ 6 }
ml={ 1 }
color={ resetTokenIconColor }
cursor="pointer"
_hover={{ color: resetTokenIconHoverColor }}
onClick={ resetTokenFilter }
/>
</Flex>
</Tooltip>
</Flex>
);
return ( return (
<> <>
{ isMobile && tokenFilterComponent }
{ !isActionBarHidden && ( { !isActionBarHidden && (
<ActionBar mt={ -6 }> <ActionBar mt={ -6 }>
<TokenTransferFilter { !isMobile && tokenFilterComponent }
defaultTypeFilters={ filters.type } { !tokenFilter && (
onTypeFilterChange={ handleTypeFilterChange } <TokenTransferFilter
appliedFiltersNum={ numActiveFilters } defaultTypeFilters={ filters.type }
withAddressFilter onTypeFilterChange={ handleTypeFilterChange }
onAddressFilterChange={ handleAddressFilterChange } appliedFiltersNum={ numActiveFilters }
defaultAddressFilter={ filters.filter } withAddressFilter
/> onAddressFilterChange={ handleAddressFilterChange }
defaultAddressFilter={ filters.filter }
/>
) }
{ currentAddress && <AddressCsvExportLink address={ currentAddress } type="token-transfers" ml={{ base: 2, lg: 'auto' }}/> } { currentAddress && <AddressCsvExportLink address={ currentAddress } type="token-transfers" ml={{ base: 2, lg: 'auto' }}/> }
{ isPaginationVisible && <Pagination ml={{ base: 'auto', lg: 8 }} { ...pagination }/> } { isPaginationVisible && <Pagination ml={{ base: 'auto', lg: 8 }} { ...pagination }/> }
</ActionBar> </ActionBar>
......
...@@ -10,7 +10,7 @@ import React from 'react'; ...@@ -10,7 +10,7 @@ import React from 'react';
import type { AddressFromToFilter } from 'types/api/address'; import type { AddressFromToFilter } from 'types/api/address';
import FilterButton from 'ui/shared/FilterButton'; import FilterButton from 'ui/shared/filters/FilterButton';
interface Props { interface Props {
isActive: boolean; isActive: boolean;
......
...@@ -49,11 +49,11 @@ const AddressCoinBalanceHistory = ({ query }: Props) => { ...@@ -49,11 +49,11 @@ const AddressCoinBalanceHistory = ({ query }: Props) => {
<Table variant="simple" size="sm"> <Table variant="simple" size="sm">
<Thead top={ 80 }> <Thead top={ 80 }>
<Tr> <Tr>
<Th width="25%">Block</Th> <Th width="20%">Block</Th>
<Th width="25%">Txn</Th> <Th width="20%">Txn</Th>
<Th width="25%">Age</Th> <Th width="20%">Age</Th>
<Th width="25%" isNumeric pr={ 1 }/> <Th width="20%" isNumeric pr={ 1 }>Balance { appConfig.network.currency.symbol }</Th>
<Th width="120px" isNumeric>Balance { appConfig.network.currency.symbol }</Th> <Th width="20%" isNumeric>Delta</Th>
</Tr> </Tr>
</Thead> </Thead>
<Tbody> <Tbody>
......
...@@ -30,7 +30,7 @@ const AddressCoinBalanceListItem = (props: Props) => { ...@@ -30,7 +30,7 @@ const AddressCoinBalanceListItem = (props: Props) => {
<StatHelpText display="flex" mb={ 0 } alignItems="center"> <StatHelpText display="flex" mb={ 0 } alignItems="center">
<StatArrow type={ isPositiveDelta ? 'increase' : 'decrease' }/> <StatArrow type={ isPositiveDelta ? 'increase' : 'decrease' }/>
<Text as="span" color={ isPositiveDelta ? 'green.500' : 'red.500' } fontWeight={ 600 }> <Text as="span" color={ isPositiveDelta ? 'green.500' : 'red.500' } fontWeight={ 600 }>
{ deltaBn.toFixed(8) } { deltaBn.toFormat() }
</Text> </Text>
</StatHelpText> </StatHelpText>
</Stat> </Stat>
......
...@@ -46,7 +46,7 @@ const AddressCoinBalanceTableItem = (props: Props) => { ...@@ -46,7 +46,7 @@ const AddressCoinBalanceTableItem = (props: Props) => {
<StatHelpText display="flex" mb={ 0 } alignItems="center"> <StatHelpText display="flex" mb={ 0 } alignItems="center">
<StatArrow type={ isPositiveDelta ? 'increase' : 'decrease' }/> <StatArrow type={ isPositiveDelta ? 'increase' : 'decrease' }/>
<Text as="span" color={ isPositiveDelta ? 'green.500' : 'red.500' } fontWeight={ 600 }> <Text as="span" color={ isPositiveDelta ? 'green.500' : 'red.500' } fontWeight={ 600 }>
{ deltaBn.toFixed(8) } { deltaBn.toFormat() }
</Text> </Text>
</StatHelpText> </StatHelpText>
</Stat> </Stat>
......
import { test, expect } from '@playwright/experimental-ct-react';
import React from 'react';
import * as contractMock from 'mocks/contract/info';
import TestApp from 'playwright/TestApp';
import buildApiUrl from 'playwright/utils/buildApiUrl';
import ContractCode from './ContractCode';
const addressHash = 'hash';
const CONTRACT_API_URL = buildApiUrl('contract', { id: addressHash });
const hooksConfig = {
router: {
query: { id: addressHash },
},
};
test('verified with changed byte code +@mobile +@dark-mode', async({ mount, page }) => {
await page.route(CONTRACT_API_URL, (route) => route.fulfill({
status: 200,
body: JSON.stringify(contractMock.withChangedByteCode),
}));
const component = await mount(
<TestApp>
<ContractCode/>
</TestApp>,
{ hooksConfig },
);
await expect(component).toHaveScreenshot();
});
test('verified with multiple sources', async({ mount, page }) => {
await page.route(CONTRACT_API_URL, (route) => route.fulfill({
status: 200,
body: JSON.stringify(contractMock.withMultiplePaths),
}));
await mount(
<TestApp>
<ContractCode/>
</TestApp>,
{ hooksConfig },
);
const section = page.locator('section', { hasText: 'Contract source code' });
await expect(section).toHaveScreenshot();
});
test('verified via sourcify', async({ mount, page }) => {
await page.route(CONTRACT_API_URL, (route) => route.fulfill({
status: 200,
body: JSON.stringify(contractMock.verifiedViaSourcify),
}));
await mount(
<TestApp>
<ContractCode/>
</TestApp>,
{ hooksConfig },
);
await expect(page).toHaveScreenshot({ clip: { x: 0, y: 0, width: 1200, height: 110 } });
});
test('self destructed', async({ mount, page }) => {
await page.route(CONTRACT_API_URL, (route) => route.fulfill({
status: 200,
body: JSON.stringify(contractMock.selfDestructed),
}));
await mount(
<TestApp>
<ContractCode/>
</TestApp>,
{ hooksConfig },
);
const section = page.locator('section', { hasText: 'Contract creation code' });
await expect(section).toHaveScreenshot();
});
test('with twin address alert +@mobile', async({ mount, page }) => {
await page.route(CONTRACT_API_URL, (route) => route.fulfill({
status: 200,
body: JSON.stringify(contractMock.withTwinAddress),
}));
const component = await mount(
<TestApp>
<ContractCode/>
</TestApp>,
{ hooksConfig },
);
await expect(component.getByRole('alert')).toHaveScreenshot();
});
test('with proxy address alert +@mobile', async({ mount, page }) => {
await page.route(CONTRACT_API_URL, (route) => route.fulfill({
status: 200,
body: JSON.stringify(contractMock.withProxyAddress),
}));
const component = await mount(
<TestApp>
<ContractCode/>
</TestApp>,
{ hooksConfig },
);
await expect(component.getByRole('alert')).toHaveScreenshot();
});
test('non verified', async({ mount, page }) => {
await page.route(CONTRACT_API_URL, (route) => route.fulfill({
status: 200,
body: JSON.stringify(contractMock.nonVerified),
}));
const component = await mount(
<TestApp>
<ContractCode/>
</TestApp>,
{ hooksConfig },
);
await expect(component).toHaveScreenshot();
});
import { Flex, Skeleton, Button, Grid, GridItem, Text } from '@chakra-ui/react'; import { Flex, Skeleton, Button, Grid, GridItem, Text, Alert, Link, chakra, Box } from '@chakra-ui/react';
import dynamic from 'next/dynamic';
import { useRouter } from 'next/router'; import { useRouter } from 'next/router';
import React from 'react'; import React from 'react';
import useApiQuery from 'lib/api/useApiQuery'; import useApiQuery from 'lib/api/useApiQuery';
import link from 'lib/link/link'; import link from 'lib/link/link';
import Address from 'ui/shared/address/Address';
import AddressIcon from 'ui/shared/address/AddressIcon';
import AddressLink from 'ui/shared/address/AddressLink';
import DataFetchAlert from 'ui/shared/DataFetchAlert'; import DataFetchAlert from 'ui/shared/DataFetchAlert';
import ExternalLink from 'ui/shared/ExternalLink';
import RawDataSnippet from 'ui/shared/RawDataSnippet'; import RawDataSnippet from 'ui/shared/RawDataSnippet';
const DynamicContractSourceCode = dynamic( import ContractSourceCode from './ContractSourceCode';
() => import('./ContractSourceCode'),
{ ssr: false },
);
const InfoItem = ({ label, value }: { label: string; value: string }) => ( const InfoItem = ({ label, value }: { label: string; value: string }) => (
<GridItem display="flex" columnGap={ 6 }> <GridItem display="flex" columnGap={ 6 }>
...@@ -23,10 +23,12 @@ const InfoItem = ({ label, value }: { label: string; value: string }) => ( ...@@ -23,10 +23,12 @@ const InfoItem = ({ label, value }: { label: string; value: string }) => (
const ContractCode = () => { const ContractCode = () => {
const router = useRouter(); const router = useRouter();
const addressHash = router.query.id?.toString();
const { data, isLoading, isError } = useApiQuery('contract', { const { data, isLoading, isError } = useApiQuery('contract', {
pathParams: { id: router.query.id?.toString() }, pathParams: { id: addressHash },
queryOptions: { queryOptions: {
enabled: Boolean(router.query.id), enabled: Boolean(addressHash),
refetchOnMount: false,
}, },
}); });
...@@ -57,14 +59,92 @@ const ContractCode = () => { ...@@ -57,14 +59,92 @@ const ContractCode = () => {
ml="auto" ml="auto"
mr={ 3 } mr={ 3 }
as="a" as="a"
href={ link('address_contract_verification', { id: router.query.id?.toString() }) } href={ link('address_contract_verification', { id: addressHash }) }
> >
Verify & publish Verify & publish
</Button> </Button>
); );
const constructorArgs = (() => {
if (!data.decoded_constructor_args) {
return data.constructor_args;
}
const decoded = data.decoded_constructor_args
.map(([ value, { name, type } ], index) => {
const valueEl = type === 'address' ? <Link href={ link('address_index', { id: value }) }>{ value }</Link> : <span>{ value }</span>;
return (
<Box key={ index }>
<span>Arg [{ index }] { name || '' } ({ type }): </span>
{ valueEl }
</Box>
);
});
return (
<>
<span>{ data.constructor_args }</span>
<br/><br/>
{ decoded }
</>
);
})();
const externalLibraries = (() => {
if (!data.external_libraries || data.external_libraries.length === 0) {
return null;
}
return data.external_libraries.map((item) => (
<Box key={ item.address_hash }>
<chakra.span fontWeight={ 500 }>{ item.name }: </chakra.span>
<Link href={ link('address_index', { id: item.address_hash }, { tab: 'contract' }) }>{ item.address_hash }</Link>
</Box>
));
})();
return ( return (
<> <>
<Flex flexDir="column" rowGap={ 2 } mb={ 6 } _empty={{ display: 'none' }}>
{ data.is_verified && <Alert status="success">Contract Source Code Verified (Exact Match)</Alert> }
{ data.is_verified_via_sourcify && (
<Alert status="warning" whiteSpace="pre-wrap" flexWrap="wrap">
<span>This contract has been { data.is_partially_verified ? 'partially ' : '' }verified via Sourcify. </span>
{ data.sourcify_repo_url && <ExternalLink href={ data.sourcify_repo_url } title="View contract in Sourcify repository" fontSize="md"/> }
</Alert>
) }
{ data.is_changed_bytecode && (
<Alert status="warning">
Warning! Contract bytecode has been changed and does not match the verified one. Therefore, interaction with this smart contract may be risky.
</Alert>
) }
{ !data.is_verified && data.verified_twin_address_hash && !data.minimal_proxy_address_hash && (
<Alert status="warning" whiteSpace="pre-wrap" flexWrap="wrap">
<span>Contract is not verified. However, we found a verified contract with the same bytecode in Blockscout DB </span>
<Address>
<AddressIcon address={{ hash: data.verified_twin_address_hash, is_contract: true, implementation_name: null }}/>
<AddressLink type="address" hash={ data.verified_twin_address_hash } truncation="constant" ml={ 2 }/>
</Address>
<chakra.span mt={ 1 }>All functions displayed below are from ABI of that contract. In order to verify current contract, proceed with </chakra.span>
<Link href={ link('address_contract_verification', { id: data.verified_twin_address_hash }) }>Verify & Publish</Link>
<span> page</span>
</Alert>
) }
{ data.minimal_proxy_address_hash && (
<Alert status="warning" flexWrap="wrap" whiteSpace="pre-wrap">
<span>Minimal Proxy Contract for </span>
<Address>
<AddressIcon address={{ hash: data.minimal_proxy_address_hash, is_contract: true, implementation_name: null }}/>
<AddressLink type="address" hash={ data.minimal_proxy_address_hash } truncation="constant" ml={ 2 }/>
</Address>
<span>. </span>
<Box>
<Link href="https://eips.ethereum.org/EIPS/eip-1167">EIP-1167</Link>
<span> - minimal bytecode implementation that delegates all calls to a known address</span>
</Box>
</Alert>
) }
</Flex>
{ data.is_verified && ( { data.is_verified && (
<Grid templateColumns={{ base: '1fr', lg: '1fr 1fr' }} rowGap={ 4 } columnGap={ 6 } mb={ 8 }> <Grid templateColumns={{ base: '1fr', lg: '1fr 1fr' }} rowGap={ 4 } columnGap={ 6 } mb={ 8 }>
{ data.name && <InfoItem label="Contract name" value={ data.name }/> } { data.name && <InfoItem label="Contract name" value={ data.name }/> }
...@@ -76,18 +156,35 @@ const ContractCode = () => { ...@@ -76,18 +156,35 @@ const ContractCode = () => {
</Grid> </Grid>
) } ) }
<Flex flexDir="column" rowGap={ 6 }> <Flex flexDir="column" rowGap={ 6 }>
{ constructorArgs && (
<RawDataSnippet
data={ constructorArgs }
title="Constructor Arguments"
textareaMaxHeight="200px"
/>
) }
{ data.source_code && ( { data.source_code && (
<DynamicContractSourceCode <ContractSourceCode
data={ data.source_code } data={ data.source_code }
hasSol2Yml={ Boolean(data.can_be_visualized_via_sol2uml) } hasSol2Yml={ Boolean(data.can_be_visualized_via_sol2uml) }
address={ router.query.id?.toString() } address={ addressHash }
isViper={ Boolean(data.is_vyper_contract) }
filePath={ data.file_path }
additionalSource={ data.additional_sources }
/>
) }
{ Boolean(data.compiler_settings) && (
<RawDataSnippet
data={ JSON.stringify(data.compiler_settings) }
title="Compiler Settings"
textareaMaxHeight="200px"
/> />
) } ) }
{ data.abi && ( { data.abi && (
<RawDataSnippet <RawDataSnippet
data={ JSON.stringify(data.abi) } data={ JSON.stringify(data.abi) }
title="Contract ABI" title="Contract ABI"
textareaMinHeight="200px" textareaMaxHeight="200px"
/> />
) } ) }
{ data.creation_bytecode && ( { data.creation_bytecode && (
...@@ -95,12 +192,27 @@ const ContractCode = () => { ...@@ -95,12 +192,27 @@ const ContractCode = () => {
data={ data.creation_bytecode } data={ data.creation_bytecode }
title="Contract creation code" title="Contract creation code"
rightSlot={ data.is_verified ? null : verificationButton } rightSlot={ data.is_verified ? null : verificationButton }
beforeSlot={ data.is_self_destructed ? (
<Alert status="info" whiteSpace="pre-wrap" mb={ 3 }>
Contracts that self destruct in their constructors have no contract code published and cannot be verified.
Displaying the init data provided of the creating transaction.
</Alert>
) : null }
textareaMaxHeight="200px"
/> />
) } ) }
{ data.deployed_bytecode && ( { data.deployed_bytecode && (
<RawDataSnippet <RawDataSnippet
data={ data.deployed_bytecode } data={ data.deployed_bytecode }
title="Deployed ByteCode" title="Deployed ByteCode"
textareaMaxHeight="200px"
/>
) }
{ externalLibraries && (
<RawDataSnippet
data={ externalLibraries }
title="External Libraries"
textareaMaxHeight="200px"
/> />
) } ) }
</Flex> </Flex>
......
import { Alert, Button, Flex } from '@chakra-ui/react';
import { useWeb3Modal } from '@web3modal/react';
import React from 'react';
import { useAccount, useDisconnect } from 'wagmi';
import useIsMobile from 'lib/hooks/useIsMobile';
import AddressIcon from 'ui/shared/address/AddressIcon';
import AddressLink from 'ui/shared/address/AddressLink';
const ContractConnectWallet = () => {
const { open } = useWeb3Modal();
const { address, isDisconnected } = useAccount();
const { disconnect } = useDisconnect();
const isMobile = useIsMobile();
const handleConnect = React.useCallback(() => {
open();
}, [ open ]);
const handleDisconnect = React.useCallback(() => {
disconnect();
}, [ disconnect ]);
const content = (() => {
if (isDisconnected || !address) {
return (
<>
<span>Disconnected</span>
<Button ml={ 3 } onClick={ handleConnect } size="sm" variant="outline">Connect wallet</Button>
</>
);
}
return (
<Flex columnGap={ 3 } rowGap={ 3 } alignItems={{ base: 'flex-start', lg: 'center' }} flexDir={{ base: 'column', lg: 'row' }}>
<Flex alignItems="center">
<span>Connected to </span>
<AddressIcon address={{ hash: address, is_contract: false, implementation_name: null }} mx={ 2 }/>
<AddressLink type="address" fontWeight={ 600 } hash={ address } truncation={ isMobile ? 'constant' : 'dynamic' }/>
</Flex>
<Button onClick={ handleDisconnect } size="sm" variant="outline">Disconnect</Button>
</Flex>
);
})();
return <Alert mb={ 6 } status={ address ? 'success' : 'warning' }>{ content }</Alert>;
};
export default ContractConnectWallet;
import { Alert } from '@chakra-ui/react';
import React from 'react';
const ContractCustomAbiAlert = () => {
return (
<Alert status="warning" mb={ 4 }>
Note: Contract with custom ABI is only meant for debugging purpose and it is the user’s responsibility to ensure that the provided ABI
matches the contract, otherwise errors may occur or results returned may be incorrect.
Blockscout is not responsible for any losses that arise from the use of Read & Write contract.
</Alert>
);
};
export default React.memo(ContractCustomAbiAlert);
import { useQueryClient } from '@tanstack/react-query';
import React from 'react';
import type { Address as TAddress } from 'types/api/address';
import { getResourceKey } from 'lib/api/useApiQuery';
import Address from 'ui/shared/address/Address';
import AddressLink from 'ui/shared/address/AddressLink';
interface Props {
hash: string | undefined;
}
const ContractImplementationAddress = ({ hash }: Props) => {
const queryClient = useQueryClient();
const data = queryClient.getQueryData<TAddress>(getResourceKey('address', {
pathParams: { id: hash },
}));
if (!data?.implementation_address) {
return null;
}
return (
<Address whiteSpace="pre-wrap" flexWrap="wrap" mb={ 6 }>
<span>Implementation address: </span>
<AddressLink type="address" hash={ data.implementation_address }/>
</Address>
);
};
export default React.memo(ContractImplementationAddress);
import { Box, Button, chakra, Flex, Icon, Text, useColorModeValue } from '@chakra-ui/react'; import { Box, Button, chakra, Flex, Icon, Text } from '@chakra-ui/react';
import _fromPairs from 'lodash/fromPairs'; import _fromPairs from 'lodash/fromPairs';
import React from 'react'; import React from 'react';
import type { SubmitHandler } from 'react-hook-form'; import type { SubmitHandler } from 'react-hook-form';
import { useForm } from 'react-hook-form'; import { useForm } from 'react-hook-form';
import type { MethodFormFields } from './types'; import type { MethodFormFields, ContractMethodCallResult } from './types';
import type { SmartContractMethodInput, SmartContractMethod } from 'types/api/contract'; import type { SmartContractMethodInput, SmartContractMethod } from 'types/api/contract';
import appConfig from 'configs/app/config'; import appConfig from 'configs/app/config';
...@@ -12,9 +12,16 @@ import arrowIcon from 'icons/arrows/down-right.svg'; ...@@ -12,9 +12,16 @@ import arrowIcon from 'icons/arrows/down-right.svg';
import ContractMethodField from './ContractMethodField'; import ContractMethodField from './ContractMethodField';
interface ResultComponentProps<T extends SmartContractMethod> {
item: T;
result: ContractMethodCallResult<T>;
onSettle: () => void;
}
interface Props<T extends SmartContractMethod> { interface Props<T extends SmartContractMethod> {
data: T; data: T;
caller: (data: T, args: Array<string>) => Promise<Array<Array<string>>>; onSubmit: (data: T, args: Array<string | Array<string>>) => Promise<ContractMethodCallResult<T>>;
ResultComponent: (props: ResultComponentProps<T>) => JSX.Element | null;
isWrite?: boolean; isWrite?: boolean;
} }
...@@ -36,32 +43,61 @@ const sortFields = (data: Array<SmartContractMethodInput>) => ([ a ]: [string, s ...@@ -36,32 +43,61 @@ const sortFields = (data: Array<SmartContractMethodInput>) => ([ a ]: [string, s
return 0; return 0;
}; };
const ContractMethodCallable = <T extends SmartContractMethod>({ data, caller, isWrite }: Props<T>) => { const castFieldValue = (data: Array<SmartContractMethodInput>) => ([ key, value ]: [ string, string ], index: number) => {
if (data[index].type.includes('[]')) {
return [ key, parseArrayValue(value) ];
}
return [ key, value ];
};
const parseArrayValue = (value: string) => value.replace(/(\[|\])|\s/g, '').split(',');
const ContractMethodCallable = <T extends SmartContractMethod>({ data, onSubmit, ResultComponent, isWrite }: Props<T>) => {
const [ result, setResult ] = React.useState<ContractMethodCallResult<T>>();
const [ isLoading, setLoading ] = React.useState(false);
const inputs = React.useMemo(() => { const inputs = React.useMemo(() => {
return data.payable && (!('inputs' in data) || data.inputs.length === 0) ? [ { return [
name: 'value', ...('inputs' in data ? data.inputs : []),
type: appConfig.network.currency.symbol, ...(data.stateMutability === 'payable' ? [ {
internalType: appConfig.network.currency.symbol, name: 'value',
} as SmartContractMethodInput ] : data.inputs; type: appConfig.network.currency.symbol,
internalType: appConfig.network.currency.symbol,
} as SmartContractMethodInput ] : []),
];
}, [ data ]); }, [ data ]);
const { control, handleSubmit, setValue } = useForm<MethodFormFields>({ const { control, handleSubmit, setValue } = useForm<MethodFormFields>({
defaultValues: _fromPairs(inputs.map(({ name }, index) => [ getFieldName(name, index), '' ])), defaultValues: _fromPairs(inputs.map(({ name }, index) => [ getFieldName(name, index), '' ])),
}); });
const [ result, setResult ] = React.useState<Array<Array<string>>>([ ]);
const onSubmit: SubmitHandler<MethodFormFields> = React.useCallback(async(formData) => { const handleTxSettle = React.useCallback(() => {
setLoading(false);
}, []);
const handleFormChange = React.useCallback(() => {
result && setResult(undefined);
}, [ result ]);
const onFormSubmit: SubmitHandler<MethodFormFields> = React.useCallback(async(formData) => {
const args = Object.entries(formData) const args = Object.entries(formData)
.sort(sortFields(inputs)) .sort(sortFields(inputs))
.map(castFieldValue(inputs))
.map(([ , value ]) => value); .map(([ , value ]) => value);
const result = await caller(data, args); setResult(undefined);
setResult(result); setLoading(true);
}, [ caller, data, inputs ]); onSubmit(data, args)
.then((result) => {
const resultBgColor = useColorModeValue('blackAlpha.50', 'whiteAlpha.50'); setResult(result);
})
.catch((error) => {
setResult(error?.error || error?.data || (error?.reason && { message: error.reason }) || error);
setLoading(false);
});
}, [ onSubmit, data, inputs ]);
return ( return (
<Box> <Box>
...@@ -72,8 +108,9 @@ const ContractMethodCallable = <T extends SmartContractMethod>({ data, caller, i ...@@ -72,8 +108,9 @@ const ContractMethodCallable = <T extends SmartContractMethod>({ data, caller, i
flexDir={{ base: 'column', lg: 'row' }} flexDir={{ base: 'column', lg: 'row' }}
rowGap={ 2 } rowGap={ 2 }
alignItems={{ base: 'flex-start', lg: 'center' }} alignItems={{ base: 'flex-start', lg: 'center' }}
onSubmit={ handleSubmit(onSubmit) } onSubmit={ handleSubmit(onFormSubmit) }
flexWrap="wrap" flexWrap="wrap"
onChange={ handleFormChange }
> >
{ inputs.map(({ type, name }, index) => { { inputs.map(({ type, name }, index) => {
const fieldName = getFieldName(name, index); const fieldName = getFieldName(name, index);
...@@ -84,10 +121,14 @@ const ContractMethodCallable = <T extends SmartContractMethod>({ data, caller, i ...@@ -84,10 +121,14 @@ const ContractMethodCallable = <T extends SmartContractMethod>({ data, caller, i
placeholder={ `${ name }(${ type })` } placeholder={ `${ name }(${ type })` }
control={ control } control={ control }
setValue={ setValue } setValue={ setValue }
isDisabled={ isLoading }
onClear={ handleFormChange }
/> />
); );
}) } }) }
<Button <Button
isLoading={ isLoading }
loadingText={ isWrite ? 'Write' : 'Query' }
variant="outline" variant="outline"
size="sm" size="sm"
flexShrink={ 0 } flexShrink={ 0 }
...@@ -102,18 +143,7 @@ const ContractMethodCallable = <T extends SmartContractMethod>({ data, caller, i ...@@ -102,18 +143,7 @@ const ContractMethodCallable = <T extends SmartContractMethod>({ data, caller, i
<Text>{ data.outputs.map(({ type }) => type).join(', ') }</Text> <Text>{ data.outputs.map(({ type }) => type).join(', ') }</Text>
</Flex> </Flex>
) } ) }
{ result.length > 0 && ( { result && <ResultComponent item={ data } result={ result } onSettle={ handleTxSettle }/> }
<Box mt={ 3 } p={ 4 } borderRadius="md" bgColor={ resultBgColor } fontSize="sm">
<p>
[ <chakra.span fontWeight={ 600 }>{ 'name' in data ? data.name : '' }</chakra.span> method response ]
</p>
<p>[</p>
{ result.map(([ key, value ], index) => (
<chakra.p key={ index } whiteSpace="break-spaces" wordBreak="break-all"> { key }: { value }</chakra.p>
)) }
<p>]</p>
</Box>
) }
</Box> </Box>
); );
}; };
......
...@@ -7,6 +7,7 @@ import type { SmartContractMethodOutput } from 'types/api/contract'; ...@@ -7,6 +7,7 @@ import type { SmartContractMethodOutput } from 'types/api/contract';
import appConfig from 'configs/app/config'; import appConfig from 'configs/app/config';
import { WEI } from 'lib/consts'; import { WEI } from 'lib/consts';
import AddressLink from 'ui/shared/address/AddressLink';
interface Props { interface Props {
data: SmartContractMethodOutput; data: SmartContractMethodOutput;
...@@ -31,9 +32,17 @@ const ContractMethodStatic = ({ data }: Props) => { ...@@ -31,9 +32,17 @@ const ContractMethodStatic = ({ data }: Props) => {
} }
}, [ data.value ]); }, [ data.value ]);
const content = (() => {
if (data.type === 'address' && data.value) {
return <AddressLink type="address" hash={ data.value }/>;
}
return <chakra.span wordBreak="break-all">({ data.type }): { value }</chakra.span>;
})();
return ( return (
<Flex flexDir={{ base: 'column', lg: 'row' }} columnGap={ 2 } rowGap={ 2 }> <Flex flexDir={{ base: 'column', lg: 'row' }} columnGap={ 2 } rowGap={ 2 }>
<chakra.span wordBreak="break-all">({ data.type }): { value }</chakra.span> { content }
{ isBigInt && <Checkbox onChange={ handleCheckboxChange }>{ label }</Checkbox> } { isBigInt && <Checkbox onChange={ handleCheckboxChange }>{ label }</Checkbox> }
</Flex> </Flex>
); );
......
...@@ -12,19 +12,28 @@ interface Props { ...@@ -12,19 +12,28 @@ interface Props {
setValue: UseFormSetValue<MethodFormFields>; setValue: UseFormSetValue<MethodFormFields>;
placeholder: string; placeholder: string;
name: string; name: string;
isDisabled: boolean;
onClear: () => void;
} }
const ContractMethodField = ({ control, name, placeholder, setValue }: Props) => { const ContractMethodField = ({ control, name, placeholder, setValue, isDisabled, onClear }: Props) => {
const ref = React.useRef<HTMLInputElement>(null); const ref = React.useRef<HTMLInputElement>(null);
const handleClear = React.useCallback(() => { const handleClear = React.useCallback(() => {
setValue(name, ''); setValue(name, '');
onClear();
ref.current?.focus(); ref.current?.focus();
}, [ name, setValue ]); }, [ name, onClear, setValue ]);
const renderInput = React.useCallback(({ field }: { field: ControllerRenderProps<MethodFormFields> }) => { const renderInput = React.useCallback(({ field }: { field: ControllerRenderProps<MethodFormFields> }) => {
return ( return (
<FormControl id={ name } maxW={{ base: '100%', lg: 'calc((100% - 24px) / 3)' }}> <FormControl
id={ name }
flexBasis={{ base: '100%', lg: 'calc((100% - 24px) / 3 - 65px)' }}
w={{ base: '100%', lg: 'auto' }}
flexGrow={ 1 }
isDisabled={ isDisabled
}>
<InputGroup size="xs"> <InputGroup size="xs">
<Input <Input
{ ...field } { ...field }
...@@ -33,13 +42,13 @@ const ContractMethodField = ({ control, name, placeholder, setValue }: Props) => ...@@ -33,13 +42,13 @@ const ContractMethodField = ({ control, name, placeholder, setValue }: Props) =>
/> />
{ field.value && ( { field.value && (
<InputRightElement> <InputRightElement>
<InputClearButton onClick={ handleClear }/> <InputClearButton onClick={ handleClear } isDisabled={ isDisabled }/>
</InputRightElement> </InputRightElement>
) } ) }
</InputGroup> </InputGroup>
</FormControl> </FormControl>
); );
}, [ handleClear, name, placeholder ]); }, [ handleClear, isDisabled, name, placeholder ]);
return ( return (
<Controller <Controller
......
...@@ -39,11 +39,11 @@ const ContractMethodsAccordion = <T extends SmartContractMethod>({ data, renderC ...@@ -39,11 +39,11 @@ const ContractMethodsAccordion = <T extends SmartContractMethod>({ data, renderC
<Accordion allowMultiple position="relative" onChange={ handleAccordionStateChange } index={ expandedSections }> <Accordion allowMultiple position="relative" onChange={ handleAccordionStateChange } index={ expandedSections }>
{ data.map((item, index) => { { data.map((item, index) => {
return ( return (
<AccordionItem key={ index } as="section"> <AccordionItem key={ index } as="section" _first={{ borderTopWidth: '0' }}>
<h2> <h2>
<AccordionButton px={ 0 } py={ 3 } _hover={{ bgColor: 'inherit' }}> <AccordionButton px={ 0 } py={ 3 } _hover={{ bgColor: 'inherit' }}>
<Box as="span" fontFamily="heading" fontWeight={ 500 } fontSize="lg" mr={ 1 }> <Box as="span" fontFamily="heading" fontWeight={ 500 } fontSize="lg" mr={ 1 }>
{ index + 1 }. { item.type === 'fallback' ? 'fallback' : item.name } { index + 1 }. { item.type === 'fallback' || item.type === 'receive' ? item.type : item.name }
</Box> </Box>
{ item.type === 'fallback' && ( { item.type === 'fallback' && (
<Tooltip <Tooltip
...@@ -58,6 +58,21 @@ const ContractMethodsAccordion = <T extends SmartContractMethod>({ data, renderC ...@@ -58,6 +58,21 @@ const ContractMethodsAccordion = <T extends SmartContractMethod>({ data, renderC
</Box> </Box>
</Tooltip> </Tooltip>
) } ) }
{ item.type === 'receive' && (
<Tooltip
label={ `The receive function is executed on a call to the contract with empty calldata.
This is the function that is executed on plain Ether transfers (e.g. via .send() or .transfer()).
If no such function exists, but a payable fallback function exists, the fallback function will be called on a plain Ether transfer.
If neither a receive Ether nor a payable fallback function is present,
the contract cannot receive Ether through regular transactions and throws an exception.` }
placement="top"
maxW="320px"
>
<Box cursor="pointer" display="inherit">
<Icon as={ infoIcon } boxSize={ 5 }/>
</Box>
</Tooltip>
) }
<AccordionIcon/> <AccordionIcon/>
</AccordionButton> </AccordionButton>
</h2> </h2>
......
import { test, expect } from '@playwright/experimental-ct-react';
import React from 'react';
import * as contractMethodsMock from 'mocks/contract/methods';
import TestApp from 'playwright/TestApp';
import buildApiUrl from 'playwright/utils/buildApiUrl';
import ContractRead from './ContractRead';
const addressHash = 'hash';
const CONTRACT_READ_METHODS_API_URL = buildApiUrl('contract_methods_read', { id: addressHash }) + '?is_custom_abi=false';
const CONTRACT_QUERY_METHOD_API_URL = buildApiUrl('contract_method_query', { id: addressHash });
const hooksConfig = {
router: {
query: { id: addressHash },
},
};
test('base view +@mobile +@dark-mode', async({ mount, page }) => {
await page.route(CONTRACT_READ_METHODS_API_URL, (route) => route.fulfill({
status: 200,
body: JSON.stringify(contractMethodsMock.read),
}));
await page.route(CONTRACT_QUERY_METHOD_API_URL, (route) => route.fulfill({
status: 200,
body: JSON.stringify(contractMethodsMock.readResultSuccess),
}));
const component = await mount(
<TestApp>
<ContractRead/>
</TestApp>,
{ hooksConfig },
);
await component.getByText(/expand all/i).click();
await expect(component).toHaveScreenshot();
await component.getByPlaceholder(/address/i).type('address-hash');
await component.getByText(/query/i).click();
await component.getByText(/wei/i).click();
await expect(component).toHaveScreenshot();
});
test('error result', async({ mount, page }) => {
await page.route(CONTRACT_READ_METHODS_API_URL, (route) => route.fulfill({
status: 200,
body: JSON.stringify(contractMethodsMock.read),
}));
await page.route(CONTRACT_QUERY_METHOD_API_URL, (route) => route.fulfill({
status: 200,
body: JSON.stringify(contractMethodsMock.readResultError),
}));
const component = await mount(
<TestApp>
<ContractRead/>
</TestApp>,
{ hooksConfig },
);
await component.getByText(/expand all/i).click();
await component.getByPlaceholder(/address/i).type('address-hash');
await component.getByText(/query/i).click();
const section = page.locator('section', { hasText: 'balanceOf' });
await expect(section).toHaveScreenshot();
});
import { Flex } from '@chakra-ui/react'; import { Alert, Flex } from '@chakra-ui/react';
import { useRouter } from 'next/router'; import { useRouter } from 'next/router';
import React from 'react'; import React from 'react';
import { useAccount } from 'wagmi';
import type { SmartContractReadMethod } from 'types/api/contract'; import type { SmartContractReadMethod, SmartContractQueryMethodRead } from 'types/api/contract';
import useApiFetch from 'lib/api/useApiFetch'; import useApiFetch from 'lib/api/useApiFetch';
import useApiQuery from 'lib/api/useApiQuery'; import useApiQuery from 'lib/api/useApiQuery';
...@@ -10,28 +11,37 @@ import ContractMethodsAccordion from 'ui/address/contract/ContractMethodsAccordi ...@@ -10,28 +11,37 @@ import ContractMethodsAccordion from 'ui/address/contract/ContractMethodsAccordi
import ContentLoader from 'ui/shared/ContentLoader'; import ContentLoader from 'ui/shared/ContentLoader';
import DataFetchAlert from 'ui/shared/DataFetchAlert'; import DataFetchAlert from 'ui/shared/DataFetchAlert';
import ContractConnectWallet from './ContractConnectWallet';
import ContractCustomAbiAlert from './ContractCustomAbiAlert';
import ContractImplementationAddress from './ContractImplementationAddress';
import ContractMethodCallable from './ContractMethodCallable'; import ContractMethodCallable from './ContractMethodCallable';
import ContractMethodConstant from './ContractMethodConstant'; import ContractMethodConstant from './ContractMethodConstant';
import ContractReadResult from './ContractReadResult';
interface Props { interface Props {
isProxy?: boolean; isProxy?: boolean;
isCustomAbi?: boolean;
} }
const ContractRead = ({ isProxy }: Props) => { const ContractRead = ({ isProxy, isCustomAbi }: Props) => {
const router = useRouter(); const router = useRouter();
const apiFetch = useApiFetch(); const apiFetch = useApiFetch();
const { address: userAddress } = useAccount();
const addressHash = router.query.id?.toString(); const addressHash = router.query.id?.toString();
const { data, isLoading, isError } = useApiQuery(isProxy ? 'contract_methods_read_proxy' : 'contract_methods_read', { const { data, isLoading, isError } = useApiQuery(isProxy ? 'contract_methods_read_proxy' : 'contract_methods_read', {
pathParams: { id: addressHash }, pathParams: { id: addressHash },
queryParams: {
is_custom_abi: isCustomAbi ? 'true' : 'false',
},
queryOptions: { queryOptions: {
enabled: Boolean(router.query.id), enabled: Boolean(router.query.id),
}, },
}); });
const contractCaller = React.useCallback(async(item: SmartContractReadMethod, args: Array<string>) => { const handleMethodFormSubmit = React.useCallback(async(item: SmartContractReadMethod, args: Array<string | Array<string>>) => {
await apiFetch('contract_method_query', { return apiFetch<'contract_method_query', SmartContractQueryMethodRead>('contract_method_query', {
pathParams: { id: addressHash }, pathParams: { id: addressHash },
fetchParams: { fetchParams: {
method: 'POST', method: 'POST',
...@@ -39,15 +49,18 @@ const ContractRead = ({ isProxy }: Props) => { ...@@ -39,15 +49,18 @@ const ContractRead = ({ isProxy }: Props) => {
args, args,
method_id: item.method_id, method_id: item.method_id,
contract_type: isProxy ? 'proxy' : 'regular', contract_type: isProxy ? 'proxy' : 'regular',
from: userAddress,
}, },
}, },
}); });
}, [ addressHash, apiFetch, isProxy, userAddress ]);
return [ [ 'string', 'this is mock' ] ];
}, [ addressHash, apiFetch, isProxy ]);
const renderContent = React.useCallback((item: SmartContractReadMethod, index: number, id: number) => { const renderContent = React.useCallback((item: SmartContractReadMethod, index: number, id: number) => {
if (item.inputs.length === 0) { if (item.error) {
return <Alert status="error" fontSize="sm">{ item.error }</Alert>;
}
if (item.outputs.some(({ value }) => value)) {
return ( return (
<Flex flexDir="column" rowGap={ 1 }> <Flex flexDir="column" rowGap={ 1 }>
{ item.outputs.map((output, index) => <ContractMethodConstant key={ index } data={ output }/>) } { item.outputs.map((output, index) => <ContractMethodConstant key={ index } data={ output }/>) }
...@@ -59,10 +72,11 @@ const ContractRead = ({ isProxy }: Props) => { ...@@ -59,10 +72,11 @@ const ContractRead = ({ isProxy }: Props) => {
<ContractMethodCallable <ContractMethodCallable
key={ id + '_' + index } key={ id + '_' + index }
data={ item } data={ item }
caller={ contractCaller } onSubmit={ handleMethodFormSubmit }
ResultComponent={ ContractReadResult }
/> />
); );
}, [ contractCaller ]); }, [ handleMethodFormSubmit ]);
if (isError) { if (isError) {
return <DataFetchAlert/>; return <DataFetchAlert/>;
...@@ -72,7 +86,18 @@ const ContractRead = ({ isProxy }: Props) => { ...@@ -72,7 +86,18 @@ const ContractRead = ({ isProxy }: Props) => {
return <ContentLoader/>; return <ContentLoader/>;
} }
return <ContractMethodsAccordion data={ data } renderContent={ renderContent }/>; if (data.length === 0 && !isProxy) {
return <span>No public read functions were found for this contract.</span>;
}
return (
<>
{ isCustomAbi && <ContractCustomAbiAlert/> }
<ContractConnectWallet/>
{ isProxy && <ContractImplementationAddress hash={ addressHash }/> }
<ContractMethodsAccordion data={ data } renderContent={ renderContent }/>
</>
);
}; };
export default ContractRead; export default React.memo(ContractRead);
import { Alert, Box, chakra, useColorModeValue } from '@chakra-ui/react';
import React from 'react';
import type { ContractMethodReadResult } from './types';
import type { SmartContractReadMethod } from 'types/api/contract';
interface Props {
item: SmartContractReadMethod;
result: ContractMethodReadResult;
onSettle: () => void;
}
const ContractReadResult = ({ item, result, onSettle }: Props) => {
const resultBgColor = useColorModeValue('blackAlpha.50', 'whiteAlpha.50');
React.useEffect(() => {
onSettle();
}, [ onSettle ]);
if ('status' in result) {
return <Alert status="error" mt={ 3 } p={ 4 } borderRadius="md" fontSize="sm">{ result.statusText }</Alert>;
}
if (result.is_error) {
const message = 'error' in result.result ? result.result.error : result.result.message;
return <Alert status="error" mt={ 3 } p={ 4 } borderRadius="md" fontSize="sm">{ message }</Alert>;
}
return (
<Box mt={ 3 } p={ 4 } borderRadius="md" bgColor={ resultBgColor } fontSize="sm">
<p>
[ <chakra.span fontWeight={ 600 }>{ 'name' in item ? item.name : '' }</chakra.span> method response ]
</p>
<p>[</p>
{ result.result.output.map(({ type, value }, index) => (
<chakra.p key={ index } whiteSpace="break-spaces" wordBreak="break-all"> { type }: { String(value) }</chakra.p>
)) }
<p>]</p>
</Box>
);
};
export default React.memo(ContractReadResult);
import { Box, Flex, Link, Text, Tooltip } from '@chakra-ui/react'; import { Box, chakra, Flex, Link, Text, Tooltip } from '@chakra-ui/react';
import React from 'react'; import React from 'react';
import type { SmartContract } from 'types/api/contract';
import link from 'lib/link/link'; import link from 'lib/link/link';
import CodeEditor from 'ui/shared/CodeEditor'; import CodeEditor from 'ui/shared/CodeEditor';
import CopyToClipboard from 'ui/shared/CopyToClipboard'; import CopyToClipboard from 'ui/shared/CopyToClipboard';
...@@ -9,28 +11,62 @@ interface Props { ...@@ -9,28 +11,62 @@ interface Props {
data: string; data: string;
hasSol2Yml: boolean; hasSol2Yml: boolean;
address?: string; address?: string;
isViper: boolean;
filePath?: string;
additionalSource?: SmartContract['additional_sources'];
} }
const ContractSourceCode = ({ data, hasSol2Yml, address }: Props) => { const ContractSourceCode = ({ data, hasSol2Yml, address, isViper, filePath, additionalSource }: Props) => {
const heading = (
<Text fontWeight={ 500 }>
<span>Contract source code</span>
<Text whiteSpace="pre" as="span" variant="secondary"> ({ isViper ? 'Vyper' : 'Solidity' })</Text>
</Text>
);
const diagramLink = hasSol2Yml && address ? (
<Tooltip label="Visualize contract code using Sol2Uml JS library">
<Link
href={ link('visualize_sol2uml', undefined, { address }) }
ml="auto"
mr={ 3 }
>
View UML diagram
</Link>
</Tooltip>
) : null;
if (!additionalSource?.length) {
return (
<section>
<Flex justifyContent="space-between" alignItems="center" mb={ 3 }>
{ heading }
{ diagramLink }
<CopyToClipboard text={ data }/>
</Flex>
<CodeEditor value={ data } id="source_code"/>
</section>
);
}
return ( return (
<Box> <section>
<Flex justifyContent="space-between" alignItems="center" mb={ 3 }> <Flex justifyContent="space-between" alignItems="center" mb={ 3 }>
<Text fontWeight={ 500 }>Contract source code</Text> { heading }
{ hasSol2Yml && address && ( { diagramLink }
<Tooltip label="Visualize contract code using Sol2Uml JS library"> </Flex>
<Link <Flex flexDir="column" rowGap={ 3 }>
href={ link('visualize_sol2uml', undefined, { address }) } { [ { file_path: filePath, source_code: data }, ...additionalSource ].map((item, index, array) => (
ml="auto" <Box key={ index }>
mr={ 3 } <Flex justifyContent="space-between" alignItems="center" mb={ 3 }>
> <chakra.span fontSize="sm">File { index + 1 } of { array.length }: { item.file_path }</chakra.span>
View Sol2uml <CopyToClipboard text={ item.source_code }/>
</Link> </Flex>
</Tooltip> <CodeEditor value={ item.source_code } id={ `source_code_${ index }` }/>
) } </Box>
<CopyToClipboard text={ data }/> )) }
</Flex> </Flex>
<CodeEditor value={ data } id="source_code"/> </section>
</Box>
); );
}; };
......
import { test, expect } from '@playwright/experimental-ct-react';
import React from 'react';
import * as contractMethodsMock from 'mocks/contract/methods';
import TestApp from 'playwright/TestApp';
import buildApiUrl from 'playwright/utils/buildApiUrl';
import ContractWrite from './ContractWrite';
const addressHash = 'hash';
const CONTRACT_WRITE_METHODS_API_URL = buildApiUrl('contract_methods_write', { id: addressHash }) + '?is_custom_abi=false';
const hooksConfig = {
router: {
query: { id: addressHash },
},
};
test('base view +@mobile', async({ mount, page }) => {
await page.route(CONTRACT_WRITE_METHODS_API_URL, (route) => route.fulfill({
status: 200,
body: JSON.stringify(contractMethodsMock.write),
}));
const component = await mount(
<TestApp>
<ContractWrite/>
</TestApp>,
{ hooksConfig },
);
await component.getByText(/expand all/i).click();
await expect(component).toHaveScreenshot();
});
import _capitalize from 'lodash/capitalize';
import { useRouter } from 'next/router'; import { useRouter } from 'next/router';
import React from 'react'; import React from 'react';
import { useAccount, useSigner } from 'wagmi';
import type { SmartContractWriteMethod } from 'types/api/contract'; import type { SmartContractWriteMethod } from 'types/api/contract';
import config from 'configs/app/config';
import useApiQuery from 'lib/api/useApiQuery'; import useApiQuery from 'lib/api/useApiQuery';
import ContractMethodsAccordion from 'ui/address/contract/ContractMethodsAccordion'; import ContractMethodsAccordion from 'ui/address/contract/ContractMethodsAccordion';
import ContentLoader from 'ui/shared/ContentLoader'; import ContentLoader from 'ui/shared/ContentLoader';
import DataFetchAlert from 'ui/shared/DataFetchAlert'; import DataFetchAlert from 'ui/shared/DataFetchAlert';
import { useContractContext } from './context';
import ContractConnectWallet from './ContractConnectWallet';
import ContractCustomAbiAlert from './ContractCustomAbiAlert';
import ContractImplementationAddress from './ContractImplementationAddress';
import ContractMethodCallable from './ContractMethodCallable'; import ContractMethodCallable from './ContractMethodCallable';
import ContractWriteResult from './ContractWriteResult';
import { getNativeCoinValue, isExtendedError } from './utils';
interface Props { interface Props {
isProxy?: boolean; isProxy?: boolean;
isCustomAbi?: boolean;
} }
const ContractWrite = ({ isProxy }: Props) => { const ContractWrite = ({ isProxy, isCustomAbi }: Props) => {
const router = useRouter(); const router = useRouter();
const addressHash = router.query.id?.toString(); const addressHash = router.query.id?.toString();
const { data: signer } = useSigner();
const { isConnected } = useAccount();
const { data, isLoading, isError } = useApiQuery(isProxy ? 'contract_methods_write_proxy' : 'contract_methods_write', { const { data, isLoading, isError } = useApiQuery(isProxy ? 'contract_methods_write_proxy' : 'contract_methods_write', {
pathParams: { id: addressHash }, pathParams: { id: addressHash },
queryOptions: { queryParams: {
enabled: Boolean(router.query.id), is_custom_abi: isCustomAbi ? 'true' : 'false',
}, },
});
const contractInfo = useApiQuery('contract', {
pathParams: { id: addressHash },
queryOptions: { queryOptions: {
enabled: Boolean(router.query.id), enabled: Boolean(addressHash),
}, },
}); });
const contractCaller = React.useCallback(async() => { const { contract, proxy } = useContractContext();
// eslint-disable-next-line no-console const _contract = isProxy ? proxy : contract;
console.log('__>__', contractInfo);
return [ [ 'string', 'this is mock' ] ]; const handleMethodFormSubmit = React.useCallback(async(item: SmartContractWriteMethod, args: Array<string | Array<string>>) => {
}, [ contractInfo ]); if (!isConnected) {
throw new Error('Wallet is not connected');
}
try {
if (!_contract) {
return;
}
if (item.type === 'receive') {
const value = args[0] ? getNativeCoinValue(args[0]) : '0';
const result = await signer?.sendTransaction({
to: addressHash,
value,
});
return { hash: result?.hash as string };
}
const _args = item.stateMutability === 'payable' ? args.slice(0, -1) : args;
const value = item.stateMutability === 'payable' ? getNativeCoinValue(args[args.length - 1]) : undefined;
const methodName = item.type === 'fallback' ? 'fallback' : item.name;
const result = await _contract[methodName](..._args, {
gasLimit: 100_000,
value,
});
return { hash: result.hash as string };
} catch (error) {
if (isExtendedError(error)) {
if ('reason' in error && error.reason === 'underlying network changed') {
if ('detectedNetwork' in error && typeof error.detectedNetwork === 'object' && error.detectedNetwork !== null) {
const networkName = error.detectedNetwork.name;
if (networkName) {
throw new Error(
// eslint-disable-next-line max-len
`You connected to ${ _capitalize(networkName) } chain in the wallet, but the current instance of Blockscout is for ${ config.network.name } chain`,
);
}
}
throw new Error('Wrong network detected, please make sure you are switched to the correct network, and try again');
}
}
throw error;
}
}, [ _contract, addressHash, isConnected, signer ]);
const renderContent = React.useCallback((item: SmartContractWriteMethod, index: number, id: number) => { const renderContent = React.useCallback((item: SmartContractWriteMethod, index: number, id: number) => {
return ( return (
<ContractMethodCallable <ContractMethodCallable
key={ id + '_' + index } key={ id + '_' + index }
data={ item } data={ item }
caller={ contractCaller } onSubmit={ handleMethodFormSubmit }
ResultComponent={ ContractWriteResult }
isWrite isWrite
/> />
); );
}, [ contractCaller ]); }, [ handleMethodFormSubmit ]);
if (isError) { if (isError) {
return <DataFetchAlert/>; return <DataFetchAlert/>;
...@@ -58,7 +114,18 @@ const ContractWrite = ({ isProxy }: Props) => { ...@@ -58,7 +114,18 @@ const ContractWrite = ({ isProxy }: Props) => {
return <ContentLoader/>; return <ContentLoader/>;
} }
return <ContractMethodsAccordion data={ data } renderContent={ renderContent }/>; if (data.length === 0 && !isProxy) {
return <span>No public write functions were found for this contract.</span>;
}
return (
<>
{ isCustomAbi && <ContractCustomAbiAlert/> }
<ContractConnectWallet/>
{ isProxy && <ContractImplementationAddress hash={ addressHash }/> }
<ContractMethodsAccordion data={ data } renderContent={ renderContent }/>
</>
);
}; };
export default ContractWrite; export default React.memo(ContractWrite);
import React from 'react';
import { useWaitForTransaction } from 'wagmi';
import type { ContractMethodWriteResult } from './types';
import ContractWriteResultDumb from './ContractWriteResultDumb';
interface Props {
result: ContractMethodWriteResult;
onSettle: () => void;
}
const ContractWriteResult = ({ result, onSettle }: Props) => {
const txHash = result && 'hash' in result ? result.hash as `0x${ string }` : undefined;
const txInfo = useWaitForTransaction({
hash: txHash,
});
return <ContractWriteResultDumb result={ result } onSettle={ onSettle } txInfo={ txInfo }/>;
};
export default React.memo(ContractWriteResult);
import { test, expect } from '@playwright/experimental-ct-react';
import React from 'react';
import TestApp from 'playwright/TestApp';
import ContractWriteResultDumb from './ContractWriteResultDumb';
test('loading', async({ mount }) => {
const props = {
txInfo: {
status: 'loading' as const,
error: null,
},
result: {
hash: '0x363574E6C5C71c343d7348093D84320c76d5Dd29',
},
onSettle: () => {},
};
const component = await mount(
<TestApp>
<ContractWriteResultDumb { ...props }/>
</TestApp>,
);
await expect(component).toHaveScreenshot();
});
test('success', async({ mount }) => {
const props = {
txInfo: {
status: 'success' as const,
error: null,
},
result: {
hash: '0x363574E6C5C71c343d7348093D84320c76d5Dd29',
},
onSettle: () => {},
};
const component = await mount(
<TestApp>
<ContractWriteResultDumb { ...props }/>
</TestApp>,
);
await expect(component).toHaveScreenshot();
});
test('error +@mobile', async({ mount }) => {
const props = {
txInfo: {
status: 'error' as const,
error: {
// eslint-disable-next-line max-len
message: 'missing revert data in call exception; Transaction reverted without a reason string [ See: https://links.ethers.org/v5-errors-CALL_EXCEPTION ]',
} as Error,
},
result: {
hash: '0x363574E6C5C71c343d7348093D84320c76d5Dd29',
},
onSettle: () => {},
};
const component = await mount(
<TestApp>
<ContractWriteResultDumb { ...props }/>
</TestApp>,
);
await expect(component).toHaveScreenshot();
});
test('error in result', async({ mount }) => {
const props = {
txInfo: {
status: 'idle' as const,
error: null,
},
result: {
message: 'wallet is not connected',
} as Error,
onSettle: () => {},
};
const component = await mount(
<TestApp>
<ContractWriteResultDumb { ...props }/>
</TestApp>,
);
await expect(component).toHaveScreenshot();
});
import { Box, chakra, Link, Spinner } from '@chakra-ui/react';
import React from 'react';
import type { ContractMethodWriteResult } from './types';
import link from 'lib/link/link';
interface Props {
result: ContractMethodWriteResult;
onSettle: () => void;
txInfo: {
status: 'loading' | 'success' | 'error' | 'idle';
error: Error | null;
};
}
const ContractWriteResultDumb = ({ result, onSettle, txInfo }: Props) => {
const txHash = result && 'hash' in result ? result.hash : undefined;
React.useEffect(() => {
if (txInfo.status !== 'loading') {
onSettle();
}
}, [ onSettle, txInfo.status ]);
if (!result) {
return null;
}
const isErrorResult = 'message' in result;
const txLink = (
<Link href={ link('tx', { id: txHash }) }>View transaction details</Link>
);
const content = (() => {
if (isErrorResult) {
return (
<>
<span>Error: </span>
<span>{ result.message }</span>
</>
);
}
switch (txInfo.status) {
case 'success': {
return (
<>
<span>Transaction has been confirmed. </span>
{ txLink }
</>
);
}
case 'loading': {
return (
<>
<Spinner size="sm" mr={ 3 }/>
<chakra.span verticalAlign="text-bottom">
{ 'Waiting for transaction\'s confirmation. ' }
{ txLink }
</chakra.span>
</>
);
}
case 'error': {
return (
<>
<span>Error: </span>
<span>{ txInfo.error ? txInfo.error.message : 'Something went wrong' } </span>
{ txLink }
</>
);
}
}
})();
return (
<Box
fontSize="sm"
pl={ 3 }
mt={ 3 }
alignItems="center"
whiteSpace="pre-wrap"
wordBreak="break-all"
color={ txInfo.status === 'error' || isErrorResult ? 'red.600' : undefined }
>
{ content }
</Box>
);
};
export default React.memo(ContractWriteResultDumb);
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
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