From 4aec1bee675734407fe65ba5cd05bbaebd3ba3cb Mon Sep 17 00:00:00 2001 From: Bob van der Helm <34470358+bobthebuidlr@users.noreply.github.com> Date: Thu, 13 Jul 2023 09:02:38 +0200 Subject: [PATCH] WIP: Tradingview charts (#296) * almost finished custom styling for TV add basic chart intermittent update finished basic route pairing datafeed update datafeed fix relative import * finish shell script add shell script for charting_library * remove wrong line in shell * fixed pr comments * fixed pr comments * fix config for TV CHart * add example for thegraph api * update favorite assets * add + to boolean operation * remove usecallback --- .env.example | 1 + .eslintignore | 2 + .gitignore | 2 + .prettierignore | 2 + copy_charting_library_files.sh | 29 ++ public/tradingview.css | 151 ++++++++++ src/components/Card.tsx | 3 +- .../TradeChart/OsmosisTheGraphDataFeed.ts | 259 ++++++++++++++++++ .../Trade/TradeChart/TVChartContainer.tsx | 112 ++++++++ src/components/Trade/TradeChart/constants.ts | 49 ++++ src/components/Trade/TradeChart/index.tsx | 29 ++ .../TradeModule/AssetSelector/AssetItem.tsx | 24 +- .../Trade/TradeModule/AssetSelector/index.tsx | 63 +++-- src/components/Trade/TradeModule/index.tsx | 16 +- src/components/Trade/TradingView.tsx | 23 -- src/constants/assets.ts | 7 +- src/constants/env.ts | 2 + src/constants/localStore.ts | 2 +- src/hooks/useAssets.ts | 23 +- src/pages/TradePage.tsx | 18 +- 20 files changed, 736 insertions(+), 81 deletions(-) create mode 100644 .eslintignore create mode 100644 .prettierignore create mode 100644 copy_charting_library_files.sh create mode 100644 public/tradingview.css create mode 100644 src/components/Trade/TradeChart/OsmosisTheGraphDataFeed.ts create mode 100644 src/components/Trade/TradeChart/TVChartContainer.tsx create mode 100644 src/components/Trade/TradeChart/constants.ts create mode 100644 src/components/Trade/TradeChart/index.tsx delete mode 100644 src/components/Trade/TradingView.tsx diff --git a/.env.example b/.env.example index 9e8dfb09..f98dc3e7 100644 --- a/.env.example +++ b/.env.example @@ -15,6 +15,7 @@ NEXT_PUBLIC_ZAPPER=osmo1dz3ysw5sl0rvvnvatv7nu6vyam687tentfuxfa22sxqqafdcnkdqht3u NEXT_PUBLIC_SWAPPER=osmo1q3p82qtudu7f5edgvqyzf6hk8xanezlr0w7ntypnsea4jfpe37ps29eay3 NEXT_PUBLIC_PARAMS=osmo1xvg28lrr72662t9u0hntt76lyax9zvptdvdmff4k2q9dhjm8x6ws9zym4v NEXT_PUBLIC_API=http://localhost:3000/api +NEXT_PUBLIC_CANDLES_ENDPOINT="https://api.thegraph.com/subgraphs/name/{NAME}/{GRAPH_NAME}" # MAINNET # # NEXT_PUBLIC_NETWORK=mainnet diff --git a/.eslintignore b/.eslintignore new file mode 100644 index 00000000..ed5412c4 --- /dev/null +++ b/.eslintignore @@ -0,0 +1,2 @@ +src/utils/charting_library +src/utils/datafeeds \ No newline at end of file diff --git a/.gitignore b/.gitignore index 07829c5b..36c3731d 100644 --- a/.gitignore +++ b/.gitignore @@ -19,6 +19,8 @@ coverage-summary.json # misc .DS_Store *.pem +charting_library/ +datafeeds/ # debug npm-debug.log* diff --git a/.prettierignore b/.prettierignore new file mode 100644 index 00000000..ed5412c4 --- /dev/null +++ b/.prettierignore @@ -0,0 +1,2 @@ +src/utils/charting_library +src/utils/datafeeds \ No newline at end of file diff --git a/copy_charting_library_files.sh b/copy_charting_library_files.sh new file mode 100644 index 00000000..3b5866a3 --- /dev/null +++ b/copy_charting_library_files.sh @@ -0,0 +1,29 @@ +#!/bin/sh + +remove_if_directory_exists() { + if [ -d "$1" ]; then rm -Rf "$1"; fi +} + +BRANCH="master" + +REPOSITORY="https://$TV_USERNAME:$TV_ACCESS_TOKEN@github.com/tradingview/charting_library/" + +echo $REPOSITORY + +LATEST_HASH=$(git ls-remote $REPOSITORY $BRANCH | grep -Eo '^[[:alnum:]]+') + +remove_if_directory_exists "$LATEST_HASH" + +git clone -q --depth 1 -b "$BRANCH" $REPOSITORY "$LATEST_HASH" + +remove_if_directory_exists "public/static/charting_library" +remove_if_directory_exists "public/static/datafeeds" +remove_if_directory_exists "src/utils/charting_library" +remove_if_directory_exists "src/utils/datafeeds" + +cp -r "$LATEST_HASH/charting_library" public/ +cp -r "$LATEST_HASH/charting_library" src/utils/ +cp -r "$LATEST_HASH/datafeeds" public/ +cp -r "$LATEST_HASH/datafeeds" src/utils/ + +remove_if_directory_exists "$LATEST_HASH" \ No newline at end of file diff --git a/public/tradingview.css b/public/tradingview.css new file mode 100644 index 00000000..c07c29ef --- /dev/null +++ b/public/tradingview.css @@ -0,0 +1,151 @@ +:root { + --tv-background: #220e1d; + --tv-menu-background: #31142a !important; + --tv-menu-text: rgba(255, 255, 255, 0.3) !important; + --tv-menu-text-hover: rgba(255, 255, 255, 1) !important; +} + +.theme-dark:root { + --tv-color-pane-background: var(--tv-background); + --tv-color-platform-background: var(--tv-background); + --tv-color-toolbar-button-text: var(--tv-menu-text); + --tv-color-toolbar-button-text-hover: var(--tv-menu-text-hover); + --tv-color-toolbar-button-text-active: var(--tv-menu-text-hover); + --tv-color-toolbar-button-text-active-hover: var(--tv-menu-text-hover); + --tv-color-toolbar-button-background-hover: var(--tv-background); + --tv-color-toolbar-button-background-expanded: var(--tv-background); + --tv-color-toolbar-button-background-active: var(--tv-background); + --tv-color-toolbar-button-background-active-hover: var(--tv-background); + --tv-color-toolbar-toggle-button-background-active: rgba(255, 255, 255, 0.2); + --tv-color-toolbar-toggle-button-background-active-hover: rgba(255, 255, 255, 0.2); + --tv-color-toolbar-divider-background: var(--tv-menu-text); +} + +/* Favorited menus */ +.tv-floating-toolbar__widget-wrapper > div { + background: var(--tv-menu-background) !important; +} +.tv-floating-toolbar__widget:hover, +.tv-favorited-drawings-toolbar__widget:hover *, +.tv-favorited-drawings-toolbar__widget:hover:before { + background: var(--tv-menu-background) !important; + color: var(--tv-menu-text-hover) !important; + border-color: var(--tv-menu-background) !important; + cursor: pointer; +} + +/* Floating menu */ +.floating-toolbar-react-widgets__button:hover, +.button-reABrhVR:hover:before, +.button-uO7HM85b.isInteractive-uO7HM85b:hover:before { + background: var(--tv-menu-background) !important; + color: var(--tv-menu-text-hover) !important; + border-color: var(--tv-menu-background) !important; + cursor: pointer; +} + +.layout__area--left { + min-width: 10px !important; +} + +div[data-role='button']:hover, +/* Indiator dialog list items */ +div[data-role="dialog-content"] div[data-role="list-item"]:hover, +/* Series left sidebar */ +.active-a7Y2yl3G { + cursor: pointer; +} + +/* General pop-up menus */ +div[data-name='popup-menu-container'] div:not(.swatch-pNRFZrPx, .opacitySliderGradient-uujjxY8O), +/* Indicator dialog */ +div[data-name='indicators-dialog'] *, +/* Layers dialog */ +div[data-name="object-tree-dialog"] * , +/* Layers dialog */ +div[data-name="series-properties-dialog"], +/* Series left sidetabs */ +.tab-a7Y2yl3G:hover, +.active-a7Y2yl3G, +/* Checkbox */ +.check-bUw_gKIQ, +/* Close buton series popup */ +.close-HS2PTQRJ:hover { + background: var(--tv-menu-background) !important; + outline: white !important; +} + +/* General toolbar popup list items */ +.item-RhC5uhZw, +/* Emoji topbar items */ +.wrapper-rSoA6gh6 svg, +.wrapper-rSoA6gh6.categories-TlKkLixs, +/* Emoji items */ +.wrapper-yrezKVPX svg, +/* Indiator dialog list items */ +div[data-role="dialog-content"] div[data-role="list-item"] span, +/* Layers dialog list item */ +.wrap-G4AKrzja span, +/* Series proerties */ +div[data-name="series-properties-dialog"], +/* Series left sidetabs */ +.tab-a7Y2yl3G , +/* Close buton series popup */ +.close-HS2PTQRJ { + color: var(--tv-menu-text) !important; +} + +/* General toolbar popup list items */ +.item-RhC5uhZw:hover, +/* Sub headers for inteval popup */ +.section-_8r4li9v:hover, +/* Emoji picker categories */ +.wrapper-wawooJAf:hover, +/* Emoji topbar items */ +.isActive-rSoA6gh6, +.isActive-rSoA6gh6.categories-TlKkLixs, +.wrapper-rSoA6gh6:hover svg, +.wrapper-rSoA6gh6.categories-TlKkLixs:hover, +/* Emoji items */ +.isActive-yrezKVPX, +.wrapper-yrezKVPX:hover svg, +/* Indiator dialog list items */ +div[data-role="dialog-content"] div[data-role="list-item"]:hover span, +/* Layers dialog list item */ +.wrap-G4AKrzja:hover span, +/* Series left sidetabs */ +.tab-a7Y2yl3G:hover, +/* Series left sidetabs */ +.active-a7Y2yl3G, +/* Close buton series popup */ +.close-HS2PTQRJ:hover { + cursor: pointer !important; + color: var(--tv-menu-text-hover) !important; +} + +/* Top and bottom scroll indicator for toolbar */ +.scrollBot-g7ay5OPA, +.scrollTop-g7ay5OPA { + cursor: pointer; + background: var(--tv-menu-background) !important; +} + +/* Checkbox */ +.check-bUw_gKIQ { + border-color: var(--tv-menu-text) !important; +} + +/* Buttons for series popup */ +.variant-secondary-OvB35Th_, +.variant-primary-OvB35Th_ { + border-color: var(--tv-menu-text) !important; + color: var(--tv-menu-text) !important; + cursor: pointer; + background: none !important; +} +.variant-secondary-OvB35Th_:hover, +.variant-primary-OvB35Th_:hover { + border-color: var(--tv-menu-text-hover) !important; + color: var(--tv-menu-text-hover) !important; + background: none !important; +} diff --git a/src/components/Card.tsx b/src/components/Card.tsx index f20e80ec..feb79cbc 100644 --- a/src/components/Card.tsx +++ b/src/components/Card.tsx @@ -17,7 +17,8 @@ export default function Card(props: Props) { id={props.id} className={classNames( props.className, - 'relative isolate max-w-full rounded-base', + 'flex flex-col', + 'relative isolate max-w-full overflow-hidden rounded-base', 'before:content-[" "] before:absolute before:inset-0 before:-z-1 before:rounded-base before:p-[1px] before:border-glas', )} > diff --git a/src/components/Trade/TradeChart/OsmosisTheGraphDataFeed.ts b/src/components/Trade/TradeChart/OsmosisTheGraphDataFeed.ts new file mode 100644 index 00000000..964d00fe --- /dev/null +++ b/src/components/Trade/TradeChart/OsmosisTheGraphDataFeed.ts @@ -0,0 +1,259 @@ +import { + Bar, + ErrorCallback, + HistoryCallback, + IDatafeedChartApi, + LibrarySymbolInfo, + OnReadyCallback, + PeriodParams, + ResolutionString, + ResolveCallback, +} from 'utils/charting_library' +import { ENV } from 'constants/env' +import { getAssetByDenom, getEnabledMarketAssets } from 'utils/assets' +import { BN } from 'utils/helpers' +import { defaultSymbolInfo } from 'components/Trade/TradeChart/constants' + +interface BarQueryData { + close: string + high: string + low: string + open: string + timestamp: string + volume: string +} + +export const PAIR_SEPARATOR = '<>' + +export class OsmosisTheGraphDataFeed implements IDatafeedChartApi { + candlesEndpoint = ENV.CANDLES_ENDPOINT + debug = false + exchangeName = 'Osmosis' + baseDecimals: number = 6 + baseDenom: string = 'uosmo' + batchSize = 1000 + enabledMarketAssetDenoms: string[] = [] + pairs: string[] = [] + pairsWithData: string[] = [] + intervals: { [key: string]: string } = { + '15': '15m', + '30': '30m', + '60': '1h', + } + + supportedPools: string[] = [] + supportedResolutions = ['15', '30', '60'] as ResolutionString[] + + constructor(debug = false, baseDecimals: number, baseDenom: string) { + if (debug) console.log('Start TheGraph charting library datafeed') + this.debug = debug + this.baseDecimals = baseDecimals + this.baseDenom = baseDenom + const enabledMarketAssets = getEnabledMarketAssets() + this.enabledMarketAssetDenoms = enabledMarketAssets.map((asset) => asset.denom) + this.supportedPools = enabledMarketAssets + .map((asset) => asset.poolId?.toString()) + .filter((poolId) => typeof poolId === 'string') as string[] + this.getAllPairs() + } + + getAllPairs() { + const assets = getEnabledMarketAssets() + const pairs: Set = new Set() + assets.forEach((asset1) => { + assets.forEach((asset2) => { + if (asset1.symbol === asset2.symbol) return + pairs.add(`${asset1.denom}${PAIR_SEPARATOR}${asset2.denom}`) + }) + }) + this.pairs = Array.from(pairs) + } + + 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.candlesEndpoint, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ query }), + }) + .then((res) => res.json()) + .then((json) => { + this.pairsWithData = json.data.pairs.map( + (pair: { baseAsset: string; quoteAsset: string }) => { + return `${pair.baseAsset}${PAIR_SEPARATOR}${pair.quoteAsset}` + }, + ) + }) + .catch((err) => { + if (this.debug) console.error(err) + throw err + }) + } + + onReady(callback: OnReadyCallback) { + const configurationData = { + supported_resolutions: this.supportedResolutions, + } + + setTimeout(async () => { + await this.getPairsWithData() + callback(configurationData) + }) + } + + async resolveSymbol(pairName: string, onResolve: ResolveCallback, onError: ErrorCallback) { + setTimeout(() => + onResolve({ + ...defaultSymbolInfo, + currency_code: pairName.split(PAIR_SEPARATOR)[0], + original_currency_code: pairName.split(PAIR_SEPARATOR)[1], + full_name: pairName, + description: pairName, + ticker: pairName, + exchange: this.exchangeName, + listed_exchange: this.exchangeName, + supported_resolutions: this.supportedResolutions, + }), + ) + } + + async getBars( + symbolInfo: LibrarySymbolInfo, + resolution: ResolutionString, + periodParams: PeriodParams, + onResult: HistoryCallback, + ): Promise { + const interval = this.intervals[resolution] + + let pair1 = symbolInfo.full_name + let pair2: string = '' + + if (!this.pairsWithData.includes(pair1)) { + if (this.debug) console.log('Pair does not have data, need to combine with 2nd pair') + + const [buyAssetDenom, sellAssetDenom] = pair1.split(PAIR_SEPARATOR) + + pair1 = `${buyAssetDenom}${PAIR_SEPARATOR}${this.baseDenom}` + pair2 = `${this.baseDenom}${PAIR_SEPARATOR}${sellAssetDenom}` + } + + const pair1Bars = this.queryBarData( + pair1.split(PAIR_SEPARATOR)[0], + pair1.split(PAIR_SEPARATOR)[1], + interval, + ) + + let pair2Bars: Promise | null = null + + if (pair2) { + pair2Bars = this.queryBarData( + pair2.split(PAIR_SEPARATOR)[0], + pair2.split(PAIR_SEPARATOR)[1], + interval, + ) + } + + await Promise.all([pair1Bars, pair2Bars]).then(([pair1Bars, pair2Bars]) => { + let bars = pair1Bars + if (pair2Bars) { + bars = this.combineBars(pair1Bars, pair2Bars) + } + onResult(bars) + }) + } + + async queryBarData(quote: string, base: string, interval: string): Promise { + const query = ` + { + candles( + first: ${this.batchSize}, + where: { + interval: "${interval}", + quote: "${quote}", + base: "${base}" + }) { + timestamp + open + high + low + close + volume + } + } + ` + + return fetch(this.candlesEndpoint, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ query }), + }) + .then((res) => res.json()) + .then((json: { data: { candles: BarQueryData[] } }) => { + return this.resolveBarData(json.data.candles, base) + }) + .catch((err) => { + if (this.debug) console.error(err) + throw err + }) + } + + resolveBarData(bars: BarQueryData[], base: string) { + const assetDecimals = getAssetByDenom(base)?.decimals || 6 + const additionalDecimals = assetDecimals - this.baseDecimals + + return 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(), + })) + } + + combineBars(pair1Bars: Bar[], pair2Bars: Bar[]): Bar[] { + const bars: Bar[] = [] + + pair1Bars.forEach((pair1Bar) => { + const pair2Bar = pair2Bars.find((pair2Bar) => pair2Bar.time == pair1Bar.time) + + if (pair2Bar) { + bars.push({ + time: pair1Bar.time, + open: pair1Bar.open * pair2Bar.open, + close: pair1Bar.close * pair2Bar.close, + high: pair1Bar.high * pair2Bar.high, + low: pair1Bar.low * pair2Bar.low, + }) + } + }) + return bars + } + + 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 new file mode 100644 index 00000000..dc56761c --- /dev/null +++ b/src/components/Trade/TradeChart/TVChartContainer.tsx @@ -0,0 +1,112 @@ +import { useEffect, useMemo, useRef } from 'react' + +import { + ChartingLibraryWidgetOptions, + IChartingLibraryWidget, + ResolutionString, + Timezone, + widget, +} from 'utils/charting_library' +import Card from 'components/Card' +import { + OsmosisTheGraphDataFeed, + PAIR_SEPARATOR, +} from 'components/Trade/TradeChart/OsmosisTheGraphDataFeed' +import useStore from 'store' +import { disabledFeatures, enabledFeatures, overrides } from 'components/Trade/TradeChart/constants' + +interface Props { + buyAsset: Asset + sellAsset: Asset +} + +export const TVChartContainer = (props: Props) => { + const chartContainerRef = useRef() as React.MutableRefObject + const widgetRef = useRef() + const defaultSymbol = useRef( + `${props.buyAsset.denom}${PAIR_SEPARATOR}${props.sellAsset.denom}`, + ) + const baseCurrency = useStore((s) => s.baseCurrency) + const dataFeed = useMemo( + () => new OsmosisTheGraphDataFeed(false, baseCurrency.decimals, baseCurrency.denom), + [baseCurrency], + ) + + 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.3)', + 'paneProperties.legendProperties.showSeriesTitle': false, + 'paneProperties.legendProperties.showVolume': false, + 'paneProperties.legendProperties.showStudyValues': false, + 'paneProperties.legendProperties.showStudyTitles': false, + 'scalesProperties.axisHighlightColor': '#381730', + 'linetooltrendline.color': 'rgba( 21, 153, 128, 1)', + '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 ( + +
+ + ) +} diff --git a/src/components/Trade/TradeChart/constants.ts b/src/components/Trade/TradeChart/constants.ts new file mode 100644 index 00000000..5e50e534 --- /dev/null +++ b/src/components/Trade/TradeChart/constants.ts @@ -0,0 +1,49 @@ +import { + ChartingLibraryFeatureset, + LibrarySymbolInfo, + ResolutionString, + SeriesFormat, + Timezone, +} from 'utils/charting_library/charting_library' + +export const disabledFeatures: ChartingLibraryFeatureset[] = [ + 'timeframes_toolbar', + 'go_to_date', + 'header_compare', + 'header_saveload', + 'popup_hints', + 'header_symbol_search', + 'symbol_info', +] + +export const enabledFeatures: ChartingLibraryFeatureset[] = [ + 'timezone_menu', + 'header_settings', + 'use_localstorage_for_settings', +] + +export const overrides = { + 'linetooltrendline.linecolor': 'rgba(255, 255, 255, 0.8)', + 'linetooltrendline.linewidth': 2, +} + +export const defaultSymbolInfo: LibrarySymbolInfo = { + currency_code: '', + original_currency_code: '', + full_name: '', + description: '', + ticker: '', + name: 'Osmosis', + exchange: 'Osmosis', + listed_exchange: 'Osmosis', + type: 'AMM', + session: '24x7', + minmov: 1, + pricescale: 100000, + timezone: 'Etc/UTC' as Timezone, + has_intraday: true, + has_daily: true, + has_weekly_and_monthly: true, + 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 new file mode 100644 index 00000000..956b7ff1 --- /dev/null +++ b/src/components/Trade/TradeChart/index.tsx @@ -0,0 +1,29 @@ +import dynamic from 'next/dynamic' +import Script from 'next/script' +import { useState } from 'react' + +const TVChartContainer = dynamic( + () => import('components/Trade/TradeChart/TVChartContainer').then((mod) => mod.TVChartContainer), + { ssr: false }, +) + +interface Props { + buyAsset: Asset + sellAsset: Asset +} + +export default function TradeChart(props: Props) { + const [isScriptReady, setIsScriptReady] = useState(true) + return ( + <> +