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
This commit is contained in:
parent
380bfb7189
commit
4aec1bee67
@ -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
|
||||
|
2
.eslintignore
Normal file
2
.eslintignore
Normal file
@ -0,0 +1,2 @@
|
||||
src/utils/charting_library
|
||||
src/utils/datafeeds
|
2
.gitignore
vendored
2
.gitignore
vendored
@ -19,6 +19,8 @@ coverage-summary.json
|
||||
# misc
|
||||
.DS_Store
|
||||
*.pem
|
||||
charting_library/
|
||||
datafeeds/
|
||||
|
||||
# debug
|
||||
npm-debug.log*
|
||||
|
2
.prettierignore
Normal file
2
.prettierignore
Normal file
@ -0,0 +1,2 @@
|
||||
src/utils/charting_library
|
||||
src/utils/datafeeds
|
29
copy_charting_library_files.sh
Normal file
29
copy_charting_library_files.sh
Normal file
@ -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"
|
151
public/tradingview.css
Normal file
151
public/tradingview.css
Normal file
@ -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;
|
||||
}
|
@ -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',
|
||||
)}
|
||||
>
|
||||
|
259
src/components/Trade/TradeChart/OsmosisTheGraphDataFeed.ts
Normal file
259
src/components/Trade/TradeChart/OsmosisTheGraphDataFeed.ts
Normal file
@ -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<string> = 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<void> {
|
||||
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<Bar[]> | 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<Bar[]> {
|
||||
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
|
||||
}
|
||||
}
|
112
src/components/Trade/TradeChart/TVChartContainer.tsx
Normal file
112
src/components/Trade/TradeChart/TVChartContainer.tsx
Normal file
@ -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<HTMLDivElement>() as React.MutableRefObject<HTMLInputElement>
|
||||
const widgetRef = useRef<IChartingLibraryWidget>()
|
||||
const defaultSymbol = useRef<string>(
|
||||
`${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 (
|
||||
<Card title='Trading Chart' contentClassName='px-0.5 pb-0.5 h-full '>
|
||||
<div ref={chartContainerRef} className='h-full' />
|
||||
</Card>
|
||||
)
|
||||
}
|
49
src/components/Trade/TradeChart/constants.ts
Normal file
49
src/components/Trade/TradeChart/constants.ts
Normal file
@ -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[],
|
||||
}
|
29
src/components/Trade/TradeChart/index.tsx
Normal file
29
src/components/Trade/TradeChart/index.tsx
Normal file
@ -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 (
|
||||
<>
|
||||
<Script
|
||||
src='/datafeeds/udf/dist/bundle.js'
|
||||
strategy='lazyOnload'
|
||||
onReady={() => {
|
||||
setIsScriptReady(true)
|
||||
}}
|
||||
/>
|
||||
{isScriptReady && <TVChartContainer buyAsset={props.buyAsset} sellAsset={props.sellAsset} />}
|
||||
</>
|
||||
)
|
||||
}
|
@ -2,7 +2,8 @@ import AssetImage from 'components/AssetImage'
|
||||
import DisplayCurrency from 'components/DisplayCurrency'
|
||||
import { StarFilled, StarOutlined } from 'components/Icons'
|
||||
import Text from 'components/Text'
|
||||
import { FAVORITE_ASSETS } from 'constants/localStore'
|
||||
import { FAVORITE_ASSETS_KEY } from 'constants/localStore'
|
||||
import useLocalStorage from 'hooks/useLocalStorage'
|
||||
import { BNCoin } from 'types/classes/BNCoin'
|
||||
import { BN } from 'utils/helpers'
|
||||
|
||||
@ -13,22 +14,21 @@ interface Props {
|
||||
|
||||
export default function AssetItem(props: Props) {
|
||||
const asset = props.asset
|
||||
const [favoriteAssetsDenoms, setFavoriteAssetsDenoms] = useLocalStorage<string[]>(
|
||||
FAVORITE_ASSETS_KEY,
|
||||
[],
|
||||
)
|
||||
|
||||
function handleToggleFavorite(event: React.MouseEvent<HTMLDivElement, MouseEvent>) {
|
||||
event.stopPropagation()
|
||||
const favoriteAssets: string[] = JSON.parse(localStorage.getItem(FAVORITE_ASSETS) || '[]')
|
||||
if (favoriteAssets) {
|
||||
if (favoriteAssets.includes(asset.denom)) {
|
||||
localStorage.setItem(
|
||||
FAVORITE_ASSETS,
|
||||
JSON.stringify(favoriteAssets.filter((item: string) => item !== asset.denom)),
|
||||
)
|
||||
} else {
|
||||
localStorage.setItem(FAVORITE_ASSETS, JSON.stringify([...favoriteAssets, asset.denom]))
|
||||
}
|
||||
window.dispatchEvent(new Event('storage'))
|
||||
|
||||
if (!favoriteAssetsDenoms.includes(asset.denom)) {
|
||||
setFavoriteAssetsDenoms([...favoriteAssetsDenoms, asset.denom])
|
||||
return
|
||||
}
|
||||
setFavoriteAssetsDenoms(favoriteAssetsDenoms.filter((item: string) => item !== asset.denom))
|
||||
}
|
||||
|
||||
return (
|
||||
<li className='border-b border-white/10 hover:bg-black/10'>
|
||||
<button
|
||||
|
@ -1,36 +1,38 @@
|
||||
import { useCallback, useState } from 'react'
|
||||
import { useCallback, useEffect, useState } from 'react'
|
||||
|
||||
import { SwapIcon } from 'components/Icons'
|
||||
import Text from 'components/Text'
|
||||
import AssetButton from 'components/Trade/TradeModule/AssetSelector/AssetButton'
|
||||
import AssetOverlay, { OverlayState } from 'components/Trade/TradeModule/AssetSelector/AssetOverlay'
|
||||
import { ASSETS } from 'constants/assets'
|
||||
|
||||
export default function AssetSelector() {
|
||||
interface Props {
|
||||
buyAsset: Asset
|
||||
sellAsset: Asset
|
||||
onChangeBuyAsset: (asset: Asset) => void
|
||||
onChangeSellAsset: (asset: Asset) => void
|
||||
}
|
||||
|
||||
export default function AssetSelector(props: Props) {
|
||||
const { buyAsset, sellAsset, onChangeBuyAsset, onChangeSellAsset } = props
|
||||
const [overlayState, setOverlayState] = useState<OverlayState>('closed')
|
||||
const [buyAsset, setBuyAsset] = useState(ASSETS[0])
|
||||
const [sellAsset, setSellAsset] = useState(ASSETS[1])
|
||||
const [cachedBuyAsset, setCachedBuyAsset] = useState<Asset>(props.buyAsset)
|
||||
const [cachedSellAsset, setCachedSellAsset] = useState<Asset>(props.sellAsset)
|
||||
|
||||
function handleSwapAssets() {
|
||||
setBuyAsset(sellAsset)
|
||||
setSellAsset(buyAsset)
|
||||
}
|
||||
const handleSwapAssets = useCallback(() => {
|
||||
onChangeBuyAsset(sellAsset)
|
||||
onChangeSellAsset(buyAsset)
|
||||
}, [onChangeBuyAsset, onChangeSellAsset, sellAsset, buyAsset])
|
||||
|
||||
const handleChangeBuyAsset = useCallback(
|
||||
(asset: Asset) => {
|
||||
setBuyAsset(asset)
|
||||
setOverlayState('sell')
|
||||
},
|
||||
[setBuyAsset],
|
||||
)
|
||||
const handleChangeBuyAsset = useCallback((asset: Asset) => {
|
||||
setCachedBuyAsset(asset)
|
||||
setOverlayState('sell')
|
||||
}, [])
|
||||
|
||||
const handleChangeSellAsset = useCallback((asset: Asset) => {
|
||||
setCachedSellAsset(asset)
|
||||
setOverlayState('closed')
|
||||
}, [])
|
||||
|
||||
const handleChangeSellAsset = useCallback(
|
||||
(asset: Asset) => {
|
||||
setSellAsset(asset)
|
||||
setOverlayState('closed')
|
||||
},
|
||||
[setSellAsset],
|
||||
)
|
||||
const handleChangeState = useCallback(
|
||||
(state: OverlayState) => {
|
||||
setOverlayState(state)
|
||||
@ -38,22 +40,29 @@ export default function AssetSelector() {
|
||||
[setOverlayState],
|
||||
)
|
||||
|
||||
useEffect(() => {
|
||||
if (overlayState === 'closed') {
|
||||
onChangeBuyAsset(cachedBuyAsset)
|
||||
onChangeSellAsset(cachedSellAsset)
|
||||
}
|
||||
}, [onChangeBuyAsset, onChangeSellAsset, overlayState, cachedBuyAsset, cachedSellAsset])
|
||||
|
||||
return (
|
||||
<div className='grid-rows-auto relative grid grid-cols-[1fr_min-content_1fr] gap-y-2 bg-white/5 p-3'>
|
||||
<Text size='sm'>Buy</Text>
|
||||
<Text size='sm' className='col-start-3'>
|
||||
Sell
|
||||
</Text>
|
||||
<AssetButton onClick={() => setOverlayState('buy')} asset={buyAsset} />
|
||||
<AssetButton onClick={() => setOverlayState('buy')} asset={props.buyAsset} />
|
||||
<button onClick={handleSwapAssets}>
|
||||
<SwapIcon className='mx-2 w-4 place-self-center' />
|
||||
</button>
|
||||
<AssetButton onClick={() => setOverlayState('sell')} asset={sellAsset} />
|
||||
<AssetButton onClick={() => setOverlayState('sell')} asset={props.sellAsset} />
|
||||
<AssetOverlay
|
||||
state={overlayState}
|
||||
onChangeState={handleChangeState}
|
||||
buyAsset={buyAsset}
|
||||
sellAsset={sellAsset}
|
||||
buyAsset={cachedBuyAsset}
|
||||
sellAsset={cachedSellAsset}
|
||||
onChangeBuyAsset={handleChangeBuyAsset}
|
||||
onChangeSellAsset={handleChangeSellAsset}
|
||||
/>
|
||||
|
@ -5,7 +5,14 @@ import Divider from 'components/Divider'
|
||||
import RangeInput from 'components/RangeInput'
|
||||
import AssetSelector from 'components/Trade/TradeModule/AssetSelector'
|
||||
|
||||
export default function TradeModule() {
|
||||
interface Props {
|
||||
buyAsset: Asset
|
||||
sellAsset: Asset
|
||||
onChangeBuyAsset: (asset: Asset) => void
|
||||
onChangeSellAsset: (asset: Asset) => void
|
||||
}
|
||||
|
||||
export default function TradeModule(props: Props) {
|
||||
const [value, setValue] = useState(0)
|
||||
|
||||
return (
|
||||
@ -16,7 +23,12 @@ export default function TradeModule() {
|
||||
'row-span-2 h-full',
|
||||
)}
|
||||
>
|
||||
<AssetSelector />
|
||||
<AssetSelector
|
||||
buyAsset={props.buyAsset}
|
||||
sellAsset={props.sellAsset}
|
||||
onChangeBuyAsset={props.onChangeBuyAsset}
|
||||
onChangeSellAsset={props.onChangeSellAsset}
|
||||
/>
|
||||
<Divider />
|
||||
<RangeInput
|
||||
max={4000}
|
||||
|
@ -1,23 +0,0 @@
|
||||
import { Suspense } from 'react'
|
||||
|
||||
import Card from 'components/Card'
|
||||
import Loading from 'components/Loading'
|
||||
import Text from 'components/Text'
|
||||
|
||||
function Content() {
|
||||
return <Text size='sm'>Chart view</Text>
|
||||
}
|
||||
|
||||
function Fallback() {
|
||||
return <Loading className='h-4 w-50' />
|
||||
}
|
||||
|
||||
export default function TradingView() {
|
||||
return (
|
||||
<Card className='h-full bg-white/5' title='Trading View' contentClassName='px-4 py-6'>
|
||||
<Suspense fallback={<Fallback />}>
|
||||
<Content />
|
||||
</Suspense>
|
||||
</Card>
|
||||
)
|
||||
}
|
@ -30,6 +30,7 @@ export const ASSETS: Asset[] = [
|
||||
isMarket: true,
|
||||
isDisplayCurrency: true,
|
||||
isAutoLendEnabled: true,
|
||||
poolId: 1,
|
||||
},
|
||||
{
|
||||
symbol: 'stATOM',
|
||||
@ -43,6 +44,7 @@ export const ASSETS: Asset[] = [
|
||||
isEnabled: !IS_TESTNET,
|
||||
isMarket: !IS_TESTNET,
|
||||
isDisplayCurrency: !IS_TESTNET,
|
||||
poolId: 803,
|
||||
},
|
||||
{
|
||||
symbol: 'WBTC.axl',
|
||||
@ -56,6 +58,7 @@ export const ASSETS: Asset[] = [
|
||||
isEnabled: !IS_TESTNET,
|
||||
isMarket: !IS_TESTNET,
|
||||
isDisplayCurrency: !IS_TESTNET,
|
||||
poolId: 712,
|
||||
},
|
||||
{
|
||||
symbol: 'WETH.axl',
|
||||
@ -69,6 +72,7 @@ export const ASSETS: Asset[] = [
|
||||
isEnabled: !IS_TESTNET,
|
||||
isMarket: !IS_TESTNET,
|
||||
isDisplayCurrency: !IS_TESTNET,
|
||||
poolId: 704,
|
||||
},
|
||||
{
|
||||
symbol: 'MARS',
|
||||
@ -80,10 +84,10 @@ export const ASSETS: Asset[] = [
|
||||
color: '#dd5b65',
|
||||
logo: '/tokens/mars.svg',
|
||||
decimals: 6,
|
||||
poolId: IS_TESTNET ? 768 : 907,
|
||||
hasOraclePrice: true,
|
||||
isMarket: false,
|
||||
isEnabled: true,
|
||||
poolId: 907,
|
||||
},
|
||||
{
|
||||
symbol: 'USDC.axl',
|
||||
@ -100,6 +104,7 @@ export const ASSETS: Asset[] = [
|
||||
isMarket: true,
|
||||
isDisplayCurrency: true,
|
||||
isStable: true,
|
||||
poolId: 678,
|
||||
},
|
||||
{
|
||||
symbol: 'USDC.n',
|
||||
|
@ -7,6 +7,7 @@ interface EnvironmentVariables {
|
||||
ADDRESS_RED_BANK: string
|
||||
ADDRESS_SWAPPER: string
|
||||
ADDRESS_ZAPPER: string
|
||||
CANDLES_ENDPOINT: string
|
||||
CHAIN_ID: string
|
||||
NETWORK: string
|
||||
URL_GQL: string
|
||||
@ -26,6 +27,7 @@ export const ENV: EnvironmentVariables = {
|
||||
ADDRESS_RED_BANK: process.env.NEXT_PUBLIC_RED_BANK || '',
|
||||
ADDRESS_SWAPPER: process.env.NEXT_PUBLIC_SWAPPER || '',
|
||||
ADDRESS_ZAPPER: process.env.NEXT_PUBLIC_ZAPPER || '',
|
||||
CANDLES_ENDPOINT: process.env.NEXT_PUBLIC_CANDLES_ENDPOINT || '',
|
||||
CHAIN_ID: process.env.NEXT_PUBLIC_CHAIN_ID || '',
|
||||
NETWORK: process.env.NEXT_PUBLIC_NETWORK || '',
|
||||
URL_GQL: process.env.NEXT_PUBLIC_GQL || '',
|
||||
|
@ -1,7 +1,7 @@
|
||||
export const PREFERRED_ASSET_KEY = 'favouriteAsset'
|
||||
export const DISPLAY_CURRENCY_KEY = 'displayCurrency'
|
||||
export const REDUCE_MOTION_KEY = 'reduceMotion'
|
||||
export const FAVORITE_ASSETS = 'favoriteAssets'
|
||||
export const FAVORITE_ASSETS_KEY = 'favoriteAssets'
|
||||
export const LEND_ASSETS_KEY = 'lendAssets'
|
||||
export const AUTO_LEND_ENABLED_ACCOUNT_IDS_KEY = 'autoLendEnabledAccountIds'
|
||||
export const SLIPPAGE_KEY = 'slippage'
|
||||
|
@ -1,24 +1,25 @@
|
||||
import { useCallback, useEffect, useState } from 'react'
|
||||
|
||||
import { ASSETS } from 'constants/assets'
|
||||
import { FAVORITE_ASSETS } from 'constants/localStore'
|
||||
import { FAVORITE_ASSETS_KEY } from 'constants/localStore'
|
||||
import { getEnabledMarketAssets } from 'utils/assets'
|
||||
|
||||
import useLocalStorage from './useLocalStorage'
|
||||
|
||||
export default function useAssets() {
|
||||
const [assets, setAssets] = useState<Asset[]>(ASSETS)
|
||||
const [favoriteAssetsDenoms] = useLocalStorage<string[]>(FAVORITE_ASSETS_KEY, [])
|
||||
|
||||
const getFavoriteAssets = useCallback(() => {
|
||||
const favoriteAssets = JSON.parse(localStorage.getItem(FAVORITE_ASSETS) || '[]')
|
||||
const assets = ASSETS.map((asset) => ({
|
||||
...asset,
|
||||
isFavorite: favoriteAssets.includes(asset.denom),
|
||||
})).sort((a, b) => {
|
||||
if (a.isFavorite && !b.isFavorite) return -1
|
||||
if (!a.isFavorite && b.isFavorite) return 1
|
||||
return 0
|
||||
})
|
||||
const assets = getEnabledMarketAssets()
|
||||
.map((asset) => ({
|
||||
...asset,
|
||||
isFavorite: favoriteAssetsDenoms.includes(asset.denom),
|
||||
}))
|
||||
.sort((a, b) => +b.isFavorite - +a.isFavorite)
|
||||
|
||||
setAssets(assets)
|
||||
}, [])
|
||||
}, [favoriteAssetsDenoms])
|
||||
|
||||
useEffect(() => {
|
||||
getFavoriteAssets()
|
||||
|
@ -1,12 +1,24 @@
|
||||
import { useState } from 'react'
|
||||
|
||||
import OrderBook from 'components/Trade/OrderBook'
|
||||
import TradeChart from 'components/Trade/TradeChart'
|
||||
import TradeModule from 'components/Trade/TradeModule'
|
||||
import TradingView from 'components/Trade/TradingView'
|
||||
import { getEnabledMarketAssets } from 'utils/assets'
|
||||
|
||||
export default function TradePage() {
|
||||
const enabledMarketAssets = getEnabledMarketAssets()
|
||||
const [buyAsset, setBuyAsset] = useState(enabledMarketAssets[0])
|
||||
const [sellAsset, setSellAsset] = useState(enabledMarketAssets[1])
|
||||
|
||||
return (
|
||||
<div className='grid h-full w-full grid-cols-[346px_auto] gap-4'>
|
||||
<TradeModule />
|
||||
<TradingView />
|
||||
<TradeModule
|
||||
buyAsset={buyAsset}
|
||||
sellAsset={sellAsset}
|
||||
onChangeBuyAsset={setBuyAsset}
|
||||
onChangeSellAsset={setSellAsset}
|
||||
/>
|
||||
<TradeChart buyAsset={buyAsset} sellAsset={sellAsset} />
|
||||
<OrderBook />
|
||||
</div>
|
||||
)
|
||||
|
Loading…
Reference in New Issue
Block a user