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

fix: use up-to-date chainId/accounts when querying EIP1193-derived wallets #749

Merged
merged 10 commits into from
Feb 1, 2023

Conversation

zzmp
Copy link
Contributor

@zzmp zzmp commented Feb 1, 2023

Serially requests accounts and then chainId, so that if a user changes chains whilst connecting, the new chain (after connection / accounts) will be used. Practically, this prevents web3-react from using incorrect initial state when connecting to certain wallets, in certain cases where the user selects a non-active chain. This is resolved by waiting for accounts to settle before requesting chainId.


For chainId and accounts, the values were fetched using a Promise.all. In some wallets, chainId resolves immediately, and then the user is prompted to change chains. In these cases, chainId will be outdated by the time accounts resolves:

  1. eth_chainId resolves to "stale" chain
  2. user is prompted to connect with a chain selector
  3. user selects a different "active" chain
  4. chainChanged is emitted
  5. eth_accounts resolves, resolving the Promise.all with an out-of-date chainId

NB: WalletConnect did not exhibit this bug, but I updated it to keep it in sync with other connectors' idioms. It likely already had this "fix" in it, based on existing comments.

@vercel
Copy link

vercel bot commented Feb 1, 2023

The latest updates on your projects. Learn more about Vercel for Git ↗︎

Name Status Preview Comments Updated
web3-react ✅ Ready (Inspect) Visit Preview 💬 Add your feedback Feb 1, 2023 at 11:02PM (UTC)

@zzmp zzmp requested a review from grabbou February 1, 2023 08:47
Copy link
Contributor

@NoahZinsmeister NoahZinsmeister left a comment

Choose a reason for hiding this comment

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

wow this is really subtle, nice catch. i have a concern about the metamask change, see comment

the non-null assertions felt a little clunky but i went through them all and i didn't see anything problematic; it's maybe slightly worse because we're now relying on the coinbase wallet/metamask providers to set properties correctly, but doesn't really bother me

packages/metamask/src/index.ts Outdated Show resolved Hide resolved
packages/metamask/src/index.ts Show resolved Hide resolved
Copy link
Contributor

@NoahZinsmeister NoahZinsmeister left a comment

Choose a reason for hiding this comment

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

lgtm, nice catch

Copy link
Contributor

@grabbou grabbou left a comment

Choose a reason for hiding this comment

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

Looks good to me! Left some overall nits / comments, but nothing blocking

packages/coinbase-wallet/src/index.ts Show resolved Hide resolved
// Wallets may resolve eth_chainId and hang on eth_accounts pending user interaction, which may include changing
// chains; they should be requested serially, with accounts first, so that the chainId can settle.
const accounts = await this.provider.request<string[]>({ method: 'eth_requestAccounts' })
const chainId = await this.provider.request<string>({ method: 'eth_chainId' })
Copy link
Contributor

Choose a reason for hiding this comment

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

nit: probably out of scope for this PR, but I would consider adding to a todo list: potential candidate for abstracting away? Looks like a common code path here and few lines earlier.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I think that web3-react is the abstraction but, more importantly, I don't want to make this a large change because it's meant to be a bug fix.

try {
// Wallets may resolve eth_chainId and hang on eth_accounts pending user interaction, which may include changing
// chains; they should be requested serially, with accounts first, so that the chainId can settle.
const accounts = await (eager ? this.provider.request({ method: 'eth_accounts' }) : this.provider
Copy link
Contributor

Choose a reason for hiding this comment

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

Question about this eager flag. I don't see it being used before? We were running eth_accounts in both instances. Is this change intentional?

Copy link
Contributor

Choose a reason for hiding this comment

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

Ok, I see this has to do with eager that can be true or false, depending on the call-site. Is there a better way to describe its intent? Is it something like connect if already authorised vs explicitly authorise? In that case, why fallback to eth_accounts when eth_requestAccounts failed? This is probably a bit out of scope for this PR, but a good opportunity to expand my web3-react knowledge a bit!

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I believe this is done because EIP1193 does not necessarily support eth_requestAccounts, so a fallback is necessary. You don't see this in other connectors because they are not explicitly EIP1193 connectors.

The codepaths for connectEagerly and activate were identical except for the usage of eth_accounts vs eth_requestAccounts, so I added connect as a shared method to express that with less code duplication.

I've refactored to activateAccounts(requestAccounts: () => Promise<string[]>).

.then(() => this.activate(desiredChainId))
// Wallets may resolve eth_chainId and hang on eth_accounts pending user interaction, which may include changing
// chains; they should be requested serially, with accounts first, so that the chainId can settle.
const accounts = await this.provider.request({ method: 'eth_requestAccounts' }) as string[]
Copy link
Contributor

Choose a reason for hiding this comment

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

follow-up to my previous question: why no fallback to eth_accounts 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.

MetaMask is known to implement this method, whereas a pure EIP1193 may not.

packages/walletconnect/src/index.ts Show resolved Hide resolved

this.actions.update({ chainId, accounts })
this.actions.update({ chainId: parseChainId(chainId), accounts })
Copy link
Contributor

Choose a reason for hiding this comment

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

nit: maybe chainId should already be parsed? parseChainId(await ....), otherwise I think this line is a bit confusing: chainId: parseChainId(chainId) vs this.actions.update({ chainId })

Copy link
Contributor

Choose a reason for hiding this comment

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

If considered a worthy nit, this most likely applies to other files too.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

fwiw, I agree with you here, but I don't want to change it because it expands the scope of this PR - I've omitted it to de-risk this bug fix.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants