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

Ledger connector, loading connect-kit via yarn package #905

Open
wants to merge 5 commits into
base: main
Choose a base branch
from
Open
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 example/components/Card.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import type { CoinbaseWallet } from '@web3-react/coinbase-wallet'
import type { Web3ReactHooks } from '@web3-react/core'
import type { GnosisSafe } from '@web3-react/gnosis-safe'
import type { Ledger } from '@web3-react/ledger'
import type { MetaMask } from '@web3-react/metamask'
import type { Network } from '@web3-react/network'
import type { WalletConnect } from '@web3-react/walletconnect'
Expand All @@ -13,7 +14,7 @@ import { ConnectWithSelect } from './ConnectWithSelect'
import { Status } from './Status'

interface Props {
connector: MetaMask | WalletConnect | WalletConnectV2 | CoinbaseWallet | Network | GnosisSafe
connector: Ledger | MetaMask | WalletConnect | WalletConnectV2 | CoinbaseWallet | Network | GnosisSafe
activeChainId: ReturnType<Web3ReactHooks['useChainId']>
chainIds?: ReturnType<Web3ReactHooks['useChainId']>[]
isActivating: ReturnType<Web3ReactHooks['useIsActivating']>
Expand Down
4 changes: 3 additions & 1 deletion example/components/ConnectWithSelect.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import type { CoinbaseWallet } from '@web3-react/coinbase-wallet'
import type { Web3ReactHooks } from '@web3-react/core'
import { GnosisSafe } from '@web3-react/gnosis-safe'
import { Ledger } from '@web3-react/ledger'
import type { MetaMask } from '@web3-react/metamask'
import { Network } from '@web3-react/network'
import { WalletConnect } from '@web3-react/walletconnect'
Expand Down Expand Up @@ -48,7 +49,7 @@ export function ConnectWithSelect({
error,
setError,
}: {
connector: MetaMask | WalletConnect | WalletConnectV2 | CoinbaseWallet | Network | GnosisSafe
connector: Ledger | MetaMask | WalletConnect | WalletConnectV2 | CoinbaseWallet | Network | GnosisSafe
activeChainId: ReturnType<Web3ReactHooks['useChainId']>
chainIds?: ReturnType<Web3ReactHooks['useChainId']>[]
isActivating: ReturnType<Web3ReactHooks['useIsActivating']>
Expand Down Expand Up @@ -86,6 +87,7 @@ export function ConnectWithSelect({
if (desiredChainId === -1 || connector instanceof GnosisSafe) {
await connector.activate()
} else if (
connector instanceof Ledger ||
connector instanceof WalletConnectV2 ||
connector instanceof WalletConnect ||
connector instanceof Network
Expand Down
5 changes: 4 additions & 1 deletion example/components/ProviderExample.tsx
Original file line number Diff line number Diff line change
@@ -1,18 +1,21 @@
import type { CoinbaseWallet } from '@web3-react/coinbase-wallet'
import { useWeb3React, Web3ReactHooks, Web3ReactProvider } from '@web3-react/core'
import type { Ledger } from '@web3-react/ledger'
import type { MetaMask } from '@web3-react/metamask'
import type { Network } from '@web3-react/network'
import type { WalletConnect } from '@web3-react/walletconnect'
import type { WalletConnect as WalletConnectV2 } from '@web3-react/walletconnect-v2'

import { coinbaseWallet, hooks as coinbaseWalletHooks } from '../connectors/coinbaseWallet'
import { hooks as ledgerHooks, ledger } from '../connectors/ledger'
import { hooks as metaMaskHooks, metaMask } from '../connectors/metaMask'
import { hooks as networkHooks, network } from '../connectors/network'
import { hooks as walletConnectHooks, walletConnect } from '../connectors/walletConnect'
import { hooks as walletConnectV2Hooks, walletConnectV2 } from '../connectors/walletConnectV2'
import { getName } from '../utils'

const connectors: [MetaMask | WalletConnect | WalletConnectV2 | CoinbaseWallet | Network, Web3ReactHooks][] = [
const connectors: [Ledger | MetaMask | WalletConnect | WalletConnectV2 | CoinbaseWallet | Network, Web3ReactHooks][] = [
[ledger, ledgerHooks],
[metaMask, metaMaskHooks],
[walletConnect, walletConnectHooks],
[walletConnectV2, walletConnectV2Hooks],
Expand Down
44 changes: 44 additions & 0 deletions example/components/connectorCards/LedgerCard.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import { useEffect, useState } from 'react'

import { MAINNET_CHAINS } from '../../chains'
import { hooks, ledger } from '../../connectors/ledger'
import { Card } from '../Card'

const CHAIN_IDS = Object.keys(MAINNET_CHAINS).map(Number)

const { useChainId, useAccounts, useIsActivating, useIsActive, useProvider, useENSNames } = hooks

export default function LedgerCard() {
const chainId = useChainId()
const accounts = useAccounts()
const isActivating = useIsActivating()

const isActive = useIsActive()

const provider = useProvider()
const ENSNames = useENSNames(provider)

const [error, setError] = useState(undefined)

// attempt to connect eagerly on mount
useEffect(() => {
ledger.connectEagerly().catch((error) => {
console.debug('Failed to connect eagerly to Ledger', error)
})
}, [])

return (
<Card
connector={ledger}
activeChainId={chainId}
chainIds={CHAIN_IDS}
isActivating={isActivating}
isActive={isActive}
error={error}
setError={setError}
accounts={accounts}
provider={provider}
ENSNames={ENSNames}
/>
)
}
18 changes: 18 additions & 0 deletions example/connectors/ledger.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { initializeConnector } from "@web3-react/core";
import { Ledger } from "@web3-react/ledger";

import { MAINNET_CHAINS } from "../chains";

const [mainnet, ...optionalChains] = Object.keys(MAINNET_CHAINS).map(Number);

export const [ledger, hooks] = initializeConnector<Ledger>(
(actions) =>
new Ledger({
actions,
options: {
projectId: process.env.walletConnectProjectId,
chains: [mainnet],
optionalChains,
},
})
);
2 changes: 2 additions & 0 deletions example/pages/index.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import LedgerCard from '../components/connectorCards/LedgerCard'
import CoinbaseWalletCard from '../components/connectorCards/CoinbaseWalletCard'
import GnosisSafeCard from '../components/connectorCards/GnosisSafeCard'
import MetaMaskCard from '../components/connectorCards/MetaMaskCard'
Expand All @@ -10,6 +11,7 @@ export default function Home() {
<>
<ProviderExample />
<div style={{ display: 'flex', flexFlow: 'wrap', fontFamily: 'sans-serif' }}>
<LedgerCard />
<MetaMaskCard />
<WalletConnectV2Card />
<CoinbaseWalletCard />
Expand Down
2 changes: 2 additions & 0 deletions example/utils.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,14 @@
import { CoinbaseWallet } from '@web3-react/coinbase-wallet'
import { GnosisSafe } from '@web3-react/gnosis-safe'
import { Ledger } from '@web3-react/ledger'
import { MetaMask } from '@web3-react/metamask'
import { Network } from '@web3-react/network'
import type { Connector } from '@web3-react/types'
import { WalletConnect as WalletConnect } from '@web3-react/walletconnect'
import { WalletConnect as WalletConnectV2 } from '@web3-react/walletconnect-v2'

export function getName(connector: Connector) {
if (connector instanceof Ledger) return 'Ledger'
if (connector instanceof MetaMask) return 'MetaMask'
if (connector instanceof WalletConnectV2) return 'WalletConnect V2'
if (connector instanceof WalletConnect) return 'WalletConnect'
Expand Down
1 change: 1 addition & 0 deletions packages/ledger/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
# @web3-react/ledger
31 changes: 31 additions & 0 deletions packages/ledger/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
{
"name": "@web3-react/ledger",
"keywords": [
"web3-react",
"ledger",
"walletconnect"
],
"author": "Ledger SAS <ledger.com>",
"license": "GPL-3.0-or-later",
"repository": "github:Uniswap/web3-react",
"publishConfig": {
"access": "public"
},
"version": "1.0.0",
"files": [
"dist/*"
],
"type": "commonjs",
"types": "./dist/index.d.ts",
"main": "./dist/index.js",
"exports": "./dist/index.js",
"scripts": {
"prebuild": "rm -rf dist",
"build": "tsc",
"start": "tsc --watch"
},
"dependencies": {
"@ledgerhq/connect-kit": "1.1.8",
"@web3-react/types": "^8.2.0"
}
}
174 changes: 174 additions & 0 deletions packages/ledger/src/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,174 @@
import { Connector } from "@web3-react/types";

import { LedgerConstructorArgs, LedgerOptions, LedgerProvider } from "./type";

export const URI_AVAILABLE = "URI_AVAILABLE";

function parseChainId(chainId: string | number) {
return typeof chainId === "string" ? Number.parseInt(chainId, 16) : chainId;
}

const isLogActive = false;

export class Ledger extends Connector {
private readonly defaultChainId: number = 1;
private readonly options: LedgerOptions;
private connectKit?: any;
public provider?: LedgerProvider;

constructor({ actions, options, onError }: LedgerConstructorArgs) {
isLogActive && console.log("Ledger Connector Constructor: Initializing...");
super(actions, onError);
this.options = options;
}

private chainChangedListener = (chainId: string): void => {
isLogActive &&
console.log("chainChangedListener: Handling chain changed event...");
this.actions.update({ chainId: Number.parseInt(chainId, 16) });
};

private accountsChangedListener = (accounts: string[]): void => {
isLogActive &&
console.log(
"accountsChangedListener: Handling accounts changed event..."
);
this.actions.update({ accounts });
};

private async isomorphicInitialize() {
console.group("isomorphicInitialize method");
isLogActive && console.log("isomorphicInitialize: Loading provider...");
try {
if (this.provider) return this.provider;
this.connectKit = require('@ledgerhq/connect-kit');

const {
projectId,
chains,
optionalChains,
requiredMethods,
optionalMethods,
requiredEvents,
optionalEvents,
rpcMap = {
1: "https://cloudflare-eth.com/", // Mainnet
5: "https://goerli.optimism.io/", // Goerli
137: "https://polygon-rpc.com/", // Polygon
},
} = this.options;

this.connectKit.checkSupport({
providerType: "Ethereum",
walletConnectVersion: 2,
projectId,
chains,
optionalChains,
methods: requiredMethods,
optionalMethods,
events: requiredEvents,
optionalEvents,
rpcMap,
});
this.connectKit.enableDebugLogs();

const provider: LedgerProvider = (this.provider =
(await this.connectKit.getProvider()) as LedgerProvider);
provider.on("chainChanged", this.chainChangedListener);
provider.on("accountsChanged", this.accountsChangedListener);

return provider;
} finally {
console.groupEnd();
}
}

async connectEagerly() {
isLogActive && console.log("connectEagerly: Connecting eagerly...");

try {
this.provider = await this.isomorphicInitialize();
if (!this.provider.session) {
throw new Error("No active session found. Connect your wallet first.");
}

const [chainId, accounts] = await Promise.all([
this.provider.request({ method: "eth_chainId" }) as Promise<string>,
this.provider.request({ method: "eth_accounts" }) as Promise<string[]>,
]);

this.actions.update({ chainId: parseChainId(chainId), accounts });
} catch (error) {
console.debug("connectEagerly: Could not connect eagerly", error);
await this.deactivate();
}
}

public async activate(
desiredChainId: number = this.defaultChainId
): Promise<void> {
console.group("activate method");
isLogActive && console.group("activate: Activating...");

try {
this.provider = await this.isomorphicInitialize();

const { request }: { request: any } = this.provider;

if (this.provider.accounts?.length === 0) {
const accounts = (await request({
method: "eth_requestAccounts",
})) as string[];

const chainId = (await request({
method: "eth_chainId",
})) as string;

this.actions.update({ chainId: parseChainId(chainId), accounts });
return;
}

if (desiredChainId === this.provider.chainId) return;

const isConnectedToDesiredChain =
this.provider.session?.namespaces?.eip155.accounts.some(
(account: string) => account.startsWith(`eip155:${desiredChainId}:`)
);

if (!isConnectedToDesiredChain) {
if (this.options.optionalChains?.includes(desiredChainId)) {
throw new Error(
`Cannot activate an optional chain (${desiredChainId}), as the wallet is not connected to it.\n\tYou should handle this error in application code, as there is no guarantee that a wallet is connected to a chain configured in "optionalChains".`
);
}
throw new Error(
`Unknown chain (${desiredChainId}). Make sure to include any chains you might connect to in the "chains" or "optionalChains" parameters when initializing WalletConnect.`
);
}

await request({
method: "wallet_switchEthereumChain",
params: [{ chainId: "0x" + desiredChainId.toString(16) }],
});

this.actions.update({
chainId: desiredChainId,
accounts: this.provider.accounts,
});
} catch (error) {
await this.deactivate();
throw error;
} finally {
console.groupEnd();
}
}

async deactivate() {
isLogActive && console.log("deactivate: Deactivating...");
if (this.provider) {
this.provider?.disconnect?.();
this.provider = undefined;
}
this.actions.resetState();
}
}
Loading