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

frontend: integrate Wallet Connect v2 #1472

Closed
wants to merge 4 commits into from
Closed
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
3 changes: 2 additions & 1 deletion frontend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,8 @@
"@metamask/onboarding": "^1.0.1",
"@vuelidate/core": "^2.0.0-alpha.43",
"@vuelidate/validators": "^2.0.0-alpha.31",
"@walletconnect/web3-provider": "^1.7.8",
"@walletconnect/ethereum-provider": "^2.7.0",
"@web3modal/standalone": "^2.3.6",
"autoprefixer": "^10",
"ethers": "^5.5.1",
"lodash.debounce": "^4.0.8",
Expand Down
1 change: 1 addition & 0 deletions frontend/src/components/WalletMenu.vue
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,7 @@ const walletOptions = ref([
{
name: 'WalletConnect',
icon: new URL('../assets/images/walletconnect.svg', import.meta.url).href,
classes: 'w-16 h-16',
description: 'Connect using mobile wallet',
connect: connectWalletConnect,
connecting: connectingWalletConnect,
Expand Down
15 changes: 13 additions & 2 deletions frontend/src/components/wallet/Disconnect.vue
Original file line number Diff line number Diff line change
@@ -1,13 +1,20 @@
<template>
<button class="inline underline w-fit text-xs" data-test="trigger" @click="disconnect">
<button
v-if="!disconnectInProgress"
class="inline underline w-fit text-xs"
data-test="trigger"
@click="disconnect"
>
Disconnect Wallet
</button>
<spinner v-else class="border-t-sea-green" size-classes="w-4 h-4" border="2"></spinner>
</template>

<script lang="ts" setup>
import { storeToRefs } from 'pinia';
import { ref } from 'vue';

import Spinner from '@/components/Spinner.vue';
import { useWallet } from '@/composables/useWallet';
import { useEthereumProvider } from '@/stores/ethereum-provider';
import { useSettings } from '@/stores/settings';
Expand All @@ -16,5 +23,9 @@ const ethereumProvider = useEthereumProvider();
const { provider } = storeToRefs(ethereumProvider);
const { connectedWallet } = storeToRefs(useSettings());

const { disconnectWallet: disconnect } = useWallet(provider, connectedWallet, ref());
const { disconnectWallet: disconnect, disconnectInProgress } = useWallet(
provider,
connectedWallet,
ref({}),
);
</script>
4 changes: 2 additions & 2 deletions frontend/src/composables/useTransferRequest.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import { reactive } from 'vue';
import { Transfer } from '@/actions/transfers';
import { useAsynchronousTask } from '@/composables/useAsynchronousTask';
import { getRequestFee } from '@/services/transactions/request-manager';
import type { EthereumProvider } from '@/services/web3-provider';
import type { IEthereumProvider } from '@/services/web3-provider';
import type { Chain, Token } from '@/types/data';
import { TokenAmount } from '@/types/token-amount';
import { UInt256 } from '@/types/uint-256';
Expand Down Expand Up @@ -63,7 +63,7 @@ export function useTransferRequest() {
await transfer.execute(signer.value, signerAddress.value);
};

const withdraw = async (transfer: Transfer, provider: EthereumProvider): Promise<void> => {
const withdraw = async (transfer: Transfer, provider: IEthereumProvider): Promise<void> => {
await transfer.withdraw(provider);
};

Expand Down
14 changes: 9 additions & 5 deletions frontend/src/composables/useWallet.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ export function useWallet(
metaMaskProvider.requestSigner();
provider.value = metaMaskProvider;
connectedWallet.value = WalletType.MetaMask;
metaMaskProvider.on('disconnect', disconnectWallet);
metaMaskProvider.on('disconnect', disconnect.run);
} else if (withOnboarding) {
onboardMetaMask();
}
Expand All @@ -36,7 +36,7 @@ export function useWallet(
injectedProvider.requestSigner();
provider.value = injectedProvider;
connectedWallet.value = WalletType.Injected;
injectedProvider.on('disconnect', disconnectWallet);
injectedProvider.on('disconnect', disconnect.run);
}
}

Expand All @@ -46,7 +46,7 @@ export function useWallet(
if (walletConnectProvider) {
provider.value = walletConnectProvider;
connectedWallet.value = WalletType.WalletConnect;
walletConnectProvider.on('disconnect', disconnectWallet);
walletConnectProvider.on('disconnect', disconnect.run);
}
}

Expand All @@ -56,7 +56,7 @@ export function useWallet(
if (coinbaseProvider) {
provider.value = coinbaseProvider;
connectedWallet.value = WalletType.Coinbase;
coinbaseProvider.on('disconnect', disconnectWallet);
coinbaseProvider.on('disconnect', disconnect.run);
}
}

Expand All @@ -77,6 +77,7 @@ export function useWallet(
}

async function disconnectWallet() {
await provider.value?.closeExternalConnection();
connectedWallet.value = undefined;
provider.value = undefined;
}
Expand All @@ -85,6 +86,7 @@ export function useWallet(
const walletConnectTask = useAsynchronousTask(connectWalletConnect);
const coinbaseTask = useAsynchronousTask(connectCoinbase);
const injectedTask = useAsynchronousTask(connectInjected);
const disconnect = useAsynchronousTask(disconnectWallet);

return {
connectMetaMask: metamaskTask.run,
Expand All @@ -95,7 +97,9 @@ export function useWallet(
connectingCoinbase: coinbaseTask.active,
connectInjected: injectedTask.run,
connectingInjected: injectedTask.active,
disconnectInProgress: disconnect.active,
disconnectWallet: disconnect.run,
disconnectError: disconnect.error,
reconnectToWallet,
disconnectWallet,
};
}
13 changes: 9 additions & 4 deletions frontend/src/services/web3-provider/coinbase-provider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,10 @@
Remove the above mentioned package from project once coinbase fixes this:
https://github.com/coinbase/coinbase-wallet-sdk/issues/56
*/
import type { CoinbaseWalletProvider } from '@coinbase/wallet-sdk';
import { hexValue } from 'ethers/lib/utils';

import { EthereumProvider } from '@/services/web3-provider/ethereum-provider';
import type { Eip1193Provider } from '@/services/web3-provider/types';
import { CoinbaseWalletSDK } from '@/services/web3-provider/util-export';

const APP_NAME = 'Beamer Bridge';
Expand Down Expand Up @@ -46,7 +46,7 @@ export async function createCoinbaseProvider(rpcList: {
await provider.enable();

if (provider.connected) {
const coinbaseProvider = new CoinbaseProvider(provider as unknown as Eip1193Provider);
const coinbaseProvider = new CoinbaseProvider(provider);

await coinbaseProvider.init();

Expand All @@ -56,11 +56,16 @@ export async function createCoinbaseProvider(rpcList: {
return undefined;
}

export class CoinbaseProvider extends EthereumProvider {
constructor(_provider: Eip1193Provider) {
export class CoinbaseProvider extends EthereumProvider<CoinbaseWalletProvider> {
constructor(_provider: CoinbaseWalletProvider) {
super(_provider);
}

async closeExternalConnection() {
this.externalProvider.close();
this.externalProvider.disconnect();
}

protected async switchChain(newChainId: number): Promise<boolean> {
const newChainIdHex = hexValue(newChainId);
try {
Expand Down
13 changes: 10 additions & 3 deletions frontend/src/services/web3-provider/ethereum-provider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,15 +8,18 @@ import { ref, shallowRef, toRaw } from 'vue';
import type { Eip1193Provider, IEthereumProvider } from '@/services/web3-provider/types';
import type { Chain, Token } from '@/types/data';

export abstract class EthereumProvider extends EventEmitter implements IEthereumProvider {
export abstract class EthereumProvider<T extends Eip1193Provider>
extends EventEmitter
implements IEthereumProvider
{
signer: ShallowRef<JsonRpcSigner | undefined> = shallowRef(undefined);
signerAddress: ShallowRef<string | undefined> = shallowRef(undefined);
chainId: Ref<number> = ref(1);

protected web3Provider: Web3Provider;
protected externalProvider: Eip1193Provider;
protected externalProvider: T;

constructor(_provider: Eip1193Provider) {
constructor(_provider: T) {
super();
this.web3Provider = new Web3Provider(_provider, 'any');
this.externalProvider = _provider;
Expand Down Expand Up @@ -53,6 +56,10 @@ export abstract class EthereumProvider extends EventEmitter implements IEthereum
return successful;
}

async closeExternalConnection(): Promise<void> {
return;
}

// Returns false in case the provider does not have the chain.
// Throws if the user rejects.
protected abstract switchChain(newChainId: number): Promise<boolean>;
Expand Down
2 changes: 1 addition & 1 deletion frontend/src/services/web3-provider/injected-provider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ export async function createInjectedProvider(): Promise<InjectedProvider | undef
return undefined;
}

export class InjectedProvider extends EthereumProvider implements ISigner {
export class InjectedProvider extends EthereumProvider<Eip1193Provider> implements ISigner {
constructor(_provider: Eip1193Provider) {
super(_provider);
}
Expand Down
6 changes: 3 additions & 3 deletions frontend/src/services/web3-provider/metamask-provider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { hexValue } from 'ethers/lib/utils';
import { InjectedProvider } from '@/services/web3-provider/injected-provider';
import type {
DetectedEthereumProvider,
Eip1193Provider,
ExternalProvider,
ISigner,
} from '@/services/web3-provider/types';
import { detectEthereumProvider, MetaMaskOnboarding } from '@/services/web3-provider/util-export';
Expand Down Expand Up @@ -38,7 +38,7 @@ export async function createMetaMaskProvider(): Promise<MetaMaskProvider | undef
}

if (injectedMetamaskProvider) {
const metaMaskProvider = new MetaMaskProvider(injectedMetamaskProvider as Eip1193Provider);
const metaMaskProvider = new MetaMaskProvider(injectedMetamaskProvider as ExternalProvider);
await metaMaskProvider.init();
return metaMaskProvider;
}
Expand All @@ -53,7 +53,7 @@ export async function onboardMetaMask() {
}

export class MetaMaskProvider extends InjectedProvider implements ISigner {
constructor(_provider: Eip1193Provider) {
constructor(_provider: ExternalProvider) {
if (!_provider.isMetaMask) {
throw new Error('Given provider is not MetaMask!');
}
Expand Down
23 changes: 14 additions & 9 deletions frontend/src/services/web3-provider/types.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,4 @@
import type {
Block,
ExternalProvider,
JsonRpcSigner,
Web3Provider,
} from '@ethersproject/providers';
import type { Block, JsonRpcSigner, Web3Provider } from '@ethersproject/providers';
import type { Ref, ShallowRef } from 'vue';

import type { Chain, Token } from '@/types/data';
Expand All @@ -18,6 +13,8 @@ export interface IEthereumProvider {
switchChainSafely(newChain: Chain): Promise<boolean>;
getChainId(): Promise<number>;
addToken(token: Token): Promise<boolean>;
disconnect(): void;
closeExternalConnection(): Promise<void>;
}
export interface EventEmitter {
on(eventName: string, listener: (...args: unknown[]) => void): void;
Expand All @@ -32,10 +29,18 @@ interface RequestArguments {
readonly params?: readonly unknown[] | object;
}

export type Eip1193Provider = ExternalProvider & {
export type ExternalProvider = Eip1193Provider & {
isMetaMask?: boolean;
sendAsync?(
request: RequestArguments,
callback: (error: unknown, response: unknown) => void,
): void;
send?(request: RequestArguments, callback: (error: unknown, response: unknown) => void): void;
};

export type Eip1193Provider = {
request(args: RequestArguments): Promise<unknown>;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
on(eventName: string, listener: (...args: any[]) => void): void;
on(eventName: string, listener: (...args: unknown[]) => void): void;
};

export type DetectedEthereumProvider =
Expand Down
4 changes: 1 addition & 3 deletions frontend/src/services/web3-provider/util-export.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,6 @@
import { CoinbaseWalletSDK } from '@coinbase/wallet-sdk';
import detectEthereumProvider from '@metamask/detect-provider';
import MetaMaskOnboarding from '@metamask/onboarding';
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore TODO: Install & use type declarations once we migrate to WalletConnect v2
import WalletConnect from '@walletconnect/web3-provider/dist/umd/index.min.js';
import WalletConnect from '@walletconnect/ethereum-provider';

export { CoinbaseWalletSDK, detectEthereumProvider, MetaMaskOnboarding, WalletConnect };
34 changes: 23 additions & 11 deletions frontend/src/services/web3-provider/walletconnect-provider.ts
Original file line number Diff line number Diff line change
@@ -1,34 +1,46 @@
import { hexValue } from 'ethers/lib/utils';

import { EthereumProvider } from '@/services/web3-provider/ethereum-provider';
import type { Eip1193Provider } from '@/services/web3-provider/types';
import { EthereumProvider } from '@/services/web3-provider';
import { WalletConnect } from '@/services/web3-provider/util-export';

const BEAMER_PROJECT_ID = 'b0909ba73ce9e30c4decb50a963c9b2a';

export async function createWalletConnectProvider(rpcList: {
[chainId: string]: string;
}): Promise<WalletConnectProvider | undefined> {
const provider = new WalletConnect({
rpc: rpcList,
}) as typeof WalletConnect;
const chains = Object.keys(rpcList).map((chainId) => parseInt(chainId));

const provider = await WalletConnect.init({
chains,
projectId: BEAMER_PROJECT_ID,
rpcMap: rpcList,
showQrModal: true,
});

await provider.enable();
try {
await provider.enable();
} catch (e) {
console.log('exception', e);
}

if (provider.connected) {
const walletConnectProvider = new WalletConnectProvider(
provider as unknown as Eip1193Provider,
);
const walletConnectProvider = new WalletConnectProvider(provider);
await walletConnectProvider.init();
return walletConnectProvider;
}

return undefined;
}

export class WalletConnectProvider extends EthereumProvider {
constructor(_provider: Eip1193Provider) {
export class WalletConnectProvider extends EthereumProvider<WalletConnect> {
constructor(_provider: WalletConnect) {
super(_provider);
}

async closeExternalConnection() {
await this.externalProvider.disconnect();
}

protected async switchChain(newChainId: number): Promise<boolean> {
const newChainIdHex = hexValue(newChainId);
try {
Expand Down
18 changes: 18 additions & 0 deletions frontend/tests/setup/window.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
/*
WalletConnect's modal library uses the window object for detecting mobile devices
therefore a window mock is required globally
*/

Object.defineProperty(window, 'matchMedia', {
GabrielBuragev marked this conversation as resolved.
Show resolved Hide resolved
writable: true,
value: vi.fn().mockImplementation((query) => ({
matches: false,
media: query,
onchange: null,
addListener: vi.fn(), // deprecated
removeListener: vi.fn(), // deprecated
addEventListener: vi.fn(),
removeEventListener: vi.fn(),
dispatchEvent: vi.fn(),
})),
});
Loading