Skip to content

Commit

Permalink
explore page: use common numeraire (#223)
Browse files Browse the repository at this point in the history
* volume and liquidity using common numeraire (usdc)

* explore: usdc price api using sql query

* round down since value view expects an integer

* filter out pairs with zero liquidity and trading volume

* linting and comments

* stats: usdc-denominated stats for explore page

* utility: split price calculations into seperate utility method

* linting and fixes
  • Loading branch information
TalDerei authored Jan 13, 2025
1 parent 8735899 commit 3687617
Show file tree
Hide file tree
Showing 14 changed files with 166 additions and 55 deletions.
3 changes: 2 additions & 1 deletion src/pages/explore/api/use-summaries.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,8 @@ import { SummaryData } from '@/shared/api/server/summary/types';
import { DurationWindow } from '@/shared/utils/duration';
import { apiFetch } from '@/shared/utils/api-fetch';

const BASE_LIMIT = 15;
/// The base limit will need to be increased as more trading pairs are added to the explore page.
const BASE_LIMIT = 20;
const BASE_PAGE = 0;
const BASE_WINDOW: DurationWindow = '1d';

Expand Down
8 changes: 6 additions & 2 deletions src/pages/explore/ui/pair-card.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import { Skeleton } from '@/shared/ui/skeleton';
import SparklineChart from './sparkline-chart.svg';
import ChevronDown from './chevron-down.svg';
import { PreviewChart } from './preview-chart';
import { useRegistryAssets } from '@/shared/api/registry';

const ShimmeringBars = () => {
return (
Expand Down Expand Up @@ -65,6 +66,9 @@ export const PairCard = ({ loading, summary }: PairCardProps) => {
const today = new Date();
const yesterday = subDays(new Date(), 1);

const { data: assets } = useRegistryAssets();
const usdcMetadata = assets?.find(asset => asset.symbol === 'USDC');

return (
<Link
href={loading ? `/trade` : `/trade/${summary.baseAsset.symbol}/${summary.quoteAsset.symbol}`}
Expand Down Expand Up @@ -119,7 +123,7 @@ export const PairCard = ({ loading, summary }: PairCardProps) => {
{shortify(Number(getFormattedAmtFromValueView(summary.liquidity)))}
</Text>
<Text detail color='text.secondary'>
{summary.quoteAsset.symbol}
{usdcMetadata?.symbol}
</Text>
</>
)}
Expand All @@ -134,7 +138,7 @@ export const PairCard = ({ loading, summary }: PairCardProps) => {
{shortify(Number(getFormattedAmtFromValueView(summary.directVolume)))}
</Text>
<Text detail color='text.secondary'>
{summary.quoteAsset.symbol}
{usdcMetadata?.symbol}
</Text>
</>
)}
Expand Down
17 changes: 10 additions & 7 deletions src/pages/explore/ui/stats.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,12 @@ import { pluralizeAndShortify } from '@/shared/utils/pluralize';
import { shortify } from '@penumbra-zone/types/shortify';
import { getFormattedAmtFromValueView } from '@penumbra-zone/types/value-view';
import { useStats } from '@/pages/explore/api/use-stats';
import { useRegistryAssets } from '@/shared/api/registry';

export const ExploreStats = () => {
const { data: stats, isLoading, error } = useStats();
const { data: assets } = useRegistryAssets();
const usdcMetadata = assets?.find(asset => asset.symbol === 'USDC');

if (error) {
return (
Expand All @@ -23,10 +26,9 @@ export const ExploreStats = () => {
<div className='grid grid-cols-1 tablet:grid-cols-2 desktop:grid-cols-3 gap-2'>
<InfoCard title='Total Trading Volume (24h)' loading={isLoading}>
{stats && (
<Text large color='text.primary'>
<Text large color='text.primary'>
{shortify(Number(getFormattedAmtFromValueView(stats.directVolume)))}
</Text>
<Text large color='success.light'>
{shortify(Number(getFormattedAmtFromValueView(stats.directVolume)))}{' '}
{usdcMetadata?.symbol}
</Text>
)}
</InfoCard>
Expand All @@ -45,7 +47,8 @@ export const ExploreStats = () => {
</Text>
{stats.largestPairLiquidity && (
<Text large color='success.light'>
{shortify(Number(getFormattedAmtFromValueView(stats.largestPairLiquidity)))}
{shortify(Number(getFormattedAmtFromValueView(stats.largestPairLiquidity)))}{' '}
{usdcMetadata?.symbol}
</Text>
)}
</>
Expand All @@ -57,8 +60,8 @@ export const ExploreStats = () => {
</InfoCard>
<InfoCard title='Total Liquidity Available' loading={isLoading}>
{stats && (
<Text large color='text.primary'>
{shortify(Number(getFormattedAmtFromValueView(stats.liquidity)))}
<Text large color='success.light'>
{shortify(Number(getFormattedAmtFromValueView(stats.liquidity)))} {usdcMetadata?.symbol}
</Text>
)}
</InfoCard>
Expand Down
File renamed without changes.
2 changes: 1 addition & 1 deletion src/pages/trade/model/useMarketPrice.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { useSummary } from './useSummary';
import { useSummary } from '../api/use-summary';

export const useMarketPrice = (baseSymbol?: string, quoteSymbol?: string) => {
const { data: summary } = useSummary('1d', baseSymbol, quoteSymbol);
Expand Down
2 changes: 1 addition & 1 deletion src/pages/trade/ui/summary.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import cn from 'clsx';
import { useAutoAnimate } from '@formkit/auto-animate/react';
import { Text } from '@penumbra-zone/ui/Text';
import { Skeleton } from '@/shared/ui/skeleton';
import { useSummary } from '../model/useSummary';
import { useSummary } from '../api/use-summary';
import { ValueViewComponent } from '@penumbra-zone/ui/ValueView';
import { round } from '@penumbra-zone/types/round';
import { Density } from '@penumbra-zone/ui/Density';
Expand Down
79 changes: 57 additions & 22 deletions src/shared/api/server/stats.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { pindexer } from '@/shared/database';
import { DurationWindow } from '@/shared/utils/duration';
import { toValueView } from '@/shared/utils/value-view';
import { Serialized, serialize } from '@/shared/utils/serializer';
import { calculateEquivalentInUSDC } from '@/shared/utils/price-conversion';

interface StatsDataBase {
activePairs: number;
Expand All @@ -31,25 +32,26 @@ export const getStats = async (): Promise<Serialized<StatsResponse>> => {
}

const registryClient = new ChainRegistryClient();

const [registry, results] = await Promise.all([
registryClient.remote.get(chainId),
pindexer.stats(STATS_DURATION_WINDOW),
]);

const stats = results[0];
if (!stats) {
return { error: `No stats found` };
}
const registry = await registryClient.remote.get(chainId);

// TODO: Add getMetadataBySymbol() helper to registry npm package
const allAssets = registry.getAllAssets();
// TODO: what asset should be used here?
const usdcMetadata = allAssets.find(asset => asset.symbol.toLowerCase() === 'usdc');
if (!usdcMetadata) {
return { error: 'USDC not found in registry' };
}

const results = await pindexer.stats(
STATS_DURATION_WINDOW,
// eslint-disable-next-line @typescript-eslint/non-nullable-type-assertion-style -- usdc is defined
usdcMetadata.penumbraAssetId as AssetId,
);

const stats = results[0];
if (!stats) {
return { error: `No stats found` };
}

const topPriceMoverStart = allAssets.find(asset => {
return asset.penumbraAssetId?.equals(new AssetId({ inner: stats.top_price_mover_start }));
});
Expand Down Expand Up @@ -79,23 +81,56 @@ export const getStats = async (): Promise<Serialized<StatsResponse>> => {
end: largestPairEnd.symbol,
};

let liquidity = toValueView({
amount: Math.floor(stats.liquidity),
metadata: usdcMetadata,
});

let directVolume = toValueView({
amount: Math.floor(stats.direct_volume),
metadata: usdcMetadata,
});

let largestPairLiquidity =
largestPairEnd &&
toValueView({
amount: Math.floor(stats.largest_dv_trading_pair_volume),
metadata: largestPairEnd,
});

// Converts liquidity and trading volume to their equivalent USDC prices if `usdc_price` is available
if (stats.usdc_price && largestPairEnd) {
liquidity = calculateEquivalentInUSDC(
stats.liquidity,
stats.usdc_price,
largestPairEnd,
usdcMetadata,
);

directVolume = calculateEquivalentInUSDC(
stats.direct_volume,
stats.usdc_price,
largestPairEnd,
usdcMetadata,
);

largestPairLiquidity = calculateEquivalentInUSDC(
stats.largest_dv_trading_pair_volume,
stats.usdc_price,
largestPairEnd,
usdcMetadata,
);
}

return serialize({
activePairs: stats.active_pairs,
trades: stats.trades,
largestPair,
topPriceMover,
time: new Date(),
largestPairLiquidity:
largestPairEnd &&
toValueView({
amount: stats.largest_dv_trading_pair_volume,
metadata: largestPairEnd,
}),
liquidity: toValueView({
amount: parseInt(`${stats.liquidity}`),
metadata: usdcMetadata,
}),
directVolume: toValueView({ amount: stats.direct_volume, metadata: usdcMetadata }),
largestPairLiquidity,
liquidity,
directVolume,
});
} catch (error) {
return { error: (error as Error).message };
Expand Down
22 changes: 15 additions & 7 deletions src/shared/api/server/summary/all.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,19 +29,18 @@ export const getAllSummaries = async (

const registryClient = new ChainRegistryClient();
const registry = await registryClient.remote.get(chainId);
const allAssets = registry.getAllAssets();

const stablecoins = registry
.getAllAssets()
.filter(asset => ['USDT', 'USDC', 'USDY'].includes(asset.symbol))
.map(asset => asset.penumbraAssetId) as AssetId[];
const stablecoins = allAssets.filter(asset => ['USDT', 'USDC', 'USDY'].includes(asset.symbol));
const usdc = stablecoins.find(asset => asset.symbol === 'USDC');

const results = await pindexer.summaries({
...params,
stablecoins,
stablecoins: stablecoins.map(asset => asset.penumbraAssetId) as AssetId[],
// eslint-disable-next-line @typescript-eslint/non-nullable-type-assertion-style -- usdc is defined
usdc: usdc?.penumbraAssetId as AssetId,
});

const allAssets = registry.getAllAssets();

const summaries = await Promise.all(
results.map(summary => {
const baseAsset = getAssetById(allAssets, summary.asset_start);
Expand All @@ -54,10 +53,19 @@ export const getAllSummaries = async (
summary,
baseAsset,
quoteAsset,
usdc,
summary.candles,
summary.candle_times,
);

// Filter out pairs with zero liquidity and trading volume
if (
(data.liquidity.valueView.value?.amount?.lo &&
data.directVolume.valueView.value?.amount?.lo) === 0n
) {
return;
}

return serialize(data);
}),
);
Expand Down
4 changes: 2 additions & 2 deletions src/shared/api/server/summary/pairs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,14 +51,14 @@ export async function GET(): Promise<NextResponse<PairsResponse>> {
return undefined;
}

// TODO: should this be `direct_volume_over_window`?
let volume = toValueView({
amount: summary.liquidity,
metadata: quoteAsset,
});

// Converts liquidity and trading volume to their equivalent USDC prices if `usdc_price` is available
if (summary.usdc_price) {
// Custom volume calculation for USDC pairs
// TODO: change to a better method (probably create `pnum.multiply()` method and use it)
const expDiff = Math.abs(
getDisplayDenomExponent(quoteAsset) - getDisplayDenomExponent(usdc),
);
Expand Down
5 changes: 4 additions & 1 deletion src/shared/api/server/summary/single.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,9 @@ export async function GET(req: NextRequest): Promise<NextResponse<Serialized<Sum

// TODO: Add getMetadataBySymbol() helper to registry npm package
const allAssets = registry.getAllAssets();
const stablecoins = allAssets.filter(asset => ['USDT', 'USDC', 'USDY'].includes(asset.symbol));
const usdc = stablecoins.find(asset => asset.symbol === 'USDC');

const baseAssetMetadata = allAssets.find(
a => a.symbol.toLowerCase() === baseAssetSymbol.toLowerCase(),
);
Expand All @@ -58,6 +61,6 @@ export async function GET(req: NextRequest): Promise<NextResponse<Serialized<Sum
return NextResponse.json({ window: durationWindow, noData: true });
}

const adapted = adaptSummary(summary, baseAssetMetadata, quoteAssetMetadata);
const adapted = adaptSummary(summary, baseAssetMetadata, quoteAssetMetadata, usdc);
return NextResponse.json(serialize(adapted));
}
23 changes: 18 additions & 5 deletions src/shared/api/server/summary/types.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { DurationWindow } from '@/shared/utils/duration.ts';
import { Metadata, ValueView } from '@penumbra-zone/protobuf/penumbra/core/asset/v1/asset_pb';
import { DexExPairsSummary } from '@/shared/database/schema';
import { calculateDisplayPrice } from '@/shared/utils/price-conversion';
import { calculateDisplayPrice, calculateEquivalentInUSDC } from '@/shared/utils/price-conversion';
import { round } from '@penumbra-zone/types/round';
import { toValueView } from '@/shared/utils/value-view';

Expand Down Expand Up @@ -39,19 +39,32 @@ export const adaptSummary = (
summary: DexExPairsSummary,
baseAsset: Metadata,
quoteAsset: Metadata,
usdc: Metadata | undefined,
candles?: number[],
candleTimes?: Date[],
): SummaryData => {
const directVolume = toValueView({
amount: Math.floor(summary.direct_volume_over_window),
let liquidity = toValueView({
amount: Math.floor(summary.liquidity),
metadata: quoteAsset,
});

const liquidity = toValueView({
amount: Math.floor(summary.liquidity),
let directVolume = toValueView({
amount: Math.floor(summary.direct_volume_over_window),
metadata: quoteAsset,
});

// Converts liquidity and trading volume to their equivalent USDC prices if `usdc_price` is available
if (summary.usdc_price && usdc) {
liquidity = calculateEquivalentInUSDC(summary.liquidity, summary.usdc_price, quoteAsset, usdc);

directVolume = calculateEquivalentInUSDC(
summary.direct_volume_over_window,
summary.usdc_price,
quoteAsset,
usdc,
);
}

const priceDiff = summary.price - summary.price_then;
const change = {
value: calculateDisplayPrice(priceDiff, baseAsset, quoteAsset),
Expand Down
Loading

0 comments on commit 3687617

Please sign in to comment.