Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

RSK ledger integration #2577

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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(
kkosiorowska marked this conversation as resolved.
Show resolved Hide resolved
(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,
},
kkosiorowska marked this conversation as resolved.
Show resolved Hide resolved
]

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