Commit ccfb4fae authored by tom's avatar tom

Highlighted address

Fixes #1446
parents 528a2324 463f3860
...@@ -4,5 +4,6 @@ NEXT_PUBLIC_WALLET_CONNECT_PROJECT_ID=xxx ...@@ -4,5 +4,6 @@ NEXT_PUBLIC_WALLET_CONNECT_PROJECT_ID=xxx
NEXT_PUBLIC_RE_CAPTCHA_APP_SITE_KEY=xxx NEXT_PUBLIC_RE_CAPTCHA_APP_SITE_KEY=xxx
NEXT_PUBLIC_GOOGLE_ANALYTICS_PROPERTY_ID=UA-XXXXXX-X NEXT_PUBLIC_GOOGLE_ANALYTICS_PROPERTY_ID=UA-XXXXXX-X
NEXT_PUBLIC_MIXPANEL_PROJECT_TOKEN=xxx NEXT_PUBLIC_MIXPANEL_PROJECT_TOKEN=xxx
NEXT_PUBLIC_GROWTH_BOOK_CLIENT_KEY=xxx
NEXT_PUBLIC_AUTH0_CLIENT_ID=xxx NEXT_PUBLIC_AUTH0_CLIENT_ID=xxx
FAVICON_GENERATOR_API_KEY=xxx FAVICON_GENERATOR_API_KEY=xxx
\ No newline at end of file
...@@ -4,6 +4,7 @@ on: ...@@ -4,6 +4,7 @@ on:
push: push:
branches: branches:
- main - main
workflow_dispatch:
concurrency: concurrency:
group: ${{ github.workflow }}-${{ github.ref }} group: ${{ github.workflow }}-${{ github.ref }}
......
...@@ -20,15 +20,15 @@ jobs: ...@@ -20,15 +20,15 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Check out the repo - name: Check out the repo
uses: actions/checkout@v3 uses: actions/checkout@v4
- name: Set up Docker Buildx - 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 # Will automatically make nice tags, see the table here https://github.com/docker/metadata-action#basic
- name: Docker meta - name: Docker meta
id: meta id: meta
uses: docker/metadata-action@v4 uses: docker/metadata-action@v5
with: with:
images: ghcr.io/blockscout/frontend images: ghcr.io/blockscout/frontend
...@@ -51,13 +51,16 @@ jobs: ...@@ -51,13 +51,16 @@ jobs:
echo "ref_name: $REF_NAME" echo "ref_name: $REF_NAME"
- name: Build and push - name: Build and push
uses: docker/build-push-action@v3 uses: docker/build-push-action@v5
with: with:
context: . context: .
file: ./Dockerfile file: ./Dockerfile
push: true push: true
cache-from: type=gha cache-from: type=gha
tags: ${{ inputs.tags || steps.meta.outputs.tags }} tags: ${{ inputs.tags || steps.meta.outputs.tags }}
platforms: |
linux/amd64
linux/arm64/v8
labels: ${{ steps.meta.outputs.labels }} labels: ${{ steps.meta.outputs.labels }}
build-args: | build-args: |
GIT_COMMIT_SHA=${{ env.SHORT_SHA }} GIT_COMMIT_SHA=${{ env.SHORT_SHA }}
......
...@@ -9,7 +9,7 @@ const baseUrl = [ ...@@ -9,7 +9,7 @@ const baseUrl = [
appHost, appHost,
appPort && ':' + appPort, appPort && ':' + appPort,
].filter(Boolean).join(''); ].filter(Boolean).join('');
const isDev = getEnvValue('NODE_ENV') === 'development'; const isDev = getEnvValue('NEXT_PUBLIC_APP_ENV') === 'development';
const app = Object.freeze({ const app = Object.freeze({
isDev, isDev,
......
import type { Feature } from './types';
import { getEnvValue } from '../utils';
const clientKey = getEnvValue('NEXT_PUBLIC_GROWTH_BOOK_CLIENT_KEY');
const title = 'GrowthBook feature flagging and A/B testing';
const config: Feature<{ clientKey: string }> = (() => {
if (clientKey) {
return Object.freeze({
title,
isEnabled: true,
clientKey,
});
}
return Object.freeze({
title,
isEnabled: false,
});
})();
export default config;
...@@ -8,8 +8,10 @@ export { default as blockchainInteraction } from './blockchainInteraction'; ...@@ -8,8 +8,10 @@ export { default as blockchainInteraction } from './blockchainInteraction';
export { default as csvExport } from './csvExport'; export { default as csvExport } from './csvExport';
export { default as googleAnalytics } from './googleAnalytics'; export { default as googleAnalytics } from './googleAnalytics';
export { default as graphqlApiDocs } from './graphqlApiDocs'; export { default as graphqlApiDocs } from './graphqlApiDocs';
export { default as growthBook } from './growthBook';
export { default as marketplace } from './marketplace'; export { default as marketplace } from './marketplace';
export { default as mixpanel } from './mixpanel'; export { default as mixpanel } from './mixpanel';
export { default as nameService } from './nameService';
export { default as restApiDocs } from './restApiDocs'; export { default as restApiDocs } from './restApiDocs';
export { default as optimisticRollup } from './optimisticRollup'; export { default as optimisticRollup } from './optimisticRollup';
export { default as safe } from './safe'; export { default as safe } from './safe';
......
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;
...@@ -46,6 +46,7 @@ NEXT_PUBLIC_STATS_API_HOST=https://stats-goerli.k8s-dev.blockscout.com ...@@ -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_VISUALIZE_API_HOST=https://visualizer.services.blockscout.com
NEXT_PUBLIC_CONTRACT_INFO_API_HOST=https://contracts-info.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_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_WEB3_WALLETS=['token_pocket','metamask']
NEXT_PUBLIC_VIEWS_CONTRACT_SOLIDITYSCAN_ENABLED='true' NEXT_PUBLIC_VIEWS_CONTRACT_SOLIDITYSCAN_ENABLED='true'
......
...@@ -425,6 +425,7 @@ const schema = yup ...@@ -425,6 +425,7 @@ const schema = yup
NEXT_PUBLIC_STATS_API_HOST: yup.string().test(urlTest), NEXT_PUBLIC_STATS_API_HOST: yup.string().test(urlTest),
NEXT_PUBLIC_VISUALIZE_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_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_GRAPHIQL_TRANSACTION: yup.string().matches(regexp.HEX_REGEXP),
NEXT_PUBLIC_WEB3_WALLETS: yup NEXT_PUBLIC_WEB3_WALLETS: yup
.mixed() .mixed()
...@@ -451,6 +452,7 @@ const schema = yup ...@@ -451,6 +452,7 @@ const schema = yup
NEXT_PUBLIC_RE_CAPTCHA_APP_SITE_KEY: yup.string(), NEXT_PUBLIC_RE_CAPTCHA_APP_SITE_KEY: yup.string(),
NEXT_PUBLIC_GOOGLE_ANALYTICS_PROPERTY_ID: yup.string(), NEXT_PUBLIC_GOOGLE_ANALYTICS_PROPERTY_ID: yup.string(),
NEXT_PUBLIC_MIXPANEL_PROJECT_TOKEN: yup.string(), NEXT_PUBLIC_MIXPANEL_PROJECT_TOKEN: yup.string(),
NEXT_PUBLIC_GROWTH_BOOK_CLIENT_KEY: yup.string(),
// Misc // Misc
NEXT_PUBLIC_USE_NEXT_JS_PROXY: yup.boolean(), NEXT_PUBLIC_USE_NEXT_JS_PROXY: yup.boolean(),
......
...@@ -153,6 +153,7 @@ frontend: ...@@ -153,6 +153,7 @@ frontend:
NEXT_PUBLIC_VISUALIZE_API_HOST: http://visualizer-svc.visualizer-testing.svc.cluster.local/ 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_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_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_LOGOUT_URL: https://blockscoutcom.us.auth0.com/v2/logout
NEXT_PUBLIC_NETWORK_RPC_URL: https://rpc.ankr.com/eth_goerli NEXT_PUBLIC_NETWORK_RPC_URL: https://rpc.ankr.com/eth_goerli
NEXT_PUBLIC_HOMEPAGE_CHARTS: "['daily_txs','coin_price','market_cap']" NEXT_PUBLIC_HOMEPAGE_CHARTS: "['daily_txs','coin_price','market_cap']"
......
...@@ -56,6 +56,7 @@ frontend: ...@@ -56,6 +56,7 @@ frontend:
NEXT_PUBLIC_VISUALIZE_API_HOST: http://visualizer-svc.visualizer-testing.svc.cluster.local/ 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_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_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_AUTH_URL: https://blockscout-main.k8s-dev.blockscout.com
NEXT_PUBLIC_MARKETPLACE_SUBMIT_FORM: https://airtable.com/shrqUAcjgGJ4jU88C NEXT_PUBLIC_MARKETPLACE_SUBMIT_FORM: https://airtable.com/shrqUAcjgGJ4jU88C
NEXT_PUBLIC_LOGOUT_URL: https://blockscoutcom.us.auth0.com/v2/logout NEXT_PUBLIC_LOGOUT_URL: https://blockscoutcom.us.auth0.com/v2/logout
...@@ -72,9 +73,6 @@ frontend: ...@@ -72,9 +73,6 @@ frontend:
NEXT_PUBLIC_VIEWS_TX_ADDITIONAL_FIELDS: "['fee_per_gas']" NEXT_PUBLIC_VIEWS_TX_ADDITIONAL_FIELDS: "['fee_per_gas']"
NEXT_PUBLIC_USE_NEXT_JS_PROXY: true NEXT_PUBLIC_USE_NEXT_JS_PROXY: true
NEXT_PUBLIC_VIEWS_NFT_MARKETPLACES: "[{'name':'LooksRare','collection_url':'https://goerli.looksrare.org/collections/{hash}','instance_url':'https://goerli.looksrare.org/collections/{hash}/{id}','logo_url':'https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/nft-marketplace-logos/looks-rare.png'}]" NEXT_PUBLIC_VIEWS_NFT_MARKETPLACES: "[{'name':'LooksRare','collection_url':'https://goerli.looksrare.org/collections/{hash}','instance_url':'https://goerli.looksrare.org/collections/{hash}/{id}','logo_url':'https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/nft-marketplace-logos/looks-rare.png'}]"
OTEL_SDK_ENABLED: true
OTEL_EXPORTER_OTLP_ENDPOINT: http://jaeger-collector.jaeger.svc.cluster.local:4318
NEXT_OTEL_VERBOSE: 1
envFromSecret: envFromSecret:
NEXT_PUBLIC_SENTRY_DSN: ref+vault://deployment-values/blockscout/dev/review?token_env=VAULT_TOKEN&address=https://vault.k8s.blockscout.com#/NEXT_PUBLIC_SENTRY_DSN NEXT_PUBLIC_SENTRY_DSN: ref+vault://deployment-values/blockscout/dev/review?token_env=VAULT_TOKEN&address=https://vault.k8s.blockscout.com#/NEXT_PUBLIC_SENTRY_DSN
SENTRY_CSP_REPORT_URI: ref+vault://deployment-values/blockscout/dev/review?token_env=VAULT_TOKEN&address=https://vault.k8s.blockscout.com#/SENTRY_CSP_REPORT_URI SENTRY_CSP_REPORT_URI: ref+vault://deployment-values/blockscout/dev/review?token_env=VAULT_TOKEN&address=https://vault.k8s.blockscout.com#/SENTRY_CSP_REPORT_URI
...@@ -83,3 +81,5 @@ frontend: ...@@ -83,3 +81,5 @@ frontend:
NEXT_PUBLIC_RE_CAPTCHA_APP_SITE_KEY: ref+vault://deployment-values/blockscout/dev/review?token_env=VAULT_TOKEN&address=https://vault.k8s.blockscout.com#/NEXT_PUBLIC_RE_CAPTCHA_APP_SITE_KEY NEXT_PUBLIC_RE_CAPTCHA_APP_SITE_KEY: ref+vault://deployment-values/blockscout/dev/review?token_env=VAULT_TOKEN&address=https://vault.k8s.blockscout.com#/NEXT_PUBLIC_RE_CAPTCHA_APP_SITE_KEY
NEXT_PUBLIC_GOOGLE_ANALYTICS_PROPERTY_ID: ref+vault://deployment-values/blockscout/dev/review?token_env=VAULT_TOKEN&address=https://vault.k8s.blockscout.com#/NEXT_PUBLIC_GOOGLE_ANALYTICS_PROPERTY_ID NEXT_PUBLIC_GOOGLE_ANALYTICS_PROPERTY_ID: ref+vault://deployment-values/blockscout/dev/review?token_env=VAULT_TOKEN&address=https://vault.k8s.blockscout.com#/NEXT_PUBLIC_GOOGLE_ANALYTICS_PROPERTY_ID
FAVICON_GENERATOR_API_KEY: ref+vault://deployment-values/blockscout/common?token_env=VAULT_TOKEN&address=https://vault.k8s.blockscout.com#/NEXT_PUBLIC_FAVICON_GENERATOR_API_KEY FAVICON_GENERATOR_API_KEY: ref+vault://deployment-values/blockscout/common?token_env=VAULT_TOKEN&address=https://vault.k8s.blockscout.com#/NEXT_PUBLIC_FAVICON_GENERATOR_API_KEY
NEXT_PUBLIC_GROWTH_BOOK_CLIENT_KEY: ref+vault://deployment-values/blockscout/dev/review?token_env=VAULT_TOKEN&address=https://vault.k8s.blockscout.com#/NEXT_PUBLIC_GROWTH_BOOK_CLIENT_KEY
NEXT_PUBLIC_MIXPANEL_PROJECT_TOKEN: ref+vault://deployment-values/blockscout/common?token_env=VAULT_TOKEN&address=https://vault.k8s.blockscout.com#/NEXT_PUBLIC_MIXPANEL_PROJECT_TOKEN
\ No newline at end of file
...@@ -38,6 +38,7 @@ Please be aware that all environment variables prefixed with `NEXT_PUBLIC_` will ...@@ -38,6 +38,7 @@ Please be aware that all environment variables prefixed with `NEXT_PUBLIC_` will
- [Export data to CSV file](ENVS.md#export-data-to-csv-file) - [Export data to CSV file](ENVS.md#export-data-to-csv-file)
- [Google analytics](ENVS.md#google-analytics) - [Google analytics](ENVS.md#google-analytics)
- [Mixpanel analytics](ENVS.md#mixpanel-analytics) - [Mixpanel analytics](ENVS.md#mixpanel-analytics)
- [GrowthBook feature flagging and A/B testing](ENVS.md#growthbook-feature-flagging-and-ab-testing)
- [GraphQL API documentation](ENVS.md#graphql-api-documentation) - [GraphQL API documentation](ENVS.md#graphql-api-documentation)
- [REST API documentation](ENVS.md#rest-api-documentation) - [REST API documentation](ENVS.md#rest-api-documentation)
- [Marketplace](ENVS.md#marketplace) - [Marketplace](ENVS.md#marketplace)
...@@ -46,6 +47,7 @@ Please be aware that all environment variables prefixed with `NEXT_PUBLIC_` will ...@@ -46,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) - [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) - [Transaction interpretation](ENVS.md#transaction-interpretation)
- [Verified tokens info](ENVS.md#verified-tokens-info) - [Verified tokens info](ENVS.md#verified-tokens-info)
- [Name service integration](ENVS.md#name-service-integration)
- [Bridged tokens](ENVS.md#bridged-tokens) - [Bridged tokens](ENVS.md#bridged-tokens)
- [Safe{Core} address tags](ENVS.md#safecore-address-tags) - [Safe{Core} address tags](ENVS.md#safecore-address-tags)
- [SUAVE chain](ENVS.md#suave-chain) - [SUAVE chain](ENVS.md#suave-chain)
...@@ -386,6 +388,14 @@ This feature is **enabled by default** with the `coinzilla` ads provider. To swi ...@@ -386,6 +388,14 @@ This feature is **enabled by default** with the `coinzilla` ads provider. To swi
&nbsp; &nbsp;
### GrowthBook feature flagging and A/B testing
| Variable | Type| Description | Compulsoriness | Default value | Example value |
| --- | --- | --- | --- | --- | --- |
| NEXT_PUBLIC_GROWTH_BOOK_CLIENT_KEY | `string` | Client SDK key for [GrowthBook](https://www.growthbook.io/) service | true | - | `<your-secret>` |
&nbsp;
### GraphQL API documentation ### GraphQL API documentation
This feature is **always enabled**, but you can configure its behavior by passing the following variables. This feature is **always enabled**, but you can configure its behavior by passing the following variables.
...@@ -490,6 +500,16 @@ This feature is **enabled by default** with the `['metamask']` value. To switch ...@@ -490,6 +500,16 @@ This feature is **enabled by default** with the `['metamask']` value. To switch
&nbsp; &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 ### 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. 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.
......
<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>
<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>
import { ChakraProvider } from '@chakra-ui/react'; import { ChakraProvider } from '@chakra-ui/react';
import { GrowthBookProvider } from '@growthbook/growthbook-react';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import type { RenderOptions } from '@testing-library/react'; import type { RenderOptions } from '@testing-library/react';
import { render } from '@testing-library/react'; import { render } from '@testing-library/react';
...@@ -19,6 +20,7 @@ const PAGE_PROPS = { ...@@ -19,6 +20,7 @@ const PAGE_PROPS = {
hash: '', hash: '',
number: '', number: '',
q: '', q: '',
name: '',
}; };
const TestApp = ({ children }: {children: React.ReactNode}) => { const TestApp = ({ children }: {children: React.ReactNode}) => {
...@@ -36,9 +38,11 @@ const TestApp = ({ children }: {children: React.ReactNode}) => { ...@@ -36,9 +38,11 @@ const TestApp = ({ children }: {children: React.ReactNode}) => {
<QueryClientProvider client={ queryClient }> <QueryClientProvider client={ queryClient }>
<AppContextProvider pageProps={ PAGE_PROPS }> <AppContextProvider pageProps={ PAGE_PROPS }>
<ScrollDirectionProvider> <ScrollDirectionProvider>
<SocketProvider> <GrowthBookProvider>
{ children } <SocketProvider>
</SocketProvider> { children }
</SocketProvider>
</GrowthBookProvider>
</ScrollDirectionProvider> </ScrollDirectionProvider>
</AppContextProvider> </AppContextProvider>
</QueryClientProvider> </QueryClientProvider>
......
...@@ -9,7 +9,7 @@ import type { ApiResource, ResourceName, ResourcePathParams } from './resources' ...@@ -9,7 +9,7 @@ import type { ApiResource, ResourceName, ResourcePathParams } from './resources'
export default function buildUrl<R extends ResourceName>( export default function buildUrl<R extends ResourceName>(
resourceName: R, resourceName: R,
pathParams?: ResourcePathParams<R>, pathParams?: ResourcePathParams<R>,
queryParams?: Record<string, string | Array<string> | number | null | undefined>, queryParams?: Record<string, string | Array<string> | number | boolean | null | undefined>,
): string { ): string {
const resource: ApiResource = RESOURCES[resourceName]; const resource: ApiResource = RESOURCES[resourceName];
const baseUrl = isNeedProxy() ? config.app.baseUrl : (resource.endpoint || config.api.endpoint); const baseUrl = isNeedProxy() ? config.app.baseUrl : (resource.endpoint || config.api.endpoint);
......
...@@ -36,6 +36,15 @@ import type { ChartMarketResponse, ChartTransactionResponse } from 'types/api/ch ...@@ -36,6 +36,15 @@ import type { ChartMarketResponse, ChartTransactionResponse } from 'types/api/ch
import type { BackendVersionConfig } from 'types/api/configs'; import type { BackendVersionConfig } from 'types/api/configs';
import type { SmartContract, SmartContractReadMethod, SmartContractWriteMethod, SmartContractVerificationConfig, SolidityscanReport } from 'types/api/contract'; import type { SmartContract, SmartContractReadMethod, SmartContractWriteMethod, SmartContractVerificationConfig, SolidityscanReport } from 'types/api/contract';
import type { VerifiedContractsResponse, VerifiedContractsFilters, VerifiedContractsCounters } from 'types/api/contracts'; 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 { IndexingStatus } from 'types/api/indexingStatus';
import type { InternalTransactionsResponse } from 'types/api/internalTransaction'; import type { InternalTransactionsResponse } from 'types/api/internalTransaction';
import type { L2DepositsResponse, L2DepositsItem } from 'types/api/l2Deposits'; import type { L2DepositsResponse, L2DepositsItem } from 'types/api/l2Deposits';
...@@ -176,6 +185,34 @@ export const RESOURCES = { ...@@ -176,6 +185,34 @@ export const RESOURCES = {
basePath: getFeaturePayload(config.features.stats)?.api.basePath, 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 // VISUALIZATION
visualize_sol2uml: { visualize_sol2uml: {
path: '/api/v1/solidity\\:visualize-contracts', path: '/api/v1/solidity\\:visualize-contracts',
...@@ -613,7 +650,8 @@ export type PaginatedResources = 'blocks' | 'block_txs' | ...@@ -613,7 +650,8 @@ export type PaginatedResources = 'blocks' | 'block_txs' |
'l2_output_roots' | 'l2_withdrawals' | 'l2_txn_batches' | 'l2_deposits' | 'l2_output_roots' | 'l2_withdrawals' | 'l2_txn_batches' | 'l2_deposits' |
'zkevm_l2_txn_batches' | 'zkevm_l2_txn_batch_txs' | 'zkevm_l2_txn_batches' | 'zkevm_l2_txn_batch_txs' |
'withdrawals' | 'address_withdrawals' | 'block_withdrawals' | '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>; export type PaginatedResponse<Q extends PaginatedResources> = ResourcePayload<Q>;
...@@ -712,6 +750,10 @@ Q extends 'zkevm_l2_txn_batches_count' ? number : ...@@ -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' ? ZkEvmL2TxnBatch :
Q extends 'zkevm_l2_txn_batch_txs' ? ZkEvmL2TxnBatchTxs : Q extends 'zkevm_l2_txn_batch_txs' ? ZkEvmL2TxnBatchTxs :
Q extends 'config_backend_version' ? BackendVersionConfig : 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; never;
/* eslint-enable @typescript-eslint/indent */ /* eslint-enable @typescript-eslint/indent */
...@@ -731,6 +773,8 @@ Q extends 'token_inventory' ? TokenInventoryFilters : ...@@ -731,6 +773,8 @@ Q extends 'token_inventory' ? TokenInventoryFilters :
Q extends 'tokens' ? TokensFilters : Q extends 'tokens' ? TokensFilters :
Q extends 'tokens_bridged' ? TokensBridgedFilters : Q extends 'tokens_bridged' ? TokensBridgedFilters :
Q extends 'verified_contracts' ? VerifiedContractsFilters : Q extends 'verified_contracts' ? VerifiedContractsFilters :
Q extends 'addresses_lookup' ? EnsAddressLookupFilters :
Q extends 'domains_lookup' ? EnsDomainLookupFilters :
never; never;
/* eslint-enable @typescript-eslint/indent */ /* eslint-enable @typescript-eslint/indent */
...@@ -740,5 +784,7 @@ Q extends 'tokens' ? TokensSorting : ...@@ -740,5 +784,7 @@ Q extends 'tokens' ? TokensSorting :
Q extends 'tokens_bridged' ? TokensSorting : Q extends 'tokens_bridged' ? TokensSorting :
Q extends 'verified_contracts' ? VerifiedContractsSorting : Q extends 'verified_contracts' ? VerifiedContractsSorting :
Q extends 'address_txs' ? TransactionsSorting : Q extends 'address_txs' ? TransactionsSorting :
Q extends 'addresses_lookup' ? EnsLookupSorting :
Q extends 'domains_lookup' ? EnsLookupSorting :
never; never;
/* eslint-enable @typescript-eslint/indent */ /* eslint-enable @typescript-eslint/indent */
import { useQueryClient } from '@tanstack/react-query'; import { useQueryClient } from '@tanstack/react-query';
import _omit from 'lodash/omit';
import _pickBy from 'lodash/pickBy'; import _pickBy from 'lodash/pickBy';
import React from 'react'; import React from 'react';
...@@ -18,8 +19,8 @@ import type { ApiResource, ResourceName, ResourcePathParams } from './resources' ...@@ -18,8 +19,8 @@ import type { ApiResource, ResourceName, ResourcePathParams } from './resources'
export interface Params<R extends ResourceName> { export interface Params<R extends ResourceName> {
pathParams?: ResourcePathParams<R>; pathParams?: ResourcePathParams<R>;
queryParams?: Record<string, string | Array<string> | number | undefined>; queryParams?: Record<string, string | Array<string> | number | boolean | undefined>;
fetchParams?: Pick<FetchParams, 'body' | 'method' | 'signal'>; fetchParams?: Pick<FetchParams, 'body' | 'method' | 'signal' | 'headers'>;
} }
export default function useApiFetch() { export default function useApiFetch() {
...@@ -40,6 +41,7 @@ export default function useApiFetch() { ...@@ -40,6 +41,7 @@ export default function useApiFetch() {
'x-endpoint': resource.endpoint && isNeedProxy() ? resource.endpoint : undefined, 'x-endpoint': resource.endpoint && isNeedProxy() ? resource.endpoint : undefined,
Authorization: resource.endpoint && resource.needAuth ? apiToken : undefined, Authorization: resource.endpoint && resource.needAuth ? apiToken : undefined,
'x-csrf-token': withBody && csrfToken ? csrfToken : undefined, 'x-csrf-token': withBody && csrfToken ? csrfToken : undefined,
...fetchParams?.headers,
}, Boolean) as HeadersInit; }, Boolean) as HeadersInit;
return fetch<SuccessType, ErrorType>( return fetch<SuccessType, ErrorType>(
...@@ -51,7 +53,7 @@ export default function useApiFetch() { ...@@ -51,7 +53,7 @@ export default function useApiFetch() {
// change condition here if something is changed // change condition here if something is changed
credentials: config.features.account.isEnabled ? 'include' : 'same-origin', credentials: config.features.account.isEnabled ? 'include' : 'same-origin',
headers, headers,
...fetchParams, ..._omit(fetchParams, 'headers'),
}, },
{ {
resource: resource.path, resource: resource.path,
......
...@@ -15,6 +15,7 @@ const AppContext = createContext<PageProps>({ ...@@ -15,6 +15,7 @@ const AppContext = createContext<PageProps>({
hash: '', hash: '',
number: '', number: '',
q: '', q: '',
name: '',
}); });
export function AppContextProvider({ children, pageProps }: Props) { export function AppContextProvider({ children, pageProps }: Props) {
......
...@@ -13,7 +13,8 @@ export enum NAMES { ...@@ -13,7 +13,8 @@ export enum NAMES {
INDEXING_ALERT='indexing_alert', INDEXING_ALERT='indexing_alert',
ADBLOCK_DETECTED='adblock_detected', ADBLOCK_DETECTED='adblock_detected',
MIXPANEL_DEBUG='_mixpanel_debug', MIXPANEL_DEBUG='_mixpanel_debug',
ADDRESS_NFT_DISPLAY_TYPE='address_nft_display_type' ADDRESS_NFT_DISPLAY_TYPE='address_nft_display_type',
UUID='uuid',
} }
export function get(name?: NAMES | undefined | null, serverCookie?: string) { export function get(name?: NAMES | undefined | null, serverCookie?: string) {
......
export const STORAGE_KEY = 'growthbook:experiments';
export const STORAGE_LIMIT = 20;
import { GrowthBook } from '@growthbook/growthbook-react';
import config from 'configs/app';
import * as mixpanel from 'lib/mixpanel';
import { STORAGE_KEY, STORAGE_LIMIT } from './consts';
export interface GrowthBookFeatures {
test_value: string;
}
export const growthBook = (() => {
const feature = config.features.growthBook;
if (!feature.isEnabled) {
return;
}
return new GrowthBook<GrowthBookFeatures>({
apiHost: 'https://cdn.growthbook.io',
clientKey: feature.clientKey,
enableDevMode: config.app.isDev,
attributes: {
id: mixpanel.getUuid(),
chain_id: config.chain.id,
},
trackingCallback: (experiment, result) => {
if (isExperimentStarted(experiment.key)) {
return;
}
saveExperimentInStorage(experiment.key);
mixpanel.logEvent(mixpanel.EventTypes.EXPERIMENT_STARTED, {
'Experiment name': experiment.key,
'Variant name': result.value,
Source: 'growthbook',
});
},
});
})();
function getStorageValue(): Array<unknown> | undefined {
const item = window.localStorage.getItem(STORAGE_KEY);
if (!item) {
return;
}
try {
const parsedValue = JSON.parse(item);
if (Array.isArray(parsedValue)) {
return parsedValue;
}
} catch {
return;
}
}
function isExperimentStarted(key: string): boolean {
const items = getStorageValue() ?? [];
return items.some((item) => item === key);
}
function saveExperimentInStorage(key: string) {
const items = getStorageValue() ?? [];
const newItems = [ key, ...items ].slice(0, STORAGE_LIMIT);
try {
window.localStorage.setItem(STORAGE_KEY, JSON.stringify(newItems));
} catch (error) {}
}
import type { WidenPrimitives } from '@growthbook/growthbook';
import { useFeatureValue, useGrowthBook } from '@growthbook/growthbook-react';
import type { GrowthBookFeatures } from './init';
export default function useGbFeatureValue<Name extends keyof GrowthBookFeatures>(
name: Name,
fallback: GrowthBookFeatures[Name],
): { value: WidenPrimitives<GrowthBookFeatures[Name]>; isLoading: boolean } {
const value = useFeatureValue(name, fallback);
const growthBook = useGrowthBook();
return { value, isLoading: !(growthBook?.ready ?? true) };
}
import React from 'react';
import { SECOND } from 'lib/consts';
import { growthBook } from './init';
export default function useLoadFeatures() {
React.useEffect(() => {
growthBook?.setAttributes({
...growthBook.getAttributes(),
timezone: Intl.DateTimeFormat().resolvedOptions().timeZone,
language: window.navigator.language,
});
growthBook?.loadFeatures({ timeout: SECOND });
}, []);
}
...@@ -53,6 +53,12 @@ export default function useNavItems(): ReturnType { ...@@ -53,6 +53,12 @@ export default function useNavItems(): ReturnType {
icon: 'verified', icon: 'verified',
isActive: pathname === '/verified-contracts', 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) { if (config.features.zkEvmRollup.isEnabled) {
blockchainNavItems = [ blockchainNavItems = [
...@@ -69,6 +75,7 @@ export default function useNavItems(): ReturnType { ...@@ -69,6 +75,7 @@ export default function useNavItems(): ReturnType {
[ [
topAccounts, topAccounts,
verifiedContracts, verifiedContracts,
ensLookup,
].filter(Boolean), ].filter(Boolean),
]; ];
} else if (config.features.optimisticRollup.isEnabled) { } else if (config.features.optimisticRollup.isEnabled) {
...@@ -90,6 +97,7 @@ export default function useNavItems(): ReturnType { ...@@ -90,6 +97,7 @@ export default function useNavItems(): ReturnType {
[ [
topAccounts, topAccounts,
verifiedContracts, verifiedContracts,
ensLookup,
].filter(Boolean), ].filter(Boolean),
]; ];
} else { } else {
...@@ -98,6 +106,7 @@ export default function useNavItems(): ReturnType { ...@@ -98,6 +106,7 @@ export default function useNavItems(): ReturnType {
blocks, blocks,
topAccounts, topAccounts,
verifiedContracts, verifiedContracts,
ensLookup,
config.features.beaconChain.isEnabled && { config.features.beaconChain.isEnabled && {
text: 'Withdrawals', text: 'Withdrawals',
nextRoute: { pathname: '/withdrawals' as const }, nextRoute: { pathname: '/withdrawals' as const },
......
...@@ -40,6 +40,8 @@ const OG_TYPE_DICT: Record<Route['pathname'], OGPageType> = { ...@@ -40,6 +40,8 @@ const OG_TYPE_DICT: Record<Route['pathname'], OGPageType> = {
'/zkevm-l2-txn-batches': 'Root page', '/zkevm-l2-txn-batches': 'Root page',
'/zkevm-l2-txn-batch/[number]': 'Regular page', '/zkevm-l2-txn-batch/[number]': 'Regular page',
'/404': 'Regular page', '/404': 'Regular page',
'/name-domains': 'Root page',
'/name-domains/[name]': 'Regular page',
// service routes, added only to make typescript happy // service routes, added only to make typescript happy
'/login': 'Regular page', '/login': 'Regular page',
......
...@@ -43,6 +43,8 @@ const TEMPLATE_MAP: Record<Route['pathname'], string> = { ...@@ -43,6 +43,8 @@ const TEMPLATE_MAP: Record<Route['pathname'], string> = {
'/zkevm-l2-txn-batches': DEFAULT_TEMPLATE, '/zkevm-l2-txn-batches': DEFAULT_TEMPLATE,
'/zkevm-l2-txn-batch/[number]': DEFAULT_TEMPLATE, '/zkevm-l2-txn-batch/[number]': DEFAULT_TEMPLATE,
'/404': DEFAULT_TEMPLATE, '/404': DEFAULT_TEMPLATE,
'/name-domains': DEFAULT_TEMPLATE,
'/name-domains/[name]': DEFAULT_TEMPLATE,
// service routes, added only to make typescript happy // service routes, added only to make typescript happy
'/login': DEFAULT_TEMPLATE, '/login': DEFAULT_TEMPLATE,
......
...@@ -33,11 +33,13 @@ const TEMPLATE_MAP: Record<Route['pathname'], string> = { ...@@ -33,11 +33,13 @@ const TEMPLATE_MAP: Record<Route['pathname'], string> = {
'/csv-export': 'export data to CSV', '/csv-export': 'export data to CSV',
'/l2-deposits': 'deposits (L1 > L2)', '/l2-deposits': 'deposits (L1 > L2)',
'/l2-output-roots': 'output roots', '/l2-output-roots': 'output roots',
'/l2-txn-batches': 'Tx batches (L2 blocks)', '/l2-txn-batches': 'tx batches (L2 blocks)',
'/l2-withdrawals': 'withdrawals (L2 > L1)', '/l2-withdrawals': 'withdrawals (L2 > L1)',
'/zkevm-l2-txn-batches': 'zkEvm L2 Tx batches', '/zkevm-l2-txn-batches': 'zkEvm L2 Tx batches',
'/zkevm-l2-txn-batch/[number]': 'zkEvm L2 Tx batch %number%', '/zkevm-l2-txn-batch/[number]': 'zkEvm L2 Tx batch %number%',
'/404': 'error - page not found', '/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 // service routes, added only to make typescript happy
'/login': 'login', '/login': 'login',
......
export default function getGoogleAnalyticsClientId() {
return window.ga?.getAll()[0].get('clientId');
}
...@@ -38,6 +38,8 @@ export const PAGE_TYPE_DICT: Record<Route['pathname'], string> = { ...@@ -38,6 +38,8 @@ export const PAGE_TYPE_DICT: Record<Route['pathname'], string> = {
'/zkevm-l2-txn-batches': 'ZkEvm L2 Tx batches', '/zkevm-l2-txn-batches': 'ZkEvm L2 Tx batches',
'/zkevm-l2-txn-batch/[number]': 'ZkEvm L2 Tx batch details', '/zkevm-l2-txn-batch/[number]': 'ZkEvm L2 Tx batch details',
'/404': '404', '/404': '404',
'/name-domains': 'Domains search and resolve',
'/name-domains/[name]': 'Domain details',
// service routes, added only to make typescript happy // service routes, added only to make typescript happy
'/login': 'Login', '/login': 'Login',
......
import * as cookies from 'lib/cookies';
import * as growthBook from 'lib/growthbook/consts';
import isBrowser from 'lib/isBrowser';
export default function getUuid() {
const cookie = cookies.get(cookies.NAMES.UUID);
if (cookie) {
return cookie;
}
const uuid = crypto.randomUUID();
cookies.set(cookies.NAMES.UUID, uuid);
if (isBrowser()) {
window.localStorage.removeItem(growthBook.STORAGE_KEY);
}
return uuid;
}
import getPageType from './getPageType'; import getPageType from './getPageType';
import getUuid from './getUuid';
import logEvent from './logEvent'; import logEvent from './logEvent';
import useInit from './useInit'; import useInit from './useInit';
import useLogPageView from './useLogPageView'; import useLogPageView from './useLogPageView';
...@@ -9,4 +10,5 @@ export { ...@@ -9,4 +10,5 @@ export {
useLogPageView, useLogPageView,
logEvent, logEvent,
getPageType, getPageType,
getUuid,
}; };
import config from 'configs/app';
import delay from 'lib/delay';
export default function isGoogleAnalyticsLoaded(retries = 3): Promise<boolean> {
if (!retries || !config.features.googleAnalytics.isEnabled) {
return Promise.resolve(false);
}
return typeof window.ga?.getAll === 'function' ? Promise.resolve(true) : delay(500).then(() => isGoogleAnalyticsLoaded(retries - 1));
}
...@@ -9,8 +9,7 @@ import config from 'configs/app'; ...@@ -9,8 +9,7 @@ import config from 'configs/app';
import * as cookies from 'lib/cookies'; import * as cookies from 'lib/cookies';
import getQueryParamString from 'lib/router/getQueryParamString'; import getQueryParamString from 'lib/router/getQueryParamString';
import getGoogleAnalyticsClientId from './getGoogleAnalyticsClientId'; import getUuid from './getUuid';
import isGoogleAnalyticsLoaded from './isGoogleAnalyticsLoaded';
export default function useMixpanelInit() { export default function useMixpanelInit() {
const [ isInited, setIsInited ] = React.useState(false); const [ isInited, setIsInited ] = React.useState(false);
...@@ -18,36 +17,34 @@ export default function useMixpanelInit() { ...@@ -18,36 +17,34 @@ export default function useMixpanelInit() {
const debugFlagQuery = React.useRef(getQueryParamString(router.query._mixpanel_debug)); const debugFlagQuery = React.useRef(getQueryParamString(router.query._mixpanel_debug));
React.useEffect(() => { React.useEffect(() => {
isGoogleAnalyticsLoaded().then((isGALoaded) => { const feature = config.features.mixpanel;
const feature = config.features.mixpanel; if (!feature.isEnabled) {
if (!feature.isEnabled) { return;
return; }
}
const debugFlagCookie = cookies.get(cookies.NAMES.MIXPANEL_DEBUG);
const debugFlagCookie = cookies.get(cookies.NAMES.MIXPANEL_DEBUG);
const mixpanelConfig: Partial<Config> = {
const mixpanelConfig: Partial<Config> = { debug: Boolean(debugFlagQuery.current || debugFlagCookie),
debug: Boolean(debugFlagQuery.current || debugFlagCookie), };
}; const isAuth = Boolean(cookies.get(cookies.NAMES.API_TOKEN));
const isAuth = Boolean(cookies.get(cookies.NAMES.API_TOKEN));
mixpanel.init(feature.projectToken, mixpanelConfig);
mixpanel.init(feature.projectToken, mixpanelConfig); mixpanel.register({
mixpanel.register({ 'Chain id': config.chain.id,
'Chain id': config.chain.id, Environment: config.app.isDev ? 'Dev' : 'Prod',
Environment: config.app.isDev ? 'Dev' : 'Prod', Authorized: isAuth,
Authorized: isAuth, 'Viewport width': window.innerWidth,
'Viewport width': window.innerWidth, 'Viewport height': window.innerHeight,
'Viewport height': window.innerHeight, Language: window.navigator.language,
Language: window.navigator.language, 'Device type': _capitalize(deviceType),
'User id': isGALoaded ? getGoogleAnalyticsClientId() : undefined, 'User id': getUuid(),
'Device type': _capitalize(deviceType),
});
setIsInited(true);
if (debugFlagQuery.current && !debugFlagCookie) {
cookies.set(cookies.NAMES.MIXPANEL_DEBUG, 'true');
}
}); });
setIsInited(true);
if (debugFlagQuery.current && !debugFlagCookie) {
cookies.set(cookies.NAMES.MIXPANEL_DEBUG, 'true');
}
}, []); }, []);
return isInited; return isInited;
......
...@@ -3,17 +3,21 @@ import type { WalletType } from 'types/client/wallets'; ...@@ -3,17 +3,21 @@ import type { WalletType } from 'types/client/wallets';
export enum EventTypes { export enum EventTypes {
PAGE_VIEW = 'Page view', PAGE_VIEW = 'Page view',
SEARCH_QUERY = 'Search query', SEARCH_QUERY = 'Search query',
LOCAL_SEARCH = 'Local search',
ADD_TO_WALLET = 'Add to wallet', ADD_TO_WALLET = 'Add to wallet',
ACCOUNT_ACCESS = 'Account access', ACCOUNT_ACCESS = 'Account access',
PRIVATE_TAG = 'Private tag', PRIVATE_TAG = 'Private tag',
VERIFY_ADDRESS = 'Verify address', VERIFY_ADDRESS = 'Verify address',
VERIFY_TOKEN = 'Verify token', VERIFY_TOKEN = 'Verify token',
WALLET_CONNECT = 'Wallet connect', WALLET_CONNECT = 'Wallet connect',
WALLET_ACTION = 'Wallet action',
CONTRACT_INTERACTION = 'Contract interaction', CONTRACT_INTERACTION = 'Contract interaction',
CONTRACT_VERIFICATION = 'Contract verification', CONTRACT_VERIFICATION = 'Contract verification',
QR_CODE = 'QR code', QR_CODE = 'QR code',
PAGE_WIDGET = 'Page widget', PAGE_WIDGET = 'Page widget',
TX_INTERPRETATION_INTERACTION = 'Transaction interpratetion interaction' TX_INTERPRETATION_INTERACTION = 'Transaction interpratetion interaction',
EXPERIMENT_STARTED = 'Experiment started',
FILTERS = 'Filters'
} }
/* eslint-disable @typescript-eslint/indent */ /* eslint-disable @typescript-eslint/indent */
...@@ -30,6 +34,10 @@ Type extends EventTypes.SEARCH_QUERY ? { ...@@ -30,6 +34,10 @@ Type extends EventTypes.SEARCH_QUERY ? {
'Source page type': string; 'Source page type': string;
'Result URL': string; 'Result URL': string;
} : } :
Type extends EventTypes.LOCAL_SEARCH ? {
'Search query': string;
'Source': 'Marketplace';
} :
Type extends EventTypes.ADD_TO_WALLET ? ( Type extends EventTypes.ADD_TO_WALLET ? (
{ {
'Wallet': WalletType; 'Wallet': WalletType;
...@@ -65,6 +73,9 @@ Type extends EventTypes.WALLET_CONNECT ? { ...@@ -65,6 +73,9 @@ Type extends EventTypes.WALLET_CONNECT ? {
'Source': 'Header' | 'Smart contracts'; 'Source': 'Header' | 'Smart contracts';
'Status': 'Started' | 'Connected'; 'Status': 'Started' | 'Connected';
} : } :
Type extends EventTypes.WALLET_ACTION ? {
'Action': 'Open' | 'Address click';
} :
Type extends EventTypes.CONTRACT_INTERACTION ? { Type extends EventTypes.CONTRACT_INTERACTION ? {
'Method type': 'Read' | 'Write'; 'Method type': 'Read' | 'Write';
'Method name': string; 'Method name': string;
...@@ -76,11 +87,25 @@ Type extends EventTypes.CONTRACT_VERIFICATION ? { ...@@ -76,11 +87,25 @@ Type extends EventTypes.CONTRACT_VERIFICATION ? {
Type extends EventTypes.QR_CODE ? { Type extends EventTypes.QR_CODE ? {
'Page type': string; 'Page type': string;
} : } :
Type extends EventTypes.PAGE_WIDGET ? { Type extends EventTypes.PAGE_WIDGET ? (
'Type': 'Tokens dropdown' | 'Tokens show all (icon)' | 'Add to watchlist' | 'Address actions (more button)'; {
} : '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 extends EventTypes.TX_INTERPRETATION_INTERACTION ? {
'Type': 'Address click' | 'Token click'; 'Type': 'Address click' | 'Token click';
} : } :
Type extends EventTypes.EXPERIMENT_STARTED ? {
'Experiment name': string;
'Variant name': string;
'Source': 'growthbook';
} :
Type extends EventTypes.FILTERS ? {
'Source': 'Marketplace';
'Filter name': string;
} :
undefined; undefined;
/* eslint-enable @typescript-eslint/indent */ /* eslint-enable @typescript-eslint/indent */
...@@ -15,6 +15,19 @@ export const withName: AddressParam = { ...@@ -15,6 +15,19 @@ export const withName: AddressParam = {
private_tags: [], private_tags: [],
watchlist_names: [], watchlist_names: [],
public_tags: [], 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 = { export const withoutName: AddressParam = {
...@@ -26,6 +39,7 @@ export const withoutName: AddressParam = { ...@@ -26,6 +39,7 @@ export const withoutName: AddressParam = {
private_tags: [], private_tags: [],
watchlist_names: [], watchlist_names: [],
public_tags: [], public_tags: [],
ens_domain_name: null,
}; };
export const token: Address = { export const token: Address = {
...@@ -56,6 +70,7 @@ export const token: Address = { ...@@ -56,6 +70,7 @@ export const token: Address = {
has_token_transfers: true, has_token_transfers: true,
has_tokens: true, has_tokens: true,
has_validated_blocks: false, has_validated_blocks: false,
ens_domain_name: null,
}; };
export const contract: Address = { export const contract: Address = {
...@@ -86,6 +101,7 @@ export const contract: Address = { ...@@ -86,6 +101,7 @@ export const contract: Address = {
token: null, token: null,
watchlist_names: [ watchlistName ], watchlist_names: [ watchlistName ],
watchlist_address_id: 42, watchlist_address_id: 42,
ens_domain_name: null,
}; };
export const validator: Address = { export const validator: Address = {
...@@ -116,4 +132,5 @@ export const validator: Address = { ...@@ -116,4 +132,5 @@ export const validator: Address = {
token: null, token: null,
watchlist_names: [], watchlist_names: [],
watchlist_address_id: null, watchlist_address_id: null,
ens_domain_name: null,
}; };
...@@ -22,6 +22,7 @@ export const base: Block = { ...@@ -22,6 +22,7 @@ export const base: Block = {
private_tags: [], private_tags: [],
public_tags: [], public_tags: [],
watchlist_names: [], watchlist_names: [],
ens_domain_name: null,
}, },
nonce: '0x0000000000000000', nonce: '0x0000000000000000',
parent_hash: '0x44125f0eb36a9d942e0c23bb4e8117f7ba86a9537a69b59c0025986ed2b7500f', parent_hash: '0x44125f0eb36a9d942e0c23bb4e8117f7ba86a9537a69b59c0025986ed2b7500f',
...@@ -71,6 +72,7 @@ export const genesis: Block = { ...@@ -71,6 +72,7 @@ export const genesis: Block = {
private_tags: [], private_tags: [],
public_tags: [], public_tags: [],
watchlist_names: [], watchlist_names: [],
ens_domain_name: 'kitty.kitty.cat.eth',
}, },
nonce: '0x0000000000000000', nonce: '0x0000000000000000',
parent_hash: '0x0000000000000000000000000000000000000000000000000000000000000000', parent_hash: '0x0000000000000000000000000000000000000000000000000000000000000000',
...@@ -99,6 +101,7 @@ export const base2: Block = { ...@@ -99,6 +101,7 @@ export const base2: Block = {
private_tags: [], private_tags: [],
public_tags: [], public_tags: [],
watchlist_names: [], watchlist_names: [],
ens_domain_name: null,
}, },
timestamp: '2022-11-11T11:46:05Z', timestamp: '2022-11-11T11:46:05Z',
tx_count: 253, tx_count: 253,
......
...@@ -10,6 +10,7 @@ export const contract1: VerifiedContract = { ...@@ -10,6 +10,7 @@ export const contract1: VerifiedContract = {
private_tags: [], private_tags: [],
public_tags: [], public_tags: [],
watchlist_names: [], watchlist_names: [],
ens_domain_name: null,
}, },
coin_balance: '2346534676900000008', coin_balance: '2346534676900000008',
compiler_version: 'v0.8.17+commit.8df45f5f', compiler_version: 'v0.8.17+commit.8df45f5f',
...@@ -31,6 +32,7 @@ export const contract2: VerifiedContract = { ...@@ -31,6 +32,7 @@ export const contract2: VerifiedContract = {
private_tags: [], private_tags: [],
public_tags: [], public_tags: [],
watchlist_names: [], watchlist_names: [],
ens_domain_name: null,
}, },
coin_balance: '9078234570352343999', coin_balance: '9078234570352343999',
compiler_version: 'v0.3.1+commit.0463ea4c', compiler_version: 'v0.3.1+commit.0463ea4c',
......
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: {},
};
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',
};
...@@ -47,6 +47,15 @@ export const block2: SearchResultBlock = { ...@@ -47,6 +47,15 @@ export const block2: SearchResultBlock = {
url: '/block/0x1af31d7535dded06bab9a88eb40ee2f8d0529a60ab3b8a7be2ba69b008cacbd2', 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 = { export const address1: SearchResultAddressOrContract = {
address: '0xb64a30399f7F6b0C154c2E7Af0a3ec7B0A5b131a', address: '0xb64a30399f7F6b0C154c2E7Af0a3ec7B0A5b131a',
name: null, name: null,
...@@ -55,6 +64,20 @@ export const address1: SearchResultAddressOrContract = { ...@@ -55,6 +64,20 @@ export const address1: SearchResultAddressOrContract = {
url: '/address/0xb64a30399f7F6b0C154c2E7Af0a3ec7B0A5b131a', 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 = { export const contract1: SearchResultAddressOrContract = {
address: '0xb64a30399f7F6b0C154c2E7Af0a3ec7B0A5b131a', address: '0xb64a30399f7F6b0C154c2E7Af0a3ec7B0A5b131a',
name: 'Unknown contract in this network', name: 'Unknown contract in this network',
......
...@@ -3,11 +3,26 @@ import type { HomeStats } from 'types/api/stats'; ...@@ -3,11 +3,26 @@ import type { HomeStats } from 'types/api/stats';
export const base: HomeStats = { export const base: HomeStats = {
average_block_time: 6212.0, average_block_time: 6212.0,
coin_price: '0.00199678', coin_price: '0.00199678',
coin_price_change_percentage: -7.42,
gas_prices: { gas_prices: {
average: 48.0, average: {
fast: 67.5, fiat_price: '1.01',
slow: 48.0, 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', gas_used_today: '4108680603',
market_cap: '330809.96443288102524', market_cap: '330809.96443288102524',
network_utilization_percentage: 1.55372064, network_utilization_percentage: 1.55372064,
......
...@@ -10,6 +10,7 @@ export const erc20: TokenTransfer = { ...@@ -10,6 +10,7 @@ export const erc20: TokenTransfer = {
private_tags: [], private_tags: [],
public_tags: [], public_tags: [],
watchlist_names: [], watchlist_names: [],
ens_domain_name: null,
}, },
to: { to: {
hash: '0x7d20a8D54F955b4483A66aB335635ab66e151c51', hash: '0x7d20a8D54F955b4483A66aB335635ab66e151c51',
...@@ -20,6 +21,7 @@ export const erc20: TokenTransfer = { ...@@ -20,6 +21,7 @@ export const erc20: TokenTransfer = {
private_tags: [], private_tags: [],
public_tags: [], public_tags: [],
watchlist_names: [], watchlist_names: [],
ens_domain_name: 'kitty.kitty.cat.eth',
}, },
token: { token: {
address: '0x55d536e4d6c1993d8ef2e2a4ef77f02088419420', address: '0x55d536e4d6c1993d8ef2e2a4ef77f02088419420',
...@@ -55,6 +57,7 @@ export const erc721: TokenTransfer = { ...@@ -55,6 +57,7 @@ export const erc721: TokenTransfer = {
private_tags: [], private_tags: [],
public_tags: [], public_tags: [],
watchlist_names: [], watchlist_names: [],
ens_domain_name: 'kitty.kitty.cat.eth',
}, },
to: { to: {
hash: '0x47eE48AEBc4ab9Ed908b805b8c8dAAa71B31Db1A', hash: '0x47eE48AEBc4ab9Ed908b805b8c8dAAa71B31Db1A',
...@@ -65,6 +68,7 @@ export const erc721: TokenTransfer = { ...@@ -65,6 +68,7 @@ export const erc721: TokenTransfer = {
private_tags: [], private_tags: [],
public_tags: [], public_tags: [],
watchlist_names: [], watchlist_names: [],
ens_domain_name: null,
}, },
token: { token: {
address: '0x363574E6C5C71c343d7348093D84320c76d5Dd29', address: '0x363574E6C5C71c343d7348093D84320c76d5Dd29',
...@@ -99,6 +103,7 @@ export const erc1155A: TokenTransfer = { ...@@ -99,6 +103,7 @@ export const erc1155A: TokenTransfer = {
private_tags: [], private_tags: [],
public_tags: [], public_tags: [],
watchlist_names: [], watchlist_names: [],
ens_domain_name: null,
}, },
to: { to: {
hash: '0xBb36c792B9B45Aaf8b848A1392B0d6559202729E', hash: '0xBb36c792B9B45Aaf8b848A1392B0d6559202729E',
...@@ -109,6 +114,7 @@ export const erc1155A: TokenTransfer = { ...@@ -109,6 +114,7 @@ export const erc1155A: TokenTransfer = {
private_tags: [], private_tags: [],
public_tags: [], public_tags: [],
watchlist_names: [], watchlist_names: [],
ens_domain_name: 'kitty.kitty.cat.eth',
}, },
token: { token: {
address: '0xF56b7693E4212C584de4a83117f805B8E89224CB', address: '0xF56b7693E4212C584de4a83117f805B8E89224CB',
......
...@@ -13,6 +13,7 @@ export const base: InternalTransaction = { ...@@ -13,6 +13,7 @@ export const base: InternalTransaction = {
private_tags: [], private_tags: [],
public_tags: [], public_tags: [],
watchlist_names: [], watchlist_names: [],
ens_domain_name: null,
}, },
gas_limit: '757586', gas_limit: '757586',
index: 1, index: 1,
...@@ -27,6 +28,7 @@ export const base: InternalTransaction = { ...@@ -27,6 +28,7 @@ export const base: InternalTransaction = {
private_tags: [], private_tags: [],
public_tags: [], public_tags: [],
watchlist_names: [], watchlist_names: [],
ens_domain_name: null,
}, },
transaction_hash: '0xe9e27dfeb183066e26cfe556f74b7219b08df6951e25d14003d4fc7af8bbff61', transaction_hash: '0xe9e27dfeb183066e26cfe556f74b7219b08df6951e25d14003d4fc7af8bbff61',
type: 'call', type: 'call',
...@@ -61,6 +63,7 @@ export const withContractCreated: InternalTransaction = { ...@@ -61,6 +63,7 @@ export const withContractCreated: InternalTransaction = {
private_tags: [], private_tags: [],
public_tags: [], public_tags: [],
watchlist_names: [], watchlist_names: [],
ens_domain_name: null,
}, },
value: '1420000000000000000', value: '1420000000000000000',
gas_limit: '5433', gas_limit: '5433',
......
...@@ -10,6 +10,7 @@ export const mintToken: TxStateChange = { ...@@ -10,6 +10,7 @@ export const mintToken: TxStateChange = {
private_tags: [], private_tags: [],
public_tags: [], public_tags: [],
watchlist_names: [], watchlist_names: [],
ens_domain_name: null,
}, },
balance_after: null, balance_after: null,
balance_before: null, balance_before: null,
...@@ -47,6 +48,7 @@ export const receiveMintedToken: TxStateChange = { ...@@ -47,6 +48,7 @@ export const receiveMintedToken: TxStateChange = {
private_tags: [], private_tags: [],
public_tags: [], public_tags: [],
watchlist_names: [], watchlist_names: [],
ens_domain_name: null,
}, },
balance_after: '1', balance_after: '1',
balance_before: '0', balance_before: '0',
...@@ -84,6 +86,7 @@ export const transfer1155Token: TxStateChange = { ...@@ -84,6 +86,7 @@ export const transfer1155Token: TxStateChange = {
private_tags: [], private_tags: [],
public_tags: [], public_tags: [],
watchlist_names: [], watchlist_names: [],
ens_domain_name: null,
}, },
balance_after: '1', balance_after: '1',
balance_before: '0', balance_before: '0',
...@@ -115,6 +118,7 @@ export const receiveCoin: TxStateChange = { ...@@ -115,6 +118,7 @@ export const receiveCoin: TxStateChange = {
private_tags: [], private_tags: [],
public_tags: [], public_tags: [],
watchlist_names: [], watchlist_names: [],
ens_domain_name: null,
}, },
balance_after: '443787514723917012805', balance_after: '443787514723917012805',
balance_before: '443787484997510408745', balance_before: '443787484997510408745',
...@@ -134,6 +138,7 @@ export const sendCoin: TxStateChange = { ...@@ -134,6 +138,7 @@ export const sendCoin: TxStateChange = {
private_tags: [], private_tags: [],
public_tags: [], public_tags: [],
watchlist_names: [], watchlist_names: [],
ens_domain_name: null,
}, },
balance_after: '828282622733717191', balance_after: '828282622733717191',
balance_before: '832127467556437753', balance_before: '832127467556437753',
......
...@@ -29,6 +29,7 @@ export const base: Transaction = { ...@@ -29,6 +29,7 @@ export const base: Transaction = {
private_tags: [ ], private_tags: [ ],
public_tags: [ publicTag ], public_tags: [ publicTag ],
watchlist_names: [], watchlist_names: [],
ens_domain_name: 'kitty.kitty.cat.eth',
}, },
gas_limit: '800000', gas_limit: '800000',
gas_price: '48000000000', gas_price: '48000000000',
...@@ -54,6 +55,7 @@ export const base: Transaction = { ...@@ -54,6 +55,7 @@ export const base: Transaction = {
private_tags: [ privateTag ], private_tags: [ privateTag ],
public_tags: [], public_tags: [],
watchlist_names: [ watchlistName ], watchlist_names: [ watchlistName ],
ens_domain_name: null,
}, },
token_transfers: [], token_transfers: [],
token_transfers_overflow: false, token_transfers_overflow: false,
...@@ -97,6 +99,7 @@ export const withContractCreation: Transaction = { ...@@ -97,6 +99,7 @@ export const withContractCreation: Transaction = {
private_tags: [], private_tags: [],
public_tags: [], public_tags: [],
watchlist_names: [], watchlist_names: [],
ens_domain_name: null,
}, },
tx_types: [ tx_types: [
'contract_creation', 'contract_creation',
...@@ -115,6 +118,7 @@ export const withTokenTransfer: Transaction = { ...@@ -115,6 +118,7 @@ export const withTokenTransfer: Transaction = {
private_tags: [ privateTag ], private_tags: [ privateTag ],
public_tags: [], public_tags: [],
watchlist_names: [ watchlistName ], watchlist_names: [ watchlistName ],
ens_domain_name: null,
}, },
token_transfers: [ token_transfers: [
tokenTransferMock.erc20, tokenTransferMock.erc20,
...@@ -168,6 +172,7 @@ export const withRawRevertReason: Transaction = { ...@@ -168,6 +172,7 @@ export const withRawRevertReason: Transaction = {
private_tags: [ ], private_tags: [ ],
public_tags: [], public_tags: [],
watchlist_names: [ ], watchlist_names: [ ],
ens_domain_name: null,
}, },
}; };
...@@ -283,6 +288,7 @@ export const stabilityTx: Transaction = { ...@@ -283,6 +288,7 @@ export const stabilityTx: Transaction = {
private_tags: [], private_tags: [],
public_tags: [], public_tags: [],
watchlist_names: [], watchlist_names: [],
ens_domain_name: null,
}, },
dapp_fee: '34381250000000', dapp_fee: '34381250000000',
token: { token: {
...@@ -307,6 +313,7 @@ export const stabilityTx: Transaction = { ...@@ -307,6 +313,7 @@ export const stabilityTx: Transaction = {
private_tags: [], private_tags: [],
public_tags: [], public_tags: [],
watchlist_names: [], watchlist_names: [],
ens_domain_name: null,
}, },
validator_fee: '34381250000000', validator_fee: '34381250000000',
}, },
......
...@@ -33,6 +33,7 @@ export const txInterpretation: TxInterpretationResponse = { ...@@ -33,6 +33,7 @@ export const txInterpretation: TxInterpretationResponse = {
private_tags: [], private_tags: [],
public_tags: [], public_tags: [],
watchlist_names: [], watchlist_names: [],
ens_domain_name: null,
}, },
}, },
timestamp: { timestamp: {
......
...@@ -9,6 +9,7 @@ function generateCspPolicy() { ...@@ -9,6 +9,7 @@ function generateCspPolicy() {
descriptors.googleAnalytics(), descriptors.googleAnalytics(),
descriptors.googleFonts(), descriptors.googleFonts(),
descriptors.googleReCaptcha(), descriptors.googleReCaptcha(),
descriptors.growthBook(),
descriptors.mixpanel(), descriptors.mixpanel(),
descriptors.monaco(), descriptors.monaco(),
descriptors.safe(), descriptors.safe(),
......
import type CspDev from 'csp-dev';
import config from 'configs/app';
export function growthBook(): CspDev.DirectiveDescriptor {
if (!config.features.growthBook.isEnabled) {
return {};
}
return {
'connect-src': [
'cdn.growthbook.io',
],
};
}
...@@ -4,6 +4,7 @@ export { cloudFlare } from './cloudFlare'; ...@@ -4,6 +4,7 @@ export { cloudFlare } from './cloudFlare';
export { googleAnalytics } from './googleAnalytics'; export { googleAnalytics } from './googleAnalytics';
export { googleFonts } from './googleFonts'; export { googleFonts } from './googleFonts';
export { googleReCaptcha } from './googleReCaptcha'; export { googleReCaptcha } from './googleReCaptcha';
export { growthBook } from './growthBook';
export { mixpanel } from './mixpanel'; export { mixpanel } from './mixpanel';
export { monaco } from './monaco'; export { monaco } from './monaco';
export { safe } from './safe'; export { safe } from './safe';
......
...@@ -10,6 +10,7 @@ export type Props = { ...@@ -10,6 +10,7 @@ export type Props = {
hash: string; hash: string;
number: string; number: string;
q: string; q: string;
name: string;
} }
export const base: GetServerSideProps<Props> = async({ req, query }) => { export const base: GetServerSideProps<Props> = async({ req, query }) => {
...@@ -22,6 +23,7 @@ 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() || '', height_or_hash: query.height_or_hash?.toString() || '',
number: query.number?.toString() || '', number: query.number?.toString() || '',
q: query.q?.toString() || '', q: query.q?.toString() || '',
name: query.name?.toString() || '',
}, },
}; };
}; };
...@@ -126,6 +128,16 @@ export const suave: GetServerSideProps<Props> = async(context) => { ...@@ -126,6 +128,16 @@ export const suave: GetServerSideProps<Props> = async(context) => {
return base(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) => { export const accounts: GetServerSideProps<Props> = async(context) => {
if (config.UI.views.address.hiddenViews?.top_accounts) { if (config.UI.views.address.hiddenViews?.top_accounts) {
return { return {
......
...@@ -37,6 +37,8 @@ declare module "nextjs-routes" { ...@@ -37,6 +37,8 @@ declare module "nextjs-routes" {
| StaticRoute<"/l2-txn-batches"> | StaticRoute<"/l2-txn-batches">
| StaticRoute<"/l2-withdrawals"> | StaticRoute<"/l2-withdrawals">
| StaticRoute<"/login"> | StaticRoute<"/login">
| DynamicRoute<"/name-domains/[name]", { "name": string }>
| StaticRoute<"/name-domains">
| StaticRoute<"/search-results"> | StaticRoute<"/search-results">
| StaticRoute<"/stats"> | StaticRoute<"/stats">
| DynamicRoute<"/token/[hash]", { "hash": string }> | DynamicRoute<"/token/[hash]", { "hash": string }>
......
import type { IncomingMessage } from 'http'; import type { IncomingMessage } from 'http';
import _pick from 'lodash/pick';
import type { NextApiRequest } from 'next'; import type { NextApiRequest } from 'next';
import type { NextApiRequestCookies } from 'next/dist/server/api-utils'; import type { NextApiRequestCookies } from 'next/dist/server/api-utils';
import type { RequestInit, Response } from 'node-fetch'; import type { RequestInit, Response } from 'node-fetch';
...@@ -14,16 +15,18 @@ export default function fetchFactory( ...@@ -14,16 +15,18 @@ export default function fetchFactory(
// first arg can be only a string // first arg can be only a string
// FIXME migrate to RequestInfo later if needed // FIXME migrate to RequestInfo later if needed
return function fetch(url: string, init?: RequestInit): Promise<Response> { 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 apiToken = _req.cookies[cookies.NAMES.API_TOKEN];
const headers = { const headers = {
accept: _req.headers['accept'] || 'application/json', accept: _req.headers['accept'] || 'application/json',
'content-type': _req.headers['content-type'] || 'application/json', 'content-type': _req.headers['content-type'] || 'application/json',
cookie: apiToken ? `${ cookies.NAMES.API_TOKEN }=${ apiToken }` : '', cookie: apiToken ? `${ cookies.NAMES.API_TOKEN }=${ apiToken }` : '',
...(csrfToken ? { 'x-csrf-token': String(csrfToken) } : {}), ..._pick(_req.headers, [
...(authToken ? { Authorization: String(authToken) } : {}), 'x-csrf-token',
'Authorization',
// feature flags
'updated-gas-oracle',
]) as Record<string, string | undefined>,
}; };
httpLogger.logger.info({ httpLogger.logger.info({
......
import type { ChakraProps } from '@chakra-ui/react'; import type { ChakraProps } from '@chakra-ui/react';
import { GrowthBookProvider } from '@growthbook/growthbook-react';
import * as Sentry from '@sentry/react'; import * as Sentry from '@sentry/react';
import { QueryClientProvider } from '@tanstack/react-query'; import { QueryClientProvider } from '@tanstack/react-query';
import { ReactQueryDevtools } from '@tanstack/react-query-devtools'; import { ReactQueryDevtools } from '@tanstack/react-query-devtools';
...@@ -12,6 +13,8 @@ import useQueryClientConfig from 'lib/api/useQueryClientConfig'; ...@@ -12,6 +13,8 @@ import useQueryClientConfig from 'lib/api/useQueryClientConfig';
import { AppContextProvider } from 'lib/contexts/app'; import { AppContextProvider } from 'lib/contexts/app';
import { ChakraProvider } from 'lib/contexts/chakra'; import { ChakraProvider } from 'lib/contexts/chakra';
import { ScrollDirectionProvider } from 'lib/contexts/scrollDirection'; import { ScrollDirectionProvider } from 'lib/contexts/scrollDirection';
import { growthBook } from 'lib/growthbook/init';
import useLoadFeatures from 'lib/growthbook/useLoadFeatures';
import { SocketProvider } from 'lib/socket/context'; import { SocketProvider } from 'lib/socket/context';
import theme from 'theme'; import theme from 'theme';
import AppErrorBoundary from 'ui/shared/AppError/AppErrorBoundary'; import AppErrorBoundary from 'ui/shared/AppError/AppErrorBoundary';
...@@ -39,6 +42,8 @@ const ERROR_SCREEN_STYLES: ChakraProps = { ...@@ -39,6 +42,8 @@ const ERROR_SCREEN_STYLES: ChakraProps = {
function MyApp({ Component, pageProps }: AppPropsWithLayout) { function MyApp({ Component, pageProps }: AppPropsWithLayout) {
useLoadFeatures();
const queryClient = useQueryClientConfig(); const queryClient = useQueryClientConfig();
const handleError = React.useCallback((error: Error) => { const handleError = React.useCallback((error: Error) => {
...@@ -56,11 +61,13 @@ function MyApp({ Component, pageProps }: AppPropsWithLayout) { ...@@ -56,11 +61,13 @@ function MyApp({ Component, pageProps }: AppPropsWithLayout) {
<Web3ModalProvider> <Web3ModalProvider>
<AppContextProvider pageProps={ pageProps }> <AppContextProvider pageProps={ pageProps }>
<QueryClientProvider client={ queryClient }> <QueryClientProvider client={ queryClient }>
<ScrollDirectionProvider> <GrowthBookProvider growthbook={ growthBook }>
<SocketProvider url={ `${ config.api.socket }${ config.api.basePath }/socket/v2` }> <ScrollDirectionProvider>
{ getLayout(<Component { ...pageProps }/>) } <SocketProvider url={ `${ config.api.socket }${ config.api.basePath }/socket/v2` }>
</SocketProvider> { getLayout(<Component { ...pageProps }/>) }
</ScrollDirectionProvider> </SocketProvider>
</ScrollDirectionProvider>
</GrowthBookProvider>
<ReactQueryDevtools buttonPosition="bottom-left" position="left"/> <ReactQueryDevtools buttonPosition="bottom-left" position="left"/>
<GoogleAnalytics/> <GoogleAnalytics/>
</QueryClientProvider> </QueryClientProvider>
......
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';
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';
import { ChakraProvider } from '@chakra-ui/react'; import { ChakraProvider } from '@chakra-ui/react';
import { GrowthBookProvider } from '@growthbook/growthbook-react';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { createWeb3Modal, defaultWagmiConfig } from '@web3modal/wagmi/react'; import { createWeb3Modal, defaultWagmiConfig } from '@web3modal/wagmi/react';
import React from 'react'; import React from 'react';
...@@ -29,6 +30,7 @@ const defaultAppContext = { ...@@ -29,6 +30,7 @@ const defaultAppContext = {
hash: '', hash: '',
number: '', number: '',
q: '', q: '',
name: '',
}, },
}; };
...@@ -63,9 +65,11 @@ const TestApp = ({ children, withSocket, appContext = defaultAppContext }: Props ...@@ -63,9 +65,11 @@ const TestApp = ({ children, withSocket, appContext = defaultAppContext }: Props
<QueryClientProvider client={ queryClient }> <QueryClientProvider client={ queryClient }>
<SocketProvider url={ withSocket ? `ws://${ app.domain }:${ app.socketPort }` : undefined }> <SocketProvider url={ withSocket ? `ws://${ app.domain }:${ app.socketPort }` : undefined }>
<AppContextProvider { ...appContext }> <AppContextProvider { ...appContext }>
<WagmiConfig config={ wagmiConfig }> <GrowthBookProvider>
{ children } <WagmiConfig config={ wagmiConfig }>
</WagmiConfig> { children }
</WagmiConfig>
</GrowthBookProvider>
</AppContextProvider> </AppContextProvider>
</SocketProvider> </SocketProvider>
</QueryClientProvider> </QueryClientProvider>
......
...@@ -31,6 +31,8 @@ ...@@ -31,6 +31,8 @@
| "email-sent" | "email-sent"
| "email" | "email"
| "empty_search_result" | "empty_search_result"
| "ENS_slim"
| "ENS"
| "error-pages/404" | "error-pages/404"
| "error-pages/422" | "error-pages/422"
| "error-pages/429" | "error-pages/429"
......
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',
};
...@@ -41,6 +41,7 @@ export const ADDRESS_INFO: Address = { ...@@ -41,6 +41,7 @@ export const ADDRESS_INFO: Address = {
public_tags: [], public_tags: [],
watchlist_names: [], watchlist_names: [],
watchlist_address_id: null, watchlist_address_id: null,
ens_domain_name: null,
}; };
export const ADDRESS_COUNTERS: AddressCounters = { export const ADDRESS_COUNTERS: AddressCounters = {
...@@ -71,6 +72,7 @@ export const TOP_ADDRESS: AddressesItem = { ...@@ -71,6 +72,7 @@ export const TOP_ADDRESS: AddressesItem = {
private_tags: [], private_tags: [],
public_tags: [ ], public_tags: [ ],
watchlist_names: [], watchlist_names: [],
ens_domain_name: null,
}; };
export const ADDRESS_COIN_BALANCE: AddressCoinBalanceHistoryItem = { export const ADDRESS_COIN_BALANCE: AddressCoinBalanceHistoryItem = {
......
...@@ -11,4 +11,5 @@ export const ADDRESS_PARAMS: AddressParam = { ...@@ -11,4 +11,5 @@ export const ADDRESS_PARAMS: AddressParam = {
private_tags: [], private_tags: [],
public_tags: [], public_tags: [],
watchlist_names: [], watchlist_names: [],
ens_domain_name: null,
}; };
...@@ -3,11 +3,26 @@ import type { Counter, HomeStats, StatsChartsSection } from 'types/api/stats'; ...@@ -3,11 +3,26 @@ import type { Counter, HomeStats, StatsChartsSection } from 'types/api/stats';
export const HOMEPAGE_STATS: HomeStats = { export const HOMEPAGE_STATS: HomeStats = {
average_block_time: 14346, average_block_time: 14346,
coin_price: '1807.68', coin_price: '1807.68',
coin_price_change_percentage: 42,
gas_prices: { gas_prices: {
average: 0.1, average: {
fast: 0.11, fiat_price: '1.01',
slow: 0.1, 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', gas_used_today: '0',
market_cap: '0', market_cap: '0',
network_utilization_percentage: 22.56, network_utilization_percentage: 22.56,
......
...@@ -12,6 +12,7 @@ export interface Address extends UserTags { ...@@ -12,6 +12,7 @@ export interface Address extends UserTags {
creator_address_hash: string | null; creator_address_hash: string | null;
creation_tx_hash: string | null; creation_tx_hash: string | null;
exchange_rate: 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? // TODO: if we are happy with tabs-counters method, should we delete has_something fields?
has_beacon_chain_withdrawals?: boolean; has_beacon_chain_withdrawals?: boolean;
has_custom_methods_read: boolean; has_custom_methods_read: boolean;
......
...@@ -21,4 +21,5 @@ export interface AddressParam extends UserTags { ...@@ -21,4 +21,5 @@ export interface AddressParam extends UserTags {
name: string | null; name: string | null;
is_contract: boolean; is_contract: boolean;
is_verified: boolean | null; is_verified: boolean | null;
ens_domain_name: string | null;
} }
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'>;
...@@ -23,6 +23,12 @@ export interface SearchResultAddressOrContract { ...@@ -23,6 +23,12 @@ export interface SearchResultAddressOrContract {
address: string; address: string;
is_smart_contract_verified: boolean; is_smart_contract_verified: boolean;
url?: string; // not used by the frontend, we build the url ourselves 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 { export interface SearchResultLabel {
...@@ -35,7 +41,7 @@ export interface SearchResultLabel { ...@@ -35,7 +41,7 @@ export interface SearchResultLabel {
export interface SearchResultBlock { export interface SearchResultBlock {
type: 'block'; type: 'block';
block_type?: 'block' | 'reorg'; block_type?: 'block' | 'reorg' | 'uncle';
block_number: number | string; block_number: number | string;
block_hash: string; block_hash: string;
timestamp: string; timestamp: string;
......
...@@ -4,10 +4,13 @@ export type HomeStats = { ...@@ -4,10 +4,13 @@ export type HomeStats = {
total_transactions: string; total_transactions: string;
average_block_time: number; average_block_time: number;
coin_price: string | null; coin_price: string | null;
coin_price_change_percentage: number | null; // e.g -6.22
total_gas_used: string; total_gas_used: string;
transactions_today: string; transactions_today: string;
gas_used_today: string; gas_used_today: string;
gas_prices: GasPrices | null; gas_prices: GasPrices | null;
gas_price_updated_at: string | null;
gas_prices_update_in: number;
static_gas_price: string | null; static_gas_price: string | null;
market_cap: string; market_cap: string;
network_utilization_percentage: number; network_utilization_percentage: number;
...@@ -16,9 +19,15 @@ export type HomeStats = { ...@@ -16,9 +19,15 @@ export type HomeStats = {
} }
export type GasPrices = { export type GasPrices = {
average: number | null; average: GasPriceInfo | null;
fast: number | null; fast: GasPriceInfo | null;
slow: number | null; slow: GasPriceInfo | null;
}
export interface GasPriceInfo {
fiat_price: string | null;
price: number | null;
time: number | null;
} }
export type Counters = { export type Counters = {
......
...@@ -14,6 +14,12 @@ export type TransactionRevertReason = { ...@@ -14,6 +14,12 @@ export type TransactionRevertReason = {
type WrappedTransactionFields = 'decoded_input' | 'fee' | 'gas_limit' | 'gas_price' | 'hash' | 'max_fee_per_gas' | 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'; '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 = { export type Transaction = {
to: AddressParam | null; to: AddressParam | null;
created_contract: AddressParam | null; created_contract: AddressParam | null;
...@@ -54,8 +60,7 @@ export type Transaction = { ...@@ -54,8 +60,7 @@ export type Transaction = {
l1_gas_used?: string; l1_gas_used?: string;
has_error_in_internal_txs: boolean | null; has_error_in_internal_txs: boolean | null;
// optimism fields // optimism fields
op_withdrawal_status?: L2WithdrawalStatus; op_withdrawals?: Array<OpWithdrawal>;
op_l1_transaction_hash?: string;
// SUAVE fields // SUAVE fields
execution_node?: AddressParam | null; execution_node?: AddressParam | null;
allowed_peekers?: Array<string>; allowed_peekers?: Array<string>;
......
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 } });
});
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);
...@@ -128,6 +128,17 @@ const BlockDetails = ({ query }: Props) => { ...@@ -128,6 +128,17 @@ const BlockDetails = ({ query }: Props) => {
return config.chain.verificationType === 'validation' ? 'Validated by' : 'Mined by'; 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 ( return (
<Grid <Grid
columnGap={ 8 } columnGap={ 8 }
...@@ -136,7 +147,7 @@ const BlockDetails = ({ query }: Props) => { ...@@ -136,7 +147,7 @@ const BlockDetails = ({ query }: Props) => {
overflow="hidden" overflow="hidden"
> >
<DetailsInfoItem <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" hint="The block height of a particular block is defined as the number of blocks preceding it in the blockchain"
isLoading={ isPlaceholderData } isLoading={ isPlaceholderData }
> >
......
...@@ -43,7 +43,7 @@ const BlocksListItem = ({ data, isLoading, enableTimeIncrement }: Props) => { ...@@ -43,7 +43,7 @@ const BlocksListItem = ({ data, isLoading, enableTimeIncrement }: Props) => {
<BlockEntity <BlockEntity
isLoading={ isLoading } isLoading={ isLoading }
number={ data.height } number={ data.height }
hash={ data.type === 'reorg' ? data.hash : undefined } hash={ data.type !== 'block' ? data.hash : undefined }
noIcon noIcon
fontWeight={ 600 } fontWeight={ 600 }
/> />
......
...@@ -14,6 +14,11 @@ interface Props { ...@@ -14,6 +14,11 @@ interface Props {
const BlocksTabSlot = ({ pagination }: Props) => { const BlocksTabSlot = ({ pagination }: Props) => {
const statsQuery = useApiQuery('homepage_stats', { const statsQuery = useApiQuery('homepage_stats', {
fetchParams: {
headers: {
'updated-gas-oracle': 'true',
},
},
queryOptions: { queryOptions: {
placeholderData: HOMEPAGE_STATS, placeholderData: HOMEPAGE_STATS,
}, },
......
...@@ -50,7 +50,7 @@ const BlocksTableItem = ({ data, isLoading, enableTimeIncrement }: Props) => { ...@@ -50,7 +50,7 @@ const BlocksTableItem = ({ data, isLoading, enableTimeIncrement }: Props) => {
<BlockEntity <BlockEntity
isLoading={ isLoading } isLoading={ isLoading }
number={ data.height } number={ data.height }
hash={ data.type === 'reorg' ? data.hash : undefined } hash={ data.type !== 'block' ? data.hash : undefined }
noIcon noIcon
fontSize="sm" fontSize="sm"
lineHeight={ 5 } lineHeight={ 5 }
......
...@@ -37,7 +37,13 @@ const LatestBlocks = () => { ...@@ -37,7 +37,13 @@ const LatestBlocks = () => {
const queryClient = useQueryClient(); const queryClient = useQueryClient();
const statsQueryResult = useApiQuery('homepage_stats', { const statsQueryResult = useApiQuery('homepage_stats', {
fetchParams: {
headers: {
'updated-gas-oracle': 'true',
},
},
queryOptions: { queryOptions: {
refetchOnMount: false,
placeholderData: HOMEPAGE_STATS, placeholderData: HOMEPAGE_STATS,
}, },
}); });
......
...@@ -16,8 +16,14 @@ const hasGasTracker = config.UI.homepage.showGasTracker; ...@@ -16,8 +16,14 @@ const hasGasTracker = config.UI.homepage.showGasTracker;
const hasAvgBlockTime = config.UI.homepage.showAvgBlockTime; const hasAvgBlockTime = config.UI.homepage.showAvgBlockTime;
const Stats = () => { const Stats = () => {
const { data, isPlaceholderData, isError } = useApiQuery('homepage_stats', { const { data, isPlaceholderData, isError, dataUpdatedAt } = useApiQuery('homepage_stats', {
fetchParams: {
headers: {
'updated-gas-oracle': 'true',
},
},
queryOptions: { queryOptions: {
refetchOnMount: false,
placeholderData: HOMEPAGE_STATS, placeholderData: HOMEPAGE_STATS,
}, },
}); });
...@@ -45,7 +51,19 @@ const Stats = () => { ...@@ -45,7 +51,19 @@ const Stats = () => {
!data.gas_prices && itemsCount--; !data.gas_prices && itemsCount--;
data.rootstock_locked_btc && itemsCount++; data.rootstock_locked_btc && itemsCount++;
const isOdd = Boolean(itemsCount % 2); 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 = ( content = (
<> <>
...@@ -92,7 +110,7 @@ const Stats = () => { ...@@ -92,7 +110,7 @@ const Stats = () => {
<StatsItem <StatsItem
icon="gas" icon="gas"
title="Gas tracker" title="Gas tracker"
value={ data.gas_prices.average !== null ? `${ Number(data.gas_prices.average).toLocaleString() } Gwei` : 'N/A' } value={ gasPriceText }
_last={ isOdd ? lastItemTouchStyle : undefined } _last={ isOdd ? lastItemTouchStyle : undefined }
tooltipLabel={ gasLabel } tooltipLabel={ gasLabel }
isLoading={ isPlaceholderData } isLoading={ isPlaceholderData }
......
...@@ -3,6 +3,7 @@ import React from 'react'; ...@@ -3,6 +3,7 @@ import React from 'react';
import config from 'configs/app'; import config from 'configs/app';
import useApiQuery from 'lib/api/useApiQuery'; import useApiQuery from 'lib/api/useApiQuery';
import { HOMEPAGE_STATS } from 'stubs/stats';
import Hint from 'ui/shared/Hint'; import Hint from 'ui/shared/Hint';
import ChainIndicatorChartContainer from './ChainIndicatorChartContainer'; import ChainIndicatorChartContainer from './ChainIndicatorChartContainer';
...@@ -29,7 +30,17 @@ const ChainIndicators = () => { ...@@ -29,7 +30,17 @@ const ChainIndicators = () => {
const indicator = indicators.find(({ id }) => id === selectedIndicator); const indicator = indicators.find(({ id }) => id === selectedIndicator);
const queryResult = useFetchChartData(indicator); const queryResult = useFetchChartData(indicator);
const statsQueryResult = useApiQuery('homepage_stats'); const statsQueryResult = useApiQuery('homepage_stats', {
fetchParams: {
headers: {
'updated-gas-oracle': 'true',
},
},
queryOptions: {
refetchOnMount: false,
placeholderData: HOMEPAGE_STATS,
},
});
const bgColorDesktop = useColorModeValue('white', 'gray.900'); const bgColorDesktop = useColorModeValue('white', 'gray.900');
const bgColorMobile = useColorModeValue('white', 'black'); const bgColorMobile = useColorModeValue('white', 'black');
......
...@@ -65,7 +65,7 @@ const DepositsListItem = ({ item, isLoading }: Props) => { ...@@ -65,7 +65,7 @@ const DepositsListItem = ({ item, isLoading }: Props) => {
<ListItemMobileGrid.Label isLoading={ isLoading }>L1 txn origin</ListItemMobileGrid.Label> <ListItemMobileGrid.Label isLoading={ isLoading }>L1 txn origin</ListItemMobileGrid.Label>
<ListItemMobileGrid.Value> <ListItemMobileGrid.Value>
<AddressEntityL1 <AddressEntityL1
address={{ hash: item.l1_tx_origin, name: '', is_contract: false, is_verified: false, implementation_name: '' }} address={{ hash: item.l1_tx_origin, name: '', is_contract: false, is_verified: false, implementation_name: '', ens_domain_name: null }}
isLoading={ isLoading } isLoading={ isLoading }
noCopy noCopy
/> />
......
...@@ -56,7 +56,7 @@ const WithdrawalsTableItem = ({ item, isLoading }: Props) => { ...@@ -56,7 +56,7 @@ const WithdrawalsTableItem = ({ item, isLoading }: Props) => {
</Td> </Td>
<Td verticalAlign="middle"> <Td verticalAlign="middle">
<AddressEntityL1 <AddressEntityL1
address={{ hash: item.l1_tx_origin, name: '', is_contract: false, is_verified: false, implementation_name: '' }} address={{ hash: item.l1_tx_origin, name: '', is_contract: false, is_verified: false, implementation_name: '', ens_domain_name: null }}
isLoading={ isLoading } isLoading={ isLoading }
truncation="constant" truncation="constant"
noCopy noCopy
......
...@@ -4,6 +4,7 @@ import React, { useCallback } from 'react'; ...@@ -4,6 +4,7 @@ import React, { useCallback } from 'react';
import type { MarketplaceAppPreview } from 'types/client/marketplace'; import type { MarketplaceAppPreview } from 'types/client/marketplace';
import * as mixpanel from 'lib/mixpanel/index';
import IconSvg from 'ui/shared/IconSvg'; import IconSvg from 'ui/shared/IconSvg';
import MarketplaceAppCardLink from './MarketplaceAppCardLink'; import MarketplaceAppCardLink from './MarketplaceAppCardLink';
...@@ -43,6 +44,7 @@ const MarketplaceAppCard = ({ ...@@ -43,6 +44,7 @@ const MarketplaceAppCard = ({
const handleInfoClick = useCallback((event: MouseEvent) => { const handleInfoClick = useCallback((event: MouseEvent) => {
event.preventDefault(); event.preventDefault();
mixpanel.logEvent(mixpanel.EventTypes.PAGE_WIDGET, { Type: 'More button', Info: id });
onInfoClick(id); onInfoClick(id);
}, [ onInfoClick, id ]); }, [ onInfoClick, id ]);
......
...@@ -6,6 +6,7 @@ import React from 'react'; ...@@ -6,6 +6,7 @@ import React from 'react';
import { MarketplaceCategory } from 'types/client/marketplace'; import { MarketplaceCategory } from 'types/client/marketplace';
import useDebounce from 'lib/hooks/useDebounce'; import useDebounce from 'lib/hooks/useDebounce';
import * as mixpanel from 'lib/mixpanel/index';
import getQueryParamString from 'lib/router/getQueryParamString'; import getQueryParamString from 'lib/router/getQueryParamString';
import useMarketplaceApps from './useMarketplaceApps'; import useMarketplaceApps from './useMarketplaceApps';
...@@ -33,6 +34,8 @@ export default function useMarketplace() { ...@@ -33,6 +34,8 @@ export default function useMarketplace() {
const [ isDisclaimerModalOpen, setIsDisclaimerModalOpen ] = React.useState<boolean>(false); const [ isDisclaimerModalOpen, setIsDisclaimerModalOpen ] = React.useState<boolean>(false);
const handleFavoriteClick = React.useCallback((id: string, isFavorite: boolean) => { const handleFavoriteClick = React.useCallback((id: string, isFavorite: boolean) => {
mixpanel.logEvent(mixpanel.EventTypes.PAGE_WIDGET, { Type: 'Favorite app', Info: id });
const favoriteApps = getFavoriteApps(); const favoriteApps = getFavoriteApps();
if (isFavorite) { if (isFavorite) {
...@@ -64,6 +67,7 @@ export default function useMarketplace() { ...@@ -64,6 +67,7 @@ export default function useMarketplace() {
}, []); }, []);
const handleCategoryChange = React.useCallback((newCategory: string) => { const handleCategoryChange = React.useCallback((newCategory: string) => {
mixpanel.logEvent(mixpanel.EventTypes.FILTERS, { Source: 'Marketplace', 'Filter name': newCategory });
setSelectedCategoryId(newCategory); setSelectedCategoryId(newCategory);
}, []); }, []);
...@@ -91,6 +95,11 @@ export default function useMarketplace() { ...@@ -91,6 +95,11 @@ export default function useMarketplace() {
category: selectedCategoryId === MarketplaceCategory.ALL ? undefined : selectedCategoryId, category: selectedCategoryId === MarketplaceCategory.ALL ? undefined : selectedCategoryId,
filter: debouncedFilterQuery, filter: debouncedFilterQuery,
}, Boolean); }, Boolean);
if (debouncedFilterQuery.length > 0) {
mixpanel.logEvent(mixpanel.EventTypes.LOCAL_SEARCH, { Source: 'Marketplace', 'Search query': debouncedFilterQuery });
}
router.replace( router.replace(
{ pathname: '/apps', query }, { pathname: '/apps', query },
undefined, undefined,
......
import { Grid, Skeleton, Tooltip, Flex } from '@chakra-ui/react';
import type { UseQueryResult } from '@tanstack/react-query';
import React from 'react';
import type { EnsDomainDetailed } from 'types/api/ens';
import { route } from 'nextjs-routes';
import type { ResourceError } from 'lib/api/resources';
import dayjs from 'lib/date/dayjs';
import DetailsInfoItem from 'ui/shared/DetailsInfoItem';
import AddressEntity from 'ui/shared/entities/address/AddressEntity';
import NftEntity from 'ui/shared/entities/nft/NftEntity';
import IconSvg from 'ui/shared/IconSvg';
import LinkInternal from 'ui/shared/LinkInternal';
import TextSeparator from 'ui/shared/TextSeparator';
import NameDomainExpiryStatus from './NameDomainExpiryStatus';
interface Props {
query: UseQueryResult<EnsDomainDetailed, ResourceError<unknown>>;
}
const NameDomainDetails = ({ query }: Props) => {
const isLoading = query.isPlaceholderData;
const otherAddresses = Object.entries(query.data?.other_addresses ?? {});
const hasExpired = query.data?.expiry_date && dayjs(query.data.expiry_date).isBefore(dayjs());
return (
<Grid columnGap={ 8 } rowGap={ 3 } templateColumns={{ base: 'minmax(0, 1fr)', lg: 'max-content minmax(728px, auto)' }}>
{ query.data?.registration_date && (
<DetailsInfoItem
title="Registration date"
hint="The date the name was registered"
isLoading={ isLoading }
>
<IconSvg name="clock" boxSize={ 5 } color="gray.500" verticalAlign="middle" isLoading={ isLoading } mr={ 2 }/>
<Skeleton isLoaded={ !isLoading } display="inline" whiteSpace="pre-wrap" lineHeight="20px">
{ dayjs(query.data.registration_date).format('llll') }
</Skeleton>
</DetailsInfoItem>
) }
{ query.data?.expiry_date && (
<DetailsInfoItem
title="Expiration date"
// eslint-disable-next-line max-len
hint="The date the name expires, upon which there is a 90 day grace period for the owner to renew. After the 90 days, the name is released to the market"
isLoading={ isLoading }
display="inline-block"
>
<IconSvg name="clock" boxSize={ 5 } color="gray.500" verticalAlign="middle" isLoading={ isLoading } mr={ 2 } mt="-2px"/>
{ hasExpired && (
<>
<Skeleton isLoaded={ !isLoading } display="inline" whiteSpace="pre-wrap" lineHeight="24px">
{ dayjs(query.data.expiry_date).fromNow() }
</Skeleton>
<TextSeparator color="gray.500"/>
</>
) }
<Skeleton isLoaded={ !isLoading } display="inline" whiteSpace="pre-wrap" lineHeight="24px">
{ dayjs(query.data.expiry_date).format('llll') }
</Skeleton>
<TextSeparator color="gray.500"/>
<Skeleton isLoaded={ !isLoading } color="text_secondary" display="inline">
<NameDomainExpiryStatus date={ query.data?.expiry_date }/>
</Skeleton>
</DetailsInfoItem>
) }
{ query.data?.registrant && (
<DetailsInfoItem
title="Registrant"
hint="The account that owns the domain name and has the rights to edit its ownership and records"
isLoading={ isLoading }
columnGap={ 2 }
flexWrap="nowrap"
>
<AddressEntity
address={ query.data.registrant }
isLoading={ isLoading }
/>
<Tooltip label="Lookup for related domain names">
<LinkInternal
flexShrink={ 0 }
display="inline-flex"
href={ route({ pathname: '/name-domains', query: { owned_by: 'true', resolved_to: 'true', address: query.data.registrant.hash } }) }
>
<IconSvg name="search" boxSize={ 5 } isLoading={ isLoading }/>
</LinkInternal>
</Tooltip>
</DetailsInfoItem>
) }
{ query.data?.owner && (
<DetailsInfoItem
title="Owner"
hint="The account that owns the rights to edit the records of this domain name"
isLoading={ isLoading }
columnGap={ 2 }
flexWrap="nowrap"
>
<AddressEntity
address={ query.data.owner }
isLoading={ isLoading }
/>
<Tooltip label="Lookup for related domain names">
<LinkInternal
flexShrink={ 0 }
display="inline-flex"
href={ route({ pathname: '/name-domains', query: { owned_by: 'true', resolved_to: 'true', address: query.data.owner.hash } }) }
>
<IconSvg name="search" boxSize={ 5 } isLoading={ isLoading }/>
</LinkInternal>
</Tooltip>
</DetailsInfoItem>
) }
{ query.data?.wrapped_owner && (
<DetailsInfoItem
title="Manager"
hint="Owner of this NFT domain in NameWrapper contract"
isLoading={ isLoading }
columnGap={ 2 }
flexWrap="nowrap"
>
<AddressEntity
address={ query.data.wrapped_owner }
isLoading={ isLoading }
/>
<Tooltip label="Lookup for related domain names">
<LinkInternal
flexShrink={ 0 }
display="inline-flex"
href={ route({ pathname: '/name-domains', query: { owned_by: 'true', resolved_to: 'true', address: query.data.wrapped_owner.hash } }) }
>
<IconSvg name="search" boxSize={ 5 } isLoading={ isLoading }/>
</LinkInternal>
</Tooltip>
</DetailsInfoItem>
) }
{ query.data?.tokens.map((token) => (
<DetailsInfoItem
key={ token.type }
title={ token.type === 'WRAPPED_DOMAIN_TOKEN' ? 'Wrapped token ID' : 'Token ID' }
hint={ `The ${ token.type === 'WRAPPED_DOMAIN_TOKEN' ? 'wrapped ' : '' }token ID of this domain name NFT` }
isLoading={ isLoading }
wordBreak="break-all"
whiteSpace="pre-wrap"
>
<NftEntity hash={ token.contract_hash } id={ token.id } isLoading={ isLoading } noIcon/>
</DetailsInfoItem>
)) }
{ otherAddresses.length > 0 && (
<DetailsInfoItem
title="Other addresses"
hint="Other cryptocurrency addresses added to this domain name"
isLoading={ isLoading }
flexDir="column"
alignItems="flex-start"
>
{ otherAddresses.map(([ type, address ]) => (
<Flex key={ type } columnGap={ 2 } minW="0" w="100%" overflow="hidden">
<Skeleton isLoaded={ !isLoading }>{ type }</Skeleton>
<AddressEntity
address={{ hash: address }}
isLoading={ isLoading }
noLink
noIcon
/>
</Flex>
)) }
</DetailsInfoItem>
) }
</Grid>
);
};
export default React.memo(NameDomainDetails);
import { chakra } from '@chakra-ui/react';
import React from 'react';
import dayjs from 'lib/date/dayjs';
interface Props {
date: string | undefined;
}
const NameDomainExpiryStatus = ({ date }: Props) => {
if (!date) {
return null;
}
const hasExpired = dayjs(date).isBefore(dayjs());
if (hasExpired) {
return <chakra.span color="red.600">Expired</chakra.span>;
}
const diff = dayjs(date).diff(dayjs(), 'day');
if (diff < 30) {
return <chakra.span color="red.600">{ diff } days left</chakra.span>;
}
return <chakra.span color="text_secondary">Expires { dayjs(date).fromNow() }</chakra.span>;
};
export default React.memo(NameDomainExpiryStatus);
import { Box, Hide, Show } from '@chakra-ui/react';
import { useRouter } from 'next/router';
import React from 'react';
import config from 'configs/app';
import useApiQuery from 'lib/api/useApiQuery';
import getQueryParamString from 'lib/router/getQueryParamString';
import { ENS_DOMAIN_EVENT } from 'stubs/ENS';
import DataListDisplay from 'ui/shared/DataListDisplay';
import NameDomainHistoryListItem from './history/NameDomainHistoryListItem';
import NameDomainHistoryTable from './history/NameDomainHistoryTable';
import { getNextSortValue, type Sort, type SortField } from './history/utils';
const NameDomainHistory = () => {
const router = useRouter();
const domainName = getQueryParamString(router.query.name);
const [ sort, setSort ] = React.useState<Sort>();
const { isPlaceholderData, isError, data } = useApiQuery('domain_events', {
pathParams: { name: domainName, chainId: config.chain.id },
queryOptions: {
placeholderData: { items: Array(4).fill(ENS_DOMAIN_EVENT) },
},
});
const handleSortToggle = React.useCallback((event: React.MouseEvent) => {
if (isPlaceholderData) {
return;
}
const field = (event.currentTarget as HTMLDivElement).getAttribute('data-field') as SortField | undefined;
if (field) {
setSort(getNextSortValue(field));
}
}, [ isPlaceholderData ]);
const content = (
<>
<Show below="lg" ssr={ false }>
<Box>
{ data?.items.map((item, index) => <NameDomainHistoryListItem key={ index } { ...item } isLoading={ isPlaceholderData }/>) }
</Box>
</Show>
<Hide below="lg" ssr={ false }>
<NameDomainHistoryTable
data={ data }
isLoading={ isPlaceholderData }
sort={ sort }
onSortToggle={ handleSortToggle }
/>
</Hide>
</>
);
return (
<DataListDisplay
isError={ isError }
items={ data?.items }
emptyText="There are no events for this domain."
content={ content }
/>
);
};
export default React.memo(NameDomainHistory);
import { Skeleton } from '@chakra-ui/react';
import React from 'react';
import type { EnsDomainEvent } from 'types/api/ens';
import dayjs from 'lib/date/dayjs';
import Tag from 'ui/shared/chakra/Tag';
import AddressEntity from 'ui/shared/entities/address/AddressEntity';
import TxEntity from 'ui/shared/entities/tx/TxEntity';
import ListItemMobileGrid from 'ui/shared/ListItemMobile/ListItemMobileGrid';
type Props = EnsDomainEvent & {
isLoading?: boolean;
}
const NameDomainHistoryListItem = ({ isLoading, transaction_hash: transactionHash, timestamp, from_address: fromAddress, action }: Props) => {
return (
<ListItemMobileGrid.Container>
<ListItemMobileGrid.Label isLoading={ isLoading }>Txn hash</ListItemMobileGrid.Label>
<ListItemMobileGrid.Value>
<TxEntity hash={ transactionHash } isLoading={ isLoading } fontWeight={ 500 }/>
</ListItemMobileGrid.Value>
<ListItemMobileGrid.Label isLoading={ isLoading }>Age</ListItemMobileGrid.Label>
<ListItemMobileGrid.Value>
<Skeleton isLoaded={ !isLoading } color="text_secondary" display="inline-block">
<span>{ dayjs(timestamp).fromNow() }</span>
</Skeleton>
</ListItemMobileGrid.Value>
{ fromAddress && (
<>
<ListItemMobileGrid.Label isLoading={ isLoading }>From</ListItemMobileGrid.Label>
<ListItemMobileGrid.Value>
<AddressEntity address={ fromAddress } isLoading={ isLoading }/>
</ListItemMobileGrid.Value>
</>
) }
{ action && (
<>
<ListItemMobileGrid.Label isLoading={ isLoading }>Method</ListItemMobileGrid.Label>
<ListItemMobileGrid.Value>
<Tag colorScheme="gray" isLoading={ isLoading }>{ action }</Tag>
</ListItemMobileGrid.Value>
</>
) }
</ListItemMobileGrid.Container>
);
};
export default React.memo(NameDomainHistoryListItem);
import { Table, Tbody, Tr, Th, Link } from '@chakra-ui/react';
import React from 'react';
import type { EnsDomainEventsResponse } from 'types/api/ens';
import IconSvg from 'ui/shared/IconSvg';
import { default as Thead } from 'ui/shared/TheadSticky';
import NameDomainHistoryTableItem from './NameDomainHistoryTableItem';
import type { Sort } from './utils';
import { sortFn } from './utils';
interface Props {
data: EnsDomainEventsResponse | undefined;
isLoading?: boolean;
sort: Sort | undefined;
onSortToggle: (event: React.MouseEvent) => void;
}
const NameDomainHistoryTable = ({ data, isLoading, sort, onSortToggle }: Props) => {
const sortIconTransform = sort?.includes('asc') ? 'rotate(-90deg)' : 'rotate(90deg)';
return (
<Table variant="simple" size="sm">
<Thead top={ 0 }>
<Tr>
<Th width="25%">Txn hash</Th>
<Th width="25%" pl={ 9 }>
<Link display="flex" alignItems="center" justifyContent="flex-start" position="relative" data-field="timestamp" onClick={ onSortToggle }>
{ sort?.includes('timestamp') && (
<IconSvg
name="arrows/east"
boxSize={ 4 }
transform={ sortIconTransform }
color="link"
position="absolute"
left={ -5 }
top={ 0 }
/>
) }
<span>Age</span>
</Link>
</Th>
<Th width="25%">From</Th>
<Th width="25%">Method</Th>
</Tr>
</Thead>
<Tbody>
{
data?.items
.slice()
.sort(sortFn(sort))
.map((item, index) => <NameDomainHistoryTableItem key={ index } { ...item } isLoading={ isLoading }/>)
}
</Tbody>
</Table>
);
};
export default React.memo(NameDomainHistoryTable);
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