Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add CoinbaseWalletProvider for solana #2879

Merged
merged 4 commits into from
Sep 19, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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()
}
}
Loading