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 3 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
44 changes: 44 additions & 0 deletions docs/testing-with-hardware-wallets.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
# 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

### Install and run emulator
Shadowfiend marked this conversation as resolved.
Show resolved Hide resolved

```bash
git clone --recurse-submodules https://github.com/trezor/trezor-firmware.git\n
cd trezor-firmware/core
make build_unix
./emu.py
```

### Install and run the `trezord` daemon

```bash
go get github.com/trezor/trezord-go
go build github.com/trezor/trezord-go
./trezord-go -e 21324
```

### Setup Trezor Wallet

In order to start using Bitcoin testnet with Trezor, you need to run custom backend in Trezor Wallet.

Follow the instructions in [their guide](https://wiki.trezor.io/Bitcoin_testnet).
Shadowfiend marked this conversation as resolved.
Show resolved Hide resolved

### Send funds

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

Now you're ready!
4 changes: 4 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,9 @@
"version": "0.12.0-pre",
"dependencies": {
"@keep-network/tbtc.js": ">0.12.0-pre <0.12.0-rc",
"@ledgerhq/hw-app-eth": "^5.11.0",
"@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,6 +21,7 @@
"react-scripts": "3.0.1",
"redux": "^4.0.4",
"redux-saga": "^1.0.5",
"web3-provider-engine": "^15.0.6",
"web3": "^1.2.6"
},
"scripts": {
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.
31 changes: 30 additions & 1 deletion src/components/lib/ConnectWalletDialog.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ 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 SUPPORTED_CHAIN_IDS = [
// Mainnet
Expand All @@ -22,12 +23,33 @@ 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"
},
}
]

Expand All @@ -44,8 +66,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 Down
114 changes: 114 additions & 0 deletions src/connectors/ledger.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
import Web3 from "web3";
import createLedgerSubprovider from "@ledgerhq/web3-subprovider";
import TransportU2F from "@ledgerhq/hw-transport-u2f";
import ProviderEngine from "web3-provider-engine";
import WebsocketSubprovider from 'web3-provider-engine/subproviders/websocket'
import { AbstractConnector } from '@web3-react/abstract-connector'


/**
* 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 HttpProvider does not
* implement out-of-the-box. There are some packages, such as Metamask's
* eth-json-rpc-filters, which will implement a middleware to achieve this. Assuming
* a Websocket provider is simpler for our case.
*/
export class LedgerConnector extends AbstractConnector {
constructor({
chainId,
url,
pollingInterval,
requestTimeoutMs,
accountFetchingConfigs,
baseDerivationPath
}) {
super({
supportedChainIds: [chainId]
Copy link
Contributor

Choose a reason for hiding this comment

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

To what extent does the issue with chain ids above break our ability to interact with our internal testnet, out of curiosity?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Just swapping out the current subproviders with ones provided by 0x - they provided more consistent exceptions, that I wasn't hellbent on reimplementing.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

They may not support the larger chainID. We'll see what we can do.

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

this.ledgerSubprovider = null
}

/**
* @returns {Promise<Object>}
*/
async activate(): Promise {
if (!this.provider) {
const engine = new ProviderEngine();

const getTransport = () => TransportU2F.create();
const ledger = createLedgerSubprovider(getTransport, {
accountsLength: 1,
networkId: await this.getChainId()
})

this.ledgerSubprovider = ledger

engine.addProvider(ledger)

engine.addProvider(new WebsocketSubprovider({
rpcUrl: this.url
}))

engine.start()

this.provider = engine
}

this.provider.start()

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

/**
* @returns {Promise<Web3ProviderEngine>}
*/
async getProvider() {
return this.provider
}

/**
* @returns {Promise<number>}
*/
async getChainId() {
return this.chainId
}

/**
* @returns {Promise<null>}
Copy link
Contributor

Choose a reason for hiding this comment

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

An explanation of what this promise represents would be ideal 😬

Also I tend to use @return rather than @returns. @return is more widely and historically supported both in JS and across languages, so it's just a bit easier of a habit 🤷‍♂️

*/
async getAccount() {
if (!this.provider) {
return null
}

return new Promise((resolve, reject) => {
console.debug(`Ledger - loading accounts...`)
this.ledgerSubprovider.getAccounts((error, accounts) => {
if(error) {
return reject(error)
}
console.debug(`Ledger - loaded ${accounts.length} accounts...`)
resolve(accounts[0])
Shadowfiend marked this conversation as resolved.
Show resolved Hide resolved
})
})
}

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