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

Merge pull request #967 from oceanprotocol/feature/publish

Publish flow changes
This commit is contained in:
Matthias Kretschmann 2022-01-12 10:45:07 +00:00 committed by GitHub
commit 7d4d526d21
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
27 changed files with 564 additions and 175 deletions

View File

@ -2,11 +2,20 @@
"extends": ["eslint:recommended", "prettier"], "extends": ["eslint:recommended", "prettier"],
"parserOptions": { "parserOptions": {
"sourceType": "module", "sourceType": "module",
"ecmaFeatures": { "jsx": true } "ecmaFeatures": {
"jsx": true
}
},
"env": {
"browser": true,
"node": true,
"es2020": true,
"jest": true
}, },
"env": { "browser": true, "node": true, "es2020": true, "jest": true },
"settings": { "settings": {
"react": { "version": "detect" } "react": {
"version": "detect"
}
}, },
"overrides": [ "overrides": [
{ {
@ -30,7 +39,14 @@
"react/jsx-no-bind": "off", "react/jsx-no-bind": "off",
"@typescript-eslint/explicit-function-return-type": "off", "@typescript-eslint/explicit-function-return-type": "off",
"no-use-before-define": "off", "no-use-before-define": "off",
"@typescript-eslint/no-use-before-define": "error" "@typescript-eslint/no-use-before-define": "error",
"prefer-destructuring": [
"error",
{
"object": true,
"array": false
}
]
} }
} }
] ]

View File

@ -128,7 +128,7 @@
"label": "Access Type", "label": "Access Type",
"help": "Choose how you want your files to be accessible for the specified price.", "help": "Choose how you want your files to be accessible for the specified price.",
"type": "boxSelection", "type": "boxSelection",
"options": ["Download", "Compute"], "options": ["Access", "Compute"],
"required": true, "required": true,
"disclaimer": "Please do not provide downloadable personal data without the consent of the data subjects.", "disclaimer": "Please do not provide downloadable personal data without the consent of the data subjects.",
"disclaimerValues": ["Download"] "disclaimerValues": ["Download"]

11
package-lock.json generated
View File

@ -35,7 +35,6 @@
"gray-matter": "^4.0.3", "gray-matter": "^4.0.3",
"is-url-superb": "^6.1.0", "is-url-superb": "^6.1.0",
"js-cookie": "^3.0.1", "js-cookie": "^3.0.1",
"js-sha256": "^0.9.0",
"jwt-decode": "^3.1.2", "jwt-decode": "^3.1.2",
"lodash.debounce": "^4.0.8", "lodash.debounce": "^4.0.8",
"lodash.omit": "^4.5.0", "lodash.omit": "^4.5.0",
@ -14512,11 +14511,6 @@
"node": ">=12" "node": ">=12"
} }
}, },
"node_modules/js-sha256": {
"version": "0.9.0",
"resolved": "https://registry.npmjs.org/js-sha256/-/js-sha256-0.9.0.tgz",
"integrity": "sha512-sga3MHh9sgQN2+pJ9VYZ+1LPwXOxuBJBA5nrR5/ofPfuiJBE2hnjsaN8se8JznOmGLN2p49Pe5U/ttafcs/apA=="
},
"node_modules/js-sha3": { "node_modules/js-sha3": {
"version": "0.8.0", "version": "0.8.0",
"resolved": "https://registry.npmjs.org/js-sha3/-/js-sha3-0.8.0.tgz", "resolved": "https://registry.npmjs.org/js-sha3/-/js-sha3-0.8.0.tgz",
@ -33620,11 +33614,6 @@
"resolved": "https://registry.npmjs.org/js-cookie/-/js-cookie-3.0.1.tgz", "resolved": "https://registry.npmjs.org/js-cookie/-/js-cookie-3.0.1.tgz",
"integrity": "sha512-+0rgsUXZu4ncpPxRL+lNEptWMOWl9etvPHc/koSRp6MPwpRYAhmk0dUG00J4bxVV3r9uUzfo24wW0knS07SKSw==" "integrity": "sha512-+0rgsUXZu4ncpPxRL+lNEptWMOWl9etvPHc/koSRp6MPwpRYAhmk0dUG00J4bxVV3r9uUzfo24wW0knS07SKSw=="
}, },
"js-sha256": {
"version": "0.9.0",
"resolved": "https://registry.npmjs.org/js-sha256/-/js-sha256-0.9.0.tgz",
"integrity": "sha512-sga3MHh9sgQN2+pJ9VYZ+1LPwXOxuBJBA5nrR5/ofPfuiJBE2hnjsaN8se8JznOmGLN2p49Pe5U/ttafcs/apA=="
},
"js-sha3": { "js-sha3": {
"version": "0.8.0", "version": "0.8.0",
"resolved": "https://registry.npmjs.org/js-sha3/-/js-sha3-0.8.0.tgz", "resolved": "https://registry.npmjs.org/js-sha3/-/js-sha3-0.8.0.tgz",

View File

@ -45,7 +45,6 @@
"gray-matter": "^4.0.3", "gray-matter": "^4.0.3",
"is-url-superb": "^6.1.0", "is-url-superb": "^6.1.0",
"js-cookie": "^3.0.1", "js-cookie": "^3.0.1",
"js-sha256": "^0.9.0",
"jwt-decode": "^3.1.2", "jwt-decode": "^3.1.2",
"lodash.debounce": "^4.0.8", "lodash.debounce": "^4.0.8",
"lodash.omit": "^4.5.0", "lodash.omit": "^4.5.0",

View File

@ -0,0 +1,20 @@
import { useEffect, useState } from 'react'
import { NftFactory } from '@oceanprotocol/lib'
import { useWeb3 } from '@context/Web3'
import { getOceanConfig } from '@utils/ocean'
function useNftFactory(): NftFactory {
const { web3, chainId } = useWeb3()
const [nftFactory, setNftFactory] = useState<NftFactory>()
useEffect(() => {
if (!web3 || !chainId) return
const config = getOceanConfig(chainId)
const factory = new NftFactory(config.erc721FactoryAddress, web3)
setNftFactory(factory)
}, [web3, chainId])
return nftFactory
}
export default useNftFactory

View File

@ -43,36 +43,36 @@ function useConsume(): UseConsume {
try { try {
setStep(0) setStep(0)
if (!orderId) { // if (!orderId) {
// if we don't have a previous valid order, get one // if we don't have a previous valid order, get one
// const userOwnedTokens = await ocean.accounts.getTokenBalance( // const userOwnedTokens = await ocean.accounts.getTokenBalance(
// dataTokenAddress, // dataTokenAddress,
// account // account
// ) // )
// if (parseFloat(userOwnedTokens) < 1) { // if (parseFloat(userOwnedTokens) < 1) {
// setConsumeError('Not enough datatokens') // setConsumeError('Not enough datatokens')
// return 'Not enough datatokens' // return 'Not enough datatokens'
// } else { // } else {
// setStep(1) // setStep(1)
// try { // try {
// orderId = await ocean.assets.order( // orderId = await ocean.assets.order(
// did as string, // did as string,
// serviceType, // serviceType,
// accountId, // accountId,
// undefined, // undefined,
// marketFeeAddress, // marketFeeAddress,
// undefined, // undefined,
// null, // null,
// false // false
// ) // )
// LoggerInstance.log('ordercreated', orderId) // LoggerInstance.log('ordercreated', orderId)
// setStep(2) // setStep(2)
// } catch (error) { // } catch (error) {
// setConsumeError(error.message) // setConsumeError(error.message)
// return error.message // return error.message
// } // }
// } // }
} // }
setStep(3) setStep(3)
// if (orderId) // if (orderId)
// await ocean.assets.download( // await ocean.assets.download(

View File

@ -1,10 +1,16 @@
import { renderStaticWaves } from './oceanWaves' import { renderStaticWaves } from './oceanWaves'
export interface NftOptions { // https://docs.opensea.io/docs/metadata-standards
export interface NftMetadata {
name: string name: string
symbol: string symbol: string
description: string description: string
image: string image?: string
/* eslint-disable camelcase */
external_url?: string
image_data?: string
background_color?: string
/* eslint-enable camelcase */
} }
function encodeSvg(svgString: string): string { function encodeSvg(svgString: string): string {
@ -25,20 +31,23 @@ function encodeSvg(svgString: string): string {
.replace(/\s+/g, ' ') .replace(/\s+/g, ' ')
} }
export function generateNftOptions(): NftOptions { export function generateNftMetadata(): NftMetadata {
// TODO: crop image properly in the end as generated SVG waves are a super-wide image, // 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. // and add a filled background deciding on either black or white.
const image = renderStaticWaves() const image = renderStaticWaves()
// const image = new XMLSerializer().serializeToString(waves) // 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 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 = { const newNft: NftMetadata = {
name: 'Ocean Asset v4 NFT', name: 'Ocean Asset v4 NFT',
symbol: 'OCEAN-V4-NFT', symbol: 'OCEAN-V4-NFT',
description: `This NFT represents an asset in the Ocean Protocol v4 ecosystem.`, description: `This NFT represents an asset in the Ocean Protocol v4 ecosystem.`,
// TODO: ideally this includes the final DID
external_url: 'https://market.oceanprotocol.com',
background_color: '141414', // dark background
// TODO: figure out if also image URI needs base64 encoding // TODO: figure out if also image URI needs base64 encoding
// generated SVG embedded as 'data:image/svg+xml' and encoded characters // generated SVG embedded as 'data:image/svg+xml' and encoded characters
image: `data:image/svg+xml,${encodeSvg(image)}` image_data: `data:image/svg+xml,${encodeSvg(image)}`
// generated SVG embedded as 'data:image/svg+xml;base64' // generated SVG embedded as 'data:image/svg+xml;base64'
// image: `data:image/svg+xml;base64,${window?.btoa(image)}` // image: `data:image/svg+xml;base64,${window?.btoa(image)}`
// image: `data:image/svg+xml;base64,${Buffer.from(image).toString('base64')}` // image: `data:image/svg+xml;base64,${Buffer.from(image).toString('base64')}`
@ -47,15 +56,15 @@ export function generateNftOptions(): NftOptions {
return newNft return newNft
} }
export function generateNftCreateData(nftOptions: NftOptions): any { export function generateNftCreateData(nftMetadata: NftMetadata): any {
const nftCreateData = { const nftCreateData = {
name: nftOptions.name, name: nftMetadata.name,
symbol: nftOptions.symbol, symbol: nftMetadata.symbol,
templateIndex: 1, templateIndex: 1,
// TODO: figure out if Buffer.from method is working in browser in final build // TODO: figure out if Buffer.from method is working in browser in final build
// as BTOA is deprecated. // as BTOA is deprecated.
tokenURI: window?.btoa(JSON.stringify(nftOptions)) tokenURI: window?.btoa(JSON.stringify(nftMetadata))
// tokenURI: Buffer.from(JSON.stringify(nftOptions)).toString('base64') // should end up as data:application/json;base64 // tokenURI: Buffer.from(JSON.stringify(nftMetadata)).toString('base64') // should end up as data:application/json;base64
} }
return nftCreateData return nftCreateData

View File

@ -16,6 +16,10 @@ export function getOceanConfig(network: string | number): Config {
? undefined ? undefined
: process.env.NEXT_PUBLIC_INFURA_PROJECT_ID : process.env.NEXT_PUBLIC_INFURA_PROJECT_ID
) as Config ) as Config
// TODO: remove hack once address is fixed
if (network === 'rinkeby' || network === 4)
config.oceanTokenAddress = '0x8967bcf84170c91b0d24d4302c2376283b0b3a07'
return config as Config return config as Config
} }

View File

@ -1,30 +1,30 @@
import axios, { CancelToken, AxiosResponse } from 'axios' import axios, { CancelToken, AxiosResponse, Method } from 'axios'
import { LoggerInstance } from '@oceanprotocol/lib' import {
FileMetadata,
LoggerInstance,
ProviderInstance
} from '@oceanprotocol/lib'
export interface FileMetadata { export async function getEncryptedFiles(
index: number files: FileMetadata[],
valid: boolean providerUrl: string
contentType: string
contentLength: string
}
export async function getEncryptedFileUrls(
files: string[],
providerUrl: string,
did: string,
accountId: string
): Promise<string> { ): Promise<string> {
try { try {
// https://github.com/oceanprotocol/provider/blob/v4main/API.md#encrypt-endpoint // https://github.com/oceanprotocol/provider/blob/v4main/API.md#encrypt-endpoint
const url = `${providerUrl}/api/v1/services/encrypt` console.log('start encr')
const response: AxiosResponse<{ encryptedDocument: string }> = const response = await ProviderInstance.encrypt(
await axios.post(url, { files,
documentId: did, providerUrl,
signature: '', // TODO: add signature (httpMethod: Method, url: string, body: string, headers: any) => {
publisherAddress: accountId, return axios(url, {
document: files method: httpMethod,
}) data: body,
return response?.data?.encryptedDocument headers: headers
})
}
)
console.log('encr eres', response)
return response.data
} catch (error) { } catch (error) {
console.error('Error parsing json: ' + error.message) console.error('Error parsing json: ' + error.message)
} }
@ -36,9 +36,12 @@ export async function getFileInfo(
cancelToken: CancelToken cancelToken: CancelToken
): Promise<FileMetadata[]> { ): Promise<FileMetadata[]> {
try { try {
const postBody = { url } // TODO: what was the point of this?
// if (url instanceof DID) postBody = { did: url.getDid() }
// else postBody = { url }
const postBody = { url, type: 'url' }
const response: AxiosResponse<FileMetadata[]> = await axios.post( const response: AxiosResponse<FileMetadata[]> = await axios.post(
`${providerUrl}/api/v1/services/fileinfo`, `${providerUrl}/api/services/fileinfo`,
postBody, postBody,
{ cancelToken } { cancelToken }
) )

View File

@ -4,7 +4,7 @@ import classNames from 'classnames/bind'
import cleanupContentType from '@utils/cleanupContentType' import cleanupContentType from '@utils/cleanupContentType'
import styles from './index.module.css' import styles from './index.module.css'
import Loader from '@shared/atoms/Loader' import Loader from '@shared/atoms/Loader'
import { FileMetadata } from '@utils/provider' import { FileMetadata } from '@oceanprotocol/lib'
const cx = classNames.bind(styles) const cx = classNames.bind(styles)

View File

@ -7,6 +7,7 @@ const cx = classNames.bind(styles)
export interface BoxSelectionOption { export interface BoxSelectionOption {
name: string name: string
value?: string
checked: boolean checked: boolean
title: JSX.Element | string title: JSX.Element | string
icon?: JSX.Element icon?: JSX.Element
@ -50,7 +51,7 @@ export default function BoxSelection({
onChange={(event) => handleChange(event)} onChange={(event) => handleChange(event)}
{...props} {...props}
disabled={disabled} disabled={disabled}
value={option.name} value={option.value ? option.value : option.name}
name={name} name={name}
/> />
<label <label

View File

@ -2,7 +2,7 @@ import React, { ReactElement } from 'react'
import { prettySize } from './utils' import { prettySize } from './utils'
import cleanupContentType from '@utils/cleanupContentType' import cleanupContentType from '@utils/cleanupContentType'
import styles from './Info.module.css' import styles from './Info.module.css'
import { FileMetadata } from '@utils/provider' import { FileMetadata } from '@oceanprotocol/lib'
export default function FileInfo({ export default function FileInfo({
file, file,
@ -13,7 +13,7 @@ export default function FileInfo({
}): ReactElement { }): ReactElement {
return ( return (
<div className={styles.info}> <div className={styles.info}>
<h3 className={styles.url}>{(file as any).url}</h3> <h3 className={styles.url}>{file.url}</h3>
<ul> <ul>
<li className={styles.success}> URL confirmed</li> <li className={styles.success}> URL confirmed</li>
{file.contentLength && <li>{prettySize(+file.contentLength)}</li>} {file.contentLength && <li>{prettySize(+file.contentLength)}</li>}

View File

@ -1,6 +1,6 @@
import Button from '@shared/atoms/Button' import Button from '@shared/atoms/Button'
import { InputProps } from '@shared/FormInput' import { InputProps } from '@shared/FormInput'
import { generateNftOptions } from '@utils/nft' import { generateNftMetadata } from '@utils/nft'
import { useField } from 'formik' import { useField } from 'formik'
import React, { ReactElement, useEffect } from 'react' import React, { ReactElement, useEffect } from 'react'
import Refresh from '@images/refresh.svg' import Refresh from '@images/refresh.svg'
@ -13,14 +13,14 @@ export default function Nft(props: InputProps): ReactElement {
useEffect(() => { useEffect(() => {
if (field.value?.name !== '') return if (field.value?.name !== '') return
const nftOptions = generateNftOptions() const nftOptions = generateNftMetadata()
helpers.setValue({ ...nftOptions }) helpers.setValue({ ...nftOptions })
}, [field.value?.name]) }, [field.value?.name])
return ( return (
<div className={styles.nft}> <div className={styles.nft}>
<figure className={styles.image}> <figure className={styles.image}>
<img src={field?.value?.image} width="128" height="128" /> <img src={field?.value?.image_data} width="128" height="128" />
<Button <Button
style="text" style="text"
size="small" size="small"
@ -28,8 +28,8 @@ export default function Nft(props: InputProps): ReactElement {
title="Generate new image" title="Generate new image"
onClick={(e) => { onClick={(e) => {
e.preventDefault() e.preventDefault()
const nftOptions = generateNftOptions() const nftMetadata = generateNftMetadata()
helpers.setValue({ ...nftOptions }) helpers.setValue({ ...nftMetadata })
}} }}
> >
<Refresh /> <Refresh />

View File

@ -5,7 +5,8 @@ import {
ComputeOutput, ComputeOutput,
Asset, Asset,
DDO, DDO,
PublisherTrustedAlgorithm PublisherTrustedAlgorithm,
FileMetadata
} from '@oceanprotocol/lib' } from '@oceanprotocol/lib'
import { toast } from 'react-toastify' import { toast } from 'react-toastify'
import Price from '@shared/Price' import Price from '@shared/Price'
@ -14,7 +15,6 @@ import Alert from '@shared/atoms/Alert'
import { useSiteMetadata } from '@hooks/useSiteMetadata' import { useSiteMetadata } from '@hooks/useSiteMetadata'
import { useWeb3 } from '@context/Web3' import { useWeb3 } from '@context/Web3'
import { usePricing } from '@hooks/usePricing' import { usePricing } from '@hooks/usePricing'
import { useAsset } from '@context/Asset'
import { import {
generateBaseQuery, generateBaseQuery,
getFilterTerm, getFilterTerm,
@ -36,7 +36,6 @@ import ComputeJobs from '../../../Profile/History/ComputeJobs'
import { useCancelToken } from '@hooks/useCancelToken' import { useCancelToken } from '@hooks/useCancelToken'
import { useIsMounted } from '@hooks/useIsMounted' import { useIsMounted } from '@hooks/useIsMounted'
import { SortTermOptions } from '../../../../@types/aquarius/SearchQuery' import { SortTermOptions } from '../../../../@types/aquarius/SearchQuery'
import { FileMetadata } from '@utils/provider'
export default function Compute({ export default function Compute({
ddo, ddo,

View File

@ -16,8 +16,7 @@ import { secondsToString } from '@utils/ddo'
import AlgorithmDatasetsListForCompute from './Compute/AlgorithmDatasetsListForCompute' import AlgorithmDatasetsListForCompute from './Compute/AlgorithmDatasetsListForCompute'
import styles from './Consume.module.css' import styles from './Consume.module.css'
import { useIsMounted } from '@hooks/useIsMounted' import { useIsMounted } from '@hooks/useIsMounted'
import { FileMetadata } from '@utils/provider' import { Asset, FileMetadata } from '@oceanprotocol/lib'
import { Asset } from '@oceanprotocol/lib'
const previousOrderQuery = gql` const previousOrderQuery = gql`
query PreviousOrder($id: String!, $account: String!) { query PreviousOrder($id: String!, $account: String!) {

View File

@ -1,7 +1,7 @@
import React, { ReactElement, useState, useEffect } from 'react' import React, { ReactElement, useState, useEffect } from 'react'
import Compute from './Compute' import Compute from './Compute'
import Consume from './Consume' import Consume from './Consume'
import { Asset, LoggerInstance } from '@oceanprotocol/lib' import { Asset, FileMetadata, LoggerInstance } from '@oceanprotocol/lib'
import Tabs, { TabsItem } from '@shared/atoms/Tabs' import Tabs, { TabsItem } from '@shared/atoms/Tabs'
import { compareAsBN } from '@utils/numbers' import { compareAsBN } from '@utils/numbers'
import Pool from './Pool' import Pool from './Pool'
@ -9,7 +9,7 @@ import Trade from './Trade'
import { useAsset } from '@context/Asset' import { useAsset } from '@context/Asset'
import { useWeb3 } from '@context/Web3' import { useWeb3 } from '@context/Web3'
import Web3Feedback from '@shared/Web3Feedback' import Web3Feedback from '@shared/Web3Feedback'
import { FileMetadata, getFileInfo } from '@utils/provider' import { getFileInfo } from '@utils/provider'
import { getOceanConfig } from '@utils/ocean' import { getOceanConfig } from '@utils/ocean'
import { useCancelToken } from '@hooks/useCancelToken' import { useCancelToken } from '@hooks/useCancelToken'
import { useIsMounted } from '@hooks/useIsMounted' import { useIsMounted } from '@hooks/useIsMounted'

View File

@ -39,7 +39,7 @@ export default function Actions({
<Button <Button
type="submit" type="submit"
style="primary" style="primary"
disabled={values.user.accountId === '' || !isValid} disabled={values.user.accountId === ''}
> >
Submit Submit
</Button> </Button>

View File

@ -90,5 +90,6 @@
.success.current:before { .success.current:before {
background: var(--font-color-heading); background: var(--font-color-heading);
color: var(--brand-alert-green);
border-color: var(--brand-alert-green); border-color: var(--brand-alert-green);
} }

View File

@ -19,10 +19,12 @@ export default function ServicesFields(): ReactElement {
const { values, setFieldValue, touched, setTouched } = const { values, setFieldValue, touched, setTouched } =
useFormikContext<FormPublishData>() useFormikContext<FormPublishData>()
// name and title should be download, but option value should be access, probably the best way would be to change the component so that option is an object like {name,value}
const accessTypeOptions = [ const accessTypeOptions = [
{ {
name: accessTypeOptionsTitles[0].toLowerCase(), name: 'download',
title: accessTypeOptionsTitles[0], value: accessTypeOptionsTitles[0].toLowerCase(),
title: 'Download',
icon: <IconDownload />, icon: <IconDownload />,
// BoxSelection component is not a Formik component // BoxSelection component is not a Formik component
// so we need to handle checked state manually. // so we need to handle checked state manually.
@ -31,6 +33,7 @@ export default function ServicesFields(): ReactElement {
}, },
{ {
name: accessTypeOptionsTitles[1].toLowerCase(), name: accessTypeOptionsTitles[1].toLowerCase(),
value: accessTypeOptionsTitles[1].toLowerCase(),
title: accessTypeOptionsTitles[1], title: accessTypeOptionsTitles[1],
icon: <IconCompute />, icon: <IconCompute />,
checked: checked:
@ -55,7 +58,7 @@ export default function ServicesFields(): ReactElement {
setFieldValue( setFieldValue(
'services[0].access', 'services[0].access',
values.services[0].algorithmPrivacy === true ? 'compute' : 'download' values.services[0].algorithmPrivacy === true ? 'compute' : 'access'
) )
}, [values.services[0].algorithmPrivacy, setFieldValue]) }, [values.services[0].algorithmPrivacy, setFieldValue])

View File

@ -2,12 +2,17 @@ import { ReactElement, useEffect } from 'react'
import { useFormikContext } from 'formik' import { useFormikContext } from 'formik'
import { wizardSteps } from './_constants' import { wizardSteps } from './_constants'
import { useWeb3 } from '@context/Web3' import { useWeb3 } from '@context/Web3'
import { FormPublishData } from './_types' import { FormPublishData, PublishFeedback } from './_types'
export function Steps(): ReactElement { export function Steps({
feedback
}: {
feedback: PublishFeedback
}): ReactElement {
const { chainId, accountId } = useWeb3() const { chainId, accountId } = useWeb3()
const { values, setFieldValue } = useFormikContext<FormPublishData>() const { values, setFieldValue } = useFormikContext<FormPublishData>()
// auto-sync user chainId & account into form data values
useEffect(() => { useEffect(() => {
if (!chainId || !accountId) return if (!chainId || !accountId) return
@ -15,6 +20,11 @@ export function Steps(): ReactElement {
setFieldValue('user.accountId', accountId) setFieldValue('user.accountId', accountId)
}, [chainId, accountId, setFieldValue]) }, [chainId, accountId, setFieldValue])
// auto-sync publish feedback into form data values
useEffect(() => {
setFieldValue('feedback', feedback)
}, [feedback, setFieldValue])
const { component } = wizardSteps.filter( const { component } = wizardSteps.filter(
(stepContent) => stepContent.step === values.user.stepCurrent (stepContent) => stepContent.step === values.user.stepCurrent
)[0] )[0]

View File

@ -0,0 +1,18 @@
import { ListItem } from '@shared/atoms/Lists'
import { useFormikContext } from 'formik'
import React, { ReactElement } from 'react'
import { FormPublishData } from '../_types'
export function Feedback(): ReactElement {
const { values } = useFormikContext<FormPublishData>()
const items = Object.entries(values.feedback).map(([key, value], index) => {
return (
<ListItem ol key={index}>
{value.name}
</ListItem>
)
})
return <ol>{items}</ol>
}

View File

@ -2,6 +2,7 @@ import React, { ReactElement } from 'react'
import styles from './index.module.css' import styles from './index.module.css'
import { FormPublishData } from '../_types' import { FormPublishData } from '../_types'
import { useFormikContext } from 'formik' import { useFormikContext } from 'formik'
import { Feedback } from './Feedback'
export default function Submission(): ReactElement { export default function Submission(): ReactElement {
const { values, handleSubmit } = useFormikContext<FormPublishData>() const { values, handleSubmit } = useFormikContext<FormPublishData>()
@ -12,6 +13,7 @@ export default function Submission(): ReactElement {
Place to teach about what happens next, output all the steps in background Place to teach about what happens next, output all the steps in background
in some list, after submission continously update this list with the in some list, after submission continously update this list with the
status of the submission. status of the submission.
<Feedback />
</div> </div>
) )
} }

View File

@ -57,7 +57,7 @@ export const initialValues: FormPublishData = {
accountId: '' accountId: ''
}, },
metadata: { metadata: {
nft: { name: '', symbol: '', description: '', image: '' }, nft: { name: '', symbol: '', description: '', image_data: '' },
type: 'dataset', type: 'dataset',
name: '', name: '',
author: '', author: '',

View File

@ -1,6 +1,6 @@
import { ServiceComputeOptions } from '@oceanprotocol/lib' import { ServiceComputeOptions } from '@oceanprotocol/lib'
import { DataTokenOptions } from '@utils/datatokens' import { DataTokenOptions } from '@utils/datatokens'
import { NftOptions } from '@utils/nft' import { NftMetadata } from '@utils/nft'
import { ReactElement } from 'react' import { ReactElement } from 'react'
interface FileMetadata { interface FileMetadata {
@ -28,7 +28,7 @@ export interface FormPublishData {
chainId: number chainId: number
} }
metadata: { metadata: {
nft: NftOptions nft: NftMetadata
type: 'dataset' | 'algorithm' type: 'dataset' | 'algorithm'
name: string name: string
description: string description: string
@ -43,6 +43,7 @@ export interface FormPublishData {
} }
services: FormPublishService[] services: FormPublishService[]
pricing: PriceOptions pricing: PriceOptions
feedback?: PublishFeedback
} }
export interface StepContent { export interface StepContent {
@ -50,3 +51,11 @@ export interface StepContent {
title: string title: string
component: ReactElement component: ReactElement
} }
export interface PublishFeedback {
[key: number]: {
name: string
status: 'success' | 'error' | 'pending'
message?: string
}
}

View File

@ -1,8 +1,23 @@
import { DDO, Metadata, Service } from '@oceanprotocol/lib' import {
Config,
DDO,
Erc20CreateParams,
FreCreationParams,
generateDid,
getHash,
LoggerInstance,
Metadata,
NftCreateData,
NftFactory,
Pool,
PoolCreationParams,
Service,
ZERO_ADDRESS
} from '@oceanprotocol/lib'
import { mapTimeoutStringToSeconds } from '@utils/ddo' import { mapTimeoutStringToSeconds } from '@utils/ddo'
import { getEncryptedFileUrls } from '@utils/provider' import { getEncryptedFiles } from '@utils/provider'
import { sha256 } from 'js-sha256'
import slugify from 'slugify' import slugify from 'slugify'
import Web3 from 'web3'
import { import {
algorithmContainerPresets, algorithmContainerPresets,
MetadataAlgorithmContainer MetadataAlgorithmContainer
@ -66,9 +81,10 @@ export async function transformPublishFormToDdo(
} = metadata } = metadata
const { access, files, links, providerUrl, timeout } = services[0] const { access, files, links, providerUrl, timeout } = services[0]
const did = nftAddress ? `0x${sha256(`${nftAddress}${chainId}`)}` : '0x...' const did = nftAddress ? generateDid(nftAddress, chainId) : '0x...'
const currentTime = dateToStringNoMS(new Date()) const currentTime = dateToStringNoMS(new Date())
const isPreview = !datatokenAddress && !nftAddress
console.log('did', did, isPreview)
// Transform from files[0].url to string[] assuming only 1 file // Transform from files[0].url to string[] assuming only 1 file
const filesTransformed = files?.length && files[0].valid && [files[0].url] const filesTransformed = files?.length && files[0].valid && [files[0].url]
const linksTransformed = links?.length && links[0].valid && [links[0].url] const linksTransformed = links?.length && links[0].valid && [links[0].url]
@ -114,21 +130,24 @@ export async function transformPublishFormToDdo(
} }
}) })
} }
console.log('new meta', newMetadata)
// Encrypt just created string[] of urls // this is the default format hardcoded
const file = [
{
type: 'url',
url: files[0].url,
method: 'GET'
}
]
const filesEncrypted = const filesEncrypted =
!isPreview &&
files?.length && files?.length &&
files[0].valid && files[0].valid &&
(await getEncryptedFileUrls( (await getEncryptedFiles(file, providerUrl.url))
filesTransformed,
providerUrl.url,
did,
accountId
))
const newService: Service = { const newService: Service = {
// TODO: give some id id: getHash(datatokenAddress + filesEncrypted),
id: '1',
type: access, type: access,
files: filesEncrypted || '', files: filesEncrypted || '',
datatokenAddress, datatokenAddress,
@ -151,10 +170,12 @@ export async function transformPublishFormToDdo(
// again, we can assume if `datatokenAddress` is not passed, // again, we can assume if `datatokenAddress` is not passed,
// we are on preview. // we are on preview.
...(!datatokenAddress && { ...(!datatokenAddress && {
dataTokenInfo: { datatokens: [
name: values.services[0].dataTokenOptions.name, {
symbol: values.services[0].dataTokenOptions.symbol name: values.services[0].dataTokenOptions.name,
}, symbol: values.services[0].dataTokenOptions.symbol
}
],
nft: { nft: {
owner: accountId owner: accountId
} }
@ -163,3 +184,132 @@ export async function transformPublishFormToDdo(
return newDdo return newDdo
} }
export async function createTokensAndPricing(
values: FormPublishData,
accountId: string,
marketFeeAddress: string,
config: Config,
nftFactory: NftFactory,
web3: Web3
) {
// image not included here for gas fees reasons. It is also an issue to reaserch how we add the image in the nft
const nftCreateData: NftCreateData = {
name: values.metadata.nft.name,
symbol: values.metadata.nft.symbol,
// tokenURI: values.metadata.nft.image_data,
tokenURI: '',
templateIndex: 1
}
// TODO: cap is hardcoded for now to 1000, this needs to be discussed at some point
// fee is default 0 for now
// TODO: templateIndex is hardcoded for now but this is incorrect, in the future it should be something like 1 for pools, and 2 for fre and free
const ercParams: Erc20CreateParams = {
templateIndex: values.pricing.type === 'dynamic' ? 1 : 2,
minter: accountId,
feeManager: accountId,
mpFeeAddress: marketFeeAddress,
feeToken: config.oceanTokenAddress,
feeAmount: `0`,
cap: '1000',
name: values.services[0].dataTokenOptions.name,
symbol: values.services[0].dataTokenOptions.symbol
}
let erc721Address = ''
let datatokenAddress = ''
// TODO: cleaner code for this huge switch !??!?
switch (values.pricing.type) {
case 'dynamic': {
// no vesting in market by default, maybe at a later time , vestingAmount and vestedBlocks are hardcoded
// we use only ocean as basetoken
// TODO: discuss swapFeeLiquidityProvider, swapFeeMarketPlaceRunner
const poolParams: PoolCreationParams = {
ssContract: config.sideStakingAddress,
basetokenAddress: config.oceanTokenAddress,
basetokenSender: config.erc721FactoryAddress,
publisherAddress: accountId,
marketFeeCollector: marketFeeAddress,
poolTemplateAddress: config.poolTemplateAddress,
rate: values.pricing.price.toString(),
basetokenDecimals: 18,
vestingAmount: '0',
vestedBlocks: 2726000,
initialBasetokenLiquidity: values.pricing.amountOcean.toString(),
swapFeeLiquidityProvider: 1e15,
swapFeeMarketRunner: 1e15
}
// the spender in this case is the erc721Factory because we are delegating
const pool = new Pool(web3, LoggerInstance)
const txApp = await pool.approve(
accountId,
config.oceanTokenAddress,
config.erc721FactoryAddress,
'200',
false
)
console.log('aprove', txApp)
const result = await nftFactory.createNftErcWithPool(
accountId,
nftCreateData,
ercParams,
poolParams
)
erc721Address = result.events.NFTCreated.returnValues[0]
datatokenAddress = result.events.TokenCreated.returnValues[0]
break
}
case 'fixed': {
const freParams: FreCreationParams = {
fixedRateAddress: config.fixedRateExchangeAddress,
baseTokenAddress: config.oceanTokenAddress,
owner: accountId,
marketFeeCollector: marketFeeAddress,
baseTokenDecimals: 18,
dataTokenDecimals: 18,
fixedRate: values.pricing.price.toString(),
marketFee: 1e15,
withMint: true
}
const result = await nftFactory.createNftErcWithFixedRate(
accountId,
nftCreateData,
ercParams,
freParams
)
erc721Address = result.events.NFTCreated.returnValues[0]
datatokenAddress = result.events.TokenCreated.returnValues[0]
break
}
case 'free': {
// maxTokens - how many tokens cand be dispensed when someone requests . If maxTokens=2 then someone can't request 3 in one tx
// maxBalance - how many dt the user has in it's wallet before the dispenser will not dispense dt
// both will be just 1 for the market
const dispenserParams = {
dispenserAddress: config.dispenserAddress,
maxTokens: web3.utils.toWei('1'),
maxBalance: web3.utils.toWei('1'),
withMint: true,
allowedSwapper: ZERO_ADDRESS
}
const result = await nftFactory.createNftErcWithDispenser(
accountId,
nftCreateData,
ercParams,
dispenserParams
)
erc721Address = result.events.NFTCreated.returnValues[0]
datatokenAddress = result.events.TokenCreated.returnValues[0]
break
}
}
return { erc721Address, datatokenAddress }
}

View File

@ -46,7 +46,7 @@ const validationService = {
}), }),
timeout: Yup.string().required('Required'), timeout: Yup.string().required('Required'),
access: Yup.string() access: Yup.string()
.matches(/compute|download/g) .matches(/compute|access/g)
.required('Required'), .required('Required'),
providerUrl: Yup.object().shape({ providerUrl: Yup.object().shape({
url: Yup.string().url('Must be a valid URL.').required('Required'), url: Yup.string().url('Must be a valid URL.').required('Required'),

View File

@ -1,10 +1,9 @@
import React, { ReactElement, useState, useRef } from 'react' import React, { ReactElement, useState, useRef } from 'react'
import { Form, Formik, validateYupSchema } from 'formik' import { Form, Formik } from 'formik'
import { initialValues } from './_constants' import { initialValues } from './_constants'
import { validationSchema } from './_validation'
import { useAccountPurgatory } from '@hooks/useAccountPurgatory' import { useAccountPurgatory } from '@hooks/useAccountPurgatory'
import { useWeb3 } from '@context/Web3' import { useWeb3 } from '@context/Web3'
import { transformPublishFormToDdo } from './_utils' import { createTokensAndPricing, transformPublishFormToDdo } from './_utils'
import PageHeader from '@shared/Page/PageHeader' import PageHeader from '@shared/Page/PageHeader'
import Title from './Title' import Title from './Title'
import styles from './index.module.css' import styles from './index.module.css'
@ -12,10 +11,15 @@ import Actions from './Actions'
import Debug from './Debug' import Debug from './Debug'
import Navigation from './Navigation' import Navigation from './Navigation'
import { Steps } from './Steps' import { Steps } from './Steps'
import { FormPublishData } from './_types' import { FormPublishData, PublishFeedback } from './_types'
import { sha256 } from 'js-sha256'
import { generateNftCreateData } from '@utils/nft'
import { useUserPreferences } from '@context/UserPreferences' import { useUserPreferences } from '@context/UserPreferences'
import useNftFactory from '@hooks/contracts/useNftFactory'
import { Nft, getHash, ProviderInstance } from '@oceanprotocol/lib'
import { useSiteMetadata } from '@hooks/useSiteMetadata'
import axios, { Method } from 'axios'
import { useCancelToken } from '@hooks/useCancelToken'
import { getOceanConfig } from '@utils/ocean'
import { validationSchema } from './_validation'
// TODO: restore FormikPersist, add back clear form action // TODO: restore FormikPersist, add back clear form action
const formName = 'ocean-publish-form' const formName = 'ocean-publish-form'
@ -26,58 +30,211 @@ export default function PublishPage({
content: { title: string; description: string; warning: string } content: { title: string; description: string; warning: string }
}): ReactElement { }): ReactElement {
const { debug } = useUserPreferences() const { debug } = useUserPreferences()
const { accountId, chainId } = useWeb3() const { accountId, web3, chainId } = useWeb3()
const { isInPurgatory, purgatoryData } = useAccountPurgatory(accountId) const { isInPurgatory, purgatoryData } = useAccountPurgatory(accountId)
// TODO: success & error states need to be handled for each step we want to display
// most likely with a nested data structure.
const [success, setSuccess] = useState<string>()
const [error, setError] = useState<string>()
const scrollToRef = useRef() const scrollToRef = useRef()
const { appConfig } = useSiteMetadata()
const nftFactory = useNftFactory()
const newCancelToken = useCancelToken()
const [feedback, setFeedback] = useState<PublishFeedback>({
1: {
name: 'Create Tokens & Pricing',
status: 'pending'
},
2: {
name: 'Encrypt DDO',
status: 'pending'
},
3: {
name: 'Publish DDO',
status: 'pending'
}
})
async function handleSubmit(values: FormPublishData) { async function handleSubmit(values: FormPublishData) {
let _erc721Address, _datatokenAddress, _ddo, _encryptedDdo
// --------------------------------------------------
// 1. Create NFT & datatokens & create pricing schema
// --------------------------------------------------
try { try {
// -------------------------------------------------- const config = getOceanConfig(chainId)
// 1. Mint NFT & datatokens & put in pool console.log('config', config)
// --------------------------------------------------
// const nftOptions = values.metadata.nft
// const nftCreateData = generateNftCreateData(nftOptions)
// TODO: figure out syntax of ercParams we most likely need to pass const { erc721Address, datatokenAddress } = await createTokensAndPricing(
// to createNftWithErc() as we need to pass options for the datatoken. values,
// const ercParams = {} accountId,
// const priceOptions = { appConfig.marketFeeAddress,
// // swapFee is tricky: to get 0.1% you need to send 0.001 as value config,
// swapFee: `${values.pricing.swapFee / 100}` nftFactory,
// } web3
// const txMint = await createNftWithErc(accountId, nftCreateData) )
// TODO: figure out how to get nftAddress & datatokenAddress from tx log. const isSuccess = erc721Address && datatokenAddress
// const { nftAddress, datatokenAddress } = txMint.logs[0].args _erc721Address = erc721Address
// if (!nftAddress || !datatokenAddress) { throw new Error() } _datatokenAddress = datatokenAddress
//
// --------------------------------------------------
// 2. Construct and publish DDO
// --------------------------------------------------
// const did = sha256(`${nftAddress}${chainId}`)
// const ddo = transformPublishFormToDdo(values, datatokenAddress, nftAddress)
// const txPublish = await publish(ddo)
//
// --------------------------------------------------
// 3. Integrity check of DDO before & after publishing
// --------------------------------------------------
// const checksumBefore = sha256(ddo)
// const ddoFromChain = await getDdoFromChain(ddo.id)
// const ddoFromChainDecrypted = await decryptDdo(ddoFromChain)
// const checksumAfter = sha256(ddoFromChainDecrypted)
// if (checksumBefore !== checksumAfter) { setFeedback({
// throw new Error('DDO integrity check failed!') ...feedback,
// } 1: {
setSuccess('Your DDO was published successfully!') ...feedback[1],
status: isSuccess ? 'success' : 'error'
}
})
} catch (error) { } catch (error) {
setError(error.message) console.error('error', error.message)
setFeedback({
...feedback,
1: {
...feedback[1],
status: 'error',
message: error.message
}
})
} }
// --------------------------------------------------
// 2. Construct and encypt DDO
// --------------------------------------------------
try {
const ddo = await transformPublishFormToDdo(
values,
_datatokenAddress,
_erc721Address
)
_ddo = ddo
const encryptedResponse = await ProviderInstance.encrypt(
ddo,
values.services[0].providerUrl.url,
(httpMethod: Method, url: string, body: string, headers: any) => {
return axios(url, {
method: httpMethod,
data: body,
headers: headers,
cancelToken: newCancelToken()
})
}
)
const encryptedDdo = encryptedResponse.data
_encryptedDdo = encryptedDdo
console.log('ddo', JSON.stringify(ddo))
setFeedback({
...feedback,
2: {
...feedback[2],
status: encryptedDdo ? 'success' : 'error'
}
})
} catch (error) {
console.error('error', error.message)
setFeedback({
...feedback,
2: {
...feedback[2],
status: 'error',
message: error.message
}
})
}
// --------------------------------------------------
// 3. Publish DDO
// --------------------------------------------------
try {
// TODO: this whole setMetadata needs to go in a function ,too many hardcoded/calculated params
// TODO: hash generation : this needs to be moved in a function (probably on ocean.js) after we figure out what is going on in provider, leave it here for now
const metadataHash = getHash(JSON.stringify(_ddo))
const nft = new Nft(web3)
// theoretically used by aquarius or provider, not implemented yet, will remain hardcoded
const flags = '0x2'
const res = await nft.setMetadata(
_erc721Address,
accountId,
0,
values.services[0].providerUrl.url,
'',
flags,
_encryptedDdo,
'0x' + metadataHash
)
console.log('result', res)
setFeedback({
...feedback,
3: {
...feedback[3],
status: res ? 'success' : 'error'
}
})
} catch (error) {
console.error('error', error.message)
setFeedback({
...feedback,
3: {
...feedback[3],
status: 'error',
message: error.message
}
})
}
// --------------------------------------------------
// 3. Integrity check of DDO before & after publishing
// --------------------------------------------------
// TODO: not sure we want to do this at this step, seems overkill
// if we want to do this we just need to fetch it from aquarius. If we want to fetch from chain and decrypt, we would have more metamask pop-ups (not UX friendly)
// decrypt also validates the checksum
// TODO: remove the commented lines of code until `setSuccess`, didn't remove them yet because maybe i missed something
// --------------------------------------------------
// 1. Mint NFT & datatokens & put in pool
// --------------------------------------------------
// const nftOptions = values.metadata.nft
// const nftCreateData = generateNftCreateData(nftOptions)
// figure out syntax of ercParams we most likely need to pass
// to createNftWithErc() as we need to pass options for the datatoken.
// const ercParams = {}
// const priceOptions = {
// // swapFee is tricky: to get 0.1% you need to send 0.001 as value
// swapFee: `${values.pricing.swapFee / 100}`
// }
// const txMint = await createNftWithErc(accountId, nftCreateData)
// figure out how to get nftAddress & datatokenAddress from tx log.
// const { nftAddress, datatokenAddress } = txMint.logs[0].args
// if (!nftAddress || !datatokenAddress) { throw new Error() }
//
// --------------------------------------------------
// 2. Construct and publish DDO
// --------------------------------------------------
// const did = sha256(`${nftAddress}${chainId}`)
// const ddo = transformPublishFormToDdo(values, datatokenAddress, nftAddress)
// const txPublish = await publish(ddo)
//
// --------------------------------------------------
// 3. Integrity check of DDO before & after publishing
// --------------------------------------------------
// const checksumBefore = sha256(ddo)
// const ddoFromChain = await getDdoFromChain(ddo.id)
// const ddoFromChainDecrypted = await decryptDdo(ddoFromChain)
// const checksumAfter = sha256(ddoFromChainDecrypted)
// if (checksumBefore !== checksumAfter) {
// throw new Error('DDO integrity check failed!')
// }
} }
return isInPurgatory && purgatoryData ? null : ( return isInPurgatory && purgatoryData ? null : (
@ -97,7 +254,7 @@ export default function PublishPage({
/> />
<Form className={styles.form} ref={scrollToRef}> <Form className={styles.form} ref={scrollToRef}>
<Navigation /> <Navigation />
<Steps /> <Steps feedback={feedback} />
<Actions scrollToRef={scrollToRef} /> <Actions scrollToRef={scrollToRef} />
</Form> </Form>
{debug && <Debug />} {debug && <Debug />}