Skip to content
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
9 changes: 8 additions & 1 deletion web/.env.example
Original file line number Diff line number Diff line change
@@ -1,4 +1,11 @@
export NEXT_PUBLIC_APP_DEPLOYMENT=devnet
export NEXT_PUBLIC_REOWN_PROJECT_ID="abcabcabcabcabcabc"
export NEXT_PUBLIC_ALCHEMY_API_KEY="abcabcabcabcabcabc"
export NEXT_PUBLIC_COURT_SITE="https://dev--kleros-v2-testnet.netlify.app/#"
export NEXT_PUBLIC_COURT_SITE="https://dev--kleros-v2-testnet.netlify.app/#"

# tenderly simulations
export TENDERLY_ACCOUNT_NAME="test"
export TENDERLY_PROJECT_NAME="governor-test"
export TENDERLY_ACCESS_KEY="abcabcabcabcabcabc"
export ALLOWED_ORIGINS="http://localhost:3000"
export ETHERSCAN_API_KEY="abcabcabcabcabcabc"
2 changes: 1 addition & 1 deletion web/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
"prettier": "@kleros/governor-v2-prettier-config",
"dependencies": {
"@kleros/kleros-v2-contracts": "^0.7.0",
"@kleros/ui-components-library": "^3.4.5",
"@kleros/ui-components-library": "^3.6.0",
"@reown/appkit": "^1.7.7",
"@reown/appkit-adapter-wagmi": "^1.7.7",
"@tailwindcss/postcss": "^4.1.7",
Expand Down
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
"use client";
import { useState } from "react";
import { useMemo, useState } from "react";

import { BigNumberField, Button, Form, Modal, Radio, TextArea, TextField } from "@kleros/ui-components-library";
import clsx from "clsx";
import { type Abi, type AbiFunction, Address, encodeFunctionData, parseEther } from "viem";

import { ListTransaction, useLists } from "@/context/LIstsContext";
import { ListTransaction, useLists } from "@/context/NewListsContext";
import { useContractInfo } from "@/hooks/useContractInfo";

import { isUndefined } from "@/utils";
import { flattenToNested, formatFunctionCall } from "@/utils/txnBuilder/format";
Expand All @@ -28,7 +29,20 @@ interface IAddTxnModal {
const AddTxnModal: React.FC<IAddTxnModal> = ({ listId, isOpen, toggleIsOpen }) => {
const [inputType, setInputType] = useState(InputType.DataInput);
const [txnValue, setTxnValue] = useState("0");
const [contractAddress, setContractAddress] = useState<Address>();
const { addTxnToList } = useLists();
const { data: contractInfo, isLoading: isLoadingContractInfo } = useContractInfo(contractAddress);

const { addressInputVariant, addressInputMessage } = useMemo(() => {
if (isLoadingContractInfo)
return { addressInputVariant: "info" as const, addressInputMessage: "Fetching contract details" };
if (contractInfo?.abi)
return {
addressInputVariant: "success" as const,
addressInputMessage: contractInfo?.name ?? "Verified contract",
};
return { addressInputVariant: undefined };
}, [contractInfo, isLoadingContractInfo]);

const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault();
Expand Down Expand Up @@ -116,6 +130,9 @@ const AddTxnModal: React.FC<IAddTxnModal> = ({ listId, isOpen, toggleIsOpen }) =
validate={(val) => validateInputValue(val, { type: "address" })}
label="Contract Address"
isRequired
variant={addressInputVariant}
message={addressInputMessage}
onChange={(val) => setContractAddress(val as Address)}
/>
<BigNumberField
name="value"
Expand Down Expand Up @@ -153,7 +170,7 @@ const AddTxnModal: React.FC<IAddTxnModal> = ({ listId, isOpen, toggleIsOpen }) =
validate={(val) => validateInputValue(val, { type: "bytes" })}
/>
) : (
<JSONInput />
<JSONInput abi={contractInfo?.abi} />
)}
<Button text="Submit" type="submit" className="self-end mt-2" />
</Form>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
"use client";
import { Button } from "@kleros/ui-components-library";

import { useLists } from "@/context/LIstsContext";
import { useLists } from "@/context/NewListsContext";

import Paper from "@/assets/svgs/icons/paper.svg";

Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
import { useState } from "react";
import { useEffect, useState } from "react";

import { BigNumberField, DropdownSelect, TextArea, TextField } from "@kleros/ui-components-library";
import clsx from "clsx";
import type { Abi, AbiFunction } from "viem";

import { isUndefined } from "@/utils";
import { getDefaultPlaceholder, mapSolidityToInputType } from "@/utils/txnBuilder/format";
import { TupleInput } from "@/utils/txnBuilder/parsing";
import { validateInputValue } from "@/utils/txnBuilder/validation";
Expand Down Expand Up @@ -84,13 +85,17 @@ function renderInputField(input: TupleInput, path: string): JSX.Element {
);
}

const JSONInput: React.FC = () => {
const JSONInput: React.FC<{ abi?: Abi }> = ({ abi }) => {
const [abiInput, setAbiInput] = useState<string>("");
const [functions, setFunctions] = useState<AbiFunction[]>([]);
const [selectedFunction, setSelectedFunction] = useState<AbiFunction | null>(null);

const [error, setError] = useState<string>("");

useEffect(() => {
if (!isUndefined(abi)) handleAbiChange(JSON.stringify(abi));
}, [abi]);

const handleAbiChange = (val: string) => {
setAbiInput(val);
setError("");
Expand Down
42 changes: 38 additions & 4 deletions web/src/app/(main)/governor/[governorAddress]/MyLists/Lists.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,15 +5,17 @@ import { Button, Card, CustomAccordion, DraggableList } from "@kleros/ui-compone
import clsx from "clsx";
import { useToggle } from "react-use";

import { List, useLists } from "@/context/LIstsContext";
import { List, useLists } from "@/context/NewListsContext";

import ExternalLink from "@/components/ExternalLink";
import DisplayCard from "@/components/ListDisplayCard";
import Status from "@/components/Status";

import Calendar from "@/assets/svgs/icons/calendar.svg";
import Trash from "@/assets/svgs/icons/trash.svg";
import TenderlyIcon from "@/assets/svgs/logos/tenderly.svg";

import { formatDate } from "@/utils";
import { formatDate, isUndefined } from "@/utils";
import { formatETH } from "@/utils/format";

import AddTxnModal from "./AddTxnModal";
Expand All @@ -25,7 +27,9 @@ const AccordionBody: React.FC<IAccordionBody> = ({ list }) => {
const { id: listId, transactions } = list;
const [isOpen, toggleIsOpen] = useToggle(false);
const [selectedTxn, setSelectedTxn] = useState<List["transactions"][number] | undefined>(transactions?.[0]);
const { governorAddress, updateTransactions, deleteList } = useLists();
const { governorAddress, updateTransactions, deleteList, simulateList, isSimulating, simulations } = useLists();

const simulationShareLink = simulations.get(listId);

// select the latest txn, when new txn added
useEffect(() => {
Expand All @@ -36,6 +40,9 @@ const AccordionBody: React.FC<IAccordionBody> = ({ list }) => {
}
}, [transactions]);

const simulate = () => {
simulateList(listId);
};
return (
<div className="w-full pt-2 lg:px-6 flex flex-col justify-end items-end">
<Button
Expand Down Expand Up @@ -77,6 +84,33 @@ const AccordionBody: React.FC<IAccordionBody> = ({ list }) => {
<Button text="Add tx" variant="secondary" small onPress={toggleIsOpen} />
<SubmissionButton {...{ governorAddress, list }} />
</div>
{transactions.length > 0 ? (
<div
className={clsx(
"w-full px-6 pt-4 gap-4",
"flex flex-wrap items-center ",
"border-t border-t-klerosUIComponentsStroke",
isUndefined(simulationShareLink) ? "justify-end" : "justify-between"
)}
>
{simulationShareLink ? (
<ExternalLink
url={simulationShareLink}
text="Inspect on Tenderly"
className="text-klerosUIComponentsPrimaryBlue"
/>
) : null}
<Button
small
text="Simulate"
variant="secondary"
onPress={simulate}
isDisabled={isSimulating}
isLoading={isSimulating}
icon={<TenderlyIcon className="size-4 mr-2 ml" />}
/>
</div>
) : null}
</Card>
<div className="flex flex-col gap-2.5 md:gap-4">
<DisplayCard label="Contract Address" value={selectedTxn?.to ?? ""} />
Expand All @@ -85,7 +119,7 @@ const AccordionBody: React.FC<IAccordionBody> = ({ list }) => {
<DisplayCard label="Decoded Input" value={selectedTxn?.decodedInput ?? ""} />
</div>

<AddTxnModal {...{ isOpen, toggleIsOpen, listId }} />
{isOpen ? <AddTxnModal {...{ isOpen, toggleIsOpen, listId }} /> : null}
</div>
</div>
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import { Address } from "viem";

import { usePublicClient } from "wagmi";

import { List, useLists } from "@/context/LIstsContext";
import { List, useLists } from "@/context/NewListsContext";
import { useSimulateSubmitList, useWriteSubmitList } from "@/hooks/useGovernor";
import { useSubmissionFee } from "@/hooks/useSubmissionFee";

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

import { Address } from "viem";

import { ListsProvider } from "@/context/LIstsContext";
import { ListsProvider } from "@/context/NewListsContext";

import Header from "./Header";
import Lists from "./Lists";
Expand Down
123 changes: 123 additions & 0 deletions web/src/app/api/contract/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
import { NextRequest, NextResponse } from "next/server";
import { isAddress } from "viem";

import { isUndefined } from "@/utils";
import { checkRateLimit } from "@/utils/simulateRouteUtils";

export async function GET(request: NextRequest) {
const ip = request.ip || request.headers.get("x-forwarded-for") || "unknown";

const rateLimitCheck = checkRateLimit(ip);
if (!rateLimitCheck.allowed) {
return NextResponse.json(
{ error: "Rate limit exceeded. Try again later." },
{
status: 429,
headers: {
"Retry-After": Math.ceil((rateLimitCheck.resetTime || 0 - Date.now()) / 1000).toString(),
},
}
);
}

const searchParams = request.nextUrl.searchParams;
const networkId = searchParams.get("networkId");
const contractAddress = searchParams.get("contractAddress");

if (!networkId || !contractAddress) {
return NextResponse.json({ error: "Missing required parameters: networkId and contractAddress" }, { status: 400 });
}

if (!isAddress(contractAddress)) {
return NextResponse.json({ error: "Invalid contract address format" }, { status: 400 });
}

try {
if (isUndefined(process.env.TENDERLY_ACCESS_KEY)) {
throw new Error("Failed to fetch contract details: Environment variables not configured.");
}

// Fetch contract details from Tenderly API
const tenderlyApiUrl = `https://api.tenderly.co/api/v1/public-contracts/${networkId}/${contractAddress}`;

const response = await fetch(tenderlyApiUrl, {
method: "GET",
headers: {
"Content-Type": "application/json",
"X-Access-Key": process.env.TENDERLY_ACCESS_KEY,
},
});

if (!response.ok) {
// If Tenderly fails, try Etherscan as fallback
return await tryEtherscanFallback(networkId, contractAddress);
}

const tenderlyData = await response.json();

// If contract is unverified in Tenderly, try Etherscan
if (tenderlyData?.type === "unverified_contract" || isUndefined(tenderlyData?.data?.abi)) {
return await tryEtherscanFallback(networkId, contractAddress);
}

// Return formatted data for verified contract from Tenderly
return NextResponse.json({
address: contractAddress,
name: tenderlyData.contract_name,
abi: tenderlyData.data.abi,
});
} catch (error) {
console.error("Contract fetch error:", error instanceof Error ? error.message : "Unknown error");

return NextResponse.json(
{
error: error instanceof Error ? error.message : "Failed to fetch contract details",
},
{ status: 500 }
);
}
}

async function tryEtherscanFallback(networkId: string, contractAddress: string) {
try {
const etherscanApiKey = process.env.ETHERSCAN_API_KEY;

if (isUndefined(etherscanApiKey)) {
return NextResponse.json({ error: "Etherscan API key not configured" }, { status: 500 });
}

const baseUrl = "https://api.etherscan.io/v2/api";
// eslint-disable-next-line max-len
const url = `${baseUrl}?chainid=${networkId}&module=contract&action=getabi&address=${contractAddress}&apikey=${etherscanApiKey}`;
const response = await fetch(url);

if (!response.ok) {
return NextResponse.json(
{ error: `Failed to fetch from Etherscan: ${response.status}` },
{ status: response.status }
);
}

const arbiscanData = await response.json();

if (arbiscanData.status !== "1" || !arbiscanData.result) {
return NextResponse.json({ error: "Contract not verified on Etherscan" }, { status: 404 });
}

const abi = JSON.parse(arbiscanData.result);

return NextResponse.json({
address: contractAddress,
name: null, // Etherscan doesn't provide contract name in this API
abi,
});
} catch (error) {
console.error("Etherscan fallback error:", error instanceof Error ? error.message : "Unknown error");
return NextResponse.json(
{
error: error instanceof Error ? error.message : "Failed to fetch from Etherscan",
},
{ status: 500 }
);
}
}
Loading