diff --git a/background/constants/networks.ts b/background/constants/networks.ts index 26003af935..c0bdad4dc8 100644 --- a/background/constants/networks.ts +++ b/background/constants/networks.ts @@ -85,7 +85,7 @@ export const TEST_NETWORK_BY_CHAIN_ID = new Set( [GOERLI].map((network) => network.chainID) ) -export const NETWORK_FOR_LEDGER_SIGNING = [ETHEREUM, POLYGON] +export const NETWORK_FOR_LEDGER_SIGNING = [ETHEREUM, POLYGON, ROOTSTOCK] // Networks that are not added to this struct will // not have an in-wallet NFT tab diff --git a/background/main.ts b/background/main.ts index 4911e5fc26..048add9039 100644 --- a/background/main.ts +++ b/background/main.ts @@ -945,6 +945,10 @@ export default class Main extends BaseService { this.ledgerService.emitter.on("usbDeviceCount", (usbDeviceCount) => { this.store.dispatch(setUsbDeviceCount({ usbDeviceCount })) }) + + uiSliceEmitter.on("derivationPathChange", async (path: string) => { + this.ledgerService.setDefaultDerivationPath(path) + }) } async connectKeyringService(): Promise { diff --git a/background/redux-slices/ledger.ts b/background/redux-slices/ledger.ts index c0ea3f573f..595936409a 100644 --- a/background/redux-slices/ledger.ts +++ b/background/redux-slices/ledger.ts @@ -31,6 +31,7 @@ export type LedgerState = { /** Devices by ID */ devices: Record usbDeviceCount: number + derivationPath?: string | null } export type Events = { @@ -58,6 +59,7 @@ export const initialState: LedgerState = { currentDeviceID: null, devices: {}, usbDeviceCount: 0, + derivationPath: null, } const ledgerSlice = createSlice({ @@ -95,6 +97,12 @@ const ledgerSlice = createSlice({ if (!(deviceID in immerState.devices)) return immerState.currentDeviceID = deviceID }, + setDerivationPath: ( + immerState, + { payload: derivationPath }: { payload: string } + ) => { + immerState.derivationPath = derivationPath + }, setDeviceConnectionStatus: ( immerState, { @@ -224,6 +232,7 @@ export const { addLedgerAccount, setUsbDeviceCount, removeDevice, + setDerivationPath, } = ledgerSlice.actions export default ledgerSlice.reducer diff --git a/background/redux-slices/ui.ts b/background/redux-slices/ui.ts index 93cce3b945..c60a1e862a 100644 --- a/background/redux-slices/ui.ts +++ b/background/redux-slices/ui.ts @@ -6,6 +6,7 @@ import { EVMNetwork } from "../networks" import { AccountSignerWithId } from "../signing" import { AccountSignerSettings } from "../ui" import { AccountState, addAddressNetwork } from "./accounts" +import { setDerivationPath } from "./ledger" import { createBackgroundAsyncThunk } from "./utils" const defaultSettings = { @@ -37,6 +38,7 @@ export type Events = { snackbarMessage: string newDefaultWalletValue: boolean refreshBackgroundPage: null + derivationPathChange: string newSelectedAccount: AddressOnNetwork newSelectedAccountSwitched: AddressOnNetwork userActivityEncountered: AddressOnNetwork @@ -223,13 +225,13 @@ export const setSelectedNetwork = createBackgroundAsyncThunk( emitter.emit("newSelectedNetwork", network) // Add any accounts on the currently selected network to the newly // selected network - if those accounts don't yet exist on it. - Object.keys(account.accountsData.evm[currentlySelectedChainID]).forEach( - (address) => { - if (!account.accountsData.evm[network.chainID]?.[address]) { - dispatch(addAddressNetwork({ address, network })) - } + Object.keys( + account.accountsData.evm[currentlySelectedChainID] ?? [] + ).forEach((address) => { + if (!account.accountsData.evm[network.chainID]?.[address]) { + dispatch(addAddressNetwork({ address, network })) } - ) + }) dispatch(setNewSelectedAccount({ ...ui.selectedAccount, network })) } ) @@ -241,6 +243,14 @@ export const refreshBackgroundPage = createBackgroundAsyncThunk( } ) +export const derivationPathChange = createBackgroundAsyncThunk( + "ui/derivationPathChange", + async (derivationPath: string, { dispatch }) => { + await emitter.emit("derivationPathChange", derivationPath) + dispatch(setDerivationPath(derivationPath)) + } +) + export const selectUI = createSelector( (state: { ui: UIState }): UIState => state.ui, (uiState) => uiState diff --git a/background/services/ledger/index.ts b/background/services/ledger/index.ts index b87a81632c..87bfb7b434 100644 --- a/background/services/ledger/index.ts +++ b/background/services/ledger/index.ts @@ -1,5 +1,6 @@ import Transport from "@ledgerhq/hw-transport" import TransportWebUSB from "@ledgerhq/hw-transport-webusb" +import { toChecksumAddress } from "@tallyho/hd-keyring" import Eth from "@ledgerhq/hw-app-eth" import { DeviceModelId } from "@ledgerhq/devices" import { @@ -25,7 +26,7 @@ import { ServiceCreatorFunction, ServiceLifecycleEvents } from "../types" import logger from "../../lib/logger" import { getOrCreateDB, LedgerAccount, LedgerDatabase } from "./db" import { ethersTransactionFromTransactionRequest } from "../chain/utils" -import { NETWORK_FOR_LEDGER_SIGNING } from "../../constants" +import { NETWORK_FOR_LEDGER_SIGNING, ROOTSTOCK } from "../../constants" import { normalizeEVMAddress } from "../../lib/utils" import { AddressOnNetwork } from "../../accounts" @@ -113,15 +114,24 @@ type Events = ServiceLifecycleEvents & { export const idDerivationPath = "44'/60'/0'/0/0" +const RSK_DERIVATION_PATH = "44'/137'/0'/0" + async function deriveAddressOnLedger(path: string, eth: Eth) { const derivedIdentifiers = await eth.getAddress(path) + + if (path.includes(RSK_DERIVATION_PATH.slice(0, 8))) { + // Using @tallyho/hd-keyring to calculate checksum because ethersGetAddress rejects RSK addresses + return toChecksumAddress(derivedIdentifiers.address, +ROOTSTOCK.chainID) + } + const address = ethersGetAddress(derivedIdentifiers.address) return address } async function generateLedgerId( transport: Transport, - eth: Eth + eth: Eth, + derivationPath: string ): Promise<[string | undefined, LedgerType]> { let extensionDeviceType = LedgerType.UNKNOWN @@ -147,7 +157,7 @@ async function generateLedgerId( return [undefined, extensionDeviceType] } - const address = await deriveAddressOnLedger(idDerivationPath, eth) + const address = await deriveAddressOnLedger(derivationPath, eth) return [address, extensionDeviceType] } @@ -172,6 +182,8 @@ async function generateLedgerId( export default class LedgerService extends BaseService { #currentLedgerId: string | null = null + #derivationPath: string = idDerivationPath + transport: Transport | undefined = undefined #lastOperationPromise = Promise.resolve() @@ -209,7 +221,11 @@ export default class LedgerService extends BaseService { const eth = new Eth(this.transport) - const [id, type] = await generateLedgerId(this.transport, eth) + const [id, type] = await generateLedgerId( + this.transport, + eth, + this.#derivationPath + ) if (!id) { throw new Error("Can't derive meaningful identification address!") @@ -239,7 +255,7 @@ export default class LedgerService extends BaseService { this.emitter.emit("ledgerAdded", { id: this.#currentLedgerId, type, - accountIDs: [idDerivationPath], + accountIDs: [this.#derivationPath], metadata: { ethereumVersion: appData.version, isArbitraryDataSigningEnabled: appData.arbitraryDataEnabled !== 0, @@ -250,6 +266,10 @@ export default class LedgerService extends BaseService { }) } + setDefaultDerivationPath(path: string): void { + this.#derivationPath = path + } + #handleUSBConnect = async (event: USBConnectionEvent): Promise => { this.emitter.emit( "usbDeviceCount", diff --git a/ui/_locales/en/messages.json b/ui/_locales/en/messages.json index 7d3622e423..14feee75f2 100644 --- a/ui/_locales/en/messages.json +++ b/ui/_locales/en/messages.json @@ -108,7 +108,8 @@ "stepsExplainer": "Please follow the steps below and click on Try Again!", "step1": "Plug in a single Ledger", "step2": "Enter pin to unlock", - "step3": "Open Ethereum App" + "step3": "Open {{network}} App", + "derivationPath": "Select derivation path to connect with ledger" }, "selectDevice": "Select the device", "clickConnect": "Click connect", diff --git a/ui/components/Ledger/LedgerPanelContainer.tsx b/ui/components/Ledger/LedgerPanelContainer.tsx index 54dbb1b38f..61e950d803 100644 --- a/ui/components/Ledger/LedgerPanelContainer.tsx +++ b/ui/components/Ledger/LedgerPanelContainer.tsx @@ -3,11 +3,13 @@ import React, { ReactElement } from "react" export default function LedgerPanelContainer({ indicatorImageSrc, heading, + derivationPath, subHeading, children, }: { indicatorImageSrc: string heading?: React.ReactNode + derivationPath?: React.ReactNode subHeading?: React.ReactNode children?: React.ReactNode }): ReactElement { @@ -21,6 +23,7 @@ export default function LedgerPanelContainer({ alt="" /> {heading &&

{heading}

} + {derivationPath} {subHeading &&

{subHeading}

} {children} + + ) +}