Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(examples): upgrade wagmi to v2 and implement viem custom client within example #2307

Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -14,12 +14,14 @@
"@latticexyz/common": "link:../../../../packages/common",
"@latticexyz/dev-tools": "link:../../../../packages/dev-tools",
"@latticexyz/store-sync": "link:../../../../packages/store-sync",
"@tanstack/react-query": "5.22.2",
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Wagmi v2 needs this @tanstack/react-query installed alongside.

"contracts": "workspace:*",
"p-retry": "^5.1.2",
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

To adapt the transaction sending extension in this example, p-retry, which is utilized in the common library, has been installed here. If the direction of this implementation proves to be suitable, I plan to move the custom client implementation to the common library later. Then, I can remove p-retry from here.

"react": "^18.2.0",
"react-dom": "^18.2.0",
"rxjs": "7.5.5",
"viem": "1.14.0",
"wagmi": "1.4.13"
"viem": "2.7.12",
"wagmi": "2.5.7"
},
"devDependencies": {
"@types/react": "18.2.22",
Expand Down
Original file line number Diff line number Diff line change
@@ -1,18 +1,17 @@
import { useMUDRead } from "./mud/read";
import { useMUDWrite } from "./mud/write";
import { useMUD } from "./mud/customWalletClient";
import { increment } from "./mud/systemCalls";

export const App = () => {
const { useStore, tables } = useMUDRead();
const mudWrite = useMUDWrite();
const { network, walletClient } = useMUD();

const counter = useStore((state) => state.getValue(tables.CounterTable, {}));
const counter = network.useStore((state) => state.getValue(network.tables.CounterTable, {}));

return (
<div>
<div>Counter: {counter?.value ?? "unset"}</div>
<div>
{mudWrite && (
<button type="button" onClick={() => mudWrite.systemCalls.increment()}>
{walletClient && (
<button type="button" onClick={() => increment(walletClient, network)}>
Increment
</button>
)}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,29 +1,29 @@
import { useEffect } from "react";
import { useMUDRead } from "./mud/read";
import { useMUDWrite } from "./mud/write";
import { useMUD } from "./mud/customWalletClient";
import IWorldAbi from "contracts/out/IWorld.sol/IWorld.abi.json";
import mudConfig from "contracts/mud.config";

export const DevTools = () => {
const mudRead = useMUDRead();
const mudWrite = useMUDWrite();
const { network, walletClient } = useMUD();

useEffect(() => {
if (!mudWrite) return;
if (!walletClient) return;

// TODO: Handle unmount properly by updating the dev-tools implementation.
let unmount: (() => void) | null = null;

import("@latticexyz/dev-tools")
.then(({ mount }) =>
mount({
config: mudRead.mudConfig,
publicClient: mudRead.publicClient,
walletClient: mudWrite.walletClient,
latestBlock$: mudRead.latestBlock$,
storedBlockLogs$: mudRead.storedBlockLogs$,
worldAddress: mudRead.worldAddress,
worldAbi: mudWrite.worldContract.abi,
write$: mudWrite.write$,
useStore: mudRead.useStore,
config: mudConfig,
publicClient: network.publicClient,
walletClient: walletClient,
latestBlock$: network.latestBlock$,
storedBlockLogs$: network.storedBlockLogs$,
worldAddress: network.worldAddress,
worldAbi: IWorldAbi,
write$: network.write$,
useStore: network.useStore,
})
)
.then((result) => {
Expand All @@ -37,7 +37,7 @@ export const DevTools = () => {
unmount();
}
};
}, [mudWrite?.walletClient.account.address]);
}, [walletClient?.account.address]);

return null;
};
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These changes are to adapt Wagmi v2 API changes.

Original file line number Diff line number Diff line change
@@ -1,71 +1,57 @@
import { BaseError } from "viem";
import { useAccount, useConnect, useDisconnect, useNetwork, useSwitchNetwork } from "wagmi";
import { useAccount, useChains, useConnect, useDisconnect, useSwitchChain } from "wagmi";

export const ExternalWallet = () => {
const { isConnected } = useAccount();

return (
<div>
<Connect />
{isConnected && <Network />}
</div>
);
return <div>{isConnected ? <Account /> : <Connect />}</div>;
};

// Based on https://github.com/wevm/create-wagmi/blob/create-wagmi%401.0.5/templates/vite-react/default/src/components/Connect.tsx
function Connect() {
const { connector, isConnected } = useAccount();
const { connect, connectors, error, isLoading, pendingConnector } = useConnect();
const { disconnect } = useDisconnect();
const { connect, connectors, error } = useConnect();

return (
<div>
<div>
{isConnected && <button onClick={() => disconnect()}>Disconnect from {connector?.name}</button>}

{connectors
.filter((x) => x.ready && x.id !== connector?.id)
.map((x) => (
<button key={x.id} onClick={() => connect({ connector: x })}>
Connect {x.name}
{isLoading && x.id === pendingConnector?.id && " (connecting)"}
</button>
))}
{connectors.map((connector) => (
<button key={connector.uid} onClick={() => connect({ connector })}>
Connect {connector.name}
</button>
))}
</div>

{error && <div>{(error as BaseError).shortMessage}</div>}
<div>{error?.message}</div>
</div>
);
}

// Based on https://github.com/wevm/create-wagmi/blob/create-wagmi%401.0.5/templates/vite-react/default/src/components/NetworkSwitcher.tsx
function Network() {
const { chain } = useNetwork();
const { chains, error, isLoading, pendingChainId, switchNetwork } = useSwitchNetwork();
const { address } = useAccount();

const otherChains = chains.filter((x) => x.id !== chain?.id);
function Account() {
const { error, switchChain } = useSwitchChain();
const { address, connector, chain, chainId } = useAccount();
const { disconnect } = useDisconnect();
const chains = useChains();

return (
<div>
<div>{address}</div>
<div>
Connected to {chain?.name ?? chain?.id}
{chain?.unsupported && " (unsupported)"}
Connected to {chain?.name ?? chainId}
{chainId && !chains.map((x) => x.id).includes(chainId) ? " (unsupported)" : ""}
</div>
{switchNetwork && !!otherChains.length && (
<div>
Switch to:{" "}
{otherChains.map((x) => (
<button key={x.id} onClick={() => switchNetwork(x.id)}>
{x.name}
{isLoading && x.id === pendingChainId && " (switching)"}
<div>
{chains
.filter((x) => x.id !== chainId)
.map((x) => (
<button key={x.id} onClick={() => switchChain({ chainId: x.id })}>
Switch to {x.name}
</button>
))}
</div>
)}
</div>

<div>{error?.message}</div>

<div>
<button onClick={() => disconnect()}>Disconnect from {connector?.name}</button>
</div>
</div>
);
}
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import ReactDOM from "react-dom/client";
import { WagmiConfig } from "wagmi";
import { WagmiProvider } from "wagmi";
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import { ExternalWallet } from "./ExternalWallet";
import { MUDReadProvider } from "./mud/read";
import { MUDWriteProvider } from "./mud/write";
import { MUDNetworkProvider } from "./mud/NetworkContext";
import { App } from "./App";
import { DevTools } from "./DevTools";
import { setup } from "./mud/setup";
Expand All @@ -11,17 +11,19 @@ const rootElement = document.getElementById("react-root");
if (!rootElement) throw new Error("React root not found");
const root = ReactDOM.createRoot(rootElement);

const queryClient = new QueryClient();

// TODO: figure out if we actually want this to be async or if we should render something else in the meantime
setup().then(({ mud, wagmiConfig }) => {
setup().then(({ network, wagmiConfig }) => {
root.render(
<WagmiConfig config={wagmiConfig}>
<ExternalWallet />
<MUDReadProvider value={mud}>
<MUDWriteProvider>
<WagmiProvider config={wagmiConfig}>
<QueryClientProvider client={queryClient}>
<ExternalWallet />
<MUDNetworkProvider value={network}>
<App />
{import.meta.env.DEV && <DevTools />}
</MUDWriteProvider>
</MUDReadProvider>
</WagmiConfig>
</MUDNetworkProvider>
</QueryClientProvider>
</WagmiProvider>
Comment on lines -15 to +27
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The write context has been removed since we don't have states to hold for that.

);
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import { createContext, type ReactNode, useContext } from "react";
import { type SetupNetworkResult } from "./setupNetwork";

export type MUDNetwork = SetupNetworkResult;

const MUDNetworkContext = createContext<MUDNetwork | null>(null);

type Props = {
children: ReactNode;
value: MUDNetwork;
};

export const MUDNetworkProvider = ({ children, value }: Props) => {
const currentValue = useContext(MUDNetworkContext);
if (currentValue) throw new Error("MUDNetworkProvider can only be used once");
return <MUDNetworkContext.Provider value={value}>{children}</MUDNetworkContext.Provider>;
};

export const useMUDNetwork = () => {
const value = useContext(MUDNetworkContext);
if (!value) throw new Error("Must be used within a MUDNetworkProvider");
return value;
};

This file was deleted.

Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
import type { WriteContractParameters, Chain, Account, WalletActions } from "viem";
import { sendTransaction, writeContract } from "viem/actions";
import { useAccount, useWalletClient, type UseWalletClientReturnType } from "wagmi";
import pRetry from "p-retry";
import { getNonceManager } from "@latticexyz/common";
import { useMUDNetwork, type MUDNetwork } from "./NetworkContext";

export const useMUD = () => {
const network = useMUDNetwork();
const { data: connectorWalletClient } = useWalletClient();
const { chainId } = useAccount();

let walletClient;
if (network.publicClient.chain.id === chainId && connectorWalletClient?.chain.id === chainId) {
// TODO: Should this be memoized?
// `walletClient = connectorWalletClient.extend(burnerActions);` is unnecessary for an external wallet
walletClient = connectorWalletClient.extend(setupObserverActions(network.onWrite));
Comment on lines +16 to +17
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Here is the code for creating the custom client.
I've realized that nonce management and retry functionality are unnecessary (though they work) for external wallets. This is because MetaMask ignores the nonce value, and the retry behavior might not be desirable for external wallets.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we're still going to ~always have a session wallet as the primary sender of txs (to have ~invisible tx signing) so may make sense to just keep this here.

}

return { network, walletClient };
};

export type WalletClient = NonNullable<UseWalletClientReturnType["data"]>;

// See @latticexyz/common/src/sendTransaction.ts
const burnerActions = (client: WalletClient): Pick<WalletActions<Chain, Account>, "sendTransaction"> => {
// TODO: Use the `debug` library once this function has been moved to the `common` library.
const debug = console.log;

return {
sendTransaction: async (args) => {
Comment on lines +26 to +31
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This functions similarly to mud_writeContract and mud_sendTransaction through the viem client interactions, with the exception of transaction simulation. This approach is consistent with the standard viem action behavior, which requires users to initiate the simulation separately when needed.

const nonceManager = await getNonceManager({
client,
address: client.account.address,
blockTag: "pending",
});

return nonceManager.mempoolQueue.add(
() =>
pRetry(
async () => {
if (!nonceManager.hasNonce()) {
await nonceManager.resetNonce();
}

const nonce = nonceManager.nextNonce();
debug("sending tx with nonce", nonce, "to", args.to);
return sendTransaction(client, { ...args, nonce } as typeof args);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we should prob use getAction('sendTransaction') here instead of importing from viem, in case the client has overridden this method already

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

session wallet

Yes, we should use all custom actions (nonce, retry, write$) for session wallets.

In the upcoming burner wallet PR #2309, all these actions are applied to the burner wallet. When using both external and burner wallets, I believe the external wallet doesn't need to be extended. This is because, we don't require nonce management for MetaMask, and dev-tools, which takes a single wallet client, should be used with the burner wallet.


getAction

Right, using getAction is better since it accounts for more extensions beforehand. I'll switch to getAction.

},
{
retries: 3,
onFailedAttempt: async (error) => {
// On nonce errors, reset the nonce and retry
if (nonceManager.shouldResetNonce(error)) {
debug("got nonce error, retrying", error.message);
await nonceManager.resetNonce();
return;
}
// TODO: prepare again if there are gas errors?
throw error;
},
}
),
{ throwOnTimeout: true }
);
},
};
};

// See @latticexyz/common/src/getContract.ts
const setupObserverActions = (onWrite: MUDNetwork["onWrite"]) => {
return (client: WalletClient): Pick<WalletActions<Chain, Account>, "writeContract"> => ({
writeContract: async (args) => {
const result = writeContract(client, args);

const id = `${client.chain.id}:${client.account.address}`;
onWrite({ id, request: args as WriteContractParameters, result });

return result;
},
});
};
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can we pull these actions out into a separate, independent PR that adds them to the common package and import them here?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

will do! 💪

This file was deleted.

Loading
Loading