Skip to content

Commit df317c6

Browse files
committed
feat: rsk ledger integration
1 parent 75e7428 commit df317c6

File tree

15 files changed

+401
-18
lines changed

15 files changed

+401
-18
lines changed

background/constants/networks.ts

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -114,7 +114,13 @@ export const TEST_NETWORK_BY_CHAIN_ID = new Set(
114114
[GOERLI].map((network) => network.chainID)
115115
)
116116

117-
export const NETWORK_FOR_LEDGER_SIGNING = [ETHEREUM, POLYGON]
117+
export const NETWORK_SUPPORTED_BY_LEDGER = [
118+
ETHEREUM,
119+
POLYGON,
120+
ROOTSTOCK,
121+
AVALANCHE,
122+
BINANCE_SMART_CHAIN,
123+
]
118124

119125
// Networks that are not added to this struct will
120126
// not have an in-wallet Swap page

background/main.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1019,6 +1019,10 @@ export default class Main extends BaseService<never> {
10191019
this.ledgerService.emitter.on("usbDeviceCount", (usbDeviceCount) => {
10201020
this.store.dispatch(setUsbDeviceCount({ usbDeviceCount }))
10211021
})
1022+
1023+
uiSliceEmitter.on("derivationPathChange", (path: string) => {
1024+
this.ledgerService.setDefaultDerivationPath(path)
1025+
})
10221026
}
10231027

10241028
async connectKeyringService(): Promise<void> {

background/redux-slices/ledger.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ export type LedgerState = {
3131
/** Devices by ID */
3232
devices: Record<string, LedgerDeviceState>
3333
usbDeviceCount: number
34+
derivationPath?: string
3435
}
3536

3637
export type Events = {
@@ -95,6 +96,12 @@ const ledgerSlice = createSlice({
9596
if (!(deviceID in immerState.devices)) return
9697
immerState.currentDeviceID = deviceID
9798
},
99+
setDerivationPath: (
100+
immerState,
101+
{ payload: derivationPath }: { payload: string }
102+
) => {
103+
immerState.derivationPath = derivationPath
104+
},
98105
setDeviceConnectionStatus: (
99106
immerState,
100107
{
@@ -224,6 +231,7 @@ export const {
224231
addLedgerAccount,
225232
setUsbDeviceCount,
226233
removeDevice,
234+
setDerivationPath,
227235
} = ledgerSlice.actions
228236

229237
export default ledgerSlice.reducer

background/redux-slices/selectors/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
export * from "./activitiesSelectors"
22
export * from "./accountsSelectors"
3+
export * from "./ledgerSelectors"
34
export * from "./keyringsSelectors"
45
export * from "./signingSelectors"
56
export * from "./dappSelectors"

background/redux-slices/selectors/ledgerSelectors.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,4 +20,9 @@ export const selectLedgerDeviceByAddresses = createSelector(
2020
}
2121
)
2222

23+
export const selectLedgerDerivationPath = createSelector(
24+
(state: RootState) => state.ledger.derivationPath,
25+
(path) => path
26+
)
27+
2328
export default {}

background/redux-slices/ui.ts

Lines changed: 16 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import { AnalyticsPreferences } from "../services/preferences/types"
77
import { AccountSignerWithId } from "../signing"
88
import { AccountSignerSettings } from "../ui"
99
import { AccountState, addAddressNetwork } from "./accounts"
10+
import { setDerivationPath } from "./ledger"
1011
import { createBackgroundAsyncThunk } from "./utils"
1112

1213
const defaultSettings = {
@@ -40,6 +41,7 @@ export type Events = {
4041
snackbarMessage: string
4142
newDefaultWalletValue: boolean
4243
refreshBackgroundPage: null
44+
derivationPathChange: string
4345
newSelectedAccount: AddressOnNetwork
4446
newSelectedAccountSwitched: AddressOnNetwork
4547
userActivityEncountered: AddressOnNetwork
@@ -247,13 +249,13 @@ export const setSelectedNetwork = createBackgroundAsyncThunk(
247249
emitter.emit("newSelectedNetwork", network)
248250
// Add any accounts on the currently selected network to the newly
249251
// selected network - if those accounts don't yet exist on it.
250-
Object.keys(account.accountsData.evm[currentlySelectedChainID]).forEach(
251-
(address) => {
252-
if (!account.accountsData.evm[network.chainID]?.[address]) {
253-
dispatch(addAddressNetwork({ address, network }))
254-
}
252+
Object.keys(
253+
account.accountsData.evm[currentlySelectedChainID] ?? []
254+
).forEach((address) => {
255+
if (!account.accountsData.evm[network.chainID]?.[address]) {
256+
dispatch(addAddressNetwork({ address, network }))
255257
}
256-
)
258+
})
257259
dispatch(setNewSelectedAccount({ ...ui.selectedAccount, network }))
258260
}
259261
)
@@ -265,6 +267,14 @@ export const refreshBackgroundPage = createBackgroundAsyncThunk(
265267
}
266268
)
267269

270+
export const derivationPathChange = createBackgroundAsyncThunk(
271+
"ui/derivationPathChange",
272+
async (derivationPath: string, { dispatch }) => {
273+
await emitter.emit("derivationPathChange", derivationPath)
274+
dispatch(setDerivationPath(derivationPath))
275+
}
276+
)
277+
268278
export const selectUI = createSelector(
269279
(state: { ui: UIState }): UIState => state.ui,
270280
(uiState) => uiState

background/services/ledger/index.ts

Lines changed: 26 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import Transport from "@ledgerhq/hw-transport"
22
import TransportWebUSB from "@ledgerhq/hw-transport-webusb"
3+
import { toChecksumAddress } from "@tallyho/hd-keyring"
34
import Eth from "@ledgerhq/hw-app-eth"
45
import { DeviceModelId } from "@ledgerhq/devices"
56
import {
@@ -25,7 +26,7 @@ import { ServiceCreatorFunction, ServiceLifecycleEvents } from "../types"
2526
import logger from "../../lib/logger"
2627
import { getOrCreateDB, LedgerAccount, LedgerDatabase } from "./db"
2728
import { ethersTransactionFromTransactionRequest } from "../chain/utils"
28-
import { NETWORK_FOR_LEDGER_SIGNING } from "../../constants"
29+
import { NETWORK_SUPPORTED_BY_LEDGER, ROOTSTOCK } from "../../constants"
2930
import { normalizeEVMAddress } from "../../lib/utils"
3031
import { AddressOnNetwork } from "../../accounts"
3132

@@ -113,15 +114,24 @@ type Events = ServiceLifecycleEvents & {
113114

114115
export const idDerivationPath = "44'/60'/0'/0/0"
115116

117+
const ROOTSTOCK_DERIVATION_PATH = "44'/137'/0'/0"
118+
116119
async function deriveAddressOnLedger(path: string, eth: Eth) {
117120
const derivedIdentifiers = await eth.getAddress(path)
121+
122+
if (path.includes(ROOTSTOCK_DERIVATION_PATH.slice(0, 8))) {
123+
// ethersGetAddress rejects Rootstock addresses so using toChecksumAddress
124+
return toChecksumAddress(derivedIdentifiers.address, +ROOTSTOCK.chainID)
125+
}
126+
118127
const address = ethersGetAddress(derivedIdentifiers.address)
119128
return address
120129
}
121130

122131
async function generateLedgerId(
123132
transport: Transport,
124-
eth: Eth
133+
eth: Eth,
134+
derivationPath: string
125135
): Promise<[string | undefined, LedgerType]> {
126136
let extensionDeviceType = LedgerType.UNKNOWN
127137

@@ -147,7 +157,7 @@ async function generateLedgerId(
147157
return [undefined, extensionDeviceType]
148158
}
149159

150-
const address = await deriveAddressOnLedger(idDerivationPath, eth)
160+
const address = await deriveAddressOnLedger(derivationPath, eth)
151161

152162
return [address, extensionDeviceType]
153163
}
@@ -172,6 +182,8 @@ async function generateLedgerId(
172182
export default class LedgerService extends BaseService<Events> {
173183
#currentLedgerId: string | null = null
174184

185+
#derivationPath: string = idDerivationPath
186+
175187
transport: Transport | undefined = undefined
176188

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

210222
const eth = new Eth(this.transport)
211223

212-
const [id, type] = await generateLedgerId(this.transport, eth)
224+
const [id, type] = await generateLedgerId(
225+
this.transport,
226+
eth,
227+
this.#derivationPath
228+
)
213229

214230
if (!id) {
215231
throw new Error("Can't derive meaningful identification address!")
@@ -239,7 +255,7 @@ export default class LedgerService extends BaseService<Events> {
239255
this.emitter.emit("ledgerAdded", {
240256
id: this.#currentLedgerId,
241257
type,
242-
accountIDs: [idDerivationPath],
258+
accountIDs: [this.#derivationPath],
243259
metadata: {
244260
ethereumVersion: appData.version,
245261
isArbitraryDataSigningEnabled: appData.arbitraryDataEnabled !== 0,
@@ -250,6 +266,10 @@ export default class LedgerService extends BaseService<Events> {
250266
})
251267
}
252268

269+
setDefaultDerivationPath(path: string): void {
270+
this.#derivationPath = path
271+
}
272+
253273
#handleUSBConnect = async (event: USBConnectionEvent): Promise<void> => {
254274
this.emitter.emit(
255275
"usbDeviceCount",
@@ -535,7 +555,7 @@ export default class LedgerService extends BaseService<Events> {
535555
hexDataToSign: HexString
536556
): Promise<string> {
537557
if (
538-
!NETWORK_FOR_LEDGER_SIGNING.find((supportedNetwork) =>
558+
!NETWORK_SUPPORTED_BY_LEDGER.find((supportedNetwork) =>
539559
sameNetwork(network, supportedNetwork)
540560
)
541561
) {

ui/_locales/en/messages.json

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -99,6 +99,13 @@
9999
"checkLedger": "Check Ledger",
100100
"onlyRejectFromLedger": "Tx can only be Rejected from Ledger",
101101
"onboarding": {
102+
"selectLedgerApp": {
103+
"initialScreenHeader": "Select Ledger Live App",
104+
"ecosystem": "{{network}} ecosystem",
105+
"includes": "Includes",
106+
"subheading": "Select which app you would like to start with",
107+
"continueButton": "Continue"
108+
},
102109
"prepare": {
103110
"continueButton": "Continue",
104111
"tryAgainButton": "Try Again",
@@ -112,7 +119,8 @@
112119
"stepsExplainer": "Please follow the steps below and click on Try Again!",
113120
"step1": "Plug in a single Ledger",
114121
"step2": "Enter pin to unlock",
115-
"step3": "Open Ethereum App"
122+
"step3": "Open {{network}} App",
123+
"derivationPath": "Select derivation path to connect with ledger"
116124
},
117125
"selectDevice": "Select the device",
118126
"clickConnect": "Click connect",

ui/components/Ledger/LedgerPanelContainer.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ export default function LedgerPanelContainer({
3030
max-width: 24rem;
3131
margin: 0 auto;
3232
padding: 1rem;
33+
position: relative;
3334
}
3435
3536
.indicator {
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
import React, { ReactElement } from "react"
2+
3+
interface Props {
4+
networkName: string
5+
}
6+
7+
export default function EcosystemNetworkIcon(props: Props): ReactElement {
8+
const { networkName } = props
9+
10+
return (
11+
<span className="icon_child">
12+
<style jsx>
13+
{`
14+
.icon_child {
15+
margin-right: 2px;
16+
background: url("./images/networks/${networkName
17+
.replaceAll(" ", "")
18+
.toLowerCase()}@2x.png");
19+
background-size: cover;
20+
width: 12px;
21+
height: 12px;
22+
}
23+
`}
24+
</style>
25+
</span>
26+
)
27+
}

0 commit comments

Comments
 (0)