-
Notifications
You must be signed in to change notification settings - Fork 196
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
Changes from all commits
f2ddcc7
59960ad
999720c
b658666
51c0e25
baa581a
583ae77
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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", | ||
"contracts": "workspace:*", | ||
"p-retry": "^5.1.2", | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. To adapt the transaction sending extension in this example, |
||
"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", | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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"; | ||
|
@@ -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
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Here is the code for creating the custom client. There was a problem hiding this comment. Choose a reason for hiding this commentThe 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
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This functions similarly to |
||
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); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. we should prob use There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
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.
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; | ||
}, | ||
}); | ||
}; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. will do! 💪 |
This file was deleted.
There was a problem hiding this comment.
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.