Skip to content

Commit

Permalink
fix: ensure large lists of WalletConnect wallets work in Brave (#492)
Browse files Browse the repository at this point in the history
  • Loading branch information
markdalgleish committed Jun 15, 2022
1 parent 9b1f866 commit ce473cd
Show file tree
Hide file tree
Showing 16 changed files with 152 additions and 158 deletions.
9 changes: 9 additions & 0 deletions .changeset/lazy-spoons-report.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
---
'@rainbow-me/rainbowkit': patch
---

Fix WalletConnect in Brave when a large number of WalletConnect-based wallets have been configured

Brave’s fingerprint prevention logic silently blocks WebSocket connections if too many are opened in the same session. Since we create a fresh WalletConnect connector instance for each wallet, consumers that have configured a large number of wallets can inadvertently break the connection flow in Brave.

To fix this, we now share WalletConnect connector instances between wallets when the connectors are being provided with the same options.
1 change: 1 addition & 0 deletions packages/rainbowkit/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
export * from './components';
export { wallet } from './wallets/walletConnectors';
export { getDefaultWallets } from './wallets/getDefaultWallets';
export { getWalletConnectConnector } from './wallets/getWalletConnectConnector';
export { connectorsForWallets } from './wallets/connectorsForWallets';
export { useAddRecentTransaction } from './transactions/useAddRecentTransaction';
export type { Wallet, WalletList } from './wallets/Wallet';
Expand Down
9 changes: 9 additions & 0 deletions packages/rainbowkit/src/utils/flatten.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
export function flatten<Item>(array: Item[][]) {
const flattenedItems: Item[] = [];

for (const items of array) {
flattenedItems.push(...items);
}

return flattenedItems;
}
1 change: 1 addition & 0 deletions packages/rainbowkit/src/wallets/Wallet.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ export type WalletList = { groupName: string; wallets: Wallet[] }[];

export type WalletInstance = Omit<Wallet, 'createConnector'> &
ReturnType<Wallet['createConnector']> & {
index: number;
groupName: string;
walletConnectModalConnector?: Connector;
};
32 changes: 17 additions & 15 deletions packages/rainbowkit/src/wallets/connectorsForWallets.ts
Original file line number Diff line number Diff line change
@@ -1,32 +1,29 @@
import { Connector } from 'wagmi';
import { WalletConnectConnector } from 'wagmi/connectors/walletConnect';
import { isMobile } from '../utils/isMobile';
import { omitUndefinedValues } from '../utils/omitUndefinedValues';
import { ConnectorArgs, WalletInstance, WalletList } from './Wallet';

export const connectorsForWallets = (walletList: WalletList) => {
let index = -1;

return function (connectorArgs: ConnectorArgs) {
const connectors: Connector[] = [];

walletList.forEach(({ groupName, wallets }) => {
wallets.forEach(({ createConnector, ...walletMeta }) => {
index++;

const { connector, ...connectionMethods } = omitUndefinedValues(
createConnector(connectorArgs)
);

// @ts-expect-error
if (connector._wallet) {
throw new Error(
`Can't connect wallet "${walletMeta.name}" to connector "${
connector.name ?? connector.id
}" as it's already connected to wallet "${
// @ts-expect-error
connector._wallet.name
}". Each wallet must have its own connector instance.`
);
}

let walletConnectModalConnector: Connector | undefined;
if (walletMeta.id === 'walletConnect' && connectionMethods.qrCode) {
if (
walletMeta.id === 'walletConnect' &&
connectionMethods.qrCode &&
!isMobile()
) {
const { chains, options } = connector;

walletConnectModalConnector = new WalletConnectConnector({
Expand All @@ -43,16 +40,21 @@ export const connectorsForWallets = (walletList: WalletList) => {
const walletInstance: WalletInstance = {
connector,
groupName,
index,
walletConnectModalConnector,
...walletMeta,
...connectionMethods,
};

// Mutate connector instance to add wallet instance
// @ts-expect-error
connector._wallet = walletInstance;
connector._wallets = connector._wallets ?? [];
// @ts-expect-error
connector._wallets.push(walletInstance);

connectors.push(connector);
if (!connectors.includes(connector)) {
connectors.push(connector);
}
});
});

Expand Down
38 changes: 38 additions & 0 deletions packages/rainbowkit/src/wallets/getWalletConnectConnector.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import { WalletConnectConnector } from 'wagmi/connectors/walletConnect';
import { Chain } from '../components/RainbowKitProvider/RainbowKitChainContext';
import { rpcUrlsForChains } from './../utils/rpcUrlsForChains';

type SerializedOptions = string;
const sharedConnectors = new Map<SerializedOptions, WalletConnectConnector>();

type WalletConnectConnectorOptions = ConstructorParameters<
typeof WalletConnectConnector
>[0];

function createConnector(options: WalletConnectConnectorOptions) {
const connector = new WalletConnectConnector(options);
sharedConnectors.set(JSON.stringify(options), connector);
return connector;
}

export function getWalletConnectConnector({
chains,
qrcode = false,
}: {
chains: Chain[];
qrcode?: boolean;
}) {
const rpc = rpcUrlsForChains(chains);
const options: WalletConnectConnectorOptions = {
chains,
options: {
qrcode,
rpc,
},
};

const serializedOptions = JSON.stringify(options);
const sharedConnector = sharedConnectors.get(serializedOptions);

return sharedConnector ?? createConnector(options);
}
96 changes: 51 additions & 45 deletions packages/rainbowkit/src/wallets/useWalletConnectors.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { Connector, useConnect } from 'wagmi';
import { flatten } from '../utils/flatten';
import { indexBy } from '../utils/indexBy';
import { isNotNullish } from '../utils/isNotNullish';
import { WalletInstance } from './Wallet';
Expand All @@ -25,64 +26,69 @@ export function useWalletConnectors(): WalletConnector[] {
return result;
}

const connectorByWalletId = indexBy(
defaultConnectors,
// @ts-expect-error
connector => (connector._wallet as WalletInstance)?.id
const walletInstances = flatten(
defaultConnectors.map(connector => {
// @ts-expect-error
return (connector._wallets as WalletInstance[]) ?? [];
})
).sort((a, b) => a.index - b.index);

const walletInstanceById = indexBy(
walletInstances,
walletInstance => walletInstance.id
);

const MAX_RECENT_WALLETS = 3;
const recentConnectors: Connector[] = getRecentWalletIds()
.map(walletId => connectorByWalletId[walletId])
const recentWallets: WalletInstance[] = getRecentWalletIds()
.map(walletId => walletInstanceById[walletId])
.filter(isNotNullish)
.slice(0, MAX_RECENT_WALLETS);

const connectors: Connector[] = [
...recentConnectors,
...defaultConnectors.filter(
connector => !recentConnectors.includes(connector)
const groupedWallets: WalletInstance[] = [
...recentWallets,
...walletInstances.filter(
walletInstance => !recentWallets.includes(walletInstance)
),
];

return connectors
.map((connector: Connector) => {
// @ts-expect-error
const wallet = connector._wallet as WalletInstance;
const walletConnectors: WalletConnector[] = [];

if (!wallet) {
return null;
}
groupedWallets.forEach((wallet: WalletInstance) => {
if (!wallet) {
return;
}

const recent = recentConnectors.includes(connector);
const recent = recentWallets.includes(wallet);

return {
...wallet,
connect: () => connectWallet(wallet.id, connector),
groupName: recent ? 'Recent' : wallet.groupName,
onConnecting: (fn: () => void) =>
connector.on('message', ({ type }) =>
type === 'connecting' ? fn() : undefined
),
ready: (wallet.installed ?? true) && connector.ready,
recent,
showWalletConnectModal: wallet.walletConnectModalConnector
? async () => {
try {
await connectWallet(
wallet.id,
wallet.walletConnectModalConnector!
);
} catch (err) {
// @ts-expect-error
const isUserRejection = err.name === 'UserRejectedRequestError';
walletConnectors.push({
...wallet,
connect: () => connectWallet(wallet.id, wallet.connector),
groupName: recent ? 'Recent' : wallet.groupName,
onConnecting: (fn: () => void) =>
wallet.connector.on('message', ({ type }) =>
type === 'connecting' ? fn() : undefined
),
ready: (wallet.installed ?? true) && wallet.connector.ready,
recent,
showWalletConnectModal: wallet.walletConnectModalConnector
? async () => {
try {
await connectWallet(
wallet.id,
wallet.walletConnectModalConnector!
);
} catch (err) {
// @ts-expect-error
const isUserRejection = err.name === 'UserRejectedRequestError';

if (!isUserRejection) {
throw err;
}
if (!isUserRejection) {
throw err;
}
}
: undefined,
};
})
.filter(isNotNullish);
}
: undefined,
});
});

return walletConnectors;
}
15 changes: 2 additions & 13 deletions packages/rainbowkit/src/wallets/walletConnectors/argent/argent.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,8 @@
/* eslint-disable sort-keys-fix/sort-keys-fix */
import { WalletConnectConnector } from 'wagmi/connectors/walletConnect';
import { Chain } from '../../../components/RainbowKitProvider/RainbowKitChainContext';
import { isAndroid } from '../../../utils/isMobile';
import { rpcUrlsForChains } from '../../../utils/rpcUrlsForChains';
import { Wallet } from '../../Wallet';
import { getWalletConnectConnector } from '../../getWalletConnectConnector';

export interface ArgentOptions {
chains: Chain[];
Expand All @@ -21,17 +20,7 @@ export const argent = ({ chains }: ArgentOptions): Wallet => ({
qrCode: 'https://argent.link/app',
},
createConnector: () => {
const rpc = rpcUrlsForChains(chains);
const connector = new WalletConnectConnector({
chains,
options: {
qrcode: false,
options: {
qrcode: false,
rpc,
},
},
});
const connector = getWalletConnectConnector({ chains });

return {
connector,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,7 @@
/* eslint-disable sort-keys-fix/sort-keys-fix */
import { WalletConnectConnector } from 'wagmi/connectors/walletConnect';
import { Chain } from '../../../components/RainbowKitProvider/RainbowKitChainContext';
import { rpcUrlsForChains } from '../../../utils/rpcUrlsForChains';
import { Wallet } from '../../Wallet';
import { getWalletConnectConnector } from '../../getWalletConnectConnector';

export interface ImTokenOptions {
chains: Chain[];
Expand All @@ -19,14 +18,8 @@ export const imToken = ({ chains }: ImTokenOptions): Wallet => ({
qrCode: 'https://token.im/download',
},
createConnector: () => {
const rpc = rpcUrlsForChains(chains);
const connector = new WalletConnectConnector({
chains,
options: {
qrcode: false,
rpc,
},
});
const connector = getWalletConnectConnector({ chains });

return {
connector,
mobile: {
Expand Down
12 changes: 2 additions & 10 deletions packages/rainbowkit/src/wallets/walletConnectors/ledger/ledger.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,8 @@
/* eslint-disable sort-keys-fix/sort-keys-fix */
import { WalletConnectConnector } from 'wagmi/connectors/walletConnect';
import { Chain } from '../../../components/RainbowKitProvider/RainbowKitChainContext';
import { isAndroid } from '../../../utils/isMobile';
import { rpcUrlsForChains } from '../../../utils/rpcUrlsForChains';
import { Wallet } from '../../Wallet';
import { getWalletConnectConnector } from '../../getWalletConnectConnector';

export interface LedgerOptions {
chains: Chain[];
Expand All @@ -20,14 +19,7 @@ export const ledger = ({ chains }: LedgerOptions): Wallet => ({
qrCode: 'https://www.ledger.com/ledger-live/download#download-device-2',
},
createConnector: () => {
const rpc = rpcUrlsForChains(chains);
const connector = new WalletConnectConnector({
chains,
options: {
qrcode: false,
rpc,
},
});
const connector = getWalletConnectConnector({ chains });

return {
connector,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,10 +1,9 @@
/* eslint-disable sort-keys-fix/sort-keys-fix */
import { MetaMaskConnector } from 'wagmi/connectors/metaMask';
import { WalletConnectConnector } from 'wagmi/connectors/walletConnect';
import { Chain } from '../../../components/RainbowKitProvider/RainbowKitChainContext';
import { isAndroid, isMobile } from '../../../utils/isMobile';
import { rpcUrlsForChains } from '../../../utils/rpcUrlsForChains';
import { Wallet } from '../../Wallet';
import { getWalletConnectConnector } from '../../getWalletConnectConnector';

export interface MetaMaskOptions {
chains: Chain[];
Expand Down Expand Up @@ -57,15 +56,8 @@ export const metaMask = ({
ios: 'https://apps.apple.com/us/app/metamask/id1438144202',
},
createConnector: () => {
const rpc = rpcUrlsForChains(chains);
const connector = shouldUseWalletConnect
? new WalletConnectConnector({
chains,
options: {
qrcode: false,
rpc,
},
})
? getWalletConnectConnector({ chains })
: new MetaMaskConnector({
chains,
options: { shimDisconnect },
Expand Down
Loading

2 comments on commit ce473cd

@vercel
Copy link

@vercel vercel bot commented on ce473cd Jun 15, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@vercel
Copy link

@vercel vercel bot commented on ce473cd Jun 15, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please sign in to comment.