Skip to content

Commit

Permalink
feat: rsk ledger integration
Browse files Browse the repository at this point in the history
  • Loading branch information
ahsan-javaiid committed Nov 15, 2022
1 parent c7b8879 commit d69c094
Show file tree
Hide file tree
Showing 10 changed files with 129 additions and 16 deletions.
2 changes: 1 addition & 1 deletion background/constants/networks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
4 changes: 4 additions & 0 deletions background/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -945,6 +945,10 @@ export default class Main extends BaseService<never> {
this.ledgerService.emitter.on("usbDeviceCount", (usbDeviceCount) => {
this.store.dispatch(setUsbDeviceCount({ usbDeviceCount }))
})

uiSliceEmitter.on("derivationPathChange", async (path: string) => {
this.ledgerService.setDefaultDerivationPath(path)
})
}

async connectKeyringService(): Promise<void> {
Expand Down
9 changes: 9 additions & 0 deletions background/redux-slices/ledger.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ export type LedgerState = {
/** Devices by ID */
devices: Record<string, LedgerDeviceState>
usbDeviceCount: number
derivationPath?: string | null
}

export type Events = {
Expand Down Expand Up @@ -58,6 +59,7 @@ export const initialState: LedgerState = {
currentDeviceID: null,
devices: {},
usbDeviceCount: 0,
derivationPath: null,
}

const ledgerSlice = createSlice({
Expand Down Expand Up @@ -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,
{
Expand Down Expand Up @@ -224,6 +232,7 @@ export const {
addLedgerAccount,
setUsbDeviceCount,
removeDevice,
setDerivationPath,
} = ledgerSlice.actions

export default ledgerSlice.reducer
Expand Down
22 changes: 16 additions & 6 deletions background/redux-slices/ui.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 = {
Expand Down Expand Up @@ -37,6 +38,7 @@ export type Events = {
snackbarMessage: string
newDefaultWalletValue: boolean
refreshBackgroundPage: null
derivationPathChange: string
newSelectedAccount: AddressOnNetwork
newSelectedAccountSwitched: AddressOnNetwork
userActivityEncountered: AddressOnNetwork
Expand Down Expand Up @@ -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 }))
}
)
Expand All @@ -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
Expand Down
30 changes: 25 additions & 5 deletions background/services/ledger/index.ts
Original file line number Diff line number Diff line change
@@ -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 {
Expand All @@ -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"

Expand Down Expand Up @@ -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

Expand All @@ -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]
}
Expand All @@ -172,6 +182,8 @@ async function generateLedgerId(
export default class LedgerService extends BaseService<Events> {
#currentLedgerId: string | null = null

#derivationPath: string = idDerivationPath

transport: Transport | undefined = undefined

#lastOperationPromise = Promise.resolve()
Expand Down Expand Up @@ -209,7 +221,11 @@ export default class LedgerService extends BaseService<Events> {

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!")
Expand Down Expand Up @@ -239,7 +255,7 @@ export default class LedgerService extends BaseService<Events> {
this.emitter.emit("ledgerAdded", {
id: this.#currentLedgerId,
type,
accountIDs: [idDerivationPath],
accountIDs: [this.#derivationPath],
metadata: {
ethereumVersion: appData.version,
isArbitraryDataSigningEnabled: appData.arbitraryDataEnabled !== 0,
Expand All @@ -250,6 +266,10 @@ export default class LedgerService extends BaseService<Events> {
})
}

setDefaultDerivationPath(path: string): void {
this.#derivationPath = path
}

#handleUSBConnect = async (event: USBConnectionEvent): Promise<void> => {
this.emitter.emit(
"usbDeviceCount",
Expand Down
3 changes: 2 additions & 1 deletion ui/_locales/en/messages.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
3 changes: 3 additions & 0 deletions ui/components/Ledger/LedgerPanelContainer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -21,6 +23,7 @@ export default function LedgerPanelContainer({
alt=""
/>
{heading && <h1 className="heading">{heading}</h1>}
{derivationPath}
{subHeading && <p className="subheading">{subHeading}</p>}
{children}
<style jsx>{`
Expand Down
10 changes: 8 additions & 2 deletions ui/components/Onboarding/OnboardingDerivationPathSelect.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import SharedButton from "../Shared/SharedButton"
import SharedInput from "../Shared/SharedInput"
import SharedModal from "../Shared/SharedModal"
import SharedSelect, { Option } from "../Shared/SharedSelect"
import { useBackgroundSelector } from "../../hooks"

// TODO make this network specific
const initialDerivationPaths: Option[] = [
Expand Down Expand Up @@ -51,11 +52,16 @@ export default function OnboardingDerivationPathSelect({
}): ReactElement {
const { t } = useTranslation("translation", { keyPrefix: "onboarding" })
const [derivationPaths, setDerivationPaths] = useState(initialDerivationPaths)

const ledgerState = useBackgroundSelector((state) => state.ledger)
const [modalStep, setModalStep] = useState(0)
const [customPath, setCustomPath] = useState(initialCustomPath)
const [customPathLabel, setCustomPathLabel] = useState("")
const [defaultIndex, setDefaultIndex] = useState<number>()
const selectedIndex = initialDerivationPaths.findIndex(
(path) => path.value === `m/${ledgerState.derivationPath}`
)
const [defaultIndex, setDefaultIndex] = useState<number>(
selectedIndex === -1 ? 0 : selectedIndex
)

// Reset value to display placeholder after adding a custom path
const customPathValue = customPath.isReset
Expand Down
7 changes: 6 additions & 1 deletion ui/pages/Ledger/LedgerPrepare.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
import React, { ReactElement } from "react"
import { useTranslation } from "react-i18next"
import { selectCurrentNetwork } from "@tallyho/tally-background/redux-slices/selectors"
import LedgerContinueButton from "../../components/Ledger/LedgerContinueButton"
import LedgerPanelContainer from "../../components/Ledger/LedgerPanelContainer"
import { useBackgroundSelector } from "../../hooks"
import LedgerSelectDerivationPath from "./LedgerSelectDerivationPath"

export default function LedgerPrepare({
onContinue,
Expand All @@ -15,6 +18,7 @@ export default function LedgerPrepare({
const { t } = useTranslation("translation", {
keyPrefix: "ledger.onboarding.prepare",
})
const selectedNetwork = useBackgroundSelector(selectCurrentNetwork)
const buttonLabel = initialScreen ? t("continueButton") : t("tryAgainButton")
const subHeadingWord = initialScreen
? t("subheadingWord1")
Expand All @@ -28,6 +32,7 @@ export default function LedgerPrepare({
subHeading={t("subheading", {
subheadingWord: subHeadingWord,
})}
derivationPath={<LedgerSelectDerivationPath />}
>
{!initialScreen && deviceCount !== 1 ? (
<div className="steps">
Expand All @@ -45,7 +50,7 @@ export default function LedgerPrepare({
<ol className="steps">
<li>{t("step1")}</li>
<li>{t("step2")}</li>
<li>{t("step3")}</li>
<li>{t("step3", { network: selectedNetwork.name })}</li>
</ol>
<LedgerContinueButton onClick={onContinue}>
{buttonLabel}
Expand Down
55 changes: 55 additions & 0 deletions ui/pages/Ledger/LedgerSelectDerivationPath.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
import React, { ReactElement } from "react"
import { useTranslation } from "react-i18next"
import {
derivationPathChange,
setSelectedNetwork,
} from "@tallyho/tally-background/redux-slices/ui"
import { ROOTSTOCK, ETHEREUM } from "@tallyho/tally-background/constants"
import { EVMNetwork } from "@tallyho/tally-background/networks"
import { useBackgroundDispatch } from "../../hooks"
import SelectDerivationPath from "../../components/Onboarding/OnboardingDerivationPathSelect"

const DERIVATION_PATH_TO_NETWORK: { [key: string]: EVMNetwork } = {
"m/44'/137'/0'/0": ROOTSTOCK,
"m/44'/60'/0'/0": ETHEREUM,
"m/44'/1'/0'/0": ETHEREUM,
"m/44'/60'/0'": ETHEREUM,
}

export default function LedgerSelectDerivationPath(): ReactElement {
const dispatch = useBackgroundDispatch()
const { t } = useTranslation("translation", {
keyPrefix: "ledger.onboarding.prepare",
})

const onDerivationPathChange = (path: string) => {
if (!path.includes("x")) {
dispatch(derivationPathChange(path.slice(2)))
dispatch(setSelectedNetwork(DERIVATION_PATH_TO_NETWORK[path]))
}
}

return (
<>
<div className="derivation_path_heading">{t("derivationPath")}</div>
<div className="derivation_path">
<SelectDerivationPath onChange={onDerivationPathChange} />
</div>
<style jsx>{`
.derivation_path {
margin: 0.5rem 0;
padding: 1rem 1.5rem;
border-radius: 4px;
background: var(--hunter-green);
}
.derivation_path_heading {
margin: 0.25rem;
font-size: 16px;
line-height: 24px;
text-align: center;
color: var(--green-40);
}
`}</style>
</>
)
}

0 comments on commit d69c094

Please sign in to comment.