Merge pull request #1460 from blockscout/tom2drum/issue-1376
Name service integration
@@ -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';
|
||||
|
||||
@@ -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;
|
||||
@@ -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'
|
||||
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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']"
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
<svg viewBox="0 0 30 30" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M14.41 3 7.764 7.625c-.396.263-.72.62-.946 1.038a4.74 4.74 0 0 0-.046.1l-.009.019a4.786 4.786 0 0 0 .01 3.93c.233.508.828 1.51.828 1.51L14.41 3ZM4.076 16.404A15.45 15.45 0 0 1 4 14.886l1.168-3.159a6.617 6.617 0 0 0 .755 2.095C9.802 20.393 14.401 27 14.401 27l-7.35-5.108A7.562 7.562 0 0 1 4.25 17.57a7.53 7.53 0 0 1-.174-1.166ZM4 14.886l1.144-3.306a3.575 3.575 0 0 1 0-1.076c-.101.187-.298.57-.298.57a8.68 8.68 0 0 0-.794 2.534A15.45 15.45 0 0 0 4 14.886ZM15.192 27l.063-.103 6.734-11.113.005.009.007-.012s.589 1.002.827 1.51a4.787 4.787 0 0 1-.045 4.05c-.225.417-.55.774-.946 1.037L15.192 27Zm9.263-7.504c.054-.357.054-.72 0-1.076a6.622 6.622 0 0 0-.776-2.242C19.796 9.607 15.2 3 15.2 3l7.35 5.108a7.533 7.533 0 0 1 2.975 5.488c.091.93.098 1.865.02 2.796a8.68 8.68 0 0 1-.794 2.535s-.196.382-.297.57v-.001Z" fill="currentColor"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 962 B |
@@ -0,0 +1,4 @@
|
||||
<svg viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M3.145 8.091c.198.423.689 1.258.689 1.258L9.508 0 3.97 3.854c-.33.22-.6.517-.788.865a3.989 3.989 0 0 0-.037 3.372ZM.897 11.17a6.277 6.277 0 0 0 2.478 4.573L9.501 20S5.668 14.495 2.436 9.018a5.52 5.52 0 0 1-.65-1.868 2.98 2.98 0 0 1 0-.897 35.09 35.09 0 0 0-.247.475A7.234 7.234 0 0 0 .877 8.84a12.86 12.86 0 0 0 .02 2.33Zm15.626.739c-.198-.423-.689-1.258-.689-1.258L10.16 20l5.538-3.852c.33-.22.6-.516.788-.864a3.99 3.99 0 0 0 .037-3.375Zm2.249-3.079a6.279 6.279 0 0 0-2.48-4.573L10.168 0s3.83 5.505 7.065 10.982a5.52 5.52 0 0 1 .647 1.868c.045.297.045.6 0 .897.084-.156.248-.475.248-.475a7.223 7.223 0 0 0 .662-2.112c.065-.776.059-1.555-.017-2.33Z" fill="currentColor"/>
|
||||
<path d="M3.182 4.719c.188-.348.458-.645.788-.865L9.508 0 3.834 9.351s-.496-.835-.69-1.257a3.989 3.989 0 0 1 .038-3.375Zm-2.285 6.45a6.278 6.278 0 0 0 2.478 4.574L9.501 20S5.668 14.495 2.436 9.018a5.52 5.52 0 0 1-.65-1.868 2.98 2.98 0 0 1 0-.897 34.31 34.31 0 0 0-.247.475A7.234 7.234 0 0 0 .877 8.84c-.064.776-.057 1.555.02 2.33Zm15.616.742c-.198-.422-.689-1.258-.689-1.258L10.16 20l5.538-3.852c.33-.22.6-.516.788-.864a3.99 3.99 0 0 0 .037-3.375l-.01.002Zm2.249-3.078a6.277 6.277 0 0 0-2.48-4.574L10.169 0s3.83 5.505 7.064 10.982a5.52 5.52 0 0 1 .647 1.868c.045.297.045.6 0 .897.084-.156.248-.475.248-.475a7.233 7.233 0 0 0 .662-2.112c.064-.776.059-1.555-.018-2.33l-.01.003Z" fill="currentColor" style="mix-blend-mode:color"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.5 KiB |
@@ -19,6 +19,7 @@ const PAGE_PROPS = {
|
||||
hash: '',
|
||||
number: '',
|
||||
q: '',
|
||||
name: '',
|
||||
};
|
||||
|
||||
const TestApp = ({ children }: {children: React.ReactNode}) => {
|
||||
|
||||
@@ -9,7 +9,7 @@ import type { ApiResource, ResourceName, ResourcePathParams } from './resources'
|
||||
export default function buildUrl<R extends ResourceName>(
|
||||
resourceName: R,
|
||||
pathParams?: ResourcePathParams<R>,
|
||||
queryParams?: Record<string, string | Array<string> | number | null | undefined>,
|
||||
queryParams?: Record<string, string | Array<string> | number | boolean | null | undefined>,
|
||||
): string {
|
||||
const resource: ApiResource = RESOURCES[resourceName];
|
||||
const baseUrl = isNeedProxy() ? config.app.baseUrl : (resource.endpoint || config.api.endpoint);
|
||||
|
||||
@@ -36,6 +36,15 @@ import type { ChartMarketResponse, ChartTransactionResponse } from 'types/api/ch
|
||||
import type { BackendVersionConfig } from 'types/api/configs';
|
||||
import type { SmartContract, SmartContractReadMethod, SmartContractWriteMethod, SmartContractVerificationConfig, SolidityscanReport } from 'types/api/contract';
|
||||
import type { VerifiedContractsResponse, VerifiedContractsFilters, VerifiedContractsCounters } from 'types/api/contracts';
|
||||
import type {
|
||||
EnsAddressLookupFilters,
|
||||
EnsAddressLookupResponse,
|
||||
EnsDomainDetailed,
|
||||
EnsDomainEventsResponse,
|
||||
EnsDomainLookupFilters,
|
||||
EnsDomainLookupResponse,
|
||||
EnsLookupSorting,
|
||||
} from 'types/api/ens';
|
||||
import type { IndexingStatus } from 'types/api/indexingStatus';
|
||||
import type { InternalTransactionsResponse } from 'types/api/internalTransaction';
|
||||
import type { L2DepositsResponse, L2DepositsItem } from 'types/api/l2Deposits';
|
||||
@@ -176,6 +185,34 @@ export const RESOURCES = {
|
||||
basePath: getFeaturePayload(config.features.stats)?.api.basePath,
|
||||
},
|
||||
|
||||
// NAME SERVICE
|
||||
addresses_lookup: {
|
||||
path: '/api/v1/:chainId/addresses\\:lookup',
|
||||
pathParams: [ 'chainId' as const ],
|
||||
endpoint: getFeaturePayload(config.features.nameService)?.api.endpoint,
|
||||
basePath: getFeaturePayload(config.features.nameService)?.api.basePath,
|
||||
filterFields: [ 'address' as const, 'resolved_to' as const, 'owned_by' as const, 'only_active' as const ],
|
||||
},
|
||||
domain_info: {
|
||||
path: '/api/v1/:chainId/domains/:name',
|
||||
pathParams: [ 'chainId' as const, 'name' as const ],
|
||||
endpoint: getFeaturePayload(config.features.nameService)?.api.endpoint,
|
||||
basePath: getFeaturePayload(config.features.nameService)?.api.basePath,
|
||||
},
|
||||
domain_events: {
|
||||
path: '/api/v1/:chainId/domains/:name/events',
|
||||
pathParams: [ 'chainId' as const, 'name' as const ],
|
||||
endpoint: getFeaturePayload(config.features.nameService)?.api.endpoint,
|
||||
basePath: getFeaturePayload(config.features.nameService)?.api.basePath,
|
||||
},
|
||||
domains_lookup: {
|
||||
path: '/api/v1/:chainId/domains\\:lookup',
|
||||
pathParams: [ 'chainId' as const ],
|
||||
endpoint: getFeaturePayload(config.features.nameService)?.api.endpoint,
|
||||
basePath: getFeaturePayload(config.features.nameService)?.api.basePath,
|
||||
filterFields: [ 'name' as const, 'only_active' as const ],
|
||||
},
|
||||
|
||||
// VISUALIZATION
|
||||
visualize_sol2uml: {
|
||||
path: '/api/v1/solidity\\:visualize-contracts',
|
||||
@@ -613,7 +650,8 @@ export type PaginatedResources = 'blocks' | 'block_txs' |
|
||||
'l2_output_roots' | 'l2_withdrawals' | 'l2_txn_batches' | 'l2_deposits' |
|
||||
'zkevm_l2_txn_batches' | 'zkevm_l2_txn_batch_txs' |
|
||||
'withdrawals' | 'address_withdrawals' | 'block_withdrawals' |
|
||||
'watchlist' | 'private_tags_address' | 'private_tags_tx';
|
||||
'watchlist' | 'private_tags_address' | 'private_tags_tx' |
|
||||
'domains_lookup' | 'addresses_lookup';
|
||||
|
||||
export type PaginatedResponse<Q extends PaginatedResources> = ResourcePayload<Q>;
|
||||
|
||||
@@ -712,6 +750,10 @@ Q extends 'zkevm_l2_txn_batches_count' ? number :
|
||||
Q extends 'zkevm_l2_txn_batch' ? ZkEvmL2TxnBatch :
|
||||
Q extends 'zkevm_l2_txn_batch_txs' ? ZkEvmL2TxnBatchTxs :
|
||||
Q extends 'config_backend_version' ? BackendVersionConfig :
|
||||
Q extends 'addresses_lookup' ? EnsAddressLookupResponse :
|
||||
Q extends 'domain_info' ? EnsDomainDetailed :
|
||||
Q extends 'domain_events' ? EnsDomainEventsResponse :
|
||||
Q extends 'domains_lookup' ? EnsDomainLookupResponse :
|
||||
never;
|
||||
/* eslint-enable @typescript-eslint/indent */
|
||||
|
||||
@@ -731,6 +773,8 @@ Q extends 'token_inventory' ? TokenInventoryFilters :
|
||||
Q extends 'tokens' ? TokensFilters :
|
||||
Q extends 'tokens_bridged' ? TokensBridgedFilters :
|
||||
Q extends 'verified_contracts' ? VerifiedContractsFilters :
|
||||
Q extends 'addresses_lookup' ? EnsAddressLookupFilters :
|
||||
Q extends 'domains_lookup' ? EnsDomainLookupFilters :
|
||||
never;
|
||||
/* eslint-enable @typescript-eslint/indent */
|
||||
|
||||
@@ -740,5 +784,7 @@ Q extends 'tokens' ? TokensSorting :
|
||||
Q extends 'tokens_bridged' ? TokensSorting :
|
||||
Q extends 'verified_contracts' ? VerifiedContractsSorting :
|
||||
Q extends 'address_txs' ? TransactionsSorting :
|
||||
Q extends 'addresses_lookup' ? EnsLookupSorting :
|
||||
Q extends 'domains_lookup' ? EnsLookupSorting :
|
||||
never;
|
||||
/* eslint-enable @typescript-eslint/indent */
|
||||
|
||||
@@ -19,7 +19,7 @@ import type { ApiResource, ResourceName, ResourcePathParams } from './resources'
|
||||
|
||||
export interface Params<R extends ResourceName> {
|
||||
pathParams?: ResourcePathParams<R>;
|
||||
queryParams?: Record<string, string | Array<string> | number | undefined>;
|
||||
queryParams?: Record<string, string | Array<string> | number | boolean | undefined>;
|
||||
fetchParams?: Pick<FetchParams, 'body' | 'method' | 'signal' | 'headers'>;
|
||||
}
|
||||
|
||||
|
||||
@@ -15,6 +15,7 @@ const AppContext = createContext<PageProps>({
|
||||
hash: '',
|
||||
number: '',
|
||||
q: '',
|
||||
name: '',
|
||||
});
|
||||
|
||||
export function AppContextProvider({ children, pageProps }: Props) {
|
||||
|
||||
@@ -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 },
|
||||
|
||||
@@ -40,6 +40,8 @@ const OG_TYPE_DICT: Record<Route['pathname'], OGPageType> = {
|
||||
'/zkevm-l2-txn-batches': 'Root page',
|
||||
'/zkevm-l2-txn-batch/[number]': 'Regular page',
|
||||
'/404': 'Regular page',
|
||||
'/name-domains': 'Root page',
|
||||
'/name-domains/[name]': 'Regular page',
|
||||
|
||||
// service routes, added only to make typescript happy
|
||||
'/login': 'Regular page',
|
||||
|
||||
@@ -43,6 +43,8 @@ const TEMPLATE_MAP: Record<Route['pathname'], string> = {
|
||||
'/zkevm-l2-txn-batches': DEFAULT_TEMPLATE,
|
||||
'/zkevm-l2-txn-batch/[number]': DEFAULT_TEMPLATE,
|
||||
'/404': DEFAULT_TEMPLATE,
|
||||
'/name-domains': DEFAULT_TEMPLATE,
|
||||
'/name-domains/[name]': DEFAULT_TEMPLATE,
|
||||
|
||||
// service routes, added only to make typescript happy
|
||||
'/login': DEFAULT_TEMPLATE,
|
||||
|
||||
@@ -33,11 +33,13 @@ const TEMPLATE_MAP: Record<Route['pathname'], string> = {
|
||||
'/csv-export': 'export data to CSV',
|
||||
'/l2-deposits': 'deposits (L1 > L2)',
|
||||
'/l2-output-roots': 'output roots',
|
||||
'/l2-txn-batches': 'Tx batches (L2 blocks)',
|
||||
'/l2-txn-batches': 'tx batches (L2 blocks)',
|
||||
'/l2-withdrawals': 'withdrawals (L2 > L1)',
|
||||
'/zkevm-l2-txn-batches': 'zkEvm L2 Tx batches',
|
||||
'/zkevm-l2-txn-batch/[number]': 'zkEvm L2 Tx batch %number%',
|
||||
'/404': 'error - page not found',
|
||||
'/name-domains': 'domains search and resolve',
|
||||
'/name-domains/[name]': '%name% domain details',
|
||||
|
||||
// service routes, added only to make typescript happy
|
||||
'/login': 'login',
|
||||
|
||||
@@ -38,6 +38,8 @@ export const PAGE_TYPE_DICT: Record<Route['pathname'], string> = {
|
||||
'/zkevm-l2-txn-batches': 'ZkEvm L2 Tx batches',
|
||||
'/zkevm-l2-txn-batch/[number]': 'ZkEvm L2 Tx batch details',
|
||||
'/404': '404',
|
||||
'/name-domains': 'Domains search and resolve',
|
||||
'/name-domains/[name]': 'Domain details',
|
||||
|
||||
// service routes, added only to make typescript happy
|
||||
'/login': 'Login',
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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: {},
|
||||
};
|
||||
@@ -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',
|
||||
};
|
||||
@@ -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',
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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',
|
||||
},
|
||||
|
||||
@@ -33,6 +33,7 @@ export const txInterpretation: TxInterpretationResponse = {
|
||||
private_tags: [],
|
||||
public_tags: [],
|
||||
watchlist_names: [],
|
||||
ens_domain_name: null,
|
||||
},
|
||||
},
|
||||
timestamp: {
|
||||
|
||||
@@ -10,6 +10,7 @@ export type Props = {
|
||||
hash: string;
|
||||
number: string;
|
||||
q: string;
|
||||
name: string;
|
||||
}
|
||||
|
||||
export const base: GetServerSideProps<Props> = async({ req, query }) => {
|
||||
@@ -22,6 +23,7 @@ export const base: GetServerSideProps<Props> = async({ req, query }) => {
|
||||
height_or_hash: query.height_or_hash?.toString() || '',
|
||||
number: query.number?.toString() || '',
|
||||
q: query.q?.toString() || '',
|
||||
name: query.name?.toString() || '',
|
||||
},
|
||||
};
|
||||
};
|
||||
@@ -126,6 +128,16 @@ export const suave: GetServerSideProps<Props> = async(context) => {
|
||||
return base(context);
|
||||
};
|
||||
|
||||
export const nameService: GetServerSideProps<Props> = async(context) => {
|
||||
if (!config.features.nameService.isEnabled) {
|
||||
return {
|
||||
notFound: true,
|
||||
};
|
||||
}
|
||||
|
||||
return base(context);
|
||||
};
|
||||
|
||||
export const accounts: GetServerSideProps<Props> = async(context) => {
|
||||
if (config.UI.views.address.hiddenViews?.top_accounts) {
|
||||
return {
|
||||
|
||||
@@ -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 }>
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
import type { NextPage } from 'next';
|
||||
import dynamic from 'next/dynamic';
|
||||
import React from 'react';
|
||||
|
||||
import type { Props } from 'nextjs/getServerSideProps';
|
||||
import PageNextJs from 'nextjs/PageNextJs';
|
||||
|
||||
const NameDomain = dynamic(() => import('ui/pages/NameDomain'), { ssr: false });
|
||||
|
||||
const Page: NextPage<Props> = (props: Props) => {
|
||||
return (
|
||||
<PageNextJs pathname="/name-domains/[name]" query={ props }>
|
||||
<NameDomain/>
|
||||
</PageNextJs>
|
||||
);
|
||||
};
|
||||
|
||||
export default Page;
|
||||
|
||||
export { nameService as getServerSideProps } from 'nextjs/getServerSideProps';
|
||||
@@ -0,0 +1,19 @@
|
||||
import type { NextPage } from 'next';
|
||||
import dynamic from 'next/dynamic';
|
||||
import React from 'react';
|
||||
|
||||
import PageNextJs from 'nextjs/PageNextJs';
|
||||
|
||||
const NameDomains = dynamic(() => import('ui/pages/NameDomains'), { ssr: false });
|
||||
|
||||
const Page: NextPage = () => {
|
||||
return (
|
||||
<PageNextJs pathname="/name-domains">
|
||||
<NameDomains/>
|
||||
</PageNextJs>
|
||||
);
|
||||
};
|
||||
|
||||
export default Page;
|
||||
|
||||
export { nameService as getServerSideProps } from 'nextjs/getServerSideProps';
|
||||
@@ -29,6 +29,7 @@ const defaultAppContext = {
|
||||
hash: '',
|
||||
number: '',
|
||||
q: '',
|
||||
name: '',
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
@@ -31,6 +31,8 @@
|
||||
| "email-sent"
|
||||
| "email"
|
||||
| "empty_search_result"
|
||||
| "ENS_slim"
|
||||
| "ENS"
|
||||
| "error-pages/404"
|
||||
| "error-pages/422"
|
||||
| "error-pages/429"
|
||||
|
||||
@@ -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',
|
||||
};
|
||||
@@ -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 = {
|
||||
|
||||
@@ -11,4 +11,5 @@ export const ADDRESS_PARAMS: AddressParam = {
|
||||
private_tags: [],
|
||||
public_tags: [],
|
||||
watchlist_names: [],
|
||||
ens_domain_name: null,
|
||||
};
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -21,4 +21,5 @@ export interface AddressParam extends UserTags {
|
||||
name: string | null;
|
||||
is_contract: boolean;
|
||||
is_verified: boolean | null;
|
||||
ens_domain_name: string | null;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,71 @@
|
||||
export interface EnsDomain {
|
||||
id: string;
|
||||
name: string;
|
||||
resolved_address: {
|
||||
hash: string;
|
||||
} | null;
|
||||
owner: {
|
||||
hash: string;
|
||||
} | null;
|
||||
wrapped_owner: {
|
||||
hash: string;
|
||||
} | null;
|
||||
registration_date?: string;
|
||||
expiry_date: string | null;
|
||||
}
|
||||
|
||||
export interface EnsDomainDetailed extends EnsDomain {
|
||||
tokens: Array<{ id: string; contract_hash: string; type: 'NATIVE_DOMAIN_TOKEN' | 'WRAPPED_DOMAIN_TOKEN' }>;
|
||||
registrant: {
|
||||
hash: string;
|
||||
} | null;
|
||||
other_addresses: Record<string, string>;
|
||||
}
|
||||
|
||||
export interface EnsDomainEvent {
|
||||
transaction_hash: string;
|
||||
timestamp: string;
|
||||
from_address: {
|
||||
hash: string;
|
||||
} | null;
|
||||
action?: string;
|
||||
}
|
||||
|
||||
export interface EnsAddressLookupResponse {
|
||||
items: Array<EnsDomain>;
|
||||
next_page_params: {
|
||||
page_token: string;
|
||||
page_size: number;
|
||||
} | null;
|
||||
}
|
||||
|
||||
export interface EnsDomainEventsResponse {
|
||||
items: Array<EnsDomainEvent>;
|
||||
}
|
||||
|
||||
export interface EnsDomainLookupResponse {
|
||||
items: Array<EnsDomain>;
|
||||
next_page_params: {
|
||||
page_token: string;
|
||||
page_size: number;
|
||||
} | null;
|
||||
}
|
||||
|
||||
export interface EnsAddressLookupFilters {
|
||||
address: string | null;
|
||||
resolved_to: boolean;
|
||||
owned_by: boolean;
|
||||
only_active: boolean;
|
||||
}
|
||||
|
||||
export interface EnsDomainLookupFilters {
|
||||
name: string | null;
|
||||
only_active: boolean;
|
||||
}
|
||||
|
||||
export interface EnsLookupSorting {
|
||||
sort: 'registration_date';
|
||||
order: 'ASC' | 'DESC';
|
||||
}
|
||||
|
||||
export type EnsDomainLookupFiltersOptions = Array<'resolved_to' | 'owned_by' | 'with_inactive'>;
|
||||
@@ -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 {
|
||||
|
||||
|
Before Width: | Height: | Size: 22 KiB After Width: | Height: | Size: 21 KiB |
|
Before Width: | Height: | Size: 24 KiB After Width: | Height: | Size: 23 KiB |
|
Before Width: | Height: | Size: 29 KiB After Width: | Height: | Size: 28 KiB |
|
Before Width: | Height: | Size: 31 KiB After Width: | Height: | Size: 30 KiB |
|
Before Width: | Height: | Size: 52 KiB After Width: | Height: | Size: 52 KiB |
|
Before Width: | Height: | Size: 55 KiB After Width: | Height: | Size: 54 KiB |
|
Before Width: | Height: | Size: 53 KiB After Width: | Height: | Size: 51 KiB |
@@ -0,0 +1,37 @@
|
||||
import { test, expect } from '@playwright/experimental-ct-react';
|
||||
import React from 'react';
|
||||
|
||||
import config from 'configs/app';
|
||||
import * as ensDomainMock from 'mocks/ens/domain';
|
||||
import TestApp from 'playwright/TestApp';
|
||||
import buildApiUrl from 'playwright/utils/buildApiUrl';
|
||||
|
||||
import AddressEnsDomains from './AddressEnsDomains';
|
||||
|
||||
const ADDRESS_HASH = ensDomainMock.ensDomainA.owner?.hash ?? '';
|
||||
const ADDRESSES_LOOKUP_API_URL = buildApiUrl('addresses_lookup', { chainId: config.chain.id }) +
|
||||
`?address=${ ADDRESS_HASH }&resolved_to=true&owned_by=true&only_active=true&order=ASC`;
|
||||
|
||||
test('base view', async({ mount, page }) => {
|
||||
await page.route(ADDRESSES_LOOKUP_API_URL, (route) => route.fulfill({
|
||||
status: 200,
|
||||
body: JSON.stringify({
|
||||
items: [
|
||||
ensDomainMock.ensDomainA,
|
||||
ensDomainMock.ensDomainB,
|
||||
ensDomainMock.ensDomainC,
|
||||
ensDomainMock.ensDomainD,
|
||||
],
|
||||
}),
|
||||
}));
|
||||
|
||||
const component = await mount(
|
||||
<TestApp>
|
||||
<AddressEnsDomains addressHash={ ADDRESS_HASH } mainDomainName={ ensDomainMock.ensDomainA.name }/>
|
||||
</TestApp>,
|
||||
);
|
||||
|
||||
await component.getByText('4 domains').click();
|
||||
|
||||
await expect(page).toHaveScreenshot({ clip: { x: 0, y: 0, width: 550, height: 350 } });
|
||||
});
|
||||
@@ -0,0 +1,152 @@
|
||||
import { Button, chakra, Flex, Grid, Popover, PopoverBody, PopoverContent, PopoverTrigger, Skeleton, useDisclosure } from '@chakra-ui/react';
|
||||
import _clamp from 'lodash/clamp';
|
||||
import React from 'react';
|
||||
|
||||
import type { EnsDomain } from 'types/api/ens';
|
||||
|
||||
import { route } from 'nextjs-routes';
|
||||
|
||||
import config from 'configs/app';
|
||||
import useApiQuery from 'lib/api/useApiQuery';
|
||||
import dayjs from 'lib/date/dayjs';
|
||||
import EnsEntity from 'ui/shared/entities/ens/EnsEntity';
|
||||
import IconSvg from 'ui/shared/IconSvg';
|
||||
import LinkInternal from 'ui/shared/LinkInternal';
|
||||
|
||||
interface Props {
|
||||
addressHash: string;
|
||||
mainDomainName: string | null;
|
||||
}
|
||||
|
||||
const DomainsGrid = ({ data }: { data: Array<EnsDomain> }) => {
|
||||
return (
|
||||
<Grid
|
||||
templateColumns={{ base: `repeat(${ _clamp(data.length, 1, 2) }, 1fr)`, lg: `repeat(${ _clamp(data.length, 1, 3) }, 1fr)` }}
|
||||
columnGap={ 8 }
|
||||
rowGap={ 4 }
|
||||
mt={ 2 }
|
||||
>
|
||||
{ data.slice(0, 9).map((domain) => <EnsEntity key={ domain.id } name={ domain.name } noCopy/>) }
|
||||
</Grid>
|
||||
);
|
||||
};
|
||||
|
||||
const AddressEnsDomains = ({ addressHash, mainDomainName }: Props) => {
|
||||
const { isOpen, onToggle, onClose } = useDisclosure();
|
||||
|
||||
const { data, isPending, isError } = useApiQuery('addresses_lookup', {
|
||||
pathParams: { chainId: config.chain.id },
|
||||
queryParams: {
|
||||
address: addressHash,
|
||||
resolved_to: true,
|
||||
owned_by: true,
|
||||
only_active: true,
|
||||
order: 'ASC',
|
||||
},
|
||||
});
|
||||
|
||||
if (isError) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (isPending) {
|
||||
return <Skeleton h={ 8 } w={{ base: '60px', lg: '120px' }} borderRadius="base"/>;
|
||||
}
|
||||
|
||||
if (data.items.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const mainDomain = data.items.find((domain) => domain.name === mainDomainName);
|
||||
const ownedDomains = data.items.filter((domain) => {
|
||||
if (domain.name === mainDomainName) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// exclude resolved address
|
||||
if (domain.resolved_address && domain.resolved_address.hash.toLowerCase() === addressHash.toLowerCase()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (domain.owner && domain.owner.hash.toLowerCase() === addressHash.toLowerCase()) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// include wrapped owner
|
||||
if (domain.wrapped_owner?.hash.toLowerCase() === addressHash.toLowerCase()) {
|
||||
return !domain.resolved_address || domain.resolved_address.hash.toLowerCase() !== addressHash.toLowerCase();
|
||||
}
|
||||
|
||||
return false;
|
||||
});
|
||||
const resolvedDomains = data.items.filter((domain) =>
|
||||
domain.resolved_address &&
|
||||
domain.resolved_address.hash.toLowerCase() === addressHash.toLowerCase() &&
|
||||
domain.name !== mainDomainName,
|
||||
);
|
||||
|
||||
const totalRecords = data.items.length > 40 ? '40+' : data.items.length;
|
||||
|
||||
return (
|
||||
<Popover isOpen={ isOpen } onClose={ onClose } placement="bottom-start" isLazy>
|
||||
<PopoverTrigger>
|
||||
<Button
|
||||
size="sm"
|
||||
variant="outline"
|
||||
colorScheme="gray"
|
||||
onClick={ onToggle }
|
||||
aria-label="Address ENS domains"
|
||||
fontWeight={ 500 }
|
||||
px={ 2 }
|
||||
h="32px"
|
||||
flexShrink={ 0 }
|
||||
>
|
||||
<IconSvg name="ENS_slim" boxSize={ 5 }/>
|
||||
<chakra.span ml={ 1 } display={{ base: 'none', lg: 'block' }}>{ totalRecords } Domain{ data.items.length > 1 ? 's' : '' }</chakra.span>
|
||||
<IconSvg name="arrows/east-mini" transform={ isOpen ? 'rotate(90deg)' : 'rotate(-90deg)' } transitionDuration="faster" boxSize={ 5 }/>
|
||||
</Button>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent w={{ base: '100vw', lg: '500px' }}>
|
||||
<PopoverBody px={ 6 } py={ 5 } fontSize="sm" display="flex" flexDir="column" rowGap={ 5 } alignItems="flex-start">
|
||||
{ mainDomain && (
|
||||
<div>
|
||||
<chakra.span color="text_secondary" fontSize="xs">Primary*</chakra.span>
|
||||
<Flex alignItems="center" fontSize="md" mt={ 2 }>
|
||||
<EnsEntity name={ mainDomain.name } fontWeight={ 600 } noCopy/>
|
||||
{ mainDomain.expiry_date &&
|
||||
<chakra.span color="text_secondary" whiteSpace="pre"> (expires { dayjs(mainDomain.expiry_date).fromNow() })</chakra.span> }
|
||||
</Flex>
|
||||
</div>
|
||||
) }
|
||||
{ ownedDomains.length > 0 && (
|
||||
<div>
|
||||
<chakra.span color="text_secondary" fontSize="xs">Owned by this address</chakra.span>
|
||||
<DomainsGrid data={ ownedDomains }/>
|
||||
</div>
|
||||
) }
|
||||
{ resolvedDomains.length > 0 && (
|
||||
<div>
|
||||
<chakra.span color="text_secondary" fontSize="xs">Resolved to this address</chakra.span>
|
||||
<DomainsGrid data={ resolvedDomains }/>
|
||||
</div>
|
||||
) }
|
||||
{ (ownedDomains.length > 9 || resolvedDomains.length > 9) && (
|
||||
<LinkInternal
|
||||
href={ route({ pathname: '/name-domains', query: { owned_by: 'true', resolved_to: 'true', address: addressHash } }) }
|
||||
>
|
||||
<span> More results</span>
|
||||
<chakra.span color="text_secondary"> ({ totalRecords })</chakra.span>
|
||||
</LinkInternal>
|
||||
) }
|
||||
{ mainDomain && (
|
||||
<chakra.span fontSize="xs" mt={ -1 }>
|
||||
*A domain name is not necessarily held by a person popularly associated with the name
|
||||
</chakra.span>
|
||||
) }
|
||||
</PopoverBody>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
);
|
||||
};
|
||||
|
||||
export default React.memo(AddressEnsDomains);
|
||||
|
After Width: | Height: | Size: 23 KiB |
|
Before Width: | Height: | Size: 54 KiB After Width: | Height: | Size: 54 KiB |
|
Before Width: | Height: | Size: 92 KiB After Width: | Height: | Size: 82 KiB |
|
Before Width: | Height: | Size: 90 KiB After Width: | Height: | Size: 81 KiB |
|
Before Width: | Height: | Size: 34 KiB After Width: | Height: | Size: 33 KiB |
|
Before Width: | Height: | Size: 67 KiB After Width: | Height: | Size: 60 KiB |
@@ -65,7 +65,7 @@ const DepositsListItem = ({ item, isLoading }: Props) => {
|
||||
<ListItemMobileGrid.Label isLoading={ isLoading }>L1 txn origin</ListItemMobileGrid.Label>
|
||||
<ListItemMobileGrid.Value>
|
||||
<AddressEntityL1
|
||||
address={{ hash: item.l1_tx_origin, name: '', is_contract: false, is_verified: false, implementation_name: '' }}
|
||||
address={{ hash: item.l1_tx_origin, name: '', is_contract: false, is_verified: false, implementation_name: '', ens_domain_name: null }}
|
||||
isLoading={ isLoading }
|
||||
noCopy
|
||||
/>
|
||||
|
||||
@@ -56,7 +56,7 @@ const WithdrawalsTableItem = ({ item, isLoading }: Props) => {
|
||||
</Td>
|
||||
<Td verticalAlign="middle">
|
||||
<AddressEntityL1
|
||||
address={{ hash: item.l1_tx_origin, name: '', is_contract: false, is_verified: false, implementation_name: '' }}
|
||||
address={{ hash: item.l1_tx_origin, name: '', is_contract: false, is_verified: false, implementation_name: '', ens_domain_name: null }}
|
||||
isLoading={ isLoading }
|
||||
truncation="constant"
|
||||
noCopy
|
||||
|
||||
@@ -0,0 +1,176 @@
|
||||
import { Grid, Skeleton, Tooltip, Flex } from '@chakra-ui/react';
|
||||
import type { UseQueryResult } from '@tanstack/react-query';
|
||||
import React from 'react';
|
||||
|
||||
import type { EnsDomainDetailed } from 'types/api/ens';
|
||||
|
||||
import { route } from 'nextjs-routes';
|
||||
|
||||
import type { ResourceError } from 'lib/api/resources';
|
||||
import dayjs from 'lib/date/dayjs';
|
||||
import DetailsInfoItem from 'ui/shared/DetailsInfoItem';
|
||||
import AddressEntity from 'ui/shared/entities/address/AddressEntity';
|
||||
import NftEntity from 'ui/shared/entities/nft/NftEntity';
|
||||
import IconSvg from 'ui/shared/IconSvg';
|
||||
import LinkInternal from 'ui/shared/LinkInternal';
|
||||
import TextSeparator from 'ui/shared/TextSeparator';
|
||||
|
||||
import NameDomainExpiryStatus from './NameDomainExpiryStatus';
|
||||
|
||||
interface Props {
|
||||
query: UseQueryResult<EnsDomainDetailed, ResourceError<unknown>>;
|
||||
}
|
||||
|
||||
const NameDomainDetails = ({ query }: Props) => {
|
||||
const isLoading = query.isPlaceholderData;
|
||||
|
||||
const otherAddresses = Object.entries(query.data?.other_addresses ?? {});
|
||||
const hasExpired = query.data?.expiry_date && dayjs(query.data.expiry_date).isBefore(dayjs());
|
||||
|
||||
return (
|
||||
<Grid columnGap={ 8 } rowGap={ 3 } templateColumns={{ base: 'minmax(0, 1fr)', lg: 'max-content minmax(728px, auto)' }}>
|
||||
{ query.data?.registration_date && (
|
||||
<DetailsInfoItem
|
||||
title="Registration date"
|
||||
hint="The date the name was registered"
|
||||
isLoading={ isLoading }
|
||||
>
|
||||
<IconSvg name="clock" boxSize={ 5 } color="gray.500" verticalAlign="middle" isLoading={ isLoading } mr={ 2 }/>
|
||||
<Skeleton isLoaded={ !isLoading } display="inline" whiteSpace="pre-wrap" lineHeight="20px">
|
||||
{ dayjs(query.data.registration_date).format('llll') }
|
||||
</Skeleton>
|
||||
</DetailsInfoItem>
|
||||
) }
|
||||
{ query.data?.expiry_date && (
|
||||
<DetailsInfoItem
|
||||
title="Expiration date"
|
||||
// eslint-disable-next-line max-len
|
||||
hint="The date the name expires, upon which there is a 90 day grace period for the owner to renew. After the 90 days, the name is released to the market"
|
||||
isLoading={ isLoading }
|
||||
display="inline-block"
|
||||
>
|
||||
<IconSvg name="clock" boxSize={ 5 } color="gray.500" verticalAlign="middle" isLoading={ isLoading } mr={ 2 } mt="-2px"/>
|
||||
{ hasExpired && (
|
||||
<>
|
||||
<Skeleton isLoaded={ !isLoading } display="inline" whiteSpace="pre-wrap" lineHeight="24px">
|
||||
{ dayjs(query.data.expiry_date).fromNow() }
|
||||
</Skeleton>
|
||||
<TextSeparator color="gray.500"/>
|
||||
</>
|
||||
) }
|
||||
<Skeleton isLoaded={ !isLoading } display="inline" whiteSpace="pre-wrap" lineHeight="24px">
|
||||
{ dayjs(query.data.expiry_date).format('llll') }
|
||||
</Skeleton>
|
||||
<TextSeparator color="gray.500"/>
|
||||
<Skeleton isLoaded={ !isLoading } color="text_secondary" display="inline">
|
||||
<NameDomainExpiryStatus date={ query.data?.expiry_date }/>
|
||||
</Skeleton>
|
||||
</DetailsInfoItem>
|
||||
) }
|
||||
{ query.data?.registrant && (
|
||||
<DetailsInfoItem
|
||||
title="Registrant"
|
||||
hint="The account that owns the domain name and has the rights to edit its ownership and records"
|
||||
isLoading={ isLoading }
|
||||
columnGap={ 2 }
|
||||
flexWrap="nowrap"
|
||||
>
|
||||
<AddressEntity
|
||||
address={ query.data.registrant }
|
||||
isLoading={ isLoading }
|
||||
/>
|
||||
<Tooltip label="Lookup for related domain names">
|
||||
<LinkInternal
|
||||
flexShrink={ 0 }
|
||||
display="inline-flex"
|
||||
href={ route({ pathname: '/name-domains', query: { owned_by: 'true', resolved_to: 'true', address: query.data.registrant.hash } }) }
|
||||
>
|
||||
<IconSvg name="search" boxSize={ 5 } isLoading={ isLoading }/>
|
||||
</LinkInternal>
|
||||
</Tooltip>
|
||||
</DetailsInfoItem>
|
||||
) }
|
||||
{ query.data?.owner && (
|
||||
<DetailsInfoItem
|
||||
title="Owner"
|
||||
hint="The account that owns the rights to edit the records of this domain name"
|
||||
isLoading={ isLoading }
|
||||
columnGap={ 2 }
|
||||
flexWrap="nowrap"
|
||||
>
|
||||
<AddressEntity
|
||||
address={ query.data.owner }
|
||||
isLoading={ isLoading }
|
||||
/>
|
||||
<Tooltip label="Lookup for related domain names">
|
||||
<LinkInternal
|
||||
flexShrink={ 0 }
|
||||
display="inline-flex"
|
||||
href={ route({ pathname: '/name-domains', query: { owned_by: 'true', resolved_to: 'true', address: query.data.owner.hash } }) }
|
||||
>
|
||||
<IconSvg name="search" boxSize={ 5 } isLoading={ isLoading }/>
|
||||
</LinkInternal>
|
||||
</Tooltip>
|
||||
</DetailsInfoItem>
|
||||
) }
|
||||
{ query.data?.wrapped_owner && (
|
||||
<DetailsInfoItem
|
||||
title="Manager"
|
||||
hint="Owner of this NFT domain in NameWrapper contract"
|
||||
isLoading={ isLoading }
|
||||
columnGap={ 2 }
|
||||
flexWrap="nowrap"
|
||||
>
|
||||
<AddressEntity
|
||||
address={ query.data.wrapped_owner }
|
||||
isLoading={ isLoading }
|
||||
/>
|
||||
<Tooltip label="Lookup for related domain names">
|
||||
<LinkInternal
|
||||
flexShrink={ 0 }
|
||||
display="inline-flex"
|
||||
href={ route({ pathname: '/name-domains', query: { owned_by: 'true', resolved_to: 'true', address: query.data.wrapped_owner.hash } }) }
|
||||
>
|
||||
<IconSvg name="search" boxSize={ 5 } isLoading={ isLoading }/>
|
||||
</LinkInternal>
|
||||
</Tooltip>
|
||||
</DetailsInfoItem>
|
||||
) }
|
||||
{ query.data?.tokens.map((token) => (
|
||||
<DetailsInfoItem
|
||||
key={ token.type }
|
||||
title={ token.type === 'WRAPPED_DOMAIN_TOKEN' ? 'Wrapped token ID' : 'Token ID' }
|
||||
hint={ `The ${ token.type === 'WRAPPED_DOMAIN_TOKEN' ? 'wrapped ' : '' }token ID of this domain name NFT` }
|
||||
isLoading={ isLoading }
|
||||
wordBreak="break-all"
|
||||
whiteSpace="pre-wrap"
|
||||
>
|
||||
<NftEntity hash={ token.contract_hash } id={ token.id } isLoading={ isLoading } noIcon/>
|
||||
</DetailsInfoItem>
|
||||
)) }
|
||||
{ otherAddresses.length > 0 && (
|
||||
<DetailsInfoItem
|
||||
title="Other addresses"
|
||||
hint="Other cryptocurrency addresses added to this domain name"
|
||||
isLoading={ isLoading }
|
||||
flexDir="column"
|
||||
alignItems="flex-start"
|
||||
>
|
||||
{ otherAddresses.map(([ type, address ]) => (
|
||||
<Flex key={ type } columnGap={ 2 } minW="0" w="100%" overflow="hidden">
|
||||
<Skeleton isLoaded={ !isLoading }>{ type }</Skeleton>
|
||||
<AddressEntity
|
||||
address={{ hash: address }}
|
||||
isLoading={ isLoading }
|
||||
noLink
|
||||
noIcon
|
||||
/>
|
||||
</Flex>
|
||||
)) }
|
||||
</DetailsInfoItem>
|
||||
) }
|
||||
</Grid>
|
||||
);
|
||||
};
|
||||
|
||||
export default React.memo(NameDomainDetails);
|
||||
@@ -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 <chakra.span color="red.600">Expired</chakra.span>;
|
||||
}
|
||||
|
||||
const diff = dayjs(date).diff(dayjs(), 'day');
|
||||
if (diff < 30) {
|
||||
return <chakra.span color="red.600">{ diff } days left</chakra.span>;
|
||||
}
|
||||
|
||||
return <chakra.span color="text_secondary">Expires { dayjs(date).fromNow() }</chakra.span>;
|
||||
};
|
||||
|
||||
export default React.memo(NameDomainExpiryStatus);
|
||||
@@ -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<Sort>();
|
||||
|
||||
const { isPlaceholderData, isError, data } = useApiQuery('domain_events', {
|
||||
pathParams: { name: domainName, chainId: config.chain.id },
|
||||
queryOptions: {
|
||||
placeholderData: { items: Array(4).fill(ENS_DOMAIN_EVENT) },
|
||||
},
|
||||
});
|
||||
|
||||
const handleSortToggle = React.useCallback((event: React.MouseEvent) => {
|
||||
if (isPlaceholderData) {
|
||||
return;
|
||||
}
|
||||
const field = (event.currentTarget as HTMLDivElement).getAttribute('data-field') as SortField | undefined;
|
||||
|
||||
if (field) {
|
||||
setSort(getNextSortValue(field));
|
||||
}
|
||||
}, [ isPlaceholderData ]);
|
||||
|
||||
const content = (
|
||||
<>
|
||||
<Show below="lg" ssr={ false }>
|
||||
<Box>
|
||||
{ data?.items.map((item, index) => <NameDomainHistoryListItem key={ index } { ...item } isLoading={ isPlaceholderData }/>) }
|
||||
</Box>
|
||||
</Show>
|
||||
<Hide below="lg" ssr={ false }>
|
||||
<NameDomainHistoryTable
|
||||
data={ data }
|
||||
isLoading={ isPlaceholderData }
|
||||
sort={ sort }
|
||||
onSortToggle={ handleSortToggle }
|
||||
/>
|
||||
</Hide>
|
||||
</>
|
||||
);
|
||||
|
||||
return (
|
||||
<DataListDisplay
|
||||
isError={ isError }
|
||||
items={ data?.items }
|
||||
emptyText="There are no events for this domain."
|
||||
content={ content }
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default React.memo(NameDomainHistory);
|
||||
@@ -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 (
|
||||
<ListItemMobileGrid.Container>
|
||||
<ListItemMobileGrid.Label isLoading={ isLoading }>Txn hash</ListItemMobileGrid.Label>
|
||||
<ListItemMobileGrid.Value>
|
||||
<TxEntity hash={ transactionHash } isLoading={ isLoading } fontWeight={ 500 }/>
|
||||
</ListItemMobileGrid.Value>
|
||||
|
||||
<ListItemMobileGrid.Label isLoading={ isLoading }>Age</ListItemMobileGrid.Label>
|
||||
<ListItemMobileGrid.Value>
|
||||
<Skeleton isLoaded={ !isLoading } color="text_secondary" display="inline-block">
|
||||
<span>{ dayjs(timestamp).fromNow() }</span>
|
||||
</Skeleton>
|
||||
</ListItemMobileGrid.Value>
|
||||
|
||||
{ fromAddress && (
|
||||
<>
|
||||
<ListItemMobileGrid.Label isLoading={ isLoading }>From</ListItemMobileGrid.Label>
|
||||
<ListItemMobileGrid.Value>
|
||||
<AddressEntity address={ fromAddress } isLoading={ isLoading }/>
|
||||
</ListItemMobileGrid.Value>
|
||||
</>
|
||||
) }
|
||||
|
||||
{ action && (
|
||||
<>
|
||||
<ListItemMobileGrid.Label isLoading={ isLoading }>Method</ListItemMobileGrid.Label>
|
||||
<ListItemMobileGrid.Value>
|
||||
<Tag colorScheme="gray" isLoading={ isLoading }>{ action }</Tag>
|
||||
</ListItemMobileGrid.Value>
|
||||
</>
|
||||
) }
|
||||
</ListItemMobileGrid.Container>
|
||||
);
|
||||
};
|
||||
|
||||
export default React.memo(NameDomainHistoryListItem);
|
||||
@@ -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 (
|
||||
<Table variant="simple" size="sm">
|
||||
<Thead top={ 0 }>
|
||||
<Tr>
|
||||
<Th width="25%">Txn hash</Th>
|
||||
<Th width="25%" pl={ 9 }>
|
||||
<Link display="flex" alignItems="center" justifyContent="flex-start" position="relative" data-field="timestamp" onClick={ onSortToggle }>
|
||||
{ sort?.includes('timestamp') && (
|
||||
<IconSvg
|
||||
name="arrows/east"
|
||||
boxSize={ 4 }
|
||||
transform={ sortIconTransform }
|
||||
color="link"
|
||||
position="absolute"
|
||||
left={ -5 }
|
||||
top={ 0 }
|
||||
/>
|
||||
) }
|
||||
<span>Age</span>
|
||||
</Link>
|
||||
</Th>
|
||||
<Th width="25%">From</Th>
|
||||
<Th width="25%">Method</Th>
|
||||
</Tr>
|
||||
</Thead>
|
||||
<Tbody>
|
||||
{
|
||||
data?.items
|
||||
.slice()
|
||||
.sort(sortFn(sort))
|
||||
.map((item, index) => <NameDomainHistoryTableItem key={ index } { ...item } isLoading={ isLoading }/>)
|
||||
}
|
||||
</Tbody>
|
||||
</Table>
|
||||
);
|
||||
};
|
||||
|
||||
export default React.memo(NameDomainHistoryTable);
|
||||
@@ -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 (
|
||||
<Tr>
|
||||
<Td verticalAlign="middle">
|
||||
<TxEntity hash={ transactionHash } isLoading={ isLoading } fontWeight={ 700 }/>
|
||||
</Td>
|
||||
<Td pl={ 9 } verticalAlign="middle">
|
||||
<Skeleton isLoaded={ !isLoading } color="text_secondary" display="inline-block">
|
||||
<span>{ dayjs(timestamp).fromNow() }</span>
|
||||
</Skeleton>
|
||||
</Td>
|
||||
<Td verticalAlign="middle">
|
||||
{ fromAddress && <AddressEntity address={ fromAddress } isLoading={ isLoading }/> }
|
||||
</Td>
|
||||
<Td verticalAlign="middle">
|
||||
{ action && <Tag colorScheme="gray" isLoading={ isLoading }>{ action }</Tag> }
|
||||
</Td>
|
||||
</Tr>
|
||||
);
|
||||
};
|
||||
|
||||
export default React.memo(NameDomainHistoryTableItem);
|
||||
@@ -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<SortField, Array<Sort | undefined>> = {
|
||||
timestamp: [ 'timestamp-desc', 'timestamp-asc', undefined ],
|
||||
};
|
||||
|
||||
export const getNextSortValue = (getNextSortValueShared<SortField, Sort>).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;
|
||||
}
|
||||
};
|
||||
@@ -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 = (
|
||||
<FilterInput
|
||||
w={{ base: '100%', lg: '360px' }}
|
||||
minW={{ base: 'auto', lg: '250px' }}
|
||||
size="xs"
|
||||
onChange={ onSearchChange }
|
||||
placeholder="Search by name"
|
||||
initialValue={ searchTerm }
|
||||
isLoading={ isInitialLoading }
|
||||
/>
|
||||
);
|
||||
|
||||
const filter = (
|
||||
<PopoverFilter appliedFiltersNum={ filterValue.length } contentProps={{ w: '220px' }} isLoading={ isInitialLoading }>
|
||||
<div>
|
||||
<CheckboxGroup size="lg" onChange={ onFilterValueChange } value={ filterValue } defaultValue={ filterValue }>
|
||||
<Text variant="secondary" fontWeight={ 600 } mb={ 3 } fontSize="sm">Address</Text>
|
||||
<Checkbox value="owned_by" display="block" isDisabled={ !isAddressSearch }>
|
||||
Owned by
|
||||
</Checkbox>
|
||||
<Checkbox
|
||||
value="resolved_to"
|
||||
display="block"
|
||||
mt={ 5 }
|
||||
mb={ 4 }
|
||||
pb={ 4 }
|
||||
borderBottom="1px solid"
|
||||
borderColor="divider"
|
||||
isDisabled={ !isAddressSearch }
|
||||
>
|
||||
Resolved to address
|
||||
</Checkbox>
|
||||
<Text variant="secondary" fontWeight={ 600 } mb={ 3 } fontSize="sm">Status</Text>
|
||||
<Checkbox value="with_inactive" display="block">
|
||||
Include expired
|
||||
</Checkbox>
|
||||
</CheckboxGroup>
|
||||
</div>
|
||||
</PopoverFilter>
|
||||
);
|
||||
|
||||
const sortButton = (
|
||||
<Sort
|
||||
options={ SORT_OPTIONS }
|
||||
sort={ sort }
|
||||
setSort={ onSortChange }
|
||||
isLoading={ isInitialLoading }
|
||||
/>
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
<HStack spacing={ 3 } mb={ 6 } display={{ base: 'flex', lg: 'none' }}>
|
||||
{ filter }
|
||||
{ sortButton }
|
||||
{ searchInput }
|
||||
</HStack>
|
||||
<ActionBar
|
||||
mt={ -6 }
|
||||
display={{ base: pagination.isVisible ? 'flex' : 'none', lg: 'flex' }}
|
||||
>
|
||||
<HStack spacing={ 3 } display={{ base: 'none', lg: 'flex' }}>
|
||||
{ filter }
|
||||
{ searchInput }
|
||||
</HStack>
|
||||
<Pagination { ...pagination } ml="auto"/>
|
||||
</ActionBar>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default React.memo(NameDomainsActionBar);
|
||||
@@ -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 (
|
||||
<ListItemMobileGrid.Container>
|
||||
<ListItemMobileGrid.Label isLoading={ isLoading }>Domain</ListItemMobileGrid.Label>
|
||||
<ListItemMobileGrid.Value>
|
||||
<EnsEntity name={ name } isLoading={ isLoading } fontWeight={ 500 }/>
|
||||
</ListItemMobileGrid.Value>
|
||||
|
||||
{ resolvedAddress && (
|
||||
<>
|
||||
<ListItemMobileGrid.Label isLoading={ isLoading }>Address</ListItemMobileGrid.Label>
|
||||
<ListItemMobileGrid.Value>
|
||||
<AddressEntity address={ resolvedAddress } isLoading={ isLoading } fontWeight={ 500 }/>
|
||||
</ListItemMobileGrid.Value>
|
||||
</>
|
||||
) }
|
||||
|
||||
{ registrationDate && (
|
||||
<>
|
||||
<ListItemMobileGrid.Label isLoading={ isLoading }>Registered on</ListItemMobileGrid.Label>
|
||||
<ListItemMobileGrid.Value>
|
||||
<Skeleton isLoaded={ !isLoading }>
|
||||
<div>{ dayjs(registrationDate).format('MMM DD YYYY HH:mm:ss A') }</div>
|
||||
<div> { dayjs(registrationDate).fromNow() }</div>
|
||||
</Skeleton>
|
||||
</ListItemMobileGrid.Value>
|
||||
</>
|
||||
) }
|
||||
|
||||
{ expiryDate && (
|
||||
<>
|
||||
<ListItemMobileGrid.Label isLoading={ isLoading }>Expiration date</ListItemMobileGrid.Label>
|
||||
<ListItemMobileGrid.Value>
|
||||
<Skeleton isLoaded={ !isLoading } whiteSpace="pre-wrap">
|
||||
<div>{ dayjs(expiryDate).format('MMM DD YYYY HH:mm:ss A') } </div>
|
||||
<NameDomainExpiryStatus date={ expiryDate }/>
|
||||
</Skeleton>
|
||||
</ListItemMobileGrid.Value>
|
||||
</>
|
||||
) }
|
||||
</ListItemMobileGrid.Container>
|
||||
);
|
||||
};
|
||||
|
||||
export default React.memo(NameDomainsListItem);
|
||||
@@ -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 (
|
||||
<Table variant="simple" size="sm">
|
||||
<Thead top={ 80 }>
|
||||
<Tr>
|
||||
<Th width="25%">Domain</Th>
|
||||
<Th width="25%">Address</Th>
|
||||
<Th width="25%" pl={ 9 }>
|
||||
<Link display="flex" alignItems="center" justifyContent="flex-start" position="relative" data-field="registration_date" onClick={ onSortToggle }>
|
||||
{ sort?.includes('registration_date') && (
|
||||
<IconSvg
|
||||
name="arrows/east"
|
||||
boxSize={ 4 }
|
||||
transform={ sortIconTransform }
|
||||
color="link"
|
||||
position="absolute"
|
||||
left={ -5 }
|
||||
top={ 0 }
|
||||
/>
|
||||
) }
|
||||
<span>Registered on</span>
|
||||
</Link>
|
||||
</Th>
|
||||
<Th width="25%">Expiration date</Th>
|
||||
</Tr>
|
||||
</Thead>
|
||||
<Tbody>
|
||||
{ data?.items.map((item, index) => <NameDomainsTableItem key={ index } { ...item } isLoading={ isLoading }/>) }
|
||||
</Tbody>
|
||||
</Table>
|
||||
);
|
||||
};
|
||||
|
||||
export default React.memo(NameDomainsTable);
|
||||
@@ -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 (
|
||||
<Tr>
|
||||
<Td verticalAlign="middle">
|
||||
<EnsEntity name={ name } isLoading={ isLoading } fontWeight={ 600 }/>
|
||||
</Td>
|
||||
<Td verticalAlign="middle">
|
||||
{ resolvedAddress && <AddressEntity address={ resolvedAddress } isLoading={ isLoading } fontWeight={ 500 }/> }
|
||||
</Td>
|
||||
<Td verticalAlign="middle" pl={ 9 }>
|
||||
{ registrationDate && (
|
||||
<Skeleton isLoaded={ !isLoading }>
|
||||
{ dayjs(registrationDate).format('MMM DD YYYY HH:mm:ss A') }
|
||||
<chakra.span color="text_secondary"> { dayjs(registrationDate).fromNow() }</chakra.span>
|
||||
</Skeleton>
|
||||
) }
|
||||
</Td>
|
||||
<Td verticalAlign="middle">
|
||||
{ expiryDate && (
|
||||
<Skeleton isLoaded={ !isLoading } whiteSpace="pre-wrap">
|
||||
<span>{ dayjs(expiryDate).format('MMM DD YYYY HH:mm:ss A') } </span>
|
||||
<NameDomainExpiryStatus date={ expiryDate }/>
|
||||
</Skeleton>
|
||||
) }
|
||||
</Td>
|
||||
</Tr>
|
||||
);
|
||||
};
|
||||
|
||||
export default React.memo(NameDomainsTableItem);
|
||||
@@ -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<Option<Sort>> = [
|
||||
{ title: 'Default', id: undefined },
|
||||
{ title: 'Registered on descending', id: 'registration_date-DESC' },
|
||||
{ title: 'Registered on ascending', id: 'registration_date-ASC' },
|
||||
];
|
||||
|
||||
const SORT_SEQUENCE: Record<SortField, Array<Sort | undefined>> = {
|
||||
registration_date: [ 'registration_date-DESC', 'registration_date-ASC', undefined ],
|
||||
};
|
||||
|
||||
export const getNextSortValue = (getNextSortValueShared<SortField, Sort>).bind(undefined, SORT_SEQUENCE);
|
||||
@@ -21,6 +21,7 @@ const addresses: AddressesResponse = {
|
||||
...addressMocks.token,
|
||||
tx_count: '109123890123',
|
||||
coin_balance: '22222345678901234567890000',
|
||||
ens_domain_name: null,
|
||||
}, {
|
||||
...addressMocks.withoutName,
|
||||
tx_count: '11',
|
||||
|
||||
@@ -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 = (
|
||||
<Flex alignItems="center" w="100%" columnGap={ 2 } rowGap={ 2 } flexWrap={{ base: 'wrap', lg: 'nowrap' }}>
|
||||
{ addressQuery.data?.ens_domain_name && (
|
||||
<EnsEntity
|
||||
name={ addressQuery.data?.ens_domain_name }
|
||||
fontFamily="heading"
|
||||
fontSize="lg"
|
||||
fontWeight={ 500 }
|
||||
mr={ 1 }
|
||||
maxW="300px"
|
||||
/>
|
||||
) }
|
||||
<AddressEntity
|
||||
address={{ ...addressQuery.data, hash, name: '' }}
|
||||
address={{ ...addressQuery.data, hash, name: '', ens_domain_name: '' }}
|
||||
isLoading={ isLoading }
|
||||
fontFamily="heading"
|
||||
fontSize="lg"
|
||||
fontWeight={ 500 }
|
||||
noLink
|
||||
isSafeAddress={ isSafeAddress }
|
||||
mr={ 4 }
|
||||
/>
|
||||
{ !isLoading && addressQuery.data?.is_contract && addressQuery.data.token &&
|
||||
<AddressAddToWallet token={ addressQuery.data.token } variant="button"/> }
|
||||
@@ -190,6 +203,8 @@ const AddressPageContent = () => {
|
||||
<AccountActionsMenu isLoading={ isLoading }/>
|
||||
<HStack ml="auto" gap={ 2 }/>
|
||||
{ addressQuery.data?.is_contract && addressQuery.data?.is_verified && config.UI.views.address.solidityscanEnabled && <SolidityscanReport hash={ hash }/> }
|
||||
{ !isLoading && addressQuery.data && config.features.nameService.isEnabled &&
|
||||
<AddressEnsDomains addressHash={ hash } mainDomainName={ addressQuery.data.ens_domain_name }/> }
|
||||
<NetworkExplorers type="address" pathParam={ hash }/>
|
||||
</Flex>
|
||||
);
|
||||
|
||||
@@ -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(
|
||||
<TestApp>
|
||||
<NameDomain/>
|
||||
</TestApp>,
|
||||
{ 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(
|
||||
<TestApp>
|
||||
<NameDomain/>
|
||||
</TestApp>,
|
||||
{ hooksConfig: {
|
||||
router: {
|
||||
query: { name: ensDomainMock.ensDomainA.name, tab: 'history' },
|
||||
isReady: true,
|
||||
},
|
||||
} },
|
||||
);
|
||||
|
||||
await expect(component).toHaveScreenshot();
|
||||
});
|
||||
@@ -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<RoutedTab> = [
|
||||
{ id: 'details', title: 'Details', component: <NameDomainDetails query={ infoQuery }/> },
|
||||
{ id: 'history', title: 'History', component: <NameDomainHistory/> },
|
||||
];
|
||||
|
||||
const tabIndex = useTabIndexFromQuery(tabs);
|
||||
|
||||
if (infoQuery.isError) {
|
||||
throw new Error(undefined, { cause: infoQuery.error });
|
||||
}
|
||||
|
||||
const isLoading = infoQuery.isPlaceholderData;
|
||||
|
||||
const titleSecondRow = (
|
||||
<Flex columnGap={ 3 } rowGap={ 3 } fontFamily="heading" fontSize="lg" fontWeight={ 500 } alignItems="center" w="100%">
|
||||
<EnsEntity
|
||||
name={ domainName }
|
||||
isLoading={ isLoading }
|
||||
noLink
|
||||
maxW={ infoQuery.data?.resolved_address ? '300px' : 'min-content' }
|
||||
/>
|
||||
{ infoQuery.data?.resolved_address && (
|
||||
<AddressEntity
|
||||
address={ infoQuery.data?.resolved_address }
|
||||
isLoading={ isLoading }
|
||||
truncation={ isMobile ? 'constant' : 'dynamic' }
|
||||
flexShrink={ 0 }
|
||||
/>
|
||||
) }
|
||||
{ infoQuery.data?.resolved_address && (
|
||||
<Tooltip label="Lookup for related domain names">
|
||||
<LinkInternal
|
||||
flexShrink={ 0 }
|
||||
display="inline-flex"
|
||||
href={ route({ pathname: '/name-domains', query: { owned_by: 'true', resolved_to: 'true', address: infoQuery.data?.resolved_address?.hash } }) }
|
||||
>
|
||||
<IconSvg name="search" boxSize={ 5 } isLoading={ isLoading }/>
|
||||
</LinkInternal>
|
||||
</Tooltip>
|
||||
) }
|
||||
</Flex>
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
<TextAd mb={ 6 }/>
|
||||
<PageTitle title="ENS Domain details" secondRow={ titleSecondRow }/>
|
||||
{ infoQuery.isPlaceholderData ? (
|
||||
<>
|
||||
<TabsSkeleton tabs={ tabs } mt={ 6 }/>
|
||||
{ tabs[tabIndex]?.component }
|
||||
</>
|
||||
) : <RoutedTabs tabs={ tabs }/> }
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default NameDomain;
|
||||
@@ -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: '<token-id>',
|
||||
},
|
||||
}),
|
||||
}));
|
||||
|
||||
const component = await mount(
|
||||
<TestApp>
|
||||
<NameDomains/>
|
||||
</TestApp>,
|
||||
);
|
||||
|
||||
await expect(component).toHaveScreenshot();
|
||||
});
|
||||
@@ -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<Sort>(router.query, SORT_OPTIONS);
|
||||
|
||||
const [ searchTerm, setSearchTerm ] = React.useState<string>(q || '');
|
||||
const [ filterValue, setFilterValue ] = React.useState<EnsDomainLookupFiltersOptions>(initialFilters);
|
||||
const [ sort, setSort ] = React.useState<Sort | undefined>(initialSort);
|
||||
|
||||
const debouncedSearchTerm = useDebounce(searchTerm, 300);
|
||||
const isAddressSearch = React.useMemo(() => ADDRESS_REGEXP.test(debouncedSearchTerm), [ debouncedSearchTerm ]);
|
||||
const sortParams = getSortParamsFromValue<Sort, EnsLookupSorting['sort'], EnsLookupSorting['order']>(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 = (
|
||||
<>
|
||||
<Show below="lg" ssr={ false }>
|
||||
<Box>
|
||||
{ data?.items.map((item, index) => (
|
||||
<NameDomainsListItem
|
||||
key={ item.id + (isLoading ? index : '') }
|
||||
{ ...item }
|
||||
isLoading={ isLoading }
|
||||
/>
|
||||
)) }
|
||||
</Box>
|
||||
</Show>
|
||||
<Hide below="lg" ssr={ false }>
|
||||
<NameDomainsTable
|
||||
data={ data }
|
||||
isLoading={ isLoading }
|
||||
sort={ sort }
|
||||
onSortToggle={ handleSortToggle }
|
||||
/>
|
||||
</Hide>
|
||||
</>
|
||||
);
|
||||
|
||||
const actionBar = (
|
||||
<NameDomainsActionBar
|
||||
isLoading={ isLoading }
|
||||
searchTerm={ searchTerm }
|
||||
onSearchChange={ handleSearchTermChange }
|
||||
filterValue={ filterValue }
|
||||
onFilterValueChange={ handleFilterValueChange }
|
||||
sort={ sort }
|
||||
onSortChange={ setSort }
|
||||
isAddressSearch={ isAddressSearch }
|
||||
pagination={ query.pagination }
|
||||
/>
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
<PageTitle title="ENS domains lookup" withTextAd/>
|
||||
<DataListDisplay
|
||||
isError={ isError }
|
||||
items={ data?.items }
|
||||
emptyText="There are no name domains."
|
||||
filterProps={{
|
||||
emptyFilteredText: `Couldn${ apos }t find name domains that match your filter query.`,
|
||||
hasActiveFilters,
|
||||
}}
|
||||
content={ content }
|
||||
actionBar={ actionBar }
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default NameDomains;
|
||||
@@ -32,6 +32,7 @@ test.describe('search by name ', () => {
|
||||
searchMock.token1,
|
||||
searchMock.token2,
|
||||
searchMock.contract1,
|
||||
searchMock.address2,
|
||||
searchMock.label1,
|
||||
],
|
||||
}),
|
||||
|
||||
|
Before Width: | Height: | Size: 238 KiB After Width: | Height: | Size: 235 KiB |
|
Before Width: | Height: | Size: 270 KiB After Width: | Height: | Size: 262 KiB |
|
Before Width: | Height: | Size: 137 KiB After Width: | Height: | Size: 137 KiB |
|
After Width: | Height: | Size: 86 KiB |
|
After Width: | Height: | Size: 48 KiB |
|
After Width: | Height: | Size: 44 KiB |
|
After Width: | Height: | Size: 63 KiB |
|
After Width: | Height: | Size: 73 KiB |
|
Before Width: | Height: | Size: 41 KiB After Width: | Height: | Size: 48 KiB |
|
Before Width: | Height: | Size: 41 KiB After Width: | Height: | Size: 48 KiB |
|
Before Width: | Height: | Size: 34 KiB After Width: | Height: | Size: 40 KiB |
@@ -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 ? <span dangerouslySetInnerHTML={{ __html: shouldHighlightHash ? xss(data.name) : highlightText(data.name, searchTerm) }}/> : 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 ? (
|
||||
<>
|
||||
<span dangerouslySetInnerHTML={{ __html: shouldHighlightHash ? xss(addressName) : highlightText(addressName, searchTerm) }}/>
|
||||
{ data.ens_info &&
|
||||
(
|
||||
data.ens_info.names_count > 1 ?
|
||||
<chakra.span color="text_secondary"> ({ data.ens_info.names_count > 39 ? '40+' : `+${ data.ens_info.names_count - 1 }` })</chakra.span> :
|
||||
<chakra.span color="text_secondary">{ expiresText }</chakra.span>
|
||||
)
|
||||
}
|
||||
</>
|
||||
) :
|
||||
null;
|
||||
}
|
||||
|
||||
default:
|
||||
|
||||
@@ -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 (
|
||||
<>
|
||||
<Td fontSize="sm">
|
||||
<AddressEntity.Container>
|
||||
<AddressEntity.Icon address={ address }/>
|
||||
<AddressEntity.Link
|
||||
address={ address }
|
||||
onClick={ handleLinkClick }
|
||||
>
|
||||
<AddressEntity.Content
|
||||
asProp={ shouldHighlightHash ? 'mark' : 'span' }
|
||||
address={ address }
|
||||
fontSize="sm"
|
||||
lineHeight={ 5 }
|
||||
fontWeight={ 700 }
|
||||
/>
|
||||
</AddressEntity.Link>
|
||||
<AddressEntity.Copy address={ address }/>
|
||||
</AddressEntity.Container>
|
||||
</Td>
|
||||
<Td colSpan={ 2 } fontSize="sm" verticalAlign="middle">
|
||||
<span dangerouslySetInnerHTML={{ __html: shouldHighlightHash ? xss(data.name) : highlightText(data.name, searchTerm) }}/>
|
||||
</Td>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
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 (
|
||||
<Td colSpan={ 3 } fontSize="sm">
|
||||
<AddressEntity.Container>
|
||||
<AddressEntity.Icon address={ address }/>
|
||||
<AddressEntity.Link
|
||||
address={ address }
|
||||
onClick={ handleLinkClick }
|
||||
>
|
||||
<AddressEntity.Content
|
||||
asProp="mark"
|
||||
<>
|
||||
<Td fontSize="sm" colSpan={ addressName ? 1 : 3 }>
|
||||
<AddressEntity.Container>
|
||||
<AddressEntity.Icon address={ address }/>
|
||||
<AddressEntity.Link
|
||||
address={ address }
|
||||
fontSize="sm"
|
||||
lineHeight={ 5 }
|
||||
fontWeight={ 700 }
|
||||
/>
|
||||
</AddressEntity.Link>
|
||||
<AddressEntity.Copy address={ address }/>
|
||||
</AddressEntity.Container>
|
||||
</Td>
|
||||
onClick={ handleLinkClick }
|
||||
>
|
||||
<AddressEntity.Content
|
||||
asProp={ shouldHighlightHash ? 'mark' : 'span' }
|
||||
address={ address }
|
||||
fontSize="sm"
|
||||
lineHeight={ 5 }
|
||||
fontWeight={ 700 }
|
||||
/>
|
||||
</AddressEntity.Link>
|
||||
<AddressEntity.Copy address={ address }/>
|
||||
</AddressEntity.Container>
|
||||
</Td>
|
||||
{ addressName && (
|
||||
<Td colSpan={ 2 } fontSize="sm" verticalAlign="middle">
|
||||
<span dangerouslySetInnerHTML={{ __html: shouldHighlightHash ? xss(addressName) : highlightText(addressName, searchTerm) }}/>
|
||||
{ data.ens_info &&
|
||||
(
|
||||
data.ens_info.names_count > 1 ?
|
||||
<chakra.span color="text_secondary"> ({ data.ens_info.names_count > 39 ? '40+' : `+${ data.ens_info.names_count - 1 }` })</chakra.span> :
|
||||
<chakra.span color="text_secondary">{ expiresText }</chakra.span>
|
||||
)
|
||||
}
|
||||
</Td>
|
||||
) }
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
|
Before Width: | Height: | Size: 101 KiB After Width: | Height: | Size: 97 KiB |
|
Before Width: | Height: | Size: 81 KiB After Width: | Height: | Size: 77 KiB |
|
Before Width: | Height: | Size: 98 KiB After Width: | Height: | Size: 95 KiB |
|
Before Width: | Height: | Size: 78 KiB After Width: | Height: | Size: 71 KiB |
@@ -80,6 +80,19 @@ test.describe('loading', () => {
|
||||
|
||||
await expect(component).toHaveScreenshot();
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
test('with ENS', async({ mount }) => {
|
||||
const component = await mount(
|
||||
<TestApp>
|
||||
<AddressEntity
|
||||
address={ addressMock.withEns }
|
||||
/>
|
||||
</TestApp>,
|
||||
);
|
||||
|
||||
await expect(component).toHaveScreenshot();
|
||||
});
|
||||
|
||||
test('external link', async({ mount }) => {
|
||||
|
||||
@@ -98,10 +98,11 @@ const Icon = (props: IconProps) => {
|
||||
type ContentProps = Omit<EntityBase.ContentBaseProps, 'text'> & Pick<EntityProps, 'address'>;
|
||||
|
||||
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 = (
|
||||
<VStack gap={ 0 } py={ 1 } color="inherit">
|
||||
<Box fontWeight={ 600 } whiteSpace="pre-wrap" wordBreak="break-word">{ props.address.name }</Box>
|
||||
<Box fontWeight={ 600 } whiteSpace="pre-wrap" wordBreak="break-word">{ text }</Box>
|
||||
<Box whiteSpace="pre-wrap" wordBreak="break-word">{ props.address.hash }</Box>
|
||||
</VStack>
|
||||
);
|
||||
@@ -109,7 +110,7 @@ const Content = chakra((props: ContentProps) => {
|
||||
return (
|
||||
<Tooltip label={ label } maxW="100vw">
|
||||
<Skeleton isLoaded={ !props.isLoading } overflow="hidden" textOverflow="ellipsis" whiteSpace="nowrap" as="span">
|
||||
{ props.address.name }
|
||||
{ text }
|
||||
</Skeleton>
|
||||
</Tooltip>
|
||||
);
|
||||
@@ -137,7 +138,7 @@ const Copy = (props: CopyProps) => {
|
||||
const Container = EntityBase.Container;
|
||||
|
||||
export interface EntityProps extends EntityBase.EntityBaseProps {
|
||||
address: Pick<AddressParam, 'hash' | 'name' | 'is_contract' | 'is_verified' | 'implementation_name'>;
|
||||
address: Pick<AddressParam, 'hash' | 'name' | 'is_contract' | 'is_verified' | 'implementation_name' | 'ens_domain_name'>;
|
||||
isSafeAddress?: boolean;
|
||||
}
|
||||
|
||||
|
||||
|
After Width: | Height: | Size: 1.7 KiB |
@@ -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(
|
||||
<TestApp>
|
||||
<EnsEntity
|
||||
name={ name }
|
||||
iconSize={ size }
|
||||
/>
|
||||
</TestApp>,
|
||||
);
|
||||
|
||||
await expect(component).toHaveScreenshot();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
test('loading', async({ mount }) => {
|
||||
const component = await mount(
|
||||
<TestApp>
|
||||
<EnsEntity
|
||||
name={ name }
|
||||
isLoading
|
||||
/>
|
||||
</TestApp>,
|
||||
);
|
||||
|
||||
await expect(component).toHaveScreenshot();
|
||||
});
|
||||
|
||||
test('with long name', async({ mount }) => {
|
||||
const component = await mount(
|
||||
<TestApp>
|
||||
<EnsEntity
|
||||
name="kitty.kitty.kitty.cat.eth"
|
||||
/>
|
||||
</TestApp>,
|
||||
);
|
||||
|
||||
await component.getByText(name.slice(0, 4)).hover();
|
||||
|
||||
await expect(component).toHaveScreenshot();
|
||||
});
|
||||
|
||||
test('customization', async({ mount }) => {
|
||||
const component = await mount(
|
||||
<TestApp>
|
||||
<EnsEntity
|
||||
name={ name }
|
||||
p={ 3 }
|
||||
borderWidth="1px"
|
||||
borderColor="blue.700"
|
||||
/>
|
||||
</TestApp>,
|
||||
);
|
||||
|
||||
await expect(component).toHaveScreenshot();
|
||||
});
|
||||