Skip to content

Commit 24e4712

Browse files
Upgrade WalletConnect and improve chain switching reliability
1 parent 8b8dfd9 commit 24e4712

File tree

9 files changed

+192
-247
lines changed

9 files changed

+192
-247
lines changed

packages/thirdweb/package.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,8 +25,8 @@
2525
"@tanstack/react-query": "5.81.5",
2626
"@thirdweb-dev/engine": "workspace:*",
2727
"@thirdweb-dev/insight": "workspace:*",
28-
"@walletconnect/sign-client": "2.20.1",
29-
"@walletconnect/universal-provider": "2.21.4",
28+
"@walletconnect/sign-client": "2.21.8",
29+
"@walletconnect/universal-provider": "2.21.8",
3030
"abitype": "1.0.8",
3131
"cross-spawn": "7.0.6",
3232
"fuse.js": "7.1.0",

packages/thirdweb/src/react/core/hooks/transaction/useSendTransaction.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -154,6 +154,11 @@ export function useSendTransactionCore(args: {
154154
await switchChain(tx.chain);
155155
// in smart wallet case, account may change after chain switch
156156
_account = wallet.getAccount();
157+
158+
// ensure that the account has switched to the correct chain
159+
if (wallet.getChain()?.id !== tx.chain.id) {
160+
throw new Error(`Could not switch to chain ${tx.chain.id}`);
161+
}
157162
}
158163

159164
const account = _account;

packages/thirdweb/src/wallets/create-wallet.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -483,6 +483,8 @@ export function createWallet<const ID extends WalletId>(
483483
id,
484484
subscribe: emitter.subscribe,
485485
switchChain: async (c) => {
486+
// TODO: this should actually throw an error if the chain switch fails
487+
// but our useSwitchActiveWalletChain hook currently doesn't handle this
486488
try {
487489
await handleSwitchChain(c);
488490
chain = c;

packages/thirdweb/src/wallets/in-app/core/authentication/siwe.ts

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { signLoginPayload } from "../../../../auth/core/sign-login-payload.js";
22
import type { LoginPayload } from "../../../../auth/core/types.js";
3-
import type { Chain } from "../../../../chains/types.js";
3+
import { getCachedChain } from "../../../../chains/utils.js";
44
import type { ThirdwebClient } from "../../../../client/client.js";
55
import { getClientFetch } from "../../../../utils/fetch.js";
66
import { stringify } from "../../../../utils/json.js";
@@ -14,14 +14,14 @@ import type { AuthStoredTokenWithCookieReturnType } from "./types.js";
1414
*/
1515
export async function siweAuthenticate(args: {
1616
wallet: Wallet;
17-
chain: Chain;
1817
client: ThirdwebClient;
1918
ecosystem?: Ecosystem;
2019
}): Promise<AuthStoredTokenWithCookieReturnType> {
21-
const { wallet, chain, client, ecosystem } = args;
20+
const { wallet, client, ecosystem } = args;
21+
const siweChain = getCachedChain(1); // always use mainnet for SIWE for wide wallet compatibility
2222
// only connect if the wallet doesn't already have an account
2323
const account =
24-
wallet.getAccount() || (await wallet.connect({ chain, client }));
24+
wallet.getAccount() || (await wallet.connect({ chain: siweChain, client }));
2525
const clientFetch = getClientFetch(client, ecosystem);
2626

2727
const payload = await (async () => {
@@ -31,7 +31,7 @@ export async function siweAuthenticate(args: {
3131
ecosystem: args.ecosystem,
3232
});
3333
const res = await clientFetch(
34-
`${path}&address=${account.address}&chainId=${chain.id}`,
34+
`${path}&address=${account.address}&chainId=${siweChain.id}`,
3535
);
3636

3737
if (!res.ok) throw new Error("Failed to generate SIWE login payload");

packages/thirdweb/src/wallets/in-app/native/native-connector.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -183,7 +183,6 @@ export class InAppNativeConnector implements InAppConnector {
183183
"../core/authentication/siwe.js"
184184
);
185185
return siweAuthenticate({
186-
chain: params.chain,
187186
client: this.client,
188187
ecosystem: params.ecosystem,
189188
wallet: params.wallet,

packages/thirdweb/src/wallets/in-app/web/lib/web-connector.test.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -104,7 +104,6 @@ describe("InAppWebConnector.connect", () => {
104104
});
105105

106106
expect(siweAuthenticate).toHaveBeenCalledWith({
107-
chain: ethereum,
108107
client: TEST_CLIENT,
109108
ecosystem: undefined,
110109
wallet: mockWallet,

packages/thirdweb/src/wallets/in-app/web/lib/web-connector.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -365,7 +365,6 @@ export class InAppWebConnector implements InAppConnector {
365365
}
366366
case "wallet": {
367367
return siweAuthenticate({
368-
chain: args.chain,
369368
client: this.client,
370369
ecosystem: this.ecosystem,
371370
wallet: args.wallet,

packages/thirdweb/src/wallets/wallet-connect/controller.ts

Lines changed: 117 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -132,7 +132,7 @@ export async function connectWC(
132132
...(wcOptions?.pairingTopic
133133
? { pairingTopic: wcOptions?.pairingTopic }
134134
: {}),
135-
namespaces: {
135+
optionalNamespaces: {
136136
[NAMESPACE]: {
137137
chains: chainsToRequest,
138138
events: ["chainChanged", "accountsChanged"],
@@ -157,14 +157,8 @@ export async function connectWC(
157157
);
158158
const currentChainId = chainsToRequest[0]?.split(":")[1] || 1;
159159
const providerChainId = normalizeChainId(currentChainId);
160-
const accounts: string[] = await provider.request(
161-
{
162-
method: "eth_requestAccounts",
163-
params: [],
164-
},
165-
`eip155:${providerChainId}`,
166-
);
167-
const address = accounts[0];
160+
const account = firstAccountOn(provider.session, `eip155:1`); // grab the address from mainnet
161+
const address = account;
168162
if (!address) {
169163
throw new Error("No accounts found on provider.");
170164
}
@@ -202,6 +196,109 @@ export async function connectWC(
202196
);
203197
}
204198

199+
async function ensureTargetChain(
200+
provider: Awaited<ReturnType<typeof initProvider>>,
201+
chain: Chain,
202+
walletInfo: WalletInfo,
203+
) {
204+
if (!provider.session) {
205+
throw new Error("No session found on provider.");
206+
}
207+
const TARGET_CAIP = `eip155:${chain.id}`;
208+
const TARGET_HEX = numberToHex(chain.id);
209+
210+
// Fast path: already enabled
211+
if (hasChainEnabled(provider.session, TARGET_CAIP)) {
212+
provider.setDefaultChain(TARGET_CAIP);
213+
return;
214+
}
215+
216+
// 1) Try switch
217+
try {
218+
await requestAndOpenWallet({
219+
provider,
220+
payload: {
221+
method: "wallet_switchEthereumChain",
222+
params: [{ chainId: TARGET_HEX }],
223+
},
224+
chain: TARGET_CAIP, // route to target
225+
walletInfo,
226+
});
227+
provider.setDefaultChain(TARGET_CAIP);
228+
return;
229+
} catch (err: any) {
230+
const code = err?.code ?? err?.data?.originalError?.code;
231+
// 4001 user rejected; stop
232+
if (code === 4001) throw new Error("User rejected chain switch");
233+
// fall through on 4902 or unknown -> try add
234+
}
235+
236+
// 2) Add the chain via any chain we already have
237+
const routeChain = anyRoutableChain(provider.session);
238+
if (!routeChain)
239+
throw new Error("No routable chain to send wallet_addEthereumChain");
240+
241+
try {
242+
await requestAndOpenWallet({
243+
provider,
244+
payload: {
245+
method: "wallet_addEthereumChain",
246+
params: [
247+
{
248+
chainId: TARGET_HEX,
249+
chainName: chain.name,
250+
nativeCurrency: chain.nativeCurrency,
251+
rpcUrls: [chain.rpc],
252+
blockExplorerUrls: [chain.blockExplorers?.[0]?.url ?? ""],
253+
},
254+
],
255+
},
256+
chain: routeChain, // route via known-good chain, not the target
257+
walletInfo,
258+
});
259+
} catch (err: any) {
260+
const code = err?.code ?? err?.data?.originalError?.code;
261+
if (code === 4001) throw new Error("User rejected add chain");
262+
throw new Error(`Add chain failed: ${err?.message || String(err)}`);
263+
}
264+
265+
// 3) Re-try switch after add
266+
await requestAndOpenWallet({
267+
provider,
268+
payload: {
269+
method: "wallet_switchEthereumChain",
270+
params: [{ chainId: TARGET_HEX }],
271+
},
272+
chain: TARGET_CAIP,
273+
walletInfo,
274+
});
275+
provider.setDefaultChain(TARGET_CAIP);
276+
277+
// 4) Verify enablement
278+
if (!hasChainEnabled(provider.session, TARGET_CAIP)) {
279+
throw new Error("Target chain still not enabled by wallet");
280+
}
281+
}
282+
283+
type WCSession = Awaited<ReturnType<typeof UniversalProvider.init>>["session"];
284+
285+
function getNS(session: WCSession) {
286+
return session?.namespaces?.eip155;
287+
}
288+
function hasChainEnabled(session: WCSession, caip: string) {
289+
const ns = getNS(session);
290+
return !!ns?.accounts?.some((a) => a.startsWith(`${caip}:`));
291+
}
292+
function firstAccountOn(session: WCSession, caip: string): string | null {
293+
const ns = getNS(session);
294+
const hit = ns?.accounts?.find((a) => a.startsWith(`${caip}:`));
295+
return hit ? (hit.split(":")[2] ?? null) : null;
296+
}
297+
function anyRoutableChain(session: WCSession): string | null {
298+
const ns = getNS(session);
299+
return ns?.accounts?.[0]?.split(":")?.slice(0, 2)?.join(":") ?? null; // e.g. "eip155:1"
300+
}
301+
205302
/**
206303
* Auto connect to already connected wallet connect session.
207304
* @internal
@@ -545,14 +642,17 @@ function onConnect(
545642
account,
546643
chain,
547644
disconnect,
548-
(newChain) => switchChainWC(provider, newChain),
645+
(newChain) => switchChainWC(provider, newChain, walletInfo),
549646
];
550647
}
551648

552-
async function switchChainWC(provider: WCProvider, chain: Chain) {
553-
const chainId = chain.id;
649+
async function switchChainWC(
650+
provider: WCProvider,
651+
chain: Chain,
652+
walletInfo: WalletInfo,
653+
) {
554654
try {
555-
provider.setDefaultChain(`eip155:${chainId}`);
655+
await ensureTargetChain(provider, chain, walletInfo);
556656
} catch (error) {
557657
const message =
558658
typeof error === "string" ? error : (error as ProviderRpcError)?.message;
@@ -605,7 +705,10 @@ function getChainsToRequest(options: {
605705
chainIds.push(chain.id);
606706
}
607707

608-
if (!options.chain && optionalChains.length === 0) {
708+
// always include mainnet
709+
// many wallets only support a handful of chains, but mainnet is always supported
710+
// we will add additional chains in switchChain if needed
711+
if (!chainIds.includes(1)) {
609712
rpcMap[1] = getCachedChain(1).rpc;
610713
chainIds.push(1);
611714
}

0 commit comments

Comments
 (0)