156 changes: 156 additions & 0 deletions src/components/pages/Profile/History/Verify.tsx
@@ -0,0 +1,156 @@
import React, { ReactElement, useEffect } from 'react'
import { Formik } from 'formik'
import { File, Logger } from '@oceanprotocol/lib'
import VerifyForm from './VerifyForm'
import * as Yup from 'yup'
import { toast } from 'react-toastify'
import axios, { AxiosRequestConfig, CancelTokenSource } from 'axios'
import { useSiteMetadata } from '../../../../hooks/useSiteMetadata'
import {
RegisterVPPayload,
RegistryApiResponse,
SignatureMessageBody,
VpDataBody
} from '../../../../@types/Verification'
import { useOcean } from '../../../../providers/Ocean'
import { getOceanConfig } from '../../../../utils/ocean'
import { useWeb3 } from '../../../../providers/Web3'

interface VerifyFormData {
file: string | File[]
}

const initialValues = {
file: ''
}

const validationSchema = Yup.object().shape({
// ---- required fields ----
file: Yup.array<File>()
.required('Enter a valid URL and click "ADD FILE"')
.nullable()
})

export default function Verify({
accountIdentifier
}: {
accountIdentifier: string
}): ReactElement {
const { ocean, connect } = useOcean()
const { accountId, networkId } = useWeb3()

const { vpRegistryUri } = useSiteMetadata().appConfig

useEffect(() => {
async function initOcean() {
const oceanInitialConfig = getOceanConfig(networkId)
await connect(oceanInitialConfig)
}
if (ocean === undefined) {
initOcean()
}
}, [networkId, ocean, connect])

const signMessage = async (): Promise<{
message: string
signature: string
}> => {
const url = `${vpRegistryUri}/signature/message`
try {
const response: RegistryApiResponse<SignatureMessageBody> =
await axios.get(url)

const { message } = response.data.data

const signature = await ocean.utils.signature.signText(message, accountId)

return {
message,
signature
}
} catch (error) {
Logger.error(error.message)
if (error.code === 4001) {
// User rejected signature (4001)
toast.error(error.message)
} else {
toast.error('Error requesting message from the registry.')
}
}
}

const registerVp = async (
file: File[],
message: string,
signature: string,
cancelTokenSource: CancelTokenSource
): Promise<void> => {
try {
const url = `${vpRegistryUri}/vp`
const method: AxiosRequestConfig['method'] = 'POST'
const data: RegisterVPPayload = {
signature: signature,
hashedMessage: message,
fileUrl: file[0].url
}

const response: RegistryApiResponse<VpDataBody> = await axios.request({
method,
url,
data,
cancelToken: cancelTokenSource.token
})

Logger.log(
'Registered a Verifiable Presentation. Explore at blockscout:',
response.data.data.transactionHash
)
toast.success(`Verifiable Presentation succesfully registered!`)
} catch (error) {
// TODO: improve error messages for user?
toast.error(
'Error registering Verifiable Presentation with the registry.'
)
if (axios.isCancel(error)) {
cancelTokenSource.cancel()
Logger.log(error.message)
} else {
Logger.error(error.message)
}
}
}

const handleSubmit = async (values: VerifyFormData): Promise<void> => {
console.log(ocean)
if (!accountId || accountIdentifier !== accountId) {
toast.error('Could not submit. Please log in with your wallet first.')
return
}
const cancelTokenSource = axios.CancelToken.source()

// Get signature from user to register VP
const { signature, message } = await signMessage()

// Register VP with the registry
signature &&
(await registerVp(
values.file as File[],
message,
signature,
cancelTokenSource
))
}

return (
<Formik
initialValues={initialValues}
initialStatus="empty"
validationSchema={validationSchema}
onSubmit={async (values) => {
await handleSubmit(values)
}}
>
<VerifyForm accountIdentifier={accountIdentifier} />
</Formik>
)
}
123 changes: 123 additions & 0 deletions src/components/pages/Profile/History/VerifyForm.tsx
@@ -0,0 +1,123 @@
import { Logger } from '@oceanprotocol/lib'
import axios from 'axios'
import { Field, Form, useFormikContext } from 'formik'
import { graphql, useStaticQuery } from 'gatsby'
import React, { ChangeEvent, ReactElement, useEffect, useState } from 'react'
import { FormContent, FormFieldProps } from '../../../../@types/Form'
import {
RegistryApiResponse,
VpDataBody
} from '../../../../@types/Verification'
import { useSiteMetadata } from '../../../../hooks/useSiteMetadata'

import { useWeb3 } from '../../../../providers/Web3'
import Button from '../../../atoms/Button'
import Input from '../../../atoms/Input'
import styles from './Verify.module.css'

const query = graphql`
query {
content: file(relativePath: { eq: "pages/history.json" }) {
childPagesJson {
verify {
data {
name
type
placeholder
label
help
required
}
}
}
}
}
`

export default function VerifyForm({
accountIdentifier
}: {
accountIdentifier: string
}): ReactElement {
const data = useStaticQuery(query)
const content: FormContent = data.content.childPagesJson.verify

const { vpRegistryUri } = useSiteMetadata().appConfig

const {
isValid,
setErrors,
setFieldValue,
setStatus,
setTouched,
status,
validateField
} = useFormikContext()

useEffect(() => {
setErrors({})
setTouched({})
}, [setErrors, setTouched])

const { accountId } = useWeb3()
const [addOrUpdate, setAddOrUpdate] = useState('Add')
useEffect(() => {
const initFileData = async () => {
try {
const response: RegistryApiResponse<VpDataBody> = await axios.get(
`${vpRegistryUri}/vp/${accountId}`
)

setFieldValue('file', [{ url: response.data.data.fileUrl }])
setAddOrUpdate('Update')
} catch (error) {
setAddOrUpdate('Add')
Logger.error(error.message)
}
}
initFileData()
}, [])

// Manually handle change events instead of using `handleChange` from Formik.
// Workaround for default `validateOnChange` not kicking in
function handleFieldChange(
e: ChangeEvent<HTMLInputElement>,
field: FormFieldProps
) {
const { value } = e.target
validateField(field.name)
setFieldValue(field.name, value)
}

return (
<Form onChange={() => status === 'empty' && setStatus(null)}>
<h2 className={styles.title}>
{addOrUpdate} Verifiable Presentation Registry
</h2>

{content.data.map((field: FormFieldProps) => (
<Field
key={field.name}
{...field}
component={Input}
onChange={(e: ChangeEvent<HTMLInputElement>) =>
handleFieldChange(e, field)
}
/>
))}

<Button
style="primary"
type="submit"
disabled={
!accountId ||
accountIdentifier !== accountId ||
!isValid ||
status === 'empty'
}
>
Submit
</Button>
</Form>
)
}
35 changes: 19 additions & 16 deletions src/components/pages/Profile/History/index.tsx
Expand Up @@ -9,6 +9,7 @@ import { useLocation } from '@reach/router'
import styles from './index.module.css'
import OceanProvider from '../../../../providers/Ocean'
import { useWeb3 } from '../../../../providers/Web3'
import Verify from './Verify'

interface HistoryTab {
title: string
Expand All @@ -22,28 +23,30 @@ function getTabs(accountId: string, userAccountId: string): HistoryTab[] {
content: <PublishedList accountId={accountId} />
},
{
title: 'Pool Shares',
content: <PoolShares accountId={accountId} />
},
title: 'Downloads',
content: <Downloads accountId={accountId} />
}
]
const userTabs: HistoryTab[] = [
{
title: 'Pool Transactions',
content: <PoolTransactions accountId={accountId} />
title: 'Compute Jobs',
content: (
<OceanProvider>
<ComputeJobs />
</OceanProvider>
)
},
{
title: 'Downloads',
content: <Downloads accountId={accountId} />
title: 'Verify',
content: (
<OceanProvider>
<Verify accountIdentifier={accountId} />
</OceanProvider>
)
}
]
const computeTab: HistoryTab = {
title: 'Compute Jobs',
content: (
<OceanProvider>
<ComputeJobs />
</OceanProvider>
)
}
if (accountId === userAccountId) {
defaultTabs.push(computeTab)
return defaultTabs.concat(userTabs)
}
return defaultTabs
}
Expand Down
2 changes: 2 additions & 0 deletions src/hooks/useSiteMetadata.ts
Expand Up @@ -22,6 +22,7 @@ interface UseSiteMetadata {
badge: string
appConfig: {
metadataCacheUri: string
vpRegistryUri: string
infuraProjectId: string
chainIds: number[]
chainIdsSupported: number[]
Expand Down Expand Up @@ -63,6 +64,7 @@ const query = graphql`
badge
appConfig {
metadataCacheUri
vpRegistryUri
infuraProjectId
chainIds
chainIdsSupported
Expand Down
10 changes: 10 additions & 0 deletions src/images/patch_check.svg
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.