Skip to content

Commit

Permalink
feat: add CoinbaseWalletProvider for solana (#2879)
Browse files Browse the repository at this point in the history
Co-authored-by: tomiir <[email protected]>
  • Loading branch information
zoruka and tomiir authored Sep 19, 2024
1 parent a632159 commit babb413
Show file tree
Hide file tree
Showing 5 changed files with 206 additions and 7 deletions.
38 changes: 38 additions & 0 deletions .changeset/small-parents-judge.md
Original file line number Diff line number Diff line change
@@ -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
34 changes: 27 additions & 7 deletions packages/adapters/solana/src/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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}`
}
}
}
})
Expand Down Expand Up @@ -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)
Expand Down
115 changes: 115 additions & 0 deletions packages/adapters/solana/src/providers/CoinbaseWalletProvider.ts
Original file line number Diff line number Diff line change
@@ -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<T extends AnyTransaction>(transaction: T): Promise<T>
signAllTransactions<T extends AnyTransaction>(transactions: T[]): Promise<T[]>
signAndSendTransaction<T extends AnyTransaction>(
transaction: T,
options?: SendOptions
): Promise<{ signature: string }>
signMessage(message: Uint8Array): Promise<{ signature: Uint8Array }>
connect(): Promise<void>
disconnect(): Promise<void>
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<T extends AnyTransaction>(transaction: T) {
return this.provider.signTransaction(transaction)
}

public async signAndSendTransaction<T extends AnyTransaction>(
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<T extends AnyTransaction[]>(transactions: T): Promise<T> {
return (await this.provider.signAllTransactions(transactions)) as T
}

private getAccount<Required extends boolean>(
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
}
}
10 changes: 10 additions & 0 deletions packages/adapters/solana/src/tests/GenericProvider.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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])

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

Expand Down
16 changes: 16 additions & 0 deletions packages/adapters/solana/src/tests/mocks/CoinbaseWallet.ts
Original file line number Diff line number Diff line change
@@ -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()
}
}

0 comments on commit babb413

Please sign in to comment.