diff --git a/.jest/__fixtures__/algorithmAquarius.ts b/.jest/__fixtures__/algorithmAquarius.ts index c4c47839a..3a7ab984d 100644 --- a/.jest/__fixtures__/algorithmAquarius.ts +++ b/.jest/__fixtures__/algorithmAquarius.ts @@ -74,7 +74,7 @@ export const algorithmAquarius: AssetExtended = { { datatokenAddress: '0xCfDdA22C9837aE76E0faA845354f33C62E03653a', name: 'string', - symbol: 'OCEAN', + symbol: 'WETH', serviceId: 'dbc42f4c62d2452f8731fd023eacfae74e9c7a42fbd12ce84310f13342e4aab1', orders: 22, diff --git a/.jest/__fixtures__/datasetAquarius.ts b/.jest/__fixtures__/datasetAquarius.ts index ad15a889b..edb8275fb 100644 --- a/.jest/__fixtures__/datasetAquarius.ts +++ b/.jest/__fixtures__/datasetAquarius.ts @@ -61,7 +61,7 @@ export const datasetAquarius: AssetExtended = { { datatokenAddress: '0xCfDdA22C9837aE76E0faA845354f33C62E03653a', name: 'string', - symbol: 'OCEAN', + symbol: 'WETH', serviceId: 'dbc42f4c62d2452f8731fd023eacfae74e9c7a42fbd12ce84310f13342e4aab1', orders: 22, diff --git a/.jest/__fixtures__/datasetWithAccessDetails.ts b/.jest/__fixtures__/datasetWithAccessDetails.ts index af527ba5d..6788d5fe7 100644 --- a/.jest/__fixtures__/datasetWithAccessDetails.ts +++ b/.jest/__fixtures__/datasetWithAccessDetails.ts @@ -15,7 +15,7 @@ export const asset: AssetExtended = { baseToken: { address: '0xcfdda22c9837ae76e0faa845354f33c62e03653a', name: 'Ocean Token', - symbol: 'OCEAN', + symbol: 'WETH', decimals: 18 }, datatoken: { diff --git a/.jest/__fixtures__/datasetsWithAccessDetails.ts b/.jest/__fixtures__/datasetsWithAccessDetails.ts index 064d948de..01bf20898 100644 --- a/.jest/__fixtures__/datasetsWithAccessDetails.ts +++ b/.jest/__fixtures__/datasetsWithAccessDetails.ts @@ -115,7 +115,7 @@ export const assets: AssetExtended[] = [ { datatokenAddress: '0xCfDdA22C9837aE76E0faA845354f33C62E03653a', name: 'string', - symbol: 'OCEAN', + symbol: 'WETH', serviceId: 'dbc42f4c62d2452f8731fd023eacfae74e9c7a42fbd12ce84310f13342e4aab1', orders: 1, @@ -230,7 +230,7 @@ export const assets: AssetExtended[] = [ { datatokenAddress: '0xCfDdA22C9837aE76E0faA845354f33C62E03653a', name: 'string', - symbol: 'OCEAN', + symbol: 'WETH', serviceId: 'dbc42f4c62d2452f8731fd023eacfae74e9c7a42fbd12ce84310f13342e4aab1', orders: 1, @@ -345,7 +345,7 @@ export const assets: AssetExtended[] = [ { datatokenAddress: '0xCfDdA22C9837aE76E0faA845354f33C62E03653a', name: 'string', - symbol: 'OCEAN', + symbol: 'WETH', serviceId: 'dbc42f4c62d2452f8731fd023eacfae74e9c7a42fbd12ce84310f13342e4aab1', orders: 2, diff --git a/.jest/__fixtures__/web3.ts b/.jest/__fixtures__/web3.ts index bd6fc8d9c..36a7bf910 100644 --- a/.jest/__fixtures__/web3.ts +++ b/.jest/__fixtures__/web3.ts @@ -5,13 +5,13 @@ export default { accountId: '0x99840Df5Cb42faBE0Feb8811Aaa4BC99cA6C84e0', approvedBaseTokens: [ { - address: '0xcfdda22c9837ae76e0faa845354f33c62e03653a', - symbol: 'OCEAN', - name: 'Ocean Token', + address: '0x5f207d42f869fd1c71d7f0f81a2a67fc20ff7323', + symbol: 'WETH', + name: 'WETH Token', decimals: 18 } ], - balance: { eth: '0', ocean: '1000' }, + balance: { eth: '0', weth: '1000' }, block: 7751969, chainId: 5, connect: jest.fn(), diff --git a/app.config.cjs b/app.config.cjs index d45b6949b..7db203c75 100644 --- a/app.config.cjs +++ b/app.config.cjs @@ -19,7 +19,7 @@ module.exports = { infuraProjectId: process.env.NEXT_PUBLIC_INFURA_PROJECT_ID || 'xxx', oceanTokenAddress: process.env.NEXT_PUBLIC_OCEAN_TOKEN_ADDRESS, - oceanTokenSymbol: process.env.NEXT_PUBLIC_OCEAN_TOKEN_SYMBOL || 'OCEAN', + oceanTokenSymbol: process.env.NEXT_PUBLIC_OCEAN_TOKEN_SYMBOL || 'WETH', defaultDatatokenCap: '115792089237316195423570985008687907853269984665640564039457', defaultDatatokenTemplateIndex: 2, diff --git a/chains.config.cjs b/chains.config.cjs index 7793ca4c7..8894126fc 100644 --- a/chains.config.cjs +++ b/chains.config.cjs @@ -7,8 +7,8 @@ const chains = [ isDefault: false, isCustom: true, network: 'Eth Sepolia', - oceanTokenSymbol: 'OCEAN', - oceanTokenAddress: '0x1B083D8584dd3e6Ff37d04a6e7e82b5F622f3985', + oceanTokenSymbol: 'WETH', + oceanTokenAddress: '0x5f207d42f869fd1c71d7f0f81a2a67fc20ff7323', nftFactoryAddress: '0xFdC4a5DEaCDfc6D82F66e894539461a269900E13', fixedRateExchangeAddress: '0x8372715D834d286c9aECE1AcD51Da5755B32D505', dispenserAddress: '0x5461b629E01f72E0A468931A36e039Eea394f9eA', @@ -30,8 +30,8 @@ const chains = [ isDefault: true, isCustom: true, network: 'pontusx-testnet', - oceanTokenSymbol: 'OCEAN', - oceanTokenAddress: '0x1B083D8584dd3e6Ff37d04a6e7e82b5F622f3985', + oceanTokenSymbol: 'WETH', + oceanTokenAddress: '0x5f207d42f869fd1c71d7f0f81a2a67fc20ff7323', nftFactoryAddress: '0x2C4d542ff791890D9290Eec89C9348A4891A6Fd2', fixedRateExchangeAddress: '0xcE0F39abB6DA2aE4d072DA78FA0A711cBB62764E', dispenserAddress: '0xaB5B68F88Bc881CAA427007559E9bbF8818026dE', diff --git a/content/pages/editMetadata.json b/content/pages/editMetadata.json index 9d7a0a04e..4ed5b1ce6 100644 --- a/content/pages/editMetadata.json +++ b/content/pages/editMetadata.json @@ -22,7 +22,7 @@ "name": "price", "label": "New Price", "type": "number", - "min": "1", + "min": "0.0001", "placeholder": "0", "help": "Enter a new price.", "required": true diff --git a/content/price.json b/content/price.json index 1c741b62c..68db29a10 100644 --- a/content/price.json +++ b/content/price.json @@ -4,7 +4,7 @@ "title": "Fixed", "info": "Set your price for accessing this dataset. The datatoken for this dataset will be worth the entered amount of the selected base token.", "tooltips": { - "communityFee": "Goes to Ocean DAO for teams to improve the tools, build apps, do outreach, and more. A small fraction is used to burn OCEAN. This fee is collected when downloading or using an asset in a compute job.", + "communityFee": "Goes to Ocean DAO for teams to improve the tools, build apps, do outreach, and more. A small fraction is used to burn WETH. This fee is collected when downloading or using an asset in a compute job.", "marketplaceFee": "Goes to the marketplace owner that is hosting and providing the marketplace and is collected when downloading or using an asset in a compute job. In Ocean Market, it is treated as network revenue that goes to the Ocean community." } }, diff --git a/documentation/build-a-marketplace/README.md b/documentation/build-a-marketplace/README.md index 6dc3af009..1f3856d54 100644 --- a/documentation/build-a-marketplace/README.md +++ b/documentation/build-a-marketplace/README.md @@ -19,9 +19,9 @@ Using Ocean Market is already a big improvement on the alternatives that are out The tutorial covers: -* Forking and running Ocean Market locally -* Customizing your fork of Ocean market -* Quick deployment of Ocean Market +- Forking and running Ocean Market locally +- Customizing your fork of Ocean market +- Quick deployment of Ocean Market ## Preparation @@ -29,19 +29,19 @@ The tutorial covers: If you’re completely unfamiliar with Ocean Market or web3 applications in general, you will benefit from reading these guides first: -* To use your clone of Ocean Market, you’ll need a wallet. We recommend getting set up with metamask. -* You’ll also need some OCEAN on a testnet to use your marketplace. -* When you have the testnet tokens, have a go at publishing a data NFT on Ocean Market. -* Run through the process of consuming a data asset on Ocean Market. +- To use your clone of Ocean Market, you’ll need a wallet. We recommend getting set up with metamask. +- You’ll also need some WETH on a testnet to use your marketplace. +- When you have the testnet tokens, have a go at publishing a data NFT on Ocean Market. +- Run through the process of consuming a data asset on Ocean Market. For more information visit the [Ocean Docs](https://docs.oceanprotocol.com/) **Required Prerequisites** -* Git. Instructions for installing Git can be found [here](https://git-scm.com/book/en/v2/Getting-Started-Installing-Git). -* Node.js can be downloaded from [here](https://nodejs.org/en/download/) (we’re using version 18 in this guide) -* A decent code editor, such as [Visual Studio Code](https://code.visualstudio.com/). -* You’ll need a Github account to fork Ocean Market via [Github](https://github.com/). +- Git. Instructions for installing Git can be found [here](https://git-scm.com/book/en/v2/Getting-Started-Installing-Git). +- Node.js can be downloaded from [here](https://nodejs.org/en/download/) (we’re using version 18 in this guide) +- A decent code editor, such as [Visual Studio Code](https://code.visualstudio.com/). +- You’ll need a Github account to fork Ocean Market via [Github](https://github.com/). {% hint style="warning" %} Let's emphasize an important aspect of building dApps. It's crucial to keep in mind that practically everything can be added to the blockchain 😵 When you integrate with these components, it becomes **crucial** for you, as a developer, to ensure **proper sanitization** of the responses on your end. This means you should carefully **validate and filter** the data received to **prevent** any potential vulnerabilities or security risks in your applications. diff --git a/documentation/using-ocean-market.md b/documentation/using-ocean-market.md index 2b3d4b0aa..c5a912e7a 100644 --- a/documentation/using-ocean-market.md +++ b/documentation/using-ocean-market.md @@ -10,14 +10,14 @@ The Ocean Market is a place for buyers + sellers of top-notch data and algorithm #### **You can:** -* Buy access to unique data, algorithms, and compute jobs. 🛍️ -* Tokenize & monetize your intellectual property through blockchain technology. 💪 +- Buy access to unique data, algorithms, and compute jobs. 🛍️ +- Tokenize & monetize your intellectual property through blockchain technology. 💪 #### **Learn to:** -* Publish an NFT -* Download NFT Assets -* Host Your Assets +- Publish an NFT +- Download NFT Assets +- Host Your Assets For more information visit the [Ocean Docs](https://docs.oceanprotocol.com/) @@ -25,6 +25,6 @@ For more information visit the [Ocean Docs](https://docs.oceanprotocol.com/) **If you are new to web3** and blockchain technologies then we suggest you first get familiar with some Web3 basics: -* Wallet Basics 👛 -* Set Up MetaMask 🦊 -* Manage Your OCEAN 🪙 +- Wallet Basics 👛 +- Set Up MetaMask 🦊 +- Manage Your WETH 🪙 diff --git a/src/@context/MarketMetadata/index.tsx b/src/@context/MarketMetadata/index.tsx index 715b3124e..e3956a655 100644 --- a/src/@context/MarketMetadata/index.tsx +++ b/src/@context/MarketMetadata/index.tsx @@ -62,8 +62,8 @@ function MarketMetadataProvider({ const oceanToken: TokenInfo = { address: appConfig.oceanTokenAddress, - name: 'OCEAN', - symbol: 'OCEAN', + name: process.env.NEXT_PUBLIC_OCEAN_TOKEN_SYMBOL, + symbol: process.env.NEXT_PUBLIC_OCEAN_TOKEN_SYMBOL, decimals: 18 } diff --git a/src/@context/Prices/_utils.ts b/src/@context/Prices/_utils.ts index 4279c7d19..d9c0b299d 100644 --- a/src/@context/Prices/_utils.ts +++ b/src/@context/Prices/_utils.ts @@ -2,10 +2,11 @@ // Deal with differences between token symbol & Coingecko API IDs // export function getCoingeckoTokenId(symbol: string) { - // can be OCEAN or mOCEAN + // can be WETH or mOCEAN const isOcean = symbol?.toLowerCase().includes('ocean') // can be H2O or H20 // const isH2o = symbol?.toLowerCase().includes('h2') + const isWeth = symbol?.toLowerCase() === 'weth' const isEth = symbol?.toLowerCase() === 'eth' const isMatic = symbol?.toLowerCase() === 'matic' @@ -13,6 +14,8 @@ export function getCoingeckoTokenId(symbol: string) { ? 'ocean-protocol' : isEth ? 'ethereum' + : isWeth + ? 'ethereum' : isMatic ? 'matic-network' : symbol?.toLowerCase() diff --git a/src/@context/Prices/index.test.tsx b/src/@context/Prices/index.test.tsx index 02e636d19..6d1c2fe3b 100644 --- a/src/@context/Prices/index.test.tsx +++ b/src/@context/Prices/index.test.tsx @@ -27,8 +27,8 @@ test('useSWR is called', async () => { // expect(result.current.prices['ocean-protocol'].eur).toBe('2') }) -test('should get correct Coingecko API ID for OCEAN', async () => { - const id1 = getCoingeckoTokenId('OCEAN') +test('should get correct Coingecko API ID for WETH', async () => { + const id1 = getCoingeckoTokenId('WETH') expect(id1).toBe('ocean-protocol') const id2 = getCoingeckoTokenId('mOCEAN') diff --git a/src/@utils/accessDetailsAndPricing.ts b/src/@utils/accessDetailsAndPricing.ts index d56d7044f..22a11b532 100644 --- a/src/@utils/accessDetailsAndPricing.ts +++ b/src/@utils/accessDetailsAndPricing.ts @@ -15,7 +15,7 @@ import { publisherMarketOrderFee, customProviderUrl } from '../../app.config.cjs' -import { Signer } from 'ethers' +import { Signer, ethers } from 'ethers' import { toast } from 'react-toastify' import { getDummySigner } from './wallet' // import { Service } from 'src/@types/ddo/Service' @@ -36,6 +36,8 @@ export async function getOrderPriceAndFees( // this function give price signer?: Signer, providerFees?: ProviderFees ): Promise { + const tokenDecimals = 18 // Replace with actual token decimals (fetch via contract if needed) + const orderPriceAndFee = { price: accessDetails.price || '0', publisherMarketOrderFee: publisherMarketOrderFee || '0', @@ -67,59 +69,73 @@ export async function getOrderPriceAndFees( // this function give price } const message = getErrorMessage(error.message) LoggerInstance.error('[Initialize Provider] Error:', message) - - // Customize error message for accountId non included in allow list if ( - // TODO: verify if the error code is correctly resolved by the provider message.includes('ConsumableCodes.CREDENTIAL_NOT_IN_ALLOW_LIST') || message.includes('denied with code: 3') ) { if (accountId !== ZERO_ADDRESS) { toast.error( - `Consumer address not found in allow list for service ${asset.id}. Access has been denied.` + `Consumer address not found in allow list for service ${asset.id}.` ) } - return + return orderPriceAndFee } - // Customize error message for accountId included in deny list if ( - // TODO: verify if the error code is correctly resolved by the provider message.includes('ConsumableCodes.CREDENTIAL_IN_DENY_LIST') || message.includes('denied with code: 4') ) { if (accountId !== ZERO_ADDRESS) { toast.error( - `Consumer address found in deny list for service ${asset.id}. Access has been denied.` + `Consumer address found in deny list for service ${asset.id}.` ) } - return + return orderPriceAndFee } toast.error(message) } orderPriceAndFee.providerFee = providerFees || initializeData?.providerFee - // fetch price and swap fees + // Fetch price and swap fees if (accessDetails.type === 'fixed') { const fixed = await getFixedBuyPrice(accessDetails, asset.chainId, signer) - orderPriceAndFee.price = accessDetails.price - orderPriceAndFee.opcFee = fixed.oceanFeeAmount - orderPriceAndFee.publisherMarketFixedSwapFee = fixed.marketFeeAmount - orderPriceAndFee.consumeMarketFixedSwapFee = fixed.consumeMarketFeeAmount - } + orderPriceAndFee.price = ethers.utils + .parseUnits(accessDetails.price, tokenDecimals) + .toString() + orderPriceAndFee.opcFee = ethers.utils + .parseUnits(fixed.oceanFeeAmount, tokenDecimals) + .toString() + orderPriceAndFee.publisherMarketFixedSwapFee = ethers.utils + .parseUnits(fixed.marketFeeAmount, tokenDecimals) + .toString() + orderPriceAndFee.consumeMarketFixedSwapFee = ethers.utils + .parseUnits(fixed.consumeMarketFeeAmount, tokenDecimals) + .toString() + } else { + const price = new Decimal(+accessDetails.price || 0) + const consumeMarketFeePercentage = + Number(orderPriceAndFee?.consumeMarketOrderFee) || 0 + const publisherMarketFeePercentage = + Number(orderPriceAndFee?.publisherMarketOrderFee) || 0 - const price = new Decimal(+accessDetails.price || 0) - const consumeMarketFeePercentage = - +orderPriceAndFee?.consumeMarketOrderFee || 0 - const publisherMarketFeePercentage = - +orderPriceAndFee?.publisherMarketOrderFee || 0 + if ( + isNaN(consumeMarketFeePercentage) || + isNaN(publisherMarketFeePercentage) + ) { + LoggerInstance.error('Invalid fee percentage') + return orderPriceAndFee + } - // Calculate percentage-based fees - const consumeMarketFee = price.mul(consumeMarketFeePercentage).div(100) - const publisherMarketFee = price.mul(publisherMarketFeePercentage).div(100) + const consumeMarketFee = price.mul(consumeMarketFeePercentage).div(100) + const publisherMarketFee = price.mul(publisherMarketFeePercentage).div(100) + const result = price.add(consumeMarketFee).add(publisherMarketFee) + + // Format result to respect token decimals + orderPriceAndFee.price = ethers.utils + .parseUnits(result.toFixed(tokenDecimals), tokenDecimals) + .toString() + } - // Calculate total - const result = price.add(consumeMarketFee).add(publisherMarketFee).toString() - orderPriceAndFee.price = result + LoggerInstance.log('OrderPriceAndFees:', orderPriceAndFee) return orderPriceAndFee } diff --git a/src/@utils/aquarius/index.ts b/src/@utils/aquarius/index.ts index 1ce4e9ed8..25c54aac1 100644 --- a/src/@utils/aquarius/index.ts +++ b/src/@utils/aquarius/index.ts @@ -406,7 +406,6 @@ export async function getPublishedAssets( ): Promise { if (!accountId) return const filters: FilterTerm[] = [] - filters.push(getFilterTerm('indexedMetadata.nft.state', [0, 4, 5])) filters.push( getFilterTerm('indexedMetadata.nft.owner', accountId.toLowerCase()) ) diff --git a/src/@utils/numbers.ts b/src/@utils/numbers.ts index aa82804b5..af17d23e4 100644 --- a/src/@utils/numbers.ts +++ b/src/@utils/numbers.ts @@ -6,12 +6,16 @@ export function formatNumber( locale: string, decimals?: string ): string { + const priceStr = price.toString() + const decimalPlacesInPrice = priceStr.includes('.') + ? priceStr.split('.')[1].length + : 0 + const targetDecimalPlaces = decimals + ? Math.min(Number(decimals), decimalPlacesInPrice) + : decimalPlacesInPrice + 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) }) + decimalPlaces: targetDecimalPlaces }) } diff --git a/src/@utils/ocean/index.ts b/src/@utils/ocean/index.ts index 160cf69eb..459b8d7a1 100644 --- a/src/@utils/ocean/index.ts +++ b/src/@utils/ocean/index.ts @@ -18,7 +18,7 @@ export function sanitizeDevelopmentConfig(config: Config): Config { process.env.NEXT_PUBLIC_FIXED_RATE_EXCHANGE_ADDRESS, dispenserAddress: process.env.NEXT_PUBLIC_DISPENSER_ADDRESS, oceanTokenAddress: process.env.NEXT_PUBLIC_OCEAN_TOKEN_ADDRESS, - oceanTokenSymbol: process.env.NEXT_PUBLIC_OCEAN_TOKEN_SYMBOL || 'OCEAN', + oceanTokenSymbol: process.env.NEXT_PUBLIC_OCEAN_TOKEN_SYMBOL || 'WETH', nftFactoryAddress: process.env.NEXT_PUBLIC_NFT_FACTORY_ADDRESS, routerFactoryAddress: process.env.NEXT_PUBLIC_ROUTER_FACTORY_ADDRESS, accessListFactory: process.env.NEXT_PUBLIC_ACCESS_LIST_FACTORY_ADDRESS @@ -48,6 +48,8 @@ export function getOceanConfig(network: string | number): Config { // Override RPC URL for Sepolia if it's set (the reason is ocean.js supports only infura) if (network === 11155111 && process.env.NEXT_PUBLIC_NODE_URI) { config.nodeUri = process.env.NEXT_PUBLIC_NODE_URI + config.oceanTokenSymbol = + process.env.NEXT_PUBLIC_OCEAN_TOKEN_SYMBOL || 'WETH' // config.oceanNodeUri = process.env.NEXT_PUBLIC_NODE_URL } diff --git a/src/components/@shared/Price/Conversion.tsx b/src/components/@shared/Price/Conversion.tsx index d97c1aa12..523b9ea87 100644 --- a/src/components/@shared/Price/Conversion.tsx +++ b/src/components/@shared/Price/Conversion.tsx @@ -10,7 +10,7 @@ export default function Conversion({ className, hideApproximateSymbol }: { - price: number // expects price in OCEAN, not wei + price: number // expects price in WETH, not wei symbol: string className?: string hideApproximateSymbol?: boolean diff --git a/src/components/@shared/Price/index.tsx b/src/components/@shared/Price/index.tsx index e6010458f..a6e95c65c 100644 --- a/src/components/@shared/Price/index.tsx +++ b/src/components/@shared/Price/index.tsx @@ -3,6 +3,7 @@ import { AssetPrice } from '@oceanprotocol/ddo-js' import PriceUnit from './PriceUnit' import { getOceanConfig } from '@utils/ocean' import Loader from '@shared/atoms/Loader' +import { useNetwork } from 'wagmi' export default function Price({ price, @@ -18,7 +19,9 @@ export default function Price({ conversion?: boolean size?: 'small' | 'mini' | 'large' }): ReactElement { - const oceanConfig = getOceanConfig(11155111) + const { chain } = useNetwork() + const chainId = chain?.id || 11155111 + const oceanConfig = getOceanConfig(chainId) const symbol = oceanConfig.oceanTokenSymbol if (!price && !orderPriceAndFees) return @@ -27,7 +30,6 @@ export default function Price({ } const rawPrice = price?.price - console.log('Raw price', rawPrice) const parsedPrice = rawPrice === null || rawPrice === undefined ? null @@ -41,6 +43,7 @@ export default function Price({ className={className} size={size} conversion={conversion} + decimals="4" /> ) } diff --git a/src/components/Asset/AssetActions/ButtonBuy/index.tsx b/src/components/Asset/AssetActions/ButtonBuy/index.tsx index 9ed1957ac..7aa8b8256 100644 --- a/src/components/Asset/AssetActions/ButtonBuy/index.tsx +++ b/src/components/Asset/AssetActions/ButtonBuy/index.tsx @@ -86,7 +86,7 @@ function getAlgoHelpText( isSupportedOceanNetwork ? `You already bought the selected ${selectedComputeAssetType}, allowing you to use it without paying again.` : hasDatatokenSelectedComputeAsset - ? `You own ${dtBalanceSelectedComputeAsset} ${dtSymbolSelectedComputeAsset} allowing you to use the selected ${selectedComputeAssetType} by spending 1 ${dtSymbolSelectedComputeAsset}, but without paying OCEAN again.` + ? `You own ${dtBalanceSelectedComputeAsset} ${dtSymbolSelectedComputeAsset} allowing you to use the selected ${selectedComputeAssetType} by spending 1 ${dtSymbolSelectedComputeAsset}, but without paying WETH again.` : isAccountConnected && !isSupportedOceanNetwork ? `Connect to the correct network to interact with this asset.` : isBalanceSufficient === false diff --git a/src/components/Asset/AssetActions/Download/index.tsx b/src/components/Asset/AssetActions/Download/index.tsx index 9a1314d7c..a180301df 100644 --- a/src/components/Asset/AssetActions/Download/index.tsx +++ b/src/components/Asset/AssetActions/Download/index.tsx @@ -1,4 +1,4 @@ -import React, { ReactElement, useEffect, useState } from 'react' +import React, { ReactElement, useEffect, useState, useCallback } from 'react' import FileIcon from '@shared/FileIcon' import Price from '@shared/Price' import { useAsset } from '@context/Asset' @@ -11,7 +11,6 @@ import { UserCustomParameters, ZERO_ADDRESS } from '@oceanprotocol/lib' -import { AssetPrice, Service } from '@oceanprotocol/ddo-js' import { order } from '@utils/order' import { downloadFile } from '@utils/provider' import { getOrderFeedback } from '@utils/feedback' @@ -33,25 +32,23 @@ import { Form, Formik, useFormikContext } from 'formik' import { getDownloadValidationSchema } from './_validation' import { getDefaultValues } from '../ConsumerParameters/FormConsumerParameters' -export default function Download({ +// Cache for pricing data +const priceCache = new Map() + +export default React.memo(function Download({ asset, file, isBalanceSufficient, dtBalance, fileIsLoading, - consumableFeedback, - service, accessDetails }: { asset: AssetExtended - service?: Service - accessDetails?: AccessDetails - serviceIndex?: number file: FileInfo isBalanceSufficient: boolean dtBalance: string fileIsLoading?: boolean - consumableFeedback?: string + accessDetails?: AccessDetails }): ReactElement { const { address: accountId, isConnected } = useAccount() const { data: signer } = useSigner() @@ -68,148 +65,158 @@ export default function Download({ const [isOwned, setIsOwned] = useState(false) const [validOrderTx, setValidOrderTx] = useState('') const [isOrderDisabled, setIsOrderDisabled] = useState(false) - const [assetPrice, setAssetPrice] = useState(null) + const [assetPrice, setAssetPrice] = useState(null) const [orderPriceAndFees, setOrderPriceAndFees] = useState() const [retry, setRetry] = useState(false) const isUnsupportedPricing = - !asset?.accessDetails || + !accessDetails || !asset.services.length || - asset?.accessDetails?.type === 'NOT_SUPPORTED' || - (asset?.accessDetails?.type === 'fixed' && - !asset?.accessDetails?.baseToken?.symbol) - - useEffect(() => { - const price: AssetPrice = getAvailablePrice(accessDetails) - setAssetPrice(price) - }, [accessDetails]) - useEffect(() => { - Number(asset?.indexedMetadata?.nft.state) === 4 && setIsOrderDisabled(true) - }, [asset?.indexedMetadata?.nft.state]) + accessDetails?.type === 'NOT_SUPPORTED' || + (accessDetails?.type === 'fixed' && !accessDetails?.baseToken?.symbol) - useEffect(() => { - if (isUnsupportedPricing) return + // Memoized price fetching + const fetchPriceAndFees = useCallback(async () => { + if ( + isUnsupportedPricing || + accessDetails.addressOrId === ZERO_ADDRESS || + accessDetails.type === 'free' + ) + return - setIsOwned(asset?.accessDetails?.isOwned || false) - setValidOrderTx(asset?.accessDetails?.validOrderTx || '') + const cacheKey = `${asset.id}-${accessDetails.addressOrId}` + if (priceCache.has(cacheKey)) { + setOrderPriceAndFees(priceCache.get(cacheKey)) + return + } - // get full price and fees - async function init() { - if ( - asset.accessDetails.addressOrId === ZERO_ADDRESS || - asset.accessDetails.type === 'free' + try { + setIsPriceLoading(true) + const _orderPriceAndFees = await getOrderPriceAndFees( + asset, + asset.services[0], + accessDetails, + ZERO_ADDRESS ) - return - - try { - !orderPriceAndFees && setIsPriceLoading(true) - const _orderPriceAndFees = await getOrderPriceAndFees( - asset, - service || asset.services[0], - accessDetails, - ZERO_ADDRESS - ) + if (isMounted()) { setOrderPriceAndFees(_orderPriceAndFees) - !orderPriceAndFees && setIsPriceLoading(false) - } catch (error) { - LoggerInstance.error('getOrderPriceAndFees', error) - setIsPriceLoading(false) + priceCache.set(cacheKey, _orderPriceAndFees) } + } catch (error) { + LoggerInstance.error('getOrderPriceAndFees', error) + } finally { + if (isMounted()) setIsPriceLoading(false) } + }, [accessDetails, isUnsupportedPricing, isMounted]) - if (!orderPriceAndFees) init() + // Initialize pricing and ownership + useEffect(() => { + if (isUnsupportedPricing) return - /** - * we listen to the assets' changes to get the most updated price - * based on the asset and the poolData's information. - * Not adding isLoading and getOpcFeeForToken because we set these here. It is a compromise - */ - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [asset, getOpcFeeForToken, isUnsupportedPricing]) + setAssetPrice(getAvailablePrice(accessDetails)) + setIsOwned(accessDetails?.isOwned || false) + setValidOrderTx(accessDetails?.validOrderTx || '') + setIsOrderDisabled(Number(asset?.indexedMetadata?.nft.state) === 4) + fetchPriceAndFees() + }, [ + accessDetails, + asset?.indexedMetadata?.nft.state, + isUnsupportedPricing, + fetchPriceAndFees + ]) + + // Update datatoken status useEffect(() => { setHasDatatoken(Number(dtBalance) >= 1) }, [dtBalance]) + // Update disabled state useEffect(() => { - if ( - (asset?.accessDetails?.type === 'fixed' && !orderPriceAndFees) || - !isMounted || - !accountId || - isUnsupportedPricing - ) - return + if (isUnsupportedPricing || !isMounted || !accountId) return - /** - * disabled in these cases: - * - if the asset is not purchasable - * - if the user is on the wrong network - * - if user balance is not sufficient - * - if user has no datatokens - */ const isDisabled = - !asset?.accessDetails.isPurchasable || + !accessDetails?.isPurchasable || !isAssetNetwork || ((!isBalanceSufficient || !isAssetNetwork) && !isOwned && !hasDatatoken) - setIsDisabled(isDisabled) + if (isMounted()) setIsDisabled(isDisabled) }, [ isMounted, - asset?.accessDetails, isBalanceSufficient, isAssetNetwork, hasDatatoken, accountId, isOwned, - isUnsupportedPricing, - orderPriceAndFees + isUnsupportedPricing ]) - async function handleOrderOrDownload(dataParams?: UserCustomParameters) { - setIsLoading(true) - setRetry(false) - try { - if (isOwned) { - setStatusText( - getOrderFeedback( - asset.accessDetails.baseToken?.symbol, - asset.accessDetails.datatoken?.symbol - )[3] - ) + // Handle order or download with retry logic + const handleOrderOrDownload = useCallback( + async (dataParams?: UserCustomParameters, attempt = 1, maxAttempts = 3) => { + setIsLoading(true) + setRetry(false) - await downloadFile(signer, asset, accountId, validOrderTx, dataParams) - } else { - setStatusText( - getOrderFeedback( - asset.accessDetails.baseToken?.symbol, - asset.accessDetails.datatoken?.symbol - )[asset.accessDetails.type === 'fixed' ? 2 : 1] - ) - const orderTx = await order( - signer, - asset, - orderPriceAndFees, - accountId, - hasDatatoken - ) - const tx = await orderTx.wait() - if (!tx) { - throw new Error() + try { + if (isOwned) { + setStatusText( + getOrderFeedback( + accessDetails.baseToken?.symbol, + accessDetails.datatoken?.symbol + )[3] + ) + await downloadFile(signer, asset, accountId, validOrderTx, dataParams) + } else { + setStatusText( + getOrderFeedback( + accessDetails.baseToken?.symbol, + accessDetails.datatoken?.symbol + )[accessDetails.type === 'fixed' ? 2 : 1] + ) + const orderTx = await order( + signer, + asset, + orderPriceAndFees, + accountId, + hasDatatoken + ) + const tx = await orderTx.wait() + if (!tx) throw new Error() + if (isMounted()) { + setIsOwned(true) + setValidOrderTx(tx.transactionHash) + } + } + } catch (error) { + LoggerInstance.error(error) + if (attempt < maxAttempts) { + const delay = Math.pow(2, attempt) * 1000 // Exponential backoff + await new Promise((resolve) => setTimeout(resolve, delay)) + return handleOrderOrDownload(dataParams, attempt + 1, maxAttempts) } - setIsOwned(true) - setValidOrderTx(tx.transactionHash) + setRetry(true) + toast.error( + isOwned + ? 'Failed to download file!' + : 'An error occurred, please retry.' + ) + } finally { + if (isMounted()) setIsLoading(false) } - } catch (error) { - LoggerInstance.error(error) - setRetry(true) - const message = isOwned - ? 'Failed to download file!' - : 'An error occurred, please retry. Check console for more information.' - toast.error(message) - } - setIsLoading(false) - } + }, + [ + isOwned, + accessDetails, + signer, + asset, + accountId, + validOrderTx, + orderPriceAndFees, + hasDatatoken, + isMounted + ] + ) const PurchaseButton = ({ isValid }: { isValid?: boolean }) => ( { const { isValid } = useFormikContext() const isPricingLoaded = - asset?.accessDetails?.type === 'free' || - (!isPriceLoading && orderPriceAndFees) + accessDetails?.type === 'free' || (!isPriceLoading && orderPriceAndFees) return (
@@ -259,7 +265,7 @@ export default function Download({ /> ) : ( <> - {asset && } + {!isInPurgatory && (
@@ -300,11 +306,7 @@ export default function Download({ ) : ( ) -} +}) diff --git a/src/components/Asset/AssetActions/index.tsx b/src/components/Asset/AssetActions/index.tsx index a47edfa84..9ca5d03a2 100644 --- a/src/components/Asset/AssetActions/index.tsx +++ b/src/components/Asset/AssetActions/index.tsx @@ -1,4 +1,10 @@ -import React, { ReactElement, useState, useEffect } from 'react' +import React, { + ReactElement, + useState, + useEffect, + useCallback, + useMemo +} from 'react' import Download from './Download' import { FileInfo, LoggerInstance, Datatoken } from '@oceanprotocol/lib' import { compareAsBN } from '@utils/numbers' @@ -15,7 +21,10 @@ import AssetStats from './AssetStats' import { useAccount, useProvider, useNetwork } from 'wagmi' import useBalance from '@hooks/useBalance' -export default function AssetActions({ +// Simple in-memory cache for file metadata +const fileMetadataCache = new Map() + +export default React.memo(function AssetActions({ asset }: { asset: AssetExtended @@ -27,106 +36,113 @@ export default function AssetActions({ const { isAssetNetwork } = useAsset() const newCancelToken = useCancelToken() const isMounted = useIsMounted() - - // TODO: using this for the publish preview works fine, but produces a console warning - // on asset details page as there is no formik context there: - // Warning: Formik context is undefined, please verify you are calling useFormikContext() - // as child of a component. - const formikState = useFormikContext() + const formikValues = useFormikContext() const [isBalanceSufficient, setIsBalanceSufficient] = useState() const [dtBalance, setDtBalance] = useState() const [fileMetadata, setFileMetadata] = useState() const [fileIsLoading, setFileIsLoading] = useState(false) - // Get and set file info - useEffect(() => { + // Memoized file info initialization + const initFileInfo = useCallback(async () => { + const cacheKey = `${asset?.id}-${asset?.services[0]?.id}` + if (fileMetadataCache.has(cacheKey)) { + setFileMetadata(fileMetadataCache.get(cacheKey)) + return + } + const oceanConfig = getOceanConfig(asset?.chainId) if (!oceanConfig) return - async function initFileInfo() { - setFileIsLoading(true) - const providerUrl = - formikState?.values?.services[0].providerUrl.url || - asset?.services[0]?.serviceEndpoint - - const storageType = formikState?.values?.services - ? formikState?.values?.services[0].files[0].type - : null - - // TODO: replace 'any' with correct typing - const file = formikState?.values?.services[0].files[0] as any - const query = file?.query || undefined - const abi = file?.abi || undefined - const headers = file?.headers || undefined - const method = file?.method || undefined - - try { - const fileInfoResponse = formikState?.values?.services?.[0].files?.[0] - .url - ? await getFileInfo( - formikState?.values?.services?.[0].files?.[0].url, - providerUrl, - storageType, - query, - headers, - abi, - chain?.id, - method - ) - : await getFileDidInfo(asset?.id, asset?.services[0]?.id, providerUrl) - - fileInfoResponse && setFileMetadata(fileInfoResponse[0]) - - // set the content type in the Dataset Schema + setFileIsLoading(true) + const providerUrl = + formikValues?.values.services[0].providerUrl.url || + asset?.services[0]?.serviceEndpoint + + const storageType = formikValues?.values.services + ? formikValues?.values.services[0].files[0].type + : null + const file = formikValues?.values.services[0].files[0] as any + const query = file?.query || undefined + const abi = file?.abi || undefined + const headers = file?.headers || undefined + const method = file?.method || undefined + + try { + const fileInfoResponse = formikValues?.values.services?.[0].files?.[0].url + ? await getFileInfo( + formikValues?.values.services?.[0].files?.[0].url, + providerUrl, + storageType, + query, + headers, + abi, + chain?.id, + method + ) + : await getFileDidInfo(asset?.id, asset?.services[0]?.id, providerUrl) + + if (fileInfoResponse && isMounted()) { + setFileMetadata(fileInfoResponse[0]) + fileMetadataCache.set(cacheKey, fileInfoResponse[0]) + + // Update dataset schema const datasetSchema = document.scripts?.namedItem('datasetSchema') if (datasetSchema) { const datasetSchemaJSON = JSON.parse(datasetSchema.innerText) if (datasetSchemaJSON?.distribution[0]['@type'] === 'DataDownload') { - const contentType = fileInfoResponse[0]?.contentType - datasetSchemaJSON.distribution[0].encodingFormat = contentType + datasetSchemaJSON.distribution[0].encodingFormat = + fileInfoResponse[0]?.contentType datasetSchema.innerText = JSON.stringify(datasetSchemaJSON) } } - - setFileIsLoading(false) - } catch (error) { - setFileIsLoading(false) - LoggerInstance.error(error.message) } + } catch (error) { + LoggerInstance.error(error.message) + } finally { + if (isMounted()) setFileIsLoading(false) } - initFileInfo() - }, [asset, isMounted, newCancelToken, formikState?.values?.services]) + }, [asset, formikValues, chain?.id, newCancelToken, isMounted]) - // Get and set user DT balance - useEffect(() => { - const isReady = - web3Provider && - accountId && - asset?.accessDetails?.baseToken?.address && - isAssetNetwork - - if (!isReady) return - - async function init() { - try { - const datatokenInstance = new Datatoken(web3Provider as any) - const dtBalance = await datatokenInstance.balance( - asset.accessDetails.baseToken.address, - accountId - ) - setDtBalance(dtBalance) - } catch (e: any) { - LoggerInstance.error('[DT Balance Error]', e.message || e) - } + // Memoized datatoken balance initialization + const initDtBalance = useCallback(async () => { + if ( + !web3Provider || + !accountId || + !asset?.accessDetails?.baseToken?.address || + !isAssetNetwork + ) + return + + try { + const datatokenInstance = new Datatoken(web3Provider as any) + const dtBalance = await datatokenInstance.balance( + asset.accessDetails.baseToken.address, + accountId + ) + if (isMounted()) setDtBalance(dtBalance) + } catch (e: any) { + LoggerInstance.error('[DT Balance Error]', e.message || e) } + }, [web3Provider, accountId, asset, isAssetNetwork, isMounted]) + + // Fetch file info + useEffect(() => { + initFileInfo() + }, [initFileInfo]) - init() - }, [web3Provider, accountId, asset, isAssetNetwork]) + // Fetch datatoken balance + useEffect(() => { + initDtBalance() + }, [initDtBalance]) // Check user balance against price useEffect(() => { - if (asset?.accessDetails?.type === 'free') setIsBalanceSufficient(true) + if (asset?.accessDetails?.type === 'free') { + setIsBalanceSufficient(true) + return + } + if ( !asset?.accessDetails?.price || !asset?.accessDetails?.baseToken?.symbol || @@ -140,15 +156,16 @@ export default function AssetActions({ balance, asset?.accessDetails?.baseToken?.symbol ) - setIsBalanceSufficient( + const isSufficient = compareAsBN(baseTokenBalance, `${asset?.accessDetails.price}`) || - Number(dtBalance) >= 1 - ) + Number(dtBalance) >= 1 + + if (isMounted()) setIsBalanceSufficient(isSufficient) return () => { - setIsBalanceSufficient(false) + if (isMounted()) setIsBalanceSufficient(false) } - }, [balance, accountId, asset?.accessDetails, dtBalance]) + }, [balance, accountId, asset?.accessDetails, dtBalance, isMounted]) return (
@@ -158,9 +175,9 @@ export default function AssetActions({ isBalanceSufficient={isBalanceSufficient} file={fileMetadata} fileIsLoading={fileIsLoading} - accessDetails={asset.accessDetails} // Ensure this is passed + accessDetails={asset.accessDetails} />
) -} +}) diff --git a/src/components/Asset/AssetContent/MetaMain/MetaAsset.tsx b/src/components/Asset/AssetContent/MetaMain/MetaAsset.tsx index 185698546..7bd8771c7 100644 --- a/src/components/Asset/AssetContent/MetaMain/MetaAsset.tsx +++ b/src/components/Asset/AssetContent/MetaMain/MetaAsset.tsx @@ -4,7 +4,7 @@ import AddToken from '@shared/AddToken' import ExplorerLink from '@shared/ExplorerLink' import Publisher from '@shared/Publisher' import React, { ReactElement } from 'react' -import { useAccount } from 'wagmi' +import { useAccount, useNetwork } from 'wagmi' import styles from './MetaAsset.module.css' import { getOceanConfig } from '@utils/ocean' @@ -15,7 +15,9 @@ export default function MetaAsset({ asset: AssetExtended isBlockscoutExplorer: boolean }): ReactElement { - const oceanConfig = getOceanConfig(11155111) // replace chainId with actual id + const { chain } = useNetwork() + const chainId = chain?.id || 11155111 + const oceanConfig = getOceanConfig(chainId) const symbol = oceanConfig.oceanTokenSymbol const { isAssetNetwork } = useAsset() const { connector: activeConnector } = useAccount() diff --git a/src/components/Asset/Edit/FormEditMetadata.tsx b/src/components/Asset/Edit/FormEditMetadata.tsx index 29e258787..3e93750d3 100644 --- a/src/components/Asset/Edit/FormEditMetadata.tsx +++ b/src/components/Asset/Edit/FormEditMetadata.tsx @@ -95,6 +95,9 @@ export default function FormEditMetadata({ {...getFieldContent('price', data)} component={Input} name="price" + type="number" + min={process.env.NEXT_PUBLIC_MIN_ASSET_PRICE} + step="any" /> )} diff --git a/src/components/Profile/Header/Stats.tsx b/src/components/Profile/Header/Stats.tsx index 5f5fae196..24a8e28c0 100644 --- a/src/components/Profile/Header/Stats.tsx +++ b/src/components/Profile/Header/Stats.tsx @@ -48,7 +48,7 @@ export default function Stats({ totalSales > 0 ? ( ) : ( diff --git a/src/components/Publish/Preview/index.tsx b/src/components/Publish/Preview/index.tsx index 3cdf5a95f..04a97c546 100644 --- a/src/components/Publish/Preview/index.tsx +++ b/src/components/Publish/Preview/index.tsx @@ -22,8 +22,8 @@ export default function Preview(): ReactElement { price: `${values.pricing.price}`, baseToken: { address: ZERO_ADDRESS, - name: values.pricing?.baseToken?.symbol || 'OCEAN', - symbol: values.pricing?.baseToken?.symbol || 'OCEAN' + name: values.pricing?.baseToken?.symbol || 'WETH', + symbol: values.pricing?.baseToken?.symbol || 'WETH' }, datatoken: { address: ZERO_ADDRESS, diff --git a/src/components/Publish/Pricing/Price.tsx b/src/components/Publish/Pricing/Price.tsx index b3aac60d6..6c71be421 100644 --- a/src/components/Publish/Pricing/Price.tsx +++ b/src/components/Publish/Pricing/Price.tsx @@ -36,7 +36,7 @@ export default function Price({
1 ? ( diff --git a/src/components/Publish/Pricing/index.tsx b/src/components/Publish/Pricing/index.tsx index 4043b6ee2..0b5fa1d68 100644 --- a/src/components/Publish/Pricing/index.tsx +++ b/src/components/Publish/Pricing/index.tsx @@ -21,7 +21,7 @@ export default function PricingFields(): ReactElement { const defaultBaseToken = approvedBaseTokens?.find((token) => - token.name.toLowerCase().includes('ocean') + token.name.toLowerCase().includes('weth') ) || approvedBaseTokens?.[0] const isBaseTokenSet = !!approvedBaseTokens?.find( diff --git a/src/components/Publish/Steps.tsx b/src/components/Publish/Steps.tsx index 317162b21..d5c8aaaa0 100644 --- a/src/components/Publish/Steps.tsx +++ b/src/components/Publish/Steps.tsx @@ -32,7 +32,9 @@ export function Steps({ const defaultBaseToken = approvedBaseTokens?.find((token) => - token.name.toLowerCase().includes('ocean') + token.name + .toLowerCase() + .includes(process.env.NEXT_PUBLIC_OCEAN_TOKEN_SYMBOL.toLowerCase()) ) || approvedBaseTokens?.[0] const isBaseTokenSet = !!approvedBaseTokens?.find( (token) => token?.address === values?.pricing?.baseToken?.address diff --git a/src/components/Publish/_constants.tsx b/src/components/Publish/_constants.tsx index d0ed49777..b46a73c4e 100644 --- a/src/components/Publish/_constants.tsx +++ b/src/components/Publish/_constants.tsx @@ -75,7 +75,7 @@ export const initialValues: FormPublishData = { } ], pricing: { - baseToken: { address: '', name: '', symbol: 'OCEAN', decimals: 18 }, + baseToken: { address: '', name: '', symbol: 'WETH', decimals: 18 }, price: 0, type: allowFixedPricing === 'true' ? 'fixed' : 'free', freeAgreement: false diff --git a/src/components/Publish/_validation.ts b/src/components/Publish/_validation.ts index 934de8bc9..5b30d6438 100644 --- a/src/components/Publish/_validation.ts +++ b/src/components/Publish/_validation.ts @@ -8,7 +8,9 @@ import { validationConsumerParameters } from '@components/@shared/FormInput/Inpu // TODO: conditional validation // e.g. when algo is selected, Docker image is required // hint, hint: https://github.com/jquense/yup#mixedwhenkeys-string--arraystring-builder-object--value-schema-schema-schema - +const minAssetPrice = parseFloat( + process.env.NEXT_PUBLIC_MIN_ASSET_PRICE || '0.0001' +) const validationMetadata = { type: Yup.string() .matches(/dataset|algorithm/g, { excludeEmptyString: true }) @@ -112,7 +114,10 @@ const validationPricing = { // https://github.com/jquense/yup#mixedwhenkeys-string--arraystring-builder-object--value-schema-schema-schema price: Yup.number() - .min(1, (param: { min: number }) => `Must be more or equal to ${param.min}`) + .min( + minAssetPrice, + (param: { min: number }) => `Must be more or equal to ${param.min}` + ) .max( 1000000, (param: { max: number }) => `Must be less than or equal to ${param.max}`