From 963ae03e7da1a929c20ef03d455b87e68672079e Mon Sep 17 00:00:00 2001 From: CapCap Date: Mon, 25 Nov 2024 15:49:11 -0800 Subject: [PATCH 1/8] Dexscreener API stubs Dexscreener events cconverion v1 Dexscreener events cconverion v1 refac out of test Scope out other enddex points small lints Event feedback changes API mapping updates Add block and event index to API set fee bps use reserve calculation from SDK decimalize outputs fake block and event index add tests, ensure ID stability lint format --- pnpm-lock.yaml | 6 +- .../frontend/src/app/dexscreener/README.md | 21 ++ .../frontend/src/app/dexscreener/asset.ts | 90 +++++ .../frontend/src/app/dexscreener/events.ts | 308 ++++++++++++++++++ .../src/app/dexscreener/latest-block.ts | 53 +++ .../frontend/src/app/dexscreener/pair.ts | 109 +++++++ .../frontend/src/app/dexscreener/util.test.ts | 26 ++ .../frontend/src/app/dexscreener/util.ts | 23 ++ src/typescript/frontend/src/middleware.ts | 2 +- src/typescript/frontend/src/router/routes.ts | 1 + .../mini-processor/event-groups/index.ts | 3 + .../mini-processor/event-groups/utils.ts | 5 +- .../src/indexer-v2/queries/app/dexscreener.ts | 59 ++++ .../sdk/src/indexer-v2/queries/app/index.ts | 1 + .../sdk/src/indexer-v2/types/index.ts | 9 + 15 files changed, 713 insertions(+), 3 deletions(-) create mode 100644 src/typescript/frontend/src/app/dexscreener/README.md create mode 100644 src/typescript/frontend/src/app/dexscreener/asset.ts create mode 100644 src/typescript/frontend/src/app/dexscreener/events.ts create mode 100644 src/typescript/frontend/src/app/dexscreener/latest-block.ts create mode 100644 src/typescript/frontend/src/app/dexscreener/pair.ts create mode 100644 src/typescript/frontend/src/app/dexscreener/util.test.ts create mode 100644 src/typescript/frontend/src/app/dexscreener/util.ts create mode 100644 src/typescript/sdk/src/indexer-v2/queries/app/dexscreener.ts diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 2b9f1883a..9b60ae178 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -1,5 +1,9 @@ -lockfileVersion: '6.0' +lockfileVersion: '9.0' settings: autoInstallPeers: true excludeLinksFromLockfile: false + +importers: + + .: {} diff --git a/src/typescript/frontend/src/app/dexscreener/README.md b/src/typescript/frontend/src/app/dexscreener/README.md new file mode 100644 index 000000000..4defc98cc --- /dev/null +++ b/src/typescript/frontend/src/app/dexscreener/README.md @@ -0,0 +1,21 @@ +# DEX Screener Adapter Specs + +Taken from https://dexscreener.notion.site/DEX-Screener-Adapter-Specs-cc1223cdf6e74a7799599106b65dcd0e + +v1.1 / Dec 2023 + +The DEX Screener Adapter is a set of HTTP endpoints that allows DEX Screener to track historical and real-time data for any Partner Decentralized Exchange. Adapters are responsible for supplying accurate and up-to-date data, whereas DEX Screener handles all data ingestion, processing and serving. + +## Overview + +- The DEX Screener Indexer queries Adapter endpoints to continuously index events as they become available, in chunks of one or many blocks at a time. Each block is only queried once, so caution must be taken to ensure that all returned data is accurate at the time of indexing. +- Adapters are to be deployed, served and maintained by the Partner +- If adapter endpoints become unreachable indexing will halt and automatically resume once they become available again +- The Indexer allows for customizable rate limits and block chunk size to ensure Adapter endpoints are not overloaded + +## Endpoints + +- An in-depth explanation of the schemas expected for each endpoint is described on the `Schemas` section below +- Numbers for amounts and price can be both `number` and `string`. Strings are more suitable when dealing with extremely small or extremely large numbers that can't be accurately serialized into JSON numbers. + +- Indexing will halt if schemas are invalid or contain unexpected values (i.e.: `swapEvent.priceNative=0` or `pair.name=""`) diff --git a/src/typescript/frontend/src/app/dexscreener/asset.ts b/src/typescript/frontend/src/app/dexscreener/asset.ts new file mode 100644 index 000000000..154d38958 --- /dev/null +++ b/src/typescript/frontend/src/app/dexscreener/asset.ts @@ -0,0 +1,90 @@ +/*** + Request: GET /asset?id=:string + + Response Schema: + // interface AssetResponse { + // asset: Asset; + // } + + Example Response: + // { + // "asset": { + // "id": "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2", + // "name": "Wrapped Ether", + // "symbol": "WETH", + // "totalSupply": 10000000, + // "circulatingSupply": 900000, + // "coinGeckoId": "ether", + // "coinMarketCapId": "ether" + // } + // } + **/ + +import { type NextRequest, NextResponse } from "next/server"; +import { toMarketEmojiData } from "@sdk/emoji_data"; +import { EMOJICOIN_SUPPLY } from "@sdk/const"; +import { calculateCirculatingSupply } from "@sdk/markets"; +import { symbolEmojiStringToArray } from "./util"; +import { fetchMarketState } from "@/queries/market"; + +/** + * - In most cases, asset ids will correspond to contract addresses. Ids are case-sensitive. + * - All `Asset` props aside from `id` may be mutable. The Indexer will periodically query assets for their most + * up-to-date info + * - `totalSupply` is optional but DEX Screener cannot calculate FDV/Market Cap if not available + * - `circulatingSupply` is optional but DEX Screener may not be able to show accurate market cap if not available + * - `coinGeckoId` and `coinMarketCapId` are optional but may be used for displaying additional token information such + * as image, description and self-reported/off-chain circulating supply + * - `metadata` includes any optional auxiliary info not covered in the default schema and not required in most cases + */ +export interface Asset { + id: string; + name: string; + symbol: string; + totalSupply: number; + circulatingSupply: number; + coinGeckoId?: string; + coinMarketCapId?: string; + metadata?: Record; +} + +export interface AssetResponse { + asset: Asset; +} + +/** + * Fetches an asset by a string of the emojis that represent the asset + * @param assetId + */ +export async function getAsset(assetId: string): Asset { + const marketEmojiData = toMarketEmojiData(assetId); + const symbolEmojis = symbolEmojiStringToArray(assetId); + const marketState = await fetchMarketState({ searchEmojis: symbolEmojis }); + + const circulatingSupply: { circulatingSupply?: number } = {}; + if (marketState && marketState.state) { + circulatingSupply.circulatingSupply = calculateCirculatingSupply(marketState.state); + } + + return { + id: assetId, + name: marketEmojiData.symbolData.name, + symbol: marketEmojiData.symbolData.symbol, + totalSupply: Number(EMOJICOIN_SUPPLY), + ...circulatingSupply, + // coinGeckoId: assetId, + // coinMarketCapId: assetId, + }; +} + +// NextJS JSON response handler +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 + return new NextResponse("id is a parameter", { status: 400 }); + } + const asset = await getAsset(assetId); + return NextResponse.json({ asset }); +} diff --git a/src/typescript/frontend/src/app/dexscreener/events.ts b/src/typescript/frontend/src/app/dexscreener/events.ts new file mode 100644 index 000000000..d07728dc0 --- /dev/null +++ b/src/typescript/frontend/src/app/dexscreener/events.ts @@ -0,0 +1,308 @@ +/*** + Request: GET /events?fromBlock=:number&toBlock=:number + + - fromBlock and toBlock are both inclusive: a request to /events?fromBlock=10&toBlock=15 should include all + available events from block 10, 11, 12, 13, 14 and 15 + + Response Schema: + // interface EventsResponse { + // events: Array<{ block: Block } & (SwapEvent | JoinExitEvent)>; + // } + + Example Response: + // { + // "events": [ + // { + // "block": { + // "blockNumber": 10, + // "blockTimestamp": 1673319600 + // }, + // "eventType": "swap", + // "txnId": "0xe9e91f1ee4b56c0df2e9f06c2b8c27c6076195a88a7b8537ba8313d80e6f124e", + // "txnIndex": 4, + // "eventIndex": 3, + // "maker": "0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045", + // "pairId": "0x123", + // "asset0In": 10000, + // "asset1Out": 20000, + // "priceNative": 2, + // "reserves": { + // "asset0": 500, + // "asset1": 1000 + // } + // }, + // { + // "block": { + // "blockNumber": 10, + // "blockTimestamp": 1673319600 + // }, + // "eventType": "join", + // "txnId": "0xea1093d492a1dcb1bef708f771a99a96ff05dcab81ca76c31940300177fcf49f", + // "txnIndex": 0, + // "eventIndex": 0, + // "maker": "0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045", + // "pairId": "0x456", + // "amount0": 10, + // "amount1": 5, + // "reserves": { + // "asset0": 100, + // "asset1": 50 + // } + // }, + // { + // "block": { + // "blockNumber": 11, + // "blockTimestamp": 1673406000 + // }, + // "eventType": "swap", + // "txnId": "0xea1093d492a1dcb1bef708f771a99a96ff05dcab81ca76c31940300177fcf49f", + // "txnIndex": 1, + // "eventIndex": 20, + // "maker": "0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045", + // "pairId": "0x456", + // "asset0In": 0.0123456789, + // "asset1Out": 0.000123456789, + // "priceNative": 0.00000012345, + // "reserves": { + // "asset0": 0.0001, + // "asset1": 0.000000000000001 + // } + // } + // ] + // } + **/ + +import { type NextRequest, NextResponse } from "next/server"; +import { type Block } from "./latest-block"; +import { + fetchLiquidityEventsByBlock, + fetchSwapEventsByBlock, +} from "@sdk/indexer-v2/queries/app/dexscreener"; +import { type toLiquidityEventModel, type toSwapEventModel } from "@sdk/indexer-v2/types"; +import Big from "big.js"; +import { calculateRealReserves } from "@sdk/markets"; +import { toCoinDecimalString } from "../../lib/utils/decimals"; +import { DECIMALS } from "@sdk/const"; +import { symbolEmojisToPairId } from "./util"; + +/** + * - `txnId` is a transaction identifier such as a transaction hash + * - `txnIndex` refers to the order of a transaction within a block, the higher the index the later in the block the + * transaction was processed + * - `eventIndex` refer to the order of the event within a transaction, the higher the index the later in the + * transaction the event was processed + * - The combination of `txnIndex` + `eventIndex` must be unique to any given event within a block, including for + * different event types + * - `maker` is an identifier for the account responsible for submitting the transaction. In most cases the transaction + * is submitted by the same account that sent/received tokens, but in cases where this doesn't apply (i.e.: transaction + * submitted by an aggregator) an effort should be made to identify the underlying account + * - All amounts (`assetIn`/`assetOut`/`reserve`) should be decimalized (`amount / (10 ** assetDecimals)`) + * - Reserves refer to the pooled amount of each asset *after* a swap event has occured. If there are multiple swap + * events on the same block and reserves cannot be determined after each individual event then it's acceptable for only + * the last event to contain the `reserves` prop + * - While `reserves` are technically optional, the Indexer relies on it for accurately calculating derived USD + * pricing (i.e.: USD price for `FOO/BAR` derived from `BAR/USDC`) from the most suitable reference pair. Pairs with no + * known reserves, from both `SwapEvent` or `JoinExitEvent`, will **not** be used for derived pricing and this may + * result in pairs with no known USD prices + * - `reserves` are also required to calculate total liquidity for each pair: if that's not available then DEX + * Screener will show liquidity as `N/A` and a warning for “Unknown liquidity” + * - A combination of either `asset0In + asset1Out` or `asset1In + asset0Out` is expected. If there are multiple assets + * in or multiple assets out then the swap event is considered invalid and indexing will halt + * - `priceNative` refers to the price of `asset0` quoted in `asset1` in that event + * - For example, in a theoretical `BTC/USD` pair, `priceNative` would be `30000` (1 BTC = 30000 USD) + * + * - Similarly, in a theoretical `USD/BTC` pair, `priceNative` would be `0.00003333333333` (1 USD = + * 0.00003333333333 USD) + * - `metadata` includes any optional auxiliary info not covered in the default schema and not required in most cases + * - The Indexer will use up to 50 decimal places for amounts/prices/reserves and all subsequent decimal places will be + * ignored. Using all 50 decimal places is highly encouraged to ensure accurate prices + * - 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; +} + +/** + * - `txnId` is a transaction identifier such as a transaction hash + * - `txnIndex` refers to the order of a transaction within a block, the higher the index the later in the block the + * transaction was processed + * - `eventIndex` refer to the order of the event within a transaction, the higher the index the later in the + * transaction the event was processed + * - The combination of `txnIndex` + `eventIndex` must be unique to any given event within a block, including for + * different event types + * - `maker` is an identifier for the account responsible for submitting the transaction. In most cases the transaction + * is submitted by the same account that sent/received tokens, but in cases where this doesn't apply (i.e.: transaction + * submitted by an aggregator) an effort should be made to identify the underlying account. + * - All amounts (`assetIn`/`assetOut`/`reserve`) should be decimalized (`amount / (10 ** assetDecimals)`) + * - Reserves refer to the pooled amount of each asset *after* a join/exit event has occured. If there are multiple + * join/exit events on the same block and reserves cannot be determined after each individual event then it's + * acceptable for only the last event to contain the `reserves` prop. + * - `metadata` includes any optional auxiliary info not covered in the default schema and not required in most cases + */ +export 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; +} + +export type BlockInfo = { block: Block }; +export type Event = (SwapEvent | JoinExitEvent) & BlockInfo; + +export interface EventsResponse { + events: Event[]; +} + +export 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.quoteVolume, DECIMALS), + }; + } else { + // We are buying with APT + assetInOut = { + asset0In: 0, + asset0Out: toCoinDecimalString(event.swap.baseVolume, DECIMALS), + asset1In: toCoinDecimalString(event.swap.inputAmount, DECIMALS), + asset1Out: 0, + }; + } + + const { base, quote } = calculateRealReserves(event.state); + const reserves = { + asset0: toCoinDecimalString(base, DECIMALS), + asset1: toCoinDecimalString(quote, DECIMALS), + }; + + const priceNative = new Big(event.swap.avgExecutionPriceQ64.toString()).div(2 ** 64).toFixed(64); + + return { + block: { + blockNumber: Number(event.blockAndEvent.blockNumber), + blockTimestamp: event.transaction.timestamp.getTime() / 1000, + }, + eventType: "swap", + txnId: event.transaction.version.toString(), + + txnIndex: Number(event.transaction.version), + eventIndex: Number(event.blockAndEvent.eventIndex), + + maker: event.swap.swapper, + pairId: symbolEmojisToPairId(event.market.symbolEmojis), + + ...assetInOut, + + asset0In: event.swap.inputAmount.toString(), + asset1Out: event.swap.quoteVolume.toString(), + priceNative, + ...reserves, + }; +} + +export function toDexscreenerJoinExitEvent( + event: ReturnType +): JoinExitEvent & BlockInfo { + const { base, quote } = calculateRealReserves(event.state); + const reserves = { + asset0: toCoinDecimalString(base, DECIMALS), + asset1: toCoinDecimalString(quote, DECIMALS), + }; + + return { + block: { + blockNumber: Number(event.blockAndEvent.blockNumber), + blockTimestamp: event.transaction.timestamp.getTime() / 1000, + }, + eventType: event.liquidity.liquidityProvided ? "join" : "exit", + + txnId: event.transaction.version.toString(), + + txnIndex: Number(event.transaction.version), + eventIndex: Number(event.blockAndEvent.eventIndex), + + maker: event.liquidity.provider, + pairId: symbolEmojisToPairId(event.market.symbolEmojis), + + amount0: toCoinDecimalString(event.liquidity.baseAmount, DECIMALS), + amount1: toCoinDecimalString(event.liquidity.quoteAmount, DECIMALS), + reserves, + }; +} + +export async function getEventsByVersion(fromBlock: number, toBlock: number): Promise { + const swapEvents = await fetchSwapEventsByBlock({ fromBlock, toBlock }); + const liquidityEvents = await fetchLiquidityEventsByBlock({ fromBlock, toBlock }); + + // Merge these two arrays by their `transaction.version`: do it iteratively across both to avoid M*N complexity + const events: Event[] = []; + let swapIndex = 0; + let liquidityIndex = 0; + while (swapIndex < swapEvents.length && liquidityIndex < liquidityEvents.length) { + const swapEvent = swapEvents[swapIndex]; + const liquidityEvent = liquidityEvents[liquidityIndex]; + if (swapEvent.transaction.version < liquidityEvent.transaction.version) { + events.push(toDexscreenerSwapEvent(swapEvent)); + swapIndex++; + } else { + events.push(toDexscreenerJoinExitEvent(liquidityEvent)); + liquidityIndex++; + } + } + + // Add any remaining events + events.push(...swapEvents.slice(swapIndex).map(toDexscreenerSwapEvent)); + events.push(...liquidityEvents.slice(liquidityIndex).map(toDexscreenerJoinExitEvent)); + + return events; +} + +// NextJS JSON response handler +/** + * 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 + * */ +export async function GET(request: NextRequest): Promise> { + const searchParams = request.nextUrl.searchParams; + const fromBlock = searchParams.get("fromBlock"); + const toBlock = searchParams.get("toBlock"); + if (fromBlock === null || toBlock === null) { + // This should never happen, and is an invalid call + return new NextResponse("fromBlock and toBlock are required parameters", { status: 400 }); + } + + const events = await getEventsByVersion(parseInt(fromBlock, 10), parseInt(toBlock, 10)); + + return NextResponse.json({ events }); +} diff --git a/src/typescript/frontend/src/app/dexscreener/latest-block.ts b/src/typescript/frontend/src/app/dexscreener/latest-block.ts new file mode 100644 index 000000000..62ae23673 --- /dev/null +++ b/src/typescript/frontend/src/app/dexscreener/latest-block.ts @@ -0,0 +1,53 @@ +/*** + - The `/latest-block` endpoint should be in sync with the `/events` endpoint, meaning it should only return the latest block where data from `/events` will be available. This doesn't mean it should return the latest block with an event, but it should not return a block for which `/events` has no data available yet. + - If `/events` fetches data on-demand this isn't an issue, but if it relies on data indexed and persisted in the backend then `/latest-block` should be aware of the latest persisted block + - During live indexing, the Indexer will continuously poll `/latest-block` and use its data to then query `/events` + Response Schema: + // interface LatestBlockResponse { + // block: Block; + // } + Example Response: + // { + // "block": { + // "blockNumber": 100, + // "blockTimestamp": 1698126147 + // } + // } + **/ + +import { type NextRequest, NextResponse } from "next/server"; +import { getProcessorStatus } from "@sdk/indexer-v2/queries"; +import { getAptosClient } from "@sdk/utils/aptos-client"; + +/** + * - `blockTimestamp` should be a UNIX timestamp, **not** including milliseconds + * - `metadata` includes any optional auxiliary info not covered in the default schema and not required in most cases + */ +export interface Block { + blockNumber: number; + blockTimestamp: number; + metadata?: Record; +} + +export interface LatestBlockResponse { + block: Block; +} + +// NextJS JSON response handler +export async function GET(_request: NextRequest): Promise> { + const status = await getProcessorStatus(); + const aptos = getAptosClient(); + const latestBlock = await aptos.getBlockByVersion({ ledgerVersion: status.lastSuccessVersion }); + // Because we may not have finished processing the entire block yet, we return block number - 1 here. This adds + // ~1s (blocktime) of latency, but ensures completeness. We set it to 0 if below. + const blockHeight = parseInt(latestBlock.block_height, 10); + const blockNumber = blockHeight > 0 ? blockHeight - 1 : 0; + + return NextResponse.json({ + block: { + blockNumber, + // Convert to seconds + blockTimestamp: status.lastTransactionTimestamp.getTime() / 1000, + }, + }); +} diff --git a/src/typescript/frontend/src/app/dexscreener/pair.ts b/src/typescript/frontend/src/app/dexscreener/pair.ts new file mode 100644 index 000000000..ce0fcc0e6 --- /dev/null +++ b/src/typescript/frontend/src/app/dexscreener/pair.ts @@ -0,0 +1,109 @@ +/*** + Request: GET /pair?id=:string + + Response Schema: + // interface PairResponse { + // pair: Pair; + // } + + Example Response: + // { + // "pair": { + // "id": "0x11b815efB8f581194ae79006d24E0d814B7697F6", + // "dexKey": "uniswap", + // "asset0Id": "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2", + // "asset1Id": "0xdAC17F958D2ee523a2206206994597C13D831ec7", + // "createdAtBlockNumber": 100, + // "createdAtBlockTimestamp": 1698126147, + // "createdAtTxnId": "0xe9e91f1ee4b56c0df2e9f06c2b8c27c6076195a88a7b8537ba8313d80e6f124e", + // "feeBps": 100 + // } + // } + **/ + +import { type NextRequest, NextResponse } from "next/server"; +import { fetchMarketRegistrationEventBySymbolEmojis } from "@sdk/indexer-v2/queries/app/dexscreener"; +import { getAptosClient } from "@sdk/utils/aptos-client"; +import { INTEGRATOR_FEE_RATE_BPS } from "@sdk/const"; +import { pairIdToSymbolEmojis } from "./util"; + +/** + * - All `Pair` props are immutable - Indexer will not query a given pair more than once + * - In most cases, pair ids will correspond to contract addresses. Ids are case-sensitive. + * - `dexKey` is an identifier for the DEX that hosts this pair. For most adapters this will be a static value such as + * `uniswap`, but if multiple DEXes are tracked an id such as a factory address may be used. + * - `asset0` and `asset1` order should **never** change. `amount0/reserve0` will always refer to the same `asset0`, + * and `amount1/reserve1` will always refer to the same `asset1`. If asset order mutates then all data after the change + * will be invalid and a re-index will be required. + * - A simple strategy to keep this in check is to simply order assets alphabetically. For example, in a pair + * containing assets `0xAAA` and `0xZZZ`, `asset0=0xAAA` and `asset1=0xZZZ` + * - DEX Screener UI will automatically invert pairs as needed and default to their most logical order (i.e.: + * `BTC/USD` as opposed to `USD/BTC`) + * - `createdAtBlockNumber`, `createdAtBlockTimestamp` and `createdAtTxnId` are optional but encouraged. If unavailable + * DEX Screener can bet set to assume pair creation date is the same date as its first ever event. + * - `feeBps` corresponds to swap fees in bps. For instance, a fee of 1% maps to `feeBps=100` + * - `pool` is only recommended for DEXes that support multi-asset pools and allows the DEX Screener UI to correlate + * multiple pairs in the same multi-asset pool + * - `metadata` includes any optional auxiliary info not covered in the default schema and not required in most cases + */ +export interface Pair { + id: string; + dexKey: string; + asset0Id: string; + asset1Id: string; + createdAtBlockNumber?: number; + createdAtBlockTimestamp?: number; + createdAtTxnId?: string; + creator?: string; + feeBps?: number; + pool?: { + id: string; + name: string; + assetIds: string[]; + pairIds: string[]; + metadata?: Record; + }; + metadata?: Record; +} + +export interface PairResponse { + pair: Pair; +} + +/** + * + * @param pairId is the pair ID. Generally it's `event.market.symbolEmojis.join("") + "-APT"` + */ +export async function getPair(pairId: string): Promise { + const sybolEmojis = pairIdToSymbolEmojis(pairId); + + const marketRegistrations = await fetchMarketRegistrationEventBySymbolEmojis({ sybolEmojis }); + const marketRegistration = marketRegistrations[0]; + + const aptos = getAptosClient(); + const block = await aptos.getBlockByVersion({ + ledgerVersion: marketRegistration.transaction.version, + }); + + return { + id: pairId, + dexKey: "emojicoin.fun", + asset0Id: emojiString, + asset1Id: "APT", + createdAtBlockNumber: parseInt(block.block_height), + createdAtBlockTimestamp: marketRegistration.transaction.timestamp.getTime() / 1000, + createdAtTxnId: String(marketRegistration.transaction.version), + feeBps: INTEGRATOR_FEE_RATE_BPS, + }; +} + +// NextJS JSON response handler +export async function GET(request: NextRequest): Promise> { + const searchParams = request.nextUrl.searchParams; + const pairId = searchParams.get("id"); + if (!pairId) { + return new NextResponse("id is a required parameter", { status: 400 }); + } + const pair = await getPair(pairId); + return NextResponse.json({ pair }); +} diff --git a/src/typescript/frontend/src/app/dexscreener/util.test.ts b/src/typescript/frontend/src/app/dexscreener/util.test.ts new file mode 100644 index 000000000..7d1b69524 --- /dev/null +++ b/src/typescript/frontend/src/app/dexscreener/util.test.ts @@ -0,0 +1,26 @@ +import type { SymbolEmoji } from "@sdk/emoji_data"; +import { describe, it } from "node:test"; +import { pairIdToSymbolEmojis, symbolEmojisToPairId, symbolEmojisToString } from "./util"; +import { expect } from "@playwright/test"; + +describe("dexscreener utilities", () => { + const marketSymbols: SymbolEmoji[][] = [["🤌"], ["🤌🏻", "🤌🏽"], ["🤌🏼", "🤌🏾", "🤌🏿"]]; + + it("ensures ID stability across different endpoints", async () => { + const expectedJoinedStrings = ["🤌", "🤌🏻🤌🏽", "🤌🏼🤌🏾🤌🏿"]; + const expectedPairIds = ["🤌-APT", "🤌🏻🤌🏽-APT", "🤌🏼🤌🏾🤌🏿-APT"]; + + for (let i = 0; i < marketSymbols.length; i++) { + const symbolEmojis = marketSymbols[i]; + + const joinedSymbolEmojis = symbolEmojisToString(symbolEmojis); + expect(joinedSymbolEmojis).toEqual(expectedJoinedStrings[i]); + + const pairId = symbolEmojisToPairId(symbolEmojis); + expect(pairId).toEqual(expectedPairIds[i]); + + const deserializedSymbolEmojis = pairIdToSymbolEmojis(pairId); + expect(deserializedSymbolEmojis).toEqual(symbolEmojis); + } + }); +}); diff --git a/src/typescript/frontend/src/app/dexscreener/util.ts b/src/typescript/frontend/src/app/dexscreener/util.ts new file mode 100644 index 000000000..4cc12bb1f --- /dev/null +++ b/src/typescript/frontend/src/app/dexscreener/util.ts @@ -0,0 +1,23 @@ +import { type SymbolEmoji, toMarketEmojiData } from "@sdk/emoji_data"; + +export function pairIdToSymbolEmojiString(pairId: string): string { + return pairId.split("-")[0]; +} + +export function symbolEmojisToString(symbolEmojis: Array): string { + return symbolEmojis.join(""); +} + +export function symbolEmojiStringToArray(symbolEmojiString: string): SymbolEmoji[] { + const marketEmojiData = toMarketEmojiData(symbolEmojiString); + return marketEmojiData.emojis.map((emojiData) => emojiData.emoji); +} + +export function pairIdToSymbolEmojis(pairId: string): SymbolEmoji[] { + const emojiString = pairIdToSymbolEmojiString(pairId); + return symbolEmojiStringToArray(emojiString); +} + +export function symbolEmojisToPairId(symbolEmojis: Array): string { + return symbolEmojisToString(symbolEmojis) + "-APT"; +} diff --git a/src/typescript/frontend/src/middleware.ts b/src/typescript/frontend/src/middleware.ts index 7c246f79d..ca1034875 100644 --- a/src/typescript/frontend/src/middleware.ts +++ b/src/typescript/frontend/src/middleware.ts @@ -20,7 +20,7 @@ export default async function middleware(request: NextRequest) { if (MAINTENANCE_MODE && pathname !== "/maintenance") { return NextResponse.redirect(new URL(ROUTES.maintenance, request.url)); } - if (pathname === "/test" || pathname === "/verify_status") { + if (pathname === "/test" || pathname === "/verify_status" || pathname === "/dexscreener") { return NextResponse.next(); } diff --git a/src/typescript/frontend/src/router/routes.ts b/src/typescript/frontend/src/router/routes.ts index eb96d0cda..be06d7c98 100644 --- a/src/typescript/frontend/src/router/routes.ts +++ b/src/typescript/frontend/src/router/routes.ts @@ -10,4 +10,5 @@ export const ROUTES = { notFound: "/not-found", maintenance: "/maintenance", launching: "/launching", + dexscreener: "/dexscreener", } as const; diff --git a/src/typescript/sdk/src/indexer-v2/mini-processor/event-groups/index.ts b/src/typescript/sdk/src/indexer-v2/mini-processor/event-groups/index.ts index 9b4dc75c8..9e5c4b71e 100644 --- a/src/typescript/sdk/src/indexer-v2/mini-processor/event-groups/index.ts +++ b/src/typescript/sdk/src/indexer-v2/mini-processor/event-groups/index.ts @@ -98,6 +98,7 @@ export function getEventsAsProcessorModels( marketLatestStateEvents: Array(), }; + let eventIndex = 0n; for (const builder of builders.values()) { const { marketID, marketNonce, bumpEvent, stateEvent, periodicStateEvents } = builder.build(); @@ -137,7 +138,9 @@ export function getEventsAsProcessorModels( lastSwap, event: bumpEvent, response, + eventIndex, }); + eventIndex += 1n; // Create fake data if we're generating an event- otherwise, use the transaction response // passed in. diff --git a/src/typescript/sdk/src/indexer-v2/mini-processor/event-groups/utils.ts b/src/typescript/sdk/src/indexer-v2/mini-processor/event-groups/utils.ts index 1e7100f28..e0f982f75 100644 --- a/src/typescript/sdk/src/indexer-v2/mini-processor/event-groups/utils.ts +++ b/src/typescript/sdk/src/indexer-v2/mini-processor/event-groups/utils.ts @@ -75,8 +75,9 @@ export const addModelsForBumpEvent = (args: { lastSwap: Types["LastSwap"]; event: BumpEvent; response?: UserTransactionResponse; // If we're parsing the WriteSet. + eventIndex?: bigint; // For liquidity and swap events. }) => { - const { rows, transaction, market, state, lastSwap, event, response } = args; + const { rows, transaction, market, state, lastSwap, event, response, eventIndex } = args; if (isChatEvent(event)) { rows.chatEvents.push({ transaction, @@ -97,6 +98,7 @@ export const addModelsForBumpEvent = (args: { const liquidity = toLiquidityEventData(event); const liquidityEventModel: DatabaseModels["liquidity_events"] = { transaction, + blockAndEvent: { blockNumber: transaction.version, eventIndex: eventIndex! }, market, state, lastSwap, @@ -127,6 +129,7 @@ export const addModelsForBumpEvent = (args: { } else if (isSwapEvent(event)) { rows.swapEvents.push({ transaction, + blockAndEvent: { blockNumber: transaction.version, eventIndex: eventIndex! }, market, state, swap: toSwapEventData(event), diff --git a/src/typescript/sdk/src/indexer-v2/queries/app/dexscreener.ts b/src/typescript/sdk/src/indexer-v2/queries/app/dexscreener.ts new file mode 100644 index 000000000..65f8268b3 --- /dev/null +++ b/src/typescript/sdk/src/indexer-v2/queries/app/dexscreener.ts @@ -0,0 +1,59 @@ +import { queryHelper } from "../utils"; +import { + toLiquidityEventModel, + toMarketRegistrationEventModel, + toSwapEventModel, +} from "../../types"; +import { LIMIT } from "../../../queries"; +import type { MarketStateQueryArgs } from "../../types/common"; +import { postgrest, toQueryArray } from "../client"; +import { TableName } from "../../types/json-types"; +import { type SymbolEmoji } from "../../../emoji_data"; + +const selectMarketRegistrationEventBySymbolEmojis = ({ + searchEmojis, + page = 1, + pageSize = LIMIT, +}: { searchEmojis: SymbolEmoji[] } & MarketStateQueryArgs) => + postgrest + .from(TableName.MarketRegistrationEvents) + .select("*") + .eq("symbol_emojis", toQueryArray(searchEmojis)) + .range((page - 1) * pageSize, page * pageSize - 1); + +const selectSwapEventsByBlock = ({ + fromBlock, + toBlock, + page = 1, + pageSize = LIMIT, +}: { fromBlock: number; toBlock: number } & MarketStateQueryArgs) => + postgrest + .from(TableName.SwapEvents) + .select("*") + .gte("block_number", fromBlock) + .lte("block_number", toBlock) + .range((page - 1) * pageSize, page * pageSize - 1); + +const selectLiquidityEventsByBlock = ({ + fromBlock, + toBlock, + page = 1, + pageSize = LIMIT, +}: { fromBlock: number; toBlock: number } & MarketStateQueryArgs) => + postgrest + .from(TableName.LiquidityEvents) + .select("*") + .gte("block_number", fromBlock) + .lte("block_number", toBlock) + .range((page - 1) * pageSize, page * pageSize - 1); + +export const fetchMarketRegistrationEventBySymbolEmojis = queryHelper( + selectMarketRegistrationEventBySymbolEmojis, + toMarketRegistrationEventModel +); + +export const fetchSwapEventsByBlock = queryHelper(selectSwapEventsByBlock, toSwapEventModel); +export const fetchLiquidityEventsByBlock = queryHelper( + selectLiquidityEventsByBlock, + toLiquidityEventModel +); diff --git a/src/typescript/sdk/src/indexer-v2/queries/app/index.ts b/src/typescript/sdk/src/indexer-v2/queries/app/index.ts index e3dc8266e..ca8ca2489 100644 --- a/src/typescript/sdk/src/indexer-v2/queries/app/index.ts +++ b/src/typescript/sdk/src/indexer-v2/queries/app/index.ts @@ -1,3 +1,4 @@ export * from "./home"; export * from "./market"; export * from "./pools"; +export * from "./dexscreener"; diff --git a/src/typescript/sdk/src/indexer-v2/types/index.ts b/src/typescript/sdk/src/indexer-v2/types/index.ts index cfb50140a..875f591fc 100644 --- a/src/typescript/sdk/src/indexer-v2/types/index.ts +++ b/src/typescript/sdk/src/indexer-v2/types/index.ts @@ -21,6 +21,7 @@ import { TableName, type ProcessedFields, DatabaseRpc, + type BlockAndEventIndexMetadata, } from "./json-types"; import { type MarketEmojiData, type SymbolEmoji, toMarketEmojiData } from "../../emoji_data"; import { toPeriod, toTrigger, type Period, type Trigger } from "../../const"; @@ -51,6 +52,11 @@ const toTransactionMetadata = ( insertedAt: data.inserted_at ? postgresTimestampToDate(data.inserted_at) : new Date(0), }); +const toBlockAndEventIndex = (data: BlockAndEventIndexMetadata) => ({ + blockNumber: BigInt(data.block_number), + eventIndex: BigInt(data.event_index), +}); + /// If received from postgres, symbol bytes come in as a hex string in the format "\\xabcd" where /// "abcd" is the hex string. /// If received from the broker, the symbolBytes will be deserialized as an array of values. @@ -351,6 +357,7 @@ export const withMarketAndStateMetadataAndEmitTime = curryToNamedType( toMarketMetadataModel, "market" ); +export const withBlockAndEventIndex = curryToNamedType(toBlockAndEventIndex, "blockAndEvent"); export const withLastSwap = curryToNamedType(toLastSwapFromDatabase, "lastSwap"); export const withGlobalStateEventData = curryToNamedType(toGlobalStateEventData, "globalState"); export const withPeriodicStateMetadata = curryToNamedType( @@ -476,6 +483,7 @@ export const toSwapEventModel = (data: DatabaseJsonType["swap_events"]) => ({ ...withMarketAndStateMetadataAndBumpTime(data), ...withSwapEventData(data), ...withStateEventData(data), + ...withBlockAndEventIndex(data), ...GuidGetters.swapEvent(data), }); @@ -494,6 +502,7 @@ export const toLiquidityEventModel = (data: DatabaseJsonType["liquidity_events"] ...withLiquidityEventData(data), ...withStateEventData(data), ...withLastSwapData(data), + ...withBlockAndEventIndex(data), ...GuidGetters.liquidityEvent(data), }); From 4694bd146d14322445204a062d0aca8b25b1518f Mon Sep 17 00:00:00 2001 From: CapCap Date: Fri, 6 Dec 2024 11:48:51 -0800 Subject: [PATCH 2/8] make optional --- src/typescript/sdk/src/indexer-v2/types/index.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/typescript/sdk/src/indexer-v2/types/index.ts b/src/typescript/sdk/src/indexer-v2/types/index.ts index 875f591fc..8e48d9a51 100644 --- a/src/typescript/sdk/src/indexer-v2/types/index.ts +++ b/src/typescript/sdk/src/indexer-v2/types/index.ts @@ -52,10 +52,10 @@ const toTransactionMetadata = ( insertedAt: data.inserted_at ? postgresTimestampToDate(data.inserted_at) : new Date(0), }); -const toBlockAndEventIndex = (data: BlockAndEventIndexMetadata) => ({ +const toBlockAndEventIndex = (data: BlockAndEventIndexMetadata) => (data && data.block_number) ? ({ blockNumber: BigInt(data.block_number), eventIndex: BigInt(data.event_index), -}); +}) : undefined; /// If received from postgres, symbol bytes come in as a hex string in the format "\\xabcd" where /// "abcd" is the hex string. @@ -345,8 +345,8 @@ export type UserPoolsRPCModel = ReturnType; */ const curryToNamedType = (to: (data: T) => U, name: K) => - (data: T): { [P in K]: U } => - ({ [name]: to(data) }) as { [P in K]: U }; + (data: T): { [P in K]: U } => + ({ [name]: to(data) }) as { [P in K]: U }; export const withTransactionMetadata = curryToNamedType(toTransactionMetadata, "transaction"); export const withMarketAndStateMetadataAndBumpTime = curryToNamedType( From 7ddf726122ba797040e233dd4af20b2783cdd2fc Mon Sep 17 00:00:00 2001 From: CapCap Date: Fri, 6 Dec 2024 14:11:44 -0800 Subject: [PATCH 3/8] Add omit to mini indexer types to leave it unchanged --- .../src/indexer-v2/mini-processor/event-groups/index.ts | 6 +++--- .../src/indexer-v2/mini-processor/event-groups/utils.ts | 4 +--- src/typescript/sdk/src/indexer-v2/types/index.ts | 8 ++++---- 3 files changed, 8 insertions(+), 10 deletions(-) diff --git a/src/typescript/sdk/src/indexer-v2/mini-processor/event-groups/index.ts b/src/typescript/sdk/src/indexer-v2/mini-processor/event-groups/index.ts index 9e5c4b71e..e9e5e17a5 100644 --- a/src/typescript/sdk/src/indexer-v2/mini-processor/event-groups/index.ts +++ b/src/typescript/sdk/src/indexer-v2/mini-processor/event-groups/index.ts @@ -29,12 +29,12 @@ export type BumpEventModel = export type EventsModels = { transaction: TxnInfo; chatEvents: Array; - liquidityEvents: Array; + liquidityEvents: Array>; marketRegistrationEvents: Array; periodicStateEvents: Array; - swapEvents: Array; + swapEvents: Array>; userPools: Array; - marketLatestStateEvents: Array; + marketLatestStateEvents: Array }; export type UserLiquidityPoolsMap = Map< diff --git a/src/typescript/sdk/src/indexer-v2/mini-processor/event-groups/utils.ts b/src/typescript/sdk/src/indexer-v2/mini-processor/event-groups/utils.ts index e0f982f75..ddb09f8fe 100644 --- a/src/typescript/sdk/src/indexer-v2/mini-processor/event-groups/utils.ts +++ b/src/typescript/sdk/src/indexer-v2/mini-processor/event-groups/utils.ts @@ -96,9 +96,8 @@ export const addModelsForBumpEvent = (args: { }); } else if (isLiquidityEvent(event)) { const liquidity = toLiquidityEventData(event); - const liquidityEventModel: DatabaseModels["liquidity_events"] = { + const liquidityEventModel = { transaction, - blockAndEvent: { blockNumber: transaction.version, eventIndex: eventIndex! }, market, state, lastSwap, @@ -129,7 +128,6 @@ export const addModelsForBumpEvent = (args: { } else if (isSwapEvent(event)) { rows.swapEvents.push({ transaction, - blockAndEvent: { blockNumber: transaction.version, eventIndex: eventIndex! }, market, state, swap: toSwapEventData(event), diff --git a/src/typescript/sdk/src/indexer-v2/types/index.ts b/src/typescript/sdk/src/indexer-v2/types/index.ts index 8e48d9a51..a79611da0 100644 --- a/src/typescript/sdk/src/indexer-v2/types/index.ts +++ b/src/typescript/sdk/src/indexer-v2/types/index.ts @@ -52,10 +52,10 @@ const toTransactionMetadata = ( insertedAt: data.inserted_at ? postgresTimestampToDate(data.inserted_at) : new Date(0), }); -const toBlockAndEventIndex = (data: BlockAndEventIndexMetadata) => (data && data.block_number) ? ({ +const toBlockAndEventIndex = (data: BlockAndEventIndexMetadata) =>({ blockNumber: BigInt(data.block_number), eventIndex: BigInt(data.event_index), -}) : undefined; +}); /// If received from postgres, symbol bytes come in as a hex string in the format "\\xabcd" where /// "abcd" is the hex string. @@ -345,8 +345,8 @@ export type UserPoolsRPCModel = ReturnType; */ const curryToNamedType = (to: (data: T) => U, name: K) => - (data: T): { [P in K]: U } => - ({ [name]: to(data) }) as { [P in K]: U }; + (data: T): { [P in K]: U } => + ({ [name]: to(data) }) as { [P in K]: U }; export const withTransactionMetadata = curryToNamedType(toTransactionMetadata, "transaction"); export const withMarketAndStateMetadataAndBumpTime = curryToNamedType( From 460117b6c458086be5f159c909accdf5fd9e73d6 Mon Sep 17 00:00:00 2001 From: CapCap Date: Fri, 6 Dec 2024 14:16:42 -0800 Subject: [PATCH 4/8] fix test and cleanup --- .../frontend/src/app/dexscreener/events.ts | 46 +++++++++---------- .../frontend/src/app/dexscreener/pair.ts | 45 ++++++++++++------ src/typescript/frontend/src/middleware.ts | 8 ++-- .../mini-processor/event-groups/index.ts | 9 ++-- .../mini-processor/event-groups/utils.ts | 5 +- .../sdk/src/indexer-v2/types/index.ts | 12 +++-- 6 files changed, 71 insertions(+), 54 deletions(-) diff --git a/src/typescript/frontend/src/app/dexscreener/events.ts b/src/typescript/frontend/src/app/dexscreener/events.ts index d07728dc0..8f75e9113 100644 --- a/src/typescript/frontend/src/app/dexscreener/events.ts +++ b/src/typescript/frontend/src/app/dexscreener/events.ts @@ -78,12 +78,17 @@ import { fetchLiquidityEventsByBlock, fetchSwapEventsByBlock, } from "@sdk/indexer-v2/queries/app/dexscreener"; -import { type toLiquidityEventModel, type toSwapEventModel } from "@sdk/indexer-v2/types"; +import { + isLiquidityEventModel, + type toLiquidityEventModel, + type toSwapEventModel, +} from "@sdk/indexer-v2/types"; import Big from "big.js"; import { 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"; /** * - `txnId` is a transaction identifier such as a transaction hash @@ -177,6 +182,8 @@ export interface EventsResponse { events: Event[]; } +const TWO_TO_64 = new Big(2).pow(64); + export function toDexscreenerSwapEvent( event: ReturnType ): SwapEvent & BlockInfo { @@ -206,7 +213,11 @@ export function toDexscreenerSwapEvent( asset1: toCoinDecimalString(quote, DECIMALS), }; - const priceNative = new Big(event.swap.avgExecutionPriceQ64.toString()).div(2 ** 64).toFixed(64); + const priceNative = new Big(event.swap.avgExecutionPriceQ64.toString()) + .div(TWO_TO_64) + .toFixed(64); + + if (!event.blockAndEvent) throw new Error("blockAndEvent is undefined"); return { block: { @@ -240,6 +251,8 @@ export function toDexscreenerJoinExitEvent( asset1: toCoinDecimalString(quote, DECIMALS), }; + if (!event.blockAndEvent) throw new Error("blockAndEvent is undefined"); + return { block: { blockNumber: Number(event.blockAndEvent.blockNumber), @@ -265,27 +278,14 @@ export async function getEventsByVersion(fromBlock: number, toBlock: number): Pr const swapEvents = await fetchSwapEventsByBlock({ fromBlock, toBlock }); const liquidityEvents = await fetchLiquidityEventsByBlock({ fromBlock, toBlock }); - // Merge these two arrays by their `transaction.version`: do it iteratively across both to avoid M*N complexity - const events: Event[] = []; - let swapIndex = 0; - let liquidityIndex = 0; - while (swapIndex < swapEvents.length && liquidityIndex < liquidityEvents.length) { - const swapEvent = swapEvents[swapIndex]; - const liquidityEvent = liquidityEvents[liquidityIndex]; - if (swapEvent.transaction.version < liquidityEvent.transaction.version) { - events.push(toDexscreenerSwapEvent(swapEvent)); - swapIndex++; - } else { - events.push(toDexscreenerJoinExitEvent(liquidityEvent)); - liquidityIndex++; - } - } - - // Add any remaining events - events.push(...swapEvents.slice(swapIndex).map(toDexscreenerSwapEvent)); - events.push(...liquidityEvents.slice(liquidityIndex).map(toDexscreenerJoinExitEvent)); - - return events; + // Merge these two arrays by their `transaction.version` + return [...swapEvents, ...liquidityEvents] + .sort((a, b) => compareBigInt(a.transaction.version, b.transaction.version)) + .map((event) => + isLiquidityEventModel(event) + ? toDexscreenerJoinExitEvent(event) + : toDexscreenerSwapEvent(event) + ); } // NextJS JSON response handler diff --git a/src/typescript/frontend/src/app/dexscreener/pair.ts b/src/typescript/frontend/src/app/dexscreener/pair.ts index ce0fcc0e6..4874e104b 100644 --- a/src/typescript/frontend/src/app/dexscreener/pair.ts +++ b/src/typescript/frontend/src/app/dexscreener/pair.ts @@ -25,7 +25,7 @@ import { type NextRequest, NextResponse } from "next/server"; import { fetchMarketRegistrationEventBySymbolEmojis } from "@sdk/indexer-v2/queries/app/dexscreener"; import { getAptosClient } from "@sdk/utils/aptos-client"; import { INTEGRATOR_FEE_RATE_BPS } from "@sdk/const"; -import { pairIdToSymbolEmojis } from "./util"; +import { pairIdToSymbolEmojis, symbolEmojisToString } from "./util"; /** * - All `Pair` props are immutable - Indexer will not query a given pair more than once @@ -74,11 +74,22 @@ export interface PairResponse { * * @param pairId is the pair ID. Generally it's `event.market.symbolEmojis.join("") + "-APT"` */ -export async function getPair(pairId: string): Promise { - const sybolEmojis = pairIdToSymbolEmojis(pairId); +export async function getPair( + pairId: string +): Promise<{ pair: Pair; error?: never } | { pair?: never; error: NextResponse }> { + const symbolEmojis = pairIdToSymbolEmojis(pairId); - const marketRegistrations = await fetchMarketRegistrationEventBySymbolEmojis({ sybolEmojis }); - const marketRegistration = marketRegistrations[0]; + const marketRegistrations = await fetchMarketRegistrationEventBySymbolEmojis({ + searchEmojis: symbolEmojis, + }); + const marketRegistration = marketRegistrations.pop(); + if (!marketRegistration) { + return { + error: new NextResponse(`Market registration not found for pairId: ${pairId}`, { + status: 404, + }), + }; + } const aptos = getAptosClient(); const block = await aptos.getBlockByVersion({ @@ -86,24 +97,28 @@ export async function getPair(pairId: string): Promise { }); return { - id: pairId, - dexKey: "emojicoin.fun", - asset0Id: emojiString, - asset1Id: "APT", - createdAtBlockNumber: parseInt(block.block_height), - createdAtBlockTimestamp: marketRegistration.transaction.timestamp.getTime() / 1000, - createdAtTxnId: String(marketRegistration.transaction.version), - feeBps: INTEGRATOR_FEE_RATE_BPS, + pair: { + id: pairId, + dexKey: "emojicoin.fun", + asset0Id: symbolEmojisToString(symbolEmojis), + asset1Id: "APT", + createdAtBlockNumber: parseInt(block.block_height), + createdAtBlockTimestamp: marketRegistration.transaction.timestamp.getTime() / 1000, + createdAtTxnId: String(marketRegistration.transaction.version), + feeBps: INTEGRATOR_FEE_RATE_BPS, + }, }; } // NextJS JSON response handler -export async function GET(request: NextRequest): Promise> { +export async function GET(request: NextRequest) { const searchParams = request.nextUrl.searchParams; const pairId = searchParams.get("id"); if (!pairId) { return new NextResponse("id is a required parameter", { status: 400 }); } - const pair = await getPair(pairId); + const { pair, error } = await getPair(pairId); + if (error) return error; + return NextResponse.json({ pair }); } diff --git a/src/typescript/frontend/src/middleware.ts b/src/typescript/frontend/src/middleware.ts index ca1034875..5a0bded2d 100644 --- a/src/typescript/frontend/src/middleware.ts +++ b/src/typescript/frontend/src/middleware.ts @@ -11,16 +11,16 @@ import { normalizePossibleMarketPath } from "utils/pathname-helpers"; export default async function middleware(request: NextRequest) { const pathname = new URL(request.url).pathname; - if (pathname === "/launching") { + if (pathname === ROUTES.launching) { return NextResponse.next(); } - if (PRE_LAUNCH_TEASER && pathname !== "/launching") { + if (PRE_LAUNCH_TEASER && pathname !== ROUTES.launching) { return NextResponse.redirect(new URL(ROUTES.launching, request.url)); } - if (MAINTENANCE_MODE && pathname !== "/maintenance") { + if (MAINTENANCE_MODE && pathname !== ROUTES.maintenance) { return NextResponse.redirect(new URL(ROUTES.maintenance, request.url)); } - if (pathname === "/test" || pathname === "/verify_status" || pathname === "/dexscreener") { + if (pathname === "/test" || pathname === "/verify_status" || pathname === ROUTES.dexscreener) { return NextResponse.next(); } diff --git a/src/typescript/sdk/src/indexer-v2/mini-processor/event-groups/index.ts b/src/typescript/sdk/src/indexer-v2/mini-processor/event-groups/index.ts index e9e5e17a5..9b4dc75c8 100644 --- a/src/typescript/sdk/src/indexer-v2/mini-processor/event-groups/index.ts +++ b/src/typescript/sdk/src/indexer-v2/mini-processor/event-groups/index.ts @@ -29,12 +29,12 @@ export type BumpEventModel = export type EventsModels = { transaction: TxnInfo; chatEvents: Array; - liquidityEvents: Array>; + liquidityEvents: Array; marketRegistrationEvents: Array; periodicStateEvents: Array; - swapEvents: Array>; + swapEvents: Array; userPools: Array; - marketLatestStateEvents: Array + marketLatestStateEvents: Array; }; export type UserLiquidityPoolsMap = Map< @@ -98,7 +98,6 @@ export function getEventsAsProcessorModels( marketLatestStateEvents: Array(), }; - let eventIndex = 0n; for (const builder of builders.values()) { const { marketID, marketNonce, bumpEvent, stateEvent, periodicStateEvents } = builder.build(); @@ -138,9 +137,7 @@ export function getEventsAsProcessorModels( lastSwap, event: bumpEvent, response, - eventIndex, }); - eventIndex += 1n; // Create fake data if we're generating an event- otherwise, use the transaction response // passed in. diff --git a/src/typescript/sdk/src/indexer-v2/mini-processor/event-groups/utils.ts b/src/typescript/sdk/src/indexer-v2/mini-processor/event-groups/utils.ts index ddb09f8fe..0cb53243c 100644 --- a/src/typescript/sdk/src/indexer-v2/mini-processor/event-groups/utils.ts +++ b/src/typescript/sdk/src/indexer-v2/mini-processor/event-groups/utils.ts @@ -75,9 +75,8 @@ export const addModelsForBumpEvent = (args: { lastSwap: Types["LastSwap"]; event: BumpEvent; response?: UserTransactionResponse; // If we're parsing the WriteSet. - eventIndex?: bigint; // For liquidity and swap events. }) => { - const { rows, transaction, market, state, lastSwap, event, response, eventIndex } = args; + const { rows, transaction, market, state, lastSwap, event, response } = args; if (isChatEvent(event)) { rows.chatEvents.push({ transaction, @@ -97,6 +96,7 @@ export const addModelsForBumpEvent = (args: { } else if (isLiquidityEvent(event)) { const liquidity = toLiquidityEventData(event); const liquidityEventModel = { + blockAndEvent: undefined, transaction, market, state, @@ -127,6 +127,7 @@ export const addModelsForBumpEvent = (args: { } } else if (isSwapEvent(event)) { rows.swapEvents.push({ + blockAndEvent: undefined, transaction, market, state, diff --git a/src/typescript/sdk/src/indexer-v2/types/index.ts b/src/typescript/sdk/src/indexer-v2/types/index.ts index a79611da0..16e7e11d9 100644 --- a/src/typescript/sdk/src/indexer-v2/types/index.ts +++ b/src/typescript/sdk/src/indexer-v2/types/index.ts @@ -52,10 +52,13 @@ const toTransactionMetadata = ( insertedAt: data.inserted_at ? postgresTimestampToDate(data.inserted_at) : new Date(0), }); -const toBlockAndEventIndex = (data: BlockAndEventIndexMetadata) =>({ - blockNumber: BigInt(data.block_number), - eventIndex: BigInt(data.event_index), -}); +const toBlockAndEventIndex = (data: BlockAndEventIndexMetadata) => + data && data.block_number + ? { + blockNumber: BigInt(data.block_number), + eventIndex: BigInt(data.event_index), + } + : undefined; /// If received from postgres, symbol bytes come in as a hex string in the format "\\xabcd" where /// "abcd" is the hex string. @@ -357,6 +360,7 @@ export const withMarketAndStateMetadataAndEmitTime = curryToNamedType( toMarketMetadataModel, "market" ); +/// The `blockAndEvent` field is only set when fetched from the DB- otherwise it's `undefined`. export const withBlockAndEventIndex = curryToNamedType(toBlockAndEventIndex, "blockAndEvent"); export const withLastSwap = curryToNamedType(toLastSwapFromDatabase, "lastSwap"); export const withGlobalStateEventData = curryToNamedType(toGlobalStateEventData, "globalState"); From 7832595f202703ba4c26aaacc0f161ef6f3daba8 Mon Sep 17 00:00:00 2001 From: CapCap Date: Mon, 9 Dec 2024 20:41:23 -0800 Subject: [PATCH 5/8] Use post cost for dexscreener --- src/typescript/frontend/src/app/dexscreener/events.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/typescript/frontend/src/app/dexscreener/events.ts b/src/typescript/frontend/src/app/dexscreener/events.ts index 8f75e9113..f30529566 100644 --- a/src/typescript/frontend/src/app/dexscreener/events.ts +++ b/src/typescript/frontend/src/app/dexscreener/events.ts @@ -195,13 +195,13 @@ export function toDexscreenerSwapEvent( asset0In: toCoinDecimalString(event.swap.inputAmount, DECIMALS), asset0Out: 0, asset1In: 0, - asset1Out: toCoinDecimalString(event.swap.quoteVolume, DECIMALS), + asset1Out: toCoinDecimalString(event.swap.baseVolume, DECIMALS), }; } else { // We are buying with APT assetInOut = { asset0In: 0, - asset0Out: toCoinDecimalString(event.swap.baseVolume, DECIMALS), + asset0Out: toCoinDecimalString(event.swap.quoteVolume, DECIMALS), asset1In: toCoinDecimalString(event.swap.inputAmount, DECIMALS), asset1Out: 0, }; From 87d1918362e67997c800ce47d1ff838a5de43df6 Mon Sep 17 00:00:00 2001 From: CapCap Date: Thu, 12 Dec 2024 12:07:04 -0800 Subject: [PATCH 6/8] use new priceNative method --- src/typescript/frontend/src/app/dexscreener/events.ts | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/src/typescript/frontend/src/app/dexscreener/events.ts b/src/typescript/frontend/src/app/dexscreener/events.ts index f30529566..1008a3703 100644 --- a/src/typescript/frontend/src/app/dexscreener/events.ts +++ b/src/typescript/frontend/src/app/dexscreener/events.ts @@ -83,8 +83,7 @@ import { type toLiquidityEventModel, type toSwapEventModel, } from "@sdk/indexer-v2/types"; -import Big from "big.js"; -import { calculateRealReserves } from "@sdk/markets"; +import { calculateCurvePrice, calculateRealReserves } from "@sdk/markets"; import { toCoinDecimalString } from "../../lib/utils/decimals"; import { DECIMALS } from "@sdk/const"; import { symbolEmojisToPairId } from "./util"; @@ -182,8 +181,6 @@ export interface EventsResponse { events: Event[]; } -const TWO_TO_64 = new Big(2).pow(64); - export function toDexscreenerSwapEvent( event: ReturnType ): SwapEvent & BlockInfo { @@ -213,9 +210,7 @@ export function toDexscreenerSwapEvent( asset1: toCoinDecimalString(quote, DECIMALS), }; - const priceNative = new Big(event.swap.avgExecutionPriceQ64.toString()) - .div(TWO_TO_64) - .toFixed(64); + const priceNative = calculateCurvePrice(event.state).toFixed(50).toString(); if (!event.blockAndEvent) throw new Error("blockAndEvent is undefined"); From 1cb0941a7c381724af7b5bb1b711223f2f07a03f Mon Sep 17 00:00:00 2001 From: Matt <90358481+xbtmatt@users.noreply.github.com> Date: Sat, 14 Dec 2024 14:20:14 -0800 Subject: [PATCH 7/8] Fix types and overflow possibility --- src/typescript/frontend/src/app/dexscreener/asset.ts | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/typescript/frontend/src/app/dexscreener/asset.ts b/src/typescript/frontend/src/app/dexscreener/asset.ts index 154d38958..de92d9554 100644 --- a/src/typescript/frontend/src/app/dexscreener/asset.ts +++ b/src/typescript/frontend/src/app/dexscreener/asset.ts @@ -41,8 +41,8 @@ export interface Asset { id: string; name: string; symbol: string; - totalSupply: number; - circulatingSupply: number; + totalSupply: number | string; + circulatingSupply?: number | string; coinGeckoId?: string; coinMarketCapId?: string; metadata?: Record; @@ -56,14 +56,14 @@ export interface AssetResponse { * Fetches an asset by a string of the emojis that represent the asset * @param assetId */ -export async function getAsset(assetId: string): Asset { +export async function getAsset(assetId: string): Promise { const marketEmojiData = toMarketEmojiData(assetId); const symbolEmojis = symbolEmojiStringToArray(assetId); const marketState = await fetchMarketState({ searchEmojis: symbolEmojis }); - const circulatingSupply: { circulatingSupply?: number } = {}; + const circulatingSupply: { circulatingSupply?: number | string } = {}; if (marketState && marketState.state) { - circulatingSupply.circulatingSupply = calculateCirculatingSupply(marketState.state); + circulatingSupply.circulatingSupply = calculateCirculatingSupply(marketState.state).toString(); } return { From c4028f1e9a590c6b000168d9b9120a8a0fad6557 Mon Sep 17 00:00:00 2001 From: Matt <90358481+xbtmatt@users.noreply.github.com> Date: Sun, 15 Dec 2024 22:18:36 -0800 Subject: [PATCH 8/8] Lint/format --- .../frontend/src/app/dexscreener/README.md | 31 ++++-- .../frontend/src/app/dexscreener/events.ts | 101 ++++++++++-------- .../src/app/dexscreener/latest-block.ts | 5 +- .../frontend/src/app/dexscreener/pair.ts | 1 + .../frontend/src/app/dexscreener/util.test.ts | 1 + src/typescript/frontend/src/middleware.ts | 1 + src/typescript/frontend/src/router/routes.ts | 1 + .../sdk/src/indexer-v2/queries/app/index.ts | 1 + 8 files changed, 87 insertions(+), 55 deletions(-) diff --git a/src/typescript/frontend/src/app/dexscreener/README.md b/src/typescript/frontend/src/app/dexscreener/README.md index 4defc98cc..8d039a8aa 100644 --- a/src/typescript/frontend/src/app/dexscreener/README.md +++ b/src/typescript/frontend/src/app/dexscreener/README.md @@ -1,21 +1,38 @@ # DEX Screener Adapter Specs + + Taken from https://dexscreener.notion.site/DEX-Screener-Adapter-Specs-cc1223cdf6e74a7799599106b65dcd0e + + v1.1 / Dec 2023 -The DEX Screener Adapter is a set of HTTP endpoints that allows DEX Screener to track historical and real-time data for any Partner Decentralized Exchange. Adapters are responsible for supplying accurate and up-to-date data, whereas DEX Screener handles all data ingestion, processing and serving. +The DEX Screener Adapter is a set of HTTP endpoints that allows DEX Screener to +track historical and real-time data for any Partner Decentralized Exchange. +Adapters are responsible for supplying accurate and up-to-date data, whereas DEX +Screener handles all data ingestion, processing and serving. ## Overview -- The DEX Screener Indexer queries Adapter endpoints to continuously index events as they become available, in chunks of one or many blocks at a time. Each block is only queried once, so caution must be taken to ensure that all returned data is accurate at the time of indexing. +- The DEX Screener Indexer queries Adapter endpoints to continuously index + events as they become available, in chunks of one or many blocks at a time. + Each block is only queried once, so caution must be taken to ensure that all + returned data is accurate at the time of indexing. - Adapters are to be deployed, served and maintained by the Partner -- If adapter endpoints become unreachable indexing will halt and automatically resume once they become available again -- The Indexer allows for customizable rate limits and block chunk size to ensure Adapter endpoints are not overloaded +- If adapter endpoints become unreachable indexing will halt and automatically + resume once they become available again +- The Indexer allows for customizable rate limits and block chunk size to ensure + Adapter endpoints are not overloaded ## Endpoints -- An in-depth explanation of the schemas expected for each endpoint is described on the `Schemas` section below -- Numbers for amounts and price can be both `number` and `string`. Strings are more suitable when dealing with extremely small or extremely large numbers that can't be accurately serialized into JSON numbers. +- An in-depth explanation of the schemas expected for each endpoint is described + on the `Schemas` section below + +- Numbers for amounts and price can be both `number` and `string`. Strings are + more suitable when dealing with extremely small or extremely large numbers + that can't be accurately serialized into JSON numbers. -- Indexing will halt if schemas are invalid or contain unexpected values (i.e.: `swapEvent.priceNative=0` or `pair.name=""`) +- Indexing will halt if schemas are invalid or contain unexpected values + (i.e.: `swapEvent.priceNative=0` or `pair.name=""`) diff --git a/src/typescript/frontend/src/app/dexscreener/events.ts b/src/typescript/frontend/src/app/dexscreener/events.ts index 1008a3703..b738fe39f 100644 --- a/src/typescript/frontend/src/app/dexscreener/events.ts +++ b/src/typescript/frontend/src/app/dexscreener/events.ts @@ -1,8 +1,9 @@ +// cspell:word dexscreener /*** Request: GET /events?fromBlock=:number&toBlock=:number - - fromBlock and toBlock are both inclusive: a request to /events?fromBlock=10&toBlock=15 should include all - available events from block 10, 11, 12, 13, 14 and 15 + - fromBlock and toBlock are both inclusive: a request to /events?fromBlock=10&toBlock=15 should + include all available events from block 10, 11, 12, 13, 14 and 15. Response Schema: // interface EventsResponse { @@ -91,36 +92,41 @@ import { compareBigInt } from "@econia-labs/emojicoin-sdk"; /** * - `txnId` is a transaction identifier such as a transaction hash - * - `txnIndex` refers to the order of a transaction within a block, the higher the index the later in the block the - * transaction was processed - * - `eventIndex` refer to the order of the event within a transaction, the higher the index the later in the - * transaction the event was processed - * - The combination of `txnIndex` + `eventIndex` must be unique to any given event within a block, including for - * different event types - * - `maker` is an identifier for the account responsible for submitting the transaction. In most cases the transaction - * is submitted by the same account that sent/received tokens, but in cases where this doesn't apply (i.e.: transaction - * submitted by an aggregator) an effort should be made to identify the underlying account - * - All amounts (`assetIn`/`assetOut`/`reserve`) should be decimalized (`amount / (10 ** assetDecimals)`) - * - Reserves refer to the pooled amount of each asset *after* a swap event has occured. If there are multiple swap - * events on the same block and reserves cannot be determined after each individual event then it's acceptable for only - * the last event to contain the `reserves` prop - * - While `reserves` are technically optional, the Indexer relies on it for accurately calculating derived USD - * pricing (i.e.: USD price for `FOO/BAR` derived from `BAR/USDC`) from the most suitable reference pair. Pairs with no - * known reserves, from both `SwapEvent` or `JoinExitEvent`, will **not** be used for derived pricing and this may - * result in pairs with no known USD prices - * - `reserves` are also required to calculate total liquidity for each pair: if that's not available then DEX - * Screener will show liquidity as `N/A` and a warning for “Unknown liquidity” - * - A combination of either `asset0In + asset1Out` or `asset1In + asset0Out` is expected. If there are multiple assets - * in or multiple assets out then the swap event is considered invalid and indexing will halt + * - `txnIndex` refers to the order of a transaction within a block, the higher the index the later + * in the block the transaction was processed + * - `eventIndex` refer to the order of the event within a transaction, the higher the index the + * later in the transaction the event was processed + * - The combination of `txnIndex` + `eventIndex` must be unique to any given event within a block, + * including for different event types + * - `maker` is an identifier for the account responsible for submitting the transaction. In most + * cases the transaction is submitted by the same account that sent/received tokens, but in cases + * where this doesn't apply (i.e.: transaction submitted by an aggregator) an effort should be made + * to identify the underlying account + * - All amounts (`assetIn`/`assetOut`/`reserve`) should be decimalized + * (`amount / (10 ** assetDecimals)`) + * - Reserves refer to the pooled amount of each asset *after* a swap event has occurred. If there + * are multiple swap events on the same block and reserves cannot be determined after each + * individual event then it's acceptable for only the last event to contain the `reserves` prop + * - While `reserves` are technically optional, the Indexer relies on it for accurately calculating + * derived USD pricing (i.e. USD price for `FOO/BAR` derived from `BAR/USDC`) from the most suitable + * reference pair. Pairs with no known reserves, from both `SwapEvent` or `JoinExitEvent`, will + * **not** be used for derived pricing and this may result in pairs with no known USD prices + * - `reserves` are also required to calculate total liquidity for each pair: if that's not + * available then DEX Screener will show liquidity as `N/A` and a warning for “Unknown liquidity” + * - A combination of either `asset0In + asset1Out` or `asset1In + asset0Out` is expected. If there + * are multiple assets in or multiple assets out then the swap event is considered invalid and + * indexing will halt * - `priceNative` refers to the price of `asset0` quoted in `asset1` in that event - * - For example, in a theoretical `BTC/USD` pair, `priceNative` would be `30000` (1 BTC = 30000 USD) - * - * - Similarly, in a theoretical `USD/BTC` pair, `priceNative` would be `0.00003333333333` (1 USD = - * 0.00003333333333 USD) - * - `metadata` includes any optional auxiliary info not covered in the default schema and not required in most cases - * - The Indexer will use up to 50 decimal places for amounts/prices/reserves and all subsequent decimal places will be - * ignored. Using all 50 decimal places is highly encouraged to ensure accurate prices - * - The Indexer automatically handles calculations for USD pricing (`priceUsd` as opposed to `priceNative`) + * - For example, in a theoretical `BTC/USD` pair, if 1 BTC = 30000 USD `priceNative` == `30000` + * - Similarly, in a theoretical `USD/BTC` pair, `priceNative` would be `0.00003333333333` + * (1 BTC = 0.00003333333333 USD) + * - `metadata` includes any optional auxiliary info not covered in the default schema and not + * required in most cases + * - The Indexer will use up to 50 decimal places for amounts/prices/reserves and all subsequent + * decimal places will be ignored. Using all 50 decimal places is highly encouraged to ensure + * accurate prices + * - The Indexer automatically handles calculations for USD pricing (`priceUsd` as opposed to + * `priceNative`) */ export interface SwapEvent { eventType: "swap"; @@ -143,20 +149,23 @@ export interface SwapEvent { /** * - `txnId` is a transaction identifier such as a transaction hash - * - `txnIndex` refers to the order of a transaction within a block, the higher the index the later in the block the - * transaction was processed - * - `eventIndex` refer to the order of the event within a transaction, the higher the index the later in the - * transaction the event was processed - * - The combination of `txnIndex` + `eventIndex` must be unique to any given event within a block, including for - * different event types - * - `maker` is an identifier for the account responsible for submitting the transaction. In most cases the transaction - * is submitted by the same account that sent/received tokens, but in cases where this doesn't apply (i.e.: transaction - * submitted by an aggregator) an effort should be made to identify the underlying account. - * - All amounts (`assetIn`/`assetOut`/`reserve`) should be decimalized (`amount / (10 ** assetDecimals)`) - * - Reserves refer to the pooled amount of each asset *after* a join/exit event has occured. If there are multiple - * join/exit events on the same block and reserves cannot be determined after each individual event then it's - * acceptable for only the last event to contain the `reserves` prop. - * - `metadata` includes any optional auxiliary info not covered in the default schema and not required in most cases + * - `txnIndex` refers to the order of a transaction within a block, the higher the index the later + * in the block the transaction was processed + * - `eventIndex` refer to the order of the event within a transaction, the higher the index the + * later in the transaction the event was processed + * - The combination of `txnIndex` + `eventIndex` must be unique to any given event within a block, + * including for different event types + * - `maker` is an identifier for the account responsible for submitting the transaction. In most + * cases the transaction is submitted by the same account that sent/received tokens, but in cases + * where this doesn't apply (i.e.: transaction submitted by an aggregator) an effort should be made + * to identify the underlying account. + * - All amounts (`assetIn`/`assetOut`/`reserve`) should be decimalized: + * (`amount / (10 ** assetDecimals)`) + * - Reserves refer to the pooled amount of each asset *after* a join/exit event has occurred. + * If there are multiple join/exit events on the same block and reserves cannot be determined after + * each individual event then it's acceptable for only the last event to contain the `reserves` prop + * - `metadata` includes any optional auxiliary info not covered in the default schema and not + * required in most cases */ export interface JoinExitEvent { eventType: "join" | "exit"; @@ -285,7 +294,7 @@ export async function getEventsByVersion(fromBlock: number, toBlock: number): Pr // NextJS JSON response handler /** - * We treat our versions as "blocks", because it's faster to implement given our current architecture + * 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 * */ export async function GET(request: NextRequest): Promise> { diff --git a/src/typescript/frontend/src/app/dexscreener/latest-block.ts b/src/typescript/frontend/src/app/dexscreener/latest-block.ts index 62ae23673..0b4bff762 100644 --- a/src/typescript/frontend/src/app/dexscreener/latest-block.ts +++ b/src/typescript/frontend/src/app/dexscreener/latest-block.ts @@ -1,3 +1,4 @@ +// cspell:word dexscreener /*** - The `/latest-block` endpoint should be in sync with the `/events` endpoint, meaning it should only return the latest block where data from `/events` will be available. This doesn't mean it should return the latest block with an event, but it should not return a block for which `/events` has no data available yet. - If `/events` fetches data on-demand this isn't an issue, but if it relies on data indexed and persisted in the backend then `/latest-block` should be aware of the latest persisted block @@ -38,8 +39,8 @@ export async function GET(_request: NextRequest): Promise 0 ? blockHeight - 1 : 0; diff --git a/src/typescript/frontend/src/app/dexscreener/pair.ts b/src/typescript/frontend/src/app/dexscreener/pair.ts index 4874e104b..1f61a33ed 100644 --- a/src/typescript/frontend/src/app/dexscreener/pair.ts +++ b/src/typescript/frontend/src/app/dexscreener/pair.ts @@ -1,3 +1,4 @@ +// cspell:word dexscreener /*** Request: GET /pair?id=:string diff --git a/src/typescript/frontend/src/app/dexscreener/util.test.ts b/src/typescript/frontend/src/app/dexscreener/util.test.ts index 7d1b69524..203c1a88d 100644 --- a/src/typescript/frontend/src/app/dexscreener/util.test.ts +++ b/src/typescript/frontend/src/app/dexscreener/util.test.ts @@ -1,3 +1,4 @@ +// cspell:word dexscreener import type { SymbolEmoji } from "@sdk/emoji_data"; import { describe, it } from "node:test"; import { pairIdToSymbolEmojis, symbolEmojisToPairId, symbolEmojisToString } from "./util"; diff --git a/src/typescript/frontend/src/middleware.ts b/src/typescript/frontend/src/middleware.ts index 5a0bded2d..a4b413201 100644 --- a/src/typescript/frontend/src/middleware.ts +++ b/src/typescript/frontend/src/middleware.ts @@ -1,3 +1,4 @@ +// cspell:word dexscreener import { COOKIE_FOR_ACCOUNT_ADDRESS, COOKIE_FOR_HASHED_ADDRESS, diff --git a/src/typescript/frontend/src/router/routes.ts b/src/typescript/frontend/src/router/routes.ts index be06d7c98..b4fe25e16 100644 --- a/src/typescript/frontend/src/router/routes.ts +++ b/src/typescript/frontend/src/router/routes.ts @@ -1,3 +1,4 @@ +// cspell:word dexscreener export const ROUTES = { root: "/", api: "/api", diff --git a/src/typescript/sdk/src/indexer-v2/queries/app/index.ts b/src/typescript/sdk/src/indexer-v2/queries/app/index.ts index ca8ca2489..0a4b81230 100644 --- a/src/typescript/sdk/src/indexer-v2/queries/app/index.ts +++ b/src/typescript/sdk/src/indexer-v2/queries/app/index.ts @@ -1,3 +1,4 @@ +// cspell:word dexscreener export * from "./home"; export * from "./market"; export * from "./pools";