Skip to content

Commit

Permalink
Merge pull request #5 from penumbra-zone/tx_result-json
Browse files Browse the repository at this point in the history
added encoding/decoding for tx_result data pulled from indexer. ht/tx views now render full JSON of the Transaction as defined in the Penumbra protobuf schema.
  • Loading branch information
ejmg authored Nov 28, 2023
2 parents 6bed3da + 018a089 commit 78b3f13
Show file tree
Hide file tree
Showing 10 changed files with 1,311 additions and 60 deletions.
1,208 changes: 1,208 additions & 0 deletions package-lock.json

Large diffs are not rendered by default.

8 changes: 8 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,14 @@
"lint": "next lint"
},
"dependencies": {
"@buf/cosmos_cosmos-sdk.bufbuild_es": "^1.4.2-20231127085224-6462d33db553.1",
"@buf/cosmos_cosmos-sdk.connectrpc_es": "^1.1.3-20231127085224-6462d33db553.1",
"@buf/penumbra-zone_penumbra.bufbuild_es": "^1.4.2-20231120132728-bc443669626d.1",
"@buf/penumbra-zone_penumbra.connectrpc_es": "^1.1.3-20231120132728-bc443669626d.1",
"@buf/penumbra-zone_penumbra.connectrpc_query-es": "^0.6.0-20231120132728-bc443669626d.2",
"@bufbuild/protobuf": "^1.4.2",
"@connectrpc/connect": "^1.1.3",
"@microlink/react-json-view": "^1.23.0",
"@prisma/client": "^5.4.1",
"@radix-ui/react-dialog": "^1.0.5",
"@radix-ui/react-slot": "^1.0.2",
Expand Down
22 changes: 18 additions & 4 deletions src/app/api/ht/route.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import db from "@/lib/db";
import { transactionFromBytes } from "@/lib/protobuf";
import { BlockHeightValidator } from "@/lib/validators/search";
import { z } from "zod";

Expand All @@ -14,7 +15,7 @@ export async function GET(req: Request) {
// NOTE: This endpoint doesn't return the plain data of a single block. It finds the block by height and, if they exist, attaches any associated events and transaction results.
// Duplicate height event attributes are also filtered out.
console.log(`querying database for block with height ${ht}.`);
const block = await db.blocks.findFirstOrThrow({
const query = await db.blocks.findFirstOrThrow({
where: {
height: ht,
},
Expand Down Expand Up @@ -78,10 +79,23 @@ export async function GET(req: Request) {
},
});

console.log("Successfully queried block:");
console.log(block);
console.log("Successfully queried block:", query);

return new Response(JSON.stringify(block));
const tx = query.tx_results.at(0);
const {tx_results : _, ...block} = query;
// eslint-disable-next-line @typescript-eslint/naming-convention
let tx_hash = "";
if (tx !== undefined) {
const penumbraTx = transactionFromBytes(tx.tx_result);
console.log("Successfully decoded Transaction from blockEvent.tx_results:", penumbraTx);

// eslint-disable-next-line @typescript-eslint/naming-convention
tx_hash = tx.tx_hash;
return new Response(JSON.stringify([{tx_hash, ...block}, penumbraTx.toJsonString()]));
}

console.log("No Transaction associated with block.");
return new Response(JSON.stringify([{tx_hash, ...block}, null]));
} catch (error) {
console.log(error);
if (error instanceof z.ZodError) {
Expand Down
14 changes: 10 additions & 4 deletions src/app/api/tx/route.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import db from "@/lib/db";
import { transactionFromBytes } from "@/lib/protobuf";
import { HashResultValidator } from "@/lib/validators/search";
import { z } from "zod";

Expand All @@ -12,7 +13,7 @@ export async function GET(req: Request) {

const hash = HashResultValidator.parse(queryParam);
console.log(`Querying db for transaction event with hash ${hash}`);
const tx = await db.tx_results.findFirstOrThrow({
const query = await db.tx_results.findFirstOrThrow({
select: {
tx_hash: true,
tx_result: true,
Expand Down Expand Up @@ -45,9 +46,14 @@ export async function GET(req: Request) {
},
});

console.log("Successfully queried transaction event:");
console.log(tx);
return new Response(JSON.stringify(tx));
console.log("Successfully queried transaction event:", query);

const penumbraTx = transactionFromBytes(query.tx_result);
console.log("Successfully decoded Transaction from tx_result:", penumbraTx);

// eslint-disable-next-line @typescript-eslint/naming-convention
const { tx_result, ...tx} = query;
return new Response(JSON.stringify([tx, penumbraTx.toJsonString()]));
} catch (error) {
console.log(error);
if (error instanceof z.ZodError) {
Expand Down
6 changes: 3 additions & 3 deletions src/app/block/[ht]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,9 +19,9 @@ const Page : FC<PageProps> = ({ params }) => {
queryFn: async () => {
console.log(`Fetching: GET /api/ht?q=${ht}`);
const { data } = await axios.get(`/api/ht?q=${ht}`);
console.log("Fetching result:");
console.log(data);
console.log("Fetching result:", data);
const result = BlockResult.safeParse(data);
console.log(result);
if (result.success) {
return result.data;
} else {
Expand Down Expand Up @@ -51,7 +51,7 @@ const Page : FC<PageProps> = ({ params }) => {
{blockData ? (
<div className="flex flex-col justify-center w-full">
<h1 className="text-3xl mx-auto py-5 font-semibold">Block Summary</h1>
<BlockEvent blockEvent={blockData}/>
<BlockEvent blockPayload={blockData}/>
</div>
) : (
<div>
Expand Down
5 changes: 2 additions & 3 deletions src/app/tx/[hash]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,7 @@ const Page : FC<PageProps> = ({ params }) => {
queryFn: async () => {
console.log(`Fetching: GET /api/tx?q=${hash}`);
const { data } = await axios.get(`/api/tx?q=${hash}`);
console.log("Fetched result:");
console.log(data);
console.log("Fetched result:", data);
const result = TransactionResult.safeParse(data);
if (result.success) {
return result.data;
Expand Down Expand Up @@ -53,7 +52,7 @@ const Page : FC<PageProps> = ({ params }) => {
{txData ? (
<div className="flex flex-col justify-center w-full">
<h1 className="text-3xl mx-auto py-5 font-semibold">Transaction Event Summary</h1>
<TransactionEvent txEvent={txData} />
<TransactionEvent txPayload={txData} />
</div>
) : (
<p>No results</p>
Expand Down
16 changes: 9 additions & 7 deletions src/components/BlockEvent/index.tsx
Original file line number Diff line number Diff line change
@@ -1,18 +1,18 @@
import { type BlockResultPayload } from "@/lib/validators/search";
import ReactJson from "@microlink/react-json-view";
import { type FC } from "react";

interface BlockEventProps {
blockEvent: BlockResultPayload
blockPayload: BlockResultPayload
}

// TODO: Similar to TransactionEvent, it looks increasingly likely that tanstack/table will actually work here so pulling out different DataTable representations will need to happen.
// TODO: This isn't meaningfully different from TransactionEvent. In fact, I almost re-wrapped that component here rather than
// re-write the rendering for BlockEvent. What I'm assuming is that there's meaningful info (and lack-of) to tease out from Blocks
// and providing for that will eventually happen here; otherwise, just re-using the same component will make sense instead of mostly
// duplicate ui code that shows the exact same information.
const BlockEvent : FC<BlockEventProps> = ({ blockEvent }) => {

const tx = blockEvent.tx_results.at(0);
const BlockEvent : FC<BlockEventProps> = ({ blockPayload }) => {
const [blockEvent, penumbraTx] = blockPayload;

return (
<div className="bg-white rounded-sm">
Expand All @@ -27,19 +27,21 @@ const BlockEvent : FC<BlockEventProps> = ({ blockEvent }) => {
</div>
<div className="flex flex-col justify-start w-full">
<p className="w-full">Transaction Event</p>
{tx !== undefined ? (
{penumbraTx ? (
<div className="flex w-full flex-wrap gap-y-5 pl-5 pt-5">
<div className="flex justify-start w-full">
<p className="w-1/6">Hash</p>
<pre>{tx.tx_hash}</pre>
<pre>{blockEvent.tx_hash}</pre>
</div>
<div className="flex w-full">
<p className="w-1/6 shrink-0">Transaction Result</p>
<details>
<summary className="list-none underline font-semibold">
click to expand
</summary>
<pre className="break-all whitespace-pre-wrap text-xs p-1 bg-slate-100">{tx.tx_result.data}</pre>
<pre className="break-all whitespace-pre-wrap text-xs p-1 bg-slate-100">
<ReactJson src={penumbraTx.toJson() as object} />
</pre>
</details>
</div>
<p>Event Attributes</p>
Expand Down
10 changes: 7 additions & 3 deletions src/components/TransactionEvent/index.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
import { type TransactionResultPayload } from "@/lib/validators/search";
import ReactJson from "@microlink/react-json-view";
import { type FC } from "react";

interface TransactionEventProps {
txEvent: TransactionResultPayload
txPayload: TransactionResultPayload
}

const TransactionEvent : FC<TransactionEventProps> = ({ txEvent }) => {
const TransactionEvent : FC<TransactionEventProps> = ({ txPayload }) => {
const [txEvent, penumbraTx] = txPayload;
return (
<div className="bg-white rounded-sm">
<div className="flex flex-wrap justify-between p-5 gap-y-10 w-full">
Expand All @@ -24,7 +26,9 @@ const TransactionEvent : FC<TransactionEventProps> = ({ txEvent }) => {
<summary className="list-none underline font-semibold">
click to expand
</summary>
<pre className="break-all whitespace-pre-wrap text-xs p-1 bg-slate-100">{txEvent.tx_result.data}</pre>
<pre className="break-all whitespace-pre-wrap text-xs p-1 bg-slate-100">
<ReactJson src={penumbraTx.toJson() as object}/>
</pre>
</details>
</div>
</div>
Expand Down
7 changes: 7 additions & 0 deletions src/lib/protobuf.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import { TxResult } from "@buf/cosmos_cosmos-sdk.bufbuild_es/tendermint/abci/types_pb";
import { Transaction } from "@buf/penumbra-zone_penumbra.bufbuild_es/penumbra/core/transaction/v1alpha1/transaction_pb";

export const transactionFromBytes = (txBytes : Buffer) => {
const txResult = TxResult.fromBinary(txBytes);
return Transaction.fromBinary(txResult.tx);
};
75 changes: 39 additions & 36 deletions src/lib/validators/search.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { Transaction } from "@buf/penumbra-zone_penumbra.bufbuild_es/penumbra/core/transaction/v1alpha1/transaction_pb";
import { z } from "zod";

// This validator is to check whether a sha256 hash conforms to what is expected by the `tx_hash` column
// of the `tx_result` table defined in cometbft's psql indexer schema.
export const HashResultValidator = z.union([
Expand All @@ -24,47 +24,50 @@ export type HashResultQuery = z.infer<typeof HashResultValidator>;
export type BlockHeightQuery = z.infer<typeof BlockHeightValidator>;

// zod schema equivalent to the /parsed/ JSON data returned by prisma in GET /api/tx?q=<hash>
export const TransactionResult = z.object({
tx_hash: z.string(),
// JSON.stringify transforms a Node Buffer into an object of { type: string, data: number[] }.
tx_result: z.object({
type: z.string(),
data: z.array(z.number()),
}),
created_at: z.string().datetime(),
events: z.array(z.object({
type: z.string(),
attributes: z.array(z.object({
value: z.string().nullable(),
key: z.string(),
export const TransactionResult = z.tuple([
z.object({
tx_hash: z.string(),
created_at: z.string().datetime(),
events: z.array(z.object({
type: z.string(),
attributes: z.array(z.object({
value: z.string().nullable(),
key: z.string(),
})),
})),
})),
blocks: z.object({
height: z.coerce.bigint(),
chain_id: z.string(),
blocks: z.object({
height: z.coerce.bigint(),
chain_id: z.string(),
}),
}),
});
// NOTE: Not sure how good this perf wise relative to JsonValue equivalent, but I would need to type out the entire object structure.
z.string().transform((jsonString) => {
const parsed = Transaction.fromJsonString(jsonString);
return parsed;
}),
]);

// zod schema equivalent to the /parsed/ JSON data returned by prisma in GET /api/ht?q=<height>
export const BlockResult = z.object({
chain_id: z.string(),
created_at: z.string().datetime(),
height: z.coerce.bigint(),
events: z.array(z.object({
type: z.string(),
attributes: z.array(z.object({
value: z.string().nullable(),
key: z.string(),
// NOTE: This definition is meaningfully different from TransactionResult in that the Transaction value may not exist at all.
export const BlockResult = z.tuple([
z.object({
chain_id: z.string(),
created_at: z.string().datetime(),
height: z.coerce.bigint(),
events: z.array(z.object({
type: z.string(),
attributes: z.array(z.object({
value: z.string().nullable(),
key: z.string(),
})),
})),
})),
tx_results: z.array(z.object({
tx_hash: z.string(),
tx_result: z.object({
type: z.string(),
data: z.array(z.number()),
}),
})),
});
}),
z.string().transform((jsonString) => {
const parsed = Transaction.fromJsonString(jsonString);
return parsed;
}).nullable(),
]);

export type TransactionResultPayload = z.infer<typeof TransactionResult>;
export type BlockResultPayload = z.infer<typeof BlockResult>;

0 comments on commit 78b3f13

Please sign in to comment.