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 type { SmartContractVerificationMethodExtra } from 'types/client/contract';
import { SMART_CONTRACT_EXTRA_VERIFICATION_METHODS } from 'types/client/contract'; import { SMART_CONTRACT_EXTRA_VERIFICATION_METHODS } from 'types/client/contract';
import type { AddressViewId, IdenticonType } from 'types/views/address'; import type { AddressFormat, AddressViewId, IdenticonType } from 'types/views/address';
import { ADDRESS_VIEWS_IDS, IDENTICON_TYPES } from 'types/views/address'; import { ADDRESS_FORMATS, ADDRESS_VIEWS_IDS, IDENTICON_TYPES } from 'types/views/address';
import { getEnvValue, parseEnvJson } from 'configs/app/utils'; import { getEnvValue, parseEnvJson } from 'configs/app/utils';
...@@ -11,6 +11,28 @@ const identiconType: IdenticonType = (() => { ...@@ -11,6 +11,28 @@ const identiconType: IdenticonType = (() => {
return IDENTICON_TYPES.find((type) => value === type) || 'jazzicon'; 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 hiddenViews = (() => {
const parsedValue = parseEnvJson<Array<AddressViewId>>(getEnvValue('NEXT_PUBLIC_VIEWS_ADDRESS_HIDDEN_VIEWS')) || []; const parsedValue = parseEnvJson<Array<AddressViewId>>(getEnvValue('NEXT_PUBLIC_VIEWS_ADDRESS_HIDDEN_VIEWS')) || [];
...@@ -43,6 +65,10 @@ const extraVerificationMethods: Array<SmartContractVerificationMethodExtra> = (( ...@@ -43,6 +65,10 @@ const extraVerificationMethods: Array<SmartContractVerificationMethodExtra> = ((
const config = Object.freeze({ const config = Object.freeze({
identiconType, identiconType,
hashFormat: {
availableFormats: formats,
bech32Prefix,
},
hiddenViews, hiddenViews,
solidityscanEnabled: getEnvValue('NEXT_PUBLIC_VIEWS_CONTRACT_SOLIDITYSCAN_ENABLED') === 'true', solidityscanEnabled: getEnvValue('NEXT_PUBLIC_VIEWS_CONTRACT_SOLIDITYSCAN_ENABLED') === 'true',
extraVerificationMethods, extraVerificationMethods,
......
...@@ -54,3 +54,5 @@ NEXT_PUBLIC_METADATA_SERVICE_API_HOST=http://localhost:3007 ...@@ -54,3 +54,5 @@ NEXT_PUBLIC_METADATA_SERVICE_API_HOST=http://localhost:3007
NEXT_PUBLIC_NAME_SERVICE_API_HOST=http://localhost:3008 NEXT_PUBLIC_NAME_SERVICE_API_HOST=http://localhost:3008
NEXT_PUBLIC_RE_CAPTCHA_V3_APP_SITE_KEY=xxx NEXT_PUBLIC_RE_CAPTCHA_V3_APP_SITE_KEY=xxx
NEXT_PUBLIC_WALLET_CONNECT_PROJECT_ID=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 ...@@ -35,8 +35,8 @@ import type { ChainIndicatorId, HeroBannerButtonState, HeroBannerConfig, HomeSta
import { type NetworkVerificationTypeEnvs, type NetworkExplorer, type FeaturedNetwork, NETWORK_GROUPS } from '../../../types/networks'; import { type NetworkVerificationTypeEnvs, type NetworkExplorer, type FeaturedNetwork, NETWORK_GROUPS } from '../../../types/networks';
import { COLOR_THEME_IDS } from '../../../types/settings'; import { COLOR_THEME_IDS } from '../../../types/settings';
import type { FontFamily } from '../../../types/ui'; import type { FontFamily } from '../../../types/ui';
import type { AddressViewId } from '../../../types/views/address'; import type { AddressFormat, AddressViewId } from '../../../types/views/address';
import { ADDRESS_VIEWS_IDS, IDENTICON_TYPES } 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 { BLOCK_FIELDS_IDS } from '../../../types/views/block';
import type { BlockFieldId } from '../../../types/views/block'; import type { BlockFieldId } from '../../../types/views/block';
import type { NftMarketplaceItem } from '../../../types/views/nft'; import type { NftMarketplaceItem } from '../../../types/views/nft';
...@@ -658,6 +658,19 @@ const schema = yup ...@@ -658,6 +658,19 @@ const schema = yup
.json() .json()
.of(yup.string<BlockFieldId>().oneOf(BLOCK_FIELDS_IDS)), .of(yup.string<BlockFieldId>().oneOf(BLOCK_FIELDS_IDS)),
NEXT_PUBLIC_VIEWS_ADDRESS_IDENTICON_TYPE: yup.string().oneOf(IDENTICON_TYPES), 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 NEXT_PUBLIC_VIEWS_ADDRESS_HIDDEN_VIEWS: yup
.array() .array()
.transform(replaceQuotes) .transform(replaceQuotes)
......
...@@ -2,3 +2,5 @@ NEXT_PUBLIC_GRAPHIQL_TRANSACTION=none ...@@ -2,3 +2,5 @@ NEXT_PUBLIC_GRAPHIQL_TRANSACTION=none
NEXT_PUBLIC_API_SPEC_URL=none NEXT_PUBLIC_API_SPEC_URL=none
NEXT_PUBLIC_VIEWS_CONTRACT_EXTRA_VERIFICATION_METHODS=none NEXT_PUBLIC_VIEWS_CONTRACT_EXTRA_VERIFICATION_METHODS=none
NEXT_PUBLIC_HOMEPAGE_STATS=[] 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 ...@@ -70,6 +70,7 @@ NEXT_PUBLIC_STATS_API_HOST=https://example.com
NEXT_PUBLIC_STATS_API_BASE_PATH=/ NEXT_PUBLIC_STATS_API_BASE_PATH=/
NEXT_PUBLIC_USE_NEXT_JS_PROXY=false NEXT_PUBLIC_USE_NEXT_JS_PROXY=false
NEXT_PUBLIC_VIEWS_ADDRESS_IDENTICON_TYPE=gradient_avatar 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_ADDRESS_HIDDEN_VIEWS=['top_accounts']
NEXT_PUBLIC_VIEWS_CONTRACT_EXTRA_VERIFICATION_METHODS=['solidity-hardhat','solidity-foundry'] NEXT_PUBLIC_VIEWS_CONTRACT_EXTRA_VERIFICATION_METHODS=['solidity-hardhat','solidity-foundry']
NEXT_PUBLIC_VIEWS_BLOCK_HIDDEN_FIELDS=['burnt_fees','total_reward'] NEXT_PUBLIC_VIEWS_BLOCK_HIDDEN_FIELDS=['burnt_fees','total_reward']
......
...@@ -235,6 +235,8 @@ Settings for meta tags, OG tags and SEO ...@@ -235,6 +235,8 @@ Settings for meta tags, OG tags and SEO
| Variable | Type | Description | Compulsoriness | Default value | Example value | Version | | 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_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_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_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+ | | 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'; ...@@ -5,7 +5,7 @@ import hexToBytes from 'lib/hexToBytes';
import removeNonSignificantZeroBytes from './removeNonSignificantZeroBytes'; import removeNonSignificantZeroBytes from './removeNonSignificantZeroBytes';
export default function guessDataType(data: string) { export default function guessDataType(data: string) {
const bytes = new Uint8Array(hexToBytes(data)); const bytes = hexToBytes(data);
const filteredBytes = removeNonSignificantZeroBytes(bytes); const filteredBytes = removeNonSignificantZeroBytes(bytes);
return filetype(filteredBytes)[0]; 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 { ...@@ -11,6 +11,7 @@ export enum NAMES {
COLOR_MODE='chakra-ui-color-mode', COLOR_MODE='chakra-ui-color-mode',
COLOR_MODE_HEX='chakra-ui-color-mode-hex', COLOR_MODE_HEX='chakra-ui-color-mode-hex',
ADDRESS_IDENTICON_TYPE='address_identicon_type', ADDRESS_IDENTICON_TYPE='address_identicon_type',
ADDRESS_FORMAT='address_format',
INDEXING_ALERT='indexing_alert', INDEXING_ALERT='indexing_alert',
ADBLOCK_DETECTED='adblock_detected', ADBLOCK_DETECTED='adblock_detected',
MIXPANEL_DEBUG='_mixpanel_debug', MIXPANEL_DEBUG='_mixpanel_debug',
......
...@@ -2,7 +2,7 @@ import bytesToBase64 from './bytesToBase64'; ...@@ -2,7 +2,7 @@ import bytesToBase64 from './bytesToBase64';
import hexToBytes from './hexToBytes'; import hexToBytes from './hexToBytes';
export default function hexToBase64(hex: string) { export default function hexToBase64(hex: string) {
const bytes = new Uint8Array(hexToBytes(hex)); const bytes = hexToBytes(hex);
return bytesToBase64(bytes); return bytesToBase64(bytes);
} }
...@@ -5,5 +5,5 @@ export default function hexToBytes(hex: string) { ...@@ -5,5 +5,5 @@ export default function hexToBytes(hex: string) {
for (let c = startIndex; c < hex.length; c += 2) { for (let c = startIndex; c < hex.length; c += 2) {
bytes.push(parseInt(hex.substring(c, c + 2), 16)); bytes.push(parseInt(hex.substring(c, c + 2), 16));
} }
return bytes; return new Uint8Array(bytes);
} }
...@@ -2,7 +2,7 @@ import hexToBytes from 'lib/hexToBytes'; ...@@ -2,7 +2,7 @@ import hexToBytes from 'lib/hexToBytes';
export default function hexToUtf8(hex: string) { export default function hexToUtf8(hex: string) {
const utf8decoder = new TextDecoder(); const utf8decoder = new TextDecoder();
const bytes = new Uint8Array(hexToBytes(hex)); const bytes = hexToBytes(hex);
return utf8decoder.decode(bytes); return utf8decoder.decode(bytes);
} }
...@@ -22,6 +22,7 @@ export function middleware(req: NextRequest) { ...@@ -22,6 +22,7 @@ export function middleware(req: NextRequest) {
const res = NextResponse.next(); const res = NextResponse.next();
middlewares.colorTheme(req, res); middlewares.colorTheme(req, res);
middlewares.addressFormat(req, res);
const end = Date.now(); 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 { account } from './account';
export { default as colorTheme } from './colorTheme'; export { default as colorTheme } from './colorTheme';
export { default as addressFormat } from './addressFormat';
...@@ -15,6 +15,7 @@ import { ChakraProvider } from 'lib/contexts/chakra'; ...@@ -15,6 +15,7 @@ import { ChakraProvider } from 'lib/contexts/chakra';
import { MarketplaceContextProvider } from 'lib/contexts/marketplace'; import { MarketplaceContextProvider } from 'lib/contexts/marketplace';
import { RewardsContextProvider } from 'lib/contexts/rewards'; import { RewardsContextProvider } from 'lib/contexts/rewards';
import { ScrollDirectionProvider } from 'lib/contexts/scrollDirection'; import { ScrollDirectionProvider } from 'lib/contexts/scrollDirection';
import { SettingsContextProvider } from 'lib/contexts/settings';
import { growthBook } from 'lib/growthbook/init'; import { growthBook } from 'lib/growthbook/init';
import useLoadFeatures from 'lib/growthbook/useLoadFeatures'; import useLoadFeatures from 'lib/growthbook/useLoadFeatures';
import useNotifyOnNavigation from 'lib/hooks/useNotifyOnNavigation'; import useNotifyOnNavigation from 'lib/hooks/useNotifyOnNavigation';
...@@ -73,8 +74,10 @@ function MyApp({ Component, pageProps }: AppPropsWithLayout) { ...@@ -73,8 +74,10 @@ function MyApp({ Component, pageProps }: AppPropsWithLayout) {
<SocketProvider url={ `${ config.api.socket }${ config.api.basePath }/socket/v2` }> <SocketProvider url={ `${ config.api.socket }${ config.api.basePath }/socket/v2` }>
<RewardsContextProvider> <RewardsContextProvider>
<MarketplaceContextProvider> <MarketplaceContextProvider>
<SettingsContextProvider>
{ getLayout(<Component { ...pageProps }/>) } { getLayout(<Component { ...pageProps }/>) }
{ config.features.rewards.isEnabled && <RewardsLoginModal/> } { config.features.rewards.isEnabled && <RewardsLoginModal/> }
</SettingsContextProvider>
</MarketplaceContextProvider> </MarketplaceContextProvider>
</RewardsContextProvider> </RewardsContextProvider>
</SocketProvider> </SocketProvider>
......
...@@ -12,6 +12,7 @@ import config from 'configs/app'; ...@@ -12,6 +12,7 @@ import config from 'configs/app';
import { AppContextProvider } from 'lib/contexts/app'; import { AppContextProvider } from 'lib/contexts/app';
import { MarketplaceContext } from 'lib/contexts/marketplace'; import { MarketplaceContext } from 'lib/contexts/marketplace';
import { RewardsContextProvider } from 'lib/contexts/rewards'; import { RewardsContextProvider } from 'lib/contexts/rewards';
import { SettingsContextProvider } from 'lib/contexts/settings';
import { SocketProvider } from 'lib/socket/context'; import { SocketProvider } from 'lib/socket/context';
import currentChain from 'lib/web3/currentChain'; import currentChain from 'lib/web3/currentChain';
import theme from 'theme/theme'; import theme from 'theme/theme';
...@@ -76,6 +77,7 @@ const TestApp = ({ children, withSocket, appContext = defaultAppContext, marketp ...@@ -76,6 +77,7 @@ const TestApp = ({ children, withSocket, appContext = defaultAppContext, marketp
<SocketProvider url={ withSocket ? `ws://${ config.app.host }:${ socketPort }` : undefined }> <SocketProvider url={ withSocket ? `ws://${ config.app.host }:${ socketPort }` : undefined }>
<AppContextProvider { ...appContext }> <AppContextProvider { ...appContext }>
<MarketplaceContext.Provider value={ marketplaceContext }> <MarketplaceContext.Provider value={ marketplaceContext }>
<SettingsContextProvider>
<GrowthBookProvider> <GrowthBookProvider>
<WagmiProvider config={ wagmiConfig }> <WagmiProvider config={ wagmiConfig }>
<RewardsContextProvider> <RewardsContextProvider>
...@@ -83,6 +85,7 @@ const TestApp = ({ children, withSocket, appContext = defaultAppContext, marketp ...@@ -83,6 +85,7 @@ const TestApp = ({ children, withSocket, appContext = defaultAppContext, marketp
</RewardsContextProvider> </RewardsContextProvider>
</WagmiProvider> </WagmiProvider>
</GrowthBookProvider> </GrowthBookProvider>
</SettingsContextProvider>
</MarketplaceContext.Provider> </MarketplaceContext.Provider>
</AppContextProvider> </AppContextProvider>
</SocketProvider> </SocketProvider>
......
...@@ -84,4 +84,8 @@ export const ENVS_MAP: Record<string, Array<[string, string]>> = { ...@@ -84,4 +84,8 @@ export const ENVS_MAP: Record<string, Array<[string, string]>> = {
rewardsService: [ rewardsService: [
[ 'NEXT_PUBLIC_REWARDS_SERVICE_API_HOST', 'http://localhost:3003' ], [ '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 = [ ...@@ -14,3 +14,6 @@ export const ADDRESS_VIEWS_IDS = [
] as const; ] as const;
export type AddressViewId = ArrayElement<typeof ADDRESS_VIEWS_IDS>; 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'; ...@@ -18,6 +18,7 @@ import AddressEntity from 'ui/shared/entities/address/AddressEntity';
import BlockEntity from 'ui/shared/entities/block/BlockEntity'; import BlockEntity from 'ui/shared/entities/block/BlockEntity';
import TxEntity from 'ui/shared/entities/tx/TxEntity'; import TxEntity from 'ui/shared/entities/tx/TxEntity';
import AddressAlternativeFormat from './details/AddressAlternativeFormat';
import AddressBalance from './details/AddressBalance'; import AddressBalance from './details/AddressBalance';
import AddressImplementations from './details/AddressImplementations'; import AddressImplementations from './details/AddressImplementations';
import AddressNameInfo from './details/AddressNameInfo'; import AddressNameInfo from './details/AddressNameInfo';
...@@ -98,6 +99,8 @@ const AddressDetails = ({ addressQuery, scrollRef }: Props) => { ...@@ -98,6 +99,8 @@ const AddressDetails = ({ addressQuery, scrollRef }: Props) => {
rowGap={{ base: 1, lg: 3 }} rowGap={{ base: 1, lg: 3 }}
templateColumns={{ base: 'minmax(0, 1fr)', lg: 'auto minmax(0, 1fr)' }} overflow="hidden" templateColumns={{ base: 'minmax(0, 1fr)', lg: 'auto minmax(0, 1fr)' }} overflow="hidden"
> >
<AddressAlternativeFormat isLoading={ addressQuery.isPlaceholderData } addressHash={ addressHash }/>
{ data.filecoin?.id && ( { data.filecoin?.id && (
<> <>
<DetailsInfoItem.Label <DetailsInfoItem.Label
......
...@@ -42,6 +42,7 @@ const ContractConnectWallet = ({ isLoading }: Props) => { ...@@ -42,6 +42,7 @@ const ContractConnectWallet = ({ isLoading }: Props) => {
truncation={ isMobile ? 'constant' : 'dynamic' } truncation={ isMobile ? 'constant' : 'dynamic' }
fontWeight={ 600 } fontWeight={ 600 }
ml={ 2 } ml={ 2 }
noAltHash
/> />
</Flex> </Flex>
<Button onClick={ web3Wallet.disconnect } size="sm" variant="outline">Disconnect</Button> <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) => { ...@@ -50,7 +50,7 @@ const BlobData = ({ data, isLoading, hash }: Props) => {
const fileBlob = (() => { const fileBlob = (() => {
switch (format) { switch (format) {
case 'Image': { case 'Image': {
const bytes = new Uint8Array(hexToBytes(data)); const bytes = hexToBytes(data);
const filteredBytes = removeNonSignificantZeroBytes(bytes); const filteredBytes = removeNonSignificantZeroBytes(bytes);
return new Blob([ filteredBytes ], { type: guessedType?.mime }); return new Blob([ filteredBytes ], { type: guessedType?.mime });
} }
...@@ -77,7 +77,7 @@ const BlobData = ({ data, isLoading, hash }: Props) => { ...@@ -77,7 +77,7 @@ const BlobData = ({ data, isLoading, hash }: Props) => {
return <RawDataSnippet data="Not an image" showCopy={ false } isLoading={ isLoading }/>; 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 filteredBytes = removeNonSignificantZeroBytes(bytes);
const base64 = bytesToBase64(filteredBytes); const base64 = bytesToBase64(filteredBytes);
......
...@@ -35,6 +35,7 @@ const MyProfileWallet = ({ profileQuery, onAddWallet }: Props) => { ...@@ -35,6 +35,7 @@ const MyProfileWallet = ({ profileQuery, onAddWallet }: Props) => {
<AddressEntity <AddressEntity
address={{ hash: profileQuery.data.address_hash }} address={{ hash: profileQuery.data.address_hash }}
fontWeight="500" fontWeight="500"
noAltHash
/> />
</Box> </Box>
) : <Button size="sm" onClick={ onAddWallet }>Link wallet</Button> } ) : <Button size="sm" onClick={ onAddWallet }>Link wallet</Button> }
......
...@@ -41,6 +41,7 @@ import AddressQrCode from 'ui/address/details/AddressQrCode'; ...@@ -41,6 +41,7 @@ import AddressQrCode from 'ui/address/details/AddressQrCode';
import AddressEnsDomains from 'ui/address/ensDomains/AddressEnsDomains'; import AddressEnsDomains from 'ui/address/ensDomains/AddressEnsDomains';
import SolidityscanReport from 'ui/address/SolidityscanReport'; import SolidityscanReport from 'ui/address/SolidityscanReport';
import useAddressQuery from 'ui/address/utils/useAddressQuery'; import useAddressQuery from 'ui/address/utils/useAddressQuery';
import useCheckAddressFormat from 'ui/address/utils/useCheckAddressFormat';
import useCheckDomainNameParam from 'ui/address/utils/useCheckDomainNameParam'; import useCheckDomainNameParam from 'ui/address/utils/useCheckDomainNameParam';
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';
...@@ -69,7 +70,9 @@ const AddressPageContent = () => { ...@@ -69,7 +70,9 @@ const AddressPageContent = () => {
const hash = getQueryParamString(router.query.hash); const hash = getQueryParamString(router.query.hash);
const checkSummedHash = React.useMemo(() => getCheckedSummedAddress(hash), [ 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 addressQuery = useAddressQuery({ hash, isEnabled: areQueriesEnabled });
const addressTabsCountersQuery = useApiQuery('address_tabs_counters', { const addressTabsCountersQuery = useApiQuery('address_tabs_counters', {
......
...@@ -6,6 +6,7 @@ import React from 'react'; ...@@ -6,6 +6,7 @@ import React from 'react';
import type { SearchResultItem } from 'types/client/search'; import type { SearchResultItem } from 'types/client/search';
import config from 'configs/app'; import config from 'configs/app';
import { useSettingsContext } from 'lib/contexts/settings';
import * as regexp from 'lib/regexp'; import * as regexp from 'lib/regexp';
import getQueryParamString from 'lib/router/getQueryParamString'; import getQueryParamString from 'lib/router/getQueryParamString';
import removeQueryParam from 'lib/router/removeQueryParam'; import removeQueryParam from 'lib/router/removeQueryParam';
...@@ -36,6 +37,7 @@ const SearchResultsPageContent = () => { ...@@ -36,6 +37,7 @@ const SearchResultsPageContent = () => {
const [ showContent, setShowContent ] = React.useState(!withRedirectCheck); const [ showContent, setShowContent ] = React.useState(!withRedirectCheck);
const marketplaceApps = useMarketplaceApps(debouncedSearchTerm); const marketplaceApps = useMarketplaceApps(debouncedSearchTerm);
const settingsContext = useSettingsContext();
React.useEffect(() => { React.useEffect(() => {
if (showContent) { if (showContent) {
...@@ -144,6 +146,7 @@ const SearchResultsPageContent = () => { ...@@ -144,6 +146,7 @@ const SearchResultsPageContent = () => {
data={ item } data={ item }
searchTerm={ debouncedSearchTerm } searchTerm={ debouncedSearchTerm }
isLoading={ isLoading } isLoading={ isLoading }
addressFormat={ settingsContext?.addressFormat }
/> />
)) } )) }
</Show> </Show>
...@@ -164,6 +167,7 @@ const SearchResultsPageContent = () => { ...@@ -164,6 +167,7 @@ const SearchResultsPageContent = () => {
data={ item } data={ item }
searchTerm={ debouncedSearchTerm } searchTerm={ debouncedSearchTerm }
isLoading={ isLoading } isLoading={ isLoading }
addressFormat={ settingsContext?.addressFormat }
/> />
)) } )) }
</Tbody> </Tbody>
......
...@@ -3,9 +3,11 @@ import React from 'react'; ...@@ -3,9 +3,11 @@ import React from 'react';
import xss from 'xss'; import xss from 'xss';
import type { SearchResultItem } from 'types/client/search'; import type { SearchResultItem } from 'types/client/search';
import type { AddressFormat } from 'types/views/address';
import { route } from 'nextjs-routes'; import { route } from 'nextjs-routes';
import { toBech32Address } from 'lib/address/bech32';
import dayjs from 'lib/date/dayjs'; import dayjs from 'lib/date/dayjs';
import highlightText from 'lib/highlightText'; import highlightText from 'lib/highlightText';
import * as mixpanel from 'lib/mixpanel/index'; import * as mixpanel from 'lib/mixpanel/index';
...@@ -31,9 +33,10 @@ interface Props { ...@@ -31,9 +33,10 @@ interface Props {
data: SearchResultItem | SearchResultAppItem; data: SearchResultItem | SearchResultAppItem;
searchTerm: string; searchTerm: string;
isLoading?: boolean; 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>) => { const handleLinkClick = React.useCallback((e: React.MouseEvent<HTMLAnchorElement>) => {
saveToRecentKeywords(searchTerm); saveToRecentKeywords(searchTerm);
...@@ -78,6 +81,8 @@ const SearchResultListItem = ({ data, searchTerm, isLoading }: Props) => { ...@@ -78,6 +81,8 @@ const SearchResultListItem = ({ data, searchTerm, isLoading }: Props) => {
case 'contract': case 'contract':
case 'address': { case 'address': {
const shouldHighlightHash = ADDRESS_REGEXP.test(searchTerm); const shouldHighlightHash = ADDRESS_REGEXP.test(searchTerm);
const hash = addressFormat === 'bech32' ? toBech32Address(data.address) : data.address;
const address = { const address = {
hash: data.address, hash: data.address,
filecoin: { filecoin: {
...@@ -99,13 +104,13 @@ const SearchResultListItem = ({ data, searchTerm, isLoading }: Props) => { ...@@ -99,13 +104,13 @@ const SearchResultListItem = ({ data, searchTerm, isLoading }: Props) => {
> >
<AddressEntity.Content <AddressEntity.Content
asProp={ shouldHighlightHash ? 'mark' : 'span' } asProp={ shouldHighlightHash ? 'mark' : 'span' }
address={ address } address={{ ...address, hash }}
fontSize="sm" fontSize="sm"
lineHeight={ 5 } lineHeight={ 5 }
fontWeight={ 700 } fontWeight={ 700 }
/> />
</AddressEntity.Link> </AddressEntity.Link>
<AddressEntity.Copy address={ address }/> <AddressEntity.Copy address={{ ...address, hash }}/>
</AddressEntity.Container> </AddressEntity.Container>
); );
} }
...@@ -286,12 +291,13 @@ const SearchResultListItem = ({ data, searchTerm, isLoading }: Props) => { ...@@ -286,12 +291,13 @@ const SearchResultListItem = ({ data, searchTerm, isLoading }: Props) => {
case 'token': { case 'token': {
const templateCols = `1fr const templateCols = `1fr
${ (data.token_type === 'ERC-20' && data.exchange_rate) || (data.token_type !== 'ERC-20' && data.total_supply) ? ' auto' : '' }`; ${ (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 ( return (
<Grid templateColumns={ templateCols } alignItems="center" gap={ 2 }> <Grid templateColumns={ templateCols } alignItems="center" gap={ 2 }>
<Skeleton isLoaded={ !isLoading } overflow="hidden" display="flex" alignItems="center"> <Skeleton isLoaded={ !isLoading } overflow="hidden" display="flex" alignItems="center">
<Text whiteSpace="nowrap" overflow="hidden"> <Text whiteSpace="nowrap" overflow="hidden">
<HashStringShortenDynamic hash={ data.filecoin_robust_address || data.address } isTooltipDisabled/> <HashStringShortenDynamic hash={ hash } isTooltipDisabled/>
</Text> </Text>
{ data.is_smart_contract_verified && <IconSvg name="status/success" boxSize="14px" color="green.500" ml={ 1 } flexShrink={ 0 }/> } { data.is_smart_contract_verified && <IconSvg name="status/success" boxSize="14px" color="green.500" ml={ 1 } flexShrink={ 0 }/> }
</Skeleton> </Skeleton>
...@@ -333,10 +339,12 @@ const SearchResultListItem = ({ data, searchTerm, isLoading }: Props) => { ...@@ -333,10 +339,12 @@ const SearchResultListItem = ({ data, searchTerm, isLoading }: Props) => {
); );
} }
case 'label': { case 'label': {
const hash = data.filecoin_robust_address || (addressFormat === 'bech32' ? toBech32Address(data.address) : data.address);
return ( return (
<Flex alignItems="center"> <Flex alignItems="center">
<Box overflow="hidden"> <Box overflow="hidden">
<HashStringShortenDynamic hash={ data.filecoin_robust_address || data.address }/> <HashStringShortenDynamic hash={ hash }/>
</Box> </Box>
{ data.is_smart_contract_verified && <IconSvg name="status/success" boxSize="14px" color="green.500" ml={ 1 } flexShrink={ 0 }/> } { data.is_smart_contract_verified && <IconSvg name="status/success" boxSize="14px" color="green.500" ml={ 1 } flexShrink={ 0 }/> }
</Flex> </Flex>
...@@ -384,10 +392,12 @@ const SearchResultListItem = ({ data, searchTerm, isLoading }: Props) => { ...@@ -384,10 +392,12 @@ const SearchResultListItem = ({ data, searchTerm, isLoading }: Props) => {
} }
case 'ens_domain': { case 'ens_domain': {
const expiresText = data.ens_info?.expiry_date ? ` expires ${ dayjs(data.ens_info.expiry_date).fromNow() }` : ''; 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 ( return (
<Flex alignItems="center" gap={ 3 }> <Flex alignItems="center" gap={ 3 }>
<Box overflow="hidden"> <Box overflow="hidden">
<HashStringShortenDynamic hash={ data.filecoin_robust_address || data.address }/> <HashStringShortenDynamic hash={ hash }/>
</Box> </Box>
{ {
data.ens_info.names_count > 1 ? data.ens_info.names_count > 1 ?
......
...@@ -3,9 +3,11 @@ import React from 'react'; ...@@ -3,9 +3,11 @@ import React from 'react';
import xss from 'xss'; import xss from 'xss';
import type { SearchResultItem } from 'types/client/search'; import type { SearchResultItem } from 'types/client/search';
import type { AddressFormat } from 'types/views/address';
import { route } from 'nextjs-routes'; import { route } from 'nextjs-routes';
import { toBech32Address } from 'lib/address/bech32';
import dayjs from 'lib/date/dayjs'; import dayjs from 'lib/date/dayjs';
import highlightText from 'lib/highlightText'; import highlightText from 'lib/highlightText';
import * as mixpanel from 'lib/mixpanel/index'; import * as mixpanel from 'lib/mixpanel/index';
...@@ -30,9 +32,10 @@ interface Props { ...@@ -30,9 +32,10 @@ interface Props {
data: SearchResultItem | SearchResultAppItem; data: SearchResultItem | SearchResultAppItem;
searchTerm: string; searchTerm: string;
isLoading?: boolean; 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>) => { const handleLinkClick = React.useCallback((e: React.MouseEvent<HTMLAnchorElement>) => {
saveToRecentKeywords(searchTerm); saveToRecentKeywords(searchTerm);
...@@ -49,6 +52,7 @@ const SearchResultTableItem = ({ data, searchTerm, isLoading }: Props) => { ...@@ -49,6 +52,7 @@ const SearchResultTableItem = ({ data, searchTerm, isLoading }: Props) => {
switch (data.type) { switch (data.type) {
case 'token': { case 'token': {
const name = data.name + (data.symbol ? ` (${ data.symbol })` : ''); const name = data.name + (data.symbol ? ` (${ data.symbol })` : '');
const hash = data.filecoin_robust_address || (addressFormat === 'bech32' ? toBech32Address(data.address) : data.address);
return ( return (
<> <>
...@@ -77,7 +81,7 @@ const SearchResultTableItem = ({ data, searchTerm, isLoading }: Props) => { ...@@ -77,7 +81,7 @@ const SearchResultTableItem = ({ data, searchTerm, isLoading }: Props) => {
<Td fontSize="sm" verticalAlign="middle"> <Td fontSize="sm" verticalAlign="middle">
<Skeleton isLoaded={ !isLoading } whiteSpace="nowrap" overflow="hidden" display="flex" alignItems="center"> <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' }> <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> </Box>
{ data.is_smart_contract_verified && <IconSvg name="status/success" boxSize="14px" color="green.500" ml={ 1 } flexShrink={ 0 }/> } { data.is_smart_contract_verified && <IconSvg name="status/success" boxSize="14px" color="green.500" ml={ 1 } flexShrink={ 0 }/> }
</Skeleton> </Skeleton>
...@@ -110,6 +114,7 @@ const SearchResultTableItem = ({ data, searchTerm, isLoading }: Props) => { ...@@ -110,6 +114,7 @@ const SearchResultTableItem = ({ data, searchTerm, isLoading }: Props) => {
ens_domain_name: null, ens_domain_name: null,
}; };
const expiresText = data.ens_info?.expiry_date ? ` (expires ${ dayjs(data.ens_info.expiry_date).fromNow() })` : ''; 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 ( return (
<> <>
...@@ -122,13 +127,13 @@ const SearchResultTableItem = ({ data, searchTerm, isLoading }: Props) => { ...@@ -122,13 +127,13 @@ const SearchResultTableItem = ({ data, searchTerm, isLoading }: Props) => {
> >
<AddressEntity.Content <AddressEntity.Content
asProp={ shouldHighlightHash ? 'mark' : 'span' } asProp={ shouldHighlightHash ? 'mark' : 'span' }
address={ address } address={{ ...address, hash }}
fontSize="sm" fontSize="sm"
lineHeight={ 5 } lineHeight={ 5 }
fontWeight={ 700 } fontWeight={ 700 }
/> />
</AddressEntity.Link> </AddressEntity.Link>
<AddressEntity.Copy address={ address }/> <AddressEntity.Copy address={{ ...address, hash }}/>
</AddressEntity.Container> </AddressEntity.Container>
</Td> </Td>
{ addressName && ( { addressName && (
...@@ -158,6 +163,8 @@ const SearchResultTableItem = ({ data, searchTerm, isLoading }: Props) => { ...@@ -158,6 +163,8 @@ const SearchResultTableItem = ({ data, searchTerm, isLoading }: Props) => {
} }
case 'label': { case 'label': {
const hash = data.filecoin_robust_address || (addressFormat === 'bech32' ? toBech32Address(data.address) : data.address);
return ( return (
<> <>
<Td fontSize="sm"> <Td fontSize="sm">
...@@ -177,7 +184,7 @@ const SearchResultTableItem = ({ data, searchTerm, isLoading }: Props) => { ...@@ -177,7 +184,7 @@ const SearchResultTableItem = ({ data, searchTerm, isLoading }: Props) => {
<Td fontSize="sm" verticalAlign="middle"> <Td fontSize="sm" verticalAlign="middle">
<Flex alignItems="center" overflow="hidden"> <Flex alignItems="center" overflow="hidden">
<Box overflow="hidden" whiteSpace="nowrap" w={ data.is_smart_contract_verified ? 'calc(100%-28px)' : 'unset' }> <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> </Box>
{ data.is_smart_contract_verified && <IconSvg name="status/success" boxSize="14px" color="green.500" ml={ 1 } flexShrink={ 0 }/> } { data.is_smart_contract_verified && <IconSvg name="status/success" boxSize="14px" color="green.500" ml={ 1 } flexShrink={ 0 }/> }
</Flex> </Flex>
...@@ -369,6 +376,8 @@ const SearchResultTableItem = ({ data, searchTerm, isLoading }: Props) => { ...@@ -369,6 +376,8 @@ const SearchResultTableItem = ({ data, searchTerm, isLoading }: Props) => {
case 'ens_domain': { case 'ens_domain': {
const expiresText = data.ens_info?.expiry_date ? ` expires ${ dayjs(data.ens_info.expiry_date).fromNow() }` : ''; 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 ( return (
<> <>
<Td fontSize="sm"> <Td fontSize="sm">
...@@ -395,7 +404,7 @@ const SearchResultTableItem = ({ data, searchTerm, isLoading }: Props) => { ...@@ -395,7 +404,7 @@ const SearchResultTableItem = ({ data, searchTerm, isLoading }: Props) => {
<Td> <Td>
<Flex alignItems="center" overflow="hidden"> <Flex alignItems="center" overflow="hidden">
<Box overflow="hidden" whiteSpace="nowrap" w={ data.is_smart_contract_verified ? 'calc(100%-28px)' : 'unset' }> <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> </Box>
{ data.is_smart_contract_verified && <IconSvg name="status/success" boxSize="14px" color="green.500" ml={ 1 } flexShrink={ 0 }/> } { data.is_smart_contract_verified && <IconSvg name="status/success" boxSize="14px" color="green.500" ml={ 1 } flexShrink={ 0 }/> }
</Flex> </Flex>
......
import { Box } from '@chakra-ui/react'; import { Box } from '@chakra-ui/react';
import type { BrowserContext } from '@playwright/test';
import React from 'react'; import React from 'react';
import config from 'configs/app';
import { AddressHighlightProvider } from 'lib/contexts/addressHighlight'; import { AddressHighlightProvider } from 'lib/contexts/addressHighlight';
import * as cookies from 'lib/cookies';
import * as addressMock from 'mocks/address/address'; import * as addressMock from 'mocks/address/address';
import * as implementationsMock from 'mocks/address/implementations'; import * as implementationsMock from 'mocks/address/implementations';
import * as metadataMock from 'mocks/metadata/address'; import * as metadataMock from 'mocks/metadata/address';
import { ENVS_MAP } from 'playwright/fixtures/mockEnvs';
import { test, expect } from 'playwright/lib'; import { test, expect } from 'playwright/lib';
import AddressEntity from './AddressEntity'; import AddressEntity from './AddressEntity';
...@@ -210,3 +214,21 @@ test('hover', async({ page, render }) => { ...@@ -210,3 +214,21 @@ test('hover', async({ page, render }) => {
await component.getByText(addressMock.hash.slice(0, 4)).hover(); await component.getByText(addressMock.hash.slice(0, 4)).hover();
await expect(page).toHaveScreenshot(); 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'; ...@@ -6,7 +6,9 @@ import type { AddressParam } from 'types/api/addressParams';
import { route } from 'nextjs-routes'; import { route } from 'nextjs-routes';
import { toBech32Address } from 'lib/address/bech32';
import { useAddressHighlightContext } from 'lib/contexts/addressHighlight'; import { useAddressHighlightContext } from 'lib/contexts/addressHighlight';
import { useSettingsContext } from 'lib/contexts/settings';
import * as EntityBase from 'ui/shared/entities/base/components'; import * as EntityBase from 'ui/shared/entities/base/components';
import { distributeEntityProps, getIconProps } from '../base/utils'; import { distributeEntityProps, getIconProps } from '../base/utils';
...@@ -83,7 +85,7 @@ const Icon = (props: IconProps) => { ...@@ -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 Content = chakra((props: ContentProps) => {
const nameTag = props.address.metadata?.tags.find(tag => tag.tagType === 'name')?.name; const nameTag = props.address.metadata?.tags.find(tag => tag.tagType === 'name')?.name;
...@@ -99,7 +101,7 @@ const Content = chakra((props: ContentProps) => { ...@@ -99,7 +101,7 @@ const Content = chakra((props: ContentProps) => {
const label = ( const label = (
<VStack gap={ 0 } py={ 1 } color="inherit"> <VStack gap={ 0 } py={ 1 } color="inherit">
<Box fontWeight={ 600 } whiteSpace="pre-wrap" wordBreak="break-word">{ nameText }</Box> <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> </VStack>
); );
...@@ -115,18 +117,18 @@ const Content = chakra((props: ContentProps) => { ...@@ -115,18 +117,18 @@ const Content = chakra((props: ContentProps) => {
return ( return (
<EntityBase.Content <EntityBase.Content
{ ...props } { ...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) => { const Copy = (props: CopyProps) => {
return ( return (
<EntityBase.Copy <EntityBase.Copy
{ ...props } { ...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 { ...@@ -141,28 +143,31 @@ export interface EntityProps extends EntityBase.EntityBaseProps {
address: AddressProp; address: AddressProp;
isSafeAddress?: boolean; isSafeAddress?: boolean;
noHighlight?: boolean; noHighlight?: boolean;
noAltHash?: boolean;
} }
const AddressEntry = (props: EntityProps) => { const AddressEntry = (props: EntityProps) => {
const partsProps = distributeEntityProps(props); 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 ( return (
<Container <Container
// we have to use the global classnames here, see theme/global.ts // 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 // 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' : '' }` } className={ `${ props.className } address-entity ${ props.noCopy ? 'address-entity_no-copy' : '' }` }
data-hash={ context && !props.isLoading ? props.address.hash : undefined } data-hash={ highlightContext && !props.isLoading ? props.address.hash : undefined }
onMouseEnter={ context?.onMouseEnter } onMouseEnter={ highlightContext?.onMouseEnter }
onMouseLeave={ context?.onMouseLeave } onMouseLeave={ highlightContext?.onMouseLeave }
position="relative" position="relative"
zIndex={ 0 } zIndex={ 0 }
> >
<Icon { ...partsProps.icon }/> <Icon { ...partsProps.icon }/>
<Link { ...partsProps.link }> <Link { ...partsProps.link }>
<Content { ...partsProps.content }/> <Content { ...partsProps.content } altHash={ altHash }/>
</Link> </Link>
<Copy { ...partsProps.copy }/> <Copy { ...partsProps.copy } altHash={ altHash }/>
</Container> </Container>
); );
}; };
......
...@@ -32,7 +32,7 @@ const AddressEntityContentProxy = (props: ContentProps) => { ...@@ -32,7 +32,7 @@ const AddressEntityContentProxy = (props: ContentProps) => {
<EntityBase.Content <EntityBase.Content
{ ...props } { ...props }
truncation={ nameTag || implementationName || props.address.name ? 'tail' : props.truncation } 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 isTooltipDisabled
/> />
</Box> </Box>
......
...@@ -7,6 +7,7 @@ import { scroller, Element } from 'react-scroll'; ...@@ -7,6 +7,7 @@ import { scroller, Element } from 'react-scroll';
import type { SearchResultItem } from 'types/api/search'; import type { SearchResultItem } from 'types/api/search';
import type { ResourceError } from 'lib/api/resources'; import type { ResourceError } from 'lib/api/resources';
import { useSettingsContext } from 'lib/contexts/settings';
import useIsMobile from 'lib/hooks/useIsMobile'; import useIsMobile from 'lib/hooks/useIsMobile';
import * as regexp from 'lib/regexp'; import * as regexp from 'lib/regexp';
import useMarketplaceApps from 'ui/marketplace/useMarketplaceApps'; import useMarketplaceApps from 'ui/marketplace/useMarketplaceApps';
...@@ -30,6 +31,7 @@ const SearchBarSuggest = ({ query, searchTerm, onItemClick, containerId }: Props ...@@ -30,6 +31,7 @@ const SearchBarSuggest = ({ query, searchTerm, onItemClick, containerId }: Props
const isMobile = useIsMobile(); const isMobile = useIsMobile();
const marketplaceApps = useMarketplaceApps(searchTerm); const marketplaceApps = useMarketplaceApps(searchTerm);
const settingsContext = useSettingsContext();
const categoriesRefs = React.useRef<Array<HTMLParagraphElement>>([]); const categoriesRefs = React.useRef<Array<HTMLParagraphElement>>([]);
const tabsRef = React.useRef<HTMLDivElement>(null); const tabsRef = React.useRef<HTMLDivElement>(null);
...@@ -165,9 +167,16 @@ const SearchBarSuggest = ({ query, searchTerm, onItemClick, containerId }: Props ...@@ -165,9 +167,16 @@ const SearchBarSuggest = ({ query, searchTerm, onItemClick, containerId }: Props
> >
{ cat.title } { cat.title }
</Text> </Text>
{ cat.id !== 'app' && itemsGroups[cat.id]?.map((item, index) => { cat.id !== 'app' && itemsGroups[cat.id]?.map((item, index) => (
<SearchBarSuggestItem key={ index } data={ item } isMobile={ isMobile } searchTerm={ searchTerm } onClick={ onItemClick }/>, <SearchBarSuggestItem
) } key={ index }
data={ item }
isMobile={ isMobile }
searchTerm={ searchTerm }
onClick={ onItemClick }
addressFormat={ settingsContext?.addressFormat }
/>
)) }
{ cat.id === 'app' && itemsGroups[cat.id]?.map((item, index) => { cat.id === 'app' && itemsGroups[cat.id]?.map((item, index) =>
<SearchBarSuggestApp key={ index } data={ item } isMobile={ isMobile } searchTerm={ searchTerm } onClick={ onItemClick }/>, <SearchBarSuggestApp key={ index } data={ item } isMobile={ isMobile } searchTerm={ searchTerm } onClick={ onItemClick }/>,
) } ) }
......
import { chakra, Box, Text, Flex } from '@chakra-ui/react'; import { chakra, Box, Text, Flex } from '@chakra-ui/react';
import React from 'react'; import React from 'react';
import type { ItemsProps } from './types';
import type { SearchResultAddressOrContract } from 'types/api/search'; import type { SearchResultAddressOrContract } from 'types/api/search';
import { toBech32Address } from 'lib/address/bech32';
import dayjs from 'lib/date/dayjs'; import dayjs from 'lib/date/dayjs';
import highlightText from 'lib/highlightText'; import highlightText from 'lib/highlightText';
import ContractCertifiedLabel from 'ui/shared/ContractCertifiedLabel'; import ContractCertifiedLabel from 'ui/shared/ContractCertifiedLabel';
...@@ -10,14 +12,9 @@ import * as AddressEntity from 'ui/shared/entities/address/AddressEntity'; ...@@ -10,14 +12,9 @@ import * as AddressEntity from 'ui/shared/entities/address/AddressEntity';
import { ADDRESS_REGEXP } from 'ui/shared/forms/validators/address'; import { ADDRESS_REGEXP } from 'ui/shared/forms/validators/address';
import HashStringShortenDynamic from 'ui/shared/HashStringShortenDynamic'; import HashStringShortenDynamic from 'ui/shared/HashStringShortenDynamic';
interface Props { const SearchBarSuggestAddress = ({ data, isMobile, searchTerm, addressFormat }: ItemsProps<SearchResultAddressOrContract>) => {
data: SearchResultAddressOrContract;
isMobile: boolean | undefined;
searchTerm: string;
}
const SearchBarSuggestAddress = ({ data, isMobile, searchTerm }: Props) => {
const shouldHighlightHash = ADDRESS_REGEXP.test(searchTerm); const shouldHighlightHash = ADDRESS_REGEXP.test(searchTerm);
const hash = data.filecoin_robust_address || (addressFormat === 'bech32' ? toBech32Address(data.address) : data.address);
const icon = ( const icon = (
<AddressEntity.Icon <AddressEntity.Icon
...@@ -52,7 +49,7 @@ const SearchBarSuggestAddress = ({ data, isMobile, searchTerm }: Props) => { ...@@ -52,7 +49,7 @@ const SearchBarSuggestAddress = ({ data, isMobile, searchTerm }: Props) => {
{ data.certified && <ContractCertifiedLabel boxSize={ 5 } iconSize={ 5 } ml={ 1 }/> } { data.certified && <ContractCertifiedLabel boxSize={ 5 } iconSize={ 5 } ml={ 1 }/> }
</Flex> </Flex>
); );
const addressEl = <HashStringShortenDynamic hash={ data.filecoin_robust_address || data.address } isTooltipDisabled/>; const addressEl = <HashStringShortenDynamic hash={ hash } isTooltipDisabled/>;
if (isMobile) { if (isMobile) {
return ( return (
......
import { chakra, Flex } from '@chakra-ui/react'; import { chakra, Flex } from '@chakra-ui/react';
import React from 'react'; import React from 'react';
import type { ItemsProps } from './types';
import type { SearchResultBlob } from 'types/api/search'; import type { SearchResultBlob } from 'types/api/search';
import * as BlobEntity from 'ui/shared/entities/blob/BlobEntity'; import * as BlobEntity from 'ui/shared/entities/blob/BlobEntity';
import HashStringShortenDynamic from 'ui/shared/HashStringShortenDynamic'; import HashStringShortenDynamic from 'ui/shared/HashStringShortenDynamic';
interface Props { const SearchBarSuggestBlob = ({ data }: ItemsProps<SearchResultBlob>) => {
data: SearchResultBlob;
searchTerm: string;
}
const SearchBarSuggestBlob = ({ data }: Props) => {
return ( return (
<Flex alignItems="center" minW={ 0 }> <Flex alignItems="center" minW={ 0 }>
<BlobEntity.Icon/> <BlobEntity.Icon/>
......
import { Text, Flex, Grid, Tag } from '@chakra-ui/react'; import { Text, Flex, Grid, Tag } from '@chakra-ui/react';
import React from 'react'; import React from 'react';
import type { ItemsProps } from './types';
import type { SearchResultBlock } from 'types/client/search'; import type { SearchResultBlock } from 'types/client/search';
import dayjs from 'lib/date/dayjs'; import dayjs from 'lib/date/dayjs';
...@@ -8,13 +9,7 @@ import highlightText from 'lib/highlightText'; ...@@ -8,13 +9,7 @@ import highlightText from 'lib/highlightText';
import * as BlockEntity from 'ui/shared/entities/block/BlockEntity'; import * as BlockEntity from 'ui/shared/entities/block/BlockEntity';
import HashStringShortenDynamic from 'ui/shared/HashStringShortenDynamic'; import HashStringShortenDynamic from 'ui/shared/HashStringShortenDynamic';
interface Props { const SearchBarSuggestBlock = ({ data, isMobile, searchTerm }: ItemsProps<SearchResultBlock>) => {
data: SearchResultBlock;
isMobile: boolean | undefined;
searchTerm: string;
}
const SearchBarSuggestBlock = ({ data, isMobile, searchTerm }: Props) => {
const icon = <BlockEntity.Icon/>; const icon = <BlockEntity.Icon/>;
const shouldHighlightHash = data.block_hash.toLowerCase() === searchTerm.toLowerCase(); const shouldHighlightHash = data.block_hash.toLowerCase() === searchTerm.toLowerCase();
const isFutureBlock = data.timestamp === undefined; const isFutureBlock = data.timestamp === undefined;
......
import { Grid, Text, Flex } from '@chakra-ui/react'; import { Grid, Text, Flex } from '@chakra-ui/react';
import React from 'react'; import React from 'react';
import type { ItemsProps } from './types';
import type { SearchResultDomain } from 'types/api/search'; import type { SearchResultDomain } from 'types/api/search';
import { toBech32Address } from 'lib/address/bech32';
import dayjs from 'lib/date/dayjs'; import dayjs from 'lib/date/dayjs';
import highlightText from 'lib/highlightText'; import highlightText from 'lib/highlightText';
import HashStringShortenDynamic from 'ui/shared/HashStringShortenDynamic'; import HashStringShortenDynamic from 'ui/shared/HashStringShortenDynamic';
import IconSvg from 'ui/shared/IconSvg'; import IconSvg from 'ui/shared/IconSvg';
interface Props { const SearchBarSuggestDomain = ({ data, isMobile, searchTerm, addressFormat }: ItemsProps<SearchResultDomain>) => {
data: SearchResultDomain;
isMobile: boolean | undefined;
searchTerm: string;
}
const SearchBarSuggestDomain = ({ data, isMobile, searchTerm }: Props) => {
const icon = <IconSvg name="ENS_slim" boxSize={ 5 } color="gray.500"/>; 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 = ( const name = (
<Text <Text
...@@ -34,7 +31,7 @@ const SearchBarSuggestDomain = ({ data, isMobile, searchTerm }: Props) => { ...@@ -34,7 +31,7 @@ const SearchBarSuggestDomain = ({ data, isMobile, searchTerm }: Props) => {
whiteSpace="nowrap" whiteSpace="nowrap"
variant="secondary" variant="secondary"
> >
<HashStringShortenDynamic hash={ data.filecoin_robust_address || data.address } isTooltipDisabled/> <HashStringShortenDynamic hash={ hash } isTooltipDisabled/>
</Text> </Text>
); );
......
...@@ -3,6 +3,7 @@ import NextLink from 'next/link'; ...@@ -3,6 +3,7 @@ import NextLink from 'next/link';
import React from 'react'; import React from 'react';
import type { SearchResultItem } from 'types/client/search'; import type { SearchResultItem } from 'types/client/search';
import type { AddressFormat } from 'types/views/address';
import { route } from 'nextjs-routes'; import { route } from 'nextjs-routes';
...@@ -21,9 +22,10 @@ interface Props { ...@@ -21,9 +22,10 @@ interface Props {
isMobile: boolean | undefined; isMobile: boolean | undefined;
searchTerm: string; searchTerm: string;
onClick: (event: React.MouseEvent<HTMLAnchorElement>) => void; 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 = (() => { const url = (() => {
switch (data.type) { switch (data.type) {
...@@ -61,15 +63,35 @@ const SearchBarSuggestItem = ({ data, isMobile, searchTerm, onClick }: Props) => ...@@ -61,15 +63,35 @@ const SearchBarSuggestItem = ({ data, isMobile, searchTerm, onClick }: Props) =>
const content = (() => { const content = (() => {
switch (data.type) { switch (data.type) {
case 'token': { case 'token': {
return <SearchBarSuggestToken data={ data } searchTerm={ searchTerm } isMobile={ isMobile }/>; return (
<SearchBarSuggestToken
data={ data }
searchTerm={ searchTerm }
isMobile={ isMobile }
addressFormat={ addressFormat }
/>
);
} }
case 'contract': case 'contract':
case 'address': { case 'address': {
return <SearchBarSuggestAddress data={ data } searchTerm={ searchTerm } isMobile={ isMobile }/>; return (
<SearchBarSuggestAddress
data={ data }
searchTerm={ searchTerm }
isMobile={ isMobile }
addressFormat={ addressFormat }
/>
);
} }
case 'label': { case 'label': {
return <SearchBarSuggestLabel data={ data } searchTerm={ searchTerm } isMobile={ isMobile }/>; return (
<SearchBarSuggestLabel
data={ data }
searchTerm={ searchTerm }
isMobile={ isMobile }
addressFormat={ addressFormat }
/>
);
} }
case 'block': { case 'block': {
return <SearchBarSuggestBlock data={ data } searchTerm={ searchTerm } isMobile={ isMobile }/>; return <SearchBarSuggestBlock data={ data } searchTerm={ searchTerm } isMobile={ isMobile }/>;
...@@ -84,7 +106,7 @@ const SearchBarSuggestItem = ({ data, isMobile, searchTerm, onClick }: Props) => ...@@ -84,7 +106,7 @@ const SearchBarSuggestItem = ({ data, isMobile, searchTerm, onClick }: Props) =>
return <SearchBarSuggestBlob data={ data } searchTerm={ searchTerm }/>; return <SearchBarSuggestBlob data={ data } searchTerm={ searchTerm }/>;
} }
case 'ens_domain': { 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 { Grid, Text, Flex } from '@chakra-ui/react';
import React from 'react'; import React from 'react';
import type { ItemsProps } from './types';
import type { SearchResultLabel } from 'types/api/search'; import type { SearchResultLabel } from 'types/api/search';
import { toBech32Address } from 'lib/address/bech32';
import highlightText from 'lib/highlightText'; import highlightText from 'lib/highlightText';
import HashStringShortenDynamic from 'ui/shared/HashStringShortenDynamic'; import HashStringShortenDynamic from 'ui/shared/HashStringShortenDynamic';
import IconSvg from 'ui/shared/IconSvg'; import IconSvg from 'ui/shared/IconSvg';
interface Props { const SearchBarSuggestLabel = ({ data, isMobile, searchTerm, addressFormat }: ItemsProps<SearchResultLabel>) => {
data: SearchResultLabel;
isMobile: boolean | undefined;
searchTerm: string;
}
const SearchBarSuggestLabel = ({ data, isMobile, searchTerm }: Props) => {
const icon = <IconSvg name="publictags_slim" boxSize={ 5 } color="gray.500"/>; 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 = ( const name = (
<Text <Text
...@@ -33,7 +30,7 @@ const SearchBarSuggestLabel = ({ data, isMobile, searchTerm }: Props) => { ...@@ -33,7 +30,7 @@ const SearchBarSuggestLabel = ({ data, isMobile, searchTerm }: Props) => {
whiteSpace="nowrap" whiteSpace="nowrap"
variant="secondary" variant="secondary"
> >
<HashStringShortenDynamic hash={ data.filecoin_robust_address || data.address } isTooltipDisabled/> <HashStringShortenDynamic hash={ hash } isTooltipDisabled/>
</Text> </Text>
); );
......
import { Grid, Text, Flex } from '@chakra-ui/react'; import { Grid, Text, Flex } from '@chakra-ui/react';
import React from 'react'; import React from 'react';
import type { ItemsProps } from './types';
import type { SearchResultToken } from 'types/api/search'; import type { SearchResultToken } from 'types/api/search';
import { toBech32Address } from 'lib/address/bech32';
import highlightText from 'lib/highlightText'; import highlightText from 'lib/highlightText';
import * as TokenEntity from 'ui/shared/entities/token/TokenEntity'; import * as TokenEntity from 'ui/shared/entities/token/TokenEntity';
import HashStringShortenDynamic from 'ui/shared/HashStringShortenDynamic'; import HashStringShortenDynamic from 'ui/shared/HashStringShortenDynamic';
import IconSvg from 'ui/shared/IconSvg'; import IconSvg from 'ui/shared/IconSvg';
interface Props { const SearchBarSuggestToken = ({ data, isMobile, searchTerm, addressFormat }: ItemsProps<SearchResultToken>) => {
data: SearchResultToken;
isMobile: boolean | undefined;
searchTerm: string;
}
const SearchBarSuggestToken = ({ data, isMobile, searchTerm }: Props) => {
const icon = <TokenEntity.Icon token={{ ...data, type: data.token_type }}/>; const icon = <TokenEntity.Icon token={{ ...data, type: data.token_type }}/>;
const verifiedIcon = <IconSvg name="certified" boxSize={ 4 } color="green.500" ml={ 1 }/>; 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 = ( const name = (
<Text <Text
fontWeight={ 700 } fontWeight={ 700 }
...@@ -30,7 +28,7 @@ const SearchBarSuggestToken = ({ data, isMobile, searchTerm }: Props) => { ...@@ -30,7 +28,7 @@ const SearchBarSuggestToken = ({ data, isMobile, searchTerm }: Props) => {
const address = ( const address = (
<Text variant="secondary" whiteSpace="nowrap" overflow="hidden"> <Text variant="secondary" whiteSpace="nowrap" overflow="hidden">
<HashStringShortenDynamic hash={ data.filecoin_robust_address || data.address } isTooltipDisabled/> <HashStringShortenDynamic hash={ hash } isTooltipDisabled/>
</Text> </Text>
); );
......
import { chakra, Text, Flex } from '@chakra-ui/react'; import { chakra, Text, Flex } from '@chakra-ui/react';
import React from 'react'; import React from 'react';
import type { ItemsProps } from './types';
import type { SearchResultTx } from 'types/api/search'; import type { SearchResultTx } from 'types/api/search';
import dayjs from 'lib/date/dayjs'; import dayjs from 'lib/date/dayjs';
import * as TxEntity from 'ui/shared/entities/tx/TxEntity'; import * as TxEntity from 'ui/shared/entities/tx/TxEntity';
import HashStringShortenDynamic from 'ui/shared/HashStringShortenDynamic'; import HashStringShortenDynamic from 'ui/shared/HashStringShortenDynamic';
interface Props { const SearchBarSuggestTx = ({ data, isMobile }: ItemsProps<SearchResultTx>) => {
data: SearchResultTx;
isMobile: boolean | undefined;
searchTerm: string;
}
const SearchBarSuggestTx = ({ data, isMobile }: Props) => {
const icon = <TxEntity.Icon/>; const icon = <TxEntity.Icon/>;
const hash = ( const hash = (
<chakra.mark overflow="hidden" whiteSpace="nowrap" fontWeight={ 700 }> <chakra.mark overflow="hidden" whiteSpace="nowrap" fontWeight={ 700 }>
......
import { chakra, Text, Flex } from '@chakra-ui/react'; import { chakra, Text, Flex } from '@chakra-ui/react';
import React from 'react'; import React from 'react';
import type { ItemsProps } from './types';
import type { SearchResultUserOp } from 'types/api/search'; import type { SearchResultUserOp } from 'types/api/search';
import dayjs from 'lib/date/dayjs'; import dayjs from 'lib/date/dayjs';
import * as UserOpEntity from 'ui/shared/entities/userOp/UserOpEntity'; import * as UserOpEntity from 'ui/shared/entities/userOp/UserOpEntity';
import HashStringShortenDynamic from 'ui/shared/HashStringShortenDynamic'; import HashStringShortenDynamic from 'ui/shared/HashStringShortenDynamic';
interface Props { const SearchBarSuggestUserOp = ({ data, isMobile }: ItemsProps<SearchResultUserOp>) => {
data: SearchResultUserOp;
isMobile: boolean | undefined;
searchTerm: string;
}
const SearchBarSuggestTx = ({ data, isMobile }: Props) => {
const icon = <UserOpEntity.Icon/>; const icon = <UserOpEntity.Icon/>;
const hash = ( const hash = (
<chakra.mark overflow="hidden" whiteSpace="nowrap" fontWeight={ 700 }> <chakra.mark overflow="hidden" whiteSpace="nowrap" fontWeight={ 700 }>
...@@ -45,4 +40,4 @@ const SearchBarSuggestTx = ({ data, isMobile }: Props) => { ...@@ -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 React from 'react';
import { isBech32Address, fromBech32Address } from 'lib/address/bech32';
import useApiQuery from 'lib/api/useApiQuery'; import useApiQuery from 'lib/api/useApiQuery';
import useDebounce from 'lib/hooks/useDebounce'; import useDebounce from 'lib/hooks/useDebounce';
...@@ -9,7 +10,7 @@ export default function useQuickSearchQuery() { ...@@ -9,7 +10,7 @@ export default function useQuickSearchQuery() {
const debouncedSearchTerm = useDebounce(searchTerm, 300); const debouncedSearchTerm = useDebounce(searchTerm, 300);
const query = useApiQuery('quick_search', { const query = useApiQuery('quick_search', {
queryParams: { q: debouncedSearchTerm }, queryParams: { q: isBech32Address(debouncedSearchTerm) ? fromBech32Address(debouncedSearchTerm) : debouncedSearchTerm },
queryOptions: { enabled: debouncedSearchTerm.trim().length > 0 }, queryOptions: { enabled: debouncedSearchTerm.trim().length > 0 },
}); });
......
import { useRouter } from 'next/router'; import { useRouter } from 'next/router';
import React from 'react'; import React from 'react';
import { fromBech32Address, isBech32Address } from 'lib/address/bech32';
import useApiQuery from 'lib/api/useApiQuery'; import useApiQuery from 'lib/api/useApiQuery';
import useDebounce from 'lib/hooks/useDebounce'; import useDebounce from 'lib/hooks/useDebounce';
import useUpdateValueEffect from 'lib/hooks/useUpdateValueEffect'; import useUpdateValueEffect from 'lib/hooks/useUpdateValueEffect';
...@@ -21,7 +22,7 @@ export default function useSearchQuery(withRedirectCheck?: boolean) { ...@@ -21,7 +22,7 @@ export default function useSearchQuery(withRedirectCheck?: boolean) {
const query = useQueryWithPages({ const query = useQueryWithPages({
resourceName: 'search', resourceName: 'search',
filters: { q: debouncedSearchTerm }, filters: { q: isBech32Address(debouncedSearchTerm) ? fromBech32Address(debouncedSearchTerm) : debouncedSearchTerm },
options: { options: {
enabled: debouncedSearchTerm.trim().length > 0, enabled: debouncedSearchTerm.trim().length > 0,
placeholderData: generateListStub<'search'>(SEARCH_RESULT_ITEM, 50, { next_page_params: SEARCH_RESULT_NEXT_PAGE_PARAMS }), placeholderData: generateListStub<'search'>(SEARCH_RESULT_ITEM, 50, { next_page_params: SEARCH_RESULT_NEXT_PAGE_PARAMS }),
......
...@@ -4,6 +4,7 @@ import React from 'react'; ...@@ -4,6 +4,7 @@ import React from 'react';
import Popover from 'ui/shared/chakra/Popover'; import Popover from 'ui/shared/chakra/Popover';
import IconSvg from 'ui/shared/IconSvg'; import IconSvg from 'ui/shared/IconSvg';
import SettingsAddressFormat from './SettingsAddressFormat';
import SettingsColorTheme from './SettingsColorTheme'; import SettingsColorTheme from './SettingsColorTheme';
import SettingsIdentIcon from './SettingsIdentIcon'; import SettingsIdentIcon from './SettingsIdentIcon';
...@@ -28,6 +29,7 @@ const Settings = () => { ...@@ -28,6 +29,7 @@ const Settings = () => {
<SettingsColorTheme onSelect={ onClose }/> <SettingsColorTheme onSelect={ onClose }/>
<Box borderColor="divider" borderWidth="1px" my={ 3 }/> <Box borderColor="divider" borderWidth="1px" my={ 3 }/>
<SettingsIdentIcon/> <SettingsIdentIcon/>
<SettingsAddressFormat/>
</PopoverBody> </PopoverBody>
</PopoverContent> </PopoverContent>
</Popover> </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 = () => { ...@@ -32,7 +32,7 @@ const SettingsIdentIcon = () => {
return ( return (
<div> <div>
<Box fontWeight={ 600 }>Address identicon</Box> <Box fontWeight={ 600 }>Address settings</Box>
<Box color="text_secondary" mt={ 1 } mb={ 2 }>{ activeIdenticon?.label }</Box> <Box color="text_secondary" mt={ 1 } mb={ 2 }>{ activeIdenticon?.label }</Box>
<Flex> <Flex>
{ IDENTICONS.map((identicon) => ( { IDENTICONS.map((identicon) => (
......
...@@ -42,6 +42,7 @@ const UserProfileContentWallet = ({ onClose, className }: Props) => { ...@@ -42,6 +42,7 @@ const UserProfileContentWallet = ({ onClose, className }: Props) => {
truncation="dynamic" truncation="dynamic"
fontSize="sm" fontSize="sm"
fontWeight={ 500 } fontWeight={ 500 }
noAltHash
/> />
<IconButton <IconButton
aria-label="Open wallet" aria-label="Open wallet"
......
...@@ -8,9 +8,7 @@ import config from 'configs/app'; ...@@ -8,9 +8,7 @@ import config from 'configs/app';
import { CONTRACT_LICENSES } from 'lib/contracts/licenses'; import { CONTRACT_LICENSES } from 'lib/contracts/licenses';
import { currencyUnits } from 'lib/units'; import { currencyUnits } from 'lib/units';
import ContractCertifiedLabel from 'ui/shared/ContractCertifiedLabel'; import ContractCertifiedLabel from 'ui/shared/ContractCertifiedLabel';
import CopyToClipboard from 'ui/shared/CopyToClipboard';
import AddressEntity from 'ui/shared/entities/address/AddressEntity'; import AddressEntity from 'ui/shared/entities/address/AddressEntity';
import HashStringShorten from 'ui/shared/HashStringShorten';
import IconSvg from 'ui/shared/IconSvg'; import IconSvg from 'ui/shared/IconSvg';
import ListItemMobile from 'ui/shared/ListItemMobile/ListItemMobile'; import ListItemMobile from 'ui/shared/ListItemMobile/ListItemMobile';
import TimeAgoWithTooltip from 'ui/shared/TimeAgoWithTooltip'; import TimeAgoWithTooltip from 'ui/shared/TimeAgoWithTooltip';
...@@ -46,10 +44,16 @@ const VerifiedContractsListItem = ({ data, isLoading }: Props) => { ...@@ -46,10 +44,16 @@ const VerifiedContractsListItem = ({ data, isLoading }: Props) => {
/> />
{ data.certified && <ContractCertifiedLabel iconSize={ 5 } boxSize={ 5 } mx={ 2 }/> } { data.certified && <ContractCertifiedLabel iconSize={ 5 } boxSize={ 5 } mx={ 2 }/> }
</Flex> </Flex>
<Skeleton isLoaded={ !isLoading } color="text_secondary" ml="auto"> <AddressEntity
<HashStringShorten hash={ data.address.filecoin?.robust ?? data.address.hash } isTooltipDisabled/> address={{ hash: data.address.filecoin?.robust ?? data.address.hash }}
</Skeleton> isLoading={ isLoading }
<CopyToClipboard text={ data.address.filecoin?.robust ?? data.address.hash } isLoading={ isLoading }/> noLink
noIcon
truncation="constant"
ml="auto"
color="text_secondary"
flexShrink={ 0 }
/>
</Flex> </Flex>
<Flex columnGap={ 3 }> <Flex columnGap={ 3 }>
<Skeleton isLoaded={ !isLoading } fontWeight={ 500 }>Balance { currencyUnits.ether }</Skeleton> <Skeleton isLoaded={ !isLoading } fontWeight={ 500 }>Balance { currencyUnits.ether }</Skeleton>
......
...@@ -7,9 +7,7 @@ import type { VerifiedContract } from 'types/api/contracts'; ...@@ -7,9 +7,7 @@ import type { VerifiedContract } from 'types/api/contracts';
import config from 'configs/app'; import config from 'configs/app';
import { CONTRACT_LICENSES } from 'lib/contracts/licenses'; import { CONTRACT_LICENSES } from 'lib/contracts/licenses';
import ContractCertifiedLabel from 'ui/shared/ContractCertifiedLabel'; import ContractCertifiedLabel from 'ui/shared/ContractCertifiedLabel';
import CopyToClipboard from 'ui/shared/CopyToClipboard';
import AddressEntity from 'ui/shared/entities/address/AddressEntity'; import AddressEntity from 'ui/shared/entities/address/AddressEntity';
import HashStringShorten from 'ui/shared/HashStringShorten';
import IconSvg from 'ui/shared/IconSvg'; import IconSvg from 'ui/shared/IconSvg';
import TimeAgoWithTooltip from 'ui/shared/TimeAgoWithTooltip'; import TimeAgoWithTooltip from 'ui/shared/TimeAgoWithTooltip';
...@@ -44,12 +42,16 @@ const VerifiedContractsTableItem = ({ data, isLoading }: Props) => { ...@@ -44,12 +42,16 @@ const VerifiedContractsTableItem = ({ data, isLoading }: Props) => {
/> />
{ data.certified && <ContractCertifiedLabel iconSize={ 5 } boxSize={ 5 } ml={ 2 }/> } { data.certified && <ContractCertifiedLabel iconSize={ 5 } boxSize={ 5 } ml={ 2 }/> }
</Flex> </Flex>
<Flex alignItems="center" ml={ 7 }> <AddressEntity
<Skeleton isLoaded={ !isLoading } color="text_secondary" my={ 1 }> address={{ hash: data.address.filecoin?.robust ?? data.address.hash }}
<HashStringShorten hash={ data.address.filecoin?.robust ?? data.address.hash } isTooltipDisabled/> isLoading={ isLoading }
</Skeleton> noLink
<CopyToClipboard text={ data.address.filecoin?.robust ?? data.address.hash } isLoading={ isLoading }/> noIcon
</Flex> truncation="constant"
my={ 1 }
ml={ 7 }
color="text_secondary"
/>
</Td> </Td>
<Td isNumeric> <Td isNumeric>
<Skeleton isLoaded={ !isLoading } display="inline-block" my={ 1 }> <Skeleton isLoaded={ !isLoading } display="inline-block" my={ 1 }>
......
...@@ -4849,6 +4849,11 @@ ...@@ -4849,6 +4849,11 @@
dependencies: dependencies:
cross-fetch "^3.1.5" 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": "@scure/base@^1.1.3", "@scure/base@~1.1.2":
version "1.1.5" version "1.1.5"
resolved "https://registry.yarnpkg.com/@scure/base/-/base-1.1.5.tgz#1d85d17269fe97694b9c592552dd9e5e33552157" 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