Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
chore: replace wagmi
coinbaseWallet
with custom connector (#1945)
* chore: coinbase connector * chore: changeset * chore: tweak * chore: force headless mode * chore: headless mode true * chore: add comment for connector source * revert: headlessMode change * chore: amend changeset * chore: revert to @coinbase/wallet-sdk v3.6.6 * chore: upgrade @coinbase/wallet-sdk to v3.7.2 * chore: trial @coinbase/wallet-sdk v3.9.3 --------- Co-authored-by: Daniel Sinclair <d@niel.nyc>
- Loading branch information
1 parent
7ab6e50
commit 515498f
Showing
7 changed files
with
317 additions
and
2 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,5 @@ | ||
--- | ||
"@rainbow-me/rainbowkit": patch | ||
--- | ||
|
||
Locked the dependencies for the `coinbaseWallet` wallet connector to Coinbase Wallet SDK v3 to temporarily mitigate breaking changes included an upcoming version of Wagmi. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,19 @@ | ||
import { describe, expect, test } from 'vitest'; | ||
import { http, createConfig } from 'wagmi'; | ||
import { mainnet } from 'wagmi/chains'; | ||
import { coinbaseWallet } from './coinbaseWallet.js'; | ||
|
||
const config = createConfig({ | ||
chains: [mainnet], | ||
transports: { | ||
[mainnet.id]: http(), | ||
}, | ||
}); | ||
|
||
describe('coinbaseWallet', () => { | ||
test('setup', () => { | ||
const connectorFn = coinbaseWallet({ appName: 'wagmi' }); | ||
const connector = config._internal.connectors.setup(connectorFn); | ||
expect(connector.name).toEqual('Coinbase Wallet'); | ||
}); | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,254 @@ | ||
import { | ||
type CoinbaseWalletProvider, | ||
type CoinbaseWalletSDK, | ||
} from '@coinbase/wallet-sdk'; | ||
|
||
import { | ||
type ProviderRpcError, | ||
SwitchChainError, | ||
UserRejectedRequestError, | ||
getAddress, | ||
numberToHex, | ||
} from 'viem'; | ||
import { ChainNotConfiguredError, Connector, createConnector } from 'wagmi'; | ||
import { Evaluate, Mutable, Omit } from '../types/utils'; | ||
|
||
// Borrowed from wagmi@2.5.22 | ||
// https://github.com/wevm/wagmi/blob/72a25ee00a7b6b1b41c1a4825d08440a852f9057/packages/connectors/src/coinbaseWallet.ts#L20 | ||
|
||
// TODO(@3): Set `enableMobileWalletLink` to `true` | ||
export type CoinbaseWalletParameters = Evaluate< | ||
Mutable< | ||
Omit< | ||
ConstructorParameters<typeof CoinbaseWalletSDK>[0], | ||
'reloadOnDisconnect' // remove property since TSDoc says default is `true` | ||
> | ||
> & { | ||
/** | ||
* Fallback Ethereum JSON RPC URL | ||
* @default "" | ||
*/ | ||
jsonRpcUrl?: string | undefined; | ||
/** | ||
* Fallback Ethereum Chain ID | ||
* @default 1 | ||
*/ | ||
chainId?: number | undefined; | ||
/** | ||
* Whether or not to reload dapp automatically after disconnect. | ||
* @default false | ||
*/ | ||
reloadOnDisconnect?: boolean | undefined; | ||
} | ||
>; | ||
|
||
coinbaseWallet.type = 'coinbaseWallet' as const; | ||
export function coinbaseWallet(parameters: CoinbaseWalletParameters) { | ||
const reloadOnDisconnect = false; | ||
|
||
type Provider = CoinbaseWalletProvider; | ||
type Properties = {}; | ||
|
||
let sdk: CoinbaseWalletSDK | undefined; | ||
let walletProvider: Provider | undefined; | ||
|
||
let accountsChanged: Connector['onAccountsChanged'] | undefined; | ||
let chainChanged: Connector['onChainChanged'] | undefined; | ||
let disconnect: Connector['onDisconnect'] | undefined; | ||
|
||
return createConnector<Provider, Properties>((config) => ({ | ||
id: 'coinbaseWalletSDK', | ||
name: 'Coinbase Wallet', | ||
type: coinbaseWallet.type, | ||
async connect({ chainId } = {}) { | ||
try { | ||
const provider = await this.getProvider(); | ||
const accounts = ( | ||
(await provider.request({ | ||
method: 'eth_requestAccounts', | ||
})) as string[] | ||
).map((x) => getAddress(x)); | ||
|
||
if (!accountsChanged) { | ||
accountsChanged = this.onAccountsChanged.bind(this); | ||
provider.on('accountsChanged', accountsChanged); | ||
} | ||
if (!chainChanged) { | ||
chainChanged = this.onChainChanged.bind(this); | ||
provider.on('chainChanged', chainChanged); | ||
} | ||
if (!disconnect) { | ||
disconnect = this.onDisconnect.bind(this); | ||
provider.on('disconnect', disconnect); | ||
} | ||
|
||
// Switch to chain if provided | ||
let currentChainId = await this.getChainId(); | ||
if (chainId && currentChainId !== chainId) { | ||
const chain = await this.switchChain!({ chainId }).catch((error) => { | ||
if (error.code === UserRejectedRequestError.code) throw error; | ||
return { id: currentChainId }; | ||
}); | ||
currentChainId = chain?.id ?? currentChainId; | ||
} | ||
|
||
return { accounts, chainId: currentChainId }; | ||
} catch (error) { | ||
if ( | ||
/(user closed modal|accounts received is empty|user denied account)/i.test( | ||
(error as Error).message, | ||
) | ||
) | ||
throw new UserRejectedRequestError(error as Error); | ||
throw error; | ||
} | ||
}, | ||
async disconnect() { | ||
const provider = await this.getProvider(); | ||
|
||
if (accountsChanged) { | ||
provider.removeListener('accountsChanged', accountsChanged); | ||
accountsChanged = undefined; | ||
} | ||
if (chainChanged) { | ||
provider.removeListener('chainChanged', chainChanged); | ||
chainChanged = undefined; | ||
} | ||
if (disconnect) { | ||
provider.removeListener('disconnect', disconnect); | ||
disconnect = undefined; | ||
} | ||
|
||
provider.disconnect(); | ||
provider.close(); | ||
}, | ||
async getAccounts() { | ||
const provider = await this.getProvider(); | ||
return ( | ||
await provider.request<string[]>({ | ||
method: 'eth_accounts', | ||
}) | ||
).map((x) => getAddress(x)); | ||
}, | ||
async getChainId() { | ||
const provider = await this.getProvider(); | ||
const chainId = await provider.request<number>({ method: 'eth_chainId' }); | ||
return Number(chainId); | ||
}, | ||
async getProvider() { | ||
if (!walletProvider) { | ||
const { default: CoinbaseWalletSDK } = await import( | ||
'@coinbase/wallet-sdk' | ||
); | ||
let SDK: typeof CoinbaseWalletSDK; | ||
if ( | ||
typeof CoinbaseWalletSDK !== 'function' && | ||
typeof (CoinbaseWalletSDK as { default: typeof CoinbaseWalletSDK }) | ||
.default === 'function' | ||
) { | ||
SDK = (CoinbaseWalletSDK as { default: typeof CoinbaseWalletSDK }) | ||
.default; | ||
} else { | ||
SDK = CoinbaseWalletSDK as unknown as typeof CoinbaseWalletSDK; | ||
} | ||
|
||
sdk = new SDK({ reloadOnDisconnect, ...parameters }); | ||
|
||
// Force types to retrieve private `walletExtension` method from the Coinbase Wallet SDK. | ||
const walletExtensionChainId = ( | ||
sdk as unknown as { | ||
get walletExtension(): { getChainId(): number } | undefined; | ||
} | ||
).walletExtension?.getChainId(); | ||
|
||
const chain = | ||
config.chains.find((chain) => | ||
parameters.chainId | ||
? chain.id === parameters.chainId | ||
: chain.id === walletExtensionChainId, | ||
) || config.chains[0]; | ||
const chainId = parameters.chainId || chain?.id; | ||
const jsonRpcUrl = | ||
parameters.jsonRpcUrl || chain?.rpcUrls.default.http[0]; | ||
|
||
walletProvider = sdk.makeWeb3Provider(jsonRpcUrl, chainId); | ||
} | ||
|
||
return walletProvider; | ||
}, | ||
async isAuthorized() { | ||
try { | ||
const accounts = await this.getAccounts(); | ||
return !!accounts.length; | ||
} catch { | ||
return false; | ||
} | ||
}, | ||
async switchChain({ chainId }) { | ||
const chain = config.chains.find((chain) => chain.id === chainId); | ||
if (!chain) throw new SwitchChainError(new ChainNotConfiguredError()); | ||
|
||
const provider = await this.getProvider(); | ||
const chainId_ = numberToHex(chain.id); | ||
|
||
try { | ||
await provider.request({ | ||
method: 'wallet_switchEthereumChain', | ||
params: [{ chainId: chainId_ }], | ||
}); | ||
return chain; | ||
} catch (error) { | ||
// Indicates chain is not added to provider | ||
if ((error as ProviderRpcError).code === 4902) { | ||
try { | ||
await provider.request({ | ||
method: 'wallet_addEthereumChain', | ||
params: [ | ||
{ | ||
chainId: chainId_, | ||
chainName: chain.name, | ||
nativeCurrency: chain.nativeCurrency, | ||
rpcUrls: [chain.rpcUrls.default?.http[0] ?? ''], | ||
blockExplorerUrls: [chain.blockExplorers?.default.url], | ||
}, | ||
], | ||
}); | ||
return chain; | ||
} catch (error) { | ||
throw new UserRejectedRequestError(error as Error); | ||
} | ||
} | ||
|
||
throw new SwitchChainError(error as Error); | ||
} | ||
}, | ||
onAccountsChanged(accounts) { | ||
if (accounts.length === 0) this.onDisconnect(); | ||
else | ||
config.emitter.emit('change', { | ||
accounts: accounts.map((x) => getAddress(x)), | ||
}); | ||
}, | ||
onChainChanged(chain) { | ||
const chainId = Number(chain); | ||
config.emitter.emit('change', { chainId }); | ||
}, | ||
async onDisconnect(_error) { | ||
config.emitter.emit('disconnect'); | ||
|
||
const provider = await this.getProvider(); | ||
if (accountsChanged) { | ||
provider.removeListener('accountsChanged', accountsChanged); | ||
accountsChanged = undefined; | ||
} | ||
if (chainChanged) { | ||
provider.removeListener('chainChanged', chainChanged); | ||
chainChanged = undefined; | ||
} | ||
if (disconnect) { | ||
provider.removeListener('disconnect', disconnect); | ||
disconnect = undefined; | ||
} | ||
}, | ||
})); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,13 @@ | ||
/** Combines members of an intersection into a readable type. */ | ||
export type Evaluate<type> = { [key in keyof type]: type[key] } & unknown; | ||
|
||
/** Removes `readonly` from all properties of an object. */ | ||
export type Mutable<type extends object> = { | ||
-readonly [key in keyof type]: type[key]; | ||
}; | ||
|
||
/** Strict version of built-in Omit type */ | ||
export type Omit<type, keys extends keyof type> = Pick< | ||
type, | ||
Exclude<keyof type, keys> | ||
>; |
2 changes: 1 addition & 1 deletion
2
packages/rainbowkit/src/wallets/walletConnectors/coinbaseWallet/coinbaseWallet.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Oops, something went wrong.