diff --git a/app.config.js b/app.config.js index c29bbcad3..aff7f569a 100644 --- a/app.config.js +++ b/app.config.js @@ -20,6 +20,8 @@ module.exports = { infuraProjectId: process.env.NEXT_PUBLIC_INFURA_PROJECT_ID || 'xxx', + defaultDatatokenCap: + '115792089237316195423570985008687907853269984665640564039457', defaultDatatokenTemplateIndex: 2, // The ETH address the marketplace fee will be sent to. marketFeeAddress: diff --git a/content/pages/edit.json b/content/pages/edit.json index 0755221bf..3e1761538 100644 --- a/content/pages/edit.json +++ b/content/pages/edit.json @@ -1,3 +1,3 @@ { - "description": "Updating metadata or updating compute settings will create an on-chain transaction you have to approve in your wallet." + "description": "Updating metadata or updating services will create an on-chain transaction you have to approve in your wallet." } diff --git a/content/pages/editComputeDataset.json b/content/pages/editComputeDataset.json index 92388dc1c..d0f1201e9 100644 --- a/content/pages/editComputeDataset.json +++ b/content/pages/editComputeDataset.json @@ -2,8 +2,6 @@ "form": { "title": "Set allowed algorithms", "description": "Only the algorithms selected here will be allowed to run on your dataset. Uncheck all to remove any access to your dataset.", - "success": "🎉 Successfully updated. 🎉\n\nUpdates might not show up right away on your asset. In this case, wait some seconds and reload your asset details page in your browser.", - "error": "Updating DDO failed.", "data": [ { "name": "publisherTrustedAlgorithms", diff --git a/content/pages/editMetadata.json b/content/pages/editMetadata.json index b39d57164..6783763d1 100644 --- a/content/pages/editMetadata.json +++ b/content/pages/editMetadata.json @@ -19,99 +19,12 @@ "required": true }, { - "name": "price", - "label": "New Price", - "type": "number", - "min": "1", - "placeholder": "0", - "help": "Enter a new price.", - "required": true - }, - { - "name": "files", - "label": "File", - "prominentHelp": false, - "type": "tabs", - "fields": [ - { - "value": "ipfs", - "title": "IPFS", - "label": "CID", - "placeholder": "e.g. bafkreidgvpkjawlxz6sffxzwgooowe5yt7i6wsyg236mfoks77nywkptdq", - "help": "This CID will be stored encrypted after publishing.", - "computeHelp": "For a compute dataset, your file should match the file type required by the algorithm, and should not exceed 1 GB in file size. ", - "prominentHelp": true, - "type": "files", - "required": true - }, - { - "value": "arweave", - "title": "Arweave", - "label": "Transaction ID", - "placeholder": "e.g. DBRCL94j3QqdPaUtt4VWRen8rZfJZBb7Ey40iMpXfhtd", - "help": "This Transaction ID will be stored encrypted after publishing.", - "computeHelp": "For a compute dataset, your file should match the file type required by the algorithm, and should not exceed 1 GB in file size. ", - "prominentHelp": true, - "type": "files", - "required": true - }, - { - "value": "url", - "title": "URL", - "label": "File", - "placeholder": "e.g. https://file.com/file.json", - "help": "This URL will be stored encrypted after publishing. **Please make sure that the endpoint is accessible over the internet and is not protected by a firewall or by credentials.**", - "computeHelp": "For a compute dataset, your file should match the file type required by the algorithm, and should not exceed 1 GB in file size. ", - "prominentHelp": true, - "type": "files", - "required": true, - "innerFields": [ - { - "value": "headers", - "title": "Headers", - "label": "Headers", - "placeholder_value": "Authorization", - "help": "This HEADERS will be stored encrypted after publishing.", - "type": "headers", - "required": true - } - ] - }, - { - "value": "graphql", - "title": "Graphql", - "label": "URL", - "placeholder": "e.g. http://172.15.0.15:8000/subgraphs/name/oceanprotocol/ocean-subgraph", - "help": "This URL will be stored encrypted after publishing.", - "computeHelp": "For a compute dataset, your file should match the file type required by the algorithm, and should not exceed 1 GB in file size. ", - "prominentHelp": true, - "type": "files", - "required": true, - "headers": true, - "innerFields": [ - { - "value": "headers", - "title": "Headers", - "label": "Headers", - "placeholder_value": "Authorization", - "help": "This HEADERS will be stored encrypted after publishing.", - "type": "headers", - "required": true - }, - { - "value": "query", - "title": "Query", - "label": "Query", - "placeholder": "query{\n nfts(\n orderBy: createdTimestamp,\n orderDirection:desc\n ){\n id\n symbol\n createdTimestamp\n }\n}", - "help": "This QUERY will be stored encrypted after publishing.", - "type": "codeeditor", - "required": true - } - ] - } - ], - "sortOptions": false, - "required": true + "name": "type", + "label": "Asset Type", + "type": "boxSelection", + "options": ["Dataset", "Algorithm"], + "disabled": true, + "help": "The asset type cannot be changed once set." }, { "name": "links", @@ -133,16 +46,6 @@ ], "required": false }, - - { - "name": "timeout", - "label": "Timeout", - "help": "Define how long buyers should be able to download the dataset again after the initial purchase.", - "type": "select", - "options": ["Forever", "1 day", "1 week", "1 month", "1 year"], - "sortOptions": false, - "required": true - }, { "name": "tags", "label": "New Tags", @@ -172,13 +75,6 @@ "help": "Enter an ETH address and click the ADD button to append to the list. If an ETH address is in the deny list, download or compute of this asset will be denied for that ETH address.", "type": "credentials" }, - { - "name": "paymentCollector", - "label": "Payment Collector Address", - "placeholder": "e.g. 0X123ABC...", - "help": "This address will receive the revenue from all sales. More info available in our [docs](https://docs.oceanprotocol.com/core-concepts/datanft-and-datatoken#revenue).", - "required": false - }, { "name": "assetState", "label": "Asset Status", @@ -193,14 +89,6 @@ "sortOptions": false, "required": false }, - { - "name": "usesServiceConsumerParameters", - "label": "User defined parameters", - "help": "User defined parameters are used to filter or query the published asset.", - "type": "checkbox", - "options": ["This asset uses user defined parameters"], - "required": false - }, { "name": "license", "label": "License", diff --git a/content/pages/editService.json b/content/pages/editService.json new file mode 100644 index 000000000..1c0dba0cf --- /dev/null +++ b/content/pages/editService.json @@ -0,0 +1,168 @@ +{ + "form": { + "success": "🎉 Successfully updated. 🎉\n\nUpdates might not show up right away on your asset. In this case, wait some seconds and reload your asset details page in your browser.", + "error": "Updating DDO failed.", + "data": [ + { + "name": "name", + "label": "Service Name", + "placeholder": "Service 1", + "help": "Enter a concise title.", + "required": false + }, + { + "name": "description", + "label": "Service Description", + "help": "Enter a detailed description.", + "type": "textarea", + "rows": 2, + "required": false + }, + { + "name": "access", + "label": "Access Type", + "help": "Choose how you want your files to be accessible for the specified price.", + "type": "boxSelection", + "options": ["Download", "Compute"], + "required": true, + "disclaimer": "Please do not provide downloadable personal data without the consent of the data subjects.", + "disclaimerValues": ["access"] + }, + { + "name": "price", + "label": "New Price", + "type": "number", + "min": "1", + "placeholder": "0", + "help": "Enter a new price.", + "required": true + }, + { + "name": "providerUrl", + "label": "Provider URL", + "type": "providerUrl", + "help": "Enter the URL for your custom [provider](https://github.com/oceanprotocol/provider/) or leave as is to use the default one. If you change your provider URL after adding your file, please add & validate your file again.", + "placeholder": "e.g. https://provider.oceanprotocol.com/", + "required": true + }, + { + "name": "files", + "label": "File", + "prominentHelp": false, + "type": "tabs", + "fields": [ + { + "value": "ipfs", + "title": "IPFS", + "label": "CID", + "placeholder": "e.g. bafkreidgvpkjawlxz6sffxzwgooowe5yt7i6wsyg236mfoks77nywkptdq", + "help": "This CID will be stored encrypted after publishing.", + "computeHelp": "For a compute dataset, your file should match the file type required by the algorithm, and should not exceed 1 GB in file size. ", + "prominentHelp": true, + "type": "files", + "required": true + }, + { + "value": "arweave", + "title": "Arweave", + "label": "Transaction ID", + "placeholder": "e.g. DBRCL94j3QqdPaUtt4VWRen8rZfJZBb7Ey40iMpXfhtd", + "help": "This Transaction ID will be stored encrypted after publishing.", + "computeHelp": "For a compute dataset, your file should match the file type required by the algorithm, and should not exceed 1 GB in file size. ", + "prominentHelp": true, + "type": "files", + "required": true + }, + { + "value": "url", + "title": "URL", + "label": "File", + "placeholder": "e.g. https://file.com/file.json", + "help": "This URL will be stored encrypted after publishing. **Please make sure that the endpoint is accessible over the internet and is not protected by a firewall or by credentials.**", + "computeHelp": "For a compute dataset, your file should match the file type required by the algorithm, and should not exceed 1 GB in file size. ", + "prominentHelp": true, + "type": "files", + "required": true, + "innerFields": [ + { + "value": "headers", + "title": "Headers", + "label": "Headers", + "placeholder_value": "Authorization", + "help": "This HEADERS will be stored encrypted after publishing.", + "type": "headers", + "required": true + } + ] + }, + { + "value": "graphql", + "title": "Graphql", + "label": "URL", + "placeholder": "e.g. http://172.15.0.15:8000/subgraphs/name/oceanprotocol/ocean-subgraph", + "help": "This URL will be stored encrypted after publishing.", + "computeHelp": "For a compute dataset, your file should match the file type required by the algorithm, and should not exceed 1 GB in file size. ", + "prominentHelp": true, + "type": "files", + "required": true, + "headers": true, + "innerFields": [ + { + "value": "headers", + "title": "Headers", + "label": "Headers", + "placeholder_value": "Authorization", + "help": "This HEADERS will be stored encrypted after publishing.", + "type": "headers", + "required": true + }, + { + "value": "query", + "title": "Query", + "label": "Query", + "placeholder": "query{\n nfts(\n orderBy: createdTimestamp,\n orderDirection:desc\n ){\n id\n symbol\n createdTimestamp\n }\n}", + "help": "This QUERY will be stored encrypted after publishing.", + "type": "codeeditor", + "required": true + } + ] + } + ], + "sortOptions": false, + "required": true + }, + { + "name": "timeout", + "label": "Timeout", + "help": "Define how long buyers should be able to download the dataset again after the initial purchase.", + "type": "select", + "options": ["Forever", "1 day", "1 week", "1 month", "1 year"], + "sortOptions": false, + "required": true + }, + { + "name": "usesConsumerParameters", + "label": "Algorithm custom parameters", + "help": "Algorithm custom parameters are used to define required consumer input before running the algorithm in a Compute-to-Data environment.", + "type": "checkbox", + "options": ["This asset uses algorithm custom parameters"], + "required": false + }, + { + "name": "paymentCollector", + "label": "Payment Collector Address", + "placeholder": "e.g. 0X123ABC...", + "help": "This address will receive the revenue from all sales. More info available in our [docs](https://docs.oceanprotocol.com/core-concepts/datanft-and-datatoken#revenue).", + "required": false + }, + { + "name": "usesServiceConsumerParameters", + "label": "User defined parameters", + "help": "User defined parameters are used to filter or query the published asset.", + "type": "checkbox", + "options": ["This asset uses user defined parameters"], + "required": false + } + ] + } +} diff --git a/src/@context/Asset.tsx b/src/@context/Asset.tsx index 877995e2f..53f8e29cc 100644 --- a/src/@context/Asset.tsx +++ b/src/@context/Asset.tsx @@ -135,9 +135,7 @@ function AssetProvider({ const accessDetails = await Promise.all( asset.services.map((service: Service) => - getAccessDetails( - asset.offchain?.stats.services.find((s) => s.serviceId === service.id) - ) + getAccessDetails(asset.chainId, service) ) ) @@ -146,7 +144,7 @@ function AssetProvider({ accessDetails })) LoggerInstance.log(`[asset] Got access details for ${did}`, accessDetails) - }, [asset?.chainId, asset?.offchain?.stats.services, asset?.services, did]) + }, [asset?.chainId, asset?.services, did]) // ----------------------------------- // 1. Get and set asset based on passed DID diff --git a/src/@types/Price.d.ts b/src/@types/Price.d.ts index 7d809e767..e76cf541a 100644 --- a/src/@types/Price.d.ts +++ b/src/@types/Price.d.ts @@ -48,6 +48,7 @@ declare global { validOrderTx: string publisherMarketOrderFee: string validProviderFees?: ProviderFees + paymentCollector: string } interface PricePublishOptions { diff --git a/src/@utils/accessDetailsAndPricing.ts b/src/@utils/accessDetailsAndPricing.ts index e3c55d73c..333e74861 100644 --- a/src/@utils/accessDetailsAndPricing.ts +++ b/src/@utils/accessDetailsAndPricing.ts @@ -1,5 +1,7 @@ import { AssetPrice, + Datatoken, + FixedRateExchange, getErrorMessage, LoggerInstance, ProviderFees, @@ -16,6 +18,7 @@ import { } from '../../app.config' import { Signer } from 'ethers' import { toast } from 'react-toastify' +import { getDummySigner } from './wallet' /** * This will be used to get price including fees before ordering @@ -108,18 +111,20 @@ export async function getOrderPriceAndFees( /** * @param {number} chainId - * @param {string} datatokenAddress - * @param {number} timeout timout of the service, this is needed to return order details - * @param {string} account account that wants to buy, is needed to return order details + * @param {Service} service service of which you want access details to * @returns {Promise} */ export async function getAccessDetails( - serviceStat: ServiceStat | undefined + chainId: number, + service: Service ): Promise { + const signer = await getDummySigner(chainId) + const datatoken = new Datatoken(signer, chainId) + const { datatokenAddress } = service + const accessDetails: AccessDetails = { type: 'NOT_SUPPORTED', price: '0', - templateId: 0, addressOrId: '', baseToken: { address: '', @@ -128,55 +133,54 @@ export async function getAccessDetails( decimals: 0 }, datatoken: { - address: '', - name: '', - symbol: '', + address: datatokenAddress, + name: await datatoken.getName(datatokenAddress), + symbol: await datatoken.getSymbol(datatokenAddress), decimals: 0 }, + paymentCollector: await datatoken.getPaymentCollector(datatokenAddress), + // TODO these 5 records + templateId: 1, isOwned: false, - validOrderTx: '', - isPurchasable: false, + validOrderTx: '', // should be possible to get from ocean-node - orders collection in typesense + isPurchasable: true, publisherMarketOrderFee: '0' } - if (serviceStat === undefined || serviceStat.prices.length === 0) { - return accessDetails + // if there is at least 1 dispenser => service is free and use first dispenser + const dispensers = await datatoken.getDispensers(datatokenAddress) + if (dispensers.length > 0) { + return { + ...accessDetails, + type: 'free', + addressOrId: dispensers[0], + price: '0' + } } - const tokenPrice = serviceStat.prices[0] // support only 1 price for now + // if there is 0 dispensers and at least 1 fixed rate => use first fixed rate to get the price details + const fixedRates = await datatoken.getFixedRates(datatokenAddress) + if (fixedRates.length > 0) { + const freAddress = fixedRates[0].contractAddress + const exchangeId = fixedRates[0].id + const fre = new FixedRateExchange(freAddress, signer) + const exchange = await fre.getExchange(exchangeId) - if (tokenPrice.type === 'dispenser') { - accessDetails.type = 'free' - accessDetails.addressOrId = tokenPrice.contract - accessDetails.price = '0' - } else if (tokenPrice.type === 'fixedrate') { - accessDetails.type = 'fixed' - accessDetails.addressOrId = tokenPrice.exchangeId - accessDetails.price = tokenPrice.price - accessDetails.baseToken = { - address: tokenPrice.token.address, - name: tokenPrice.token.name, - symbol: tokenPrice.token.symbol, - decimals: tokenPrice.token.decimals + return { + ...accessDetails, + type: 'fixed', + addressOrId: exchangeId, + price: exchange.fixedRate, + baseToken: { + address: exchange.baseToken, + name: await datatoken.getName(exchange.baseToken), // reuse the datatoken instance since it is ERC20 + symbol: await datatoken.getSymbol(exchange.baseToken), + decimals: parseInt(exchange.btDecimals) + } } - } else { - // unsupported type - return accessDetails - } - - accessDetails.datatoken = { - address: serviceStat.datatokenAddress, - name: serviceStat.name, - symbol: serviceStat.symbol } - // TODO - accessDetails.templateId = 1 - accessDetails.isPurchasable = true - accessDetails.isOwned = false - accessDetails.validOrderTx = '' // should be possible to get from ocean-node - orders collection in typesense - accessDetails.publisherMarketOrderFee = '0' - + // no dispensers and no fixed rates => service doesn't have price set up return accessDetails } diff --git a/src/@utils/ddo.ts b/src/@utils/ddo.ts index 3366c8e45..30009c4c6 100644 --- a/src/@utils/ddo.ts +++ b/src/@utils/ddo.ts @@ -1,6 +1,6 @@ import { - ComputeEditForm, - MetadataEditForm + MetadataEditForm, + ServiceEditForm } from '@components/Asset/Edit/_types' import { FormConsumerParameter, @@ -25,6 +25,7 @@ export function isValidDid(did: string): boolean { return regex.test(did) } +// TODO: this function doesn't make sense, since market is now supporting multiple services. We should remove it after checking all the flows where it's being used. export function getServiceByName( ddo: Asset | DDO, name: 'access' | 'compute' @@ -171,25 +172,12 @@ export function normalizeFile( } export function previewDebugPatch( - values: FormPublishData | Partial | ComputeEditForm, - chainId: number + values: FormPublishData | MetadataEditForm | ServiceEditForm ) { // handle file's object property dynamically // without braking Yup and type validation const buildValuesPreview = JSON.parse(JSON.stringify(values)) - // fallback for edit mode under "edit compute settings" - if (!buildValuesPreview.services) return buildValuesPreview - - const valuesService = buildValuesPreview.services - ? buildValuesPreview.services[0] - : buildValuesPreview - valuesService.files[0] = normalizeFile( - valuesService.files[0].type, - valuesService.files[0], - chainId - ) - return buildValuesPreview } diff --git a/src/@utils/dispenser.ts b/src/@utils/dispenser.ts deleted file mode 100644 index e4eec5056..000000000 --- a/src/@utils/dispenser.ts +++ /dev/null @@ -1,43 +0,0 @@ -import { LoggerInstance, Datatoken } from '@oceanprotocol/lib' -import { Signer, ethers } from 'ethers' - -export async function setMinterToPublisher( - signer: Signer, - datatokenAddress: string, - accountId: string, - setError: (msg: string) => void -): Promise { - const datatokenInstance = new Datatoken(signer) - - const response = await datatokenInstance.removeMinter( - datatokenAddress, - accountId, - accountId - ) - - if (!response) { - setError('Updating DDO failed.') - LoggerInstance.error('Failed at cancelMinter') - } - return response -} - -export async function setMinterToDispenser( - signer: Signer, - datatokenAddress: string, - accountId: string, - setError: (msg: string) => void -): Promise { - const datatokenInstance = new Datatoken(signer) - - const response = await datatokenInstance.addMinter( - datatokenAddress, - accountId, - accountId - ) - if (!response) { - setError('Updating DDO failed.') - LoggerInstance.error('Failed at makeMinter') - } - return response -} diff --git a/src/@utils/ocean/index.ts b/src/@utils/ocean/index.ts index 2dbb57470..2fc8292a9 100644 --- a/src/@utils/ocean/index.ts +++ b/src/@utils/ocean/index.ts @@ -1,6 +1,4 @@ import { ConfigHelper, Config } from '@oceanprotocol/lib' -import { ethers } from 'ethers' -import abiDatatoken from '@oceanprotocol/contracts/artifacts/contracts/templates/ERC20TemplateEnterprise.sol/ERC20TemplateEnterprise.json' /** This function takes a Config object as an input and returns a new sanitized Config object @@ -54,18 +52,3 @@ export function getDevelopmentConfig(): Config { subgraphUri: 'https://v4.subgraph.sepolia.oceanprotocol.com' } as Config } - -/** - * getPaymentCollector - returns the current paymentCollector - * @param dtAddress datatoken address - * @param provider the ethers.js web3 provider - * @return {Promise} - */ -export async function getPaymentCollector( - dtAddress: string, - provider: ethers.providers.Provider -): Promise { - const dtContract = new ethers.Contract(dtAddress, abiDatatoken.abi, provider) - const paymentCollector = await dtContract.getPaymentCollector() - return paymentCollector -} diff --git a/src/components/@shared/FormInput/InputElement/BoxSelection/index.tsx b/src/components/@shared/FormInput/InputElement/BoxSelection/index.tsx index bd3476e94..93c951da6 100644 --- a/src/components/@shared/FormInput/InputElement/BoxSelection/index.tsx +++ b/src/components/@shared/FormInput/InputElement/BoxSelection/index.tsx @@ -38,6 +38,7 @@ export default function BoxSelection({ handleChange(event)} {...props} type="radio" diff --git a/src/components/@shared/FormInput/InputElement/index.tsx b/src/components/@shared/FormInput/InputElement/index.tsx index fd3144a73..287ca320a 100644 --- a/src/components/@shared/FormInput/InputElement/index.tsx +++ b/src/components/@shared/FormInput/InputElement/index.tsx @@ -41,6 +41,7 @@ const DefaultInput = forwardRef( ref={ref} className={cx({ input: true, [size]: size, [className]: className })} id={props.name} + onWheel={(e) => props.type === 'number' && e.target.blur()} {...props} /> ) diff --git a/src/components/Asset/AssetActions/Compute/FormComputeDataset.tsx b/src/components/Asset/AssetActions/Compute/FormComputeDataset.tsx index 91137768d..102010e88 100644 --- a/src/components/Asset/AssetActions/Compute/FormComputeDataset.tsx +++ b/src/components/Asset/AssetActions/Compute/FormComputeDataset.tsx @@ -151,11 +151,7 @@ export default function FormStartCompute({ const algoAccessDetails = await Promise.all( algorithmAsset.services.map((service) => - getAccessDetails( - algorithmAsset.offchain?.stats.services.find( - (s) => s.serviceId === service.id - ) - ) + getAccessDetails(algorithmAsset.chainId, service) ) ) diff --git a/src/components/Asset/AssetContent/MetaFull.tsx b/src/components/Asset/AssetContent/MetaFull.tsx index 596c5a244..7b6ae3eb6 100644 --- a/src/components/Asset/AssetContent/MetaFull.tsx +++ b/src/components/Asset/AssetContent/MetaFull.tsx @@ -4,8 +4,6 @@ import styles from './MetaFull.module.css' import Publisher from '@shared/Publisher' import { useAsset } from '@context/Asset' import { Asset, LoggerInstance, Datatoken } from '@oceanprotocol/lib' -import { getPaymentCollector } from '@utils/ocean' -import { useProvider } from 'wagmi' import { getDummySigner } from '@utils/wallet' export default function MetaFull({ ddo }: { ddo: Asset }): ReactElement { diff --git a/src/components/Asset/AssetContent/ServiceCard.module.css b/src/components/Asset/AssetContent/ServiceCard.module.css index 61d3788ef..6afac9103 100644 --- a/src/components/Asset/AssetContent/ServiceCard.module.css +++ b/src/components/Asset/AssetContent/ServiceCard.module.css @@ -3,7 +3,6 @@ border: var(--box-template-border-size) solid var(--box-template-border-color); cursor: pointer; padding: calc(var(--spacer) * 0.5) var(--spacer); - margin: var(--spacer) 0; } .service:hover { diff --git a/src/components/Asset/AssetContent/ServiceCard.tsx b/src/components/Asset/AssetContent/ServiceCard.tsx index 9d596ddb4..3fd2afbe7 100644 --- a/src/components/Asset/AssetContent/ServiceCard.tsx +++ b/src/components/Asset/AssetContent/ServiceCard.tsx @@ -11,6 +11,8 @@ export default function ServiceCard({ accessDetails: AccessDetails onClick: () => void }): ReactElement { + if (!accessDetails) return null + return (
Name: diff --git a/src/components/Asset/AssetContent/index.module.css b/src/components/Asset/AssetContent/index.module.css index 992dd5081..76048bcdf 100644 --- a/src/components/Asset/AssetContent/index.module.css +++ b/src/components/Asset/AssetContent/index.module.css @@ -39,3 +39,8 @@ border-top: 1px solid var(--border-color); border-bottom: 1px solid var(--border-color); } + +.servicesGrid { + display: grid; + gap: calc(var(--spacer) * 0.5); +} diff --git a/src/components/Asset/AssetContent/index.tsx b/src/components/Asset/AssetContent/index.tsx index eec3958b3..4b49c1fc4 100644 --- a/src/components/Asset/AssetContent/index.tsx +++ b/src/components/Asset/AssetContent/index.tsx @@ -81,14 +81,16 @@ export default function AssetContent({ {selectedService === undefined ? ( <>

Available services:

- {asset.services.map((service, index) => ( - setSelectedService(index)} - /> - ))} +
+ {asset.services.map((service, index) => ( + setSelectedService(index)} + /> + ))} +
) : ( () + const [error, setError] = useState() + const hasFeedback = error || success + + // add new service + async function handleSubmit(values: ServiceEditForm, resetForm: () => void) { + try { + if (!isAssetNetwork) { + setError('Please switch to the correct network.') + return + } + + // -------------------------------------------------- + // 1. Create Datatoken + // -------------------------------------------------- + const nft = new Nft(signer) + + const datatokenAddress = await nft.createDatatoken( + asset.nftAddress, + accountId, + accountId, + values.paymentCollector, + marketFeeAddress, + config.oceanTokenAddress, + publisherMarketFixedSwapFee, + defaultDatatokenCap, + 'DataToken', + 'DT', + 1 + ) + + LoggerInstance.log('Datatoken created.', datatokenAddress) + + // -------------------------------------------------- + // 2. Create Pricing + // -------------------------------------------------- + const datatoken = new Datatoken(signer) + + let pricingTransactionReceipt + if (values.price > 0) { + LoggerInstance.log( + `Creating fixed rate exchange with price ${values.price} for datatoken ${datatokenAddress}` + ) + + const freParams: FreCreationParams = { + fixedRateAddress: config.fixedRateExchangeAddress, + baseTokenAddress: config.oceanTokenAddress, + owner: accountId, + marketFeeCollector: marketFeeAddress, + baseTokenDecimals: 18, + datatokenDecimals: 18, + fixedRate: ethers.utils + .parseEther(values.price.toString()) + .toString(), + marketFee: publisherMarketFixedSwapFee, + withMint: true + } + + pricingTransactionReceipt = await datatoken.createFixedRate( + datatokenAddress, + accountId, + freParams + ) + } else { + LoggerInstance.log( + `Creating dispenser for datatoken ${datatokenAddress}` + ) + + const dispenserParams: DispenserParams = { + maxTokens: ethers.utils.parseEther('1').toString(), + maxBalance: ethers.utils.parseEther('1').toString(), + withMint: true + } + + pricingTransactionReceipt = await datatoken.createDispenser( + datatokenAddress, + accountId, + config.dispenserAddress, + dispenserParams + ) + } + + await pricingTransactionReceipt.wait() + LoggerInstance.log('Pricing scheme created.') + + // -------------------------------------------------- + // 2. Update DDO + // -------------------------------------------------- + let newFiles = asset.services[0].files // by default it could be the same file as in other services + if (values.files[0]?.url) { + const file = { + nftAddress: asset.nftAddress, + datatokenAddress, + files: [ + normalizeFile(values.files[0].type, values.files[0], chain?.id) + ] + } + + const filesEncrypted = await getEncryptedFiles( + file, + asset.chainId, + values.providerUrl.url + ) + newFiles = filesEncrypted + } + + const newService: Service = { + id: getHash(datatokenAddress + newFiles), + type: values.access, + name: values.name, + description: values.description, + files: newFiles || '', + datatokenAddress, + serviceEndpoint: values.providerUrl.url, + timeout: mapTimeoutStringToSeconds(values.timeout), + ...(values.access === 'compute' && { + compute: await transformComputeFormToServiceComputeOptions( + values, + defaultServiceComputeOptions, + asset.chainId, + newCancelToken() + ) + }), + consumerParameters: transformConsumerParameters( + values.consumerParameters + ) + } + + // update asset with new service + const updatedAsset = { ...asset } + updatedAsset.services.push(newService) + + // delete custom helper properties injected in the market so we don't write them on chain + delete (updatedAsset as AssetExtended).accessDetails + delete (updatedAsset as AssetExtended).datatokens + delete (updatedAsset as AssetExtended).stats + delete (updatedAsset as AssetExtended).offchain + + const setMetadataTx = await setNftMetadata( + updatedAsset, + accountId, + signer, + newAbortController() + ) + + if (!setMetadataTx) { + setError(content.form.error) + LoggerInstance.error(content.form.error) + return + } + // Edit succeeded + setSuccess(content.form.success) + resetForm() + } catch (error) { + LoggerInstance.error(error.message) + setError(error.message) + } + } + + return ( + { + // move user's focus to top of screen + window.scrollTo({ top: 0, left: 0, behavior: 'smooth' }) + // kick off editing + await handleSubmit(values, resetForm) + }} + > + {({ isSubmitting, values }) => + isSubmitting || hasFeedback ? ( + { + await fetchAsset() + }, + to: `/asset/${asset.id}` + }} + /> + ) : ( + <> + + + + + {debug === true && ( +
+ +
+ )} + + ) + } +
+ ) +} diff --git a/src/components/Asset/Edit/AddServiceCard.module.css b/src/components/Asset/Edit/AddServiceCard.module.css new file mode 100644 index 000000000..be1f6f0d2 --- /dev/null +++ b/src/components/Asset/Edit/AddServiceCard.module.css @@ -0,0 +1,22 @@ +.service { + border-radius: var(--box-template-border-radius); + border: var(--box-template-border-size) solid var(--box-template-border-color); + cursor: pointer; + padding: calc(var(--spacer) * 0.5) var(--spacer); + display: flex; + justify-content: center; + align-items: center; +} + +.service:hover { + box-shadow: var(--box-template-box-shadow); +} + +.title { + font-family: var(--font-family-base); + font-weight: var(--font-weight-bold); + font-size: var(--font-size-small); + margin-bottom: calc(var(--spacer) / 4); + color: var(--font-color-heading); + text-transform: uppercase; +} diff --git a/src/components/Asset/Edit/AddServiceCard.tsx b/src/components/Asset/Edit/AddServiceCard.tsx new file mode 100644 index 000000000..8ffefaed8 --- /dev/null +++ b/src/components/Asset/Edit/AddServiceCard.tsx @@ -0,0 +1,14 @@ +import { ReactElement } from 'react' +import styles from './AddServiceCard.module.css' + +export default function AddServiceCard({ + onClick +}: { + onClick: () => void +}): ReactElement { + return ( +
+ Add a new service +
+ ) +} diff --git a/src/components/Asset/Edit/DebugEditCompute.tsx b/src/components/Asset/Edit/DebugEditCompute.tsx deleted file mode 100644 index 7d3b1fa9a..000000000 --- a/src/components/Asset/Edit/DebugEditCompute.tsx +++ /dev/null @@ -1,41 +0,0 @@ -import { Asset, ServiceComputeOptions } from '@oceanprotocol/lib' -import { ReactElement, useEffect, useState } from 'react' -import DebugOutput from '@shared/DebugOutput' -import { useCancelToken } from '@hooks/useCancelToken' -import { transformComputeFormToServiceComputeOptions } from '@utils/compute' -import { ComputeEditForm } from './_types' -import { previewDebugPatch } from '@utils/ddo' - -export default function DebugEditCompute({ - values, - asset -}: { - values: ComputeEditForm - asset: Asset -}): ReactElement { - const [valuePreview, setValuePreview] = useState({}) - const [formTransformed, setFormTransformed] = - useState() - const newCancelToken = useCancelToken() - - useEffect(() => { - async function transformValues() { - const privacy = await transformComputeFormToServiceComputeOptions( - values, - asset.services[0].compute, - asset.chainId, - newCancelToken() - ) - setFormTransformed(privacy) - } - transformValues() - setValuePreview(previewDebugPatch(values, asset.chainId)) - }, [values, asset]) - - return ( - <> - - - - ) -} diff --git a/src/components/Asset/Edit/DebugEditMetadata.tsx b/src/components/Asset/Edit/DebugEditMetadata.tsx index be568eb25..de24f45af 100644 --- a/src/components/Asset/Edit/DebugEditMetadata.tsx +++ b/src/components/Asset/Edit/DebugEditMetadata.tsx @@ -1,8 +1,8 @@ -import { Asset, Credentials, Metadata, Service } from '@oceanprotocol/lib' +import { Asset, Credentials, Metadata } from '@oceanprotocol/lib' import { ReactElement, useEffect, useState } from 'react' import DebugOutput from '@shared/DebugOutput' import { MetadataEditForm } from './_types' -import { mapTimeoutStringToSeconds, previewDebugPatch } from '@utils/ddo' +import { previewDebugPatch } from '@utils/ddo' import { sanitizeUrl } from '@utils/url' import { generateCredentials, @@ -13,62 +13,60 @@ export default function DebugEditMetadata({ values, asset }: { - values: Partial + values: MetadataEditForm asset: Asset }): ReactElement { const [valuePreview, setValuePreview] = useState({}) - const linksTransformed = values.links?.length && - values.links[0].valid && [sanitizeUrl(values.links[0].url)] + const [updatedAsset, setUpdatedAsset] = useState() - const newMetadata: Metadata = { - ...asset?.metadata, - name: values.name, - description: values.description, - links: linksTransformed, - author: values.author, - tags: values.tags, - license: values.license, - additionalInformation: { - ...asset?.metadata?.additionalInformation - } - } - if (asset.metadata.type === 'algorithm') { - newMetadata.algorithm.consumerParameters = !values.usesConsumerParameters - ? undefined - : transformConsumerParameters(values.consumerParameters) - } + useEffect(() => { + function transformValues() { + const linksTransformed = values.links?.length && + values.links[0].valid && [sanitizeUrl(values.links[0].url)] - const updatedService: Service = { - ...asset?.services[0], - timeout: mapTimeoutStringToSeconds(values.timeout) - } - if (values?.service?.consumerParameters) { - updatedService.consumerParameters = transformConsumerParameters( - values.service.consumerParameters - ) - } + const newMetadata: Metadata = { + ...asset?.metadata, + name: values.name, + description: values.description, + links: linksTransformed, + author: values.author, + tags: values.tags, + license: values.license, + additionalInformation: { + ...asset?.metadata?.additionalInformation + } + } + if (asset.metadata.type === 'algorithm') { + newMetadata.algorithm.consumerParameters = + !values.usesConsumerParameters + ? undefined + : transformConsumerParameters(values.consumerParameters) + } - const updatedCredentials: Credentials = generateCredentials( - asset?.credentials, - values?.allow, - values?.deny - ) + const updatedCredentials: Credentials = generateCredentials( + asset?.credentials, + values?.allow, + values?.deny + ) - const updatedAsset: Asset = { - ...asset, - metadata: newMetadata, - services: [updatedService], - credentials: updatedCredentials - } + const tmpAsset: Asset = { + ...asset, + metadata: newMetadata, + credentials: updatedCredentials + } - // delete custom helper properties injected in the market that will not be written on chain - delete (updatedAsset as AssetExtended).accessDetails - delete (updatedAsset as AssetExtended).datatokens - delete (updatedAsset as AssetExtended).stats + // delete custom helper properties injected in the market that will not be written on chain + delete (tmpAsset as AssetExtended).accessDetails + delete (tmpAsset as AssetExtended).datatokens + delete (tmpAsset as AssetExtended).stats + delete (tmpAsset as AssetExtended).offchain - useEffect(() => { - setValuePreview(previewDebugPatch(values, asset.chainId)) - }, [asset.chainId, values]) + setUpdatedAsset(tmpAsset) + } + + transformValues() + setValuePreview(previewDebugPatch(values)) + }, [asset, values]) return ( <> diff --git a/src/components/Asset/Edit/DebugEditService.tsx b/src/components/Asset/Edit/DebugEditService.tsx new file mode 100644 index 000000000..827c9dd6a --- /dev/null +++ b/src/components/Asset/Edit/DebugEditService.tsx @@ -0,0 +1,91 @@ +import { Asset, LoggerInstance, Service } from '@oceanprotocol/lib' +import { ReactElement, useEffect, useState } from 'react' +import DebugOutput from '@shared/DebugOutput' +import { useCancelToken } from '@hooks/useCancelToken' +import { transformComputeFormToServiceComputeOptions } from '@utils/compute' +import { ServiceEditForm } from './_types' +import { + mapTimeoutStringToSeconds, + normalizeFile, + previewDebugPatch +} from '@utils/ddo' +import { getEncryptedFiles } from '@utils/provider' +import { transformConsumerParameters } from '@components/Publish/_utils' + +export default function DebugEditService({ + values, + asset, + service +}: { + values: ServiceEditForm + asset: Asset + service: Service +}): ReactElement { + const [valuePreview, setValuePreview] = useState({}) + const [updatedService, setUpdatedService] = useState() + const newCancelToken = useCancelToken() + + useEffect(() => { + async function transformValues() { + let updatedFiles = service.files + try { + if (values.files[0]?.url) { + const file = { + nftAddress: asset.nftAddress, + datatokenAddress: service.datatokenAddress, + files: [ + normalizeFile( + values.files[0].type, + values.files[0], + asset.chainId + ) + ] + } + + const filesEncrypted = await getEncryptedFiles( + file, + asset.chainId, + service.serviceEndpoint + ) + updatedFiles = filesEncrypted + } + } catch (error) { + LoggerInstance.error('Error encrypting files:', error.message) + } + + const updatedService: Service = { + ...service, + name: values.name, + description: values.description, + type: values.access, + timeout: mapTimeoutStringToSeconds(values.timeout), + files: updatedFiles, // TODO: check if this works + ...(values.access === 'compute' && { + compute: await transformComputeFormToServiceComputeOptions( + values, + service.compute, + asset.chainId, + newCancelToken() + ) + }) + } + if (values.consumerParameters) { + updatedService.consumerParameters = transformConsumerParameters( + values.consumerParameters + ) + } + + setUpdatedService(updatedService) + } + + transformValues() + setValuePreview(previewDebugPatch(values)) + }, [values, asset, newCancelToken, service]) + + return ( + <> + + + + ) +} diff --git a/src/components/Asset/Edit/EditComputeDataset.tsx b/src/components/Asset/Edit/EditComputeDataset.tsx deleted file mode 100644 index 6ae662901..000000000 --- a/src/components/Asset/Edit/EditComputeDataset.tsx +++ /dev/null @@ -1,178 +0,0 @@ -import { Formik } from 'formik' -import { ReactElement, useState } from 'react' -import FormEditComputeDataset from './FormEditComputeDataset' -import { - LoggerInstance, - ServiceComputeOptions, - Service, - Asset -} from '@oceanprotocol/lib' -import { useUserPreferences } from '@context/UserPreferences' -import styles from './index.module.css' -import Web3Feedback from '@shared/Web3Feedback' -import { useCancelToken } from '@hooks/useCancelToken' -import { getComputeSettingsInitialValues } from './_constants' -import { computeSettingsValidationSchema } from './_validation' -import content from '../../../../content/pages/editComputeDataset.json' -import { getServiceByName } from '@utils/ddo' -import { setMinterToPublisher, setMinterToDispenser } from '@utils/dispenser' -import { transformComputeFormToServiceComputeOptions } from '@utils/compute' -import { useAbortController } from '@hooks/useAbortController' -import DebugEditCompute from './DebugEditCompute' -import { useAsset } from '@context/Asset' -import EditFeedback from './EditFeedback' -import { - decodeTokenURI, - setNFTMetadataAndTokenURI, - setNftMetadata -} from '@utils/nft' -import { ComputeEditForm } from './_types' -import { useAccount, useSigner } from 'wagmi' - -export default function EditComputeDataset({ - asset -}: { - asset: AssetExtended -}): ReactElement { - const { debug } = useUserPreferences() - const { address: accountId } = useAccount() - const { data: signer } = useSigner() - const { fetchAsset, isAssetNetwork } = useAsset() - - const [success, setSuccess] = useState() - const [error, setError] = useState() - const newAbortController = useAbortController() - const newCancelToken = useCancelToken() - const hasFeedback = error || success - - async function handleSubmit(values: ComputeEditForm, resetForm: () => void) { - try { - if ( - asset?.accessDetails?.type === 'free' && - asset?.accessDetails?.isPurchasable - ) { - const tx = await setMinterToPublisher( - signer, - asset?.accessDetails?.datatoken?.address, - accountId, - setError - ) - if (!tx) return - } - const newComputeSettings: ServiceComputeOptions = - await transformComputeFormToServiceComputeOptions( - values, - asset.services[0].compute, - asset.chainId, - newCancelToken() - ) - - LoggerInstance.log( - '[edit compute settings] newComputeSettings', - newComputeSettings - ) - - const updatedService: Service = { - ...asset.services[0], - compute: newComputeSettings - } - - LoggerInstance.log( - '[edit compute settings] updatedService', - updatedService - ) - - const updatedAsset: Asset = { - ...asset, - services: [updatedService] - } - - // TODO: revert to setMetadata function - const setMetadataTx = await setNFTMetadataAndTokenURI( - updatedAsset, - accountId, - signer, - decodeTokenURI(asset.nft.tokenURI), - newAbortController() - ) - // const setMetadataTx = await setNftMetadata( - // updatedAsset, - // accountId, - // web3, - // newAbortController() - // ) - - LoggerInstance.log('[edit] setMetadata result', setMetadataTx) - - if (!setMetadataTx) { - setError(content.form.error) - LoggerInstance.error(content.form.error) - return - } else { - if (asset.accessDetails.type === 'free') { - const tx = await setMinterToDispenser( - signer, - asset?.accessDetails?.datatoken?.address, - accountId, - setError - ) - if (!tx) return - } - } - // Edit succeeded - setSuccess(content.form.success) - resetForm() - } catch (error) { - LoggerInstance.error(error.message) - setError(error.message) - } - } - - return ( - { - // move user's focus to top of screen - window.scrollTo({ top: 0, left: 0, behavior: 'smooth' }) - // kick off editing - await handleSubmit(values, resetForm) - }} - enableReinitialize - > - {({ values, isSubmitting }) => - isSubmitting || hasFeedback ? ( - { - await fetchAsset() - }, - to: `/asset/${asset.id}` - }} - /> - ) : ( - <> - - - {debug === true && ( -
- -
- )} - - ) - } -
- ) -} diff --git a/src/components/Asset/Edit/EditMetadata.tsx b/src/components/Asset/Edit/EditMetadata.tsx index 274f86bd7..cc3a6a0c4 100644 --- a/src/components/Asset/Edit/EditMetadata.tsx +++ b/src/components/Asset/Edit/EditMetadata.tsx @@ -1,38 +1,22 @@ -import { ReactElement, useState, useEffect } from 'react' +import { ReactElement, useState } from 'react' import { Formik } from 'formik' -import { - LoggerInstance, - FixedRateExchange, - Asset, - Datatoken, - Nft, - Metadata, - Service -} from '@oceanprotocol/lib' -import { validationSchema } from './_validation' +import { LoggerInstance, Asset, Nft, Metadata } from '@oceanprotocol/lib' +import { metadataValidationSchema } from './_validation' import { getInitialValues } from './_constants' import { MetadataEditForm } from './_types' import { useUserPreferences } from '@context/UserPreferences' import Web3Feedback from '@shared/Web3Feedback' import FormEditMetadata from './FormEditMetadata' -import { mapTimeoutStringToSeconds, normalizeFile } from '@utils/ddo' import styles from './index.module.css' import content from '../../../../content/pages/editMetadata.json' import { useAbortController } from '@hooks/useAbortController' import DebugEditMetadata from './DebugEditMetadata' -import { getOceanConfig, getPaymentCollector } from '@utils/ocean' import EditFeedback from './EditFeedback' import { useAsset } from '@context/Asset' -import { - decodeTokenURI, - setNftMetadata, - setNFTMetadataAndTokenURI -} from '@utils/nft' +import { setNftMetadata } from '@utils/nft' import { sanitizeUrl } from '@utils/url' -import { getEncryptedFiles } from '@utils/provider' import { assetStateToNumber } from '@utils/assetState' -import { setMinterToPublisher, setMinterToDispenser } from '@utils/dispenser' -import { useAccount, useProvider, useNetwork, useSigner } from 'wagmi' +import { useAccount, useSigner } from 'wagmi' import { transformConsumerParameters, generateCredentials @@ -46,62 +30,15 @@ export default function Edit({ const { debug } = useUserPreferences() const { fetchAsset, isAssetNetwork, assetState } = useAsset() const { address: accountId } = useAccount() - const { chain } = useNetwork() - const provider = useProvider() const { data: signer } = useSigner() const newAbortController = useAbortController() const [success, setSuccess] = useState() - const [paymentCollector, setPaymentCollector] = useState() const [error, setError] = useState() - const isComputeType = asset?.services[0]?.type === 'compute' const hasFeedback = error || success - useEffect(() => { - if (!asset || !provider) return - - async function getInitialPaymentCollector() { - try { - const paymentCollector = await getPaymentCollector( - asset.datatokens[0].address, - provider - ) - setPaymentCollector(paymentCollector) - } catch (error) { - LoggerInstance.error( - '[EditMetadata: getInitialPaymentCollector]', - error - ) - } - } - getInitialPaymentCollector() - }, [asset, provider]) - - async function updateFixedPrice(newPrice: string) { - const config = getOceanConfig(asset.chainId) - - const fixedRateInstance = new FixedRateExchange( - config.fixedRateExchangeAddress, - signer - ) - - const setPriceResp = await fixedRateInstance.setRate( - asset.accessDetails.addressOrId, - newPrice.toString() - ) - LoggerInstance.log('[edit] setFixedRate result', setPriceResp) - if (!setPriceResp) { - setError(content.form.error) - LoggerInstance.error(content.form.error) - } - } - - async function handleSubmit( - values: Partial, - resetForm: () => void - ) { + async function handleSubmit(values: MetadataEditForm, resetForm: () => void) { try { - let updatedFiles = asset.services[0].files const linksTransformed = values.links?.length && values.links[0].valid && [sanitizeUrl(values.links[0].url)] const updatedMetadata: Metadata = { @@ -124,46 +61,6 @@ export default function Edit({ : transformConsumerParameters(values.consumerParameters) } - asset?.accessDetails?.type === 'fixed' && - values.price !== asset.accessDetails.price && - (await updateFixedPrice(values.price)) - - if (values.paymentCollector !== paymentCollector) { - const datatoken = new Datatoken(signer) - await datatoken.setPaymentCollector( - asset?.datatokens[0].address, - accountId, - values.paymentCollector - ) - } - - if (values.files[0]?.url) { - const file = { - nftAddress: asset.nftAddress, - datatokenAddress: asset.services[0].datatokenAddress, - files: [ - normalizeFile(values.files[0].type, values.files[0], chain?.id) - ] - } - - const filesEncrypted = await getEncryptedFiles( - file, - asset.chainId, - asset.services[0].serviceEndpoint - ) - updatedFiles = filesEncrypted - } - const updatedService: Service = { - ...asset.services[0], - timeout: mapTimeoutStringToSeconds(values.timeout), - files: updatedFiles - } - if (values?.service?.consumerParameters) { - updatedService.consumerParameters = transformConsumerParameters( - values.service.consumerParameters - ) - } - const updatedCredentials = generateCredentials( asset?.credentials, values?.allow, @@ -175,43 +72,22 @@ export default function Edit({ ...(asset as Asset), version: '4.1.0', metadata: updatedMetadata, - services: [updatedService], credentials: updatedCredentials } - if ( - asset?.accessDetails?.type === 'free' && - asset?.accessDetails?.isPurchasable - ) { - const tx = await setMinterToPublisher( - signer, - asset?.accessDetails?.datatoken?.address, - accountId, - setError - ) - if (!tx) return - } - // delete custom helper properties injected in the market so we don't write them on chain delete (updatedAsset as AssetExtended).accessDetails delete (updatedAsset as AssetExtended).datatokens delete (updatedAsset as AssetExtended).stats - // TODO: revert to setMetadata function - const setMetadataTx = await setNFTMetadataAndTokenURI( + delete (updatedAsset as AssetExtended).offchain + + const setMetadataTx = await setNftMetadata( updatedAsset, accountId, signer, - decodeTokenURI(asset.nft.tokenURI), newAbortController() ) - // const setMetadataTx = await setNftMetadata( - // updatedAsset, - // accountId, - // signer, - // newAbortController() - // ) - console.log({ state: values.assetState, assetState }) if (values.assetState !== assetState) { const nft = new Nft(signer) @@ -228,17 +104,8 @@ export default function Edit({ setError(content.form.error) LoggerInstance.error(content.form.error) return - } else { - if (asset.accessDetails.type === 'free') { - const tx = await setMinterToDispenser( - signer, - asset?.accessDetails?.datatoken?.address, - accountId, - setError - ) - if (!tx) return - } } + // Edit succeeded setSuccess(content.form.success) resetForm() @@ -253,13 +120,10 @@ export default function Edit({ enableReinitialize initialValues={getInitialValues( asset?.metadata, - asset?.services[0], asset?.credentials, - asset?.accessDetails?.price || '0', - paymentCollector, assetState )} - validationSchema={validationSchema} + validationSchema={metadataValidationSchema} onSubmit={async (values, { resetForm }) => { // move user's focus to top of screen window.scrollTo({ top: 0, left: 0, behavior: 'smooth' }) @@ -284,11 +148,7 @@ export default function Edit({ /> ) : ( <> - + () + const [error, setError] = useState() + const hasFeedback = error || success + + async function updateFixedPrice(newPrice: number) { + const config = getOceanConfig(asset.chainId) + + const fixedRateInstance = new FixedRateExchange( + config.fixedRateExchangeAddress, + signer + ) + + const setPriceResp = await fixedRateInstance.setRate( + accessDetails.addressOrId, + newPrice.toString() + ) + LoggerInstance.log('[edit] setFixedRate result', setPriceResp) + if (!setPriceResp) { + setError(content.form.error) + LoggerInstance.error(content.form.error) + } + } + + // edit 1 service + async function handleSubmit(values: ServiceEditForm, resetForm: () => void) { + try { + // update fixed price if changed + accessDetails.type === 'fixed' && + values.price !== parseFloat(accessDetails.price) && + (await updateFixedPrice(values.price)) + + // update payment collector if changed + if (values.paymentCollector !== accessDetails.paymentCollector) { + const datatoken = new Datatoken(signer) + await datatoken.setPaymentCollector( + service.datatokenAddress, + accountId, + values.paymentCollector + ) + } + + let updatedFiles = service.files + if (values.files[0]?.url) { + const file = { + nftAddress: asset.nftAddress, + datatokenAddress: service.datatokenAddress, + files: [ + normalizeFile(values.files[0].type, values.files[0], chain?.id) + ] + } + + const filesEncrypted = await getEncryptedFiles( + file, + asset.chainId, + service.serviceEndpoint + ) + updatedFiles = filesEncrypted + } + + const updatedService: Service = { + ...service, + name: values.name, + description: values.description, + timeout: mapTimeoutStringToSeconds(values.timeout), + files: updatedFiles, // TODO: check if this works + ...(values.access === 'compute' && { + compute: await transformComputeFormToServiceComputeOptions( + values, + service.compute, + asset.chainId, + newCancelToken() + ) + }) + } + if (values.consumerParameters) { + updatedService.consumerParameters = transformConsumerParameters( + values.consumerParameters + ) + } + + // update asset with new service + const serviceIndex = asset.services.findIndex((s) => s.id === service.id) + const updatedAsset = { ...asset } + updatedAsset.services[serviceIndex] = updatedService + + // delete custom helper properties injected in the market so we don't write them on chain + delete (updatedAsset as AssetExtended).accessDetails + delete (updatedAsset as AssetExtended).datatokens + delete (updatedAsset as AssetExtended).stats + delete (updatedAsset as AssetExtended).offchain + + const setMetadataTx = await setNftMetadata( + updatedAsset, + accountId, + signer, + newAbortController() + ) + + if (!setMetadataTx) { + setError(content.form.error) + LoggerInstance.error(content.form.error) + return + } + // Edit succeeded + setSuccess(content.form.success) + resetForm() + } catch (error) { + LoggerInstance.error(error.message) + setError(error.message) + } + } + + if (!accessDetails) return null + + return ( + { + // move user's focus to top of screen + window.scrollTo({ top: 0, left: 0, behavior: 'smooth' }) + // kick off editing + await handleSubmit(values, resetForm) + }} + > + {({ isSubmitting, values }) => + isSubmitting || hasFeedback ? ( + { + await fetchAsset() + }, + to: `/asset/${asset.id}` + }} + /> + ) : ( + <> + + + + + {debug === true && ( +
+ +
+ )} + + ) + } +
+ ) +} diff --git a/src/components/Asset/Edit/EditServices.tsx b/src/components/Asset/Edit/EditServices.tsx new file mode 100644 index 000000000..ab9344830 --- /dev/null +++ b/src/components/Asset/Edit/EditServices.tsx @@ -0,0 +1,62 @@ +import { ReactElement, useState } from 'react' +import EditService from './EditService' +import Button from '@components/@shared/atoms/Button' +import AddService from './AddService' +import ServiceCard from '../AssetContent/ServiceCard' +import AddServiceCard from './AddServiceCard' +import styles from './index.module.css' + +export default function EditServices({ + asset +}: { + asset: AssetExtended +}): ReactElement { + const [selectedService, setSelectedService] = useState() // -1 is the new service, undefined is none + + return ( +
+
+ {asset.services.map((service, index) => ( + setSelectedService(index)} + /> + ))} + setSelectedService(-1)} /> +
+ + {selectedService !== undefined && ( + <> +
+ +
+

+ {selectedService === -1 + ? 'Add a new service' + : `Edit service ${asset.services[selectedService].name}`} +

+ +
+ + {selectedService === -1 ? ( + + ) : ( + + )} + + )} +
+ ) +} diff --git a/src/components/Asset/Edit/FormActions.tsx b/src/components/Asset/Edit/FormActions.tsx index 7a4807d61..eac7c9aec 100644 --- a/src/components/Asset/Edit/FormActions.tsx +++ b/src/components/Asset/Edit/FormActions.tsx @@ -4,7 +4,7 @@ import { useAsset } from '@context/Asset' import Button from '@shared/atoms/Button' import styles from './FormActions.module.css' import Link from 'next/link' -import { ComputeEditForm, MetadataEditForm } from './_types' +import { MetadataEditForm, ServiceEditForm } from './_types' export default function FormActions({ handleClick @@ -12,7 +12,7 @@ export default function FormActions({ handleClick?: () => void }): ReactElement { const { isAssetNetwork, asset } = useAsset() - const { isValid }: FormikContextType = + const { isValid }: FormikContextType = useFormikContext() const isSubmitDisabled = !isValid || !isAssetNetwork diff --git a/src/components/Asset/Edit/FormAddService.tsx b/src/components/Asset/Edit/FormAddService.tsx new file mode 100644 index 000000000..82cb9d53a --- /dev/null +++ b/src/components/Asset/Edit/FormAddService.tsx @@ -0,0 +1,120 @@ +import { ReactElement } from 'react' +import { Field, Form, useFormikContext } from 'formik' +import Input from '@shared/FormInput' +import FormActions from './FormActions' +import { getFieldContent } from '@utils/form' +import consumerParametersContent from '../../../../content/publish/consumerParameters.json' +import { ServiceEditForm } from './_types' +import IconDownload from '@images/download.svg' +import IconCompute from '@images/compute.svg' +import FormEditComputeService from './FormEditComputeService' +import { defaultServiceComputeOptions } from './_constants' +import styles from './index.module.css' + +export default function FormAddService({ + data, + chainId +}: { + data: FormFieldContent[] + chainId: number +}): ReactElement { + const { values, setFieldValue } = useFormikContext() + + const accessTypeOptionsTitles = getFieldContent('access', data).options + + const accessTypeOptions = [ + { + name: 'access-download', + value: 'access', + title: accessTypeOptionsTitles[0], + icon: , + // BoxSelection component is not a Formik component + // so we need to handle checked state manually. + checked: values.access === 'access' + }, + { + name: 'access-compute', + value: 'compute', + title: accessTypeOptionsTitles[1], + icon: , + checked: values.access === 'compute' + } + ] + + return ( +
+ + + + + + + {values.access === 'compute' && ( + + )} + + + + + + + + + + + + + {values.usesConsumerParameters && ( + + )} + + + ) +} diff --git a/src/components/Asset/Edit/FormEditComputeDataset.tsx b/src/components/Asset/Edit/FormEditComputeService.tsx similarity index 73% rename from src/components/Asset/Edit/FormEditComputeDataset.tsx rename to src/components/Asset/Edit/FormEditComputeService.tsx index 20425d0ae..a1e3300b2 100644 --- a/src/components/Asset/Edit/FormEditComputeDataset.tsx +++ b/src/components/Asset/Edit/FormEditComputeService.tsx @@ -1,5 +1,5 @@ import { ReactElement, useCallback, useEffect, useState } from 'react' -import { Field, Form, FormikContextType, useFormikContext } from 'formik' +import { Field, FormikContextType, useFormikContext } from 'formik' import Input from '@shared/FormInput' import { AssetSelectionAsset } from '@shared/FormInput/InputElement/AssetSelection' import stylesIndex from './index.module.css' @@ -8,22 +8,29 @@ import { getFilterTerm, queryMetadata } from '@utils/aquarius' -import { useAsset } from '@context/Asset' -import { PublisherTrustedAlgorithm } from '@oceanprotocol/lib' -import FormActions from './FormActions' +import { + PublisherTrustedAlgorithm, + ServiceComputeOptions +} from '@oceanprotocol/lib' import { useCancelToken } from '@hooks/useCancelToken' import { SortTermOptions } from '../../../@types/aquarius/SearchQuery' -import { getServiceByName } from '@utils/ddo' import { transformAssetToAssetSelection } from '@utils/assetConvertor' -import { ComputeEditForm } from './_types' +import { ServiceEditForm } from './_types' import content from '../../../../content/pages/editComputeDataset.json' import { getFieldContent } from '@utils/form' import { useAccount } from 'wagmi' -export default function FormEditComputeDataset(): ReactElement { - const { asset } = useAsset() +export default function FormEditComputeService({ + chainId, + serviceEndpoint, + serviceCompute +}: { + chainId: number + serviceEndpoint: string + serviceCompute: ServiceComputeOptions +}): ReactElement { const { address: accountId } = useAccount() - const { values }: FormikContextType = useFormikContext() + const { values }: FormikContextType = useFormikContext() const newCancelToken = useCancelToken() const [allAlgorithms, setAllAlgorithms] = useState() @@ -33,40 +40,34 @@ export default function FormEditComputeDataset(): ReactElement { publisherTrustedAlgorithms: PublisherTrustedAlgorithm[] ): Promise => { const baseParams = { - chainIds: [asset.chainId], + chainIds: [chainId], sort: { sortBy: SortTermOptions.Created }, filters: [getFilterTerm('metadata.type', 'algorithm')] } as BaseQueryParams const query = generateBaseQuery(baseParams) const queryResult = await queryMetadata(query, newCancelToken()) - const datasetComputeService = getServiceByName(asset, 'compute') const algorithmSelectionList = await transformAssetToAssetSelection( - datasetComputeService?.serviceEndpoint, - queryResult?.results, + serviceEndpoint, + queryResult?.results || [], accountId, publisherTrustedAlgorithms ) return algorithmSelectionList }, - [accountId, asset, newCancelToken] + [accountId, chainId, newCancelToken, serviceEndpoint] ) useEffect(() => { - if (!asset) return - - const { publisherTrustedAlgorithms } = getServiceByName( - asset, - 'compute' - ).compute + const { publisherTrustedAlgorithms } = serviceCompute getAlgorithmList(publisherTrustedAlgorithms).then((algorithms) => { setAllAlgorithms(algorithms) }) - }, [asset, getAlgorithmList]) + }, [serviceCompute, getAlgorithmList]) return ( -
+ <>

{content.form.title}

@@ -91,8 +92,6 @@ export default function FormEditComputeDataset(): ReactElement { .options } /> - - - + ) } diff --git a/src/components/Asset/Edit/FormEditMetadata.module.css b/src/components/Asset/Edit/FormEditMetadata.module.css deleted file mode 100644 index 6b4c9d07d..000000000 --- a/src/components/Asset/Edit/FormEditMetadata.module.css +++ /dev/null @@ -1,15 +0,0 @@ -.serviceContainer { - border-top: 1px solid var(--border-color); - padding-top: var(--spacer); -} - -.gdpr { - border: 1px solid var(--border-color); - padding: calc(var(--spacer) / 2); - margin-top: calc(var(--spacer) * -0.5); - margin-bottom: var(--spacer); -} - -.gdpr > div { - margin-bottom: calc(var(--spacer) / 2); -} diff --git a/src/components/Asset/Edit/FormEditMetadata.tsx b/src/components/Asset/Edit/FormEditMetadata.tsx index 4e427c11e..78041851a 100644 --- a/src/components/Asset/Edit/FormEditMetadata.tsx +++ b/src/components/Asset/Edit/FormEditMetadata.tsx @@ -3,56 +3,42 @@ import { Field, Form, useFormikContext } from 'formik' import Input from '@shared/FormInput' import FormActions from './FormActions' import { useAsset } from '@context/Asset' -import { FormPublishData } from '@components/Publish/_types' import { getFileInfo } from '@utils/provider' import { getFieldContent } from '@utils/form' import { isGoogleUrl } from '@utils/url' import { MetadataEditForm } from './_types' +import content from '../../../../content/pages/editMetadata.json' import consumerParametersContent from '../../../../content/publish/consumerParameters.json' -import styles from './FormEditMetadata.module.css' +import IconDataset from '@images/dataset.svg' +import IconAlgorithm from '@images/algorithm.svg' +import { BoxSelectionOption } from '@components/@shared/FormInput/InputElement/BoxSelection' -export function checkIfTimeoutInPredefinedValues( - timeout: string, - timeoutOptions: string[] -): boolean { - if (timeoutOptions.indexOf(timeout) > -1) { - return true - } - return false -} +const { data } = content.form +const assetTypeOptionsTitles = getFieldContent('type', data).options -export default function FormEditMetadata({ - data, - showPrice, - isComputeDataset -}: { - data: FormFieldContent[] - showPrice: boolean - isComputeDataset: boolean -}): ReactElement { +export default function FormEditMetadata(): ReactElement { const { asset } = useAsset() - const { values, setFieldValue } = useFormikContext() - - // This component is handled by Formik so it's not rendered like a "normal" react component, - // so handleTimeoutCustomOption is called only once. - // https://github.com/oceanprotocol/market/pull/324#discussion_r561132310 - // if (data && values) handleTimeoutCustomOption(data, values) - - const timeoutOptionsArray = data.filter( - (field) => field.name === 'timeout' - )[0].options as string[] - - if (isComputeDataset && timeoutOptionsArray.includes('Forever')) { - const foreverOptionIndex = timeoutOptionsArray.indexOf('Forever') - timeoutOptionsArray.splice(foreverOptionIndex, 1) - } else if (!isComputeDataset && !timeoutOptionsArray.includes('Forever')) { - timeoutOptionsArray.push('Forever') - } + const { values, setFieldValue } = useFormikContext() + + // BoxSelection component is not a Formik component + // so we need to handle checked state manually. + const assetTypeOptions: BoxSelectionOption[] = [ + { + name: assetTypeOptionsTitles[0].toLowerCase(), + title: assetTypeOptionsTitles[0], + checked: values.type === assetTypeOptionsTitles[0].toLowerCase(), + icon: + }, + { + name: assetTypeOptionsTitles[1].toLowerCase(), + title: assetTypeOptionsTitles[1], + checked: values.type === assetTypeOptionsTitles[1].toLowerCase(), + icon: + } + ] useEffect(() => { - const providerUrl = values?.services - ? values?.services[0].providerUrl.url - : asset.services[0].serviceEndpoint + const providerUrl = asset.services[0].serviceEndpoint // if we have a sample file, we need to get the files' info before setting defaults links value asset?.metadata?.links?.[0] && @@ -82,26 +68,20 @@ export default function FormEditMetadata({ return (

- - - {showPrice && ( - - )} + - - {asset.metadata.type === 'algorithm' && ( @@ -143,41 +117,19 @@ export default function FormEditMetadata({ name="allow" /> - + -
-

Service

- - {(values as unknown as MetadataEditForm).service - .usesConsumerParameters && ( - - )} -
+ ) diff --git a/src/components/Asset/Edit/FormEditService.tsx b/src/components/Asset/Edit/FormEditService.tsx new file mode 100644 index 000000000..7a4ab120a --- /dev/null +++ b/src/components/Asset/Edit/FormEditService.tsx @@ -0,0 +1,126 @@ +import { ReactElement } from 'react' +import { Field, Form, useFormikContext } from 'formik' +import Input from '@shared/FormInput' +import FormActions from './FormActions' +import { getFieldContent } from '@utils/form' +import consumerParametersContent from '../../../../content/publish/consumerParameters.json' +import { Service } from '@oceanprotocol/lib' +import { ServiceEditForm } from './_types' +import IconDownload from '@images/download.svg' +import IconCompute from '@images/compute.svg' +import FormEditComputeService from './FormEditComputeService' +import { defaultServiceComputeOptions } from './_constants' +import styles from './index.module.css' + +export default function FormEditService({ + data, + chainId, + service, + accessDetails +}: { + data: FormFieldContent[] + chainId: number + service: Service + accessDetails: AccessDetails +}): ReactElement { + const formUniqueId = service.id // because BoxSelection component is not a Formik component + const { values, setFieldValue } = useFormikContext() + + const accessTypeOptionsTitles = getFieldContent('access', data).options + + const accessTypeOptions = [ + { + name: `access-${formUniqueId}-download`, + value: 'access', + title: accessTypeOptionsTitles[0], + icon: , + // BoxSelection component is not a Formik component + // so we need to handle checked state manually. + checked: values.access === 'access' + }, + { + name: `access-${formUniqueId}-compute`, + value: 'compute', + title: accessTypeOptionsTitles[1], + icon: , + checked: values.access === 'compute' + } + ] + + return ( +
+ + + + + + + {values.access === 'compute' && ( + + )} + + + + + + + + + + + + + {values.usesConsumerParameters && ( + + )} + + + ) +} diff --git a/src/components/Asset/Edit/_constants.ts b/src/components/Asset/Edit/_constants.ts index d8827eb63..bec50ff3b 100644 --- a/src/components/Asset/Edit/_constants.ts +++ b/src/components/Asset/Edit/_constants.ts @@ -5,30 +5,31 @@ import { ServiceComputeOptions } from '@oceanprotocol/lib' import { parseConsumerParameters, secondsToString } from '@utils/ddo' -import { ComputeEditForm, MetadataEditForm } from './_types' +import { ComputeEditForm, MetadataEditForm, ServiceEditForm } from './_types' + +export const defaultServiceComputeOptions: ServiceComputeOptions = { + allowRawAlgorithm: false, + allowNetworkAccess: true, + publisherTrustedAlgorithmPublishers: [], + publisherTrustedAlgorithms: [] +} export function getInitialValues( metadata: Metadata, - service: Service, credentials: Credentials, - price: string, - paymentCollector: string, assetState: string -): Partial { +): MetadataEditForm { return { name: metadata?.name, description: metadata?.description, - price, + type: metadata?.type, links: [{ url: '', type: 'url' }], - files: [{ url: '', type: 'hidden' }], - timeout: secondsToString(service?.timeout), author: metadata?.author, tags: metadata?.tags, usesConsumerParameters: metadata?.algorithm?.consumerParameters?.length > 0, consumerParameters: parseConsumerParameters( metadata?.algorithm?.consumerParameters ), - paymentCollector, allow: credentials?.allow?.find((credential) => credential.type === 'address') ?.values || [], @@ -36,15 +37,11 @@ export function getInitialValues( credentials?.deny?.find((credential) => credential.type === 'address') ?.values || [], assetState, - service: { - usesConsumerParameters: service?.consumerParameters?.length > 0, - consumerParameters: parseConsumerParameters(service?.consumerParameters) - }, license: metadata?.license } } -export function getComputeSettingsInitialValues({ +function getComputeSettingsInitialValues({ publisherTrustedAlgorithms, publisherTrustedAlgorithmPublishers }: ServiceComputeOptions): ComputeEditForm { @@ -59,3 +56,55 @@ export function getComputeSettingsInitialValues({ publisherTrustedAlgorithmPublishers } } + +export const getNewServiceInitialValues = ( + accountId: string, + firstService: Service +): ServiceEditForm => { + const computeSettings = getComputeSettingsInitialValues( + defaultServiceComputeOptions + ) + return { + name: 'New Service', + description: '', + access: 'access', + price: 1, + paymentCollector: accountId, + providerUrl: { + url: firstService.serviceEndpoint, + valid: false, + custom: false + }, + files: [{ url: '', type: 'hidden' }], + timeout: '1 day', + usesConsumerParameters: false, + consumerParameters: [], + ...computeSettings + } +} + +export const getServiceInitialValues = ( + service: Service, + accessDetails: AccessDetails +): ServiceEditForm => { + const computeSettings = getComputeSettingsInitialValues( + service.compute || defaultServiceComputeOptions + ) + return { + name: service.name, + description: service.description, + access: service.type as 'access' | 'compute', + price: parseFloat(accessDetails.price), + paymentCollector: accessDetails.paymentCollector, + providerUrl: { + url: service.serviceEndpoint, + valid: true, + custom: false + }, + files: [{ url: '', type: 'hidden' }], + timeout: secondsToString(service.timeout), + usesConsumerParameters: service.consumerParameters?.length > 0, + consumerParameters: parseConsumerParameters(service.consumerParameters), + ...computeSettings + } +} diff --git a/src/components/Asset/Edit/_types.ts b/src/components/Asset/Edit/_types.ts index 81f9ab08f..cfe2e2385 100644 --- a/src/components/Asset/Edit/_types.ts +++ b/src/components/Asset/Edit/_types.ts @@ -4,10 +4,7 @@ import { FileInfo } from '@oceanprotocol/lib' export interface MetadataEditForm { name: string description: string - timeout: string - paymentCollector: string - price?: string - files: FileInfo[] + type: 'dataset' | 'algorithm' links?: FileInfo[] author?: string tags?: string[] @@ -16,13 +13,27 @@ export interface MetadataEditForm { allow?: string[] deny?: string[] assetState?: string - service?: { - usesConsumerParameters?: boolean - consumerParameters?: FormConsumerParameter[] - } license?: string } +export interface ServiceEditForm { + name: string + description: string + access: 'access' | 'compute' + providerUrl: { url: string; valid: boolean; custom: boolean } + price: number + paymentCollector: string + files: FileInfo[] + timeout: string + usesConsumerParameters: boolean + consumerParameters: FormConsumerParameter[] + // compute + allowAllPublishedAlgorithms: boolean + publisherTrustedAlgorithms: string[] + publisherTrustedAlgorithmPublishers: string[] +} + +// TODO delete export interface ComputeEditForm { allowAllPublishedAlgorithms: boolean publisherTrustedAlgorithms: string[] diff --git a/src/components/Asset/Edit/_validation.ts b/src/components/Asset/Edit/_validation.ts index 79579feb6..79333ec53 100644 --- a/src/components/Asset/Edit/_validation.ts +++ b/src/components/Asset/Edit/_validation.ts @@ -4,25 +4,11 @@ import { isAddress } from 'ethers/lib/utils' import { testLinks } from '@utils/yup' import { validationConsumerParameters } from '@shared/FormInput/InputElement/ConsumerParameters/_validation' -export const validationSchema = Yup.object().shape({ +export const metadataValidationSchema = Yup.object().shape({ name: Yup.string() .min(4, (param) => `Title must be at least ${param.min} characters`) .required('Required'), description: Yup.string().required('Required').min(10), - price: Yup.number().required('Required'), - files: Yup.array() - .of( - Yup.object().shape({ - url: testLinks(true), - valid: Yup.boolean().test((value, context) => { - const { type } = context.parent - // allow user to submit if the value type is hidden - if (type === 'hidden') return true - return value || false - }) - }) - ) - .nullable(), links: Yup.array().of( Yup.object().shape({ url: testLinks(true), @@ -35,7 +21,6 @@ export const validationSchema = Yup.object().shape({ }) }) ), - timeout: Yup.string().required('Required'), tags: Yup.array().nullable(), usesConsumerParameters: Yup.boolean(), consumerParameters: Yup.array().when('usesConsumerParameters', { @@ -49,6 +34,39 @@ export const validationSchema = Yup.object().shape({ }), allow: Yup.array().of(Yup.string()).nullable(), deny: Yup.array().of(Yup.string()).nullable(), + retireAsset: Yup.string() +}) + +export const serviceValidationSchema = Yup.object().shape({ + name: Yup.string() + .min(4, (param) => `Name must be at least ${param.min} characters`) + .required('Required'), + description: Yup.string().required('Required').min(10), + price: Yup.number().required('Required'), + files: Yup.array() + .of( + Yup.object().shape({ + url: testLinks(true), + valid: Yup.boolean().test((value, context) => { + const { type } = context.parent + // allow user to submit if the value type is hidden + if (type === 'hidden') return true + return value || false + }) + }) + ) + .nullable(), + timeout: Yup.string().required('Required'), + usesConsumerParameters: Yup.boolean(), + consumerParameters: Yup.array().when('usesConsumerParameters', { + is: true, + then: Yup.array() + .of(Yup.object().shape(validationConsumerParameters)) + .required('Required'), + otherwise: Yup.array() + .nullable() + .transform((value) => value || null) + }), paymentCollector: Yup.string().test( 'ValidAddress', 'Must be a valid Ethereum Address.', @@ -56,22 +74,6 @@ export const validationSchema = Yup.object().shape({ return isAddress(value) } ), - retireAsset: Yup.string(), - service: Yup.object().shape({ - usesConsumerParameters: Yup.boolean(), - consumerParameters: Yup.array().when('usesConsumerParameters', { - is: true, - then: Yup.array() - .of(Yup.object().shape(validationConsumerParameters)) - .required('Required'), - otherwise: Yup.array() - .nullable() - .transform((value) => value || null) - }) - }) -}) - -export const computeSettingsValidationSchema = Yup.object().shape({ allowAllPublishedAlgorithms: Yup.boolean().nullable(), publisherTrustedAlgorithms: Yup.array().nullable(), publisherTrustedAlgorithmPublishers: Yup.array().nullable() diff --git a/src/components/Asset/Edit/index.module.css b/src/components/Asset/Edit/index.module.css index 5af3b39e3..d5b8add14 100644 --- a/src/components/Asset/Edit/index.module.css +++ b/src/components/Asset/Edit/index.module.css @@ -17,7 +17,7 @@ @media (min-width: 60rem) { .grid { - grid-template-columns: 1.5fr 1fr; + grid-template-columns: 1fr 1fr; } } @@ -30,3 +30,32 @@ margin-bottom: calc(var(--spacer) / 8); max-width: 50rem; } + +.servicesGrid { + display: grid; + grid-template-columns: repeat(3, 1fr); + grid-auto-rows: 1fr; /* This ensures all rows have the same height */ + gap: 1rem; + margin-bottom: 20px; +} + +@media (max-width: 992px) { + .servicesGrid { + grid-template-columns: repeat(2, 1fr); + } +} +@media (max-width: 576px) { + .servicesGrid { + grid-template-columns: 1fr; + } +} + +.servicesHeader { + display: flex; + justify-content: space-between; + margin-top: 40px; +} + +.form { + margin: 20px; +} diff --git a/src/components/Asset/Edit/index.tsx b/src/components/Asset/Edit/index.tsx index ef34c62f5..07764c80e 100644 --- a/src/components/Asset/Edit/index.tsx +++ b/src/components/Asset/Edit/index.tsx @@ -3,16 +3,15 @@ import { useAsset } from '@context/Asset' import styles from './index.module.css' import Tabs from '@shared/atoms/Tabs' import EditMetadata from './EditMetadata' -import EditComputeDataset from './EditComputeDataset' import Page from '@shared/Page' import Loader from '@shared/atoms/Loader' import Alert from '@shared/atoms/Alert' import contentPage from '../../../../content/pages/edit.json' import Container from '@shared/atoms/Container' +import EditServices from './EditServices' export default function Edit({ uri }: { uri: string }): ReactElement { const { asset, error, isInPurgatory, title, isOwner } = useAsset() - const [isCompute, setIsCompute] = useState(false) const [pageTitle, setPageTitle] = useState('') const [tabIndex, setTabIndex] = useState(0) @@ -26,7 +25,6 @@ export default function Edit({ uri }: { uri: string }): ReactElement { : `Edit ${title}` setPageTitle(pageTitle) - setIsCompute(asset?.services[0]?.type === 'compute') }, [asset, isInPurgatory, title, isOwner]) const tabs = [ @@ -34,15 +32,11 @@ export default function Edit({ uri }: { uri: string }): ReactElement { title: 'Edit Metadata', content: }, - ...[ - isCompute && asset?.metadata.type !== 'algorithm' - ? { - title: 'Edit Compute Settings', - content: - } - : undefined - ] - ].filter((tab) => tab !== undefined) + { + title: 'Edit Services', + content: + } + ] return ( diff --git a/src/components/Publish/Debug/index.tsx b/src/components/Publish/Debug/index.tsx index ad9f0d4d9..c84e058b2 100644 --- a/src/components/Publish/Debug/index.tsx +++ b/src/components/Publish/Debug/index.tsx @@ -6,18 +6,16 @@ import { transformPublishFormToDdo } from '../_utils' import styles from './index.module.css' import { DDO } from '@oceanprotocol/lib' import { previewDebugPatch } from '@utils/ddo' -import { useNetwork } from 'wagmi' export default function Debug(): ReactElement { const { values } = useFormikContext() const [valuePreview, setValuePreview] = useState({}) const [ddo, setDdo] = useState() - const { chain } = useNetwork() useEffect(() => { async function makeDdo() { const ddo = await transformPublishFormToDdo(values) - setValuePreview(previewDebugPatch(values, chain?.id)) + setValuePreview(previewDebugPatch(values)) setDdo(ddo) } makeDdo() diff --git a/src/components/Publish/_utils.ts b/src/components/Publish/_utils.ts index 8cd79dede..581305b07 100644 --- a/src/components/Publish/_utils.ts +++ b/src/components/Publish/_utils.ts @@ -32,7 +32,8 @@ import { publisherMarketFixedSwapFee, defaultDatatokenTemplateIndex, customProviderUrl, - defaultAccessTerms + defaultAccessTerms, + defaultDatatokenCap } from '../../../app.config' import { sanitizeUrl } from '@utils/url' import { getContainerChecksum } from '@utils/docker' @@ -292,7 +293,7 @@ export async function createTokensAndPricing( values.pricing.baseToken.address, feeAmount: publisherMarketOrderFee, // max number - cap: '115792089237316195423570985008687907853269984665640564039457', + cap: defaultDatatokenCap, name: values.services[0].dataTokenOptions.name, symbol: values.services[0].dataTokenOptions.symbol }