From 20d119bef46c510cfd641b3ce650410e44a02413 Mon Sep 17 00:00:00 2001 From: Pablo Castellano Date: Sun, 20 Mar 2022 12:32:32 +0100 Subject: [PATCH] basic ui and deps --- .env.defaults | 3 +- background/features/features.ts | 1 + background/main.ts | 61 ++- background/package.json | 1 + background/services/index.ts | 1 + background/services/trezor/db.ts | 42 ++ background/services/trezor/index.ts | 524 ++++++++++++++++++++ ui/package.json | 1 + ui/pages/Onboarding/OnboardingAddWallet.tsx | 18 + ui/pages/Tab.tsx | 4 + ui/pages/Trezor/Trezor.tsx | 110 ++++ ui/pages/Trezor/TrezorPrepare.tsx | 84 ++++ yarn.lock | 48 ++ 13 files changed, 894 insertions(+), 4 deletions(-) create mode 100644 background/services/trezor/db.ts create mode 100644 background/services/trezor/index.ts create mode 100644 ui/pages/Trezor/Trezor.tsx create mode 100644 ui/pages/Trezor/TrezorPrepare.tsx diff --git a/.env.defaults b/.env.defaults index 461acf3505..0ec798e254 100644 --- a/.env.defaults +++ b/.env.defaults @@ -10,9 +10,10 @@ HIDE_SWAP=false HIDE_IMPORT_DERIVATION_PATH=true HIDE_CREATE_PHRASE=false HIDE_IMPORT_LEDGER=false +HIDE_IMPORT_TREZOR=true GAS_PRICE_POOLING_FREQUENCY=120 ETHEREUM_NETWORK=mainnet PERSIST_UI_LOCATION=false USE_MAINNET_FORK=false MAINNET_FORK_URL="http://127.0.0.1:8545" -MAINNET_FORK_CHAIN_ID=1337 \ No newline at end of file +MAINNET_FORK_CHAIN_ID=1337 diff --git a/background/features/features.ts b/background/features/features.ts index bdc0692fdf..49075981c9 100644 --- a/background/features/features.ts +++ b/background/features/features.ts @@ -6,5 +6,6 @@ export const HIDE_IMPORT_DERIVATION_PATH = process.env.HIDE_IMPORT_DERIVATION_PATH === "true" export const HIDE_CREATE_PHRASE = process.env.HIDE_CREATE_PHRASE === "true" export const HIDE_IMPORT_LEDGER = process.env.HIDE_IMPORT_LEDGER === "true" +export const HIDE_IMPORT_TREZOR = process.env.HIDE_IMPORT_TREZOR === "true" export const PERSIST_UI_LOCATION = process.env.PERSIST_UI_LOCATION === "true" export const USE_MAINNET_FORK = process.env.USE_MAINNET_FORK === "true" diff --git a/background/main.ts b/background/main.ts index f44fcddc0f..2405d3d99a 100644 --- a/background/main.ts +++ b/background/main.ts @@ -19,6 +19,7 @@ import { TelemetryService, ServiceCreatorFunction, LedgerService, + TrezorService, SigningService, } from "./services" @@ -86,7 +87,7 @@ import { setUsbDeviceCount, } from "./redux-slices/ledger" import { ETHEREUM } from "./constants" -import { HIDE_IMPORT_LEDGER } from "./features/features" +import { HIDE_IMPORT_LEDGER, HIDE_IMPORT_TREZOR } from "./features/features" import { clearApprovalInProgress } from "./redux-slices/0x-swap" import { SignatureResponse, TXSignatureResponse } from "./services/signing" @@ -338,6 +339,10 @@ export default class Main extends BaseService { ? (Promise.resolve(null) as unknown as Promise) : SigningService.create(keyringService, ledgerService, chainService) + const trezorService = HIDE_IMPORT_TREZOR + ? (Promise.resolve(null) as unknown as Promise) + : TrezorService.create() + let savedReduxState = {} // Setting READ_REDUX_CACHE to false will start the extension with an empty // initial state, which can be useful for development @@ -375,7 +380,8 @@ export default class Main extends BaseService { await providerBridgeService, await telemetryService, await ledgerService, - await signingService + await signingService, + await trezorService ) } @@ -440,7 +446,14 @@ export default class Main extends BaseService { * A promise to the signing service which will route operations between the UI * and the exact signing services. */ - private signingService: SigningService + private signingService: SigningService, + + /** + * A promise to the Trezor service, handling the communication + * with attached Trezor device + */ + private trezorService: TrezorService + ) { super({ initialLoadWaitExpired: { @@ -502,6 +515,10 @@ export default class Main extends BaseService { servicesToBeStarted.push(this.signingService.startService()) } + if (!HIDE_IMPORT_TREZOR) { + servicesToBeStarted.push(this.trezorService.startService()) + } + await Promise.all(servicesToBeStarted) } @@ -523,6 +540,10 @@ export default class Main extends BaseService { servicesToBeStopped.push(this.signingService.stopService()) } + if (!HIDE_IMPORT_TREZOR) { + servicesToBeStopped.push(this.trezorService.stopService()) + } + await Promise.all(servicesToBeStopped) await super.internalStopService() } @@ -542,6 +563,10 @@ export default class Main extends BaseService { this.connectSigningService() } + if (!HIDE_IMPORT_TREZOR) { + this.connectTrezorService() + } + await this.connectChainService() // FIXME Should no longer be necessary once transaction queueing enters the @@ -624,6 +649,10 @@ export default class Main extends BaseService { return this.ledgerService.refreshConnectedLedger() } + async connectTrezor(): Promise { + return this.trezorService.refreshConnectedTrezor() + } + async getAccountEthBalanceUncached(address: string): Promise { const amountBigNumber = await this.chainService.providers.ethereum.getBalance(address) @@ -882,6 +911,32 @@ export default class Main extends BaseService { }) } + async connectTrezorService(): Promise { + //this.store.dispatch(resetLedgerState()) + + this.trezorService.emitter.on("connected", ({ id, metadata }) => { + console.log("Got Trezor connected event " + id + " | " + metadata) + this.store.dispatch( + setDeviceConnectionStatus({ + deviceID: id, + status: "available", + isBlindSigner: metadata.ethereumBlindSigner, + }) + ) + }) + + this.trezorService.emitter.on("disconnected", ({ id }) => { + console.log("Got Trezor disconnected event " + id) + this.store.dispatch( + setDeviceConnectionStatus({ deviceID: id, status: "disconnected" }) + ) + }) + + this.trezorService.emitter.on("usbDeviceCount", (usbDeviceCount) => { + console.log("Got Trezor usbDeviceCount event " + usbDeviceCount) + }) + } + async connectKeyringService(): Promise { this.keyringService.emitter.on("keyrings", (keyrings) => { this.store.dispatch(updateKeyrings(keyrings)) diff --git a/background/package.json b/background/package.json index 22ac776185..4f3c760585 100644 --- a/background/package.json +++ b/background/package.json @@ -52,6 +52,7 @@ "ethers": "^5.5.1", "node-fetch": "^2.6.1", "siwe": "^1.1.0", + "trezor-connect": "^8.2.7", "util": "^0.12.4", "webextension-polyfill": "^0.8.0" }, diff --git a/background/services/index.ts b/background/services/index.ts index 0fcc5d093b..b2db38aa26 100644 --- a/background/services/index.ts +++ b/background/services/index.ts @@ -15,4 +15,5 @@ export { default as ProviderBridgeService } from "./provider-bridge" export { default as InternalEthereumProviderService } from "./internal-ethereum-provider" export { default as TelemetryService } from "./telemetry" export { default as LedgerService } from "./ledger" +export { default as TrezorService } from "./trezor" export { default as SigningService } from "./signing" diff --git a/background/services/trezor/db.ts b/background/services/trezor/db.ts new file mode 100644 index 0000000000..c8b8035b2c --- /dev/null +++ b/background/services/trezor/db.ts @@ -0,0 +1,42 @@ +import Dexie from "dexie" +import { normalizeEVMAddress } from "../../lib/utils" +import { HexString } from "../../types" + +export interface TrezorAccount { + trezorId: string + address: HexString + path: string +} + +export class TrezorDatabase extends Dexie { + private trezor!: Dexie.Table + + constructor() { + super("tally/trezor") + + this.version(1).stores({ + ledger: "&address,trezorId", + }) + } + + async addAccount(account: TrezorAccount): Promise { + await this.trezor.add(account) + } + + async getAccountByAddress(address: HexString): Promise { + return ( + (await this.trezor + .where("address") + .equals(normalizeEVMAddress(address)) + .first()) ?? null + ) + } + + async getAllAccountsByTrezorId(trezorId: string): Promise { + return this.trezor.where("trezorId").equals(trezorId).toArray() + } +} + +export async function getOrCreateDB(): Promise { + return new TrezorDatabase() +} diff --git a/background/services/trezor/index.ts b/background/services/trezor/index.ts new file mode 100644 index 0000000000..4927e0ca98 --- /dev/null +++ b/background/services/trezor/index.ts @@ -0,0 +1,524 @@ +import TrezorConnect, { DEVICE_EVENT, DEVICE } from "trezor-connect" +import { + serialize, + UnsignedTransaction, + parse as parseRawTransaction, +} from "@ethersproject/transactions" +import { + joinSignature, + _TypedDataEncoder, + getAddress as ethersGetAddress, +} from "ethers/lib/utils" +import { + EIP1559TransactionRequest, + EVMNetwork, + SignedEVMTransaction, +} from "../../networks" +import { EIP712TypedData, HexString } from "../../types" +import BaseService from "../base" +import { ServiceCreatorFunction, ServiceLifecycleEvents } from "../types" +import logger from "../../lib/logger" +import { getOrCreateDB, TrezorAccount, TrezorDatabase } from "./db" +import { ethersTransactionRequestFromEIP1559TransactionRequest } from "../chain/utils" +import { ETH } from "../../constants" +import { normalizeEVMAddress } from "../../lib/utils" +import { HIDE_IMPORT_TREZOR } from "../../features/features" + +enum TrezorType { + UNKNOWN, + TREZOR_ONE, + TREZOR_T, +} + +const TrezorTypeAsString = Object.values(TrezorType) + +export const isTrezorSupported = + !HIDE_IMPORT_TREZOR && typeof navigator.usb === "object" + +type MetaData = { + ethereumVersion: string + ethereumBlindSigner: boolean +} + +export type ConnectedDevice = { + id: string + type: TrezorType + metadata: MetaData +} + +type Events = ServiceLifecycleEvents & { + trezorAdded: { + id: string + type: TrezorType + accountIDs: string[] + metadata: MetaData + } + trezorAccountAdded: { + id: string + trezorID: string + derivationPath: string + addresses: HexString[] + } + connected: ConnectedDevice + disconnected: { id: string; type: TrezorType } + address: { trezorID: string; derivationPath: string; address: HexString } + signedTransaction: SignedEVMTransaction + signedData: string + usbDeviceCount: number +} + +export const idDerivationPath = "m44'/60'/0'/0/0" + +async function deriveAddressOnTrezor(path: string) { + const result = await TrezorConnect.ethereumGetAddress({ + path: idDerivationPath, + }) + + if (!result.success) { + // throw new Error(result.payload.error) + throw new Error("Error deriving on Trezor") + } + + const derivedIdentifiers = result.payload.address + const address = ethersGetAddress(derivedIdentifiers) + return address +} + +/** + * The TrezorService is responsible for maintaining the connection + * with a Trezor device. + */ +export default class TrezorService extends BaseService { + #currentTrezorId: string | null = null + + #lastOperationPromise = Promise.resolve() + + static create: ServiceCreatorFunction = + async () => { + return new this(await getOrCreateDB()) + } + + private constructor(private db: TrezorDatabase) { + super() + // + // QUESTION: Should we run TrezorConnect.manifest() here instead? + // + } + + private runSerialized(operation: () => Promise) { + const oldOperationPromise = this.#lastOperationPromise + const newOperationPromise = oldOperationPromise.then(async () => + operation() + ) + + this.#lastOperationPromise = newOperationPromise.then( + () => {}, + () => {} + ) + + return newOperationPromise + } + + // TODO: Remove unused productId + async onConnection(productId: number): Promise { + return this.runSerialized(async () => { + + const result = await TrezorConnect.getPublicKey({ + path: idDerivationPath, + coin: "eth", + }) + + if (!result.success) { + throw new Error("Error getPublicKey on trezor") + //throw new Error(result.payload.error) + } + const id = result.payload.publicKey + + if (!id) { + throw new Error("Can't derive meaningful identification address!") + } + const type = TrezorType.TREZOR_ONE + + //const appData = await eth.getAppConfiguration() + + // What does appData.version return ??? + // const version = appData.version + const fakeVersion = "1.0" + const blingSignerModel = true + const normalizedID = normalizeEVMAddress(id) + + this.#currentTrezorId = `${TrezorTypeAsString[type]}_${normalizedID}` + + this.emitter.emit("connected", { + id: this.#currentTrezorId, + type, + metadata: { + ethereumVersion: fakeVersion, + ethereumBlindSigner: blingSignerModel, + }, + }) + + const knownAddresses = await this.db.getAllAccountsByTrezorId( + this.#currentTrezorId + ) + + if (!knownAddresses.length) { + this.emitter.emit("trezorAdded", { + id: this.#currentTrezorId, + type, + accountIDs: [idDerivationPath], + metadata: { + ethereumVersion: fakeVersion, + ethereumBlindSigner: blingSignerModel, + }, + }) + } + }) + } + + #handleUSBConnect = async (event: USBConnectionEvent): Promise => { + this.emitter.emit( + "usbDeviceCount", + (await navigator.usb.getDevices()).length + ) + console.log("Emitted usbDeviceCount " + event.device.productId) + this.onConnection(event.device.productId) + } + + #handleUSBDisconnect = async (event: USBConnectionEvent): Promise => { + this.emitter.emit( + "usbDeviceCount", + (await navigator.usb.getDevices()).length + ) + if (!this.#currentTrezorId) { + return + } + + this.emitter.emit("disconnected", { + id: this.#currentTrezorId, + type: TrezorType.TREZOR_ONE, + }) + + this.#currentTrezorId = null + } + + protected async internalStartService(): Promise { + await super.internalStartService() // Not needed, but better to stick to the patterns + + this.refreshConnectedTrezor() + + TrezorConnect.on(DEVICE_EVENT, (event) => { + if (event.type === DEVICE.CONNECT) { + navigator.usb.addEventListener("connect", this.#handleUSBConnect) + } else if (event.type === DEVICE.DISCONNECT) { + navigator.usb.addEventListener("disconnect", this.#handleUSBDisconnect) + } + }) + + // TODO: Review and remove if unneeded + navigator.usb.addEventListener("connect", this.#handleUSBConnect) + navigator.usb.addEventListener("disconnect", this.#handleUSBDisconnect) + } + + protected async internalStopService(): Promise { + await super.internalStartService() // Not needed, but better to stick to the patterns + + navigator.usb.removeEventListener("disconnect", this.#handleUSBDisconnect) + navigator.usb.removeEventListener("connect", this.#handleUSBConnect) + } + + async refreshConnectedTrezor(): Promise { + const usbDeviceArray = await navigator.usb.getDevices() + + this.emitter.emit("usbDeviceCount", usbDeviceArray.length) + + if (usbDeviceArray.length === 0 || usbDeviceArray.length > 1) { + return null // Nasty things may happen when we've got zero or multiple choices + } + + if (usbDeviceArray.length === 1) { + await this.onConnection(usbDeviceArray[0].productId) + } + + return this.#currentTrezorId + } + + async deriveAddress(accountID: string): Promise { + return this.runSerialized(async () => { + try { + if (!this.#currentTrezorId) { + throw new Error("Uninitialized Trezor ID!") + } + + const accountAddress = normalizeEVMAddress( + await deriveAddressOnTrezor(accountID) + ) + + this.emitter.emit("address", { + trezorID: this.#currentTrezorId, + derivationPath: accountID, + address: accountAddress, + }) + + return accountAddress + } catch (err) { + logger.error( + `Error encountered! trezorID: ${ + this.#currentTrezorId + } accountID: ${accountID} error: ${err}` + ) + throw err + } + }) + } + + async saveAddress(path: HexString, address: string): Promise { + if (!this.#currentTrezorId) { + throw new Error("No Trezor id is set!") + } + + await this.db.addAccount({ trezorId: this.#currentTrezorId, path, address }) + } + + async signTransaction( + network: EVMNetwork, + transactionRequest: EIP1559TransactionRequest & { nonce: number }, + deviceID: string, + path: string + ): Promise { + return this.runSerialized(async () => { + try { + if (!this.#currentTrezorId) { + throw new Error("Uninitialized Trezor ID!") + } + + const ethersTx = + ethersTransactionRequestFromEIP1559TransactionRequest( + transactionRequest + ) + + const serializedTx = serialize( + ethersTx as UnsignedTransaction + ).substring(2) // serialize adds 0x prefix which kills Eth::signTransaction + + const accountData = await this.db.getAccountByAddress( + transactionRequest.from + ) + + this.checkCanSign(accountData, path, deviceID) + + // TODO: change + const result = await TrezorConnect.ethereumSignTransaction({ + path: idDerivationPath, + transaction: { + to: transactionRequest.to!, + value: "0xf4240", + data: "0x01", + chainId: 1, + nonce: "0x0", + gasLimit: "0x5208", + gasPrice: "0xbebc200", + }, + }) + + /* + const result = await TrezorConnect.ethereumSignTransaction({ + //path: idDerivationPath, + //path: "m44'/60'/0'/0/0", + path: "m/44'/60'/0'", + transaction: { + //from: transactionRequest.from, + to: transactionRequest.to, + // value: transactionRequest.value, + value: "0xf4240", + // data: transactionRequest + //chainId: transactionRequest.chainID, + chainId: 1, + nonce: transactionRequest.nonce, + gasLimit: transactionRequest.gasLimit, + gasPrice: "0xbebc200" + } + }); + */ + + + if (!result.success) { + // throw new Error(result.payload.error) + throw new Error("Error TrezorConnect.ethereumSignTransaction") + } + + const signature = result.payload + + const signedTransaction = serialize(ethersTx as UnsignedTransaction, { + r: `0x${signature.r}`, + s: `0x${signature.s}`, + v: parseInt(signature.v, 16), + }) + const tx = parseRawTransaction(signedTransaction) + + if ( + !tx.hash || + !tx.from || + !tx.r || + !tx.s || + typeof tx.v === "undefined" + ) { + throw new Error("Transaction doesn't appear to have been signed.") + } + + if ( + typeof tx.maxPriorityFeePerGas === "undefined" || + typeof tx.maxFeePerGas === "undefined" || + tx.type !== 2 + ) { + throw new Error("Can only sign EIP-1559 conforming transactions") + } + + const signedTx: SignedEVMTransaction = { + hash: tx.hash, + from: tx.from, + to: tx.to, + nonce: tx.nonce, + input: tx.data, + value: tx.value.toBigInt(), + type: tx.type, + gasPrice: null, + maxFeePerGas: tx.maxFeePerGas.toBigInt(), + maxPriorityFeePerGas: tx.maxPriorityFeePerGas.toBigInt(), + gasLimit: tx.gasLimit.toBigInt(), + r: tx.r, + s: tx.s, + v: tx.v, + + blockHash: null, + blockHeight: null, + asset: ETH, + network, + } + + return signedTx + } catch (err) { + logger.error( + `Error encountered! ledgerID: ${ + this.#currentTrezorId + } transactionRequest: ${transactionRequest} error: ${err}` + ) + + throw err + } + }) + } + + async signTypedData( + typedData: EIP712TypedData, + account: HexString, + deviceID: string, + path: string + ): Promise { + return this.runSerialized(async () => { + if (!this.#currentTrezorId) { + throw new Error("Uninitialized Trezor ID!") + } + + const { EIP712Domain, ...typesForSigning } = typedData.types + const hashedDomain = _TypedDataEncoder.hashDomain(typedData.domain) + const hashedMessage = _TypedDataEncoder + .from(typesForSigning) + .hash(typedData.message) + + const accountData = await this.db.getAccountByAddress(account) + + this.checkCanSign(accountData, path, deviceID) + + // Example from https://github.com/trezor/connect/blob/develop/docs/methods/ethereumSignTypedData.md + const eip712Data = { + types: { + EIP712Domain: [ + { + name: 'name', + type: 'string', + }, + ], + Message: [ + { + name: "Best Wallet", + type: "string" + } + ] + }, + primaryType: "Message", + domain: { + name: 'example.trezor.io', + }, + message: { + "Best Wallet": "Trezor Model T", + }, + }; + + // This functionality is separate from trezor-connect, as it requires @metamask/eth-sig-utils, + // which is a large JavaScript dependency + //const transformTypedDataPlugin = require("trezor-connect/lib/plugins/ethereum/typedData.js"); + //const {domain_separator_hash, message_hash} = transformTypedDataPlugin(eip712Data, true); + + /* + const result = await TrezorConnect.ethereumSignTypedData({ + path: idDerivationPath, + data: eip712Data, + metamask_v4_compat: true, + // These are optional, but required for Trezor Model 1 compatibility + domain_separator_hash, + message_hash, + }) + + if (!result.success) { + //throw new Error(result.payload.error) + throw new Error("Error on TrezorConnect.ethereumSignTypedData") + } + + const { signature } = result.payload + */ + + const signature = "TODO" + this.emitter.emit("signedData", signature) + return signature + }) + } + + private checkCanSign( + accountData: TrezorAccount | null, + path: string, + deviceID: string + ) { + if ( + !accountData || + path !== accountData.path || + deviceID !== accountData.trezorId + ) { + throw new Error("Signing method mismatch!") + } + + if (deviceID !== this.#currentTrezorId) { + throw new Error("Cannot sign on wrong device attached!") + } + } + + async signMessage(address: string, message: string): Promise { + if (!this.#currentTrezorId) { + throw new Error("Uninitialized Trezor ID!") + } + + const result = await TrezorConnect.ethereumSignMessage({ + path: idDerivationPath, + message, + }) + + if (!result.success) { + //throw new Error(result.payload.error) + throw new Error("Error on ethereumSignMessage (trezor)") + } + + const { signature } = result.payload + this.emitter.emit("signedData", signature) + return signature + } +} diff --git a/ui/package.json b/ui/package.json index 376ecb6902..44591e554d 100644 --- a/ui/package.json +++ b/ui/package.json @@ -45,6 +45,7 @@ "react-router-dom": "^5.3.0", "react-transition-group": "^4.4.2", "redux": "^4.1.0", + "trezor-connect": "^8.2.7", "webextension-polyfill": "^0.8.0" }, "devDependencies": { diff --git a/ui/pages/Onboarding/OnboardingAddWallet.tsx b/ui/pages/Onboarding/OnboardingAddWallet.tsx index 43a34b9c1a..668276e3db 100644 --- a/ui/pages/Onboarding/OnboardingAddWallet.tsx +++ b/ui/pages/Onboarding/OnboardingAddWallet.tsx @@ -2,6 +2,7 @@ import React, { ReactElement } from "react" import { HIDE_ADD_SEED, HIDE_CREATE_PHRASE, + HIDE_IMPORT_TREZOR, } from "@tallyho/tally-background/features/features" import { isLedgerSupported } from "@tallyho/tally-background/services/ledger" import SharedBackButton from "../../components/Shared/SharedBackButton" @@ -60,6 +61,23 @@ export default function OnboardingStartTheHunt(): ReactElement { )} + {HIDE_IMPORT_TREZOR ? ( + <> + ) : ( +
  • +
    + { + window.open("/tab.html#/trezor", "_blank")?.focus() + window.close() + }} + > + Connect to a Trezor + +
  • + )} {HIDE_CREATE_PHRASE ? ( <> ) : ( diff --git a/ui/pages/Tab.tsx b/ui/pages/Tab.tsx index 0c6f2ef397..3ebf539a90 100644 --- a/ui/pages/Tab.tsx +++ b/ui/pages/Tab.tsx @@ -3,6 +3,7 @@ import { Provider } from "react-redux" import { HashRouter, Route, Switch } from "react-router-dom" import { Store } from "webext-redux" import Ledger from "./Ledger/Ledger" +import Trezor from "./Trezor/Trezor" import TabNotFound from "./TabNotFound" /** @@ -18,6 +19,9 @@ export default function Tab({ store }: { store: Store }): ReactElement { + + + diff --git a/ui/pages/Trezor/Trezor.tsx b/ui/pages/Trezor/Trezor.tsx new file mode 100644 index 0000000000..56538fea51 --- /dev/null +++ b/ui/pages/Trezor/Trezor.tsx @@ -0,0 +1,110 @@ +import TrezorConnect from "trezor-connect" +import React, { ReactElement, useState } from "react" +import LedgerPanelContainer from "../../components/Ledger/LedgerPanelContainer" +import BrowserTabContainer from "../../components/BrowserTab/BrowserTabContainer" +import { useBackgroundDispatch, useBackgroundSelector } from "../../hooks" +import LedgerConnectPopup from "../Ledger/LedgerConnectPopup" +import LedgerImportDone from "../Ledger/LedgerImportDone" +import LedgerImportAccounts from "../Ledger/LedgerImportAccounts" +import TrezorPrepare from "./TrezorPrepare" + +export default function Trezor(): ReactElement { + const [phase, setPhase] = useState< + "0-prepare" | "1-request" | "2-connect" | "3-done" + >("0-prepare") + + const idDerivationPath = "m/44'/60'/0'/0/0" + const [connecting, setConnecting] = useState(false) + const device = true + const dispatch = useBackgroundDispatch() + const connectionError = phase === "2-connect" && !device && !connecting + return ( + + {(phase === "0-prepare" || connectionError) && ( + { + setPhase("1-request") + + // I'm doing something wrong here. Calling TrezorConnect.init() and then TrezorConnect.manifest() + // does not seem to work: "no manifest specified" + // Calling TrezorConnect.manifest() only is fine, but the popup is closed :-/ + /* + console.log("trezor: init"); + TrezorConnect.init({ + //connectSrc: "https://localhost:8088/", + lazyLoad: false, // this param will prevent iframe injection until TrezorConnect.method will be called + manifest: { + email: "pablo@anche.no", + appUrl: "https://tally.cash", + }, + popup: false, + }) + */ + + console.log("trezor: manifest"); + TrezorConnect.manifest({ + email: "pablo@anche.no", + appUrl: "https://tally.cash", + }) + + // This opens https://connect.trezor.io/8/popup.html + // however the popup is closed after some seconds without leaving the possibility to enter your pin. + // + // The javascript console logs the following lines: + // + // 127.0.0.1:21325/acquire/1/null:1 "Failed to load resource: the server responded with a status of 400 (Bad Request)" + // 127.0.0.1:21325/release/4:1 "Failed to load resource: the server responded with a status of 400 (Bad Request)" + // 127.0.0.1:21325/listen:1 "Failed to load resource: net::ERR_CONNECTION_REFUSED" + // + // So it might be related to some problem with the trezor bridge? + const result2 = await TrezorConnect.ethereumGetAddress({ + path: idDerivationPath + }) + + console.log(result2) + if (!result2.success) { + throw new Error(result2.payload.error) + } + + + /* + setPhase("2-connect") + setConnecting(true) + try { + await dispatch(connectTrezor()) + } finally { + setConnecting(false) + } + */ + }} + /> + + )} + {phase === "1-request" && } + {phase === "2-connect" && !device && connecting && ( + + )} + {phase === "2-connect" && device && ( + console.log(phase) + /* + { + setPhase("3-done") + }} + /> + */ + )} + {phase === "3-done" && ( + { + window.close() + }} + /> + )} + + ) +} diff --git a/ui/pages/Trezor/TrezorPrepare.tsx b/ui/pages/Trezor/TrezorPrepare.tsx new file mode 100644 index 0000000000..e7789866c3 --- /dev/null +++ b/ui/pages/Trezor/TrezorPrepare.tsx @@ -0,0 +1,84 @@ +import React, { ReactElement } from "react" +import LedgerContinueButton from "../../components/Ledger/LedgerContinueButton" +import LedgerPanelContainer from "../../components/Ledger/LedgerPanelContainer" + +export default function TrezorPrepare({ + onContinue, +}: { + onContinue: () => void +}): ReactElement { + return ( + +
      +
    1. Plug in Trezor
    2. +
    + + Continue + +
    + ) +} diff --git a/yarn.lock b/yarn.lock index 28ef2f4d6c..37eec0022f 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1055,6 +1055,13 @@ dependencies: regenerator-runtime "^0.13.4" +"@babel/runtime@^7.15.4": + version "7.17.8" + resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.17.8.tgz#3e56e4aff81befa55ac3ac6a0967349fd1c5bca2" + integrity sha512-dQpEpK0O9o6lj6oPu0gRDbbnk+4LeHlNcBpspf6Olzt3GIX4P1lWF1gS+pHLDFlaJvbR6q7jCfQ08zA4QJBnmA== + dependencies: + regenerator-runtime "^0.13.4" + "@babel/runtime@^7.16.3": version "7.17.2" resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.17.2.tgz#66f68591605e59da47523c631416b18508779941" @@ -4505,6 +4512,13 @@ create-require@^1.1.0: resolved "https://registry.yarnpkg.com/create-require/-/create-require-1.1.1.tgz#c1d7e8f1e5f6cfc9ff65f9cd352d37348756c333" integrity sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ== +cross-fetch@^3.1.5: + version "3.1.5" + resolved "https://registry.yarnpkg.com/cross-fetch/-/cross-fetch-3.1.5.tgz#e1389f44d9e7ba767907f7af8454787952ab534f" + integrity sha512-lvb1SBsI0Z7GDwmuid+mU3kWVBwTVUbe7S0H52yaaAdQOXq2YktTCZdlAcNKFzE6QtRz0snpw9bNiPeOIkkQvw== + dependencies: + node-fetch "2.6.7" + cross-spawn@^6.0.5: version "6.0.5" resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-6.0.5.tgz#4a5ec7c64dfae22c3a14124dbacdee846d80cbc4" @@ -8512,6 +8526,13 @@ node-addon-api@^2.0.0: resolved "https://registry.yarnpkg.com/node-addon-api/-/node-addon-api-2.0.2.tgz#432cfa82962ce494b132e9d72a15b29f71ff5d32" integrity sha512-Ntyt4AIXyaLIuMHF6IOoTakB3K+RWxwtsHNRxllEoA6vPwP9o4866g6YWDLUdnucilZhmkxiHwHr11gAENw+QA== +node-fetch@2.6.7: + version "2.6.7" + resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.6.7.tgz#24de9fba827e3b4ae44dc8b20256a379160052ad" + integrity sha512-ZjMPFEfVx5j+y2yF35Kzx5sF7kDzxuDj6ziH4FFbOp87zKDZNx8yExJIb05OGF4Nlt9IHFIMBkRl41VdvcNdbQ== + dependencies: + whatwg-url "^5.0.0" + node-fetch@^2.6.1: version "2.6.1" resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.6.1.tgz#045bd323631f76ed2e2b55573394416b639a0052" @@ -11182,11 +11203,25 @@ tr46@^2.1.0: dependencies: punycode "^2.1.1" +tr46@~0.0.3: + version "0.0.3" + resolved "https://registry.yarnpkg.com/tr46/-/tr46-0.0.3.tgz#8184fd347dac9cdc185992f3a6622e14b9d9ab6a" + integrity sha1-gYT9NH2snNwYWZLzpmIuFLnZq2o= + treeverse@^1.0.4: version "1.0.4" resolved "https://registry.yarnpkg.com/treeverse/-/treeverse-1.0.4.tgz#a6b0ebf98a1bca6846ddc7ecbc900df08cb9cd5f" integrity sha512-whw60l7r+8ZU8Tu/Uc2yxtc4ZTZbR/PF3u1IPNKGQ6p8EICLb3Z2lAgoqw9bqYd8IkgnsaOcLzYHFckjqNsf0g== +trezor-connect@^8.2.7: + version "8.2.7" + resolved "https://registry.yarnpkg.com/trezor-connect/-/trezor-connect-8.2.7.tgz#4bdd4d5e2560f7bd0847b9858cdc944de21150b6" + integrity sha512-SoRDqZoTLb7W0nk7B9OimRoUCGRUc6htEJrHcB0nbC1Fs6Uw5lxCGn/agYCbqgX3oiWs2MIu0UMt+1Ky634Enw== + dependencies: + "@babel/runtime" "^7.15.4" + cross-fetch "^3.1.5" + events "^3.3.0" + ts-invariant@^0.4.0: version "0.4.4" resolved "https://registry.yarnpkg.com/ts-invariant/-/ts-invariant-0.4.4.tgz#97a523518688f93aafad01b0e80eb803eb2abd86" @@ -11586,6 +11621,11 @@ webextension-polyfill@^0.8.0: resolved "https://registry.yarnpkg.com/webextension-polyfill/-/webextension-polyfill-0.8.0.tgz#f80e9f4b7f81820c420abd6ffbebfa838c60e041" integrity sha512-a19+DzlT6Kp9/UI+mF9XQopeZ+n2ussjhxHJ4/pmIGge9ijCDz7Gn93mNnjpZAk95T4Tae8iHZ6sSf869txqiQ== +webidl-conversions@^3.0.0: + version "3.0.1" + resolved "https://registry.yarnpkg.com/webidl-conversions/-/webidl-conversions-3.0.1.tgz#24534275e2a7bc6be7bc86611cc16ae0a5654871" + integrity sha1-JFNCdeKnvGvnvIZhHMFq4KVlSHE= + webidl-conversions@^5.0.0: version "5.0.0" resolved "https://registry.yarnpkg.com/webidl-conversions/-/webidl-conversions-5.0.0.tgz#ae59c8a00b121543a2acc65c0434f57b0fc11aff" @@ -11725,6 +11765,14 @@ whatwg-mimetype@^2.3.0: resolved "https://registry.yarnpkg.com/whatwg-mimetype/-/whatwg-mimetype-2.3.0.tgz#3d4b1e0312d2079879f826aff18dbeeca5960fbf" integrity sha512-M4yMwr6mAnQz76TbJm914+gPpB/nCwvZbJU28cUD6dR004SAxDLOOSUaB1JDRqLtaOV/vi0IC5lEAGFgrjGv/g== +whatwg-url@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/whatwg-url/-/whatwg-url-5.0.0.tgz#966454e8765462e37644d3626f6742ce8b70965d" + integrity sha1-lmRU6HZUYuN2RNNib2dCzotwll0= + dependencies: + tr46 "~0.0.3" + webidl-conversions "^3.0.0" + whatwg-url@^8.0.0, whatwg-url@^8.5.0: version "8.7.0" resolved "https://registry.yarnpkg.com/whatwg-url/-/whatwg-url-8.7.0.tgz#656a78e510ff8f3937bc0bcbe9f5c0ac35941b77"