Repository: oceanprotocol/market (ts)

Issue:
Round veOCEAN amounts
On the market homepage, at the bottom of each asset card, it is difficult to discern commas and decimal points in the veOCEAN value, especially with the grey colour.

For example, it is hard to tell that 403,832.438 veOCEAN is 403 thousand and not 403 million.

If this value was rounded (as is done in other places in the market), it would make comparing the values easier.

Pull request description:
special veOCEAN number treatment
* fixes #1769

Use a tweaked `formatNumber()` method for formatting all occurances of veOCEAN number in the UI, and remove the digits for better readability. This still keeps the formatting consistent with all our other numbers. Both, `formatNumber()` and our `PriceUnit` component now accept optional `decimals` param/prop.

This is not doing any rounding, as we only use this number for presentation and not doing any chain events with it so this should be fine.

Before:

<img width="428" alt="Screenshot 2022-11-09 at 11 23 01" src="https://user-images.githubusercontent.com/90316/200817641-4914341e-167b-4a97-ab46-0526f52090fb.png">

After:

<img width="417" alt="Screenshot 2022-11-09 at 11 23 14" src="https://user-images.githubusercontent.com/90316/200817677-040248e2-1661-40e1-9f24-c00938d7d673.png">


And on asset teasers:

Before:

<img width="405" alt="Screenshot 2022-11-09 at 12 19 12" src="https://user-images.githubusercontent.com/90316/200828689-c58d2c84-aa03-4ca3-b24f-a8d6f44903fe.png">

After:

<img width="388" alt="Screenshot 2022-11-09 at 12 18 22" src="https://user-images.githubusercontent.com/90316/200828746-ca3c6c6a-8e4d-4a3b-b423-a3464d4267e8.png">

In footer:

After:

<img width="461" alt="Screenshot 2022-11-09 at 13 02 05" src="https://user-images.githubusercontent.com/90316/200837324-1203452b-c314-44b2-a603-5ac470242a75.png">


### Bonus

There were a lot of duplicate declarations in `@utils/veAllocation`, meaning some import name which then is declared again as a `const` with same name. That really needs to be avoided as this will lead to all sorts of unintended consequences, like broken builds. We never catched this cause `@utils/veAllocation` was never part of any test run. Earlier iteration of this PR was touching that file and this had to be fixed then


Patch:
diff --git a/src/@utils/nft.ts b/src/@utils/nft.ts
index 2bbc3015..a4343801 100644
--- a/src/@utils/nft.ts
+++ b/src/@utils/nft.ts
@@ -82,10 +82,13 @@ export function generateNftCreateData(
 
 export function decodeTokenURI(tokenURI: string): NftMetadata {
   if (!tokenURI) return undefined
+
   try {
-    const nftMeta = JSON.parse(
-      Buffer.from(tokenURI.replace(tokenUriPrefix, ''), 'base64').toString()
-    ) as NftMetadata
+    const nftMeta = tokenURI.includes('data:application/json')
+      ? (JSON.parse(
+          Buffer.from(tokenURI.replace(tokenUriPrefix, ''), 'base64').toString()
+        ) as NftMetadata)
+      : ({ image: tokenURI } as NftMetadata)
 
     return nftMeta
   } catch (error) {
diff --git a/src/@utils/numbers.ts b/src/@utils/numbers.ts
index e7b4a432..aa82804b 100644
--- a/src/@utils/numbers.ts
+++ b/src/@utils/numbers.ts
@@ -1,5 +1,20 @@
+import { formatCurrency } from '@coingecko/cryptoformat'
 import { Decimal } from 'decimal.js'
 
+export function formatNumber(
+  price: number,
+  locale: string,
+  decimals?: string
+): string {
+  return formatCurrency(price, '', locale, false, {
+    // Not exactly clear what `significant figures` are for this library,
+    // but setting this seems to give us the formatting we want.
+    // See https://github.com/oceanprotocol/market/issues/70
+    significantFigures: 4,
+    ...(decimals && { decimalPlaces: Number(decimals) })
+  })
+}
+
 // Run decimal.js comparison
 // http://mikemcl.github.io/decimal.js/#cmp
 export function compareAsBN(balance: string, price: string): boolean {
diff --git a/src/@utils/veAllocation.ts b/src/@utils/veAllocation.ts
index bba0ea06..98ab7fc6 100644
--- a/src/@utils/veAllocation.ts
+++ b/src/@utils/veAllocation.ts
@@ -1,7 +1,7 @@
-import { AllLocked } from 'src/@types/subgraph/AllLocked'
-import { OwnAllocations } from 'src/@types/subgraph/OwnAllocations'
-import { NftOwnAllocation } from 'src/@types/subgraph/NftOwnAllocation'
-import { OceanLocked } from 'src/@types/subgraph/OceanLocked'
+import { AllLockedQuery } from 'src/@types/subgraph/AllLockedQuery'
+import { OwnAllocationsQuery } from 'src/@types/subgraph/OwnAllocationsQuery'
+import { NftOwnAllocationQuery } from 'src/@types/subgraph/NftOwnAllocationQuery'
+import { OceanLockedQuery } from 'src/@types/subgraph/OceanLockedQuery'
 import { gql, OperationResult } from 'urql'
 import { fetchData, getQueryContext } from './subgraph'
 import axios from 'axios'
@@ -12,11 +12,11 @@ import {
   NetworkType
 } from '@hooks/useNetworkMetadata'
 import { getAssetsFromNftList } from './aquarius'
-import { chainIdsSupported } from 'app.config'
+import { chainIdsSupported } from '../../app.config'
 import { Asset } from '@oceanprotocol/lib'
 
 const AllLocked = gql`
-  query AllLocked {
+  query AllLockedQuery {
     veOCEANs(first: 1000) {
       lockedAmount
     }
@@ -24,7 +24,7 @@ const AllLocked = gql`
 `
 
 const OwnAllocations = gql`
-  query OwnAllocations($address: String) {
+  query OwnAllocationsQuery($address: String) {
     veAllocations(where: { allocationUser: $address }) {
       id
       nftAddress
@@ -33,7 +33,7 @@ const OwnAllocations = gql`
   }
 `
 const NftOwnAllocation = gql`
-  query NftOwnAllocation($address: String, $nftAddress: String) {
+  query NftOwnAllocationQuery($address: String, $nftAddress: String) {
     veAllocations(
       where: { allocationUser: $address, nftAddress: $nftAddress }
     ) {
@@ -42,7 +42,7 @@ const NftOwnAllocation = gql`
   }
 `
 const OceanLocked = gql`
-  query OceanLocked($address: ID!) {
+  query OceanLockedQuery($address: ID!) {
     veOCEAN(id: $address) {
       id
       lockedAmount
@@ -87,7 +87,7 @@ export async function getNftOwnAllocation(
 ): Promise<number> {
   const veNetworkId = getVeChainNetworkId(networkId)
   const queryContext = getQueryContext(veNetworkId)
-  const fetchedAllocation: OperationResult<NftOwnAllocation, any> =
+  const fetchedAllocation: OperationResult<NftOwnAllocationQuery, any> =
     await fetchData(
       NftOwnAllocation,
       {
@@ -115,7 +115,7 @@ export async function getTotalAllocatedAndLocked(): Promise<TotalVe> {
     0
   )
 
-  const fetchedLocked: OperationResult<AllLocked, any> = await fetchData(
+  const fetchedLocked: OperationResult<AllLockedQuery, any> = await fetchData(
     AllLocked,
     null,
     queryContext
@@ -136,11 +136,12 @@ export async function getLocked(
   const veNetworkIds = getVeChainNetworkIds(networkIds)
   for (let i = 0; i < veNetworkIds.length; i++) {
     const queryContext = getQueryContext(veNetworkIds[i])
-    const fetchedLocked: OperationResult<OceanLocked, any> = await fetchData(
-      OceanLocked,
-      { address: userAddress.toLowerCase() },
-      queryContext
-    )
+    const fetchedLocked: OperationResult<OceanLockedQuery, any> =
+      await fetchData(
+        OceanLocked,
+        { address: userAddress.toLowerCase() },
+        queryContext
+      )
 
     fetchedLocked.data?.veOCEAN?.lockedAmount &&
       (total += Number(fetchedLocked.data?.veOCEAN?.lockedAmount))
@@ -157,7 +158,7 @@ export async function getOwnAllocations(
   const veNetworkIds = getVeChainNetworkIds(networkIds)
   for (let i = 0; i < veNetworkIds.length; i++) {
     const queryContext = getQueryContext(veNetworkIds[i])
-    const fetchedAllocations: OperationResult<OwnAllocations, any> =
+    const fetchedAllocations: OperationResult<OwnAllocationsQuery, any> =
       await fetchData(
         OwnAllocations,
         { address: userAddress.toLowerCase() },
diff --git a/src/components/@shared/AssetList/index.tsx b/src/components/@shared/AssetList/index.tsx
index f58a43c3..7624a61a 100644
--- a/src/components/@shared/AssetList/index.tsx
+++ b/src/components/@shared/AssetList/index.tsx
@@ -69,11 +69,13 @@ export default function AssetList({
 
   const styleClasses = `${styles.assetList} ${className || ''}`
 
-  return assetsWithPrices && !loading ? (
+  return loading ? (
+    <LoaderArea />
+  ) : (
     <>
       <div className={styleClasses}>
-        {assetsWithPrices.length > 0 ? (
-          assetsWithPrices.map((assetWithPrice) => (
+        {assetsWithPrices?.length > 0 ? (
+          assetsWithPrices?.map((assetWithPrice) => (
             <AssetTeaser
               asset={assetWithPrice}
               key={assetWithPrice.id}
@@ -95,7 +97,5 @@ export default function AssetList({
         />
       )}
     </>
-  ) : (
-    <LoaderArea />
   )
 }
diff --git a/src/components/@shared/AssetTeaser/index.module.css b/src/components/@shared/AssetTeaser/index.module.css
index c88f6f70..336f4f78 100644
--- a/src/components/@shared/AssetTeaser/index.module.css
+++ b/src/components/@shared/AssetTeaser/index.module.css
@@ -48,7 +48,7 @@
 }
 
 .footer {
-  margin-top: calc(var(--spacer) / 12);
+  margin-top: calc(var(--spacer) / 24);
 }
 
 .typeLabel {
diff --git a/src/components/@shared/AssetTeaser/index.tsx b/src/components/@shared/AssetTeaser/index.tsx
index 1b8b0d5e..f91b2c30 100644
--- a/src/components/@shared/AssetTeaser/index.tsx
+++ b/src/components/@shared/AssetTeaser/index.tsx
@@ -8,8 +8,8 @@ import AssetType from '@shared/AssetType'
 import NetworkName from '@shared/NetworkName'
 import styles from './index.module.css'
 import { getServiceByName } from '@utils/ddo'
-import { formatPrice } from '@shared/Price/PriceUnit'
 import { useUserPreferences } from '@context/UserPreferences'
+import { formatNumber } from '@utils/numbers'
 
 export declare type AssetTeaserProps = {
   asset: AssetExtended
@@ -77,23 +77,37 @@ export default function AssetTeaser({
           <footer className={styles.footer}>
             {allocated && allocated > 0 ? (
               <span className={styles.typeLabel}>
-                {allocated < 0
-                  ? ''
-                  : `${formatPrice(allocated, locale)} veOCEAN`}
+                {allocated < 0 ? (
+                  ''
+                ) : (
+                  <>
+                    <strong>{formatNumber(allocated, locale, '0')}</strong>{' '}
+                    veOCEAN
+                  </>
+                )}
               </span>
             ) : null}
             {orders && orders > 0 ? (
               <span className={styles.typeLabel}>
-                {orders < 0
-                  ? 'N/A'
-                  : `${orders} ${orders === 1 ? 'sale' : 'sales'}`}
+                {orders < 0 ? (
+                  'N/A'
+                ) : (
+                  <>
+                    <strong>{orders}</strong> {orders === 1 ? 'sale' : 'sales'}
+                  </>
+                )}
               </span>
             ) : null}
             {asset.views && asset.views > 0 ? (
               <span className={styles.typeLabel}>
-                {asset.views < 0
-                  ? 'N/A'
-                  : `${asset.views} ${asset.views === 1 ? 'view' : 'views'}`}
+                {asset.views < 0 ? (
+                  'N/A'
+                ) : (
+                  <>
+                    <strong>{asset.views}</strong>{' '}
+                    {asset.views === 1 ? 'view' : 'views'}
+                  </>
+                )}
               </span>
             ) : null}
           </footer>
diff --git a/src/components/@shared/Price/PriceUnit.tsx b/src/components/@shared/Price/PriceUnit.tsx
index 12a1ea7e..1ddd742a 100644
--- a/src/components/@shared/Price/PriceUnit.tsx
+++ b/src/components/@shared/Price/PriceUnit.tsx
@@ -1,17 +1,8 @@
 import React, { ReactElement } from 'react'
-import { formatCurrency } from '@coingecko/cryptoformat'
 import Conversion from './Conversion'
 import styles from './PriceUnit.module.css'
 import { useUserPreferences } from '@context/UserPreferences'
-
-export function formatPrice(price: number, locale: string): string {
-  return formatCurrency(price, '', locale, false, {
-    // Not exactly clear what `significant figures` are for this library,
-    // but setting this seems to give us the formatting we want.
-    // See https://github.com/oceanprotocol/market/issues/70
-    significantFigures: 4
-  })
-}
+import { formatNumber } from '@utils/numbers'
 
 export default function PriceUnit({
   price,
@@ -19,7 +10,8 @@ export default function PriceUnit({
   size = 'small',
   conversion,
   symbol,
-  type
+  type,
+  decimals
 }: {
   price: number
   type?: string
@@ -27,6 +19,7 @@ export default function PriceUnit({
   size?: 'small' | 'mini' | 'large'
   conversion?: boolean
   symbol?: string
+  decimals?: string
 }): ReactElement {
   const { locale } = useUserPreferences()
 
@@ -37,7 +30,7 @@ export default function PriceUnit({
       ) : (
         <>
           <div>
-            {Number.isNaN(price) ? '-' : formatPrice(price, locale)}{' '}
+            {Number.isNaN(price) ? '-' : formatNumber(price, locale, decimals)}{' '}
             <span className={styles.symbol}>{symbol}</span>
           </div>
           {conversion && <Conversion price={price} symbol={symbol} />}
diff --git a/src/components/@shared/atoms/Tags/index.tsx b/src/components/@shared/atoms/Tags/index.tsx
index 1f5c7094..2922417c 100644
--- a/src/components/@shared/atoms/Tags/index.tsx
+++ b/src/components/@shared/atoms/Tags/index.tsx
@@ -30,6 +30,9 @@ export default function Tags({
   className,
   noLinks
 }: TagsProps): ReactElement {
+  // safeguard against faults in the metadata
+  if (!(items instanceof Array)) return null
+
   max = max || items.length
   const remainder = items.length - max
   // filter out empty array items, and restrict to `max`
diff --git a/src/components/@shared/atoms/Tooltip/index.tsx b/src/components/@shared/atoms/Tooltip/index.tsx
index 76ee1fe7..7d5e9883 100644
--- a/src/components/@shared/atoms/Tooltip/index.tsx
+++ b/src/components/@shared/atoms/Tooltip/index.tsx
@@ -17,7 +17,8 @@ const DefaultTrigger = React.forwardRef((props, ref: any) => {
 })
 
 export default function Tooltip(props: TippyProps): ReactElement {
-  const { content, children, trigger, disabled, className, placement } = props
+  const { className, ...restProps } = props
+  const { content, children, trigger, disabled, placement } = props
   const [styles, api] = useSpring(() => animation.from)
 
   function onMount() {
@@ -60,7 +61,7 @@ export default function Tooltip(props: TippyProps): ReactElement {
       onMount={onMount}
       onHide={onHide}
       // animation
-      {...props}
+      {...restProps}
     >
       <div className={styleClasses}>{children || <DefaultTrigger />}</div>
     </Tippy>
diff --git a/src/components/Asset/AssetActions/AssetStats/index.tsx b/src/components/Asset/AssetActions/AssetStats/index.tsx
index a32dc2d3..4d02d035 100644
--- a/src/components/Asset/AssetActions/AssetStats/index.tsx
+++ b/src/components/Asset/AssetActions/AssetStats/index.tsx
@@ -2,7 +2,7 @@ import { useAsset } from '@context/Asset'
 import { useUserPreferences } from '@context/UserPreferences'
 import { useWeb3 } from '@context/Web3'
 import Tooltip from '@shared/atoms/Tooltip'
-import { formatPrice } from '@shared/Price/PriceUnit'
+import { formatNumber } from '@utils/numbers'
 import { getNftOwnAllocation } from '@utils/veAllocation'
 import React, { useEffect, useState } from 'react'
 import styles from './index.module.css'
@@ -33,8 +33,8 @@ export default function AssetStats() {
       {asset?.stats?.allocated && asset?.stats?.allocated > 0 ? (
         <span className={styles.stat}>
           <span className={styles.number}>
-            {formatPrice(asset.stats.allocated, locale)}
-          </span>
+            {formatNumber(asset.stats.allocated, locale, '0')}
+          </span>{' '}
           veOCEAN
         </span>
       ) : null}
diff --git a/src/components/Asset/AssetContent/Nft/NftTooltip.module.css b/src/components/Asset/AssetContent/Nft/NftTooltip.module.css
index 3916afc2..1b7b73c3 100644
--- a/src/components/Asset/AssetContent/Nft/NftTooltip.module.css
+++ b/src/components/Asset/AssetContent/Nft/NftTooltip.module.css
@@ -7,7 +7,7 @@
 .wrapper img {
   margin: 0;
   width: 128px;
-  height: 128px;
+  height: auto;
 }
 
 .info {
diff --git a/src/components/Asset/AssetContent/Nft/NftTooltip.tsx b/src/components/Asset/AssetContent/Nft/NftTooltip.tsx
index 3a13868b..9290bd9f 100644
--- a/src/components/Asset/AssetContent/Nft/NftTooltip.tsx
+++ b/src/components/Asset/AssetContent/Nft/NftTooltip.tsx
@@ -14,11 +14,13 @@ const openSeaTestNetworks = [4]
 
 export default function NftTooltip({
   nft,
+  nftImage,
   address,
   chainId,
   isBlockscoutExplorer
 }: {
   nft: NftMetadata
+  nftImage: string
   address: string
   chainId: number
   isBlockscoutExplorer: boolean
@@ -39,7 +41,7 @@ export default function NftTooltip({
 
   return (
     <div className={styles.wrapper}>
-      {nft && <img src={nft.image_data || nft.image} alt={nft?.name} />}
+      {nftImage && <img src={nftImage} alt={nft?.name} />}
       <div className={styles.info}>
         {nft && <h5>{nft.name}</h5>}
         {address && (
diff --git a/src/components/Asset/AssetContent/Nft/index.module.css b/src/components/Asset/AssetContent/Nft/index.module.css
index 4c6e3850..eb54b9cd 100644
--- a/src/components/Asset/AssetContent/Nft/index.module.css
+++ b/src/components/Asset/AssetContent/Nft/index.module.css
@@ -4,14 +4,19 @@
   border-right: 1px solid var(--border-color);
   width: calc(var(--spacer) * 2);
   height: calc(var(--spacer) * 2);
+  display: flex;
+  align-items: center;
 }
 
-.nftImage img,
 .nftImage > svg:first-of-type {
   width: 100%;
   height: 100%;
 }
 
+.nftImage img {
+  height: auto;
+}
+
 .nftImage > svg:first-of-type {
   transform: scale(0.7);
 }
diff --git a/src/components/Asset/AssetContent/Nft/index.tsx b/src/components/Asset/AssetContent/Nft/index.tsx
index f741c6ea..7f063bd7 100644
--- a/src/components/Asset/AssetContent/Nft/index.tsx
+++ b/src/components/Asset/AssetContent/Nft/index.tsx
@@ -48,6 +48,7 @@ export default function Nft({
           content={
             <NftTooltip
               nft={nftMetadata}
+              nftImage={nftImage}
               address={asset?.nftAddress}
               chainId={asset?.chainId}
               isBlockscoutExplorer={isBlockscoutExplorer}
diff --git a/src/components/Asset/RelatedAssets/index.tsx b/src/components/Asset/RelatedAssets/index.tsx
index f6cba742..9476e323 100644
--- a/src/components/Asset/RelatedAssets/index.tsx
+++ b/src/components/Asset/RelatedAssets/index.tsx
@@ -31,11 +31,17 @@ export default function RelatedAssets(): ReactElement {
       setIsLoading(true)
 
       try {
-        const tagQuery = generateBaseQuery(
-          generateQuery(chainIds, asset.nftAddress, 4, asset.metadata.tags)
-        )
-        const tagResults = (await queryMetadata(tagQuery, newCancelToken()))
-          ?.results
+        let tagResults: Asset[] = []
+
+        // safeguard against faults in the metadata
+        if (asset.metadata.tags instanceof Array) {
+          const tagQuery = generateBaseQuery(
+            generateQuery(chainIds, asset.nftAddress, 4, asset.metadata.tags)
+          )
+
+          tagResults = (await queryMetadata(tagQuery, newCancelToken()))
+            ?.results
+        }
 
         if (tagResults.length === 4) {
           setRelatedAssets(tagResults)
diff --git a/src/components/Footer/MarketStats/index.tsx b/src/components/Footer/MarketStats/index.tsx
index 19f4ab32..99caa292 100644
--- a/src/components/Footer/MarketStats/index.tsx
+++ b/src/components/Footer/MarketStats/index.tsx
@@ -124,8 +124,19 @@ export default function MarketStats(): ReactElement {
         />
       </div>
       <div>
-        <PriceUnit price={total.veLocked} symbol="OCEAN" size="small" /> locked.{' '}
-        <PriceUnit price={total.veAllocated} symbol="veOCEAN" size="small" />{' '}
+        <PriceUnit
+          decimals="0"
+          price={total.veLocked}
+          symbol="OCEAN"
+          size="small"
+        />{' '}
+        locked.{' '}
+        <PriceUnit
+          decimals="0"
+          price={total.veAllocated}
+          symbol="veOCEAN"
+          size="small"
+        />{' '}
         allocated.
       </div>
     </div>


Test patch:
diff --git a/src/components/@shared/atoms/Tags/index.test.tsx b/src/components/@shared/atoms/Tags/index.test.tsx
index 5867e96a..71b25331 100644
--- a/src/components/@shared/atoms/Tags/index.test.tsx
+++ b/src/components/@shared/atoms/Tags/index.test.tsx
@@ -24,4 +24,9 @@ describe('Tags', () => {
   it('renders WithoutLinks', () => {
     render(<Tags {...argsWithoutLinks} />)
   })
+
+  it('renders with faulty tags', () => {
+    // eslint-disable-next-line @typescript-eslint/no-explicit-any
+    render(<Tags items={'tags' as any} />)
+  })
 })
diff --git a/src/components/Home/MostViews/index.test.tsx b/src/components/Home/MostViews/index.test.tsx
index 8c61a378..dbdb91cb 100644
--- a/src/components/Home/MostViews/index.test.tsx
+++ b/src/components/Home/MostViews/index.test.tsx
@@ -32,7 +32,7 @@ describe('components/Home/MostViews', () => {
     )
     queryMetadataMock.mockResolvedValue(queryMetadataBaseReturn)
     render(<MostViews />)
-    await screen.findByText('666 views')
+    await screen.findByText('666')
   })
 
   it('catches errors', async () => {