From c0383eef247481829f603e4178248afa1b2ec5d0 Mon Sep 17 00:00:00 2001 From: Kevin Ingersoll Date: Thu, 18 Apr 2024 21:50:13 +0100 Subject: [PATCH] rework config --- .../account-kit/src/AccountKitProvider.tsx | 32 +---- packages/account-kit/src/config.ts | 77 ++++++++++++ .../src/steps/gas-tank/GasSpenderContent.tsx | 7 +- .../src/steps/gas-tank/RelayLinkContent.tsx | 10 +- .../steps/gas-tank/hooks/useDepositHandler.ts | 14 ++- .../steps/gas-tank/hooks/useEstimatedFees.ts | 29 ----- .../gas-tank/hooks/useTransactionFees.ts | 11 +- .../account-kit/src/useAppAccountClient.ts | 113 ++++++++++-------- packages/account-kit/src/useErc4337Config.ts | 34 ++++++ packages/account-kit/src/useGasTankBalance.ts | 8 +- packages/account-kit/src/useIsGasSpender.ts | 8 +- packages/account-kit/src/usePaymaster.ts | 7 ++ packages/common/src/chains/mudFoundry.ts | 12 +- packages/common/src/chains/types.ts | 18 ++- packages/common/src/utils/assert.ts | 6 + packages/common/src/utils/index.ts | 1 + 16 files changed, 243 insertions(+), 144 deletions(-) create mode 100644 packages/account-kit/src/config.ts delete mode 100644 packages/account-kit/src/steps/gas-tank/hooks/useEstimatedFees.ts create mode 100644 packages/account-kit/src/useErc4337Config.ts create mode 100644 packages/account-kit/src/usePaymaster.ts create mode 100644 packages/common/src/utils/assert.ts diff --git a/packages/account-kit/src/AccountKitProvider.tsx b/packages/account-kit/src/AccountKitProvider.tsx index 54f18a8302..1a4dd13015 100644 --- a/packages/account-kit/src/AccountKitProvider.tsx +++ b/packages/account-kit/src/AccountKitProvider.tsx @@ -1,29 +1,6 @@ import { createContext, useContext, type ReactNode } from "react"; -import { Address } from "viem"; -import { MUDChain } from "@latticexyz/common/chains"; import { AccountModal } from "./AccountModal"; - -export type Config = { - readonly chain: MUDChain; - readonly worldAddress: Address; - /** - * Address of the `GasTank` paymaster. Defaults to `chain.contracts.gasTank.address` if set. - * @link http://www.npmjs.com/package/@latticexyz/gas-tank - */ - readonly gasTankAddress?: Address; - readonly appInfo?: { - readonly name?: string; - /** - * The app icon used throughout the onboarding process. It will be used as a fallback if no `image` is provided. Icon should be 1:1 aspect ratio, at least 200x200. - */ - readonly icon?: string; - /** - * The image displayed during the first step of onboarding. Ideally around 600x250. - */ - readonly image?: string; - }; - theme?: "dark" | "light"; -}; +import { Config } from "./config"; /** @internal */ const Context = createContext(null); @@ -37,12 +14,7 @@ export function AccountKitProvider({ config, children }: Props) { const currentConfig = useContext(Context); if (currentConfig) throw new Error("`AccountKitProvider` can only be used once."); return ( - + {children} diff --git a/packages/account-kit/src/config.ts b/packages/account-kit/src/config.ts new file mode 100644 index 0000000000..d05ad36502 --- /dev/null +++ b/packages/account-kit/src/config.ts @@ -0,0 +1,77 @@ +import { MUDChain } from "@latticexyz/common/chains"; +import { satisfy } from "@latticexyz/common/type-utils"; +import { Transport } from "viem"; +import { Address } from "viem/accounts"; + +export type PaymasterType = "gasTank"; +export type PaymasterBase = { readonly type: PaymasterType; readonly address: Address }; + +/** + * @link http://www.npmjs.com/package/@latticexyz/gas-tank + */ +export type GasTankPaymaster = { + readonly type: "gasTank"; + /** + * Address of the `GasTank` paymaster. Defaults to `chain.contracts.gasTank.address` if set. + * @link http://www.npmjs.com/package/@latticexyz/gas-tank + */ + readonly address: Address; +}; + +export type Paymaster = satisfy; + +export type Erc4337Config = { + /** + * viem `Transport` for ERC-4337-specific RPC methods (e.g. `eth_sendUserOperation`). + * + * If not set, defaults to `http(chain.rpcUrls.erc4337Bundler.http[0])`. + */ + readonly transport: Transport; + /** + * List of ERC-4337 paymasters. + * + * If not set, defaults to `gasTank` paymaster using `chain.contracts.gasTank.address`. + */ + readonly paymasters: readonly [Paymaster, ...Paymaster[]]; +}; + +export type Config = { + /** + * The chain the world is deployed to. This is the chain used to return an app account client once fully signed in. + */ + readonly chain: MUDChain; + /* + * The world address. + */ + readonly worldAddress: Address; + + /** + * Account Kit UI theme. + * + * If not set, defaults to OS' light or dark mode. + */ + theme?: "dark" | "light"; + + readonly appInfo?: { + /** + * The app name. + * + * If not set, defaults to page's ``. + */ + readonly name?: string; + /** + * The app icon used throughout the onboarding process. It will be used as a fallback if no `image` is provided. Icon should be 1:1 aspect ratio, at least 200x200. + * + * If not set, defaults to the page's `<link rel="icon">` or the origin's `/favicon.ico`. + */ + readonly icon?: string; + /** + * The splash image displayed during the first step of onboarding. Ideally around 600x250. + * + * If not set, defaults to displaying the name, icon, and origin. + */ + readonly image?: string; + }; + + readonly erc4337?: Erc4337Config | false; +}; diff --git a/packages/account-kit/src/steps/gas-tank/GasSpenderContent.tsx b/packages/account-kit/src/steps/gas-tank/GasSpenderContent.tsx index 5308ce814a..4e1b481870 100644 --- a/packages/account-kit/src/steps/gas-tank/GasSpenderContent.tsx +++ b/packages/account-kit/src/steps/gas-tank/GasSpenderContent.tsx @@ -10,10 +10,12 @@ import { useMutation, useQueryClient } from "@tanstack/react-query"; import { Button } from "../../ui/Button"; import { AccountModalSection } from "../../AccountModalSection"; import { useOnboardingSteps } from "../../useOnboardingSteps"; +import { usePaymaster } from "../../usePaymaster"; export function GasSpenderContent() { const queryClient = useQueryClient(); - const { chain, gasTankAddress } = useConfig(); + const { chain } = useConfig(); + const gasTank = usePaymaster("gasTank"); const publicClient = usePublicClient({ chainId: chain.id }); const { data: userAccountClient } = useWalletClient({ chainId: chain.id }); const appAccountClient = useAppAccountClient(); @@ -24,11 +26,12 @@ export function GasSpenderContent() { if (!publicClient) throw new Error("Public client not ready. Not connected?"); if (!userAccountClient) throw new Error("Wallet client not ready. Not connected?"); if (!appAccountClient) throw new Error("App account client not ready."); + if (!gasTank) throw new Error("No gas tank configured."); console.log("registerSpender"); const hash = await callWithSignature({ chainId: chain.id, - worldAddress: gasTankAddress, + worldAddress: gasTank.address, systemId: resourceToHex({ type: "system", namespace: "", name: "PaymasterSystem" }), callData: encodeFunctionData({ abi: GasTankAbi, diff --git a/packages/account-kit/src/steps/gas-tank/RelayLinkContent.tsx b/packages/account-kit/src/steps/gas-tank/RelayLinkContent.tsx index c21b615ecb..5664c7299f 100644 --- a/packages/account-kit/src/steps/gas-tank/RelayLinkContent.tsx +++ b/packages/account-kit/src/steps/gas-tank/RelayLinkContent.tsx @@ -6,6 +6,7 @@ import { Button } from "../../ui/Button"; import { parseEther } from "viem"; import { createRelayClient } from "./utils/createRelayClient"; import { useConfig } from "../../AccountKitProvider"; +import { usePaymaster } from "../../usePaymaster"; createRelayClient(); @@ -15,7 +16,8 @@ type RelayLinkContentProps = { export function RelayLinkContent({ amount }: RelayLinkContentProps) { const wallet = useWalletClient(); - const { chain, gasTankAddress } = useConfig(); + const { chain } = useConfig(); + const gasTank = usePaymaster("gasTank"); const userAccount = useAccount(); const userAccountChainId = userAccount?.chain?.id; const publicClient = usePublicClient({ @@ -51,12 +53,12 @@ export function RelayLinkContent({ amount }: RelayLinkContentProps) { }, [amount, wallet.data, chain, selectedSourceChainId]); const executeDeposit = useCallback(async () => { - if (!wallet.data || !amount || selectedSourceChainId == null || !publicClient) return; + if (!wallet.data || !amount || selectedSourceChainId == null || !publicClient || !gasTank) return; await wallet.data.switchChain({ id: selectedSourceChainId }); const { request } = await publicClient.simulateContract({ - address: gasTankAddress, + address: gasTank.address, abi: GasTankAbi, functionName: "depositTo", args: [wallet.data.account.address], @@ -77,7 +79,7 @@ export function RelayLinkContent({ amount }: RelayLinkContentProps) { } }, }); - }, [wallet.data, amount, selectedSourceChainId, publicClient, gasTankAddress, chain.id]); + }, [wallet.data, amount, selectedSourceChainId, publicClient, gasTank, chain.id]); const handleSubmit = async (evt: React.FormEvent<HTMLFormElement>) => { evt.preventDefault(); diff --git a/packages/account-kit/src/steps/gas-tank/hooks/useDepositHandler.ts b/packages/account-kit/src/steps/gas-tank/hooks/useDepositHandler.ts index c732b33a8d..630b38b911 100644 --- a/packages/account-kit/src/steps/gas-tank/hooks/useDepositHandler.ts +++ b/packages/account-kit/src/steps/gas-tank/hooks/useDepositHandler.ts @@ -9,6 +9,7 @@ import { nativeDeposit } from "./nativeDeposit"; import { relayLinkDeposit } from "./relayLinkDeposit"; import { useGasTankBalance } from "../../../useGasTankBalance"; import { usePrevious } from "../../../utils/usePrevious"; +import { usePaymaster } from "../../../usePaymaster"; export type StatusType = "pending" | "loading" | "loadingL2" | "success" | "error" | "idle"; @@ -69,7 +70,8 @@ function reducer(state: StateType, action: ActionType): StateType { export const useDepositHandler = (depositMethod: DepositMethod) => { const [state, dispatch] = useReducer(reducer, initialState); - const { chain, gasTankAddress } = useConfig(); + const { chain } = useConfig(); + const gasTank = usePaymaster("gasTank"); const wagmiConfig = useWagmiConfig(); const wallet = useWalletClient(); const userAccount = useAccount(); @@ -86,7 +88,7 @@ export const useDepositHandler = (depositMethod: DepositMethod) => { const deposit = useCallback( async (amount: string) => { - if (!wallet.data || !userAccountAddress || !userAccountChainId || !gasTankAddress || !amount) return; + if (!wallet.data || !userAccountAddress || !userAccountChainId || !amount || !gasTank) return; try { dispatch({ type: "SET_STATUS", payload: "pending" }); @@ -95,7 +97,7 @@ export const useDepositHandler = (depositMethod: DepositMethod) => { const txHash = await directDeposit({ config: wagmiConfig, chainId: chain.id, - gasTankAddress, + gasTankAddress: gasTank.address, userAccountAddress, amount, }); @@ -108,7 +110,7 @@ export const useDepositHandler = (depositMethod: DepositMethod) => { config: wagmiConfig, wallet, chainId: userAccountChainId, - gasTankAddress, + gasTankAddress: gasTank.address, userAccountAddress, amount, }); @@ -123,7 +125,7 @@ export const useDepositHandler = (depositMethod: DepositMethod) => { config: wagmiConfig, chainId: userAccountChainId, toChainId: chain.id, - gasTankAddress, + gasTankAddress: gasTank.address, wallet, amount, onProgress: (data1, data2, data3, data4, data5) => { @@ -141,7 +143,7 @@ export const useDepositHandler = (depositMethod: DepositMethod) => { dispatch({ type: "SET_ERROR", payload: error }); } }, - [userAccountAddress, depositMethod, wagmiConfig, chain.id, gasTankAddress, wallet, userAccountChainId], + [userAccountAddress, depositMethod, wagmiConfig, chain.id, gasTank, wallet, userAccountChainId], ); return useMemo(() => { diff --git a/packages/account-kit/src/steps/gas-tank/hooks/useEstimatedFees.ts b/packages/account-kit/src/steps/gas-tank/hooks/useEstimatedFees.ts deleted file mode 100644 index 0f4f6d748f..0000000000 --- a/packages/account-kit/src/steps/gas-tank/hooks/useEstimatedFees.ts +++ /dev/null @@ -1,29 +0,0 @@ -import GasTankAbi from "@latticexyz/gas-tank/out/IWorld.sol/IWorld.abi.json"; -import { encodeFunctionData, formatEther, parseEther } from "viem"; -import { useAccount, useEstimateGas, useGasPrice } from "wagmi"; -import { useConfig } from "../../../AccountKitProvider"; - -export const useEstimatedFees = () => { - const { chain, gasTankAddress } = useConfig(); - const userAccount = useAccount(); - const userAccountAddress = userAccount.address; - const gasPrice = useGasPrice(); - const estimateGas = useEstimateGas({ - chainId: chain.id, - to: gasTankAddress, - data: encodeFunctionData({ - abi: GasTankAbi, - functionName: "depositTo", - args: [userAccountAddress!], - }), - value: parseEther("0.01"), - }); - - let estimatedGasCost; - if (gasPrice?.data && estimateGas?.data) { - estimatedGasCost = formatEther(gasPrice.data * BigInt(1000) * estimateGas.data); - estimatedGasCost = parseFloat(estimatedGasCost).toLocaleString("en", { minimumFractionDigits: 5 }); - } - - return estimatedGasCost; -}; diff --git a/packages/account-kit/src/steps/gas-tank/hooks/useTransactionFees.ts b/packages/account-kit/src/steps/gas-tank/hooks/useTransactionFees.ts index 33287cbb2c..38ac033c4f 100644 --- a/packages/account-kit/src/steps/gas-tank/hooks/useTransactionFees.ts +++ b/packages/account-kit/src/steps/gas-tank/hooks/useTransactionFees.ts @@ -9,6 +9,7 @@ import { useConfig } from "../../../AccountKitProvider"; import { encodeFullNativeDeposit } from "./nativeDeposit"; import { OPTIMISM_PORTAL_ADDRESS } from "../constants"; import { fetchRelayLinkQuote } from "./relayLinkDeposit"; +import { usePaymaster } from "../../../usePaymaster"; const estimateDirectFee = async ({ config, @@ -80,7 +81,9 @@ const estimateNativeFee = async ({ export const useTransactionFees = (amount: string, depositMethod: DepositMethod) => { const [fees, setFees] = useState<string>(""); - const { chain, gasTankAddress } = useConfig(); + const { chain } = useConfig(); + const gasTank = usePaymaster("gasTank"); + if (!gasTank) throw new Error("No gas tank configured."); const wagmiConfig = useWagmiConfig(); const userAccount = useAccount(); const userAccountAddress = userAccount.address; @@ -93,7 +96,7 @@ export const useTransactionFees = (amount: string, depositMethod: DepositMethod) fees = await estimateDirectFee({ config: wagmiConfig, chainId: userAccountChainId, - gasTankAddress, + gasTankAddress: gasTank.address, userAccountAddress, }); @@ -102,7 +105,7 @@ export const useTransactionFees = (amount: string, depositMethod: DepositMethod) fees = await estimateNativeFee({ config: wagmiConfig, chainId: userAccountChainId, - gasTankAddress, + gasTankAddress: gasTank.address, userAccountAddress, }); @@ -120,7 +123,7 @@ export const useTransactionFees = (amount: string, depositMethod: DepositMethod) }; fetchFees(); - }, [amount, chain, depositMethod, gasTankAddress, userAccountAddress, userAccountChainId, wagmiConfig]); + }, [amount, chain, depositMethod, gasTank, userAccountAddress, userAccountChainId, wagmiConfig]); return { fees, diff --git a/packages/account-kit/src/useAppAccountClient.ts b/packages/account-kit/src/useAppAccountClient.ts index 528d304a64..b218ce120c 100644 --- a/packages/account-kit/src/useAppAccountClient.ts +++ b/packages/account-kit/src/useAppAccountClient.ts @@ -1,6 +1,6 @@ import { useEffect, useMemo } from "react"; import { useAccount, usePublicClient } from "wagmi"; -import { http, maxUint256, toHex, publicActions, createClient, ClientConfig } from "viem"; +import { maxUint256, toHex, publicActions, createClient } from "viem"; import { callFrom } from "@latticexyz/world/internal"; import { SmartAccountClientConfig, smartAccountActions } from "permissionless"; import { createPimlicoBundlerClient } from "permissionless/clients/pimlico"; @@ -12,10 +12,16 @@ import { getUserBalanceSlot } from "./utils/getUserBalanceSlot"; import { getEntryPointDepositSlot } from "./utils/getEntryPointDepositSlot"; import { transportObserver } from "./transportObserver"; import { ENTRYPOINT_ADDRESS_V07_TYPE } from "permissionless/types/entrypoint"; +import { useErc4337Config } from "./useErc4337Config"; +import { usePaymaster } from "./usePaymaster"; + +type Middleware = SmartAccountClientConfig<ENTRYPOINT_ADDRESS_V07_TYPE>["middleware"]; export function useAppAccountClient(): AppAccountClient | undefined { const [appSignerAccount] = useAppSigner(); - const { chain, worldAddress, gasTankAddress } = useConfig(); + const { chain, worldAddress } = useConfig(); + const erc4337Config = useErc4337Config(); + const gasTank = usePaymaster("gasTank"); const { address: userAddress } = useAccount(); const publicClient = usePublicClient({ chainId: chain.id }); const { data: appAccount, error: appAccountError } = useAppAccount({ publicClient, appSignerAccount }); @@ -36,63 +42,67 @@ export function useAppAccountClient(): AppAccountClient | undefined { if (!publicClient) return; if (!appAccount) return; - if (!chain.erc4337BundlerUrl) { - throw new Error(`No ERC4337 bundler URL found for chain ${chain.name} (id: ${chain.id})`); + // TODO: return a different client if we're not using ERC-4337 + if (!erc4337Config) { + throw new Error("No ERC-4337 config was found."); } const pimlicoBundlerClient = createPimlicoBundlerClient({ chain: publicClient.chain, - transport: transportObserver("pimlico bundler client", http(chain.erc4337BundlerUrl.http)), + transport: transportObserver("pimlico bundler client", erc4337Config.transport), entryPoint: entryPointAddress, }).extend(() => publicActions(publicClient)); - const smartAccountClientConfig = { - key: "Account", - name: "Smart Account Client", - type: "smartAccountClient", - chain: publicClient.chain, - account: appAccount, - transport: transportObserver("bundler transport", http(chain.erc4337BundlerUrl.http)), - middleware: { - sponsorUserOperation: async ({ userOperation }) => { - const gasEstimates = await pimlicoBundlerClient.estimateUserOperationGas( - { - userOperation: { - ...userOperation, - paymaster: gasTankAddress, - paymasterData: "0x", - }, - }, - { - // Pimlico's gas estimation runs with high gas limits, which can make the estimation fail if - // the cost would exceed the user's balance. - // We override the user's balance in the paymaster contract and the deposit balance of the - // paymaster in the entry point contract to make the gas estimation succeed. - [gasTankAddress]: { - stateDiff: { - [getUserBalanceSlot(userAddress)]: toHex(maxUint256), + const baseMiddleware = { + gasPrice: async () => (await pimlicoBundlerClient.getUserOperationGasPrice()).fast, // use pimlico bundler to get gas prices + } satisfies Middleware; + + const gasTankMiddleware = gasTank + ? ({ + sponsorUserOperation: async ({ userOperation }) => { + const gasEstimates = await pimlicoBundlerClient.estimateUserOperationGas( + { + userOperation: { + ...userOperation, + paymaster: gasTank.address, + paymasterData: "0x", }, }, - [entryPointAddress]: { - stateDiff: { - [getEntryPointDepositSlot(gasTankAddress)]: toHex(maxUint256), + { + // Pimlico's gas estimation runs with high gas limits, which can make the estimation fail if + // the cost would exceed the user's balance. + // We override the user's balance in the paymaster contract and the deposit balance of the + // paymaster in the entry point contract to make the gas estimation succeed. + [gasTank.address]: { + stateDiff: { + [getUserBalanceSlot(userAddress)]: toHex(maxUint256), + }, + }, + [entryPointAddress]: { + stateDiff: { + [getEntryPointDepositSlot(gasTank.address)]: toHex(maxUint256), + }, }, }, - }, - ); + ); - return { - paymasterData: "0x", - paymaster: gasTankAddress, - ...gasEstimates, - }; - }, - gasPrice: async () => (await pimlicoBundlerClient.getUserOperationGasPrice()).fast, // use pimlico bundler to get gas prices - }, - } as const satisfies Omit<SmartAccountClientConfig<ENTRYPOINT_ADDRESS_V07_TYPE>, "bundlerTransport"> & - Pick<ClientConfig, "type" | "transport">; + return { + paymasterData: "0x", + paymaster: gasTank.address, + ...gasEstimates, + }; + }, + } satisfies Middleware) + : null; - const appAccountClient = createClient(smartAccountClientConfig) + const appAccountClient = createClient({ + key: "Account", + name: "Smart Account Client", + type: "smartAccountClient", + chain: publicClient.chain, + account: appAccount, + transport: transportObserver("bundler transport", erc4337Config.transport), + }) .extend( callFrom({ worldAddress, @@ -100,11 +110,18 @@ export function useAppAccountClient(): AppAccountClient | undefined { publicClient, }), ) - .extend(smartAccountActions({ middleware: smartAccountClientConfig.middleware })) + .extend( + smartAccountActions({ + middleware: { + ...baseMiddleware, + ...gasTankMiddleware, + }, + }), + ) // .extend(transactionQueue(publicClient)) // .extend(writeObserver({ onWrite: (write) => write$.next(write) })) .extend(() => publicActions(publicClient)); return appAccountClient; - }, [appSignerAccount, userAddress, publicClient, appAccount, worldAddress, gasTankAddress, chain]); + }, [appSignerAccount, userAddress, publicClient, appAccount, erc4337Config, gasTank, worldAddress]); } diff --git a/packages/account-kit/src/useErc4337Config.ts b/packages/account-kit/src/useErc4337Config.ts new file mode 100644 index 0000000000..d93f086462 --- /dev/null +++ b/packages/account-kit/src/useErc4337Config.ts @@ -0,0 +1,34 @@ +import { http } from "wagmi"; +import { assert } from "@latticexyz/common/utils"; +import { Erc4337Config } from "./config"; +import { useConfig } from "./AccountKitProvider"; + +export function useErc4337Config(): Erc4337Config | undefined { + const config = useConfig(); + + // TODO: do bundler transport health check? + // TODO: check if paymaster contracts exist? + + if (config.erc4337 === false) return undefined; + if (config.erc4337 != null) return config.erc4337; + + return { + transport: http( + assert( + config.chain.rpcUrls.erc4337Bundler?.http[0], + // eslint-disable-next-line max-len + "Account Kit was not configured with `erc4337` and is attempting to set up a default transport, but did not find an `erc4337Bundler.http` URL on `chain.rpcUrls`.\n\nYou can either add that to your chain config or disable ERC-4337 with `erc4337: false`.", + ), + ), + paymasters: [ + { + type: "gasTank", + address: assert( + config.chain.contracts?.gasTank?.address, + // eslint-disable-next-line max-len + "Account Kit was not configured with `erc4337` and is attempting to set up a default paymaster, but did not find a `gasTank` contract on `chain.contracts`.\n\nYou can either add that to your chain config or disable ERC-4337 with `erc4337: false`.", + ), + }, + ], + }; +} diff --git a/packages/account-kit/src/useGasTankBalance.ts b/packages/account-kit/src/useGasTankBalance.ts index 04931eedc7..32acdd8daa 100644 --- a/packages/account-kit/src/useGasTankBalance.ts +++ b/packages/account-kit/src/useGasTankBalance.ts @@ -2,17 +2,19 @@ import { useAccount } from "wagmi"; import { useConfig } from "./AccountKitProvider"; import gasTankConfig from "@latticexyz/gas-tank/mud.config"; import { useRecord } from "./useRecord"; +import { usePaymaster } from "./usePaymaster"; export function useGasTankBalance() { - const { chain, gasTankAddress } = useConfig(); + const { chain } = useConfig(); + const gasTank = usePaymaster("gasTank"); const userAccount = useAccount(); const userAccountAddress = userAccount.address; const result = useRecord( - userAccountAddress + userAccountAddress && gasTank ? { chainId: chain.id, - address: gasTankAddress, + address: gasTank.address, table: gasTankConfig.tables.UserBalances, key: { userAccount: userAccountAddress }, blockTag: "pending", diff --git a/packages/account-kit/src/useIsGasSpender.ts b/packages/account-kit/src/useIsGasSpender.ts index ae67dd1789..e7207e9d9a 100644 --- a/packages/account-kit/src/useIsGasSpender.ts +++ b/packages/account-kit/src/useIsGasSpender.ts @@ -4,9 +4,11 @@ import gasTankConfig from "@latticexyz/gas-tank/mud.config"; import { useAppSigner } from "./useAppSigner"; import { useAppAccount } from "./useAppAccount"; import { useRecord } from "./useRecord"; +import { usePaymaster } from "./usePaymaster"; export function useIsGasSpender() { - const { chain, gasTankAddress } = useConfig(); + const { chain } = useConfig(); + const gasTank = usePaymaster("gasTank"); const userAccount = useAccount(); const userAccountAddress = userAccount.address; @@ -17,10 +19,10 @@ export function useIsGasSpender() { const appAccountAddress = appAccount.data?.address; const result = useRecord( - appAccountAddress + appAccountAddress && gasTank ? { chainId: chain.id, - address: gasTankAddress, + address: gasTank.address, table: gasTankConfig.tables.Spender, key: { spender: appAccountAddress }, blockTag: "pending", diff --git a/packages/account-kit/src/usePaymaster.ts b/packages/account-kit/src/usePaymaster.ts new file mode 100644 index 0000000000..98cbced872 --- /dev/null +++ b/packages/account-kit/src/usePaymaster.ts @@ -0,0 +1,7 @@ +import { Paymaster, PaymasterType } from "./config"; +import { useErc4337Config } from "./useErc4337Config"; + +export function usePaymaster(type: PaymasterType): Paymaster | undefined { + const config = useErc4337Config(); + return config?.paymasters.find((paymaster) => paymaster.type === type); +} diff --git a/packages/common/src/chains/mudFoundry.ts b/packages/common/src/chains/mudFoundry.ts index 9a607ae2ec..ac62511abc 100644 --- a/packages/common/src/chains/mudFoundry.ts +++ b/packages/common/src/chains/mudFoundry.ts @@ -3,12 +3,14 @@ import { MUDChain } from "./types"; export const mudFoundry = { ...foundry, - erc4337BundlerUrl: { - http: "http://127.0.0.1:4337", - }, fees: { - // This is intentionally defined as a function as a workaround for https://github.com/wagmi-dev/viem/pull/1280 - defaultPriorityFee: () => 0n, + defaultPriorityFee: 0n, + }, + rpcUrls: { + ...foundry.rpcUrls, + erc4337Bundler: { + http: ["http://127.0.0.1:4337"], + }, }, contracts: { ...foundry.contracts, diff --git a/packages/common/src/chains/types.ts b/packages/common/src/chains/types.ts index c83fcc3762..7917c62cd2 100644 --- a/packages/common/src/chains/types.ts +++ b/packages/common/src/chains/types.ts @@ -1,20 +1,18 @@ import { ChainContract } from "viem"; import type { Chain } from "viem/chains"; +// TODO: import from viem once available +export type RpcUrls = { + http: readonly [string, ...string[]]; + webSocket?: readonly [string, ...string[]] | undefined; +}; + export type MUDChain = Chain & { faucetUrl?: string; - erc4337BundlerUrl?: { - http: string; - webSocket?: string; - }; rpcUrls?: Chain["rpcUrls"] & { - // TODO: replace with ChainRpcURLs from viem once exported - readonly erc4337Bundler?: { - http: readonly string[]; - webSocket?: readonly string[] | undefined; - }; + erc4337Bundler?: RpcUrls | undefined; }; contracts?: Chain["contracts"] & { - readonly gasTank?: ChainContract | undefined; + gasTank?: ChainContract | undefined; }; }; diff --git a/packages/common/src/utils/assert.ts b/packages/common/src/utils/assert.ts new file mode 100644 index 0000000000..59757e62cd --- /dev/null +++ b/packages/common/src/utils/assert.ts @@ -0,0 +1,6 @@ +export function assert<T>(value: T | undefined, message: string): T { + if (typeof value === "undefined") { + throw new Error(message); + } + return value; +} diff --git a/packages/common/src/utils/index.ts b/packages/common/src/utils/index.ts index 29bcee4e25..bb5344114a 100644 --- a/packages/common/src/utils/index.ts +++ b/packages/common/src/utils/index.ts @@ -1,3 +1,4 @@ +export * from "./assert"; export * from "./assertExhaustive"; export * from "./bigIntMax"; export * from "./bigIntMin";