diff --git a/packages/common/src/chains/mudFoundry.ts b/packages/common/src/chains/mudFoundry.ts index 1754534d8d..e971901277 100644 --- a/packages/common/src/chains/mudFoundry.ts +++ b/packages/common/src/chains/mudFoundry.ts @@ -3,7 +3,7 @@ import { MUDChain } from "./types"; export const mudFoundry = { ...foundry, - fees: { - defaultPriorityFee: 0n, - }, + // fees: { + // defaultPriorityFee: 0n, + // }, } as const satisfies MUDChain; diff --git a/packages/common/src/createContract.ts b/packages/common/src/createContract.ts index c327709352..46cc0ed236 100644 --- a/packages/common/src/createContract.ts +++ b/packages/common/src/createContract.ts @@ -15,7 +15,6 @@ import { getAbiItem, getContract, getFunctionSelector, - trim, } from "viem"; import pRetry from "p-retry"; import { AbiFunction } from "abitype"; diff --git a/templates/react/packages/client/package.json b/templates/react/packages/client/package.json index 1855fbf09e..831345ae19 100644 --- a/templates/react/packages/client/package.json +++ b/templates/react/packages/client/package.json @@ -20,11 +20,13 @@ "@latticexyz/store-sync": "link:../../../../packages/store-sync", "@latticexyz/utils": "link:../../../../packages/utils", "@latticexyz/world": "link:../../../../packages/world", + "@rainbow-me/rainbowkit": "^1.0.11", "contracts": "workspace:*", "react": "^18.2.0", "react-dom": "^18.2.0", "rxjs": "7.5.5", - "viem": "1.6.0" + "viem": "1.6.0", + "wagmi": "^1.4.2" }, "devDependencies": { "@types/react": "18.2.22", diff --git a/templates/react/packages/client/src/App.tsx b/templates/react/packages/client/src/App.tsx index a9660de463..a025d67b3e 100644 --- a/templates/react/packages/client/src/App.tsx +++ b/templates/react/packages/client/src/App.tsx @@ -1,13 +1,28 @@ +import "@rainbow-me/rainbowkit/styles.css"; +import { ConnectButton } from "@rainbow-me/rainbowkit"; +import { useWalletClient, useAccount } from "wagmi"; import { useComponentValue } from "@latticexyz/react"; import { useMUD } from "./MUDContext"; +import { setup } from "./mud/setup"; import { singletonEntity } from "@latticexyz/store-sync/recs"; +import { useSystemCalls } from "./hooks/useSystemCalls"; +import { useDelegationControl } from "./hooks/useDelegationControl"; -export const App = () => { - const { - components: { Counter }, - systemCalls: { increment }, - } = useMUD(); +type Props = { + setup: Awaited>; +}; + +export const App = ({ setup }: Props) => { + const { network, components } = setup; + + // TODO rename to delegatee + const { walletClient: burnerWalletClient } = network; + const { Counter } = components; + const walletClient = useWalletClient(); + const account = useAccount(); + const systemCalls = useSystemCalls(network, components, walletClient.data); + const delegationControlId = useDelegationControl(walletClient.data, burnerWalletClient, components); const counter = useComponentValue(Counter, singletonEntity); return ( @@ -19,11 +34,24 @@ export const App = () => { type="button" onClick={async (event) => { event.preventDefault(); - console.log("new counter value:", await increment()); + console.log("new counter value:", await systemCalls?.increment()); }} > Increment + + {account.isConnected && !delegationControlId ? ( + + ) : null} ); }; diff --git a/templates/react/packages/client/src/MUDContext.tsx b/templates/react/packages/client/src/MUDContext.tsx index 7b5637f6a6..6568ef6312 100644 --- a/templates/react/packages/client/src/MUDContext.tsx +++ b/templates/react/packages/client/src/MUDContext.tsx @@ -1,21 +1,42 @@ -import { createContext, ReactNode, useContext } from "react"; -import { SetupResult } from "./mud/setup"; +import { createContext, ReactNode, useContext, useState, useEffect, useMemo } from "react"; +import { setup, SetupResult } from "./mud/setup"; +import { usePromise } from "@latticexyz/react"; const MUDContext = createContext(null); type Props = { children: ReactNode; - value: SetupResult; }; -export const MUDProvider = ({ children, value }: Props) => { +// THis is not used. Context doesn't play well with async data fetch so used hook instead +export const MUDProvider = ({ children }: Props) => { const currentValue = useContext(MUDContext); if (currentValue) throw new Error("MUDProvider can only be used once"); - return {children}; + + // Had weird re-rendering issue with the approach below + // const setupResult = usePromise(setup()); + + // if (setupResult.status === "rejected") throw new Error("Ecountered error while setting up MUD"); + + // if (setupResult.status !== "fulfilled") return; + const setupPromise = useMemo(() => { + return setup(); + }, []); + + const [setupResult, setSetupResult] = useState> | null>(null); + + useEffect(() => { + setupPromise.then((result) => setSetupResult(result)); + + return () => { + setupPromise.then((result) => result.network.world.dispose()); + }; + }, [setupPromise]); + + return {children}; }; export const useMUD = () => { const value = useContext(MUDContext); - if (!value) throw new Error("Must be used within a MUDProvider"); return value; }; diff --git a/templates/react/packages/client/src/hooks/useDelegationControl.ts b/templates/react/packages/client/src/hooks/useDelegationControl.ts new file mode 100644 index 0000000000..5d157e8aa7 --- /dev/null +++ b/templates/react/packages/client/src/hooks/useDelegationControl.ts @@ -0,0 +1,41 @@ +import { ComponentValue, Type, getComponentValue } from "@latticexyz/recs"; +import { useEffect, useState } from "react"; +import { WalletClient, Transport, Chain, Account } from "viem"; +import { encodeEntity } from "@latticexyz/store-sync/recs"; +import { ClientComponents } from "../mud/createClientComponents"; + +export function useDelegationControl( + delegator: WalletClient | null | undefined, + delegatee: WalletClient, + components: ClientComponents +) { + const [delegationControlId, setDelegationControlId] = useState< + | ComponentValue< + { + __staticData: Type.OptionalString; + __encodedLengths: Type.OptionalString; + __dynamicData: Type.OptionalString; + } & { + delegationControlId: Type.String; + }, + unknown + > + | undefined + >(undefined); + + useEffect(() => { + if (!delegator) return; + + const delegationsKeyEntity = encodeEntity(components.Delegations.metadata.keySchema, { + delegator: delegator.account.address, + delegatee: delegatee.account.address, + }); + const delegationControlId = getComponentValue(components.Delegations, delegationsKeyEntity); + + setDelegationControlId(delegationControlId); + + // TODO what's the cleanup fn? + }, [delegator]); + + return delegationControlId; +} diff --git a/templates/react/packages/client/src/hooks/usePromiseValue.ts b/templates/react/packages/client/src/hooks/usePromiseValue.ts new file mode 100644 index 0000000000..bd71492d14 --- /dev/null +++ b/templates/react/packages/client/src/hooks/usePromiseValue.ts @@ -0,0 +1,24 @@ +import { useEffect, useState, useRef } from "react"; + +export const usePromiseValue = (promise: Promise | null) => { + const promiseRef = useRef(promise); + const [value, setValue] = useState(null); + useEffect(() => { + if (!promise) return; + let isMounted = true; + promiseRef.current = promise; + // TODO: do something with promise errors? + promise.then((resolvedValue) => { + // skip if unmounted (state changes will cause errors otherwise) + if (!isMounted) return; + // If our promise was replaced before it resolved, ignore the result + if (promiseRef.current !== promise) return; + + setValue(resolvedValue); + }); + return () => { + isMounted = false; + }; + }, [promise]); + return value; +}; diff --git a/templates/react/packages/client/src/hooks/useSetup.tsx b/templates/react/packages/client/src/hooks/useSetup.tsx new file mode 100644 index 0000000000..8f11d8c765 --- /dev/null +++ b/templates/react/packages/client/src/hooks/useSetup.tsx @@ -0,0 +1,17 @@ +import { useEffect, useMemo } from "react"; +import { usePromiseValue } from "./usePromiseValue"; +import { setup } from "../mud/setup"; + +export const useSetup = () => { + const setupPromise = useMemo(() => { + return setup(); + }, []); + + useEffect(() => { + return () => { + setupPromise.then((result) => result.network.world.dispose()); + }; + }, [setupPromise]); + + return usePromiseValue(setupPromise); +}; diff --git a/templates/react/packages/client/src/hooks/useSystemCalls.ts b/templates/react/packages/client/src/hooks/useSystemCalls.ts new file mode 100644 index 0000000000..7046b91f11 --- /dev/null +++ b/templates/react/packages/client/src/hooks/useSystemCalls.ts @@ -0,0 +1,25 @@ +import { useEffect, useState } from "react"; +import { WalletClient, Transport, Chain, Account } from "viem"; +import { createSystemCalls } from "../mud/createSystemCalls"; +import { SetupNetworkResult } from "../mud/setupNetwork"; +import { ClientComponents } from "../mud/createClientComponents"; + +export function useSystemCalls( + network: SetupNetworkResult, + components: ClientComponents, + client: WalletClient | null | undefined +) { + // TODO type systemCalls + const [systemCalls, setSystemCalls] = useState | undefined>(undefined); + + useEffect(() => { + if (!client) return; + + const systemCalls = createSystemCalls(network, components, client); + setSystemCalls(systemCalls); + + // TODO what's the cleanup fn? + }, [client]); + + return systemCalls; +} diff --git a/templates/react/packages/client/src/index.tsx b/templates/react/packages/client/src/index.tsx index da8d70f020..8703e405d6 100644 --- a/templates/react/packages/client/src/index.tsx +++ b/templates/react/packages/client/src/index.tsx @@ -1,34 +1,45 @@ import ReactDOM from "react-dom/client"; -import { App } from "./App"; -import { setup } from "./mud/setup"; -import { MUDProvider } from "./MUDContext"; +import { RainbowKitProvider } from "@rainbow-me/rainbowkit"; +import { WagmiConfig } from "wagmi"; +import { share } from "rxjs"; import mudConfig from "contracts/mud.config"; +import IWorldAbi from "contracts/out/IWorld.sol/IWorld.abi.json"; +import { App } from "./App"; +import { MUDProvider, useMUD } from "./MUDContext"; +import { useSetup } from "./hooks/useSetup"; const rootElement = document.getElementById("react-root"); if (!rootElement) throw new Error("React root not found"); const root = ReactDOM.createRoot(rootElement); -// TODO: figure out if we actually want this to be async or if we should render something else in the meantime -setup().then(async (result) => { - root.render( - - - - ); +const AppWithWalletContext = () => { + const setup = useSetup(); // https://vitejs.dev/guide/env-and-mode.html - if (import.meta.env.DEV) { - const { mount: mountDevTools } = await import("@latticexyz/dev-tools"); - mountDevTools({ - config: mudConfig, - publicClient: result.network.publicClient, - walletClient: result.network.walletClient, - latestBlock$: result.network.latestBlock$, - storedBlockLogs$: result.network.storedBlockLogs$, - worldAddress: result.network.worldContract.address, - worldAbi: result.network.worldContract.abi, - write$: result.network.write$, - recsWorld: result.network.world, - }); + if (import.meta.env.DEV && setup) { + import("@latticexyz/dev-tools").then(({ mount: mountDevTools }) => + mountDevTools({ + config: mudConfig, + publicClient: setup.network.publicClient, + walletClient: setup.network.walletClient, + latestBlock$: setup.network.latestBlock$, + storedBlockLogs$: setup.network.storedBlockLogs$, + worldAddress: setup.network.worldAddress, + worldAbi: IWorldAbi, + write$: setup.network.write$.asObservable().pipe(share()), + recsWorld: setup.network.world, + }) + ); } -}); + + if (!setup) return <>Loading...; + return ( + + + + + + ); +}; + +root.render(); diff --git a/templates/react/packages/client/src/mud/constants.ts b/templates/react/packages/client/src/mud/constants.ts new file mode 100644 index 0000000000..78c56c0cf2 --- /dev/null +++ b/templates/react/packages/client/src/mud/constants.ts @@ -0,0 +1,7 @@ +import { encodePacked, toHex } from "viem"; + +const ROOT_NAMESPACE = 0; + +const encodedRootSpace = toHex(ROOT_NAMESPACE, { size: 16 }); +const encodedDelegationId = toHex("unlimited.d", { size: 16 }); +export const UNLIMITED_DELEGATION = encodePacked(["bytes16", "bytes16"], [encodedRootSpace, encodedDelegationId]); diff --git a/templates/react/packages/client/src/mud/createSystemCalls.ts b/templates/react/packages/client/src/mud/createSystemCalls.ts index 9858910ed2..a904cfa4cc 100644 --- a/templates/react/packages/client/src/mud/createSystemCalls.ts +++ b/templates/react/packages/client/src/mud/createSystemCalls.ts @@ -2,11 +2,14 @@ * Create the system calls that the client can use to ask * for changes in the World state (using the System contracts). */ - +import { Hex, WalletClient, Transport, Chain, Account } from "viem"; import { getComponentValue } from "@latticexyz/recs"; -import { ClientComponents } from "./createClientComponents"; -import { SetupNetworkResult } from "./setupNetwork"; import { singletonEntity } from "@latticexyz/store-sync/recs"; +import { SetupNetworkResult } from "./setupNetwork"; +import { ClientComponents } from "./createClientComponents"; +import { UNLIMITED_DELEGATION } from "./constants"; +import { createContract } from "@latticexyz/common"; +import IWorldAbi from "contracts/out/IWorld.sol/IWorld.abi.json"; export type SystemCalls = ReturnType; @@ -28,8 +31,9 @@ export function createSystemCalls( * through createClientComponents.ts, but it originates in * syncToRecs (https://github.com/latticexyz/mud/blob/26dabb34321eedff7a43f3fcb46da4f3f5ba3708/templates/react/packages/client/src/mud/setupNetwork.ts#L39). */ - { worldContract, waitForTransaction }: SetupNetworkResult, - { Counter }: ClientComponents + { worldAddress, waitForTransaction, getResourceSelector, publicClient, write$, worldContract }: SetupNetworkResult, + { Counter }: ClientComponents, + walletClient: WalletClient ) { const increment = async () => { /* @@ -38,12 +42,29 @@ export function createSystemCalls( * is in the root namespace, `.increment` can be called directly * on the World contract. */ - const tx = await worldContract.write.increment(); + // gas added here is a workaround for intrinsic gas too high error + // likely something along with commenting out the defaultPriorityFee on chain config? + const tx = await worldContract.write.increment({ gas: 1500000 }); await waitForTransaction(tx); return getComponentValue(Counter, singletonEntity); }; + const registerDelegation = async (delegatee: Hex) => { + const contractWithDelegator = createContract({ + address: worldAddress as Hex, + abi: IWorldAbi, + publicClient, + walletClient, + onWrite: (write) => write$.next(write), + getResourceSelector, + }); + const callData = "0x"; + const tx = await contractWithDelegator.write.registerDelegation([delegatee, UNLIMITED_DELEGATION, callData]); + await waitForTransaction(tx); + }; + return { increment, + registerDelegation, }; } diff --git a/templates/react/packages/client/src/mud/setup.ts b/templates/react/packages/client/src/mud/setup.ts index 8f9fdbab34..c95fa97b68 100644 --- a/templates/react/packages/client/src/mud/setup.ts +++ b/templates/react/packages/client/src/mud/setup.ts @@ -3,7 +3,6 @@ */ import { createClientComponents } from "./createClientComponents"; -import { createSystemCalls } from "./createSystemCalls"; import { setupNetwork } from "./setupNetwork"; export type SetupResult = Awaited>; @@ -11,11 +10,9 @@ export type SetupResult = Awaited>; export async function setup() { const network = await setupNetwork(); const components = createClientComponents(network); - const systemCalls = createSystemCalls(network, components); return { network, components, - systemCalls, }; } diff --git a/templates/react/packages/client/src/mud/setupNetwork.ts b/templates/react/packages/client/src/mud/setupNetwork.ts index 59f5663277..f4c336f55d 100644 --- a/templates/react/packages/client/src/mud/setupNetwork.ts +++ b/templates/react/packages/client/src/mud/setupNetwork.ts @@ -6,13 +6,13 @@ import { createPublicClient, fallback, webSocket, http, createWalletClient, Hex, parseEther, ClientConfig } from "viem"; import { createFaucetService } from "@latticexyz/services/faucet"; import { encodeEntity, syncToRecs } from "@latticexyz/store-sync/recs"; - +import { createConfig } from "wagmi"; +import { getDefaultWallets } from "@rainbow-me/rainbowkit"; +import { Subject } from "rxjs"; import { getNetworkConfig } from "./getNetworkConfig"; import { world } from "./world"; +import { createBurnerAccount, transportObserver, ContractWrite, createContract } from "@latticexyz/common"; import IWorldAbi from "contracts/out/IWorld.sol/IWorld.abi.json"; -import { createBurnerAccount, createContract, transportObserver, ContractWrite } from "@latticexyz/common"; - -import { Subject, share } from "rxjs"; /* * Import our MUD config, which includes strong types for @@ -51,6 +51,18 @@ export async function setupNetwork() { account: burnerAccount, }); + const { connectors } = getDefaultWallets({ + appName: "MUD", + projectId: "MUD", + chains: [networkConfig.chain], + }); + + const wagmiConfig = createConfig({ + autoConnect: true, + connectors, + publicClient, + }); + /* * Create an observable for contract writes that we can * pass into MUD dev tools for transaction observability. @@ -113,14 +125,18 @@ export async function setupNetwork() { return { world, + worldAddress: networkConfig.worldAddress, + worldContract, components, playerEntity: encodeEntity({ address: "address" }, { address: burnerWalletClient.account.address }), publicClient, walletClient: burnerWalletClient, + wagmiConfig, + chain: networkConfig.chain, latestBlock$, storedBlockLogs$, waitForTransaction, - worldContract, - write$: write$.asObservable().pipe(share()), + getResourceSelector, + write$, }; }