1
0
mirror of https://github.com/oceanprotocol/market.git synced 2024-06-30 05:41:41 +02: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
#GATSBY_ALLOW_ADVANCED_SETTINGS="true"
#GATSBY_ALLOW_ADVANCED_PUBLISH_SETTINGS="true"
# Allow/Deny Lists
#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 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`

View File

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

View File

@ -5,7 +5,7 @@ documents:
- './src/components/pages/History/PoolShares.tsx'
- './src/components/pages/History/Downloads.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/Graph.tsx'
- './src/components/organisms/AssetActions/Consume.tsx'

View File

@ -91,6 +91,14 @@
"placeholder": "e.g. logistics, ai",
"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",
"label": "Terms & Conditions",

View File

@ -38,6 +38,14 @@
"options": ["Download", "Compute"],
"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",
"label": "Timeout",

View File

@ -12,18 +12,18 @@
"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.",
"tooltips": {
"communityFee": "Explain community fee...",
"marketplaceFee": "Explain marketplace 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": "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": {
"title": "Dynamic",
"info": "Let's create a decentralized, automated market for your data set. The datatoken for this data set will be worth the entered amount of OCEAN. Additionally, you will provide liquidity into a Datatoken/OCEAN liquidity pool with Balancer.",
"tooltips": {
"poolInfo": "Explain what is going on here...",
"swapFee": "Explain liquidity provider fee...",
"communityFee": "Explain community fee...",
"marketplaceFee": "Explain marketplace fee..."
"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": "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": "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": "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": {
@ -33,7 +33,7 @@
},
"pool": {
"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."
},
"add": {

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -63,7 +63,7 @@ export default function Publisher({
) : (
<>
<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."
>
{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 { toast } from 'react-toastify'
import FileInfo from './Info'
import FileInput from './Input'
import CustomInput from '../URLInput/Input'
import { InputProps } from '../../../atoms/Input'
import { fileinfo } from '../../../../utils/provider'
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' ? (
<FileInfo name={props.name} file={field.value[0]} />
) : (
<FileInput
<CustomInput
submitText="Add File"
{...props}
{...field}
isLoading={isLoading}

View File

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

View File

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

View File

@ -38,3 +38,6 @@
.action {
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>
{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'}
</Button>
<ActionError setError={setError} />

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,5 +1,10 @@
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(
ocean: Ocean,
poolAddress: string,
@ -15,9 +20,15 @@ export async function getMaxPercentRemove(
amountMaxOcean
)
let amountMaxPercent = `${Math.floor(
(Number(amountMaxPoolShares) / Number(poolTokens)) * 100
)}`
let amountMaxPercent =
isValidNumber(amountMaxPoolShares) && isValidNumber(poolTokens)
? new Decimal(amountMaxPoolShares)
.dividedBy(new Decimal(poolTokens))
.mul(100)
.floor()
.toString()
: '0'
if (Number(amountMaxPercent) > 100) {
amountMaxPercent = '100'
}

View File

@ -42,8 +42,8 @@ export default function FormTrade({
}: {
ddo: DDO
balance: PoolBalance
maxDt: number
maxOcean: number
maxDt: string
maxOcean: string
price: BestPrice
}): ReactElement {
const data = useStaticQuery(contentQuery)
@ -61,12 +61,18 @@ export default function FormTrade({
const validationSchema: Yup.SchemaOf<FormTradeData> = Yup.object()
.shape({
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}`)
.required('Required')
.nullable(),
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}`)
.required('Required')
.nullable(),
@ -77,7 +83,9 @@ export default function FormTrade({
async function handleTrade(values: FormTradeData) {
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 tx =
values.type === 'buy'

View File

@ -5,6 +5,11 @@ import { useOcean } from '../../../../providers/Ocean'
import Token from '../Pool/Token'
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({
dtSymbol,
poolAddress
@ -25,12 +30,20 @@ export default function Output({
async function getSwapFee() {
const swapFee = await ocean.pool.getSwapFee(poolAddress)
// 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 =
values.type === 'buy'
? Number(swapFee) * values.ocean
: Number(swapFee) * values.datatoken
? isValidNumber(swapFee) && isValidNumber(values.ocean)
? 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())
}
getSwapFee()
@ -46,8 +59,14 @@ export default function Output({
const maxImpact = 1 - Number(values.slippage) / 100
const maxPrice =
values.type === 'buy'
? (values.datatoken * maxImpact).toString()
: (values.ocean * maxImpact).toString()
? isValidNumber(values.datatoken) && isValidNumber(maxImpact)
? 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)
}

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-right: -2rem;
padding: calc(var(--spacer) / 4) var(--spacer);
text-align: center;
color: var(--color-secondary);
display: grid;
grid-template-columns: 1fr 1fr;
gap: calc(var(--spacer) / 3);
}
.slippage strong {
font-weight: var(--font-weight-base);
color: var(--font-color-heading);
text-align: right;
}
.title {
@ -25,6 +27,4 @@
.slippage select {
width: fit-content;
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 React, { ChangeEvent, ReactElement, useEffect, useState } from 'react'
import React, { ChangeEvent, ReactElement } from 'react'
import { FormTradeData, slippagePresets } from '../../../../models/FormTrade'
import InputElement from '../../../atoms/Input/InputElement'
import Tooltip from '../../../atoms/Tooltip'
import styles from './Slippage.module.css'
export default function Slippage(): ReactElement {
@ -14,9 +15,9 @@ export default function Slippage(): ReactElement {
}
return (
<>
<div className={styles.slippage}>
<strong>Expected price impact</strong>
<div className={styles.slippage}>
<strong>Slippage Tolerance</strong>
<div>
<InputElement
name="slippage"
type="select"
@ -27,7 +28,8 @@ export default function Slippage(): ReactElement {
value={values.slippage}
onChange={handleChange}
/>
<Tooltip content="Your transaction will revert if the price changes unfavorably by more than this percentage." />
</div>
</>
</div>
)
}

View File

@ -10,6 +10,11 @@ import Output from './Output'
import Slippage from './Slippage'
import { FormTradeData, TradeItem } from '../../../../models/FormTrade'
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({
ddo,
@ -21,23 +26,23 @@ export default function Swap({
setMaximumOcean
}: {
ddo: DDO
maxDt: number
maxOcean: number
maxDt: string
maxOcean: string
balance: PoolBalance
price: BestPrice
setMaximumDt: (value: number) => void
setMaximumOcean: (value: number) => void
setMaximumDt: (value: string) => void
setMaximumOcean: (value: string) => void
}): ReactElement {
const { ocean } = useOcean()
const [oceanItem, setOceanItem] = useState<TradeItem>({
amount: 0,
amount: '0',
token: 'OCEAN',
maxAmount: 0
maxAmount: '0'
})
const [dtItem, setDtItem] = useState<TradeItem>({
amount: 0,
amount: '0',
token: ddo.dataTokenInfo.symbol,
maxAmount: 0
maxAmount: '0'
})
const {
@ -47,6 +52,11 @@ export default function Swap({
validateForm
}: 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(() => {
if (!ddo || !balance || !values || !price) return
@ -66,32 +76,32 @@ export default function Swap({
const maximumDt =
values.type === 'buy'
? Number(dtAmount) > Number(maxBuyDt)
? Number(maxBuyDt)
: Number(dtAmount)
: Number(dtAmount) > balance.datatoken
? balance.datatoken
: Number(dtAmount)
? new Decimal(maxBuyDt)
: new Decimal(dtAmount)
: Number(dtAmount) > Number(balance.datatoken)
? new Decimal(balance.datatoken)
: new Decimal(dtAmount)
const maximumOcean =
values.type === 'sell'
? Number(oceanAmount) > Number(maxBuyOcean)
? Number(maxBuyOcean)
: Number(oceanAmount)
: Number(oceanAmount) > balance.ocean
? balance.ocean
: Number(oceanAmount)
? new Decimal(maxBuyOcean)
: new Decimal(oceanAmount)
: Number(oceanAmount) > Number(balance.ocean)
? new Decimal(balance.ocean)
: new Decimal(oceanAmount)
setMaximumDt(maximumDt)
setMaximumOcean(maximumOcean)
setMaximumDt(maximumDt.toString())
setMaximumOcean(maximumOcean.toString())
setOceanItem({
...oceanItem,
amount: oceanAmount,
maxAmount: maximumOcean
amount: oceanAmount.toString(),
maxAmount: maximumOcean.toString()
})
setDtItem({
...dtItem,
amount: dtAmount,
maxAmount: maximumDt
amount: dtAmount.toString(),
maxAmount: maximumDt.toString()
})
}
calculateMaximum()
@ -106,16 +116,63 @@ export default function Swap({
}
const handleValueChange = async (name: string, value: number) => {
const newValue =
name === 'ocean'
? values.type === 'sell'
? 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())
let tokenIn = ''
let tokenOut = ''
let newValue
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()
}
@ -139,6 +196,11 @@ export default function Swap({
<Output dtSymbol={dtItem.token} poolAddress={price?.address} />
<PriceImpact
totalValue={totalValue}
tokenAmount={tokenAmount}
spotPrice={spotPrice}
/>
<Slippage />
</div>
)

View File

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

View File

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

View File

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

View File

@ -2,10 +2,10 @@ import React, { ReactElement, useEffect, useState } from 'react'
import { useAsset } from '../../../providers/Asset'
import ExplorerLink from '../../atoms/ExplorerLink'
import Time from '../../atoms/Time'
import styles from './EditHistory.module.css'
import { gql, useQuery } from 'urql'
import { gql, OperationContext, useQuery } from 'urql'
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`
query ReceiptData($address: ID!) {
@ -20,14 +20,32 @@ const getReceipts = gql`
`
export default function EditHistory(): ReactElement {
const { networkId } = useWeb3()
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({
query: getReceipts,
variables: { address: ddo?.dataToken.toLowerCase() }
variables: { address: ddo?.dataToken.toLowerCase() },
context: queryContext,
pause: !ddo || !queryContext
})
const { data } = result
//
// 2. Construct display data based on fetched data.
//
const [receipts, setReceipts] = useState<ReceiptData[]>()
const [creationTx, setCreationTx] = useState<string>()
@ -51,8 +69,7 @@ export default function EditHistory(): ReactElement {
{receipts?.map((receipt) => (
<li key={receipt.id} className={styles.item}>
<ExplorerLink networkId={ddo.chainId} path={`/tx/${receipt.tx}`}>
edited{' '}
<Time date={receipt.timestamp.toString()} relative isUnix />
edited <Time date={`${receipt.timestamp}`} relative isUnix />
</ExplorerLink>
</li>
))}

View File

@ -12,6 +12,11 @@ import { DDO } from '@oceanprotocol/lib'
import FormHelp from '../../../../atoms/Input/Help'
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({
ddo,
setShowPricing,
@ -41,8 +46,15 @@ export default function FormPricing({
useEffect(() => {
if (type === 'fixed') return
const dtAmount =
(Number(oceanAmount) / Number(weightOnOcean) / price) *
Number(weightOnDataToken)
isValidNumber(oceanAmount) &&
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)
}, [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 [showPricing, setShowPricing] = useState(false)
const [showEdit, setShowEdit] = useState<boolean>()
const [isComputeType, setIsComputeType] = useState<boolean>(false)
const [showEditCompute, setShowEditCompute] = useState<boolean>()
const [showEditAdvancedSettings, setShowEditAdvancedSettings] =
useState<boolean>()
@ -63,7 +64,8 @@ export default function AssetContent(props: AssetContentProps): ReactElement {
const isOwner = accountId.toLowerCase() === owner.toLowerCase()
setIsOwner(isOwner)
setShowPricing(isOwner && price.type === '')
}, [accountId, price, owner])
setIsComputeType(Boolean(ddo.findServiceByType('compute')))
}, [accountId, price, owner, ddo])
function handleEditButton() {
// move user's focus to top of screen
@ -82,7 +84,7 @@ export default function AssetContent(props: AssetContentProps): ReactElement {
}
return showEdit ? (
<Edit setShowEdit={setShowEdit} />
<Edit setShowEdit={setShowEdit} isComputeType={isComputeType} />
) : showEditCompute ? (
<EditComputeDataset setShowEdit={setShowEditCompute} />
) : showEditAdvancedSettings ? (

View File

@ -6,6 +6,7 @@ import { DDO } from '@oceanprotocol/lib'
import classNames from 'classnames/bind'
import { getAssetsBestPrices, AssetListPrices } from '../../utils/subgraph'
import Loader from '../atoms/Loader'
import { useUserPreferences } from '../../providers/UserPreferences'
const cx = classNames.bind(styles)
@ -36,6 +37,7 @@ const AssetList: React.FC<AssetListProps> = ({
onPageChange,
className
}) => {
const { chainIds } = useUserPreferences()
const [assetsWithPrices, setAssetWithPrices] = useState<AssetListPrices[]>()
const [loading, setLoading] = useState<boolean>(true)
@ -71,6 +73,8 @@ const AssetList: React.FC<AssetListProps> = ({
key={assetWithPrice.ddo.id}
/>
))
) : chainIds.length === 0 ? (
<div className={styles.empty}>No network selected.</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 axios from 'axios'
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
@ -90,11 +94,16 @@ function Liquidity({ row, type }: { row: Asset; type: string }) {
).toString()
}
if (type === 'pool') {
price = `${
Number(row.poolShare.poolId.oceanReserve) +
Number(row.poolShare.poolId.datatokenReserve) *
row.poolShare.poolId.consumePrice
}`
price =
isValidNumber(row.poolShare.poolId.oceanReserve) &&
isValidNumber(row.poolShare.poolId.datatokenReserve) &&
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()
dataTokenBalance = row.poolShare.poolId.datatokenReserve.toString()
}

View File

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

View File

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

View File

@ -11,6 +11,7 @@ import Input from '../../atoms/Input'
import { FormContent, FormFieldProps } from '../../../@types/Form'
import { MetadataPublishFormAlgorithm } from '../../../@types/MetaData'
import { initialValues as initialValuesAlgorithm } from '../../../models/FormAlgoPublish'
import AdvancedSettings from '../../molecules/FormFields/AdvancedSettings'
import FormTitle from './FormTitle'
import FormActions from './FormActions'
import styles from './FormPublish.module.css'
@ -33,6 +34,7 @@ const query = graphql`
required
sortOptions
options
advanced
}
warning
}
@ -145,6 +147,7 @@ export default function FormPublish(): ReactElement {
{content.data.map(
(field: FormFieldProps) =>
field.advanced !== true &&
((field.name !== 'entrypoint' &&
field.name !== 'image' &&
field.name !== 'containerTag') ||
@ -164,7 +167,10 @@ export default function FormPublish(): ReactElement {
/>
)
)}
<AdvancedSettings
content={content}
handleFieldChange={handleFieldChange}
/>
<FormActions
isValid={isValid}
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 { useFormikContext, Field, Form, FormikContextType } from 'formik'
import Input from '../../atoms/Input'
import { FormContent, FormFieldProps } from '../../../@types/Form'
import { MetadataPublishFormDataset } from '../../../@types/MetaData'
import { initialValues as initialValuesDataset } from '../../../models/FormAlgoPublish'
import { useOcean } from '../../../providers/Ocean'
import { ReactComponent as Download } from '../../../images/download.svg'
import { ReactComponent as Compute } from '../../../images/compute.svg'
import FormTitle from './FormTitle'
import FormActions from './FormActions'
import styles from './FormPublish.module.css'
import AdvancedSettings from '../../molecules/FormFields/AdvancedSettings'
const query = graphql`
query {
@ -30,6 +36,7 @@ const query = graphql`
required
sortOptions
options
advanced
}
warning
}
@ -47,6 +54,7 @@ export default function FormPublish(): ReactElement {
status,
setStatus,
isValid,
values,
setErrors,
setTouched,
resetForm,
@ -54,6 +62,8 @@ export default function FormPublish(): ReactElement {
setFieldValue
}: FormikContextType<MetadataPublishFormDataset> = useFormikContext()
const [computeTypeSelected, setComputeTypeSelected] = useState<boolean>(false)
// reset form validation on every mount
useEffect(() => {
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.
// Workaround for default `validateOnChange` not kicking in
function handleFieldChange(
@ -84,6 +96,16 @@ export default function FormPublish(): ReactElement {
const 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)
setFieldValue(field.name, value)
}
@ -105,19 +127,30 @@ export default function FormPublish(): ReactElement {
>
<FormTitle title={content.title} />
{content.data.map((field: FormFieldProps) => (
<Field
key={field.name}
{...field}
options={
field.type === 'boxSelection' ? accessTypeOptions : field.options
}
component={Input}
onChange={(e: ChangeEvent<HTMLInputElement>) =>
handleFieldChange(e, field)
}
/>
))}
{content.data.map(
(field: FormFieldProps) =>
field.advanced !== true && (
<Field
key={field.name}
{...field}
options={
field.type === 'boxSelection'
? accessTypeOptions
: field.name === 'timeout' && computeTypeSelected === true
? computeTypeOptions
: field.options
}
component={Input}
onChange={(e: ChangeEvent<HTMLInputElement>) =>
handleFieldChange(e, field)
}
/>
)
)}
<AdvancedSettings
content={content}
handleFieldChange={handleFieldChange}
/>
<FormActions
isValid={isValid}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,6 +1,5 @@
import { createClient, Provider, Client } from 'urql'
import React, { useState, useEffect, ReactNode, ReactElement } from 'react'
import { useWeb3 } from './Web3'
import { Logger } from '@oceanprotocol/lib'
import { getOceanConfig } from '../utils/ocean'
@ -22,11 +21,16 @@ export default function UrqlClientProvider({
}: {
children: ReactNode
}): 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>()
useEffect(() => {
const oceanConfig = getOceanConfig(networkId || 1)
const oceanConfig = getOceanConfig(1)
if (!oceanConfig?.subgraphUri) {
Logger.error(
@ -39,8 +43,9 @@ export default function UrqlClientProvider({
urqlClient = newClient
setClient(newClient)
Logger.log(`[URQL] Client connected to ${oceanConfig.subgraphUri}`)
}, [networkId])
}, [])
return client ? <Provider value={client}>{children}</Provider> : <></>
}
export { UrqlClientProvider }

View File

@ -8,7 +8,8 @@ export default function compareAsBN(balance: string, price: string): boolean {
const compare = aBN.comparedTo(bBN)
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
default:
return false

View File

@ -42,6 +42,7 @@ export function secondsToString(numberOfSeconds: number): string {
if (numberOfSeconds === 0) return 'Forever'
const years = Math.floor(numberOfSeconds / 31536000)
const months = Math.floor((numberOfSeconds %= 31536000) / 2630000)
const weeks = Math.floor((numberOfSeconds %= 31536000) / 604800)
const days = Math.floor((numberOfSeconds %= 604800) / 86400)
const hours = Math.floor((numberOfSeconds %= 86400) / 3600)
@ -50,6 +51,8 @@ export function secondsToString(numberOfSeconds: number): string {
return years
? `${years} year${numberEnding(years)}`
: months
? `${months} month${numberEnding(months)}`
: weeks
? `${weeks} week${numberEnding(weeks)}`
: 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)
return config.subgraphUri
}
export function getQueryContext(chainId: number): OperationContext {
const queryContext: OperationContext = {
url: `${getSubgrahUri(
url: `${getSubgraphUri(
Number(chainId)
)}/subgraphs/name/oceanprotocol/ocean-subgraph`,
requestPolicy: 'network-only'
requestPolicy: 'cache-and-network'
}
return queryContext
@ -180,7 +180,7 @@ export async function fetchDataForMultipleChains(
let datas: any[] = []
for (const chainId of chainIds) {
const context: OperationContext = {
url: `${getSubgrahUri(
url: `${getSubgraphUri(
chainId
)}/subgraphs/name/oceanprotocol/ocean-subgraph`,
requestPolicy: 'network-only'
@ -410,6 +410,21 @@ export async function getPrice(asset: DDO): Promise<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(
assets: DDO[]
): Promise<AssetListPrices[]> {
@ -454,7 +469,7 @@ export async function getAssetsBestPrices(
export async function getHighestLiquidityDIDs(
chainIds: number[]
): Promise<string> {
): Promise<[string, number]> {
const didList: string[] = []
let highestLiquidiyAssets: HighestLiquidityAssetsPools[] = []
for (const chain of chainIds) {
@ -480,5 +495,5 @@ export async function getHighestLiquidityDIDs(
.replace(/"/g, '')
.replace(/(\[|\])/g, '')
.replace(/(did:op:)/g, '0x')
return searchDids
return [searchDids, didList.length]
}