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 Dec 8, 2022
1 parent 10cba2f commit fdd260a
Show file tree
Hide file tree
Showing 12 changed files with 352 additions and 18 deletions.
2 changes: 1 addition & 1 deletion background/constants/networks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -114,7 +114,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_SUPPORTED_BY_LEDGER = [ETHEREUM, POLYGON, ROOTSTOCK]

// Networks that are not added to this struct will
// not have an in-wallet Swap page
Expand Down
4 changes: 4 additions & 0 deletions background/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1011,6 +1011,10 @@ export default class Main extends BaseService<never> {
this.ledgerService.emitter.on("usbDeviceCount", (usbDeviceCount) => {
this.store.dispatch(setUsbDeviceCount({ usbDeviceCount }))
})

uiSliceEmitter.on("derivationPathChange", (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
}

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

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 @@ -7,6 +7,7 @@ import { AnalyticsPreferences } from "../services/preferences/types"
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 @@ -40,6 +41,7 @@ export type Events = {
snackbarMessage: string
newDefaultWalletValue: boolean
refreshBackgroundPage: null
derivationPathChange: string
newSelectedAccount: AddressOnNetwork
newSelectedAccountSwitched: AddressOnNetwork
userActivityEncountered: AddressOnNetwork
Expand Down Expand Up @@ -247,13 +249,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 @@ -265,6 +267,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
32 changes: 26 additions & 6 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_SUPPORTED_BY_LEDGER, 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 ROOTSTOCK_DERIVATION_PATH = "44'/137'/0'/0"

async function deriveAddressOnLedger(path: string, eth: Eth) {
const derivedIdentifiers = await eth.getAddress(path)

if (path.includes(ROOTSTOCK_DERIVATION_PATH.slice(0, 8))) {
// ethersGetAddress rejects Rootstock addresses so using toChecksumAddress
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 Expand Up @@ -535,7 +555,7 @@ export default class LedgerService extends BaseService<Events> {
hexDataToSign: HexString
): Promise<string> {
if (
!NETWORK_FOR_LEDGER_SIGNING.find((supportedNetwork) =>
!NETWORK_SUPPORTED_BY_LEDGER.find((supportedNetwork) =>
sameNetwork(network, supportedNetwork)
)
) {
Expand Down
10 changes: 9 additions & 1 deletion ui/_locales/en/messages.json
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,13 @@
"checkLedger": "Check Ledger",
"onlyRejectFromLedger": "Tx can only be Rejected from Ledger",
"onboarding": {
"selectLedgerApp": {
"initialScreenHeader": "Select Ledger Live App",
"ecosystem": "{{network}} ecosystem",
"includes": "Includes",
"subheading": "Select which app you would like to start with",
"continueButton": "Continue"
},
"prepare": {
"continueButton": "Continue",
"tryAgainButton": "Try Again",
Expand All @@ -112,7 +119,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
28 changes: 28 additions & 0 deletions ui/components/LedgerMenu/EcosystemNetworkIcon.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import React, { ReactElement } from "react"
import { EVMNetwork } from "@tallyho/tally-background/networks"

interface Props {
network: EVMNetwork
}

export default function EcosystemNetworkIcon(props: Props): ReactElement {
const { network } = props

return (
<span className="icon_child">
<style jsx>
{`
.icon_child {
margin-right: 2px;
background: url("./images/networks/${network.name
.replaceAll(" ", "")
.toLowerCase()}@2x.png");
background-size: cover;
width: 15px;
height: 15px;
}
`}
</style>
</span>
)
}
49 changes: 49 additions & 0 deletions ui/components/LedgerMenu/LedgerMenuProtocolList.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
import React, { ReactElement } from "react"
import {
ARBITRUM_ONE,
ETHEREUM,
OPTIMISM,
POLYGON,
ROOTSTOCK,
} from "@tallyho/tally-background/constants"
import { FeatureFlags, isEnabled } from "@tallyho/tally-background/features"
import { sameNetwork } from "@tallyho/tally-background/networks"
import { selectCurrentNetwork } from "@tallyho/tally-background/redux-slices/selectors"
import { useBackgroundSelector } from "../../hooks"
import LedgerMenuProtocolListItem from "./LedgerMenuProtocolListItem"

const ledgerApps = [
{
network: ETHEREUM,
ecosystem: [OPTIMISM, ARBITRUM_ONE],
},
{
network: POLYGON,
},
...(isEnabled(FeatureFlags.SUPPORT_RSK)
? [
{
network: ROOTSTOCK,
},
]
: []),
]

export default function LedgerMenuProtocolList(): ReactElement {
const currentNetwork = useBackgroundSelector(selectCurrentNetwork)

return (
<div className="standard_width_padded center_horizontal">
<ul>
{ledgerApps.map((info) => (
<LedgerMenuProtocolListItem
isSelected={sameNetwork(currentNetwork, info.network)}
key={info.network.name}
network={info.network}
ecosystem={info.ecosystem}
/>
))}
</ul>
</div>
)
}
Loading

0 comments on commit fdd260a

Please sign in to comment.