From 721831384343c59fba8ec23666c5e2f79f4b0106 Mon Sep 17 00:00:00 2001 From: michael1011 Date: Sat, 5 Oct 2024 18:24:13 +0200 Subject: [PATCH] feat: hardware custom derivation paths --- src/components/ConnectWallet.tsx | 33 ++-- src/components/HardwareDerivationPaths.tsx | 177 +++++++++++++++++++++ src/consts/Types.ts | 1 + src/context/Global.tsx | 12 ++ src/context/Web3.tsx | 6 +- src/i18n/i18n.ts | 2 + src/utils/hardware/HadwareSigner.ts | 10 ++ src/utils/{ => hardware}/LedgerSigner.ts | 22 ++- src/utils/{ => hardware}/TrezorSigner.ts | 26 +-- 9 files changed, 253 insertions(+), 36 deletions(-) create mode 100644 src/components/HardwareDerivationPaths.tsx create mode 100644 src/utils/hardware/HadwareSigner.ts rename src/utils/{ => hardware}/LedgerSigner.ts (90%) rename src/utils/{ => hardware}/TrezorSigner.ts (86%) diff --git a/src/components/ConnectWallet.tsx b/src/components/ConnectWallet.tsx index 352911e3..df9b5677 100644 --- a/src/components/ConnectWallet.tsx +++ b/src/components/ConnectWallet.tsx @@ -17,21 +17,7 @@ import { useWeb3Signer } from "../context/Web3"; import "../style/web3.scss"; import { formatError } from "../utils/errors"; import { cropString, isMobile } from "../utils/helper"; - -const connect = async ( - notify: (type: string, message: string) => void, - connectProvider: (rdns: string) => Promise, - provider: EIP6963ProviderInfo, -) => { - try { - await connectProvider(provider.rdns); - } catch (e) { - log.error( - `Provider connect to ${provider.rdns} failed: ${formatError(e)}`, - ); - notify("error", `Wallet connection failed: ${formatError(e)}`); - } -}; +import HardwareDerivationPaths, { connect } from "./HardwareDerivationPaths"; const Modal = ({ show, @@ -43,6 +29,11 @@ const Modal = ({ const { t, notify } = useGlobalContext(); const { providers, connectProvider } = useWeb3Signer(); + const [showDerivationPaths, setShowDerivationPaths] = + createSignal(false); + const [hardwareProvider, setHardwareProvider] = + createSignal(undefined); + const Provider = ({ provider }: { provider: EIP6963ProviderInfo }) => { return (

@@ -74,7 +71,6 @@ const Modal = ({ return (
setShow(false)} style={show() ? "display: block;" : "display: none;"}> @@ -93,6 +89,11 @@ const Modal = ({ {(item) => }
+
); }; diff --git a/src/components/HardwareDerivationPaths.tsx b/src/components/HardwareDerivationPaths.tsx new file mode 100644 index 00000000..d64a5b2f --- /dev/null +++ b/src/components/HardwareDerivationPaths.tsx @@ -0,0 +1,177 @@ +import log from "loglevel"; +import { IoClose } from "solid-icons/io"; +import { Accessor, For, Setter, createSignal } from "solid-js"; + +import type { + EIP6963ProviderDetail, + EIP6963ProviderInfo, +} from "../consts/Types"; +import { useGlobalContext } from "../context/Global"; +import { useWeb3Signer } from "../context/Web3"; +import { formatError } from "../utils/errors"; +import { + HardwareSigner, + derivationPaths, +} from "../utils/hardware/HadwareSigner"; + +export const connect = async ( + notify: (type: string, message: string) => void, + connectProvider: (rdns: string) => Promise, + provider: EIP6963ProviderInfo, +) => { + try { + await connectProvider(provider.rdns); + } catch (e) { + log.error( + `Provider connect to ${provider.rdns} failed: ${formatError(e)}`, + ); + notify("error", `Wallet connection failed: ${formatError(e)}`); + } +}; + +const connectHardware = async ( + notify: (type: string, message: string) => void, + connectProvider: (rdns: string) => Promise, + provider: Accessor, + providers: Accessor>, + path: string, +) => { + const hardwareProvider = provider(); + const prov = providers()[hardwareProvider.rdns] + .provider as unknown as HardwareSigner; + prov.setDerivationPath(path); + + await connect(notify, connectProvider, hardwareProvider); +}; + +const DerivationPath = ({ + name, + path, + provider, +}: { + name: string; + path: string; + provider: Accessor; +}) => { + const { notify } = useGlobalContext(); + const { connectProvider, providers } = useWeb3Signer(); + + return ( +
{ + await connectHardware( + notify, + connectProvider, + provider, + providers, + path, + ); + }}> +
+
+

{name}

+ {path} +
+
+ ); +}; + +const CustomPath = ({ + provider, +}: { + provider: Accessor; +}) => { + const { t, notify, hardwareDerivationPath, setHardwareDerivationPath } = + useGlobalContext(); + const { connectProvider, providers } = useWeb3Signer(); + + const [path, setPath] = createSignal(hardwareDerivationPath()); + + const updatePath = (input: HTMLInputElement) => { + setPath(input.value); + }; + + return ( +
+
+

Custom

+ updatePath(e.currentTarget)} + onKeyUp={(e) => updatePath(e.currentTarget)} + onPaste={(e) => updatePath(e.currentTarget)} + /> +
+ +
+ +
+
+ ); +}; + +const HardwareDerivationPaths = ({ + show, + setShow, + provider, +}: { + show: Accessor; + setShow: Setter; + provider: Accessor; +}) => { + const { t } = useGlobalContext(); + + return ( +
setShow(false)} + style={show() ? "display: block;" : "display: none;"}> +
e.stopPropagation()}> +

{t("select_derivation_path")}

+ setShow(false)}> + + +
+ + a.toLowerCase().localeCompare(b.toLowerCase()), + )}> + {([name, path]) => ( + + )} + +
+ +
+
+ ); +}; + +export default HardwareDerivationPaths; diff --git a/src/consts/Types.ts b/src/consts/Types.ts index 5bc8631f..47e979b3 100644 --- a/src/consts/Types.ts +++ b/src/consts/Types.ts @@ -11,6 +11,7 @@ export type EIP6963ProviderInfo = { name: string; icon?: string; disabled?: boolean; + isHardware?: boolean; }; export type EIP1193Provider = { diff --git a/src/context/Global.tsx b/src/context/Global.tsx index 33592289..c5848d90 100644 --- a/src/context/Global.tsx +++ b/src/context/Global.tsx @@ -90,6 +90,9 @@ export type GlobalContextType = { clearSwaps: () => Promise; updateSwapStatus: (id: string, newStatus: string) => Promise; + hardwareDerivationPath: Accessor; + setHardwareDerivationPath: Setter; + setRdns: (address: string, rdns: string) => Promise; getRdnsForAddress: (address: string) => Promise; }; @@ -311,6 +314,13 @@ const GlobalProvider = (props: { children: any }) => { }, ); + const [hardwareDerivationPath, setHardwareDerivationPath] = makePersisted( + createSignal(""), + { + name: "hardwareDerivationPath", + }, + ); + // i18n createMemo(() => setI18n(i18nConfigured())); const dictLocale = createMemo(() => @@ -381,6 +391,8 @@ const GlobalProvider = (props: { children: any }) => { setRdns, getRdnsForAddress, + hardwareDerivationPath, + setHardwareDerivationPath, }}> {props.children} diff --git a/src/context/Web3.tsx b/src/context/Web3.tsx index e5e54560..e9c7dfcf 100644 --- a/src/context/Web3.tsx +++ b/src/context/Web3.tsx @@ -15,9 +15,9 @@ import { import { config } from "../config"; import { RBTC } from "../consts/Assets"; import { EIP1193Provider, EIP6963ProviderDetail } from "../consts/Types"; -import LedgerSigner from "../utils/LedgerSigner"; -import TrezorSigner from "../utils/TrezorSigner"; import { Contracts, getContracts } from "../utils/boltzClient"; +import LedgerSigner from "../utils/hardware/LedgerSigner"; +import TrezorSigner from "../utils/hardware/TrezorSigner"; import { useGlobalContext } from "./Global"; import LedgerIcon from "/ledger.svg"; import TrezorIcon from "/trezor.svg"; @@ -71,6 +71,7 @@ const Web3SignerProvider = (props: { uuid: "ledger", rdns: "ledger", icon: LedgerIcon, + isHardware: true, disabled: navigator.hid === undefined, }, }, @@ -81,6 +82,7 @@ const Web3SignerProvider = (props: { uuid: "trezor", rdns: "trezor", icon: TrezorIcon, + isHardware: true, }, }, }); diff --git a/src/i18n/i18n.ts b/src/i18n/i18n.ts index 593311f0..cebe051e 100644 --- a/src/i18n/i18n.ts +++ b/src/i18n/i18n.ts @@ -207,6 +207,8 @@ const dict = { insufficient_balance_line: "You do not have enough balance in your wallet for this swap.", select_wallet: "Select wallet", + select_derivation_path: "Select derivation path", + submit_derivation_path: "Submit", switch_network: "Switch network", block: "block", logs_scan_progress: "Scan progress {{ value }}%", diff --git a/src/utils/hardware/HadwareSigner.ts b/src/utils/hardware/HadwareSigner.ts new file mode 100644 index 00000000..27d54cb6 --- /dev/null +++ b/src/utils/hardware/HadwareSigner.ts @@ -0,0 +1,10 @@ +const derivationPaths = { + RSK: "44'/137'/0'/0/0", + Ethereum: "44'/60'/0'/0/0", +}; + +interface HardwareSigner { + setDerivationPath(path: string): void; +} + +export { HardwareSigner, derivationPaths }; diff --git a/src/utils/LedgerSigner.ts b/src/utils/hardware/LedgerSigner.ts similarity index 90% rename from src/utils/LedgerSigner.ts rename to src/utils/hardware/LedgerSigner.ts index ca9d6d18..ec0125d3 100644 --- a/src/utils/LedgerSigner.ts +++ b/src/utils/hardware/LedgerSigner.ts @@ -10,14 +10,16 @@ import { } from "ethers"; import log from "loglevel"; -import { config } from "../config"; -import { EIP1193Provider } from "../consts/Types"; +import { config } from "../../config"; +import { EIP1193Provider } from "../../consts/Types"; +import { HardwareSigner, derivationPaths } from "./HadwareSigner"; -class LedgerSigner implements EIP1193Provider { +class LedgerSigner implements EIP1193Provider, HardwareSigner { private static readonly ethereumApp = "Ethereum"; - private static readonly path = "44'/60'/0'/0/0"; private readonly provider: JsonRpcProvider; + private derivationPath = derivationPaths.Ethereum; + private transport?: Transport; constructor() { @@ -26,6 +28,10 @@ class LedgerSigner implements EIP1193Provider { ); } + public setDerivationPath = (path: string) => { + this.derivationPath = path; + }; + public request = async (request: { method: string; params?: Array; @@ -46,7 +52,7 @@ class LedgerSigner implements EIP1193Provider { } const eth = new Eth(this.transport); - const { address } = await eth.getAddress(LedgerSigner.path); + const { address } = await eth.getAddress(this.derivationPath); return [address]; } @@ -74,7 +80,7 @@ class LedgerSigner implements EIP1193Provider { const eth = new Eth(this.transport); const signature = await eth.clearSignTransaction( - LedgerSigner.path, + this.derivationPath, tx.unsignedSerialized.substring(2), {}, ); @@ -95,7 +101,7 @@ class LedgerSigner implements EIP1193Provider { try { const signature = await eth.signEIP712Message( - LedgerSigner.path, + this.derivationPath, message, ); return this.serializeSignature(signature); @@ -107,7 +113,7 @@ class LedgerSigner implements EIP1193Provider { delete types["EIP712Domain"]; const signature = await eth.signEIP712HashedMessage( - LedgerSigner.path, + this.derivationPath, TypedDataEncoder.hashDomain(message.domain), TypedDataEncoder.hashStruct( message.primaryType, diff --git a/src/utils/TrezorSigner.ts b/src/utils/hardware/TrezorSigner.ts similarity index 86% rename from src/utils/TrezorSigner.ts rename to src/utils/hardware/TrezorSigner.ts index ab022b49..259dc5fc 100644 --- a/src/utils/TrezorSigner.ts +++ b/src/utils/hardware/TrezorSigner.ts @@ -12,21 +12,27 @@ import { } from "ethers"; import log from "loglevel"; -import { config } from "../config"; -import { EIP1193Provider } from "../consts/Types"; - -class TrezorSigner implements EIP1193Provider { - private static readonly path = "m/44'/60'/0'/0/0"; +import { config } from "../../config"; +import { EIP1193Provider } from "../../consts/Types"; +import { HardwareSigner, derivationPaths } from "./HadwareSigner"; +class TrezorSigner implements EIP1193Provider, HardwareSigner { private readonly provider: JsonRpcProvider; + private initialized = false; + private derivationPath!: string; constructor() { this.provider = new JsonRpcProvider( config.assets["RBTC"].network.rpcUrls[0], ); + this.setDerivationPath(derivationPaths.Ethereum); } + public setDerivationPath = (path: string) => { + this.derivationPath = `m/${path}`; + }; + public request = async (request: { method: string; params?: Array; @@ -39,12 +45,12 @@ class TrezorSigner implements EIP1193Provider { const addresses = this.handleError
( await TrezorConnect.ethereumGetAddress({ - path: TrezorSigner.path, showOnTrezor: false, + path: this.derivationPath, } as any), ); - return [addresses.payload.address]; + return [addresses.payload.address.toLowerCase()]; } case "eth_sendTransaction": { @@ -71,8 +77,8 @@ class TrezorSigner implements EIP1193Provider { }; const signature = this.handleError( await TrezorConnect.ethereumSignTransaction({ - path: TrezorSigner.path, transaction: trezorTx, + path: this.derivationPath, } as unknown as any), ); @@ -101,8 +107,8 @@ class TrezorSigner implements EIP1193Provider { const signature = this.handleError( await TrezorConnect.ethereumSignTypedData({ data: message, - path: TrezorSigner.path, metamask_v4_compat: true, + path: this.derivationPath, }), ); return signature.payload.signature; @@ -114,7 +120,7 @@ class TrezorSigner implements EIP1193Provider { public on = () => {}; - public removeAllListeners = (event: string) => {}; + public removeAllListeners = () => {}; private initialize = async () => { if (this.initialized) {