This repository has been archived by the owner on Mar 28, 2023. It is now read-only.
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #205 from keep-network/heath-ledger
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
Showing
18 changed files
with
16,972 additions
and
201 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Large diffs are not rendered by default.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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}>╳</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> } | ||
</> | ||
} |
Oops, something went wrong.