Skip to content

Commit 22f1574

Browse files
committed
feat: rsk ledger integration
1 parent 33ab3cb commit 22f1574

File tree

20 files changed

+391
-60
lines changed

20 files changed

+391
-60
lines changed

background/constants/networks.ts

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,8 @@ export const GOERLI: EVMNetwork = {
8585
coingeckoPlatformID: "ethereum",
8686
}
8787

88+
export const DEFAULT_DERIVATION_PATH = "44'/60'/0'/0/0"
89+
8890
export const DEFAULT_NETWORKS = [
8991
ETHEREUM,
9092
POLYGON,
@@ -142,7 +144,13 @@ export const TEST_NETWORK_BY_CHAIN_ID = new Set(
142144
[GOERLI].map((network) => network.chainID)
143145
)
144146

145-
export const NETWORK_FOR_LEDGER_SIGNING = [ETHEREUM, POLYGON]
147+
export const NETWORK_SUPPORTED_BY_LEDGER = [
148+
ETHEREUM,
149+
POLYGON,
150+
ROOTSTOCK,
151+
AVALANCHE,
152+
BINANCE_SMART_CHAIN,
153+
]
146154

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

background/main.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1087,6 +1087,10 @@ export default class Main extends BaseService<never> {
10871087
this.ledgerService.emitter.on("usbDeviceCount", (usbDeviceCount) => {
10881088
this.store.dispatch(setUsbDeviceCount({ usbDeviceCount }))
10891089
})
1090+
1091+
uiSliceEmitter.on("derivationPathChange", (path: string) => {
1092+
this.ledgerService.setDefaultDerivationPath(path)
1093+
})
10901094
}
10911095

10921096
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
export const defaultSettings = {
@@ -41,6 +42,7 @@ export type Events = {
4142
deleteAnalyticsData: never
4243
newDefaultWalletValue: boolean
4344
refreshBackgroundPage: null
45+
derivationPathChange: string
4446
newSelectedAccount: AddressOnNetwork
4547
newSelectedAccountSwitched: AddressOnNetwork
4648
userActivityEncountered: AddressOnNetwork
@@ -271,13 +273,13 @@ export const setSelectedNetwork = createBackgroundAsyncThunk(
271273
emitter.emit("newSelectedNetwork", network)
272274
// Add any accounts on the currently selected network to the newly
273275
// selected network - if those accounts don't yet exist on it.
274-
Object.keys(account.accountsData.evm[currentlySelectedChainID]).forEach(
275-
(address) => {
276-
if (!account.accountsData.evm[network.chainID]?.[address]) {
277-
dispatch(addAddressNetwork({ address, network }))
278-
}
276+
Object.keys(
277+
account.accountsData.evm[currentlySelectedChainID] ?? []
278+
).forEach((address) => {
279+
if (!account.accountsData.evm[network.chainID]?.[address]) {
280+
dispatch(addAddressNetwork({ address, network }))
279281
}
280-
)
282+
})
281283
dispatch(setNewSelectedAccount({ ...ui.selectedAccount, network }))
282284
}
283285
)
@@ -289,6 +291,14 @@ export const refreshBackgroundPage = createBackgroundAsyncThunk(
289291
}
290292
)
291293

294+
export const derivationPathChange = createBackgroundAsyncThunk(
295+
"ui/derivationPathChange",
296+
async (derivationPath: string, { dispatch }) => {
297+
await emitter.emit("derivationPathChange", derivationPath)
298+
dispatch(setDerivationPath(derivationPath))
299+
}
300+
)
301+
292302
export const selectUI = createSelector(
293303
(state: { ui: UIState }): UIState => state.ui,
294304
(uiState) => uiState

background/services/ledger/index.ts

Lines changed: 31 additions & 8 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,11 @@ 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 {
30+
NETWORK_SUPPORTED_BY_LEDGER,
31+
ROOTSTOCK,
32+
DEFAULT_DERIVATION_PATH as idDerivationPath,
33+
} from "../../constants"
2934
import { normalizeEVMAddress } from "../../lib/utils"
3035
import { AddressOnNetwork } from "../../accounts"
3136

@@ -111,17 +116,25 @@ type Events = ServiceLifecycleEvents & {
111116
usbDeviceCount: number
112117
}
113118

114-
export const idDerivationPath = "44'/60'/0'/0/0"
115-
116119
async function deriveAddressOnLedger(path: string, eth: Eth) {
117120
const derivedIdentifiers = await eth.getAddress(path)
121+
122+
if (
123+
ROOTSTOCK.derivationPath &&
124+
path.includes(ROOTSTOCK.derivationPath.slice(0, 8))
125+
) {
126+
// ethersGetAddress rejects Rootstock addresses so using toChecksumAddress
127+
return toChecksumAddress(derivedIdentifiers.address, +ROOTSTOCK.chainID)
128+
}
129+
118130
const address = ethersGetAddress(derivedIdentifiers.address)
119131
return address
120132
}
121133

122134
async function generateLedgerId(
123135
transport: Transport,
124-
eth: Eth
136+
eth: Eth,
137+
derivationPath: string
125138
): Promise<[string | undefined, LedgerType]> {
126139
let extensionDeviceType = LedgerType.UNKNOWN
127140

@@ -147,7 +160,7 @@ async function generateLedgerId(
147160
return [undefined, extensionDeviceType]
148161
}
149162

150-
const address = await deriveAddressOnLedger(idDerivationPath, eth)
163+
const address = await deriveAddressOnLedger(derivationPath, eth)
151164

152165
return [address, extensionDeviceType]
153166
}
@@ -172,6 +185,8 @@ async function generateLedgerId(
172185
export default class LedgerService extends BaseService<Events> {
173186
#currentLedgerId: string | null = null
174187

188+
#derivationPath: string = idDerivationPath
189+
175190
transport: Transport | undefined = undefined
176191

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

210225
const eth = new Eth(this.transport)
211226

212-
const [id, type] = await generateLedgerId(this.transport, eth)
227+
const [id, type] = await generateLedgerId(
228+
this.transport,
229+
eth,
230+
this.#derivationPath
231+
)
213232

214233
if (!id) {
215234
throw new Error("Can't derive meaningful identification address!")
@@ -239,7 +258,7 @@ export default class LedgerService extends BaseService<Events> {
239258
this.emitter.emit("ledgerAdded", {
240259
id: this.#currentLedgerId,
241260
type,
242-
accountIDs: [idDerivationPath],
261+
accountIDs: [this.#derivationPath],
243262
metadata: {
244263
ethereumVersion: appData.version,
245264
isArbitraryDataSigningEnabled: appData.arbitraryDataEnabled !== 0,
@@ -250,6 +269,10 @@ export default class LedgerService extends BaseService<Events> {
250269
})
251270
}
252271

272+
setDefaultDerivationPath(path: string): void {
273+
this.#derivationPath = path
274+
}
275+
253276
#handleUSBConnect = async (event: USBConnectionEvent): Promise<void> => {
254277
this.emitter.emit(
255278
"usbDeviceCount",
@@ -540,7 +563,7 @@ export default class LedgerService extends BaseService<Events> {
540563
hexDataToSign: HexString
541564
): Promise<string> {
542565
if (
543-
!NETWORK_FOR_LEDGER_SIGNING.find((supportedNetwork) =>
566+
!NETWORK_SUPPORTED_BY_LEDGER.find((supportedNetwork) =>
544567
sameNetwork(network, supportedNetwork)
545568
)
546569
) {

ui/_locales/en/messages.json

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -104,6 +104,13 @@
104104
"onlyRejectFromLedger": "Tx can only be Rejected from Ledger",
105105
"onboarding": {
106106
"connecting": "Connecting...",
107+
"selectLedgerApp": {
108+
"initialScreenHeader": "Select Ledger Live App",
109+
"ecosystem": "{{network}} ecosystem",
110+
"includes": "Includes",
111+
"subheading": "Select which app you would like to start with",
112+
"continueButton": "Continue"
113+
},
107114
"prepare": {
108115
"continueButton": "Continue",
109116
"tryAgainButton": "Try Again",
@@ -117,8 +124,9 @@
117124
"stepsExplainer": "Please follow the steps below and click on Try Again!",
118125
"step1": "Plug in a single Ledger",
119126
"step2": "Enter pin to unlock",
120-
"step3": "Open Ethereum App",
121-
"tip": "After clicking continue, select device and click connect"
127+
"step3": "Open {{network}} App",
128+
"tip": "After clicking continue, select device and click connect",
129+
"derivationPath": "Select derivation path to connect with Ledger"
122130
},
123131
"selectDevice": "Select the device",
124132
"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: 450px;
3131
margin: 0 auto;
3232
padding: 1rem;
33+
position: relative;
3334
}
3435
3536
.indicator {
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
import React, { ReactElement } from "react"
2+
import { useTranslation } from "react-i18next"
3+
import LedgerContinueButton from "./LedgerContinueButton"
4+
import LedgerPanelContainer from "./LedgerPanelContainer"
5+
import LedgerMenuProtocolList from "../LedgerMenu/LedgerMenuProtocolList"
6+
7+
export default function LedgerSelectNetwork({
8+
onContinue,
9+
}: {
10+
onContinue: () => void
11+
}): ReactElement {
12+
const { t } = useTranslation("translation", {
13+
keyPrefix: "ledger.onboarding.selectLedgerApp",
14+
})
15+
16+
return (
17+
<LedgerPanelContainer
18+
indicatorImageSrc="/images/connect_ledger_indicator_disconnected.svg"
19+
heading={t("initialScreenHeader")}
20+
subHeading={t("subheading")}
21+
>
22+
<div className="box">
23+
<LedgerMenuProtocolList />
24+
</div>
25+
<LedgerContinueButton onClick={onContinue}>
26+
{t("continueButton")}
27+
</LedgerContinueButton>
28+
29+
<style jsx>{`
30+
.box {
31+
margin: 0.5rem 0;
32+
padding: 0.8rem 0.8rem;
33+
border-radius: 4px;
34+
background: var(--hunter-green);
35+
}
36+
`}</style>
37+
</LedgerPanelContainer>
38+
)
39+
}

0 commit comments

Comments
 (0)