From e5f929085c21850c303aa4b75a04e113fdfc9e33 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sehyun=20Chung=20=E2=9C=8C=EF=B8=8E?= <41171808+sehyunc@users.noreply.github.com> Date: Wed, 27 Mar 2024 13:24:09 -0700 Subject: [PATCH] trade: integrate price reporter --- .../app/(desktop)/[base]/[quote]/page.tsx | 30 ++--- .../app/(desktop)/[base]/[quote]/trading.tsx | 4 +- .../app/(desktop)/[base]/page.tsx | 28 ++-- trade.renegade.fi/app/(desktop)/layout.tsx | 1 + trade.renegade.fi/app/(mobile)/m/body.tsx | 4 +- .../app/(mobile)/m/tokens-banner.tsx | 10 +- trade.renegade.fi/app/providers.tsx | 11 +- .../components/banners/median-banner.tsx | 119 +++++++++------- .../components/banners/tokens-banner.tsx | 12 +- trade.renegade.fi/components/live-price.tsx | 88 ++++-------- .../components/panels/wallets-panel.tsx | 4 +- .../components/place-order-button.tsx | 29 ++-- .../contexts/Exchange/exchange-context.tsx | 9 +- trade.renegade.fi/contexts/Exchange/types.ts | 13 +- .../contexts/PriceContext/price-context.tsx | 127 ++++++++++++++++++ trade.renegade.fi/env.mjs | 14 +- trade.renegade.fi/hooks/use-price.ts | 27 ++++ trade.renegade.fi/hooks/use-usd-price.ts | 53 ++------ trade.renegade.fi/lib/utils.ts | 35 ++++- trade.renegade.fi/package.json | 2 +- trade.renegade.fi/yarn.lock | 8 +- 21 files changed, 383 insertions(+), 245 deletions(-) create mode 100644 trade.renegade.fi/contexts/PriceContext/price-context.tsx create mode 100644 trade.renegade.fi/hooks/use-price.ts diff --git a/trade.renegade.fi/app/(desktop)/[base]/[quote]/page.tsx b/trade.renegade.fi/app/(desktop)/[base]/[quote]/page.tsx index 508d0a88..7de897b2 100644 --- a/trade.renegade.fi/app/(desktop)/[base]/[quote]/page.tsx +++ b/trade.renegade.fi/app/(desktop)/[base]/[quote]/page.tsx @@ -1,4 +1,3 @@ -import { Renegade } from "@renegade-fi/renegade-js" import Image from "next/image" import { Main } from "@/app/(desktop)/[base]/[quote]/main" @@ -6,9 +5,7 @@ import { MedianBanner } from "@/components/banners/median-banner" import { RelayerStatusData } from "@/components/banners/relayer-status-data" import { OrdersAndCounterpartiesPanel } from "@/components/panels/orders-panel" import { WalletsPanel } from "@/components/panels/wallets-panel" -import { env } from "@/env.mjs" import backgroundPattern from "@/icons/background_pattern.png" -import { getToken } from "@/lib/utils" // export function generateStaticParams() { // return DISPLAYED_TICKERS.filter(([base]) => base !== "USDC").map( @@ -21,24 +18,25 @@ import { getToken } from "@/lib/utils" // ) // } -const renegade = new Renegade({ - relayerHostname: env.NEXT_PUBLIC_RENEGADE_RELAYER_HOSTNAME, - relayerHttpPort: 3000, - relayerWsPort: 4000, - useInsecureTransport: - env.NEXT_PUBLIC_RENEGADE_RELAYER_HOSTNAME === "localhost", - verbose: false, -}) +// const renegade = new Renegade({ +// relayerHostname: env.NEXT_PUBLIC_RENEGADE_RELAYER_HOSTNAME, +// relayerHttpPort: 3000, +// relayerWsPort: 4000, +// useInsecureTransport: +// env.NEXT_PUBLIC_RENEGADE_RELAYER_HOSTNAME === "localhost", +// verbose: false, +// }) export default async function Page({ params: { base, quote }, }: { params: { base: string; quote: string } }) { - const report = await renegade.queryExchangeHealthStates( - getToken({ input: base }), - getToken({ input: quote }) - ) + // const report = await renegade.queryPriceReporter( + // getToken({ input: base }), + // getToken({ input: quote }) + // ) + // console.log("🚀 ~ report:", report) return (
diff --git a/trade.renegade.fi/app/(desktop)/[base]/[quote]/trading.tsx b/trade.renegade.fi/app/(desktop)/[base]/[quote]/trading.tsx index efe38b47..b41660ce 100644 --- a/trade.renegade.fi/app/(desktop)/[base]/[quote]/trading.tsx +++ b/trade.renegade.fi/app/(desktop)/[base]/[quote]/trading.tsx @@ -10,6 +10,7 @@ import { PlaceOrderButton } from "@/components/place-order-button" import { OrderProvider, useOrder } from "@/contexts/Order/order-context" import { Direction } from "@/contexts/Order/types" import { useUSDPrice } from "@/hooks/use-usd-price" +import { formatPrice } from "@/lib/utils" interface SelectableProps { text: string @@ -181,6 +182,7 @@ function TradingInner() { function HelperText({ baseTicker }: { baseTicker: string }) { const usdPrice = useUSDPrice(baseTicker, 1) + const formattedUsdPrice = formatPrice(usdPrice) return ( Trades are end-to-end encrypted and always clear at the real-time - midpoint of ${usdPrice} + midpoint of ${formattedUsdPrice} ) diff --git a/trade.renegade.fi/app/(desktop)/[base]/page.tsx b/trade.renegade.fi/app/(desktop)/[base]/page.tsx index b57a0cd5..5cbacb6d 100644 --- a/trade.renegade.fi/app/(desktop)/[base]/page.tsx +++ b/trade.renegade.fi/app/(desktop)/[base]/page.tsx @@ -1,4 +1,3 @@ -import { Renegade, Token } from "@renegade-fi/renegade-js" import Image from "next/image" import { DepositBody } from "@/app/(desktop)/[base]/[quote]/deposit" @@ -6,7 +5,6 @@ import { MedianBanner } from "@/components/banners/median-banner" import { RelayerStatusData } from "@/components/banners/relayer-status-data" import { OrdersAndCounterpartiesPanel } from "@/components/panels/orders-panel" import { WalletsPanel } from "@/components/panels/wallets-panel" -import { env } from "@/env.mjs" import backgroundPattern from "@/icons/background_pattern.png" // export function generateStaticParams() { @@ -18,24 +16,24 @@ import backgroundPattern from "@/icons/background_pattern.png" // }) // } -const renegade = new Renegade({ - relayerHostname: env.NEXT_PUBLIC_RENEGADE_RELAYER_HOSTNAME, - relayerHttpPort: 3000, - relayerWsPort: 4000, - useInsecureTransport: - env.NEXT_PUBLIC_RENEGADE_RELAYER_HOSTNAME === "localhost", - verbose: false, -}) +// const renegade = new Renegade({ +// relayerHostname: env.NEXT_PUBLIC_RENEGADE_RELAYER_HOSTNAME, +// relayerHttpPort: 3000, +// relayerWsPort: 4000, +// useInsecureTransport: +// env.NEXT_PUBLIC_RENEGADE_RELAYER_HOSTNAME === "localhost", +// verbose: false, +// }) export default async function Page({ params: { base, quote }, }: { params: { base: string; quote: string } }) { - const report = await renegade.queryExchangeHealthStates( - new Token({ ticker: base === "USDC" ? "WETH" : base }), - new Token({ ticker: "USDC" }) - ) + // const report = await renegade.queryExchangeHealthStates( + // new Token({ ticker: base === "USDC" ? "WETH" : base }), + // new Token({ ticker: "USDC" }) + // ) return (
diff --git a/trade.renegade.fi/app/(desktop)/layout.tsx b/trade.renegade.fi/app/(desktop)/layout.tsx index 5ec8ed32..613742a7 100644 --- a/trade.renegade.fi/app/(desktop)/layout.tsx +++ b/trade.renegade.fi/app/(desktop)/layout.tsx @@ -39,6 +39,7 @@ export default async function RootLayout({ }) { const icons = await TICKER_TO_LOGO_URL_HANDLE const prices = await getTokenBannerData(renegade) + console.log("🚀 ~ prices:", prices) return ( diff --git a/trade.renegade.fi/app/(mobile)/m/body.tsx b/trade.renegade.fi/app/(mobile)/m/body.tsx index 38fc782d..550d621d 100644 --- a/trade.renegade.fi/app/(mobile)/m/body.tsx +++ b/trade.renegade.fi/app/(mobile)/m/body.tsx @@ -2,7 +2,7 @@ import { ArrowForwardIcon } from "@chakra-ui/icons" import { Button, Flex, HStack, Text } from "@chakra-ui/react" -import { ExchangeHealthState, PriceReport } from "@renegade-fi/renegade-js" +import { ExchangeHealthState } from "@renegade-fi/renegade-js" import Image from "next/image" import { TokensBanner } from "@/app/(mobile)/m/tokens-banner" @@ -74,7 +74,7 @@ export function MobileBody({ prices, report, }: { - prices: (PriceReport | undefined)[] + prices: number[] report: ExchangeHealthState }) { return ( diff --git a/trade.renegade.fi/app/(mobile)/m/tokens-banner.tsx b/trade.renegade.fi/app/(mobile)/m/tokens-banner.tsx index 530bbc77..aed71022 100644 --- a/trade.renegade.fi/app/(mobile)/m/tokens-banner.tsx +++ b/trade.renegade.fi/app/(mobile)/m/tokens-banner.tsx @@ -1,7 +1,7 @@ "use client" import { Box, Stack, Text } from "@chakra-ui/react" -import { Exchange, PriceReport } from "@renegade-fi/renegade-js" +import { Exchange } from "@renegade-fi/renegade-js" import { LivePrices } from "@/components/live-price" @@ -15,11 +15,7 @@ const DISPLAYED_TICKERS = [ ["LDO", "USDC"], ] -export function TokensBanner({ - prices, -}: { - prices: (PriceReport | undefined)[] -}) { +export function TokensBanner({ prices }: { prices: number[] }) { return ( ) diff --git a/trade.renegade.fi/app/providers.tsx b/trade.renegade.fi/app/providers.tsx index 4dabada9..3d445d5a 100644 --- a/trade.renegade.fi/app/providers.tsx +++ b/trade.renegade.fi/app/providers.tsx @@ -21,6 +21,7 @@ import { WagmiProvider, createConfig } from "wagmi" import { AppProvider } from "@/contexts/App/app-context" import { ExchangeProvider } from "@/contexts/Exchange/exchange-context" +import { PriceProvider } from "@/contexts/PriceContext/price-context" import { RenegadeProvider } from "@/contexts/Renegade/renegade-context" import { env } from "@/env.mjs" import { stylusDevnetEc2 } from "@/lib/viem" @@ -247,10 +248,12 @@ export function Providers({ > - - - {children} - + + + + {children} + + diff --git a/trade.renegade.fi/components/banners/median-banner.tsx b/trade.renegade.fi/components/banners/median-banner.tsx index 11171c5d..271b1c06 100644 --- a/trade.renegade.fi/components/banners/median-banner.tsx +++ b/trade.renegade.fi/components/banners/median-banner.tsx @@ -4,7 +4,6 @@ import { Box, Flex, Link, Spacer, Stack, Text } from "@chakra-ui/react" import { Exchange, ExchangeHealthState, - HealthState, PriceReport, Token, } from "@renegade-fi/renegade-js" @@ -13,6 +12,7 @@ import React from "react" import { BannerSeparator } from "@/components/banner-separator" import { LivePrices } from "@/components/live-price" import { PulsingConnection } from "@/components/pulsing-connection-indicator" +import { useExchangePrice } from "@/hooks/use-price" function LinkWrapper(props: { link?: string @@ -60,7 +60,7 @@ interface ExchangeConnectionTripleProps { activeBaseTicker: string activeQuoteTicker: string exchange: Exchange - priceReport: PriceReport + priceReport?: PriceReport isMobile?: boolean } function ExchangeConnectionTriple(props: ExchangeConnectionTripleProps) { @@ -94,41 +94,62 @@ function ExchangeConnectionTriple(props: ExchangeConnectionTripleProps) { median: "", }[props.exchange] - let healthState = props.priceReport.healthState - let showPrice: boolean - let connectionText: string - let textVariant: string - if (healthState === HealthState.enum.Connecting) { - showPrice = true - connectionText = "LIVE" - textVariant = "status-green" - } else if (healthState === HealthState.enum.Unsupported) { - showPrice = false - connectionText = "UNSUPPORTED" - textVariant = "status-gray" - } else if (healthState === HealthState.enum.Live) { - showPrice = true - connectionText = "LIVE" - textVariant = "status-green" - } else if (healthState === HealthState.enum.NoDataReported) { - showPrice = false - connectionText = "NO DATA" - textVariant = "status-gray" - } else if (healthState === HealthState.enum.TooStale) { - showPrice = false - connectionText = "TOO STALE" - textVariant = "status-red" - } else if (healthState === HealthState.enum.NotEnoughData) { - showPrice = false - connectionText = "NOT ENOUGH DATA" - textVariant = "status-gray" - } else if (healthState === HealthState.enum.TooMuchDeviation) { - showPrice = false - connectionText = "TOO MUCH DEVIATION" - textVariant = "status-red" - } else { - throw new Error("Invalid health state: " + healthState) - } + // Test if price state is accurate + const { state } = useExchangePrice( + props.exchange, + props.activeBaseTicker, + props.activeQuoteTicker + ) + + // let healthState = props.priceReport.healthState + let showPrice: boolean = state === "live" + let connectionText: string = + state === "live" ? "LIVE" : state === "stale" ? "STALE" : "LOADING" + let textVariant: string = + state === "live" + ? "status-green" + : state === "stale" + ? "status-red" + : "status-gray" + // If exchange is not supported, modify accordingly + // if (props.exchange === Exchange.Uniswapv3) { + // showPrice = false + // connectionText = "UNSUPPORTED" + // textVariant = "status-gray" + // } + // If incoming price is stale (longer than 1 minute), modify accordingly + + // if (healthState === HealthState.enum.Connecting) { + // showPrice = true + // connectionText = "LIVE" + // textVariant = "status-green" + // } else if (healthState === HealthState.enum.Unsupported) { + // showPrice = false + // connectionText = "UNSUPPORTED" + // textVariant = "status-gray" + // } else if (healthState === HealthState.enum.Live) { + // showPrice = true + // connectionText = "LIVE" + // textVariant = "status-green" + // } else if (healthState === HealthState.enum.NoDataReported) { + // showPrice = false + // connectionText = "NO DATA" + // textVariant = "status-gray" + // } else if (healthState === HealthState.enum.TooStale) { + // showPrice = false + // connectionText = "TOO STALE" + // textVariant = "status-red" + // } else if (healthState === HealthState.enum.NotEnoughData) { + // showPrice = false + // connectionText = "NOT ENOUGH DATA" + // textVariant = "status-gray" + // } else if (healthState === HealthState.enum.TooMuchDeviation) { + // showPrice = false + // connectionText = "TOO MUCH DEVIATION" + // textVariant = "status-red" + // } else { + // throw new Error("Invalid health state: " + healthState) + // } const pulseState = { "status-green": "live", @@ -148,7 +169,7 @@ function ExchangeConnectionTriple(props: ExchangeConnectionTripleProps) { quoteTicker={props.activeQuoteTicker} exchange={props.exchange} isMobile={props.isMobile} - price={props.priceReport.midpointPrice} + // price={props.priceReport.midpointPrice} /> )} @@ -218,7 +239,7 @@ interface ExchangeConnectionsBannerProps { activeBaseTicker: string activeQuoteTicker: string isMobile?: boolean - report: ExchangeHealthState + report?: ExchangeHealthState } interface ExchangeConnectionsBannerState { exchangeConnectionsBannerRef: React.RefObject @@ -428,7 +449,7 @@ export class MedianBanner extends React.Component< activeBaseTicker={this.props.activeBaseTicker} activeQuoteTicker={this.props.activeQuoteTicker} isMobile={this.props.isMobile} - priceReport={this.props.report.Median} + // priceReport={this.props.report.Median} /> - - + */} @@ -500,7 +521,7 @@ export class MedianBanner extends React.Component< activeBaseTicker={this.props.activeBaseTicker} activeQuoteTicker={this.props.activeQuoteTicker} exchange={Exchange.Kraken} - priceReport={this.props.report.Kraken} + // priceReport={this.props.report.Kraken} isMobile={this.props.isMobile} /> @@ -508,7 +529,7 @@ export class MedianBanner extends React.Component< activeBaseTicker={this.props.activeBaseTicker} activeQuoteTicker={this.props.activeQuoteTicker} exchange={Exchange.Okx} - priceReport={this.props.report.Okx} + // priceReport={this.props.report.Okx} isMobile={this.props.isMobile} /> @@ -516,7 +537,7 @@ export class MedianBanner extends React.Component< activeBaseTicker={this.props.activeBaseTicker} activeQuoteTicker={this.props.activeQuoteTicker} exchange={Exchange.Uniswapv3} - priceReport={this.props.report.UniswapV3} + // priceReport={this.props.report.UniswapV3} isMobile={this.props.isMobile} /> diff --git a/trade.renegade.fi/components/banners/tokens-banner.tsx b/trade.renegade.fi/components/banners/tokens-banner.tsx index 42b39d3e..42e0b096 100644 --- a/trade.renegade.fi/components/banners/tokens-banner.tsx +++ b/trade.renegade.fi/components/banners/tokens-banner.tsx @@ -1,18 +1,14 @@ "use client" import { Stack, Text } from "@chakra-ui/react" -import { Exchange, PriceReport } from "@renegade-fi/renegade-js" +import { Exchange } from "@renegade-fi/renegade-js" import Link from "next/link" import Marquee from "@/components/banners/marquee" import { LivePrices } from "@/components/live-price" import { DISPLAYED_TICKERS } from "@/lib/tokens" -export function TokensBanner({ - prices, -}: { - prices: (PriceReport | undefined)[] -}) { +export function TokensBanner({ prices }: { prices: number[] }) { return ( diff --git a/trade.renegade.fi/components/live-price.tsx b/trade.renegade.fi/components/live-price.tsx index 8ac57942..b1836773 100644 --- a/trade.renegade.fi/components/live-price.tsx +++ b/trade.renegade.fi/components/live-price.tsx @@ -1,9 +1,9 @@ import { TriangleDownIcon, TriangleUpIcon } from "@chakra-ui/icons" import { Box, Flex, Text } from "@chakra-ui/react" -import { Exchange, PriceReport } from "@renegade-fi/renegade-js" -import { useEffect, useMemo, useRef, useState } from "react" +import { Exchange } from "@renegade-fi/renegade-js" +import { useEffect, useMemo, useState } from "react" -import { useExchange } from "@/contexts/Exchange/exchange-context" +import { usePrice } from "@/contexts/PriceContext/price-context" import { TICKER_TO_DEFAULT_DECIMALS } from "@/lib/tokens" import { BannerSeparator } from "./banner-separator" @@ -14,7 +14,7 @@ interface LivePricesProps { quoteTicker: string isMobile?: boolean onlyShowPrice?: boolean - price?: number + initialPrice?: number scaleBy?: number shouldRotate?: boolean withCommas?: boolean @@ -26,27 +26,11 @@ export const LivePrices = ({ quoteTicker, isMobile, onlyShowPrice, - price: priceProp, + initialPrice = 0, scaleBy, shouldRotate, withCommas, }: LivePricesProps) => { - const [previousPriceReport, setPreviousPriceReport] = useState( - {} - ) - const [currentPriceReport, setCurrentPriceReport] = useState({}) - - const { getPriceData, onRegisterPriceListener } = useExchange() - const priceReport = getPriceData(exchange, baseTicker, quoteTicker) - - useEffect(() => { - if (!priceReport) return - setCurrentPriceReport((prev) => { - setPreviousPriceReport(prev) - return priceReport - }) - }, [priceReport]) - const baseDefaultDecimals = TICKER_TO_DEFAULT_DECIMALS[baseTicker] || 0 const trailingDecimals = useMemo(() => { if (["USDC", "WETH", "WBTC"].includes(baseTicker)) { @@ -59,63 +43,43 @@ export const LivePrices = ({ return Math.abs(baseDefaultDecimals) + 2 } }, [baseDefaultDecimals, baseTicker, quoteTicker]) + const [price, setPrice] = useState(initialPrice) + const [prevPrice, setPrevPrice] = useState(price) - const callbackIdRef = useRef(false) + const { handleSubscribe, handleGetPrice } = usePrice() + const priceReport = handleGetPrice(exchange, baseTicker, quoteTicker) useEffect(() => { - if (callbackIdRef.current) return - onRegisterPriceListener( - exchange, - baseTicker, - quoteTicker, - trailingDecimals - ).then((callbackId) => { - if (callbackId) { - callbackIdRef.current = true - } + if (!priceReport) return + setPrice((prev) => { + setPrevPrice(prev) + return priceReport }) - }, [ - baseTicker, - quoteTicker, - onRegisterPriceListener, - exchange, - trailingDecimals, - ]) + }, [priceReport]) + useEffect(() => { + handleSubscribe(exchange, baseTicker, quoteTicker, trailingDecimals) + }, [baseTicker, exchange, handleSubscribe, quoteTicker, trailingDecimals]) // Given the previous and current price reports, determine the displayed // price and red/green fade class let priceStrClass = "" - if ( - previousPriceReport.midpointPrice && - currentPriceReport.midpointPrice && - currentPriceReport.midpointPrice > previousPriceReport.midpointPrice - ) { + if (prevPrice && price > prevPrice) { priceStrClass = "fade-green-to-white" - } else if ( - previousPriceReport.midpointPrice && - currentPriceReport.midpointPrice && - currentPriceReport.midpointPrice < previousPriceReport.midpointPrice - ) { + } else if (prevPrice && price < prevPrice) { priceStrClass = "fade-red-to-white" } - let price = currentPriceReport.midpointPrice - ? currentPriceReport.midpointPrice - : baseTicker === "USDC" - ? 1 - : priceProp - ? priceProp - : 0 - + let scaledPrice = price // If the caller supplied a scaleBy prop, scale the price appropriately if (scaleBy !== undefined) { - price *= scaleBy + scaledPrice *= scaleBy } // Format the price as a string let priceStr = price.toFixed(trailingDecimals) if ( - (!Object.keys(currentPriceReport).length || scaleBy === 0) && - baseDefaultDecimals > 0 && + // (!scaledPrice || scaleBy === 0) && + // baseDefaultDecimals > 0 && + !price && baseTicker !== "USDC" ) { const leadingDecimals = priceStr.split(".")[0].length @@ -136,9 +100,7 @@ export const LivePrices = ({ return ${priceStr} } - const key = [baseTicker, quoteTicker, currentPriceReport.localTimestamp].join( - "_" - ) + const key = [baseTicker, quoteTicker, price].join("_") // Create the icon to display next to the price let priceIcon diff --git a/trade.renegade.fi/components/panels/wallets-panel.tsx b/trade.renegade.fi/components/panels/wallets-panel.tsx index b3ffab73..4ec9f3f5 100644 --- a/trade.renegade.fi/components/panels/wallets-panel.tsx +++ b/trade.renegade.fi/components/panels/wallets-panel.tsx @@ -109,9 +109,9 @@ function TokenBalance(props: TokenBalanceProps) { color="white.40" fontSize="0.8em" lineHeight="1" - opacity={usdPrice === "0.00" ? "40%" : undefined} + opacity={!usdPrice ? "40%" : undefined} > - ${usdPrice} + ${usdPrice.toFixed(2)} toast.error(`Error placing order: ${e.message}`)) } + const costInUsd = useUSDPrice(base.ticker, parseFloat(baseTokenAmount)) const hasInsufficientBalance = useMemo(() => { const baseBalance = findBalanceByTicker(balances, baseTicker).amount const quoteBalance = findBalanceByTicker(balances, quoteTicker).amount - const price = priceReport?.midpointPrice - ? priceReport.midpointPrice * parseFloat(baseTokenAmount) - : 0 if (direction === Direction.SELL) { return baseBalance < parseAmount(baseTokenAmount, base) } // TODO: Check this - return parseFloat(formatAmount(quoteBalance, quote)) < price + + return parseFloat(formatAmount(quoteBalance, quote)) < costInUsd }, [ balances, base, baseTicker, baseTokenAmount, + costInUsd, direction, - priceReport?.midpointPrice, quote, quoteTicker, ]) + const [price, setPrice] = useState(0) + const { handleSubscribe, handleGetPrice } = usePrice() + const priceReport = handleGetPrice(Exchange.Binance, baseTicker, quoteTicker) + useEffect(() => { + if (!priceReport) return + setPrice(priceReport) + }, [priceReport]) + useEffect(() => { + handleSubscribe(Exchange.Binance, baseTicker, quoteTicker, 2) + }, [baseTicker, quoteTicker, handleSubscribe]) const isSignedIn = accountId !== undefined let placeOrderButtonContent: string if (shouldUse) { placeOrderButtonContent = buttonText - } else if (!priceReport?.midpointPrice) { + } else if (!price) { placeOrderButtonContent = "No Exchange Data" } else if (hasInsufficientBalance) { placeOrderButtonContent = "Insufficient Balance" diff --git a/trade.renegade.fi/contexts/Exchange/exchange-context.tsx b/trade.renegade.fi/contexts/Exchange/exchange-context.tsx index c771e697..323b12bf 100644 --- a/trade.renegade.fi/contexts/Exchange/exchange-context.tsx +++ b/trade.renegade.fi/contexts/Exchange/exchange-context.tsx @@ -1,4 +1,4 @@ -import { CallbackId, Exchange, PriceReport } from "@renegade-fi/renegade-js" +import { CallbackId, Exchange } from "@renegade-fi/renegade-js" import { PropsWithChildren, createContext, @@ -12,7 +12,7 @@ import { import { renegade } from "@/app/providers" import { getToken } from "@/lib/utils" -import { ExchangeContextValue } from "./types" +import { ExchangeContextValue, PriceReport } from "./types" const UPDATE_THRESHOLD_MS = 1000 @@ -39,7 +39,6 @@ function ExchangeProvider({ children }: PropsWithChildren) { } let lastUpdate = 0 - const callbackId = await renegade .registerPriceReportCallback( (message: string) => { @@ -54,8 +53,8 @@ function ExchangeProvider({ children }: PropsWithChildren) { setPriceReport((prev) => { if ( !prev[key] || - prev[key].midpointPrice?.toFixed(decimals || 2) !== - priceReport.midpointPrice?.toFixed(decimals || 2) + prev[key].price?.toFixed(decimals || 2) !== + priceReport.price?.toFixed(decimals || 2) ) { return { ...prev, diff --git a/trade.renegade.fi/contexts/Exchange/types.ts b/trade.renegade.fi/contexts/Exchange/types.ts index d6d47df6..a3d50ebb 100644 --- a/trade.renegade.fi/contexts/Exchange/types.ts +++ b/trade.renegade.fi/contexts/Exchange/types.ts @@ -1,5 +1,16 @@ -import { CallbackId, Exchange, PriceReport } from "@renegade-fi/renegade-js" +import { CallbackId, Exchange, } from "@renegade-fi/renegade-js" +export type PriceReport = { + type: "PriceReport" + baseToken: { + addr: string + } + quoteToken: { + addr: string + } + price: number + localTimestamp: number +} export interface ExchangeContextValue { onRegisterPriceListener: ( exchange: Exchange, diff --git a/trade.renegade.fi/contexts/PriceContext/price-context.tsx b/trade.renegade.fi/contexts/PriceContext/price-context.tsx new file mode 100644 index 00000000..6de4b4ba --- /dev/null +++ b/trade.renegade.fi/contexts/PriceContext/price-context.tsx @@ -0,0 +1,127 @@ +import { Exchange, PriceReporterWs, Token } from "@renegade-fi/renegade-js" +import React, { + createContext, + useCallback, + useContext, + useEffect, + useState, +} from "react" + +import { env } from "@/env.mjs" + +const PriceContext = createContext<{ + priceReporter: PriceReporterWs | null + handleSubscribe: ( + exchange: Exchange, + base: string, + quote: string, + decimals: number + ) => void + handleGetPrice: ( + exchange: Exchange, + base: string, + quote: string + ) => number | undefined + handleGetLastUpdate: ( + exchange: Exchange, + base: string, + quote: string + ) => number | undefined +} | null>(null) + +const UPDATE_THRESHOLD_MS = 1000 + +const invalid = ["USDT", "BUSD", "CBETH", "RNG"] + +export const PriceProvider = ({ children }: { children: React.ReactNode }) => { + const [priceReporter, setPriceReporter] = useState( + null + ) + const [prices, setPrices] = useState>({}) + const [lastUpdate, setLastUpdate] = useState>({}) + const [attempted, setAttempted] = useState>({}) + useEffect(() => { + const priceReporter = new PriceReporterWs( + env.NEXT_PUBLIC_PRICE_REPORTER_URL + ) + setPriceReporter(priceReporter) + return () => { + priceReporter.teardown() + } + }, []) + + const handleSubscribe = useCallback( + (exchange: Exchange, base: string, quote: string, decimals: number) => { + if (!priceReporter || invalid.includes(base)) return + + const topic = getTopic(exchange, base, quote) + if (attempted[topic]) return + + let lastUpdate = 0 + const now = Date.now() + if (now - lastUpdate <= UPDATE_THRESHOLD_MS) { + return + } + lastUpdate = now + + priceReporter.subscribeToTokenPair( + exchange, + new Token({ ticker: base }), + new Token({ ticker: quote || "USDT" }), + (price) => { + setPrices((prevPrices) => { + if ( + prevPrices[topic]?.toFixed(decimals) !== + Number(price).toFixed(decimals) + ) { + return { ...prevPrices, [topic]: Number(price) } + } + return prevPrices + }) + setLastUpdate((prev) => ({ ...prev, [topic]: Date.now() })) + } + ) + setAttempted((prev) => ({ ...prev, [topic]: true })) + }, + [attempted, priceReporter] + ) + + const handleGetPrice = (exchange: Exchange, base: string, quote: string) => { + const topic = getTopic(exchange, base, quote) + return prices[topic] + } + + const handleGetLastUpdate = ( + exchange: Exchange, + base: string, + quote: string + ) => { + const topic = getTopic(exchange, base, quote) + return lastUpdate[topic] + } + + return ( + + {children} + + ) +} + +export const usePrice = () => { + const context = useContext(PriceContext) + if (!context) { + throw new Error("usePrice must be used within a PriceProvider") + } + return context +} + +function getTopic(exchange: Exchange, base: string, quote: string) { + return `${exchange}-${base}-${quote}` +} diff --git a/trade.renegade.fi/env.mjs b/trade.renegade.fi/env.mjs index 81c14626..d96c5344 100644 --- a/trade.renegade.fi/env.mjs +++ b/trade.renegade.fi/env.mjs @@ -4,20 +4,22 @@ import { z } from "zod" export const env = createEnv({ server: {}, client: { + NEXT_PUBLIC_DARKPOOL_CONTRACT: z.string().min(1), + NEXT_PUBLIC_INTERCOM_APP_ID: z.string().min(1), + NEXT_PUBLIC_PERMIT2_CONTRACT: z.string().min(1), + NEXT_PUBLIC_PRICE_REPORTER_URL: z.string().min(1), NEXT_PUBLIC_RENEGADE_RELAYER_HOSTNAME: z.string().min(1), NEXT_PUBLIC_WALLETCONNECT_PROJECT_ID: z.string().min(1), - NEXT_PUBLIC_INTERCOM_APP_ID: z.string().min(1), - NEXT_PUBLIC_DARKPOOL_CONTRACT: z.string().min(1), - NEXT_PUBLIC_PERMIT2_CONTRACT: z.string().nonempty() }, // For Next.js >= 13.4.4, you only need to destructure client variables: experimental__runtimeEnv: { + NEXT_PUBLIC_DARKPOOL_CONTRACT: process.env.NEXT_PUBLIC_DARKPOOL_CONTRACT, + NEXT_PUBLIC_INTERCOM_APP_ID: process.env.NEXT_PUBLIC_INTERCOM_APP_ID, + NEXT_PUBLIC_PERMIT2_CONTRACT: process.env.NEXT_PUBLIC_PERMIT2_CONTRACT, + NEXT_PUBLIC_PRICE_REPORTER_URL: process.env.NEXT_PUBLIC_PRICE_REPORTER_URL, NEXT_PUBLIC_RENEGADE_RELAYER_HOSTNAME: process.env.NEXT_PUBLIC_RENEGADE_RELAYER_HOSTNAME, NEXT_PUBLIC_WALLETCONNECT_PROJECT_ID: process.env.NEXT_PUBLIC_WALLETCONNECT_PROJECT_ID, - NEXT_PUBLIC_INTERCOM_APP_ID: process.env.NEXT_PUBLIC_INTERCOM_APP_ID, - NEXT_PUBLIC_DARKPOOL_CONTRACT: process.env.NEXT_PUBLIC_DARKPOOL_CONTRACT, - NEXT_PUBLIC_PERMIT2_CONTRACT: process.env.NEXT_PUBLIC_PERMIT2_CONTRACT, }, }) diff --git a/trade.renegade.fi/hooks/use-price.ts b/trade.renegade.fi/hooks/use-price.ts new file mode 100644 index 00000000..95ba7abc --- /dev/null +++ b/trade.renegade.fi/hooks/use-price.ts @@ -0,0 +1,27 @@ +import { usePrice } from "@/contexts/PriceContext/price-context" +import { Exchange } from "@renegade-fi/renegade-js" +import { useEffect, useState } from "react" + +const THRESHOLD = 60 * 1000 // 1 minute + +export const useExchangePrice = (exchange: Exchange, base: string, quote: string) => { + const [price, setPrice] = useState(0) + + const { handleSubscribe, handleGetPrice, handleGetLastUpdate } = usePrice() + const priceReport = handleGetPrice(exchange, base, quote) + useEffect(() => { + if (!priceReport) return + setPrice(priceReport) + }, [priceReport]) + useEffect(() => { + handleSubscribe(exchange, base, quote, 2) + }, [base, handleSubscribe, quote, exchange]) + + const lastUpdate = handleGetLastUpdate(exchange, base, quote) + let state: "live" | "stale" | "idle" = "idle" + if (lastUpdate) { + state = lastUpdate < Date.now() - THRESHOLD ? "stale" : "live" + } + + return { price, state } +} diff --git a/trade.renegade.fi/hooks/use-usd-price.ts b/trade.renegade.fi/hooks/use-usd-price.ts index b35a62ce..5f93da4c 100644 --- a/trade.renegade.fi/hooks/use-usd-price.ts +++ b/trade.renegade.fi/hooks/use-usd-price.ts @@ -1,52 +1,19 @@ -import { Exchange, PriceReport } from "@renegade-fi/renegade-js" -import { useEffect, useMemo, useRef, useState } from "react" - -import { useExchange } from "@/contexts/Exchange/exchange-context" +import { usePrice } from "@/contexts/PriceContext/price-context" +import { Exchange } from "@renegade-fi/renegade-js" +import { useEffect, useState } from "react" export const useUSDPrice = (base: string, amount: number) => { - const [currentPriceReport, setCurrentPriceReport] = useState({}) - - const { getPriceData, onRegisterPriceListener } = useExchange() - const priceReport = getPriceData(Exchange.Median, base, "USDC") + const [price, setPrice] = useState(0) + const { handleSubscribe, handleGetPrice } = usePrice() + const priceReport = handleGetPrice(Exchange.Binance, base, "USDC") useEffect(() => { if (!priceReport) return - setCurrentPriceReport(priceReport) + setPrice(priceReport) }, [priceReport]) - - const callbackIdRef = useRef(false) useEffect(() => { - if (callbackIdRef.current) return - onRegisterPriceListener(Exchange.Median, base, "USDC", 2).then( - (callbackId) => { - if (callbackId) { - callbackIdRef.current = true - } - } - ) - }, [base, onRegisterPriceListener]) - - const formattedPrice = useMemo(() => { - let basePrice - - if (currentPriceReport.midpointPrice) { - basePrice = currentPriceReport.midpointPrice - } else if (base === "USDC") { - basePrice = 1 - } else { - basePrice = 0 - } - - let totalPrice = basePrice * amount - - let formattedPriceStr = totalPrice.toFixed(2) - const priceStrParts = formattedPriceStr.split(".") - - // Add commas for thousands separation - priceStrParts[0] = priceStrParts[0].replace(/\B(?=(\d{3})+(?!\d))/g, ",") - - return priceStrParts.join(".") - }, [amount, base, currentPriceReport.midpointPrice]) + handleSubscribe(Exchange.Binance, base, "USDC", 2) + }, [base, handleSubscribe]) - return formattedPrice + return price * amount } diff --git a/trade.renegade.fi/lib/utils.ts b/trade.renegade.fi/lib/utils.ts index 7441605b..7c6e30ee 100644 --- a/trade.renegade.fi/lib/utils.ts +++ b/trade.renegade.fi/lib/utils.ts @@ -17,14 +17,29 @@ export function safeLocalStorageSetItem(key: string, value: string): void { } export async function getTokenBannerData(renegade: Renegade) { - return await Promise.all( - DISPLAYED_TICKERS.map(([baseTicker, quoteTicker]) => - renegade.queryExchangeHealthStates( - getToken({ ticker: baseTicker }), - getToken({ ticker: quoteTicker }) + try { + const priceReports = await Promise.all( + DISPLAYED_TICKERS.map(([baseTicker, quoteTicker]) => + renegade.queryPriceReporter( + getToken({ ticker: baseTicker }), + getToken({ ticker: quoteTicker }) + ) ) - ) - ).then((res) => res.map((e) => e.Median)) + ); + + const formattedPrices = priceReports.map(report => { + if (report.price_report?.Nominal) { + return report.price_report.Nominal.price as number; + } else { + return 0; + } + }); + + return formattedPrices; + } catch (error) { + console.error('Error fetching token banner data:', error); + return []; + } } export function findBalanceByTicker( @@ -83,3 +98,9 @@ export function parseAmount(amount: string, token: Token) { // TODO: Should try to fetch decimals from on chain return parseUnits(amount, decimals) } + +export function formatPrice(num: number) { + const formatted = num.toFixed(2) + return formatted.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ",") +} + diff --git a/trade.renegade.fi/package.json b/trade.renegade.fi/package.json index 2a836505..5a8c26aa 100644 --- a/trade.renegade.fi/package.json +++ b/trade.renegade.fi/package.json @@ -21,7 +21,7 @@ "@chakra-ui/react": "^2.8.0", "@emotion/react": "^11.11.1", "@emotion/styled": "^11.11.0", - "@renegade-fi/renegade-js": "^0.4.24", + "@renegade-fi/renegade-js": "^0.4.25", "@t3-oss/env-nextjs": "^0.6.0", "@tanstack/react-query": "^5.24.1", "connectkit": "^1.7.2", diff --git a/trade.renegade.fi/yarn.lock b/trade.renegade.fi/yarn.lock index 1f350d1f..fd3e8a07 100644 --- a/trade.renegade.fi/yarn.lock +++ b/trade.renegade.fi/yarn.lock @@ -1910,10 +1910,10 @@ dependencies: merge-options "^3.0.4" -"@renegade-fi/renegade-js@^0.4.24": - version "0.4.24" - resolved "https://registry.yarnpkg.com/@renegade-fi/renegade-js/-/renegade-js-0.4.24.tgz#410c185486edb418a688a41b4687d790a8881fd3" - integrity sha512-qaG9dvInRKTY3dbr+f2LMm88TtwdzOv0LfErK+4h2f+8PMO42MD46RXLowh5ADXzY1hVda38wGu9crki/Js5YA== +"@renegade-fi/renegade-js@^0.4.25": + version "0.4.25" + resolved "https://registry.yarnpkg.com/@renegade-fi/renegade-js/-/renegade-js-0.4.25.tgz#4afc8740ea7d80d6d53cfb3e77e0a5421b4e2698" + integrity sha512-Dt58oloZRF/pvdUjVNGiXJ3U2npHJ35hiQnAii/GN6UbQaAoMWaMe8IB6EvwPmrgmq7kYdHrNkiVZOJAQE+yIQ== dependencies: "@noble/hashes" "^1.3.0" axios "^1.3.5"