Skip to content

Commit

Permalink
Uplift of #11056 (squashed) to beta
Browse files Browse the repository at this point in the history
  • Loading branch information
onyb committed Nov 13, 2021
1 parent 5ae00f4 commit f9f5239
Show file tree
Hide file tree
Showing 11 changed files with 167 additions and 30 deletions.
39 changes: 38 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,20 @@ export async function signMessageWithHardwareKeyring (apiProxy: WalletApiProxy,
}
return { success: false, error: getLocale('braveWalletUnknownKeyringError') }
}

export function parseLedgerDeviceError (e: any): HardwareWalletErrorType {
// @ts-ignore
if (e instanceof LedgerTransportStatusError && e.statusCode === LedgerStatusCodes.CONDITIONS_OF_USE_NOT_SATISFIED) {
return 'transactionRejected'
}

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 @@ -45,11 +45,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 @@ -5,7 +5,13 @@
/* global window */

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 @@ -38,7 +44,8 @@ const defaultState: PanelState = {
url: ''
},
chainId: ''
}
},
hardwareWalletError: undefined
}

const reducer = createReducer<PanelState>({}, defaultState)
Expand Down Expand Up @@ -98,4 +105,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 @@ -614,13 +614,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

0 comments on commit f9f5239

Please sign in to comment.