Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Display recent executions #222

Merged
merged 2 commits into from
Jan 13, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions app/api/recent-executions/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { GET } from '@/shared/api/server/recent-executions';
67 changes: 67 additions & 0 deletions src/pages/trade/api/recent-executions.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
import { useQuery } from '@tanstack/react-query';
import { useRefetchOnNewBlock } from '@/shared/api/compact-block.ts';
import { usePathSymbols } from '@/pages/trade/model/use-path.ts';
import { apiFetch } from '@/shared/utils/api-fetch.ts';
import { RecentExecution } from '@/shared/api/server/recent-executions.ts';
import { registryQueryFn } from '@/shared/api/registry.ts';
import { Registry } from '@penumbra-labs/registry';
import { formatAmount } from '@penumbra-zone/types/amount';
import { getDisplayDenomExponent } from '@penumbra-zone/getters/metadata';
import { calculateDisplayPrice } from '@/shared/utils/price-conversion.ts';
import BigNumber from 'bignumber.js';

export interface RecentExecutionVV {
kind: 'buy' | 'sell';
amount: string;
price: string;
timestamp: string;
}

const addVV = (res: RecentExecution[], registry: Registry): RecentExecutionVV[] => {
return res.map(r => {
if (!r.amount.assetId || !r.amount.amount) {
throw new Error('No asseId or Amount passed for recent execution');
}
const baseMetadata = registry.getMetadata(r.amount.assetId);
const baseDisplayDenomExponent = getDisplayDenomExponent.optional(baseMetadata) ?? 0;

const quoteMetadata = registry.getMetadata(r.price.assetId);
const price = calculateDisplayPrice(r.price.amount, baseMetadata, quoteMetadata);

return {
kind: r.kind,
amount: formatAmount({
amount: r.amount.amount,
exponent: baseDisplayDenomExponent,
decimalPlaces: 4,
}),
price: new BigNumber(price).toFormat(4),
timestamp: r.timestamp,
};
});
};

const LIMIT = 10;

export const useRecentExecutions = () => {
const { baseSymbol, quoteSymbol } = usePathSymbols();

const query = useQuery({
queryKey: ['recent-executions', baseSymbol, quoteSymbol],
queryFn: async () => {
const results = await apiFetch<RecentExecution[]>('/api/recent-executions', {
baseAsset: baseSymbol,
quoteAsset: quoteSymbol,
limit: String(LIMIT),
});

const registry = await registryQueryFn();

return addVV(results, registry);
},
});

useRefetchOnNewBlock('recent-executions', query);

return query;
};
70 changes: 59 additions & 11 deletions src/pages/trade/ui/market-trades.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import { Text } from '@penumbra-zone/ui/Text';
import { ReactNode } from 'react';
import { Skeleton } from '@/shared/ui/skeleton';
import { RecentExecutionVV, useRecentExecutions } from '@/pages/trade/api/recent-executions.ts';
import { Density } from '@penumbra-zone/ui/Density';

export const Cell = ({ children }: { children: ReactNode }) => {
return <div className='flex items-center py-1.5 px-3 min-h-12'>{children}</div>;
Expand Down Expand Up @@ -37,19 +39,65 @@ const LoadingRow = () => {
);
};

const ErrorState = ({ error }: { error: Error }) => {
return <div className='text-red-500'>{String(error)}</div>;
};

const formatLocalTime = (isoString: string): string => {
const date = new Date(isoString);
return date.toLocaleTimeString('en-US', {
hour12: false,
hour: '2-digit',
minute: '2-digit',
second: '2-digit',
});
};

const LoadedState = ({ data }: { data: RecentExecutionVV[] }) => {
return data.map((e, i) => {
return (
<div
key={i}
className='grid grid-cols-subgrid col-span-4 text-text-secondary border-b border-other-tonalStroke'
>
<Cell>
<Text small color={e.kind === 'buy' ? 'success.light' : 'destructive.light'}>
{e.price}
</Text>
</Cell>
<Cell>
<Text small color='text.primary'>
{e.amount}
</Text>
</Cell>
<Cell>
<Text small color='text.primary'>
{formatLocalTime(e.timestamp)}
</Text>
</Cell>
<Cell>-</Cell>
</div>
);
});
};

export const MarketTrades = () => {
const { data, isLoading, error } = useRecentExecutions();

return (
<div className='grid grid-cols-4 pt-4 px-4 pb-0 h-auto overflow-auto'>
<div className='grid grid-cols-subgrid col-span-4 text-text-secondary border-b border-other-tonalStroke'>
<HeaderCell>Price</HeaderCell>
<HeaderCell>Amount</HeaderCell>
<HeaderCell>Time</HeaderCell>
<HeaderCell>Route</HeaderCell>
</div>
<Density slim>
<div className='grid grid-cols-4 pt-4 px-4 pb-0 h-auto overflow-auto'>
<div className='grid grid-cols-subgrid col-span-4 text-text-secondary border-b border-other-tonalStroke'>
<HeaderCell>Price</HeaderCell>
<HeaderCell>Amount</HeaderCell>
<HeaderCell>Time</HeaderCell>
<HeaderCell>Route</HeaderCell>
</div>

{new Array(15).fill(0).map((_, i) => (
<LoadingRow key={i} />
))}
</div>
{isLoading && new Array(15).fill(0).map((_, i) => <LoadingRow key={i} />)}
{error && <ErrorState error={error} />}
{data && <LoadedState data={data} />}
</div>
</Density>
);
};
4 changes: 3 additions & 1 deletion src/pages/trade/ui/trades-tabs.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,9 @@ export const TradesTabs = ({ withChart = false }: { withChart?: boolean }) => {
</div>
)}
{tab === TradesTabsType.MarketTrades && <MarketTrades />}
{tab === TradesTabsType.MyTrades && <MarketTrades />}
{tab === TradesTabsType.MyTrades && (
<div className='text-text-secondary p-4'>Coming soon...</div>
)}
</>
)}
</div>
Expand Down
109 changes: 109 additions & 0 deletions src/shared/api/server/recent-executions.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
import { NextRequest, NextResponse } from 'next/server';
import { pindexer } from '@/shared/database';
import { ChainRegistryClient } from '@penumbra-labs/registry';
import { serialize, Serialized } from '@/shared/utils/serializer';
import { AssetId, Value } from '@penumbra-zone/protobuf/penumbra/core/asset/v1/asset_pb';
import { pnum } from '@penumbra-zone/types/pnum';

const transformDbVal = ({
context_asset_end,
context_asset_start,
delta_1,
delta_2,
lambda_1,
lambda_2,
time,
}: {
context_asset_end: Buffer;
context_asset_start: Buffer;
delta_1: string;
delta_2: string;
lambda_1: string;
lambda_2: string;
time: Date;
}): RecentExecution => {
const baseAssetId = new AssetId({
inner: Uint8Array.from(context_asset_start),
});
const quoteAssetId = new AssetId({ inner: Uint8Array.from(context_asset_end) });

// Determine trade direction
const isBaseAssetInput = BigInt(delta_1) !== 0n;
const kind = isBaseAssetInput ? 'sell' : 'buy';

// Amount of base & quote asset being traded in or out of
const baseAmount = isBaseAssetInput ? pnum(delta_1) : pnum(lambda_1);
const quoteAmount = isBaseAssetInput ? pnum(lambda_2) : pnum(delta_2);

const price = baseAmount.toBigNumber().div(quoteAmount.toBigNumber()).toNumber();
const timestamp = time.toISOString();

return {
kind,
amount: new Value({ amount: baseAmount.toAmount(), assetId: baseAssetId }),
price: { amount: price, assetId: quoteAssetId },
timestamp,
Comment on lines +28 to +45
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This math is very off currently. Need to sync with @erwanor in office hours 😅 .

};
};

export type RecentExecutionsResponse = RecentExecution[] | { error: string };

interface FloatValue {
assetId: AssetId;
amount: number;
}

export interface RecentExecution {
kind: 'buy' | 'sell';
amount: Value;
price: FloatValue;
timestamp: string;
}

export async function GET(
req: NextRequest,
): Promise<NextResponse<Serialized<RecentExecutionsResponse>>> {
const chainId = process.env['PENUMBRA_CHAIN_ID'];
if (!chainId) {
return NextResponse.json({ error: 'PENUMBRA_CHAIN_ID is not set' }, { status: 500 });
}

const { searchParams } = new URL(req.url);
const baseAssetSymbol = searchParams.get('baseAsset');
const quoteAssetSymbol = searchParams.get('quoteAsset');
const limit = searchParams.get('limit');
if (!baseAssetSymbol || !quoteAssetSymbol || !limit) {
return NextResponse.json(
{ error: 'Missing required baseAsset, quoteAsset, or limit' },
{ 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 assetId not found in registry` },
{ status: 400 },
);
}

const results = await pindexer.recentExecutions(
baseAssetMetadata.penumbraAssetId,
quoteAssetMetadata.penumbraAssetId,
Number(limit),
);

const response = results.map(transformDbVal);

return NextResponse.json(serialize(response));
}
21 changes: 21 additions & 0 deletions src/shared/database/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -333,6 +333,27 @@ class Pindexer {
.execute();
}

async recentExecutions(base: AssetId, quote: AssetId, amount: number) {
return await this.db
.selectFrom('dex_ex_position_executions')
.select([
'context_asset_end',
'context_asset_start',
'delta_1',
'delta_2',
'lambda_1',
'lambda_2',
'time',
'rowid',
])
.where('context_asset_start', '=', Buffer.from(base.inner))
.where('context_asset_end', '=', Buffer.from(quote.inner))
.orderBy('time', 'desc')
.orderBy('rowid', 'asc') // Secondary sort by ID to maintain order within the same time frame
.limit(amount)
.execute();
}

async getPositionVolumeAndFees(positionId: PositionId): Promise<VolumeAndFees[]> {
const results = await this.db
.selectFrom('dex_ex_position_executions')
Expand Down