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

Hardware wallets #184

wants to merge 14 commits into from

Conversation

liamzebedee
Copy link
Contributor

@liamzebedee liamzebedee commented Feb 18, 2020

Work in this branch implements support for Ledger and Trezor for use as Ethereum wallets, and in a future PR that is not far far away, Bitcoin wallets too. It uses web3-react, a well-written library for Ethereum interactions in React, which supports various types of wallets (eg. WalletConnect).

This PR implements "connect a wallet" functionality, with UX inspired from the Uniswap dApp. Some screenshots are attached below to illustrate. Ignore the terrible design until [WIP] is removed.

  • Use WSS for ElectrumX connection, as hardware wallets require HTTPS with no mixed transport security.
  • Integrate Ledger support
  • Fix Ledger error handling when it's on the lockscreen - TransportStatusError: Ledger device: UNKNOWN_ERROR (0x6804). Looks like a weird Ledger bug.
  • UX audit of choose wallet
  • Trezor support
  • Trusted certificate for ElectrumX server (refs: keep-dev/test: SSL #194)

Depends-On: #195

Epic: #177

Image assets

Logos were taken from:

@liamzebedee
Copy link
Contributor Author

Example of Uniswap's distinction between Web3 connection and wallet connection.

Screen Shot 2020-02-18 at 3 52 57 pm

Screen Shot 2020-02-18 at 11 33 30 pm

Another example of the "Connect Wallet" dialog, which this is modelled on.

Screen Shot 2020-02-18 at 4 15 45 pm

liamzebedee added a commit that referenced this pull request Feb 19, 2020
- remove our HOC helpers which were barely used: withAccount, withContract, withBalance, withConnectDapp
- Begin using two Web3 connections - one to the network, and a second to the wallet (which could be a hardware wallet, mobile wallet via WalletConnect, etc.). We will be "connecting the wallet" as a separate step to connecting to Web3 (as Trezor/Ledger don't have a built-in connection to Infura... 😛). The rationale for this is detailed more in #184.
- temporarily remove logic for Web3Status
- fix Start page to use the react-web3 hook, useWeb3React.
@liamzebedee
Copy link
Contributor Author

Github doesn't support webm/mp4, so here's a brief video on Flowdock of the brutalist functional UI.

@liz-shinn
Copy link

liz-shinn commented Mar 18, 2020

Nice! Thanks for linking me to this. I had looked into the "connect wallet" dialog when doing initial research on the token dash. I've attached some screenshots below of other approaches I've seen.

From Basechain:
Image 2020-03-18 at 3 51 31 PM

From IoTex:
Image 2020-03-18 at 3 52 28 PM

From Staked:
Image 2020-03-18 at 3 53 04 PM

@liamzebedee liamzebedee marked this pull request as ready for review March 27, 2020 11:47
@liamzebedee liamzebedee changed the title [WIP] Hardware wallets Hardware wallets Apr 1, 2020
@Shadowfiend
Copy link
Contributor

Shadowfiend commented Apr 2, 2020

The LOC on this change scared the bespitters out of me (before I realized there was a huge SVG here, that is).

Can we actually split this out to:

? Or would that be too much of a pain? It would be helpful for me to maintain context and keep review turnaround times short, even if each PR is built on the other (as long as the base branch is set to keep the diff to just what that PR is adding).

@liamzebedee liamzebedee changed the base branch from master to react-to-web3 April 3, 2020 09:42
@liamzebedee
Copy link
Contributor Author

@Shadowfiend If that helps us ship faster, then of course. Thanks for explaining why this would help too, that's something I find really useful.

@Shadowfiend Shadowfiend changed the base branch from react-to-web3 to master April 3, 2020 16:23
@Shadowfiend
Copy link
Contributor

Now that I see this updated diff, feels fine as is. Will try and do a full pass today.

Copy link
Contributor

@Shadowfiend Shadowfiend left a comment

Choose a reason for hiding this comment

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

Left some thoughts, questions, and ideas. Feels like we're pretty close, particularly if we punt Trezor to a separate PR.

docs/testing-with-hardware-wallets.md Outdated Show resolved Hide resolved
docs/testing-with-hardware-wallets.md Outdated Show resolved Hide resolved
src/components/lib/ConnectWalletDialog.js Show resolved Hide resolved
@@ -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.
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.

//
// 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.

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.

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.

}

/**
* @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 🤷‍♂️

src/connectors/ledger.js Outdated Show resolved Hide resolved
package.json Show resolved Hide resolved
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.
We use @0x/subproviders for the Ledger/Trezor integration.

@ledgerhq/hw-app-eth is being upgraded as an older version was installed by accident.
"Stolen" from the Compound dApp. We'll leave these here as a placeholder, until we get the real UX in.
Copy link
Contributor

@Shadowfiend Shadowfiend left a comment

Choose a reason for hiding this comment

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

Few more notes; only the one about the annotations is blocking atm I think.

@@ -103,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

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?

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.

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.

{ 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'.

@@ -130,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>.

* @returns {Promise<Object>}
*/
async activate(): Promise {
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 <_<

}))

engine.start()
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?

// 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).

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... 😄

Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants