1
0
mirror of https://github.com/oceanprotocol/market.git synced 2024-11-15 01:34:57 +01:00

Merge branch 'main' into feature/history-compute

Signed-off-by: mihaisc <mihai.scarlat@smartcontrol.ro>
This commit is contained in:
mihaisc 2020-10-22 12:28:28 +03:00
commit 67cd26258c
No known key found for this signature in database
GPG Key ID: 4FB0C2329B4C6E29
57 changed files with 1641 additions and 1220 deletions

View File

@ -42,9 +42,9 @@
"required": true "required": true
}, },
{ {
"name": "price", "name": "dataTokenOptions",
"label": "Price", "label": "Datatoken",
"type": "price", "type": "datatoken",
"required": true "required": true
}, },
{ {
@ -96,21 +96,5 @@
} }
], ],
"success": "Asset Created!" "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": { "pool": {
"tooltips": { "tooltips": {
"price": "Explain how this price is determined...", "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,13 @@
"@coingecko/cryptoformat": "^0.4.2", "@coingecko/cryptoformat": "^0.4.2",
"@loadable/component": "5.13.1", "@loadable/component": "5.13.1",
"@oceanprotocol/art": "^3.0.0", "@oceanprotocol/art": "^3.0.0",
<<<<<<< HEAD
"@oceanprotocol/lib": "^0.6.7", "@oceanprotocol/lib": "^0.6.7",
"@oceanprotocol/react": "^0.2.2", "@oceanprotocol/react": "^0.2.2",
=======
"@oceanprotocol/lib": "^0.7.1",
"@oceanprotocol/react": "^0.3.2",
>>>>>>> main
"@oceanprotocol/typographies": "^0.1.0", "@oceanprotocol/typographies": "^0.1.0",
"@sindresorhus/slugify": "^1.0.0", "@sindresorhus/slugify": "^1.0.0",
"@tippyjs/react": "^4.2.0", "@tippyjs/react": "^4.2.0",
@ -40,19 +45,19 @@
"ethereum-blockies": "github:MyEtherWallet/blockies", "ethereum-blockies": "github:MyEtherWallet/blockies",
"filesize": "^6.1.0", "filesize": "^6.1.0",
"formik": "^2.2.0", "formik": "^2.2.0",
"gatsby": "^2.24.80", "gatsby": "^2.24.84",
"gatsby-image": "^2.4.21", "gatsby-image": "^2.4.21",
"gatsby-plugin-manifest": "^2.4.35", "gatsby-plugin-manifest": "^2.4.35",
"gatsby-plugin-react-helmet": "^3.3.14", "gatsby-plugin-react-helmet": "^3.3.14",
"gatsby-plugin-remove-trailing-slashes": "^2.3.13", "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-svgr": "^2.0.2",
"gatsby-plugin-webpack-size": "^1.0.0", "gatsby-plugin-webpack-size": "^1.0.0",
"gatsby-source-filesystem": "^2.3.35", "gatsby-source-filesystem": "^2.3.35",
"gatsby-source-graphql": "^2.7.6", "gatsby-source-graphql": "^2.7.6",
"gatsby-transformer-json": "^2.4.14", "gatsby-transformer-json": "^2.4.15",
"gatsby-transformer-remark": "^2.8.42", "gatsby-transformer-remark": "^2.8.45",
"gatsby-transformer-sharp": "^2.5.18", "gatsby-transformer-sharp": "^2.5.19",
"intersection-observer": "^0.11.0", "intersection-observer": "^0.11.0",
"is-url-superb": "^4.0.0", "is-url-superb": "^4.0.0",
"lodash.debounce": "^4.0.8", "lodash.debounce": "^4.0.8",
@ -74,7 +79,7 @@
"remove-markdown": "^0.3.0", "remove-markdown": "^0.3.0",
"shortid": "^2.2.15", "shortid": "^2.2.15",
"slugify": "^1.4.5", "slugify": "^1.4.5",
"swr": "^0.3.5", "swr": "^0.3.6",
"yup": "^0.29.3" "yup": "^0.29.3"
}, },
"devDependencies": { "devDependencies": {
@ -86,11 +91,11 @@
"@svgr/webpack": "^5.4.0", "@svgr/webpack": "^5.4.0",
"@testing-library/jest-dom": "^5.11.4", "@testing-library/jest-dom": "^5.11.4",
"@testing-library/react": "^11.1.0", "@testing-library/react": "^11.1.0",
"@types/jest": "^26.0.14", "@types/jest": "^26.0.15",
"@types/loadable__component": "^5.13.1", "@types/loadable__component": "^5.13.1",
"@types/lodash.debounce": "^4.0.3", "@types/lodash.debounce": "^4.0.3",
"@types/lodash.omit": "^4.5.6", "@types/lodash.omit": "^4.5.6",
"@types/node": "^14.11.10", "@types/node": "^14.14.0",
"@types/react": "^16.9.53", "@types/react": "^16.9.53",
"@types/react-datepicker": "^3.1.1", "@types/react-datepicker": "^3.1.1",
"@types/react-helmet": "^6.1.0", "@types/react-helmet": "^6.1.0",
@ -99,18 +104,18 @@
"@types/remove-markdown": "^0.1.1", "@types/remove-markdown": "^0.1.1",
"@types/shortid": "0.0.29", "@types/shortid": "0.0.29",
"@types/yup": "^0.29.8", "@types/yup": "^0.29.8",
"@typescript-eslint/eslint-plugin": "^4.4.1", "@typescript-eslint/eslint-plugin": "^4.5.0",
"@typescript-eslint/parser": "^4.4.1", "@typescript-eslint/parser": "^4.5.0",
"babel-loader": "^8.1.0", "babel-loader": "^8.1.0",
"babel-preset-react-app": "^9.1.2", "babel-preset-react-app": "^9.1.2",
"eslint": "^7.11.0", "eslint": "^7.11.0",
"eslint-config-oceanprotocol": "^1.5.0", "eslint-config-oceanprotocol": "^1.5.0",
"eslint-config-prettier": "^6.13.0", "eslint-config-prettier": "^6.13.0",
"eslint-plugin-prettier": "^3.1.4", "eslint-plugin-prettier": "^3.1.4",
"eslint-plugin-react": "^7.21.4", "eslint-plugin-react": "^7.21.5",
"eslint-plugin-react-hooks": "^4.1.2", "eslint-plugin-react-hooks": "^4.2.0",
"identity-obj-proxy": "^3.0.0", "identity-obj-proxy": "^3.0.0",
"jest": "^26.5.3", "jest": "^26.6.0",
"prettier": "^2.1.2", "prettier": "^2.1.2",
"serve": "^11.3.2", "serve": "^11.3.2",
"source-map-explorer": "^2.5.0", "source-map-explorer": "^2.5.0",

View File

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

View File

@ -30,6 +30,10 @@
font-size: var(--font-size-small); font-size: var(--font-size-small);
} }
.action {
margin-top: calc(var(--spacer) / 2);
}
/* States */ /* States */
.error { .error {
border-color: var(--rbrand-alert-ed); border-color: var(--rbrand-alert-ed);
@ -43,7 +47,7 @@
.info { .info {
border-color: var(--brand-alert-yellow); border-color: var(--brand-alert-yellow);
color: var(--brand-alert-yellow); color: #9f7e19;
} }
.warning { .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 styles from './Alert.module.css'
import Button from './Button'
export default function Alert({ export default function Alert({
title, title,
text, text,
state state,
action
}: { }: {
title?: string title?: string
text: string text: string
state: 'error' | 'warning' | 'info' | 'success' state: 'error' | 'warning' | 'info' | 'success'
action?: {
name: string
handleAction: (e: FormEvent<HTMLButtonElement>) => void
}
}): ReactElement { }): ReactElement {
return ( return (
<div className={`${styles.alert} ${styles[state]}`}> <div className={`${styles.alert} ${styles[state]}`}>
{title && <h3 className={styles.title}>{title}</h3>} {title && <h3 className={styles.title}>{title}</h3>}
<p className={styles.text}>{text}</p> <p className={styles.text}>{text}</p>
{action && (
<Button
className={styles.action}
size="small"
style="primary"
onClick={action.handleAction}
>
{action.name}
</Button>
)}
</div> </div>
) )
} }

View File

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

View File

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

View File

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

View File

@ -28,7 +28,7 @@ export default function SuccessConfetti({
action action
}: { }: {
success: string success: string
action: ReactNode action?: ReactNode
}): ReactElement { }): ReactElement {
// Have some confetti upon success // Have some confetti upon success
useEffect(() => { 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 { .feedback {
width: 100%; width: 100%;
} }
.hasTokens {
composes: hasTokens from './index.module.css';
}

View File

@ -13,5 +13,5 @@ export default {
} }
export const Default = (): ReactElement => ( 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, computeOptions,
useCompute, useCompute,
readFileContent, readFileContent,
useOcean useOcean,
usePricing
} from '@oceanprotocol/react' } from '@oceanprotocol/react'
import styles from './Compute.module.css' import styles from './Compute.module.css'
import Button from '../../atoms/Button' import Button from '../../atoms/Button'
@ -19,14 +20,19 @@ import { useSiteMetadata } from '../../../hooks/useSiteMetadata'
export default function Compute({ export default function Compute({
ddo, ddo,
isBalanceSufficient isBalanceSufficient,
dtBalance
}: { }: {
ddo: DDO ddo: DDO
isBalanceSufficient: boolean isBalanceSufficient: boolean
dtBalance: string
}): ReactElement { }): ReactElement {
const { marketFeeAddress } = useSiteMetadata()
const { ocean } = useOcean() const { ocean } = useOcean()
const { compute, isLoading, computeStepText, computeError } = useCompute() const { compute, isLoading, computeStepText, computeError } = useCompute()
const { marketFeeAddress } = useSiteMetadata() const { buyDT, dtSymbol } = usePricing(ddo)
const computeService = ddo.findServiceByType('compute') const computeService = ddo.findServiceByType('compute')
const metadataService = ddo.findServiceByType('metadata') const metadataService = ddo.findServiceByType('metadata')
@ -46,6 +52,7 @@ export default function Compute({
computeType === '' || computeType === '' ||
!ocean || !ocean ||
!isBalanceSufficient !isBalanceSufficient
const hasDatatoken = Number(dtBalance) >= 1
const onDrop = async (files: File[]) => { const onDrop = async (files: File[]) => {
setFile(files[0]) setFile(files[0])
@ -70,6 +77,8 @@ export default function Compute({
setIsPublished(false) setIsPublished(false)
setError('') setError('')
!hasDatatoken && (await buyDT('1'))
await compute( await compute(
ddo.id, ddo.id,
computeService, computeService,
@ -97,6 +106,12 @@ export default function Compute({
</div> </div>
<div className={styles.pricewrapper}> <div className={styles.pricewrapper}>
<Price ddo={ddo} conversion /> <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>
</div> </div>
@ -118,7 +133,7 @@ export default function Compute({
onClick={() => startJob()} onClick={() => startJob()}
disabled={isComputeButtonDisabled} disabled={isComputeButtonDisabled}
> >
Start job {hasDatatoken ? 'Start job' : 'Buy'}
</Button> </Button>
</div> </div>

View File

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

View File

@ -15,6 +15,7 @@ export default {
export const PricedAsset = (): ReactElement => ( export const PricedAsset = (): ReactElement => (
<Consume <Consume
ddo={ddo as DDO} ddo={ddo as DDO}
dtBalance="1"
isBalanceSufficient isBalanceSufficient
file={new DDO(ddo).findServiceByType('metadata').attributes.main.files[0]} 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 { 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 Button from '../../atoms/Button'
@ -7,41 +7,56 @@ 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 Loader from '../../atoms/Loader'
import { useOcean, useConsume } from '@oceanprotocol/react' import { useOcean, useConsume, usePricing } from '@oceanprotocol/react'
import { useSiteMetadata } from '../../../hooks/useSiteMetadata' import { useSiteMetadata } from '../../../hooks/useSiteMetadata'
export default function Consume({ export default function Consume({
ddo, ddo,
file, file,
isBalanceSufficient isBalanceSufficient,
dtBalance
}: { }: {
ddo: DDO ddo: DDO
file: FileMetadata file: FileMetadata
isBalanceSufficient: boolean isBalanceSufficient: boolean
dtBalance: string
}): ReactElement { }): ReactElement {
const { ocean } = useOcean() const { ocean } = useOcean()
const { marketFeeAddress } = useSiteMetadata() const { marketFeeAddress } = useSiteMetadata()
const {
dtSymbol,
buyDT,
pricingStepText,
pricingError,
pricingIsLoading
} = usePricing(ddo)
const { consumeStepText, consume, consumeError } = useConsume() const { consumeStepText, consume, consumeError } = useConsume()
const isDisabled = !ocean || !isBalanceSufficient const isDisabled =
!ocean ||
!isBalanceSufficient ||
typeof consumeStepText !== 'undefined' ||
pricingIsLoading
const hasDatatoken = Number(dtBalance) >= 1
if (consumeError) { async function handleConsume() {
toast.error(consumeError) !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 = () => ( const PurchaseButton = () => (
<div> <div className={styles.actions}>
{consumeStepText ? ( {consumeStepText || pricingIsLoading ? (
<Loader message={consumeStepText} /> <Loader message={consumeStepText || pricingStepText} />
) : ( ) : (
<Button <Button style="primary" onClick={handleConsume} disabled={isDisabled}>
style="primary" {hasDatatoken ? 'Download' : 'Buy'}
onClick={() =>
consume(ddo.id, ddo.dataToken, 'access', marketFeeAddress)
}
disabled={isDisabled}
>
Buy
</Button> </Button>
)} )}
</div> </div>
@ -55,6 +70,12 @@ export default function Consume({
</div> </div>
<div className={styles.pricewrapper}> <div className={styles.pricewrapper}>
<Price ddo={ddo} conversion /> <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 /> <PurchaseButton />
</div> </div>
</div> </div>

View File

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

View File

@ -4,3 +4,9 @@
margin: auto; margin: auto;
padding: 0; 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 styles from './index.module.css'
import Compute from './Compute' import Compute from './Compute'
import Consume from './Consume' import Consume from './Consume'
import { DDO } from '@oceanprotocol/lib' import { DDO, Logger } from '@oceanprotocol/lib'
import Tabs from '../../atoms/Tabs' import Tabs from '../../atoms/Tabs'
import { useOcean, useMetadata } from '@oceanprotocol/react' import { useOcean, useMetadata } from '@oceanprotocol/react'
import compareAsBN from '../../../utils/compareAsBN' import compareAsBN from '../../../utils/compareAsBN'
import Pool from './Pool' import Pool from './Pool'
import { AdditionalInformationMarket } from '../../../@types/MetaData'
export default function AssetActions({ ddo }: { ddo: DDO }): ReactElement { export default function AssetActions({ ddo }: { ddo: DDO }): ReactElement {
const { balance } = useOcean() const { ocean, balance, accountId } = useOcean()
const { price } = useMetadata(ddo) const { price } = useMetadata(ddo)
const [isBalanceSufficient, setIsBalanceSufficient] = useState<boolean>() const [isBalanceSufficient, setIsBalanceSufficient] = useState<boolean>()
const [dtBalance, setDtBalance] = useState<string>()
const isCompute = Boolean(ddo.findServiceByType('compute')) const isCompute = Boolean(ddo.findServiceByType('compute'))
const { attributes } = ddo.findServiceByType('metadata') 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 // Check user balance against price
useEffect(() => { 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 () => { return () => {
setIsBalanceSufficient(false) setIsBalanceSufficient(false)
} }
}, [balance, price]) }, [balance, price, dtBalance])
const UseContent = isCompute ? ( const UseContent = isCompute ? (
<Compute ddo={ddo} isBalanceSufficient={isBalanceSufficient} /> <Compute
ddo={ddo}
dtBalance={dtBalance}
isBalanceSufficient={isBalanceSufficient}
/>
) : ( ) : (
<Consume <Consume
ddo={ddo} ddo={ddo}
dtBalance={dtBalance}
isBalanceSufficient={isBalanceSufficient} isBalanceSufficient={isBalanceSufficient}
file={attributes.main.files[0]} 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 // Check from metadata, cause that is available earlier
const hasPool = const hasPool = ddo.price?.type === 'pool'
((attributes.additionalInformation as unknown) as AdditionalInformationMarket)
?.priceType === 'dynamic'
// price?.type === 'pool'
hasPool && hasPool &&
tabs.push({ 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 Time from '../../atoms/Time'
import MetaItem from './MetaItem' import MetaItem from './MetaItem'
import styles from './MetaFull.module.css' import styles from './MetaFull.module.css'
import { MetadataMarket } from '../../../@types/MetaData' import { MetadataMarket } from '../../../@types/MetaData'
import { DDO } from '@oceanprotocol/lib' import { DDO } from '@oceanprotocol/lib'
import EtherscanLink from '../../atoms/EtherscanLink' import EtherscanLink from '../../atoms/EtherscanLink'
import { usePricing } from '@oceanprotocol/react'
import { useOcean } from '@oceanprotocol/react'
export default function MetaFull({ export default function MetaFull({
ddo, ddo,
@ -15,24 +14,9 @@ export default function MetaFull({
ddo: DDO ddo: DDO
metadata: MetadataMarket metadata: MetadataMarket
}): ReactElement { }): ReactElement {
const { ocean } = useOcean()
const { id, dataToken } = ddo const { id, dataToken } = ddo
const { dateCreated, datePublished, author, license } = metadata.main const { dateCreated, datePublished, author, license } = metadata.main
const { dtSymbol, dtName } = usePricing(ddo)
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])
return ( return (
<div className={styles.metaFull}> <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 { .icon {
composes: box from '../../../atoms/Box.module.css'; composes: box from '../../../../atoms/Box.module.css';
padding: calc(var(--spacer) / 1.5); padding: calc(var(--spacer) / 1.5);
width: 6rem; width: 6rem;
height: 6rem; height: 6rem;

View File

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

View File

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

View File

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

View File

@ -1,6 +1,6 @@
import { FieldMetaProps } from 'formik' import { FieldMetaProps } from 'formik'
import React, { ReactElement } from 'react' 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({ export default function Error({
meta meta

View File

@ -1,16 +1,16 @@
.fees { .fees {
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(8rem, 1fr));
margin-left: -2rem; margin-left: -2rem;
margin-right: -2rem; margin-right: -2rem;
border-bottom: 1px solid var(--brand-grey-lighter); border-bottom: 1px solid var(--brand-grey-lighter);
margin-top: var(--spacer); padding: var(--spacer) var(--spacer) calc(var(--spacer) / 2) var(--spacer);
padding: 0 var(--spacer) calc(var(--spacer) / 2) var(--spacer);
justify-content: center; justify-content: center;
text-align: center; text-align: center;
border-bottom: 1px solid var(--brand-grey-lighter); border-bottom: 1px solid var(--brand-grey-lighter);
background: var(--brand-grey-dimmed);
} }
.fees label { .fees label {

View File

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

View File

@ -14,11 +14,16 @@
} }
.grid { .grid {
margin-top: var(--spacer); margin-left: -2rem;
margin-right: -2rem;
padding-top: var(--spacer);
display: grid; display: grid;
gap: var(--spacer); gap: var(--spacer);
grid-template-columns: repeat(auto-fit, minmax(12rem, 1fr)); grid-template-columns: repeat(auto-fit, minmax(12rem, 1fr));
justify-content: center; 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 { .fixed label {
@ -26,14 +31,14 @@
} }
.datatoken { .datatoken {
margin-top: calc(var(--spacer) / 6); margin-top: calc(var(--spacer) / 2);
color: var(--color-secondary); color: var(--color-secondary);
font-size: var(--font-size-small); font-size: var(--font-size-small);
font-weight: var(--font-weight-bold); font-weight: var(--font-weight-bold);
} }
.datatoken h4 { .datatoken h4 {
font-size: var(--font-size-small); font-size: var(--font-size-base);
color: var(--color-secondary); color: var(--color-secondary);
margin: 0; margin: 0;
} }

View File

@ -1,24 +1,23 @@
import React, { ReactElement } from 'react' import React, { ReactElement } from 'react'
import stylesIndex from './index.module.css' import stylesIndex from './index.module.css'
import styles from './Fixed.module.css' import styles from './Fixed.module.css'
import FormHelp from '../../../atoms/Input/Help' import FormHelp from '../../../../atoms/Input/Help'
import Conversion from '../../../atoms/Price/Conversion' import Conversion from '../../../../atoms/Price/Conversion'
import { DataTokenOptions } from '@oceanprotocol/react'
import RefreshName from './RefreshName'
import { useField } from 'formik' import { useField } from 'formik'
import Input from '../../../atoms/Input' import Input from '../../../../atoms/Input'
import Error from './Error' import Error from './Error'
import { DDO } from '@oceanprotocol/lib'
import { usePricing } from '@oceanprotocol/react'
export default function Fixed({ export default function Fixed({
datatokenOptions, ddo,
generateName,
content content
}: { }: {
datatokenOptions: DataTokenOptions ddo: DDO
generateName: () => void
content: any content: any
}): ReactElement { }): ReactElement {
const [field, meta] = useField('price.price') const [field, meta] = useField('price')
const { dtName, dtSymbol } = usePricing(ddo)
return ( return (
<div className={styles.fixed}> <div className={styles.fixed}>
@ -29,7 +28,7 @@ export default function Fixed({
<Input <Input
label="Ocean Token" label="Ocean Token"
value={field.value} value={field.value}
name="price.price" name="price"
type="number" type="number"
prefix="OCEAN" prefix="OCEAN"
min="1" min="1"
@ -43,16 +42,11 @@ export default function Fixed({
/> />
<Error meta={meta} /> <Error meta={meta} />
</div> </div>
<div className={styles.datatoken}>
{datatokenOptions && ( <h4>
<div className={styles.datatoken}> = <strong>1</strong> {dtName} {dtSymbol}
<h4> </h4>
Data Token <RefreshName generateName={generateName} /> </div>
</h4>
<strong>{datatokenOptions?.name}</strong> {' '}
<strong>{datatokenOptions?.symbol}</strong>
</div>
)}
</div> </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 { .content {
padding: 0; padding: 0;
} }
@ -36,3 +30,18 @@
text-align: center; text-align: center;
margin-bottom: calc(var(--spacer) / 1.5); 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; margin-top: -1.5rem;
} }
.grid > div {
overflow: hidden;
}
.content { .content {
composes: box from '../../atoms/Box.module.css'; composes: box from '../../atoms/Box.module.css';
margin-top: var(--spacer); margin-top: var(--spacer);
@ -12,7 +16,7 @@
@media (min-width: 60rem) { @media (min-width: 60rem) {
.grid { .grid {
grid-template-columns: 1.5fr minmax(0, 1fr); grid-template-columns: 1.5fr 1fr;
} }
.sticky { .sticky {

View File

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

View File

@ -3,28 +3,43 @@ import { MetadataPublishForm } from '../../../@types/MetaData'
import styles from './index.module.css' import styles from './index.module.css'
import { transformPublishFormToMetadata } from './utils' 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({ export default function Debug({
values values
}: { }: {
values: Partial<MetadataPublishForm> values: Partial<MetadataPublishForm>
}): ReactElement { }): 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 ( return (
<div className={styles.grid}> <div className={styles.grid}>
<div> <Output title="Collected Form Values" output={values} />
<h5>Collected Form Values</h5> <Output title="Transformed DDO Values" output={ddo} />
<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>
</div> </div>
) )
} }

View File

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

View File

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

View File

@ -44,11 +44,3 @@
align-items: center; align-items: center;
margin-bottom: calc(var(--spacer) / 2); 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 File from '../../atoms/File'
import { MetadataPublishForm } from '../../../@types/MetaData' import { MetadataPublishForm } from '../../../@types/MetaData'
import Button from '../../atoms/Button' import Button from '../../atoms/Button'
import Conversion from '../../atoms/Price/Conversion'
import PriceUnit from '../../atoms/Price/PriceUnit'
export default function Preview({ export default function Preview({
values values
@ -60,25 +58,6 @@ export default function Preview({
small 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> </div>
{typeof values.links !== 'string' && values.links?.length && ( {typeof values.links !== 'string' && values.links?.length && (
@ -107,7 +86,7 @@ export default function Preview({
key.includes('files') || key.includes('files') ||
key.includes('links') || key.includes('links') ||
key.includes('termsAndConditions') || key.includes('termsAndConditions') ||
key.includes('price') || key.includes('dataTokenOptions') ||
value === undefined || value === undefined ||
value === '' value === ''
) )

View File

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

View File

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

View File

@ -23,13 +23,17 @@ export default function PageTemplateAssetDetails({
useEffect(() => { useEffect(() => {
async function init() { async function init() {
if (ddo) return
try { try {
const metadataCache = new MetadataCache(config.metadataCacheUri, Logger) const metadataCache = new MetadataCache(config.metadataCacheUri, Logger)
const ddo = await metadataCache.retrieveDDO(did) const ddo = await metadataCache.retrieveDDO(did)
if (!ddo) { if (!ddo) {
setTitle('Could not retrieve asset') 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 return
} }
@ -44,7 +48,11 @@ export default function PageTemplateAssetDetails({
} }
} }
init() 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 ? ( return did && metadata ? (
<Layout title={title} uri={uri}> <Layout title={title} uri={uri}>

View File

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

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 ---- // ---- required fields ----
name: Yup.string().required('Required'), name: Yup.string().required('Required'),
author: Yup.string().required('Required'), author: Yup.string().required('Required'),
price: Yup.object() dataTokenOptions: Yup.object()
.shape({ .shape({
price: Yup.number().min(1, 'Must be greater than 0').required('Required'), name: Yup.string(),
tokensToMint: Yup.number() symbol: Yup.string()
.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()
}) })
.required('Required'), .required('Required'),
files: Yup.array<FileMetadata>().required('Required').nullable(), files: Yup.array<FileMetadata>().required('Required').nullable(),
@ -45,12 +29,9 @@ export const validationSchema = Yup.object().shape<MetadataPublishForm>({
export const initialValues: Partial<MetadataPublishForm> = { export const initialValues: Partial<MetadataPublishForm> = {
name: '', name: '',
author: '', author: '',
price: { dataTokenOptions: {
price: 1, name: '',
type: 'dynamic', symbol: ''
tokensToMint: 1,
weightOnDataToken: '9', // 90% on data token
swapFee: 0.1 // in %
}, },
files: '', files: '',
description: '', description: '',

View File

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

View File

@ -5,7 +5,7 @@ import {
MetadataMarket, MetadataMarket,
MetadataPublishForm MetadataPublishForm
} from '../../../src/@types/MetaData' } 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 publishFormData from '../__fixtures__/testFormData'
import content from '../../../content/pages/publish.json' import content from '../../../content/pages/publish.json'