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:
Bob van der Helm 2023-07-13 09:02:38 +02:00 committed by GitHub
parent 380bfb7189
commit 4aec1bee67
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
20 changed files with 736 additions and 81 deletions

View File

@ -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
View File

@ -0,0 +1,2 @@
src/utils/charting_library
src/utils/datafeeds

2
.gitignore vendored
View File

@ -19,6 +19,8 @@ coverage-summary.json
# misc
.DS_Store
*.pem
charting_library/
datafeeds/
# debug
npm-debug.log*

2
.prettierignore Normal file
View File

@ -0,0 +1,2 @@
src/utils/charting_library
src/utils/datafeeds

View 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
View 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;
}

View File

@ -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',
)}
>

View 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
}
}

View 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>
)
}

View 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[],
}

View 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} />}
</>
)
}

View File

@ -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

View File

@ -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}
/>

View File

@ -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}

View File

@ -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>
)
}

View File

@ -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',

View File

@ -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 || '',

View File

@ -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'

View File

@ -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()

View File

@ -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>
)