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:
commit
1649e075e2
@ -5,7 +5,7 @@ export const assetAquarius: Asset = {
|
||||
id: 'did:op:6654b0793765b269696cec8d2f0d077d9bbcdd3c4f033d941ab9684e8ad06630',
|
||||
nftAddress: '0xbA5BA7B09e2FA1eb0258f647503F81D2Af5cb07d',
|
||||
version: '4.1.0',
|
||||
chainId: 5,
|
||||
chainId: 1,
|
||||
metadata: {
|
||||
created: '2022-09-29T11:30:26Z',
|
||||
updated: '2022-09-29T11:30:26Z',
|
||||
|
1272
package-lock.json
generated
1272
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@ -1,10 +1,12 @@
|
||||
import { useRef, useEffect, useCallback } from 'react'
|
||||
import axios, { CancelToken } from 'axios'
|
||||
|
||||
export const useCancelToken = (): (() => CancelToken) => {
|
||||
const axiosSource = useRef(null)
|
||||
|
||||
const newCancelToken = useCallback(() => {
|
||||
axiosSource.current = axios.CancelToken.source()
|
||||
return axiosSource.current.token
|
||||
return axiosSource?.current?.token
|
||||
}, [])
|
||||
|
||||
useEffect(
|
||||
|
4
src/@types/Analytics.d.ts
vendored
Normal file
4
src/@types/Analytics.d.ts
vendored
Normal file
@ -0,0 +1,4 @@
|
||||
interface PageViews {
|
||||
count: number
|
||||
did: string
|
||||
}
|
1
src/@types/AssetExtended.d.ts
vendored
1
src/@types/AssetExtended.d.ts
vendored
@ -5,5 +5,6 @@ import { Asset } from '@oceanprotocol/lib'
|
||||
declare global {
|
||||
interface AssetExtended extends Asset {
|
||||
accessDetails?: AccessDetails
|
||||
views?: number
|
||||
}
|
||||
}
|
||||
|
@ -55,13 +55,13 @@ export function generateBaseQuery(
|
||||
...(baseQueryParams.filters || []),
|
||||
baseQueryParams.chainIds
|
||||
? getFilterTerm('chainId', baseQueryParams.chainIds)
|
||||
: [],
|
||||
: '',
|
||||
getFilterTerm('_index', 'aquarius'),
|
||||
...(baseQueryParams.ignorePurgatory
|
||||
? []
|
||||
? ''
|
||||
: [getFilterTerm('purgatory.state', false)]),
|
||||
...(baseQueryParams.ignoreState
|
||||
? []
|
||||
? ''
|
||||
: [
|
||||
{
|
||||
bool: {
|
||||
|
@ -1,3 +1,5 @@
|
||||
import { Asset } from '@oceanprotocol/lib'
|
||||
|
||||
// Boolean value that will be true if we are inside a browser, false otherwise
|
||||
export const isBrowser = typeof window !== 'undefined'
|
||||
|
||||
@ -14,3 +16,10 @@ export function removeItemFromArray<T>(arr: Array<T>, value: T): Array<T> {
|
||||
}
|
||||
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
|
||||
}
|
||||
|
@ -82,10 +82,13 @@ export function generateNftCreateData(
|
||||
|
||||
export function decodeTokenURI(tokenURI: string): NftMetadata {
|
||||
if (!tokenURI) return undefined
|
||||
|
||||
try {
|
||||
const nftMeta = JSON.parse(
|
||||
const nftMeta = tokenURI.includes('data:application/json')
|
||||
? (JSON.parse(
|
||||
Buffer.from(tokenURI.replace(tokenUriPrefix, ''), 'base64').toString()
|
||||
) as NftMetadata
|
||||
) as NftMetadata)
|
||||
: ({ image: tokenURI } as NftMetadata)
|
||||
|
||||
return nftMeta
|
||||
} catch (error) {
|
||||
|
@ -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 {
|
||||
|
@ -8,7 +8,8 @@ import {
|
||||
OrderParams,
|
||||
ProviderComputeInitialize,
|
||||
ProviderFees,
|
||||
ProviderInstance
|
||||
ProviderInstance,
|
||||
ProviderInitialize
|
||||
} from '@oceanprotocol/lib'
|
||||
import Web3 from 'web3'
|
||||
import { getOceanConfig } from './ocean'
|
||||
@ -20,6 +21,26 @@ import {
|
||||
} from '../../app.config'
|
||||
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 asset
|
||||
@ -40,15 +61,11 @@ export async function order(
|
||||
const datatoken = new Datatoken(web3)
|
||||
const config = getOceanConfig(asset.chainId)
|
||||
|
||||
const initializeData =
|
||||
!providerFees &&
|
||||
(await ProviderInstance.initialize(
|
||||
asset.id,
|
||||
asset.services[0].id,
|
||||
0,
|
||||
const initializeData = await initializeProvider(
|
||||
asset,
|
||||
accountId,
|
||||
asset.services[0].serviceEndpoint
|
||||
))
|
||||
providerFees
|
||||
)
|
||||
|
||||
const orderParams = {
|
||||
consumer: computeConsumerAddress || accountId,
|
||||
@ -130,15 +147,11 @@ export async function reuseOrder(
|
||||
providerFees?: ProviderFees
|
||||
): Promise<TransactionReceipt> {
|
||||
const datatoken = new Datatoken(web3)
|
||||
const initializeData =
|
||||
!providerFees &&
|
||||
(await ProviderInstance.initialize(
|
||||
asset.id,
|
||||
asset.services[0].id,
|
||||
0,
|
||||
const initializeData = await initializeProvider(
|
||||
asset,
|
||||
accountId,
|
||||
asset.services[0].serviceEndpoint
|
||||
))
|
||||
providerFees
|
||||
)
|
||||
|
||||
const tx = await datatoken.reuseOrder(
|
||||
asset.accessDetails.datatoken.address,
|
||||
|
@ -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,7 +136,8 @@ 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(
|
||||
const fetchedLocked: OperationResult<OceanLockedQuery, any> =
|
||||
await fetchData(
|
||||
OceanLocked,
|
||||
{ address: userAddress.toLowerCase() },
|
||||
queryContext
|
||||
@ -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() },
|
||||
|
@ -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 />
|
||||
)
|
||||
}
|
||||
|
@ -48,7 +48,7 @@
|
||||
}
|
||||
|
||||
.footer {
|
||||
margin-top: calc(var(--spacer) / 12);
|
||||
margin-top: calc(var(--spacer) / 24);
|
||||
}
|
||||
|
||||
.typeLabel {
|
||||
|
@ -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,16 +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'
|
||||
) : (
|
||||
<>
|
||||
<strong>{asset.views}</strong>{' '}
|
||||
{asset.views === 1 ? 'view' : 'views'}
|
||||
</>
|
||||
)}
|
||||
</span>
|
||||
) : null}
|
||||
</footer>
|
||||
|
@ -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} />}
|
||||
|
@ -1,15 +1,16 @@
|
||||
import React from 'react'
|
||||
import { render, screen } from '@testing-library/react'
|
||||
import * as axios from 'axios'
|
||||
import axios from 'axios'
|
||||
import Publisher from './'
|
||||
|
||||
const account = '0x0000000000000000000000000000000000000000'
|
||||
|
||||
jest.mock('axios')
|
||||
const axiosMock = axios as jest.Mocked<typeof axios>
|
||||
|
||||
describe('@shared/Publisher', () => {
|
||||
test('should return correct markup by default', async () => {
|
||||
;(axios as any).get.mockImplementationOnce(() =>
|
||||
axiosMock.get.mockImplementationOnce(() =>
|
||||
Promise.resolve({ data: { name: 'jellymcjellyfish.eth' } })
|
||||
)
|
||||
|
||||
@ -22,7 +23,7 @@ describe('@shared/Publisher', () => {
|
||||
})
|
||||
|
||||
test('should truncate account by default', async () => {
|
||||
;(axios as any).get.mockImplementationOnce(() =>
|
||||
axiosMock.get.mockImplementationOnce(() =>
|
||||
Promise.resolve({ data: { name: null } })
|
||||
)
|
||||
|
||||
@ -33,7 +34,7 @@ describe('@shared/Publisher', () => {
|
||||
})
|
||||
|
||||
test('should return correct markup in minimal state', async () => {
|
||||
;(axios as any).get.mockImplementationOnce(() =>
|
||||
axiosMock.get.mockImplementationOnce(() =>
|
||||
Promise.resolve({ data: { name: null } })
|
||||
)
|
||||
|
||||
@ -44,7 +45,7 @@ describe('@shared/Publisher', () => {
|
||||
})
|
||||
|
||||
test('should return markup with empty account', async () => {
|
||||
;(axios as any).get.mockImplementationOnce(() =>
|
||||
axiosMock.get.mockImplementationOnce(() =>
|
||||
Promise.resolve({ data: { name: null } })
|
||||
)
|
||||
|
||||
|
@ -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} />)
|
||||
})
|
||||
})
|
||||
|
@ -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`
|
||||
|
@ -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>
|
||||
|
@ -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}
|
||||
|
143
src/components/Asset/AssetActions/ButtonBuy/index.test.tsx
Normal file
143
src/components/Asset/AssetActions/ButtonBuy/index.test.tsx
Normal 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')
|
||||
})
|
||||
})
|
@ -5,7 +5,7 @@ import Loader from '../../../@shared/atoms/Loader'
|
||||
import { useWeb3 } from '@context/Web3'
|
||||
import Web3 from 'web3'
|
||||
|
||||
interface ButtonBuyProps {
|
||||
export interface ButtonBuyProps {
|
||||
action: 'download' | 'compute'
|
||||
disabled: boolean
|
||||
hasPreviousOrder: boolean
|
||||
@ -32,6 +32,7 @@ interface ButtonBuyProps {
|
||||
isAlgorithmConsumable?: boolean
|
||||
isSupportedOceanNetwork?: boolean
|
||||
hasProviderFee?: boolean
|
||||
retry?: boolean
|
||||
}
|
||||
|
||||
function getConsumeHelpText(
|
||||
@ -168,12 +169,14 @@ export default function ButtonBuy({
|
||||
priceType,
|
||||
algorithmPriceType,
|
||||
isAlgorithmConsumable,
|
||||
isSupportedOceanNetwork,
|
||||
hasProviderFee
|
||||
hasProviderFee,
|
||||
retry,
|
||||
isSupportedOceanNetwork
|
||||
}: ButtonBuyProps): ReactElement {
|
||||
const { web3 } = useWeb3()
|
||||
const buttonText =
|
||||
action === 'download'
|
||||
const buttonText = retry
|
||||
? 'Retry'
|
||||
: action === 'download'
|
||||
? hasPreviousOrder
|
||||
? 'Download'
|
||||
: priceType === 'free'
|
||||
|
@ -43,7 +43,8 @@ export default function FormStartCompute({
|
||||
datasetOrderPriceAndFees,
|
||||
algoOrderPriceAndFees,
|
||||
providerFeeAmount,
|
||||
validUntil
|
||||
validUntil,
|
||||
retry
|
||||
}: {
|
||||
algorithms: AssetSelectionAsset[]
|
||||
ddoListAlgorithms: Asset[]
|
||||
@ -71,6 +72,7 @@ export default function FormStartCompute({
|
||||
algoOrderPriceAndFees?: OrderPriceAndFees
|
||||
providerFeeAmount?: string
|
||||
validUntil?: string
|
||||
retry: boolean
|
||||
}): ReactElement {
|
||||
const { siteContent } = useMarketMetadata()
|
||||
const { accountId, balance, isSupportedOceanNetwork } = useWeb3()
|
||||
@ -301,6 +303,7 @@ export default function FormStartCompute({
|
||||
}
|
||||
isSupportedOceanNetwork={isSupportedOceanNetwork}
|
||||
hasProviderFee={providerFeeAmount && providerFeeAmount !== '0'}
|
||||
retry={retry}
|
||||
/>
|
||||
</Form>
|
||||
)
|
||||
|
@ -100,6 +100,7 @@ export default function Compute({
|
||||
const [refetchJobs, setRefetchJobs] = useState(false)
|
||||
const [isLoadingJobs, setIsLoadingJobs] = useState(false)
|
||||
const [jobs, setJobs] = useState<ComputeJobMetaData[]>([])
|
||||
const [retry, setRetry] = useState<boolean>(false)
|
||||
|
||||
const hasDatatoken = Number(dtBalance) >= 1
|
||||
const isComputeButtonDisabled =
|
||||
@ -299,7 +300,8 @@ export default function Compute({
|
||||
useEffect(() => {
|
||||
const newError = error
|
||||
if (!newError) return
|
||||
toast.error(newError)
|
||||
const errorMsg = newError + '. Please retry.'
|
||||
toast.error(errorMsg)
|
||||
}, [error])
|
||||
|
||||
async function startJob(): Promise<void> {
|
||||
@ -395,6 +397,7 @@ export default function Compute({
|
||||
initPriceAndFees()
|
||||
} catch (error) {
|
||||
setError(error.message)
|
||||
setRetry(true)
|
||||
LoggerInstance.error(`[compute] ${error.message} `)
|
||||
} finally {
|
||||
setIsOrdering(false)
|
||||
@ -484,6 +487,7 @@ export default function Compute({
|
||||
algoOrderPriceAndFees={algoOrderPriceAndFees}
|
||||
providerFeeAmount={providerFeeAmount}
|
||||
validUntil={computeValidUntil}
|
||||
retry={retry}
|
||||
/>
|
||||
</Formik>
|
||||
)}
|
||||
|
@ -48,6 +48,7 @@ export default function Download({
|
||||
const [isOrderDisabled, setIsOrderDisabled] = useState(false)
|
||||
const [orderPriceAndFees, setOrderPriceAndFees] =
|
||||
useState<OrderPriceAndFees>()
|
||||
const [retry, setRetry] = useState<boolean>(false)
|
||||
|
||||
const isUnsupportedPricing = asset?.accessDetails?.type === 'NOT_SUPPORTED'
|
||||
|
||||
@ -155,9 +156,10 @@ export default function Download({
|
||||
}
|
||||
} catch (error) {
|
||||
LoggerInstance.error(error)
|
||||
setRetry(true)
|
||||
const message = isOwned
|
||||
? '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)
|
||||
}
|
||||
setIsLoading(false)
|
||||
@ -181,6 +183,7 @@ export default function Download({
|
||||
isConsumable={asset.accessDetails?.isPurchasable}
|
||||
isBalanceSufficient={isBalanceSufficient}
|
||||
consumableFeedback={consumableFeedback}
|
||||
retry={retry}
|
||||
isSupportedOceanNetwork={isSupportedOceanNetwork}
|
||||
/>
|
||||
)
|
||||
|
@ -7,7 +7,7 @@
|
||||
.wrapper img {
|
||||
margin: 0;
|
||||
width: 128px;
|
||||
height: 128px;
|
||||
height: auto;
|
||||
}
|
||||
|
||||
.info {
|
||||
|
@ -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 && (
|
||||
|
@ -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);
|
||||
}
|
||||
|
@ -48,6 +48,7 @@ export default function Nft({
|
||||
content={
|
||||
<NftTooltip
|
||||
nft={nftMetadata}
|
||||
nftImage={nftImage}
|
||||
address={asset?.nftAddress}
|
||||
chainId={asset?.chainId}
|
||||
isBlockscoutExplorer={isBlockscoutExplorer}
|
||||
|
@ -23,7 +23,6 @@ export default function RelatedAssets(): ReactElement {
|
||||
!asset?.nft ||
|
||||
!asset?.metadata
|
||||
) {
|
||||
setIsLoading(false)
|
||||
return
|
||||
}
|
||||
|
||||
@ -31,11 +30,17 @@ export default function RelatedAssets(): ReactElement {
|
||||
setIsLoading(true)
|
||||
|
||||
try {
|
||||
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)
|
||||
)
|
||||
const tagResults = (await queryMetadata(tagQuery, newCancelToken()))
|
||||
|
||||
tagResults = (await queryMetadata(tagQuery, newCancelToken()))
|
||||
?.results
|
||||
}
|
||||
|
||||
if (tagResults.length === 4) {
|
||||
setRelatedAssets(tagResults)
|
||||
|
@ -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>
|
||||
|
56
src/components/Home/MostViews/index.test.tsx
Normal file
56
src/components/Home/MostViews/index.test.tsx
Normal 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
|
||||
})
|
||||
})
|
73
src/components/Home/MostViews/index.tsx
Normal file
73
src/components/Home/MostViews/index.tsx
Normal 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>
|
||||
)
|
||||
}
|
@ -1,29 +1,27 @@
|
||||
import { useUserPreferences } from '@context/UserPreferences'
|
||||
import { useCancelToken } from '@hooks/useCancelToken'
|
||||
import { useIsMounted } from '@hooks/useIsMounted'
|
||||
import { Asset, LoggerInstance } from '@oceanprotocol/lib'
|
||||
import { LoggerInstance } from '@oceanprotocol/lib'
|
||||
import AssetList from '@shared/AssetList'
|
||||
import Tooltip from '@shared/atoms/Tooltip'
|
||||
import Markdown from '@shared/Markdown'
|
||||
import { queryMetadata } from '@utils/aquarius'
|
||||
import { sortAssets } from '@utils/index'
|
||||
import React, { ReactElement, useState, useEffect } from 'react'
|
||||
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({
|
||||
title,
|
||||
query,
|
||||
action,
|
||||
queryData
|
||||
queryData,
|
||||
tooltip
|
||||
}: {
|
||||
title: ReactElement | string
|
||||
query: SearchQuery
|
||||
action?: ReactElement
|
||||
queryData?: string[]
|
||||
tooltip?: string
|
||||
}): ReactElement {
|
||||
const { chainIds } = useUserPreferences()
|
||||
const [result, setResult] = useState<PagedAssets>()
|
||||
@ -52,7 +50,7 @@ export default function SectionQueryResult({
|
||||
const result = await queryMetadata(query, newCancelToken())
|
||||
if (!isMounted()) return
|
||||
if (queryData && result?.totalResults > 0) {
|
||||
const sortedAssets = sortElements(result.results, queryData)
|
||||
const sortedAssets = sortAssets(result.results, queryData)
|
||||
const overflow = sortedAssets.length - 6
|
||||
sortedAssets.splice(sortedAssets.length - overflow, overflow)
|
||||
result.results = sortedAssets
|
||||
@ -69,7 +67,9 @@ export default function SectionQueryResult({
|
||||
|
||||
return (
|
||||
<section className={styles.section}>
|
||||
<h3>{title}</h3>
|
||||
<h3>
|
||||
{title} {tooltip && <Tooltip content={<Markdown text={tooltip} />} />}
|
||||
</h3>
|
||||
|
||||
<AssetList
|
||||
assets={result?.results}
|
||||
|
@ -13,6 +13,13 @@
|
||||
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'] {
|
||||
margin-top: var(--spacer);
|
||||
}
|
||||
|
@ -9,12 +9,14 @@ import TopTags from './TopTags'
|
||||
import SectionQueryResult from './SectionQueryResult'
|
||||
import styles from './index.module.css'
|
||||
import Allocations from './Allocations'
|
||||
import MostViews from './MostViews'
|
||||
|
||||
export default function HomePage(): ReactElement {
|
||||
const { chainIds } = useUserPreferences()
|
||||
|
||||
const [queryLatest, setQueryLatest] = useState<SearchQuery>()
|
||||
const [queryMostSales, setQueryMostSales] = useState<SearchQuery>()
|
||||
|
||||
const [queryMostAllocation, setQueryMostAllocation] = useState<SearchQuery>()
|
||||
|
||||
useEffect(() => {
|
||||
@ -66,7 +68,7 @@ export default function HomePage(): ReactElement {
|
||||
/>
|
||||
|
||||
<SectionQueryResult title="Most Sales" query={queryMostSales} />
|
||||
|
||||
<MostViews />
|
||||
<TopSales title="Publishers With Most Sales" />
|
||||
<TopTags title="Top Tags By Sales" />
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user