mirror of
https://github.com/oceanprotocol/market.git
synced 2024-12-02 05:57:29 +01:00
Swap tokens (#204)
* swap Signed-off-by: mihaisc <mihai.scarlat@smartcontrol.ro> * validation and calculation Signed-off-by: mihaisc <mihai.scarlat@smartcontrol.ro> * refactor Signed-off-by: mihaisc <mihai.scarlat@smartcontrol.ro> * remove unused effect Signed-off-by: mihaisc <mihai.scarlat@smartcontrol.ro> * fix interval Signed-off-by: mihaisc <mihai.scarlat@smartcontrol.ro> * increase refresh timer, remove optional params Signed-off-by: mihaisc <mihai.scarlat@smartcontrol.ro> * make inputs show up without wallet * style fixes * restyling * styling * more styling * fix refresh price Signed-off-by: mihaisc <mihai.scarlat@smartcontrol.ro> * remove test effect * fixes, get data as early as possible from DDO and initial state * refactor * refactor * refactor * label tweaks * copy * typo * prototype output * remove price header * ouput swap fee * fix * spacing * copy * refactor pool transaction titles * copy * update math Signed-off-by: mihaisc <mihai.scarlat@smartcontrol.ro> * use messaging tweaks * tab tweaks, output refactor * fix dark mode selection style * prototype output * method tweaks * slippage to 1%, added warnig banner Signed-off-by: mihaisc <mihai.scarlat@smartcontrol.ro> * form tweaks * error fix * empty inputs by default * longer intervals * maxOcean validation fix * slippage tolerance UI * modified slippage UI * refactor, refresh ocean user balance * move typings/models around * typing fix * fixed output values Signed-off-by: mihaisc <mihai.scarlat@smartcontrol.ro> * bump oceanlib Signed-off-by: mihaisc <mihai.scarlat@smartcontrol.ro> * remove console.log * remove placeholder * tweak * non-web3 browser tweak Co-authored-by: Matthias Kretschmann <m@kretschmann.io>
This commit is contained in:
parent
461fcaf8ae
commit
bb80c4df78
@ -47,5 +47,9 @@
|
|||||||
},
|
},
|
||||||
"action": "Approve & Remove"
|
"action": "Approve & Remove"
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"trade": {
|
||||||
|
"action": "Approve & Swap",
|
||||||
|
"warning": "Use at your own risk. Please familiarize yourself [with the risks](https://blog.oceanprotocol.com/on-staking-on-data-in-ocean-market-3d8e09eb0a13) and the [Terms of Use](/terms)."
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
6
package-lock.json
generated
6
package-lock.json
generated
@ -3550,9 +3550,9 @@
|
|||||||
"integrity": "sha512-p0oOHXr60hXZuLNsQ/PsOQtCfia79thm7MjPxTrnnBvD+csJoHzARYMB0IFj/KTw6U5vLXODgjJAn8x6QksLwg=="
|
"integrity": "sha512-p0oOHXr60hXZuLNsQ/PsOQtCfia79thm7MjPxTrnnBvD+csJoHzARYMB0IFj/KTw6U5vLXODgjJAn8x6QksLwg=="
|
||||||
},
|
},
|
||||||
"@oceanprotocol/lib": {
|
"@oceanprotocol/lib": {
|
||||||
"version": "0.9.12",
|
"version": "0.9.14",
|
||||||
"resolved": "https://registry.npmjs.org/@oceanprotocol/lib/-/lib-0.9.12.tgz",
|
"resolved": "https://registry.npmjs.org/@oceanprotocol/lib/-/lib-0.9.14.tgz",
|
||||||
"integrity": "sha512-R52kWSwwpKNzNHfnNbF6seFPvXEtExK3bWIi4V4eIkgmAf272sa6PVza4mJrtEpTAS1WcJv5ihF7cczIDecxbg==",
|
"integrity": "sha512-UDJpDHqJ5o6O/cOdTuyxhABaebQM1vz+RyfC3zVRSdz5Ke/5xjAChk+3LlYbXBA8VaacxioYa4OlaJLE5uB+Qg==",
|
||||||
"requires": {
|
"requires": {
|
||||||
"@ethereum-navigator/navigator": "^0.5.0",
|
"@ethereum-navigator/navigator": "^0.5.0",
|
||||||
"@oceanprotocol/contracts": "^0.5.7",
|
"@oceanprotocol/contracts": "^0.5.7",
|
||||||
|
@ -24,7 +24,7 @@
|
|||||||
"@coingecko/cryptoformat": "^0.4.2",
|
"@coingecko/cryptoformat": "^0.4.2",
|
||||||
"@loadable/component": "^5.14.1",
|
"@loadable/component": "^5.14.1",
|
||||||
"@oceanprotocol/art": "^3.0.0",
|
"@oceanprotocol/art": "^3.0.0",
|
||||||
"@oceanprotocol/lib": "^0.9.12",
|
"@oceanprotocol/lib": "^0.9.14",
|
||||||
"@oceanprotocol/list-datapartners": "^1.0.3",
|
"@oceanprotocol/list-datapartners": "^1.0.3",
|
||||||
"@oceanprotocol/react": "^0.3.19",
|
"@oceanprotocol/react": "^0.3.19",
|
||||||
"@oceanprotocol/typographies": "^0.1.0",
|
"@oceanprotocol/typographies": "^0.1.0",
|
||||||
|
4
src/@types/TokenBalance.d.ts
vendored
Normal file
4
src/@types/TokenBalance.d.ts
vendored
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
export default interface TokenBalance {
|
||||||
|
ocean: number
|
||||||
|
datatoken: number
|
||||||
|
}
|
@ -26,7 +26,7 @@
|
|||||||
.input::placeholder {
|
.input::placeholder {
|
||||||
font-family: var(--font-family-base);
|
font-family: var(--font-family-base);
|
||||||
font-size: var(--font-size-base);
|
font-size: var(--font-size-base);
|
||||||
color: var(--brand-grey-light);
|
color: var(--color-secondary);
|
||||||
font-weight: var(--font-weight-base);
|
font-weight: var(--font-weight-base);
|
||||||
transition: 0.2s ease-out;
|
transition: 0.2s ease-out;
|
||||||
opacity: 0.7;
|
opacity: 0.7;
|
||||||
@ -167,6 +167,7 @@
|
|||||||
.postfixGroup {
|
.postfixGroup {
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.prefixGroup input {
|
.prefixGroup input {
|
||||||
@ -259,6 +260,31 @@ input[type='range']::-moz-range-track {
|
|||||||
|
|
||||||
/* Size modifiers */
|
/* Size modifiers */
|
||||||
|
|
||||||
|
.mini,
|
||||||
|
.select.mini {
|
||||||
|
font-size: var(--font-size-mini);
|
||||||
|
height: 24px;
|
||||||
|
padding: calc(var(--spacer) / 8);
|
||||||
|
}
|
||||||
|
|
||||||
|
.mini::placeholder {
|
||||||
|
font-size: var(--font-size-mini);
|
||||||
|
}
|
||||||
|
|
||||||
|
.prefix.mini,
|
||||||
|
.postfix.mini {
|
||||||
|
height: 24px;
|
||||||
|
font-size: var(--font-size-mini);
|
||||||
|
}
|
||||||
|
|
||||||
|
.select.mini {
|
||||||
|
padding-right: 2rem;
|
||||||
|
|
||||||
|
/* custom arrow */
|
||||||
|
background-position: calc(100% - 14px) 0.6rem, calc(100% - 9px) 0.6rem, 100% 0;
|
||||||
|
background-size: 5px 5px, 5px 5px, 1.75rem 3rem;
|
||||||
|
}
|
||||||
|
|
||||||
.small,
|
.small,
|
||||||
.select.small {
|
.select.small {
|
||||||
font-size: var(--font-size-small);
|
font-size: var(--font-size-small);
|
||||||
|
@ -26,6 +26,7 @@ const DefaultInput = ({
|
|||||||
export default function InputElement({
|
export default function InputElement({
|
||||||
type,
|
type,
|
||||||
options,
|
options,
|
||||||
|
sortOptions,
|
||||||
name,
|
name,
|
||||||
prefix,
|
prefix,
|
||||||
postfix,
|
postfix,
|
||||||
@ -40,22 +41,25 @@ export default function InputElement({
|
|||||||
const styleClasses = cx({ select: true, [size]: size })
|
const styleClasses = cx({ select: true, [size]: size })
|
||||||
|
|
||||||
switch (type) {
|
switch (type) {
|
||||||
case 'select':
|
case 'select': {
|
||||||
|
const sortedOptions =
|
||||||
|
!sortOptions && sortOptions === false
|
||||||
|
? options
|
||||||
|
: options.sort((a: string, b: string) => a.localeCompare(b))
|
||||||
return (
|
return (
|
||||||
<select id={name} className={styleClasses} {...props}>
|
<select id={name} className={styleClasses} {...props}>
|
||||||
{field !== undefined && field.value === '' && (
|
{field !== undefined && field.value === '' && (
|
||||||
<option value="">---</option>
|
<option value="">---</option>
|
||||||
)}
|
)}
|
||||||
{options &&
|
{sortedOptions &&
|
||||||
options
|
sortedOptions.map((option: string, index: number) => (
|
||||||
.sort((a: string, b: string) => a.localeCompare(b))
|
<option key={index} value={option}>
|
||||||
.map((option: string, index: number) => (
|
{option} {postfix}
|
||||||
<option key={index} value={option}>
|
</option>
|
||||||
{option}
|
))}
|
||||||
</option>
|
|
||||||
))}
|
|
||||||
</select>
|
</select>
|
||||||
)
|
)
|
||||||
|
}
|
||||||
case 'textarea':
|
case 'textarea':
|
||||||
return (
|
return (
|
||||||
<textarea
|
<textarea
|
||||||
|
@ -17,6 +17,7 @@ export interface InputProps {
|
|||||||
tag?: string
|
tag?: string
|
||||||
type?: string
|
type?: string
|
||||||
options?: string[]
|
options?: string[]
|
||||||
|
sortOptions?: boolean
|
||||||
additionalComponent?: ReactElement
|
additionalComponent?: ReactElement
|
||||||
value?: string
|
value?: string
|
||||||
onChange?(
|
onChange?(
|
||||||
@ -39,7 +40,7 @@ export interface InputProps {
|
|||||||
postfix?: string | ReactElement
|
postfix?: string | ReactElement
|
||||||
step?: string
|
step?: string
|
||||||
defaultChecked?: boolean
|
defaultChecked?: boolean
|
||||||
size?: 'small' | 'large' | 'default'
|
size?: 'mini' | 'small' | 'large' | 'default'
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function Input(props: Partial<InputProps>): ReactElement {
|
export default function Input(props: Partial<InputProps>): ReactElement {
|
||||||
|
@ -8,17 +8,30 @@
|
|||||||
.tab {
|
.tab {
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
padding: calc(var(--spacer) / 12) var(--spacer);
|
padding: calc(var(--spacer) / 12) var(--spacer);
|
||||||
border-radius: var(--border-radius);
|
|
||||||
font-weight: var(--font-weight-bold);
|
font-weight: var(--font-weight-bold);
|
||||||
font-size: var(--font-size-small);
|
font-size: var(--font-size-small);
|
||||||
text-transform: uppercase;
|
text-transform: uppercase;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
color: var(--color-secondary);
|
color: var(--color-secondary);
|
||||||
|
border: 1px solid var(--border-color);
|
||||||
|
margin-right: -1px;
|
||||||
|
min-width: 100px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tab:first-child {
|
||||||
|
border-top-left-radius: var(--border-radius);
|
||||||
|
border-bottom-left-radius: var(--border-radius);
|
||||||
|
}
|
||||||
|
|
||||||
|
.tab:last-child {
|
||||||
|
border-top-right-radius: var(--border-radius);
|
||||||
|
border-bottom-right-radius: var(--border-radius);
|
||||||
}
|
}
|
||||||
|
|
||||||
.tab[aria-selected='true'] {
|
.tab[aria-selected='true'] {
|
||||||
background: var(--font-color-heading);
|
background: var(--font-color-heading);
|
||||||
color: var(--background-body);
|
color: var(--background-body);
|
||||||
|
border-color: var(--font-color-heading);
|
||||||
}
|
}
|
||||||
|
|
||||||
.tabContent {
|
.tabContent {
|
||||||
|
12
src/components/atoms/UserLiquidity.module.css
Normal file
12
src/components/atoms/UserLiquidity.module.css
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
.userLiquidity > div {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
font-size: var(--font-size-mini);
|
||||||
|
color: var(--color-secondary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.userLiquidity span + div {
|
||||||
|
transform: scale(0.8);
|
||||||
|
transform-origin: right center;
|
||||||
|
}
|
51
src/components/atoms/UserLiquidity.tsx
Normal file
51
src/components/atoms/UserLiquidity.tsx
Normal file
@ -0,0 +1,51 @@
|
|||||||
|
import React, { ReactElement } from 'react'
|
||||||
|
import PriceUnit from './Price/PriceUnit'
|
||||||
|
import styles from './UserLiquidity.module.css'
|
||||||
|
|
||||||
|
function UserLiquidityLine({
|
||||||
|
title,
|
||||||
|
amount,
|
||||||
|
symbol
|
||||||
|
}: {
|
||||||
|
title: string
|
||||||
|
amount: string
|
||||||
|
symbol: string
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<span>{title}</span>
|
||||||
|
<PriceUnit price={amount} symbol={symbol} small />
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function UserLiquidity({
|
||||||
|
amount,
|
||||||
|
symbol,
|
||||||
|
amountMax,
|
||||||
|
titleAvailable = 'Balance',
|
||||||
|
titleMaximum = 'Maximum'
|
||||||
|
}: {
|
||||||
|
amount: string
|
||||||
|
symbol: string
|
||||||
|
titleAvailable?: string
|
||||||
|
titleMaximum?: string
|
||||||
|
amountMax?: string
|
||||||
|
}): ReactElement {
|
||||||
|
return (
|
||||||
|
<div className={styles.userLiquidity}>
|
||||||
|
<UserLiquidityLine
|
||||||
|
title={titleAvailable}
|
||||||
|
amount={amount}
|
||||||
|
symbol={symbol}
|
||||||
|
/>
|
||||||
|
{amountMax && (
|
||||||
|
<UserLiquidityLine
|
||||||
|
title={titleMaximum}
|
||||||
|
amount={amountMax}
|
||||||
|
symbol={symbol}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
@ -20,13 +20,19 @@ function Title({ row }: { row: PoolTransaction }) {
|
|||||||
const [dtSymbol, setDtSymbol] = useState<string>()
|
const [dtSymbol, setDtSymbol] = useState<string>()
|
||||||
const { locale } = useUserPreferences()
|
const { locale } = useUserPreferences()
|
||||||
|
|
||||||
const title = row.tokenAmountIn
|
const symbol = dtSymbol || 'OCEAN'
|
||||||
? `Add ${formatNumber(Number(row.tokenAmountIn), locale)} ${
|
const title =
|
||||||
dtSymbol || 'OCEAN'
|
row.type === 'join'
|
||||||
}`
|
? `Add ${formatNumber(Number(row.tokenAmountIn), locale)} ${symbol}`
|
||||||
: `Remove ${formatNumber(Number(row.tokenAmountOut), locale)} ${
|
: row.type === 'exit'
|
||||||
dtSymbol || 'OCEAN'
|
? `Remove ${formatNumber(Number(row.tokenAmountOut), locale)} ${symbol}`
|
||||||
}`
|
: `Swap ${formatNumber(
|
||||||
|
Number(row.tokenAmountIn),
|
||||||
|
locale
|
||||||
|
)} ${symbol} for ${formatNumber(
|
||||||
|
Number(row.tokenAmountOut),
|
||||||
|
locale
|
||||||
|
)} ${symbol}`
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!ocean) return
|
if (!ocean) return
|
||||||
|
@ -23,6 +23,6 @@
|
|||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.hasTokens {
|
.help {
|
||||||
composes: hasTokens from './index.module.css';
|
composes: help from './index.module.css';
|
||||||
}
|
}
|
||||||
|
@ -17,8 +17,8 @@
|
|||||||
margin-top: calc(var(--spacer) / 2);
|
margin-top: calc(var(--spacer) / 2);
|
||||||
}
|
}
|
||||||
|
|
||||||
.hasTokens {
|
.help {
|
||||||
composes: hasTokens from './index.module.css';
|
composes: help from './index.module.css';
|
||||||
}
|
}
|
||||||
|
|
||||||
.feedback {
|
.feedback {
|
||||||
|
@ -94,9 +94,23 @@ export default function Consume({
|
|||||||
{consumeStepText || pricingIsLoading ? (
|
{consumeStepText || pricingIsLoading ? (
|
||||||
<Loader message={consumeStepText || pricingStepText} />
|
<Loader message={consumeStepText || pricingStepText} />
|
||||||
) : (
|
) : (
|
||||||
<Button style="primary" onClick={handleConsume} disabled={isDisabled}>
|
<>
|
||||||
{hasDatatoken || hasPreviousOrder ? 'Download' : 'Buy'}
|
<Button style="primary" onClick={handleConsume} disabled={isDisabled}>
|
||||||
</Button>
|
{hasDatatoken || hasPreviousOrder ? 'Download' : 'Buy'}
|
||||||
|
</Button>
|
||||||
|
{hasDatatoken && (
|
||||||
|
<div className={styles.help}>
|
||||||
|
You own {dtBalance} {dtSymbol} allowing you to use this data set
|
||||||
|
without paying again.
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{(!hasDatatoken || !hasPreviousOrder) && (
|
||||||
|
<div className={styles.help}>
|
||||||
|
For using this data set, you will buy 1 {dtSymbol} and immediatly
|
||||||
|
spend it back to the publisher and pool.
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
@ -109,12 +123,6 @@ export default function Consume({
|
|||||||
</div>
|
</div>
|
||||||
<div className={styles.pricewrapper}>
|
<div className={styles.pricewrapper}>
|
||||||
<Price ddo={ddo} conversion />
|
<Price ddo={ddo} conversion />
|
||||||
{hasDatatoken && (
|
|
||||||
<div className={styles.hasTokens}>
|
|
||||||
You own {dtBalance} {dtSymbol} allowing you to use this data set
|
|
||||||
without paying again.
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
{!isInPurgatory && <PurchaseButton />}
|
{!isInPurgatory && <PurchaseButton />}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -3,7 +3,6 @@
|
|||||||
margin-right: -2rem;
|
margin-right: -2rem;
|
||||||
padding-left: var(--spacer);
|
padding-left: var(--spacer);
|
||||||
padding-right: var(--spacer);
|
padding-right: var(--spacer);
|
||||||
margin-top: calc(var(--spacer) / 4);
|
|
||||||
padding-top: calc(var(--spacer) / 1.5);
|
padding-top: calc(var(--spacer) / 1.5);
|
||||||
border-top: 1px solid var(--border-color);
|
border-top: 1px solid var(--border-color);
|
||||||
text-align: center;
|
text-align: center;
|
||||||
|
@ -12,7 +12,8 @@ export default function Actions({
|
|||||||
successMessage,
|
successMessage,
|
||||||
txId,
|
txId,
|
||||||
actionName,
|
actionName,
|
||||||
action
|
action,
|
||||||
|
isDisabled
|
||||||
}: {
|
}: {
|
||||||
isLoading: boolean
|
isLoading: boolean
|
||||||
loaderMessage: string
|
loaderMessage: string
|
||||||
@ -20,6 +21,7 @@ export default function Actions({
|
|||||||
txId: string
|
txId: string
|
||||||
actionName: string
|
actionName: string
|
||||||
action: () => void
|
action: () => void
|
||||||
|
isDisabled?: boolean
|
||||||
}): ReactElement {
|
}): ReactElement {
|
||||||
const { networkId, ocean } = useOcean()
|
const { networkId, ocean } = useOcean()
|
||||||
|
|
||||||
@ -33,7 +35,7 @@ export default function Actions({
|
|||||||
style="primary"
|
style="primary"
|
||||||
size="small"
|
size="small"
|
||||||
onClick={() => action()}
|
onClick={() => action()}
|
||||||
disabled={!ocean}
|
disabled={!ocean || isDisabled}
|
||||||
>
|
>
|
||||||
{actionName}
|
{actionName}
|
||||||
</Button>
|
</Button>
|
||||||
|
@ -4,20 +4,3 @@
|
|||||||
bottom: calc(var(--spacer) / 2);
|
bottom: calc(var(--spacer) / 2);
|
||||||
right: calc(var(--spacer) * 2.5);
|
right: calc(var(--spacer) * 2.5);
|
||||||
}
|
}
|
||||||
|
|
||||||
.userLiquidity > div {
|
|
||||||
display: flex;
|
|
||||||
justify-content: space-between;
|
|
||||||
align-items: center;
|
|
||||||
font-size: var(--font-size-mini);
|
|
||||||
color: var(--color-secondary);
|
|
||||||
}
|
|
||||||
|
|
||||||
.userLiquidity > div:last-child {
|
|
||||||
margin-bottom: calc(var(--spacer) / 4);
|
|
||||||
}
|
|
||||||
|
|
||||||
.userLiquidity span + div {
|
|
||||||
transform: scale(0.8);
|
|
||||||
transform-origin: right center;
|
|
||||||
}
|
|
||||||
|
@ -1,4 +1,3 @@
|
|||||||
import PriceUnit from '../../../../atoms/Price/PriceUnit'
|
|
||||||
import React, { ChangeEvent, ReactElement, useEffect } from 'react'
|
import React, { ChangeEvent, ReactElement, useEffect } from 'react'
|
||||||
import styles from './FormAdd.module.css'
|
import styles from './FormAdd.module.css'
|
||||||
import Input from '../../../../atoms/Input'
|
import Input from '../../../../atoms/Input'
|
||||||
@ -12,7 +11,8 @@ import Button from '../../../../atoms/Button'
|
|||||||
import CoinSelect from '../CoinSelect'
|
import CoinSelect from '../CoinSelect'
|
||||||
import { FormAddLiquidity } from '.'
|
import { FormAddLiquidity } from '.'
|
||||||
import { useOcean } from '@oceanprotocol/react'
|
import { useOcean } from '@oceanprotocol/react'
|
||||||
import { Balance } from '..'
|
import TokenBalance from '../../../../../@types/TokenBalance'
|
||||||
|
import UserLiquidity from '../../../../atoms/UserLiquidity'
|
||||||
|
|
||||||
export default function FormAdd({
|
export default function FormAdd({
|
||||||
coin,
|
coin,
|
||||||
@ -32,7 +32,7 @@ export default function FormAdd({
|
|||||||
amountMax: string
|
amountMax: string
|
||||||
setCoin: (value: string) => void
|
setCoin: (value: string) => void
|
||||||
totalPoolTokens: string
|
totalPoolTokens: string
|
||||||
totalBalance: Balance
|
totalBalance: TokenBalance
|
||||||
poolAddress: string
|
poolAddress: string
|
||||||
setNewPoolTokens: (value: string) => void
|
setNewPoolTokens: (value: string) => void
|
||||||
setNewPoolShare: (value: string) => void
|
setNewPoolShare: (value: string) => void
|
||||||
@ -86,20 +86,11 @@ export default function FormAdd({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div className={styles.userLiquidity}>
|
<UserLiquidity
|
||||||
<div>
|
amount={coin === 'OCEAN' ? balance.ocean : dtBalance}
|
||||||
<span>Available:</span>
|
amountMax={amountMax}
|
||||||
{coin === 'OCEAN' ? (
|
symbol={coin}
|
||||||
<PriceUnit price={balance.ocean} symbol="OCEAN" small />
|
/>
|
||||||
) : (
|
|
||||||
<PriceUnit price={dtBalance} symbol={dtSymbol} small />
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<span>Maximum:</span>
|
|
||||||
<PriceUnit price={amountMax} symbol={coin} small />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<Field name="amount">
|
<Field name="amount">
|
||||||
{({
|
{({
|
||||||
|
@ -22,6 +22,7 @@
|
|||||||
display: grid;
|
display: grid;
|
||||||
gap: var(--spacer);
|
gap: var(--spacer);
|
||||||
grid-template-columns: 1fr 1fr;
|
grid-template-columns: 1fr 1fr;
|
||||||
|
padding-bottom: calc(var(--spacer) / 2);
|
||||||
}
|
}
|
||||||
|
|
||||||
.output p {
|
.output p {
|
||||||
|
@ -2,7 +2,6 @@ import React, { ReactElement, useState, useEffect } from 'react'
|
|||||||
import { useOcean } from '@oceanprotocol/react'
|
import { useOcean } from '@oceanprotocol/react'
|
||||||
import Header from '../Header'
|
import Header from '../Header'
|
||||||
import { toast } from 'react-toastify'
|
import { toast } from 'react-toastify'
|
||||||
import { Balance } from '..'
|
|
||||||
import Actions from '../Actions'
|
import Actions from '../Actions'
|
||||||
import { graphql, useStaticQuery } from 'gatsby'
|
import { graphql, useStaticQuery } from 'gatsby'
|
||||||
import * as Yup from 'yup'
|
import * as Yup from 'yup'
|
||||||
@ -11,6 +10,7 @@ import FormAdd from './FormAdd'
|
|||||||
import styles from './index.module.css'
|
import styles from './index.module.css'
|
||||||
import Token from '../Token'
|
import Token from '../Token'
|
||||||
import Alert from '../../../../atoms/Alert'
|
import Alert from '../../../../atoms/Alert'
|
||||||
|
import TokenBalance from '../../../../../@types/TokenBalance'
|
||||||
import { useUserPreferences } from '../../../../../providers/UserPreferences'
|
import { useUserPreferences } from '../../../../../providers/UserPreferences'
|
||||||
|
|
||||||
const contentQuery = graphql`
|
const contentQuery = graphql`
|
||||||
@ -59,7 +59,7 @@ export default function Add({
|
|||||||
refreshInfo: () => void
|
refreshInfo: () => void
|
||||||
poolAddress: string
|
poolAddress: string
|
||||||
totalPoolTokens: string
|
totalPoolTokens: string
|
||||||
totalBalance: Balance
|
totalBalance: TokenBalance
|
||||||
swapFee: string
|
swapFee: string
|
||||||
dtSymbol: string
|
dtSymbol: string
|
||||||
dtAddress: string
|
dtAddress: string
|
||||||
@ -199,6 +199,7 @@ export default function Add({
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Actions
|
<Actions
|
||||||
|
isDisabled={!isWarningAccepted}
|
||||||
isLoading={isSubmitting}
|
isLoading={isSubmitting}
|
||||||
loaderMessage="Adding Liquidity..."
|
loaderMessage="Adding Liquidity..."
|
||||||
successMessage="Successfully added liquidity."
|
successMessage="Successfully added liquidity."
|
||||||
|
@ -5,16 +5,8 @@
|
|||||||
padding-bottom: calc(var(--spacer) / 2);
|
padding-bottom: calc(var(--spacer) / 2);
|
||||||
}
|
}
|
||||||
|
|
||||||
.userLiquidity {
|
|
||||||
composes: userLiquidity from './Add/FormAdd.module.css';
|
|
||||||
max-width: 12rem;
|
|
||||||
margin-top: -1rem;
|
|
||||||
margin-left: auto;
|
|
||||||
margin-right: auto;
|
|
||||||
margin-bottom: calc(var(--spacer) / 2);
|
|
||||||
}
|
|
||||||
|
|
||||||
.range {
|
.range {
|
||||||
|
margin-top: calc(var(--spacer) / 2);
|
||||||
text-align: center;
|
text-align: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -19,6 +19,7 @@ import { getMaxPercentRemove } from './utils'
|
|||||||
import { graphql, useStaticQuery } from 'gatsby'
|
import { graphql, useStaticQuery } from 'gatsby'
|
||||||
import PriceUnit from '../../../atoms/Price/PriceUnit'
|
import PriceUnit from '../../../atoms/Price/PriceUnit'
|
||||||
import debounce from 'lodash.debounce'
|
import debounce from 'lodash.debounce'
|
||||||
|
import UserLiquidity from '../../../atoms/UserLiquidity'
|
||||||
|
|
||||||
const contentQuery = graphql`
|
const contentQuery = graphql`
|
||||||
query PoolRemoveQuery {
|
query PoolRemoveQuery {
|
||||||
@ -137,7 +138,6 @@ export default function Remove({
|
|||||||
// Check and set outputs when amountPoolShares changes
|
// Check and set outputs when amountPoolShares changes
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!ocean || !poolTokens) return
|
if (!ocean || !poolTokens) return
|
||||||
console.log('eff', amountPoolShares, isAdvanced)
|
|
||||||
getValues.current(amountPoolShares, isAdvanced)
|
getValues.current(amountPoolShares, isAdvanced)
|
||||||
}, [
|
}, [
|
||||||
amountPoolShares,
|
amountPoolShares,
|
||||||
@ -184,12 +184,7 @@ export default function Remove({
|
|||||||
<Header title={content.title} backAction={() => setShowRemove(false)} />
|
<Header title={content.title} backAction={() => setShowRemove(false)} />
|
||||||
|
|
||||||
<form className={styles.removeInput}>
|
<form className={styles.removeInput}>
|
||||||
<div className={styles.userLiquidity}>
|
<UserLiquidity amount={poolTokens} symbol="pool shares" />
|
||||||
<div>
|
|
||||||
<span>Available:</span>
|
|
||||||
<PriceUnit price={poolTokens} symbol="pool shares" small />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className={styles.range}>
|
<div className={styles.range}>
|
||||||
<h3>{amountPercent}%</h3>
|
<h3>{amountPercent}%</h3>
|
||||||
|
@ -46,6 +46,7 @@
|
|||||||
text-align: center;
|
text-align: center;
|
||||||
border-top: 1px solid var(--border-color);
|
border-top: 1px solid var(--border-color);
|
||||||
padding-top: calc(var(--spacer) / 4);
|
padding-top: calc(var(--spacer) / 4);
|
||||||
|
padding-bottom: calc(var(--spacer) / 4);
|
||||||
}
|
}
|
||||||
|
|
||||||
.update:before {
|
.update:before {
|
||||||
|
@ -17,15 +17,11 @@ import EtherscanLink from '../../../atoms/EtherscanLink'
|
|||||||
import Token from './Token'
|
import Token from './Token'
|
||||||
import TokenList from './TokenList'
|
import TokenList from './TokenList'
|
||||||
import { graphql, useStaticQuery } from 'gatsby'
|
import { graphql, useStaticQuery } from 'gatsby'
|
||||||
|
import TokenBalance from '../../../../@types/TokenBalance'
|
||||||
import Transactions from './Transactions'
|
import Transactions from './Transactions'
|
||||||
import Graph, { ChartDataLiqudity } from './Graph'
|
import Graph, { ChartDataLiqudity } from './Graph'
|
||||||
import axios from 'axios'
|
import axios from 'axios'
|
||||||
|
|
||||||
export interface Balance {
|
|
||||||
ocean: number
|
|
||||||
datatoken: number
|
|
||||||
}
|
|
||||||
|
|
||||||
const contentQuery = graphql`
|
const contentQuery = graphql`
|
||||||
query PoolQuery {
|
query PoolQuery {
|
||||||
content: allFile(filter: { relativePath: { eq: "price.json" } }) {
|
content: allFile(filter: { relativePath: { eq: "price.json" } }) {
|
||||||
@ -58,7 +54,7 @@ export default function Pool({ ddo }: { ddo: DDO }): ReactElement {
|
|||||||
|
|
||||||
const [poolTokens, setPoolTokens] = useState<string>()
|
const [poolTokens, setPoolTokens] = useState<string>()
|
||||||
const [totalPoolTokens, setTotalPoolTokens] = useState<string>()
|
const [totalPoolTokens, setTotalPoolTokens] = useState<string>()
|
||||||
const [userLiquidity, setUserLiquidity] = useState<Balance>()
|
const [userLiquidity, setUserLiquidity] = useState<TokenBalance>()
|
||||||
const [swapFee, setSwapFee] = useState<string>()
|
const [swapFee, setSwapFee] = useState<string>()
|
||||||
const [weightOcean, setWeightOcean] = useState<string>()
|
const [weightOcean, setWeightOcean] = useState<string>()
|
||||||
const [weightDt, setWeightDt] = useState<string>()
|
const [weightDt, setWeightDt] = useState<string>()
|
||||||
@ -76,7 +72,7 @@ export default function Pool({ ddo }: { ddo: DDO }): ReactElement {
|
|||||||
creatorTotalLiquidityInOcean,
|
creatorTotalLiquidityInOcean,
|
||||||
setCreatorTotalLiquidityInOcean
|
setCreatorTotalLiquidityInOcean
|
||||||
] = useState(0)
|
] = useState(0)
|
||||||
const [creatorLiquidity, setCreatorLiquidity] = useState<Balance>()
|
const [creatorLiquidity, setCreatorLiquidity] = useState<TokenBalance>()
|
||||||
const [creatorPoolTokens, setCreatorPoolTokens] = useState<string>()
|
const [creatorPoolTokens, setCreatorPoolTokens] = useState<string>()
|
||||||
const [creatorPoolShare, setCreatorPoolShare] = useState<string>()
|
const [creatorPoolShare, setCreatorPoolShare] = useState<string>()
|
||||||
const [graphData, setGraphData] = useState<ChartDataLiqudity>()
|
const [graphData, setGraphData] = useState<ChartDataLiqudity>()
|
||||||
@ -84,6 +80,12 @@ export default function Pool({ ddo }: { ddo: DDO }): ReactElement {
|
|||||||
// the purpose of the value is just to trigger the effect
|
// the purpose of the value is just to trigger the effect
|
||||||
const [refreshPool, setRefreshPool] = useState(false)
|
const [refreshPool, setRefreshPool] = useState(false)
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
// Re-fetch price periodically, triggering re-calculation of everything
|
||||||
|
const interval = setInterval(() => refreshPrice(), refreshInterval)
|
||||||
|
return () => clearInterval(interval)
|
||||||
|
}, [ddo, refreshPrice])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setIsRemoveDisabled(isInPurgatory && owner === accountId)
|
setIsRemoveDisabled(isInPurgatory && owner === accountId)
|
||||||
}, [isInPurgatory, owner, accountId])
|
}, [isInPurgatory, owner, accountId])
|
||||||
|
@ -0,0 +1,5 @@
|
|||||||
|
.alertWrap {
|
||||||
|
min-height: 320px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
155
src/components/organisms/AssetActions/Trade/FormTrade.tsx
Normal file
155
src/components/organisms/AssetActions/Trade/FormTrade.tsx
Normal file
@ -0,0 +1,155 @@
|
|||||||
|
import React, { ReactElement, useState } from 'react'
|
||||||
|
import { useOcean } from '@oceanprotocol/react'
|
||||||
|
import { BestPrice, DDO, Logger } from '@oceanprotocol/lib'
|
||||||
|
import * as Yup from 'yup'
|
||||||
|
import { Formik } from 'formik'
|
||||||
|
import Actions from '../Pool/Actions'
|
||||||
|
import { graphql, useStaticQuery } from 'gatsby'
|
||||||
|
import { useUserPreferences } from '../../../../providers/UserPreferences'
|
||||||
|
import { toast } from 'react-toastify'
|
||||||
|
import Swap from './Swap'
|
||||||
|
import TokenBalance from '../../../../@types/TokenBalance'
|
||||||
|
import Alert from '../../../atoms/Alert'
|
||||||
|
import styles from './FormTrade.module.css'
|
||||||
|
import { FormTradeData, initialValues } from '../../../../models/FormTrade'
|
||||||
|
|
||||||
|
const contentQuery = graphql`
|
||||||
|
query TradeQuery {
|
||||||
|
content: allFile(filter: { relativePath: { eq: "price.json" } }) {
|
||||||
|
edges {
|
||||||
|
node {
|
||||||
|
childContentJson {
|
||||||
|
trade {
|
||||||
|
action
|
||||||
|
warning
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`
|
||||||
|
|
||||||
|
export default function FormTrade({
|
||||||
|
ddo,
|
||||||
|
balance,
|
||||||
|
maxDt,
|
||||||
|
maxOcean,
|
||||||
|
price
|
||||||
|
}: {
|
||||||
|
ddo: DDO
|
||||||
|
balance: TokenBalance
|
||||||
|
maxDt: number
|
||||||
|
maxOcean: number
|
||||||
|
price: BestPrice
|
||||||
|
}): ReactElement {
|
||||||
|
const data = useStaticQuery(contentQuery)
|
||||||
|
const content = data.content.edges[0].node.childContentJson.trade
|
||||||
|
const { ocean, accountId } = useOcean()
|
||||||
|
const { debug } = useUserPreferences()
|
||||||
|
const [txId, setTxId] = useState<string>()
|
||||||
|
|
||||||
|
const [maximumOcean, setMaximumOcean] = useState(maxOcean)
|
||||||
|
const [maximumDt, setMaximumDt] = useState(maxDt)
|
||||||
|
const [isWarningAccepted, setIsWarningAccepted] = useState(false)
|
||||||
|
|
||||||
|
const validationSchema = Yup.object().shape<FormTradeData>({
|
||||||
|
ocean: Yup.number()
|
||||||
|
.max(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(maxDt, `Must be less or equal than ${maximumDt}`)
|
||||||
|
.min(0.00001, (param) => `Must be more or equal to ${param.min}`)
|
||||||
|
.required('Required')
|
||||||
|
.nullable(),
|
||||||
|
type: Yup.string(),
|
||||||
|
slippage: Yup.string()
|
||||||
|
})
|
||||||
|
|
||||||
|
async function handleTrade(values: FormTradeData) {
|
||||||
|
try {
|
||||||
|
const tx =
|
||||||
|
values.type === 'buy'
|
||||||
|
? // ? await ocean.pool.buyDT(
|
||||||
|
// accountId,
|
||||||
|
// price.address,
|
||||||
|
// values.datatoken.toString(),
|
||||||
|
// (values.ocean * 1.01).toString()
|
||||||
|
// )
|
||||||
|
await ocean.pool.buyDTWithExactOcean(
|
||||||
|
accountId,
|
||||||
|
price.address,
|
||||||
|
(values.datatoken * 0.99).toString(),
|
||||||
|
values.ocean.toString()
|
||||||
|
)
|
||||||
|
: await ocean.pool.sellDT(
|
||||||
|
accountId,
|
||||||
|
price.address,
|
||||||
|
values.datatoken.toString(),
|
||||||
|
(values.ocean * 0.99).toString()
|
||||||
|
)
|
||||||
|
|
||||||
|
setTxId(tx?.transactionHash)
|
||||||
|
} catch (error) {
|
||||||
|
Logger.error(error.message)
|
||||||
|
toast.error(error.message)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Formik
|
||||||
|
initialValues={initialValues}
|
||||||
|
validationSchema={validationSchema}
|
||||||
|
onSubmit={async (values, { setSubmitting, resetForm }) => {
|
||||||
|
await handleTrade(values)
|
||||||
|
resetForm()
|
||||||
|
setSubmitting(false)
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{({ isSubmitting, submitForm, values }) => (
|
||||||
|
<>
|
||||||
|
{isWarningAccepted ? (
|
||||||
|
<Swap
|
||||||
|
ddo={ddo}
|
||||||
|
balance={balance}
|
||||||
|
maxDt={maxDt}
|
||||||
|
maxOcean={maxOcean}
|
||||||
|
price={price}
|
||||||
|
setMaximumOcean={setMaximumOcean}
|
||||||
|
setMaximumDt={setMaximumDt}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<div className={styles.alertWrap}>
|
||||||
|
<Alert
|
||||||
|
text={content.warning}
|
||||||
|
state="info"
|
||||||
|
action={{
|
||||||
|
name: 'I understand',
|
||||||
|
style: 'text',
|
||||||
|
handleAction: () => setIsWarningAccepted(true)
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<Actions
|
||||||
|
isDisabled={!isWarningAccepted}
|
||||||
|
isLoading={isSubmitting}
|
||||||
|
loaderMessage="Swapping tokens..."
|
||||||
|
successMessage="Successfully swapped tokens."
|
||||||
|
actionName={content.action}
|
||||||
|
action={submitForm}
|
||||||
|
txId={txId}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{debug && (
|
||||||
|
<pre>
|
||||||
|
<code>{JSON.stringify(values, null, 2)}</code>
|
||||||
|
</pre>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</Formik>
|
||||||
|
)
|
||||||
|
}
|
@ -0,0 +1,22 @@
|
|||||||
|
.output {
|
||||||
|
border-top: 1px solid var(--border-color);
|
||||||
|
padding: var(--spacer);
|
||||||
|
display: grid;
|
||||||
|
gap: var(--spacer);
|
||||||
|
grid-template-columns: 1fr 1fr;
|
||||||
|
margin-left: -2rem;
|
||||||
|
margin-right: -2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.output p {
|
||||||
|
font-weight: var(--font-weight-bold);
|
||||||
|
margin-bottom: calc(var(--spacer) / 8);
|
||||||
|
}
|
||||||
|
|
||||||
|
.output [class*='token'] {
|
||||||
|
white-space: normal;
|
||||||
|
}
|
||||||
|
|
||||||
|
.output [class*='token'] > figure {
|
||||||
|
display: none;
|
||||||
|
}
|
77
src/components/organisms/AssetActions/Trade/Output.tsx
Normal file
77
src/components/organisms/AssetActions/Trade/Output.tsx
Normal file
@ -0,0 +1,77 @@
|
|||||||
|
import { useOcean } from '@oceanprotocol/react'
|
||||||
|
import { FormikContextType, useFormikContext } from 'formik'
|
||||||
|
import React, { ReactElement, useEffect, useState } from 'react'
|
||||||
|
import { FormTradeData } from '../../../../models/FormTrade'
|
||||||
|
import Token from '../Pool/Token'
|
||||||
|
import styles from './Output.module.css'
|
||||||
|
|
||||||
|
export default function Output({
|
||||||
|
dtSymbol,
|
||||||
|
poolAddress
|
||||||
|
}: {
|
||||||
|
dtSymbol: string
|
||||||
|
poolAddress: string
|
||||||
|
}): ReactElement {
|
||||||
|
const { ocean } = useOcean()
|
||||||
|
const [maxOutput, setMaxOutput] = useState<string>()
|
||||||
|
const [swapFee, setSwapFee] = useState<string>()
|
||||||
|
const [swapFeeValue, setSwapFeeValue] = useState<string>()
|
||||||
|
// Connect with form
|
||||||
|
const { values }: FormikContextType<FormTradeData> = useFormikContext()
|
||||||
|
|
||||||
|
// Get swap fee
|
||||||
|
useEffect(() => {
|
||||||
|
if (!ocean || !poolAddress) return
|
||||||
|
|
||||||
|
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}`)
|
||||||
|
const value =
|
||||||
|
values.type === 'buy'
|
||||||
|
? Number(swapFee) * values.ocean
|
||||||
|
: Number(swapFee) * values.datatoken
|
||||||
|
setSwapFeeValue(value.toString())
|
||||||
|
}
|
||||||
|
getSwapFee()
|
||||||
|
}, [ocean, poolAddress, values])
|
||||||
|
|
||||||
|
// Get output values
|
||||||
|
useEffect(() => {
|
||||||
|
if (!ocean || !poolAddress) return
|
||||||
|
|
||||||
|
async function getOutput() {
|
||||||
|
// Minimum received
|
||||||
|
// TODO: check if this here is redundant cause we call some of that already in Swap.tsx
|
||||||
|
const maxImpact = 1 - Number(values.slippage) / 100
|
||||||
|
const maxPrice =
|
||||||
|
values.type === 'buy'
|
||||||
|
? (values.datatoken * maxImpact).toString()
|
||||||
|
: (values.ocean * maxImpact).toString()
|
||||||
|
|
||||||
|
setMaxOutput(maxPrice)
|
||||||
|
}
|
||||||
|
getOutput()
|
||||||
|
}, [ocean, poolAddress, values])
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={styles.output}>
|
||||||
|
<div>
|
||||||
|
<p>Minimum Received</p>
|
||||||
|
<Token
|
||||||
|
symbol={values.type === 'buy' ? dtSymbol : 'OCEAN'}
|
||||||
|
balance={maxOutput}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<p>Swap fee</p>
|
||||||
|
<Token
|
||||||
|
symbol={`${values.type === 'buy' ? `OCEAN` : dtSymbol} ${
|
||||||
|
swapFee ? `(${swapFee}%)` : ''
|
||||||
|
}`}
|
||||||
|
balance={swapFeeValue}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
@ -0,0 +1,30 @@
|
|||||||
|
.slippage {
|
||||||
|
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);
|
||||||
|
text-align: center;
|
||||||
|
color: var(--color-secondary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.slippage strong {
|
||||||
|
font-weight: var(--font-weight-base);
|
||||||
|
color: var(--font-color-heading);
|
||||||
|
}
|
||||||
|
|
||||||
|
.title {
|
||||||
|
font-family: var(--font-family-base);
|
||||||
|
font-weight: var(--font-weight-base);
|
||||||
|
font-size: var(--font-size-mini);
|
||||||
|
text-align: center;
|
||||||
|
margin-bottom: calc(var(--spacer) / 4);
|
||||||
|
color: var(--color-secondary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.slippage select {
|
||||||
|
width: fit-content;
|
||||||
|
display: inline-block;
|
||||||
|
margin-left: calc(var(--spacer) / 4);
|
||||||
|
margin-right: calc(var(--spacer) / 4);
|
||||||
|
}
|
35
src/components/organisms/AssetActions/Trade/Slippage.tsx
Normal file
35
src/components/organisms/AssetActions/Trade/Slippage.tsx
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
import { FormikContextType, useFormikContext } from 'formik'
|
||||||
|
import React, { ChangeEvent, ReactElement, useEffect, useState } from 'react'
|
||||||
|
import { FormTradeData, slippagePresets } from '../../../../models/FormTrade'
|
||||||
|
import InputElement from '../../../atoms/Input/InputElement'
|
||||||
|
import styles from './Slippage.module.css'
|
||||||
|
|
||||||
|
export default function Slippage(): ReactElement {
|
||||||
|
// Connect with form
|
||||||
|
const {
|
||||||
|
setFieldValue,
|
||||||
|
values
|
||||||
|
}: FormikContextType<FormTradeData> = useFormikContext()
|
||||||
|
|
||||||
|
function handleChange(e: ChangeEvent<HTMLSelectElement>) {
|
||||||
|
setFieldValue('slippage', e.target.value)
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<div className={styles.slippage}>
|
||||||
|
<strong>Expected price impact</strong>
|
||||||
|
<InputElement
|
||||||
|
name="slippage"
|
||||||
|
type="select"
|
||||||
|
size="mini"
|
||||||
|
postfix="%"
|
||||||
|
sortOptions={false}
|
||||||
|
options={slippagePresets}
|
||||||
|
value={values.slippage}
|
||||||
|
onChange={handleChange}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
24
src/components/organisms/AssetActions/Trade/Swap.module.css
Normal file
24
src/components/organisms/AssetActions/Trade/Swap.module.css
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
.swap {
|
||||||
|
margin-top: -2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.swapButton,
|
||||||
|
.swapButton:hover {
|
||||||
|
padding: 0;
|
||||||
|
display: block;
|
||||||
|
width: calc(100% + 4rem);
|
||||||
|
text-align: center;
|
||||||
|
margin-left: -2rem;
|
||||||
|
margin-right: -2rem;
|
||||||
|
border-top: 1px solid var(--border-color);
|
||||||
|
border-bottom: 1px solid var(--border-color);
|
||||||
|
padding: calc(var(--spacer) / 3) 0 calc(var(--spacer) / 6) 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.swapButton svg {
|
||||||
|
display: inline-block;
|
||||||
|
width: var(--font-size-large);
|
||||||
|
height: var(--font-size-large);
|
||||||
|
fill: var(--brand-pink);
|
||||||
|
transform: rotate(90deg);
|
||||||
|
}
|
145
src/components/organisms/AssetActions/Trade/Swap.tsx
Normal file
145
src/components/organisms/AssetActions/Trade/Swap.tsx
Normal file
@ -0,0 +1,145 @@
|
|||||||
|
import React, { ReactElement, useEffect, useState } from 'react'
|
||||||
|
import { useOcean } from '@oceanprotocol/react'
|
||||||
|
import { BestPrice, DDO } from '@oceanprotocol/lib'
|
||||||
|
import styles from './Swap.module.css'
|
||||||
|
import TradeInput from './TradeInput'
|
||||||
|
import Button from '../../../atoms/Button'
|
||||||
|
import { ReactComponent as Arrow } from '../../../../images/arrow.svg'
|
||||||
|
import { FormikContextType, useFormikContext } from 'formik'
|
||||||
|
import TokenBalance from '../../../../@types/TokenBalance'
|
||||||
|
import Output from './Output'
|
||||||
|
import Slippage from './Slippage'
|
||||||
|
import { FormTradeData, TradeItem } from '../../../../models/FormTrade'
|
||||||
|
|
||||||
|
export default function Swap({
|
||||||
|
ddo,
|
||||||
|
maxDt,
|
||||||
|
maxOcean,
|
||||||
|
balance,
|
||||||
|
price,
|
||||||
|
setMaximumDt,
|
||||||
|
setMaximumOcean
|
||||||
|
}: {
|
||||||
|
ddo: DDO
|
||||||
|
maxDt: number
|
||||||
|
maxOcean: number
|
||||||
|
balance: TokenBalance
|
||||||
|
price: BestPrice
|
||||||
|
setMaximumDt: (value: number) => void
|
||||||
|
setMaximumOcean: (value: number) => void
|
||||||
|
}): ReactElement {
|
||||||
|
const { ocean } = useOcean()
|
||||||
|
const [oceanItem, setOceanItem] = useState<TradeItem>({
|
||||||
|
amount: 0,
|
||||||
|
token: 'OCEAN',
|
||||||
|
maxAmount: 0
|
||||||
|
})
|
||||||
|
const [dtItem, setDtItem] = useState<TradeItem>({
|
||||||
|
amount: 0,
|
||||||
|
token: ddo.dataTokenInfo.symbol,
|
||||||
|
maxAmount: 0
|
||||||
|
})
|
||||||
|
|
||||||
|
const {
|
||||||
|
setFieldValue,
|
||||||
|
values,
|
||||||
|
setErrors,
|
||||||
|
validateForm
|
||||||
|
}: FormikContextType<FormTradeData> = useFormikContext()
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!ddo || !balance || !values || !price) return
|
||||||
|
|
||||||
|
async function calculateMaximum() {
|
||||||
|
const dtAmount = values.type === 'buy' ? maxDt : balance.datatoken
|
||||||
|
const oceanAmount = values.type === 'buy' ? balance.ocean : maxOcean
|
||||||
|
|
||||||
|
const maxBuyOcean = await ocean.pool.getOceanReceived(
|
||||||
|
price.address,
|
||||||
|
dtAmount.toString()
|
||||||
|
)
|
||||||
|
const maxBuyDt = await ocean.pool.getDTReceived(
|
||||||
|
price.address,
|
||||||
|
oceanAmount.toString()
|
||||||
|
)
|
||||||
|
|
||||||
|
const maximumDt =
|
||||||
|
values.type === 'buy'
|
||||||
|
? Number(dtAmount) > Number(maxBuyDt)
|
||||||
|
? Number(maxBuyDt)
|
||||||
|
: Number(dtAmount)
|
||||||
|
: Number(dtAmount) > balance.datatoken
|
||||||
|
? balance.datatoken
|
||||||
|
: Number(dtAmount)
|
||||||
|
|
||||||
|
const maximumOcean =
|
||||||
|
values.type === 'sell'
|
||||||
|
? Number(oceanAmount) > Number(maxBuyOcean)
|
||||||
|
? Number(maxBuyOcean)
|
||||||
|
: Number(oceanAmount)
|
||||||
|
: Number(oceanAmount) > balance.ocean
|
||||||
|
? balance.ocean
|
||||||
|
: Number(oceanAmount)
|
||||||
|
|
||||||
|
setMaximumDt(maximumDt)
|
||||||
|
setMaximumOcean(maximumOcean)
|
||||||
|
setOceanItem({
|
||||||
|
...oceanItem,
|
||||||
|
amount: oceanAmount,
|
||||||
|
maxAmount: maximumOcean
|
||||||
|
})
|
||||||
|
setDtItem({
|
||||||
|
...dtItem,
|
||||||
|
amount: dtAmount,
|
||||||
|
maxAmount: maximumDt
|
||||||
|
})
|
||||||
|
}
|
||||||
|
calculateMaximum()
|
||||||
|
}, [ddo, maxOcean, maxDt, balance, price?.value, values.type])
|
||||||
|
|
||||||
|
const switchTokens = () => {
|
||||||
|
setFieldValue('type', values.type === 'buy' ? 'sell' : 'buy')
|
||||||
|
// don't reset form because we don't want to reset type
|
||||||
|
setFieldValue('datatoken', 0)
|
||||||
|
setFieldValue('ocean', 0)
|
||||||
|
setErrors({})
|
||||||
|
}
|
||||||
|
|
||||||
|
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())
|
||||||
|
|
||||||
|
setFieldValue(name === 'ocean' ? 'datatoken' : 'ocean', newValue)
|
||||||
|
validateForm()
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={styles.swap}>
|
||||||
|
<TradeInput
|
||||||
|
name={values.type === 'sell' ? 'datatoken' : 'ocean'}
|
||||||
|
item={values.type === 'sell' ? dtItem : oceanItem}
|
||||||
|
handleValueChange={handleValueChange}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Button className={styles.swapButton} style="text" onClick={switchTokens}>
|
||||||
|
<Arrow />
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
<TradeInput
|
||||||
|
name={values.type === 'sell' ? 'ocean' : 'datatoken'}
|
||||||
|
item={values.type === 'sell' ? oceanItem : dtItem}
|
||||||
|
handleValueChange={handleValueChange}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Output dtSymbol={dtItem.token} poolAddress={price?.address} />
|
||||||
|
|
||||||
|
<Slippage />
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
@ -0,0 +1,35 @@
|
|||||||
|
.tradeInput {
|
||||||
|
position: relative;
|
||||||
|
padding: var(--spacer) calc(var(--spacer) * 2);
|
||||||
|
margin-left: -2rem;
|
||||||
|
margin-right: -2rem;
|
||||||
|
background: var(--background-highlight);
|
||||||
|
}
|
||||||
|
|
||||||
|
.tradeInput input {
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tradeInput div[class*='field'] {
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tradeInput div[class*='prefix'] {
|
||||||
|
min-width: 6.5rem;
|
||||||
|
width: fit-content;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.label {
|
||||||
|
font-family: var(--font-family-heading);
|
||||||
|
font-size: var(--font-size-base);
|
||||||
|
text-align: center;
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.buttonMax {
|
||||||
|
position: absolute;
|
||||||
|
font-size: var(--font-size-mini);
|
||||||
|
bottom: calc(var(--spacer) / 2);
|
||||||
|
right: calc(var(--spacer) * 2);
|
||||||
|
}
|
86
src/components/organisms/AssetActions/Trade/TradeInput.tsx
Normal file
86
src/components/organisms/AssetActions/Trade/TradeInput.tsx
Normal file
@ -0,0 +1,86 @@
|
|||||||
|
import React, { ChangeEvent, ReactElement } from 'react'
|
||||||
|
import styles from './TradeInput.module.css'
|
||||||
|
|
||||||
|
import {
|
||||||
|
Field,
|
||||||
|
FieldInputProps,
|
||||||
|
FormikContextType,
|
||||||
|
useFormikContext
|
||||||
|
} from 'formik'
|
||||||
|
|
||||||
|
import { useOcean } from '@oceanprotocol/react'
|
||||||
|
import Input from '../../../atoms/Input'
|
||||||
|
import Button from '../../../atoms/Button'
|
||||||
|
import UserLiquidity from '../../../atoms/UserLiquidity'
|
||||||
|
import { FormTradeData, TradeItem } from '../../../../models/FormTrade'
|
||||||
|
|
||||||
|
export default function TradeInput({
|
||||||
|
name,
|
||||||
|
item,
|
||||||
|
handleValueChange
|
||||||
|
}: {
|
||||||
|
name: string
|
||||||
|
item: TradeItem
|
||||||
|
handleValueChange: (name: string, value: number) => void
|
||||||
|
}): ReactElement {
|
||||||
|
const { ocean } = useOcean()
|
||||||
|
|
||||||
|
// Connect with form
|
||||||
|
const {
|
||||||
|
handleChange,
|
||||||
|
setFieldValue,
|
||||||
|
validateForm,
|
||||||
|
values
|
||||||
|
}: FormikContextType<FormTradeData> = useFormikContext()
|
||||||
|
|
||||||
|
const isTopField =
|
||||||
|
(name === 'ocean' && values.type === 'buy') ||
|
||||||
|
(name === 'datatoken' && values.type === 'sell')
|
||||||
|
const titleAvailable = isTopField ? `Balance` : `Available from pool`
|
||||||
|
const titleMaximum = isTopField ? `Maximum to spend` : `Maximum to receive`
|
||||||
|
|
||||||
|
return (
|
||||||
|
<section className={styles.tradeInput}>
|
||||||
|
<UserLiquidity
|
||||||
|
amount={`${item?.amount}`}
|
||||||
|
amountMax={`${item?.maxAmount}`}
|
||||||
|
symbol={item?.token}
|
||||||
|
titleAvailable={titleAvailable}
|
||||||
|
titleMaximum={titleMaximum}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Field name={name}>
|
||||||
|
{({ field, form }: { field: FieldInputProps<number>; form: any }) => (
|
||||||
|
<Input
|
||||||
|
type="number"
|
||||||
|
max={`${item?.maxAmount}`}
|
||||||
|
prefix={item?.token}
|
||||||
|
placeholder="0"
|
||||||
|
field={field}
|
||||||
|
form={form}
|
||||||
|
value={`${field.value}`}
|
||||||
|
onChange={(e: ChangeEvent<HTMLInputElement>) => {
|
||||||
|
handleValueChange(name, Number(e.target.value))
|
||||||
|
validateForm()
|
||||||
|
handleChange(e)
|
||||||
|
}}
|
||||||
|
disabled={!ocean}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</Field>
|
||||||
|
{!isTopField && (
|
||||||
|
<Button
|
||||||
|
className={styles.buttonMax}
|
||||||
|
style="text"
|
||||||
|
size="small"
|
||||||
|
onClick={() => {
|
||||||
|
setFieldValue(name, item?.maxAmount)
|
||||||
|
handleValueChange(name, item?.maxAmount)
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Use Max
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</section>
|
||||||
|
)
|
||||||
|
}
|
69
src/components/organisms/AssetActions/Trade/index.tsx
Normal file
69
src/components/organisms/AssetActions/Trade/index.tsx
Normal file
@ -0,0 +1,69 @@
|
|||||||
|
import React, { ReactElement, useEffect, useState } from 'react'
|
||||||
|
import { useOcean, useMetadata } from '@oceanprotocol/react'
|
||||||
|
import { DDO } from '@oceanprotocol/lib'
|
||||||
|
import FormTrade from './FormTrade'
|
||||||
|
import TokenBalance from '../../../../@types/TokenBalance'
|
||||||
|
|
||||||
|
const refreshInterval = 6000 // 6 sec, if the interval is bellow 3-5 seconds the price will be 0 all the time
|
||||||
|
|
||||||
|
export default function Trade({ ddo }: { ddo: DDO }): ReactElement {
|
||||||
|
const { ocean, balance, accountId, networkId, refreshBalance } = useOcean()
|
||||||
|
const [tokenBalance, setTokenBalance] = useState<TokenBalance>()
|
||||||
|
const { price, refreshPrice } = useMetadata(ddo)
|
||||||
|
const [maxDt, setMaxDt] = useState(0)
|
||||||
|
const [maxOcean, setMaxOcean] = useState(0)
|
||||||
|
|
||||||
|
// Get datatoken balance, and combine with OCEAN balance from hooks into one object
|
||||||
|
useEffect(() => {
|
||||||
|
if (!ocean || !balance?.ocean || !accountId || !ddo?.dataToken) return
|
||||||
|
|
||||||
|
async function getTokenBalance() {
|
||||||
|
const dtBalance = await ocean.datatokens.balance(ddo.dataToken, accountId)
|
||||||
|
setTokenBalance({
|
||||||
|
ocean: Number(balance.ocean),
|
||||||
|
datatoken: Number(dtBalance)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
getTokenBalance()
|
||||||
|
}, [balance.ocean, ocean, accountId, ddo.dataToken])
|
||||||
|
|
||||||
|
// Re-fetch price & balance periodically, triggering re-calculation of everything
|
||||||
|
useEffect(() => {
|
||||||
|
if (!ocean || !networkId || !accountId) return
|
||||||
|
|
||||||
|
const interval = setInterval(async () => {
|
||||||
|
refreshPrice()
|
||||||
|
refreshBalance()
|
||||||
|
}, refreshInterval)
|
||||||
|
|
||||||
|
return () => clearInterval(interval)
|
||||||
|
}, [ocean, ddo, networkId, accountId, refreshPrice, refreshBalance])
|
||||||
|
|
||||||
|
// Get maximum amount for either OCEAN or datatoken
|
||||||
|
useEffect(() => {
|
||||||
|
if (!ocean || !price || price.value === 0) return
|
||||||
|
|
||||||
|
async function getMaximum() {
|
||||||
|
const maxTokensInPool = await ocean.pool.getDTMaxBuyQuantity(
|
||||||
|
price.address
|
||||||
|
)
|
||||||
|
setMaxDt(Number(maxTokensInPool))
|
||||||
|
|
||||||
|
const maxOceanInPool = await ocean.pool.getOceanMaxBuyQuantity(
|
||||||
|
price.address
|
||||||
|
)
|
||||||
|
setMaxOcean(Number(maxOceanInPool))
|
||||||
|
}
|
||||||
|
getMaximum()
|
||||||
|
}, [ocean, balance.ocean, price])
|
||||||
|
|
||||||
|
return (
|
||||||
|
<FormTrade
|
||||||
|
ddo={ddo}
|
||||||
|
price={price}
|
||||||
|
balance={tokenBalance}
|
||||||
|
maxDt={maxDt}
|
||||||
|
maxOcean={maxOcean}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
@ -5,8 +5,8 @@
|
|||||||
padding: 0;
|
padding: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.hasTokens {
|
.help {
|
||||||
font-size: var(--font-size-mini);
|
font-size: var(--font-size-mini);
|
||||||
color: var(--color-secondary);
|
color: var(--color-secondary);
|
||||||
margin-top: calc(var(--spacer) / 12);
|
margin-top: calc(var(--spacer) / 3);
|
||||||
}
|
}
|
||||||
|
@ -7,6 +7,7 @@ import Tabs from '../../atoms/Tabs'
|
|||||||
import { useOcean, useMetadata } from '@oceanprotocol/react'
|
import { useOcean, useMetadata } from '@oceanprotocol/react'
|
||||||
import compareAsBN from '../../../utils/compareAsBN'
|
import compareAsBN from '../../../utils/compareAsBN'
|
||||||
import Pool from './Pool'
|
import Pool from './Pool'
|
||||||
|
import Trade from './Trade'
|
||||||
|
|
||||||
export default function AssetActions({ ddo }: { ddo: DDO }): ReactElement {
|
export default function AssetActions({ ddo }: { ddo: DDO }): ReactElement {
|
||||||
const { ocean, balance, accountId } = useOcean()
|
const { ocean, balance, accountId } = useOcean()
|
||||||
@ -74,10 +75,16 @@ export default function AssetActions({ ddo }: { ddo: DDO }): ReactElement {
|
|||||||
const hasPool = ddo.price?.type === 'pool'
|
const hasPool = ddo.price?.type === 'pool'
|
||||||
|
|
||||||
hasPool &&
|
hasPool &&
|
||||||
tabs.push({
|
tabs.push(
|
||||||
title: 'Pool',
|
{
|
||||||
content: <Pool ddo={ddo} />
|
title: 'Pool',
|
||||||
})
|
content: <Pool ddo={ddo} />
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'Trade',
|
||||||
|
content: <Trade ddo={ddo} />
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
return <Tabs items={tabs} className={styles.actions} />
|
return <Tabs items={tabs} className={styles.actions} />
|
||||||
}
|
}
|
||||||
|
@ -71,7 +71,6 @@ export async function getResults(
|
|||||||
page,
|
page,
|
||||||
offset
|
offset
|
||||||
)
|
)
|
||||||
console.log(searchQuery)
|
|
||||||
const queryResult = await metadataCache.queryMetadata(searchQuery)
|
const queryResult = await metadataCache.queryMetadata(searchQuery)
|
||||||
|
|
||||||
return queryResult
|
return queryResult
|
||||||
|
@ -122,8 +122,8 @@ ul li {
|
|||||||
}
|
}
|
||||||
|
|
||||||
::selection {
|
::selection {
|
||||||
background: var(--brand-black);
|
background: var(--font-color-heading);
|
||||||
color: var(--brand-white);
|
color: var(--background-body);
|
||||||
}
|
}
|
||||||
|
|
||||||
form,
|
form,
|
||||||
|
3
src/images/arrow.svg
Normal file
3
src/images/arrow.svg
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
<svg width="20" height="19" viewBox="0 0 20 19" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path d="M0 10.4304L16.3396 10.4304L8.88727 17.6833L10.2401 19L20 9.5L10.2401 0L8.88727 1.31491L16.3396 8.56959L0 8.56959V10.4304Z" />
|
||||||
|
</svg>
|
After Width: | Height: | Size: 227 B |
24
src/models/FormTrade.ts
Normal file
24
src/models/FormTrade.ts
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
import TokenBalance from '../@types/TokenBalance'
|
||||||
|
|
||||||
|
export interface FormTradeData extends TokenBalance {
|
||||||
|
// in reference to datatoken, buy = swap from ocean to dt ( buy dt) , sell = swap from dt to ocean (sell dt)
|
||||||
|
type: 'buy' | 'sell'
|
||||||
|
slippage: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface TradeItem {
|
||||||
|
amount: number
|
||||||
|
token: string
|
||||||
|
maxAmount: number
|
||||||
|
}
|
||||||
|
|
||||||
|
export const initialValues: FormTradeData = {
|
||||||
|
ocean: undefined,
|
||||||
|
datatoken: undefined,
|
||||||
|
type: 'buy',
|
||||||
|
slippage: '5'
|
||||||
|
}
|
||||||
|
|
||||||
|
export const slippagePresets = ['5', '10', '15', '25', '50']
|
||||||
|
|
||||||
|
// validationSchema lives in components/organisms/AssetActions/Trade/FormTrade.tsx
|
@ -43,7 +43,7 @@ export default function PricesProvider({
|
|||||||
|
|
||||||
// Fetch new prices periodically with swr
|
// Fetch new prices periodically with swr
|
||||||
useSWR(url, fetchData, {
|
useSWR(url, fetchData, {
|
||||||
refreshInterval: 30000, // 30 sec.
|
refreshInterval: 60000, // 60 sec.
|
||||||
onSuccess
|
onSuccess
|
||||||
})
|
})
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user