mirror of
https://github.com/Instadapp/assembly.git
synced 2024-07-29 22:37:06 +00:00
commit
af78644f67
12
assets/icons/ledger.svg
Normal file
12
assets/icons/ledger.svg
Normal file
|
|
@ -0,0 +1,12 @@
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 35 35">
|
||||||
|
<defs/>
|
||||||
|
<defs/>
|
||||||
|
<g id="prefix__Group_26536" data-name="Group 26536" transform="translate(-80 -205)">
|
||||||
|
<path id="prefix__Shape" d="M23.588 0h-16v21.583h21.6v-16A5.585 5.585 0 0023.588 0z" class="prefix__cls-1" transform="translate(85.739 205)"/>
|
||||||
|
<path id="prefix__Path_8749" d="M8.342 0H5.585A5.585 5.585 0 000 5.585v2.757h8.342z" class="prefix__cls-1" data-name="Path 8749" transform="translate(80 205)"/>
|
||||||
|
<path id="prefix__Rectangle-path" d="M0 7.59h8.342v8.342H0z" class="prefix__cls-1" transform="translate(80 210.739)"/>
|
||||||
|
<path id="prefix__Path_8750" d="M15.18 23.451h2.757a5.585 5.585 0 005.585-5.6V15.18H15.18z" class="prefix__cls-1" data-name="Path 8750" transform="translate(91.478 216.478)"/>
|
||||||
|
<path id="prefix__Path_8751" d="M7.59 15.18h8.342v8.342H7.59z" class="prefix__cls-1" data-name="Path 8751" transform="translate(85.739 216.478)"/>
|
||||||
|
<path id="prefix__Path_8752" d="M0 15.18v2.757a5.585 5.585 0 005.585 5.585h2.757V15.18z" class="prefix__cls-1" data-name="Path 8752" transform="translate(80 216.478)"/>
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 1.1 KiB |
|
|
@ -23,6 +23,7 @@
|
||||||
<button
|
<button
|
||||||
class="w-full px-6 py-3 text-left flex items-center h-[80px] border border-[#DBE5F4] rounded-[4px] text-lg text-[#374253] font-semibold hover:bg-background-light"
|
class="w-full px-6 py-3 text-left flex items-center h-[80px] border border-[#DBE5F4] rounded-[4px] text-lg text-[#374253] font-semibold hover:bg-background-light"
|
||||||
v-for="(wallet, key) in wallets"
|
v-for="(wallet, key) in wallets"
|
||||||
|
:disabled="connecting"
|
||||||
:key="key"
|
:key="key"
|
||||||
@click="connect(wallet.connector)"
|
@click="connect(wallet.connector)"
|
||||||
>
|
>
|
||||||
|
|
@ -71,10 +72,12 @@ import { computed, defineComponent, ref } from '@nuxtjs/composition-api'
|
||||||
import Input from '~/components/common/input/Input.vue'
|
import Input from '~/components/common/input/Input.vue'
|
||||||
import { useModal } from '~/composables/useModal'
|
import { useModal } from '~/composables/useModal'
|
||||||
import { useWeb3 } from '@instadapp/vue-web3'
|
import { useWeb3 } from '@instadapp/vue-web3'
|
||||||
import { injected } from '~/connectors'
|
import { injected, ledger } from '~/connectors'
|
||||||
import { SUPPORTED_WALLETS } from '~/constant/wallet'
|
import { SUPPORTED_WALLETS } from '~/constant/wallet'
|
||||||
import ButtonCTA from '../../common/input/ButtonCTA.vue'
|
import ButtonCTA from '../../common/input/ButtonCTA.vue'
|
||||||
import ButtonCTAOutlined from '../../common/input/ButtonCTAOutlined.vue'
|
import ButtonCTAOutlined from '../../common/input/ButtonCTAOutlined.vue'
|
||||||
|
import { Network, useNetwork } from '~/composables/useNetwork'
|
||||||
|
import { useNotification } from '~/composables/useNotification'
|
||||||
|
|
||||||
export default defineComponent({
|
export default defineComponent({
|
||||||
props: {
|
props: {
|
||||||
|
|
@ -87,11 +90,27 @@ export default defineComponent({
|
||||||
setup() {
|
setup() {
|
||||||
const { close } = useModal()
|
const { close } = useModal()
|
||||||
const { activate } = useWeb3()
|
const { activate } = useWeb3()
|
||||||
|
const { activeNetworkId } = useNetwork()
|
||||||
|
const { showError, showAwaiting, closeAll } = useNotification()
|
||||||
|
const connecting = ref(false)
|
||||||
|
|
||||||
const connect = async (connector) => {
|
const connect = async (connector) => {
|
||||||
await activate(connector, console.log)
|
connecting.value = true
|
||||||
|
|
||||||
|
showAwaiting("Connecting...")
|
||||||
|
|
||||||
|
try {
|
||||||
|
await activate(connector, undefined, true)
|
||||||
|
connecting.value = false
|
||||||
|
close()
|
||||||
|
closeAll()
|
||||||
|
} catch (error) {
|
||||||
|
closeAll()
|
||||||
|
showError("", error.message)
|
||||||
|
}
|
||||||
|
|
||||||
|
connecting.value = false
|
||||||
|
|
||||||
close()
|
|
||||||
}
|
}
|
||||||
const isMetamask = computed(() => process.server ? false : window.ethereum && window.ethereum.isMetaMask)
|
const isMetamask = computed(() => process.server ? false : window.ethereum && window.ethereum.isMetaMask)
|
||||||
|
|
||||||
|
|
@ -102,6 +121,10 @@ export default defineComponent({
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (wallet.connector === ledger && activeNetworkId.value !== Network.Mainnet) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
return wallet
|
return wallet
|
||||||
}).filter(Boolean))
|
}).filter(Boolean))
|
||||||
|
|
||||||
|
|
@ -111,6 +134,7 @@ export default defineComponent({
|
||||||
wallets,
|
wallets,
|
||||||
isMetamask,
|
isMetamask,
|
||||||
injected,
|
injected,
|
||||||
|
connecting,
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,8 @@ import { InjectedConnector } from "@web3-react/injected-connector";
|
||||||
import { WalletConnectConnector } from "@web3-react/walletconnect-connector";
|
import { WalletConnectConnector } from "@web3-react/walletconnect-connector";
|
||||||
import { PortisConnector } from "@web3-react/portis-connector";
|
import { PortisConnector } from "@web3-react/portis-connector";
|
||||||
import { WalletLinkConnector } from "@web3-react/walletlink-connector";
|
import { WalletLinkConnector } from "@web3-react/walletlink-connector";
|
||||||
|
// import { LedgerConnector } from "@web3-react/ledger-connector";
|
||||||
|
import { LedgerConnector } from "./ledger-connector";
|
||||||
|
|
||||||
import INSTADAPP_LOGO_URL from "~/assets/logo/instadapp-logo-icon.svg?inline";
|
import INSTADAPP_LOGO_URL from "~/assets/logo/instadapp-logo-icon.svg?inline";
|
||||||
|
|
||||||
|
|
@ -44,3 +46,20 @@ if (process.client) {
|
||||||
}
|
}
|
||||||
|
|
||||||
export { gnosisSafe };
|
export { gnosisSafe };
|
||||||
|
|
||||||
|
const POLLING_INTERVAL = 12000;
|
||||||
|
|
||||||
|
export enum LedgerDerivationPath {
|
||||||
|
"Legacy" = "44'/60'/0'/x",
|
||||||
|
"LedgerLive" = "44'/60'/x'/0/0"
|
||||||
|
}
|
||||||
|
|
||||||
|
export const ledger = new LedgerConnector({
|
||||||
|
chainId: 1,
|
||||||
|
url: `https://mainnet.infura.io/v3/${process.env.INFURA_ID}`,
|
||||||
|
requestTimeoutMs: 60000,
|
||||||
|
pollingInterval: POLLING_INTERVAL,
|
||||||
|
baseDerivationPath: "44'/60'/x'/0/0",
|
||||||
|
accountsOffset: 0,
|
||||||
|
accountsLength: 1
|
||||||
|
});
|
||||||
|
|
|
||||||
102
connectors/ledger-connector.ts
Normal file
102
connectors/ledger-connector.ts
Normal file
|
|
@ -0,0 +1,102 @@
|
||||||
|
// https://github.com/aave/aave-ui/blob/e0602e3560b2aabf86e8afd29db20c0ee8c249fc/src/libs/web3-data-provider/web3-providers/connectors/ledger-connector.ts
|
||||||
|
|
||||||
|
import { ConnectorUpdate } from '@web3-react/types';
|
||||||
|
import { AbstractConnector } from '@web3-react/abstract-connector';
|
||||||
|
import Web3ProviderEngine from 'web3-provider-engine';
|
||||||
|
import { RPCSubprovider } from '@0x/subproviders/lib/src/subproviders/rpc_subprovider'; // https://github.com/0xProject/0x-monorepo/issues/1400
|
||||||
|
import createLedgerSubprovider from './ledger-subprovider';
|
||||||
|
import TransportU2F from '@ledgerhq/hw-transport-u2f';
|
||||||
|
import webUsbTransport from '@ledgerhq/hw-transport-webusb';
|
||||||
|
import type Transport from '@ledgerhq/hw-transport';
|
||||||
|
|
||||||
|
interface LedgerConnectorArguments {
|
||||||
|
chainId: number;
|
||||||
|
url: string;
|
||||||
|
pollingInterval?: number;
|
||||||
|
requestTimeoutMs?: number;
|
||||||
|
baseDerivationPath?: string;
|
||||||
|
accountsOffset?: number;
|
||||||
|
accountsLength: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
const getTransport = async (): Promise<Transport> => {
|
||||||
|
if (await webUsbTransport.isSupported()) {
|
||||||
|
return await webUsbTransport.create();
|
||||||
|
}
|
||||||
|
return await TransportU2F.create();
|
||||||
|
};
|
||||||
|
|
||||||
|
export class LedgerConnector extends AbstractConnector {
|
||||||
|
private readonly chainId: number;
|
||||||
|
private readonly url: string;
|
||||||
|
private readonly pollingInterval?: number;
|
||||||
|
private readonly requestTimeoutMs?: number;
|
||||||
|
private readonly baseDerivationPath?: string;
|
||||||
|
private readonly accountsOffset?: number;
|
||||||
|
private readonly accountsLength: number;
|
||||||
|
|
||||||
|
private provider: any;
|
||||||
|
|
||||||
|
constructor({
|
||||||
|
chainId,
|
||||||
|
url,
|
||||||
|
pollingInterval,
|
||||||
|
requestTimeoutMs,
|
||||||
|
baseDerivationPath,
|
||||||
|
accountsOffset = 0,
|
||||||
|
accountsLength = 1,
|
||||||
|
}: LedgerConnectorArguments) {
|
||||||
|
super({ supportedChainIds: [chainId] });
|
||||||
|
|
||||||
|
this.chainId = chainId;
|
||||||
|
this.url = url;
|
||||||
|
this.requestTimeoutMs = requestTimeoutMs;
|
||||||
|
this.baseDerivationPath = baseDerivationPath;
|
||||||
|
this.pollingInterval = pollingInterval;
|
||||||
|
this.accountsOffset = accountsOffset;
|
||||||
|
this.accountsLength = accountsLength;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async activate(): Promise<ConnectorUpdate> {
|
||||||
|
if (!this.provider) {
|
||||||
|
const engine = new Web3ProviderEngine({ pollingInterval: this.pollingInterval });
|
||||||
|
const ledgerProvider = await createLedgerSubprovider(getTransport, {
|
||||||
|
networkId: this.chainId,
|
||||||
|
paths: this.baseDerivationPath ? [this.baseDerivationPath] : undefined,
|
||||||
|
accountsLength: this.accountsLength,
|
||||||
|
accountsOffset: this.accountsOffset,
|
||||||
|
});
|
||||||
|
engine.addProvider(ledgerProvider);
|
||||||
|
engine.addProvider(new RPCSubprovider(this.url, this.requestTimeoutMs));
|
||||||
|
this.provider = engine;
|
||||||
|
this.provider.start();
|
||||||
|
}
|
||||||
|
const account = await this.getAccount();
|
||||||
|
return { provider: this.provider, chainId: this.chainId, account };
|
||||||
|
}
|
||||||
|
|
||||||
|
public async getProvider(): Promise<Web3ProviderEngine> {
|
||||||
|
return this.provider;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async getChainId(): Promise<number> {
|
||||||
|
return this.chainId;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async getAccount(): Promise<string> {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
this.provider._providers[0].getAccounts(function (error: any, result: string[]) {
|
||||||
|
if (error) {
|
||||||
|
return reject(error);
|
||||||
|
}
|
||||||
|
return resolve(result[0]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public deactivate() {
|
||||||
|
if (this.provider) {
|
||||||
|
this.provider.stop();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
138
connectors/ledger-subprovider.ts
Normal file
138
connectors/ledger-subprovider.ts
Normal file
|
|
@ -0,0 +1,138 @@
|
||||||
|
import AppEth from '@ledgerhq/hw-app-eth';
|
||||||
|
import type Transport from '@ledgerhq/hw-transport';
|
||||||
|
import HookedWalletSubprovider from 'web3-provider-engine/subproviders/hooked-wallet';
|
||||||
|
import stripHexPrefix from 'strip-hex-prefix';
|
||||||
|
import { Transaction as EthereumTx } from 'ethereumjs-tx';
|
||||||
|
|
||||||
|
function makeError(msg: string, id: string) {
|
||||||
|
const err: any = new Error(msg);
|
||||||
|
err.id = id;
|
||||||
|
return err;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
*/
|
||||||
|
type SubproviderOptions = {
|
||||||
|
// refer to https://github.com/ethereum/EIPs/blob/master/EIPS/eip-155.md
|
||||||
|
networkId: number;
|
||||||
|
// derivation path schemes (with a x in the path)
|
||||||
|
paths?: string[];
|
||||||
|
// should use actively validate on the device
|
||||||
|
askConfirm?: boolean;
|
||||||
|
// number of accounts to derivate
|
||||||
|
accountsLength?: number;
|
||||||
|
// offset index to use to start derivating the accounts
|
||||||
|
accountsOffset?: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
const defaultOptions = {
|
||||||
|
networkId: 1, // mainnet
|
||||||
|
paths: ["44'/60'/x'/0/0", "44'/60'/0'/x"], // ledger live derivation path
|
||||||
|
askConfirm: false,
|
||||||
|
accountsLength: 1,
|
||||||
|
accountsOffset: 0,
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a HookedWalletSubprovider for Ledger devices.
|
||||||
|
*/
|
||||||
|
export default async function createLedgerSubprovider(
|
||||||
|
getTransport: () => Promise<Transport>,
|
||||||
|
options?: SubproviderOptions
|
||||||
|
) {
|
||||||
|
if (options && 'path' in options) {
|
||||||
|
throw new Error(
|
||||||
|
"@ledgerhq/web3-subprovider: path options was replaced by paths. example: paths: [\"44'/60'/x'/0/0\"]"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
const { networkId, paths, askConfirm, accountsLength, accountsOffset } = {
|
||||||
|
...defaultOptions,
|
||||||
|
...options,
|
||||||
|
};
|
||||||
|
|
||||||
|
if (!paths.length) {
|
||||||
|
throw new Error('paths must not be empty');
|
||||||
|
}
|
||||||
|
|
||||||
|
const addressToPathMap: any = {};
|
||||||
|
const transport = await getTransport();
|
||||||
|
|
||||||
|
async function getAccounts() {
|
||||||
|
const eth = new AppEth(transport);
|
||||||
|
const addresses: any = {};
|
||||||
|
for (let i = accountsOffset; i < accountsOffset + accountsLength; i++) {
|
||||||
|
const x = Math.floor(i / paths.length);
|
||||||
|
const pathIndex = i - paths.length * x;
|
||||||
|
const path = paths[pathIndex].replace('x', String(x));
|
||||||
|
const address = await eth.getAddress(path, askConfirm, false);
|
||||||
|
addresses[path] = address.address;
|
||||||
|
addressToPathMap[address.address.toLowerCase()] = path;
|
||||||
|
}
|
||||||
|
return addresses;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function signPersonalMessage(msgData: any) {
|
||||||
|
const path = addressToPathMap[msgData.from.toLowerCase()];
|
||||||
|
if (!path) throw new Error("address unknown '" + msgData.from + "'");
|
||||||
|
const eth = new AppEth(transport);
|
||||||
|
const result = await eth.signPersonalMessage(path, stripHexPrefix(msgData.data));
|
||||||
|
const v = parseInt(result.v, 10) - 27;
|
||||||
|
let vHex = v.toString(16);
|
||||||
|
if (vHex.length < 2) {
|
||||||
|
vHex = `0${v}`;
|
||||||
|
}
|
||||||
|
return `0x${result.r}${result.s}${vHex}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function signTransaction(txData: any) {
|
||||||
|
const path = addressToPathMap[txData.from.toLowerCase()];
|
||||||
|
if (!path) throw new Error("address unknown '" + txData.from + "'");
|
||||||
|
const eth = new AppEth(transport);
|
||||||
|
const tx = new EthereumTx(txData, { chain: networkId });
|
||||||
|
|
||||||
|
// Set the EIP155 bits
|
||||||
|
tx.raw[6] = Buffer.from([networkId]); // v
|
||||||
|
tx.raw[7] = Buffer.from([]); // r
|
||||||
|
tx.raw[8] = Buffer.from([]); // s
|
||||||
|
|
||||||
|
// Pass hex-rlp to ledger for signing
|
||||||
|
const result = await eth.signTransaction(path, tx.serialize().toString('hex'));
|
||||||
|
|
||||||
|
// Store signature in transaction
|
||||||
|
tx.v = Buffer.from(result.v, 'hex');
|
||||||
|
tx.r = Buffer.from(result.r, 'hex');
|
||||||
|
tx.s = Buffer.from(result.s, 'hex');
|
||||||
|
|
||||||
|
// EIP155: v should be chain_id * 2 + {35, 36}
|
||||||
|
const signedChainId = Math.floor((tx.v[0] - 35) / 2);
|
||||||
|
const validChainId = networkId & 0xff; // FIXME this is to fixed a current workaround that app don't support > 0xff
|
||||||
|
if (signedChainId !== validChainId) {
|
||||||
|
throw makeError(
|
||||||
|
'Invalid networkId signature returned. Expected: ' + networkId + ', Got: ' + signedChainId,
|
||||||
|
'InvalidNetworkId'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return `0x${tx.serialize().toString('hex')}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
const subprovider = new HookedWalletSubprovider({
|
||||||
|
getAccounts: (callback: (err: any, res: any) => void) => {
|
||||||
|
getAccounts()
|
||||||
|
.then((res) => callback(null, Object.values(res)))
|
||||||
|
.catch((err) => callback(err, null));
|
||||||
|
},
|
||||||
|
signPersonalMessage: (txData: any, callback: (err: any, res: any) => void) => {
|
||||||
|
signPersonalMessage(txData)
|
||||||
|
.then((res) => callback(null, res))
|
||||||
|
.catch((err) => callback(err, null));
|
||||||
|
},
|
||||||
|
signTransaction: (txData: any, callback: (err: any, res: any) => void) => {
|
||||||
|
signTransaction(txData)
|
||||||
|
.then((res) => callback(null, res))
|
||||||
|
.catch((err) => callback(err, null));
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
return subprovider;
|
||||||
|
}
|
||||||
|
|
@ -1,10 +1,11 @@
|
||||||
import { AbstractConnector } from '@web3-react/abstract-connector'
|
import { AbstractConnector } from '@web3-react/abstract-connector'
|
||||||
import { injected, walletconnect, portis, walletlink } from '~/connectors'
|
import { injected, walletconnect, portis, walletlink, ledger } from '~/connectors'
|
||||||
|
|
||||||
import METAMASK_ICON_URL from '~/assets/icons/metamask.svg?inline'
|
import METAMASK_ICON_URL from '~/assets/icons/metamask.svg?inline'
|
||||||
import WALLETCONNECT_ICON_URL from '~/assets/icons/wallet-connect-icon.svg?inline'
|
import WALLETCONNECT_ICON_URL from '~/assets/icons/wallet-connect-icon.svg?inline'
|
||||||
import PORTIS_ICON_URL from '~/assets/icons/portis.svg?inline'
|
import PORTIS_ICON_URL from '~/assets/icons/portis.svg?inline'
|
||||||
import COINBASE_ICON_URL from '~/assets/icons/coinbase.svg?inline'
|
import COINBASE_ICON_URL from '~/assets/icons/coinbase.svg?inline'
|
||||||
|
import LEDGER_ICON_URL from '~/assets/icons/ledger.svg?inline'
|
||||||
|
|
||||||
interface WalletInfo {
|
interface WalletInfo {
|
||||||
connector?: AbstractConnector;
|
connector?: AbstractConnector;
|
||||||
|
|
@ -34,5 +35,10 @@ export const SUPPORTED_WALLETS: { [key: string]: WalletInfo } = {
|
||||||
name: 'Coinbase Wallet',
|
name: 'Coinbase Wallet',
|
||||||
iconURL: COINBASE_ICON_URL,
|
iconURL: COINBASE_ICON_URL,
|
||||||
},
|
},
|
||||||
|
LEDGER: {
|
||||||
|
connector: ledger,
|
||||||
|
name: 'Ledger',
|
||||||
|
iconURL: LEDGER_ICON_URL,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -13,6 +13,7 @@
|
||||||
"@gnosis.pm/safe-apps-web3-react": "^0.6.2",
|
"@gnosis.pm/safe-apps-web3-react": "^0.6.2",
|
||||||
"@gnosis.pm/safe-apps-web3modal": "^2.0.0",
|
"@gnosis.pm/safe-apps-web3modal": "^2.0.0",
|
||||||
"@instadapp/vue-web3": "^0.3.0",
|
"@instadapp/vue-web3": "^0.3.0",
|
||||||
|
"@ledgerhq/hw-transport-webusb": "^6.6.0",
|
||||||
"@nuxtjs/axios": "^5.13.6",
|
"@nuxtjs/axios": "^5.13.6",
|
||||||
"@nuxtjs/composition-api": "^0.27.0",
|
"@nuxtjs/composition-api": "^0.27.0",
|
||||||
"@portis/web3": "^4.0.5",
|
"@portis/web3": "^4.0.5",
|
||||||
|
|
@ -22,6 +23,7 @@
|
||||||
"@walletconnect/web3-provider": "^1.4.1",
|
"@walletconnect/web3-provider": "^1.4.1",
|
||||||
"@web3-react/core": "^6.1.9",
|
"@web3-react/core": "^6.1.9",
|
||||||
"@web3-react/injected-connector": "^6.0.7",
|
"@web3-react/injected-connector": "^6.0.7",
|
||||||
|
"@web3-react/ledger-connector": "^6.1.9",
|
||||||
"@web3-react/portis-connector": "^6.1.9",
|
"@web3-react/portis-connector": "^6.1.9",
|
||||||
"@web3-react/walletconnect-connector": "^6.2.4",
|
"@web3-react/walletconnect-connector": "^6.2.4",
|
||||||
"@web3-react/walletlink-connector": "^6.2.3",
|
"@web3-react/walletlink-connector": "^6.2.3",
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user