Websocket implementation (#726)
* feat: first steps * feat: added websocket support and set the TradingChart to USD * fix: oracle staleness button
This commit is contained in:
parent
ebe05b12fd
commit
b57ae05db1
@ -40,6 +40,16 @@
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.layout__area--center {
|
||||
background: var(--tv-background) !important;
|
||||
}
|
||||
|
||||
.chart-widget.chart-widget--themed-dark.chart-widget__top--themed-dark.chart-widget__bottom--themed-dark
|
||||
> table
|
||||
canvas {
|
||||
background: transparent !important;
|
||||
}
|
||||
|
||||
/* Floating menu */
|
||||
.floating-toolbar-react-widgets__button:hover,
|
||||
[class^='button-']:hover:before {
|
||||
|
@ -27,7 +27,7 @@ export default async function getPrices(chainConfig: ChainConfig): Promise<BNCoi
|
||||
const oraclePrices = await getOraclePrices(chainConfig, assetsWithOraclePrices)
|
||||
const poolPrices = await requestPoolPrices(chainConfig, assetsWithPoolIds, pythAndOraclePrices)
|
||||
|
||||
useStore.setState({ isOracleStale: false })
|
||||
if (oraclePrices) useStore.setState({ isOracleStale: false })
|
||||
|
||||
return [...pythAndOraclePrices, ...oraclePrices, ...poolPrices, usdPrice]
|
||||
} catch (ex) {
|
||||
|
@ -1,5 +1,6 @@
|
||||
import classNames from 'classnames'
|
||||
import { isDesktop } from 'react-device-detect'
|
||||
import { useMemo } from 'react'
|
||||
|
||||
import AccountMenu from 'components/Account/AccountMenu'
|
||||
import EscButton from 'components/Button/EscButton'
|
||||
@ -54,6 +55,8 @@ export default function DesktopHeader() {
|
||||
useStore.setState({ focusComponent: null })
|
||||
}
|
||||
|
||||
const showStaleOracle = useMemo(() => isOracleStale && address, [isOracleStale, address])
|
||||
|
||||
if (!isDesktop) return null
|
||||
|
||||
return (
|
||||
@ -84,7 +87,7 @@ export default function DesktopHeader() {
|
||||
</div>
|
||||
) : (
|
||||
<div className='flex gap-4'>
|
||||
{address && isOracleStale && <OracleResyncButton />}
|
||||
{showStaleOracle && <OracleResyncButton />}
|
||||
{accountId && <RewardsCenter />}
|
||||
{address && !isHLS && <AccountMenu />}
|
||||
<Wallet />
|
||||
|
@ -1,508 +1,142 @@
|
||||
import { defaultSymbolInfo } from 'components/Trade/TradeChart/constants'
|
||||
import { MILLISECONDS_PER_MINUTE } from 'constants/math'
|
||||
import { subscribeOnStream, unsubscribeFromStream } from 'components/Trade/TradeChart/streaming'
|
||||
import { pythEndpoints } from 'constants/pyth'
|
||||
import { byDenom } from 'utils/array'
|
||||
import {
|
||||
Bar,
|
||||
ErrorCallback,
|
||||
HistoryCallback,
|
||||
IDatafeedChartApi,
|
||||
LibrarySymbolInfo,
|
||||
OnReadyCallback,
|
||||
PeriodParams,
|
||||
ResolutionString,
|
||||
ResolveCallback,
|
||||
SearchSymbolsCallback,
|
||||
SubscribeBarsCallback,
|
||||
SymbolResolveExtension,
|
||||
} 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[]
|
||||
}
|
||||
const lastBarsCache = new Map()
|
||||
|
||||
interface TheGraphBarQueryData {
|
||||
close: string
|
||||
high: string
|
||||
low: string
|
||||
open: string
|
||||
timestamp: string
|
||||
volume: string
|
||||
}
|
||||
|
||||
export const PAIR_SEPARATOR = '<>'
|
||||
|
||||
export class DataFeed implements IDatafeedChartApi {
|
||||
candlesEndpoint: string
|
||||
candlesEndpointTheGraph: string
|
||||
assets: Asset[]
|
||||
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 } = {
|
||||
'1': MILLISECONDS_PER_MINUTE * 1,
|
||||
'5': MILLISECONDS_PER_MINUTE * 5,
|
||||
'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 = ['1', '5', '15', '30', '60', '240', 'D'] as ResolutionString[]
|
||||
|
||||
constructor(
|
||||
debug = false,
|
||||
assets: Asset[],
|
||||
baseDecimals: number,
|
||||
baseDenom: string,
|
||||
chainConfig: ChainConfig,
|
||||
) {
|
||||
if (debug) console.log('Start charting library datafeed')
|
||||
this.candlesEndpoint = pythEndpoints.candles
|
||||
this.candlesEndpointTheGraph = chainConfig.endpoints.graphCandles ?? ''
|
||||
this.assets = assets
|
||||
this.debug = debug
|
||||
this.baseDecimals = baseDecimals
|
||||
this.baseDenom = baseDenom
|
||||
const enabledMarketAssets = assets.filter((asset) => asset.isEnabled && asset.isMarket)
|
||||
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, inverted: boolean) {
|
||||
const [denom1, denom2] = pairName.split(PAIR_SEPARATOR)
|
||||
const asset1 = this.assets.find(byDenom(denom1))
|
||||
const asset2 = this.assets.find(byDenom(denom2))
|
||||
return inverted ? `${asset2?.symbol}/${asset1?.symbol}` : `${asset1?.symbol}/${asset2?.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 }),
|
||||
export const datafeed = {
|
||||
onReady: (callback: OnReadyCallback) => {
|
||||
callback({
|
||||
supported_resolutions: [
|
||||
'1',
|
||||
'2',
|
||||
'5',
|
||||
'15',
|
||||
'30',
|
||||
'60',
|
||||
'120',
|
||||
'240',
|
||||
'360',
|
||||
'720',
|
||||
'D',
|
||||
'1D',
|
||||
'W',
|
||||
'1W',
|
||||
'M',
|
||||
'1M',
|
||||
] as ResolutionString[],
|
||||
supports_marks: true,
|
||||
supports_timescale_marks: false,
|
||||
})
|
||||
.then((res) => res.json())
|
||||
.then((json) => {
|
||||
this.pairs = json.data.pairs
|
||||
|
||||
this.pairsWithData = json.data.pairs.map(
|
||||
(pair: { baseAsset: string; quoteAsset: string }) => {
|
||||
return `${pair.quoteAsset}${PAIR_SEPARATOR}${pair.baseAsset}`
|
||||
},
|
||||
)
|
||||
})
|
||||
.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, false),
|
||||
full_name: this.getDescription(pairName, true),
|
||||
description: this.getDescription(pairName, true),
|
||||
ticker: this.getDescription(pairName, false),
|
||||
exchange: this.getExchangeName(pairName),
|
||||
listed_exchange: this.getExchangeName(pairName),
|
||||
supported_resolutions: this.supportedResolutions,
|
||||
base_name: [this.getDescription(pairName, false)],
|
||||
pricescale: this.getPriceScale(pairName),
|
||||
} as LibrarySymbolInfo
|
||||
onResolve(info)
|
||||
})
|
||||
}
|
||||
|
||||
async getBars(
|
||||
},
|
||||
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)
|
||||
onHistoryCallback: HistoryCallback,
|
||||
onErrorCallback: ErrorCallback,
|
||||
) => {
|
||||
const { from, to, firstDataRequest } = periodParams
|
||||
fetch(
|
||||
`${pythEndpoints.candles}/history?symbol=${symbolInfo.ticker}&from=${from}&to=${to}&resolution=${resolution}`,
|
||||
).then((response) => {
|
||||
response
|
||||
.json()
|
||||
.then((data) => {
|
||||
if (data.errmsg) {
|
||||
onHistoryCallback([], { noData: true })
|
||||
return
|
||||
}
|
||||
if (data.t.length === 0) {
|
||||
onHistoryCallback([], { noData: true })
|
||||
return
|
||||
}
|
||||
const bars = []
|
||||
for (let i = 0; i < data.t.length; ++i) {
|
||||
bars.push({
|
||||
time: data.t[i] * 1000,
|
||||
low: data.l[i],
|
||||
high: data.h[i],
|
||||
open: data.o[i],
|
||||
close: data.c[i],
|
||||
})
|
||||
}
|
||||
if (firstDataRequest) {
|
||||
lastBarsCache.set(symbolInfo.ticker, {
|
||||
...bars[bars.length - 1],
|
||||
})
|
||||
}
|
||||
onHistoryCallback(bars, { noData: false })
|
||||
})
|
||||
} else {
|
||||
//await this.getBarsFromTheGraph(symbolInfo, resolution, to).then((bars) => onResult(bars))
|
||||
onResult([], { noData: true })
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
return onResult([], { noData: true })
|
||||
}
|
||||
}
|
||||
|
||||
async getBarsFromTheGraph(
|
||||
.catch((error) => {
|
||||
onErrorCallback(error)
|
||||
})
|
||||
})
|
||||
},
|
||||
subscribeBars(
|
||||
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],
|
||||
onRealtimeCallback: SubscribeBarsCallback,
|
||||
subscriberUID: string,
|
||||
onResetCacheNeededCallback: () => void,
|
||||
): void {
|
||||
subscribeOnStream(
|
||||
symbolInfo,
|
||||
resolution,
|
||||
to,
|
||||
onRealtimeCallback,
|
||||
subscriberUID,
|
||||
onResetCacheNeededCallback,
|
||||
lastBarsCache.get(symbolInfo.ticker),
|
||||
)
|
||||
|
||||
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
|
||||
},
|
||||
unsubscribeBars(subscriberUID: string) {
|
||||
unsubscribeFromStream(subscriberUID)
|
||||
},
|
||||
searchSymbols: (
|
||||
userInput: string,
|
||||
exchange: string,
|
||||
symbolType: string,
|
||||
onResult: SearchSymbolsCallback,
|
||||
) => {
|
||||
return
|
||||
},
|
||||
resolveSymbol: (
|
||||
symbolName: string,
|
||||
onResolve: ResolveCallback,
|
||||
onError: ErrorCallback,
|
||||
extension?: SymbolResolveExtension,
|
||||
) => {
|
||||
try {
|
||||
fetch(`${pythEndpoints.candles}/symbols?symbol=${symbolName}`).then((response) => {
|
||||
response
|
||||
.json()
|
||||
.then((symbolInfo) => {
|
||||
if (symbolInfo.errmsg) {
|
||||
symbolInfo.description = symbolName
|
||||
} else {
|
||||
symbolInfo.description = symbolInfo.ticker.split('Crypto.')[1]
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
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,
|
||||
)
|
||||
onResolve(symbolInfo)
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error(error)
|
||||
return
|
||||
})
|
||||
})
|
||||
.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],
|
||||
}))
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
return
|
||||
}
|
||||
|
||||
return this.fillBarData(barData, resolution, to)
|
||||
}
|
||||
|
||||
resolveBarDataTheGraph(
|
||||
bars: TheGraphBarQueryData[],
|
||||
toDenom: string,
|
||||
fromDenom: string,
|
||||
resolution: ResolutionString,
|
||||
to: number,
|
||||
) {
|
||||
let barData = [] as Bar[]
|
||||
const toDecimals = this.assets.find(byDenom(toDenom))?.decimals || 6
|
||||
const fromDecimals = this.assets.find(byDenom(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 = this.assets.find((asset) => asset.symbol === symbol1)
|
||||
const asset2 = this.assets.find((asset) => asset.symbol === symbol2)
|
||||
return `${asset1?.denom}${PAIR_SEPARATOR}${asset2?.denom}`
|
||||
}
|
||||
|
||||
getPriceScale(name: string) {
|
||||
const denoms = name.split(PAIR_SEPARATOR)
|
||||
const asset2 = this.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 = this.assets.find(byDenom(denoms[0]))?.pythHistoryFeedId
|
||||
const pythFeedId2 = this.assets.find(byDenom(denoms[1]))?.pythHistoryFeedId
|
||||
//if (!pythFeedId1 || !pythFeedId2) return 'Osmosis'
|
||||
return 'Pyth Oracle'
|
||||
}
|
||||
|
||||
getPythFeedIds(name: string) {
|
||||
if (name.includes(PAIR_SEPARATOR)) {
|
||||
const [denom1, denom2] = name.split(PAIR_SEPARATOR)
|
||||
const denomFeedId1 = this.assets.find((asset) => asset.denom === denom1)?.pythHistoryFeedId
|
||||
const denomFeedId2 = this.assets.find((asset) => asset.denom === denom2)?.pythHistoryFeedId
|
||||
return [denomFeedId1, denomFeedId2]
|
||||
}
|
||||
|
||||
const [symbol1, symbol2] = name.split('/')
|
||||
const feedId1 = this.assets.find((asset) => asset.symbol === symbol1)?.pythHistoryFeedId
|
||||
const feedId2 = this.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
|
||||
}
|
||||
},
|
||||
}
|
||||
|
@ -1,170 +0,0 @@
|
||||
import { useEffect, useMemo, useRef } from 'react'
|
||||
|
||||
import Card from 'components/Card'
|
||||
import DisplayCurrency from 'components/DisplayCurrency'
|
||||
import { FormattedNumber } from 'components/FormattedNumber'
|
||||
import Loading from 'components/Loading'
|
||||
import Text from 'components/Text'
|
||||
import { disabledFeatures, enabledFeatures, overrides } from 'components/Trade/TradeChart/constants'
|
||||
import { DataFeed, PAIR_SEPARATOR } from 'components/Trade/TradeChart/DataFeed'
|
||||
import PoweredByPyth from 'components/Trade/TradeChart/PoweredByPyth'
|
||||
import { BN_ZERO } from 'constants/math'
|
||||
import useAllAssets from 'hooks/assets/useAllAssets'
|
||||
import useBaseAsset from 'hooks/assets/useBasetAsset'
|
||||
import usePrices from 'hooks/usePrices'
|
||||
import useStore from 'store'
|
||||
import { BNCoin } from 'types/classes/BNCoin'
|
||||
import { byDenom } from 'utils/array'
|
||||
import {
|
||||
ChartingLibraryWidgetOptions,
|
||||
IChartingLibraryWidget,
|
||||
ResolutionString,
|
||||
Timezone,
|
||||
widget,
|
||||
} from 'utils/charting_library'
|
||||
import { magnify } from 'utils/formatters'
|
||||
|
||||
interface Props {
|
||||
buyAsset: Asset
|
||||
sellAsset: Asset
|
||||
}
|
||||
|
||||
export const TVChartContainer = (props: Props) => {
|
||||
const chartContainerRef = useRef<HTMLDivElement>() as React.MutableRefObject<HTMLInputElement>
|
||||
const widgetRef = useRef<IChartingLibraryWidget>()
|
||||
const defaultSymbol = useRef<string>(
|
||||
`${props.sellAsset.denom}${PAIR_SEPARATOR}${props.buyAsset.denom}`,
|
||||
)
|
||||
const chainConfig = useStore((s) => s.chainConfig)
|
||||
const baseAsset = useBaseAsset()
|
||||
const assets = useAllAssets()
|
||||
const dataFeed = useMemo(
|
||||
() => new DataFeed(false, assets, baseAsset.decimals, baseAsset.denom, chainConfig),
|
||||
[assets, baseAsset.decimals, baseAsset.denom, chainConfig],
|
||||
)
|
||||
const { data: prices, isLoading } = usePrices()
|
||||
const ratio = useMemo(() => {
|
||||
const priceBuyAsset = prices.find(byDenom(props.buyAsset.denom))?.amount
|
||||
const priceSellAsset = prices.find(byDenom(props.sellAsset.denom))?.amount
|
||||
|
||||
if (!priceBuyAsset || !priceSellAsset) return BN_ZERO
|
||||
return priceBuyAsset.dividedBy(priceSellAsset)
|
||||
}, [prices, props.buyAsset.denom, props.sellAsset.denom])
|
||||
|
||||
useEffect(() => {
|
||||
const widgetOptions: ChartingLibraryWidgetOptions = {
|
||||
symbol: defaultSymbol.current,
|
||||
datafeed: dataFeed,
|
||||
interval: '1h' as ResolutionString,
|
||||
library_path: '/charting_library/',
|
||||
locale: 'en',
|
||||
time_scale: {
|
||||
min_bar_spacing: 12,
|
||||
},
|
||||
toolbar_bg: '#220E1D',
|
||||
disabled_features: disabledFeatures,
|
||||
enabled_features: enabledFeatures,
|
||||
charts_storage_api_version: '1.1',
|
||||
client_id: 'sample-implementation',
|
||||
timezone: 'Etc/UTC' as Timezone,
|
||||
user_id: 'not-set',
|
||||
fullscreen: false,
|
||||
autosize: true,
|
||||
container: chartContainerRef.current,
|
||||
custom_css_url: '/tradingview.css',
|
||||
settings_overrides: {
|
||||
'paneProperties.background': '#220E1D',
|
||||
'paneProperties.backgroundType': 'solid',
|
||||
'paneProperties.vertGridProperties.color': '#220E1D',
|
||||
'paneProperties.horzGridProperties.color': '#220E1D',
|
||||
'mainSeriesProperties.candleStyle.upColor': '#3DAE9A',
|
||||
'mainSeriesProperties.candleStyle.downColor': '#AE3D3D',
|
||||
'mainSeriesProperties.candleStyle.borderColor': '#232834',
|
||||
'mainSeriesProperties.candleStyle.borderUpColor': '#3DAE9A',
|
||||
'mainSeriesProperties.candleStyle.borderDownColor': '#AE3D3D',
|
||||
'mainSeriesProperties.candleStyle.wickUpColor': '#3DAE9A',
|
||||
'mainSeriesProperties.candleStyle.wickDownColor': '#AE3D3D',
|
||||
'mainSeriesProperties.candleStyle.barColorsOnPrevClose': false,
|
||||
'scalesProperties.textColor': 'rgba(255, 255, 255, 0.7)',
|
||||
'paneProperties.legendProperties.showSeriesTitle': true,
|
||||
'paneProperties.legendProperties.showVolume': false,
|
||||
'paneProperties.legendProperties.showStudyValues': false,
|
||||
'paneProperties.legendProperties.showStudyTitles': false,
|
||||
'scalesProperties.axisHighlightColor': '#381730',
|
||||
'linetooltrendline.color': '#3DAE9A',
|
||||
'linetooltrendline.linewidth': 10,
|
||||
},
|
||||
overrides,
|
||||
loading_screen: {
|
||||
backgroundColor: '#220E1D',
|
||||
foregroundColor: 'rgba(255, 255, 255, 0.3)',
|
||||
},
|
||||
theme: 'dark',
|
||||
}
|
||||
|
||||
const tvWidget = new widget(widgetOptions)
|
||||
|
||||
tvWidget.onChartReady(() => {
|
||||
widgetRef.current = tvWidget
|
||||
})
|
||||
|
||||
return () => {
|
||||
tvWidget.remove()
|
||||
}
|
||||
}, [dataFeed, defaultSymbol])
|
||||
|
||||
useEffect(() => {
|
||||
if (widgetRef?.current) {
|
||||
widgetRef.current.setSymbol(
|
||||
`${props.sellAsset.denom}${PAIR_SEPARATOR}${props.buyAsset.denom}`,
|
||||
widgetRef.current.chart().resolution() || ('1h' as ResolutionString),
|
||||
() => {},
|
||||
)
|
||||
}
|
||||
}, [props.buyAsset.denom, props.sellAsset.denom])
|
||||
|
||||
return (
|
||||
<Card
|
||||
title={
|
||||
<div className='flex items-center w-full bg-white/10'>
|
||||
<Text size='lg' className='flex items-center flex-1 p-4 font-semibold'>
|
||||
Trading Chart
|
||||
</Text>
|
||||
{ratio.isZero() || isLoading ? (
|
||||
<Loading className='h-4 mr-4 w-60' />
|
||||
) : (
|
||||
<div className='flex items-center gap-1 p-4'>
|
||||
<Text size='sm'>1 {props.buyAsset.symbol}</Text>
|
||||
<FormattedNumber
|
||||
className='text-sm'
|
||||
amount={Number(ratio.toPrecision(6))}
|
||||
options={{
|
||||
prefix: '= ',
|
||||
suffix: ` ${props.sellAsset.symbol}`,
|
||||
abbreviated: false,
|
||||
maxDecimals: props.sellAsset.decimals,
|
||||
}}
|
||||
/>
|
||||
<DisplayCurrency
|
||||
parentheses
|
||||
options={{ abbreviated: false }}
|
||||
className='justify-end pl-2 text-sm text-white/50'
|
||||
coin={
|
||||
new BNCoin({
|
||||
denom: props.buyAsset.denom,
|
||||
amount: magnify(1, props.buyAsset).toString(),
|
||||
})
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
}
|
||||
contentClassName='px-0.5 pb-0.5 h-full bg-chart w-[calc(100%-2px)] ml-[1px]'
|
||||
className='h-[70dvh] max-h-[980px] min-h-[560px]'
|
||||
>
|
||||
<div ref={chartContainerRef} className='h-[calc(100%-32px)] overflow-hidden' />
|
||||
<PoweredByPyth />
|
||||
</Card>
|
||||
)
|
||||
}
|
@ -1,7 +1,6 @@
|
||||
import {
|
||||
ChartingLibraryFeatureset,
|
||||
LibrarySymbolInfo,
|
||||
ResolutionString,
|
||||
SeriesFormat,
|
||||
Timezone,
|
||||
} from 'utils/charting_library/charting_library'
|
||||
@ -20,6 +19,7 @@ export const enabledFeatures: ChartingLibraryFeatureset[] = [
|
||||
'timezone_menu',
|
||||
'header_settings',
|
||||
'use_localstorage_for_settings',
|
||||
'chart_zoom',
|
||||
]
|
||||
|
||||
export const overrides = {
|
||||
@ -38,5 +38,4 @@ export const defaultSymbolInfo: Partial<LibrarySymbolInfo> = {
|
||||
has_daily: true,
|
||||
has_weekly_and_monthly: false,
|
||||
format: 'price' as SeriesFormat,
|
||||
supported_resolutions: ['15'] as ResolutionString[],
|
||||
}
|
||||
|
@ -1,59 +1,124 @@
|
||||
import dynamic from 'next/dynamic'
|
||||
import Script from 'next/script'
|
||||
import { useState } from 'react'
|
||||
import { useEffect, useMemo, useRef } from 'react'
|
||||
|
||||
import Card from 'components/Card'
|
||||
import { CircularProgress } from 'components/CircularProgress'
|
||||
import DisplayCurrency from 'components/DisplayCurrency'
|
||||
import { FormattedNumber } from 'components/FormattedNumber'
|
||||
import Loading from 'components/Loading'
|
||||
import Text from 'components/Text'
|
||||
import { datafeed } from 'components/Trade/TradeChart/DataFeed'
|
||||
import PoweredByPyth from 'components/Trade/TradeChart/PoweredByPyth'
|
||||
|
||||
const TVChartContainer = dynamic(
|
||||
() => import('components/Trade/TradeChart/TVChartContainer').then((mod) => mod.TVChartContainer),
|
||||
{ ssr: false },
|
||||
)
|
||||
|
||||
import { disabledFeatures, enabledFeatures } from 'components/Trade/TradeChart/constants'
|
||||
import { BN_ZERO } from 'constants/math'
|
||||
import usePrices from 'hooks/usePrices'
|
||||
import { BNCoin } from 'types/classes/BNCoin'
|
||||
import { byDenom } from 'utils/array'
|
||||
import { ChartingLibraryWidgetOptions, ResolutionString, widget } from 'utils/charting_library'
|
||||
import { magnify } from 'utils/formatters'
|
||||
interface Props {
|
||||
buyAsset: Asset
|
||||
sellAsset: Asset
|
||||
}
|
||||
|
||||
export default function TradeChart(props: Props) {
|
||||
const [isScriptReady, setIsScriptReady] = useState(false)
|
||||
const { data: prices, isLoading } = usePrices()
|
||||
const ratio = useMemo(() => {
|
||||
const priceBuyAsset = prices.find(byDenom(props.buyAsset.denom))?.amount
|
||||
const priceSellAsset = prices.find(byDenom(props.sellAsset.denom))?.amount
|
||||
|
||||
if (!priceBuyAsset || !priceSellAsset) return BN_ZERO
|
||||
return priceBuyAsset.dividedBy(priceSellAsset)
|
||||
}, [prices, props.buyAsset.denom, props.sellAsset.denom])
|
||||
|
||||
const chartContainerRef = useRef<HTMLDivElement>() as React.MutableRefObject<HTMLInputElement>
|
||||
|
||||
useEffect(() => {
|
||||
if (typeof window !== 'undefined' && window.TradingView) {
|
||||
const widgetOptions: ChartingLibraryWidgetOptions = {
|
||||
symbol: props.buyAsset.pythFeedName ?? `${props.buyAsset.symbol}/USD`,
|
||||
datafeed: datafeed,
|
||||
interval: '1h' as ResolutionString,
|
||||
library_path: '/charting_library/',
|
||||
locale: 'en',
|
||||
time_scale: {
|
||||
min_bar_spacing: 12,
|
||||
},
|
||||
toolbar_bg: '#220E1D',
|
||||
disabled_features: disabledFeatures,
|
||||
enabled_features: enabledFeatures,
|
||||
fullscreen: false,
|
||||
autosize: true,
|
||||
container: chartContainerRef.current,
|
||||
theme: 'dark',
|
||||
overrides: {
|
||||
'paneProperties.background': '#220E1D',
|
||||
'linetooltrendline.linecolor': 'rgba(255, 255, 255, 0.8)',
|
||||
'linetooltrendline.linewidth': 2,
|
||||
},
|
||||
loading_screen: {
|
||||
backgroundColor: '#220E1D',
|
||||
foregroundColor: 'rgba(255, 255, 255, 0.3)',
|
||||
},
|
||||
custom_css_url: '/tradingview.css',
|
||||
}
|
||||
|
||||
const tvWidget = new widget(widgetOptions)
|
||||
tvWidget.onChartReady(() => {
|
||||
const chart = tvWidget.chart()
|
||||
chart.getSeries().setChartStyleProperties(1, {
|
||||
upColor: '#3DAE9A',
|
||||
downColor: '#AE3D3D',
|
||||
borderColor: '#232834',
|
||||
borderUpColor: '#3DAE9A',
|
||||
borderDownColor: '#AE3D3D',
|
||||
wickUpColor: '#3DAE9A',
|
||||
wickDownColor: '#AE3D3D',
|
||||
barColorsOnPrevClose: false,
|
||||
})
|
||||
})
|
||||
}
|
||||
}, [props.buyAsset.pythFeedName, props.buyAsset.symbol])
|
||||
|
||||
return (
|
||||
<>
|
||||
<Script
|
||||
src='/datafeeds/udf/dist/bundle.js'
|
||||
strategy='lazyOnload'
|
||||
onReady={() => {
|
||||
setIsScriptReady(true)
|
||||
}}
|
||||
onLoad={() => {
|
||||
setIsScriptReady(true)
|
||||
}}
|
||||
/>
|
||||
{isScriptReady ? (
|
||||
<TVChartContainer buyAsset={props.buyAsset} sellAsset={props.sellAsset} />
|
||||
) : (
|
||||
<Card
|
||||
title={
|
||||
<div className='flex items-center w-full bg-white/10'>
|
||||
<Text size='lg' className='flex items-center flex-1 p-4 font-semibold'>
|
||||
Trading Chart
|
||||
</Text>
|
||||
<Loading className='h-4 mr-4 w-60' />
|
||||
<Card
|
||||
title={
|
||||
<div className='flex items-center w-full bg-white/10'>
|
||||
<Text size='lg' className='flex items-center flex-1 p-4 font-semibold'>
|
||||
Trading Chart
|
||||
</Text>
|
||||
{ratio.isZero() || isLoading ? (
|
||||
<Loading className='h-4 mr-4 w-60' />
|
||||
) : (
|
||||
<div className='flex items-center gap-1 p-4'>
|
||||
<Text size='sm'>1 {props.buyAsset.symbol}</Text>
|
||||
<FormattedNumber
|
||||
className='text-sm'
|
||||
amount={Number(ratio.toPrecision(6))}
|
||||
options={{
|
||||
prefix: '= ',
|
||||
suffix: ` ${props.sellAsset.symbol}`,
|
||||
abbreviated: false,
|
||||
maxDecimals: props.sellAsset.decimals,
|
||||
}}
|
||||
/>
|
||||
<DisplayCurrency
|
||||
parentheses
|
||||
options={{ abbreviated: false }}
|
||||
className='justify-end pl-2 text-sm text-white/50'
|
||||
coin={
|
||||
new BNCoin({
|
||||
denom: props.buyAsset.denom,
|
||||
amount: magnify(1, props.buyAsset).toString(),
|
||||
})
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
}
|
||||
contentClassName='px-0.5 pb-0.5 h-full bg-chart w-[calc(100%-2px)] ml-[1px]'
|
||||
className='h-[70dvh] max-h-[980px] min-h-[560px]'
|
||||
>
|
||||
<div className='flex items-center justify-center w-full h-[calc(100%-32px)] rounded-b-base bg-chart'>
|
||||
<CircularProgress size={60} className='opacity-50' />
|
||||
</div>
|
||||
<PoweredByPyth />
|
||||
</Card>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
}
|
||||
contentClassName='px-0.5 pb-0.5 h-full bg-chart w-[calc(100%-2px)] ml-[1px]'
|
||||
className='h-[70dvh] max-h-[980px] min-h-[560px]'
|
||||
>
|
||||
<div ref={chartContainerRef} className='h-[calc(100%-32px)] overflow-hidden' />
|
||||
<PoweredByPyth />
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
|
148
src/components/Trade/TradeChart/streaming.ts
Normal file
148
src/components/Trade/TradeChart/streaming.ts
Normal file
@ -0,0 +1,148 @@
|
||||
import { pythEndpoints } from 'constants/pyth'
|
||||
import {
|
||||
LibrarySymbolInfo,
|
||||
ResolutionString,
|
||||
SubscribeBarsCallback,
|
||||
} from 'utils/charting_library/charting_library'
|
||||
|
||||
const streamingUrl = `${pythEndpoints.candles}/streaming`
|
||||
const channelToSubscription = new Map()
|
||||
|
||||
function handleStreamingData(data: StreamData) {
|
||||
const { id, p, t } = data
|
||||
|
||||
const tradePrice = p
|
||||
const tradeTime = t * 1000 // Multiplying by 1000 to get milliseconds
|
||||
|
||||
const channelString = id
|
||||
const subscriptionItem = channelToSubscription.get(channelString)
|
||||
|
||||
if (!subscriptionItem) {
|
||||
return
|
||||
}
|
||||
|
||||
const lastDailyBar = subscriptionItem.lastDailyBar
|
||||
const nextDailyBarTime = getNextDailyBarTime(lastDailyBar.time)
|
||||
|
||||
let bar: Bar
|
||||
if (tradeTime >= nextDailyBarTime) {
|
||||
bar = {
|
||||
time: nextDailyBarTime,
|
||||
open: tradePrice,
|
||||
high: tradePrice,
|
||||
low: tradePrice,
|
||||
close: tradePrice,
|
||||
}
|
||||
} else {
|
||||
bar = {
|
||||
...lastDailyBar,
|
||||
high: Math.max(lastDailyBar.high, tradePrice),
|
||||
low: Math.min(lastDailyBar.low, tradePrice),
|
||||
close: tradePrice,
|
||||
}
|
||||
}
|
||||
|
||||
subscriptionItem.lastDailyBar = bar
|
||||
|
||||
// Send data to every subscriber of that symbol
|
||||
subscriptionItem.handlers.forEach((handler: any) => handler.callback(bar))
|
||||
channelToSubscription.set(channelString, subscriptionItem)
|
||||
}
|
||||
|
||||
function startStreaming(retries = 3, delay = 3000) {
|
||||
fetch(streamingUrl)
|
||||
.then((response) => {
|
||||
if (response.body === null) return
|
||||
const reader = response.body.getReader()
|
||||
|
||||
function streamData() {
|
||||
reader
|
||||
.read()
|
||||
.then(({ value, done }) => {
|
||||
if (done) {
|
||||
// console.error('Streaming ended.')
|
||||
return
|
||||
}
|
||||
|
||||
const dataStrings = new TextDecoder().decode(value).split('\n')
|
||||
dataStrings.forEach((dataString) => {
|
||||
const trimmedDataString = dataString.trim()
|
||||
if (trimmedDataString) {
|
||||
try {
|
||||
var jsonData = JSON.parse(trimmedDataString)
|
||||
handleStreamingData(jsonData)
|
||||
} catch (e) {
|
||||
if (e instanceof Error) {
|
||||
// console.error('Error parsing JSON:', e.message)
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
streamData()
|
||||
})
|
||||
.catch((error) => {
|
||||
// console.error('Error reading from stream:', error)
|
||||
attemptReconnect(retries, delay)
|
||||
})
|
||||
}
|
||||
|
||||
streamData()
|
||||
})
|
||||
.catch((error) => {
|
||||
// console.error('Error fetching from the streaming endpoint:', error)
|
||||
})
|
||||
function attemptReconnect(retriesLeft: number, delay: number) {
|
||||
if (retriesLeft > 0) {
|
||||
setTimeout(() => {
|
||||
startStreaming(retriesLeft - 1, delay)
|
||||
}, delay)
|
||||
} else {
|
||||
// console.error('Maximum reconnection attempts reached.')
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function getNextDailyBarTime(barTime: number) {
|
||||
const date = new Date(barTime * 1000)
|
||||
date.setDate(date.getDate() + 1)
|
||||
return date.getTime() / 1000
|
||||
}
|
||||
|
||||
export function subscribeOnStream(
|
||||
symbolInfo: LibrarySymbolInfo,
|
||||
resolution: ResolutionString,
|
||||
onRealtimeCallback: SubscribeBarsCallback,
|
||||
subscriberUID: string,
|
||||
onResetCacheNeededCallback: () => void,
|
||||
lastDailyBar: Bar,
|
||||
) {
|
||||
const channelString = symbolInfo.ticker
|
||||
const handler = {
|
||||
id: subscriberUID,
|
||||
callback: onRealtimeCallback,
|
||||
}
|
||||
let subscriptionItem = channelToSubscription.get(channelString)
|
||||
subscriptionItem = {
|
||||
subscriberUID,
|
||||
resolution,
|
||||
lastDailyBar,
|
||||
handlers: [handler],
|
||||
}
|
||||
channelToSubscription.set(channelString, subscriptionItem)
|
||||
startStreaming()
|
||||
}
|
||||
|
||||
export function unsubscribeFromStream(subscriberUID: string) {
|
||||
for (const channelString of channelToSubscription.keys()) {
|
||||
const subscriptionItem = channelToSubscription.get(channelString)
|
||||
const handlerIndex = subscriptionItem.handlers.findIndex(
|
||||
(handler: any) => handler.id === subscriberUID,
|
||||
)
|
||||
|
||||
if (handlerIndex !== -1) {
|
||||
channelToSubscription.delete(channelString)
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
@ -12,7 +12,7 @@ const AKT: AssetMetaData = {
|
||||
isAutoLendEnabled: true,
|
||||
isBorrowEnabled: true,
|
||||
pythPriceFeedId: '4ea5bb4d2f5900cc2e97ba534240950740b4d3b89fe712a94a7304fd2fd92702',
|
||||
pythHistoryFeedId: 'Crypto.AKT/USD',
|
||||
pythFeedName: 'AKTUSD',
|
||||
}
|
||||
|
||||
export default AKT
|
||||
|
@ -13,7 +13,7 @@ const ATOM: AssetMetaData = {
|
||||
isAutoLendEnabled: true,
|
||||
isBorrowEnabled: true,
|
||||
pythPriceFeedId: 'b00b60f88b03a6a625a8d1c048c3f66653edf217439983d037e7222c4e612819',
|
||||
pythHistoryFeedId: 'Crypto.ATOM/USD',
|
||||
pythFeedName: 'ATOMUSD',
|
||||
}
|
||||
|
||||
export default ATOM
|
||||
|
@ -11,7 +11,7 @@ const AXL: AssetMetaData = {
|
||||
isDisplayCurrency: true,
|
||||
isAutoLendEnabled: false,
|
||||
pythPriceFeedId: '60144b1d5c9e9851732ad1d9760e3485ef80be39b984f6bf60f82b28a2b7f126',
|
||||
pythHistoryFeedId: 'Crypto.AXL/USD',
|
||||
pythFeedName: 'AXLUSD',
|
||||
}
|
||||
|
||||
export default AXL
|
||||
|
@ -12,7 +12,7 @@ const DYDX: AssetMetaData = {
|
||||
isAutoLendEnabled: true,
|
||||
isBorrowEnabled: true,
|
||||
pythPriceFeedId: '6489800bb8974169adfe35937bf6736507097d13c190d760c557108c7e93a81b',
|
||||
pythHistoryFeedId: 'Crypto.DYDX/USD',
|
||||
pythFeedName: 'DYDXUSD',
|
||||
}
|
||||
|
||||
export default DYDX
|
||||
|
@ -12,7 +12,7 @@ const INJ: AssetMetaData = {
|
||||
isAutoLendEnabled: true,
|
||||
isBorrowEnabled: true,
|
||||
pythPriceFeedId: '7a5bc1d2b56ad029048cd63964b3ad2776eadf812edc1a43a31406cb54bff592',
|
||||
pythHistoryFeedId: 'Crypto.INJ/USD',
|
||||
pythFeedName: 'INJUSD',
|
||||
}
|
||||
|
||||
export default INJ
|
||||
|
@ -11,7 +11,7 @@ const NTRN: AssetMetaData = {
|
||||
isMarket: true,
|
||||
isBorrowEnabled: true,
|
||||
isAutoLendEnabled: true,
|
||||
pythHistoryFeedId: 'Crypto.NTRN/USD',
|
||||
pythFeedName: 'NTRNUSD',
|
||||
}
|
||||
|
||||
export default NTRN
|
||||
|
@ -12,7 +12,7 @@ const OSMO: AssetMetaData = {
|
||||
isDisplayCurrency: true,
|
||||
isAutoLendEnabled: true,
|
||||
pythPriceFeedId: '5867f5683c757393a0670ef0f701490950fe93fdb006d181c8265a831ac0c5c6',
|
||||
pythHistoryFeedId: 'Crypto.OSMO/USD',
|
||||
pythFeedName: 'OSMOUSD',
|
||||
}
|
||||
|
||||
export default OSMO
|
||||
|
@ -12,7 +12,7 @@ const TIA: AssetMetaData = {
|
||||
isAutoLendEnabled: true,
|
||||
isBorrowEnabled: true,
|
||||
pythPriceFeedId: '09f7c1d7dfbb7df2b8fe3d3d87ee94a2259d212da4f30c1f0540d066dfa44723',
|
||||
pythHistoryFeedId: 'Crypto.TIA/USD',
|
||||
pythFeedName: 'TIAUSD',
|
||||
}
|
||||
|
||||
export default TIA
|
||||
|
@ -13,7 +13,7 @@ const USDCaxl: AssetMetaData = {
|
||||
isBorrowEnabled: true,
|
||||
isAutoLendEnabled: true,
|
||||
pythPriceFeedId: 'eaa020c61cc479712813461ce153894a96a6c00b21ed0cfc2798d1f9a9e9c94a',
|
||||
pythHistoryFeedId: 'Crypto.USDC/USD',
|
||||
pythFeedName: 'USDCUSD',
|
||||
}
|
||||
|
||||
export default USDCaxl
|
||||
|
@ -13,7 +13,7 @@ const USDC: AssetMetaData = {
|
||||
isBorrowEnabled: true,
|
||||
isAutoLendEnabled: true,
|
||||
pythPriceFeedId: 'eaa020c61cc479712813461ce153894a96a6c00b21ed0cfc2798d1f9a9e9c94a',
|
||||
pythHistoryFeedId: 'Crypto.USDC/USD',
|
||||
pythFeedName: 'USDCUSD',
|
||||
}
|
||||
|
||||
export default USDC
|
||||
|
@ -13,7 +13,7 @@ const USDT: AssetMetaData = {
|
||||
isAutoLendEnabled: true,
|
||||
isBorrowEnabled: true,
|
||||
pythPriceFeedId: '2b89b9dc8fdf9f34709a5b106b472f0f39bb6ca9ce04b0fd7f2e971688e2e53b',
|
||||
pythHistoryFeedId: 'Crypto.USDT/USD',
|
||||
pythFeedName: 'USDTUSD',
|
||||
}
|
||||
|
||||
export default USDT
|
||||
|
@ -12,7 +12,7 @@ const WBTCaxl: AssetMetaData = {
|
||||
isAutoLendEnabled: true,
|
||||
isBorrowEnabled: true,
|
||||
pythPriceFeedId: 'c9d8b075a5c69303365ae23633d4e085199bf5c520a3b90fed1322a0342ffc33',
|
||||
pythHistoryFeedId: 'Crypto.WBTC/USD',
|
||||
pythFeedName: 'WBTCUSD',
|
||||
}
|
||||
|
||||
export default WBTCaxl
|
||||
|
@ -12,7 +12,7 @@ const WETHaxl: AssetMetaData = {
|
||||
isAutoLendEnabled: true,
|
||||
isBorrowEnabled: true,
|
||||
pythPriceFeedId: 'ff61491a931112ddf1bd8147cd1b641375f79f5825126d665480874634fd0ace',
|
||||
pythHistoryFeedId: 'Crypto.ETH/USD',
|
||||
pythFeedName: 'ETHUSD',
|
||||
}
|
||||
|
||||
export default WETHaxl
|
||||
|
@ -134,7 +134,6 @@ const Osmosis1: ChainConfig = {
|
||||
rpc: 'https://osmosis-node.marsprotocol.io/GGSFGSFGFG34/osmosis-rpc-front/',
|
||||
rest: 'https://osmosis-node.marsprotocol.io/GGSFGSFGFG34/osmosis-lcd-front/',
|
||||
swap: 'https://app.osmosis.zone',
|
||||
graphCandles: 'https://osmosis-candles.marsprotocol.io',
|
||||
explorer: 'https://www.mintscan.io/osmosis/transactions/',
|
||||
pools:
|
||||
(process.env.NEXT_PUBLIC_OSMOSIS_REST ||
|
||||
|
@ -1,4 +1,4 @@
|
||||
export const pythEndpoints = {
|
||||
api: 'https://hermes.pyth.network/api',
|
||||
candles: 'https://benchmarks.pyth.network',
|
||||
candles: 'https://benchmarks.pyth.network/v1/shims/tradingview',
|
||||
}
|
||||
|
@ -3,7 +3,11 @@ import { Head, Html, Main, NextScript } from 'next/document'
|
||||
export default function Document() {
|
||||
return (
|
||||
<Html className='p-0 m-0' lang='en'>
|
||||
<Head />
|
||||
<Head>
|
||||
<script defer src='/charting_library/charting_library.standalone.js' />
|
||||
<script defer src='/datafeeds/udf/dist/bundle.js' />
|
||||
<script defer src='https://cdnjs.cloudflare.com/ajax/libs/socket.io/4.5.4/socket.io.js' />
|
||||
</Head>
|
||||
<body className='p-0 m-0 font-sans text-white cursor-default bg-body scrollbar-hide'>
|
||||
<Main />
|
||||
<NextScript />
|
||||
|
2
src/types/interfaces/asset.d.ts
vendored
2
src/types/interfaces/asset.d.ts
vendored
@ -21,7 +21,7 @@ interface AssetMetaData {
|
||||
logo: string
|
||||
name: string
|
||||
prefix?: string
|
||||
pythHistoryFeedId?: string
|
||||
pythFeedName?: string
|
||||
pythPriceFeedId?: string
|
||||
symbol: string
|
||||
testnetDenom?: string
|
||||
|
1
src/types/interfaces/chain.d.ts
vendored
1
src/types/interfaces/chain.d.ts
vendored
@ -27,7 +27,6 @@ interface ChainConfig {
|
||||
rest: string
|
||||
rpc: string
|
||||
swap: string
|
||||
graphCandles?: string
|
||||
explorer: string
|
||||
pools: string
|
||||
aprs: {
|
||||
|
34
src/types/interfaces/components/Trade/TradingChart.d.ts
vendored
Normal file
34
src/types/interfaces/components/Trade/TradingChart.d.ts
vendored
Normal file
@ -0,0 +1,34 @@
|
||||
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
|
||||
}
|
||||
|
||||
interface Bar {
|
||||
time: number
|
||||
open: number
|
||||
high: number
|
||||
low: number
|
||||
close: number
|
||||
}
|
||||
|
||||
interface StreamData {
|
||||
id: string
|
||||
p: number
|
||||
t: number
|
||||
f: string
|
||||
s: number
|
||||
}
|
@ -1,6 +1,6 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "es5",
|
||||
"target": "ES2020",
|
||||
"lib": ["dom", "dom.iterable", "esnext"],
|
||||
"allowJs": true,
|
||||
"skipLibCheck": true,
|
||||
|
Loading…
Reference in New Issue
Block a user