From 0d553efd55d3f29f8cf788dcea0de39c36592c2d Mon Sep 17 00:00:00 2001 From: michael1011 Date: Fri, 29 Nov 2024 10:21:56 +0100 Subject: [PATCH] feat: nicer HWW derivation path selection --- src/components/HardwareDerivationPaths.tsx | 150 ++++++++++++++++----- src/utils/hardware/HadwareSigner.ts | 17 ++- src/utils/hardware/LedgerSigner.ts | 66 ++++++--- src/utils/hardware/TrezorSigner.ts | 39 +++++- src/utils/strings.ts | 7 + 5 files changed, 227 insertions(+), 52 deletions(-) create mode 100644 src/utils/strings.ts diff --git a/src/components/HardwareDerivationPaths.tsx b/src/components/HardwareDerivationPaths.tsx index 16140994..8a42baa6 100644 --- a/src/components/HardwareDerivationPaths.tsx +++ b/src/components/HardwareDerivationPaths.tsx @@ -1,3 +1,4 @@ +import BigNumber from "bignumber.js"; import log from "loglevel"; import { IoClose } from "solid-icons/io"; import { @@ -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 ( @@ -68,24 +76,13 @@ const connectHardware = async ( const DerivationPath = (props: { name: string; path: string; - provider: Accessor; - setLoading: Setter; + setBasePath: Setter; }) => { - const { notify } = useGlobalContext(); - const { connectProvider, providers } = useWeb3Signer(); - return (
{ - await connectHardware( - notify, - connectProvider, - props.provider, - providers, - props.path, - props.setLoading, - ); + onClick={() => { + props.setBasePath(props.path); }}>
@@ -96,6 +93,79 @@ const DerivationPath = (props: { ); }; +const HwAddressSelection = (props: { + setLoading: Setter; + basePath: Accessor; + setBasePath: Setter; + provider: Accessor; +}) => { + 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 ( + }> + + {({ address, balance, path }) => ( +
{ + await connectHardware( + notify, + connectProvider, + props.provider, + providers, + path, + props.setLoading, + ); + }}> +
+
+

+ {cropString(address, 15, 10)} +

+ + {formatAmount( + new BigNumber( + weiToSatoshi(balance).toString(), + ), + Denomination.Btc, + separator(), + )}{" "} + {RBTC} + +
+
+ )} +
+
+ ); +}; + const CustomPath = (props: { provider: Accessor; setLoading: Setter; @@ -160,6 +230,7 @@ const HardwareDerivationPaths = (props: { const { t } = useGlobalContext(); const [loading, setLoading] = createSignal(false); + const [basePath, setBasePath] = createSignal(); const paths = createMemo(() => { switch (config.network) { @@ -184,36 +255,51 @@ const HardwareDerivationPaths = (props: { } }); + const close = () => { + props.setShow(false); + setBasePath(undefined); + }; + return (
props.setShow(false)} + onClick={() => close()} style={props.show() ? "display: block;" : "display: none;"}>
e.stopPropagation()}>

{t("select_derivation_path")}

- props.setShow(false)}> + close()}>
}> - - a.toLowerCase().localeCompare(b.toLowerCase()), - )}> - {([name, path]) => ( - - )} - -
- + }> + + a.toLowerCase().localeCompare(b.toLowerCase()), + )}> + {([name, path]) => ( + + )} + +
+ +
diff --git a/src/utils/hardware/HadwareSigner.ts b/src/utils/hardware/HadwareSigner.ts index 87c0fcad..76e03a05 100644 --- a/src/utils/hardware/HadwareSigner.ts +++ b/src/utils/hardware/HadwareSigner.ts @@ -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; + getDerivationPath(): string; setDerivationPath(path: string): void; } diff --git a/src/utils/hardware/LedgerSigner.ts b/src/utils/hardware/LedgerSigner.ts index 107caff5..a1c8af06 100644 --- a/src/utils/hardware/LedgerSigner.ts +++ b/src/utils/hardware/LedgerSigner.ts @@ -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"]; @@ -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 => { + 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; }; @@ -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); @@ -155,6 +170,25 @@ class LedgerSigner implements EIP1193Provider, HardwareSigner { public removeAllListeners = () => {}; + private checkApp = async ( + modules: Awaited>, + ) => { + 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; diff --git a/src/utils/hardware/TrezorSigner.ts b/src/utils/hardware/TrezorSigner.ts index 45fcad78..4ae112d9 100644 --- a/src/utils/hardware/TrezorSigner.ts +++ b/src/utils/hardware/TrezorSigner.ts @@ -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; @@ -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 => { + 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( + 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; }; diff --git a/src/utils/strings.ts b/src/utils/strings.ts new file mode 100644 index 00000000..c9feb0c9 --- /dev/null +++ b/src/utils/strings.ts @@ -0,0 +1,7 @@ +export const trimPrefix = (str: string, prefix: string) => { + if (str.startsWith(prefix)) { + return str.slice(prefix.length); + } + + return str; +};