From 427dde3cfb3bcb8a61d22b3732150c39958483e8 Mon Sep 17 00:00:00 2001 From: Felipe Mendes Date: Fri, 10 Jan 2025 09:50:00 -0300 Subject: [PATCH] refactor: solana adapter clean up and get accounts (#3523) Co-authored-by: tomiir --- .changeset/breezy-candles-nail.md | 23 ++ packages/adapters/ethers/src/client.ts | 62 +++-- .../adapters/ethers/src/tests/client.test.ts | 7 +- .../ethers/src/tests/mocks/AuthConnector.ts | 9 +- packages/adapters/ethers5/src/client.ts | 56 ++-- .../adapters/ethers5/src/tests/client.test.ts | 7 +- packages/adapters/solana/src/client.ts | 240 +++++------------- .../solana/src/providers/AuthProvider.ts | 183 +++++-------- .../src/providers/CoinbaseWalletProvider.ts | 62 +++-- .../src/providers/WalletConnectProvider.ts | 73 ++---- .../src/providers/WalletStandardProvider.ts | 41 ++- .../solana/src/tests/AuthProvider.test.ts | 31 +-- .../solana/src/tests/GenericProvider.test.ts | 22 +- .../adapters/solana/src/tests/client.test.ts | 117 ++++++--- .../src/tests/mocks/W3mFrameProvider.ts | 2 + packages/adapters/wagmi/src/client.ts | 2 +- .../wagmi/src/connectors/AuthConnector.ts | 6 +- .../src/solana/SolanaTypesUtil.ts | 28 +- .../src/adapters/ChainAdapterBlueprint.ts | 59 ++--- packages/appkit/src/client.ts | 67 +++-- .../appkit/src/universal-adapter/client.ts | 4 +- packages/appkit/tests/appkit.test.ts | 39 +-- packages/appkit/tests/mocks/Options.ts | 10 +- .../appkit/tests/universal-adapter.test.ts | 1 + .../core/src/controllers/ChainController.ts | 11 +- .../core/src/controllers/OptionsController.ts | 6 +- packages/wallet/src/W3mFrameProvider.ts | 13 +- 27 files changed, 566 insertions(+), 615 deletions(-) create mode 100644 .changeset/breezy-candles-nail.md diff --git a/.changeset/breezy-candles-nail.md b/.changeset/breezy-candles-nail.md new file mode 100644 index 0000000000..f7709f457c --- /dev/null +++ b/.changeset/breezy-candles-nail.md @@ -0,0 +1,23 @@ +--- +'@reown/appkit-adapter-solana': patch +'@reown/appkit-utils': patch +'@reown/appkit-adapter-bitcoin': patch +'@reown/appkit-adapter-ethers': patch +'@reown/appkit-adapter-ethers5': patch +'@reown/appkit-adapter-wagmi': patch +'@reown/appkit': patch +'@reown/appkit-cdn': patch +'@reown/appkit-cli': patch +'@reown/appkit-common': patch +'@reown/appkit-core': patch +'@reown/appkit-experimental': patch +'@reown/appkit-polyfills': patch +'@reown/appkit-scaffold-ui': patch +'@reown/appkit-siwe': patch +'@reown/appkit-siwx': patch +'@reown/appkit-ui': patch +'@reown/appkit-wallet': patch +'@reown/appkit-wallet-button': patch +--- + +Abstracts Connectors management in Solana Adapter diff --git a/packages/adapters/ethers/src/client.ts b/packages/adapters/ethers/src/client.ts index f161f4782f..0d220310b9 100644 --- a/packages/adapters/ethers/src/client.ts +++ b/packages/adapters/ethers/src/client.ts @@ -523,42 +523,38 @@ export class EthersAdapter extends AdapterBlueprint { } } - public async switchNetwork(params: AdapterBlueprint.SwitchNetworkParams): Promise { + public override async switchNetwork(params: AdapterBlueprint.SwitchNetworkParams): Promise { const { caipNetwork, provider, providerType } = params - if (providerType === 'WALLET_CONNECT') { - ;(provider as UniversalProvider).setDefaultChain(String(`eip155:${String(caipNetwork.id)}`)) - } else if (providerType === 'AUTH') { - const authProvider = provider as W3mFrameProvider - await authProvider.switchNetwork(caipNetwork.id) - await authProvider.connect({ - chainId: caipNetwork.id + + if (providerType === 'AUTH' || providerType === 'WALLET_CONNECT') { + await super.switchNetwork(params) + + return + } + + try { + await (provider as Provider).request({ + method: 'wallet_switchEthereumChain', + params: [{ chainId: EthersHelpersUtil.numberToHexString(caipNetwork.id) }] }) - } else { - try { - await (provider as Provider).request({ - method: 'wallet_switchEthereumChain', - params: [{ chainId: EthersHelpersUtil.numberToHexString(caipNetwork.id) }] - }) - // eslint-disable-next-line @typescript-eslint/no-explicit-any - } catch (switchError: any) { - if ( - switchError.code === WcConstantsUtil.ERROR_CODE_UNRECOGNIZED_CHAIN_ID || - switchError.code === WcConstantsUtil.ERROR_CODE_DEFAULT || - switchError?.data?.originalError?.code === - WcConstantsUtil.ERROR_CODE_UNRECOGNIZED_CHAIN_ID - ) { - try { - await EthersHelpersUtil.addEthereumChain(provider as Provider, caipNetwork) - } catch (e) { - console.warn('Could not add chain to wallet', e) - } - } else if ( - providerType === 'ANNOUNCED' || - providerType === 'EXTERNAL' || - providerType === 'INJECTED' - ) { - throw new Error('Chain is not supported') + // eslint-disable-next-line @typescript-eslint/no-explicit-any + } catch (switchError: any) { + if ( + switchError.code === WcConstantsUtil.ERROR_CODE_UNRECOGNIZED_CHAIN_ID || + switchError.code === WcConstantsUtil.ERROR_CODE_DEFAULT || + switchError?.data?.originalError?.code === WcConstantsUtil.ERROR_CODE_UNRECOGNIZED_CHAIN_ID + ) { + try { + await EthersHelpersUtil.addEthereumChain(provider as Provider, caipNetwork) + } catch (e) { + console.warn('Could not add chain to wallet', e) } + } else if ( + providerType === 'ANNOUNCED' || + providerType === 'EXTERNAL' || + providerType === 'INJECTED' + ) { + throw new Error('Chain is not supported') } } } diff --git a/packages/adapters/ethers/src/tests/client.test.ts b/packages/adapters/ethers/src/tests/client.test.ts index 33f5739a86..5d03f8615e 100644 --- a/packages/adapters/ethers/src/tests/client.test.ts +++ b/packages/adapters/ethers/src/tests/client.test.ts @@ -75,7 +75,8 @@ const mockWalletConnectProvider = { const mockAuthProvider = { connect: vi.fn(), disconnect: vi.fn(), - switchNetwork: vi.fn() + switchNetwork: vi.fn(), + getUser: vi.fn() } as unknown as W3mFrameProvider const mockNetworks = [mainnet] @@ -312,8 +313,8 @@ describe('EthersAdapter', () => { providerType: 'AUTH' }) - expect(mockAuthProvider.switchNetwork).toHaveBeenCalledWith(1) - expect(mockAuthProvider.connect).toHaveBeenCalledWith({ chainId: 1 }) + expect(mockAuthProvider.switchNetwork).toHaveBeenCalledWith('eip155:1') + expect(mockAuthProvider.getUser).toHaveBeenCalledWith({ chainId: 'eip155:1' }) }) it('should add Ethereum chain with external provider and use chain default', async () => { diff --git a/packages/adapters/ethers/src/tests/mocks/AuthConnector.ts b/packages/adapters/ethers/src/tests/mocks/AuthConnector.ts index bc7d53af51..c7a39dfd45 100644 --- a/packages/adapters/ethers/src/tests/mocks/AuthConnector.ts +++ b/packages/adapters/ethers/src/tests/mocks/AuthConnector.ts @@ -25,5 +25,12 @@ export const mockAuthConnector = { disconnect: vi.fn(), switchNetwork: vi.fn(), rejectRpcRequests: vi.fn(), - request: vi.fn() + request: vi.fn(), + getUser: vi.fn().mockResolvedValue({ + address: '0x1234567890123456789012345678901234567890', + chainId: 1, + smartAccountDeployed: true, + preferredAccountType: 'eoa', + accounts: [] + }) } diff --git a/packages/adapters/ethers5/src/client.ts b/packages/adapters/ethers5/src/client.ts index ccd3c042ee..9a17003370 100644 --- a/packages/adapters/ethers5/src/client.ts +++ b/packages/adapters/ethers5/src/client.ts @@ -526,38 +526,34 @@ export class Ethers5Adapter extends AdapterBlueprint { } } - public async switchNetwork(params: AdapterBlueprint.SwitchNetworkParams): Promise { + public override async switchNetwork(params: AdapterBlueprint.SwitchNetworkParams): Promise { const { caipNetwork, provider, providerType } = params - if (providerType === 'WALLET_CONNECT') { - ;(provider as UniversalProvider).setDefaultChain(String(`eip155:${String(caipNetwork.id)}`)) - } else if (providerType === 'AUTH') { - const authProvider = provider as W3mFrameProvider - await authProvider.switchNetwork(caipNetwork.id) - await authProvider.connect({ - chainId: caipNetwork.id + + if (providerType === 'AUTH' || providerType === 'WALLET_CONNECT') { + await super.switchNetwork(params) + + return + } + + try { + await (provider as Provider).request({ + method: 'wallet_switchEthereumChain', + params: [{ chainId: EthersHelpersUtil.numberToHexString(caipNetwork.id) }] }) - } else { - try { - await (provider as Provider).request({ - method: 'wallet_switchEthereumChain', - params: [{ chainId: EthersHelpersUtil.numberToHexString(caipNetwork.id) }] - }) - // eslint-disable-next-line @typescript-eslint/no-explicit-any - } catch (switchError: any) { - if ( - switchError.code === WcConstantsUtil.ERROR_CODE_UNRECOGNIZED_CHAIN_ID || - switchError.code === WcConstantsUtil.ERROR_CODE_DEFAULT || - switchError?.data?.originalError?.code === - WcConstantsUtil.ERROR_CODE_UNRECOGNIZED_CHAIN_ID - ) { - await EthersHelpersUtil.addEthereumChain(provider as Provider, caipNetwork) - } else if ( - providerType === 'ANNOUNCED' || - providerType === 'EXTERNAL' || - providerType === 'INJECTED' - ) { - throw new Error('Chain is not supported') - } + // eslint-disable-next-line @typescript-eslint/no-explicit-any + } catch (switchError: any) { + if ( + switchError.code === WcConstantsUtil.ERROR_CODE_UNRECOGNIZED_CHAIN_ID || + switchError.code === WcConstantsUtil.ERROR_CODE_DEFAULT || + switchError?.data?.originalError?.code === WcConstantsUtil.ERROR_CODE_UNRECOGNIZED_CHAIN_ID + ) { + await EthersHelpersUtil.addEthereumChain(provider as Provider, caipNetwork) + } else if ( + providerType === 'ANNOUNCED' || + providerType === 'EXTERNAL' || + providerType === 'INJECTED' + ) { + throw new Error('Chain is not supported') } } } diff --git a/packages/adapters/ethers5/src/tests/client.test.ts b/packages/adapters/ethers5/src/tests/client.test.ts index fbf422df59..e2b5b2a216 100644 --- a/packages/adapters/ethers5/src/tests/client.test.ts +++ b/packages/adapters/ethers5/src/tests/client.test.ts @@ -67,7 +67,8 @@ const mockWalletConnectProvider = { const mockAuthProvider = { connect: vi.fn(), disconnect: vi.fn(), - switchNetwork: vi.fn() + switchNetwork: vi.fn(), + getUser: vi.fn() } as unknown as W3mFrameProvider const mockNetworks = [mainnet] @@ -304,8 +305,8 @@ describe('Ethers5Adapter', () => { providerType: 'AUTH' }) - expect(mockAuthProvider.switchNetwork).toHaveBeenCalledWith(1) - expect(mockAuthProvider.connect).toHaveBeenCalledWith({ chainId: 1 }) + expect(mockAuthProvider.switchNetwork).toHaveBeenCalledWith('eip155:1') + expect(mockAuthProvider.getUser).toHaveBeenCalledWith({ chainId: 'eip155:1' }) }) }) diff --git a/packages/adapters/solana/src/client.ts b/packages/adapters/solana/src/client.ts index 9b6f4a875f..c969af646e 100644 --- a/packages/adapters/solana/src/client.ts +++ b/packages/adapters/solana/src/client.ts @@ -1,23 +1,15 @@ import { WcHelpersUtil, type AppKit, type AppKitOptions } from '@reown/appkit' -import { - ConstantsUtil as CommonConstantsUtil, - type CaipNetwork, - type ChainNamespace -} from '@reown/appkit-common' +import { ConstantsUtil as CommonConstantsUtil, type CaipNetwork } from '@reown/appkit-common' import { AlertController, ChainController, CoreHelperUtil, EventsController, - StorageUtil, - type ConnectorType, - type Provider + type Provider as CoreProvider } from '@reown/appkit-core' -import { ErrorUtil, PresetsUtil } from '@reown/appkit-utils' +import { ErrorUtil } from '@reown/appkit-utils' import { SolConstantsUtil } from '@reown/appkit-utils/solana' -import type { W3mFrameProvider } from '@reown/appkit-wallet' import { AdapterBlueprint } from '@reown/appkit/adapters' -import { W3mFrameProviderSingleton } from '@reown/appkit/auth-provider' import type { BaseWalletAdapter } from '@solana/wallet-adapter-base' import type { Commitment, ConnectionConfig } from '@solana/web3.js' import { Connection, PublicKey } from '@solana/web3.js' @@ -29,24 +21,20 @@ import { type SolanaCoinbaseWallet } from './providers/CoinbaseWalletProvider.js' import { WalletConnectProvider } from './providers/WalletConnectProvider.js' -import type { WalletStandardProvider } from './providers/WalletStandardProvider.js' import { createSendTransaction } from './utils/createSendTransaction.js' import { handleMobileWalletRedirection } from './utils/handleMobileWalletRedirection.js' import { SolStoreUtil } from './utils/SolanaStoreUtil.js' import { watchStandard } from './utils/watchStandard.js' -import { withSolanaNamespace } from './utils/withSolanaNamespace.js' -import { solana } from '@reown/appkit/networks' +import type { Provider as SolanaProvider } from '@reown/appkit-utils/solana' +import { W3mFrameProvider } from '@reown/appkit-wallet' export interface AdapterOptions { connectionSettings?: Commitment | ConnectionConfig wallets?: BaseWalletAdapter[] } -export class SolanaAdapter extends AdapterBlueprint { +export class SolanaAdapter extends AdapterBlueprint { private connectionSettings: Commitment | ConnectionConfig - private w3mFrameProvider?: W3mFrameProvider - private authProvider?: AuthProvider - private authSession?: AuthProvider.Session public adapterType = 'solana' public wallets?: BaseWalletAdapter[] @@ -68,9 +56,14 @@ export class SolanaAdapter extends AdapterBlueprint { }) } - // We don't need to set auth provider since we already set it in syncConnectors - public override setAuthProvider() { - return undefined + public override setAuthProvider(w3mFrameProvider: W3mFrameProvider) { + this.addConnector( + new AuthProvider({ + w3mFrameProvider, + getActiveChain: () => ChainController.state.activeCaipNetwork, + chains: this.caipNetworks as CaipNetwork[] + }) + ) } public syncConnectors(options: AppKitOptions, appKit: AppKit) { @@ -78,81 +71,22 @@ export class SolanaAdapter extends AdapterBlueprint { AlertController.open(ErrorUtil.ALERT_ERRORS.PROJECT_ID_NOT_CONFIGURED, 'error') } - // Initialize Auth Provider if email/socials enabled - const emailEnabled = options.features?.email !== false - const socialsEnabled = - options.features?.socials !== false && - Array.isArray(options.features?.socials) && - options.features.socials.length > 0 - - if (emailEnabled || socialsEnabled) { - this.w3mFrameProvider = W3mFrameProviderSingleton.getInstance({ - projectId: options.projectId, - enableLogger: appKit.options.enableAuthLogger, - chainId: withSolanaNamespace(appKit?.getCaipNetwork(this.namespace)?.id), - onTimeout: () => { - AlertController.open(ErrorUtil.ALERT_ERRORS.SOCIALS_TIMEOUT, 'error') - } - }) - - this.authProvider = new AuthProvider({ - getProvider: () => this.w3mFrameProvider as W3mFrameProvider, - getActiveChain: () => appKit.getCaipNetwork(this.namespace), - getActiveNamespace: () => appKit.getActiveChainNamespace(), - getSession: () => this.authSession, - setSession: session => { - this.authSession = session - }, - chains: this.caipNetworks as CaipNetwork[] - }) - - this.addConnector({ - id: CommonConstantsUtil.CONNECTOR_ID.AUTH, - type: 'AUTH', - provider: this.authProvider as unknown as W3mFrameProvider, - name: 'Auth', - chain: this.namespace as ChainNamespace, - chains: [] - }) - } + // eslint-disable-next-line arrow-body-style + const getActiveChain = () => appKit.getCaipNetwork(this.namespace) // Add Coinbase Wallet if available if (CoreHelperUtil.isClient() && 'coinbaseSolana' in window) { - this.addConnector({ - id: 'coinbaseWallet', - type: 'EXTERNAL', - // @ts-expect-error window.coinbaseSolana exists - provider: new CoinbaseWalletProvider({ + this.addConnector( + new CoinbaseWalletProvider({ provider: window.coinbaseSolana as SolanaCoinbaseWallet, chains: this.caipNetworks as CaipNetwork[], - getActiveChain: () => appKit.getCaipNetwork(this.namespace) as CaipNetwork - }), - name: 'Coinbase Wallet', - chain: this.namespace as ChainNamespace, - explorerId: PresetsUtil.ConnectorExplorerIds[CommonConstantsUtil.CONNECTOR_ID.COINBASE_SDK], - chains: [] - }) + getActiveChain + }) + ) } // Watch for standard wallet adapters - watchStandard( - this.caipNetworks as CaipNetwork[], - () => appKit.getCaipNetwork(this.namespace), - (...providers: WalletStandardProvider[]) => { - providers.forEach(provider => { - this.addConnector({ - id: PresetsUtil.ConnectorExplorerIds[provider.name] || provider.name, - type: 'ANNOUNCED', - provider: provider as unknown as Provider, - imageUrl: provider.icon, - name: provider.name, - chain: CommonConstantsUtil.CHAIN.SOLANA, - explorerId: PresetsUtil.ConnectorExplorerIds[provider.name], - chains: [] - }) - }) - } - ) + watchStandard(this.caipNetworks as CaipNetwork[], getActiveChain, this.addConnector.bind(this)) } // -- Transaction methods --------------------------------------------------- @@ -186,23 +120,26 @@ export class SolanaAdapter extends AdapterBlueprint { return Promise.resolve('0x') } - public async getAccounts(): Promise { - return Promise.resolve({ - accounts: [] - }) + public async getAccounts( + params: AdapterBlueprint.GetAccountsParams + ): Promise { + const connector = this.connectors.find(c => c.id === params.id) + if (!connector) { + return { accounts: [] } + } + + return { accounts: await connector.getAccounts() } } public async signMessage( params: AdapterBlueprint.SignMessageParams ): Promise { - const walletStandardProvider = params.provider as unknown as WalletStandardProvider - if (!walletStandardProvider) { + const provider = params.provider as SolanaProvider + if (!provider) { throw new Error('connectionControllerClient:signMessage - provider is undefined') } - const signature = await walletStandardProvider.signMessage( - new TextEncoder().encode(params.message) - ) + const signature = await provider.signMessage(new TextEncoder().encode(params.message)) return { signature: bs58.encode(signature) @@ -219,7 +156,7 @@ export class SolanaAdapter extends AdapterBlueprint { } const transaction = await createSendTransaction({ - provider: params.provider as unknown as WalletStandardProvider, + provider: params.provider as SolanaProvider, connection, to: '11111111111111111111111111111111', value: 1 @@ -241,16 +178,16 @@ export class SolanaAdapter extends AdapterBlueprint { throw new Error('Connection is not set') } - const walletStandardProvider = params.provider as unknown as WalletStandardProvider + const provider = params.provider as SolanaProvider const transaction = await createSendTransaction({ - provider: walletStandardProvider, + provider, connection, to: params.to, value: params.value as number }) - const result = await walletStandardProvider.sendTransaction(transaction, connection) + const result = await provider.sendTransaction(transaction, connection) await new Promise(resolve => { const interval = setInterval(async () => { @@ -279,39 +216,33 @@ export class SolanaAdapter extends AdapterBlueprint { public async connect( params: AdapterBlueprint.ConnectParams ): Promise { - const { id, type, rpcUrl } = params - - const selectedProvider = this.connectors.find(c => c.id === id)?.provider as Provider + const connector = this.connectors.find(c => c.id === params.id) - if (!selectedProvider) { + if (!connector) { throw new Error('Provider not found') } - // eslint-disable-next-line init-declarations - let address: string + const rpcUrl = + params.rpcUrl || + this.caipNetworks?.find(n => n.id === params.chainId)?.rpcUrls.default.http[0] - if (type === 'AUTH') { - const data = await this.authProvider?.connect() - - if (!data) { - throw new Error('No address found') - } - - address = data - } else { - address = await selectedProvider.connect() + if (!rpcUrl) { + throw new Error(`RPC URL not found for chainId: ${params.chainId}`) } - this.listenProviderEvents(selectedProvider as unknown as WalletStandardProvider) + const address = await connector.connect({ + chainId: params.chainId as string + }) + this.listenProviderEvents(connector) - SolStoreUtil.setConnection(new Connection(rpcUrl as string, 'confirmed')) + SolStoreUtil.setConnection(new Connection(rpcUrl, this.connectionSettings)) return { + id: connector.id, address, chainId: params.chainId as string, - provider: selectedProvider, - type: type as ConnectorType, - id + provider: connector as CoreProvider, + type: connector.type } } @@ -336,17 +267,10 @@ export class SolanaAdapter extends AdapterBlueprint { } } - public async switchNetwork(params: AdapterBlueprint.SwitchNetworkParams): Promise { - const { caipNetwork, provider, providerType } = params + public override async switchNetwork(params: AdapterBlueprint.SwitchNetworkParams): Promise { + await super.switchNetwork(params) - if (providerType === CommonConstantsUtil.CONNECTOR_ID.AUTH) { - await (provider as unknown as W3mFrameProvider).switchNetwork(caipNetwork.id) - const user = await (provider as unknown as W3mFrameProvider).getUser({ - chainId: caipNetwork.id - }) - this.authSession = user - this.emit('switchNetwork', { chainId: caipNetwork.id, address: user.address }) - } + const { caipNetwork } = params if (caipNetwork?.rpcUrls?.default?.http?.[0]) { SolStoreUtil.setConnection( @@ -355,7 +279,7 @@ export class SolanaAdapter extends AdapterBlueprint { } } - private listenProviderEvents(provider: WalletStandardProvider) { + private listenProviderEvents(provider: SolanaProvider) { const disconnectHandler = () => { this.removeProviderListeners(provider) this.emit('disconnect') @@ -386,7 +310,7 @@ export class SolanaAdapter extends AdapterBlueprint { accountsChanged: (publicKey: PublicKey) => void } | null = null - private removeProviderListeners(provider: WalletStandardProvider) { + private removeProviderListeners(provider: SolanaProvider) { if (this.providerHandlers) { provider.removeListener('disconnect', this.providerHandlers.disconnect) provider.removeListener('accountsChanged', this.providerHandlers.accountsChanged) @@ -433,50 +357,10 @@ export class SolanaAdapter extends AdapterBlueprint { public async syncConnection( params: AdapterBlueprint.SyncConnectionParams ): Promise { - const { id, rpcUrl } = params - const connector = this.connectors.find(c => c.id === id) - const selectedProvider = connector?.provider as Provider - - if (!selectedProvider) { - throw new Error('Provider not found') - } - - // Handle different provider types - if (connector?.type === 'AUTH') { - const authProvider = selectedProvider as unknown as W3mFrameProvider - const user = await authProvider.getUser({ - chainId: Number(this.caipNetworks?.[0]?.id) - }) - - if (!user?.address) { - throw new Error('No address found') - } - - return { - address: user.address, - chainId: typeof user.chainId === 'string' ? Number(user.chainId.split(':')[1]) : 1, - provider: selectedProvider, - type: connector.type, - id - } - } - - // For standard Solana wallets - const address = await selectedProvider.connect() - const { chainId: activeChainId } = StorageUtil.getActiveNetworkProps() - const chainId = activeChainId || solana.id - - this.listenProviderEvents(selectedProvider as unknown as WalletStandardProvider) - - SolStoreUtil.setConnection(new Connection(rpcUrl, 'confirmed')) - - return { - address, - chainId, - provider: selectedProvider, - type: connector?.type as ConnectorType, - id - } + return this.connect({ + ...params, + type: '' + }) } public getWalletConnectProvider( diff --git a/packages/adapters/solana/src/providers/AuthProvider.ts b/packages/adapters/solana/src/providers/AuthProvider.ts index 024863296a..cf821b6e42 100644 --- a/packages/adapters/solana/src/providers/AuthProvider.ts +++ b/packages/adapters/solana/src/providers/AuthProvider.ts @@ -2,107 +2,85 @@ import type { AnyTransaction, Connection, GetActiveChain, - Provider + Provider as SolanaProvider } from '@reown/appkit-utils/solana' import { ProviderEventEmitter } from './shared/ProviderEventEmitter.js' import { PublicKey, Transaction, VersionedTransaction, type SendOptions } from '@solana/web3.js' -import { - W3mFrameProvider, - type W3mFrameProviderMethods as ProviderAuthMethods -} from '@reown/appkit-wallet' -import { withSolanaNamespace } from '../utils/withSolanaNamespace.js' +import { W3mFrameProvider } from '@reown/appkit-wallet' import base58 from 'bs58' import { isVersionedTransaction } from '@solana/wallet-adapter-base' -import type { CaipNetwork, ChainNamespace } from '@reown/appkit-common' +import type { CaipNetwork } from '@reown/appkit-common' import { ConstantsUtil } from '@reown/appkit-common' +import type { RequestArguments } from '@reown/appkit-core' +import { withSolanaNamespace } from '../utils/withSolanaNamespace.js' -export type AuthProviderConfig = { - getProvider: () => W3mFrameProvider - getActiveChain: GetActiveChain - getActiveNamespace: () => ChainNamespace | undefined - getSession: () => AuthProvider.Session | undefined - setSession: (session: AuthProvider.Session | undefined) => void - chains: CaipNetwork[] -} - -export class AuthProvider extends ProviderEventEmitter implements Provider, ProviderAuthMethods { +export class AuthProvider extends ProviderEventEmitter implements SolanaProvider { + public readonly id = ConstantsUtil.CONNECTOR_ID.AUTH public readonly name = ConstantsUtil.CONNECTOR_ID.AUTH public readonly type = 'AUTH' + public readonly chain = ConstantsUtil.CHAIN.SOLANA + public readonly provider: W3mFrameProvider - private readonly getProvider: AuthProviderConfig['getProvider'] - private readonly getActiveChain: AuthProviderConfig['getActiveChain'] - private readonly getActiveNamespace: AuthProviderConfig['getActiveNamespace'] private readonly requestedChains: CaipNetwork[] - private readonly getSession: AuthProviderConfig['getSession'] - private readonly setSession: AuthProviderConfig['setSession'] - - constructor({ - getProvider, - getActiveChain, - getActiveNamespace, - getSession, - setSession, - chains - }: AuthProviderConfig) { + private readonly getActiveChain: GetActiveChain + + constructor(params: AuthProvider.ConstructorParams) { super() - this.getProvider = getProvider - this.getActiveChain = getActiveChain - this.getActiveNamespace = getActiveNamespace - this.requestedChains = chains - this.getSession = getSession - this.setSession = setSession - this.bindEvents() + this.provider = params.w3mFrameProvider + this.requestedChains = params.chains + this.getActiveChain = params.getActiveChain } get publicKey(): PublicKey | undefined { - const session = this.getSession() - const namespace = this.getActiveNamespace() - if (session && namespace === 'solana') { - return new PublicKey(session.address) - } + const address = this.provider.user?.address - return undefined + return address ? new PublicKey(address) : undefined } get chains() { - const availableChainIds = this.getProvider().getAvailableChainIds() + const availableChainIds = this.provider.getAvailableChainIds() return this.requestedChains.filter(requestedChain => - availableChainIds.includes(withSolanaNamespace(requestedChain.id) as string) + availableChainIds.includes(requestedChain.caipNetworkId) ) } - public async connect() { - const session = await this.getProvider().connect({ - chainId: withSolanaNamespace(this.getActiveChain()?.id) + public async connect(params: { chainId?: string } = {}) { + const chainId = params.chainId || this.getActiveChain()?.id + await this.provider.connect({ + chainId: withSolanaNamespace(chainId) }) - this.setSession(session) - const publicKey = this.getPublicKey(true) + if (!this.publicKey) { + throw new Error('Failed to connect to the wallet') + } - this.emit('connect', publicKey) + this.emit('connect', this.publicKey) - return publicKey.toBase58() + return this.publicKey.toBase58() } public async disconnect() { - await this.getProvider().disconnect() - this.setSession(undefined) + await this.provider.disconnect() this.emit('disconnect', undefined) } public async signMessage(message: Uint8Array) { - const result = await this.getProvider().request({ + if (!this.publicKey) { + throw new Error('Wallet not connected') + } + + const result = await this.provider.request({ method: 'solana_signMessage', - params: { message: base58.encode(message), pubkey: this.getPublicKey(true).toBase58() } + params: { message: base58.encode(message), pubkey: this.publicKey.toBase58() } }) return base58.decode(result.signature) } public async signTransaction(transaction: T) { - const result = await this.getProvider().request({ + const result = await this.provider.request({ method: 'solana_signTransaction', params: { transaction: this.serializeTransaction(transaction) } }) @@ -122,7 +100,7 @@ export class AuthProvider extends ProviderEventEmitter implements Provider, Prov ) { const serializedTransaction = this.serializeTransaction(transaction) - const result = await this.getProvider().request({ + const result = await this.provider.request({ method: 'solana_signAndSendTransaction', params: { transaction: serializedTransaction, @@ -145,7 +123,7 @@ export class AuthProvider extends ProviderEventEmitter implements Provider, Prov } public async signAllTransactions(transactions: T): Promise { - const result = await this.getProvider().request({ + const result = await this.provider.request({ method: 'solana_signAllTransactions', params: { transactions: transactions.map(transaction => this.serializeTransaction(transaction)) @@ -169,82 +147,37 @@ export class AuthProvider extends ProviderEventEmitter implements Provider, Prov }) as T } - // -- W3mFrameProvider methods ------------------------------------------- // - connectEmail: ProviderAuthMethods['connectEmail'] = args => this.getProvider().connectEmail(args) - connectOtp: ProviderAuthMethods['connectOtp'] = args => this.getProvider().connectOtp(args) - updateEmail: ProviderAuthMethods['updateEmail'] = args => this.getProvider().updateEmail(args) - updateEmailPrimaryOtp: ProviderAuthMethods['updateEmailPrimaryOtp'] = args => - this.getProvider().updateEmailPrimaryOtp(args) - updateEmailSecondaryOtp: ProviderAuthMethods['updateEmailSecondaryOtp'] = args => - this.getProvider().updateEmailSecondaryOtp(args) - getEmail: ProviderAuthMethods['getEmail'] = () => this.getProvider().getEmail() - getSocialRedirectUri: ProviderAuthMethods['getSocialRedirectUri'] = args => - this.getProvider().getSocialRedirectUri(args) - connectDevice: ProviderAuthMethods['connectDevice'] = () => this.getProvider().connectDevice() - connectSocial: ProviderAuthMethods['connectSocial'] = args => - this.getProvider().connectSocial(args) - connectFarcaster: ProviderAuthMethods['connectFarcaster'] = () => - this.getProvider().connectFarcaster() - getFarcasterUri: ProviderAuthMethods['getFarcasterUri'] = () => - this.getProvider().getFarcasterUri() - syncTheme: ProviderAuthMethods['syncTheme'] = args => this.getProvider().syncTheme(args) - syncDappData: ProviderAuthMethods['syncDappData'] = args => this.getProvider().syncDappData(args) - switchNetwork: ProviderAuthMethods['switchNetwork'] = async args => { - const result = await this.getProvider().switchNetwork(args) - this.emit('chainChanged', args as string) - - return result + public async request(args: RequestArguments): Promise { + // @ts-expect-error - There is a miss match in `args` from CoreProvider and W3mFrameProvider + return this.provider.request({ method: args.method, params: args.params }) } - // -- Private ------------------------------------------- // - private getPublicKey( - required?: Required - ): Required extends true ? PublicKey : PublicKey | undefined { - const session = this.getSession() - if (!session) { - if (required) { - throw new Error('Account is required') - } - - return undefined as Required extends true ? PublicKey : PublicKey | undefined + public async getAccounts() { + if (!this.publicKey) { + return Promise.resolve([]) } - return new PublicKey(session.address) + return Promise.resolve([ + { + namespace: this.chain, + address: this.publicKey.toBase58(), + type: 'eoa' + } as const + ]) } + // -- Private ------------------------------------------- // private serializeTransaction(transaction: AnyTransaction) { return base58.encode(transaction.serialize({ verifySignatures: false })) } - - private bindEvents() { - this.getProvider().onRpcRequest(request => { - this.emit('auth_rpcRequest', request) - }) - - this.getProvider().onRpcSuccess(response => { - this.emit('auth_rpcSuccess', response) - }) - - this.getProvider().onRpcError(error => { - this.emit('auth_rpcError', error) - }) - - this.getProvider().onConnect(response => { - const isSolanaNamespace = - typeof response.chainId === 'string' ? response.chainId?.startsWith('solana') : false - - if (isSolanaNamespace) { - this.setSession(response) - this.emit('connect', this.getPublicKey(true)) - } - }) - - this.getProvider().onNotConnected(() => { - this.emit('disconnect', undefined) - }) - } } export namespace AuthProvider { + export type ConstructorParams = { + w3mFrameProvider: W3mFrameProvider + getActiveChain: GetActiveChain + chains: CaipNetwork[] + } + export type Session = Awaited> } diff --git a/packages/adapters/solana/src/providers/CoinbaseWalletProvider.ts b/packages/adapters/solana/src/providers/CoinbaseWalletProvider.ts index ca537c8d1c..ef08fc9877 100644 --- a/packages/adapters/solana/src/providers/CoinbaseWalletProvider.ts +++ b/packages/adapters/solana/src/providers/CoinbaseWalletProvider.ts @@ -1,8 +1,11 @@ -import { type AnyTransaction, type Provider } from '@reown/appkit-utils/solana' +import { type AnyTransaction, type Provider as SolanaProvider } from '@reown/appkit-utils/solana' import { ProviderEventEmitter } from './shared/ProviderEventEmitter.js' import type { Connection, PublicKey, SendOptions } from '@solana/web3.js' -import type { CaipNetwork } from '@reown/appkit-common' +import { ConstantsUtil, type CaipNetwork } from '@reown/appkit-common' import { solana } from '@reown/appkit/networks' +import { PresetsUtil } from '@reown/appkit-utils' +import type { RequestArguments } from '@reown/appkit-core' +import type { Provider as CoreProvider } from '@reown/appkit-core' export type SolanaCoinbaseWallet = { publicKey?: PublicKey @@ -24,18 +27,24 @@ export type CoinbaseWalletProviderConfig = { getActiveChain: () => CaipNetwork | undefined } -export class CoinbaseWalletProvider extends ProviderEventEmitter implements Provider { +export class CoinbaseWalletProvider extends ProviderEventEmitter implements SolanaProvider { public readonly name = 'Coinbase Wallet' + public readonly id = + PresetsUtil.ConnectorExplorerIds[ConstantsUtil.CONNECTOR_ID.COINBASE_SDK] || this.name + public readonly explorerId = + PresetsUtil.ConnectorExplorerIds[ConstantsUtil.CONNECTOR_ID.COINBASE_SDK] public readonly type = 'ANNOUNCED' - public readonly icon = + public readonly imageUrl = 'data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMTAyNCIgaGVpZ2h0PSIxMDI0IiB2aWV3Qm94PSIwIDAgMTAyNCAxMDI0IiBmaWxsPSJub25lIiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciPgo8Y2lyY2xlIGN4PSI1MTIiIGN5PSI1MTIiIHI9IjUxMiIgZmlsbD0iIzAwNTJGRiIvPgo8cGF0aCBmaWxsLXJ1bGU9ImV2ZW5vZGQiIGNsaXAtcnVsZT0iZXZlbm9kZCIgZD0iTTE1MiA1MTJDMTUyIDcxMC44MjMgMzEzLjE3NyA4NzIgNTEyIDg3MkM3MTAuODIzIDg3MiA4NzIgNzEwLjgyMyA4NzIgNTEyQzg3MiAzMTMuMTc3IDcxMC44MjMgMTUyIDUxMiAxNTJDMzEzLjE3NyAxNTIgMTUyIDMxMy4xNzcgMTUyIDUxMlpNNDIwIDM5NkM0MDYuNzQ1IDM5NiAzOTYgNDA2Ljc0NSAzOTYgNDIwVjYwNEMzOTYgNjE3LjI1NSA0MDYuNzQ1IDYyOCA0MjAgNjI4SDYwNEM2MTcuMjU1IDYyOCA2MjggNjE3LjI1NSA2MjggNjA0VjQyMEM2MjggNDA2Ljc0NSA2MTcuMjU1IDM5NiA2MDQgMzk2SDQyMFoiIGZpbGw9IndoaXRlIi8+Cjwvc3ZnPgo=' + public readonly chain = ConstantsUtil.CHAIN.SOLANA + public readonly provider = this as CoreProvider - private provider: SolanaCoinbaseWallet + private coinbase: SolanaCoinbaseWallet private requestedChains: CaipNetwork[] constructor(params: CoinbaseWalletProviderConfig) { super() - this.provider = params.provider + this.coinbase = params.provider this.requestedChains = params.chains } @@ -45,44 +54,44 @@ export class CoinbaseWalletProvider extends ProviderEventEmitter implements Prov } public get publicKey() { - return this.provider.publicKey + return this.coinbase.publicKey } public async connect() { try { - await this.provider.connect() + await this.coinbase.connect() const account = this.getAccount(true) - this.provider.emit('connect', this.provider.publicKey) + this.coinbase.emit('connect', this.coinbase.publicKey) this.emit('connect', account) return account.toBase58() } catch (error) { - this.provider.emit('error', error) + this.coinbase.emit('error', error) throw error } } public async disconnect() { - await this.provider.disconnect() - this.provider.emit('disconnect', undefined) + await this.coinbase.disconnect() + this.coinbase.emit('disconnect', undefined) this.emit('disconnect', undefined) } public async signMessage(message: Uint8Array) { - const result = await this.provider.signMessage(message) + const result = await this.coinbase.signMessage(message) return result.signature } public async signTransaction(transaction: T) { - return this.provider.signTransaction(transaction) + return this.coinbase.signTransaction(transaction) } public async signAndSendTransaction( transaction: T, sendOptions?: SendOptions ) { - const result = await this.provider.signAndSendTransaction(transaction, sendOptions) + const result = await this.coinbase.signAndSendTransaction(transaction, sendOptions) return result.signature } @@ -99,13 +108,32 @@ export class CoinbaseWalletProvider extends ProviderEventEmitter implements Prov } public async signAllTransactions(transactions: T): Promise { - return (await this.provider.signAllTransactions(transactions)) as T + return (await this.coinbase.signAllTransactions(transactions)) as T + } + + public async request(_args: RequestArguments): Promise { + return Promise.reject(new Error('The "request" method is not supported on Coinbase Wallet')) + } + + public async getAccounts() { + const account = this.getAccount() + if (!account) { + return Promise.resolve([]) + } + + return Promise.resolve([ + { + namespace: this.chain, + address: account.toBase58(), + type: 'eoa' + } as const + ]) } private getAccount( required?: Required ): Required extends true ? PublicKey : PublicKey | undefined { - const account = this.provider.publicKey + const account = this.coinbase.publicKey if (required && !account) { throw new Error('Not connected') } diff --git a/packages/adapters/solana/src/providers/WalletConnectProvider.ts b/packages/adapters/solana/src/providers/WalletConnectProvider.ts index 8a9950dc4d..3f86981de1 100644 --- a/packages/adapters/solana/src/providers/WalletConnectProvider.ts +++ b/packages/adapters/solana/src/providers/WalletConnectProvider.ts @@ -12,9 +12,9 @@ import { type SendOptions } from '@solana/web3.js' import { isVersionedTransaction } from '@solana/wallet-adapter-base' -import type { CaipNetwork, ChainId } from '@reown/appkit-common' +import { ConstantsUtil, type CaipNetwork, ParseUtil, type CaipAddress } from '@reown/appkit-common' import { withSolanaNamespace } from '../utils/withSolanaNamespace.js' -import { WcHelpersUtil } from '@reown/appkit' +import { WcHelpersUtil, type RequestArguments } from '@reown/appkit' import { WalletConnectMethodNotSupportedError } from './shared/Errors.js' export type WalletConnectProviderConfig = { @@ -24,12 +24,14 @@ export type WalletConnectProviderConfig = { } export class WalletConnectProvider extends ProviderEventEmitter implements Provider { - public readonly name = 'WalletConnect' + public readonly id = ConstantsUtil.CONNECTOR_ID.WALLET_CONNECT + public readonly name = ConstantsUtil.CONNECTOR_ID.WALLET_CONNECT public readonly type = 'WALLET_CONNECT' public readonly icon = 'https://imagedelivery.net/_aTEfDRm7z3tKgu9JhfeKA/05338e12-4f75-4982-4e8a-83c67b826b00/md' public session?: SessionTypes.Struct public provider: UniversalProvider + public readonly chain = ConstantsUtil.CHAIN.SOLANA private readonly requestedChains: CaipNetwork[] private readonly getActiveChain: WalletConnectProviderConfig['getActiveChain'] @@ -78,31 +80,12 @@ export class WalletConnectProvider extends ProviderEventEmitter implements Provi } public async connect() { - const rpcMap = this.requestedChains.reduce>((acc, chain) => { - acc[withSolanaNamespace(chain.id as string)] = chain.rpcUrls.default.http[0] || '' - - return acc - }, {}) - if (this.provider.session?.namespaces['solana']) { this.session = this.provider.session } else { this.provider.on('display_uri', this.onUri) this.session = await this.provider.connect({ - optionalNamespaces: { - solana: { - // Double check these with Felipe - chains: this.getRequestedChainsWithDeprecated() as string[], - methods: [ - 'solana_signMessage', - 'solana_signTransaction', - 'solana_signAndSendTransaction', - 'solana_signAllTransactions' - ], - events: [], - rpcMap - } - } + optionalNamespaces: WcHelpersUtil.createNamespaces(this.requestedChains) }) this.provider.removeListener('display_uri', this.onUri) } @@ -122,7 +105,7 @@ export class WalletConnectProvider extends ProviderEventEmitter implements Provi public async signMessage(message: Uint8Array) { this.checkIfMethodIsSupported('solana_signMessage') - const signedMessage = await this.request('solana_signMessage', { + const signedMessage = await this.internalRequest('solana_signMessage', { message: base58.encode(message), pubkey: this.getAccount(true).address }) @@ -135,7 +118,7 @@ export class WalletConnectProvider extends ProviderEventEmitter implements Provi const serializedTransaction = this.serializeTransaction(transaction) - const result = await this.request('solana_signTransaction', { + const result = await this.internalRequest('solana_signTransaction', { transaction: serializedTransaction, pubkey: this.getAccount(true).address, ...this.getRawRPCParams(transaction) @@ -168,7 +151,7 @@ export class WalletConnectProvider extends ProviderEventEmitter implements Provi const serializedTransaction = this.serializeTransaction(transaction) - const result = await this.request('solana_signAndSendTransaction', { + const result = await this.internalRequest('solana_signAndSendTransaction', { transaction: serializedTransaction, pubkey: this.getAccount(true).address, sendOptions @@ -196,7 +179,7 @@ export class WalletConnectProvider extends ProviderEventEmitter implements Provi try { this.checkIfMethodIsSupported('solana_signAllTransactions') - const result = await this.request('solana_signAllTransactions', { + const result = await this.internalRequest('solana_signAllTransactions', { transactions: transactions.map(transaction => this.serializeTransaction(transaction)) }) @@ -233,8 +216,25 @@ export class WalletConnectProvider extends ProviderEventEmitter implements Provi } } + public request(args: RequestArguments): Promise { + // @ts-expect-error - There is a miss match in `args` from CoreProvider and internalRequest + return this.internalRequest(args.method, args.params) + } + + public async getAccounts() { + const accounts = (this.session?.namespaces['solana']?.accounts || []) as CaipAddress[] + + return Promise.resolve( + accounts.map(account => ({ + namespace: this.chain, + address: ParseUtil.parseCaipAddress(account).address, + type: 'eoa' as const + })) + ) + } + // -- Private ------------------------------------------ // - private request( + private internalRequest( method: Method, params: WalletConnectProvider.RequestMethods[Method]['params'] ) { @@ -313,23 +313,6 @@ export class WalletConnectProvider extends ProviderEventEmitter implements Provi } } - /** - * This method is a workaround for wallets that only accept Solana deprecated networks - */ - private getRequestedChainsWithDeprecated() { - const chains = this.requestedChains.map(chain => withSolanaNamespace(chain.id)) - - if (chains.includes(SolConstantsUtil.CHAIN_IDS.Mainnet)) { - chains.push(SolConstantsUtil.CHAIN_IDS.Deprecated_Mainnet) - } - - if (chains.includes(SolConstantsUtil.CHAIN_IDS.Devnet)) { - chains.push(SolConstantsUtil.CHAIN_IDS.Deprecated_Devnet) - } - - return chains - } - /* * This is a deprecated method that is used to support older versions of the * WalletConnect RPC API. It should be removed in the future diff --git a/packages/adapters/solana/src/providers/WalletStandardProvider.ts b/packages/adapters/solana/src/providers/WalletStandardProvider.ts index 1995a2f421..2e0aa2b6db 100644 --- a/packages/adapters/solana/src/providers/WalletStandardProvider.ts +++ b/packages/adapters/solana/src/providers/WalletStandardProvider.ts @@ -26,12 +26,19 @@ import { StandardEvents, type StandardEventsFeature } from '@wallet-standard/features' -import type { AnyTransaction, GetActiveChain, Provider } from '@reown/appkit-utils/solana' +import type { + AnyTransaction, + GetActiveChain, + Provider as SolanaProvider +} from '@reown/appkit-utils/solana' import base58 from 'bs58' import { WalletStandardFeatureNotSupportedError } from './shared/Errors.js' import { ProviderEventEmitter } from './shared/ProviderEventEmitter.js' import { solanaChains } from '../utils/chains.js' -import type { CaipNetwork } from '@reown/appkit-common' +import { ConstantsUtil, type CaipNetwork } from '@reown/appkit-common' +import { PresetsUtil } from '@reown/appkit-utils' +import type { RequestArguments } from '@reown/appkit-core' +import type { Provider as CoreProvider } from '@reown/appkit-core' export interface WalletStandardProviderConfig { wallet: Wallet @@ -47,9 +54,11 @@ type AvailableFeatures = StandardConnectFeature & SolanaSignInFeature & StandardEventsFeature -export class WalletStandardProvider extends ProviderEventEmitter implements Provider { +export class WalletStandardProvider extends ProviderEventEmitter implements SolanaProvider { readonly wallet: Wallet readonly getActiveChain: WalletStandardProviderConfig['getActiveChain'] + readonly chain = ConstantsUtil.CHAIN.SOLANA + public readonly provider = this as CoreProvider private readonly requestedChains: WalletStandardProviderConfig['requestedChains'] @@ -64,6 +73,12 @@ export class WalletStandardProvider extends ProviderEventEmitter implements Prov } // -- Public ------------------------------------------- // + public get id() { + const name = this.name + + return PresetsUtil.ConnectorExplorerIds[name] || name + } + public get name() { if (this.wallet.name === 'Trust') { // The wallets from our list of wallets have not matching with the extension name @@ -77,6 +92,10 @@ export class WalletStandardProvider extends ProviderEventEmitter implements Prov return 'ANNOUNCED' as const } + public get explorerId() { + return PresetsUtil.ConnectorExplorerIds[this.name] + } + public get publicKey() { const account = this.getAccount(false) @@ -87,7 +106,7 @@ export class WalletStandardProvider extends ProviderEventEmitter implements Prov return undefined } - public get icon() { + public get imageUrl() { return this.wallet.icon } @@ -224,6 +243,20 @@ export class WalletStandardProvider extends ProviderEventEmitter implements Prov }) as T } + public async request(_args: RequestArguments): Promise { + return Promise.reject(new WalletStandardFeatureNotSupportedError('request')) + } + + public async getAccounts() { + return Promise.resolve( + this.wallet.accounts.map(account => ({ + namespace: this.chain, + address: account.address, + type: 'eoa' as const + })) + ) + } + // -- Private ------------------------------------------- // private serializeTransaction(transaction: AnyTransaction) { return transaction.serialize({ verifySignatures: false }) diff --git a/packages/adapters/solana/src/tests/AuthProvider.test.ts b/packages/adapters/solana/src/tests/AuthProvider.test.ts index 659a8d8729..9004eb55f1 100644 --- a/packages/adapters/solana/src/tests/AuthProvider.test.ts +++ b/packages/adapters/solana/src/tests/AuthProvider.test.ts @@ -8,13 +8,7 @@ describe('AuthProvider specific tests', () => { let provider = mockW3mFrameProvider() let getActiveChain = vi.fn(() => TestConstants.chains[0]) let authProvider = new AuthProvider({ - getProvider: () => mockW3mFrameProvider(), - getActiveNamespace: () => 'solana', - getSession: () => ({ - chainId: 'solana', - address: '2VqKhjZ766ZN3uBtBpb7Ls3cN4HrocP1rzxzekhVEgoP' - }), - setSession: vi.fn(), + w3mFrameProvider: mockW3mFrameProvider(), getActiveChain, chains: TestConstants.chains }) @@ -23,13 +17,7 @@ describe('AuthProvider specific tests', () => { provider = mockW3mFrameProvider() getActiveChain = vi.fn(() => TestConstants.chains[0]) authProvider = new AuthProvider({ - getProvider: () => mockW3mFrameProvider(), - getActiveNamespace: () => 'solana', - getSession: () => ({ - chainId: 'solana', - address: '2VqKhjZ766ZN3uBtBpb7Ls3cN4HrocP1rzxzekhVEgoP' - }), - setSession: vi.fn(), + w3mFrameProvider: mockW3mFrameProvider(), getActiveChain, chains: TestConstants.chains }) @@ -42,11 +30,8 @@ describe('AuthProvider specific tests', () => { }) it('should call disconnect', async () => { - const setSessionSpy = vi.spyOn(authProvider, 'setSession' as any) await authProvider.disconnect() - expect(setSessionSpy).toHaveBeenCalledOnce() - expect(setSessionSpy).toHaveBeenCalledWith(undefined) expect(provider.disconnect).toHaveBeenCalled() }) @@ -119,18 +104,6 @@ describe('AuthProvider specific tests', () => { }) }) - it('should call switch network with correct params and emit event', async () => { - await authProvider.connect() - const newChain = TestConstants.chains[1]! - const listener = vi.fn() - - authProvider.on('chainChanged', listener) - await authProvider.switchNetwork(newChain.id) - - expect(provider.switchNetwork).toHaveBeenCalledWith(newChain.id) - expect(listener).toHaveBeenCalledWith(newChain.id) - }) - it('should call signAllTransactions with correct params', async () => { await authProvider.connect() const transactions = [mockLegacyTransaction(), mockVersionedTransaction()] diff --git a/packages/adapters/solana/src/tests/GenericProvider.test.ts b/packages/adapters/solana/src/tests/GenericProvider.test.ts index 5afc15dc9b..f45435f407 100644 --- a/packages/adapters/solana/src/tests/GenericProvider.test.ts +++ b/packages/adapters/solana/src/tests/GenericProvider.test.ts @@ -35,15 +35,9 @@ const providers: { name: string; provider: Provider }[] = [ { name: 'AuthProvider', provider: new AuthProvider({ - getProvider: () => mockW3mFrameProvider(), getActiveChain, - getActiveNamespace: () => 'solana', - getSession: () => ({ - chainId: 'solana', - address: '2VqKhjZ766ZN3uBtBpb7Ls3cN4HrocP1rzxzekhVEgoP' - }), - setSession: vi.fn(), - chains: TestConstants.chains + chains: TestConstants.chains, + w3mFrameProvider: mockW3mFrameProvider() }) }, { @@ -78,12 +72,6 @@ describe.each(providers)('Generic provider tests for $name', ({ provider }) => { expect(events.connect).toHaveBeenCalledWith(TestConstants.accounts[0].publicKey) }) - it('should disconnect and emit event', async () => { - await provider.disconnect() - - expect(events.disconnect).toHaveBeenCalledWith(undefined) - }) - it('should signMessage', async () => { const result = await provider.signMessage(new TextEncoder().encode('test')) @@ -133,4 +121,10 @@ describe.each(providers)('Generic provider tests for $name', ({ provider }) => { } }) }) + + it('should disconnect and emit event', async () => { + await provider.disconnect() + + expect(events.disconnect).toHaveBeenCalledWith(undefined) + }) }) diff --git a/packages/adapters/solana/src/tests/client.test.ts b/packages/adapters/solana/src/tests/client.test.ts index b666eb5d97..14b42ef663 100644 --- a/packages/adapters/solana/src/tests/client.test.ts +++ b/packages/adapters/solana/src/tests/client.test.ts @@ -1,22 +1,25 @@ -import { describe, it, expect, beforeEach, vi, type Mock } from 'vitest' +import { describe, it, expect, beforeEach, vi } from 'vitest' import { CaipNetworksUtil, PresetsUtil } from '@reown/appkit-utils' import { solana } from '@reown/appkit/networks' -import type { ConnectorType, Provider } from '@reown/appkit-core' -import type { W3mFrameProvider } from '@reown/appkit-wallet' +import type { ConnectorType, Provider as CoreProvider } from '@reown/appkit-core' import UniversalProvider from '@walletconnect/universal-provider' -import type { ChainNamespace } from '@reown/appkit-common' +import { ConstantsUtil, type ChainNamespace } from '@reown/appkit-common' import { SolanaAdapter } from '../client' import { SolStoreUtil } from '../utils/SolanaStoreUtil' import type { WalletStandardProvider } from '../providers/WalletStandardProvider' import { watchStandard } from '../utils/watchStandard' import mockAppKit from './mocks/AppKit' import { mockCoinbaseWallet } from './mocks/CoinbaseWallet' +import { type Provider } from '@reown/appkit-utils/solana' +import { AuthProvider } from '../providers/AuthProvider' +import { mockAuthConnector } from './mocks/AuthConnector' // Mock external dependencies vi.mock('@solana/web3.js', () => ({ - Connection: vi.fn(() => ({ + Connection: vi.fn(endpoint => ({ getBalance: vi.fn().mockResolvedValue(1500000000), - getSignatureStatus: vi.fn().mockResolvedValue({ value: true }) + getSignatureStatus: vi.fn().mockResolvedValue({ value: true }), + rpcEndpoint: endpoint })), PublicKey: vi.fn(key => ({ toBase58: () => key })) })) @@ -44,6 +47,8 @@ const mockProvider = { } as unknown as WalletStandardProvider const mockWalletConnectProvider = { + id: 'walletconnect', + name: 'WalletConnect', connect: vi.fn(), disconnect: vi.fn(), on: vi.fn(), @@ -52,17 +57,6 @@ const mockWalletConnectProvider = { setDefaultChain: vi.fn() } as unknown as UniversalProvider -const mockAuthProvider = { - id: 'auth', - connect: vi.fn().mockResolvedValue('mock-auth-address'), - disconnect: vi.fn(), - switchNetwork: vi.fn(), - getUser: vi.fn().mockResolvedValue({ - address: 'mock-auth-address', - chainId: '5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp' - }) -} as unknown as W3mFrameProvider - const mockNetworks = [solana] const mockCaipNetworks = CaipNetworksUtil.extendCaipNetworks(mockNetworks, { projectId: 'test-project-id', @@ -75,8 +69,22 @@ const mockWalletConnectConnector = { provider: mockWalletConnectProvider, type: 'WALLET_CONNECT' as ConnectorType, chains: mockNetworks, - chain: 'solana' as ChainNamespace -} + chain: 'solana' as ChainNamespace, + signMessage: vi.fn(), + signAllTransactions: vi.fn(), + signTransaction: vi.fn(), + sendTransaction: vi.fn(), + signAndSendTransaction: vi.fn(), + getAccounts: vi.fn(), + connect: vi.fn(), + disconnect: vi.fn(), + on: vi.fn(), + removeListener: vi.fn(), + session: true, + setDefaultChain: vi.fn(), + request: vi.fn(), + emit: vi.fn() +} as Provider describe('SolanaAdapter', () => { let adapter: SolanaAdapter @@ -111,11 +119,11 @@ describe('SolanaAdapter', () => { expect(addConnectorSpy).toHaveBeenCalledOnce() expect(addConnectorSpy).toHaveBeenCalledWith( expect.objectContaining({ - id: 'coinbaseWallet', - type: 'EXTERNAL', + id: PresetsUtil.ConnectorExplorerIds[ConstantsUtil.CONNECTOR_ID.COINBASE_SDK], + type: 'ANNOUNCED', name: 'Coinbase Wallet', chain: 'solana', - chains: [] + requestedChains: [solana] }) ) }) @@ -129,36 +137,66 @@ describe('SolanaAdapter', () => { }) describe('SolanaAdapter - connect', () => { - it('should connect with external provider', async () => { + beforeEach(() => { const connectors = [ { id: 'test', provider: mockProvider, - type: 'EXTERNAL' + type: 'EXTERNAL', + connect: vi.fn().mockResolvedValue('mock-address'), + on: vi.fn() } ] Object.defineProperty(adapter, 'connectors', { value: connectors }) + }) + it('should connect with external provider', async () => { const result = await adapter.connect({ id: 'test', provider: mockProvider, type: 'EXTERNAL', chainId: '5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp', - rpcUrl: 'https://api.mainnet-beta.solana.com' + rpcUrl: 'mock_rpc_url' }) expect(result.address).toBe('mock-address') expect(result.chainId).toBe('5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp') - expect(SolStoreUtil.setConnection).toHaveBeenCalled() + expect(SolStoreUtil.setConnection).toHaveBeenCalledWith( + expect.objectContaining({ rpcEndpoint: 'mock_rpc_url' }) + ) + }) + + it('should fallback for network rpc url if param is not provider', async () => { + await adapter.connect({ + id: 'test', + provider: mockProvider, + type: 'EXTERNAL', + chainId: solana.id + }) + + expect(SolStoreUtil.setConnection).toHaveBeenCalledWith( + expect.objectContaining({ rpcEndpoint: solana.rpcUrls.default.http[0] }) + ) + }) + + it('should throw if not possible to get a rpc url', async () => { + await expect( + adapter.connect({ + id: 'test', + provider: mockProvider, + type: 'EXTERNAL', + chainId: 'mock_chain_id' + }) + ).rejects.toThrowError('RPC URL not found for chainId: mock_chain_id') }) }) describe('SolanaAdapter - disconnect', () => { it('should disconnect provider', async () => { await adapter.disconnect({ - provider: mockProvider as unknown as Provider, + provider: mockProvider as unknown as CoreProvider, providerType: 'EXTERNAL' }) @@ -186,7 +224,7 @@ describe('SolanaAdapter', () => { const result = await adapter.signMessage({ message: 'Hello', address: 'mock-address', - provider: mockProvider as unknown as Provider + provider: mockProvider as unknown as CoreProvider }) expect(result.signature).toBeDefined() @@ -196,13 +234,20 @@ describe('SolanaAdapter', () => { describe('SolanaAdapter - switchNetwork', () => { it('should switch network with auth provider', async () => { + const switchNetworkSpy = vi.fn() + const provider = Object.assign(Object.create(AuthProvider.prototype), { + type: 'AUTH', + switchNetwork: switchNetworkSpy, + getUser: mockAuthConnector.connect + }) + await adapter.switchNetwork({ caipNetwork: mockCaipNetworks[0], - provider: mockAuthProvider, - providerType: 'ID_AUTH' + provider: provider, + providerType: 'AUTH' }) - expect(mockAuthProvider.switchNetwork).toHaveBeenCalled() + expect(switchNetworkSpy).toHaveBeenCalled() expect(SolStoreUtil.setConnection).toHaveBeenCalled() }) }) @@ -235,16 +280,20 @@ describe('SolanaAdapter', () => { it.each(['Phantom', 'Trust Wallet', 'Solflare', 'unknown wallet'])( 'should parse watchStandard ids from cloud', walletName => { - adapter.syncConnectors({ features: { email: false, socials: false } } as any, {} as any) - const watchStandardSpy = watchStandard as Mock + const watchStandardSpy = vi.mocked(watchStandard) const addProviderSpy = vi.spyOn(adapter as any, 'addConnector') + adapter.syncConnectors( + { features: { email: false, socials: false }, projectId: '1234' } as any, + {} as any + ) const callback = watchStandardSpy.mock.calls[0]![2] callback({ name: walletName } as any) + expect(watchStandard).toHaveBeenCalled() expect(addProviderSpy).toHaveBeenCalledWith( expect.objectContaining({ - id: PresetsUtil.ConnectorExplorerIds[walletName] || walletName + name: walletName }) ) } diff --git a/packages/adapters/solana/src/tests/mocks/W3mFrameProvider.ts b/packages/adapters/solana/src/tests/mocks/W3mFrameProvider.ts index d6bf7b74b5..c4a9603983 100644 --- a/packages/adapters/solana/src/tests/mocks/W3mFrameProvider.ts +++ b/packages/adapters/solana/src/tests/mocks/W3mFrameProvider.ts @@ -39,6 +39,8 @@ export function mockW3mFrameProvider() { } }) w3mFrame.switchNetwork = vi.fn((chainId: string | number) => Promise.resolve({ chainId })) + w3mFrame.getUser = vi.fn(() => Promise.resolve(mockSession())) + w3mFrame.user = mockSession() return w3mFrame } diff --git a/packages/adapters/wagmi/src/client.ts b/packages/adapters/wagmi/src/client.ts index 4a9bc69cf4..74d5575544 100644 --- a/packages/adapters/wagmi/src/client.ts +++ b/packages/adapters/wagmi/src/client.ts @@ -594,7 +594,7 @@ export class WagmiAdapter extends AdapterBlueprint { ) } - public async switchNetwork(params: AdapterBlueprint.SwitchNetworkParams) { + public override async switchNetwork(params: AdapterBlueprint.SwitchNetworkParams) { await switchChain(this.wagmiConfig, { chainId: params.caipNetwork.id as number }) } diff --git a/packages/adapters/wagmi/src/connectors/AuthConnector.ts b/packages/adapters/wagmi/src/connectors/AuthConnector.ts index a290bb4aa9..a73a521a80 100644 --- a/packages/adapters/wagmi/src/connectors/AuthConnector.ts +++ b/packages/adapters/wagmi/src/connectors/AuthConnector.ts @@ -42,7 +42,11 @@ export function authConnector(parameters: AuthParameters) { let chainId = options.chainId if (options.isReconnecting) { - chainId = provider.getLastUsedChainId() + const lastUsedChainId = NetworkUtil.parseEvmChainId(provider.getLastUsedChainId() || '') + const defaultChainId = parameters.chains?.[0].id + + chainId = lastUsedChainId || defaultChainId + if (!chainId) { throw new Error('ChainId not found in provider') } diff --git a/packages/appkit-utils/src/solana/SolanaTypesUtil.ts b/packages/appkit-utils/src/solana/SolanaTypesUtil.ts index 2bfa3d4ce1..422175bda0 100644 --- a/packages/appkit-utils/src/solana/SolanaTypesUtil.ts +++ b/packages/appkit-utils/src/solana/SolanaTypesUtil.ts @@ -6,11 +6,13 @@ import type { VersionedTransaction, SendOptions } from '@solana/web3.js' +import UniversalProvider from '@walletconnect/universal-provider' import type { SendTransactionOptions } from '@solana/wallet-adapter-base' -import type { CaipNetwork } from '@reown/appkit-common' +import type { CaipNetwork, ChainNamespace } from '@reown/appkit-common' import type { ConnectorType } from '@reown/appkit-core' -import type { W3mFrameTypes } from '@reown/appkit-wallet' +import type { W3mFrameProvider, W3mFrameTypes } from '@reown/appkit-wallet' +import type { Provider as CoreProvider } from '@reown/appkit-core' export type Connection = SolanaConnection @@ -33,16 +35,23 @@ export interface RequestArguments { readonly params?: readonly unknown[] | object } -export interface Provider extends ProviderEventEmitterMethods { +export interface Provider + extends ProviderEventEmitterMethods, + Omit { // Metadata + id: string name: string - publicKey?: PublicKey - icon?: string chains: CaipNetwork[] type: ConnectorType + chain: ChainNamespace + publicKey?: PublicKey + provider: CoreProvider | W3mFrameProvider | UniversalProvider // Methods - connect: () => Promise + connect: (params?: { + chainId?: string + onUri?: ((uri: string) => void) | undefined + }) => Promise disconnect: () => Promise signMessage: (message: Uint8Array) => Promise signTransaction: (transaction: T) => Promise @@ -56,6 +65,13 @@ export interface Provider extends ProviderEventEmitterMethods { options?: SendTransactionOptions ) => Promise signAllTransactions: (transactions: T) => Promise + getAccounts: () => Promise< + { + namespace: 'solana' + address: string + type: 'eoa' + }[] + > } export interface ProviderEventEmitterMethods { diff --git a/packages/appkit/src/adapters/ChainAdapterBlueprint.ts b/packages/appkit/src/adapters/ChainAdapterBlueprint.ts index 4e3f48befc..987601c7f7 100644 --- a/packages/appkit/src/adapters/ChainAdapterBlueprint.ts +++ b/packages/appkit/src/adapters/ChainAdapterBlueprint.ts @@ -1,5 +1,4 @@ import { - getW3mThemeVariables, ConstantsUtil as CommonConstantsUtil, type CaipAddress, type CaipNetwork, @@ -8,23 +7,17 @@ import { import type { ChainAdapterConnector } from './ChainAdapterConnector.js' import { AccountController, - OptionsController, - ThemeController, type AccountType, type AccountControllerState, type Connector as AppKitConnector, - type AuthConnector, - type Metadata, type Tokens, type WriteContractArgs } from '@reown/appkit-core' -import type UniversalProvider from '@walletconnect/universal-provider' -import type { W3mFrameProvider } from '@reown/appkit-wallet' +import UniversalProvider from '@walletconnect/universal-provider' +import { W3mFrameProvider } from '@reown/appkit-wallet' import { PresetsUtil } from '@reown/appkit-utils' import type { AppKitOptions } from '../utils/index.js' import type { AppKit } from '../client.js' -import { snapshot } from 'valtio/vanilla' - type EventName = | 'disconnect' | 'accountChanged' @@ -130,28 +123,6 @@ export abstract class AdapterBlueprint< * @param {...Connector} connectors - The connectors to add */ protected addConnector(...connectors: Connector[]) { - if (connectors.some(connector => connector.id === CommonConstantsUtil.CONNECTOR_ID.AUTH)) { - const authConnector = connectors.find( - connector => connector.id === CommonConstantsUtil.CONNECTOR_ID.AUTH - ) as AuthConnector - - const optionsState = snapshot(OptionsController.state) - const themeMode = ThemeController.getSnapshot().themeMode - const themeVariables = ThemeController.getSnapshot().themeVariables - - authConnector?.provider?.syncDappData?.({ - metadata: optionsState.metadata as Metadata, - sdkVersion: optionsState.sdkVersion, - projectId: optionsState.projectId, - sdkType: optionsState.sdkType - }) - authConnector.provider.syncTheme({ - themeMode, - themeVariables, - w3mThemeVariables: getW3mThemeVariables(themeVariables, themeMode) - }) - } - const connectorsAdded = new Set() this.availableConnectors = [...connectors, ...this.availableConnectors].filter(connector => { if (connectorsAdded.has(connector.id)) { @@ -251,7 +222,31 @@ export abstract class AdapterBlueprint< * Switches the network. * @param {AdapterBlueprint.SwitchNetworkParams} params - Network switching parameters */ - public abstract switchNetwork(params: AdapterBlueprint.SwitchNetworkParams): Promise + public async switchNetwork(params: AdapterBlueprint.SwitchNetworkParams): Promise { + const { caipNetwork, providerType } = params + + if (!params.provider) { + return + } + + const provider = 'provider' in params.provider ? params.provider.provider : params.provider + + if (providerType === 'WALLET_CONNECT') { + ;(provider as UniversalProvider).setDefaultChain(caipNetwork.caipNetworkId) + + return + } + + if (provider && providerType === 'AUTH') { + const authProvider = provider as W3mFrameProvider + await authProvider.switchNetwork(caipNetwork.caipNetworkId) + const user = await authProvider.getUser({ + chainId: caipNetwork.caipNetworkId + }) + + this.emit('switchNetwork', user) + } + } /** * Disconnects the current wallet. diff --git a/packages/appkit/src/client.ts b/packages/appkit/src/client.ts index a11541018b..12ba63bc42 100644 --- a/packages/appkit/src/client.ts +++ b/packages/appkit/src/client.ts @@ -23,7 +23,8 @@ import { type OptionsControllerState, type WalletFeature, type ConnectMethod, - type SocialProvider + type SocialProvider, + type Metadata } from '@reown/appkit-core' import { AccountController, @@ -53,7 +54,8 @@ import { type CaipNetworkId, NetworkUtil, ConstantsUtil, - ParseUtil + ParseUtil, + getW3mThemeVariables } from '@reown/appkit-common' import type { AppKitOptions } from './utils/TypesUtil.js' import { @@ -946,7 +948,7 @@ export class AppKit { disconnect: async () => { const namespace = ChainController.state.activeChain as ChainNamespace const adapter = this.getAdapter(namespace) - const provider = ProviderUtil.getProvider(namespace) + const provider = ProviderUtil.getProvider(namespace) const providerType = ProviderUtil.state.providerIds[namespace] await adapter?.disconnect({ provider, providerType }) @@ -1086,12 +1088,9 @@ export class AppKit { AccountController.state.address && caipNetwork.chainNamespace === ChainController.state.activeChain ) { - const adapter = this.getAdapter(ChainController.state.activeChain as ChainNamespace) - const provider = ProviderUtil.getProvider( - ChainController.state.activeChain as ChainNamespace - ) - const providerType = - ProviderUtil.state.providerIds[ChainController.state.activeChain as ChainNamespace] + const adapter = this.getAdapter(ChainController.state.activeChain) + const provider = ProviderUtil.getProvider(ChainController.state.activeChain) + const providerType = ProviderUtil.state.providerIds[ChainController.state.activeChain] await adapter?.switchNetwork({ caipNetwork, provider, providerType }) this.setCaipNetwork(caipNetwork) @@ -1115,6 +1114,7 @@ export class AppKit { type: UtilConstantsUtil.CONNECTOR_TYPE_AUTH as ConnectorType, caipNetwork }) + this.setCaipNetwork(caipNetwork) } catch (error) { const adapter = this.getAdapter(caipNetwork.chainNamespace as ChainNamespace) await adapter?.switchNetwork({ @@ -1222,18 +1222,8 @@ export class AppKit { this.setLoading(false) } }) - provider.onIsConnected(() => { - provider.connect() - StorageUtil.addConnectedNamespace(ChainController.state.activeChain as ChainNamespace) - }) provider.onConnect(async user => { const namespace = ChainController.state.activeChain as ChainNamespace - this.syncProvider({ - type: UtilConstantsUtil.CONNECTOR_TYPE_AUTH as ConnectorType, - provider, - id: ConstantsUtil.CONNECTOR_ID.AUTH, - chainNamespace: namespace - }) // To keep backwards compatibility, eip155 chainIds are numbers and not actual caipChainIds const caipAddress = @@ -1311,6 +1301,21 @@ export class AppKit { const { isConnected } = await provider.isConnected() + const theme = ThemeController.getSnapshot() + const options = OptionsController.getSnapshot() + + provider.syncDappData({ + metadata: options.metadata as Metadata, + sdkVersion: options.sdkVersion, + projectId: options.projectId, + sdkType: options.sdkType + }) + provider.syncTheme({ + themeMode: theme.themeMode, + themeVariables: theme.themeVariables, + w3mThemeVariables: getW3mThemeVariables(theme.themeVariables, theme.themeMode) + }) + const namespace = StorageUtil.getActiveNamespace() if (namespace) { @@ -1394,7 +1399,10 @@ export class AppKit { } adapter.on('switchNetwork', ({ address, chainId }) => { - if (chainId && this.caipNetworks?.find(n => n.id === chainId)) { + if ( + chainId && + this.caipNetworks?.find(n => n.id === chainId || n.caipNetworkId === chainId) + ) { if (ChainController.state.activeChain === chainNamespace && address) { this.syncAccount({ address, chainId, chainNamespace }) } else if ( @@ -1597,8 +1605,7 @@ export class AppKit { ) { const { address, chainId, chainNamespace } = params - const { namespace: activeNamespace, chainId: activeChainId } = - StorageUtil.getActiveNetworkProps() + const { chainId: activeChainId } = StorageUtil.getActiveNetworkProps() const chainIdToUse = chainId || activeChainId // Only update state when needed @@ -1609,15 +1616,15 @@ export class AppKit { this.setStatus('connected', chainNamespace) - if (chainIdToUse && chainNamespace === activeNamespace) { + if (chainIdToUse) { let caipNetwork = this.caipNetworks?.find(n => n.id.toString() === chainIdToUse.toString()) let fallbackCaipNetwork = this.caipNetworks?.find(n => n.chainNamespace === chainNamespace) - const shouldSupportsAllNetworks = ChainController.getNetworkProp( + const shouldSupportAllNetworks = ChainController.getNetworkProp( 'supportsAllNetworks', chainNamespace ) - if (!shouldSupportsAllNetworks) { + if (!shouldSupportAllNetworks) { // Connection can be requested for a chain that is not supported by the wallet so we need to use approved networks here const caipNetworkIds = this.getApprovedCaipNetworkIds() || [] const caipNetworkId = caipNetworkIds.find( @@ -1629,12 +1636,17 @@ export class AppKit { caipNetwork = this.caipNetworks?.find(n => n.caipNetworkId === caipNetworkId) fallbackCaipNetwork = this.caipNetworks?.find( - n => n.caipNetworkId === fallBackCaipNetworkId + n => + n.caipNetworkId === fallBackCaipNetworkId || + // This is a workaround used in Solana network to support deprecated caipNetworkId + ('deprecatedCaipNetworkId' in n && n.deprecatedCaipNetworkId === fallBackCaipNetworkId) ) } const network = (caipNetwork || fallbackCaipNetwork) as CaipNetwork - this.setCaipNetwork(network) + if (network.chainNamespace === ChainController.state.activeChain) { + this.setCaipNetwork(network) + } this.syncConnectedWalletInfo(chainNamespace) await this.syncBalance({ address, chainId: network.id, chainNamespace }) @@ -1953,6 +1965,7 @@ export class AppKit { this.authProvider = W3mFrameProviderSingleton.getInstance({ projectId: this.options.projectId, enableLogger: this.options.enableAuthLogger, + chainId: this.getCaipNetwork()?.caipNetworkId, onTimeout: () => { AlertController.open(ErrorUtil.ALERT_ERRORS.SOCIALS_TIMEOUT, 'error') } diff --git a/packages/appkit/src/universal-adapter/client.ts b/packages/appkit/src/universal-adapter/client.ts index cd2d48ae1a..329dbdcf9f 100644 --- a/packages/appkit/src/universal-adapter/client.ts +++ b/packages/appkit/src/universal-adapter/client.ts @@ -194,7 +194,7 @@ export class UniversalAdapter extends AdapterBlueprint { } // eslint-disable-next-line @typescript-eslint/require-await - public async switchNetwork(params: AdapterBlueprint.SwitchNetworkParams) { + public override async switchNetwork(params: AdapterBlueprint.SwitchNetworkParams) { const { caipNetwork } = params const connector = this.connectors.find(c => c.type === 'WALLET_CONNECT') const provider = connector?.provider as UniversalProvider @@ -202,7 +202,7 @@ export class UniversalAdapter extends AdapterBlueprint { if (!provider) { throw new Error('UniversalAdapter:switchNetwork - provider is undefined') } - provider.setDefaultChain(`${caipNetwork.chainNamespace}:${String(caipNetwork.id)}`) + provider.setDefaultChain(caipNetwork.caipNetworkId) } public getWalletConnectProvider() { diff --git a/packages/appkit/tests/appkit.test.ts b/packages/appkit/tests/appkit.test.ts index f0ada099b2..a0c425dcae 100644 --- a/packages/appkit/tests/appkit.test.ts +++ b/packages/appkit/tests/appkit.test.ts @@ -72,16 +72,17 @@ describe('Base', () => { let appKit: AppKit beforeEach(() => { - vi.resetAllMocks() - vi.mocked(ConnectorController).getConnectors = vi.fn().mockReturnValue([]) vi.mocked(CaipNetworksUtil).extendCaipNetworks = vi.fn().mockReturnValue([]) appKit = new AppKit(mockOptions) + + vi.spyOn(OptionsController, 'getSnapshot').mockReturnValue({ ...OptionsController.state }) + vi.spyOn(ThemeController, 'getSnapshot').mockReturnValue({ ...ThemeController.state }) }) afterEach(() => { - vi.clearAllMocks() + vi.restoreAllMocks() }) describe('Base Initialization', () => { @@ -147,7 +148,7 @@ describe('Base', () => { }) it('should get theme mode', () => { - vi.mocked(ThemeController).state = { themeMode: 'dark' } as any + vi.spyOn(ThemeController.state, 'themeMode', 'get').mockReturnValueOnce('dark') expect(appKit.getThemeMode()).toBe('dark') }) @@ -157,9 +158,9 @@ describe('Base', () => { }) it('should get theme variables', () => { - vi.mocked(ThemeController).state = { - themeVariables: { '--w3m-accent': '#000' } - } as any + vi.spyOn(ThemeController.state, 'themeVariables', 'get').mockReturnValueOnce({ + '--w3m-accent': '#000' + }) expect(appKit.getThemeVariables()).toEqual({ '--w3m-accent': '#000' }) }) @@ -333,7 +334,7 @@ describe('Base', () => { it('should set CAIP address', () => { // First mock AccountController.setCaipAddress to update ChainController state - vi.mocked(AccountController.setCaipAddress).mockImplementation(() => { + vi.spyOn(AccountController, 'setCaipAddress').mockImplementation(() => { vi.spyOn(ChainController, 'state', 'get').mockReturnValueOnce({ ...ChainController.state, activeCaipAddress: 'eip155:1:0x123', @@ -523,7 +524,7 @@ describe('Base', () => { chains: new Map([['eip155', { namespace: 'eip155' }]]) } as any) - vi.mocked(CoreHelperUtil.createAccount).mockImplementation((namespace, address, type) => { + vi.spyOn(CoreHelperUtil, 'createAccount').mockImplementation((namespace, address, type) => { if (namespace === 'eip155') { return { address, @@ -549,7 +550,9 @@ describe('Base', () => { isConnected: vi.fn().mockResolvedValue({ isConnected: false }), getEmail: vi.fn().mockReturnValue('email@email.com'), getUsername: vi.fn().mockReturnValue('test'), - onSocialConnected: vi.fn() + onSocialConnected: vi.fn(), + syncDappData: vi.fn(), + syncTheme: vi.fn() } const appKitWithAuth = new AppKit({ @@ -662,6 +665,7 @@ describe('Base', () => { ]) vi.mocked(ChainController.getAllApprovedCaipNetworkIds).mockReturnValue(['eip155:1']) vi.spyOn(ChainController, 'getNetworkProp').mockReturnValue(false) + vi.spyOn(ChainController.state, 'activeChain', 'get').mockReturnValueOnce('eip155') vi.mocked(appKit as any).caipNetworks = [ { id: '1', @@ -681,9 +685,8 @@ describe('Base', () => { caipNetworkId: 'eip155:1' }) - vi.mocked(OptionsController).state = { - allowAllNetworks: false - } as any + OptionsController.state.allowUnsupportedChain = undefined + vi.spyOn(OptionsController.state, 'allowUnsupportedChain', 'get').mockReturnValueOnce(false) await appKit['syncAccount'](mockAccountData) @@ -1003,9 +1006,8 @@ describe('Base', () => { } as any) ;(appKit as any).caipNetworks = [{ id: 'eip155:1', chainNamespace: 'eip155' }] - vi.mocked(OptionsController).state = { - allowUnsupportedChain: true - } as any + OptionsController.state.allowUnsupportedChain = undefined + vi.spyOn(OptionsController.state, 'allowUnsupportedChain', 'get').mockResolvedValueOnce(true) const overrideAdapter = { getAccounts: vi.fn().mockResolvedValue({ accounts: [] }), @@ -1216,8 +1218,6 @@ describe('Base', () => { let mockUniversalAdapter: any beforeEach(() => { - vi.restoreAllMocks() - vi.spyOn(ChainController, 'state', 'get').mockReturnValue({ chains: new Map(), activeChain: 'eip155' @@ -1541,6 +1541,9 @@ describe('Adapter Management', () => { let mockNetwork: AppKitNetwork beforeEach(() => { + vi.spyOn(OptionsController, 'getSnapshot').mockReturnValue({ ...OptionsController.state }) + vi.spyOn(ThemeController, 'getSnapshot').mockReturnValue({ ...ThemeController.state }) + mockAdapter = { namespace: 'eip155', construct: vi.fn(), diff --git a/packages/appkit/tests/mocks/Options.ts b/packages/appkit/tests/mocks/Options.ts index 988cc708c7..d803b56b8f 100644 --- a/packages/appkit/tests/mocks/Options.ts +++ b/packages/appkit/tests/mocks/Options.ts @@ -4,7 +4,10 @@ import { mainnet, solana } from '../../src/networks/index.js' import type { SdkVersion } from '@reown/appkit-core' import { vi } from 'vitest' -export const mockOptions = { +export const mockOptions: AppKitOptions & { + sdkVersion: SdkVersion + sdkType: string +} = { projectId: 'test-project-id', adapters: [ { @@ -31,7 +34,6 @@ export const mockOptions = { url: 'https://test-app.com', icons: ['https://test-app.com/icon.png'] }, - sdkVersion: `html-wagmi-5.1.6` as SdkVersion -} as unknown as AppKitOptions & { - sdkVersion: SdkVersion + sdkVersion: `html-wagmi-5.1.6`, + sdkType: 'appkit' } diff --git a/packages/appkit/tests/universal-adapter.test.ts b/packages/appkit/tests/universal-adapter.test.ts index 7e7c94578f..e58d42cfd5 100644 --- a/packages/appkit/tests/universal-adapter.test.ts +++ b/packages/appkit/tests/universal-adapter.test.ts @@ -115,6 +115,7 @@ describe('UniversalAdapter', () => { it('should switch network successfully', async () => { const polygonNetwork: CaipNetwork = { ...mockCaipNetwork, + caipNetworkId: 'eip155:137', id: 137, name: 'Polygon', nativeCurrency: { diff --git a/packages/core/src/controllers/ChainController.ts b/packages/core/src/controllers/ChainController.ts index 0e95a1e94b..9240605ecf 100644 --- a/packages/core/src/controllers/ChainController.ts +++ b/packages/core/src/controllers/ChainController.ts @@ -294,17 +294,16 @@ export const ChainController = { const unsupportedNetwork = !activeAdapter?.caipNetworks?.some( caipNetwork => caipNetwork.id === state.activeCaipNetwork?.id ) - const networkControllerClient = this.getNetworkControllerClient(network.chainNamespace) - - if (networkControllerClient) { - await networkControllerClient.switchCaipNetwork(network) - } if (unsupportedNetwork) { RouterController.goBack() } - this.setActiveCaipNetwork(network) + const networkControllerClient = this.getNetworkControllerClient(network.chainNamespace) + + if (networkControllerClient) { + await networkControllerClient.switchCaipNetwork(network) + } if (network) { EventsController.sendEvent({ diff --git a/packages/core/src/controllers/OptionsController.ts b/packages/core/src/controllers/OptionsController.ts index fdb458ee5f..38e84023d9 100644 --- a/packages/core/src/controllers/OptionsController.ts +++ b/packages/core/src/controllers/OptionsController.ts @@ -1,5 +1,5 @@ import { subscribeKey as subKey } from 'valtio/vanilla/utils' -import { proxy } from 'valtio/vanilla' +import { proxy, snapshot } from 'valtio/vanilla' import type { ConnectMethod, CustomWallet, @@ -317,5 +317,9 @@ export const OptionsController = { useInjectedUniversalProvider: OptionsControllerState['useInjectedUniversalProvider'] ) { state.useInjectedUniversalProvider = useInjectedUniversalProvider + }, + + getSnapshot() { + return snapshot(state) } } diff --git a/packages/wallet/src/W3mFrameProvider.ts b/packages/wallet/src/W3mFrameProvider.ts index 33595b98ea..d8b7effaf0 100644 --- a/packages/wallet/src/W3mFrameProvider.ts +++ b/packages/wallet/src/W3mFrameProvider.ts @@ -30,6 +30,8 @@ export class W3mFrameProvider { public onTimeout?: () => void + public user?: W3mFrameTypes.Responses['FrameGetUserResponse'] + public constructor({ projectId, chainId, @@ -44,6 +46,12 @@ export class W3mFrameProvider { if (this.getLoginEmailUsed()) { this.w3mFrame.initFrame() } + + this.w3mFrame.events.onFrameEvent(event => { + if (event.type === W3mFrameConstants.FRAME_GET_USER_SUCCESS) { + this.user = event.payload + } + }) } // -- Extended Methods ------------------------------------------------ @@ -618,7 +626,10 @@ export class W3mFrameProvider { } public getLastUsedChainId() { - return Number(W3mFrameStorage.get(W3mFrameConstants.LAST_USED_CHAIN_KEY)) + const chainId = W3mFrameStorage.get(W3mFrameConstants.LAST_USED_CHAIN_KEY) ?? undefined + const numberChainId = Number(chainId) + + return isNaN(numberChainId) ? chainId : numberChainId } private persistSmartAccountEnabledNetworks(networks: number[]) {