-
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): add external wallet support example #2268
Conversation
|
ad34bd5
to
07d7eaf
Compare
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.
OOC why is this example app set up differently from the others? (setup.tsx
, calling syncStore
in App
etc.)
I would expect /mud
to be mostly unchanged, but maybe the setup has to be different for external wallets.
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.
Thank you for the comment!
While introducing the top-level WagmiConfig
, the distinction between the synchronous network setup and the asynchronous store setup became apparent, which resulted in the diff. The benefit of this PR structure is it doesn't wait for the sync fn for non-relevant code.
Perhaps I should retain the setup().then(async (result) => { root.render( ...
code?
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.
I don't think it needs to follow the other examples exactly (though I defer to @holic for that), but it would be interesting to explain the set up maybe in the PR description 🙌
In the existing templates, there is a certainty that wallets (burner wallets) are present and do not change throughout the app, at the setup stage. As a result, all underlying components have access to the fixed wallets and transaction interfaces. However, with an external wallet setup, the wallet is not fixed (or empty), and it can change. Also, we don't want to block the UI as much as possible for users who do not have wallets. This contributes to the difference in structure. Another solution might be preparing a burner wallet at first anyway and then later replacing it with an injected wallet. |
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.
Another solution might be preparing a burner wallet at first anyway and then later replacing it with an injected wallet.
I don't love this, especially on real chains where the burner wallet would be empty and not able to pay gas anyway.
I think we're better off changing the overall structure to get folks thinking more about only doing writes when a wallet is connected.
I have a bunch of comments related to this but realizing this is quite a big lift. Maybe we should sketch out some designs for our ideal React component tree and render flow?
|
||
useEffect(() => { | ||
syncStore(networkConfig, publicClient).then(setStore); | ||
}, []); |
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.
Should this useEffect
have networkConfig
and publicClient
as dependencies so it can be reevaluated if the e.g. wallet changes?
Is there a way we can do this without the useEffect
?
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.
Right, I should take into account the config and client for this.
}); | ||
|
||
return { worldContract, write$ }; | ||
}; |
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.
I wonder if we could separate the public/wallet clients some more, so that the public client doesn't depend on the wallet (since it's just used to read from RPC).
It would involve some rethinking this worldContract
approach, maybe separating it into two: readWorldContract
and writeWorldContract
or something. I don't love this though.
The main reason we even use this contract instance abstraction is so that we can overwrite some internal behavior, like adding better tx queuing and nonce handling.
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.
I wonder if we can achieve the same goals with a custom viem client that overrides these methods and pass that custom client through to wagmi, etc.
Would need to make sure that calling various viem actions or wagmi hooks would lean on the custom client behavior rather than other viem internals
)} | ||
</> | ||
); | ||
}; |
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.
The way these various parts are separated is a little hard for me to grok. I wonder if there are other approaches that would make this easier to understand.
One pattern we've used in some apps is a loading screen that is shown while we're syncing data from the chain or indexer. We could use that here while waiting for a public client.
And like mentioned above, we could separate the things that depend on the wallet client so it doesn't block showing the UI, etc.
useEffect(() => { | ||
if (import.meta.env.DEV) { | ||
import("@latticexyz/dev-tools").then(({ mount }) => | ||
mount({ |
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.
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.
I didn't notice that issue, but you're right. I'll look into it.
@holic Thank you so much for your comment! I'll create a design sketch to reevaluate the structure. I'll also reconsider how we manage the transaction interface. |
EDIT: This comment is now outdated and can be ignored. As you both mentioned, the React structure can be much simpler. Should be something like this:
I'll look into the viem's "getContract" / sending transaction matter. (custom client) EDIT: The above store doesn't support the network related changes (e.g., chain, world). Should be something like this:
|
> React Hook "useEffect" is called conditionally. React Hooks must be called in the exact same order in every component render
I've updated the React structure as follows: <WagmiConfig config={wagmiConfig}> // manages external wallet
<ExternalWallet /> // displays the external wallet UI
<MUDReadProvider value={mud}> // manages read and immutable mud states (chain, world, publicClient, syncToZustand's result, etc.)
<MUDWriteProvider> // manages write mud state (transaction interface, etc.)
<App /> // and its children:
mudRead = useMUDRead() // can be used similarly to the former `useMUD().network`
mudWrite = useMUDWrite() // can be used similarly to the former `useMUD().systemCalls` Also, the following issues have been addressed:
Other considerations:
I've not yet explored viem v2/custom client and am still using I believe I've addressed all comments except for those regarding the v2/custom client. Could you please take a look? @holic |
Sketching out my thoughts: If we move to a viem custom client, we can remove the need for // extend viem client with our own tx queue with nonce handling (like `getContract` does today)
const client = createWalletClient({ ... }).extend(transactionQueue());
await callSomeSystem(client, ...); Ultimately trying to move away from "bound" functions and objects into a more functional approach where we pass in the args, which means we can lean on more libs out of the box (i.e. viem/wagmi actions) instead of wrappers and threading data through in complex data flows. Then we could keep the "MUD read context" focused more on what it's actually doing: setting up the sync stack to hydrate the client with data you can query from. |
@holic I agree with your points! Yes, by having the custom wallet client, we can remove the const mudWalletActions = (client) => ({sendTransaction: (...) => {...}})
const customWalletClient = walletClient.extend(mudWalletActions) Since Wagmi hooks don't retain the Inside the |
I've merged main and now have viem v2 (#2284) in this branch. |
Closing this in favor of #2306 |
This React example project uses external wallets instead of burner wallets. It currently employs Wagmi v1, but the same component structure should be usable with Wagmi v2 or other libraries.
The example works as expected, except when multiple wallets are installed, which will be resolved in v2. Users must transfer some balance to external wallet accounts for transaction fees beforehand.
out.mp4