Commit 2081c83e authored by tom goriunov's avatar tom goriunov Committed by GitHub

ENS: add offchain support and fix bugs (#2156)

* ens: add offchain support and fix bugs

Fixes #2136

* fix ts
parent d5ed4f85
......@@ -63,6 +63,14 @@ export const ensDomainA: bens.DetailedDomain = {
NEAR: 'a0b86991c6218b36c1d19d4a2e9eb0ce3606eb48.factory.bridge.near',
},
protocol: protocolA,
resolver_address: {
hash: '0xD578780f1dA7404d9CC0eEbC9D684c140CC4b638',
},
resolved_with_wildcard: true,
stored_offchain: true,
wrapped_owner: {
hash: '0xD4416b13d2b3a9aBae7AcD5D6C2BbDBE25686401',
},
};
export const ensDomainB: bens.DetailedDomain = {
......@@ -81,6 +89,8 @@ export const ensDomainB: bens.DetailedDomain = {
expiry_date: undefined,
other_addresses: {},
protocol: undefined,
resolved_with_wildcard: false,
stored_offchain: false,
};
export const ensDomainC: bens.DetailedDomain = {
......@@ -101,6 +111,8 @@ export const ensDomainC: bens.DetailedDomain = {
expiry_date: '2022-11-01T13:10:36.000Z',
other_addresses: {},
protocol: undefined,
resolved_with_wildcard: false,
stored_offchain: false,
};
export const ensDomainD: bens.DetailedDomain = {
......@@ -119,4 +131,6 @@ export const ensDomainD: bens.DetailedDomain = {
expiry_date: '2027-09-23T13:10:36.000Z',
other_addresses: {},
protocol: undefined,
resolved_with_wildcard: false,
stored_offchain: false,
};
......@@ -36,7 +36,7 @@
"monitoring:grafana:local": "docker run -d -p 4000:3000 --name=blockscout_grafana --user $(id -u) --volume $(pwd)/grafana:/var/lib/grafana grafana/grafana-enterprise"
},
"dependencies": {
"@blockscout/bens-types": "1.3.4",
"@blockscout/bens-types": "1.4.1",
"@blockscout/stats-types": "1.6.0",
"@blockscout/visualizer-types": "0.2.0",
"@chakra-ui/react": "2.7.1",
......
......@@ -22,6 +22,8 @@ export const ENS_DOMAIN: bens.DetailedDomain = {
ETH: ADDRESS_HASH,
},
protocol: undefined,
resolved_with_wildcard: false,
stored_offchain: false,
};
export const ENS_DOMAIN_EVENT: bens.DomainEvent = {
......
......@@ -31,7 +31,7 @@ import PopoverTriggerTooltip from 'ui/shared/PopoverTriggerTooltip';
interface Props {
query: UseQueryResult<bens.LookupAddressResponse, ResourceError<unknown>>;
addressHash: string;
mainDomainName: string | null;
mainDomainName: string | null | undefined;
}
const DomainsGrid = ({ data }: { data: Array<bens.Domain> }) => {
......@@ -64,9 +64,9 @@ const AddressEnsDomains = ({ query, addressHash, mainDomainName }: Props) => {
return null;
}
const mainDomain = data.items.find((domain) => domain.name === mainDomainName);
const mainDomain = data.items.find((domain) => mainDomainName && domain.name === mainDomainName);
const ownedDomains = data.items.filter((domain) => {
if (domain.name === mainDomainName) {
if (mainDomainName && domain.name === mainDomainName) {
return false;
}
......
......@@ -17,6 +17,7 @@ import IconSvg from 'ui/shared/IconSvg';
import LinkInternal from 'ui/shared/links/LinkInternal';
import TextSeparator from 'ui/shared/TextSeparator';
import NameDomainDetailsAlert from './details/NameDomainDetailsAlert';
import NameDomainExpiryStatus from './NameDomainExpiryStatus';
interface Props {
......@@ -30,196 +31,217 @@ const NameDomainDetails = ({ query }: Props) => {
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.Label
hint="The date the name was registered"
isLoading={ isLoading }
>
<>
<NameDomainDetailsAlert data={ query.data }/>
<Grid columnGap={ 8 } rowGap={ 3 } templateColumns={{ base: 'minmax(0, 1fr)', lg: 'max-content minmax(728px, auto)' }}>
{ query.data?.registration_date && (
<>
<DetailsInfoItem.Label
hint="The date the name was registered"
isLoading={ isLoading }
>
Registration date
</DetailsInfoItem.Label>
<DetailsInfoItem.Value>
<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.Value>
</>
) }
{ query.data?.expiry_date && (
<>
<DetailsInfoItem.Label
</DetailsInfoItem.Label>
<DetailsInfoItem.Value>
<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.Value>
</>
) }
{ query.data?.expiry_date && (
<>
<DetailsInfoItem.Label
// 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 }
>
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 }
>
Expiration date
</DetailsInfoItem.Label>
<DetailsInfoItem.Value>
<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.Value>
</>
) }
{ query.data?.registrant && (
<>
<DetailsInfoItem.Label
hint="The account that owns the domain name and has the rights to edit its ownership and records"
isLoading={ isLoading }
>
</DetailsInfoItem.Label>
<DetailsInfoItem.Value>
<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.Value>
</>
) }
{ query.data?.resolver_address && (
<>
<DetailsInfoItem.Label
hint="The resolver contract provides information about a domain name"
isLoading={ isLoading }
>
Resolver
</DetailsInfoItem.Label>
<DetailsInfoItem.Value
>
<AddressEntity
address={ query.data.resolver_address }
isLoading={ isLoading }
/>
</DetailsInfoItem.Value>
</>
) }
{ query.data?.registrant && (
<>
<DetailsInfoItem.Label
hint="The account that owns the domain name and has the rights to edit its ownership and records"
isLoading={ isLoading }
>
Registrant
</DetailsInfoItem.Label>
<DetailsInfoItem.Value
columnGap={ 2 }
flexWrap="nowrap"
>
<AddressEntity
address={ query.data.registrant }
</DetailsInfoItem.Label>
<DetailsInfoItem.Value
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.Value>
</>
) }
{ query.data?.owner && (
<>
<DetailsInfoItem.Label
hint="The account that owns the rights to edit the records of this domain name"
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.Value>
</>
) }
{ query.data?.owner && (
<>
<DetailsInfoItem.Label
hint="The account that owns the rights to edit the records of this domain name"
isLoading={ isLoading }
>
>
Owner
</DetailsInfoItem.Label>
<DetailsInfoItem.Value
columnGap={ 2 }
flexWrap="nowrap"
>
<AddressEntity
address={ query.data.owner }
</DetailsInfoItem.Label>
<DetailsInfoItem.Value
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.Value>
</>
) }
{ query.data?.wrapped_owner && (
<>
<DetailsInfoItem.Label
hint="Owner of this NFT domain in NameWrapper contract"
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.Value>
</>
) }
{ query.data?.wrapped_owner && (
<>
<DetailsInfoItem.Label
hint="Owner of this NFT domain in NameWrapper contract"
isLoading={ isLoading }
>
>
Manager
</DetailsInfoItem.Label>
<DetailsInfoItem.Value
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.Value>
</>
) }
{ query.data?.tokens.map((token) => {
const isProtocolBaseChain = stripTrailingSlash(query.data.protocol?.deployment_blockscout_base_url ?? '') === config.app.baseUrl;
const entityProps = {
isExternal: !isProtocolBaseChain ? true : false,
href: !isProtocolBaseChain ? (
stripTrailingSlash(query.data.protocol?.deployment_blockscout_base_url ?? '') +
</DetailsInfoItem.Label>
<DetailsInfoItem.Value
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.Value>
</>
) }
{ query.data?.tokens.map((token) => {
const isProtocolBaseChain = stripTrailingSlash(query.data.protocol?.deployment_blockscout_base_url ?? '') === config.app.baseUrl;
const entityProps = {
isExternal: !isProtocolBaseChain ? true : false,
href: !isProtocolBaseChain ? (
stripTrailingSlash(query.data.protocol?.deployment_blockscout_base_url ?? '') +
route({ pathname: '/token/[hash]/instance/[id]', query: { hash: token.contract_hash, id: token.id } })
) : undefined,
};
return (
<React.Fragment key={ token.type }>
) : undefined,
};
return (
<React.Fragment key={ token.type }>
<DetailsInfoItem.Label
hint={ `The ${ token.type === bens.TokenType.WRAPPED_DOMAIN_TOKEN ? 'wrapped ' : '' }token ID of this domain name NFT` }
isLoading={ isLoading }
>
{ token.type === bens.TokenType.WRAPPED_DOMAIN_TOKEN ? 'Wrapped token ID' : 'Token ID' }
</DetailsInfoItem.Label>
<DetailsInfoItem.Value
wordBreak="break-all"
whiteSpace="pre-wrap"
>
<NftEntity { ...entityProps } hash={ token.contract_hash } id={ token.id } isLoading={ isLoading } noIcon/>
</DetailsInfoItem.Value>
</React.Fragment>
);
}) }
{ otherAddresses.length > 0 && (
<>
<DetailsInfoItem.Label
hint={ `The ${ token.type === bens.TokenType.WRAPPED_DOMAIN_TOKEN ? 'wrapped ' : '' }token ID of this domain name NFT` }
hint="Other cryptocurrency addresses added to this domain name"
isLoading={ isLoading }
>
{ token.type === bens.TokenType.WRAPPED_DOMAIN_TOKEN ? 'Wrapped token ID' : 'Token ID' }
Other addresses
</DetailsInfoItem.Label>
<DetailsInfoItem.Value
wordBreak="break-all"
whiteSpace="pre-wrap"
flexDir="column"
alignItems="flex-start"
>
<NftEntity { ...entityProps } hash={ token.contract_hash } id={ token.id } isLoading={ isLoading } noIcon/>
{ 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.Value>
</React.Fragment>
);
}) }
{ otherAddresses.length > 0 && (
<>
<DetailsInfoItem.Label
hint="Other cryptocurrency addresses added to this domain name"
isLoading={ isLoading }
>
Other addresses
</DetailsInfoItem.Label>
<DetailsInfoItem.Value
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.Value>
</>
) }
</Grid>
</>
) }
</Grid>
</>
);
};
......
import { Alert } from '@chakra-ui/react';
import React from 'react';
import type * as bens from '@blockscout/bens-types';
import LinkExternal from 'ui/shared/links/LinkExternal';
interface Props {
data: bens.DetailedDomain | undefined;
}
const NameDomainDetailsAlert = ({ data }: Props) => {
if (!data?.stored_offchain || !data?.resolved_with_wildcard) {
return null;
}
return (
<Alert status="info" colorScheme="gray" display="inline-block" whiteSpace="pre-wrap" mb={ 6 }>
<span>The domain name is resolved offchain using </span>
{ data.stored_offchain && <LinkExternal href="https://eips.ethereum.org/EIPS/eip-3668">EIP-3668: CCIP Read</LinkExternal> }
{ data.stored_offchain && data.resolved_with_wildcard && <span> & </span> }
{ data.resolved_with_wildcard && <LinkExternal href="https://eips.ethereum.org/EIPS/eip-2544">EIP-2544: Wildcard Resolution</LinkExternal> }
</Alert>
);
};
export default React.memo(NameDomainDetailsAlert);
......@@ -327,8 +327,8 @@ const AddressPageContent = () => {
<HStack ml="auto" gap={ 2 }/>
{ !isLoading && addressQuery.data?.is_contract && addressQuery.data?.is_verified && config.UI.views.address.solidityscanEnabled &&
<SolidityscanReport hash={ hash }/> }
{ !isLoading && addressQuery.data && config.features.nameService.isEnabled &&
<AddressEnsDomains query={ addressEnsDomainsQuery } addressHash={ hash } mainDomainName={ addressQuery.data.ens_domain_name }/> }
{ !isLoading && addressEnsDomainsQuery.data && config.features.nameService.isEnabled &&
<AddressEnsDomains query={ addressEnsDomainsQuery } addressHash={ hash } mainDomainName={ addressQuery.data?.ens_domain_name }/> }
<NetworkExplorers type="address" pathParam={ hash }/>
</Flex>
);
......
......@@ -1327,10 +1327,10 @@
resolved "https://registry.yarnpkg.com/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz#75a2e8b51cb758a7553d6804a5932d7aace75c39"
integrity sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==
"@blockscout/bens-types@1.3.4":
version "1.3.4"
resolved "https://registry.yarnpkg.com/@blockscout/bens-types/-/bens-types-1.3.4.tgz#e75b863c6d065e7d6d5d01e1a1d64da8df261640"
integrity sha512-kKRa8jKu/CBLR3QbWpRXmtwIXiIwIPDrFeEPIYUQp5bg9uY+ActOyQERixo/9FE+BHZShWUDm+75FoaAmIGIOw==
"@blockscout/bens-types@1.4.1":
version "1.4.1"
resolved "https://registry.yarnpkg.com/@blockscout/bens-types/-/bens-types-1.4.1.tgz#9182a79d9015b7fa2339edf0bfa3cd0c32045e66"
integrity sha512-TlZ1HVdZ2Cswm/CcvNoxS+Ydiht/YGaLo//PJR/UmkmihlEFoY4HfVJvVcUnOQXi+Si7FwJ486DPii889nTJsQ==
"@blockscout/stats-types@1.6.0":
version "1.6.0"
......@@ -14899,16 +14899,7 @@ string-template@~0.2.1:
resolved "https://registry.yarnpkg.com/string-template/-/string-template-0.2.1.tgz#42932e598a352d01fc22ec3367d9d84eec6c9add"
integrity sha512-Yptehjogou2xm4UJbxJ4CxgZx12HBfeystp0y3x7s4Dj32ltVVG1Gg8YhKjHZkHicuKpZX/ffilA8505VbUbpw==
"string-width-cjs@npm:string-width@^4.2.0":
version "4.2.3"
resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010"
integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==
dependencies:
emoji-regex "^8.0.0"
is-fullwidth-code-point "^3.0.0"
strip-ansi "^6.0.1"
string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.3:
"string-width-cjs@npm:string-width@^4.2.0", string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.3:
version "4.2.3"
resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010"
integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==
......@@ -15036,14 +15027,7 @@ string_decoder@~1.1.1:
dependencies:
safe-buffer "~5.1.0"
"strip-ansi-cjs@npm:strip-ansi@^6.0.1":
version "6.0.1"
resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9"
integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==
dependencies:
ansi-regex "^5.0.1"
strip-ansi@^6.0.0, strip-ansi@^6.0.1:
"strip-ansi-cjs@npm:strip-ansi@^6.0.1", strip-ansi@^6.0.0, strip-ansi@^6.0.1:
version "6.0.1"
resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9"
integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==
......@@ -16203,7 +16187,7 @@ word-wrap@^1.2.5, word-wrap@~1.2.3:
resolved "https://registry.yarnpkg.com/word-wrap/-/word-wrap-1.2.5.tgz#d2c45c6dd4fbce621a66f136cbe328afd0410b34"
integrity sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==
"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0":
"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0", wrap-ansi@^7.0.0:
version "7.0.0"
resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43"
integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==
......@@ -16221,15 +16205,6 @@ wrap-ansi@^6.2.0:
string-width "^4.1.0"
strip-ansi "^6.0.0"
wrap-ansi@^7.0.0:
version "7.0.0"
resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43"
integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==
dependencies:
ansi-styles "^4.0.0"
string-width "^4.1.0"
strip-ansi "^6.0.0"
wrap-ansi@^8.1.0:
version "8.1.0"
resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-8.1.0.tgz#56dc22368ee570face1b49819975d9b9a5ead214"
......
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