diff --git a/.changeset/small-parents-judge.md b/.changeset/small-parents-judge.md new file mode 100644 index 0000000000..3462b95f5c --- /dev/null +++ b/.changeset/small-parents-judge.md @@ -0,0 +1,38 @@ +--- +'@reown/appkit-adapter-ethers5': patch +'@reown/appkit-adapter-ethers': patch +'@reown/appkit-adapter-solana': patch +'@reown/appkit-adapter-wagmi': patch +'@reown/appkit-wallet': patch +'@reown/appkit-core': patch +'@apps/demo': patch +'@apps/gallery': patch +'@apps/laboratory': patch +'@examples/html-ethers': patch +'@examples/html-ethers5': patch +'@examples/html-wagmi': patch +'@examples/next-ethers': patch +'@examples/next-wagmi': patch +'@examples/react-ethers': patch +'@examples/react-ethers5': patch +'@examples/react-solana': patch +'@examples/react-wagmi': patch +'@examples/vue-ethers5': patch +'@examples/vue-solana': patch +'@examples/vue-wagmi': patch +'@reown/appkit-adapter-polkadot': patch +'@reown/appkit': patch +'@reown/appkit-utils': patch +'@reown/appkit-cdn': patch +'@reown/appkit-common': patch +'@reown/appkit-ethers': patch +'@reown/appkit-ethers5': patch +'@reown/appkit-polyfills': patch +'@reown/appkit-scaffold-ui': patch +'@reown/appkit-siwe': patch +'@reown/appkit-solana': patch +'@reown/appkit-ui': patch +'@reown/appkit-wagmi': patch +--- + +Add Solana CoinbaseWalletProvider to allow connecting with coinbase extension diff --git a/.changeset/wise-bobcats-hang.md b/.changeset/wise-bobcats-hang.md new file mode 100644 index 0000000000..d2b1000398 --- /dev/null +++ b/.changeset/wise-bobcats-hang.md @@ -0,0 +1,38 @@ +--- +'@reown/appkit-adapter-ethers5': patch +'@reown/appkit-adapter-ethers': patch +'@reown/appkit-utils': patch +'@reown/appkit': patch +'@reown/appkit-common': patch +'@reown/appkit-wallet': patch +'@reown/appkit-core': patch +'@apps/demo': patch +'@apps/gallery': patch +'@apps/laboratory': patch +'@examples/html-ethers': patch +'@examples/html-ethers5': patch +'@examples/html-wagmi': patch +'@examples/next-ethers': patch +'@examples/next-wagmi': patch +'@examples/react-ethers': patch +'@examples/react-ethers5': patch +'@examples/react-solana': patch +'@examples/react-wagmi': patch +'@examples/vue-ethers5': patch +'@examples/vue-solana': patch +'@examples/vue-wagmi': patch +'@reown/appkit-adapter-polkadot': patch +'@reown/appkit-adapter-solana': patch +'@reown/appkit-adapter-wagmi': patch +'@reown/appkit-cdn': patch +'@reown/appkit-ethers': patch +'@reown/appkit-ethers5': patch +'@reown/appkit-polyfills': patch +'@reown/appkit-scaffold-ui': patch +'@reown/appkit-siwe': patch +'@reown/appkit-solana': patch +'@reown/appkit-ui': patch +'@reown/appkit-wagmi': patch +--- + +Updates the localstorage keys diff --git a/apps/laboratory/src/pages/library/multichain-wagmi-solana.tsx b/apps/laboratory/src/pages/library/multichain-wagmi-solana.tsx index 26add712ef..25c12cbad2 100644 --- a/apps/laboratory/src/pages/library/multichain-wagmi-solana.tsx +++ b/apps/laboratory/src/pages/library/multichain-wagmi-solana.tsx @@ -7,17 +7,13 @@ import { ConstantsUtil } from '../../utils/ConstantsUtil' import { QueryClient, QueryClientProvider } from '@tanstack/react-query' import { WagmiProvider } from 'wagmi' import { - arbitrum, mainnet, polygon, - base, - binanceSmartChain, solana, - solanaTestnet, - solanaDevnet, + arbitrum, optimism, - zkSync, - sepolia + solanaTestnet, + solanaDevnet } from '@reown/appkit/networks' import { AppKitButtons } from '../../components/AppKitButtons' import { HuobiWalletAdapter, SolflareWalletAdapter } from '@solana/wallet-adapter-wallets' @@ -25,7 +21,7 @@ import { MultiChainTestsWagmiSolana } from '../../components/MultiChainTestsWagm const queryClient = new QueryClient() -const networks = [mainnet, optimism, polygon, zkSync, arbitrum, sepolia] +const networks = [mainnet, polygon, solana, arbitrum, optimism, solanaTestnet, solanaDevnet] const wagmiAdapter = new WagmiAdapter({ ssr: true, @@ -39,16 +35,7 @@ const solanaWeb3JsAdapter = new SolanaAdapter({ const modal = createAppKit({ adapters: [wagmiAdapter, solanaWeb3JsAdapter], - networks: [ - mainnet, - polygon, - base, - binanceSmartChain, - arbitrum, - solana, - solanaTestnet, - solanaDevnet - ], + networks, defaultNetwork: mainnet, projectId: ConstantsUtil.ProjectId, features: { diff --git a/apps/laboratory/tests/multichain/multichain-wagmi-solana.spec.ts b/apps/laboratory/tests/multichain/multichain-wagmi-solana.spec.ts index 3aa394866d..696430d028 100644 --- a/apps/laboratory/tests/multichain/multichain-wagmi-solana.spec.ts +++ b/apps/laboratory/tests/multichain/multichain-wagmi-solana.spec.ts @@ -48,7 +48,7 @@ test.skip('it should show disabled networks', async () => { await modalPage.closeModal() }) -test('it should switch networks and sign', async () => { +test.only('it should switch networks and sign', async () => { const chains = ['Polygon', 'Solana'] async function processChain(index: number) { diff --git a/apps/laboratory/tests/shared/utils/validation.ts b/apps/laboratory/tests/shared/utils/validation.ts index bed6966135..57493449fb 100644 --- a/apps/laboratory/tests/shared/utils/validation.ts +++ b/apps/laboratory/tests/shared/utils/validation.ts @@ -8,6 +8,6 @@ export async function expectConnection( await modalValidator.expectConnected() await walletValidator.expectConnected() await modalValidator.page.evaluate( - `window.localStorage.setItem('WALLETCONNECT_DEEPLINK_CHOICE', '')` + `window.localStorage.setItem('@appkit/deeplink_choice', JSON.stringify({ href: '', name: '' }))` ) } diff --git a/apps/laboratory/tests/shared/validators/ModalValidator.ts b/apps/laboratory/tests/shared/validators/ModalValidator.ts index 23eb041ecf..2dc78b59a1 100644 --- a/apps/laboratory/tests/shared/validators/ModalValidator.ts +++ b/apps/laboratory/tests/shared/validators/ModalValidator.ts @@ -96,6 +96,10 @@ export class ModalValidator { async expectRejectedSign() { // We use Chakra Toast and it's not quite straightforward to set the `data-testid` attribute on the toast element. await expect(this.page.getByText(ConstantsUtil.SigningFailedToastTitle)).toBeVisible() + const closeButton = this.page.locator('#toast-close-button') + + await expect(closeButton).toBeVisible() + await closeButton.click() } async expectSwitchedNetwork(network: string) { diff --git a/packages/adapters/ethers/src/tests/client.test.ts b/packages/adapters/ethers/src/tests/client.test.ts index 385c33d41e..fa86bac689 100644 --- a/packages/adapters/ethers/src/tests/client.test.ts +++ b/packages/adapters/ethers/src/tests/client.test.ts @@ -9,8 +9,8 @@ import { EthersHelpersUtil, type ProviderId, type ProviderType } from '@reown/ap import { ConstantsUtil } from '@reown/appkit-utils' import { arbitrum, mainnet, polygon } from '@reown/appkit/networks' import { ProviderUtil } from '@reown/appkit/store' -import { SafeLocalStorage } from '@reown/appkit-common' -import { WcConstantsUtil, type BlockchainApiLookupEnsName } from '@reown/appkit' +import { SafeLocalStorage, SafeLocalStorageKeys } from '@reown/appkit-common' +import { type BlockchainApiLookupEnsName } from '@reown/appkit' import { InfuraProvider, JsonRpcProvider } from 'ethers' import type { CaipNetwork, ChainNamespace } from '@reown/appkit-common' @@ -519,8 +519,14 @@ describe('EthersAdapter', () => { const mockProvider = { request: vi.fn() } await client['setProvider'](mockProvider as any, 'injected', 'MetaMask') - expect(SafeLocalStorage.setItem).toHaveBeenCalledWith(WcConstantsUtil.WALLET_ID, 'injected') - expect(SafeLocalStorage.setItem).toHaveBeenCalledWith(WcConstantsUtil.WALLET_NAME, 'MetaMask') + expect(SafeLocalStorage.setItem).toHaveBeenCalledWith( + SafeLocalStorageKeys.WALLET_ID, + 'injected' + ) + expect(SafeLocalStorage.setItem).toHaveBeenCalledWith( + SafeLocalStorageKeys.WALLET_NAME, + 'MetaMask' + ) expect(mockAppKit.setCaipNetwork).toHaveBeenCalled() expect(mockAppKit.setCaipAddress).toHaveBeenCalled() expect(ProviderUtil.setProviderId).toHaveBeenCalledWith('eip155', 'injected') @@ -557,7 +563,7 @@ describe('EthersAdapter', () => { )[1] await disconnectHandler() - expect(SafeLocalStorage.removeItem).toHaveBeenCalledWith(WcConstantsUtil.WALLET_ID) + expect(SafeLocalStorage.removeItem).toHaveBeenCalledWith(SafeLocalStorageKeys.WALLET_ID) expect(mockProvider.removeListener).toHaveBeenCalledTimes(3) }) @@ -595,9 +601,9 @@ describe('EthersAdapter', () => { } vi.spyOn(SafeLocalStorage, 'getItem').mockImplementation(key => { - if (key === WcConstantsUtil.WALLET_ID) return ConstantsUtil.INJECTED_CONNECTOR_ID - if (key === WcConstantsUtil.WALLET_NAME) return 'MetaMask' - return null + if (key === SafeLocalStorageKeys.WALLET_ID) return ConstantsUtil.INJECTED_CONNECTOR_ID + if (key === SafeLocalStorageKeys.WALLET_NAME) return 'MetaMask' + return undefined }) vi.spyOn(client as any, 'setProvider').mockImplementation(() => Promise.resolve()) @@ -613,7 +619,7 @@ describe('EthersAdapter', () => { client['checkActiveProviders'](mockConfig as ProviderType) - expect(SafeLocalStorage.getItem).toHaveBeenCalledWith(WcConstantsUtil.WALLET_ID) + expect(SafeLocalStorage.getItem).toHaveBeenCalledWith(SafeLocalStorageKeys.WALLET_ID) expect(client['setProvider']).toHaveBeenCalledWith( mockInjectedProvider, ConstantsUtil.INJECTED_CONNECTOR_ID @@ -625,7 +631,7 @@ describe('EthersAdapter', () => { }) it('should not set provider when wallet ID is not found', () => { - vi.spyOn(SafeLocalStorage, 'getItem').mockReturnValue(null) + vi.spyOn(SafeLocalStorage, 'getItem').mockReturnValue(undefined) const mockConfig = { injected: mockInjectedProvider, @@ -635,7 +641,7 @@ describe('EthersAdapter', () => { client['checkActiveProviders'](mockConfig as ProviderType) - expect(SafeLocalStorage.getItem).toHaveBeenCalledWith(WcConstantsUtil.WALLET_ID) + expect(SafeLocalStorage.getItem).toHaveBeenCalledWith(SafeLocalStorageKeys.WALLET_ID) expect(client['setProvider']).not.toHaveBeenCalled() expect(client['setupProviderListeners']).not.toHaveBeenCalled() }) @@ -649,7 +655,7 @@ describe('EthersAdapter', () => { client['checkActiveProviders'](mockConfig as ProviderType) - expect(SafeLocalStorage.getItem).toHaveBeenCalledWith(WcConstantsUtil.WALLET_ID) + expect(SafeLocalStorage.getItem).toHaveBeenCalledWith(SafeLocalStorageKeys.WALLET_ID) expect(client['setProvider']).not.toHaveBeenCalled() expect(client['setupProviderListeners']).not.toHaveBeenCalled() }) diff --git a/packages/adapters/ethers5/src/tests/client.test.ts b/packages/adapters/ethers5/src/tests/client.test.ts index 37dd009a4e..a0e57eebb8 100644 --- a/packages/adapters/ethers5/src/tests/client.test.ts +++ b/packages/adapters/ethers5/src/tests/client.test.ts @@ -9,8 +9,8 @@ import { EthersHelpersUtil, type ProviderId, type ProviderType } from '@reown/ap import { ConstantsUtil } from '@reown/appkit-utils' import { arbitrum, mainnet, polygon } from '@reown/appkit/networks' import { ProviderUtil } from '@reown/appkit/store' -import { SafeLocalStorage } from '@reown/appkit-common' -import { WcConstantsUtil, type BlockchainApiLookupEnsName } from '@reown/appkit' +import { SafeLocalStorage, SafeLocalStorageKeys } from '@reown/appkit-common' +import { type BlockchainApiLookupEnsName } from '@reown/appkit' import { ethers } from 'ethers5' import type { CaipNetwork, ChainNamespace } from '@reown/appkit-common' @@ -524,8 +524,14 @@ describe('EthersAdapter', () => { const mockProvider = { request: vi.fn() } await client['setProvider'](mockProvider as any, 'injected', 'MetaMask') - expect(SafeLocalStorage.setItem).toHaveBeenCalledWith(WcConstantsUtil.WALLET_ID, 'injected') - expect(SafeLocalStorage.setItem).toHaveBeenCalledWith(WcConstantsUtil.WALLET_NAME, 'MetaMask') + expect(SafeLocalStorage.setItem).toHaveBeenCalledWith( + SafeLocalStorageKeys.WALLET_ID, + 'injected' + ) + expect(SafeLocalStorage.setItem).toHaveBeenCalledWith( + SafeLocalStorageKeys.WALLET_NAME, + 'MetaMask' + ) expect(mockAppKit.setCaipNetwork).toHaveBeenCalled() expect(ProviderUtil.setProviderId).toHaveBeenCalledWith('eip155', 'injected') expect(ProviderUtil.setProvider).toHaveBeenCalledWith('eip155', mockProvider) @@ -561,7 +567,7 @@ describe('EthersAdapter', () => { )[1] await disconnectHandler() - expect(SafeLocalStorage.removeItem).toHaveBeenCalledWith(WcConstantsUtil.WALLET_ID) + expect(SafeLocalStorage.removeItem).toHaveBeenCalledWith(SafeLocalStorageKeys.WALLET_ID) expect(mockProvider.removeListener).toHaveBeenCalledTimes(3) }) @@ -599,9 +605,9 @@ describe('EthersAdapter', () => { } vi.spyOn(SafeLocalStorage, 'getItem').mockImplementation(key => { - if (key === WcConstantsUtil.WALLET_ID) return ConstantsUtil.INJECTED_CONNECTOR_ID - if (key === WcConstantsUtil.WALLET_NAME) return 'MetaMask' - return null + if (key === SafeLocalStorageKeys.WALLET_ID) return ConstantsUtil.INJECTED_CONNECTOR_ID + if (key === SafeLocalStorageKeys.WALLET_NAME) return 'MetaMask' + return undefined }) vi.spyOn(client as any, 'setProvider').mockImplementation(() => Promise.resolve()) @@ -617,7 +623,7 @@ describe('EthersAdapter', () => { client['checkActiveProviders'](mockConfig as ProviderType) - expect(SafeLocalStorage.getItem).toHaveBeenCalledWith(WcConstantsUtil.WALLET_ID) + expect(SafeLocalStorage.getItem).toHaveBeenCalledWith(SafeLocalStorageKeys.WALLET_ID) expect(client['setProvider']).toHaveBeenCalledWith( mockInjectedProvider, ConstantsUtil.INJECTED_CONNECTOR_ID @@ -629,7 +635,7 @@ describe('EthersAdapter', () => { }) it('should not set provider when wallet ID is not found', () => { - vi.spyOn(SafeLocalStorage, 'getItem').mockReturnValue(null) + vi.spyOn(SafeLocalStorage, 'getItem').mockReturnValue(undefined) const mockConfig = { injected: mockInjectedProvider, @@ -639,7 +645,7 @@ describe('EthersAdapter', () => { client['checkActiveProviders'](mockConfig as ProviderType) - expect(SafeLocalStorage.getItem).toHaveBeenCalledWith(WcConstantsUtil.WALLET_ID) + expect(SafeLocalStorage.getItem).toHaveBeenCalledWith(SafeLocalStorageKeys.WALLET_ID) expect(client['setProvider']).not.toHaveBeenCalled() expect(client['setupProviderListeners']).not.toHaveBeenCalled() }) @@ -653,7 +659,7 @@ describe('EthersAdapter', () => { client['checkActiveProviders'](mockConfig as ProviderType) - expect(SafeLocalStorage.getItem).toHaveBeenCalledWith(WcConstantsUtil.WALLET_ID) + expect(SafeLocalStorage.getItem).toHaveBeenCalledWith(SafeLocalStorageKeys.WALLET_ID) expect(client['setProvider']).not.toHaveBeenCalled() expect(client['setupProviderListeners']).not.toHaveBeenCalled() }) diff --git a/packages/adapters/solana/src/client.ts b/packages/adapters/solana/src/client.ts index a12ea5fbba..6195d52c1c 100644 --- a/packages/adapters/solana/src/client.ts +++ b/packages/adapters/solana/src/client.ts @@ -46,6 +46,7 @@ import { ProviderUtil } from '@reown/appkit/store' import { W3mFrameProviderSingleton } from '@reown/appkit/auth-provider' import { ConstantsUtil } from '@reown/appkit-utils' import { createSendTransaction } from './utils/createSendTransaction.js' +import { CoinbaseWalletProvider } from './providers/CoinbaseWalletProvider.js' export interface AdapterOptions { connectionSettings?: Commitment | ConnectionConfig @@ -140,14 +141,10 @@ export class SolanaAdapter implements ChainAdapter { this.networkControllerClient = { switchCaipNetwork: async caipNetwork => { if (caipNetwork) { - SafeLocalStorage.setItem( - SafeLocalStorageKeys.ACTIVE_CAIP_NETWORK, - JSON.stringify(caipNetwork) - ) try { await this.switchNetwork(caipNetwork) } catch (error) { - // SolStoreUtil.setError(error) + console.warn('Error switching network', error) } } }, @@ -326,15 +323,23 @@ export class SolanaAdapter implements ChainAdapter { }) EventsController.subscribe(state => { - if (state.data.event === 'SELECT_WALLET' && state.data.properties?.name === 'Phantom') { + if (state.data.event === 'SELECT_WALLET') { const isMobile = CoreHelperUtil.isMobile() const isClient = CoreHelperUtil.isClient() - if (isMobile && isClient && !('phantom' in window)) { - const href = window.location.href - const protocol = href.startsWith('https') ? 'https' : 'http' - const host = href.split('/')[2] - const ref = `${protocol}://${host}` - window.location.href = `https://phantom.app/ul/browse/${href}?ref=${ref}` + + if (isMobile && isClient) { + if (state.data.properties?.name === 'Phantom' && !('phantom' in window)) { + const href = window.location.href + const protocol = href.startsWith('https') ? 'https' : 'http' + const host = href.split('/')[2] + const ref = `${protocol}://${host}` + window.location.href = `https://phantom.app/ul/browse/${href}?ref=${ref}` + } + + if (state.data.properties?.name === 'Coinbase Wallet' && !('coinbaseSolana' in window)) { + const href = window.location.href + window.location.href = `https://go.cb-w.com/dapp?cb_url=${href}` + } } } }) @@ -634,6 +639,17 @@ export class SolanaAdapter implements ChainAdapter { this.addProvider(this.authProvider) } + if ('coinbaseSolana' in window) { + this.addProvider( + new CoinbaseWalletProvider({ + // @ts-expect-error - window is not typed + provider: window.coinbaseSolana, + chains: this.caipNetworks, + getActiveChain: () => this.appKit?.getCaipNetwork(this.chainNamespace) + }) + ) + } + if (this.appKit && this.caipNetworks[0]) { watchStandard(this.appKit, this.caipNetworks[0], standardAdapters => this.addProvider.bind(this)(...standardAdapters) diff --git a/packages/adapters/solana/src/providers/AuthProvider.ts b/packages/adapters/solana/src/providers/AuthProvider.ts index abcebb7e9f..a0c780cf22 100644 --- a/packages/adapters/solana/src/providers/AuthProvider.ts +++ b/packages/adapters/solana/src/providers/AuthProvider.ts @@ -57,8 +57,8 @@ export class AuthProvider extends ProviderEventEmitter implements Provider, Prov get publicKey(): PublicKey | undefined { const session = this.getSession() - - if (session) { + const namespace = this.getActiveNamespace() + if (session && namespace === 'solana') { return new PublicKey(session.address) } @@ -201,7 +201,8 @@ export class AuthProvider extends ProviderEventEmitter implements Provider, Prov required?: Required ): Required extends true ? PublicKey : PublicKey | undefined { const session = this.getSession() - if (!session) { + const namespace = this.getActiveNamespace() + if (!session || namespace !== 'solana') { if (required) { throw new Error('Account is required') } diff --git a/packages/adapters/solana/src/providers/CoinbaseWalletProvider.ts b/packages/adapters/solana/src/providers/CoinbaseWalletProvider.ts new file mode 100644 index 0000000000..fe5d4bbe5b --- /dev/null +++ b/packages/adapters/solana/src/providers/CoinbaseWalletProvider.ts @@ -0,0 +1,115 @@ +import { type AnyTransaction, type Provider } from '@reown/appkit-utils/solana' +import { ProviderEventEmitter } from './shared/ProviderEventEmitter.js' +import type { Connection, PublicKey, SendOptions } from '@solana/web3.js' +import { solana } from '../utils/chains.js' +import type { CaipNetwork } from '@reown/appkit-common' + +export type SolanaCoinbaseWallet = { + publicKey?: PublicKey + signTransaction(transaction: T): Promise + signAllTransactions(transactions: T[]): Promise + signAndSendTransaction( + transaction: T, + options?: SendOptions + ): Promise<{ signature: string }> + signMessage(message: Uint8Array): Promise<{ signature: Uint8Array }> + connect(): Promise + disconnect(): Promise + emit(event: string, ...args: unknown[]): void +} + +export type CoinbaseWalletProviderConfig = { + provider: SolanaCoinbaseWallet + chains: CaipNetwork[] + getActiveChain: () => CaipNetwork | undefined +} + +export class CoinbaseWalletProvider extends ProviderEventEmitter implements Provider { + public readonly name = 'Coinbase Wallet' + public readonly type = 'ANNOUNCED' + public readonly icon = + 'data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMTAyNCIgaGVpZ2h0PSIxMDI0IiB2aWV3Qm94PSIwIDAgMTAyNCAxMDI0IiBmaWxsPSJub25lIiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciPgo8Y2lyY2xlIGN4PSI1MTIiIGN5PSI1MTIiIHI9IjUxMiIgZmlsbD0iIzAwNTJGRiIvPgo8cGF0aCBmaWxsLXJ1bGU9ImV2ZW5vZGQiIGNsaXAtcnVsZT0iZXZlbm9kZCIgZD0iTTE1MiA1MTJDMTUyIDcxMC44MjMgMzEzLjE3NyA4NzIgNTEyIDg3MkM3MTAuODIzIDg3MiA4NzIgNzEwLjgyMyA4NzIgNTEyQzg3MiAzMTMuMTc3IDcxMC44MjMgMTUyIDUxMiAxNTJDMzEzLjE3NyAxNTIgMTUyIDMxMy4xNzcgMTUyIDUxMlpNNDIwIDM5NkM0MDYuNzQ1IDM5NiAzOTYgNDA2Ljc0NSAzOTYgNDIwVjYwNEMzOTYgNjE3LjI1NSA0MDYuNzQ1IDYyOCA0MjAgNjI4SDYwNEM2MTcuMjU1IDYyOCA2MjggNjE3LjI1NSA2MjggNjA0VjQyMEM2MjggNDA2Ljc0NSA2MTcuMjU1IDM5NiA2MDQgMzk2SDQyMFoiIGZpbGw9IndoaXRlIi8+Cjwvc3ZnPgo=' + + private provider: SolanaCoinbaseWallet + private requestedChains: CaipNetwork[] + + constructor(params: CoinbaseWalletProviderConfig) { + super() + this.provider = params.provider + this.requestedChains = params.chains + } + + public get chains() { + // For Coinbase Wallet, we only support the Solana mainnet + return this.requestedChains.filter(chain => chain.chainId === solana.chainId) + } + + public get publicKey() { + return this.provider.publicKey + } + + public async connect() { + try { + await this.provider.connect() + const account = this.getAccount(true) + this.provider.emit('connect', this.provider.publicKey) + this.emit('connect', account) + + return account.toBase58() + } catch (error) { + this.provider.emit('error', error) + throw error + } + } + + public async disconnect() { + await this.provider.disconnect() + this.provider.emit('disconnect', undefined) + this.emit('disconnect', undefined) + } + + public async signMessage(message: Uint8Array) { + const result = await this.provider.signMessage(message) + + return result.signature + } + + public async signTransaction(transaction: T) { + return this.provider.signTransaction(transaction) + } + + public async signAndSendTransaction( + transaction: T, + sendOptions?: SendOptions + ) { + const result = await this.provider.signAndSendTransaction(transaction, sendOptions) + + return result.signature + } + + public async sendTransaction( + transaction: AnyTransaction, + connection: Connection, + options?: SendOptions + ) { + const signedTransaction = await this.signTransaction(transaction) + const signature = await connection.sendRawTransaction(signedTransaction.serialize(), options) + + return signature + } + + public async signAllTransactions(transactions: T): Promise { + return (await this.provider.signAllTransactions(transactions)) as T + } + + private getAccount( + required?: Required + ): Required extends true ? PublicKey : PublicKey | undefined { + const account = this.provider.publicKey + if (required && !account) { + throw new Error('Not connected') + } + + return account as Required extends true ? PublicKey : PublicKey | undefined + } +} diff --git a/packages/adapters/solana/src/tests/GenericProvider.test.ts b/packages/adapters/solana/src/tests/GenericProvider.test.ts index 46fc978504..08e2757479 100644 --- a/packages/adapters/solana/src/tests/GenericProvider.test.ts +++ b/packages/adapters/solana/src/tests/GenericProvider.test.ts @@ -7,9 +7,11 @@ import { mockWalletStandard } from './mocks/WalletStandard.js' import { TestConstants } from './util/TestConstants.js' import { Transaction, VersionedTransaction } from '@solana/web3.js' import { mockLegacyTransaction, mockVersionedTransaction } from './mocks/Transaction.js' +import { mockCoinbaseWallet } from './mocks/CoinbaseWallet.js' import { AuthProvider } from '../providers/AuthProvider.js' import { mockW3mFrameProvider } from './mocks/W3mFrameProvider.js' import { isVersionedTransaction } from '@solana/wallet-adapter-base' +import { CoinbaseWalletProvider } from '../providers/CoinbaseWalletProvider.js' const getActiveChain = vi.fn(() => TestConstants.chains[0]) @@ -42,6 +44,14 @@ const providers: { name: string; provider: Provider }[] = [ setSession: vi.fn(), chains: TestConstants.chains }) + }, + { + name: 'CoinbaseWalletProvider', + provider: new CoinbaseWalletProvider({ + provider: mockCoinbaseWallet(), + chains: TestConstants.chains, + getActiveChain + }) } ] diff --git a/packages/adapters/solana/src/tests/mocks/CoinbaseWallet.ts b/packages/adapters/solana/src/tests/mocks/CoinbaseWallet.ts new file mode 100644 index 0000000000..f10c2c95b0 --- /dev/null +++ b/packages/adapters/solana/src/tests/mocks/CoinbaseWallet.ts @@ -0,0 +1,16 @@ +import { vi } from 'vitest' +import type { SolanaCoinbaseWallet } from '../../providers/CoinbaseWalletProvider.js' +import { TestConstants } from '../util/TestConstants.js' + +export function mockCoinbaseWallet(): SolanaCoinbaseWallet { + return { + publicKey: TestConstants.accounts[0].publicKey, + connect: vi.fn().mockResolvedValue(undefined), + disconnect: vi.fn().mockResolvedValue(undefined), + signMessage: vi.fn().mockResolvedValue({ signature: new Uint8Array() }), + signTransaction: vi.fn(tx => tx), + signAllTransactions: vi.fn(tx => tx), + signAndSendTransaction: vi.fn().mockResolvedValue({ signature: '' }), + emit: vi.fn() + } +} diff --git a/packages/adapters/wagmi/src/client.ts b/packages/adapters/wagmi/src/client.ts index a3bec7dea4..9ee8283a7b 100644 --- a/packages/adapters/wagmi/src/client.ts +++ b/packages/adapters/wagmi/src/client.ts @@ -234,10 +234,6 @@ export class WagmiAdapter implements ChainAdapter { this.networkControllerClient = { switchCaipNetwork: async caipNetwork => { - SafeLocalStorage.setItem( - SafeLocalStorageKeys.ACTIVE_CAIP_NETWORK, - JSON.stringify(caipNetwork) - ) const chainId = Number(NetworkUtil.caipNetworkIdToNumber(caipNetwork?.id)) if (chainId && this.wagmiConfig) { @@ -607,7 +603,7 @@ export class WagmiAdapter implements ChainAdapter { >) { const isConnected = ChainController.state.activeCaipAddress - if (status === 'disconnected' && isConnected) { + if (status === 'disconnected' && !isConnected) { this.appKit?.resetAccount(this.chainNamespace) this.appKit?.resetWcConnection() this.appKit?.resetNetwork() diff --git a/packages/adapters/wagmi/src/connectors/UniversalConnector.ts b/packages/adapters/wagmi/src/connectors/UniversalConnector.ts index 039c4902df..ec0324c416 100644 --- a/packages/adapters/wagmi/src/connectors/UniversalConnector.ts +++ b/packages/adapters/wagmi/src/connectors/UniversalConnector.ts @@ -22,7 +22,7 @@ import { WcHelpersUtil } from '@reown/appkit' import type { AppKitOptions } from '@reown/appkit' import type { AppKit } from '@reown/appkit' import { convertToAppKitChains } from '../utils/helpers.js' -import { SafeLocalStorage, SafeLocalStorageKeys, type CaipNetwork } from '@reown/appkit-common' +import { SafeLocalStorage, SafeLocalStorageKeys } from '@reown/appkit-common' type UniversalConnector = Connector & { onDisplayUri(uri: string): void @@ -232,11 +232,8 @@ export function walletConnect(parameters: AppKitOptionsParams, appKit: AppKit) { if (chainId && currentChainId !== chainId) { const storedCaipNetwork = SafeLocalStorage.getItem(SafeLocalStorageKeys.ACTIVE_CAIP_NETWORK) - if (storedCaipNetwork) { - const parsedCaipNetwork = JSON.parse(storedCaipNetwork) as CaipNetwork - if (parsedCaipNetwork.chainNamespace === 'eip155') { - await this.switchChain?.({ chainId: Number(parsedCaipNetwork.chainId) }) - } + if (storedCaipNetwork && storedCaipNetwork.chainNamespace === 'eip155') { + await this.switchChain?.({ chainId: Number(storedCaipNetwork.chainId) }) } else { await this.switchChain?.({ chainId }) } diff --git a/packages/appkit-utils/src/ethers/EthersConstantsUtil.ts b/packages/appkit-utils/src/ethers/EthersConstantsUtil.ts index f123e4a819..70696b74c3 100644 --- a/packages/appkit-utils/src/ethers/EthersConstantsUtil.ts +++ b/packages/appkit-utils/src/ethers/EthersConstantsUtil.ts @@ -1,5 +1,4 @@ export const EthersConstantsUtil = { - WALLET_ID: '@w3m/wallet_id', ERROR_CODE_UNRECOGNIZED_CHAIN_ID: 4902, ERROR_CODE_DEFAULT: 5000 } as const diff --git a/packages/appkit-utils/src/solana/SolanaConstantsUtil.ts b/packages/appkit-utils/src/solana/SolanaConstantsUtil.ts index a1c2de0d50..3c03554ad3 100644 --- a/packages/appkit-utils/src/solana/SolanaConstantsUtil.ts +++ b/packages/appkit-utils/src/solana/SolanaConstantsUtil.ts @@ -19,8 +19,6 @@ export const SolConstantsUtil = { /** * Mainnet program ID */ - WALLET_ID: '@w3m/solana_wallet', - CAIP_CHAIN_ID: '@w3m/solana_caip_chain', ERROR_CODE_UNRECOGNIZED_CHAIN_ID: 4902, ERROR_CODE_DEFAULT: 5000, DEFAULT_CHAIN: { diff --git a/packages/appkit/src/universal-adapter/client.ts b/packages/appkit/src/universal-adapter/client.ts index c8677e567d..d631475f46 100644 --- a/packages/appkit/src/universal-adapter/client.ts +++ b/packages/appkit/src/universal-adapter/client.ts @@ -89,10 +89,6 @@ export class UniversalAdapterClient { // @ts-expect-error switchCaipNetwork is async for some adapter but not for this adapter switchCaipNetwork: caipNetwork => { if (caipNetwork) { - SafeLocalStorage.setItem( - SafeLocalStorageKeys.ACTIVE_CAIP_NETWORK, - JSON.stringify(caipNetwork) - ) try { this.switchNetwork(caipNetwork) } catch (error) { @@ -386,32 +382,23 @@ export class UniversalAdapterClient { }) const storedCaipNetwork = SafeLocalStorage.getItem(SafeLocalStorageKeys.ACTIVE_CAIP_NETWORK) + const activeCaipNetwork = ChainController.state.activeCaipNetwork - if (storedCaipNetwork) { - try { - const parsedCaipNetwork = JSON.parse(storedCaipNetwork) as CaipNetwork - if (parsedCaipNetwork) { - NetworkController.setActiveCaipNetwork(parsedCaipNetwork) - } - } catch (error) { - console.warn('>>> Error setting active caip network', error) + try { + if (storedCaipNetwork) { + NetworkController.setActiveCaipNetwork(storedCaipNetwork) + } else if (!activeCaipNetwork) { + this.setDefaultNetwork(nameSpaces) + } else if ( + !NetworkController.state.approvedCaipNetworkIds?.includes(activeCaipNetwork.id) + ) { + this.setDefaultNetwork(nameSpaces) } - } else if (!ChainController.state.activeCaipNetwork) { - this.setDefaultNetwork(nameSpaces) - } else if ( - !NetworkController.state.approvedCaipNetworkIds?.includes( - ChainController.state.activeCaipNetwork.id - ) - ) { - this.setDefaultNetwork(nameSpaces) + } catch (error) { + console.warn('>>> Error setting active caip network', error) } } - SafeLocalStorage.setItem( - SafeLocalStorageKeys.ACTIVE_CAIP_NETWORK, - JSON.stringify(this.appKit?.getCaipNetwork()) - ) - this.syncAccount() this.watchWalletConnect() } diff --git a/packages/appkit/src/utils/ConstantsUtil.ts b/packages/appkit/src/utils/ConstantsUtil.ts index 39773b1535..3ff57eb05b 100644 --- a/packages/appkit/src/utils/ConstantsUtil.ts +++ b/packages/appkit/src/utils/ConstantsUtil.ts @@ -1,7 +1,4 @@ export const WcConstantsUtil = { - WALLET_ID: '@w3m/wallet_id' as const, - WALLET_NAME: '@w3m/wallet_name' as const, - ACTIVE_CAIPNETWORK: '@w3m/active_caipnetwork' as const, ERROR_CODE_UNRECOGNIZED_CHAIN_ID: 4902, ERROR_CODE_DEFAULT: 5000 } diff --git a/packages/common/src/utils/SafeLocalStorage.ts b/packages/common/src/utils/SafeLocalStorage.ts index 107003d96b..56f2697ffd 100644 --- a/packages/common/src/utils/SafeLocalStorage.ts +++ b/packages/common/src/utils/SafeLocalStorage.ts @@ -1,21 +1,31 @@ +import type { CaipNetwork } from './TypeUtil.js' + export type SafeLocalStorageItems = { - '@w3m/wallet_id': string - '@w3m/wallet_name': string - '@w3m/solana_wallet': string - '@w3m/solana_caip_chain': string - '@w3m/active_caip_network': string - '@w3m/active_caip_network_id': string - '@w3m/connected_connector': string + '@appkit/wallet_id': string + '@appkit/wallet_name': string + '@appkit/solana_wallet': string + '@appkit/solana_caip_chain': string + '@appkit/active_caip_network': CaipNetwork + '@appkit/active_caip_network_id': string + '@appkit/connected_connector': string + '@appkit/connected_social': string + '@appkit/connected_social_username': string + '@appkit/recent_wallets': string + '@appkit/deeplink_choice': { href: string; name: string } } export const SafeLocalStorageKeys = { - WALLET_ID: '@w3m/wallet_id', - WALLET_NAME: '@w3m/wallet_name', - SOLANA_WALLET: '@w3m/solana_wallet', - SOLANA_CAIP_CHAIN: '@w3m/solana_caip_chain', - ACTIVE_CAIP_NETWORK: '@w3m/active_caip_network', - ACTIVE_CAIP_NETWORK_ID: '@w3m/active_caip_network_id', - CONNECTED_CONNECTOR: '@w3m/connected_connector' + WALLET_ID: '@appkit/wallet_id', + WALLET_NAME: '@appkit/wallet_name', + SOLANA_WALLET: '@appkit/solana_wallet', + SOLANA_CAIP_CHAIN: '@appkit/solana_caip_chain', + ACTIVE_CAIP_NETWORK: '@appkit/active_caip_network', + ACTIVE_CAIP_NETWORK_ID: '@appkit/active_caip_network_id', + CONNECTED_CONNECTOR: '@appkit/connected_connector', + CONNECTED_SOCIAL: '@appkit/connected_social', + CONNECTED_SOCIAL_USERNAME: '@appkit/connected_social_username', + RECENT_WALLETS: '@appkit/recent_wallets', + DEEPLINK_CHOICE: '@appkit/deeplink_choice' } as const export const SafeLocalStorage = { @@ -27,25 +37,34 @@ export const SafeLocalStorage = { localStorage.setItem(key, JSON.stringify(value)) } }, - getItem(key: Key): SafeLocalStorageItems[Key] | null { + getItem( + key: Key + ): SafeLocalStorageItems[Key] | undefined { if (isSafe()) { const value = localStorage.getItem(key) if (value) { try { return JSON.parse(value) - } catch { - return value + } catch (e) { + console.warn('Error parsing value from localStorage', key, e) + + return undefined } } } - return null + return undefined }, removeItem(key: Key): void { if (isSafe()) { localStorage.removeItem(key) } + }, + clear(): void { + if (isSafe()) { + localStorage.clear() + } } } diff --git a/packages/common/tests/SafeLocalStorage.test.ts b/packages/common/tests/SafeLocalStorage.test.ts index dfabb7e131..c9e2f574dd 100644 --- a/packages/common/tests/SafeLocalStorage.test.ts +++ b/packages/common/tests/SafeLocalStorage.test.ts @@ -15,10 +15,10 @@ describe('SafeLocalStorage unsafe', () => { }) it('should not setItem', () => { - const key = '@w3m/wallet_id' + const key = '@appkit/wallet_id' expect(SafeLocalStorage.setItem(key, '1')).toBe(undefined) - expect(SafeLocalStorage.getItem(key)).toBe(null) + expect(SafeLocalStorage.getItem(key)).toBe(undefined) expect(SafeLocalStorage.removeItem(key)).toBe(undefined) }) }) @@ -32,18 +32,31 @@ describe('SafeLocalStorage safe', () => { Object.assign(globalThis, { window: {}, localStorage: { getItem, setItem, removeItem } }) }) + afterAll(() => { + getItem.mockClear() + setItem.mockClear() + removeItem.mockClear() + }) + it('should setItem', () => { - expect(SafeLocalStorage.setItem('@w3m/wallet_id', 'test')).toBe(undefined) - expect(setItem).toHaveBeenCalledWith('@w3m/wallet_id', '"test"') + expect(SafeLocalStorage.setItem('@appkit/wallet_id', 'test')).toBe(undefined) + expect(setItem).toHaveBeenCalledWith('@appkit/wallet_id', JSON.stringify('test')) }) - it('should getItem', () => { - expect(SafeLocalStorage.getItem('@w3m/wallet_id')).toEqual({ test: 'test' }) - expect(getItem).toHaveBeenCalledWith('@w3m/wallet_id') + it('should getItem ', () => { + expect(SafeLocalStorage.getItem('@appkit/wallet_id')).toEqual({ test: 'test' }) + expect(getItem).toHaveBeenCalledWith('@appkit/wallet_id') }) it('should removeItem', () => { - expect(SafeLocalStorage.removeItem('@w3m/wallet_id')).toBe(undefined) - expect(removeItem).toHaveBeenCalledWith('@w3m/wallet_id') + expect(SafeLocalStorage.removeItem('@appkit/wallet_id')).toBe(undefined) + expect(removeItem).toHaveBeenCalledWith('@appkit/wallet_id') + }) + + it('getItem should return undefined when value is not valid JSON', () => { + getItem.mockReturnValueOnce('test') + + expect(SafeLocalStorage.getItem('@appkit/wallet_id')).toBe(undefined) + expect(getItem).toHaveBeenCalledWith('@appkit/wallet_id') }) }) diff --git a/packages/core/src/controllers/ChainController.ts b/packages/core/src/controllers/ChainController.ts index a344f7e241..057cae7b49 100644 --- a/packages/core/src/controllers/ChainController.ts +++ b/packages/core/src/controllers/ChainController.ts @@ -131,20 +131,17 @@ export const ChainController = { if (adapters.length === 0) { const storedCaipNetwork = SafeLocalStorage.getItem(SafeLocalStorageKeys.ACTIVE_CAIP_NETWORK) - if (storedCaipNetwork) { - try { - const parsedCaipNetwork = JSON.parse(storedCaipNetwork) as CaipNetwork - if (parsedCaipNetwork) { - state.activeChain = parsedCaipNetwork.chainNamespace - this.setActiveCaipNetwork(parsedCaipNetwork) - } - } catch (error) { - console.warn('>>> Error setting active caip network', error) + try { + if (storedCaipNetwork) { + state.activeChain = storedCaipNetwork.chainNamespace + this.setActiveCaipNetwork(storedCaipNetwork) + } else { + state.activeChain = + adapter?.defaultNetwork?.chainNamespace ?? adapter.caipNetworks[0]?.chainNamespace + this.setActiveCaipNetwork(adapter?.defaultNetwork ?? adapter.caipNetworks[0]) } - } else { - state.activeChain = - adapter?.defaultNetwork?.chainNamespace ?? adapter.caipNetworks[0]?.chainNamespace - this.setActiveCaipNetwork(adapter?.defaultNetwork ?? adapter.caipNetworks[0]) + } catch (error) { + console.warn('>>> Error setting active caip network', error) } } @@ -262,6 +259,8 @@ export const ChainController = { if (caipNetwork.chainNamespace !== state.activeChain) { this.setActiveChain(caipNetwork.chainNamespace, caipNetwork) + SafeLocalStorage.setItem(SafeLocalStorageKeys.ACTIVE_CAIP_NETWORK, caipNetwork) + SafeLocalStorage.setItem(SafeLocalStorageKeys.ACTIVE_CAIP_NETWORK_ID, caipNetwork.id) return } @@ -273,7 +272,7 @@ export const ChainController = { selectedNetworkId: caipNetwork?.id }) - SafeLocalStorage.setItem(SafeLocalStorageKeys.ACTIVE_CAIP_NETWORK, JSON.stringify(caipNetwork)) + SafeLocalStorage.setItem(SafeLocalStorageKeys.ACTIVE_CAIP_NETWORK, caipNetwork) SafeLocalStorage.setItem(SafeLocalStorageKeys.ACTIVE_CAIP_NETWORK_ID, caipNetwork.id) }, @@ -306,9 +305,8 @@ export const ChainController = { const chain = state.activeChain const isWcConnector = walletId === 'walletConnect' const universalNetworkControllerClient = state.universalAdapter.networkControllerClient - const hasWagmiAdapter = state.chains.get('eip155')?.adapterType === 'wagmi' - const shouldUseUniversalAdapter = (isWcConnector && !hasWagmiAdapter) || state.noAdapters + const shouldUseUniversalAdapter = isWcConnector || state.noAdapters if (shouldUseUniversalAdapter) { if (!universalNetworkControllerClient) { diff --git a/packages/core/src/controllers/NetworkController.ts b/packages/core/src/controllers/NetworkController.ts index 8db276fbe2..4a2e93583e 100644 --- a/packages/core/src/controllers/NetworkController.ts +++ b/packages/core/src/controllers/NetworkController.ts @@ -4,8 +4,6 @@ import { ModalController } from './ModalController.js' import { CoreHelperUtil } from '../utils/CoreHelperUtil.js' import { NetworkUtil, - SafeLocalStorage, - SafeLocalStorageKeys, type CaipNetwork, type CaipNetworkId, type ChainNamespace @@ -185,29 +183,7 @@ export const NetworkController = { }, async switchActiveNetwork(network: NetworkControllerState['caipNetwork']) { - const sameNamespace = network?.chainNamespace === ChainController.state.activeChain - - let networkControllerClient: NetworkControllerState['_client'] = undefined - const isWcConnector = - SafeLocalStorage.getItem(SafeLocalStorageKeys.WALLET_ID) === 'walletConnect' - const hasWagmiAdapter = ChainController.state.chains.get('eip155')?.adapterType === 'wagmi' - - if (isWcConnector && network?.chainNamespace === 'solana') { - if (hasWagmiAdapter) { - networkControllerClient = ChainController.state.chains.get(network.chainNamespace) - ?.networkControllerClient - } else { - networkControllerClient = ChainController.state.universalAdapter.networkControllerClient - } - } else if (isWcConnector && !hasWagmiAdapter) { - networkControllerClient = ChainController.state.universalAdapter.networkControllerClient - } else if (sameNamespace) { - networkControllerClient = ChainController.getNetworkControllerClient() - } else { - networkControllerClient = network - ? ChainController.state.chains.get(network.chainNamespace)?.networkControllerClient - : undefined - } + const networkControllerClient = ChainController.getNetworkControllerClient() await networkControllerClient?.switchCaipNetwork(network) ChainController.setActiveCaipNetwork(network) diff --git a/packages/core/src/utils/StorageUtil.ts b/packages/core/src/utils/StorageUtil.ts index 964c299c56..f1b9ff8dd6 100644 --- a/packages/core/src/utils/StorageUtil.ts +++ b/packages/core/src/utils/StorageUtil.ts @@ -1,18 +1,12 @@ /* eslint-disable no-console */ +import { SafeLocalStorage, SafeLocalStorageKeys } from '@reown/appkit-common' import type { WcWallet, ConnectorType, SocialProvider } from './TypeUtil.js' -// -- Helpers ----------------------------------------------------------------- -const WC_DEEPLINK = 'WALLETCONNECT_DEEPLINK_CHOICE' -const W3M_RECENT = '@w3m/recent' -const W3M_CONNECTED_CONNECTOR = '@w3m/connected_connector' -const W3M_CONNECTED_SOCIAL = '@w3m/connected_social' -const W3M_CONNECTED_SOCIAL_USERNAME = '@w3m-storage/SOCIAL_USERNAME' - // -- Utility ----------------------------------------------------------------- export const StorageUtil = { setWalletConnectDeepLink({ href, name }: { href: string; name: string }) { try { - localStorage.setItem(WC_DEEPLINK, JSON.stringify({ href, name })) + SafeLocalStorage.setItem(SafeLocalStorageKeys.DEEPLINK_CHOICE, { href, name }) } catch { console.info('Unable to set WalletConnect deep link') } @@ -20,9 +14,9 @@ export const StorageUtil = { getWalletConnectDeepLink() { try { - const deepLink = localStorage.getItem(WC_DEEPLINK) + const deepLink = SafeLocalStorage.getItem(SafeLocalStorageKeys.DEEPLINK_CHOICE) if (deepLink) { - return JSON.parse(deepLink) + return deepLink } } catch { console.info('Unable to get WalletConnect deep link') @@ -33,7 +27,7 @@ export const StorageUtil = { deleteWalletConnectDeepLink() { try { - localStorage.removeItem(WC_DEEPLINK) + SafeLocalStorage.removeItem(SafeLocalStorageKeys.DEEPLINK_CHOICE) } catch { console.info('Unable to delete WalletConnect deep link') } @@ -48,7 +42,7 @@ export const StorageUtil = { if (recentWallets.length > 2) { recentWallets.pop() } - localStorage.setItem(W3M_RECENT, JSON.stringify(recentWallets)) + SafeLocalStorage.setItem(SafeLocalStorageKeys.RECENT_WALLETS, JSON.stringify(recentWallets)) } } catch { console.info('Unable to set AppKit recent') @@ -57,7 +51,7 @@ export const StorageUtil = { getRecentWallets(): WcWallet[] { try { - const recent = localStorage.getItem(W3M_RECENT) + const recent = SafeLocalStorage.getItem(SafeLocalStorageKeys.RECENT_WALLETS) return recent ? JSON.parse(recent) : [] } catch { @@ -69,7 +63,7 @@ export const StorageUtil = { setConnectedConnector(connectorType: ConnectorType) { try { - localStorage.setItem(W3M_CONNECTED_CONNECTOR, connectorType) + SafeLocalStorage.setItem(SafeLocalStorageKeys.CONNECTED_CONNECTOR, connectorType) } catch { console.info('Unable to set Connected Connector') } @@ -77,7 +71,7 @@ export const StorageUtil = { getConnectedConnector() { try { - return localStorage.getItem(W3M_CONNECTED_CONNECTOR) as ConnectorType + return SafeLocalStorage.getItem(SafeLocalStorageKeys.CONNECTED_CONNECTOR) as ConnectorType } catch { console.info('Unable to get Connected Connector') } @@ -87,7 +81,7 @@ export const StorageUtil = { setConnectedSocialProvider(socialProvider: SocialProvider) { try { - localStorage.setItem(W3M_CONNECTED_SOCIAL, socialProvider) + SafeLocalStorage.setItem(SafeLocalStorageKeys.CONNECTED_SOCIAL, socialProvider) } catch { console.info('Unable to set Connected Social Provider') } @@ -95,7 +89,7 @@ export const StorageUtil = { getConnectedSocialProvider() { try { - return localStorage.getItem(W3M_CONNECTED_SOCIAL) + return SafeLocalStorage.getItem(SafeLocalStorageKeys.CONNECTED_SOCIAL) } catch { console.info('Unable to get Connected Social Provider') } @@ -105,7 +99,7 @@ export const StorageUtil = { getConnectedSocialUsername() { try { - return localStorage.getItem(W3M_CONNECTED_SOCIAL_USERNAME) + return SafeLocalStorage.getItem(SafeLocalStorageKeys.CONNECTED_SOCIAL_USERNAME) } catch { console.info('Unable to get Connected Social Username') } diff --git a/packages/core/tests/utils/StorageUtil.test.ts b/packages/core/tests/utils/StorageUtil.test.ts index 8f2b810d45..5095c69998 100644 --- a/packages/core/tests/utils/StorageUtil.test.ts +++ b/packages/core/tests/utils/StorageUtil.test.ts @@ -1,30 +1,39 @@ -import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest' +import { describe, it, expect, vi, afterEach, beforeEach, beforeAll, afterAll } from 'vitest' import { StorageUtil } from '../../src/utils/StorageUtil' import type { WcWallet, ConnectorType, SocialProvider } from '../../src/utils/TypeUtil' +import { SafeLocalStorage } from '@reown/appkit-common' +import { SafeLocalStorageKeys } from '@reown/appkit-common' -// Mock localStorage -const localStorageMock = (() => { - let store: { [key: string]: string } = {} - return { - getItem: (key: string) => store[key] || null, - setItem: (key: string, value: string) => { - store[key] = value.toString() - }, - removeItem: (key: string) => { - delete store[key] - }, - clear: () => { - store = {} - } - } -})() - -Object.defineProperty(global, 'localStorage', { value: localStorageMock }) +const previousLocalStorage = globalThis.localStorage +const previousWindow = globalThis.window +let store: { [key: string]: string } = {} + +afterAll(() => { + Object.assign(globalThis, { localStorage: previousLocalStorage, window: previousWindow }) +}) describe('StorageUtil', () => { + beforeAll(() => { + Object.assign(globalThis, { + window: {}, + localStorage: { + getItem: (key: string) => store[key] || null, + setItem: (key: string, value: string) => { + store[key] = value.toString() + }, + removeItem: (key: string) => { + delete store[key] + }, + clear: () => { + store = {} + } + } + }) + }) + beforeEach(() => { // Clear localStorage before each test - localStorage.clear() + SafeLocalStorage.clear() }) afterEach(() => { @@ -36,7 +45,9 @@ describe('StorageUtil', () => { it('should set WalletConnect deep link in localStorage', () => { const deepLink = { href: 'https://example.com', name: 'Example Wallet' } StorageUtil.setWalletConnectDeepLink(deepLink) - expect(localStorage.getItem('WALLETCONNECT_DEEPLINK_CHOICE')).toBe(JSON.stringify(deepLink)) + const savedDL = SafeLocalStorage.getItem(SafeLocalStorageKeys.DEEPLINK_CHOICE) + expect(savedDL?.href).toBe(deepLink.href) + expect(savedDL?.name).toBe(deepLink.name) }) it('should handle errors when setting deep link', () => { @@ -53,7 +64,7 @@ describe('StorageUtil', () => { describe('getWalletConnectDeepLink', () => { it('should get WalletConnect deep link from localStorage', () => { const deepLink = { href: 'https://example.com', name: 'Example Wallet' } - localStorage.setItem('WALLETCONNECT_DEEPLINK_CHOICE', JSON.stringify(deepLink)) + SafeLocalStorage.setItem('@appkit/deeplink_choice', deepLink) expect(StorageUtil.getWalletConnectDeepLink()).toEqual(deepLink) }) @@ -74,12 +85,12 @@ describe('StorageUtil', () => { describe('deleteWalletConnectDeepLink', () => { it('should delete WalletConnect deep link from localStorage', () => { - localStorage.setItem( - 'WALLETCONNECT_DEEPLINK_CHOICE', - JSON.stringify({ href: 'https://example.com', name: 'Example Wallet' }) - ) + SafeLocalStorage.setItem('@appkit/deeplink_choice', { + href: 'https://example.com', + name: 'Example Wallet' + }) StorageUtil.deleteWalletConnectDeepLink() - expect(localStorage.getItem('WALLETCONNECT_DEEPLINK_CHOICE')).toBeNull() + expect(SafeLocalStorage.getItem('@appkit/deeplink_choice')).toBeUndefined() }) it('should handle errors when deleting deep link', () => { @@ -125,7 +136,7 @@ describe('StorageUtil', () => { it('should return recent wallets', () => { const wallet: WcWallet = { id: 'wallet1', name: 'Wallet 1' } - localStorage.setItem('@w3m/recent', JSON.stringify([wallet])) + SafeLocalStorage.setItem(SafeLocalStorageKeys.RECENT_WALLETS, JSON.stringify([wallet])) expect(StorageUtil.getRecentWallets()).toEqual([wallet]) }) }) @@ -134,14 +145,14 @@ describe('StorageUtil', () => { it('should set connected connector', () => { const connector: ConnectorType = 'INJECTED' StorageUtil.setConnectedConnector(connector) - expect(localStorage.getItem('@w3m/connected_connector')).toBe(connector) + expect(SafeLocalStorage.getItem(SafeLocalStorageKeys.CONNECTED_CONNECTOR)).toBe(connector) }) }) describe('getConnectedConnector', () => { it('should get connected connector', () => { const connector: ConnectorType = 'INJECTED' - localStorage.setItem('@w3m/connected_connector', connector) + SafeLocalStorage.setItem(SafeLocalStorageKeys.CONNECTED_CONNECTOR, connector) expect(StorageUtil.getConnectedConnector()).toBe(connector) }) }) @@ -150,14 +161,14 @@ describe('StorageUtil', () => { it('should set connected social provider', () => { const provider: SocialProvider = 'google' StorageUtil.setConnectedSocialProvider(provider) - expect(localStorage.getItem('@w3m/connected_social')).toBe(provider) + expect(SafeLocalStorage.getItem(SafeLocalStorageKeys.CONNECTED_SOCIAL)).toBe(provider) }) }) describe('getConnectedSocialProvider', () => { it('should get connected social provider', () => { const provider: SocialProvider = 'google' - localStorage.setItem('@w3m/connected_social', provider) + SafeLocalStorage.setItem(SafeLocalStorageKeys.CONNECTED_SOCIAL, provider) expect(StorageUtil.getConnectedSocialProvider()).toBe(provider) }) }) @@ -165,7 +176,7 @@ describe('StorageUtil', () => { describe('getConnectedSocialUsername', () => { it('should get connected social username', () => { const username = 'testuser' - localStorage.setItem('@w3m-storage/SOCIAL_USERNAME', username) + SafeLocalStorage.setItem(SafeLocalStorageKeys.CONNECTED_SOCIAL_USERNAME, username) expect(StorageUtil.getConnectedSocialUsername()).toBe(username) }) }) diff --git a/packages/wallet/src/W3mFrameConstants.ts b/packages/wallet/src/W3mFrameConstants.ts index 6d83e2d4a1..02761eb121 100644 --- a/packages/wallet/src/W3mFrameConstants.ts +++ b/packages/wallet/src/W3mFrameConstants.ts @@ -7,7 +7,7 @@ export const W3mFrameConstants = { APP_EVENT_KEY: '@w3m-app/', FRAME_EVENT_KEY: '@w3m-frame/', RPC_METHOD_KEY: 'RPC_', - STORAGE_KEY: '@w3m-storage/', + STORAGE_KEY: '@appkit-wallet/', SESSION_TOKEN_KEY: 'SESSION_TOKEN_KEY', EMAIL_LOGIN_USED_KEY: 'EMAIL_LOGIN_USED_KEY', @@ -18,7 +18,6 @@ export const W3mFrameConstants = { SMART_ACCOUNT_ENABLED: 'SMART_ACCOUNT_ENABLED', SMART_ACCOUNT_ENABLED_NETWORKS: 'SMART_ACCOUNT_ENABLED_NETWORKS', SOCIAL_USERNAME: 'SOCIAL_USERNAME', - SOCIAL: '@w3m/connected_social', APP_SWITCH_NETWORK: '@w3m-app/SWITCH_NETWORK', APP_CONNECT_EMAIL: '@w3m-app/CONNECT_EMAIL', diff --git a/packages/wallet/src/W3mFrameProvider.ts b/packages/wallet/src/W3mFrameProvider.ts index 1c994d3107..ef1c39c268 100644 --- a/packages/wallet/src/W3mFrameProvider.ts +++ b/packages/wallet/src/W3mFrameProvider.ts @@ -472,7 +472,13 @@ export class W3mFrameProvider { } }) - function handler(framEvent: W3mFrameTypes.FrameEvent) { + function handler(framEvent: W3mFrameTypes.FrameEvent, logger: W3mFrameLogger) { + if (framEvent.id !== id) { + return + } + + logger.logger.info?.({ framEvent, id }, 'Received frame response') + if (framEvent.type === `@w3m-frame/${type}_SUCCESS`) { if ('payload' in framEvent) { resolve(framEvent.payload) @@ -485,7 +491,11 @@ export class W3mFrameProvider { reject(new Error('An error occurred')) } } - this.w3mFrame.events.registerFrameEventHandler(id, handler, abortController.signal) + this.w3mFrame.events.registerFrameEventHandler( + id, + frameEvent => handler(frameEvent, this.w3mLogger), + abortController.signal + ) }) } @@ -511,7 +521,6 @@ export class W3mFrameProvider { W3mFrameStorage.delete(W3mFrameConstants.EMAIL) W3mFrameStorage.delete(W3mFrameConstants.LAST_USED_CHAIN_KEY) W3mFrameStorage.delete(W3mFrameConstants.SOCIAL_USERNAME) - W3mFrameStorage.delete(W3mFrameConstants.SOCIAL, true) } private setLastUsedChainId(chainId: string | number) {