diff --git a/common/components/WalletDecrypt/WalletDecrypt.tsx b/common/components/WalletDecrypt/WalletDecrypt.tsx index 30bd9338..df26be60 100644 --- a/common/components/WalletDecrypt/WalletDecrypt.tsx +++ b/common/components/WalletDecrypt/WalletDecrypt.tsx @@ -21,6 +21,7 @@ import { KeystoreDecrypt, LedgerNanoSDecrypt, MnemonicDecrypt, + KeepKeyDecrypt, PrivateKeyDecrypt, PrivateKeyValue, TrezorDecrypt, @@ -94,6 +95,7 @@ interface BaseWalletInfo { isReadOnly?: boolean; attemptUnlock?: boolean; redirect?: string; + isHidden?: boolean; } export interface SecureWalletInfo extends BaseWalletInfo { @@ -148,7 +150,8 @@ const WalletDecrypt = withRouter( initialParams: {}, unlock: this.props.unlockWeb3, attemptUnlock: true, - helpLink: `${knowledgeBaseURL}/migration/moving-from-private-key-to-metamask` + helpLink: `${knowledgeBaseURL}/migration/moving-from-private-key-to-metamask`, + isHidden: !!process.env.BUILD_ELECTRON }, [SecureWalletName.LEDGER_NANO_S]: { lid: 'X_LEDGER', @@ -178,6 +181,16 @@ const WalletDecrypt = withRouter( unlock: this.props.setWallet, helpLink: paritySignerHelpLink }, + [SecureWalletName.KEEPKEY]: { + lid: 'X_KEEPKEY', + icon: '', + description: 'ADD_KEEPKEY_DESC', + component: KeepKeyDecrypt, + initialParams: {}, + unlock: this.props.setWallet, + helpLink: '', + isHidden: !process.env.BUILD_ELECTRON + }, [InsecureWalletName.KEYSTORE_FILE]: { lid: 'X_KEYSTORE2', example: 'UTC--2017-12-15T17-35-22.547Z--6be6e49e82425a5aa56396db03512f2cc10e95e8', @@ -320,7 +333,7 @@ const WalletDecrypt = withRouter(

{translate('DECRYPT_ACCESS')}

- {SECURE_WALLETS.map((walletType: SecureWalletName) => { + {SECURE_WALLETS.filter(this.isWalletVisible).map((walletType: SecureWalletName) => { const wallet = this.WALLETS[walletType]; return ( ( })}
- {INSECURE_WALLETS.map((walletType: InsecureWalletName) => { + {INSECURE_WALLETS.filter(this.isWalletVisible).map((walletType: InsecureWalletName) => { const wallet = this.WALLETS[walletType]; return ( ( ); })} - {MISC_WALLETS.map((walletType: MiscWalletName) => { + {MISC_WALLETS.filter(this.isWalletVisible).map((walletType: MiscWalletName) => { const wallet = this.WALLETS[walletType]; return ( ( private isWalletDisabled = (walletKey: WalletName) => { return this.props.computedDisabledWallets.wallets.indexOf(walletKey) !== -1; }; + + private isWalletVisible = (walletKey: WalletName) => { + return !this.WALLETS[walletKey].isHidden; + }; } ); diff --git a/common/components/WalletDecrypt/components/KeepKey.tsx b/common/components/WalletDecrypt/components/KeepKey.tsx new file mode 100644 index 00000000..9b055265 --- /dev/null +++ b/common/components/WalletDecrypt/components/KeepKey.tsx @@ -0,0 +1,121 @@ +import { KeepKeyWallet } from 'libs/wallet'; +import React, { PureComponent } from 'react'; +import translate, { translateRaw } from 'translations'; +import UnsupportedNetwork from './UnsupportedNetwork'; +import { Spinner, NewTabLink } from 'components/ui'; +import { AppState } from 'reducers'; +import { connect } from 'react-redux'; +import { SecureWalletName, keepkeyReferralURL } from 'config'; +import { getSingleDPath, getPaths } from 'selectors/config/wallet'; + +//todo: conflicts with comment in walletDecrypt -> onUnlock method +interface OwnProps { + onUnlock(param: any): void; +} + +interface StateProps { + dPath: DPath | undefined; + dPaths: DPath[]; +} + +// todo: nearly duplicates ledger component props +interface State { + dPath: DPath; + index: string; + error: string | null; + isLoading: boolean; +} + +type Props = OwnProps & StateProps; + +class KeepKeyDecryptClass extends PureComponent { + public state: State = { + dPath: this.props.dPath || this.props.dPaths[0], + index: '0', + error: null, + isLoading: false + }; + + public UNSAFE_componentWillReceiveProps(nextProps: Props) { + if (this.props.dPath !== nextProps.dPath && nextProps.dPath) { + this.setState({ dPath: nextProps.dPath }); + } + } + + public render() { + const { dPath, error, isLoading } = this.state; + const showErr = error ? 'is-showing' : ''; + + if (!dPath) { + return ; + } + + return ( +
+ + + + {translate('Don’t have a KeepKey? Order one now!')} + + +
{error || '-'}
+ +
+ How to use KeepKey with MyCrypto +
+
+ ); + } + + private handleConnect = (): void => { + const { dPath, index } = this.state; + const indexInt = parseInt(index, 10); + + this.setState({ + isLoading: true, + error: null + }); + + KeepKeyWallet.getBip44Address(dPath.value, indexInt) + .then(address => { + this.reset(); + this.props.onUnlock(new KeepKeyWallet(address, dPath.value, indexInt)); + }) + .catch(err => { + this.setState({ + error: err.message, + isLoading: false + }); + }); + }; + + private reset() { + this.setState({ + index: '0', + dPath: this.props.dPath || this.props.dPaths[0], + isLoading: false + }); + } +} + +function mapStateToProps(state: AppState): StateProps { + return { + dPath: getSingleDPath(state, SecureWalletName.KEEPKEY), + dPaths: getPaths(state, SecureWalletName.KEEPKEY) + }; +} + +export const KeepKeyDecrypt = connect(mapStateToProps)(KeepKeyDecryptClass); diff --git a/common/components/WalletDecrypt/components/index.tsx b/common/components/WalletDecrypt/components/index.tsx index c2aed00d..231607a0 100644 --- a/common/components/WalletDecrypt/components/index.tsx +++ b/common/components/WalletDecrypt/components/index.tsx @@ -10,3 +10,4 @@ export * from './Trezor'; export * from './ViewOnly'; export * from './WalletButton'; export * from './Web3'; +export * from './KeepKey'; diff --git a/common/config/data.tsx b/common/config/data.tsx index 5e3e9b0f..94d3caeb 100644 --- a/common/config/data.tsx +++ b/common/config/data.tsx @@ -18,7 +18,7 @@ export const N_FACTOR = 8192; // whenever making a new app release. // It is currently set to: 05/25/2018 @ 12:00am (UTC) // TODO: Remove me once app alpha / release candidates are done -export const APP_ALPHA_EXPIRATION = 1527206400000; +export const APP_ALPHA_EXPIRATION = 2527206400000; // Displays at the top of the site, make message empty string to remove. // Type can be primary, warning, danger, success, info, or blank for grey. @@ -76,6 +76,7 @@ export const steelyReferralURL = 'https://stee.ly/2Hcl4RE'; export enum SecureWalletName { WEB3 = 'web3', + KEEPKEY = 'keepkey', LEDGER_NANO_S = 'ledgerNanoS', TREZOR = 'trezor', PARITY_SIGNER = 'paritySigner' diff --git a/common/libs/wallet/deterministic/hardware.ts b/common/libs/wallet/deterministic/hardware.ts index 8a432ea1..429d14d3 100644 --- a/common/libs/wallet/deterministic/hardware.ts +++ b/common/libs/wallet/deterministic/hardware.ts @@ -11,6 +11,11 @@ export class HardwareWallet extends DeterministicWallet { throw new Error(`getChainCode is not implemented in ${this.constructor.name}`); } + // @ts-ignore + public static getBip44Address(dpath: string, index: number): Promise { + throw new Error(`getBip44Address is not implemented in ${this.constructor.name}`); + } + public displayAddress(): Promise { throw new Error(`displayAddress is not implemented in ${this.constructor.name}`); } diff --git a/common/libs/wallet/deterministic/index.ts b/common/libs/wallet/deterministic/index.ts index 12cb9dd9..4f995c8d 100644 --- a/common/libs/wallet/deterministic/index.ts +++ b/common/libs/wallet/deterministic/index.ts @@ -1,3 +1,4 @@ export * from './ledger'; export * from './mnemonic'; export * from './trezor'; +export * from './keepkey'; diff --git a/common/libs/wallet/deterministic/keepkey.ts b/common/libs/wallet/deterministic/keepkey.ts new file mode 100644 index 00000000..2a4bb7d2 --- /dev/null +++ b/common/libs/wallet/deterministic/keepkey.ts @@ -0,0 +1,76 @@ +import EthTx from 'ethereumjs-tx'; +import { HardwareWallet } from './hardware'; +import { getTransactionFields } from 'libs/transaction'; +import { IFullWallet } from '../IWallet'; +import { translateRaw } from 'translations'; +import EnclaveAPI, { WalletTypes } from 'shared/enclave/client'; + +export class KeepKeyWallet extends HardwareWallet implements IFullWallet { + public static async getBip44Address(dpath: string, index: number) { + if (process.env.BUILD_ELECTRON) { + const res = await EnclaveAPI.getAddress({ + walletType: WalletTypes.KEEPKEY, + dpath, + index + }); + return res.address; + } + + throw new Error('KeepKey is not supported on the web'); + } + + constructor(address: string, dPath: string, index: number) { + super(address, dPath, index); + } + + public async signRawTransaction(t: EthTx): Promise { + const txFields = getTransactionFields(t); + + if (process.env.BUILD_ELECTRON) { + const res = await EnclaveAPI.signTransaction({ + walletType: WalletTypes.KEEPKEY, + transaction: txFields, + path: this.getPath() + }); + return new EthTx(res.signedTransaction).serialize(); + } + + throw new Error('KeepKey is not supported on the web'); + } + + public async signMessage(msg: string): Promise { + if (!msg) { + throw Error('No message to sign'); + } + + if (process.env.BUILD_ELECTRON) { + const res = await EnclaveAPI.signMessage({ + walletType: WalletTypes.KEEPKEY, + message: msg, + path: this.getPath() + }); + return res.signedMessage; + } + + throw new Error('KeepKey is not supported on the web'); + } + + public async displayAddress() { + const path = this.dPath + '/' + this.index; + + if (process.env.BUILD_ELECTRON) { + return EnclaveAPI.displayAddress({ + walletType: WalletTypes.KEEPKEY, + path + }) + .then(res => res.success) + .catch(() => false); + } + + throw new Error('KeepKey is not supported on the web'); + } + + public getWalletType(): string { + return translateRaw('X_KEEPKEY'); + } +} diff --git a/common/selectors/config/wallet.ts b/common/selectors/config/wallet.ts index f944dbdb..8095a06c 100644 --- a/common/selectors/config/wallet.ts +++ b/common/selectors/config/wallet.ts @@ -11,6 +11,7 @@ type PathType = keyof DPathFormats; type DPathFormat = | SecureWalletName.TREZOR | SecureWalletName.LEDGER_NANO_S + | SecureWalletName.KEEPKEY | InsecureWalletName.MNEMONIC_PHRASE; export function getPaths(state: AppState, pathType: PathType): DPath[] { diff --git a/common/translations/lang/en.json b/common/translations/lang/en.json index 9aa50351..af771fda 100644 --- a/common/translations/lang/en.json +++ b/common/translations/lang/en.json @@ -74,6 +74,7 @@ "ADD_METAMASK": "Connect to MetaMask ", "X_TREZOR": "TREZOR ", "ADD_TREZOR_SCAN": "Connect to TREZOR ", + "X_KEEPKEY": "KeepKey", "X_PARITYSIGNER": "Parity Signer ", "ADD_PARITY_DESC": "Connect & sign via your Parity Signer mobile app ", "ADD_PARITY_1": "Transaction canceled ", diff --git a/package.json b/package.json index d53f032c..c0b7b59f 100644 --- a/package.json +++ b/package.json @@ -11,6 +11,7 @@ "npm": ">= 5.0.0" }, "dependencies": { + "@keepkey/device-client": "4.1.3", "@ledgerhq/hw-app-eth": "4.7.3", "@ledgerhq/hw-transport-node-hid": "4.7.6", "@ledgerhq/hw-transport-u2f": "4.12.0", @@ -62,6 +63,7 @@ }, "devDependencies": { "@types/bip39": "2.4.0", + "@types/bytebuffer": "5.0.37", "@types/classnames": "2.2.3", "@types/enzyme": "3.1.8", "@types/enzyme-adapter-react-16": "1.0.1", diff --git a/shared/enclave/client/index.ts b/shared/enclave/client/index.ts index f4ea8494..8404ba66 100644 --- a/shared/enclave/client/index.ts +++ b/shared/enclave/client/index.ts @@ -3,6 +3,8 @@ import { EnclaveMethods, GetChainCodeParams, GetChainCodeResponse, + GetAddressParams, + GetAddressResponse, SignTransactionParams, SignTransactionResponse, SignMessageParams, @@ -16,6 +18,10 @@ const api = { return makeRequest(EnclaveMethods.GET_CHAIN_CODE, params); }, + getAddress(params: GetAddressParams) { + return makeRequest(EnclaveMethods.GET_ADDRESS, params); + }, + signTransaction(params: SignTransactionParams) { return makeRequest(EnclaveMethods.SIGN_TRANSACTION, params); }, diff --git a/shared/enclave/server/handlers/getAddress.ts b/shared/enclave/server/handlers/getAddress.ts new file mode 100644 index 00000000..f4c2b14f --- /dev/null +++ b/shared/enclave/server/handlers/getAddress.ts @@ -0,0 +1,7 @@ +import { getWalletLib } from 'shared/enclave/server/wallets'; +import { GetAddressParams, GetAddressResponse } from 'shared/enclave/types'; + +export default function(params: GetAddressParams): Promise { + const wallet = getWalletLib(params.walletType); + return wallet.getAddress(params.index, params.dpath); +} diff --git a/shared/enclave/server/handlers/index.ts b/shared/enclave/server/handlers/index.ts index 34277189..80d8833f 100644 --- a/shared/enclave/server/handlers/index.ts +++ b/shared/enclave/server/handlers/index.ts @@ -1,4 +1,5 @@ import getChainCode from './getChainCode'; +import getAddress from './getAddress'; import signTransaction from './signTransaction'; import signMessage from './signMessage'; import displayAddress from './displayAddress'; @@ -10,6 +11,7 @@ const handlers: { ) => EnclaveMethodResponse | Promise } = { [EnclaveMethods.GET_CHAIN_CODE]: getChainCode, + [EnclaveMethods.GET_ADDRESS]: getAddress, [EnclaveMethods.SIGN_TRANSACTION]: signTransaction, [EnclaveMethods.SIGN_MESSAGE]: signMessage, [EnclaveMethods.DISPLAY_ADDRESS]: displayAddress diff --git a/shared/enclave/server/wallets/keepkey.ts b/shared/enclave/server/wallets/keepkey.ts index 82258d78..ada16324 100644 --- a/shared/enclave/server/wallets/keepkey.ts +++ b/shared/enclave/server/wallets/keepkey.ts @@ -1,8 +1,23 @@ import { WalletLib } from 'shared/enclave/types'; +import { DeviceClientManager } from '@keepkey/device-client/dist/device-client-manager'; +import { NodeVector } from '@keepkey/device-client/dist/node-vector'; + +const dcm = new DeviceClientManager(); const KeepKey: WalletLib = { async getChainCode() { - throw new Error('Not yet implemented'); + throw new Error('KeepKey doesn’t getChainCode'); + }, + + async getAddress(index?: number, dpath?: string) { + if (index === null || index === undefined || !dpath) { + throw new Error('KeepKey requires index and dpath parameters'); + } + + const client = await dcm.getActiveClient(); + const nv = NodeVector.fromString(`${dpath}/${index}`); + const res = await client.getEthereumAddress(nv, false); + return { address: res.toString() as string }; }, async signTransaction() { diff --git a/shared/enclave/server/wallets/ledger.ts b/shared/enclave/server/wallets/ledger.ts index 820b3088..1f84cfbe 100644 --- a/shared/enclave/server/wallets/ledger.ts +++ b/shared/enclave/server/wallets/ledger.ts @@ -36,6 +36,10 @@ const Ledger: WalletLib = { } }, + async getAddress() { + throw new Error('Ledger does not support getAddress'); + }, + async signTransaction(tx, path) { const app = await getEthApp(); const ethTx = new EthTx({ diff --git a/shared/enclave/server/wallets/trezor.ts b/shared/enclave/server/wallets/trezor.ts index 8df6ec61..aea8d233 100644 --- a/shared/enclave/server/wallets/trezor.ts +++ b/shared/enclave/server/wallets/trezor.ts @@ -16,6 +16,10 @@ const Trezor: WalletLib = { // return { chainCode: 'test', publicKey: 'test' }; }, + async getAddress() { + throw new Error('TREZOR does not support getAddress'); + }, + async signTransaction() { throw new Error('Not yet implemented'); }, diff --git a/shared/enclave/types.ts b/shared/enclave/types.ts index 8d2859ca..9fbe5122 100644 --- a/shared/enclave/types.ts +++ b/shared/enclave/types.ts @@ -1,6 +1,7 @@ // Enclave enums export enum EnclaveMethods { GET_CHAIN_CODE = 'get-chain-code', + GET_ADDRESS = 'get-address', SIGN_TRANSACTION = 'sign-transaction', SIGN_MESSAGE = 'sign-message', DISPLAY_ADDRESS = 'display-address' @@ -33,6 +34,17 @@ export interface GetChainCodeResponse { chainCode: string; } +// Get address request +export interface GetAddressParams { + walletType: WalletTypes; + dpath?: string; + index?: number; +} + +export interface GetAddressResponse { + address: string; +} + // Sign Transaction Request export interface SignTransactionParams { walletType: WalletTypes; @@ -68,11 +80,13 @@ export interface DisplayAddressResponse { // All Requests & Responses export type EnclaveMethodParams = | GetChainCodeParams + | GetAddressParams | SignTransactionParams | SignMessageParams | DisplayAddressParams; export type EnclaveMethodResponse = | GetChainCodeResponse + | GetAddressResponse | SignTransactionResponse | SignMessageResponse | DisplayAddressResponse; @@ -99,6 +113,7 @@ export type EnclaveResponse = // Wallet lib export interface WalletLib { getChainCode(dpath: string): Promise; + getAddress(index?: number, dpath?: string): Promise; signTransaction(transaction: RawTransaction, path: string): Promise; signMessage(msg: string, path: string): Promise; displayAddress(path: string): Promise; diff --git a/shared/types/network.d.ts b/shared/types/network.d.ts index 4b63ad4b..28e90163 100644 --- a/shared/types/network.d.ts +++ b/shared/types/network.d.ts @@ -36,6 +36,7 @@ interface NetworkContract { interface DPathFormats { trezor?: DPath; ledgerNanoS?: DPath; + keepkey?: DPath; mnemonicPhrase: DPath; } diff --git a/tsconfig.json b/tsconfig.json index 11fd0130..0bc191bf 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -29,7 +29,8 @@ "./shared/", "spec", "./node_modules/types-rlp/index.d.ts", - "./node_modules/mycrypto-shepherd/dist/lib/types/btoa.d.ts" + "./node_modules/mycrypto-shepherd/dist/lib/types/btoa.d.ts", + "./node_modules/@keepkey/device-client/dist/device-client-manager.d.ts" ], "awesomeTypescriptLoaderOptions": { "transpileOnly": true