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
15 changes: 3 additions & 12 deletions demo/vue-app-new/src/components/AppDashboard.vue
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -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,
});
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -182,7 +173,7 @@ const onGetPrivateKey = async () => {
};

const getConnectedChainId = async () => {
printToConsole("chainId", wagmiChainId.value);
printToConsole("chainId", web3Auth.value?.currentChain?.chainId);
};

const onGetBalance = async () => {
Expand Down
2 changes: 1 addition & 1 deletion packages/modal/src/modalManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -215,7 +215,7 @@ export class Web3Auth extends Web3AuthNoModal implements IWeb3AuthModal {
public async connect(): Promise<Connection | null> {
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.
Expand Down
14 changes: 7 additions & 7 deletions packages/modal/test/modalManager.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -49,11 +49,11 @@ class TestWeb3Auth extends Web3Auth {
return this.getInitializationTrackData();
}

public exposeSetConnectedWalletConnector(connector: IConnector<unknown>, account?: LinkedAccountInfo | null) {
public exposeSetConnectedWalletConnector(connector: IConnector<unknown>, account?: ConnectedAccountInfo | null) {
this.setConnectedWalletConnector(connector, account);
}

public exposeSetActiveWalletConnectorKey(account?: LinkedAccountInfo | null) {
public exposeSetActiveWalletConnectorKey(account?: ConnectedAccountInfo | null) {
this.setActiveWalletConnectorKey(account);
}
}
Expand Down Expand Up @@ -85,7 +85,7 @@ function createSdk(overrides: Partial<Web3AuthOptions> = {}) {
} as never);
}

function createConnectedWalletAccount(overrides: Partial<LinkedAccountInfo> = {}): LinkedAccountInfo {
function createConnectedWalletAccount(overrides: Partial<ConnectedAccountInfo> = {}): ConnectedAccountInfo {
return {
id: "wallet-1",
accountType: "external_wallet",
Expand Down Expand Up @@ -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<string, unknown> }).state = {
connectedConnectorName: WALLET_CONNECTORS.AUTH,
primaryConnectorName: WALLET_CONNECTORS.AUTH,
cachedConnector: null,
currentChainId: "0x1",
idToken: null,
Expand Down Expand Up @@ -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);

Expand Down Expand Up @@ -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);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -588,16 +588,17 @@ class AuthConnector extends BaseConnector<AuthLoginParams> implements IAuthConne
}

public getChainIdForLinkedAccount(account: Pick<LinkedAccountInfo, "chainNamespace" | "connector">, 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;
}
Expand Down
30 changes: 25 additions & 5 deletions packages/no-modal/src/noModal.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1359,6 +1359,24 @@ export class Web3AuthNoModal extends SafeEventEmitter<Web3AuthNoModalEvents> 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<LinkedAccountInfo, "chainNamespace">, activeChainId: string): string {
const targetChainNamespace = account.chainNamespace ? parseChainNamespaceFromCitadelResponse(account.chainNamespace) : null;
if (targetChainNamespace && this.currentChain.chainNamespace === targetChainNamespace) {
return this.currentChain.chainId;
Comment thread
chaitanyapotti marked this conversation as resolved.
}

return activeChainId;
}

protected async createLinkingWalletConnector(
connectorName: WALLET_CONNECTOR_TYPE | string,
chainId: string,
Expand Down Expand Up @@ -1532,6 +1550,8 @@ export class Web3AuthNoModal extends SafeEventEmitter<Web3AuthNoModalEvents> imp
switchResult: AuthConnectorSwitchAccountResult,
options: { walletConnector?: IConnector<unknown>; projectConfig?: ProjectConfig } = {}
): Promise<void> {
const resolvedSwitchChainId = this.resolveSwitchAccountChainId(switchResult.targetAccount, switchResult.activeChainId);

if (switchResult.kind === "primary") {
const existingPrimaryConnectedWalletState = this.getConnectedWalletConnectorState();
const primaryConnectedWalletState =
Expand All @@ -1556,17 +1576,17 @@ export class Web3AuthNoModal extends SafeEventEmitter<Web3AuthNoModalEvents> 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.`
Expand All @@ -1588,7 +1608,7 @@ export class Web3AuthNoModal extends SafeEventEmitter<Web3AuthNoModalEvents> imp
}
}

await this.setCurrentChain(switchResult.activeChainId);
await this.setCurrentChain(resolvedSwitchChainId);
await this.setState({ activeAccount: switchResult.activeAccount });
const connection = this.connection;
if (!connection) {
Expand Down
101 changes: 90 additions & 11 deletions packages/no-modal/test/noModal.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";

Expand Down Expand Up @@ -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",
Expand Down Expand Up @@ -139,7 +140,7 @@ describe("Web3AuthNoModal", () => {
const sdk = createSdk(
{},
{
connectedConnectorName: WALLET_CONNECTORS.AUTH,
primaryConnectorName: WALLET_CONNECTORS.AUTH,
currentChainId: "0x1",
activeAccount,
}
Expand All @@ -166,7 +167,7 @@ describe("Web3AuthNoModal", () => {
const sdk = createSdk(
{},
{
connectedConnectorName: WALLET_CONNECTORS.AUTH,
primaryConnectorName: WALLET_CONNECTORS.AUTH,
currentChainId: "0x1",
}
);
Expand Down Expand Up @@ -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<LinkedAccountInfo, "chainNamespace" | "connector">, 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",
}
);
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -364,7 +443,7 @@ describe("Web3AuthNoModal", () => {
it("clearCache resets persisted state fields", async () => {
const sdk = createSdk();
(sdk as unknown as { state: Record<string, unknown> }).state = {
connectedConnectorName: WALLET_CONNECTORS.AUTH,
primaryConnectorName: WALLET_CONNECTORS.AUTH,
cachedConnector: WALLET_CONNECTORS.AUTH,
currentChainId: "0x1",
idToken: "id-token",
Expand All @@ -374,7 +453,7 @@ describe("Web3AuthNoModal", () => {

await sdk.clearCache();
const state = (sdk as unknown as { state: Record<string, unknown> }).state;
expect(state.connectedConnectorName).toBeNull();
expect(state.primaryConnectorName).toBeNull();
expect(state.cachedConnector).toBeNull();
expect(state.currentChainId).toBeNull();
expect(state.idToken).toBeNull();
Expand All @@ -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",
Expand Down Expand Up @@ -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",
Expand Down Expand Up @@ -522,7 +601,7 @@ describe("Web3AuthNoModal", () => {
} as never);
(sdk as unknown as { connectors: MockConnector[] }).connectors = [connector];
(sdk as unknown as { state: Record<string, unknown> }).state = {
connectedConnectorName: WALLET_CONNECTORS.METAMASK,
primaryConnectorName: WALLET_CONNECTORS.METAMASK,
cachedConnector: null,
currentChainId: "0x1",
idToken: null,
Expand Down Expand Up @@ -755,7 +834,7 @@ describe("Web3AuthNoModal", () => {
it("checkIfAutoConnect returns true only for cached matching connector", () => {
const sdk = createSdk();
(sdk as unknown as { state: Record<string, unknown> }).state = {
connectedConnectorName: null,
primaryConnectorName: null,
cachedConnector: WALLET_CONNECTORS.METAMASK,
currentChainId: "0x1",
idToken: null,
Expand Down