Skip to content

Commit

Permalink
chore: absorb coinbase connector changes
Browse files Browse the repository at this point in the history
  • Loading branch information
DanielSinclair committed May 8, 2024
1 parent ec1a899 commit e6f9712
Show file tree
Hide file tree
Showing 4 changed files with 249 additions and 25 deletions.
8 changes: 6 additions & 2 deletions package.json
Expand Up @@ -43,10 +43,10 @@
"devDependencies": {
"@biomejs/biome": "1.4.1",
"@changesets/cli": "2.27.1",
"@tanstack/react-query": "^5.28.4",
"@commitlint/cli": "^18.4.3",
"@commitlint/config-conventional": "^18.4.3",
"@lavamoat/preinstall-always-fail": "^2.0.0",
"@tanstack/react-query": "^5.28.4",
"@testing-library/jest-dom": "^6.2.0",
"@testing-library/react": "^14.1.2",
"@testing-library/user-event": "^14.5.2",
Expand All @@ -72,7 +72,8 @@
"typescript": "5.4.2",
"viem": "^2.10.1",
"vitest": "^0.33.0",
"wagmi": "^2.8.1"
"wagmi": "^2.8.1",
"@coinbase/wallet-sdk": "4.0.0-beta.14"
},
"resolutions": {
"@coinbase/wallet-sdk": "4.0.0-beta.14"
Expand All @@ -82,5 +83,8 @@
"onlyBuiltDependencies": [
"esbuild"
]
},
"dependencies": {
"@wagmi/core": "^2.9.1"
}
}
234 changes: 234 additions & 0 deletions packages/rainbowkit/src/connectors/coinbaseWallet.ts
@@ -0,0 +1,234 @@
import {
CoinbaseWalletSDK,
type ProviderInterface,
} from '@coinbase/wallet-sdk';
import {
ChainNotConfiguredError,
type Connector,
createConnector,
} from '@wagmi/core';
import type { Evaluate, Mutable } from '@wagmi/core/internal';
import {
type AddEthereumChainParameter,
type ProviderRpcError,
SwitchChainError,
UserRejectedRequestError,
getAddress,
numberToHex,
} from 'viem';

export type CoinbaseWalletParameters = Evaluate<
Mutable<ConstructorParameters<typeof CoinbaseWalletSDK>[0]> & {
preference?: {
options: 'all' | 'smartWalletOnly' | 'eoaOnly';
};
}
>;

coinbaseWallet.type = 'coinbaseWallet' as const;
export function coinbaseWallet(parameters: CoinbaseWalletParameters) {
type Properties = {};

let sdk: CoinbaseWalletSDK | undefined;
let walletProvider: ProviderInterface | undefined;

let accountsChanged: Connector['onAccountsChanged'] | undefined;
let chainChanged: Connector['onChainChanged'] | undefined;
let disconnect: Connector['onDisconnect'] | undefined;

return createConnector<ProviderInterface, Properties>((config) => ({
id: 'coinbaseWalletSDK',
name: 'Coinbase Wallet',
supportsSimulation: true,
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: number = 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();
},
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({ 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(parameters);

walletProvider = sdk.makeWeb3Provider(
parameters.preference,
) as ProviderInterface;
}

return walletProvider as ProviderInterface;
},
async isAuthorized() {
try {
const accounts = await this.getAccounts();
return !!accounts.length;
} catch {
return false;
}
},
async switchChain({ addEthereumChainParameter, chainId }) {
const chain = config.chains.find((chain) => chain.id === chainId);
if (!chain) throw new SwitchChainError(new ChainNotConfiguredError());

const provider = await this.getProvider();

try {
await provider.request({
method: 'wallet_switchEthereumChain',
params: [{ chainId: numberToHex(chain.id) }],
});
return chain;
} catch (error) {
// Indicates chain is not added to provider
if ((error as ProviderRpcError).code === 4902) {
try {
let blockExplorerUrls;
if (addEthereumChainParameter?.blockExplorerUrls)
blockExplorerUrls = addEthereumChainParameter.blockExplorerUrls;
else
blockExplorerUrls = chain.blockExplorers?.default.url
? [chain.blockExplorers?.default.url]
: [];

let rpcUrls;
if (addEthereumChainParameter?.rpcUrls?.length)
rpcUrls = addEthereumChainParameter.rpcUrls;
else rpcUrls = [chain.rpcUrls.default?.http[0] ?? ''];

const addEthereumChain = {
blockExplorerUrls,
chainId: numberToHex(chainId),
chainName: addEthereumChainParameter?.chainName ?? chain.name,
iconUrls: addEthereumChainParameter?.iconUrls,
nativeCurrency:
addEthereumChainParameter?.nativeCurrency ??
chain.nativeCurrency,
rpcUrls,
} satisfies AddEthereumChainParameter;

await provider.request({
method: 'wallet_addEthereumChain',
params: [addEthereumChain],
});

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;
}
},
}));
}
@@ -1,5 +1,5 @@
import { createConnector } from 'wagmi';
import { coinbaseWallet as coinbaseWagmiWallet } from 'wagmi/connectors';
import { coinbaseWallet as coinbaseConnector } from '../../../connectors/coinbaseWallet';
import { isIOS } from '../../../utils/isMobile';
import { Wallet, WalletDetailsParams } from '../../Wallet';
import { hasInjectedProvider } from '../../getInjectedConnector';
Expand Down Expand Up @@ -100,7 +100,7 @@ export const coinbaseWallet = ({
}),
createConnector: (walletDetails: WalletDetailsParams) =>
createConnector((config) => ({
...coinbaseWagmiWallet({
...coinbaseConnector({
appName,
appLogoUrl: appIcon,
})(config),
Expand Down

0 comments on commit e6f9712

Please sign in to comment.