diff --git a/.changeset/gentle-jobs-visit.md b/.changeset/gentle-jobs-visit.md new file mode 100644 index 0000000000..35e1272944 --- /dev/null +++ b/.changeset/gentle-jobs-visit.md @@ -0,0 +1,5 @@ +--- +"wagmi": patch +--- + +Disabled `useConnectorClient` and `useWalletClient` during reconnection if connector is not fully restored. diff --git a/.changeset/lazy-shrimps-occur.md b/.changeset/lazy-shrimps-occur.md new file mode 100644 index 0000000000..a6f4987c62 --- /dev/null +++ b/.changeset/lazy-shrimps-occur.md @@ -0,0 +1,5 @@ +--- +"@wagmi/core": patch +--- + +Added guard to `getConnectorClient` when reconnecting to check if connector is fully restored. diff --git a/.changeset/long-pears-behave.md b/.changeset/long-pears-behave.md new file mode 100644 index 0000000000..033f467326 --- /dev/null +++ b/.changeset/long-pears-behave.md @@ -0,0 +1,5 @@ +--- +"@wagmi/vue": patch +--- + +Disabled `useConnectorClient` during reconnection if connector is not fully restored. diff --git a/packages/core/src/actions/getConnectorClient.test.ts b/packages/core/src/actions/getConnectorClient.test.ts index f0eec291df..dd1947b838 100644 --- a/packages/core/src/actions/getConnectorClient.test.ts +++ b/packages/core/src/actions/getConnectorClient.test.ts @@ -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' @@ -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/core@x.y.z] + `) + config.setState((state) => ({ ...state, status: 'disconnected' })) +}) diff --git a/packages/core/src/actions/getConnectorClient.ts b/packages/core/src/actions/getConnectorClient.ts index c613644d88..21751c94c6 100644 --- a/packages/core/src/actions/getConnectorClient.ts +++ b/packages/core/src/actions/getConnectorClient.ts @@ -17,6 +17,8 @@ import { type ConnectorChainMismatchErrorType, ConnectorNotConnectedError, type ConnectorNotConnectedErrorType, + ConnectorUnavailableReconnectingError, + type ConnectorUnavailableReconnectingErrorType, } from '../errors/config.js' import type { ChainIdParameter, @@ -51,6 +53,7 @@ export type GetConnectorClientErrorType = | ConnectorAccountNotFoundErrorType | ConnectorChainMismatchErrorType | ConnectorNotConnectedErrorType + | ConnectorUnavailableReconnectingErrorType // base | BaseErrorType | ErrorType @@ -67,6 +70,13 @@ export async function getConnectorClient< let connection: Connection | undefined if (parameters.connector) { const { connector } = parameters + if ( + config.state.status === 'reconnecting' && + !connector.getAccounts && + !connector.getChainId + ) + throw new ConnectorUnavailableReconnectingError({ connector }) + const [accounts, chainId] = await Promise.all([ connector.getAccounts(), connector.getChainId(), @@ -99,11 +109,6 @@ export async function getConnectorClient< 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 - } - // If account was provided, check that it exists on the connector if ( parameters.account && @@ -116,6 +121,11 @@ export async function getConnectorClient< connector, }) + const chain = config.chains.find((chain) => chain.id === chainId) + const provider = (await connection.connector.getProvider({ chainId })) as { + request(...args: any): Promise + } + return createClient({ account, chain, diff --git a/packages/core/src/errors/config.test.ts b/packages/core/src/errors/config.test.ts index 7d50f0907c..9a569290b9 100644 --- a/packages/core/src/errors/config.test.ts +++ b/packages/core/src/errors/config.test.ts @@ -8,6 +8,7 @@ import { ConnectorChainMismatchError, ConnectorNotConnectedError, ConnectorNotFoundError, + ConnectorUnavailableReconnectingError, } from './config.js' test('constructors', () => { @@ -54,4 +55,14 @@ test('constructors', () => { Version: @wagmi/core@x.y.z] `) + 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/core@x.y.z] + `) }) diff --git a/packages/core/src/errors/config.ts b/packages/core/src/errors/config.ts index 5c679498cf..46cd2cb18e 100644 --- a/packages/core/src/errors/config.ts +++ b/packages/core/src/errors/config.ts @@ -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(' '), + }) + } +} diff --git a/packages/core/src/exports/index.test.ts b/packages/core/src/exports/index.test.ts index 34c546e088..2d6953b9cc 100644 --- a/packages/core/src/exports/index.test.ts +++ b/packages/core/src/exports/index.test.ts @@ -90,6 +90,7 @@ test('exports', () => { "ConnectorNotFoundError", "ConnectorAccountNotFoundError", "ConnectorChainMismatchError", + "ConnectorUnavailableReconnectingError", "ProviderNotFoundError", "SwitchChainNotSupportedError", "custom", diff --git a/packages/core/src/exports/index.ts b/packages/core/src/exports/index.ts index 6b71048f7d..69dae7cc17 100644 --- a/packages/core/src/exports/index.ts +++ b/packages/core/src/exports/index.ts @@ -456,6 +456,7 @@ export { type Connector, type Config, type CreateConfigParameters, + type PartializedState, type State, type Transport, createConfig, @@ -498,6 +499,8 @@ export { ConnectorAccountNotFoundError, type ConnectorChainMismatchErrorType, ConnectorChainMismatchError, + type ConnectorUnavailableReconnectingErrorType, + ConnectorUnavailableReconnectingError, } from '../errors/config.js' export { diff --git a/packages/react/src/exports/index.test.ts b/packages/react/src/exports/index.test.ts index 4c94b9d149..cba2fa8d59 100644 --- a/packages/react/src/exports/index.test.ts +++ b/packages/react/src/exports/index.test.ts @@ -80,6 +80,8 @@ test('exports', () => { "ConnectorAlreadyConnectedError", "ConnectorNotFoundError", "ConnectorAccountNotFoundError", + "ConnectorChainMismatchError", + "ConnectorUnavailableReconnectingError", "ProviderNotFoundError", "SwitchChainNotSupportedError", "createStorage", diff --git a/packages/react/src/exports/index.ts b/packages/react/src/exports/index.ts index 583a4e1aad..7a08838ce4 100644 --- a/packages/react/src/exports/index.ts +++ b/packages/react/src/exports/index.ts @@ -399,6 +399,7 @@ export { type Connector, type Config, type CreateConfigParameters, + type PartializedState, type State, createConfig, // Connector @@ -414,6 +415,10 @@ export { ConnectorNotFoundError, type ConnectorAccountNotFoundErrorType, ConnectorAccountNotFoundError, + type ConnectorChainMismatchErrorType, + ConnectorChainMismatchError, + type ConnectorUnavailableReconnectingErrorType, + ConnectorUnavailableReconnectingError, type ProviderNotFoundErrorType, ProviderNotFoundError, type SwitchChainNotSupportedErrorType, diff --git a/packages/react/src/hooks/useConnectorClient.ts b/packages/react/src/hooks/useConnectorClient.ts index 8d3332e41e..16bbf33ae0 100644 --- a/packages/react/src/hooks/useConnectorClient.ts +++ b/packages/react/src/hooks/useConnectorClient.ts @@ -72,6 +72,7 @@ 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, @@ -79,10 +80,11 @@ export function useConnectorClient< >(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), ) diff --git a/packages/react/src/hooks/useWalletClient.ts b/packages/react/src/hooks/useWalletClient.ts index a045eee5d3..24a3bccdda 100644 --- a/packages/react/src/hooks/useWalletClient.ts +++ b/packages/react/src/hooks/useWalletClient.ts @@ -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, @@ -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 diff --git a/packages/vue/src/composables/useConnectorClient.ts b/packages/vue/src/composables/useConnectorClient.ts index f63365257b..08735718bf 100644 --- a/packages/vue/src/composables/useConnectorClient.ts +++ b/packages/vue/src/composables/useConnectorClient.ts @@ -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 { diff --git a/packages/vue/src/exports/index.test.ts b/packages/vue/src/exports/index.test.ts index d388244a2e..70fadc9db6 100644 --- a/packages/vue/src/exports/index.test.ts +++ b/packages/vue/src/exports/index.test.ts @@ -49,6 +49,7 @@ test('exports', () => { "ConnectorNotFoundError", "ConnectorAccountNotFoundError", "ConnectorChainMismatchError", + "ConnectorUnavailableReconnectingError", "ProviderNotFoundError", "SwitchChainNotSupportedError", "createStorage", diff --git a/packages/vue/src/exports/index.ts b/packages/vue/src/exports/index.ts index 90cedd340d..11e78b04db 100644 --- a/packages/vue/src/exports/index.ts +++ b/packages/vue/src/exports/index.ts @@ -223,6 +223,7 @@ export { type Connector, type Config, type CreateConfigParameters, + type PartializedState, type State, createConfig, // Connector @@ -240,6 +241,8 @@ export { ConnectorAccountNotFoundError, type ConnectorChainMismatchErrorType, ConnectorChainMismatchError, + type ConnectorUnavailableReconnectingErrorType, + ConnectorUnavailableReconnectingError, type ProviderNotFoundErrorType, ProviderNotFoundError, type SwitchChainNotSupportedErrorType, diff --git a/site/shared/errors.md b/site/shared/errors.md index c410ad8a2f..c518172fc7 100644 --- a/site/shared/errors.md +++ b/site/shared/errors.md @@ -15,14 +15,6 @@ import { BaseError } from '{{packageName}}' ## Config -### ChainNotConfiguredError - -When a chain is not configured. You likely need to add the chain to `Config['chains']`. - -```ts-vue -import { ChainNotConfiguredError } from '{{packageName}}' -``` - ### ConnectorAccountNotFoundError When an account does not exist on the connector or is unable to be used. @@ -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 `Config['chains']`. ```ts-vue -import { ConnectorChainMismatchError } from '{{packageName}}' +import { ChainNotConfiguredError } from '{{packageName}}' +``` + +### ConnectorNotConnectedError + +When a connector is not connected. + +```ts-vue +import { ConnectorNotConnectedError } from '{{packageName}}' ``` ### ConnectorNotFoundError @@ -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