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: guard (get|use)ConnectorClient when reconnecting #4259

Merged
merged 3 commits into from
Sep 10, 2024
Merged
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
5 changes: 5 additions & 0 deletions .changeset/gentle-jobs-visit.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"wagmi": patch
---

Disabled `useConnectorClient` and `useWalletClient` during reconnection if connector is not fully restored.
5 changes: 5 additions & 0 deletions .changeset/lazy-shrimps-occur.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@wagmi/core": patch
---

Added guard to `getConnectorClient` when reconnecting to check if connector is fully restored.
5 changes: 5 additions & 0 deletions .changeset/long-pears-behave.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@wagmi/vue": patch
---

Disabled `useConnectorClient` during reconnection if connector is not fully restored.
22 changes: 22 additions & 0 deletions packages/core/src/actions/getConnectorClient.test.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { address, config } from '@wagmi/test'
import { expect, test } from 'vitest'

import type { Connector } from '../createConfig.js'
import { connect } from './connect.js'
import { disconnect } from './disconnect.js'
import { getConnectorClient } from './getConnectorClient.js'
Expand Down Expand Up @@ -82,3 +83,24 @@ test('behavior: account does not exist on connector', async () => {
`)
await disconnect(config, { connector })
})

test('behavior: reconnecting', async () => {
config.setState((state) => ({ ...state, status: 'reconnecting' }))
const { id, name, type, uuid } = connector
await expect(
getConnectorClient(config, {
connector: {
id,
name,
type,
uuid,
} as unknown as Connector,
}),
).rejects.toThrowErrorMatchingInlineSnapshot(`
[ConnectorUnavailableReconnectingError: Connector "Mock Connector" unavailable while reconnecting.

Details: During the reconnection step, the only connector methods guaranteed to be available are: \`id\`, \`name\`, \`type\`, \`uuid\`. All other methods are not guaranteed to be available until reconnection completes and connectors are fully restored. This error commonly occurs for connectors that asynchronously inject after reconnection has already started.
Version: @wagmi/[email protected]]
`)
config.setState((state) => ({ ...state, status: 'disconnected' }))
})
20 changes: 15 additions & 5 deletions packages/core/src/actions/getConnectorClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@
type ConnectorChainMismatchErrorType,
ConnectorNotConnectedError,
type ConnectorNotConnectedErrorType,
ConnectorUnavailableReconnectingError,
type ConnectorUnavailableReconnectingErrorType,
} from '../errors/config.js'
import type {
ChainIdParameter,
Expand Down Expand Up @@ -51,6 +53,7 @@
| ConnectorAccountNotFoundErrorType
| ConnectorChainMismatchErrorType
| ConnectorNotConnectedErrorType
| ConnectorUnavailableReconnectingErrorType
// base
| BaseErrorType
| ErrorType
Expand All @@ -67,6 +70,13 @@
let connection: Connection | undefined
if (parameters.connector) {
const { connector } = parameters
if (
config.state.status === 'reconnecting' &&
!connector.getAccounts &&
!connector.getChainId

Check warning on line 76 in packages/core/src/actions/getConnectorClient.ts

View check run for this annotation

Codecov / codecov/patch

packages/core/src/actions/getConnectorClient.ts#L76

Added line #L76 was not covered by tests
)
throw new ConnectorUnavailableReconnectingError({ connector })

const [accounts, chainId] = await Promise.all([
connector.getAccounts(),
connector.getChainId(),
Expand Down Expand Up @@ -99,11 +109,6 @@
const account = parseAccount(parameters.account ?? connection.accounts[0]!)
account.address = getAddress(account.address) // TODO: Checksum address as part of `parseAccount`?

const chain = config.chains.find((chain) => chain.id === chainId)
const provider = (await connection.connector.getProvider({ chainId })) as {
request(...args: any): Promise<any>
}

// If account was provided, check that it exists on the connector
if (
parameters.account &&
Expand All @@ -116,6 +121,11 @@
connector,
})

const chain = config.chains.find((chain) => chain.id === chainId)
const provider = (await connection.connector.getProvider({ chainId })) as {
request(...args: any): Promise<any>
}

return createClient({
account,
chain,
Expand Down
11 changes: 11 additions & 0 deletions packages/core/src/errors/config.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import {
ConnectorChainMismatchError,
ConnectorNotConnectedError,
ConnectorNotFoundError,
ConnectorUnavailableReconnectingError,
} from './config.js'

test('constructors', () => {
Expand Down Expand Up @@ -54,4 +55,14 @@ test('constructors', () => {

Version: @wagmi/[email protected]]
`)
expect(
new ConnectorUnavailableReconnectingError({
connector: { name: 'Rabby Wallet' },
}),
).toMatchInlineSnapshot(`
[ConnectorUnavailableReconnectingError: Connector "Rabby Wallet" unavailable while reconnecting.

Details: During the reconnection step, the only connector methods guaranteed to be available are: \`id\`, \`name\`, \`type\`, \`uuid\`. All other methods are not guaranteed to be available until reconnection completes and connectors are fully restored. This error commonly occurs for connectors that asynchronously inject after reconnection has already started.
Version: @wagmi/[email protected]]
`)
})
17 changes: 17 additions & 0 deletions packages/core/src/errors/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -84,3 +84,20 @@ export class ConnectorChainMismatchError extends BaseError {
)
}
}

export type ConnectorUnavailableReconnectingErrorType =
ConnectorUnavailableReconnectingError & {
name: 'ConnectorUnavailableReconnectingError'
}
export class ConnectorUnavailableReconnectingError extends BaseError {
override name = 'ConnectorUnavailableReconnectingError'
constructor({ connector }: { connector: { name: string } }) {
super(`Connector "${connector.name}" unavailable while reconnecting.`, {
details: [
'During the reconnection step, the only connector methods guaranteed to be available are: `id`, `name`, `type`, `uuid`.',
'All other methods are not guaranteed to be available until reconnection completes and connectors are fully restored.',
'This error commonly occurs for connectors that asynchronously inject after reconnection has already started.',
].join(' '),
})
}
}
1 change: 1 addition & 0 deletions packages/core/src/exports/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,7 @@ test('exports', () => {
"ConnectorNotFoundError",
"ConnectorAccountNotFoundError",
"ConnectorChainMismatchError",
"ConnectorUnavailableReconnectingError",
"ProviderNotFoundError",
"SwitchChainNotSupportedError",
"custom",
Expand Down
3 changes: 3 additions & 0 deletions packages/core/src/exports/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -456,6 +456,7 @@ export {
type Connector,
type Config,
type CreateConfigParameters,
type PartializedState,
type State,
type Transport,
createConfig,
Expand Down Expand Up @@ -498,6 +499,8 @@ export {
ConnectorAccountNotFoundError,
type ConnectorChainMismatchErrorType,
ConnectorChainMismatchError,
type ConnectorUnavailableReconnectingErrorType,
ConnectorUnavailableReconnectingError,
} from '../errors/config.js'

export {
Expand Down
2 changes: 2 additions & 0 deletions packages/react/src/exports/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,8 @@ test('exports', () => {
"ConnectorAlreadyConnectedError",
"ConnectorNotFoundError",
"ConnectorAccountNotFoundError",
"ConnectorChainMismatchError",
"ConnectorUnavailableReconnectingError",
"ProviderNotFoundError",
"SwitchChainNotSupportedError",
"createStorage",
Expand Down
5 changes: 5 additions & 0 deletions packages/react/src/exports/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -399,6 +399,7 @@ export {
type Connector,
type Config,
type CreateConfigParameters,
type PartializedState,
type State,
createConfig,
// Connector
Expand All @@ -414,6 +415,10 @@ export {
ConnectorNotFoundError,
type ConnectorAccountNotFoundErrorType,
ConnectorAccountNotFoundError,
type ConnectorChainMismatchErrorType,
ConnectorChainMismatchError,
type ConnectorUnavailableReconnectingErrorType,
ConnectorUnavailableReconnectingError,
type ProviderNotFoundErrorType,
ProviderNotFoundError,
type SwitchChainNotSupportedErrorType,
Expand Down
6 changes: 4 additions & 2 deletions packages/react/src/hooks/useConnectorClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -72,17 +72,19 @@ export function useConnectorClient<
const queryClient = useQueryClient()
const { address, connector, status } = useAccount({ config })
const chainId = useChainId({ config })
const activeConnector = parameters.connector ?? connector

const { queryKey, ...options } = getConnectorClientQueryOptions<
config,
chainId
>(config, {
...parameters,
chainId: parameters.chainId ?? chainId,
connector: parameters.connector ?? connector,
connector: activeConnector,
})
const enabled = Boolean(
(status === 'connected' || status === 'reconnecting') &&
(status === 'connected' ||
(status === 'reconnecting' && activeConnector?.getProvider)) &&
(query.enabled ?? true),
)

Expand Down
7 changes: 6 additions & 1 deletion packages/react/src/hooks/useWalletClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,7 @@ export function useWalletClient<
const queryClient = useQueryClient()
const { address, connector, status } = useAccount({ config })
const chainId = useChainId({ config })
const activeConnector = parameters.connector ?? connector

const { queryKey, ...options } = getWalletClientQueryOptions<config, chainId>(
config,
Expand All @@ -84,7 +85,11 @@ export function useWalletClient<
connector: parameters.connector ?? connector,
},
)
const enabled = Boolean(status !== 'disconnected' && (query.enabled ?? true))
const enabled = Boolean(
(status === 'connected' ||
(status === 'reconnecting' && activeConnector?.getProvider)) &&
(query.enabled ?? true),
)

const addressRef = useRef(address)
// biome-ignore lint/correctness/useExhaustiveDependencies: `queryKey` not required
Expand Down
3 changes: 2 additions & 1 deletion packages/vue/src/composables/useConnectorClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -97,7 +97,8 @@ export function useConnectorClient<
connector: connector as Connector,
})
const enabled = Boolean(
(status.value === 'connected' || status.value === 'reconnecting') &&
(status.value === 'connected' ||
(status.value === 'reconnecting' && connector?.getProvider)) &&
(query.enabled ?? true),
)
return {
Expand Down
1 change: 1 addition & 0 deletions packages/vue/src/exports/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ test('exports', () => {
"ConnectorNotFoundError",
"ConnectorAccountNotFoundError",
"ConnectorChainMismatchError",
"ConnectorUnavailableReconnectingError",
"ProviderNotFoundError",
"SwitchChainNotSupportedError",
"createStorage",
Expand Down
3 changes: 3 additions & 0 deletions packages/vue/src/exports/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -223,6 +223,7 @@ export {
type Connector,
type Config,
type CreateConfigParameters,
type PartializedState,
type State,
createConfig,
// Connector
Expand All @@ -240,6 +241,8 @@ export {
ConnectorAccountNotFoundError,
type ConnectorChainMismatchErrorType,
ConnectorChainMismatchError,
type ConnectorUnavailableReconnectingErrorType,
ConnectorUnavailableReconnectingError,
type ProviderNotFoundErrorType,
ProviderNotFoundError,
type SwitchChainNotSupportedErrorType,
Expand Down
36 changes: 22 additions & 14 deletions site/shared/errors.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,14 +15,6 @@ import { BaseError } from '{{packageName}}'

## Config

### ChainNotConfiguredError

When a chain is not configured. You likely need to add the chain to <a :href="`/${docsPath}/api/createConfig#chains`">`Config['chains']`</a>.

```ts-vue
import { ChainNotConfiguredError } from '{{packageName}}'
```

### ConnectorAccountNotFoundError

When an account does not exist on the connector or is unable to be used.
Expand All @@ -39,20 +31,28 @@ When a connector is already connected.
import { ConnectorAlreadyConnectedError } from '{{packageName}}'
```

### ConnectorNotConnectedError
### ConnectorChainMismatchError

When a connector is not connected.
When the Wagmi Config is out-of-sync with the connector's active chain ID. This is rare and likely an upstream wallet issue.

```ts-vue
import { ConnectorNotConnectedError } from '{{packageName}}'
import { ConnectorChainMismatchError } from '{{packageName}}'
```

### ConnectorChainMismatchError
### ChainNotConfiguredError

When the Wagmi Config is out-of-sync with the connector's active chain ID. This is rare and likely an upstream wallet issue.
When a chain is not configured. You likely need to add the chain to <a :href="`/${docsPath}/api/createConfig#chains`">`Config['chains']`</a>.

```ts-vue
import { ConnectorChainMismatchError } from '{{packageName}}'
import { ChainNotConfiguredError } from '{{packageName}}'
```

### ConnectorNotConnectedError

When a connector is not connected.

```ts-vue
import { ConnectorNotConnectedError } from '{{packageName}}'
```

### ConnectorNotFoundError
Expand All @@ -63,6 +63,14 @@ When a connector is not found or able to be used.
import { ConnectorNotFoundError } from '{{packageName}}'
```

### ConnectorUnavailableReconnectingError

During the reconnection step, the only connector methods guaranteed to be available are: `id`, `name`, `type`, `uuid`. All other methods are not guaranteed to be available until reconnection completes and connectors are fully restored. This error commonly occurs for connectors that asynchronously inject after reconnection has already started.

```ts-vue
import { ConnectorUnavailableReconnectingError } from '{{packageName}}'
```

## Connector

### ProviderNotFoundError
Expand Down
Loading