From b57ae05db1c4170500de4ca99593e8028c764da2 Mon Sep 17 00:00:00 2001 From: Linkie Link Date: Fri, 12 Jan 2024 09:07:55 +0100 Subject: [PATCH] Websocket implementation (#726) * feat: first steps * feat: added websocket support and set the TradingChart to USD * fix: oracle staleness button --- public/tradingview.css | 10 + src/api/prices/getPrices.ts | 2 +- src/components/Header/DesktopHeader.tsx | 5 +- src/components/Trade/TradeChart/DataFeed.ts | 600 ++++-------------- .../Trade/TradeChart/TVChartContainer.tsx | 170 ----- src/components/Trade/TradeChart/constants.ts | 3 +- src/components/Trade/TradeChart/index.tsx | 153 +++-- src/components/Trade/TradeChart/streaming.ts | 148 +++++ src/configs/assets/AKT.ts | 2 +- src/configs/assets/ATOM.ts | 2 +- src/configs/assets/AXL.ts | 2 +- src/configs/assets/DYDX.ts | 2 +- src/configs/assets/INJ.ts | 2 +- src/configs/assets/NTRN.ts | 2 +- src/configs/assets/OSMO.ts | 2 +- src/configs/assets/TIA.ts | 2 +- src/configs/assets/USDC.axl.ts | 2 +- src/configs/assets/USDC.ts | 2 +- src/configs/assets/USDT.ts | 2 +- src/configs/assets/WBTC.axl.ts | 2 +- src/configs/assets/WETH.xal.ts | 2 +- src/configs/chains/osmosis/osmosis-1.ts | 1 - src/constants/pyth.ts | 2 +- src/pages/_document.tsx | 6 +- src/types/interfaces/asset.d.ts | 2 +- src/types/interfaces/chain.d.ts | 1 - .../components/Trade/TradingChart.d.ts | 34 + tsconfig.json | 2 +- 28 files changed, 445 insertions(+), 720 deletions(-) delete mode 100644 src/components/Trade/TradeChart/TVChartContainer.tsx create mode 100644 src/components/Trade/TradeChart/streaming.ts create mode 100644 src/types/interfaces/components/Trade/TradingChart.d.ts diff --git a/public/tradingview.css b/public/tradingview.css index fbbc05d9..c0031ccc 100644 --- a/public/tradingview.css +++ b/public/tradingview.css @@ -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 { diff --git a/src/api/prices/getPrices.ts b/src/api/prices/getPrices.ts index 8984339f..6dc5a1fd 100644 --- a/src/api/prices/getPrices.ts +++ b/src/api/prices/getPrices.ts @@ -27,7 +27,7 @@ export default async function getPrices(chainConfig: ChainConfig): Promise isOracleStale && address, [isOracleStale, address]) + if (!isDesktop) return null return ( @@ -84,7 +87,7 @@ export default function DesktopHeader() { ) : (
- {address && isOracleStale && } + {showStaleOracle && } {accountId && } {address && !isHLS && } diff --git a/src/components/Trade/TradeChart/DataFeed.ts b/src/components/Trade/TradeChart/DataFeed.ts index 8762f309..ba7653ee 100644 --- a/src/components/Trade/TradeChart/DataFeed.ts +++ b/src/components/Trade/TradeChart/DataFeed.ts @@ -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 { - 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 | null = null - - if (pair2) { - pair2Bars = this.queryBarDataTheGraph( - pair2.split(PAIR_SEPARATOR)[0], - pair2.split(PAIR_SEPARATOR)[1], - resolution, - to, - ) - } - - let pair3Bars: Promise | 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 { - 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 { - 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 - } + }, } diff --git a/src/components/Trade/TradeChart/TVChartContainer.tsx b/src/components/Trade/TradeChart/TVChartContainer.tsx deleted file mode 100644 index 3d60b96b..00000000 --- a/src/components/Trade/TradeChart/TVChartContainer.tsx +++ /dev/null @@ -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() as React.MutableRefObject - const widgetRef = useRef() - const defaultSymbol = useRef( - `${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 ( - - - Trading Chart - - {ratio.isZero() || isLoading ? ( - - ) : ( -
- 1 {props.buyAsset.symbol} - -
- )} -
- } - 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]' - > -
- - - ) -} diff --git a/src/components/Trade/TradeChart/constants.ts b/src/components/Trade/TradeChart/constants.ts index 8b2689d4..0a4a4c2a 100644 --- a/src/components/Trade/TradeChart/constants.ts +++ b/src/components/Trade/TradeChart/constants.ts @@ -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 = { has_daily: true, has_weekly_and_monthly: false, format: 'price' as SeriesFormat, - supported_resolutions: ['15'] as ResolutionString[], } diff --git a/src/components/Trade/TradeChart/index.tsx b/src/components/Trade/TradeChart/index.tsx index e52cc1ae..d0e575fd 100644 --- a/src/components/Trade/TradeChart/index.tsx +++ b/src/components/Trade/TradeChart/index.tsx @@ -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() as React.MutableRefObject + + 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 ( - <> -