1
0
mirror of https://github.com/oceanprotocol/market.git synced 2024-06-30 05:41:41 +02: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"],
"parserOptions": {
"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": {
"react": { "version": "detect" }
"react": {
"version": "detect"
}
},
"overrides": [
{
@ -30,7 +39,14 @@
"react/jsx-no-bind": "off",
"@typescript-eslint/explicit-function-return-type": "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",
"help": "Choose how you want your files to be accessible for the specified price.",
"type": "boxSelection",
"options": ["Download", "Compute"],
"options": ["Access", "Compute"],
"required": true,
"disclaimer": "Please do not provide downloadable personal data without the consent of the data subjects.",
"disclaimerValues": ["Download"]

11
package-lock.json generated
View File

@ -35,7 +35,6 @@
"gray-matter": "^4.0.3",
"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",
@ -14512,11 +14511,6 @@
"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": {
"version": "0.8.0",
"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",
"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": {
"version": "0.8.0",
"resolved": "https://registry.npmjs.org/js-sha3/-/js-sha3-0.8.0.tgz",

View File

@ -45,7 +45,6 @@
"gray-matter": "^4.0.3",
"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",

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

View File

@ -1,10 +1,16 @@
import { renderStaticWaves } from './oceanWaves'
export interface NftOptions {
// https://docs.opensea.io/docs/metadata-standards
export interface NftMetadata {
name: string
symbol: 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 {
@ -25,20 +31,23 @@ function encodeSvg(svgString: string): string {
.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,
// 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 = {
const newNft: NftMetadata = {
name: 'Ocean Asset v4 NFT',
symbol: 'OCEAN-V4-NFT',
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
// 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'
// image: `data:image/svg+xml;base64,${window?.btoa(image)}`
// image: `data:image/svg+xml;base64,${Buffer.from(image).toString('base64')}`
@ -47,15 +56,15 @@ export function generateNftOptions(): NftOptions {
return newNft
}
export function generateNftCreateData(nftOptions: NftOptions): any {
export function generateNftCreateData(nftMetadata: NftMetadata): any {
const nftCreateData = {
name: nftOptions.name,
symbol: nftOptions.symbol,
name: nftMetadata.name,
symbol: nftMetadata.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
tokenURI: window?.btoa(JSON.stringify(nftMetadata))
// tokenURI: Buffer.from(JSON.stringify(nftMetadata)).toString('base64') // should end up as data:application/json;base64
}
return nftCreateData

View File

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

View File

@ -1,30 +1,30 @@
import axios, { CancelToken, AxiosResponse } from 'axios'
import { LoggerInstance } from '@oceanprotocol/lib'
import axios, { CancelToken, AxiosResponse, Method } from 'axios'
import {
FileMetadata,
LoggerInstance,
ProviderInstance
} from '@oceanprotocol/lib'
export interface FileMetadata {
index: number
valid: boolean
contentType: string
contentLength: string
}
export async function getEncryptedFileUrls(
files: string[],
providerUrl: string,
did: string,
accountId: string
export async function getEncryptedFiles(
files: FileMetadata[],
providerUrl: string
): Promise<string> {
try {
// 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
})
return response?.data?.encryptedDocument
console.log('start encr')
const response = await ProviderInstance.encrypt(
files,
providerUrl,
(httpMethod: Method, url: string, body: string, headers: any) => {
return axios(url, {
method: httpMethod,
data: body,
headers: headers
})
}
)
console.log('encr eres', response)
return response.data
} catch (error) {
console.error('Error parsing json: ' + error.message)
}
@ -36,9 +36,12 @@ export async function getFileInfo(
cancelToken: CancelToken
): Promise<FileMetadata[]> {
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(
`${providerUrl}/api/v1/services/fileinfo`,
`${providerUrl}/api/services/fileinfo`,
postBody,
{ cancelToken }
)

View File

@ -4,7 +4,7 @@ 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'
import { FileMetadata } from '@oceanprotocol/lib'
const cx = classNames.bind(styles)

View File

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

View File

@ -2,7 +2,7 @@ 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'
import { FileMetadata } from '@oceanprotocol/lib'
export default function FileInfo({
file,
@ -13,7 +13,7 @@ export default function FileInfo({
}): ReactElement {
return (
<div className={styles.info}>
<h3 className={styles.url}>{(file as any).url}</h3>
<h3 className={styles.url}>{file.url}</h3>
<ul>
<li className={styles.success}> URL confirmed</li>
{file.contentLength && <li>{prettySize(+file.contentLength)}</li>}

View File

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

View File

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

View File

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

View File

@ -1,7 +1,7 @@
import React, { ReactElement, useState, useEffect } from 'react'
import Compute from './Compute'
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 { compareAsBN } from '@utils/numbers'
import Pool from './Pool'
@ -9,7 +9,7 @@ import Trade from './Trade'
import { useAsset } from '@context/Asset'
import { useWeb3 } from '@context/Web3'
import Web3Feedback from '@shared/Web3Feedback'
import { FileMetadata, getFileInfo } from '@utils/provider'
import { getFileInfo } from '@utils/provider'
import { getOceanConfig } from '@utils/ocean'
import { useCancelToken } from '@hooks/useCancelToken'
import { useIsMounted } from '@hooks/useIsMounted'

View File

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

View File

@ -90,5 +90,6 @@
.success.current:before {
background: var(--font-color-heading);
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 } =
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 = [
{
name: accessTypeOptionsTitles[0].toLowerCase(),
title: accessTypeOptionsTitles[0],
name: 'download',
value: accessTypeOptionsTitles[0].toLowerCase(),
title: 'Download',
icon: <IconDownload />,
// BoxSelection component is not a Formik component
// so we need to handle checked state manually.
@ -31,6 +33,7 @@ export default function ServicesFields(): ReactElement {
},
{
name: accessTypeOptionsTitles[1].toLowerCase(),
value: accessTypeOptionsTitles[1].toLowerCase(),
title: accessTypeOptionsTitles[1],
icon: <IconCompute />,
checked:
@ -55,7 +58,7 @@ export default function ServicesFields(): ReactElement {
setFieldValue(
'services[0].access',
values.services[0].algorithmPrivacy === true ? 'compute' : 'download'
values.services[0].algorithmPrivacy === true ? 'compute' : 'access'
)
}, [values.services[0].algorithmPrivacy, setFieldValue])

View File

@ -2,12 +2,17 @@ import { ReactElement, useEffect } from 'react'
import { useFormikContext } from 'formik'
import { wizardSteps } from './_constants'
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 { values, setFieldValue } = useFormikContext<FormPublishData>()
// auto-sync user chainId & account into form data values
useEffect(() => {
if (!chainId || !accountId) return
@ -15,6 +20,11 @@ export function Steps(): ReactElement {
setFieldValue('user.accountId', accountId)
}, [chainId, accountId, setFieldValue])
// auto-sync publish feedback into form data values
useEffect(() => {
setFieldValue('feedback', feedback)
}, [feedback, setFieldValue])
const { component } = wizardSteps.filter(
(stepContent) => stepContent.step === values.user.stepCurrent
)[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 { FormPublishData } from '../_types'
import { useFormikContext } from 'formik'
import { Feedback } from './Feedback'
export default function Submission(): ReactElement {
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
in some list, after submission continously update this list with the
status of the submission.
<Feedback />
</div>
)
}

View File

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

View File

@ -1,6 +1,6 @@
import { ServiceComputeOptions } from '@oceanprotocol/lib'
import { DataTokenOptions } from '@utils/datatokens'
import { NftOptions } from '@utils/nft'
import { NftMetadata } from '@utils/nft'
import { ReactElement } from 'react'
interface FileMetadata {
@ -28,7 +28,7 @@ export interface FormPublishData {
chainId: number
}
metadata: {
nft: NftOptions
nft: NftMetadata
type: 'dataset' | 'algorithm'
name: string
description: string
@ -43,6 +43,7 @@ export interface FormPublishData {
}
services: FormPublishService[]
pricing: PriceOptions
feedback?: PublishFeedback
}
export interface StepContent {
@ -50,3 +51,11 @@ export interface StepContent {
title: string
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 { getEncryptedFileUrls } from '@utils/provider'
import { sha256 } from 'js-sha256'
import { getEncryptedFiles } from '@utils/provider'
import slugify from 'slugify'
import Web3 from 'web3'
import {
algorithmContainerPresets,
MetadataAlgorithmContainer
@ -66,9 +81,10 @@ export async function transformPublishFormToDdo(
} = metadata
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 isPreview = !datatokenAddress && !nftAddress
console.log('did', did, isPreview)
// Transform from files[0].url to string[] assuming only 1 file
const filesTransformed = files?.length && files[0].valid && [files[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 =
!isPreview &&
files?.length &&
files[0].valid &&
(await getEncryptedFileUrls(
filesTransformed,
providerUrl.url,
did,
accountId
))
(await getEncryptedFiles(file, providerUrl.url))
const newService: Service = {
// TODO: give some id
id: '1',
id: getHash(datatokenAddress + filesEncrypted),
type: access,
files: filesEncrypted || '',
datatokenAddress,
@ -151,10 +170,12 @@ export async function transformPublishFormToDdo(
// again, we can assume if `datatokenAddress` is not passed,
// we are on preview.
...(!datatokenAddress && {
dataTokenInfo: {
name: values.services[0].dataTokenOptions.name,
symbol: values.services[0].dataTokenOptions.symbol
},
datatokens: [
{
name: values.services[0].dataTokenOptions.name,
symbol: values.services[0].dataTokenOptions.symbol
}
],
nft: {
owner: accountId
}
@ -163,3 +184,132 @@ export async function transformPublishFormToDdo(
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'),
access: Yup.string()
.matches(/compute|download/g)
.matches(/compute|access/g)
.required('Required'),
providerUrl: Yup.object().shape({
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 { Form, Formik, validateYupSchema } from 'formik'
import { Form, Formik } from 'formik'
import { initialValues } from './_constants'
import { validationSchema } from './_validation'
import { useAccountPurgatory } from '@hooks/useAccountPurgatory'
import { useWeb3 } from '@context/Web3'
import { transformPublishFormToDdo } from './_utils'
import { createTokensAndPricing, transformPublishFormToDdo } from './_utils'
import PageHeader from '@shared/Page/PageHeader'
import Title from './Title'
import styles from './index.module.css'
@ -12,10 +11,15 @@ import Actions from './Actions'
import Debug from './Debug'
import Navigation from './Navigation'
import { Steps } from './Steps'
import { FormPublishData } from './_types'
import { sha256 } from 'js-sha256'
import { generateNftCreateData } from '@utils/nft'
import { FormPublishData, PublishFeedback } from './_types'
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
const formName = 'ocean-publish-form'
@ -26,58 +30,211 @@ export default function PublishPage({
content: { title: string; description: string; warning: string }
}): ReactElement {
const { debug } = useUserPreferences()
const { accountId, chainId } = useWeb3()
const { accountId, web3, chainId } = useWeb3()
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 { 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) {
let _erc721Address, _datatokenAddress, _ddo, _encryptedDdo
// --------------------------------------------------
// 1. Create NFT & datatokens & create pricing schema
// --------------------------------------------------
try {
// --------------------------------------------------
// 1. Mint NFT & datatokens & put in pool
// --------------------------------------------------
// const nftOptions = values.metadata.nft
// const nftCreateData = generateNftCreateData(nftOptions)
const config = getOceanConfig(chainId)
console.log('config', config)
// TODO: 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)
const { erc721Address, datatokenAddress } = await createTokensAndPricing(
values,
accountId,
appConfig.marketFeeAddress,
config,
nftFactory,
web3
)
// TODO: 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)
const isSuccess = erc721Address && datatokenAddress
_erc721Address = erc721Address
_datatokenAddress = datatokenAddress
// if (checksumBefore !== checksumAfter) {
// throw new Error('DDO integrity check failed!')
// }
setSuccess('Your DDO was published successfully!')
setFeedback({
...feedback,
1: {
...feedback[1],
status: isSuccess ? 'success' : '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 : (
@ -97,7 +254,7 @@ export default function PublishPage({
/>
<Form className={styles.form} ref={scrollToRef}>
<Navigation />
<Steps />
<Steps feedback={feedback} />
<Actions scrollToRef={scrollToRef} />
</Form>
{debug && <Debug />}