fix: use shortenAddress for mini portfolio addresses (#6774)

* fix: use shortenAddress for mini portfolio addresses

* Update src/utils/addresses.ts

Co-authored-by: Jordan Frankfurt <jordanwfrankfurt@gmail.com>

---------

Co-authored-by: Jordan Frankfurt <jordanwfrankfurt@gmail.com>
This commit is contained in:
Nate Wienert 2023-06-16 08:51:03 -10:00 committed by GitHub
parent 6528fd136e
commit f27bba9ffa
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
17 changed files with 108 additions and 87 deletions

@ -24,8 +24,8 @@ import { useAppDispatch } from 'state/hooks'
import { updateSelectedWallet } from 'state/user/reducer' import { updateSelectedWallet } from 'state/user/reducer'
import styled, { useTheme } from 'styled-components/macro' import styled, { useTheme } from 'styled-components/macro'
import { CopyHelper, ExternalLink, ThemedText } from 'theme' import { CopyHelper, ExternalLink, ThemedText } from 'theme'
import { shortenAddress } from 'utils'
import { shortenAddress } from '../../nft/utils/address'
import { useCloseModal, useFiatOnrampAvailability, useOpenModal, useToggleModal } from '../../state/application/hooks' import { useCloseModal, useFiatOnrampAvailability, useOpenModal, useToggleModal } from '../../state/application/hooks'
import { ApplicationModal } from '../../state/application/reducer' import { ApplicationModal } from '../../state/application/reducer'
import { useUserHasAvailableClaim, useUserUnclaimedAmount } from '../../state/claim/hooks' import { useUserHasAvailableClaim, useUserUnclaimedAmount } from '../../state/claim/hooks'
@ -244,12 +244,12 @@ export default function AuthenticatedHeader({ account, openSettings }: { account
{account && ( {account && (
<AccountNamesWrapper> <AccountNamesWrapper>
<ThemedText.SubHeader> <ThemedText.SubHeader>
<CopyText toCopy={ENSName ?? account}>{ENSName ?? shortenAddress(account, 4, 4)}</CopyText> <CopyText toCopy={ENSName ?? account}>{ENSName ?? shortenAddress(account)}</CopyText>
</ThemedText.SubHeader> </ThemedText.SubHeader>
{/* Displays smaller view of account if ENS name was rendered above */} {/* Displays smaller view of account if ENS name was rendered above */}
{ENSName && ( {ENSName && (
<ThemedText.BodySmall color="textTertiary"> <ThemedText.BodySmall color="textTertiary">
<CopyText toCopy={account}>{shortenAddress(account, 4, 4)}</CopyText> <CopyText toCopy={account}>{shortenAddress(account)}</CopyText>
</ThemedText.BodySmall> </ThemedText.BodySmall>
)} )}
</AccountNamesWrapper> </AccountNamesWrapper>

@ -7,6 +7,7 @@ import { TransactionStatus } from 'graphql/data/__generated__/types-and-hooks'
import useENSName from 'hooks/useENSName' import useENSName from 'hooks/useENSName'
import styled from 'styled-components/macro' import styled from 'styled-components/macro'
import { EllipsisStyle, ThemedText } from 'theme' import { EllipsisStyle, ThemedText } from 'theme'
import { shortenAddress } from 'utils'
import { ExplorerDataType, getExplorerLink } from 'utils/getExplorerLink' import { ExplorerDataType, getExplorerLink } from 'utils/getExplorerLink'
import { PortfolioLogo } from '../PortfolioLogo' import { PortfolioLogo } from '../PortfolioLogo'
@ -52,7 +53,7 @@ export function ActivityRow({
descriptor={ descriptor={
<ActivityRowDescriptor color="textSecondary"> <ActivityRowDescriptor color="textSecondary">
{descriptor} {descriptor}
{ENSName ?? otherAccount} {ENSName ?? shortenAddress(otherAccount)}
</ActivityRowDescriptor> </ActivityRowDescriptor>
} }
right={ right={

@ -1,7 +1,6 @@
import React from 'react' import React from 'react'
import styled from 'styled-components/macro' import styled from 'styled-components/macro'
import { escapeRegExp } from 'utils'
import { escapeRegExp } from '../../utils'
const StyledInput = styled.input<{ error?: boolean; fontSize?: string; align?: string }>` const StyledInput = styled.input<{ error?: boolean; fontSize?: string; align?: string }>`
color: ${({ error, theme }) => (error ? theme.accentFailure : theme.textPrimary)}; color: ${({ error, theme }) => (error ? theme.accentFailure : theme.textPrimary)};

@ -15,10 +15,10 @@ import { useCallback, useMemo } from 'react'
import styled from 'styled-components/macro' import styled from 'styled-components/macro'
import { colors } from 'theme/colors' import { colors } from 'theme/colors'
import { flexRowNoWrap } from 'theme/styles' import { flexRowNoWrap } from 'theme/styles'
import { shortenAddress } from 'utils'
import { isTransactionRecent, useAllTransactions } from '../../state/transactions/hooks' import { isTransactionRecent, useAllTransactions } from '../../state/transactions/hooks'
import { TransactionDetails } from '../../state/transactions/types' import { TransactionDetails } from '../../state/transactions/types'
import { shortenAddress } from '../../utils'
import { ButtonSecondary } from '../Button' import { ButtonSecondary } from '../Button'
import StatusIcon from '../Identicon/StatusIcon' import StatusIcon from '../Identicon/StatusIcon'
import { RowBetween } from '../Row' import { RowBetween } from '../Row'

@ -5,6 +5,7 @@ import { useWeb3React } from '@web3-react/core'
import { useState } from 'react' import { useState } from 'react'
import { Text } from 'rebass' import { Text } from 'rebass'
import styled from 'styled-components/macro' import styled from 'styled-components/macro'
import { shortenAddress } from 'utils'
import Circle from '../../assets/images/blue-loader.svg' import Circle from '../../assets/images/blue-loader.svg'
import tokenLogo from '../../assets/images/token-logo.png' import tokenLogo from '../../assets/images/token-logo.png'
@ -12,7 +13,6 @@ import useENS from '../../hooks/useENS'
import { useClaimCallback, useUserHasAvailableClaim, useUserUnclaimedAmount } from '../../state/claim/hooks' import { useClaimCallback, useUserHasAvailableClaim, useUserUnclaimedAmount } from '../../state/claim/hooks'
import { useIsTransactionPending } from '../../state/transactions/hooks' import { useIsTransactionPending } from '../../state/transactions/hooks'
import { CloseIcon, CustomLightSpinner, ExternalLink, ThemedText, UniTokenAnimated } from '../../theme' import { CloseIcon, CustomLightSpinner, ExternalLink, ThemedText, UniTokenAnimated } from '../../theme'
import { shortenAddress } from '../../utils'
import { ExplorerDataType, getExplorerLink } from '../../utils/getExplorerLink' import { ExplorerDataType, getExplorerLink } from '../../utils/getExplorerLink'
import AddressInputPanel from '../AddressInputPanel' import AddressInputPanel from '../AddressInputPanel'
import { ButtonPrimary } from '../Button' import { ButtonPrimary } from '../Button'

@ -32,8 +32,7 @@ import { WRAPPED_NATIVE_CURRENCY } from 'constants/tokens'
import { useMemo } from 'react' import { useMemo } from 'react'
import { NonfungiblePositionManager, Quoter, QuoterV2, TickLens, UniswapInterfaceMulticall } from 'types/v3' import { NonfungiblePositionManager, Quoter, QuoterV2, TickLens, UniswapInterfaceMulticall } from 'types/v3'
import { V3Migrator } from 'types/v3/V3Migrator' import { V3Migrator } from 'types/v3/V3Migrator'
import { getContract } from 'utils'
import { getContract } from '../utils'
const { abi: IUniswapV2PairABI } = IUniswapV2PairJson const { abi: IUniswapV2PairABI } = IUniswapV2PairJson
const { abi: IUniswapV2Router02ABI } = IUniswapV2Router02Json const { abi: IUniswapV2Router02ABI } = IUniswapV2Router02Json

@ -25,7 +25,6 @@ import {
TokenRarity, TokenRarity,
} from 'nft/types' } from 'nft/types'
import { getMarketplaceIcon } from 'nft/utils' import { getMarketplaceIcon } from 'nft/utils'
import { shortenAddress } from 'nft/utils/address'
import { buildActivityAsset } from 'nft/utils/buildActivityAsset' import { buildActivityAsset } from 'nft/utils/buildActivityAsset'
import { formatEth } from 'nft/utils/currency' import { formatEth } from 'nft/utils/currency'
import { getTimeDifference } from 'nft/utils/date' import { getTimeDifference } from 'nft/utils/date'
@ -33,6 +32,7 @@ import { putCommas } from 'nft/utils/putCommas'
import { MouseEvent, ReactNode, useMemo, useState } from 'react' import { MouseEvent, ReactNode, useMemo, useState } from 'react'
import styled from 'styled-components/macro' import styled from 'styled-components/macro'
import { ExternalLink } from 'theme' import { ExternalLink } from 'theme'
import { shortenAddress } from 'utils'
import { ExplorerDataType, getExplorerLink } from 'utils/getExplorerLink' import { ExplorerDataType, getExplorerLink } from 'utils/getExplorerLink'
import * as styles from './Activity.css' import * as styles from './Activity.css'
@ -165,7 +165,7 @@ export const AddressCell = ({ address, desktopLBreakpoint, chainId }: AddressCel
href={getExplorerLink(chainId ?? ChainId.MAINNET, address ?? '', ExplorerDataType.ADDRESS)} href={getExplorerLink(chainId ?? ChainId.MAINNET, address ?? '', ExplorerDataType.ADDRESS)}
style={{ textDecoration: 'none' }} style={{ textDecoration: 'none' }}
> >
<Box onClick={(e) => e.stopPropagation()}>{address ? shortenAddress(address, 2, 4) : '-'}</Box> <Box onClick={(e) => e.stopPropagation()}>{address ? shortenAddress(address, 2) : '-'}</Box>
</AddressLink> </AddressLink>
</Column> </Column>
) )

@ -4,11 +4,11 @@ import { LoadingBubble } from 'components/Tokens/loading'
import { EventCell } from 'nft/components/collection/ActivityCells' import { EventCell } from 'nft/components/collection/ActivityCells'
import { ActivityEvent } from 'nft/types' import { ActivityEvent } from 'nft/types'
import { getMarketplaceIcon } from 'nft/utils' import { getMarketplaceIcon } from 'nft/utils'
import { shortenAddress } from 'nft/utils/address'
import { formatEth } from 'nft/utils/currency' import { formatEth } from 'nft/utils/currency'
import { getTimeDifference } from 'nft/utils/date' import { getTimeDifference } from 'nft/utils/date'
import { ReactNode } from 'react' import { ReactNode } from 'react'
import styled from 'styled-components/macro' import styled from 'styled-components/macro'
import { shortenAddress } from 'utils'
const TR = styled.tr` const TR = styled.tr`
border-bottom: ${({ theme }) => `1px solid ${theme.backgroundOutline}`}; border-bottom: ${({ theme }) => `1px solid ${theme.backgroundOutline}`};
@ -177,7 +177,7 @@ const AssetActivity = ({ events }: { events?: ActivityEvent[] }) => {
<TD> <TD>
{fromAddress && ( {fromAddress && (
<Link href={`https://etherscan.io/address/${fromAddress}`} target="_blank" rel="noopener noreferrer"> <Link href={`https://etherscan.io/address/${fromAddress}`} target="_blank" rel="noopener noreferrer">
{shortenAddress(fromAddress, 2, 4)} {shortenAddress(fromAddress, 2)}
</Link> </Link>
)} )}
</TD> </TD>
@ -185,7 +185,7 @@ const AssetActivity = ({ events }: { events?: ActivityEvent[] }) => {
<TD> <TD>
{toAddress && ( {toAddress && (
<Link href={`https://etherscan.io/address/${toAddress}`} target="_blank" rel="noopener noreferrer"> <Link href={`https://etherscan.io/address/${toAddress}`} target="_blank" rel="noopener noreferrer">
{shortenAddress(toAddress, 2, 4)} {shortenAddress(toAddress, 2)}
</Link> </Link>
)} )}
</TD> </TD>

@ -10,7 +10,6 @@ import { AssetPriceDetails } from 'nft/components/details/AssetPriceDetails'
import { Center } from 'nft/components/Flex' import { Center } from 'nft/components/Flex'
import { themeVars, vars } from 'nft/css/sprinkles.css' import { themeVars, vars } from 'nft/css/sprinkles.css'
import { ActivityEventType, CollectionInfoForAsset, GenieAsset } from 'nft/types' import { ActivityEventType, CollectionInfoForAsset, GenieAsset } from 'nft/types'
import { shortenAddress } from 'nft/utils/address'
import { formatEth } from 'nft/utils/currency' import { formatEth } from 'nft/utils/currency'
import { isAudio } from 'nft/utils/isAudio' import { isAudio } from 'nft/utils/isAudio'
import { isVideo } from 'nft/utils/isVideo' import { isVideo } from 'nft/utils/isVideo'
@ -20,6 +19,7 @@ import InfiniteScroll from 'react-infinite-scroll-component'
import { Link as RouterLink } from 'react-router-dom' import { Link as RouterLink } from 'react-router-dom'
import styled from 'styled-components/macro' import styled from 'styled-components/macro'
import { useIsDarkMode } from 'theme/components/ThemeToggle' import { useIsDarkMode } from 'theme/components/ThemeToggle'
import { shortenAddress } from 'utils/addresses'
import AssetActivity, { LoadingAssetActivity } from './AssetActivity' import AssetActivity, { LoadingAssetActivity } from './AssetActivity'
import * as styles from './AssetDetails.css' import * as styles from './AssetDetails.css'
@ -427,7 +427,7 @@ export const AssetDetails = ({ asset, collection }: AssetDetailsProps) => {
target="_blank" target="_blank"
rel="noopener noreferrer" rel="noopener noreferrer"
> >
{shortenAddress(asset.creator.address, 2, 4)} {shortenAddress(asset.creator.address, 2)}
</AddressTextLink> </AddressTextLink>
)} )}

@ -16,13 +16,13 @@ import {
timeLeft, timeLeft,
useUsdPrice, useUsdPrice,
} from 'nft/utils' } from 'nft/utils'
import { shortenAddress } from 'nft/utils/address'
import { useMemo } from 'react' import { useMemo } from 'react'
import { Upload } from 'react-feather' import { Upload } from 'react-feather'
import { useQuery } from 'react-query' import { useQuery } from 'react-query'
import { Link, useNavigate } from 'react-router-dom' import { Link, useNavigate } from 'react-router-dom'
import styled, { css, useTheme } from 'styled-components/macro' import styled, { css, useTheme } from 'styled-components/macro'
import { ExternalLink, ThemedText } from 'theme' import { ExternalLink, ThemedText } from 'theme'
import { shortenAddress } from 'utils/addresses'
const TWITTER_WIDTH = 560 const TWITTER_WIDTH = 560
const TWITTER_HEIGHT = 480 const TWITTER_HEIGHT = 480
@ -425,7 +425,7 @@ export const AssetPriceDetails = ({ asset, collection }: AssetPriceDetailsProps)
{asset.tokenType === 'ERC1155' ? ( {asset.tokenType === 'ERC1155' ? (
'' ''
) : ( ) : (
<span> {isOwner ? 'You' : asset.ownerAddress && shortenAddress(asset.ownerAddress, 2, 4)}</span> <span> {isOwner ? 'You' : asset.ownerAddress && shortenAddress(asset.ownerAddress, 2)}</span>
)} )}
</OwnerText> </OwnerText>
</OwnerInformationContainer> </OwnerInformationContainer>

@ -2,10 +2,10 @@ import { OpacityHoverState } from 'components/Common'
import useCopyClipboard from 'hooks/useCopyClipboard' import useCopyClipboard from 'hooks/useCopyClipboard'
import { CollectionInfoForAsset, GenieAsset } from 'nft/types' import { CollectionInfoForAsset, GenieAsset } from 'nft/types'
import { putCommas } from 'nft/utils' import { putCommas } from 'nft/utils'
import { shortenAddress } from 'nft/utils/address'
import { useCallback } from 'react' import { useCallback } from 'react'
import { Copy } from 'react-feather' import { Copy } from 'react-feather'
import styled from 'styled-components/macro' import styled from 'styled-components/macro'
import { shortenAddress } from 'utils'
const Details = styled.div` const Details = styled.div`
display: grid; display: grid;
@ -80,7 +80,7 @@ const DetailsContainer = ({ asset, collection }: { asset: GenieAsset; collection
header="Contract address" header="Contract address"
body={ body={
<Center onClick={copy}> <Center onClick={copy}>
{shortenAddress(address, 2, 4)} <CopyIcon size={13} /> {shortenAddress(address, 2)} <CopyIcon size={13} />
</Center> </Center>
} }
/> />
@ -97,7 +97,7 @@ const DetailsContainer = ({ asset, collection }: { asset: GenieAsset; collection
rel="noopener noreferrer" rel="noopener noreferrer"
target="_blank" target="_blank"
> >
{shortenAddress(creator.address, 2, 4)} {shortenAddress(creator.address, 2)}
</CreatorLink> </CreatorLink>
) )
} }

@ -1,15 +0,0 @@
import { isAddress } from '@ethersproject/address'
/**
* Shortens an Ethereum address by N characters
* @param address blockchain address
* @param charsStart amount of character to shorten (from both ends / in the beginning)
* @param charsEnd amount of characters to shorten in the end
* @returns formatted string
*/
export function shortenAddress(address: string, charsStart = 4, charsEnd?: number): string {
const parsed = isAddress(address)
if (!parsed) return ''
return `${address.substring(0, charsStart + 2)}...${address.substring(42 - (charsEnd || charsStart))}`
}

@ -1,4 +1,4 @@
import { isAddress, shortenAddress } from '.' import { isAddress, shortenAddress } from './addresses'
describe('utils', () => { describe('utils', () => {
describe('#isAddress', () => { describe('#isAddress', () => {
@ -18,14 +18,15 @@ describe('utils', () => {
it('succeeds even without prefix', () => { it('succeeds even without prefix', () => {
expect(isAddress('f164fc0ec4e93095b804a4795bbe1e041497b92a')).toBe('0xf164fC0Ec4E93095b804a4795bBe1e041497b92a') expect(isAddress('f164fc0ec4e93095b804a4795bbe1e041497b92a')).toBe('0xf164fC0Ec4E93095b804a4795bBe1e041497b92a')
}) })
it('fails if too long', () => { it('fails if too long', () => {
expect(isAddress('f164fc0ec4e93095b804a4795bbe1e041497b92a0')).toBe(false) expect(isAddress('f164fc0ec4e93095b804a4795bbe1e041497b92a0')).toBe(false)
}) })
}) })
describe('#shortenAddress', () => { describe('#shortenAddress', () => {
it('throws on invalid address', () => { it('doesnt throw on invalid address', () => {
expect(() => shortenAddress('abc')).toThrow("Invalid 'address'") expect(shortenAddress('abc123')).toEqual('')
}) })
it('truncates middle characters', () => { it('truncates middle characters', () => {
@ -39,5 +40,16 @@ describe('utils', () => {
it('renders checksummed address', () => { it('renders checksummed address', () => {
expect(shortenAddress('0x2E1b342132A67Ea578e4E3B814bae2107dc254CC'.toLowerCase())).toBe('0x2E1b...54CC') expect(shortenAddress('0x2E1b342132A67Ea578e4E3B814bae2107dc254CC'.toLowerCase())).toBe('0x2E1b...54CC')
}) })
it('allows undefined', () => {
expect(shortenAddress()).toBe('')
})
it('allows custom amounts of start/end chars', () => {
expect(shortenAddress('0x2E1b342132A67Ea578e4E3B814bae2107dc254CC', 2)).toBe('0x2E...54CC')
expect(shortenAddress('0x2E1b342132A67Ea578e4E3B814bae2107dc254CC', 6)).toBe('0x2E1b34...54CC')
expect(shortenAddress('0x2E1b342132A67Ea578e4E3B814bae2107dc254CC', 2, 2)).toBe('0x2E...CC')
expect(shortenAddress('0x2E1b342132A67Ea578e4E3B814bae2107dc254CC', 2, 6)).toBe('0x2E...c254CC')
})
}) })
}) })

43
src/utils/addresses.ts Normal file

@ -0,0 +1,43 @@
import { getAddress } from '@ethersproject/address'
// returns the checksummed address if the address is valid, otherwise returns false
export function isAddress(value: any): string | false {
try {
// Alphabetical letters must be made lowercase for getAddress to work.
// See documentation here: https://docs.ethers.io/v5/api/utils/address/
return getAddress(value.toLowerCase())
} catch {
return false
}
}
// Shortens an Ethereum address
export function shortenAddress(address = '', charsStart = 4, charsEnd = 4): string {
const parsed = isAddress(address)
if (!parsed) return ''
return ellipseAddressAdd0x(parsed, charsStart, charsEnd)
}
/**
* Shorten an address and add 0x to the start if missing
* @param targetAddress
* @param charsStart amount of character to shorten (from both ends / in the beginning)
* @param charsEnd amount of characters to shorten in the end
* @returns formatted string
*/
function ellipseAddressAdd0x(targetAddress: string, charsStart = 4, charsEnd = 4): string {
const hasPrefix = targetAddress.startsWith('0x')
const prefix = hasPrefix ? '' : '0x'
return ellipseMiddle(prefix + targetAddress, charsStart + 2, charsEnd)
}
/**
* Shorten a string with "..." in the middle
* @param target
* @param charsStart amount of character to shorten (from both ends / in the beginning)
* @param charsEnd amount of characters to shorten in the end
* @returns formatted string
*/
function ellipseMiddle(target: string, charsStart = 4, charsEnd = 4): string {
return `${target.slice(0, charsStart)}...${target.slice(target.length - charsEnd)}`
}

@ -0,0 +1,3 @@
export function escapeRegExp(string: string): string {
return string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&') // $& means the whole matched string
}

23
src/utils/getContract.ts Normal file

@ -0,0 +1,23 @@
import { AddressZero } from '@ethersproject/constants'
import { Contract } from '@ethersproject/contracts'
import { JsonRpcProvider, JsonRpcSigner } from '@ethersproject/providers'
import { isAddress } from './addresses'
// account is optional
export function getContract(address: string, ABI: any, provider: JsonRpcProvider, account?: string): Contract {
if (!isAddress(address) || address === AddressZero) {
throw Error(`Invalid 'address' parameter '${address}'.`)
}
return new Contract(address, ABI, getProviderOrSigner(provider, account) as any)
}
// account is not optional
function getSigner(provider: JsonRpcProvider, account: string): JsonRpcSigner {
return provider.getSigner(account).connectUnchecked()
}
// account is optional
function getProviderOrSigner(provider: JsonRpcProvider, account?: string): JsonRpcProvider | JsonRpcSigner {
return account ? getSigner(provider, account) : provider
}

@ -1,47 +1,3 @@
import { getAddress } from '@ethersproject/address' export * from './addresses'
import { AddressZero } from '@ethersproject/constants' export * from './escapeRegExp'
import { Contract } from '@ethersproject/contracts' export * from './getContract'
import type { JsonRpcProvider, JsonRpcSigner } from '@ethersproject/providers'
// returns the checksummed address if the address is valid, otherwise returns false
export function isAddress(value: any): string | false {
try {
// Alphabetical letters must be made lowercase for getAddress to work.
// See documentation here: https://docs.ethers.io/v5/api/utils/address/
return getAddress(value.toLowerCase())
} catch {
return false
}
}
// shorten the checksummed version of the input address to have 0x + 4 characters at start and end
export function shortenAddress(address: string, chars = 4): string {
const parsed = isAddress(address)
if (!parsed) {
throw Error(`Invalid 'address' parameter '${address}'.`)
}
return `${parsed.substring(0, chars + 2)}...${parsed.substring(42 - chars)}`
}
// account is not optional
function getSigner(provider: JsonRpcProvider, account: string): JsonRpcSigner {
return provider.getSigner(account).connectUnchecked()
}
// account is optional
function getProviderOrSigner(provider: JsonRpcProvider, account?: string): JsonRpcProvider | JsonRpcSigner {
return account ? getSigner(provider, account) : provider
}
// account is optional
export function getContract(address: string, ABI: any, provider: JsonRpcProvider, account?: string): Contract {
if (!isAddress(address) || address === AddressZero) {
throw Error(`Invalid 'address' parameter '${address}'.`)
}
return new Contract(address, ABI, getProviderOrSigner(provider, account) as any)
}
export function escapeRegExp(string: string): string {
return string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&') // $& means the whole matched string
}