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

Decode execution code #57

Merged
merged 9 commits into from
Feb 29, 2024
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
5 changes: 5 additions & 0 deletions .env
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
NEXT_PUBLIC_CELO_EXPLORER_API_URL=https://explorer.celo.org/mainnet/graphiql
NEXT_PUBLIC_CELO_EXPLORER_API_URL_ALFAJORES=https://explorer.celo.org/alfajores/graphiql
NEXT_PUBLIC_CELO_EXPLORER_API_URL_BAKLAVA=https://explorer.celo.org/baklava/graphiql
NEXT_PUBLIC_WALLET_CONNECT_ID=3f6f578ca4a77fd09bc984689c9d095f
NEXT_PUBLIC_SUBGRAPH_URL=https://api.studio.thegraph.com/query/63311/mento/version/latest
2 changes: 1 addition & 1 deletion .eslintignore
Original file line number Diff line number Diff line change
@@ -1 +1 @@
app/graphql/generated
app/graphql/**/generated
132 changes: 124 additions & 8 deletions app/(routes)/proposals/[id]/_components/execution-code.component.tsx
Original file line number Diff line number Diff line change
@@ -1,27 +1,64 @@
import BlockExplorerLink from "@/app/components/_shared/block-explorer-link/block-explorer-link.component";
import type { ProposalCall } from "@/app/graphql";
import { useAccount } from "wagmi";
import {
GetContractsInfo,
type GetContractsInfoQuery,
type ProposalCall,
} from "@/app/graphql";
import { useCeloExplorerApi } from "@/app/hooks/useCeloExplorer";
import { useQuery } from "@apollo/experimental-nextjs-app-support/ssr";
import { useMemo } from "react";
import { decodeFunctionData } from "viem";

type Props = {
calls: ProposalCall[];
};

type ContractInfo = {
[address: string]: {
abi?: string;
name?: string;
};
};

export default function ExecutionCode({ calls }: Props) {
const { chain } = useAccount();
const blockExplorerUrl = chain?.blockExplorers?.default.url;
const { name: apiName } = useCeloExplorerApi();

const { data, error: apolloError } = useQuery(GetContractsInfo, {
variables: {
addresses: calls.map((call) => call.target.id),
},
context: { apiName },
skip: !calls.length,
});

if (apolloError) {
// TODO: Sentrify me
console.error(
"Failed to fetch contract metadata from Celo Explorer",
apolloError,
);
}

const contractMetadata = getContractMetadata(data);

const formattedCalls = useMemo(
() => calls.map((call) => formatCall(call, contractMetadata)),
[calls, contractMetadata],
);

return (
<div>
<h3 className="flex justify-center font-size-x6 line-height-x6 font-medium mb-x6">
Execution Code
</h3>
<div className="rounded-lg border-2 p-4">
{calls.map((call, index) => (
<div key={index} className="break-words">
{formattedCalls.map((call, index) => (
<div key={call.target + call.id} className="break-words">
{index > 0 && <hr className="my-4" />}
<h5 className="font-semibold mb-1">Target {index + 1}</h5>
<pre className="text-wrap">
<BlockExplorerLink type="address" item={call.target.id}>
{call.target.id}
<BlockExplorerLink type="address" item={call.target}>
{call.target}
</BlockExplorerLink>
</pre>
<br />
Expand All @@ -36,3 +73,82 @@ export default function ExecutionCode({ calls }: Props) {
</div>
);
}

/**
* The Celo Explorer API returns nothing (as in not even undefined) for
* non-existing addresses. So we can't rely on the input amount of
* addresses to equal the output amount of addresses as the output amount
* might be lower than the input amount. Hence this `reduce` to create a map
* of addresses to contract names and ABIs.
*/
function getContractMetadata(
data: GetContractsInfoQuery | undefined,
): ContractInfo | undefined {
return data?.addresses?.reduce((acc, address) => {
if (address && address.hash) {
acc[address.hash] = {
abi: address.smartContract?.abi ?? undefined,
name: address.smartContract?.name ?? undefined,
};
}

return acc;
}, {} as ContractInfo);
}

function formatCall(
call: ProposalCall,
contractMetadata: ContractInfo | undefined,
) {
const address = call.target.id;
const contractName = contractMetadata?.[address]?.name ?? undefined;
const decodedCalldata = decodeCalldata(call, contractMetadata);

return {
...call,
target: contractName ? `${contractName} (${address})` : address,
calldata: decodedCalldata || call.calldata,
};
}

function decodeCalldata(
call: ProposalCall,
contractMetadata: ContractInfo | undefined,
) {
const address = call.target.id;
const abiRaw = contractMetadata?.[address]?.abi;

if (typeof abiRaw === "string") {
let abi;

try {
abi = JSON.parse(abiRaw);
} catch (error) {
// TODO: Sentrify me
console.error(
"Failed to parse ABI. Falling back to returning raw calldata",
error,
);

return call.calldata;
}

let data;

if (call.calldata.startsWith("0x")) {
try {
data = decodeFunctionData({ abi, data: call.calldata });
} catch (error) {
// TODO: Sentrify me
console.error("Failed to decode calldata", error);
}
}

if (data?.functionName) {
// output example: `transfer(0x1234etc, 100)`
return `${data.functionName}(${data.args.length ? data.args.join(", ") : ""})`;
} else {
return call.calldata;
}
}
}
9 changes: 5 additions & 4 deletions app/(routes)/proposals/[id]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -37,11 +37,8 @@ const Page = ({ params }: { params: { id: string } }) => {

useProposalStates(data.proposals);

const [mobileVotingModalActive, setMobileVotingModalActive] = useState(false);
const [mobileParticipantsModalActive, setMobileParticipantsModelActive] =
useState(false);

const proposal = data.proposals[0];

const { title, description } = proposal.metadata;
const currentBlock = useBlockNumber();
const endBlock = useBlock({ blockNumber: BigInt(proposal.endBlock) });
Expand All @@ -68,6 +65,10 @@ const Page = ({ params }: { params: { id: string } }) => {
}
}, [currentBlock, endBlock, proposal.endBlock]);

const [mobileVotingModalActive, setMobileVotingModalActive] = useState(false);
const [mobileParticipantsModalActive, setMobileParticipantsModelActive] =
useState(false);

return (
<main className="flex flex-col">
{!proposal && <div>Proposal not found</div>}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
import classNames from "classnames";
import { useAccount } from "wagmi";
import styles from "./block-explorer-link.module.scss";

type Props = {
children: React.ReactNode;
Expand All @@ -11,7 +13,7 @@ function BlockExplorerLink({ children, type, item }: Props) {
const blockExplorerUrl = chain?.blockExplorers?.default.url;
return blockExplorerUrl ? (
<a
className="underline decoration-from-font"
className={classNames("underline", "decoration-from-font", styles.link)}
href={`${blockExplorerUrl}/${type}/${item}`}
target="_blank"
rel="noopener noreferrer"
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
.link {
text-underline-offset: 0.25em;
}
36 changes: 30 additions & 6 deletions app/graphql/apollo.client.ts
Original file line number Diff line number Diff line change
@@ -1,19 +1,43 @@
"use client";
// ^ this file needs the "use client" pragma

import { ApolloLink, HttpLink, gql } from "@apollo/client";
import { ApolloLink, createHttpLink } from "@apollo/client";
import {
NextSSRInMemoryCache,
NextSSRApolloClient,
NextSSRInMemoryCache,
SSRMultipartLink,
} from "@apollo/experimental-nextjs-app-support/ssr";
import { ProposalPolicy } from "./policies/Proposal";
import loadEnvVar from "../helpers/load-env-var";
import { ProposalPolicy } from "./subgraph/policies/Proposal";

const CELO_EXPLORER_API_URL = loadEnvVar(
process.env.NEXT_PUBLIC_CELO_EXPLORER_API_URL,
);
const CELO_EXPLORER_API_URL_ALFAJORES = loadEnvVar(
process.env.NEXT_PUBLIC_CELO_EXPLORER_API_URL_ALFAJORES,
);

const CELO_EXPLORER_API_URL_BAKLAVA = loadEnvVar(
process.env.NEXT_PUBLIC_CELO_EXPLORER_API_URL_BAKLAVA,
);
const SUBGRAPH_URL = loadEnvVar(process.env.NEXT_PUBLIC_SUBGRAPH_URL);

// have a function to create a client for you
export function newApolloClient() {
const httpLink = new HttpLink({
// this needs to be an absolute url, as relative urls cannot be used in SSR
uri: "https://api.studio.thegraph.com/query/63311/mento/version/latest",
const httpLink = createHttpLink({
// needs to be an absolute url, as relative urls cannot be used in SSR
uri: (operation) => {
const { apiName } = operation.getContext();

if (apiName === "celoExplorer") return CELO_EXPLORER_API_URL;
if (apiName === "celoExplorerAlfajores")
return CELO_EXPLORER_API_URL_ALFAJORES;
if (apiName === "celoExplorerBaklava")
return CELO_EXPLORER_API_URL_BAKLAVA;

return SUBGRAPH_URL;
},

// you can disable result caching here if you want to
// (this does not work if you are rendering your page with `export const dynamic = "force-static"`)
fetchOptions: { cache: "no-store" },
Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
/* eslint-disable */
import { ResultOf, DocumentTypeDecoration, TypedDocumentNode } from '@graphql-typed-document-node/core';
import { FragmentDefinitionNode } from 'graphql';
import { Incremental } from './graphql';
Expand Down
42 changes: 42 additions & 0 deletions app/graphql/celo-explorer/generated/gql.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
/* eslint-disable */
import * as types from './graphql';
import { TypedDocumentNode as DocumentNode } from '@graphql-typed-document-node/core';

/**
* Map of all GraphQL operations in the project.
*
* This map has several performance disadvantages:
* 1. It is not tree-shakeable, so it will include all operations in the project.
* 2. It is not minifiable, so the string of a GraphQL query will be multiple times inside the bundle.
* 3. It does not support dead code elimination, so it will add unused operations.
*
* Therefore it is highly recommended to use the babel or swc plugin for production.
*/
const documents = {
"query getContractsInfo($addresses: [AddressHash!]!) {\n addresses(hashes: $addresses) {\n hash\n smartContract {\n name\n abi\n }\n }\n}": types.GetContractsInfoDocument,
};

/**
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
*
*
* @example
* ```ts
* const query = graphql(`query GetUser($id: ID!) { user(id: $id) { name } }`);
* ```
*
* The query argument is unknown!
* Please regenerate the types.
*/
export function graphql(source: string): unknown;

/**
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
*/
export function graphql(source: "query getContractsInfo($addresses: [AddressHash!]!) {\n addresses(hashes: $addresses) {\n hash\n smartContract {\n name\n abi\n }\n }\n}"): (typeof documents)["query getContractsInfo($addresses: [AddressHash!]!) {\n addresses(hashes: $addresses) {\n hash\n smartContract {\n name\n abi\n }\n }\n}"];

export function graphql(source: string) {
return (documents as any)[source] ?? {};
}

export type DocumentType<TDocumentNode extends DocumentNode<any, any>> = TDocumentNode extends DocumentNode< infer TType, any> ? TType : never;
Loading
Loading