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

Save ABI to localstorage on chain id 31337 #172

Open
wants to merge 17 commits into
base: main
Choose a base branch
from
Open
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
116 changes: 115 additions & 1 deletion packages/nextjs/components/MiniHeader.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,51 @@
import { useEffect, useState } from "react";
import Image from "next/image";
import Link from "next/link";
import { Bars3Icon } from "@heroicons/react/24/outline";
import { Bars3Icon, Cog6ToothIcon, TrashIcon, XMarkIcon } from "@heroicons/react/24/outline";
import { RainbowKitCustomConnectButton } from "~~/components/scaffold-eth";
import { useGlobalState } from "~~/services/store/store";
import { getAbiFromLocalStorage, removeAbiFromLocalStorage, saveAbiToLocalStorage } from "~~/utils/abi";
import { parseAndCorrectJSON } from "~~/utils/abi";
import { notification } from "~~/utils/scaffold-eth";

export const MiniHeader = () => {
const [showSuccess, setShowSuccess] = useState(false);
const [editedAbi, setEditedAbi] = useState("");
const { contractAddress, setContractAbi } = useGlobalState(state => ({
contractAddress: state.abiContractAddress,
setContractAbi: state.setContractAbi,
}));

const savedAbi = getAbiFromLocalStorage(contractAddress);
const formattedAbi = savedAbi ? JSON.stringify(savedAbi, null, 2) : "";

useEffect(() => {
setEditedAbi(formattedAbi);
}, [formattedAbi, contractAddress]);

const handleRemoveSavedAbi = () => {
removeAbiFromLocalStorage(contractAddress);
setContractAbi([]);
setShowSuccess(true);
};

const handleSaveEdit = () => {
try {
const parsedAbi = parseAndCorrectJSON(editedAbi);
if (parsedAbi) {
saveAbiToLocalStorage(contractAddress, parsedAbi);
setContractAbi(parsedAbi);
notification.success("ABI updated successfully!");
}
} catch (error) {
notification.error("Invalid ABI format. Please ensure it is valid JSON.");
}
};

const handleResetAbi = () => {
setEditedAbi(formattedAbi);
};

return (
<div className="sticky lg:static top-0 navbar bg-base-200 border-b border-secondary min-h-0 flex-shrink-0 justify-between z-20 px-0 sm:px-2">
<div className="navbar-start w-auto lg:w-1/2">
Expand All @@ -20,6 +62,78 @@ export const MiniHeader = () => {
</Link>
</div>
<div className="navbar-end flex-grow mr-4">
<button
className="mr-4 hover:transition-all hover:scale-110"
onClick={() => {
const modal = document.getElementById("configuration-modal") as HTMLDialogElement;
modal?.showModal();
}}
>
<Cog6ToothIcon className="h-6 w-6" />
</button>
<dialog id="configuration-modal" className="modal">
<div className="modal-box max-w-2xl bg-base-200 p-0">
<div className="flex justify-between items-center p-4 border-b border-base-300">
<h3 className="font-bold text-xl">Configuration</h3>
<form method="dialog">
<button className="mr-1 hover:transition-all hover:scale-110 hover:text-error">
<XMarkIcon className="h-6 w-6" />
</button>
</form>
</div>

<div className="p-6 space-y-6">
<div className="space-y-4">
<h2 className="text-lg font-semibold">Local Storage</h2>
<div className="space-y-3">
<p>
🥷abi.ninja automatically saves the contract ABI to your browser&apos;s local storage when you first
interact with a contract on local networks (like Anvil or Hardhat). This speeds up future
interactions with the same contract. You can modify or remove the saved ABI below.
</p>
</div>
</div>

{savedAbi && (
<div className="space-y-4">
<h3 className="text-md font-semibold">Saved ABI for {contractAddress}</h3>
<div className="space-y-4">
<textarea
className="textarea textarea-bordered bg-base-300 w-full h-96 font-mono text-sm"
value={editedAbi}
onChange={e => setEditedAbi(e.target.value)}
spellCheck="false"
/>
<div className="flex justify-between items-center">
<button className="btn btn-sm btn-ghost gap-2" onClick={handleResetAbi}>
Reset Changes
</button>
<div className="flex gap-2">
<button
className="btn btn-sm btn-error gap-2"
onClick={handleRemoveSavedAbi}
title="Remove saved ABI"
>
<TrashIcon className="h-4 w-4" />
Remove ABI
</button>
<button className="btn btn-sm btn-primary" onClick={handleSaveEdit}>
Save Changes
</button>
</div>
</div>
</div>
</div>
)}

{showSuccess && (
<div className="alert alert-success">
<span>ABI removed successfully! Please reload the page to see the changes.</span>
</div>
)}
</div>
</div>
</dialog>
<RainbowKitCustomConnectButton />
</div>
</div>
Expand Down
54 changes: 40 additions & 14 deletions packages/nextjs/pages/[contractAddress]/[network].tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import { SwitchTheme } from "~~/components/SwitchTheme";
import { ContractUI } from "~~/components/scaffold-eth";
import useFetchContractAbi from "~~/hooks/useFetchContractAbi";
import { useGlobalState } from "~~/services/store/store";
import { getNetworkName, parseAndCorrectJSON } from "~~/utils/abi";
import { getAbiFromLocalStorage, getNetworkName, parseAndCorrectJSON, saveAbiToLocalStorage } from "~~/utils/abi";
import { notification } from "~~/utils/scaffold-eth";

interface ParsedQueryContractDetailsPage extends ParsedUrlQuery {
Expand Down Expand Up @@ -51,17 +51,27 @@ const ContractDetailPage = ({ addressFromUrl, chainIdFromUrl }: ServerSideProps)
const [localContractAbi, setLocalContractAbi] = useState<string>("");
const [isUseLocalAbi, setIsUseLocalAbi] = useState(false);
const [localContractData, setLocalContractData] = useState<ContractData | null>(null);
const [isLoadingLocalStorage, setIsLoadingLocalStorage] = useState(true);

const { chainId, setImplementationAddress, contractAbi, chains, addChain, setTargetNetwork } = useGlobalState(
state => ({
chains: state.chains,
addChain: state.addChain,
chainId: state.targetNetwork.id,
setTargetNetwork: state.setTargetNetwork,
setImplementationAddress: state.setImplementationAddress,
contractAbi: state.contractAbi,
}),
);
const {
chainId,
setAbiContractAddress,
setImplementationAddress,
contractAbi,
chains,
addChain,
setTargetNetwork,
setContractAbi,
} = useGlobalState(state => ({
chains: state.chains,
addChain: state.addChain,
chainId: state.targetNetwork.id,
setAbiContractAddress: state.setAbiContractAddress,
setTargetNetwork: state.setTargetNetwork,
setImplementationAddress: state.setImplementationAddress,
contractAbi: state.contractAbi,
setContractAbi: state.setContractAbi,
}));

const {
contractData: fetchedContractData,
Expand All @@ -74,14 +84,28 @@ const ContractDetailPage = ({ addressFromUrl, chainIdFromUrl }: ServerSideProps)
disabled: contractAbi.length > 0,
});

useEffect(() => {
const savedAbi = getAbiFromLocalStorage(contractAddress);
if (savedAbi) {
setLocalContractData({ abi: savedAbi, address: contractAddress });
setAbiContractAddress(contractAddress);
setIsUseLocalAbi(true);
}
setIsLoadingLocalStorage(false);

if (!isLoadingLocalStorage && savedAbi) {
notification.success("Loaded ABI from local storage. Click the gear icon to manage ABIs.");
}
}, [contractAddress, isLoadingLocalStorage, setAbiContractAddress]);

const effectiveContractData =
contractAbi.length > 0
? { abi: contractAbi, address: contractAddress }
: isUseLocalAbi && localContractData
? localContractData
: fetchedContractData;

const error = isUseLocalAbi ? null : fetchError;
const error = isUseLocalAbi || isLoadingLocalStorage ? null : fetchError;

useEffect(() => {
if (network) {
Expand All @@ -101,8 +125,10 @@ const ContractDetailPage = ({ addressFromUrl, chainIdFromUrl }: ServerSideProps)
const parsedAbi = parseAndCorrectJSON(localContractAbi);
if (parsedAbi) {
setIsUseLocalAbi(true);
saveAbiToLocalStorage(contractAddress, parsedAbi);
setLocalContractData({ abi: parsedAbi, address: contractAddress });
notification.success("ABI successfully loaded.");
setAbiContractAddress(contractAddress);
setContractAbi(parsedAbi);
} else {
throw new Error("Parsed ABI is null or undefined");
}
Expand Down Expand Up @@ -132,7 +158,7 @@ const ContractDetailPage = ({ addressFromUrl, chainIdFromUrl }: ServerSideProps)
<div className="bg-base-100 h-screen flex flex-col">
<MiniHeader />
<div className="flex flex-col gap-y-6 lg:gap-y-8 flex-grow h-full overflow-hidden">
{isLoading && !isUseLocalAbi ? (
{isLoading || isLoadingLocalStorage ? (
<div className="flex justify-center h-full mt-14">
<span className="loading loading-spinner text-primary h-14 w-14"></span>
</div>
Expand Down
18 changes: 16 additions & 2 deletions packages/nextjs/pages/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ import { AddressInput } from "~~/components/scaffold-eth";
import useFetchContractAbi from "~~/hooks/useFetchContractAbi";
import { useHeimdall } from "~~/hooks/useHeimdall";
import { useGlobalState } from "~~/services/store/store";
import { parseAndCorrectJSON } from "~~/utils/abi";
import { getAbiFromLocalStorage, parseAndCorrectJSON, saveAbiToLocalStorage } from "~~/utils/abi";
import { notification } from "~~/utils/scaffold-eth";

enum TabName {
Expand Down Expand Up @@ -88,6 +88,13 @@ const Home: NextPage = () => {
}

if (network === "31337" && isAddress(verifiedContractAddress)) {
const savedAbi = getAbiFromLocalStorage(verifiedContractAddress);
if (savedAbi) {
setContractAbi(savedAbi);
setAbiContractAddress(verifiedContractAddress);
router.push(`/${verifiedContractAddress}/${network}`);
return;
}
setActiveTab(TabName.addressAbi);
setLocalAbiContractAddress(verifiedContractAddress);
return;
Expand All @@ -105,6 +112,8 @@ const Home: NextPage = () => {
handleFetchError,
setContractAbi,
setImplementationAddress,
router,
setAbiContractAddress,
]);

useEffect(() => {
Expand All @@ -128,8 +137,13 @@ const Home: NextPage = () => {
try {
const parsedAbi = parseAndCorrectJSON(localContractAbi);
setContractAbi(parsedAbi);
if (network === "31337") {
notification.success("Saving ABI to local storage...");
saveAbiToLocalStorage(localAbiContractAddress, parsedAbi);
} else {
notification.success("ABI successfully loaded.");
}
router.push(`/${localAbiContractAddress}/${network}`);
notification.success("ABI successfully loaded.");
} catch (error) {
notification.error("Invalid ABI format. Please ensure it is a valid JSON.");
}
Expand Down
62 changes: 61 additions & 1 deletion packages/nextjs/utils/abi.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,65 @@
import { isZeroAddress } from "./scaffold-eth/common";
import { Address, Chain } from "viem";
import { Abi, Address, Chain } from "viem";

const ABI_STORAGE_KEY = "abi_ninja_storage";

type AbiStorage = {
version: string;
abis: Record<string, Abi>;
};

const getAbiStorage = (): AbiStorage => {
if (typeof window === "undefined") return { version: "1.0", abis: {} };
try {
const storage = localStorage.getItem(ABI_STORAGE_KEY);
return storage ? JSON.parse(storage) : { version: "1.0", abis: {} };
} catch (error) {
console.error("Failed to get ABI storage:", error);
return { version: "1.0", abis: {} };
}
};

const saveAbiStorage = (storage: AbiStorage) => {
if (typeof window === "undefined") return;
try {
localStorage.setItem(ABI_STORAGE_KEY, JSON.stringify(storage));
} catch (error) {
console.error("Failed to save ABI storage:", error);
}
};

export const getAbiFromLocalStorage = (contractAddress: string): Abi | null => {
if (typeof window === "undefined") return null;
try {
const storage = getAbiStorage();
return storage.abis[contractAddress.toLowerCase()] || null;
} catch (error) {
console.error("Failed to get ABI from localStorage:", error);
return null;
}
};

export const saveAbiToLocalStorage = (contractAddress: string, abi: Abi) => {
if (typeof window === "undefined") return;
try {
const storage = getAbiStorage();
storage.abis[contractAddress.toLowerCase()] = abi;
saveAbiStorage(storage);
} catch (error) {
console.error("Failed to save ABI to localStorage:", error);
}
};

export const removeAbiFromLocalStorage = (contractAddress: string) => {
if (typeof window === "undefined") return;
try {
const storage = getAbiStorage();
delete storage.abis[contractAddress.toLowerCase()];
saveAbiStorage(storage);
} catch (error) {
console.error("Failed to remove ABI from localStorage:", error);
}
};

export const fetchContractABIFromEtherscan = async (verifiedContractAddress: Address, chainId: number) => {
const apiKey = process.env.NEXT_PUBLIC_ETHERSCAN_V2_API_KEY;
Expand Down
Loading