From 3e5b68511655d37295851dc07c284856e9f68ebd Mon Sep 17 00:00:00 2001 From: ECWireless Date: Tue, 28 May 2024 18:33:49 -0600 Subject: [PATCH 1/4] Move MUD files to a lib folder --- packages/client/src/MUDContext.tsx | 2 +- packages/client/src/index.tsx | 2 +- packages/client/src/{ => lib}/mud/createClientComponents.ts | 0 packages/client/src/{ => lib}/mud/createSystemCalls.ts | 0 packages/client/src/{ => lib}/mud/getNetworkConfig.ts | 0 packages/client/src/{ => lib}/mud/setup.ts | 0 packages/client/src/{ => lib}/mud/setupNetwork.ts | 0 packages/client/src/{ => lib}/mud/supportedChains.ts | 0 packages/client/src/{ => lib}/mud/world.ts | 0 9 files changed, 2 insertions(+), 2 deletions(-) rename packages/client/src/{ => lib}/mud/createClientComponents.ts (100%) rename packages/client/src/{ => lib}/mud/createSystemCalls.ts (100%) rename packages/client/src/{ => lib}/mud/getNetworkConfig.ts (100%) rename packages/client/src/{ => lib}/mud/setup.ts (100%) rename packages/client/src/{ => lib}/mud/setupNetwork.ts (100%) rename packages/client/src/{ => lib}/mud/supportedChains.ts (100%) rename packages/client/src/{ => lib}/mud/world.ts (100%) diff --git a/packages/client/src/MUDContext.tsx b/packages/client/src/MUDContext.tsx index c681c21bb..c9184787b 100644 --- a/packages/client/src/MUDContext.tsx +++ b/packages/client/src/MUDContext.tsx @@ -1,6 +1,6 @@ import { createContext, ReactNode, useContext } from 'react'; -import { SetupResult } from './mud/setup'; +import { SetupResult } from './lib/mud/setup'; const MUDContext = createContext(null); diff --git a/packages/client/src/index.tsx b/packages/client/src/index.tsx index 3f0c46eec..b19cff760 100644 --- a/packages/client/src/index.tsx +++ b/packages/client/src/index.tsx @@ -14,7 +14,7 @@ import { createRoot } from 'react-dom/client'; import { App } from './App'; import { Web3Provider } from './contexts/Web3Provider'; -import { setup } from './mud/setup'; +import { setup } from './lib/mud/setup'; import { MUDProvider } from './MUDContext'; import { globalStyles, theme } from './utils/theme'; diff --git a/packages/client/src/mud/createClientComponents.ts b/packages/client/src/lib/mud/createClientComponents.ts similarity index 100% rename from packages/client/src/mud/createClientComponents.ts rename to packages/client/src/lib/mud/createClientComponents.ts diff --git a/packages/client/src/mud/createSystemCalls.ts b/packages/client/src/lib/mud/createSystemCalls.ts similarity index 100% rename from packages/client/src/mud/createSystemCalls.ts rename to packages/client/src/lib/mud/createSystemCalls.ts diff --git a/packages/client/src/mud/getNetworkConfig.ts b/packages/client/src/lib/mud/getNetworkConfig.ts similarity index 100% rename from packages/client/src/mud/getNetworkConfig.ts rename to packages/client/src/lib/mud/getNetworkConfig.ts diff --git a/packages/client/src/mud/setup.ts b/packages/client/src/lib/mud/setup.ts similarity index 100% rename from packages/client/src/mud/setup.ts rename to packages/client/src/lib/mud/setup.ts diff --git a/packages/client/src/mud/setupNetwork.ts b/packages/client/src/lib/mud/setupNetwork.ts similarity index 100% rename from packages/client/src/mud/setupNetwork.ts rename to packages/client/src/lib/mud/setupNetwork.ts diff --git a/packages/client/src/mud/supportedChains.ts b/packages/client/src/lib/mud/supportedChains.ts similarity index 100% rename from packages/client/src/mud/supportedChains.ts rename to packages/client/src/lib/mud/supportedChains.ts diff --git a/packages/client/src/mud/world.ts b/packages/client/src/lib/mud/world.ts similarity index 100% rename from packages/client/src/mud/world.ts rename to packages/client/src/lib/mud/world.ts From 8652939e54899260ceb2fb214393951bdd3994cf Mon Sep 17 00:00:00 2001 From: ECWireless Date: Tue, 28 May 2024 18:35:31 -0600 Subject: [PATCH 2/4] Move MUDContext --- packages/client/src/{ => contexts}/MUDContext.tsx | 2 +- packages/client/src/index.tsx | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) rename packages/client/src/{ => contexts}/MUDContext.tsx (92%) diff --git a/packages/client/src/MUDContext.tsx b/packages/client/src/contexts/MUDContext.tsx similarity index 92% rename from packages/client/src/MUDContext.tsx rename to packages/client/src/contexts/MUDContext.tsx index c9184787b..acd6b08ed 100644 --- a/packages/client/src/MUDContext.tsx +++ b/packages/client/src/contexts/MUDContext.tsx @@ -1,6 +1,6 @@ import { createContext, ReactNode, useContext } from 'react'; -import { SetupResult } from './lib/mud/setup'; +import { SetupResult } from '../lib/mud/setup'; const MUDContext = createContext(null); diff --git a/packages/client/src/index.tsx b/packages/client/src/index.tsx index b19cff760..7b6546de4 100644 --- a/packages/client/src/index.tsx +++ b/packages/client/src/index.tsx @@ -13,9 +13,9 @@ import mudConfig from 'contracts/mud.config'; import { createRoot } from 'react-dom/client'; import { App } from './App'; +import { MUDProvider } from './contexts/MUDContext'; import { Web3Provider } from './contexts/Web3Provider'; import { setup } from './lib/mud/setup'; -import { MUDProvider } from './MUDContext'; import { globalStyles, theme } from './utils/theme'; const rootElement = document.getElementById('react-root'); From 987ca449f198408701743b6ba56e3eae0f69902f Mon Sep 17 00:00:00 2001 From: ECWireless Date: Tue, 28 May 2024 19:01:20 -0600 Subject: [PATCH 3/4] Add lib files for delegation --- packages/client/src/contexts/MUDContext.tsx | 112 +++++++++++++++-- packages/client/src/index.tsx | 2 +- packages/client/src/lib/mud/createBurner.ts | 115 ++++++++++++++++++ .../src/lib/mud/createViemClientConfig.ts | 25 ++++ .../src/lib/mud/getBurnerAccount copy.ts | 10 ++ .../client/src/lib/mud/getBurnerAccount.ts | 10 ++ packages/client/src/lib/mud/setup.ts | 4 +- packages/client/src/lib/mud/setupNetwork.ts | 35 ++---- 8 files changed, 275 insertions(+), 38 deletions(-) create mode 100644 packages/client/src/lib/mud/createBurner.ts create mode 100644 packages/client/src/lib/mud/createViemClientConfig.ts create mode 100644 packages/client/src/lib/mud/getBurnerAccount copy.ts create mode 100644 packages/client/src/lib/mud/getBurnerAccount.ts diff --git a/packages/client/src/contexts/MUDContext.tsx b/packages/client/src/contexts/MUDContext.tsx index acd6b08ed..686d90db9 100644 --- a/packages/client/src/contexts/MUDContext.tsx +++ b/packages/client/src/contexts/MUDContext.tsx @@ -1,21 +1,117 @@ -import { createContext, ReactNode, useContext } from 'react'; +import { + createContext, + ReactNode, + useCallback, + useContext, + useEffect, + useMemo, + useState, +} from 'react'; +import { formatEther } from 'viem'; -import { SetupResult } from '../lib/mud/setup'; +import { type Burner } from '../lib/mud/createBurner'; +import { + ComponentsResult, + NetworkResult, + SystemCallsResult, +} from '../lib/mud/setup'; -const MUDContext = createContext(null); +const MUDContext = createContext<{ + burner: Burner | null; + burnerBalance: string; + components: ComponentsResult; + network: NetworkResult; + setBurnerWithCleanup: (burner: Burner) => () => void; + systemCalls: SystemCallsResult; +} | null>(null); type Props = { children: ReactNode; - value: SetupResult; + setupResult: { + components: ComponentsResult; + network: NetworkResult; + systemCalls: SystemCallsResult; + }; }; -export const MUDProvider = ({ children, value }: Props): JSX.Element => { - const currentValue = useContext(MUDContext); - if (currentValue) throw new Error('MUDProvider can only be used once'); +export const MUDProvider = ({ children, setupResult }: Props): JSX.Element => { + const [burner, setBurner] = useState(null); + const [burnerBalance, setBurnerBalance] = useState('0'); + const [components, setComponents] = useState(null); + const [network, setNetwork] = useState(null); + const [systemCalls, setSystemCalls] = useState( + null, + ); + + const setBurnerWithCleanup = useCallback((burner: Burner) => { + setBurner(burner); + + return () => { + setBurner(null); + }; + }, []); + + useEffect(() => { + if (network && components && systemCalls) return; + if (setupResult) { + setComponents(setupResult.components); + setNetwork(setupResult.network); + setSystemCalls(setupResult.systemCalls); + } + }, [components, network, setupResult, systemCalls]); + + const getBurnerBalance = useCallback(async () => { + if (!(burner && network)) return; + const balance = await network.publicClient.getBalance({ + address: burner.walletClient.account.address, + }); + setBurnerBalance(formatEther(balance)); + }, [burner, network]); + + useEffect(() => { + if (!burner) return () => {}; + getBurnerBalance(); + + const interval = setInterval(getBurnerBalance, 5000); + return () => clearInterval(interval); + }, [burner, getBurnerBalance]); + + const value = useMemo(() => { + if (!setupResult) return null; + if (!burner) { + return { + burner, + burnerBalance, + components: setupResult.components, + network: setupResult.network, + setBurnerWithCleanup, + systemCalls: setupResult.systemCalls, + }; + } + + return { + network: burner.network, + components: burner.components, + systemCalls: burner.systemCalls, + burner, + burnerBalance, + setBurnerWithCleanup, + }; + }, [burner, burnerBalance, setBurnerWithCleanup, setupResult]); + + // const currentValue = useContext(MUDContext); + // if (currentValue) throw new Error('MUDProvider can only be used once'); return {children}; }; -export const useMUD = (): SetupResult => { +export const useMUD = (): { + burner: Burner | null; + burnerBalance: string; + components: ComponentsResult; + network: NetworkResult; + setBurnerWithCleanup: (burner: Burner) => () => void; + systemCalls: SystemCallsResult; +} => { const value = useContext(MUDContext); if (!value) throw new Error('Must be used within a MUDProvider'); return value; diff --git a/packages/client/src/index.tsx b/packages/client/src/index.tsx index 7b6546de4..fe4f1f4b7 100644 --- a/packages/client/src/index.tsx +++ b/packages/client/src/index.tsx @@ -28,7 +28,7 @@ setup().then(async result => { - + diff --git a/packages/client/src/lib/mud/createBurner.ts b/packages/client/src/lib/mud/createBurner.ts new file mode 100644 index 000000000..64328e722 --- /dev/null +++ b/packages/client/src/lib/mud/createBurner.ts @@ -0,0 +1,115 @@ +import { type ContractWrite } from '@latticexyz/common'; +import { transactionQueue, writeObserver } from '@latticexyz/common/actions'; +import { getComponentValue } from '@latticexyz/recs'; +import {} from '@latticexyz/store-sync'; +import { encodeEntity } from '@latticexyz/store-sync/recs'; +import { callFrom } from '@latticexyz/world/internal'; +import IWorldAbi from 'contracts/out/IWorld.sol/IWorld.abi.json'; +import { share, Subject } from 'rxjs'; +import { createWalletClient, getContract, type Hex } from 'viem'; + +import { createSystemCalls } from '../mud/createSystemCalls'; +import { type SetupNetworkResult } from '../mud/setupNetwork'; +import { createViemClientConfig } from './createViemClientConfig'; +import { getBurnerAccount } from './getBurnerAccount'; + +export type Burner = ReturnType; +export type WorldContract = Burner['worldContract']; + +// Create a burner object including `walletClient` and `worldContract`. +// +// A burner account is a temporary account stored in local storage. +// This function checks its existence in storage; if absent, generates and saves the account. +// +// If `delegatorAddress` is provided, delegation is automatically applied to `walletClient.writeContract(world...)` and `worldContract.write()`. + +// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types +export function createBurner( + network: SetupNetworkResult, + delegatorAddress?: Hex, +) { + /* + * Create an observable for contract writes that we can + * pass into MUD dev tools for transaction observability. + */ + const write$ = new Subject(); + + /* + * Get or create a burner account, and create a viem client for it + * (see https://viem.sh/docs/clients/wallet.html). + */ + let walletClient = createWalletClient({ + ...createViemClientConfig(network.publicClient.chain), + account: getBurnerAccount(), + }) + .extend(transactionQueue()) + .extend(writeObserver({ onWrite: write => write$.next(write) })); + + if (delegatorAddress) { + walletClient = walletClient.extend( + callFrom({ + worldAddress: network.worldContract.address, + delegatorAddress, + worldFunctionToSystemFunction: async worldFunctionSelector => { + const encodedWorldFunctionSelector = encodeEntity( + { string: 'bytes4' }, + { string: worldFunctionSelector }, + ); + + const systemFunction = getComponentValue( + network.components.FunctionSelectors, + encodedWorldFunctionSelector, + ); + + if (!systemFunction) + throw new Error( + `Possibly store not synced: ${worldFunctionSelector}`, + ); + + return { + systemId: systemFunction.systemId as Hex, + systemFunctionSelector: + systemFunction.systemFunctionSelector as Hex, + }; + }, + }), + ); + } + + /* + * Create an object for communicating with the deployed World. + */ + const worldContract = getContract({ + address: network.worldContract.address, + abi: IWorldAbi, + client: { public: network.publicClient, wallet: walletClient }, + }); + + const components = { + ...network.components, + // Position: overridableComponent(network.components.Position), + }; + + return { + components, + delegatorAddress, + delegatorEntity: delegatorAddress + ? encodeEntity({ address: 'address' }, { address: delegatorAddress }) + : undefined, + network, + playerEntity: encodeEntity( + { address: 'address' }, + { address: walletClient.account.address }, + ), + systemCalls: createSystemCalls( + { + ...network, + worldContract: worldContract, + }, + components, + ), + walletClient, + worldContract, + write$: write$.asObservable().pipe(share()), + }; +} diff --git a/packages/client/src/lib/mud/createViemClientConfig.ts b/packages/client/src/lib/mud/createViemClientConfig.ts new file mode 100644 index 000000000..2000c3bbc --- /dev/null +++ b/packages/client/src/lib/mud/createViemClientConfig.ts @@ -0,0 +1,25 @@ +import { transportObserver } from '@latticexyz/common'; +import { type MUDChain } from '@latticexyz/common/chains'; +import { + type ClientConfig, + fallback, + FallbackTransport, + http, + webSocket, +} from 'viem'; + +/* + * Create a viem public (read only) client + * (https://viem.sh/docs/clients/public.html) + */ +export function createViemClientConfig(chain: MUDChain): { + readonly chain: MUDChain; + readonly transport: FallbackTransport; + readonly pollingInterval: 1000; +} { + return { + chain, + transport: transportObserver(fallback([webSocket(), http()])), + pollingInterval: 1000, + } as const satisfies ClientConfig; +} diff --git a/packages/client/src/lib/mud/getBurnerAccount copy.ts b/packages/client/src/lib/mud/getBurnerAccount copy.ts new file mode 100644 index 000000000..f169fc95e --- /dev/null +++ b/packages/client/src/lib/mud/getBurnerAccount copy.ts @@ -0,0 +1,10 @@ +import { createBurnerAccount, getBurnerPrivateKey } from '@latticexyz/common'; +import { type PrivateKeyAccount } from 'viem'; + +// Get or create a burner account. +// +// A burner account is a temporary account stored in local storage. +// This function checks its existence in storage; if absent, generates and saves the account. +export function getBurnerAccount(): PrivateKeyAccount { + return createBurnerAccount(getBurnerPrivateKey()); +} diff --git a/packages/client/src/lib/mud/getBurnerAccount.ts b/packages/client/src/lib/mud/getBurnerAccount.ts new file mode 100644 index 000000000..f169fc95e --- /dev/null +++ b/packages/client/src/lib/mud/getBurnerAccount.ts @@ -0,0 +1,10 @@ +import { createBurnerAccount, getBurnerPrivateKey } from '@latticexyz/common'; +import { type PrivateKeyAccount } from 'viem'; + +// Get or create a burner account. +// +// A burner account is a temporary account stored in local storage. +// This function checks its existence in storage; if absent, generates and saves the account. +export function getBurnerAccount(): PrivateKeyAccount { + return createBurnerAccount(getBurnerPrivateKey()); +} diff --git a/packages/client/src/lib/mud/setup.ts b/packages/client/src/lib/mud/setup.ts index 56f108919..a3c35def0 100644 --- a/packages/client/src/lib/mud/setup.ts +++ b/packages/client/src/lib/mud/setup.ts @@ -6,7 +6,9 @@ import { createClientComponents } from './createClientComponents'; import { createSystemCalls } from './createSystemCalls'; import { setupNetwork } from './setupNetwork'; -export type SetupResult = Awaited>; +export type NetworkResult = Awaited>; +export type ComponentsResult = ReturnType; +export type SystemCallsResult = ReturnType; // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types export async function setup() { diff --git a/packages/client/src/lib/mud/setupNetwork.ts b/packages/client/src/lib/mud/setupNetwork.ts index a5ebe701a..7a55acee5 100644 --- a/packages/client/src/lib/mud/setupNetwork.ts +++ b/packages/client/src/lib/mud/setupNetwork.ts @@ -3,11 +3,7 @@ * (https://viem.sh/docs/getting-started.html). * This line imports the functions we need from it. */ -import { - ContractWrite, - createBurnerAccount, - transportObserver, -} from '@latticexyz/common'; +import { ContractWrite, createBurnerAccount } from '@latticexyz/common'; import { transactionQueue, writeObserver } from '@latticexyz/common/actions'; import { encodeEntity, syncToRecs } from '@latticexyz/store-sync/recs'; /* @@ -21,17 +17,9 @@ import { encodeEntity, syncToRecs } from '@latticexyz/store-sync/recs'; import mudConfig from 'contracts/mud.config'; import IWorldAbi from 'contracts/out/IWorld.sol/IWorld.abi.json'; import { share, Subject } from 'rxjs'; -import { - ClientConfig, - createPublicClient, - createWalletClient, - fallback, - getContract, - Hex, - http, - webSocket, -} from 'viem'; +import { createPublicClient, createWalletClient, getContract, Hex } from 'viem'; +import { createViemClientConfig } from './createViemClientConfig'; import { getNetworkConfig } from './getNetworkConfig'; import { world } from './world'; @@ -41,16 +29,7 @@ export type SetupNetworkResult = Awaited>; export async function setupNetwork() { const networkConfig = await getNetworkConfig(); - /* - * Create a viem public (read only) client - * (https://viem.sh/docs/clients/public.html) - */ - const clientOptions = { - chain: networkConfig.chain, - transport: transportObserver(fallback([webSocket(), http()])), - pollingInterval: 1000, - } as const satisfies ClientConfig; - + const clientOptions = createViemClientConfig(networkConfig.chain); const publicClient = createPublicClient(clientOptions); /* @@ -96,17 +75,17 @@ export async function setupNetwork() { }); return { - world, components, + latestBlock$, playerEntity: encodeEntity( { address: 'address' }, { address: burnerWalletClient.account.address }, ), publicClient, - walletClient: burnerWalletClient, - latestBlock$, storedBlockLogs$, waitForTransaction, + walletClient: burnerWalletClient, + world, worldContract, write$: write$.asObservable().pipe(share()), }; From 165046b1eb8ce75e7f5aeb482a25d027f70faf7f Mon Sep 17 00:00:00 2001 From: ECWireless Date: Wed, 29 May 2024 08:48:27 -0600 Subject: [PATCH 4/4] Add delegation button --- packages/client/index.html | 4 +- .../src/components/ConnectWalletModal.tsx | 134 +++++++++++++++++- packages/client/src/contexts/MUDContext.tsx | 22 ++- packages/client/src/hooks/useDelegation.ts | 46 ++++++ packages/client/src/lib/mud/delegation.ts | 40 ++++++ 5 files changed, 230 insertions(+), 16 deletions(-) create mode 100644 packages/client/src/hooks/useDelegation.ts create mode 100644 packages/client/src/lib/mud/delegation.ts diff --git a/packages/client/index.html b/packages/client/index.html index c3f06eb80..e0abe524a 100644 --- a/packages/client/index.html +++ b/packages/client/index.html @@ -1,9 +1,9 @@ - + - a minimal MUD client + Ultimate Dominion
diff --git a/packages/client/src/components/ConnectWalletModal.tsx b/packages/client/src/components/ConnectWalletModal.tsx index f264dcee6..41142e1fc 100644 --- a/packages/client/src/components/ConnectWalletModal.tsx +++ b/packages/client/src/components/ConnectWalletModal.tsx @@ -1,4 +1,5 @@ import { + Button, Modal, ModalBody, ModalCloseButton, @@ -8,7 +9,13 @@ import { Text, VStack, } from '@chakra-ui/react'; +import { useEffect, useMemo } from 'react'; +import type { Account, Chain, Hex, Transport, WalletClient } from 'viem'; +import { useAccount, useSwitchChain, useWalletClient } from 'wagmi'; +import { useMUD } from '../contexts/MUDContext'; +import { useDelegation } from '../hooks/useDelegation'; +import { type Burner, createBurner } from '../lib/mud/createBurner'; import { ConnectWalletButton } from './ConnectWalletButton'; export const ConnectWalletModal = ({ @@ -18,19 +25,132 @@ export const ConnectWalletModal = ({ isOpen: boolean; onClose: () => void; }): JSX.Element => { + const { data: externalWalletClient } = useWalletClient(); + const { isConnected, address } = useAccount(); + const { delegatorAddress, setBurnerWithCleanup } = useMUD(); + + const bodyContent = useMemo(() => { + if (externalWalletClient && delegatorAddress) { + return ( + + + + + ); + } + + if (address && externalWalletClient && isConnected) { + return ( + + + + Connected account: + + + {address.slice(0, 6)}...{address.slice(-4)} + + + In order to play, you must delegate in-game power to a session + account. + + + A session account is a private key stored in your browser's + local storage. It allows you to play games without having to + confirm transactions, but is less secure. + + + Do not deposit any funds into this account that you are not + willing to lose. + + + + + ); + } + + return ( + + Connect your wallet to play. + + + ); + }, [ + address, + externalWalletClient, + delegatorAddress, + isConnected, + onClose, + setBurnerWithCleanup, + ]); + return ( - Connect Wallet + + {isConnected ? 'Delegate Account' : 'Connect Wallet'} + - - - Connect your wallet to play. - - - + {bodyContent} ); }; + +export type SetBurnerProps = { setBurner: (burner: Burner) => () => void }; + +const DelegationButton = ({ + externalWalletClient, + setBurner, +}: SetBurnerProps & { + externalWalletClient: WalletClient; +}) => { + const { chains, switchChain } = useSwitchChain(); + const { chainId } = useAccount(); + const { status, setupDelegation } = useDelegation(externalWalletClient); + + const wrongNetwork = useMemo(() => { + if (!chainId) return true; + const chainIds = chains.map(chain => chain.id); + return !chainIds.includes(chainId); + }, [chainId, chains]); + + if (wrongNetwork) { + return ( + + ); + } + + if (status === 'delegated') { + return ( + + ); + } + + return ; +}; + +const SetBurner = ({ + externalWalletAccountAddress, + setBurner, +}: SetBurnerProps & { externalWalletAccountAddress: Hex }) => { + const { network } = useMUD(); + + useEffect( + () => setBurner(createBurner(network, externalWalletAccountAddress)), + [externalWalletAccountAddress, network, setBurner], + ); + + return null; +}; diff --git a/packages/client/src/contexts/MUDContext.tsx b/packages/client/src/contexts/MUDContext.tsx index 686d90db9..b06cf5bb9 100644 --- a/packages/client/src/contexts/MUDContext.tsx +++ b/packages/client/src/contexts/MUDContext.tsx @@ -1,3 +1,4 @@ +import { encodeEntity } from '@latticexyz/store-sync/recs'; import { createContext, ReactNode, @@ -17,9 +18,10 @@ import { } from '../lib/mud/setup'; const MUDContext = createContext<{ - burner: Burner | null; burnerBalance: string; components: ComponentsResult; + delegatorAddress: string | null; + delegatorEntity: string | null; network: NetworkResult; setBurnerWithCleanup: (burner: Burner) => () => void; systemCalls: SystemCallsResult; @@ -78,11 +80,12 @@ export const MUDProvider = ({ children, setupResult }: Props): JSX.Element => { const value = useMemo(() => { if (!setupResult) return null; - if (!burner) { + if (!(burner && burner.delegatorAddress)) { return { - burner, burnerBalance, components: setupResult.components, + delegatorAddress: null, + delegatorEntity: null, network: setupResult.network, setBurnerWithCleanup, systemCalls: setupResult.systemCalls, @@ -90,11 +93,15 @@ export const MUDProvider = ({ children, setupResult }: Props): JSX.Element => { } return { - network: burner.network, + burnerBalance, components: burner.components, + delegatorAddress: burner.delegatorAddress, + delegatorEntity: encodeEntity( + { address: 'address' }, + { address: burner.delegatorAddress }, + ), + network: burner.network, systemCalls: burner.systemCalls, - burner, - burnerBalance, setBurnerWithCleanup, }; }, [burner, burnerBalance, setBurnerWithCleanup, setupResult]); @@ -105,9 +112,10 @@ export const MUDProvider = ({ children, setupResult }: Props): JSX.Element => { }; export const useMUD = (): { - burner: Burner | null; burnerBalance: string; components: ComponentsResult; + delegatorAddress: string | null; + delegatorEntity: string | null; network: NetworkResult; setBurnerWithCleanup: (burner: Burner) => () => void; systemCalls: SystemCallsResult; diff --git a/packages/client/src/hooks/useDelegation.ts b/packages/client/src/hooks/useDelegation.ts new file mode 100644 index 000000000..1c1486560 --- /dev/null +++ b/packages/client/src/hooks/useDelegation.ts @@ -0,0 +1,46 @@ +import { useComponentValue } from '@latticexyz/react'; +import { encodeEntity } from '@latticexyz/store-sync/recs'; +import { useMemo } from 'react'; +import type { Account, Chain, Hex, Transport, WalletClient } from 'viem'; +import { useAccount } from 'wagmi'; + +import { useMUD } from '../contexts/MUDContext'; +import { isDelegated, setupDelegation } from '../lib/mud/delegation'; +import { getBurnerAccount } from '../lib/mud/getBurnerAccount'; + +export function useDelegation( + externalWalletClient: WalletClient, +): + | { + status: 'delegated'; + setupDelegation?: undefined; + } + | { + status: 'unset'; + setupDelegation: () => Promise; + } { + const { + network, + components: { UserDelegationControl }, + } = useMUD(); + const { address } = useAccount(); + + const burnerAddress = useMemo(() => getBurnerAccount().address, []); + + const delegation = useComponentValue( + UserDelegationControl, + encodeEntity( + { delegatee: 'address', delegator: 'address' }, + { delegatee: address ?? '0x', delegator: burnerAddress }, + ), + ); + + if (isDelegated(delegation as { delegationControlId: Hex })) + return { status: 'delegated' as const }; + + return { + status: 'unset' as const, + setupDelegation: () => + setupDelegation(network, externalWalletClient, burnerAddress), + }; +} diff --git a/packages/client/src/lib/mud/delegation.ts b/packages/client/src/lib/mud/delegation.ts new file mode 100644 index 000000000..3b39fc70d --- /dev/null +++ b/packages/client/src/lib/mud/delegation.ts @@ -0,0 +1,40 @@ +import { resourceToHex } from '@latticexyz/common'; +import IWorldAbi from 'contracts/out/IWorld.sol/IWorld.abi.json'; +import { + type Account, + type Chain, + type Hex, + type Transport, + type WalletClient, +} from 'viem'; + +import { type SetupNetworkResult } from '../mud/setupNetwork'; + +const UNLIMITED_DELEGATION = resourceToHex({ + type: 'system', + namespace: '', + name: 'unlimited', +}); + +export async function setupDelegation( + network: SetupNetworkResult, + externalWalletClient: WalletClient, + delegateeAddress: Hex, +): Promise { + const { request } = await network.publicClient.simulateContract({ + account: externalWalletClient.account, + address: network.worldContract.address, + abi: IWorldAbi, + functionName: 'registerDelegation', + args: [delegateeAddress, UNLIMITED_DELEGATION, '0x0'], + }); + + const delegationTx = await externalWalletClient.writeContract(request); + await network.waitForTransaction(delegationTx); +} + +export function isDelegated( + delegation: { delegationControlId: Hex } | undefined, +): boolean { + return delegation?.delegationControlId === UNLIMITED_DELEGATION; +}