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

Pool stats updates and pool context provider (#1057)

* remove datatokens from liquidity stats, multiply base token

* naming: Pool Creator Statistics → Owner Liquidity

* remove all the noise

* more pool stats data

* simplify user liquidity data structure, remove datatoken calculations

* chart tweaks, new calculation for liquidity

* tweaks

* todo

* frontpage error fixes

* account switch fixes

* comment out fees

* pool context provider

* double pool share

* move subgraph-related methods into context provider

* typing fix
This commit is contained in:
Matthias Kretschmann 2022-02-07 14:58:47 +00:00 committed by GitHub
parent d51d909a66
commit b8c2b01b54
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
21 changed files with 528 additions and 589 deletions

View File

@ -41,7 +41,7 @@
"add": {
"title": "Add Liquidity",
"output": {
"help": "Providing liquidity will earn you SWAPFEE% on every transaction in this pool, proportionally to your share of the pool. Your token input will be converted based on the weight of the pool.",
"help": "Providing liquidity will earn you SWAPFEE% on every transaction in this pool, proportionally to your share of the pool.",
"titleIn": "You will receive",
"titleOut": "Pool conversion"
},
@ -50,8 +50,7 @@
},
"remove": {
"title": "Remove Liquidity",
"simple": "Set the amount of your pool shares to spend. You will get the equivalent value in OCEAN, limited to maximum amount for pool protection. If you have Datatokens left in your wallet, you can add them to the pool to increase the maximum amount.",
"advanced": "Set the amount of your pool shares to spend. You will get OCEAN and Datatokens equivalent to your pool share, without any limit. You can use these Datatokens in other DeFi tools.",
"simple": "Set the amount of your pool shares to spend. You will get the equivalent value in OCEAN, limited to maximum amount for pool protection.",
"output": {
"titleIn": "You will spend",
"titleOut": "You will receive"

View File

@ -25,7 +25,6 @@ interface AssetProviderValue {
title: string
owner: string
error?: string
refreshInterval: number
isAssetNetwork: boolean
oceanConfig: Config
loading: boolean
@ -34,8 +33,6 @@ interface AssetProviderValue {
const AssetContext = createContext({} as AssetProviderValue)
const refreshInterval = 10000 // 10 sec.
function AssetProvider({
did,
children
@ -168,7 +165,6 @@ function AssetProvider({
error,
isInPurgatory,
purgatoryData,
refreshInterval,
loading,
fetchAsset,
isAssetNetwork,

View File

@ -0,0 +1,46 @@
import { gql } from 'urql'
export const poolDataQuery = gql`
query PoolData(
$pool: ID!
$poolAsString: String!
$owner: String!
$user: String
) {
poolData: pool(id: $pool) {
id
totalShares
poolFee
opfFee
marketFee
spotPrice
baseToken {
address
symbol
}
baseTokenWeight
baseTokenLiquidity
datatoken {
address
symbol
}
datatokenWeight
datatokenLiquidity
shares(where: { user: $owner }) {
shares
}
}
poolDataUser: pool(id: $pool) {
shares(where: { user: $user }) {
shares
}
}
poolSnapshots(first: 1000, where: { pool: $poolAsString }, orderBy: date) {
date
spotPrice
baseTokenLiquidity
datatokenLiquidity
swapVolume
}
}
`

View File

@ -0,0 +1,36 @@
import Decimal from 'decimal.js'
import {
PoolData_poolSnapshots as PoolDataPoolSnapshots,
PoolData_poolData as PoolDataPoolData
} from 'src/@types/subgraph/PoolData'
export interface PoolInfo {
poolFee: string
marketFee: string
opfFee: string
weightBaseToken: string
weightDt: string
datatokenSymbol: string
baseTokenSymbol: string
baseTokenAddress: string
totalPoolTokens: string
totalLiquidityInOcean: Decimal
}
export interface PoolInfoUser {
liquidity: Decimal // liquidity in base token
poolShares: string // pool share tokens
poolShare: string // in %
}
export interface PoolProviderValue {
poolData: PoolDataPoolData
poolInfo: PoolInfo
poolInfoOwner: PoolInfoUser
poolInfoUser: PoolInfoUser
poolSnapshots: PoolDataPoolSnapshots[]
hasUserAddedLiquidity: boolean
isRemoveDisabled: boolean
refreshInterval: number
fetchAllData: () => void
}

View File

@ -0,0 +1,38 @@
import { isValidNumber } from '@utils/numbers'
import { getQueryContext, fetchData } from '@utils/subgraph'
import Decimal from 'decimal.js'
import { PoolData } from 'src/@types/subgraph/PoolData'
import { OperationResult } from 'urql'
import { poolDataQuery } from './_queries'
export async function getPoolData(
chainId: number,
pool: string,
owner: string,
user: string
) {
const queryVariables = {
// Using `pool` & `poolAsString` is a workaround to make the mega query work.
// See https://github.com/oceanprotocol/ocean-subgraph/issues/301
pool: pool.toLowerCase(),
poolAsString: pool.toLowerCase(),
owner: owner.toLowerCase(),
user: user.toLowerCase()
}
const response: OperationResult<PoolData> = await fetchData(
poolDataQuery,
queryVariables,
getQueryContext(chainId)
)
return response?.data
}
export function getWeight(weight: string) {
return isValidNumber(weight) ? new Decimal(weight).mul(10).toString() : '0'
}
export function getFee(fee: string) {
// fees are tricky: to get 0.1% you need to convert from 0.001
return isValidNumber(fee) ? new Decimal(fee).mul(100).toString() : '0'
}

280
src/@context/Pool/index.tsx Normal file
View File

@ -0,0 +1,280 @@
import { LoggerInstance } from '@oceanprotocol/lib'
import { isValidNumber } from '@utils/numbers'
import Decimal from 'decimal.js'
import React, {
useContext,
useState,
useEffect,
createContext,
ReactElement,
useCallback,
ReactNode
} from 'react'
import {
PoolData_poolSnapshots as PoolDataPoolSnapshots,
PoolData_poolData as PoolDataPoolData
} from 'src/@types/subgraph/PoolData'
import { useAsset } from '../Asset'
import { useWeb3 } from '../Web3'
import { PoolProviderValue, PoolInfo, PoolInfoUser } from './_types'
import { getFee, getPoolData, getWeight } from './_utils'
Decimal.set({ toExpNeg: -18, precision: 18, rounding: 1 })
const PoolContext = createContext({} as PoolProviderValue)
const refreshInterval = 10000 // 10 sec.
const initialPoolInfo: Partial<PoolInfo> = {
totalLiquidityInOcean: new Decimal(0)
}
const initialPoolInfoUser: Partial<PoolInfoUser> = {
liquidity: new Decimal(0)
}
const initialPoolInfoCreator: Partial<PoolInfoUser> = initialPoolInfoUser
function PoolProvider({ children }: { children: ReactNode }): ReactElement {
const { accountId } = useWeb3()
const { isInPurgatory, asset, owner } = useAsset()
const [poolData, setPoolData] = useState<PoolDataPoolData>()
const [poolInfo, setPoolInfo] = useState<PoolInfo>(
initialPoolInfo as PoolInfo
)
const [poolInfoOwner, setPoolInfoOwner] = useState<PoolInfoUser>(
initialPoolInfoCreator as PoolInfoUser
)
const [poolInfoUser, setPoolInfoUser] = useState<PoolInfoUser>(
initialPoolInfoUser as PoolInfoUser
)
const [poolSnapshots, setPoolSnapshots] = useState<PoolDataPoolSnapshots[]>()
const [hasUserAddedLiquidity, setUserHasAddedLiquidity] = useState(false)
const [isRemoveDisabled, setIsRemoveDisabled] = useState(false)
const [fetchInterval, setFetchInterval] = useState<NodeJS.Timeout>()
const fetchAllData = useCallback(async () => {
if (!asset?.chainId || !asset?.accessDetails?.addressOrId || !owner) return
const response = await getPoolData(
asset.chainId,
asset.accessDetails.addressOrId,
owner,
accountId || ''
)
if (!response) return
setPoolData(response.poolData)
setPoolInfoUser((prevState) => ({
...prevState,
poolShares: response.poolDataUser?.shares[0]?.shares
}))
setPoolSnapshots(response.poolSnapshots)
LoggerInstance.log('[pool] Fetched pool data:', response.poolData)
LoggerInstance.log('[pool] Fetched user data:', response.poolDataUser)
LoggerInstance.log('[pool] Fetched pool snapshots:', response.poolSnapshots)
}, [asset?.chainId, asset?.accessDetails?.addressOrId, owner, accountId])
// Helper: start interval fetching
// Having `accountId` as dependency is important for interval to
// change after user account switch.
const initFetchInterval = useCallback(() => {
if (fetchInterval) return
const newInterval = setInterval(() => {
fetchAllData()
LoggerInstance.log(
`[pool] Refetch interval fired after ${refreshInterval / 1000}s`
)
}, refreshInterval)
setFetchInterval(newInterval)
}, [fetchInterval, fetchAllData, accountId])
useEffect(() => {
return () => {
clearInterval(fetchInterval)
}
}, [fetchInterval])
//
// 0 Fetch all the data on mount if we are on a pool.
// All further effects depend on the fetched data
// and only do further data checking and manipulation.
//
useEffect(() => {
if (asset?.accessDetails?.type !== 'dynamic') return
fetchAllData()
initFetchInterval()
}, [fetchAllData, initFetchInterval, asset?.accessDetails?.type])
//
// 1 General Pool Info
//
useEffect(() => {
if (!poolData) return
// Fees
const poolFee = getFee(poolData.poolFee)
const marketFee = getFee(poolData.marketFee)
const opfFee = getFee(poolData.opfFee)
// Total Liquidity
const totalLiquidityInOcean = isValidNumber(poolData.spotPrice)
? new Decimal(poolData.baseTokenLiquidity).add(
new Decimal(poolData.datatokenLiquidity).mul(poolData.spotPrice)
)
: new Decimal(0)
const newPoolInfo = {
poolFee,
marketFee,
opfFee,
weightBaseToken: getWeight(poolData.baseTokenWeight),
weightDt: getWeight(poolData.datatokenWeight),
datatokenSymbol: poolData.datatoken.symbol,
baseTokenSymbol: poolData.baseToken.symbol,
baseTokenAddress: poolData.baseToken.address,
totalPoolTokens: poolData.totalShares,
totalLiquidityInOcean
}
setPoolInfo(newPoolInfo)
LoggerInstance.log('[pool] Created new pool info:', newPoolInfo)
}, [poolData])
//
// 2 Pool Creator Info
//
useEffect(() => {
if (!poolData || !poolInfo?.totalPoolTokens) return
// Staking bot receives half the pool shares so for display purposes
// we can multiply by 2 as we have a hardcoded 50/50 pool weight.
const ownerPoolShares = new Decimal(poolData.shares[0]?.shares)
.mul(2)
.toString()
// Liquidity in base token, calculated from pool share tokens.
const liquidity =
isValidNumber(ownerPoolShares) &&
isValidNumber(poolInfo.totalPoolTokens) &&
isValidNumber(poolData.baseTokenLiquidity)
? new Decimal(ownerPoolShares)
.dividedBy(new Decimal(poolInfo.totalPoolTokens))
.mul(poolData.baseTokenLiquidity)
: new Decimal(0)
// Pool share tokens.
const poolShare =
isValidNumber(ownerPoolShares) && isValidNumber(poolInfo.totalPoolTokens)
? new Decimal(ownerPoolShares)
.dividedBy(new Decimal(poolInfo.totalPoolTokens))
.mul(100)
.toFixed(2)
: '0'
const newPoolOwnerInfo = {
liquidity,
poolShares: ownerPoolShares,
poolShare
}
setPoolInfoOwner(newPoolOwnerInfo)
LoggerInstance.log('[pool] Created new owner pool info:', newPoolOwnerInfo)
}, [poolData, poolInfo?.totalPoolTokens])
//
// 3 User Pool Info
//
useEffect(() => {
if (
!poolData ||
!poolInfo?.totalPoolTokens ||
!asset?.chainId ||
!accountId
)
return
// Staking bot receives half the pool shares so for display purposes
// we can multiply by 2 as we have a hardcoded 50/50 pool weight.
const userPoolShares = new Decimal(poolInfoUser.poolShares)
.mul(2)
.toString()
// Pool share in %.
const poolShare =
isValidNumber(userPoolShares) &&
isValidNumber(poolInfo.totalPoolTokens) &&
new Decimal(userPoolShares)
.dividedBy(new Decimal(poolInfo.totalPoolTokens))
.mul(100)
.toFixed(2)
setUserHasAddedLiquidity(Number(poolShare) > 0)
// Liquidity in base token, calculated from pool share tokens.
const liquidity =
isValidNumber(userPoolShares) &&
isValidNumber(poolInfo.totalPoolTokens) &&
isValidNumber(poolData.baseTokenLiquidity)
? new Decimal(userPoolShares)
.dividedBy(new Decimal(poolInfo.totalPoolTokens))
.mul(poolData.baseTokenLiquidity)
: new Decimal(0)
const newPoolInfoUser = {
liquidity,
poolShare
}
setPoolInfoUser((prevState: PoolInfoUser) => ({
...prevState,
...newPoolInfoUser
}))
LoggerInstance.log('[pool] Created new user pool info:', {
poolShares: userPoolShares,
...newPoolInfoUser
})
}, [
poolData,
poolInfoUser?.poolShares,
accountId,
asset?.chainId,
owner,
poolInfo?.totalPoolTokens
])
//
// Check if removing liquidity should be disabled.
//
useEffect(() => {
if (!owner || !accountId) return
setIsRemoveDisabled(isInPurgatory && owner === accountId)
}, [isInPurgatory, owner, accountId])
return (
<PoolContext.Provider
value={
{
poolData,
poolInfo,
poolInfoOwner,
poolInfoUser,
poolSnapshots,
hasUserAddedLiquidity,
isRemoveDisabled,
refreshInterval,
fetchAllData
} as PoolProviderValue
}
>
{children}
</PoolContext.Provider>
)
}
// Helper hook to access the provider values
const usePool = (): PoolProviderValue => useContext(PoolContext)
export { PoolProvider, usePool, PoolContext }
export default PoolProvider

View File

@ -10,11 +10,15 @@ import useSWR from 'swr'
import { useSiteMetadata } from '@hooks/useSiteMetadata'
import { LoggerInstance } from '@oceanprotocol/lib'
interface PricesValue {
interface Prices {
[key: string]: number
}
const initialData: PricesValue = {
interface PricesValue {
prices: Prices
}
const initialData: Prices = {
eur: 0.0,
usd: 0.0,
eth: 0.0,
@ -37,7 +41,7 @@ export default function PricesProvider({
const [prices, setPrices] = useState(initialData)
const onSuccess = async (data: { [tokenId]: { [key: string]: number } }) => {
const onSuccess = async (data: { [tokenId]: Prices }) => {
if (!data) return
LoggerInstance.log('[prices] Got new OCEAN spot prices.', data[tokenId])
setPrices(data[tokenId])

View File

@ -2,18 +2,7 @@ import { gql, OperationResult, TypedDocumentNode, OperationContext } from 'urql'
import { Asset, LoggerInstance } from '@oceanprotocol/lib'
import { getUrqlClientInstance } from '@context/UrqlProvider'
import { getOceanConfig } from './ocean'
import {
AssetsPoolPrice,
AssetsPoolPrice_pools as AssetsPoolPricePool
} from '../@types/subgraph/AssetsPoolPrice'
import {
AssetsFrePrice,
AssetsFrePrice_fixedRateExchanges as AssetsFrePriceFixedRateExchange
} from '../@types/subgraph/AssetsFrePrice'
import {
AssetsFreePrice,
AssetsFreePrice_dispensers as AssetFreePriceDispenser
} from '../@types/subgraph/AssetsFreePrice'
import { AssetPoolPrice } from '../@types/subgraph/AssetPoolPrice'
import { AssetPreviousOrder } from '../@types/subgraph/AssetPreviousOrder'
import {
HighestLiquidityAssets_pools as HighestLiquidityAssetsPool,
@ -25,7 +14,6 @@ import {
} from '../@types/subgraph/PoolShares'
import { OrdersData_orders as OrdersData } from '../@types/subgraph/OrdersData'
import { UserSalesQuery as UsersSalesList } from '../@types/subgraph/UserSalesQuery'
import { PoolData } from 'src/@types/subgraph/PoolData'
export interface UserLiquidity {
price: string
@ -36,91 +24,6 @@ export interface PriceList {
[key: string]: string
}
interface DidAndDatatokenMap {
[name: string]: string
}
const FreeQuery = gql`
query AssetsFreePrice($datatoken_in: [String!]) {
dispensers(orderBy: id, where: { token_in: $datatoken_in }) {
token {
id
address
}
}
}
`
const AssetFreeQuery = gql`
query AssetFreePrice($datatoken: String) {
dispensers(orderBy: id, where: { token: $datatoken }) {
active
owner
isMinter
maxTokens
maxBalance
balance
token {
id
isDatatoken
}
}
}
`
const FreQuery = gql`
query AssetsFrePrice($datatoken_in: [String!]) {
fixedRateExchanges(orderBy: id, where: { datatoken_in: $datatoken_in }) {
id
price
baseToken {
symbol
}
datatoken {
id
address
symbol
}
}
}
`
const AssetFreQuery = gql`
query AssetFrePrice($datatoken: String) {
fixedRateExchanges(orderBy: id, where: { datatoken: $datatoken }) {
id
price
baseToken {
symbol
}
datatoken {
id
address
symbol
}
}
}
`
const PoolQuery = gql`
query AssetsPoolPrice($datatokenAddress_in: [String!]) {
pools(where: { datatoken_in: $datatokenAddress_in }) {
id
spotPrice
datatoken {
address
symbol
}
baseToken {
address
symbol
}
datatokenLiquidity
baseTokenLiquidity
}
}
`
const AssetPoolPriceQuery = gql`
query AssetPoolPrice($datatokenAddress: String) {
pools(where: { datatoken: $datatokenAddress }) {
@ -280,51 +183,6 @@ const TopSalesQuery = gql`
}
`
const poolDataQuery = gql`
query PoolData(
$pool: ID!
$poolAsString: String!
$owner: String!
$user: String
) {
poolData: pool(id: $pool) {
id
totalShares
poolFee
opfFee
marketFee
spotPrice
baseToken {
address
symbol
}
baseTokenWeight
baseTokenLiquidity
datatoken {
address
symbol
}
datatokenWeight
datatokenLiquidity
shares(where: { user: $owner }) {
shares
}
}
poolDataUser: pool(id: $pool) {
shares(where: { user: $user }) {
shares
}
}
poolSnapshots(first: 1000, where: { pool: $poolAsString }, orderBy: date) {
date
spotPrice
baseTokenLiquidity
datatokenLiquidity
swapVolume
}
}
`
export function getSubgraphUri(chainId: number): string {
const config = getOceanConfig(chainId)
return config.subgraphUri
@ -411,7 +269,7 @@ export async function getSpotPrice(asset: Asset): Promise<number> {
}
const queryContext = getQueryContext(Number(asset.chainId))
const poolPriceResponse: OperationResult<AssetsPoolPrice> = await fetchData(
const poolPriceResponse: OperationResult<AssetPoolPrice> = await fetchData(
AssetPoolPriceQuery,
poolVariables,
queryContext
@ -430,14 +288,14 @@ export async function getHighestLiquidityDatatokens(
const fetchedPools: OperationResult<HighestLiquidityGraphAssets, any> =
await fetchData(HighestLiquidityAssets, null, queryContext)
highestLiquidityAssets = highestLiquidityAssets.concat(
fetchedPools.data.pools
fetchedPools?.data?.pools
)
}
highestLiquidityAssets.sort(
(a, b) => b.baseTokenLiquidity - a.baseTokenLiquidity
)
for (let i = 0; i < highestLiquidityAssets.length; i++) {
if (!highestLiquidityAssets[i].datatoken.address) continue
if (!highestLiquidityAssets[i]?.datatoken?.address) continue
dtList.push(highestLiquidityAssets[i].datatoken.address)
}
return dtList
@ -588,26 +446,3 @@ export async function getTopAssetsPublishers(
return publisherSales.slice(0, nrItems)
}
export async function getPoolData(
chainId: number,
pool: string,
owner: string,
user: string
) {
const queryVariables = {
// Using `pool` & `poolAsString` is a workaround to make the mega query work.
// See https://github.com/oceanprotocol/ocean-subgraph/issues/301
pool: pool.toLowerCase(),
poolAsString: pool.toLowerCase(),
owner: owner.toLowerCase(),
user: user.toLowerCase()
}
const response: OperationResult<PoolData> = await fetchData(
poolDataQuery,
queryVariables,
getQueryContext(chainId)
)
return response?.data
}

View File

@ -10,13 +10,11 @@ const cx = classNames.bind(styles)
export default function Conversion({
price,
className,
hideApproximateSymbol,
showTVLLabel
hideApproximateSymbol
}: {
price: string // expects price in OCEAN, not wei
className?: string
hideApproximateSymbol?: boolean
showTVLLabel?: boolean
}): ReactElement {
const { prices } = usePrices()
const { currency, locale } = useUserPreferences()
@ -29,7 +27,6 @@ export default function Conversion({
const styleClasses = cx({
conversion: true,
removeTvlPadding: showTVLLabel,
[className]: className
})
@ -39,7 +36,7 @@ export default function Conversion({
return
}
const conversionValue = (prices as any)[currency.toLowerCase()]
const conversionValue = prices[currency.toLowerCase()]
const converted = conversionValue * Number(price)
const convertedFormatted = formatCurrency(
converted,
@ -64,7 +61,6 @@ export default function Conversion({
className={styleClasses}
title="Approximation based on current OCEAN spot price on Coingecko"
>
{showTVLLabel && 'TVL'}
{!hideApproximateSymbol && '≈ '}
<strong dangerouslySetInnerHTML={{ __html: priceConverted }} />{' '}
{!isFiat && currency}

View File

@ -14,8 +14,8 @@
}
.icon {
width: 1em;
height: 1em;
width: 0.85em;
height: 0.85em;
cursor: help;
display: inline-block;
margin-bottom: -0.1em;

View File

@ -120,7 +120,7 @@ export default function FormAdd({
)}
</Field>
{Number(balance.ocean) && (
{Number(balance.ocean) > 0 && (
<Button
className={styles.buttonMax}
style="text"

View File

@ -1,7 +1,4 @@
.output {
display: grid;
gap: var(--spacer);
grid-template-columns: 1fr 1fr;
padding-bottom: calc(var(--spacer) / 2);
}
@ -11,14 +8,6 @@
font-size: var(--font-size-small);
}
.output div:first-child [class*='token'] {
white-space: normal;
}
.output div:first-child [class*='token'] > figure {
display: none;
}
.help {
text-align: center;
}

View File

@ -57,16 +57,9 @@ export default function Output({
{help.replace('SWAPFEE', swapFee)}
</FormHelp>
<div className={styles.output}>
<div>
<p>{titleIn}</p>
<Token symbol="pool shares" balance={newPoolTokens} />
<Token symbol="% of pool" balance={newPoolShare} />
</div>
<div>
<p>{titleOut}</p>
<Token symbol="OCEAN" balance={poolOcean} />
<Token symbol={datatokenSymbol} balance={poolDatatoken} />
</div>
<p>{titleIn}</p>
<Token symbol="pool shares" balance={newPoolTokens} noIcon />
<Token symbol="% of pool" balance={newPoolShare} noIcon />
</div>
</>
)

View File

@ -2,7 +2,11 @@ import { formatPrice } from '@shared/Price/PriceUnit'
import { ChartOptions, TooltipItem } from 'chart.js'
import { tooltipOptions } from './_constants'
export function getOptions(locale: string, isDarkMode: boolean): ChartOptions {
export function getOptions(
locale: string,
isDarkMode: boolean,
symbol: string
): ChartOptions {
return {
layout: {
padding: {
@ -21,7 +25,7 @@ export function getOptions(locale: string, isDarkMode: boolean): ChartOptions {
borderColor: isDarkMode ? `#41474e` : `#e2e2e2`,
callbacks: {
label: (tooltipItem: TooltipItem<any>) =>
`${formatPrice(`${tooltipItem.formattedValue}`, locale)} OCEAN`
`${formatPrice(`${tooltipItem.formattedValue}`, locale)} ${symbol}`
}
}
},

View File

@ -12,13 +12,15 @@ import { lineStyle, GraphType } from './_constants'
import Nav from './Nav'
import { getOptions } from './_utils'
import { PoolData_poolSnapshots as PoolDataPoolSnapshots } from 'src/@types/subgraph/PoolData'
import { usePrices } from '@context/Prices'
export default function Graph({
poolSnapshots
}: {
poolSnapshots: PoolDataPoolSnapshots[]
}): ReactElement {
const { locale } = useUserPreferences()
const { locale, currency } = useUserPreferences()
const { prices } = usePrices()
const darkMode = useDarkMode(false, darkModeConfig)
const [options, setOptions] = useState<ChartOptions<any>>()
@ -29,10 +31,18 @@ export default function Graph({
// 0 Get Graph options
//
useEffect(() => {
if (!poolSnapshots) return
LoggerInstance.log('[pool graph] Fired getOptions().')
const options = getOptions(locale, darkMode.value)
const symbol =
graphType === 'liquidity'
? currency
: // TODO: remove any once baseToken works
// see https://github.com/oceanprotocol/ocean-subgraph/issues/312
(poolSnapshots[0] as any)?.baseToken?.symbol
const options = getOptions(locale, darkMode.value, symbol)
setOptions(options)
}, [locale, darkMode.value, graphType])
}, [locale, darkMode.value, graphType, currency, poolSnapshots])
//
// 1 Data manipulation
@ -42,14 +52,21 @@ export default function Graph({
const timestamps = poolSnapshots.map((item) => {
const date = new Date(item.date * 1000)
return `${date.toLocaleDateString()} ${date.toLocaleTimeString()}`
return `${date.toLocaleDateString(locale)} ${date.toLocaleTimeString(
locale,
{ hour: '2-digit', minute: '2-digit' }
)}`
})
let baseTokenLiquidityCumulative = '0'
const liquidityHistory = poolSnapshots.map((item) => {
const conversionSpotPrice = prices[currency.toLowerCase()]
baseTokenLiquidityCumulative = new Decimal(baseTokenLiquidityCumulative)
.add(item.baseTokenLiquidity)
.mul(2) // double baseTokenLiquidity as we have 50/50 weight
.mul(conversionSpotPrice) // convert to user currency
.toString()
return baseTokenLiquidityCumulative
})
@ -66,23 +83,23 @@ export default function Graph({
let data
switch (graphType) {
case 'price':
data = priceHistory.slice(0)
data = priceHistory
break
case 'volume':
data = volumeHistory.slice(0)
data = volumeHistory
break
default:
data = liquidityHistory.slice(0)
data = liquidityHistory
break
}
const newGraphData = {
labels: timestamps.slice(0),
labels: timestamps,
datasets: [{ ...lineStyle, data, borderColor: `#8b98a9` }]
}
setGraphData(newGraphData)
LoggerInstance.log('[pool graph] New graph data created:', newGraphData)
}, [poolSnapshots, graphType])
}, [poolSnapshots, graphType, currency, prices, locale])
return (
<div className={styles.graphWrap}>

View File

@ -1,5 +1,5 @@
.removeInput {
composes: addInput from './Add/index.module.css';
composes: addInput from '../Add/index.module.css';
padding-left: calc(var(--spacer) * 2);
padding-right: calc(var(--spacer) * 2);
padding-bottom: calc(var(--spacer) / 2);
@ -46,7 +46,7 @@
}
.output {
composes: output from './Add/Output.module.css';
composes: output from '../Add/Output.module.css';
}
.output [class*='token'] {
@ -66,5 +66,5 @@
}
.slippage {
composes: slippage from '../Trade/Slippage.module.css';
composes: slippage from '../../Trade/Slippage.module.css';
}

View File

@ -5,22 +5,22 @@ import React, {
useEffect,
useRef
} from 'react'
import styles from './Remove.module.css'
import Header from './Header'
import styles from './index.module.css'
import Header from '../Header'
import { toast } from 'react-toastify'
import Actions from './Actions'
import Actions from '../Actions'
import { LoggerInstance, Pool } from '@oceanprotocol/lib'
import Token from './Token'
import Token from '../Token'
import FormHelp from '@shared/FormInput/Help'
import Button from '@shared/atoms/Button'
import { getMaxPercentRemove } from './utils'
import { getMaxPercentRemove } from '../utils'
import debounce from 'lodash.debounce'
import UserLiquidity from '../UserLiquidity'
import UserLiquidity from '../../UserLiquidity'
import InputElement from '@shared/FormInput/InputElement'
import { useWeb3 } from '@context/Web3'
import Decimal from 'decimal.js'
import { useAsset } from '@context/Asset'
import content from '../../../../../content/price.json'
import content from '../../../../../../content/price.json'
const slippagePresets = ['5', '10', '15', '25', '50']
@ -179,14 +179,14 @@ export default function Remove({
</div>
</form>
<div className={styles.output}>
<div>
<p>{content.pool.remove.output.titleIn}</p>
<Token symbol="pool shares" balance={amountPoolShares} noIcon />
</div>
<div>
<p>{content.pool.remove.output.titleOut} minimum</p>
<Token symbol={tokenOutSymbol} balance={minOceanAmount} />
</div>
{/* <div>
<p>{content.pool.remove.output.titleIn}</p>
<Token symbol="pool shares" balance={amountPoolShares} noIcon />
</div> */}
</div>
<div className={styles.slippage}>
<strong>Expected price impact</strong>

View File

@ -20,7 +20,7 @@
.tokens {
display: grid;
grid-template-columns: 1fr 1fr;
grid-template-columns: repeat(auto-fill, minmax(10rem, 1fr));
}
.title {

View File

@ -11,42 +11,33 @@ export default function TokenList({
baseTokenSymbol,
datatokenValue,
datatokenSymbol,
poolShares,
conversion,
highlight,
showTVLLabel
highlight
}: {
title: string | ReactNode
children: ReactNode
children?: ReactNode
baseTokenValue: string
baseTokenSymbol: string
datatokenValue: string
datatokenSymbol: string
poolShares: string
datatokenValue?: string
datatokenSymbol?: string
conversion: Decimal
highlight?: boolean
showTVLLabel?: boolean
}): ReactElement {
return (
<div className={`${styles.tokenlist} ${highlight ? styles.highlight : ''}`}>
<h3 className={styles.title}>{title}</h3>
<div className={styles.tokens}>
<div>
<Token symbol={baseTokenSymbol} balance={baseTokenValue} />
<Token symbol={baseTokenSymbol} balance={baseTokenValue} />
{datatokenValue && (
<Token symbol={datatokenSymbol} balance={datatokenValue} />
{conversion.greaterThan(0) && (
<Conversion
price={conversion.toString()}
className={styles.totalLiquidity}
showTVLLabel={showTVLLabel}
/>
)}
</div>
<div>
<Token symbol="pool shares" balance={poolShares} noIcon />
{children}
</div>
)}
{conversion.greaterThan(0) && (
<Conversion
price={conversion.toString()}
className={styles.totalLiquidity}
/>
)}
{children}
</div>
</div>
)

View File

@ -1,5 +1,4 @@
import React, { ReactElement, useCallback, useEffect, useState } from 'react'
import { LoggerInstance } from '@oceanprotocol/lib'
import React, { ReactElement, useState } from 'react'
import styles from './index.module.css'
import stylesActions from './Actions.module.css'
import PriceUnit from '@shared/Price/PriceUnit'
@ -8,314 +7,33 @@ import Add from './Add'
import Remove from './Remove'
import Tooltip from '@shared/atoms/Tooltip'
import ExplorerLink from '@shared/ExplorerLink'
import Token from './Token'
import TokenList from './TokenList'
import AssetActionHistoryTable from '../AssetActionHistoryTable'
import Graph from './Graph'
import { useAsset } from '@context/Asset'
import { useWeb3 } from '@context/Web3'
import PoolTransactions from '@shared/PoolTransactions'
import { isValidNumber } from '@utils/numbers'
import Decimal from 'decimal.js'
import content from '../../../../../content/price.json'
import { getPoolData } from '@utils/subgraph'
import {
PoolData_poolSnapshots as PoolDataPoolSnapshots,
PoolData_poolData as PoolDataPoolData
} from 'src/@types/subgraph/PoolData'
Decimal.set({ toExpNeg: -18, precision: 18, rounding: 1 })
function getWeight(weight: string) {
return isValidNumber(weight) ? new Decimal(weight).mul(10).toString() : '0'
}
interface PoolInfo {
poolFee: string
weightBaseToken: string
weightDt: string
datatokenSymbol: string
baseTokenSymbol: string
baseTokenAddress: string
totalPoolTokens: string
totalLiquidityInOcean: Decimal
}
interface PoolInfoUser {
totalLiquidityInOcean: Decimal
liquidity: PoolBalance
poolShares: string
poolShare: string // in %
}
const initialPoolInfo: Partial<PoolInfo> = {
totalLiquidityInOcean: new Decimal(0)
}
const initialPoolInfoUser: Partial<PoolInfoUser> = {
totalLiquidityInOcean: new Decimal(0)
}
const initialPoolInfoCreator: Partial<PoolInfoUser> = initialPoolInfoUser
import { usePool } from '@context/Pool'
export default function Pool(): ReactElement {
const { accountId } = useWeb3()
const { isInPurgatory, asset, owner, refreshInterval, isAssetNetwork } =
useAsset()
const { isInPurgatory, asset, isAssetNetwork } = useAsset()
const {
poolData,
poolInfo,
poolInfoUser,
poolInfoOwner,
poolSnapshots,
hasUserAddedLiquidity,
isRemoveDisabled,
refreshInterval,
fetchAllData
} = usePool()
const [poolData, setPoolData] = useState<PoolDataPoolData>()
const [poolInfo, setPoolInfo] = useState<PoolInfo>(
initialPoolInfo as PoolInfo
)
const [poolInfoOwner, setPoolInfoOwner] = useState<PoolInfoUser>(
initialPoolInfoCreator as PoolInfoUser
)
const [poolInfoUser, setPoolInfoUser] = useState<PoolInfoUser>(
initialPoolInfoUser as PoolInfoUser
)
const [poolSnapshots, setPoolSnapshots] = useState<PoolDataPoolSnapshots[]>()
const [hasUserAddedLiquidity, setUserHasAddedLiquidity] = useState(false)
const [showAdd, setShowAdd] = useState(false)
const [showRemove, setShowRemove] = useState(false)
const [isRemoveDisabled, setIsRemoveDisabled] = useState(false)
const [fetchInterval, setFetchInterval] = useState<NodeJS.Timeout>()
const fetchAllData = useCallback(async () => {
if (!asset?.chainId || !asset?.accessDetails?.addressOrId || !owner) return
const response = await getPoolData(
asset.chainId,
asset.accessDetails.addressOrId,
owner,
accountId || ''
)
if (!response) return
setPoolData(response.poolData)
setPoolInfoUser((prevState) => ({
...prevState,
poolShares: response.poolDataUser?.shares[0]?.shares
}))
setPoolSnapshots(response.poolSnapshots)
LoggerInstance.log('[pool] Fetched pool data:', response.poolData)
LoggerInstance.log('[pool] Fetched user data:', response.poolDataUser)
LoggerInstance.log('[pool] Fetched pool snapshots:', response.poolSnapshots)
}, [asset?.chainId, asset?.accessDetails?.addressOrId, owner, accountId])
// Helper: start interval fetching
const initFetchInterval = useCallback(() => {
if (fetchInterval) return
const newInterval = setInterval(() => {
fetchAllData()
LoggerInstance.log(
`[pool] Refetch interval fired after ${refreshInterval / 1000}s`
)
}, refreshInterval)
setFetchInterval(newInterval)
}, [fetchInterval, fetchAllData, refreshInterval])
useEffect(() => {
return () => {
clearInterval(fetchInterval)
}
}, [fetchInterval])
//
// 0 Fetch all the data on mount
// All further effects depend on the fetched data
// and only do further data checking and manipulation.
//
useEffect(() => {
fetchAllData()
initFetchInterval()
}, [fetchAllData, initFetchInterval])
//
// 1 General Pool Info
//
useEffect(() => {
if (!poolData) return
// Pool Fee (swap fee)
// poolFee is tricky: to get 0.1% you need to convert from 0.001
const poolFee = isValidNumber(poolData.poolFee)
? new Decimal(poolData.poolFee).mul(100).toString()
: '0'
// Total Liquidity
const totalLiquidityInOcean = isValidNumber(poolData.spotPrice)
? new Decimal(poolData.baseTokenLiquidity).add(
new Decimal(poolData.datatokenLiquidity).mul(poolData.spotPrice)
)
: new Decimal(0)
const newPoolInfo = {
poolFee,
weightBaseToken: getWeight(poolData.baseTokenWeight),
weightDt: getWeight(poolData.datatokenWeight),
datatokenSymbol: poolData.datatoken.symbol,
baseTokenSymbol: poolData.baseToken.symbol,
baseTokenAddress: poolData.baseToken.address,
totalPoolTokens: poolData.totalShares,
totalLiquidityInOcean
}
setPoolInfo(newPoolInfo)
LoggerInstance.log('[pool] Created new pool info:', newPoolInfo)
}, [poolData])
//
// 2 Pool Creator Info
//
useEffect(() => {
if (!poolData || !poolInfo?.totalPoolTokens) return
const ownerPoolTokens = poolData.shares[0]?.shares
const ownerBaseTokenBalance =
isValidNumber(ownerPoolTokens) &&
isValidNumber(poolInfo.totalPoolTokens) &&
isValidNumber(poolData.baseTokenLiquidity)
? new Decimal(ownerPoolTokens)
.dividedBy(new Decimal(poolInfo.totalPoolTokens))
.mul(poolData.baseTokenLiquidity)
.toString()
: '0'
const ownerDtBalance =
isValidNumber(ownerPoolTokens) &&
isValidNumber(poolInfo.totalPoolTokens) &&
isValidNumber(poolData.datatokenLiquidity)
? new Decimal(ownerPoolTokens)
.dividedBy(new Decimal(poolInfo.totalPoolTokens))
.mul(poolData.datatokenLiquidity)
.toString()
: '0'
const liquidity = {
baseToken: ownerBaseTokenBalance,
datatoken: ownerDtBalance
}
const totalLiquidityInOcean =
isValidNumber(liquidity.baseToken) &&
isValidNumber(liquidity.datatoken) &&
isValidNumber(poolData.spotPrice)
? new Decimal(liquidity.baseToken).add(
new Decimal(liquidity.datatoken).mul(
new Decimal(poolData.spotPrice)
)
)
: new Decimal(0)
const poolShare =
liquidity &&
isValidNumber(ownerPoolTokens) &&
isValidNumber(poolInfo.totalPoolTokens)
? new Decimal(ownerPoolTokens)
.dividedBy(new Decimal(poolInfo.totalPoolTokens))
.mul(100)
.toFixed(2)
: '0'
const newPoolOwnerInfo = {
totalLiquidityInOcean,
liquidity,
poolShares: ownerPoolTokens,
poolShare
}
setPoolInfoOwner(newPoolOwnerInfo)
LoggerInstance.log('[pool] Created new owner pool info:', newPoolOwnerInfo)
}, [poolData, poolInfo?.totalPoolTokens])
//
// 3 User Pool Info
//
useEffect(() => {
if (
!poolData ||
!poolInfo?.totalPoolTokens ||
!asset?.chainId ||
!accountId
)
return
const poolShare =
isValidNumber(poolInfoUser.poolShares) &&
isValidNumber(poolInfo.totalPoolTokens) &&
new Decimal(poolInfoUser.poolShares)
.dividedBy(new Decimal(poolInfo.totalPoolTokens))
.mul(100)
.toFixed(5)
setUserHasAddedLiquidity(Number(poolShare) > 0)
// calculate user's provided liquidity based on pool tokens
const userBaseTokenBalance =
isValidNumber(poolInfoUser.poolShares) &&
isValidNumber(poolInfo.totalPoolTokens) &&
isValidNumber(poolData.baseTokenLiquidity)
? new Decimal(poolInfoUser.poolShares)
.dividedBy(new Decimal(poolInfo.totalPoolTokens))
.mul(poolData.baseTokenLiquidity)
.toString()
: '0'
const userDtBalance =
isValidNumber(poolInfoUser.poolShares) &&
isValidNumber(poolInfo.totalPoolTokens) &&
isValidNumber(poolData.datatokenLiquidity)
? new Decimal(poolInfoUser.poolShares)
.dividedBy(new Decimal(poolInfo.totalPoolTokens))
.mul(poolData.datatokenLiquidity)
.toString()
: '0'
const liquidity = {
baseToken: userBaseTokenBalance,
datatoken: userDtBalance
}
const totalLiquidityInOcean =
isValidNumber(liquidity.baseToken) &&
isValidNumber(liquidity.datatoken) &&
isValidNumber(poolData.spotPrice)
? new Decimal(liquidity.baseToken).add(
new Decimal(liquidity.datatoken).mul(poolData.spotPrice)
)
: new Decimal(0)
const newPoolInfoUser = {
totalLiquidityInOcean,
liquidity,
poolShare
}
setPoolInfoUser((prevState: PoolInfoUser) => ({
...prevState,
...newPoolInfoUser
}))
LoggerInstance.log('[pool] Created new user pool info:', {
poolShares: poolInfoUser?.poolShares,
...newPoolInfoUser
})
}, [
poolData,
poolInfoUser?.poolShares,
accountId,
asset?.chainId,
owner,
poolInfo?.totalPoolTokens
])
//
// Check if removing liquidity should be disabled.
//
useEffect(() => {
if (!owner || !accountId) return
setIsRemoveDisabled(isInPurgatory && owner === accountId)
}, [isInPurgatory, owner, accountId])
return (
<>
@ -364,8 +82,8 @@ export default function Pool(): ReactElement {
networkId={asset?.chainId}
path={
asset?.chainId === 2021000 || asset?.chainId === 1287
? `tokens/${asset.services[0].datatokenAddress}`
: `token/${asset.services[0].datatokenAddress}`
? `tokens/${asset?.services[0].datatokenAddress}`
: `token/${asset?.services[0].datatokenAddress}`
}
>
Datatoken
@ -383,38 +101,32 @@ export default function Pool(): ReactElement {
poolInfo?.poolFee
)}
/>
{poolInfoUser?.poolShare && (
<span className={styles.titleInfo}>
{poolInfoUser?.poolShare}% of pool
</span>
)}
</>
}
baseTokenValue={`${poolInfoUser?.liquidity?.baseToken}`}
baseTokenValue={poolInfoUser?.liquidity.toString()}
baseTokenSymbol={poolInfo?.baseTokenSymbol}
datatokenValue={`${poolInfoUser?.liquidity?.datatoken}`}
datatokenSymbol={poolInfo?.datatokenSymbol}
poolShares={poolInfoUser?.poolShares}
conversion={poolInfoUser?.totalLiquidityInOcean}
conversion={poolInfoUser?.liquidity}
highlight
>
<Token
symbol="% of pool"
balance={poolInfoUser?.poolShare}
noIcon
/>
</TokenList>
/>
<TokenList
title="Pool Creator Statistics"
baseTokenValue={`${poolInfoOwner?.liquidity?.baseToken}`}
title={
<>
Owner Liquidity
<span className={styles.titleInfo}>
{poolInfoOwner?.poolShare}% of pool
</span>
</>
}
baseTokenValue={poolInfoOwner?.liquidity.toString()}
baseTokenSymbol={poolInfo?.baseTokenSymbol}
datatokenValue={`${poolInfoOwner?.liquidity?.datatoken}`}
datatokenSymbol={poolInfo?.datatokenSymbol}
poolShares={poolInfoOwner?.poolShares}
conversion={poolInfoOwner?.totalLiquidityInOcean}
>
<Token
symbol="% of pool"
balance={poolInfoOwner?.poolShare}
noIcon
/>
</TokenList>
conversion={poolInfoOwner?.liquidity}
/>
<TokenList
title={
@ -435,11 +147,11 @@ export default function Pool(): ReactElement {
baseTokenSymbol={poolInfo?.baseTokenSymbol}
datatokenValue={`${poolData?.datatokenLiquidity}`}
datatokenSymbol={poolInfo?.datatokenSymbol}
poolShares={poolInfo?.totalPoolTokens}
conversion={poolInfo?.totalLiquidityInOcean}
showTVLLabel
>
<Token symbol="% pool fee" balance={poolInfo?.poolFee} noIcon />
{/* <Token symbol="% pool fee" balance={poolInfo?.poolFee} noIcon />
<Token symbol="% market fee" balance={poolInfo?.marketFee} noIcon />
<Token symbol="% OPF fee" balance={poolInfo?.opfFee} noIcon /> */}
</TokenList>
<div className={styles.update}>

View File

@ -17,6 +17,7 @@ import styles from './index.module.css'
import { useFormikContext } from 'formik'
import { FormPublishData } from 'src/components/Publish/_types'
import { AssetExtended } from 'src/@types/AssetExtended'
import PoolProvider from '@context/Pool'
export default function AssetActions({
asset
@ -174,11 +175,13 @@ export default function AssetActions({
return (
<>
<Tabs items={tabs} className={styles.actions} />
<Web3Feedback
networkId={asset?.chainId}
isAssetNetwork={isAssetNetwork}
/>
<PoolProvider>
<Tabs items={tabs} className={styles.actions} />
<Web3Feedback
networkId={asset?.chainId}
isAssetNetwork={isAssetNetwork}
/>
</PoolProvider>
</>
)
}