Skip to content

Commit

Permalink
feat: nicer HWW derivation path selection (#755)
Browse files Browse the repository at this point in the history
  • Loading branch information
michael1011 authored Nov 29, 2024
1 parent a162f67 commit a5f46e3
Show file tree
Hide file tree
Showing 6 changed files with 277 additions and 52 deletions.
178 changes: 146 additions & 32 deletions src/components/HardwareDerivationPaths.tsx
Original file line number Diff line number Diff line change
@@ -1,28 +1,36 @@
import BigNumber from "bignumber.js";
import log from "loglevel";
import { ImArrowLeft2, ImArrowRight2 } from "solid-icons/im";
import { IoClose } from "solid-icons/io";
import {
Accessor,
For,
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,
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,107 @@ const DerivationPath = (props: {
);
};

const HwAddressSelection = (props: {
setLoading: Setter<boolean>;
basePath: Accessor<string>;
setBasePath: Setter<string>;
provider: Accessor<EIP6963ProviderInfo>;
}) => {
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 (
<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>
<div class="paginator">
<div
classList={{ button: true, disabled: isFirstPage() }}
onClick={() => {
if (isFirstPage()) {
return;
}

setOffset(offset() - limit);
}}>
<ImArrowLeft2 />
</div>
<div
class="button"
onClick={() => {
setOffset(offset() + limit);
}}>
<ImArrowRight2 />
</div>
</div>
</Show>
);
};

const CustomPath = (props: {
provider: Accessor<EIP6963ProviderInfo>;
setLoading: Setter<boolean>;
Expand Down Expand Up @@ -160,6 +258,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 +283,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
16 changes: 16 additions & 0 deletions src/style/web3.scss
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
21 changes: 18 additions & 3 deletions src/utils/hardware/HadwareSigner.ts
Original file line number Diff line number Diff line change
@@ -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<DerivedAddress[]>;

getDerivationPath(): string;
setDerivationPath(path: string): void;
}
Loading

0 comments on commit a5f46e3

Please sign in to comment.