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

Start compute job (#439)

* Wip start compute job

* Wip select algorithm design

* Asset selection form component, for start compute job (#442)

* prototype AssetSelection

* assetselection styling

* typing "fix"

* put back file info icon

* AssetSelection styling in context

* update start job method, fixed algo select, and fixed option typing

* compute logic update

* add has previous orders for algo asset

* fixed search algorithm assets in start compute form

* fixed lint errors

* updated previous order for algo logic and compute flow

* update use price hook and added buy DT for algo

* display only alg of type exchange and sort by value

* display only trusted algo for asset if field is set

* added logic for allow all published algorithms or no algorithms allowed

* asset selection style & spacing tweaks

* refactor get algorithms for compute and edit compute

* fixed form options and more refactoring

* new ButtonBuy component

* shared component between consume/compute
* dealing with various states: loading, previous orders, help text output

* effect dependencies

* move error output into toast

* formik data flow refactor

* ditch custom field change handler
* fix initialValues
* typed form data & validation
* fixes multiple form validation issues along the way

* isInitialValid → validateOnMount

* metadata display tweaks

* error feedback tweaks

* oler assets checks, confeti on succes job, market fee on order, removed algo compute logic

* more startJob logging

* feedback & messaging changes

* metadata display

* return all algos, fixed & dynamic priced ones

* fix DOM nesting

* messaging updates

* copy tweaks

* check algorithm previous history for both acces and compute sercive types

* handle start compute error

* extra checks on start compute response

* styling tweaks, fix toast UI errors

* AssetSelection: empty screen, tweak min/max height

* fix FRE issues on start compute

* check is ordarable before start compute job

* logging tweaks

* disable eslint no-unused-vars rule for some Apollo code blocks

* fix metadata editing for compute assets

* consider dataset timeout for compute too

Co-authored-by: Matthias Kretschmann <m@kretschmann.io>
This commit is contained in:
Bogdan Fazakas 2021-04-01 18:21:08 +03:00 committed by GitHub
parent 6d14181d17
commit 18f2c99e78
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
35 changed files with 981 additions and 384 deletions

View File

@ -19,7 +19,7 @@
"name": "files", "name": "files",
"label": "File", "label": "File",
"placeholder": "e.g. https://file.com/file.json", "placeholder": "e.g. https://file.com/file.json",
"help": "Please provide a URL to your algorith file. This URL will be stored encrypted after publishing.", "help": "Please provide a URL to your algorithm file. This URL will be stored encrypted after publishing.",
"type": "files", "type": "files",
"required": true "required": true
}, },
@ -27,23 +27,23 @@
"name": "dockerImage", "name": "dockerImage",
"label": "Docker Image", "label": "Docker Image",
"placeholder": "e.g. python3.7", "placeholder": "e.g. python3.7",
"help": "Please select a image to run your algorithm.", "help": "Please select an image to run your algorithm.",
"type": "select", "type": "select",
"options": ["node:pre-defined", "python:pre-defined", "custom image"], "options": ["node:latest", "python:latest", "custom image"],
"required": true "required": true
}, },
{ {
"name": "image", "name": "image",
"label": "Image URL", "label": "Image URL",
"placeholder": "e.g. oceanprotocol/algo_dockers or https://example.com/image_path", "placeholder": "e.g. oceanprotocol/algo_dockers or https://example.com/image_path",
"help": "Provide the name of a public docker image or the full url if you have it hosted in a 3rd party repo", "help": "Provide the name of a public Docker image or the full url if you have it hosted in a 3rd party repo",
"required": false "required": false
}, },
{ {
"name": "containerTag", "name": "containerTag",
"label": "Docker Image Tag", "label": "Docker Image Tag",
"placeholder": "e.g. latest", "placeholder": "e.g. latest",
"help": "Provide the tag for your docker image.", "help": "Provide the tag for your Docker image.",
"required": false "required": false
}, },
{ {
@ -58,13 +58,14 @@
"label": "Algorithm Privacy", "label": "Algorithm Privacy",
"type": "checkbox", "type": "checkbox",
"options": ["Keep my algorithm private"], "options": ["Keep my algorithm private"],
"help": "By default, your algorithm can be downloaded for a fixed or dynamic price in addition to running in compute jobs. Enabling this option will prevent downloading, so your algorithm can only be run as part of a compute job on a data set.",
"required": false "required": false
}, },
{ {
"name": "author", "name": "author",
"label": "Author", "label": "Author",
"placeholder": "e.g. Jelly McJellyfish", "placeholder": "e.g. Jelly McJellyfish",
"help": "Give proper attribution for your algorith.", "help": "Give proper attribution for your algorithm.",
"required": true "required": true
}, },
{ {

View File

@ -35,7 +35,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": "select", "type": "select",
"options": ["Download"], "options": ["Download", "Compute"],
"required": true "required": true
}, },
{ {

View File

@ -0,0 +1,16 @@
{
"form": {
"success": "🎉 Your Compute job started. 🎉",
"error": "Compute job could not be started.",
"data": [
{
"name": "algorithm",
"label": "Select an algorithm to start a compute job",
"type": "assetSelection",
"value": false,
"options": [],
"sortOptions": false
}
]
}
}

View File

@ -0,0 +1,14 @@
.actions {
width: 100%;
margin-top: calc(var(--spacer) / 2);
}
.help {
font-size: var(--font-size-mini);
color: var(--color-secondary);
margin-top: calc(var(--spacer) / 3);
}
.help:not(:empty) {
margin-top: calc(var(--spacer) / 2);
}

View File

@ -0,0 +1,90 @@
import React, { FormEvent, ReactElement } from 'react'
import Button from './Button'
import styles from './ButtonBuy.module.css'
import Loader from './Loader'
interface ButtonBuyProps {
action: 'download' | 'compute'
disabled: boolean
hasPreviousOrder: boolean
hasDatatoken: boolean
dtSymbol: string
dtBalance: string
isLoading: boolean
assetTimeout: string
onClick?: (e: FormEvent<HTMLButtonElement>) => void
stepText?: string
type?: 'submit'
}
function getHelpText(
token: {
dtBalance: string
dtSymbol: string
},
hasDatatoken: boolean,
hasPreviousOrder: boolean,
timeout: string
) {
const { dtBalance, dtSymbol } = token
const assetTimeout = timeout === 'Forever' ? '' : ` for ${timeout}`
const text = hasPreviousOrder
? `You bought this data set already allowing you to use it without paying again${assetTimeout}.`
: hasDatatoken
? `You own ${dtBalance} ${dtSymbol} allowing you to use this data set by spending 1 ${dtSymbol}, but without paying OCEAN again.`
: `For using this data set, you will buy 1 ${dtSymbol} and immediately spend it back to the publisher and pool.`
return text
}
export default function ButtonBuy({
action,
disabled,
hasPreviousOrder,
hasDatatoken,
dtSymbol,
dtBalance,
onClick,
assetTimeout,
stepText,
isLoading,
type
}: ButtonBuyProps): ReactElement {
const buttonText =
action === 'download'
? hasPreviousOrder
? 'Download'
: `Buy ${assetTimeout === 'Forever' ? '' : ` for ${assetTimeout}`}`
: hasPreviousOrder
? 'Start Compute Job'
: `Buy Compute Job ${
assetTimeout === 'Forever' ? '' : ` for ${assetTimeout}`
}`
return (
<div className={styles.actions}>
{isLoading ? (
<Loader message={stepText} />
) : (
<>
<Button
style="primary"
type={type}
onClick={onClick}
disabled={disabled}
>
{buttonText}
</Button>
<div className={styles.help}>
{getHelpText(
{ dtBalance, dtSymbol },
hasDatatoken,
hasPreviousOrder,
assetTimeout
)}
</div>
</>
)}
</div>
)
}

View File

@ -21,6 +21,7 @@
.file.small { .file.small {
font-size: var(--font-size-mini); font-size: var(--font-size-mini);
height: 5.75rem; height: 5.5rem;
width: 4.5rem; width: 4.5rem;
padding: calc(var(--spacer) / 2) calc(var(--spacer) / 4);
} }

View File

@ -100,6 +100,12 @@
padding-left: 0.5rem; padding-left: 0.5rem;
} }
.algorithmLabel {
display: grid;
gap: var(--spacer);
grid-template-columns: 2fr 1fr;
}
.radio, .radio,
.checkbox { .checkbox {
composes: input; composes: input;
@ -140,16 +146,17 @@
opacity: 1; opacity: 1;
} }
.radio { .radio,
.radio::after {
border-radius: 50%; border-radius: 50%;
} }
.radio::after { .radio::after {
width: 16px; width: 8px;
height: 16px; height: 8px;
border-radius: 50%; top: 4px;
left: 4px;
background: var(--brand-white); background: var(--brand-white);
transform: scale(0.7);
} }
.checkbox::after { .checkbox::after {

View File

@ -1,4 +1,5 @@
.action { .action {
text-align: center; text-align: center;
display: block; display: block;
margin-top: calc(var(--spacer) / 2);
} }

View File

@ -5,6 +5,7 @@
border-radius: var(--border-radius); border-radius: var(--border-radius);
margin-bottom: calc(var(--spacer) / 2); margin-bottom: calc(var(--spacer) / 2);
font-size: var(--font-size-small); font-size: var(--font-size-small);
min-height: 200px;
} }
.disabled { .disabled {
@ -18,8 +19,8 @@ div [class*='loaderWrap'] {
.scroll { .scroll {
border-top: 1px solid var(--border-color); border-top: 1px solid var(--border-color);
margin-top: calc(var(--spacer) / 4); margin-top: calc(var(--spacer) / 4);
min-height: 200px; min-height: fit-content;
max-height: 300px; max-height: 50vh;
position: relative; position: relative;
/* smooth overflow scrolling for pre-iOS 13 */ /* smooth overflow scrolling for pre-iOS 13 */
overflow: auto; overflow: auto;
@ -30,7 +31,11 @@ div [class*='loaderWrap'] {
display: flex; display: flex;
align-items: center; align-items: center;
border-bottom: 1px solid var(--border-color); border-bottom: 1px solid var(--border-color);
padding: calc(var(--spacer) / 3) calc(var(--spacer) / 4); padding: calc(var(--spacer) / 3) calc(var(--spacer) / 2);
}
.row:last-child {
border-bottom: none;
} }
.content { .content {
@ -46,11 +51,10 @@ div [class*='loaderWrap'] {
} }
.input { .input {
align-self: flex-start;
min-width: 1.2rem; min-width: 1.2rem;
margin-top: 0; margin-top: 0;
margin-left: 0; margin-left: 0;
margin-right: calc(var(--spacer) / 4); margin-right: calc(var(--spacer) / 3);
} }
.radio { .radio {
@ -64,7 +68,7 @@ div [class*='loaderWrap'] {
.title { .title {
font-size: var(--font-size-small); font-size: var(--font-size-small);
margin-top: calc(var(--spacer) / 12); margin-top: calc(var(--spacer) / 12);
margin-bottom: calc(var(--spacer) / 12); margin-bottom: 0;
} }
.link { .link {
@ -89,11 +93,12 @@ div [class*='loaderWrap'] {
.price { .price {
white-space: pre; white-space: pre;
font-size: var(--font-size-small) !important; font-size: var(--font-size-small) !important;
padding-left: calc(var(--spacer) / 4);
} }
.search { .search {
margin: calc(var(--spacer) / 4); margin: calc(var(--spacer) / 4) calc(var(--spacer) / 2);
width: calc(100% - calc(var(--spacer) / 2)); width: calc(100% - var(--spacer));
} }
.did { .did {
@ -102,4 +107,14 @@ div [class*='loaderWrap'] {
display: block; display: block;
text-align: left; text-align: left;
color: var(--color-secondary); color: var(--color-secondary);
/* makes sure DotDotDot will kick in */
overflow-wrap: break-word;
word-wrap: break-word;
word-break: break-all;
}
.empty {
padding: var(--spacer) calc(var(--spacer) / 2);
text-align: center;
color: var(--color-secondary);
} }

View File

@ -17,6 +17,10 @@ export interface AssetSelectionAsset {
checked: boolean checked: boolean
} }
function Empty() {
return <div className={styles.empty}>No assets found.</div>
}
export default function AssetSelection({ export default function AssetSelection({
assets, assets,
multiple, multiple,
@ -52,7 +56,11 @@ export default function AssetSelection({
disabled={disabled} disabled={disabled}
/> />
<div className={styles.scroll}> <div className={styles.scroll}>
{assets ? ( {!assets ? (
<Loader />
) : assets && !assets.length ? (
<Empty />
) : (
assets assets
.filter((asset: AssetSelectionAsset) => .filter((asset: AssetSelectionAsset) =>
searchValue !== '' searchValue !== ''
@ -98,8 +106,6 @@ export default function AssetSelection({
<PriceUnit price={asset.price} small className={styles.price} /> <PriceUnit price={asset.price} small className={styles.price} />
</div> </div>
)) ))
) : (
<Loader />
)} )}
</div> </div>
</div> </div>

View File

@ -76,11 +76,11 @@
min-width: 6rem; min-width: 6rem;
} }
.walletInfo{ .walletInfo {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
} }
.walletInfo button{ .walletInfo button {
margin-top: calc(var(--spacer) / 5) !important; margin-top: calc(var(--spacer) / 5) !important;
} }

View File

@ -1,178 +0,0 @@
import React, { useState, ReactElement, ChangeEvent, useEffect } from 'react'
import { DDO } from '@oceanprotocol/lib'
import Loader from '../../atoms/Loader'
import Web3Feedback from '../../molecules/Wallet/Feedback'
import Price from '../../atoms/Price'
import File from '../../atoms/File'
import { computeOptions, useCompute } from '../../../hooks/useCompute'
import styles from './Compute.module.css'
import Input from '../../atoms/Input'
import Alert from '../../atoms/Alert'
import { useSiteMetadata } from '../../../hooks/useSiteMetadata'
import checkPreviousOrder from '../../../utils/checkPreviousOrder'
import { useOcean } from '../../../providers/Ocean'
import { useWeb3 } from '../../../providers/Web3'
import { usePricing } from '../../../hooks/usePricing'
import { useAsset } from '../../../providers/Asset'
export default function Compute({
ddo,
isBalanceSufficient,
dtBalance
}: {
ddo: DDO
isBalanceSufficient: boolean
dtBalance: string
}): ReactElement {
const { marketFeeAddress } = useSiteMetadata()
const { accountId } = useWeb3()
const { ocean } = useOcean()
const { type } = useAsset()
const { compute, isLoading, computeStepText, computeError } = useCompute()
const { buyDT, dtSymbol } = usePricing(ddo)
const { price } = useAsset()
const computeService = ddo.findServiceByType('compute')
const metadataService = ddo.findServiceByType('metadata')
const [isJobStarting, setIsJobStarting] = useState(false)
const [, setError] = useState('')
const [computeType, setComputeType] = useState('nodejs')
const [computeContainer, setComputeContainer] = useState(
computeOptions[0].value
)
const [algorithmRawCode, setAlgorithmRawCode] = useState('')
const [isPublished, setIsPublished] = useState(false)
const [file, setFile] = useState(null)
const [hasPreviousOrder, setHasPreviousOrder] = useState(false)
const [previousOrderId, setPreviousOrderId] = useState<string>()
const isComputeButtonDisabled =
isJobStarting === true ||
file === null ||
computeType === '' ||
!ocean ||
!isBalanceSufficient
const hasDatatoken = Number(dtBalance) >= 1
useEffect(() => {
if (!ocean || !accountId) return
async function checkPreviousOrders() {
const orderId = await checkPreviousOrder(ocean, accountId, ddo, 'compute')
setPreviousOrderId(orderId)
setHasPreviousOrder(!!orderId)
}
checkPreviousOrders()
}, [ocean, ddo, accountId])
const handleSelectChange = (event: ChangeEvent<HTMLSelectElement>) => {
const comType = event.target.value
setComputeType(comType)
const selectedComputeOption = computeOptions.find((x) => x.name === comType)
if (selectedComputeOption !== undefined)
setComputeContainer(selectedComputeOption.value)
}
// const startJob = async () => {
// try {
// if (!ocean) return
// setIsJobStarting(true)
// setIsPublished(false)
// setError('')
// !hasPreviousOrder && !hasDatatoken && (await buyDT('1'))
// await compute(
// ddo.id,
// computeService,
// ddo.dataToken,
// algorithmRawCode,
// computeContainer,
// marketFeeAddress,
// previousOrderId
// )
// setHasPreviousOrder(true)
// setIsPublished(true)
// setFile(null)
// } catch (error) {
// setError('Failed to start job!')
// Logger.error(error.message)
// } finally {
// setIsJobStarting(false)
// }
// }
return (
<>
<div className={styles.info}>
<div className={styles.filewrapper}>
<File file={metadataService.attributes.main.files[0]} small />
</div>
<div className={styles.pricewrapper}>
<Price price={price} conversion />
{hasDatatoken && (
<div className={styles.hasTokens}>
You own {dtBalance} {dtSymbol} allowing you to use this data set
without paying again.
</div>
)}
</div>
</div>
{type === 'algorithm' ? (
<Input
type="select"
name="data"
label="Select dataset for the algorithm"
placeholder=""
size="small"
value="dataset-1"
options={['dataset-1', 'dataset-2', 'dataset-3'].map((x) => x)}
onChange={handleSelectChange}
/>
) : (
<Input
type="select"
name="algorithm"
label="Select image to run the algorithm"
placeholder=""
size="small"
value={computeType}
options={computeOptions.map((x) => x.name)}
onChange={handleSelectChange}
/>
)}
<div className={styles.actions}>
{isLoading ? (
<Loader message={computeStepText} />
) : (
<Alert text="Compute is coming back at a later stage." state="info" />
// <Button
// style="primary"
// onClick={() => startJob()}
// disabled={isComputeButtonDisabled}
// >
// {hasDatatoken || hasPreviousOrder ? 'Start job' : 'Buy'}
// </Button>
)}
</div>
<footer className={styles.feedback}>
{computeError !== undefined && (
<Alert text={computeError} state="error" />
)}
{isPublished && (
<Alert
title="Your job started!"
text="Watch the progress in the history page."
state="success"
/>
)}
<Web3Feedback isBalanceSufficient={isBalanceSufficient} />
</footer>
</>
)
}

View File

@ -0,0 +1,37 @@
.form {
padding: 0;
border: none;
margin-top: -0.8rem;
}
.form > div > label,
[class*='ButtonBuy-module--actions'] {
text-align: center;
}
.form > div > label {
margin-bottom: calc(var(--spacer) / 2);
margin-left: -1rem;
}
.form [class*='AssetSelection-module--selection'] {
margin-left: -2rem;
margin-right: -2rem;
border-top-left-radius: 0;
border-top-right-radius: 0;
border-left: 0;
border-right: 0;
padding: 0;
}
.actions {
display: flex;
justify-content: center;
max-height: 100%;
}
.actions a,
.actions button {
margin-left: calc(var(--spacer) / 2);
margin-right: calc(var(--spacer) / 2);
}

View File

@ -0,0 +1,112 @@
import React, { ReactElement, useEffect } from 'react'
import styles from './FormComputeDataset.module.css'
import { Field, Form, FormikContextType, useFormikContext } from 'formik'
import Input from '../../../atoms/Input'
import { FormFieldProps } from '../../../../@types/Form'
import { useStaticQuery, graphql } from 'gatsby'
import { DDO } from '@oceanprotocol/lib'
import { AssetSelectionAsset } from '../../../molecules/FormFields/AssetSelection'
import ButtonBuy from '../../../atoms/ButtonBuy'
const contentQuery = graphql`
query StartComputeDatasetQuery {
content: allFile(
filter: { relativePath: { eq: "pages/startComputeDataset.json" } }
) {
edges {
node {
childPagesJson {
description
form {
success
successAction
error
data {
name
label
help
type
required
sortOptions
options
}
}
}
}
}
}
}
`
export default function FormStartCompute({
algorithms,
ddoListAlgorithms,
setSelectedAlgorithm,
isLoading,
isComputeButtonDisabled,
hasPreviousOrder,
hasDatatoken,
dtSymbol,
dtBalance,
stepText,
datasetTimeout
}: {
algorithms: AssetSelectionAsset[]
ddoListAlgorithms: DDO[]
setSelectedAlgorithm: React.Dispatch<React.SetStateAction<DDO>>
isLoading: boolean
isComputeButtonDisabled: boolean
hasPreviousOrder: boolean
hasDatatoken: boolean
dtSymbol: string
dtBalance: string
stepText: string
datasetTimeout: string
}): ReactElement {
const data = useStaticQuery(contentQuery)
const content = data.content.edges[0].node.childPagesJson
const {
isValid,
values
}: FormikContextType<{ algorithm: string }> = useFormikContext()
function getAlgorithmAsset(algorithmId: string): DDO {
let assetDdo = null
ddoListAlgorithms.forEach((ddo: DDO) => {
if (ddo.id === algorithmId) assetDdo = ddo
})
return assetDdo
}
useEffect(() => {
if (!values.algorithm) return
setSelectedAlgorithm(getAlgorithmAsset(values.algorithm))
}, [values.algorithm])
return (
<Form className={styles.form}>
{content.form.data.map((field: FormFieldProps) => (
<Field
key={field.name}
{...field}
options={algorithms}
component={Input}
/>
))}
<ButtonBuy
action="compute"
disabled={isComputeButtonDisabled || !isValid}
hasPreviousOrder={hasPreviousOrder}
hasDatatoken={hasDatatoken}
dtSymbol={dtSymbol}
dtBalance={dtBalance}
stepText={stepText}
isLoading={isLoading}
type="submit"
assetTimeout={datasetTimeout}
/>
</Form>
)
}

View File

@ -21,8 +21,9 @@
.feedback { .feedback {
width: 100%; width: 100%;
margin-top: calc(var(--spacer) / 2);
} }
.help { .help {
composes: help from './index.module.css'; composes: help from '../index.module.css';
} }

View File

@ -1,6 +1,6 @@
import React, { ReactElement } from 'react' import React, { ReactElement } from 'react'
import Compute from './Compute' import Compute from '.'
import ddo from '../../../../tests/unit/__fixtures__/ddo' import ddo from '../../../../../tests/unit/__fixtures__/ddo'
import { DDO } from '@oceanprotocol/lib' import { DDO } from '@oceanprotocol/lib'
export default { export default {
@ -13,5 +13,10 @@ export default {
} }
export const Default = (): ReactElement => ( export const Default = (): ReactElement => (
<Compute ddo={ddo as DDO} dtBalance="1" isBalanceSufficient /> <Compute
ddo={ddo as DDO}
dtBalance="1"
isBalanceSufficient
file={ddo.service[0].attributes.main.files[0]}
/>
) )

View File

@ -0,0 +1,462 @@
import React, { useState, ReactElement, useEffect, useCallback } from 'react'
import {
DDO,
File as FileMetadata,
Logger,
ServiceType,
publisherTrustedAlgorithm,
BestPrice
} from '@oceanprotocol/lib'
import { toast } from 'react-toastify'
import Price from '../../../atoms/Price'
import File from '../../../atoms/File'
import Alert from '../../../atoms/Alert'
import Web3Feedback from '../../../molecules/Wallet/Feedback'
import { useSiteMetadata } from '../../../../hooks/useSiteMetadata'
import checkPreviousOrder from '../../../../utils/checkPreviousOrder'
import { useOcean } from '../../../../providers/Ocean'
import { useWeb3 } from '../../../../providers/Web3'
import { usePricing } from '../../../../hooks/usePricing'
import { useAsset } from '../../../../providers/Asset'
import {
queryMetadata,
transformDDOToAssetSelection
} from '../../../../utils/aquarius'
import { Formik } from 'formik'
import {
getInitialValues,
validationSchema
} from '../../../../models/FormStartComputeDataset'
import { AssetSelectionAsset } from '../../../molecules/FormFields/AssetSelection'
import { SearchQuery } from '@oceanprotocol/lib/dist/node/metadatacache/MetadataCache'
import axios from 'axios'
import FormStartComputeDataset from './FormComputeDataset'
import styles from './index.module.css'
import SuccessConfetti from '../../../atoms/SuccessConfetti'
import Button from '../../../atoms/Button'
import { gql, useQuery } from '@apollo/client'
import { FrePrice } from '../../../../@types/apollo/FrePrice'
import { PoolPrice } from '../../../../@types/apollo/PoolPrice'
import { secondsToString } from '../../../../utils/metadata'
const SuccessAction = () => (
<Button style="text" to="/history" size="small">
Go to history
</Button>
)
const freQuery = gql`
query AlgorithmFrePrice($datatoken: String) {
fixedRateExchanges(orderBy: id, where: { datatoken: $datatoken }) {
rate
id
}
}
`
const poolQuery = gql`
query AlgorithmPoolPrice($datatoken: String) {
pools(where: { datatokenAddress: $datatoken }) {
spotPrice
}
}
`
export default function Compute({
ddo,
isBalanceSufficient,
dtBalance,
file
}: {
ddo: DDO
isBalanceSufficient: boolean
dtBalance: string
file: FileMetadata
}): ReactElement {
const { marketFeeAddress } = useSiteMetadata()
const { accountId } = useWeb3()
const { ocean, account, config } = useOcean()
const { price, type } = useAsset()
const { buyDT, pricingError, pricingStepText } = usePricing()
const [isJobStarting, setIsJobStarting] = useState(false)
const [error, setError] = useState<string>()
const [algorithmList, setAlgorithmList] = useState<AssetSelectionAsset[]>()
const [ddoAlgorithmList, setDdoAlgorithmList] = useState<DDO[]>()
const [selectedAlgorithmAsset, setSelectedAlgorithmAsset] = useState<DDO>()
const [hasAlgoAssetDatatoken, setHasAlgoAssetDatatoken] = useState<boolean>()
const [isPublished, setIsPublished] = useState(false)
const [hasPreviousDatasetOrder, setHasPreviousDatasetOrder] = useState(false)
const [previousDatasetOrderId, setPreviousDatasetOrderId] = useState<string>()
const [hasPreviousAlgorithmOrder, setHasPreviousAlgorithmOrder] = useState(
false
)
const [algorithmPrice, setAlgorithmPrice] = useState<BestPrice>()
const [variables, setVariables] = useState({})
const [
previousAlgorithmOrderId,
setPreviousAlgorithmOrderId
] = useState<string>()
const [datasetTimeout, setDatasetTimeout] = useState<string>()
/* eslint-disable @typescript-eslint/no-unused-vars */
const {
refetch: refetchFre,
startPolling: startPollingFre,
data: frePrice
} = useQuery<FrePrice>(freQuery, {
variables,
skip: false
})
const {
refetch: refetchPool,
startPolling: startPollingPool,
data: poolPrice
} = useQuery<PoolPrice>(poolQuery, {
variables,
skip: false
})
/* eslint-enable @typescript-eslint/no-unused-vars */
const isComputeButtonDisabled =
isJobStarting === true || file === null || !ocean || !isBalanceSufficient
const hasDatatoken = Number(dtBalance) >= 1
async function checkPreviousOrders(ddo: DDO, serviceType: ServiceType) {
const orderId = await checkPreviousOrder(ocean, accountId, ddo, serviceType)
const assetType = ddo.findServiceByType('metadata').attributes.main.type
if (assetType === 'algorithm') {
setPreviousAlgorithmOrderId(orderId)
setHasPreviousAlgorithmOrder(!!orderId)
} else {
setPreviousDatasetOrderId(orderId)
setHasPreviousDatasetOrder(!!orderId)
}
}
async function checkAssetDTBalance(asset: DDO) {
const AssetDtBalance = await ocean.datatokens.balance(
asset.dataToken,
accountId
)
setHasAlgoAssetDatatoken(Number(AssetDtBalance) >= 1)
}
function getQuerryString(
trustedAlgorithmList: publisherTrustedAlgorithm[]
): SearchQuery {
let algoQuerry = ''
trustedAlgorithmList.forEach((trusteAlgo) => {
algoQuerry += `id:"${trusteAlgo.did}" OR `
})
if (trustedAlgorithmList.length > 1) {
algoQuerry = algoQuerry.substring(0, algoQuerry.length - 3)
}
const algorithmQuery =
trustedAlgorithmList.length > 0 ? `(${algoQuerry}) AND` : ``
const query = {
page: 1,
query: {
query_string: {
query: `${algorithmQuery} service.attributes.main.type:algorithm -isInPurgatory:true`
}
},
sort: { created: -1 }
}
return query
}
async function getAlgorithmList(): Promise<AssetSelectionAsset[]> {
const source = axios.CancelToken.source()
const computeService = ddo.findServiceByType('compute')
let algorithmSelectionList: AssetSelectionAsset[]
if (
!computeService.attributes.main.privacy ||
!computeService.attributes.main.privacy.publisherTrustedAlgorithms ||
(computeService.attributes.main.privacy.publisherTrustedAlgorithms
.length === 0 &&
!computeService.attributes.main.privacy.allowAllPublishedAlgorithms)
) {
algorithmSelectionList = []
} else {
const gueryResults = await queryMetadata(
getQuerryString(
computeService.attributes.main.privacy.publisherTrustedAlgorithms
),
config.metadataCacheUri,
source.token
)
setDdoAlgorithmList(gueryResults.results)
algorithmSelectionList = await transformDDOToAssetSelection(
gueryResults.results,
config.metadataCacheUri,
[]
)
}
return algorithmSelectionList
}
useEffect(() => {
const { timeout } = (
ddo.findServiceByType('access') || ddo.findServiceByType('compute')
).attributes.main
setDatasetTimeout(secondsToString(timeout))
}, [ddo])
useEffect(() => {
if (
!frePrice ||
frePrice.fixedRateExchanges.length === 0 ||
algorithmPrice.type !== 'exchange'
)
return
setAlgorithmPrice((prevState) => ({
...prevState,
value: frePrice.fixedRateExchanges[0].rate,
address: frePrice.fixedRateExchanges[0].id
}))
}, [frePrice])
useEffect(() => {
if (
!poolPrice ||
poolPrice.pools.length === 0 ||
algorithmPrice.type !== 'pool'
)
return
setAlgorithmPrice((prevState) => ({
...prevState,
value: poolPrice.pools[0].spotPrice
}))
}, [poolPrice])
const initMetadata = useCallback(async (ddo: DDO): Promise<void> => {
if (!ddo) return
setAlgorithmPrice(ddo.price)
setVariables({ datatoken: ddo?.dataToken.toLowerCase() })
}, [])
useEffect(() => {
if (!ddo) return
getAlgorithmList().then((algorithms) => {
setAlgorithmList(algorithms)
})
}, [ddo])
useEffect(() => {
if (!ocean || !accountId) return
checkPreviousOrders(ddo, 'compute')
}, [ocean, ddo, accountId])
useEffect(() => {
if (!ocean || !accountId || !selectedAlgorithmAsset) return
if (selectedAlgorithmAsset.findServiceByType('access')) {
checkPreviousOrders(selectedAlgorithmAsset, 'access').then(() => {
if (
!hasPreviousAlgorithmOrder &&
selectedAlgorithmAsset.findServiceByType('compute')
) {
checkPreviousOrders(selectedAlgorithmAsset, 'compute')
}
})
} else if (selectedAlgorithmAsset.findServiceByType('compute')) {
checkPreviousOrders(selectedAlgorithmAsset, 'compute')
}
checkAssetDTBalance(selectedAlgorithmAsset)
initMetadata(selectedAlgorithmAsset)
}, [selectedAlgorithmAsset, ocean, accountId, hasPreviousAlgorithmOrder])
// Output errors in toast UI
useEffect(() => {
const newError = error || pricingError
if (!newError) return
toast.error(newError)
}, [error, pricingError])
async function startJob(algorithmId: string) {
try {
if (!ocean) return
setIsJobStarting(true)
setIsPublished(false)
setError(undefined)
const computeService = ddo.findServiceByType('compute')
const serviceAlgo = selectedAlgorithmAsset.findServiceByType('access')
? selectedAlgorithmAsset.findServiceByType('access')
: selectedAlgorithmAsset.findServiceByType('compute')
const allowed = await ocean.compute.isOrderable(
ddo.id,
computeService.index,
selectedAlgorithmAsset.id
)
Logger.log('[compute] Is data set orderable?', allowed)
if (!allowed) {
setError(
'Data set is not orderable in combination with selected algorithm.'
)
Logger.error(
'[compute] Error starting compute job. Dataset is not orderable in combination with selected algorithm.'
)
return
}
if (!hasPreviousDatasetOrder && !hasDatatoken) {
const tx = await buyDT('1', price, ddo)
if (!tx) {
setError('Error buying datatoken.')
Logger.error('[compute] Error buying datatoken for data set ', ddo.id)
return
}
}
if (!hasPreviousAlgorithmOrder && !hasAlgoAssetDatatoken) {
const tx = await buyDT('1', algorithmPrice, selectedAlgorithmAsset)
if (!tx) {
setError('Error buying datatoken.')
Logger.error(
'[compute] Error buying datatoken for algorithm ',
selectedAlgorithmAsset.id
)
return
}
}
// TODO: pricingError is always undefined even upon errors during buyDT for whatever reason.
// So manually drop out above, but ideally could be replaced with this alone.
if (pricingError) {
setError(pricingError)
return
}
const assetOrderId = hasPreviousDatasetOrder
? previousDatasetOrderId
: await ocean.compute.orderAsset(
accountId,
ddo.id,
computeService.index,
undefined,
undefined,
marketFeeAddress
)
assetOrderId &&
Logger.log(
`[compute] Got ${
hasPreviousDatasetOrder ? 'existing' : 'new'
} order ID for dataset: `,
assetOrderId
)
const algorithmAssetOrderId = hasPreviousAlgorithmOrder
? previousAlgorithmOrderId
: await ocean.compute.orderAlgorithm(
algorithmId,
serviceAlgo.type,
accountId,
serviceAlgo.index,
marketFeeAddress
)
algorithmAssetOrderId &&
Logger.log(
`[compute] Got ${
hasPreviousAlgorithmOrder ? 'existing' : 'new'
} order ID for algorithm: `,
algorithmAssetOrderId
)
if (!assetOrderId || !algorithmAssetOrderId) {
setError('Error ordering assets.')
return
}
Logger.log('[compute] Starting compute job.')
const output = {}
const response = await ocean.compute.start(
ddo.id,
assetOrderId,
ddo.dataToken,
account,
algorithmId,
undefined,
output,
`${computeService.index}`,
computeService.type,
algorithmAssetOrderId,
selectedAlgorithmAsset.dataToken
)
if (
!response ||
response.status !== 10 ||
response.statusText !== 'Job started'
) {
setError('Error starting compute job.')
return
}
Logger.log('[compute] Starting compute job response: ', response)
setHasPreviousDatasetOrder(true)
setIsPublished(true)
} catch (error) {
setError('Failed to start job!')
Logger.error('[compute] Failed to start job: ', error.message)
} finally {
setIsJobStarting(false)
}
}
return (
<>
<div className={styles.info}>
<File file={file} small />
<Price price={(ddo as DDO).price} conversion />
</div>
{type === 'algorithm' ? (
<Alert
text="This algorithm has been set to private by the publisher and can't be downloaded. You can run it against any allowed data sets though!"
state="info"
/>
) : (
<Formik
initialValues={getInitialValues()}
validateOnMount
validationSchema={validationSchema}
onSubmit={async (values) => await startJob(values.algorithm)}
>
<FormStartComputeDataset
algorithms={algorithmList}
ddoListAlgorithms={ddoAlgorithmList}
setSelectedAlgorithm={setSelectedAlgorithmAsset}
isLoading={isJobStarting}
isComputeButtonDisabled={isComputeButtonDisabled}
hasPreviousOrder={
hasPreviousDatasetOrder || hasPreviousAlgorithmOrder
}
hasDatatoken={hasDatatoken}
dtSymbol={ddo.dataTokenInfo?.symbol}
dtBalance={dtBalance}
stepText={pricingStepText || 'Starting Compute Job...'}
datasetTimeout={datasetTimeout}
/>
</Formik>
)}
<footer className={styles.feedback}>
{isPublished && (
<SuccessConfetti
success="Your job started successfully! Watch the progress on the history page."
action={<SuccessAction />}
/>
)}
<Web3Feedback isBalanceSufficient={isBalanceSufficient} />
</footer>
</>
)
}

View File

@ -12,19 +12,6 @@
flex-shrink: 0; flex-shrink: 0;
} }
.actions {
width: 100%;
margin-top: calc(var(--spacer) / 2);
}
.help {
composes: help from './index.module.css';
}
.help:not(:empty) {
margin-top: calc(var(--spacer) / 2);
}
.feedback { .feedback {
width: 100%; width: 100%;
} }

View File

@ -1,12 +1,10 @@
import React, { ReactElement, useEffect, useState } from 'react' import React, { ReactElement, useEffect, useState } from 'react'
import { toast } from 'react-toastify' import { toast } from 'react-toastify'
import { File as FileMetadata, DDO } from '@oceanprotocol/lib' import { File as FileMetadata, DDO } from '@oceanprotocol/lib'
import Button from '../../atoms/Button'
import File from '../../atoms/File' import File from '../../atoms/File'
import Price from '../../atoms/Price' import Price from '../../atoms/Price'
import Web3Feedback from '../../molecules/Wallet/Feedback' import Web3Feedback from '../../molecules/Wallet/Feedback'
import styles from './Consume.module.css' import styles from './Consume.module.css'
import Loader from '../../atoms/Loader'
import { useSiteMetadata } from '../../../hooks/useSiteMetadata' import { useSiteMetadata } from '../../../hooks/useSiteMetadata'
import { useAsset } from '../../../providers/Asset' import { useAsset } from '../../../providers/Asset'
import { secondsToString } from '../../../utils/metadata' import { secondsToString } from '../../../utils/metadata'
@ -17,6 +15,7 @@ import { useOcean } from '../../../providers/Ocean'
import { useWeb3 } from '../../../providers/Web3' import { useWeb3 } from '../../../providers/Web3'
import { usePricing } from '../../../hooks/usePricing' import { usePricing } from '../../../hooks/usePricing'
import { useConsume } from '../../../hooks/useConsume' import { useConsume } from '../../../hooks/useConsume'
import ButtonBuy from '../../atoms/ButtonBuy'
const previousOrderQuery = gql` const previousOrderQuery = gql`
query PreviousOrder($id: String!, $account: String!) { query PreviousOrder($id: String!, $account: String!) {
@ -32,26 +31,6 @@ const previousOrderQuery = gql`
} }
` `
function getHelpText(
token: {
dtBalance: string
dtSymbol: string
},
hasDatatoken: boolean,
hasPreviousOrder: boolean,
timeout: string
) {
const { dtBalance, dtSymbol } = token
const assetTimeout = timeout === 'Forever' ? '' : ` for ${timeout}`
const text = hasPreviousOrder
? `You bought this data set already allowing you to download it without paying again${assetTimeout}.`
: hasDatatoken
? `You own ${dtBalance} ${dtSymbol} allowing you to use this data set by spending 1 ${dtSymbol}, but without paying OCEAN again.`
: `For using this data set, you will buy 1 ${dtSymbol} and immediately spend it back to the publisher and pool.`
return text
}
export default function Consume({ export default function Consume({
ddo, ddo,
file, file,
@ -68,10 +47,13 @@ export default function Consume({
const { marketFeeAddress } = useSiteMetadata() const { marketFeeAddress } = useSiteMetadata()
const [hasPreviousOrder, setHasPreviousOrder] = useState(false) const [hasPreviousOrder, setHasPreviousOrder] = useState(false)
const [previousOrderId, setPreviousOrderId] = useState<string>() const [previousOrderId, setPreviousOrderId] = useState<string>()
const { isInPurgatory, price, type } = useAsset() const { isInPurgatory, price } = useAsset()
const { buyDT, pricingStepText, pricingError, pricingIsLoading } = usePricing( const {
ddo buyDT,
) pricingStepText,
pricingError,
pricingIsLoading
} = usePricing()
const { consumeStepText, consume, consumeError } = useConsume() const { consumeStepText, consume, consumeError } = useConsume()
const [isDisabled, setIsDisabled] = useState(true) const [isDisabled, setIsDisabled] = useState(true)
const [hasDatatoken, setHasDatatoken] = useState(false) const [hasDatatoken, setHasDatatoken] = useState(false)
@ -144,7 +126,7 @@ export default function Consume({
]) ])
async function handleConsume() { async function handleConsume() {
!hasPreviousOrder && !hasDatatoken && (await buyDT('1', price)) !hasPreviousOrder && !hasDatatoken && (await buyDT('1', price, ddo))
await consume( await consume(
ddo.id, ddo.id,
ddo.dataToken, ddo.dataToken,
@ -162,29 +144,18 @@ export default function Consume({
}, [consumeError, pricingError]) }, [consumeError, pricingError])
const PurchaseButton = () => ( const PurchaseButton = () => (
<div className={styles.actions}> <ButtonBuy
{consumeStepText || pricingIsLoading ? ( action="download"
<Loader message={consumeStepText || pricingStepText} /> disabled={isDisabled}
) : ( hasPreviousOrder={hasPreviousOrder}
<> hasDatatoken={hasDatatoken}
<Button style="primary" onClick={handleConsume} disabled={isDisabled}> dtSymbol={ddo.dataTokenInfo?.symbol}
{hasPreviousOrder dtBalance={dtBalance}
? 'Download' onClick={handleConsume}
: `Buy ${ assetTimeout={assetTimeout}
assetTimeout === 'Forever' ? '' : ` for ${assetTimeout}` stepText={consumeStepText || pricingStepText}
}`} isLoading={pricingIsLoading}
</Button> />
<div className={styles.help}>
{getHelpText(
{ dtBalance, dtSymbol: ddo.dataTokenInfo.symbol },
hasDatatoken,
hasPreviousOrder,
assetTimeout
)}
</div>
</>
)}
</div>
) )
return ( return (

View File

@ -8,9 +8,14 @@ import { FormFieldProps } from '../../../../@types/Form'
import { AssetSelectionAsset } from '../../../molecules/FormFields/AssetSelection' import { AssetSelectionAsset } from '../../../molecules/FormFields/AssetSelection'
import stylesIndex from './index.module.css' import stylesIndex from './index.module.css'
import styles from './FormEditMetadata.module.css' import styles from './FormEditMetadata.module.css'
import { getAlgorithmsForAssetSelection } from '../../../../utils/aquarius' import {
queryMetadata,
transformDDOToAssetSelection
} from '../../../../utils/aquarius'
import { useAsset } from '../../../../providers/Asset' import { useAsset } from '../../../../providers/Asset'
import { ComputePrivacyForm } from '../../../../models/FormEditComputeDataset' import { ComputePrivacyForm } from '../../../../models/FormEditComputeDataset'
import { publisherTrustedAlgorithm as PublisherTrustedAlgorithm } from '@oceanprotocol/lib'
import axios from 'axios'
export default function FormEditComputeDataset({ export default function FormEditComputeDataset({
data, data,
@ -34,11 +39,34 @@ export default function FormEditComputeDataset({
'compute' 'compute'
).attributes.main.privacy ).attributes.main.privacy
useEffect(() => { async function getAlgorithmList(
getAlgorithmsForAssetSelection( publisherTrustedAlgorithms: PublisherTrustedAlgorithm[]
): Promise<AssetSelectionAsset[]> {
const source = axios.CancelToken.source()
const query = {
page: 1,
query: {
query_string: {
query: `service.attributes.main.type:algorithm -isInPurgatory:true`
}
},
sort: { created: -1 }
}
const querryResult = await queryMetadata(
query,
config.metadataCacheUri,
source.token
)
const algorithmSelectionList = await transformDDOToAssetSelection(
querryResult.results,
config.metadataCacheUri, config.metadataCacheUri,
publisherTrustedAlgorithms publisherTrustedAlgorithms
).then((algorithms) => { )
return algorithmSelectionList
}
useEffect(() => {
getAlgorithmList(publisherTrustedAlgorithms).then((algorithms) => {
setAllAlgorithms(algorithms) setAllAlgorithms(algorithms)
}) })
}, [config.metadataCacheUri, publisherTrustedAlgorithms]) }, [config.metadataCacheUri, publisherTrustedAlgorithms])

View File

@ -89,7 +89,9 @@ export default function Edit({
} }
let ddoEditedTimeout = ddoEditedMetdata let ddoEditedTimeout = ddoEditedMetdata
if (timeoutStringValue !== values.timeout) { if (timeoutStringValue !== values.timeout) {
const service = ddoEditedMetdata.findServiceByType('access') const service =
ddoEditedMetdata.findServiceByType('access') ||
ddoEditedMetdata.findServiceByType('compute')
const timeout = mapTimeoutStringToSeconds(values.timeout) const timeout = mapTimeoutStringToSeconds(values.timeout)
ddoEditedTimeout = await ocean.assets.editServiceTimeout( ddoEditedTimeout = await ocean.assets.editServiceTimeout(
ddoEditedMetdata, ddoEditedMetdata,

View File

@ -4,9 +4,3 @@
margin: auto; margin: auto;
padding: 0; padding: 0;
} }
.help {
font-size: var(--font-size-mini);
color: var(--color-secondary);
margin-top: calc(var(--spacer) / 3);
}

View File

@ -57,6 +57,7 @@ export default function AssetActions(): ReactElement {
ddo={ddo} ddo={ddo}
dtBalance={dtBalance} dtBalance={dtBalance}
isBalanceSufficient={isBalanceSufficient} isBalanceSufficient={isBalanceSufficient}
file={metadata?.main.files[0]}
/> />
) : ( ) : (
<Consume <Consume

View File

@ -2,7 +2,7 @@
margin-top: var(--spacer); margin-top: var(--spacer);
display: grid; display: grid;
gap: var(--spacer); gap: var(--spacer);
grid-template-columns: repeat(auto-fit, minmax(10rem, 1fr)); grid-template-columns: repeat(auto-fit, minmax(12rem, 1fr));
} }
.metaFull code { .metaFull code {

View File

@ -8,11 +8,11 @@
} }
.date { .date {
font-size: var(--font-size-mini); font-size: var(--font-size-small);
} }
.typeAndDate { .typeAndDate {
margin-top: calc(var(--spacer) / 2); margin-bottom: calc(var(--spacer) / 12);
display: flex; display: flex;
} }
@ -21,5 +21,5 @@
padding-right: calc(var(--spacer) / 3.5); padding-right: calc(var(--spacer) / 3.5);
margin-right: calc(var(--spacer) / 4); margin-right: calc(var(--spacer) / 4);
width: auto; width: auto;
font-size: var(--font-size-mini); font-size: var(--font-size-small);
} }

View File

@ -15,21 +15,6 @@ export default function MetaMain(): ReactElement {
return ( return (
<aside className={styles.meta}> <aside className={styles.meta}>
<p>
<ExplorerLink
networkId={networkId}
path={
networkId === 137
? `tokens/${ddo?.dataToken}`
: `token/${ddo?.dataToken}`
}
>
{`${ddo?.dataTokenInfo.name}${ddo?.dataTokenInfo.symbol}`}
</ExplorerLink>
</p>
<div>
Published By <Publisher account={owner} />
</div>
<div className={styles.typeAndDate}> <div className={styles.typeAndDate}>
<AssetType <AssetType
type={type} type={type}
@ -46,6 +31,19 @@ export default function MetaMain(): ReactElement {
)} )}
</p> </p>
</div> </div>
<p>
<ExplorerLink
networkId={networkId}
path={
networkId === 137
? `tokens/${ddo?.dataToken}`
: `token/${ddo?.dataToken}`
}
>
{`${ddo?.dataTokenInfo.name}${ddo?.dataTokenInfo.symbol}`}
</ExplorerLink>
</p>
Published By <Publisher account={owner} />
</aside> </aside>
) )
} }

View File

@ -1,6 +1,6 @@
import Conversion from '../../../../atoms/Price/Conversion' import Conversion from '../../../../atoms/Price/Conversion'
import { useField } from 'formik' import { useField } from 'formik'
import React, { ReactElement } from 'react' import React, { ReactElement, useState, useEffect } from 'react'
import Input from '../../../../atoms/Input' import Input from '../../../../atoms/Input'
import styles from './Price.module.css' import styles from './Price.module.css'
import Error from './Error' import Error from './Error'
@ -16,7 +16,23 @@ export default function Price({
firstPrice?: string firstPrice?: string
}): ReactElement { }): ReactElement {
const [field, meta] = useField('price') const [field, meta] = useField('price')
const { dtName, dtSymbol } = usePricing(ddo) const { getDTName, getDTSymbol } = usePricing()
const [dtSymbol, setDtSymbol] = useState<string>()
const [dtName, setDtName] = useState<string>()
useEffect(() => {
if (!ddo) return
async function setDatatokenSymbol(ddo: DDO) {
const dtSymbol = await getDTSymbol(ddo)
setDtSymbol(dtSymbol)
}
async function setDatatokenName(ddo: DDO) {
const dtName = await getDTName(ddo)
setDtName(dtName)
}
setDatatokenSymbol(ddo)
setDatatokenName(ddo)
}, [])
return ( return (
<div className={styles.price}> <div className={styles.price}>

View File

@ -62,7 +62,7 @@ export default function Pricing({ ddo }: { ddo: DDO }): ReactElement {
pricingIsLoading, pricingIsLoading,
pricingError, pricingError,
pricingStepText pricingStepText
} = usePricing(ddo) } = usePricing()
const hasFeedback = pricingIsLoading || typeof success !== 'undefined' const hasFeedback = pricingIsLoading || typeof success !== 'undefined'
@ -74,7 +74,7 @@ export default function Pricing({ ddo }: { ddo: DDO }): ReactElement {
swapFee: `${values.swapFee / 100}` swapFee: `${values.swapFee / 100}`
} }
const tx = await createPricing(priceOptions) const tx = await createPricing(priceOptions, ddo)
// Pricing failed // Pricing failed
if (!tx || pricingError) { if (!tx || pricingError) {

View File

@ -72,13 +72,13 @@ export default function FormPublish(): ReactElement {
function handleImageSelectChange(imageSelected: string) { function handleImageSelectChange(imageSelected: string) {
switch (imageSelected) { switch (imageSelected) {
case 'node:pre-defined': { case 'node:latest': {
setFieldValue('image', 'node') setFieldValue('image', 'node')
setFieldValue('containerTag', '10') setFieldValue('containerTag', 'latest')
setFieldValue('entrypoint', 'node $ALGO') setFieldValue('entrypoint', 'node $ALGO')
break break
} }
case 'python:pre-defined': { case 'python:latest': {
setFieldValue('image', 'oceanprotocol/algo_dockers') setFieldValue('image', 'oceanprotocol/algo_dockers')
setFieldValue('containerTag', 'python-panda') setFieldValue('containerTag', 'python-panda')
setFieldValue('entrypoint', 'python $ALGO') setFieldValue('entrypoint', 'python $ALGO')

View File

@ -4,7 +4,6 @@ import {
} from '@oceanprotocol/lib/dist/node/metadatacache/MetadataCache' } from '@oceanprotocol/lib/dist/node/metadatacache/MetadataCache'
import { MetadataCache, Logger } from '@oceanprotocol/lib' import { MetadataCache, Logger } from '@oceanprotocol/lib'
import queryString from 'query-string' import queryString from 'query-string'
import { TypeOf } from 'yup'
export const SortTermOptions = { export const SortTermOptions = {
Liquidity: 'liquidity', Liquidity: 'liquidity',

View File

@ -1,5 +1,5 @@
import { DDO, Logger, BestPrice } from '@oceanprotocol/lib' import { DDO, Logger, BestPrice } from '@oceanprotocol/lib'
import { useEffect, useState } from 'react' import { useState } from 'react'
import { TransactionReceipt } from 'web3-core' import { TransactionReceipt } from 'web3-core'
import { Decimal } from 'decimal.js' import { Decimal } from 'decimal.js'
import { getFirstPoolPrice } from '../utils/dtUtils' import { getFirstPoolPrice } from '../utils/dtUtils'
@ -24,16 +24,21 @@ interface PriceOptions {
} }
interface UsePricing { interface UsePricing {
dtSymbol?: string getDTSymbol: (ddo: DDO) => Promise<string>
dtName?: string getDTName: (ddo: DDO) => Promise<string>
createPricing: ( createPricing: (
priceOptions: PriceOptions priceOptions: PriceOptions,
ddo: DDO
) => Promise<TransactionReceipt | string | void> ) => Promise<TransactionReceipt | string | void>
sellDT: (dtAmount: number | string) => Promise<TransactionReceipt | void> sellDT: (
mint: (tokensToMint: string) => Promise<TransactionReceipt | void> dtAmount: number | string,
ddo: DDO
) => Promise<TransactionReceipt | void>
mint: (tokensToMint: string, ddo: DDO) => Promise<TransactionReceipt | void>
buyDT: ( buyDT: (
dtAmount: number | string, dtAmount: number | string,
price: BestPrice price: BestPrice,
ddo: DDO
) => Promise<TransactionReceipt | void> ) => Promise<TransactionReceipt | void>
pricingStep?: number pricingStep?: number
pricingStepText?: string pricingStepText?: string
@ -41,38 +46,37 @@ interface UsePricing {
pricingIsLoading: boolean pricingIsLoading: boolean
} }
function usePricing(ddo: DDO): UsePricing { function usePricing(): UsePricing {
const { accountId } = useWeb3() const { accountId } = useWeb3()
const { ocean, config } = useOcean() const { ocean, config } = useOcean()
const [pricingIsLoading, setPricingIsLoading] = useState(false) const [pricingIsLoading, setPricingIsLoading] = useState(false)
const [pricingStep, setPricingStep] = useState<number>() const [pricingStep, setPricingStep] = useState<number>()
const [pricingStepText, setPricingStepText] = useState<string>() const [pricingStepText, setPricingStepText] = useState<string>()
const [pricingError, setPricingError] = useState<string>() const [pricingError, setPricingError] = useState<string>()
const [dtSymbol, setDtSymbol] = useState<string>()
const [dtName, setDtName] = useState<string>()
async function getDTSymbol(ddo: DDO): Promise<string> {
if (!ocean || !accountId) return
const { dataToken, dataTokenInfo } = ddo const { dataToken, dataTokenInfo } = ddo
return dataTokenInfo
// Get Datatoken info, from DDO first, then from chain
useEffect(() => {
if (!dataToken) return
async function init() {
const dtSymbol = dataTokenInfo
? dataTokenInfo.symbol ? dataTokenInfo.symbol
: await ocean?.datatokens.getSymbol(dataToken) : await ocean?.datatokens.getSymbol(dataToken)
setDtSymbol(dtSymbol) }
const dtName = dataTokenInfo async function getDTName(ddo: DDO): Promise<string> {
if (!ocean || !accountId) return
const { dataToken, dataTokenInfo } = ddo
return dataTokenInfo
? dataTokenInfo.name ? dataTokenInfo.name
: await ocean?.datatokens.getName(dataToken) : await ocean?.datatokens.getName(dataToken)
setDtName(dtName)
} }
init()
}, [ocean, dataToken, dataTokenInfo])
// Helper for setting steps & feedback for all flows // Helper for setting steps & feedback for all flows
function setStep(index: number, type: 'pool' | 'exchange' | 'buy' | 'sell') { async function setStep(
index: number,
type: 'pool' | 'exchange' | 'buy' | 'sell',
ddo: DDO
) {
const dtSymbol = await getDTSymbol(ddo)
setPricingStep(index) setPricingStep(index)
if (!dtSymbol) return if (!dtSymbol) return
@ -97,8 +101,10 @@ function usePricing(ddo: DDO): UsePricing {
} }
async function mint( async function mint(
tokensToMint: string tokensToMint: string,
ddo: DDO
): Promise<TransactionReceipt | void> { ): Promise<TransactionReceipt | void> {
const { dataToken } = ddo
Logger.log('mint function', dataToken, accountId) Logger.log('mint function', dataToken, accountId)
const balance = new Decimal( const balance = new Decimal(
await ocean.datatokens.balance(dataToken, accountId) await ocean.datatokens.balance(dataToken, accountId)
@ -117,7 +123,8 @@ function usePricing(ddo: DDO): UsePricing {
async function buyDT( async function buyDT(
dtAmount: number | string, dtAmount: number | string,
price: BestPrice price: BestPrice,
ddo: DDO
): Promise<TransactionReceipt | void> { ): Promise<TransactionReceipt | void> {
if (!ocean || !accountId) return if (!ocean || !accountId) return
@ -126,14 +133,14 @@ function usePricing(ddo: DDO): UsePricing {
try { try {
setPricingIsLoading(true) setPricingIsLoading(true)
setPricingError(undefined) setPricingError(undefined)
setStep(1, 'buy') setStep(1, 'buy', ddo)
Logger.log('Price found for buying', price) Logger.log('Price found for buying', price)
switch (price?.type) { switch (price?.type) {
case 'pool': { case 'pool': {
const oceanAmmount = new Decimal(price.value).times(1.05).toString() const oceanAmmount = new Decimal(price.value).times(1.05).toString()
const maxPrice = new Decimal(price.value).times(2).toString() const maxPrice = new Decimal(price.value).times(2).toString()
setStep(2, 'buy') setStep(2, 'buy', ddo)
Logger.log('Buying token from pool', price, accountId, price) Logger.log('Buying token from pool', price, accountId, price)
tx = await ocean.pool.buyDT( tx = await ocean.pool.buyDT(
accountId, accountId,
@ -142,7 +149,7 @@ function usePricing(ddo: DDO): UsePricing {
oceanAmmount, oceanAmmount,
maxPrice maxPrice
) )
setStep(3, 'buy') setStep(3, 'buy', ddo)
Logger.log('DT buy response', tx) Logger.log('DT buy response', tx)
break break
} }
@ -162,13 +169,13 @@ function usePricing(ddo: DDO): UsePricing {
`${price.value}`, `${price.value}`,
accountId accountId
) )
setStep(2, 'buy') setStep(2, 'buy', ddo)
tx = await ocean.fixedRateExchange.buyDT( tx = await ocean.fixedRateExchange.buyDT(
price.address, price.address,
`${dtAmount}`, `${dtAmount}`,
accountId accountId
) )
setStep(3, 'buy') setStep(3, 'buy', ddo)
Logger.log('DT exchange buy response', tx) Logger.log('DT exchange buy response', tx)
break break
} }
@ -177,7 +184,7 @@ function usePricing(ddo: DDO): UsePricing {
setPricingError(error.message) setPricingError(error.message)
Logger.error(error) Logger.error(error)
} finally { } finally {
setStep(0, 'buy') setStep(0, 'buy', ddo)
setPricingStepText(undefined) setPricingStepText(undefined)
setPricingIsLoading(false) setPricingIsLoading(false)
} }
@ -186,7 +193,8 @@ function usePricing(ddo: DDO): UsePricing {
} }
async function sellDT( async function sellDT(
dtAmount: number | string dtAmount: number | string,
ddo: DDO
): Promise<TransactionReceipt | void> { ): Promise<TransactionReceipt | void> {
if (!ocean || !accountId) return if (!ocean || !accountId) return
@ -196,13 +204,14 @@ function usePricing(ddo: DDO): UsePricing {
} }
try { try {
const { dataToken } = ddo
setPricingIsLoading(true) setPricingIsLoading(true)
setPricingError(undefined) setPricingError(undefined)
setStep(1, 'sell') setStep(1, 'sell', ddo)
const pool = await getFirstPoolPrice(ocean, dataToken) const pool = await getFirstPoolPrice(ocean, dataToken)
if (!pool || pool.value === 0) return if (!pool || pool.value === 0) return
const price = new Decimal(pool.value).times(0.95).toString() const price = new Decimal(pool.value).times(0.95).toString()
setStep(2, 'sell') setStep(2, 'sell', ddo)
Logger.log('Selling token to pool', pool, accountId, price) Logger.log('Selling token to pool', pool, accountId, price)
const tx = await ocean.pool.sellDT( const tx = await ocean.pool.sellDT(
accountId, accountId,
@ -210,22 +219,26 @@ function usePricing(ddo: DDO): UsePricing {
`${dtAmount}`, `${dtAmount}`,
price price
) )
setStep(3, 'sell') setStep(3, 'sell', ddo)
Logger.log('DT sell response', tx) Logger.log('DT sell response', tx)
return tx return tx
} catch (error) { } catch (error) {
setPricingError(error.message) setPricingError(error.message)
Logger.error(error) Logger.error(error)
} finally { } finally {
setStep(0, 'sell') setStep(0, 'sell', ddo)
setPricingStepText(undefined) setPricingStepText(undefined)
setPricingIsLoading(false) setPricingIsLoading(false)
} }
} }
async function createPricing( async function createPricing(
priceOptions: PriceOptions priceOptions: PriceOptions,
ddo: DDO
): Promise<TransactionReceipt | void> { ): Promise<TransactionReceipt | void> {
const { dataToken } = ddo
const dtSymbol = await getDTSymbol(ddo)
if (!ocean || !accountId || !dtSymbol) return if (!ocean || !accountId || !dtSymbol) return
const { const {
@ -247,12 +260,12 @@ function usePricing(ddo: DDO): UsePricing {
setPricingIsLoading(true) setPricingIsLoading(true)
setPricingError(undefined) setPricingError(undefined)
setStep(99, 'pool') setStep(99, 'pool', ddo)
try { try {
// if fixedPrice set dt to max amount // if fixedPrice set dt to max amount
if (!isPool) dtAmount = 1000 if (!isPool) dtAmount = 1000
await mint(`${dtAmount}`) await mint(`${dtAmount}`, ddo)
// dtAmount for fixed price is set to max // dtAmount for fixed price is set to max
const tx = isPool const tx = isPool
@ -265,10 +278,10 @@ function usePricing(ddo: DDO): UsePricing {
`${oceanAmount}`, `${oceanAmount}`,
swapFee swapFee
) )
.next((step: number) => setStep(step, 'pool')) .next((step: number) => setStep(step, 'pool', ddo))
: await ocean.fixedRateExchange : await ocean.fixedRateExchange
.create(dataToken, `${price}`, accountId, `${dtAmount}`) .create(dataToken, `${price}`, accountId, `${dtAmount}`)
.next((step: number) => setStep(step, 'exchange')) .next((step: number) => setStep(step, 'exchange', ddo))
await sleep(20000) await sleep(20000)
return tx return tx
} catch (error) { } catch (error) {
@ -282,8 +295,8 @@ function usePricing(ddo: DDO): UsePricing {
} }
return { return {
dtSymbol, getDTSymbol,
dtName, getDTName,
createPricing, createPricing,
buyDT, buyDT,
sellDT, sellDT,

View File

@ -11,7 +11,7 @@ export const validationSchema: Yup.SchemaOf<MetadataPublishFormAlgorithm> = Yup.
description: Yup.string().min(10).required('Required'), description: Yup.string().min(10).required('Required'),
files: Yup.array<FileMetadata>().required('Required').nullable(), files: Yup.array<FileMetadata>().required('Required').nullable(),
dockerImage: Yup.string() dockerImage: Yup.string()
.matches(/node:pre-defined|python:pre-defined|custom image/g, { .matches(/node:latest|python:latest|custom image/g, {
excludeEmptyString: true excludeEmptyString: true
}) })
.required('Required'), .required('Required'),
@ -30,9 +30,9 @@ export const validationSchema: Yup.SchemaOf<MetadataPublishFormAlgorithm> = Yup.
export const initialValues: Partial<MetadataPublishFormAlgorithm> = { export const initialValues: Partial<MetadataPublishFormAlgorithm> = {
name: '', name: '',
author: '', author: '',
dockerImage: 'node:pre-defined', dockerImage: 'node:latest',
image: 'node', image: 'node',
containerTag: '10', containerTag: 'latest',
entrypoint: 'node $ALGO', entrypoint: 'node $ALGO',
files: '', files: '',
description: '', description: '',

View File

@ -0,0 +1,13 @@
import * as Yup from 'yup'
export const validationSchema: Yup.SchemaOf<{
algorithm: string
}> = Yup.object().shape({
algorithm: Yup.string().required('Required')
})
export function getInitialValues(): { algorithm: string } {
return {
algorithm: undefined
}
}

View File

@ -74,6 +74,7 @@ function AssetProvider({
const [type, setType] = useState<MetadataMain['type']>() const [type, setType] = useState<MetadataMain['type']>()
const [variables, setVariables] = useState({}) const [variables, setVariables] = useState({})
/* eslint-disable @typescript-eslint/no-unused-vars */
const { const {
refetch: refetchFre, refetch: refetchFre,
startPolling: startPollingFre, startPolling: startPollingFre,
@ -90,6 +91,7 @@ function AssetProvider({
variables, variables,
skip: false skip: false
}) })
/* eslint-enable @typescript-eslint/no-unused-vars */
// this is not working as expected, thus we need to fetch both pool and fre // this is not working as expected, thus we need to fetch both pool and fre
// useEffect(() => { // useEffect(() => {

View File

@ -1,10 +1,10 @@
import { import {
Config,
DDO, DDO,
DID, DID,
Logger, Logger,
publisherTrustedAlgorithm as PublisherTrustedAlgorithm publisherTrustedAlgorithm as PublisherTrustedAlgorithm
} from '@oceanprotocol/lib/' } from '@oceanprotocol/lib/'
import { import {
QueryResult, QueryResult,
SearchQuery SearchQuery
@ -12,7 +12,6 @@ import {
import { AssetSelectionAsset } from '../components/molecules/FormFields/AssetSelection' import { AssetSelectionAsset } from '../components/molecules/FormFields/AssetSelection'
import axios, { CancelToken, AxiosResponse } from 'axios' import axios, { CancelToken, AxiosResponse } from 'axios'
import web3 from 'web3' import web3 from 'web3'
import { ConfigHelperConfig } from '@oceanprotocol/lib/dist/node/utils/ConfigHelper'
// TODO: import directly from ocean.js somehow. // TODO: import directly from ocean.js somehow.
// Transforming Aquarius' direct response is needed for getting actual DDOs // Transforming Aquarius' direct response is needed for getting actual DDOs
@ -107,33 +106,17 @@ export async function getAssetsNames(
} }
} }
export async function getAlgorithmsForAssetSelection( export async function transformDDOToAssetSelection(
ddoList: DDO[],
metadataCacheUri: string, metadataCacheUri: string,
selectedAlgorithms?: PublisherTrustedAlgorithm[] selectedAlgorithms?: PublisherTrustedAlgorithm[]
): Promise<AssetSelectionAsset[]> { ): Promise<AssetSelectionAsset[]> {
const query = {
page: 1,
query: {
query_string: {
query: `(service.attributes.main.type:algorithm) -isInPurgatory:true`
}
},
sort: { created: -1 }
}
const source = axios.CancelToken.source() const source = axios.CancelToken.source()
const didList: string[] = [] const didList: string[] = []
const priceList: any = {} const priceList: any = {}
const result = await queryMetadata( ddoList.forEach((ddo: DDO) => {
query as any, didList.push(ddo.id)
metadataCacheUri, priceList[ddo.id] = ddo.price.value
source.token
)
result?.results?.forEach((ddo: DDO) => {
const did: string = web3.utils
.toChecksumAddress(ddo.dataToken)
.replace('0x', 'did:op:')
didList.push(did)
priceList[did] = ddo.price.value
}) })
const ddoNames = await getAssetsNames(didList, metadataCacheUri, source.token) const ddoNames = await getAssetsNames(didList, metadataCacheUri, source.token)
const algorithmList: AssetSelectionAsset[] = [] const algorithmList: AssetSelectionAsset[] = []