From b245695f1c44ef798de215ac0096ce21be6039cb Mon Sep 17 00:00:00 2001 From: Gabe Rodriguez Date: Wed, 6 Nov 2024 09:52:54 -0800 Subject: [PATCH] Candles v2 (time windows) (#128) * Query Pindexer db for candles * Add symbol hook * working candles w/ buttons * Remove old candle api route * remove more * docs: update database reqs to pindexer * Review updates --------- Co-authored-by: Conor Schaefer --- README.md | 11 +- .../[symbol2]/[startHeight]/[limit]/route.ts | 1 - app/api/candles/route.ts | 1 + app/trade/page.ts | 6 +- src/pages/trade/api/book.ts | 25 +-- src/pages/trade/api/candles.tsx | 34 ++-- .../{use-path-to-metadata.ts => use-path.ts} | 16 +- src/pages/trade/model/useSummary.ts | 22 +-- src/pages/trade/redirect.tsx | 41 +++++ src/pages/trade/ui/chart.tsx | 166 ++++++++---------- src/pages/trade/ui/page.tsx | 2 +- src/pages/trade/ui/pair-selector.tsx | 2 +- src/pages/trade/ui/route-book.tsx | 18 +- src/shared/api/indexer/connector.tsx | 116 ------------ src/shared/api/indexer/lps.tsx | 72 -------- src/shared/api/server/book/index.ts | 21 ++- src/shared/api/server/candles.ts | 136 -------------- src/shared/api/server/candles/index.ts | 73 ++++++++ src/shared/api/server/candles/types.ts | 13 ++ src/shared/api/server/candles/utils.ts | 88 ++++++++++ src/shared/api/server/token-fetch.tsx | 55 ------ src/shared/const/token.d.ts | 7 - src/shared/database/index.ts | 13 +- src/shared/database/schema.ts | 7 +- src/shared/utils/candles/index.ts | 83 --------- .../protos/services/app/shielded-pool.ts | 21 --- .../services/dex/dex-query-service-client.ts | 119 ------------- .../protos/services/dex/simulated-trades.ts | 19 -- .../types/DexQueryServiceClientInterface.ts | 33 ---- .../utils/protos/types/ShieldedPoolQuerier.ts | 7 - .../utils/protos/{services => }/utils.ts | 0 31 files changed, 388 insertions(+), 840 deletions(-) delete mode 100644 app/api/candles/[symbol1]/[symbol2]/[startHeight]/[limit]/route.ts create mode 100644 app/api/candles/route.ts rename src/pages/trade/model/{use-path-to-metadata.ts => use-path.ts} (62%) create mode 100644 src/pages/trade/redirect.tsx delete mode 100644 src/shared/api/indexer/connector.tsx delete mode 100644 src/shared/api/indexer/lps.tsx delete mode 100644 src/shared/api/server/candles.ts create mode 100644 src/shared/api/server/candles/index.ts create mode 100644 src/shared/api/server/candles/types.ts create mode 100644 src/shared/api/server/candles/utils.ts delete mode 100644 src/shared/api/server/token-fetch.tsx delete mode 100644 src/shared/const/token.d.ts delete mode 100644 src/shared/utils/candles/index.ts delete mode 100644 src/shared/utils/protos/services/app/shielded-pool.ts delete mode 100644 src/shared/utils/protos/services/dex/dex-query-service-client.ts delete mode 100644 src/shared/utils/protos/services/dex/simulated-trades.ts delete mode 100644 src/shared/utils/protos/types/DexQueryServiceClientInterface.ts delete mode 100644 src/shared/utils/protos/types/ShieldedPoolQuerier.ts rename src/shared/utils/protos/{services => }/utils.ts (100%) diff --git a/README.md b/README.md index 4fddc4c7..6b6bc969 100644 --- a/README.md +++ b/README.md @@ -18,7 +18,7 @@ However, you still need a database to connect to. ## Connecting to a database The DEX explorer application requires a PostgreSQL database containing ABCI event information -[as emitted by a Penumbra node](https://guide.penumbra.zone/node/pd/indexing-events). +as written by [pindexer]. You can set up a local devnet by following the [Penumbra devnet quickstart guide](https://guide.penumbra.zone/dev/devnet-quickstart), or plug in credentials for an already running database via environment variables: @@ -60,10 +60,11 @@ It'd be nice to have a cool name for the DEX explorer. We don't have one yet. Using https://buf.build/penumbra-zone/penumbra/sdks/main -[NextJS]: https://nextjs.org/ -[pnpm]: https://pnpm.io/ -[Nix]: https://nixos.org/download/ - ## Code structure Read the sub-article about the code structure [here](./pages/readme.md). + +[NextJS]: https://nextjs.org/ +[Nix]: https://nixos.org/download/ +[pindexer]: https://guide.penumbra.zone/node/pd/indexing-events#using-pindexer +[pnpm]: https://pnpm.io/ diff --git a/app/api/candles/[symbol1]/[symbol2]/[startHeight]/[limit]/route.ts b/app/api/candles/[symbol1]/[symbol2]/[startHeight]/[limit]/route.ts deleted file mode 100644 index 326798e6..00000000 --- a/app/api/candles/[symbol1]/[symbol2]/[startHeight]/[limit]/route.ts +++ /dev/null @@ -1 +0,0 @@ -export { GET } from '@/shared/api/server/candles'; diff --git a/app/api/candles/route.ts b/app/api/candles/route.ts new file mode 100644 index 00000000..cabe8b79 --- /dev/null +++ b/app/api/candles/route.ts @@ -0,0 +1 @@ +export { GET } from '@/shared/api/server/candles/index.ts'; diff --git a/app/trade/page.ts b/app/trade/page.ts index a8054a4a..52c11093 100644 --- a/app/trade/page.ts +++ b/app/trade/page.ts @@ -1,5 +1,3 @@ -import { redirect } from 'next/navigation'; +import { RedirectToPair } from '@/pages/trade/redirect.tsx'; -export default function RedirectPage() { - redirect('/trade/UM/GM'); -} +export default RedirectToPair; diff --git a/src/pages/trade/api/book.ts b/src/pages/trade/api/book.ts index 8c9b4818..6d7bb902 100644 --- a/src/pages/trade/api/book.ts +++ b/src/pages/trade/api/book.ts @@ -1,27 +1,28 @@ import { useQuery } from '@tanstack/react-query'; import { useRefetchOnNewBlock } from '@/shared/api/compact-block.ts'; -import { RouteBookResponse, RouteBookResponseJson } from '@/shared/api/server/book/types'; +import { RouteBookResponse } from '@/shared/api/server/book/types'; import { deserializeRouteBookResponseJson } from '@/shared/api/server/book/serialization.ts'; +import { RouteBookApiResponse } from '@/shared/api/server/book'; +import { usePathSymbols } from '@/pages/trade/model/use-path.ts'; -export const useBook = (symbol1: string | undefined, symbol2: string | undefined) => { +export const useBook = () => { + const { baseSymbol, quoteSymbol } = usePathSymbols(); const query = useQuery({ - queryKey: ['book', symbol1, symbol2], + queryKey: ['book', baseSymbol, quoteSymbol], queryFn: async (): Promise => { - if (!symbol1 || !symbol2) { - throw new Error('Missing symbols'); - } - const paramsObj = { - baseAsset: symbol1, - quoteAsset: symbol2, + baseAsset: baseSymbol, + quoteAsset: quoteSymbol, }; const baseUrl = '/api/book'; const urlParams = new URLSearchParams(paramsObj).toString(); const res = await fetch(`${baseUrl}?${urlParams}`); - const data = (await res.json()) as RouteBookResponseJson; - return deserializeRouteBookResponseJson(data); + const jsonRes = (await res.json()) as RouteBookApiResponse; + if ('error' in jsonRes) { + throw new Error(jsonRes.error); + } + return deserializeRouteBookResponseJson(jsonRes); }, - enabled: !!symbol1 && !!symbol2, }); useRefetchOnNewBlock(query); diff --git a/src/pages/trade/api/candles.tsx b/src/pages/trade/api/candles.tsx index 58b2b53b..4afc12d5 100644 --- a/src/pages/trade/api/candles.tsx +++ b/src/pages/trade/api/candles.tsx @@ -1,21 +1,29 @@ import { useQuery } from '@tanstack/react-query'; -import { CandlestickData } from '@penumbra-zone/protobuf/penumbra/core/component/dex/v1/dex_pb'; import { useRefetchOnNewBlock } from '@/shared/api/compact-block.ts'; +import { CandleApiResponse } from '@/shared/api/server/candles/types.ts'; +import { usePathSymbols } from '@/pages/trade/model/use-path.ts'; +import { OhlcData } from 'lightweight-charts'; +import { DurationWindow } from '@/shared/database/schema.ts'; + +export const useCandles = (durationWindow: DurationWindow) => { + const { baseSymbol, quoteSymbol } = usePathSymbols(); -export const useCandles = ( - symbol1: string, - symbol2: string, - startBlock: number | undefined, - limit: number, -) => { const query = useQuery({ - queryKey: ['candles', symbol1, symbol2, startBlock, limit], - queryFn: async (): Promise => { - if (startBlock === undefined) { - return []; + queryKey: ['candles', baseSymbol, quoteSymbol, durationWindow], + queryFn: async (): Promise => { + const paramsObj = { + baseAsset: baseSymbol, + quoteAsset: quoteSymbol, + durationWindow, + }; + const baseUrl = '/api/candles'; + const urlParams = new URLSearchParams(paramsObj).toString(); + const res = await fetch(`${baseUrl}?${urlParams}`); + const jsonRes = (await res.json()) as CandleApiResponse; + if ('error' in jsonRes) { + throw new Error(jsonRes.error); } - const res = await fetch(`/api/candles/${symbol1}/${symbol2}/${startBlock}/${limit}`); - return (await res.json()) as CandlestickData[]; + return jsonRes; }, }); diff --git a/src/pages/trade/model/use-path-to-metadata.ts b/src/pages/trade/model/use-path.ts similarity index 62% rename from src/pages/trade/model/use-path-to-metadata.ts rename to src/pages/trade/model/use-path.ts index e3c181ef..debab94a 100644 --- a/src/pages/trade/model/use-path-to-metadata.ts +++ b/src/pages/trade/model/use-path.ts @@ -8,17 +8,25 @@ interface PathParams { [key: string]: string; // required for useParams signature } +export const usePathSymbols = () => { + const params = useParams(); + if (!params) { + throw new Error('No symbol params in path'); + } + return { baseSymbol: params.baseSymbol, quoteSymbol: params.quoteSymbol }; +}; + // Converts symbol to Metadata export const usePathToMetadata = () => { const { data, error, isLoading } = useAssets(); - const params = useParams(); + const { baseSymbol, quoteSymbol } = usePathSymbols(); const query = useQuery({ - queryKey: ['pathToMetadata', data, params], + queryKey: ['pathToMetadata', data, baseSymbol, quoteSymbol], queryFn: () => { return { - baseAsset: data?.find(a => a.symbol === params?.baseSymbol), - quoteAsset: data?.find(a => a.symbol === params?.quoteSymbol), + baseAsset: data?.find(m => m.symbol === baseSymbol), + quoteAsset: data?.find(a => a.symbol === quoteSymbol), }; }, }); diff --git a/src/pages/trade/model/useSummary.ts b/src/pages/trade/model/useSummary.ts index 73570efe..69392318 100644 --- a/src/pages/trade/model/useSummary.ts +++ b/src/pages/trade/model/useSummary.ts @@ -1,22 +1,17 @@ import { useQuery } from '@tanstack/react-query'; -import { usePathToMetadata } from '@/pages/trade/model/use-path-to-metadata.ts'; +import { usePathSymbols } from '@/pages/trade/model/use-path.ts'; import { SummaryResponse } from '@/shared/api/server/summary.ts'; export const useSummary = () => { - const { baseAsset, quoteAsset, error: pathError } = usePathToMetadata(); + const { baseSymbol, quoteSymbol } = usePathSymbols(); - const res = useQuery({ - queryKey: ['summary', baseAsset, quoteAsset], - enabled: !!baseAsset && !!quoteAsset, + return useQuery({ + queryKey: ['summary', baseSymbol, quoteSymbol], retry: 1, queryFn: async () => { - if (!baseAsset || !quoteAsset) { - throw new Error('Missing assets to get summary for'); - } - const paramsObj = { - baseAsset: baseAsset.symbol, - quoteAsset: quoteAsset.symbol, + baseAsset: baseSymbol, + quoteAsset: quoteSymbol, }; const baseUrl = '/api/summary'; const urlParams = new URLSearchParams(paramsObj).toString(); @@ -28,9 +23,4 @@ export const useSummary = () => { return jsonRes; }, }); - - return { - ...res, - error: pathError ?? res.error, - }; }; diff --git a/src/pages/trade/redirect.tsx b/src/pages/trade/redirect.tsx new file mode 100644 index 00000000..e036a58b --- /dev/null +++ b/src/pages/trade/redirect.tsx @@ -0,0 +1,41 @@ +'use client'; + +import { redirect } from 'next/navigation'; +import { ChainRegistryClient } from '@penumbra-labs/registry'; +import { envQueryFn } from '@/shared/api/env/env.ts'; +import { useQuery } from '@tanstack/react-query'; +import { assetPatterns } from '@penumbra-zone/types/assets'; + +const redirectSymbolsQueryFn = async () => { + const { PENUMBRA_CHAIN_ID } = await envQueryFn(); + const chainRegistryClient = new ChainRegistryClient(); + const registry = await chainRegistryClient.remote.get(PENUMBRA_CHAIN_ID); + const allAssets = registry + .getAllAssets() + .filter(m => !assetPatterns.delegationToken.matches(m.display)) + .toSorted((a, b) => Number(b.priorityScore - a.priorityScore)); + + const baseAsset = allAssets[0]?.symbol; + const quoteAsset = allAssets[1]?.symbol; + if (!baseAsset || !quoteAsset) { + throw new Error('Could not find symbols in registry'); + } + + return { baseAsset, quoteAsset }; +}; + +export const RedirectToPair = () => { + const { data, isLoading, error } = useQuery({ + queryKey: ['redirectSymbols'], + retry: 1, + queryFn: redirectSymbolsQueryFn, + }); + + if (error) { + return
{String(error)}
; + } else if (isLoading || !data) { + return
Loading...
; + } else { + redirect(`/trade/${data.baseAsset}/${data.quoteAsset}`); + } +}; diff --git a/src/pages/trade/ui/chart.tsx b/src/pages/trade/ui/chart.tsx index e813d4f2..ef2efdb2 100644 --- a/src/pages/trade/ui/chart.tsx +++ b/src/pages/trade/ui/chart.tsx @@ -1,117 +1,97 @@ -import { useEffect, useRef } from 'react'; -import { CandlestickData, createChart, IChartApi } from 'lightweight-charts'; -import { Metadata } from '@penumbra-zone/protobuf/penumbra/core/asset/v1/asset_pb'; +import { useEffect, useRef, useState } from 'react'; +import { createChart, IChartApi, OhlcData } from 'lightweight-charts'; import { tailwindConfig } from '@penumbra-zone/ui/tailwind'; -import { usePathToMetadata } from '../model/use-path-to-metadata'; import { useCandles } from '../api/candles'; import { observer } from 'mobx-react-lite'; +import { DurationWindow, durationWindows } from '@/shared/database/schema.ts'; +import { Button } from '@penumbra-zone/ui/Button'; const { colors } = tailwindConfig.theme.extend; -interface ChartProps { - height: number; -} +const CHART_HEIGHT = 512; -const ChartLoadingState = ({ height }: ChartProps) => { +const ChartLoadingState = () => { return ( -
-
+
+
Loading...
); }; -const ChartData = observer( - ({ - height, - baseAsset, - quoteAsset, - }: { - height: number; - baseAsset: Metadata; - quoteAsset: Metadata; - }) => { - const chartElRef = useRef(null); - const chartRef = useRef(null); +const ChartData = observer(({ candles }: { candles: OhlcData[] }) => { + const chartElRef = useRef(null); + const chartRef = useRef(null); - const { - data: candles, - isLoading, - error, - } = useCandles(baseAsset.symbol, quoteAsset.symbol, 0, 10000); - - useEffect(() => { - if (chartElRef.current && !chartRef.current) { - chartRef.current = createChart(chartElRef.current, { - autoSize: true, - layout: { - textColor: colors.text.primary, - background: { - color: 'transparent', - }, + useEffect(() => { + if (chartElRef.current && !chartRef.current) { + chartRef.current = createChart(chartElRef.current, { + autoSize: true, + layout: { + textColor: colors.text.primary, + background: { + color: 'transparent', }, - grid: { - vertLines: { - color: colors.other.tonalStroke, - }, - horzLines: { - color: colors.other.tonalStroke, - }, + }, + grid: { + vertLines: { + color: colors.other.tonalStroke, }, - }); - } - - return () => { - if (chartRef.current) { - chartRef.current.remove(); - chartRef.current = null; - } - }; - }, [chartElRef]); - - useEffect(() => { - if (chartRef.current && !isLoading && candles) { - chartRef.current - .addCandlestickSeries({ - upColor: colors.success.light, - downColor: colors.destructive.light, - borderVisible: false, - wickUpColor: colors.success.light, - wickDownColor: colors.destructive.light, - }) - .setData(candles as unknown[] as CandlestickData[]); + horzLines: { + color: colors.other.tonalStroke, + }, + }, + }); + } - chartRef.current.timeScale().fitContent(); + return () => { + if (chartRef.current) { + chartRef.current.remove(); + chartRef.current = null; } - }, [chartRef, isLoading, candles]); + }; + }, [chartElRef]); - return ( -
- {isLoading && ( -
-
Loading...
-
- )} - {error && ( -
-
{String(error)}
-
- )} -
- ); - }, -); + useEffect(() => { + if (chartRef.current) { + chartRef.current + .addCandlestickSeries({ + upColor: colors.success.light, + downColor: colors.destructive.light, + borderVisible: false, + wickUpColor: colors.success.light, + wickDownColor: colors.destructive.light, + }) + .setData(candles); -export const Chart = observer(({ height }: ChartProps) => { - const { baseAsset, quoteAsset, error, isLoading: pairIsLoading } = usePathToMetadata(); - if (pairIsLoading || !baseAsset || !quoteAsset) { - return ; - } + chartRef.current.timeScale().fitContent(); + } + }, [chartRef, candles]); - if (error) { - return
Error loading pair selector: ${String(error)}
; - } + return
; +}); + +export const Chart = observer(() => { + const [duration, setDuration] = useState('1d'); + const { data, isLoading, error } = useCandles(duration); - return ; + return ( +
+
+ {durationWindows.map(w => ( + + ))} +
+ {error &&
Error loading pair selector: ${String(error)}
} + {isLoading && } + {data && } +
+ ); }); diff --git a/src/pages/trade/ui/page.tsx b/src/pages/trade/ui/page.tsx index 76a2323f..1d6ca218 100644 --- a/src/pages/trade/ui/page.tsx +++ b/src/pages/trade/ui/page.tsx @@ -19,7 +19,7 @@ export const TradePage = () => {
- +
diff --git a/src/pages/trade/ui/pair-selector.tsx b/src/pages/trade/ui/pair-selector.tsx index 266e6975..cd9544d7 100644 --- a/src/pages/trade/ui/pair-selector.tsx +++ b/src/pages/trade/ui/pair-selector.tsx @@ -14,7 +14,7 @@ import { Button } from '@penumbra-zone/ui/Button'; import { useAssets } from '@/shared/api/assets'; import { useBalances } from '@/shared/api/balances'; import { PagePath } from '@/shared/const/pages.ts'; -import { usePathToMetadata } from '../model/use-path-to-metadata'; +import { usePathToMetadata } from '../model/use-path.ts'; const handleRouting = ({ router, diff --git a/src/pages/trade/ui/route-book.tsx b/src/pages/trade/ui/route-book.tsx index 7ed9b8ec..3b1c03aa 100644 --- a/src/pages/trade/ui/route-book.tsx +++ b/src/pages/trade/ui/route-book.tsx @@ -1,4 +1,3 @@ -import { usePathToMetadata } from '../model/use-path-to-metadata'; import { useBook } from '../api/book'; import { observer } from 'mobx-react-lite'; import { RouteBookResponse } from '@/shared/api/server/book/types'; @@ -47,20 +46,15 @@ const RouteBookData = observer(({ bookData: { multiHops } }: { bookData: RouteBo }); export const RouteBook = observer(() => { - const { baseAsset, quoteAsset, error: pairError } = usePathToMetadata(); - const { - data: bookData, - isLoading: bookIsLoading, - error: bookErr, - } = useBook(baseAsset?.symbol, quoteAsset?.symbol); + const { data, isLoading, error } = useBook(); - if (bookIsLoading || !bookData) { - return ; + if (error) { + return
Error loading route book: ${String(error)}
; } - if (bookErr ?? pairError) { - return
Error loading route book: ${String(bookErr ?? pairError)}
; + if (isLoading || !data) { + return ; } - return ; + return ; }); diff --git a/src/shared/api/indexer/connector.tsx b/src/shared/api/indexer/connector.tsx deleted file mode 100644 index 8df4b972..00000000 --- a/src/shared/api/indexer/connector.tsx +++ /dev/null @@ -1,116 +0,0 @@ -import { Pool, QueryConfigValues, QueryResultRow } from 'pg'; -import fs from 'fs'; -import { BlockInfo } from './lps'; -import { JsonValue } from '@bufbuild/protobuf'; - -const INDEXER_CERT = process.env['PENUMBRA_INDEXER_CA_CERT']; - -// TODO: Delete when possible, this is deprecated -export class IndexerQuerier { - private pool: Pool; - - constructor(connectionString: string) { - const dbConfig = { - connectionString: connectionString, - // If a CA certificate was specified as an env var, pass that info to the database config. - // Be advised that if PENUMBRA_INDEXER_CA_CERT is set, then PENUMBRA_INDEXER_ENDPOINT must - // *lack* an `sslmode` param! This is documented here: - // https://node-postgres.com/features/ssl#usage-with-connectionstring - ...(INDEXER_CERT && { - ssl: { - rejectUnauthorized: true, - ca: INDEXER_CERT.startsWith('-----BEGIN CERTIFICATE-----') - ? INDEXER_CERT - : fs.readFileSync(INDEXER_CERT, 'utf-8'), - }, - }), - }; - this.pool = new Pool(dbConfig); - } - - /** - * @param {string} query - The SQL query string. - * @returns {Promise>} - The result set of the query as an array. - */ - private async query(queryText: string, params: QueryConfigValues

): Promise { - const client = await this.pool.connect(); - try { - const res = await client.query(queryText, params); - - // TODO: This feels like a bad pattern - // convert timestamps to ISO strings - res.rows.forEach(row => { - Object.keys(row).forEach(key => { - if (row[key] instanceof Date) { - row[key] = row[key].toISOString(); - } - }); - }); - - return this.recursivelyParseJSON(res.rows) as T; - } finally { - client.release(); - } - } - - private recursivelyParseJSON(value: JsonValue): JsonValue { - if (typeof value === 'string') { - try { - // Attempt to parse the string as JSON - const parsed = JSON.parse(value) as JsonValue; - // If parsing succeeds, continue recursively parsing this object - return this.recursivelyParseJSON(parsed); - } catch (error) { - // If it's not parseable JSON, return the original string - return value; - } - } else if (Array.isArray(value)) { - // If it's an array, apply recursively to each element - return value.map(item => this.recursivelyParseJSON(item)); - } else if (typeof value === 'object' && value !== null) { - // If it's an object, apply recursively to each value - const parsedObject: Record = {}; - for (const key of Object.keys(value)) { - const fieldValue = value[key]; - if (fieldValue === undefined) { - continue; - } - parsedObject[key] = this.recursivelyParseJSON(fieldValue); - } - return parsedObject as JsonValue; - } - // If none of the above, return the value as is (e.g., numbers, null) - return value; - } - - public async fetchMostRecentNBlocks(n: number): Promise { - const queryText = ` - SELECT - height, - created_at - FROM blocks - ORDER BY height DESC - LIMIT $1; - `; - return await this.query(queryText, [`${n}`]); - } - - public async fetchBlocksByHeight(heights: number[]): Promise { - const queryText = ` - SELECT - height, - created_at - FROM blocks - WHERE height = ANY($1) - ORDER BY height DESC - `; - return this.query(queryText, [heights]); - } - - /** - * Closes the database connection pool. - */ - public async close(): Promise { - await this.pool.end(); - } -} diff --git a/src/shared/api/indexer/lps.tsx b/src/shared/api/indexer/lps.tsx deleted file mode 100644 index e2cf63e5..00000000 --- a/src/shared/api/indexer/lps.tsx +++ /dev/null @@ -1,72 +0,0 @@ -// @ts-nocheck -/* eslint-disable -- disabling this file as this was created before our strict rules */ -export interface LiquidityPositionEvent { - block_height: number; - event_id: number; // ! Needed for sorting - block_id: number; - tx_id: number; - type: string; - tx_hash: string; - created_at: string; - index: number; - lpevent_attributes: { - positionId: { - inner: string; - }; - reserves1?: { - hi?: number; - lo?: number; - }; - reserves2?: { - hi?: number; - lo?: number; - }; - tradingFee?: number; - tradingPair?: { - asset1: { - inner: string; - }; - asset2: { - inner: string; - }; - }; - }; -} - -export interface PositionExecutionEvent { - block_height: number; - event_id: number; // ! Needed for sorting - block_id: number; - tx_id: number; - type: string; - tx_hash: string; - created_at: string; - index: number; - execution_event_attributes: { - positionId: { - inner: string; - }; - reserves1?: { - hi?: number; - lo?: number; - }; - reserves2?: { - hi?: number; - lo?: number; - }; - tradingFee?: number; - tradingPair?: { - asset1: { - inner: string; - }; - asset2: { - inner: string; - }; - }; - }; -} - -export interface BlockInfo { - height: number; - created_at: string; -} diff --git a/src/shared/api/server/book/index.ts b/src/shared/api/server/book/index.ts index f3dae9f1..4a9fc299 100644 --- a/src/shared/api/server/book/index.ts +++ b/src/shared/api/server/book/index.ts @@ -1,6 +1,5 @@ import { NextRequest, NextResponse } from 'next/server'; import { ChainRegistryClient } from '@penumbra-labs/registry'; -import { SimulationQuerier } from '@/shared/utils/protos/services/dex/simulated-trades.ts'; import { SimulateTradeRequest, SimulateTradeResponse, @@ -10,13 +9,16 @@ import { Amount } from '@penumbra-zone/protobuf/penumbra/core/num/v1/num_pb'; import { RouteBookResponseJson } from '@/shared/api/server/book/types.ts'; import { processSimulation } from '@/shared/api/server/book/helpers.ts'; import { serializeResponse } from '@/shared/api/server/book/serialization.ts'; +import { SimulationService } from '@penumbra-zone/protobuf'; +import { PromiseClient } from '@connectrpc/connect'; +import { createClient } from '@/shared/utils/protos/utils.ts'; export const VERY_HIGH_AMOUNT = new Amount({ hi: 10000n }); // Used as default to generate sufficient amount of traces export const TRACE_LIMIT_DEFAULT = 8; -type AllResponses = RouteBookResponseJson | { error: string }; +export type RouteBookApiResponse = RouteBookResponseJson | { error: string }; -export async function GET(req: NextRequest): Promise> { +export async function GET(req: NextRequest): Promise> { const grpcEndpoint = process.env['PENUMBRA_GRPC_ENDPOINT']; const chainId = process.env['PENUMBRA_CHAIN_ID']; if (!grpcEndpoint || !chainId) { @@ -72,10 +74,10 @@ export async function GET(req: NextRequest): Promise> output: baseAssetMetadata.penumbraAssetId, }); - const simQuerier = new SimulationQuerier({ grpcEndpoint }); + const client = createClient(grpcEndpoint, SimulationService); const [buyRes, sellRes] = await Promise.all([ - simulateTrade(simQuerier, buySideRequest), - simulateTrade(simQuerier, sellSideRequest), + simulateTrade(client, buySideRequest), + simulateTrade(client, sellSideRequest), ]); const buyMulti = processSimulation({ res: buyRes, registry, limit }); const sellMulti = processSimulation({ res: sellRes, registry, limit, invertPrice: true }); @@ -91,9 +93,12 @@ export async function GET(req: NextRequest): Promise> ); } -const simulateTrade = async (querier: SimulationQuerier, req: SimulateTradeRequest) => { +const simulateTrade = async ( + client: PromiseClient, + req: SimulateTradeRequest, +) => { try { - return await querier.simulateTrade(req); + return await client.simulateTrade(req); } catch (e) { // If the error contains 'there are no orders to fulfill this swap', there are no orders to fulfill the trade, // so just return an empty array diff --git a/src/shared/api/server/candles.ts b/src/shared/api/server/candles.ts deleted file mode 100644 index 4e80b65b..00000000 --- a/src/shared/api/server/candles.ts +++ /dev/null @@ -1,136 +0,0 @@ -import { NextRequest, NextResponse } from 'next/server'; -import { DexQueryServiceClient } from '@/shared/utils/protos/services/dex/dex-query-service-client'; -import { IndexerQuerier } from '@/shared/api/indexer/connector'; -import { - CandlestickData, - DirectedTradingPair, -} from '@penumbra-zone/protobuf/penumbra/core/component/dex/v1/dex_pb'; -import { AssetId } from '@penumbra-zone/protobuf/penumbra/core/asset/v1/asset_pb'; -import { fetchAllTokenAssets_deprecated } from '@/shared/api/server/token-fetch'; -import { createMergeCandles } from '@/shared/utils/candles'; -import { Token } from '@/shared/const/token'; -import { base64ToUint8Array } from '@penumbra-zone/types/base64'; - -interface QueryParams { - symbol1?: string; - symbol2?: string; - startHeight?: string; - limit?: string; -} - -interface Candle extends Omit { - height: string; - time: number; -} - -function getTokenAssetBySymbol(tokenAssets: Token[], symbol: string): Token | undefined { - const regex = new RegExp(`^${symbol}$`, 'i'); - return tokenAssets.find(asset => regex.test(asset.symbol)); -} - -function getDirectedTradingPair(assetIn: Token, assetOut: Token) { - const tradingPair = new DirectedTradingPair(); - tradingPair.start = new AssetId(); - tradingPair.start.inner = base64ToUint8Array(assetIn.inner); - tradingPair.end = new AssetId(); - tradingPair.end.inner = base64ToUint8Array(assetOut.inner); - return tradingPair; -} - -function fillBlocks(startHeight: number, endHeight: number) { - return Array.from({ length: endHeight - startHeight + 1 }, (_, i) => startHeight + i); -} - -async function getTimestampsByBlockheight( - startHeight: number, - limit: number, -): Promise> { - // TODO: Old indexer is deprecated - const indexerQuerier = new IndexerQuerier(process.env['PENUMBRA_INDEXER_ENDPOINT'] ?? ''); - - if (startHeight === 0) { - const endHeight = await indexerQuerier - .fetchMostRecentNBlocks(1) - .then(resp => Number(resp[0]?.height)); - - if (!endHeight) { - return {}; - } - - const startHeight = endHeight - limit; - const data = await indexerQuerier.fetchBlocksByHeight(fillBlocks(startHeight, endHeight)); - const timestampsByHeight = Object.fromEntries( - data.map(block => [block.height.toString(), block.created_at]), - ); - - return timestampsByHeight; - } - - const endHeight = startHeight + limit; - const data = await indexerQuerier.fetchBlocksByHeight(fillBlocks(startHeight, endHeight)); - const timestampsByHeight = Object.fromEntries( - data.map(block => [block.height.toString(), block.created_at]), - ); - return timestampsByHeight; -} - -export async function GET(_req: NextRequest, context: { params: Promise }) { - const { - symbol1, - symbol2, - startHeight: startHeightQuery, - limit: limitQuery, - } = await context.params; - const startHeight = Number(startHeightQuery); - const limit = Number(limitQuery); - - if ((!startHeight && startHeight !== 0) || !symbol1 || !symbol2 || !limit) { - return NextResponse.json({ error: 'Invalid query parameters' }, { status: 400 }); - } - - // Set a HARD limit to prevent abuse - if (limit > 10000) { - return NextResponse.json({ error: 'Limit exceeded' }, { status: 400 }); - } - - const tokenAssets = fetchAllTokenAssets_deprecated(process.env['PENUMBRA_CHAIN_ID'] ?? ''); - const asset1 = getTokenAssetBySymbol(tokenAssets, symbol1); - const asset2 = getTokenAssetBySymbol(tokenAssets, symbol2); - - if (!asset1 || !asset2) { - return NextResponse.json( - { - error: `Invalid token pair ${symbol1}:${symbol2}`, - }, - { status: 400 }, - ); - } - - const dexQuerier = new DexQueryServiceClient({ - grpcEndpoint: process.env['PENUMBRA_GRPC_ENDPOINT'] ?? '', - }); - const tradingPair = getDirectedTradingPair(asset1, asset2); - const reversePair = getDirectedTradingPair(asset2, asset1); - - const [candlesFwd, candlesRev, timestampsByHeight] = await Promise.all([ - dexQuerier.candlestickData(tradingPair, startHeight, limit), - dexQuerier.candlestickData(reversePair, startHeight, limit), - getTimestampsByBlockheight(startHeight, limit), - ]); - - const mergeCandles = createMergeCandles(asset1, asset2); - const mergedCandles = mergeCandles(candlesFwd, candlesRev); - - const candlesWithTime = mergedCandles - .map( - (candle: CandlestickData) => - ({ - ...candle, - height: candle.height.toString(), - time: new Date(timestampsByHeight[candle.height.toString()] ?? '').getTime(), - }) as Candle, - ) - .filter(candle => !Number.isNaN(candle.time)); - - return NextResponse.json(candlesWithTime); -} diff --git a/src/shared/api/server/candles/index.ts b/src/shared/api/server/candles/index.ts new file mode 100644 index 00000000..5f210848 --- /dev/null +++ b/src/shared/api/server/candles/index.ts @@ -0,0 +1,73 @@ +import { NextRequest, NextResponse } from 'next/server'; +import { ChainRegistryClient } from '@penumbra-labs/registry'; +import { pindexer } from '@/shared/database'; +import { CandleApiResponse } from '@/shared/api/server/candles/types.ts'; +import { durationWindows, isDurationWindow } from '@/shared/database/schema.ts'; +import { dbCandleToOhlc, mergeCandles } from '@/shared/api/server/candles/utils.ts'; + +export async function GET(req: NextRequest): Promise> { + const grpcEndpoint = process.env['PENUMBRA_GRPC_ENDPOINT']; + const chainId = process.env['PENUMBRA_CHAIN_ID']; + if (!grpcEndpoint || !chainId) { + return NextResponse.json( + { error: 'PENUMBRA_GRPC_ENDPOINT or PENUMBRA_CHAIN_ID is not set' }, + { status: 500 }, + ); + } + + const { searchParams } = new URL(req.url); + const baseAssetSymbol = searchParams.get('baseAsset'); + const quoteAssetSymbol = searchParams.get('quoteAsset'); + if (!baseAssetSymbol || !quoteAssetSymbol) { + return NextResponse.json( + { error: 'Missing required baseAsset or quoteAsset' }, + { status: 400 }, + ); + } + const durationWindow = searchParams.get('durationWindow'); + if (!durationWindow || !isDurationWindow(durationWindow)) { + return NextResponse.json( + { error: `durationWindow missing or invalid window. Options: ${durationWindows.join(', ')}` }, + { status: 400 }, + ); + } + + const registryClient = new ChainRegistryClient(); + const registry = await registryClient.remote.get(chainId); + + // TODO: Add getMetadataBySymbol() helper to registry npm package + const allAssets = registry.getAllAssets(); + const baseAssetMetadata = allAssets.find( + a => a.symbol.toLowerCase() === baseAssetSymbol.toLowerCase(), + ); + const quoteAssetMetadata = allAssets.find( + a => a.symbol.toLowerCase() === quoteAssetSymbol.toLowerCase(), + ); + if (!baseAssetMetadata?.penumbraAssetId || !quoteAssetMetadata?.penumbraAssetId) { + return NextResponse.json( + { error: `Base asset or quoteAsset asset ids not found in registry` }, + { status: 400 }, + ); + } + + // Need to query both directions and aggregate results + const candlesFwd = await pindexer.candles( + baseAssetMetadata.penumbraAssetId, + quoteAssetMetadata.penumbraAssetId, + durationWindow, + ); + const candlesReverse = await pindexer.candles( + quoteAssetMetadata.penumbraAssetId, + baseAssetMetadata.penumbraAssetId, + durationWindow, + ); + + const mergedCandles = mergeCandles( + { metadata: baseAssetMetadata, candles: candlesFwd }, + { metadata: quoteAssetMetadata, candles: candlesReverse }, + ); + + const response = mergedCandles.map(dbCandleToOhlc); + + return NextResponse.json(response); +} diff --git a/src/shared/api/server/candles/types.ts b/src/shared/api/server/candles/types.ts new file mode 100644 index 00000000..f8ef1360 --- /dev/null +++ b/src/shared/api/server/candles/types.ts @@ -0,0 +1,13 @@ +import { OhlcData } from 'lightweight-charts'; + +export type CandleApiResponse = OhlcData[] | { error: string }; + +export interface DbCandle { + close: number; + direct_volume: number; + high: number; + low: number; + open: number; + swap_volume: number; + start_time: Date; +} diff --git a/src/shared/api/server/candles/utils.ts b/src/shared/api/server/candles/utils.ts new file mode 100644 index 00000000..3b99812b --- /dev/null +++ b/src/shared/api/server/candles/utils.ts @@ -0,0 +1,88 @@ +import { OhlcData, UTCTimestamp } from 'lightweight-charts'; +import { DbCandle } from '@/shared/api/server/candles/types.ts'; +import { Metadata } from '@penumbra-zone/protobuf/penumbra/core/asset/v1/asset_pb'; +import { getDisplayDenomExponent } from '@penumbra-zone/getters/metadata'; + +export const dbCandleToOhlc = (c: DbCandle): OhlcData => { + return { + close: c.close, + high: c.high, + low: c.low, + open: c.open, + time: (c.start_time.getTime() / 1000) as UTCTimestamp, + }; +}; + +const mergeCandle = (candle1: DbCandle, candle2: DbCandle): DbCandle => { + const mergedCandle = { ...candle1 }; + + // OHLC should be weighted average + const candle1TotalVolume = candle1.swap_volume + candle1.direct_volume; + const candle2TotalVolume = candle2.swap_volume + candle2.direct_volume; + + mergedCandle.open = + (candle1.open * candle1TotalVolume + candle2.open * candle2TotalVolume) / + (candle1TotalVolume + candle2TotalVolume); + mergedCandle.close = + (candle1.close * candle1TotalVolume + candle2.close * candle2TotalVolume) / + (candle1TotalVolume + candle2TotalVolume); + + mergedCandle.high = Math.max(candle1.high, candle2.high); + mergedCandle.low = Math.min(candle1.low, candle2.low); + + mergedCandle.swap_volume = candle1.swap_volume + candle2.swap_volume; + mergedCandle.direct_volume = candle1.direct_volume + candle2.direct_volume; + + return mergedCandle; +}; + +interface CandleSet { + metadata: Metadata; + candles: DbCandle[]; +} + +// Should be reversed to be in terms of base asset +const normalizeQuoteCandles = (base: CandleSet, quote: CandleSet): DbCandle[] => { + const baseExponent = getDisplayDenomExponent(base.metadata); + const quoteExponent = getDisplayDenomExponent(quote.metadata); + return quote.candles.map(prevCandle => { + const candle = { ...prevCandle }; + candle.open = 1 / candle.open; + candle.close = 1 / candle.close; + candle.high = 1 / candle.high; + candle.low = 1 / candle.low; + + // TODO: Adjust volumes based on price? But what price??? + candle.swap_volume = + (candle.swap_volume * (1 / candle.close)) / 10 ** Math.abs(baseExponent - quoteExponent); + candle.direct_volume = + (candle.direct_volume * (1 / candle.close)) / 10 ** Math.abs(baseExponent - quoteExponent); + + return candle; + }); +}; + +export const mergeCandles = (base: CandleSet, quote: CandleSet): DbCandle[] => { + // If theres any data at the same height, combine them + const combinedDataMap = new Map(); + base.candles.forEach(candle => { + combinedDataMap.set(candle.start_time.getTime(), candle); + }); + + normalizeQuoteCandles(base, quote).forEach(candle => { + const utcTime = candle.start_time.getTime(); + const entry = combinedDataMap.get(utcTime); + if (entry) { + const combinedCandle = mergeCandle(entry, candle); + combinedDataMap.set(utcTime, combinedCandle); + } else { + combinedDataMap.set(utcTime, candle); + } + }); + + const sortedCandles = Array.from(combinedDataMap.values()).sort((a, b) => + a.start_time > b.start_time ? 1 : -1, + ); + + return sortedCandles; +}; diff --git a/src/shared/api/server/token-fetch.tsx b/src/shared/api/server/token-fetch.tsx deleted file mode 100644 index 194703e1..00000000 --- a/src/shared/api/server/token-fetch.tsx +++ /dev/null @@ -1,55 +0,0 @@ -import { uint8ArrayToBase64 } from '@penumbra-zone/types/base64'; -import { AssetImage, DenomUnit } from '@penumbra-zone/protobuf/penumbra/core/asset/v1/asset_pb'; -import { ChainRegistryClient, Registry } from '@penumbra-labs/registry'; -import { Token } from '../../const/token'; - -const getRegistry = (chainId: string): Registry => { - const registryClient = new ChainRegistryClient(); - return registryClient.bundled.get(chainId); -}; - -// TODO: Deprecated. Use remote ChainRegistryClient directly. -export const fetchAllTokenAssets_deprecated = (chainId: string): Token[] => { - const registry = getRegistry(chainId); - const metadata = registry.getAllAssets(); - const tokens: Token[] = []; - metadata.forEach(x => { - // Filter out assets with no assetId and "Delegation" assets -- need to check this - // Standardize case - if (x.penumbraAssetId && !x.display.startsWith('delegation_')) { - const displayParts = x.display.split('/'); - tokens.push({ - decimals: decimalsFromDenomUnits(x.denomUnits), - display: displayParts[displayParts.length - 1] ?? '', - symbol: x.symbol, - inner: uint8ArrayToBase64(x.penumbraAssetId.inner), - imagePath: imagePathFromAssetImages(x.images), - }); - } - }); - return tokens; -}; - -export const imagePathFromAssetImages = (assetImages: AssetImage[]): string | undefined => { - // Take first png/svg from first AssetImage - let imagePath: string | undefined = undefined; - assetImages.forEach(x => { - if (x.png.length > 0) { - imagePath = x.png; - } else if (x.svg.length > 0) { - imagePath = x.svg; - } - }); - return imagePath; -}; - -export const decimalsFromDenomUnits = (denomUnits: DenomUnit[]): number => { - // Search denomUnits for highest exponent - let decimals = 0; - denomUnits.forEach(x => { - if (x.exponent >= decimals) { - decimals = x.exponent; - } - }); - return decimals; -}; diff --git a/src/shared/const/token.d.ts b/src/shared/const/token.d.ts deleted file mode 100644 index 88f3f225..00000000 --- a/src/shared/const/token.d.ts +++ /dev/null @@ -1,7 +0,0 @@ -export interface Token { - decimals: number; - display: string; - symbol: string; - inner: string; - imagePath?: string; -} diff --git a/src/shared/database/index.ts b/src/shared/database/index.ts index c054c06a..7bb7b047 100644 --- a/src/shared/database/index.ts +++ b/src/shared/database/index.ts @@ -1,7 +1,7 @@ import pkg from 'pg'; import fs from 'fs'; import { Kysely, PostgresDialect } from 'kysely'; -import { DB } from '@/shared/database/schema.ts'; +import { DB, DurationWindow } from '@/shared/database/schema.ts'; import { AssetId } from '@penumbra-zone/protobuf/penumbra/core/asset/v1/asset_pb'; const { Pool, types } = pkg; @@ -41,6 +41,17 @@ class Pindexer { .where('asset_end', '=', Buffer.from(quoteAsset.inner)) .execute(); } + + async candles(baseAsset: AssetId, quoteAsset: AssetId, window: DurationWindow) { + return this.db + .selectFrom('dex_ex_price_charts') + .innerJoin('dex_ex_candlesticks', 'candlestick_id', 'dex_ex_candlesticks.id') + .select(['start_time', 'open', 'close', 'low', 'high', 'swap_volume', 'direct_volume']) + .where('the_window', '=', window) + .where('asset_start', '=', Buffer.from(baseAsset.inner)) + .where('asset_end', '=', Buffer.from(quoteAsset.inner)) + .execute(); + } } export const pindexer = new Pindexer(); diff --git a/src/shared/database/schema.ts b/src/shared/database/schema.ts index d69fdfea..3c7b0f64 100644 --- a/src/shared/database/schema.ts +++ b/src/shared/database/schema.ts @@ -30,13 +30,18 @@ export interface DexExCandlesticks { swap_volume: number; } +export const durationWindows = ['1m', '15m', '1h', '4h', '1d', '1w', '1mo'] as const; +export type DurationWindow = (typeof durationWindows)[number]; +export const isDurationWindow = (str: string): str is DurationWindow => + durationWindows.includes(str as DurationWindow); + export interface DexExPriceCharts { asset_end: Buffer; asset_start: Buffer; candlestick_id: number | null; id: Generated; start_time: Timestamp; - the_window: string; + the_window: DurationWindow; } export interface DexExSummary { diff --git a/src/shared/utils/candles/index.ts b/src/shared/utils/candles/index.ts deleted file mode 100644 index 11b339c0..00000000 --- a/src/shared/utils/candles/index.ts +++ /dev/null @@ -1,83 +0,0 @@ -import { CandlestickData } from '@penumbra-zone/protobuf/penumbra/core/component/dex/v1/dex_pb'; -import { Token } from '@/shared/const/token'; - -export interface VolumeCandle extends CandlestickData { - volume: number; -} - -// Merge the two arrays, forward will be left alone, however backward will need to -// have 1/price and volumes will have to account for the pricing and decimal difference -export function createMergeCandles(asset1Token: Token, asset2Token: Token) { - function mergeCandle(candle1: CandlestickData, candle2: CandlestickData): VolumeCandle { - const mergedCandle = { ...candle1 } as VolumeCandle; - - // OHLC should be weighted average - const candle1TotalVolume = candle1.swapVolume + candle1.directVolume; - const candle2TotalVolume = candle2.swapVolume + candle2.directVolume; - - mergedCandle.open = - (candle1.open * candle1TotalVolume + candle2.open * candle2TotalVolume) / - (candle1TotalVolume + candle2TotalVolume); - mergedCandle.close = - (candle1.close * candle1TotalVolume + candle2.close * candle2TotalVolume) / - (candle1TotalVolume + candle2TotalVolume); - - mergedCandle.high = Math.max(candle1.high, candle2.high); - mergedCandle.low = Math.min(candle1.low, candle2.low); - - mergedCandle.directVolume = candle1.directVolume + candle2.directVolume; - mergedCandle.swapVolume = candle1.swapVolume + candle2.swapVolume; - mergedCandle.volume = candle1TotalVolume + candle2TotalVolume; - - return mergedCandle; - } - - return function mergeCandles( - candles1: CandlestickData[] | undefined, - candles2: CandlestickData[] | undefined, - ): VolumeCandle[] { - if (!candles1?.length || !candles2?.length) { - return []; - } - - const normalizedCandles2 = candles2.map((prevCandle: CandlestickData) => { - const candle = { ...prevCandle }; - candle.open = 1 / candle.open; - candle.close = 1 / candle.close; - candle.high = 1 / candle.high; - candle.low = 1 / candle.low; - - // TODO: Adjust volumes based on price? But what price??? - candle.swapVolume = - (candle.swapVolume * (1 / candle.close)) / - 10 ** Math.abs(asset2Token.decimals - asset2Token.decimals); - candle.directVolume = - (candle.directVolume * (1 / candle.close)) / - 10 ** Math.abs(asset1Token.decimals - asset2Token.decimals); - - return candle; - }) as CandlestickData[]; - - // If theres any data at the same height, combine them - const combinedDataMap = new Map(); - candles1.forEach((candle: CandlestickData) => { - combinedDataMap.set(candle.height, candle); - }); - - normalizedCandles2.forEach((candle: CandlestickData) => { - if (combinedDataMap.has(candle.height)) { - const prevCandle = combinedDataMap.get(candle.height) as CandlestickData; - const combinedCandle = mergeCandle(prevCandle, candle); - combinedDataMap.set(candle.height, combinedCandle); - } else { - combinedDataMap.set(candle.height, candle); - } - }); - - const sortedCandles = (Array.from(combinedDataMap.values()) as VolumeCandle[]).sort((a, b) => - a.height > b.height ? 1 : -1, - ); - - return sortedCandles; - }; -} diff --git a/src/shared/utils/protos/services/app/shielded-pool.ts b/src/shared/utils/protos/services/app/shielded-pool.ts deleted file mode 100644 index 59a4e48a..00000000 --- a/src/shared/utils/protos/services/app/shielded-pool.ts +++ /dev/null @@ -1,21 +0,0 @@ -// @ts-nocheck -/* eslint-disable -- disabling this file as this was created before our strict rules */ -import { PromiseClient } from '@connectrpc/connect'; -import { createClient } from '../utils'; -import { ShieldedPoolService } from '@penumbra-zone/protobuf'; -import { AssetId, Metadata } from '@penumbra-zone/protobuf/penumbra/core/asset/v1/asset_pb'; -import { ShieldedPoolQuerierInterface } from '../../types/ShieldedPoolQuerier'; - -export class ShieldedPoolQuerier implements ShieldedPoolQuerierInterface { - private readonly client: PromiseClient; - - constructor({ grpcEndpoint }: { grpcEndpoint: string }) { - this.client = createClient(grpcEndpoint, ShieldedPoolService); - } - - async assetMetadata(assetId: AssetId): Promise { - const res = await this.client.assetMetadataById({ assetId }); - // console.info(res) - return res.denomMetadata; - } -} diff --git a/src/shared/utils/protos/services/dex/dex-query-service-client.ts b/src/shared/utils/protos/services/dex/dex-query-service-client.ts deleted file mode 100644 index c055ea83..00000000 --- a/src/shared/utils/protos/services/dex/dex-query-service-client.ts +++ /dev/null @@ -1,119 +0,0 @@ -// @ts-nocheck -/* eslint-disable -- disabling this file as this was created before our strict rules */ -import { PromiseClient } from '@connectrpc/connect'; -import { createClient } from '../utils'; -import { DexService } from '@penumbra-zone/protobuf'; -import { - PositionId, - Position, - DirectedTradingPair, - SwapExecution, - CandlestickData, -} from '@penumbra-zone/protobuf/penumbra/core/component/dex/v1/dex_pb'; -import { - DexQueryServiceClientInterface, - SwapExecutionWithBlockHeight, -} from '../../types/DexQueryServiceClientInterface'; -import { Readable } from 'stream'; - -export class DexQueryServiceClient implements DexQueryServiceClientInterface { - private readonly client: PromiseClient; - - constructor({ grpcEndpoint }: { grpcEndpoint: string }) { - this.client = createClient(grpcEndpoint, DexService); - } - - async liquidityPositionById(positionId: PositionId): Promise { - // console.log('liquidityPositionById', positionId) - const res = await this.client.liquidityPositionById({ positionId }); - return res.data; - } - - async liquidityPositionsByPrice( - tradingPair: DirectedTradingPair, - limit: number, - ): Promise { - const res = await this.client.liquidityPositionsByPrice({ - tradingPair, - limit: BigInt(limit), - }); - - if (!res[Symbol.asyncIterator]) { - console.error('Received:', res); - throw new Error( - 'Received an unexpected response type from the server, expected an async iterable.', - ); - } - - const positions: Position[] = []; - // Res is Symbol(Symbol.asyncIterator)]: [Function: [Symbol.asyncIterator]] - for await (const position of res) { - positions.push(position.data); - } - return positions; - } - - async arbExecutions( - startHeight: number, - endHeight: number, - ): Promise { - const res = await this.client.arbExecutions({ - startHeight: BigInt(startHeight), - endHeight: BigInt(endHeight), - }); - - if (!res[Symbol.asyncIterator]) { - console.error('Received:', res); - throw new Error( - 'Received an unexpected response type from the server, expected an async iterable.', - ); - } - - const arbs: SwapExecutionWithBlockHeight[] = []; - for await (const arb of res as Readable) { - const swapExecution: SwapExecution = arb.swapExecution; - const blockHeight = Number(arb.height); - arbs.push({ swapExecution, blockHeight }); - } - return arbs; - } - - async swapExecutions( - startHeight: number, - endHeight: number, - ): Promise { - const res = await this.client.swapExecutions({ - startHeight: BigInt(startHeight), - endHeight: BigInt(endHeight), - }); - - if (!res[Symbol.asyncIterator]) { - console.error('Received:', res); - throw new Error( - 'Received an unexpected response type from the server, expected an async iterable.', - ); - } - - const swaps: SwapExecutionWithBlockHeight[] = []; - for await (const swap of res as Readable) { - const swapExecution: SwapExecution = swap.swapExecution; - const blockHeight = Number(swap.height); - swaps.push({ swapExecution, blockHeight }); - } - return swaps; - } - - async candlestickData( - pair: DirectedTradingPair, - startHeight: number, - limit: number, - ): Promise { - const res = await this.client.candlestickData({ - pair, - startHeight: BigInt(startHeight), - limit: BigInt(limit), - }); - - return res.data; - } -} diff --git a/src/shared/utils/protos/services/dex/simulated-trades.ts b/src/shared/utils/protos/services/dex/simulated-trades.ts deleted file mode 100644 index 6886de5b..00000000 --- a/src/shared/utils/protos/services/dex/simulated-trades.ts +++ /dev/null @@ -1,19 +0,0 @@ -import { PromiseClient } from '@connectrpc/connect'; -import { createClient } from '../utils'; -import { SimulationService } from '@penumbra-zone/protobuf'; -import { - SimulateTradeRequest, - SimulateTradeResponse, -} from '@penumbra-zone/protobuf/penumbra/core/component/dex/v1/dex_pb'; - -export class SimulationQuerier { - private readonly client: PromiseClient; - - constructor({ grpcEndpoint }: { grpcEndpoint: string }) { - this.client = createClient(grpcEndpoint, SimulationService); - } - - async simulateTrade(request: SimulateTradeRequest): Promise { - return this.client.simulateTrade(request); - } -} diff --git a/src/shared/utils/protos/types/DexQueryServiceClientInterface.ts b/src/shared/utils/protos/types/DexQueryServiceClientInterface.ts deleted file mode 100644 index 35c9d21d..00000000 --- a/src/shared/utils/protos/types/DexQueryServiceClientInterface.ts +++ /dev/null @@ -1,33 +0,0 @@ -import { - PositionId, - Position, - DirectedTradingPair, - SwapExecution, - CandlestickData, -} from '@penumbra-zone/protobuf/penumbra/core/component/dex/v1/dex_pb'; - -export interface DexQueryServiceClientInterface { - liquidityPositionById(id: PositionId): Promise; - liquidityPositionsByPrice( - directedTradingPair: DirectedTradingPair, - limit: number, - ): Promise; - arbExecutions( - starHheight: number, - endHeight: number, - ): Promise; - swapExecutions( - startHeight: number, - endHeight: number, - ): Promise; - candlestickData( - tradingPair: DirectedTradingPair, - limit: number, - startHeight: number, - ): Promise; -} - -export interface SwapExecutionWithBlockHeight { - swapExecution: SwapExecution; - blockHeight: number; -} diff --git a/src/shared/utils/protos/types/ShieldedPoolQuerier.ts b/src/shared/utils/protos/types/ShieldedPoolQuerier.ts deleted file mode 100644 index 37970fa1..00000000 --- a/src/shared/utils/protos/types/ShieldedPoolQuerier.ts +++ /dev/null @@ -1,7 +0,0 @@ -// @ts-nocheck -/* eslint-disable -- disabling this file as this was created before our strict rules */ -import { AssetId, Metadata } from '@penumbra-zone/protobuf/penumbra/core/asset/v1/asset_pb'; - -export interface ShieldedPoolQuerierInterface { - assetMetadata(assetId: AssetId): Promise; -} diff --git a/src/shared/utils/protos/services/utils.ts b/src/shared/utils/protos/utils.ts similarity index 100% rename from src/shared/utils/protos/services/utils.ts rename to src/shared/utils/protos/utils.ts