* fix: fix the trade interface * fix: z-index adjustment * fix: z-index on trade * fix: header z-index
492 lines
15 KiB
TypeScript
492 lines
15 KiB
TypeScript
import { defaultSymbolInfo } from 'components/Trade/TradeChart/constants'
|
|
import { ASSETS } from 'constants/assets'
|
|
import { ENV } from 'constants/env'
|
|
import { MILLISECONDS_PER_MINUTE } from 'constants/math'
|
|
import { byDenom } from 'utils/array'
|
|
import { getAssetByDenom, getEnabledMarketAssets } from 'utils/assets'
|
|
import {
|
|
Bar,
|
|
ErrorCallback,
|
|
HistoryCallback,
|
|
IDatafeedChartApi,
|
|
LibrarySymbolInfo,
|
|
OnReadyCallback,
|
|
PeriodParams,
|
|
ResolutionString,
|
|
ResolveCallback,
|
|
} from 'utils/charting_library'
|
|
import { BN } from 'utils/helpers'
|
|
import { devideByPotentiallyZero } from 'utils/math'
|
|
|
|
interface PythBarQueryData {
|
|
s: string
|
|
t: number[]
|
|
o: number[]
|
|
h: number[]
|
|
l: number[]
|
|
c: number[]
|
|
v: number[]
|
|
}
|
|
|
|
interface TheGraphBarQueryData {
|
|
close: string
|
|
high: string
|
|
low: string
|
|
open: string
|
|
timestamp: string
|
|
volume: string
|
|
}
|
|
|
|
export const PAIR_SEPARATOR = '<>'
|
|
|
|
export class DataFeed implements IDatafeedChartApi {
|
|
candlesEndpoint = ENV.CANDLES_ENDPOINT_PYTH
|
|
candlesEndpointTheGraph = ENV.CANDLES_ENDPOINT_THE_GRAPH
|
|
debug = false
|
|
enabledMarketAssetDenoms: string[] = []
|
|
batchSize = 1000
|
|
baseDecimals: number = 6
|
|
baseDenom: string = 'uosmo'
|
|
intervalsTheGraph: { [key: string]: string } = {
|
|
'15': '15m',
|
|
'30': '30m',
|
|
'60': '1h',
|
|
'240': '4h',
|
|
'1D': '1d',
|
|
}
|
|
millisecondsPerInterval: { [key: string]: number } = {
|
|
'15': MILLISECONDS_PER_MINUTE * 15,
|
|
'30': MILLISECONDS_PER_MINUTE * 30,
|
|
'60': MILLISECONDS_PER_MINUTE * 60,
|
|
'240': MILLISECONDS_PER_MINUTE * 240,
|
|
'1D': MILLISECONDS_PER_MINUTE * 1440,
|
|
}
|
|
pairs: { baseAsset: string; quoteAsset: string }[] = []
|
|
pairsWithData: string[] = []
|
|
supportedPools: string[] = []
|
|
supportedResolutions = ['15', '30', '60', '240', 'D'] as ResolutionString[]
|
|
|
|
constructor(debug = false, baseDecimals: number, baseDenom: string) {
|
|
if (debug) console.log('Start charting library datafeed')
|
|
this.debug = debug
|
|
this.baseDecimals = baseDecimals
|
|
this.baseDenom = baseDenom
|
|
const enabledMarketAssets = getEnabledMarketAssets()
|
|
this.enabledMarketAssetDenoms = enabledMarketAssets.map((asset) => asset.denom)
|
|
this.supportedPools = enabledMarketAssets
|
|
.map((asset) => asset.poolId?.toString())
|
|
.filter((poolId) => typeof poolId === 'string') as string[]
|
|
}
|
|
|
|
getDescription(pairName: string) {
|
|
const denom1 = pairName.split(PAIR_SEPARATOR)[0]
|
|
const denom2 = pairName.split(PAIR_SEPARATOR)[1]
|
|
const asset1 = ASSETS.find(byDenom(denom1))
|
|
const asset2 = ASSETS.find(byDenom(denom2))
|
|
return `${asset2?.symbol}/${asset1?.symbol}`
|
|
}
|
|
|
|
async getPairsWithData() {
|
|
const query = `
|
|
{
|
|
pairs(first: ${this.batchSize},
|
|
orderBy: symbol,
|
|
orderDirection: asc,
|
|
where: {
|
|
baseAsset_in: ${JSON.stringify(this.enabledMarketAssetDenoms)},
|
|
quoteAsset_in: ${JSON.stringify(this.enabledMarketAssetDenoms)},
|
|
poolId_in: ${JSON.stringify(this.supportedPools)}
|
|
}
|
|
) {
|
|
baseAsset
|
|
quoteAsset
|
|
}
|
|
}`
|
|
|
|
return fetch(this.candlesEndpointTheGraph, {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({ query }),
|
|
})
|
|
.then((res) => res.json())
|
|
.then((json) => {
|
|
this.pairs = json.data.pairs
|
|
this.pairsWithData = json.data.pairs.map(
|
|
(pair: { baseAsset: string; quoteAsset: string }) => {
|
|
return `${pair.baseAsset}${PAIR_SEPARATOR}${pair.quoteAsset}`
|
|
},
|
|
)
|
|
})
|
|
.catch((err) => {
|
|
if (this.debug) console.error(err)
|
|
throw err
|
|
})
|
|
}
|
|
|
|
onReady(callback: OnReadyCallback) {
|
|
const configurationData = {
|
|
supported_resolutions: this.supportedResolutions,
|
|
}
|
|
|
|
setTimeout(async () => {
|
|
await this.getPairsWithData()
|
|
callback(configurationData)
|
|
})
|
|
}
|
|
|
|
resolveSymbol(pairName: string, onResolve: ResolveCallback, onError: ErrorCallback) {
|
|
pairName = this.getPairName(pairName)
|
|
setTimeout(() => {
|
|
const info: LibrarySymbolInfo = {
|
|
...defaultSymbolInfo,
|
|
name: this.getDescription(pairName),
|
|
full_name: this.getDescription(pairName),
|
|
description: this.getDescription(pairName),
|
|
ticker: this.getDescription(pairName),
|
|
exchange: this.getExchangeName(pairName),
|
|
listed_exchange: this.getExchangeName(pairName),
|
|
supported_resolutions: this.supportedResolutions,
|
|
base_name: [this.getDescription(pairName)],
|
|
pricescale: this.getPriceScale(pairName),
|
|
} as LibrarySymbolInfo
|
|
onResolve(info)
|
|
})
|
|
}
|
|
|
|
async getBars(
|
|
symbolInfo: LibrarySymbolInfo,
|
|
resolution: ResolutionString,
|
|
periodParams: PeriodParams,
|
|
onResult: HistoryCallback,
|
|
): Promise<void> {
|
|
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 pair2: string = ''
|
|
let pair3: string = ''
|
|
let theGraphBars = [] as Bar[]
|
|
|
|
if (!this.pairsWithData.includes(pair1)) {
|
|
if (this.debug) console.log('Pair does not have data, need to combine with other pairs')
|
|
|
|
const [buyAssetDenom, sellAssetDenom] = pair1.split(PAIR_SEPARATOR)
|
|
|
|
const pair1Pools = this.pairs.filter((pair) => pair.baseAsset === buyAssetDenom)
|
|
const pair2Pools = this.pairs.filter((pair) => pair.quoteAsset === sellAssetDenom)
|
|
|
|
const matchedPools = pair1Pools.filter((pool) => {
|
|
const asset = pool.quoteAsset
|
|
return !!pair2Pools.find((pool) => pool.baseAsset === asset)
|
|
})
|
|
|
|
if (matchedPools.length) {
|
|
pair1 = `${buyAssetDenom}${PAIR_SEPARATOR}${matchedPools[0].quoteAsset}`
|
|
pair2 = `${matchedPools[0].quoteAsset}${PAIR_SEPARATOR}${sellAssetDenom}`
|
|
} else {
|
|
const middlePair = this.pairs.filter(
|
|
(pair) =>
|
|
pair1Pools.map((pairs) => pairs.quoteAsset).includes(pair.baseAsset) &&
|
|
pair2Pools.map((pairs) => pairs.baseAsset).includes(pair.quoteAsset),
|
|
)
|
|
|
|
pair1 = `${buyAssetDenom}${PAIR_SEPARATOR}${middlePair[0].baseAsset}`
|
|
pair2 = `${middlePair[0].baseAsset}${PAIR_SEPARATOR}${middlePair[0].quoteAsset}`
|
|
pair3 = `${middlePair[0].quoteAsset}${PAIR_SEPARATOR}${sellAssetDenom}`
|
|
}
|
|
}
|
|
|
|
const pair1Bars = this.queryBarDataTheGraph(
|
|
pair1.split(PAIR_SEPARATOR)[0],
|
|
pair1.split(PAIR_SEPARATOR)[1],
|
|
resolution,
|
|
to,
|
|
)
|
|
|
|
let pair2Bars: Promise<Bar[]> | null = null
|
|
|
|
if (pair2) {
|
|
pair2Bars = this.queryBarDataTheGraph(
|
|
pair2.split(PAIR_SEPARATOR)[0],
|
|
pair2.split(PAIR_SEPARATOR)[1],
|
|
resolution,
|
|
to,
|
|
)
|
|
}
|
|
|
|
let pair3Bars: Promise<Bar[]> | null = null
|
|
|
|
if (pair3) {
|
|
pair3Bars = this.queryBarDataTheGraph(
|
|
pair3.split(PAIR_SEPARATOR)[0],
|
|
pair3.split(PAIR_SEPARATOR)[1],
|
|
resolution,
|
|
to,
|
|
)
|
|
}
|
|
|
|
await Promise.all([pair1Bars, pair2Bars, pair3Bars]).then(
|
|
([pair1Bars, pair2Bars, pair3Bars]) => {
|
|
let bars = pair1Bars
|
|
|
|
if (!bars.length) {
|
|
return
|
|
}
|
|
|
|
if (pair2Bars) {
|
|
bars = this.combineBars(pair1Bars, pair2Bars)
|
|
}
|
|
if (pair3Bars) {
|
|
bars = this.combineBars(bars, pair3Bars)
|
|
}
|
|
|
|
const filler = Array.from({ length: this.batchSize - bars.length }).map((_, index) => ({
|
|
time:
|
|
(bars[0]?.time || new Date().getTime()) -
|
|
(index * this.millisecondsPerInterval[resolution]) / 1000,
|
|
close: 0,
|
|
open: 0,
|
|
high: 0,
|
|
low: 0,
|
|
volume: 0,
|
|
}))
|
|
theGraphBars = [...filler, ...bars]
|
|
},
|
|
)
|
|
|
|
return theGraphBars
|
|
}
|
|
|
|
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 = `
|
|
{
|
|
candles(
|
|
first: ${this.batchSize},
|
|
orderBy: "timestamp",
|
|
orderDirection: "desc",
|
|
where: {
|
|
interval: "${interval}",
|
|
quote: "${quote}",
|
|
base: "${base}"
|
|
poolId_in: ${JSON.stringify(this.supportedPools)}
|
|
}
|
|
) {
|
|
timestamp
|
|
open
|
|
high
|
|
low
|
|
close
|
|
volume
|
|
}
|
|
}
|
|
`
|
|
|
|
return fetch(this.candlesEndpointTheGraph, {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({ query }),
|
|
})
|
|
.then((res) => res.json())
|
|
.then((json: { data?: { candles: TheGraphBarQueryData[] } }) => {
|
|
return this.resolveBarDataTheGraph(
|
|
json.data?.candles.reverse() || [],
|
|
base,
|
|
quote,
|
|
resolution,
|
|
to,
|
|
)
|
|
})
|
|
.catch((err) => {
|
|
if (this.debug) console.error(err)
|
|
throw err
|
|
})
|
|
}
|
|
|
|
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 fromDecimals = getAssetByDenom(fromDenom)?.decimals || 6
|
|
const additionalDecimals = toDecimals - fromDecimals
|
|
|
|
barData = bars.map((bar) => ({
|
|
time: BN(bar.timestamp).multipliedBy(1000).toNumber(),
|
|
close: BN(bar.close).shiftedBy(additionalDecimals).toNumber(),
|
|
open: BN(bar.open).shiftedBy(additionalDecimals).toNumber(),
|
|
high: BN(bar.high).shiftedBy(additionalDecimals).toNumber(),
|
|
low: BN(bar.low).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[] {
|
|
const bars: Bar[] = []
|
|
|
|
pair1Bars.forEach((pair1Bar, index) => {
|
|
const pair2Bar = pair2Bars[index]
|
|
|
|
bars.push({
|
|
time: pair1Bar.time,
|
|
open: devideByPotentiallyZero(pair1Bar.open, pair2Bar.open),
|
|
close: devideByPotentiallyZero(pair1Bar.close, pair2Bar.close),
|
|
high: devideByPotentiallyZero(pair1Bar.high, pair2Bar.high),
|
|
low: devideByPotentiallyZero(pair1Bar.low, pair2Bar.low),
|
|
})
|
|
})
|
|
return bars
|
|
}
|
|
|
|
getPairName(name: string) {
|
|
if (name.includes(PAIR_SEPARATOR)) return name
|
|
|
|
const [symbol1, symbol2] = name.split('/')
|
|
|
|
const asset1 = ASSETS.find((asset) => asset.symbol === symbol1)
|
|
const asset2 = ASSETS.find((asset) => asset.symbol === symbol2)
|
|
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 {
|
|
// Don't allow to search for symbols
|
|
}
|
|
|
|
subscribeBars(): void {
|
|
// TheGraph doesn't support websockets yet
|
|
}
|
|
|
|
unsubscribeBars(listenerGuid: string): void {
|
|
// TheGraph doesn't support websockets yet
|
|
}
|
|
}
|