diff --git a/src/components/HardwareDerivationPaths.tsx b/src/components/HardwareDerivationPaths.tsx index 16140994..bae1b712 100644 --- a/src/components/HardwareDerivationPaths.tsx +++ b/src/components/HardwareDerivationPaths.tsx @@ -1,4 +1,6 @@ +import BigNumber from "bignumber.js"; import log from "loglevel"; +import { ImArrowLeft2, ImArrowRight2 } from "solid-icons/im"; import { IoClose } from "solid-icons/io"; import { Accessor, @@ -6,16 +8,20 @@ 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 { HardwareSigner, @@ -23,6 +29,8 @@ import { 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,107 @@ const DerivationPath = (props: { ); }; +const HwAddressSelection = (props: { + setLoading: Setter; + basePath: Accessor; + setBasePath: Setter; + provider: Accessor; +}) => { + const limit = 5; + + const { separator, notify } = useGlobalContext(); + const { providers, connectProvider } = useWeb3Signer(); + + const [offset, setOffset] = createSignal(0); + const isFirstPage = () => offset() === 0; + + // eslint-disable-next-line solid/reactivity + const [addresses] = createResource(offset, async () => { + try { + const prov = providers()[props.provider().rdns] + .provider as unknown as HardwareSigner; + + const addresses = await prov.deriveAddresses( + props.basePath(), + offset(), + limit, + ); + 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} + +
+
+ )} +
+
+
{ + if (isFirstPage()) { + return; + } + + setOffset(offset() - limit); + }}> + +
+
{ + setOffset(offset() + limit); + }}> + +
+
+
+ ); +}; + const CustomPath = (props: { provider: Accessor; setLoading: Setter; @@ -160,6 +258,7 @@ const HardwareDerivationPaths = (props: { const { t } = useGlobalContext(); const [loading, setLoading] = createSignal(false); + const [basePath, setBasePath] = createSignal(); const paths = createMemo(() => { switch (config.network) { @@ -184,36 +283,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/style/web3.scss b/src/style/web3.scss index 1a13090d..7ea3dd28 100644 --- a/src/style/web3.scss +++ b/src/style/web3.scss @@ -37,3 +37,19 @@ .no-browser-wallet { margin: 1rem; } + +.paginator { + display: flex; + justify-content: center; + gap: 1rem; +} + +.paginator > .button { + padding: 1rem 1rem; + cursor: pointer; +} + +.paginator .disabled { + opacity: 0.6; + cursor: not-allowed; +} diff --git a/src/utils/hardware/HadwareSigner.ts b/src/utils/hardware/HadwareSigner.ts index 87c0fcad..d7408f18 100644 --- a/src/utils/hardware/HadwareSigner.ts +++ b/src/utils/hardware/HadwareSigner.ts @@ -1,16 +1,31 @@ +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, + offset: number, + 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..376ca5a7 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,32 @@ class LedgerSigner implements EIP1193Provider, HardwareSigner { this.loader = ledgerLoader; } + public getProvider = (): JsonRpcProvider => this.provider; + + public deriveAddresses = async ( + basePath: string, + offset: number, + limit: number, + ): Promise => { + log.debug( + `Deriving ${limit} Ledger addresses with offset ${offset} 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}/${offset + i}`; + const { address } = await eth.getAddress(path); + addresses.push({ path, address: address.toLowerCase() }); + } + + return addresses; + }; + public getDerivationPath = () => { return this.derivationPath; }; @@ -52,21 +82,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 +171,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..75b4289e 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,39 @@ class TrezorSigner implements EIP1193Provider, HardwareSigner { this.loader = trezorLoader; } + public getProvider = (): JsonRpcProvider => this.provider; + + public deriveAddresses = async ( + basePath: string, + offset: number, + limit: number, + ): Promise => { + log.debug( + `Deriving ${limit} Trezor addresses with offset ${offset} for base path: ${basePath}`, + ); + + const paths: string[] = []; + for (let i = 0; i < limit; i++) { + paths.push(`${basePath}/${offset + 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; +};