From 18d65bd90b9def6a8caf3f008e8734ff67098ce3 Mon Sep 17 00:00:00 2001 From: liamzebedee Date: Thu, 2 Apr 2020 02:01:09 +1000 Subject: [PATCH 01/48] Implement a Ledger wallet connector --- package.json | 4 + public/images/ledger.svg | 4 + src/components/lib/ConnectWalletDialog.js | 31 ++++++- src/connectors/ledger.js | 106 ++++++++++++++++++++++ 4 files changed, 144 insertions(+), 1 deletion(-) create mode 100644 public/images/ledger.svg create mode 100644 src/connectors/ledger.js diff --git a/package.json b/package.json index fa908cfd..351f9ab8 100644 --- a/package.json +++ b/package.json @@ -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", + "@ledgerhq/web3-subprovider": "^5.11.0", "@web3-react/core": "^6.0.7", "@web3-react/injected-connector": "^6.0.7", "bignumber.js": "^9.0.0", @@ -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": { diff --git a/public/images/ledger.svg b/public/images/ledger.svg new file mode 100644 index 00000000..b53710a3 --- /dev/null +++ b/public/images/ledger.svg @@ -0,0 +1,4 @@ + + + + diff --git a/src/components/lib/ConnectWalletDialog.js b/src/components/lib/ConnectWalletDialog.js index aa9a4442..a484865d 100644 --- a/src/components/lib/ConnectWalletDialog.js +++ b/src/components/lib/ConnectWalletDialog.js @@ -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 @@ -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. + // It currently throws an error for the default chainId of 1377 used by Geth/Ganache. + // + // 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. + // + // [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" + }, } ] @@ -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 + } + try { - await activate(injectedConnector, undefined, true) + await activate(connector, undefined, true) onConnected() } catch(ex) { setError(ex.toString()) diff --git a/src/connectors/ledger.js b/src/connectors/ledger.js new file mode 100644 index 00000000..42719a64 --- /dev/null +++ b/src/connectors/ledger.js @@ -0,0 +1,106 @@ +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. + * + * Allows direct access to the LedgerJS client (useful for custom BTC logic), and + * uses WebSockets instead of HTTP JSON-RPC. + */ +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 + + this.ledgerSubprovider = null + } + + /** + * @returns {Promise} + */ + 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} + */ + async getProvider() { + return this.provider + } + + /** + * @returns {Promise} + */ + async getChainId() { + return this.chainId + } + + /** + * @returns {Promise} + */ + 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]) + }) + }) + } + + deactivate() { + this.provider.stop() + } +} From 0c615d5a97da46319d5193dacc5466e811cf628b Mon Sep 17 00:00:00 2001 From: liamzebedee Date: Thu, 2 Apr 2020 02:56:50 +1000 Subject: [PATCH 02/48] Improve doc for LedgerConnector --- src/connectors/ledger.js | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/src/connectors/ledger.js b/src/connectors/ledger.js index 42719a64..0969c109 100644 --- a/src/connectors/ledger.js +++ b/src/connectors/ledger.js @@ -8,10 +8,18 @@ import { AbstractConnector } from '@web3-react/abstract-connector' /** * An implementation of a LedgerConnector for web3-react, based on the original - * @web3-react/ledger-connector. + * `@web3-react/ledger-connector`. * - * Allows direct access to the LedgerJS client (useful for custom BTC logic), and - * uses WebSockets instead of HTTP JSON-RPC. + * 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({ From afaec4ebd158d199e52e6fc884947912f79169e2 Mon Sep 17 00:00:00 2001 From: liamzebedee Date: Fri, 3 Apr 2020 02:19:00 +1000 Subject: [PATCH 03/48] Begin documenting the process of testing the hardware wallets Feeling kinda documenting, might delete later. --- docs/testing-with-hardware-wallets.md | 44 +++++++++++++++++++++++++++ 1 file changed, 44 insertions(+) create mode 100644 docs/testing-with-hardware-wallets.md diff --git a/docs/testing-with-hardware-wallets.md b/docs/testing-with-hardware-wallets.md new file mode 100644 index 00000000..b7ed76a7 --- /dev/null +++ b/docs/testing-with-hardware-wallets.md @@ -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 + +```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). + +### Send funds + +Connect to the wallet in Metamask, and deposit some ether for testing. + +Now you're ready! \ No newline at end of file From 0b9cdac2cf688f6bd3e261b41a610562a96af602 Mon Sep 17 00:00:00 2001 From: liamzebedee Date: Mon, 6 Apr 2020 20:07:58 +1000 Subject: [PATCH 04/48] Update Trezor links to their documentation --- docs/testing-with-hardware-wallets.md | 25 ++++--------------------- 1 file changed, 4 insertions(+), 21 deletions(-) diff --git a/docs/testing-with-hardware-wallets.md b/docs/testing-with-hardware-wallets.md index b7ed76a7..881bdb9e 100644 --- a/docs/testing-with-hardware-wallets.md +++ b/docs/testing-with-hardware-wallets.md @@ -14,31 +14,14 @@ To-Do. ## Trezor -### Install and run emulator +### Setup Software -```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 emulator](https://docs.trezor.io/trezor-firmware/core/emulator/index.html) + - Install and run the [Trezor bridge daemon](https://github.com/trezor/trezord-go) -### 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). ### Send funds -Connect to the wallet in Metamask, and deposit some ether for testing. +Connect to the Trezor in Metamask, and deposit some ether for testing. Now you're ready! \ No newline at end of file From e2302ae8c1a46083d80312bead53a34cd90d639e Mon Sep 17 00:00:00 2001 From: liamzebedee Date: Mon, 6 Apr 2020 23:36:36 +1000 Subject: [PATCH 05/48] Clicking Web3Status when "Connected" displays dialog --- src/components/lib/Web3Status.js | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/components/lib/Web3Status.js b/src/components/lib/Web3Status.js index 947eb483..604d188e 100644 --- a/src/components/lib/Web3Status.js +++ b/src/components/lib/Web3Status.js @@ -23,13 +23,16 @@ export const Web3Status = (props) => { } else if(active) { - body =
+ body =
setShowConnectWallet(true)}> Connected
} return
- setShowConnectWallet(false)} shown={showConnectWallet} /> + setShowConnectWallet(false)} + onClose={() => setShowConnectWallet(false)} + shown={showConnectWallet} /> {body}
} From 739304a8fb268eedf38384abf72611dc3a259650 Mon Sep 17 00:00:00 2001 From: liamzebedee Date: Tue, 7 Apr 2020 00:47:55 +1000 Subject: [PATCH 06/48] Rewrite LedgerConnector to use 0x LedgerSubprovider The 0x API's are generally of a higher engineering quality. The exceptions encapsulate the error cases better, and they also manage the Ledger Transport lifecycle, which was a flaw of the previous implementation. We use them in keep-core too. --- src/connectors/ledger.js | 99 +++++++++++++++------------------------- 1 file changed, 36 insertions(+), 63 deletions(-) diff --git a/src/connectors/ledger.js b/src/connectors/ledger.js index 0969c109..eb251c49 100644 --- a/src/connectors/ledger.js +++ b/src/connectors/ledger.js @@ -1,10 +1,14 @@ -import Web3 from "web3"; -import createLedgerSubprovider from "@ledgerhq/web3-subprovider"; -import TransportU2F from "@ledgerhq/hw-transport-u2f"; -import ProviderEngine from "web3-provider-engine"; +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' -import { AbstractConnector } from '@web3-react/abstract-connector' - /** * An implementation of a LedgerConnector for web3-react, based on the original @@ -16,10 +20,8 @@ import { AbstractConnector } from '@web3-react/abstract-connector' * 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. + * 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({ @@ -30,42 +32,35 @@ export class LedgerConnector extends AbstractConnector { accountFetchingConfigs, baseDerivationPath }) { - super({ - supportedChainIds: [chainId] - }) + super({ supportedChainIds: [chainId] }) + this.chainId = chainId this.url = url this.pollingInterval = pollingInterval this.requestTimeoutMs = requestTimeoutMs this.accountFetchingConfigs = accountFetchingConfigs this.baseDerivationPath = baseDerivationPath - - this.ledgerSubprovider = null } - /** - * @returns {Promise} - */ - async activate(): Promise { + 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() + let ledgerEthereumClientFactoryAsync = async () => { + const ledgerConnection = await TransportU2F.create() + const ledgerEthClient = new AppEth(ledgerConnection) + return ledgerEthClient + } + const engine = new Web3ProviderEngine({ pollingInterval: this.pollingInterval }) + engine.addProvider( + new LedgerSubprovider({ + networkId: this.chainId, + ledgerEthereumClientFactoryAsync, + accountFetchingConfigs: this.accountFetchingConfigs, + baseDerivationPath: this.baseDerivationPath + }) + ) + engine.addProvider(new CacheSubprovider()) + engine.addProvider(new WebsocketSubprovider({ rpcUrl: this.url })) this.provider = engine } @@ -74,41 +69,19 @@ export class LedgerConnector extends AbstractConnector { return { provider: this.provider, chainId: this.chainId } } - /** - * @returns {Promise} - */ - async getProvider() { + async getProvider(): Promise { return this.provider } - /** - * @returns {Promise} - */ - async getChainId() { + async getChainId(): Promise { return this.chainId } - /** - * @returns {Promise} - */ - 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]) - }) - }) + async getAccount(): Promise { + return this.provider._providers[0].getAccountsAsync(1).then((accounts: string[]): string => accounts[0]) } deactivate() { this.provider.stop() } -} +} \ No newline at end of file From d26756e98f767b83ea5df1b185fadf44272597dc Mon Sep 17 00:00:00 2001 From: liamzebedee Date: Tue, 7 Apr 2020 00:48:30 +1000 Subject: [PATCH 07/48] fix: U2F DEVICE_INELIGIBLE with Ledger TransportU2F --- src/connectors/ledger.js | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/connectors/ledger.js b/src/connectors/ledger.js index eb251c49..ecdecef7 100644 --- a/src/connectors/ledger.js +++ b/src/connectors/ledger.js @@ -46,6 +46,11 @@ export class LedgerConnector extends AbstractConnector { if (!this.provider) { let ledgerEthereumClientFactoryAsync = async () => { 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. + ledgerConnection.setExchangeTimeout(100000) const ledgerEthClient = new AppEth(ledgerConnection) return ledgerEthClient } From 50d39d7f9ee8e49693b1e0b483aad106ac4a7022 Mon Sep 17 00:00:00 2001 From: liamzebedee Date: Tue, 7 Apr 2020 00:51:11 +1000 Subject: [PATCH 08/48] Add close button to dialog --- src/components/lib/ConnectWalletDialog.js | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/components/lib/ConnectWalletDialog.js b/src/components/lib/ConnectWalletDialog.js index a484865d..16944066 100644 --- a/src/components/lib/ConnectWalletDialog.js +++ b/src/components/lib/ConnectWalletDialog.js @@ -53,8 +53,7 @@ const WALLETS = [ } ] -export const ConnectWalletDialog = ({ shown, onConnected }) => { - const { active, account, activate } = useWeb3React() +export const ConnectWalletDialog = ({ shown, onConnected, onClose }) => { let [chosenWallet, setChosenWallet] = useState(null) let [error, setError] = useState(null) @@ -130,6 +129,9 @@ export const ConnectWalletDialog = ({ shown, onConnected }) => { return
+
+
+
{!chosenWallet && } {(chosenWallet && !active) && } {(chosenWallet && active) && } From ce2e747d838807fe36dd8cfdc31d29e5f571f0e4 Mon Sep 17 00:00:00 2001 From: liamzebedee Date: Tue, 7 Apr 2020 00:52:34 +1000 Subject: [PATCH 09/48] Show connected network in dialog details --- src/components/lib/ConnectWalletDialog.js | 29 +++++++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/src/components/lib/ConnectWalletDialog.js b/src/components/lib/ConnectWalletDialog.js index 16944066..d35742a1 100644 --- a/src/components/lib/ConnectWalletDialog.js +++ b/src/components/lib/ConnectWalletDialog.js @@ -4,6 +4,33 @@ 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 1, @@ -54,6 +81,7 @@ const WALLETS = [ ] export const ConnectWalletDialog = ({ shown, onConnected, onClose }) => { + const { active, account, activate, chainId, connector } = useWeb3React() let [chosenWallet, setChosenWallet] = useState(null) let [error, setError] = useState(null) @@ -118,6 +146,7 @@ export const ConnectWalletDialog = ({ shown, onConnected, onClose }) => {
+

Chain: {CHAINS.filter(chain => chain.id == chainId)[0].name}

{chosenWallet}

{account} From 621773c33837914ba4b1790338ee03362b7cc1bf Mon Sep 17 00:00:00 2001 From: liamzebedee Date: Tue, 7 Apr 2020 00:55:52 +1000 Subject: [PATCH 10/48] Add @0x/subproviders, upgrade @ledgerhq/hw-app-eth We use @0x/subproviders for the Ledger/Trezor integration. @ledgerhq/hw-app-eth is being upgraded as an older version was installed by accident. --- package.json | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/package.json b/package.json index 351f9ab8..71e36a20 100644 --- a/package.json +++ b/package.json @@ -2,8 +2,9 @@ "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.11.0", + "@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", @@ -21,8 +22,8 @@ "react-scripts": "3.0.1", "redux": "^4.0.4", "redux-saga": "^1.0.5", - "web3-provider-engine": "^15.0.6", - "web3": "^1.2.6" + "web3": "^1.2.6", + "web3-provider-engine": "^15.0.6" }, "scripts": { "start-js": "react-scripts start", From 6fd969fa037889e478640014fc1d2276e31235b2 Mon Sep 17 00:00:00 2001 From: liamzebedee Date: Tue, 7 Apr 2020 00:57:19 +1000 Subject: [PATCH 11/48] Correct title in ConnectedView --- src/components/lib/ConnectWalletDialog.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/lib/ConnectWalletDialog.js b/src/components/lib/ConnectWalletDialog.js index d35742a1..02813548 100644 --- a/src/components/lib/ConnectWalletDialog.js +++ b/src/components/lib/ConnectWalletDialog.js @@ -142,7 +142,7 @@ export const ConnectWalletDialog = ({ shown, onConnected, onClose }) => { const ConnectedView = () => { return

-
Connect To A Wallet
+
Wallet connected
From c73798a744c2f0dcc79cd38d70ee8c86802602f8 Mon Sep 17 00:00:00 2001 From: liamzebedee Date: Tue, 7 Apr 2020 00:58:41 +1000 Subject: [PATCH 12/48] Add ErrorConnecting view with "Try Again" button --- src/components/lib/ConnectWalletDialog.js | 22 +++++++++++++++++++++- 1 file changed, 21 insertions(+), 1 deletion(-) diff --git a/src/components/lib/ConnectWalletDialog.js b/src/components/lib/ConnectWalletDialog.js index 02813548..613d1f6f 100644 --- a/src/components/lib/ConnectWalletDialog.js +++ b/src/components/lib/ConnectWalletDialog.js @@ -130,11 +130,31 @@ export const ConnectWalletDialog = ({ shown, onConnected, onClose }) => { } const ConnectToWalletStep = () => { + if(error) { + return + } + + return <>
-
Connect To A Wallet
+
Connect to a wallet

Connecting to {chosenWallet} wallet...

+ + } + + const ErrorConnecting = () => { + return <> +
+
Connect to a wallet
+
+

Error connecting to {chosenWallet} wallet...

+ { + setError(null) + await chooseWallet(chosenWallet) + }}> + Try Again + { error &&

{error}

} } From e25fc670113f45685f32e3e430e70f4e579a4c53 Mon Sep 17 00:00:00 2001 From: liamzebedee Date: Tue, 7 Apr 2020 00:59:53 +1000 Subject: [PATCH 13/48] Add Ledger-specific connection instructions "Stolen" from the Compound dApp. We'll leave these here as a placeholder, until we get the real UX in. --- src/components/lib/ConnectWalletDialog.js | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/src/components/lib/ConnectWalletDialog.js b/src/components/lib/ConnectWalletDialog.js index 613d1f6f..1c288549 100644 --- a/src/components/lib/ConnectWalletDialog.js +++ b/src/components/lib/ConnectWalletDialog.js @@ -134,6 +134,15 @@ export const ConnectWalletDialog = ({ shown, onConnected, onClose }) => { return } + if(chosenWallet == 'Ledger') { + return <> +
+
Plug In Ledger & Enter Pin
+
+

Open Ethereum application and make sure Contract Data and Browser Support are enabled.

+

Connecting...

+ + } return <>
From 847449a8da75a29dcdc4f3fb0ec96da6d2339fb8 Mon Sep 17 00:00:00 2001 From: liamzebedee Date: Tue, 7 Apr 2020 01:00:20 +1000 Subject: [PATCH 14/48] Consistent capitalisation --- src/components/lib/ConnectWalletDialog.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/lib/ConnectWalletDialog.js b/src/components/lib/ConnectWalletDialog.js index 1c288549..3e7f8495 100644 --- a/src/components/lib/ConnectWalletDialog.js +++ b/src/components/lib/ConnectWalletDialog.js @@ -112,7 +112,7 @@ export const ConnectWalletDialog = ({ shown, onConnected, onClose }) => { const ChooseWalletStep = () => { return <>
-
Connect To A Wallet
+
Connect to a wallet

This wallet will be used to sign transactions on Ethereum.

From eed6c41bee80cf7082542f937673c7d9d57b73b1 Mon Sep 17 00:00:00 2001 From: liamzebedee Date: Wed, 15 Apr 2020 16:09:17 +1000 Subject: [PATCH 15/48] Fix Ledger signing issues when chainId > 255 This introduces a custom LedgerSubprovider, which mitigates the existing 0x LedgerSubprovider's failure to produce the correct transaction signature values. --- package.json | 2 + src/connectors/ledger.js | 5 +- src/connectors/ledger_subprovider.js | 101 +++++++++++++++++++++++++++ 3 files changed, 105 insertions(+), 3 deletions(-) create mode 100644 src/connectors/ledger_subprovider.js diff --git a/package.json b/package.json index 71e36a20..f8954395 100644 --- a/package.json +++ b/package.json @@ -11,6 +11,8 @@ "@web3-react/injected-connector": "^6.0.7", "bignumber.js": "^9.0.0", "classnames": "^2.2.6", + "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", diff --git a/src/connectors/ledger.js b/src/connectors/ledger.js index ecdecef7..ef9743fb 100644 --- a/src/connectors/ledger.js +++ b/src/connectors/ledger.js @@ -1,11 +1,10 @@ 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 { LedgerSubprovider } from './ledger_subprovider' 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' @@ -58,7 +57,7 @@ export class LedgerConnector extends AbstractConnector { const engine = new Web3ProviderEngine({ pollingInterval: this.pollingInterval }) engine.addProvider( new LedgerSubprovider({ - networkId: this.chainId, + chainId: this.chainId, ledgerEthereumClientFactoryAsync, accountFetchingConfigs: this.accountFetchingConfigs, baseDerivationPath: this.baseDerivationPath diff --git a/src/connectors/ledger_subprovider.js b/src/connectors/ledger_subprovider.js new file mode 100644 index 00000000..b7283cc1 --- /dev/null +++ b/src/connectors/ledger_subprovider.js @@ -0,0 +1,101 @@ +import { LedgerSubprovider as LedgerSubprovider0x } from '@0x/subproviders/lib/src/subproviders/ledger' // https://github.com/0xProject/0x-monorepo/issues/1400 +import { Transaction } from 'ethereumjs-tx' +import Common from 'ethereumjs-common' +import { addressUtils } from '@0x/utils'; +import web3 from 'web3' +import { + LedgerSubproviderErrors, + WalletSubproviderErrors +} from '@0x/subproviders/lib/src/types' + + +class LedgerSubprovider extends LedgerSubprovider0x { + chainId + + constructor(config) { + super(config) + this.chainId = config.chainId + } + + async signTransactionAsync(txParams) { + LedgerSubprovider._validateTxParams(txParams) + if (txParams.from === undefined || !addressUtils.isAddress(txParams.from)) { + throw new Error(WalletSubproviderErrors.FromAddressMissingOrInvalid); + } + const initialDerivedKeyInfo = await this._initialDerivedKeyInfoAsync(); + const derivedKeyInfo = this._findDerivedKeyInfoForAddress(initialDerivedKeyInfo, txParams.from); + + this._ledgerClientIfExists = await this._createLedgerClientAsync(); + + try { + const fullDerivationPath = derivedKeyInfo.derivationPath + const common = Common.forCustomChain( + 'mainnet', + { + name: 'keep-dev', + chainId: this.chainId, + }, + 'petersburg', ['petersburg'] + ) + let tx = new Transaction(txParams, { common }) + + // Set the EIP155 bits + const vIndex = 6; + tx.raw[vIndex] = getBufferFromHex(this.chainId.toString(16)) // v + const rIndex = 7; + tx.raw[rIndex] = Buffer.from([]); // r + const sIndex = 8; + tx.raw[sIndex] = Buffer.from([]); // s + + const response = await this._ledgerClientIfExists.signTransaction( + fullDerivationPath, + tx.serialize().toString('hex') + ) + + // Ledger computes the signature over the full 4 bytes of `v`, but the transport layer + // only returns the lower 2 bytes. The returned `v` will be wrong for chainId's < 255, + // and has to be recomputed by the client [1] [2]. + // [1]: https://github.com/LedgerHQ/ledgerjs/issues/168 + // [2]: https://github.com/LedgerHQ/ledger-app-eth/commit/8260268b0214810872dabd154b476f5bb859aac0 + const ledgerSignedV = parseInt(response.v, 16) + + // Recompute `v` according to the algorithm detailed in EIP155 [1]. + // [1] https://github.com/ethereum/EIPs/blob/master/EIPS/eip-155.md + const eip155Constant = 35 + let signedV = (this.chainId * 2) + eip155Constant + if(ledgerSignedV % 2 == 0) { + signedV += 1 + } + + // Verify signature `v` value returned from Ledger. + if ((signedV & 0xff) !== ledgerSignedV) { + await this._destroyLedgerClientAsync(); + const err = new Error(LedgerSubproviderErrors.TooOldLedgerFirmware); + throw err; + } + + // Store signature in transaction. + tx.v = getBufferFromHex(signedV.toString(16)) + tx.r = Buffer.from(response.r, 'hex') + tx.s = Buffer.from(response.s, 'hex') + + const signedTxHex = `0x${tx.serialize().toString('hex')}` + await this._destroyLedgerClientAsync(); + return signedTxHex; + } catch (err) { + await this._destroyLedgerClientAsync(); + throw err; + } + } +} + +const getBufferFromHex = (hex) => { + hex = hex.substring(0, 2) == '0x' ? hex.substring(2) : hex + if (hex == '') { + return new Buffer('', 'hex') + } + const padLeft = hex.length % 2 != 0 ? '0' + hex : hex + return new Buffer(padLeft.toLowerCase(), 'hex') +} + +export { LedgerSubprovider } \ No newline at end of file From 060c590dc43313325bf60769a96b5f769dbc9091 Mon Sep 17 00:00:00 2001 From: liamzebedee Date: Wed, 15 Apr 2020 16:19:27 +1000 Subject: [PATCH 16/48] Refactor chooseWallet code Colocate connector with other wallet info --- src/components/lib/ConnectWalletDialog.js | 40 +++++++---------------- 1 file changed, 11 insertions(+), 29 deletions(-) diff --git a/src/components/lib/ConnectWalletDialog.js b/src/components/lib/ConnectWalletDialog.js index 3e7f8495..4bee2e01 100644 --- a/src/components/lib/ConnectWalletDialog.js +++ b/src/components/lib/ConnectWalletDialog.js @@ -46,38 +46,27 @@ const SUPPORTED_CHAIN_IDS = [ ] // Connectors. -const injectedConnector = new InjectedConnector({ - supportedChainIds: SUPPORTED_CHAIN_IDS -}) +const injectedConnector = new InjectedConnector({}) const ledgerConnector = new LedgerConnector({ - // We use the chainId of mainnet here to workaround an issue with the ledgerjs library. - // It currently throws an error for the default chainId of 1377 used by Geth/Ganache. - // - // 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. - // - // [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, + // TODO: introduce CHAIN_ID and ETH_RPC_URL env variables. + chainId: process.env.CHAIN_ID || 1377, url: 'ws://localhost:8545' }) + // Wallets. const WALLETS = [ { name: "Metamask", icon: "/images/metamask-fox.svg", - showName: true + showName: true, + connector: injectedConnector }, { name: "Ledger", - icon: "/images/ledger.svg" + icon: "/images/ledger.svg", + connector: ledgerConnector }, - } ] export const ConnectWalletDialog = ({ shown, onConnected, onClose }) => { @@ -90,16 +79,9 @@ export const ConnectWalletDialog = ({ shown, onConnected, onClose }) => { error } - async function chooseWallet(wallet) { + async function chooseWallet(wallet, connector) { setChosenWallet(wallet) - let connector - if (wallet == 'Ledger') { - connector = ledgerConnector - } else if (wallet == 'Metamask') { - connector = injectedConnector - } - try { await activate(connector, undefined, true) onConnected() @@ -118,8 +100,8 @@ export const ConnectWalletDialog = ({ shown, onConnected, onClose }) => {
    { - WALLETS.map(({ name, icon, showName }) => { - return
  • chooseWallet(name)}> + WALLETS.map(({ name, icon, showName, connector }) => { + return
  • chooseWallet(name, connector)}> {showName && name}
  • From 785845789b74d82934de30d3e99c70830b7ccaa1 Mon Sep 17 00:00:00 2001 From: liamzebedee Date: Wed, 15 Apr 2020 16:19:57 +1000 Subject: [PATCH 17/48] Remove SUPPORTED_CHAIN_IDS We don't need this anymore - it was only supported by InjectedConnector and didn't appear to be useful for our case anyways. --- src/components/lib/ConnectWalletDialog.js | 13 ------------- 1 file changed, 13 deletions(-) diff --git a/src/components/lib/ConnectWalletDialog.js b/src/components/lib/ConnectWalletDialog.js index 4bee2e01..663e3838 100644 --- a/src/components/lib/ConnectWalletDialog.js +++ b/src/components/lib/ConnectWalletDialog.js @@ -31,19 +31,6 @@ const CHAINS = [ } ] -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 -] // Connectors. const injectedConnector = new InjectedConnector({}) From f7c1541064254a506517d56074591879e3e7c7da Mon Sep 17 00:00:00 2001 From: liamzebedee Date: Wed, 15 Apr 2020 19:03:36 +1000 Subject: [PATCH 18/48] Pass around chosenWallet object instead of name/connector combo. This is simpler and a bit more efficient. --- src/components/lib/ConnectWalletDialog.js | 33 +++++++++-------------- 1 file changed, 13 insertions(+), 20 deletions(-) diff --git a/src/components/lib/ConnectWalletDialog.js b/src/components/lib/ConnectWalletDialog.js index 663e3838..b4f33fd3 100644 --- a/src/components/lib/ConnectWalletDialog.js +++ b/src/components/lib/ConnectWalletDialog.js @@ -66,11 +66,11 @@ export const ConnectWalletDialog = ({ shown, onConnected, onClose }) => { error } - async function chooseWallet(wallet, connector) { + async function chooseWallet(wallet) { setChosenWallet(wallet) try { - await activate(connector, undefined, true) + await activate(wallet.connector, undefined, true) onConnected() } catch(ex) { setError(ex.toString()) @@ -87,10 +87,10 @@ export const ConnectWalletDialog = ({ shown, onConnected, onClose }) => {
} From c3046892e8ab512d2255f28c16fb94b1d389aa42 Mon Sep 17 00:00:00 2001 From: liamzebedee Date: Wed, 15 Apr 2020 19:03:55 +1000 Subject: [PATCH 19/48] Clean up unused
boilerplate --- src/components/lib/ConnectWalletDialog.js | 31 ++++++++--------------- 1 file changed, 11 insertions(+), 20 deletions(-) diff --git a/src/components/lib/ConnectWalletDialog.js b/src/components/lib/ConnectWalletDialog.js index b4f33fd3..5c575be4 100644 --- a/src/components/lib/ConnectWalletDialog.js +++ b/src/components/lib/ConnectWalletDialog.js @@ -80,11 +80,9 @@ export const ConnectWalletDialog = ({ shown, onConnected, onClose }) => { const ChooseWalletStep = () => { return <> -
-
Connect to a wallet
-
+
Connect to a wallet

This wallet will be used to sign transactions on Ethereum.

- +