diff --git a/configs/app/ui/views/address.ts b/configs/app/ui/views/address.ts index 088d288deb..6794a2735f 100644 --- a/configs/app/ui/views/address.ts +++ b/configs/app/ui/views/address.ts @@ -1,7 +1,7 @@ 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 = (() => { + const value = (parseEnvJson>(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>(getEnvValue('NEXT_PUBLIC_VIEWS_ADDRESS_HIDDEN_VIEWS')) || []; @@ -43,6 +65,10 @@ const extraVerificationMethods: Array = (( const config = Object.freeze({ identiconType, + hashFormat: { + availableFormats: formats, + bech32Prefix, + }, hiddenViews, solidityscanEnabled: getEnvValue('NEXT_PUBLIC_VIEWS_CONTRACT_SOLIDITYSCAN_ENABLED') === 'true', extraVerificationMethods, diff --git a/configs/envs/.env.pw b/configs/envs/.env.pw index 95b518074a..5c9176651f 100644 --- a/configs/envs/.env.pw +++ b/configs/envs/.env.pw @@ -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 diff --git a/deploy/tools/envs-validator/schema.ts b/deploy/tools/envs-validator/schema.ts index 574901e1a9..00d2a136d5 100644 --- a/deploy/tools/envs-validator/schema.ts +++ b/deploy/tools/envs-validator/schema.ts @@ -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().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().oneOf(ADDRESS_FORMATS)), + NEXT_PUBLIC_VIEWS_ADDRESS_BECH_32_PREFIX: yup + .string() + .when('NEXT_PUBLIC_VIEWS_ADDRESS_FORMAT', { + is: (value: Array | 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) diff --git a/deploy/tools/envs-validator/test/.env.alt b/deploy/tools/envs-validator/test/.env.alt index 6482ea4197..c4f7c79a83 100644 --- a/deploy/tools/envs-validator/test/.env.alt +++ b/deploy/tools/envs-validator/test/.env.alt @@ -1,4 +1,6 @@ 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 diff --git a/deploy/tools/envs-validator/test/.env.base b/deploy/tools/envs-validator/test/.env.base index 36cca46b2b..3851db8f69 100644 --- a/deploy/tools/envs-validator/test/.env.base +++ b/deploy/tools/envs-validator/test/.env.base @@ -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'] diff --git a/docs/ENVS.md b/docs/ENVS.md index 681d6f76dc..a3a6046f8e 100644 --- a/docs/ENVS.md +++ b/docs/ENVS.md @@ -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` | 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+ | diff --git a/lib/address/bech32.ts b/lib/address/bech32.ts new file mode 100644 index 0000000000..85fcfda3ed --- /dev/null +++ b/lib/address/bech32.ts @@ -0,0 +1,49 @@ +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; +} diff --git a/lib/blob/guessDataType.ts b/lib/blob/guessDataType.ts index fb409019e3..ea149236a0 100644 --- a/lib/blob/guessDataType.ts +++ b/lib/blob/guessDataType.ts @@ -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]; diff --git a/lib/bytesToHex.ts b/lib/bytesToHex.ts new file mode 100644 index 0000000000..be2a1c2757 --- /dev/null +++ b/lib/bytesToHex.ts @@ -0,0 +1,8 @@ +export default function bytesToBase64(bytes: Uint8Array) { + let result = ''; + for (const byte of bytes) { + result += Number(byte).toString(16).padStart(2, '0'); + } + + return `0x${ result }`; +} diff --git a/lib/contexts/settings.tsx b/lib/contexts/settings.tsx new file mode 100644 index 0000000000..ea09e8730e --- /dev/null +++ b/lib/contexts/settings.tsx @@ -0,0 +1,56 @@ +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(null); + +export function SettingsContextProvider({ children }: SettingsProviderProps) { + const { cookies: appCookies } = useAppContext(); + const initialAddressFormat = cookies.get(cookies.NAMES.ADDRESS_FORMAT, appCookies); + + const [ addressFormat, setAddressFormat ] = React.useState( + 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 ( + + { children } + + ); +} + +export function useSettingsContext(disabled?: boolean) { + const context = React.useContext(SettingsContext); + if (context === undefined || disabled) { + return null; + } + return context; +} diff --git a/lib/cookies.ts b/lib/cookies.ts index cfcc2e5416..bae649df80 100644 --- a/lib/cookies.ts +++ b/lib/cookies.ts @@ -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', diff --git a/lib/hexToBase64.ts b/lib/hexToBase64.ts index 5b1366a6da..6fced2dc39 100644 --- a/lib/hexToBase64.ts +++ b/lib/hexToBase64.ts @@ -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); } diff --git a/lib/hexToBytes.ts b/lib/hexToBytes.ts index e34435fbf4..d42c931930 100644 --- a/lib/hexToBytes.ts +++ b/lib/hexToBytes.ts @@ -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); } diff --git a/lib/hexToUtf8.ts b/lib/hexToUtf8.ts index 8766ee25cc..95e40ba090 100644 --- a/lib/hexToUtf8.ts +++ b/lib/hexToUtf8.ts @@ -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); } diff --git a/middleware.ts b/middleware.ts index 2f017d9cd5..cd820f475e 100644 --- a/middleware.ts +++ b/middleware.ts @@ -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(); diff --git a/nextjs/middlewares/addressFormat.ts b/nextjs/middlewares/addressFormat.ts new file mode 100644 index 0000000000..bfd8bf14e0 --- /dev/null +++ b/nextjs/middlewares/addressFormat.ts @@ -0,0 +1,20 @@ +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: '/' }); + } +} diff --git a/nextjs/middlewares/index.ts b/nextjs/middlewares/index.ts index b9466373a3..4e4fdbef4e 100644 --- a/nextjs/middlewares/index.ts +++ b/nextjs/middlewares/index.ts @@ -1,2 +1,3 @@ export { account } from './account'; export { default as colorTheme } from './colorTheme'; +export { default as addressFormat } from './addressFormat'; diff --git a/package.json b/package.json index 06016e4572..0ba86747e8 100644 --- a/package.json +++ b/package.json @@ -57,6 +57,7 @@ "@opentelemetry/sdk-node": "0.49.1", "@opentelemetry/sdk-trace-node": "1.22.0", "@opentelemetry/semantic-conventions": "1.22.0", + "@scure/base": "1.1.9", "@sentry/cli": "^2.21.2", "@sentry/react": "7.24.0", "@sentry/tracing": "7.24.0", diff --git a/pages/_app.tsx b/pages/_app.tsx index 525c9ce88a..08a9866598 100644 --- a/pages/_app.tsx +++ b/pages/_app.tsx @@ -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) { - { getLayout() } - { config.features.rewards.isEnabled && } + + { getLayout() } + { config.features.rewards.isEnabled && } + diff --git a/playwright/TestApp.tsx b/playwright/TestApp.tsx index aaa4bc7259..264e821c47 100644 --- a/playwright/TestApp.tsx +++ b/playwright/TestApp.tsx @@ -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 - - - - { children } - - - + + + + + { children } + + + + diff --git a/playwright/fixtures/mockEnvs.ts b/playwright/fixtures/mockEnvs.ts index e010a8c4f1..689091e225 100644 --- a/playwright/fixtures/mockEnvs.ts +++ b/playwright/fixtures/mockEnvs.ts @@ -84,4 +84,8 @@ export const ENVS_MAP: Record> = { 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' ], + ], }; diff --git a/types/views/address.ts b/types/views/address.ts index 271e661e02..98ac1844b0 100644 --- a/types/views/address.ts +++ b/types/views/address.ts @@ -14,3 +14,6 @@ export const ADDRESS_VIEWS_IDS = [ ] as const; export type AddressViewId = ArrayElement; + +export const ADDRESS_FORMATS = [ 'base16', 'bech32' ] as const; +export type AddressFormat = typeof ADDRESS_FORMATS[ number ]; diff --git a/ui/address/AddressDetails.tsx b/ui/address/AddressDetails.tsx index 613f31e74c..9396c2fe88 100644 --- a/ui/address/AddressDetails.tsx +++ b/ui/address/AddressDetails.tsx @@ -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" > + + { data.filecoin?.id && ( <> { truncation={ isMobile ? 'constant' : 'dynamic' } fontWeight={ 600 } ml={ 2 } + noAltHash /> diff --git a/ui/address/details/AddressAlternativeFormat.tsx b/ui/address/details/AddressAlternativeFormat.tsx new file mode 100644 index 0000000000..18da689936 --- /dev/null +++ b/ui/address/details/AddressAlternativeFormat.tsx @@ -0,0 +1,46 @@ +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 ( + <> + + { label } + + + + + + ); +}; + +export default React.memo(AddressAlternativeFormat); diff --git a/ui/address/utils/useCheckAddressFormat.ts b/ui/address/utils/useCheckAddressFormat.ts new file mode 100644 index 0000000000..f4b3315f09 --- /dev/null +++ b/ui/address/utils/useCheckAddressFormat.ts @@ -0,0 +1,26 @@ +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; +} diff --git a/ui/blob/BlobData.tsx b/ui/blob/BlobData.tsx index dad79e30fa..f5e0f56338 100644 --- a/ui/blob/BlobData.tsx +++ b/ui/blob/BlobData.tsx @@ -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 ; } - const bytes = new Uint8Array(hexToBytes(data)); + const bytes = hexToBytes(data); const filteredBytes = removeNonSignificantZeroBytes(bytes); const base64 = bytesToBase64(filteredBytes); diff --git a/ui/myProfile/MyProfileWallet.tsx b/ui/myProfile/MyProfileWallet.tsx index 5a9ce56f1b..210acf8ef7 100644 --- a/ui/myProfile/MyProfileWallet.tsx +++ b/ui/myProfile/MyProfileWallet.tsx @@ -35,6 +35,7 @@ const MyProfileWallet = ({ profileQuery, onAddWallet }: Props) => { ) : } diff --git a/ui/pages/Address.tsx b/ui/pages/Address.tsx index dc9f52090d..315852530d 100644 --- a/ui/pages/Address.tsx +++ b/ui/pages/Address.tsx @@ -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', { diff --git a/ui/pages/SearchResults.tsx b/ui/pages/SearchResults.tsx index b895c0ebab..b25b40d827 100644 --- a/ui/pages/SearchResults.tsx +++ b/ui/pages/SearchResults.tsx @@ -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 } /> )) } @@ -164,6 +167,7 @@ const SearchResultsPageContent = () => { data={ item } searchTerm={ debouncedSearchTerm } isLoading={ isLoading } + addressFormat={ settingsContext?.addressFormat } /> )) } diff --git a/ui/pages/__screenshots__/Address.pw.tsx_default_degradation-view-1.png b/ui/pages/__screenshots__/Address.pw.tsx_default_degradation-view-1.png index 1df45db0e7..dfecabe5e5 100644 Binary files a/ui/pages/__screenshots__/Address.pw.tsx_default_degradation-view-1.png and b/ui/pages/__screenshots__/Address.pw.tsx_default_degradation-view-1.png differ diff --git a/ui/searchResults/SearchResultListItem.tsx b/ui/searchResults/SearchResultListItem.tsx index 45aa6c76a1..464df8f36c 100644 --- a/ui/searchResults/SearchResultListItem.tsx +++ b/ui/searchResults/SearchResultListItem.tsx @@ -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) => { 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) => { > - + ); } @@ -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 ( - + { data.is_smart_contract_verified && } @@ -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 ( - + { data.is_smart_contract_verified && } @@ -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 ( - + { data.ens_info.names_count > 1 ? diff --git a/ui/searchResults/SearchResultTableItem.tsx b/ui/searchResults/SearchResultTableItem.tsx index 5b13563aca..d1baa10b02 100644 --- a/ui/searchResults/SearchResultTableItem.tsx +++ b/ui/searchResults/SearchResultTableItem.tsx @@ -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) => { 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) => { - + { data.is_smart_contract_verified && } @@ -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) => { > - + { 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 ( <> @@ -177,7 +184,7 @@ const SearchResultTableItem = ({ data, searchTerm, isLoading }: Props) => { - + { data.is_smart_contract_verified && } @@ -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 ( <> @@ -395,7 +404,7 @@ const SearchResultTableItem = ({ data, searchTerm, isLoading }: Props) => { - + { data.is_smart_contract_verified && } diff --git a/ui/shared/entities/address/AddressEntity.pw.tsx b/ui/shared/entities/address/AddressEntity.pw.tsx index a06d3a6092..1df0d4b99f 100644 --- a/ui/shared/entities/address/AddressEntity.pw.tsx +++ b/ui/shared/entities/address/AddressEntity.pw.tsx @@ -1,10 +1,14 @@ 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( + , + ); + + await expect(component).toHaveScreenshot(); +}); diff --git a/ui/shared/entities/address/AddressEntity.tsx b/ui/shared/entities/address/AddressEntity.tsx index 422929e3f3..24ff651fac 100644 --- a/ui/shared/entities/address/AddressEntity.tsx +++ b/ui/shared/entities/address/AddressEntity.tsx @@ -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 & Pick; +export type ContentProps = Omit & Pick & { 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 = ( { nameText } - { props.address.filecoin?.robust ?? props.address.hash } + { props.address.filecoin?.robust ?? props.altHash ?? props.address.hash } ); @@ -115,18 +117,18 @@ const Content = chakra((props: ContentProps) => { return ( ); }); -type CopyProps = Omit & Pick; +type CopyProps = Omit & Pick & { altHash?: string }; const Copy = (props: CopyProps) => { return ( ); }; @@ -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 ( - + - + ); }; diff --git a/ui/shared/entities/address/AddressEntityContentProxy.tsx b/ui/shared/entities/address/AddressEntityContentProxy.tsx index 73039f71cc..77736eb80f 100644 --- a/ui/shared/entities/address/AddressEntityContentProxy.tsx +++ b/ui/shared/entities/address/AddressEntityContentProxy.tsx @@ -32,7 +32,7 @@ const AddressEntityContentProxy = (props: ContentProps) => { diff --git a/ui/shared/entities/address/__screenshots__/AddressEntity.pw.tsx_default_bech32-format-1.png b/ui/shared/entities/address/__screenshots__/AddressEntity.pw.tsx_default_bech32-format-1.png new file mode 100644 index 0000000000..bc47ca2589 Binary files /dev/null and b/ui/shared/entities/address/__screenshots__/AddressEntity.pw.tsx_default_bech32-format-1.png differ diff --git a/ui/snippets/searchBar/SearchBarSuggest/SearchBarSuggest.tsx b/ui/snippets/searchBar/SearchBarSuggest/SearchBarSuggest.tsx index ae5f5bbb8a..2d54c0bd4c 100644 --- a/ui/snippets/searchBar/SearchBarSuggest/SearchBarSuggest.tsx +++ b/ui/snippets/searchBar/SearchBarSuggest/SearchBarSuggest.tsx @@ -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>([]); const tabsRef = React.useRef(null); @@ -165,9 +167,16 @@ const SearchBarSuggest = ({ query, searchTerm, onItemClick, containerId }: Props > { cat.title } - { cat.id !== 'app' && itemsGroups[cat.id]?.map((item, index) => - , - ) } + { cat.id !== 'app' && itemsGroups[cat.id]?.map((item, index) => ( + + )) } { cat.id === 'app' && itemsGroups[cat.id]?.map((item, index) => , ) } diff --git a/ui/snippets/searchBar/SearchBarSuggest/SearchBarSuggestAddress.tsx b/ui/snippets/searchBar/SearchBarSuggest/SearchBarSuggestAddress.tsx index 4304e095de..4e864ecad9 100644 --- a/ui/snippets/searchBar/SearchBarSuggest/SearchBarSuggestAddress.tsx +++ b/ui/snippets/searchBar/SearchBarSuggest/SearchBarSuggestAddress.tsx @@ -1,8 +1,10 @@ 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) => { const shouldHighlightHash = ADDRESS_REGEXP.test(searchTerm); + const hash = data.filecoin_robust_address || (addressFormat === 'bech32' ? toBech32Address(data.address) : data.address); const icon = ( { { data.certified && } ); - const addressEl = ; + const addressEl = ; if (isMobile) { return ( diff --git a/ui/snippets/searchBar/SearchBarSuggest/SearchBarSuggestBlob.tsx b/ui/snippets/searchBar/SearchBarSuggest/SearchBarSuggestBlob.tsx index 6c852d9b7c..9b635350be 100644 --- a/ui/snippets/searchBar/SearchBarSuggest/SearchBarSuggestBlob.tsx +++ b/ui/snippets/searchBar/SearchBarSuggest/SearchBarSuggestBlob.tsx @@ -1,17 +1,13 @@ 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) => { return ( diff --git a/ui/snippets/searchBar/SearchBarSuggest/SearchBarSuggestBlock.tsx b/ui/snippets/searchBar/SearchBarSuggest/SearchBarSuggestBlock.tsx index d0cb352a9a..3b9d8f2fee 100644 --- a/ui/snippets/searchBar/SearchBarSuggest/SearchBarSuggestBlock.tsx +++ b/ui/snippets/searchBar/SearchBarSuggest/SearchBarSuggestBlock.tsx @@ -1,6 +1,7 @@ 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) => { const icon = ; const shouldHighlightHash = data.block_hash.toLowerCase() === searchTerm.toLowerCase(); const isFutureBlock = data.timestamp === undefined; diff --git a/ui/snippets/searchBar/SearchBarSuggest/SearchBarSuggestDomain.tsx b/ui/snippets/searchBar/SearchBarSuggest/SearchBarSuggestDomain.tsx index 37b2de65b1..421aecdce9 100644 --- a/ui/snippets/searchBar/SearchBarSuggest/SearchBarSuggestDomain.tsx +++ b/ui/snippets/searchBar/SearchBarSuggest/SearchBarSuggestDomain.tsx @@ -1,21 +1,18 @@ 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) => { const icon = ; + const hash = data.filecoin_robust_address || (addressFormat === 'bech32' ? toBech32Address(data.address) : data.address); const name = ( { whiteSpace="nowrap" variant="secondary" > - + ); diff --git a/ui/snippets/searchBar/SearchBarSuggest/SearchBarSuggestItem.tsx b/ui/snippets/searchBar/SearchBarSuggest/SearchBarSuggestItem.tsx index e33256e168..7b8af1ef21 100644 --- a/ui/snippets/searchBar/SearchBarSuggest/SearchBarSuggestItem.tsx +++ b/ui/snippets/searchBar/SearchBarSuggest/SearchBarSuggestItem.tsx @@ -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) => 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 ; + return ( + + ); } case 'contract': case 'address': { - return ; + return ( + + ); } case 'label': { - return ; - + return ( + + ); } case 'block': { return ; @@ -84,7 +106,7 @@ const SearchBarSuggestItem = ({ data, isMobile, searchTerm, onClick }: Props) => return ; } case 'ens_domain': { - return ; + return ; } } })(); diff --git a/ui/snippets/searchBar/SearchBarSuggest/SearchBarSuggestLabel.tsx b/ui/snippets/searchBar/SearchBarSuggest/SearchBarSuggestLabel.tsx index dbb98005b4..20e1a58a94 100644 --- a/ui/snippets/searchBar/SearchBarSuggest/SearchBarSuggestLabel.tsx +++ b/ui/snippets/searchBar/SearchBarSuggest/SearchBarSuggestLabel.tsx @@ -1,20 +1,17 @@ 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) => { const icon = ; + const hash = data.filecoin_robust_address || (addressFormat === 'bech32' ? toBech32Address(data.address) : data.address); const name = ( { whiteSpace="nowrap" variant="secondary" > - + ); diff --git a/ui/snippets/searchBar/SearchBarSuggest/SearchBarSuggestToken.tsx b/ui/snippets/searchBar/SearchBarSuggest/SearchBarSuggestToken.tsx index 78d23f9f78..aa2d38c999 100644 --- a/ui/snippets/searchBar/SearchBarSuggest/SearchBarSuggestToken.tsx +++ b/ui/snippets/searchBar/SearchBarSuggest/SearchBarSuggestToken.tsx @@ -1,22 +1,20 @@ 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) => { const icon = ; const verifiedIcon = ; + const hash = data.filecoin_robust_address || (addressFormat === 'bech32' ? toBech32Address(data.address) : data.address); + const name = ( { const address = ( - + ); diff --git a/ui/snippets/searchBar/SearchBarSuggest/SearchBarSuggestTx.tsx b/ui/snippets/searchBar/SearchBarSuggest/SearchBarSuggestTx.tsx index 3c9c74375f..f6572d2e98 100644 --- a/ui/snippets/searchBar/SearchBarSuggest/SearchBarSuggestTx.tsx +++ b/ui/snippets/searchBar/SearchBarSuggest/SearchBarSuggestTx.tsx @@ -1,19 +1,14 @@ 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) => { const icon = ; const hash = ( diff --git a/ui/snippets/searchBar/SearchBarSuggest/SearchBarSuggestUserOp.tsx b/ui/snippets/searchBar/SearchBarSuggest/SearchBarSuggestUserOp.tsx index 21e0701822..c65e43de07 100644 --- a/ui/snippets/searchBar/SearchBarSuggest/SearchBarSuggestUserOp.tsx +++ b/ui/snippets/searchBar/SearchBarSuggest/SearchBarSuggestUserOp.tsx @@ -1,19 +1,14 @@ 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) => { const icon = ; const hash = ( @@ -45,4 +40,4 @@ const SearchBarSuggestTx = ({ data, isMobile }: Props) => { ); }; -export default React.memo(SearchBarSuggestTx); +export default React.memo(SearchBarSuggestUserOp); diff --git a/ui/snippets/searchBar/SearchBarSuggest/types.ts b/ui/snippets/searchBar/SearchBarSuggest/types.ts new file mode 100644 index 0000000000..f69f857e76 --- /dev/null +++ b/ui/snippets/searchBar/SearchBarSuggest/types.ts @@ -0,0 +1,8 @@ +import type { AddressFormat } from 'types/views/address'; + +export interface ItemsProps { + data: Data; + searchTerm: string; + isMobile?: boolean | undefined; + addressFormat?: AddressFormat; +} diff --git a/ui/snippets/searchBar/useQuickSearchQuery.tsx b/ui/snippets/searchBar/useQuickSearchQuery.tsx index 585d6f1280..8ef224ff85 100644 --- a/ui/snippets/searchBar/useQuickSearchQuery.tsx +++ b/ui/snippets/searchBar/useQuickSearchQuery.tsx @@ -1,5 +1,6 @@ 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 }, }); diff --git a/ui/snippets/searchBar/useSearchQuery.tsx b/ui/snippets/searchBar/useSearchQuery.tsx index da24b3e0d7..5f87fabf15 100644 --- a/ui/snippets/searchBar/useSearchQuery.tsx +++ b/ui/snippets/searchBar/useSearchQuery.tsx @@ -1,6 +1,7 @@ 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 }), diff --git a/ui/snippets/topBar/__screenshots__/TopBar.pw.tsx_dark-color-mode_default-view-dark-mode-mobile-2.png b/ui/snippets/topBar/__screenshots__/TopBar.pw.tsx_dark-color-mode_default-view-dark-mode-mobile-2.png index 45b34d445e..69ea9699de 100644 Binary files a/ui/snippets/topBar/__screenshots__/TopBar.pw.tsx_dark-color-mode_default-view-dark-mode-mobile-2.png and b/ui/snippets/topBar/__screenshots__/TopBar.pw.tsx_dark-color-mode_default-view-dark-mode-mobile-2.png differ diff --git a/ui/snippets/topBar/__screenshots__/TopBar.pw.tsx_default_default-view-dark-mode-mobile-2.png b/ui/snippets/topBar/__screenshots__/TopBar.pw.tsx_default_default-view-dark-mode-mobile-2.png index 7b545b0cc3..2e8e709266 100644 Binary files a/ui/snippets/topBar/__screenshots__/TopBar.pw.tsx_default_default-view-dark-mode-mobile-2.png and b/ui/snippets/topBar/__screenshots__/TopBar.pw.tsx_default_default-view-dark-mode-mobile-2.png differ diff --git a/ui/snippets/topBar/__screenshots__/TopBar.pw.tsx_mobile_default-view-dark-mode-mobile-2.png b/ui/snippets/topBar/__screenshots__/TopBar.pw.tsx_mobile_default-view-dark-mode-mobile-2.png index 004fda31b0..cc43c45930 100644 Binary files a/ui/snippets/topBar/__screenshots__/TopBar.pw.tsx_mobile_default-view-dark-mode-mobile-2.png and b/ui/snippets/topBar/__screenshots__/TopBar.pw.tsx_mobile_default-view-dark-mode-mobile-2.png differ diff --git a/ui/snippets/topBar/settings/Settings.tsx b/ui/snippets/topBar/settings/Settings.tsx index 95b445ab47..065a992230 100644 --- a/ui/snippets/topBar/settings/Settings.tsx +++ b/ui/snippets/topBar/settings/Settings.tsx @@ -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 = () => { + diff --git a/ui/snippets/topBar/settings/SettingsAddressFormat.tsx b/ui/snippets/topBar/settings/SettingsAddressFormat.tsx new file mode 100644 index 0000000000..a8ae61a861 --- /dev/null +++ b/ui/snippets/topBar/settings/SettingsAddressFormat.tsx @@ -0,0 +1,27 @@ +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 ( + + + Show { config.UI.views.address.hashFormat.bech32Prefix }{ BECH_32_SEPARATOR } format + + + + ); +}; + +export default React.memo(SettingsAddressFormat); diff --git a/ui/snippets/topBar/settings/SettingsIdentIcon.tsx b/ui/snippets/topBar/settings/SettingsIdentIcon.tsx index 97a2b8af2c..16924ddc5f 100644 --- a/ui/snippets/topBar/settings/SettingsIdentIcon.tsx +++ b/ui/snippets/topBar/settings/SettingsIdentIcon.tsx @@ -32,7 +32,7 @@ const SettingsIdentIcon = () => { return (
- Address identicon + Address settings { activeIdenticon?.label } { IDENTICONS.map((identicon) => ( diff --git a/ui/snippets/user/profile/UserProfileContentWallet.tsx b/ui/snippets/user/profile/UserProfileContentWallet.tsx index f811b6809f..3899c38046 100644 --- a/ui/snippets/user/profile/UserProfileContentWallet.tsx +++ b/ui/snippets/user/profile/UserProfileContentWallet.tsx @@ -42,6 +42,7 @@ const UserProfileContentWallet = ({ onClose, className }: Props) => { truncation="dynamic" fontSize="sm" fontWeight={ 500 } + noAltHash /> { /> { data.certified && } - - - - + Balance { currencyUnits.ether } diff --git a/ui/verifiedContracts/VerifiedContractsTableItem.tsx b/ui/verifiedContracts/VerifiedContractsTableItem.tsx index 2771a07f55..dde4e6c5d6 100644 --- a/ui/verifiedContracts/VerifiedContractsTableItem.tsx +++ b/ui/verifiedContracts/VerifiedContractsTableItem.tsx @@ -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 && } - - - - - - + diff --git a/yarn.lock b/yarn.lock index 3fb427db5a..c088af43f2 100644 --- a/yarn.lock +++ b/yarn.lock @@ -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"