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 Mar 20, 2023
1 parent 33ab3cb commit 22f1574
Show file tree
Hide file tree
Showing 20 changed files with 391 additions and 60 deletions.
10 changes: 9 additions & 1 deletion background/constants/networks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,8 @@ export const GOERLI: EVMNetwork = {
coingeckoPlatformID: "ethereum",
}

export const DEFAULT_DERIVATION_PATH = "44'/60'/0'/0/0"

export const DEFAULT_NETWORKS = [
ETHEREUM,
POLYGON,
Expand Down Expand Up @@ -142,7 +144,13 @@ 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,
AVALANCHE,
BINANCE_SMART_CHAIN,
]

// 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 @@ -1087,6 +1087,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
8 changes: 8 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 @@ -95,6 +96,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 +231,7 @@ export const {
addLedgerAccount,
setUsbDeviceCount,
removeDevice,
setDerivationPath,
} = ledgerSlice.actions

export default ledgerSlice.reducer
Expand Down
1 change: 1 addition & 0 deletions background/redux-slices/selectors/index.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
export * from "./activitiesSelectors"
export * from "./accountsSelectors"
export * from "./ledgerSelectors"
export * from "./keyringsSelectors"
export * from "./signingSelectors"
export * from "./dappSelectors"
Expand Down
5 changes: 5 additions & 0 deletions background/redux-slices/selectors/ledgerSelectors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,4 +20,9 @@ export const selectLedgerDeviceByAddresses = createSelector(
}
)

export const selectLedgerDerivationPath = createSelector(
(state: RootState) => state.ledger.derivationPath,
(path) => path
)

export default {}
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"

export const defaultSettings = {
Expand Down Expand Up @@ -41,6 +42,7 @@ export type Events = {
deleteAnalyticsData: never
newDefaultWalletValue: boolean
refreshBackgroundPage: null
derivationPathChange: string
newSelectedAccount: AddressOnNetwork
newSelectedAccountSwitched: AddressOnNetwork
userActivityEncountered: AddressOnNetwork
Expand Down Expand Up @@ -271,13 +273,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 @@ -289,6 +291,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
39 changes: 31 additions & 8 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,11 @@ 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,
DEFAULT_DERIVATION_PATH as idDerivationPath,
} from "../../constants"
import { normalizeEVMAddress } from "../../lib/utils"
import { AddressOnNetwork } from "../../accounts"

Expand Down Expand Up @@ -111,17 +116,25 @@ type Events = ServiceLifecycleEvents & {
usbDeviceCount: number
}

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

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

if (
ROOTSTOCK.derivationPath &&
path.includes(ROOTSTOCK.derivationPath.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 +160,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 +185,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 +224,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 +258,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 +269,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 @@ -540,7 +563,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
12 changes: 10 additions & 2 deletions ui/_locales/en/messages.json
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,13 @@
"onlyRejectFromLedger": "Tx can only be Rejected from Ledger",
"onboarding": {
"connecting": "Connecting...",
"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 @@ -117,8 +124,9 @@
"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",
"tip": "After clicking continue, select device and click connect"
"step3": "Open {{network}} App",
"tip": "After clicking continue, select device and click connect",
"derivationPath": "Select derivation path to connect with Ledger"
},
"selectDevice": "Select the device",
"clickConnect": "Click connect",
Expand Down
1 change: 1 addition & 0 deletions ui/components/Ledger/LedgerPanelContainer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ export default function LedgerPanelContainer({
max-width: 450px;
margin: 0 auto;
padding: 1rem;
position: relative;
}
.indicator {
Expand Down
39 changes: 39 additions & 0 deletions ui/components/Ledger/LedgerSelectNetwork.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import React, { ReactElement } from "react"
import { useTranslation } from "react-i18next"
import LedgerContinueButton from "./LedgerContinueButton"
import LedgerPanelContainer from "./LedgerPanelContainer"
import LedgerMenuProtocolList from "../LedgerMenu/LedgerMenuProtocolList"

export default function LedgerSelectNetwork({
onContinue,
}: {
onContinue: () => void
}): ReactElement {
const { t } = useTranslation("translation", {
keyPrefix: "ledger.onboarding.selectLedgerApp",
})

return (
<LedgerPanelContainer
indicatorImageSrc="/images/connect_ledger_indicator_disconnected.svg"
heading={t("initialScreenHeader")}
subHeading={t("subheading")}
>
<div className="box">
<LedgerMenuProtocolList />
</div>
<LedgerContinueButton onClick={onContinue}>
{t("continueButton")}
</LedgerContinueButton>

<style jsx>{`
.box {
margin: 0.5rem 0;
padding: 0.8rem 0.8rem;
border-radius: 4px;
background: var(--hunter-green);
}
`}</style>
</LedgerPanelContainer>
)
}
52 changes: 52 additions & 0 deletions ui/components/LedgerMenu/LedgerMenuProtocolList.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
import React, { ReactElement } from "react"
import {
ARBITRUM_ONE,
ETHEREUM,
OPTIMISM,
AVALANCHE,
BINANCE_SMART_CHAIN,
POLYGON,
ROOTSTOCK,
} from "@tallyho/tally-background/constants"
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 LEDGER_APPS = [
{
network: ETHEREUM,
ecosystem: [OPTIMISM, ARBITRUM_ONE],
},
{
network: POLYGON,
},
{
network: ROOTSTOCK,
},
{
network: AVALANCHE,
},
{
network: BINANCE_SMART_CHAIN,
},
]

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

return (
<div className="standard_width_padded center_horizontal">
<ul>
{LEDGER_APPS.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 22f1574

Please sign in to comment.