From babb4133ddff939061b788c3115b9d988b6e3ce2 Mon Sep 17 00:00:00 2001 From: Felipe Mendes Date: Thu, 19 Sep 2024 15:49:38 -0300 Subject: [PATCH] feat: add CoinbaseWalletProvider for solana (#2879) Co-authored-by: tomiir --- .changeset/small-parents-judge.md | 38 ++++++ packages/adapters/solana/src/client.ts | 34 ++++-- .../src/providers/CoinbaseWalletProvider.ts | 115 ++++++++++++++++++ .../solana/src/tests/GenericProvider.test.ts | 10 ++ .../solana/src/tests/mocks/CoinbaseWallet.ts | 16 +++ 5 files changed, 206 insertions(+), 7 deletions(-) create mode 100644 .changeset/small-parents-judge.md create mode 100644 packages/adapters/solana/src/providers/CoinbaseWalletProvider.ts create mode 100644 packages/adapters/solana/src/tests/mocks/CoinbaseWallet.ts 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/packages/adapters/solana/src/client.ts b/packages/adapters/solana/src/client.ts index 3a88327966..eeb3dadbcf 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, PresetsUtil } from '@reown/appkit-utils' import { createSendTransaction } from './utils/createSendTransaction.js' +import { CoinbaseWalletProvider } from './providers/CoinbaseWalletProvider.js' export interface AdapterOptions { connectionSettings?: Commitment | ConnectionConfig @@ -341,15 +342,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}` + } } } }) @@ -644,6 +653,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/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() + } +}