Commit eb3459d4 authored by tom's avatar tom

address domains info

parent d611f2e2
...@@ -10,6 +10,7 @@ export { default as googleAnalytics } from './googleAnalytics'; ...@@ -10,6 +10,7 @@ export { default as googleAnalytics } from './googleAnalytics';
export { default as graphqlApiDocs } from './graphqlApiDocs'; export { default as graphqlApiDocs } from './graphqlApiDocs';
export { default as marketplace } from './marketplace'; export { default as marketplace } from './marketplace';
export { default as mixpanel } from './mixpanel'; export { default as mixpanel } from './mixpanel';
export { default as nameService } from './nameService';
export { default as restApiDocs } from './restApiDocs'; export { default as restApiDocs } from './restApiDocs';
export { default as optimisticRollup } from './optimisticRollup'; export { default as optimisticRollup } from './optimisticRollup';
export { default as safe } from './safe'; export { default as safe } from './safe';
......
import type { Feature } from './types';
import { getEnvValue } from '../utils';
const apiHost = getEnvValue('NEXT_PUBLIC_NAME_SERVICE_API_HOST');
const title = 'Name service integration';
const config: Feature<{ api: { endpoint: string; basePath: string } }> = (() => {
if (apiHost) {
return Object.freeze({
title,
isEnabled: true,
api: {
endpoint: apiHost,
basePath: '',
},
});
}
return Object.freeze({
title,
isEnabled: false,
});
})();
export default config;
...@@ -46,6 +46,7 @@ NEXT_PUBLIC_STATS_API_HOST=https://stats-goerli.k8s-dev.blockscout.com ...@@ -46,6 +46,7 @@ NEXT_PUBLIC_STATS_API_HOST=https://stats-goerli.k8s-dev.blockscout.com
NEXT_PUBLIC_VISUALIZE_API_HOST=https://visualizer.services.blockscout.com NEXT_PUBLIC_VISUALIZE_API_HOST=https://visualizer.services.blockscout.com
NEXT_PUBLIC_CONTRACT_INFO_API_HOST=https://contracts-info.services.blockscout.com NEXT_PUBLIC_CONTRACT_INFO_API_HOST=https://contracts-info.services.blockscout.com
NEXT_PUBLIC_ADMIN_SERVICE_API_HOST=https://admin-rs.services.blockscout.com NEXT_PUBLIC_ADMIN_SERVICE_API_HOST=https://admin-rs.services.blockscout.com
NEXT_PUBLIC_NAME_SERVICE_API_HOST=https://bens-rs-test.k8s-dev.blockscout.com
NEXT_PUBLIC_WEB3_WALLETS=['token_pocket','metamask'] NEXT_PUBLIC_WEB3_WALLETS=['token_pocket','metamask']
NEXT_PUBLIC_VIEWS_CONTRACT_SOLIDITYSCAN_ENABLED='true' NEXT_PUBLIC_VIEWS_CONTRACT_SOLIDITYSCAN_ENABLED='true'
......
...@@ -424,6 +424,7 @@ const schema = yup ...@@ -424,6 +424,7 @@ const schema = yup
NEXT_PUBLIC_STATS_API_HOST: yup.string().test(urlTest), NEXT_PUBLIC_STATS_API_HOST: yup.string().test(urlTest),
NEXT_PUBLIC_VISUALIZE_API_HOST: yup.string().test(urlTest), NEXT_PUBLIC_VISUALIZE_API_HOST: yup.string().test(urlTest),
NEXT_PUBLIC_CONTRACT_INFO_API_HOST: yup.string().test(urlTest), NEXT_PUBLIC_CONTRACT_INFO_API_HOST: yup.string().test(urlTest),
NEXT_PUBLIC_NAME_SERVICE_API_HOST: yup.string().test(urlTest),
NEXT_PUBLIC_GRAPHIQL_TRANSACTION: yup.string().matches(regexp.HEX_REGEXP), NEXT_PUBLIC_GRAPHIQL_TRANSACTION: yup.string().matches(regexp.HEX_REGEXP),
NEXT_PUBLIC_WEB3_WALLETS: yup NEXT_PUBLIC_WEB3_WALLETS: yup
.mixed() .mixed()
......
...@@ -45,6 +45,7 @@ Please be aware that all environment variables prefixed with `NEXT_PUBLIC_` will ...@@ -45,6 +45,7 @@ Please be aware that all environment variables prefixed with `NEXT_PUBLIC_` will
- [Blockchain statistics](ENVS.md#blockchain-statistics) - [Blockchain statistics](ENVS.md#blockchain-statistics)
- [Web3 wallet integration](ENVS.md#web3-wallet-integration-add-token-or-network-to-the-wallet) (add token or network to the wallet) - [Web3 wallet integration](ENVS.md#web3-wallet-integration-add-token-or-network-to-the-wallet) (add token or network to the wallet)
- [Verified tokens info](ENVS.md#verified-tokens-info) - [Verified tokens info](ENVS.md#verified-tokens-info)
- [Name service integration](ENVS.md#name-service-integration)
- [Bridged tokens](ENVS.md#bridged-tokens) - [Bridged tokens](ENVS.md#bridged-tokens)
- [Safe{Core} address tags](ENVS.md#safecore-address-tags) - [Safe{Core} address tags](ENVS.md#safecore-address-tags)
- [SUAVE chain](ENVS.md#suave-chain) - [SUAVE chain](ENVS.md#suave-chain)
...@@ -481,6 +482,16 @@ This feature is **enabled by default** with the `['metamask']` value. To switch ...@@ -481,6 +482,16 @@ This feature is **enabled by default** with the `['metamask']` value. To switch
&nbsp; &nbsp;
### Name service integration
This feature allows resolving blockchain addresses using human-readable domain names.
| Variable | Type| Description | Compulsoriness | Default value | Example value |
| --- | --- | --- | --- | --- | --- |
| NEXT_PUBLIC_NAME_SERVICE_API_HOST | `string` | Name Service API endpoint url | Required | - | `https://bens.services.blockscout.com` |
&nbsp;
### Bridged tokens ### Bridged tokens
This feature allows users to view tokens that have been bridged from other EVM chains. Additional tab "Bridged" will be added to the tokens page and the link to original token will be displayed on the token page. This feature allows users to view tokens that have been bridged from other EVM chains. Additional tab "Bridged" will be added to the tokens page and the link to original token will be displayed on the token page.
......
...@@ -36,6 +36,7 @@ import type { ChartMarketResponse, ChartTransactionResponse } from 'types/api/ch ...@@ -36,6 +36,7 @@ import type { ChartMarketResponse, ChartTransactionResponse } from 'types/api/ch
import type { BackendVersionConfig } from 'types/api/configs'; import type { BackendVersionConfig } from 'types/api/configs';
import type { SmartContract, SmartContractReadMethod, SmartContractWriteMethod, SmartContractVerificationConfig, SolidityscanReport } from 'types/api/contract'; import type { SmartContract, SmartContractReadMethod, SmartContractWriteMethod, SmartContractVerificationConfig, SolidityscanReport } from 'types/api/contract';
import type { VerifiedContractsResponse, VerifiedContractsFilters, VerifiedContractsCounters } from 'types/api/contracts'; import type { VerifiedContractsResponse, VerifiedContractsFilters, VerifiedContractsCounters } from 'types/api/contracts';
import type { EnsAddressLookupResponse, EnsDomainDetailed, EnsDomainEventsResponse, EnsDomainLookupResponse } from 'types/api/ens';
import type { IndexingStatus } from 'types/api/indexingStatus'; import type { IndexingStatus } from 'types/api/indexingStatus';
import type { InternalTransactionsResponse } from 'types/api/internalTransaction'; import type { InternalTransactionsResponse } from 'types/api/internalTransaction';
import type { L2DepositsResponse, L2DepositsItem } from 'types/api/l2Deposits'; import type { L2DepositsResponse, L2DepositsItem } from 'types/api/l2Deposits';
...@@ -175,6 +176,32 @@ export const RESOURCES = { ...@@ -175,6 +176,32 @@ export const RESOURCES = {
basePath: getFeaturePayload(config.features.stats)?.api.basePath, basePath: getFeaturePayload(config.features.stats)?.api.basePath,
}, },
// NAME SERVICE
addresses_lookup: {
path: '/api/v1/:chainId/addresses\\:lookup',
pathParams: [ 'chainId' as const ],
endpoint: getFeaturePayload(config.features.nameService)?.api.endpoint,
basePath: getFeaturePayload(config.features.nameService)?.api.basePath,
},
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,
},
// VISUALIZATION // VISUALIZATION
visualize_sol2uml: { visualize_sol2uml: {
path: '/api/v1/solidity\\:visualize-contracts', path: '/api/v1/solidity\\:visualize-contracts',
...@@ -706,6 +733,10 @@ Q extends 'zkevm_l2_txn_batches_count' ? number : ...@@ -706,6 +733,10 @@ Q extends 'zkevm_l2_txn_batches_count' ? number :
Q extends 'zkevm_l2_txn_batch' ? ZkEvmL2TxnBatch : Q extends 'zkevm_l2_txn_batch' ? ZkEvmL2TxnBatch :
Q extends 'zkevm_l2_txn_batch_txs' ? ZkEvmL2TxnBatchTxs : Q extends 'zkevm_l2_txn_batch_txs' ? ZkEvmL2TxnBatchTxs :
Q extends 'config_backend_version' ? BackendVersionConfig : Q extends 'config_backend_version' ? BackendVersionConfig :
Q extends 'addresses_lookup' ? EnsAddressLookupResponse :
Q extends 'domain_info' ? EnsDomainDetailed :
Q extends 'domain_events' ? EnsDomainEventsResponse :
Q extends 'domains_lookup' ? EnsDomainLookupResponse :
never; never;
/* eslint-enable @typescript-eslint/indent */ /* eslint-enable @typescript-eslint/indent */
......
export interface EnsDomain {
id: string;
name: string;
resolvedAddress: {
hash: string;
};
owner: {
hash: string;
};
registrationDate?: string;
expiryDate?: string;
}
export interface EnsDomainDetailed {
id: string;
name: string;
tokenId: string;
resolvedAddress: {
hash: string;
};
owner: {
hash: string;
};
registrant: {
hash: string;
};
registrationDate?: string;
expiryDate?: string;
otherAddresses: Record<string, string>;
}
export interface EnsDomainEvent {
transactionHash: string;
timestamp: string;
fromAddress: {
hash: string;
};
action?: string;
}
export interface EnsAddressLookupResponse {
items: Array<EnsDomain>;
totalRecords: number;
}
export interface EnsDomainEventsResponse {
items: Array<EnsDomainEvent>;
totalRecords: number;
}
export interface EnsDomainLookupResponse {
items: Array<EnsDomain>;
totalRecords: number;
}
import { Button, chakra, Flex, Grid, Icon, Link, Popover, PopoverBody, PopoverContent, PopoverTrigger, Skeleton, useDisclosure } from '@chakra-ui/react';
import React from 'react';
import config from 'configs/app';
import arrowIcon from 'icons/arrows/east-mini.svg';
import ensIcon from 'icons/ENS.svg';
import useApiQuery from 'lib/api/useApiQuery';
import dayjs from 'lib/date/dayjs';
import EnsEntity from 'ui/shared/entities/ens/EnsEntity';
interface Props {
addressHash: string;
mainDomainName: string | null;
}
const AddressEnsDomains = ({ addressHash, mainDomainName }: Props) => {
const { isOpen, onToggle, onClose } = useDisclosure();
const { data, isPending, isError } = useApiQuery('addresses_lookup', {
pathParams: { chainId: config.chain.id },
fetchParams: {
method: 'POST',
body: {
address: addressHash,
resolvedTo: true,
ownedBy: true,
onlyActive: true,
},
},
});
if (isError) {
return null;
}
if (isPending) {
return <Skeleton h={ 8 } w={{ base: '60px', lg: '120px' }} borderRadius="base"/>;
}
const mainDomain = data.items.find((domain) => domain.name === mainDomainName);
const ownedDomains = data.items.filter((domain) =>
domain.owner.hash.toLowerCase() === addressHash.toLowerCase() &&
domain.name !== mainDomainName,
);
const resolvedDomains = data.items.filter((domain) =>
domain.resolvedAddress.hash.toLowerCase() === addressHash.toLowerCase() &&
domain.name !== mainDomainName,
);
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 }
>
<Icon as={ ensIcon } boxSize={ 5 }/>
<chakra.span ml={ 1 } display={{ base: 'none', lg: 'block' }}>{ data.totalRecords } Domains</chakra.span>
<Icon as={ arrowIcon } 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>
<p>A domain name is not necessarily held by a person popularly associated with the name.</p>
<Flex alignItems="center" fontSize="md" mt={ 4 }>
<EnsEntity name={ mainDomain.name } fontWeight={ 600 } noCopy/>
{ mainDomain.expiryDate &&
<chakra.span color="text_secondary" whiteSpace="pre"> (expires { dayjs(mainDomain.expiryDate).fromNow() })</chakra.span> }
</Flex>
</div>
) }
{ ownedDomains.length > 0 && (
<div>
<chakra.span color="text_secondary" fontSize="xs">Other domain names owned by this address</chakra.span>
<Grid templateColumns={{ base: 'repeat(2, 1fr)', lg: 'repeat(3, 1fr)' }} columnGap={ 8 } rowGap={ 4 } mt={ 2 }>
{ ownedDomains.slice(0, 9).map((domain) => <EnsEntity key={ domain.id } name={ domain.name } noCopy/>) }
</Grid>
</div>
) }
{ resolvedDomains.length > 0 && (
<div>
<chakra.span color="text_secondary" fontSize="xs">Other domain names resolved to this address</chakra.span>
<Grid templateColumns={{ base: 'repeat(2, 1fr)', lg: 'repeat(3, 1fr)' }} columnGap={ 8 } rowGap={ 4 } mt={ 2 }>
{ resolvedDomains.slice(0, 9).map((domain) => <EnsEntity key={ domain.id } name={ domain.name } noCopy/>) }
</Grid>
</div>
) }
{ (ownedDomains.length > 9 || resolvedDomains.length > 9) && (
// TODO @tom2drum add href to link
<Link>
<span> More results</span>
<chakra.span color="text_secondary"> ({ data.totalRecords })</chakra.span>
</Link>
) }
</PopoverBody>
</PopoverContent>
</Popover>
);
};
export default React.memo(AddressEnsDomains);
...@@ -24,6 +24,7 @@ import AddressTxs from 'ui/address/AddressTxs'; ...@@ -24,6 +24,7 @@ import AddressTxs from 'ui/address/AddressTxs';
import AddressWithdrawals from 'ui/address/AddressWithdrawals'; import AddressWithdrawals from 'ui/address/AddressWithdrawals';
import AddressFavoriteButton from 'ui/address/details/AddressFavoriteButton'; import AddressFavoriteButton from 'ui/address/details/AddressFavoriteButton';
import AddressQrCode from 'ui/address/details/AddressQrCode'; import AddressQrCode from 'ui/address/details/AddressQrCode';
import AddressEnsDomains from 'ui/address/ensDomains/AddressEnsDomains';
import SolidityscanReport from 'ui/address/SolidityscanReport'; import SolidityscanReport from 'ui/address/SolidityscanReport';
import AccountActionsMenu from 'ui/shared/AccountActionsMenu/AccountActionsMenu'; import AccountActionsMenu from 'ui/shared/AccountActionsMenu/AccountActionsMenu';
import TextAd from 'ui/shared/ad/TextAd'; import TextAd from 'ui/shared/ad/TextAd';
...@@ -203,6 +204,8 @@ const AddressPageContent = () => { ...@@ -203,6 +204,8 @@ const AddressPageContent = () => {
<AccountActionsMenu isLoading={ isLoading }/> <AccountActionsMenu isLoading={ isLoading }/>
<HStack ml="auto" gap={ 2 }/> <HStack ml="auto" gap={ 2 }/>
{ addressQuery.data?.is_contract && addressQuery.data?.is_verified && config.UI.views.address.solidityscanEnabled && <SolidityscanReport hash={ hash }/> } { addressQuery.data?.is_contract && addressQuery.data?.is_verified && config.UI.views.address.solidityscanEnabled && <SolidityscanReport hash={ hash }/> }
{ addressQuery.data && config.features.nameService.isEnabled &&
<AddressEnsDomains addressHash={ hash } mainDomainName={ addressQuery.data.ens_domain_name }/> }
<NetworkExplorers type="address" pathParam={ hash }/> <NetworkExplorers type="address" pathParam={ hash }/>
</Flex> </Flex>
); );
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment