Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

wip(feat): add trezor support #7

Open
wants to merge 7 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -39,4 +39,7 @@ next-env.d.ts

xcode

/dist
/dist

# ignore jetbrains IDEs files
.idea
1 change: 1 addition & 0 deletions .nvmrc
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
20
1,188 changes: 1,038 additions & 150 deletions package-lock.json

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,7 @@
"@paulmillr/qr": "^0.1.1",
"@scure/bip32": "^1.3.2",
"@scure/bip39": "^1.2.1",
"@trezor/connect-web": "^9.1.5",
"ethers": "^6.8.1",
"idna-uts46-hx": "^5.1.2",
"next": "14.0.1",
Expand Down
45 changes: 45 additions & 0 deletions src/libs/trezor/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import TrezorConnect, { Address, Success } from "@trezor/connect-web";
import { TrezorWallet } from "@/libs/trezor/types";

export class Trezor {
private static instance: Trezor | null = null

private constructor() {
// Private constructor to prevent instantiation
}

public static init(): Trezor {
if (!Trezor.instance) {
Trezor.instance = new Trezor()

TrezorConnect.init({
lazyLoad: true, // this param will prevent iframe injection until TrezorConnect.method will be called
manifest: {
email: 'developer@xyz.com',
appUrl: 'http://your.application.com',
},
})
}

return Trezor.instance;
}

public async getEthereumAddressesToIndex(endIndex: number): Promise<TrezorWallet[]> {
const result = await TrezorConnect.ethereumGetAddress({
bundle: Array.from(Array(endIndex).keys()).map(index => ({
path: `m/44'/60'/${index}'/0/0`,
showOnTrezor: false,
})),
});

if (result.success) {
const { payload } = result as Success<Address[]>;
return payload.map(({ address }, index) => ({
address,
path: `m/44'/60'/${index}'/0/0`,
}));
}

throw new Error(result.payload.error);
}
}
4 changes: 4 additions & 0 deletions src/libs/trezor/types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
export interface TrezorWallet {
path: string;
address: string;
}
7 changes: 7 additions & 0 deletions src/libs/ui/logo/ledger.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
export default function LedgerLogo({ className }: { className?: string }) {
return (
<svg className={className} width="383" height="128" viewBox="0 0 383 128" fill="none" xmlns="http://www.w3.org/2000/svg">
<path className="fill-black dark:fill-white" d="M327.262 119.94V127.998H382.57V91.6548H374.511V119.94H327.262ZM327.262 0V8.05844H374.511V36.3452H382.57V0H327.262ZM298.74 62.3411V43.6158H311.382C317.546 43.6158 319.758 45.6696 319.758 51.2803V54.5982C319.758 60.3657 317.624 62.3411 311.382 62.3411H298.74ZM318.808 65.6589C324.575 64.1578 328.604 58.7842 328.604 52.3856C328.604 48.3564 327.025 44.7211 324.023 41.7972C320.23 38.1619 315.172 36.3452 308.615 36.3452H290.838V91.6529H298.74V69.6097H310.592C316.675 69.6097 319.125 72.1378 319.125 78.4599V91.6548H327.184V79.7239C327.184 71.0325 325.13 67.7147 318.808 66.7662V65.6589ZM252.282 67.4756H276.618V60.207H252.282V43.6139H278.988V36.3452H244.222V91.6529H280.173V84.3842H252.282V67.4756ZM225.812 70.3995V74.1916C225.812 82.1717 222.888 84.78 215.541 84.78H213.803C206.454 84.78 202.899 82.4088 202.899 71.4264V56.5717C202.899 45.5109 206.613 43.2181 213.96 43.2181H215.539C222.73 43.2181 225.021 45.9048 225.099 53.3322H233.791C233.001 42.4283 225.732 35.5555 214.828 35.5555C209.535 35.5555 205.11 37.2153 201.792 40.3745C196.814 45.0367 194.049 52.9383 194.049 63.9991C194.049 74.6659 196.42 82.5675 201.318 87.4649C204.636 90.7044 209.219 92.4426 213.723 92.4426C218.463 92.4426 222.81 90.5456 225.021 86.438H226.126V91.6529H233.395V63.1309H211.983V70.3995H225.812ZM156.126 43.6139H164.739C172.878 43.6139 177.303 45.6677 177.303 56.7304V71.2677C177.303 82.3285 172.878 84.3842 164.739 84.3842H156.126V43.6139ZM165.449 91.6548C180.541 91.6548 186.149 80.1982 186.149 64.001C186.149 47.5666 180.145 36.3471 165.29 36.3471H148.223V91.6548H165.449ZM110.063 67.4756H134.399V60.207H110.063V43.6139H136.768V36.3452H102.002V91.6529H137.954V84.3842H110.063V67.4756ZM63.4464 36.3452H55.3879V91.6529H91.7332V84.3842H63.4464V36.3452ZM0 91.6548V128H55.3076V119.94H8.05844V91.6548H0ZM0 0V36.3452H8.05844V8.05844H55.3076V0H0Z" />
</svg>
)
}
7 changes: 7 additions & 0 deletions src/libs/ui/logo/trezor.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
export default function TrezorLogo({ className }: { className?: string }) {
return (
<svg className={`fill-black dark:fill-white ${className}`} viewBox="0 0 256 182" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlnsXlink="http://www.w3.org/1999/xlink">
<path d="M0,133.784717 L0,144.723089 L13.0419064,144.723089 L13.0419064,180.693508 L25.2423994,180.693508 L25.2423994,144.723089 L38.0739523,144.723089 L38.0739523,133.784717 L0,133.784717 Z M81.8274445,149.350863 C81.8274445,140.095317 75.3064913,133.784717 65.6302383,133.784717 L43.5431389,133.784717 L43.5431389,180.693508 L55.7436319,180.693508 L55.7436319,164.917009 L60.371405,164.917009 L68.9958915,180.693508 L83.0895644,180.693508 L72.7822514,163.234182 C77.4100246,161.551355 81.8274445,157.133936 81.8274445,149.350863 L81.8274445,149.350863 Z M55.7436319,144.723089 L64.3681183,144.723089 C67.5234181,144.723089 69.6269515,146.61627 69.6269515,149.561216 C69.6269515,152.295809 67.5234181,154.399342 64.3681183,154.399342 L55.7436319,154.399342 L55.7436319,144.723089 Z M86.665571,180.693508 L120.74281,180.693508 L120.74281,169.965489 L98.866064,169.965489 L98.866064,162.392769 L120.322104,162.392769 L120.322104,151.454396 L98.866064,151.454396 L98.866064,144.723089 L120.74281,144.723089 L120.74281,133.784717 L86.665571,133.784717 L86.665571,180.693508 Z M161.972062,143.250617 L161.972062,133.784717 L126.001643,133.784717 L126.001643,144.723089 L145.564502,144.723089 L126.001643,171.437962 L126.001643,180.693508 L161.972062,180.693508 L161.972062,169.755136 L142.409202,169.755136 L161.972062,143.250617 Z M187.635168,133.153657 C173.541495,133.153657 163.234182,143.46097 163.234182,157.344289 C163.234182,171.437962 173.541495,181.534922 187.635168,181.534922 C201.939195,181.534922 212.246507,171.227608 212.246507,157.344289 C212.246507,143.250617 201.939195,133.153657 187.635168,133.153657 Z M187.635168,170.386195 C180.483155,170.386195 175.645029,164.917009 175.645029,157.133936 C175.645029,149.350863 180.483155,143.881676 187.635168,143.881676 C194.787182,143.881676 199.835661,149.350863 199.835661,157.133936 C199.835661,165.127362 194.787182,170.386195 187.635168,170.386195 Z M245.692687,163.444536 C250.110107,161.761709 254.737879,157.344289 254.737879,149.561216 C254.737879,140.30567 248.216926,133.99507 238.540673,133.99507 L216.453574,133.99507 L216.453574,180.903861 L228.654067,180.903861 L228.654067,165.127362 L233.281841,165.127362 L241.906326,180.903861 L256,180.903861 L245.692687,163.444536 Z M228.443714,144.723089 L237.0682,144.723089 C240.2235,144.723089 242.327034,146.61627 242.327034,149.561216 C242.327034,152.295809 240.2235,154.399342 237.0682,154.399342 L228.443714,154.399342 L228.443714,144.723089 Z M147.878389,29.8701725 L147.878389,22.507806 C147.878389,10.0969597 137.360723,0 124.318817,0 C111.276911,0 100.759244,10.0969597 100.759244,22.507806 L100.759244,29.8701725 L91.0829909,29.8701725 L91.0829909,81.6170911 L124.52917,97.1832374 L157.975349,81.6170911 L157.975349,29.8701725 L147.878389,29.8701725 Z M112.749383,22.507806 C112.749383,16.8282662 118.008217,11.9901396 124.318817,11.9901396 C130.629417,11.9901396 135.888249,16.6179129 135.888249,22.507806 L135.888249,29.8701725 L112.749383,29.8701725 L112.749383,22.507806 Z M144.302383,72.9926048 L124.318817,82.2481511 L104.335251,72.9926048 L104.335251,42.0706655 L144.302383,42.0706655 L144.302383,72.9926048 Z"></path>
</svg>
)
}
16 changes: 16 additions & 0 deletions src/mods/background/service_worker/entities/wallets/data.ts
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@ export type EthereumWalletData =
export type EthereumSignableWalletData =
| EthereumPrivateKeyWalletData
| EthereumSeededWalletData
| EthereumTrezorWalletData

export type EthereumPrivateKeyWalletData =
| EthereumUnauthPrivateKeyWalletData
Expand Down Expand Up @@ -125,6 +126,21 @@ export interface EthereumSeededWalletData {
readonly path: string
}

export interface EthereumTrezorWalletData {
readonly coin: "ethereum"
readonly type: "trezor"

readonly uuid: string
readonly name: string,

readonly color: number,
readonly emoji: string

readonly address: ZeroHexString

readonly path: string
}

export namespace Wallet {

export type Key = ReturnType<typeof key>
Expand Down
153 changes: 153 additions & 0 deletions src/mods/foreground/entities/wallets/all/create/hardware.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,153 @@
import { Button } from "@/libs/ui/button";
import { Outline } from "@/libs/icons/icons";
import { Dialog, useDialogContext } from "@/libs/ui/dialog/dialog";
import TrezorLogo from "@/libs/ui/logo/trezor";
import LedgerLogo from "@/libs/ui/logo/ledger";
import { useCallback, useState } from "react";
import { Trezor } from "@/libs/trezor";
import { TrezorWallet } from "@/libs/trezor/types";
import { Wallet, WalletData } from "@/mods/background/service_worker/entities/wallets/data";
import { useBackgroundContext } from "@/mods/foreground/background/context";
import { Colors } from "@/libs/colors/colors";
import { Emojis } from "@/libs/emojis/emojis";
import { Modhash } from "@/libs/modhash/modhash";
import { useWallets } from "@/mods/foreground/entities/wallets/all/data";

interface SelectableTrezorWallet extends TrezorWallet {
selected: boolean;
}

export function HardwareWalletCreatorDialog({}) {
const { close } = useDialogContext().unwrap()
const walletsQuery = useWallets()
const maybeWallets = walletsQuery.data?.inner
const background = useBackgroundContext().unwrap()
const [error, setError] = useState<string | undefined>()
const [trezorWallets, setTrezorWallets] = useState<SelectableTrezorWallet[]>([
// { path: "m/44'/60'/0'/0/0", address: "0x6f2a88EeA710c58bbB8F2681Bf8732Bfb4350062", selected: false },
// { path: "m/44'/60'/1'/0/0", address: "0xc9babCee61024AA21152858f5e592C0091949f49", selected: false },
// { path: "m/44'/60'/2'/0/0", address: "0xaA96a50A2f67111262Fe24576bd85Bb56eC65016", selected: false },
// { path: "m/44'/60'/3'/0/0", address: "0x22a71133E0a9514145B5eA4Ce0B874A9aFD596FB", selected: false },
// { path: "m/44'/60'/4'/0/0", address: "0xD2AC479d7F5F792f40129B47441Ab56e47de86cF", selected: false },
// { path: "m/44'/60'/5'/0/0", address: "0xD2AC479d7F5F792f40129B47441Ab56e47de86cF", selected: false },
// { path: "m/44'/60'/6'/0/0", address: "0xD2AC479d7F5F792f40129B47441Ab56e47de86cF", selected: false },
// { path: "m/44'/60'/7'/0/0", address: "0xD2AC479d7F5F792f40129B47441Ab56e47de86cF", selected: false },
// { path: "m/44'/60'/8'/0/0", address: "0xD2AC479d7F5F792f40129B47441Ab56e47de86cF", selected: false },
// { path: "m/44'/60'/9'/0/0", address: "0xD2AC479d7F5F792f40129B47441Ab56e47de86cF", selected: false },
])

const isTrezorWalletSelected = useCallback(() =>
trezorWallets.some(wallet => wallet.selected),
[trezorWallets]
)

const connectTrezor = async () => {
const trezor = Trezor.init()

trezor.getEthereumAddressesToIndex(10)
.then(wallets => {
setError(undefined)

const importableWallets = wallets.map(wallet => {
// TODO: check if wallet is already imported and if so, mark it as selected
// const isAlreadyImported = maybeWallets?.some(w => w.uuid === wallet.address)

return { ...wallet, selected: false };
})

setTrezorWallets(importableWallets as SelectableTrezorWallet[])
})
.catch((err: Error) => setError(err.message))
}

const updateSelectedWallet = (wallet: SelectableTrezorWallet) => {
setTrezorWallets(trezorWallets.map(w => w.path === wallet.path ? { ...w, selected: !w.selected } : w))
}

const importWallets = async () => {
const selectedWallets = trezorWallets.filter(wallet => wallet.selected)

const walletsImport = selectedWallets.map(async ({ path, address }) => {
const uuid = crypto.randomUUID()
const modhash = Modhash.from(uuid)
const color = Colors.mod(modhash)
const emoji = Emojis.get(modhash)

const wallet: WalletData = { coin: "ethereum", type: "trezor", uuid, name: address, path, address: address as `0x${string}`, color, emoji }

await background.tryRequest<Wallet[]>({
method: "brume_createWallet",
params: [wallet]
})
})

Promise.all(walletsImport)
.then(() => close())
}

return (
<>
<Dialog.Title close={close}>
Connect a hardware wallet
</Dialog.Title>
<div className="h-2" />
{trezorWallets.length > 0
? (
<div className="flex flex-col justify-between">
<div className="h-64 overflow-scroll">
{trezorWallets.map(wallet => {
return (
<div
key={wallet.path}
onClick={() => updateSelectedWallet(wallet)}
className="w-full flex items-center justify-between cursor-pointer px-2 py-2"
>
<div className="w-full flex items-center justify-start gap-2">
<input
type="checkbox"
checked={wallet.selected}
onChange={() => updateSelectedWallet(wallet)}
className="w-4 h-4 text-blue-600 bg-gray-100 border-gray-300 rounded focus:ring-blue-500 dark:focus:ring-blue-600 dark:ring-offset-gray-800 focus:ring-2 dark:bg-gray-700 dark:border-gray-600"
/>
<span>{wallet.address}</span>
</div>
<span className="whitespace-nowrap">0 ETH</span>
</div>
)
})}
</div>
<div className="mt-auto flex items-center flex-wrap-reverse gap-2 pt-4">
<Button.Gradient className="grow po-md"
colorIndex={0}
disabled={!isTrezorWalletSelected()}
onClick={importWallets}>
<div className={`${Button.Shrinker.className}`}>
<Outline.PlusIcon className="s-sm" />
{isTrezorWalletSelected()
? "Import selected wallets"
: "Select wallets to import"
}
</div>
</Button.Gradient>
</div>
</div>
)
: (
<div className="w-full flex items-center gap-2">
<Button.Contrast className="flex-1 whitespace-nowrap p-4 rounded-xl" onClick={connectTrezor}>
<div className={`${Button.Shrinker.className} flex-col`}>
<TrezorLogo className="w-24 h-24" />
</div>
</Button.Contrast>
<Button.Contrast className="flex-1 whitespace-nowrap p-4 rounded-xl" disabled>
<div className={`${Button.Shrinker.className} flex-col`}>
<LedgerLogo className="w-24 h-24" />
</div>
</Button.Contrast>
</div>
)
}
{error && <div className="text-red-500 text-sm text-center mt-4">{error}</div>}
</>
)
}
18 changes: 17 additions & 1 deletion src/mods/foreground/entities/wallets/all/create/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,12 @@ import { Dialog, useDialogContext } from "@/libs/ui/dialog/dialog";
import { useCallback, useState } from "react";
import { ReadonlyWalletCreatorDialog } from "./readonly";
import { StandaloneWalletCreatorDialog } from "./standalone";
import { HardwareWalletCreatorDialog } from "./hardware";

export function WalletCreatorDialog(props: {}) {
const { opened, close } = useDialogContext().unwrap()

const [type, setType] = useState<"readonly" | "privateKey">()
const [type, setType] = useState<"readonly" | "privateKey" | "hardwareWallet">()

const onWatchonlyClick = useCallback(() => {
setType("readonly")
Expand All @@ -18,6 +19,10 @@ export function WalletCreatorDialog(props: {}) {
setType("privateKey")
}, [])

const onHardwareWalletClick = useCallback(() => {
setType("hardwareWallet")
}, [])

return <>
<Dialog
opened={opened && type === "readonly"}
Expand All @@ -29,6 +34,11 @@ export function WalletCreatorDialog(props: {}) {
close={close}>
<StandaloneWalletCreatorDialog />
</Dialog>
<Dialog
opened={opened && type === "hardwareWallet"}
close={close}>
<HardwareWalletCreatorDialog />
</Dialog>
<Dialog.Title close={close}>
New wallet
</Dialog.Title>
Expand All @@ -48,6 +58,12 @@ export function WalletCreatorDialog(props: {}) {
<span>Private key</span>
</div>
</Button.Contrast>
<Button.Contrast className="flex-1 whitespace-nowrap p-4 rounded-xl" onClick={onHardwareWalletClick}>
<div className={`${Button.Shrinker.className} flex-col`}>
<Outline.CpuChipIcon className="s-md" />
<span>Hardware wallet</span>
</div>
</Button.Contrast>
</div>
</>
}
1 change: 1 addition & 0 deletions src/mods/foreground/entities/wallets/all/data.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { Wallet } from "@/mods/background/service_worker/entities/wallets/data";
import { useSubscribe } from "@/mods/foreground/storage/storage";
import { UserStorage, useUserStorageContext } from "@/mods/foreground/storage/user";
import { createQuery, useQuery } from "@hazae41/glacier";
import {useWallet} from "@/mods/foreground/entities/wallets/data";

export function getWallets(storage: UserStorage) {
return createQuery<string, Wallet[], never>({ key: `wallets`, storage })
Expand Down