diff --git a/packages/adapters/wagmi/src/client.ts b/packages/adapters/wagmi/src/client.ts index fd2cbe43f9..b77b203fc9 100644 --- a/packages/adapters/wagmi/src/client.ts +++ b/packages/adapters/wagmi/src/client.ts @@ -1,5 +1,5 @@ import type UniversalProvider from '@walletconnect/universal-provider' -import type { AppKitNetwork, BaseNetwork, CaipNetwork } from '@reown/appkit-common' +import type { AppKitNetwork, BaseNetwork, CaipNetwork, ChainNamespace } from '@reown/appkit-common' import { AdapterBlueprint } from '@reown/appkit/adapters' import { CoreHelperUtil } from '@reown/appkit-core' import { @@ -27,7 +27,8 @@ import { getAccount, prepareTransactionRequest, reconnect, - watchPendingTransactions + watchPendingTransactions, + watchConnectors } from '@wagmi/core' import { type Chain } from '@wagmi/core/chains' @@ -186,7 +187,6 @@ export class WagmiAdapter extends AdapterBlueprint { } } }) - watchConnections(this.wagmiConfig, { onChange: connections => { if (connections.length === 0) { @@ -232,9 +232,7 @@ export class WagmiAdapter extends AdapterBlueprint { customConnectors.push( authConnector({ chains: this.wagmiChains, - options: { projectId: options.projectId }, - provider: this.availableConnectors.find(c => c.id === ConstantsUtil.AUTH_CONNECTOR_ID) - ?.provider as W3mFrameProvider + options: { projectId: options.projectId } }) ) } @@ -358,40 +356,43 @@ export class WagmiAdapter extends AdapterBlueprint { return formatUnits(params.value, params.decimals) } - public syncConnectors(options: AppKitOptions, appKit: AppKit) { - this.addWagmiConnectors(options, appKit) + private addWagmiConnector(connector: Connector, options: AppKitOptions) { + /* + * We don't need to set auth connector from wagmi + * since we already set it in chain adapter blueprint + */ + if (connector.id === ConstantsUtil.AUTH_CONNECTOR_ID) { + return + } - const connectors = this.wagmiConfig.connectors.map(connector => ({ - ...connector, - chain: this.namespace - })) + this.addConnector({ + id: connector.id, + explorerId: PresetsUtil.ConnectorExplorerIds[connector.id], + imageUrl: options?.connectorImages?.[connector.id] ?? connector.icon, + name: PresetsUtil.ConnectorNamesMap[connector.id] ?? connector.name, + imageId: PresetsUtil.ConnectorImageIds[connector.id], + type: PresetsUtil.ConnectorTypesMap[connector.type] ?? 'EXTERNAL', + info: + connector.id === ConstantsUtil.INJECTED_CONNECTOR_ID ? undefined : { rdns: connector.id }, + chain: this.namespace as ChainNamespace, + chains: [] + }) + } - const uniqueIds = new Set() - const filteredConnectors = connectors.filter(item => { - const isDuplicate = uniqueIds.has(item.id) - uniqueIds.add(item.id) + public syncConnectors(options: AppKitOptions, appKit: AppKit) { + // Add wagmi connectors + this.addWagmiConnectors(options, appKit) - return !isDuplicate - }) + // Add current wagmi connectors to chain adapter blueprint + this.wagmiConfig.connectors.forEach(connector => this.addWagmiConnector(connector, options)) - filteredConnectors.forEach(connector => { - const shouldSkip = ConstantsUtil.AUTH_CONNECTOR_ID === connector.id - - const injectedConnector = connector.id === ConstantsUtil.INJECTED_CONNECTOR_ID - - if (!shouldSkip && this.namespace) { - this.addConnector({ - id: connector.id, - explorerId: PresetsUtil.ConnectorExplorerIds[connector.id], - imageUrl: options?.connectorImages?.[connector.id] ?? connector.icon, - name: PresetsUtil.ConnectorNamesMap[connector.id] ?? connector.name, - imageId: PresetsUtil.ConnectorImageIds[connector.id], - type: PresetsUtil.ConnectorTypesMap[connector.type] ?? 'EXTERNAL', - info: injectedConnector ? undefined : { rdns: connector.id }, - chain: this.namespace, - chains: [] - }) - } + /* + * Watch for new connectors. This is needed because some EIP6963 connectors + * that are added later in the process the initial setup + */ + watchConnectors(this.wagmiConfig, { + onChange: connectors => + connectors.forEach(connector => this.addWagmiConnector(connector, options)) }) } diff --git a/packages/adapters/wagmi/src/connectors/AuthConnector.ts b/packages/adapters/wagmi/src/connectors/AuthConnector.ts index a82190a9a6..29f28453c0 100644 --- a/packages/adapters/wagmi/src/connectors/AuthConnector.ts +++ b/packages/adapters/wagmi/src/connectors/AuthConnector.ts @@ -16,7 +16,6 @@ interface W3mFrameProviderOptions { export type AuthParameters = { chains?: CreateConfigParameters['chains'] options: W3mFrameProviderOptions - provider: W3mFrameProvider } // -- Connector ------------------------------------------------------------------------------------ diff --git a/packages/adapters/wagmi/src/connectors/AuthConnectorExport.ts b/packages/adapters/wagmi/src/connectors/AuthConnectorExport.ts index 279e382bc7..e724c12deb 100644 --- a/packages/adapters/wagmi/src/connectors/AuthConnectorExport.ts +++ b/packages/adapters/wagmi/src/connectors/AuthConnectorExport.ts @@ -1,8 +1,5 @@ import type { CreateConfigParameters } from '@wagmi/core' import { authConnector as authConnectorWagmi } from './AuthConnector.js' -import { ErrorUtil } from '@reown/appkit-utils' -import { AlertController } from '@reown/appkit-core' -import { W3mFrameProviderSingleton } from '@reown/appkit/auth-provider' interface W3mFrameProviderOptions { projectId: string @@ -14,13 +11,5 @@ export type AuthParameters = { } export function authConnector(parameters: AuthParameters) { - return authConnectorWagmi({ - ...parameters, - provider: W3mFrameProviderSingleton.getInstance({ - projectId: parameters.options.projectId, - onTimeout: () => { - AlertController.open(ErrorUtil.ALERT_ERRORS.SOCIALS_TIMEOUT, 'error') - } - }) - }) + return authConnectorWagmi(parameters) } diff --git a/packages/adapters/wagmi/src/tests/client.test.ts b/packages/adapters/wagmi/src/tests/client.test.ts index c1bacc5be2..00e4b4e4ba 100644 --- a/packages/adapters/wagmi/src/tests/client.test.ts +++ b/packages/adapters/wagmi/src/tests/client.test.ts @@ -18,9 +18,11 @@ import { watchPendingTransactions, http } from '@wagmi/core' +import * as wagmiCore from '@wagmi/core' import { mainnet } from '@wagmi/core/chains' import { CaipNetworksUtil } from '@reown/appkit-utils' import type UniversalProvider from '@walletconnect/universal-provider' +import { mockAppKit } from './mocks/AppKit' vi.mock('@wagmi/core', async () => { const actual = await vi.importActual('@wagmi/core') @@ -97,6 +99,26 @@ describe('WagmiAdapter', () => { expect(adapter.namespace).toBe('eip155') }) + it('should set wagmi connectors', () => { + vi.spyOn(wagmiCore, 'watchConnectors').mockImplementation(vi.fn()) + + adapter.syncConnectors({ networks: [mainnet], projectId: 'YOUR_PROJECT_ID' }, mockAppKit) + + expect(adapter.connectors).toStrictEqual([ + { + chain: 'eip155', + chains: [], + explorerId: undefined, + id: 'test-connector', + imageId: undefined, + imageUrl: undefined, + info: { rdns: 'test-connector' }, + name: undefined, + type: 'EXTERNAL' + } + ]) + }) + it('should not set info property for injected connector', () => { const mockConnectors = [ { diff --git a/packages/appkit/src/adapters/ChainAdapterBlueprint.ts b/packages/appkit/src/adapters/ChainAdapterBlueprint.ts index 8a8f034fc4..84d785a08e 100644 --- a/packages/appkit/src/adapters/ChainAdapterBlueprint.ts +++ b/packages/appkit/src/adapters/ChainAdapterBlueprint.ts @@ -24,11 +24,17 @@ import type { AppKitOptions } from '../utils/index.js' import type { AppKit } from '../client.js' import { snapshot } from 'valtio/vanilla' -type EventName = 'disconnect' | 'accountChanged' | 'switchNetwork' | 'pendingTransactions' +type EventName = + | 'disconnect' + | 'accountChanged' + | 'switchNetwork' + | 'connectors' + | 'pendingTransactions' type EventData = { disconnect: () => void accountChanged: { address: string; chainId?: number | string } switchNetwork: { address?: string; chainId: number | string } + connectors: ChainAdapterConnector[] pendingTransactions: () => void } type EventCallback = (data: EventData[T]) => void @@ -155,6 +161,8 @@ export abstract class AdapterBlueprint< return true }) + + this.emit('connectors', this.availableConnectors) } protected setStatus(status: AccountControllerState['status'], chainNamespace?: ChainNamespace) { diff --git a/packages/appkit/src/adapters/index.ts b/packages/appkit/src/adapters/index.ts index 9babae1e07..f09b2ba1f4 100644 --- a/packages/appkit/src/adapters/index.ts +++ b/packages/appkit/src/adapters/index.ts @@ -1 +1,2 @@ export { AdapterBlueprint } from './ChainAdapterBlueprint.js' +export type { ChainAdapterConnector } from './ChainAdapterConnector.js' diff --git a/packages/appkit/src/client.ts b/packages/appkit/src/client.ts index f7c5672afd..3adae9e13b 100644 --- a/packages/appkit/src/client.ts +++ b/packages/appkit/src/client.ts @@ -12,7 +12,6 @@ import { type UseAppKitNetworkReturn, type NetworkControllerClient, type ConnectionControllerClient, - ConstantsUtil as CoreConstantsUtil, type ConnectorType, type WriteContractArgs, type Provider, @@ -20,6 +19,7 @@ import { type EstimateGasTransactionArgs, type AccountControllerState, type AdapterNetworkState, + ConstantsUtil as CoreConstantsUtil, type Features, SIWXUtil, type ConnectionStatus, @@ -50,12 +50,12 @@ import { } from '@reown/appkit-core' import { setColorTheme, setThemeVariables } from '@reown/appkit-ui' import { - ConstantsUtil, type CaipNetwork, type ChainNamespace, type CaipAddress, type CaipNetworkId, NetworkUtil, + ConstantsUtil, ParseUtil } from '@reown/appkit-common' import type { AppKitOptions } from './utils/TypesUtil.js' @@ -214,14 +214,10 @@ export class AppKit { ) { this.caipNetworks = this.extendCaipNetworks(options) this.defaultCaipNetwork = this.extendDefaultCaipNetwork(options) - await this.initControllers(options) - this.createAuthProvider() - await this.createUniversalProvider() + this.initControllers(options) this.createClients() ChainController.initialize(options.adapters ?? [], this.caipNetworks) - this.chainAdapters = await this.createAdapters( - options.adapters as unknown as AdapterBlueprint[] - ) + this.chainAdapters = this.createAdapters(options.adapters as unknown as AdapterBlueprint[]) await this.initChainAdapters() this.syncRequestedNetworks() await this.initOrContinue() @@ -662,7 +658,7 @@ export class AppKit { } // -- Private ------------------------------------------------------------------ - private async initControllers( + private initControllers( options: AppKitOptions & { adapters?: ChainAdapter[] } & { @@ -684,8 +680,6 @@ export class AppKit { return } - this.adapters = options.adapters - const defaultMetaData = this.getDefaultMetaData() if (!options.metadata && defaultMetaData) { @@ -742,12 +736,7 @@ export class AppKit { throw new Error('Cannot set both `siweConfig` and `siwx` options') } - const siwe = await import('@reown/appkit-siwe') - if (typeof siwe.mapToSIWX !== 'function') { - throw new Error('Please update the `@reown/appkit-siwe` package to the latest version') - } - - OptionsController.setSIWX(siwe.mapToSIWX(options.siweConfig)) + OptionsController.setSIWX(options.siweConfig.mapToSIWX()) } } } @@ -815,9 +804,7 @@ export class AppKit { connectWalletConnect: async (onUri: (uri: string) => void) => { const adapter = this.getAdapter(ChainController.state.activeChain as ChainNamespace) - this.universalProvider?.on('display_uri', (uri: string) => { - onUri(uri) - }) + this.universalProvider?.on('display_uri', onUri) this.setClientId( (await this.universalProvider?.client?.core?.crypto?.getClientId()) || null @@ -861,37 +848,16 @@ export class AppKit { throw new Error('Adapter not found') } - let res: AdapterBlueprint.ConnectResult | undefined = undefined - try { - res = await adapter.connect({ - id, - info, - type, - provider, - chainId: caipNetwork?.id || this.getCaipNetwork()?.id, - rpcUrl: - caipNetwork?.rpcUrls?.default?.http?.[0] || - this.getCaipNetwork()?.rpcUrls?.default?.http?.[0] - }) - /** - * In some cases with wagmi connectors, the connector is already connected - * which throws an `Is already connected`error. In such cases, we need to reconnect - * to restore the session. - * We check if the reconnect method exists (which it does for wagmi connectors) and if so - * we attempt to reconnect and restore the session state. - */ - } catch (error) { - if (!adapter?.reconnect) { - throw new Error('Adapter is not able to connect') - } - await adapter.reconnect({ - id, - info, - type, - provider, - chainId: this.getCaipNetwork()?.id - }) - } + const res = await adapter.connect({ + id, + info, + type, + provider, + chainId: caipNetwork?.id || this.getCaipNetwork()?.id, + rpcUrl: + caipNetwork?.rpcUrls?.default?.http?.[0] || + this.getCaipNetwork()?.rpcUrls?.default?.http?.[0] + }) if (res) { this.syncProvider({ @@ -1763,7 +1729,7 @@ export class AppKit { private createUniversalProvider() { if ( !this.universalProviderInitPromise && - typeof window !== 'undefined' && + CoreHelperUtil.isClient() && this.options?.projectId ) { this.universalProviderInitPromise = this.initializeUniversalAdapter() @@ -1815,6 +1781,7 @@ export class AppKit { OptionsController.setUsingInjectedUniversalProvider(Boolean(this.options?.universalProvider)) this.universalProvider = this.options.universalProvider ?? (await UniversalProvider.init(universalProviderOptions)) + this.listenWalletConnect() } public async getUniversalProvider() { @@ -1830,14 +1797,14 @@ export class AppKit { } private createAuthProvider() { - const emailEnabled = + const isEmailEnabled = this.options?.features?.email === undefined ? CoreConstantsUtil.DEFAULT_FEATURES.email : this.options?.features?.email - const socialsEnabled = this.options?.features?.socials + const isSocialsEnabled = this.options?.features?.socials ? this.options?.features?.socials?.length > 0 : CoreConstantsUtil.DEFAULT_FEATURES.socials - if (this.options?.projectId && (emailEnabled || socialsEnabled)) { + if (this.options?.projectId && (isEmailEnabled || isSocialsEnabled)) { this.authProvider = W3mFrameProviderSingleton.getInstance({ projectId: this.options.projectId, onTimeout: () => { @@ -1848,11 +1815,23 @@ export class AppKit { } } - private async createAdapters(blueprints?: AdapterBlueprint[]): Promise { - if (!this.universalProvider) { - this.universalProvider = await this.getUniversalProvider() + private async createUniversalProviderForAdapter(chainNamespace: ChainNamespace) { + await this.getUniversalProvider() + + if (this.universalProvider) { + this.chainAdapters?.[chainNamespace].setUniversalProvider(this.universalProvider) + } + } + + private createAuthProviderForAdapter(chainNamespace: ChainNamespace) { + this.createAuthProvider() + + if (this.authProvider) { + this.chainAdapters?.[chainNamespace].setAuthProvider(this.authProvider) } + } + private createAdapters(blueprints?: AdapterBlueprint[]) { this.syncRequestedNetworks() return this.chainNamespaces.reduce((adapters, namespace) => { @@ -1866,25 +1845,11 @@ export class AppKit { projectId: this.options?.projectId, networks: this.caipNetworks }) - if (this.universalProvider) { - adapters[namespace].setUniversalProvider(this.universalProvider) - } - if (this.authProvider) { - adapters[namespace].setAuthProvider(this.authProvider) - } - - adapters[namespace].syncConnectors(this.options, this) } else { adapters[namespace] = new UniversalAdapter({ namespace, networks: this.caipNetworks }) - if (this.universalProvider) { - adapters[namespace].setUniversalProvider(this.universalProvider) - } - if (this.authProvider) { - adapters[namespace].setAuthProvider(this.authProvider) - } } ChainController.state.chains.set(namespace, { @@ -1901,18 +1866,26 @@ export class AppKit { }, {} as Adapters) } + private async createConnectorsForAdapter(namespace: ChainNamespace) { + await this.createUniversalProviderForAdapter(namespace) + this.createAuthProviderForAdapter(namespace) + } + + private onConnectors(chainNamespace: ChainNamespace) { + const adapter = this.getAdapter(chainNamespace) + + adapter?.on('connectors', this.setConnectors.bind(this)) + } + private async initChainAdapters() { await Promise.all( - // eslint-disable-next-line @typescript-eslint/require-await this.chainNamespaces.map(async namespace => { - if (this.options) { - this.listenAdapter(namespace) - - this.setConnectors(this.chainAdapters?.[namespace]?.connectors || []) - } + this.onConnectors(namespace) + this.listenAdapter(namespace) + this.chainAdapters?.[namespace].syncConnectors(this.options, this) + await this.createConnectorsForAdapter(namespace) }) ) - this.listenWalletConnect() } private setDefaultNetwork() { diff --git a/packages/appkit/src/tests/appkit.test.ts b/packages/appkit/src/tests/appkit.test.ts index c0b65b0555..fd92cf94f4 100644 --- a/packages/appkit/src/tests/appkit.test.ts +++ b/packages/appkit/src/tests/appkit.test.ts @@ -1,6 +1,6 @@ import { describe, it, expect, beforeEach, vi } from 'vitest' import { AppKit } from '../client' -import { mainnet, polygon } from '../networks/index.js' +import { base, mainnet, polygon, solana } from '../networks/index.js' import { AccountController, ModalController, @@ -20,24 +20,30 @@ import { type Connector, StorageUtil, CoreHelperUtil, - AlertController + AlertController, + type ConnectorType } from '@reown/appkit-core' -import { - SafeLocalStorage, - SafeLocalStorageKeys, - type CaipNetwork, - type SafeLocalStorageItems -} from '@reown/appkit-common' +import { SafeLocalStorage, SafeLocalStorageKeys, type CaipNetwork } from '@reown/appkit-common' import { mockOptions } from './mocks/Options' import { UniversalAdapter } from '../universal-adapter/client' import type { AdapterBlueprint } from '../adapters/ChainAdapterBlueprint' import { ProviderUtil } from '../store' -import { ErrorUtil } from '@reown/appkit-utils' +import { CaipNetworksUtil, ErrorUtil } from '@reown/appkit-utils' +import mockUniversalAdapter from './mocks/Adapter' import { UniversalProvider } from '@walletconnect/universal-provider' // Mock all controllers and UniversalAdapterClient vi.mock('@reown/appkit-core') vi.mock('../universal-adapter/client') +vi.mock('../client.ts', async () => { + const actual = await vi.importActual('../client.ts') + + return { + ...actual, + initOrContinue: vi.fn(), + syncExistingConnection: vi.fn() + } +}) vi.mocked(global).window = { location: { origin: '' } } as any vi.mocked(global).document = { @@ -61,23 +67,32 @@ describe('Base', () => { } as any vi.mocked(ConnectorController).getConnectors = vi.fn().mockReturnValue([]) + vi.mocked(CaipNetworksUtil).extendCaipNetworks = vi.fn().mockReturnValue([]) + appKit = new AppKit(mockOptions) }) describe('Base Initialization', () => { - it('should initialize controllers with required provided options', () => { - expect(OptionsController.setSdkVersion).toHaveBeenCalledWith(mockOptions.sdkVersion) - expect(OptionsController.setProjectId).toHaveBeenCalledWith(mockOptions.projectId) - expect(OptionsController.setMetadata).toHaveBeenCalledWith(mockOptions.metadata) - + it('should initialize controllers', () => { const copyMockOptions = { ...mockOptions } + delete copyMockOptions.adapters - expect(EventsController.sendEvent).toHaveBeenCalledWith(mockOptions) - }) + expect(EventsController.sendEvent).toHaveBeenCalledOnce() + expect(EventsController.sendEvent).toHaveBeenCalledWith({ + type: 'track', + event: 'INITIALIZE', + properties: { + ...copyMockOptions, + networks: copyMockOptions.networks.map(n => n.id), + siweConfig: { + options: copyMockOptions.siweConfig?.options || {} + } + } + }) - it('should initialize adapters in ChainController', () => { - expect(ChainController.initialize).toHaveBeenCalledWith(mockOptions.adapters) + expect(ChainController.initialize).toHaveBeenCalledOnce() + expect(ChainController.initialize).toHaveBeenCalledWith(mockOptions.adapters, []) }) it('should set EIP6963 enabled by default', () => { @@ -513,9 +528,15 @@ describe('Base', () => { }) it('should switch network when requested', async () => { + vi.mocked(CaipNetworksUtil).extendCaipNetworks = vi + .fn() + .mockReturnValue([{ id: mainnet.id, name: mainnet.name }]) + + const mockAppKit = new AppKit(mockOptions) + vi.mocked(ChainController.switchActiveNetwork).mockResolvedValue(undefined) - await appKit.switchNetwork(mainnet) + await mockAppKit.switchNetwork(mainnet) expect(ChainController.switchActiveNetwork).toHaveBeenCalledWith( expect.objectContaining({ @@ -524,7 +545,7 @@ describe('Base', () => { }) ) - await appKit.switchNetwork(polygon) + await mockAppKit.switchNetwork(polygon) expect(ChainController.switchActiveNetwork).toHaveBeenCalledTimes(1) }) @@ -536,6 +557,11 @@ describe('Base', () => { } as Connector vi.mocked(ConnectorController.getConnectors).mockReturnValue([mockConnector]) + vi.mocked(StorageUtil.getActiveNetworkProps).mockReturnValue({ + namespace: 'eip155', + chainId: '1', + caipNetworkId: '1' + }) const mockAccountData = { address: '0x123', @@ -543,13 +569,8 @@ describe('Base', () => { chainNamespace: 'eip155' as const } - vi.spyOn(SafeLocalStorage, 'getItem').mockImplementation( - (key: keyof SafeLocalStorageItems) => { - if (key === SafeLocalStorageKeys.CONNECTED_CONNECTOR) { - return mockConnector.id - } - return undefined - } + vi.spyOn(StorageUtil, 'getConnectedConnector').mockReturnValue( + mockConnector.id as ConnectorType ) await appKit['syncAccount'](mockAccountData) @@ -568,6 +589,13 @@ describe('Base', () => { chainId: '1', chainNamespace: 'eip155' as const } + + vi.mocked(StorageUtil.getActiveNetworkProps).mockReturnValue({ + namespace: 'eip155', + chainId: '1', + caipNetworkId: '1' + }) + vi.mocked(BlockchainApiController.fetchIdentity).mockResolvedValue({ name: 'John Doe', avatar: null @@ -585,27 +613,35 @@ describe('Base', () => { }) it('should disconnect correctly', async () => { + vi.mocked(CaipNetworksUtil.extendCaipNetworks).mockReturnValue([ + { id: 'eip155:1', chainNamespace: 'eip155' } as CaipNetwork + ]) + vi.mocked(ChainController).state = { chains: new Map([['eip155', { namespace: 'eip155' }]]), activeChain: 'eip155' } as any const mockRemoveItem = vi.fn() - vi.spyOn(SafeLocalStorage, 'removeItem').mockImplementation(mockRemoveItem) - await appKit.disconnect() + vi.spyOn(SafeLocalStorage, 'removeItem').mockImplementation(mockRemoveItem) - expect(mockRemoveItem).toHaveBeenCalledWith(SafeLocalStorageKeys.CONNECTED_CONNECTOR) - expect(mockRemoveItem).toHaveBeenCalledWith(SafeLocalStorageKeys.ACTIVE_CAIP_NETWORK_ID) + const appKit = new AppKit({ + ...mockOptions, + networks: [base], + projectId: 'YOUR_PROJECT_ID', + adapters: [mockUniversalAdapter] + }) - expect(AccountController.resetAccount).toHaveBeenCalledWith('eip155') + await appKit.disconnect() + expect(mockUniversalAdapter.disconnect).toHaveBeenCalled() expect(AccountController.setStatus).toHaveBeenCalledWith('disconnected', 'eip155') - expect(AccountController.resetAccount).toHaveBeenCalledWith('eip155') }) it('should set unsupported chain when synced chainId is not supported', async () => { - const isClientSpy = vi.spyOn(CoreHelperUtil, 'isClient').mockReturnValue(true) + vi.mocked(StorageUtil.getConnectedConnector).mockReturnValue('EXTERNAL') + vi.mocked(StorageUtil.getActiveNamespace).mockReturnValue('eip155') vi.mocked(ChainController).state = { chains: new Map([['eip155', { namespace: 'eip155' }]]), activeChain: 'eip155' @@ -629,6 +665,8 @@ describe('Base', () => { vi.spyOn(StorageUtil, 'setConnectedConnector').mockImplementation(vi.fn()) + vi.spyOn(appKit as any, 'syncAccount').mockImplementation(vi.fn()) + vi.spyOn(appKit as any, 'setUnsupportedNetwork').mockImplementation(vi.fn()) vi.spyOn(SafeLocalStorage, 'getItem').mockImplementation((key: string) => { @@ -646,7 +684,6 @@ describe('Base', () => { await (appKit as any).syncExistingConnection() expect((appKit as any).setUnsupportedNetwork).toHaveBeenCalled() - expect(isClientSpy).toHaveBeenCalled() }) it('should not show unsupported chain UI when allowUnsupportedChain is true', async () => { @@ -722,14 +759,12 @@ describe('Base', () => { describe('syncExistingConnection', () => { it('should set status to "connecting" and sync the connection when a connector and namespace are present', async () => { vi.mocked(CoreHelperUtil.isClient).mockReturnValueOnce(true) - vi.spyOn(SafeLocalStorage, 'getItem').mockImplementation(key => { - if (key === SafeLocalStorageKeys.CONNECTED_CONNECTOR) { - return 'test-wallet' - } - if (key === SafeLocalStorageKeys.ACTIVE_CAIP_NETWORK_ID) { - return 'eip155:1' - } - return undefined + vi.spyOn(StorageUtil, 'getActiveNamespace').mockReturnValue('eip155') + vi.spyOn(StorageUtil, 'getConnectedConnector').mockReturnValue('EXTERNAL') + vi.mocked(StorageUtil.getActiveNetworkProps).mockReturnValue({ + namespace: 'eip155', + chainId: '1', + caipNetworkId: '1' }) const mockAdapter = { @@ -827,22 +862,38 @@ describe('Base', () => { }) it('should call syncConnectors when initializing adapters', async () => { - const createAdapters = (appKit as any).createAdapters.bind(appKit) + vi.mocked(CaipNetworksUtil.extendCaipNetworks).mockReturnValue([ + { id: 'eip155:1', chainNamespace: 'eip155' } as CaipNetwork + ]) - vi.spyOn(appKit as any, 'createUniversalProvider').mockResolvedValue(undefined) + const appKit = new AppKit({ + ...mockOptions, + networks: [base], + projectId: 'YOUR_PROJECT_ID', + adapters: [mockAdapter] + }) - await createAdapters([mockAdapter]) + const initChainAdapters = (appKit as any).initChainAdapters.bind(appKit) - expect(mockAdapter.syncConnectors).toHaveBeenCalledWith( - expect.objectContaining({ - projectId: mockOptions.projectId, - metadata: mockOptions.metadata - }), - expect.any(Object) - ) + vi.spyOn(appKit as any, 'createConnectorsForAdapter').mockResolvedValue(undefined) + + await initChainAdapters([mockAdapter]) + + expect(mockAdapter.syncConnectors).toHaveBeenCalled() }) it('should create UniversalAdapter when no blueprint is provided for namespace', async () => { + vi.mocked(CaipNetworksUtil.extendCaipNetworks).mockReturnValue([ + { id: 'eip155:1', chainNamespace: 'eip155' } as CaipNetwork + ]) + + const appKit = new AppKit({ + ...mockOptions, + networks: [mainnet], + projectId: 'YOUR_PROJECT_ID', + adapters: [mockAdapter] + }) + const createAdapters = (appKit as any).createAdapters.bind(appKit) vi.spyOn(appKit as any, 'createUniversalProvider').mockResolvedValue(undefined) @@ -857,7 +908,11 @@ describe('Base', () => { const adapters = await createAdapters([]) expect(adapters.eip155).toBeDefined() - expect(mockUniversalAdapter.setUniversalProvider).toHaveBeenCalled() + + expect(UniversalAdapter).toHaveBeenCalledWith({ + namespace: 'eip155', + networks: [{ id: 'eip155:1', chainNamespace: 'eip155' } as CaipNetwork] + }) }) it('should initialize UniversalProvider when not provided in options', () => { @@ -885,7 +940,10 @@ describe('Base', () => { }) it('should initialize multiple adapters for different namespaces', async () => { - const createAdapters = (appKit as any).createAdapters.bind(appKit) + vi.mocked(CaipNetworksUtil.extendCaipNetworks).mockReturnValue([ + { id: '1', chainNamespace: 'eip155' } as CaipNetwork, + { id: 'solana', chainNamespace: 'solana' } as CaipNetwork + ]) const mockSolanaAdapter = { namespace: 'solana', @@ -899,6 +957,15 @@ describe('Base', () => { emit: vi.fn() } as unknown as AdapterBlueprint + const appKit = new AppKit({ + ...mockOptions, + networks: [mainnet, solana], + projectId: 'YOUR_PROJECT_ID', + adapters: [mockSolanaAdapter, mockAdapter] + }) + + const createAdapters = (appKit as any).createAdapters.bind(appKit) + vi.spyOn(appKit as any, 'createUniversalProvider').mockResolvedValue(undefined) const adapters = await createAdapters([mockAdapter, mockSolanaAdapter]) @@ -910,29 +977,47 @@ describe('Base', () => { }) it('should set universal provider and auth provider for each adapter', async () => { - const createAdapters = (appKit as any).createAdapters.bind(appKit) + vi.mocked(CaipNetworksUtil.extendCaipNetworks).mockReturnValue([ + { id: '1', chainNamespace: 'eip155' } as CaipNetwork + ]) + + const appKit = new AppKit({ + ...mockOptions, + networks: [mainnet], + projectId: 'YOUR_PROJECT_ID', + adapters: [mockAdapter] + }) const mockUniversalProvider = { on: vi.fn(), off: vi.fn(), emit: vi.fn() } - vi.spyOn(appKit as any, 'createUniversalProvider').mockResolvedValue(undefined) - vi.spyOn(appKit as any, 'getUniversalProvider').mockResolvedValue(mockUniversalProvider) - await createAdapters([mockAdapter]) + vi.spyOn(appKit as any, 'initialize').mockResolvedValue(undefined) + vi.spyOn(CoreHelperUtil, 'isClient').mockReturnValue(true) + vi.spyOn(UniversalProvider, 'init').mockResolvedValue(mockUniversalProvider as any) - expect(mockAdapter.setUniversalProvider).toHaveBeenCalledWith( - expect.objectContaining({ - on: expect.any(Function), - off: expect.any(Function), - emit: expect.any(Function) - }) - ) + const initChainAdapters = (appKit as any).initChainAdapters.bind(appKit) + + await initChainAdapters([mockAdapter]) + + expect(mockAdapter.setUniversalProvider).toHaveBeenCalled() expect(mockAdapter.setAuthProvider).toHaveBeenCalled() }) it('should update ChainController state with initialized adapters', async () => { + vi.mocked(CaipNetworksUtil.extendCaipNetworks).mockReturnValue([ + { id: '1', chainNamespace: 'eip155' } as CaipNetwork + ]) + + const appKit = new AppKit({ + ...mockOptions, + networks: [mainnet], + projectId: 'YOUR_PROJECT_ID', + adapters: [mockAdapter] + }) + const createAdapters = (appKit as any).createAdapters.bind(appKit) vi.spyOn(appKit as any, 'createUniversalProvider').mockResolvedValue(undefined) diff --git a/packages/appkit/src/tests/mocks/Options.ts b/packages/appkit/src/tests/mocks/Options.ts index 6db58d5e31..ac248a4015 100644 --- a/packages/appkit/src/tests/mocks/Options.ts +++ b/packages/appkit/src/tests/mocks/Options.ts @@ -2,10 +2,19 @@ import type { ChainAdapter } from '@reown/appkit-core' import type { AppKitOptions } from '../../utils/index.js' import { mainnet, solana } from '../../networks/index.js' import type { SdkVersion } from '@reown/appkit-core' +import { vi } from 'vitest' export const mockOptions = { projectId: 'test-project-id', - adapters: [{ chainNamespace: 'eip155' } as unknown as ChainAdapter], + adapters: [ + { + chainNamespace: 'eip155', + construct: vi.fn(), + on: vi.fn(), + syncConnectors: vi.fn(), + setAuthProvider: vi.fn() + } as unknown as ChainAdapter + ], networks: [mainnet, solana], metadata: { name: 'Test App', diff --git a/packages/appkit/src/tests/siwe.test.ts b/packages/appkit/src/tests/siwe.test.ts index d38919ac28..e58a7d677f 100644 --- a/packages/appkit/src/tests/siwe.test.ts +++ b/packages/appkit/src/tests/siwe.test.ts @@ -1,12 +1,13 @@ import { beforeEach, describe, expect, it, vi } from 'vitest' import { + ChainController, ConnectionController, ModalController, OptionsController, RouterController, SIWXUtil } from '@reown/appkit-core' -import { AppKit } from '@reown/appkit' +import { AppKit, type CaipNetwork } from '@reown/appkit' import * as networks from '@reown/appkit/networks' import { createSIWEConfig, type AppKitSIWEClient } from '@reown/appkit-siwe' import { mockUniversalAdapter } from './mocks/Adapter' @@ -84,7 +85,12 @@ describe('SIWE mapped to SIWX', () => { }) it('should initializeIfEnabled', async () => { - vi.spyOn(siweConfig.methods, 'getSession').mockResolvedValueOnce(null) + vi.spyOn(ChainController, 'checkIfSupportedNetwork').mockReturnValue(true) + + OptionsController.state.siwx = { + getSessions: vi.fn().mockResolvedValueOnce([]) + } as any + await SIWXUtil.initializeIfEnabled() expect(RouterController.state.view).toBe('SIWXSignMessage') @@ -96,6 +102,12 @@ describe('SIWE mapped to SIWX', () => { const createMessageSpy = vi.spyOn(siweConfig.methods, 'createMessage') const verifyMessageSpy = vi.spyOn(siweConfig.methods, 'verifyMessage') + vi.spyOn(ChainController, 'getActiveCaipNetwork').mockReturnValue({ + id: '1', + name: 'Ethereum', + caipNetworkId: 'eip155:1' + } as unknown as CaipNetwork) + await SIWXUtil.requestSignMessage() expect(getNonceSpy).toHaveBeenCalled() diff --git a/packages/core/src/controllers/ConnectorController.ts b/packages/core/src/controllers/ConnectorController.ts index fef906384d..b50868a06d 100644 --- a/packages/core/src/controllers/ConnectorController.ts +++ b/packages/core/src/controllers/ConnectorController.ts @@ -144,7 +144,7 @@ export const ConnectorController = { projectId: optionsState.projectId, sdkType: optionsState.sdkType }) - authConnector.provider.syncTheme({ + authConnector?.provider?.syncTheme({ themeMode, themeVariables, w3mThemeVariables: getW3mThemeVariables(themeVariables, themeMode) diff --git a/packages/siwe/src/client.ts b/packages/siwe/src/client.ts index 04bbbcc4ae..e790541e96 100644 --- a/packages/siwe/src/client.ts +++ b/packages/siwe/src/client.ts @@ -7,7 +7,7 @@ import type { SIWESession, SIWEVerifyMessageArgs } from '../core/utils/TypeUtils.js' - +import { mapToSIWX } from '../src/mapToSIWX.js' import { SIWXUtil } from '@reown/appkit-core' import { ConstantsUtil } from '../core/utils/ConstantsUtil.js' @@ -41,6 +41,10 @@ export class AppKitSIWEClient { this.methods = siweConfigMethods } + public mapToSIWX() { + return mapToSIWX(this) + } + async getNonce(address?: string) { const nonce = await this.methods.getNonce(address) if (!nonce) {