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

Hardware wallets #184

Closed
wants to merge 14 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
27 changes: 27 additions & 0 deletions docs/testing-with-hardware-wallets.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
# Testing with hardware wallets.

## Running the dApp on HTTPS

You need to be connecting to the dApp through a HTTPS connection. We can do this by using `mitmproxy` -

`mitmdump -p 443 --mode reverse:http://localhost:3000/`

Then open the app on [https://localhost](https://localhost).

## Ledger

To-Do.

## Trezor

### Setup Software

- Install and [run the emulator](https://docs.trezor.io/trezor-firmware/core/emulator/index.html)
- Install and run the [Trezor bridge daemon](https://github.com/trezor/trezord-go)


### Send funds

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

Now you're ready!
7 changes: 6 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,11 @@
"name": "tbtc-dapp",
"version": "0.12.0-pre",
"dependencies": {
"@0x/subproviders": "^6.0.8",
"@keep-network/tbtc.js": ">0.12.0-pre <0.12.0-rc",
"@ledgerhq/hw-app-eth": "^5.12.2",
"@ledgerhq/hw-transport-u2f": "^5.11.0",
Shadowfiend marked this conversation as resolved.
Show resolved Hide resolved
"@ledgerhq/web3-subprovider": "^5.11.0",
"@web3-react/core": "^6.0.7",
"@web3-react/injected-connector": "^6.0.7",
"bignumber.js": "^9.0.0",
Expand All @@ -18,7 +22,8 @@
"react-scripts": "3.0.1",
"redux": "^4.0.4",
"redux-saga": "^1.0.5",
"web3": "^1.2.6"
"web3": "^1.2.6",
"web3-provider-engine": "^15.0.6"
},
"scripts": {
"start-js": "react-scripts start",
Expand Down
4 changes: 4 additions & 0 deletions public/images/ledger.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
101 changes: 95 additions & 6 deletions src/components/lib/ConnectWalletDialog.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,34 @@ import React, { Component, useReducer, useState } from 'react'
import Check from '../svgs/Check'
import { useWeb3React } from '@web3-react/core'
import { InjectedConnector } from '@web3-react/injected-connector'
import { LedgerConnector } from '../../connectors/ledger'

const CHAINS = [
{
name: 'Mainnet',
id: 1,
},
{
name: 'Ropsten',
id: 3,
},
{
name: 'Rinkeby',
id: 4,
},
{
name: 'Kovan',
id: 42,
},
{
name: 'Ganache',
id: 1337,
},
{
name: 'Ganache',
id: 123,
}
]

const SUPPORTED_CHAIN_IDS = [
// Mainnet
Expand All @@ -22,17 +50,38 @@ const injectedConnector = new InjectedConnector({
supportedChainIds: SUPPORTED_CHAIN_IDS
})

const ledgerConnector = new LedgerConnector({
// We use the chainId of mainnet here to workaround an issue with the ledgerjs library.
Shadowfiend marked this conversation as resolved.
Show resolved Hide resolved
// It currently throws an error for the default chainId of 1377 used by Geth/Ganache.
Copy link
Contributor

Choose a reason for hiding this comment

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

Our chainId is 1101 though, no?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yah - we may have changed the default for Geth? In either case, they're both >8 bits.

Copy link
Contributor

Choose a reason for hiding this comment

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

Or geth changed theirs. Can't remember for sure.

//
// The `v` value in ECDSA sigs is typically used as a recovery ID, but we also encode it
// differently depending on the chain to prevent transaction replay (the so called chainId of EIP155).
//
// At some point, Ledger had to update their firmware, to swap from a uint8 chainId to a uint32 chainId [1].
//
// They updated their client library with a 'workaround' [2], but it doesn't appear to work.
Copy link
Contributor

Choose a reason for hiding this comment

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

This workaround actually seems to do the opposite, masking any >8-bit chain id to only keep the lowest 8 bits. Guessing it's designed not to screw up when communicating with the ledger, rather than being designed to support larger chain ids?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Guessing it's designed not to screw up when communicating with the ledger, rather than being designed to support larger chain ids?

Yup. Poor wording on my end, deriving from a poor understanding. Basically, Ledger has implemented 32 bit chainID support in their device firmware, but the Ledger Ethereum app still transports the truncated 8 bit ID, I'm guessing. eg. one chain, Piri, has its 32 bit chainID working.

That and my assumptions were wrong - the LedgerSubprovider from 0x uses an older version of @ledgerhq/hw-app-eth, without this workaround. Soooo, going to address that in a bit.

//
// [1]: https://github.com/LedgerHQ/ledger-app-eth/commit/8260268b0214810872dabd154b476f5bb859aac0
// [2]: https://github.com/LedgerHQ/ledgerjs/blob/master/packages/web3-subprovider/src/index.js#L143
chainId: 1,
url: 'ws://localhost:8545'
})
// Wallets.
const WALLETS = [
{
name: "Metamask",
icon: "/images/metamask-fox.svg",
showName: true
},
{
name: "Ledger",
icon: "/images/ledger.svg"
},
}
]

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

let [chosenWallet, setChosenWallet] = useState(null)
let [error, setError] = useState(null)
Expand All @@ -44,8 +93,15 @@ export const ConnectWalletDialog = ({ shown, onConnected }) => {
async function chooseWallet(wallet) {
setChosenWallet(wallet)

let connector
if (wallet == 'Ledger') {
connector = ledgerConnector
} else if (wallet == 'Metamask') {
connector = injectedConnector
Copy link
Contributor

Choose a reason for hiding this comment

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

This may be a good opportunity to use an object to map wallet name -> connector.

}

try {
await activate(injectedConnector, undefined, true)
await activate(connector, undefined, true)
onConnected()
} catch(ex) {
setError(ex.toString())
Expand All @@ -56,7 +112,7 @@ export const ConnectWalletDialog = ({ shown, onConnected }) => {
const ChooseWalletStep = () => {
return <>
<header>
<div className="title">Connect To A Wallet</div>
<div className="title">Connect to a wallet</div>
</header>
<p>This wallet will be used to sign transactions on Ethereum.</p>

Expand All @@ -74,22 +130,52 @@ export const ConnectWalletDialog = ({ shown, onConnected }) => {
}

const ConnectToWalletStep = () => {
if(error) {
Copy link
Contributor

Choose a reason for hiding this comment

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

Let's at least log this error… We had this issue in the token dApp also where we were displaying a super-generic error and there was no easy way to get to the underlying one.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yep, got that covered. The error is set in chooseWallet, where it is thrown immediately after -

https://github.com/keep-network/tbtc-dapp/blob/master/src/components/lib/ConnectWalletDialog.js#L52

return <ErrorConnecting/>
}

if(chosenWallet == 'Ledger') {
return <>
<header>
<div className="title">Plug In Ledger & Enter Pin</div>
Copy link
Contributor

Choose a reason for hiding this comment

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

Do we need an extra div here?

</header>
<p>Open Ethereum application and make sure Contract Data and Browser Support are enabled.</p>
<p>Connecting...</p>
</>
}

return <>
<header>
<div className="title">Connect To A Wallet</div>
<div className="title">Connect to a wallet</div>
Copy link
Contributor

Choose a reason for hiding this comment

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

As above re: the div.

</header>
<p>Connecting to {chosenWallet} wallet...</p>
</>
}

const ErrorConnecting = () => {
return <>
<header>
<div className="title">Connect to a wallet</div>
Copy link
Contributor

Choose a reason for hiding this comment

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

F' three.

</header>
<p>Error connecting to {chosenWallet} wallet...</p>
<a onClick={async () => {
setError(null)
await chooseWallet(chosenWallet)
}}>
Try Again
</a>
{ error && <p>{error}</p> }
</>
}

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

Choose a reason for hiding this comment

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

F' fo'.

</header>

<div className='details'>
<p>Chain: {CHAINS.filter(chain => chain.id == chainId)[0].name}</p>
<p>{chosenWallet}</p>
<p>
{account}
Expand All @@ -101,6 +187,9 @@ export const ConnectWalletDialog = ({ shown, onConnected }) => {
return <div>
<div className={`modal connect-wallet ${shown ? 'open' : 'closed'}`}>
<div className="modal-body">
<div className="close">
<div className="x" onClick={onClose}>&#9587;</div>
</div>
Copy link
Contributor

Choose a reason for hiding this comment

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

I would rather prefer a boutonnière, my dear sir.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I'll chuck that in another PR - we've still got the lingering global styles on <button>.

{!chosenWallet && <ChooseWalletStep />}
{(chosenWallet && !active) && <ConnectToWalletStep />}
{(chosenWallet && active) && <ConnectedView />}
Expand Down
7 changes: 5 additions & 2 deletions src/components/lib/Web3Status.js
Original file line number Diff line number Diff line change
Expand Up @@ -23,13 +23,16 @@ export const Web3Status = (props) => {
}

else if(active) {
body = <div className="web3-status success">
body = <div className="web3-status success" onClick={() => setShowConnectWallet(true)}>
<Check width="15px" /> Connected
</div>
}

return <div>
<ConnectWalletDialog onConnected={() => setShowConnectWallet(false)} shown={showConnectWallet} />
<ConnectWalletDialog
onConnected={() => setShowConnectWallet(false)}
onClose={() => setShowConnectWallet(false)}
shown={showConnectWallet} />
{body}
</div>
}
Expand Down
92 changes: 92 additions & 0 deletions src/connectors/ledger.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
import createLedgerSubprovider from "@ledgerhq/web3-subprovider"
import TransportWebUSB from "@ledgerhq/hw-transport-webusb"
import TransportU2F from "@ledgerhq/hw-transport-u2f"
import AppEth from '@ledgerhq/hw-app-eth'
import { ConnectorUpdate } from '@web3-react/types'
import { AbstractConnector } from '@web3-react/abstract-connector'
import Web3ProviderEngine from 'web3-provider-engine'
import { LedgerSubprovider } from '@0x/subproviders/lib/src/subproviders/ledger' // https://github.com/0xProject/0x-monorepo/issues/1400
import CacheSubprovider from 'web3-provider-engine/subproviders/cache.js'
import { RPCSubprovider } from '@0x/subproviders/lib/src/subproviders/rpc_subprovider' // https://github.com/0xProject/0x-monorepo/issues/1400
import WebsocketSubprovider from 'web3-provider-engine/subproviders/websocket'

/**
* An implementation of a LedgerConnector for web3-react, based on the original
* `@web3-react/ledger-connector`.
*
* Some differences:
*
* 1. The original doesn't expose the LedgerJS client API.
* We will probably want access to this in future, eg. signing BTC transactions
*
* 2. The original doesn't work with event subscriptions, as it assumes a HTTP RPC
* endpoint. Event subscriptions use `eth_subscribe`, which Ganache does not
* support out-of-the-box. Assuming a Websocket provider is simpler for our case.
*/
export class LedgerConnector extends AbstractConnector {
constructor({
chainId,
url,
pollingInterval,
requestTimeoutMs,
accountFetchingConfigs,
baseDerivationPath
}) {
super({ supportedChainIds: [chainId] })

this.chainId = chainId
this.url = url
this.pollingInterval = pollingInterval
this.requestTimeoutMs = requestTimeoutMs
this.accountFetchingConfigs = accountFetchingConfigs
this.baseDerivationPath = baseDerivationPath
}

async activate(): Promise<ConnectorUpdate> {
Copy link
Contributor

Choose a reason for hiding this comment

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

Uh-oh, looks like some TypeScript annotations sneaked through on this JS file.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Weirdly enough, they didn't break my build? create-react-app / babel must have some support, somewhere in those depths of hell.

Copy link
Contributor

Choose a reason for hiding this comment

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

I guarantee it, yes. This is why babel makes me squeamish… I like to know what's actually going on <_<

if (!this.provider) {
let ledgerEthereumClientFactoryAsync = async () => {
Copy link
Contributor

Choose a reason for hiding this comment

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

Could be a const yah?

const ledgerConnection = await TransportU2F.create()
// Ledger will automatically timeout the U2F "sign" request after `exchangeTimeout` ms.
// This will result in a cryptic error:
// `{name: "TransportError", message: "Failed to sign with Ledger device: U2F DEVICE_INELIGIBLE", ...}`
// Setting the exchange timeout fixes that, although I haven't seen it documented anywhere else in the Ledger docs.
Copy link
Contributor

Choose a reason for hiding this comment

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

Fixes it in what sense? The error message is better?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Oops. exchangeTimeout is set unrealistically low by default (5s or so).

ledgerConnection.setExchangeTimeout(100000)
const ledgerEthClient = new AppEth(ledgerConnection)
return ledgerEthClient
}

const engine = new Web3ProviderEngine({ pollingInterval: this.pollingInterval })
engine.addProvider(
new LedgerSubprovider({
networkId: this.chainId,
ledgerEthereumClientFactoryAsync,
Copy link
Contributor

Choose a reason for hiding this comment

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

What a property name.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I mean it's no AbstractSingletonProxyFactoryBean... 😄

accountFetchingConfigs: this.accountFetchingConfigs,
baseDerivationPath: this.baseDerivationPath
})
)
engine.addProvider(new CacheSubprovider())
engine.addProvider(new WebsocketSubprovider({ rpcUrl: this.url }))
this.provider = engine
}

this.provider.start()

return { provider: this.provider, chainId: this.chainId }
}

async getProvider(): Promise<Web3ProviderEngine> {
return this.provider
}

async getChainId(): Promise<number> {
return this.chainId
}

async getAccount(): Promise<null> {
return this.provider._providers[0].getAccountsAsync(1).then((accounts: string[]): string => accounts[0])
}

deactivate() {
this.provider.stop()
}
}