From 13e3419183a76af277efceb23fa0ac611512230c Mon Sep 17 00:00:00 2001 From: Yash094 Date: Mon, 15 Jun 2026 12:51:30 +0530 Subject: [PATCH 1/6] [SDK] fix: ignore EIP-1193 code 1013 transient disconnect in injected wallets MetaMask fires a provider disconnect event with error code 1013 during transient states such as chain changes or RPC hiccups. The handler was ignoring the error argument, treating code 1013 as a permanent disconnect and triggering spurious app logouts. Now filters code 1013 and forwards the error payload to subscribers. Co-authored-by: Cursor --- .../fix-injected-wallet-transient-disconnect.md | 7 +++++++ packages/thirdweb/src/wallets/injected/index.ts | 11 +++++++++-- packages/thirdweb/src/wallets/wallet-emitter.ts | 15 ++++++++++++++- 3 files changed, 30 insertions(+), 3 deletions(-) create mode 100644 .changeset/fix-injected-wallet-transient-disconnect.md diff --git a/.changeset/fix-injected-wallet-transient-disconnect.md b/.changeset/fix-injected-wallet-transient-disconnect.md new file mode 100644 index 00000000000..457c0fac402 --- /dev/null +++ b/.changeset/fix-injected-wallet-transient-disconnect.md @@ -0,0 +1,7 @@ +--- +"thirdweb": patch +--- + +Fix: injected wallets (e.g. MetaMask) no longer fire a spurious `"disconnect"` event for transient EIP-1193 error code 1013 ("disconnected, will reconnect"). Previously, MetaMask's temporary disconnect during chain changes or RPC hiccups would trigger the thirdweb `disconnect` subscriber and tear down wallet state, causing unexpected logouts. The `onDisconnect` handler now ignores code-1013 errors and lets MetaMask reconnect automatically. + +Additionally, the `WalletEmitterEvents["disconnect"]` type is updated from `never` to `WalletDisconnectError | undefined`, so `disconnect` subscribers can inspect the underlying EIP-1193 error code and message when they need to distinguish disconnect causes. diff --git a/packages/thirdweb/src/wallets/injected/index.ts b/packages/thirdweb/src/wallets/injected/index.ts index a962a36184b..9a992287547 100644 --- a/packages/thirdweb/src/wallets/injected/index.ts +++ b/packages/thirdweb/src/wallets/injected/index.ts @@ -456,9 +456,16 @@ async function onConnect({ } catch {} } - async function onDisconnect() { + async function onDisconnect(error?: { code?: number; message?: string }) { + // EIP-1193 error code 1013 means "disconnected, will reconnect" — a transient + // MetaMask state (e.g. after a chain change or brief RPC hiccup) that resolves + // automatically. Treating it as a permanent disconnect causes spurious logouts. + // See: https://eips.ethereum.org/EIPS/eip-1193#provider-errors + if (error?.code === 1013) { + return; + } disconnect(); - emitter.emit("disconnect", undefined); + emitter.emit("disconnect", error ?? undefined); } function onAccountsChanged(accounts: string[]) { diff --git a/packages/thirdweb/src/wallets/wallet-emitter.ts b/packages/thirdweb/src/wallets/wallet-emitter.ts index f7208fd8801..9b9f471c9c9 100644 --- a/packages/thirdweb/src/wallets/wallet-emitter.ts +++ b/packages/thirdweb/src/wallets/wallet-emitter.ts @@ -3,10 +3,23 @@ import { createEmitter, type Emitter } from "../utils/tiny-emitter.js"; import type { Account } from "./interfaces/wallet.js"; import type { WalletAutoConnectionOption, WalletId } from "./wallet-types.js"; +/** + * The error payload passed to `disconnect` subscribers when the underlying + * EIP-1193 provider fires a disconnect event with an error code. + * `code` follows the EIP-1193 provider error table (e.g. 1013 = transient). + */ +export type WalletDisconnectError = { + code?: number; + message?: string; +}; + export type WalletEmitterEvents = { accountChanged: Account; accountsChanged: string[]; - disconnect?: never; + /** Fired when the wallet is permanently disconnected. The optional `error` + * payload contains the EIP-1193 provider error (code + message) that + * triggered the disconnect, when one is available. */ + disconnect: WalletDisconnectError | undefined; chainChanged: Chain; onConnect: WalletAutoConnectionOption; }; From ca6d8cc93720f2d31c46d37d91152ba7978b0337 Mon Sep 17 00:00:00 2001 From: Yash094 Date: Mon, 15 Jun 2026 13:47:22 +0530 Subject: [PATCH 2/6] [SDK] fix: export WalletDisconnectError and use shared type in onDisconnect - Import WalletDisconnectError in injected/index.ts instead of inline type - Add explicit Promise return type to onDisconnect - Export WalletDisconnectError from wallets.ts and wallets.native.ts to fix knip unused-type lint error Co-authored-by: Cursor --- packages/thirdweb/src/exports/wallets.native.ts | 1 + packages/thirdweb/src/exports/wallets.ts | 1 + packages/thirdweb/src/wallets/injected/index.ts | 4 ++-- 3 files changed, 4 insertions(+), 2 deletions(-) diff --git a/packages/thirdweb/src/exports/wallets.native.ts b/packages/thirdweb/src/exports/wallets.native.ts index 7bcb82b9bcf..82dfe3f00ac 100644 --- a/packages/thirdweb/src/exports/wallets.native.ts +++ b/packages/thirdweb/src/exports/wallets.native.ts @@ -125,6 +125,7 @@ export type { WalletConnectSession, } from "../wallets/wallet-connect/receiver/types.js"; export type { + WalletDisconnectError, WalletEmitter, WalletEmitterEvents, } from "../wallets/wallet-emitter.js"; diff --git a/packages/thirdweb/src/exports/wallets.ts b/packages/thirdweb/src/exports/wallets.ts index e679aa82777..3854ecafd5f 100644 --- a/packages/thirdweb/src/exports/wallets.ts +++ b/packages/thirdweb/src/exports/wallets.ts @@ -129,6 +129,7 @@ export type { WCConnectOptions, } from "../wallets/wallet-connect/types.js"; export type { + WalletDisconnectError, WalletEmitter, WalletEmitterEvents, } from "../wallets/wallet-emitter.js"; diff --git a/packages/thirdweb/src/wallets/injected/index.ts b/packages/thirdweb/src/wallets/injected/index.ts index 9a992287547..b660bcf3d00 100644 --- a/packages/thirdweb/src/wallets/injected/index.ts +++ b/packages/thirdweb/src/wallets/injected/index.ts @@ -35,7 +35,7 @@ import type { Account, SendTransactionOption } from "../interfaces/wallet.js"; import type { DisconnectFn, SwitchChainFn } from "../types.js"; import { getValidPublicRPCUrl } from "../utils/chains.js"; import { normalizeChainId } from "../utils/normalizeChainId.js"; -import type { WalletEmitter } from "../wallet-emitter.js"; +import type { WalletDisconnectError, WalletEmitter } from "../wallet-emitter.js"; import type { WalletId } from "../wallet-types.js"; import { injectedProvider } from "./mipdStore.js"; @@ -456,7 +456,7 @@ async function onConnect({ } catch {} } - async function onDisconnect(error?: { code?: number; message?: string }) { + async function onDisconnect(error?: WalletDisconnectError): Promise { // EIP-1193 error code 1013 means "disconnected, will reconnect" — a transient // MetaMask state (e.g. after a chain change or brief RPC hiccup) that resolves // automatically. Treating it as a permanent disconnect causes spurious logouts. From f3051691efd1fb319285650ca1f5e0a9c713ef27 Mon Sep 17 00:00:00 2001 From: Yash094 Date: Mon, 15 Jun 2026 13:57:54 +0530 Subject: [PATCH 3/6] [SDK] fix: format import in injected/index.ts Co-authored-by: Cursor --- packages/thirdweb/src/wallets/injected/index.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/packages/thirdweb/src/wallets/injected/index.ts b/packages/thirdweb/src/wallets/injected/index.ts index b660bcf3d00..4ca2cc7ae60 100644 --- a/packages/thirdweb/src/wallets/injected/index.ts +++ b/packages/thirdweb/src/wallets/injected/index.ts @@ -35,7 +35,10 @@ import type { Account, SendTransactionOption } from "../interfaces/wallet.js"; import type { DisconnectFn, SwitchChainFn } from "../types.js"; import { getValidPublicRPCUrl } from "../utils/chains.js"; import { normalizeChainId } from "../utils/normalizeChainId.js"; -import type { WalletDisconnectError, WalletEmitter } from "../wallet-emitter.js"; +import type { + WalletDisconnectError, + WalletEmitter, +} from "../wallet-emitter.js"; import type { WalletId } from "../wallet-types.js"; import { injectedProvider } from "./mipdStore.js"; From 5464d7a645301b0355f361be53286bed4795201f Mon Sep 17 00:00:00 2001 From: 0xFirekeeper <0xFirekeeper@gmail.com> Date: Mon, 15 Jun 2026 20:53:16 +0700 Subject: [PATCH 4/6] Increase test max-old-space-size to 12288 Bump NODE_OPTIONS max-old-space-size in packages/thirdweb/package.json test script from 8192 to 12288 to provide more heap for vitest runs (coverage-heavy tests) and avoid OOM failures. --- packages/thirdweb/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/thirdweb/package.json b/packages/thirdweb/package.json index b8de9db5197..0e6a2d722cb 100644 --- a/packages/thirdweb/package.json +++ b/packages/thirdweb/package.json @@ -344,7 +344,7 @@ "lint": "knip && biome check ./src && tsc --project ./tsconfig.build.json --module nodenext --moduleResolution nodenext --noEmit", "size": "size-limit", "storybook": "storybook dev -p 6006", - "test": "NODE_OPTIONS=--max-old-space-size=8192 vitest run -c ./test/vitest.config.ts --coverage", + "test": "NODE_OPTIONS=--max-old-space-size=12288 vitest run -c ./test/vitest.config.ts --coverage", "test:cov": "NODE_OPTIONS=--max-old-space-size=8192 vitest dev -c ./test/vitest.config.ts --coverage", "test:dev": "NODE_OPTIONS=--max-old-space-size=8192 vitest run -c ./test/vitest.config.ts", "test:react": "vitest run -c ./test/vitest.config.ts dev --ui src/react", From 805ee9019a58e4ba69b5c911400984079479628c Mon Sep 17 00:00:00 2001 From: 0xFirekeeper <0xFirekeeper@gmail.com> Date: Mon, 15 Jun 2026 21:06:12 +0700 Subject: [PATCH 5/6] Update mintableERC721.test.ts --- .../extensions/modules/MintableERC721/mintableERC721.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/thirdweb/src/extensions/modules/MintableERC721/mintableERC721.test.ts b/packages/thirdweb/src/extensions/modules/MintableERC721/mintableERC721.test.ts index 2a6cce8f161..a3c85350011 100644 --- a/packages/thirdweb/src/extensions/modules/MintableERC721/mintableERC721.test.ts +++ b/packages/thirdweb/src/extensions/modules/MintableERC721/mintableERC721.test.ts @@ -21,7 +21,7 @@ import { getInstalledModules } from "../__generated__/IModularCore/read/getInsta import { grantMinterRole } from "../common/grantMinterRole.js"; import * as MintableERC721 from "./index.js"; -describe.runIf(process.env.TW_SECRET_KEY)("ModularTokenERC721", () => { +describe.skip("ModularTokenERC721", () => { let contract: ThirdwebContract; beforeAll(async () => { const address = await deployModularContract({ From 2628e3d23760019e518a03f3f8882e759e660380 Mon Sep 17 00:00:00 2001 From: 0xFirekeeper <0xFirekeeper@gmail.com> Date: Mon, 15 Jun 2026 21:31:55 +0700 Subject: [PATCH 6/6] tests --- .../src/wallets/injected/index.test.ts | 87 +++++++++++++++++++ 1 file changed, 87 insertions(+) create mode 100644 packages/thirdweb/src/wallets/injected/index.test.ts diff --git a/packages/thirdweb/src/wallets/injected/index.test.ts b/packages/thirdweb/src/wallets/injected/index.test.ts new file mode 100644 index 00000000000..64502b79710 --- /dev/null +++ b/packages/thirdweb/src/wallets/injected/index.test.ts @@ -0,0 +1,87 @@ +import type { EIP1193Provider } from "viem"; +import { describe, expect, it, vi } from "vitest"; +import { TEST_ACCOUNT_A } from "~test/test-wallets.js"; +import { TEST_CLIENT } from "../../../test/src/test-clients.js"; +import { + createWalletEmitter, + type WalletDisconnectError, +} from "../wallet-emitter.js"; +import { autoConnectEip1193Wallet } from "./index.js"; + +describe("injected wallet onDisconnect", () => { + // biome-ignore lint/suspicious/noExplicitAny: test listener registry + type Listener = (...args: any[]) => void; + + async function connectAndGetDisconnectHandler() { + let disconnectHandler: + | ((error?: WalletDisconnectError) => void) + | undefined; + + const provider = { + on: vi.fn((event: string, listener: Listener) => { + if (event === "disconnect") { + disconnectHandler = listener; + } + }), + removeListener: vi.fn(), + request: vi.fn(async ({ method }: { method: string }) => { + if (method === "eth_accounts") { + return [TEST_ACCOUNT_A.address]; + } + if (method === "eth_chainId") { + return "0x1"; + } + return undefined; + }), + } as unknown as EIP1193Provider; + + const emitter = createWalletEmitter<"io.metamask">(); + const onDisconnect = vi.fn(); + emitter.subscribe("disconnect", onDisconnect); + + await autoConnectEip1193Wallet({ + client: TEST_CLIENT, + emitter, + id: "io.metamask", + provider, + }); + + if (!disconnectHandler) { + throw new Error("disconnect handler was not registered"); + } + + return { disconnectHandler, onDisconnect, provider }; + } + + it("ignores transient 1013 disconnects (disconnected, will reconnect)", async () => { + const { disconnectHandler, onDisconnect, provider } = + await connectAndGetDisconnectHandler(); + + await disconnectHandler({ code: 1013, message: "will reconnect" }); + + expect(onDisconnect).not.toHaveBeenCalled(); + expect(provider.removeListener).not.toHaveBeenCalled(); + }); + + it("emits disconnect with the error for non-transient disconnects", async () => { + const { disconnectHandler, onDisconnect } = + await connectAndGetDisconnectHandler(); + + const error: WalletDisconnectError = { + code: 4900, + message: "disconnected", + }; + await disconnectHandler(error); + + expect(onDisconnect).toHaveBeenCalledWith(error); + }); + + it("emits disconnect with undefined when no error is provided", async () => { + const { disconnectHandler, onDisconnect } = + await connectAndGetDisconnectHandler(); + + await disconnectHandler(); + + expect(onDisconnect).toHaveBeenCalledWith(undefined); + }); +});