Skip to content

Commit

Permalink
[ECO-2615] Fix the dexscreener endpoints to return floored numbers, s…
Browse files Browse the repository at this point in the history
…et caching policies (#473)
  • Loading branch information
xbtmatt authored Dec 20, 2024
1 parent f09c3a8 commit 2e12330
Show file tree
Hide file tree
Showing 6 changed files with 144 additions and 73 deletions.
18 changes: 14 additions & 4 deletions src/typescript/frontend/src/app/dexscreener/asset/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -63,26 +64,35 @@ async function getAsset(assetId: string): Promise<Asset> {

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<NextResponse<AssetResponse>> {
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);
Expand Down
130 changes: 69 additions & 61 deletions src/typescript/frontend/src/app/dexscreener/events/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<Asset0In1Out, Asset1In0Out>;

export type DexscreenerReserves = {
reserves: {
asset0: number | string;
asset1: number | string;
};
};

/**
* - `txnId` is a transaction identifier such as a transaction hash
Expand Down Expand Up @@ -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<string, string>;
}
export type SwapEvent = Flatten<
{
eventType: "swap";
txnId: string;
txnIndex: number;
eventIndex: number;
maker: string;
pairId: string;
priceNative: number | string;
metadata?: Record<string, string>;
} & AssetInOut &
DexscreenerReserves
>;

/**
* - `txnId` is a transaction identifier such as a transaction hash
Expand All @@ -167,49 +182,38 @@ 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<string, string>;
}
type JoinExitEvent = Flatten<
{
eventType: "join" | "exit";
txnId: string;
txnIndex: number;
eventIndex: number;
maker: string;
pairId: string;
amount0: number | string;
amount1: number | string;
metadata?: Record<string, string>;
} & DexscreenerReserves
>;

type BlockInfo = { block: Block };
type Event = (SwapEvent | JoinExitEvent) & BlockInfo;

interface EventsResponse {
events: Event[];
}

function toDexscreenerSwapEvent(event: ReturnType<typeof toSwapEventModel>): 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 = {
Expand All @@ -224,7 +228,7 @@ function toDexscreenerSwapEvent(event: ReturnType<typeof toSwapEventModel>): 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(),
Expand All @@ -236,11 +240,8 @@ function toDexscreenerSwapEvent(event: ReturnType<typeof toSwapEventModel>): Swa
pairId: symbolEmojisToPairId(event.market.symbolEmojis),

...assetInOut,

asset0In: event.swap.inputAmount.toString(),
asset1Out: event.swap.quoteVolume.toString(),
priceNative,
...reserves,
reserves,
};
}

Expand All @@ -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",

Expand Down Expand Up @@ -290,7 +291,14 @@ async function getEventsByVersion(fromBlock: number, toBlock: number): Promise<E
);
}

// NextJS JSON response handler
// Don't cache this request, because we could inadvertently cache data that is immediately invalid
// in the case where the `toBlock` is larger than the present block. Although this shouldn't happen,
// it's not worth caching these queries anyway, because they should only be called once or twice
// with the same query parameters.
export const revalidate = 0;
export const dynamic = "force-dynamic";
export const fetchCache = "force-no-store";

/**
* We treat our versions as "blocks" because it's faster to implement given our current architecture
* This requires dexscreener to have relatively large `fromBlock - toBlock` ranges to keep up
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,15 +26,16 @@ import { getAptosClient } from "@sdk/utils/aptos-client";
*/
export interface Block {
blockNumber: number;
blockTimestamp: number;
blockTimestamp: number; // Whole number representing a UNIX timestamp in seconds.
metadata?: Record<string, string>;
}

interface LatestBlockResponse {
block: Block;
}

// NextJS JSON response handler
export const revalidate = 1;

export async function GET(_request: NextRequest): Promise<NextResponse<LatestBlockResponse>> {
const status = await getProcessorStatus();
const aptos = getAptosClient();
Expand All @@ -47,8 +48,7 @@ export async function GET(_request: NextRequest): Promise<NextResponse<LatestBlo
return NextResponse.json({
block: {
blockNumber,
// Convert to seconds
blockTimestamp: status.lastTransactionTimestamp.getTime() / 1000,
blockTimestamp: Math.floor(status.lastTransactionTimestamp.getTime() / 1000),
},
});
}
17 changes: 14 additions & 3 deletions src/typescript/frontend/src/app/dexscreener/pair/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ interface Pair {
asset0Id: string;
asset1Id: string;
createdAtBlockNumber?: number;
createdAtBlockTimestamp?: number;
createdAtBlockTimestamp?: number; // Whole number representing a UNIX timestamp in seconds.
createdAtTxnId?: string;
creator?: string;
feeBps?: number;
Expand Down Expand Up @@ -104,14 +104,25 @@ async function getPair(
asset0Id: symbolEmojisToString(symbolEmojis),
asset1Id: "APT",
createdAtBlockNumber: parseInt(block.block_height),
createdAtBlockTimestamp: marketRegistration.transaction.timestamp.getTime() / 1000,
createdAtBlockTimestamp: Math.floor(
marketRegistration.transaction.timestamp.getTime() / 1000
),
createdAtTxnId: String(marketRegistration.transaction.version),
feeBps: INTEGRATOR_FEE_RATE_BPS,
},
};
}

// 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<NextResponse<PairResponse>> {
const searchParams = request.nextUrl.searchParams;
const pairId = searchParams.get("id");
Expand Down
39 changes: 38 additions & 1 deletion src/typescript/sdk/src/markets/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.
*
Expand Down Expand Up @@ -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}
Expand Down
5 changes: 5 additions & 0 deletions src/typescript/sdk/src/utils/utility-types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,3 +36,8 @@ export type Writable<T> = {
export type DeepWritable<T> = {
-readonly [P in keyof T]: DeepWritable<T[P]>;
};

// prettier-ignore
export type XOR<T, U> =
| (T & { [K in keyof U]?: never })
| (U & { [K in keyof T]?: never });

0 comments on commit 2e12330

Please sign in to comment.