diff --git a/.changelog/1069.feature.md b/.changelog/1069.feature.md
new file mode 100644
index 000000000..40c1421c8
--- /dev/null
+++ b/.changelog/1069.feature.md
@@ -0,0 +1 @@
+Add NFT feature
diff --git a/src/app/components/Account/ContractCreatorInfo.tsx b/src/app/components/Account/ContractCreatorInfo.tsx
index 5fd065436..a944c8362 100644
--- a/src/app/components/Account/ContractCreatorInfo.tsx
+++ b/src/app/components/Account/ContractCreatorInfo.tsx
@@ -1,6 +1,6 @@
import { FC } from 'react'
import { SearchScope } from '../../../types/searchScope'
-import { Trans, useTranslation } from 'react-i18next'
+import { useTranslation } from 'react-i18next'
import { TransactionLink } from '../Transactions/TransactionLink'
import {
Layer,
@@ -12,6 +12,7 @@ import { AppErrors } from '../../../types/errors'
import { AccountLink } from './AccountLink'
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 { t } = useTranslation()
@@ -42,6 +43,7 @@ export const ContractCreatorInfo: FC<{
creationTxHash: string | undefined
}> = ({ scope, isLoading, creationTxHash }) => {
const { t } = useTranslation()
+ const { isMobile } = useScreenSize()
return isLoading ? (
@@ -51,20 +53,15 @@ export const ContractCreatorInfo: FC<{
-
- ,
- }}
- />
+ {t('contract.createdAt')}
+
)
}
diff --git a/src/app/components/AppendMobileSearch/index.tsx b/src/app/components/AppendMobileSearch/index.tsx
index 2ea0aab38..0211fc628 100644
--- a/src/app/components/AppendMobileSearch/index.tsx
+++ b/src/app/components/AppendMobileSearch/index.tsx
@@ -9,12 +9,16 @@ interface AppendMobileSearchProps {
action?: ReactNode
}
+interface AppendMobileSearchLayoutProps {
+ action?: ReactNode
+ isMobile: boolean
+}
+
const Layout = styled(Box, {
- shouldForwardProp: (prop: PropertyKey) =>
- !(['action'] as (keyof AppendMobileSearchProps)[]).includes(prop as keyof AppendMobileSearchProps),
-})(({ action }) => ({
+ shouldForwardProp: prop => prop !== 'action' && prop !== 'isMobile',
+})(({ action, isMobile }) => ({
position: 'relative',
- alignItems: 'flex-start',
+ alignItems: isMobile ? 'center' : 'flex-start',
width: '100%',
...(action
? {
@@ -41,7 +45,7 @@ export const AppendMobileSearch: FC &
const { isMobile } = useScreenSize()
return (
-
+
{children}
{action}
diff --git a/src/app/components/CardHeaderWithCounter/index.tsx b/src/app/components/CardHeaderWithCounter/index.tsx
new file mode 100644
index 000000000..58b93f800
--- /dev/null
+++ b/src/app/components/CardHeaderWithCounter/index.tsx
@@ -0,0 +1,29 @@
+import { FC } from 'react'
+import Box from '@mui/material/Box'
+import Typography from '@mui/material/Typography'
+import { COLORS } from 'styles/theme/colors'
+
+type CardHeaderWithCounterProps = {
+ label: string | undefined
+ totalCount: number | undefined
+ isTotalCountClipped: boolean | undefined
+}
+
+export const CardHeaderWithCounter: FC = ({
+ label,
+ totalCount,
+ isTotalCountClipped,
+}) => {
+ return (
+
+
+ {label}
+
+ {!!totalCount && (
+
+ ({`${isTotalCountClipped ? ' > ' : ''}${totalCount}`})
+
+ )}
+
+ )
+}
diff --git a/src/app/components/NoPreview/index.tsx b/src/app/components/NoPreview/index.tsx
index 80f6e6127..9459efcc2 100644
--- a/src/app/components/NoPreview/index.tsx
+++ b/src/app/components/NoPreview/index.tsx
@@ -18,6 +18,7 @@ export const NoPreview: FC = ({ placeholderSize }) => {
height: placeholderSize,
width: placeholderSize,
display: 'flex',
+ flex: 1,
justifyContent: 'center',
flexDirection: 'column',
alignItems: 'center',
diff --git a/src/app/components/ProgressBar/index.ts b/src/app/components/ProgressBar/index.ts
index d2ee8b69c..325237cc6 100644
--- a/src/app/components/ProgressBar/index.ts
+++ b/src/app/components/ProgressBar/index.ts
@@ -1,8 +1,22 @@
-import LinearProgress from '@mui/material/LinearProgress'
+import MuiLinearProgress, { linearProgressClasses } from '@mui/material/LinearProgress'
import { styled } from '@mui/material/styles'
+import { COLORS } from 'styles/theme/colors'
-export const VerticalProgressBar = styled(LinearProgress)(() => ({
+export const VerticalProgressBar = styled(MuiLinearProgress)(() => ({
+ [`&.${linearProgressClasses.determinate} > .${linearProgressClasses.bar1Determinate}`]: {
+ backgroundColor: COLORS.brandDark,
+ },
height: 36,
width: 36,
transform: 'rotate(-90deg)',
}))
+
+export const ProgressBar = styled(MuiLinearProgress)(() => ({
+ [`&.${linearProgressClasses.determinate} > .${linearProgressClasses.bar1Determinate}`]: {
+ backgroundColor: COLORS.eucalyptus,
+ },
+ borderColor: COLORS.eucalyptus,
+ backgroundColor: COLORS.honeydew,
+ height: '12px',
+ width: '85px',
+}))
diff --git a/src/app/components/Tokens/TokenDetails.tsx b/src/app/components/Tokens/TokenDetails.tsx
index 400c86107..0ba7e6c27 100644
--- a/src/app/components/Tokens/TokenDetails.tsx
+++ b/src/app/components/Tokens/TokenDetails.tsx
@@ -12,7 +12,7 @@ import { VerificationIcon } from '../ContractVerificationIcon'
import Box from '@mui/material/Box'
import { COLORS } from '../../../styles/theme/colors'
import { TokenTypeTag } from './TokenList'
-import { getPreciseNumberFormat } from '../../../locales/getPreciseNumberFormat'
+import { RoundedBalance } from '../RoundedBalance'
export const TokenDetails: FC<{
isLoading?: boolean
@@ -39,7 +39,9 @@ export const TokenDetails: FC<{
{t('common.name')}
- ({token.symbol})
+
+ ({token.symbol})
+
{t('common.type')}
@@ -64,12 +66,11 @@ export const TokenDetails: FC<{
{t('tokens.totalSupply')}
- {token.total_supply
- ? t('tokens.totalSupplyValue', {
- ...getPreciseNumberFormat(token.total_supply),
- ticker: token.symbol,
- })
- : t('common.missing')}
+ {token.total_supply ? (
+
+ ) : (
+ t('common.missing')
+ )}
)
diff --git a/src/app/components/Tokens/TokenHolders.tsx b/src/app/components/Tokens/TokenHolders.tsx
index da83e66c7..77daeae62 100644
--- a/src/app/components/Tokens/TokenHolders.tsx
+++ b/src/app/components/Tokens/TokenHolders.tsx
@@ -1,11 +1,13 @@
import { FC } from 'react'
import { useTranslation } from 'react-i18next'
+import Box from '@mui/material/Box'
import { Table, TableCellAlign, TableColProps } from '../Table'
import { BareTokenHolder } from '../../../oasis-nexus/api'
import { TablePaginationProps } from '../Table/TablePagination'
import { AccountLink } from '../Account/AccountLink'
import { fromBaseUnits } from '../../utils/helpers'
import { RoundedBalance } from '../RoundedBalance'
+import { ProgressBar } from '../ProgressBar'
type TableTokenHolder = BareTokenHolder & {
markAsNew?: boolean
@@ -30,16 +32,14 @@ export const TokenHolders: FC = ({
}) => {
const { t } = useTranslation()
const tableColumns: TableColProps[] = [
- { key: 'rank', content: t('common.rank'), align: TableCellAlign.Center },
+ { key: 'rank', content: t('common.rank') },
{ key: 'address', content: t('common.address') },
{ key: 'quantity', content: t('common.quantity'), align: TableCellAlign.Right },
{ key: 'percentage', content: t('common.percentage'), align: TableCellAlign.Right },
]
- const calculateRatio = (balance: string): string => {
- return totalSupply === undefined
- ? t('common.missing')
- : `${((100 * parseFloat(fromBaseUnits(balance, decimals))) / parseFloat(totalSupply)).toFixed(4)}%`
+ const calculateRatio = (balance: string, totalSupply: string, decimals: number): number => {
+ return (100 * parseFloat(fromBaseUnits(balance, decimals))) / parseFloat(totalSupply)
}
const tableRows = holders?.map((holder, index) => {
@@ -49,7 +49,6 @@ export const TokenHolders: FC = ({
{
key: 'rank',
content: holder.rank.toLocaleString(),
- align: TableCellAlign.Center,
},
{
key: 'address',
@@ -64,7 +63,21 @@ export const TokenHolders: FC = ({
},
{
key: 'percentage',
- content: calculateRatio(holder.balance),
+ content: (
+ <>
+ {totalSupply ? (
+
+ {`${calculateRatio(holder.balance, totalSupply, decimals).toFixed(4)}%`}
+
+
+ ) : (
+ {t('common.missing')}
+ )}
+ >
+ ),
align: TableCellAlign.Right,
},
],
diff --git a/src/app/components/Tokens/TokenList.tsx b/src/app/components/Tokens/TokenList.tsx
index 813d41ed0..9519e119d 100644
--- a/src/app/components/Tokens/TokenList.tsx
+++ b/src/app/components/Tokens/TokenList.tsx
@@ -67,7 +67,7 @@ export const TokenList = (props: TokensProps) => {
{ key: 'verification', content: t('contract.verification.title') },
{
key: 'holders',
- content: t('tokens.holdersCount'),
+ content: t('tokens.holdersCount_short'),
align: TableCellAlign.Right,
},
{ key: 'supply', content: t('tokens.totalSupply'), align: TableCellAlign.Right },
@@ -98,7 +98,11 @@ export const TokenList = (props: TokensProps) => {
},
{
key: 'type',
- content: ,
+ content: (
+
+
+
+ ),
},
{
content: (
diff --git a/src/app/components/Tokens/TokenTransfers.tsx b/src/app/components/Tokens/TokenTransfers.tsx
index 702a5d20b..79c3bb232 100644
--- a/src/app/components/Tokens/TokenTransfers.tsx
+++ b/src/app/components/Tokens/TokenTransfers.tsx
@@ -20,6 +20,7 @@ import { TokenLink } from './TokenLink'
import { PlaceholderLabel } from '../../utils/PlaceholderLabel'
import { TokenTypeTag } from './TokenList'
import { parseEvmEvent } from '../../utils/parseEvmEvent'
+import { formatDistanceToNow } from '../../utils/dateFormatter'
const iconSize = '28px'
const StyledCircle = styled(Box)(({ theme }) => ({
@@ -119,6 +120,7 @@ export const TokenTransfers: FC = ({
const tableColumns: TableColProps[] = [
{ key: 'hash', content: t('common.hash') },
{ key: 'block', content: t('common.block') },
+ { key: 'timestamp', content: t('common.age'), align: TableCellAlign.Right },
{ key: 'type', content: t('common.type'), align: TableCellAlign.Center },
{ key: 'from', content: t('common.from'), width: '150px' },
{ key: 'to', content: t('common.to'), width: '150px' },
@@ -147,6 +149,11 @@ export const TokenTransfers: FC = ({
content: ,
key: 'round',
},
+ {
+ align: TableCellAlign.Right,
+ content: formatDistanceToNow(new Date(transfer.timestamp)),
+ key: 'timestamp',
+ },
{
key: 'type',
align: TableCellAlign.Center,
diff --git a/src/app/pages/AccountDetailsPage/AccountNFTCollectionCard.tsx b/src/app/pages/AccountDetailsPage/AccountNFTCollectionCard.tsx
index 711a60d6d..449792103 100644
--- a/src/app/pages/AccountDetailsPage/AccountNFTCollectionCard.tsx
+++ b/src/app/pages/AccountDetailsPage/AccountNFTCollectionCard.tsx
@@ -18,7 +18,6 @@ import { AccountDetailsContext } from './index'
import { AccountLink } from 'app/components/Account/AccountLink'
import { CopyToClipboard } from 'app/components/CopyToClipboard'
import { RouteUtils } from 'app/utils/route-utils'
-import { COLORS } from 'styles/theme/colors'
import { ImageListItemImage } from '../TokenDashboardPage/ImageListItemImage'
import { CardEmptyState } from '../AccountDetailsPage/CardEmptyState'
import { TablePagination } from '../../components/Table/TablePagination'
@@ -26,6 +25,7 @@ import { useAccountTokenInventory } from '../TokenDashboardPage/hook'
import { EvmNft } from 'oasis-nexus/api'
import { SearchScope } from '../../../types/searchScope'
import { NFTCollectionLink, NFTInstanceLink } from '../TokenDashboardPage/NFTLinks'
+import { CardHeaderWithCounter } from 'app/components/CardHeaderWithCounter'
export const accountNFTCollectionContainerId = 'nftCollection'
@@ -63,14 +63,11 @@ export const AccountNFTCollectionCard: FC = ({ scope, add
{isFetched && (
-
-
- {firstToken?.name ? inventory?.[0].token.name : t('common.collection')}
-
- {!!totalCount && (
- ({`${isTotalCountClipped ? ' > ' : ''}${totalCount}`})
- )}
-
+
)}
{isLoading && }
diff --git a/src/app/pages/NFTInstanceDashboardPage/InstanceDetailsCard.tsx b/src/app/pages/NFTInstanceDashboardPage/InstanceDetailsCard.tsx
index c583ab371..5e25bfd94 100644
--- a/src/app/pages/NFTInstanceDashboardPage/InstanceDetailsCard.tsx
+++ b/src/app/pages/NFTInstanceDashboardPage/InstanceDetailsCard.tsx
@@ -72,10 +72,10 @@ export const InstanceDetailsCard: FC = ({
>
)}
- {nft?.token?.num_transfers && (
+ {nft?.num_transfers && (
<>
{t('nft.transfers')}
- {nft.token.num_transfers!.toLocaleString()}
+ {nft.num_transfers.toLocaleString()}
>
)}
{t(isMobile ? 'common.smartContract_short' : 'common.smartContract')}
diff --git a/src/app/pages/NFTInstanceDashboardPage/NFTMetadataCard.tsx b/src/app/pages/NFTInstanceDashboardPage/NFTMetadataCard.tsx
index 1c65e3c9f..5303130da 100644
--- a/src/app/pages/NFTInstanceDashboardPage/NFTMetadataCard.tsx
+++ b/src/app/pages/NFTInstanceDashboardPage/NFTMetadataCard.tsx
@@ -2,23 +2,18 @@ import { FC } from 'react'
import { useTranslation } from 'react-i18next'
import Card from '@mui/material/Card'
import CardContent from '@mui/material/CardContent'
-import { Runtime, useGetRuntimeEvmTokensAddressNftsId } from '../../../oasis-nexus/api'
import { LinkableDiv } from '../../components/PageLayout/LinkableDiv'
import { CardEmptyState } from './../AccountDetailsPage/CardEmptyState'
import { NftDashboardContext } from '../NFTInstanceDashboardPage'
import { JsonCodeDisplay } from '../../components/CodeDisplay'
+import { useNFTInstance } from '../TokenDashboardPage/hook'
export const nftMetadataId = 'metadata'
export const NFTMetadataCard: FC = ({ scope, address, instanceId }) => {
const { t } = useTranslation()
- const { data, isFetched } = useGetRuntimeEvmTokensAddressNftsId(
- scope.network,
- scope.layer as Runtime,
- address,
- instanceId,
- )
- const metadata = data?.data?.metadata
+ const { isFetched, nft } = useNFTInstance(scope, address, instanceId)
+ const metadata = nft?.metadata
return (
diff --git a/src/app/pages/NFTInstanceDashboardPage/index.tsx b/src/app/pages/NFTInstanceDashboardPage/index.tsx
index 8fe0056a1..ac55d7e59 100644
--- a/src/app/pages/NFTInstanceDashboardPage/index.tsx
+++ b/src/app/pages/NFTInstanceDashboardPage/index.tsx
@@ -6,10 +6,10 @@ import { PageLayout } from '../../components/PageLayout'
import { InstanceTitleCard } from './InstanceTitleCard'
import { InstanceDetailsCard } from './InstanceDetailsCard'
import { InstanceImageCard } from './InstanceImageCard'
-import { Layer, Runtime, useGetRuntimeEvmTokensAddressNftsId } from '../../../oasis-nexus/api'
import { AppErrors } from '../../../types/errors'
import { RouterTabs } from 'app/components/RouterTabs'
import { SearchScope } from '../../../types/searchScope'
+import { useNFTInstance } from '../TokenDashboardPage/hook'
export type NftDashboardContext = {
scope: SearchScope
@@ -23,22 +23,11 @@ export const NFTInstanceDashboardPage: FC = () => {
const { t } = useTranslation()
const scope = useRequiredScopeParam()
const { address, instanceId } = useParams()
- if (scope.layer === Layer.consensus) {
- // There can be no ERC-20 or ERC-721 tokens on consensus
- throw AppErrors.UnsupportedLayer
- }
-
if (!address || !instanceId) {
throw AppErrors.InvalidUrl
}
+ const { isFetched, isLoading, nft } = useNFTInstance(scope, address, instanceId)
- const { data, isFetched, isLoading } = useGetRuntimeEvmTokensAddressNftsId(
- scope.network,
- scope.layer as Runtime,
- address,
- instanceId,
- )
- const nft = data?.data
const metadataLink = useHref('')
const context: NftDashboardContext = {
scope,
diff --git a/src/app/pages/TokenDashboardPage/ImageListItemImage.tsx b/src/app/pages/TokenDashboardPage/ImageListItemImage.tsx
index 89cada5f7..3fb266cbb 100644
--- a/src/app/pages/TokenDashboardPage/ImageListItemImage.tsx
+++ b/src/app/pages/TokenDashboardPage/ImageListItemImage.tsx
@@ -1,19 +1,53 @@
import { FC } from 'react'
+import { useTranslation } from 'react-i18next'
import { Link as RouterLink } from 'react-router-dom'
+import Box from '@mui/material/Box'
import Link from '@mui/material/Link'
+import OpenInBrowserIcon from '@mui/icons-material/OpenInBrowser'
import { styled } from '@mui/material/styles'
import { isNftImageUrlValid, processNftImageUrl } from 'app/utils/nft-images'
import { NoPreview } from '../../components/NoPreview'
import { getNftInstanceLabel } from '../../utils/nft'
import { EvmNft } from '../../../oasis-nexus/api'
+import { useScreenSize } from 'app/hooks/useScreensize'
+import { COLORS } from 'styles/theme/colors'
-const imageSize = '210px'
+const minMobileSize = '150px'
+const minSize = '210px'
-const StyledImage = styled('img')({
- width: imageSize,
- height: imageSize,
+const StyledImage = styled('img', {
+ shouldForwardProp: prop => prop !== 'isMobile',
+})<{ isMobile: boolean }>(({ isMobile }) => ({
+ minWidth: isMobile ? minMobileSize : minSize,
+ minHeight: isMobile ? minMobileSize : minSize,
+ width: '100%',
+ height: '100%',
+ maxHeight: minSize,
objectFit: 'cover',
-})
+ transition: 'opacity 250ms ease-in-out',
+ '&:hover, &:focus-visible': {
+ opacity: 0.15,
+ },
+}))
+
+const StyledBox = styled(Box)(() => ({
+ position: 'absolute',
+ display: 'flex',
+ flexDirection: 'column',
+ justifyContent: 'center',
+ alignItems: 'center',
+ width: '100%',
+ height: '100%',
+ fontSize: '14px',
+ fontWeight: 500,
+ color: COLORS.white,
+ transition: 'opacity, background-color 250ms ease-in-out',
+ opacity: 0,
+ '&:hover, &:focus-visible': {
+ opacity: 1,
+ backgroundColor: 'rgba(0,0,0,0.8)',
+ },
+}))
type ImageListItemImageProps = {
instance: EvmNft
@@ -21,17 +55,25 @@ type ImageListItemImageProps = {
}
export const ImageListItemImage: FC = ({ instance, to }) => {
+ const { t } = useTranslation()
+ const { isMobile } = useScreenSize()
+
return (
-
+
{isNftImageUrlValid(instance.image) ? (
) : (
-
+
)}
+
+
+ {t('common.view')}
+
)
}
diff --git a/src/app/pages/TokenDashboardPage/NFTLinks.tsx b/src/app/pages/TokenDashboardPage/NFTLinks.tsx
index cde013101..6b867828f 100644
--- a/src/app/pages/TokenDashboardPage/NFTLinks.tsx
+++ b/src/app/pages/TokenDashboardPage/NFTLinks.tsx
@@ -4,6 +4,7 @@ import { Link as RouterLink } from 'react-router-dom'
import { EvmNft } from 'oasis-nexus/api'
import Link from '@mui/material/Link'
import Typography from '@mui/material/Typography'
+import { styled } from '@mui/material/styles'
import { RouteUtils } from '../../utils/route-utils'
import { SearchScope } from '../../../types/searchScope'
import { trimLongString } from '../../utils/trimLongString'
@@ -13,12 +14,18 @@ type NFTLinkProps = {
instance: EvmNft
}
+const StyledTypography = styled(Typography)({
+ whiteSpace: 'nowrap',
+ overflow: 'hidden',
+ textOverflow: 'ellipsis',
+})
+
export const NFTCollectionLink: FC = ({ scope, instance }) => {
const { t } = useTranslation()
const to = RouteUtils.getTokenRoute(scope, instance.token?.contract_addr)
return (
-
+
= ({ scope, instance }) => {
),
}}
/>
-
+
)
}
export const NFTInstanceLink: FC = ({ scope, instance }) => {
const { t } = useTranslation()
- const to = RouteUtils.getNFTInstanceRoute(scope, instance.token?.contract_addr, instance.id)
+ const to = RouteUtils.getNFTInstanceRoute(scope, instance.token?.eth_contract_addr, instance.id)
return (
-
+
= ({ scope, instance }) => {
),
}}
/>
-
+
)
}
diff --git a/src/app/pages/TokenDashboardPage/TokenDetailsCard.tsx b/src/app/pages/TokenDashboardPage/TokenDetailsCard.tsx
index 2c652b9c5..7b3572b4d 100644
--- a/src/app/pages/TokenDashboardPage/TokenDetailsCard.tsx
+++ b/src/app/pages/TokenDashboardPage/TokenDetailsCard.tsx
@@ -19,6 +19,8 @@ import { SearchScope } from '../../../types/searchScope'
import { getPreciseNumberFormat } from '../../../locales/getPreciseNumberFormat'
import { RouteUtils } from '../../utils/route-utils'
import { tokenTransfersContainerId } from '../../pages/TokenDashboardPage/TokenTransfersCard'
+import { tokenHoldersContainerId } from '../../pages/TokenDashboardPage/TokenHoldersCard'
+import { RoundedBalance } from 'app/components/RoundedBalance'
export const TokenDetailsCard: FC<{ scope: SearchScope; address: string }> = ({ scope, address }) => {
const { t } = useTranslation()
@@ -75,6 +77,27 @@ export const TokenDetailsCard: FC<{ scope: SearchScope; address: string }> = ({
: t('common.valueInToken', { ...getPreciseNumberFormat(balance), ticker: tickerName })}
+ {t('tokens.totalSupply')}
+
+ {token.total_supply ? (
+
+ ) : (
+ t('common.not_defined')
+ )}
+
+ {t('tokens.holders')}
+
+
+ {t('tokens.holdersValue', { value: token?.num_holders })}
+
+
+
{token.num_transfers && (
<>
{t('tokens.transfers')}
diff --git a/src/app/pages/TokenDashboardPage/TokenInventoryCard.tsx b/src/app/pages/TokenDashboardPage/TokenInventoryCard.tsx
index f5303a66f..a5adfff07 100644
--- a/src/app/pages/TokenDashboardPage/TokenInventoryCard.tsx
+++ b/src/app/pages/TokenDashboardPage/TokenInventoryCard.tsx
@@ -17,29 +17,68 @@ import { TablePagination } from '../../components/Table/TablePagination'
import { useTokenInventory } from './hook'
import { ImageListItemImage } from './ImageListItemImage'
import { NFTInstanceLink } from './NFTLinks'
+import { CardHeaderWithCounter } from 'app/components/CardHeaderWithCounter'
+import { EvmNft } from 'oasis-nexus/api'
+import { To } from 'react-router-dom'
+import { SearchScope } from 'types/searchScope'
export const tokenInventoryContainerId = 'inventory'
export const TokenInventoryCard: FC = ({ scope, address }) => {
const { t } = useTranslation()
+ const { inventory, isFetched, pagination, totalCount } = useTokenInventory(scope, address)
return (
-
+
+ }
+ />
-
+
)
}
-const TokenInventoryView: FC = ({ scope, address }) => {
+type TokenInventoryViewProps = {
+ inventory: EvmNft[] | undefined
+ isFetched: boolean
+ totalCount: number | undefined
+ pagination: {
+ isTotalCountClipped: boolean | undefined
+ rowsPerPage: number
+ selectedPage: number
+ linkToPage: (pageNumber: number) => To
+ }
+ scope: SearchScope
+}
+
+const TokenInventoryView: FC = ({
+ inventory,
+ isFetched,
+ pagination,
+ scope,
+ totalCount,
+}) => {
const { t } = useTranslation()
- const { inventory, isFetched, pagination, totalCount } = useTokenInventory(scope, address)
return (
<>
diff --git a/src/app/pages/TokenDashboardPage/TokenSnapshot.tsx b/src/app/pages/TokenDashboardPage/TokenSnapshot.tsx
index bd471d3c1..347657cc7 100644
--- a/src/app/pages/TokenDashboardPage/TokenSnapshot.tsx
+++ b/src/app/pages/TokenDashboardPage/TokenSnapshot.tsx
@@ -16,7 +16,6 @@ import { SearchScope } from '../../../types/searchScope'
const StyledGrid = styled(Grid)(() => ({
display: 'flex',
}))
-
export const TokenSnapshot: FC<{ scope: SearchScope; address: string }> = ({ scope, address }) => {
const { t } = useTranslation()
@@ -29,10 +28,7 @@ export const TokenSnapshot: FC<{ scope: SearchScope; address: string }> = ({ sco
-
+
{t('tokenSnapshot.header')}
diff --git a/src/app/pages/TokenDashboardPage/TokenTitleCard.tsx b/src/app/pages/TokenDashboardPage/TokenTitleCard.tsx
index 368188a1e..06d30e6ab 100644
--- a/src/app/pages/TokenDashboardPage/TokenTitleCard.tsx
+++ b/src/app/pages/TokenDashboardPage/TokenTitleCard.tsx
@@ -8,19 +8,26 @@ import Skeleton from '@mui/material/Skeleton'
import { VerificationIcon } from '../../components/ContractVerificationIcon'
import { AccountLink } from '../../components/Account/AccountLink'
import Box from '@mui/material/Box'
+import { styled } from '@mui/material/styles'
import { CopyToClipboard } from '../../components/CopyToClipboard'
import { useTranslation } from 'react-i18next'
import { SearchScope } from '../../../types/searchScope'
+export const StyledCard = styled(Card)(() => ({
+ '&': {
+ paddingTop: '24px', // custom spacing
+ marginBottom: '50px',
+ },
+}))
+
const TitleSkeleton: FC = () =>
export const TokenTitleCard: FC<{ scope: SearchScope; address: string }> = ({ scope, address }) => {
const { t } = useTranslation()
-
const { isLoading, token } = useTokenInfo(scope, address)
return (
-
+
{isLoading ? (
@@ -32,32 +39,29 @@ export const TokenTitleCard: FC<{ scope: SearchScope; address: string }> = ({ sc
justifyContent: 'space-between',
alignItems: 'center',
}}
+ gap={3}
>
-
-
- {token?.name ?? t('common.missing')}
-
+ {token?.name ?? t('common.missing')}
- {token?.symbol ?? t('common.missing')}
+ ({token?.symbol ?? t('common.missing')})
-
+
{token && (
= ({ sc
)}
-
+
)
}
diff --git a/src/app/pages/TokenDashboardPage/hook.ts b/src/app/pages/TokenDashboardPage/hook.ts
index 4e2560b53..16ee0a3c2 100644
--- a/src/app/pages/TokenDashboardPage/hook.ts
+++ b/src/app/pages/TokenDashboardPage/hook.ts
@@ -7,6 +7,7 @@ import {
useGetRuntimeEvmTokensAddressHolders,
useGetRuntimeEvmTokensAddressNfts,
useGetRuntimeAccountsAddressNfts,
+ useGetRuntimeEvmTokensAddressNftsId,
} from '../../../oasis-nexus/api'
import { AppErrors } from '../../../types/errors'
import { SearchScope } from '../../../types/searchScope'
@@ -222,3 +223,29 @@ export const useAccountTokenInventory = (scope: SearchScope, address: string, to
totalCount,
}
}
+
+export const useNFTInstance = (scope: SearchScope, address: string, id: string) => {
+ const { network, layer } = scope
+ if (layer === Layer.consensus) {
+ throw AppErrors.UnsupportedLayer
+ // There are no tokens on the consensus layer.
+ }
+ const oasisAddress = useTransformToOasisAddress(address)
+ const query = useGetRuntimeEvmTokensAddressNftsId(network, layer, oasisAddress!, id, {
+ query: {
+ enabled: !!oasisAddress,
+ },
+ })
+
+ const { data, isError, isFetched, isLoading } = query
+ if (isError) {
+ throw AppErrors.InvalidAddress
+ }
+ const nft = data?.data
+
+ return {
+ isLoading,
+ isFetched,
+ nft,
+ }
+}
diff --git a/src/app/pages/TokenDashboardPage/index.tsx b/src/app/pages/TokenDashboardPage/index.tsx
index 8f043e08c..807789133 100644
--- a/src/app/pages/TokenDashboardPage/index.tsx
+++ b/src/app/pages/TokenDashboardPage/index.tsx
@@ -49,7 +49,7 @@ export const TokenDashboardPage: FC = () => {
- {!isMobile && }
+
{
tableView === TableLayout.Vertical &&
}
>
- {!isMobile && }
+ {!isMobile && }
}
diff --git a/src/app/utils/route-utils.ts b/src/app/utils/route-utils.ts
index 2f7b8cc64..dd38bcf08 100644
--- a/src/app/utils/route-utils.ts
+++ b/src/app/utils/route-utils.ts
@@ -85,6 +85,12 @@ export abstract class RouteUtils {
)}`
}
+ static getTokenHoldersRoute = ({ network, layer }: SearchScope, tokenAddress: string) => {
+ return `/${encodeURIComponent(network)}/${encodeURIComponent(layer)}/token/${encodeURIComponent(
+ tokenAddress,
+ )}/holders`
+ }
+
static getNFTInstanceRoute = (
{ network, layer }: SearchScope,
contractAddress: string,
diff --git a/src/locales/en/translation.json b/src/locales/en/translation.json
index 605d20867..01542ea1a 100644
--- a/src/locales/en/translation.json
+++ b/src/locales/en/translation.json
@@ -111,7 +111,7 @@
"copyButton": "Copy {{subject}}",
"creationByteCode": "Creation ByteCode",
"creator": "Creator",
- "createdAt": "at ",
+ "createdAt": "at",
"noCode": "There is no bytecode on record for this account. (Are you sure this is a contract?)",
"title": "Contract",
"runtimeByteCode": "Runtime ByteCode",
diff --git a/src/oasis-nexus/generated/api.ts b/src/oasis-nexus/generated/api.ts
index 5ce53efd9..dd32cb6df 100644
--- a/src/oasis-nexus/generated/api.ts
+++ b/src/oasis-nexus/generated/api.ts
@@ -659,6 +659,8 @@ Currently only ERC-721 is supported, where the document is an Asset Metadata fro
metadata_uri?: string;
/** Identifies the asset which this NFT represents */
name?: string;
+ /** The total number of transfers of this NFT instance. */
+ num_transfers?: number;
/** The Oasis address of this NFT instance's owner. */
owner?: string;
/** The Ethereum address of this NFT instance's owner. */
diff --git a/src/styles/theme/defaultTheme.ts b/src/styles/theme/defaultTheme.ts
index a5eff0156..496250307 100644
--- a/src/styles/theme/defaultTheme.ts
+++ b/src/styles/theme/defaultTheme.ts
@@ -530,11 +530,13 @@ export const defaultTheme = createTheme({
},
MuiImageList: {
styleOverrides: {
- root: {
- // default gridTemplateColumns is set by cols prop default number via inline styles
- // and cannot be overridden without !important statement
- gridTemplateColumns: `repeat(auto-fill, minmax(210px, 210px))!important`,
- },
+ root: ({ theme }) => ({
+ [theme.breakpoints.up('sm')]: {
+ // default gridTemplateColumns is set by cols prop default number via inline styles
+ // and cannot be overridden without !important statement
+ gridTemplateColumns: `repeat(auto-fill, minmax(210px, auto))!important`,
+ },
+ }),
},
},
MuiImageListItem: {
@@ -545,9 +547,10 @@ export const defaultTheme = createTheme({
borderColor: COLORS.brandExtraDark,
borderRadius: 8,
overflow: 'hidden',
- transition: 'box-shadow 250ms ease-in-out',
+ transition: 'border-color, box-shadow 250ms ease-in-out',
'&:hover, &:focus-visible': {
boxShadow: '0px 8px 8px 0px rgba(0, 0, 0, 0.15)',
+ borderColor: COLORS.brandDark,
},
},
},
@@ -597,7 +600,6 @@ export const defaultTheme = createTheme({
borderRadius: 5,
},
bar: {
- backgroundColor: COLORS.brandDark,
borderRadius: 5,
},
},
@@ -607,7 +609,9 @@ export const defaultTheme = createTheme({
root: ({ theme }) => ({
[theme.breakpoints.down('sm')]: {
paddingRight: theme.spacing(4),
+ // force scrollbar to cover the whole table horizontally
marginLeft: `-${theme.spacing(4)}`,
+ width: `calc(100% + ${theme.spacing(4)})`,
},
}),
},