diff --git a/.changelog/1246.feature.md b/.changelog/1246.feature.md
new file mode 100644
index 000000000..fba6defd8
--- /dev/null
+++ b/.changelog/1246.feature.md
@@ -0,0 +1 @@
+Initial support for displaying account names
diff --git a/package.json b/package.json
index 70136bc92..495bcd393 100644
--- a/package.json
+++ b/package.json
@@ -74,6 +74,7 @@
"axios": "1.6.8",
"bignumber.js": "9.1.2",
"bip39": "^3.1.0",
+ "chance": "^1.1.11",
"date-fns": "3.6.0",
"i18next": "23.11.2",
"react": "18.2.0",
@@ -101,6 +102,7 @@
"@testing-library/jest-dom": "6.4.2",
"@testing-library/react": "14.2.2",
"@testing-library/user-event": "14.5.2",
+ "@types/chance": "^1.1.6",
"@types/jest": "^29.5.12",
"@types/node": "20.12.7",
"@types/node-fetch": "2.6.11",
diff --git a/src/app/components/Account/AccountLink.tsx b/src/app/components/Account/AccountLink.tsx
index ddffd1e84..a81e3b65a 100644
--- a/src/app/components/Account/AccountLink.tsx
+++ b/src/app/components/Account/AccountLink.tsx
@@ -1,30 +1,141 @@
-import { FC } from 'react'
+import { FC, ReactNode } from 'react'
import { Link as RouterLink } from 'react-router-dom'
import { useScreenSize } from '../../hooks/useScreensize'
import Link from '@mui/material/Link'
-import { TrimLinkLabel } from '../TrimLinkLabel'
import { RouteUtils } from '../../utils/route-utils'
+import InfoIcon from '@mui/icons-material/Info'
import Typography from '@mui/material/Typography'
import { SearchScope } from '../../../types/searchScope'
+import { useAccountName } from '../../hooks/useAccountName'
+import { trimLongString } from '../../utils/trimLongString'
+import { MaybeWithTooltip } from '../AdaptiveTrimmer/MaybeWithTooltip'
+import Box from '@mui/material/Box'
+import { HighlightedText } from '../HighlightedText'
+import { AdaptiveHighlightedText } from '../HighlightedText/AdaptiveHighlightedText'
+import { AdaptiveTrimmer } from '../AdaptiveTrimmer/AdaptiveTrimmer'
+
+const WithTypographyAndLink: FC<{
+ to: string
+ mobile?: boolean
+ children: ReactNode
+}> = ({ children, to, mobile }) => {
+ return (
+
+
+ {children}
+
+
+ )
+}
interface Props {
scope: SearchScope
address: string
+
+ /**
+ * Should we always trim the text to a short line?
+ */
alwaysTrim?: boolean
+
+ /**
+ * What part of the name should be highlighted (if any)
+ */
+ highlightedPartOfName?: string | undefined
+
+ /**
+ * Any extra tooltips to display
+ *
+ * (Besides the content necessary because of potential shortening)
+ */
+ extraTooltip?: ReactNode
}
-export const AccountLink: FC = ({ scope, address, alwaysTrim }) => {
+export const AccountLink: FC = ({
+ scope,
+ address,
+ alwaysTrim,
+ highlightedPartOfName,
+ extraTooltip,
+}) => {
const { isTablet } = useScreenSize()
+ const { name: accountName } = useAccountName(scope, address)
const to = RouteUtils.getAccountRoute(scope, address)
+
+ const tooltipPostfix = extraTooltip ? (
+ <>
+
+ {extraTooltip}
+ >
+ ) : undefined
+
+ // Are we in a table?
+ if (alwaysTrim) {
+ // In a table, we only ever want one short line
+
+ return (
+
+
+ {accountName}
+ {address}
+ {tooltipPostfix}
+
+ ) : (
+ address
+ )
+ }
+ >
+ {accountName ? trimLongString(accountName, 12, 0) : trimLongString(address, 6, 6)}
+
+
+ )
+ }
+
+ if (!isTablet) {
+ // Details in desktop mode.
+ // We want one long line, with name and address.
+
+ return (
+
+
+ {accountName ? (
+
+ ({address})
+
+ ) : (
+ address
+ )}
+
+
+ )
+ }
+
+ // We need to show the data in details mode on mobile.
+ // We want two lines, one for name (if available), one for address
+ // Both line adaptively shortened to fill available space
return (
-
- {alwaysTrim || isTablet ? (
-
- ) : (
-
- {address}
-
- )}
-
+
+ <>
+
+
+ >
+
)
}
diff --git a/src/app/components/Account/ContractCreatorInfo.tsx b/src/app/components/Account/ContractCreatorInfo.tsx
index a944c8362..fed858109 100644
--- a/src/app/components/Account/ContractCreatorInfo.tsx
+++ b/src/app/components/Account/ContractCreatorInfo.tsx
@@ -14,7 +14,11 @@ import Box from '@mui/material/Box'
import Skeleton from '@mui/material/Skeleton'
import { useScreenSize } from '../../hooks/useScreensize'
-const TxSender: FC<{ scope: SearchScope; txHash: string }> = ({ scope, txHash }) => {
+const TxSender: FC<{ scope: SearchScope; txHash: string; alwaysTrim?: boolean }> = ({
+ scope,
+ txHash,
+ alwaysTrim,
+}) => {
const { t } = useTranslation()
if (scope.layer === Layer.consensus) {
throw AppErrors.UnsupportedLayer
@@ -31,7 +35,7 @@ const TxSender: FC<{ scope: SearchScope; txHash: string }> = ({ scope, txHash })
}}
/>
) : senderAddress ? (
-
+
) : (
t('common.missing')
)
@@ -41,7 +45,8 @@ export const ContractCreatorInfo: FC<{
scope: SearchScope
isLoading?: boolean
creationTxHash: string | undefined
-}> = ({ scope, isLoading, creationTxHash }) => {
+ alwaysTrim?: boolean
+}> = ({ scope, isLoading, creationTxHash, alwaysTrim }) => {
const { t } = useTranslation()
const { isMobile } = useScreenSize()
@@ -59,9 +64,9 @@ export const ContractCreatorInfo: FC<{
minWidth: '25%',
}}
>
-
+
{t('contract.createdAt')}
-
+
)
}
@@ -69,7 +74,8 @@ export const ContractCreatorInfo: FC<{
export const DelayedContractCreatorInfo: FC<{
scope: SearchScope
contractOasisAddress: string | undefined
-}> = ({ scope, contractOasisAddress }) => {
+ alwaysTrim?: boolean
+}> = ({ scope, contractOasisAddress, alwaysTrim }) => {
const accountQuery = useGetRuntimeAccountsAddress(
scope.network,
scope.layer as Runtime,
@@ -82,6 +88,11 @@ export const DelayedContractCreatorInfo: FC<{
const creationTxHash = contract?.eth_creation_tx || contract?.creation_tx
return (
-
+
)
}
diff --git a/src/app/components/Account/index.tsx b/src/app/components/Account/index.tsx
index 538ac8e0c..bc275873e 100644
--- a/src/app/components/Account/index.tsx
+++ b/src/app/components/Account/index.tsx
@@ -30,9 +30,17 @@ type AccountProps = {
isLoading: boolean
tokenPrices: AllTokenPrices
showLayer?: boolean
+ highlightedPartOfName: string | undefined
}
-export const Account: FC = ({ account, token, isLoading, tokenPrices, showLayer }) => {
+export const Account: FC = ({
+ account,
+ token,
+ isLoading,
+ tokenPrices,
+ showLayer,
+ highlightedPartOfName,
+}) => {
const { t } = useTranslation()
const { isMobile } = useScreenSize()
const address = account ? account.address_eth ?? account.address : undefined
@@ -67,7 +75,7 @@ export const Account: FC = ({ account, token, isLoading, tokenPric
-
+
@@ -100,6 +108,7 @@ export const Account: FC = ({ account, token, isLoading, tokenPric
>
diff --git a/src/app/components/AccountList/index.tsx b/src/app/components/AccountList/index.tsx
index 2a6450348..1635190d9 100644
--- a/src/app/components/AccountList/index.tsx
+++ b/src/app/components/AccountList/index.tsx
@@ -39,7 +39,7 @@ export const AccountList: FC = ({ isLoading, limit, pagination
key: 'size',
},
{
- content: ,
+ content: ,
key: 'address',
},
...(verbose
diff --git a/src/app/components/AdaptiveTrimmer/AdaptiveDynamicTrimmer.tsx b/src/app/components/AdaptiveTrimmer/AdaptiveDynamicTrimmer.tsx
new file mode 100644
index 000000000..d020a7fc2
--- /dev/null
+++ b/src/app/components/AdaptiveTrimmer/AdaptiveDynamicTrimmer.tsx
@@ -0,0 +1,151 @@
+import { FC, ReactNode, useCallback, useEffect, useRef, useState } from 'react'
+import Box from '@mui/material/Box'
+import InfoIcon from '@mui/icons-material/Info'
+import { MaybeWithTooltip } from './MaybeWithTooltip'
+
+type AdaptiveDynamicTrimmerProps = {
+ getFullContent: () => {
+ content: ReactNode
+ length: number
+ }
+ getShortenedContent: (wantedLength: number) => ReactNode
+ extraTooltip: ReactNode
+}
+
+/**
+ * Display content, potentially shortened as needed.
+ *
+ * This component will do automatic detection of available space,
+ * and determine the best way to display content accordingly.
+ *
+ * The difference compared to AdaptiveTrimmer is that this component
+ * expects a function to provide a shortened version of the components.
+ */
+export const AdaptiveDynamicTrimmer: FC = ({
+ getFullContent,
+ getShortenedContent,
+ extraTooltip,
+}) => {
+ // Initial setup
+ const textRef = useRef(null)
+ const { content: fullContent, length: fullLength } = getFullContent()
+
+ // Data about the currently rendered version
+ const [currentContent, setCurrentContent] = useState()
+ const [currentLength, setCurrentLength] = useState(0)
+
+ // Known good - this fits
+ const [largestKnownGood, setLargestKnownGood] = useState(0)
+
+ // Known bad - this doesn't fit
+ const [smallestKnownBad, setSmallestKnownBad] = useState(fullLength + 1)
+
+ // Are we exploring our possibilities now?
+ const [inDiscovery, setInDiscovery] = useState(false)
+
+ const attemptContent = useCallback((content: ReactNode, length: number) => {
+ setCurrentContent(content)
+ setCurrentLength(length)
+ }, [])
+
+ const attemptShortenedContent = useCallback(
+ (wantedLength: number) => attemptContent(getShortenedContent(wantedLength), wantedLength),
+ [attemptContent, getShortenedContent],
+ )
+
+ const initDiscovery = useCallback(() => {
+ setLargestKnownGood(0)
+ setSmallestKnownBad(fullLength + 1)
+ attemptContent(fullContent, fullLength)
+ setInDiscovery(true)
+ }, [fullContent, fullLength, attemptContent])
+
+ useEffect(() => {
+ initDiscovery()
+ const handleResize = () => {
+ initDiscovery()
+ }
+
+ window.addEventListener('resize', handleResize)
+ return () => window.removeEventListener('resize', handleResize)
+ }, [initDiscovery])
+
+ useEffect(() => {
+ if (inDiscovery) {
+ if (!textRef.current) {
+ return
+ }
+ const isOverflow = textRef.current.scrollWidth > textRef.current.clientWidth
+ // log('Overflow?', isOverflow)
+
+ if (isOverflow) {
+ // This is too much
+
+ // Update known bad length
+ const newSmallestKnownBad = Math.min(currentLength, smallestKnownBad)
+ setSmallestKnownBad(newSmallestKnownBad)
+
+ // We should try something smaller
+ attemptShortenedContent(Math.floor((largestKnownGood + newSmallestKnownBad) / 2))
+ } else {
+ // This is OK
+
+ // Update known good length
+ const newLargestKnownGood = Math.max(currentLength, largestKnownGood)
+ setLargestKnownGood(currentLength)
+
+ if (currentLength === fullLength) {
+ // The whole thing fits, so we are good.
+ setInDiscovery(false)
+ } else {
+ if (currentLength + 1 === smallestKnownBad) {
+ // This the best we can do, for now
+ setInDiscovery(false)
+ } else {
+ // So far, so good, but we should try something longer
+ attemptShortenedContent(Math.floor((newLargestKnownGood + smallestKnownBad) / 2))
+ }
+ }
+ }
+ }
+ }, [
+ attemptShortenedContent,
+ currentLength,
+ fullContent,
+ fullLength,
+ inDiscovery,
+ initDiscovery,
+ largestKnownGood,
+ smallestKnownBad,
+ ])
+
+ const title =
+ currentLength !== fullLength ? (
+
+ {fullContent}
+ {extraTooltip && (
+
+
+ {extraTooltip}
+
+ )}
+
+ ) : (
+ extraTooltip
+ )
+
+ return (
+
+
+ {currentContent}
+
+
+ )
+}
diff --git a/src/app/components/AdaptiveTrimmer/AdaptiveTrimmer.tsx b/src/app/components/AdaptiveTrimmer/AdaptiveTrimmer.tsx
new file mode 100644
index 000000000..878252654
--- /dev/null
+++ b/src/app/components/AdaptiveTrimmer/AdaptiveTrimmer.tsx
@@ -0,0 +1,140 @@
+import { FC, ReactNode, useCallback, useEffect, useRef, useState } from 'react'
+import Box from '@mui/material/Box'
+import InfoIcon from '@mui/icons-material/Info'
+import { trimLongString } from '../../utils/trimLongString'
+import { MaybeWithTooltip } from './MaybeWithTooltip'
+
+type AdaptiveTrimmerProps = {
+ text: string | undefined
+ strategy: 'middle' | 'end'
+ extraTooltip?: ReactNode
+}
+
+/**
+ * Display content, potentially shortened as needed.
+ *
+ * This component will do automatic detection of available space,
+ * and determine the best way to display content accordingly.
+ */
+export const AdaptiveTrimmer: FC = ({ text = '', strategy = 'end', extraTooltip }) => {
+ // Initial setup
+ const fullLength = text.length
+ const textRef = useRef(null)
+
+ // Data about the currently rendered version
+ const [currentContent, setCurrentContent] = useState('')
+ const [currentLength, setCurrentLength] = useState(0)
+
+ // Known good - this fits
+ const [largestKnownGood, setLargestKnownGood] = useState(0)
+
+ // Known bad - this doesn't fit
+ const [smallestKnownBad, setSmallestKnownBad] = useState(fullLength + 1)
+
+ // Are we exploring our possibilities now?
+ const [inDiscovery, setInDiscovery] = useState(false)
+
+ const attemptContent = useCallback((content: string, length: number) => {
+ setCurrentContent(content)
+ setCurrentLength(length)
+ }, [])
+
+ const attemptShortenedContent = useCallback(
+ (length: number) => {
+ const content =
+ strategy === 'middle'
+ ? trimLongString(text, Math.floor(length / 2) - 1, Math.floor(length / 2) - 1)!
+ : trimLongString(text, length, 0)!
+
+ attemptContent(content, length)
+ },
+ [strategy, text, attemptContent],
+ )
+
+ const initDiscovery = useCallback(() => {
+ setLargestKnownGood(0)
+ setSmallestKnownBad(fullLength + 1)
+ attemptContent(text, fullLength)
+ setInDiscovery(true)
+ }, [text, fullLength, attemptContent])
+
+ useEffect(() => {
+ initDiscovery()
+ const handleResize = () => {
+ initDiscovery()
+ }
+
+ window.addEventListener('resize', handleResize)
+ return () => window.removeEventListener('resize', handleResize)
+ }, [initDiscovery])
+
+ useEffect(() => {
+ if (inDiscovery) {
+ if (!textRef.current) {
+ return
+ }
+ const isOverflow = textRef.current.scrollWidth > textRef.current.clientWidth
+
+ if (isOverflow) {
+ // This is too much
+
+ // Update known bad length
+ const newSmallestKnownBad = Math.min(currentLength, smallestKnownBad)
+ setSmallestKnownBad(newSmallestKnownBad)
+
+ // We should try something smaller
+ attemptShortenedContent(Math.floor((largestKnownGood + newSmallestKnownBad) / 2))
+ } else {
+ // This is OK
+
+ // Update known good length
+ const newLargestKnownGood = Math.max(currentLength, largestKnownGood)
+ setLargestKnownGood(currentLength)
+
+ if (currentLength === fullLength) {
+ // The whole thing fits, so we are good.
+ setInDiscovery(false)
+ } else {
+ if (currentLength + 1 === smallestKnownBad) {
+ // This the best we can do, for now
+ setInDiscovery(false)
+ } else {
+ // So far, so good, but we should try something longer
+ attemptShortenedContent(Math.floor((newLargestKnownGood + smallestKnownBad) / 2))
+ }
+ }
+ }
+ }
+ }, [inDiscovery, currentLength, largestKnownGood, smallestKnownBad, attemptShortenedContent, fullLength])
+
+ if (!text) return null
+
+ const title =
+ currentLength !== fullLength ? (
+
+ {text}
+ {extraTooltip && (
+
+
+ {extraTooltip}
+
+ )}
+
+ ) : (
+ extraTooltip
+ )
+
+ return (
+
+
+ {currentContent}
+
+
+ )
+}
diff --git a/src/app/components/AdaptiveTrimmer/MaybeWithTooltip.tsx b/src/app/components/AdaptiveTrimmer/MaybeWithTooltip.tsx
new file mode 100644
index 000000000..4edfe0c26
--- /dev/null
+++ b/src/app/components/AdaptiveTrimmer/MaybeWithTooltip.tsx
@@ -0,0 +1,65 @@
+import { FC, ReactNode } from 'react'
+import Tooltip from '@mui/material/Tooltip'
+import Box from '@mui/material/Box'
+import { SxProps } from '@mui/material/styles'
+
+type MaybeWithTooltipProps = {
+ /**
+ * Do we want to show the tooltip?
+ *
+ * Default is true
+ */
+ tooltipWanted?: boolean
+
+ /**
+ * What should be the content of the tooltip?
+ *
+ * Undefined means no tooltip
+ */
+ title?: ReactNode
+
+ /**
+ * Any extra styles to apply to the span
+ */
+ spanSx?: SxProps
+
+ /**
+ * The content to show
+ */
+ children: ReactNode
+}
+
+/**
+ * A component to display some content with or without a tooltip
+ */
+export const MaybeWithTooltip: FC = ({
+ tooltipWanted = true,
+ title,
+ children,
+ spanSx,
+}) =>
+ tooltipWanted && !!title ? (
+
+ {title}
+
+ }
+ >
+
+ {children}
+
+
+ ) : (
+
+ {children}
+
+ )
diff --git a/src/app/components/Blocks/BlockLink.tsx b/src/app/components/Blocks/BlockLink.tsx
index f9673b7c8..53a55cf7c 100644
--- a/src/app/components/Blocks/BlockLink.tsx
+++ b/src/app/components/Blocks/BlockLink.tsx
@@ -7,6 +7,7 @@ import { RouteUtils } from '../../utils/route-utils'
import { TrimLinkLabel } from '../TrimLinkLabel'
import { SearchScope } from '../../../types/searchScope'
import { useScreenSize } from '../../hooks/useScreensize'
+import { AdaptiveTrimmer } from '../AdaptiveTrimmer/AdaptiveTrimmer'
export const BlockLink: FC<{ scope: SearchScope; height: number }> = ({ scope, height }) => (
@@ -23,15 +24,34 @@ export const BlockHashLink: FC<{
alwaysTrim?: boolean
}> = ({ scope, hash, height, alwaysTrim }) => {
const { isTablet } = useScreenSize()
- return (
-
- {isTablet || alwaysTrim ? (
-
- ) : (
-
+ const to = RouteUtils.getBlockRoute(scope, height)
+
+ if (alwaysTrim) {
+ // Table view
+ return (
+
+
+
+ )
+ }
+
+ if (!isTablet) {
+ // Desktop view
+ return (
+
+
{hash}
- )}
+
+ )
+ }
+
+ // Mobile view
+ return (
+
+
+
+
)
}
diff --git a/src/app/components/HighlightedText/AdaptiveHighlightedText.tsx b/src/app/components/HighlightedText/AdaptiveHighlightedText.tsx
new file mode 100644
index 000000000..0676bfc26
--- /dev/null
+++ b/src/app/components/HighlightedText/AdaptiveHighlightedText.tsx
@@ -0,0 +1,66 @@
+import { FC, ReactNode } from 'react'
+import InfoIcon from '@mui/icons-material/Info'
+import { HighlightedText, HighlightOptions } from './index'
+import { AdaptiveDynamicTrimmer } from '../AdaptiveTrimmer/AdaptiveDynamicTrimmer'
+import { HighlightedTrimmedText } from './HighlightedTrimmedText'
+
+type AdaptiveHighlightedTextProps = {
+ /**
+ * The text to display
+ */
+ text: string | undefined
+
+ /**
+ * The pattern to search for (and highlight)
+ */
+ pattern: string | undefined
+
+ /**
+ * Options for highlighting (case sensitivity, styling, etc.)
+ *
+ * (This is optional, sensible defaults are provided.)
+ */
+ options?: HighlightOptions
+
+ /**
+ * Extra content to put into the tooltip
+ */
+ extraTooltip?: ReactNode
+}
+
+/**
+ * Display a text with a part highlighted, potentially trimmed to an adaptive length around the highlight
+ */
+export const AdaptiveHighlightedText: FC = ({
+ text,
+ pattern,
+ options,
+ extraTooltip,
+}) => {
+ const fullContent =
+
+ return text ? (
+ ({
+ content: fullContent,
+ length: text.length,
+ })}
+ getShortenedContent={wantedLength => (
+
+ )}
+ extraTooltip={
+ extraTooltip ? (
+ <>
+
+ {extraTooltip}
+ >
+ ) : undefined
+ }
+ />
+ ) : undefined
+}
diff --git a/src/app/components/HighlightedText/HighlightedTrimmedText.tsx b/src/app/components/HighlightedText/HighlightedTrimmedText.tsx
new file mode 100644
index 000000000..ff7c816b9
--- /dev/null
+++ b/src/app/components/HighlightedText/HighlightedTrimmedText.tsx
@@ -0,0 +1,43 @@
+import { FC } from 'react'
+
+import { HighlightedText, HighlightOptions } from './index'
+import { cutAroundMatch } from './text-cutting'
+
+type HighlightedTrimmedTextProps = {
+ /**
+ * The text to display
+ */
+ text: string | undefined
+
+ /**
+ * The pattern to search for (and highlight)
+ */
+ pattern: string | undefined
+
+ /**
+ * Options for highlighting (case sensitivity, styling, etc.)
+ *
+ * (This is optional, sensible defaults are provided.)
+ */
+ options?: HighlightOptions
+
+ /**
+ * What should be the length of the fragment delivered, which
+ * has the pattern inside it?
+ */
+ fragmentLength: number
+}
+
+/**
+ * Display a text with a part highlighted, potentially trimmed to shorter length around the highlight
+ */
+export const HighlightedTrimmedText: FC = props => {
+ const { text, pattern, fragmentLength, options } = props
+ return (
+
+ )
+}
diff --git a/src/app/components/HighlightedText/text-cutting.ts b/src/app/components/HighlightedText/text-cutting.ts
new file mode 100644
index 000000000..cb7555fa6
--- /dev/null
+++ b/src/app/components/HighlightedText/text-cutting.ts
@@ -0,0 +1,74 @@
+import { findTextMatch, NormalizerOptions } from './text-matching'
+
+export interface CutAroundOptions extends NormalizerOptions {
+ /**
+ * What should be the length of the fragment delivered, which
+ * has the pattern inside it?
+ *
+ * The default value is 80.
+ */
+ fragmentLength?: number
+}
+
+/**
+ * Return a part of the corpus that contains the match to the pattern, if any
+ *
+ * If the corpus is undefined or empty, undefined is returned.
+ *
+ * If either the pattern is undefined or empty, or there is no match,
+ * an adequately sized part from the beginning of the corpus is returned.
+ *
+ * If there is a match, but the corpus is at most as long as the desired fragment length,
+ * the whole corpus is returned.
+ *
+ * If there is a match, and the corpus is longer than the desired fragment length,
+ * then a part of a corpus is returned, so that the match is within the returned part,
+ * around the middle.
+ */
+export function cutAroundMatch(
+ corpus: string | undefined,
+ pattern: string | undefined,
+ options: CutAroundOptions = {},
+): {
+ hasMatch: boolean
+ part: string | undefined
+} {
+ const { fragmentLength = 80, ...matchOptions } = options
+
+ if (!corpus) {
+ // there is nothing to see here
+ return {
+ hasMatch: false,
+ part: undefined,
+ }
+ }
+
+ // do we have a match?
+ const match = pattern ? findTextMatch(corpus, [pattern], matchOptions) : undefined
+
+ if (corpus.length <= fragmentLength) {
+ // the whole corpus fits into the max size, no need to cut.
+ return {
+ hasMatch: !!match,
+ part: corpus,
+ }
+ }
+
+ // how much extra space do we have?
+ const buffer = fragmentLength - (pattern || '').length
+
+ const matchStart = match?.startPos ?? 0
+
+ // We will start before the start of the match, by buffer / 2 chars
+ const startPos = Math.max(Math.min(matchStart - Math.floor(buffer / 2), corpus.length - fragmentLength), 0)
+ const endPos = Math.min(startPos + fragmentLength, corpus.length)
+
+ // compile the result
+ const part =
+ (startPos ? '…' : '') + corpus.substring(startPos, endPos) + (endPos < corpus.length - 1 ? '…' : '')
+
+ return {
+ hasMatch: true,
+ part,
+ }
+}
diff --git a/src/app/components/Search/search-utils.ts b/src/app/components/Search/search-utils.ts
index bebe55a4a..6cc4fd520 100644
--- a/src/app/components/Search/search-utils.ts
+++ b/src/app/components/Search/search-utils.ts
@@ -119,6 +119,11 @@ export const validateAndNormalize = {
return searchTerm.toLowerCase()
}
},
+ accountNameFragment: (searchTerm: string) => {
+ if (searchTerm?.length >= textSearchMininumLength) {
+ return searchTerm.toLowerCase()
+ }
+ },
} satisfies { [name: string]: (searchTerm: string) => string | undefined }
export function isSearchValid(searchTerm: string) {
diff --git a/src/app/components/StyledDescriptionList/index.tsx b/src/app/components/StyledDescriptionList/index.tsx
index 2ba5058ac..08865f369 100644
--- a/src/app/components/StyledDescriptionList/index.tsx
+++ b/src/app/components/StyledDescriptionList/index.tsx
@@ -61,6 +61,8 @@ export const StyledDescriptionList = styled(InlineDescriptionList, {
color: COLORS.brandExtraDark,
overflowWrap: 'anywhere',
alignItems: 'center',
+ maxWidth: '100%',
+ overflowX: 'hidden',
},
...(standalone && {
'&&': {
diff --git a/src/app/components/Tokens/TokenHolders.tsx b/src/app/components/Tokens/TokenHolders.tsx
index 77daeae62..8bc52e63b 100644
--- a/src/app/components/Tokens/TokenHolders.tsx
+++ b/src/app/components/Tokens/TokenHolders.tsx
@@ -53,7 +53,11 @@ export const TokenHolders: FC = ({
{
key: 'address',
content: (
-
+
),
},
{
diff --git a/src/app/components/Tokens/TokenList.tsx b/src/app/components/Tokens/TokenList.tsx
index 676f8aa60..ec2e0bea4 100644
--- a/src/app/components/Tokens/TokenList.tsx
+++ b/src/app/components/Tokens/TokenList.tsx
@@ -107,7 +107,11 @@ export const TokenList = (props: TokensProps) => {
{
content: (
-
+
),
diff --git a/src/app/components/Tokens/TokenTransfers.tsx b/src/app/components/Tokens/TokenTransfers.tsx
index 139ccb814..a35c0ec17 100644
--- a/src/app/components/Tokens/TokenTransfers.tsx
+++ b/src/app/components/Tokens/TokenTransfers.tsx
@@ -116,7 +116,7 @@ export const TokenTransfers: FC = ({
ownAddress,
}) => {
const { t } = useTranslation()
- const { isMobile } = useScreenSize()
+ const { isTablet } = useScreenSize()
const tableColumns: TableColProps[] = [
{ key: 'hash', content: t('common.hash') },
{ key: 'block', content: t('common.block') },
@@ -139,7 +139,7 @@ export const TokenTransfers: FC = ({
content: (
),
@@ -186,7 +186,7 @@ export const TokenTransfers: FC = ({
{trimLongString(fromAddress)}
) : (
-
+
)}
@@ -210,7 +210,7 @@ export const TokenTransfers: FC = ({
{trimLongString(toAddress)}
) : (
-
+
),
},
...(differentTokens
diff --git a/src/app/components/Transactions/ConsensusTransactions.tsx b/src/app/components/Transactions/ConsensusTransactions.tsx
index 273752c40..4d5329878 100644
--- a/src/app/components/Transactions/ConsensusTransactions.tsx
+++ b/src/app/components/Transactions/ConsensusTransactions.tsx
@@ -1,11 +1,9 @@
import { FC } from 'react'
import { useTranslation } from 'react-i18next'
import Box from '@mui/material/Box'
-import Typography from '@mui/material/Typography'
import { Transaction } from '../../../oasis-nexus/api'
import { Table, TableCellAlign, TableColProps } from '../../components/Table'
import { RoundedBalance } from '../../components/RoundedBalance'
-import { trimLongString } from '../../utils/trimLongString'
import { TablePaginationProps } from '../Table/TablePagination'
import { BlockLink } from '../Blocks/BlockLink'
import { AccountLink } from '../Account/AccountLink'
@@ -38,7 +36,6 @@ export const ConsensusTransactions: FC = ({
limit,
pagination,
transactions,
- ownAddress,
verbose = true,
}) => {
const { t } = useTranslation()
@@ -66,7 +63,7 @@ export const ConsensusTransactions: FC = ({
key: 'success',
},
{
- content: ,
+ content: ,
key: 'hash',
},
{
@@ -94,19 +91,7 @@ export const ConsensusTransactions: FC = ({
pr: 3,
}}
>
- {!!ownAddress && transaction.sender === ownAddress ? (
-
- {trimLongString(transaction.sender)}
-
- ) : (
-
- )}
+
),
key: 'from',
diff --git a/src/app/components/Transactions/RuntimeTransactions.tsx b/src/app/components/Transactions/RuntimeTransactions.tsx
index 1c2208ec9..0268e1f2e 100644
--- a/src/app/components/Transactions/RuntimeTransactions.tsx
+++ b/src/app/components/Transactions/RuntimeTransactions.tsx
@@ -14,8 +14,6 @@ import { TablePaginationProps } from '../Table/TablePagination'
import { BlockLink } from '../Blocks/BlockLink'
import { AccountLink } from '../Account/AccountLink'
import { TransactionLink } from './TransactionLink'
-import { trimLongString } from '../../utils/trimLongString'
-import Typography from '@mui/material/Typography'
import { doesAnyOfTheseLayersSupportEncryptedTransactions } from '../../../types/layers'
import { TransactionEncryptionStatus } from '../TransactionEncryptionStatus'
import { Age } from '../Age'
@@ -58,7 +56,6 @@ export const RuntimeTransactions: FC = ({
limit,
pagination,
transactions,
- ownAddress,
verbose = true,
}) => {
const { t } = useTranslation()
@@ -104,11 +101,7 @@ export const RuntimeTransactions: FC = ({
: []),
{
content: (
-
+
),
key: 'hash',
},
@@ -138,24 +131,11 @@ export const RuntimeTransactions: FC = ({
pr: 3,
}}
>
- {!!ownAddress &&
- (transaction.sender_0_eth === ownAddress || transaction.sender_0 === ownAddress) ? (
-
- {trimLongString(transaction.sender_0_eth || transaction.sender_0)}
-
- ) : (
-
- )}
+
{targetAddress && (
@@ -167,19 +147,7 @@ export const RuntimeTransactions: FC = ({
},
{
content: targetAddress ? (
- !!ownAddress && (transaction.to_eth === ownAddress || transaction.to === ownAddress) ? (
-
- {trimLongString(targetAddress)}
-
- ) : (
-
- )
+
) : null,
key: 'to',
},
diff --git a/src/app/components/Transactions/TransactionLink.tsx b/src/app/components/Transactions/TransactionLink.tsx
index 1afcc359e..ca4590002 100644
--- a/src/app/components/Transactions/TransactionLink.tsx
+++ b/src/app/components/Transactions/TransactionLink.tsx
@@ -1,29 +1,80 @@
-import { FC } from 'react'
+import { FC, ReactNode } from 'react'
import { Link as RouterLink } from 'react-router-dom'
import Link from '@mui/material/Link'
import Typography from '@mui/material/Typography'
+import InfoIcon from '@mui/icons-material/Info'
import { useScreenSize } from '../../hooks/useScreensize'
-import { TrimLinkLabel } from '../TrimLinkLabel'
import { RouteUtils } from '../../utils/route-utils'
import { SearchScope } from '../../../types/searchScope'
+import { AdaptiveTrimmer } from '../AdaptiveTrimmer/AdaptiveTrimmer'
+import { MaybeWithTooltip } from '../AdaptiveTrimmer/MaybeWithTooltip'
+import { trimLongString } from '../../utils/trimLongString'
+import Box from '@mui/material/Box'
+
+const WithTypographyAndLink: FC<{ children: ReactNode; mobile?: boolean; to: string }> = ({
+ children,
+ mobile,
+ to,
+}) => (
+
+
+ {children}
+
+
+)
export const TransactionLink: FC<{
alwaysTrim?: boolean
scope: SearchScope
hash: string
-}> = ({ alwaysTrim, hash, scope }) => {
- const { isMobile } = useScreenSize()
+ extraTooltip?: ReactNode
+}> = ({ alwaysTrim, hash, scope, extraTooltip }) => {
+ const { isTablet } = useScreenSize()
const to = RouteUtils.getTransactionRoute(scope, hash)
+ const tooltipPostfix = extraTooltip ? (
+
+
+ {extraTooltip}
+
+ ) : undefined
+
+ if (alwaysTrim) {
+ // Table mode
+ return (
+
+
+ {hash}
+ {tooltipPostfix}
+
+ }
+ >
+ {trimLongString(hash, 6, 6)}
+
+
+ )
+ }
+
+ if (!isTablet) {
+ // Desktop mode
+ return (
+
+ {hash}
+
+ )
+ }
+ // Mobile mode
return (
-
- {alwaysTrim || isMobile ? (
-
- ) : (
-
- {hash}
-
- )}
-
+
+
+
)
}
diff --git a/src/app/data/pontusx-account-names.ts b/src/app/data/pontusx-account-names.ts
new file mode 100644
index 000000000..b5474f0c9
--- /dev/null
+++ b/src/app/data/pontusx-account-names.ts
@@ -0,0 +1,94 @@
+import axios from 'axios'
+import { useQuery } from '@tanstack/react-query'
+import type { AccountNameInfo } from '../hooks/useAccountName'
+import { Layer } from '../../oasis-nexus/api'
+import { Network } from '../../types/network'
+import { findTextMatch } from '../components/HighlightedText/text-matching'
+import * as process from 'process'
+
+const DATA_SOURCE_URL = 'https://raw.githubusercontent.com/deltaDAO/mvg-portal/main/pontusxAddresses.json'
+
+type AccountMap = Map
+type AccountEntry = {
+ name: string
+ address: string
+}
+type AccountData = {
+ map: AccountMap
+ list: AccountEntry[]
+}
+
+const getPontusXAccountNames = () =>
+ new Promise((resolve, reject) => {
+ axios.get(DATA_SOURCE_URL).then(response => {
+ if (response.status !== 200) reject("Couldn't load names")
+ if (!response.data) reject("Couldn't load names")
+ const map = new Map()
+ const list: AccountEntry[] = []
+ Object.entries(response.data).forEach(([address, name]) => {
+ map.set(address, name)
+ const normalizedEntry: AccountEntry = {
+ name: name as string,
+ address,
+ }
+ list.push(normalizedEntry)
+ })
+ resolve({
+ map,
+ list,
+ })
+ }, reject)
+ })
+
+export const usePontusXAccountNames = (enabled: boolean) => {
+ return useQuery(['pontusXNames'], getPontusXAccountNames, {
+ enabled,
+ staleTime: Infinity,
+ })
+}
+
+export const usePontusXAccountName = (address: string, enabled: boolean): AccountNameInfo => {
+ // When running jest tests, we don't want to load from Pontus-X.
+ if (process.env.NODE_ENV === 'test') {
+ return {
+ name: undefined,
+ loading: false,
+ }
+ }
+ // This is not a condition that can change while the app is running, so it's OK.
+ // eslint-disable-next-line react-hooks/rules-of-hooks
+ const { isLoading, error, data: allNames } = usePontusXAccountNames(enabled)
+ if (error) {
+ console.log('Failed to load Pontus-X account names', error)
+ }
+ return {
+ name: allNames?.map.get(address),
+ loading: isLoading,
+ }
+}
+
+export const useSearchForPontusXAccountsByName = (
+ network: Network,
+ nameFragment: string,
+ enabled: boolean,
+) => {
+ const { isLoading, error, data: allNames } = usePontusXAccountNames(enabled)
+ if (error) {
+ console.log('Failed to load Pontus-X account names', error)
+ }
+
+ const textMatcher =
+ nameFragment && enabled
+ ? (entry: AccountEntry): boolean => {
+ return !!findTextMatch(entry.name, [nameFragment])
+ }
+ : () => false
+ return {
+ results: (allNames?.list || []).filter(textMatcher).map(entry => ({
+ network,
+ layer: Layer.pontusx,
+ address: entry.address,
+ })),
+ isLoading,
+ }
+}
diff --git a/src/app/hooks/useAccountName.ts b/src/app/hooks/useAccountName.ts
new file mode 100644
index 000000000..fe7a95ea7
--- /dev/null
+++ b/src/app/hooks/useAccountName.ts
@@ -0,0 +1,72 @@
+import { SearchScope } from '../../types/searchScope'
+import Chance from 'chance'
+import { Layer } from '../../oasis-nexus/api'
+import { usePontusXAccountName, useSearchForPontusXAccountsByName } from '../data/pontusx-account-names'
+
+const NO_MATCH = '__no_match__'
+
+export type AccountNameInfo = {
+ name: string | undefined
+ loading: boolean
+}
+
+/**
+ * Do we want to see some random names?
+ */
+const DEBUG_MODE = true
+
+/**
+ * Look up the name of an account.
+ */
+const lookupName = (scope: SearchScope, _address: string): string | undefined => {
+ switch (scope.layer) {
+ // TODO: look up the data
+ default:
+ // If debug mode is on, return mock names in ~50% of the cases, no nome otherwise
+ return DEBUG_MODE && Math.random() < 0.5 ? new Chance().name() : undefined
+ }
+}
+
+const nameCache: Map = new Map()
+
+/**
+ * Find out the name of an account
+ *
+ * This is the entry point that should be used by the application,
+ * since this function also includes caching.
+ */
+export const useAccountName = (scope: SearchScope, address: string, dropCache = false): AccountNameInfo => {
+ const isPontusX = scope.layer === Layer.pontusx
+
+ const pontusXName = usePontusXAccountName(address, isPontusX)
+ if (isPontusX) return pontusXName
+
+ const key = `${scope.network}.${scope.layer}.${address}`
+
+ if (dropCache) nameCache.delete(key)
+ const hasMatch = nameCache.has(key)
+ if (hasMatch) {
+ const cachedName = nameCache.get(key)
+ return {
+ name: cachedName === NO_MATCH ? undefined : cachedName,
+ loading: false,
+ }
+ }
+ const name = lookupName(scope, address)
+ nameCache.set(key, name ?? NO_MATCH)
+ return {
+ name,
+ loading: false,
+ }
+}
+
+export const useSearchForAccountsByName = (scope: SearchScope, nameFragment = '') => {
+ const isValidPontusXSearch = scope.layer === Layer.pontusx && !!nameFragment
+ const pontusXResults = useSearchForPontusXAccountsByName(scope.network, nameFragment, isValidPontusXSearch)
+ return isValidPontusXSearch
+ ? pontusXResults
+ : {
+ isLoading: false,
+ results: [],
+ }
+}
diff --git a/src/app/pages/ConsensusAccountDetailsPage/DeferredConsensusAccountDetails.tsx b/src/app/pages/ConsensusAccountDetailsPage/DeferredConsensusAccountDetails.tsx
new file mode 100644
index 000000000..5720b4ecd
--- /dev/null
+++ b/src/app/pages/ConsensusAccountDetailsPage/DeferredConsensusAccountDetails.tsx
@@ -0,0 +1,21 @@
+import { FC } from 'react'
+import { AllTokenPrices } from '../../../coin-gecko/api'
+import { Network } from '../../../types/network'
+
+/**
+ * Load and display details of a RuntimeAccount
+ */
+export const DeferredConsensusAccountDetails: FC<{
+ network: Network
+ address: string
+ tokenPrices: AllTokenPrices
+ highlightedPartOfName: string | undefined
+ showLayer?: boolean
+}> = () =>
+ // {
+ // network, address, tokenPrices, highlightedPartOfName, showLayer
+ // },
+ {
+ // TODO: load and display consensus account details when API and component becomes available
+ return null
+ }
diff --git a/src/app/pages/NFTInstanceDashboardPage/InstanceTitleCard.tsx b/src/app/pages/NFTInstanceDashboardPage/InstanceTitleCard.tsx
index ef0b1d0e1..beb512b66 100644
--- a/src/app/pages/NFTInstanceDashboardPage/InstanceTitleCard.tsx
+++ b/src/app/pages/NFTInstanceDashboardPage/InstanceTitleCard.tsx
@@ -66,7 +66,7 @@ export const InstanceTitleCard: FC = ({ isFetched, isLoa
}}
>
-
+
diff --git a/src/app/pages/RuntimeAccountDetailsPage/AccountDetailsCard.tsx b/src/app/pages/RuntimeAccountDetailsPage/AccountDetailsCard.tsx
index 18c57866d..ca4f98625 100644
--- a/src/app/pages/RuntimeAccountDetailsPage/AccountDetailsCard.tsx
+++ b/src/app/pages/RuntimeAccountDetailsPage/AccountDetailsCard.tsx
@@ -12,6 +12,7 @@ type AccountDetailsProps = {
account: RuntimeAccount | undefined
token: EvmToken | undefined
tokenPrices: AllTokenPrices
+ highlightedPartOfName?: string | undefined
}
export const AccountDetailsCard: FC = ({
@@ -21,6 +22,7 @@ export const AccountDetailsCard: FC = ({
account,
token,
tokenPrices,
+ highlightedPartOfName,
}) => {
const { t } = useTranslation()
return (
@@ -36,6 +38,7 @@ export const AccountDetailsCard: FC = ({
account={account}
token={token}
tokenPrices={tokenPrices}
+ highlightedPartOfName={highlightedPartOfName}
/>
)
diff --git a/src/app/pages/RuntimeAccountDetailsPage/AccountDetailsView.tsx b/src/app/pages/RuntimeAccountDetailsPage/AccountDetailsView.tsx
index 0c9f70225..4e94e2a4d 100644
--- a/src/app/pages/RuntimeAccountDetailsPage/AccountDetailsView.tsx
+++ b/src/app/pages/RuntimeAccountDetailsPage/AccountDetailsView.tsx
@@ -12,7 +12,8 @@ export const AccountDetailsView: FC<{
token?: EvmToken
tokenPrices: AllTokenPrices
showLayer?: boolean
-}> = ({ isLoading, isError, account, token, tokenPrices, showLayer }) => {
+ highlightedPartOfName?: string | undefined
+}> = ({ isLoading, isError, account, token, tokenPrices, showLayer, highlightedPartOfName }) => {
const { t } = useTranslation()
return isError ? (
@@ -23,6 +24,7 @@ export const AccountDetailsView: FC<{
isLoading={isLoading}
tokenPrices={tokenPrices}
showLayer={showLayer}
+ highlightedPartOfName={highlightedPartOfName}
/>
)
}
diff --git a/src/app/pages/RuntimeAccountDetailsPage/AccountNFTCollectionCard.tsx b/src/app/pages/RuntimeAccountDetailsPage/AccountNFTCollectionCard.tsx
index 6c2d9dd63..5a2deced1 100644
--- a/src/app/pages/RuntimeAccountDetailsPage/AccountNFTCollectionCard.tsx
+++ b/src/app/pages/RuntimeAccountDetailsPage/AccountNFTCollectionCard.tsx
@@ -44,7 +44,7 @@ export const AccountNFTCollectionCard: FC = ({ sco
isFetched &&
firstToken && (
-
+
)
diff --git a/src/app/pages/RuntimeAccountDetailsPage/AccountTokensCard.tsx b/src/app/pages/RuntimeAccountDetailsPage/AccountTokensCard.tsx
index f412300a2..77c34231d 100644
--- a/src/app/pages/RuntimeAccountDetailsPage/AccountTokensCard.tsx
+++ b/src/app/pages/RuntimeAccountDetailsPage/AccountTokensCard.tsx
@@ -30,10 +30,14 @@ type AccountTokensCardProps = RuntimeAccountDetailsContext & {
export const accountTokenContainerId = 'tokens'
-export const ContractLink: FC<{ scope: SearchScope; address: string }> = ({ scope, address }) => {
+export const ContractLink: FC<{ scope: SearchScope; address: string; alwaysTrim?: boolean }> = ({
+ scope,
+ address,
+ alwaysTrim,
+}) => {
return (
-
+
)
@@ -79,7 +83,11 @@ export const AccountTokensCard: FC = ({ scope, account,
{
content: (
-
+
),
key: 'hash',
diff --git a/src/app/pages/RuntimeAccountDetailsPage/DeferredRuntimeAccountDetails.tsx b/src/app/pages/RuntimeAccountDetailsPage/DeferredRuntimeAccountDetails.tsx
new file mode 100644
index 000000000..882f742cc
--- /dev/null
+++ b/src/app/pages/RuntimeAccountDetailsPage/DeferredRuntimeAccountDetails.tsx
@@ -0,0 +1,29 @@
+import { FC } from 'react'
+import { Runtime, useGetRuntimeAccountsAddress } from '../../../oasis-nexus/api'
+import { AllTokenPrices } from '../../../coin-gecko/api'
+import { AccountDetailsView } from './AccountDetailsView'
+import { Network } from '../../../types/network'
+
+/**
+ * Load and display details of a RuntimeAccount
+ */
+export const DeferredRuntimeAccountDetails: FC<{
+ network: Network
+ layer: Runtime
+ address: string
+ tokenPrices: AllTokenPrices
+ highlightedPartOfName: string | undefined
+ showLayer?: boolean
+}> = ({ network, layer, address, tokenPrices, highlightedPartOfName, showLayer }) => {
+ const { data, isLoading, isError } = useGetRuntimeAccountsAddress(network, layer, address)
+ return (
+
+ )
+}
diff --git a/src/app/pages/RuntimeAccountDetailsPage/index.tsx b/src/app/pages/RuntimeAccountDetailsPage/index.tsx
index b6c50e28d..8d57ecd01 100644
--- a/src/app/pages/RuntimeAccountDetailsPage/index.tsx
+++ b/src/app/pages/RuntimeAccountDetailsPage/index.tsx
@@ -32,7 +32,7 @@ export const RuntimeAccountDetailsPage: FC = () => {
const { t } = useTranslation()
const scope = useRequiredScopeParam()
- const { address } = useLoaderData() as AddressLoaderData
+ const { address, searchTerm } = useLoaderData() as AddressLoaderData
const { account, isLoading: isAccountLoading, isError } = useAccount(scope, address)
const isContract = !!account?.evm_contract
const { token, isLoading: isTokenLoading } = useTokenInfo(scope, address, isContract)
@@ -62,6 +62,7 @@ export const RuntimeAccountDetailsPage: FC = () => {
account={account}
token={token}
tokenPrices={tokenPrices}
+ highlightedPartOfName={searchTerm}
/>
> = ({ label, children }) => {
- return (
-
-
- {label}
-
- }
- >
- {children}
-
- )
-}
-
export const RuntimeTransactionDetailView: FC<{
isLoading?: boolean
transaction: TransactionDetailRuntimeBlock | undefined
@@ -214,22 +194,20 @@ export const RuntimeTransactionDetailView: FC<{
<>
{t('common.hash')}
-
-
-
+ />
+
{hash && }
>
@@ -270,23 +248,20 @@ export const RuntimeTransactionDetailView: FC<{
<>
{t('common.from')}
-
-
-
+ />
{from && }
>
@@ -296,20 +271,17 @@ export const RuntimeTransactionDetailView: FC<{
<>
{t('common.to')}
-
-
-
+ />
{to && }
>
diff --git a/src/app/pages/SearchResultsPage/SearchResultsList.tsx b/src/app/pages/SearchResultsPage/SearchResultsList.tsx
index 335174f87..f6984f29a 100644
--- a/src/app/pages/SearchResultsPage/SearchResultsList.tsx
+++ b/src/app/pages/SearchResultsPage/SearchResultsList.tsx
@@ -7,6 +7,7 @@ import { RuntimeTransactionDetailView } from '../RuntimeTransactionDetailPage'
import { AccountDetailsView } from '../RuntimeAccountDetailsPage/AccountDetailsView'
import {
AccountResult,
+ AccountAddressResult,
BlockResult,
ContractResult,
ProposalResult,
@@ -21,6 +22,9 @@ import { AllTokenPrices } from '../../../coin-gecko/api'
import { ResultListFrame } from './ResultListFrame'
import { TokenDetails } from '../../components/Tokens/TokenDetails'
import { ProposalDetailView } from '../ProposalDetailsPage'
+import { DeferredRuntimeAccountDetails } from '../RuntimeAccountDetailsPage/DeferredRuntimeAccountDetails'
+import { Layer } from '../../../oasis-nexus/api'
+import { DeferredConsensusAccountDetails } from '../ConsensusAccountDetailsPage/DeferredConsensusAccountDetails'
/**
* Component for displaying a list of search results
@@ -94,6 +98,35 @@ export const SearchResultsList: FC<{
linkLabel={t('search.results.accounts.viewLink')}
/>
+ item.resultType === 'accountAddress',
+ )}
+ resultComponent={item =>
+ item.layer === Layer.consensus ? (
+
+ ) : (
+
+ )
+ }
+ link={acc => RouteUtils.getAccountRoute(acc, acc.address)}
+ linkLabel={t('search.results.accounts.viewLink')}
+ />
+
item.resultType === 'contract')}
diff --git a/src/app/pages/SearchResultsPage/hooks.ts b/src/app/pages/SearchResultsPage/hooks.ts
index 395a96cbf..b5450202e 100644
--- a/src/app/pages/SearchResultsPage/hooks.ts
+++ b/src/app/pages/SearchResultsPage/hooks.ts
@@ -19,6 +19,7 @@ import {
import { RouteUtils } from '../../utils/route-utils'
import { SearchParams } from '../../components/Search/search-utils'
import { SearchScope } from '../../../types/searchScope'
+import { useSearchForAccountsByName } from '../../hooks/useAccountName'
function isDefined(item: T): item is NonNullable {
return item != null
@@ -27,7 +28,7 @@ function isDefined(item: T): item is NonNullable {
export type ConditionalResults = { isLoading: boolean; results: T[] }
type SearchResultItemCore = HasScope & {
- resultType: 'block' | 'transaction' | 'account' | 'contract' | 'token' | 'proposal'
+ resultType: 'block' | 'transaction' | 'account' | 'accountAddress' | 'contract' | 'token' | 'proposal'
}
export type BlockResult = SearchResultItemCore & RuntimeBlock & { resultType: 'block' }
@@ -36,6 +37,10 @@ export type TransactionResult = SearchResultItemCore & RuntimeTransaction & { re
export type AccountResult = SearchResultItemCore & RuntimeAccount & { resultType: 'account' }
+export type AccountAddressResult = SearchResultItemCore & { address: string } & {
+ resultType: 'accountAddress'
+}
+
export type ContractResult = SearchResultItemCore & RuntimeAccount & { resultType: 'contract' }
export type TokenResult = SearchResultItemCore & EvmToken & { resultType: 'token' }
@@ -46,6 +51,7 @@ export type SearchResultItem =
| BlockResult
| TransactionResult
| AccountResult
+ | AccountAddressResult
| ContractResult
| TokenResult
| ProposalResult
@@ -193,6 +199,25 @@ export function useNetworkProposalsConditionally(
}
}
+type AccountAddressInfo = Pick
+
+export function useNamedAccountConditionally(
+ currentScope: SearchScope | undefined,
+ nameFragment: string | undefined,
+): ConditionalResults {
+ const queries = RouteUtils.getVisibleScopes(currentScope).map(scope =>
+ // eslint-disable-next-line react-hooks/rules-of-hooks
+ useSearchForAccountsByName(scope, nameFragment),
+ )
+ return {
+ isLoading: queries.some(query => query.isLoading),
+ results: queries
+ .map(query => query.results)
+ .filter(isDefined)
+ .flat(),
+ }
+}
+
export const useSearch = (currentScope: SearchScope | undefined, q: SearchParams) => {
const queries = {
blockHeight: useBlocksByHeightConditionally(currentScope, q.blockHeight),
@@ -201,6 +226,7 @@ export const useSearch = (currentScope: SearchScope | undefined, q: SearchParams
oasisAccount: useRuntimeAccountConditionally(currentScope, q.consensusAccount),
// TODO: remove evmBech32Account and use evmAccount when API is ready
evmBech32Account: useRuntimeAccountConditionally(currentScope, q.evmBech32Account),
+ accountsByName: useNamedAccountConditionally(currentScope, q.accountNameFragment),
tokens: useRuntimeTokenConditionally(currentScope, q.evmTokenNameFragment),
proposals: useNetworkProposalsConditionally(q.networkProposalNameFragment),
}
@@ -211,6 +237,7 @@ export const useSearch = (currentScope: SearchScope | undefined, q: SearchParams
...(queries.oasisAccount.results || []),
...(queries.evmBech32Account.results || []),
].filter(isAccountNonEmpty)
+ const accountAddresses = queries.accountsByName.results || []
const tokens = queries.tokens.results
.map(l => l.evm_tokens)
.flat()
@@ -228,6 +255,9 @@ export const useSearch = (currentScope: SearchScope | undefined, q: SearchParams
...accounts
.filter(account => account.evm_contract)
.map((account): ContractResult => ({ ...account, resultType: 'contract' })),
+ ...accountAddresses.map(
+ (account): AccountAddressResult => ({ ...account, resultType: 'accountAddress' }),
+ ),
...tokens.map((token): TokenResult => ({ ...token, resultType: 'token' })),
...proposals.map((proposal): ProposalResult => ({ ...proposal, resultType: 'proposal' })),
]
diff --git a/src/app/pages/SearchResultsPage/useRedirectIfSingleResult.ts b/src/app/pages/SearchResultsPage/useRedirectIfSingleResult.ts
index ebcb816b8..e3e704e02 100644
--- a/src/app/pages/SearchResultsPage/useRedirectIfSingleResult.ts
+++ b/src/app/pages/SearchResultsPage/useRedirectIfSingleResult.ts
@@ -35,6 +35,9 @@ export function useRedirectIfSingleResult(
case 'account':
redirectTo = RouteUtils.getAccountRoute(item, item.address_eth ?? item.address)
break
+ case 'accountAddress':
+ redirectTo = `${RouteUtils.getAccountRoute(item, item.address)}?q=${searchTerm}`
+ break
case 'contract':
redirectTo = RouteUtils.getAccountRoute(item, item.address_eth ?? item.address)
break
diff --git a/src/app/pages/TokenDashboardPage/NFTLinks.tsx b/src/app/pages/TokenDashboardPage/NFTLinks.tsx
index e70dd170d..a2c6979dc 100644
--- a/src/app/pages/TokenDashboardPage/NFTLinks.tsx
+++ b/src/app/pages/TokenDashboardPage/NFTLinks.tsx
@@ -66,8 +66,9 @@ export const NFTInstanceLink: FC = ({ scope, instance }) => {
type NFTOwnerLinkProps = {
scope: SearchScope
owner: string
+ alwaysTrim?: boolean
}
-export const NFTOwnerLink: FC = ({ scope, owner }) => {
+export const NFTOwnerLink: FC = ({ scope, owner, alwaysTrim }) => {
const { t } = useTranslation()
return (
@@ -76,7 +77,7 @@ export const NFTOwnerLink: FC = ({ scope, owner }) => {
i18nKey="nft.ownerLink"
t={t}
components={{
- OwnerLink: ,
+ OwnerLink: ,
}}
/>
diff --git a/src/app/pages/TokenDashboardPage/TokenDetailsCard.tsx b/src/app/pages/TokenDashboardPage/TokenDetailsCard.tsx
index 8c023c955..5fd211da1 100644
--- a/src/app/pages/TokenDashboardPage/TokenDetailsCard.tsx
+++ b/src/app/pages/TokenDashboardPage/TokenDetailsCard.tsx
@@ -69,7 +69,11 @@ export const TokenDetailsCard: FC<{ scope: SearchScope; address: string; searchT
{t('contract.creator')}
-
+
{t('common.balance')}
diff --git a/src/app/pages/TokenDashboardPage/TokenTitleCard.tsx b/src/app/pages/TokenDashboardPage/TokenTitleCard.tsx
index cfad3b218..fdfb5ab8a 100644
--- a/src/app/pages/TokenDashboardPage/TokenTitleCard.tsx
+++ b/src/app/pages/TokenDashboardPage/TokenTitleCard.tsx
@@ -25,7 +25,11 @@ export const TokenTitleCard: FC<{ scope: SearchScope; address: string; searchTer
{token && (
<>
-
+
>
)}
diff --git a/yarn.lock b/yarn.lock
index e8b05519d..12d280054 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -4239,6 +4239,11 @@
"@types/connect" "*"
"@types/node" "*"
+"@types/chance@^1.1.6":
+ version "1.1.6"
+ resolved "https://registry.yarnpkg.com/@types/chance/-/chance-1.1.6.tgz#2fe3de58742629602c3fbab468093b27207f04ad"
+ integrity sha512-V+pm3stv1Mvz8fSKJJod6CglNGVqEQ6OyuqitoDkWywEODM/eJd1eSuIp9xt6DrX8BWZ2eDSIzbw1tPCUTvGbQ==
+
"@types/connect@*":
version "3.4.35"
resolved "https://registry.yarnpkg.com/@types/connect/-/connect-3.4.35.tgz#5fcf6ae445e4021d1fc2219a4873cc73a3bb2ad1"
@@ -5789,6 +5794,11 @@ chalk@^4.0.0, chalk@^4.0.2, chalk@^4.1.0, chalk@^4.1.2:
ansi-styles "^4.1.0"
supports-color "^7.1.0"
+chance@^1.1.11:
+ version "1.1.11"
+ resolved "https://registry.yarnpkg.com/chance/-/chance-1.1.11.tgz#78e10e1f9220a5bbc60a83e3f28a5d8558d84d1b"
+ integrity sha512-kqTg3WWywappJPqtgrdvbA380VoXO2eu9VCV895JgbyHsaErXdyHK9LOZ911OvAk6L0obK7kDk9CGs8+oBawVA==
+
char-regex@^1.0.2:
version "1.0.2"
resolved "https://registry.yarnpkg.com/char-regex/-/char-regex-1.0.2.tgz#d744358226217f981ed58f479b1d6bcc29545dcf"