Merge branch 'main' into tom2drum/issue-1484

This commit is contained in:
tom goriunov
2024-01-17 18:29:23 +04:00
committed by GitHub
239 changed files with 3345 additions and 406 deletions
+7 -4
View File
@@ -20,15 +20,15 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Check out the repo
uses: actions/checkout@v3
uses: actions/checkout@v4
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v2
uses: docker/setup-buildx-action@v3
# Will automatically make nice tags, see the table here https://github.com/docker/metadata-action#basic
- name: Docker meta
id: meta
uses: docker/metadata-action@v4
uses: docker/metadata-action@v5
with:
images: ghcr.io/blockscout/frontend
@@ -51,13 +51,16 @@ jobs:
echo "ref_name: $REF_NAME"
- name: Build and push
uses: docker/build-push-action@v3
uses: docker/build-push-action@v5
with:
context: .
file: ./Dockerfile
push: true
cache-from: type=gha
tags: ${{ inputs.tags || steps.meta.outputs.tags }}
platforms: |
linux/amd64
linux/arm64/v8
labels: ${{ steps.meta.outputs.labels }}
build-args: |
GIT_COMMIT_SHA=${{ env.SHORT_SHA }}
+1
View File
@@ -11,6 +11,7 @@ export { default as graphqlApiDocs } from './graphqlApiDocs';
export { default as growthBook } from './growthBook';
export { default as marketplace } from './marketplace';
export { default as mixpanel } from './mixpanel';
export { default as nameService } from './nameService';
export { default as restApiDocs } from './restApiDocs';
export { default as optimisticRollup } from './optimisticRollup';
export { default as safe } from './safe';
+27
View File
@@ -0,0 +1,27 @@
import type { Feature } from './types';
import { getEnvValue } from '../utils';
const apiHost = getEnvValue('NEXT_PUBLIC_NAME_SERVICE_API_HOST');
const title = 'Name service integration';
const config: Feature<{ api: { endpoint: string; basePath: string } }> = (() => {
if (apiHost) {
return Object.freeze({
title,
isEnabled: true,
api: {
endpoint: apiHost,
basePath: '',
},
});
}
return Object.freeze({
title,
isEnabled: false,
});
})();
export default config;
+1
View File
@@ -46,6 +46,7 @@ NEXT_PUBLIC_STATS_API_HOST=https://stats-goerli.k8s-dev.blockscout.com
NEXT_PUBLIC_VISUALIZE_API_HOST=https://visualizer.services.blockscout.com
NEXT_PUBLIC_CONTRACT_INFO_API_HOST=https://contracts-info.services.blockscout.com
NEXT_PUBLIC_ADMIN_SERVICE_API_HOST=https://admin-rs.services.blockscout.com
NEXT_PUBLIC_NAME_SERVICE_API_HOST=https://bens-rs-test.k8s-dev.blockscout.com
NEXT_PUBLIC_WEB3_WALLETS=['token_pocket','metamask']
NEXT_PUBLIC_VIEWS_CONTRACT_SOLIDITYSCAN_ENABLED='true'
+1
View File
@@ -425,6 +425,7 @@ const schema = yup
NEXT_PUBLIC_STATS_API_HOST: yup.string().test(urlTest),
NEXT_PUBLIC_VISUALIZE_API_HOST: yup.string().test(urlTest),
NEXT_PUBLIC_CONTRACT_INFO_API_HOST: yup.string().test(urlTest),
NEXT_PUBLIC_NAME_SERVICE_API_HOST: yup.string().test(urlTest),
NEXT_PUBLIC_GRAPHIQL_TRANSACTION: yup.string().matches(regexp.HEX_REGEXP),
NEXT_PUBLIC_WEB3_WALLETS: yup
.mixed()
+1
View File
@@ -153,6 +153,7 @@ frontend:
NEXT_PUBLIC_VISUALIZE_API_HOST: http://visualizer-svc.visualizer-testing.svc.cluster.local/
NEXT_PUBLIC_CONTRACT_INFO_API_HOST: https://contracts-info-test.k8s-dev.blockscout.com
NEXT_PUBLIC_ADMIN_SERVICE_API_HOST: https://admin-rs-test.k8s-dev.blockscout.com
NEXT_PUBLIC_NAME_SERVICE_API_HOST: https://bens-rs-test.k8s-dev.blockscout.com
NEXT_PUBLIC_LOGOUT_URL: https://blockscoutcom.us.auth0.com/v2/logout
NEXT_PUBLIC_NETWORK_RPC_URL: https://rpc.ankr.com/eth_goerli
NEXT_PUBLIC_HOMEPAGE_CHARTS: "['daily_txs','coin_price','market_cap']"
+1
View File
@@ -56,6 +56,7 @@ frontend:
NEXT_PUBLIC_VISUALIZE_API_HOST: http://visualizer-svc.visualizer-testing.svc.cluster.local/
NEXT_PUBLIC_CONTRACT_INFO_API_HOST: https://contracts-info-test.k8s-dev.blockscout.com
NEXT_PUBLIC_ADMIN_SERVICE_API_HOST: https://admin-rs-test.k8s-dev.blockscout.com
NEXT_PUBLIC_NAME_SERVICE_API_HOST: https://bens-rs-test.k8s-dev.blockscout.com
NEXT_PUBLIC_AUTH_URL: https://blockscout-main.k8s-dev.blockscout.com
NEXT_PUBLIC_MARKETPLACE_SUBMIT_FORM: https://airtable.com/shrqUAcjgGJ4jU88C
NEXT_PUBLIC_LOGOUT_URL: https://blockscoutcom.us.auth0.com/v2/logout
+11
View File
@@ -47,6 +47,7 @@ Please be aware that all environment variables prefixed with `NEXT_PUBLIC_` will
- [Web3 wallet integration](ENVS.md#web3-wallet-integration-add-token-or-network-to-the-wallet) (add token or network to the wallet)
- [Transaction interpretation](ENVS.md#transaction-interpretation)
- [Verified tokens info](ENVS.md#verified-tokens-info)
- [Name service integration](ENVS.md#name-service-integration)
- [Bridged tokens](ENVS.md#bridged-tokens)
- [Safe{Core} address tags](ENVS.md#safecore-address-tags)
- [SUAVE chain](ENVS.md#suave-chain)
@@ -499,6 +500,16 @@ This feature is **enabled by default** with the `['metamask']` value. To switch
&nbsp;
### Name service integration
This feature allows resolving blockchain addresses using human-readable domain names.
| Variable | Type| Description | Compulsoriness | Default value | Example value |
| --- | --- | --- | --- | --- | --- |
| NEXT_PUBLIC_NAME_SERVICE_API_HOST | `string` | Name Service API endpoint url | Required | - | `https://bens.services.blockscout.com` |
&nbsp;
### Bridged tokens
This feature allows users to view tokens that have been bridged from other EVM chains. Additional tab "Bridged" will be added to the tokens page and the link to original token will be displayed on the token page.
+3
View File
@@ -0,0 +1,3 @@
<svg viewBox="0 0 30 30" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M14.41 3 7.764 7.625c-.396.263-.72.62-.946 1.038a4.74 4.74 0 0 0-.046.1l-.009.019a4.786 4.786 0 0 0 .01 3.93c.233.508.828 1.51.828 1.51L14.41 3ZM4.076 16.404A15.45 15.45 0 0 1 4 14.886l1.168-3.159a6.617 6.617 0 0 0 .755 2.095C9.802 20.393 14.401 27 14.401 27l-7.35-5.108A7.562 7.562 0 0 1 4.25 17.57a7.53 7.53 0 0 1-.174-1.166ZM4 14.886l1.144-3.306a3.575 3.575 0 0 1 0-1.076c-.101.187-.298.57-.298.57a8.68 8.68 0 0 0-.794 2.534A15.45 15.45 0 0 0 4 14.886ZM15.192 27l.063-.103 6.734-11.113.005.009.007-.012s.589 1.002.827 1.51a4.787 4.787 0 0 1-.045 4.05c-.225.417-.55.774-.946 1.037L15.192 27Zm9.263-7.504c.054-.357.054-.72 0-1.076a6.622 6.622 0 0 0-.776-2.242C19.796 9.607 15.2 3 15.2 3l7.35 5.108a7.533 7.533 0 0 1 2.975 5.488c.091.93.098 1.865.02 2.796a8.68 8.68 0 0 1-.794 2.535s-.196.382-.297.57v-.001Z" fill="currentColor"/>
</svg>

After

Width:  |  Height:  |  Size: 962 B

+4
View File
@@ -0,0 +1,4 @@
<svg viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M3.145 8.091c.198.423.689 1.258.689 1.258L9.508 0 3.97 3.854c-.33.22-.6.517-.788.865a3.989 3.989 0 0 0-.037 3.372ZM.897 11.17a6.277 6.277 0 0 0 2.478 4.573L9.501 20S5.668 14.495 2.436 9.018a5.52 5.52 0 0 1-.65-1.868 2.98 2.98 0 0 1 0-.897 35.09 35.09 0 0 0-.247.475A7.234 7.234 0 0 0 .877 8.84a12.86 12.86 0 0 0 .02 2.33Zm15.626.739c-.198-.423-.689-1.258-.689-1.258L10.16 20l5.538-3.852c.33-.22.6-.516.788-.864a3.99 3.99 0 0 0 .037-3.375Zm2.249-3.079a6.279 6.279 0 0 0-2.48-4.573L10.168 0s3.83 5.505 7.065 10.982a5.52 5.52 0 0 1 .647 1.868c.045.297.045.6 0 .897.084-.156.248-.475.248-.475a7.223 7.223 0 0 0 .662-2.112c.065-.776.059-1.555-.017-2.33Z" fill="currentColor"/>
<path d="M3.182 4.719c.188-.348.458-.645.788-.865L9.508 0 3.834 9.351s-.496-.835-.69-1.257a3.989 3.989 0 0 1 .038-3.375Zm-2.285 6.45a6.278 6.278 0 0 0 2.478 4.574L9.501 20S5.668 14.495 2.436 9.018a5.52 5.52 0 0 1-.65-1.868 2.98 2.98 0 0 1 0-.897 34.31 34.31 0 0 0-.247.475A7.234 7.234 0 0 0 .877 8.84c-.064.776-.057 1.555.02 2.33Zm15.616.742c-.198-.422-.689-1.258-.689-1.258L10.16 20l5.538-3.852c.33-.22.6-.516.788-.864a3.99 3.99 0 0 0 .037-3.375l-.01.002Zm2.249-3.078a6.277 6.277 0 0 0-2.48-4.574L10.169 0s3.83 5.505 7.064 10.982a5.52 5.52 0 0 1 .647 1.868c.045.297.045.6 0 .897.084-.156.248-.475.248-.475a7.233 7.233 0 0 0 .662-2.112c.064-.776.059-1.555-.018-2.33l-.01.003Z" fill="currentColor" style="mix-blend-mode:color"/>
</svg>

After

Width:  |  Height:  |  Size: 1.5 KiB

+1
View File
@@ -20,6 +20,7 @@ const PAGE_PROPS = {
hash: '',
number: '',
q: '',
name: '',
};
const TestApp = ({ children }: {children: React.ReactNode}) => {
+3
View File
@@ -1,4 +1,5 @@
import dotenv from 'dotenv';
import { TextEncoder, TextDecoder } from 'util';
import fetchMock from 'jest-fetch-mock';
@@ -6,6 +7,8 @@ fetchMock.enableMocks();
const envs = dotenv.config({ path: './configs/envs/.env.jest' });
Object.assign(global, { TextDecoder, TextEncoder });
Object.defineProperty(window, 'matchMedia', {
writable: true,
value: jest.fn().mockImplementation(query => ({
+1 -1
View File
@@ -9,7 +9,7 @@ import type { ApiResource, ResourceName, ResourcePathParams } from './resources'
export default function buildUrl<R extends ResourceName>(
resourceName: R,
pathParams?: ResourcePathParams<R>,
queryParams?: Record<string, string | Array<string> | number | null | undefined>,
queryParams?: Record<string, string | Array<string> | number | boolean | null | undefined>,
): string {
const resource: ApiResource = RESOURCES[resourceName];
const baseUrl = isNeedProxy() ? config.app.baseUrl : (resource.endpoint || config.api.endpoint);
+47 -1
View File
@@ -36,6 +36,15 @@ import type { ChartMarketResponse, ChartTransactionResponse } from 'types/api/ch
import type { BackendVersionConfig } from 'types/api/configs';
import type { SmartContract, SmartContractReadMethod, SmartContractWriteMethod, SmartContractVerificationConfig, SolidityscanReport } from 'types/api/contract';
import type { VerifiedContractsResponse, VerifiedContractsFilters, VerifiedContractsCounters } from 'types/api/contracts';
import type {
EnsAddressLookupFilters,
EnsAddressLookupResponse,
EnsDomainDetailed,
EnsDomainEventsResponse,
EnsDomainLookupFilters,
EnsDomainLookupResponse,
EnsLookupSorting,
} from 'types/api/ens';
import type { IndexingStatus } from 'types/api/indexingStatus';
import type { InternalTransactionsResponse } from 'types/api/internalTransaction';
import type { L2DepositsResponse, L2DepositsItem } from 'types/api/l2Deposits';
@@ -176,6 +185,34 @@ export const RESOURCES = {
basePath: getFeaturePayload(config.features.stats)?.api.basePath,
},
// NAME SERVICE
addresses_lookup: {
path: '/api/v1/:chainId/addresses\\:lookup',
pathParams: [ 'chainId' as const ],
endpoint: getFeaturePayload(config.features.nameService)?.api.endpoint,
basePath: getFeaturePayload(config.features.nameService)?.api.basePath,
filterFields: [ 'address' as const, 'resolved_to' as const, 'owned_by' as const, 'only_active' as const ],
},
domain_info: {
path: '/api/v1/:chainId/domains/:name',
pathParams: [ 'chainId' as const, 'name' as const ],
endpoint: getFeaturePayload(config.features.nameService)?.api.endpoint,
basePath: getFeaturePayload(config.features.nameService)?.api.basePath,
},
domain_events: {
path: '/api/v1/:chainId/domains/:name/events',
pathParams: [ 'chainId' as const, 'name' as const ],
endpoint: getFeaturePayload(config.features.nameService)?.api.endpoint,
basePath: getFeaturePayload(config.features.nameService)?.api.basePath,
},
domains_lookup: {
path: '/api/v1/:chainId/domains\\:lookup',
pathParams: [ 'chainId' as const ],
endpoint: getFeaturePayload(config.features.nameService)?.api.endpoint,
basePath: getFeaturePayload(config.features.nameService)?.api.basePath,
filterFields: [ 'name' as const, 'only_active' as const ],
},
// VISUALIZATION
visualize_sol2uml: {
path: '/api/v1/solidity\\:visualize-contracts',
@@ -613,7 +650,8 @@ export type PaginatedResources = 'blocks' | 'block_txs' |
'l2_output_roots' | 'l2_withdrawals' | 'l2_txn_batches' | 'l2_deposits' |
'zkevm_l2_txn_batches' | 'zkevm_l2_txn_batch_txs' |
'withdrawals' | 'address_withdrawals' | 'block_withdrawals' |
'watchlist' | 'private_tags_address' | 'private_tags_tx';
'watchlist' | 'private_tags_address' | 'private_tags_tx' |
'domains_lookup' | 'addresses_lookup';
export type PaginatedResponse<Q extends PaginatedResources> = ResourcePayload<Q>;
@@ -712,6 +750,10 @@ Q extends 'zkevm_l2_txn_batches_count' ? number :
Q extends 'zkevm_l2_txn_batch' ? ZkEvmL2TxnBatch :
Q extends 'zkevm_l2_txn_batch_txs' ? ZkEvmL2TxnBatchTxs :
Q extends 'config_backend_version' ? BackendVersionConfig :
Q extends 'addresses_lookup' ? EnsAddressLookupResponse :
Q extends 'domain_info' ? EnsDomainDetailed :
Q extends 'domain_events' ? EnsDomainEventsResponse :
Q extends 'domains_lookup' ? EnsDomainLookupResponse :
never;
/* eslint-enable @typescript-eslint/indent */
@@ -731,6 +773,8 @@ Q extends 'token_inventory' ? TokenInventoryFilters :
Q extends 'tokens' ? TokensFilters :
Q extends 'tokens_bridged' ? TokensBridgedFilters :
Q extends 'verified_contracts' ? VerifiedContractsFilters :
Q extends 'addresses_lookup' ? EnsAddressLookupFilters :
Q extends 'domains_lookup' ? EnsDomainLookupFilters :
never;
/* eslint-enable @typescript-eslint/indent */
@@ -740,5 +784,7 @@ Q extends 'tokens' ? TokensSorting :
Q extends 'tokens_bridged' ? TokensSorting :
Q extends 'verified_contracts' ? VerifiedContractsSorting :
Q extends 'address_txs' ? TransactionsSorting :
Q extends 'addresses_lookup' ? EnsLookupSorting :
Q extends 'domains_lookup' ? EnsLookupSorting :
never;
/* eslint-enable @typescript-eslint/indent */
+5 -3
View File
@@ -1,4 +1,5 @@
import { useQueryClient } from '@tanstack/react-query';
import _omit from 'lodash/omit';
import _pickBy from 'lodash/pickBy';
import React from 'react';
@@ -18,8 +19,8 @@ import type { ApiResource, ResourceName, ResourcePathParams } from './resources'
export interface Params<R extends ResourceName> {
pathParams?: ResourcePathParams<R>;
queryParams?: Record<string, string | Array<string> | number | undefined>;
fetchParams?: Pick<FetchParams, 'body' | 'method' | 'signal'>;
queryParams?: Record<string, string | Array<string> | number | boolean | undefined>;
fetchParams?: Pick<FetchParams, 'body' | 'method' | 'signal' | 'headers'>;
}
export default function useApiFetch() {
@@ -40,6 +41,7 @@ export default function useApiFetch() {
'x-endpoint': resource.endpoint && isNeedProxy() ? resource.endpoint : undefined,
Authorization: resource.endpoint && resource.needAuth ? apiToken : undefined,
'x-csrf-token': withBody && csrfToken ? csrfToken : undefined,
...fetchParams?.headers,
}, Boolean) as HeadersInit;
return fetch<SuccessType, ErrorType>(
@@ -51,7 +53,7 @@ export default function useApiFetch() {
// change condition here if something is changed
credentials: config.features.account.isEnabled ? 'include' : 'same-origin',
headers,
...fetchParams,
..._omit(fetchParams, 'headers'),
},
{
resource: resource.path,
+1
View File
@@ -15,6 +15,7 @@ const AppContext = createContext<PageProps>({
hash: '',
number: '',
q: '',
name: '',
});
export function AppContextProvider({ children, pageProps }: Props) {
+6 -6
View File
@@ -15,22 +15,22 @@ export default function useContractTabs(data: Address | undefined) {
// { id: 'contact_decompiled_code', title: 'Decompiled code', component: <div>Decompiled code</div> } :
// undefined,
data?.has_methods_read ?
{ id: 'read_contract', title: 'Read contract', component: <ContractRead addressHash={ data?.hash }/> } :
{ id: 'read_contract', title: 'Read contract', component: <ContractRead/> } :
undefined,
data?.has_methods_read_proxy ?
{ id: 'read_proxy', title: 'Read proxy', component: <ContractRead addressHash={ data?.hash } isProxy/> } :
{ id: 'read_proxy', title: 'Read proxy', component: <ContractRead/> } :
undefined,
data?.has_custom_methods_read ?
{ id: 'read_custom_methods', title: 'Read custom', component: <ContractRead addressHash={ data?.hash } isCustomAbi/> } :
{ id: 'read_custom_methods', title: 'Read custom', component: <ContractRead/> } :
undefined,
data?.has_methods_write ?
{ id: 'write_contract', title: 'Write contract', component: <ContractWrite addressHash={ data?.hash }/> } :
{ id: 'write_contract', title: 'Write contract', component: <ContractWrite/> } :
undefined,
data?.has_methods_write_proxy ?
{ id: 'write_proxy', title: 'Write proxy', component: <ContractWrite addressHash={ data?.hash } isProxy/> } :
{ id: 'write_proxy', title: 'Write proxy', component: <ContractWrite/> } :
undefined,
data?.has_custom_methods_write ?
{ id: 'write_custom_methods', title: 'Write custom', component: <ContractWrite addressHash={ data?.hash } isCustomAbi/> } :
{ id: 'write_custom_methods', title: 'Write custom', component: <ContractWrite/> } :
undefined,
].filter(Boolean);
}, [ data ]);
+9
View File
@@ -53,6 +53,12 @@ export default function useNavItems(): ReturnType {
icon: 'verified',
isActive: pathname === '/verified-contracts',
};
const ensLookup = config.features.nameService.isEnabled ? {
text: 'ENS lookup',
nextRoute: { pathname: '/name-domains' as const },
icon: 'ENS',
isActive: pathname === '/name-domains' || pathname === '/name-domains/[name]',
} : null;
if (config.features.zkEvmRollup.isEnabled) {
blockchainNavItems = [
@@ -69,6 +75,7 @@ export default function useNavItems(): ReturnType {
[
topAccounts,
verifiedContracts,
ensLookup,
].filter(Boolean),
];
} else if (config.features.optimisticRollup.isEnabled) {
@@ -90,6 +97,7 @@ export default function useNavItems(): ReturnType {
[
topAccounts,
verifiedContracts,
ensLookup,
].filter(Boolean),
];
} else {
@@ -98,6 +106,7 @@ export default function useNavItems(): ReturnType {
blocks,
topAccounts,
verifiedContracts,
ensLookup,
config.features.beaconChain.isEnabled && {
text: 'Withdrawals',
nextRoute: { pathname: '/withdrawals' as const },
+2
View File
@@ -40,6 +40,8 @@ const OG_TYPE_DICT: Record<Route['pathname'], OGPageType> = {
'/zkevm-l2-txn-batches': 'Root page',
'/zkevm-l2-txn-batch/[number]': 'Regular page',
'/404': 'Regular page',
'/name-domains': 'Root page',
'/name-domains/[name]': 'Regular page',
// service routes, added only to make typescript happy
'/login': 'Regular page',
+2
View File
@@ -43,6 +43,8 @@ const TEMPLATE_MAP: Record<Route['pathname'], string> = {
'/zkevm-l2-txn-batches': DEFAULT_TEMPLATE,
'/zkevm-l2-txn-batch/[number]': DEFAULT_TEMPLATE,
'/404': DEFAULT_TEMPLATE,
'/name-domains': DEFAULT_TEMPLATE,
'/name-domains/[name]': DEFAULT_TEMPLATE,
// service routes, added only to make typescript happy
'/login': DEFAULT_TEMPLATE,
+3 -1
View File
@@ -33,11 +33,13 @@ const TEMPLATE_MAP: Record<Route['pathname'], string> = {
'/csv-export': 'export data to CSV',
'/l2-deposits': 'deposits (L1 > L2)',
'/l2-output-roots': 'output roots',
'/l2-txn-batches': 'Tx batches (L2 blocks)',
'/l2-txn-batches': 'tx batches (L2 blocks)',
'/l2-withdrawals': 'withdrawals (L2 > L1)',
'/zkevm-l2-txn-batches': 'zkEvm L2 Tx batches',
'/zkevm-l2-txn-batch/[number]': 'zkEvm L2 Tx batch %number%',
'/404': 'error - page not found',
'/name-domains': 'domains search and resolve',
'/name-domains/[name]': '%name% domain details',
// service routes, added only to make typescript happy
'/login': 'login',
+2
View File
@@ -38,6 +38,8 @@ export const PAGE_TYPE_DICT: Record<Route['pathname'], string> = {
'/zkevm-l2-txn-batches': 'ZkEvm L2 Tx batches',
'/zkevm-l2-txn-batch/[number]': 'ZkEvm L2 Tx batch details',
'/404': '404',
'/name-domains': 'Domains search and resolve',
'/name-domains/[name]': 'Domain details',
// service routes, added only to make typescript happy
'/login': 'Login',
+22 -3
View File
@@ -3,18 +3,21 @@ import type { WalletType } from 'types/client/wallets';
export enum EventTypes {
PAGE_VIEW = 'Page view',
SEARCH_QUERY = 'Search query',
LOCAL_SEARCH = 'Local search',
ADD_TO_WALLET = 'Add to wallet',
ACCOUNT_ACCESS = 'Account access',
PRIVATE_TAG = 'Private tag',
VERIFY_ADDRESS = 'Verify address',
VERIFY_TOKEN = 'Verify token',
WALLET_CONNECT = 'Wallet connect',
WALLET_ACTION = 'Wallet action',
CONTRACT_INTERACTION = 'Contract interaction',
CONTRACT_VERIFICATION = 'Contract verification',
QR_CODE = 'QR code',
PAGE_WIDGET = 'Page widget',
TX_INTERPRETATION_INTERACTION = 'Transaction interpratetion interaction',
EXPERIMENT_STARTED = 'Experiment started',
FILTERS = 'Filters'
}
/* eslint-disable @typescript-eslint/indent */
@@ -31,6 +34,10 @@ Type extends EventTypes.SEARCH_QUERY ? {
'Source page type': string;
'Result URL': string;
} :
Type extends EventTypes.LOCAL_SEARCH ? {
'Search query': string;
'Source': 'Marketplace';
} :
Type extends EventTypes.ADD_TO_WALLET ? (
{
'Wallet': WalletType;
@@ -66,6 +73,9 @@ Type extends EventTypes.WALLET_CONNECT ? {
'Source': 'Header' | 'Smart contracts';
'Status': 'Started' | 'Connected';
} :
Type extends EventTypes.WALLET_ACTION ? {
'Action': 'Open' | 'Address click';
} :
Type extends EventTypes.CONTRACT_INTERACTION ? {
'Method type': 'Read' | 'Write';
'Method name': string;
@@ -77,9 +87,14 @@ Type extends EventTypes.CONTRACT_VERIFICATION ? {
Type extends EventTypes.QR_CODE ? {
'Page type': string;
} :
Type extends EventTypes.PAGE_WIDGET ? {
'Type': 'Tokens dropdown' | 'Tokens show all (icon)' | 'Add to watchlist' | 'Address actions (more button)';
} :
Type extends EventTypes.PAGE_WIDGET ? (
{
'Type': 'Tokens dropdown' | 'Tokens show all (icon)' | 'Add to watchlist' | 'Address actions (more button)';
} | {
'Type': 'Favorite app' | 'More button';
'Info': string;
}
) :
Type extends EventTypes.TX_INTERPRETATION_INTERACTION ? {
'Type': 'Address click' | 'Token click';
} :
@@ -88,5 +103,9 @@ Type extends EventTypes.EXPERIMENT_STARTED ? {
'Variant name': string;
'Source': 'growthbook';
} :
Type extends EventTypes.FILTERS ? {
'Source': 'Marketplace';
'Filter name': string;
} :
undefined;
/* eslint-enable @typescript-eslint/indent */
+17
View File
@@ -15,6 +15,19 @@ export const withName: AddressParam = {
private_tags: [],
watchlist_names: [],
public_tags: [],
ens_domain_name: null,
};
export const withEns: AddressParam = {
hash: hash,
implementation_name: null,
is_contract: false,
is_verified: null,
name: 'ArianeeStore',
private_tags: [],
watchlist_names: [],
public_tags: [],
ens_domain_name: 'kitty.kitty.kitty.cat.eth',
};
export const withoutName: AddressParam = {
@@ -26,6 +39,7 @@ export const withoutName: AddressParam = {
private_tags: [],
watchlist_names: [],
public_tags: [],
ens_domain_name: null,
};
export const token: Address = {
@@ -56,6 +70,7 @@ export const token: Address = {
has_token_transfers: true,
has_tokens: true,
has_validated_blocks: false,
ens_domain_name: null,
};
export const contract: Address = {
@@ -86,6 +101,7 @@ export const contract: Address = {
token: null,
watchlist_names: [ watchlistName ],
watchlist_address_id: 42,
ens_domain_name: null,
};
export const validator: Address = {
@@ -116,4 +132,5 @@ export const validator: Address = {
token: null,
watchlist_names: [],
watchlist_address_id: null,
ens_domain_name: null,
};
+3
View File
@@ -22,6 +22,7 @@ export const base: Block = {
private_tags: [],
public_tags: [],
watchlist_names: [],
ens_domain_name: null,
},
nonce: '0x0000000000000000',
parent_hash: '0x44125f0eb36a9d942e0c23bb4e8117f7ba86a9537a69b59c0025986ed2b7500f',
@@ -71,6 +72,7 @@ export const genesis: Block = {
private_tags: [],
public_tags: [],
watchlist_names: [],
ens_domain_name: 'kitty.kitty.cat.eth',
},
nonce: '0x0000000000000000',
parent_hash: '0x0000000000000000000000000000000000000000000000000000000000000000',
@@ -99,6 +101,7 @@ export const base2: Block = {
private_tags: [],
public_tags: [],
watchlist_names: [],
ens_domain_name: null,
},
timestamp: '2022-11-11T11:46:05Z',
tx_count: 253,
+1 -1
View File
@@ -9,7 +9,7 @@ export const read: Array<SmartContractReadMethod> = [
{
constant: true,
inputs: [
{ internalType: 'address', name: '', type: 'address' },
{ internalType: 'address', name: 'wallet', type: 'address' },
],
method_id: '70a08231',
name: 'FLASHLOAN_PREMIUM_TOTAL',
+2
View File
@@ -10,6 +10,7 @@ export const contract1: VerifiedContract = {
private_tags: [],
public_tags: [],
watchlist_names: [],
ens_domain_name: null,
},
coin_balance: '2346534676900000008',
compiler_version: 'v0.8.17+commit.8df45f5f',
@@ -31,6 +32,7 @@ export const contract2: VerifiedContract = {
private_tags: [],
public_tags: [],
watchlist_names: [],
ens_domain_name: null,
},
coin_balance: '9078234570352343999',
compiler_version: 'v0.3.1+commit.0463ea4c',
+91
View File
@@ -0,0 +1,91 @@
import type { EnsDomainDetailed } from 'types/api/ens';
const domainTokenA = {
id: '97352314626701792030827861137068748433918254309635329404916858191911576754327',
contract_hash: '0x57f1887a8bf19b14fc0df6fd9b2acc9af147ea85',
type: 'NATIVE_DOMAIN_TOKEN' as const,
};
const domainTokenB = {
id: '423546333',
contract_hash: '0x57f1887a8bf19b14fc0df6fd9b2acc9af147ea86',
type: 'WRAPPED_DOMAIN_TOKEN' as const,
};
export const ensDomainA: EnsDomainDetailed = {
id: '0xb140bf9645e54f02ed3c1bcc225566b515a98d1688f10494a5c3bc5b447936a7',
tokens: [
domainTokenA,
domainTokenB,
],
name: 'cat.eth',
registrant: {
hash: '0x114d4603199df73e7d157787f8778e21fcd13066',
},
resolved_address: {
hash: '0xfe6ab8a0dafe7d41adf247c210451c264155c9b0',
},
owner: {
hash: '0x114d4603199df73e7d157787f8778e21fcd13066',
},
wrapped_owner: null,
registration_date: '2021-06-27T13:34:44.000Z',
expiry_date: '2025-03-01T14:20:24.000Z',
other_addresses: {
ETH: 'fe6ab8a0dafe7d41adf247c210451c264155c9b0',
GNO: 'DDAfbb505ad214D7b80b1f830fcCc89B60fb7A83',
NEAR: 'a0b86991c6218b36c1d19d4a2e9eb0ce3606eb48.factory.bridge.near',
},
};
export const ensDomainB: EnsDomainDetailed = {
id: '0x632ac7bec8e883416b371b36beaa822f4784208c99d063ee030020e2bd09b885',
tokens: [ domainTokenA ],
name: 'kitty.kitty.kitty.cat.eth',
resolved_address: null,
registrant: {
hash: '0x114d4603199df73e7d157787f8778e21fcd13066',
},
owner: {
hash: '0x114d4603199df73e7d157787f8778e21fcd13066',
},
wrapped_owner: null,
registration_date: '2023-08-13T13:01:12.000Z',
expiry_date: null,
other_addresses: {},
};
export const ensDomainC: EnsDomainDetailed = {
id: '0xdb7f351de6d93bda077a9211bdc49f249326d87932e4787d109b0262e9d189ad',
tokens: [ domainTokenA ],
name: 'duck.duck.eth',
registrant: {
hash: '0x114d4603199df73e7d157787f8778e21fcd13066',
},
resolved_address: {
hash: '0xfe6ab8a0dafe7d41adf247c210451c264155c9b0',
},
owner: {
hash: '0x114d4603199df73e7d157787f8778e21fcd13066',
},
wrapped_owner: null,
registration_date: '2022-04-24T07:34:44.000Z',
expiry_date: '2022-11-01T13:10:36.000Z',
other_addresses: {},
};
export const ensDomainD: EnsDomainDetailed = {
id: '0xdb7f351de6d93bda077a9211bdc49f249326d87932e4787d109b0262e9d189ae',
tokens: [ domainTokenA ],
name: '🦆.duck.eth',
registrant: {
hash: '0x114d4603199df73e7d157787f8778e21fcd13066',
},
resolved_address: {
hash: '0x114d4603199df73e7d157787f8778e21fcd13066',
},
owner: null,
wrapped_owner: null,
registration_date: '2022-04-24T07:34:44.000Z',
expiry_date: '2027-09-23T13:10:36.000Z',
other_addresses: {},
};
+19
View File
@@ -0,0 +1,19 @@
import type { EnsDomainEvent } from 'types/api/ens';
export const ensDomainEventA: EnsDomainEvent = {
transaction_hash: '0x86c66b9fad66e4f20d42a6eed4fe12a0f48a274786fd85e9d4aa6c60e84b5874',
timestamp: '2021-06-27T13:34:44.000000Z',
from_address: {
hash: '0xaa96a50a2f67111262fe24576bd85bb56ec65016',
},
action: '0xf7a16963',
};
export const ensDomainEventB = {
transaction_hash: '0x150bf7d5cd42457dd9c799ddd9d4bf6c30c703e1954a88c6d4b668b23fe0fbf8',
timestamp: '2022-11-02T14:20:24.000000Z',
from_address: {
hash: '0xfe6ab8a0dafe7d41adf247c210451c264155c9b0',
},
action: 'register',
};
+23
View File
@@ -47,6 +47,15 @@ export const block2: SearchResultBlock = {
url: '/block/0x1af31d7535dded06bab9a88eb40ee2f8d0529a60ab3b8a7be2ba69b008cacbd2',
};
export const block3: SearchResultBlock = {
block_hash: '0x1af31d7535dded06bab9a88eb40ee2f8d0529a60ab3b8a7be2ba69b008cacbd3',
block_number: 8198536,
block_type: 'uncle',
type: 'block' as const,
timestamp: '2022-12-11T18:11:11Z',
url: '/block/0x1af31d7535dded06bab9a88eb40ee2f8d0529a60ab3b8a7be2ba69b008cacbd3',
};
export const address1: SearchResultAddressOrContract = {
address: '0xb64a30399f7F6b0C154c2E7Af0a3ec7B0A5b131a',
name: null,
@@ -55,6 +64,20 @@ export const address1: SearchResultAddressOrContract = {
url: '/address/0xb64a30399f7F6b0C154c2E7Af0a3ec7B0A5b131a',
};
export const address2: SearchResultAddressOrContract = {
address: '0xb64a30399f7F6b0C154c2E7Af0a3ec7B0A5b131b',
name: null,
type: 'address' as const,
is_smart_contract_verified: false,
url: '/address/0xb64a30399f7F6b0C154c2E7Af0a3ec7B0A5b131b',
ens_info: {
address_hash: '0x1234567890123456789012345678901234567890',
expiry_date: '2022-12-11T17:55:20Z',
name: 'utko.eth',
names_count: 1,
},
};
export const contract1: SearchResultAddressOrContract = {
address: '0xb64a30399f7F6b0C154c2E7Af0a3ec7B0A5b131a',
name: 'Unknown contract in this network',
+18 -3
View File
@@ -3,11 +3,26 @@ import type { HomeStats } from 'types/api/stats';
export const base: HomeStats = {
average_block_time: 6212.0,
coin_price: '0.00199678',
coin_price_change_percentage: -7.42,
gas_prices: {
average: 48.0,
fast: 67.5,
slow: 48.0,
average: {
fiat_price: '1.01',
price: 20.41,
time: 12283,
},
fast: {
fiat_price: '1.26',
price: 25.47,
time: 9321,
},
slow: {
fiat_price: '0.97',
price: 19.55,
time: 24543,
},
},
gas_price_updated_at: '2022-11-11T11:09:49.051171Z',
gas_prices_update_in: 300000,
gas_used_today: '4108680603',
market_cap: '330809.96443288102524',
network_utilization_percentage: 1.55372064,
+6
View File
@@ -10,6 +10,7 @@ export const erc20: TokenTransfer = {
private_tags: [],
public_tags: [],
watchlist_names: [],
ens_domain_name: null,
},
to: {
hash: '0x7d20a8D54F955b4483A66aB335635ab66e151c51',
@@ -20,6 +21,7 @@ export const erc20: TokenTransfer = {
private_tags: [],
public_tags: [],
watchlist_names: [],
ens_domain_name: 'kitty.kitty.cat.eth',
},
token: {
address: '0x55d536e4d6c1993d8ef2e2a4ef77f02088419420',
@@ -55,6 +57,7 @@ export const erc721: TokenTransfer = {
private_tags: [],
public_tags: [],
watchlist_names: [],
ens_domain_name: 'kitty.kitty.cat.eth',
},
to: {
hash: '0x47eE48AEBc4ab9Ed908b805b8c8dAAa71B31Db1A',
@@ -65,6 +68,7 @@ export const erc721: TokenTransfer = {
private_tags: [],
public_tags: [],
watchlist_names: [],
ens_domain_name: null,
},
token: {
address: '0x363574E6C5C71c343d7348093D84320c76d5Dd29',
@@ -99,6 +103,7 @@ export const erc1155A: TokenTransfer = {
private_tags: [],
public_tags: [],
watchlist_names: [],
ens_domain_name: null,
},
to: {
hash: '0xBb36c792B9B45Aaf8b848A1392B0d6559202729E',
@@ -109,6 +114,7 @@ export const erc1155A: TokenTransfer = {
private_tags: [],
public_tags: [],
watchlist_names: [],
ens_domain_name: 'kitty.kitty.cat.eth',
},
token: {
address: '0xF56b7693E4212C584de4a83117f805B8E89224CB',
+3
View File
@@ -13,6 +13,7 @@ export const base: InternalTransaction = {
private_tags: [],
public_tags: [],
watchlist_names: [],
ens_domain_name: null,
},
gas_limit: '757586',
index: 1,
@@ -27,6 +28,7 @@ export const base: InternalTransaction = {
private_tags: [],
public_tags: [],
watchlist_names: [],
ens_domain_name: null,
},
transaction_hash: '0xe9e27dfeb183066e26cfe556f74b7219b08df6951e25d14003d4fc7af8bbff61',
type: 'call',
@@ -61,6 +63,7 @@ export const withContractCreated: InternalTransaction = {
private_tags: [],
public_tags: [],
watchlist_names: [],
ens_domain_name: null,
},
value: '1420000000000000000',
gas_limit: '5433',
+5
View File
@@ -10,6 +10,7 @@ export const mintToken: TxStateChange = {
private_tags: [],
public_tags: [],
watchlist_names: [],
ens_domain_name: null,
},
balance_after: null,
balance_before: null,
@@ -47,6 +48,7 @@ export const receiveMintedToken: TxStateChange = {
private_tags: [],
public_tags: [],
watchlist_names: [],
ens_domain_name: null,
},
balance_after: '1',
balance_before: '0',
@@ -84,6 +86,7 @@ export const transfer1155Token: TxStateChange = {
private_tags: [],
public_tags: [],
watchlist_names: [],
ens_domain_name: null,
},
balance_after: '1',
balance_before: '0',
@@ -115,6 +118,7 @@ export const receiveCoin: TxStateChange = {
private_tags: [],
public_tags: [],
watchlist_names: [],
ens_domain_name: null,
},
balance_after: '443787514723917012805',
balance_before: '443787484997510408745',
@@ -134,6 +138,7 @@ export const sendCoin: TxStateChange = {
private_tags: [],
public_tags: [],
watchlist_names: [],
ens_domain_name: null,
},
balance_after: '828282622733717191',
balance_before: '832127467556437753',
+7
View File
@@ -29,6 +29,7 @@ export const base: Transaction = {
private_tags: [ ],
public_tags: [ publicTag ],
watchlist_names: [],
ens_domain_name: 'kitty.kitty.cat.eth',
},
gas_limit: '800000',
gas_price: '48000000000',
@@ -54,6 +55,7 @@ export const base: Transaction = {
private_tags: [ privateTag ],
public_tags: [],
watchlist_names: [ watchlistName ],
ens_domain_name: null,
},
token_transfers: [],
token_transfers_overflow: false,
@@ -97,6 +99,7 @@ export const withContractCreation: Transaction = {
private_tags: [],
public_tags: [],
watchlist_names: [],
ens_domain_name: null,
},
tx_types: [
'contract_creation',
@@ -115,6 +118,7 @@ export const withTokenTransfer: Transaction = {
private_tags: [ privateTag ],
public_tags: [],
watchlist_names: [ watchlistName ],
ens_domain_name: null,
},
token_transfers: [
tokenTransferMock.erc20,
@@ -168,6 +172,7 @@ export const withRawRevertReason: Transaction = {
private_tags: [ ],
public_tags: [],
watchlist_names: [ ],
ens_domain_name: null,
},
};
@@ -283,6 +288,7 @@ export const stabilityTx: Transaction = {
private_tags: [],
public_tags: [],
watchlist_names: [],
ens_domain_name: null,
},
dapp_fee: '34381250000000',
token: {
@@ -307,6 +313,7 @@ export const stabilityTx: Transaction = {
private_tags: [],
public_tags: [],
watchlist_names: [],
ens_domain_name: null,
},
validator_fee: '34381250000000',
},
+1
View File
@@ -33,6 +33,7 @@ export const txInterpretation: TxInterpretationResponse = {
private_tags: [],
public_tags: [],
watchlist_names: [],
ens_domain_name: null,
},
},
timestamp: {
+12
View File
@@ -10,6 +10,7 @@ export type Props = {
hash: string;
number: string;
q: string;
name: string;
}
export const base: GetServerSideProps<Props> = async({ req, query }) => {
@@ -22,6 +23,7 @@ export const base: GetServerSideProps<Props> = async({ req, query }) => {
height_or_hash: query.height_or_hash?.toString() || '',
number: query.number?.toString() || '',
q: query.q?.toString() || '',
name: query.name?.toString() || '',
},
};
};
@@ -126,6 +128,16 @@ export const suave: GetServerSideProps<Props> = async(context) => {
return base(context);
};
export const nameService: GetServerSideProps<Props> = async(context) => {
if (!config.features.nameService.isEnabled) {
return {
notFound: true,
};
}
return base(context);
};
export const accounts: GetServerSideProps<Props> = async(context) => {
if (config.UI.views.address.hiddenViews?.top_accounts) {
return {
+2
View File
@@ -37,6 +37,8 @@ declare module "nextjs-routes" {
| StaticRoute<"/l2-txn-batches">
| StaticRoute<"/l2-withdrawals">
| StaticRoute<"/login">
| DynamicRoute<"/name-domains/[name]", { "name": string }>
| StaticRoute<"/name-domains">
| StaticRoute<"/search-results">
| StaticRoute<"/stats">
| DynamicRoute<"/token/[hash]", { "hash": string }>
+7 -4
View File
@@ -1,4 +1,5 @@
import type { IncomingMessage } from 'http';
import _pick from 'lodash/pick';
import type { NextApiRequest } from 'next';
import type { NextApiRequestCookies } from 'next/dist/server/api-utils';
import type { RequestInit, Response } from 'node-fetch';
@@ -14,16 +15,18 @@ export default function fetchFactory(
// first arg can be only a string
// FIXME migrate to RequestInfo later if needed
return function fetch(url: string, init?: RequestInit): Promise<Response> {
const csrfToken = _req.headers['x-csrf-token'];
const authToken = _req.headers['Authorization'];
const apiToken = _req.cookies[cookies.NAMES.API_TOKEN];
const headers = {
accept: _req.headers['accept'] || 'application/json',
'content-type': _req.headers['content-type'] || 'application/json',
cookie: apiToken ? `${ cookies.NAMES.API_TOKEN }=${ apiToken }` : '',
...(csrfToken ? { 'x-csrf-token': String(csrfToken) } : {}),
...(authToken ? { Authorization: String(authToken) } : {}),
..._pick(_req.headers, [
'x-csrf-token',
'Authorization',
// feature flags
'updated-gas-oracle',
]) as Record<string, string | undefined>,
};
httpLogger.logger.info({
+1
View File
@@ -90,6 +90,7 @@
"react-identicons": "^1.2.5",
"react-intersection-observer": "^9.5.2",
"react-jazzicon": "^1.0.4",
"react-number-format": "^5.3.1",
"react-scroll": "^1.8.7",
"swagger-ui-react": "^5.9.0",
"use-font-face-observer": "^1.2.1",
+20
View File
@@ -0,0 +1,20 @@
import type { NextPage } from 'next';
import dynamic from 'next/dynamic';
import React from 'react';
import type { Props } from 'nextjs/getServerSideProps';
import PageNextJs from 'nextjs/PageNextJs';
const NameDomain = dynamic(() => import('ui/pages/NameDomain'), { ssr: false });
const Page: NextPage<Props> = (props: Props) => {
return (
<PageNextJs pathname="/name-domains/[name]" query={ props }>
<NameDomain/>
</PageNextJs>
);
};
export default Page;
export { nameService as getServerSideProps } from 'nextjs/getServerSideProps';
+19
View File
@@ -0,0 +1,19 @@
import type { NextPage } from 'next';
import dynamic from 'next/dynamic';
import React from 'react';
import PageNextJs from 'nextjs/PageNextJs';
const NameDomains = dynamic(() => import('ui/pages/NameDomains'), { ssr: false });
const Page: NextPage = () => {
return (
<PageNextJs pathname="/name-domains">
<NameDomains/>
</PageNextJs>
);
};
export default Page;
export { nameService as getServerSideProps } from 'nextjs/getServerSideProps';
+1
View File
@@ -30,6 +30,7 @@ const defaultAppContext = {
hash: '',
number: '',
q: '',
name: '',
},
};
+2
View File
@@ -31,6 +31,8 @@
| "email-sent"
| "email"
| "empty_search_result"
| "ENS_slim"
| "ENS"
| "error-pages/404"
| "error-pages/422"
| "error-pages/429"
+32
View File
@@ -0,0 +1,32 @@
import type { EnsDomainDetailed, EnsDomainEvent } from 'types/api/ens';
import { ADDRESS_PARAMS, ADDRESS_HASH } from './addressParams';
import { TX_HASH } from './tx';
export const ENS_DOMAIN: EnsDomainDetailed = {
id: '0x126d74db13895f8d3a1d362410212731d1e1d9be8add83e388385f93d84c8c84',
name: 'kitty.cat.eth',
tokens: [
{
id: '973523146267017920308',
contract_hash: ADDRESS_HASH,
type: 'NATIVE_DOMAIN_TOKEN',
},
],
owner: ADDRESS_PARAMS,
wrapped_owner: null,
resolved_address: ADDRESS_PARAMS,
registrant: ADDRESS_PARAMS,
registration_date: '2023-12-20T01:29:12.000Z',
expiry_date: '2099-01-02T01:29:12.000Z',
other_addresses: {
ETH: ADDRESS_HASH,
},
};
export const ENS_DOMAIN_EVENT: EnsDomainEvent = {
transaction_hash: TX_HASH,
timestamp: '2022-06-06T08:43:15.000000Z',
from_address: ADDRESS_PARAMS,
action: '0xf7a16963',
};
+2
View File
@@ -41,6 +41,7 @@ export const ADDRESS_INFO: Address = {
public_tags: [],
watchlist_names: [],
watchlist_address_id: null,
ens_domain_name: null,
};
export const ADDRESS_COUNTERS: AddressCounters = {
@@ -71,6 +72,7 @@ export const TOP_ADDRESS: AddressesItem = {
private_tags: [],
public_tags: [ ],
watchlist_names: [],
ens_domain_name: null,
};
export const ADDRESS_COIN_BALANCE: AddressCoinBalanceHistoryItem = {
+1
View File
@@ -11,4 +11,5 @@ export const ADDRESS_PARAMS: AddressParam = {
private_tags: [],
public_tags: [],
watchlist_names: [],
ens_domain_name: null,
};
+18 -3
View File
@@ -3,11 +3,26 @@ import type { Counter, HomeStats, StatsChartsSection } from 'types/api/stats';
export const HOMEPAGE_STATS: HomeStats = {
average_block_time: 14346,
coin_price: '1807.68',
coin_price_change_percentage: 42,
gas_prices: {
average: 0.1,
fast: 0.11,
slow: 0.1,
average: {
fiat_price: '1.01',
price: 20.41,
time: 12283,
},
fast: {
fiat_price: '1.26',
price: 25.47,
time: 9321,
},
slow: {
fiat_price: '0.97',
price: 19.55,
time: 24543,
},
},
gas_price_updated_at: '2022-11-11T11:09:49.051171Z',
gas_prices_update_in: 300000,
gas_used_today: '0',
market_cap: '0',
network_utilization_percentage: 22.56,
+1 -1
View File
@@ -13,7 +13,7 @@ function getBg(props: StyleFunctionProps) {
const { theme, colorScheme: c } = props;
const darkBg = transparentize(`${ c }.200`, 0.16)(theme);
return {
light: `colors.${ c }.100`,
light: `colors.${ c }.${ c === 'red' ? '50' : '100' }`,
dark: darkBg,
};
}
+1
View File
@@ -12,6 +12,7 @@ export interface Address extends UserTags {
creator_address_hash: string | null;
creation_tx_hash: string | null;
exchange_rate: string | null;
ens_domain_name: string | null;
// TODO: if we are happy with tabs-counters method, should we delete has_something fields?
has_beacon_chain_withdrawals?: boolean;
has_custom_methods_read: boolean;
+1
View File
@@ -21,4 +21,5 @@ export interface AddressParam extends UserTags {
name: string | null;
is_contract: boolean;
is_verified: boolean | null;
ens_domain_name: string | null;
}
+4 -2
View File
@@ -1,6 +1,6 @@
import type { Abi } from 'abitype';
import type { Abi, AbiType } from 'abitype';
export type SmartContractMethodArgType = 'address' | 'uint256' | 'bool' | 'string' | 'bytes' | 'bytes32' | 'bytes32[]';
export type SmartContractMethodArgType = AbiType;
export type SmartContractMethodStateMutability = 'view' | 'nonpayable' | 'payable';
export interface SmartContract {
@@ -88,6 +88,8 @@ export interface SmartContractMethodInput {
internalType?: SmartContractMethodArgType;
name: string;
type: SmartContractMethodArgType;
components?: Array<SmartContractMethodInput>;
fieldType?: 'native_coin';
}
export interface SmartContractMethodOutput extends SmartContractMethodInput {
+71
View File
@@ -0,0 +1,71 @@
export interface EnsDomain {
id: string;
name: string;
resolved_address: {
hash: string;
} | null;
owner: {
hash: string;
} | null;
wrapped_owner: {
hash: string;
} | null;
registration_date?: string;
expiry_date: string | null;
}
export interface EnsDomainDetailed extends EnsDomain {
tokens: Array<{ id: string; contract_hash: string; type: 'NATIVE_DOMAIN_TOKEN' | 'WRAPPED_DOMAIN_TOKEN' }>;
registrant: {
hash: string;
} | null;
other_addresses: Record<string, string>;
}
export interface EnsDomainEvent {
transaction_hash: string;
timestamp: string;
from_address: {
hash: string;
} | null;
action?: string;
}
export interface EnsAddressLookupResponse {
items: Array<EnsDomain>;
next_page_params: {
page_token: string;
page_size: number;
} | null;
}
export interface EnsDomainEventsResponse {
items: Array<EnsDomainEvent>;
}
export interface EnsDomainLookupResponse {
items: Array<EnsDomain>;
next_page_params: {
page_token: string;
page_size: number;
} | null;
}
export interface EnsAddressLookupFilters {
address: string | null;
resolved_to: boolean;
owned_by: boolean;
only_active: boolean;
}
export interface EnsDomainLookupFilters {
name: string | null;
only_active: boolean;
}
export interface EnsLookupSorting {
sort: 'registration_date';
order: 'ASC' | 'DESC';
}
export type EnsDomainLookupFiltersOptions = Array<'resolved_to' | 'owned_by' | 'with_inactive'>;
+7 -1
View File
@@ -23,6 +23,12 @@ export interface SearchResultAddressOrContract {
address: string;
is_smart_contract_verified: boolean;
url?: string; // not used by the frontend, we build the url ourselves
ens_info?: {
address_hash: string;
expiry_date?: string;
name: string;
names_count: number;
};
}
export interface SearchResultLabel {
@@ -35,7 +41,7 @@ export interface SearchResultLabel {
export interface SearchResultBlock {
type: 'block';
block_type?: 'block' | 'reorg';
block_type?: 'block' | 'reorg' | 'uncle';
block_number: number | string;
block_hash: string;
timestamp: string;
+12 -3
View File
@@ -4,10 +4,13 @@ export type HomeStats = {
total_transactions: string;
average_block_time: number;
coin_price: string | null;
coin_price_change_percentage: number | null; // e.g -6.22
total_gas_used: string;
transactions_today: string;
gas_used_today: string;
gas_prices: GasPrices | null;
gas_price_updated_at: string | null;
gas_prices_update_in: number;
static_gas_price: string | null;
market_cap: string;
network_utilization_percentage: number;
@@ -16,9 +19,15 @@ export type HomeStats = {
}
export type GasPrices = {
average: number | null;
fast: number | null;
slow: number | null;
average: GasPriceInfo | null;
fast: GasPriceInfo | null;
slow: GasPriceInfo | null;
}
export interface GasPriceInfo {
fiat_price: string | null;
price: number | null;
time: number | null;
}
export type Counters = {
+7 -2
View File
@@ -14,6 +14,12 @@ export type TransactionRevertReason = {
type WrappedTransactionFields = 'decoded_input' | 'fee' | 'gas_limit' | 'gas_price' | 'hash' | 'max_fee_per_gas' |
'max_priority_fee_per_gas' | 'method' | 'nonce' | 'raw_input' | 'to' | 'type' | 'value';
export interface OpWithdrawal {
l1_transaction_hash: string;
nonce: number;
status: L2WithdrawalStatus;
}
export type Transaction = {
to: AddressParam | null;
created_contract: AddressParam | null;
@@ -54,8 +60,7 @@ export type Transaction = {
l1_gas_used?: string;
has_error_in_internal_txs: boolean | null;
// optimism fields
op_withdrawal_status?: L2WithdrawalStatus;
op_l1_transaction_hash?: string;
op_withdrawals?: Array<OpWithdrawal>;
// SUAVE fields
execution_node?: AddressParam | null;
allowed_peekers?: Array<string>;
Binary file not shown.

Before

Width:  |  Height:  |  Size: 22 KiB

After

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 24 KiB

After

Width:  |  Height:  |  Size: 23 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 29 KiB

After

Width:  |  Height:  |  Size: 28 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 31 KiB

After

Width:  |  Height:  |  Size: 30 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 52 KiB

After

Width:  |  Height:  |  Size: 52 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 55 KiB

After

Width:  |  Height:  |  Size: 54 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 53 KiB

After

Width:  |  Height:  |  Size: 51 KiB

+92 -84
View File
@@ -1,16 +1,17 @@
import { Box, Button, chakra, Flex } from '@chakra-ui/react';
import _fromPairs from 'lodash/fromPairs';
import React from 'react';
import type { SubmitHandler } from 'react-hook-form';
import { useForm } from 'react-hook-form';
import { useForm, FormProvider } from 'react-hook-form';
import type { MethodFormFields, ContractMethodCallResult } from './types';
import type { SmartContractMethodInput, SmartContractMethod } from 'types/api/contract';
import config from 'configs/app';
import * as mixpanel from 'lib/mixpanel/index';
import IconSvg from 'ui/shared/IconSvg';
import ContractMethodField from './ContractMethodField';
import ContractMethodCallableRow from './ContractMethodCallableRow';
import { formatFieldValues, transformFieldsToArgs } from './utils';
interface ResultComponentProps<T extends SmartContractMethod> {
item: T;
@@ -25,42 +26,9 @@ interface Props<T extends SmartContractMethod> {
isWrite?: boolean;
}
const getFieldName = (name: string | undefined, index: number): string => name || String(index);
const sortFields = (data: Array<SmartContractMethodInput>) => ([ a ]: [string, string], [ b ]: [string, string]): 1 | -1 | 0 => {
const fieldNames = data.map(({ name }, index) => getFieldName(name, index));
const indexA = fieldNames.indexOf(a);
const indexB = fieldNames.indexOf(b);
if (indexA > indexB) {
return 1;
}
if (indexA < indexB) {
return -1;
}
return 0;
};
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) => {
try {
const parsedResult = JSON.parse(value);
if (Array.isArray(parsedResult)) {
return parsedResult;
}
throw new Error('Not an array');
} catch (error) {
return '';
}
};
// groupName%groupIndex:inputName%inputIndex
const getFormFieldName = (input: { index: number; name: string }, group?: { index: number; name: string }) =>
`${ group ? `${ group.name }%${ group.index }:` : '' }${ input.name || 'input' }%${ input.index }`;
const ContractMethodCallable = <T extends SmartContractMethod>({ data, onSubmit, resultComponent: ResultComponent, isWrite }: Props<T>) => {
@@ -71,15 +39,16 @@ const ContractMethodCallable = <T extends SmartContractMethod>({ data, onSubmit,
return [
...('inputs' in data ? data.inputs : []),
...('stateMutability' in data && data.stateMutability === 'payable' ? [ {
name: 'value',
name: `Send native ${ config.chain.currency.symbol }`,
type: 'uint256' as const,
internalType: 'uint256' as const,
fieldType: 'native_coin' as const,
} ] : []),
];
}, [ data ]);
const { control, handleSubmit, setValue, getValues } = useForm<MethodFormFields>({
defaultValues: _fromPairs(inputs.map(({ name }, index) => [ getFieldName(name, index), '' ])),
const formApi = useForm<MethodFormFields>({
mode: 'onBlur',
});
const handleTxSettle = React.useCallback(() => {
@@ -91,10 +60,8 @@ const ContractMethodCallable = <T extends SmartContractMethod>({ data, onSubmit,
}, [ result ]);
const onFormSubmit: SubmitHandler<MethodFormFields> = React.useCallback(async(formData) => {
const args = Object.entries(formData)
.sort(sortFields(inputs))
.map(castFieldValue(inputs))
.map(([ , value ]) => value);
const formattedData = formatFieldValues(formData, inputs);
const args = transformFieldsToArgs(formattedData);
setResult(undefined);
setLoading(true);
@@ -117,46 +84,87 @@ const ContractMethodCallable = <T extends SmartContractMethod>({ data, onSubmit,
return (
<Box>
<chakra.form
noValidate
display="flex"
columnGap={ 3 }
flexDir={{ base: 'column', lg: 'row' }}
rowGap={ 2 }
alignItems={{ base: 'flex-start', lg: 'center' }}
onSubmit={ handleSubmit(onFormSubmit) }
flexWrap="wrap"
onChange={ handleFormChange }
>
{ inputs.map(({ type, name }, index) => {
const fieldName = getFieldName(name, index);
return (
<ContractMethodField
key={ fieldName }
name={ fieldName }
valueType={ type }
placeholder={ `${ name }(${ type })` }
control={ control }
setValue={ setValue }
getValues={ getValues }
isDisabled={ isLoading }
onChange={ handleFormChange }
/>
);
}) }
<Button
isLoading={ isLoading }
loadingText={ isWrite ? 'Write' : 'Query' }
variant="outline"
size="sm"
flexShrink={ 0 }
type="submit"
<FormProvider { ...formApi }>
<chakra.form
noValidate
onSubmit={ formApi.handleSubmit(onFormSubmit) }
onChange={ handleFormChange }
>
{ isWrite ? 'Write' : 'Query' }
</Button>
</chakra.form>
<Flex
flexDir="column"
rowGap={ 3 }
mb={ 3 }
_empty={{ display: 'none' }}
>
{ inputs.map((input, index) => {
const fieldName = getFormFieldName({ name: input.name, index });
if (input.type === 'tuple' && input.components) {
return (
<React.Fragment key={ fieldName }>
{ index !== 0 && <><Box h={{ base: 0, lg: 3 }}/><div/></> }
<Box
fontWeight={ 500 }
lineHeight="20px"
py={{ lg: '6px' }}
fontSize="sm"
wordBreak="break-word"
>
{ input.name } ({ input.type })
</Box>
{ input.components.map((component, componentIndex) => {
const fieldName = getFormFieldName(
{ name: component.name, index: componentIndex },
{ name: input.name, index },
);
return (
<ContractMethodCallableRow
key={ fieldName }
fieldName={ fieldName }
argName={ component.name }
argType={ component.type }
isDisabled={ isLoading }
onChange={ handleFormChange }
isGrouped
/>
);
}) }
{ index !== inputs.length - 1 && <><Box h={{ base: 0, lg: 3 }}/><div/></> }
</React.Fragment>
);
}
return (
<ContractMethodCallableRow
key={ fieldName }
fieldName={ fieldName }
fieldType={ input.fieldType }
argName={ input.name }
argType={ input.type }
isDisabled={ isLoading }
isOptional={ input.fieldType === 'native_coin' && inputs.length > 1 }
onChange={ handleFormChange }
/>
);
}) }
</Flex>
<Button
isLoading={ isLoading }
loadingText={ isWrite ? 'Write' : 'Read' }
variant="outline"
size="sm"
flexShrink={ 0 }
width="min-content"
px={ 4 }
type="submit"
>
{ isWrite ? 'Write' : 'Read' }
</Button>
</chakra.form>
</FormProvider>
{ 'outputs' in data && !isWrite && data.outputs.length > 0 && (
<Flex mt={ 3 }>
<Flex mt={ 3 } fontSize="sm">
<IconSvg name="arrows/down-right" boxSize={ 5 } mr={ 1 }/>
<p>
{ data.outputs.map(({ type, name }, index) => {
@@ -0,0 +1,84 @@
import { Box, Flex, useColorModeValue } from '@chakra-ui/react';
import React from 'react';
import { useFormContext } from 'react-hook-form';
import type { MethodFormFields } from './types';
import type { SmartContractMethodArgType, SmartContractMethodInput } from 'types/api/contract';
import ContractMethodField from './ContractMethodField';
import ContractMethodFieldArray from './ContractMethodFieldArray';
import { ARRAY_REGEXP } from './utils';
interface Props {
fieldName: string;
fieldType?: SmartContractMethodInput['fieldType'];
argName: string;
argType: SmartContractMethodArgType;
onChange: () => void;
isDisabled: boolean;
isGrouped?: boolean;
isOptional?: boolean;
}
const ContractMethodCallableRow = ({ argName, fieldName, fieldType, argType, onChange, isDisabled, isGrouped, isOptional }: Props) => {
const { control, getValues, setValue } = useFormContext<MethodFormFields>();
const arrayTypeMatch = argType.match(ARRAY_REGEXP);
const nativeCoinFieldBgColor = useColorModeValue('blackAlpha.50', 'whiteAlpha.100');
const content = arrayTypeMatch ? (
<ContractMethodFieldArray
name={ fieldName }
argType={ arrayTypeMatch[1] as SmartContractMethodArgType }
size={ Number(arrayTypeMatch[2] || Infinity) }
control={ control }
setValue={ setValue }
getValues={ getValues }
isDisabled={ isDisabled }
onChange={ onChange }
/>
) : (
<ContractMethodField
name={ fieldName }
argType={ argType }
placeholder={ argType }
control={ control }
setValue={ setValue }
getValues={ getValues }
isDisabled={ isDisabled }
isOptional={ isOptional }
onChange={ onChange }
/>
);
const isNativeCoinField = fieldType === 'native_coin';
return (
<Flex
flexDir={{ base: 'column', lg: 'row' }}
columnGap={ 3 }
rowGap={{ base: 2, lg: 0 }}
bgColor={ isNativeCoinField ? nativeCoinFieldBgColor : undefined }
py={ isNativeCoinField ? 1 : undefined }
px={ isNativeCoinField ? '6px' : undefined }
mx={ isNativeCoinField ? '-6px' : undefined }
borderRadius="base"
>
<Box
position="relative"
fontWeight={ 500 }
lineHeight="20px"
py={{ lg: '6px' }}
fontSize="sm"
color={ isGrouped ? 'text_secondary' : 'initial' }
wordBreak="break-word"
w={{ lg: '250px' }}
flexShrink={ 0 }
>
{ argName }{ isOptional ? '' : '*' } ({ argType })
</Box>
{ content }
</Flex>
);
};
export default React.memo(ContractMethodCallableRow);
+140 -32
View File
@@ -1,12 +1,16 @@
import {
Box,
FormControl,
Input,
InputGroup,
InputRightElement,
useColorModeValue,
} from '@chakra-ui/react';
import React from 'react';
import type { Control, ControllerRenderProps, UseFormGetValues, UseFormSetValue } from 'react-hook-form';
import type { Control, ControllerRenderProps, FieldError, UseFormGetValues, UseFormSetValue, UseFormStateReturn } from 'react-hook-form';
import { Controller } from 'react-hook-form';
import { NumericFormat } from 'react-number-format';
import { isAddress, isHex, getAddress } from 'viem';
import type { MethodFormFields } from './types';
import type { SmartContractMethodArgType } from 'types/api/contract';
@@ -14,21 +18,25 @@ import type { SmartContractMethodArgType } from 'types/api/contract';
import ClearButton from 'ui/shared/ClearButton';
import ContractMethodFieldZeroes from './ContractMethodFieldZeroes';
import { addZeroesAllowed } from './utils';
import { INT_REGEXP, BYTES_REGEXP, getIntBoundaries, formatBooleanValue } from './utils';
interface Props {
name: string;
index?: number;
groupName?: string;
placeholder: string;
argType: SmartContractMethodArgType;
control: Control<MethodFormFields>;
setValue: UseFormSetValue<MethodFormFields>;
getValues: UseFormGetValues<MethodFormFields>;
placeholder: string;
name: string;
valueType: SmartContractMethodArgType;
isDisabled: boolean;
isOptional?: boolean;
onChange: () => void;
}
const ContractMethodField = ({ control, name, valueType, placeholder, setValue, getValues, isDisabled, onChange }: Props) => {
const ContractMethodField = ({ control, name, groupName, index, argType, placeholder, setValue, getValues, isDisabled, isOptional, onChange }: Props) => {
const ref = React.useRef<HTMLInputElement>(null);
const bgColor = useColorModeValue('white', 'black');
const handleClear = React.useCallback(() => {
setValue(name, '');
@@ -37,47 +45,147 @@ const ContractMethodField = ({ control, name, valueType, placeholder, setValue,
}, [ name, onChange, setValue ]);
const handleAddZeroesClick = React.useCallback((power: number) => {
const value = getValues()[name];
const value = groupName && index !== undefined ? getValues()[groupName][index] : getValues()[name];
const zeroes = Array(power).fill('0').join('');
const newValue = value ? value + zeroes : '1' + zeroes;
setValue(name, newValue);
onChange();
}, [ getValues, name, onChange, setValue ]);
}, [ getValues, groupName, index, name, onChange, setValue ]);
const hasZerosControl = addZeroesAllowed(valueType);
const intMatch = React.useMemo(() => {
const match = argType.match(INT_REGEXP);
if (!match) {
return null;
}
const [ , isUnsigned, power = '256' ] = match;
const [ min, max ] = getIntBoundaries(Number(power), Boolean(isUnsigned));
return { isUnsigned, power, min, max };
}, [ argType ]);
const bytesMatch = React.useMemo(() => {
return argType.match(BYTES_REGEXP);
}, [ argType ]);
const renderInput = React.useCallback((
{ field, formState }: { field: ControllerRenderProps<MethodFormFields>; formState: UseFormStateReturn<MethodFormFields> },
) => {
const error: FieldError | undefined = index !== undefined && groupName !== undefined ?
(formState.errors[groupName] as unknown as Array<FieldError>)?.[index] :
formState.errors[name];
// show control for all inputs which allows to insert 10^18 or greater numbers
const hasZerosControl = intMatch && Number(intMatch.power) >= 64;
const renderInput = React.useCallback(({ field }: { field: ControllerRenderProps<MethodFormFields> }) => {
return (
<FormControl
id={ name }
flexBasis={{ base: '100%', lg: 'calc((100% - 24px) / 3 - 65px)' }}
w={{ base: '100%', lg: 'auto' }}
flexGrow={ 1 }
isDisabled={ isDisabled }
>
<InputGroup size="xs">
<Input
{ ...field }
ref={ ref }
placeholder={ placeholder }
paddingRight={ hasZerosControl ? '120px' : '40px' }
/>
<InputRightElement w="auto" right={ 1 }>
{ field.value && <ClearButton onClick={ handleClear } isDisabled={ isDisabled }/> }
{ hasZerosControl && <ContractMethodFieldZeroes onClick={ handleAddZeroesClick } isDisabled={ isDisabled }/> }
</InputRightElement>
</InputGroup>
</FormControl>
<Box w="100%">
<FormControl
id={ name }
isDisabled={ isDisabled }
>
<InputGroup size="xs">
<Input
{ ...field }
{ ...(intMatch ? {
as: NumericFormat,
thousandSeparator: ' ',
decimalScale: 0,
allowNegative: !intMatch.isUnsigned,
} : {}) }
ref={ ref }
isInvalid={ Boolean(error) }
required={ !isOptional }
placeholder={ placeholder }
paddingRight={ hasZerosControl ? '120px' : '40px' }
autoComplete="off"
bgColor={ bgColor }
/>
<InputRightElement w="auto" right={ 1 }>
{ typeof field.value === 'string' && field.value.replace('\n', '') && <ClearButton onClick={ handleClear } isDisabled={ isDisabled }/> }
{ hasZerosControl && <ContractMethodFieldZeroes onClick={ handleAddZeroesClick } isDisabled={ isDisabled }/> }
</InputRightElement>
</InputGroup>
</FormControl>
{ error && <Box color="error" fontSize="sm" mt={ 1 }>{ error.message }</Box> }
</Box>
);
}, [ name, isDisabled, placeholder, hasZerosControl, handleClear, handleAddZeroesClick ]);
}, [ index, groupName, name, intMatch, isDisabled, isOptional, placeholder, bgColor, handleClear, handleAddZeroesClick ]);
const validate = React.useCallback((_value: string | Array<string> | undefined) => {
if (typeof _value === 'object' || !_value) {
return;
}
const value = _value.replace('\n', '');
if (!value && !isOptional) {
return 'Field is required';
}
if (argType === 'address') {
if (!isAddress(value)) {
return 'Invalid address format';
}
// all lowercase addresses are valid
const isInLowerCase = value === value.toLowerCase();
if (isInLowerCase) {
return true;
}
// check if address checksum is valid
return getAddress(value) === value ? true : 'Invalid address checksum';
}
if (intMatch) {
const formattedValue = Number(value.replace(/\s/g, ''));
if (Object.is(formattedValue, NaN)) {
return 'Invalid integer format';
}
if (formattedValue > intMatch.max || formattedValue < intMatch.min) {
const lowerBoundary = intMatch.isUnsigned ? '0' : `-1 * 2 ^ ${ Number(intMatch.power) / 2 }`;
const upperBoundary = intMatch.isUnsigned ? `2 ^ ${ intMatch.power } - 1` : `2 ^ ${ Number(intMatch.power) / 2 } - 1`;
return `Value should be in range from "${ lowerBoundary }" to "${ upperBoundary }" inclusively`;
}
return true;
}
if (argType === 'bool') {
const formattedValue = formatBooleanValue(value);
if (formattedValue === undefined) {
return 'Invalid boolean format. Allowed values: 0, 1, true, false';
}
}
if (bytesMatch) {
const [ , length ] = bytesMatch;
if (!isHex(value)) {
return 'Invalid bytes format';
}
if (length) {
const valueLengthInBytes = value.replace('0x', '').length / 2;
return valueLengthInBytes !== Number(length) ? `Value should be ${ length } bytes in length` : true;
}
return true;
}
return true;
}, [ isOptional, argType, intMatch, bytesMatch ]);
return (
<Controller
name={ name }
control={ control }
render={ renderInput }
rules={{ required: isOptional ? false : 'Field is required', validate }}
/>
);
};
@@ -0,0 +1,106 @@
import { Flex, IconButton } from '@chakra-ui/react';
import React from 'react';
import type { Control, UseFormGetValues, UseFormSetValue } from 'react-hook-form';
import { useFieldArray } from 'react-hook-form';
import type { MethodFormFields } from './types';
import type { SmartContractMethodArgType } from 'types/api/contract';
import IconSvg from 'ui/shared/IconSvg';
import ContractMethodField from './ContractMethodField';
interface Props {
name: string;
size: number;
argType: SmartContractMethodArgType;
control: Control<MethodFormFields>;
setValue: UseFormSetValue<MethodFormFields>;
getValues: UseFormGetValues<MethodFormFields>;
isDisabled: boolean;
onChange: () => void;
}
const ContractMethodFieldArray = ({ control, name, setValue, getValues, isDisabled, argType, onChange, size }: Props) => {
const { fields, append, remove } = useFieldArray({
name: name as never,
control,
});
React.useEffect(() => {
if (fields.length === 0) {
if (size === Infinity) {
append('');
} else {
for (let i = 0; i < size - 1; i++) {
// a little hack to append multiple empty fields in the array
// had to adjust code in ContractMethodField as well
append('\n');
}
}
}
}, [ fields.length, append, size ]);
const handleAddButtonClick = React.useCallback(() => {
append('');
}, [ append ]);
const handleRemoveButtonClick = React.useCallback((event: React.MouseEvent<HTMLButtonElement>) => {
const itemIndex = event.currentTarget.getAttribute('data-index');
if (itemIndex) {
remove(Number(itemIndex));
}
}, [ remove ]);
return (
<Flex flexDir="column" rowGap={ 3 } w="100%">
{ fields.map((field, index, array) => {
return (
<Flex key={ field.id } columnGap={ 3 }>
<ContractMethodField
name={ `${ name }[${ index }]` }
groupName={ name }
index={ index }
argType={ argType }
placeholder={ argType }
control={ control }
setValue={ setValue }
getValues={ getValues }
isDisabled={ isDisabled }
onChange={ onChange }
/>
{ array.length > 1 && size === Infinity && (
<IconButton
aria-label="remove"
data-index={ index }
variant="outline"
w="30px"
h="30px"
flexShrink={ 0 }
onClick={ handleRemoveButtonClick }
icon={ <IconSvg name="minus" boxSize={ 4 }/> }
isDisabled={ isDisabled }
/>
) }
{ index === array.length - 1 && size === Infinity && (
<IconButton
aria-label="add"
data-index={ index }
variant="outline"
w="30px"
h="30px"
flexShrink={ 0 }
onClick={ handleAddButtonClick }
icon={ <IconSvg name="plus" boxSize={ 4 }/> }
isDisabled={ isDisabled }
/>
) }
</Flex>
);
}) }
</Flex>
);
};
export default React.memo(ContractMethodFieldArray);
@@ -11,9 +11,10 @@ interface Props<T extends SmartContractMethod> {
data: Array<T>;
addressHash?: string;
renderItemContent: (item: T, index: number, id: number) => React.ReactNode;
tab: string;
}
const ContractMethodsAccordion = <T extends SmartContractMethod>({ data, addressHash, renderItemContent }: Props<T>) => {
const ContractMethodsAccordion = <T extends SmartContractMethod>({ data, addressHash, renderItemContent, tab }: Props<T>) => {
const [ expandedSections, setExpandedSections ] = React.useState<Array<number>>(data.length === 1 ? [ 0 ] : []);
const [ id, setId ] = React.useState(0);
@@ -79,6 +80,7 @@ const ContractMethodsAccordion = <T extends SmartContractMethod>({ data, address
index={ index }
addressHash={ addressHash }
renderContent={ renderItemContent as (item: SmartContractMethod, index: number, id: number) => React.ReactNode }
tab={ tab }
/>
)) }
</Accordion>
@@ -16,9 +16,10 @@ interface Props<T extends SmartContractMethod> {
id: number;
addressHash?: string;
renderContent: (item: T, index: number, id: number) => React.ReactNode;
tab: string;
}
const ContractMethodsAccordionItem = <T extends SmartContractMethod>({ data, index, id, addressHash, renderContent }: Props<T>) => {
const ContractMethodsAccordionItem = <T extends SmartContractMethod>({ data, index, id, addressHash, renderContent, tab }: Props<T>) => {
const url = React.useMemo(() => {
if (!('method_id' in data)) {
return '';
@@ -28,11 +29,11 @@ const ContractMethodsAccordionItem = <T extends SmartContractMethod>({ data, ind
pathname: '/address/[hash]',
query: {
hash: addressHash ?? '',
tab: 'read_contract',
tab,
},
hash: data.method_id,
});
}, [ addressHash, data ]);
}, [ addressHash, data, tab ]);
const { hasCopied, onCopy } = useClipboard(url, 1000);
const { isOpen, onOpen, onClose } = useDisclosure();
@@ -85,7 +86,7 @@ const ContractMethodsAccordionItem = <T extends SmartContractMethod>({ data, ind
<AccordionIcon/>
</AccordionButton>
</Element>
<AccordionPanel pb={ 4 } px={ 0 }>
<AccordionPanel pb={ 4 } pr={ 0 } pl="28px" w="calc(100% - 6px)">
{ renderContent(data, index, id) }
</AccordionPanel>
</AccordionItem>
+3 -3
View File
@@ -28,7 +28,7 @@ test('base view +@mobile +@dark-mode', async({ mount, page }) => {
const component = await mount(
<TestApp>
<ContractRead addressHash={ addressHash }/>
<ContractRead/>
</TestApp>,
{ hooksConfig },
);
@@ -37,8 +37,8 @@ test('base view +@mobile +@dark-mode', async({ mount, page }) => {
await expect(component).toHaveScreenshot();
await component.getByPlaceholder(/address/i).type('address-hash');
await component.getByText(/query/i).click();
await component.getByPlaceholder(/address/i).type('0xa113Ce24919C08a26C952E81681dAc861d6a2466');
await component.getByText(/read/i).click();
await component.getByText(/wei/i).click();
+10 -8
View File
@@ -1,10 +1,12 @@
import { Alert, Flex } from '@chakra-ui/react';
import { useRouter } from 'next/router';
import React from 'react';
import type { SmartContractReadMethod, SmartContractQueryMethodRead } from 'types/api/contract';
import useApiFetch from 'lib/api/useApiFetch';
import useApiQuery from 'lib/api/useApiQuery';
import getQueryParamString from 'lib/router/getQueryParamString';
import ContractMethodsAccordion from 'ui/address/contract/ContractMethodsAccordion';
import ContentLoader from 'ui/shared/ContentLoader';
import DataFetchAlert from 'ui/shared/DataFetchAlert';
@@ -17,15 +19,15 @@ import ContractMethodConstant from './ContractMethodConstant';
import ContractReadResult from './ContractReadResult';
import useWatchAccount from './useWatchAccount';
interface Props {
addressHash?: string;
isProxy?: boolean;
isCustomAbi?: boolean;
}
const ContractRead = ({ addressHash, isProxy, isCustomAbi }: Props) => {
const ContractRead = () => {
const apiFetch = useApiFetch();
const account = useWatchAccount();
const router = useRouter();
const tab = getQueryParamString(router.query.tab);
const addressHash = getQueryParamString(router.query.hash);
const isProxy = tab === 'read_proxy';
const isCustomAbi = tab === 'read_custom_methods';
const { data, isPending, isError } = useApiQuery(isProxy ? 'contract_methods_read_proxy' : 'contract_methods_read', {
pathParams: { hash: addressHash },
@@ -96,7 +98,7 @@ const ContractRead = ({ addressHash, isProxy, isCustomAbi }: Props) => {
{ isCustomAbi && <ContractCustomAbiAlert/> }
{ account && <ContractConnectWallet/> }
{ isProxy && <ContractImplementationAddress hash={ addressHash }/> }
<ContractMethodsAccordion data={ data } addressHash={ addressHash } renderItemContent={ renderItemContent }/>
<ContractMethodsAccordion data={ data } addressHash={ addressHash } renderItemContent={ renderItemContent } tab={ tab }/>
</>
);
};
+1 -1
View File
@@ -23,7 +23,7 @@ test('base view +@mobile', async({ mount, page }) => {
const component = await mount(
<TestApp>
<ContractWrite addressHash={ addressHash }/>
<ContractWrite/>
</TestApp>,
{ hooksConfig },
);
+11 -8
View File
@@ -1,3 +1,4 @@
import { useRouter } from 'next/router';
import React from 'react';
import { useAccount, useWalletClient, useNetwork, useSwitchNetwork } from 'wagmi';
@@ -5,6 +6,7 @@ import type { SmartContractWriteMethod } from 'types/api/contract';
import config from 'configs/app';
import useApiQuery from 'lib/api/useApiQuery';
import getQueryParamString from 'lib/router/getQueryParamString';
import ContractMethodsAccordion from 'ui/address/contract/ContractMethodsAccordion';
import ContentLoader from 'ui/shared/ContentLoader';
import DataFetchAlert from 'ui/shared/DataFetchAlert';
@@ -17,18 +19,19 @@ import ContractWriteResult from './ContractWriteResult';
import useContractAbi from './useContractAbi';
import { getNativeCoinValue, prepareAbi } from './utils';
interface Props {
addressHash?: string;
isProxy?: boolean;
isCustomAbi?: boolean;
}
const ContractWrite = ({ addressHash, isProxy, isCustomAbi }: Props) => {
const ContractWrite = () => {
const { data: walletClient } = useWalletClient();
const { isConnected } = useAccount();
const { chain } = useNetwork();
const { switchNetworkAsync } = useSwitchNetwork();
const router = useRouter();
const tab = getQueryParamString(router.query.tab);
const addressHash = getQueryParamString(router.query.hash);
const isProxy = tab === 'write_proxy';
const isCustomAbi = tab === 'write_custom_methods';
const { data, isPending, isError } = useApiQuery(isProxy ? 'contract_methods_write_proxy' : 'contract_methods_write', {
pathParams: { hash: addressHash },
queryParams: {
@@ -112,7 +115,7 @@ const ContractWrite = ({ addressHash, isProxy, isCustomAbi }: Props) => {
{ isCustomAbi && <ContractCustomAbiAlert/> }
<ContractConnectWallet/>
{ isProxy && <ContractImplementationAddress hash={ addressHash }/> }
<ContractMethodsAccordion data={ data } addressHash={ addressHash } renderItemContent={ renderItemContent }/>
<ContractMethodsAccordion data={ data } addressHash={ addressHash } renderItemContent={ renderItemContent } tab={ tab }/>
</>
);
};
@@ -82,7 +82,6 @@ const ContractWriteResultDumb = ({ result, onSettle, txInfo }: Props) => {
return (
<Box
fontSize="sm"
pl={ 3 }
mt={ 3 }
alignItems="center"
whiteSpace="pre-wrap"
Binary file not shown.

Before

Width:  |  Height:  |  Size: 64 KiB

After

Width:  |  Height:  |  Size: 65 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 71 KiB

After

Width:  |  Height:  |  Size: 77 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 62 KiB

After

Width:  |  Height:  |  Size: 64 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 70 KiB

After

Width:  |  Height:  |  Size: 75 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 53 KiB

After

Width:  |  Height:  |  Size: 54 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 60 KiB

After

Width:  |  Height:  |  Size: 64 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 99 KiB

After

Width:  |  Height:  |  Size: 147 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 101 KiB

After

Width:  |  Height:  |  Size: 129 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.2 KiB

After

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.3 KiB

After

Width:  |  Height:  |  Size: 8.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.9 KiB

After

Width:  |  Height:  |  Size: 4.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.2 KiB

After

Width:  |  Height:  |  Size: 4.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.2 KiB

After

Width:  |  Height:  |  Size: 8.5 KiB

+4 -1
View File
@@ -2,7 +2,10 @@ import type { SmartContractQueryMethodRead, SmartContractMethod } from 'types/ap
import type { ResourceError } from 'lib/api/resources';
export type MethodFormFields = Record<string, string>;
export type MethodFormFields = Record<string, string | Array<string>>;
export type MethodFormFieldsFormatted = Record<string, MethodArgType>;
export type MethodArgType = string | boolean | Array<MethodArgType>;
export type ContractMethodReadResult = SmartContractQueryMethodRead | ResourceError;
+100 -1
View File
@@ -1,4 +1,6 @@
import { prepareAbi } from './utils';
import type { SmartContractMethodInput } from 'types/api/contract';
import { prepareAbi, transformFieldsToArgs, formatFieldValues } from './utils';
describe('function prepareAbi()', () => {
const commonAbi = [
@@ -98,3 +100,100 @@ describe('function prepareAbi()', () => {
expect(item).toEqual(commonAbi[2]);
});
});
describe('function formatFieldValues()', () => {
const formFields = {
'_tx%0:nonce%0': '1 000 000 000 000 000 000',
'_tx%0:sender%1': '0xB375d4150A853482f25E3922A4C64c6C4fF6Ae3c',
'_tx%0:targets%2': [
'1',
'true',
],
'_l2OutputIndex%1': '0xaeff',
'_paused%2': '0',
'_withdrawalProof%3': [
'0x0f',
'0x02',
],
};
const inputs: Array<SmartContractMethodInput> = [
{
components: [
{ internalType: 'uint256', name: 'nonce', type: 'uint256' },
{ internalType: 'address', name: 'sender', type: 'address' },
{ internalType: 'bool[]', name: 'targets', type: 'bool[]' },
],
internalType: 'tuple',
name: '_tx',
type: 'tuple',
},
{ internalType: 'bytes32', name: '_l2OutputIndex', type: 'bytes32' },
{
internalType: 'bool',
name: '_paused',
type: 'bool',
},
{
internalType: 'bytes32[]',
name: '_withdrawalProof',
type: 'bytes32[]',
},
];
it('converts values to correct format', () => {
const result = formatFieldValues(formFields, inputs);
expect(result).toEqual({
'_tx%0:nonce%0': '1000000000000000000',
'_tx%0:sender%1': '0xB375d4150A853482f25E3922A4C64c6C4fF6Ae3c',
'_tx%0:targets%2': [
true,
true,
],
'_l2OutputIndex%1': '0xaeff',
'_paused%2': false,
'_withdrawalProof%3': [
'0x0f',
'0x02',
],
});
});
it('converts nested array string representation to correct format', () => {
const formFields = {
'_withdrawalProof%0': '[ [ 1 ], [ 2, 3 ], [ 4 ]]',
};
const inputs: Array<SmartContractMethodInput> = [
{ internalType: 'uint[][]', name: '_withdrawalProof', type: 'uint[][]' },
];
const result = formatFieldValues(formFields, inputs);
expect(result).toEqual({
'_withdrawalProof%0': [ [ 1 ], [ 2, 3 ], [ 4 ] ],
});
});
});
describe('function transformFieldsToArgs()', () => {
it('groups struct and array fields', () => {
const formFields = {
'_paused%2': 'primitive_1',
'_l2OutputIndex%1': 'primitive_0',
'_tx%0:nonce%0': 'struct_0',
'_tx%0:sender%1': 'struct_1',
'_tx%0:target%2': [ 'struct_2_0', 'struct_2_1' ],
'_withdrawalProof%3': [
'array_0',
'array_1',
],
};
const args = transformFieldsToArgs(formFields);
expect(args).toEqual([
[ 'struct_0', 'struct_1', [ 'struct_2_0', 'struct_2_1' ] ],
'primitive_0',
'primitive_1',
[ 'array_0', 'array_1' ],
]);
});
});
+138 -19
View File
@@ -1,6 +1,40 @@
import type { Abi } from 'abitype';
import _mapValues from 'lodash/mapValues';
import type { SmartContractWriteMethod } from 'types/api/contract';
import type { MethodArgType, MethodFormFields, MethodFormFieldsFormatted } from './types';
import type { SmartContractMethodArgType, SmartContractMethodInput, SmartContractWriteMethod } from 'types/api/contract';
export const INT_REGEXP = /^(u)?int(\d+)?$/i;
export const BYTES_REGEXP = /^bytes(\d+)?$/i;
export const ARRAY_REGEXP = /^(.*)\[(\d*)\]$/;
export const getIntBoundaries = (power: number, isUnsigned: boolean) => {
const maxUnsigned = 2 ** power;
const max = isUnsigned ? maxUnsigned - 1 : maxUnsigned / 2 - 1;
const min = isUnsigned ? 0 : -maxUnsigned / 2;
return [ min, max ];
};
export const formatBooleanValue = (value: string) => {
const formattedValue = value.toLowerCase();
switch (formattedValue) {
case 'true':
case '1': {
return 'true';
}
case 'false':
case '0': {
return 'false';
}
default:
return;
}
};
export const getNativeCoinValue = (value: string | Array<unknown>) => {
const _value = Array.isArray(value) ? value[0] : value;
@@ -12,24 +46,6 @@ export const getNativeCoinValue = (value: string | Array<unknown>) => {
return BigInt(_value);
};
export const addZeroesAllowed = (valueType: string) => {
if (valueType.includes('[')) {
return false;
}
const REGEXP = /^u?int(\d+)/i;
const match = valueType.match(REGEXP);
const power = match?.[1];
if (power) {
// show control for all inputs which allows to insert 10^18 or greater numbers
return Number(power) >= 64;
}
return false;
};
interface ExtendedError extends Error {
detectedNetwork?: {
chain: number;
@@ -75,3 +91,106 @@ export function prepareAbi(abi: Abi, item: SmartContractWriteMethod): Abi {
return abi;
}
function getFieldType(fieldName: string, inputs: Array<SmartContractMethodInput>) {
const chunks = fieldName.split(':');
if (chunks.length === 1) {
const [ , index ] = chunks[0].split('%');
return inputs[Number(index)].type;
} else {
const group = chunks[0].split('%');
const input = chunks[1].split('%');
return inputs[Number(group[1])].components?.[Number(input[1])].type;
}
}
function parseArrayValue(value: string) {
try {
const parsedResult = JSON.parse(value);
if (Array.isArray(parsedResult)) {
return parsedResult as Array<string>;
}
throw new Error('Not an array');
} catch (error) {
return '';
}
}
function castValue(value: string, type: SmartContractMethodArgType) {
if (type === 'bool') {
return formatBooleanValue(value) === 'true';
}
const intMatch = type.match(INT_REGEXP);
if (intMatch) {
return value.replaceAll(' ', '');
}
const isNestedArray = (type.match(/\[/g) || []).length > 1;
if (isNestedArray) {
return parseArrayValue(value) || value;
}
return value;
}
export function formatFieldValues(formFields: MethodFormFields, inputs: Array<SmartContractMethodInput>) {
const formattedFields = _mapValues(formFields, (value, key) => {
const type = getFieldType(key, inputs);
if (!type) {
return value;
}
if (Array.isArray(value)) {
const arrayMatch = type.match(ARRAY_REGEXP);
if (arrayMatch) {
return value.map((item) => castValue(item, arrayMatch[1] as SmartContractMethodArgType));
}
return value;
}
return castValue(value, type);
});
return formattedFields;
}
export function transformFieldsToArgs(formFields: MethodFormFieldsFormatted) {
const unGroupedFields = Object.entries(formFields)
.reduce((
result: Record<string, MethodArgType>,
[ key, value ]: [ string, MethodArgType ],
) => {
const chunks = key.split(':');
if (chunks.length > 1) {
const groupKey = chunks[0];
const [ , fieldIndex ] = chunks[1].split('%');
if (result[groupKey] === undefined) {
result[groupKey] = [];
}
(result[groupKey] as Array<MethodArgType>)[Number(fieldIndex)] = value;
return result;
}
result[key] = value;
return result;
}, {});
const args = (Object.entries(unGroupedFields)
.map(([ key, value ]) => {
const [ , index ] = key.split('%');
return [ Number(index), value ];
}) as Array<[ number, string | Array<string> ]>)
.sort((a, b) => a[0] - b[0])
.map(([ , value ]) => value);
return args;
}
@@ -0,0 +1,37 @@
import { test, expect } from '@playwright/experimental-ct-react';
import React from 'react';
import config from 'configs/app';
import * as ensDomainMock from 'mocks/ens/domain';
import TestApp from 'playwright/TestApp';
import buildApiUrl from 'playwright/utils/buildApiUrl';
import AddressEnsDomains from './AddressEnsDomains';
const ADDRESS_HASH = ensDomainMock.ensDomainA.owner?.hash ?? '';
const ADDRESSES_LOOKUP_API_URL = buildApiUrl('addresses_lookup', { chainId: config.chain.id }) +
`?address=${ ADDRESS_HASH }&resolved_to=true&owned_by=true&only_active=true&order=ASC`;
test('base view', async({ mount, page }) => {
await page.route(ADDRESSES_LOOKUP_API_URL, (route) => route.fulfill({
status: 200,
body: JSON.stringify({
items: [
ensDomainMock.ensDomainA,
ensDomainMock.ensDomainB,
ensDomainMock.ensDomainC,
ensDomainMock.ensDomainD,
],
}),
}));
const component = await mount(
<TestApp>
<AddressEnsDomains addressHash={ ADDRESS_HASH } mainDomainName={ ensDomainMock.ensDomainA.name }/>
</TestApp>,
);
await component.getByText('4 domains').click();
await expect(page).toHaveScreenshot({ clip: { x: 0, y: 0, width: 550, height: 350 } });
});
+152
View File
@@ -0,0 +1,152 @@
import { Button, chakra, Flex, Grid, Popover, PopoverBody, PopoverContent, PopoverTrigger, Skeleton, useDisclosure } from '@chakra-ui/react';
import _clamp from 'lodash/clamp';
import React from 'react';
import type { EnsDomain } from 'types/api/ens';
import { route } from 'nextjs-routes';
import config from 'configs/app';
import useApiQuery from 'lib/api/useApiQuery';
import dayjs from 'lib/date/dayjs';
import EnsEntity from 'ui/shared/entities/ens/EnsEntity';
import IconSvg from 'ui/shared/IconSvg';
import LinkInternal from 'ui/shared/LinkInternal';
interface Props {
addressHash: string;
mainDomainName: string | null;
}
const DomainsGrid = ({ data }: { data: Array<EnsDomain> }) => {
return (
<Grid
templateColumns={{ base: `repeat(${ _clamp(data.length, 1, 2) }, 1fr)`, lg: `repeat(${ _clamp(data.length, 1, 3) }, 1fr)` }}
columnGap={ 8 }
rowGap={ 4 }
mt={ 2 }
>
{ data.slice(0, 9).map((domain) => <EnsEntity key={ domain.id } name={ domain.name } noCopy/>) }
</Grid>
);
};
const AddressEnsDomains = ({ addressHash, mainDomainName }: Props) => {
const { isOpen, onToggle, onClose } = useDisclosure();
const { data, isPending, isError } = useApiQuery('addresses_lookup', {
pathParams: { chainId: config.chain.id },
queryParams: {
address: addressHash,
resolved_to: true,
owned_by: true,
only_active: true,
order: 'ASC',
},
});
if (isError) {
return null;
}
if (isPending) {
return <Skeleton h={ 8 } w={{ base: '60px', lg: '120px' }} borderRadius="base"/>;
}
if (data.items.length === 0) {
return null;
}
const mainDomain = data.items.find((domain) => domain.name === mainDomainName);
const ownedDomains = data.items.filter((domain) => {
if (domain.name === mainDomainName) {
return false;
}
// exclude resolved address
if (domain.resolved_address && domain.resolved_address.hash.toLowerCase() === addressHash.toLowerCase()) {
return false;
}
if (domain.owner && domain.owner.hash.toLowerCase() === addressHash.toLowerCase()) {
return true;
}
// include wrapped owner
if (domain.wrapped_owner?.hash.toLowerCase() === addressHash.toLowerCase()) {
return !domain.resolved_address || domain.resolved_address.hash.toLowerCase() !== addressHash.toLowerCase();
}
return false;
});
const resolvedDomains = data.items.filter((domain) =>
domain.resolved_address &&
domain.resolved_address.hash.toLowerCase() === addressHash.toLowerCase() &&
domain.name !== mainDomainName,
);
const totalRecords = data.items.length > 40 ? '40+' : data.items.length;
return (
<Popover isOpen={ isOpen } onClose={ onClose } placement="bottom-start" isLazy>
<PopoverTrigger>
<Button
size="sm"
variant="outline"
colorScheme="gray"
onClick={ onToggle }
aria-label="Address ENS domains"
fontWeight={ 500 }
px={ 2 }
h="32px"
flexShrink={ 0 }
>
<IconSvg name="ENS_slim" boxSize={ 5 }/>
<chakra.span ml={ 1 } display={{ base: 'none', lg: 'block' }}>{ totalRecords } Domain{ data.items.length > 1 ? 's' : '' }</chakra.span>
<IconSvg name="arrows/east-mini" transform={ isOpen ? 'rotate(90deg)' : 'rotate(-90deg)' } transitionDuration="faster" boxSize={ 5 }/>
</Button>
</PopoverTrigger>
<PopoverContent w={{ base: '100vw', lg: '500px' }}>
<PopoverBody px={ 6 } py={ 5 } fontSize="sm" display="flex" flexDir="column" rowGap={ 5 } alignItems="flex-start">
{ mainDomain && (
<div>
<chakra.span color="text_secondary" fontSize="xs">Primary*</chakra.span>
<Flex alignItems="center" fontSize="md" mt={ 2 }>
<EnsEntity name={ mainDomain.name } fontWeight={ 600 } noCopy/>
{ mainDomain.expiry_date &&
<chakra.span color="text_secondary" whiteSpace="pre"> (expires { dayjs(mainDomain.expiry_date).fromNow() })</chakra.span> }
</Flex>
</div>
) }
{ ownedDomains.length > 0 && (
<div>
<chakra.span color="text_secondary" fontSize="xs">Owned by this address</chakra.span>
<DomainsGrid data={ ownedDomains }/>
</div>
) }
{ resolvedDomains.length > 0 && (
<div>
<chakra.span color="text_secondary" fontSize="xs">Resolved to this address</chakra.span>
<DomainsGrid data={ resolvedDomains }/>
</div>
) }
{ (ownedDomains.length > 9 || resolvedDomains.length > 9) && (
<LinkInternal
href={ route({ pathname: '/name-domains', query: { owned_by: 'true', resolved_to: 'true', address: addressHash } }) }
>
<span> More results</span>
<chakra.span color="text_secondary"> ({ totalRecords })</chakra.span>
</LinkInternal>
) }
{ mainDomain && (
<chakra.span fontSize="xs" mt={ -1 }>
*A domain name is not necessarily held by a person popularly associated with the name
</chakra.span>
) }
</PopoverBody>
</PopoverContent>
</Popover>
);
};
export default React.memo(AddressEnsDomains);
Binary file not shown.

After

Width:  |  Height:  |  Size: 23 KiB

+12 -1
View File
@@ -128,6 +128,17 @@ const BlockDetails = ({ query }: Props) => {
return config.chain.verificationType === 'validation' ? 'Validated by' : 'Mined by';
})();
const blockTypeLabel = (() => {
switch (data.type) {
case 'reorg':
return 'Reorg';
case 'uncle':
return 'Uncle';
default:
return 'Block';
}
})();
return (
<Grid
columnGap={ 8 }
@@ -136,7 +147,7 @@ const BlockDetails = ({ query }: Props) => {
overflow="hidden"
>
<DetailsInfoItem
title={ `${ data.type === 'reorg' ? 'Reorg' : 'Block' } height` }
title={ `${ blockTypeLabel } height` }
hint="The block height of a particular block is defined as the number of blocks preceding it in the blockchain"
isLoading={ isPlaceholderData }
>
Binary file not shown.

Before

Width:  |  Height:  |  Size: 54 KiB

After

Width:  |  Height:  |  Size: 54 KiB

+1 -1
View File
@@ -43,7 +43,7 @@ const BlocksListItem = ({ data, isLoading, enableTimeIncrement }: Props) => {
<BlockEntity
isLoading={ isLoading }
number={ data.height }
hash={ data.type === 'reorg' ? data.hash : undefined }
hash={ data.type !== 'block' ? data.hash : undefined }
noIcon
fontWeight={ 600 }
/>
+5
View File
@@ -14,6 +14,11 @@ interface Props {
const BlocksTabSlot = ({ pagination }: Props) => {
const statsQuery = useApiQuery('homepage_stats', {
fetchParams: {
headers: {
'updated-gas-oracle': 'true',
},
},
queryOptions: {
placeholderData: HOMEPAGE_STATS,
},
+1 -1
View File
@@ -50,7 +50,7 @@ const BlocksTableItem = ({ data, isLoading, enableTimeIncrement }: Props) => {
<BlockEntity
isLoading={ isLoading }
number={ data.height }
hash={ data.type === 'reorg' ? data.hash : undefined }
hash={ data.type !== 'block' ? data.hash : undefined }
noIcon
fontSize="sm"
lineHeight={ 5 }
+6
View File
@@ -37,7 +37,13 @@ const LatestBlocks = () => {
const queryClient = useQueryClient();
const statsQueryResult = useApiQuery('homepage_stats', {
fetchParams: {
headers: {
'updated-gas-oracle': 'true',
},
},
queryOptions: {
refetchOnMount: false,
placeholderData: HOMEPAGE_STATS,
},
});
+21 -3
View File
@@ -16,8 +16,14 @@ const hasGasTracker = config.UI.homepage.showGasTracker;
const hasAvgBlockTime = config.UI.homepage.showAvgBlockTime;
const Stats = () => {
const { data, isPlaceholderData, isError } = useApiQuery('homepage_stats', {
const { data, isPlaceholderData, isError, dataUpdatedAt } = useApiQuery('homepage_stats', {
fetchParams: {
headers: {
'updated-gas-oracle': 'true',
},
},
queryOptions: {
refetchOnMount: false,
placeholderData: HOMEPAGE_STATS,
},
});
@@ -45,7 +51,19 @@ const Stats = () => {
!data.gas_prices && itemsCount--;
data.rootstock_locked_btc && itemsCount++;
const isOdd = Boolean(itemsCount % 2);
const gasLabel = hasGasTracker && data.gas_prices ? <GasInfoTooltipContent gasPrices={ data.gas_prices }/> : null;
const gasLabel = hasGasTracker && data.gas_prices ? <GasInfoTooltipContent data={ data } dataUpdatedAt={ dataUpdatedAt }/> : null;
const gasPriceText = (() => {
if (data.gas_prices?.average?.fiat_price) {
return `$${ data.gas_prices.average.fiat_price }`;
}
if (data.gas_prices?.average?.price) {
return `${ data.gas_prices.average.price.toLocaleString() } Gwei`;
}
return 'N/A';
})();
content = (
<>
@@ -92,7 +110,7 @@ const Stats = () => {
<StatsItem
icon="gas"
title="Gas tracker"
value={ data.gas_prices.average !== null ? `${ Number(data.gas_prices.average).toLocaleString() } Gwei` : 'N/A' }
value={ gasPriceText }
_last={ isOdd ? lastItemTouchStyle : undefined }
tooltipLabel={ gasLabel }
isLoading={ isPlaceholderData }

Some files were not shown because too many files have changed in this diff Show More