1
0
mirror of https://github.com/oceanprotocol/market.git synced 2024-12-02 05:57:29 +01:00

Merge branch 'main' into fix/issue-1069-c2d-unsupported-networks

This commit is contained in:
EnzoVezzaro 2022-11-14 11:47:44 -04:00
commit 1649e075e2
36 changed files with 566 additions and 1340 deletions

View File

@ -5,7 +5,7 @@ export const assetAquarius: Asset = {
id: 'did:op:6654b0793765b269696cec8d2f0d077d9bbcdd3c4f033d941ab9684e8ad06630', id: 'did:op:6654b0793765b269696cec8d2f0d077d9bbcdd3c4f033d941ab9684e8ad06630',
nftAddress: '0xbA5BA7B09e2FA1eb0258f647503F81D2Af5cb07d', nftAddress: '0xbA5BA7B09e2FA1eb0258f647503F81D2Af5cb07d',
version: '4.1.0', version: '4.1.0',
chainId: 5, chainId: 1,
metadata: { metadata: {
created: '2022-09-29T11:30:26Z', created: '2022-09-29T11:30:26Z',
updated: '2022-09-29T11:30:26Z', updated: '2022-09-29T11:30:26Z',

1272
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -1,10 +1,12 @@
import { useRef, useEffect, useCallback } from 'react' import { useRef, useEffect, useCallback } from 'react'
import axios, { CancelToken } from 'axios' import axios, { CancelToken } from 'axios'
export const useCancelToken = (): (() => CancelToken) => { export const useCancelToken = (): (() => CancelToken) => {
const axiosSource = useRef(null) const axiosSource = useRef(null)
const newCancelToken = useCallback(() => { const newCancelToken = useCallback(() => {
axiosSource.current = axios.CancelToken.source() axiosSource.current = axios.CancelToken.source()
return axiosSource.current.token return axiosSource?.current?.token
}, []) }, [])
useEffect( useEffect(

4
src/@types/Analytics.d.ts vendored Normal file
View File

@ -0,0 +1,4 @@
interface PageViews {
count: number
did: string
}

View File

@ -5,5 +5,6 @@ import { Asset } from '@oceanprotocol/lib'
declare global { declare global {
interface AssetExtended extends Asset { interface AssetExtended extends Asset {
accessDetails?: AccessDetails accessDetails?: AccessDetails
views?: number
} }
} }

View File

@ -55,13 +55,13 @@ export function generateBaseQuery(
...(baseQueryParams.filters || []), ...(baseQueryParams.filters || []),
baseQueryParams.chainIds baseQueryParams.chainIds
? getFilterTerm('chainId', baseQueryParams.chainIds) ? getFilterTerm('chainId', baseQueryParams.chainIds)
: [], : '',
getFilterTerm('_index', 'aquarius'), getFilterTerm('_index', 'aquarius'),
...(baseQueryParams.ignorePurgatory ...(baseQueryParams.ignorePurgatory
? [] ? ''
: [getFilterTerm('purgatory.state', false)]), : [getFilterTerm('purgatory.state', false)]),
...(baseQueryParams.ignoreState ...(baseQueryParams.ignoreState
? [] ? ''
: [ : [
{ {
bool: { bool: {

View File

@ -1,3 +1,5 @@
import { Asset } from '@oceanprotocol/lib'
// Boolean value that will be true if we are inside a browser, false otherwise // Boolean value that will be true if we are inside a browser, false otherwise
export const isBrowser = typeof window !== 'undefined' export const isBrowser = typeof window !== 'undefined'
@ -14,3 +16,10 @@ export function removeItemFromArray<T>(arr: Array<T>, value: T): Array<T> {
} }
return arr return arr
} }
export function sortAssets(items: Asset[], sorted: string[]) {
items.sort(function (a, b) {
return sorted?.indexOf(a.id) - sorted?.indexOf(b.id)
})
return items
}

View File

@ -82,10 +82,13 @@ export function generateNftCreateData(
export function decodeTokenURI(tokenURI: string): NftMetadata { export function decodeTokenURI(tokenURI: string): NftMetadata {
if (!tokenURI) return undefined if (!tokenURI) return undefined
try { try {
const nftMeta = JSON.parse( const nftMeta = tokenURI.includes('data:application/json')
Buffer.from(tokenURI.replace(tokenUriPrefix, ''), 'base64').toString() ? (JSON.parse(
) as NftMetadata Buffer.from(tokenURI.replace(tokenUriPrefix, ''), 'base64').toString()
) as NftMetadata)
: ({ image: tokenURI } as NftMetadata)
return nftMeta return nftMeta
} catch (error) { } catch (error) {

View File

@ -1,5 +1,20 @@
import { formatCurrency } from '@coingecko/cryptoformat'
import { Decimal } from 'decimal.js' 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 // Run decimal.js comparison
// http://mikemcl.github.io/decimal.js/#cmp // http://mikemcl.github.io/decimal.js/#cmp
export function compareAsBN(balance: string, price: string): boolean { export function compareAsBN(balance: string, price: string): boolean {

View File

@ -8,7 +8,8 @@ import {
OrderParams, OrderParams,
ProviderComputeInitialize, ProviderComputeInitialize,
ProviderFees, ProviderFees,
ProviderInstance ProviderInstance,
ProviderInitialize
} from '@oceanprotocol/lib' } from '@oceanprotocol/lib'
import Web3 from 'web3' import Web3 from 'web3'
import { getOceanConfig } from './ocean' import { getOceanConfig } from './ocean'
@ -20,6 +21,26 @@ import {
} from '../../app.config' } from '../../app.config'
import { toast } from 'react-toastify' import { toast } from 'react-toastify'
async function initializeProvider(
asset: AssetExtended,
accountId: string,
providerFees?: ProviderFees
): Promise<ProviderInitialize> {
if (providerFees) return
try {
const provider = await ProviderInstance.initialize(
asset.id,
asset.services[0].id,
0,
accountId,
asset.services[0].serviceEndpoint
)
return provider
} catch (error) {
LoggerInstance.log('[Initialize Provider] Error:', error)
}
}
/** /**
* @param web3 * @param web3
* @param asset * @param asset
@ -40,15 +61,11 @@ export async function order(
const datatoken = new Datatoken(web3) const datatoken = new Datatoken(web3)
const config = getOceanConfig(asset.chainId) const config = getOceanConfig(asset.chainId)
const initializeData = const initializeData = await initializeProvider(
!providerFees && asset,
(await ProviderInstance.initialize( accountId,
asset.id, providerFees
asset.services[0].id, )
0,
accountId,
asset.services[0].serviceEndpoint
))
const orderParams = { const orderParams = {
consumer: computeConsumerAddress || accountId, consumer: computeConsumerAddress || accountId,
@ -130,15 +147,11 @@ export async function reuseOrder(
providerFees?: ProviderFees providerFees?: ProviderFees
): Promise<TransactionReceipt> { ): Promise<TransactionReceipt> {
const datatoken = new Datatoken(web3) const datatoken = new Datatoken(web3)
const initializeData = const initializeData = await initializeProvider(
!providerFees && asset,
(await ProviderInstance.initialize( accountId,
asset.id, providerFees
asset.services[0].id, )
0,
accountId,
asset.services[0].serviceEndpoint
))
const tx = await datatoken.reuseOrder( const tx = await datatoken.reuseOrder(
asset.accessDetails.datatoken.address, asset.accessDetails.datatoken.address,

View File

@ -1,7 +1,7 @@
import { AllLocked } from 'src/@types/subgraph/AllLocked' import { AllLockedQuery } from 'src/@types/subgraph/AllLockedQuery'
import { OwnAllocations } from 'src/@types/subgraph/OwnAllocations' import { OwnAllocationsQuery } from 'src/@types/subgraph/OwnAllocationsQuery'
import { NftOwnAllocation } from 'src/@types/subgraph/NftOwnAllocation' import { NftOwnAllocationQuery } from 'src/@types/subgraph/NftOwnAllocationQuery'
import { OceanLocked } from 'src/@types/subgraph/OceanLocked' import { OceanLockedQuery } from 'src/@types/subgraph/OceanLockedQuery'
import { gql, OperationResult } from 'urql' import { gql, OperationResult } from 'urql'
import { fetchData, getQueryContext } from './subgraph' import { fetchData, getQueryContext } from './subgraph'
import axios from 'axios' import axios from 'axios'
@ -12,11 +12,11 @@ import {
NetworkType NetworkType
} from '@hooks/useNetworkMetadata' } from '@hooks/useNetworkMetadata'
import { getAssetsFromNftList } from './aquarius' import { getAssetsFromNftList } from './aquarius'
import { chainIdsSupported } from 'app.config' import { chainIdsSupported } from '../../app.config'
import { Asset } from '@oceanprotocol/lib' import { Asset } from '@oceanprotocol/lib'
const AllLocked = gql` const AllLocked = gql`
query AllLocked { query AllLockedQuery {
veOCEANs(first: 1000) { veOCEANs(first: 1000) {
lockedAmount lockedAmount
} }
@ -24,7 +24,7 @@ const AllLocked = gql`
` `
const OwnAllocations = gql` const OwnAllocations = gql`
query OwnAllocations($address: String) { query OwnAllocationsQuery($address: String) {
veAllocations(where: { allocationUser: $address }) { veAllocations(where: { allocationUser: $address }) {
id id
nftAddress nftAddress
@ -33,7 +33,7 @@ const OwnAllocations = gql`
} }
` `
const NftOwnAllocation = gql` const NftOwnAllocation = gql`
query NftOwnAllocation($address: String, $nftAddress: String) { query NftOwnAllocationQuery($address: String, $nftAddress: String) {
veAllocations( veAllocations(
where: { allocationUser: $address, nftAddress: $nftAddress } where: { allocationUser: $address, nftAddress: $nftAddress }
) { ) {
@ -42,7 +42,7 @@ const NftOwnAllocation = gql`
} }
` `
const OceanLocked = gql` const OceanLocked = gql`
query OceanLocked($address: ID!) { query OceanLockedQuery($address: ID!) {
veOCEAN(id: $address) { veOCEAN(id: $address) {
id id
lockedAmount lockedAmount
@ -87,7 +87,7 @@ export async function getNftOwnAllocation(
): Promise<number> { ): Promise<number> {
const veNetworkId = getVeChainNetworkId(networkId) const veNetworkId = getVeChainNetworkId(networkId)
const queryContext = getQueryContext(veNetworkId) const queryContext = getQueryContext(veNetworkId)
const fetchedAllocation: OperationResult<NftOwnAllocation, any> = const fetchedAllocation: OperationResult<NftOwnAllocationQuery, any> =
await fetchData( await fetchData(
NftOwnAllocation, NftOwnAllocation,
{ {
@ -115,7 +115,7 @@ export async function getTotalAllocatedAndLocked(): Promise<TotalVe> {
0 0
) )
const fetchedLocked: OperationResult<AllLocked, any> = await fetchData( const fetchedLocked: OperationResult<AllLockedQuery, any> = await fetchData(
AllLocked, AllLocked,
null, null,
queryContext queryContext
@ -136,11 +136,12 @@ export async function getLocked(
const veNetworkIds = getVeChainNetworkIds(networkIds) const veNetworkIds = getVeChainNetworkIds(networkIds)
for (let i = 0; i < veNetworkIds.length; i++) { for (let i = 0; i < veNetworkIds.length; i++) {
const queryContext = getQueryContext(veNetworkIds[i]) const queryContext = getQueryContext(veNetworkIds[i])
const fetchedLocked: OperationResult<OceanLocked, any> = await fetchData( const fetchedLocked: OperationResult<OceanLockedQuery, any> =
OceanLocked, await fetchData(
{ address: userAddress.toLowerCase() }, OceanLocked,
queryContext { address: userAddress.toLowerCase() },
) queryContext
)
fetchedLocked.data?.veOCEAN?.lockedAmount && fetchedLocked.data?.veOCEAN?.lockedAmount &&
(total += Number(fetchedLocked.data?.veOCEAN?.lockedAmount)) (total += Number(fetchedLocked.data?.veOCEAN?.lockedAmount))
@ -157,7 +158,7 @@ export async function getOwnAllocations(
const veNetworkIds = getVeChainNetworkIds(networkIds) const veNetworkIds = getVeChainNetworkIds(networkIds)
for (let i = 0; i < veNetworkIds.length; i++) { for (let i = 0; i < veNetworkIds.length; i++) {
const queryContext = getQueryContext(veNetworkIds[i]) const queryContext = getQueryContext(veNetworkIds[i])
const fetchedAllocations: OperationResult<OwnAllocations, any> = const fetchedAllocations: OperationResult<OwnAllocationsQuery, any> =
await fetchData( await fetchData(
OwnAllocations, OwnAllocations,
{ address: userAddress.toLowerCase() }, { address: userAddress.toLowerCase() },

View File

@ -69,11 +69,13 @@ export default function AssetList({
const styleClasses = `${styles.assetList} ${className || ''}` const styleClasses = `${styles.assetList} ${className || ''}`
return assetsWithPrices && !loading ? ( return loading ? (
<LoaderArea />
) : (
<> <>
<div className={styleClasses}> <div className={styleClasses}>
{assetsWithPrices.length > 0 ? ( {assetsWithPrices?.length > 0 ? (
assetsWithPrices.map((assetWithPrice) => ( assetsWithPrices?.map((assetWithPrice) => (
<AssetTeaser <AssetTeaser
asset={assetWithPrice} asset={assetWithPrice}
key={assetWithPrice.id} key={assetWithPrice.id}
@ -95,7 +97,5 @@ export default function AssetList({
/> />
)} )}
</> </>
) : (
<LoaderArea />
) )
} }

View File

@ -48,7 +48,7 @@
} }
.footer { .footer {
margin-top: calc(var(--spacer) / 12); margin-top: calc(var(--spacer) / 24);
} }
.typeLabel { .typeLabel {

View File

@ -8,8 +8,8 @@ import AssetType from '@shared/AssetType'
import NetworkName from '@shared/NetworkName' import NetworkName from '@shared/NetworkName'
import styles from './index.module.css' import styles from './index.module.css'
import { getServiceByName } from '@utils/ddo' import { getServiceByName } from '@utils/ddo'
import { formatPrice } from '@shared/Price/PriceUnit'
import { useUserPreferences } from '@context/UserPreferences' import { useUserPreferences } from '@context/UserPreferences'
import { formatNumber } from '@utils/numbers'
export declare type AssetTeaserProps = { export declare type AssetTeaserProps = {
asset: AssetExtended asset: AssetExtended
@ -77,16 +77,37 @@ export default function AssetTeaser({
<footer className={styles.footer}> <footer className={styles.footer}>
{allocated && allocated > 0 ? ( {allocated && allocated > 0 ? (
<span className={styles.typeLabel}> <span className={styles.typeLabel}>
{allocated < 0 {allocated < 0 ? (
? '' ''
: `${formatPrice(allocated, locale)} veOCEAN`} ) : (
<>
<strong>{formatNumber(allocated, locale, '0')}</strong>{' '}
veOCEAN
</>
)}
</span> </span>
) : null} ) : null}
{orders && orders > 0 ? ( {orders && orders > 0 ? (
<span className={styles.typeLabel}> <span className={styles.typeLabel}>
{orders < 0 {orders < 0 ? (
? 'N/A' 'N/A'
: `${orders} ${orders === 1 ? 'sale' : 'sales'}`} ) : (
<>
<strong>{orders}</strong> {orders === 1 ? 'sale' : 'sales'}
</>
)}
</span>
) : null}
{asset.views && asset.views > 0 ? (
<span className={styles.typeLabel}>
{asset.views < 0 ? (
'N/A'
) : (
<>
<strong>{asset.views}</strong>{' '}
{asset.views === 1 ? 'view' : 'views'}
</>
)}
</span> </span>
) : null} ) : null}
</footer> </footer>

View File

@ -1,17 +1,8 @@
import React, { ReactElement } from 'react' import React, { ReactElement } from 'react'
import { formatCurrency } from '@coingecko/cryptoformat'
import Conversion from './Conversion' import Conversion from './Conversion'
import styles from './PriceUnit.module.css' import styles from './PriceUnit.module.css'
import { useUserPreferences } from '@context/UserPreferences' import { useUserPreferences } from '@context/UserPreferences'
import { formatNumber } from '@utils/numbers'
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
})
}
export default function PriceUnit({ export default function PriceUnit({
price, price,
@ -19,7 +10,8 @@ export default function PriceUnit({
size = 'small', size = 'small',
conversion, conversion,
symbol, symbol,
type type,
decimals
}: { }: {
price: number price: number
type?: string type?: string
@ -27,6 +19,7 @@ export default function PriceUnit({
size?: 'small' | 'mini' | 'large' size?: 'small' | 'mini' | 'large'
conversion?: boolean conversion?: boolean
symbol?: string symbol?: string
decimals?: string
}): ReactElement { }): ReactElement {
const { locale } = useUserPreferences() const { locale } = useUserPreferences()
@ -37,7 +30,7 @@ export default function PriceUnit({
) : ( ) : (
<> <>
<div> <div>
{Number.isNaN(price) ? '-' : formatPrice(price, locale)}{' '} {Number.isNaN(price) ? '-' : formatNumber(price, locale, decimals)}{' '}
<span className={styles.symbol}>{symbol}</span> <span className={styles.symbol}>{symbol}</span>
</div> </div>
{conversion && <Conversion price={price} symbol={symbol} />} {conversion && <Conversion price={price} symbol={symbol} />}

View File

@ -1,15 +1,16 @@
import React from 'react' import React from 'react'
import { render, screen } from '@testing-library/react' import { render, screen } from '@testing-library/react'
import * as axios from 'axios' import axios from 'axios'
import Publisher from './' import Publisher from './'
const account = '0x0000000000000000000000000000000000000000' const account = '0x0000000000000000000000000000000000000000'
jest.mock('axios') jest.mock('axios')
const axiosMock = axios as jest.Mocked<typeof axios>
describe('@shared/Publisher', () => { describe('@shared/Publisher', () => {
test('should return correct markup by default', async () => { test('should return correct markup by default', async () => {
;(axios as any).get.mockImplementationOnce(() => axiosMock.get.mockImplementationOnce(() =>
Promise.resolve({ data: { name: 'jellymcjellyfish.eth' } }) Promise.resolve({ data: { name: 'jellymcjellyfish.eth' } })
) )
@ -22,7 +23,7 @@ describe('@shared/Publisher', () => {
}) })
test('should truncate account by default', async () => { test('should truncate account by default', async () => {
;(axios as any).get.mockImplementationOnce(() => axiosMock.get.mockImplementationOnce(() =>
Promise.resolve({ data: { name: null } }) Promise.resolve({ data: { name: null } })
) )
@ -33,7 +34,7 @@ describe('@shared/Publisher', () => {
}) })
test('should return correct markup in minimal state', async () => { test('should return correct markup in minimal state', async () => {
;(axios as any).get.mockImplementationOnce(() => axiosMock.get.mockImplementationOnce(() =>
Promise.resolve({ data: { name: null } }) Promise.resolve({ data: { name: null } })
) )
@ -44,7 +45,7 @@ describe('@shared/Publisher', () => {
}) })
test('should return markup with empty account', async () => { test('should return markup with empty account', async () => {
;(axios as any).get.mockImplementationOnce(() => axiosMock.get.mockImplementationOnce(() =>
Promise.resolve({ data: { name: null } }) Promise.resolve({ data: { name: null } })
) )

View File

@ -24,4 +24,9 @@ describe('Tags', () => {
it('renders WithoutLinks', () => { it('renders WithoutLinks', () => {
render(<Tags {...argsWithoutLinks} />) render(<Tags {...argsWithoutLinks} />)
}) })
it('renders with faulty tags', () => {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
render(<Tags items={'tags' as any} />)
})
}) })

View File

@ -30,6 +30,9 @@ export default function Tags({
className, className,
noLinks noLinks
}: TagsProps): ReactElement { }: TagsProps): ReactElement {
// safeguard against faults in the metadata
if (!(items instanceof Array)) return null
max = max || items.length max = max || items.length
const remainder = items.length - max const remainder = items.length - max
// filter out empty array items, and restrict to `max` // filter out empty array items, and restrict to `max`

View File

@ -17,7 +17,8 @@ const DefaultTrigger = React.forwardRef((props, ref: any) => {
}) })
export default function Tooltip(props: TippyProps): ReactElement { 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) const [styles, api] = useSpring(() => animation.from)
function onMount() { function onMount() {
@ -60,7 +61,7 @@ export default function Tooltip(props: TippyProps): ReactElement {
onMount={onMount} onMount={onMount}
onHide={onHide} onHide={onHide}
// animation // animation
{...props} {...restProps}
> >
<div className={styleClasses}>{children || <DefaultTrigger />}</div> <div className={styleClasses}>{children || <DefaultTrigger />}</div>
</Tippy> </Tippy>

View File

@ -2,7 +2,7 @@ import { useAsset } from '@context/Asset'
import { useUserPreferences } from '@context/UserPreferences' import { useUserPreferences } from '@context/UserPreferences'
import { useWeb3 } from '@context/Web3' import { useWeb3 } from '@context/Web3'
import Tooltip from '@shared/atoms/Tooltip' import Tooltip from '@shared/atoms/Tooltip'
import { formatPrice } from '@shared/Price/PriceUnit' import { formatNumber } from '@utils/numbers'
import { getNftOwnAllocation } from '@utils/veAllocation' import { getNftOwnAllocation } from '@utils/veAllocation'
import React, { useEffect, useState } from 'react' import React, { useEffect, useState } from 'react'
import styles from './index.module.css' import styles from './index.module.css'
@ -33,8 +33,8 @@ export default function AssetStats() {
{asset?.stats?.allocated && asset?.stats?.allocated > 0 ? ( {asset?.stats?.allocated && asset?.stats?.allocated > 0 ? (
<span className={styles.stat}> <span className={styles.stat}>
<span className={styles.number}> <span className={styles.number}>
{formatPrice(asset.stats.allocated, locale)} {formatNumber(asset.stats.allocated, locale, '0')}
</span> </span>{' '}
veOCEAN veOCEAN
</span> </span>
) : null} ) : null}

View File

@ -0,0 +1,143 @@
import React from 'react'
import { render, screen } from '@testing-library/react'
import ButtonBuy, { ButtonBuyProps } from './'
const downloadProps: ButtonBuyProps = {
action: 'download',
disabled: false,
hasPreviousOrder: false,
hasDatatoken: false,
btSymbol: 'btSymbol',
dtSymbol: 'dtSymbol',
dtBalance: '100000000000',
assetTimeout: '1 day',
assetType: 'Dataset',
stepText: 'TEST',
priceType: 'fixed',
isConsumable: true,
isBalanceSufficient: true,
consumableFeedback: 'TEST: consumableFeedback'
}
const computeProps: ButtonBuyProps = {
action: 'compute',
disabled: false,
hasPreviousOrder: false,
hasDatatoken: true,
btSymbol: 'btSymbol',
dtSymbol: 'dtSymbol',
dtBalance: '100000000000',
assetTimeout: '1 day',
assetType: 'algorithm',
hasPreviousOrderSelectedComputeAsset: false,
hasDatatokenSelectedComputeAsset: true,
dtSymbolSelectedComputeAsset: 'dtSymbol',
dtBalanceSelectedComputeAsset: 'dtBalance',
selectedComputeAssetType: 'selectedComputeAssetType',
stepText: ' ',
isLoading: false,
type: 'submit',
priceType: 'fixed',
algorithmPriceType: 'free',
isBalanceSufficient: true,
isConsumable: true,
consumableFeedback: 'consumableFeedback',
isAlgorithmConsumable: true,
hasProviderFee: false,
retry: false
}
describe('Asset/AssetActions/ButtonBuy', () => {
// TESTS FOR LOADING
it('Renders Buy button without crashing', () => {
render(<ButtonBuy {...downloadProps} isLoading />)
const button = screen.getByText('TEST')
expect(button).toContainHTML('<Loader')
})
// TESTS FOR DOWNLOAD
it('Renders Buy button without crashing', () => {
render(<ButtonBuy {...downloadProps} />)
const button = screen.getByText('Buy for 1 day')
expect(button).toContainHTML('<button')
})
it('Renders Buy button without crashing when hasPreviousOrder=true', () => {
render(<ButtonBuy {...downloadProps} hasPreviousOrder />)
const button = screen.getByText('Download')
expect(button).toContainHTML('<button')
})
it('Renders retry button for download without crashing', () => {
render(<ButtonBuy {...downloadProps} retry />)
const button = screen.getByText('Retry')
expect(button).toContainHTML('<button')
})
it('Renders get button for free download without crashing', () => {
render(<ButtonBuy {...downloadProps} priceType="free" hasPreviousOrder />)
const button = screen.getByText('Download')
expect(button).toContainHTML('<button')
})
it('Renders "Get" button for free assets without crashing', () => {
render(<ButtonBuy {...downloadProps} priceType="free" />)
const button = screen.getByText('Get')
expect(button).toContainHTML('<button')
})
it('Renders Buy button without crashing', () => {
render(
<ButtonBuy
{...downloadProps}
assetTimeout="Forever"
isConsumable={false}
/>
)
const button = screen.getByText('Buy')
expect(button).toContainHTML('<button')
})
// TESTS FOR COMPUTE
it('Renders "Buy Compute Job" button for compute without crashing', () => {
render(<ButtonBuy {...computeProps} />)
const button = screen.getByText('Buy Compute Job')
expect(button).toContainHTML('<button')
})
it('Renders "Buy Compute Job" button for compute without crashing', () => {
render(<ButtonBuy {...computeProps} hasDatatokenSelectedComputeAsset />)
const button = screen.getByText('Buy Compute Job')
expect(button).toContainHTML('<button')
})
it('Renders "Start Compute Job" button', () => {
render(
<ButtonBuy
{...computeProps}
hasPreviousOrder
hasPreviousOrderSelectedComputeAsset
/>
)
const button = screen.getByText('Start Compute Job')
expect(button).toContainHTML('<button')
})
it('Renders "Order Compute Job" button', () => {
render(<ButtonBuy {...computeProps} priceType="free" hasProviderFee />)
const button = screen.getByText('Order Compute Job')
expect(button).toContainHTML('<button')
})
it('Renders "Order Compute Job" button', () => {
render(<ButtonBuy {...computeProps} priceType="free" hasProviderFee />)
const button = screen.getByText('Order Compute Job')
expect(button).toContainHTML('<button')
})
it('Renders "retry" button for compute without crashing', () => {
render(<ButtonBuy {...computeProps} retry />)
const button = screen.getByText('Retry')
expect(button).toContainHTML('<button')
})
})

View File

@ -5,7 +5,7 @@ import Loader from '../../../@shared/atoms/Loader'
import { useWeb3 } from '@context/Web3' import { useWeb3 } from '@context/Web3'
import Web3 from 'web3' import Web3 from 'web3'
interface ButtonBuyProps { export interface ButtonBuyProps {
action: 'download' | 'compute' action: 'download' | 'compute'
disabled: boolean disabled: boolean
hasPreviousOrder: boolean hasPreviousOrder: boolean
@ -32,6 +32,7 @@ interface ButtonBuyProps {
isAlgorithmConsumable?: boolean isAlgorithmConsumable?: boolean
isSupportedOceanNetwork?: boolean isSupportedOceanNetwork?: boolean
hasProviderFee?: boolean hasProviderFee?: boolean
retry?: boolean
} }
function getConsumeHelpText( function getConsumeHelpText(
@ -168,24 +169,26 @@ export default function ButtonBuy({
priceType, priceType,
algorithmPriceType, algorithmPriceType,
isAlgorithmConsumable, isAlgorithmConsumable,
isSupportedOceanNetwork, hasProviderFee,
hasProviderFee retry,
isSupportedOceanNetwork
}: ButtonBuyProps): ReactElement { }: ButtonBuyProps): ReactElement {
const { web3 } = useWeb3() const { web3 } = useWeb3()
const buttonText = const buttonText = retry
action === 'download' ? 'Retry'
? hasPreviousOrder : action === 'download'
? 'Download' ? hasPreviousOrder
: priceType === 'free' ? 'Download'
? 'Get' : priceType === 'free'
: `Buy ${assetTimeout === 'Forever' ? '' : ` for ${assetTimeout}`}` ? 'Get'
: hasPreviousOrder && : `Buy ${assetTimeout === 'Forever' ? '' : ` for ${assetTimeout}`}`
hasPreviousOrderSelectedComputeAsset && : hasPreviousOrder &&
!hasProviderFee hasPreviousOrderSelectedComputeAsset &&
? 'Start Compute Job' !hasProviderFee
: priceType === 'free' && algorithmPriceType === 'free' ? 'Start Compute Job'
? 'Order Compute Job' : priceType === 'free' && algorithmPriceType === 'free'
: `Buy Compute Job` ? 'Order Compute Job'
: `Buy Compute Job`
return ( return (
<div className={styles.actions}> <div className={styles.actions}>

View File

@ -43,7 +43,8 @@ export default function FormStartCompute({
datasetOrderPriceAndFees, datasetOrderPriceAndFees,
algoOrderPriceAndFees, algoOrderPriceAndFees,
providerFeeAmount, providerFeeAmount,
validUntil validUntil,
retry
}: { }: {
algorithms: AssetSelectionAsset[] algorithms: AssetSelectionAsset[]
ddoListAlgorithms: Asset[] ddoListAlgorithms: Asset[]
@ -71,6 +72,7 @@ export default function FormStartCompute({
algoOrderPriceAndFees?: OrderPriceAndFees algoOrderPriceAndFees?: OrderPriceAndFees
providerFeeAmount?: string providerFeeAmount?: string
validUntil?: string validUntil?: string
retry: boolean
}): ReactElement { }): ReactElement {
const { siteContent } = useMarketMetadata() const { siteContent } = useMarketMetadata()
const { accountId, balance, isSupportedOceanNetwork } = useWeb3() const { accountId, balance, isSupportedOceanNetwork } = useWeb3()
@ -301,6 +303,7 @@ export default function FormStartCompute({
} }
isSupportedOceanNetwork={isSupportedOceanNetwork} isSupportedOceanNetwork={isSupportedOceanNetwork}
hasProviderFee={providerFeeAmount && providerFeeAmount !== '0'} hasProviderFee={providerFeeAmount && providerFeeAmount !== '0'}
retry={retry}
/> />
</Form> </Form>
) )

View File

@ -100,6 +100,7 @@ export default function Compute({
const [refetchJobs, setRefetchJobs] = useState(false) const [refetchJobs, setRefetchJobs] = useState(false)
const [isLoadingJobs, setIsLoadingJobs] = useState(false) const [isLoadingJobs, setIsLoadingJobs] = useState(false)
const [jobs, setJobs] = useState<ComputeJobMetaData[]>([]) const [jobs, setJobs] = useState<ComputeJobMetaData[]>([])
const [retry, setRetry] = useState<boolean>(false)
const hasDatatoken = Number(dtBalance) >= 1 const hasDatatoken = Number(dtBalance) >= 1
const isComputeButtonDisabled = const isComputeButtonDisabled =
@ -299,7 +300,8 @@ export default function Compute({
useEffect(() => { useEffect(() => {
const newError = error const newError = error
if (!newError) return if (!newError) return
toast.error(newError) const errorMsg = newError + '. Please retry.'
toast.error(errorMsg)
}, [error]) }, [error])
async function startJob(): Promise<void> { async function startJob(): Promise<void> {
@ -395,6 +397,7 @@ export default function Compute({
initPriceAndFees() initPriceAndFees()
} catch (error) { } catch (error) {
setError(error.message) setError(error.message)
setRetry(true)
LoggerInstance.error(`[compute] ${error.message} `) LoggerInstance.error(`[compute] ${error.message} `)
} finally { } finally {
setIsOrdering(false) setIsOrdering(false)
@ -484,6 +487,7 @@ export default function Compute({
algoOrderPriceAndFees={algoOrderPriceAndFees} algoOrderPriceAndFees={algoOrderPriceAndFees}
providerFeeAmount={providerFeeAmount} providerFeeAmount={providerFeeAmount}
validUntil={computeValidUntil} validUntil={computeValidUntil}
retry={retry}
/> />
</Formik> </Formik>
)} )}

View File

@ -48,6 +48,7 @@ export default function Download({
const [isOrderDisabled, setIsOrderDisabled] = useState(false) const [isOrderDisabled, setIsOrderDisabled] = useState(false)
const [orderPriceAndFees, setOrderPriceAndFees] = const [orderPriceAndFees, setOrderPriceAndFees] =
useState<OrderPriceAndFees>() useState<OrderPriceAndFees>()
const [retry, setRetry] = useState<boolean>(false)
const isUnsupportedPricing = asset?.accessDetails?.type === 'NOT_SUPPORTED' const isUnsupportedPricing = asset?.accessDetails?.type === 'NOT_SUPPORTED'
@ -155,9 +156,10 @@ export default function Download({
} }
} catch (error) { } catch (error) {
LoggerInstance.error(error) LoggerInstance.error(error)
setRetry(true)
const message = isOwned const message = isOwned
? 'Failed to download file!' ? 'Failed to download file!'
: 'An error occurred. Check console for more information.' : 'An error occurred, please retry. Check console for more information.'
toast.error(message) toast.error(message)
} }
setIsLoading(false) setIsLoading(false)
@ -181,6 +183,7 @@ export default function Download({
isConsumable={asset.accessDetails?.isPurchasable} isConsumable={asset.accessDetails?.isPurchasable}
isBalanceSufficient={isBalanceSufficient} isBalanceSufficient={isBalanceSufficient}
consumableFeedback={consumableFeedback} consumableFeedback={consumableFeedback}
retry={retry}
isSupportedOceanNetwork={isSupportedOceanNetwork} isSupportedOceanNetwork={isSupportedOceanNetwork}
/> />
) )

View File

@ -7,7 +7,7 @@
.wrapper img { .wrapper img {
margin: 0; margin: 0;
width: 128px; width: 128px;
height: 128px; height: auto;
} }
.info { .info {

View File

@ -14,11 +14,13 @@ const openSeaTestNetworks = [4]
export default function NftTooltip({ export default function NftTooltip({
nft, nft,
nftImage,
address, address,
chainId, chainId,
isBlockscoutExplorer isBlockscoutExplorer
}: { }: {
nft: NftMetadata nft: NftMetadata
nftImage: string
address: string address: string
chainId: number chainId: number
isBlockscoutExplorer: boolean isBlockscoutExplorer: boolean
@ -39,7 +41,7 @@ export default function NftTooltip({
return ( return (
<div className={styles.wrapper}> <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}> <div className={styles.info}>
{nft && <h5>{nft.name}</h5>} {nft && <h5>{nft.name}</h5>}
{address && ( {address && (

View File

@ -4,14 +4,19 @@
border-right: 1px solid var(--border-color); border-right: 1px solid var(--border-color);
width: calc(var(--spacer) * 2); width: calc(var(--spacer) * 2);
height: calc(var(--spacer) * 2); height: calc(var(--spacer) * 2);
display: flex;
align-items: center;
} }
.nftImage img,
.nftImage > svg:first-of-type { .nftImage > svg:first-of-type {
width: 100%; width: 100%;
height: 100%; height: 100%;
} }
.nftImage img {
height: auto;
}
.nftImage > svg:first-of-type { .nftImage > svg:first-of-type {
transform: scale(0.7); transform: scale(0.7);
} }

View File

@ -48,6 +48,7 @@ export default function Nft({
content={ content={
<NftTooltip <NftTooltip
nft={nftMetadata} nft={nftMetadata}
nftImage={nftImage}
address={asset?.nftAddress} address={asset?.nftAddress}
chainId={asset?.chainId} chainId={asset?.chainId}
isBlockscoutExplorer={isBlockscoutExplorer} isBlockscoutExplorer={isBlockscoutExplorer}

View File

@ -23,7 +23,6 @@ export default function RelatedAssets(): ReactElement {
!asset?.nft || !asset?.nft ||
!asset?.metadata !asset?.metadata
) { ) {
setIsLoading(false)
return return
} }
@ -31,11 +30,17 @@ export default function RelatedAssets(): ReactElement {
setIsLoading(true) setIsLoading(true)
try { try {
const tagQuery = generateBaseQuery( let tagResults: Asset[] = []
generateQuery(chainIds, asset.nftAddress, 4, asset.metadata.tags)
) // safeguard against faults in the metadata
const tagResults = (await queryMetadata(tagQuery, newCancelToken())) if (asset.metadata.tags instanceof Array) {
?.results const tagQuery = generateBaseQuery(
generateQuery(chainIds, asset.nftAddress, 4, asset.metadata.tags)
)
tagResults = (await queryMetadata(tagQuery, newCancelToken()))
?.results
}
if (tagResults.length === 4) { if (tagResults.length === 4) {
setRelatedAssets(tagResults) setRelatedAssets(tagResults)

View File

@ -124,8 +124,19 @@ export default function MarketStats(): ReactElement {
/> />
</div> </div>
<div> <div>
<PriceUnit price={total.veLocked} symbol="OCEAN" size="small" /> locked.{' '} <PriceUnit
<PriceUnit price={total.veAllocated} symbol="veOCEAN" size="small" />{' '} decimals="0"
price={total.veLocked}
symbol="OCEAN"
size="small"
/>{' '}
locked.{' '}
<PriceUnit
decimals="0"
price={total.veAllocated}
symbol="veOCEAN"
size="small"
/>{' '}
allocated. allocated.
</div> </div>
</div> </div>

View File

@ -0,0 +1,56 @@
import { render, screen } from '@testing-library/react'
import React from 'react'
import MostViews from '.'
import axios from 'axios'
import { queryMetadata } from '@utils/aquarius'
import { assetAquarius } from '../../../../.jest/__fixtures__/assetAquarius'
jest.mock('axios')
jest.mock('@utils/aquarius')
const axiosMock = axios as jest.Mocked<typeof axios>
const queryMetadataMock = queryMetadata as jest.Mock
const queryMetadataBaseReturn: PagedAssets = {
results: [assetAquarius],
page: 1,
totalPages: 1,
totalResults: 1,
aggregations: {}
}
describe('components/Home/MostViews', () => {
beforeEach(() => {
jest.resetAllMocks()
})
it('renders without crashing', async () => {
axiosMock.get.mockImplementation(() =>
Promise.resolve({
data: [{ count: 666, did: assetAquarius.id }]
})
)
queryMetadataMock.mockResolvedValue(queryMetadataBaseReturn)
render(<MostViews />)
await screen.findByText('666')
})
it('catches errors', async () => {
queryMetadataMock.mockImplementation(() => {
throw new Error('Hello error')
})
// prevent console error from showing up in test log
const originalError = console.error
console.error = jest.fn()
try {
render(<MostViews />)
await screen.findByText('No results found')
} catch (error) {
expect(error).toEqual({ message: 'Hello error' })
}
console.error = originalError
})
})

View File

@ -0,0 +1,73 @@
import React, { ReactElement, useCallback, useEffect, useState } from 'react'
import styles from '../index.module.css'
import {
generateBaseQuery,
getFilterTerm,
queryMetadata
} from '@utils/aquarius'
import { useCancelToken } from '@hooks/useCancelToken'
import Tooltip from '@shared/atoms/Tooltip'
import AssetList from '@shared/AssetList'
import { LoggerInstance } from '@oceanprotocol/lib'
import { sortAssets } from '@utils/index'
import axios, { AxiosResponse } from 'axios'
export default function MostViews(): ReactElement {
const [loading, setLoading] = useState<boolean>()
const [mostViewed, setMostViewed] = useState<AssetExtended[]>([])
const newCancelToken = useCancelToken()
const getMostViewed = useCallback(async () => {
try {
setLoading(true)
const response: AxiosResponse<PageViews[]> = await axios.get(
'https://market-analytics.oceanprotocol.com/pages?limit=6',
{ cancelToken: newCancelToken() }
)
const dids = response?.data?.map((x: PageViews) => x.did)
const assetsWithViews: AssetExtended[] = []
const baseParams = {
esPaginationOptions: { size: 6 },
filters: [getFilterTerm('_id', dids)]
} as BaseQueryParams
const query = generateBaseQuery(baseParams)
const result = await queryMetadata(query, newCancelToken())
if (result?.totalResults > 0) {
const sortedAssets = sortAssets(result.results, dids)
const overflow = sortedAssets.length - 6
sortedAssets.splice(sortedAssets.length - overflow, overflow)
sortedAssets.forEach((asset) => {
assetsWithViews.push({
...asset,
views: response.data.filter((x) => x.did === asset.id)?.[0]?.count
})
})
setMostViewed(assetsWithViews)
}
} catch (error) {
LoggerInstance.error(error.message)
} finally {
setLoading(false)
}
}, [newCancelToken])
useEffect(() => {
getMostViewed()
}, [getMostViewed])
return (
<section className={styles.section}>
<h3>
Most Views <span>last 30 days</span>
<Tooltip content="Assets from all supported chains. Not affected by your selected networks." />
</h3>
<AssetList
assets={mostViewed}
showPagination={false}
isLoading={loading}
/>
</section>
)
}

View File

@ -1,29 +1,27 @@
import { useUserPreferences } from '@context/UserPreferences' import { useUserPreferences } from '@context/UserPreferences'
import { useCancelToken } from '@hooks/useCancelToken' import { useCancelToken } from '@hooks/useCancelToken'
import { useIsMounted } from '@hooks/useIsMounted' import { useIsMounted } from '@hooks/useIsMounted'
import { Asset, LoggerInstance } from '@oceanprotocol/lib' import { LoggerInstance } from '@oceanprotocol/lib'
import AssetList from '@shared/AssetList' import AssetList from '@shared/AssetList'
import Tooltip from '@shared/atoms/Tooltip'
import Markdown from '@shared/Markdown'
import { queryMetadata } from '@utils/aquarius' import { queryMetadata } from '@utils/aquarius'
import { sortAssets } from '@utils/index'
import React, { ReactElement, useState, useEffect } from 'react' import React, { ReactElement, useState, useEffect } from 'react'
import styles from './index.module.css' import styles from './index.module.css'
function sortElements(items: Asset[], sorted: string[]) {
items.sort(function (a, b) {
return sorted.indexOf(a.nftAddress) - sorted.indexOf(b.nftAddress)
})
return items
}
export default function SectionQueryResult({ export default function SectionQueryResult({
title, title,
query, query,
action, action,
queryData queryData,
tooltip
}: { }: {
title: ReactElement | string title: ReactElement | string
query: SearchQuery query: SearchQuery
action?: ReactElement action?: ReactElement
queryData?: string[] queryData?: string[]
tooltip?: string
}): ReactElement { }): ReactElement {
const { chainIds } = useUserPreferences() const { chainIds } = useUserPreferences()
const [result, setResult] = useState<PagedAssets>() const [result, setResult] = useState<PagedAssets>()
@ -52,7 +50,7 @@ export default function SectionQueryResult({
const result = await queryMetadata(query, newCancelToken()) const result = await queryMetadata(query, newCancelToken())
if (!isMounted()) return if (!isMounted()) return
if (queryData && result?.totalResults > 0) { if (queryData && result?.totalResults > 0) {
const sortedAssets = sortElements(result.results, queryData) const sortedAssets = sortAssets(result.results, queryData)
const overflow = sortedAssets.length - 6 const overflow = sortedAssets.length - 6
sortedAssets.splice(sortedAssets.length - overflow, overflow) sortedAssets.splice(sortedAssets.length - overflow, overflow)
result.results = sortedAssets result.results = sortedAssets
@ -69,7 +67,9 @@ export default function SectionQueryResult({
return ( return (
<section className={styles.section}> <section className={styles.section}>
<h3>{title}</h3> <h3>
{title} {tooltip && <Tooltip content={<Markdown text={tooltip} />} />}
</h3>
<AssetList <AssetList
assets={result?.results} assets={result?.results}

View File

@ -13,6 +13,13 @@
color: var(--color-secondary); color: var(--color-secondary);
} }
.section h3 span {
font-size: var(--font-size-small);
color: var(--color-secondary);
font-family: var(--font-family-base);
font-weight: var(--font-weight-base);
}
.section [class*='button'] { .section [class*='button'] {
margin-top: var(--spacer); margin-top: var(--spacer);
} }

View File

@ -9,12 +9,14 @@ import TopTags from './TopTags'
import SectionQueryResult from './SectionQueryResult' import SectionQueryResult from './SectionQueryResult'
import styles from './index.module.css' import styles from './index.module.css'
import Allocations from './Allocations' import Allocations from './Allocations'
import MostViews from './MostViews'
export default function HomePage(): ReactElement { export default function HomePage(): ReactElement {
const { chainIds } = useUserPreferences() const { chainIds } = useUserPreferences()
const [queryLatest, setQueryLatest] = useState<SearchQuery>() const [queryLatest, setQueryLatest] = useState<SearchQuery>()
const [queryMostSales, setQueryMostSales] = useState<SearchQuery>() const [queryMostSales, setQueryMostSales] = useState<SearchQuery>()
const [queryMostAllocation, setQueryMostAllocation] = useState<SearchQuery>() const [queryMostAllocation, setQueryMostAllocation] = useState<SearchQuery>()
useEffect(() => { useEffect(() => {
@ -66,7 +68,7 @@ export default function HomePage(): ReactElement {
/> />
<SectionQueryResult title="Most Sales" query={queryMostSales} /> <SectionQueryResult title="Most Sales" query={queryMostSales} />
<MostViews />
<TopSales title="Publishers With Most Sales" /> <TopSales title="Publishers With Most Sales" />
<TopTags title="Top Tags By Sales" /> <TopTags title="Top Tags By Sales" />