1
0
mirror of https://github.com/oceanprotocol/market.git synced 2024-07-01 06:11:43 +02:00

Merge pull request #112 from oceanprotocol/feature/flow-splits

Split consume/compute/publish flows
This commit is contained in:
Matthias Kretschmann 2020-10-21 17:38:02 +02:00 committed by GitHub
commit b67dce4252
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
57 changed files with 1638 additions and 1222 deletions

View File

@ -42,9 +42,9 @@
"required": true
},
{
"name": "price",
"label": "Price",
"type": "price",
"name": "dataTokenOptions",
"label": "Datatoken",
"type": "datatoken",
"required": true
},
{
@ -96,21 +96,5 @@
}
],
"success": "Asset Created!"
},
"price": {
"fixed": {
"title": "Fixed",
"info": "Set your price for accessing this data set. A Datatoken for this data set, worth the entered amount of OCEAN, will be created."
},
"dynamic": {
"title": "Dynamic",
"info": "Let's create a decentralized, automated market for your data set. A Datatoken for this data set, worth the entered amount of OCEAN, will be created. Additionally, you will provide liquidity into a OCEAN/Datatoken liquidity pool with Balancer.",
"tooltips": {
"poolInfo": "Explain what is going on here...",
"swapFee": "Explain liquidity provider fee...",
"communityFee": "Explain community fee...",
"marketplaceFee": "Explain marketplace fee..."
}
}
}
}

View File

@ -1,4 +1,28 @@
{
"create": {
"empty": {
"title": "No Price Created",
"info": "This data set has no price yet. As the publisher you can create a fixed price, or a dynamic price for it. Onwards!",
"action": {
"name": "Create Pricing",
"help": "Create Pricing will mint your datatokens, approve spending, and create either a pool or a fixed rate exchange in one process. You will need to approve those multiple steps in your wallet."
}
},
"fixed": {
"title": "Fixed",
"info": "Set your price for accessing this data set. The datatoken for this data set will be worth the entered amount of OCEAN."
},
"dynamic": {
"title": "Dynamic",
"info": "Let's create a decentralized, automated market for your data set. The datatoken for this data set will be worth the entered amount of OCEAN. Additionally, you will provide liquidity into a Datatoken/OCEAN liquidity pool with Balancer.",
"tooltips": {
"poolInfo": "Explain what is going on here...",
"swapFee": "Explain liquidity provider fee...",
"communityFee": "Explain community fee...",
"marketplaceFee": "Explain marketplace fee..."
}
}
},
"pool": {
"tooltips": {
"price": "Explain how this price is determined...",

1642
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -22,8 +22,8 @@
"@coingecko/cryptoformat": "^0.4.2",
"@loadable/component": "5.13.1",
"@oceanprotocol/art": "^3.0.0",
"@oceanprotocol/lib": "^0.6.5",
"@oceanprotocol/react": "^0.2.2",
"@oceanprotocol/lib": "^0.7.1",
"@oceanprotocol/react": "^0.3.2",
"@oceanprotocol/typographies": "^0.1.0",
"@sindresorhus/slugify": "^1.0.0",
"@tippyjs/react": "^4.2.0",
@ -40,19 +40,19 @@
"ethereum-blockies": "github:MyEtherWallet/blockies",
"filesize": "^6.1.0",
"formik": "^2.2.0",
"gatsby": "^2.24.80",
"gatsby": "^2.24.84",
"gatsby-image": "^2.4.21",
"gatsby-plugin-manifest": "^2.4.35",
"gatsby-plugin-react-helmet": "^3.3.14",
"gatsby-plugin-remove-trailing-slashes": "^2.3.13",
"gatsby-plugin-sharp": "^2.6.42",
"gatsby-plugin-sharp": "^2.6.43",
"gatsby-plugin-svgr": "^2.0.2",
"gatsby-plugin-webpack-size": "^1.0.0",
"gatsby-source-filesystem": "^2.3.35",
"gatsby-source-graphql": "^2.7.6",
"gatsby-transformer-json": "^2.4.14",
"gatsby-transformer-remark": "^2.8.42",
"gatsby-transformer-sharp": "^2.5.18",
"gatsby-transformer-json": "^2.4.15",
"gatsby-transformer-remark": "^2.8.45",
"gatsby-transformer-sharp": "^2.5.19",
"intersection-observer": "^0.11.0",
"is-url-superb": "^4.0.0",
"lodash.debounce": "^4.0.8",
@ -74,7 +74,7 @@
"remove-markdown": "^0.3.0",
"shortid": "^2.2.15",
"slugify": "^1.4.5",
"swr": "^0.3.5",
"swr": "^0.3.6",
"yup": "^0.29.3"
},
"devDependencies": {
@ -86,11 +86,11 @@
"@svgr/webpack": "^5.4.0",
"@testing-library/jest-dom": "^5.11.4",
"@testing-library/react": "^11.1.0",
"@types/jest": "^26.0.14",
"@types/jest": "^26.0.15",
"@types/loadable__component": "^5.13.1",
"@types/lodash.debounce": "^4.0.3",
"@types/lodash.omit": "^4.5.6",
"@types/node": "^14.11.10",
"@types/node": "^14.14.0",
"@types/react": "^16.9.53",
"@types/react-datepicker": "^3.1.1",
"@types/react-helmet": "^6.1.0",
@ -99,18 +99,18 @@
"@types/remove-markdown": "^0.1.1",
"@types/shortid": "0.0.29",
"@types/yup": "^0.29.8",
"@typescript-eslint/eslint-plugin": "^4.4.1",
"@typescript-eslint/parser": "^4.4.1",
"@typescript-eslint/eslint-plugin": "^4.5.0",
"@typescript-eslint/parser": "^4.5.0",
"babel-loader": "^8.1.0",
"babel-preset-react-app": "^9.1.2",
"eslint": "^7.11.0",
"eslint-config-oceanprotocol": "^1.5.0",
"eslint-config-prettier": "^6.13.0",
"eslint-plugin-prettier": "^3.1.4",
"eslint-plugin-react": "^7.21.4",
"eslint-plugin-react-hooks": "^4.1.2",
"eslint-plugin-react": "^7.21.5",
"eslint-plugin-react-hooks": "^4.2.0",
"identity-obj-proxy": "^3.0.0",
"jest": "^26.5.3",
"jest": "^26.6.0",
"prettier": "^2.1.2",
"serve": "^11.3.2",
"source-map-explorer": "^2.5.0",

View File

@ -4,12 +4,11 @@ import {
AdditionalInformation,
ServiceMetadata
} from '@oceanprotocol/lib'
import { PriceOptions, DataTokenOptions } from '@oceanprotocol/react'
import { DataTokenOptions, PriceOptions } from '@oceanprotocol/react'
export interface AdditionalInformationMarket extends AdditionalInformation {
links?: File[]
termsAndConditions: boolean
priceType: string
}
export interface MetadataMarket extends Metadata {
@ -22,8 +21,6 @@ export interface MetadataMarket extends Metadata {
export interface PriceOptionsMarket extends PriceOptions {
// easier to keep this as number for Yup input validation
swapFee: number
// collect datatoken info for publishing
datatoken?: DataTokenOptions
}
export interface MetadataPublishForm {
@ -33,7 +30,7 @@ export interface MetadataPublishForm {
files: string | File[]
author: string
license: string
price: PriceOptionsMarket
dataTokenOptions: DataTokenOptions
access: 'Download' | 'Compute' | string
termsAndConditions: boolean
// ---- optional fields ----

View File

@ -30,6 +30,10 @@
font-size: var(--font-size-small);
}
.action {
margin-top: calc(var(--spacer) / 2);
}
/* States */
.error {
border-color: var(--rbrand-alert-ed);
@ -43,7 +47,7 @@
.info {
border-color: var(--brand-alert-yellow);
color: var(--brand-alert-yellow);
color: #9f7e19;
}
.warning {

View File

@ -1,19 +1,35 @@
import React, { ReactElement } from 'react'
import React, { ReactElement, FormEvent } from 'react'
import styles from './Alert.module.css'
import Button from './Button'
export default function Alert({
title,
text,
state
state,
action
}: {
title?: string
text: string
state: 'error' | 'warning' | 'info' | 'success'
action?: {
name: string
handleAction: (e: FormEvent<HTMLButtonElement>) => void
}
}): ReactElement {
return (
<div className={`${styles.alert} ${styles[state]}`}>
{title && <h3 className={styles.title}>{title}</h3>}
<p className={styles.text}>{text}</p>
{action && (
<Button
className={styles.action}
size="small"
style="primary"
onClick={action.handleAction}
>
{action.name}
</Button>
)}
</div>
)
}

View File

@ -242,7 +242,7 @@ input[type='range']::-moz-range-track {
.small {
font-size: var(--font-size-small);
min-height: 34px;
height: 34px;
padding: calc(var(--spacer) / 4);
}
@ -252,7 +252,7 @@ input[type='range']::-moz-range-track {
.prefix.small,
.postfix.small {
min-height: 34px;
height: 34px;
font-size: var(--font-size-mini);
}

View File

@ -4,7 +4,7 @@ import styles from './InputElement.module.css'
import { InputProps } from '.'
import FilesInput from '../../molecules/FormFields/FilesInput'
import Terms from '../../molecules/FormFields/Terms'
import Price from '../../molecules/FormFields/Price'
import Datatoken from '../../molecules/FormFields/Datatoken'
const DefaultInput = ({
small,
@ -87,8 +87,8 @@ export default function InputElement({
)
case 'files':
return <FilesInput name={name} {...field} {...props} />
case 'price':
return <Price name={name} {...field} {...props} />
case 'datatoken':
return <Datatoken name={name} {...field} {...props} />
case 'terms':
return <Terms name={name} options={options} {...field} {...props} />
default:

View File

@ -1,12 +1,12 @@
.loaderWrap {
display: flex;
align-items: center;
}
.loader {
display: block;
width: 20px;
height: 20px;
flex: 0 0 1.2rem;
width: 1.2rem;
height: 1.2rem;
border-radius: 50%;
border: 2px solid var(--brand-purple);
border-top-color: var(--brand-violet);

View File

@ -28,7 +28,7 @@ export default function SuccessConfetti({
action
}: {
success: string
action: ReactNode
action?: ReactNode
}): ReactElement {
// Have some confetti upon success
useEffect(() => {

View File

@ -0,0 +1,5 @@
.datatoken {
border: 1px solid var(--brand-grey-lighter);
padding: calc(var(--spacer) / 3);
border-radius: var(--border-radius);
}

View File

@ -0,0 +1,32 @@
import { useField } from 'formik'
import { InputProps } from '../../../atoms/Input'
import { useOcean } from '@oceanprotocol/react'
import React, { ReactElement, useEffect } from 'react'
import styles from './index.module.css'
import RefreshName from './RefreshName'
export default function Datatoken(props: InputProps): ReactElement {
const { ocean } = useOcean()
const [field, meta, helpers] = useField(props.name)
function generateName() {
if (!ocean) return
const dataTokenOptions = ocean.datatokens.generateDtName()
helpers.setValue({ ...dataTokenOptions })
}
// Generate new DT name & symbol
useEffect(() => {
if (!ocean) return
generateName()
}, [ocean])
return (
<div className={styles.datatoken}>
<strong>{field?.value?.name}</strong> {' '}
<strong>{field?.value?.symbol}</strong>
<RefreshName generateName={generateName} />
</div>
)
}

View File

@ -1,116 +0,0 @@
import React, { ReactElement, useState, useEffect } from 'react'
import { graphql, useStaticQuery } from 'gatsby'
import { InputProps } from '../../../atoms/Input'
import styles from './index.module.css'
import Tabs from '../../../atoms/Tabs'
import Fixed from './Fixed'
import Dynamic from './Dynamic'
import { useField, useFormikContext } from 'formik'
import { useUserPreferences } from '../../../../providers/UserPreferences'
import { useOcean } from '@oceanprotocol/react'
import { PriceOptionsMarket } from '../../../../@types/MetaData'
const query = graphql`
query PriceFieldQuery {
content: allFile(filter: { relativePath: { eq: "pages/publish.json" } }) {
edges {
node {
childPagesJson {
price {
fixed {
title
info
}
dynamic {
title
info
tooltips {
poolInfo
swapFee
communityFee
marketplaceFee
}
}
}
}
}
}
}
}
`
export default function Price(props: InputProps): ReactElement {
const { debug } = useUserPreferences()
const data = useStaticQuery(query)
const content = data.content.edges[0].node.childPagesJson.price
const { ocean } = useOcean()
const [field, meta, helpers] = useField(props.name)
const { price }: PriceOptionsMarket = field.value
const [tokensToMint, setTokensToMint] = useState<number>()
function handleTabChange(tabName: string) {
const type = tabName.toLowerCase()
helpers.setValue({ ...field.value, type })
}
function generateName() {
if (!ocean) return
const datatoken = ocean.datatokens.generateDtName()
helpers.setValue({ ...field.value, datatoken })
}
// Always update everything when amountOcean changes
useEffect(() => {
const tokensToMint = Number(price) * Number(field.value.weightOnDataToken)
setTokensToMint(tokensToMint)
helpers.setValue({ ...field.value, tokensToMint })
}, [price])
// Generate new DT name & symbol, but only once automatically
useEffect(() => {
if (!ocean || typeof field?.value?.datatoken?.name !== 'undefined') return
generateName()
}, [ocean])
const tabs = [
{
title: content.fixed.title,
content: (
<Fixed
datatokenOptions={field.value.datatoken}
generateName={generateName}
content={content.fixed}
/>
)
},
{
title: content.dynamic.title,
content: (
<Dynamic
ocean={price}
priceOptions={{ ...field.value, tokensToMint }}
datatokenOptions={field.value.datatoken}
generateName={generateName}
content={content.dynamic}
/>
)
}
]
return (
<div className={styles.price}>
<Tabs
items={tabs}
handleTabChange={handleTabChange}
defaultIndex={field?.value?.type === 'fixed' ? 0 : 1}
/>
{debug === true && (
<pre>
<code>{JSON.stringify(field.value, null, 2)}</code>
</pre>
)}
</div>
)
}

View File

@ -22,3 +22,7 @@
.feedback {
width: 100%;
}
.hasTokens {
composes: hasTokens from './index.module.css';
}

View File

@ -13,5 +13,5 @@ export default {
}
export const Default = (): ReactElement => (
<Compute ddo={ddo as DDO} isBalanceSufficient />
<Compute ddo={ddo as DDO} dtBalance="1" isBalanceSufficient />
)

View File

@ -9,7 +9,8 @@ import {
computeOptions,
useCompute,
readFileContent,
useOcean
useOcean,
usePricing
} from '@oceanprotocol/react'
import styles from './Compute.module.css'
import Button from '../../atoms/Button'
@ -19,14 +20,19 @@ import { useSiteMetadata } from '../../../hooks/useSiteMetadata'
export default function Compute({
ddo,
isBalanceSufficient
isBalanceSufficient,
dtBalance
}: {
ddo: DDO
isBalanceSufficient: boolean
dtBalance: string
}): ReactElement {
const { marketFeeAddress } = useSiteMetadata()
const { ocean } = useOcean()
const { compute, isLoading, computeStepText, computeError } = useCompute()
const { marketFeeAddress } = useSiteMetadata()
const { buyDT, dtSymbol } = usePricing(ddo)
const computeService = ddo.findServiceByType('compute')
const metadataService = ddo.findServiceByType('metadata')
@ -48,6 +54,7 @@ export default function Compute({
computeType === '' ||
!ocean ||
!isBalanceSufficient
const hasDatatoken = Number(dtBalance) >= 1
const onDrop = async (files: File[]) => {
setFile(files[0])
@ -72,6 +79,8 @@ export default function Compute({
setIsPublished(false)
setError('')
!hasDatatoken && (await buyDT('1'))
await compute(
ddo.id,
computeService,
@ -99,6 +108,12 @@ export default function Compute({
</div>
<div className={styles.pricewrapper}>
<Price ddo={ddo} conversion />
{hasDatatoken && (
<div className={styles.hasTokens}>
You own {dtBalance} {dtSymbol} allowing you to use this data set
without paying again.
</div>
)}
</div>
</div>
@ -120,7 +135,7 @@ export default function Compute({
onClick={() => startJob()}
disabled={isComputeButtonDisabled}
>
Start job
{hasDatatoken ? 'Start job' : 'Buy'}
</Button>
</div>

View File

@ -12,10 +12,11 @@
flex-shrink: 0;
}
.pricewrapper button {
margin-top: var(--spacer);
.actions {
width: 100%;
margin-top: calc(var(--spacer) / 2);
}
.feedback {
width: 100%;
.hasTokens {
composes: hasTokens from './index.module.css';
}

View File

@ -15,6 +15,7 @@ export default {
export const PricedAsset = (): ReactElement => (
<Consume
ddo={ddo as DDO}
dtBalance="1"
isBalanceSufficient
file={new DDO(ddo).findServiceByType('metadata').attributes.main.files[0]}
/>

View File

@ -1,4 +1,4 @@
import React, { ReactElement } from 'react'
import React, { ReactElement, useEffect } from 'react'
import { toast } from 'react-toastify'
import { File as FileMetadata, DDO } from '@oceanprotocol/lib'
import Button from '../../atoms/Button'
@ -7,41 +7,56 @@ import Price from '../../atoms/Price'
import Web3Feedback from '../../molecules/Wallet/Feedback'
import styles from './Consume.module.css'
import Loader from '../../atoms/Loader'
import { useOcean, useConsume } from '@oceanprotocol/react'
import { useOcean, useConsume, usePricing } from '@oceanprotocol/react'
import { useSiteMetadata } from '../../../hooks/useSiteMetadata'
export default function Consume({
ddo,
file,
isBalanceSufficient
isBalanceSufficient,
dtBalance
}: {
ddo: DDO
file: FileMetadata
isBalanceSufficient: boolean
dtBalance: string
}): ReactElement {
const { ocean } = useOcean()
const { marketFeeAddress } = useSiteMetadata()
const {
dtSymbol,
buyDT,
pricingStepText,
pricingError,
pricingIsLoading
} = usePricing(ddo)
const { consumeStepText, consume, consumeError } = useConsume()
const isDisabled = !ocean || !isBalanceSufficient
const isDisabled =
!ocean ||
!isBalanceSufficient ||
typeof consumeStepText !== 'undefined' ||
pricingIsLoading
const hasDatatoken = Number(dtBalance) >= 1
if (consumeError) {
toast.error(consumeError)
async function handleConsume() {
!hasDatatoken && (await buyDT('1'))
await consume(ddo.id, ddo.dataToken, 'access', marketFeeAddress)
}
// Output errors in UI
useEffect(() => {
consumeError && toast.error(consumeError)
pricingError && toast.error(pricingError)
}, [consumeError, pricingError])
const PurchaseButton = () => (
<div>
{consumeStepText ? (
<Loader message={consumeStepText} />
<div className={styles.actions}>
{consumeStepText || pricingIsLoading ? (
<Loader message={consumeStepText || pricingStepText} />
) : (
<Button
style="primary"
onClick={() =>
consume(ddo.id, ddo.dataToken, 'access', marketFeeAddress)
}
disabled={isDisabled}
>
Buy
<Button style="primary" onClick={handleConsume} disabled={isDisabled}>
{hasDatatoken ? 'Download' : 'Buy'}
</Button>
)}
</div>
@ -55,6 +70,12 @@ export default function Consume({
</div>
<div className={styles.pricewrapper}>
<Price ddo={ddo} conversion />
{hasDatatoken && (
<div className={styles.hasTokens}>
You own {dtBalance} {dtSymbol} allowing you to use this data set
without paying again.
</div>
)}
<PurchaseButton />
</div>
</div>

View File

@ -1,5 +1,5 @@
import React, { ReactElement, useEffect, useState } from 'react'
import { useOcean, useMetadata } from '@oceanprotocol/react'
import { useOcean, useMetadata, usePricing } from '@oceanprotocol/react'
import { DDO, Logger } from '@oceanprotocol/lib'
import styles from './index.module.css'
import stylesActions from './Actions.module.css'
@ -45,10 +45,10 @@ export default function Pool({ ddo }: { ddo: DDO }): ReactElement {
const { ocean, accountId } = useOcean()
const { price } = useMetadata(ddo)
const { dtSymbol } = usePricing(ddo)
const [poolTokens, setPoolTokens] = useState<string>()
const [totalPoolTokens, setTotalPoolTokens] = useState<string>()
const [dtSymbol, setDtSymbol] = useState<string>()
const [userLiquidity, setUserLiquidity] = useState<Balance>()
const [swapFee, setSwapFee] = useState<string>()
@ -79,12 +79,6 @@ export default function Pool({ ddo }: { ddo: DDO }): ReactElement {
setIsLoading(true)
try {
//
// Get data token symbol
//
const dtSymbol = await ocean.datatokens.getSymbol(ddo.dataToken)
setDtSymbol(dtSymbol)
//
// Get everything which is in the pool
//

View File

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

View File

@ -2,37 +2,63 @@ import React, { ReactElement, useState, useEffect } from 'react'
import styles from './index.module.css'
import Compute from './Compute'
import Consume from './Consume'
import { DDO } from '@oceanprotocol/lib'
import { DDO, Logger } from '@oceanprotocol/lib'
import Tabs from '../../atoms/Tabs'
import { useOcean, useMetadata } from '@oceanprotocol/react'
import compareAsBN from '../../../utils/compareAsBN'
import Pool from './Pool'
import { AdditionalInformationMarket } from '../../../@types/MetaData'
export default function AssetActions({ ddo }: { ddo: DDO }): ReactElement {
const { balance } = useOcean()
const { ocean, balance, accountId } = useOcean()
const { price } = useMetadata(ddo)
const [isBalanceSufficient, setIsBalanceSufficient] = useState<boolean>()
const [dtBalance, setDtBalance] = useState<string>()
const isCompute = Boolean(ddo.findServiceByType('compute'))
const { attributes } = ddo.findServiceByType('metadata')
// Get and set user DT balance
useEffect(() => {
if (!ocean || !accountId) return
async function init() {
try {
const dtBalance = await ocean.datatokens.balance(
ddo.dataToken,
accountId
)
setDtBalance(dtBalance)
} catch (e) {
Logger.error(e.message)
}
}
init()
}, [ocean, accountId, ddo.dataToken])
// Check user balance against price
useEffect(() => {
if (!price || !price.value || !balance || !balance.ocean) return
if (!price || !price.value || !balance || !balance.ocean || !dtBalance)
return
setIsBalanceSufficient(compareAsBN(balance.ocean, `${price.value}`))
setIsBalanceSufficient(
compareAsBN(balance.ocean, `${price.value}`) || Number(dtBalance) >= 1
)
return () => {
setIsBalanceSufficient(false)
}
}, [balance, price])
}, [balance, price, dtBalance])
const UseContent = isCompute ? (
<Compute ddo={ddo} isBalanceSufficient={isBalanceSufficient} />
<Compute
ddo={ddo}
dtBalance={dtBalance}
isBalanceSufficient={isBalanceSufficient}
/>
) : (
<Consume
ddo={ddo}
dtBalance={dtBalance}
isBalanceSufficient={isBalanceSufficient}
file={attributes.main.files[0]}
/>
@ -46,10 +72,7 @@ export default function AssetActions({ ddo }: { ddo: DDO }): ReactElement {
]
// Check from metadata, cause that is available earlier
const hasPool =
((attributes.additionalInformation as unknown) as AdditionalInformationMarket)
?.priceType === 'dynamic'
// price?.type === 'pool'
const hasPool = ddo.price?.type === 'pool'
hasPool &&
tabs.push({

View File

@ -1,12 +1,11 @@
import React, { ReactElement, useEffect, useState } from 'react'
import React, { ReactElement } from 'react'
import Time from '../../atoms/Time'
import MetaItem from './MetaItem'
import styles from './MetaFull.module.css'
import { MetadataMarket } from '../../../@types/MetaData'
import { DDO } from '@oceanprotocol/lib'
import EtherscanLink from '../../atoms/EtherscanLink'
import { useOcean } from '@oceanprotocol/react'
import { usePricing } from '@oceanprotocol/react'
export default function MetaFull({
ddo,
@ -15,24 +14,9 @@ export default function MetaFull({
ddo: DDO
metadata: MetadataMarket
}): ReactElement {
const { ocean } = useOcean()
const { id, dataToken } = ddo
const { dateCreated, datePublished, author, license } = metadata.main
const [dtName, setDtName] = useState<string>()
const [dtSymbol, setDtSymbol] = useState<string>()
useEffect(() => {
if (!ocean) return
async function getDataTokenInfo() {
const name = await ocean.datatokens.getName(dataToken)
setDtName(name)
const symbol = await ocean.datatokens.getSymbol(dataToken)
setDtSymbol(symbol)
}
getDataTokenInfo()
}, [ocean, dataToken])
const { dtSymbol, dtName } = usePricing(ddo)
return (
<div className={styles.metaFull}>

View File

@ -0,0 +1,8 @@
.feedback {
width: 100%;
min-height: 20vh;
display: flex;
flex-wrap: wrap;
justify-content: center;
align-items: center;
}

View File

@ -0,0 +1,34 @@
import Loader from '../../../atoms/Loader'
import SuccessConfetti from '../../../atoms/SuccessConfetti'
import React, { ReactElement } from 'react'
import styles from './Feedback.module.css'
import Button from '../../../atoms/Button'
export default function Feedback({
success,
pricingStepText
}: {
success: string
pricingStepText: string
}): ReactElement {
const SuccessAction = () => (
<Button
style="primary"
size="small"
className={styles.action}
onClick={() => window?.location.reload()}
>
Reload Page
</Button>
)
return (
<div className={styles.feedback}>
{success ? (
<SuccessConfetti success={success} action={<SuccessAction />} />
) : (
<Loader message={pricingStepText} />
)}
</div>
)
}

View File

@ -8,7 +8,7 @@
}
.icon {
composes: box from '../../../atoms/Box.module.css';
composes: box from '../../../../atoms/Box.module.css';
padding: calc(var(--spacer) / 1.5);
width: 6rem;
height: 6rem;

View File

@ -1,11 +1,10 @@
import React, { ReactElement } from 'react'
import stylesIndex from './index.module.css'
import styles from './Coin.module.css'
import InputElement from '../../../atoms/Input/InputElement'
import { ReactComponent as Logo } from '../../../../images/logo.svg'
import Conversion from '../../../atoms/Price/Conversion'
import InputElement from '../../../../atoms/Input/InputElement'
import { ReactComponent as Logo } from '../../../../../images/logo.svg'
import Conversion from '../../../../atoms/Price/Conversion'
import { DataTokenOptions } from '@oceanprotocol/react'
import RefreshName from './RefreshName'
import { useField } from 'formik'
import Error from './Error'
@ -13,13 +12,11 @@ export default function Coin({
datatokenOptions,
name,
weight,
generateName,
readOnly
}: {
datatokenOptions: DataTokenOptions
name: string
weight: string
generateName?: () => void
readOnly?: boolean
}): ReactElement {
const [field, meta] = useField(name)
@ -32,9 +29,6 @@ export default function Coin({
<h4 className={styles.tokenName}>
{datatokenOptions?.name || 'Data Token'}
{datatokenOptions?.name && typeof generateName === 'function' && (
<RefreshName generateName={generateName} />
)}
</h4>
<div className={styles.weight}>
@ -47,6 +41,8 @@ export default function Coin({
readOnly={readOnly}
prefix={datatokenOptions?.symbol || 'DT'}
min="1"
name={name}
value={field.value}
{...field}
/>
{datatokenOptions?.symbol === 'OCEAN' && (

View File

@ -40,6 +40,7 @@
margin-left: -2rem;
margin-right: -2rem;
border-bottom: 1px solid var(--brand-grey-lighter);
background: var(--brand-grey-dimmed);
}
@media screen and (min-width: 40rem) {

View File

@ -1,34 +1,34 @@
import { DataTokenOptions, useOcean } from '@oceanprotocol/react'
import PriceUnit from '../../../atoms/Price/PriceUnit'
import { useOcean, usePricing } from '@oceanprotocol/react'
import PriceUnit from '../../../../atoms/Price/PriceUnit'
import React, { ReactElement, useEffect, useState } from 'react'
import { PriceOptionsMarket } from '../../../../@types/MetaData'
import { useSiteMetadata } from '../../../../hooks/useSiteMetadata'
import { isCorrectNetwork } from '../../../../utils/wallet'
import Alert from '../../../atoms/Alert'
import FormHelp from '../../../atoms/Input/Help'
import Tooltip from '../../../atoms/Tooltip'
import Wallet from '../../Wallet'
import { useSiteMetadata } from '../../../../../hooks/useSiteMetadata'
import { isCorrectNetwork } from '../../../../../utils/wallet'
import Alert from '../../../../atoms/Alert'
import FormHelp from '../../../../atoms/Input/Help'
import Tooltip from '../../../../atoms/Tooltip'
import Wallet from '../../../../molecules/Wallet'
import Coin from './Coin'
import styles from './Dynamic.module.css'
import Fees from './Fees'
import stylesIndex from './index.module.css'
import { useFormikContext } from 'formik'
import { PriceOptionsMarket } from '../../../../../@types/MetaData'
import { DDO } from '@oceanprotocol/lib'
export default function Dynamic({
ocean,
priceOptions,
datatokenOptions,
generateName,
ddo,
content
}: {
ocean: number
priceOptions: PriceOptionsMarket
datatokenOptions: DataTokenOptions
generateName: () => void
ddo: DDO
content: any
}): ReactElement {
const { appConfig } = useSiteMetadata()
const { account, balance, networkId, refreshBalance } = useOcean()
const { weightOnDataToken } = priceOptions
const { dtSymbol, dtName } = usePricing(ddo)
// Connect with form
const { values } = useFormikContext()
const { price, weightOnDataToken } = values as PriceOptionsMarket
const [error, setError] = useState<string>()
const correctNetwork = isCorrectNetwork(networkId)
@ -42,12 +42,12 @@ export default function Dynamic({
setError(`No account connected. Please connect your Web3 wallet.`)
} else if (!correctNetwork) {
setError(`Wrong Network. Please connect to ${desiredNetworkName}.`)
} else if (Number(balance.ocean) < Number(ocean)) {
setError(`Insufficient balance. You need at least ${ocean} OCEAN`)
} else if (Number(balance.ocean) < Number(price)) {
setError(`Insufficient balance. You need at least ${price} OCEAN`)
} else {
setError(undefined)
}
}, [ocean, networkId, account, balance, correctNetwork, desiredNetworkName])
}, [price, networkId, account, balance, correctNetwork, desiredNetworkName])
// refetch balance periodically
useEffect(() => {
@ -59,7 +59,7 @@ export default function Dynamic({
return () => {
clearInterval(balanceInterval)
}
}, [ocean, networkId, account])
}, [networkId, account])
return (
<div className={styles.dynamic}>
@ -83,15 +83,14 @@ export default function Dynamic({
<div className={styles.tokens}>
<Coin
name="price.price"
name="price"
datatokenOptions={{ symbol: 'OCEAN', name: 'Ocean Token' }}
weight={`${100 - Number(Number(weightOnDataToken) * 10)}%`}
/>
<Coin
name="price.tokensToMint"
datatokenOptions={datatokenOptions}
name="dtAmount"
datatokenOptions={{ symbol: dtSymbol, name: dtName }}
weight={`${Number(weightOnDataToken) * 10}%`}
generateName={generateName}
readOnly
/>
</div>

View File

@ -1,6 +1,6 @@
import { FieldMetaProps } from 'formik'
import React, { ReactElement } from 'react'
import stylesInput from '../../../atoms/Input/index.module.css'
import stylesInput from '../../../../atoms/Input/index.module.css'
export default function Error({
meta

View File

@ -1,16 +1,16 @@
.fees {
display: grid;
gap: var(--spacer);
grid-template-columns: repeat(auto-fit, minmax(10rem, 1fr));
grid-template-columns: repeat(auto-fit, minmax(8rem, 1fr));
margin-left: -2rem;
margin-right: -2rem;
border-bottom: 1px solid var(--brand-grey-lighter);
margin-top: var(--spacer);
padding: 0 var(--spacer) calc(var(--spacer) / 2) var(--spacer);
padding: var(--spacer) var(--spacer) calc(var(--spacer) / 2) var(--spacer);
justify-content: center;
text-align: center;
border-bottom: 1px solid var(--brand-grey-lighter);
background: var(--brand-grey-dimmed);
}
.fees label {

View File

@ -1,18 +1,40 @@
import React, { ReactElement } from 'react'
import Tooltip from '../../../atoms/Tooltip'
import Tooltip from '../../../../atoms/Tooltip'
import styles from './Fees.module.css'
import { useSiteMetadata } from '../../../../hooks/useSiteMetadata'
import { useField } from 'formik'
import Input from '../../../atoms/Input'
import { useField, useFormikContext } from 'formik'
import Input from '../../../../atoms/Input'
import Error from './Error'
const Default = ({
title,
name,
tooltip
}: {
title: string
name: string
tooltip: string
}) => (
<Input
label={
<>
{title}
<Tooltip content={tooltip} />
</>
}
value="0.1"
name={name}
postfix="%"
readOnly
small
/>
)
export default function Fees({
tooltips
}: {
tooltips: { [key: string]: string }
}): ReactElement {
const { appConfig } = useSiteMetadata()
const [field, meta] = useField('price.swapFee')
const [field, meta] = useField('swapFee')
return (
<>
@ -34,32 +56,16 @@ export default function Fees({
additionalComponent={<Error meta={meta} />}
/>
<Input
label={
<>
Community Fee
<Tooltip content={tooltips.communityFee} />
</>
}
value="0.1"
<Default
title="Community Fee"
name="communityFee"
postfix="%"
readOnly
small
tooltip={tooltips.communityFee}
/>
<Input
label={
<>
Marketplace Fee
<Tooltip content={tooltips.marketplaceFee} />
</>
}
value="0.1"
<Default
title="Marketplace Fee"
name="marketplaceFee"
postfix="%"
readOnly
small
tooltip={tooltips.marketplaceFee}
/>
</div>
</>

View File

@ -14,11 +14,16 @@
}
.grid {
margin-top: var(--spacer);
margin-left: -2rem;
margin-right: -2rem;
padding-top: var(--spacer);
display: grid;
gap: var(--spacer);
grid-template-columns: repeat(auto-fit, minmax(12rem, 1fr));
justify-content: center;
background: var(--brand-grey-dimmed);
border-top: 1px solid var(--brand-grey-lighter);
border-bottom: 1px solid var(--brand-grey-lighter);
}
.fixed label {
@ -26,14 +31,14 @@
}
.datatoken {
margin-top: calc(var(--spacer) / 6);
margin-top: calc(var(--spacer) / 2);
color: var(--color-secondary);
font-size: var(--font-size-small);
font-weight: var(--font-weight-bold);
}
.datatoken h4 {
font-size: var(--font-size-small);
font-size: var(--font-size-base);
color: var(--color-secondary);
margin: 0;
}

View File

@ -1,24 +1,23 @@
import React, { ReactElement } from 'react'
import stylesIndex from './index.module.css'
import styles from './Fixed.module.css'
import FormHelp from '../../../atoms/Input/Help'
import Conversion from '../../../atoms/Price/Conversion'
import { DataTokenOptions } from '@oceanprotocol/react'
import RefreshName from './RefreshName'
import FormHelp from '../../../../atoms/Input/Help'
import Conversion from '../../../../atoms/Price/Conversion'
import { useField } from 'formik'
import Input from '../../../atoms/Input'
import Input from '../../../../atoms/Input'
import Error from './Error'
import { DDO } from '@oceanprotocol/lib'
import { usePricing } from '@oceanprotocol/react'
export default function Fixed({
datatokenOptions,
generateName,
ddo,
content
}: {
datatokenOptions: DataTokenOptions
generateName: () => void
ddo: DDO
content: any
}): ReactElement {
const [field, meta] = useField('price.price')
const [field, meta] = useField('price')
const { dtName, dtSymbol } = usePricing(ddo)
return (
<div className={styles.fixed}>
@ -29,7 +28,7 @@ export default function Fixed({
<Input
label="Ocean Token"
value={field.value}
name="price.price"
name="price"
type="number"
prefix="OCEAN"
min="1"
@ -43,16 +42,11 @@ export default function Fixed({
/>
<Error meta={meta} />
</div>
{datatokenOptions && (
<div className={styles.datatoken}>
<h4>
Data Token <RefreshName generateName={generateName} />
</h4>
<strong>{datatokenOptions?.name}</strong> {' '}
<strong>{datatokenOptions?.symbol}</strong>
</div>
)}
<div className={styles.datatoken}>
<h4>
= <strong>1</strong> {dtName} {dtSymbol}
</h4>
</div>
</div>
</div>
)

View File

@ -1,9 +1,3 @@
.price {
border: 1px solid var(--brand-grey-lighter);
background: var(--brand-grey-dimmed);
border-radius: var(--border-radius);
}
.content {
padding: 0;
}
@ -36,3 +30,18 @@
text-align: center;
margin-bottom: calc(var(--spacer) / 1.5);
}
.actions {
text-align: center;
}
.actions button {
margin-left: calc(var(--spacer) / 2);
margin-right: calc(var(--spacer) / 2);
}
.actionsHelp {
margin-top: calc(var(--spacer) / 2);
padding-left: var(--spacer);
padding-right: var(--spacer);
}

View File

@ -0,0 +1,78 @@
import React, { ReactElement, useEffect } from 'react'
import styles from './index.module.css'
import Tabs from '../../../../atoms/Tabs'
import Fixed from './Fixed'
import Dynamic from './Dynamic'
import { useFormikContext } from 'formik'
import { useUserPreferences } from '../../../../../providers/UserPreferences'
import { PriceOptionsMarket } from '../../../../../@types/MetaData'
import Button from '../../../../atoms/Button'
import { DDO } from '@oceanprotocol/lib'
import FormHelp from '../../../../atoms/Input/Help'
export default function FormPricing({
ddo,
setShowPricing,
content
}: {
ddo: DDO
setShowPricing: (value: boolean) => void
content: any
}): ReactElement {
const { debug } = useUserPreferences()
// Connect with form
const { values, setFieldValue, submitForm } = useFormikContext()
const { price, weightOnDataToken, type } = values as PriceOptionsMarket
// Switch type value upon tab change
function handleTabChange(tabName: string) {
const type = tabName.toLowerCase()
setFieldValue('type', type)
}
// Always update everything when price value changes
useEffect(() => {
const dtAmount = Number(price) * Number(weightOnDataToken)
setFieldValue('dtAmount', dtAmount)
}, [price, weightOnDataToken])
const tabs = [
{
title: content.fixed.title,
content: <Fixed content={content.fixed} ddo={ddo} />
},
{
title: content.dynamic.title,
content: <Dynamic content={content.dynamic} ddo={ddo} />
}
]
return (
<>
<Tabs
items={tabs}
handleTabChange={handleTabChange}
defaultIndex={type === 'fixed' ? 0 : 1}
/>
<div className={styles.actions}>
<Button style="primary" onClick={() => submitForm()}>
{content.empty.action.name}
</Button>
<Button style="text" size="small" onClick={() => setShowPricing(false)}>
Cancel
</Button>
<FormHelp className={styles.actionsHelp}>
{content.empty.action.help}
</FormHelp>
</div>
{debug === true && (
<pre>
<code>{JSON.stringify(values, null, 2)}</code>
</pre>
)}
</>
)
}

View File

@ -0,0 +1,11 @@
.pricing {
composes: box from '../../../atoms/Box.module.css';
padding: 0;
padding-bottom: var(--spacer);
margin-top: var(--spacer);
}
.pricing [class*='alert'] {
margin: var(--spacer);
margin-bottom: 0;
}

View File

@ -0,0 +1,138 @@
import React, { FormEvent, ReactElement, useState } from 'react'
import { Formik } from 'formik'
import { initialValues, validationSchema } from '../../../../models/FormPricing'
import { DDO, Logger } from '@oceanprotocol/lib'
import { usePricing } from '@oceanprotocol/react'
import { PriceOptionsMarket } from '../../../../@types/MetaData'
import Alert from '../../../atoms/Alert'
import styles from './index.module.css'
import FormPricing from './FormPricing'
import { toast } from 'react-toastify'
import Feedback from './Feedback'
import { graphql, useStaticQuery } from 'gatsby'
const query = graphql`
query PricingQuery {
content: allFile(filter: { relativePath: { eq: "price.json" } }) {
edges {
node {
childContentJson {
create {
empty {
title
info
action {
name
help
}
}
fixed {
title
info
}
dynamic {
title
info
tooltips {
poolInfo
swapFee
communityFee
marketplaceFee
}
}
}
}
}
}
}
}
`
export default function Pricing({ ddo }: { ddo: DDO }): ReactElement {
// Get content
const data = useStaticQuery(query)
const content = data.content.edges[0].node.childContentJson.create
// View states
const [showPricing, setShowPricing] = useState(false)
const [success, setSuccess] = useState<string>()
const {
createPricing,
pricingIsLoading,
pricingError,
pricingStepText
} = usePricing(ddo)
const hasFeedback = pricingIsLoading || typeof success !== 'undefined'
async function handleCreatePricing(values: PriceOptionsMarket) {
try {
const priceOptions = {
...values,
// swapFee is tricky: to get 0.1% you need to send 0.001 as value
swapFee: `${values.swapFee / 100}`
}
const tx = await createPricing(priceOptions)
// Pricing failed
if (!tx || pricingError) {
toast.error(pricingError || 'Price creation failed.')
Logger.error(pricingError || 'Price creation failed.')
return
}
// Pricing succeeded
setSuccess(
`🎉 Successfully created a ${values.type} price. 🎉 Reload the page to get all updates.`
)
Logger.log(`Transaction: ${tx}`)
} catch (error) {
toast.error(error.message)
Logger.error(error.message)
}
}
function handleShowPricingForm(e: FormEvent<HTMLButtonElement>) {
e.preventDefault()
setShowPricing(true)
}
return (
<div className={styles.pricing}>
<Formik
initialValues={initialValues}
validationSchema={validationSchema}
onSubmit={async (values, { setSubmitting }) => {
// move user's focus to top of screen
window.scrollTo({ top: 0, left: 0, behavior: 'smooth' })
// Kick off price creation
await handleCreatePricing(values)
setSubmitting(false)
}}
>
{hasFeedback ? (
<Feedback success={success} pricingStepText={pricingStepText} />
) : showPricing ? (
<FormPricing
ddo={ddo}
setShowPricing={setShowPricing}
content={content}
/>
) : (
<Alert
state="info"
title={content.empty.title}
text={content.empty.info}
action={{
name: content.empty.action.name,
handleAction: handleShowPricingForm
}}
/>
)}
</Formik>
</div>
)
}

View File

@ -5,6 +5,10 @@
margin-top: -1.5rem;
}
.grid > div {
overflow: hidden;
}
.content {
composes: box from '../../atoms/Box.module.css';
margin-top: var(--spacer);
@ -12,7 +16,7 @@
@media (min-width: 60rem) {
.grid {
grid-template-columns: 1.5fr minmax(0, 1fr);
grid-template-columns: 1.5fr 1fr;
}
.sticky {

View File

@ -9,6 +9,8 @@ import styles from './index.module.css'
import AssetActions from '../AssetActions'
import { DDO } from '@oceanprotocol/lib'
import { useUserPreferences } from '../../../providers/UserPreferences'
import Pricing from './Pricing'
import { useOcean } from '@oceanprotocol/react'
export interface AssetContentProps {
metadata: MetadataMarket
@ -22,45 +24,55 @@ export default function AssetContent({
}: AssetContentProps): ReactElement {
const { datePublished } = metadata.main
const { debug } = useUserPreferences()
const { accountId } = useOcean()
const isOwner = accountId === ddo.publicKey[0].owner
const hasNoPrice = ddo.price.type === ''
const showPricing = isOwner && hasNoPrice
return (
<article className={styles.grid}>
<div className={styles.content}>
<aside className={styles.meta}>
<p>{datePublished && <Time date={datePublished} />}</p>
{metadata?.additionalInformation?.categories?.length && (
<p>
<Link
to={`/search?categories=["${metadata?.additionalInformation?.categories[0]}"]`}
>
{metadata?.additionalInformation?.categories[0]}
</Link>
</p>
)}
</aside>
<div>
{showPricing && <Pricing ddo={ddo} />}
<Markdown text={metadata?.additionalInformation?.description || ''} />
<div className={styles.content}>
<aside className={styles.meta}>
<p>{datePublished && <Time date={datePublished} />}</p>
{metadata?.additionalInformation?.categories?.length && (
<p>
<Link
to={`/search?categories=["${metadata?.additionalInformation?.categories[0]}"]`}
>
{metadata?.additionalInformation?.categories[0]}
</Link>
</p>
)}
</aside>
<MetaSecondary metadata={metadata} />
<Markdown text={metadata?.additionalInformation?.description || ''} />
<MetaFull ddo={ddo} metadata={metadata} />
<MetaSecondary metadata={metadata} />
<div className={styles.buttonGroup}>
{/* <EditAction
<MetaFull ddo={ddo} metadata={metadata} />
<div className={styles.buttonGroup}>
{/* <EditAction
ddo={ddo}
ocean={ocean}
account={account}
refetchMetadata={refetchMetadata}
/> */}
{/* <DeleteAction ddo={ddo} /> */}
</div>
{/* <DeleteAction ddo={ddo} /> */}
</div>
{debug === true && (
<pre>
<code>{JSON.stringify(ddo, null, 2)}</code>
</pre>
)}
{debug === true && (
<pre>
<code>{JSON.stringify(ddo, null, 2)}</code>
</pre>
)}
</div>
</div>
<div>
<div className={styles.sticky}>
<AssetActions ddo={ddo} />

View File

@ -3,28 +3,43 @@ import { MetadataPublishForm } from '../../../@types/MetaData'
import styles from './index.module.css'
import { transformPublishFormToMetadata } from './utils'
const Output = ({ title, output }: { title: string; output: any }) => (
<div>
<h5>{title}</h5>
<pre>
<code>{JSON.stringify(output, null, 2)}</code>
</pre>
</div>
)
export default function Debug({
values
}: {
values: Partial<MetadataPublishForm>
}): ReactElement {
const ddo = {
'@context': 'https://w3id.org/did/v1',
dataTokenInfo: {
...values.dataTokenOptions
},
service: [
{
index: 0,
type: 'metadata',
attributes: { ...transformPublishFormToMetadata(values) }
},
{
index: 1,
type: values.access,
attributes: {}
}
]
}
return (
<div className={styles.grid}>
<div>
<h5>Collected Form Values</h5>
<pre>
<code>{JSON.stringify(values, null, 2)}</code>
</pre>
</div>
<div>
<h5>Transformed Values</h5>
<pre>
<code>
{JSON.stringify(transformPublishFormToMetadata(values), null, 2)}
</code>
</pre>
</div>
<Output title="Collected Form Values" output={values} />
<Output title="Transformed DDO Values" output={ddo} />
</div>
)
}

View File

@ -4,17 +4,18 @@ import Loader from '../../atoms/Loader'
import React, { ReactElement } from 'react'
import styles from './Feedback.module.css'
import SuccessConfetti from '../../atoms/SuccessConfetti'
import { DDO } from '@oceanprotocol/lib'
export default function Feedback({
error,
success,
did,
ddo,
publishStepText,
setError
}: {
error: string
success: string
did: string
ddo: DDO
publishStepText: string
setError: (error: string) => void
}): ReactElement {
@ -22,7 +23,7 @@ export default function Feedback({
<Button
style="primary"
size="small"
href={`/asset/${did}`}
to={`/asset/${ddo?.id}`}
className={styles.action}
>
Go to data set

View File

@ -1,12 +1,12 @@
import React, { ReactElement, useEffect, FormEvent } from 'react'
import styles from './PublishForm.module.css'
import styles from './FormPublish.module.css'
import { useOcean } from '@oceanprotocol/react'
import { useFormikContext, Field } from 'formik'
import { useFormikContext, Field, Form } from 'formik'
import Input from '../../atoms/Input'
import Button from '../../atoms/Button'
import { FormContent, FormFieldProps } from '../../../@types/Form'
export default function PublishForm({
export default function FormPublish({
content
}: {
content: FormContent
@ -37,7 +37,7 @@ export default function PublishForm({
}
return (
<form
<Form
className={styles.form}
// do we need this?
onChange={() => status === 'empty' && setStatus(null)}
@ -61,6 +61,6 @@ export default function PublishForm({
</Button>
)}
</footer>
</form>
</Form>
)
}

View File

@ -44,11 +44,3 @@
align-items: center;
margin-bottom: calc(var(--spacer) / 2);
}
.price {
min-width: 0;
}
.price:only-child {
margin-right: -100%;
}

View File

@ -7,8 +7,6 @@ import styles from './Preview.module.css'
import File from '../../atoms/File'
import { MetadataPublishForm } from '../../../@types/MetaData'
import Button from '../../atoms/Button'
import Conversion from '../../atoms/Price/Conversion'
import PriceUnit from '../../atoms/Price/PriceUnit'
export default function Preview({
values
@ -60,25 +58,6 @@ export default function Preview({
small
/>
)}
{values.price && (
<div className={styles.price}>
<MetaItem
title={`Price: ${values.price.type}`}
content={
<>
<PriceUnit
price="1"
symbol={values.price.datatoken?.symbol}
small
/>{' '}
= <PriceUnit price={`${values.price.price}`} small />
<Conversion price={`${values.price.price}`} />
</>
}
/>
</div>
)}
</div>
{typeof values.links !== 'string' && values.links?.length && (
@ -107,7 +86,7 @@ export default function Preview({
key.includes('files') ||
key.includes('links') ||
key.includes('termsAndConditions') ||
key.includes('price') ||
key.includes('dataTokenOptions') ||
value === undefined ||
value === ''
)

View File

@ -2,7 +2,7 @@ import React, { ReactElement, useState } from 'react'
import { Formik } from 'formik'
import { usePublish } from '@oceanprotocol/react'
import styles from './index.module.css'
import PublishForm from './PublishForm'
import FormPublish from './FormPublish'
import Web3Feedback from '../../molecules/Wallet/Feedback'
import { FormContent } from '../../../@types/Form'
import { initialValues, validationSchema } from '../../../models/FormPublish'
@ -10,7 +10,7 @@ import { transformPublishFormToMetadata } from './utils'
import Preview from './Preview'
import { MetadataPublishForm } from '../../../@types/MetaData'
import { useUserPreferences } from '../../../providers/UserPreferences'
import { Logger, Metadata } from '@oceanprotocol/lib'
import { DDO, Logger, Metadata } from '@oceanprotocol/lib'
import { Persist } from '../../atoms/FormikPersist'
import Debug from './Debug'
import Feedback from './Feedback'
@ -27,7 +27,7 @@ export default function PublishPage({
const [success, setSuccess] = useState<string>()
const [error, setError] = useState<string>()
const [did, setDid] = useState<string>()
const [ddo, setDdo] = useState<DDO>()
const hasFeedback = isLoading || error || success
@ -36,33 +36,35 @@ export default function PublishPage({
resetForm: () => void
): Promise<void> {
const metadata = transformPublishFormToMetadata(values)
const { price } = values
const serviceType = values.access === 'Download' ? 'access' : 'compute'
try {
Logger.log('Publish with ', price, serviceType, price.datatoken)
Logger.log(
'Publish with ',
metadata,
serviceType,
values.dataTokenOptions
)
const ddo = await publish(
(metadata as unknown) as Metadata,
// swapFee is tricky: to get 0.1% you need to send 0.001 as value
{ ...price, swapFee: `${price.swapFee / 100}` },
serviceType,
price.datatoken
values.dataTokenOptions
)
// Publish failed
if (publishError) {
setError(publishError)
Logger.error(publishError)
if (!ddo || publishError) {
setError(publishError || 'Publishing DDO failed.')
Logger.error(publishError || 'Publishing DDO failed.')
return
}
// Publish succeeded
if (ddo) {
setDid(ddo.id)
setSuccess('🎉 Successfully published your data set. 🎉')
resetForm()
}
setDdo(ddo)
setSuccess(
'🎉 Successfully published. 🎉 Now create a price on your data set.'
)
resetForm()
} catch (error) {
setError(error.message)
Logger.error(error.message)
@ -91,12 +93,12 @@ export default function PublishPage({
error={error}
success={success}
publishStepText={publishStepText}
did={did}
ddo={ddo}
setError={setError}
/>
) : (
<article className={styles.grid}>
<PublishForm content={content.form} />
<FormPublish content={content.form} />
<aside>
<div className={styles.sticky}>
<Preview values={values} />

View File

@ -16,8 +16,7 @@ export function transformPublishFormToMetadata(
tags,
links,
termsAndConditions,
files,
price
files
} = data
const metadata: MetadataMarket = {
@ -36,8 +35,7 @@ export function transformPublishFormToMetadata(
copyrightHolder,
tags: tags?.split(','),
links: typeof links !== 'string' && links,
termsAndConditions,
priceType: price.type
termsAndConditions
}
}

View File

@ -23,13 +23,17 @@ export default function PageTemplateAssetDetails({
useEffect(() => {
async function init() {
if (ddo) return
try {
const metadataCache = new MetadataCache(config.metadataCacheUri, Logger)
const ddo = await metadataCache.retrieveDDO(did)
if (!ddo) {
setTitle('Could not retrieve asset')
setError('The DDO was not found in MetadataCache.')
setError(
`The DDO for ${did} was not found in MetadataCache. If you just published a new data set, wait some seconds and refresh this page.`
)
return
}
@ -44,7 +48,11 @@ export default function PageTemplateAssetDetails({
}
}
init()
}, [did, config.metadataCacheUri])
// Periodically try to get DDO when not present yet
const timer = !ddo && setInterval(() => init(), 2000)
return () => clearInterval(timer)
}, [ddo, did, config.metadataCacheUri])
return did && metadata ? (
<Layout title={title} uri={uri}>

View File

@ -18,8 +18,7 @@ const AssetModel: MetadataMarket = {
links: undefined,
// custom items
termsAndConditions: false,
priceType: undefined
termsAndConditions: false
}
}

24
src/models/FormPricing.ts Normal file
View File

@ -0,0 +1,24 @@
import { PriceOptionsMarket } from '../@types/MetaData'
import * as Yup from 'yup'
export const validationSchema = Yup.object().shape<PriceOptionsMarket>({
price: Yup.number().min(1, 'Must be greater than 0').required('Required'),
dtAmount: Yup.number().min(1, 'Must be greater than 0').required('Required'),
type: Yup.string()
.matches(/fixed|dynamic/g)
.required('Required'),
weightOnDataToken: Yup.string().required('Required'),
swapFee: Yup.number()
.min(0.1, 'Must be more or equal to 0.1')
.max(10, 'Maximum is 10%')
.required('Required')
.nullable()
})
export const initialValues: PriceOptionsMarket = {
price: 1,
type: 'dynamic',
dtAmount: 1,
weightOnDataToken: '9', // 90% on data token
swapFee: 0.1 // in %
}

View File

@ -6,26 +6,10 @@ export const validationSchema = Yup.object().shape<MetadataPublishForm>({
// ---- required fields ----
name: Yup.string().required('Required'),
author: Yup.string().required('Required'),
price: Yup.object()
dataTokenOptions: Yup.object()
.shape({
price: Yup.number().min(1, 'Must be greater than 0').required('Required'),
tokensToMint: Yup.number()
.min(1, 'Must be greater than 0')
.required('Required'),
type: Yup.string()
.matches(/fixed|dynamic/g)
.required('Required'),
weightOnDataToken: Yup.string().required('Required'),
swapFee: Yup.number()
.min(0.1, 'Must be more or equal to 0.1')
.max(0.9, 'Must be less or equal to 0.9')
.required('Required'),
datatoken: Yup.object()
.shape({
name: Yup.string(),
symbol: Yup.string()
})
.nullable()
name: Yup.string(),
symbol: Yup.string()
})
.required('Required'),
files: Yup.array<FileMetadata>().required('Required').nullable(),
@ -45,12 +29,9 @@ export const validationSchema = Yup.object().shape<MetadataPublishForm>({
export const initialValues: Partial<MetadataPublishForm> = {
name: '',
author: '',
price: {
price: 1,
type: 'dynamic',
tokensToMint: 1,
weightOnDataToken: '9', // 90% on data token
swapFee: 0.1 // in %
dataTokenOptions: {
name: '',
symbol: ''
},
files: '',
description: '',

View File

@ -4,12 +4,9 @@ const testFormData: MetadataPublishForm = {
author: '',
files: [],
license: '',
price: {
price: 1,
tokensToMint: 9,
type: 'fixed',
weightOnDataToken: '1',
swapFee: 0.1
dataTokenOptions: {
name: '',
symbol: ''
},
name: '',
description: 'description',

View File

@ -5,7 +5,7 @@ import {
MetadataMarket,
MetadataPublishForm
} from '../../../src/@types/MetaData'
import PublishForm from '../../../src/components/pages/Publish/PublishForm'
import PublishForm from '../../../src/components/pages/Publish/FormPublish'
import publishFormData from '../__fixtures__/testFormData'
import content from '../../../content/pages/publish.json'