Skip to content

Commit 8dc47b9

Browse files
committed
fix(core): catch connector.isAuthorized() rejection in reconnect and revalidate
Fixes #5233 Signed-off-by: Liang Xu <lx3133584@users.noreply.github.com>
1 parent 6add619 commit 8dc47b9

29 files changed

Lines changed: 851 additions & 2823 deletions

packages/connectors/package.json

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,8 +32,68 @@
3232
"types": "./dist/types/exports/index.d.ts",
3333
"default": "./dist/esm/exports/index.js"
3434
},
35+
"./baseAccount": {
36+
"types": "./dist/types/exports/baseAccount.d.ts",
37+
"default": "./dist/esm/exports/baseAccount.js"
38+
},
39+
"./coinbaseWallet": {
40+
"types": "./dist/types/exports/coinbaseWallet.d.ts",
41+
"default": "./dist/esm/exports/coinbaseWallet.js"
42+
},
43+
"./injected": {
44+
"types": "./dist/types/exports/injected.d.ts",
45+
"default": "./dist/esm/exports/injected.js"
46+
},
47+
"./metaMask": {
48+
"types": "./dist/types/exports/metaMask.d.ts",
49+
"default": "./dist/esm/exports/metaMask.js"
50+
},
51+
"./mock": {
52+
"types": "./dist/types/exports/mock.d.ts",
53+
"default": "./dist/esm/exports/mock.js"
54+
},
55+
"./safe": {
56+
"types": "./dist/types/exports/safe.d.ts",
57+
"default": "./dist/esm/exports/safe.js"
58+
},
59+
"./tempoWallet": {
60+
"types": "./dist/types/exports/tempoWallet.d.ts",
61+
"default": "./dist/esm/exports/tempoWallet.js"
62+
},
63+
"./walletConnect": {
64+
"types": "./dist/types/exports/walletConnect.d.ts",
65+
"default": "./dist/esm/exports/walletConnect.js"
66+
},
3567
"./package.json": "./package.json"
3668
},
69+
"typesVersions": {
70+
"*": {
71+
"baseAccount": [
72+
"./dist/types/exports/baseAccount.d.ts"
73+
],
74+
"coinbaseWallet": [
75+
"./dist/types/exports/coinbaseWallet.d.ts"
76+
],
77+
"injected": [
78+
"./dist/types/exports/injected.d.ts"
79+
],
80+
"metaMask": [
81+
"./dist/types/exports/metaMask.d.ts"
82+
],
83+
"mock": [
84+
"./dist/types/exports/mock.d.ts"
85+
],
86+
"safe": [
87+
"./dist/types/exports/safe.d.ts"
88+
],
89+
"tempoWallet": [
90+
"./dist/types/exports/tempoWallet.d.ts"
91+
],
92+
"walletConnect": [
93+
"./dist/types/exports/walletConnect.d.ts"
94+
]
95+
}
96+
},
3797
"peerDependencies": {
3898
"@base-org/account": "^2.5.1",
3999
"@coinbase/wallet-sdk": "^4.3.6",
Lines changed: 9 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,9 @@
1-
// biome-ignore lint/performance/noBarrelFile: entrypoint module
2-
export {
3-
type InjectedParameters,
4-
injected,
5-
type MockParameters,
6-
mock,
7-
} from '@wagmi/core'
8-
export { type TempoWalletParameters, tempoWallet } from '@wagmi/core/tempo'
9-
export { type BaseAccountParameters, baseAccount } from '../baseAccount.js'
10-
export {
11-
type CoinbaseWalletParameters,
12-
coinbaseWallet,
13-
} from '../coinbaseWallet.js'
14-
export { type MetaMaskParameters, metaMask } from '../metaMask.js'
15-
export { type SafeParameters, safe } from '../safe.js'
16-
export { version } from '../version.js'
17-
export {
18-
type WalletConnectParameters,
19-
walletConnect,
20-
} from '../walletConnect.js'
1+
export { type InjectedParameters, injected, type MockParameters, mock, } from '@wagmi/core';
2+
export { type TempoWalletParameters, tempoWallet } from '@wagmi/core/tempo';
3+
export { type BaseAccountParameters, baseAccount } from '../baseAccount.js';
4+
export { type CoinbaseWalletParameters, coinbaseWallet, } from '../coinbaseWallet.js';
5+
export { type MetaMaskParameters, metaMask } from '../metaMask.js';
6+
export { type SafeParameters, safe } from '../safe.js';
7+
export { version } from '../version.js';
8+
export { type WalletConnectParameters, walletConnect, } from '../walletConnect.js';
9+
//# sourceMappingURL=index.d.ts.map

packages/core/src/actions/reconnect.test.ts

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,31 @@ test("behavior: doesn't reconnect if already reconnecting", async () => {
7272
config.setState((x) => ({ ...x, status: previousStatus }))
7373
})
7474

75+
test('behavior: connector.isAuthorized() rejects', async () => {
76+
const rejectingConnector = config._internal.connectors.setup(
77+
mock({
78+
accounts,
79+
features: {
80+
reconnect: true,
81+
defaultConnected: true,
82+
},
83+
}),
84+
)
85+
rejectingConnector.isAuthorized = async () => {
86+
throw new Error('stale session')
87+
}
88+
89+
await expect(
90+
reconnect(config, { connectors: [rejectingConnector] }),
91+
).resolves.toStrictEqual([])
92+
expect(config.state.status).toEqual('disconnected')
93+
94+
// subsequent reconnect should still be able to run
95+
await expect(
96+
reconnect(config, { connectors: [connector] }),
97+
).resolves.toStrictEqual([])
98+
})
99+
75100
test('behavior: recovers from invalid state', async () => {
76101
const state = {
77102
'wagmi.store': JSON.stringify({

packages/core/src/actions/reconnect.ts

Lines changed: 98 additions & 95 deletions
Original file line numberDiff line numberDiff line change
@@ -25,103 +25,106 @@ export async function reconnect(
2525
if (isReconnecting) return []
2626
isReconnecting = true
2727

28-
config.setState((x) => ({
29-
...x,
30-
status: x.current ? 'reconnecting' : 'connecting',
31-
}))
32-
33-
const connectors: Connector[] = []
34-
if (parameters.connectors?.length) {
35-
for (const connector_ of parameters.connectors) {
36-
let connector: Connector
37-
// "Register" connector if not already created
38-
if (typeof connector_ === 'function')
39-
connector = config._internal.connectors.setup(connector_)
40-
else connector = connector_
41-
connectors.push(connector)
42-
}
43-
} else connectors.push(...config.connectors)
44-
45-
// Try recently-used connectors first
46-
let recentConnectorId: string | null | undefined
4728
try {
48-
recentConnectorId = await config.storage?.getItem('recentConnectorId')
49-
} catch {}
50-
const scores: Record<string, number> = {}
51-
for (const [, connection] of config.state.connections) {
52-
scores[connection.connector.id] = 1
53-
}
54-
if (recentConnectorId) scores[recentConnectorId] = 0
55-
const sorted =
56-
Object.keys(scores).length > 0
57-
? // .toSorted()
58-
[...connectors].sort(
59-
(a, b) => (scores[a.id] ?? 10) - (scores[b.id] ?? 10),
60-
)
61-
: connectors
62-
63-
// Iterate through each connector and try to connect
64-
let connected = false
65-
const connections: Connection[] = []
66-
const providers: unknown[] = []
67-
for (const connector of sorted) {
68-
const provider = await connector.getProvider().catch(() => undefined)
69-
if (!provider) continue
70-
71-
// If we already have an instance of this connector's provider,
72-
// then we have already checked it (ie. injected connectors can
73-
// share the same `window.ethereum` instance, so we don't want to
74-
// connect to it again).
75-
if (providers.some((x) => x === provider)) continue
76-
77-
const isAuthorized = await connector.isAuthorized()
78-
if (!isAuthorized) continue
79-
80-
const data = await connector
81-
.connect({ isReconnecting: true })
82-
.catch(() => null)
83-
if (!data) continue
84-
85-
connector.emitter.off('connect', config._internal.events.connect)
86-
connector.emitter.on('change', config._internal.events.change)
87-
connector.emitter.on('disconnect', config._internal.events.disconnect)
88-
89-
config.setState((x) => {
90-
const connections = new Map(connected ? x.connections : new Map()).set(
91-
connector.uid,
92-
{ accounts: data.accounts, chainId: data.chainId, connector },
93-
)
94-
return {
95-
...x,
96-
current: connected ? x.current : connector.uid,
97-
connections,
29+
config.setState((x) => ({
30+
...x,
31+
status: x.current ? 'reconnecting' : 'connecting',
32+
}))
33+
34+
const connectors: Connector[] = []
35+
if (parameters.connectors?.length) {
36+
for (const connector_ of parameters.connectors) {
37+
let connector: Connector
38+
// "Register" connector if not already created
39+
if (typeof connector_ === 'function')
40+
connector = config._internal.connectors.setup(connector_)
41+
else connector = connector_
42+
connectors.push(connector)
9843
}
99-
})
100-
connections.push({
101-
accounts: data.accounts as readonly [Address, ...Address[]],
102-
chainId: data.chainId,
103-
connector,
104-
})
105-
providers.push(provider)
106-
connected = true
107-
}
44+
} else connectors.push(...config.connectors)
45+
46+
// Try recently-used connectors first
47+
let recentConnectorId: string | null | undefined
48+
try {
49+
recentConnectorId = await config.storage?.getItem('recentConnectorId')
50+
} catch {}
51+
const scores: Record<string, number> = {}
52+
for (const [, connection] of config.state.connections) {
53+
scores[connection.connector.id] = 1
54+
}
55+
if (recentConnectorId) scores[recentConnectorId] = 0
56+
const sorted =
57+
Object.keys(scores).length > 0
58+
? // .toSorted()
59+
[...connectors].sort(
60+
(a, b) => (scores[a.id] ?? 10) - (scores[b.id] ?? 10),
61+
)
62+
: connectors
63+
64+
// Iterate through each connector and try to connect
65+
let connected = false
66+
const connections: Connection[] = []
67+
const providers: unknown[] = []
68+
for (const connector of sorted) {
69+
const provider = await connector.getProvider().catch(() => undefined)
70+
if (!provider) continue
71+
72+
// If we already have an instance of this connector's provider,
73+
// then we have already checked it (ie. injected connectors can
74+
// share the same `window.ethereum` instance, so we don't want to
75+
// connect to it again).
76+
if (providers.some((x) => x === provider)) continue
77+
78+
const isAuthorized = await connector.isAuthorized().catch(() => false)
79+
if (!isAuthorized) continue
80+
81+
const data = await connector
82+
.connect({ isReconnecting: true })
83+
.catch(() => null)
84+
if (!data) continue
85+
86+
connector.emitter.off('connect', config._internal.events.connect)
87+
connector.emitter.on('change', config._internal.events.change)
88+
connector.emitter.on('disconnect', config._internal.events.disconnect)
89+
90+
config.setState((x) => {
91+
const connections = new Map(connected ? x.connections : new Map()).set(
92+
connector.uid,
93+
{ accounts: data.accounts, chainId: data.chainId, connector },
94+
)
95+
return {
96+
...x,
97+
current: connected ? x.current : connector.uid,
98+
connections,
99+
}
100+
})
101+
connections.push({
102+
accounts: data.accounts as readonly [Address, ...Address[]],
103+
chainId: data.chainId,
104+
connector,
105+
})
106+
providers.push(provider)
107+
connected = true
108+
}
108109

109-
// Prevent overwriting connected status from race condition
110-
if (
111-
config.state.status === 'reconnecting' ||
112-
config.state.status === 'connecting'
113-
) {
114-
// If connecting didn't succeed, set to disconnected
115-
if (!connected)
116-
config.setState((x) => ({
117-
...x,
118-
connections: new Map(),
119-
current: null,
120-
status: 'disconnected',
121-
}))
122-
else config.setState((x) => ({ ...x, status: 'connected' }))
123-
}
110+
// Prevent overwriting connected status from race condition
111+
if (
112+
config.state.status === 'reconnecting' ||
113+
config.state.status === 'connecting'
114+
) {
115+
// If connecting didn't succeed, set to disconnected
116+
if (!connected)
117+
config.setState((x) => ({
118+
...x,
119+
connections: new Map(),
120+
current: null,
121+
status: 'disconnected',
122+
}))
123+
else config.setState((x) => ({ ...x, status: 'connected' }))
124+
}
124125

125-
isReconnecting = false
126-
return connections
126+
return connections
127+
} finally {
128+
isReconnecting = false
129+
}
127130
}

packages/core/src/createConfig.test.ts

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -483,6 +483,32 @@ test('behavior: revalidate connections', async () => {
483483
expect([...config.state.connections.keys()]).toEqual([c2.uid])
484484
})
485485

486+
test('behavior: revalidate handles connector.isAuthorized() rejection', async () => {
487+
const config = createConfig({
488+
chains: [mainnet],
489+
connectors: [
490+
mock({ accounts, features: { defaultConnected: true, reconnect: true } }),
491+
],
492+
storage: null,
493+
transports: {
494+
[mainnet.id]: http(),
495+
},
496+
})
497+
498+
const c1 = config.connectors.at(0)!
499+
c1.isAuthorized = async () => {
500+
throw new Error('revalidation error')
501+
}
502+
503+
const connections = new Map<string, Connection>()
504+
connections.set(c1.uid, { accounts: ['0x'], chainId: 1, connector: c1 })
505+
config.setState((state) => ({ ...state, connections, current: c1.uid }))
506+
507+
await expect(config._internal.revalidate()).resolves.toBeUndefined()
508+
expect(config.state.connections.size).toEqual(0)
509+
expect(config.state.current).toBeNull()
510+
})
511+
486512
function getProviderDetail(
487513
info: Pick<EIP6963ProviderDetail['info'], 'name' | 'rdns'>,
488514
): EIP6963ProviderDetail {

packages/core/src/createConfig.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -466,14 +466,14 @@ export function createConfig<
466466
async revalidate() {
467467
// Check connections to see if they are still active
468468
const state = store.getState()
469-
const connections = state.connections
469+
const connections = new Map(state.connections)
470470
let current = state.current
471-
for (const [, connection] of connections) {
471+
for (const [, connection] of state.connections) {
472472
const connector = connection.connector
473473
// check if `connect.isAuthorized` exists
474474
// partial connectors in storage do not have it
475475
const isAuthorized = connector.isAuthorized
476-
? await connector.isAuthorized()
476+
? await connector.isAuthorized().catch(() => false)
477477
: false
478478
if (isAuthorized) continue
479479
// Remove stale connection

0 commit comments

Comments
 (0)