more publish flow preparation

* consolidate scattered methods into publish utils
* new encrypt method
* remove DDO File typings
This commit is contained in:
Matthias Kretschmann 2021-11-11 13:40:38 +00:00
parent 8ce573b2a0
commit 704b52a3c4
Signed by: m
GPG Key ID: 606EEEF3C479A91F
19 changed files with 126 additions and 180 deletions

View File

@ -1,18 +0,0 @@
// This is all super questionable,
// but we most likely need something to represent what we get
// back from fileinfo endpoint in Provider. But then should be moved out of DDO typings.
interface FileMetadata {
url: string
contentType: string
name?: string
checksum?: string
checksumType?: string
contentLength?: string
encoding?: string
compression?: string
encrypted?: boolean
encryptionMode?: string
resourceId?: string
attributes?: { [key: string]: any }
}

View File

@ -1,5 +1,3 @@
import slugify from 'slugify'
export function getServiceByName(
ddo: Asset | DDO,
name: 'access' | 'compute'
@ -10,16 +8,6 @@ export function getServiceByName(
return service
}
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':
@ -68,18 +56,3 @@ export function secondsToString(numberOfSeconds: number): string {
? `${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
}
export function getUrlFileExtension(fileUrl: string): string {
const splitedFileUrl = fileUrl.split('.')
return splitedFileUrl[splitedFileUrl.length - 1]
}

View File

@ -1,70 +1,16 @@
import axios, { CancelToken, AxiosResponse } from 'axios'
import { toast } from 'react-toastify'
import { DID, Logger } from '@oceanprotocol/lib'
export async function fileinfo(
url: string,
providerUri: string,
cancelToken: CancelToken
): Promise<FileMetadata> {
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
})
}
const { contentLength, contentType } = response.data[0]
return {
contentLength: contentLength || '',
contentType: contentType || '', // need to do that cause lib-js File interface requires contentType
url
}
} catch (error) {
Logger.error(error.message)
}
export interface FileMetadata {
contentType: string
contentLength: string
}
export async function getFileInfo(
url: string | DID,
providerUri: string,
cancelToken: CancelToken
): Promise<any> {
): Promise<FileMetadata[]> {
let postBody
try {
if (url instanceof DID)
@ -75,10 +21,12 @@ export async function getFileInfo(
postBody = {
url
}
const response = await axios.post(
const response: AxiosResponse<FileMetadata[]> = await axios.post(
`${providerUri}/api/v1/services/fileinfo`,
postBody,
{ cancelToken }
{
cancelToken
}
)
if (!response || response.status !== 200 || !response.data) return

View File

@ -4,6 +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'
const cx = classNames.bind(styles)

View File

@ -3,6 +3,7 @@ import { prettySize } from './utils'
import cleanupContentType from '@utils/cleanupContentType'
import styles from './Info.module.css'
import { useField, useFormikContext } from 'formik'
import { FileMetadata } from '@utils/provider'
export default function FileInfo({
name,
@ -21,7 +22,7 @@ export default function FileInfo({
return (
<div className={styles.info}>
<h3 className={styles.url}>{file.url}</h3>
{/* <h3 className={styles.url}>{file}</h3> */}
<ul>
<li>URL confirmed</li>
{file.contentLength && <li>{prettySize(+file.contentLength)}</li>}

View File

@ -4,7 +4,7 @@ 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 { getFileInfo } from '@utils/provider'
import { useWeb3 } from '@context/Web3'
import { getOceanConfig } from '@utils/ocean'
import { useCancelToken } from '@hooks/useCancelToken'
@ -22,7 +22,7 @@ export default function FilesInput(props: InputProps): ReactElement {
async function validateUrl() {
try {
setIsLoading(true)
const checkedFile = await fileinfo(
const checkedFile = await getFileInfo(
fileUrl,
config?.providerUri,
newCancelToken()

View File

@ -34,6 +34,7 @@ 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({
dtBalance,

View File

@ -17,6 +17,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'
const previousOrderQuery = gql`
query PreviousOrder($id: String!, $account: String!) {

View File

@ -2,7 +2,6 @@ import React, { ChangeEvent, ReactElement } from 'react'
import { Field, Form, FormikContextType, useFormikContext } from 'formik'
import { useOcean } from '@context/Ocean'
import Input, { InputProps } from '@shared/Form/Input'
import { checkIfTimeoutInPredefinedValues } from '@utils/ddo'
import FormActions from './FormActions'
import styles from './FormEditMetadata.module.css'
import { FormPublishData } from '../../../Publish/_types'

View File

@ -10,7 +10,7 @@ import { useAsset } from '@context/Asset'
import { useOcean } from '@context/Ocean'
import { useWeb3 } from '@context/Web3'
import Web3Feedback from '@shared/Web3Feedback'
import { getFileInfo } from '@utils/provider'
import { FileMetadata, getFileInfo } from '@utils/provider'
import { getOceanConfig } from '@utils/ocean'
import { useCancelToken } from '@hooks/useCancelToken'
import { useIsMounted } from '@hooks/useIsMounted'
@ -23,7 +23,7 @@ export default function AssetActions(): ReactElement {
const [isBalanceSufficient, setIsBalanceSufficient] = useState<boolean>()
const [dtBalance, setDtBalance] = useState<string>()
const [fileMetadata, setFileMetadata] = useState<FileMetadata>(Object)
const [fileMetadata, setFileMetadata] = useState<FileMetadata>()
const [fileIsLoading, setFileIsLoading] = useState<boolean>(false)
const isCompute = Boolean(
ddo?.services.filter((service) => service.type === 'compute')[0]

View File

@ -21,21 +21,21 @@ export default function Actions({
function handleNext(e: FormEvent) {
e.preventDefault()
setFieldValue('step', values.step + 1)
setFieldValue('stepCurrent', values.stepCurrent + 1)
scrollToRef.current.scrollIntoView()
}
function handlePrevious(e: FormEvent) {
e.preventDefault()
setFieldValue('step', values.step - 1)
setFieldValue('stepCurrent', values.stepCurrent - 1)
scrollToRef.current.scrollIntoView()
}
return (
<footer className={styles.actions}>
{values.step > 1 && <Button onClick={handlePrevious}>Back</Button>}
{values.stepCurrent > 1 && <Button onClick={handlePrevious}>Back</Button>}
{values.step < wizardSteps.length ? (
{values.stepCurrent < wizardSteps.length ? (
<Button style="primary" onClick={handleNext}>
Continue
</Button>

View File

@ -3,12 +3,10 @@ import DebugOutput from '@shared/DebugOutput'
import styles from './index.module.css'
// import { transformPublishFormToMetadata } from '@utils/metadata'
import { FormPublishData } from './_types'
import { useFormikContext } from 'formik'
export default function Debug({
values
}: {
values: Partial<FormPublishData>
}): ReactElement {
export default function Debug(): ReactElement {
const { values } = useFormikContext<FormPublishData>()
const ddo = {
'@context': 'https://w3id.org/did/v1'
// dataTokenInfo: {

View File

@ -5,15 +5,11 @@ import { wizardSteps } from '../_constants'
import styles from './index.module.css'
export default function Navigation(): ReactElement {
const {
values,
errors,
touched,
setFieldValue
}: FormikContextType<FormPublishData> = useFormikContext()
const { values, setFieldValue }: FormikContextType<FormPublishData> =
useFormikContext()
function handleStepClick(step: number) {
setFieldValue('step', step)
setFieldValue('stepCurrent', step)
}
return (
@ -24,7 +20,7 @@ export default function Navigation(): ReactElement {
key={step.title}
onClick={() => handleStepClick(step.step)}
// TODO: add success class
className={values.step === step.step ? styles.current : null}
className={values.stepCurrent === step.step ? styles.current : null}
>
{step.title}
</li>

View File

@ -4,7 +4,6 @@ import Tags from '@shared/atoms/Tags'
import MetaItem from '../../Asset/AssetContent/MetaItem'
import FileIcon from '@shared/FileIcon'
import Button from '@shared/atoms/Button'
import { transformTags } from '@utils/ddo'
import NetworkName from '@shared/NetworkName'
import { useWeb3 } from '@context/Web3'
import styles from './index.module.css'

View File

@ -4,18 +4,19 @@ import { wizardSteps } from './_constants'
import { useWeb3 } from '@context/Web3'
import { FormPublishData } from './_types'
export function Steps({ step }: { step: number }): ReactElement {
const { chainId } = useWeb3()
const { setFieldValue } = useFormikContext<FormPublishData>()
export function Steps(): ReactElement {
const { chainId, accountId } = useWeb3()
const { values, setFieldValue } = useFormikContext<FormPublishData>()
useEffect(() => {
if (!chainId) return
if (!chainId || !accountId) return
setFieldValue('chainId', chainId)
}, [chainId, setFieldValue])
setFieldValue('accountId', accountId)
}, [chainId, accountId, setFieldValue])
const { component } = wizardSteps.filter(
(stepContent) => stepContent.step === step
(stepContent) => stepContent.step === values.stepCurrent
)[0]
return component

View File

@ -32,8 +32,9 @@ export const wizardSteps: StepContent[] = [
]
export const initialValues: FormPublishData = {
step: 1,
stepCurrent: 1,
chainId: 1,
accountId: '',
metadata: {
type: 'dataset',
name: '',
@ -47,9 +48,9 @@ export const initialValues: FormPublishData = {
files: [],
links: [],
dataTokenOptions: { name: '', symbol: '' },
timeout: 'Forever',
timeout: '',
access: '',
providerUrl: ''
providerUrl: 'https://provider.oceanprotocol.com'
}
],
pricing: {
@ -84,17 +85,17 @@ const validationMetadata = {
}
const validationService = {
files: Yup.array<FileMetadata>()
files: Yup.array<string[]>()
.required('Enter a valid URL and click "ADD FILE"')
.nullable(),
links: Yup.array<FileMetadata[]>().nullable(),
links: Yup.array<string[]>().nullable(),
dataTokenOptions: Yup.object().shape({
name: Yup.string(),
symbol: Yup.string()
}),
timeout: Yup.string().required('Required'),
access: Yup.string()
.matches(/Compute|Download/g, { excludeEmptyString: true })
.matches(/compute|download/g, { excludeEmptyString: true })
.required('Required'),
providerUrl: Yup.string().url().nullable()
}
@ -123,8 +124,9 @@ const validationPricing = {
// export const validationSchema: Yup.SchemaOf<FormPublishData> =
export const validationSchema: Yup.SchemaOf<any> = Yup.object().shape({
step: Yup.number(),
stepCurrent: Yup.number(),
chainId: Yup.number(),
accountId: Yup.string(),
metadata: Yup.object().shape(validationMetadata),
services: Yup.array().of(Yup.object().shape(validationService)),
pricing: Yup.object().shape(validationPricing)

View File

@ -2,7 +2,7 @@ import { DataTokenOptions } from '@hooks/usePublish'
import { ReactElement } from 'react'
export interface FormPublishService {
files: string | FileMetadata[]
files: string | string[]
links?: string[]
timeout: string
dataTokenOptions: DataTokenOptions
@ -14,7 +14,9 @@ export interface FormPublishService {
}
export interface FormPublishData {
step: number
stepCurrent: number
accountId: string
chainId: number
metadata: {
type: 'dataset' | 'algorithm'
name: string
@ -25,7 +27,6 @@ export interface FormPublishData {
}
services: FormPublishService[]
pricing: PriceOptions
chainId: number
}
export interface StepContent {

View File

@ -1,15 +1,8 @@
import axios, { AxiosResponse } from 'axios'
import { sha256 } from 'js-sha256'
import {
dateToStringNoMS,
transformTags,
getUrlFileExtension
} from '@utils/ddo'
import slugify from 'slugify'
import { FormPublishData } from './_types'
function encryptMe(files: string | FileMetadata[]): string {
throw new Error('Function not implemented.')
}
export function getFieldContent(
fieldName: string,
fields: FormFieldContent[]
@ -17,15 +10,52 @@ export function getFieldContent(
return fields.filter((field: FormFieldContent) => field.name === fieldName)[0]
}
export function transformPublishFormToDdo(
async function getEncryptedFileUrls(
files: string[],
providerUrl: string,
did: string,
accountId: 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
} catch (error) {
console.error('Error parsing json: ' + error.message)
}
}
function getUrlFileExtension(fileUrl: string): string {
const splittedFileUrl = fileUrl.split('.')
return splittedFileUrl[splittedFileUrl.length - 1]
}
function dateToStringNoMS(date: Date): string {
return date.toISOString().replace(/\.[0-9]{3}Z/, 'Z')
}
function transformTags(value: string): string[] {
const originalTags = value?.split(',')
const transformedTags = originalTags?.map((tag) => slugify(tag).toLowerCase())
return transformedTags
}
export async function transformPublishFormToDdo(
values: FormPublishData,
datatokenAddress: string,
nftAddress: string
): DDO {
const did = sha256(`${nftAddress}${values.chainId}`)
): Promise<DDO> {
const { chainId, accountId, metadata, services } = values
const did = sha256(`${nftAddress}${chainId}`)
const currentTime = dateToStringNoMS(new Date())
const { type, name, description, tags, author, termsAndConditions } =
values.metadata
const { type, name, description, tags, author, termsAndConditions } = metadata
const {
access,
files,
@ -35,11 +65,16 @@ export function transformPublishFormToDdo(
entrypoint,
providerUrl,
timeout
} = values.services[0]
} = services[0]
const fileUrl = typeof files !== 'string' && files[0].url
const filesEncrypted = await getEncryptedFileUrls(
files as string[],
providerUrl,
did,
accountId
)
const metadata: Metadata = {
const newMetadata: Metadata = {
created: currentTime,
updated: currentTime,
type,
@ -54,7 +89,7 @@ export function transformPublishFormToDdo(
},
...(type === 'algorithm' && {
algorithm: {
language: getUrlFileExtension(fileUrl),
language: getUrlFileExtension(files[0]),
version: '0.1',
container: {
entrypoint,
@ -66,9 +101,9 @@ export function transformPublishFormToDdo(
})
}
const service: Service = {
const newService: Service = {
type: access,
files: encryptMe(files),
files: filesEncrypted,
datatokenAddress,
serviceEndpoint: providerUrl,
timeout,
@ -92,9 +127,9 @@ export function transformPublishFormToDdo(
'@context': ['https://w3id.org/did/v1'],
id: did,
version: '4.0.0',
chainId: values.chainId,
metadata,
services: [service]
chainId,
metadata: newMetadata,
services: [newService]
}
return newDdo

View File

@ -1,6 +1,5 @@
import React, { ReactElement, useState, useRef } from 'react'
import { Form, Formik, FormikState } from 'formik'
import { usePublish } from '@hooks/usePublish'
import { initialValues, validationSchema } from './_constants'
import { validateDockerImage } from '@utils/docker'
import { Logger, Metadata } from '@oceanprotocol/lib'
@ -15,6 +14,7 @@ import Debug from './Debug'
import Navigation from './Navigation'
import { Steps } from './Steps'
import { FormPublishData } from './_types'
import { sha256 } from 'js-sha256'
const formName = 'ocean-publish-form'
@ -25,7 +25,7 @@ export default function PublishPage({
}): ReactElement {
const { accountId, chainId } = useWeb3()
const { isInPurgatory, purgatoryData } = useAccountPurgatory(accountId)
const { publish, publishError, isLoading, publishStepText } = usePublish()
// const { publish, publishError, isLoading, publishStepText } = usePublish()
const [success, setSuccess] = useState<string>()
const [error, setError] = useState<string>()
const scrollToRef = useRef()
@ -35,10 +35,22 @@ export default function PublishPage({
// 1. Mint NFT & datatokens & put in pool
// const txMint = await createNftWithErc()
// const { nftAddress, datatokenAddress } = txMint.logs[0].args
//
// 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!')
// }
setSuccess('Your DDO was published successfully!')
} catch (error) {
setError(error.message)
@ -109,16 +121,12 @@ export default function PublishPage({
await handleSubmit(values)
}}
>
{({ values }) => (
<>
<Form className={styles.form} ref={scrollToRef}>
<Navigation />
<Steps step={values.step} />
<Actions scrollToRef={scrollToRef} />
</Form>
<Debug values={values} />
</>
)}
<Form className={styles.form} ref={scrollToRef}>
<Navigation />
<Steps />
<Actions scrollToRef={scrollToRef} />
</Form>
<Debug />
</Formik>
)}
</>