Initial commit with 🏗️ Scaffold-ETH 2 @ 1.0.5
This commit is contained in:
17
packages/nextjs/utils/scaffold-eth/block.ts
Normal file
17
packages/nextjs/utils/scaffold-eth/block.ts
Normal file
@@ -0,0 +1,17 @@
|
||||
import { Block, Transaction, TransactionReceipt } from "viem";
|
||||
|
||||
export type TransactionWithFunction = Transaction & {
|
||||
functionName?: string;
|
||||
functionArgs?: any[];
|
||||
functionArgNames?: string[];
|
||||
functionArgTypes?: string[];
|
||||
};
|
||||
|
||||
type TransactionReceipts = {
|
||||
[key: string]: TransactionReceipt;
|
||||
};
|
||||
|
||||
export type TransactionsTableProps = {
|
||||
blocks: Block[];
|
||||
transactionReceipts: TransactionReceipts;
|
||||
};
|
||||
8
packages/nextjs/utils/scaffold-eth/common.ts
Normal file
8
packages/nextjs/utils/scaffold-eth/common.ts
Normal file
@@ -0,0 +1,8 @@
|
||||
// To be used in JSON.stringify when a field might be bigint
|
||||
|
||||
// https://wagmi.sh/react/faq#bigint-serialization
|
||||
export const replacer = (_key: string, value: unknown) => (typeof value === "bigint" ? value.toString() : value);
|
||||
|
||||
export const ZERO_ADDRESS = "0x0000000000000000000000000000000000000000";
|
||||
|
||||
export const isZeroAddress = (address: string) => address === ZERO_ADDRESS;
|
||||
424
packages/nextjs/utils/scaffold-eth/contract.ts
Normal file
424
packages/nextjs/utils/scaffold-eth/contract.ts
Normal file
@@ -0,0 +1,424 @@
|
||||
import { getParsedError } from "./getParsedError";
|
||||
import { AllowedChainIds } from "./networks";
|
||||
import { notification } from "./notification";
|
||||
import { MutateOptions } from "@tanstack/react-query";
|
||||
import {
|
||||
Abi,
|
||||
AbiParameter,
|
||||
AbiParameterToPrimitiveType,
|
||||
AbiParametersToPrimitiveTypes,
|
||||
ExtractAbiEvent,
|
||||
ExtractAbiEventNames,
|
||||
ExtractAbiFunction,
|
||||
} from "abitype";
|
||||
import type { ExtractAbiFunctionNames } from "abitype";
|
||||
import type { Simplify } from "type-fest";
|
||||
import type { MergeDeepRecord } from "type-fest/source/merge-deep";
|
||||
import {
|
||||
Address,
|
||||
Block,
|
||||
GetEventArgs,
|
||||
GetTransactionReceiptReturnType,
|
||||
GetTransactionReturnType,
|
||||
Log,
|
||||
TransactionReceipt,
|
||||
WriteContractErrorType,
|
||||
keccak256,
|
||||
toHex,
|
||||
} from "viem";
|
||||
import { Config, UseReadContractParameters, UseWatchContractEventParameters, UseWriteContractParameters } from "wagmi";
|
||||
import { WriteContractParameters, WriteContractReturnType, simulateContract } from "wagmi/actions";
|
||||
import { WriteContractVariables } from "wagmi/query";
|
||||
import deployedContractsData from "~~/contracts/deployedContracts";
|
||||
import externalContractsData from "~~/contracts/externalContracts";
|
||||
import scaffoldConfig from "~~/scaffold.config";
|
||||
|
||||
type AddExternalFlag<T> = {
|
||||
[ChainId in keyof T]: {
|
||||
[ContractName in keyof T[ChainId]]: T[ChainId][ContractName] & { external?: true };
|
||||
};
|
||||
};
|
||||
|
||||
const deepMergeContracts = <L extends Record<PropertyKey, any>, E extends Record<PropertyKey, any>>(
|
||||
local: L,
|
||||
external: E,
|
||||
) => {
|
||||
const result: Record<PropertyKey, any> = {};
|
||||
const allKeys = Array.from(new Set([...Object.keys(external), ...Object.keys(local)]));
|
||||
for (const key of allKeys) {
|
||||
if (!external[key]) {
|
||||
result[key] = local[key];
|
||||
continue;
|
||||
}
|
||||
const amendedExternal = Object.fromEntries(
|
||||
Object.entries(external[key] as Record<string, Record<string, unknown>>).map(([contractName, declaration]) => [
|
||||
contractName,
|
||||
{ ...declaration, external: true },
|
||||
]),
|
||||
);
|
||||
result[key] = { ...local[key], ...amendedExternal };
|
||||
}
|
||||
return result as MergeDeepRecord<AddExternalFlag<L>, AddExternalFlag<E>, { arrayMergeMode: "replace" }>;
|
||||
};
|
||||
|
||||
const contractsData = deepMergeContracts(deployedContractsData, externalContractsData);
|
||||
|
||||
export type InheritedFunctions = { readonly [key: string]: string };
|
||||
|
||||
export type GenericContract = {
|
||||
address: Address;
|
||||
abi: Abi;
|
||||
inheritedFunctions?: InheritedFunctions;
|
||||
external?: true;
|
||||
deployedOnBlock?: number;
|
||||
};
|
||||
|
||||
export type GenericContractsDeclaration = {
|
||||
[chainId: number]: {
|
||||
[contractName: string]: GenericContract;
|
||||
};
|
||||
};
|
||||
|
||||
export const contracts = contractsData as GenericContractsDeclaration | null;
|
||||
|
||||
type ConfiguredChainId = (typeof scaffoldConfig)["targetNetworks"][0]["id"];
|
||||
|
||||
type IsContractDeclarationMissing<TYes, TNo> = typeof contractsData extends { [key in ConfiguredChainId]: any }
|
||||
? TNo
|
||||
: TYes;
|
||||
|
||||
type ContractsDeclaration = IsContractDeclarationMissing<GenericContractsDeclaration, typeof contractsData>;
|
||||
|
||||
type Contracts = ContractsDeclaration[ConfiguredChainId];
|
||||
|
||||
export type ContractName = keyof Contracts;
|
||||
|
||||
export type Contract<TContractName extends ContractName> = Contracts[TContractName];
|
||||
|
||||
type InferContractAbi<TContract> = TContract extends { abi: infer TAbi } ? TAbi : never;
|
||||
|
||||
export type ContractAbi<TContractName extends ContractName = ContractName> = InferContractAbi<Contract<TContractName>>;
|
||||
|
||||
export type AbiFunctionInputs<TAbi extends Abi, TFunctionName extends string> = ExtractAbiFunction<
|
||||
TAbi,
|
||||
TFunctionName
|
||||
>["inputs"];
|
||||
|
||||
export type AbiFunctionArguments<TAbi extends Abi, TFunctionName extends string> = AbiParametersToPrimitiveTypes<
|
||||
AbiFunctionInputs<TAbi, TFunctionName>
|
||||
>;
|
||||
|
||||
export type AbiFunctionOutputs<TAbi extends Abi, TFunctionName extends string> = ExtractAbiFunction<
|
||||
TAbi,
|
||||
TFunctionName
|
||||
>["outputs"];
|
||||
|
||||
export type AbiFunctionReturnType<TAbi extends Abi, TFunctionName extends string> = IsContractDeclarationMissing<
|
||||
any,
|
||||
AbiParametersToPrimitiveTypes<AbiFunctionOutputs<TAbi, TFunctionName>> extends readonly [any]
|
||||
? AbiParametersToPrimitiveTypes<AbiFunctionOutputs<TAbi, TFunctionName>>[0]
|
||||
: AbiParametersToPrimitiveTypes<AbiFunctionOutputs<TAbi, TFunctionName>>
|
||||
>;
|
||||
|
||||
export type AbiEventInputs<TAbi extends Abi, TEventName extends ExtractAbiEventNames<TAbi>> = ExtractAbiEvent<
|
||||
TAbi,
|
||||
TEventName
|
||||
>["inputs"];
|
||||
|
||||
export enum ContractCodeStatus {
|
||||
"LOADING",
|
||||
"DEPLOYED",
|
||||
"NOT_FOUND",
|
||||
}
|
||||
|
||||
type AbiStateMutability = "pure" | "view" | "nonpayable" | "payable";
|
||||
export type ReadAbiStateMutability = "view" | "pure";
|
||||
export type WriteAbiStateMutability = "nonpayable" | "payable";
|
||||
|
||||
export type FunctionNamesWithInputs<
|
||||
TContractName extends ContractName,
|
||||
TAbiStateMutability extends AbiStateMutability = AbiStateMutability,
|
||||
> = Exclude<
|
||||
Extract<
|
||||
ContractAbi<TContractName>[number],
|
||||
{
|
||||
type: "function";
|
||||
stateMutability: TAbiStateMutability;
|
||||
}
|
||||
>,
|
||||
{
|
||||
inputs: readonly [];
|
||||
}
|
||||
>["name"];
|
||||
|
||||
type Expand<T> = T extends object ? (T extends infer O ? { [K in keyof O]: O[K] } : never) : T;
|
||||
|
||||
type UnionToIntersection<U> = Expand<(U extends any ? (k: U) => void : never) extends (k: infer I) => void ? I : never>;
|
||||
|
||||
type OptionalTuple<T> = T extends readonly [infer H, ...infer R] ? readonly [H | undefined, ...OptionalTuple<R>] : T;
|
||||
|
||||
type UseScaffoldArgsParam<
|
||||
TContractName extends ContractName,
|
||||
TFunctionName extends ExtractAbiFunctionNames<ContractAbi<TContractName>>,
|
||||
> =
|
||||
TFunctionName extends FunctionNamesWithInputs<TContractName>
|
||||
? {
|
||||
args: OptionalTuple<UnionToIntersection<AbiFunctionArguments<ContractAbi<TContractName>, TFunctionName>>>;
|
||||
value?: ExtractAbiFunction<ContractAbi<TContractName>, TFunctionName>["stateMutability"] extends "payable"
|
||||
? bigint | undefined
|
||||
: undefined;
|
||||
}
|
||||
: {
|
||||
args?: never;
|
||||
};
|
||||
|
||||
export type UseDeployedContractConfig<TContractName extends ContractName> = {
|
||||
contractName: TContractName;
|
||||
chainId?: AllowedChainIds;
|
||||
};
|
||||
|
||||
export type UseScaffoldWriteConfig<TContractName extends ContractName> = {
|
||||
contractName: TContractName;
|
||||
chainId?: AllowedChainIds;
|
||||
disableSimulate?: boolean;
|
||||
writeContractParams?: UseWriteContractParameters;
|
||||
};
|
||||
|
||||
export type UseScaffoldReadConfig<
|
||||
TContractName extends ContractName,
|
||||
TFunctionName extends ExtractAbiFunctionNames<ContractAbi<TContractName>, ReadAbiStateMutability>,
|
||||
> = {
|
||||
contractName: TContractName;
|
||||
chainId?: AllowedChainIds;
|
||||
watch?: boolean;
|
||||
} & IsContractDeclarationMissing<
|
||||
Partial<UseReadContractParameters>,
|
||||
{
|
||||
functionName: TFunctionName;
|
||||
} & UseScaffoldArgsParam<TContractName, TFunctionName> &
|
||||
Omit<UseReadContractParameters, "chainId" | "abi" | "address" | "functionName" | "args">
|
||||
>;
|
||||
|
||||
export type ScaffoldWriteContractVariables<
|
||||
TContractName extends ContractName,
|
||||
TFunctionName extends ExtractAbiFunctionNames<ContractAbi<TContractName>, WriteAbiStateMutability>,
|
||||
> = IsContractDeclarationMissing<
|
||||
Partial<WriteContractParameters>,
|
||||
{
|
||||
functionName: TFunctionName;
|
||||
} & UseScaffoldArgsParam<TContractName, TFunctionName> &
|
||||
Omit<WriteContractParameters, "chainId" | "abi" | "address" | "functionName" | "args">
|
||||
>;
|
||||
|
||||
type WriteVariables = WriteContractVariables<Abi, string, any[], Config, number>;
|
||||
|
||||
export type TransactorFuncOptions = {
|
||||
onBlockConfirmation?: (txnReceipt: TransactionReceipt) => void;
|
||||
blockConfirmations?: number;
|
||||
};
|
||||
|
||||
export type ScaffoldWriteContractOptions = MutateOptions<
|
||||
WriteContractReturnType,
|
||||
WriteContractErrorType,
|
||||
WriteVariables,
|
||||
unknown
|
||||
> &
|
||||
TransactorFuncOptions;
|
||||
|
||||
export type UseScaffoldEventConfig<
|
||||
TContractName extends ContractName,
|
||||
TEventName extends ExtractAbiEventNames<ContractAbi<TContractName>>,
|
||||
TEvent extends ExtractAbiEvent<ContractAbi<TContractName>, TEventName> = ExtractAbiEvent<
|
||||
ContractAbi<TContractName>,
|
||||
TEventName
|
||||
>,
|
||||
> = {
|
||||
contractName: TContractName;
|
||||
eventName: TEventName;
|
||||
chainId?: AllowedChainIds;
|
||||
} & IsContractDeclarationMissing<
|
||||
Omit<UseWatchContractEventParameters, "onLogs" | "address" | "abi" | "eventName"> & {
|
||||
onLogs: (
|
||||
logs: Simplify<
|
||||
Omit<Log<bigint, number, any>, "args" | "eventName"> & {
|
||||
args: Record<string, unknown>;
|
||||
eventName: string;
|
||||
}
|
||||
>[],
|
||||
) => void;
|
||||
},
|
||||
Omit<UseWatchContractEventParameters<ContractAbi<TContractName>>, "onLogs" | "address" | "abi" | "eventName"> & {
|
||||
onLogs: (
|
||||
logs: Simplify<
|
||||
Omit<Log<bigint, number, false, TEvent, false, [TEvent], TEventName>, "args"> & {
|
||||
args: AbiParametersToPrimitiveTypes<TEvent["inputs"]> &
|
||||
GetEventArgs<
|
||||
ContractAbi<TContractName>,
|
||||
TEventName,
|
||||
{
|
||||
IndexedOnly: false;
|
||||
}
|
||||
>;
|
||||
}
|
||||
>[],
|
||||
) => void;
|
||||
}
|
||||
>;
|
||||
|
||||
type IndexedEventInputs<
|
||||
TContractName extends ContractName,
|
||||
TEventName extends ExtractAbiEventNames<ContractAbi<TContractName>>,
|
||||
> = Extract<AbiEventInputs<ContractAbi<TContractName>, TEventName>[number], { indexed: true }>;
|
||||
|
||||
export type EventFilters<
|
||||
TContractName extends ContractName,
|
||||
TEventName extends ExtractAbiEventNames<ContractAbi<TContractName>>,
|
||||
> = IsContractDeclarationMissing<
|
||||
any,
|
||||
IndexedEventInputs<TContractName, TEventName> extends never
|
||||
? never
|
||||
: {
|
||||
[Key in IsContractDeclarationMissing<
|
||||
any,
|
||||
IndexedEventInputs<TContractName, TEventName>["name"]
|
||||
>]?: AbiParameterToPrimitiveType<Extract<IndexedEventInputs<TContractName, TEventName>, { name: Key }>>;
|
||||
}
|
||||
>;
|
||||
|
||||
export type UseScaffoldEventHistoryConfig<
|
||||
TContractName extends ContractName,
|
||||
TEventName extends ExtractAbiEventNames<ContractAbi<TContractName>>,
|
||||
TBlockData extends boolean = false,
|
||||
TTransactionData extends boolean = false,
|
||||
TReceiptData extends boolean = false,
|
||||
> = {
|
||||
contractName: TContractName;
|
||||
eventName: IsContractDeclarationMissing<string, TEventName>;
|
||||
fromBlock?: bigint;
|
||||
toBlock?: bigint;
|
||||
chainId?: AllowedChainIds;
|
||||
filters?: EventFilters<TContractName, TEventName>;
|
||||
blockData?: TBlockData;
|
||||
transactionData?: TTransactionData;
|
||||
receiptData?: TReceiptData;
|
||||
watch?: boolean;
|
||||
enabled?: boolean;
|
||||
blocksBatchSize?: number;
|
||||
};
|
||||
|
||||
export type UseScaffoldEventHistoryData<
|
||||
TContractName extends ContractName,
|
||||
TEventName extends ExtractAbiEventNames<ContractAbi<TContractName>>,
|
||||
TBlockData extends boolean = false,
|
||||
TTransactionData extends boolean = false,
|
||||
TReceiptData extends boolean = false,
|
||||
TEvent extends ExtractAbiEvent<ContractAbi<TContractName>, TEventName> = ExtractAbiEvent<
|
||||
ContractAbi<TContractName>,
|
||||
TEventName
|
||||
>,
|
||||
> =
|
||||
| IsContractDeclarationMissing<
|
||||
any[],
|
||||
{
|
||||
args: AbiParametersToPrimitiveTypes<TEvent["inputs"]> &
|
||||
GetEventArgs<
|
||||
ContractAbi<TContractName>,
|
||||
TEventName,
|
||||
{
|
||||
IndexedOnly: false;
|
||||
}
|
||||
>;
|
||||
blockData: TBlockData extends true ? Block<bigint, true> : null;
|
||||
receiptData: TReceiptData extends true ? GetTransactionReturnType : null;
|
||||
transactionData: TTransactionData extends true ? GetTransactionReceiptReturnType : null;
|
||||
} & Log<bigint, number, false, TEvent, false, [TEvent], TEventName>[]
|
||||
>
|
||||
| undefined;
|
||||
|
||||
export type AbiParameterTuple = Extract<AbiParameter, { type: "tuple" | `tuple[${string}]` }>;
|
||||
|
||||
/**
|
||||
* Enhanced error parsing that creates a lookup table from all deployed contracts
|
||||
* to decode error signatures from any contract in the system
|
||||
*/
|
||||
export const getParsedErrorWithAllAbis = (error: any, chainId: AllowedChainIds): string => {
|
||||
const originalParsedError = getParsedError(error);
|
||||
|
||||
// Check if this is an unrecognized error signature
|
||||
if (/Encoded error signature.*not found on ABI/i.test(originalParsedError)) {
|
||||
const signatureMatch = originalParsedError.match(/0x[a-fA-F0-9]{8}/);
|
||||
const signature = signatureMatch ? signatureMatch[0] : "";
|
||||
|
||||
if (!signature) {
|
||||
return originalParsedError;
|
||||
}
|
||||
|
||||
try {
|
||||
// Get all deployed contracts for the current chain
|
||||
const chainContracts = deployedContractsData[chainId as keyof typeof deployedContractsData];
|
||||
|
||||
if (!chainContracts) {
|
||||
return originalParsedError;
|
||||
}
|
||||
|
||||
// Build a lookup table of error signatures to error names
|
||||
const errorLookup: Record<string, { name: string; contract: string; signature: string }> = {};
|
||||
|
||||
Object.entries(chainContracts).forEach(([contractName, contract]: [string, any]) => {
|
||||
if (contract.abi) {
|
||||
contract.abi.forEach((item: any) => {
|
||||
if (item.type === "error") {
|
||||
// Create the proper error signature like Solidity does
|
||||
const errorName = item.name;
|
||||
const inputs = item.inputs || [];
|
||||
const inputTypes = inputs.map((input: any) => input.type).join(",");
|
||||
const errorSignature = `${errorName}(${inputTypes})`;
|
||||
|
||||
// Hash the signature and take the first 4 bytes (8 hex chars)
|
||||
const hash = keccak256(toHex(errorSignature));
|
||||
const errorSelector = hash.slice(0, 10); // 0x + 8 chars = 10 total
|
||||
|
||||
errorLookup[errorSelector] = {
|
||||
name: errorName,
|
||||
contract: contractName,
|
||||
signature: errorSignature,
|
||||
};
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// Check if we can find the error in our lookup
|
||||
const errorInfo = errorLookup[signature];
|
||||
if (errorInfo) {
|
||||
return `Contract function execution reverted with the following reason:\n${errorInfo.signature} from ${errorInfo.contract} contract`;
|
||||
}
|
||||
|
||||
// If not found in simple lookup, provide a helpful message with context
|
||||
return `${originalParsedError}\n\nThis error occurred when calling a function that internally calls another contract. Check the contract that your function calls internally for more details.`;
|
||||
} catch (lookupError) {
|
||||
console.log("Failed to create error lookup table:", lookupError);
|
||||
}
|
||||
}
|
||||
|
||||
return originalParsedError;
|
||||
};
|
||||
|
||||
export const simulateContractWriteAndNotifyError = async ({
|
||||
wagmiConfig,
|
||||
writeContractParams: params,
|
||||
chainId,
|
||||
}: {
|
||||
wagmiConfig: Config;
|
||||
writeContractParams: WriteContractVariables<Abi, string, any[], Config, number>;
|
||||
chainId: AllowedChainIds;
|
||||
}) => {
|
||||
try {
|
||||
await simulateContract(wagmiConfig, params);
|
||||
} catch (error) {
|
||||
const parsedError = getParsedErrorWithAllAbis(error, chainId);
|
||||
|
||||
notification.error(parsedError);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
11
packages/nextjs/utils/scaffold-eth/contractsData.ts
Normal file
11
packages/nextjs/utils/scaffold-eth/contractsData.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
import { useTargetNetwork } from "~~/hooks/scaffold-eth";
|
||||
import { GenericContractsDeclaration, contracts } from "~~/utils/scaffold-eth/contract";
|
||||
|
||||
const DEFAULT_ALL_CONTRACTS: GenericContractsDeclaration[number] = {};
|
||||
|
||||
export function useAllContracts() {
|
||||
const { targetNetwork } = useTargetNetwork();
|
||||
const contractsData = contracts?.[targetNetwork.id];
|
||||
// using constant to avoid creating a new object on every call
|
||||
return contractsData || DEFAULT_ALL_CONTRACTS;
|
||||
}
|
||||
65
packages/nextjs/utils/scaffold-eth/decodeTxData.ts
Normal file
65
packages/nextjs/utils/scaffold-eth/decodeTxData.ts
Normal file
@@ -0,0 +1,65 @@
|
||||
import { TransactionWithFunction } from "./block";
|
||||
import { GenericContractsDeclaration } from "./contract";
|
||||
import { Abi, AbiFunction, decodeFunctionData, getAbiItem } from "viem";
|
||||
import { hardhat } from "viem/chains";
|
||||
import contractData from "~~/contracts/deployedContracts";
|
||||
|
||||
type ContractsInterfaces = Record<string, Abi>;
|
||||
type TransactionType = TransactionWithFunction | null;
|
||||
|
||||
const deployedContracts = contractData as GenericContractsDeclaration | null;
|
||||
const chainMetaData = deployedContracts?.[hardhat.id];
|
||||
const interfaces = chainMetaData
|
||||
? Object.entries(chainMetaData).reduce((finalInterfacesObj, [contractName, contract]) => {
|
||||
finalInterfacesObj[contractName] = contract.abi;
|
||||
return finalInterfacesObj;
|
||||
}, {} as ContractsInterfaces)
|
||||
: {};
|
||||
|
||||
export const decodeTransactionData = (tx: TransactionWithFunction) => {
|
||||
if (tx.input.length >= 10 && !tx.input.startsWith("0x60e06040")) {
|
||||
let foundInterface = false;
|
||||
for (const [, contractAbi] of Object.entries(interfaces)) {
|
||||
try {
|
||||
const { functionName, args } = decodeFunctionData({
|
||||
abi: contractAbi,
|
||||
data: tx.input,
|
||||
});
|
||||
tx.functionName = functionName;
|
||||
tx.functionArgs = args as any[];
|
||||
tx.functionArgNames = getAbiItem<AbiFunction[], string>({
|
||||
abi: contractAbi as AbiFunction[],
|
||||
name: functionName,
|
||||
})?.inputs?.map((input: any) => input.name);
|
||||
tx.functionArgTypes = getAbiItem<AbiFunction[], string>({
|
||||
abi: contractAbi as AbiFunction[],
|
||||
name: functionName,
|
||||
})?.inputs.map((input: any) => input.type);
|
||||
foundInterface = true;
|
||||
break;
|
||||
} catch {
|
||||
// do nothing
|
||||
}
|
||||
}
|
||||
if (!foundInterface) {
|
||||
tx.functionName = "⚠️ Unknown";
|
||||
}
|
||||
}
|
||||
return tx;
|
||||
};
|
||||
|
||||
export const getFunctionDetails = (transaction: TransactionType) => {
|
||||
if (
|
||||
transaction &&
|
||||
transaction.functionName &&
|
||||
transaction.functionArgNames &&
|
||||
transaction.functionArgTypes &&
|
||||
transaction.functionArgs
|
||||
) {
|
||||
const details = transaction.functionArgNames.map(
|
||||
(name, i) => `${transaction.functionArgTypes?.[i] || ""} ${name} = ${transaction.functionArgs?.[i] ?? ""}`,
|
||||
);
|
||||
return `${transaction.functionName}(${details.join(", ")})`;
|
||||
}
|
||||
return "";
|
||||
};
|
||||
72
packages/nextjs/utils/scaffold-eth/fetchPriceFromUniswap.ts
Normal file
72
packages/nextjs/utils/scaffold-eth/fetchPriceFromUniswap.ts
Normal file
@@ -0,0 +1,72 @@
|
||||
import { ChainWithAttributes, getAlchemyHttpUrl } from "./networks";
|
||||
import { CurrencyAmount, Token } from "@uniswap/sdk-core";
|
||||
import { Pair, Route } from "@uniswap/v2-sdk";
|
||||
import { Address, createPublicClient, fallback, http, parseAbi } from "viem";
|
||||
import { mainnet } from "viem/chains";
|
||||
|
||||
const alchemyHttpUrl = getAlchemyHttpUrl(mainnet.id);
|
||||
const rpcFallbacks = alchemyHttpUrl ? [http(alchemyHttpUrl), http()] : [http()];
|
||||
const publicClient = createPublicClient({
|
||||
chain: mainnet,
|
||||
transport: fallback(rpcFallbacks),
|
||||
});
|
||||
|
||||
const ABI = parseAbi([
|
||||
"function getReserves() external view returns (uint112 reserve0, uint112 reserve1, uint32 blockTimestampLast)",
|
||||
"function token0() external view returns (address)",
|
||||
"function token1() external view returns (address)",
|
||||
]);
|
||||
|
||||
export const fetchPriceFromUniswap = async (targetNetwork: ChainWithAttributes): Promise<number> => {
|
||||
if (
|
||||
targetNetwork.nativeCurrency.symbol !== "ETH" &&
|
||||
targetNetwork.nativeCurrency.symbol !== "SEP" &&
|
||||
!targetNetwork.nativeCurrencyTokenAddress
|
||||
) {
|
||||
return 0;
|
||||
}
|
||||
try {
|
||||
const DAI = new Token(1, "0x6B175474E89094C44Da98b954EedeAC495271d0F", 18);
|
||||
const TOKEN = new Token(
|
||||
1,
|
||||
targetNetwork.nativeCurrencyTokenAddress || "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2",
|
||||
18,
|
||||
);
|
||||
const pairAddress = Pair.getAddress(TOKEN, DAI) as Address;
|
||||
|
||||
const wagmiConfig = {
|
||||
address: pairAddress,
|
||||
abi: ABI,
|
||||
};
|
||||
|
||||
const reserves = await publicClient.readContract({
|
||||
...wagmiConfig,
|
||||
functionName: "getReserves",
|
||||
});
|
||||
|
||||
const token0Address = await publicClient.readContract({
|
||||
...wagmiConfig,
|
||||
functionName: "token0",
|
||||
});
|
||||
|
||||
const token1Address = await publicClient.readContract({
|
||||
...wagmiConfig,
|
||||
functionName: "token1",
|
||||
});
|
||||
const token0 = [TOKEN, DAI].find(token => token.address === token0Address) as Token;
|
||||
const token1 = [TOKEN, DAI].find(token => token.address === token1Address) as Token;
|
||||
const pair = new Pair(
|
||||
CurrencyAmount.fromRawAmount(token0, reserves[0].toString()),
|
||||
CurrencyAmount.fromRawAmount(token1, reserves[1].toString()),
|
||||
);
|
||||
const route = new Route([pair], TOKEN, DAI);
|
||||
const price = parseFloat(route.midPrice.toSignificant(6));
|
||||
return price;
|
||||
} catch (error) {
|
||||
console.error(
|
||||
`useNativeCurrencyPrice - Error fetching ${targetNetwork.nativeCurrency.symbol} price from Uniswap: `,
|
||||
error,
|
||||
);
|
||||
return 0;
|
||||
}
|
||||
};
|
||||
56
packages/nextjs/utils/scaffold-eth/getMetadata.ts
Normal file
56
packages/nextjs/utils/scaffold-eth/getMetadata.ts
Normal file
@@ -0,0 +1,56 @@
|
||||
import type { Metadata } from "next";
|
||||
|
||||
const baseUrl = process.env.VERCEL_PROJECT_PRODUCTION_URL
|
||||
? `https://${process.env.VERCEL_PROJECT_PRODUCTION_URL}`
|
||||
: `http://localhost:${process.env.PORT || 3000}`;
|
||||
const titleTemplate = "%s | SpeedRunEthereum";
|
||||
|
||||
export const getMetadata = ({
|
||||
title,
|
||||
description,
|
||||
imageRelativePath = "/thumbnail-challenge.png",
|
||||
}: {
|
||||
title: string;
|
||||
description: string;
|
||||
imageRelativePath?: string;
|
||||
}): Metadata => {
|
||||
const imageUrl = `${baseUrl}${imageRelativePath}`;
|
||||
|
||||
return {
|
||||
metadataBase: new URL(baseUrl),
|
||||
title: {
|
||||
default: title,
|
||||
template: titleTemplate,
|
||||
},
|
||||
description: description,
|
||||
openGraph: {
|
||||
title: {
|
||||
default: title,
|
||||
template: titleTemplate,
|
||||
},
|
||||
description: description,
|
||||
images: [
|
||||
{
|
||||
url: imageUrl,
|
||||
},
|
||||
],
|
||||
},
|
||||
twitter: {
|
||||
title: {
|
||||
default: title,
|
||||
template: titleTemplate,
|
||||
},
|
||||
description: description,
|
||||
images: [imageUrl],
|
||||
},
|
||||
icons: {
|
||||
icon: [
|
||||
{
|
||||
url: "/favicon.png",
|
||||
sizes: "32x32",
|
||||
type: "image/png",
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
};
|
||||
35
packages/nextjs/utils/scaffold-eth/getParsedError.ts
Normal file
35
packages/nextjs/utils/scaffold-eth/getParsedError.ts
Normal file
@@ -0,0 +1,35 @@
|
||||
import { BaseError as BaseViemError, ContractFunctionRevertedError } from "viem";
|
||||
|
||||
/**
|
||||
* Parses an viem/wagmi error to get a displayable string
|
||||
* @param e - error object
|
||||
* @returns parsed error string
|
||||
*/
|
||||
export const getParsedError = (error: any): string => {
|
||||
const parsedError = error?.walk ? error.walk() : error;
|
||||
|
||||
if (parsedError instanceof BaseViemError) {
|
||||
if (parsedError.details) {
|
||||
return parsedError.details;
|
||||
}
|
||||
|
||||
if (parsedError.shortMessage) {
|
||||
if (
|
||||
parsedError instanceof ContractFunctionRevertedError &&
|
||||
parsedError.data &&
|
||||
parsedError.data.errorName !== "Error"
|
||||
) {
|
||||
const customErrorArgs = parsedError.data.args?.toString() ?? "";
|
||||
return `${parsedError.shortMessage.replace(/reverted\.$/, "reverted with the following reason:")}\n${
|
||||
parsedError.data.errorName
|
||||
}(${customErrorArgs})`;
|
||||
}
|
||||
|
||||
return parsedError.shortMessage;
|
||||
}
|
||||
|
||||
return parsedError.message ?? parsedError.name ?? "An unknown error occurred";
|
||||
}
|
||||
|
||||
return parsedError?.message ?? "An unknown error occurred";
|
||||
};
|
||||
6
packages/nextjs/utils/scaffold-eth/index.ts
Normal file
6
packages/nextjs/utils/scaffold-eth/index.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
export * from "./fetchPriceFromUniswap";
|
||||
export * from "./networks";
|
||||
export * from "./notification";
|
||||
export * from "./block";
|
||||
export * from "./decodeTxData";
|
||||
export * from "./getParsedError";
|
||||
145
packages/nextjs/utils/scaffold-eth/networks.ts
Normal file
145
packages/nextjs/utils/scaffold-eth/networks.ts
Normal file
@@ -0,0 +1,145 @@
|
||||
import * as chains from "viem/chains";
|
||||
import scaffoldConfig from "~~/scaffold.config";
|
||||
|
||||
type ChainAttributes = {
|
||||
// color | [lightThemeColor, darkThemeColor]
|
||||
color: string | [string, string];
|
||||
// Used to fetch price by providing mainnet token address
|
||||
// for networks having native currency other than ETH
|
||||
nativeCurrencyTokenAddress?: string;
|
||||
};
|
||||
|
||||
export type ChainWithAttributes = chains.Chain & Partial<ChainAttributes>;
|
||||
export type AllowedChainIds = (typeof scaffoldConfig.targetNetworks)[number]["id"];
|
||||
|
||||
// Mapping of chainId to RPC chain name an format followed by alchemy and infura
|
||||
export const RPC_CHAIN_NAMES: Record<number, string> = {
|
||||
[chains.mainnet.id]: "eth-mainnet",
|
||||
[chains.goerli.id]: "eth-goerli",
|
||||
[chains.sepolia.id]: "eth-sepolia",
|
||||
[chains.optimism.id]: "opt-mainnet",
|
||||
[chains.optimismGoerli.id]: "opt-goerli",
|
||||
[chains.optimismSepolia.id]: "opt-sepolia",
|
||||
[chains.arbitrum.id]: "arb-mainnet",
|
||||
[chains.arbitrumGoerli.id]: "arb-goerli",
|
||||
[chains.arbitrumSepolia.id]: "arb-sepolia",
|
||||
[chains.polygon.id]: "polygon-mainnet",
|
||||
[chains.polygonMumbai.id]: "polygon-mumbai",
|
||||
[chains.polygonAmoy.id]: "polygon-amoy",
|
||||
[chains.astar.id]: "astar-mainnet",
|
||||
[chains.polygonZkEvm.id]: "polygonzkevm-mainnet",
|
||||
[chains.polygonZkEvmTestnet.id]: "polygonzkevm-testnet",
|
||||
[chains.base.id]: "base-mainnet",
|
||||
[chains.baseGoerli.id]: "base-goerli",
|
||||
[chains.baseSepolia.id]: "base-sepolia",
|
||||
[chains.celo.id]: "celo-mainnet",
|
||||
[chains.celoSepolia.id]: "celo-sepolia",
|
||||
};
|
||||
|
||||
export const getAlchemyHttpUrl = (chainId: number) => {
|
||||
return scaffoldConfig.alchemyApiKey && RPC_CHAIN_NAMES[chainId]
|
||||
? `https://${RPC_CHAIN_NAMES[chainId]}.g.alchemy.com/v2/${scaffoldConfig.alchemyApiKey}`
|
||||
: undefined;
|
||||
};
|
||||
|
||||
export const NETWORKS_EXTRA_DATA: Record<string, ChainAttributes> = {
|
||||
[chains.hardhat.id]: {
|
||||
color: "#b8af0c",
|
||||
},
|
||||
[chains.mainnet.id]: {
|
||||
color: "#ff8b9e",
|
||||
},
|
||||
[chains.sepolia.id]: {
|
||||
color: ["#5f4bb6", "#87ff65"],
|
||||
},
|
||||
[chains.gnosis.id]: {
|
||||
color: "#48a9a6",
|
||||
},
|
||||
[chains.polygon.id]: {
|
||||
color: "#2bbdf7",
|
||||
nativeCurrencyTokenAddress: "0x7D1AfA7B718fb893dB30A3aBc0Cfc608AaCfeBB0",
|
||||
},
|
||||
[chains.polygonMumbai.id]: {
|
||||
color: "#92D9FA",
|
||||
nativeCurrencyTokenAddress: "0x7D1AfA7B718fb893dB30A3aBc0Cfc608AaCfeBB0",
|
||||
},
|
||||
[chains.optimismSepolia.id]: {
|
||||
color: "#f01a37",
|
||||
},
|
||||
[chains.optimism.id]: {
|
||||
color: "#f01a37",
|
||||
},
|
||||
[chains.arbitrumSepolia.id]: {
|
||||
color: "#28a0f0",
|
||||
},
|
||||
[chains.arbitrum.id]: {
|
||||
color: "#28a0f0",
|
||||
},
|
||||
[chains.fantom.id]: {
|
||||
color: "#1969ff",
|
||||
},
|
||||
[chains.fantomTestnet.id]: {
|
||||
color: "#1969ff",
|
||||
},
|
||||
[chains.scrollSepolia.id]: {
|
||||
color: "#fbebd4",
|
||||
},
|
||||
[chains.celo.id]: {
|
||||
color: "#FCFF52",
|
||||
},
|
||||
[chains.celoSepolia.id]: {
|
||||
color: "#476520",
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
* Gives the block explorer transaction URL, returns empty string if the network is a local chain
|
||||
*/
|
||||
export function getBlockExplorerTxLink(chainId: number, txnHash: string) {
|
||||
const chainNames = Object.keys(chains);
|
||||
|
||||
const targetChainArr = chainNames.filter(chainName => {
|
||||
const wagmiChain = chains[chainName as keyof typeof chains];
|
||||
return wagmiChain.id === chainId;
|
||||
});
|
||||
|
||||
if (targetChainArr.length === 0) {
|
||||
return "";
|
||||
}
|
||||
|
||||
const targetChain = targetChainArr[0] as keyof typeof chains;
|
||||
const blockExplorerTxURL = chains[targetChain]?.blockExplorers?.default?.url;
|
||||
|
||||
if (!blockExplorerTxURL) {
|
||||
return "";
|
||||
}
|
||||
|
||||
return `${blockExplorerTxURL}/tx/${txnHash}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gives the block explorer URL for a given address.
|
||||
* Defaults to Etherscan if no (wagmi) block explorer is configured for the network.
|
||||
*/
|
||||
export function getBlockExplorerAddressLink(network: chains.Chain, address: string) {
|
||||
const blockExplorerBaseURL = network.blockExplorers?.default?.url;
|
||||
if (network.id === chains.hardhat.id) {
|
||||
return `/blockexplorer/address/${address}`;
|
||||
}
|
||||
|
||||
if (!blockExplorerBaseURL) {
|
||||
return `https://etherscan.io/address/${address}`;
|
||||
}
|
||||
|
||||
return `${blockExplorerBaseURL}/address/${address}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns targetNetworks array containing networks configured in scaffold.config including extra network metadata
|
||||
*/
|
||||
export function getTargetNetworks(): ChainWithAttributes[] {
|
||||
return scaffoldConfig.targetNetworks.map(targetNetwork => ({
|
||||
...targetNetwork,
|
||||
...NETWORKS_EXTRA_DATA[targetNetwork.id],
|
||||
}));
|
||||
}
|
||||
90
packages/nextjs/utils/scaffold-eth/notification.tsx
Normal file
90
packages/nextjs/utils/scaffold-eth/notification.tsx
Normal file
@@ -0,0 +1,90 @@
|
||||
import React from "react";
|
||||
import { Toast, ToastPosition, toast } from "react-hot-toast";
|
||||
import { XMarkIcon } from "@heroicons/react/20/solid";
|
||||
import {
|
||||
CheckCircleIcon,
|
||||
ExclamationCircleIcon,
|
||||
ExclamationTriangleIcon,
|
||||
InformationCircleIcon,
|
||||
} from "@heroicons/react/24/solid";
|
||||
|
||||
type NotificationProps = {
|
||||
content: React.ReactNode;
|
||||
status: "success" | "info" | "loading" | "error" | "warning";
|
||||
duration?: number;
|
||||
icon?: string;
|
||||
position?: ToastPosition;
|
||||
};
|
||||
|
||||
type NotificationOptions = {
|
||||
duration?: number;
|
||||
icon?: string;
|
||||
position?: ToastPosition;
|
||||
};
|
||||
|
||||
const ENUM_STATUSES = {
|
||||
success: <CheckCircleIcon className="w-7 text-success" />,
|
||||
loading: <span className="w-6 loading loading-spinner"></span>,
|
||||
error: <ExclamationCircleIcon className="w-7 text-error" />,
|
||||
info: <InformationCircleIcon className="w-7 text-info" />,
|
||||
warning: <ExclamationTriangleIcon className="w-7 text-warning" />,
|
||||
};
|
||||
|
||||
const DEFAULT_DURATION = 3000;
|
||||
const DEFAULT_POSITION: ToastPosition = "top-center";
|
||||
|
||||
/**
|
||||
* Custom Notification
|
||||
*/
|
||||
const Notification = ({
|
||||
content,
|
||||
status,
|
||||
duration = DEFAULT_DURATION,
|
||||
icon,
|
||||
position = DEFAULT_POSITION,
|
||||
}: NotificationProps) => {
|
||||
return toast.custom(
|
||||
(t: Toast) => (
|
||||
<div
|
||||
className={`flex flex-row items-start justify-between max-w-sm rounded-xl shadow-center shadow-accent bg-base-200 p-4 transform-gpu relative transition-all duration-500 ease-in-out space-x-2
|
||||
${
|
||||
position.substring(0, 3) == "top"
|
||||
? `hover:translate-y-1 ${t.visible ? "top-0" : "-top-96"}`
|
||||
: `hover:-translate-y-1 ${t.visible ? "bottom-0" : "-bottom-96"}`
|
||||
}`}
|
||||
>
|
||||
<div className="leading-[0] self-center">{icon ? icon : ENUM_STATUSES[status]}</div>
|
||||
<div className={`overflow-x-hidden break-words whitespace-pre-line ${icon ? "mt-1" : ""}`}>{content}</div>
|
||||
|
||||
<div className={`cursor-pointer text-lg ${icon ? "mt-1" : ""}`} onClick={() => toast.dismiss(t.id)}>
|
||||
<XMarkIcon className="w-6 cursor-pointer" onClick={() => toast.remove(t.id)} />
|
||||
</div>
|
||||
</div>
|
||||
),
|
||||
{
|
||||
duration: status === "loading" ? Infinity : duration,
|
||||
position,
|
||||
},
|
||||
);
|
||||
};
|
||||
|
||||
export const notification = {
|
||||
success: (content: React.ReactNode, options?: NotificationOptions) => {
|
||||
return Notification({ content, status: "success", ...options });
|
||||
},
|
||||
info: (content: React.ReactNode, options?: NotificationOptions) => {
|
||||
return Notification({ content, status: "info", ...options });
|
||||
},
|
||||
warning: (content: React.ReactNode, options?: NotificationOptions) => {
|
||||
return Notification({ content, status: "warning", ...options });
|
||||
},
|
||||
error: (content: React.ReactNode, options?: NotificationOptions) => {
|
||||
return Notification({ content, status: "error", ...options });
|
||||
},
|
||||
loading: (content: React.ReactNode, options?: NotificationOptions) => {
|
||||
return Notification({ content, status: "loading", ...options });
|
||||
},
|
||||
remove: (toastId: string) => {
|
||||
toast.remove(toastId);
|
||||
},
|
||||
};
|
||||
Reference in New Issue
Block a user