Skip to content
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/hot-bikes-relax.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"thirdweb": patch
---

Improve WalletConnect chain switching reliability
4 changes: 2 additions & 2 deletions packages/thirdweb/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -25,8 +25,8 @@
"@tanstack/react-query": "5.81.5",
"@thirdweb-dev/engine": "workspace:*",
"@thirdweb-dev/insight": "workspace:*",
"@walletconnect/sign-client": "2.20.1",
"@walletconnect/universal-provider": "2.21.4",
"@walletconnect/sign-client": "2.21.8",
"@walletconnect/universal-provider": "2.21.8",
"abitype": "1.0.8",
"cross-spawn": "7.0.6",
"fuse.js": "7.1.0",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -154,6 +154,11 @@
await switchChain(tx.chain);
// in smart wallet case, account may change after chain switch
_account = wallet.getAccount();

// ensure that the account has switched to the correct chain
if (wallet.getChain()?.id !== tx.chain.id) {
throw new Error(`Could not switch to chain ${tx.chain.id}`);
}

Check warning on line 161 in packages/thirdweb/src/react/core/hooks/transaction/useSendTransaction.ts

View check run for this annotation

Codecov / codecov/patch

packages/thirdweb/src/react/core/hooks/transaction/useSendTransaction.ts#L159-L161

Added lines #L159 - L161 were not covered by tests
}

const account = _account;
Expand Down
2 changes: 2 additions & 0 deletions packages/thirdweb/src/wallets/create-wallet.ts
Original file line number Diff line number Diff line change
Expand Up @@ -483,6 +483,8 @@ export function createWallet<const ID extends WalletId>(
id,
subscribe: emitter.subscribe,
switchChain: async (c) => {
// TODO: this should actually throw an error if the chain switch fails
// but our useSwitchActiveWalletChain hook currently doesn't handle this
try {
await handleSwitchChain(c);
chain = c;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { signLoginPayload } from "../../../../auth/core/sign-login-payload.js";
import type { LoginPayload } from "../../../../auth/core/types.js";
import type { Chain } from "../../../../chains/types.js";
import { getCachedChain } from "../../../../chains/utils.js";
import type { ThirdwebClient } from "../../../../client/client.js";
import { getClientFetch } from "../../../../utils/fetch.js";
import { stringify } from "../../../../utils/json.js";
Expand All @@ -14,14 +14,14 @@
*/
export async function siweAuthenticate(args: {
wallet: Wallet;
chain: Chain;
client: ThirdwebClient;
ecosystem?: Ecosystem;
}): Promise<AuthStoredTokenWithCookieReturnType> {
const { wallet, chain, client, ecosystem } = args;
const { wallet, client, ecosystem } = args;
const siweChain = getCachedChain(1); // always use mainnet for SIWE for wide wallet compatibility

Check warning on line 21 in packages/thirdweb/src/wallets/in-app/core/authentication/siwe.ts

View check run for this annotation

Codecov / codecov/patch

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

Added lines #L20 - L21 were not covered by tests
// only connect if the wallet doesn't already have an account
const account =
wallet.getAccount() || (await wallet.connect({ chain, client }));
wallet.getAccount() || (await wallet.connect({ chain: siweChain, client }));

Check warning on line 24 in packages/thirdweb/src/wallets/in-app/core/authentication/siwe.ts

View check run for this annotation

Codecov / codecov/patch

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

Added line #L24 was not covered by tests
const clientFetch = getClientFetch(client, ecosystem);

const payload = await (async () => {
Expand All @@ -31,7 +31,7 @@
ecosystem: args.ecosystem,
});
const res = await clientFetch(
`${path}&address=${account.address}&chainId=${chain.id}`,
`${path}&address=${account.address}&chainId=${siweChain.id}`,

Check warning on line 34 in packages/thirdweb/src/wallets/in-app/core/authentication/siwe.ts

View check run for this annotation

Codecov / codecov/patch

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

Added line #L34 was not covered by tests
);

if (!res.ok) throw new Error("Failed to generate SIWE login payload");
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -183,7 +183,6 @@ export class InAppNativeConnector implements InAppConnector {
"../core/authentication/siwe.js"
);
return siweAuthenticate({
chain: params.chain,
client: this.client,
ecosystem: params.ecosystem,
wallet: params.wallet,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -104,7 +104,6 @@ describe("InAppWebConnector.connect", () => {
});

expect(siweAuthenticate).toHaveBeenCalledWith({
chain: ethereum,
client: TEST_CLIENT,
ecosystem: undefined,
wallet: mockWallet,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -365,7 +365,6 @@ export class InAppWebConnector implements InAppConnector {
}
case "wallet": {
return siweAuthenticate({
chain: args.chain,
client: this.client,
ecosystem: this.ecosystem,
wallet: args.wallet,
Expand Down
131 changes: 117 additions & 14 deletions packages/thirdweb/src/wallets/wallet-connect/controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -132,7 +132,7 @@
...(wcOptions?.pairingTopic
? { pairingTopic: wcOptions?.pairingTopic }
: {}),
namespaces: {
optionalNamespaces: {

Check warning on line 135 in packages/thirdweb/src/wallets/wallet-connect/controller.ts

View check run for this annotation

Codecov / codecov/patch

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

Added line #L135 was not covered by tests
[NAMESPACE]: {
chains: chainsToRequest,
events: ["chainChanged", "accountsChanged"],
Expand All @@ -157,14 +157,8 @@
);
const currentChainId = chainsToRequest[0]?.split(":")[1] || 1;
const providerChainId = normalizeChainId(currentChainId);
const accounts: string[] = await provider.request(
{
method: "eth_requestAccounts",
params: [],
},
`eip155:${providerChainId}`,
);
const address = accounts[0];
const account = firstAccountOn(provider.session, `eip155:1`); // grab the address from mainnet
const address = account;

Check warning on line 161 in packages/thirdweb/src/wallets/wallet-connect/controller.ts

View check run for this annotation

Codecov / codecov/patch

packages/thirdweb/src/wallets/wallet-connect/controller.ts#L160-L161

Added lines #L160 - L161 were not covered by tests
if (!address) {
throw new Error("No accounts found on provider.");
}
Comment on lines +160 to 164
Copy link
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Fallback when no mainnet account exists in session.
Some wallets/sessions may not expose an account on eip155:1. Fall back to the first available account instead of failing hard.

-  const account = firstAccountOn(provider.session, `eip155:1`); // grab the address from mainnet
-  const address = account;
-  if (!address) {
-    throw new Error("No accounts found on provider.");
-  }
+  const address =
+    firstAccountOn(provider.session, `eip155:1`) ??
+    provider.session?.namespaces?.[NAMESPACE]?.accounts?.[0]?.split(":")[2] ??
+    null;
+  if (!address) {
+    throw new Error("No accounts found in WalletConnect session.");
+  }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const account = firstAccountOn(provider.session, `eip155:1`); // grab the address from mainnet
const address = account;
if (!address) {
throw new Error("No accounts found on provider.");
}
const address =
firstAccountOn(provider.session, `eip155:1`) ??
provider.session?.namespaces?.[NAMESPACE]?.accounts?.[0]?.split(":")[2] ??
null;
if (!address) {
throw new Error("No accounts found in WalletConnect session.");
}

Expand Down Expand Up @@ -202,6 +196,109 @@
);
}

async function ensureTargetChain(
provider: Awaited<ReturnType<typeof initProvider>>,
chain: Chain,
walletInfo: WalletInfo,
) {
if (!provider.session) {
throw new Error("No session found on provider.");
}
const TARGET_CAIP = `eip155:${chain.id}`;
const TARGET_HEX = numberToHex(chain.id);

Check warning on line 208 in packages/thirdweb/src/wallets/wallet-connect/controller.ts

View check run for this annotation

Codecov / codecov/patch

packages/thirdweb/src/wallets/wallet-connect/controller.ts#L199-L208

Added lines #L199 - L208 were not covered by tests

// Fast path: already enabled
if (hasChainEnabled(provider.session, TARGET_CAIP)) {
provider.setDefaultChain(TARGET_CAIP);
return;
}

Check warning on line 214 in packages/thirdweb/src/wallets/wallet-connect/controller.ts

View check run for this annotation

Codecov / codecov/patch

packages/thirdweb/src/wallets/wallet-connect/controller.ts#L211-L214

Added lines #L211 - L214 were not covered by tests

// 1) Try switch
try {
await requestAndOpenWallet({
provider,
payload: {
method: "wallet_switchEthereumChain",
params: [{ chainId: TARGET_HEX }],
},
chain: TARGET_CAIP, // route to target
walletInfo,
});
provider.setDefaultChain(TARGET_CAIP);
return;
} catch (err: any) {
const code = err?.code ?? err?.data?.originalError?.code;

Check warning on line 230 in packages/thirdweb/src/wallets/wallet-connect/controller.ts

View check run for this annotation

Codecov / codecov/patch

packages/thirdweb/src/wallets/wallet-connect/controller.ts#L217-L230

Added lines #L217 - L230 were not covered by tests
// 4001 user rejected; stop
if (code === 4001) throw new Error("User rejected chain switch");

Check warning on line 232 in packages/thirdweb/src/wallets/wallet-connect/controller.ts

View check run for this annotation

Codecov / codecov/patch

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

Added line #L232 was not covered by tests
// fall through on 4902 or unknown -> try add
}

Check warning on line 234 in packages/thirdweb/src/wallets/wallet-connect/controller.ts

View check run for this annotation

Codecov / codecov/patch

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

Added line #L234 was not covered by tests

// 2) Add the chain via any chain we already have
const routeChain = anyRoutableChain(provider.session);
if (!routeChain)
throw new Error("No routable chain to send wallet_addEthereumChain");

Check warning on line 239 in packages/thirdweb/src/wallets/wallet-connect/controller.ts

View check run for this annotation

Codecov / codecov/patch

packages/thirdweb/src/wallets/wallet-connect/controller.ts#L237-L239

Added lines #L237 - L239 were not covered by tests

try {
await requestAndOpenWallet({
provider,
payload: {
method: "wallet_addEthereumChain",
params: [
{
chainId: TARGET_HEX,
chainName: chain.name,
nativeCurrency: chain.nativeCurrency,
rpcUrls: [chain.rpc],
blockExplorerUrls: [chain.blockExplorers?.[0]?.url ?? ""],
},
],
},
chain: routeChain, // route via known-good chain, not the target
walletInfo,
});
} catch (err: any) {
const code = err?.code ?? err?.data?.originalError?.code;
if (code === 4001) throw new Error("User rejected add chain");
throw new Error(`Add chain failed: ${err?.message || String(err)}`);
}

Check warning on line 263 in packages/thirdweb/src/wallets/wallet-connect/controller.ts

View check run for this annotation

Codecov / codecov/patch

packages/thirdweb/src/wallets/wallet-connect/controller.ts#L241-L263

Added lines #L241 - L263 were not covered by tests

// 3) Re-try switch after add
await requestAndOpenWallet({
provider,
payload: {
method: "wallet_switchEthereumChain",
params: [{ chainId: TARGET_HEX }],
},
chain: TARGET_CAIP,
walletInfo,
});
provider.setDefaultChain(TARGET_CAIP);

Check warning on line 275 in packages/thirdweb/src/wallets/wallet-connect/controller.ts

View check run for this annotation

Codecov / codecov/patch

packages/thirdweb/src/wallets/wallet-connect/controller.ts#L266-L275

Added lines #L266 - L275 were not covered by tests

// 4) Verify enablement
if (!hasChainEnabled(provider.session, TARGET_CAIP)) {
throw new Error("Target chain still not enabled by wallet");
}
}

Check warning on line 281 in packages/thirdweb/src/wallets/wallet-connect/controller.ts

View check run for this annotation

Codecov / codecov/patch

packages/thirdweb/src/wallets/wallet-connect/controller.ts#L278-L281

Added lines #L278 - L281 were not covered by tests

type WCSession = Awaited<ReturnType<typeof UniversalProvider.init>>["session"];

function getNS(session: WCSession) {
return session?.namespaces?.eip155;
}
function hasChainEnabled(session: WCSession, caip: string) {
const ns = getNS(session);
return !!ns?.accounts?.some((a) => a.startsWith(`${caip}:`));
}
function firstAccountOn(session: WCSession, caip: string): string | null {
const ns = getNS(session);
const hit = ns?.accounts?.find((a) => a.startsWith(`${caip}:`));
return hit ? (hit.split(":")[2] ?? null) : null;
}
function anyRoutableChain(session: WCSession): string | null {
const ns = getNS(session);
return ns?.accounts?.[0]?.split(":")?.slice(0, 2)?.join(":") ?? null; // e.g. "eip155:1"
}

Check warning on line 300 in packages/thirdweb/src/wallets/wallet-connect/controller.ts

View check run for this annotation

Codecov / codecov/patch

packages/thirdweb/src/wallets/wallet-connect/controller.ts#L285-L300

Added lines #L285 - L300 were not covered by tests

/**
* Auto connect to already connected wallet connect session.
* @internal
Expand Down Expand Up @@ -545,14 +642,17 @@
account,
chain,
disconnect,
(newChain) => switchChainWC(provider, newChain),
(newChain) => switchChainWC(provider, newChain, walletInfo),

Check warning on line 645 in packages/thirdweb/src/wallets/wallet-connect/controller.ts

View check run for this annotation

Codecov / codecov/patch

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

Added line #L645 was not covered by tests
];
}

async function switchChainWC(provider: WCProvider, chain: Chain) {
const chainId = chain.id;
async function switchChainWC(
provider: WCProvider,
chain: Chain,
walletInfo: WalletInfo,
) {

Check warning on line 653 in packages/thirdweb/src/wallets/wallet-connect/controller.ts

View check run for this annotation

Codecov / codecov/patch

packages/thirdweb/src/wallets/wallet-connect/controller.ts#L649-L653

Added lines #L649 - L653 were not covered by tests
try {
provider.setDefaultChain(`eip155:${chainId}`);
await ensureTargetChain(provider, chain, walletInfo);

Check warning on line 655 in packages/thirdweb/src/wallets/wallet-connect/controller.ts

View check run for this annotation

Codecov / codecov/patch

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

Added line #L655 was not covered by tests
} catch (error) {
const message =
typeof error === "string" ? error : (error as ProviderRpcError)?.message;
Expand Down Expand Up @@ -605,7 +705,10 @@
chainIds.push(chain.id);
}

if (!options.chain && optionalChains.length === 0) {
// always include mainnet
// many wallets only support a handful of chains, but mainnet is always supported
// we will add additional chains in switchChain if needed
if (!chainIds.includes(1)) {

Check warning on line 711 in packages/thirdweb/src/wallets/wallet-connect/controller.ts

View check run for this annotation

Codecov / codecov/patch

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

Added line #L711 was not covered by tests
rpcMap[1] = getCachedChain(1).rpc;
chainIds.push(1);
}
Expand Down
Loading
Loading