diff --git a/packages/pools/src/__tests__/concentrated.spec.ts b/packages/pools/src/__tests__/concentrated.spec.ts index beff432894..7d1a027cc5 100644 --- a/packages/pools/src/__tests__/concentrated.spec.ts +++ b/packages/pools/src/__tests__/concentrated.spec.ts @@ -24,6 +24,8 @@ export class MockTickProvider implements TickDataProvider { '[{"liquidity_net":"1063928513.516692280118630934","tick_index":"244"},{"liquidity_net":"12821176827.612857487321385327","tick_index":"507"},{"liquidity_net":"-19112293.184876715840844082","tick_index":"975"},{"liquidity_net":"-7042487.204272221556955330","tick_index":"1070"},{"liquidity_net":"119846363299223.491923667407418441","tick_index":"1176"},{"liquidity_net":"-12821176827.612857487321385327","tick_index":"1331"},{"liquidity_net":"25864763042758566.825780314141787151","tick_index":"1642"},{"liquidity_net":"789067578720062952.312030597659307660","tick_index":"1767"},{"liquidity_net":"5548573714963426149.345245435735691668","tick_index":"1918"},{"liquidity_net":"-789067578720062952.312030597659307660","tick_index":"1968"},{"liquidity_net":"-7533671.157383740445274322","tick_index":"2118"},{"liquidity_net":"-5548573714963426149.345245435735691668","tick_index":"2133"},{"liquidity_net":"-119846363299223.491923667407418441","tick_index":"2438"},{"liquidity_net":"-13690973.612258627731976066","tick_index":"2593"},{"liquidity_net":"75212224210553284471974.853997539381387792","tick_index":"2677"},{"liquidity_net":"-25864763042758566.825780314141787151","tick_index":"2798"},{"liquidity_net":"-991724173.842077641035110916","tick_index":"2841"},{"liquidity_net":"-1063928513.516692280118630934","tick_index":"2889"},{"liquidity_net":"-75212224210553284471974.853997539381387792","tick_index":"2954"},{"liquidity_net":"2300427651236616516001198205.268726763564344440","tick_index":"3361"},{"liquidity_net":"-2300427651236616516001198205.268726763564344440","tick_index":"3390"}]' ) as LiquidityDepth[], isMaxTicks: true, + currentLiquidity: new Dec(100), + currentTick: new Int(0), }; } else { // token0 @@ -32,6 +34,8 @@ export class MockTickProvider implements TickDataProvider { '[{"liquidity_net":"13690973.612258627731976066","tick_index":"-420"},{"liquidity_net":"7042487.204272221556955330","tick_index":"-770"},{"liquidity_net":"7533671.157383740445274322","tick_index":"-923"},{"liquidity_net":"991724173.842077641035110916","tick_index":"-1014"},{"liquidity_net":"19112293.184876715840844082","tick_index":"-1073"}]' ) as LiquidityDepth[], isMaxTicks: true, + currentLiquidity: new Dec(100), + currentTick: new Int(0), }; } } @@ -42,17 +46,27 @@ export class MockTickProvider implements TickDataProvider { ): Promise { // TODO: verify if (token.denom === pool.token1) { - return { allTicks: [], isMaxTicks: false }; + return { + allTicks: [], + isMaxTicks: false, + currentLiquidity: new Dec(100), + currentTick: new Int(0), + }; } else { // token0 - return { allTicks: [], isMaxTicks: false }; + return { + allTicks: [], + isMaxTicks: false, + currentLiquidity: new Dec(100), + currentTick: new Int(0), + }; } } } export class MockAmountProvider implements AmountsDataProvider { async getPoolAmounts(): Promise<{ token0Amount: Int; token1Amount: Int }> { - return { token0Amount: new Int(100), token1Amount: new Int(100) }; + return { token0Amount: new Int(0), token1Amount: new Int(0) }; } } @@ -70,7 +84,7 @@ class TestPool extends ConcentratedLiquidityPool { } const raw1: ConcentratedLiquidityPoolRaw = JSON.parse( - '{"@type":"/osmosis.concentratedliquidity.v1beta1.Pool","address":"osmo1lzwv0glchfcw0fpwzdwfdsepmvluv6z6eh4qunxdml33sj06q3yq7xwtde","id":"4","current_tick_liquidity":"141421356.237309510000200000","token0":"uion","token1":"uosmo","current_sqrt_price":"0.014142135623730951","current_tick":"-350000","tick_spacing":"1","precision_factor_at_price_one":"-4","swap_fee":"0.010000000000000000","last_liquidity_update":"2023-03-21T02:07:13.890847048Z"}' + '{"@type":"/osmosis.concentratedliquidity.v1beta1.Pool","address":"osmo1lzwv0glchfcw0fpwzdwfdsepmvluv6z6eh4qunxdml33sj06q3yq7xwtde","id":"4","current_tick_liquidity":"141421356.2373095000200000","token0":"uion","token1":"uosmo","current_sqrt_price":"0.014142135623730951","current_tick":"-350000","tick_spacing":"1","precision_factor_at_price_one":"-4","swap_fee":"0.0000000000000000","last_liquidity_update":"2023-03-21T02:07:13.890847048Z"}' ); describe("ConcentratedLiquidityPool", () => { @@ -152,6 +166,8 @@ describe("ConcentratedLiquidityPool.getTokenOutByTokenIn", () => { tickDataProvider.getTickDepthsTokenOutGivenIn.mockResolvedValueOnce({ allTicks: mockTicks, isMaxTicks: false, + currentLiquidity: mockClPool.current_tick_liquidity, + currentTick: new Int(0), }); const tokenIn = { denom: "uosmo", amount: new Int(1_000_000) }; @@ -170,22 +186,32 @@ describe("ConcentratedLiquidityPool.getTokenOutByTokenIn", () => { .mockResolvedValueOnce({ allTicks: [], isMaxTicks: false, + currentLiquidity: mockClPool.current_tick_liquidity, + currentTick: new Int(0), }) .mockResolvedValueOnce({ allTicks: [], isMaxTicks: false, + currentLiquidity: mockClPool.current_tick_liquidity, + currentTick: new Int(0), }) .mockResolvedValueOnce({ allTicks: [], isMaxTicks: false, + currentLiquidity: mockClPool.current_tick_liquidity, + currentTick: new Int(0), }) .mockResolvedValueOnce({ allTicks: [], isMaxTicks: false, + currentLiquidity: mockClPool.current_tick_liquidity, + currentTick: new Int(0), }) .mockResolvedValueOnce({ allTicks: mockTicks.slice(0, 1), isMaxTicks: false, + currentLiquidity: mockClPool.current_tick_liquidity, + currentTick: new Int(0), }); const tokenIn = { denom: "uosmo", amount: new Int(1_000_000) }; @@ -204,10 +230,14 @@ describe("ConcentratedLiquidityPool.getTokenOutByTokenIn", () => { .mockResolvedValueOnce({ allTicks: [], isMaxTicks: false, + currentLiquidity: new Dec(100), + currentTick: new Int(0), }) .mockResolvedValueOnce({ allTicks: [], isMaxTicks: true, + currentLiquidity: new Dec(100), + currentTick: new Int(0), }); const tokenIn = { denom: "uosmo", amount: new Int(1_000_000) }; diff --git a/packages/pools/src/concentrated/fetch-tick-data-provider.ts b/packages/pools/src/concentrated/fetch-tick-data-provider.ts index d9b5cb59ec..d8c80b6b35 100644 --- a/packages/pools/src/concentrated/fetch-tick-data-provider.ts +++ b/packages/pools/src/concentrated/fetch-tick-data-provider.ts @@ -13,16 +13,27 @@ import { } from "./pool"; type TickDepthsResponse = { + current_liquidity: string; + current_tick: string; liquidity_depths: { liquidity_net: string; tick_index: string; }[]; }; +type SerializedTickDepthsResponse = { + currentLiquidity: Dec; + currentTick: Int; + depths: LiquidityDepth[]; +}; + /** Default tick data provider that fetches ticks for a single CL pool with `fetch` if the environment supports it, if not a fetcher can be supplied. * It is assumed this instance follows the instance of the pool. * Stores some cache data statically, assuming ticks are being fetched from a single query node. */ export class FetchTickDataProvider implements TickDataProvider { + protected _currentLiquidity: Dec = new Dec(0); + protected _currentTick: Int = new Int(0); + protected _zeroForOneTicks: LiquidityDepth[] = []; protected _oneForZeroTicks: LiquidityDepth[] = []; @@ -128,6 +139,8 @@ export class FetchTickDataProvider implements TickDataProvider { // check if has fetched all ticks, if so return existing ticks if (isMaxTicks) { return { + currentLiquidity: this._currentLiquidity, + currentTick: this._currentTick, allTicks: prevTicks, isMaxTicks: true, }; @@ -168,24 +181,31 @@ export class FetchTickDataProvider implements TickDataProvider { currentTickLiquidity: pool.currentTickLiquidity, }).boundTickIndex; - const depths = await this.fetchTicks( + const { depths, currentLiquidity, currentTick } = await this.fetchTicks( tokenInDenom, initialEstimatedTick ); + this._currentLiquidity = currentLiquidity; + this._currentTick = currentTick; setTicks(depths); setLatestBoundTickIndex(initialEstimatedTick); } else if (getMoreTicks) { // have fetched ticks, but requested to get more const nextBoundIndex = rampNextQueryTick( zeroForOne, - pool.currentTick, + this._currentTick, prevBoundIndex, this.nextTicksRampMultiplier ); - const depths = await this.fetchTicks(tokenInDenom, nextBoundIndex); + const { depths, currentLiquidity, currentTick } = await this.fetchTicks( + tokenInDenom, + nextBoundIndex + ); + this._currentLiquidity = currentLiquidity; + this._currentTick = currentTick; setTicks(depths); setLatestBoundTickIndex(nextBoundIndex); } @@ -196,6 +216,8 @@ export class FetchTickDataProvider implements TickDataProvider { const allTicks = zeroForOne ? this._zeroForOneTicks : this._oneForZeroTicks; return { + currentLiquidity: this._currentLiquidity, + currentTick: this._currentTick, allTicks, isMaxTicks: false, }; @@ -206,7 +228,7 @@ export class FetchTickDataProvider implements TickDataProvider { async fetchTicks( tokenInDenom: string, boundTick: Int - ): Promise { + ): Promise { const requestKey = [this.poolId, tokenInDenom, boundTick] .map((p) => p.toString()) .join("_"); @@ -226,17 +248,23 @@ export class FetchTickDataProvider implements TickDataProvider { const response = await request; - const depths = serializeTickDepths(response); + const serializedResponse = serializeRequest(response); FetchTickDataProvider._inFlightTickRequests.delete(requestKey); - return depths; + return serializedResponse; } } -function serializeTickDepths(tickDepths: TickDepthsResponse): LiquidityDepth[] { - return tickDepths.liquidity_depths.map((depth) => ({ - tickIndex: new Int(depth.tick_index), - netLiquidity: new Dec(depth.liquidity_net), - })); +function serializeRequest( + response: TickDepthsResponse +): SerializedTickDepthsResponse { + return { + currentLiquidity: new Dec(response.current_liquidity), + currentTick: new Int(response.current_tick), + depths: response.liquidity_depths.map((depth) => ({ + tickIndex: new Int(depth.tick_index), + netLiquidity: new Dec(depth.liquidity_net), + })), + }; } /** diff --git a/packages/pools/src/concentrated/pool.ts b/packages/pools/src/concentrated/pool.ts index 3b41b70265..669b9a037e 100644 --- a/packages/pools/src/concentrated/pool.ts +++ b/packages/pools/src/concentrated/pool.ts @@ -24,6 +24,8 @@ export interface ConcentratedLiquidityPoolRaw { } export type TickDepths = { + currentLiquidity: Dec; + currentTick: Int; allTicks: LiquidityDepth[]; isMaxTicks: boolean; }; @@ -93,10 +95,6 @@ export class ConcentratedLiquidityPool implements BasePool, RoutablePool { return new Dec(0); } - get currentTick(): Int { - return new Int(this.raw.current_tick); - } - /** amountToken1/amountToken0 or token 1 per token 0 */ get currentSqrtPrice(): BigDec { return new BigDec(this.raw.current_sqrt_price); @@ -106,16 +104,6 @@ export class ConcentratedLiquidityPool implements BasePool, RoutablePool { return new Dec(this.raw.current_tick_liquidity); } - get currentTickLiquidityXY(): [Dec, Dec] { - const baseAmount = new BigDec(this.currentTickLiquidity) - .quo(this.currentSqrtPrice) - .toDec(); - const quoteAmount = new BigDec(this.currentTickLiquidity) - .mul(this.currentSqrtPrice) - .toDec(); - return [baseAmount, quoteAmount]; - } - get tickSpacing(): number { const ts = parseInt(this.raw.tick_spacing); if (isNaN(ts)) { @@ -196,7 +184,7 @@ export class ConcentratedLiquidityPool implements BasePool, RoutablePool { let calcResult = undefined; do { const needMoreTicks = calcResult === "no-more-ticks"; - const { allTicks, isMaxTicks } = + const { allTicks, isMaxTicks, currentLiquidity } = await this.tickDataProvider.getTickDepthsTokenOutGivenIn( this, tokenIn, @@ -206,7 +194,7 @@ export class ConcentratedLiquidityPool implements BasePool, RoutablePool { calcResult = ConcentratedLiquidityMath.calcOutGivenIn({ tokenIn: new Coin(tokenIn.denom, tokenIn.amount), tokenDenom0: this.raw.token0, - poolLiquidity: this.currentTickLiquidity, + poolLiquidity: currentLiquidity, inittedTicks: allTicks, curSqrtPrice: this.currentSqrtPrice, swapFee, @@ -302,17 +290,20 @@ export class ConcentratedLiquidityPool implements BasePool, RoutablePool { let calcResult = undefined; do { const needMoreTicks = calcResult === "no-more-ticks"; - const { allTicks: inittedTicks, isMaxTicks } = - await this.tickDataProvider.getTickDepthsTokenInGivenOut( - this, - tokenOut, - needMoreTicks - ); + const { + allTicks: inittedTicks, + isMaxTicks, + currentLiquidity, + } = await this.tickDataProvider.getTickDepthsTokenInGivenOut( + this, + tokenOut, + needMoreTicks + ); calcResult = ConcentratedLiquidityMath.calcInGivenOut({ tokenOut: new Coin(tokenOut.denom, tokenOut.amount), tokenDenom0: this.raw.token0, - poolLiquidity: this.currentTickLiquidity, + poolLiquidity: currentLiquidity, inittedTicks, curSqrtPrice: this.currentSqrtPrice, swapFee, diff --git a/packages/stores/src/queries-external/token-historical-chart/index.ts b/packages/stores/src/queries-external/token-historical-chart/index.ts index 33d4315d76..70e328e8ff 100644 --- a/packages/stores/src/queries-external/token-historical-chart/index.ts +++ b/packages/stores/src/queries-external/token-historical-chart/index.ts @@ -8,10 +8,29 @@ import { IMPERATOR_TIMESERIES_DEFAULT_BASEURL } from ".."; import { ObservableQueryExternalBase } from "../base"; import { TokenHistoricalPrice } from "./types"; +/** + * Time frame represents the amount of minutes per bar, basically price every + * `tf` minutes. E.g. 5 - Price every 5 minutes, 1440 - price every day, etc. + * + * For example, if you want to get the 1 day chart, you should set `tf` to 1440. + * This will return 365 bars of data for each year. Each year has 525600 minutes, + * so 525600 / 1440 = 365 bars. + * + * 5 - 5 minutes + * 15 - 15 minutes + * 30 - 30 minutes + * 60 - 1 hour also known as '1H' in chart + * 120 - 2 hours + * 240 - 4 hours + * 720 - 12 hours + * 1440 - 1 day also known as '1D' in chart + * 10080 - 1 week also known as '1W' in chart + * 43800 - 1 month also known as '30D' in chart + */ const AvailableRangeValues = [ 5, 15, 30, 60, 120, 240, 720, 1440, 10080, 43800, ] as const; -type Tf = (typeof AvailableRangeValues)[number]; +export type TimeFrame = (typeof AvailableRangeValues)[number]; /** Queries Imperator token history data chart. */ export class ObservableQueryTokenHistoricalChart extends ObservableQueryExternalBase< @@ -26,7 +45,7 @@ export class ObservableQueryTokenHistoricalChart extends ObservableQueryExternal * Range of historical data represented by minutes * Available values: 5,15,30,60,120,240,720,1440,10080,43800 */ - protected readonly tf: Tf = 60 + protected readonly tf: TimeFrame = 60 ) { super(kvStore, baseURL, `/tokens/v2/historical/${symbol}/chart?tf=${tf}`); @@ -42,6 +61,20 @@ export class ObservableQueryTokenHistoricalChart extends ObservableQueryExternal ); } + @computed + get getRawChartPrices(): TokenHistoricalPrice[] { + if (!this.response) return []; + + try { + return this.response.data.map((data) => ({ + ...data, + time: data.time * 1000, + })); + } catch { + return []; + } + } + @computed get getChartPrices(): PricePretty[] | undefined { const fiat = this.priceStore.getFiatCurrency("usd"); @@ -72,12 +105,17 @@ export class ObservableQueryTokensHistoricalChart extends HasMapStore void)[] = []; + + @computed + protected get queryTokenHistoricalChart() { + let tf: TimeFrame = 60; + + if (this._historicalRange === "7d") { + tf = 10080; + } else if (this._historicalRange === "1mo") { + tf = 43800; + } + + return this.queriesExternalStore.queryTokenHistoricalChart.get( + this.denom, + tf + ); + } + + @computed + get historicalChartData(): TokenHistoricalPrice[] { + return this.queryTokenHistoricalChart.getRawChartPrices; + } + + @computed + get isHistoricalChartUnavailable(): boolean { + return ( + !this.isHistoricalChartLoading && this.historicalChartData.length === 0 + ); + } + + @computed + get isHistoricalChartLoading(): boolean { + return this.queryTokenHistoricalChart.isFetching; + } + + @computed + get yRange(): [number, number] { + const prices = this.historicalChartData?.map(({ close }) => close) || []; + const zoom = this._zoom; + const padding = 0.1; + + const chartMin = Math.max(0, Math.min(...prices)); + const chartMax = Math.max(...prices); + + const delta = Math.abs(chartMax - chartMin); + + const minWithPadding = Math.max(0, chartMin - delta * padding); + const maxWithPadding = chartMax + delta * padding; + + const zoomAdjustedMin = zoom > 1 ? chartMin / zoom : chartMin * zoom; + const zoomAdjustedMax = chartMax * zoom; + + const finalMin = Math.min(minWithPadding, zoomAdjustedMin); + const finalMax = Math.max(maxWithPadding, zoomAdjustedMax); + + return [finalMin, finalMax]; + } + + @computed + get hoverPrice(): PricePretty | undefined { + const fiat = this.priceStore.getFiatCurrency("usd"); + if (!fiat) { + return undefined; + } + return new PricePretty(fiat, this._hoverPrice); + } + + @computed + get lastChartPrice(): TokenHistoricalPrice | undefined { + return this.historicalChartData[this.historicalChartData.length - 1]; + } + + get historicalRange(): PriceRange { + return this._historicalRange; + } + + constructor( + denom: string, + private readonly queriesExternalStore: QueriesExternalStore, + private readonly priceStore: IPriceStore + ) { + this.denom = denom; + makeObservable(this); + + // Init last hover price to current price in pool once loaded + this._disposers.push( + autorun(() => { + if (this.lastChartPrice) this.setHoverPrice(this.lastChartPrice.close); + }) + ); + } + + @action + readonly setHoverPrice = (price: number) => { + this._hoverPrice = price; + }; + + @action + readonly setZoom = (zoom: number) => { + this._zoom = zoom; + }; + + @action + readonly resetZoom = () => { + this._zoom = INITIAL_ZOOM; + }; + + @action + readonly zoomIn = () => { + this._zoom = Math.max(1, this._zoom - ZOOM_STEP); + }; + + @action + readonly zoomOut = () => { + this._zoom = this._zoom + ZOOM_STEP; + }; + + @action + setHistoricalRange = (range: PriceRange) => { + this._historicalRange = range; + }; + + dispose() { + this._disposers.forEach((dispose) => dispose()); + } +} diff --git a/packages/stores/src/ui-config/index.ts b/packages/stores/src/ui-config/index.ts index 0c468705d4..1cd6f60790 100644 --- a/packages/stores/src/ui-config/index.ts +++ b/packages/stores/src/ui-config/index.ts @@ -1,3 +1,4 @@ +export * from "./asset-info-config"; export * from "./convert-to-stake"; export * from "./create-pool"; export * from "./errors"; diff --git a/packages/web/components/buttons/button.tsx b/packages/web/components/buttons/button.tsx index 382b47042d..18c5f0fbcb 100644 --- a/packages/web/components/buttons/button.tsx +++ b/packages/web/components/buttons/button.tsx @@ -129,7 +129,7 @@ export const buttonCVA = cva( ], "icon-social": [ "rounded-full", - "bg-[#201B43]", + "bg-osmoverse-850", "hover:bg-osmoverse-700", "active:bg-osmoverse-700", ], diff --git a/packages/web/components/chart/token-pair-historical.tsx b/packages/web/components/chart/token-pair-historical.tsx index b174476d3e..220c30f3ac 100644 --- a/packages/web/components/chart/token-pair-historical.tsx +++ b/packages/web/components/chart/token-pair-historical.tsx @@ -1,8 +1,10 @@ import { Dec } from "@keplr-wallet/unit"; import { PriceRange } from "@osmosis-labs/stores"; import { curveNatural } from "@visx/curve"; +import { LinearGradient } from "@visx/gradient"; import { ParentSize } from "@visx/responsive"; import { + AnimatedAreaSeries, AnimatedAxis, AnimatedGrid, AnimatedLineSeries, @@ -31,7 +33,15 @@ const TokenPairHistoricalChart: FunctionComponent<{ domain: [number, number]; onPointerHover?: (price: number) => void; onPointerOut?: () => void; -}> = ({ data, annotations, domain, onPointerHover, onPointerOut }) => { + showGradient?: boolean; +}> = ({ + data, + annotations, + domain, + onPointerHover, + onPointerOut, + showGradient = true, +}) => { return ( {({ height, width }) => ( @@ -52,7 +62,7 @@ const TokenPairHistoricalChart: FunctionComponent<{ onPointerOut={onPointerOut} theme={buildChartTheme({ backgroundColor: "transparent", - colors: ["white"], + colors: showGradient ? [theme.colors.wosmongton["300"]] : ["white"], gridColor: theme.colors.osmoverse["600"], gridColorDark: theme.colors.osmoverse["300"], svgLabelSmall: { @@ -80,15 +90,40 @@ const TokenPairHistoricalChart: FunctionComponent<{ - d?.time} - yAccessor={(d: { time: number; close: number }) => d?.close} - stroke={theme.colors.wosmongton["200"]} - /> + + {showGradient ? ( + <> + d?.time} + yAccessor={(d: { time: number; close: number }) => d?.close} + fillOpacity={0.4} + curve={curveNatural} + fill="url(#gradient)" + /> + + + ) : ( + d?.time} + yAccessor={(d: { time: number; close: number }) => d?.close} + stroke={theme.colors.wosmongton["200"]} + /> + )} {annotations.map((dec, i) => ( void; - baseDenom: string; - quoteDenom: string; hoverPrice: number; decimal: number; + fiatSymbol?: string; + baseDenom?: string; + quoteDenom?: string; hideButtons?: boolean; classes?: { buttons?: string; priceHeaderClass?: string; priceSubheaderClass?: string; pricesHeaderContainerClass?: string; + pricesHeaderRootContainer?: string; }; }> = observer( ({ @@ -163,11 +200,17 @@ export const PriceChartHeader: FunctionComponent<{ decimal, hideButtons, classes, + fiatSymbol, }) => { const t = useTranslation(); return ( -
+
+ {fiatSymbol} {formatPretty(new Dec(hoverPrice), { maxDecimals: decimal, notation: "compact", }) || ""} -
-
- {t("addConcentratedLiquidity.currentPrice")} -
-
- {t("addConcentratedLiquidity.basePerQuote", { - base: baseDenom, - quote: quoteDenom, - })} + {baseDenom && quoteDenom ? ( +
+
+ {t("addConcentratedLiquidity.currentPrice")} +
+
+ {t("addConcentratedLiquidity.basePerQuote", { + base: baseDenom, + quote: quoteDenom, + })} +
-
+ ) : undefined}
{!hideButtons && (
Number(price.toDec().toString())); diff --git a/packages/web/config/generate-chain-infos/source-chain-infos.ts b/packages/web/config/generate-chain-infos/source-chain-infos.ts index 37f923a584..27869d866e 100644 --- a/packages/web/config/generate-chain-infos/source-chain-infos.ts +++ b/packages/web/config/generate-chain-infos/source-chain-infos.ts @@ -4045,6 +4045,14 @@ export const mainnetChainInfos: SimplifiedChainInfo[] = [ high: 0.25, }, }, + { + coinDenom: "OIN", + coinMinimalDenom: + "factory/sei1thgp6wamxwqt7rthfkeehktmq0ujh5kspluw6w/OIN", + coinDecimals: 6, + coinImageUrl: "/tokens/oin.png", + coinGeckoId: "pool:oin", + }, ], features: ["ibc-transfer", "ibc-go"], explorerUrlToTx: "https://www.mintscan.io/sei/txs/{txHash}", diff --git a/packages/web/config/ibc-assets.ts b/packages/web/config/ibc-assets.ts index e48b1d6092..769fe3fe31 100644 --- a/packages/web/config/ibc-assets.ts +++ b/packages/web/config/ibc-assets.ts @@ -2159,6 +2159,13 @@ export const IBCAssetInfos: (IBCAsset & { depositUrlOverride: "https://ibc.xpla.io/", isVerified: true, }, + { + counterpartyChainId: "pacific-1", + sourceChannelId: "channel-782", + destChannelId: "channel-0", + coinMinimalDenom: + "factory/sei1thgp6wamxwqt7rthfkeehktmq0ujh5kspluw6w/OIN", + }, { counterpartyChainId: "evmos_9001-2", sourceChannelId: "channel-204", diff --git a/packages/web/config/price.ts b/packages/web/config/price.ts index 80bfb7e98b..4c1a844607 100644 --- a/packages/web/config/price.ts +++ b/packages/web/config/price.ts @@ -2169,6 +2169,16 @@ const mainnetPoolPriceRoutes: IntermediateRoute[] = [ spotPriceDestDenom: "uosmo", destCoinId: "pool:uosmo", }, + { + alternativeCoinId: "pool:oin", + poolId: "1210", + spotPriceSourceDenom: DenomHelper.ibcDenom( + [{ portId: "transfer", channelId: "channel-782" }], + "factory/sei1thgp6wamxwqt7rthfkeehktmq0ujh5kspluw6w/OIN" + ), + spotPriceDestDenom: "uosmo", + destCoinId: "pool:uosmo", + }, { alternativeCoinId: "pool:aneok", poolId: "1121", diff --git a/packages/web/hooks/ui-config/index.ts b/packages/web/hooks/ui-config/index.ts index b918814180..03f7752095 100644 --- a/packages/web/hooks/ui-config/index.ts +++ b/packages/web/hooks/ui-config/index.ts @@ -1,6 +1,7 @@ export * from "./use-add-concentrated-liquidity-config"; export * from "./use-add-liquidity-config"; export * from "./use-amount-config"; +export * from "./use-asset-info-config"; export * from "./use-create-pool-config"; export * from "./use-fake-fee-config"; export * from "./use-lock-token-config"; diff --git a/packages/web/hooks/ui-config/use-asset-info-config.ts b/packages/web/hooks/ui-config/use-asset-info-config.ts new file mode 100644 index 0000000000..55c455ff34 --- /dev/null +++ b/packages/web/hooks/ui-config/use-asset-info-config.ts @@ -0,0 +1,17 @@ +import { + IPriceStore, + ObservableAssetInfoConfig, + QueriesExternalStore, +} from "@osmosis-labs/stores"; +import { useState } from "react"; + +export const useAssetInfoConfig = ( + denom: string, + queriesExternalStore: QueriesExternalStore, + priceStore: IPriceStore +) => { + const [assetsInfoConfig] = useState( + new ObservableAssetInfoConfig(denom, queriesExternalStore, priceStore) + ); + return assetsInfoConfig; +}; diff --git a/packages/web/integrations/notifi/notifi-modal-context.tsx b/packages/web/integrations/notifi/notifi-modal-context.tsx index 2c478b37a6..52ce0486dc 100644 --- a/packages/web/integrations/notifi/notifi-modal-context.tsx +++ b/packages/web/integrations/notifi/notifi-modal-context.tsx @@ -34,6 +34,10 @@ interface NotifiModalFunctions { setIsCardOpen: React.Dispatch>; isPreventingCardClosed: boolean; // Preventing card from closing while isCardOpen is true setIsPreventingCardClosed: React.Dispatch>; + closeCard?: () => void; // close notification popover/modalBase + setCloseCard: React.Dispatch< + React.SetStateAction<(() => void | undefined) | undefined> + >; } const NotifiModalContext = createContext({ @@ -53,6 +57,7 @@ export const NotifiModalContextProvider: FunctionComponent< const [selectedHistoryEntry, setSelectedHistoryEntry] = useState< HistoryRowData | undefined >(undefined); + const [closeCard, setCloseCard] = useState<() => void>(); const config = useNotifiConfig(); const titles = useMemo(() => { if (config.state === "fetched" && config.data.titles?.active) { @@ -135,6 +140,8 @@ export const NotifiModalContextProvider: FunctionComponent< isCardOpen, setIsCardOpen, setInnerState, + closeCard, + setCloseCard, }} > {children} diff --git a/packages/web/integrations/notifi/notifi-modal.tsx b/packages/web/integrations/notifi/notifi-modal.tsx index 6fc618d077..f70c92dcfe 100644 --- a/packages/web/integrations/notifi/notifi-modal.tsx +++ b/packages/web/integrations/notifi/notifi-modal.tsx @@ -75,7 +75,8 @@ export const NotifiModal: FunctionComponent = (props) => { /> )}
diff --git a/packages/web/integrations/notifi/notifi-popover.tsx b/packages/web/integrations/notifi/notifi-popover.tsx index ee11932a7a..138c9a4b00 100644 --- a/packages/web/integrations/notifi/notifi-popover.tsx +++ b/packages/web/integrations/notifi/notifi-popover.tsx @@ -118,7 +118,7 @@ export const NotifiPopover: FunctionComponent = ({ >
)} - {({ open: popOverOpen }) => { + {({ open: popOverOpen, close }) => { return ( <> @@ -187,7 +187,8 @@ export const NotifiPopover: FunctionComponent = ({ pb-0`} >
diff --git a/packages/web/integrations/notifi/notifi-subscription-card/fetched-card/history-rows.tsx b/packages/web/integrations/notifi/notifi-subscription-card/fetched-card/history-rows.tsx index bb4ad9f7d0..13ee0ea0ed 100644 --- a/packages/web/integrations/notifi/notifi-subscription-card/fetched-card/history-rows.tsx +++ b/packages/web/integrations/notifi/notifi-subscription-card/fetched-card/history-rows.tsx @@ -94,8 +94,13 @@ const validateHistoryRow = ( }; export const HistoryRow: FunctionComponent = ({ row }) => { - const { renderView, selectedHistoryEntry, setSelectedHistoryEntry } = - useNotifiModalContext(); + const { + renderView, + selectedHistoryEntry, + setSelectedHistoryEntry, + closeCard, + setIsOverLayEnabled, + } = useNotifiModalContext(); const router = useRouter(); const t = useTranslation(); const { logEvent } = useAmplitudeAnalytics(); @@ -282,10 +287,15 @@ export const HistoryRow: FunctionComponent = ({ row }) => { }, [row]); const handleClick = useCallback(() => { + setIsOverLayEnabled(false); + if (popOutUrl) { - popOutUrl.startsWith("/") - ? router.push(popOutUrl) - : window.open(popOutUrl, "_blank"); + if (popOutUrl.startsWith("/")) { + router.push(popOutUrl); + closeCard?.(); + return; + } + router.push(popOutUrl); return; } diff --git a/packages/web/integrations/notifi/notifi-subscription-card/notifi-subscription-card.tsx b/packages/web/integrations/notifi/notifi-subscription-card/notifi-subscription-card.tsx index c75d40f762..492def537c 100644 --- a/packages/web/integrations/notifi/notifi-subscription-card/notifi-subscription-card.tsx +++ b/packages/web/integrations/notifi/notifi-subscription-card/notifi-subscription-card.tsx @@ -12,13 +12,15 @@ import { FetchedCard } from "~/integrations/notifi/notifi-subscription-card/fetc import { LoadingCard } from "~/integrations/notifi/notifi-subscription-card/loading-card"; type Props = { - parentType?: "popover" | "modalBase"; + isPopoverOrModalBaseOpen: boolean; + closeCard: () => void; }; export const NotifiSubscriptionCard: FunctionComponent = ({ - parentType, + isPopoverOrModalBaseOpen, + closeCard, }) => { - const { setIsOverLayEnabled, renderView, setIsCardOpen } = + const { setIsOverLayEnabled, renderView, setIsCardOpen, setCloseCard } = useNotifiModalContext(); const { client } = useNotifiClientContext(); @@ -30,8 +32,9 @@ export const NotifiSubscriptionCard: FunctionComponent = ({ const firstLoadRef = useRef(false); useEffect(() => { - setIsCardOpen(parentType ? true : false); - }, [parentType]); + setIsCardOpen(isPopoverOrModalBaseOpen); + setCloseCard(() => closeCard); + }, [isPopoverOrModalBaseOpen]); useEffect(() => { if (client.isInitialized && firstLoadRef.current !== true) { diff --git a/packages/web/package.json b/packages/web/package.json index d61f33049b..9cf26ec49e 100644 --- a/packages/web/package.json +++ b/packages/web/package.json @@ -59,6 +59,7 @@ "@tippyjs/react": "^4.2.6", "@transak/transak-sdk": "^1.2.2", "@visx/curve": "^2.17.0", + "@visx/gradient": "^3.3.0", "@visx/responsive": "^2.17.0", "@visx/scale": "^2.18.0", "@visx/xychart": "^2.18.0", diff --git a/packages/web/pages/assets/[denom].tsx b/packages/web/pages/assets/[denom].tsx index 8ad40bd83d..76bd23662c 100644 --- a/packages/web/pages/assets/[denom].tsx +++ b/packages/web/pages/assets/[denom].tsx @@ -1,13 +1,27 @@ +import { Dec } from "@keplr-wallet/unit"; +import { ObservableAssetInfoConfig } from "@osmosis-labs/stores"; +import { observer } from "mobx-react-lite"; import { NextPage } from "next"; import { useRouter } from "next/router"; +import { useMemo } from "react"; import { useEffect } from "react"; +import { useUnmount } from "react-use"; import { Icon } from "~/components/assets"; import { Button } from "~/components/buttons"; import LinkIconButton from "~/components/buttons/link-icon-button"; -import { useFeatureFlags } from "~/hooks"; +import TokenPairHistoricalChart, { + ChartUnavailable, + PriceChartHeader, +} from "~/components/chart/token-pair-historical"; +import SkeletonLoader from "~/components/skeleton-loader"; +import Spinner from "~/components/spinner"; +import { useAssetInfoConfig, useFeatureFlags } from "~/hooks"; +import { useStore } from "~/stores"; +import { getDecimalCount } from "~/utils/number"; +import { createContext } from "~/utils/react-context"; -const AssetInfoPage: NextPage = () => { +const AssetInfoPage: NextPage = observer(() => { const featureFlags = useFeatureFlags(); const router = useRouter(); @@ -20,16 +34,65 @@ const AssetInfoPage: NextPage = () => { } }, [featureFlags.tokenInfo, router]); + if (!router.query.denom) { + return null; // TODO: Add skeleton loader + } + + return ; +}); + +const [AssetInfoViewProvider, useAssetInfoView] = createContext<{ + assetInfoConfig: ObservableAssetInfoConfig; +}>({ + name: "AssetInfoViewContext", + strict: true, +}); + +const AssetInfoView = observer(() => { + const featureFlags = useFeatureFlags(); + const router = useRouter(); + const { queriesExternalStore, priceStore } = useStore(); + const assetInfoConfig = useAssetInfoConfig( + router.query.denom as string, + queriesExternalStore, + priceStore + ); + + useEffect(() => { + if ( + typeof featureFlags.tokenInfo !== "undefined" && + !featureFlags.tokenInfo + ) { + router.push("/assets"); + } + }, [featureFlags.tokenInfo, router]); + + useUnmount(() => { + assetInfoConfig.dispose(); + }); + + const contextValue = useMemo( + () => ({ + assetInfoConfig, + }), + [assetInfoConfig] + ); + return ( -
- -
+ +
+ +
+ +
+
+
); -}; +}); -const Navigation = () => { - const router = useRouter(); - const denom = router.query.denom as string; +const Navigation = observer(() => { + const { assetInfoConfig } = useAssetInfoView(); + const denom = assetInfoConfig.denom; const chain = "Osmosis"; @@ -84,6 +147,79 @@ const Navigation = () => {
); +}); + +const TokenChartSection = () => { + return ( +
+ + +
+ ); }; +const TokenChartHeader = observer(() => { + const { assetInfoConfig } = useAssetInfoView(); + + const minimumDecimals = 2; + const maxDecimals = Math.max( + getDecimalCount( + (assetInfoConfig.hoverPrice?.toDec() ?? new Dec(0)).toString() + ), + minimumDecimals + ); + + return ( +
+ + + +
+ ); +}); + +const TokenChart = observer(() => { + const { assetInfoConfig } = useAssetInfoView(); + return ( +
+ {assetInfoConfig.isHistoricalChartLoading ? ( +
+ +
+ ) : !assetInfoConfig.isHistoricalChartUnavailable ? ( + <> + { + if (assetInfoConfig.lastChartPrice) { + assetInfoConfig.setHoverPrice( + assetInfoConfig.lastChartPrice.close + ); + } + }} + /> + + ) : ( + + )} +
+ ); +}); + export default AssetInfoPage; diff --git a/packages/web/public/tokens/oin.png b/packages/web/public/tokens/oin.png new file mode 100644 index 0000000000..3f762326cc Binary files /dev/null and b/packages/web/public/tokens/oin.png differ diff --git a/packages/web/tailwind.config.js b/packages/web/tailwind.config.js index 6f2dbbbad0..3bb9b4fcd2 100644 --- a/packages/web/tailwind.config.js +++ b/packages/web/tailwind.config.js @@ -50,6 +50,7 @@ module.exports = { 600: "#565081", 700: "#3C356D", 800: "#282750", + 850: "#201B43", 900: "#140F34", 1000: "#090524", }, @@ -77,6 +78,8 @@ module.exports = { black: "black", inherit: "inherit", barFill: "#4f4aa2", + chartGradientPrimary: "#C41BFF", + chartGradientSecondary: "#1867FF", }, fontSize: { xxs: "0.5rem", @@ -247,6 +250,7 @@ module.exports = { "2xlinset": "0.938rem", // 1 px smaller than rounded-2xl "4x4pxlinset": "1.5rem", // 4px smaller than 4xl "4xl": "1.75rem", + "5xl": "2rem", }, transitionTimingFunction: { bounce: "cubic-bezier(0.175, 0.885, 0.32, 1.275)", diff --git a/packages/web/utils/__tests__/number.ts b/packages/web/utils/__tests__/number.ts index 656bce1c20..e3e0c79774 100644 --- a/packages/web/utils/__tests__/number.ts +++ b/packages/web/utils/__tests__/number.ts @@ -1,7 +1,11 @@ // eslint-disable-next-line import/no-extraneous-dependencies import cases from "jest-in-case"; -import { getNumberMagnitude, toScientificNotation } from "~/utils/number"; +import { + getDecimalCount, + getNumberMagnitude, + toScientificNotation, +} from "~/utils/number"; cases( "getNumberMagnitude(value)", @@ -100,3 +104,72 @@ cases( }, ] ); + +cases( + "getDecimalCount(value)", + (opts) => { + expect(getDecimalCount(opts.number)).toEqual(opts.result); + }, + [ + { + name: "should return correct decimal count for integer", + number: "1000", + result: 0, + }, + { + name: "should return correct decimal count for decimal number", + number: "1000.123", + result: 3, + }, + { + name: "should return correct decimal count for negative integer", + number: "-1000", + result: 0, + }, + { + name: "should return correct decimal count for negative decimal number", + number: "-1000.123", + result: 3, + }, + { + name: "should return correct decimal count for zero", + number: "0", + result: 0, + }, + { + name: "should return correct decimal count for decimal fraction", + number: "0.123", + result: 3, + }, + { + name: "should return correct decimal count for negative decimal fraction", + number: "-0.123", + result: 3, + }, + { + name: "should return correct decimal count for number with no decimal part", + number: "1.", + result: 0, + }, + { + name: "should return correct decimal count for number without counting trailing zeros", + number: "1.200", + result: 1, + }, + { + name: "should return correct decimal count for a really big decimal number", + number: "0.12345678901234", + result: 14, + }, + { + name: "should return correct decimal count for a really big decimal number", + number: "0.0000000000004", + result: 13, + }, + { + name: "should return correct decimal count for a really big decimal number", + number: "0.0000000168", + result: 8, + }, + ] +); diff --git a/packages/web/utils/number.ts b/packages/web/utils/number.ts index 4488c2cc55..bea8fff887 100644 --- a/packages/web/utils/number.ts +++ b/packages/web/utils/number.ts @@ -5,6 +5,22 @@ export function getNumberMagnitude(val: string | number) { return Number(Number(val).toExponential().split("e")[1]); } +export function getDecimalCount(val: string | number) { + if (!isNumeric(val)) return 0; + const valAsNumber = Number(val); + + if (valAsNumber.toString() === valAsNumber.toExponential()) { + return Math.abs(Number(valAsNumber.toString().split("e")[1] || 0)); + } + if (valAsNumber > Number.MAX_SAFE_INTEGER) { + console.warn("getDecimalCount: value is too large to get count."); + return 0; + } + + if (Math.floor(valAsNumber) === valAsNumber) return 0; + return valAsNumber.toString().split(".")[1].length || 0; +} + export function toScientificNotation( val: string | number, maxDecimals?: number diff --git a/yarn.lock b/yarn.lock index f017638101..aded7c7c6f 100644 --- a/yarn.lock +++ b/yarn.lock @@ -6515,6 +6515,14 @@ d3-shape "^1.2.0" prop-types "^15.6.2" +"@visx/gradient@^3.3.0": + version "3.3.0" + resolved "https://registry.yarnpkg.com/@visx/gradient/-/gradient-3.3.0.tgz#c0d0a7435b78053742e61731dae22c7464cd342c" + integrity sha512-t3vqukahDQsJ64/fcm85woFm2XPpSPMBz92gFvaY4J8EJY3e6rFOg382v5Dm17fgNsLRKJA0Vqo7mUtDe2pWOw== + dependencies: + "@types/react" "*" + prop-types "^15.5.7" + "@visx/grid@2.18.0": version "2.18.0" resolved "https://registry.yarnpkg.com/@visx/grid/-/grid-2.18.0.tgz#ae68e975d4b203a626ddba3a39d67a135816d6be" @@ -14779,7 +14787,7 @@ promzard@^0.3.0: dependencies: read "1" -prop-types@^15.5.10, prop-types@^15.5.8, prop-types@^15.6.0, prop-types@^15.6.1, prop-types@^15.6.2, prop-types@^15.7.2: +prop-types@^15.5.10, prop-types@^15.5.7, prop-types@^15.5.8, prop-types@^15.6.0, prop-types@^15.6.1, prop-types@^15.6.2, prop-types@^15.7.2: version "15.8.1" resolved "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz" integrity sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==