diff --git a/demo/vue-app-new/src/components/AppDashboard.vue b/demo/vue-app-new/src/components/AppDashboard.vue index 4442d19fd..c5f850554 100644 --- a/demo/vue-app-new/src/components/AppDashboard.vue +++ b/demo/vue-app-new/src/components/AppDashboard.vue @@ -17,15 +17,7 @@ import { CONNECTOR_INITIAL_AUTHENTICATION_MODE } from "@web3auth/no-modal"; import { useI18n } from "petite-vue-i18n"; import { useSignMessage as useSolanaSignMessage, useSolanaWallet, useSolanaClient } from "@web3auth/modal/vue/solana"; -import { - useConnection, - useBalance, - useChainId, - useSignMessage, - useSignTypedData, - useSwitchChain as useWagmiSwitchChain, - useConfig, -} from "@wagmi/vue"; +import { useConnection, useBalance, useSignMessage, useSignTypedData, useSwitchChain as useWagmiSwitchChain, useConfig } from "@wagmi/vue"; import { getCapabilities, getCallsStatus, sendCalls, showCallsStatus } from "@wagmi/core"; import { parseEther } from "viem"; import { createWalletTransactionSigner, toAddress } from "@solana/client"; @@ -56,7 +48,6 @@ const { getAuthTokenInfo, loading: getAuthTokenInfoLoading } = useAuthTokenInfo( const { status, address } = useConnection(); const { mutateAsync: signTypedDataAsync } = useSignTypedData(); const { mutateAsync: signMessageAsync } = useSignMessage(); -const wagmiChainId = useChainId(); const balance = useBalance({ address: address, }); @@ -105,7 +96,7 @@ const isDisplay = (name: "dashboard" | "ethServices" | "solServices" | "walletSe return Boolean(conn?.solanaWallet); case "walletServices": - return web3Auth.value?.connectedConnectorName === WALLET_CONNECTORS.AUTH && Boolean(conn?.ethereumProvider || conn?.solanaWallet); + return web3Auth.value?.primaryConnectorName === WALLET_CONNECTORS.AUTH && Boolean(conn?.ethereumProvider || conn?.solanaWallet); default: { return false; @@ -182,7 +173,7 @@ const onGetPrivateKey = async () => { }; const getConnectedChainId = async () => { - printToConsole("chainId", wagmiChainId.value); + printToConsole("chainId", web3Auth.value?.currentChain?.chainId); }; const onGetBalance = async () => { diff --git a/packages/modal/src/modalManager.ts b/packages/modal/src/modalManager.ts index 48bb03061..261469a74 100644 --- a/packages/modal/src/modalManager.ts +++ b/packages/modal/src/modalManager.ts @@ -215,7 +215,7 @@ export class Web3Auth extends Web3AuthNoModal implements IWeb3AuthModal { public async connect(): Promise { if (!this.loginModal) throw WalletInitializationError.notReady("Login modal is not initialized"); // if already connected return connection - if (this.connectedConnectorName && CONNECTED_STATUSES.includes(this.status) && this.connection) return this.connection; + if (CONNECTED_STATUSES.includes(this.status) && this.connection) return this.connection; this.loginModal.open(); return new Promise((resolve, reject) => { // remove all listeners when promise is resolved or rejected. diff --git a/packages/modal/test/modalManager.test.ts b/packages/modal/test/modalManager.test.ts index 4888043a8..678d0e565 100644 --- a/packages/modal/test/modalManager.test.ts +++ b/packages/modal/test/modalManager.test.ts @@ -23,12 +23,12 @@ vi.mock("@web3auth/no-modal", async () => { import { CHAIN_NAMESPACES, CONNECTED_STATUSES, + type ConnectedAccountInfo, Connection, CONNECTOR_EVENTS, CONNECTOR_INITIAL_AUTHENTICATION_MODE, type IConnector, type LinkAccountResult, - type LinkedAccountInfo, type ProjectConfig, WALLET_CONNECTORS, WalletInitializationError, @@ -49,11 +49,11 @@ class TestWeb3Auth extends Web3Auth { return this.getInitializationTrackData(); } - public exposeSetConnectedWalletConnector(connector: IConnector, account?: LinkedAccountInfo | null) { + public exposeSetConnectedWalletConnector(connector: IConnector, account?: ConnectedAccountInfo | null) { this.setConnectedWalletConnector(connector, account); } - public exposeSetActiveWalletConnectorKey(account?: LinkedAccountInfo | null) { + public exposeSetActiveWalletConnectorKey(account?: ConnectedAccountInfo | null) { this.setActiveWalletConnectorKey(account); } } @@ -85,7 +85,7 @@ function createSdk(overrides: Partial = {}) { } as never); } -function createConnectedWalletAccount(overrides: Partial = {}): LinkedAccountInfo { +function createConnectedWalletAccount(overrides: Partial = {}): ConnectedAccountInfo { return { id: "wallet-1", accountType: "external_wallet", @@ -131,7 +131,7 @@ describe("Web3Auth (modal)", () => { const open = vi.fn(); (sdk as unknown as { loginModal: { open: () => void } }).loginModal = { open }; (sdk as unknown as { state: Record }).state = { - connectedConnectorName: WALLET_CONNECTORS.AUTH, + primaryConnectorName: WALLET_CONNECTORS.AUTH, cachedConnector: null, currentChainId: "0x1", idToken: null, @@ -258,7 +258,7 @@ describe("Web3Auth (modal)", () => { vi.spyOn(sdk as unknown as { getMainAuthConnector: () => unknown }, "getMainAuthConnector").mockReturnValue(authConnector as never); vi.spyOn( - sdk as unknown as { getConnectedWalletConnector: (account?: LinkedAccountInfo | null) => unknown }, + sdk as unknown as { getConnectedWalletConnector: (account?: ConnectedAccountInfo | null) => unknown }, "getConnectedWalletConnector" ).mockReturnValue(existingConnector as never); @@ -342,7 +342,7 @@ describe("Web3Auth (modal)", () => { vi.spyOn(sdk as unknown as { getMainAuthConnector: () => unknown }, "getMainAuthConnector").mockReturnValue(authConnector as never); vi.spyOn( - sdk as unknown as { getConnectedWalletConnector: (account?: LinkedAccountInfo | null) => unknown }, + sdk as unknown as { getConnectedWalletConnector: (account?: ConnectedAccountInfo | null) => unknown }, "getConnectedWalletConnector" ).mockReturnValue(null); diff --git a/packages/no-modal/src/connectors/auth-connector/authConnector.ts b/packages/no-modal/src/connectors/auth-connector/authConnector.ts index 45a344d64..8af301ca7 100644 --- a/packages/no-modal/src/connectors/auth-connector/authConnector.ts +++ b/packages/no-modal/src/connectors/auth-connector/authConnector.ts @@ -588,16 +588,17 @@ class AuthConnector extends BaseConnector implements IAuthConne } public getChainIdForLinkedAccount(account: Pick, preferredChainId?: string | null): string { + const accountChainNamespace = account.chainNamespace ? parseChainNamespaceFromCitadelResponse(account.chainNamespace) : null; + if (preferredChainId) { const preferredChain = this.coreOptions.chains.find((chain) => chain.chainId === preferredChainId); - if (preferredChain && (!account.chainNamespace || preferredChain.chainNamespace === account.chainNamespace)) { + if (preferredChain && (!accountChainNamespace || preferredChain.chainNamespace === accountChainNamespace)) { return preferredChainId; } } - if (account.chainNamespace) { - const parsedChainNamespace = parseChainNamespaceFromCitadelResponse(account.chainNamespace); - const namespaceChain = this.coreOptions.chains.find((chain) => chain.chainNamespace === parsedChainNamespace); + if (accountChainNamespace) { + const namespaceChain = this.coreOptions.chains.find((chain) => chain.chainNamespace === accountChainNamespace); if (namespaceChain) { return namespaceChain.chainId; } diff --git a/packages/no-modal/src/noModal.ts b/packages/no-modal/src/noModal.ts index e43b69d4d..8bb0b2413 100644 --- a/packages/no-modal/src/noModal.ts +++ b/packages/no-modal/src/noModal.ts @@ -1359,6 +1359,24 @@ export class Web3AuthNoModal extends SafeEventEmitter imp return finalChainId; } + /** + * Resolves the chain ID for a switch account operation. + * If the account's chain namespace is the same as the current chain namespace, return the current chain ID. + * If the account's chain namespace is different from the current chain namespace, return the chainId the account was linked in. + * + * @param account - The account to switch to. + * @param activeChainId - The current active chain ID. + * @returns The resolved chain ID. + */ + protected resolveSwitchAccountChainId(account: Pick, activeChainId: string): string { + const targetChainNamespace = account.chainNamespace ? parseChainNamespaceFromCitadelResponse(account.chainNamespace) : null; + if (targetChainNamespace && this.currentChain.chainNamespace === targetChainNamespace) { + return this.currentChain.chainId; + } + + return activeChainId; + } + protected async createLinkingWalletConnector( connectorName: WALLET_CONNECTOR_TYPE | string, chainId: string, @@ -1532,6 +1550,8 @@ export class Web3AuthNoModal extends SafeEventEmitter imp switchResult: AuthConnectorSwitchAccountResult, options: { walletConnector?: IConnector; projectConfig?: ProjectConfig } = {} ): Promise { + const resolvedSwitchChainId = this.resolveSwitchAccountChainId(switchResult.targetAccount, switchResult.activeChainId); + if (switchResult.kind === "primary") { const existingPrimaryConnectedWalletState = this.getConnectedWalletConnectorState(); const primaryConnectedWalletState = @@ -1556,17 +1576,17 @@ export class Web3AuthNoModal extends SafeEventEmitter imp const walletConnector = options.walletConnector ?? this.getConnectedWalletConnector(switchResult.targetAccount) ?? - (await this.createSwitchingWalletConnector(switchResult.targetAccount.connector, switchResult.activeChainId, options.projectConfig)); + (await this.createSwitchingWalletConnector(switchResult.targetAccount.connector, resolvedSwitchChainId, options.projectConfig)); let linkedAccountConnection: Connection | null = null; try { if (!this.hasUsableConnectedSwitchConnector(walletConnector)) { - const switchChainConfig = this.coreOptions.chains.find((c) => c.chainId === switchResult.activeChainId); + const switchChainConfig = this.coreOptions.chains.find((c) => c.chainId === resolvedSwitchChainId); if (!switchChainConfig) { - throw WalletLoginError.connectionError(`Chain config is not available for chain ${switchResult.activeChainId}`); + throw WalletLoginError.connectionError(`Chain config is not available for chain ${resolvedSwitchChainId}`); } const caipChainId = getCaipChainId(switchChainConfig); const caipAccountId = `${caipChainId}:${switchResult.targetAccount.eoaAddress}` as CaipAccountId; - linkedAccountConnection = await walletConnector.connect({ chainId: switchResult.activeChainId, caipAccountIds: [caipAccountId] }); + linkedAccountConnection = await walletConnector.connect({ chainId: resolvedSwitchChainId, caipAccountIds: [caipAccountId] }); if (!linkedAccountConnection) { throw AccountLinkingError.requestFailed( `Failed to connect isolated connector "${switchResult.targetAccount.connector}" for account switch.` @@ -1588,7 +1608,7 @@ export class Web3AuthNoModal extends SafeEventEmitter imp } } - await this.setCurrentChain(switchResult.activeChainId); + await this.setCurrentChain(resolvedSwitchChainId); await this.setState({ activeAccount: switchResult.activeAccount }); const connection = this.connection; if (!connection) { diff --git a/packages/no-modal/test/noModal.test.ts b/packages/no-modal/test/noModal.test.ts index b53c5656d..8316eaafd 100644 --- a/packages/no-modal/test/noModal.test.ts +++ b/packages/no-modal/test/noModal.test.ts @@ -16,6 +16,7 @@ import { WalletLoginError, WEB3AUTH_STATE_STORAGE_KEY, } from "../src/base"; +import { authConnector } from "../src/connectors/auth-connector"; import { Web3AuthNoModal } from "../src/noModal"; import { createChain, createMockStorage, createProjectConfig, MockConnector, MULTICHAIN_CONNECTOR_NAMESPACE } from "./helpers"; @@ -91,7 +92,7 @@ describe("Web3AuthNoModal", () => { const sdk = createSdk( {}, { - connectedConnectorName: WALLET_CONNECTORS.AUTH, + primaryConnectorName: WALLET_CONNECTORS.AUTH, cachedConnector: WALLET_CONNECTORS.AUTH, currentChainId: "0x1", idToken: "id-token", @@ -139,7 +140,7 @@ describe("Web3AuthNoModal", () => { const sdk = createSdk( {}, { - connectedConnectorName: WALLET_CONNECTORS.AUTH, + primaryConnectorName: WALLET_CONNECTORS.AUTH, currentChainId: "0x1", activeAccount, } @@ -166,7 +167,7 @@ describe("Web3AuthNoModal", () => { const sdk = createSdk( {}, { - connectedConnectorName: WALLET_CONNECTORS.AUTH, + primaryConnectorName: WALLET_CONNECTORS.AUTH, currentChainId: "0x1", } ); @@ -224,12 +225,35 @@ describe("Web3AuthNoModal", () => { }); }); + it("keeps the preferred chain when the linked account stays in the same namespace", () => { + const connector = authConnector()({ + projectConfig: createProjectConfig(), + coreOptions: { + clientId: "test-client-id", + web3AuthNetwork: "sapphire_devnet", + chains: [ + createChain({ + chainId: "0xaa36a7", + rpcTarget: "https://rpc.ankr.com/eth_sepolia", + displayName: "Ethereum Sepolia", + }), + createChain(), + ], + } as never, + analytics: { track: vi.fn() } as never, + }) as unknown as { + getChainIdForLinkedAccount: (account: Pick, preferredChainId?: string | null) => string; + }; + + expect(connector.getChainIdForLinkedAccount(createExternalAccount({ chainNamespace: "evm" }), "0xaa36a7")).toBe("0xaa36a7"); + }); + it("switches back to the primary account without rebinding the AUTH provider proxy", async () => { const activeAccount = createExternalAccount(); const sdk = createSdk( {}, { - connectedConnectorName: WALLET_CONNECTORS.AUTH, + primaryConnectorName: WALLET_CONNECTORS.AUTH, currentChainId: "0x1", } ); @@ -301,12 +325,67 @@ describe("Web3AuthNoModal", () => { }); }); + it("reuses the current chain when switching linked accounts within the same namespace", async () => { + const sdk = createSdk( + { + chains: [ + createChain({ + chainId: "0xaa36a7", + rpcTarget: "https://rpc.ankr.com/eth_sepolia", + displayName: "Ethereum Sepolia", + }), + createChain(), + ], + }, + { + primaryConnectorName: WALLET_CONNECTORS.AUTH, + currentChainId: "0xaa36a7", + } + ); + await Promise.resolve(); + + const targetAccount = createExternalAccount({ chainNamespace: "evm" }); + const linkedProvider = { request: vi.fn() }; + const walletConnector = new MockConnector({ + name: WALLET_CONNECTORS.METAMASK, + status: CONNECTOR_STATUS.READY, + } as never); + walletConnector.connect = vi.fn().mockResolvedValue({ + connectorName: WALLET_CONNECTORS.METAMASK, + ethereumProvider: linkedProvider as never, + solanaWallet: null, + }); + + const authConnector = { + assertSwitchAccountConnectorMatchesTarget: vi.fn().mockResolvedValue(undefined), + toSwitchAccountConnectorError: vi.fn((_: unknown, error: unknown) => error), + } as never; + + await sdk.exposeProcessSwitchAccountResult( + authConnector, + { + kind: "external", + targetAccount, + activeAccount: targetAccount, + activeChainId: "0x1", + }, + { walletConnector } + ); + + expect(walletConnector.connect).toHaveBeenCalledWith( + expect.objectContaining({ + chainId: "0xaa36a7", + }) + ); + expect(sdk.currentChainId).toBe("0xaa36a7"); + }); + it("rehydrates an active linked account without rebinding the AUTH proxy to the linked wallet", async () => { const activeAccount = createExternalAccount(); const sdk = createSdk( {}, { - connectedConnectorName: WALLET_CONNECTORS.AUTH, + primaryConnectorName: WALLET_CONNECTORS.AUTH, cachedConnector: WALLET_CONNECTORS.AUTH, currentChainId: "0x1", activeAccount, @@ -364,7 +443,7 @@ describe("Web3AuthNoModal", () => { it("clearCache resets persisted state fields", async () => { const sdk = createSdk(); (sdk as unknown as { state: Record }).state = { - connectedConnectorName: WALLET_CONNECTORS.AUTH, + primaryConnectorName: WALLET_CONNECTORS.AUTH, cachedConnector: WALLET_CONNECTORS.AUTH, currentChainId: "0x1", idToken: "id-token", @@ -374,7 +453,7 @@ describe("Web3AuthNoModal", () => { await sdk.clearCache(); const state = (sdk as unknown as { state: Record }).state; - expect(state.connectedConnectorName).toBeNull(); + expect(state.primaryConnectorName).toBeNull(); expect(state.cachedConnector).toBeNull(); expect(state.currentChainId).toBeNull(); expect(state.idToken).toBeNull(); @@ -386,7 +465,7 @@ describe("Web3AuthNoModal", () => { const sdk = createSdk( {}, { - connectedConnectorName: WALLET_CONNECTORS.AUTH, + primaryConnectorName: WALLET_CONNECTORS.AUTH, cachedConnector: WALLET_CONNECTORS.AUTH, currentChainId: "0x1", idToken: "id-token", @@ -434,7 +513,7 @@ describe("Web3AuthNoModal", () => { const sdk = createSdk( {}, { - connectedConnectorName: WALLET_CONNECTORS.AUTH, + primaryConnectorName: WALLET_CONNECTORS.AUTH, cachedConnector: WALLET_CONNECTORS.AUTH, currentChainId: "0x1", idToken: "id-token", @@ -522,7 +601,7 @@ describe("Web3AuthNoModal", () => { } as never); (sdk as unknown as { connectors: MockConnector[] }).connectors = [connector]; (sdk as unknown as { state: Record }).state = { - connectedConnectorName: WALLET_CONNECTORS.METAMASK, + primaryConnectorName: WALLET_CONNECTORS.METAMASK, cachedConnector: null, currentChainId: "0x1", idToken: null, @@ -755,7 +834,7 @@ describe("Web3AuthNoModal", () => { it("checkIfAutoConnect returns true only for cached matching connector", () => { const sdk = createSdk(); (sdk as unknown as { state: Record }).state = { - connectedConnectorName: null, + primaryConnectorName: null, cachedConnector: WALLET_CONNECTORS.METAMASK, currentChainId: "0x1", idToken: null,