Pyth history feed (#623)

* MP-3556: first PythDataFeed class

* fix: fixed the timestamp

* fix: fixed the chart data

* fix: fixed the prices

* fix: fixed the math and decimal scale

* tidy: refactor

* fix: update the pythFeedId

* fix: updated OsmosisTheGraphDataFeed

* fix: add a fallback for non pyth data

* tidy: refactor

* fix: adjusted to feedback
This commit is contained in:
Linkie Link 2023-11-10 13:56:17 +01:00 committed by GitHub
parent 7439bea0d8
commit a8dc0950fa
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
13 changed files with 258 additions and 85 deletions

View File

@ -28,7 +28,8 @@ NEXT_PUBLIC_ZAPPER=osmo17qwvc70pzc9mudr8t02t3pl74hhqsgwnskl734p4hug3s8mkerdqzduf
NEXT_PUBLIC_PARAMS=osmo1nlmdxt9ctql2jr47qd4fpgzg84cjswxyw6q99u4y4u4q6c2f5ksq7ysent NEXT_PUBLIC_PARAMS=osmo1nlmdxt9ctql2jr47qd4fpgzg84cjswxyw6q99u4y4u4q6c2f5ksq7ysent
NEXT_PUBLIC_PYTH_ENDPOINT=https://hermes.pyth.network/api NEXT_PUBLIC_PYTH_ENDPOINT=https://hermes.pyth.network/api
NEXT_PUBLIC_MAINNET_REST=https://osmosis-node.marsprotocol.io/GGSFGSFGFG34/osmosis-rpc-front/ NEXT_PUBLIC_MAINNET_REST=https://osmosis-node.marsprotocol.io/GGSFGSFGFG34/osmosis-rpc-front/
NEXT_PUBLIC_CANDLES_ENDPOINT=https://osmosis-candles.marsprotocol.io/ NEXT_PUBLIC_CANDLES_ENDPOINT_THE_GRAPH=https://osmosis-candles.marsprotocol.io/
NEXT_PUBLIC_CANDLES_ENDPOINT_PYTH=https://benchmarks.pyth.network
NEXT_PUBLIC_WALLET_CONNECT_ID=d93fdffb159bae5ec87d8fee4cdbb045 NEXT_PUBLIC_WALLET_CONNECT_ID=d93fdffb159bae5ec87d8fee4cdbb045
CHARTING_LIBRARY_REPOSITORY=github.com/tradingview/charting_library CHARTING_LIBRARY_REPOSITORY=github.com/tradingview/charting_library
CHARTING_LIBRARY_ACCESS_TOKEN=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 CHARTING_LIBRARY_ACCESS_TOKEN=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

View File

@ -1,10 +1,5 @@
import { getSwapperQueryClient } from 'api/cosmwasm-client' import { getSwapperQueryClient } from 'api/cosmwasm-client'
interface Route {
pool_id: string
token_out_denom: string
}
export default async function getSwapRoute(denomIn: string, denomOut: string): Promise<Route[]> { export default async function getSwapRoute(denomIn: string, denomOut: string): Promise<Route[]> {
try { try {
const swapperClient = await getSwapperQueryClient() const swapperClient = await getSwapperQueryClient()

View File

@ -1,6 +1,8 @@
import { defaultSymbolInfo } from 'components/Trade/TradeChart/constants' import { defaultSymbolInfo } from 'components/Trade/TradeChart/constants'
import { ASSETS } from 'constants/assets' import { ASSETS } from 'constants/assets'
import { ENV } from 'constants/env' import { ENV } from 'constants/env'
import { MILLISECONDS_PER_MINUTE } from 'constants/math'
import { byDenom } from 'utils/array'
import { getAssetByDenom, getEnabledMarketAssets } from 'utils/assets' import { getAssetByDenom, getEnabledMarketAssets } from 'utils/assets'
import { import {
Bar, Bar,
@ -14,8 +16,19 @@ import {
ResolveCallback, ResolveCallback,
} from 'utils/charting_library' } from 'utils/charting_library'
import { BN } from 'utils/helpers' import { BN } from 'utils/helpers'
import { devideByPotentiallyZero } from 'utils/math'
interface BarQueryData { interface PythBarQueryData {
s: string
t: number[]
o: number[]
h: number[]
l: number[]
c: number[]
v: number[]
}
interface TheGraphBarQueryData {
close: string close: string
high: string high: string
low: string low: string
@ -26,41 +39,40 @@ interface BarQueryData {
export const PAIR_SEPARATOR = '<>' export const PAIR_SEPARATOR = '<>'
export class OsmosisTheGraphDataFeed implements IDatafeedChartApi { export class DataFeed implements IDatafeedChartApi {
candlesEndpoint = ENV.CANDLES_ENDPOINT candlesEndpoint = ENV.CANDLES_ENDPOINT_PYTH
candlesEndpointTheGraph = ENV.CANDLES_ENDPOINT_THE_GRAPH
debug = false debug = false
exchangeName = 'Osmosis' enabledMarketAssetDenoms: string[] = []
batchSize = 1000
baseDecimals: number = 6 baseDecimals: number = 6
baseDenom: string = 'uosmo' baseDenom: string = 'uosmo'
batchSize = 1000 intervalsTheGraph: { [key: string]: string } = {
enabledMarketAssetDenoms: string[] = []
pairs: { baseAsset: string; quoteAsset: string }[] = []
pairsWithData: string[] = []
intervals: { [key: string]: string } = {
'15': '15m', '15': '15m',
'30': '30m', '30': '30m',
'60': '1h', '60': '1h',
'240': '4h', '240': '4h',
'1D': '1d', '1D': '1d',
} }
minutesPerInterval: { [key: string]: number } = { millisecondsPerInterval: { [key: string]: number } = {
'15': 15, '15': MILLISECONDS_PER_MINUTE * 15,
'30': 30, '30': MILLISECONDS_PER_MINUTE * 30,
'60': 60, '60': MILLISECONDS_PER_MINUTE * 60,
'240': 60 * 4, '240': MILLISECONDS_PER_MINUTE * 240,
'1D': 60 * 24, '1D': MILLISECONDS_PER_MINUTE * 1440,
} }
pairs: { baseAsset: string; quoteAsset: string }[] = []
pairsWithData: string[] = []
supportedPools: string[] = [] supportedPools: string[] = []
supportedResolutions = ['15', '30', '60', '4h', 'D'] as ResolutionString[] supportedResolutions = ['15', '30', '60', '240', 'D'] as ResolutionString[]
constructor(debug = false, baseDecimals: number, baseDenom: string) { constructor(debug = false, baseDecimals: number, baseDenom: string) {
if (debug) console.log('Start TheGraph charting library datafeed') if (debug) console.log('Start charting library datafeed')
this.debug = debug this.debug = debug
this.baseDecimals = baseDecimals this.baseDecimals = baseDecimals
this.baseDenom = baseDenom this.baseDenom = baseDenom
const enabledMarketAssets = getEnabledMarketAssets() const enabledMarketAssets = getEnabledMarketAssets()
this.enabledMarketAssetDenoms = enabledMarketAssets.map((asset) => asset.mainnetDenom) this.enabledMarketAssetDenoms = enabledMarketAssets.map((asset) => asset.denom)
this.supportedPools = enabledMarketAssets this.supportedPools = enabledMarketAssets
.map((asset) => asset.poolId?.toString()) .map((asset) => asset.poolId?.toString())
.filter((poolId) => typeof poolId === 'string') as string[] .filter((poolId) => typeof poolId === 'string') as string[]
@ -69,8 +81,8 @@ export class OsmosisTheGraphDataFeed implements IDatafeedChartApi {
getDescription(pairName: string) { getDescription(pairName: string) {
const denom1 = pairName.split(PAIR_SEPARATOR)[0] const denom1 = pairName.split(PAIR_SEPARATOR)[0]
const denom2 = pairName.split(PAIR_SEPARATOR)[1] const denom2 = pairName.split(PAIR_SEPARATOR)[1]
const asset1 = ASSETS.find((asset) => asset.mainnetDenom === denom1) const asset1 = ASSETS.find(byDenom(denom1))
const asset2 = ASSETS.find((asset) => asset.mainnetDenom === denom2) const asset2 = ASSETS.find(byDenom(denom2))
return `${asset1?.symbol}/${asset2?.symbol}` return `${asset1?.symbol}/${asset2?.symbol}`
} }
@ -91,7 +103,7 @@ export class OsmosisTheGraphDataFeed implements IDatafeedChartApi {
} }
}` }`
return fetch(this.candlesEndpoint, { return fetch(this.candlesEndpointTheGraph, {
method: 'POST', method: 'POST',
headers: { 'Content-Type': 'application/json' }, headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ query }), body: JSON.stringify({ query }),
@ -131,10 +143,11 @@ export class OsmosisTheGraphDataFeed implements IDatafeedChartApi {
full_name: this.getDescription(pairName), full_name: this.getDescription(pairName),
description: this.getDescription(pairName), description: this.getDescription(pairName),
ticker: this.getDescription(pairName), ticker: this.getDescription(pairName),
exchange: this.exchangeName, exchange: this.getExchangeName(pairName),
listed_exchange: this.exchangeName, listed_exchange: this.getExchangeName(pairName),
supported_resolutions: this.supportedResolutions, supported_resolutions: this.supportedResolutions,
base_name: [this.getDescription(pairName)], base_name: [this.getDescription(pairName)],
pricescale: this.getPriceScale(pairName),
} as LibrarySymbolInfo } as LibrarySymbolInfo
onResolve(info) onResolve(info)
}) })
@ -146,11 +159,45 @@ export class OsmosisTheGraphDataFeed implements IDatafeedChartApi {
periodParams: PeriodParams, periodParams: PeriodParams,
onResult: HistoryCallback, onResult: HistoryCallback,
): Promise<void> { ): Promise<void> {
const interval = this.intervals[resolution] try {
let bars = [] as Bar[]
const pythFeedIds = this.getPythFeedIds(symbolInfo.full_name)
const now = new Date().getTime()
const to = BN(now).dividedBy(1000).integerValue().toNumber()
const from = BN(now)
.minus(this.batchSize * this.millisecondsPerInterval[resolution])
.dividedBy(1000)
.integerValue()
.toNumber()
const pythFeedId1 = pythFeedIds[0]
const pythFeedId2 = pythFeedIds[1]
if (pythFeedId1 && pythFeedId2) {
const asset1Bars = this.queryBarData(pythFeedId1, resolution, from, to)
const asset2Bars = this.queryBarData(pythFeedId2, resolution, from, to)
await Promise.all([asset1Bars, asset2Bars]).then(([asset1Bars, asset2Bars]) => {
bars = this.combineBars(asset1Bars, asset2Bars)
onResult(bars)
})
} else {
await this.getBarsFromTheGraph(symbolInfo, resolution, to).then((bars) => onResult(bars))
}
} catch (error) {
console.error(error)
return onResult([], { noData: true })
}
}
async getBarsFromTheGraph(
symbolInfo: LibrarySymbolInfo,
resolution: ResolutionString,
to: number,
) {
let pair1 = this.getPairName(symbolInfo.full_name) let pair1 = this.getPairName(symbolInfo.full_name)
let pair2: string = '' let pair2: string = ''
let pair3: string = '' let pair3: string = ''
let theGraphBars = [] as Bar[]
if (!this.pairsWithData.includes(pair1)) { if (!this.pairsWithData.includes(pair1)) {
if (this.debug) console.log('Pair does not have data, need to combine with other pairs') if (this.debug) console.log('Pair does not have data, need to combine with other pairs')
@ -181,29 +228,32 @@ export class OsmosisTheGraphDataFeed implements IDatafeedChartApi {
} }
} }
const pair1Bars = this.queryBarData( const pair1Bars = this.queryBarDataTheGraph(
pair1.split(PAIR_SEPARATOR)[0], pair1.split(PAIR_SEPARATOR)[0],
pair1.split(PAIR_SEPARATOR)[1], pair1.split(PAIR_SEPARATOR)[1],
interval, resolution,
to,
) )
let pair2Bars: Promise<Bar[]> | null = null let pair2Bars: Promise<Bar[]> | null = null
if (pair2) { if (pair2) {
pair2Bars = this.queryBarData( pair2Bars = this.queryBarDataTheGraph(
pair2.split(PAIR_SEPARATOR)[0], pair2.split(PAIR_SEPARATOR)[0],
pair2.split(PAIR_SEPARATOR)[1], pair2.split(PAIR_SEPARATOR)[1],
interval, resolution,
to,
) )
} }
let pair3Bars: Promise<Bar[]> | null = null let pair3Bars: Promise<Bar[]> | null = null
if (pair3) { if (pair3) {
pair3Bars = this.queryBarData( pair3Bars = this.queryBarDataTheGraph(
pair3.split(PAIR_SEPARATOR)[0], pair3.split(PAIR_SEPARATOR)[0],
pair3.split(PAIR_SEPARATOR)[1], pair3.split(PAIR_SEPARATOR)[1],
interval, resolution,
to,
) )
} }
@ -212,7 +262,6 @@ export class OsmosisTheGraphDataFeed implements IDatafeedChartApi {
let bars = pair1Bars let bars = pair1Bars
if (!bars.length) { if (!bars.length) {
onResult([], { noData: true })
return return
} }
@ -225,20 +274,55 @@ export class OsmosisTheGraphDataFeed implements IDatafeedChartApi {
const filler = Array.from({ length: this.batchSize - bars.length }).map((_, index) => ({ const filler = Array.from({ length: this.batchSize - bars.length }).map((_, index) => ({
time: time:
(bars[0]?.time || new Date().getTime()) - index * this.minutesPerInterval[resolution], (bars[0]?.time || new Date().getTime()) -
(index * this.millisecondsPerInterval[resolution]) / 1000,
close: 0, close: 0,
open: 0, open: 0,
high: 0, high: 0,
low: 0, low: 0,
volume: 0, volume: 0,
})) }))
theGraphBars = [...filler, ...bars]
onResult([...filler, ...bars])
}, },
) )
return theGraphBars
} }
async queryBarData(quote: string, base: string, interval: string): Promise<Bar[]> { async queryBarData(
feedSymbol: string,
resolution: ResolutionString,
from: PeriodParams['from'],
to: PeriodParams['to'],
): Promise<Bar[]> {
const URI = new URL('/v1/shims/tradingview/history', this.candlesEndpoint)
const params = new URLSearchParams(URI.search)
params.append('to', to.toString())
params.append('from', from.toString())
params.append('resolution', resolution)
params.append('symbol', feedSymbol)
URI.search = params.toString()
return fetch(URI)
.then((res) => res.json())
.then((json) => {
return this.resolveBarData(json, resolution, to)
})
.catch((err) => {
if (this.debug) console.error(err)
throw err
})
}
async queryBarDataTheGraph(
quote: string,
base: string,
resolution: ResolutionString,
to: PeriodParams['to'],
): Promise<Bar[]> {
const interval = this.intervalsTheGraph[resolution]
const query = ` const query = `
{ {
candles( candles(
@ -262,14 +346,20 @@ export class OsmosisTheGraphDataFeed implements IDatafeedChartApi {
} }
` `
return fetch(this.candlesEndpoint, { return fetch(this.candlesEndpointTheGraph, {
method: 'POST', method: 'POST',
headers: { 'Content-Type': 'application/json' }, headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ query }), body: JSON.stringify({ query }),
}) })
.then((res) => res.json()) .then((res) => res.json())
.then((json: { data?: { candles: BarQueryData[] } }) => { .then((json: { data?: { candles: TheGraphBarQueryData[] } }) => {
return this.resolveBarData(json.data?.candles.reverse() || [], base, quote) return this.resolveBarDataTheGraph(
json.data?.candles.reverse() || [],
base,
quote,
resolution,
to,
)
}) })
.catch((err) => { .catch((err) => {
if (this.debug) console.error(err) if (this.debug) console.error(err)
@ -277,12 +367,35 @@ export class OsmosisTheGraphDataFeed implements IDatafeedChartApi {
}) })
} }
resolveBarData(bars: BarQueryData[], toDenom: string, fromDenom: string) { resolveBarData(data: PythBarQueryData, resolution: ResolutionString, to: number) {
let barData = [] as Bar[]
if (data['s'] === 'ok') {
barData = data['t'].map((timestamp, index) => ({
time: timestamp * 1000,
close: data['c'][index],
open: data['o'][index],
high: data['h'][index],
low: data['l'][index],
}))
}
return this.fillBarData(barData, resolution, to)
}
resolveBarDataTheGraph(
bars: TheGraphBarQueryData[],
toDenom: string,
fromDenom: string,
resolution: ResolutionString,
to: number,
) {
let barData = [] as Bar[]
const toDecimals = getAssetByDenom(toDenom)?.decimals || 6 const toDecimals = getAssetByDenom(toDenom)?.decimals || 6
const fromDecimals = getAssetByDenom(fromDenom)?.decimals || 6 const fromDecimals = getAssetByDenom(fromDenom)?.decimals || 6
const additionalDecimals = toDecimals - fromDecimals const additionalDecimals = toDecimals - fromDecimals
return bars.map((bar) => ({ barData = bars.map((bar) => ({
time: BN(bar.timestamp).multipliedBy(1000).toNumber(), time: BN(bar.timestamp).multipliedBy(1000).toNumber(),
close: BN(bar.close).shiftedBy(additionalDecimals).toNumber(), close: BN(bar.close).shiftedBy(additionalDecimals).toNumber(),
open: BN(bar.open).shiftedBy(additionalDecimals).toNumber(), open: BN(bar.open).shiftedBy(additionalDecimals).toNumber(),
@ -290,23 +403,40 @@ export class OsmosisTheGraphDataFeed implements IDatafeedChartApi {
low: BN(bar.low).shiftedBy(additionalDecimals).toNumber(), low: BN(bar.low).shiftedBy(additionalDecimals).toNumber(),
volume: BN(bar.volume).shiftedBy(additionalDecimals).toNumber(), volume: BN(bar.volume).shiftedBy(additionalDecimals).toNumber(),
})) }))
return this.fillBarData(barData, resolution, to)
}
fillBarData(barData: Bar[], resolution: ResolutionString, to: number) {
if (barData.length < this.batchSize) {
const filler = Array.from({ length: this.batchSize - barData.length }).map((_, index) => ({
time: (barData[0]?.time || to) - index * this.millisecondsPerInterval[resolution],
close: 0,
open: 0,
high: 0,
low: 0,
volume: 0,
}))
barData = [...filler, ...barData]
}
return barData.length > this.batchSize ? barData.slice(0, this.batchSize) : barData
} }
combineBars(pair1Bars: Bar[], pair2Bars: Bar[]): Bar[] { combineBars(pair1Bars: Bar[], pair2Bars: Bar[]): Bar[] {
const bars: Bar[] = [] const bars: Bar[] = []
pair1Bars.forEach((pair1Bar) => { pair1Bars.forEach((pair1Bar, index) => {
const pair2Bar = pair2Bars.find((pair2Bar) => pair2Bar.time == pair1Bar.time) const pair2Bar = pair2Bars[index]
if (pair2Bar) { bars.push({
bars.push({ time: pair1Bar.time,
time: pair1Bar.time, open: devideByPotentiallyZero(pair1Bar.open, pair2Bar.open),
open: pair1Bar.open * pair2Bar.open, close: devideByPotentiallyZero(pair1Bar.close, pair2Bar.close),
close: pair1Bar.close * pair2Bar.close, high: devideByPotentiallyZero(pair1Bar.high, pair2Bar.high),
high: pair1Bar.high * pair2Bar.high, low: devideByPotentiallyZero(pair1Bar.low, pair2Bar.low),
low: pair1Bar.low * pair2Bar.low, })
})
}
}) })
return bars return bars
} }
@ -321,6 +451,32 @@ export class OsmosisTheGraphDataFeed implements IDatafeedChartApi {
return `${asset1?.denom}${PAIR_SEPARATOR}${asset2?.denom}` return `${asset1?.denom}${PAIR_SEPARATOR}${asset2?.denom}`
} }
getPriceScale(name: string) {
const denoms = name.split(PAIR_SEPARATOR)
const asset2 = ASSETS.find(byDenom(denoms[1]))
const decimalsOut = asset2?.decimals ?? 6
return BN(1)
.shiftedBy(decimalsOut > 8 ? 8 : decimalsOut)
.toNumber()
}
getExchangeName(name: string) {
const denoms = name.split(PAIR_SEPARATOR)
const pythFeedId1 = ASSETS.find(byDenom(denoms[0]))?.pythHistoryFeedId
const pythFeedId2 = ASSETS.find(byDenom(denoms[1]))?.pythHistoryFeedId
if (!pythFeedId1 || !pythFeedId2) return 'Osmosis'
return 'Pyth Oracle'
}
getPythFeedIds(name: string) {
if (name.includes(PAIR_SEPARATOR)) return []
const [symbol1, symbol2] = name.split('/')
const feedId1 = ASSETS.find((asset) => asset.symbol === symbol1)?.pythHistoryFeedId
const feedId2 = ASSETS.find((asset) => asset.symbol === symbol2)?.pythHistoryFeedId
return [feedId1, feedId2]
}
searchSymbols(): void { searchSymbols(): void {
// Don't allow to search for symbols // Don't allow to search for symbols
} }

View File

@ -1,11 +1,8 @@
import { useEffect, useMemo, useRef } from 'react' import { useEffect, useMemo, useRef } from 'react'
import Card from 'components/Card' import Card from 'components/Card'
import { DataFeed, PAIR_SEPARATOR } from 'components/Trade/TradeChart/DataFeed'
import { disabledFeatures, enabledFeatures, overrides } from 'components/Trade/TradeChart/constants' import { disabledFeatures, enabledFeatures, overrides } from 'components/Trade/TradeChart/constants'
import {
OsmosisTheGraphDataFeed,
PAIR_SEPARATOR,
} from 'components/Trade/TradeChart/OsmosisTheGraphDataFeed'
import useStore from 'store' import useStore from 'store'
import { import {
ChartingLibraryWidgetOptions, ChartingLibraryWidgetOptions,
@ -28,7 +25,7 @@ export const TVChartContainer = (props: Props) => {
) )
const baseCurrency = useStore((s) => s.baseCurrency) const baseCurrency = useStore((s) => s.baseCurrency)
const dataFeed = useMemo( const dataFeed = useMemo(
() => new OsmosisTheGraphDataFeed(false, baseCurrency.decimals, baseCurrency.denom), () => new DataFeed(false, baseCurrency.decimals, baseCurrency.denom),
[baseCurrency], [baseCurrency],
) )

View File

@ -28,7 +28,7 @@ export const overrides = {
} }
export const defaultSymbolInfo: Partial<LibrarySymbolInfo> = { export const defaultSymbolInfo: Partial<LibrarySymbolInfo> = {
listed_exchange: 'Osmosis', listed_exchange: 'Pyth Oracle',
type: 'AMM', type: 'AMM',
session: '24x7', session: '24x7',
minmov: 1, minmov: 1,

View File

@ -3,7 +3,6 @@ import { useMemo } from 'react'
import ActionButton from 'components/Button/ActionButton' import ActionButton from 'components/Button/ActionButton'
import { FormattedNumber } from 'components/FormattedNumber' import { FormattedNumber } from 'components/FormattedNumber'
import useSwapRoute from 'hooks/useSwapRoute'
import { getAssetByDenom } from 'utils/assets' import { getAssetByDenom } from 'utils/assets'
import { formatAmountWithSymbol, formatPercent } from 'utils/formatters' import { formatAmountWithSymbol, formatPercent } from 'utils/formatters'
@ -18,7 +17,9 @@ interface Props {
borrowAmount: BigNumber borrowAmount: BigNumber
estimatedFee: StdFee estimatedFee: StdFee
buyAction: () => void buyAction: () => void
route: Route[]
} }
const infoLineClasses = 'flex flex-row justify-between flex-1 mb-1 text-xs text-white'
export default function TradeSummary(props: Props) { export default function TradeSummary(props: Props) {
const { const {
@ -32,25 +33,23 @@ export default function TradeSummary(props: Props) {
borrowAmount, borrowAmount,
estimatedFee, estimatedFee,
showProgressIndicator, showProgressIndicator,
route,
} = props } = props
const { data: routes, isLoading: isRouteLoading } = useSwapRoute(sellAsset.denom, buyAsset.denom)
const parsedRoutes = useMemo(() => { const parsedRoutes = useMemo(() => {
if (!routes.length) return '-' if (!route.length) return '-'
const routeSymbols = routes.map((r) => getAssetByDenom(r.token_out_denom)?.symbol) const routeSymbols = route.map((r) => getAssetByDenom(r.token_out_denom)?.symbol)
routeSymbols.unshift(sellAsset.symbol) routeSymbols.unshift(sellAsset.symbol)
return routeSymbols.join(' -> ') return routeSymbols.join(' -> ')
}, [routes, sellAsset.symbol]) }, [route, sellAsset.symbol])
const buttonText = useMemo( const buttonText = useMemo(
() => (routes.length ? `Buy ${buyAsset.symbol}` : 'No route found'), () => (route.length ? `Buy ${buyAsset.symbol}` : 'No route found'),
[buyAsset.symbol, routes], [buyAsset.symbol, route],
) )
const infoLineClasses = 'flex flex-row justify-between flex-1 mb-1 text-xs text-white'
return ( return (
<div <div
className={classNames( className={classNames(
@ -94,8 +93,8 @@ export default function TradeSummary(props: Props) {
</div> </div>
</div> </div>
<ActionButton <ActionButton
disabled={routes.length === 0 || buyButtonDisabled} disabled={buyButtonDisabled}
showProgressIndicator={showProgressIndicator || isRouteLoading} showProgressIndicator={showProgressIndicator}
text={buttonText} text={buttonText}
onClick={buyAction} onClick={buyAction}
size='md' size='md'

View File

@ -20,6 +20,7 @@ import useHealthComputer from 'hooks/useHealthComputer'
import useLocalStorage from 'hooks/useLocalStorage' import useLocalStorage from 'hooks/useLocalStorage'
import useMarketAssets from 'hooks/useMarketAssets' import useMarketAssets from 'hooks/useMarketAssets'
import useMarketBorrowings from 'hooks/useMarketBorrowings' import useMarketBorrowings from 'hooks/useMarketBorrowings'
import useSwapRoute from 'hooks/useSwapRoute'
import useToggle from 'hooks/useToggle' import useToggle from 'hooks/useToggle'
import { useUpdatedAccount } from 'hooks/useUpdatedAccount' import { useUpdatedAccount } from 'hooks/useUpdatedAccount'
import useStore from 'store' import useStore from 'store'
@ -43,6 +44,7 @@ export default function SwapForm(props: Props) {
const { computeMaxSwapAmount } = useHealthComputer(account) const { computeMaxSwapAmount } = useHealthComputer(account)
const { data: borrowAssets } = useMarketBorrowings() const { data: borrowAssets } = useMarketBorrowings()
const { data: marketAssets } = useMarketAssets() const { data: marketAssets } = useMarketAssets()
const { data: route, isLoading: isRouteLoading } = useSwapRoute(sellAsset.denom, buyAsset.denom)
const isBorrowEnabled = !!marketAssets.find(byDenom(sellAsset.denom))?.borrowEnabled const isBorrowEnabled = !!marketAssets.find(byDenom(sellAsset.denom))?.borrowEnabled
const [isMarginChecked, setMarginChecked] = useToggle(isBorrowEnabled ? useMargin : false) const [isMarginChecked, setMarginChecked] = useToggle(isBorrowEnabled ? useMargin : false)
const [buyAssetAmount, setBuyAssetAmount] = useState(BN_ZERO) const [buyAssetAmount, setBuyAssetAmount] = useState(BN_ZERO)
@ -265,6 +267,14 @@ export default function SwapForm(props: Props) {
[borrowAsset?.liquidity?.amount], [borrowAsset?.liquidity?.amount],
) )
const isSwapDisabled = useMemo(
() =>
sellAssetAmount.isZero() ||
depositCapReachedCoins.length > 0 ||
borrowAmount.isGreaterThanOrEqualTo(availableLiquidity),
[sellAssetAmount, depositCapReachedCoins, borrowAmount, availableLiquidity],
)
return ( return (
<> <>
<Divider /> <Divider />
@ -323,15 +333,12 @@ export default function SwapForm(props: Props) {
sellAsset={sellAsset} sellAsset={sellAsset}
borrowRate={borrowAsset?.borrowRate} borrowRate={borrowAsset?.borrowRate}
buyAction={handleBuyClick} buyAction={handleBuyClick}
buyButtonDisabled={ buyButtonDisabled={isSwapDisabled || route.length === 0}
sellAssetAmount.isZero() || showProgressIndicator={isConfirming || isRouteLoading}
depositCapReachedCoins.length > 0 ||
borrowAmount.isGreaterThanOrEqualTo(availableLiquidity)
}
showProgressIndicator={isConfirming}
isMargin={isMarginChecked} isMargin={isMarginChecked}
borrowAmount={borrowAmount} borrowAmount={borrowAmount}
estimatedFee={estimatedFee} estimatedFee={estimatedFee}
route={route}
/> />
</div> </div>
</> </>

View File

@ -21,6 +21,7 @@ export const ASSETS: Asset[] = [
isDisplayCurrency: true, isDisplayCurrency: true,
isAutoLendEnabled: true, isAutoLendEnabled: true,
pythPriceFeedId: '5867f5683c757393a0670ef0f701490950fe93fdb006d181c8265a831ac0c5c6', pythPriceFeedId: '5867f5683c757393a0670ef0f701490950fe93fdb006d181c8265a831ac0c5c6',
pythHistoryFeedId: 'Crypto.OSMO/USD',
}, },
{ {
symbol: 'ATOM', symbol: 'ATOM',
@ -42,6 +43,7 @@ export const ASSETS: Asset[] = [
isBorrowEnabled: true, isBorrowEnabled: true,
pythPriceFeedId: 'b00b60f88b03a6a625a8d1c048c3f66653edf217439983d037e7222c4e612819', pythPriceFeedId: 'b00b60f88b03a6a625a8d1c048c3f66653edf217439983d037e7222c4e612819',
poolId: 1, poolId: 1,
pythHistoryFeedId: 'Crypto.ATOM/USD',
}, },
{ {
symbol: 'stATOM', symbol: 'stATOM',
@ -76,6 +78,7 @@ export const ASSETS: Asset[] = [
isAutoLendEnabled: true, isAutoLendEnabled: true,
isBorrowEnabled: true, isBorrowEnabled: true,
pythPriceFeedId: 'e62df6c8b4a85fe1a67db44dc12de5db330f7ac66b72dc658afedf0f4a415b43', pythPriceFeedId: 'e62df6c8b4a85fe1a67db44dc12de5db330f7ac66b72dc658afedf0f4a415b43',
pythHistoryFeedId: 'Crypto.BTC/USD',
poolId: 712, poolId: 712,
}, },
{ {
@ -94,6 +97,7 @@ export const ASSETS: Asset[] = [
isAutoLendEnabled: true, isAutoLendEnabled: true,
isBorrowEnabled: true, isBorrowEnabled: true,
pythPriceFeedId: 'ff61491a931112ddf1bd8147cd1b641375f79f5825126d665480874634fd0ace', pythPriceFeedId: 'ff61491a931112ddf1bd8147cd1b641375f79f5825126d665480874634fd0ace',
pythHistoryFeedId: 'Crypto.ETH/USD',
poolId: 704, poolId: 704,
}, },
{ {
@ -134,6 +138,7 @@ export const ASSETS: Asset[] = [
isBorrowEnabled: true, isBorrowEnabled: true,
isAutoLendEnabled: true, isAutoLendEnabled: true,
pythPriceFeedId: 'eaa020c61cc479712813461ce153894a96a6c00b21ed0cfc2798d1f9a9e9c94a', pythPriceFeedId: 'eaa020c61cc479712813461ce153894a96a6c00b21ed0cfc2798d1f9a9e9c94a',
pythHistoryFeedId: 'Crypto.USDC/USD',
poolId: 678, poolId: 678,
}, },
{ {
@ -156,6 +161,7 @@ export const ASSETS: Asset[] = [
isBorrowEnabled: true, isBorrowEnabled: true,
isAutoLendEnabled: true, isAutoLendEnabled: true,
pythPriceFeedId: 'eaa020c61cc479712813461ce153894a96a6c00b21ed0cfc2798d1f9a9e9c94a', pythPriceFeedId: 'eaa020c61cc479712813461ce153894a96a6c00b21ed0cfc2798d1f9a9e9c94a',
pythHistoryFeedId: 'Crypto.USDC/USD',
poolId: ENV.NETWORK === NETWORK.DEVNET ? 678 : 1221, poolId: ENV.NETWORK === NETWORK.DEVNET ? 678 : 1221,
}, },
{ {
@ -173,6 +179,7 @@ export const ASSETS: Asset[] = [
isDisplayCurrency: ENV.NETWORK !== NETWORK.TESTNET, isDisplayCurrency: ENV.NETWORK !== NETWORK.TESTNET,
isAutoLendEnabled: false, isAutoLendEnabled: false,
pythPriceFeedId: '60144b1d5c9e9851732ad1d9760e3485ef80be39b984f6bf60f82b28a2b7f126', pythPriceFeedId: '60144b1d5c9e9851732ad1d9760e3485ef80be39b984f6bf60f82b28a2b7f126',
pythHistoryFeedId: 'Crypto.AXL/USD',
poolId: 812, poolId: 812,
}, },
{ {

View File

@ -8,14 +8,14 @@ interface EnvironmentVariables {
ADDRESS_RED_BANK: string ADDRESS_RED_BANK: string
ADDRESS_SWAPPER: string ADDRESS_SWAPPER: string
ADDRESS_ZAPPER: string ADDRESS_ZAPPER: string
CANDLES_ENDPOINT: string CANDLES_ENDPOINT_THE_GRAPH: string
CANDLES_ENDPOINT_PYTH: string
CHAIN_ID: string CHAIN_ID: string
NETWORK: string NETWORK: string
URL_GQL: string URL_GQL: string
URL_REST: string URL_REST: string
URL_RPC: string URL_RPC: string
URL_VAULT_APR: string URL_VAULT_APR: string
WALLETS: string[]
PYTH_ENDPOINT: string PYTH_ENDPOINT: string
MAINNET_REST_API: string MAINNET_REST_API: string
WALLET_CONNECT_ID: string WALLET_CONNECT_ID: string
@ -31,14 +31,14 @@ export const ENV: EnvironmentVariables = {
ADDRESS_RED_BANK: process.env.NEXT_PUBLIC_RED_BANK || '', ADDRESS_RED_BANK: process.env.NEXT_PUBLIC_RED_BANK || '',
ADDRESS_SWAPPER: process.env.NEXT_PUBLIC_SWAPPER || '', ADDRESS_SWAPPER: process.env.NEXT_PUBLIC_SWAPPER || '',
ADDRESS_ZAPPER: process.env.NEXT_PUBLIC_ZAPPER || '', ADDRESS_ZAPPER: process.env.NEXT_PUBLIC_ZAPPER || '',
CANDLES_ENDPOINT: process.env.NEXT_PUBLIC_CANDLES_ENDPOINT || '', CANDLES_ENDPOINT_THE_GRAPH: process.env.NEXT_PUBLIC_CANDLES_ENDPOINT_THE_GRAPH || '',
CANDLES_ENDPOINT_PYTH: process.env.NEXT_PUBLIC_CANDLES_ENDPOINT_PYTH || '',
CHAIN_ID: process.env.NEXT_PUBLIC_CHAIN_ID || '', CHAIN_ID: process.env.NEXT_PUBLIC_CHAIN_ID || '',
NETWORK: process.env.NEXT_PUBLIC_NETWORK || '', NETWORK: process.env.NEXT_PUBLIC_NETWORK || '',
URL_GQL: process.env.NEXT_PUBLIC_GQL || '', URL_GQL: process.env.NEXT_PUBLIC_GQL || '',
URL_REST: process.env.NEXT_PUBLIC_REST || '', URL_REST: process.env.NEXT_PUBLIC_REST || '',
URL_RPC: process.env.NEXT_PUBLIC_RPC || '', URL_RPC: process.env.NEXT_PUBLIC_RPC || '',
URL_VAULT_APR: process.env.NEXT_PUBLIC_VAULT_APR || '', URL_VAULT_APR: process.env.NEXT_PUBLIC_VAULT_APR || '',
WALLETS: process.env.NEXT_PUBLIC_WALLETS?.split(',') || [],
PYTH_ENDPOINT: process.env.NEXT_PUBLIC_PYTH_ENDPOINT || '', PYTH_ENDPOINT: process.env.NEXT_PUBLIC_PYTH_ENDPOINT || '',
MAINNET_REST_API: process.env.NEXT_PUBLIC_MAINNET_REST || '', MAINNET_REST_API: process.env.NEXT_PUBLIC_MAINNET_REST || '',
WALLET_CONNECT_ID: process.env.NEXT_PUBLIC_WALLET_CONNECT_ID || '', WALLET_CONNECT_ID: process.env.NEXT_PUBLIC_WALLET_CONNECT_ID || '',

View File

@ -6,3 +6,5 @@ export const BN_ONE = BN(1)
export const MARGIN_TRADE_BUFFER = 0.9 export const MARGIN_TRADE_BUFFER = 0.9
export const MIN_AMOUNT = 0.000001 export const MIN_AMOUNT = 0.000001
export const MAX_AMOUNT_DECIMALS = 6 export const MAX_AMOUNT_DECIMALS = 6
export const MILLISECONDS_PER_MINUTE = 60000

View File

@ -52,6 +52,7 @@ interface Asset {
isAutoLendEnabled?: boolean isAutoLendEnabled?: boolean
isBorrowEnabled?: boolean isBorrowEnabled?: boolean
pythPriceFeedId?: string pythPriceFeedId?: string
pythHistoryFeedId?: string
forceFetchPrice?: boolean forceFetchPrice?: boolean
testnetDenom?: string testnetDenom?: string
isStaking?: boolean isStaking?: boolean

View File

@ -0,0 +1,4 @@
interface Route {
pool_id: string
token_out_denom: string
}

4
src/utils/math.ts Normal file
View File

@ -0,0 +1,4 @@
export const devideByPotentiallyZero = (numerator: number, denominator: number): number => {
if (denominator === 0) return 0
return numerator / denominator
}