1
0
mirror of https://github.com/oceanprotocol/market.git synced 2024-06-30 05:41:41 +02:00

Merge pull request #933 from oceanprotocol/v4-publish

new publish form: setup & data collection & live validation
This commit is contained in:
Matthias Kretschmann 2021-11-30 10:21:24 +00:00 committed by GitHub
commit 8b47cf2275
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
213 changed files with 5683 additions and 4544 deletions

2
.gitignore vendored
View File

@ -13,4 +13,4 @@ networks-metadata.json
src/@types/apollo
graphql.schema.json
src/@types/graph.types.ts
public
tsconfig.tsbuildinfo

View File

@ -1,111 +0,0 @@
{
"title": "Publish an Algorithm",
"data": [
{
"name": "name",
"label": "Title",
"placeholder": "e.g. Shapes of Desert Plants",
"help": "Enter a concise title.",
"required": true
},
{
"name": "description",
"label": "Description",
"help": "Add a thorough description with as much detail as possible. You can use [Markdown](https://daringfireball.net/projects/markdown/basics). You can change the description at any time. If you provide personal data, please note that it will remain in the transaction history. For more information on how personal data is handled within the metadata, please refer to our [privacy policy](/privacy/en).",
"type": "textarea",
"required": true
},
{
"name": "files",
"label": "File",
"placeholder": "e.g. https://file.com/file.json",
"help": "Please enter the URL to your algorithm file and click \"ADD FILE\" to validate the data. This URL will be stored encrypted after publishing. Some restrictions apply:\n\n- max. running time: 1 min.\n- [Writing Algorithms for Compute to Data](https://docs.oceanprotocol.com/tutorials/compute-to-data-algorithms/)",
"type": "files",
"required": true
},
{
"name": "dockerImage",
"label": "Docker Image",
"placeholder": "e.g. python3.7",
"help": "Please select an image to run your algorithm.",
"type": "boxSelection",
"options": [],
"required": true
},
{
"name": "image",
"label": "Image URL",
"placeholder": "e.g. oceanprotocol/algo_dockers or https://example.com/image_path",
"help": "Provide the name of a public Docker image or the full url if you have it hosted in a 3rd party repo",
"required": false
},
{
"name": "containerTag",
"label": "Docker Image Tag",
"placeholder": "e.g. latest",
"help": "Provide the tag for your Docker image.",
"required": false
},
{
"name": "timeout",
"label": "Timeout",
"help": "Define how long buyers should be able to download the algorithm again after the initial purchase.",
"placeholder": "Forever",
"type": "select",
"options": ["Forever", "1 day", "1 week", "1 month", "1 year"],
"sortOptions": false,
"required": true
},
{
"name": "dataTokenOptions",
"label": "Datatoken Name & Symbol",
"type": "datatoken",
"help": "The datatoken for this algorithm will be created with this name & symbol.",
"required": true
},
{
"name": "entrypoint",
"label": "Entrypoint",
"placeholder": "e.g. python $ALGO",
"help": "Provide the entrypoint for your algorithm.",
"required": false
},
{
"name": "algorithmPrivacy",
"label": "Algorithm Privacy",
"type": "checkbox",
"options": ["Keep my algorithm private"],
"help": "By default, your algorithm can be downloaded for a fixed or dynamic price in addition to running in compute jobs. Enabling this option will prevent downloading, so your algorithm can only be run as part of a compute job on a data set.",
"required": false
},
{
"name": "author",
"label": "Author",
"placeholder": "e.g. Jelly McJellyfish",
"help": "Give proper attribution for your data set. You are welcome to use a pseudonym, and you can change your author name at any time. Please note that it will remain in the transaction history. For more information on how personal data is handled within the metadata, please refer to our [privacy policy](/privacy/en).",
"required": true
},
{
"name": "tags",
"label": "Tags",
"placeholder": "e.g. logistics, ai",
"help": "Separate tags with comma."
},
{
"name": "providerUri",
"label": "Custom Provider URL",
"type": "providerUri",
"help": "Enter the URL for your custom provider or leave blank to use the default provider. [Learn more](https://github.com/oceanprotocol/provider/).",
"placeholder": "https://provider.polygon.oceanprotocol.com/",
"advanced": true
},
{
"name": "termsAndConditions",
"label": "Terms & Conditions",
"type": "terms",
"options": ["I agree to these Terms and Conditions"],
"required": true
}
],
"success": "Algorithm Published!"
}

View File

@ -1,89 +0,0 @@
{
"title": "Publish a Data Set",
"data": [
{
"name": "name",
"label": "Title",
"placeholder": "e.g. Shapes of Desert Plants",
"help": "Enter a concise title.",
"required": true
},
{
"name": "description",
"label": "Description",
"help": "Add a thorough description with as much detail as possible. You can use [Markdown](https://daringfireball.net/projects/markdown/basics). You can change the description at any time. If you provide personal data, please note that it will remain in the transaction history. For more information on how personal data is handled within the metadata, please refer to our [privacy policy](/privacy/en).",
"type": "textarea",
"required": true
},
{
"name": "files",
"label": "File",
"placeholder": "e.g. https://file.com/file.json",
"help": "Please enter the URL to your data set file and click \"ADD FILE\" to validate the data. This URL will be stored encrypted after publishing. For a compute data set, your file should match the file type required by the algorithm, and should not exceed 1 GB in file size.",
"type": "files",
"required": true
},
{
"name": "links",
"label": "Sample file",
"placeholder": "e.g. https://file.com/samplefile.json",
"help": "Please enter the URL to a sample of your data set file and click \"ADD FILE\" to validate the data. This file should reveal the data structure of your data set, e.g. by including the header and one line of a CSV file. This file URL will be publicly available after publishing.",
"type": "files"
},
{
"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": ["Download"]
},
{
"name": "providerUri",
"label": "Custom Provider URL",
"type": "providerUri",
"help": "Enter the URL for your custom provider or leave blank to use the default provider. [Learn more](https://github.com/oceanprotocol/provider/).",
"placeholder": "https://provider.polygon.oceanprotocol.com/",
"advanced": true
},
{
"name": "timeout",
"label": "Timeout",
"help": "Define how long buyers should be able to download the data set again after the initial purchase.",
"type": "select",
"options": ["Forever", "1 day", "1 week", "1 month", "1 year"],
"sortOptions": false,
"required": true
},
{
"name": "dataTokenOptions",
"label": "Datatoken Name & Symbol",
"type": "datatoken",
"help": "The datatoken for this data set will be created with this name & symbol.",
"required": true
},
{
"name": "author",
"label": "Author",
"placeholder": "e.g. Jelly McJellyfish",
"help": "Give proper attribution for your data set. You are welcome to use a pseudonym, and you can change your author name at any time. Please note that it will remain in the transaction history. For more information on how personal data is handled within the metadata, please refer to our [privacy policy](/privacy/en).",
"required": true
},
{
"name": "tags",
"label": "Tags",
"placeholder": "e.g. logistics, ai",
"help": "Separate tags with comma."
},
{
"name": "termsAndConditions",
"label": "Terms & Conditions",
"type": "terms",
"options": ["I agree to these Terms and Conditions"],
"required": true
}
],
"success": "Asset Created!"
}

170
content/publish/form.json Normal file
View File

@ -0,0 +1,170 @@
{
"metadata": {
"title": "Metadata",
"fields": [
{
"name": "nft",
"label": "Data NFT",
"type": "nft",
"help": "All metadata is stored on-chain in a newly deployed ERC-721 contract representing this asset, created with this name & symbol.",
"required": true
},
{
"name": "type",
"label": "Asset Type",
"type": "boxSelection",
"options": ["Dataset", "Algorithm"],
"required": true
},
{
"name": "name",
"label": "Title",
"placeholder": "e.g. Shapes of Desert Plants",
"required": true
},
{
"name": "description",
"label": "Description",
"help": "Add a thorough description with as much detail as possible. You can use [Markdown](https://daringfireball.net/projects/markdown/basics). You can change the description at any time. If you provide personal data, please note that it will remain in the transaction history. For more information on how personal data is handled within the metadata, please refer to our [privacy policy](/privacy/en).",
"type": "textarea",
"required": true
},
{
"name": "author",
"label": "Author",
"placeholder": "e.g. Jelly McJellyfish",
"help": "Give proper attribution for your data set. You are welcome to use a pseudonym, and you can change your author name at any time. Please note that it will remain in the transaction history. For more information on how personal data is handled within the metadata, please refer to our [privacy policy](/privacy/en).",
"required": true
},
{
"name": "tags",
"label": "Tags",
"placeholder": "e.g. logistics, ai",
"help": "Separate tags with comma."
},
{
"name": "dockerImage",
"label": "Docker Image",
"help": "Please select an image to run your algorithm.",
"type": "boxSelection",
"options": [
"populated from algorithmContainerPresets in Publish/_constants"
],
"required": true
},
{
"name": "dockerImageCustom",
"label": "Docker Image URL",
"placeholder": "e.g. oceanprotocol/algo_dockers or https://example.com/image_path",
"help": "Provide the name of a public Docker image or the full url if you have it hosted in a 3rd party repo",
"required": true
},
{
"name": "dockerImageCustomTag",
"label": "Docker Image Tag",
"placeholder": "e.g. latest",
"help": "Provide the tag for your Docker image.",
"required": true
},
{
"name": "dockerImageCustomEntrypoint",
"label": "Docker Entrypoint",
"placeholder": "e.g. python $ALGO",
"help": "Provide the entrypoint for your algorithm.",
"required": true
},
{
"name": "termsAndConditions",
"label": "Terms & Conditions",
"type": "checkbox",
"options": ["I agree to the Terms and Conditions"],
"required": true
}
]
},
"services": {
"title": "Access",
"fields": [
{
"name": "dataTokenOptions",
"label": "Datatoken",
"type": "datatoken",
"help": "The datatoken used for accessing this asset will be created with this name & symbol.",
"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",
"placeholder": "e.g. https://file.com/file.json",
"help": "Please enter the URL to your data set file and click \"ADD FILE\" to validate the data. This URL will be stored encrypted after publishing. For a compute data set, your file should match the file type required by the algorithm, and should not exceed 1 GB in file size.",
"type": "files",
"required": true
},
{
"name": "links",
"label": "Sample file",
"placeholder": "e.g. https://file.com/samplefile.json",
"help": "Please enter the URL to a sample of your data set file and click \"ADD FILE\" to validate the data. This file should reveal the data structure of your data set, e.g. by including the header and one line of a CSV file. This file URL will be publicly available after publishing.",
"type": "files"
},
{
"name": "algorithmPrivacy",
"label": "Algorithm Privacy",
"type": "checkbox",
"options": ["Keep my algorithm private"],
"help": "By default, your algorithm can be downloaded for a fixed or dynamic price in addition to running in compute jobs. Enabling this option will prevent downloading, so your algorithm can only be run as part of a compute job on a data set.",
"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": ["Download"]
},
{
"name": "timeout",
"label": "Timeout",
"help": "Define how long buyers should be able to download the data set again after the initial purchase.",
"type": "select",
"options": ["Forever", "1 day", "1 week", "1 month", "1 year"],
"sortOptions": false,
"required": true
},
{
"name": "computeOptions",
"label": "Compute Environment",
"type": "radio",
"options": [
"populated from computeEnvironmentDefaults in Publish/_constants & computeEnvironmentOptions in Publish/Services/"
],
"required": true
}
]
},
"pricing": {
"title": "Pricing",
"fields": [
{
"name": "dummy content, actual content is defined under 'create' key in ../price.json"
}
]
},
"preview": {
"title": "Preview"
},
"submission": {
"title": "Submit"
}
}

View File

@ -1,6 +1,6 @@
{
"title": "Publish",
"description": "Highlight the important features of your data set or algorithm to make it more discoverable and catch the interest of data consumers.",
"warning": "Given the beta status, publishing on Ropsten or Rinkeby first is strongly recommended. Please familiarize yourself with [the market](https://oceanprotocol.com/technology/marketplaces), [the risks](https://blog.oceanprotocol.com/on-staking-on-data-in-ocean-market-3d8e09eb0a13), and the [Terms of Use](/terms).",
"warning": "Publishing into a test network first is strongly recommended. Please familiarize yourself with [the market](https://oceanprotocol.com/technology/marketplaces), [the risks](https://blog.oceanprotocol.com/on-staking-on-data-in-ocean-market-3d8e09eb0a13), and the [Terms of Use](/terms).",
"tooltipNetwork": "Assets are published into the network your wallet is connected to. Switch your wallet's network to publish into another one."
}

2815
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -32,21 +32,23 @@
"axios": "^0.24.0",
"chart.js": "^2.9.4",
"classnames": "^2.3.1",
"d3": "^7.1.1",
"date-fns": "^2.25.0",
"decimal.js": "^10.3.1",
"dom-confetti": "^0.2.2",
"dotenv": "^10.0.0",
"ethereum-address": "0.0.4",
"ethereum-blockies": "github:MyEtherWallet/blockies",
"filesize": "^8.0.3",
"filesize": "^8.0.6",
"formik": "^2.2.9",
"gray-matter": "^4.0.3",
"is-url-superb": "^6.0.0",
"is-url-superb": "^6.1.0",
"js-cookie": "^3.0.1",
"js-sha256": "^0.9.0",
"jwt-decode": "^3.1.2",
"lodash.debounce": "^4.0.8",
"lodash.omit": "^4.5.0",
"next": "^12.0.1",
"next": "^12.0.3",
"query-string": "^7.0.1",
"react": "^17.0.2",
"react-chartjs-2": "^2.11.2",
@ -54,16 +56,16 @@
"react-data-table-component": "^6.11.7",
"react-dom": "^17.0.2",
"react-dotdotdot": "^1.3.1",
"react-modal": "^3.14.3",
"react-paginate": "^7.1.3",
"react-modal": "^3.14.4",
"react-paginate": "^8.0.0",
"react-spring": "^9.3.0",
"react-tabs": "^3.2.2",
"react-toastify": "^8.0.3",
"react-tabs": "^3.2.3",
"react-toastify": "^8.1.0",
"remark": "^13.0.0",
"remark-gfm": "^1.0.0",
"remark-html": "^13.0.1",
"remove-markdown": "^0.3.0",
"slugify": "^1.6.1",
"slugify": "^1.6.2",
"swr": "^1.0.1",
"urql": "^2.0.5",
"use-dark-mode": "^2.3.1",
@ -79,13 +81,14 @@
"@graphql-codegen/typescript-react-apollo": "^3.1.6",
"@svgr/webpack": "^5.5.0",
"@types/chart.js": "^2.9.32",
"@types/d3": "^7.1.0",
"@types/js-cookie": "^3.0.0",
"@types/loadable__component": "^5.13.1",
"@types/lodash.debounce": "^4.0.3",
"@types/lodash.omit": "^4.5.6",
"@types/node": "^16.11.5",
"@types/react": "^17.0.32",
"@types/react-dom": "^17.0.10",
"@types/react-dom": "^17.0.11",
"@types/react-modal": "^3.13.1",
"@types/react-paginate": "^7.1.1",
"@types/react-tabs": "^2.3.3",
@ -98,13 +101,13 @@
"eslint-config-oceanprotocol": "^1.5.0",
"eslint-config-prettier": "^8.3.0",
"eslint-plugin-prettier": "^4.0.0",
"eslint-plugin-react": "^7.24.0",
"eslint-plugin-react-hooks": "^4.2.0",
"eslint-plugin-react": "^7.27.0",
"eslint-plugin-react-hooks": "^4.3.0",
"file-loader": "^6.2.0",
"husky": "^7.0.2",
"prettier": "^2.4.1",
"pretty-quick": "^3.1.1",
"serve": "^12.0.1",
"serve": "^13.0.2",
"typescript": "^4.4.4"
},
"repository": {

View File

@ -7,7 +7,7 @@ import React, {
useCallback,
ReactNode
} from 'react'
import { Logger, DDO, MetadataMain } from '@oceanprotocol/lib'
import { Logger } from '@oceanprotocol/lib'
import { PurgatoryData } from '@oceanprotocol/lib/dist/node/ddo/interfaces/PurgatoryData'
import getAssetPurgatoryData from '@utils/purgatory'
import { CancelToken } from 'axios'
@ -20,13 +20,10 @@ import { useCancelToken } from '@hooks/useCancelToken'
interface AssetProviderValue {
isInPurgatory: boolean
purgatoryData: PurgatoryData
ddo: DDO
did: string
metadata: MetadataMarket
ddo: Asset
title: string
owner: string
price: BestPrice
type: MetadataMain['type']
error?: string
refreshInterval: number
isAssetNetwork: boolean
@ -42,7 +39,7 @@ function AssetProvider({
asset,
children
}: {
asset: string | DDO
asset: string | Asset
children: ReactNode
}): ReactElement {
const { appConfig } = useSiteMetadata()
@ -50,17 +47,17 @@ function AssetProvider({
const { networkId } = useWeb3()
const [isInPurgatory, setIsInPurgatory] = useState(false)
const [purgatoryData, setPurgatoryData] = useState<PurgatoryData>()
const [ddo, setDDO] = useState<DDO>()
const [ddo, setDDO] = useState<Asset>()
const [did, setDID] = useState<string>()
const [metadata, setMetadata] = useState<MetadataMarket>()
const [title, setTitle] = useState<string>()
const [price, setPrice] = useState<BestPrice>()
const [owner, setOwner] = useState<string>()
const [error, setError] = useState<string>()
const [type, setType] = useState<MetadataMain['type']>()
const [loading, setLoading] = useState(false)
const [isAssetNetwork, setIsAssetNetwork] = useState<boolean>()
const newCancelToken = useCancelToken()
const fetchDdo = async (token?: CancelToken) => {
Logger.log('[asset] Init asset, get DDO')
setLoading(true)
@ -85,27 +82,6 @@ function AssetProvider({
setLoading(false)
}
//
// Get and set DDO based on passed DDO or DID
//
useEffect(() => {
if (!asset || !appConfig.metadataCacheUri) return
let isMounted = true
async function init() {
const ddo = await fetchDdo(newCancelToken())
if (!isMounted) return
Logger.debug('[asset] Got DDO', ddo)
setDDO(ddo)
setDID(asset as string)
}
init()
return () => {
isMounted = false
}
}, [asset, appConfig.metadataCacheUri])
const setPurgatory = useCallback(async (did: string): Promise<void> => {
if (!did) return
@ -119,29 +95,41 @@ function AssetProvider({
}
}, [])
const initMetadata = useCallback(async (ddo: DDO): Promise<void> => {
//
// Get and set DDO based on passed DDO or DID
//
useEffect(() => {
if (!asset || !appConfig.metadataCacheUri) return
let isMounted = true
async function init() {
const ddo = await fetchDdo(newCancelToken())
if (!isMounted || !ddo) return
Logger.debug('[asset] Got DDO', ddo)
setDDO(ddo)
setDID(asset as string)
setTitle(ddo.metadata.name)
setOwner(ddo.nft.owner)
setIsInPurgatory(ddo.isInPurgatory === 'true')
await setPurgatory(ddo.id)
}
init()
return () => {
isMounted = false
}
}, [asset, appConfig.metadataCacheUri])
const initPrice = useCallback(async (ddo: Asset): Promise<void> => {
if (!ddo) return
setLoading(true)
const returnedPrice = await getPrice(ddo)
setPrice({ ...returnedPrice })
// Get metadata from DDO
const { attributes } = ddo.findServiceByType('metadata')
setMetadata(attributes as unknown as MetadataMarket)
setTitle(attributes?.main.name)
setType(attributes.main.type)
setOwner(ddo.publicKey[0].owner)
Logger.log('[asset] Got Metadata from DDO', attributes)
setIsInPurgatory(ddo.isInPurgatory === 'true')
await setPurgatory(ddo.id)
setLoading(false)
}, [])
useEffect(() => {
if (!ddo) return
initMetadata(ddo)
}, [ddo, initMetadata])
initPrice(ddo)
}, [ddo, initPrice])
// Check user network against asset network
useEffect(() => {
@ -157,11 +145,9 @@ function AssetProvider({
{
ddo,
did,
metadata,
title,
owner,
price,
type,
error,
isInPurgatory,
purgatoryData,

View File

@ -14,7 +14,7 @@ import {
} from '@utils/subgraph'
import { useUserPreferences } from './UserPreferences'
import { PoolShares_poolShares as PoolShare } from '../@types/apollo/PoolShares'
import { DDO, Logger } from '@oceanprotocol/lib'
import { Logger } from '@oceanprotocol/lib'
import { getDownloadAssets, getPublishedAssets } from '@utils/aquarius'
import { useSiteMetadata } from '@hooks/useSiteMetadata'
import { accountTruncate } from '@utils/web3'
@ -27,7 +27,7 @@ interface ProfileProviderValue {
profile: Profile
poolShares: PoolShare[]
isPoolSharesLoading: boolean
assets: DDO[]
assets: Asset[]
assetsTotal: number
isEthAddress: boolean
downloads: DownloadedAsset[]
@ -170,7 +170,7 @@ function ProfileProvider({
//
// PUBLISHED ASSETS
//
const [assets, setAssets] = useState<DDO[]>()
const [assets, setAssets] = useState<Asset[]>()
const [assetsTotal, setAssetsTotal] = useState(0)
// const [assetsWithPrices, setAssetsWithPrices] = useState<AssetListPrices[]>()

View File

@ -35,7 +35,7 @@ async function getBlockHead(config: ConfigHelperConfig) {
// for ETH main, get block from graph fetch
if (config.network === 'mainnet') {
const response: any = await fetchGraph(ethGraphUrl, ethGraphQuery)
return Number(response?.data?.blocks[0].number)
return Number(response?.data?.blocks[0]?.number)
}
// for everything else, create new web3 instance with infura

View File

@ -1,4 +1,4 @@
import { DDO, Logger } from '@oceanprotocol/lib'
import { Logger } from '@oceanprotocol/lib'
import { useState } from 'react'
import { TransactionReceipt } from 'web3-core'
import { Decimal } from 'decimal.js'
@ -14,17 +14,17 @@ import { useOcean } from '@context/Ocean'
import { useWeb3 } from '@context/Web3'
interface UsePricing {
getDTSymbol: (ddo: DDO) => Promise<string>
getDTName: (ddo: DDO) => Promise<string>
getDTSymbol: (ddo: Asset) => Promise<string>
getDTName: (ddo: Asset) => Promise<string>
createPricing: (
priceOptions: PriceOptions,
ddo: DDO
ddo: Asset
) => Promise<TransactionReceipt | string | void>
mint: (tokensToMint: string, ddo: DDO) => Promise<TransactionReceipt | void>
mint: (tokensToMint: string, ddo: Asset) => Promise<TransactionReceipt | void>
buyDT: (
dtAmount: number | string,
amountDataToken: number | string,
price: BestPrice,
ddo: DDO
ddo: Asset
) => Promise<TransactionReceipt | void>
pricingStep?: number
pricingStepText?: string
@ -40,28 +40,28 @@ function usePricing(): UsePricing {
const [pricingStepText, setPricingStepText] = useState<string>()
const [pricingError, setPricingError] = useState<string>()
async function getDTSymbol(ddo: DDO): Promise<string> {
async function getDTSymbol(ddo: Asset): Promise<string> {
if (!ocean || !accountId) return
const { dataToken, dataTokenInfo } = ddo
const { dataTokenInfo } = ddo
return dataTokenInfo
? dataTokenInfo.symbol
: await ocean?.datatokens.getSymbol(dataToken)
: await ocean?.datatokens.getSymbol(dataTokenInfo.address)
}
async function getDTName(ddo: DDO): Promise<string> {
async function getDTName(ddo: Asset): Promise<string> {
if (!ocean || !accountId) return
const { dataToken, dataTokenInfo } = ddo
const { dataTokenInfo } = ddo
return dataTokenInfo
? dataTokenInfo.name
: await ocean?.datatokens.getName(dataToken)
: await ocean?.datatokens.getName(dataTokenInfo.address)
}
// Helper for setting steps & feedback for all flows
async function setStep(
index: number,
type: 'pool' | 'exchange' | 'free' | 'buy' | 'dispense',
ddo: DDO
ddo: Asset
) {
const dtSymbol = await getDTSymbol(ddo)
setPricingStep(index)
@ -92,18 +92,18 @@ function usePricing(): UsePricing {
async function mint(
tokensToMint: string,
ddo: DDO
ddo: Asset
): Promise<TransactionReceipt | void> {
const { dataToken } = ddo
Logger.log('mint function', dataToken, accountId)
const { dataTokenInfo } = ddo
Logger.log('mint function', dataTokenInfo.address, accountId)
const balance = new Decimal(
await ocean.datatokens.balance(dataToken, accountId)
await ocean.datatokens.balance(dataTokenInfo.address, accountId)
)
const tokens = new Decimal(tokensToMint)
if (tokens.greaterThan(balance)) {
const mintAmount = tokens.minus(balance)
const tx = await ocean.datatokens.mint(
dataToken,
dataTokenInfo.address,
accountId,
mintAmount.toString()
)
@ -112,9 +112,9 @@ function usePricing(): UsePricing {
}
async function buyDT(
dtAmount: number | string,
amountDataToken: number | string,
price: BestPrice,
ddo: DDO
ddo: Asset
): Promise<TransactionReceipt | void> {
if (!ocean || !accountId) return
@ -129,7 +129,7 @@ function usePricing(): UsePricing {
Decimal.set({ precision: 18 })
switch (price?.type) {
case 'pool': {
case 'dynamic': {
const oceanAmmount = new Decimal(price.value).times(1.05).toString()
const maxPrice = new Decimal(price.value).times(2).toString()
@ -144,7 +144,7 @@ function usePricing(): UsePricing {
tx = await ocean.pool.buyDT(
accountId,
price.address,
String(dtAmount),
String(amountDataToken),
oceanAmmount,
maxPrice
)
@ -152,7 +152,7 @@ function usePricing(): UsePricing {
Logger.log('DT buy response', tx)
break
}
case 'exchange': {
case 'fixed': {
if (!config.oceanTokenAddress) {
Logger.error(`'oceanTokenAddress' not set in config`)
return
@ -171,7 +171,7 @@ function usePricing(): UsePricing {
setStep(2, 'buy', ddo)
tx = await ocean.fixedRateExchange.buyDT(
price.address,
`${dtAmount}`,
`${amountDataToken}`,
accountId
)
setStep(3, 'buy', ddo)
@ -181,18 +181,20 @@ function usePricing(): UsePricing {
case 'free': {
setStep(1, 'dispense', ddo)
const isDispensable = await ocean.OceanDispenser.isDispensable(
ddo.dataToken,
ddo?.services[0].datatokenAddress,
accountId,
'1'
)
if (!isDispensable) {
Logger.error(`Dispenser for ${ddo.dataToken} failed to dispense`)
Logger.error(
`Dispenser for ${ddo?.services[0].datatokenAddress} failed to dispense`
)
return
}
tx = await ocean.OceanDispenser.dispense(
ddo.dataToken,
ddo?.services[0].datatokenAddress,
accountId,
'1'
)
@ -215,17 +217,17 @@ function usePricing(): UsePricing {
async function createPricing(
priceOptions: PriceOptions,
ddo: DDO
ddo: Asset
): Promise<TransactionReceipt | void> {
const { dataToken } = ddo
const dataToken = ddo?.services[0].datatokenAddress
const dtSymbol = await getDTSymbol(ddo)
if (!ocean || !accountId || !dtSymbol) return
const { type, oceanAmount, price, weightOnDataToken, swapFee } =
const { type, amountOcean, price, weightOnDataToken, swapFee } =
priceOptions
let { dtAmount } = priceOptions
let { amountDataToken } = priceOptions
const isPool = type === 'dynamic'
if (!isPool && !config.fixedRateExchangeAddress) {
@ -244,25 +246,25 @@ function usePricing(): UsePricing {
await ocean.OceanDispenser.activate(dataToken, '1', '1', accountId)
} else {
// if fixedPrice set dt to max amount
if (!isPool) dtAmount = 1000
await mint(`${dtAmount}`, ddo)
if (!isPool) amountDataToken = 1000
await mint(`${amountDataToken}`, ddo)
}
// dtAmount for fixed price is set to max
// amountDataToken for fixed price is set to max
const tx = isPool
? await ocean.pool
.create(
accountId,
dataToken,
`${dtAmount}`,
`${amountDataToken}`,
weightOnDataToken,
`${oceanAmount}`,
`${amountOcean}`,
`${swapFee}`
)
.next((step: number) => setStep(step, 'pool', ddo))
: type === 'fixed'
? await ocean.fixedRateExchange
.create(dataToken, `${price}`, accountId, `${dtAmount}`)
.create(dataToken, `${price}`, accountId, `${amountDataToken}`)
.next((step: number) => setStep(step, 'exchange', ddo))
: await ocean.OceanDispenser.makeMinter(dataToken, accountId).next(
(step: number) => setStep(step, 'free', ddo)

View File

@ -1,165 +0,0 @@
import { DDO, Logger, Metadata } from '@oceanprotocol/lib'
import {
Service,
ServiceComputePrivacy,
ServiceType
} from '@oceanprotocol/lib/dist/node/ddo/interfaces/Service'
import { useEffect, useState } from 'react'
import { sleep } from '@utils/index'
import { publishFeedback } from '@utils/feedback'
import { useOcean } from '@context/Ocean'
import { useWeb3 } from '@context/Web3'
import { getOceanConfig } from '@utils/ocean'
export interface DataTokenOptions {
cap?: string
name?: string
symbol?: string
}
interface UsePublish {
publish: (
asset: Metadata,
serviceType: ServiceType,
dataTokenOptions?: DataTokenOptions,
timeout?: number,
providerUri?: string
) => Promise<DDO | undefined | null>
publishStep?: number
publishStepText?: string
publishError?: string
isLoading: boolean
}
function usePublish(): UsePublish {
const { networkId, web3Loading } = useWeb3()
const { connect, ocean, account } = useOcean()
const [isLoading, setIsLoading] = useState(false)
const [publishStep, setPublishStep] = useState<number | undefined>()
const [publishStepText, setPublishStepText] = useState<string | undefined>()
const [publishError, setPublishError] = useState<string | undefined>()
function setStep(index?: number) {
setPublishStep(index)
index && setPublishStepText(publishFeedback[index])
}
//
// Initiate OceanProvider based on user wallet
//
useEffect(() => {
if (web3Loading || !connect) return
async function initOcean() {
const config = getOceanConfig(networkId)
await connect(config)
}
initOcean()
}, [web3Loading, networkId, connect])
/**
* Publish an asset. It also creates the datatoken, mints tokens and gives the market allowance
* @param {Metadata} asset The metadata of the asset.
* @param {PriceOptions} priceOptions : number of tokens to mint, datatoken weight , liquidity fee, type : fixed, dynamic
* @param {ServiceType} serviceType Desired service type of the asset access or compute
* @param {DataTokenOptions} dataTokenOptions custom name, symbol and cap for datatoken
* @return {Promise<DDO>} Returns the newly published ddo
*/
async function publish(
asset: Metadata,
serviceType: ServiceType,
dataTokenOptions?: DataTokenOptions,
timeout?: number,
providerUri?: string
): Promise<DDO | undefined | null> {
if (!ocean || !account) return null
setIsLoading(true)
setPublishError(undefined)
setStep(0)
try {
const publishedDate =
new Date(Date.now()).toISOString().split('.')[0] + 'Z'
const services: Service[] = []
const price = '1'
asset.main.dateCreated = asset.main.datePublished = publishedDate
switch (serviceType) {
case 'access': {
if (!timeout) timeout = 0
const accessService =
await ocean.assets.createAccessServiceAttributes(
account,
price,
publishedDate,
timeout,
providerUri,
null
)
Logger.log('access service created', accessService)
services.push(accessService)
break
}
case 'compute': {
if (!timeout) timeout = 3600
const provider = {}
const origComputePrivacy: ServiceComputePrivacy = {
allowRawAlgorithm: false,
allowNetworkAccess: false,
allowAllPublishedAlgorithms: false,
publisherTrustedAlgorithms: []
}
const computeService = ocean.compute.createComputeService(
account,
price,
publishedDate,
provider,
origComputePrivacy,
timeout,
providerUri
)
services.push(computeService)
break
}
}
Logger.log('services created', services)
const ddo = await ocean.assets
.create(
asset,
account,
services,
undefined,
dataTokenOptions?.cap,
dataTokenOptions?.name,
dataTokenOptions?.symbol,
providerUri
)
.next(setStep)
Logger.log('ddo created', ddo)
await ocean.assets.publishDdo(ddo, account.getId())
Logger.log('ddo published')
await sleep(20000)
setStep(7)
return ddo
} catch (error) {
setPublishError(error.message)
Logger.error(error)
setStep()
} finally {
setIsLoading(false)
}
}
return {
publish,
publishStep,
publishStepText,
isLoading,
publishError
}
}
export { usePublish }
export default usePublish

View File

@ -0,0 +1,3 @@
<svg width="15" height="9" viewBox="0 0 15 9" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M1.56946 0.51192C1.29989 0.197426 0.826414 0.161005 0.51192 0.430571C0.197426 0.700138 0.161005 1.17361 0.430571 1.48811L3.01221 4.50001L0.430571 7.51192C0.161005 7.82641 0.197426 8.29989 0.51192 8.56946C0.826414 8.83902 1.29989 8.8026 1.56946 8.48811L4.56946 4.98811C4.8102 4.70724 4.8102 4.29279 4.56946 4.01192L1.56946 0.51192ZM9.00001 4.25001C8.5858 4.25001 8.25001 4.5858 8.25001 5.00001C8.25001 5.41423 8.5858 5.75001 9.00001 5.75001H14C14.4142 5.75001 14.75 5.41423 14.75 5.00001C14.75 4.5858 14.4142 4.25001 14 4.25001H9.00001Z" />
</svg>

After

Width:  |  Height:  |  Size: 678 B

3
src/@images/dataset.svg Normal file
View File

@ -0,0 +1,3 @@
<svg width="15" height="12" viewBox="0 0 15 12" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M2.84593 0.356445H0V3.21453H2.84593V0.356445ZM2.84593 4.64355H0V7.50164H2.84593V4.64355ZM0 9H2.84593V11.8581H0V9ZM8.5377 0.356445H5.69177V3.21453H8.5377V0.356445ZM5.69177 4.64355H8.5377V7.50164H5.69177V4.64355ZM8.5377 9H5.69177V11.8581H8.5377V9ZM11.3837 0.356445H14.2296V3.21453H11.3837V0.356445ZM14.2296 4.64355H11.3837V7.50164H14.2296V4.64355ZM11.3837 9H14.2296V11.8581H11.3837V9Z" />
</svg>

After

Width:  |  Height:  |  Size: 527 B

33
src/@types/Asset.d.ts vendored Normal file
View File

@ -0,0 +1,33 @@
interface AssetNft {
address: string
name: string
symbol: string
owner: string
state: 0 | 1 | 2 | 3 | 4
}
interface AssetDatatoken {
name: string
symbol: string
address: string
serviceId: string
}
interface AssetLastEvent {
tx: string
block: number
from: string
contract: string
}
interface Asset extends DDO {
nft: AssetNft
datatokens: AssetDatatoken[]
event: AssetLastEvent
stats: { consume: number }
isInPurgatory: string
// This is fake and most likely won't be used like this.
// Just here so we can continue to have successful builds.
dataTokenInfo: AssetDatatoken
}

9
src/@types/DDO/Credentials.d.ts vendored Normal file
View File

@ -0,0 +1,9 @@
interface Credential {
type: string
values: string[]
}
interface Credentials {
allow: Credential[]
deny: Credential[]
}

28
src/@types/DDO/Metadata.d.ts vendored Normal file
View File

@ -0,0 +1,28 @@
interface MetadataAlgorithmContainer {
entrypoint: string
image: string
tag: string
checksum: string
}
interface MetadataAlgorithm {
language?: string
version?: string
container: MetadataAlgorithmContainer
}
interface Metadata {
created: string
updated: string
name: string
description: string
type: 'dataset' | 'algorithm' | string
author: string
license: string
links?: string[]
tags?: string[]
copyrightHolder?: string
contentLanguage?: string
algorithm?: MetadataAlgorithm
additionalInformation?: any
}

29
src/@types/DDO/Services.d.ts vendored Normal file
View File

@ -0,0 +1,29 @@
interface PublisherTrustedAlgorithm {
did: string
filesChecksum: string
containerSectionChecksum: string
}
interface ServiceComputeOptions {
namespace: string
cpu?: number
gpu?: number
gpuType?: string
memory?: string
volumeSize?: string
allowRawAlgorithm: boolean
allowNetworkAccess: boolean
publisherTrustedAlgorithmPublishers: string[]
publisherTrustedAlgorithms: PublisherTrustedAlgorithm[]
}
interface Service {
type: 'access' | 'compute' | string
files: string
datatokenAddress: string
serviceEndpoint: string
timeout: number
name?: string
description?: string
compute?: ServiceComputeOptions
}

10
src/@types/DDO/index.d.ts vendored Normal file
View File

@ -0,0 +1,10 @@
// DDO spec
interface DDO {
'@context': string[]
id: string
version: string
chainId: number
metadata: Metadata
services: Service[]
credentials?: Credentials
}

51
src/@types/Form.d.ts vendored
View File

@ -1,30 +1,23 @@
import { AssetSelectionAsset } from '@shared/Form/FormFields/AssetSelection'
// declaring into global scope to be able to use this as
// ambiant types despite the above imports
declare global {
interface FormFieldProps {
label: string
name: string
type?: string
options?: string[] | AssetSelectionAsset[]
sortOptions?: boolean
required?: boolean
multiple?: boolean
disabled?: boolean
help?: string
placeholder?: string
pattern?: string
min?: string
disclaimer?: string
disclaimerValues?: string[]
advanced?: boolean
}
interface FormContent {
title: string
description?: string
success: string
data: FormFieldProps[]
}
interface FormFieldContent {
label: string
name: string
type?: string
options?: string[]
sortOptions?: boolean
required?: boolean
multiple?: boolean
disabled?: boolean
help?: string
placeholder?: string
pattern?: string
min?: string
disclaimer?: string
disclaimerValues?: string[]
advanced?: boolean
}
interface FormStepContent {
title: string
description?: string
fields: FormFieldContent[]
}

View File

@ -1,5 +1,5 @@
interface BestPrice {
type: 'pool' | 'exchange' | 'free' | ''
type: 'dynamic' | 'fixed' | 'free' | ''
address: string
value: number
isConsumable?: 'true' | 'false' | ''
@ -13,9 +13,9 @@ interface BestPrice {
interface PriceOptions {
price: number
dtAmount: number
oceanAmount: number
type: 'fixed' | 'dynamic' | 'free' | string
amountDataToken: number
amountOcean: number
type: 'dynamic' | 'fixed' | 'free' | ''
weightOnDataToken: string
weightOnOcean: string
// easier to keep this as number for Yup input validation

View File

@ -1,12 +1,6 @@
import { DDO } from '@oceanprotocol/lib'
// declaring into global scope to be able to use this as
// ambiant types despite the above imports
declare global {
interface DownloadedAsset {
dtSymbol: string
timestamp: number
networkId: number
ddo: DDO
}
interface DownloadedAsset {
dtSymbol: string
timestamp: number
networkId: number
ddo: Asset
}

View File

@ -1,29 +0,0 @@
import {
Metadata,
AdditionalInformation,
EditableMetadataLinks
} from '@oceanprotocol/lib'
// declaring into global scope to be able to use this as
// ambiant types despite the above imports
declare global {
interface AdditionalInformationMarket extends AdditionalInformation {
termsAndConditions: boolean
}
interface MetadataMarket extends Metadata {
// While required for this market, Aquarius/Plecos will keep this as optional
// allowing external pushes of assets without `additionalInformation`.
// Making it optional here helps safeguarding against those assets.
additionalInformation?: AdditionalInformationMarket
}
interface MetadataEditForm {
name: string
description: string
timeout: string
price?: number
links?: string | EditableMetadataLinks[]
author?: string
}
}

View File

@ -1,12 +1,6 @@
import { DDO } from '@oceanprotocol/lib'
// declaring into global scope to be able to use this as
// ambiant types despite the above imports
declare global {
interface PagedAssets {
results: DDO[]
page: number
totalPages: number
totalResults: number
}
interface PagedAssets {
results: Asset[]
page: number
totalPages: number
totalResults: number
}

View File

@ -1,46 +1,41 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
/* eslint-disable camelcase */
import { DDO } from '@oceanprotocol/lib'
// declaring into global scope to be able to use this as
// ambiant types despite the above imports
declare global {
interface Explanation {
value: number
description: string
details: Explanation[]
}
interface ShardsResponse {
total: number
successful: number
failed: number
skipped: number
}
interface SearchResponse {
took: number
timed_out: boolean
_scroll_id?: string | undefined
_shards: ShardsResponse
hits: {
total: number
max_score: number
hits: Array<{
_index: string
_type: string
_id: string
_score: number
_source: DDO
_version?: number | undefined
_explanation?: Explanation | undefined
fields?: any
highlight?: any
inner_hits?: any
matched_queries?: string[] | undefined
sort?: string[] | undefined
}>
}
aggregations?: any
}
interface Explanation {
value: number
description: string
details: Explanation[]
}
interface ShardsResponse {
total: number
successful: number
failed: number
skipped: number
}
interface SearchResponse {
took: number
timed_out: boolean
_scroll_id?: string | undefined
_shards: ShardsResponse
hits: {
total: number
max_score: number
hits: Array<{
_index: string
_type: string
_id: string
_score: number
_source: Asset
_version?: number | undefined
_explanation?: Explanation | undefined
fields?: any
highlight?: any
inner_hits?: any
matched_queries?: string[] | undefined
sort?: string[] | undefined
}>
}
aggregations?: any
}

View File

@ -1,10 +1,5 @@
import {
DDO,
DID,
Logger,
publisherTrustedAlgorithm as PublisherTrustedAlgorithm
} from '@oceanprotocol/lib'
import { AssetSelectionAsset } from '@shared/Form/FormFields/AssetSelection'
import { Logger } from '@oceanprotocol/lib'
import { AssetSelectionAsset } from '@shared/FormFields/AssetSelection'
import { PriceList, getAssetsPriceList } from './subgraph'
import axios, { CancelToken, AxiosResponse } from 'axios'
import { OrdersData_tokenOrders as OrdersData } from '../@types/apollo/OrdersData'
@ -13,6 +8,7 @@ import {
SortDirectionOptions,
SortTermOptions
} from '../@types/aquarius/SearchQuery'
import { getServiceByName } from './ddo'
export const MAXIMUM_NUMBER_OF_PAGES_WITH_RESULTS = 476
@ -77,7 +73,7 @@ export function transformQueryResult(
}
result.results = (queryResult.hits.hits || []).map(
(hit) => new DDO(hit._source as DDO)
(hit) => hit._source as Asset
)
result.totalResults = queryResult.hits.total
result.totalPages =
@ -111,18 +107,18 @@ export async function queryMetadata(
}
export async function retrieveDDO(
did: string | DID,
did: string,
cancelToken: CancelToken
): Promise<DDO> {
): Promise<Asset> {
try {
const response: AxiosResponse<DDO> = await axios.get(
const response: AxiosResponse<Asset> = await axios.get(
`${metadataCacheUri}/api/v1/aquarius/assets/ddo/${did}`,
{ cancelToken }
)
if (!response || response.status !== 200 || !response.data) return
const data = { ...response.data }
return new DDO(data)
return data
} catch (error) {
if (axios.isCancel(error)) {
Logger.log(error.message)
@ -133,7 +129,7 @@ export async function retrieveDDO(
}
export async function getAssetsNames(
didList: string[] | DID[],
didList: string[],
cancelToken: CancelToken
): Promise<Record<string, string>> {
try {
@ -179,10 +175,10 @@ export async function retrieveDDOListByDIDs(
didList: string[],
chainIds: number[],
cancelToken: CancelToken
): Promise<DDO[]> {
): Promise<Asset[]> {
try {
if (didList?.length === 0 || chainIds?.length === 0) return []
const orderedDDOListByDIDList: DDO[] = []
const orderedDDOListByDIDList: Asset[] = []
const baseQueryparams = {
chainIds,
filters: [getFilterTerm('id', didList)],
@ -190,8 +186,8 @@ export async function retrieveDDOListByDIDs(
} as BaseQueryParams
const query = generateBaseQuery(baseQueryparams)
const result = await queryMetadata(query, cancelToken)
didList.forEach((did: string | DID) => {
const ddo: DDO = result.results.find((ddo: DDO) => ddo.id === did)
didList.forEach((did: string) => {
const ddo = result.results.find((ddo: Asset) => ddo.id === did)
orderedDDOListByDIDList.push(ddo)
})
return orderedDDOListByDIDList
@ -202,7 +198,7 @@ export async function retrieveDDOListByDIDs(
export async function transformDDOToAssetSelection(
datasetProviderEndpoint: string,
ddoList: DDO[],
ddoList: Asset[],
selectedAlgorithms?: PublisherTrustedAlgorithm[],
cancelToken?: CancelToken
): Promise<AssetSelectionAsset[]> {
@ -213,7 +209,7 @@ export async function transformDDOToAssetSelection(
for (const ddo of ddoList) {
didList.push(ddo.id)
symbolList[ddo.id] = ddo.dataTokenInfo.symbol
const algoComputeService = ddo.findServiceByType('compute')
const algoComputeService = getServiceByName(ddo, 'compute')
algoComputeService?.serviceEndpoint &&
(didProviderEndpointMap[ddo.id] = algoComputeService?.serviceEndpoint)
}
@ -350,7 +346,8 @@ export async function getDownloadAssets(
.map((ddo) => {
const order = tokenOrders.find(
({ datatokenId }) =>
datatokenId?.address.toLowerCase() === ddo.dataToken.toLowerCase()
datatokenId?.address.toLowerCase() ===
ddo.services[0].datatokenAddress.toLowerCase()
)
return {

View File

@ -1,21 +1,23 @@
import { DDO, Ocean, ServiceType } from '@oceanprotocol/lib'
import { Ocean } from '@oceanprotocol/lib'
import { getServiceByName } from './ddo'
export default async function checkPreviousOrder(
ocean: Ocean,
accountId: string,
ddo: DDO,
serviceType: ServiceType
ddo: Asset,
serviceType: 'access' | 'compute'
): Promise<string> {
if (!ocean) return
const service = ddo.findServiceByType(serviceType)
const service = getServiceByName(ddo, serviceType)
// apparenlty cost and timeout are not found, even though they are there...
const previousOrder = await ocean.datatokens.getPreviousValidOrders(
ddo.dataToken,
(service.attributes.main as any).cost,
service.index,
(service.attributes.main as any).timeout,
accountId
)
return previousOrder
// const previousOrder = await ocean.datatokens.getPreviousValidOrders(
// ddo?.services[0].datatokenAddress,
// service.cost,
// service.index,
// service.timeout,
// accountId
// )
// return previousOrder
return 'dummy'
}

View File

@ -1,7 +1,6 @@
import {
ServiceComputePrivacy,
publisherTrustedAlgorithm as PublisherTrustedAlgorithm,
DDO,
Service,
Logger,
Provider,
@ -59,7 +58,7 @@ async function getAssetMetadata(
queryDtList: string[],
cancelToken: CancelToken,
chainIds: number[]
): Promise<DDO[]> {
): Promise<Asset[]> {
const baseQueryparams = {
chainIds,
filters: [
@ -75,35 +74,37 @@ async function getAssetMetadata(
return result.results
}
function getServiceEndpoints(data: TokenOrder[], assets: DDO[]): string[] {
const serviceEndpoints: string[] = []
function getServiceEndpoints(data: TokenOrder[], assets: Asset[]): string[] {
// const serviceEndpoints: string[] = []
for (let i = 0; i < data.length; i++) {
try {
const did = web3.utils
.toChecksumAddress(data[i].datatokenId.address)
.replace('0x', 'did:op:')
const ddo = assets.filter((x) => x.id === did)[0]
if (ddo === undefined) continue
// for (let i = 0; i < data.length; i++) {
// try {
// const did = web3.utils
// .toChecksumAddress(data[i].datatokenId.address)
// .replace('0x', 'did:op:')
// const ddo = assets.filter((x) => x.id === did)[0]
// if (ddo === undefined) continue
const service = ddo.service.filter(
(x: Service) => x.index === data[i].serviceId
)[0]
// const service = ddo.services.filter(
// (x: Service) => x.index === data[i].serviceId
// )[0]
if (!service || service.type !== 'compute') continue
const { serviceEndpoint } = service
// if (!service || service.type !== 'compute') continue
// const { providerEndpoint } = service
const wasProviderQueried =
serviceEndpoints?.filter((x) => x === serviceEndpoint).length > 0
// const wasProviderQueried =
// serviceEndpoints?.filter((x) => x === providerEndpoint).length > 0
if (wasProviderQueried) continue
serviceEndpoints.push(serviceEndpoint)
} catch (err) {
Logger.error(err.message)
}
}
// if (wasProviderQueried) continue
// serviceEndpoints.push(providerEndpoint)
// } catch (err) {
// Logger.error(err.message)
// }
// }
return serviceEndpoints
// return serviceEndpoints
return ['dummy']
}
async function getProviders(
@ -138,7 +139,7 @@ async function getProviders(
async function getJobs(
providers: Provider[],
account: Account,
assets: DDO[]
assets: Asset[]
): Promise<ComputeJobMetaData[]> {
const computeJobs: ComputeJobMetaData[] = []
@ -170,13 +171,10 @@ async function getJobs(
const ddo = assets.filter((x) => x.id === did)[0]
if (!ddo) continue
const serviceMetadata = ddo.service.filter(
(x: Service) => x.type === 'metadata'
)[0]
const compJob: ComputeJobMetaData = {
...job,
assetName: serviceMetadata.attributes.main.name,
assetName: ddo.metadata.name,
assetDtSymbol: ddo.dataTokenInfo.symbol,
networkId: ddo.chainId
}
@ -205,7 +203,7 @@ export async function getComputeJobs(
config: Config,
ocean: Ocean,
account: Account,
ddo?: DDO,
ddo?: Asset,
token?: CancelToken
): Promise<ComputeResults> {
const assetDTAddress = ddo?.dataTokenInfo?.address

View File

@ -0,0 +1,38 @@
import wordListDefault from './words.json'
export interface DataTokenOptions {
cap?: string
name?: string
symbol?: string
}
/**
* Generate new datatoken name & symbol from a word list
* @return {<{ name: String; symbol: String }>} datatoken name & symbol. Produces e.g. "Endemic Jellyfish Token" & "ENDJEL-45"
*/
export function generateDatatokenName(wordList?: {
nouns: string[]
adjectives: string[]
}): {
name: string
symbol: string
} {
const list = wordList || wordListDefault
const random1 = Math.floor(Math.random() * list.adjectives.length)
const random2 = Math.floor(Math.random() * list.nouns.length)
const indexNumber = Math.floor(Math.random() * 100)
// Capitalized adjective & noun
const adjective = list.adjectives[random1].replace(/^\w/, (c) =>
c.toUpperCase()
)
const noun = list.nouns[random2].replace(/^\w/, (c) => c.toUpperCase())
const name = `${adjective} ${noun} Token`
// use first 3 letters of name, uppercase it, and add random number
const symbol = `${(
adjective.substring(0, 3) + noun.substring(0, 3)
).toUpperCase()}-${indexNumber}`
return { name, symbol }
}

View File

@ -0,0 +1,204 @@
{
"nouns": [
"Crab",
"Fish",
"Seal",
"Octopus",
"Shark",
"Seahorse",
"Walrus",
"Starfish",
"Whale",
"Orca",
"Penguin",
"Jellyfish",
"Squid",
"Lobster",
"Pelican",
"Shrimp",
"Oyster",
"Clam",
"Seagull",
"Dolphin",
"Shell",
"Cormorant",
"Otter",
"Anemone",
"Turtle",
"Coral",
"Ray",
"Barracuda",
"Krill",
"Anchovy",
"Angelfish",
"Barnacle",
"Clownfish",
"Cod",
"Cuttlefish",
"Eel",
"Fugu",
"Herring",
"Haddock",
"Ling",
"Mackerel",
"Manatee",
"Narwhal",
"Nautilus",
"Plankton",
"Porpoise",
"Prawn",
"Pufferfish",
"Swordfish",
"Tuna"
],
"adjectives": [
"adamant",
"adroit",
"amatory",
"ambitious",
"amused",
"animistic",
"antic",
"arcadian",
"artistic",
"astonishing",
"astounding",
"baleful",
"bellicose",
"bilious",
"blissful",
"boorish",
"brave",
"breathtaking",
"brilliant",
"calamitous",
"caustic",
"cerulean",
"clever",
"charming",
"comely",
"competent",
"concomitant",
"confident",
"contumacious",
"corpulent",
"crapulous",
"creative",
"dazzling",
"dedicated",
"defamatory",
"delighted",
"delightful",
"determined",
"didactic",
"dilatory",
"dowdy",
"efficacious",
"effulgent",
"egregious",
"empowered",
"endemic",
"enthusiastic",
"equanimous",
"exceptional",
"execrable",
"fabulous",
"fantastic",
"fastidious",
"feckless",
"fecund",
"friable",
"fulsome",
"garrulous",
"generous",
"gentle",
"guileless",
"gustatory",
"heuristic",
"histrionic",
"hubristic",
"incendiary",
"incredible",
"insidious",
"insolent",
"inspired",
"intransigent",
"inveterate",
"invidious",
"invigorated",
"irksome",
"jejune",
"juicy",
"jocular",
"joyful",
"judicious",
"kind",
"lachrymose",
"limpid",
"loquacious",
"lovely",
"luminous",
"mannered",
"marvelous",
"mendacious",
"meretricious",
"minatory",
"mordant",
"motivated",
"munificent",
"nefarious",
"noxious",
"obtuse",
"optimistic",
"parsimonious",
"pendulous",
"pernicious",
"pervasive",
"petulant",
"passionate",
"phenomenal",
"platitudinous",
"pleasant",
"powerful",
"precipitate",
"propitious",
"puckish",
"querulous",
"quiescent",
"rebarbative",
"recalcitant",
"redolent",
"rhadamanthine",
"risible",
"ruminative",
"sagacious",
"salubrious",
"sartorial",
"sclerotic",
"serpentine",
"smart",
"spasmodic",
"strident",
"stunning",
"stupendous",
"taciturn",
"tactful",
"tasty",
"tenacious",
"tremendous",
"tremulous",
"trenchant",
"turbulent",
"turgid",
"ubiquitous",
"uxorious",
"verdant",
"vibrant",
"voluble",
"voracious",
"wheedling",
"withering",
"wonderful",
"zealous"
]
}

58
src/@utils/ddo.ts Normal file
View File

@ -0,0 +1,58 @@
export function getServiceByName(
ddo: Asset | DDO,
name: 'access' | 'compute'
): Service {
if (!ddo) return
const service = ddo.services.filter((service) => service.type === name)[0]
return service
}
export function mapTimeoutStringToSeconds(timeout: string): number {
switch (timeout) {
case 'Forever':
return 0
case '1 day':
return 86400
case '1 week':
return 604800
case '1 month':
return 2630000
case '1 year':
return 31556952
default:
return 0
}
}
function numberEnding(number: number): string {
return number > 1 ? 's' : ''
}
export function secondsToString(numberOfSeconds: number): string {
if (numberOfSeconds === 0) return 'Forever'
const years = Math.floor(numberOfSeconds / 31536000)
const months = Math.floor((numberOfSeconds %= 31536000) / 2630000)
const weeks = Math.floor((numberOfSeconds %= 31536000) / 604800)
const days = Math.floor((numberOfSeconds %= 604800) / 86400)
const hours = Math.floor((numberOfSeconds %= 86400) / 3600)
const minutes = Math.floor((numberOfSeconds %= 3600) / 60)
const seconds = numberOfSeconds % 60
return years
? `${years} year${numberEnding(years)}`
: months
? `${months} month${numberEnding(months)}`
: weeks
? `${weeks} week${numberEnding(weeks)}`
: days
? `${days} day${numberEnding(days)}`
: hours
? `${hours} hour${numberEnding(hours)}`
: minutes
? `${minutes} minute${numberEnding(minutes)}`
: seconds
? `${seconds} second${numberEnding(seconds)}`
: 'less than a second'
}

63
src/@utils/docker.ts Normal file
View File

@ -0,0 +1,63 @@
import { Logger } from '@oceanprotocol/lib'
import axios from 'axios'
import isUrl from 'is-url-superb'
import { toast } from 'react-toastify'
async function isDockerHubImageValid(
image: string,
tag: string
): Promise<boolean> {
try {
const response = await axios.post(
`https://dockerhub-proxy.oceanprotocol.com`,
{ image, tag }
)
if (
!response ||
response.status !== 200 ||
response.data.status !== 'success'
) {
toast.error(
'Could not fetch docker hub image info. Please check image name and tag and try again'
)
return false
}
return true
} catch (error) {
Logger.error(error.message)
toast.error(
'Could not fetch docker hub image info. Please check image name and tag and try again'
)
return false
}
}
async function is3rdPartyImageValid(imageURL: string): Promise<boolean> {
try {
const response = await axios.head(imageURL)
if (!response || response.status !== 200) {
toast.error(
'Could not fetch docker image info. Please check URL and try again'
)
return false
}
return true
} catch (error) {
Logger.error(error.message)
toast.error(
'Could not fetch docker image info. Please check URL and try again'
)
return false
}
}
export async function validateDockerImage(
dockerImage: string,
tag: string
): Promise<boolean> {
const isValid = isUrl(dockerImage)
? await is3rdPartyImageValid(dockerImage)
: await isDockerHubImageValid(dockerImage, tag)
return isValid
}

View File

@ -1,237 +0,0 @@
import axios from 'axios'
import { toast } from 'react-toastify'
import isUrl from 'is-url-superb'
import slugify from 'slugify'
import { DDO, MetadataAlgorithm, Logger } from '@oceanprotocol/lib'
import { FormPublishData } from '../components/Publish/_types'
export function dateToStringNoMS(date: Date): string {
return date.toISOString().replace(/\.[0-9]{3}Z/, 'Z')
}
export function transformTags(value: string): string[] {
const originalTags = value?.split(',')
const transformedTags = originalTags?.map((tag) => slugify(tag).toLowerCase())
return transformedTags
}
export function mapTimeoutStringToSeconds(timeout: string): number {
switch (timeout) {
case 'Forever':
return 0
case '1 day':
return 86400
case '1 week':
return 604800
case '1 month':
return 2630000
case '1 year':
return 31556952
default:
return 0
}
}
function numberEnding(number: number): string {
return number > 1 ? 's' : ''
}
export function secondsToString(numberOfSeconds: number): string {
if (numberOfSeconds === 0) return 'Forever'
const years = Math.floor(numberOfSeconds / 31536000)
const months = Math.floor((numberOfSeconds %= 31536000) / 2630000)
const weeks = Math.floor((numberOfSeconds %= 31536000) / 604800)
const days = Math.floor((numberOfSeconds %= 604800) / 86400)
const hours = Math.floor((numberOfSeconds %= 86400) / 3600)
const minutes = Math.floor((numberOfSeconds %= 3600) / 60)
const seconds = numberOfSeconds % 60
return years
? `${years} year${numberEnding(years)}`
: months
? `${months} month${numberEnding(months)}`
: weeks
? `${weeks} week${numberEnding(weeks)}`
: days
? `${days} day${numberEnding(days)}`
: hours
? `${hours} hour${numberEnding(hours)}`
: minutes
? `${minutes} minute${numberEnding(minutes)}`
: seconds
? `${seconds} second${numberEnding(seconds)}`
: 'less than a second'
}
export function checkIfTimeoutInPredefinedValues(
timeout: string,
timeoutOptions: string[]
): boolean {
if (timeoutOptions.indexOf(timeout) > -1) {
return true
}
return false
}
function getAlgorithmComponent(
image: string,
containerTag: string,
entrypoint: string,
algorithmLanguace: string
): MetadataAlgorithm {
return {
language: algorithmLanguace,
format: 'docker-image',
version: '0.1',
container: {
entrypoint: entrypoint,
image: image,
tag: containerTag
}
}
}
function getAlgorithmFileExtension(fileUrl: string): string {
const splitedFileUrl = fileUrl.split('.')
return splitedFileUrl[splitedFileUrl.length - 1]
}
// export function transformPublishFormToMetadata(
// {
// name,
// author,
// description,
// tags,
// links,
// termsAndConditions,
// files
// }: Partial<FormPublishData>,
// ddo?: DDO
// ): MetadataMarket {
// const currentTime = dateToStringNoMS(new Date())
// const metadata: MetadataMarket = {
// main: {
// name,
// author,
// dateCreated: ddo ? ddo.created : currentTime,
// datePublished: '',
// files: typeof files !== 'string' && files,
// license: 'https://market.oceanprotocol.com/terms'
// },
// additionalInformation: {
// description,
// tags: transformTags(tags),
// links: typeof links !== 'string' ? links : [],
// termsAndConditions
// }
// }
// return metadata
// }
// async function isDockerHubImageValid(
// image: string,
// tag: string
// ): Promise<boolean> {
// try {
// const response = await axios.post(
// `https://dockerhub-proxy.oceanprotocol.com`,
// {
// image,
// tag
// }
// )
// if (
// !response ||
// response.status !== 200 ||
// response.data.status !== 'success'
// ) {
// toast.error(
// 'Could not fetch docker hub image info. Please check image name and tag and try again'
// )
// return false
// }
// return true
// } catch (error) {
// Logger.error(error.message)
// toast.error(
// 'Could not fetch docker hub image info. Please check image name and tag and try again'
// )
// return false
// }
// }
async function is3rdPartyImageValid(imageURL: string): Promise<boolean> {
try {
const response = await axios.head(imageURL)
if (!response || response.status !== 200) {
toast.error(
'Could not fetch docker image info. Please check URL and try again'
)
return false
}
return true
} catch (error) {
Logger.error(error.message)
toast.error(
'Could not fetch docker image info. Please check URL and try again'
)
return false
}
}
// export async function validateDockerImage(
// dockerImage: string,
// tag: string
// ): Promise<boolean> {
// const isValid = isUrl(dockerImage)
// ? await is3rdPartyImageValid(dockerImage)
// : await isDockerHubImageValid(dockerImage, tag)
// return isValid
// }
// export function transformPublishAlgorithmFormToMetadata(
// {
// name,
// author,
// description,
// tags,
// image,
// containerTag,
// entrypoint,
// termsAndConditions,
// files
// }: Partial<FormPublishData>,
// ddo?: DDO
// ): MetadataMarket {
// const currentTime = dateToStringNoMS(new Date())
// const fileUrl = typeof files !== 'string' && files[0].url
// const algorithmLanguage = getAlgorithmFileExtension(fileUrl)
// const algorithm = getAlgorithmComponent(
// image,
// containerTag,
// entrypoint,
// algorithmLanguage
// )
// const metadata: MetadataMarket = {
// main: {
// name,
// type: 'algorithm',
// author,
// dateCreated: ddo ? ddo.created : currentTime,
// files: typeof files !== 'string' && files,
// license: 'https://market.oceanprotocol.com/terms',
// algorithm
// },
// additionalInformation: {
// description,
// tags: transformTags(tags),
// termsAndConditions
// }
// }
// return metadata
// }

62
src/@utils/nft.ts Normal file
View File

@ -0,0 +1,62 @@
import { renderStaticWaves } from './oceanWaves'
export interface NftOptions {
name: string
symbol: string
description: string
image: string
}
function encodeSvg(svgString: string): string {
return svgString
.replace(
'<svg',
~svgString.indexOf('xmlns')
? '<svg'
: '<svg xmlns="http://www.w3.org/2000/svg"'
)
.replace(/"/g, "'")
.replace(/%/g, '%25')
.replace(/#/g, '%23')
.replace(/{/g, '%7B')
.replace(/}/g, '%7D')
.replace(/</g, '%3C')
.replace(/>/g, '%3E')
.replace(/\s+/g, ' ')
}
export function generateNftOptions(): NftOptions {
// TODO: crop image properly in the end as generated SVG waves are a super-wide image,
// and add a filled background deciding on either black or white.
const image = renderStaticWaves()
// const image = new XMLSerializer().serializeToString(waves)
// const image = `<svg><path d="M0 10.4304L16.3396 10.4304L8.88727 17.6833L10.2401 19L20 9.5L10.2401 0L8.88727 1.31491L16.3396 8.56959L0 8.56959V10.4304Z" /></svg>`
const newNft: NftOptions = {
name: 'Ocean Asset v4 NFT',
symbol: 'OCEAN-V4-NFT',
description: `This NFT represents an asset in the Ocean Protocol v4 ecosystem.`,
// TODO: figure out if also image URI needs base64 encoding
// generated SVG embedded as 'data:image/svg+xml' and encoded characters
image: `data:image/svg+xml,${encodeSvg(image)}`
// generated SVG embedded as 'data:image/svg+xml;base64'
// image: `data:image/svg+xml;base64,${window?.btoa(image)}`
// image: `data:image/svg+xml;base64,${Buffer.from(image).toString('base64')}`
}
return newNft
}
export function generateNftCreateData(nftOptions: NftOptions): any {
const nftCreateData = {
name: nftOptions.name,
symbol: nftOptions.symbol,
templateIndex: 1,
// TODO: figure out if Buffer.from method is working in browser in final build
// as BTOA is deprecated.
tokenURI: window?.btoa(JSON.stringify(nftOptions))
// tokenURI: Buffer.from(JSON.stringify(nftOptions)).toString('base64') // should end up as data:application/json;base64
}
return nftCreateData
}

49
src/@utils/oceanWaves.ts Normal file
View File

@ -0,0 +1,49 @@
import * as d3 from 'd3'
/*
* Ocean Protocol D3 waves
* https://oceanprotocol.com/art
* Based off of Bostock's Circle Wave
* https://bl.ocks.org/mbostock/2d466ec3417722e3568cd83fc35338e3
*/
export function renderStaticWaves(): string {
const svg = d3.create('svg')
const width = 1000
const height = 250
const x = d3.scaleLinear().range([0, width])
const angles = d3.range(Math.random(), 4 * Math.PI, Math.PI / 20)
const path = svg
// .append('rect')
// .attr('fill', '#fff')
// .attr('width', '100%')
// .attr('height', '100%')
.append('g')
.attr('transform', `translate(${width / -4}, ${height / 2})`)
.attr('fill', 'none')
.attr('stroke-width', 2)
.selectAll('path')
.data(['#FF4092', '#E000CF', '#8B98A9', '#E2E2E2'])
.enter()
.append('path')
.attr('stroke', (d) => d)
.style('mix-blend-mode', 'darken')
.datum((d, i) => {
return d3
.line()
.curve(d3.curveBasisOpen)
.x((angles: any) => x(angles / 4))
.y((angles: any) => {
const t = d3.now() / 3000
return (
Math.cos(angles * 8 - (i * 2 * Math.PI) / 10 + t) *
Math.pow((2 + Math.cos(angles - t)) / 2, 4) *
15
)
})
})
path.attr('d', (d) => d(angles as any))
return `<svg>${svg.node().innerHTML}</svg>`
}

View File

@ -1,82 +1,47 @@
import axios, { CancelToken, AxiosResponse } from 'axios'
import { toast } from 'react-toastify'
import { DID, File as FileMetadata, Logger } from '@oceanprotocol/lib'
import { DID, Logger } from '@oceanprotocol/lib'
export async function fileinfo(
url: string,
providerUri: string,
cancelToken: CancelToken
): Promise<FileMetadata> {
export interface FileMetadata {
index: number
valid: boolean
contentType: string
contentLength: string
}
export async function getEncryptedFileUrls(
files: string[],
providerUrl: string,
did: string,
accountId: string
): Promise<string> {
try {
const response = (await axios.post(
`${providerUri}/api/v1/services/fileinfo`,
{
url,
cancelToken
}
)) as AxiosResponse<
{ valid: boolean; contentLength: string; contentType: string }[]
>
if (!response || response.status !== 200 || !response.data) {
toast.error('Could not connect to File API')
return
}
if (!response.data[0] || !response.data[0].valid) {
toast.error(
'The data file URL you entered apears to be invalid. Please check URL and try again',
{
autoClose: false,
hideProgressBar: false,
closeOnClick: true,
pauseOnHover: true,
draggable: true,
progress: undefined
}
)
return
} else {
toast.dismiss() // Remove any existing error message
toast.success('Great! That file looks good. 🐳', {
position: 'bottom-right',
autoClose: 5000,
hideProgressBar: false,
closeOnClick: true,
pauseOnHover: true,
draggable: true,
progress: undefined
// https://github.com/oceanprotocol/provider/blob/v4main/API.md#encrypt-endpoint
const url = `${providerUrl}/api/v1/services/encrypt`
const response: AxiosResponse<{ encryptedDocument: string }> =
await axios.post(url, {
documentId: did,
signature: '', // TODO: add signature
publisherAddress: accountId,
document: files
})
}
const { contentLength, contentType } = response.data[0]
return {
contentLength: contentLength || '',
contentType: contentType || '', // need to do that cause lib-js File interface requires contentType
url
}
return response?.data?.encryptedDocument
} catch (error) {
Logger.error(error.message)
console.error('Error parsing json: ' + error.message)
}
}
export async function getFileInfo(
url: string | DID,
providerUri: string,
providerUrl: string,
cancelToken: CancelToken
): Promise<any> {
): Promise<FileMetadata[]> {
let postBody
try {
if (url instanceof DID)
postBody = {
did: url.getDid()
}
else
postBody = {
url
}
const response = await axios.post(
`${providerUri}/api/v1/services/fileinfo`,
if (url instanceof DID) postBody = { did: url.getDid() }
else postBody = { url }
const response: AxiosResponse<FileMetadata[]> = await axios.post(
`${providerUrl}/api/v1/services/fileinfo`,
postBody,
{ cancelToken }
)

View File

@ -1,5 +1,5 @@
import { gql, OperationResult, TypedDocumentNode, OperationContext } from 'urql'
import { DDO, Logger } from '@oceanprotocol/lib'
import { Logger } from '@oceanprotocol/lib'
import { getUrqlClientInstance } from '@context/UrqlProvider'
import { getOceanConfig } from './ocean'
import {
@ -35,7 +35,7 @@ export interface PriceList {
}
export interface AssetListPrices {
ddo: DDO
ddo: Asset
price: BestPrice
}
@ -346,7 +346,7 @@ function transformPriceToBestPrice(
) {
if (poolPrice?.length > 0) {
const price: BestPrice = {
type: 'pool',
type: 'dynamic',
address: poolPrice[0]?.id,
value:
poolPrice[0]?.consumePrice === '-1'
@ -363,7 +363,7 @@ function transformPriceToBestPrice(
// TODO Hacky hack, temporary™: set isConsumable to true for fre assets.
// isConsumable: 'true'
const price: BestPrice = {
type: 'exchange',
type: 'fixed',
value: frePrice[0]?.rate,
address: frePrice[0]?.id,
exchangeId: frePrice[0]?.id,
@ -402,7 +402,7 @@ function transformPriceToBestPrice(
}
async function getAssetsPoolsExchangesAndDatatokenMap(
assets: DDO[]
assets: Asset[]
): Promise<
[
AssetsPoolPricePool[],
@ -415,13 +415,17 @@ async function getAssetsPoolsExchangesAndDatatokenMap(
const chainAssetLists: any = {}
for (const ddo of assets) {
didDTMap[ddo?.dataToken.toLowerCase()] = ddo.id
didDTMap[ddo?.services[0].datatokenAddress.toLowerCase()] = ddo.id
// harcoded until we have chainId on assets
if (chainAssetLists[ddo.chainId]) {
chainAssetLists[ddo.chainId].push(ddo?.dataToken.toLowerCase())
chainAssetLists[ddo.chainId].push(
ddo?.services[0].datatokenAddress.toLowerCase()
)
} else {
chainAssetLists[ddo.chainId] = []
chainAssetLists[ddo.chainId].push(ddo?.dataToken.toLowerCase())
chainAssetLists[ddo.chainId].push(
ddo?.services[0].datatokenAddress.toLowerCase()
)
}
}
let poolPriceResponse: AssetsPoolPricePool[] = []
@ -464,7 +468,7 @@ async function getAssetsPoolsExchangesAndDatatokenMap(
return [poolPriceResponse, frePriceResponse, freePriceResponse, didDTMap]
}
export async function getAssetsPriceList(assets: DDO[]): Promise<PriceList> {
export async function getAssetsPriceList(assets: Asset[]): Promise<PriceList> {
const priceList: PriceList = {}
const values: [
@ -493,15 +497,15 @@ export async function getAssetsPriceList(assets: DDO[]): Promise<PriceList> {
return priceList
}
export async function getPrice(asset: DDO): Promise<BestPrice> {
export async function getPrice(asset: Asset): Promise<BestPrice> {
const freVariables = {
datatoken: asset?.dataToken.toLowerCase()
datatoken: asset?.services[0].datatokenAddress.toLowerCase()
}
const freeVariables = {
datatoken: asset?.dataToken.toLowerCase()
datatoken: asset?.services[0].datatokenAddress.toLowerCase()
}
const poolVariables = {
datatokenAddress: asset?.dataToken.toLowerCase()
datatokenAddress: asset?.services[0].datatokenAddress.toLowerCase()
}
const queryContext = getQueryContext(Number(asset.chainId))
@ -530,9 +534,9 @@ export async function getPrice(asset: DDO): Promise<BestPrice> {
return bestPrice
}
export async function getSpotPrice(asset: DDO): Promise<number> {
export async function getSpotPrice(asset: Asset): Promise<number> {
const poolVariables = {
datatokenAddress: asset?.dataToken.toLowerCase()
datatokenAddress: asset?.services[0].datatokenAddress.toLowerCase()
}
const queryContext = getQueryContext(Number(asset.chainId))
@ -546,7 +550,7 @@ export async function getSpotPrice(asset: DDO): Promise<number> {
}
export async function getAssetsBestPrices(
assets: DDO[]
assets: Asset[]
): Promise<AssetListPrices[]> {
const assetsWithPrice: AssetListPrices[] = []
@ -561,7 +565,7 @@ export async function getAssetsBestPrices(
const frePriceResponse = values[1]
const freePriceResponse = values[2]
for (const ddo of assets) {
const dataToken = ddo.dataToken.toLowerCase()
const dataToken = ddo.services[0].datatokenAddress.toLowerCase()
const poolPrice: AssetsPoolPricePool[] = []
const frePrice: AssetsFrePriceFixedRateExchange[] = []
const freePrice: AssetFreePriceDispenser[] = []

View File

@ -3,7 +3,7 @@ import { Logger } from '@oceanprotocol/lib'
import { getOceanConfig } from './ocean'
export function accountTruncate(account: string): string {
if (!account) return
if (!account || account === '') return
const middle = account.substring(6, 38)
const truncated = account.replace(middle, '…')
return truncated

View File

@ -1,5 +1,5 @@
.display {
composes: selection from '@shared/Form/FormFields/AssetSelection.module.css';
composes: selection from '@shared/FormFields/AssetSelection/index.module.css';
}
.display [class*='loaderWrap'] {
@ -7,14 +7,14 @@
}
.scroll {
composes: scroll from '@shared/Form/FormFields/AssetSelection.module.css';
composes: scroll from '@shared/FormFields/AssetSelection/index.module.css';
margin-top: 0;
border-top: none;
width: 100%;
}
.row {
composes: row from '@shared/Form/FormFields/AssetSelection.module.css';
composes: row from '@shared/FormFields/AssetSelection/index.module.css';
}
.row:last-child {
@ -35,7 +35,7 @@
}
.title {
composes: title from '@shared/Form/FormFields/AssetSelection.module.css';
composes: title from '@shared/FormFields/AssetSelection/index.module.css';
}
.hover:hover {
@ -43,7 +43,7 @@
}
.price {
composes: price from '@shared/Form/FormFields/AssetSelection.module.css';
composes: price from '@shared/FormFields/AssetSelection/index.module.css';
}
.price [class*='symbol'] {
@ -51,9 +51,9 @@
}
.did {
composes: did from '@shared/Form/FormFields/AssetSelection.module.css';
composes: did from '@shared/FormFields/AssetSelection/index.module.css';
}
.empty {
composes: empty from '@shared/Form/FormFields/AssetSelection.module.css';
composes: empty from '@shared/FormFields/AssetSelection/index.module.css';
}

View File

@ -4,7 +4,7 @@ import Link from 'next/link'
import PriceUnit from '@shared/Price/PriceUnit'
import Loader from '@shared/atoms/Loader'
import styles from './AssetComputeList.module.css'
import { AssetSelectionAsset } from '@shared/Form/FormFields/AssetSelection'
import { AssetSelectionAsset } from '@shared/FormFields/AssetSelection'
function Empty() {
return <div className={styles.empty}>No assets found.</div>

View File

@ -1,4 +1,3 @@
import { DDO } from '@oceanprotocol/lib'
import Link from 'next/link'
import React, { ReactElement, useEffect, useState } from 'react'
import { getAssetsNames } from '@utils/aquarius'
@ -11,7 +10,7 @@ export default function AssetListTitle({
did,
title
}: {
ddo?: DDO
ddo?: Asset
did?: string
title?: string
}): ReactElement {
@ -21,8 +20,7 @@ export default function AssetListTitle({
useEffect(() => {
if (title || !appConfig.metadataCacheUri) return
if (ddo) {
const { attributes } = ddo.findServiceByType('metadata')
setAssetTitle(attributes.main.name)
setAssetTitle(ddo.metadata.name)
return
}

View File

@ -1,8 +1,7 @@
import AssetTeaser from '@shared/AssetTeaser/AssetTeaser'
import React, { useEffect, useState } from 'react'
import Pagination from '@shared/Pagination'
import styles from './AssetList.module.css'
import { DDO } from '@oceanprotocol/lib'
import styles from './index.module.css'
import classNames from 'classnames/bind'
import { getAssetsBestPrices, AssetListPrices } from '@utils/subgraph'
import Loader from '@shared/atoms/Loader'
@ -20,7 +19,7 @@ function LoaderArea() {
}
declare type AssetListProps = {
assets: DDO[]
assets: Asset[]
showPagination: boolean
page?: number
totalPages?: number

View File

@ -2,15 +2,15 @@ import React from 'react'
import Link from 'next/link'
import Dotdotdot from 'react-dotdotdot'
import Price from '@shared/Price'
import { DDO } from '@oceanprotocol/lib'
import removeMarkdown from 'remove-markdown'
import Publisher from '@shared/Publisher'
import AssetType from '@shared/AssetType'
import NetworkName from '@shared/NetworkName'
import styles from './AssetTeaser.module.css'
import { getServiceByName } from '@utils/ddo'
declare type AssetTeaserProps = {
ddo: DDO
ddo: Asset
price: BestPrice
noPublisher?: boolean
}
@ -20,12 +20,11 @@ const AssetTeaser: React.FC<AssetTeaserProps> = ({
price,
noPublisher
}: AssetTeaserProps) => {
const { attributes } = ddo.findServiceByType('metadata')
const { name, type } = attributes.main
const { name, type, description } = ddo.metadata
const { dataTokenInfo } = ddo
const isCompute = Boolean(ddo?.findServiceByType('compute'))
const isCompute = Boolean(getServiceByName(ddo, 'compute'))
const accessType = isCompute ? 'compute' : 'access'
const { owner } = ddo.publicKey[0]
const { owner } = ddo.nft
return (
<article className={`${styles.teaser} ${styles[type]}`}>
@ -49,12 +48,7 @@ const AssetTeaser: React.FC<AssetTeaserProps> = ({
<div className={styles.content}>
<Dotdotdot tagName="p" clamp={3}>
{removeMarkdown(
attributes?.additionalInformation?.description?.substring(
0,
300
) || ''
)}
{removeMarkdown(description?.substring(0, 300) || '')}
</Dotdotdot>
</div>

View File

@ -10,7 +10,7 @@ export default function DebugOutput({
return (
<div style={{ marginTop: 'var(--spacer)' }}>
<h5>{title}</h5>
<pre>
<pre style={{ wordWrap: 'break-word' }}>
<code>{JSON.stringify(output, null, 2)}</code>
</pre>
</div>

View File

@ -1,10 +1,10 @@
import React, { ReactElement } from 'react'
import { File as FileMetadata } from '@oceanprotocol/lib'
import filesize from 'filesize'
import classNames from 'classnames/bind'
import cleanupContentType from '@utils/cleanupContentType'
import styles from './index.module.css'
import Loader from '@shared/atoms/Loader'
import { FileMetadata } from '@utils/provider'
const cx = classNames.bind(styles)

View File

@ -1,3 +0,0 @@
.advancedBtn {
margin-bottom: 2rem;
}

View File

@ -1,48 +0,0 @@
import React, { ReactElement, useState, FormEvent, ChangeEvent } from 'react'
import Input from '@shared/Form/Input'
import Button from '@shared/atoms/Button'
import { Field } from 'formik'
import styles from './AdvancedSettings.module.css'
export default function AdvancedSettings(prop: {
content: FormContent
handleFieldChange: (
e: ChangeEvent<HTMLInputElement>,
field: FormFieldProps
) => void
}): ReactElement {
const [showAdvancedSettings, setShowAdvancedSettings] =
useState<boolean>(false)
function toggleAdvancedSettings(e: FormEvent<Element>) {
e.preventDefault()
setShowAdvancedSettings(!!showAdvancedSettings)
}
return (
<>
<Button
className={styles.advancedBtn}
style="text"
size="small"
onClick={toggleAdvancedSettings}
>
Advanced Settings
</Button>
{showAdvancedSettings &&
prop.content.data.map(
(field: FormFieldProps) =>
field.advanced === true && (
<Field
key={field.name}
{...field}
options={field.options}
component={Input}
onChange={(e: ChangeEvent<HTMLInputElement>) =>
prop.handleFieldChange(e, field)
}
/>
)
)}
</>
)
}

View File

@ -1,60 +0,0 @@
import React, { ReactElement, useState, useEffect } from 'react'
import { useField } from 'formik'
import { toast } from 'react-toastify'
import CustomInput from './URLInput/Input'
import { useOcean } from '@context/Ocean'
import { InputProps } from '@shared/Form/Input'
export default function CustomProvider(props: InputProps): ReactElement {
const [field, meta, helpers] = useField(props.name)
const [isLoading, setIsLoading] = useState(false)
const [providerUrl, setProviderUrl] = useState<string>()
const { ocean, config } = useOcean()
function loadProvider() {
if (!providerUrl) return
async function validateProvider() {
let valid: boolean
try {
setIsLoading(true)
valid = await ocean.provider.isValidProvider(providerUrl)
} catch (error) {
valid = false
console.error(error.message)
} finally {
valid
? toast.success('Perfect! That provider URL looks good 🐳')
: toast.error(
'Could not validate provider. Please check URL and try again'
)
setIsLoading(false)
}
}
validateProvider()
}
useEffect(() => {
loadProvider()
}, [providerUrl, config?.providerUri])
async function handleButtonClick(e: React.SyntheticEvent, url: string) {
helpers.setTouched(false)
e.preventDefault()
if (providerUrl === url) {
loadProvider()
}
setProviderUrl(url)
}
return (
<CustomInput
submitText="Validate"
{...props}
{...field}
isLoading={isLoading}
handleButtonClick={handleButtonClick}
/>
)
}

View File

@ -1,5 +0,0 @@
.datatoken {
border: 1px solid var(--border-color);
padding: calc(var(--spacer) / 3);
border-radius: var(--border-radius);
}

View File

@ -1,28 +0,0 @@
import { useField } from 'formik'
import React, { ReactElement, useEffect } from 'react'
import { utils } from '@oceanprotocol/lib'
import { InputProps } from '@shared/Form/Input'
import RefreshName from './RefreshName'
import styles from './index.module.css'
export default function Datatoken(props: InputProps): ReactElement {
const [field, meta, helpers] = useField(props.name)
async function generateName() {
const dataTokenOptions = utils.generateDatatokenName()
helpers.setValue({ ...dataTokenOptions })
}
// Generate new DT name & symbol on first mount
useEffect(() => {
generateName()
}, [])
return (
<div className={styles.datatoken}>
<strong>{field?.value?.name}</strong> {' '}
<strong>{field?.value?.symbol}</strong>
<RefreshName generateName={generateName} />
</div>
)
}

View File

@ -1,39 +0,0 @@
import React, { ReactElement, useEffect } from 'react'
import { File as FileMetadata } from '@oceanprotocol/lib/dist/node/ddo/interfaces/File'
import { prettySize } from './utils'
import cleanupContentType from '@utils/cleanupContentType'
import styles from './Info.module.css'
import { useField, useFormikContext } from 'formik'
export default function FileInfo({
name,
file
}: {
name: string
file: FileMetadata
}): ReactElement {
const { validateField } = useFormikContext()
const [field, meta, helpers] = useField(name)
// On mount, validate the field manually
useEffect(() => {
validateField(name)
}, [name, validateField])
return (
<div className={styles.info}>
<h3 className={styles.url}>{file.url}</h3>
<ul>
<li>URL confirmed</li>
{file.contentLength && <li>{prettySize(+file.contentLength)}</li>}
{file.contentType && <li>{cleanupContentType(file.contentType)}</li>}
</ul>
<button
className={styles.removeButton}
onClick={() => helpers.setValue(undefined)}
>
&times;
</button>
</div>
)
}

View File

@ -1,25 +0,0 @@
.terms {
composes: content from '@shared/Page/PageMarkdown.module.css';
padding: calc(var(--spacer) / 2);
border: 1px solid var(--border-color);
background-color: var(--background-highlight);
border-radius: var(--border-radius);
margin-bottom: calc(var(--spacer) / 2);
font-size: var(--font-size-small);
max-height: 250px;
/* smooth overflow scrolling for pre-iOS 13 */
overflow: auto;
-webkit-overflow-scrolling: touch;
}
.terms h1 {
font-size: var(--font-size-base);
margin-bottom: calc(var(--spacer) / 2);
}
.terms h2 {
font-size: var(--font-size-small);
margin-bottom: calc(var(--spacer) / 2);
}

View File

@ -1,17 +0,0 @@
import React, { ReactElement } from 'react'
import { InputProps } from '@shared/Form/Input'
import InputElement from '@shared/Form/Input/InputElement'
import styles from './Terms.module.css'
export default function Terms(props: InputProps): ReactElement {
const termsProps: InputProps = {
...props,
defaultChecked: props.value.toString() === 'true'
}
return (
<>
<InputElement {...termsProps} type="checkbox" />
</>
)
}

View File

@ -1,3 +0,0 @@
.input {
composes: input from '@shared/Form/Input/InputElement.module.css';
}

View File

@ -1,39 +0,0 @@
import React, { ReactElement } from 'react'
import Button from '@shared/atoms/Button'
import { FieldInputProps, useField } from 'formik'
import Loader from '@shared/atoms/Loader'
import styles from './Input.module.css'
import InputGroup from '@shared/Form/Input/InputGroup'
export default function URLInput({
submitText,
handleButtonClick,
isLoading,
...props
}: {
submitText: string
handleButtonClick(e: React.SyntheticEvent, data: string): void
isLoading: boolean
}): ReactElement {
const [field, meta] = useField(props as FieldInputProps<any>)
return (
<InputGroup>
<input
className={styles.input}
{...props}
type="url"
onBlur={(e: React.SyntheticEvent) => handleButtonClick(e, field.value)}
/>
<Button
style="primary"
size="small"
onClick={(e: React.SyntheticEvent) => e.preventDefault()}
disabled={!field.value}
>
{isLoading ? <Loader /> : submitText}
</Button>
</InputGroup>
)
}

View File

@ -58,11 +58,11 @@
}
.radio {
composes: radio from '@shared/Form/Input/InputElement.module.css';
composes: radio from '@shared/FormInput/InputElement.module.css';
}
.checkbox {
composes: checkbox from '@shared/Form/Input/InputElement.module.css';
composes: checkbox from '@shared/FormInput/InputElement.module.css';
}
.title {

View File

@ -4,9 +4,9 @@ import slugify from 'slugify'
import classNames from 'classnames/bind'
import PriceUnit from '@shared/Price/PriceUnit'
import External from '@images/external.svg'
import InputElement from '@shared/Form/Input/InputElement'
import InputElement from '@shared/FormInput/InputElement'
import Loader from '@shared/atoms/Loader'
import styles from './AssetSelection.module.css'
import styles from './index.module.css'
const cx = classNames.bind(styles)

View File

@ -6,6 +6,7 @@
.boxSelectionsWrapper > div {
width: 100%;
position: relative;
}
.boxSelection {
@ -36,13 +37,14 @@
}
.boxSelectionsWrapper input[type='radio']:checked + label {
color: var(--font-color-text);
border-color: var(--color-secondary);
color: var(--background-body);
border-color: var(--background-body);
background: var(--font-color-heading);
}
.boxSelection svg {
width: var(--font-size-h4);
height: var(--font-size-h4);
width: var(--font-size-h5);
height: var(--font-size-h5);
fill: currentColor;
margin-bottom: calc(var(--spacer) / 5);
}

View File

@ -1,7 +1,7 @@
import React, { ChangeEvent } from 'react'
import classNames from 'classnames/bind'
import Loader from '@shared/atoms/Loader'
import styles from './BoxSelection.module.css'
import styles from './index.module.css'
const cx = classNames.bind(styles)
@ -40,27 +40,27 @@ export default function BoxSelection({
{!options ? (
<Loader />
) : (
options.map((value: BoxSelectionOption) => (
<div key={value.name}>
options.map((option: BoxSelectionOption) => (
<div key={option.name}>
<input
id={value.name}
id={option.name}
type="radio"
className={styleClassesInput}
defaultChecked={value.checked}
defaultChecked={option.checked}
onChange={(event) => handleChange(event)}
{...props}
disabled={disabled}
value={value.name}
value={option.name}
name={name}
/>
<label
className={`${styles.boxSelection} ${styles.label}`}
htmlFor={value.name}
title={value.name}
htmlFor={option.name}
title={option.name}
>
{value.icon}
<span className={styles.title}>{value.title}</span>
{value.text}
{option.icon}
<span className={styles.title}>{option.title}</span>
{option.text}
</label>
</div>
))

View File

@ -0,0 +1,28 @@
.datatoken {
display: grid;
gap: var(--spacer);
grid-template-columns: 1fr 11fr;
margin-bottom: var(--spacer);
align-items: center;
}
.token {
border: 1px solid var(--border-color);
padding: calc(var(--spacer) / 3);
border-radius: var(--border-radius);
}
.image {
width: 128px;
height: 128px;
padding: var(--spacer);
background: var(--background-body);
border: 1px solid var(--border-color);
fill: var(--brand-violet);
border-radius: 50%;
}
.image svg {
width: 100%;
height: 100%;
}

View File

@ -0,0 +1,36 @@
import { useField } from 'formik'
import React, { ReactElement, useEffect } from 'react'
import { InputProps } from '@shared/FormInput'
import Logo from '@images/logo.svg'
import RefreshName from './RefreshName'
import styles from './index.module.css'
import { generateDatatokenName } from '@utils/datatokens'
export default function Datatoken(props: InputProps): ReactElement {
const [field, meta, helpers] = useField(props.name)
async function generateName() {
const dataTokenOptions = generateDatatokenName()
helpers.setValue({ ...dataTokenOptions })
}
// Generate new DT name & symbol on first mount
useEffect(() => {
if (field.value?.name !== '') return
generateName()
}, [field.value?.name])
return (
<div className={styles.datatoken}>
<figure className={styles.image}>
<Logo />
</figure>
<div className={styles.token}>
<strong>{field?.value?.name}</strong> {' '}
<strong>{field?.value?.symbol}</strong>
<RefreshName generateName={generateName} />
</div>
</div>
)
}

View File

@ -38,3 +38,7 @@
color: var(--font-color-text);
background-color: transparent;
}
.info li.success {
color: var(--brand-alert-green);
}

View File

@ -0,0 +1,27 @@
import React, { ReactElement } from 'react'
import { prettySize } from './utils'
import cleanupContentType from '@utils/cleanupContentType'
import styles from './Info.module.css'
import { FileMetadata } from '@utils/provider'
export default function FileInfo({
file,
handleClose
}: {
file: FileMetadata
handleClose(): void
}): ReactElement {
return (
<div className={styles.info}>
<h3 className={styles.url}>{(file as any).url}</h3>
<ul>
<li className={styles.success}> URL confirmed</li>
{file.contentLength && <li>{prettySize(+file.contentLength)}</li>}
{file.contentType && <li>{cleanupContentType(file.contentType)}</li>}
</ul>
<button className={styles.removeButton} onClick={handleClose}>
&times;
</button>
</div>
)
}

View File

@ -1,33 +1,33 @@
import React, { ReactElement, useState, useEffect } from 'react'
import React, { ReactElement, useState } from 'react'
import { useField } from 'formik'
import { toast } from 'react-toastify'
import FileInfo from './Info'
import CustomInput from '../URLInput/Input'
import { InputProps } from '@shared/Form/Input'
import { fileinfo } from '@utils/provider'
import UrlInput from '../URLInput'
import { InputProps } from '@shared/FormInput'
import { getFileInfo } from '@utils/provider'
import { useWeb3 } from '@context/Web3'
import { getOceanConfig } from '@utils/ocean'
import { useCancelToken } from '@hooks/useCancelToken'
import { initialValues } from 'src/components/Publish/_constants'
export default function FilesInput(props: InputProps): ReactElement {
const [field, meta, helpers] = useField(props.name)
const [isLoading, setIsLoading] = useState(false)
const [fileUrl, setFileUrl] = useState<string>()
const { chainId } = useWeb3()
const newCancelToken = useCancelToken()
function loadFileInfo() {
function loadFileInfo(url: string) {
const config = getOceanConfig(chainId || 1)
async function validateUrl() {
try {
setIsLoading(true)
const checkedFile = await fileinfo(
fileUrl,
const checkedFile = await getFileInfo(
url,
config?.providerUri,
newCancelToken()
)
checkedFile && helpers.setValue([checkedFile])
checkedFile && helpers.setValue([{ url, ...checkedFile[0] }])
} catch (error) {
toast.error('Could not fetch file info. Please check URL and try again')
console.error(error.message)
@ -36,38 +36,32 @@ export default function FilesInput(props: InputProps): ReactElement {
}
}
fileUrl && validateUrl()
validateUrl()
}
useEffect(() => {
loadFileInfo()
}, [fileUrl])
async function handleButtonClick(e: React.SyntheticEvent, url: string) {
// hack so the onBlur-triggered validation does not show,
// like when this field is required
helpers.setTouched(false)
// File example 'https://oceanprotocol.com/tech-whitepaper.pdf'
e.preventDefault()
loadFileInfo(url)
}
// In the case when the user re-add the same URL after it was removed (by accident or intentionally)
if (fileUrl === url) {
loadFileInfo()
}
setFileUrl(url)
function handleClose() {
helpers.setValue(initialValues.services[0].files)
helpers.setTouched(false)
}
return (
<>
{field?.value && field.value[0] && typeof field.value === 'object' ? (
<FileInfo name={props.name} file={field.value[0]} />
{field?.value?.length &&
field.value[0].url !== '' &&
field.value[0].valid ? (
<FileInfo file={field.value[0]} handleClose={handleClose} />
) : (
<CustomInput
submitText="Add File"
<UrlInput
submitText="Validate"
{...props}
{...field}
name={`${field.name}[0].url`}
hasError={Boolean(meta.touched && meta.error)}
isLoading={isLoading}
handleButtonClick={handleButtonClick}
/>

View File

@ -0,0 +1,34 @@
.nft {
display: grid;
gap: var(--spacer);
grid-template-columns: 1fr 11fr;
margin-bottom: var(--spacer);
align-items: center;
}
.token {
border: 1px solid var(--border-color);
padding: calc(var(--spacer) / 3);
border-radius: var(--border-radius);
}
.image {
display: block;
width: 128px;
height: 128px;
border-radius: var(--border-radius);
border: 1px solid var(--border-color);
position: relative;
}
.refresh {
position: absolute;
right: calc(var(--spacer) / 4);
bottom: calc(var(--spacer) / 4);
}
.refresh svg {
fill: var(--brand-pink);
width: var(--font-size-mini);
height: var(--font-size-mini);
}

View File

@ -0,0 +1,47 @@
import Button from '@shared/atoms/Button'
import { InputProps } from '@shared/FormInput'
import { generateNftOptions } from '@utils/nft'
import { useField } from 'formik'
import React, { ReactElement, useEffect } from 'react'
import Refresh from '@images/refresh.svg'
import styles from './index.module.css'
export default function Nft(props: InputProps): ReactElement {
const [field, meta, helpers] = useField(props.name)
// Generate on first mount
useEffect(() => {
if (field.value?.name !== '') return
const nftOptions = generateNftOptions()
helpers.setValue({ ...nftOptions })
}, [field.value?.name])
return (
<div className={styles.nft}>
<figure className={styles.image}>
<img src={field?.value?.image} width="128" height="128" />
<Button
style="text"
size="small"
className={styles.refresh}
title="Generate new image"
onClick={(e) => {
e.preventDefault()
const nftOptions = generateNftOptions()
helpers.setValue({ ...nftOptions })
}}
>
<Refresh />
</Button>
</figure>
<div className={styles.token}>
<strong>{field?.value?.name}</strong> {' '}
<strong>{field?.value?.symbol}</strong>
<br />
{field?.value?.description}
</div>
</div>
)
}

View File

@ -0,0 +1,9 @@
.error {
composes: error from '@shared/FormInput/index.module.css';
}
.restore {
font-family: var(--font-family-base);
text-transform: none;
font-weight: var(--font-weight-base);
}

View File

@ -0,0 +1,78 @@
import React, { ReactElement, useState } from 'react'
import { ErrorMessage, useField } from 'formik'
import UrlInput from '../URLInput'
import { useOcean } from '@context/Ocean'
import { InputProps } from '@shared/FormInput'
import FileInfo from '../FilesInput/Info'
import styles from './index.module.css'
import Button from '@shared/atoms/Button'
import { initialValues } from 'src/components/Publish/_constants'
export default function CustomProvider(props: InputProps): ReactElement {
const [field, meta, helpers] = useField(props.name)
const [isLoading, setIsLoading] = useState(false)
const { ocean, config } = useOcean()
async function validateProvider(url: string) {
setIsLoading(true)
try {
// TODO: #948 Remove ocean.provider.isValidProvider dependency.
const isValid = await ocean.provider.isValidProvider(url)
helpers.setValue({ url, valid: isValid })
helpers.setError(undefined)
} catch (error) {
helpers.setError(
'Could not validate provider. Please check URL and try again.'
)
} finally {
setIsLoading(false)
}
}
async function handleValidateButtonClick(
e: React.SyntheticEvent,
url: string
) {
e.preventDefault()
validateProvider(url)
}
function handleFileInfoClose() {
helpers.setValue({ url: '', valid: false })
helpers.setTouched(false)
}
function handleRestore(e: React.SyntheticEvent) {
e.preventDefault()
helpers.setValue(initialValues.services[0].providerUrl)
}
return field?.value?.valid ? (
<FileInfo file={field.value} handleClose={handleFileInfoClose} />
) : (
<>
<UrlInput
submitText="Validate"
{...props}
name={`${field.name}.url`}
hasError={Boolean(meta.touched && meta.error)}
isLoading={isLoading}
handleButtonClick={handleValidateButtonClick}
/>
<Button
style="text"
size="small"
onClick={handleRestore}
className={styles.restore}
>
Use Default Provider
</Button>
{typeof meta.error === 'string' && meta.touched && meta.error && (
<div className={styles.error}>
<ErrorMessage name={field.name} />
</div>
)}
</>
)
}

View File

@ -0,0 +1,17 @@
.input {
composes: input from '@shared/FormInput/InputElement.module.css';
}
.hasError {
color: var(--brand-alert-red);
border-color: var(--brand-alert-red);
}
.error {
composes: error from '@shared/FormInput/index.module.css';
}
.success {
background: var(--brand-alert-green);
opacity: 1 !important;
}

View File

@ -0,0 +1,58 @@
import React, { ReactElement } from 'react'
import Button from '@shared/atoms/Button'
import { ErrorMessage, useField } from 'formik'
import Loader from '@shared/atoms/Loader'
import styles from './index.module.css'
import InputGroup from '@shared/FormInput/InputGroup'
import InputElement from '@shared/FormInput/InputElement'
export default function URLInput({
submitText,
handleButtonClick,
isLoading,
name,
hasError,
...props
}: {
submitText: string
handleButtonClick(e: React.SyntheticEvent, data: string): void
isLoading: boolean
name: string
hasError: boolean
}): ReactElement {
const [field, meta] = useField(name)
const isButtonDisabled = !field?.value || field.value === ''
return (
<>
<InputGroup>
<InputElement
className={`${styles.input} ${
!isLoading && hasError ? styles.hasError : ''
}`}
{...props}
{...field}
type="url"
/>
<Button
style="primary"
size="small"
onClick={(e: React.SyntheticEvent) => {
e.preventDefault()
handleButtonClick(e, field.value)
}}
disabled={isButtonDisabled}
>
{isLoading ? <Loader /> : submitText}
</Button>
</InputGroup>
{meta.touched && meta.error && (
<div className={styles.error}>
<ErrorMessage name={field.name} />
</div>
)}
</>
)
}

View File

@ -3,14 +3,14 @@ import slugify from 'slugify'
import styles from './InputElement.module.css'
import { InputProps } from '.'
import FilesInput from '../FormFields/FilesInput'
import CustomProvider from '../FormFields/CustomProvider'
import Terms from '../FormFields/Terms'
import CustomProvider from '../FormFields/Provider'
import BoxSelection, { BoxSelectionOption } from '../FormFields/BoxSelection'
import Datatoken from '../FormFields/Datatoken'
import classNames from 'classnames/bind'
import AssetSelection, {
AssetSelectionAsset
} from '../FormFields/AssetSelection'
import Nft from '../FormFields/Nft'
const cx = classNames.bind(styles)
@ -54,7 +54,9 @@ export default function InputElement({
const sortedOptions =
!sortOptions && sortOptions === false
? options
: options.sort((a: string, b: string) => a.localeCompare(b))
: (options as string[]).sort((a: string, b: string) =>
a.localeCompare(b)
)
return (
<select
id={name}
@ -67,7 +69,7 @@ export default function InputElement({
<option value="">---</option>
)}
{sortedOptions &&
sortedOptions.map((option: string, index: number) => (
(sortedOptions as string[]).map((option: string, index: number) => (
<option key={index} value={option}>
{option} {postfix}
</option>
@ -89,7 +91,7 @@ export default function InputElement({
return (
<div className={styles.radioGroup}>
{options &&
options.map((option: string, index: number) => (
(options as string[]).map((option: string, index: number) => (
<div className={styles.radioWrap} key={index}>
<input
className={styles[type]}
@ -129,12 +131,12 @@ export default function InputElement({
)
case 'files':
return <FilesInput name={name} {...field} {...props} />
case 'providerUri':
case 'providerUrl':
return <CustomProvider name={name} {...field} {...props} />
case 'nft':
return <Nft name={name} {...field} {...props} />
case 'datatoken':
return <Datatoken name={name} {...field} {...props} />
case 'terms':
return <Terms name={name} options={options} {...field} {...props} />
case 'boxSelection':
return (
<BoxSelection
@ -155,6 +157,7 @@ export default function InputElement({
type={type || 'text'}
size={size}
disabled={disabled}
{...field}
{...props}
/>
{postfix && (
@ -167,6 +170,7 @@ export default function InputElement({
type={type || 'text'}
size={size}
disabled={disabled}
{...field}
{...props}
/>
)

View File

@ -6,11 +6,3 @@
display: block;
margin-bottom: calc(var(--spacer) / 4);
}
.required:after {
content: '*';
font-size: var(--font-size-base);
color: var(--color-secondary);
display: inline-block;
margin-left: 0.1rem;
}

View File

@ -2,19 +2,13 @@ import React, { ReactElement, ReactNode } from 'react'
import styles from './Label.module.css'
const Label = ({
required,
children,
...props
}: {
required?: boolean
children: ReactNode
htmlFor: string
}): ReactElement => (
<label
className={`${styles.label} ${required && styles.required}`}
title={required ? 'Required' : ''}
{...props}
>
<label className={styles.label} {...props}>
{children}
</label>
)

View File

@ -1,5 +1,5 @@
.field {
margin-bottom: var(--spacer);
margin-bottom: calc(var(--spacer) * var(--line-height));
position: relative;
}
@ -12,6 +12,13 @@
margin-bottom: calc(var(--spacer) / 2);
}
.required {
font-size: var(--font-size-base);
color: var(--color-secondary);
display: inline-block;
margin-left: 0.1rem;
}
.error {
display: inline-block;
font-size: var(--font-size-mini);

View File

@ -1,18 +1,20 @@
import React, {
FormEvent,
ChangeEvent,
FormEvent,
KeyboardEvent,
ReactElement,
ReactNode,
useEffect,
useState
} from 'react'
import InputElement from './InputElement'
import Help from './Help'
import Label from './Label'
import styles from './index.module.css'
import { ErrorMessage, FieldInputProps } from 'formik'
import classNames from 'classnames/bind'
import Disclaimer from './Disclaimer'
import Tooltip from '@shared/atoms/Tooltip'
import Markdown from '@shared/Markdown'
const cx = classNames.bind(styles)
@ -37,10 +39,10 @@ export interface InputProps {
): void
onKeyPress?(
e:
| React.KeyboardEvent<HTMLInputElement>
| React.KeyboardEvent<HTMLInputElement>
| React.KeyboardEvent<HTMLSelectElement>
| React.KeyboardEvent<HTMLTextAreaElement>
| KeyboardEvent<HTMLInputElement>
| KeyboardEvent<HTMLInputElement>
| KeyboardEvent<HTMLSelectElement>
| KeyboardEvent<HTMLTextAreaElement>
): void
rows?: number
multiple?: boolean
@ -68,47 +70,64 @@ export default function Input(props: Partial<InputProps>): ReactElement {
help,
additionalComponent,
size,
form,
field,
disclaimer,
disclaimerValues
} = props
const hasError =
props.form?.touched[field.name] && props.form?.errors[field.name]
const isFormikField = typeof field !== 'undefined'
const isNestedField = field?.name?.includes('.')
// TODO: this feels hacky as it assumes nested `values` store. But we can't use the
// `useField()` hook in here to get `meta.error` so we have to match against form?.errors?
// handling flat and nested data at same time.
const parsedFieldName =
isFormikField && (isNestedField ? field?.name.split('.') : [field?.name])
// const hasFormikError = !!meta?.touched && !!meta?.error
const hasFormikError =
form?.errors !== {} &&
form?.touched?.[parsedFieldName[0]]?.[parsedFieldName[1]] &&
form?.errors?.[parsedFieldName[0]]?.[parsedFieldName[1]]
const styleClasses = cx({
field: true,
hasError: hasError
hasError: hasFormikError
})
const [disclaimerVisible, setDisclaimerVisible] = useState(true)
useEffect(() => {
if (!isFormikField) return
if (disclaimer && disclaimerValues) {
setDisclaimerVisible(
disclaimerValues.includes(props.form?.values[field.name])
disclaimerValues.includes(
props.form?.values[parsedFieldName[0]]?.[parsedFieldName[1]]
)
)
}
}, [props.form?.values[field.name]])
}, [isFormikField, props.form?.values])
return (
<div
className={styleClasses}
data-is-submitting={props.form?.isSubmitting ? true : null}
>
<Label htmlFor={props.name} required={props.required}>
<div className={styleClasses}>
<Label htmlFor={props.name}>
{label}
{props.required && (
<span title="Required" className={styles.required}>
*
</span>
)}
{help && <Tooltip content={<Markdown text={help} />} />}
</Label>
<InputElement size={size} {...field} {...props} />
{field && hasError && (
{isFormikField && hasFormikError && (
<div className={styles.error}>
<ErrorMessage name={field.name} />
</div>
)}
{help && <Help>{help}</Help>}
{disclaimer && (
<Disclaimer visible={disclaimerVisible}>{disclaimer}</Disclaimer>
)}

View File

@ -12,6 +12,7 @@ export interface PersistProps {
isSessionStorage?: boolean
}
// TODO: refactor into functional component
class PersistImpl extends React.Component<
PersistProps & { formik: FormikProps<any> },
any

View File

@ -7,6 +7,7 @@
font-size: var(--font-size-h3);
margin-top: 0;
margin-bottom: 0;
display: inline-flex;
}
@media (min-width: 40rem) {

View File

@ -10,7 +10,7 @@ export default function PageHeader({
description,
center
}: {
title: string
title: ReactElement
description?: string
center?: boolean
}): ReactElement {

View File

@ -26,7 +26,7 @@ export default function Page({
<Container>
{title && !noPageHeader && (
<PageHeader
title={title}
title={<>{title}</>}
description={description}
center={headerCenter}
/>

View File

@ -13,7 +13,7 @@ import { retrieveDDOListByDIDs } from '@utils/aquarius'
import { CancelToken } from 'axios'
import Title from './Title'
import styles from './index.module.css'
import { DDO, Logger } from '@oceanprotocol/lib'
import { Logger } from '@oceanprotocol/lib'
import { useCancelToken } from '@hooks/useCancelToken'
const REFETCH_INTERVAL = 20000
@ -77,7 +77,7 @@ export interface Datatoken {
export interface PoolTransaction extends TransactionHistoryPoolTransactions {
networkId: number
ddo: DDO
ddo: Asset
}
const columns = [

View File

@ -21,13 +21,13 @@ export default function Publisher({
minimal?: boolean
className?: string
}): ReactElement {
const { accountId } = useWeb3()
// const { accountId } = useWeb3()
const isMounted = useIsMounted()
const [profile, setProfile] = useState<Profile>()
const [name, setName] = useState(accountTruncate(account))
const [accountEns, setAccountEns] = useState<string>()
const showAdd = account === accountId && !profile
// const showAdd = account === accountId && !profile
useEffect(() => {
if (!account) return
@ -70,7 +70,7 @@ export default function Publisher({
<Link href={`/profile/${accountEns || account}`}>
<a title="Show profile page.">{name}</a>
</Link>
{showAdd && <Add />}
{/* {showAdd && <Add />} */}
</>
)}
</div>

View File

@ -26,7 +26,9 @@ export default function TokenApproval({
const config = getOceanConfig(ddo.chainId)
const tokenAddress =
coin === 'OCEAN' ? config.oceanTokenAddress : ddo.dataTokenInfo.address
coin === 'OCEAN'
? config.oceanTokenAddress
: ddo.services[0].datatokenAddress
const spender = price.address
const checkTokenApproval = useCallback(async () => {

View File

@ -1,8 +1,8 @@
import React, { ReactElement } from 'react'
import { useWeb3 } from '@context/Web3'
import { addCustomNetwork } from '@utils/web3'
import Button from '@shared/atoms/Button'
import styles from './index.module.css'
import { addCustomNetwork } from '@utils/web3'
import useNetworkMetadata, {
getNetworkDataById,
getNetworkDisplayName

View File

@ -34,6 +34,10 @@
border-color: var(--font-color-heading);
}
.tab[aria-disabled='true'] {
cursor: not-allowed;
}
.tabContent {
padding: calc(var(--spacer) / 2);
}

View File

@ -2,9 +2,10 @@ import React, { ReactElement, ReactNode } from 'react'
import { Tab, Tabs as ReactTabs, TabList, TabPanel } from 'react-tabs'
import styles from './Tabs.module.css'
interface TabsItem {
export interface TabsItem {
title: string
content: ReactNode
disabled?: boolean
}
export default function Tabs({
@ -29,6 +30,7 @@ export default function Tabs({
className={styles.tab}
key={item.title}
onClick={handleTabChange ? () => handleTabChange(item.title) : null}
disabled={item.disabled}
>
{item.title}
</Tab>

View File

@ -1,39 +1,41 @@
import React, { ReactElement, useEffect, useState } from 'react'
import styles from './AlgorithmDatasetsListForCompute.module.css'
import { getAlgorithmDatasetsForCompute } from '@utils/aquarius'
import { AssetSelectionAsset } from '@shared/Form/FormFields/AssetSelection'
import { AssetSelectionAsset } from '@shared/FormFields/AssetSelection'
import AssetComputeList from '@shared/AssetList/AssetComputeList'
import { useAsset } from '@context/Asset'
import { DDO } from '@oceanprotocol/lib'
import { useCancelToken } from '@hooks/useCancelToken'
import { getServiceByName } from '@utils/ddo'
export default function AlgorithmDatasetsListForCompute({
algorithmDid,
dataset
ddo,
algorithmDid
}: {
ddo: Asset
algorithmDid: string
dataset: DDO
}): ReactElement {
const { type } = useAsset()
const [datasetsForCompute, setDatasetsForCompute] =
useState<AssetSelectionAsset[]>()
const newCancelToken = useCancelToken()
useEffect(() => {
if (!ddo) return
async function getDatasetsAllowedForCompute() {
const isCompute = Boolean(dataset?.findServiceByType('compute'))
const datasetComputeService = dataset.findServiceByType(
const isCompute = Boolean(getServiceByName(ddo, 'compute'))
const datasetComputeService = getServiceByName(
ddo,
isCompute ? 'compute' : 'access'
)
const datasets = await getAlgorithmDatasetsForCompute(
algorithmDid,
datasetComputeService?.serviceEndpoint,
dataset?.chainId,
ddo?.chainId,
newCancelToken()
)
setDatasetsForCompute(datasets)
}
type === 'algorithm' && getDatasetsAllowedForCompute()
}, [type])
ddo.metadata.type === 'algorithm' && getDatasetsAllowedForCompute()
}, [ddo?.metadata?.type])
return (
<div className={styles.datasetsContainer}>

Some files were not shown because too many files have changed in this diff Show More