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

merge branch main into feature/account-page and fixed merge conflicts

This commit is contained in:
Bogdan Fazakas 2021-08-25 09:15:31 +03:00
commit 69f02afd59
58 changed files with 812 additions and 196 deletions

View File

@ -18,6 +18,7 @@
# Enables another asset editing button holder further advanced settings # Enables another asset editing button holder further advanced settings
#GATSBY_ALLOW_ADVANCED_SETTINGS="true" #GATSBY_ALLOW_ADVANCED_SETTINGS="true"
#GATSBY_ALLOW_ADVANCED_PUBLISH_SETTINGS="true"
# Allow/Deny Lists # Allow/Deny Lists
#GATSBY_CREDENTIAL_TYPE="address" #GATSBY_CREDENTIAL_TYPE="address"

View File

@ -81,7 +81,7 @@ npm start
To use the app together with MetaMask, importing one of the accounts auto-generated by the Ganache container is the easiest way to have test ETH available. All of them have 100 ETH by default. Upon start, the `ocean_ganache_1` container will print out the private keys of multiple accounts in its logs. Pick one of them and import into MetaMask. To use the app together with MetaMask, importing one of the accounts auto-generated by the Ganache container is the easiest way to have test ETH available. All of them have 100 ETH by default. Upon start, the `ocean_ganache_1` container will print out the private keys of multiple accounts in its logs. Pick one of them and import into MetaMask.
To fully test all [The Graph](https://thegraph.com) integrations, you have to run your own local Graph node with our [`ocean-subgraph`](https://github.com/oceanprotocol/ocean-subgraph) deployed to it. Barge does not include a local subgraph so by default, the `config.subgraphUri` is hardcoded to the Rinkeby subgraph in our [`NetworkMonitor` component](https://github.com/oceanprotocol/market/blob/main/src/helpers/NetworkMonitor.tsx). To fully test all [The Graph](https://thegraph.com) integrations, you have to run your own local Graph node with our [`ocean-subgraph`](https://github.com/oceanprotocol/ocean-subgraph) deployed to it. Barge does not include a local subgraph so by default, the `subgraphUri` is hardcoded to the Rinkeby subgraph in our [`getDevelopmentConfig` function](https://github.com/oceanprotocol/market/blob/d0b1534d105e5dcb3790c65d4bb04ff1d2dbc575/src/utils/ocean.ts#L31).
> Cleaning all Docker images so they are fetched freshly is often a good idea to make sure no issues are caused by old or stale images: `docker system prune --all --volumes` > Cleaning all Docker images so they are fetched freshly is often a good idea to make sure no issues are caused by old or stale images: `docker system prune --all --volumes`

View File

@ -60,5 +60,7 @@ module.exports = {
// Used to show or hide advanced settings button in asset details page // Used to show or hide advanced settings button in asset details page
allowAdvancedSettings: process.env.GATSBY_ALLOW_ADVANCED_SETTINGS || 'false', allowAdvancedSettings: process.env.GATSBY_ALLOW_ADVANCED_SETTINGS || 'false',
allowAdvancedPublishSettings:
process.env.GATSBY_ALLOW_ADVANCED_PUBLISH_SETTINGS || 'false',
credentialType: process.env.GATSBY_CREDENTIAL_TYPE || 'address' credentialType: process.env.GATSBY_CREDENTIAL_TYPE || 'address'
} }

View File

@ -5,7 +5,7 @@ documents:
- './src/components/pages/History/PoolShares.tsx' - './src/components/pages/History/PoolShares.tsx'
- './src/components/pages/History/Downloads.tsx' - './src/components/pages/History/Downloads.tsx'
- './src/components/pages/History/ComputeJobs/index.tsx' - './src/components/pages/History/ComputeJobs/index.tsx'
- './src/ //Users/bogdanfazakas/Sites/ocean-market/src/components/organisms/AssetContent/EditHistory.tsx' - './src/components/organisms/AssetContent/EditHistory.tsx'
# - './src/components/organisms/AssetActions/Pool/index.tsx' # - './src/components/organisms/AssetActions/Pool/index.tsx'
- './src/components/organisms/AssetActions/Pool/Graph.tsx' - './src/components/organisms/AssetActions/Pool/Graph.tsx'
- './src/components/organisms/AssetActions/Consume.tsx' - './src/components/organisms/AssetActions/Consume.tsx'

View File

@ -91,6 +91,14 @@
"placeholder": "e.g. logistics, ai", "placeholder": "e.g. logistics, ai",
"help": "Separate tags with comma." "help": "Separate tags with comma."
}, },
{
"name": "providerUri",
"label": "Custom Provider URL",
"type": "providerUri",
"help": "Enter the URL for your custom provider or leave blank to use the default provider. [Learn more](https://github.com/oceanprotocol/provider/).",
"placeholder": "https://provider.polygon.oceanprotocol.com/",
"advanced": true
},
{ {
"name": "termsAndConditions", "name": "termsAndConditions",
"label": "Terms & Conditions", "label": "Terms & Conditions",

View File

@ -38,6 +38,14 @@
"options": ["Download", "Compute"], "options": ["Download", "Compute"],
"required": true "required": true
}, },
{
"name": "providerUri",
"label": "Custom Provider URL",
"type": "providerUri",
"help": "Enter the URL for your custom provider or leave blank to use the default provider. [Learn more](https://github.com/oceanprotocol/provider/).",
"placeholder": "https://provider.polygon.oceanprotocol.com/",
"advanced": true
},
{ {
"name": "timeout", "name": "timeout",
"label": "Timeout", "label": "Timeout",

View File

@ -12,18 +12,18 @@
"title": "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.", "info": "Set your price for accessing this data set. The datatoken for this data set will be worth the entered amount of OCEAN.",
"tooltips": { "tooltips": {
"communityFee": "Explain community fee...", "communityFee": "Goes to Ocean DAO for teams to improve the tools, build apps, do outreach, and more. A small fraction is used to burn OCEAN. This fee is collected when downloading or using an asset in a compute job.",
"marketplaceFee": "Explain marketplace fee..." "marketplaceFee": "Goes to the marketplace owner that is hosting and providing the marketplace and is collected when downloading or using an asset in a compute job. In Ocean Market, it is treated as network revenue that goes to the Ocean community."
} }
}, },
"dynamic": { "dynamic": {
"title": "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.", "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": { "tooltips": {
"poolInfo": "Explain what is going on here...", "poolInfo": "The liquidity pool provides the funds for traders to trade against. The price of the asset is determined by the ratio of OCEAN to datatokens.",
"swapFee": "Explain liquidity provider fee...", "swapFee": "Liquidity providers earn this fee on all pool trades, proportionally to their share of the pool. The fee is set by the creator of the pool and is used to incentivize liquidity providers to join the pool.",
"communityFee": "Explain community fee...", "communityFee": "Goes to Ocean DAO for teams to improve the tools, build apps, do outreach, and more. A small fraction is used to burn OCEAN. This fee is collected when downloading or using an asset in a compute job.",
"marketplaceFee": "Explain marketplace fee..." "marketplaceFee": "Goes to the marketplace owner that is hosting and providing the marketplace and is collected when downloading or using an asset in a compute job. In Ocean Market, it is treated as network revenue that goes to the Ocean community."
} }
}, },
"free": { "free": {
@ -33,7 +33,7 @@
}, },
"pool": { "pool": {
"tooltips": { "tooltips": {
"price": "Explain how this price is determined...", "price": "The price is determined by an automated market maker, which is a type of decentralized exchange protocol that relies on a mathematical formula. It is an alternative to a traditional order book.",
"liquidity": "Providing liquidity will earn you SWAPFEE% on every transaction in this pool, proportionally to your share of the pool." "liquidity": "Providing liquidity will earn you SWAPFEE% on every transaction in this pool, proportionally to your share of the pool."
}, },
"add": { "add": {

View File

@ -13,6 +13,7 @@ export interface FormFieldProps {
placeholder?: string placeholder?: string
pattern?: string pattern?: string
min?: string min?: string
advanced?: boolean
} }
export interface FormContent { export interface FormContent {

View File

@ -38,6 +38,7 @@ export interface MetadataPublishFormDataset {
// ---- optional fields ---- // ---- optional fields ----
tags?: string tags?: string
links?: string | EditableMetadataLinks[] links?: string | EditableMetadataLinks[]
providerUri?: string
} }
export interface MetadataPublishFormAlgorithm { export interface MetadataPublishFormAlgorithm {
@ -56,6 +57,7 @@ export interface MetadataPublishFormAlgorithm {
containerTag: string containerTag: string
entrypoint: string entrypoint: string
tags?: string tags?: string
providerUri?: string
} }
export interface MetadataEditForm { export interface MetadataEditForm {

View File

@ -1,6 +1,6 @@
export interface PoolBalance { export interface PoolBalance {
ocean: number ocean: string
datatoken: number datatoken: string
} }
export interface UserBalance { export interface UserBalance {

View File

@ -8,7 +8,6 @@ import { useWeb3 } from '../providers/Web3'
import { useSiteMetadata } from '../hooks/useSiteMetadata' import { useSiteMetadata } from '../hooks/useSiteMetadata'
import { useAccountPurgatory } from '../hooks/useAccountPurgatory' import { useAccountPurgatory } from '../hooks/useAccountPurgatory'
import AnnouncementBanner from './atoms/AnnouncementBanner' import AnnouncementBanner from './atoms/AnnouncementBanner'
import { useGraphSyncStatus } from '../hooks/useGraphSyncStatus'
import styles from './App.module.css' import styles from './App.module.css'
const contentQuery = graphql` const contentQuery = graphql`
@ -40,7 +39,6 @@ export default function App({
const { warning } = useSiteMetadata() const { warning } = useSiteMetadata()
const { accountId } = useWeb3() const { accountId } = useWeb3()
const { isInPurgatory, purgatoryData } = useAccountPurgatory(accountId) const { isInPurgatory, purgatoryData } = useAccountPurgatory(accountId)
// const { isGraphSynced, blockHead, blockGraph } = useGraphSyncStatus()
return ( return (
<Styles> <Styles>

View File

@ -3,6 +3,7 @@ import slugify from '@sindresorhus/slugify'
import styles from './InputElement.module.css' 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 CustomProvider from '../../molecules/FormFields/CustomProvider'
import Terms from '../../molecules/FormFields/Terms' import Terms from '../../molecules/FormFields/Terms'
import BoxSelection, { import BoxSelection, {
BoxSelectionOption BoxSelectionOption
@ -125,6 +126,8 @@ export default function InputElement({
) )
case 'files': case 'files':
return <FilesInput name={name} {...field} {...props} /> return <FilesInput name={name} {...field} {...props} />
case 'providerUri':
return <CustomProvider name={name} {...field} {...props} />
case 'datatoken': case 'datatoken':
return <Datatoken name={name} {...field} {...props} /> return <Datatoken name={name} {...field} {...props} />
case 'terms': case 'terms':

View File

@ -63,7 +63,7 @@ export default function Publisher({
) : ( ) : (
<> <>
<Link <Link
to={`/search/?owner=${account}&sort=created&sortOrder=desc`} to={`/search?sort=created&sortOrder=desc&text=${account}`}
title="Show all data sets created by this account." title="Show all data sets created by this account."
> >
{name} {name}

View File

@ -0,0 +1,3 @@
.advancedBtn {
margin-bottom: 2rem;
}

View File

@ -0,0 +1,53 @@
import React, { ReactElement, useState, FormEvent, ChangeEvent } from 'react'
import { useSiteMetadata } from '../../../hooks/useSiteMetadata'
import Input from '../../atoms/Input'
import Button from '../../atoms/Button'
import { FormContent, FormFieldProps } from '../../../@types/Form'
import { Field } from 'formik'
import styles from './AdvancedSettings.module.css'
export default function AdvancedSettings(prop: {
content: FormContent
handleFieldChange: (
e: ChangeEvent<HTMLInputElement>,
field: FormFieldProps
) => void
}): ReactElement {
const { appConfig } = useSiteMetadata()
const [advancedSettings, setAdvancedSettings] = useState<boolean>(false)
function toggleAdvancedSettings(e: FormEvent<Element>) {
e.preventDefault()
advancedSettings === true
? setAdvancedSettings(false)
: setAdvancedSettings(true)
}
return (
<>
{appConfig.allowAdvancedPublishSettings === 'true' && (
<Button
className={styles.advancedBtn}
style="text"
size="small"
onClick={toggleAdvancedSettings}
>
Advanced Settings
</Button>
)}
{prop.content.data.map(
(field: FormFieldProps) =>
advancedSettings === true &&
field.advanced === true && (
<Field
key={field.name}
{...field}
options={field.options}
component={Input}
onChange={(e: ChangeEvent<HTMLInputElement>) =>
prop.handleFieldChange(e, field)
}
/>
)
)}
</>
)
}

View File

@ -0,0 +1,60 @@
import React, { ReactElement, useState, useEffect } from 'react'
import { useField } from 'formik'
import { toast } from 'react-toastify'
import CustomInput from './URLInput/Input'
import { useOcean } from '../../../providers/Ocean'
import { InputProps } from '../../atoms/Input'
export default function CustomProvider(props: InputProps): ReactElement {
const [field, meta, helpers] = useField(props.name)
const [isLoading, setIsLoading] = useState(false)
const [providerUrl, setProviderUrl] = useState<string>()
const { ocean, config } = useOcean()
function loadProvider() {
if (!providerUrl) return
async function validateProvider() {
let valid: boolean
try {
setIsLoading(true)
valid = await ocean.provider.isValidProvider(providerUrl)
} catch (error) {
valid = false
console.error(error.message)
} finally {
valid
? toast.success('Perfect! That provider URL looks good 🐳')
: toast.error(
'Could not validate provider. Please check URL and try again'
)
setIsLoading(false)
}
}
validateProvider()
}
useEffect(() => {
loadProvider()
}, [providerUrl, config.providerUri])
async function handleButtonClick(e: React.SyntheticEvent, url: string) {
helpers.setTouched(false)
e.preventDefault()
if (providerUrl === url) {
loadProvider()
}
setProviderUrl(url)
}
return (
<CustomInput
submitText="Validate"
{...props}
{...field}
isLoading={isLoading}
handleButtonClick={handleButtonClick}
/>
)
}

View File

@ -3,7 +3,7 @@ import axios from 'axios'
import { useField } from 'formik' import { useField } from 'formik'
import { toast } from 'react-toastify' import { toast } from 'react-toastify'
import FileInfo from './Info' import FileInfo from './Info'
import FileInput from './Input' import CustomInput from '../URLInput/Input'
import { InputProps } from '../../../atoms/Input' import { InputProps } from '../../../atoms/Input'
import { fileinfo } from '../../../../utils/provider' import { fileinfo } from '../../../../utils/provider'
import { useWeb3 } from '../../../../providers/Web3' import { useWeb3 } from '../../../../providers/Web3'
@ -68,7 +68,8 @@ export default function FilesInput(props: InputProps): ReactElement {
{field?.value && field.value[0] && typeof field.value === 'object' ? ( {field?.value && field.value[0] && typeof field.value === 'object' ? (
<FileInfo name={props.name} file={field.value[0]} /> <FileInfo name={props.name} file={field.value[0]} />
) : ( ) : (
<FileInput <CustomInput
submitText="Add File"
{...props} {...props}
{...field} {...field}
isLoading={isLoading} isLoading={isLoading}

View File

@ -5,11 +5,13 @@ import Loader from '../../../atoms/Loader'
import styles from './Input.module.css' import styles from './Input.module.css'
import InputGroup from '../../../atoms/Input/InputGroup' import InputGroup from '../../../atoms/Input/InputGroup'
export default function FileInput({ export default function URLInput({
submitText,
handleButtonClick, handleButtonClick,
isLoading, isLoading,
...props ...props
}: { }: {
submitText: string
handleButtonClick(e: React.SyntheticEvent, data: string): void handleButtonClick(e: React.SyntheticEvent, data: string): void
isLoading: boolean isLoading: boolean
}): ReactElement { }): ReactElement {
@ -30,7 +32,7 @@ export default function FileInput({
onClick={(e: React.SyntheticEvent) => e.preventDefault()} onClick={(e: React.SyntheticEvent) => e.preventDefault()}
disabled={!field.value} disabled={!field.value}
> >
{isLoading ? <Loader /> : 'Add File'} {isLoading ? <Loader /> : submitText}
</Button> </Button>
</InputGroup> </InputGroup>
) )

View File

@ -4,7 +4,7 @@ import Conversion from '../atoms/Price/Conversion'
import PriceUnit from '../atoms/Price/PriceUnit' import PriceUnit from '../atoms/Price/PriceUnit'
import Tooltip from '../atoms/Tooltip' import Tooltip from '../atoms/Tooltip'
import NetworkName from '../atoms/NetworkName' import NetworkName from '../atoms/NetworkName'
import { fetchData, getSubgrahUri } from '../../utils/subgraph' import { fetchData, getSubgraphUri } from '../../utils/subgraph'
import { filterNetworksByType } from './UserPreferences/Networks/index' import { filterNetworksByType } from './UserPreferences/Networks/index'
import { useSiteMetadata } from '../../hooks/useSiteMetadata' import { useSiteMetadata } from '../../hooks/useSiteMetadata'
import useNetworkMetadata from '../../hooks/useNetworkMetadata' import useNetworkMetadata from '../../hooks/useNetworkMetadata'
@ -113,7 +113,7 @@ export default function MarketStats(): ReactElement {
for (const chainId of mainChainIdsList) { for (const chainId of mainChainIdsList) {
const context: OperationContext = { const context: OperationContext = {
url: `${getSubgrahUri( url: `${getSubgraphUri(
chainId chainId
)}/subgraphs/name/oceanprotocol/ocean-subgraph`, )}/subgraphs/name/oceanprotocol/ocean-subgraph`,
requestPolicy: 'network-only' requestPolicy: 'network-only'

View File

@ -38,3 +38,6 @@
.action { .action {
margin-top: calc(var(--spacer) / 1.5); margin-top: calc(var(--spacer) / 1.5);
} }
.moreInfo {
padding: calc(var(--spacer) / 4) calc(var(--spacer) / 2) !important;
}

View File

@ -70,7 +70,12 @@ export default function MetadataFeedback({
<> <>
<p>Sorry, something went wrong. Please try again.</p> <p>Sorry, something went wrong. Please try again.</p>
{moreInfo && <Alert text={error} state="error" />} {moreInfo && <Alert text={error} state="error" />}
<Button style="text" size="small" onClick={toggleMoreInfo}> <Button
style="text"
size="small"
onClick={toggleMoreInfo}
className={styles.moreInfo}
>
{moreInfo === false ? 'More Info' : 'Hide error'} {moreInfo === false ? 'More Info' : 'Hide error'}
</Button> </Button>
<ActionError setError={setError} /> <ActionError setError={setError} />

View File

@ -129,10 +129,12 @@ const columnsMinimal = [columns[0], columns[3]]
export default function PoolTransactions({ export default function PoolTransactions({
poolAddress, poolAddress,
poolChainId,
minimal, minimal,
accountId accountId
}: { }: {
poolAddress?: string poolAddress?: string
poolChainId?: number[]
minimal?: boolean minimal?: boolean
accountId: string accountId: string
}): ReactElement { }): ReactElement {
@ -144,13 +146,17 @@ export default function PoolTransactions({
const [data, setData] = useState<PoolTransaction[]>() const [data, setData] = useState<PoolTransaction[]>()
async function fetchPoolTransactionData() { async function fetchPoolTransactionData() {
const variables = { user: accountId?.toLowerCase() } const variables = {
user: accountId?.toLowerCase(),
pool: poolAddress?.toLowerCase()
}
const transactions: PoolTransaction[] = [] const transactions: PoolTransaction[] = []
const result = await fetchDataForMultipleChains( const result = await fetchDataForMultipleChains(
poolAddress ? txHistoryQueryByPool : txHistoryQuery, poolAddress ? txHistoryQueryByPool : txHistoryQuery,
variables, variables,
chainIds poolAddress ? poolChainId : chainIds
) )
for (let i = 0; i < result.length; i++) { for (let i = 0; i < result.length; i++) {
result[i].poolTransactions.forEach((poolTransaction: PoolTransaction) => { result[i].poolTransactions.forEach((poolTransaction: PoolTransaction) => {
transactions.push(poolTransaction) transactions.push(poolTransaction)

View File

@ -41,19 +41,20 @@ function handleTimeoutCustomOption(
data[timeoutInputIndex].options[5] = values.timeout data[timeoutInputIndex].options[5] = values.timeout
} }
} }
export default function FormEditMetadata({ export default function FormEditMetadata({
data, data,
setShowEdit, setShowEdit,
setTimeoutStringValue, setTimeoutStringValue,
values, values,
showPrice showPrice,
isComputeDataset
}: { }: {
data: FormFieldProps[] data: FormFieldProps[]
setShowEdit: (show: boolean) => void setShowEdit: (show: boolean) => void
setTimeoutStringValue: (value: string) => void setTimeoutStringValue: (value: string) => void
values: Partial<MetadataPublishFormDataset> values: Partial<MetadataPublishFormDataset>
showPrice: boolean showPrice: boolean
isComputeDataset: boolean
}): ReactElement { }): ReactElement {
const { config } = useOcean() const { config } = useOcean()
const { const {
@ -70,12 +71,22 @@ export default function FormEditMetadata({
validateField(field.name) validateField(field.name)
setFieldValue(field.name, e.target.value) setFieldValue(field.name, e.target.value)
} }
// This component is handled by Formik so it's not rendered like a "normal" react component, // This component is handled by Formik so it's not rendered like a "normal" react component,
// so handleTimeoutCustomOption is called only once. // so handleTimeoutCustomOption is called only once.
// https://github.com/oceanprotocol/market/pull/324#discussion_r561132310 // https://github.com/oceanprotocol/market/pull/324#discussion_r561132310
if (data && values) handleTimeoutCustomOption(data, values) if (data && values) handleTimeoutCustomOption(data, values)
const timeoutOptionsArray = data.filter(
(field) => field.name === 'timeout'
)[0].options
if (isComputeDataset && timeoutOptionsArray.includes('Forever')) {
const foreverOptionIndex = timeoutOptionsArray.indexOf('Forever')
timeoutOptionsArray.splice(foreverOptionIndex, 1)
} else if (!isComputeDataset && !timeoutOptionsArray.includes('Forever')) {
timeoutOptionsArray.push('Forever')
}
return ( return (
<Form className={styles.form}> <Form className={styles.form}>
{data.map( {data.map(
@ -83,6 +94,11 @@ export default function FormEditMetadata({
(!showPrice && field.name === 'price') || ( (!showPrice && field.name === 'price') || (
<Field <Field
key={field.name} key={field.name}
options={
field.name === 'timeout' && isComputeDataset === true
? timeoutOptionsArray
: field.options
}
{...field} {...field}
component={Input} component={Input}
prefix={field.name === 'price' && config.oceanTokenSymbol} prefix={field.name === 'price' && config.oceanTokenSymbol}

View File

@ -55,9 +55,11 @@ const contentQuery = graphql`
` `
export default function Edit({ export default function Edit({
setShowEdit setShowEdit,
isComputeType
}: { }: {
setShowEdit: (show: boolean) => void setShowEdit: (show: boolean) => void
isComputeType?: boolean
}): ReactElement { }): ReactElement {
const data = useStaticQuery(contentQuery) const data = useStaticQuery(contentQuery)
const content = data.content.edges[0].node.childPagesJson const content = data.content.edges[0].node.childPagesJson
@ -201,6 +203,7 @@ export default function Edit({
setTimeoutStringValue={setTimeoutStringValue} setTimeoutStringValue={setTimeoutStringValue}
values={initialValues} values={initialValues}
showPrice={price.type === 'exchange'} showPrice={price.type === 'exchange'}
isComputeDataset={isComputeType}
/> />
<aside> <aside>

View File

@ -15,6 +15,9 @@ import UserLiquidity from '../../../../atoms/UserLiquidity'
import { useOcean } from '../../../../../providers/Ocean' import { useOcean } from '../../../../../providers/Ocean'
import { useWeb3 } from '../../../../../providers/Web3' import { useWeb3 } from '../../../../../providers/Web3'
import { isValidNumber } from './../../../../../utils/numberValidations'
import Decimal from 'decimal.js'
export default function FormAdd({ export default function FormAdd({
coin, coin,
dtBalance, dtBalance,
@ -67,6 +70,7 @@ export default function FormAdd({
setNewPoolShare('0') setNewPoolShare('0')
return return
} }
if (Number(values.amount) > Number(amountMax)) return if (Number(values.amount) > Number(amountMax)) return
const poolTokens = await ocean.pool.calcPoolOutGivenSingleIn( const poolTokens = await ocean.pool.calcPoolOutGivenSingleIn(
@ -74,15 +78,20 @@ export default function FormAdd({
coin === 'OCEAN' ? ocean.pool.oceanAddress : ocean.pool.dtAddress, coin === 'OCEAN' ? ocean.pool.oceanAddress : ocean.pool.dtAddress,
`${values.amount}` `${values.amount}`
) )
setNewPoolTokens(poolTokens) setNewPoolTokens(poolTokens)
totalBalance &&
setNewPoolShare( const newPoolShareDecimal =
`${ isValidNumber(poolTokens) && isValidNumber(totalPoolTokens)
(Number(poolTokens) / ? new Decimal(poolTokens)
(Number(totalPoolTokens) + Number(poolTokens))) * .dividedBy(
100 new Decimal(totalPoolTokens).plus(new Decimal(poolTokens))
}`
) )
.mul(100)
.toString()
: '0'
totalBalance && setNewPoolShare(newPoolShareDecimal)
} }
calculatePoolShares() calculatePoolShares()
}, [ }, [

View File

@ -2,6 +2,7 @@ import Conversion from '../../../atoms/Price/Conversion'
import React, { ReactElement, ReactNode } from 'react' import React, { ReactElement, ReactNode } from 'react'
import Token from './Token' import Token from './Token'
import styles from './TokenList.module.css' import styles from './TokenList.module.css'
import Decimal from 'decimal.js'
export default function TokenList({ export default function TokenList({
title, title,
@ -20,7 +21,7 @@ export default function TokenList({
dt: string dt: string
dtSymbol: string dtSymbol: string
poolShares: string poolShares: string
conversion: number conversion: Decimal
highlight?: boolean highlight?: boolean
showTVLLabel?: boolean showTVLLabel?: boolean
}): ReactElement { }): ReactElement {
@ -31,9 +32,9 @@ export default function TokenList({
<div> <div>
<Token symbol="OCEAN" balance={ocean} /> <Token symbol="OCEAN" balance={ocean} />
<Token symbol={dtSymbol} balance={dt} /> <Token symbol={dtSymbol} balance={dt} />
{conversion > 0 && ( {conversion.greaterThan(0) && (
<Conversion <Conversion
price={`${conversion}`} price={conversion.toString()}
className={styles.totalLiquidity} className={styles.totalLiquidity}
showTVLLabel={showTVLLabel} showTVLLabel={showTVLLabel}
/> />

View File

@ -22,8 +22,13 @@ import { useWeb3 } from '../../../../providers/Web3'
import PoolTransactions from '../../../molecules/PoolTransactions' import PoolTransactions from '../../../molecules/PoolTransactions'
import { fetchData, getQueryContext } from '../../../../utils/subgraph' import { fetchData, getQueryContext } from '../../../../utils/subgraph'
import { isValidNumber } from './../../../../utils/numberValidations'
import Decimal from 'decimal.js'
const REFETCH_INTERVAL = 5000 const REFETCH_INTERVAL = 5000
Decimal.set({ toExpNeg: -18, precision: 18, rounding: 1 })
const contentQuery = graphql` const contentQuery = graphql`
query PoolQuery { query PoolQuery {
content: allFile(filter: { relativePath: { eq: "price.json" } }) { content: allFile(filter: { relativePath: { eq: "price.json" } }) {
@ -85,11 +90,15 @@ export default function Pool(): ReactElement {
const [hasAddedLiquidity, setHasAddedLiquidity] = useState(false) const [hasAddedLiquidity, setHasAddedLiquidity] = useState(false)
const [poolShare, setPoolShare] = useState<string>() const [poolShare, setPoolShare] = useState<string>()
const [totalUserLiquidityInOcean, setTotalUserLiquidityInOcean] = useState(0) const [totalUserLiquidityInOcean, setTotalUserLiquidityInOcean] = useState(
const [totalLiquidityInOcean, setTotalLiquidityInOcean] = useState(0) new Decimal(0)
)
const [totalLiquidityInOcean, setTotalLiquidityInOcean] = useState(
new Decimal(0)
)
const [creatorTotalLiquidityInOcean, setCreatorTotalLiquidityInOcean] = const [creatorTotalLiquidityInOcean, setCreatorTotalLiquidityInOcean] =
useState(0) useState(new Decimal(0))
const [creatorLiquidity, setCreatorLiquidity] = useState<PoolBalance>() const [creatorLiquidity, setCreatorLiquidity] = useState<PoolBalance>()
const [creatorPoolTokens, setCreatorPoolTokens] = useState<string>() const [creatorPoolTokens, setCreatorPoolTokens] = useState<string>()
const [creatorPoolShare, setCreatorPoolShare] = useState<string>() const [creatorPoolShare, setCreatorPoolShare] = useState<string>()
@ -144,15 +153,28 @@ export default function Pool(): ReactElement {
// Get swap fee // Get swap fee
// swapFee is tricky: to get 0.1% you need to convert from 0.001 // swapFee is tricky: to get 0.1% you need to convert from 0.001
setSwapFee(`${Number(dataLiquidity.pool.swapFee) * 100}`) const swapFee = isValidNumber(dataLiquidity.pool.swapFee)
? new Decimal(dataLiquidity.pool.swapFee).mul(100).toString()
: '0'
setSwapFee(swapFee)
// Get weights // Get weights
const weightDt = dataLiquidity.pool.tokens.filter( const weightDt = dataLiquidity.pool.tokens.filter(
(token: any) => token.tokenAddress === ddo.dataToken.toLowerCase() (token: any) => token.tokenAddress === ddo.dataToken.toLowerCase()
)[0].denormWeight )[0].denormWeight
setWeightDt(`${Number(weightDt) * 10}`) const weightDtDecimal = isValidNumber(weightDt)
setWeightOcean(`${100 - Number(weightDt) * 10}`) ? new Decimal(weightDt).mul(10).toString()
: '0'
setWeightDt(weightDtDecimal)
const weightOceanDecimal = isValidNumber(weightDt)
? new Decimal(100).minus(new Decimal(weightDt).mul(10)).toString()
: '0'
setWeightOcean(weightOceanDecimal)
// //
// Get everything the creator put into the pool // Get everything the creator put into the pool
@ -161,12 +183,25 @@ export default function Pool(): ReactElement {
const creatorPoolTokens = dataLiquidity.pool.shares[0].balance const creatorPoolTokens = dataLiquidity.pool.shares[0].balance
setCreatorPoolTokens(creatorPoolTokens) setCreatorPoolTokens(creatorPoolTokens)
// Calculate creator's provided liquidity based on pool tokens
const creatorOceanBalance = const creatorOceanBalance =
(Number(creatorPoolTokens) / Number(totalPoolTokens)) * price.ocean isValidNumber(creatorPoolTokens) &&
isValidNumber(totalPoolTokens) &&
isValidNumber(price.ocean)
? new Decimal(creatorPoolTokens)
.dividedBy(new Decimal(totalPoolTokens))
.mul(price.ocean)
.toString()
: '0'
const creatorDtBalance = const creatorDtBalance =
(Number(creatorPoolTokens) / Number(totalPoolTokens)) * price.datatoken isValidNumber(creatorPoolTokens) &&
isValidNumber(totalPoolTokens) &&
isValidNumber(price.datatoken)
? new Decimal(creatorPoolTokens)
.dividedBy(new Decimal(totalPoolTokens))
.mul(price.datatoken)
.toString()
: '0'
const creatorLiquidity = { const creatorLiquidity = {
ocean: creatorOceanBalance, ocean: creatorOceanBalance,
@ -175,14 +210,30 @@ export default function Pool(): ReactElement {
setCreatorLiquidity(creatorLiquidity) setCreatorLiquidity(creatorLiquidity)
const totalCreatorLiquidityInOcean = const totalCreatorLiquidityInOcean =
creatorLiquidity?.ocean + isValidNumber(creatorLiquidity?.ocean) &&
creatorLiquidity?.datatoken * dataLiquidity.pool.spotPrice isValidNumber(creatorLiquidity?.datatoken) &&
isValidNumber(dataLiquidity.pool.spotPrice)
? new Decimal(creatorLiquidity?.ocean).add(
new Decimal(creatorLiquidity?.datatoken).mul(
new Decimal(dataLiquidity.pool.spotPrice)
)
)
: new Decimal(0)
setCreatorTotalLiquidityInOcean(totalCreatorLiquidityInOcean) setCreatorTotalLiquidityInOcean(totalCreatorLiquidityInOcean)
const creatorPoolShare = const creatorPoolShare =
price?.ocean && price?.ocean &&
price?.datatoken && price?.datatoken &&
creatorLiquidity && creatorLiquidity &&
((Number(creatorPoolTokens) / Number(totalPoolTokens)) * 100).toFixed(2) isValidNumber(creatorPoolTokens) &&
isValidNumber(totalPoolTokens)
? new Decimal(creatorPoolTokens)
.dividedBy(new Decimal(totalPoolTokens))
.mul(100)
.toFixed(2)
: '0'
setCreatorPoolShare(creatorPoolShare) setCreatorPoolShare(creatorPoolShare)
refetchLiquidity() refetchLiquidity()
} }
@ -195,17 +246,38 @@ export default function Pool(): ReactElement {
useEffect(() => { useEffect(() => {
const poolShare = const poolShare =
isValidNumber(poolTokens) &&
isValidNumber(totalPoolTokens) &&
price?.ocean && price?.ocean &&
price?.datatoken && price?.datatoken &&
((Number(poolTokens) / Number(totalPoolTokens)) * 100).toFixed(5) new Decimal(poolTokens)
.dividedBy(new Decimal(totalPoolTokens))
.mul(100)
.toFixed(5)
setPoolShare(poolShare) setPoolShare(poolShare)
setHasAddedLiquidity(Number(poolShare) > 0) setHasAddedLiquidity(Number(poolShare) > 0)
const totalUserLiquidityInOcean = const totalUserLiquidityInOcean =
userLiquidity?.ocean + userLiquidity?.datatoken * price?.value isValidNumber(userLiquidity?.ocean) &&
isValidNumber(userLiquidity?.datatoken) &&
isValidNumber(price?.value)
? new Decimal(userLiquidity?.ocean).add(
new Decimal(userLiquidity?.datatoken).mul(price?.value)
)
: new Decimal(0)
setTotalUserLiquidityInOcean(totalUserLiquidityInOcean) setTotalUserLiquidityInOcean(totalUserLiquidityInOcean)
const totalLiquidityInOcean = const totalLiquidityInOcean =
Number(price?.ocean) + Number(price?.datatoken) * Number(price?.value) isValidNumber(price?.ocean) &&
isValidNumber(price?.datatoken) &&
isValidNumber(price?.value)
? new Decimal(price?.ocean).add(
new Decimal(price?.datatoken).mul(price?.value)
)
: new Decimal(0)
setTotalLiquidityInOcean(totalLiquidityInOcean) setTotalLiquidityInOcean(totalLiquidityInOcean)
}, [userLiquidity, price, poolTokens, totalPoolTokens]) }, [userLiquidity, price, poolTokens, totalPoolTokens])
@ -221,11 +293,28 @@ export default function Pool(): ReactElement {
price.address price.address
) )
setPoolTokens(poolTokens) setPoolTokens(poolTokens)
// calculate user's provided liquidity based on pool tokens // calculate user's provided liquidity based on pool tokens
const userOceanBalance = const userOceanBalance =
(Number(poolTokens) / Number(totalPoolTokens)) * price.ocean isValidNumber(poolTokens) &&
isValidNumber(totalPoolTokens) &&
isValidNumber(price.ocean)
? new Decimal(poolTokens)
.dividedBy(new Decimal(totalPoolTokens))
.mul(price.ocean)
.toString()
: '0'
const userDtBalance = const userDtBalance =
(Number(poolTokens) / Number(totalPoolTokens)) * price.datatoken isValidNumber(poolTokens) &&
isValidNumber(totalPoolTokens) &&
isValidNumber(price.datatoken)
? new Decimal(poolTokens)
.dividedBy(new Decimal(totalPoolTokens))
.mul(price.datatoken)
.toString()
: '0'
const userLiquidity = { const userLiquidity = {
ocean: userOceanBalance, ocean: userOceanBalance,
datatoken: userDtBalance datatoken: userDtBalance
@ -255,8 +344,8 @@ export default function Pool(): ReactElement {
poolAddress={price.address} poolAddress={price.address}
totalPoolTokens={totalPoolTokens} totalPoolTokens={totalPoolTokens}
totalBalance={{ totalBalance={{
ocean: price.ocean, ocean: new Decimal(price.ocean).toString(),
datatoken: price.datatoken datatoken: new Decimal(price.datatoken).toString()
}} }}
swapFee={swapFee} swapFee={swapFee}
dtSymbol={dtSymbol} dtSymbol={dtSymbol}
@ -367,7 +456,7 @@ export default function Pool(): ReactElement {
style="primary" style="primary"
size="small" size="small"
onClick={() => setShowAdd(true)} onClick={() => setShowAdd(true)}
disabled={isInPurgatory || !isAssetNetwork} disabled={isInPurgatory}
> >
Add Liquidity Add Liquidity
</Button> </Button>
@ -389,6 +478,7 @@ export default function Pool(): ReactElement {
<PoolTransactions <PoolTransactions
accountId={accountId} accountId={accountId}
poolAddress={price?.address} poolAddress={price?.address}
poolChainId={[ddo.chainId]}
minimal minimal
/> />
</AssetActionHistoryTable> </AssetActionHistoryTable>

View File

@ -1,5 +1,10 @@
import { Ocean } from '@oceanprotocol/lib' import { Ocean } from '@oceanprotocol/lib'
import { isValidNumber } from './../../../../utils/numberValidations'
import Decimal from 'decimal.js'
Decimal.set({ toExpNeg: -18, precision: 18, rounding: 1 })
export async function getMaxPercentRemove( export async function getMaxPercentRemove(
ocean: Ocean, ocean: Ocean,
poolAddress: string, poolAddress: string,
@ -15,9 +20,15 @@ export async function getMaxPercentRemove(
amountMaxOcean amountMaxOcean
) )
let amountMaxPercent = `${Math.floor( let amountMaxPercent =
(Number(amountMaxPoolShares) / Number(poolTokens)) * 100 isValidNumber(amountMaxPoolShares) && isValidNumber(poolTokens)
)}` ? new Decimal(amountMaxPoolShares)
.dividedBy(new Decimal(poolTokens))
.mul(100)
.floor()
.toString()
: '0'
if (Number(amountMaxPercent) > 100) { if (Number(amountMaxPercent) > 100) {
amountMaxPercent = '100' amountMaxPercent = '100'
} }

View File

@ -42,8 +42,8 @@ export default function FormTrade({
}: { }: {
ddo: DDO ddo: DDO
balance: PoolBalance balance: PoolBalance
maxDt: number maxDt: string
maxOcean: number maxOcean: string
price: BestPrice price: BestPrice
}): ReactElement { }): ReactElement {
const data = useStaticQuery(contentQuery) const data = useStaticQuery(contentQuery)
@ -61,12 +61,18 @@ export default function FormTrade({
const validationSchema: Yup.SchemaOf<FormTradeData> = Yup.object() const validationSchema: Yup.SchemaOf<FormTradeData> = Yup.object()
.shape({ .shape({
ocean: Yup.number() ocean: Yup.number()
.max(maximumOcean, (param) => `Must be more or equal to ${param.max}`) .max(
Number(maximumOcean),
(param) => `Must be more or equal to ${param.max}`
)
.min(0.001, (param) => `Must be more or equal to ${param.min}`) .min(0.001, (param) => `Must be more or equal to ${param.min}`)
.required('Required') .required('Required')
.nullable(), .nullable(),
datatoken: Yup.number() datatoken: Yup.number()
.max(maximumDt, (param) => `Must be less or equal than ${param.max}`) .max(
Number(maximumDt),
(param) => `Must be less or equal than ${param.max}`
)
.min(0.00001, (param) => `Must be more or equal to ${param.min}`) .min(0.00001, (param) => `Must be more or equal to ${param.min}`)
.required('Required') .required('Required')
.nullable(), .nullable(),
@ -77,7 +83,9 @@ export default function FormTrade({
async function handleTrade(values: FormTradeData) { async function handleTrade(values: FormTradeData) {
try { try {
const impact = new Decimal(100 - Number(values.slippage)).div(100) const impact = new Decimal(
new Decimal(100).sub(new Decimal(values.slippage))
).div(100)
const precision = 15 const precision = 15
const tx = const tx =
values.type === 'buy' values.type === 'buy'

View File

@ -5,6 +5,11 @@ import { useOcean } from '../../../../providers/Ocean'
import Token from '../Pool/Token' import Token from '../Pool/Token'
import styles from './Output.module.css' import styles from './Output.module.css'
import { isValidNumber } from './../../../../utils/numberValidations'
import Decimal from 'decimal.js'
Decimal.set({ toExpNeg: -18, precision: 18, rounding: 1 })
export default function Output({ export default function Output({
dtSymbol, dtSymbol,
poolAddress poolAddress
@ -25,12 +30,20 @@ export default function Output({
async function getSwapFee() { async function getSwapFee() {
const swapFee = await ocean.pool.getSwapFee(poolAddress) const swapFee = await ocean.pool.getSwapFee(poolAddress)
// swapFee is tricky: to get 0.1% you need to convert from 0.001 // swapFee is tricky: to get 0.1% you need to convert from 0.001
setSwapFee(`${Number(swapFee) * 100}`) setSwapFee(
isValidNumber(swapFee) ? new Decimal(swapFee).mul(100).toString() : '0'
)
const value = const value =
values.type === 'buy' values.type === 'buy'
? Number(swapFee) * values.ocean ? isValidNumber(swapFee) && isValidNumber(values.ocean)
: Number(swapFee) * values.datatoken ? new Decimal(swapFee).mul(new Decimal(values.ocean))
: 0
: isValidNumber(swapFee) && isValidNumber(values.datatoken)
? new Decimal(swapFee).mul(new Decimal(values.datatoken))
: 0
setSwapFeeValue(value.toString()) setSwapFeeValue(value.toString())
} }
getSwapFee() getSwapFee()
@ -46,8 +59,14 @@ export default function Output({
const maxImpact = 1 - Number(values.slippage) / 100 const maxImpact = 1 - Number(values.slippage) / 100
const maxPrice = const maxPrice =
values.type === 'buy' values.type === 'buy'
? (values.datatoken * maxImpact).toString() ? isValidNumber(values.datatoken) && isValidNumber(maxImpact)
: (values.ocean * maxImpact).toString() ? new Decimal(values.datatoken)
.mul(new Decimal(maxImpact))
.toString()
: '0'
: isValidNumber(values.ocean) && isValidNumber(maxImpact)
? new Decimal(values.ocean).mul(new Decimal(maxImpact)).toString()
: '0'
setMaxOutput(maxPrice) setMaxOutput(maxPrice)
} }

View File

@ -0,0 +1,24 @@
.priceImpact {
font-size: var(--font-size-small);
border-top: 1px solid var(--border-color);
margin-left: -2rem;
margin-right: -2rem;
padding: calc(var(--spacer) / 4) var(--spacer);
color: var(--color-secondary);
display: grid;
grid-template-columns: 1fr 1fr;
gap: calc(var(--spacer) / 3);
}
.priceImpact strong {
font-weight: var(--font-weight-base);
text-align: right;
}
.alert {
color: var(--brand-alert-red);
}
.number {
font-weight: var(--font-weight-bold);
}

View File

@ -0,0 +1,65 @@
import React, { ReactElement, useEffect, useState } from 'react'
import Decimal from 'decimal.js'
import Tooltip from '../../../atoms/Tooltip'
import styles from './PriceImpact.module.css'
export default function PriceImpact({
totalValue,
tokenAmount,
spotPrice
}: {
/// how much the user actually pays (doesn't matter witch token it is)
totalValue: string
/// how many tokens the user trades (doesn't matter witch token it is)
tokenAmount: string
/// the spot price of the traded token (doesn't matter witch token it is))
spotPrice: string
}): ReactElement {
const [priceImpact, setPriceImpact] = useState<string>('0')
async function getPriceImpact(
totalValue: string,
tokenAmount: string,
spotPrice: string
) {
const dtotalValue = new Decimal(totalValue)
const dTokenAmount = new Decimal(tokenAmount)
const dSpotPrice = new Decimal(spotPrice)
let priceImpact = Decimal.abs(
dtotalValue.div(dTokenAmount.times(dSpotPrice)).minus(1)
).mul(100)
if (priceImpact.isNaN()) priceImpact = new Decimal(0)
return priceImpact.toDecimalPlaces(2, Decimal.ROUND_DOWN)
}
useEffect(() => {
if (!totalValue || !tokenAmount || !spotPrice) {
setPriceImpact('0')
return
}
async function init() {
const newPriceImpact = await getPriceImpact(
totalValue,
tokenAmount,
spotPrice
)
setPriceImpact(newPriceImpact.toString())
}
init()
}, [totalValue, tokenAmount, spotPrice])
return (
<div className={styles.priceImpact}>
<strong>Price impact</strong>
<div>
<span
className={`${styles.number} ${
parseInt(priceImpact) > 5 && styles.alert
}`}
>{`${priceImpact}%`}</span>
<Tooltip content="The difference between the market price and estimated price due to trade size." />
</div>
</div>
)
}

View File

@ -4,13 +4,15 @@
margin-left: -2rem; margin-left: -2rem;
margin-right: -2rem; margin-right: -2rem;
padding: calc(var(--spacer) / 4) var(--spacer); padding: calc(var(--spacer) / 4) var(--spacer);
text-align: center;
color: var(--color-secondary); color: var(--color-secondary);
display: grid;
grid-template-columns: 1fr 1fr;
gap: calc(var(--spacer) / 3);
} }
.slippage strong { .slippage strong {
font-weight: var(--font-weight-base); font-weight: var(--font-weight-base);
color: var(--font-color-heading); text-align: right;
} }
.title { .title {
@ -25,6 +27,4 @@
.slippage select { .slippage select {
width: fit-content; width: fit-content;
display: inline-block; display: inline-block;
margin-left: calc(var(--spacer) / 4);
margin-right: calc(var(--spacer) / 4);
} }

View File

@ -1,7 +1,8 @@
import { FormikContextType, useFormikContext } from 'formik' import { FormikContextType, useFormikContext } from 'formik'
import React, { ChangeEvent, ReactElement, useEffect, useState } from 'react' import React, { ChangeEvent, ReactElement } from 'react'
import { FormTradeData, slippagePresets } from '../../../../models/FormTrade' import { FormTradeData, slippagePresets } from '../../../../models/FormTrade'
import InputElement from '../../../atoms/Input/InputElement' import InputElement from '../../../atoms/Input/InputElement'
import Tooltip from '../../../atoms/Tooltip'
import styles from './Slippage.module.css' import styles from './Slippage.module.css'
export default function Slippage(): ReactElement { export default function Slippage(): ReactElement {
@ -14,9 +15,9 @@ export default function Slippage(): ReactElement {
} }
return ( return (
<>
<div className={styles.slippage}> <div className={styles.slippage}>
<strong>Expected price impact</strong> <strong>Slippage Tolerance</strong>
<div>
<InputElement <InputElement
name="slippage" name="slippage"
type="select" type="select"
@ -27,7 +28,8 @@ export default function Slippage(): ReactElement {
value={values.slippage} value={values.slippage}
onChange={handleChange} onChange={handleChange}
/> />
<Tooltip content="Your transaction will revert if the price changes unfavorably by more than this percentage." />
</div>
</div> </div>
</>
) )
} }

View File

@ -10,6 +10,11 @@ import Output from './Output'
import Slippage from './Slippage' import Slippage from './Slippage'
import { FormTradeData, TradeItem } from '../../../../models/FormTrade' import { FormTradeData, TradeItem } from '../../../../models/FormTrade'
import { useOcean } from '../../../../providers/Ocean' import { useOcean } from '../../../../providers/Ocean'
import PriceImpact from './PriceImpact'
import Decimal from 'decimal.js'
Decimal.set({ toExpNeg: -18, precision: 18, rounding: 1 })
export default function Swap({ export default function Swap({
ddo, ddo,
@ -21,23 +26,23 @@ export default function Swap({
setMaximumOcean setMaximumOcean
}: { }: {
ddo: DDO ddo: DDO
maxDt: number maxDt: string
maxOcean: number maxOcean: string
balance: PoolBalance balance: PoolBalance
price: BestPrice price: BestPrice
setMaximumDt: (value: number) => void setMaximumDt: (value: string) => void
setMaximumOcean: (value: number) => void setMaximumOcean: (value: string) => void
}): ReactElement { }): ReactElement {
const { ocean } = useOcean() const { ocean } = useOcean()
const [oceanItem, setOceanItem] = useState<TradeItem>({ const [oceanItem, setOceanItem] = useState<TradeItem>({
amount: 0, amount: '0',
token: 'OCEAN', token: 'OCEAN',
maxAmount: 0 maxAmount: '0'
}) })
const [dtItem, setDtItem] = useState<TradeItem>({ const [dtItem, setDtItem] = useState<TradeItem>({
amount: 0, amount: '0',
token: ddo.dataTokenInfo.symbol, token: ddo.dataTokenInfo.symbol,
maxAmount: 0 maxAmount: '0'
}) })
const { const {
@ -47,6 +52,11 @@ export default function Swap({
validateForm validateForm
}: FormikContextType<FormTradeData> = useFormikContext() }: FormikContextType<FormTradeData> = useFormikContext()
/// Values used for calculation of price impact
const [spotPrice, setSpotPrice] = useState<string>()
const [totalValue, setTotalValue] = useState<string>()
const [tokenAmount, setTokenAmount] = useState<string>()
///
useEffect(() => { useEffect(() => {
if (!ddo || !balance || !values || !price) return if (!ddo || !balance || !values || !price) return
@ -66,32 +76,32 @@ export default function Swap({
const maximumDt = const maximumDt =
values.type === 'buy' values.type === 'buy'
? Number(dtAmount) > Number(maxBuyDt) ? Number(dtAmount) > Number(maxBuyDt)
? Number(maxBuyDt) ? new Decimal(maxBuyDt)
: Number(dtAmount) : new Decimal(dtAmount)
: Number(dtAmount) > balance.datatoken : Number(dtAmount) > Number(balance.datatoken)
? balance.datatoken ? new Decimal(balance.datatoken)
: Number(dtAmount) : new Decimal(dtAmount)
const maximumOcean = const maximumOcean =
values.type === 'sell' values.type === 'sell'
? Number(oceanAmount) > Number(maxBuyOcean) ? Number(oceanAmount) > Number(maxBuyOcean)
? Number(maxBuyOcean) ? new Decimal(maxBuyOcean)
: Number(oceanAmount) : new Decimal(oceanAmount)
: Number(oceanAmount) > balance.ocean : Number(oceanAmount) > Number(balance.ocean)
? balance.ocean ? new Decimal(balance.ocean)
: Number(oceanAmount) : new Decimal(oceanAmount)
setMaximumDt(maximumDt) setMaximumDt(maximumDt.toString())
setMaximumOcean(maximumOcean) setMaximumOcean(maximumOcean.toString())
setOceanItem({ setOceanItem({
...oceanItem, ...oceanItem,
amount: oceanAmount, amount: oceanAmount.toString(),
maxAmount: maximumOcean maxAmount: maximumOcean.toString()
}) })
setDtItem({ setDtItem({
...dtItem, ...dtItem,
amount: dtAmount, amount: dtAmount.toString(),
maxAmount: maximumDt maxAmount: maximumDt.toString()
}) })
} }
calculateMaximum() calculateMaximum()
@ -106,16 +116,63 @@ export default function Swap({
} }
const handleValueChange = async (name: string, value: number) => { const handleValueChange = async (name: string, value: number) => {
const newValue = let tokenIn = ''
name === 'ocean' let tokenOut = ''
? values.type === 'sell' let newValue
? await ocean.pool.getDTNeeded(price.address, value.toString())
: await ocean.pool.getDTReceived(price.address, value.toString())
: values.type === 'sell'
? await ocean.pool.getOceanReceived(price.address, value.toString())
: await ocean.pool.getOceanNeeded(price.address, value.toString())
setFieldValue(name === 'ocean' ? 'datatoken' : 'ocean', newValue) if (name === 'ocean') {
if (values.type === 'sell') {
newValue = await ocean.pool.getDTNeeded(price.address, value.toString())
setTotalValue(newValue)
setTokenAmount(value.toString())
tokenIn = ddo.dataToken
tokenOut = ocean.pool.oceanAddress
} else {
newValue = await ocean.pool.getDTReceived(
price.address,
value.toString()
)
setTotalValue(value.toString())
setTokenAmount(newValue)
tokenIn = ocean.pool.oceanAddress
tokenOut = ddo.dataToken
}
} else {
if (values.type === 'sell') {
newValue = await ocean.pool.getOceanReceived(
price.address,
value.toString()
)
setTotalValue(value.toString())
setTokenAmount(newValue)
tokenIn = ddo.dataToken
tokenOut = ocean.pool.oceanAddress
} else {
newValue = await ocean.pool.getOceanNeeded(
price.address,
value.toString()
)
setTotalValue(newValue)
setTokenAmount(value.toString())
tokenIn = ocean.pool.oceanAddress
tokenOut = ddo.dataToken
}
}
await setFieldValue(name === 'ocean' ? 'datatoken' : 'ocean', newValue)
const spotPrice = await ocean.pool.getSpotPrice(
price.address,
tokenIn,
tokenOut
)
setSpotPrice(spotPrice)
validateForm() validateForm()
} }
@ -139,6 +196,11 @@ export default function Swap({
<Output dtSymbol={dtItem.token} poolAddress={price?.address} /> <Output dtSymbol={dtItem.token} poolAddress={price?.address} />
<PriceImpact
totalValue={totalValue}
tokenAmount={tokenAmount}
spotPrice={spotPrice}
/>
<Slippage /> <Slippage />
</div> </div>
) )

View File

@ -73,7 +73,7 @@ export default function TradeInput({
size="small" size="small"
onClick={() => { onClick={() => {
setFieldValue(name, item?.maxAmount) setFieldValue(name, item?.maxAmount)
handleValueChange(name, item?.maxAmount) handleValueChange(name, Number(item?.maxAmount))
}} }}
> >
Use Max Use Max

View File

@ -5,13 +5,18 @@ import { useAsset } from '../../../../providers/Asset'
import { useOcean } from '../../../../providers/Ocean' import { useOcean } from '../../../../providers/Ocean'
import { useWeb3 } from '../../../../providers/Web3' import { useWeb3 } from '../../../../providers/Web3'
import { isValidNumber } from './../../../../utils/numberValidations'
import Decimal from 'decimal.js'
Decimal.set({ toExpNeg: -18, precision: 18, rounding: 1 })
export default function Trade(): ReactElement { export default function Trade(): ReactElement {
const { accountId, balance } = useWeb3() const { accountId, balance } = useWeb3()
const { ocean } = useOcean() const { ocean } = useOcean()
const [tokenBalance, setTokenBalance] = useState<PoolBalance>() const [tokenBalance, setTokenBalance] = useState<PoolBalance>()
const { price, ddo } = useAsset() const { price, ddo } = useAsset()
const [maxDt, setMaxDt] = useState(0) const [maxDt, setMaxDt] = useState('0')
const [maxOcean, setMaxOcean] = useState(0) const [maxOcean, setMaxOcean] = useState('0')
// Get datatoken balance, and combine with OCEAN balance from hooks into one object // Get datatoken balance, and combine with OCEAN balance from hooks into one object
useEffect(() => { useEffect(() => {
@ -20,8 +25,8 @@ export default function Trade(): ReactElement {
async function getTokenBalance() { async function getTokenBalance() {
const dtBalance = await ocean.datatokens.balance(ddo.dataToken, accountId) const dtBalance = await ocean.datatokens.balance(ddo.dataToken, accountId)
setTokenBalance({ setTokenBalance({
ocean: Number(balance.ocean), ocean: new Decimal(balance.ocean).toString(),
datatoken: Number(dtBalance) datatoken: new Decimal(dtBalance).toString()
}) })
} }
getTokenBalance() getTokenBalance()
@ -35,12 +40,20 @@ export default function Trade(): ReactElement {
const maxTokensInPool = await ocean.pool.getDTMaxBuyQuantity( const maxTokensInPool = await ocean.pool.getDTMaxBuyQuantity(
price.address price.address
) )
setMaxDt(Number(maxTokensInPool)) setMaxDt(
isValidNumber(maxTokensInPool)
? new Decimal(maxTokensInPool).toString()
: '0'
)
const maxOceanInPool = await ocean.pool.getOceanMaxBuyQuantity( const maxOceanInPool = await ocean.pool.getOceanMaxBuyQuantity(
price.address price.address
) )
setMaxOcean(Number(maxOceanInPool)) setMaxOcean(
isValidNumber(maxOceanInPool)
? new Decimal(maxOceanInPool).toString()
: '0'
)
} }
getMaximum() getMaximum()
}, [ocean, balance.ocean, price]) }, [ocean, balance.ocean, price])

View File

@ -14,10 +14,11 @@ import { useWeb3 } from '../../../providers/Web3'
import Web3Feedback from '../../molecules/Web3Feedback' import Web3Feedback from '../../molecules/Web3Feedback'
import { getFileInfo } from '../../../utils/provider' import { getFileInfo } from '../../../utils/provider'
import axios from 'axios' import axios from 'axios'
import { getOceanConfig } from '../../../utils/ocean'
export default function AssetActions(): ReactElement { export default function AssetActions(): ReactElement {
const { accountId, balance } = useWeb3() const { accountId, balance } = useWeb3()
const { ocean, config, account } = useOcean() const { ocean, account } = useOcean()
const { price, ddo, isAssetNetwork } = useAsset() const { price, ddo, isAssetNetwork } = useAsset()
const [isBalanceSufficient, setIsBalanceSufficient] = useState<boolean>() const [isBalanceSufficient, setIsBalanceSufficient] = useState<boolean>()
@ -30,9 +31,10 @@ export default function AssetActions(): ReactElement {
const [consumableFeedback, setConsumableFeedback] = useState<string>('') const [consumableFeedback, setConsumableFeedback] = useState<string>('')
useEffect(() => { useEffect(() => {
if (!ddo || !accountId) return if (!ddo || !accountId || !ocean || !isAssetNetwork) return
async function checkIsConsumable() { async function checkIsConsumable() {
const consumable: any = await ocean.assets.isConsumable( const consumable = await ocean.assets.isConsumable(
ddo, ddo,
accountId.toLowerCase() accountId.toLowerCase()
) )
@ -42,17 +44,20 @@ export default function AssetActions(): ReactElement {
} }
} }
checkIsConsumable() checkIsConsumable()
}, [accountId, ddo]) }, [accountId, isAssetNetwork, ddo, ocean])
useEffect(() => { useEffect(() => {
if (!config) return const oceanConfig = getOceanConfig(ddo.chainId)
if (!oceanConfig) return
const source = axios.CancelToken.source() const source = axios.CancelToken.source()
async function initFileInfo() { async function initFileInfo() {
setFileIsLoading(true) setFileIsLoading(true)
try { try {
const fileInfo = await getFileInfo( const fileInfo = await getFileInfo(
DID.parse(`${ddo.id}`), DID.parse(`${ddo.id}`),
config.providerUri, oceanConfig.providerUri,
source.token source.token
) )
@ -60,15 +65,16 @@ export default function AssetActions(): ReactElement {
} catch (error) { } catch (error) {
Logger.error(error.message) Logger.error(error.message)
} finally { } finally {
// this triggers a memory leak warrning, no idea how to fix // this triggers a memory leak warning, no idea how to fix
setFileIsLoading(false) setFileIsLoading(false)
} }
} }
initFileInfo() initFileInfo()
return () => { return () => {
source.cancel() source.cancel()
} }
}, [config, ddo]) }, [ddo])
// Get and set user DT balance // Get and set user DT balance
useEffect(() => { useEffect(() => {

View File

@ -2,10 +2,10 @@ import React, { ReactElement, useEffect, useState } from 'react'
import { useAsset } from '../../../providers/Asset' import { useAsset } from '../../../providers/Asset'
import ExplorerLink from '../../atoms/ExplorerLink' import ExplorerLink from '../../atoms/ExplorerLink'
import Time from '../../atoms/Time' import Time from '../../atoms/Time'
import styles from './EditHistory.module.css' import { gql, OperationContext, useQuery } from 'urql'
import { gql, useQuery } from 'urql'
import { ReceiptData_datatokens_updates as ReceiptData } from '../../../@types/apollo/ReceiptData' import { ReceiptData_datatokens_updates as ReceiptData } from '../../../@types/apollo/ReceiptData'
import { useWeb3 } from '../../../providers/Web3' import { getQueryContext } from '../../../utils/subgraph'
import styles from './EditHistory.module.css'
const getReceipts = gql` const getReceipts = gql`
query ReceiptData($address: ID!) { query ReceiptData($address: ID!) {
@ -20,14 +20,32 @@ const getReceipts = gql`
` `
export default function EditHistory(): ReactElement { export default function EditHistory(): ReactElement {
const { networkId } = useWeb3()
const { ddo } = useAsset() const { ddo } = useAsset()
//
// 1. Construct subgraph query based on DDO.
// Need to wait for it to avoid infinite rerender loop with useQuery.
//
const [queryContext, setQueryContext] = useState<OperationContext>()
useEffect(() => {
if (!ddo) return
const queryContext = getQueryContext(ddo.chainId)
setQueryContext(queryContext)
}, [ddo])
const [result] = useQuery({ const [result] = useQuery({
query: getReceipts, query: getReceipts,
variables: { address: ddo?.dataToken.toLowerCase() } variables: { address: ddo?.dataToken.toLowerCase() },
context: queryContext,
pause: !ddo || !queryContext
}) })
const { data } = result const { data } = result
//
// 2. Construct display data based on fetched data.
//
const [receipts, setReceipts] = useState<ReceiptData[]>() const [receipts, setReceipts] = useState<ReceiptData[]>()
const [creationTx, setCreationTx] = useState<string>() const [creationTx, setCreationTx] = useState<string>()
@ -51,8 +69,7 @@ export default function EditHistory(): ReactElement {
{receipts?.map((receipt) => ( {receipts?.map((receipt) => (
<li key={receipt.id} className={styles.item}> <li key={receipt.id} className={styles.item}>
<ExplorerLink networkId={ddo.chainId} path={`/tx/${receipt.tx}`}> <ExplorerLink networkId={ddo.chainId} path={`/tx/${receipt.tx}`}>
edited{' '} edited <Time date={`${receipt.timestamp}`} relative isUnix />
<Time date={receipt.timestamp.toString()} relative isUnix />
</ExplorerLink> </ExplorerLink>
</li> </li>
))} ))}

View File

@ -12,6 +12,11 @@ import { DDO } from '@oceanprotocol/lib'
import FormHelp from '../../../../atoms/Input/Help' import FormHelp from '../../../../atoms/Input/Help'
import { useSiteMetadata } from '../../../../../hooks/useSiteMetadata' import { useSiteMetadata } from '../../../../../hooks/useSiteMetadata'
import { isValidNumber } from './../../../../../utils/numberValidations'
import Decimal from 'decimal.js'
Decimal.set({ toExpNeg: -18, precision: 18, rounding: 1 })
export default function FormPricing({ export default function FormPricing({
ddo, ddo,
setShowPricing, setShowPricing,
@ -41,8 +46,15 @@ export default function FormPricing({
useEffect(() => { useEffect(() => {
if (type === 'fixed') return if (type === 'fixed') return
const dtAmount = const dtAmount =
(Number(oceanAmount) / Number(weightOnOcean) / price) * isValidNumber(oceanAmount) &&
Number(weightOnDataToken) isValidNumber(weightOnOcean) &&
isValidNumber(price) &&
isValidNumber(weightOnDataToken)
? new Decimal(oceanAmount)
.dividedBy(new Decimal(weightOnOcean))
.dividedBy(new Decimal(price))
.mul(new Decimal(weightOnDataToken))
: 0
setFieldValue('dtAmount', dtAmount) setFieldValue('dtAmount', dtAmount)
}, [price, oceanAmount, weightOnOcean, weightOnDataToken, type]) }, [price, oceanAmount, weightOnOcean, weightOnDataToken, type])

View File

@ -50,6 +50,7 @@ export default function AssetContent(props: AssetContentProps): ReactElement {
const { owner, isInPurgatory, purgatoryData, isAssetNetwork } = useAsset() const { owner, isInPurgatory, purgatoryData, isAssetNetwork } = useAsset()
const [showPricing, setShowPricing] = useState(false) const [showPricing, setShowPricing] = useState(false)
const [showEdit, setShowEdit] = useState<boolean>() const [showEdit, setShowEdit] = useState<boolean>()
const [isComputeType, setIsComputeType] = useState<boolean>(false)
const [showEditCompute, setShowEditCompute] = useState<boolean>() const [showEditCompute, setShowEditCompute] = useState<boolean>()
const [showEditAdvancedSettings, setShowEditAdvancedSettings] = const [showEditAdvancedSettings, setShowEditAdvancedSettings] =
useState<boolean>() useState<boolean>()
@ -63,7 +64,8 @@ export default function AssetContent(props: AssetContentProps): ReactElement {
const isOwner = accountId.toLowerCase() === owner.toLowerCase() const isOwner = accountId.toLowerCase() === owner.toLowerCase()
setIsOwner(isOwner) setIsOwner(isOwner)
setShowPricing(isOwner && price.type === '') setShowPricing(isOwner && price.type === '')
}, [accountId, price, owner]) setIsComputeType(Boolean(ddo.findServiceByType('compute')))
}, [accountId, price, owner, ddo])
function handleEditButton() { function handleEditButton() {
// move user's focus to top of screen // move user's focus to top of screen
@ -82,7 +84,7 @@ export default function AssetContent(props: AssetContentProps): ReactElement {
} }
return showEdit ? ( return showEdit ? (
<Edit setShowEdit={setShowEdit} /> <Edit setShowEdit={setShowEdit} isComputeType={isComputeType} />
) : showEditCompute ? ( ) : showEditCompute ? (
<EditComputeDataset setShowEdit={setShowEditCompute} /> <EditComputeDataset setShowEdit={setShowEditCompute} />
) : showEditAdvancedSettings ? ( ) : showEditAdvancedSettings ? (

View File

@ -6,6 +6,7 @@ import { DDO } from '@oceanprotocol/lib'
import classNames from 'classnames/bind' import classNames from 'classnames/bind'
import { getAssetsBestPrices, AssetListPrices } from '../../utils/subgraph' import { getAssetsBestPrices, AssetListPrices } from '../../utils/subgraph'
import Loader from '../atoms/Loader' import Loader from '../atoms/Loader'
import { useUserPreferences } from '../../providers/UserPreferences'
const cx = classNames.bind(styles) const cx = classNames.bind(styles)
@ -36,6 +37,7 @@ const AssetList: React.FC<AssetListProps> = ({
onPageChange, onPageChange,
className className
}) => { }) => {
const { chainIds } = useUserPreferences()
const [assetsWithPrices, setAssetWithPrices] = useState<AssetListPrices[]>() const [assetsWithPrices, setAssetWithPrices] = useState<AssetListPrices[]>()
const [loading, setLoading] = useState<boolean>(true) const [loading, setLoading] = useState<boolean>(true)
@ -71,6 +73,8 @@ const AssetList: React.FC<AssetListProps> = ({
key={assetWithPrice.ddo.id} key={assetWithPrice.ddo.id}
/> />
)) ))
) : chainIds.length === 0 ? (
<div className={styles.empty}>No network selected.</div>
) : ( ) : (
<div className={styles.empty}>No results found.</div> <div className={styles.empty}>No results found.</div>
)} )}

View File

@ -17,6 +17,10 @@ import { fetchDataForMultipleChains } from '../../../../utils/subgraph'
import NetworkName from '../../../atoms/NetworkName' import NetworkName from '../../../atoms/NetworkName'
import axios from 'axios' import axios from 'axios'
import { retrieveDDO } from '../../../../utils/aquarius' import { retrieveDDO } from '../../../../utils/aquarius'
import { isValidNumber } from '../../../../utils/numberValidations'
import Decimal from 'decimal.js'
Decimal.set({ toExpNeg: -18, precision: 18, rounding: 1 })
const REFETCH_INTERVAL = 20000 const REFETCH_INTERVAL = 20000
@ -90,11 +94,16 @@ function Liquidity({ row, type }: { row: Asset; type: string }) {
).toString() ).toString()
} }
if (type === 'pool') { if (type === 'pool') {
price = `${ price =
Number(row.poolShare.poolId.oceanReserve) + isValidNumber(row.poolShare.poolId.oceanReserve) &&
Number(row.poolShare.poolId.datatokenReserve) * isValidNumber(row.poolShare.poolId.datatokenReserve) &&
row.poolShare.poolId.consumePrice isValidNumber(row.poolShare.poolId.consumePrice)
}` ? new Decimal(row.poolShare.poolId.datatokenReserve)
.mul(new Decimal(row.poolShare.poolId.consumePrice))
.plus(row.poolShare.poolId.oceanReserve)
.toString()
: '0'
oceanTokenBalance = row.poolShare.poolId.oceanReserve.toString() oceanTokenBalance = row.poolShare.poolId.oceanReserve.toString()
dataTokenBalance = row.poolShare.poolId.datatokenReserve.toString() dataTokenBalance = row.poolShare.poolId.datatokenReserve.toString()
} }

View File

@ -1,12 +1,9 @@
import React, { ReactElement, useEffect, useState } from 'react' import React, { ReactElement, useEffect, useState } from 'react'
import SearchBar from '../molecules/SearchBar'
import styles from './Home.module.css'
import AssetList from '../organisms/AssetList' import AssetList from '../organisms/AssetList'
import { import {
QueryResult, QueryResult,
SearchQuery SearchQuery
} from '@oceanprotocol/lib/dist/node/metadatacache/MetadataCache' } from '@oceanprotocol/lib/dist/node/metadatacache/MetadataCache'
import Container from '../atoms/Container'
import Button from '../atoms/Button' import Button from '../atoms/Button'
import Bookmarks from '../molecules/Bookmarks' import Bookmarks from '../molecules/Bookmarks'
import axios from 'axios' import axios from 'axios'
@ -19,14 +16,15 @@ import { getHighestLiquidityDIDs } from '../../utils/subgraph'
import { DDO, Logger } from '@oceanprotocol/lib' import { DDO, Logger } from '@oceanprotocol/lib'
import { useSiteMetadata } from '../../hooks/useSiteMetadata' import { useSiteMetadata } from '../../hooks/useSiteMetadata'
import { useUserPreferences } from '../../providers/UserPreferences' import { useUserPreferences } from '../../providers/UserPreferences'
import styles from './Home.module.css'
async function getQueryHighest( async function getQueryHighest(
chainIds: number[] chainIds: number[]
): Promise<[SearchQuery, string]> { ): Promise<[SearchQuery, string]> {
const dids = await getHighestLiquidityDIDs(chainIds) const [dids, didsLength] = await getHighestLiquidityDIDs(chainIds)
const queryHighest = { const queryHighest = {
page: 1, page: 1,
offset: dids.length, offset: didsLength,
query: { query: {
query_string: { query_string: {
query: `(${dids}) AND (${transformChainIdsListToQuery( query: `(${dids}) AND (${transformChainIdsListToQuery(
@ -74,15 +72,25 @@ function SectionQueryResult({
queryData?: string queryData?: string
}) { }) {
const { appConfig } = useSiteMetadata() const { appConfig } = useSiteMetadata()
const { chainIds } = useUserPreferences()
const [result, setResult] = useState<QueryResult>() const [result, setResult] = useState<QueryResult>()
const [loading, setLoading] = useState<boolean>() const [loading, setLoading] = useState<boolean>()
const { chainIds } = useUserPreferences()
useEffect(() => { useEffect(() => {
if (!appConfig.metadataCacheUri) return if (!appConfig.metadataCacheUri) return
const source = axios.CancelToken.source() const source = axios.CancelToken.source()
async function init() { async function init() {
if (chainIds.length === 0) {
const result: QueryResult = {
results: [],
page: 0,
totalPages: 0,
totalResults: 0
}
setResult(result)
setLoading(false)
} else {
try { try {
setLoading(true) setLoading(true)
const result = await queryMetadata(query, source.token) const result = await queryMetadata(query, source.token)
@ -96,7 +104,8 @@ function SectionQueryResult({
setResult(result) setResult(result)
setLoading(false) setLoading(false)
} catch (error) { } catch (error) {
Logger.log(error.message) Logger.error(error.message)
}
} }
} }
init() init()

View File

@ -23,6 +23,7 @@ export default function Debug({
{ {
index: 1, index: 1,
type: values.access, type: values.access,
serviceEndpoint: values.providerUri,
attributes: {} attributes: {}
} }
] ]

View File

@ -11,6 +11,7 @@ import Input from '../../atoms/Input'
import { FormContent, FormFieldProps } from '../../../@types/Form' import { FormContent, FormFieldProps } from '../../../@types/Form'
import { MetadataPublishFormAlgorithm } from '../../../@types/MetaData' import { MetadataPublishFormAlgorithm } from '../../../@types/MetaData'
import { initialValues as initialValuesAlgorithm } from '../../../models/FormAlgoPublish' import { initialValues as initialValuesAlgorithm } from '../../../models/FormAlgoPublish'
import AdvancedSettings from '../../molecules/FormFields/AdvancedSettings'
import FormTitle from './FormTitle' import FormTitle from './FormTitle'
import FormActions from './FormActions' import FormActions from './FormActions'
import styles from './FormPublish.module.css' import styles from './FormPublish.module.css'
@ -33,6 +34,7 @@ const query = graphql`
required required
sortOptions sortOptions
options options
advanced
} }
warning warning
} }
@ -145,6 +147,7 @@ export default function FormPublish(): ReactElement {
{content.data.map( {content.data.map(
(field: FormFieldProps) => (field: FormFieldProps) =>
field.advanced !== true &&
((field.name !== 'entrypoint' && ((field.name !== 'entrypoint' &&
field.name !== 'image' && field.name !== 'image' &&
field.name !== 'containerTag') || field.name !== 'containerTag') ||
@ -164,7 +167,10 @@ export default function FormPublish(): ReactElement {
/> />
) )
)} )}
<AdvancedSettings
content={content}
handleFieldChange={handleFieldChange}
/>
<FormActions <FormActions
isValid={isValid} isValid={isValid}
resetFormAndClearStorage={resetFormAndClearStorage} resetFormAndClearStorage={resetFormAndClearStorage}

View File

@ -1,16 +1,22 @@
import React, { ReactElement, useEffect, FormEvent, ChangeEvent } from 'react' import React, {
ReactElement,
useEffect,
FormEvent,
ChangeEvent,
useState
} from 'react'
import { useStaticQuery, graphql } from 'gatsby' import { useStaticQuery, graphql } from 'gatsby'
import { useFormikContext, Field, Form, FormikContextType } from 'formik' import { useFormikContext, Field, Form, FormikContextType } from 'formik'
import Input from '../../atoms/Input' import Input from '../../atoms/Input'
import { FormContent, FormFieldProps } from '../../../@types/Form' import { FormContent, FormFieldProps } from '../../../@types/Form'
import { MetadataPublishFormDataset } from '../../../@types/MetaData' import { MetadataPublishFormDataset } from '../../../@types/MetaData'
import { initialValues as initialValuesDataset } from '../../../models/FormAlgoPublish' import { initialValues as initialValuesDataset } from '../../../models/FormAlgoPublish'
import { useOcean } from '../../../providers/Ocean'
import { ReactComponent as Download } from '../../../images/download.svg' import { ReactComponent as Download } from '../../../images/download.svg'
import { ReactComponent as Compute } from '../../../images/compute.svg' import { ReactComponent as Compute } from '../../../images/compute.svg'
import FormTitle from './FormTitle' import FormTitle from './FormTitle'
import FormActions from './FormActions' import FormActions from './FormActions'
import styles from './FormPublish.module.css' import styles from './FormPublish.module.css'
import AdvancedSettings from '../../molecules/FormFields/AdvancedSettings'
const query = graphql` const query = graphql`
query { query {
@ -30,6 +36,7 @@ const query = graphql`
required required
sortOptions sortOptions
options options
advanced
} }
warning warning
} }
@ -47,6 +54,7 @@ export default function FormPublish(): ReactElement {
status, status,
setStatus, setStatus,
isValid, isValid,
values,
setErrors, setErrors,
setTouched, setTouched,
resetForm, resetForm,
@ -54,6 +62,8 @@ export default function FormPublish(): ReactElement {
setFieldValue setFieldValue
}: FormikContextType<MetadataPublishFormDataset> = useFormikContext() }: FormikContextType<MetadataPublishFormDataset> = useFormikContext()
const [computeTypeSelected, setComputeTypeSelected] = useState<boolean>(false)
// reset form validation on every mount // reset form validation on every mount
useEffect(() => { useEffect(() => {
setErrors({}) setErrors({})
@ -75,6 +85,8 @@ export default function FormPublish(): ReactElement {
} }
] ]
const computeTypeOptions = ['1 day', '1 week', '1 month', '1 year']
// Manually handle change events instead of using `handleChange` from Formik. // Manually handle change events instead of using `handleChange` from Formik.
// Workaround for default `validateOnChange` not kicking in // Workaround for default `validateOnChange` not kicking in
function handleFieldChange( function handleFieldChange(
@ -84,6 +96,16 @@ export default function FormPublish(): ReactElement {
const value = const value =
field.type === 'terms' ? !JSON.parse(e.target.value) : e.target.value field.type === 'terms' ? !JSON.parse(e.target.value) : e.target.value
if (field.name === 'access' && value === 'Compute') {
setComputeTypeSelected(true)
if (values.timeout === 'Forever')
setFieldValue('timeout', computeTypeOptions[0])
} else {
if (field.name === 'access' && value === 'Download') {
setComputeTypeSelected(false)
}
}
validateField(field.name) validateField(field.name)
setFieldValue(field.name, value) setFieldValue(field.name, value)
} }
@ -105,19 +127,30 @@ export default function FormPublish(): ReactElement {
> >
<FormTitle title={content.title} /> <FormTitle title={content.title} />
{content.data.map((field: FormFieldProps) => ( {content.data.map(
(field: FormFieldProps) =>
field.advanced !== true && (
<Field <Field
key={field.name} key={field.name}
{...field} {...field}
options={ options={
field.type === 'boxSelection' ? accessTypeOptions : field.options field.type === 'boxSelection'
? accessTypeOptions
: field.name === 'timeout' && computeTypeSelected === true
? computeTypeOptions
: field.options
} }
component={Input} component={Input}
onChange={(e: ChangeEvent<HTMLInputElement>) => onChange={(e: ChangeEvent<HTMLInputElement>) =>
handleFieldChange(e, field) handleFieldChange(e, field)
} }
/> />
))} )
)}
<AdvancedSettings
content={content}
handleFieldChange={handleFieldChange}
/>
<FormActions <FormActions
isValid={isValid} isValid={isValid}

View File

@ -132,14 +132,16 @@ export default function PublishPage({
'Publish with ', 'Publish with ',
metadata, metadata,
serviceType, serviceType,
values.dataTokenOptions values.dataTokenOptions,
values.providerUri
) )
const ddo = await publish( const ddo = await publish(
metadata as unknown as Metadata, metadata as unknown as Metadata,
serviceType, serviceType,
values.dataTokenOptions, values.dataTokenOptions,
timeout timeout,
values.providerUri
) )
// Publish failed // Publish failed

View File

@ -32,6 +32,7 @@ interface UseSiteMetadata {
allowFreePricing: string allowFreePricing: string
allowAdvancedSettings: string allowAdvancedSettings: string
credentialType: string credentialType: string
allowAdvancedPublishSettings: string
} }
} }
@ -68,6 +69,7 @@ const query = graphql`
allowDynamicPricing allowDynamicPricing
allowFreePricing allowFreePricing
allowAdvancedSettings allowAdvancedSettings
allowAdvancedPublishSettings
credentialType credentialType
} }
} }

View File

@ -51,5 +51,6 @@ export const initialValues: Partial<MetadataPublishFormAlgorithm> = {
algorithmPrivacy: false, algorithmPrivacy: false,
termsAndConditions: false, termsAndConditions: false,
tags: '', tags: '',
timeout: 'Forever' timeout: 'Forever',
providerUri: ''
} }

View File

@ -25,10 +25,10 @@ export const validationSchema: Yup.SchemaOf<MetadataPublishFormDataset> =
.matches(/Compute|Download/g, { excludeEmptyString: true }) .matches(/Compute|Download/g, { excludeEmptyString: true })
.required('Required'), .required('Required'),
termsAndConditions: Yup.boolean().required('Required'), termsAndConditions: Yup.boolean().required('Required'),
// ---- optional fields ---- // ---- optional fields ----
tags: Yup.string().nullable(), tags: Yup.string().nullable(),
links: Yup.array<FileMetadata[]>().nullable() links: Yup.array<FileMetadata[]>().nullable(),
providerUri: Yup.string().url().nullable()
}) })
.defined() .defined()
@ -44,5 +44,6 @@ export const initialValues: Partial<MetadataPublishFormDataset> = {
timeout: 'Forever', timeout: 'Forever',
access: '', access: '',
termsAndConditions: false, termsAndConditions: false,
tags: '' tags: '',
providerUri: ''
} }

View File

@ -7,9 +7,9 @@ export interface FormTradeData extends PoolBalance {
} }
export interface TradeItem { export interface TradeItem {
amount: number amount: string
token: string token: string
maxAmount: number maxAmount: string
} }
export const initialValues: FormTradeData = { export const initialValues: FormTradeData = {

View File

@ -1,6 +1,5 @@
import { createClient, Provider, Client } from 'urql' import { createClient, Provider, Client } from 'urql'
import React, { useState, useEffect, ReactNode, ReactElement } from 'react' import React, { useState, useEffect, ReactNode, ReactElement } from 'react'
import { useWeb3 } from './Web3'
import { Logger } from '@oceanprotocol/lib' import { Logger } from '@oceanprotocol/lib'
import { getOceanConfig } from '../utils/ocean' import { getOceanConfig } from '../utils/ocean'
@ -22,11 +21,16 @@ export default function UrqlClientProvider({
}: { }: {
children: ReactNode children: ReactNode
}): ReactElement { }): ReactElement {
const { networkId } = useWeb3() //
// Set a default client here based on ETH Mainnet, as that's required for
// urql to work.
// Throughout code base this client is then used and altered by passing
// a new queryContext holding different subgraph URLs.
//
const [client, setClient] = useState<Client>() const [client, setClient] = useState<Client>()
useEffect(() => { useEffect(() => {
const oceanConfig = getOceanConfig(networkId || 1) const oceanConfig = getOceanConfig(1)
if (!oceanConfig?.subgraphUri) { if (!oceanConfig?.subgraphUri) {
Logger.error( Logger.error(
@ -39,8 +43,9 @@ export default function UrqlClientProvider({
urqlClient = newClient urqlClient = newClient
setClient(newClient) setClient(newClient)
Logger.log(`[URQL] Client connected to ${oceanConfig.subgraphUri}`) Logger.log(`[URQL] Client connected to ${oceanConfig.subgraphUri}`)
}, [networkId]) }, [])
return client ? <Provider value={client}>{children}</Provider> : <></> return client ? <Provider value={client}>{children}</Provider> : <></>
} }
export { UrqlClientProvider } export { UrqlClientProvider }

View File

@ -8,7 +8,8 @@ export default function compareAsBN(balance: string, price: string): boolean {
const compare = aBN.comparedTo(bBN) const compare = aBN.comparedTo(bBN)
switch (compare) { switch (compare) {
case 1 || 0: // balance is greater or equal to price case 1: // balance is greater than price
case 0: // balance is equal to price
return true return true
default: default:
return false return false

View File

@ -42,6 +42,7 @@ export function secondsToString(numberOfSeconds: number): string {
if (numberOfSeconds === 0) return 'Forever' if (numberOfSeconds === 0) return 'Forever'
const years = Math.floor(numberOfSeconds / 31536000) const years = Math.floor(numberOfSeconds / 31536000)
const months = Math.floor((numberOfSeconds %= 31536000) / 2630000)
const weeks = Math.floor((numberOfSeconds %= 31536000) / 604800) const weeks = Math.floor((numberOfSeconds %= 31536000) / 604800)
const days = Math.floor((numberOfSeconds %= 604800) / 86400) const days = Math.floor((numberOfSeconds %= 604800) / 86400)
const hours = Math.floor((numberOfSeconds %= 86400) / 3600) const hours = Math.floor((numberOfSeconds %= 86400) / 3600)
@ -50,6 +51,8 @@ export function secondsToString(numberOfSeconds: number): string {
return years return years
? `${years} year${numberEnding(years)}` ? `${years} year${numberEnding(years)}`
: months
? `${months} month${numberEnding(months)}`
: weeks : weeks
? `${weeks} week${numberEnding(weeks)}` ? `${weeks} week${numberEnding(weeks)}`
: days : days

View File

@ -0,0 +1,8 @@
export function isValidNumber(value: any): boolean {
const isUndefinedValue = typeof value === 'undefined'
const isNullValue = value === null
const isNaNValue = isNaN(Number(value))
const isEmptyString = value === ''
return !isUndefinedValue && !isNullValue && !isNaNValue && !isEmptyString
}

View File

@ -141,17 +141,17 @@ const HighestLiquidityAssets = gql`
} }
` `
export function getSubgrahUri(chainId: number): string { export function getSubgraphUri(chainId: number): string {
const config = getOceanConfig(chainId) const config = getOceanConfig(chainId)
return config.subgraphUri return config.subgraphUri
} }
export function getQueryContext(chainId: number): OperationContext { export function getQueryContext(chainId: number): OperationContext {
const queryContext: OperationContext = { const queryContext: OperationContext = {
url: `${getSubgrahUri( url: `${getSubgraphUri(
Number(chainId) Number(chainId)
)}/subgraphs/name/oceanprotocol/ocean-subgraph`, )}/subgraphs/name/oceanprotocol/ocean-subgraph`,
requestPolicy: 'network-only' requestPolicy: 'cache-and-network'
} }
return queryContext return queryContext
@ -180,7 +180,7 @@ export async function fetchDataForMultipleChains(
let datas: any[] = [] let datas: any[] = []
for (const chainId of chainIds) { for (const chainId of chainIds) {
const context: OperationContext = { const context: OperationContext = {
url: `${getSubgrahUri( url: `${getSubgraphUri(
chainId chainId
)}/subgraphs/name/oceanprotocol/ocean-subgraph`, )}/subgraphs/name/oceanprotocol/ocean-subgraph`,
requestPolicy: 'network-only' requestPolicy: 'network-only'
@ -410,6 +410,21 @@ export async function getPrice(asset: DDO): Promise<BestPrice> {
return bestPrice return bestPrice
} }
export async function getSpotPrice(asset: DDO): Promise<number> {
const poolVariables = {
datatokenAddress: asset?.dataToken.toLowerCase()
}
const queryContext = getQueryContext(Number(asset.chainId))
const poolPriceResponse: OperationResult<AssetsPoolPrice> = await fetchData(
AssetPoolPriceQuerry,
poolVariables,
queryContext
)
return poolPriceResponse.data.pools[0].spotPrice
}
export async function getAssetsBestPrices( export async function getAssetsBestPrices(
assets: DDO[] assets: DDO[]
): Promise<AssetListPrices[]> { ): Promise<AssetListPrices[]> {
@ -454,7 +469,7 @@ export async function getAssetsBestPrices(
export async function getHighestLiquidityDIDs( export async function getHighestLiquidityDIDs(
chainIds: number[] chainIds: number[]
): Promise<string> { ): Promise<[string, number]> {
const didList: string[] = [] const didList: string[] = []
let highestLiquidiyAssets: HighestLiquidityAssetsPools[] = [] let highestLiquidiyAssets: HighestLiquidityAssetsPools[] = []
for (const chain of chainIds) { for (const chain of chainIds) {
@ -480,5 +495,5 @@ export async function getHighestLiquidityDIDs(
.replace(/"/g, '') .replace(/"/g, '')
.replace(/(\[|\])/g, '') .replace(/(\[|\])/g, '')
.replace(/(did:op:)/g, '0x') .replace(/(did:op:)/g, '0x')
return searchDids return [searchDids, didList.length]
} }