1
0
mirror of https://github.com/oceanprotocol/market.git synced 2024-09-28 03:58:59 +02:00

Merge pull request #81 from oceanprotocol/feature/fees

Fees input and output
This commit is contained in:
Matthias Kretschmann 2020-09-22 13:36:19 +02:00 committed by GitHub
commit 8de48d88e6
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
25 changed files with 308 additions and 156 deletions

View File

@ -100,14 +100,16 @@
"price": { "price": {
"fixed": { "fixed": {
"title": "Fixed", "title": "Fixed",
"info": "Set your price for accessing this data set. A Data Token for this data set, worth the entered amount of OCEAN, will be created." "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": { "dynamic": {
"title": "Dynamic", "title": "Dynamic",
"info": "Let's create a decentralized, automated market for your data set. A Data Token for this data set, worth the entered amount of OCEAN, will be created. Additionally, you will provide liquidity into a Data Token/OCEAN liquidity pool with Balancer.", "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": { "tooltips": {
"poolInfo": "Help me", "poolInfo": "Explain what is going on here...",
"liquidityProviderFee": "Help me" "liquidityProviderFee": "Explain liquidity provider fee...",
"communityFee": "Explain community fee...",
"marketplaceFee": "Explain marketplace fee..."
} }
} }
} }

View File

@ -4,7 +4,7 @@ import {
AdditionalInformation, AdditionalInformation,
ServiceMetadata ServiceMetadata
} from '@oceanprotocol/lib' } from '@oceanprotocol/lib'
import { PriceOptions } from '@oceanprotocol/react' import { PriceOptions, DataTokenOptions } from '@oceanprotocol/react'
export interface AdditionalInformationMarket extends AdditionalInformation { export interface AdditionalInformationMarket extends AdditionalInformation {
links?: File[] links?: File[]
@ -13,7 +13,17 @@ export interface AdditionalInformationMarket extends AdditionalInformation {
} }
export interface MetadataMarket extends Metadata { export interface MetadataMarket extends Metadata {
additionalInformation: AdditionalInformationMarket // While required for this market, Aquarius/Plecos will keep this as optional
// allowing external pushes of assets without `additionalInformation`.
// Making it optional here helps safeguarding against those assets.
additionalInformation?: AdditionalInformationMarket
}
export interface PriceOptionsMarket extends PriceOptions {
// easier to keep this as number for Yup input validation
liquidityProviderFee: number
// collect datatoken info for publishing
datatoken?: DataTokenOptions
} }
export interface MetadataPublishForm { export interface MetadataPublishForm {
@ -23,7 +33,7 @@ export interface MetadataPublishForm {
files: string | File[] files: string | File[]
author: string author: string
license: string license: string
price: PriceOptions price: PriceOptionsMarket
access: 'Download' | 'Compute' | string access: 'Download' | 'Compute' | string
termsAndConditions: boolean termsAndConditions: boolean
// ---- optional fields ---- // ---- optional fields ----

View File

@ -38,6 +38,15 @@
color: var(--brand-grey-light); color: var(--brand-grey-light);
cursor: not-allowed; cursor: not-allowed;
pointer-events: none; pointer-events: none;
/* for hiding spin buttons in Firefox */
-moz-appearance: textfield;
}
.input[readonly]::-webkit-inner-spin-button,
.input[disabled]::-webkit-inner-spin-button,
.input[readonly]::-webkit-outer-spin-button,
.input[disabled]::-webkit-outer-spin-button {
display: none;
} }
.select { .select {
@ -207,6 +216,12 @@
font-size: var(--font-size-small); font-size: var(--font-size-small);
} }
.prefix.small,
.postfix.small {
min-height: 34px;
font-size: var(--font-size-mini);
}
.selectSmall { .selectSmall {
composes: small; composes: small;
height: 34px; height: 34px;

View File

@ -6,8 +6,12 @@ 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 Price from '../../molecules/FormFields/Price'
const DefaultInput = (props: InputProps) => ( const DefaultInput = ({ small, ...props }: InputProps) => (
<input className={styles.input} id={props.name} {...props} /> <input
className={`${styles.input} ${small ? styles.small : null}`}
id={props.name}
{...props}
/>
) )
export default function InputElement({ export default function InputElement({
@ -18,6 +22,10 @@ export default function InputElement({
postfix, postfix,
small, small,
field, field,
label,
help,
form,
additionalComponent,
...props ...props
}: InputProps): ReactElement { }: InputProps): ReactElement {
switch (type) { switch (type) {
@ -75,12 +83,30 @@ export default function InputElement({
default: default:
return prefix || postfix ? ( return prefix || postfix ? (
<div className={`${prefix ? styles.prefixGroup : styles.postfixGroup}`}> <div className={`${prefix ? styles.prefixGroup : styles.postfixGroup}`}>
{prefix && <div className={styles.prefix}>{prefix}</div>} {prefix && (
<DefaultInput name={name} type={type || 'text'} {...props} /> <div className={`${styles.prefix} ${small ? styles.small : ''}`}>
{postfix && <div className={styles.postfix}>{postfix}</div>} {prefix}
</div>
)}
<DefaultInput
name={name}
type={type || 'text'}
small={small}
{...props}
/>
{postfix && (
<div className={`${styles.postfix} ${small ? styles.small : ''}`}>
{postfix}
</div>
)}
</div> </div>
) : ( ) : (
<DefaultInput name={name} type={type || 'text'} {...props} /> <DefaultInput
name={name}
type={type || 'text'}
small={small}
{...props}
/>
) )
} }
} }

View File

@ -10,7 +10,7 @@ const cx = classNames.bind(styles)
export interface InputProps { export interface InputProps {
name: string name: string
label?: string label?: string | ReactNode
placeholder?: string placeholder?: string
required?: boolean required?: boolean
help?: string help?: string
@ -30,6 +30,7 @@ export interface InputProps {
multiple?: boolean multiple?: boolean
pattern?: string pattern?: string
min?: string min?: string
max?: string
disabled?: boolean disabled?: boolean
readOnly?: boolean readOnly?: boolean
field?: any field?: any
@ -53,8 +54,7 @@ export default function Input(props: Partial<InputProps>): ReactElement {
} = props } = props
const hasError = const hasError =
props.form?.touched[field.name] && props.form?.touched[field.name] && props.form?.errors[field.name]
typeof props.form.errors[field.name] === 'string'
const styleClasses = cx({ const styleClasses = cx({
field: true, field: true,
@ -71,7 +71,7 @@ export default function Input(props: Partial<InputProps>): ReactElement {
</Label> </Label>
<InputElement small={small} {...field} {...props} /> <InputElement small={small} {...field} {...props} />
{field && ( {field && field.name !== 'price' && (
<div className={styles.error}> <div className={styles.error}>
<ErrorMessage name={field.name} /> <ErrorMessage name={field.name} />
</div> </div>

View File

@ -6,12 +6,8 @@ import FileInput from './Input'
import { getFileInfo } from '../../../../utils' import { getFileInfo } from '../../../../utils'
import { InputProps } from '../../../atoms/Input' import { InputProps } from '../../../atoms/Input'
interface Values {
url: string
}
export default function FilesInput(props: InputProps): ReactElement { export default function FilesInput(props: InputProps): ReactElement {
const [field, meta, helpers] = useField(props) const [field, meta, helpers] = useField(props.name)
const [isLoading, setIsLoading] = useState(false) const [isLoading, setIsLoading] = useState(false)
async function handleButtonClick(e: React.SyntheticEvent, url: string) { async function handleButtonClick(e: React.SyntheticEvent, url: string) {
@ -36,7 +32,7 @@ export default function FilesInput(props: InputProps): ReactElement {
return ( return (
<> <>
{field && typeof field.value === 'object' ? ( {field?.value && field.value[0] && typeof field.value === 'object' ? (
<FileInfo file={field.value[0]} removeItem={removeItem} /> <FileInfo file={field.value[0]} removeItem={removeItem} />
) : ( ) : (
<FileInput <FileInput

View File

@ -1,4 +1,4 @@
import React, { ReactElement, ChangeEvent } 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'
@ -6,24 +6,24 @@ 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 RefreshName from './RefreshName'
import { useField } from 'formik'
import Error from './Error'
export default function Coin({ export default function Coin({
datatokenOptions, datatokenOptions,
name, name,
value,
weight, weight,
onOceanChange,
generateName, generateName,
readOnly readOnly
}: { }: {
datatokenOptions: DataTokenOptions datatokenOptions: DataTokenOptions
name: string name: string
value: string
weight: string weight: string
onOceanChange?: (event: ChangeEvent<HTMLInputElement>) => void
generateName?: () => void generateName?: () => void
readOnly?: boolean readOnly?: boolean
}): ReactElement { }): ReactElement {
const [field, meta] = useField(name)
return ( return (
<div className={styles.coin}> <div className={styles.coin}>
<figure className={styles.icon}> <figure className={styles.icon}>
@ -43,16 +43,16 @@ export default function Coin({
<div className={styles.data}> <div className={styles.data}>
<InputElement <InputElement
value={value}
name={name}
type="number" type="number"
onChange={onOceanChange}
readOnly={readOnly} readOnly={readOnly}
prefix={datatokenOptions?.symbol || 'DT'} prefix={datatokenOptions?.symbol || 'DT'}
min="1"
{...field}
/> />
{datatokenOptions?.symbol === 'OCEAN' && ( {datatokenOptions?.symbol === 'OCEAN' && (
<Conversion price={value} className={stylesIndex.conversion} /> <Conversion price={field.value} className={stylesIndex.conversion} />
)} )}
<Error meta={meta} />
</div> </div>
</div> </div>
) )

View File

@ -49,14 +49,9 @@
} }
.summary { .summary {
text-align: center;
margin-top: var(--spacer); margin-top: var(--spacer);
} }
.summary input {
max-width: 5rem;
}
.alertArea { .alertArea {
margin-left: -2rem; margin-left: -2rem;
margin-right: -2rem; margin-right: -2rem;

View File

@ -1,35 +1,33 @@
import React, { ReactElement, useState, ChangeEvent, useEffect } from 'react' import React, { ReactElement, useState, useEffect } from 'react'
import stylesIndex from './index.module.css' import stylesIndex from './index.module.css'
import styles from './Dynamic.module.css' import styles from './Dynamic.module.css'
import FormHelp from '../../../atoms/Input/Help' import FormHelp from '../../../atoms/Input/Help'
import Wallet from '../../Wallet' import Wallet from '../../Wallet'
import { DataTokenOptions, PriceOptions, useOcean } from '@oceanprotocol/react' import { DataTokenOptions, useOcean } from '@oceanprotocol/react'
import Alert from '../../../atoms/Alert' import Alert from '../../../atoms/Alert'
import Coin from './Coin' import Coin from './Coin'
import { isCorrectNetwork } from '../../../../utils/wallet' import { isCorrectNetwork } from '../../../../utils/wallet'
import { useSiteMetadata } from '../../../../hooks/useSiteMetadata' import { useSiteMetadata } from '../../../../hooks/useSiteMetadata'
import InputElement from '../../../atoms/Input/InputElement'
import Label from '../../../atoms/Input/Label'
import Tooltip from '../../../atoms/Tooltip' import Tooltip from '../../../atoms/Tooltip'
import Fees from './Fees'
import { PriceOptionsMarket } from '../../../../@types/MetaData'
export default function Dynamic({ export default function Dynamic({
ocean, ocean,
priceOptions, priceOptions,
datatokenOptions, datatokenOptions,
onOceanChange,
generateName, generateName,
content content
}: { }: {
ocean: string ocean: number
priceOptions: PriceOptions priceOptions: PriceOptionsMarket
datatokenOptions: DataTokenOptions datatokenOptions: DataTokenOptions
onOceanChange: (event: ChangeEvent<HTMLInputElement>) => void
generateName: () => void generateName: () => void
content: any content: any
}): ReactElement { }): ReactElement {
const { appConfig } = useSiteMetadata() const { appConfig } = useSiteMetadata()
const { account, balance, chainId, refreshBalance } = useOcean() const { account, balance, chainId, refreshBalance } = useOcean()
const { weightOnDataToken, tokensToMint, liquidityProviderFee } = priceOptions const { weightOnDataToken } = priceOptions
const [error, setError] = useState<string>() const [error, setError] = useState<string>()
const correctNetwork = isCorrectNetwork(chainId) const correctNetwork = isCorrectNetwork(chainId)
@ -37,14 +35,14 @@ export default function Dynamic({
c.toUpperCase() c.toUpperCase()
) )
// Check: account, network & insuffciant balance // Check: account, network & insufficient balance
useEffect(() => { useEffect(() => {
if (!account) { if (!account) {
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(ocean)) {
setError(`Insufficiant balance. You need at least ${ocean} OCEAN`) setError(`Insufficient balance. You need at least ${ocean} OCEAN`)
} else { } else {
setError(undefined) setError(undefined)
} }
@ -76,39 +74,29 @@ export default function Dynamic({
</aside> </aside>
<h4 className={styles.title}> <h4 className={styles.title}>
Data Token Liquidity Pool{' '} Datatoken Liquidity Pool <Tooltip content={content.tooltips.poolInfo} />
<Tooltip content={content.tooltips.poolInfo} />
</h4> </h4>
<div className={styles.tokens}> <div className={styles.tokens}>
<Coin <Coin
name="ocean" name="price.price"
datatokenOptions={{ symbol: 'OCEAN', name: 'Ocean Token' }} datatokenOptions={{ symbol: 'OCEAN', name: 'Ocean Token' }}
value={ocean}
weight={`${100 - Number(Number(weightOnDataToken) * 10)}%`} weight={`${100 - Number(Number(weightOnDataToken) * 10)}%`}
onOceanChange={onOceanChange}
/> />
<Coin <Coin
name="tokensToMint" name="price.tokensToMint"
datatokenOptions={datatokenOptions} datatokenOptions={datatokenOptions}
value={tokensToMint.toString()}
weight={`${Number(weightOnDataToken) * 10}%`} weight={`${Number(weightOnDataToken) * 10}%`}
generateName={generateName} generateName={generateName}
readOnly readOnly
/> />
</div> </div>
<Fees tooltips={content.tooltips} />
<footer className={styles.summary}> <footer className={styles.summary}>
<Label htmlFor="liquidityProviderFee"> You will get: <br />
Liquidity Provider Fee{' '} 100% share of pool
<Tooltip content={content.tooltips.liquidityProviderFee} />
</Label>
<InputElement
value={liquidityProviderFee}
name="liquidityProviderFee"
readOnly
postfix="%"
/>
</footer> </footer>
{error && ( {error && (

View File

@ -0,0 +1,13 @@
import { FieldMetaProps } from 'formik'
import React, { ReactElement } from 'react'
import stylesInput from '../../../atoms/Input/index.module.css'
export default function Error({
meta
}: {
meta: FieldMetaProps<any>
}): ReactElement {
return meta.error ? (
<div className={stylesInput.error}>{meta.error}</div>
) : null
}

View File

@ -0,0 +1,22 @@
.fees {
display: grid;
gap: var(--spacer);
grid-template-columns: repeat(auto-fit, minmax(10rem, 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);
justify-content: center;
text-align: center;
border-bottom: 1px solid var(--brand-grey-lighter);
}
.fees label {
white-space: nowrap;
}
.fees input {
max-width: 5rem;
}

View File

@ -0,0 +1,67 @@
import React, { ReactElement } from 'react'
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 Error from './Error'
export default function Fees({
tooltips
}: {
tooltips: { [key: string]: string }
}): ReactElement {
const { appConfig } = useSiteMetadata()
const [field, meta] = useField('price.liquidityProviderFee')
return (
<>
<div className={styles.fees}>
<Input
label={
<>
Liquidity Provider Fee
<Tooltip content={tooltips.liquidityProviderFee} />
</>
}
type="number"
postfix="%"
min="0.1"
max="0.9"
step="0.1"
small
{...field}
additionalComponent={<Error meta={meta} />}
/>
<Input
label={
<>
Community Fee
<Tooltip content={tooltips.communityFee} />
</>
}
value="0.1"
name="communityFee"
postfix="%"
readOnly
small
/>
<Input
label={
<>
Marketplace Fee
<Tooltip content={tooltips.marketplaceFee} />
</>
}
value={appConfig.marketFeeAmount}
name="marketplaceFee"
postfix="%"
readOnly
small
/>
</div>
</>
)
}

View File

@ -2,6 +2,10 @@
composes: content from './index.module.css'; composes: content from './index.module.css';
} }
.form {
position: relative;
}
@media (min-width: 55rem) { @media (min-width: 55rem) {
.form { .form {
max-width: 12rem; max-width: 12rem;

View File

@ -1,42 +1,49 @@
import React, { ReactElement, ChangeEvent } 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 Label from '../../../atoms/Input/Label'
import InputElement from '../../../atoms/Input/InputElement'
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 RefreshName from './RefreshName'
import { useField } from 'formik'
import Input from '../../../atoms/Input'
import Error from './Error'
export default function Fixed({ export default function Fixed({
ocean,
datatokenOptions, datatokenOptions,
onChange,
generateName, generateName,
content content
}: { }: {
ocean: string
datatokenOptions: DataTokenOptions datatokenOptions: DataTokenOptions
onChange: (event: ChangeEvent<HTMLInputElement>) => void
generateName: () => void generateName: () => void
content: any content: any
}): ReactElement { }): ReactElement {
const [field, meta] = useField('price.price')
return ( return (
<div className={styles.fixed}> <div className={styles.fixed}>
<FormHelp className={stylesIndex.help}>{content.info}</FormHelp> <FormHelp className={stylesIndex.help}>{content.info}</FormHelp>
<div className={styles.grid}> <div className={styles.grid}>
<div className={styles.form}> <div className={styles.form}>
<Label htmlFor="ocean">Ocean Token</Label> <Input
<InputElement label="Ocean Token"
value={ocean} value={field.value}
name="ocean" name="price.price"
type="number" type="number"
prefix="OCEAN" prefix="OCEAN"
onChange={onChange} min="1"
{...field}
additionalComponent={
<Conversion
price={field.value}
className={stylesIndex.conversion}
/>
}
/> />
<Conversion price={ocean} className={stylesIndex.conversion} /> <Error meta={meta} />
</div> </div>
{datatokenOptions && ( {datatokenOptions && (
<div className={styles.datatoken}> <div className={styles.datatoken}>
<h4> <h4>

View File

@ -20,6 +20,11 @@
margin-bottom: 0; margin-bottom: 0;
} }
.content [class*='error'] {
text-align: left;
top: 100%;
}
.conversion { .conversion {
width: 100%; width: 100%;
display: block; display: block;

View File

@ -1,4 +1,4 @@
import React, { ReactElement, useState, ChangeEvent, useEffect } from 'react' import React, { ReactElement, useState, useEffect } from 'react'
import { graphql, useStaticQuery } from 'gatsby' import { graphql, useStaticQuery } from 'gatsby'
import { InputProps } from '../../../atoms/Input' import { InputProps } from '../../../atoms/Input'
import styles from './index.module.css' import styles from './index.module.css'
@ -7,7 +7,8 @@ import Fixed from './Fixed'
import Dynamic from './Dynamic' import Dynamic from './Dynamic'
import { useField } from 'formik' import { useField } from 'formik'
import { useUserPreferences } from '../../../../providers/UserPreferences' import { useUserPreferences } from '../../../../providers/UserPreferences'
import { DataTokenOptions, PriceOptions, useOcean } from '@oceanprotocol/react' import { useOcean } from '@oceanprotocol/react'
import { PriceOptionsMarket } from '../../../../@types/MetaData'
const query = graphql` const query = graphql`
query PriceFieldQuery { query PriceFieldQuery {
@ -26,6 +27,8 @@ const query = graphql`
tooltips { tooltips {
poolInfo poolInfo
liquidityProviderFee liquidityProviderFee
communityFee
marketplaceFee
} }
} }
} }
@ -42,17 +45,10 @@ export default function Price(props: InputProps): ReactElement {
const content = data.content.edges[0].node.childPagesJson.price const content = data.content.edges[0].node.childPagesJson.price
const { ocean } = useOcean() const { ocean } = useOcean()
const [field, meta, helpers] = useField(props) const [field, meta, helpers] = useField(props.name)
const priceOptions: PriceOptions = field.value const { price }: PriceOptionsMarket = field.value
const [amountOcean, setAmountOcean] = useState('1')
const [tokensToMint, setTokensToMint] = useState<number>() const [tokensToMint, setTokensToMint] = useState<number>()
const [datatokenOptions, setDatatokenOptions] = useState<DataTokenOptions>()
function handleOceanChange(event: ChangeEvent<HTMLInputElement>) {
setAmountOcean(event.target.value)
helpers.setValue({ ...field.value, price: event.target.value })
}
function handleTabChange(tabName: string) { function handleTabChange(tabName: string) {
const type = tabName.toLowerCase() const type = tabName.toLowerCase()
@ -61,17 +57,16 @@ export default function Price(props: InputProps): ReactElement {
function generateName() { function generateName() {
if (!ocean) return if (!ocean) return
const newDatatokenOptions = ocean.datatokens.generateDtName() const datatoken = ocean.datatokens.generateDtName()
setDatatokenOptions(newDatatokenOptions) helpers.setValue({ ...field.value, datatoken })
} }
// Always update everything when amountOcean changes // Always update everything when amountOcean changes
useEffect(() => { useEffect(() => {
const tokensToMint = const tokensToMint = Number(price) * Number(field.value.weightOnDataToken)
Number(amountOcean) * Number(priceOptions.weightOnDataToken)
setTokensToMint(tokensToMint) setTokensToMint(tokensToMint)
helpers.setValue({ ...field.value, tokensToMint }) helpers.setValue({ ...field.value, tokensToMint })
}, [amountOcean]) }, [price])
// Generate new DT name & symbol // Generate new DT name & symbol
useEffect(() => { useEffect(() => {
@ -83,9 +78,7 @@ export default function Price(props: InputProps): ReactElement {
title: content.fixed.title, title: content.fixed.title,
content: ( content: (
<Fixed <Fixed
ocean={amountOcean} datatokenOptions={field.value.datatoken}
datatokenOptions={datatokenOptions}
onChange={handleOceanChange}
generateName={generateName} generateName={generateName}
content={content.fixed} content={content.fixed}
/> />
@ -95,10 +88,9 @@ export default function Price(props: InputProps): ReactElement {
title: content.dynamic.title, title: content.dynamic.title,
content: ( content: (
<Dynamic <Dynamic
ocean={amountOcean} ocean={price}
priceOptions={{ ...priceOptions, tokensToMint }} priceOptions={{ ...field.value, tokensToMint }}
datatokenOptions={datatokenOptions} datatokenOptions={field.value.datatoken}
onOceanChange={handleOceanChange}
generateName={generateName} generateName={generateName}
content={content.dynamic} content={content.dynamic}
/> />
@ -111,7 +103,7 @@ export default function Price(props: InputProps): ReactElement {
<Tabs items={tabs} handleTabChange={handleTabChange} /> <Tabs items={tabs} handleTabChange={handleTabChange} />
{debug === true && ( {debug === true && (
<pre> <pre>
<code>{JSON.stringify(field.value)}</code> <code>{JSON.stringify(field.value, null, 2)}</code>
</pre> </pre>
)} )}
</div> </div>

View File

@ -16,17 +16,18 @@ export default function Add({
setShowAdd, setShowAdd,
poolAddress, poolAddress,
totalPoolTokens, totalPoolTokens,
totalBalance totalBalance,
liquidityProviderFee
}: { }: {
setShowAdd: (show: boolean) => void setShowAdd: (show: boolean) => void
poolAddress: string poolAddress: string
totalPoolTokens: string totalPoolTokens: string
totalBalance: Balance totalBalance: Balance
liquidityProviderFee: string
}): ReactElement { }): ReactElement {
const { debug } = useUserPreferences() const { debug } = useUserPreferences()
const { ocean, accountId, balance } = useOcean() const { ocean, accountId, balance } = useOcean()
const [amount, setAmount] = useState('') const [amount, setAmount] = useState('')
const [swapFee, setSwapFee] = useState<string>()
const [txId, setTxId] = useState<string>('') const [txId, setTxId] = useState<string>('')
const [isLoading, setIsLoading] = useState<boolean>() const [isLoading, setIsLoading] = useState<boolean>()
@ -38,14 +39,6 @@ export default function Add({
totalBalance && totalBalance &&
((Number(newPoolTokens) / Number(totalPoolTokens)) * 100).toFixed(2) ((Number(newPoolTokens) / Number(totalPoolTokens)) * 100).toFixed(2)
useEffect(() => {
async function getFee() {
const swapFee = await ocean.pool.getSwapFee(accountId, poolAddress)
setSwapFee(swapFee)
}
getFee()
}, [])
async function handleAddLiquidity() { async function handleAddLiquidity() {
setIsLoading(true) setIsLoading(true)
@ -96,7 +89,7 @@ export default function Add({
</div> </div>
<div> <div>
<p>You will earn</p> <p>You will earn</p>
<Token symbol="%" balance={swapFee} /> <Token symbol="%" balance={liquidityProviderFee} />
of each pool transaction of each pool transaction
</div> </div>
</div> </div>

View File

@ -33,6 +33,7 @@ export default function Pool({ ddo }: { ddo: DDO }): ReactElement {
const [totalBalance, setTotalBalance] = useState<Balance>() const [totalBalance, setTotalBalance] = useState<Balance>()
const [dtSymbol, setDtSymbol] = useState<string>() const [dtSymbol, setDtSymbol] = useState<string>()
const [userBalance, setUserBalance] = useState<Balance>() const [userBalance, setUserBalance] = useState<Balance>()
const [liquidityProviderFee, setLiquidityProviderFee] = useState<string>()
const [showAdd, setShowAdd] = useState(false) const [showAdd, setShowAdd] = useState(false)
const [showRemove, setShowRemove] = useState(false) const [showRemove, setShowRemove] = useState(false)
@ -103,6 +104,13 @@ export default function Pool({ ddo }: { ddo: DDO }): ReactElement {
} }
setUserBalance(userBalance) setUserBalance(userBalance)
// Get liquidity provider fee
const liquidityProviderFee = await ocean.pool.getSwapFee(
accountId,
price.address
)
setLiquidityProviderFee(liquidityProviderFee)
} catch (error) { } catch (error) {
console.error(error.message) console.error(error.message)
} finally { } finally {
@ -122,6 +130,7 @@ export default function Pool({ ddo }: { ddo: DDO }): ReactElement {
poolAddress={price.address} poolAddress={price.address}
totalPoolTokens={totalPoolTokens} totalPoolTokens={totalPoolTokens}
totalBalance={totalBalance} totalBalance={totalBalance}
liquidityProviderFee={liquidityProviderFee}
/> />
) : showRemove ? ( ) : showRemove ? (
<Remove <Remove
@ -144,7 +153,7 @@ export default function Pool({ ddo }: { ddo: DDO }): ReactElement {
Pool Pool
</EtherscanLink> </EtherscanLink>
<EtherscanLink network="rinkeby" path={`token/${ddo.dataToken}`}> <EtherscanLink network="rinkeby" path={`token/${ddo.dataToken}`}>
Data Token Datatoken
</EtherscanLink> </EtherscanLink>
</div> </div>
</div> </div>
@ -168,6 +177,10 @@ export default function Pool({ ddo }: { ddo: DDO }): ReactElement {
{debug === true && ( {debug === true && (
<Token symbol="BPT" balance={totalPoolTokens} /> <Token symbol="BPT" balance={totalPoolTokens} />
)} )}
<Token
symbol="% liquidity provider fee"
balance={liquidityProviderFee}
/>
</div> </div>
</div> </div>

View File

@ -56,7 +56,7 @@ export default function MetaFull({
/> />
<MetaItem <MetaItem
title="Data Token" title="Datatoken"
content={ content={
<EtherscanLink network="rinkeby" path={`token/${dataToken}`}> <EtherscanLink network="rinkeby" path={`token/${dataToken}`}>
{dtName ? `${dtName} - ${dtSymbol}` : <code>{dataToken}</code>} {dtName ? `${dtName} - ${dtSymbol}` : <code>{dataToken}</code>}

View File

@ -10,7 +10,6 @@ export default function MetaSecondary({
}: { }: {
metadata: MetadataMarket metadata: MetadataMarket
}): ReactElement { }): ReactElement {
console.log(metadata)
return ( return (
<aside className={styles.metaSecondary}> <aside className={styles.metaSecondary}>
{metadata?.additionalInformation?.tags?.length > 0 && ( {metadata?.additionalInformation?.tags?.length > 0 && (

View File

@ -11,7 +11,7 @@ import Button from '../../atoms/Button'
export default function Preview({ export default function Preview({
values values
}: { }: {
values: MetadataPublishForm values: Partial<MetadataPublishForm>
}): ReactElement { }): ReactElement {
return ( return (
<div className={styles.preview}> <div className={styles.preview}>

View File

@ -2,7 +2,7 @@ import React, { ReactElement } from 'react'
import { useNavigate } from '@reach/router' import { useNavigate } from '@reach/router'
import { toast } from 'react-toastify' import { toast } from 'react-toastify'
import { Formik } from 'formik' import { Formik } from 'formik'
import { usePublish, DataTokenOptions } 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 PublishForm from './PublishForm'
import Web3Feedback from '../../molecules/Wallet/Feedback' import Web3Feedback from '../../molecules/Wallet/Feedback'
@ -11,7 +11,6 @@ import { initialValues, validationSchema } from '../../../models/FormPublish'
import { transformPublishFormToMetadata } from './utils' import { transformPublishFormToMetadata } from './utils'
import Preview from './Preview' import Preview from './Preview'
import { MetadataPublishForm } from '../../../@types/MetaData' import { MetadataPublishForm } from '../../../@types/MetaData'
// import { useSiteMetadata } from '../../../hooks/useSiteMetadata'
import { useUserPreferences } from '../../../providers/UserPreferences' import { useUserPreferences } from '../../../providers/UserPreferences'
import { Logger } from '@oceanprotocol/lib' import { Logger } from '@oceanprotocol/lib'
@ -20,44 +19,38 @@ export default function PublishPage({
}: { }: {
content: { form: FormContent } content: { form: FormContent }
}): ReactElement { }): ReactElement {
// TODO: implement marketFee
// const { marketFeeAddress, marketFeeAmount } = useSiteMetadata()
const { debug } = useUserPreferences() const { debug } = useUserPreferences()
const { publish, publishError, isLoading, publishStepText } = usePublish() const { publish, publishError, isLoading, publishStepText } = usePublish()
const navigate = useNavigate() const navigate = useNavigate()
async function handleSubmit( async function handleSubmit(
values: MetadataPublishForm, values: Partial<MetadataPublishForm>,
resetForm: () => void resetForm: () => void
): Promise<void> { ): Promise<void> {
const metadata = transformPublishFormToMetadata(values) const metadata = transformPublishFormToMetadata(values)
const priceOptions = values.price const { price } = values
const serviceType = values.access === 'Download' ? 'access' : 'compute' const serviceType = values.access === 'Download' ? 'access' : 'compute'
let datatokenOptions: DataTokenOptions
try { try {
Logger.log('Publish with ', priceOptions, serviceType, datatokenOptions) Logger.log('Publish with ', price, serviceType, price.datatoken)
const ddo = await publish( const ddo = await publish(
metadata as any, metadata as any,
priceOptions, {
...price,
liquidityProviderFee: `${price.liquidityProviderFee}`
},
serviceType, serviceType,
datatokenOptions price.datatoken
) )
if (publishError) { if (publishError) {
toast.error(publishError) toast.error(publishError) && console.error(publishError)
console.error(publishError)
return null return null
} }
// User feedback and redirect to new asset detail page // User feedback and redirect to new asset detail page
ddo && toast.success('Asset created successfully.') ddo && toast.success('Asset created successfully.') && resetForm()
// reset form state
// TODO: verify persistant form in localStorage is cleared with it too
resetForm()
// Go to new asset detail page // Go to new asset detail page
navigate(`/asset/${ddo.id}`) navigate(`/asset/${ddo.id}`)
} catch (error) { } catch (error) {

View File

@ -3,7 +3,7 @@ import { toStringNoMS } from '../../../utils'
import AssetModel from '../../../models/Asset' import AssetModel from '../../../models/Asset'
export function transformPublishFormToMetadata( export function transformPublishFormToMetadata(
data: MetadataPublishForm data: Partial<MetadataPublishForm>
): MetadataMarket { ): MetadataMarket {
const currentTime = toStringNoMS(new Date()) const currentTime = toStringNoMS(new Date())

View File

@ -6,14 +6,28 @@ 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().shape({ price: Yup.object()
tokensToMint: Yup.number().required('Required'), .shape({
type: Yup.string() price: Yup.number().min(1, 'Must be greater than 0').required('Required'),
.matches(/fixed|dynamic/g) tokensToMint: Yup.number()
.required('Required'), .min(1, 'Must be greater than 0')
weightOnDataToken: Yup.string().required('Required'), .required('Required'),
liquidityProviderFee: Yup.string() type: Yup.string()
}), .matches(/fixed|dynamic/g)
.required('Required'),
weightOnDataToken: Yup.string().required('Required'),
liquidityProviderFee: 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'),
files: Yup.array<FileMetadata>().required('Required').nullable(), files: Yup.array<FileMetadata>().required('Required').nullable(),
description: Yup.string().required('Required'), description: Yup.string().required('Required'),
license: Yup.string().required('Required'), license: Yup.string().required('Required'),
@ -23,27 +37,24 @@ export const validationSchema = Yup.object().shape<MetadataPublishForm>({
termsAndConditions: Yup.boolean().required('Required'), termsAndConditions: Yup.boolean().required('Required'),
// ---- optional fields ---- // ---- optional fields ----
copyrightHolder: Yup.string(), copyrightHolder: Yup.string().nullable(),
tags: Yup.string(), tags: Yup.string().nullable(),
links: Yup.object<FileMetadata[]>().nullable() links: Yup.object<FileMetadata[]>().nullable()
}) })
export const initialValues: MetadataPublishForm = { export const initialValues: Partial<MetadataPublishForm> = {
name: '', name: '',
author: '', author: '',
price: { price: {
price: 1,
type: 'fixed', type: 'fixed',
tokensToMint: 1, tokensToMint: 1,
weightOnDataToken: '9', // 90% on data token weightOnDataToken: '9', // 90% on data token
liquidityProviderFee: '0.1', // in % liquidityProviderFee: 0.1 // in %
price: 1
}, },
files: '', files: '',
description: '', description: '',
license: '', license: '',
access: '', access: '',
termsAndConditions: false, termsAndConditions: false
copyrightHolder: '',
tags: '',
links: ''
} }

View File

@ -5,10 +5,11 @@ const testFormData: MetadataPublishForm = {
files: [], files: [],
license: '', license: '',
price: { price: {
tokensToMint: 1, price: 1,
type: 'simple', tokensToMint: 9,
type: 'fixed',
weightOnDataToken: '1', weightOnDataToken: '1',
liquidityProviderFee: '0.1' liquidityProviderFee: 0.1
}, },
name: '', name: '',
description: 'description', description: 'description',