From 2e1233099e7cc2226858ee278b0b0cd4b43f12ee Mon Sep 17 00:00:00 2001 From: Matt <90358481+xbtmatt@users.noreply.github.com> Date: Fri, 20 Dec 2024 09:01:40 -0800 Subject: [PATCH] [ECO-2615] Fix the dexscreener endpoints to return floored numbers, set caching policies (#473) --- .../src/app/dexscreener/asset/route.ts | 18 ++- .../src/app/dexscreener/events/route.ts | 130 ++++++++++-------- .../src/app/dexscreener/latest-block/route.ts | 8 +- .../src/app/dexscreener/pair/route.ts | 17 ++- src/typescript/sdk/src/markets/utils.ts | 39 +++++- src/typescript/sdk/src/utils/utility-types.ts | 5 + 6 files changed, 144 insertions(+), 73 deletions(-) diff --git a/src/typescript/frontend/src/app/dexscreener/asset/route.ts b/src/typescript/frontend/src/app/dexscreener/asset/route.ts index 16746b59f..03cba3e95 100644 --- a/src/typescript/frontend/src/app/dexscreener/asset/route.ts +++ b/src/typescript/frontend/src/app/dexscreener/asset/route.ts @@ -26,6 +26,7 @@ import { EMOJICOIN_SUPPLY } from "@sdk/const"; import { calculateCirculatingSupply } from "@sdk/markets"; import { symbolEmojiStringToArray } from "../util"; import { fetchMarketState } from "@/queries/market"; +import { toNominal } from "lib/utils/decimals"; /** * - In most cases, asset ids will correspond to contract addresses. Ids are case-sensitive. @@ -63,26 +64,35 @@ async function getAsset(assetId: string): Promise { const circulatingSupply: { circulatingSupply?: number | string } = {}; if (marketState && marketState.state) { - circulatingSupply.circulatingSupply = calculateCirculatingSupply(marketState.state).toString(); + circulatingSupply.circulatingSupply = toNominal(calculateCirculatingSupply(marketState.state)); } return { id: assetId, name: marketEmojiData.symbolData.name, symbol: marketEmojiData.symbolData.symbol, - totalSupply: Number(EMOJICOIN_SUPPLY), + totalSupply: toNominal(EMOJICOIN_SUPPLY), ...circulatingSupply, // coinGeckoId: assetId, // coinMarketCapId: assetId, }; } -// NextJS JSON response handler +// Although this route would be ideal for caching, nextjs doesn't offer the ability to control +// caches for failed responses. In other words, if someone queries an asset that doesn't exist +// yet at this endpoint, it would permanently cache that asset as not existing and thus return +// the failed query JSON response. This is obviously problematic for not yet existing markets, +// so unless we have some way to not cache failed queries/empty responses, we can't cache this +// endpoint at all. +export const revalidate = 0; +export const dynamic = "force-dynamic"; +export const fetchCache = "force-no-store"; + export async function GET(request: NextRequest): Promise> { const searchParams = request.nextUrl.searchParams; const assetId = searchParams.get("id"); if (!assetId) { - // This is a required field, and is an error otherwise + // This is a required field, and is an error otherwise. return new NextResponse("id is a parameter", { status: 400 }); } const asset = await getAsset(assetId); diff --git a/src/typescript/frontend/src/app/dexscreener/events/route.ts b/src/typescript/frontend/src/app/dexscreener/events/route.ts index 78159a246..1ab68e09d 100644 --- a/src/typescript/frontend/src/app/dexscreener/events/route.ts +++ b/src/typescript/frontend/src/app/dexscreener/events/route.ts @@ -88,7 +88,27 @@ import { calculateCurvePrice, calculateRealReserves } from "@sdk/markets"; import { toCoinDecimalString } from "../../../lib/utils/decimals"; import { DECIMALS } from "@sdk/const"; import { symbolEmojisToPairId } from "../util"; -import { compareBigInt } from "@econia-labs/emojicoin-sdk"; +import { compareBigInt, type Flatten } from "@econia-labs/emojicoin-sdk"; +import { type XOR } from "@sdk/utils/utility-types"; + +export type Asset0In1Out = { + asset0In: number | string; + asset1Out: number | string; +}; + +export type Asset1In0Out = { + asset0Out: number | string; + asset1In: number | string; +}; + +export type AssetInOut = XOR; + +export type DexscreenerReserves = { + reserves: { + asset0: number | string; + asset1: number | string; + }; +}; /** * - `txnId` is a transaction identifier such as a transaction hash @@ -128,24 +148,19 @@ import { compareBigInt } from "@econia-labs/emojicoin-sdk"; * - The Indexer automatically handles calculations for USD pricing (`priceUsd` as opposed to * `priceNative`) */ -export interface SwapEvent { - eventType: "swap"; - txnId: string; - txnIndex: number; - eventIndex: number; - maker: string; - pairId: string; - asset0In?: number | string; - asset1In?: number | string; - asset0Out?: number | string; - asset1Out?: number | string; - priceNative: number | string; - reserves?: { - asset0: number | string; - asset1: number | string; - }; - metadata?: Record; -} +export type SwapEvent = Flatten< + { + eventType: "swap"; + txnId: string; + txnIndex: number; + eventIndex: number; + maker: string; + pairId: string; + priceNative: number | string; + metadata?: Record; + } & AssetInOut & + DexscreenerReserves +>; /** * - `txnId` is a transaction identifier such as a transaction hash @@ -167,21 +182,19 @@ export interface SwapEvent { * - `metadata` includes any optional auxiliary info not covered in the default schema and not * required in most cases */ -interface JoinExitEvent { - eventType: "join" | "exit"; - txnId: string; - txnIndex: number; - eventIndex: number; - maker: string; - pairId: string; - amount0: number | string; - amount1: number | string; - reserves?: { - asset0: number | string; - asset1: number | string; - }; - metadata?: Record; -} +type JoinExitEvent = Flatten< + { + eventType: "join" | "exit"; + txnId: string; + txnIndex: number; + eventIndex: number; + maker: string; + pairId: string; + amount0: number | string; + amount1: number | string; + metadata?: Record; + } & DexscreenerReserves +>; type BlockInfo = { block: Block }; type Event = (SwapEvent | JoinExitEvent) & BlockInfo; @@ -189,27 +202,18 @@ type Event = (SwapEvent | JoinExitEvent) & BlockInfo; interface EventsResponse { events: Event[]; } - function toDexscreenerSwapEvent(event: ReturnType): SwapEvent & BlockInfo { - let assetInOut; - - if (event.swap.isSell) { - // We are selling to APT - assetInOut = { - asset0In: toCoinDecimalString(event.swap.inputAmount, DECIMALS), - asset0Out: 0, - asset1In: 0, - asset1Out: toCoinDecimalString(event.swap.baseVolume, DECIMALS), - }; - } else { - // We are buying with APT - assetInOut = { - asset0In: 0, - asset0Out: toCoinDecimalString(event.swap.quoteVolume, DECIMALS), - asset1In: toCoinDecimalString(event.swap.inputAmount, DECIMALS), - asset1Out: 0, - }; - } + // Base / quote is emojicoin / APT. + // Thus asset0 / asset1 is always base volume / quote volume. + const assetInOut = event.swap.isSell + ? { + asset0In: toCoinDecimalString(event.swap.baseVolume, DECIMALS), + asset1Out: toCoinDecimalString(event.swap.quoteVolume, DECIMALS), + } + : { + asset0Out: toCoinDecimalString(event.swap.baseVolume, DECIMALS), + asset1In: toCoinDecimalString(event.swap.quoteVolume, DECIMALS), + }; const { base, quote } = calculateRealReserves(event.state); const reserves = { @@ -224,7 +228,7 @@ function toDexscreenerSwapEvent(event: ReturnType): Swa return { block: { blockNumber: Number(event.blockAndEvent.blockNumber), - blockTimestamp: event.transaction.timestamp.getTime() / 1000, + blockTimestamp: Math.floor(event.transaction.timestamp.getTime() / 1000), }, eventType: "swap", txnId: event.transaction.version.toString(), @@ -236,11 +240,8 @@ function toDexscreenerSwapEvent(event: ReturnType): Swa pairId: symbolEmojisToPairId(event.market.symbolEmojis), ...assetInOut, - - asset0In: event.swap.inputAmount.toString(), - asset1Out: event.swap.quoteVolume.toString(), priceNative, - ...reserves, + reserves, }; } @@ -258,7 +259,7 @@ function toDexscreenerJoinExitEvent( return { block: { blockNumber: Number(event.blockAndEvent.blockNumber), - blockTimestamp: event.transaction.timestamp.getTime() / 1000, + blockTimestamp: Math.floor(event.transaction.timestamp.getTime() / 1000), }, eventType: event.liquidity.liquidityProvided ? "join" : "exit", @@ -290,7 +291,14 @@ async function getEventsByVersion(fromBlock: number, toBlock: number): Promise; } @@ -34,7 +34,8 @@ interface LatestBlockResponse { block: Block; } -// NextJS JSON response handler +export const revalidate = 1; + export async function GET(_request: NextRequest): Promise> { const status = await getProcessorStatus(); const aptos = getAptosClient(); @@ -47,8 +48,7 @@ export async function GET(_request: NextRequest): Promise> { const searchParams = request.nextUrl.searchParams; const pairId = searchParams.get("id"); diff --git a/src/typescript/sdk/src/markets/utils.ts b/src/typescript/sdk/src/markets/utils.ts index db6b3e238..006dac048 100644 --- a/src/typescript/sdk/src/markets/utils.ts +++ b/src/typescript/sdk/src/markets/utils.ts @@ -416,6 +416,16 @@ export const calculateRealReserves = ({ } : cpammRealReserves; +// Returns the reserves of a market that are used to calculate the price of the asset along its +// AMM curve. For `inBondingCurve` markets, this is the virtual reserves, for post-bonding curve +// markets, this is the market's real reserves. +export const getReserves = ({ + clammVirtualReserves, + cpammRealReserves, + ...args +}: ReservesAndBondingCurveState): Types["Reserves"] => + isInBondingCurve(args) ? clammVirtualReserves : cpammRealReserves; + /** * *NOTE*: If you already have a market's state, call {@link calculateRealReserves} directly. * @@ -456,7 +466,34 @@ PreciseBig.DP = 100; * This is equivalent to calculating the slope of the tangent line created from the exact point on * the curve, where the curve is the function the AMM uses to calculate the price for the market. * - * The price is denominated in `quote / base`, where `base` is the emojicoin and `quote` is APT. + * The price is denominated in `base / quote`, but this is *NOT* a mathematical representation. + * Since the `base` is simply `1`, in a `base / quote` representation, the order of base and quote + * don't actually mean anything other than which one is being expressed as the whole number `1`. + * + * That is, `base / quote` does not imply that you divide `base` by `quote` to get the price, it + * just indicates how the price is going to be expressed semantically. What it really means is + * that you are describing the price in terms of `1 base` per `P quote`. + * + * To calculate this with the reserves, we need to find `P` below: + * + * 1 emojicoin / P APT + * + * We already know the reserves, now just structure them as the same fraction: + * + * emojicoin_reserves / APT_reserves + * + * Thus: + * + * 1 / P = emojicoin_reserves / APT_reserves + * => + * 1 = (emojicoin_reserves / APT_reserves) * P + * => + * 1 / (emojicoin_reserves / APT_reserves) = P + * => + * APT_reserves / emojicoin_reserves = P + * => + * Thus, when APT = quote and emojicoin = base, + * the price P = (quote.reserves / base.reserves) * * * For an in depth explanation of the math and behavior behind the AMMs: * @see {@link https://github.com/econia-labs/emojicoin-dot-fun/blob/main/doc/blackpaper/emojicoin-dot-fun-blackpaper.pdf} diff --git a/src/typescript/sdk/src/utils/utility-types.ts b/src/typescript/sdk/src/utils/utility-types.ts index b23ea9b35..86b65efec 100644 --- a/src/typescript/sdk/src/utils/utility-types.ts +++ b/src/typescript/sdk/src/utils/utility-types.ts @@ -36,3 +36,8 @@ export type Writable = { export type DeepWritable = { -readonly [P in keyof T]: DeepWritable; }; + +// prettier-ignore +export type XOR = + | (T & { [K in keyof U]?: never }) + | (U & { [K in keyof T]?: never });