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"
|
||||
}
|
||||
},
|
||||
"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=="
|
||||
},
|
||||
"@oceanprotocol/lib": {
|
||||
"version": "0.9.12",
|
||||
"resolved": "https://registry.npmjs.org/@oceanprotocol/lib/-/lib-0.9.12.tgz",
|
||||
"integrity": "sha512-R52kWSwwpKNzNHfnNbF6seFPvXEtExK3bWIi4V4eIkgmAf272sa6PVza4mJrtEpTAS1WcJv5ihF7cczIDecxbg==",
|
||||
"version": "0.9.14",
|
||||
"resolved": "https://registry.npmjs.org/@oceanprotocol/lib/-/lib-0.9.14.tgz",
|
||||
"integrity": "sha512-UDJpDHqJ5o6O/cOdTuyxhABaebQM1vz+RyfC3zVRSdz5Ke/5xjAChk+3LlYbXBA8VaacxioYa4OlaJLE5uB+Qg==",
|
||||
"requires": {
|
||||
"@ethereum-navigator/navigator": "^0.5.0",
|
||||
"@oceanprotocol/contracts": "^0.5.7",
|
||||
|
@ -24,7 +24,7 @@
|
||||
"@coingecko/cryptoformat": "^0.4.2",
|
||||
"@loadable/component": "^5.14.1",
|
||||
"@oceanprotocol/art": "^3.0.0",
|
||||
"@oceanprotocol/lib": "^0.9.12",
|
||||
"@oceanprotocol/lib": "^0.9.14",
|
||||
"@oceanprotocol/list-datapartners": "^1.0.3",
|
||||
"@oceanprotocol/react": "^0.3.19",
|
||||
"@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 {
|
||||
font-family: var(--font-family-base);
|
||||
font-size: var(--font-size-base);
|
||||
color: var(--brand-grey-light);
|
||||
color: var(--color-secondary);
|
||||
font-weight: var(--font-weight-base);
|
||||
transition: 0.2s ease-out;
|
||||
opacity: 0.7;
|
||||
@ -167,6 +167,7 @@
|
||||
.postfixGroup {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.prefixGroup input {
|
||||
@ -259,6 +260,31 @@ input[type='range']::-moz-range-track {
|
||||
|
||||
/* 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,
|
||||
.select.small {
|
||||
font-size: var(--font-size-small);
|
||||
|
@ -26,6 +26,7 @@ const DefaultInput = ({
|
||||
export default function InputElement({
|
||||
type,
|
||||
options,
|
||||
sortOptions,
|
||||
name,
|
||||
prefix,
|
||||
postfix,
|
||||
@ -40,22 +41,25 @@ export default function InputElement({
|
||||
const styleClasses = cx({ select: true, [size]: size })
|
||||
|
||||
switch (type) {
|
||||
case 'select':
|
||||
case 'select': {
|
||||
const sortedOptions =
|
||||
!sortOptions && sortOptions === false
|
||||
? options
|
||||
: options.sort((a: string, b: string) => a.localeCompare(b))
|
||||
return (
|
||||
<select id={name} className={styleClasses} {...props}>
|
||||
{field !== undefined && field.value === '' && (
|
||||
<option value="">---</option>
|
||||
)}
|
||||
{options &&
|
||||
options
|
||||
.sort((a: string, b: string) => a.localeCompare(b))
|
||||
.map((option: string, index: number) => (
|
||||
{sortedOptions &&
|
||||
sortedOptions.map((option: string, index: number) => (
|
||||
<option key={index} value={option}>
|
||||
{option}
|
||||
{option} {postfix}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
)
|
||||
}
|
||||
case 'textarea':
|
||||
return (
|
||||
<textarea
|
||||
|
@ -17,6 +17,7 @@ export interface InputProps {
|
||||
tag?: string
|
||||
type?: string
|
||||
options?: string[]
|
||||
sortOptions?: boolean
|
||||
additionalComponent?: ReactElement
|
||||
value?: string
|
||||
onChange?(
|
||||
@ -39,7 +40,7 @@ export interface InputProps {
|
||||
postfix?: string | ReactElement
|
||||
step?: string
|
||||
defaultChecked?: boolean
|
||||
size?: 'small' | 'large' | 'default'
|
||||
size?: 'mini' | 'small' | 'large' | 'default'
|
||||
}
|
||||
|
||||
export default function Input(props: Partial<InputProps>): ReactElement {
|
||||
|
@ -8,17 +8,30 @@
|
||||
.tab {
|
||||
display: inline-block;
|
||||
padding: calc(var(--spacer) / 12) var(--spacer);
|
||||
border-radius: var(--border-radius);
|
||||
font-weight: var(--font-weight-bold);
|
||||
font-size: var(--font-size-small);
|
||||
text-transform: uppercase;
|
||||
cursor: pointer;
|
||||
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'] {
|
||||
background: var(--font-color-heading);
|
||||
color: var(--background-body);
|
||||
border-color: var(--font-color-heading);
|
||||
}
|
||||
|
||||
.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 { locale } = useUserPreferences()
|
||||
|
||||
const title = row.tokenAmountIn
|
||||
? `Add ${formatNumber(Number(row.tokenAmountIn), locale)} ${
|
||||
dtSymbol || 'OCEAN'
|
||||
}`
|
||||
: `Remove ${formatNumber(Number(row.tokenAmountOut), locale)} ${
|
||||
dtSymbol || 'OCEAN'
|
||||
}`
|
||||
const symbol = dtSymbol || 'OCEAN'
|
||||
const title =
|
||||
row.type === 'join'
|
||||
? `Add ${formatNumber(Number(row.tokenAmountIn), locale)} ${symbol}`
|
||||
: row.type === 'exit'
|
||||
? `Remove ${formatNumber(Number(row.tokenAmountOut), locale)} ${symbol}`
|
||||
: `Swap ${formatNumber(
|
||||
Number(row.tokenAmountIn),
|
||||
locale
|
||||
)} ${symbol} for ${formatNumber(
|
||||
Number(row.tokenAmountOut),
|
||||
locale
|
||||
)} ${symbol}`
|
||||
|
||||
useEffect(() => {
|
||||
if (!ocean) return
|
||||
|
@ -23,6 +23,6 @@
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.hasTokens {
|
||||
composes: hasTokens from './index.module.css';
|
||||
.help {
|
||||
composes: help from './index.module.css';
|
||||
}
|
||||
|
@ -17,8 +17,8 @@
|
||||
margin-top: calc(var(--spacer) / 2);
|
||||
}
|
||||
|
||||
.hasTokens {
|
||||
composes: hasTokens from './index.module.css';
|
||||
.help {
|
||||
composes: help from './index.module.css';
|
||||
}
|
||||
|
||||
.feedback {
|
||||
|
@ -94,9 +94,23 @@ export default function Consume({
|
||||
{consumeStepText || pricingIsLoading ? (
|
||||
<Loader message={consumeStepText || pricingStepText} />
|
||||
) : (
|
||||
<>
|
||||
<Button style="primary" onClick={handleConsume} disabled={isDisabled}>
|
||||
{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>
|
||||
)
|
||||
@ -109,12 +123,6 @@ export default function Consume({
|
||||
</div>
|
||||
<div className={styles.pricewrapper}>
|
||||
<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 />}
|
||||
</div>
|
||||
</div>
|
||||
|
@ -3,7 +3,6 @@
|
||||
margin-right: -2rem;
|
||||
padding-left: var(--spacer);
|
||||
padding-right: var(--spacer);
|
||||
margin-top: calc(var(--spacer) / 4);
|
||||
padding-top: calc(var(--spacer) / 1.5);
|
||||
border-top: 1px solid var(--border-color);
|
||||
text-align: center;
|
||||
|
@ -12,7 +12,8 @@ export default function Actions({
|
||||
successMessage,
|
||||
txId,
|
||||
actionName,
|
||||
action
|
||||
action,
|
||||
isDisabled
|
||||
}: {
|
||||
isLoading: boolean
|
||||
loaderMessage: string
|
||||
@ -20,6 +21,7 @@ export default function Actions({
|
||||
txId: string
|
||||
actionName: string
|
||||
action: () => void
|
||||
isDisabled?: boolean
|
||||
}): ReactElement {
|
||||
const { networkId, ocean } = useOcean()
|
||||
|
||||
@ -33,7 +35,7 @@ export default function Actions({
|
||||
style="primary"
|
||||
size="small"
|
||||
onClick={() => action()}
|
||||
disabled={!ocean}
|
||||
disabled={!ocean || isDisabled}
|
||||
>
|
||||
{actionName}
|
||||
</Button>
|
||||
|
@ -4,20 +4,3 @@
|
||||
bottom: calc(var(--spacer) / 2);
|
||||
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 styles from './FormAdd.module.css'
|
||||
import Input from '../../../../atoms/Input'
|
||||
@ -12,7 +11,8 @@ import Button from '../../../../atoms/Button'
|
||||
import CoinSelect from '../CoinSelect'
|
||||
import { FormAddLiquidity } from '.'
|
||||
import { useOcean } from '@oceanprotocol/react'
|
||||
import { Balance } from '..'
|
||||
import TokenBalance from '../../../../../@types/TokenBalance'
|
||||
import UserLiquidity from '../../../../atoms/UserLiquidity'
|
||||
|
||||
export default function FormAdd({
|
||||
coin,
|
||||
@ -32,7 +32,7 @@ export default function FormAdd({
|
||||
amountMax: string
|
||||
setCoin: (value: string) => void
|
||||
totalPoolTokens: string
|
||||
totalBalance: Balance
|
||||
totalBalance: TokenBalance
|
||||
poolAddress: string
|
||||
setNewPoolTokens: (value: string) => void
|
||||
setNewPoolShare: (value: string) => void
|
||||
@ -86,20 +86,11 @@ export default function FormAdd({
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className={styles.userLiquidity}>
|
||||
<div>
|
||||
<span>Available:</span>
|
||||
{coin === 'OCEAN' ? (
|
||||
<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>
|
||||
<UserLiquidity
|
||||
amount={coin === 'OCEAN' ? balance.ocean : dtBalance}
|
||||
amountMax={amountMax}
|
||||
symbol={coin}
|
||||
/>
|
||||
|
||||
<Field name="amount">
|
||||
{({
|
||||
|
@ -22,6 +22,7 @@
|
||||
display: grid;
|
||||
gap: var(--spacer);
|
||||
grid-template-columns: 1fr 1fr;
|
||||
padding-bottom: calc(var(--spacer) / 2);
|
||||
}
|
||||
|
||||
.output p {
|
||||
|
@ -2,7 +2,6 @@ import React, { ReactElement, useState, useEffect } from 'react'
|
||||
import { useOcean } from '@oceanprotocol/react'
|
||||
import Header from '../Header'
|
||||
import { toast } from 'react-toastify'
|
||||
import { Balance } from '..'
|
||||
import Actions from '../Actions'
|
||||
import { graphql, useStaticQuery } from 'gatsby'
|
||||
import * as Yup from 'yup'
|
||||
@ -11,6 +10,7 @@ import FormAdd from './FormAdd'
|
||||
import styles from './index.module.css'
|
||||
import Token from '../Token'
|
||||
import Alert from '../../../../atoms/Alert'
|
||||
import TokenBalance from '../../../../../@types/TokenBalance'
|
||||
import { useUserPreferences } from '../../../../../providers/UserPreferences'
|
||||
|
||||
const contentQuery = graphql`
|
||||
@ -59,7 +59,7 @@ export default function Add({
|
||||
refreshInfo: () => void
|
||||
poolAddress: string
|
||||
totalPoolTokens: string
|
||||
totalBalance: Balance
|
||||
totalBalance: TokenBalance
|
||||
swapFee: string
|
||||
dtSymbol: string
|
||||
dtAddress: string
|
||||
@ -199,6 +199,7 @@ export default function Add({
|
||||
</div>
|
||||
|
||||
<Actions
|
||||
isDisabled={!isWarningAccepted}
|
||||
isLoading={isSubmitting}
|
||||
loaderMessage="Adding Liquidity..."
|
||||
successMessage="Successfully added liquidity."
|
||||
|
@ -5,16 +5,8 @@
|
||||
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 {
|
||||
margin-top: calc(var(--spacer) / 2);
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
|
@ -19,6 +19,7 @@ import { getMaxPercentRemove } from './utils'
|
||||
import { graphql, useStaticQuery } from 'gatsby'
|
||||
import PriceUnit from '../../../atoms/Price/PriceUnit'
|
||||
import debounce from 'lodash.debounce'
|
||||
import UserLiquidity from '../../../atoms/UserLiquidity'
|
||||
|
||||
const contentQuery = graphql`
|
||||
query PoolRemoveQuery {
|
||||
@ -137,7 +138,6 @@ export default function Remove({
|
||||
// Check and set outputs when amountPoolShares changes
|
||||
useEffect(() => {
|
||||
if (!ocean || !poolTokens) return
|
||||
console.log('eff', amountPoolShares, isAdvanced)
|
||||
getValues.current(amountPoolShares, isAdvanced)
|
||||
}, [
|
||||
amountPoolShares,
|
||||
@ -184,12 +184,7 @@ export default function Remove({
|
||||
<Header title={content.title} backAction={() => setShowRemove(false)} />
|
||||
|
||||
<form className={styles.removeInput}>
|
||||
<div className={styles.userLiquidity}>
|
||||
<div>
|
||||
<span>Available:</span>
|
||||
<PriceUnit price={poolTokens} symbol="pool shares" small />
|
||||
</div>
|
||||
</div>
|
||||
<UserLiquidity amount={poolTokens} symbol="pool shares" />
|
||||
|
||||
<div className={styles.range}>
|
||||
<h3>{amountPercent}%</h3>
|
||||
|
@ -46,6 +46,7 @@
|
||||
text-align: center;
|
||||
border-top: 1px solid var(--border-color);
|
||||
padding-top: calc(var(--spacer) / 4);
|
||||
padding-bottom: calc(var(--spacer) / 4);
|
||||
}
|
||||
|
||||
.update:before {
|
||||
|
@ -17,15 +17,11 @@ import EtherscanLink from '../../../atoms/EtherscanLink'
|
||||
import Token from './Token'
|
||||
import TokenList from './TokenList'
|
||||
import { graphql, useStaticQuery } from 'gatsby'
|
||||
import TokenBalance from '../../../../@types/TokenBalance'
|
||||
import Transactions from './Transactions'
|
||||
import Graph, { ChartDataLiqudity } from './Graph'
|
||||
import axios from 'axios'
|
||||
|
||||
export interface Balance {
|
||||
ocean: number
|
||||
datatoken: number
|
||||
}
|
||||
|
||||
const contentQuery = graphql`
|
||||
query PoolQuery {
|
||||
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 [totalPoolTokens, setTotalPoolTokens] = useState<string>()
|
||||
const [userLiquidity, setUserLiquidity] = useState<Balance>()
|
||||
const [userLiquidity, setUserLiquidity] = useState<TokenBalance>()
|
||||
const [swapFee, setSwapFee] = useState<string>()
|
||||
const [weightOcean, setWeightOcean] = useState<string>()
|
||||
const [weightDt, setWeightDt] = useState<string>()
|
||||
@ -76,7 +72,7 @@ export default function Pool({ ddo }: { ddo: DDO }): ReactElement {
|
||||
creatorTotalLiquidityInOcean,
|
||||
setCreatorTotalLiquidityInOcean
|
||||
] = useState(0)
|
||||
const [creatorLiquidity, setCreatorLiquidity] = useState<Balance>()
|
||||
const [creatorLiquidity, setCreatorLiquidity] = useState<TokenBalance>()
|
||||
const [creatorPoolTokens, setCreatorPoolTokens] = useState<string>()
|
||||
const [creatorPoolShare, setCreatorPoolShare] = useState<string>()
|
||||
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
|
||||
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(() => {
|
||||
setIsRemoveDisabled(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;
|
||||
}
|
||||
|
||||
.hasTokens {
|
||||
.help {
|
||||
font-size: var(--font-size-mini);
|
||||
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 compareAsBN from '../../../utils/compareAsBN'
|
||||
import Pool from './Pool'
|
||||
import Trade from './Trade'
|
||||
|
||||
export default function AssetActions({ ddo }: { ddo: DDO }): ReactElement {
|
||||
const { ocean, balance, accountId } = useOcean()
|
||||
@ -74,10 +75,16 @@ export default function AssetActions({ ddo }: { ddo: DDO }): ReactElement {
|
||||
const hasPool = ddo.price?.type === 'pool'
|
||||
|
||||
hasPool &&
|
||||
tabs.push({
|
||||
tabs.push(
|
||||
{
|
||||
title: 'Pool',
|
||||
content: <Pool ddo={ddo} />
|
||||
})
|
||||
},
|
||||
{
|
||||
title: 'Trade',
|
||||
content: <Trade ddo={ddo} />
|
||||
}
|
||||
)
|
||||
|
||||
return <Tabs items={tabs} className={styles.actions} />
|
||||
}
|
||||
|
@ -71,7 +71,6 @@ export async function getResults(
|
||||
page,
|
||||
offset
|
||||
)
|
||||
console.log(searchQuery)
|
||||
const queryResult = await metadataCache.queryMetadata(searchQuery)
|
||||
|
||||
return queryResult
|
||||
|
@ -122,8 +122,8 @@ ul li {
|
||||
}
|
||||
|
||||
::selection {
|
||||
background: var(--brand-black);
|
||||
color: var(--brand-white);
|
||||
background: var(--font-color-heading);
|
||||
color: var(--background-body);
|
||||
}
|
||||
|
||||
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
|
||||
useSWR(url, fetchData, {
|
||||
refreshInterval: 30000, // 30 sec.
|
||||
refreshInterval: 60000, // 60 sec.
|
||||
onSuccess
|
||||
})
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user