Commit 8c6d7400 authored by tom goriunov's avatar tom goriunov Committed by GitHub

Support `bech32` address standard (#2351)

* create settings context with simple address format toggler

* transform base16 hash to bech32 hash

* add ENV variables

* add snippet to address page

* add redirect for bech32 addresses

* change address format in search

* add provider to tests

* update demo values

* migrate from Buffer to Uint8Array and add tests

* bug fixes and screenshots updates

* review fixes

* roll back changes in env values

* update screenshots
parent e0b89d05
import type { SmartContractVerificationMethodExtra } from 'types/client/contract';
import { SMART_CONTRACT_EXTRA_VERIFICATION_METHODS } from 'types/client/contract';
import type { AddressViewId, IdenticonType } from 'types/views/address';
import { ADDRESS_VIEWS_IDS, IDENTICON_TYPES } from 'types/views/address';
import type { AddressFormat, AddressViewId, IdenticonType } from 'types/views/address';
import { ADDRESS_FORMATS, ADDRESS_VIEWS_IDS, IDENTICON_TYPES } from 'types/views/address';
import { getEnvValue, parseEnvJson } from 'configs/app/utils';
......@@ -11,6 +11,28 @@ const identiconType: IdenticonType = (() => {
return IDENTICON_TYPES.find((type) => value === type) || 'jazzicon';
})();
const formats: Array<AddressFormat> = (() => {
const value = (parseEnvJson<Array<AddressFormat>>(getEnvValue('NEXT_PUBLIC_VIEWS_ADDRESS_FORMAT')) || [])
.filter((format) => ADDRESS_FORMATS.includes(format));
if (value.length === 0) {
return [ 'base16' ];
}
return value;
})();
const bech32Prefix = (() => {
const value = getEnvValue('NEXT_PUBLIC_VIEWS_ADDRESS_BECH_32_PREFIX');
if (!value || !formats.includes('bech32')) {
return undefined;
}
// these are the limits of the bech32 prefix - https://github.com/bitcoin/bips/blob/master/bip-0173.mediawiki#bech32
return value.length >= 1 && value.length <= 83 ? value : undefined;
})();
const hiddenViews = (() => {
const parsedValue = parseEnvJson<Array<AddressViewId>>(getEnvValue('NEXT_PUBLIC_VIEWS_ADDRESS_HIDDEN_VIEWS')) || [];
......@@ -43,6 +65,10 @@ const extraVerificationMethods: Array<SmartContractVerificationMethodExtra> = ((
const config = Object.freeze({
identiconType,
hashFormat: {
availableFormats: formats,
bech32Prefix,
},
hiddenViews,
solidityscanEnabled: getEnvValue('NEXT_PUBLIC_VIEWS_CONTRACT_SOLIDITYSCAN_ENABLED') === 'true',
extraVerificationMethods,
......
......@@ -54,3 +54,5 @@ NEXT_PUBLIC_METADATA_SERVICE_API_HOST=http://localhost:3007
NEXT_PUBLIC_NAME_SERVICE_API_HOST=http://localhost:3008
NEXT_PUBLIC_RE_CAPTCHA_V3_APP_SITE_KEY=xxx
NEXT_PUBLIC_WALLET_CONNECT_PROJECT_ID=xxx
NEXT_PUBLIC_VIEWS_ADDRESS_FORMAT=['base16','bech32']
NEXT_PUBLIC_VIEWS_ADDRESS_BECH_32_PREFIX=tom
......@@ -35,8 +35,8 @@ import type { ChainIndicatorId, HeroBannerButtonState, HeroBannerConfig, HomeSta
import { type NetworkVerificationTypeEnvs, type NetworkExplorer, type FeaturedNetwork, NETWORK_GROUPS } from '../../../types/networks';
import { COLOR_THEME_IDS } from '../../../types/settings';
import type { FontFamily } from '../../../types/ui';
import type { AddressViewId } from '../../../types/views/address';
import { ADDRESS_VIEWS_IDS, IDENTICON_TYPES } from '../../../types/views/address';
import type { AddressFormat, AddressViewId } from '../../../types/views/address';
import { ADDRESS_FORMATS, ADDRESS_VIEWS_IDS, IDENTICON_TYPES } from '../../../types/views/address';
import { BLOCK_FIELDS_IDS } from '../../../types/views/block';
import type { BlockFieldId } from '../../../types/views/block';
import type { NftMarketplaceItem } from '../../../types/views/nft';
......@@ -658,6 +658,19 @@ const schema = yup
.json()
.of(yup.string<BlockFieldId>().oneOf(BLOCK_FIELDS_IDS)),
NEXT_PUBLIC_VIEWS_ADDRESS_IDENTICON_TYPE: yup.string().oneOf(IDENTICON_TYPES),
NEXT_PUBLIC_VIEWS_ADDRESS_FORMAT: yup
.array()
.transform(replaceQuotes)
.json()
.of(yup.string<AddressFormat>().oneOf(ADDRESS_FORMATS)),
NEXT_PUBLIC_VIEWS_ADDRESS_BECH_32_PREFIX: yup
.string()
.when('NEXT_PUBLIC_VIEWS_ADDRESS_FORMAT', {
is: (value: Array<AddressFormat> | undefined) => value && value.includes('bech32'),
then: (schema) => schema.required().min(1).max(83),
otherwise: (schema) => schema.max(-1, 'NEXT_PUBLIC_VIEWS_ADDRESS_BECH_32_PREFIX is required if NEXT_PUBLIC_VIEWS_ADDRESS_FORMAT contains "bech32"'),
}),
NEXT_PUBLIC_VIEWS_ADDRESS_HIDDEN_VIEWS: yup
.array()
.transform(replaceQuotes)
......
NEXT_PUBLIC_GRAPHIQL_TRANSACTION=none
NEXT_PUBLIC_API_SPEC_URL=none
NEXT_PUBLIC_VIEWS_CONTRACT_EXTRA_VERIFICATION_METHODS=none
NEXT_PUBLIC_HOMEPAGE_STATS=[]
\ No newline at end of file
NEXT_PUBLIC_HOMEPAGE_STATS=[]
NEXT_PUBLIC_VIEWS_ADDRESS_FORMAT=['base16','bech32']
NEXT_PUBLIC_VIEWS_ADDRESS_BECH_32_PREFIX=foo
\ No newline at end of file
......@@ -70,6 +70,7 @@ NEXT_PUBLIC_STATS_API_HOST=https://example.com
NEXT_PUBLIC_STATS_API_BASE_PATH=/
NEXT_PUBLIC_USE_NEXT_JS_PROXY=false
NEXT_PUBLIC_VIEWS_ADDRESS_IDENTICON_TYPE=gradient_avatar
NEXT_PUBLIC_VIEWS_ADDRESS_FORMAT=['base16']
NEXT_PUBLIC_VIEWS_ADDRESS_HIDDEN_VIEWS=['top_accounts']
NEXT_PUBLIC_VIEWS_CONTRACT_EXTRA_VERIFICATION_METHODS=['solidity-hardhat','solidity-foundry']
NEXT_PUBLIC_VIEWS_BLOCK_HIDDEN_FIELDS=['burnt_fees','total_reward']
......
......@@ -235,6 +235,8 @@ Settings for meta tags, OG tags and SEO
| Variable | Type | Description | Compulsoriness | Default value | Example value | Version |
| --- | --- | --- | --- | --- | --- | --- |
| NEXT_PUBLIC_VIEWS_ADDRESS_IDENTICON_TYPE | `"github" \| "jazzicon" \| "gradient_avatar" \| "blockie"` | Default style of address identicon appearance. Choose between [GitHub](https://github.blog/2013-08-14-identicons/), [Metamask Jazzicon](https://metamask.github.io/jazzicon/), [Gradient Avatar](https://github.com/varld/gradient-avatar) and [Ethereum Blocky](https://mycryptohq.github.io/ethereum-blockies-base64/) | - | `jazzicon` | `gradient_avatar` | v1.12.0+ |
| NEXT_PUBLIC_VIEWS_ADDRESS_FORMAT | `Array<"base16" \| "bech32">` | Displayed address format, could be either `base16` standard or [`bech32`](https://github.com/bitcoin/bips/blob/master/bip-0173.mediawiki#bech32) standard. If the array contains multiple values, the address format toggle will appear in the UI, allowing the user to switch between formats. The first item in the array will be the default format. | - | `'["base16"]'` | `'["bech32", "base16"]'` | v1.36.0+ |
| NEXT_PUBLIC_VIEWS_ADDRESS_BECH_32_PREFIX | `string` | Human-readable prefix of `bech32` address format. | Required, if `NEXT_PUBLIC_VIEWS_ADDRESS_FORMAT` contains "bech32" value | - | `duck` | v1.36.0+ |
| NEXT_PUBLIC_VIEWS_ADDRESS_HIDDEN_VIEWS | `Array<AddressViewId>` | Address views that should not be displayed. See below the list of the possible id values. | - | - | `'["top_accounts"]'` | v1.15.0+ |
| NEXT_PUBLIC_VIEWS_CONTRACT_SOLIDITYSCAN_ENABLED | `boolean` | Set to `true` if SolidityScan reports are supported | - | - | `true` | v1.19.0+ |
| NEXT_PUBLIC_VIEWS_CONTRACT_EXTRA_VERIFICATION_METHODS | `Array<'solidity-hardhat' \| 'solidity-foundry'>` | Pass an array of additional methods from which users can choose while verifying a smart contract. Both methods are available by default, pass `'none'` string to disable them all. | - | - | `['solidity-hardhat']` | v1.33.0+ |
......
import { bech32 } from '@scure/base';
import config from 'configs/app';
import bytesToHex from 'lib/bytesToHex';
import hexToBytes from 'lib/hexToBytes';
export const DATA_PART_REGEXP = /^[\da-z]{38}$/;
export const BECH_32_SEPARATOR = '1'; // https://github.com/bitcoin/bips/blob/master/bip-0173.mediawiki#bech32
export function toBech32Address(hash: string) {
if (config.UI.views.address.hashFormat.bech32Prefix) {
try {
const words = bech32.toWords(hexToBytes(hash));
return bech32.encode(config.UI.views.address.hashFormat.bech32Prefix, words);
} catch (error) {}
}
return hash;
}
export function isBech32Address(hash: string) {
if (!config.UI.views.address.hashFormat.bech32Prefix) {
return false;
}
if (!hash.startsWith(`${ config.UI.views.address.hashFormat.bech32Prefix }${ BECH_32_SEPARATOR }`)) {
return false;
}
const strippedHash = hash.replace(`${ config.UI.views.address.hashFormat.bech32Prefix }${ BECH_32_SEPARATOR }`, '');
return DATA_PART_REGEXP.test(strippedHash);
}
export function fromBech32Address(hash: string) {
if (config.UI.views.address.hashFormat.bech32Prefix) {
try {
const { words, prefix } = bech32.decode(hash as `${ string }${ typeof BECH_32_SEPARATOR }${ string }`);
if (prefix !== config.UI.views.address.hashFormat.bech32Prefix) {
return hash;
}
const bytes = bech32.fromWords(words);
return bytesToHex(bytes);
} catch (error) {}
}
return hash;
}
......@@ -5,7 +5,7 @@ import hexToBytes from 'lib/hexToBytes';
import removeNonSignificantZeroBytes from './removeNonSignificantZeroBytes';
export default function guessDataType(data: string) {
const bytes = new Uint8Array(hexToBytes(data));
const bytes = hexToBytes(data);
const filteredBytes = removeNonSignificantZeroBytes(bytes);
return filetype(filteredBytes)[0];
......
export default function bytesToBase64(bytes: Uint8Array) {
let result = '';
for (const byte of bytes) {
result += Number(byte).toString(16).padStart(2, '0');
}
return `0x${ result }`;
}
import React from 'react';
import { ADDRESS_FORMATS, type AddressFormat } from 'types/views/address';
import * as cookies from 'lib/cookies';
import { useAppContext } from './app';
interface SettingsProviderProps {
children: React.ReactNode;
}
interface TSettingsContext {
addressFormat: AddressFormat;
toggleAddressFormat: () => void;
}
export const SettingsContext = React.createContext<TSettingsContext | null>(null);
export function SettingsContextProvider({ children }: SettingsProviderProps) {
const { cookies: appCookies } = useAppContext();
const initialAddressFormat = cookies.get(cookies.NAMES.ADDRESS_FORMAT, appCookies);
const [ addressFormat, setAddressFormat ] = React.useState<AddressFormat>(
initialAddressFormat && ADDRESS_FORMATS.includes(initialAddressFormat) ? initialAddressFormat as AddressFormat : 'base16',
);
const toggleAddressFormat = React.useCallback(() => {
setAddressFormat(prev => {
const nextValue = prev === 'base16' ? 'bech32' : 'base16';
cookies.set(cookies.NAMES.ADDRESS_FORMAT, nextValue);
return nextValue;
});
}, []);
const value = React.useMemo(() => {
return {
addressFormat,
toggleAddressFormat,
};
}, [ addressFormat, toggleAddressFormat ]);
return (
<SettingsContext.Provider value={ value }>
{ children }
</SettingsContext.Provider>
);
}
export function useSettingsContext(disabled?: boolean) {
const context = React.useContext(SettingsContext);
if (context === undefined || disabled) {
return null;
}
return context;
}
......@@ -11,6 +11,7 @@ export enum NAMES {
COLOR_MODE='chakra-ui-color-mode',
COLOR_MODE_HEX='chakra-ui-color-mode-hex',
ADDRESS_IDENTICON_TYPE='address_identicon_type',
ADDRESS_FORMAT='address_format',
INDEXING_ALERT='indexing_alert',
ADBLOCK_DETECTED='adblock_detected',
MIXPANEL_DEBUG='_mixpanel_debug',
......
......@@ -2,7 +2,7 @@ import bytesToBase64 from './bytesToBase64';
import hexToBytes from './hexToBytes';
export default function hexToBase64(hex: string) {
const bytes = new Uint8Array(hexToBytes(hex));
const bytes = hexToBytes(hex);
return bytesToBase64(bytes);
}
......@@ -5,5 +5,5 @@ export default function hexToBytes(hex: string) {
for (let c = startIndex; c < hex.length; c += 2) {
bytes.push(parseInt(hex.substring(c, c + 2), 16));
}
return bytes;
return new Uint8Array(bytes);
}
......@@ -2,7 +2,7 @@ import hexToBytes from 'lib/hexToBytes';
export default function hexToUtf8(hex: string) {
const utf8decoder = new TextDecoder();
const bytes = new Uint8Array(hexToBytes(hex));
const bytes = hexToBytes(hex);
return utf8decoder.decode(bytes);
}
......@@ -22,6 +22,7 @@ export function middleware(req: NextRequest) {
const res = NextResponse.next();
middlewares.colorTheme(req, res);
middlewares.addressFormat(req, res);
const end = Date.now();
......
import type { NextRequest, NextResponse } from 'next/server';
import type { AddressFormat } from 'types/views/address';
import config from 'configs/app';
import * as cookiesLib from 'lib/cookies';
export default function addressFormatMiddleware(req: NextRequest, res: NextResponse) {
const addressFormatCookie = req.cookies.get(cookiesLib.NAMES.ADDRESS_FORMAT);
const defaultFormat = config.UI.views.address.hashFormat.availableFormats[0];
if (addressFormatCookie) {
const isValidCookie = config.UI.views.address.hashFormat.availableFormats.includes(addressFormatCookie.value as AddressFormat);
if (!isValidCookie) {
res.cookies.set(cookiesLib.NAMES.ADDRESS_FORMAT, defaultFormat, { path: '/' });
}
} else {
res.cookies.set(cookiesLib.NAMES.ADDRESS_FORMAT, defaultFormat, { path: '/' });
}
}
export { account } from './account';
export { default as colorTheme } from './colorTheme';
export { default as addressFormat } from './addressFormat';
......@@ -15,6 +15,7 @@ import { ChakraProvider } from 'lib/contexts/chakra';
import { MarketplaceContextProvider } from 'lib/contexts/marketplace';
import { RewardsContextProvider } from 'lib/contexts/rewards';
import { ScrollDirectionProvider } from 'lib/contexts/scrollDirection';
import { SettingsContextProvider } from 'lib/contexts/settings';
import { growthBook } from 'lib/growthbook/init';
import useLoadFeatures from 'lib/growthbook/useLoadFeatures';
import useNotifyOnNavigation from 'lib/hooks/useNotifyOnNavigation';
......@@ -73,8 +74,10 @@ function MyApp({ Component, pageProps }: AppPropsWithLayout) {
<SocketProvider url={ `${ config.api.socket }${ config.api.basePath }/socket/v2` }>
<RewardsContextProvider>
<MarketplaceContextProvider>
{ getLayout(<Component { ...pageProps }/>) }
{ config.features.rewards.isEnabled && <RewardsLoginModal/> }
<SettingsContextProvider>
{ getLayout(<Component { ...pageProps }/>) }
{ config.features.rewards.isEnabled && <RewardsLoginModal/> }
</SettingsContextProvider>
</MarketplaceContextProvider>
</RewardsContextProvider>
</SocketProvider>
......
......@@ -12,6 +12,7 @@ import config from 'configs/app';
import { AppContextProvider } from 'lib/contexts/app';
import { MarketplaceContext } from 'lib/contexts/marketplace';
import { RewardsContextProvider } from 'lib/contexts/rewards';
import { SettingsContextProvider } from 'lib/contexts/settings';
import { SocketProvider } from 'lib/socket/context';
import currentChain from 'lib/web3/currentChain';
import theme from 'theme/theme';
......@@ -76,13 +77,15 @@ const TestApp = ({ children, withSocket, appContext = defaultAppContext, marketp
<SocketProvider url={ withSocket ? `ws://${ config.app.host }:${ socketPort }` : undefined }>
<AppContextProvider { ...appContext }>
<MarketplaceContext.Provider value={ marketplaceContext }>
<GrowthBookProvider>
<WagmiProvider config={ wagmiConfig }>
<RewardsContextProvider>
{ children }
</RewardsContextProvider>
</WagmiProvider>
</GrowthBookProvider>
<SettingsContextProvider>
<GrowthBookProvider>
<WagmiProvider config={ wagmiConfig }>
<RewardsContextProvider>
{ children }
</RewardsContextProvider>
</WagmiProvider>
</GrowthBookProvider>
</SettingsContextProvider>
</MarketplaceContext.Provider>
</AppContextProvider>
</SocketProvider>
......
......@@ -84,4 +84,8 @@ export const ENVS_MAP: Record<string, Array<[string, string]>> = {
rewardsService: [
[ 'NEXT_PUBLIC_REWARDS_SERVICE_API_HOST', 'http://localhost:3003' ],
],
addressBech32Format: [
[ 'NEXT_PUBLIC_ADDRESS_FORMAT', '["bech32","base16"]' ],
[ 'NEXT_PUBLIC_VIEWS_ADDRESS_BECH_32_PREFIX', 'tom' ],
],
};
......@@ -14,3 +14,6 @@ export const ADDRESS_VIEWS_IDS = [
] as const;
export type AddressViewId = ArrayElement<typeof ADDRESS_VIEWS_IDS>;
export const ADDRESS_FORMATS = [ 'base16', 'bech32' ] as const;
export type AddressFormat = typeof ADDRESS_FORMATS[ number ];
......@@ -18,6 +18,7 @@ import AddressEntity from 'ui/shared/entities/address/AddressEntity';
import BlockEntity from 'ui/shared/entities/block/BlockEntity';
import TxEntity from 'ui/shared/entities/tx/TxEntity';
import AddressAlternativeFormat from './details/AddressAlternativeFormat';
import AddressBalance from './details/AddressBalance';
import AddressImplementations from './details/AddressImplementations';
import AddressNameInfo from './details/AddressNameInfo';
......@@ -98,6 +99,8 @@ const AddressDetails = ({ addressQuery, scrollRef }: Props) => {
rowGap={{ base: 1, lg: 3 }}
templateColumns={{ base: 'minmax(0, 1fr)', lg: 'auto minmax(0, 1fr)' }} overflow="hidden"
>
<AddressAlternativeFormat isLoading={ addressQuery.isPlaceholderData } addressHash={ addressHash }/>
{ data.filecoin?.id && (
<>
<DetailsInfoItem.Label
......
......@@ -42,6 +42,7 @@ const ContractConnectWallet = ({ isLoading }: Props) => {
truncation={ isMobile ? 'constant' : 'dynamic' }
fontWeight={ 600 }
ml={ 2 }
noAltHash
/>
</Flex>
<Button onClick={ web3Wallet.disconnect } size="sm" variant="outline">Disconnect</Button>
......
import React from 'react';
import config from 'configs/app';
import { BECH_32_SEPARATOR, toBech32Address } from 'lib/address/bech32';
import { useSettingsContext } from 'lib/contexts/settings';
import * as DetailsInfoItem from 'ui/shared/DetailsInfoItem';
import AddressEntity from 'ui/shared/entities/address/AddressEntity';
interface Props {
isLoading: boolean;
addressHash: string;
}
const AddressAlternativeFormat = ({ isLoading, addressHash }: Props) => {
const settingsContext = useSettingsContext();
if (!settingsContext || config.UI.views.address.hashFormat.availableFormats.length < 2) {
return null;
}
const label = settingsContext.addressFormat === 'bech32' ? '0x hash' : `${ config.UI.views.address.hashFormat.bech32Prefix }${ BECH_32_SEPARATOR } hash`;
const hint = settingsContext.addressFormat === 'bech32' ? 'Address hash encoded in base16 format' : 'Address hash encoded in bech32 format';
const altHash = settingsContext.addressFormat === 'bech32' ? addressHash : toBech32Address(addressHash);
return (
<>
<DetailsInfoItem.Label
hint={ hint }
isLoading={ isLoading }
>
{ label }
</DetailsInfoItem.Label>
<DetailsInfoItem.Value>
<AddressEntity
address={{ hash: altHash }}
isLoading={ isLoading }
noIcon
noLink
noAltHash
/>
</DetailsInfoItem.Value>
</>
);
};
export default React.memo(AddressAlternativeFormat);
import { useRouter } from 'next/router';
import React from 'react';
import config from 'configs/app';
import { fromBech32Address, isBech32Address } from 'lib/address/bech32';
export default function useCheckAddressFormat(hash: string) {
const router = useRouter();
const hasBech32Format = config.UI.views.address.hashFormat.availableFormats.includes('bech32') && isBech32Address(hash);
const [ isLoading, setIsLoading ] = React.useState(hasBech32Format);
React.useEffect(() => {
if (!isLoading) {
return;
}
const base16Hash = fromBech32Address(hash);
if (base16Hash !== hash) {
router.replace({ pathname: '/address/[hash]', query: { ...router.query, hash: base16Hash } });
} else {
setIsLoading(false);
}
}, [ hash, isLoading, router ]);
return isLoading;
}
......@@ -50,7 +50,7 @@ const BlobData = ({ data, isLoading, hash }: Props) => {
const fileBlob = (() => {
switch (format) {
case 'Image': {
const bytes = new Uint8Array(hexToBytes(data));
const bytes = hexToBytes(data);
const filteredBytes = removeNonSignificantZeroBytes(bytes);
return new Blob([ filteredBytes ], { type: guessedType?.mime });
}
......@@ -77,7 +77,7 @@ const BlobData = ({ data, isLoading, hash }: Props) => {
return <RawDataSnippet data="Not an image" showCopy={ false } isLoading={ isLoading }/>;
}
const bytes = new Uint8Array(hexToBytes(data));
const bytes = hexToBytes(data);
const filteredBytes = removeNonSignificantZeroBytes(bytes);
const base64 = bytesToBase64(filteredBytes);
......
......@@ -35,6 +35,7 @@ const MyProfileWallet = ({ profileQuery, onAddWallet }: Props) => {
<AddressEntity
address={{ hash: profileQuery.data.address_hash }}
fontWeight="500"
noAltHash
/>
</Box>
) : <Button size="sm" onClick={ onAddWallet }>Link wallet</Button> }
......
......@@ -41,6 +41,7 @@ import AddressQrCode from 'ui/address/details/AddressQrCode';
import AddressEnsDomains from 'ui/address/ensDomains/AddressEnsDomains';
import SolidityscanReport from 'ui/address/SolidityscanReport';
import useAddressQuery from 'ui/address/utils/useAddressQuery';
import useCheckAddressFormat from 'ui/address/utils/useCheckAddressFormat';
import useCheckDomainNameParam from 'ui/address/utils/useCheckDomainNameParam';
import AccountActionsMenu from 'ui/shared/AccountActionsMenu/AccountActionsMenu';
import TextAd from 'ui/shared/ad/TextAd';
......@@ -69,7 +70,9 @@ const AddressPageContent = () => {
const hash = getQueryParamString(router.query.hash);
const checkSummedHash = React.useMemo(() => getCheckedSummedAddress(hash), [ hash ]);
const areQueriesEnabled = !useCheckDomainNameParam(hash);
const checkDomainName = useCheckDomainNameParam(hash);
const checkAddressFormat = useCheckAddressFormat(hash);
const areQueriesEnabled = !checkDomainName && !checkAddressFormat;
const addressQuery = useAddressQuery({ hash, isEnabled: areQueriesEnabled });
const addressTabsCountersQuery = useApiQuery('address_tabs_counters', {
......
......@@ -6,6 +6,7 @@ import React from 'react';
import type { SearchResultItem } from 'types/client/search';
import config from 'configs/app';
import { useSettingsContext } from 'lib/contexts/settings';
import * as regexp from 'lib/regexp';
import getQueryParamString from 'lib/router/getQueryParamString';
import removeQueryParam from 'lib/router/removeQueryParam';
......@@ -36,6 +37,7 @@ const SearchResultsPageContent = () => {
const [ showContent, setShowContent ] = React.useState(!withRedirectCheck);
const marketplaceApps = useMarketplaceApps(debouncedSearchTerm);
const settingsContext = useSettingsContext();
React.useEffect(() => {
if (showContent) {
......@@ -144,6 +146,7 @@ const SearchResultsPageContent = () => {
data={ item }
searchTerm={ debouncedSearchTerm }
isLoading={ isLoading }
addressFormat={ settingsContext?.addressFormat }
/>
)) }
</Show>
......@@ -164,6 +167,7 @@ const SearchResultsPageContent = () => {
data={ item }
searchTerm={ debouncedSearchTerm }
isLoading={ isLoading }
addressFormat={ settingsContext?.addressFormat }
/>
)) }
</Tbody>
......
......@@ -3,9 +3,11 @@ import React from 'react';
import xss from 'xss';
import type { SearchResultItem } from 'types/client/search';
import type { AddressFormat } from 'types/views/address';
import { route } from 'nextjs-routes';
import { toBech32Address } from 'lib/address/bech32';
import dayjs from 'lib/date/dayjs';
import highlightText from 'lib/highlightText';
import * as mixpanel from 'lib/mixpanel/index';
......@@ -31,9 +33,10 @@ interface Props {
data: SearchResultItem | SearchResultAppItem;
searchTerm: string;
isLoading?: boolean;
addressFormat?: AddressFormat;
}
const SearchResultListItem = ({ data, searchTerm, isLoading }: Props) => {
const SearchResultListItem = ({ data, searchTerm, isLoading, addressFormat }: Props) => {
const handleLinkClick = React.useCallback((e: React.MouseEvent<HTMLAnchorElement>) => {
saveToRecentKeywords(searchTerm);
......@@ -78,6 +81,8 @@ const SearchResultListItem = ({ data, searchTerm, isLoading }: Props) => {
case 'contract':
case 'address': {
const shouldHighlightHash = ADDRESS_REGEXP.test(searchTerm);
const hash = addressFormat === 'bech32' ? toBech32Address(data.address) : data.address;
const address = {
hash: data.address,
filecoin: {
......@@ -99,13 +104,13 @@ const SearchResultListItem = ({ data, searchTerm, isLoading }: Props) => {
>
<AddressEntity.Content
asProp={ shouldHighlightHash ? 'mark' : 'span' }
address={ address }
address={{ ...address, hash }}
fontSize="sm"
lineHeight={ 5 }
fontWeight={ 700 }
/>
</AddressEntity.Link>
<AddressEntity.Copy address={ address }/>
<AddressEntity.Copy address={{ ...address, hash }}/>
</AddressEntity.Container>
);
}
......@@ -286,12 +291,13 @@ const SearchResultListItem = ({ data, searchTerm, isLoading }: Props) => {
case 'token': {
const templateCols = `1fr
${ (data.token_type === 'ERC-20' && data.exchange_rate) || (data.token_type !== 'ERC-20' && data.total_supply) ? ' auto' : '' }`;
const hash = data.filecoin_robust_address || (addressFormat === 'bech32' ? toBech32Address(data.address) : data.address);
return (
<Grid templateColumns={ templateCols } alignItems="center" gap={ 2 }>
<Skeleton isLoaded={ !isLoading } overflow="hidden" display="flex" alignItems="center">
<Text whiteSpace="nowrap" overflow="hidden">
<HashStringShortenDynamic hash={ data.filecoin_robust_address || data.address } isTooltipDisabled/>
<HashStringShortenDynamic hash={ hash } isTooltipDisabled/>
</Text>
{ data.is_smart_contract_verified && <IconSvg name="status/success" boxSize="14px" color="green.500" ml={ 1 } flexShrink={ 0 }/> }
</Skeleton>
......@@ -333,10 +339,12 @@ const SearchResultListItem = ({ data, searchTerm, isLoading }: Props) => {
);
}
case 'label': {
const hash = data.filecoin_robust_address || (addressFormat === 'bech32' ? toBech32Address(data.address) : data.address);
return (
<Flex alignItems="center">
<Box overflow="hidden">
<HashStringShortenDynamic hash={ data.filecoin_robust_address || data.address }/>
<HashStringShortenDynamic hash={ hash }/>
</Box>
{ data.is_smart_contract_verified && <IconSvg name="status/success" boxSize="14px" color="green.500" ml={ 1 } flexShrink={ 0 }/> }
</Flex>
......@@ -384,10 +392,12 @@ const SearchResultListItem = ({ data, searchTerm, isLoading }: Props) => {
}
case 'ens_domain': {
const expiresText = data.ens_info?.expiry_date ? ` expires ${ dayjs(data.ens_info.expiry_date).fromNow() }` : '';
const hash = data.filecoin_robust_address || (addressFormat === 'bech32' ? toBech32Address(data.address) : data.address);
return (
<Flex alignItems="center" gap={ 3 }>
<Box overflow="hidden">
<HashStringShortenDynamic hash={ data.filecoin_robust_address || data.address }/>
<HashStringShortenDynamic hash={ hash }/>
</Box>
{
data.ens_info.names_count > 1 ?
......
......@@ -3,9 +3,11 @@ import React from 'react';
import xss from 'xss';
import type { SearchResultItem } from 'types/client/search';
import type { AddressFormat } from 'types/views/address';
import { route } from 'nextjs-routes';
import { toBech32Address } from 'lib/address/bech32';
import dayjs from 'lib/date/dayjs';
import highlightText from 'lib/highlightText';
import * as mixpanel from 'lib/mixpanel/index';
......@@ -30,9 +32,10 @@ interface Props {
data: SearchResultItem | SearchResultAppItem;
searchTerm: string;
isLoading?: boolean;
addressFormat?: AddressFormat;
}
const SearchResultTableItem = ({ data, searchTerm, isLoading }: Props) => {
const SearchResultTableItem = ({ data, searchTerm, isLoading, addressFormat }: Props) => {
const handleLinkClick = React.useCallback((e: React.MouseEvent<HTMLAnchorElement>) => {
saveToRecentKeywords(searchTerm);
......@@ -49,6 +52,7 @@ const SearchResultTableItem = ({ data, searchTerm, isLoading }: Props) => {
switch (data.type) {
case 'token': {
const name = data.name + (data.symbol ? ` (${ data.symbol })` : '');
const hash = data.filecoin_robust_address || (addressFormat === 'bech32' ? toBech32Address(data.address) : data.address);
return (
<>
......@@ -77,7 +81,7 @@ const SearchResultTableItem = ({ data, searchTerm, isLoading }: Props) => {
<Td fontSize="sm" verticalAlign="middle">
<Skeleton isLoaded={ !isLoading } whiteSpace="nowrap" overflow="hidden" display="flex" alignItems="center">
<Box overflow="hidden" whiteSpace="nowrap" w={ data.is_smart_contract_verified ? 'calc(100%-28px)' : 'unset' }>
<HashStringShortenDynamic hash={ data.filecoin_robust_address || data.address }/>
<HashStringShortenDynamic hash={ hash }/>
</Box>
{ data.is_smart_contract_verified && <IconSvg name="status/success" boxSize="14px" color="green.500" ml={ 1 } flexShrink={ 0 }/> }
</Skeleton>
......@@ -110,6 +114,7 @@ const SearchResultTableItem = ({ data, searchTerm, isLoading }: Props) => {
ens_domain_name: null,
};
const expiresText = data.ens_info?.expiry_date ? ` (expires ${ dayjs(data.ens_info.expiry_date).fromNow() })` : '';
const hash = addressFormat === 'bech32' ? toBech32Address(data.address) : data.address;
return (
<>
......@@ -122,13 +127,13 @@ const SearchResultTableItem = ({ data, searchTerm, isLoading }: Props) => {
>
<AddressEntity.Content
asProp={ shouldHighlightHash ? 'mark' : 'span' }
address={ address }
address={{ ...address, hash }}
fontSize="sm"
lineHeight={ 5 }
fontWeight={ 700 }
/>
</AddressEntity.Link>
<AddressEntity.Copy address={ address }/>
<AddressEntity.Copy address={{ ...address, hash }}/>
</AddressEntity.Container>
</Td>
{ addressName && (
......@@ -158,6 +163,8 @@ const SearchResultTableItem = ({ data, searchTerm, isLoading }: Props) => {
}
case 'label': {
const hash = data.filecoin_robust_address || (addressFormat === 'bech32' ? toBech32Address(data.address) : data.address);
return (
<>
<Td fontSize="sm">
......@@ -177,7 +184,7 @@ const SearchResultTableItem = ({ data, searchTerm, isLoading }: Props) => {
<Td fontSize="sm" verticalAlign="middle">
<Flex alignItems="center" overflow="hidden">
<Box overflow="hidden" whiteSpace="nowrap" w={ data.is_smart_contract_verified ? 'calc(100%-28px)' : 'unset' }>
<HashStringShortenDynamic hash={ data.filecoin_robust_address || data.address }/>
<HashStringShortenDynamic hash={ hash }/>
</Box>
{ data.is_smart_contract_verified && <IconSvg name="status/success" boxSize="14px" color="green.500" ml={ 1 } flexShrink={ 0 }/> }
</Flex>
......@@ -369,6 +376,8 @@ const SearchResultTableItem = ({ data, searchTerm, isLoading }: Props) => {
case 'ens_domain': {
const expiresText = data.ens_info?.expiry_date ? ` expires ${ dayjs(data.ens_info.expiry_date).fromNow() }` : '';
const hash = data.filecoin_robust_address || (addressFormat === 'bech32' ? toBech32Address(data.address) : data.address);
return (
<>
<Td fontSize="sm">
......@@ -395,7 +404,7 @@ const SearchResultTableItem = ({ data, searchTerm, isLoading }: Props) => {
<Td>
<Flex alignItems="center" overflow="hidden">
<Box overflow="hidden" whiteSpace="nowrap" w={ data.is_smart_contract_verified ? 'calc(100%-28px)' : 'unset' }>
<HashStringShortenDynamic hash={ data.filecoin_robust_address || data.address }/>
<HashStringShortenDynamic hash={ hash }/>
</Box>
{ data.is_smart_contract_verified && <IconSvg name="status/success" boxSize="14px" color="green.500" ml={ 1 } flexShrink={ 0 }/> }
</Flex>
......
import { Box } from '@chakra-ui/react';
import type { BrowserContext } from '@playwright/test';
import React from 'react';
import config from 'configs/app';
import { AddressHighlightProvider } from 'lib/contexts/addressHighlight';
import * as cookies from 'lib/cookies';
import * as addressMock from 'mocks/address/address';
import * as implementationsMock from 'mocks/address/implementations';
import * as metadataMock from 'mocks/metadata/address';
import { ENVS_MAP } from 'playwright/fixtures/mockEnvs';
import { test, expect } from 'playwright/lib';
import AddressEntity from './AddressEntity';
......@@ -210,3 +214,21 @@ test('hover', async({ page, render }) => {
await component.getByText(addressMock.hash.slice(0, 4)).hover();
await expect(page).toHaveScreenshot();
});
const bech32test = test.extend<{ context: BrowserContext }>({
context: async({ context }, use) => {
context.addCookies([ { name: cookies.NAMES.ADDRESS_FORMAT, value: 'bech32', domain: config.app.host, path: '/' } ]);
use(context);
},
});
bech32test('bech32 format', async({ render, mockEnvs }) => {
await mockEnvs(ENVS_MAP.addressBech32Format);
const component = await render(
<AddressEntity
address={ addressMock.withoutName }
/>,
);
await expect(component).toHaveScreenshot();
});
......@@ -6,7 +6,9 @@ import type { AddressParam } from 'types/api/addressParams';
import { route } from 'nextjs-routes';
import { toBech32Address } from 'lib/address/bech32';
import { useAddressHighlightContext } from 'lib/contexts/addressHighlight';
import { useSettingsContext } from 'lib/contexts/settings';
import * as EntityBase from 'ui/shared/entities/base/components';
import { distributeEntityProps, getIconProps } from '../base/utils';
......@@ -83,7 +85,7 @@ const Icon = (props: IconProps) => {
);
};
export type ContentProps = Omit<EntityBase.ContentBaseProps, 'text'> & Pick<EntityProps, 'address'>;
export type ContentProps = Omit<EntityBase.ContentBaseProps, 'text'> & Pick<EntityProps, 'address'> & { altHash?: string };
const Content = chakra((props: ContentProps) => {
const nameTag = props.address.metadata?.tags.find(tag => tag.tagType === 'name')?.name;
......@@ -99,7 +101,7 @@ const Content = chakra((props: ContentProps) => {
const label = (
<VStack gap={ 0 } py={ 1 } color="inherit">
<Box fontWeight={ 600 } whiteSpace="pre-wrap" wordBreak="break-word">{ nameText }</Box>
<Box whiteSpace="pre-wrap" wordBreak="break-word">{ props.address.filecoin?.robust ?? props.address.hash }</Box>
<Box whiteSpace="pre-wrap" wordBreak="break-word">{ props.address.filecoin?.robust ?? props.altHash ?? props.address.hash }</Box>
</VStack>
);
......@@ -115,18 +117,18 @@ const Content = chakra((props: ContentProps) => {
return (
<EntityBase.Content
{ ...props }
text={ props.address.filecoin?.robust ?? props.address.hash }
text={ props.address.filecoin?.robust ?? props.altHash ?? props.address.hash }
/>
);
});
type CopyProps = Omit<EntityBase.CopyBaseProps, 'text'> & Pick<EntityProps, 'address'>;
type CopyProps = Omit<EntityBase.CopyBaseProps, 'text'> & Pick<EntityProps, 'address'> & { altHash?: string };
const Copy = (props: CopyProps) => {
return (
<EntityBase.Copy
{ ...props }
text={ props.address.filecoin?.robust ?? props.address.hash }
text={ props.address.filecoin?.robust ?? props.altHash ?? props.address.hash }
/>
);
};
......@@ -141,28 +143,31 @@ export interface EntityProps extends EntityBase.EntityBaseProps {
address: AddressProp;
isSafeAddress?: boolean;
noHighlight?: boolean;
noAltHash?: boolean;
}
const AddressEntry = (props: EntityProps) => {
const partsProps = distributeEntityProps(props);
const context = useAddressHighlightContext(props.noHighlight);
const highlightContext = useAddressHighlightContext(props.noHighlight);
const settingsContext = useSettingsContext();
const altHash = !props.noAltHash && settingsContext?.addressFormat === 'bech32' ? toBech32Address(props.address.hash) : undefined;
return (
<Container
// we have to use the global classnames here, see theme/global.ts
// otherwise, if we use sx prop, Chakra will generate the same styles for each instance of the component on the page
className={ `${ props.className } address-entity ${ props.noCopy ? 'address-entity_no-copy' : '' }` }
data-hash={ context && !props.isLoading ? props.address.hash : undefined }
onMouseEnter={ context?.onMouseEnter }
onMouseLeave={ context?.onMouseLeave }
data-hash={ highlightContext && !props.isLoading ? props.address.hash : undefined }
onMouseEnter={ highlightContext?.onMouseEnter }
onMouseLeave={ highlightContext?.onMouseLeave }
position="relative"
zIndex={ 0 }
>
<Icon { ...partsProps.icon }/>
<Link { ...partsProps.link }>
<Content { ...partsProps.content }/>
<Content { ...partsProps.content } altHash={ altHash }/>
</Link>
<Copy { ...partsProps.copy }/>
<Copy { ...partsProps.copy } altHash={ altHash }/>
</Container>
);
};
......
......@@ -32,7 +32,7 @@ const AddressEntityContentProxy = (props: ContentProps) => {
<EntityBase.Content
{ ...props }
truncation={ nameTag || implementationName || props.address.name ? 'tail' : props.truncation }
text={ nameTag || implementationName || props.address.name || props.address.hash }
text={ nameTag || implementationName || props.address.name || props.altHash || props.address.hash }
isTooltipDisabled
/>
</Box>
......
......@@ -7,6 +7,7 @@ import { scroller, Element } from 'react-scroll';
import type { SearchResultItem } from 'types/api/search';
import type { ResourceError } from 'lib/api/resources';
import { useSettingsContext } from 'lib/contexts/settings';
import useIsMobile from 'lib/hooks/useIsMobile';
import * as regexp from 'lib/regexp';
import useMarketplaceApps from 'ui/marketplace/useMarketplaceApps';
......@@ -30,6 +31,7 @@ const SearchBarSuggest = ({ query, searchTerm, onItemClick, containerId }: Props
const isMobile = useIsMobile();
const marketplaceApps = useMarketplaceApps(searchTerm);
const settingsContext = useSettingsContext();
const categoriesRefs = React.useRef<Array<HTMLParagraphElement>>([]);
const tabsRef = React.useRef<HTMLDivElement>(null);
......@@ -165,9 +167,16 @@ const SearchBarSuggest = ({ query, searchTerm, onItemClick, containerId }: Props
>
{ cat.title }
</Text>
{ cat.id !== 'app' && itemsGroups[cat.id]?.map((item, index) =>
<SearchBarSuggestItem key={ index } data={ item } isMobile={ isMobile } searchTerm={ searchTerm } onClick={ onItemClick }/>,
) }
{ cat.id !== 'app' && itemsGroups[cat.id]?.map((item, index) => (
<SearchBarSuggestItem
key={ index }
data={ item }
isMobile={ isMobile }
searchTerm={ searchTerm }
onClick={ onItemClick }
addressFormat={ settingsContext?.addressFormat }
/>
)) }
{ cat.id === 'app' && itemsGroups[cat.id]?.map((item, index) =>
<SearchBarSuggestApp key={ index } data={ item } isMobile={ isMobile } searchTerm={ searchTerm } onClick={ onItemClick }/>,
) }
......
import { chakra, Box, Text, Flex } from '@chakra-ui/react';
import React from 'react';
import type { ItemsProps } from './types';
import type { SearchResultAddressOrContract } from 'types/api/search';
import { toBech32Address } from 'lib/address/bech32';
import dayjs from 'lib/date/dayjs';
import highlightText from 'lib/highlightText';
import ContractCertifiedLabel from 'ui/shared/ContractCertifiedLabel';
......@@ -10,14 +12,9 @@ import * as AddressEntity from 'ui/shared/entities/address/AddressEntity';
import { ADDRESS_REGEXP } from 'ui/shared/forms/validators/address';
import HashStringShortenDynamic from 'ui/shared/HashStringShortenDynamic';
interface Props {
data: SearchResultAddressOrContract;
isMobile: boolean | undefined;
searchTerm: string;
}
const SearchBarSuggestAddress = ({ data, isMobile, searchTerm }: Props) => {
const SearchBarSuggestAddress = ({ data, isMobile, searchTerm, addressFormat }: ItemsProps<SearchResultAddressOrContract>) => {
const shouldHighlightHash = ADDRESS_REGEXP.test(searchTerm);
const hash = data.filecoin_robust_address || (addressFormat === 'bech32' ? toBech32Address(data.address) : data.address);
const icon = (
<AddressEntity.Icon
......@@ -52,7 +49,7 @@ const SearchBarSuggestAddress = ({ data, isMobile, searchTerm }: Props) => {
{ data.certified && <ContractCertifiedLabel boxSize={ 5 } iconSize={ 5 } ml={ 1 }/> }
</Flex>
);
const addressEl = <HashStringShortenDynamic hash={ data.filecoin_robust_address || data.address } isTooltipDisabled/>;
const addressEl = <HashStringShortenDynamic hash={ hash } isTooltipDisabled/>;
if (isMobile) {
return (
......
import { chakra, Flex } from '@chakra-ui/react';
import React from 'react';
import type { ItemsProps } from './types';
import type { SearchResultBlob } from 'types/api/search';
import * as BlobEntity from 'ui/shared/entities/blob/BlobEntity';
import HashStringShortenDynamic from 'ui/shared/HashStringShortenDynamic';
interface Props {
data: SearchResultBlob;
searchTerm: string;
}
const SearchBarSuggestBlob = ({ data }: Props) => {
const SearchBarSuggestBlob = ({ data }: ItemsProps<SearchResultBlob>) => {
return (
<Flex alignItems="center" minW={ 0 }>
<BlobEntity.Icon/>
......
import { Text, Flex, Grid, Tag } from '@chakra-ui/react';
import React from 'react';
import type { ItemsProps } from './types';
import type { SearchResultBlock } from 'types/client/search';
import dayjs from 'lib/date/dayjs';
......@@ -8,13 +9,7 @@ import highlightText from 'lib/highlightText';
import * as BlockEntity from 'ui/shared/entities/block/BlockEntity';
import HashStringShortenDynamic from 'ui/shared/HashStringShortenDynamic';
interface Props {
data: SearchResultBlock;
isMobile: boolean | undefined;
searchTerm: string;
}
const SearchBarSuggestBlock = ({ data, isMobile, searchTerm }: Props) => {
const SearchBarSuggestBlock = ({ data, isMobile, searchTerm }: ItemsProps<SearchResultBlock>) => {
const icon = <BlockEntity.Icon/>;
const shouldHighlightHash = data.block_hash.toLowerCase() === searchTerm.toLowerCase();
const isFutureBlock = data.timestamp === undefined;
......
import { Grid, Text, Flex } from '@chakra-ui/react';
import React from 'react';
import type { ItemsProps } from './types';
import type { SearchResultDomain } from 'types/api/search';
import { toBech32Address } from 'lib/address/bech32';
import dayjs from 'lib/date/dayjs';
import highlightText from 'lib/highlightText';
import HashStringShortenDynamic from 'ui/shared/HashStringShortenDynamic';
import IconSvg from 'ui/shared/IconSvg';
interface Props {
data: SearchResultDomain;
isMobile: boolean | undefined;
searchTerm: string;
}
const SearchBarSuggestDomain = ({ data, isMobile, searchTerm }: Props) => {
const SearchBarSuggestDomain = ({ data, isMobile, searchTerm, addressFormat }: ItemsProps<SearchResultDomain>) => {
const icon = <IconSvg name="ENS_slim" boxSize={ 5 } color="gray.500"/>;
const hash = data.filecoin_robust_address || (addressFormat === 'bech32' ? toBech32Address(data.address) : data.address);
const name = (
<Text
......@@ -34,7 +31,7 @@ const SearchBarSuggestDomain = ({ data, isMobile, searchTerm }: Props) => {
whiteSpace="nowrap"
variant="secondary"
>
<HashStringShortenDynamic hash={ data.filecoin_robust_address || data.address } isTooltipDisabled/>
<HashStringShortenDynamic hash={ hash } isTooltipDisabled/>
</Text>
);
......
......@@ -3,6 +3,7 @@ import NextLink from 'next/link';
import React from 'react';
import type { SearchResultItem } from 'types/client/search';
import type { AddressFormat } from 'types/views/address';
import { route } from 'nextjs-routes';
......@@ -21,9 +22,10 @@ interface Props {
isMobile: boolean | undefined;
searchTerm: string;
onClick: (event: React.MouseEvent<HTMLAnchorElement>) => void;
addressFormat?: AddressFormat;
}
const SearchBarSuggestItem = ({ data, isMobile, searchTerm, onClick }: Props) => {
const SearchBarSuggestItem = ({ data, isMobile, searchTerm, onClick, addressFormat }: Props) => {
const url = (() => {
switch (data.type) {
......@@ -61,15 +63,35 @@ const SearchBarSuggestItem = ({ data, isMobile, searchTerm, onClick }: Props) =>
const content = (() => {
switch (data.type) {
case 'token': {
return <SearchBarSuggestToken data={ data } searchTerm={ searchTerm } isMobile={ isMobile }/>;
return (
<SearchBarSuggestToken
data={ data }
searchTerm={ searchTerm }
isMobile={ isMobile }
addressFormat={ addressFormat }
/>
);
}
case 'contract':
case 'address': {
return <SearchBarSuggestAddress data={ data } searchTerm={ searchTerm } isMobile={ isMobile }/>;
return (
<SearchBarSuggestAddress
data={ data }
searchTerm={ searchTerm }
isMobile={ isMobile }
addressFormat={ addressFormat }
/>
);
}
case 'label': {
return <SearchBarSuggestLabel data={ data } searchTerm={ searchTerm } isMobile={ isMobile }/>;
return (
<SearchBarSuggestLabel
data={ data }
searchTerm={ searchTerm }
isMobile={ isMobile }
addressFormat={ addressFormat }
/>
);
}
case 'block': {
return <SearchBarSuggestBlock data={ data } searchTerm={ searchTerm } isMobile={ isMobile }/>;
......@@ -84,7 +106,7 @@ const SearchBarSuggestItem = ({ data, isMobile, searchTerm, onClick }: Props) =>
return <SearchBarSuggestBlob data={ data } searchTerm={ searchTerm }/>;
}
case 'ens_domain': {
return <SearchBarSuggestDomain data={ data } searchTerm={ searchTerm } isMobile={ isMobile }/>;
return <SearchBarSuggestDomain data={ data } searchTerm={ searchTerm } isMobile={ isMobile } addressFormat={ addressFormat }/>;
}
}
})();
......
import { Grid, Text, Flex } from '@chakra-ui/react';
import React from 'react';
import type { ItemsProps } from './types';
import type { SearchResultLabel } from 'types/api/search';
import { toBech32Address } from 'lib/address/bech32';
import highlightText from 'lib/highlightText';
import HashStringShortenDynamic from 'ui/shared/HashStringShortenDynamic';
import IconSvg from 'ui/shared/IconSvg';
interface Props {
data: SearchResultLabel;
isMobile: boolean | undefined;
searchTerm: string;
}
const SearchBarSuggestLabel = ({ data, isMobile, searchTerm }: Props) => {
const SearchBarSuggestLabel = ({ data, isMobile, searchTerm, addressFormat }: ItemsProps<SearchResultLabel>) => {
const icon = <IconSvg name="publictags_slim" boxSize={ 5 } color="gray.500"/>;
const hash = data.filecoin_robust_address || (addressFormat === 'bech32' ? toBech32Address(data.address) : data.address);
const name = (
<Text
......@@ -33,7 +30,7 @@ const SearchBarSuggestLabel = ({ data, isMobile, searchTerm }: Props) => {
whiteSpace="nowrap"
variant="secondary"
>
<HashStringShortenDynamic hash={ data.filecoin_robust_address || data.address } isTooltipDisabled/>
<HashStringShortenDynamic hash={ hash } isTooltipDisabled/>
</Text>
);
......
import { Grid, Text, Flex } from '@chakra-ui/react';
import React from 'react';
import type { ItemsProps } from './types';
import type { SearchResultToken } from 'types/api/search';
import { toBech32Address } from 'lib/address/bech32';
import highlightText from 'lib/highlightText';
import * as TokenEntity from 'ui/shared/entities/token/TokenEntity';
import HashStringShortenDynamic from 'ui/shared/HashStringShortenDynamic';
import IconSvg from 'ui/shared/IconSvg';
interface Props {
data: SearchResultToken;
isMobile: boolean | undefined;
searchTerm: string;
}
const SearchBarSuggestToken = ({ data, isMobile, searchTerm }: Props) => {
const SearchBarSuggestToken = ({ data, isMobile, searchTerm, addressFormat }: ItemsProps<SearchResultToken>) => {
const icon = <TokenEntity.Icon token={{ ...data, type: data.token_type }}/>;
const verifiedIcon = <IconSvg name="certified" boxSize={ 4 } color="green.500" ml={ 1 }/>;
const hash = data.filecoin_robust_address || (addressFormat === 'bech32' ? toBech32Address(data.address) : data.address);
const name = (
<Text
fontWeight={ 700 }
......@@ -30,7 +28,7 @@ const SearchBarSuggestToken = ({ data, isMobile, searchTerm }: Props) => {
const address = (
<Text variant="secondary" whiteSpace="nowrap" overflow="hidden">
<HashStringShortenDynamic hash={ data.filecoin_robust_address || data.address } isTooltipDisabled/>
<HashStringShortenDynamic hash={ hash } isTooltipDisabled/>
</Text>
);
......
import { chakra, Text, Flex } from '@chakra-ui/react';
import React from 'react';
import type { ItemsProps } from './types';
import type { SearchResultTx } from 'types/api/search';
import dayjs from 'lib/date/dayjs';
import * as TxEntity from 'ui/shared/entities/tx/TxEntity';
import HashStringShortenDynamic from 'ui/shared/HashStringShortenDynamic';
interface Props {
data: SearchResultTx;
isMobile: boolean | undefined;
searchTerm: string;
}
const SearchBarSuggestTx = ({ data, isMobile }: Props) => {
const SearchBarSuggestTx = ({ data, isMobile }: ItemsProps<SearchResultTx>) => {
const icon = <TxEntity.Icon/>;
const hash = (
<chakra.mark overflow="hidden" whiteSpace="nowrap" fontWeight={ 700 }>
......
import { chakra, Text, Flex } from '@chakra-ui/react';
import React from 'react';
import type { ItemsProps } from './types';
import type { SearchResultUserOp } from 'types/api/search';
import dayjs from 'lib/date/dayjs';
import * as UserOpEntity from 'ui/shared/entities/userOp/UserOpEntity';
import HashStringShortenDynamic from 'ui/shared/HashStringShortenDynamic';
interface Props {
data: SearchResultUserOp;
isMobile: boolean | undefined;
searchTerm: string;
}
const SearchBarSuggestTx = ({ data, isMobile }: Props) => {
const SearchBarSuggestUserOp = ({ data, isMobile }: ItemsProps<SearchResultUserOp>) => {
const icon = <UserOpEntity.Icon/>;
const hash = (
<chakra.mark overflow="hidden" whiteSpace="nowrap" fontWeight={ 700 }>
......@@ -45,4 +40,4 @@ const SearchBarSuggestTx = ({ data, isMobile }: Props) => {
);
};
export default React.memo(SearchBarSuggestTx);
export default React.memo(SearchBarSuggestUserOp);
import type { AddressFormat } from 'types/views/address';
export interface ItemsProps<Data> {
data: Data;
searchTerm: string;
isMobile?: boolean | undefined;
addressFormat?: AddressFormat;
}
import React from 'react';
import { isBech32Address, fromBech32Address } from 'lib/address/bech32';
import useApiQuery from 'lib/api/useApiQuery';
import useDebounce from 'lib/hooks/useDebounce';
......@@ -9,7 +10,7 @@ export default function useQuickSearchQuery() {
const debouncedSearchTerm = useDebounce(searchTerm, 300);
const query = useApiQuery('quick_search', {
queryParams: { q: debouncedSearchTerm },
queryParams: { q: isBech32Address(debouncedSearchTerm) ? fromBech32Address(debouncedSearchTerm) : debouncedSearchTerm },
queryOptions: { enabled: debouncedSearchTerm.trim().length > 0 },
});
......
import { useRouter } from 'next/router';
import React from 'react';
import { fromBech32Address, isBech32Address } from 'lib/address/bech32';
import useApiQuery from 'lib/api/useApiQuery';
import useDebounce from 'lib/hooks/useDebounce';
import useUpdateValueEffect from 'lib/hooks/useUpdateValueEffect';
......@@ -21,7 +22,7 @@ export default function useSearchQuery(withRedirectCheck?: boolean) {
const query = useQueryWithPages({
resourceName: 'search',
filters: { q: debouncedSearchTerm },
filters: { q: isBech32Address(debouncedSearchTerm) ? fromBech32Address(debouncedSearchTerm) : debouncedSearchTerm },
options: {
enabled: debouncedSearchTerm.trim().length > 0,
placeholderData: generateListStub<'search'>(SEARCH_RESULT_ITEM, 50, { next_page_params: SEARCH_RESULT_NEXT_PAGE_PARAMS }),
......
......@@ -4,6 +4,7 @@ import React from 'react';
import Popover from 'ui/shared/chakra/Popover';
import IconSvg from 'ui/shared/IconSvg';
import SettingsAddressFormat from './SettingsAddressFormat';
import SettingsColorTheme from './SettingsColorTheme';
import SettingsIdentIcon from './SettingsIdentIcon';
......@@ -28,6 +29,7 @@ const Settings = () => {
<SettingsColorTheme onSelect={ onClose }/>
<Box borderColor="divider" borderWidth="1px" my={ 3 }/>
<SettingsIdentIcon/>
<SettingsAddressFormat/>
</PopoverBody>
</PopoverContent>
</Popover>
......
import { FormLabel, FormControl, Switch } from '@chakra-ui/react';
import React from 'react';
import config from 'configs/app';
import { BECH_32_SEPARATOR } from 'lib/address/bech32';
import { useSettingsContext } from 'lib/contexts/settings';
const SettingsAddressFormat = () => {
const settingsContext = useSettingsContext();
if (!settingsContext || config.UI.views.address.hashFormat.availableFormats.length < 2) {
return null;
}
const { addressFormat, toggleAddressFormat } = settingsContext;
return (
<FormControl display="flex" alignItems="center" columnGap={ 2 } mt={ 4 }>
<FormLabel htmlFor="address-format" m="0" fontWeight={ 400 } fontSize="sm" lineHeight={ 5 }>
Show { config.UI.views.address.hashFormat.bech32Prefix }{ BECH_32_SEPARATOR } format
</FormLabel>
<Switch id="address-format" defaultChecked={ addressFormat === 'bech32' } onChange={ toggleAddressFormat }/>
</FormControl>
);
};
export default React.memo(SettingsAddressFormat);
......@@ -32,7 +32,7 @@ const SettingsIdentIcon = () => {
return (
<div>
<Box fontWeight={ 600 }>Address identicon</Box>
<Box fontWeight={ 600 }>Address settings</Box>
<Box color="text_secondary" mt={ 1 } mb={ 2 }>{ activeIdenticon?.label }</Box>
<Flex>
{ IDENTICONS.map((identicon) => (
......
......@@ -42,6 +42,7 @@ const UserProfileContentWallet = ({ onClose, className }: Props) => {
truncation="dynamic"
fontSize="sm"
fontWeight={ 500 }
noAltHash
/>
<IconButton
aria-label="Open wallet"
......
......@@ -8,9 +8,7 @@ import config from 'configs/app';
import { CONTRACT_LICENSES } from 'lib/contracts/licenses';
import { currencyUnits } from 'lib/units';
import ContractCertifiedLabel from 'ui/shared/ContractCertifiedLabel';
import CopyToClipboard from 'ui/shared/CopyToClipboard';
import AddressEntity from 'ui/shared/entities/address/AddressEntity';
import HashStringShorten from 'ui/shared/HashStringShorten';
import IconSvg from 'ui/shared/IconSvg';
import ListItemMobile from 'ui/shared/ListItemMobile/ListItemMobile';
import TimeAgoWithTooltip from 'ui/shared/TimeAgoWithTooltip';
......@@ -46,10 +44,16 @@ const VerifiedContractsListItem = ({ data, isLoading }: Props) => {
/>
{ data.certified && <ContractCertifiedLabel iconSize={ 5 } boxSize={ 5 } mx={ 2 }/> }
</Flex>
<Skeleton isLoaded={ !isLoading } color="text_secondary" ml="auto">
<HashStringShorten hash={ data.address.filecoin?.robust ?? data.address.hash } isTooltipDisabled/>
</Skeleton>
<CopyToClipboard text={ data.address.filecoin?.robust ?? data.address.hash } isLoading={ isLoading }/>
<AddressEntity
address={{ hash: data.address.filecoin?.robust ?? data.address.hash }}
isLoading={ isLoading }
noLink
noIcon
truncation="constant"
ml="auto"
color="text_secondary"
flexShrink={ 0 }
/>
</Flex>
<Flex columnGap={ 3 }>
<Skeleton isLoaded={ !isLoading } fontWeight={ 500 }>Balance { currencyUnits.ether }</Skeleton>
......
......@@ -7,9 +7,7 @@ import type { VerifiedContract } from 'types/api/contracts';
import config from 'configs/app';
import { CONTRACT_LICENSES } from 'lib/contracts/licenses';
import ContractCertifiedLabel from 'ui/shared/ContractCertifiedLabel';
import CopyToClipboard from 'ui/shared/CopyToClipboard';
import AddressEntity from 'ui/shared/entities/address/AddressEntity';
import HashStringShorten from 'ui/shared/HashStringShorten';
import IconSvg from 'ui/shared/IconSvg';
import TimeAgoWithTooltip from 'ui/shared/TimeAgoWithTooltip';
......@@ -44,12 +42,16 @@ const VerifiedContractsTableItem = ({ data, isLoading }: Props) => {
/>
{ data.certified && <ContractCertifiedLabel iconSize={ 5 } boxSize={ 5 } ml={ 2 }/> }
</Flex>
<Flex alignItems="center" ml={ 7 }>
<Skeleton isLoaded={ !isLoading } color="text_secondary" my={ 1 }>
<HashStringShorten hash={ data.address.filecoin?.robust ?? data.address.hash } isTooltipDisabled/>
</Skeleton>
<CopyToClipboard text={ data.address.filecoin?.robust ?? data.address.hash } isLoading={ isLoading }/>
</Flex>
<AddressEntity
address={{ hash: data.address.filecoin?.robust ?? data.address.hash }}
isLoading={ isLoading }
noLink
noIcon
truncation="constant"
my={ 1 }
ml={ 7 }
color="text_secondary"
/>
</Td>
<Td isNumeric>
<Skeleton isLoaded={ !isLoading } display="inline-block" my={ 1 }>
......
......@@ -4849,6 +4849,11 @@
dependencies:
cross-fetch "^3.1.5"
"@scure/base@1.1.9":
version "1.1.9"
resolved "https://registry.yarnpkg.com/@scure/base/-/base-1.1.9.tgz#e5e142fbbfe251091f9c5f1dd4c834ac04c3dbd1"
integrity sha512-8YKhl8GHiNI/pU2VMaofa2Tor7PJRAjwQLBBuilkJ9L5+13yVbC7JO/wS7piioAvPSwR3JKM1IJ/u4xQzbcXKg==
"@scure/base@^1.1.3", "@scure/base@~1.1.2":
version "1.1.5"
resolved "https://registry.yarnpkg.com/@scure/base/-/base-1.1.5.tgz#1d85d17269fe97694b9c592552dd9e5e33552157"
......
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