From aeb25d3afaba035c9aa8f1c530eb767252eed9a4 Mon Sep 17 00:00:00 2001 From: "Jason M. Hasperhoven" Date: Wed, 29 Jan 2025 16:57:21 +0400 Subject: [PATCH 01/12] Add inspect block page --- app/inspect/block/[height]/page.tsx | 3 +++ src/pages/inspect/block/index.tsx | 3 +++ 2 files changed, 6 insertions(+) create mode 100644 app/inspect/block/[height]/page.tsx create mode 100644 src/pages/inspect/block/index.tsx diff --git a/app/inspect/block/[height]/page.tsx b/app/inspect/block/[height]/page.tsx new file mode 100644 index 00000000..b9ae0fa2 --- /dev/null +++ b/app/inspect/block/[height]/page.tsx @@ -0,0 +1,3 @@ +import { InspectBlock } from '@/pages/inspect/block/index.tsx'; + +export default InspectBlock; diff --git a/src/pages/inspect/block/index.tsx b/src/pages/inspect/block/index.tsx new file mode 100644 index 00000000..30fb99e7 --- /dev/null +++ b/src/pages/inspect/block/index.tsx @@ -0,0 +1,3 @@ +export function InspectBlock() { + return
InspectBlock
; +} From a0b6456d2bc127652062ba7d63d23e091679e28f Mon Sep 17 00:00:00 2001 From: "Jason M. Hasperhoven" Date: Wed, 29 Jan 2025 17:07:11 +0400 Subject: [PATCH 02/12] Fetch transactions by height --- src/pages/inspect/block/index.tsx | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/src/pages/inspect/block/index.tsx b/src/pages/inspect/block/index.tsx index 30fb99e7..9c0c4c86 100644 --- a/src/pages/inspect/block/index.tsx +++ b/src/pages/inspect/block/index.tsx @@ -1,3 +1,29 @@ +'use client'; + +import { penumbra } from '@/shared/const/penumbra'; +import { ViewService } from '@penumbra-zone/protobuf'; +import { useParams } from 'next/navigation'; +import { useEffect } from 'react'; + export function InspectBlock() { + const params = useParams<{ height: string }>(); + const blockheight = params.height; + console.log('TCL: InspectBlock -> blockheight', blockheight); + + useEffect(() => { + const fetchTransactions = async () => { + const transactions = await Array.fromAsync( + penumbra.service(ViewService).transactionInfo({ + startHeight: BigInt(blockheight - 999), + endHeight: BigInt(blockheight), + }), + ); + + return transactions; + }; + + fetchTransactions().then(console.log); + }, [blockheight]); + return
InspectBlock
; } From d1af3c02f3511c8a9361efc7ec003c83787748f1 Mon Sep 17 00:00:00 2001 From: "Jason M. Hasperhoven" Date: Mon, 17 Feb 2025 18:24:35 +0400 Subject: [PATCH 03/12] Setup block summary nextjs api route --- src/pages/inspect/block/api/block.ts | 13 ++++ src/pages/inspect/block/index.tsx | 30 +------- src/pages/inspect/block/ui/block-summary.tsx | 72 ++++++++++++++++++++ src/pages/inspect/block/ui/index.tsx | 61 +++++++++++++++++ src/shared/api/server/block/index.ts | 18 +++++ src/shared/api/server/block/types.ts | 23 +++++++ src/shared/database/index.ts | 8 +++ 7 files changed, 196 insertions(+), 29 deletions(-) create mode 100644 src/pages/inspect/block/api/block.ts create mode 100644 src/pages/inspect/block/ui/block-summary.tsx create mode 100644 src/pages/inspect/block/ui/index.tsx create mode 100644 src/shared/api/server/block/index.ts create mode 100644 src/shared/api/server/block/types.ts diff --git a/src/pages/inspect/block/api/block.ts b/src/pages/inspect/block/api/block.ts new file mode 100644 index 00000000..0fa36b71 --- /dev/null +++ b/src/pages/inspect/block/api/block.ts @@ -0,0 +1,13 @@ +import { useQuery } from '@tanstack/react-query'; +import { BlockSummaryApiResponse } from '@/shared/api/server/block/types'; + +export const useBlockSummary = (height: string) => { + return useQuery({ + queryKey: ['block', height], + retry: 1, + queryFn: async (): Promise => { + const response = await fetch(`/api/block/${height}`); + return response.json() as Promise; + }, + }); +}; diff --git a/src/pages/inspect/block/index.tsx b/src/pages/inspect/block/index.tsx index 9c0c4c86..3b6c41c1 100644 --- a/src/pages/inspect/block/index.tsx +++ b/src/pages/inspect/block/index.tsx @@ -1,29 +1 @@ -'use client'; - -import { penumbra } from '@/shared/const/penumbra'; -import { ViewService } from '@penumbra-zone/protobuf'; -import { useParams } from 'next/navigation'; -import { useEffect } from 'react'; - -export function InspectBlock() { - const params = useParams<{ height: string }>(); - const blockheight = params.height; - console.log('TCL: InspectBlock -> blockheight', blockheight); - - useEffect(() => { - const fetchTransactions = async () => { - const transactions = await Array.fromAsync( - penumbra.service(ViewService).transactionInfo({ - startHeight: BigInt(blockheight - 999), - endHeight: BigInt(blockheight), - }), - ); - - return transactions; - }; - - fetchTransactions().then(console.log); - }, [blockheight]); - - return
InspectBlock
; -} +export { InspectBlock } from './ui'; diff --git a/src/pages/inspect/block/ui/block-summary.tsx b/src/pages/inspect/block/ui/block-summary.tsx new file mode 100644 index 00000000..5640e65c --- /dev/null +++ b/src/pages/inspect/block/ui/block-summary.tsx @@ -0,0 +1,72 @@ +import { InfoCard } from '@/pages/explore/ui/info-card'; +import { Text } from '@penumbra-zone/ui/Text'; +import { BlockSummaryApiResponse } from '@/shared/api/server/block/types'; + +export function BlockSummary({ blockSummary }: { blockSummary: BlockSummaryApiResponse }) { + // rowid: number; + // height: number; + // time: Date; + // batch_swaps: BatchSwapSummary[]; + // num_open_lps: number; + // num_closed_lps: number; + // num_withdrawn_lps: number; + // num_swaps: number; + // num_swap_claims: number; + // num_txs: number; + + // asset_start: Buffer; + // asset_end: Buffer; + // input: string; + // output: string; + // num_swaps: number; + // price_float: number; + + return ( +
+
+ + + {blockSummary.num_txs} + + + + + {blockSummary.num_swaps} + + + + + {blockSummary.num_swap_claims} + + + + + {blockSummary.num_open_lps} + + + + + {blockSummary.num_closed_lps} + + + + + {blockSummary.num_withdrawn_lps} + + +
+
+ + Swaps + + {blockSummary.batch_swaps.map(swap => ( +
+ + {swap.asset_start.toString()} + +
+ ))} +
+
+ ); +} diff --git a/src/pages/inspect/block/ui/index.tsx b/src/pages/inspect/block/ui/index.tsx new file mode 100644 index 00000000..9cf8c7e4 --- /dev/null +++ b/src/pages/inspect/block/ui/index.tsx @@ -0,0 +1,61 @@ +'use client'; + +import { useParams } from 'next/navigation'; +import { useBlockSummary } from '../api/block'; +import { Card } from '@penumbra-zone/ui/Card'; +import { Skeleton } from '@/shared/ui/skeleton'; +import { BlockSummary } from './block-summary'; + +export function InspectBlock() { + const params = useParams<{ height: string }>(); + const blockheight = params.height; + // const { data: blockSummary, isLoading, isError } = useBlockSummary(blockheight); + const isError = false; + const isLoading = false; + const blockSummary = { + rowid: 1, + height: blockheight, + time: new Date(), + batch_swaps: [], + num_open_lps: 0, + num_closed_lps: 0, + num_withdrawn_lps: 0, + num_swaps: 0, + num_swap_claims: 0, + num_txs: 0, + }; + + return ( +
+
+ {isError ? ( + +
+ Something went wrong while fetching the transaction. +
+
+ ) : ( + +
+ {isLoading ? ( +
+
+ +
+
+ +
+
+ +
+
+ ) : ( + + )} +
+
+ )} +
+
+ ); +} diff --git a/src/shared/api/server/block/index.ts b/src/shared/api/server/block/index.ts new file mode 100644 index 00000000..899b3458 --- /dev/null +++ b/src/shared/api/server/block/index.ts @@ -0,0 +1,18 @@ +import { NextRequest, NextResponse } from 'next/server'; +import { pindexer } from '@/shared/database'; +import { BlockSummaryApiResponse } from '@/shared/api/server/block/types.ts'; + +export async function GET(req: NextRequest): Promise> { + const height = req.nextUrl.searchParams.get('height'); + if (!height) { + return NextResponse.json({ error: 'height is required' }, { status: 400 }); + } + + const blockSummary = await pindexer.getBlockSummary(Number(height)); + + if (!blockSummary) { + return NextResponse.json({ error: 'Block summary not found' }, { status: 404 }); + } + + return NextResponse.json(blockSummary); +} diff --git a/src/shared/api/server/block/types.ts b/src/shared/api/server/block/types.ts new file mode 100644 index 00000000..b8952bb7 --- /dev/null +++ b/src/shared/api/server/block/types.ts @@ -0,0 +1,23 @@ +export interface BatchSwapSummary { + asset_start: Buffer; + asset_end: Buffer; + input: string; + output: string; + num_swaps: number; + price_float: number; +} + +export type BlockSummaryApiResponse = + | { + rowid: number; + height: number; + time: Date; + batch_swaps: BatchSwapSummary[]; + num_open_lps: number; + num_closed_lps: number; + num_withdrawn_lps: number; + num_swaps: number; + num_swap_claims: number; + num_txs: number; + } + | { error: string }; diff --git a/src/shared/database/index.ts b/src/shared/database/index.ts index 30b8fb72..32e839cf 100644 --- a/src/shared/database/index.ts +++ b/src/shared/database/index.ts @@ -463,6 +463,14 @@ class Pindexer { executionCount: row.executionCount, })); } + + async getBlockSummary(height: number) { + return this.db + .selectFrom('dex_ex_block_summary') + .selectAll() + .where('height', '=', height) + .executeTakeFirst(); + } } export const pindexer = new Pindexer(); From 3323d3c3b753715d8518f0e26844636c24b1a986 Mon Sep 17 00:00:00 2001 From: "Jason M. Hasperhoven" Date: Tue, 18 Feb 2025 22:10:22 +0400 Subject: [PATCH 04/12] Improve display type for batchSwaps --- src/pages/inspect/block/ui/block-summary.tsx | 75 ++++++----- src/pages/inspect/block/ui/index.tsx | 127 ++++++++++++++++++- src/shared/api/server/block/index.ts | 76 ++++++++++- src/shared/api/server/block/types.ts | 28 +++- src/shared/database/index.ts | 3 +- src/shared/database/schema.ts | 22 ++++ 6 files changed, 280 insertions(+), 51 deletions(-) diff --git a/src/pages/inspect/block/ui/block-summary.tsx b/src/pages/inspect/block/ui/block-summary.tsx index 5640e65c..6eeff488 100644 --- a/src/pages/inspect/block/ui/block-summary.tsx +++ b/src/pages/inspect/block/ui/block-summary.tsx @@ -1,71 +1,78 @@ import { InfoCard } from '@/pages/explore/ui/info-card'; import { Text } from '@penumbra-zone/ui/Text'; +import { Table } from '@penumbra-zone/ui/Table'; import { BlockSummaryApiResponse } from '@/shared/api/server/block/types'; +import { ValueViewComponent } from '@penumbra-zone/ui/ValueView'; export function BlockSummary({ blockSummary }: { blockSummary: BlockSummaryApiResponse }) { - // rowid: number; - // height: number; - // time: Date; - // batch_swaps: BatchSwapSummary[]; - // num_open_lps: number; - // num_closed_lps: number; - // num_withdrawn_lps: number; - // num_swaps: number; - // num_swap_claims: number; - // num_txs: number; - - // asset_start: Buffer; - // asset_end: Buffer; - // input: string; - // output: string; - // num_swaps: number; - // price_float: number; + if ('error' in blockSummary) { + return
Error: {blockSummary.error}
; + } return (
-
+
- {blockSummary.num_txs} + {blockSummary.numTxs} - {blockSummary.num_swaps} + {blockSummary.numSwaps} - {blockSummary.num_swap_claims} + {blockSummary.numSwapClaims} - {blockSummary.num_open_lps} + {blockSummary.numOpenLps} - {blockSummary.num_closed_lps} + {blockSummary.numClosedLps} - {blockSummary.num_withdrawn_lps} + {blockSummary.numWithdrawnLps}
- - Swaps - - {blockSummary.batch_swaps.map(swap => ( -
- - {swap.asset_start.toString()} - -
- ))} +
+ + Swaps + +
+ + + + From + To + Number of Hops + + + + {blockSummary.batchSwaps.map(swap => ( + + + + + + + + + {swap.numSwaps} + + + ))} + +
); diff --git a/src/pages/inspect/block/ui/index.tsx b/src/pages/inspect/block/ui/index.tsx index 9cf8c7e4..bde6c341 100644 --- a/src/pages/inspect/block/ui/index.tsx +++ b/src/pages/inspect/block/ui/index.tsx @@ -5,6 +5,8 @@ import { useBlockSummary } from '../api/block'; import { Card } from '@penumbra-zone/ui/Card'; import { Skeleton } from '@/shared/ui/skeleton'; import { BlockSummary } from './block-summary'; +import { Metadata, ValueView } from '@penumbra-zone/protobuf/penumbra/core/asset/v1/asset_pb'; +import { Amount } from '@penumbra-zone/protobuf/penumbra/core/num/v1/num_pb'; export function InspectBlock() { const params = useParams<{ height: string }>(); @@ -16,13 +18,124 @@ export function InspectBlock() { rowid: 1, height: blockheight, time: new Date(), - batch_swaps: [], - num_open_lps: 0, - num_closed_lps: 0, - num_withdrawn_lps: 0, - num_swaps: 0, - num_swap_claims: 0, - num_txs: 0, + batchSwaps: [ + { + startAsset: new Metadata({ + display: 'penumbra', + base: 'upenumbra', + denomUnits: [ + { denom: 'penumbra', exponent: 6 }, + { denom: 'upenumbra', exponent: 0 }, + ], + }), + endAsset: new Metadata({ + display: 'penumbra', + base: 'upenumbra', + denomUnits: [ + { denom: 'penumbra', exponent: 6 }, + { denom: 'upenumbra', exponent: 0 }, + ], + }), + startPrice: 1.0, + endPrice: 2.0, + startAmount: '11.11', + endAmount: '22.22', + startValueView: new ValueView({ + valueView: { + case: 'knownAssetId', + value: { + amount: new Amount({ lo: 100n, hi: 0n }), + metadata: new Metadata({ + display: 'penumbra', + base: 'upenumbra', + denomUnits: [ + { denom: 'penumbra', exponent: 6 }, + { denom: 'upenumbra', exponent: 0 }, + ], + }), + }, + }, + }), + endValueView: new ValueView({ + valueView: { + case: 'knownAssetId', + value: { + amount: new Amount({ lo: 100n, hi: 0n }), + metadata: new Metadata({ + display: 'penumbra', + base: 'upenumbra', + denomUnits: [ + { denom: 'penumbra', exponent: 6 }, + { denom: 'upenumbra', exponent: 0 }, + ], + }), + }, + }, + }), + numSwaps: 3, + }, + { + startAsset: new Metadata({ + display: 'penumbra', + base: 'upenumbra', + denomUnits: [ + { denom: 'penumbra', exponent: 6 }, + { denom: 'upenumbra', exponent: 0 }, + ], + }), + endAsset: new Metadata({ + display: 'penumbra', + base: 'upenumbra', + denomUnits: [ + { denom: 'penumbra', exponent: 6 }, + { denom: 'upenumbra', exponent: 0 }, + ], + }), + startPrice: 1.0, + endPrice: 2.0, + startAmount: '11.11', + endAmount: '22.22', + startValueView: new ValueView({ + valueView: { + case: 'knownAssetId', + value: { + amount: new Amount({ lo: 100n, hi: 0n }), + metadata: new Metadata({ + display: 'penumbra', + base: 'upenumbra', + denomUnits: [ + { denom: 'penumbra', exponent: 6 }, + { denom: 'upenumbra', exponent: 0 }, + ], + }), + }, + }, + }), + endValueView: new ValueView({ + valueView: { + case: 'knownAssetId', + value: { + amount: new Amount({ lo: 100n, hi: 0n }), + metadata: new Metadata({ + display: 'penumbra', + base: 'upenumbra', + denomUnits: [ + { denom: 'penumbra', exponent: 6 }, + { denom: 'upenumbra', exponent: 0 }, + ], + }), + }, + }, + }), + numSwaps: 2, + }, + ], + numOpenLps: 0, + numClosedLps: 0, + numWithdrawnLps: 0, + numSwaps: 0, + numSwapClaims: 0, + numTxs: 0, }; return ( diff --git a/src/shared/api/server/block/index.ts b/src/shared/api/server/block/index.ts index 899b3458..77c1ce4d 100644 --- a/src/shared/api/server/block/index.ts +++ b/src/shared/api/server/block/index.ts @@ -1,18 +1,90 @@ import { NextRequest, NextResponse } from 'next/server'; import { pindexer } from '@/shared/database'; -import { BlockSummaryApiResponse } from '@/shared/api/server/block/types.ts'; +import { + BlockSummaryApiResponse, + BatchSwapSummaryDisplay, +} from '@/shared/api/server/block/types.ts'; +import { AssetId } from '@penumbra-zone/protobuf/penumbra/core/asset/v1/asset_pb'; +import { ChainRegistryClient, Registry } from '@penumbra-labs/registry'; +import { getDisplayDenomExponent } from '@penumbra-zone/getters/metadata'; +import { pnum } from '@penumbra-zone/types/pnum'; +import { BatchSwapSummary } from '@/shared/database/schema'; + +export const getBatchSwapDisplayData = + (registry: Registry) => + (batchSwapSummary: BatchSwapSummary): BatchSwapSummaryDisplay => { + const startAssetId = new AssetId({ + inner: Uint8Array.from(batchSwapSummary.asset_start), + }); + const startMetadata = registry.getMetadata(startAssetId); + const startExponent = getDisplayDenomExponent.optional(startMetadata) ?? 0; + + const endAssetId = new AssetId({ inner: Uint8Array.from(batchSwapSummary.asset_end) }); + const endMetadata = registry.getMetadata(endAssetId); + const endExponent = getDisplayDenomExponent.optional(endMetadata) ?? 0; + + return { + startAsset: startMetadata, + endAsset: endMetadata, + startPrice: Number(batchSwapSummary.output) / Number(batchSwapSummary.input), + endPrice: Number(batchSwapSummary.input) / Number(batchSwapSummary.output), + startAmount: pnum( + // convert string to bigint so that pnum parses it in base units + // which means we can use the exponent to format it in display units + batchSwapSummary.input, + startExponent, + ).toFormattedString(), + startValueView: pnum( + // convert string to bigint so that pnum parses it in base units + // which means we can use the exponent to format it in display units + batchSwapSummary.input, + startExponent, + ).toValueView(), + endAmount: pnum( + // convert string to bigint so that pnum parses it in base units + // which means we can use the exponent to format it in display units + batchSwapSummary.output, + endExponent, + ).toFormattedString(), + endValueView: pnum( + // convert string to bigint so that pnum parses it in base units + // which means we can use the exponent to format it in display units + batchSwapSummary.output, + endExponent, + ).toValueView(), + numSwaps: batchSwapSummary.num_swaps, + }; + }; export async function GET(req: NextRequest): Promise> { + const chainId = process.env['PENUMBRA_CHAIN_ID']; + if (!chainId) { + return NextResponse.json({ error: 'PENUMBRA_CHAIN_ID is not set' }, { status: 500 }); + } + const height = req.nextUrl.searchParams.get('height'); if (!height) { return NextResponse.json({ error: 'height is required' }, { status: 400 }); } + const registryClient = new ChainRegistryClient(); + const registry = await registryClient.remote.get(chainId); + const blockSummary = await pindexer.getBlockSummary(Number(height)); if (!blockSummary) { return NextResponse.json({ error: 'Block summary not found' }, { status: 404 }); } - return NextResponse.json(blockSummary); + return NextResponse.json({ + height: blockSummary.height, + time: blockSummary.time, + batchSwaps: blockSummary.batch_swaps.map(getBatchSwapDisplayData(registry)), + numOpenLps: blockSummary.num_open_lps, + numClosedLps: blockSummary.num_closed_lps, + numWithdrawnLps: blockSummary.num_withdrawn_lps, + numSwaps: blockSummary.num_swaps, + numSwapClaims: blockSummary.num_swap_claims, + numTxs: blockSummary.num_txs, + }); } diff --git a/src/shared/api/server/block/types.ts b/src/shared/api/server/block/types.ts index b8952bb7..8c04ec24 100644 --- a/src/shared/api/server/block/types.ts +++ b/src/shared/api/server/block/types.ts @@ -1,3 +1,17 @@ +import { Metadata, ValueView } from '@penumbra-zone/protobuf/penumbra/core/asset/v1/asset_pb'; + +export interface BatchSwapSummaryDisplay { + startAsset: Metadata; + endAsset: Metadata; + startPrice: number; + endPrice: number; + startAmount: string; + endAmount: string; + startValueView: ValueView; + endValueView: ValueView; + numSwaps: number; +} + export interface BatchSwapSummary { asset_start: Buffer; asset_end: Buffer; @@ -12,12 +26,12 @@ export type BlockSummaryApiResponse = rowid: number; height: number; time: Date; - batch_swaps: BatchSwapSummary[]; - num_open_lps: number; - num_closed_lps: number; - num_withdrawn_lps: number; - num_swaps: number; - num_swap_claims: number; - num_txs: number; + batchSwaps: BatchSwapSummaryDisplay[]; + numOpenLps: number; + numClosedLps: number; + numWithdrawnLps: number; + numSwaps: number; + numSwapClaims: number; + numTxs: number; } | { error: string }; diff --git a/src/shared/database/index.ts b/src/shared/database/index.ts index 32e839cf..ba326267 100644 --- a/src/shared/database/index.ts +++ b/src/shared/database/index.ts @@ -7,6 +7,7 @@ import { DexExPositionExecutions, DexExPositionReserves, DexExPositionWithdrawals, + DexExBlockSummary, } from '@/shared/database/schema.ts'; import { AssetId } from '@penumbra-zone/protobuf/penumbra/core/asset/v1/asset_pb'; import { DurationWindow } from '@/shared/utils/duration.ts'; @@ -464,7 +465,7 @@ class Pindexer { })); } - async getBlockSummary(height: number) { + async getBlockSummary(height: number): Promise | undefined> { return this.db .selectFrom('dex_ex_block_summary') .selectAll() diff --git a/src/shared/database/schema.ts b/src/shared/database/schema.ts index a0b68708..a42a884d 100644 --- a/src/shared/database/schema.ts +++ b/src/shared/database/schema.ts @@ -197,6 +197,28 @@ export interface DexExPriceCharts { the_window: DurationWindow; } +export interface BatchSwapSummary { + asset_start: Buffer; + asset_end: Buffer; + input: string; + output: string; + num_swaps: number; + price_float: number; +} + +export interface DexExBlockSummary { + rowid: number; + height: number; + time: Date; + batch_swaps: BatchSwapSummary[]; + num_open_lps: number; + num_closed_lps: number; + num_withdrawn_lps: number; + num_swaps: number; + num_swap_claims: number; + num_txs: number; +} + export interface GovernanceDelegatorVotes { block_height: Int8; id: Generated; From c7606aa46f4eb840da71cc3a75ca2a7ec6817954 Mon Sep 17 00:00:00 2001 From: "Jason M. Hasperhoven" Date: Wed, 19 Feb 2025 17:53:08 +0400 Subject: [PATCH 05/12] Parse batchSwaps from pindexer --- app/api/block/[height]/route.ts | 1 + pnpm-lock.yaml | 4 +- src/pages/inspect/block/api/block.ts | 3 + src/pages/inspect/block/ui/block-summary.tsx | 46 +++++-- src/pages/inspect/block/ui/index.tsx | 137 +------------------ src/shared/api/server/block/index.ts | 30 ++-- src/shared/api/server/block/types.ts | 8 +- src/shared/database/index.ts | 54 +++++++- src/shared/database/schema.ts | 2 +- 9 files changed, 127 insertions(+), 158 deletions(-) create mode 100644 app/api/block/[height]/route.ts diff --git a/app/api/block/[height]/route.ts b/app/api/block/[height]/route.ts new file mode 100644 index 00000000..ebb9032d --- /dev/null +++ b/app/api/block/[height]/route.ts @@ -0,0 +1 @@ +export { GET } from '@/shared/api/server/block'; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index db516271..4966c845 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -9623,7 +9623,7 @@ snapshots: safe-regex-test: 1.0.3 string.prototype.includes: 2.0.0 - eslint-plugin-prettier@5.2.1(@types/eslint@9.6.1)(eslint-config-prettier@9.1.0(eslint@9.10.0(jiti@2.4.2)))(eslint@9.10.0(jiti@2.4.2))(prettier@3.4.2): + eslint-plugin-prettier@5.2.1(@types/eslint@9.6.1)(eslint-config-prettier@9.1.0(eslint@8.57.0))(eslint@9.10.0(jiti@2.4.2))(prettier@3.4.2): dependencies: eslint: 9.10.0(jiti@2.4.2) prettier: 3.4.2 @@ -10866,7 +10866,7 @@ snapshots: eslint-import-resolver-typescript: 3.6.3(@typescript-eslint/parser@7.18.0(eslint@8.57.0)(typescript@5.7.2))(eslint-plugin-import-x@0.5.3(eslint@8.57.0)(typescript@5.7.2))(eslint-plugin-import@2.30.0)(eslint@9.10.0(jiti@2.4.2)) eslint-plugin-import: 2.30.0(@typescript-eslint/parser@7.18.0(eslint@8.57.0)(typescript@5.7.2))(eslint@8.57.0) eslint-plugin-import-x: 0.5.3(eslint@9.10.0(jiti@2.4.2))(typescript@5.7.2) - eslint-plugin-prettier: 5.2.1(@types/eslint@9.6.1)(eslint-config-prettier@9.1.0(eslint@9.10.0(jiti@2.4.2)))(eslint@9.10.0(jiti@2.4.2))(prettier@3.4.2) + eslint-plugin-prettier: 5.2.1(@types/eslint@9.6.1)(eslint-config-prettier@9.1.0(eslint@8.57.0))(eslint@9.10.0(jiti@2.4.2))(prettier@3.4.2) eslint-plugin-react: 7.37.2(eslint@9.10.0(jiti@2.4.2)) eslint-plugin-react-hooks: 5.1.0-rc-c3cdbec0a7-20240708(eslint@9.10.0(jiti@2.4.2)) eslint-plugin-react-refresh: 0.4.11(eslint@9.10.0(jiti@2.4.2)) diff --git a/src/pages/inspect/block/api/block.ts b/src/pages/inspect/block/api/block.ts index 0fa36b71..55e7a5fa 100644 --- a/src/pages/inspect/block/api/block.ts +++ b/src/pages/inspect/block/api/block.ts @@ -6,6 +6,9 @@ export const useBlockSummary = (height: string) => { queryKey: ['block', height], retry: 1, queryFn: async (): Promise => { + if (!height) { + throw new Error('Invalid block height'); + } const response = await fetch(`/api/block/${height}`); return response.json() as Promise; }, diff --git a/src/pages/inspect/block/ui/block-summary.tsx b/src/pages/inspect/block/ui/block-summary.tsx index 6eeff488..6757692d 100644 --- a/src/pages/inspect/block/ui/block-summary.tsx +++ b/src/pages/inspect/block/ui/block-summary.tsx @@ -3,6 +3,7 @@ import { Text } from '@penumbra-zone/ui/Text'; import { Table } from '@penumbra-zone/ui/Table'; import { BlockSummaryApiResponse } from '@/shared/api/server/block/types'; import { ValueViewComponent } from '@penumbra-zone/ui/ValueView'; +import { pnum } from '@penumbra-zone/types/pnum'; export function BlockSummary({ blockSummary }: { blockSummary: BlockSummaryApiResponse }) { if ('error' in blockSummary) { @@ -54,23 +55,44 @@ export function BlockSummary({ blockSummary }: { blockSummary: BlockSummaryApiRe From To + Price Number of Hops - {blockSummary.batchSwaps.map(swap => ( - - - - - - - - - {swap.numSwaps} - + {blockSummary.batchSwaps.length ? ( + blockSummary.batchSwaps.map(swap => ( + + + + + + + + + + {swap.endPrice} {swap.endAsset.symbol} + + + + {swap.numSwaps} + + + )) + ) : ( + + -- + -- + -- + -- - ))} + )}
diff --git a/src/pages/inspect/block/ui/index.tsx b/src/pages/inspect/block/ui/index.tsx index bde6c341..6a586821 100644 --- a/src/pages/inspect/block/ui/index.tsx +++ b/src/pages/inspect/block/ui/index.tsx @@ -5,138 +5,11 @@ import { useBlockSummary } from '../api/block'; import { Card } from '@penumbra-zone/ui/Card'; import { Skeleton } from '@/shared/ui/skeleton'; import { BlockSummary } from './block-summary'; -import { Metadata, ValueView } from '@penumbra-zone/protobuf/penumbra/core/asset/v1/asset_pb'; -import { Amount } from '@penumbra-zone/protobuf/penumbra/core/num/v1/num_pb'; export function InspectBlock() { const params = useParams<{ height: string }>(); - const blockheight = params.height; - // const { data: blockSummary, isLoading, isError } = useBlockSummary(blockheight); - const isError = false; - const isLoading = false; - const blockSummary = { - rowid: 1, - height: blockheight, - time: new Date(), - batchSwaps: [ - { - startAsset: new Metadata({ - display: 'penumbra', - base: 'upenumbra', - denomUnits: [ - { denom: 'penumbra', exponent: 6 }, - { denom: 'upenumbra', exponent: 0 }, - ], - }), - endAsset: new Metadata({ - display: 'penumbra', - base: 'upenumbra', - denomUnits: [ - { denom: 'penumbra', exponent: 6 }, - { denom: 'upenumbra', exponent: 0 }, - ], - }), - startPrice: 1.0, - endPrice: 2.0, - startAmount: '11.11', - endAmount: '22.22', - startValueView: new ValueView({ - valueView: { - case: 'knownAssetId', - value: { - amount: new Amount({ lo: 100n, hi: 0n }), - metadata: new Metadata({ - display: 'penumbra', - base: 'upenumbra', - denomUnits: [ - { denom: 'penumbra', exponent: 6 }, - { denom: 'upenumbra', exponent: 0 }, - ], - }), - }, - }, - }), - endValueView: new ValueView({ - valueView: { - case: 'knownAssetId', - value: { - amount: new Amount({ lo: 100n, hi: 0n }), - metadata: new Metadata({ - display: 'penumbra', - base: 'upenumbra', - denomUnits: [ - { denom: 'penumbra', exponent: 6 }, - { denom: 'upenumbra', exponent: 0 }, - ], - }), - }, - }, - }), - numSwaps: 3, - }, - { - startAsset: new Metadata({ - display: 'penumbra', - base: 'upenumbra', - denomUnits: [ - { denom: 'penumbra', exponent: 6 }, - { denom: 'upenumbra', exponent: 0 }, - ], - }), - endAsset: new Metadata({ - display: 'penumbra', - base: 'upenumbra', - denomUnits: [ - { denom: 'penumbra', exponent: 6 }, - { denom: 'upenumbra', exponent: 0 }, - ], - }), - startPrice: 1.0, - endPrice: 2.0, - startAmount: '11.11', - endAmount: '22.22', - startValueView: new ValueView({ - valueView: { - case: 'knownAssetId', - value: { - amount: new Amount({ lo: 100n, hi: 0n }), - metadata: new Metadata({ - display: 'penumbra', - base: 'upenumbra', - denomUnits: [ - { denom: 'penumbra', exponent: 6 }, - { denom: 'upenumbra', exponent: 0 }, - ], - }), - }, - }, - }), - endValueView: new ValueView({ - valueView: { - case: 'knownAssetId', - value: { - amount: new Amount({ lo: 100n, hi: 0n }), - metadata: new Metadata({ - display: 'penumbra', - base: 'upenumbra', - denomUnits: [ - { denom: 'penumbra', exponent: 6 }, - { denom: 'upenumbra', exponent: 0 }, - ], - }), - }, - }, - }), - numSwaps: 2, - }, - ], - numOpenLps: 0, - numClosedLps: 0, - numWithdrawnLps: 0, - numSwaps: 0, - numSwapClaims: 0, - numTxs: 0, - }; + const blockheight = params?.height; + const { data: blockSummary, isError } = useBlockSummary(blockheight ?? ''); return (
@@ -150,7 +23,9 @@ export function InspectBlock() { ) : (
- {isLoading ? ( + {blockSummary ? ( + + ) : (
@@ -162,8 +37,6 @@ export function InspectBlock() {
- ) : ( - )}
diff --git a/src/shared/api/server/block/index.ts b/src/shared/api/server/block/index.ts index 77c1ce4d..cf184c71 100644 --- a/src/shared/api/server/block/index.ts +++ b/src/shared/api/server/block/index.ts @@ -14,20 +14,30 @@ export const getBatchSwapDisplayData = (registry: Registry) => (batchSwapSummary: BatchSwapSummary): BatchSwapSummaryDisplay => { const startAssetId = new AssetId({ - inner: Uint8Array.from(batchSwapSummary.asset_start), + inner: batchSwapSummary.asset_start, }); const startMetadata = registry.getMetadata(startAssetId); const startExponent = getDisplayDenomExponent.optional(startMetadata) ?? 0; - const endAssetId = new AssetId({ inner: Uint8Array.from(batchSwapSummary.asset_end) }); + const endAssetId = new AssetId({ inner: batchSwapSummary.asset_end }); const endMetadata = registry.getMetadata(endAssetId); const endExponent = getDisplayDenomExponent.optional(endMetadata) ?? 0; return { startAsset: startMetadata, endAsset: endMetadata, - startPrice: Number(batchSwapSummary.output) / Number(batchSwapSummary.input), - endPrice: Number(batchSwapSummary.input) / Number(batchSwapSummary.output), + startInput: batchSwapSummary.input, + endOutput: batchSwapSummary.output, + startExponent, + endExponent, + startPrice: pnum( + Number(batchSwapSummary.output) / Number(batchSwapSummary.input), + startExponent, + ).toFormattedString(), + endPrice: pnum( + Number(batchSwapSummary.input) / Number(batchSwapSummary.output), + endExponent, + ).toFormattedString(), startAmount: pnum( // convert string to bigint so that pnum parses it in base units // which means we can use the exponent to format it in display units @@ -39,7 +49,7 @@ export const getBatchSwapDisplayData = // which means we can use the exponent to format it in display units batchSwapSummary.input, startExponent, - ).toValueView(), + ).toValueView(startMetadata), endAmount: pnum( // convert string to bigint so that pnum parses it in base units // which means we can use the exponent to format it in display units @@ -51,18 +61,21 @@ export const getBatchSwapDisplayData = // which means we can use the exponent to format it in display units batchSwapSummary.output, endExponent, - ).toValueView(), + ).toValueView(endMetadata), numSwaps: batchSwapSummary.num_swaps, }; }; -export async function GET(req: NextRequest): Promise> { +export async function GET( + _req: NextRequest, + { params }: { params: { height: string } }, +): Promise> { const chainId = process.env['PENUMBRA_CHAIN_ID']; if (!chainId) { return NextResponse.json({ error: 'PENUMBRA_CHAIN_ID is not set' }, { status: 500 }); } - const height = req.nextUrl.searchParams.get('height'); + const height = params.height; if (!height) { return NextResponse.json({ error: 'height is required' }, { status: 400 }); } @@ -71,6 +84,7 @@ export async function GET(req: NextRequest): Promise | undefined> { - return this.db + const result = await this.db .selectFrom('dex_ex_block_summary') .selectAll() .where('height', '=', height) .executeTakeFirst(); + + if (!result) { + return undefined; + } + + function parseBatchSwaps(rawBatchSwaps: string): BatchSwapSummary[] { + const parseableBatchSwaps = rawBatchSwaps + // Remove escaped double quotes + .replace(/\\"/g, '"') + // convert to parseable array + .replace(/^\{/, '[') + .replace(/\}$/, ']') + // convert tuple to array + .replace(/"\(/g, '[') + .replace(/\)"/g, ']'); + + const batchSwaps = JSON.parse(parseableBatchSwaps) as BatchSwapSummary[]; + + const mapping = { + 0: 'asset_start', + 1: 'asset_end', + 2: 'input', + 3: 'output', + 4: 'num_swaps', + 5: 'price_float', + }; + + const hexRegex = /\\x[0-9a-fA-F]+/; + + const parseHex = (value: string) => { + const cleanValue = value.replace(/\\+x/g, ''); + return Buffer.from(hexToUint8Array(cleanValue)); + }; + + return batchSwaps.map(batchSwap => { + return batchSwap.reduce( + (acc: object, value: unknown, index: number) => ({ + ...acc, + [mapping[index as keyof typeof mapping]]: + typeof value === 'string' && hexRegex.test(value) ? parseHex(value) : value, + }), + {}, + ) as BatchSwapSummary; + }); + } + + return { + ...result, + batch_swaps: parseBatchSwaps(result.batch_swaps), + }; } } diff --git a/src/shared/database/schema.ts b/src/shared/database/schema.ts index a42a884d..3085b1a7 100644 --- a/src/shared/database/schema.ts +++ b/src/shared/database/schema.ts @@ -210,7 +210,7 @@ export interface DexExBlockSummary { rowid: number; height: number; time: Date; - batch_swaps: BatchSwapSummary[]; + batch_swaps: string; num_open_lps: number; num_closed_lps: number; num_withdrawn_lps: number; From deb152b80710b25e2642d3732816533261581d91 Mon Sep 17 00:00:00 2001 From: "Jason M. Hasperhoven" Date: Wed, 19 Feb 2025 18:12:09 +0400 Subject: [PATCH 06/12] Fix lint issues --- scripts/generate-pindexer-schema.ts | 1 + src/shared/api/server/block/index.ts | 1 - src/shared/api/server/block/types.ts | 16 +++++++++++++++- src/shared/database/index.ts | 15 +++++++++------ src/shared/database/schema.ts | 2 ++ 5 files changed, 27 insertions(+), 8 deletions(-) diff --git a/scripts/generate-pindexer-schema.ts b/scripts/generate-pindexer-schema.ts index 30ef3d76..8885bbde 100644 --- a/scripts/generate-pindexer-schema.ts +++ b/scripts/generate-pindexer-schema.ts @@ -14,6 +14,7 @@ const pindexerTableWhitelist = [ 'dex_ex_position_withdrawals', 'dex_ex_batch_swap_traces', 'dex_ex_metadata', + 'dex_ex_block_summary', ]; const envFileReady = (): boolean => { diff --git a/src/shared/api/server/block/index.ts b/src/shared/api/server/block/index.ts index cf184c71..461982cb 100644 --- a/src/shared/api/server/block/index.ts +++ b/src/shared/api/server/block/index.ts @@ -84,7 +84,6 @@ export async function GET( const registry = await registryClient.remote.get(chainId); const blockSummary = await pindexer.getBlockSummary(Number(height)); - console.log('TCL: blockSummary', blockSummary.batch_swaps); if (!blockSummary) { return NextResponse.json({ error: 'Block summary not found' }, { status: 404 }); diff --git a/src/shared/api/server/block/types.ts b/src/shared/api/server/block/types.ts index 679dde52..29a759be 100644 --- a/src/shared/api/server/block/types.ts +++ b/src/shared/api/server/block/types.ts @@ -27,7 +27,6 @@ export interface BatchSwapSummary { export type BlockSummaryApiResponse = | { - rowid: number; height: number; time: Date; batchSwaps: BatchSwapSummaryDisplay[]; @@ -39,3 +38,18 @@ export type BlockSummaryApiResponse = numTxs: number; } | { error: string }; + +export type BlockSummaryPindexerResponse = + | { + rowid: number; + height: number; + time: Date; + batch_swaps: BatchSwapSummary[]; + num_open_lps: number; + num_closed_lps: number; + num_withdrawn_lps: number; + num_swaps: number; + num_swap_claims: number; + num_txs: number; + } + | undefined; diff --git a/src/shared/database/index.ts b/src/shared/database/index.ts index 28383a51..20d77d5c 100644 --- a/src/shared/database/index.ts +++ b/src/shared/database/index.ts @@ -14,6 +14,7 @@ import { AssetId } from '@penumbra-zone/protobuf/penumbra/core/asset/v1/asset_pb import { DurationWindow } from '@/shared/utils/duration.ts'; import { PositionId } from '@penumbra-zone/protobuf/penumbra/core/component/dex/v1/dex_pb'; import { hexToUint8Array } from '@penumbra-zone/types/hex'; +import { BlockSummaryPindexerResponse } from '../api/server/block/types'; const MAINNET_CHAIN_ID = 'penumbra-1'; @@ -467,12 +468,12 @@ class Pindexer { })); } - async getBlockSummary(height: number): Promise | undefined> { - const result = await this.db + async getBlockSummary(height: number): Promise { + const result = (await this.db .selectFrom('dex_ex_block_summary') .selectAll() .where('height', '=', height) - .executeTakeFirst(); + .executeTakeFirst()) as DexExBlockSummary | undefined; if (!result) { return undefined; @@ -489,7 +490,7 @@ class Pindexer { .replace(/"\(/g, '[') .replace(/\)"/g, ']'); - const batchSwaps = JSON.parse(parseableBatchSwaps) as BatchSwapSummary[]; + const batchSwaps = JSON.parse(parseableBatchSwaps) as string[][]; const mapping = { 0: 'asset_start', @@ -508,14 +509,16 @@ class Pindexer { }; return batchSwaps.map(batchSwap => { - return batchSwap.reduce( + const parsedBatchSwap = batchSwap.reduce( (acc: object, value: unknown, index: number) => ({ ...acc, [mapping[index as keyof typeof mapping]]: typeof value === 'string' && hexRegex.test(value) ? parseHex(value) : value, }), {}, - ) as BatchSwapSummary; + ); + + return parsedBatchSwap as unknown as BatchSwapSummary; }); } diff --git a/src/shared/database/schema.ts b/src/shared/database/schema.ts index 3085b1a7..e0577400 100644 --- a/src/shared/database/schema.ts +++ b/src/shared/database/schema.ts @@ -364,6 +364,7 @@ interface RawDB { dex_ex_position_state: DexExPositionState; dex_ex_position_withdrawals: DexExPositionWithdrawals; dex_ex_price_charts: DexExPriceCharts; + dex_ex_block_summary: DexExBlockSummary; governance_delegator_votes: GovernanceDelegatorVotes; governance_proposals: GovernanceProposals; governance_validator_votes: GovernanceValidatorVotes; @@ -393,4 +394,5 @@ export type DB = Pick< | 'dex_ex_position_withdrawals' | 'dex_ex_batch_swap_traces' | 'dex_ex_metadata' + | 'dex_ex_block_summary' >; From 16f047613ebff19401970fc89a292f7272ee00db Mon Sep 17 00:00:00 2001 From: "Jason M. Hasperhoven" Date: Mon, 24 Feb 2025 18:56:40 +0400 Subject: [PATCH 07/12] Parse from jsonb --- src/shared/api/server/block/index.ts | 76 ++++++++++++++-------------- src/shared/api/server/block/types.ts | 9 +--- src/shared/database/index.ts | 52 +------------------ 3 files changed, 40 insertions(+), 97 deletions(-) diff --git a/src/shared/api/server/block/index.ts b/src/shared/api/server/block/index.ts index 461982cb..d68de387 100644 --- a/src/shared/api/server/block/index.ts +++ b/src/shared/api/server/block/index.ts @@ -9,59 +9,59 @@ import { ChainRegistryClient, Registry } from '@penumbra-labs/registry'; import { getDisplayDenomExponent } from '@penumbra-zone/getters/metadata'; import { pnum } from '@penumbra-zone/types/pnum'; import { BatchSwapSummary } from '@/shared/database/schema'; +import { hexToUint8Array, base64ToHex } from '@penumbra-zone/types/hex'; +import { joinLoHi, LoHi } from '@penumbra-zone/types/lo-hi'; + +function isLoHi(value: unknown): value is LoHi { + return typeof value === 'object' && value !== null && 'lo' in value; +} + +interface AssetWithInner { + inner: string; +} + +function isAsset(value: unknown): value is AssetWithInner { + return ( + typeof value === 'object' && + value !== null && + 'inner' in value && + value.inner !== null && + typeof (value as AssetWithInner).inner === 'string' + ); +} export const getBatchSwapDisplayData = (registry: Registry) => (batchSwapSummary: BatchSwapSummary): BatchSwapSummaryDisplay => { + if (!isLoHi(batchSwapSummary.input) || !isLoHi(batchSwapSummary.output)) { + throw new Error('Invalid input or output: expected LoHi type'); + } + + if (!isAsset(batchSwapSummary.asset_start) || !isAsset(batchSwapSummary.asset_end)) { + throw new Error('Invalid asset_start or asset_end'); + } + const startAssetId = new AssetId({ - inner: batchSwapSummary.asset_start, + inner: hexToUint8Array(base64ToHex(batchSwapSummary.asset_start.inner)), }); const startMetadata = registry.getMetadata(startAssetId); const startExponent = getDisplayDenomExponent.optional(startMetadata) ?? 0; - const endAssetId = new AssetId({ inner: batchSwapSummary.asset_end }); + const endAssetId = new AssetId({ + inner: hexToUint8Array(base64ToHex(batchSwapSummary.asset_end.inner)), + }); const endMetadata = registry.getMetadata(endAssetId); const endExponent = getDisplayDenomExponent.optional(endMetadata) ?? 0; + const inputBigInt = joinLoHi(batchSwapSummary.input.lo, batchSwapSummary.input.hi); + const outputBigInt = joinLoHi(batchSwapSummary.output.lo, batchSwapSummary.output.hi); + return { startAsset: startMetadata, endAsset: endMetadata, - startInput: batchSwapSummary.input, - endOutput: batchSwapSummary.output, - startExponent, - endExponent, - startPrice: pnum( - Number(batchSwapSummary.output) / Number(batchSwapSummary.input), - startExponent, - ).toFormattedString(), - endPrice: pnum( - Number(batchSwapSummary.input) / Number(batchSwapSummary.output), - endExponent, - ).toFormattedString(), - startAmount: pnum( - // convert string to bigint so that pnum parses it in base units - // which means we can use the exponent to format it in display units - batchSwapSummary.input, - startExponent, - ).toFormattedString(), - startValueView: pnum( - // convert string to bigint so that pnum parses it in base units - // which means we can use the exponent to format it in display units - batchSwapSummary.input, - startExponent, - ).toValueView(startMetadata), - endAmount: pnum( - // convert string to bigint so that pnum parses it in base units - // which means we can use the exponent to format it in display units - batchSwapSummary.output, - endExponent, - ).toFormattedString(), - endValueView: pnum( - // convert string to bigint so that pnum parses it in base units - // which means we can use the exponent to format it in display units - batchSwapSummary.output, - endExponent, - ).toValueView(endMetadata), + startInput: pnum(inputBigInt, startExponent).toString(), + endOutput: pnum(outputBigInt, endExponent).toString(), + endPrice: pnum(outputBigInt / inputBigInt, endExponent).toFormattedString(), numSwaps: batchSwapSummary.num_swaps, }; }; diff --git a/src/shared/api/server/block/types.ts b/src/shared/api/server/block/types.ts index 29a759be..7f6802b7 100644 --- a/src/shared/api/server/block/types.ts +++ b/src/shared/api/server/block/types.ts @@ -1,18 +1,11 @@ -import { Metadata, ValueView } from '@penumbra-zone/protobuf/penumbra/core/asset/v1/asset_pb'; +import { Metadata } from '@penumbra-zone/protobuf/penumbra/core/asset/v1/asset_pb'; export interface BatchSwapSummaryDisplay { startAsset: Metadata; endAsset: Metadata; - startExponent: number; - endExponent: number; startInput: string; endOutput: string; - startPrice: string; endPrice: string; - startAmount: string; - endAmount: string; - startValueView: ValueView; - endValueView: ValueView; numSwaps: number; } diff --git a/src/shared/database/index.ts b/src/shared/database/index.ts index 20d77d5c..a00ead8f 100644 --- a/src/shared/database/index.ts +++ b/src/shared/database/index.ts @@ -475,57 +475,7 @@ class Pindexer { .where('height', '=', height) .executeTakeFirst()) as DexExBlockSummary | undefined; - if (!result) { - return undefined; - } - - function parseBatchSwaps(rawBatchSwaps: string): BatchSwapSummary[] { - const parseableBatchSwaps = rawBatchSwaps - // Remove escaped double quotes - .replace(/\\"/g, '"') - // convert to parseable array - .replace(/^\{/, '[') - .replace(/\}$/, ']') - // convert tuple to array - .replace(/"\(/g, '[') - .replace(/\)"/g, ']'); - - const batchSwaps = JSON.parse(parseableBatchSwaps) as string[][]; - - const mapping = { - 0: 'asset_start', - 1: 'asset_end', - 2: 'input', - 3: 'output', - 4: 'num_swaps', - 5: 'price_float', - }; - - const hexRegex = /\\x[0-9a-fA-F]+/; - - const parseHex = (value: string) => { - const cleanValue = value.replace(/\\+x/g, ''); - return Buffer.from(hexToUint8Array(cleanValue)); - }; - - return batchSwaps.map(batchSwap => { - const parsedBatchSwap = batchSwap.reduce( - (acc: object, value: unknown, index: number) => ({ - ...acc, - [mapping[index as keyof typeof mapping]]: - typeof value === 'string' && hexRegex.test(value) ? parseHex(value) : value, - }), - {}, - ); - - return parsedBatchSwap as unknown as BatchSwapSummary; - }); - } - - return { - ...result, - batch_swaps: parseBatchSwaps(result.batch_swaps), - }; + return result; } } From 27c5b0f4d7d18483c0fd89bededdcdffef74aef4 Mon Sep 17 00:00:00 2001 From: "Jason M. Hasperhoven" Date: Mon, 24 Feb 2025 19:09:58 +0400 Subject: [PATCH 08/12] Update batch swaps type --- src/shared/database/index.ts | 2 -- src/shared/database/schema.ts | 2 +- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/src/shared/database/index.ts b/src/shared/database/index.ts index a00ead8f..b346cc07 100644 --- a/src/shared/database/index.ts +++ b/src/shared/database/index.ts @@ -8,12 +8,10 @@ import { DexExPositionReserves, DexExPositionWithdrawals, DexExBlockSummary, - BatchSwapSummary, } from '@/shared/database/schema.ts'; import { AssetId } from '@penumbra-zone/protobuf/penumbra/core/asset/v1/asset_pb'; import { DurationWindow } from '@/shared/utils/duration.ts'; import { PositionId } from '@penumbra-zone/protobuf/penumbra/core/component/dex/v1/dex_pb'; -import { hexToUint8Array } from '@penumbra-zone/types/hex'; import { BlockSummaryPindexerResponse } from '../api/server/block/types'; const MAINNET_CHAIN_ID = 'penumbra-1'; diff --git a/src/shared/database/schema.ts b/src/shared/database/schema.ts index e0577400..179e059f 100644 --- a/src/shared/database/schema.ts +++ b/src/shared/database/schema.ts @@ -210,7 +210,7 @@ export interface DexExBlockSummary { rowid: number; height: number; time: Date; - batch_swaps: string; + batch_swaps: BatchSwapSummary[]; num_open_lps: number; num_closed_lps: number; num_withdrawn_lps: number; From 3d6fe0df0bd8a6997f871bef9fddad47dd9d24c9 Mon Sep 17 00:00:00 2001 From: "Jason M. Hasperhoven" Date: Tue, 25 Feb 2025 19:47:40 +0400 Subject: [PATCH 09/12] Merge branch main into inspect-block --- app/api/transactions/[txHash]/route.ts | 1 + src/pages/inspect/tx/api/transaction.ts | 16 ++++++++------ src/shared/api/server/transaction/index.ts | 25 ++++++++++++++++++++++ src/shared/api/server/transaction/types.ts | 6 ++++++ src/shared/database/index.ts | 10 +++++++++ src/shared/database/schema.ts | 9 ++++++++ 6 files changed, 61 insertions(+), 6 deletions(-) create mode 100644 app/api/transactions/[txHash]/route.ts create mode 100644 src/shared/api/server/transaction/index.ts create mode 100644 src/shared/api/server/transaction/types.ts diff --git a/app/api/transactions/[txHash]/route.ts b/app/api/transactions/[txHash]/route.ts new file mode 100644 index 00000000..6cc0471c --- /dev/null +++ b/app/api/transactions/[txHash]/route.ts @@ -0,0 +1 @@ +export { GET } from '@/shared/api/server/transaction'; diff --git a/src/pages/inspect/tx/api/transaction.ts b/src/pages/inspect/tx/api/transaction.ts index 7fb958f7..6d2dfca3 100644 --- a/src/pages/inspect/tx/api/transaction.ts +++ b/src/pages/inspect/tx/api/transaction.ts @@ -1,6 +1,6 @@ import { useQuery } from '@tanstack/react-query'; import { createClient } from '@connectrpc/connect'; -import { ViewService, TendermintProxyService } from '@penumbra-zone/protobuf'; +import { ViewService } from '@penumbra-zone/protobuf'; import { getGrpcTransport } from '@/shared/api/transport'; import { TransactionInfo } from '@penumbra-zone/protobuf/penumbra/view/v1/view_pb'; import { @@ -34,6 +34,7 @@ import { ActionDutchAuctionScheduleView, ActionDutchAuctionWithdrawView, } from '@penumbra-zone/protobuf/penumbra/core/component/auction/v1/auction_pb'; +import { TransactionApiResponse } from '@/shared/api/server/transaction/types'; export const useTransactionInfo = (txHash: string, connected: boolean) => { return useQuery({ @@ -53,15 +54,18 @@ export const useTransactionInfo = (txHash: string, connected: boolean) => { return viewServiceRes.txInfo; } - const tendermintClient = createClient(TendermintProxyService, grpc.transport); - const res = await tendermintClient.getTx({ hash }); + const res = await fetch(`/api/transactions/${txHash}`); + const jsonRes = (await res.json()) as TransactionApiResponse; + if ('error' in jsonRes) { + throw new Error(jsonRes.error); + } - const { tx, height } = res; + const { tx, height } = jsonRes; - const transaction = Transaction.fromBinary(tx); + const transaction = Transaction.fromBinary(hexToUint8Array(tx)); const txInfo = new TransactionInfo({ - height, + height: BigInt(height), id: new TransactionId({ inner: hash }), transaction, perspective: new TransactionPerspective({ diff --git a/src/shared/api/server/transaction/index.ts b/src/shared/api/server/transaction/index.ts new file mode 100644 index 00000000..3f9c93a7 --- /dev/null +++ b/src/shared/api/server/transaction/index.ts @@ -0,0 +1,25 @@ +import { NextRequest, NextResponse } from 'next/server'; +import { pindexer } from '@/shared/database'; +import { TransactionApiResponse } from './types'; +import { uint8ArrayToHex } from '@penumbra-zone/types/hex'; + +export async function GET( + _req: NextRequest, + { params }: { params: { txHash: string } }, +): Promise> { + const txHash = params.txHash; + if (!txHash) { + return NextResponse.json({ error: 'txHash is required' }, { status: 400 }); + } + + const response = await pindexer.getTransaction(txHash); + + if (!response) { + return NextResponse.json({ error: 'Transaction not found' }, { status: 404 }); + } + + return NextResponse.json({ + tx: uint8ArrayToHex(response.transaction), + height: response.height, + }); +} diff --git a/src/shared/api/server/transaction/types.ts b/src/shared/api/server/transaction/types.ts new file mode 100644 index 00000000..8cc48f46 --- /dev/null +++ b/src/shared/api/server/transaction/types.ts @@ -0,0 +1,6 @@ +export type TransactionApiResponse = + | { + tx: string; + height: number; + } + | { error: string }; diff --git a/src/shared/database/index.ts b/src/shared/database/index.ts index b346cc07..2fd5a257 100644 --- a/src/shared/database/index.ts +++ b/src/shared/database/index.ts @@ -8,11 +8,13 @@ import { DexExPositionReserves, DexExPositionWithdrawals, DexExBlockSummary, + DexExTransactions, } from '@/shared/database/schema.ts'; import { AssetId } from '@penumbra-zone/protobuf/penumbra/core/asset/v1/asset_pb'; import { DurationWindow } from '@/shared/utils/duration.ts'; import { PositionId } from '@penumbra-zone/protobuf/penumbra/core/component/dex/v1/dex_pb'; import { BlockSummaryPindexerResponse } from '../api/server/block/types'; +import { hexToUint8Array } from '@penumbra-zone/types/hex'; const MAINNET_CHAIN_ID = 'penumbra-1'; @@ -475,6 +477,14 @@ class Pindexer { return result; } + + async getTransaction(txHash: string): Promise | undefined> { + return this.db + .selectFrom('dex_ex_transactions') + .selectAll() + .where('transaction_id', '=', Buffer.from(hexToUint8Array(txHash))) + .executeTakeFirst(); + } } export const pindexer = new Pindexer(); diff --git a/src/shared/database/schema.ts b/src/shared/database/schema.ts index 179e059f..ee1313bd 100644 --- a/src/shared/database/schema.ts +++ b/src/shared/database/schema.ts @@ -219,6 +219,13 @@ export interface DexExBlockSummary { num_txs: number; } +export interface DexExTransactions { + transaction_id: Buffer; + transaction: Buffer; + height: number; + time: Timestamp; +} + export interface GovernanceDelegatorVotes { block_height: Int8; id: Generated; @@ -365,6 +372,7 @@ interface RawDB { dex_ex_position_withdrawals: DexExPositionWithdrawals; dex_ex_price_charts: DexExPriceCharts; dex_ex_block_summary: DexExBlockSummary; + dex_ex_transactions: DexExTransactions; governance_delegator_votes: GovernanceDelegatorVotes; governance_proposals: GovernanceProposals; governance_validator_votes: GovernanceValidatorVotes; @@ -395,4 +403,5 @@ export type DB = Pick< | 'dex_ex_batch_swap_traces' | 'dex_ex_metadata' | 'dex_ex_block_summary' + | 'dex_ex_transactions' >; From 73a77c49eec875719051c5b4a24af859c5e58d91 Mon Sep 17 00:00:00 2001 From: "Jason M. Hasperhoven" Date: Tue, 25 Feb 2025 20:19:46 +0400 Subject: [PATCH 10/12] Replace Table with TableCell --- src/pages/inspect/block/ui/block-summary.tsx | 88 ++++++++++---------- 1 file changed, 42 insertions(+), 46 deletions(-) diff --git a/src/pages/inspect/block/ui/block-summary.tsx b/src/pages/inspect/block/ui/block-summary.tsx index 6757692d..725eb80c 100644 --- a/src/pages/inspect/block/ui/block-summary.tsx +++ b/src/pages/inspect/block/ui/block-summary.tsx @@ -1,6 +1,6 @@ import { InfoCard } from '@/pages/explore/ui/info-card'; import { Text } from '@penumbra-zone/ui/Text'; -import { Table } from '@penumbra-zone/ui/Table'; +import { TableCell } from '@penumbra-zone/ui/TableCell'; import { BlockSummaryApiResponse } from '@/shared/api/server/block/types'; import { ValueViewComponent } from '@penumbra-zone/ui/ValueView'; import { pnum } from '@penumbra-zone/types/pnum'; @@ -50,51 +50,47 @@ export function BlockSummary({ blockSummary }: { blockSummary: BlockSummaryApiRe Swaps
- - - - From - To - Price - Number of Hops - - - - {blockSummary.batchSwaps.length ? ( - blockSummary.batchSwaps.map(swap => ( - - - - - - - - - - {swap.endPrice} {swap.endAsset.symbol} - - - - {swap.numSwaps} - - - )) - ) : ( - - -- - -- - -- - -- - - )} - -
+
+
+ From + To + Price + Number of Hops +
+ {blockSummary.batchSwaps.length ? ( + blockSummary.batchSwaps.map(swap => ( +
+ + + + + + + + + {swap.endPrice} {swap.endAsset.symbol} + + + + {swap.numSwaps} + +
+ )) + ) : ( +
+ -- + -- + -- + -- +
+ )} +
); From e8b45e0edd72d54dfe353477d8d0a5ea0877986c Mon Sep 17 00:00:00 2001 From: "Jason M. Hasperhoven" Date: Tue, 25 Feb 2025 20:45:01 +0400 Subject: [PATCH 11/12] Address PR comments --- src/pages/inspect/block/api/block.ts | 4 +- src/pages/inspect/block/ui/block-summary.tsx | 54 +++++++++++--------- src/shared/api/server/block/index.ts | 4 +- src/shared/api/server/block/types.ts | 21 ++------ src/shared/database/index.ts | 9 ++-- 5 files changed, 40 insertions(+), 52 deletions(-) diff --git a/src/pages/inspect/block/api/block.ts b/src/pages/inspect/block/api/block.ts index 55e7a5fa..f0709866 100644 --- a/src/pages/inspect/block/api/block.ts +++ b/src/pages/inspect/block/api/block.ts @@ -1,5 +1,6 @@ import { useQuery } from '@tanstack/react-query'; import { BlockSummaryApiResponse } from '@/shared/api/server/block/types'; +import { apiFetch } from '@/shared/utils/api-fetch'; export const useBlockSummary = (height: string) => { return useQuery({ @@ -9,8 +10,7 @@ export const useBlockSummary = (height: string) => { if (!height) { throw new Error('Invalid block height'); } - const response = await fetch(`/api/block/${height}`); - return response.json() as Promise; + return apiFetch(`/api/block/${height}`); }, }); }; diff --git a/src/pages/inspect/block/ui/block-summary.tsx b/src/pages/inspect/block/ui/block-summary.tsx index 725eb80c..945aea35 100644 --- a/src/pages/inspect/block/ui/block-summary.tsx +++ b/src/pages/inspect/block/ui/block-summary.tsx @@ -4,6 +4,7 @@ import { TableCell } from '@penumbra-zone/ui/TableCell'; import { BlockSummaryApiResponse } from '@/shared/api/server/block/types'; import { ValueViewComponent } from '@penumbra-zone/ui/ValueView'; import { pnum } from '@penumbra-zone/types/pnum'; +import { Metadata } from '@penumbra-zone/protobuf/penumbra/core/asset/v1/asset_pb'; export function BlockSummary({ blockSummary }: { blockSummary: BlockSummaryApiResponse }) { if ('error' in blockSummary) { @@ -58,30 +59,35 @@ export function BlockSummary({ blockSummary }: { blockSummary: BlockSummaryApiRe Number of Hops {blockSummary.batchSwaps.length ? ( - blockSummary.batchSwaps.map(swap => ( -
- - - - - - - - - {swap.endPrice} {swap.endAsset.symbol} - - - - {swap.numSwaps} - -
- )) + blockSummary.batchSwaps.map(swap => { + const startAsset = Metadata.fromJson(swap.startAsset); + const endAsset = Metadata.fromJson(swap.endAsset); + + return ( +
+ + + + + + + + + {swap.endPrice} {endAsset.symbol} + + + + {swap.numSwaps} + +
+ ); + }) ) : (
-- diff --git a/src/shared/api/server/block/index.ts b/src/shared/api/server/block/index.ts index d68de387..c38e7ce0 100644 --- a/src/shared/api/server/block/index.ts +++ b/src/shared/api/server/block/index.ts @@ -57,8 +57,8 @@ export const getBatchSwapDisplayData = const outputBigInt = joinLoHi(batchSwapSummary.output.lo, batchSwapSummary.output.hi); return { - startAsset: startMetadata, - endAsset: endMetadata, + startAsset: startMetadata.toJson(), + endAsset: endMetadata.toJson(), startInput: pnum(inputBigInt, startExponent).toString(), endOutput: pnum(outputBigInt, endExponent).toString(), endPrice: pnum(outputBigInt / inputBigInt, endExponent).toFormattedString(), diff --git a/src/shared/api/server/block/types.ts b/src/shared/api/server/block/types.ts index 7f6802b7..029ca62e 100644 --- a/src/shared/api/server/block/types.ts +++ b/src/shared/api/server/block/types.ts @@ -1,8 +1,8 @@ -import { Metadata } from '@penumbra-zone/protobuf/penumbra/core/asset/v1/asset_pb'; +import { JsonValue } from '@bufbuild/protobuf'; export interface BatchSwapSummaryDisplay { - startAsset: Metadata; - endAsset: Metadata; + startAsset: JsonValue; + endAsset: JsonValue; startInput: string; endOutput: string; endPrice: string; @@ -31,18 +31,3 @@ export type BlockSummaryApiResponse = numTxs: number; } | { error: string }; - -export type BlockSummaryPindexerResponse = - | { - rowid: number; - height: number; - time: Date; - batch_swaps: BatchSwapSummary[]; - num_open_lps: number; - num_closed_lps: number; - num_withdrawn_lps: number; - num_swaps: number; - num_swap_claims: number; - num_txs: number; - } - | undefined; diff --git a/src/shared/database/index.ts b/src/shared/database/index.ts index 2fd5a257..30cfa4aa 100644 --- a/src/shared/database/index.ts +++ b/src/shared/database/index.ts @@ -13,7 +13,6 @@ import { import { AssetId } from '@penumbra-zone/protobuf/penumbra/core/asset/v1/asset_pb'; import { DurationWindow } from '@/shared/utils/duration.ts'; import { PositionId } from '@penumbra-zone/protobuf/penumbra/core/component/dex/v1/dex_pb'; -import { BlockSummaryPindexerResponse } from '../api/server/block/types'; import { hexToUint8Array } from '@penumbra-zone/types/hex'; const MAINNET_CHAIN_ID = 'penumbra-1'; @@ -468,14 +467,12 @@ class Pindexer { })); } - async getBlockSummary(height: number): Promise { - const result = (await this.db + async getBlockSummary(height: number): Promise | undefined> { + return this.db .selectFrom('dex_ex_block_summary') .selectAll() .where('height', '=', height) - .executeTakeFirst()) as DexExBlockSummary | undefined; - - return result; + .executeTakeFirst(); } async getTransaction(txHash: string): Promise | undefined> { From f349f1428a1cca6448167f1c6960f8da3244ecee Mon Sep 17 00:00:00 2001 From: "Jason M. Hasperhoven" Date: Wed, 26 Feb 2025 17:43:16 +0400 Subject: [PATCH 12/12] Use serialization functions --- src/pages/inspect/block/ui/block-summary.tsx | 54 +++++++++----------- src/shared/api/server/block/index.ts | 40 +++++++-------- src/shared/api/server/block/types.ts | 19 ++----- 3 files changed, 49 insertions(+), 64 deletions(-) diff --git a/src/pages/inspect/block/ui/block-summary.tsx b/src/pages/inspect/block/ui/block-summary.tsx index 945aea35..725eb80c 100644 --- a/src/pages/inspect/block/ui/block-summary.tsx +++ b/src/pages/inspect/block/ui/block-summary.tsx @@ -4,7 +4,6 @@ import { TableCell } from '@penumbra-zone/ui/TableCell'; import { BlockSummaryApiResponse } from '@/shared/api/server/block/types'; import { ValueViewComponent } from '@penumbra-zone/ui/ValueView'; import { pnum } from '@penumbra-zone/types/pnum'; -import { Metadata } from '@penumbra-zone/protobuf/penumbra/core/asset/v1/asset_pb'; export function BlockSummary({ blockSummary }: { blockSummary: BlockSummaryApiResponse }) { if ('error' in blockSummary) { @@ -59,35 +58,30 @@ export function BlockSummary({ blockSummary }: { blockSummary: BlockSummaryApiRe Number of Hops
{blockSummary.batchSwaps.length ? ( - blockSummary.batchSwaps.map(swap => { - const startAsset = Metadata.fromJson(swap.startAsset); - const endAsset = Metadata.fromJson(swap.endAsset); - - return ( -
- - - - - - - - - {swap.endPrice} {endAsset.symbol} - - - - {swap.numSwaps} - -
- ); - }) + blockSummary.batchSwaps.map(swap => ( +
+ + + + + + + + + {swap.endPrice} {swap.endAsset.symbol} + + + + {swap.numSwaps} + +
+ )) ) : (
-- diff --git a/src/shared/api/server/block/index.ts b/src/shared/api/server/block/index.ts index c38e7ce0..30492330 100644 --- a/src/shared/api/server/block/index.ts +++ b/src/shared/api/server/block/index.ts @@ -1,16 +1,14 @@ import { NextRequest, NextResponse } from 'next/server'; import { pindexer } from '@/shared/database'; -import { - BlockSummaryApiResponse, - BatchSwapSummaryDisplay, -} from '@/shared/api/server/block/types.ts'; import { AssetId } from '@penumbra-zone/protobuf/penumbra/core/asset/v1/asset_pb'; import { ChainRegistryClient, Registry } from '@penumbra-labs/registry'; import { getDisplayDenomExponent } from '@penumbra-zone/getters/metadata'; import { pnum } from '@penumbra-zone/types/pnum'; -import { BatchSwapSummary } from '@/shared/database/schema'; import { hexToUint8Array, base64ToHex } from '@penumbra-zone/types/hex'; import { joinLoHi, LoHi } from '@penumbra-zone/types/lo-hi'; +import { serialize, Serialized } from '@/shared/utils/serializer'; +import { BatchSwapSummary as PindexerBatchSwapSummary } from '@/shared/database/schema'; +import { BatchSwapSummary, BlockSummaryApiResponse } from './types'; function isLoHi(value: unknown): value is LoHi { return typeof value === 'object' && value !== null && 'lo' in value; @@ -32,7 +30,7 @@ function isAsset(value: unknown): value is AssetWithInner { export const getBatchSwapDisplayData = (registry: Registry) => - (batchSwapSummary: BatchSwapSummary): BatchSwapSummaryDisplay => { + (batchSwapSummary: PindexerBatchSwapSummary): BatchSwapSummary => { if (!isLoHi(batchSwapSummary.input) || !isLoHi(batchSwapSummary.output)) { throw new Error('Invalid input or output: expected LoHi type'); } @@ -57,8 +55,8 @@ export const getBatchSwapDisplayData = const outputBigInt = joinLoHi(batchSwapSummary.output.lo, batchSwapSummary.output.hi); return { - startAsset: startMetadata.toJson(), - endAsset: endMetadata.toJson(), + startAsset: startMetadata, + endAsset: endMetadata, startInput: pnum(inputBigInt, startExponent).toString(), endOutput: pnum(outputBigInt, endExponent).toString(), endPrice: pnum(outputBigInt / inputBigInt, endExponent).toFormattedString(), @@ -69,7 +67,7 @@ export const getBatchSwapDisplayData = export async function GET( _req: NextRequest, { params }: { params: { height: string } }, -): Promise> { +): Promise>> { const chainId = process.env['PENUMBRA_CHAIN_ID']; if (!chainId) { return NextResponse.json({ error: 'PENUMBRA_CHAIN_ID is not set' }, { status: 500 }); @@ -89,15 +87,17 @@ export async function GET( return NextResponse.json({ error: 'Block summary not found' }, { status: 404 }); } - return NextResponse.json({ - height: blockSummary.height, - time: blockSummary.time, - batchSwaps: blockSummary.batch_swaps.map(getBatchSwapDisplayData(registry)), - numOpenLps: blockSummary.num_open_lps, - numClosedLps: blockSummary.num_closed_lps, - numWithdrawnLps: blockSummary.num_withdrawn_lps, - numSwaps: blockSummary.num_swaps, - numSwapClaims: blockSummary.num_swap_claims, - numTxs: blockSummary.num_txs, - }); + return NextResponse.json( + serialize({ + height: blockSummary.height, + time: blockSummary.time, + batchSwaps: blockSummary.batch_swaps.map(getBatchSwapDisplayData(registry)), + numOpenLps: blockSummary.num_open_lps, + numClosedLps: blockSummary.num_closed_lps, + numWithdrawnLps: blockSummary.num_withdrawn_lps, + numSwaps: blockSummary.num_swaps, + numSwapClaims: blockSummary.num_swap_claims, + numTxs: blockSummary.num_txs, + }), + ); } diff --git a/src/shared/api/server/block/types.ts b/src/shared/api/server/block/types.ts index 029ca62e..36d33e26 100644 --- a/src/shared/api/server/block/types.ts +++ b/src/shared/api/server/block/types.ts @@ -1,28 +1,19 @@ -import { JsonValue } from '@bufbuild/protobuf'; +import { Metadata } from '@penumbra-zone/protobuf/penumbra/core/asset/v1/asset_pb'; -export interface BatchSwapSummaryDisplay { - startAsset: JsonValue; - endAsset: JsonValue; +export interface BatchSwapSummary { + startAsset: Metadata; + endAsset: Metadata; startInput: string; endOutput: string; endPrice: string; numSwaps: number; } -export interface BatchSwapSummary { - asset_start: Buffer; - asset_end: Buffer; - input: string; - output: string; - num_swaps: number; - price_float: number; -} - export type BlockSummaryApiResponse = | { height: number; time: Date; - batchSwaps: BatchSwapSummaryDisplay[]; + batchSwaps: BatchSwapSummary[]; numOpenLps: number; numClosedLps: number; numWithdrawnLps: number;