Skip to content

Commit

Permalink
feat: nicer HWW derivation path selection
Browse files Browse the repository at this point in the history
  • Loading branch information
michael1011 committed Nov 29, 2024
1 parent a162f67 commit 0d553ef
Show file tree
Hide file tree
Showing 5 changed files with 227 additions and 52 deletions.
150 changes: 118 additions & 32 deletions src/components/HardwareDerivationPaths.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import BigNumber from "bignumber.js";
import log from "loglevel";
import { IoClose } from "solid-icons/io";
import {
Expand All @@ -6,23 +7,30 @@ import {
Setter,
Show,
createMemo,
createResource,
createSignal,
} from "solid-js";

import { config } from "../config";
import { RBTC } from "../consts/Assets";
import { Denomination } from "../consts/Enums";
import type {
EIP6963ProviderDetail,
EIP6963ProviderInfo,
} from "../consts/Types";
import { useGlobalContext } from "../context/Global";
import { useWeb3Signer } from "../context/Web3";
import { formatAmount } from "../utils/denomination";
import { formatError } from "../utils/errors";
import {
DerivedAddress,
HardwareSigner,
derivationPaths,
derivationPathsMainnet,
derivationPathsTestnet,
} from "../utils/hardware/HadwareSigner";
import { cropString } from "../utils/helper";
import { weiToSatoshi } from "../utils/rootstock";
import LoadingSpinner from "./LoadingSpinner";

export const connect = async (
Expand Down Expand Up @@ -68,24 +76,13 @@ const connectHardware = async (
const DerivationPath = (props: {
name: string;
path: string;
provider: Accessor<EIP6963ProviderInfo>;
setLoading: Setter<boolean>;
setBasePath: Setter<string>;
}) => {
const { notify } = useGlobalContext();
const { connectProvider, providers } = useWeb3Signer();

return (
<div
class="provider-modal-entry-wrapper"
onClick={async () => {
await connectHardware(
notify,
connectProvider,
props.provider,
providers,
props.path,
props.setLoading,
);
onClick={() => {
props.setBasePath(props.path);
}}>
<hr />
<div class="provider-modal-entry">
Expand All @@ -96,6 +93,79 @@ const DerivationPath = (props: {
);
};

const HwAddressSelection = (props: {
setLoading: Setter<boolean>;
basePath: Accessor<string>;
setBasePath: Setter<string>;
provider: Accessor<EIP6963ProviderInfo>;
}) => {
const { separator, notify } = useGlobalContext();
const { providers, connectProvider } = useWeb3Signer();

const [addresses] = createResource<
(DerivedAddress & { balance: bigint })[]
>(async () => {
try {
const prov = providers()[props.provider().rdns]
.provider as unknown as HardwareSigner;

const addresses = await prov.deriveAddresses(props.basePath(), 5);
return await Promise.all(
addresses.map(async ({ address, path }) => ({
path,
address,
balance: await prov.getProvider().getBalance(address),
})),
);
} catch (e) {
props.setBasePath(undefined);
log.error(`Deriving addresses failed: ${formatError(e)}`);
notify("error", `Deriving addresses failed: ${formatError(e)}`);
throw e;
}
});

return (
<Show when={!addresses.loading} fallback={<LoadingSpinner />}>
<For each={addresses()}>
{({ address, balance, path }) => (
<div
class="provider-modal-entry-wrapper"
onClick={async () => {
await connectHardware(
notify,
connectProvider,
props.provider,
providers,
path,
props.setLoading,
);
}}>
<hr />
<div
class="provider-modal-entry"
style={{ padding: "8px 10%" }}>
<h4 class="no-grow">
{cropString(address, 15, 10)}
</h4>
<span>
{formatAmount(
new BigNumber(
weiToSatoshi(balance).toString(),
),
Denomination.Btc,
separator(),
)}{" "}
{RBTC}
</span>
</div>
</div>
)}
</For>
</Show>
);
};

const CustomPath = (props: {
provider: Accessor<EIP6963ProviderInfo>;
setLoading: Setter<boolean>;
Expand Down Expand Up @@ -160,6 +230,7 @@ const HardwareDerivationPaths = (props: {
const { t } = useGlobalContext();

const [loading, setLoading] = createSignal<boolean>(false);
const [basePath, setBasePath] = createSignal<string | undefined>();

const paths = createMemo(() => {
switch (config.network) {
Expand All @@ -184,36 +255,51 @@ const HardwareDerivationPaths = (props: {
}
});

const close = () => {
props.setShow(false);
setBasePath(undefined);
};

return (
<div
class="frame assets-select"
onClick={() => props.setShow(false)}
onClick={() => close()}
style={props.show() ? "display: block;" : "display: none;"}>
<div onClick={(e) => e.stopPropagation()}>
<h2>{t("select_derivation_path")}</h2>
<span class="close" onClick={() => props.setShow(false)}>
<span class="close" onClick={() => close()}>
<IoClose />
</span>
<hr class="spacer" />
<Show when={!loading()} fallback={<LoadingSpinner />}>
<For
each={Object.entries(paths()).sort(([a], [b]) =>
a.toLowerCase().localeCompare(b.toLowerCase()),
)}>
{([name, path]) => (
<DerivationPath
name={name}
path={path}
provider={props.provider}
<Show
when={basePath() === undefined}
fallback={
<HwAddressSelection
basePath={basePath}
setLoading={setLoading}
setBasePath={setBasePath}
provider={props.provider}
/>
)}
</For>
<hr style={{ "margin-top": "0" }} />
<CustomPath
provider={props.provider}
setLoading={setLoading}
/>
}>
<For
each={Object.entries(paths()).sort(([a], [b]) =>
a.toLowerCase().localeCompare(b.toLowerCase()),
)}>
{([name, path]) => (
<DerivationPath
name={name}
path={path}
setBasePath={setBasePath}
/>
)}
</For>
<hr style={{ "margin-top": "0" }} />
<CustomPath
provider={props.provider}
setLoading={setLoading}
/>
</Show>
</Show>
</div>
</div>
Expand Down
17 changes: 14 additions & 3 deletions src/utils/hardware/HadwareSigner.ts
Original file line number Diff line number Diff line change
@@ -1,16 +1,27 @@
import type { JsonRpcProvider } from "ethers";

export const derivationPaths = {
Ethereum: "44'/60'/0'/0/0",
Ethereum: "44'/60'/0'/0",
};

export const derivationPathsMainnet = {
RSK: "44'/137'/0'/0/0",
RSK: "44'/137'/0'",
};

export const derivationPathsTestnet = {
["RSK Testnet"]: "44'/37310'/0'/0/0",
["RSK Testnet"]: "44'/37310'/0'",
};

export type DerivedAddress = {
path: string;
address: string;
};

export interface HardwareSigner {
getProvider(): JsonRpcProvider;

deriveAddresses(basePath: string, limit: number): Promise<DerivedAddress[]>;

getDerivationPath(): string;
setDerivationPath(path: string): void;
}
66 changes: 50 additions & 16 deletions src/utils/hardware/LedgerSigner.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,11 @@ import { config } from "../../config";
import { EIP1193Provider } from "../../consts/Types";
import type { DictKey } from "../../i18n/i18n";
import ledgerLoader, { Transport } from "../../lazy/ledger";
import { HardwareSigner, derivationPaths } from "./HadwareSigner";
import {
DerivedAddress,
HardwareSigner,
derivationPaths,
} from "./HadwareSigner";

class LedgerSigner implements EIP1193Provider, HardwareSigner {
private static readonly supportedApps = ["Ethereum", "RSK", "RSK Test"];
Expand All @@ -35,6 +39,31 @@ class LedgerSigner implements EIP1193Provider, HardwareSigner {
this.loader = ledgerLoader;
}

public getProvider = (): JsonRpcProvider => this.provider;

public deriveAddresses = async (
basePath: string,
limit: number,
): Promise<DerivedAddress[]> => {
log.debug(
`Deriving ${limit} Ledger addresses for base path: ${basePath}`,
);

const modules = await this.loader.get();
await this.checkApp(modules);

const eth = new modules.eth(this.transport);

const addresses: DerivedAddress[] = [];
for (let i = 0; i < limit; i++) {
const path = `${basePath}/${i}`;
const { address } = await eth.getAddress(path);
addresses.push({ path, address: address.toLowerCase() });
}

return addresses;
};

public getDerivationPath = () => {
return this.derivationPath;
};
Expand All @@ -52,21 +81,7 @@ class LedgerSigner implements EIP1193Provider, HardwareSigner {
log.debug("Getting Ledger accounts");

const modules = await this.loader.get();

if (this.transport === undefined) {
this.transport = await modules.webhid.create();
}

const openApp = (await this.getApp()).name;
log.debug(`Ledger has app open: ${openApp}`);
if (!LedgerSigner.supportedApps.includes(openApp)) {
log.warn(
`Open Ledger app ${openApp} not in supported: ${LedgerSigner.supportedApps.join(", ")}`,
);
await this.transport.close();
this.transport = undefined;
throw this.t("ledger_open_app_prompt");
}
await this.checkApp(modules);

const eth = new modules.eth(this.transport);
const { address } = await eth.getAddress(this.derivationPath);
Expand Down Expand Up @@ -155,6 +170,25 @@ class LedgerSigner implements EIP1193Provider, HardwareSigner {

public removeAllListeners = () => {};

private checkApp = async (
modules: Awaited<ReturnType<typeof ledgerLoader.get>>,
) => {
if (this.transport === undefined) {
this.transport = await modules.webhid.create();
}

const openApp = (await this.getApp()).name;
log.debug(`Ledger has app open: ${openApp}`);
if (!LedgerSigner.supportedApps.includes(openApp)) {
log.warn(
`Open Ledger app ${openApp} not in supported: ${LedgerSigner.supportedApps.join(", ")}`,
);
await this.transport.close();
this.transport = undefined;
throw this.t("ledger_open_app_prompt");
}
};

private getApp = async (): Promise<{
name: string;
version: string;
Expand Down
39 changes: 38 additions & 1 deletion src/utils/hardware/TrezorSigner.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,12 @@ import trezorLoader, {
SuccessWithDevice,
Unsuccessful,
} from "../../lazy/trezor";
import { HardwareSigner, derivationPaths } from "./HadwareSigner";
import { trimPrefix } from "../strings";
import {
DerivedAddress,
HardwareSigner,
derivationPaths,
} from "./HadwareSigner";

class TrezorSigner implements EIP1193Provider, HardwareSigner {
private readonly provider: JsonRpcProvider;
Expand All @@ -32,6 +37,38 @@ class TrezorSigner implements EIP1193Provider, HardwareSigner {
this.loader = trezorLoader;
}

public getProvider = (): JsonRpcProvider => this.provider;

public deriveAddresses = async (
basePath: string,
limit: number,
): Promise<DerivedAddress[]> => {
log.debug(
`Deriving ${limit} Trezor addresses for base path: ${basePath}`,
);

const paths: string[] = [];
for (let i = 0; i < limit; i++) {
paths.push(`${basePath}/${i}`);
}

await this.initialize();
const connect = await this.loader.get();
const addresses = this.handleError<Address[]>(
await connect.ethereumGetAddress({
bundle: paths.map((path) => ({
path: `m/${path}`,
showOnTrezor: false,
})),
}),
);

return addresses.payload.map((res) => ({
address: res.address,
path: trimPrefix(res.serializedPath, "m/"),
}));
};

public getDerivationPath = () => {
return this.derivationPath;
};
Expand Down
Loading

0 comments on commit 0d553ef

Please sign in to comment.