Skip to content

Commit

Permalink
Merge pull request #49 from penumbra-zone/minimal-ibc-client-support
Browse files Browse the repository at this point in the history
Minimal IBC Clients Support
  • Loading branch information
ejmg authored Dec 21, 2023
2 parents ac60df9 + a7eafb3 commit 5c14759
Show file tree
Hide file tree
Showing 10 changed files with 268 additions and 0 deletions.
52 changes: 52 additions & 0 deletions src/app/api/ibc/clients/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
import db from "@/lib/db";
import { type NextRequest } from "next/server";

export async function GET(req: NextRequest) {
console.log("SUCCESS: GET /api/ibc/clients");
try {
console.log("Querying indexer for IBC clients.");
const clients = await db.events.findMany({
select: {
tx_results: {
select: {
tx_hash: true,
},
},
blocks: {
select: {
created_at: true,
height: true,
},
},
attributes: {
select: {
key: true,
value: true,
},
},
},
where: {
type: {
equals: "create_client",
},
},
});

console.log("Successfully queried for IBC Clients.", clients);

// NOTE: As of now, cannot completely decode the Protobuf data for an IBC client related transaction
// due to ibc.core.client.v1.MsgCreateClient not having a defined URL protobuf schema that can be resolved.
// What data that can be returned by decoding from TxResult and Transaction is not all that useful.
// const clientTx = clients.at(0)?.tx_results?.tx_result;
// if (clientTx) {
// const [tx, ibcClient] = ibcEventFromBytes(clientTx);
// console.log("Successfully extracted IBC data from txResult data.", tx, ibcClient);
// }

return new Response(JSON.stringify(clients));

} catch (error) {
console.error("GET request failed.", error);
return new Response("Could not query IBC Clients.", { status: 500 });
}
}
9 changes: 9 additions & 0 deletions src/app/ibc/channels/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
const Page = () => {
return (
<div>
<p className="font-bold">IBC Channels are not yet implemented...</p>
</div>
);
};

export default Page;
57 changes: 57 additions & 0 deletions src/app/ibc/clients/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
"use client";
import ClientsTable from "@/components/ibc/clients/ClientsTable";
import { useQuery } from "@tanstack/react-query";
import axios from "axios";

const Page = () => {
const { data , isFetched, isError } = useQuery({
queryFn: async () => {
console.log("Fetching: GET /api/ibc/clients");
const { data } = await axios.get("/api/ibc/clients");
console.log("Fetched result:", data);
return data;
// const result = SearchResultValidator.safeParse(data);
// if (result.success) {
// console.log(result.data);
// return result.data;
// } else {
// throw new Error(result.error.message);
// }
},
queryKey: ["IBCClients"],
retry: false,
meta: {
errorMessage: "Failed to query for IBC Clients. Please try again.",
},
});

if (isError) {
return (
<div className="py-5 flex justify-center">
<h1 className="text-4xl font-semibold">No results found.</h1>
</div>
);
}

return (
<div>
{isFetched ? (
<div>
<h1 className="text-3xl mx-auto py-5 font-semibold">IBC Clients</h1>
{// eslint-disable-next-line @typescript-eslint/strict-boolean-expressions
data ? (
<div className="flex flex-col justify-center w-full">
<ClientsTable data={data}/>
</div>
) : (
<p>No results</p>
)}
</div>
) : (
<p>loading...</p>
)}
</div>
);
};

export default Page;
9 changes: 9 additions & 0 deletions src/app/ibc/connections/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
const Page = () => {
return (
<div>
<p className="font-bold">IBC Connections are not yet implemented...</p>
</div>
);
};

export default Page;
28 changes: 28 additions & 0 deletions src/app/ibc/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import Link from "next/link";

const Page = () => {
return (
<div className="flex flex-col gap-5 items-center pt-5">
<h1 className="text-lg font-bold">Available IBC data to explore</h1>
<div className="flex w-full justify-around">
<p className="font-bold underline">
<Link href="/ibc/clients">
IBC Clients
</Link>
</p>
<p className="font-bold underline">
<Link href="/ibc/channels">
IBC Channels
</Link>
</p>
<p className="font-bold underline">
<Link href="/ibc/connections">
IBC Connections
</Link>
</p>
</div>
</div>
);
};

export default Page;
5 changes: 5 additions & 0 deletions src/app/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,11 @@ export default async function Home() {
Recent Blocks
</Link>
</p>
<p className="font-bold underline">
<Link href="/ibc/clients">
IBC Clients
</Link>
</p>
</div>
</div>
);
Expand Down
5 changes: 5 additions & 0 deletions src/components/Providers/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,11 @@ const Providers = ({ children } : { children: React.ReactNode }) => {
},
queryCache: new QueryCache({
onError: (error, query) => {
// TODO: Overall model is fine but need to change how meta is used.
// Idea: Add a `errorTitle` field instead that can be used in place of "Error" below. This gives a top level, succinct explanation.
// `description` becomes whatever value we store inside our error value. This is what needs to be refactored to make all queries play nicely.
// This allows each error to clearly signal its nature while also giving detail where appropriate. The issue of that detail is delegated to the useQuery callsite
// and any component/route that throws errors.
// There may be a more elegant way of expressing this but the general typing of onError's `error` and `query` arguments requires some amount of refinement for safety.
// https://tanstack.com/query/latest/docs/react/reference/QueryCache
let errorMessage = "";
Expand Down
30 changes: 30 additions & 0 deletions src/components/ibc/clients/ClientsTable.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import { columns } from "./columns";
import { DataTable } from "../../ui/data-table";
// import { type QueryKind } from "@/lib/validators/search";
import { type FC } from "react";

interface Props {
data: Array<{
tx_results: {
tx_hash: string | null,
},
blocks: {
created_at: string,
height: bigint,
},
attributes: Array<{
key: string,
value: string,
}>,
}>,
}

const ClientsTable : FC<Props> = ({ data }) => {
return (
<div>
<DataTable columns={columns} data={data}/>
</div>
);
};

export default ClientsTable;
61 changes: 61 additions & 0 deletions src/components/ibc/clients/columns.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
"use client";

import { type ColumnDef } from "@tanstack/react-table";
import Link from "next/link";

export interface ClientsColumns {
tx_results: {
tx_hash: string | null,
},
blocks: {
created_at: string,
height: bigint,
},
attributes: Array<{
key: string,
value: string,
}>,
};

// TODO formating, styling, etc
export const columns : Array<ColumnDef<ClientsColumns>> = [
{
accessorKey: "blocks.height",
header: () => <div className="font-semibold text-gray-800">Height</div>,
cell: ({ getValue }) => {
const ht: bigint = getValue() as bigint;
return <Link href={`/block/${ht}`} className="underline">{ht.toString()}</Link>;
},
},
{
accessorKey: "blocks.created_at",
header: () => <div className="font-semibold text-gray-800 text-center">Timestamp</div>,
cell: ({ getValue }) => {
const timestamp : string = getValue() as string;
return <p className="text-xs text-center">{timestamp}</p>;
},
},
{
accessorKey: "tx_results",
header: () => <div className="font-semibold text-gray-800 text-center">Hash</div>,
cell: ({ getValue }) => {
const tx = getValue() as { tx_hash : string | null };
if (tx.tx_hash !== null) {
return <p className="text-xs text-center">{tx.tx_hash}</p>;
}
},
},
{
accessorKey: "attributes",
header: () => <div className="font-semibold text-gray-800 text-center">info</div>,
cell: ({ getValue }) => {
const tx = getValue() as Array<{ key: string, value: string | null }>;
// TODO: besides styling itself, do better re: null value case for value
return (
<ul>
{tx.map(({key, value}, index) => (<li key={index}>{key}{value}</li>))}
</ul>
);
},
},
];
12 changes: 12 additions & 0 deletions src/lib/protobuf.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,19 @@
import { TxResult } from "@buf/cosmos_cosmos-sdk.bufbuild_es/tendermint/abci/types_pb";
// import { MsgCreateClient } from "@buf/cosmos_ibc.bufbuild_es/ibc/core/client/v1/tx_pb";
import { Transaction } from "@buf/penumbra-zone_penumbra.bufbuild_es/penumbra/core/transaction/v1alpha1/transaction_pb";
// import { IbcRelay } from "@buf/penumbra-zone_penumbra.bufbuild_es/penumbra/core/component/ibc/v1alpha1/ibc_pb";


export const transactionFromBytes = (txBytes : Buffer) => {
const txResult = TxResult.fromBinary(txBytes);
return Transaction.fromBinary(txResult.tx);
};

// NOTE: As of now, cannot completely decode the Protobuf data for an IBC client related transaction
// due to ibc.core.client.v1.MsgCreateClient not having a defined URL protobuf schema that can be resolved.
// What data that can be returned by decoding from TxResult and Transaction is not all that useful.
export const ibcEventFromBytes = (txBytes : Buffer) : [Transaction, TxResult] => {
const ibcEvent = TxResult.fromBinary(txBytes);
const tx = Transaction.fromBinary(ibcEvent.tx);
return [tx, ibcEvent];
};

0 comments on commit 5c14759

Please sign in to comment.