Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(wallet): add connect screen for Ledger hardware wallet #11056

Merged
merged 1 commit into from
Nov 13, 2021
Merged
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
38 changes: 37 additions & 1 deletion components/brave_wallet_ui/common/async/hardware.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,20 @@
// License, v. 2.0. If a copy of the MPL was not distributed with this file,
// you can obtain one at http://mozilla.org/MPL/2.0/.

import {
TransportStatusError as LedgerTransportStatusError,
TransportError as LedgerTransportError,
DisconnectedDeviceDuringOperation as LedgerDisconnectedDeviceDuringOperation,
StatusCodes as LedgerStatusCodes
} from '@ledgerhq/errors'

import * as BraveWallet from 'gen/brave/components/brave_wallet/common/brave_wallet.mojom.m.js'
import { SignHardwareTransactionType, SignHardwareMessageOperationResult } from '../hardware_operations'
import { getLocale } from '../../../common/locale'
import WalletApiProxy from '../../common/wallet_api_proxy'
import LedgerBridgeKeyring from '../../common/ledgerjs/eth_ledger_bridge_keyring'
import TrezorBridgeKeyring from '../../common/trezor/trezor_bridge_keyring'
import { HardwareWalletErrorType } from '../../constants/types'

export async function signTrezorTransaction (apiProxy: WalletApiProxy, path: string, txInfo: BraveWallet.TransactionInfo): Promise<SignHardwareTransactionType> {
const chainId = await apiProxy.ethJsonRpcController.getChainId()
Expand Down Expand Up @@ -41,7 +49,19 @@ export async function signLedgerTransaction (apiProxy: WalletApiProxy, path: str
return { success: false, error: getLocale('braveWalletNoMessageToSignError') }
}
const deviceKeyring = apiProxy.getKeyringsByType(BraveWallet.LEDGER_HARDWARE_VENDOR) as LedgerBridgeKeyring
const signed = await deviceKeyring.signTransaction(path, data.message.replace('0x', ''))

let signed
try {
signed = await deviceKeyring.signTransaction(path, data.message.replace('0x', ''))
} catch (e) {
const ledgerError = parseLedgerDeviceError(e)
if (ledgerError === 'needsConnectionReset') {
await deviceKeyring.makeApp()
}

return { success: false, deviceError: ledgerError }
}

if (!signed || !signed.success || !signed.payload) {
const error = signed && signed.error ? signed.error : getLocale('braveWalletSignOnDeviceError')
return { success: false, error: error }
Expand All @@ -63,3 +83,19 @@ export async function signMessageWithHardwareKeyring (apiProxy: WalletApiProxy,
}
return { success: false, error: getLocale('braveWalletUnknownKeyringError') }
}

export function parseLedgerDeviceError (e: any): HardwareWalletErrorType {
if (e instanceof LedgerTransportStatusError && (e as any).statusCode === LedgerStatusCodes.CONDITIONS_OF_USE_NOT_SATISFIED) {
return 'transactionRejected'
onyb marked this conversation as resolved.
Show resolved Hide resolved
}

if (e instanceof LedgerTransportError && e.message === 'Ledger Device is busy (lock signTransaction)') {
return 'deviceBusy'
}

if (e instanceof LedgerDisconnectedDeviceDuringOperation) {
return 'needsConnectionReset'
}

return 'deviceNotConnected'
}
2 changes: 2 additions & 0 deletions components/brave_wallet_ui/common/hardware_operations.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
import { EthereumSignedTx } from 'trezor-connect/lib/typescript'
import { HardwareWalletErrorType } from '../constants/types'

export interface SignHardwareTransactionType {
success: boolean
error?: string
deviceError?: HardwareWalletErrorType
}
export interface SignatureVRS {
v: number
Expand Down
23 changes: 23 additions & 0 deletions components/brave_wallet_ui/common/hooks/interval.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import { useEffect, useLayoutEffect, useRef } from 'react'

function useInterval (callback: () => void, delay: number | null) {
const savedCallback = useRef(callback)

// Remember the latest callback if it changes.
useLayoutEffect(() => {
savedCallback.current = callback
}, [callback])

// Set up the interval.
useEffect(() => {
// Don't schedule if no delay is specified.
if (!delay) {
return
}

const id = setInterval(() => savedCallback.current(), delay)
return () => clearInterval(id)
}, [delay])
}

export default useInterval
Original file line number Diff line number Diff line change
Expand Up @@ -44,11 +44,17 @@ export default class LedgerBridgeKeyring extends EventEmitter {
return this.app !== undefined
}

makeApp = async () => {
this.app = new Eth(await TransportWebHID.create())
}

unlock = async () => {
if (this.app) {
return this.app
}
this.app = new Eth(await TransportWebHID.create())

await this.makeApp()

if (this.app) {
const zeroPath = this._getPathForIndex(0, LedgerDerivationPaths.LedgerLive)
const address = await this._getAddress(zeroPath)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,21 +12,33 @@ import {
} from './style'
import { NavButton } from '..'
import { getLocale } from '../../../../common/locale'
import { HardwareWalletErrorType } from '../../../constants/types'
import useInterval from '../../../common/hooks/interval'

export interface Props {
onCancel: () => void
isConnected: boolean
walletName: string
requestingConfirmation: boolean
hardwareWalletError?: HardwareWalletErrorType
retryCallable: () => void
}

function ConnectHardwareWalletPanel (props: Props) {
const { onCancel, walletName, isConnected, requestingConfirmation } = props
const {
onCancel,
walletName,
hardwareWalletError,
retryCallable
} = props

const isConnected = hardwareWalletError !== undefined && hardwareWalletError !== 'deviceNotConnected'
const requestingConfirmation = hardwareWalletError === 'deviceBusy'

const onClickInstructions = () => {
window.open('https://support.brave.com/hc/en-us/articles/4409309138701', '_blank')
}

useInterval(retryCallable, 3000)

return (
<StyledWrapper>
<ConnectionRow>
Expand Down
7 changes: 7 additions & 0 deletions components/brave_wallet_ui/constants/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -134,6 +134,12 @@ export type ChartTimelineType =
| '1Year'
| 'AllTime'

export type HardwareWalletErrorType =
| 'deviceNotConnected'
| 'transactionRejected'
| 'deviceBusy'
| 'needsConnectionReset'

export interface BuySendSwapObjectType {
name: string
id: BuySendSwapTypes
Expand Down Expand Up @@ -213,6 +219,7 @@ export interface PanelState {
swapError?: SwapErrorResponse
signMessageData: BraveWallet.SignMessageRequest[]
switchChainRequest: BraveWallet.SwitchChainRequest
hardwareWalletError?: HardwareWalletErrorType
}

export interface PageState {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,8 @@ import {
SwapErrorResponse,
SwapResponse,
SignMessageRequest,
SwitchChainRequest
SwitchChainRequest,
HardwareWalletErrorType
} from '../../constants/types'
import { SwapParamsPayloadType } from '../../common/constants/action_types'
import { TransactionInfo } from 'gen/brave/components/brave_wallet/common/brave_wallet.mojom.m.js'
Expand Down Expand Up @@ -47,3 +48,4 @@ export const signMessageProcessed = createAction<SignMessageProcessedPayload>('s
export const signMessageHardware = createAction<SignMessageRequest>('signMessageHardware')
export const signMessageHardwareProcessed = createAction<SignMessageHardwareProcessedPayload>('signMessageHardwareProcessed')
export const approveHardwareTransaction = createAction<TransactionInfo>('approveHardwareTransaction')
export const setHardwareWalletInteractionError = createAction<HardwareWalletErrorType | undefined>('setHardwareWalletInteractionError')
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,8 @@ import {
SwitchEthereumChainProcessedPayload
} from '../constants/action_types'
import {
findHardwareAccountInfo
findHardwareAccountInfo,
refreshTransactionHistory
} from '../../common/async/lib'
import {
signTrezorTransaction,
Expand Down Expand Up @@ -150,13 +151,42 @@ handler.on(PanelActions.approveHardwareTransaction.getType(), async (store: Stor
if (!hardwareAccount || !hardwareAccount.hardware) {
return
}

const apiProxy = getWalletPanelApiProxy()
apiProxy.panelHandler.setCloseOnDeactivate(false)

if (hardwareAccount.hardware.vendor === LEDGER_HARDWARE_VENDOR) {
await signLedgerTransaction(apiProxy, hardwareAccount.hardware.path, txInfo)
await store.dispatch(PanelActions.navigateTo('connectHardwareWallet'))
await store.dispatch(PanelActions.setHardwareWalletInteractionError(undefined))

const { success, error, deviceError } = await signLedgerTransaction(apiProxy, hardwareAccount.hardware.path, txInfo)
if (!success) {
if (deviceError) {
if (deviceError === 'transactionRejected') {
await store.dispatch(WalletActions.rejectTransaction(txInfo))
await store.dispatch(PanelActions.navigateTo('main'))
} else {
await store.dispatch(PanelActions.setHardwareWalletInteractionError(deviceError))
}
} else if (error) {
// TODO: handle non-device errors
console.log(error)
}
} else {
await store.dispatch(PanelActions.navigateTo('main'))
await store.dispatch(PanelActions.setHardwareWalletInteractionError(undefined))
refreshTransactionHistory(txInfo.fromAddress)
}
} else if (hardwareAccount.hardware.vendor === TREZOR_HARDWARE_VENDOR) {
await signTrezorTransaction(apiProxy, hardwareAccount.hardware.path, txInfo)
const { success, error } = await signTrezorTransaction(apiProxy, hardwareAccount.hardware.path, txInfo)
if (!success) {
console.log(error)
await store.dispatch(WalletActions.rejectTransaction(txInfo))
} else {
refreshTransactionHistory(txInfo.fromAddress)
}
}

apiProxy.panelHandler.setCloseOnDeactivate(true)
apiProxy.panelHandler.showUI()
})
Expand Down
35 changes: 18 additions & 17 deletions components/brave_wallet_ui/panel/container.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -431,7 +431,9 @@ function Container (props: Props) {
}

const onCancelConnectHardwareWallet = () => {
// Logic here to cancel connecting your hardware wallet
// Navigating to main panel view will unmount ConnectHardwareWalletPanel
// and therefore forfeit connecting to the hardware wallet.
props.walletPanelActions.navigateTo('main')
}

const removeSitePermission = (origin: string, address: string) => {
Expand Down Expand Up @@ -480,6 +482,21 @@ function Container (props: Props) {
)
}

if (selectedPendingTransaction && selectedPanel === 'connectHardwareWallet') {
return (
<PanelWrapper isLonger={false}>
<StyledExtensionWrapper>
<ConnectHardwareWalletPanel
onCancel={onCancelConnectHardwareWallet}
walletName={selectedAccount.name}
hardwareWalletError={props.panel.hardwareWalletError}
retryCallable={onConfirmTransaction}
/>
</StyledExtensionWrapper>
</PanelWrapper>
)
}

if (selectedPendingTransaction) {
return (
<PanelWrapper isLonger={true}>
Expand Down Expand Up @@ -508,22 +525,6 @@ function Container (props: Props) {
)
}

if (selectedPanel === 'connectHardwareWallet') {
return (
<PanelWrapper isLonger={false}>
<StyledExtensionWrapper>
<ConnectHardwareWalletPanel
onCancel={onCancelConnectHardwareWallet}
isConnected={false}
walletName='Ledger 1'
// Pass a boolean true here to show needs Transaction Confirmation state
requestingConfirmation={false}
/>
</StyledExtensionWrapper>
</PanelWrapper>
)
}

if (selectedPanel === 'addEthereumChain') {
return (
<PanelWrapper isLonger={true}>
Expand Down
18 changes: 16 additions & 2 deletions components/brave_wallet_ui/panel/reducers/panel_reducer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,13 @@
* You can obtain one at http://mozilla.org/MPL/2.0/. */

import { createReducer } from 'redux-act'
import { PanelState, SwapErrorResponse, SwapResponse, SwitchChainRequest } from '../../constants/types'
import {
HardwareWalletErrorType,
PanelState,
SwapErrorResponse,
SwapResponse,
SwitchChainRequest
} from '../../constants/types'
import * as PanelActions from '../actions/wallet_panel_actions'
import {
ShowConnectToSitePayload,
Expand Down Expand Up @@ -43,7 +49,8 @@ const defaultState: PanelState = {
url: ''
},
chainId: ''
}
},
hardwareWalletError: undefined
}

const reducer = createReducer<PanelState>({}, defaultState)
Expand Down Expand Up @@ -103,4 +110,11 @@ reducer.on(PanelActions.signMessage, (state: any, payload: SignMessagePayload[])
}
})

reducer.on(PanelActions.setHardwareWalletInteractionError, (state: any, payload?: HardwareWalletErrorType) => {
return {
...state,
hardwareWalletError: payload
}
})

export default reducer
Original file line number Diff line number Diff line change
Expand Up @@ -609,13 +609,16 @@ export const _ConnectHardwareWallet = () => {
// Doesn't do anything in storybook
}

const onConfirmTransaction = () => {
// Doesn't do anything in storybook
}

return (
<StyledExtensionWrapper>
<ConnectHardwareWalletPanel
walletName='Ledger 1'
isConnected={true}
onCancel={onCancel}
requestingConfirmation={true}
retryCallable={onConfirmTransaction}
/>
</StyledExtensionWrapper>
)
Expand Down