From c1f27411254e4ac0a03f6918488783b7cba9997e Mon Sep 17 00:00:00 2001 From: Leandro Boscariol Date: Wed, 27 May 2020 12:18:46 -0700 Subject: [PATCH 1/4] Fixing token images on dark mode (#1061) Co-authored-by: Leandro Boscariol --- src/components/TokenImg.tsx | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/components/TokenImg.tsx b/src/components/TokenImg.tsx index a87f0fab1..6bd2ab63a 100644 --- a/src/components/TokenImg.tsx +++ b/src/components/TokenImg.tsx @@ -13,6 +13,8 @@ const TokenImg = styled.img.attrs(() => ({ onError: _loadFallbackTokenImage }))` border-radius: 3.6rem; object-fit: contain; margin: 0 1rem 0 0; + background-color: white; + padding: 2px; ` export default TokenImg From e02972168d15935fe8fb83ca1312815d3af081bf Mon Sep 17 00:00:00 2001 From: Anxo Rodriguez Date: Thu, 28 May 2020 15:36:22 +0200 Subject: [PATCH 2/4] Improve order book representation (#1062) * Improve order book representation * `View Order Book` displays the Order Book of the opposite pair Fixes #1056 * Be consistent with div function --- src/components/OrderBookBtn.tsx | 6 +- src/components/OrderBookWidget.tsx | 277 +++++++++++++++++---------- src/components/TradeWidget/Price.tsx | 2 +- 3 files changed, 184 insertions(+), 101 deletions(-) diff --git a/src/components/OrderBookBtn.tsx b/src/components/OrderBookBtn.tsx index 475bf04ca..6216ead6e 100644 --- a/src/components/OrderBookBtn.tsx +++ b/src/components/OrderBookBtn.tsx @@ -54,7 +54,7 @@ const ModalWrapper = styled(ModalBodyWrapper)` } > span:first-of-type::after { - content: '⟶'; + content: '/'; margin: 0 1rem; @media ${MEDIA.mobile} { @@ -137,7 +137,6 @@ export const OrderBookBtn: React.FC = (props: OrderBookBtnPro message: ( -

Bid

{' '} = (props: OrderBookBtnPro otherToken: baseToken, }) } - />{' '} -

Ask

+ />
diff --git a/src/components/OrderBookWidget.tsx b/src/components/OrderBookWidget.tsx index 31adf906c..c47db7f41 100644 --- a/src/components/OrderBookWidget.tsx +++ b/src/components/OrderBookWidget.tsx @@ -8,32 +8,15 @@ import am4themesSpiritedaway from '@amcharts/amcharts4/themes/spiritedaway' import { dexPriceEstimatorApi } from 'api' -import { getNetworkFromId, safeTokenName, formatSmart, toBN } from 'utils' +import { getNetworkFromId, safeTokenName, formatSmart, logDebug } from 'utils' -import { TEN_BIG_NUMBER } from 'const' +import { TEN_BIG_NUMBER, ZERO_BIG_NUMBER } from 'const' import { TokenDetails, Network } from 'types' +import BN from 'bn.js' +import { DEFAULT_PRECISION } from '@gnosis.pm/dex-js' -function checkPriceAndConvert(price: number): string { - // prices come in as type number so need to be converted - // some of the WEI values are so small we need to cast to Wei twice - const priceAsBigNumber = new BigNumber(price) - const calculatedPriceAsBnWei = toBN( - priceAsBigNumber - .times(TEN_BIG_NUMBER.pow(new BigNumber('18'))) - // we don't want any decimals - // before passing into BN conversion - .decimalPlaces(0) - .toString(10), - ) - - return formatSmart({ - amount: calculatedPriceAsBnWei, - precision: 18, - decimals: 6, - smallLimit: '0', - }) -} +const SMALL_VOLUME_THRESHOLD = 0.01 interface OrderBookProps { baseToken: TokenDetails @@ -84,17 +67,69 @@ enum Offer { Ask, } -interface RawItem { +/** + * Price point as defined in the API + * Both price and volume are numbers (floats) + * + * The price and volume are expressed in atoms + */ +interface RawPricePoint { price: number volume: number } -interface ProcessedItem { - volume: number - totalVolume: number +/** + * Normalized price point + * Both price and volume are BigNumbers (decimal) + * + * The price and volume are expressed in atoms + */ +interface PricePoint { + price: BigNumber + volume: BigNumber +} + +/** + * Price point data represented in the graph. Contains BigNumbers for operate with less errors and more precission + * but for representation uses number as expected by the library + */ +interface PricePointDetails { + // Basic data + type: Offer + volume: BigNumber // volume for the price point + totalVolume: BigNumber // cumulative volume + price: BigNumber + + // Data for representation + priceNumber: number + priceFormatted: string + totalVolumeNumber: number + totalVolumeFormatted: string askValueY: number | null bidValueY: number | null - price: string | number +} + +function _toPricePoint(pricePoint: RawPricePoint, quoteTokenDecimals: number, baseTokenDecimals: number): PricePoint { + return { + price: new BigNumber(pricePoint.price).div(TEN_BIG_NUMBER.pow(quoteTokenDecimals - baseTokenDecimals)), + volume: new BigNumber(pricePoint.volume).div(TEN_BIG_NUMBER.pow(baseTokenDecimals)), + } +} + +function _formatSmartBigNumber(amount: BigNumber): string { + return formatSmart({ + amount: new BN( + amount + .times(TEN_BIG_NUMBER.pow(new BigNumber(DEFAULT_PRECISION))) + // we don't want any decimals + // before passing into BN conversion + .decimalPlaces(0) + .toString(10), + ), + precision: DEFAULT_PRECISION, + decimals: 6, + smallLimit: '0', + }) } /** @@ -102,49 +137,91 @@ interface ProcessedItem { * This involves aggregating the total volume and accounting for decimals */ const processData = ( - list: RawItem[], + rawPricePoints: RawPricePoint[], baseToken: TokenDetails, quoteToken: TokenDetails, type: Offer, -): ProcessedItem[] => { - let totalVolume = 0 +): PricePointDetails[] => { const isBid = type == Offer.Bid - return ( - list - // Account fo decimals - .map(element => { - return { - price: element.price / 10 ** (quoteToken.decimals - baseToken.decimals), - volume: element.volume / 10 ** baseToken.decimals, - } - }) - // Filter tiny orders - .filter(e => e.volume > 0.01) - // Accumulate totalVolume - .map(e => { - const previousTotalVolume = totalVolume - totalVolume += e.volume - return { - price: e.price, - volume: e.volume, - totalVolume, - // Amcharts draws step lines so that the x value is centered (Default). To correctly display the orderbook, we want - // the x value to be at the left side of the step for asks and at the right side of the step for bids. - // - // Default Bids Asks - // | | | - // --------- --------- --------- - // | | | - // x x x - // - // For asks, we can offset the "startLocation" by 0.5. However, Amcharts does not support a "startLocation" of -0.5. - // For bids, we therefore offset the curve by -1 (expose the previous total volume) and use an offset of 0.5. - // Otherwise our steps would be off by one. - askValueY: isBid ? null : totalVolume, - bidValueY: isBid ? previousTotalVolume : null, - } - }) + const quoteTokenDecimals = quoteToken.decimals + const baseTokenDecimals = baseToken.decimals + + // Convert RawPricePoint into PricePoint: + // Raw items use number (floats) and are given in "atoms" + // Normalized items use decimals (BigNumber) and are given in natural units + let pricePoints: PricePoint[] = rawPricePoints.map(pricePoint => + _toPricePoint(pricePoint, quoteTokenDecimals, baseTokenDecimals), + ) + + // Filter tiny orders + pricePoints = pricePoints.filter(pricePoint => pricePoint.volume.gt(SMALL_VOLUME_THRESHOLD)) + + // Convert the price points that can be represented in the graph (PricePointDetails) + const { points } = pricePoints.reduce( + (acc, pricePoint, index) => { + const { price, volume } = pricePoint + const totalVolume = acc.totalVolume.plus(volume) + + // Amcharts draws step lines so that the x value is centered (Default). To correctly display the order book, we want + // the x value to be at the left side of the step for asks and at the right side of the step for bids. + // + // Default Bids Asks + // | | | + // --------- --------- --------- + // | | | + // x x x + // + // For asks, we can offset the "startLocation" by 0.5. However, Amcharts does not support a "startLocation" of -0.5. + // For bids, we therefore offset the curve by -1 (expose the previous total volume) and use an offset of 0.5. + // Otherwise our steps would be off by one. + let askValueY, bidValueY + if (isBid) { + const previousPricePoint = acc.points[index - 1] + askValueY = null + bidValueY = previousPricePoint?.totalVolume.toNumber() || 0 + } else { + askValueY = totalVolume.toNumber() + bidValueY = null + } + + // Add the new point + const pricePointDetails: PricePointDetails = { + type, + volume, + totalVolume, + price, + + // Data for representation + priceNumber: price.toNumber(), + totalVolumeNumber: totalVolume.toNumber(), + priceFormatted: _formatSmartBigNumber(price), + totalVolumeFormatted: _formatSmartBigNumber(totalVolume), + askValueY, + bidValueY, + } + acc.points.push(pricePointDetails) + + return { totalVolume, points: acc.points } + }, + { + totalVolume: ZERO_BIG_NUMBER, + points: [] as PricePointDetails[], + }, ) + + return points +} + +function _printOrderBook(pricePoints: PricePointDetails[], baseToken: TokenDetails, quoteToken: TokenDetails): void { + logDebug('Order Book: ' + baseToken.symbol + '-' + quoteToken.symbol) + pricePoints.forEach(pricePoint => { + const isBid = pricePoint.type === Offer.Bid + logDebug( + `\t${isBid ? 'Bid' : 'Ask'} ${pricePoint.totalVolumeFormatted} ${baseToken.symbol} at ${ + pricePoint.priceFormatted + } ${quoteToken.symbol}`, + ) + }) } const draw = ( @@ -155,6 +232,8 @@ const draw = ( hops?: number, ): am4charts.XYChart => { const baseTokenLabel = safeTokenName(baseToken) + const quoteTokenLabel = safeTokenName(quoteToken) + const market = baseTokenLabel + '-' + quoteTokenLabel am4core.useTheme(am4themesSpiritedaway) am4core.options.autoSetClassName = true const chart = am4core.create(chartElement, am4charts.XYChart) @@ -168,15 +247,21 @@ const draw = ( networkId, }) chart.dataSource.adapter.add('parsedData', data => { - const processed = processData(data.bids, baseToken, quoteToken, Offer.Bid).concat( - processData(data.asks, baseToken, quoteToken, Offer.Ask), - ) - processed - // cast as number to sort... - .sort((lhs, rhs) => +lhs.price - +rhs.price) - // show as string - .map(processedItem => ({ ...processedItem, price: checkPriceAndConvert(processedItem.price as number) })) - return processed + try { + const bids = processData(data.bids, baseToken, quoteToken, Offer.Bid) + const asks = processData(data.asks, baseToken, quoteToken, Offer.Ask) + const pricePoints = bids.concat(asks) + + // Sort points by price + pricePoints.sort((lhs, rhs) => lhs.price.comparedTo(rhs.price)) + + _printOrderBook(pricePoints, baseToken, quoteToken) + + return pricePoints + } catch (error) { + console.error('Error processing data', error) + return [] + } }) // Colors @@ -187,32 +272,32 @@ const draw = ( // Create axes const xAxis = chart.xAxes.push(new am4charts.CategoryAxis()) - xAxis.dataFields.category = 'price' - xAxis.title.text = `${networkDescription} Price (${baseToken.symbol}/${quoteToken.symbol})` + xAxis.dataFields.category = 'priceNumber' + xAxis.title.text = `${networkDescription} Price (${quoteTokenLabel})` const yAxis = chart.yAxes.push(new am4charts.ValueAxis()) - yAxis.title.text = 'Volume' + yAxis.title.text = baseTokenLabel // Create series - const bidCurve = chart.series.push(new am4charts.StepLineSeries()) - bidCurve.dataFields.categoryX = 'price' - bidCurve.dataFields.valueY = 'bidValueY' - bidCurve.strokeWidth = 1 - bidCurve.stroke = am4core.color(colors.green) - bidCurve.fill = bidCurve.stroke - bidCurve.startLocation = 0.5 - bidCurve.fillOpacity = 0.1 - bidCurve.tooltipText = `Bid: [bold]{categoryX}[/]\nVolume: [bold]{totalVolume} ${baseTokenLabel}[/]` - - const askCurve = chart.series.push(new am4charts.StepLineSeries()) - askCurve.dataFields.categoryX = 'price' - askCurve.dataFields.valueY = 'askValueY' - askCurve.strokeWidth = 1 - askCurve.stroke = am4core.color(colors.red) - askCurve.fill = askCurve.stroke - askCurve.fillOpacity = 0.1 - askCurve.startLocation = 0.5 - askCurve.tooltipText = `Ask: [bold]{categoryX}[/]\nVolume: [bold]{totalVolume} ${baseTokenLabel}[/]` + const bidSeries = chart.series.push(new am4charts.StepLineSeries()) + bidSeries.dataFields.categoryX = 'priceNumber' + bidSeries.dataFields.valueY = 'bidValueY' + bidSeries.strokeWidth = 1 + bidSeries.stroke = am4core.color(colors.green) + bidSeries.fill = bidSeries.stroke + bidSeries.startLocation = 0.5 + bidSeries.fillOpacity = 0.1 + bidSeries.tooltipText = `[bold]${market}[/]\nBid Price: [bold]{priceFormatted}[/] ${quoteTokenLabel}\nVolume: [bold]{totalVolumeFormatted}[/] ${baseTokenLabel}` + + const askSeries = chart.series.push(new am4charts.StepLineSeries()) + askSeries.dataFields.categoryX = 'priceNumber' + askSeries.dataFields.valueY = 'askValueY' + askSeries.strokeWidth = 1 + askSeries.stroke = am4core.color(colors.red) + askSeries.fill = askSeries.stroke + askSeries.fillOpacity = 0.1 + askSeries.startLocation = 0.5 + askSeries.tooltipText = `[bold]${market}[/]\nAsk Price: [bold]{priceFormatted}[/] ${quoteTokenLabel}\nVolume: [bold]{totalVolumeFormatted}[/] ${baseTokenLabel}` // Add cursor chart.cursor = new am4charts.XYCursor() diff --git a/src/components/TradeWidget/Price.tsx b/src/components/TradeWidget/Price.tsx index d00c4444d..dad9a4633 100644 --- a/src/components/TradeWidget/Price.tsx +++ b/src/components/TradeWidget/Price.tsx @@ -220,7 +220,7 @@ const Price: React.FC = ({ sellToken, receiveToken, priceInputId, priceIn return ( - Limit Price + Limit Price