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;
|
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 menu */
|
||||||
.floating-toolbar-react-widgets__button:hover,
|
.floating-toolbar-react-widgets__button:hover,
|
||||||
[class^='button-']:hover:before {
|
[class^='button-']:hover:before {
|
||||||
|
@ -27,7 +27,7 @@ export default async function getPrices(chainConfig: ChainConfig): Promise<BNCoi
|
|||||||
const oraclePrices = await getOraclePrices(chainConfig, assetsWithOraclePrices)
|
const oraclePrices = await getOraclePrices(chainConfig, assetsWithOraclePrices)
|
||||||
const poolPrices = await requestPoolPrices(chainConfig, assetsWithPoolIds, pythAndOraclePrices)
|
const poolPrices = await requestPoolPrices(chainConfig, assetsWithPoolIds, pythAndOraclePrices)
|
||||||
|
|
||||||
useStore.setState({ isOracleStale: false })
|
if (oraclePrices) useStore.setState({ isOracleStale: false })
|
||||||
|
|
||||||
return [...pythAndOraclePrices, ...oraclePrices, ...poolPrices, usdPrice]
|
return [...pythAndOraclePrices, ...oraclePrices, ...poolPrices, usdPrice]
|
||||||
} catch (ex) {
|
} catch (ex) {
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
import classNames from 'classnames'
|
import classNames from 'classnames'
|
||||||
import { isDesktop } from 'react-device-detect'
|
import { isDesktop } from 'react-device-detect'
|
||||||
|
import { useMemo } from 'react'
|
||||||
|
|
||||||
import AccountMenu from 'components/Account/AccountMenu'
|
import AccountMenu from 'components/Account/AccountMenu'
|
||||||
import EscButton from 'components/Button/EscButton'
|
import EscButton from 'components/Button/EscButton'
|
||||||
@ -54,6 +55,8 @@ export default function DesktopHeader() {
|
|||||||
useStore.setState({ focusComponent: null })
|
useStore.setState({ focusComponent: null })
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const showStaleOracle = useMemo(() => isOracleStale && address, [isOracleStale, address])
|
||||||
|
|
||||||
if (!isDesktop) return null
|
if (!isDesktop) return null
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@ -84,7 +87,7 @@ export default function DesktopHeader() {
|
|||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div className='flex gap-4'>
|
<div className='flex gap-4'>
|
||||||
{address && isOracleStale && <OracleResyncButton />}
|
{showStaleOracle && <OracleResyncButton />}
|
||||||
{accountId && <RewardsCenter />}
|
{accountId && <RewardsCenter />}
|
||||||
{address && !isHLS && <AccountMenu />}
|
{address && !isHLS && <AccountMenu />}
|
||||||
<Wallet />
|
<Wallet />
|
||||||
|
@ -1,508 +1,142 @@
|
|||||||
import { defaultSymbolInfo } from 'components/Trade/TradeChart/constants'
|
import { subscribeOnStream, unsubscribeFromStream } from 'components/Trade/TradeChart/streaming'
|
||||||
import { MILLISECONDS_PER_MINUTE } from 'constants/math'
|
|
||||||
import { pythEndpoints } from 'constants/pyth'
|
import { pythEndpoints } from 'constants/pyth'
|
||||||
import { byDenom } from 'utils/array'
|
|
||||||
import {
|
import {
|
||||||
Bar,
|
|
||||||
ErrorCallback,
|
ErrorCallback,
|
||||||
HistoryCallback,
|
HistoryCallback,
|
||||||
IDatafeedChartApi,
|
|
||||||
LibrarySymbolInfo,
|
LibrarySymbolInfo,
|
||||||
OnReadyCallback,
|
OnReadyCallback,
|
||||||
PeriodParams,
|
PeriodParams,
|
||||||
ResolutionString,
|
ResolutionString,
|
||||||
ResolveCallback,
|
ResolveCallback,
|
||||||
|
SearchSymbolsCallback,
|
||||||
|
SubscribeBarsCallback,
|
||||||
|
SymbolResolveExtension,
|
||||||
} from 'utils/charting_library'
|
} from 'utils/charting_library'
|
||||||
import { BN } from 'utils/helpers'
|
|
||||||
import { devideByPotentiallyZero } from 'utils/math'
|
|
||||||
|
|
||||||
interface PythBarQueryData {
|
const lastBarsCache = new Map()
|
||||||
s: string
|
|
||||||
t: number[]
|
|
||||||
o: number[]
|
|
||||||
h: number[]
|
|
||||||
l: number[]
|
|
||||||
c: number[]
|
|
||||||
v: number[]
|
|
||||||
}
|
|
||||||
|
|
||||||
interface TheGraphBarQueryData {
|
export const datafeed = {
|
||||||
close: string
|
onReady: (callback: OnReadyCallback) => {
|
||||||
high: string
|
callback({
|
||||||
low: string
|
supported_resolutions: [
|
||||||
open: string
|
'1',
|
||||||
timestamp: string
|
'2',
|
||||||
volume: string
|
'5',
|
||||||
}
|
'15',
|
||||||
|
'30',
|
||||||
export const PAIR_SEPARATOR = '<>'
|
'60',
|
||||||
|
'120',
|
||||||
export class DataFeed implements IDatafeedChartApi {
|
'240',
|
||||||
candlesEndpoint: string
|
'360',
|
||||||
candlesEndpointTheGraph: string
|
'720',
|
||||||
assets: Asset[]
|
'D',
|
||||||
debug = false
|
'1D',
|
||||||
enabledMarketAssetDenoms: string[] = []
|
'W',
|
||||||
batchSize = 1000
|
'1W',
|
||||||
baseDecimals: number = 6
|
'M',
|
||||||
baseDenom: string = 'uosmo'
|
'1M',
|
||||||
intervalsTheGraph: { [key: string]: string } = {
|
] as ResolutionString[],
|
||||||
'15': '15m',
|
supports_marks: true,
|
||||||
'30': '30m',
|
supports_timescale_marks: false,
|
||||||
'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 }),
|
|
||||||
})
|
})
|
||||||
.then((res) => res.json())
|
},
|
||||||
.then((json) => {
|
getBars: (
|
||||||
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(
|
|
||||||
symbolInfo: LibrarySymbolInfo,
|
symbolInfo: LibrarySymbolInfo,
|
||||||
resolution: ResolutionString,
|
resolution: ResolutionString,
|
||||||
periodParams: PeriodParams,
|
periodParams: PeriodParams,
|
||||||
onResult: HistoryCallback,
|
onHistoryCallback: HistoryCallback,
|
||||||
): Promise<void> {
|
onErrorCallback: ErrorCallback,
|
||||||
try {
|
) => {
|
||||||
let bars = [] as Bar[]
|
const { from, to, firstDataRequest } = periodParams
|
||||||
const pythFeedIds = this.getPythFeedIds(symbolInfo.full_name)
|
fetch(
|
||||||
const now = new Date().getTime()
|
`${pythEndpoints.candles}/history?symbol=${symbolInfo.ticker}&from=${from}&to=${to}&resolution=${resolution}`,
|
||||||
const to = BN(now).dividedBy(1000).integerValue().toNumber()
|
).then((response) => {
|
||||||
const from = BN(now)
|
response
|
||||||
.minus(this.batchSize * this.millisecondsPerInterval[resolution])
|
.json()
|
||||||
.dividedBy(1000)
|
.then((data) => {
|
||||||
.integerValue()
|
if (data.errmsg) {
|
||||||
.toNumber()
|
onHistoryCallback([], { noData: true })
|
||||||
const pythFeedId1 = pythFeedIds[0]
|
return
|
||||||
const pythFeedId2 = pythFeedIds[1]
|
}
|
||||||
|
if (data.t.length === 0) {
|
||||||
if (pythFeedId1 && pythFeedId2) {
|
onHistoryCallback([], { noData: true })
|
||||||
const asset1Bars = this.queryBarData(pythFeedId1, resolution, from, to)
|
return
|
||||||
const asset2Bars = this.queryBarData(pythFeedId2, resolution, from, to)
|
}
|
||||||
|
const bars = []
|
||||||
await Promise.all([asset1Bars, asset2Bars]).then(([asset1Bars, asset2Bars]) => {
|
for (let i = 0; i < data.t.length; ++i) {
|
||||||
bars = this.combineBars(asset1Bars, asset2Bars)
|
bars.push({
|
||||||
onResult(bars)
|
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 {
|
.catch((error) => {
|
||||||
//await this.getBarsFromTheGraph(symbolInfo, resolution, to).then((bars) => onResult(bars))
|
onErrorCallback(error)
|
||||||
onResult([], { noData: true })
|
})
|
||||||
}
|
})
|
||||||
} catch (error) {
|
},
|
||||||
console.error(error)
|
subscribeBars(
|
||||||
return onResult([], { noData: true })
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async getBarsFromTheGraph(
|
|
||||||
symbolInfo: LibrarySymbolInfo,
|
symbolInfo: LibrarySymbolInfo,
|
||||||
resolution: ResolutionString,
|
resolution: ResolutionString,
|
||||||
to: number,
|
onRealtimeCallback: SubscribeBarsCallback,
|
||||||
) {
|
subscriberUID: string,
|
||||||
let pair1 = this.getPairName(symbolInfo.full_name)
|
onResetCacheNeededCallback: () => void,
|
||||||
let pair2: string = ''
|
): void {
|
||||||
let pair3: string = ''
|
subscribeOnStream(
|
||||||
let theGraphBars = [] as Bar[]
|
symbolInfo,
|
||||||
|
|
||||||
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,
|
resolution,
|
||||||
to,
|
onRealtimeCallback,
|
||||||
|
subscriberUID,
|
||||||
|
onResetCacheNeededCallback,
|
||||||
|
lastBarsCache.get(symbolInfo.ticker),
|
||||||
)
|
)
|
||||||
|
},
|
||||||
let pair2Bars: Promise<Bar[]> | null = null
|
unsubscribeBars(subscriberUID: string) {
|
||||||
|
unsubscribeFromStream(subscriberUID)
|
||||||
if (pair2) {
|
},
|
||||||
pair2Bars = this.queryBarDataTheGraph(
|
searchSymbols: (
|
||||||
pair2.split(PAIR_SEPARATOR)[0],
|
userInput: string,
|
||||||
pair2.split(PAIR_SEPARATOR)[1],
|
exchange: string,
|
||||||
resolution,
|
symbolType: string,
|
||||||
to,
|
onResult: SearchSymbolsCallback,
|
||||||
)
|
) => {
|
||||||
}
|
return
|
||||||
|
},
|
||||||
let pair3Bars: Promise<Bar[]> | null = null
|
resolveSymbol: (
|
||||||
|
symbolName: string,
|
||||||
if (pair3) {
|
onResolve: ResolveCallback,
|
||||||
pair3Bars = this.queryBarDataTheGraph(
|
onError: ErrorCallback,
|
||||||
pair3.split(PAIR_SEPARATOR)[0],
|
extension?: SymbolResolveExtension,
|
||||||
pair3.split(PAIR_SEPARATOR)[1],
|
) => {
|
||||||
resolution,
|
try {
|
||||||
to,
|
fetch(`${pythEndpoints.candles}/symbols?symbol=${symbolName}`).then((response) => {
|
||||||
)
|
response
|
||||||
}
|
.json()
|
||||||
|
.then((symbolInfo) => {
|
||||||
await Promise.all([pair1Bars, pair2Bars, pair3Bars]).then(
|
if (symbolInfo.errmsg) {
|
||||||
([pair1Bars, pair2Bars, pair3Bars]) => {
|
symbolInfo.description = symbolName
|
||||||
let bars = pair1Bars
|
} else {
|
||||||
|
symbolInfo.description = symbolInfo.ticker.split('Crypto.')[1]
|
||||||
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
|
|
||||||
}
|
}
|
||||||
}
|
onResolve(symbolInfo)
|
||||||
`
|
})
|
||||||
|
.catch((error) => {
|
||||||
return fetch(this.candlesEndpointTheGraph, {
|
console.error(error)
|
||||||
method: 'POST',
|
return
|
||||||
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) => {
|
} catch (error) {
|
||||||
if (this.debug) console.error(err)
|
console.error(error)
|
||||||
throw err
|
return
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
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 = 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 {
|
import {
|
||||||
ChartingLibraryFeatureset,
|
ChartingLibraryFeatureset,
|
||||||
LibrarySymbolInfo,
|
LibrarySymbolInfo,
|
||||||
ResolutionString,
|
|
||||||
SeriesFormat,
|
SeriesFormat,
|
||||||
Timezone,
|
Timezone,
|
||||||
} from 'utils/charting_library/charting_library'
|
} from 'utils/charting_library/charting_library'
|
||||||
@ -20,6 +19,7 @@ export const enabledFeatures: ChartingLibraryFeatureset[] = [
|
|||||||
'timezone_menu',
|
'timezone_menu',
|
||||||
'header_settings',
|
'header_settings',
|
||||||
'use_localstorage_for_settings',
|
'use_localstorage_for_settings',
|
||||||
|
'chart_zoom',
|
||||||
]
|
]
|
||||||
|
|
||||||
export const overrides = {
|
export const overrides = {
|
||||||
@ -38,5 +38,4 @@ export const defaultSymbolInfo: Partial<LibrarySymbolInfo> = {
|
|||||||
has_daily: true,
|
has_daily: true,
|
||||||
has_weekly_and_monthly: false,
|
has_weekly_and_monthly: false,
|
||||||
format: 'price' as SeriesFormat,
|
format: 'price' as SeriesFormat,
|
||||||
supported_resolutions: ['15'] as ResolutionString[],
|
|
||||||
}
|
}
|
||||||
|
@ -1,59 +1,124 @@
|
|||||||
import dynamic from 'next/dynamic'
|
import { useEffect, useMemo, useRef } from 'react'
|
||||||
import Script from 'next/script'
|
|
||||||
import { useState } from 'react'
|
|
||||||
|
|
||||||
import Card from 'components/Card'
|
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 Loading from 'components/Loading'
|
||||||
import Text from 'components/Text'
|
import Text from 'components/Text'
|
||||||
|
import { datafeed } from 'components/Trade/TradeChart/DataFeed'
|
||||||
import PoweredByPyth from 'components/Trade/TradeChart/PoweredByPyth'
|
import PoweredByPyth from 'components/Trade/TradeChart/PoweredByPyth'
|
||||||
|
import { disabledFeatures, enabledFeatures } from 'components/Trade/TradeChart/constants'
|
||||||
const TVChartContainer = dynamic(
|
import { BN_ZERO } from 'constants/math'
|
||||||
() => import('components/Trade/TradeChart/TVChartContainer').then((mod) => mod.TVChartContainer),
|
import usePrices from 'hooks/usePrices'
|
||||||
{ ssr: false },
|
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 {
|
interface Props {
|
||||||
buyAsset: Asset
|
buyAsset: Asset
|
||||||
sellAsset: Asset
|
sellAsset: Asset
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function TradeChart(props: Props) {
|
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 (
|
return (
|
||||||
<>
|
<Card
|
||||||
<Script
|
title={
|
||||||
src='/datafeeds/udf/dist/bundle.js'
|
<div className='flex items-center w-full bg-white/10'>
|
||||||
strategy='lazyOnload'
|
<Text size='lg' className='flex items-center flex-1 p-4 font-semibold'>
|
||||||
onReady={() => {
|
Trading Chart
|
||||||
setIsScriptReady(true)
|
</Text>
|
||||||
}}
|
{ratio.isZero() || isLoading ? (
|
||||||
onLoad={() => {
|
<Loading className='h-4 mr-4 w-60' />
|
||||||
setIsScriptReady(true)
|
) : (
|
||||||
}}
|
<div className='flex items-center gap-1 p-4'>
|
||||||
/>
|
<Text size='sm'>1 {props.buyAsset.symbol}</Text>
|
||||||
{isScriptReady ? (
|
<FormattedNumber
|
||||||
<TVChartContainer buyAsset={props.buyAsset} sellAsset={props.sellAsset} />
|
className='text-sm'
|
||||||
) : (
|
amount={Number(ratio.toPrecision(6))}
|
||||||
<Card
|
options={{
|
||||||
title={
|
prefix: '= ',
|
||||||
<div className='flex items-center w-full bg-white/10'>
|
suffix: ` ${props.sellAsset.symbol}`,
|
||||||
<Text size='lg' className='flex items-center flex-1 p-4 font-semibold'>
|
abbreviated: false,
|
||||||
Trading Chart
|
maxDecimals: props.sellAsset.decimals,
|
||||||
</Text>
|
}}
|
||||||
<Loading className='h-4 mr-4 w-60' />
|
/>
|
||||||
|
<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]'
|
</div>
|
||||||
className='h-[70dvh] max-h-[980px] min-h-[560px]'
|
}
|
||||||
>
|
contentClassName='px-0.5 pb-0.5 h-full bg-chart w-[calc(100%-2px)] ml-[1px]'
|
||||||
<div className='flex items-center justify-center w-full h-[calc(100%-32px)] rounded-b-base bg-chart'>
|
className='h-[70dvh] max-h-[980px] min-h-[560px]'
|
||||||
<CircularProgress size={60} className='opacity-50' />
|
>
|
||||||
</div>
|
<div ref={chartContainerRef} className='h-[calc(100%-32px)] overflow-hidden' />
|
||||||
<PoweredByPyth />
|
<PoweredByPyth />
|
||||||
</Card>
|
</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,
|
isAutoLendEnabled: true,
|
||||||
isBorrowEnabled: true,
|
isBorrowEnabled: true,
|
||||||
pythPriceFeedId: '4ea5bb4d2f5900cc2e97ba534240950740b4d3b89fe712a94a7304fd2fd92702',
|
pythPriceFeedId: '4ea5bb4d2f5900cc2e97ba534240950740b4d3b89fe712a94a7304fd2fd92702',
|
||||||
pythHistoryFeedId: 'Crypto.AKT/USD',
|
pythFeedName: 'AKTUSD',
|
||||||
}
|
}
|
||||||
|
|
||||||
export default AKT
|
export default AKT
|
||||||
|
@ -13,7 +13,7 @@ const ATOM: AssetMetaData = {
|
|||||||
isAutoLendEnabled: true,
|
isAutoLendEnabled: true,
|
||||||
isBorrowEnabled: true,
|
isBorrowEnabled: true,
|
||||||
pythPriceFeedId: 'b00b60f88b03a6a625a8d1c048c3f66653edf217439983d037e7222c4e612819',
|
pythPriceFeedId: 'b00b60f88b03a6a625a8d1c048c3f66653edf217439983d037e7222c4e612819',
|
||||||
pythHistoryFeedId: 'Crypto.ATOM/USD',
|
pythFeedName: 'ATOMUSD',
|
||||||
}
|
}
|
||||||
|
|
||||||
export default ATOM
|
export default ATOM
|
||||||
|
@ -11,7 +11,7 @@ const AXL: AssetMetaData = {
|
|||||||
isDisplayCurrency: true,
|
isDisplayCurrency: true,
|
||||||
isAutoLendEnabled: false,
|
isAutoLendEnabled: false,
|
||||||
pythPriceFeedId: '60144b1d5c9e9851732ad1d9760e3485ef80be39b984f6bf60f82b28a2b7f126',
|
pythPriceFeedId: '60144b1d5c9e9851732ad1d9760e3485ef80be39b984f6bf60f82b28a2b7f126',
|
||||||
pythHistoryFeedId: 'Crypto.AXL/USD',
|
pythFeedName: 'AXLUSD',
|
||||||
}
|
}
|
||||||
|
|
||||||
export default AXL
|
export default AXL
|
||||||
|
@ -12,7 +12,7 @@ const DYDX: AssetMetaData = {
|
|||||||
isAutoLendEnabled: true,
|
isAutoLendEnabled: true,
|
||||||
isBorrowEnabled: true,
|
isBorrowEnabled: true,
|
||||||
pythPriceFeedId: '6489800bb8974169adfe35937bf6736507097d13c190d760c557108c7e93a81b',
|
pythPriceFeedId: '6489800bb8974169adfe35937bf6736507097d13c190d760c557108c7e93a81b',
|
||||||
pythHistoryFeedId: 'Crypto.DYDX/USD',
|
pythFeedName: 'DYDXUSD',
|
||||||
}
|
}
|
||||||
|
|
||||||
export default DYDX
|
export default DYDX
|
||||||
|
@ -12,7 +12,7 @@ const INJ: AssetMetaData = {
|
|||||||
isAutoLendEnabled: true,
|
isAutoLendEnabled: true,
|
||||||
isBorrowEnabled: true,
|
isBorrowEnabled: true,
|
||||||
pythPriceFeedId: '7a5bc1d2b56ad029048cd63964b3ad2776eadf812edc1a43a31406cb54bff592',
|
pythPriceFeedId: '7a5bc1d2b56ad029048cd63964b3ad2776eadf812edc1a43a31406cb54bff592',
|
||||||
pythHistoryFeedId: 'Crypto.INJ/USD',
|
pythFeedName: 'INJUSD',
|
||||||
}
|
}
|
||||||
|
|
||||||
export default INJ
|
export default INJ
|
||||||
|
@ -11,7 +11,7 @@ const NTRN: AssetMetaData = {
|
|||||||
isMarket: true,
|
isMarket: true,
|
||||||
isBorrowEnabled: true,
|
isBorrowEnabled: true,
|
||||||
isAutoLendEnabled: true,
|
isAutoLendEnabled: true,
|
||||||
pythHistoryFeedId: 'Crypto.NTRN/USD',
|
pythFeedName: 'NTRNUSD',
|
||||||
}
|
}
|
||||||
|
|
||||||
export default NTRN
|
export default NTRN
|
||||||
|
@ -12,7 +12,7 @@ const OSMO: AssetMetaData = {
|
|||||||
isDisplayCurrency: true,
|
isDisplayCurrency: true,
|
||||||
isAutoLendEnabled: true,
|
isAutoLendEnabled: true,
|
||||||
pythPriceFeedId: '5867f5683c757393a0670ef0f701490950fe93fdb006d181c8265a831ac0c5c6',
|
pythPriceFeedId: '5867f5683c757393a0670ef0f701490950fe93fdb006d181c8265a831ac0c5c6',
|
||||||
pythHistoryFeedId: 'Crypto.OSMO/USD',
|
pythFeedName: 'OSMOUSD',
|
||||||
}
|
}
|
||||||
|
|
||||||
export default OSMO
|
export default OSMO
|
||||||
|
@ -12,7 +12,7 @@ const TIA: AssetMetaData = {
|
|||||||
isAutoLendEnabled: true,
|
isAutoLendEnabled: true,
|
||||||
isBorrowEnabled: true,
|
isBorrowEnabled: true,
|
||||||
pythPriceFeedId: '09f7c1d7dfbb7df2b8fe3d3d87ee94a2259d212da4f30c1f0540d066dfa44723',
|
pythPriceFeedId: '09f7c1d7dfbb7df2b8fe3d3d87ee94a2259d212da4f30c1f0540d066dfa44723',
|
||||||
pythHistoryFeedId: 'Crypto.TIA/USD',
|
pythFeedName: 'TIAUSD',
|
||||||
}
|
}
|
||||||
|
|
||||||
export default TIA
|
export default TIA
|
||||||
|
@ -13,7 +13,7 @@ const USDCaxl: AssetMetaData = {
|
|||||||
isBorrowEnabled: true,
|
isBorrowEnabled: true,
|
||||||
isAutoLendEnabled: true,
|
isAutoLendEnabled: true,
|
||||||
pythPriceFeedId: 'eaa020c61cc479712813461ce153894a96a6c00b21ed0cfc2798d1f9a9e9c94a',
|
pythPriceFeedId: 'eaa020c61cc479712813461ce153894a96a6c00b21ed0cfc2798d1f9a9e9c94a',
|
||||||
pythHistoryFeedId: 'Crypto.USDC/USD',
|
pythFeedName: 'USDCUSD',
|
||||||
}
|
}
|
||||||
|
|
||||||
export default USDCaxl
|
export default USDCaxl
|
||||||
|
@ -13,7 +13,7 @@ const USDC: AssetMetaData = {
|
|||||||
isBorrowEnabled: true,
|
isBorrowEnabled: true,
|
||||||
isAutoLendEnabled: true,
|
isAutoLendEnabled: true,
|
||||||
pythPriceFeedId: 'eaa020c61cc479712813461ce153894a96a6c00b21ed0cfc2798d1f9a9e9c94a',
|
pythPriceFeedId: 'eaa020c61cc479712813461ce153894a96a6c00b21ed0cfc2798d1f9a9e9c94a',
|
||||||
pythHistoryFeedId: 'Crypto.USDC/USD',
|
pythFeedName: 'USDCUSD',
|
||||||
}
|
}
|
||||||
|
|
||||||
export default USDC
|
export default USDC
|
||||||
|
@ -13,7 +13,7 @@ const USDT: AssetMetaData = {
|
|||||||
isAutoLendEnabled: true,
|
isAutoLendEnabled: true,
|
||||||
isBorrowEnabled: true,
|
isBorrowEnabled: true,
|
||||||
pythPriceFeedId: '2b89b9dc8fdf9f34709a5b106b472f0f39bb6ca9ce04b0fd7f2e971688e2e53b',
|
pythPriceFeedId: '2b89b9dc8fdf9f34709a5b106b472f0f39bb6ca9ce04b0fd7f2e971688e2e53b',
|
||||||
pythHistoryFeedId: 'Crypto.USDT/USD',
|
pythFeedName: 'USDTUSD',
|
||||||
}
|
}
|
||||||
|
|
||||||
export default USDT
|
export default USDT
|
||||||
|
@ -12,7 +12,7 @@ const WBTCaxl: AssetMetaData = {
|
|||||||
isAutoLendEnabled: true,
|
isAutoLendEnabled: true,
|
||||||
isBorrowEnabled: true,
|
isBorrowEnabled: true,
|
||||||
pythPriceFeedId: 'c9d8b075a5c69303365ae23633d4e085199bf5c520a3b90fed1322a0342ffc33',
|
pythPriceFeedId: 'c9d8b075a5c69303365ae23633d4e085199bf5c520a3b90fed1322a0342ffc33',
|
||||||
pythHistoryFeedId: 'Crypto.WBTC/USD',
|
pythFeedName: 'WBTCUSD',
|
||||||
}
|
}
|
||||||
|
|
||||||
export default WBTCaxl
|
export default WBTCaxl
|
||||||
|
@ -12,7 +12,7 @@ const WETHaxl: AssetMetaData = {
|
|||||||
isAutoLendEnabled: true,
|
isAutoLendEnabled: true,
|
||||||
isBorrowEnabled: true,
|
isBorrowEnabled: true,
|
||||||
pythPriceFeedId: 'ff61491a931112ddf1bd8147cd1b641375f79f5825126d665480874634fd0ace',
|
pythPriceFeedId: 'ff61491a931112ddf1bd8147cd1b641375f79f5825126d665480874634fd0ace',
|
||||||
pythHistoryFeedId: 'Crypto.ETH/USD',
|
pythFeedName: 'ETHUSD',
|
||||||
}
|
}
|
||||||
|
|
||||||
export default WETHaxl
|
export default WETHaxl
|
||||||
|
@ -134,7 +134,6 @@ const Osmosis1: ChainConfig = {
|
|||||||
rpc: 'https://osmosis-node.marsprotocol.io/GGSFGSFGFG34/osmosis-rpc-front/',
|
rpc: 'https://osmosis-node.marsprotocol.io/GGSFGSFGFG34/osmosis-rpc-front/',
|
||||||
rest: 'https://osmosis-node.marsprotocol.io/GGSFGSFGFG34/osmosis-lcd-front/',
|
rest: 'https://osmosis-node.marsprotocol.io/GGSFGSFGFG34/osmosis-lcd-front/',
|
||||||
swap: 'https://app.osmosis.zone',
|
swap: 'https://app.osmosis.zone',
|
||||||
graphCandles: 'https://osmosis-candles.marsprotocol.io',
|
|
||||||
explorer: 'https://www.mintscan.io/osmosis/transactions/',
|
explorer: 'https://www.mintscan.io/osmosis/transactions/',
|
||||||
pools:
|
pools:
|
||||||
(process.env.NEXT_PUBLIC_OSMOSIS_REST ||
|
(process.env.NEXT_PUBLIC_OSMOSIS_REST ||
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
export const pythEndpoints = {
|
export const pythEndpoints = {
|
||||||
api: 'https://hermes.pyth.network/api',
|
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() {
|
export default function Document() {
|
||||||
return (
|
return (
|
||||||
<Html className='p-0 m-0' lang='en'>
|
<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'>
|
<body className='p-0 m-0 font-sans text-white cursor-default bg-body scrollbar-hide'>
|
||||||
<Main />
|
<Main />
|
||||||
<NextScript />
|
<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
|
logo: string
|
||||||
name: string
|
name: string
|
||||||
prefix?: string
|
prefix?: string
|
||||||
pythHistoryFeedId?: string
|
pythFeedName?: string
|
||||||
pythPriceFeedId?: string
|
pythPriceFeedId?: string
|
||||||
symbol: string
|
symbol: string
|
||||||
testnetDenom?: 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
|
rest: string
|
||||||
rpc: string
|
rpc: string
|
||||||
swap: string
|
swap: string
|
||||||
graphCandles?: string
|
|
||||||
explorer: string
|
explorer: string
|
||||||
pools: string
|
pools: string
|
||||||
aprs: {
|
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": {
|
"compilerOptions": {
|
||||||
"target": "es5",
|
"target": "ES2020",
|
||||||
"lib": ["dom", "dom.iterable", "esnext"],
|
"lib": ["dom", "dom.iterable", "esnext"],
|
||||||
"allowJs": true,
|
"allowJs": true,
|
||||||
"skipLibCheck": true,
|
"skipLibCheck": true,
|
||||||
|
Loading…
Reference in New Issue
Block a user