Skip to content
This repository has been archived by the owner on Mar 28, 2023. It is now read-only.

Commit

Permalink
Merge pull request #205 from keep-network/heath-ledger
Browse files Browse the repository at this point in the history
Heath Ledger: Add Ledger wallet support

This PR implements Ledger and Trezor hardware wallet support. Due to 0x and
ledgerjs API's not fully supporting chainId's greater than 255 (ie. our geth testnets),
we had to implement a custom LedgerSubprovider for signing transactions.

There's also some cleanup of the ChooseWalletDialog, based on previous feedback.
  • Loading branch information
Shadowfiend committed Aug 6, 2020
2 parents e3e5e36 + a06cb50 commit 47675e9
Show file tree
Hide file tree
Showing 18 changed files with 16,972 additions and 201 deletions.
21 changes: 21 additions & 0 deletions docs/testing-with-hardware-wallets.md
@@ -0,0 +1,21 @@
# Testing with hardware wallets.

## Running the dApp on HTTPS

To test with hardware wallets, the dApp must be served over HTTPS. Run `HTTPS=true npm start` and access it at https://localhost:3000.

### Hardware wallets

## Trezor

To run the trezor hardware wallet emulator, follow these steps:

1. [Download and run the trezor bridge](https://github.com/trezor/trezord-go).
2. [Clone the Trezor repo](https://github.com/trezor/trezor-firmware/blob/master/docs/core/build/index.md)
3. [Setup and run the emulator](https://github.com/trezor/trezor-firmware/blob/master/docs/core/emulator/index.md)

Connect to the Trezor in Metamask, and deposit some ether for testing.

## Ledger

We suggest testing with a physical Ledger wallet.
13 changes: 13 additions & 0 deletions infrastructure/kube/keep-dev/tbtc-dapp-deployment.yaml
Expand Up @@ -25,3 +25,16 @@ spec:
image: gcr.io/keep-dev-fe24/tbtc-dapp
ports:
- containerPort: 80
env:
# TODO: ETH_WS_URL needs to be using wss:// instead of ws://, otherwise
# insecure connection will be blocked.
- name: ETH_WS_URL
valueFrom:
configMapKeyRef:
name: eth-network-internal
key: ws-url
- name: ETH_CHAIN_ID
valueFrom:
configMapKeyRef:
name: eth-network-internal
key: network-id
11 changes: 11 additions & 0 deletions infrastructure/kube/keep-test/tbtc-dapp-deployment.yaml
Expand Up @@ -25,3 +25,14 @@ spec:
image: gcr.io/keep-test-f3e0/tbtc-dapp
ports:
- containerPort: 80
env:
- name: ETH_WS_URL
valueFrom:
configMapKeyRef:
name: eth-network-ropsten
key: ws-url
- name: ETH_CHAIN_ID
valueFrom:
configMapKeyRef:
name: eth-network-ropsten
key: network-id
16,399 changes: 16,270 additions & 129 deletions package-lock.json

Large diffs are not rendered by default.

8 changes: 8 additions & 0 deletions package.json
Expand Up @@ -3,11 +3,17 @@
"version": "0.16.4-rc",
"dependencies": {
"@keep-network/tbtc.js": ">0.18.0-rc <0.18.0",
"@0x/subproviders": "^6.0.8",
"@ledgerhq/hw-app-eth": "^5.12.2",
"@ledgerhq/hw-transport-u2f": "^5.11.0",
"@ledgerhq/web3-subprovider": "^5.11.0",
"@web3-react/core": "^6.0.7",
"@web3-react/injected-connector": "^6.0.7",
"bignumber.js": "^9.0.0",
"classnames": "^2.2.6",
"console.history": "^1.5.1",
"ethereumjs-common": "^1.5.0",
"ethereumjs-tx": "^2.1.2",
"history": "^4.9.0",
"node-sass": "^4.13.1",
"npm-run-all": "^4.1.2",
Expand All @@ -21,7 +27,9 @@
"redux": "^4.0.4",
"redux-devtools-extension": "^2.13.8",
"redux-saga": "^1.0.5",
"trezor-connect": "^8.1.1",
"web3": "^1.2.6",
"web3-provider-engine": "^15.0.6",
"web3-utils": "^1.2.8"
},
"scripts": {
Expand Down
20 changes: 20 additions & 0 deletions public/images/ledger.svg
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
20 changes: 20 additions & 0 deletions public/images/trezor.svg
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
226 changes: 160 additions & 66 deletions src/components/lib/ConnectWalletDialog.js
@@ -1,105 +1,199 @@
import React, { useState } from 'react'
import { useWeb3React } from '@web3-react/core'
import { InjectedConnector } from '@web3-react/injected-connector'
import { LedgerConnector } from '../../connectors/ledger'
import { TrezorConnector } from '../../connectors/trezor'
import { getChainId, getWsUrl } from '../../connectors/utils'

const SUPPORTED_CHAIN_IDS = [
// Mainnet
1,
// Ropsten
3,
// Rinkeby
4,
// Dev chains (Ganache, Geth)
123, // Low chainId to workaround ledgerjs signing issues.
1337,
// Keep testnet
1101
]
const CHAIN_ID = getChainId()
const ETH_RPC_URL = getWsUrl()

// Connectors.
const injectedConnector = new InjectedConnector({
supportedChainIds: SUPPORTED_CHAIN_IDS
const injectedConnector = new InjectedConnector({})

const ledgerLiveConnector = new LedgerConnector({
chainId: CHAIN_ID,
url: ETH_RPC_URL,
baseDerivationPath: "44'/60'/0'/0",
})

const ledgerLegacyConnector = new LedgerConnector({
chainId: CHAIN_ID,
url: ETH_RPC_URL,
baseDerivationPath: "44'/60'/0'",
})

const trezorConnector = new TrezorConnector({
chainId: CHAIN_ID,
pollingInterval: 1000,
requestTimeoutMs: 1000,
config: {
chainId: CHAIN_ID,
},
url: ETH_RPC_URL,
manifestEmail: 'contact@keep.network',
manifestAppUrl: 'https://localhost'
})

// Wallets.
const WALLETS = [
{
name: "Metamask",
icon: "/images/metamask-fox.svg",
showName: true
connector: injectedConnector
},
{
name: "Ledger Legacy",
icon: "/images/ledger.svg",
connector: ledgerLegacyConnector,
isHardwareWallet: true,
},
{
name: "Ledger Live",
icon: "/images/ledger.svg",
connector: ledgerLiveConnector,
isHardwareWallet: true,
},
{
name: "Trezor",
icon: "/images/trezor.svg",
connector: trezorConnector,
isHardwareWallet: true,
}
]

export const ConnectWalletDialog = ({ shown, onConnected }) => {

export const ConnectWalletDialog = ({ shown, onConnected, onClose }) => {
const { active, account, activate } = useWeb3React()

let [chosenWallet, setChosenWallet] = useState(null)
let [chosenWallet, setChosenWallet] = useState({})
let [error, setError] = useState(null)
const [availableAccounts, setAvailableAccounts] = useState([])

async function chooseWallet(wallet) {
setChosenWallet(wallet)
try {
setChosenWallet(wallet)
if(wallet.isHardwareWallet) {
await wallet.connector.activate()
setAvailableAccounts(await wallet.connector.getAccounts())
} else {
await activateProvider(null, wallet)
}
} catch(error) {
setError(error.toString())
}
}

const activateProvider = async (selectedAccount, wallet = chosenWallet) => {
try {
await activate(injectedConnector, undefined, true)
if(wallet.isHardwareWallet) {
wallet.connector.setDefaultAccount(selectedAccount)
}
await activate(wallet.connector, undefined, true)
onConnected()
} catch(ex) {
setError(ex.toString())
throw ex
}
}

const ChooseWalletStep = () => {
return <>
<header>
<div className="title">Connect To A Wallet</div>
</header>
<p>This wallet will be used to sign transactions on Ethereum.</p>

<ul className='wallets'>
{
WALLETS.map(({ name, icon, showName }) => {
return <li key={name} className='wallet-option' onClick={() => chooseWallet(name)}>
<img src={icon} alt={`${name} icon`} />
{showName && name}
</li>
})
}
</ul>
</>
const reconnectWallet = async () => {
setError(null)
await chooseWallet(chosenWallet)
}

const ConnectToWalletStep = () => {
return <div className={`modal connect-wallet ${shown ? 'open' : 'closed'}`}>
<div className="modal-body">
<div className="close">
<div className="x" onClick={onClose}>&#9587;</div>
</div>
{!chosenWallet.name && <ChooseWalletStep onChooseWallet={chooseWallet} />}
{(chosenWallet.name && !active) &&
<ConnectToWalletStep
wallet={chosenWallet}
error={error}onTryAgainClick={reconnectWallet}
/>}
{(chosenWallet.name && active) && <ConnectedView wallet={chosenWallet} account={account} />}
<ChooseAccount
wallet={chosenWallet}
availableAccounts={availableAccounts}
active={active}
onAccountSelect={activateProvider}
/>
</div>
</div>
}

const ChooseWalletStep = ({ onChooseWallet }) => {
return <>
<div className="title">Connect to a wallet</div>
<p>This wallet will be used to sign transactions on Ethereum.</p>

<ul className='wallets'>
{
WALLETS.map(wallet => {
return <li key={wallet.name} className='wallet-option' onClick={() => onChooseWallet(wallet)}>
<img alt="wallet-icon" src={wallet.icon} />
{wallet.name}
</li>
})
}
</ul>
</>
}

const ConnectToWalletStep = ({ error, wallet, onTryAgainClick }) => {
if(error) {
return <ErrorConnecting error={error} wallet={wallet} onTryAgainClick={onTryAgainClick} />
}

if(wallet.name.includes('Ledger')) {
return <>
<header>
<div className="title">Connect To A Wallet</div>
</header>
<p>Connecting to {chosenWallet} wallet...</p>
{ error && <p>{error}</p> }
<div className="title">Plug In Ledger & Enter Pin</div>
<p>Open Ethereum application and make sure Contract Data and Browser Support are enabled.</p>
<p>Connecting...</p>
</>
}

const ConnectedView = () => {
return <div className='connected-view'>
<header>
<div className="title">Connect To A Wallet</div>
</header>

<div className='details'>
<p>{chosenWallet}</p>
<p>
{account}
</p>
</div>
</div>
}
return <>
<div className="title">Connect to a wallet</div>
<p>Connecting to {wallet.name} wallet...</p>
</>
}

return <div>
<div className={`modal connect-wallet ${shown ? 'open' : 'closed'}`}>
<div className="modal-body">
{!chosenWallet && <ChooseWalletStep />}
{(chosenWallet && !active) && <ConnectToWalletStep />}
{(chosenWallet && active) && <ConnectedView />}
</div>
const ChooseAccount = ({ wallet, availableAccounts, active, onAccountSelect }) => {
if(wallet.isHardwareWallet && availableAccounts.length !== 0 && !active) {
return (
<>
<div className="title mb-2">Select account</div>
{availableAccounts.map(account => (
<div key={account} className="cursor-pointer mb-1" onClick={() => onAccountSelect(account)}>
{account}
</div>
))}
</>
)
}

return null
}

const ConnectedView = ({ wallet, account }) => {
return <div className='connected-view'>
<div className="title">Wallet connected</div>
<div className='details'>
<p>{wallet.name}</p>
<p>Account: {account}</p>
</div>
</div>
}

const ErrorConnecting = ({ wallet, error, onTryAgainClick }) => {
return <>
<div className="title">Connect to a wallet</div>
<p>Error connecting to {wallet.name} wallet...</p>
<span onClick={onTryAgainClick}>
Try Again
</span>
{ error && <p>{error}</p> }
</>
}

0 comments on commit 47675e9

Please sign in to comment.