diff --git a/configs/app/features/index.ts b/configs/app/features/index.ts index 0879fb17d5..e174eaf37c 100644 --- a/configs/app/features/index.ts +++ b/configs/app/features/index.ts @@ -10,6 +10,7 @@ export { default as googleAnalytics } from './googleAnalytics'; export { default as graphqlApiDocs } from './graphqlApiDocs'; export { default as marketplace } from './marketplace'; export { default as mixpanel } from './mixpanel'; +export { default as nameService } from './nameService'; export { default as restApiDocs } from './restApiDocs'; export { default as optimisticRollup } from './optimisticRollup'; export { default as safe } from './safe'; diff --git a/configs/app/features/nameService.ts b/configs/app/features/nameService.ts new file mode 100644 index 0000000000..3af536bbe2 --- /dev/null +++ b/configs/app/features/nameService.ts @@ -0,0 +1,27 @@ +import type { Feature } from './types'; + +import { getEnvValue } from '../utils'; + +const apiHost = getEnvValue('NEXT_PUBLIC_NAME_SERVICE_API_HOST'); + +const title = 'Name service integration'; + +const config: Feature<{ api: { endpoint: string; basePath: string } }> = (() => { + if (apiHost) { + return Object.freeze({ + title, + isEnabled: true, + api: { + endpoint: apiHost, + basePath: '', + }, + }); + } + + return Object.freeze({ + title, + isEnabled: false, + }); +})(); + +export default config; diff --git a/configs/envs/.env.eth_goerli b/configs/envs/.env.eth_goerli index da1fac4d97..d5a0d74f0d 100644 --- a/configs/envs/.env.eth_goerli +++ b/configs/envs/.env.eth_goerli @@ -46,6 +46,7 @@ NEXT_PUBLIC_STATS_API_HOST=https://stats-goerli.k8s-dev.blockscout.com NEXT_PUBLIC_VISUALIZE_API_HOST=https://visualizer.services.blockscout.com NEXT_PUBLIC_CONTRACT_INFO_API_HOST=https://contracts-info.services.blockscout.com NEXT_PUBLIC_ADMIN_SERVICE_API_HOST=https://admin-rs.services.blockscout.com +NEXT_PUBLIC_NAME_SERVICE_API_HOST=https://bens-rs-test.k8s-dev.blockscout.com NEXT_PUBLIC_WEB3_WALLETS=['token_pocket','metamask'] NEXT_PUBLIC_VIEWS_CONTRACT_SOLIDITYSCAN_ENABLED='true' diff --git a/deploy/tools/envs-validator/schema.ts b/deploy/tools/envs-validator/schema.ts index 6dee3620b3..f776e03a26 100644 --- a/deploy/tools/envs-validator/schema.ts +++ b/deploy/tools/envs-validator/schema.ts @@ -425,6 +425,7 @@ const schema = yup NEXT_PUBLIC_STATS_API_HOST: yup.string().test(urlTest), NEXT_PUBLIC_VISUALIZE_API_HOST: yup.string().test(urlTest), NEXT_PUBLIC_CONTRACT_INFO_API_HOST: yup.string().test(urlTest), + NEXT_PUBLIC_NAME_SERVICE_API_HOST: yup.string().test(urlTest), NEXT_PUBLIC_GRAPHIQL_TRANSACTION: yup.string().matches(regexp.HEX_REGEXP), NEXT_PUBLIC_WEB3_WALLETS: yup .mixed() diff --git a/deploy/values/main/values.yaml b/deploy/values/main/values.yaml index 65f36f4dcd..b859431545 100644 --- a/deploy/values/main/values.yaml +++ b/deploy/values/main/values.yaml @@ -153,6 +153,7 @@ frontend: NEXT_PUBLIC_VISUALIZE_API_HOST: http://visualizer-svc.visualizer-testing.svc.cluster.local/ NEXT_PUBLIC_CONTRACT_INFO_API_HOST: https://contracts-info-test.k8s-dev.blockscout.com NEXT_PUBLIC_ADMIN_SERVICE_API_HOST: https://admin-rs-test.k8s-dev.blockscout.com + NEXT_PUBLIC_NAME_SERVICE_API_HOST: https://bens-rs-test.k8s-dev.blockscout.com NEXT_PUBLIC_LOGOUT_URL: https://blockscoutcom.us.auth0.com/v2/logout NEXT_PUBLIC_NETWORK_RPC_URL: https://rpc.ankr.com/eth_goerli NEXT_PUBLIC_HOMEPAGE_CHARTS: "['daily_txs','coin_price','market_cap']" diff --git a/deploy/values/review/values.yaml.gotmpl b/deploy/values/review/values.yaml.gotmpl index dbfd815a82..266951d9fe 100644 --- a/deploy/values/review/values.yaml.gotmpl +++ b/deploy/values/review/values.yaml.gotmpl @@ -56,6 +56,7 @@ frontend: NEXT_PUBLIC_VISUALIZE_API_HOST: http://visualizer-svc.visualizer-testing.svc.cluster.local/ NEXT_PUBLIC_CONTRACT_INFO_API_HOST: https://contracts-info-test.k8s-dev.blockscout.com NEXT_PUBLIC_ADMIN_SERVICE_API_HOST: https://admin-rs-test.k8s-dev.blockscout.com + NEXT_PUBLIC_NAME_SERVICE_API_HOST: https://bens-rs-test.k8s-dev.blockscout.com NEXT_PUBLIC_AUTH_URL: https://blockscout-main.k8s-dev.blockscout.com NEXT_PUBLIC_MARKETPLACE_SUBMIT_FORM: https://airtable.com/shrqUAcjgGJ4jU88C NEXT_PUBLIC_LOGOUT_URL: https://blockscoutcom.us.auth0.com/v2/logout diff --git a/docs/ENVS.md b/docs/ENVS.md index bee048088a..f664ff91aa 100644 --- a/docs/ENVS.md +++ b/docs/ENVS.md @@ -46,6 +46,7 @@ Please be aware that all environment variables prefixed with `NEXT_PUBLIC_` will - [Web3 wallet integration](ENVS.md#web3-wallet-integration-add-token-or-network-to-the-wallet) (add token or network to the wallet) - [Transaction interpretation](ENVS.md#transaction-interpretation) - [Verified tokens info](ENVS.md#verified-tokens-info) + - [Name service integration](ENVS.md#name-service-integration) - [Bridged tokens](ENVS.md#bridged-tokens) - [Safe{Core} address tags](ENVS.md#safecore-address-tags) - [SUAVE chain](ENVS.md#suave-chain) @@ -490,6 +491,16 @@ This feature is **enabled by default** with the `['metamask']` value. To switch   +### 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` | + +  + ### 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. diff --git a/icons/ENS.svg b/icons/ENS.svg new file mode 100644 index 0000000000..9832944dab --- /dev/null +++ b/icons/ENS.svg @@ -0,0 +1,3 @@ + + + diff --git a/icons/ENS_slim.svg b/icons/ENS_slim.svg new file mode 100644 index 0000000000..cd999b523a --- /dev/null +++ b/icons/ENS_slim.svg @@ -0,0 +1,4 @@ + + + + diff --git a/jest/lib.tsx b/jest/lib.tsx index 2c590932cf..f07ad76fdb 100644 --- a/jest/lib.tsx +++ b/jest/lib.tsx @@ -19,6 +19,7 @@ const PAGE_PROPS = { hash: '', number: '', q: '', + name: '', }; const TestApp = ({ children }: {children: React.ReactNode}) => { diff --git a/lib/api/buildUrl.ts b/lib/api/buildUrl.ts index 2feb358e1c..07805dbd9c 100644 --- a/lib/api/buildUrl.ts +++ b/lib/api/buildUrl.ts @@ -9,7 +9,7 @@ import type { ApiResource, ResourceName, ResourcePathParams } from './resources' export default function buildUrl( resourceName: R, pathParams?: ResourcePathParams, - queryParams?: Record | number | null | undefined>, + queryParams?: Record | number | boolean | null | undefined>, ): string { const resource: ApiResource = RESOURCES[resourceName]; const baseUrl = isNeedProxy() ? config.app.baseUrl : (resource.endpoint || config.api.endpoint); diff --git a/lib/api/resources.ts b/lib/api/resources.ts index 60d224dad6..5504d65c22 100644 --- a/lib/api/resources.ts +++ b/lib/api/resources.ts @@ -36,6 +36,15 @@ import type { ChartMarketResponse, ChartTransactionResponse } from 'types/api/ch import type { BackendVersionConfig } from 'types/api/configs'; import type { SmartContract, SmartContractReadMethod, SmartContractWriteMethod, SmartContractVerificationConfig, SolidityscanReport } from 'types/api/contract'; import type { VerifiedContractsResponse, VerifiedContractsFilters, VerifiedContractsCounters } from 'types/api/contracts'; +import type { + EnsAddressLookupFilters, + EnsAddressLookupResponse, + EnsDomainDetailed, + EnsDomainEventsResponse, + EnsDomainLookupFilters, + EnsDomainLookupResponse, + EnsLookupSorting, +} from 'types/api/ens'; import type { IndexingStatus } from 'types/api/indexingStatus'; import type { InternalTransactionsResponse } from 'types/api/internalTransaction'; import type { L2DepositsResponse, L2DepositsItem } from 'types/api/l2Deposits'; @@ -176,6 +185,34 @@ export const RESOURCES = { basePath: getFeaturePayload(config.features.stats)?.api.basePath, }, + // NAME SERVICE + addresses_lookup: { + path: '/api/v1/:chainId/addresses\\:lookup', + pathParams: [ 'chainId' as const ], + endpoint: getFeaturePayload(config.features.nameService)?.api.endpoint, + basePath: getFeaturePayload(config.features.nameService)?.api.basePath, + filterFields: [ 'address' as const, 'resolved_to' as const, 'owned_by' as const, 'only_active' as const ], + }, + domain_info: { + path: '/api/v1/:chainId/domains/:name', + pathParams: [ 'chainId' as const, 'name' as const ], + endpoint: getFeaturePayload(config.features.nameService)?.api.endpoint, + basePath: getFeaturePayload(config.features.nameService)?.api.basePath, + }, + domain_events: { + path: '/api/v1/:chainId/domains/:name/events', + pathParams: [ 'chainId' as const, 'name' as const ], + endpoint: getFeaturePayload(config.features.nameService)?.api.endpoint, + basePath: getFeaturePayload(config.features.nameService)?.api.basePath, + }, + domains_lookup: { + path: '/api/v1/:chainId/domains\\:lookup', + pathParams: [ 'chainId' as const ], + endpoint: getFeaturePayload(config.features.nameService)?.api.endpoint, + basePath: getFeaturePayload(config.features.nameService)?.api.basePath, + filterFields: [ 'name' as const, 'only_active' as const ], + }, + // VISUALIZATION visualize_sol2uml: { path: '/api/v1/solidity\\:visualize-contracts', @@ -613,7 +650,8 @@ export type PaginatedResources = 'blocks' | 'block_txs' | 'l2_output_roots' | 'l2_withdrawals' | 'l2_txn_batches' | 'l2_deposits' | 'zkevm_l2_txn_batches' | 'zkevm_l2_txn_batch_txs' | 'withdrawals' | 'address_withdrawals' | 'block_withdrawals' | -'watchlist' | 'private_tags_address' | 'private_tags_tx'; +'watchlist' | 'private_tags_address' | 'private_tags_tx' | +'domains_lookup' | 'addresses_lookup'; export type PaginatedResponse = ResourcePayload; @@ -712,6 +750,10 @@ Q extends 'zkevm_l2_txn_batches_count' ? number : Q extends 'zkevm_l2_txn_batch' ? ZkEvmL2TxnBatch : Q extends 'zkevm_l2_txn_batch_txs' ? ZkEvmL2TxnBatchTxs : Q extends 'config_backend_version' ? BackendVersionConfig : +Q extends 'addresses_lookup' ? EnsAddressLookupResponse : +Q extends 'domain_info' ? EnsDomainDetailed : +Q extends 'domain_events' ? EnsDomainEventsResponse : +Q extends 'domains_lookup' ? EnsDomainLookupResponse : never; /* eslint-enable @typescript-eslint/indent */ @@ -731,6 +773,8 @@ Q extends 'token_inventory' ? TokenInventoryFilters : Q extends 'tokens' ? TokensFilters : Q extends 'tokens_bridged' ? TokensBridgedFilters : Q extends 'verified_contracts' ? VerifiedContractsFilters : +Q extends 'addresses_lookup' ? EnsAddressLookupFilters : +Q extends 'domains_lookup' ? EnsDomainLookupFilters : never; /* eslint-enable @typescript-eslint/indent */ @@ -740,5 +784,7 @@ Q extends 'tokens' ? TokensSorting : Q extends 'tokens_bridged' ? TokensSorting : Q extends 'verified_contracts' ? VerifiedContractsSorting : Q extends 'address_txs' ? TransactionsSorting : +Q extends 'addresses_lookup' ? EnsLookupSorting : +Q extends 'domains_lookup' ? EnsLookupSorting : never; /* eslint-enable @typescript-eslint/indent */ diff --git a/lib/api/useApiFetch.tsx b/lib/api/useApiFetch.tsx index a3962b7a1d..3822e26ad3 100644 --- a/lib/api/useApiFetch.tsx +++ b/lib/api/useApiFetch.tsx @@ -19,7 +19,7 @@ import type { ApiResource, ResourceName, ResourcePathParams } from './resources' export interface Params { pathParams?: ResourcePathParams; - queryParams?: Record | number | undefined>; + queryParams?: Record | number | boolean | undefined>; fetchParams?: Pick; } diff --git a/lib/contexts/app.tsx b/lib/contexts/app.tsx index f0a1eec14e..ec0a25e61b 100644 --- a/lib/contexts/app.tsx +++ b/lib/contexts/app.tsx @@ -15,6 +15,7 @@ const AppContext = createContext({ hash: '', number: '', q: '', + name: '', }); export function AppContextProvider({ children, pageProps }: Props) { diff --git a/lib/hooks/useNavItems.tsx b/lib/hooks/useNavItems.tsx index f330a53c07..0700fa40c5 100644 --- a/lib/hooks/useNavItems.tsx +++ b/lib/hooks/useNavItems.tsx @@ -53,6 +53,12 @@ export default function useNavItems(): ReturnType { icon: 'verified', isActive: pathname === '/verified-contracts', }; + const ensLookup = config.features.nameService.isEnabled ? { + text: 'ENS lookup', + nextRoute: { pathname: '/name-domains' as const }, + icon: 'ENS', + isActive: pathname === '/name-domains' || pathname === '/name-domains/[name]', + } : null; if (config.features.zkEvmRollup.isEnabled) { blockchainNavItems = [ @@ -69,6 +75,7 @@ export default function useNavItems(): ReturnType { [ topAccounts, verifiedContracts, + ensLookup, ].filter(Boolean), ]; } else if (config.features.optimisticRollup.isEnabled) { @@ -90,6 +97,7 @@ export default function useNavItems(): ReturnType { [ topAccounts, verifiedContracts, + ensLookup, ].filter(Boolean), ]; } else { @@ -98,6 +106,7 @@ export default function useNavItems(): ReturnType { blocks, topAccounts, verifiedContracts, + ensLookup, config.features.beaconChain.isEnabled && { text: 'Withdrawals', nextRoute: { pathname: '/withdrawals' as const }, diff --git a/lib/metadata/getPageOgType.ts b/lib/metadata/getPageOgType.ts index 36e70e9fee..53b7fbcb0f 100644 --- a/lib/metadata/getPageOgType.ts +++ b/lib/metadata/getPageOgType.ts @@ -40,6 +40,8 @@ const OG_TYPE_DICT: Record = { '/zkevm-l2-txn-batches': 'Root page', '/zkevm-l2-txn-batch/[number]': 'Regular page', '/404': 'Regular page', + '/name-domains': 'Root page', + '/name-domains/[name]': 'Regular page', // service routes, added only to make typescript happy '/login': 'Regular page', diff --git a/lib/metadata/templates/description.ts b/lib/metadata/templates/description.ts index 175e118f13..c46dd9a36b 100644 --- a/lib/metadata/templates/description.ts +++ b/lib/metadata/templates/description.ts @@ -43,6 +43,8 @@ const TEMPLATE_MAP: Record = { '/zkevm-l2-txn-batches': DEFAULT_TEMPLATE, '/zkevm-l2-txn-batch/[number]': DEFAULT_TEMPLATE, '/404': DEFAULT_TEMPLATE, + '/name-domains': DEFAULT_TEMPLATE, + '/name-domains/[name]': DEFAULT_TEMPLATE, // service routes, added only to make typescript happy '/login': DEFAULT_TEMPLATE, diff --git a/lib/metadata/templates/title.ts b/lib/metadata/templates/title.ts index 5a9e48eebb..542dfb7966 100644 --- a/lib/metadata/templates/title.ts +++ b/lib/metadata/templates/title.ts @@ -33,11 +33,13 @@ const TEMPLATE_MAP: Record = { '/csv-export': 'export data to CSV', '/l2-deposits': 'deposits (L1 > L2)', '/l2-output-roots': 'output roots', - '/l2-txn-batches': 'Tx batches (L2 blocks)', + '/l2-txn-batches': 'tx batches (L2 blocks)', '/l2-withdrawals': 'withdrawals (L2 > L1)', '/zkevm-l2-txn-batches': 'zkEvm L2 Tx batches', '/zkevm-l2-txn-batch/[number]': 'zkEvm L2 Tx batch %number%', '/404': 'error - page not found', + '/name-domains': 'domains search and resolve', + '/name-domains/[name]': '%name% domain details', // service routes, added only to make typescript happy '/login': 'login', diff --git a/lib/mixpanel/getPageType.ts b/lib/mixpanel/getPageType.ts index 4ff5e22aa2..c9013c6360 100644 --- a/lib/mixpanel/getPageType.ts +++ b/lib/mixpanel/getPageType.ts @@ -38,6 +38,8 @@ export const PAGE_TYPE_DICT: Record = { '/zkevm-l2-txn-batches': 'ZkEvm L2 Tx batches', '/zkevm-l2-txn-batch/[number]': 'ZkEvm L2 Tx batch details', '/404': '404', + '/name-domains': 'Domains search and resolve', + '/name-domains/[name]': 'Domain details', // service routes, added only to make typescript happy '/login': 'Login', diff --git a/mocks/address/address.ts b/mocks/address/address.ts index fe1903164a..7c7bc612cb 100644 --- a/mocks/address/address.ts +++ b/mocks/address/address.ts @@ -15,6 +15,19 @@ export const withName: AddressParam = { private_tags: [], watchlist_names: [], public_tags: [], + ens_domain_name: null, +}; + +export const withEns: AddressParam = { + hash: hash, + implementation_name: null, + is_contract: false, + is_verified: null, + name: 'ArianeeStore', + private_tags: [], + watchlist_names: [], + public_tags: [], + ens_domain_name: 'kitty.kitty.kitty.cat.eth', }; export const withoutName: AddressParam = { @@ -26,6 +39,7 @@ export const withoutName: AddressParam = { private_tags: [], watchlist_names: [], public_tags: [], + ens_domain_name: null, }; export const token: Address = { @@ -56,6 +70,7 @@ export const token: Address = { has_token_transfers: true, has_tokens: true, has_validated_blocks: false, + ens_domain_name: null, }; export const contract: Address = { @@ -86,6 +101,7 @@ export const contract: Address = { token: null, watchlist_names: [ watchlistName ], watchlist_address_id: 42, + ens_domain_name: null, }; export const validator: Address = { @@ -116,4 +132,5 @@ export const validator: Address = { token: null, watchlist_names: [], watchlist_address_id: null, + ens_domain_name: null, }; diff --git a/mocks/blocks/block.ts b/mocks/blocks/block.ts index a1e9b31f0c..c386d16916 100644 --- a/mocks/blocks/block.ts +++ b/mocks/blocks/block.ts @@ -22,6 +22,7 @@ export const base: Block = { private_tags: [], public_tags: [], watchlist_names: [], + ens_domain_name: null, }, nonce: '0x0000000000000000', parent_hash: '0x44125f0eb36a9d942e0c23bb4e8117f7ba86a9537a69b59c0025986ed2b7500f', @@ -71,6 +72,7 @@ export const genesis: Block = { private_tags: [], public_tags: [], watchlist_names: [], + ens_domain_name: 'kitty.kitty.cat.eth', }, nonce: '0x0000000000000000', parent_hash: '0x0000000000000000000000000000000000000000000000000000000000000000', @@ -99,6 +101,7 @@ export const base2: Block = { private_tags: [], public_tags: [], watchlist_names: [], + ens_domain_name: null, }, timestamp: '2022-11-11T11:46:05Z', tx_count: 253, diff --git a/mocks/contracts/index.ts b/mocks/contracts/index.ts index 219333ef2b..6db06926ba 100644 --- a/mocks/contracts/index.ts +++ b/mocks/contracts/index.ts @@ -10,6 +10,7 @@ export const contract1: VerifiedContract = { private_tags: [], public_tags: [], watchlist_names: [], + ens_domain_name: null, }, coin_balance: '2346534676900000008', compiler_version: 'v0.8.17+commit.8df45f5f', @@ -31,6 +32,7 @@ export const contract2: VerifiedContract = { private_tags: [], public_tags: [], watchlist_names: [], + ens_domain_name: null, }, coin_balance: '9078234570352343999', compiler_version: 'v0.3.1+commit.0463ea4c', diff --git a/mocks/ens/domain.ts b/mocks/ens/domain.ts new file mode 100644 index 0000000000..3126aaecf5 --- /dev/null +++ b/mocks/ens/domain.ts @@ -0,0 +1,91 @@ +import type { EnsDomainDetailed } from 'types/api/ens'; + +const domainTokenA = { + id: '97352314626701792030827861137068748433918254309635329404916858191911576754327', + contract_hash: '0x57f1887a8bf19b14fc0df6fd9b2acc9af147ea85', + type: 'NATIVE_DOMAIN_TOKEN' as const, +}; +const domainTokenB = { + id: '423546333', + contract_hash: '0x57f1887a8bf19b14fc0df6fd9b2acc9af147ea86', + type: 'WRAPPED_DOMAIN_TOKEN' as const, +}; + +export const ensDomainA: EnsDomainDetailed = { + id: '0xb140bf9645e54f02ed3c1bcc225566b515a98d1688f10494a5c3bc5b447936a7', + tokens: [ + domainTokenA, + domainTokenB, + ], + name: 'cat.eth', + registrant: { + hash: '0x114d4603199df73e7d157787f8778e21fcd13066', + }, + resolved_address: { + hash: '0xfe6ab8a0dafe7d41adf247c210451c264155c9b0', + }, + owner: { + hash: '0x114d4603199df73e7d157787f8778e21fcd13066', + }, + wrapped_owner: null, + registration_date: '2021-06-27T13:34:44.000Z', + expiry_date: '2025-03-01T14:20:24.000Z', + other_addresses: { + ETH: 'fe6ab8a0dafe7d41adf247c210451c264155c9b0', + GNO: 'DDAfbb505ad214D7b80b1f830fcCc89B60fb7A83', + NEAR: 'a0b86991c6218b36c1d19d4a2e9eb0ce3606eb48.factory.bridge.near', + }, +}; + +export const ensDomainB: EnsDomainDetailed = { + id: '0x632ac7bec8e883416b371b36beaa822f4784208c99d063ee030020e2bd09b885', + tokens: [ domainTokenA ], + name: 'kitty.kitty.kitty.cat.eth', + resolved_address: null, + registrant: { + hash: '0x114d4603199df73e7d157787f8778e21fcd13066', + }, + owner: { + hash: '0x114d4603199df73e7d157787f8778e21fcd13066', + }, + wrapped_owner: null, + registration_date: '2023-08-13T13:01:12.000Z', + expiry_date: null, + other_addresses: {}, +}; + +export const ensDomainC: EnsDomainDetailed = { + id: '0xdb7f351de6d93bda077a9211bdc49f249326d87932e4787d109b0262e9d189ad', + tokens: [ domainTokenA ], + name: 'duck.duck.eth', + registrant: { + hash: '0x114d4603199df73e7d157787f8778e21fcd13066', + }, + resolved_address: { + hash: '0xfe6ab8a0dafe7d41adf247c210451c264155c9b0', + }, + owner: { + hash: '0x114d4603199df73e7d157787f8778e21fcd13066', + }, + wrapped_owner: null, + registration_date: '2022-04-24T07:34:44.000Z', + expiry_date: '2022-11-01T13:10:36.000Z', + other_addresses: {}, +}; + +export const ensDomainD: EnsDomainDetailed = { + id: '0xdb7f351de6d93bda077a9211bdc49f249326d87932e4787d109b0262e9d189ae', + tokens: [ domainTokenA ], + name: '🦆.duck.eth', + registrant: { + hash: '0x114d4603199df73e7d157787f8778e21fcd13066', + }, + resolved_address: { + hash: '0x114d4603199df73e7d157787f8778e21fcd13066', + }, + owner: null, + wrapped_owner: null, + registration_date: '2022-04-24T07:34:44.000Z', + expiry_date: '2027-09-23T13:10:36.000Z', + other_addresses: {}, +}; diff --git a/mocks/ens/events.ts b/mocks/ens/events.ts new file mode 100644 index 0000000000..2d60cc721a --- /dev/null +++ b/mocks/ens/events.ts @@ -0,0 +1,19 @@ +import type { EnsDomainEvent } from 'types/api/ens'; + +export const ensDomainEventA: EnsDomainEvent = { + transaction_hash: '0x86c66b9fad66e4f20d42a6eed4fe12a0f48a274786fd85e9d4aa6c60e84b5874', + timestamp: '2021-06-27T13:34:44.000000Z', + from_address: { + hash: '0xaa96a50a2f67111262fe24576bd85bb56ec65016', + }, + action: '0xf7a16963', +}; + +export const ensDomainEventB = { + transaction_hash: '0x150bf7d5cd42457dd9c799ddd9d4bf6c30c703e1954a88c6d4b668b23fe0fbf8', + timestamp: '2022-11-02T14:20:24.000000Z', + from_address: { + hash: '0xfe6ab8a0dafe7d41adf247c210451c264155c9b0', + }, + action: 'register', +}; diff --git a/mocks/search/index.ts b/mocks/search/index.ts index 0fbdf9e946..a5fb983fde 100644 --- a/mocks/search/index.ts +++ b/mocks/search/index.ts @@ -55,6 +55,20 @@ export const address1: SearchResultAddressOrContract = { url: '/address/0xb64a30399f7F6b0C154c2E7Af0a3ec7B0A5b131a', }; +export const address2: SearchResultAddressOrContract = { + address: '0xb64a30399f7F6b0C154c2E7Af0a3ec7B0A5b131b', + name: null, + type: 'address' as const, + is_smart_contract_verified: false, + url: '/address/0xb64a30399f7F6b0C154c2E7Af0a3ec7B0A5b131b', + ens_info: { + address_hash: '0x1234567890123456789012345678901234567890', + expiry_date: '2022-12-11T17:55:20Z', + name: 'utko.eth', + names_count: 1, + }, +}; + export const contract1: SearchResultAddressOrContract = { address: '0xb64a30399f7F6b0C154c2E7Af0a3ec7B0A5b131a', name: 'Unknown contract in this network', diff --git a/mocks/tokens/tokenTransfer.ts b/mocks/tokens/tokenTransfer.ts index 670f276867..1e0573d43e 100644 --- a/mocks/tokens/tokenTransfer.ts +++ b/mocks/tokens/tokenTransfer.ts @@ -10,6 +10,7 @@ export const erc20: TokenTransfer = { private_tags: [], public_tags: [], watchlist_names: [], + ens_domain_name: null, }, to: { hash: '0x7d20a8D54F955b4483A66aB335635ab66e151c51', @@ -20,6 +21,7 @@ export const erc20: TokenTransfer = { private_tags: [], public_tags: [], watchlist_names: [], + ens_domain_name: 'kitty.kitty.cat.eth', }, token: { address: '0x55d536e4d6c1993d8ef2e2a4ef77f02088419420', @@ -55,6 +57,7 @@ export const erc721: TokenTransfer = { private_tags: [], public_tags: [], watchlist_names: [], + ens_domain_name: 'kitty.kitty.cat.eth', }, to: { hash: '0x47eE48AEBc4ab9Ed908b805b8c8dAAa71B31Db1A', @@ -65,6 +68,7 @@ export const erc721: TokenTransfer = { private_tags: [], public_tags: [], watchlist_names: [], + ens_domain_name: null, }, token: { address: '0x363574E6C5C71c343d7348093D84320c76d5Dd29', @@ -99,6 +103,7 @@ export const erc1155A: TokenTransfer = { private_tags: [], public_tags: [], watchlist_names: [], + ens_domain_name: null, }, to: { hash: '0xBb36c792B9B45Aaf8b848A1392B0d6559202729E', @@ -109,6 +114,7 @@ export const erc1155A: TokenTransfer = { private_tags: [], public_tags: [], watchlist_names: [], + ens_domain_name: 'kitty.kitty.cat.eth', }, token: { address: '0xF56b7693E4212C584de4a83117f805B8E89224CB', diff --git a/mocks/txs/internalTxs.ts b/mocks/txs/internalTxs.ts index 8bd2773236..07eb83dc5e 100644 --- a/mocks/txs/internalTxs.ts +++ b/mocks/txs/internalTxs.ts @@ -13,6 +13,7 @@ export const base: InternalTransaction = { private_tags: [], public_tags: [], watchlist_names: [], + ens_domain_name: null, }, gas_limit: '757586', index: 1, @@ -27,6 +28,7 @@ export const base: InternalTransaction = { private_tags: [], public_tags: [], watchlist_names: [], + ens_domain_name: null, }, transaction_hash: '0xe9e27dfeb183066e26cfe556f74b7219b08df6951e25d14003d4fc7af8bbff61', type: 'call', @@ -61,6 +63,7 @@ export const withContractCreated: InternalTransaction = { private_tags: [], public_tags: [], watchlist_names: [], + ens_domain_name: null, }, value: '1420000000000000000', gas_limit: '5433', diff --git a/mocks/txs/state.ts b/mocks/txs/state.ts index 3192de294d..204fe1c867 100644 --- a/mocks/txs/state.ts +++ b/mocks/txs/state.ts @@ -10,6 +10,7 @@ export const mintToken: TxStateChange = { private_tags: [], public_tags: [], watchlist_names: [], + ens_domain_name: null, }, balance_after: null, balance_before: null, @@ -47,6 +48,7 @@ export const receiveMintedToken: TxStateChange = { private_tags: [], public_tags: [], watchlist_names: [], + ens_domain_name: null, }, balance_after: '1', balance_before: '0', @@ -84,6 +86,7 @@ export const transfer1155Token: TxStateChange = { private_tags: [], public_tags: [], watchlist_names: [], + ens_domain_name: null, }, balance_after: '1', balance_before: '0', @@ -115,6 +118,7 @@ export const receiveCoin: TxStateChange = { private_tags: [], public_tags: [], watchlist_names: [], + ens_domain_name: null, }, balance_after: '443787514723917012805', balance_before: '443787484997510408745', @@ -134,6 +138,7 @@ export const sendCoin: TxStateChange = { private_tags: [], public_tags: [], watchlist_names: [], + ens_domain_name: null, }, balance_after: '828282622733717191', balance_before: '832127467556437753', diff --git a/mocks/txs/tx.ts b/mocks/txs/tx.ts index c2d7d8ad0f..3ca7876b99 100644 --- a/mocks/txs/tx.ts +++ b/mocks/txs/tx.ts @@ -29,6 +29,7 @@ export const base: Transaction = { private_tags: [ ], public_tags: [ publicTag ], watchlist_names: [], + ens_domain_name: 'kitty.kitty.cat.eth', }, gas_limit: '800000', gas_price: '48000000000', @@ -54,6 +55,7 @@ export const base: Transaction = { private_tags: [ privateTag ], public_tags: [], watchlist_names: [ watchlistName ], + ens_domain_name: null, }, token_transfers: [], token_transfers_overflow: false, @@ -97,6 +99,7 @@ export const withContractCreation: Transaction = { private_tags: [], public_tags: [], watchlist_names: [], + ens_domain_name: null, }, tx_types: [ 'contract_creation', @@ -115,6 +118,7 @@ export const withTokenTransfer: Transaction = { private_tags: [ privateTag ], public_tags: [], watchlist_names: [ watchlistName ], + ens_domain_name: null, }, token_transfers: [ tokenTransferMock.erc20, @@ -168,6 +172,7 @@ export const withRawRevertReason: Transaction = { private_tags: [ ], public_tags: [], watchlist_names: [ ], + ens_domain_name: null, }, }; @@ -283,6 +288,7 @@ export const stabilityTx: Transaction = { private_tags: [], public_tags: [], watchlist_names: [], + ens_domain_name: null, }, dapp_fee: '34381250000000', token: { @@ -307,6 +313,7 @@ export const stabilityTx: Transaction = { private_tags: [], public_tags: [], watchlist_names: [], + ens_domain_name: null, }, validator_fee: '34381250000000', }, diff --git a/mocks/txs/txInterpretation.ts b/mocks/txs/txInterpretation.ts index b351363a52..e9c1a43b85 100644 --- a/mocks/txs/txInterpretation.ts +++ b/mocks/txs/txInterpretation.ts @@ -33,6 +33,7 @@ export const txInterpretation: TxInterpretationResponse = { private_tags: [], public_tags: [], watchlist_names: [], + ens_domain_name: null, }, }, timestamp: { diff --git a/nextjs/getServerSideProps.ts b/nextjs/getServerSideProps.ts index 2f62492617..c0c1faaa20 100644 --- a/nextjs/getServerSideProps.ts +++ b/nextjs/getServerSideProps.ts @@ -10,6 +10,7 @@ export type Props = { hash: string; number: string; q: string; + name: string; } export const base: GetServerSideProps = async({ req, query }) => { @@ -22,6 +23,7 @@ export const base: GetServerSideProps = async({ req, query }) => { height_or_hash: query.height_or_hash?.toString() || '', number: query.number?.toString() || '', q: query.q?.toString() || '', + name: query.name?.toString() || '', }, }; }; @@ -126,6 +128,16 @@ export const suave: GetServerSideProps = async(context) => { return base(context); }; +export const nameService: GetServerSideProps = async(context) => { + if (!config.features.nameService.isEnabled) { + return { + notFound: true, + }; + } + + return base(context); +}; + export const accounts: GetServerSideProps = async(context) => { if (config.UI.views.address.hiddenViews?.top_accounts) { return { diff --git a/nextjs/nextjs-routes.d.ts b/nextjs/nextjs-routes.d.ts index aa7c203b56..16aba3bf46 100644 --- a/nextjs/nextjs-routes.d.ts +++ b/nextjs/nextjs-routes.d.ts @@ -37,6 +37,8 @@ declare module "nextjs-routes" { | StaticRoute<"/l2-txn-batches"> | StaticRoute<"/l2-withdrawals"> | StaticRoute<"/login"> + | DynamicRoute<"/name-domains/[name]", { "name": string }> + | StaticRoute<"/name-domains"> | StaticRoute<"/search-results"> | StaticRoute<"/stats"> | DynamicRoute<"/token/[hash]", { "hash": string }> diff --git a/pages/name-domains/[name].tsx b/pages/name-domains/[name].tsx new file mode 100644 index 0000000000..7a01829baa --- /dev/null +++ b/pages/name-domains/[name].tsx @@ -0,0 +1,20 @@ +import type { NextPage } from 'next'; +import dynamic from 'next/dynamic'; +import React from 'react'; + +import type { Props } from 'nextjs/getServerSideProps'; +import PageNextJs from 'nextjs/PageNextJs'; + +const NameDomain = dynamic(() => import('ui/pages/NameDomain'), { ssr: false }); + +const Page: NextPage = (props: Props) => { + return ( + + + + ); +}; + +export default Page; + +export { nameService as getServerSideProps } from 'nextjs/getServerSideProps'; diff --git a/pages/name-domains/index.tsx b/pages/name-domains/index.tsx new file mode 100644 index 0000000000..f00e06e9ba --- /dev/null +++ b/pages/name-domains/index.tsx @@ -0,0 +1,19 @@ +import type { NextPage } from 'next'; +import dynamic from 'next/dynamic'; +import React from 'react'; + +import PageNextJs from 'nextjs/PageNextJs'; + +const NameDomains = dynamic(() => import('ui/pages/NameDomains'), { ssr: false }); + +const Page: NextPage = () => { + return ( + + + + ); +}; + +export default Page; + +export { nameService as getServerSideProps } from 'nextjs/getServerSideProps'; diff --git a/playwright/TestApp.tsx b/playwright/TestApp.tsx index 748e52c052..197756b854 100644 --- a/playwright/TestApp.tsx +++ b/playwright/TestApp.tsx @@ -29,6 +29,7 @@ const defaultAppContext = { hash: '', number: '', q: '', + name: '', }, }; diff --git a/public/icons/name.d.ts b/public/icons/name.d.ts index 31e3f50b9d..09d8e061ce 100644 --- a/public/icons/name.d.ts +++ b/public/icons/name.d.ts @@ -31,6 +31,8 @@ | "email-sent" | "email" | "empty_search_result" + | "ENS_slim" + | "ENS" | "error-pages/404" | "error-pages/422" | "error-pages/429" diff --git a/stubs/ENS.ts b/stubs/ENS.ts new file mode 100644 index 0000000000..4fbd654adf --- /dev/null +++ b/stubs/ENS.ts @@ -0,0 +1,32 @@ +import type { EnsDomainDetailed, EnsDomainEvent } from 'types/api/ens'; + +import { ADDRESS_PARAMS, ADDRESS_HASH } from './addressParams'; +import { TX_HASH } from './tx'; + +export const ENS_DOMAIN: EnsDomainDetailed = { + id: '0x126d74db13895f8d3a1d362410212731d1e1d9be8add83e388385f93d84c8c84', + name: 'kitty.cat.eth', + tokens: [ + { + id: '973523146267017920308', + contract_hash: ADDRESS_HASH, + type: 'NATIVE_DOMAIN_TOKEN', + }, + ], + owner: ADDRESS_PARAMS, + wrapped_owner: null, + resolved_address: ADDRESS_PARAMS, + registrant: ADDRESS_PARAMS, + registration_date: '2023-12-20T01:29:12.000Z', + expiry_date: '2099-01-02T01:29:12.000Z', + other_addresses: { + ETH: ADDRESS_HASH, + }, +}; + +export const ENS_DOMAIN_EVENT: EnsDomainEvent = { + transaction_hash: TX_HASH, + timestamp: '2022-06-06T08:43:15.000000Z', + from_address: ADDRESS_PARAMS, + action: '0xf7a16963', +}; diff --git a/stubs/address.ts b/stubs/address.ts index c2e0ffd7c6..19f2a398ad 100644 --- a/stubs/address.ts +++ b/stubs/address.ts @@ -41,6 +41,7 @@ export const ADDRESS_INFO: Address = { public_tags: [], watchlist_names: [], watchlist_address_id: null, + ens_domain_name: null, }; export const ADDRESS_COUNTERS: AddressCounters = { @@ -71,6 +72,7 @@ export const TOP_ADDRESS: AddressesItem = { private_tags: [], public_tags: [ ], watchlist_names: [], + ens_domain_name: null, }; export const ADDRESS_COIN_BALANCE: AddressCoinBalanceHistoryItem = { diff --git a/stubs/addressParams.ts b/stubs/addressParams.ts index a5f7374bde..819a71402b 100644 --- a/stubs/addressParams.ts +++ b/stubs/addressParams.ts @@ -11,4 +11,5 @@ export const ADDRESS_PARAMS: AddressParam = { private_tags: [], public_tags: [], watchlist_names: [], + ens_domain_name: null, }; diff --git a/types/api/address.ts b/types/api/address.ts index 7edd55659e..1a75f593ee 100644 --- a/types/api/address.ts +++ b/types/api/address.ts @@ -12,6 +12,7 @@ export interface Address extends UserTags { creator_address_hash: string | null; creation_tx_hash: string | null; exchange_rate: string | null; + ens_domain_name: string | null; // TODO: if we are happy with tabs-counters method, should we delete has_something fields? has_beacon_chain_withdrawals?: boolean; has_custom_methods_read: boolean; diff --git a/types/api/addressParams.ts b/types/api/addressParams.ts index b2d8aa1ce2..e076d046cb 100644 --- a/types/api/addressParams.ts +++ b/types/api/addressParams.ts @@ -21,4 +21,5 @@ export interface AddressParam extends UserTags { name: string | null; is_contract: boolean; is_verified: boolean | null; + ens_domain_name: string | null; } diff --git a/types/api/ens.ts b/types/api/ens.ts new file mode 100644 index 0000000000..6ee12f846b --- /dev/null +++ b/types/api/ens.ts @@ -0,0 +1,71 @@ +export interface EnsDomain { + id: string; + name: string; + resolved_address: { + hash: string; + } | null; + owner: { + hash: string; + } | null; + wrapped_owner: { + hash: string; + } | null; + registration_date?: string; + expiry_date: string | null; +} + +export interface EnsDomainDetailed extends EnsDomain { + tokens: Array<{ id: string; contract_hash: string; type: 'NATIVE_DOMAIN_TOKEN' | 'WRAPPED_DOMAIN_TOKEN' }>; + registrant: { + hash: string; + } | null; + other_addresses: Record; +} + +export interface EnsDomainEvent { + transaction_hash: string; + timestamp: string; + from_address: { + hash: string; + } | null; + action?: string; +} + +export interface EnsAddressLookupResponse { + items: Array; + next_page_params: { + page_token: string; + page_size: number; + } | null; +} + +export interface EnsDomainEventsResponse { + items: Array; +} + +export interface EnsDomainLookupResponse { + items: Array; + 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'>; diff --git a/types/api/search.ts b/types/api/search.ts index 3d2ad1d2c5..fc16add55e 100644 --- a/types/api/search.ts +++ b/types/api/search.ts @@ -23,6 +23,12 @@ export interface SearchResultAddressOrContract { address: string; is_smart_contract_verified: boolean; url?: string; // not used by the frontend, we build the url ourselves + ens_info?: { + address_hash: string; + expiry_date?: string; + name: string; + names_count: number; + }; } export interface SearchResultLabel { diff --git a/ui/address/__screenshots__/AddressTokenTransfers.pw.tsx_default_mobile-with-token-filter-and-no-pagination-1.png b/ui/address/__screenshots__/AddressTokenTransfers.pw.tsx_default_mobile-with-token-filter-and-no-pagination-1.png index 53d53b249c..9f903f1748 100644 Binary files a/ui/address/__screenshots__/AddressTokenTransfers.pw.tsx_default_mobile-with-token-filter-and-no-pagination-1.png and b/ui/address/__screenshots__/AddressTokenTransfers.pw.tsx_default_mobile-with-token-filter-and-no-pagination-1.png differ diff --git a/ui/address/__screenshots__/AddressTokenTransfers.pw.tsx_default_mobile-with-token-filter-and-pagination-1.png b/ui/address/__screenshots__/AddressTokenTransfers.pw.tsx_default_mobile-with-token-filter-and-pagination-1.png index ebb49117dd..960c9d224e 100644 Binary files a/ui/address/__screenshots__/AddressTokenTransfers.pw.tsx_default_mobile-with-token-filter-and-pagination-1.png and b/ui/address/__screenshots__/AddressTokenTransfers.pw.tsx_default_mobile-with-token-filter-and-pagination-1.png differ diff --git a/ui/address/__screenshots__/AddressTokenTransfers.pw.tsx_default_with-token-filter-and-no-pagination-1.png b/ui/address/__screenshots__/AddressTokenTransfers.pw.tsx_default_with-token-filter-and-no-pagination-1.png index c7d9422137..5d3e0e32a1 100644 Binary files a/ui/address/__screenshots__/AddressTokenTransfers.pw.tsx_default_with-token-filter-and-no-pagination-1.png and b/ui/address/__screenshots__/AddressTokenTransfers.pw.tsx_default_with-token-filter-and-no-pagination-1.png differ diff --git a/ui/address/__screenshots__/AddressTokenTransfers.pw.tsx_default_with-token-filter-and-pagination-1.png b/ui/address/__screenshots__/AddressTokenTransfers.pw.tsx_default_with-token-filter-and-pagination-1.png index 6ce1cc6b65..586d1feccc 100644 Binary files a/ui/address/__screenshots__/AddressTokenTransfers.pw.tsx_default_with-token-filter-and-pagination-1.png and b/ui/address/__screenshots__/AddressTokenTransfers.pw.tsx_default_with-token-filter-and-pagination-1.png differ diff --git a/ui/address/__screenshots__/AddressTxs.pw.tsx_default_base-view-mobile-1.png b/ui/address/__screenshots__/AddressTxs.pw.tsx_default_base-view-mobile-1.png index e993699729..7fa83d079d 100644 Binary files a/ui/address/__screenshots__/AddressTxs.pw.tsx_default_base-view-mobile-1.png and b/ui/address/__screenshots__/AddressTxs.pw.tsx_default_base-view-mobile-1.png differ diff --git a/ui/address/__screenshots__/AddressTxs.pw.tsx_default_base-view-screen-xl-1.png b/ui/address/__screenshots__/AddressTxs.pw.tsx_default_base-view-screen-xl-1.png index f6e0c639a8..98c140ad02 100644 Binary files a/ui/address/__screenshots__/AddressTxs.pw.tsx_default_base-view-screen-xl-1.png and b/ui/address/__screenshots__/AddressTxs.pw.tsx_default_base-view-screen-xl-1.png differ diff --git a/ui/address/__screenshots__/AddressTxs.pw.tsx_mobile_base-view-mobile-1.png b/ui/address/__screenshots__/AddressTxs.pw.tsx_mobile_base-view-mobile-1.png index 9ec178775e..e2ff29dcb6 100644 Binary files a/ui/address/__screenshots__/AddressTxs.pw.tsx_mobile_base-view-mobile-1.png and b/ui/address/__screenshots__/AddressTxs.pw.tsx_mobile_base-view-mobile-1.png differ diff --git a/ui/address/ensDomains/AddressEnsDomains.pw.tsx b/ui/address/ensDomains/AddressEnsDomains.pw.tsx new file mode 100644 index 0000000000..0f48f2593a --- /dev/null +++ b/ui/address/ensDomains/AddressEnsDomains.pw.tsx @@ -0,0 +1,37 @@ +import { test, expect } from '@playwright/experimental-ct-react'; +import React from 'react'; + +import config from 'configs/app'; +import * as ensDomainMock from 'mocks/ens/domain'; +import TestApp from 'playwright/TestApp'; +import buildApiUrl from 'playwright/utils/buildApiUrl'; + +import AddressEnsDomains from './AddressEnsDomains'; + +const ADDRESS_HASH = ensDomainMock.ensDomainA.owner?.hash ?? ''; +const ADDRESSES_LOOKUP_API_URL = buildApiUrl('addresses_lookup', { chainId: config.chain.id }) + + `?address=${ ADDRESS_HASH }&resolved_to=true&owned_by=true&only_active=true&order=ASC`; + +test('base view', async({ mount, page }) => { + await page.route(ADDRESSES_LOOKUP_API_URL, (route) => route.fulfill({ + status: 200, + body: JSON.stringify({ + items: [ + ensDomainMock.ensDomainA, + ensDomainMock.ensDomainB, + ensDomainMock.ensDomainC, + ensDomainMock.ensDomainD, + ], + }), + })); + + const component = await mount( + + + , + ); + + await component.getByText('4 domains').click(); + + await expect(page).toHaveScreenshot({ clip: { x: 0, y: 0, width: 550, height: 350 } }); +}); diff --git a/ui/address/ensDomains/AddressEnsDomains.tsx b/ui/address/ensDomains/AddressEnsDomains.tsx new file mode 100644 index 0000000000..3a6c872f5f --- /dev/null +++ b/ui/address/ensDomains/AddressEnsDomains.tsx @@ -0,0 +1,152 @@ +import { Button, chakra, Flex, Grid, Popover, PopoverBody, PopoverContent, PopoverTrigger, Skeleton, useDisclosure } from '@chakra-ui/react'; +import _clamp from 'lodash/clamp'; +import React from 'react'; + +import type { EnsDomain } from 'types/api/ens'; + +import { route } from 'nextjs-routes'; + +import config from 'configs/app'; +import useApiQuery from 'lib/api/useApiQuery'; +import dayjs from 'lib/date/dayjs'; +import EnsEntity from 'ui/shared/entities/ens/EnsEntity'; +import IconSvg from 'ui/shared/IconSvg'; +import LinkInternal from 'ui/shared/LinkInternal'; + +interface Props { + addressHash: string; + mainDomainName: string | null; +} + +const DomainsGrid = ({ data }: { data: Array }) => { + return ( + + { data.slice(0, 9).map((domain) => ) } + + ); +}; + +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 ; + } + + 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 ( + + + + + + + { mainDomain && ( +
+ Primary* + + + { mainDomain.expiry_date && + (expires { dayjs(mainDomain.expiry_date).fromNow() }) } + +
+ ) } + { ownedDomains.length > 0 && ( +
+ Owned by this address + +
+ ) } + { resolvedDomains.length > 0 && ( +
+ Resolved to this address + +
+ ) } + { (ownedDomains.length > 9 || resolvedDomains.length > 9) && ( + + More results + ({ totalRecords }) + + ) } + { mainDomain && ( + + *A domain name is not necessarily held by a person popularly associated with the name + + ) } +
+
+
+ ); +}; + +export default React.memo(AddressEnsDomains); diff --git a/ui/address/ensDomains/__screenshots__/AddressEnsDomains.pw.tsx_default_base-view-1.png b/ui/address/ensDomains/__screenshots__/AddressEnsDomains.pw.tsx_default_base-view-1.png new file mode 100644 index 0000000000..47268c3d4d Binary files /dev/null and b/ui/address/ensDomains/__screenshots__/AddressEnsDomains.pw.tsx_default_base-view-1.png differ diff --git a/ui/block/__screenshots__/BlockDetails.pw.tsx_default_genesis-block-1.png b/ui/block/__screenshots__/BlockDetails.pw.tsx_default_genesis-block-1.png index ccaa465904..1da0c55f92 100644 Binary files a/ui/block/__screenshots__/BlockDetails.pw.tsx_default_genesis-block-1.png and b/ui/block/__screenshots__/BlockDetails.pw.tsx_default_genesis-block-1.png differ diff --git a/ui/home/__screenshots__/LatestTxs.pw.tsx_dark-color-mode_default-view-dark-mode-1.png b/ui/home/__screenshots__/LatestTxs.pw.tsx_dark-color-mode_default-view-dark-mode-1.png index 0b1978fb30..f434028c0b 100644 Binary files a/ui/home/__screenshots__/LatestTxs.pw.tsx_dark-color-mode_default-view-dark-mode-1.png and b/ui/home/__screenshots__/LatestTxs.pw.tsx_dark-color-mode_default-view-dark-mode-1.png differ diff --git a/ui/home/__screenshots__/LatestTxs.pw.tsx_default_default-view-dark-mode-1.png b/ui/home/__screenshots__/LatestTxs.pw.tsx_default_default-view-dark-mode-1.png index d6f9f5b45b..c0afd4ef43 100644 Binary files a/ui/home/__screenshots__/LatestTxs.pw.tsx_default_default-view-dark-mode-1.png and b/ui/home/__screenshots__/LatestTxs.pw.tsx_default_default-view-dark-mode-1.png differ diff --git a/ui/home/__screenshots__/LatestTxs.pw.tsx_default_mobile-default-view-1.png b/ui/home/__screenshots__/LatestTxs.pw.tsx_default_mobile-default-view-1.png index 6622d1c204..e83ebc22f1 100644 Binary files a/ui/home/__screenshots__/LatestTxs.pw.tsx_default_mobile-default-view-1.png and b/ui/home/__screenshots__/LatestTxs.pw.tsx_default_mobile-default-view-1.png differ diff --git a/ui/home/__screenshots__/LatestTxs.pw.tsx_default_socket-new-item-1.png b/ui/home/__screenshots__/LatestTxs.pw.tsx_default_socket-new-item-1.png index 78d3f47519..d20665434d 100644 Binary files a/ui/home/__screenshots__/LatestTxs.pw.tsx_default_socket-new-item-1.png and b/ui/home/__screenshots__/LatestTxs.pw.tsx_default_socket-new-item-1.png differ diff --git a/ui/l2Deposits/DepositsListItem.tsx b/ui/l2Deposits/DepositsListItem.tsx index 7e4caefc57..658d09d979 100644 --- a/ui/l2Deposits/DepositsListItem.tsx +++ b/ui/l2Deposits/DepositsListItem.tsx @@ -65,7 +65,7 @@ const DepositsListItem = ({ item, isLoading }: Props) => { L1 txn origin diff --git a/ui/l2Deposits/DepositsTableItem.tsx b/ui/l2Deposits/DepositsTableItem.tsx index 99ba9ad465..2c9f437ab7 100644 --- a/ui/l2Deposits/DepositsTableItem.tsx +++ b/ui/l2Deposits/DepositsTableItem.tsx @@ -56,7 +56,7 @@ const WithdrawalsTableItem = ({ item, isLoading }: Props) => { >; +} + +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 ( + + { query.data?.registration_date && ( + + + + { dayjs(query.data.registration_date).format('llll') } + + + ) } + { query.data?.expiry_date && ( + + + { hasExpired && ( + <> + + { dayjs(query.data.expiry_date).fromNow() } + + + + ) } + + { dayjs(query.data.expiry_date).format('llll') } + + + + + + + ) } + { query.data?.registrant && ( + + + + + + + + + ) } + { query.data?.owner && ( + + + + + + + + + ) } + { query.data?.wrapped_owner && ( + + + + + + + + + ) } + { query.data?.tokens.map((token) => ( + + + + )) } + { otherAddresses.length > 0 && ( + + { otherAddresses.map(([ type, address ]) => ( + + { type } + + + )) } + + ) } + + ); +}; + +export default React.memo(NameDomainDetails); diff --git a/ui/nameDomain/NameDomainExpiryStatus.tsx b/ui/nameDomain/NameDomainExpiryStatus.tsx new file mode 100644 index 0000000000..4a6460d068 --- /dev/null +++ b/ui/nameDomain/NameDomainExpiryStatus.tsx @@ -0,0 +1,29 @@ +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 Expired; + } + + const diff = dayjs(date).diff(dayjs(), 'day'); + if (diff < 30) { + return { diff } days left; + } + + return Expires { dayjs(date).fromNow() }; +}; + +export default React.memo(NameDomainExpiryStatus); diff --git a/ui/nameDomain/NameDomainHistory.tsx b/ui/nameDomain/NameDomainHistory.tsx new file mode 100644 index 0000000000..ba5747ad46 --- /dev/null +++ b/ui/nameDomain/NameDomainHistory.tsx @@ -0,0 +1,67 @@ +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(); + + 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 = ( + <> + + + { data?.items.map((item, index) => ) } + + + + + + + ); + + return ( + + ); +}; + +export default React.memo(NameDomainHistory); diff --git a/ui/nameDomain/history/NameDomainHistoryListItem.tsx b/ui/nameDomain/history/NameDomainHistoryListItem.tsx new file mode 100644 index 0000000000..afa2e526b3 --- /dev/null +++ b/ui/nameDomain/history/NameDomainHistoryListItem.tsx @@ -0,0 +1,52 @@ +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 ( + + Txn hash + + + + + Age + + + { dayjs(timestamp).fromNow() } + + + + { fromAddress && ( + <> + From + + + + + ) } + + { action && ( + <> + Method + + { action } + + + ) } + + ); +}; + +export default React.memo(NameDomainHistoryListItem); diff --git a/ui/nameDomain/history/NameDomainHistoryTable.tsx b/ui/nameDomain/history/NameDomainHistoryTable.tsx new file mode 100644 index 0000000000..f6560372eb --- /dev/null +++ b/ui/nameDomain/history/NameDomainHistoryTable.tsx @@ -0,0 +1,60 @@ +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 ( + + + + + + + + + + + { + data?.items + .slice() + .sort(sortFn(sort)) + .map((item, index) => ) + } + +
Txn hash + + { sort?.includes('timestamp') && ( + + ) } + Age + + FromMethod
+ ); +}; + +export default React.memo(NameDomainHistoryTable); diff --git a/ui/nameDomain/history/NameDomainHistoryTableItem.tsx b/ui/nameDomain/history/NameDomainHistoryTableItem.tsx new file mode 100644 index 0000000000..d52e4626ca --- /dev/null +++ b/ui/nameDomain/history/NameDomainHistoryTableItem.tsx @@ -0,0 +1,37 @@ +import { Tr, Td, 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'; + +type Props = EnsDomainEvent & { + isLoading?: boolean; +} + +const NameDomainHistoryTableItem = ({ isLoading, transaction_hash: transactionHash, from_address: fromAddress, action, timestamp }: Props) => { + + return ( + + + + + + + { dayjs(timestamp).fromNow() } + + + + { fromAddress && } + + + { action && { action } } + + + ); +}; + +export default React.memo(NameDomainHistoryTableItem); diff --git a/ui/nameDomain/history/utils.ts b/ui/nameDomain/history/utils.ts new file mode 100644 index 0000000000..168050df2d --- /dev/null +++ b/ui/nameDomain/history/utils.ts @@ -0,0 +1,27 @@ +import type { EnsDomainEvent } from 'types/api/ens'; + +import getNextSortValueShared from 'ui/shared/sort/getNextSortValue'; + +export type SortField = 'timestamp'; +export type Sort = `${ SortField }-asc` | `${ SortField }-desc`; + +const SORT_SEQUENCE: Record> = { + timestamp: [ 'timestamp-desc', 'timestamp-asc', undefined ], +}; + +export const getNextSortValue = (getNextSortValueShared).bind(undefined, SORT_SEQUENCE); + +export const sortFn = (sort: Sort | undefined) => (a: EnsDomainEvent, b: EnsDomainEvent) => { + switch (sort) { + case 'timestamp-asc': { + return b.timestamp.localeCompare(a.timestamp); + } + + case 'timestamp-desc': { + return a.timestamp.localeCompare(b.timestamp); + } + + default: + return 0; + } +}; diff --git a/ui/nameDomains/NameDomainsActionBar.tsx b/ui/nameDomains/NameDomainsActionBar.tsx new file mode 100644 index 0000000000..ff57107799 --- /dev/null +++ b/ui/nameDomains/NameDomainsActionBar.tsx @@ -0,0 +1,113 @@ +import { Checkbox, CheckboxGroup, HStack, Text } from '@chakra-ui/react'; +import React from 'react'; + +import type { EnsDomainLookupFiltersOptions } from 'types/api/ens'; +import type { PaginationParams } from 'ui/shared/pagination/types'; + +import useIsInitialLoading from 'lib/hooks/useIsInitialLoading'; +import ActionBar from 'ui/shared/ActionBar'; +import FilterInput from 'ui/shared/filters/FilterInput'; +import PopoverFilter from 'ui/shared/filters/PopoverFilter'; +import Pagination from 'ui/shared/pagination/Pagination'; +import Sort from 'ui/shared/sort/Sort'; + +import type { Sort as TSort } from './utils'; +import { SORT_OPTIONS } from './utils'; + +interface Props { + pagination: PaginationParams; + searchTerm: string | undefined; + onSearchChange: (value: string) => void; + filterValue: EnsDomainLookupFiltersOptions; + onFilterValueChange: (nextValue: EnsDomainLookupFiltersOptions) => void; + sort: TSort | undefined; + onSortChange: (nextValue: TSort | undefined) => void; + isLoading: boolean; + isAddressSearch: boolean; +} + +const NameDomainsActionBar = ({ + searchTerm, + onSearchChange, + filterValue, + onFilterValueChange, + sort, + onSortChange, + isLoading, + isAddressSearch, + pagination, +}: Props) => { + const isInitialLoading = useIsInitialLoading(isLoading); + + const searchInput = ( + + ); + + const filter = ( + +
+ + Address + + Owned by + + + Resolved to address + + Status + + Include expired + + +
+
+ ); + + const sortButton = ( + + ); + + return ( + <> + + { filter } + { sortButton } + { searchInput } + + + + { filter } + { searchInput } + + + + + ); +}; + +export default React.memo(NameDomainsActionBar); diff --git a/ui/nameDomains/NameDomainsListItem.tsx b/ui/nameDomains/NameDomainsListItem.tsx new file mode 100644 index 0000000000..ac7275d9cc --- /dev/null +++ b/ui/nameDomains/NameDomainsListItem.tsx @@ -0,0 +1,60 @@ +import { Skeleton } from '@chakra-ui/react'; +import React from 'react'; + +import type { EnsDomain } from 'types/api/ens'; + +import dayjs from 'lib/date/dayjs'; +import NameDomainExpiryStatus from 'ui/nameDomain/NameDomainExpiryStatus'; +import AddressEntity from 'ui/shared/entities/address/AddressEntity'; +import EnsEntity from 'ui/shared/entities/ens/EnsEntity'; +import ListItemMobileGrid from 'ui/shared/ListItemMobile/ListItemMobileGrid'; + +interface Props extends EnsDomain { + isLoading: boolean; +} + +const NameDomainsListItem = ({ name, isLoading, resolved_address: resolvedAddress, registration_date: registrationDate, expiry_date: expiryDate }: Props) => { + return ( + + Domain + + + + + { resolvedAddress && ( + <> + Address + + + + + ) } + + { registrationDate && ( + <> + Registered on + + +
{ dayjs(registrationDate).format('MMM DD YYYY HH:mm:ss A') }
+
{ dayjs(registrationDate).fromNow() }
+
+
+ + ) } + + { expiryDate && ( + <> + Expiration date + + +
{ dayjs(expiryDate).format('MMM DD YYYY HH:mm:ss A') }
+ +
+
+ + ) } +
+ ); +}; + +export default React.memo(NameDomainsListItem); diff --git a/ui/nameDomains/NameDomainsTable.tsx b/ui/nameDomains/NameDomainsTable.tsx new file mode 100644 index 0000000000..ec46b7cec8 --- /dev/null +++ b/ui/nameDomains/NameDomainsTable.tsx @@ -0,0 +1,54 @@ +import { Table, Tbody, Tr, Th, Link } from '@chakra-ui/react'; +import React from 'react'; + +import type { EnsDomainLookupResponse } from 'types/api/ens'; + +import IconSvg from 'ui/shared/IconSvg'; +import { default as Thead } from 'ui/shared/TheadSticky'; + +import NameDomainsTableItem from './NameDomainsTableItem'; +import { type Sort } from './utils'; + +interface Props { + data: EnsDomainLookupResponse | undefined; + isLoading?: boolean; + sort: Sort | undefined; + onSortToggle: (event: React.MouseEvent) => void; +} + +const NameDomainsTable = ({ data, isLoading, sort, onSortToggle }: Props) => { + const sortIconTransform = sort?.toLowerCase().includes('asc') ? 'rotate(-90deg)' : 'rotate(90deg)'; + + return ( + + + + + + + + + + + { data?.items.map((item, index) => ) } + +
DomainAddress + + { sort?.includes('registration_date') && ( + + ) } + Registered on + + Expiration date
+ ); +}; + +export default React.memo(NameDomainsTable); diff --git a/ui/nameDomains/NameDomainsTableItem.tsx b/ui/nameDomains/NameDomainsTableItem.tsx new file mode 100644 index 0000000000..a4147ae8cf --- /dev/null +++ b/ui/nameDomains/NameDomainsTableItem.tsx @@ -0,0 +1,45 @@ +import { chakra, Tr, Td, Skeleton } from '@chakra-ui/react'; +import React from 'react'; + +import type { EnsDomain } from 'types/api/ens'; + +import dayjs from 'lib/date/dayjs'; +import NameDomainExpiryStatus from 'ui/nameDomain/NameDomainExpiryStatus'; +import AddressEntity from 'ui/shared/entities/address/AddressEntity'; +import EnsEntity from 'ui/shared/entities/ens/EnsEntity'; + +type Props = EnsDomain & { + isLoading?: boolean; +} + +const NameDomainsTableItem = ({ isLoading, name, resolved_address: resolvedAddress, registration_date: registrationDate, expiry_date: expiryDate }: Props) => { + + return ( + + + + + + { resolvedAddress && } + + + { registrationDate && ( + + { dayjs(registrationDate).format('MMM DD YYYY HH:mm:ss A') } + { dayjs(registrationDate).fromNow() } + + ) } + + + { expiryDate && ( + + { dayjs(expiryDate).format('MMM DD YYYY HH:mm:ss A') } + + + ) } + + + ); +}; + +export default React.memo(NameDomainsTableItem); diff --git a/ui/nameDomains/utils.ts b/ui/nameDomains/utils.ts new file mode 100644 index 0000000000..c240e5c93e --- /dev/null +++ b/ui/nameDomains/utils.ts @@ -0,0 +1,19 @@ +import type { EnsLookupSorting } from 'types/api/ens'; + +import getNextSortValueShared from 'ui/shared/sort/getNextSortValue'; +import type { Option } from 'ui/shared/sort/Sort'; + +export type SortField = EnsLookupSorting['sort']; +export type Sort = `${ EnsLookupSorting['sort'] }-${ EnsLookupSorting['order'] }`; + +export const SORT_OPTIONS: Array> = [ + { title: 'Default', id: undefined }, + { title: 'Registered on descending', id: 'registration_date-DESC' }, + { title: 'Registered on ascending', id: 'registration_date-ASC' }, +]; + +const SORT_SEQUENCE: Record> = { + registration_date: [ 'registration_date-DESC', 'registration_date-ASC', undefined ], +}; + +export const getNextSortValue = (getNextSortValueShared).bind(undefined, SORT_SEQUENCE); diff --git a/ui/pages/Accounts.pw.tsx b/ui/pages/Accounts.pw.tsx index 84072417be..77eb639235 100644 --- a/ui/pages/Accounts.pw.tsx +++ b/ui/pages/Accounts.pw.tsx @@ -21,6 +21,7 @@ const addresses: AddressesResponse = { ...addressMocks.token, tx_count: '109123890123', coin_balance: '22222345678901234567890000', + ens_domain_name: null, }, { ...addressMocks.withoutName, tx_count: '11', diff --git a/ui/pages/Address.tsx b/ui/pages/Address.tsx index f5e75429f1..c0cf60c530 100644 --- a/ui/pages/Address.tsx +++ b/ui/pages/Address.tsx @@ -23,11 +23,13 @@ import AddressTxs from 'ui/address/AddressTxs'; import AddressWithdrawals from 'ui/address/AddressWithdrawals'; import AddressFavoriteButton from 'ui/address/details/AddressFavoriteButton'; import AddressQrCode from 'ui/address/details/AddressQrCode'; +import AddressEnsDomains from 'ui/address/ensDomains/AddressEnsDomains'; import SolidityscanReport from 'ui/address/SolidityscanReport'; import AccountActionsMenu from 'ui/shared/AccountActionsMenu/AccountActionsMenu'; import TextAd from 'ui/shared/ad/TextAd'; import AddressAddToWallet from 'ui/shared/address/AddressAddToWallet'; import AddressEntity from 'ui/shared/entities/address/AddressEntity'; +import EnsEntity from 'ui/shared/entities/ens/EnsEntity'; import EntityTags from 'ui/shared/EntityTags'; import IconSvg from 'ui/shared/IconSvg'; import NetworkExplorers from 'ui/shared/NetworkExplorers'; @@ -172,14 +174,25 @@ const AddressPageContent = () => { const titleSecondRow = ( + { addressQuery.data?.ens_domain_name && ( + + ) } { !isLoading && addressQuery.data?.is_contract && addressQuery.data.token && } @@ -190,6 +203,8 @@ const AddressPageContent = () => { { addressQuery.data?.is_contract && addressQuery.data?.is_verified && config.UI.views.address.solidityscanEnabled && } + { !isLoading && addressQuery.data && config.features.nameService.isEnabled && + } ); diff --git a/ui/pages/NameDomain.pw.tsx b/ui/pages/NameDomain.pw.tsx new file mode 100644 index 0000000000..d91b3297b5 --- /dev/null +++ b/ui/pages/NameDomain.pw.tsx @@ -0,0 +1,79 @@ +import { test, expect } from '@playwright/experimental-ct-react'; +import React from 'react'; + +import config from 'configs/app'; +import * as textAdMock from 'mocks/ad/textAd'; +import * as ensDomainMock from 'mocks/ens/domain'; +import * as ensDomainEventsMock from 'mocks/ens/events'; +import TestApp from 'playwright/TestApp'; +import buildApiUrl from 'playwright/utils/buildApiUrl'; + +import NameDomain from './NameDomain'; + +const DOMAIN_API_URL = buildApiUrl('domain_info', { chainId: config.chain.id, name: ensDomainMock.ensDomainA.name }); +const DOMAIN_EVENTS_API_URL = buildApiUrl('domain_events', { chainId: config.chain.id, name: ensDomainMock.ensDomainA.name }); + +test.beforeEach(async({ page }) => { + await page.route('https://request-global.czilladx.com/serve/native.php?z=19260bf627546ab7242', (route) => route.fulfill({ + status: 200, + body: JSON.stringify(textAdMock.duck), + })); + await page.route(textAdMock.duck.ad.thumbnail, (route) => { + return route.fulfill({ + status: 200, + path: './playwright/mocks/image_s.jpg', + }); + }); +}); + +test('details tab', async({ mount, page }) => { + await page.route(DOMAIN_API_URL, (route) => route.fulfill({ + status: 200, + body: JSON.stringify(ensDomainMock.ensDomainA), + })); + + const component = await mount( + + + , + { hooksConfig: { + router: { + query: { name: ensDomainMock.ensDomainA.name }, + isReady: true, + }, + } }, + ); + + await expect(component).toHaveScreenshot(); +}); + +test('history tab +@mobile', async({ mount, page }) => { + await page.route(DOMAIN_API_URL, (route) => route.fulfill({ + status: 200, + body: JSON.stringify(ensDomainMock.ensDomainA), + })); + await page.route(DOMAIN_EVENTS_API_URL, (route) => route.fulfill({ + status: 200, + body: JSON.stringify({ + items: [ + ensDomainEventsMock.ensDomainEventA, + ensDomainEventsMock.ensDomainEventB, + ], + totalRecords: 2, + }), + })); + + const component = await mount( + + + , + { hooksConfig: { + router: { + query: { name: ensDomainMock.ensDomainA.name, tab: 'history' }, + isReady: true, + }, + } }, + ); + + await expect(component).toHaveScreenshot(); +}); diff --git a/ui/pages/NameDomain.tsx b/ui/pages/NameDomain.tsx new file mode 100644 index 0000000000..b00cec0bbf --- /dev/null +++ b/ui/pages/NameDomain.tsx @@ -0,0 +1,95 @@ +import { Flex, Tooltip } from '@chakra-ui/react'; +import { useRouter } from 'next/router'; +import React from 'react'; + +import type { RoutedTab } from 'ui/shared/Tabs/types'; + +import { route } from 'nextjs-routes'; + +import config from 'configs/app'; +import useApiQuery from 'lib/api/useApiQuery'; +import useIsMobile from 'lib/hooks/useIsMobile'; +import getQueryParamString from 'lib/router/getQueryParamString'; +import { ENS_DOMAIN } from 'stubs/ENS'; +import NameDomainDetails from 'ui/nameDomain/NameDomainDetails'; +import NameDomainHistory from 'ui/nameDomain/NameDomainHistory'; +import TextAd from 'ui/shared/ad/TextAd'; +import AddressEntity from 'ui/shared/entities/address/AddressEntity'; +import EnsEntity from 'ui/shared/entities/ens/EnsEntity'; +import IconSvg from 'ui/shared/IconSvg'; +import LinkInternal from 'ui/shared/LinkInternal'; +import PageTitle from 'ui/shared/Page/PageTitle'; +import RoutedTabs from 'ui/shared/Tabs/RoutedTabs'; +import TabsSkeleton from 'ui/shared/Tabs/TabsSkeleton'; +import useTabIndexFromQuery from 'ui/shared/Tabs/useTabIndexFromQuery'; + +const NameDomain = () => { + const isMobile = useIsMobile(); + const router = useRouter(); + const domainName = getQueryParamString(router.query.name); + + const infoQuery = useApiQuery('domain_info', { + pathParams: { name: domainName, chainId: config.chain.id }, + queryOptions: { + placeholderData: ENS_DOMAIN, + }, + }); + + const tabs: Array = [ + { id: 'details', title: 'Details', component: }, + { id: 'history', title: 'History', component: }, + ]; + + const tabIndex = useTabIndexFromQuery(tabs); + + if (infoQuery.isError) { + throw new Error(undefined, { cause: infoQuery.error }); + } + + const isLoading = infoQuery.isPlaceholderData; + + const titleSecondRow = ( + + + { infoQuery.data?.resolved_address && ( + + ) } + { infoQuery.data?.resolved_address && ( + + + + + + ) } + + ); + + return ( + <> + + + { infoQuery.isPlaceholderData ? ( + <> + + { tabs[tabIndex]?.component } + + ) : } + + ); +}; + +export default NameDomain; diff --git a/ui/pages/NameDomains.pw.tsx b/ui/pages/NameDomains.pw.tsx new file mode 100644 index 0000000000..6a522b6faf --- /dev/null +++ b/ui/pages/NameDomains.pw.tsx @@ -0,0 +1,50 @@ +import { test, expect } from '@playwright/experimental-ct-react'; +import React from 'react'; + +import config from 'configs/app'; +import * as textAdMock from 'mocks/ad/textAd'; +import * as ensDomainMock from 'mocks/ens/domain'; +import TestApp from 'playwright/TestApp'; +import buildApiUrl from 'playwright/utils/buildApiUrl'; + +import NameDomains from './NameDomains'; + +const DOMAINS_LOOKUP_API_URL = buildApiUrl('domains_lookup', { chainId: config.chain.id }) + '?only_active=true'; + +test.beforeEach(async({ page }) => { + await page.route('https://request-global.czilladx.com/serve/native.php?z=19260bf627546ab7242', (route) => route.fulfill({ + status: 200, + body: JSON.stringify(textAdMock.duck), + })); + await page.route(textAdMock.duck.ad.thumbnail, (route) => { + return route.fulfill({ + status: 200, + path: './playwright/mocks/image_s.jpg', + }); + }); +}); + +test('default view +@mobile', async({ mount, page }) => { + await page.route(DOMAINS_LOOKUP_API_URL, (route) => route.fulfill({ + status: 200, + body: JSON.stringify({ + items: [ + ensDomainMock.ensDomainA, + ensDomainMock.ensDomainB, + ensDomainMock.ensDomainC, + ensDomainMock.ensDomainD, + ], + next_page_params: { + token_id: '', + }, + }), + })); + + const component = await mount( + + + , + ); + + await expect(component).toHaveScreenshot(); +}); diff --git a/ui/pages/NameDomains.tsx b/ui/pages/NameDomains.tsx new file mode 100644 index 0000000000..05099a5c24 --- /dev/null +++ b/ui/pages/NameDomains.tsx @@ -0,0 +1,212 @@ +import { Box, Hide, Show } from '@chakra-ui/react'; +import { useRouter } from 'next/router'; +import React from 'react'; + +import type { EnsDomainLookupFiltersOptions, EnsLookupSorting } from 'types/api/ens'; + +import config from 'configs/app'; +import useDebounce from 'lib/hooks/useDebounce'; +import { apos } from 'lib/html-entities'; +import getQueryParamString from 'lib/router/getQueryParamString'; +import { ADDRESS_REGEXP } from 'lib/validations/address'; +import { ENS_DOMAIN } from 'stubs/ENS'; +import { generateListStub } from 'stubs/utils'; +import NameDomainsActionBar from 'ui/nameDomains/NameDomainsActionBar'; +import NameDomainsListItem from 'ui/nameDomains/NameDomainsListItem'; +import NameDomainsTable from 'ui/nameDomains/NameDomainsTable'; +import type { Sort, SortField } from 'ui/nameDomains/utils'; +import { SORT_OPTIONS, getNextSortValue } from 'ui/nameDomains/utils'; +import DataListDisplay from 'ui/shared/DataListDisplay'; +import PageTitle from 'ui/shared/Page/PageTitle'; +import useQueryWithPages from 'ui/shared/pagination/useQueryWithPages'; +import getSortParamsFromValue from 'ui/shared/sort/getSortParamsFromValue'; +import getSortValueFromQuery from 'ui/shared/sort/getSortValueFromQuery'; + +const NameDomains = () => { + const router = useRouter(); + + const q = getQueryParamString(router.query.name) || getQueryParamString(router.query.address); + const ownedBy = getQueryParamString(router.query.owned_by); + const resolvedTo = getQueryParamString(router.query.resolved_to); + const onlyActive = getQueryParamString(router.query.only_active); + + const initialFilters: EnsDomainLookupFiltersOptions = [ + ownedBy === 'true' ? 'owned_by' as const : undefined, + resolvedTo === 'true' ? 'resolved_to' as const : undefined, + onlyActive === 'false' ? 'with_inactive' as const : undefined, + ].filter(Boolean); + const initialSort = getSortValueFromQuery(router.query, SORT_OPTIONS); + + const [ searchTerm, setSearchTerm ] = React.useState(q || ''); + const [ filterValue, setFilterValue ] = React.useState(initialFilters); + const [ sort, setSort ] = React.useState(initialSort); + + const debouncedSearchTerm = useDebounce(searchTerm, 300); + const isAddressSearch = React.useMemo(() => ADDRESS_REGEXP.test(debouncedSearchTerm), [ debouncedSearchTerm ]); + const sortParams = getSortParamsFromValue(sort); + + const addressesLookupQuery = useQueryWithPages({ + resourceName: 'addresses_lookup', + pathParams: { chainId: config.chain.id }, + filters: { + address: debouncedSearchTerm, + resolved_to: filterValue.includes('resolved_to'), + owned_by: filterValue.includes('owned_by'), + only_active: !filterValue.includes('with_inactive'), + }, + sorting: sortParams, + options: { + enabled: isAddressSearch, + placeholderData: generateListStub<'addresses_lookup'>(ENS_DOMAIN, 50, { next_page_params: null }), + }, + }); + + const domainsLookupQuery = useQueryWithPages({ + resourceName: 'domains_lookup', + pathParams: { chainId: config.chain.id }, + filters: { + name: debouncedSearchTerm, + only_active: !filterValue.includes('with_inactive'), + }, + sorting: sortParams, + options: { + enabled: !isAddressSearch, + placeholderData: generateListStub<'domains_lookup'>(ENS_DOMAIN, 50, { next_page_params: null }), + }, + }); + + const query = isAddressSearch ? addressesLookupQuery : domainsLookupQuery; + const { data, isError, isPlaceholderData: isLoading, onFilterChange, onSortingChange } = query; + + React.useEffect(() => { + const hasInactiveFilter = filterValue.some((value) => value === 'with_inactive'); + if (isAddressSearch) { + setFilterValue([ 'owned_by' as const, 'resolved_to' as const, hasInactiveFilter ? 'with_inactive' as const : undefined ].filter(Boolean)); + onFilterChange<'addresses_lookup'>({ + address: debouncedSearchTerm, + resolved_to: true, + owned_by: true, + only_active: !hasInactiveFilter, + }); + } else { + setFilterValue([ hasInactiveFilter ? 'with_inactive' as const : undefined ].filter(Boolean)); + onFilterChange<'domains_lookup'>({ + name: debouncedSearchTerm, + only_active: !hasInactiveFilter, + }); + } + // should run only the type of search changes + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [ isAddressSearch ]); + + const handleSortToggle = React.useCallback((event: React.MouseEvent) => { + if (isLoading) { + return; + } + const field = (event.currentTarget as HTMLDivElement).getAttribute('data-field') as SortField | undefined; + + if (field) { + setSort((prevValue) => { + const nextSortValue = getNextSortValue(field)(prevValue); + onSortingChange(getSortParamsFromValue(nextSortValue)); + return nextSortValue; + }); + } + }, [ isLoading, onSortingChange ]); + + const handleSearchTermChange = React.useCallback((value: string) => { + setSearchTerm(value); + const isAddressSearch = ADDRESS_REGEXP.test(value); + if (isAddressSearch) { + onFilterChange<'addresses_lookup'>({ + address: value, + resolved_to: filterValue.includes('resolved_to'), + owned_by: filterValue.includes('owned_by'), + only_active: !filterValue.includes('with_inactive'), + }); + } else { + onFilterChange<'domains_lookup'>({ + name: value, + only_active: !filterValue.includes('with_inactive'), + }); + } + }, [ onFilterChange, filterValue ]); + + const handleFilterValueChange = React.useCallback((value: EnsDomainLookupFiltersOptions) => { + setFilterValue(value); + + const isAddressSearch = ADDRESS_REGEXP.test(debouncedSearchTerm); + if (isAddressSearch) { + onFilterChange<'addresses_lookup'>({ + address: debouncedSearchTerm, + resolved_to: value.includes('resolved_to'), + owned_by: value.includes('owned_by'), + only_active: !value.includes('with_inactive'), + }); + } else { + onFilterChange<'domains_lookup'>({ + name: debouncedSearchTerm, + only_active: !value.includes('with_inactive'), + }); + } + }, [ debouncedSearchTerm, onFilterChange ]); + + const hasActiveFilters = Boolean(debouncedSearchTerm) || filterValue.length > 0; + + const content = ( + <> + + + { data?.items.map((item, index) => ( + + )) } + + + + + + + ); + + const actionBar = ( + + ); + + return ( + <> + + + + ); +}; + +export default NameDomains; diff --git a/ui/pages/SearchResults.pw.tsx b/ui/pages/SearchResults.pw.tsx index 4e78e6f35f..a5acddd0ab 100644 --- a/ui/pages/SearchResults.pw.tsx +++ b/ui/pages/SearchResults.pw.tsx @@ -32,6 +32,7 @@ test.describe('search by name ', () => { searchMock.token1, searchMock.token2, searchMock.contract1, + searchMock.address2, searchMock.label1, ], }), diff --git a/ui/pages/__screenshots__/Home.pw.tsx_dark-color-mode_default-view---default-dark-mode-1.png b/ui/pages/__screenshots__/Home.pw.tsx_dark-color-mode_default-view---default-dark-mode-1.png index 236d307a77..f9ae337b71 100644 Binary files a/ui/pages/__screenshots__/Home.pw.tsx_dark-color-mode_default-view---default-dark-mode-1.png and b/ui/pages/__screenshots__/Home.pw.tsx_dark-color-mode_default-view---default-dark-mode-1.png differ diff --git a/ui/pages/__screenshots__/Home.pw.tsx_default_default-view-screen-xl-1.png b/ui/pages/__screenshots__/Home.pw.tsx_default_default-view-screen-xl-1.png index dcf50da8e4..5513a62a55 100644 Binary files a/ui/pages/__screenshots__/Home.pw.tsx_default_default-view-screen-xl-1.png and b/ui/pages/__screenshots__/Home.pw.tsx_default_default-view-screen-xl-1.png differ diff --git a/ui/pages/__screenshots__/Home.pw.tsx_default_mobile-base-view-1.png b/ui/pages/__screenshots__/Home.pw.tsx_default_mobile-base-view-1.png index f90ad744bb..6385a6d305 100644 Binary files a/ui/pages/__screenshots__/Home.pw.tsx_default_mobile-base-view-1.png and b/ui/pages/__screenshots__/Home.pw.tsx_default_mobile-base-view-1.png differ diff --git a/ui/pages/__screenshots__/NameDomain.pw.tsx_default_details-tab-1.png b/ui/pages/__screenshots__/NameDomain.pw.tsx_default_details-tab-1.png new file mode 100644 index 0000000000..8cf0c45158 Binary files /dev/null and b/ui/pages/__screenshots__/NameDomain.pw.tsx_default_details-tab-1.png differ diff --git a/ui/pages/__screenshots__/NameDomain.pw.tsx_default_history-tab-mobile-1.png b/ui/pages/__screenshots__/NameDomain.pw.tsx_default_history-tab-mobile-1.png new file mode 100644 index 0000000000..9b4e735d67 Binary files /dev/null and b/ui/pages/__screenshots__/NameDomain.pw.tsx_default_history-tab-mobile-1.png differ diff --git a/ui/pages/__screenshots__/NameDomain.pw.tsx_mobile_history-tab-mobile-1.png b/ui/pages/__screenshots__/NameDomain.pw.tsx_mobile_history-tab-mobile-1.png new file mode 100644 index 0000000000..fd69150bf3 Binary files /dev/null and b/ui/pages/__screenshots__/NameDomain.pw.tsx_mobile_history-tab-mobile-1.png differ diff --git a/ui/pages/__screenshots__/NameDomains.pw.tsx_default_default-view-mobile-1.png b/ui/pages/__screenshots__/NameDomains.pw.tsx_default_default-view-mobile-1.png new file mode 100644 index 0000000000..1e5d97efd3 Binary files /dev/null and b/ui/pages/__screenshots__/NameDomains.pw.tsx_default_default-view-mobile-1.png differ diff --git a/ui/pages/__screenshots__/NameDomains.pw.tsx_mobile_default-view-mobile-1.png b/ui/pages/__screenshots__/NameDomains.pw.tsx_mobile_default-view-mobile-1.png new file mode 100644 index 0000000000..63929d04b1 Binary files /dev/null and b/ui/pages/__screenshots__/NameDomains.pw.tsx_mobile_default-view-mobile-1.png differ diff --git a/ui/pages/__screenshots__/SearchResults.pw.tsx_dark-color-mode_search-by-name-mobile-dark-mode-1.png b/ui/pages/__screenshots__/SearchResults.pw.tsx_dark-color-mode_search-by-name-mobile-dark-mode-1.png index f218590790..febacb309f 100644 Binary files a/ui/pages/__screenshots__/SearchResults.pw.tsx_dark-color-mode_search-by-name-mobile-dark-mode-1.png and b/ui/pages/__screenshots__/SearchResults.pw.tsx_dark-color-mode_search-by-name-mobile-dark-mode-1.png differ diff --git a/ui/pages/__screenshots__/SearchResults.pw.tsx_default_search-by-name-mobile-dark-mode-1.png b/ui/pages/__screenshots__/SearchResults.pw.tsx_default_search-by-name-mobile-dark-mode-1.png index 0f70ac15c8..22a47915c5 100644 Binary files a/ui/pages/__screenshots__/SearchResults.pw.tsx_default_search-by-name-mobile-dark-mode-1.png and b/ui/pages/__screenshots__/SearchResults.pw.tsx_default_search-by-name-mobile-dark-mode-1.png differ diff --git a/ui/pages/__screenshots__/SearchResults.pw.tsx_mobile_search-by-name-mobile-dark-mode-1.png b/ui/pages/__screenshots__/SearchResults.pw.tsx_mobile_search-by-name-mobile-dark-mode-1.png index 4a67cc56a9..d995bb1565 100644 Binary files a/ui/pages/__screenshots__/SearchResults.pw.tsx_mobile_search-by-name-mobile-dark-mode-1.png and b/ui/pages/__screenshots__/SearchResults.pw.tsx_mobile_search-by-name-mobile-dark-mode-1.png differ diff --git a/ui/searchResults/SearchResultListItem.tsx b/ui/searchResults/SearchResultListItem.tsx index b6d14f0a85..340f753219 100644 --- a/ui/searchResults/SearchResultListItem.tsx +++ b/ui/searchResults/SearchResultListItem.tsx @@ -1,4 +1,4 @@ -import { Flex, Grid, Image, Box, Text, Skeleton, useColorMode, Tag } from '@chakra-ui/react'; +import { chakra, Flex, Grid, Image, Box, Text, Skeleton, useColorMode, Tag } from '@chakra-ui/react'; import React from 'react'; import xss from 'xss'; @@ -10,6 +10,7 @@ import dayjs from 'lib/date/dayjs'; import highlightText from 'lib/highlightText'; import * as mixpanel from 'lib/mixpanel/index'; import { saveToRecentKeywords } from 'lib/recentSearchKeywords'; +import { ADDRESS_REGEXP } from 'lib/validations/address'; import * as AddressEntity from 'ui/shared/entities/address/AddressEntity'; import * as BlockEntity from 'ui/shared/entities/block/BlockEntity'; import * as TokenEntity from 'ui/shared/entities/token/TokenEntity'; @@ -73,13 +74,14 @@ const SearchResultListItem = ({ data, searchTerm, isLoading }: Props) => { case 'contract': case 'address': { - const shouldHighlightHash = data.address.toLowerCase() === searchTerm.toLowerCase(); + const shouldHighlightHash = ADDRESS_REGEXP.test(searchTerm); const address = { hash: data.address, is_contract: data.type === 'contract', is_verified: data.is_smart_contract_verified, name: null, implementation_name: null, + ens_domain_name: null, }; return ( @@ -264,8 +266,23 @@ const SearchResultListItem = ({ data, searchTerm, isLoading }: Props) => { } case 'contract': case 'address': { - const shouldHighlightHash = data.address.toLowerCase() === searchTerm.toLowerCase(); - return data.name ? : null; + const shouldHighlightHash = ADDRESS_REGEXP.test(searchTerm); + const addressName = data.name || data.ens_info?.name; + const expiresText = data.ens_info?.expiry_date ? ` (expires ${ dayjs(data.ens_info.expiry_date).fromNow() })` : ''; + + return addressName ? ( + <> + + { data.ens_info && + ( + data.ens_info.names_count > 1 ? + ({ data.ens_info.names_count > 39 ? '40+' : `+${ data.ens_info.names_count - 1 }` }) : + { expiresText } + ) + } + + ) : + null; } default: diff --git a/ui/searchResults/SearchResultTableItem.tsx b/ui/searchResults/SearchResultTableItem.tsx index b91e337df8..660c77d2f9 100644 --- a/ui/searchResults/SearchResultTableItem.tsx +++ b/ui/searchResults/SearchResultTableItem.tsx @@ -1,4 +1,4 @@ -import { Tr, Td, Text, Flex, Image, Box, Skeleton, useColorMode, Tag } from '@chakra-ui/react'; +import { chakra, Tr, Td, Text, Flex, Image, Box, Skeleton, useColorMode, Tag } from '@chakra-ui/react'; import React from 'react'; import xss from 'xss'; @@ -10,6 +10,7 @@ import dayjs from 'lib/date/dayjs'; import highlightText from 'lib/highlightText'; import * as mixpanel from 'lib/mixpanel/index'; import { saveToRecentKeywords } from 'lib/recentSearchKeywords'; +import { ADDRESS_REGEXP } from 'lib/validations/address'; import * as AddressEntity from 'ui/shared/entities/address/AddressEntity'; import * as BlockEntity from 'ui/shared/entities/block/BlockEntity'; import * as TokenEntity from 'ui/shared/entities/token/TokenEntity'; @@ -20,7 +21,6 @@ import LinkExternal from 'ui/shared/LinkExternal'; import LinkInternal from 'ui/shared/LinkInternal'; import type { SearchResultAppItem } from 'ui/shared/search/utils'; import { getItemCategory, searchItemTitles } from 'ui/shared/search/utils'; - interface Props { data: SearchResultItem | SearchResultAppItem; searchTerm: string; @@ -91,70 +91,51 @@ const SearchResultTableItem = ({ data, searchTerm, isLoading }: Props) => { case 'contract': case 'address': { - if (data.name) { - const shouldHighlightHash = data.address.toLowerCase() === searchTerm.toLowerCase(); - const address = { - hash: data.address, - is_contract: data.type === 'contract', - is_verified: data.is_smart_contract_verified, - name: null, - implementation_name: null, - }; - - return ( - <> - - - - - - - - - - - - - - ); - } - + const shouldHighlightHash = ADDRESS_REGEXP.test(searchTerm); + const addressName = data.name || data.ens_info?.name; const address = { hash: data.address, is_contract: data.type === 'contract', is_verified: data.is_smart_contract_verified, name: null, implementation_name: null, + ens_domain_name: null, }; + const expiresText = data.ens_info?.expiry_date ? ` (expires ${ dayjs(data.ens_info.expiry_date).fromNow() })` : ''; return ( - - - - - + + + + - - - - + onClick={ handleLinkClick } + > + + + + + + { addressName && ( + + + { data.ens_info && + ( + data.ens_info.names_count > 1 ? + ({ data.ens_info.names_count > 39 ? '40+' : `+${ data.ens_info.names_count - 1 }` }) : + { expiresText } + ) + } + + ) } + ); } diff --git a/ui/shared/TokenTransfer/__screenshots__/TokenTransferList.pw.tsx_default_with-tx-info-1.png b/ui/shared/TokenTransfer/__screenshots__/TokenTransferList.pw.tsx_default_with-tx-info-1.png index cb52e15b4d..a678e21e98 100644 Binary files a/ui/shared/TokenTransfer/__screenshots__/TokenTransferList.pw.tsx_default_with-tx-info-1.png and b/ui/shared/TokenTransfer/__screenshots__/TokenTransferList.pw.tsx_default_with-tx-info-1.png differ diff --git a/ui/shared/TokenTransfer/__screenshots__/TokenTransferList.pw.tsx_default_without-tx-info-1.png b/ui/shared/TokenTransfer/__screenshots__/TokenTransferList.pw.tsx_default_without-tx-info-1.png index 76300132d9..5af3308e09 100644 Binary files a/ui/shared/TokenTransfer/__screenshots__/TokenTransferList.pw.tsx_default_without-tx-info-1.png and b/ui/shared/TokenTransfer/__screenshots__/TokenTransferList.pw.tsx_default_without-tx-info-1.png differ diff --git a/ui/shared/TokenTransfer/__screenshots__/TokenTransferTable.pw.tsx_default_with-tx-info-1.png b/ui/shared/TokenTransfer/__screenshots__/TokenTransferTable.pw.tsx_default_with-tx-info-1.png index 3848d01acb..070d72be06 100644 Binary files a/ui/shared/TokenTransfer/__screenshots__/TokenTransferTable.pw.tsx_default_with-tx-info-1.png and b/ui/shared/TokenTransfer/__screenshots__/TokenTransferTable.pw.tsx_default_with-tx-info-1.png differ diff --git a/ui/shared/TokenTransfer/__screenshots__/TokenTransferTable.pw.tsx_default_without-tx-info-1.png b/ui/shared/TokenTransfer/__screenshots__/TokenTransferTable.pw.tsx_default_without-tx-info-1.png index f4549dc16c..9dc83bef17 100644 Binary files a/ui/shared/TokenTransfer/__screenshots__/TokenTransferTable.pw.tsx_default_without-tx-info-1.png and b/ui/shared/TokenTransfer/__screenshots__/TokenTransferTable.pw.tsx_default_without-tx-info-1.png differ diff --git a/ui/shared/entities/address/AddressEntity.pw.tsx b/ui/shared/entities/address/AddressEntity.pw.tsx index 8d7b16e570..e94038817b 100644 --- a/ui/shared/entities/address/AddressEntity.pw.tsx +++ b/ui/shared/entities/address/AddressEntity.pw.tsx @@ -80,6 +80,19 @@ test.describe('loading', () => { await expect(component).toHaveScreenshot(); }); + +}); + +test('with ENS', async({ mount }) => { + const component = await mount( + + + , + ); + + await expect(component).toHaveScreenshot(); }); test('external link', async({ mount }) => { diff --git a/ui/shared/entities/address/AddressEntity.tsx b/ui/shared/entities/address/AddressEntity.tsx index fb46d80a94..4e64a683b5 100644 --- a/ui/shared/entities/address/AddressEntity.tsx +++ b/ui/shared/entities/address/AddressEntity.tsx @@ -98,10 +98,11 @@ const Icon = (props: IconProps) => { type ContentProps = Omit & Pick; const Content = chakra((props: ContentProps) => { - if (props.address.name) { + if (props.address.name || props.address.ens_domain_name) { + const text = props.address.ens_domain_name || props.address.name; const label = ( - { props.address.name } + { text } { props.address.hash } ); @@ -109,7 +110,7 @@ const Content = chakra((props: ContentProps) => { return ( - { props.address.name } + { text } ); @@ -137,7 +138,7 @@ const Copy = (props: CopyProps) => { const Container = EntityBase.Container; export interface EntityProps extends EntityBase.EntityBaseProps { - address: Pick; + address: Pick; isSafeAddress?: boolean; } diff --git a/ui/shared/entities/address/__screenshots__/AddressEntity.pw.tsx_default_with-ENS-1.png b/ui/shared/entities/address/__screenshots__/AddressEntity.pw.tsx_default_with-ENS-1.png new file mode 100644 index 0000000000..102632e226 Binary files /dev/null and b/ui/shared/entities/address/__screenshots__/AddressEntity.pw.tsx_default_with-ENS-1.png differ diff --git a/ui/shared/entities/ens/EnsEntity.pw.tsx b/ui/shared/entities/ens/EnsEntity.pw.tsx new file mode 100644 index 0000000000..fc6d5c01c7 --- /dev/null +++ b/ui/shared/entities/ens/EnsEntity.pw.tsx @@ -0,0 +1,70 @@ +import { test, expect } from '@playwright/experimental-ct-react'; +import React from 'react'; + +import TestApp from 'playwright/TestApp'; + +import EnsEntity from './EnsEntity'; + +const name = 'cat.eth'; +const iconSizes = [ 'md', 'lg' ]; + +test.use({ viewport: { width: 180, height: 30 } }); + +test.describe('icon size', () => { + iconSizes.forEach((size) => { + test(size, async({ mount }) => { + const component = await mount( + + + , + ); + + await expect(component).toHaveScreenshot(); + }); + }); +}); + +test('loading', async({ mount }) => { + const component = await mount( + + + , + ); + + await expect(component).toHaveScreenshot(); +}); + +test('with long name', async({ mount }) => { + const component = await mount( + + + , + ); + + await component.getByText(name.slice(0, 4)).hover(); + + await expect(component).toHaveScreenshot(); +}); + +test('customization', async({ mount }) => { + const component = await mount( + + + , + ); + + await expect(component).toHaveScreenshot(); +}); diff --git a/ui/shared/entities/ens/EnsEntity.tsx b/ui/shared/entities/ens/EnsEntity.tsx new file mode 100644 index 0000000000..c0623426c1 --- /dev/null +++ b/ui/shared/entities/ens/EnsEntity.tsx @@ -0,0 +1,89 @@ +import { chakra } from '@chakra-ui/react'; +import _omit from 'lodash/omit'; +import React from 'react'; + +import { route } from 'nextjs-routes'; + +import * as EntityBase from 'ui/shared/entities/base/components'; +import TruncatedValue from 'ui/shared/TruncatedValue'; + +type LinkProps = EntityBase.LinkBaseProps & Pick; + +const Link = chakra((props: LinkProps) => { + const defaultHref = route({ pathname: '/name-domains/[name]', query: { name: props.name } }); + + return ( + + { props.children } + + ); +}); + +type IconProps = Omit & { + iconName?: EntityBase.IconBaseProps['name']; +}; + +const Icon = (props: IconProps) => { + return ( + + ); +}; + +type ContentProps = Omit & Pick; + +const Content = chakra((props: ContentProps) => { + return ( + + ); +}); + +type CopyProps = Omit & Pick; + +const Copy = (props: CopyProps) => { + return ( + + ); +}; + +const Container = EntityBase.Container; + +export interface EntityProps extends EntityBase.EntityBaseProps { + name: string; +} + +const EnsEntity = (props: EntityProps) => { + const linkProps = _omit(props, [ 'className' ]); + const partsProps = _omit(props, [ 'className', 'onClick' ]); + + return ( + + + + + + + + ); +}; + +export default React.memo(chakra(EnsEntity)); + +export { + Container, + Link, + Icon, + Content, + Copy, +}; diff --git a/ui/shared/entities/ens/__screenshots__/EnsEntity.pw.tsx_default_customization-1.png b/ui/shared/entities/ens/__screenshots__/EnsEntity.pw.tsx_default_customization-1.png new file mode 100644 index 0000000000..30c522addf Binary files /dev/null and b/ui/shared/entities/ens/__screenshots__/EnsEntity.pw.tsx_default_customization-1.png differ diff --git a/ui/shared/entities/ens/__screenshots__/EnsEntity.pw.tsx_default_icon-size-lg-1.png b/ui/shared/entities/ens/__screenshots__/EnsEntity.pw.tsx_default_icon-size-lg-1.png new file mode 100644 index 0000000000..cd32a435a1 Binary files /dev/null and b/ui/shared/entities/ens/__screenshots__/EnsEntity.pw.tsx_default_icon-size-lg-1.png differ diff --git a/ui/shared/entities/ens/__screenshots__/EnsEntity.pw.tsx_default_icon-size-md-1.png b/ui/shared/entities/ens/__screenshots__/EnsEntity.pw.tsx_default_icon-size-md-1.png new file mode 100644 index 0000000000..aba449d9c1 Binary files /dev/null and b/ui/shared/entities/ens/__screenshots__/EnsEntity.pw.tsx_default_icon-size-md-1.png differ diff --git a/ui/shared/entities/ens/__screenshots__/EnsEntity.pw.tsx_default_loading-1.png b/ui/shared/entities/ens/__screenshots__/EnsEntity.pw.tsx_default_loading-1.png new file mode 100644 index 0000000000..b50a444966 Binary files /dev/null and b/ui/shared/entities/ens/__screenshots__/EnsEntity.pw.tsx_default_loading-1.png differ diff --git a/ui/shared/entities/ens/__screenshots__/EnsEntity.pw.tsx_default_with-long-name-1.png b/ui/shared/entities/ens/__screenshots__/EnsEntity.pw.tsx_default_with-long-name-1.png new file mode 100644 index 0000000000..273fbf9cb9 Binary files /dev/null and b/ui/shared/entities/ens/__screenshots__/EnsEntity.pw.tsx_default_with-long-name-1.png differ diff --git a/ui/shared/filters/FilterButton.tsx b/ui/shared/filters/FilterButton.tsx index b75eeb21ed..a61246587c 100644 --- a/ui/shared/filters/FilterButton.tsx +++ b/ui/shared/filters/FilterButton.tsx @@ -19,7 +19,7 @@ const FilterButton = ({ isActive, isLoading, appliedFiltersNum, onClick, as }: P const badgeBgColor = useColorModeValue('blue.700', 'gray.50'); if (isLoading) { - return ; + return ; } return ( diff --git a/ui/shared/filters/FilterInput.tsx b/ui/shared/filters/FilterInput.tsx index 5cfaae212f..5c16f65ce6 100644 --- a/ui/shared/filters/FilterInput.tsx +++ b/ui/shared/filters/FilterInput.tsx @@ -37,6 +37,7 @@ const FilterInput = ({ onChange, className, size = 'sm', placeholder, initialVal isLoaded={ !isLoading } className={ className } minW="250px" + borderRadius="base" > { return ( - + { children } ); diff --git a/ui/shared/nft/NftMedia.tsx b/ui/shared/nft/NftMedia.tsx index 9427791f5f..e01569c314 100644 --- a/ui/shared/nft/NftMedia.tsx +++ b/ui/shared/nft/NftMedia.tsx @@ -20,13 +20,19 @@ interface Props { } const NftMedia = ({ url, className, isLoading, withFullscreen }: Props) => { - const [ isMediaLoading, setIsMediaLoading ] = React.useState(Boolean(url)); + const [ isMediaLoading, setIsMediaLoading ] = React.useState(true); const [ isLoadingError, setIsLoadingError ] = React.useState(false); const { ref, inView } = useInView({ triggerOnce: true }); const type = useNftMediaType(url, !isLoading && inView); + React.useEffect(() => { + if (!isLoading) { + setIsMediaLoading(Boolean(url)); + } + }, [ isLoading, url ]); + const handleMediaLoaded = React.useCallback(() => { setIsMediaLoading(false); }, []); diff --git a/ui/shared/pagination/useQueryWithPages.ts b/ui/shared/pagination/useQueryWithPages.ts index 8f6edc7a89..847ee3b09d 100644 --- a/ui/shared/pagination/useQueryWithPages.ts +++ b/ui/shared/pagination/useQueryWithPages.ts @@ -37,7 +37,7 @@ function getPaginationParamsFromQuery(queryString: string | Array | unde export type QueryWithPagesResult = UseQueryResult, ResourceError> & { - onFilterChange: (filters: PaginationFilters) => void; + onFilterChange: (filters: PaginationFilters) => void; onSortingChange: (sorting?: PaginationSorting) => void; pagination: PaginationParams; } @@ -136,12 +136,13 @@ export default function useQueryWithPages({ }); }, [ queryClient, resourceName, router, scrollToTop ]); - const onFilterChange = useCallback((newFilters: PaginationFilters | undefined) => { + const onFilterChange = useCallback((newFilters: PaginationFilters | undefined) => { const newQuery = omit(router.query, 'next_page_params', 'page', resource.filterFields); if (newFilters) { Object.entries(newFilters).forEach(([ key, value ]) => { - if (value && value.length) { - newQuery[key] = Array.isArray(value) ? value.join(',') : (value || ''); + const isValidValue = typeof value === 'boolean' || (value && value.length); + if (isValidValue) { + newQuery[key] = Array.isArray(value) ? value.join(',') : (String(value) || ''); } }); } diff --git a/ui/snippets/searchBar/SearchBar.pw.tsx b/ui/snippets/searchBar/SearchBar.pw.tsx index ca7e6b32f0..6da1396d2f 100644 --- a/ui/snippets/searchBar/SearchBar.pw.tsx +++ b/ui/snippets/searchBar/SearchBar.pw.tsx @@ -65,6 +65,7 @@ test('search by contract name +@mobile +@dark-mode', async({ mount, page }) => status: 200, body: JSON.stringify([ searchMock.contract1, + searchMock.address2, ]), })); diff --git a/ui/snippets/searchBar/SearchBarSuggest/SearchBarSuggestAddress.tsx b/ui/snippets/searchBar/SearchBarSuggest/SearchBarSuggestAddress.tsx index 458abb31d1..0a69e99525 100644 --- a/ui/snippets/searchBar/SearchBarSuggest/SearchBarSuggestAddress.tsx +++ b/ui/snippets/searchBar/SearchBarSuggest/SearchBarSuggestAddress.tsx @@ -1,9 +1,11 @@ -import { Box, Text, Flex } from '@chakra-ui/react'; +import { chakra, Box, Text, Flex } from '@chakra-ui/react'; import React from 'react'; import type { SearchResultAddressOrContract } from 'types/api/search'; +import dayjs from 'lib/date/dayjs'; import highlightText from 'lib/highlightText'; +import { ADDRESS_REGEXP } from 'lib/validations/address'; import * as AddressEntity from 'ui/shared/entities/address/AddressEntity'; import HashStringShortenDynamic from 'ui/shared/HashStringShortenDynamic'; @@ -14,23 +16,41 @@ interface Props { } const SearchBarSuggestAddress = ({ data, isMobile, searchTerm }: Props) => { - const shouldHighlightHash = data.address.toLowerCase() === searchTerm.toLowerCase(); + const shouldHighlightHash = ADDRESS_REGEXP.test(searchTerm); + const icon = ( ); - const name = data.name && ( + const addressName = data.name || data.ens_info?.name; + const expiresText = data.ens_info?.expiry_date ? ` (expires ${ dayjs(data.ens_info.expiry_date).fromNow() })` : ''; + + const nameEl = addressName && ( - + + { data.ens_info && + ( + data.ens_info.names_count > 1 ? + ({ data.ens_info.names_count > 39 ? '40+' : `+${ data.ens_info.names_count - 1 }` }) : + { expiresText } + ) + } ); - const address = ; + const addressEl = ; if (isMobile) { return ( @@ -44,10 +64,10 @@ const SearchBarSuggestAddress = ({ data, isMobile, searchTerm }: Props) => { whiteSpace="nowrap" fontWeight={ 700 } > - { address } + { addressEl } - { name } + { nameEl } ); } @@ -63,10 +83,10 @@ const SearchBarSuggestAddress = ({ data, isMobile, searchTerm }: Props) => { whiteSpace="nowrap" fontWeight={ 700 } > - { address } + { addressEl } - { name } + { nameEl } ); }; diff --git a/ui/snippets/searchBar/__screenshots__/SearchBar.pw.tsx_dark-color-mode_search-by-contract-name-mobile-dark-mode-1.png b/ui/snippets/searchBar/__screenshots__/SearchBar.pw.tsx_dark-color-mode_search-by-contract-name-mobile-dark-mode-1.png index 72ed4ec998..6b1c79a62f 100644 Binary files a/ui/snippets/searchBar/__screenshots__/SearchBar.pw.tsx_dark-color-mode_search-by-contract-name-mobile-dark-mode-1.png and b/ui/snippets/searchBar/__screenshots__/SearchBar.pw.tsx_dark-color-mode_search-by-contract-name-mobile-dark-mode-1.png differ diff --git a/ui/snippets/searchBar/__screenshots__/SearchBar.pw.tsx_default_scroll-suggest-to-category-1.png b/ui/snippets/searchBar/__screenshots__/SearchBar.pw.tsx_default_scroll-suggest-to-category-1.png index 6a08ad4fb7..9017b6762c 100644 Binary files a/ui/snippets/searchBar/__screenshots__/SearchBar.pw.tsx_default_scroll-suggest-to-category-1.png and b/ui/snippets/searchBar/__screenshots__/SearchBar.pw.tsx_default_scroll-suggest-to-category-1.png differ diff --git a/ui/snippets/searchBar/__screenshots__/SearchBar.pw.tsx_default_search-by-contract-name-mobile-dark-mode-1.png b/ui/snippets/searchBar/__screenshots__/SearchBar.pw.tsx_default_search-by-contract-name-mobile-dark-mode-1.png index 10858ccfc6..89a811c836 100644 Binary files a/ui/snippets/searchBar/__screenshots__/SearchBar.pw.tsx_default_search-by-contract-name-mobile-dark-mode-1.png and b/ui/snippets/searchBar/__screenshots__/SearchBar.pw.tsx_default_search-by-contract-name-mobile-dark-mode-1.png differ diff --git a/ui/snippets/searchBar/__screenshots__/SearchBar.pw.tsx_mobile_search-by-contract-name-mobile-dark-mode-1.png b/ui/snippets/searchBar/__screenshots__/SearchBar.pw.tsx_mobile_search-by-contract-name-mobile-dark-mode-1.png index a72f116610..4b21e36376 100644 Binary files a/ui/snippets/searchBar/__screenshots__/SearchBar.pw.tsx_mobile_search-by-contract-name-mobile-dark-mode-1.png and b/ui/snippets/searchBar/__screenshots__/SearchBar.pw.tsx_mobile_search-by-contract-name-mobile-dark-mode-1.png differ diff --git a/ui/token/TokenTransfer/__screenshots__/TokenTransfer.pw.tsx_default_erc1155-mobile-1.png b/ui/token/TokenTransfer/__screenshots__/TokenTransfer.pw.tsx_default_erc1155-mobile-1.png index 883558d9aa..a65640db2b 100644 Binary files a/ui/token/TokenTransfer/__screenshots__/TokenTransfer.pw.tsx_default_erc1155-mobile-1.png and b/ui/token/TokenTransfer/__screenshots__/TokenTransfer.pw.tsx_default_erc1155-mobile-1.png differ diff --git a/ui/token/TokenTransfer/__screenshots__/TokenTransfer.pw.tsx_default_erc20-mobile-1.png b/ui/token/TokenTransfer/__screenshots__/TokenTransfer.pw.tsx_default_erc20-mobile-1.png index 44a65059ad..cab94218e9 100644 Binary files a/ui/token/TokenTransfer/__screenshots__/TokenTransfer.pw.tsx_default_erc20-mobile-1.png and b/ui/token/TokenTransfer/__screenshots__/TokenTransfer.pw.tsx_default_erc20-mobile-1.png differ diff --git a/ui/token/TokenTransfer/__screenshots__/TokenTransfer.pw.tsx_default_erc721-mobile-1.png b/ui/token/TokenTransfer/__screenshots__/TokenTransfer.pw.tsx_default_erc721-mobile-1.png index 5231ad1c6f..5027ffd14d 100644 Binary files a/ui/token/TokenTransfer/__screenshots__/TokenTransfer.pw.tsx_default_erc721-mobile-1.png and b/ui/token/TokenTransfer/__screenshots__/TokenTransfer.pw.tsx_default_erc721-mobile-1.png differ diff --git a/ui/token/TokenTransfer/__screenshots__/TokenTransfer.pw.tsx_mobile_erc1155-mobile-1.png b/ui/token/TokenTransfer/__screenshots__/TokenTransfer.pw.tsx_mobile_erc1155-mobile-1.png index c6add2e46a..16fed052cf 100644 Binary files a/ui/token/TokenTransfer/__screenshots__/TokenTransfer.pw.tsx_mobile_erc1155-mobile-1.png and b/ui/token/TokenTransfer/__screenshots__/TokenTransfer.pw.tsx_mobile_erc1155-mobile-1.png differ diff --git a/ui/token/TokenTransfer/__screenshots__/TokenTransfer.pw.tsx_mobile_erc20-mobile-1.png b/ui/token/TokenTransfer/__screenshots__/TokenTransfer.pw.tsx_mobile_erc20-mobile-1.png index 2b6f10df88..ed8a59156a 100644 Binary files a/ui/token/TokenTransfer/__screenshots__/TokenTransfer.pw.tsx_mobile_erc20-mobile-1.png and b/ui/token/TokenTransfer/__screenshots__/TokenTransfer.pw.tsx_mobile_erc20-mobile-1.png differ diff --git a/ui/token/TokenTransfer/__screenshots__/TokenTransfer.pw.tsx_mobile_erc721-mobile-1.png b/ui/token/TokenTransfer/__screenshots__/TokenTransfer.pw.tsx_mobile_erc721-mobile-1.png index 3d163bd4ef..54ccd232ac 100644 Binary files a/ui/token/TokenTransfer/__screenshots__/TokenTransfer.pw.tsx_mobile_erc721-mobile-1.png and b/ui/token/TokenTransfer/__screenshots__/TokenTransfer.pw.tsx_mobile_erc721-mobile-1.png differ diff --git a/ui/tokens/TokensTableItem.tsx b/ui/tokens/TokensTableItem.tsx index 37d3f04dee..e8f2916866 100644 --- a/ui/tokens/TokensTableItem.tsx +++ b/ui/tokens/TokensTableItem.tsx @@ -48,6 +48,7 @@ const TokensTableItem = ({ implementation_name: null, is_contract: true, is_verified: false, + ens_domain_name: null, }; return ( diff --git a/ui/tx/__screenshots__/TxDetails.pw.tsx_dark-color-mode_between-addresses-mobile-dark-mode-1.png b/ui/tx/__screenshots__/TxDetails.pw.tsx_dark-color-mode_between-addresses-mobile-dark-mode-1.png index 68ad19bf42..23463170c8 100644 Binary files a/ui/tx/__screenshots__/TxDetails.pw.tsx_dark-color-mode_between-addresses-mobile-dark-mode-1.png and b/ui/tx/__screenshots__/TxDetails.pw.tsx_dark-color-mode_between-addresses-mobile-dark-mode-1.png differ diff --git a/ui/tx/__screenshots__/TxDetails.pw.tsx_dark-color-mode_with-actions-uniswap-mobile-dark-mode-1.png b/ui/tx/__screenshots__/TxDetails.pw.tsx_dark-color-mode_with-actions-uniswap-mobile-dark-mode-1.png index e8d07d8ee0..39fb7fb131 100644 Binary files a/ui/tx/__screenshots__/TxDetails.pw.tsx_dark-color-mode_with-actions-uniswap-mobile-dark-mode-1.png and b/ui/tx/__screenshots__/TxDetails.pw.tsx_dark-color-mode_with-actions-uniswap-mobile-dark-mode-1.png differ diff --git a/ui/tx/__screenshots__/TxDetails.pw.tsx_default_between-addresses-mobile-dark-mode-1.png b/ui/tx/__screenshots__/TxDetails.pw.tsx_default_between-addresses-mobile-dark-mode-1.png index bfc23c2e9c..1f55e5d296 100644 Binary files a/ui/tx/__screenshots__/TxDetails.pw.tsx_default_between-addresses-mobile-dark-mode-1.png and b/ui/tx/__screenshots__/TxDetails.pw.tsx_default_between-addresses-mobile-dark-mode-1.png differ diff --git a/ui/tx/__screenshots__/TxDetails.pw.tsx_default_creating-contact-1.png b/ui/tx/__screenshots__/TxDetails.pw.tsx_default_creating-contact-1.png index 26a0a022d2..9541e801e8 100644 Binary files a/ui/tx/__screenshots__/TxDetails.pw.tsx_default_creating-contact-1.png and b/ui/tx/__screenshots__/TxDetails.pw.tsx_default_creating-contact-1.png differ diff --git a/ui/tx/__screenshots__/TxDetails.pw.tsx_default_l2-1.png b/ui/tx/__screenshots__/TxDetails.pw.tsx_default_l2-1.png index dd560f465d..ff34a9052d 100644 Binary files a/ui/tx/__screenshots__/TxDetails.pw.tsx_default_l2-1.png and b/ui/tx/__screenshots__/TxDetails.pw.tsx_default_l2-1.png differ diff --git a/ui/tx/__screenshots__/TxDetails.pw.tsx_default_pending-1.png b/ui/tx/__screenshots__/TxDetails.pw.tsx_default_pending-1.png index b8fd8b5594..9c806be32e 100644 Binary files a/ui/tx/__screenshots__/TxDetails.pw.tsx_default_pending-1.png and b/ui/tx/__screenshots__/TxDetails.pw.tsx_default_pending-1.png differ diff --git a/ui/tx/__screenshots__/TxDetails.pw.tsx_default_stability-customization-1.png b/ui/tx/__screenshots__/TxDetails.pw.tsx_default_stability-customization-1.png index 7f82ce728f..e5288983b3 100644 Binary files a/ui/tx/__screenshots__/TxDetails.pw.tsx_default_stability-customization-1.png and b/ui/tx/__screenshots__/TxDetails.pw.tsx_default_stability-customization-1.png differ diff --git a/ui/tx/__screenshots__/TxDetails.pw.tsx_default_with-actions-uniswap-mobile-dark-mode-1.png b/ui/tx/__screenshots__/TxDetails.pw.tsx_default_with-actions-uniswap-mobile-dark-mode-1.png index 5e1108feca..16eacff02a 100644 Binary files a/ui/tx/__screenshots__/TxDetails.pw.tsx_default_with-actions-uniswap-mobile-dark-mode-1.png and b/ui/tx/__screenshots__/TxDetails.pw.tsx_default_with-actions-uniswap-mobile-dark-mode-1.png differ diff --git a/ui/tx/__screenshots__/TxDetails.pw.tsx_default_with-decoded-raw-reason-1.png b/ui/tx/__screenshots__/TxDetails.pw.tsx_default_with-decoded-raw-reason-1.png index 952d53e5dc..38e7b1c439 100644 Binary files a/ui/tx/__screenshots__/TxDetails.pw.tsx_default_with-decoded-raw-reason-1.png and b/ui/tx/__screenshots__/TxDetails.pw.tsx_default_with-decoded-raw-reason-1.png differ diff --git a/ui/tx/__screenshots__/TxDetails.pw.tsx_default_with-decoded-revert-reason-1.png b/ui/tx/__screenshots__/TxDetails.pw.tsx_default_with-decoded-revert-reason-1.png index 8c62fbd45b..bac354c208 100644 Binary files a/ui/tx/__screenshots__/TxDetails.pw.tsx_default_with-decoded-revert-reason-1.png and b/ui/tx/__screenshots__/TxDetails.pw.tsx_default_with-decoded-revert-reason-1.png differ diff --git a/ui/tx/__screenshots__/TxDetails.pw.tsx_default_with-token-transfer-mobile-1.png b/ui/tx/__screenshots__/TxDetails.pw.tsx_default_with-token-transfer-mobile-1.png index 974f0373e8..e5aa9648eb 100644 Binary files a/ui/tx/__screenshots__/TxDetails.pw.tsx_default_with-token-transfer-mobile-1.png and b/ui/tx/__screenshots__/TxDetails.pw.tsx_default_with-token-transfer-mobile-1.png differ diff --git a/ui/tx/__screenshots__/TxDetails.pw.tsx_default_without-testnet-warning-1.png b/ui/tx/__screenshots__/TxDetails.pw.tsx_default_without-testnet-warning-1.png index 48ac2733f9..0d2911796a 100644 Binary files a/ui/tx/__screenshots__/TxDetails.pw.tsx_default_without-testnet-warning-1.png and b/ui/tx/__screenshots__/TxDetails.pw.tsx_default_without-testnet-warning-1.png differ diff --git a/ui/tx/__screenshots__/TxDetails.pw.tsx_mobile_between-addresses-mobile-dark-mode-1.png b/ui/tx/__screenshots__/TxDetails.pw.tsx_mobile_between-addresses-mobile-dark-mode-1.png index d7b8988da4..861cfaa1e4 100644 Binary files a/ui/tx/__screenshots__/TxDetails.pw.tsx_mobile_between-addresses-mobile-dark-mode-1.png and b/ui/tx/__screenshots__/TxDetails.pw.tsx_mobile_between-addresses-mobile-dark-mode-1.png differ diff --git a/ui/tx/__screenshots__/TxDetails.pw.tsx_mobile_with-actions-uniswap-mobile-dark-mode-1.png b/ui/tx/__screenshots__/TxDetails.pw.tsx_mobile_with-actions-uniswap-mobile-dark-mode-1.png index c04ced57cd..2e94ad9774 100644 Binary files a/ui/tx/__screenshots__/TxDetails.pw.tsx_mobile_with-actions-uniswap-mobile-dark-mode-1.png and b/ui/tx/__screenshots__/TxDetails.pw.tsx_mobile_with-actions-uniswap-mobile-dark-mode-1.png differ diff --git a/ui/tx/__screenshots__/TxDetails.pw.tsx_mobile_with-token-transfer-mobile-1.png b/ui/tx/__screenshots__/TxDetails.pw.tsx_mobile_with-token-transfer-mobile-1.png index 58863375dd..8d756286ed 100644 Binary files a/ui/tx/__screenshots__/TxDetails.pw.tsx_mobile_with-token-transfer-mobile-1.png and b/ui/tx/__screenshots__/TxDetails.pw.tsx_mobile_with-token-transfer-mobile-1.png differ diff --git a/ui/txs/__screenshots__/TxsListItem.pw.tsx_dark-color-mode_base-view-dark-mode-1.png b/ui/txs/__screenshots__/TxsListItem.pw.tsx_dark-color-mode_base-view-dark-mode-1.png index 50ff51df8f..c4b277a457 100644 Binary files a/ui/txs/__screenshots__/TxsListItem.pw.tsx_dark-color-mode_base-view-dark-mode-1.png and b/ui/txs/__screenshots__/TxsListItem.pw.tsx_dark-color-mode_base-view-dark-mode-1.png differ diff --git a/ui/txs/__screenshots__/TxsListItem.pw.tsx_default_base-view-dark-mode-1.png b/ui/txs/__screenshots__/TxsListItem.pw.tsx_default_base-view-dark-mode-1.png index 304fba7300..2dac9221c3 100644 Binary files a/ui/txs/__screenshots__/TxsListItem.pw.tsx_default_base-view-dark-mode-1.png and b/ui/txs/__screenshots__/TxsListItem.pw.tsx_default_base-view-dark-mode-1.png differ diff --git a/ui/txs/__screenshots__/TxsListItem.pw.tsx_default_with-base-address-1.png b/ui/txs/__screenshots__/TxsListItem.pw.tsx_default_with-base-address-1.png index ac6657a22a..8557a1f28a 100644 Binary files a/ui/txs/__screenshots__/TxsListItem.pw.tsx_default_with-base-address-1.png and b/ui/txs/__screenshots__/TxsListItem.pw.tsx_default_with-base-address-1.png differ diff --git a/ui/txs/__screenshots__/TxsTable.pw.tsx_dark-color-mode_base-view-dark-mode-1.png b/ui/txs/__screenshots__/TxsTable.pw.tsx_dark-color-mode_base-view-dark-mode-1.png index a0d6907004..08323441ca 100644 Binary files a/ui/txs/__screenshots__/TxsTable.pw.tsx_dark-color-mode_base-view-dark-mode-1.png and b/ui/txs/__screenshots__/TxsTable.pw.tsx_dark-color-mode_base-view-dark-mode-1.png differ diff --git a/ui/txs/__screenshots__/TxsTable.pw.tsx_default_base-view-dark-mode-1.png b/ui/txs/__screenshots__/TxsTable.pw.tsx_default_base-view-dark-mode-1.png index c27152bc67..ec4fb6b415 100644 Binary files a/ui/txs/__screenshots__/TxsTable.pw.tsx_default_base-view-dark-mode-1.png and b/ui/txs/__screenshots__/TxsTable.pw.tsx_default_base-view-dark-mode-1.png differ diff --git a/ui/txs/__screenshots__/TxsTable.pw.tsx_default_base-view-screen-xl-1.png b/ui/txs/__screenshots__/TxsTable.pw.tsx_default_base-view-screen-xl-1.png index f4a76d6f03..1cfcf7fc1c 100644 Binary files a/ui/txs/__screenshots__/TxsTable.pw.tsx_default_base-view-screen-xl-1.png and b/ui/txs/__screenshots__/TxsTable.pw.tsx_default_base-view-screen-xl-1.png differ