diff --git a/packages/account-kit/src/LoginButton.tsx b/packages/account-kit/src/AccountButton.tsx similarity index 54% rename from packages/account-kit/src/LoginButton.tsx rename to packages/account-kit/src/AccountButton.tsx index 573228a962..c55a3f5374 100644 --- a/packages/account-kit/src/LoginButton.tsx +++ b/packages/account-kit/src/AccountButton.tsx @@ -1,14 +1,15 @@ import { Button } from "./ui/Button"; -import { useLoginDialog } from "./useLoginDialog"; -import { LoginDialog } from "./LoginDialog"; -import { useLoginRequirements } from "./useLoginRequirements"; +import { useAccountModal } from "./useAccountModal"; +import { AccountModal } from "./AccountModal"; +import { useAccountRequirements } from "./useAccountRequirements"; import { Shadow } from "./Shadow"; const buttonClassName = "w-48"; -export function LoginButton() { - const { requirement } = useLoginRequirements(); - const { openConnectModal, connectPending, openLoginDialog, toggleLoginDialog, loginDialogOpen } = useLoginDialog(); +export function AccountButton() { + const { requirement } = useAccountRequirements(); + const { openConnectModal, connectPending, openAccountModal, toggleAccountModal, accountModalOpen } = + useAccountModal(); if (requirement === "connectedWallet") { return ( @@ -18,7 +19,7 @@ export function LoginButton() { pending={connectPending} onClick={() => { openConnectModal?.(); - openLoginDialog(); + openAccountModal(); }} > Connect wallet @@ -31,11 +32,11 @@ export function LoginButton() { return ( <> - - + ); } diff --git a/packages/account-kit/src/AccountDelegationDialogContent.tsx b/packages/account-kit/src/AccountDelegationDialogContent.tsx index 3aba42aed5..7f847611a5 100644 --- a/packages/account-kit/src/AccountDelegationDialogContent.tsx +++ b/packages/account-kit/src/AccountDelegationDialogContent.tsx @@ -1,7 +1,7 @@ import * as Dialog from "@radix-ui/react-dialog"; import { useAppAccountClient } from "./useAppAccountClient"; import { usePublicClient, useWalletClient } from "wagmi"; -import { useLoginConfig } from "./Context"; +import { useConfig } from "./MUDAccountKitProvider"; import { encodeFunctionData } from "viem"; import { waitForTransactionReceipt } from "viem/actions"; import IBaseWorldAbi from "@latticexyz/world/out/IBaseWorld.sol/IBaseWorld.abi.json"; @@ -15,7 +15,7 @@ import { ModalContent } from "./ui/ModalContent"; export function AccountDelegationDialogContent() { const queryClient = useQueryClient(); - const { chainId, worldAddress } = useLoginConfig(); + const { chainId, worldAddress } = useConfig(); const publicClient = usePublicClient({ chainId }); const { data: userAccountClient } = useWalletClient({ chainId }); const appAccountClient = useAppAccountClient(); diff --git a/packages/account-kit/src/LoginDialog.tsx b/packages/account-kit/src/AccountModal.tsx similarity index 88% rename from packages/account-kit/src/LoginDialog.tsx rename to packages/account-kit/src/AccountModal.tsx index 7d244a3938..144a7535f8 100644 --- a/packages/account-kit/src/LoginDialog.tsx +++ b/packages/account-kit/src/AccountModal.tsx @@ -3,16 +3,16 @@ import { assertExhaustive } from "@latticexyz/common/utils"; import { AppSignerDialogContent } from "./AppSignerDialogContent"; import { GasAllowanceDialogContent } from "./GasAllowanceDialogContent"; import { AccountDelegationDialogContent } from "./AccountDelegationDialogContent"; -import { LoginRequirement } from "./common"; import { GasSpenderDialogContent } from "./GasSpenderDialogContent"; import { ConnectedChainDialogContent } from "./ConnectedChainDialogContent"; import { Modal, Props as ModalProps } from "./ui/Modal"; +import { AccountRequirement } from "./useAccountRequirements"; export type Props = Pick & { - requirement: LoginRequirement; + requirement: AccountRequirement; }; -export function LoginDialog({ requirement, ...dialogProps }: Props) { +export function AccountModal({ requirement, ...dialogProps }: Props) { const content = useMemo(() => { switch (requirement) { case "connectedWallet": diff --git a/packages/account-kit/src/ConnectedChainDialogContent.tsx b/packages/account-kit/src/ConnectedChainDialogContent.tsx index 79deb64f45..0d5e3f25dd 100644 --- a/packages/account-kit/src/ConnectedChainDialogContent.tsx +++ b/packages/account-kit/src/ConnectedChainDialogContent.tsx @@ -1,11 +1,11 @@ import * as Dialog from "@radix-ui/react-dialog"; import { useSwitchChain } from "wagmi"; -import { useLoginConfig } from "./Context"; +import { useConfig } from "./MUDAccountKitProvider"; import { Button } from "./ui/Button"; import { ModalContent } from "./ui/ModalContent"; export function ConnectedChainDialogContent() { - const { chainId } = useLoginConfig(); + const { chainId } = useConfig(); const { switchChain, isPending, error } = useSwitchChain(); // TODO: prompt user to add chain if missing diff --git a/packages/account-kit/src/Context.tsx b/packages/account-kit/src/Context.tsx deleted file mode 100644 index 7a9b559a18..0000000000 --- a/packages/account-kit/src/Context.tsx +++ /dev/null @@ -1,22 +0,0 @@ -import { createContext, useContext, type ReactNode } from "react"; -import { MUDLoginConfig } from "./common"; - -/** @internal */ -const Context = createContext(null); - -export type Props = { - config: MUDLoginConfig; - children: ReactNode; -}; - -export function MUDLoginProvider({ config, children }: Props) { - const currentConfig = useContext(Context); - if (currentConfig) throw new Error("`MUDLoginProvider` can only be used once."); - return {children}; -} - -export function useLoginConfig(): MUDLoginConfig { - const config = useContext(Context); - if (!config) throw new Error("`useLoginConfig` be used within a `MUDLoginProvider`."); - return config; -} diff --git a/packages/account-kit/src/GasAllowanceDialogContent.tsx b/packages/account-kit/src/GasAllowanceDialogContent.tsx index aa8021fabc..c2b0be8fd9 100644 --- a/packages/account-kit/src/GasAllowanceDialogContent.tsx +++ b/packages/account-kit/src/GasAllowanceDialogContent.tsx @@ -1,6 +1,6 @@ import { parseEther } from "viem"; -import { useAccount, useConfig, useWriteContract } from "wagmi"; -import { useLoginConfig } from "./Context"; +import { useAccount, useConfig as useWagmiConfig, useWriteContract } from "wagmi"; +import { useConfig } from "./MUDAccountKitProvider"; import GasTankAbi from "@latticexyz/gas-tank/out/IWorld.sol/IWorld.abi.json"; import { getGasTankBalanceQueryKey } from "./useGasTankBalance"; import { waitForTransactionReceipt } from "wagmi/actions"; @@ -10,8 +10,8 @@ import { ModalContent } from "./ui/ModalContent"; export function GasAllowanceDialogContent() { const queryClient = useQueryClient(); - const wagmiConfig = useConfig(); - const { chainId, gasTankAddress } = useLoginConfig(); + const wagmiConfig = useWagmiConfig(); + const { chainId, gasTankAddress } = useConfig(); const userAccount = useAccount(); const userAccountAddress = userAccount.address; const { writeContractAsync, isPending, error } = useWriteContract({ diff --git a/packages/account-kit/src/GasSpenderDialogContent.tsx b/packages/account-kit/src/GasSpenderDialogContent.tsx index 4ed6fa60ae..439e8fee4d 100644 --- a/packages/account-kit/src/GasSpenderDialogContent.tsx +++ b/packages/account-kit/src/GasSpenderDialogContent.tsx @@ -1,7 +1,7 @@ import * as Dialog from "@radix-ui/react-dialog"; import { useAppAccountClient } from "./useAppAccountClient"; import { usePublicClient, useWalletClient } from "wagmi"; -import { useLoginConfig } from "./Context"; +import { useConfig } from "./MUDAccountKitProvider"; import { encodeFunctionData } from "viem"; import { waitForTransactionReceipt } from "viem/actions"; import { resourceToHex } from "@latticexyz/common"; @@ -14,7 +14,7 @@ import { ModalContent } from "./ui/ModalContent"; export function GasSpenderDialogContent() { const queryClient = useQueryClient(); - const { chainId, gasTankAddress } = useLoginConfig(); + const { chainId, gasTankAddress } = useConfig(); const publicClient = usePublicClient({ chainId }); const { data: userAccountClient } = useWalletClient({ chainId }); const appAccountClient = useAppAccountClient(); diff --git a/packages/account-kit/src/MUDAccountKitProvider.tsx b/packages/account-kit/src/MUDAccountKitProvider.tsx new file mode 100644 index 0000000000..aaaffd32d1 --- /dev/null +++ b/packages/account-kit/src/MUDAccountKitProvider.tsx @@ -0,0 +1,28 @@ +import { createContext, useContext, type ReactNode } from "react"; +import { Address } from "viem"; + +export type Config = { + chainId: number; + worldAddress: Address; + gasTankAddress: Address; +}; + +/** @internal */ +const Context = createContext(null); + +export type Props = { + config: Config; + children: ReactNode; +}; + +export function MUDAccountKitProvider({ config, children }: Props) { + const currentConfig = useContext(Context); + if (currentConfig) throw new Error("`MUDAccountKitProvider` can only be used once."); + return {children}; +} + +export function useConfig(): Config { + const config = useContext(Context); + if (!config) throw new Error("`useConfig` be used within a `MUDAccountKitProvider`."); + return config; +} diff --git a/packages/account-kit/src/common.ts b/packages/account-kit/src/common.ts index 973527b0a4..310bb69f42 100644 --- a/packages/account-kit/src/common.ts +++ b/packages/account-kit/src/common.ts @@ -1,7 +1,7 @@ import { resourceToHex } from "@latticexyz/common"; import { ENTRYPOINT_ADDRESS_V07, SmartAccountClient } from "permissionless"; import { SmartAccount } from "permissionless/accounts"; -import { Address, Chain, Transport } from "viem"; +import { Chain, Transport } from "viem"; export const entryPointAddress = ENTRYPOINT_ADDRESS_V07; /** @@ -15,21 +15,4 @@ export type AppAccountClient = SmartAccountClient ({ open: false })); + +export type UseAccountModalResult = { + readonly openConnectModal: (() => void) | undefined; + readonly connectPending: boolean; + readonly accountModalOpen: boolean; + readonly openAccountModal: () => void; + readonly closeAccountModal: () => void; + readonly toggleAccountModal: (open: boolean) => void; +}; + +export function useAccountModal(): UseAccountModalResult { + const { openConnectModal, connectModalOpen } = useConnectModal(); + const connectPending = !openConnectModal || connectModalOpen; + + const accountModalOpen = useStore(store, (state) => state.open); + + const openAccountModal = useCallback(() => { + store.setState({ open: true }); + }, []); + + const closeAccountModal = useCallback(() => { + store.setState({ open: false }); + }, []); + + const toggleAccountModal = useCallback((open: boolean) => { + store.setState({ open }); + }, []); + + return useMemo( + () => ({ + openConnectModal, + connectPending, + accountModalOpen, + openAccountModal, + closeAccountModal, + toggleAccountModal, + }), + [closeAccountModal, connectPending, accountModalOpen, openConnectModal, openAccountModal, toggleAccountModal], + ); +} diff --git a/packages/account-kit/src/useLoginRequirements.ts b/packages/account-kit/src/useAccountRequirements.ts similarity index 60% rename from packages/account-kit/src/useLoginRequirements.ts rename to packages/account-kit/src/useAccountRequirements.ts index 366dea25e0..1242aaa1cc 100644 --- a/packages/account-kit/src/useLoginRequirements.ts +++ b/packages/account-kit/src/useAccountRequirements.ts @@ -1,19 +1,29 @@ import { useAccount } from "wagmi"; -import { LoginRequirement, loginRequirements } from "./common"; import { useAppSigner } from "./useAppSigner"; import { useHasDelegation } from "./useHasDelegation"; import { useMemo } from "react"; import { useGasTankBalance } from "./useGasTankBalance"; import { useIsGasSpender } from "./useIsGasSpender"; -import { useLoginConfig } from "./Context"; +import { useConfig } from "./MUDAccountKitProvider"; -export type UseLoginRequirementsResult = { - readonly requirement: LoginRequirement | null; - readonly requirements: readonly LoginRequirement[]; +export const accountRequirements = [ + "connectedWallet", + "connectedChain", + "appSigner", + "gasAllowance", + "gasSpender", + "accountDelegation", +] as const; + +export type AccountRequirement = (typeof accountRequirements)[number]; + +export type UseAccountRequirementsResult = { + readonly requirement: AccountRequirement | null; + readonly requirements: readonly AccountRequirement[]; }; -export function useLoginRequirements(): UseLoginRequirementsResult { - const { chainId } = useLoginConfig(); +export function useAccountRequirements(): UseAccountRequirementsResult { + const { chainId } = useConfig(); const userAccount = useAccount(); const [appSignerAccount] = useAppSigner(); @@ -29,9 +39,9 @@ export function useLoginRequirements(): UseLoginRequirementsResult { gasAllowance: () => gasTankBalance != null && gasTankBalance > 0n, gasSpender: () => isGasSpender === true, accountDelegation: () => hasDelegation === true, - } as const satisfies Record boolean>; + } as const satisfies Record boolean>; - const requirements = loginRequirements.filter((requirement) => !satisfiesRequirement[requirement]()); + const requirements = accountRequirements.filter((requirement) => !satisfiesRequirement[requirement]()); return { requirement: requirements.at(0) ?? null, diff --git a/packages/account-kit/src/useAppAccountClient.ts b/packages/account-kit/src/useAppAccountClient.ts index 44cf708d72..fb23baeb89 100644 --- a/packages/account-kit/src/useAppAccountClient.ts +++ b/packages/account-kit/src/useAppAccountClient.ts @@ -5,16 +5,16 @@ import { callFrom } from "@latticexyz/world/internal"; import { createSmartAccountClient } from "permissionless"; import { createPimlicoBundlerClient } from "permissionless/clients/pimlico"; import { call, getTransactionCount } from "viem/actions"; -import { useLoginConfig } from "./Context"; +import { useConfig } from "./MUDAccountKitProvider"; import { useAppSigner } from "./useAppSigner"; import { useAppAccount } from "./useAppAccount"; import { AppAccountClient, entryPointAddress } from "./common"; import { getUserBalanceSlot } from "./utils/getUserBalanceSlot"; -import { getEntryPointDepositSlot } from "./getEntryPointDepositSlot"; +import { getEntryPointDepositSlot } from "./utils/getEntryPointDepositSlot"; export function useAppAccountClient(): AppAccountClient | undefined { const [appSignerAccount] = useAppSigner(); - const { chainId, worldAddress, gasTankAddress } = useLoginConfig(); + const { chainId, worldAddress, gasTankAddress } = useConfig(); const { address: userAddress } = useAccount(); const publicClient = usePublicClient({ chainId }); const { data: appAccount } = useAppAccount({ publicClient, appSignerAccount }); diff --git a/packages/account-kit/src/useGasTankBalance.ts b/packages/account-kit/src/useGasTankBalance.ts index f9da33503e..2428ecf7dd 100644 --- a/packages/account-kit/src/useGasTankBalance.ts +++ b/packages/account-kit/src/useGasTankBalance.ts @@ -1,5 +1,5 @@ import { useAccount, usePublicClient } from "wagmi"; -import { useLoginConfig } from "./Context"; +import { useConfig } from "./MUDAccountKitProvider"; import { useQuery } from "@tanstack/react-query"; import { getRecord } from "./utils/getRecord"; import { Address } from "abitype"; @@ -37,7 +37,7 @@ export async function getGasTankBalance({ } export function useGasTankBalance(): bigint | undefined { - const { chainId, gasTankAddress } = useLoginConfig(); + const { chainId, gasTankAddress } = useConfig(); const publicClient = usePublicClient({ chainId }); const userAccount = useAccount(); diff --git a/packages/account-kit/src/useHasDelegation.ts b/packages/account-kit/src/useHasDelegation.ts index da1074cdee..7e176d4df9 100644 --- a/packages/account-kit/src/useHasDelegation.ts +++ b/packages/account-kit/src/useHasDelegation.ts @@ -1,5 +1,5 @@ import { useAccount, usePublicClient } from "wagmi"; -import { useLoginConfig } from "./Context"; +import { useConfig } from "./MUDAccountKitProvider"; import { useQuery } from "@tanstack/react-query"; import { useAppAccount } from "./useAppAccount"; import { useAppSigner } from "./useAppSigner"; @@ -44,7 +44,7 @@ export async function hasDelegation({ } export function useHasDelegation(): boolean | undefined { - const { chainId, worldAddress } = useLoginConfig(); + const { chainId, worldAddress } = useConfig(); const publicClient = usePublicClient({ chainId }); const userAccount = useAccount(); const [appSignerAccount] = useAppSigner(); diff --git a/packages/account-kit/src/useIsGasSpender.ts b/packages/account-kit/src/useIsGasSpender.ts index 40132489ee..74ded3425f 100644 --- a/packages/account-kit/src/useIsGasSpender.ts +++ b/packages/account-kit/src/useIsGasSpender.ts @@ -1,5 +1,5 @@ import { useAccount, usePublicClient } from "wagmi"; -import { useLoginConfig } from "./Context"; +import { useConfig } from "./MUDAccountKitProvider"; import { useQuery } from "@tanstack/react-query"; import { getRecord } from "./utils/getRecord"; import { Address } from "abitype"; @@ -42,7 +42,7 @@ export async function isGasSpender({ } export function useIsGasSpender(): boolean | undefined { - const { chainId, gasTankAddress } = useLoginConfig(); + const { chainId, gasTankAddress } = useConfig(); const publicClient = usePublicClient({ chainId }); const userAccount = useAccount(); diff --git a/packages/account-kit/src/useLoginDialog.ts b/packages/account-kit/src/useLoginDialog.ts deleted file mode 100644 index e3e2b6ef6d..0000000000 --- a/packages/account-kit/src/useLoginDialog.ts +++ /dev/null @@ -1,46 +0,0 @@ -import { useConnectModal } from "@rainbow-me/rainbowkit"; -import { useCallback, useMemo } from "react"; -import { useStore } from "zustand"; -import { createStore } from "zustand/vanilla"; - -const store = createStore(() => ({ open: false })); - -export type UseLoginDialogResult = { - readonly openConnectModal: (() => void) | undefined; - readonly connectPending: boolean; - readonly loginDialogOpen: boolean; - readonly openLoginDialog: () => void; - readonly closeLoginDialog: () => void; - readonly toggleLoginDialog: (open: boolean) => void; -}; - -export function useLoginDialog(): UseLoginDialogResult { - const { openConnectModal, connectModalOpen } = useConnectModal(); - const connectPending = !openConnectModal || connectModalOpen; - - const loginDialogOpen = useStore(store, (state) => state.open); - - const openLoginDialog = useCallback(() => { - store.setState({ open: true }); - }, []); - - const closeLoginDialog = useCallback(() => { - store.setState({ open: false }); - }, []); - - const toggleLoginDialog = useCallback((open: boolean) => { - store.setState({ open }); - }, []); - - return useMemo( - () => ({ - openConnectModal, - connectPending, - loginDialogOpen, - openLoginDialog, - closeLoginDialog, - toggleLoginDialog, - }), - [closeLoginDialog, connectPending, loginDialogOpen, openConnectModal, openLoginDialog, toggleLoginDialog], - ); -} diff --git a/packages/account-kit/src/getEntryPointDepositSlot.ts b/packages/account-kit/src/utils/getEntryPointDepositSlot.ts similarity index 73% rename from packages/account-kit/src/getEntryPointDepositSlot.ts rename to packages/account-kit/src/utils/getEntryPointDepositSlot.ts index 123bf27410..00738e2917 100644 --- a/packages/account-kit/src/getEntryPointDepositSlot.ts +++ b/packages/account-kit/src/utils/getEntryPointDepositSlot.ts @@ -1,5 +1,7 @@ import { Address, Hex, encodeAbiParameters, keccak256 } from "viem"; -import { entryPointDepositsSlot } from "./common"; +import { entryPointDepositsSlot } from "../common"; + +// TODO: move this to gas-tank package or similar export function getEntryPointDepositSlot(gasTankAddress: Address): Hex { return keccak256(