Skip to content

Commit

Permalink
Candles v2 (time windows) (#128)
Browse files Browse the repository at this point in the history
* 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 <[email protected]>
  • Loading branch information
grod220 and conorsch authored Nov 6, 2024
1 parent 2f3633d commit b245695
Show file tree
Hide file tree
Showing 31 changed files with 388 additions and 840 deletions.
11 changes: 6 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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:

Expand Down Expand Up @@ -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/

This file was deleted.

1 change: 1 addition & 0 deletions app/api/candles/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { GET } from '@/shared/api/server/candles/index.ts';
6 changes: 2 additions & 4 deletions app/trade/page.ts
Original file line number Diff line number Diff line change
@@ -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;
25 changes: 13 additions & 12 deletions src/pages/trade/api/book.ts
Original file line number Diff line number Diff line change
@@ -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<RouteBookResponse> => {
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);
Expand Down
34 changes: 21 additions & 13 deletions src/pages/trade/api/candles.tsx
Original file line number Diff line number Diff line change
@@ -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<CandlestickData[]> => {
if (startBlock === undefined) {
return [];
queryKey: ['candles', baseSymbol, quoteSymbol, durationWindow],
queryFn: async (): Promise<OhlcData[]> => {
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;
},
});

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,17 +8,25 @@ interface PathParams {
[key: string]: string; // required for useParams signature
}

export const usePathSymbols = () => {
const params = useParams<PathParams>();
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<PathParams>();
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),
};
},
});
Expand Down
22 changes: 6 additions & 16 deletions src/pages/trade/model/useSummary.ts
Original file line number Diff line number Diff line change
@@ -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();
Expand All @@ -28,9 +23,4 @@ export const useSummary = () => {
return jsonRes;
},
});

return {
...res,
error: pathError ?? res.error,
};
};
41 changes: 41 additions & 0 deletions src/pages/trade/redirect.tsx
Original file line number Diff line number Diff line change
@@ -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 <div className='text-red-600'>{String(error)}</div>;
} else if (isLoading || !data) {
return <div className='text-white'>Loading...</div>;
} else {
redirect(`/trade/${data.baseAsset}/${data.quoteAsset}`);
}
};
Loading

0 comments on commit b245695

Please sign in to comment.