Skip to content

Commit

Permalink
enhancement: handle Ledger EVM transactions when blind signing is dis…
Browse files Browse the repository at this point in the history
…abled (#434)

* Add Ledger verification prompt for L2 tx

* Refactor function for calculating tx value

* Align UI wording with that of the ETH Ledger app

* Cleanup code

* Fix type check

* Add logic for getting app settings

* Get logic hooked up

* Fix imports

* Make changes per PR review

* Bring close popup outside of if statement

* Add logic for detecting and displaying blind signing popup

* Refine logic around the send flow

* Remove global polling

* Fix logic around polling for Ethereum app settings

* Remove unnecessary callback

* refactor: combine ledger status

* refactor: cleanup files

* rename ledgerNanoStatus -> ledgerNanoState

* refactor: more name improvements

* fix use of app name

* Rename to Ledger device state

* Fix conditional in component

* Use alert box from UI kit

* Cleanup import

* fix: code quality

* Fix PR comments

* Fix linting issue

* fix: ledger api polling

Co-authored-by: Matthew Maxwell <[email protected]>

* chore: update polling intervals

* Change constant name

* Cleanup logic and bugs for EVM

* Fix blind signing for L1 transactions

* Make small improvements

---------

Co-authored-by: Nicole O'Brien <[email protected]>
Co-authored-by: Matthew Maxwell <[email protected]>
  • Loading branch information
3 people authored Sep 13, 2023
1 parent 0b9dc4b commit 49b5396
Show file tree
Hide file tree
Showing 56 changed files with 425 additions and 310 deletions.
4 changes: 2 additions & 2 deletions packages/desktop/components/popups/ConnectLedgerPopup.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
LedgerAppName,
LedgerConnectionState,
determineLedgerConnectionState,
ledgerNanoStatus,
ledgerDeviceState,
} from '@core/ledger'
import { isFunction } from '@core/utils'
import { closePopup } from '@desktop/auxiliary/popup'
Expand All @@ -15,7 +15,7 @@
export let onCancel: () => void
export let onContinue: () => void
$: ledgerConnectionState = determineLedgerConnectionState($ledgerNanoStatus, ledgerAppName)
$: ledgerConnectionState = determineLedgerConnectionState($ledgerDeviceState, ledgerAppName)
$: isNotConnected = ledgerConnectionState === LedgerConnectionState.NotConnected
$: isLocked = ledgerConnectionState === LedgerConnectionState.Locked
Expand Down
Original file line number Diff line number Diff line change
@@ -1,50 +1,35 @@
<script lang="ts">
import { Text, Icon, TextType } from '@ui'
import { Icon as IconEnum } from '@auxiliary/icon'
import { onDestroy } from 'svelte'
import { Alert } from '@bloomwalletio/ui'
import { localize } from '@core/i18n'
import {
checkOrConnectLedger,
ledgerNanoStatus,
ledgerPreparedOutput,
resetLedgerPreparedOutput,
} from '@core/ledger'
import { LedgerAppName, ledgerDeviceState } from '@core/ledger'
import { closePopup } from '@desktop/auxiliary/popup'
import { sendOutput } from '@core/wallet'
import { handleError } from '@core/error/handlers'
import { Text, TextType } from '@ui'
import { UiEventFunction } from '@core/utils'
export let appName: LedgerAppName
export let onEnabled: UiEventFunction = () => {}
export let onClose: UiEventFunction = () => {}
const STEPS = [1, 2, 3, 4]
$: if ($ledgerNanoStatus.blindSigningEnabled) {
closePopup()
checkOrConnectLedger(async () => {
try {
if ($ledgerPreparedOutput) {
await sendOutput($ledgerPreparedOutput)
resetLedgerPreparedOutput()
}
} catch (err) {
handleError(err)
}
})
$: if ($ledgerDeviceState && $ledgerDeviceState.settings[appName]?.blindSigningEnabled) {
closePopup(true)
onEnabled && onEnabled()
}
onDestroy(() => {
onClose && onClose()
})
</script>

<Text type={TextType.h3} classes="mb-6">{localize('popups.enableLedgerBlindSigning.title')}</Text>

<div class="w-full h-full space-y-2 flex flex-auto flex-col shrink-0">
<div class="bg-yellow-50 w-full h-full space-y-6 rounded-md px-6 py-4">
<span class="flex flex-row items-center space-x-4">
<Icon boxed height={18} width={18} icon={IconEnum.InfoFilled} classes="text-yellow-700" />
<Text type={TextType.p} fontSize="14" color="gray-700" darkColor="gray-700"
>{localize('popups.enableLedgerBlindSigning.info')}</Text
>
</span>
</div>
<div>
{#each STEPS as step}
<Text type={TextType.p} fontSize="15" color="gray-600" classes="my-2">
{step}. {localize(`popups.enableLedgerBlindSigning.step_${step}`)}
</Text>
{/each}
</div>
<Alert variant="warning" text={localize('popups.enableLedgerBlindSigning.info')} />
{#each STEPS as step}
<Text type={TextType.p} fontSize="15" color="gray-600">
{step}. {localize(`popups.enableLedgerBlindSigning.step_${step}`, { appName })}
</Text>
{/each}
</div>
16 changes: 13 additions & 3 deletions packages/desktop/lib/electron/processes/ledger.process.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,13 @@

import { LedgerApiMethod } from '@core/ledger/enums'
import type { ILedgerProcessMessage } from '../interfaces/ledger-process-message.interface'
import { closeTransport, getEvmAddress, openTransport, signTransactionData } from '../utils/ledger.utils'
import {
closeTransport,
getEthereumAppSettings,
getEvmAddress,
openTransport,
signTransactionData,
} from '../utils/ledger.utils'

/**
* CAUTION: `process` is initialized using `utilityProcess.fork()`.
Expand All @@ -29,6 +35,10 @@ async function messageHandler(message: ILedgerProcessMessage): Promise<void> {
data = await getEvmAddress(payload[0] as string)
break
}
case LedgerApiMethod.GetEthereumAppSettings: {
data = await getEthereumAppSettings()
break
}
case LedgerApiMethod.SignEvmTransaction: {
data = await signTransactionData(payload[0] as string, payload[1] as string)
break
Expand All @@ -37,10 +47,10 @@ async function messageHandler(message: ILedgerProcessMessage): Promise<void> {
break
}

await closeTransport()

process.parentPort.postMessage({ method, payload: data })
} catch (error) {
process.parentPort.postMessage({ error })
} finally {
await closeTransport()
}
}
19 changes: 12 additions & 7 deletions packages/desktop/lib/electron/processes/main.process.ts
Original file line number Diff line number Diff line change
@@ -1,16 +1,16 @@
// Modules to control application life and create native browser window
import {
app,
BrowserWindow,
dialog,
ipcMain,
shell,
BrowserWindow,
session,
utilityProcess,
nativeTheme,
PopupOptions,
UtilityProcess,
powerMonitor,
session,
shell,
utilityProcess,
UtilityProcess,
} from 'electron'
import { WebPreferences } from 'electron/main'
import path from 'path'
Expand Down Expand Up @@ -296,6 +296,9 @@ ipcMain.on('start-ledger-process', () => {
case LedgerApiMethod.GenerateEvmAddress:
windows.main.webContents.send('evm-address', payload)
break
case LedgerApiMethod.GetEthereumAppSettings:
windows.main.webContents.send('ethereum-app-settings', payload)
break
case LedgerApiMethod.SignEvmTransaction:
windows.main.webContents.send('evm-signed-transaction', payload)
break
Expand All @@ -317,12 +320,14 @@ ipcMain.on(LedgerApiMethod.GenerateEvmAddress, (_e, bip32Path, verify) => {
ledgerProcess?.postMessage({ method: LedgerApiMethod.GenerateEvmAddress, payload: [bip32Path, verify] })
})

ipcMain.on(LedgerApiMethod.GetEthereumAppSettings, () => {
ledgerProcess?.postMessage({ method: LedgerApiMethod.GetEthereumAppSettings })
})

ipcMain.on(LedgerApiMethod.SignEvmTransaction, (_e, transactionHex, bip32Path) => {
ledgerProcess?.postMessage({ method: LedgerApiMethod.SignEvmTransaction, payload: [transactionHex, bip32Path] })
})

export const getWindow = (windowName: string): BrowserWindow => windows[windowName]

export function getOrInitWindow(windowName: string): BrowserWindow {
if (!windows[windowName]) {
if (windowName === 'main') {
Expand Down
13 changes: 13 additions & 0 deletions packages/desktop/lib/electron/utils/ledger.utils.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import AppEth from '@ledgerhq/hw-app-eth'
import TransportNodeHid from '@ledgerhq/hw-transport-node-hid'
import { listen } from '@ledgerhq/logs'
import type { ILedgerEthereumAppSettings } from '@core/ledger/interfaces'

// import specifity for core modules to prevent circular dependencies
import { IEvmTransactionSignature } from '@core/layer-2/interfaces'
Expand All @@ -24,6 +25,18 @@ export async function closeTransport(): Promise<void> {
}
}

export async function getEthereumAppSettings(): Promise<ILedgerEthereumAppSettings> {
const appEth = new AppEth(transport)
const settings = await appEth.getAppConfiguration()
return <ILedgerEthereumAppSettings>{
version: settings.version,
blindSigningEnabled: Boolean(settings.arbitraryDataEnabled),
erc20ProvisioningNecessary: Boolean(settings.erc20ProvisioningNecessary),
starkEnabled: Boolean(settings.starkEnabled),
starkv2Supported: Boolean(settings.starkv2Supported),
}
}

export async function getEvmAddress(bip32Path: string): Promise<{ evmAddress: string; bip32Path: string }> {
const appEth = new AppEth(transport)
const data = await appEth.getAddress(bip32Path)
Expand Down
6 changes: 1 addition & 5 deletions packages/desktop/views/dashboard/Dashboard.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,14 @@
import { selectedAccount, selectedAccountIndex } from '@core/account/stores'
import { Platform } from '@core/app'
import { clearLayer2TokensPoll, pollLayer2Tokens } from '@core/layer-2/actions'
import { stopPollingLedgerNanoStatus } from '@core/ledger'
import {
addNftsToDownloadQueue,
downloadNextNftInQueue,
interruptNftDownloadAfterTimeout,
} from '@core/nfts/actions'
import { downloadingNftId, nftDownloadQueue, resetNftDownloadQueue, selectedAccountNfts } from '@core/nfts/stores'
import { logout, reflectLockedStronghold } from '@core/profile/actions'
import { hasStrongholdLocked, isActiveLedgerProfile } from '@core/profile/stores'
import { hasStrongholdLocked } from '@core/profile/stores'
import { appRouter, dashboardRoute } from '@core/router'
import features from '@features/features'
import { Idle } from '@ui'
Expand Down Expand Up @@ -64,9 +63,6 @@
onDestroy(() => {
Platform.DeepLinkManager.clearDeepLinkRequest()
if ($isActiveLedgerProfile) {
stopPollingLedgerNanoStatus()
}
clearLayer2TokensPoll()
})
</script>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
LedgerAppName,
LedgerConnectionState,
determineLedgerConnectionState,
ledgerNanoStatus,
ledgerDeviceState,
} from '@core/ledger'
import { Router } from '@core/router'
import { Animation, FontWeight, Icon, Pane, Text, TextType } from '@ui'
Expand All @@ -16,7 +16,7 @@
const LOCALE_BASE_PATH = 'views.dashboard.drawers.networkConfig.connectLedgerDevice'
$: ledgerConnectionState = determineLedgerConnectionState($ledgerNanoStatus, LedgerAppName.Ethereum)
$: ledgerConnectionState = determineLedgerConnectionState($ledgerDeviceState, LedgerAppName.Ethereum)
$: isConnectedAndUnlocked =
ledgerConnectionState !== LedgerConnectionState.NotConnected &&
ledgerConnectionState !== LedgerConnectionState.Locked
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
<script lang="ts">
import { localize } from '@core/i18n'
import { LedgerAppName, pollLedgerNanoStatus, stopPollingLedgerNanoStatus } from '@core/ledger'
import { LedgerAppName, pollLedgerDeviceState, stopPollingLedgerDeviceState } from '@core/ledger'
import { PopupId, openPopup } from '@desktop/auxiliary/popup'
import { Icon, Link, Text } from '@ui'
import { OnboardingLayout } from '@views/components'
Expand All @@ -12,7 +12,7 @@
}
function onBackClick(): void {
stopPollingLedgerNanoStatus()
stopPollingLedgerDeviceState()
$createFromLedgerRouter.previous()
}
Expand All @@ -23,7 +23,7 @@
}
onMount(() => {
pollLedgerNanoStatus()
pollLedgerDeviceState()
})
</script>

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,8 +26,8 @@
import {
checkOrConnectLedger,
handleLedgerError,
pollLedgerNanoStatus,
stopPollingLedgerNanoStatus,
pollLedgerDeviceState,
stopPollingLedgerDeviceState,
} from '@core/ledger'
import { unsubscribeFromWalletApiEvents } from '@core/profile-manager'
import { closePopup } from '@desktop/auxiliary/popup'
Expand Down Expand Up @@ -63,14 +63,14 @@
async function ledgerRaceConditionProtectionWrapper(_function: () => unknown): Promise<void> {
try {
if ($isOnboardingLedgerProfile) {
stopPollingLedgerNanoStatus()
stopPollingLedgerDeviceState()
}
await _function()
} catch (err) {
console.error('Error in ledgerRaceConditionProtectionWrapper')
} finally {
if ($isOnboardingLedgerProfile) {
pollLedgerNanoStatus()
pollLedgerDeviceState()
}
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { get } from 'svelte/store'
import { stopPollingLedgerNanoStatus } from '@core/ledger'
import { stopPollingLedgerDeviceState } from '@core/ledger'
import { destroyProfileManager, profileManager } from '@core/profile-manager'
import { waitForPreviousManagerToBeDestroyed } from '@core/profile/utils'
import { buildInitialOnboardingProfile } from '../helpers'
Expand All @@ -13,7 +13,7 @@ export async function initialiseOnboardingProfile(): Promise<void> {

if (get(profileManager)) {
if (get(isOnboardingLedgerProfile)) {
stopPollingLedgerNanoStatus()
stopPollingLedgerDeviceState()
}
await destroyProfileManager()
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { IAppUpdateDownloadProgress, IAppVersionDetails, INFTDownloadState } from '.'
import { IEvmAddress, IEvmTransactionSignature } from '@core/layer-2/interfaces'
import { ILedgerEthereumAppSettings } from '@core/ledger/interfaces'

export interface IPlatformEventMap {
'menu-logout': void
Expand All @@ -17,6 +18,7 @@ export interface IPlatformEventMap {
'notification-activated': unknown
'nft-download-done': INFTDownloadState
'nft-download-interrupted': INFTDownloadState
'ethereum-app-settings': ILedgerEthereumAppSettings
'evm-address': IEvmAddress
'evm-signed-transaction': IEvmTransactionSignature
'ledger-error': Error
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { openPopup, PopupId, popupState } from '../../../../../../desktop/lib/auxiliary/popup'
import { get } from 'svelte/store'
import { handleError } from '@core/error/handlers/handleError'
import { determineLedgerConnectionState, LedgerAppName, LedgerConnectionState, ledgerNanoStatus } from '..'
import { determineLedgerConnectionState, LedgerAppName, LedgerConnectionState, ledgerDeviceState } from '..'

export function checkOrConnectLedger(
callback: () => Promise<unknown> = async (): Promise<void> => {},
Expand All @@ -19,7 +19,7 @@ export function checkOrConnectLedger(
}
}
try {
const ledgerConnectionState = determineLedgerConnectionState(get(ledgerNanoStatus), ledgerAppName)
const ledgerConnectionState = determineLedgerConnectionState(get(ledgerDeviceState), ledgerAppName)
const ledgerConnected = ledgerConnectionState === LedgerConnectionState.CorrectAppOpen
if (ledgerConnected) {
return callback()
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import { profileManager as _profileManager } from '@core/profile-manager/stores'
import { getLedgerNanoStatus } from '@lib/core/profile-manager/api'
import { Ledger } from '../classes'
import { resetLedgerDeviceState, setLedgerDeviceState } from '../stores'
import { sleep } from '@core/utils'

let isMakingRequest: boolean = false

export async function getAndUpdateLedgerDeviceState(
profileManager = _profileManager,
forwardErrors = false
): Promise<void> {
try {
if (!isMakingRequest) {
isMakingRequest = true
const ledgerNanoStatus = await getLedgerNanoStatus(profileManager)
// sleep to make sure ledger process on iota sdk is wrapped up
await sleep(50)
const ethereumAppSettings = await Ledger.getEthereumAppSettings()
setLedgerDeviceState(ledgerNanoStatus, ethereumAppSettings)
}
} catch (err) {
resetLedgerDeviceState()
if (forwardErrors) {
return Promise.reject(err)
} else {
console.error(err)
}
} finally {
isMakingRequest = false
}
}
Loading

0 comments on commit 49b5396

Please sign in to comment.