Electron Ledger + Trezor Support (#1836)

* Initial scaffold of enclave

* Cleanup types

* Add comments

* Do not truncate errors, pretty output

* Introduce helpers for sagas

* Update yarn lock

* Convert enclave into its own lib. Implement client and server.

* Check in progress

* Initial types

* Remove unused lib

* Finish types

* cleanup

* Switch over to using electron protocol, remove code thats no longer necessary

* Refactor Ledger and Trezor wallets to provide all functionality via libs. Run chain code generation thru Enclave.

* Check in trezor work

* Transaction signing

* Message signing

* Display address

* Fix deallocation of trezor

* Adjust API

* Remove unused getAddresses

* Fix imports and filenames to cooperate with internal typings

* Fix type uncertainty

* Add persistent message to Ledger unlock.

* Update ledger help link to kb

* Convert ledger over to updated libs

* Fix jest config

* Enclave README

* Unnecessary assertion

* Adjust tip

* Type ledger errors

* Reduce enclave client code.

* No default exports

* l18n user facing enclave errors

* Reduce repeated enclave code by splitting it into its own wallet lib. Fix some types

* tslint

* Reduce repeated enclave code by splitting it into its own wallet lib. Fix some types and error messages.

* Electron TREZOR Support (#1946)

* Type trezor connect.

* Check in trezor code

* Implement TREZOR wallet

* Convert TREZOR to use enclave class like Ledger.

* Switch to mycrypto fork of trezor lib. Remove unused dependencies.

* remove unnecessary window attachment

* tslint
This commit is contained in:
William O'Beirne 2018-06-15 19:25:29 -04:00 committed by Daniel Ternyak
parent 41f8ab8966
commit cb92f59e57
42 changed files with 1477 additions and 243 deletions

View File

@ -2,7 +2,7 @@ import React from 'react';
import { connect } from 'react-redux';
import { toChecksumAddress } from 'ethereumjs-util';
import { UnitDisplay, NewTabLink } from 'components/ui';
import { IWallet, TrezorWallet, LedgerWallet, Balance } from 'libs/wallet';
import { IWallet, HardwareWallet, Balance } from 'libs/wallet';
import translate, { translateRaw } from 'translations';
import Spinner from 'components/ui/Spinner';
import { getNetworkConfig, getOffline } from 'selectors/config';
@ -73,7 +73,7 @@ class AccountInfo extends React.Component<Props, State> {
};
public render() {
const { network, isOffline, balance } = this.props;
const { network, isOffline, balance, wallet } = this.props;
const { address, showLongBalance, confirmAddr } = this.state;
let blockExplorer;
@ -84,12 +84,11 @@ class AccountInfo extends React.Component<Props, State> {
tokenExplorer = network.tokenExplorer;
}
const wallet = this.props.wallet as LedgerWallet | TrezorWallet;
return (
<div>
<AccountAddress address={toChecksumAddress(address)} />
{typeof wallet.displayAddress === 'function' && (
{isHardwareWallet(wallet) && (
<div className="AccountInfo-section">
<a
className="AccountInfo-address-hw-addr"
@ -98,9 +97,9 @@ class AccountInfo extends React.Component<Props, State> {
wallet
.displayAddress()
.then(() => this.toggleConfirmAddr())
.catch(e => {
.catch((e: Error | string) => {
console.error('Display address failed', e);
this.toggleConfirmAddr();
throw new Error(e);
});
}}
>
@ -192,6 +191,10 @@ class AccountInfo extends React.Component<Props, State> {
}
}
function isHardwareWallet(wallet: IWallet): wallet is HardwareWallet {
return typeof (wallet as any).displayAddress === 'function';
}
function mapStateToProps(state: AppState): StateProps {
return {
balance: state.wallet.balance,

View File

@ -1,6 +1,14 @@
@import 'common/sass/variables';
.LedgerDecrypt {
text-align: center;
&-tip {
font-size: $font-size-xs-bump;
opacity: 0.7;
margin-bottom: $space;
}
&-help {
margin-top: 10px;
font-size: 13px;

View File

@ -1,13 +1,12 @@
import React, { PureComponent } from 'react';
import ledger from 'ledgerco';
import translate, { translateRaw } from 'translations';
import DeterministicWalletsModal from './DeterministicWalletsModal';
import UnsupportedNetwork from './UnsupportedNetwork';
import { LedgerWallet } from 'libs/wallet';
import { Spinner, NewTabLink } from 'components/ui';
import { Spinner, NewTabLink, HelpLink } from 'components/ui';
import { connect } from 'react-redux';
import { AppState } from 'reducers';
import { SecureWalletName, ledgerReferralURL } from 'config';
import { SecureWalletName, ledgerReferralURL, HELP_ARTICLE } from 'config';
import { getPaths, getSingleDPath } from 'selectors/config/wallet';
import { getNetworkConfig } from 'selectors/config';
import { NetworkConfig } from 'types/network';
@ -29,7 +28,6 @@ interface State {
dPath: DPath;
error: string | null;
isLoading: boolean;
showTip: boolean;
}
type Props = OwnProps & StateProps;
@ -40,14 +38,7 @@ class LedgerNanoSDecryptClass extends PureComponent<Props, State> {
chainCode: '',
dPath: this.props.dPath || this.props.dPaths[0],
error: null,
isLoading: false,
showTip: false
};
public showTip = () => {
this.setState({
showTip: true
});
isLoading: false
};
public UNSAFE_componentWillReceiveProps(nextProps: Props) {
@ -58,14 +49,14 @@ class LedgerNanoSDecryptClass extends PureComponent<Props, State> {
public render() {
const { network } = this.props;
const { dPath, publicKey, chainCode, error, isLoading, showTip } = this.state;
const { dPath, publicKey, chainCode, error, isLoading } = this.state;
const showErr = error ? 'is-showing' : '';
if (!dPath) {
return <UnsupportedNetwork walletType={translateRaw('x_Ledger')} />;
}
if (window.location.protocol !== 'https:') {
if (!process.env.BUILD_ELECTRON && window.location.protocol !== 'https:') {
return (
<div className="LedgerDecrypt">
<div className="alert alert-danger">
@ -78,6 +69,15 @@ class LedgerNanoSDecryptClass extends PureComponent<Props, State> {
return (
<div className="LedgerDecrypt">
<div className="LedgerDecrypt-tip">
{translate('LEDGER_TIP', {
$network: network.unit,
$browserSupportState: process.env.BUILD_ELECTRON
? translateRaw('DISABLED')
: translateRaw('ENABLED')
})}
</div>
<button
className="LedgerDecrypt-decrypt btn btn-primary btn-lg btn-block"
onClick={this.handleNullConnect}
@ -99,14 +99,10 @@ class LedgerNanoSDecryptClass extends PureComponent<Props, State> {
<div className={`LedgerDecrypt-error alert alert-danger ${showErr}`}>{error || '-'}</div>
{showTip && (
<p className="LedgerDecrypt-tip">{translate('LEDGER_TIP', { $network: network.unit })}</p>
)}
<div className="LedgerDecrypt-help">
<NewTabLink href="https://support.ledgerwallet.com/hc/en-us/articles/115005200009">
<HelpLink article={HELP_ARTICLE.HOW_TO_USE_LEDGER}>
{translate('HELP_ARTICLE_1')}
</NewTabLink>
</HelpLink>
</div>
<DeterministicWalletsModal
@ -133,51 +129,23 @@ class LedgerNanoSDecryptClass extends PureComponent<Props, State> {
private handleConnect = (dPath: DPath) => {
this.setState({
isLoading: true,
error: null,
showTip: false
error: null
});
ledger.comm_u2f.create_async().then((comm: any) => {
new ledger.eth(comm)
.getAddress_async(dPath.value, false, true)
.then(res => {
this.setState({
publicKey: res.publicKey,
chainCode: res.chainCode,
isLoading: false
});
})
.catch((err: any) => {
let showTip;
let errMsg;
// Timeout
if (err && err.metaData && err.metaData.code === 5) {
showTip = true;
errMsg = translateRaw('LEDGER_TIMEOUT');
}
// Wrong app logged into
if (err && err.includes && err.includes('6804')) {
showTip = true;
errMsg = translateRaw('LEDGER_WRONG_APP');
}
// Ledger locked
if (err && err.includes && err.includes('6801')) {
errMsg = translateRaw('LEDGER_LOCKED');
}
// Other
if (!errMsg) {
errMsg = err && err.metaData ? err.metaData.type : err.toString();
}
this.setState({
error: errMsg,
isLoading: false
});
if (showTip) {
this.showTip();
}
LedgerWallet.getChainCode(dPath.value)
.then(res => {
this.setState({
publicKey: res.publicKey,
chainCode: res.chainCode,
isLoading: false
});
});
})
.catch(err => {
this.setState({
error: translateRaw(err.message),
isLoading: false
});
});
};
private handleCancel = () => {

View File

@ -1,7 +1,6 @@
import { TrezorWallet, TREZOR_MINIMUM_FIRMWARE } from 'libs/wallet';
import { TrezorWallet } from 'libs/wallet';
import React, { PureComponent } from 'react';
import translate, { translateRaw } from 'translations';
import TrezorConnect from 'vendor/trezor-connect';
import DeterministicWalletsModal from './DeterministicWalletsModal';
import UnsupportedNetwork from './UnsupportedNetwork';
import { Spinner, NewTabLink } from 'components/ui';
@ -109,25 +108,21 @@ class TrezorDecryptClass extends PureComponent<Props, State> {
error: null
});
(TrezorConnect as any).getXPubKey(
dPath.value,
(res: any) => {
if (res.success) {
this.setState({
dPath,
publicKey: res.publicKey,
chainCode: res.chainCode,
isLoading: false
});
} else {
this.setState({
error: res.error,
isLoading: false
});
}
},
TREZOR_MINIMUM_FIRMWARE
);
TrezorWallet.getChainCode(dPath.value)
.then(res => {
this.setState({
dPath,
publicKey: res.publicKey,
chainCode: res.chainCode,
isLoading: false
});
})
.catch(err => {
this.setState({
error: err.message,
isLoading: false
});
});
};
private handleCancel = () => {

View File

@ -9,6 +9,7 @@ export enum HELP_ARTICLE {
ADDING_NEW_TOKENS = 'tokens/adding-new-token-and-sending-custom-tokens.html',
HARDWARE_WALLET_RECOMMENDATIONS = 'hardware-wallets/hardware-wallet-recommendations.html',
SENDING_TO_TREZOR = 'hardware-wallets/trezor-sending-to-trezor-device.html',
HOW_TO_USE_LEDGER = 'accessing-your-wallet/how-to-use-your-ledger-with-mycrypto.html',
SECURING_YOUR_ETH = 'security/securing-your-ethereum.html',
PROTECT_YOUR_FUNDS = 'getting-started/protecting-yourself-and-your-funds.html',
WHAT_IS_NONCE = 'transactions/what-is-nonce.html',

View File

@ -0,0 +1,66 @@
import EthTx from 'ethereumjs-tx';
import EnclaveAPI, { WalletTypes } from 'shared/enclave/client';
import { getTransactionFields } from 'libs/transaction';
import { HardwareWallet, ChainCodeResponse } from './hardware';
import { IFullWallet } from '../IWallet';
import { translateRaw } from 'translations';
const walletTypeNames = {
[WalletTypes.LEDGER]: 'X_LEDGER',
[WalletTypes.TREZOR]: 'X_TREZOR',
[WalletTypes.KEEPKEY]: 'X_KEEPKEY'
};
export function makeEnclaveWallet(walletType: WalletTypes) {
class EnclaveWallet extends HardwareWallet implements IFullWallet {
public static async getChainCode(dpath: string): Promise<ChainCodeResponse> {
return EnclaveAPI.getChainCode({
walletType,
dpath
});
}
constructor(address: string, dPath: string, index: number) {
super(address, dPath, index);
}
public async signRawTransaction(t: EthTx): Promise<Buffer> {
const txFields = getTransactionFields(t);
const res = await EnclaveAPI.signTransaction({
walletType,
transaction: txFields,
path: this.getPath()
});
return new EthTx(res.signedTransaction).serialize();
}
public async signMessage(msg: string): Promise<string> {
if (!msg) {
throw Error('No message to sign');
}
const res = await EnclaveAPI.signMessage({
walletType,
message: msg,
path: this.getPath()
});
return res.signedMessage;
}
public async displayAddress() {
const path = `${this.dPath}/${this.index}`;
return EnclaveAPI.displayAddress({
walletType,
path
})
.then(res => res.success)
.catch(() => false);
}
public getWalletType(): string {
return translateRaw(walletTypeNames[walletType]);
}
}
return EnclaveWallet;
}

View File

@ -0,0 +1,21 @@
import EthTx from 'ethereumjs-tx';
import { DeterministicWallet } from './deterministic';
import { IFullWallet } from '../IWallet';
export interface ChainCodeResponse {
chainCode: string;
publicKey: string;
}
export abstract class HardwareWallet extends DeterministicWallet implements IFullWallet {
// Static functions can't be abstract, so implement an errorous one
// @ts-ignore
public static getChainCode(dpath: string): Promise<ChainCodeResponse> {
throw new Error(`getChainCode is not implemented in ${this.constructor.name}`);
}
public abstract signRawTransaction(t: EthTx): Promise<Buffer>;
public abstract signMessage(msg: string): Promise<string>;
public abstract displayAddress(): Promise<boolean>;
public abstract getWalletType(): string;
}

View File

@ -1,3 +1,13 @@
export * from './ledger';
import { makeEnclaveWallet } from './enclave';
import { WalletTypes } from 'shared/enclave/client';
import { LedgerWallet as LedgerWalletWeb } from './ledger';
import { TrezorWallet as TrezorWalletWeb } from './trezor';
function enclaveOrWallet<T>(type: WalletTypes, lib: T) {
return process.env.BUILD_ELECTRON ? makeEnclaveWallet(type) : lib;
}
export * from './mnemonic';
export * from './trezor';
export * from './hardware';
export const LedgerWallet = enclaveOrWallet(WalletTypes.LEDGER, LedgerWalletWeb);
export const TrezorWallet = enclaveOrWallet(WalletTypes.TREZOR, TrezorWalletWeb);

View File

@ -1,80 +1,128 @@
import ledger from 'ledgerco';
import TransportU2F from '@ledgerhq/hw-transport-u2f';
import LedgerEth from '@ledgerhq/hw-app-eth';
import EthTx, { TxObj } from 'ethereumjs-tx';
import { addHexPrefix, toBuffer } from 'ethereumjs-util';
import { DeterministicWallet } from './deterministic';
import { HardwareWallet, ChainCodeResponse } from './hardware';
import { getTransactionFields } from 'libs/transaction';
import { IFullWallet } from '../IWallet';
import { translateRaw } from 'translations';
export class LedgerWallet extends DeterministicWallet implements IFullWallet {
private ethApp: ledger.eth;
// Ledger throws a few types of errors
interface U2FError {
metaData: {
type: string;
code: number;
};
}
type LedgerError = U2FError | Error | string;
export class LedgerWallet extends HardwareWallet {
public static async getChainCode(dpath: string): Promise<ChainCodeResponse> {
return makeApp()
.then(app => app.getAddress(dpath, false, true))
.then(res => {
return {
publicKey: res.publicKey,
chainCode: res.chainCode
};
})
.catch((err: LedgerError) => {
throw new Error(ledgerErrToMessage(err));
});
}
constructor(address: string, dPath: string, index: number) {
super(address, dPath, index);
ledger.comm_u2f.create_async().then((comm: any) => {
this.ethApp = new ledger.eth(comm);
});
}
// modeled after
// https://github.com/kvhnuke/etherwallet/blob/3f7ff809e5d02d7ea47db559adaca1c930025e24/app/scripts/uiFuncs.js#L58
public signRawTransaction(t: EthTx): Promise<Buffer> {
public async signRawTransaction(t: EthTx): Promise<Buffer> {
const txFields = getTransactionFields(t);
t.v = Buffer.from([t._chainId]);
t.r = toBuffer(0);
t.s = toBuffer(0);
return new Promise((resolve, reject) => {
this.ethApp
.signTransaction_async(this.getPath(), t.serialize().toString('hex'))
.then(result => {
const strTx = getTransactionFields(t);
const txToSerialize: TxObj = {
...strTx,
v: addHexPrefix(result.v),
r: addHexPrefix(result.r),
s: addHexPrefix(result.s)
};
try {
const ethApp = await makeApp();
const result = await ethApp.signTransaction(this.getPath(), t.serialize().toString('hex'));
const serializedTx = new EthTx(txToSerialize).serialize();
resolve(serializedTx);
})
.catch(err => {
return reject(Error(err + '. Check to make sure contract data is on'));
});
});
const txToSerialize: TxObj = {
...txFields,
v: addHexPrefix(result.v),
r: addHexPrefix(result.r),
s: addHexPrefix(result.s)
};
return new EthTx(txToSerialize).serialize();
} catch (err) {
throw Error(err + '. Check to make sure contract data is on');
}
}
// modeled after
// https://github.com/kvhnuke/etherwallet/blob/3f7ff809e5d02d7ea47db559adaca1c930025e24/app/scripts/controllers/signMsgCtrl.js#L53
public async signMessage(msg: string): Promise<string> {
if (!msg) {
throw Error('No message to sign');
}
const msgHex = Buffer.from(msg).toString('hex');
const signed = await this.ethApp.signPersonalMessage_async(this.getPath(), msgHex);
const combined = addHexPrefix(signed.r + signed.s + signed.v.toString(16));
return combined;
try {
const msgHex = Buffer.from(msg).toString('hex');
const ethApp = await makeApp();
const signed = await ethApp.signPersonalMessage(this.getPath(), msgHex);
const combined = addHexPrefix(signed.r + signed.s + signed.v.toString(16));
return combined;
} catch (err) {
throw new Error(ledgerErrToMessage(err));
}
}
public displayAddress = (
dPath?: string,
index?: number
): Promise<{
publicKey: string;
address: string;
chainCode?: string;
}> => {
if (!dPath) {
dPath = this.dPath;
public async displayAddress() {
const path = `${this.dPath}/${this.index}`;
try {
const ethApp = await makeApp();
await ethApp.getAddress(path, true, false);
return true;
} catch (err) {
console.error('Failed to display Ledger address:', err);
return false;
}
if (!index) {
index = this.index;
}
return this.ethApp.getAddress_async(dPath + '/' + index, true, false);
};
}
public getWalletType(): string {
return translateRaw('X_LEDGER');
}
}
async function makeApp() {
const transport = await TransportU2F.create();
return new LedgerEth(transport);
}
const isU2FError = (err: LedgerError): err is U2FError => !!err && !!(err as U2FError).metaData;
const isStringError = (err: LedgerError): err is string => typeof err === 'string';
function ledgerErrToMessage(err: LedgerError) {
// https://developers.yubico.com/U2F/Libraries/Client_error_codes.html
if (isU2FError(err)) {
// Timeout
if (err.metaData.code === 5) {
return translateRaw('LEDGER_TIMEOUT');
}
return err.metaData.type;
}
if (isStringError(err)) {
// Wrong app logged into
if (err.includes('6804')) {
return translateRaw('LEDGER_WRONG_APP');
}
// Ledger locked
if (err.includes('6801')) {
return translateRaw('LEDGER_LOCKED');
}
return err;
}
// Other
return err.toString();
}

View File

@ -3,23 +3,40 @@ import EthTx, { TxObj } from 'ethereumjs-tx';
import { addHexPrefix } from 'ethereumjs-util';
import { stripHexPrefixAndLower, padLeftEven } from 'libs/values';
import TrezorConnect from 'vendor/trezor-connect';
import { DeterministicWallet } from './deterministic';
import { HardwareWallet, ChainCodeResponse } from './hardware';
import { getTransactionFields } from 'libs/transaction';
import mapValues from 'lodash/mapValues';
import { IFullWallet } from '../IWallet';
import { translateRaw } from 'translations';
export const TREZOR_MINIMUM_FIRMWARE = '1.5.2';
export class TrezorWallet extends DeterministicWallet implements IFullWallet {
export class TrezorWallet extends HardwareWallet {
public static getChainCode(dpath: string): Promise<ChainCodeResponse> {
return new Promise(resolve => {
TrezorConnect.getXPubKey(
dpath,
res => {
if (res.success) {
resolve({
publicKey: res.publicKey,
chainCode: res.chainCode
});
} else {
throw new Error(res.error);
}
},
TREZOR_MINIMUM_FIRMWARE
);
});
}
public signRawTransaction(tx: EthTx): Promise<Buffer> {
return new Promise((resolve, reject) => {
const { chainId, ...strTx } = getTransactionFields(tx);
// stripHexPrefixAndLower identical to ethFuncs.getNakedAddress
const cleanedTx = mapValues(mapValues(strTx, stripHexPrefixAndLower), padLeftEven);
(TrezorConnect as any).ethereumSignTx(
TrezorConnect.ethereumSignTx(
// Args
this.getPath(),
cleanedTx.nonce,
@ -30,7 +47,7 @@ export class TrezorWallet extends DeterministicWallet implements IFullWallet {
cleanedTx.data,
chainId,
// Callback
(result: any) => {
result => {
if (!result.success) {
return reject(Error(result.error));
}
@ -40,7 +57,7 @@ export class TrezorWallet extends DeterministicWallet implements IFullWallet {
const txToSerialize: TxObj = {
...strTx,
v: addHexPrefix(new BN(result.v).toString(16)),
r: addHexPrefix(result.r),
r: addHexPrefix(result.r.toString()),
s: addHexPrefix(result.s)
};
const eTx = new EthTx(txToSerialize);
@ -51,30 +68,25 @@ export class TrezorWallet extends DeterministicWallet implements IFullWallet {
});
}
public signMessage = () => Promise.reject(new Error('Signing via Trezor not yet supported.'));
public signMessage() {
return Promise.reject(new Error('Signing via Trezor not yet supported.'));
}
public displayAddress = (dPath?: string, index?: number): Promise<any> => {
if (!dPath) {
dPath = this.dPath;
}
if (!index) {
index = this.index;
}
return new Promise((resolve, reject) => {
(TrezorConnect as any).ethereumGetAddress(
dPath + '/' + index,
(res: any) => {
public displayAddress(): Promise<boolean> {
return new Promise(resolve => {
TrezorConnect.ethereumGetAddress(
`${this.dPath}/${this.index}`,
res => {
if (res.error) {
reject(res.error);
resolve(false);
} else {
resolve(res);
resolve(true);
}
},
TREZOR_MINIMUM_FIRMWARE
);
});
};
}
public getWalletType(): string {
return translateRaw('X_TREZOR');

View File

@ -187,9 +187,6 @@ export function getDisabledWallets(state: AppState): DisabledWallets {
}
// Some wallets are disabled on certain platforms
if (process.env.BUILD_DOWNLOADABLE) {
addReason([SecureWalletName.LEDGER_NANO_S], 'This wallet is only supported at MyCrypto.com');
}
if (process.env.BUILD_ELECTRON) {
addReason([SecureWalletName.WEB3], 'This wallet is not supported in the MyCrypto app');
}

View File

@ -50,6 +50,7 @@
"X_MIST": "Mist",
"X_WEB3": "Web3",
"X_MNEMONIC": "Mnemonic Phrase ",
"X_HARDWARE_WALLET": "hardware wallet ",
"X_PRINT": "Print Paper Wallet ",
"X_PRINTDESC": "ProTip: Click print and save this as a PDF, even if you do not own a printer! ",
"X_PRIVKEY": "Private Key (unencrypted) ",
@ -76,6 +77,7 @@
"X_TREZOR": "TREZOR ",
"X_HARDWARE_WALLET": "Hardware Wallet",
"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 ",
@ -447,7 +449,9 @@
"OLD_MYCRYPTO": "MyCrypto (Legacy Site)",
"LEDGER_REFERRAL_1": "Buy a Ledger Wallet",
"LEDGER_REFERRAL_2": "Dont have a Ledger? Order one now!",
"LEDGER_TIP": "**Tip:** Make sure you're logged into the $network app on your hardware wallet, and have enabled browser support in the settings",
"LEDGER_TIP": "Make sure your Ledger is unlocked, you've selected the **$network** app on the device, and browser support is **$browserSupportState**.",
"ENABLED": "enabled",
"DISABLED": "disabled",
"LEDGER_TIMEOUT": "The request timed out",
"LEDGER_WRONG_APP": "Wrong application selected on your device",
"LEDGER_LOCKED": "Your Ledger device is locked",
@ -636,6 +640,8 @@
"COINBASE_PROMO_SMALL": "Its now easier to get more ETH",
"COINBASE_PROMO": "Buy ETH with USD",
"SIMPLEX_PROMO": "Buy ETH with EUR",
"TESTNET": "Testnet"
"TESTNET": "Testnet",
"ENCLAVE_LEDGER_FAIL": "Failed to connect to Ledger",
"ENCLAVE_LEDGER_IN_USE": "Your Ledger is currently in use with another application. Please wait, or close other wallet applications before trying again."
}
}

View File

@ -13,11 +13,15 @@ declare module '@ledgerhq/hw-app-eth' {
* @returns {Promise<{ publicKey: string; address: string; chainCode?: string }>}
* @memberof Eth
*/
public getAddress(
public getAddress<BoolChaincode extends boolean>(
path: string,
boolDisplay?: boolean,
boolChaincode?: boolean
): Promise<{ publicKey: string; address: string; chainCode?: string }>;
boolChaincode?: BoolChaincode
): Promise<{
publicKey: string;
address: string;
chainCode: BoolChaincode extends true ? string : undefined;
}>;
/**
*

View File

@ -1,45 +0,0 @@
declare module 'ledgerco' {
// TODO: fill the library typings out
export const comm_u2f: any;
export class eth {
constructor(transport: any);
getAddress_async(
path: string,
boolDisplay?: boolean,
boolChaincode?: boolean
): Promise<{
publicKey: string;
address: string;
chainCode: string;
}>;
signTransaction_async(
path: string,
rawTxHex: string
): Promise<{
s: string;
v: string;
r: string;
}>;
getAppConfiguration_async(): Promise<{
arbitraryDataEnabled: number;
version: string;
}>;
signPersonalMessage_async(
path: string,
messageHex: string
): Promise<{
v: number;
s: string;
r: string;
}>;
signPersonalMessage_async(
path: string,
messageHex: string,
cb: (signed: any, error: any) => any
): void;
}
}

69
common/typescript/trezor-connect.d.ts vendored Normal file
View File

@ -0,0 +1,69 @@
declare module 'vendor/trezor-connect' {
type Path = number[] | string;
interface TxSignature {
r: number;
s: string;
v: string;
}
interface MessageSignature {
signature: string;
address: string;
}
interface PublicKey {
xpubkey: string;
path: string;
serializedPath: string;
chainCode: string;
publicKey: string;
}
interface ErrorResponse {
success: false;
error: string;
}
type SuccessResponse<T> = {
success: true;
error: undefined;
} & T;
type Response<T> = ErrorResponse | SuccessResponse<T>;
namespace TrezorConnect {
export function getXPubKey(
path: Path,
cb: (res: Response<PublicKey>) => void,
minFirmware?: string
): void;
export function ethereumSignTx(
path: Path,
nonce: string,
gasPrice: string,
gasLimit: string,
to: string,
value: string,
data: string | null,
chainId: number | null,
cb: (signature: Response<TxSignature>) => void,
minFirmware?: string
): void;
export function signMessage(
path: Path,
message: string,
cb: (res: Response<MessageSignature>) => void,
coin?: string,
minFirmware?: string
): void;
export function ethereumGetAddress(
path: Path,
cb: (res: Response<{ address: string }>) => void,
minFirmware?: string
): void;
}
export default TrezorConnect;
}

191
common/typescript/trezor-js.d.ts vendored Normal file
View File

@ -0,0 +1,191 @@
/* tslint:disable max-classes-per-file */
// Types are only based off of what's mentioned in the API
// https://github.com/trezor/trezor.js/blob/master/API.md
declare module 'mycrypto-trezor.js' {
import { EventEmitter } from 'events';
import { Transport, TrezorDeviceInfoWithSession as DeviceDescriptor } from 'trezor-link';
/***************/
/* Device List */
/***************/
export interface DeviceListOptions {
debug?: boolean;
debugInfo?: boolean;
transport?: Transport;
nodeTransport?: Transport;
configUrl?: string;
config?: string;
bridgeVersionUrl?: string;
clearSession?: boolean;
clearSessionTime?: number;
rememberDevicePasshprase?: boolean;
// Unsure of these options or our need for them
// getPassphraseHash?(device: Device): number[] | undefined;
// xpubDerive?: (xpub: string, network: bitcoin.Network, index: number) => Promise<string>;
}
export class DeviceList extends EventEmitter {
public transport: Transport | undefined;
public devices: { [k: string]: Device };
public unacquiredDevices: { [k: string]: UnacquiredDevice };
constructor(opts?: DeviceListOptions);
public acquireFirstDevice(
rejectOnEmpty?: boolean
): Promise<{ device: Device; session: Session }>;
}
/**********/
/* Device */
/**********/
export interface CoinType {
coin_name: string;
coin_shortcut: string;
address_type: number;
maxfee_kb: number;
address_type_p2sh: number;
}
export interface Features {
vendor: string;
major_version: number;
minor_version: number;
patch_version: number;
bootloader_mode: boolean;
device_id: string;
pin_protection: boolean;
passphrase_protection: boolean;
language: string;
label: string;
coins: CoinType[];
initialized: boolean;
revision: string;
bootloader_hash: string;
imported: boolean;
pin_cached: boolean;
passphrase_cached: boolean;
needs_backup?: boolean;
firmware_present?: boolean;
flags?: number;
model?: string;
unfinished_backup?: boolean;
}
export interface RunOptions {
aggressive?: boolean;
skipFinalReload?: boolean;
waiting?: boolean;
onlyOneActivity?: boolean;
}
export class Device extends EventEmitter {
public path: string;
public features: Features;
constructor(
transport: Transport,
descriptor: DeviceDescriptor,
features: Features,
deviceList: DeviceList
);
public isBootloader(): boolean;
public isInitialized(): boolean;
public getVersion(): string;
public atLeast(v: string): boolean;
public isUsed(): boolean;
public isUsedHere(): boolean;
public isUsedElsewhere(): boolean;
public run<T>(fn: (session: Session) => Promise<T> | T, options?: RunOptions): Promise<T>;
public waitForSessionAndRun<T>(
fn: (session: Session) => Promise<T> | T,
options?: RunOptions
): Promise<T>;
public steal(): Promise<boolean>;
}
/*********************/
/* Unacquired Device */
/*********************/
export class UnacquiredDevice extends EventEmitter {
public path: string;
constructor(transport: Transport, descriptor: DeviceDescriptor, deviceList: DeviceList);
public steal(): Promise<boolean>;
}
/***********/
/* Session */
/***********/
export interface MessageResponse<T> {
type: string;
message: T;
}
export interface EthereumSignature {
v: number;
r: string;
s: string;
}
export interface HDPubNode {
depth: number;
fingerprint: number;
child_num: number;
chain_code: string;
public_key: string;
}
export interface PublicKey {
node: HDPubNode;
xpub: string;
}
export type DefaultMessageResponse = MessageResponse<object>;
export class Session extends EventEmitter {
public typedCall<T>(type: string, resType: string, message: T): Promise<T>;
public getEntropy(size: number): Promise<MessageResponse<{ bytes: string }>>;
public ethereumGetAddress(
path: number[],
display?: boolean
): Promise<MessageResponse<{ address: string; path: number[] }>>;
public clearSession(): Promise<boolean>;
public signEthMessage(
path: number[],
message: string
): Promise<MessageResponse<{ address: string; signature: string }>>;
public verifyEthMessage(address: string, signature: string, message: string): Promise<boolean>;
public signEthTx(
path: number[],
nonce: string,
gasPrice: string,
gasLimit: string,
to: string,
value: string,
data?: string,
chainId?: number
): Promise<EthereumSignature>;
public getPublicKey(
path: number[],
coin?: string | CoinType
): Promise<MessageResponse<PublicKey>>;
/* Unused functions, either Bitcoin-centric or just things we wouldn't want to touch */
// public getAddress(path: number[], coin: string | CoinType, display: boolean): Promise<MessageResponse<{ address: string }>>;
// public verifyAddress(path: number[], refAddress: string, coin: string | CoinType): Promise<boolean>;
// public getHDNode(path: number[], coin: string | CoinType): Promise<HDNode>;
// public wipeDevice(): Promise<boolean>;
// public resetDevice(...): Promise<boolean>;
// public loadDevice(...): Promise<boolean>;
// public recoverDevice(...): Promise<boolean>;
// public updateFirmware(payload: string): Promise<boolean>;
// public signMessage(path: number[], message: string, coin: string | CoinType, segwit: boolean): Promise<object>;
// public verifyMessage(...): Promise<boolean>;
// public signIdentity(...): any;
}
}

39
common/typescript/trezor-link.d.ts vendored Normal file
View File

@ -0,0 +1,39 @@
declare module 'trezor-link' {
export interface TrezorDeviceInfo {
path: string;
}
export interface TrezorDeviceInfoWithSession extends TrezorDeviceInfo {
session?: string;
}
export interface AcquireInput {
path: string;
previous?: string;
checkPrevious: boolean;
}
export interface MessageFromTrezor {
type: string;
message: any;
}
export interface Transport {
configured: boolean;
version: string;
name: string;
requestNeeded: boolean;
isOutdated: boolean;
enumerate(): Promise<TrezorDeviceInfoWithSession[]>;
listen(old?: TrezorDeviceInfoWithSession[]): Promise<TrezorDeviceInfoWithSession[]>;
acquire(input: AcquireInput): Promise<string>;
release(session: string, onclose: boolean): Promise<void>;
configure(signedData: string): Promise<void>;
call(session: string, name: string, data: any): Promise<MessageFromTrezor>;
init(debug?: boolean): Promise<void>;
stop(): void;
requestDevice(): Promise<void>;
setBridgeLatestUrl(url: string): void;
}
}

View File

@ -1,4 +1,6 @@
import 'babel-polyfill';
import { app } from 'electron';
import { registerServer } from 'shared/enclave/server';
import getWindow from './window';
// Quit application when all windows are closed
@ -20,3 +22,6 @@ app.on('activate', () => {
app.on('ready', () => {
getWindow();
});
// Register enclave protocol
registerServer(app);

View File

@ -1,5 +1,6 @@
import { BrowserWindow, Menu, shell } from 'electron';
import { URL } from 'url';
import path from 'path';
import MENU from './menu';
import popupContextMenu from './contextMenu';
import { APP_TITLE } from '../constants';
@ -25,7 +26,8 @@ export default function getWindow() {
webPreferences: {
devTools: true,
nodeIntegration: false,
contextIsolation: true
contextIsolation: true,
preload: path.join(__dirname, 'preload.js')
}
});

View File

@ -0,0 +1,2 @@
import { registerProtocol } from 'shared/enclave/preload';
registerProtocol();

View File

@ -7,6 +7,7 @@
"moduleDirectories": ["node_modules", "common"],
"moduleFileExtensions": ["ts", "tsx", "js", "jsx", "json", "worker.ts"],
"moduleNameMapper": {
"shared/(.*)": "<rootDir>/shared/$1",
"\\.(jpg|jpeg|png|gif|eot|otf|webp|svg|ttf|woff|woff2|mp4|webm|wav|mp3|m4a|aac|oga)$":
"<rootDir>/jest_config/__mocks__/fileMock.ts",
"\\.(css|scss)$": "<rootDir>/jest_config/__mocks__/styleMock.ts",

View File

@ -7,6 +7,7 @@
"moduleDirectories": ["node_modules", "common"],
"moduleFileExtensions": ["ts", "tsx", "js", "jsx", "json"],
"moduleNameMapper": {
"shared/(.*)": "<rootDir>/shared/$1",
"\\.(jpg|jpeg|png|gif|eot|otf|webp|svg|ttf|woff|woff2|mp4|webm|wav|mp3|m4a|aac|oga)$":
"<rootDir>/jest_config/__mocks__/fileMock.ts",
"\\.(css|scss)$": "<rootDir>/jest_config/__mocks__/styleMock.ts"

View File

@ -11,6 +11,9 @@
"npm": ">= 5.0.0"
},
"dependencies": {
"@ledgerhq/hw-app-eth": "4.7.3",
"@ledgerhq/hw-transport-node-hid": "4.7.6",
"@ledgerhq/hw-transport-u2f": "4.12.0",
"@parity/qr-signer": "0.2.1",
"babel-polyfill": "6.26.0",
"bip39": "2.5.0",
@ -18,7 +21,6 @@
"bootstrap-sass": "3.3.7",
"classnames": "2.2.5",
"electron-updater": "2.21.10",
"mycrypto-eth-exists": "1.0.0",
"ethereum-blockies-base64": "1.0.1",
"ethereumjs-abi": "git://github.com/ethereumjs/ethereumjs-abi.git#09c3c48fd3bed143df7fa8f36f6f164205e23796",
"ethereumjs-tx": "1.3.4",
@ -28,11 +30,12 @@
"hdkey": "0.8.0",
"idna-uts46": "1.1.0",
"jsonschema": "1.2.4",
"ledgerco": "1.2.1",
"lodash": "4.17.5",
"moment": "2.22.1",
"moment-timezone": "0.5.14",
"mycrypto-eth-exists": "1.0.0",
"mycrypto-shepherd": "1.4.0",
"mycrypto-trezor.js": "6.17.5",
"normalizr": "3.2.4",
"qrcode": "1.2.0",
"qrcode.react": "0.8.0",
@ -95,6 +98,7 @@
"css-loader": "0.28.11",
"electron": "2.0.1",
"electron-builder": "20.13.4",
"electron-rebuild": "1.7.3",
"empty": "0.10.1",
"enzyme": "3.3.0",
"enzyme-adapter-react-16": "1.1.1",
@ -112,6 +116,7 @@
"lint-staged": "7.0.4",
"mini-css-extract-plugin": "0.4.0",
"minimist": "1.2.0",
"node-hid": "0.7.2",
"mycrypto-nano-result": "0.0.1",
"node-sass": "4.8.3",
"nodemon": "1.17.3",
@ -185,7 +190,8 @@
"formatAll": "find ./common/ -name '*.ts*' | xargs prettier --write --config ./.prettierrc --config-precedence file-override",
"prettier:diff": "prettier --write --config ./.prettierrc --list-different \"common/**/*.ts\" \"common/**/*.tsx\"",
"prepush": "npm run tslint && npm run tscheck",
"update:tokens": "ts-node scripts/update-tokens"
"update:tokens": "ts-node scripts/update-tokens",
"postinstall": "electron-rebuild --force"
},
"lint-staged": {
"*.{ts,tsx}": [

34
shared/enclave/README.md Normal file
View File

@ -0,0 +1,34 @@
# ETH Enclave
Enclave is the communication layer between hardware wallets and the Electron
web view. This layer is necessary if you've disabled node integration, and
enabled context isolation on your webview ([Which is something you should do.](https://github.com/electron/electron/blob/master/docs/tutorial/security.md))
Enclave uses Electron's Protocol API to open up an HTTP-like communication layer
between Electron and the web view. You can read [more about this approach here](https://gist.github.com/wbobeirne/ec3e52b3db1359278c19f29e1bbfd5f1).
## Setup
```js
// Electron main js
import { registerServer } from 'enclave/server';
registerServer(app);
```
```js
// Electron preload js
import { registerProtocol } from 'enclave/preload';
registerProtocol();
```
## Usage
```js
import EnclaveAPI, { WalletTypes } from 'enclave/client';
EnclaveAPI.getChainCode({
walletType: WalletTypes.LEDGER,
dpath: "m/44'/60'/0'/0"
}).then(({ publicKey, chainCode }) => {
// ...
});
```

View File

@ -0,0 +1,35 @@
import { makeRequest } from './requests';
import {
EnclaveMethods,
EnclaveMethodParams,
GetChainCodeParams,
GetChainCodeResponse,
SignTransactionParams,
SignTransactionResponse,
SignMessageParams,
SignMessageResponse,
DisplayAddressParams,
DisplayAddressResponse
} from 'shared/enclave/types';
function makeMethod<ParamsType extends EnclaveMethodParams, ResponseType>(method: EnclaveMethods) {
return (params: ParamsType) => makeRequest<ResponseType>(method, params);
}
export class EnclaveAPIClass {
public getChainCode = makeMethod<GetChainCodeParams, GetChainCodeResponse>(
EnclaveMethods.GET_CHAIN_CODE
);
public signTransaction = makeMethod<SignTransactionParams, SignTransactionResponse>(
EnclaveMethods.SIGN_TRANSACTION
);
public signMessage = makeMethod<SignMessageParams, SignMessageResponse>(
EnclaveMethods.SIGN_MESSAGE
);
public displayAddress = makeMethod<DisplayAddressParams, DisplayAddressResponse>(
EnclaveMethods.DISPLAY_ADDRESS
);
}
export default new EnclaveAPIClass();
export * from 'shared/enclave/types';

View File

@ -0,0 +1,17 @@
import { EnclaveMethods, EnclaveMethodParams, EnclaveResponse } from 'shared/enclave/types';
import { PROTOCOL_NAME } from 'shared/enclave/utils';
export function makeRequest<T>(type: EnclaveMethods, params: EnclaveMethodParams): Promise<T> {
return fetch(`${PROTOCOL_NAME}://${type}`, {
method: 'POST',
body: JSON.stringify(params)
})
.then(res => res.json())
.then((res: EnclaveResponse<T>) => {
const { error, data } = res;
if (data) {
return data;
}
throw new Error(error!.message || 'Unknown response from server');
});
}

View File

@ -0,0 +1,7 @@
import { webFrame } from 'electron';
import { PROTOCOL_NAME } from 'shared/enclave/utils';
export function registerProtocol() {
// Whitelist custom protocol
webFrame.registerURLSchemeAsPrivileged(PROTOCOL_NAME);
}

View File

@ -0,0 +1,7 @@
import { getWalletLib } from 'shared/enclave/server/wallets';
import { DisplayAddressParams, DisplayAddressResponse } from 'shared/enclave/types';
export function displayAddress(params: DisplayAddressParams): Promise<DisplayAddressResponse> {
const wallet = getWalletLib(params.walletType);
return wallet.displayAddress(params.path);
}

View File

@ -0,0 +1,7 @@
import { getWalletLib } from 'shared/enclave/server/wallets';
import { GetChainCodeParams, GetChainCodeResponse } from 'shared/enclave/types';
export function getChainCode(params: GetChainCodeParams): Promise<GetChainCodeResponse> {
const wallet = getWalletLib(params.walletType);
return wallet.getChainCode(params.dpath);
}

View File

@ -0,0 +1,20 @@
import { getChainCode } from './getChainCode';
import { signTransaction } from './signTransaction';
import { signMessage } from './signMessage';
import { displayAddress } from './displayAddress';
import { EnclaveMethods, EnclaveMethodParams, EnclaveMethodResponse } from 'shared/enclave/types';
type EnclaveHandlers = {
[key in EnclaveMethods]: (
params: EnclaveMethodParams
) => EnclaveMethodResponse | Promise<EnclaveMethodResponse>
};
const handlers: EnclaveHandlers = {
[EnclaveMethods.GET_CHAIN_CODE]: getChainCode,
[EnclaveMethods.SIGN_TRANSACTION]: signTransaction,
[EnclaveMethods.SIGN_MESSAGE]: signMessage,
[EnclaveMethods.DISPLAY_ADDRESS]: displayAddress
};
export default handlers;

View File

@ -0,0 +1,7 @@
import { getWalletLib } from 'shared/enclave/server/wallets';
import { SignMessageParams, SignMessageResponse } from 'shared/enclave/types';
export function signMessage(params: SignMessageParams): Promise<SignMessageResponse> {
const wallet = getWalletLib(params.walletType);
return wallet.signMessage(params.message, params.path);
}

View File

@ -0,0 +1,7 @@
import { getWalletLib } from 'shared/enclave/server/wallets';
import { SignTransactionParams, SignTransactionResponse } from 'shared/enclave/types';
export function signTransaction(params: SignTransactionParams): Promise<SignTransactionResponse> {
const wallet = getWalletLib(params.walletType);
return wallet.signTransaction(params.transaction, params.path);
}

View File

@ -0,0 +1,68 @@
import { protocol, App } from 'electron';
import handlers from './handlers';
import { PROTOCOL_NAME, isValidEventType } from 'shared/enclave/utils';
import { EnclaveMethods, EnclaveMethodParams, EnclaveResponse } from 'shared/enclave/types';
export function registerServer(app: App) {
// Register protocol scheme
protocol.registerStandardSchemes([PROTOCOL_NAME]);
app.on('ready', () => {
// Register custom protocol behavior
protocol.registerStringProtocol(PROTOCOL_NAME, async (req, cb) => {
let res: EnclaveResponse;
try {
const method = getMethod(req);
const params = getParams(method, req);
const data = await handlers[method](params);
res = { data };
} catch (err) {
console.error(`Request to '${req.url}' failed with error:`, err);
res = {
error: {
code: 500,
type: err.name,
message: err.message
}
};
}
cb(JSON.stringify(res));
});
});
}
function getMethod(req: Electron.RegisterStringProtocolRequest): EnclaveMethods {
const urlSplit = req.url.split(`${PROTOCOL_NAME}://`);
if (!urlSplit[1]) {
throw new Error('No method provided');
}
const method = urlSplit[1].replace('/', '');
if (!isValidEventType(method)) {
throw new Error(`Invalid or unknown method '${method}'`);
}
return method;
}
function getParams(
method: EnclaveMethods,
req: Electron.RegisterStringProtocolRequest
): EnclaveMethodParams {
const data = req.uploadData.find(d => !!d.bytes);
if (!data) {
throw new Error(`No data provided for '${method}'`);
}
try {
// TODO: Validate params based on provided method
const params = JSON.parse(data.bytes.toString());
return params as EnclaveMethodParams;
} catch (err) {
throw new Error(`Invalid JSON blob provided for '${method}': ${err.message}`);
}
}

View File

@ -0,0 +1,14 @@
import Ledger from './ledger';
import Trezor from './trezor';
import KeepKey from './keepkey';
import { WalletTypes, WalletLib } from 'shared/enclave/types';
export const wallets: { [key in WalletTypes]: WalletLib } = {
[WalletTypes.LEDGER]: Ledger,
[WalletTypes.TREZOR]: Trezor,
[WalletTypes.KEEPKEY]: KeepKey
};
export function getWalletLib(type: WalletTypes): WalletLib {
return wallets[type];
}

View File

@ -0,0 +1,21 @@
import { WalletLib } from 'shared/enclave/types';
const KeepKey: WalletLib = {
async getChainCode() {
throw new Error('Not yet implemented');
},
async signTransaction() {
throw new Error('Not yet implemented');
},
async signMessage() {
throw new Error('Not yet implemented');
},
async displayAddress() {
throw new Error('Not yet implemented');
}
};
export default KeepKey;

View File

@ -0,0 +1,84 @@
import EthTx from 'ethereumjs-tx';
import { addHexPrefix, toBuffer } from 'ethereumjs-util';
import { WalletLib } from 'shared/enclave/types';
import LedgerTransport from '@ledgerhq/hw-transport';
import TransportNodeHid from '@ledgerhq/hw-transport-node-hid';
import LedgerEth from '@ledgerhq/hw-app-eth';
let transport: LedgerTransport<string> | null;
async function getEthApp() {
try {
if (!transport) {
transport = await TransportNodeHid.create();
transport.on('disconnect', () => (transport = null));
}
return new LedgerEth(transport);
} catch (err) {
if (err && err.name === 'TransportError') {
throw new Error('ENCLAVE_LEDGER_IN_USE');
}
if (err && err.message && err.message.includes('cannot open device with path')) {
throw new Error('ENCLAVE_LEDGER_IN_USE');
}
throw err;
}
}
const Ledger: WalletLib = {
async getChainCode(dpath) {
const app = await getEthApp();
try {
const res = await app.getAddress(dpath, false, true);
return {
publicKey: res.publicKey,
chainCode: res.chainCode
};
} catch (err) {
console.error('Failed to get chain code from ledger:', err);
throw new Error('ENCLAVE_LEDGER_FAIL');
}
},
async signTransaction(tx, path) {
const app = await getEthApp();
const ethTx = new EthTx({
...tx,
v: Buffer.from([tx.chainId]),
r: toBuffer(0),
s: toBuffer(0)
});
const rsv = await app.signTransaction(path, ethTx.serialize().toString('hex'));
const signedTx = new EthTx({
...tx,
r: addHexPrefix(rsv.r),
s: addHexPrefix(rsv.s),
v: addHexPrefix(rsv.v)
});
return {
signedTransaction: signedTx.serialize().toString('hex')
};
},
async signMessage(msg, path) {
const app = await getEthApp();
const msgHex = Buffer.from(msg).toString('hex');
const signed = await app.signPersonalMessage(path, msgHex);
const combined = addHexPrefix(signed.r + signed.s + signed.v.toString(16));
return {
signedMessage: combined
};
},
async displayAddress(path) {
try {
const app = await getEthApp();
await app.getAddress(path, true, false);
return { success: true };
} catch (err) {
return { success: false };
}
}
};
export default Ledger;

View File

@ -0,0 +1,108 @@
import { DeviceList, Session } from 'mycrypto-trezor.js';
import BN from 'bn.js';
import mapValues from 'lodash/mapValues';
import { addHexPrefix } from 'ethereumjs-util';
import EthTx from 'ethereumjs-tx';
import { stripHexPrefixAndLower, padLeftEven } from 'libs/values';
import { WalletLib } from 'shared/enclave/types';
const deviceList = new DeviceList({ debug: false });
// Keep session in memory so that we're not constantly re-acquiring
// Null it out if session is grabbed somewhere else first
let currentSession: Session | null;
async function getSession() {
if (currentSession) {
return currentSession;
}
const { device, session } = await deviceList.acquireFirstDevice(true);
device.on('disconnect', () => (currentSession = null));
device.on('changedSessions', (_, isUsedHere) => {
if (isUsedHere) {
currentSession = null;
}
});
currentSession = session;
return currentSession;
}
const Trezor: WalletLib = {
async getChainCode(dpath) {
const session = await getSession();
const { message } = await session.getPublicKey(parseHDPath(dpath));
return {
chainCode: message.node.chain_code,
publicKey: message.node.public_key
};
},
async signTransaction(tx, dpath) {
const { chainId, ...strTx } = tx;
const cleanedTx = mapValues(mapValues(strTx, stripHexPrefixAndLower), padLeftEven);
const session = await getSession();
const res = await session.signEthTx(
parseHDPath(dpath),
cleanedTx.nonce,
cleanedTx.gasPrice,
cleanedTx.gasLimit,
cleanedTx.to,
cleanedTx.value,
cleanedTx.data,
chainId
);
const signedTx = new EthTx({
...strTx,
v: addHexPrefix(new BN(res.v).toString(16)),
r: addHexPrefix(res.r.toString()),
s: addHexPrefix(res.s)
});
return {
signedTransaction: signedTx.serialize().toString('hex')
};
},
async signMessage() {
throw new Error('Signing is not supported on TREZOR devices');
},
async displayAddress(path) {
const session = await getSession();
try {
await session.ethereumGetAddress(parseHDPath(path), true);
return { success: true };
} catch (err) {
return { success: false };
}
}
};
// Lifted from https://github.com/trezor/connect/blob/7919d47ca9d483cf303d77907505ccc7d389c68c/popup/src/utils/path.js#L110
// tslint:disable no-bitwise
function parseHDPath(path: string) {
return path
.toLowerCase()
.split('/')
.filter(p => p !== 'm')
.map(p => {
let hardened = false;
let n = parseInt(p, 10);
if (p[p.length - 1] === "'") {
hardened = true;
p = p.substr(0, p.length - 1);
}
if (isNaN(n)) {
throw new Error('Invalid path specified');
}
if (hardened) {
// hardened index
n = (n | 0x80000000) >>> 0;
}
return n;
});
}
export default Trezor;

105
shared/enclave/types.ts Normal file
View File

@ -0,0 +1,105 @@
// Enclave enums
export enum EnclaveMethods {
GET_CHAIN_CODE = 'get-chain-code',
SIGN_TRANSACTION = 'sign-transaction',
SIGN_MESSAGE = 'sign-message',
DISPLAY_ADDRESS = 'display-address'
}
export enum WalletTypes {
LEDGER = 'ledger',
TREZOR = 'trezor',
KEEPKEY = 'keepkey'
}
export interface RawTransaction {
chainId: number;
gasLimit: string;
gasPrice: string;
to: string;
nonce: string;
data: string;
value: string;
}
// Get chain code request
export interface GetChainCodeParams {
walletType: WalletTypes;
dpath: string;
}
export interface GetChainCodeResponse {
publicKey: string;
chainCode: string;
}
// Sign Transaction Request
export interface SignTransactionParams {
walletType: WalletTypes;
transaction: RawTransaction;
path: string;
}
export interface SignTransactionResponse {
signedTransaction: string;
}
// Sign Message Request
export interface SignMessageParams {
walletType: WalletTypes;
message: string;
path: string;
}
export interface SignMessageResponse {
signedMessage: string;
}
// Display Address Request
export interface DisplayAddressParams {
walletType: WalletTypes;
path: string;
}
export interface DisplayAddressResponse {
success: boolean;
}
// All Requests & Responses
export type EnclaveMethodParams =
| GetChainCodeParams
| SignTransactionParams
| SignMessageParams
| DisplayAddressParams;
export type EnclaveMethodResponse =
| GetChainCodeResponse
| SignTransactionResponse
| SignMessageResponse
| DisplayAddressResponse;
// RPC requests, responses & failures
export interface EnclaveSuccessResponse<T = EnclaveMethodResponse> {
data: T;
error?: undefined;
}
export interface EnclaveErrorResponse {
data?: undefined;
error: {
code: number;
type: string;
message: string;
};
}
export type EnclaveResponse<T = EnclaveMethodResponse> =
| EnclaveSuccessResponse<T>
| EnclaveErrorResponse;
// Wallet lib
export interface WalletLib {
getChainCode(dpath: string): Promise<GetChainCodeResponse>;
signTransaction(transaction: RawTransaction, path: string): Promise<SignTransactionResponse>;
signMessage(msg: string, path: string): Promise<SignMessageResponse>;
displayAddress(path: string): Promise<DisplayAddressResponse>;
}

6
shared/enclave/utils.ts Normal file
View File

@ -0,0 +1,6 @@
import { EnclaveMethods } from './types';
export const PROTOCOL_NAME = 'eth-enclave';
const eventTypes = Object.values(EnclaveMethods);
export const isValidEventType = (e: string): e is EnclaveMethods => eventTypes.includes(e);

View File

@ -3,19 +3,20 @@ const webpack = require('webpack');
const path = require('path');
const ClearDistPlugin = require('./plugins/clearDist');
const config = require('./config');
const makeConfig = require('./makeConfig');
const electronConfig = {
target: 'electron-main',
mode: 'development',
entry: {
main: path.join(config.path.electron, 'main/index.ts')
main: path.join(config.path.electron, 'main/index.ts'),
preload: path.join(config.path.electron, 'preload/index.ts')
},
module: {
rules: [config.typescriptRule]
},
resolve: {
extensions: ['.ts', '.js', '.json']
extensions: ['.ts', '.js', '.json'],
modules: config.resolve.modules
},
output: {
filename: '[name].js',
@ -27,10 +28,14 @@ const electronConfig = {
'process.env.NODE_ENV': JSON.stringify('development')
}),
],
externals: {
'node-hid': 'commonjs node-hid'
},
node: {
__dirname: false,
__filename: false
}
},
devtool: 'eval'
};
module.exports = electronConfig;

View File

@ -23,4 +23,6 @@ electronConfig.plugins = [
new DelayPlugin(500)
];
electronConfig.devtool = undefined;
module.exports = [electronConfig, jsConfig];

306
yarn.lock
View File

@ -47,6 +47,32 @@
core-js "^2.5.3"
regenerator-runtime "^0.11.1"
"@ledgerhq/hw-app-eth@4.7.3":
version "4.7.3"
resolved "https://registry.yarnpkg.com/@ledgerhq/hw-app-eth/-/hw-app-eth-4.7.3.tgz#d352e19658ae296532e522c53c8ec2a1a77b64e5"
dependencies:
"@ledgerhq/hw-transport" "^4.7.3"
"@ledgerhq/hw-transport-node-hid@4.7.6":
version "4.7.6"
resolved "https://registry.yarnpkg.com/@ledgerhq/hw-transport-node-hid/-/hw-transport-node-hid-4.7.6.tgz#f2bd7c714e359af84377d07dd6431f2aa582e71e"
dependencies:
"@ledgerhq/hw-transport" "^4.7.3"
node-hid "^0.7.2"
"@ledgerhq/hw-transport-u2f@4.12.0":
version "4.12.0"
resolved "https://registry.yarnpkg.com/@ledgerhq/hw-transport-u2f/-/hw-transport-u2f-4.12.0.tgz#151c7ccaf14627bd26b95a9f4863e4eb76d09865"
dependencies:
"@ledgerhq/hw-transport" "^4.12.0"
u2f-api "0.2.7"
"@ledgerhq/hw-transport@^4.12.0", "@ledgerhq/hw-transport@^4.7.3":
version "4.13.0"
resolved "https://registry.yarnpkg.com/@ledgerhq/hw-transport/-/hw-transport-4.13.0.tgz#463043200c96c91379318052ad4dbe8c80d95cf5"
dependencies:
events "^2.0.0"
"@parity/qr-signer@0.2.1":
version "0.2.1"
resolved "https://registry.yarnpkg.com/@parity/qr-signer/-/qr-signer-0.2.1.tgz#f8b0e0ff5d8ee90b1788951c8524cdf8b1aadf27"
@ -542,6 +568,13 @@ asap@~2.0.3:
version "2.0.6"
resolved "https://registry.yarnpkg.com/asap/-/asap-2.0.6.tgz#e50347611d7e690943208bbdafebcbc2fb866d46"
ascli@~0.3:
version "0.3.0"
resolved "https://registry.yarnpkg.com/ascli/-/ascli-0.3.0.tgz#5e66230e5219fe3e8952a4efb4f20fae596a813a"
dependencies:
colour latest
optjs latest
asn1.js@^4.0.0:
version "4.10.1"
resolved "https://registry.yarnpkg.com/asn1.js/-/asn1.js-4.10.1.tgz#b9c2bf5805f1e64aadeed6df3a2bfafb5a73f5a0"
@ -1317,7 +1350,7 @@ base-x@^1.1.0:
version "1.1.0"
resolved "https://registry.yarnpkg.com/base-x/-/base-x-1.1.0.tgz#42d3d717474f9ea02207f6d1aa1f426913eeb7ac"
base-x@^3.0.4:
base-x@^3.0.2, base-x@^3.0.4:
version "3.0.4"
resolved "https://registry.yarnpkg.com/base-x/-/base-x-3.0.4.tgz#94c1788736da065edb1d68808869e357c977fa77"
dependencies:
@ -1351,20 +1384,39 @@ base@^0.11.1:
mixin-deep "^1.2.0"
pascalcase "^0.1.1"
bchaddrjs@^0.2.1:
version "0.2.1"
resolved "https://registry.yarnpkg.com/bchaddrjs/-/bchaddrjs-0.2.1.tgz#8c2f1582e24e5ca11687ae19e2463543cfa0dd24"
dependencies:
bs58check "^2.1.1"
cashaddrjs "^0.2.7"
bcrypt-pbkdf@^1.0.0:
version "1.0.1"
resolved "https://registry.yarnpkg.com/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.1.tgz#63bc5dcb61331b92bc05fd528953c33462a06f8d"
dependencies:
tweetnacl "^0.14.3"
bech32@^1.1.2:
version "1.1.3"
resolved "https://registry.yarnpkg.com/bech32/-/bech32-1.1.3.tgz#bd47a8986bbb3eec34a56a097a84b8d3e9a2dfcd"
beeper@^1.0.0:
version "1.1.1"
resolved "https://registry.yarnpkg.com/beeper/-/beeper-1.1.1.tgz#e6d5ea8c5dad001304a70b22638447f69cb2f809"
big-integer@^1.6.26:
version "1.6.31"
resolved "https://registry.yarnpkg.com/big-integer/-/big-integer-1.6.31.tgz#6d7852486e67c642502dcc03f7225a245c9fc7fa"
big.js@^3.1.3:
version "3.2.0"
resolved "https://registry.yarnpkg.com/big.js/-/big.js-3.2.0.tgz#a5fc298b81b9e0dca2e458824784b65c52ba588e"
bigi@^1.1.0, bigi@^1.4.0, bigi@^1.4.1:
version "1.4.2"
resolved "https://registry.yarnpkg.com/bigi/-/bigi-1.4.2.tgz#9c665a95f88b8b08fc05cfd731f561859d725825"
bignumber.js@^2.1.0:
version "2.4.0"
resolved "https://registry.yarnpkg.com/bignumber.js/-/bignumber.js-2.4.0.tgz#838a992da9f9d737e0f4b2db0be62bb09dd0c5e8"
@ -1445,12 +1497,36 @@ bip39@2.5.0:
safe-buffer "^5.0.1"
unorm "^1.3.3"
bip66@^1.1.3:
bip66@^1.1.0, bip66@^1.1.3:
version "1.1.5"
resolved "https://registry.yarnpkg.com/bip66/-/bip66-1.1.5.tgz#01fa8748785ca70955d5011217d1b3139969ca22"
dependencies:
safe-buffer "^5.0.1"
bitcoin-ops@^1.3.0:
version "1.4.1"
resolved "https://registry.yarnpkg.com/bitcoin-ops/-/bitcoin-ops-1.4.1.tgz#e45de620398e22fd4ca6023de43974ff42240278"
bitcoinjs-lib-zcash@^3.0.0, bitcoinjs-lib-zcash@^3.3.2:
version "3.4.1"
resolved "https://registry.yarnpkg.com/bitcoinjs-lib-zcash/-/bitcoinjs-lib-zcash-3.4.1.tgz#ea440857188ed3168f66ae8f6ecba603c53e3f22"
dependencies:
bech32 "^1.1.2"
bigi "^1.4.0"
bip66 "^1.1.0"
bitcoin-ops "^1.3.0"
bs58check "^2.0.0"
create-hash "^1.1.0"
create-hmac "^1.1.3"
ecurve "^1.0.0"
merkle-lib "^2.0.10"
pushdata-bitcoin "^1.0.1"
randombytes "^2.0.1"
safe-buffer "^5.0.1"
typeforce "^1.11.3"
varuint-bitcoin "^1.0.4"
wif "^2.0.1"
bl@^1.0.0:
version "1.2.2"
resolved "https://registry.yarnpkg.com/bl/-/bl-1.2.2.tgz#a160911717103c07410cef63ef51b397c025af9c"
@ -1658,6 +1734,19 @@ bs58@^3.1.0:
dependencies:
base-x "^1.1.0"
bs58@^4.0.0:
version "4.0.1"
resolved "https://registry.yarnpkg.com/bs58/-/bs58-4.0.1.tgz#be161e76c354f6f788ae4071f63f34e8c4f0a42a"
dependencies:
base-x "^3.0.2"
bs58check@<3.0.0, bs58check@^2.0.0, bs58check@^2.1.1:
version "2.1.1"
resolved "https://registry.yarnpkg.com/bs58check/-/bs58check-2.1.1.tgz#8a5d0e587af97b784bf9cbf1b29f454d82bc0222"
dependencies:
bs58 "^4.0.0"
create-hash "^1.1.0"
bs58check@^1.0.8:
version "1.3.4"
resolved "https://registry.yarnpkg.com/bs58check/-/bs58check-1.3.4.tgz#c52540073749117714fa042c3047eb8f9151cbf8"
@ -1735,6 +1824,10 @@ buffer@^4.3.0:
ieee754 "^1.1.4"
isarray "^1.0.0"
bufferview@~1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/bufferview/-/bufferview-1.0.1.tgz#7afd74a45f937fa422a1d338c08bbfdc76cd725d"
builder-util-runtime@4.2.1, builder-util-runtime@^4.2.1, builder-util-runtime@~4.2.1:
version "4.2.1"
resolved "https://registry.yarnpkg.com/builder-util-runtime/-/builder-util-runtime-4.2.1.tgz#0caa358f1331d70680010141ca591952b69b35bc"
@ -1790,6 +1883,13 @@ builtin-status-codes@^3.0.0:
version "3.0.0"
resolved "https://registry.yarnpkg.com/builtin-status-codes/-/builtin-status-codes-3.0.0.tgz#85982878e21b98e1c66425e03d0174788f569ee8"
bytebuffer-old-fixed-webpack@3.5.6:
version "3.5.6"
resolved "https://registry.yarnpkg.com/bytebuffer-old-fixed-webpack/-/bytebuffer-old-fixed-webpack-3.5.6.tgz#5adc419c6a9b4692f217206703ec7431c759aa3f"
dependencies:
bufferview "~1"
long "~2 >=2.2.3"
bytes@3.0.0:
version "3.0.0"
resolved "https://registry.yarnpkg.com/bytes/-/bytes-3.0.0.tgz#d32815404d689699f85a4ea4fa8755dd13a96048"
@ -1916,6 +2016,12 @@ caseless@~0.12.0:
version "0.12.0"
resolved "https://registry.yarnpkg.com/caseless/-/caseless-0.12.0.tgz#1b681c21ff84033c826543090689420d187151dc"
cashaddrjs@^0.2.7:
version "0.2.8"
resolved "https://registry.yarnpkg.com/cashaddrjs/-/cashaddrjs-0.2.8.tgz#74003dd4beb55c6328c1a13117d4093f7becb580"
dependencies:
big-integer "^1.6.26"
caw@^1.0.1:
version "1.2.0"
resolved "https://registry.yarnpkg.com/caw/-/caw-1.2.0.tgz#ffb226fe7efc547288dc62ee3e97073c212d1034"
@ -2135,6 +2241,10 @@ cli-spinners@^0.1.2:
version "0.1.2"
resolved "https://registry.yarnpkg.com/cli-spinners/-/cli-spinners-0.1.2.tgz#bb764d88e185fb9e1e6a2a1f19772318f605e31c"
cli-spinners@^1.0.1:
version "1.3.1"
resolved "https://registry.yarnpkg.com/cli-spinners/-/cli-spinners-1.3.1.tgz#002c1990912d0d59580c93bd36c056de99e4259a"
cli-table@^0.3.1:
version "0.3.1"
resolved "https://registry.yarnpkg.com/cli-table/-/cli-table-0.3.1.tgz#f53b05266a8b1a0b934b3d0821e6e2dc5914ae23"
@ -2325,6 +2435,10 @@ colors@~1.1.2:
version "1.1.2"
resolved "https://registry.yarnpkg.com/colors/-/colors-1.1.2.tgz#168a4701756b6a7f51a12ce0c97bfa28c084ed63"
colour@latest:
version "0.7.1"
resolved "https://registry.yarnpkg.com/colour/-/colour-0.7.1.tgz#9cb169917ec5d12c0736d3e8685746df1cadf778"
combined-stream@1.0.6, combined-stream@^1.0.5, combined-stream@~1.0.5:
version "1.0.6"
resolved "https://registry.yarnpkg.com/combined-stream/-/combined-stream-1.0.6.tgz#723e7df6e801ac5613113a7e445a9b69cb632818"
@ -2587,7 +2701,7 @@ create-hash@^1.1.0, create-hash@^1.1.1, create-hash@^1.1.2:
ripemd160 "^2.0.1"
sha.js "^2.4.0"
create-hmac@^1.1.0, create-hmac@^1.1.2, create-hmac@^1.1.4:
create-hmac@^1.1.0, create-hmac@^1.1.2, create-hmac@^1.1.3, create-hmac@^1.1.4:
version "1.1.7"
resolved "https://registry.yarnpkg.com/create-hmac/-/create-hmac-1.1.7.tgz#69170c78b3ab957147b2b8b04572e47ead2243ff"
dependencies:
@ -2878,7 +2992,7 @@ dateformat@^3.0.2:
version "3.0.3"
resolved "https://registry.yarnpkg.com/dateformat/-/dateformat-3.0.3.tgz#a6e37499a4d9a9cf85ef5872044d62901c9889ae"
debug@2.6.9, debug@^2.1.3, debug@^2.2.0, debug@^2.3.3, debug@^2.6.8:
debug@2.6.9, debug@^2.1.3, debug@^2.2.0, debug@^2.3.3, debug@^2.5.1, debug@^2.6.3, debug@^2.6.8:
version "2.6.9"
resolved "https://registry.yarnpkg.com/debug/-/debug-2.6.9.tgz#5d128515df134ff327e90a4c93f4e077a536341f"
dependencies:
@ -3358,6 +3472,13 @@ ecc-jsbn@~0.1.1:
dependencies:
jsbn "~0.1.0"
ecurve@^1.0.0, ecurve@^1.0.2, ecurve@^1.0.3:
version "1.0.6"
resolved "https://registry.yarnpkg.com/ecurve/-/ecurve-1.0.6.tgz#dfdabbb7149f8d8b78816be5a7d5b83fcf6de797"
dependencies:
bigi "^1.1.0"
safe-buffer "^5.0.1"
editions@^1.3.3:
version "1.3.4"
resolved "https://registry.yarnpkg.com/editions/-/editions-1.3.4.tgz#3662cb592347c3168eb8e498a0ff73271d67f50b"
@ -3506,6 +3627,21 @@ electron-publish@20.13.2:
lazy-val "^1.0.3"
mime "^2.3.1"
electron-rebuild@1.7.3:
version "1.7.3"
resolved "https://registry.yarnpkg.com/electron-rebuild/-/electron-rebuild-1.7.3.tgz#24ae06ad9dd61cb7e4d688961f49118c40a110eb"
dependencies:
colors "^1.1.2"
debug "^2.6.3"
detect-libc "^1.0.3"
fs-extra "^3.0.1"
node-abi "^2.0.0"
node-gyp "^3.6.0"
ora "^1.2.0"
rimraf "^2.6.1"
spawn-rx "^2.0.10"
yargs "^7.0.2"
electron-to-chromium@^1.2.7:
version "1.3.42"
resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.3.42.tgz#95c33bf01d0cc405556aec899fe61fd4d76ea0f9"
@ -3844,6 +3980,10 @@ events@^1.0.0:
version "1.1.1"
resolved "https://registry.yarnpkg.com/events/-/events-1.1.1.tgz#9ebdb7635ad099c70dcc4c2a1f5004288e8bd924"
events@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/events/-/events-2.0.0.tgz#cbbb56bf3ab1ac18d71c43bb32c86255062769f2"
evp_bytestokey@^1.0.0, evp_bytestokey@^1.0.3:
version "1.0.3"
resolved "https://registry.yarnpkg.com/evp_bytestokey/-/evp_bytestokey-1.0.3.tgz#7fcbdb198dc71959432efe13842684e0525acb02"
@ -4461,6 +4601,14 @@ fs-extra@^1.0.0:
jsonfile "^2.1.0"
klaw "^1.0.0"
fs-extra@^3.0.1:
version "3.0.1"
resolved "https://registry.yarnpkg.com/fs-extra/-/fs-extra-3.0.1.tgz#3794f378c58b342ea7dbbb23095109c4b3b62291"
dependencies:
graceful-fs "^4.1.2"
jsonfile "^3.0.0"
universalify "^0.1.0"
fs-extra@^6.0.0:
version "6.0.1"
resolved "https://registry.yarnpkg.com/fs-extra/-/fs-extra-6.0.1.tgz#8abc128f7946e310135ddc93b98bddb410e7a34b"
@ -6538,6 +6686,12 @@ jsonfile@^2.1.0:
optionalDependencies:
graceful-fs "^4.1.6"
jsonfile@^3.0.0:
version "3.0.1"
resolved "https://registry.yarnpkg.com/jsonfile/-/jsonfile-3.0.1.tgz#a5ecc6f65f53f662c4415c7675a0331d0992ec66"
optionalDependencies:
graceful-fs "^4.1.6"
jsonfile@^4.0.0:
version "4.0.0"
resolved "https://registry.yarnpkg.com/jsonfile/-/jsonfile-4.0.0.tgz#8771aae0799b64076b76640fca058f9c10e33ecb"
@ -6679,13 +6833,6 @@ lcov-parse@^0.0.10:
version "0.0.10"
resolved "https://registry.yarnpkg.com/lcov-parse/-/lcov-parse-0.0.10.tgz#1b0b8ff9ac9c7889250582b70b71315d9da6d9a3"
ledgerco@1.2.1:
version "1.2.1"
resolved "https://registry.yarnpkg.com/ledgerco/-/ledgerco-1.2.1.tgz#a1fdefd7aaf5a8857bf9b47887f5465bdcdf0602"
dependencies:
node-hid "0.6.0"
u2f-api "0.2.7"
left-pad@^1.2.0:
version "1.3.0"
resolved "https://registry.yarnpkg.com/left-pad/-/left-pad-1.3.0.tgz#5b8a3a7765dfe001261dde915589e782f8c94d1e"
@ -7021,6 +7168,10 @@ loglevelnext@^1.0.1:
version "1.0.4"
resolved "https://registry.yarnpkg.com/loglevelnext/-/loglevelnext-1.0.4.tgz#0d991d9998180991dac8bd81e73a596a8720a645"
"long@~2 >=2.2.3":
version "2.4.0"
resolved "https://registry.yarnpkg.com/long/-/long-2.4.0.tgz#9fa180bb1d9500cdc29c4156766a1995e1f4524f"
longest@^1.0.0, longest@^1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/longest/-/longest-1.0.1.tgz#30a0b2da38f73770e8294a0d22e6625ed77d0097"
@ -7202,6 +7353,10 @@ merge@^1.1.3:
version "1.2.0"
resolved "https://registry.yarnpkg.com/merge/-/merge-1.2.0.tgz#7531e39d4949c281a66b8c5a6e0265e8b05894da"
merkle-lib@^2.0.10:
version "2.0.10"
resolved "https://registry.yarnpkg.com/merkle-lib/-/merkle-lib-2.0.10.tgz#82b8dbae75e27a7785388b73f9d7725d0f6f3326"
methods@~1.1.2:
version "1.1.2"
resolved "https://registry.yarnpkg.com/methods/-/methods-1.1.2.tgz#5529a4d67654134edcc5266656835b0f851afcee"
@ -7454,6 +7609,34 @@ mycrypto-shepherd@1.4.0:
remote-redux-devtools "^0.5.12"
url-search-params "^0.10.0"
mycrypto-trezor-link@1.5.1:
version "1.5.1"
resolved "https://registry.yarnpkg.com/mycrypto-trezor-link/-/mycrypto-trezor-link-1.5.1.tgz#1d0b13bd854022dcd9960062d04f8d9e7b4c67c0"
dependencies:
bigi "^1.4.1"
bitcoinjs-lib-zcash "^3.0.0"
ecurve "^1.0.3"
json-stable-stringify "^1.0.1"
node-fetch "^1.6.0"
object.values "^1.0.3"
protobufjs-old-fixed-webpack "3.8.5"
semver-compare "^1.0.0"
whatwg-fetch "0.11.0"
mycrypto-trezor.js@6.17.5:
version "6.17.5"
resolved "https://registry.yarnpkg.com/mycrypto-trezor.js/-/mycrypto-trezor.js-6.17.5.tgz#f92cf11e614aa813814f8e444f459f02bf7ce906"
dependencies:
bchaddrjs "^0.2.1"
bitcoinjs-lib-zcash "^3.3.2"
ecurve "^1.0.2"
mycrypto-trezor-link "1.5.1"
node-fetch "^1.6.0"
randombytes "^2.0.1"
semver-compare "1.0.0"
unorm "^1.3.3"
whatwg-fetch "0.11.0"
nan@^2.0.5, nan@^2.0.8, nan@^2.10.0, nan@^2.2.1, nan@^2.3.0, nan@^2.6.2:
version "2.10.0"
resolved "https://registry.yarnpkg.com/nan/-/nan-2.10.0.tgz#96d0cd610ebd58d4b4de9cc0c6828cda99c7548f"
@ -7506,6 +7689,12 @@ no-case@^2.2.0:
dependencies:
lower-case "^1.1.1"
node-abi@^2.0.0:
version "2.4.1"
resolved "https://registry.yarnpkg.com/node-abi/-/node-abi-2.4.1.tgz#7628c4d4ec4e9cd3764ceb3652f36b2e7f8d4923"
dependencies:
semver "^5.4.1"
node-abi@^2.2.0:
version "2.3.0"
resolved "https://registry.yarnpkg.com/node-abi/-/node-abi-2.3.0.tgz#f3d554d6ac72a9ee16f0f4dc9548db7c08de4986"
@ -7516,7 +7705,7 @@ node-dir@0.1.8:
version "0.1.8"
resolved "https://registry.yarnpkg.com/node-dir/-/node-dir-0.1.8.tgz#55fb8deb699070707fb67f91a460f0448294c77d"
node-fetch@^1.0.1:
node-fetch@^1.0.1, node-fetch@^1.6.0:
version "1.7.3"
resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-1.7.3.tgz#980f6f72d85211a5347c6b2bc18c5b84c3eb47ef"
dependencies:
@ -7541,9 +7730,26 @@ node-gyp@^3.3.1:
tar "^2.0.0"
which "1"
node-hid@0.6.0:
version "0.6.0"
resolved "https://registry.yarnpkg.com/node-hid/-/node-hid-0.6.0.tgz#336bb29a2c4efdb67c5cb83eedbe4a4b1443ee52"
node-gyp@^3.6.0:
version "3.7.0"
resolved "https://registry.yarnpkg.com/node-gyp/-/node-gyp-3.7.0.tgz#789478e8f6c45e277aa014f3e28f958f286f9203"
dependencies:
fstream "^1.0.0"
glob "^7.0.3"
graceful-fs "^4.1.2"
mkdirp "^0.5.0"
nopt "2 || 3"
npmlog "0 || 1 || 2 || 3 || 4"
osenv "0"
request ">=2.9.0 <2.82.0"
rimraf "2"
semver "~5.3.0"
tar "^2.0.0"
which "1"
node-hid@0.7.2, node-hid@^0.7.2:
version "0.7.2"
resolved "https://registry.yarnpkg.com/node-hid/-/node-hid-0.7.2.tgz#15025cdea2e9756aca2de7266529996d40e52c56"
dependencies:
bindings "^1.3.0"
nan "^2.6.2"
@ -7892,7 +8098,7 @@ object.pick@^1.3.0:
dependencies:
isobject "^3.0.1"
object.values@^1.0.4:
object.values@^1.0.3, object.values@^1.0.4:
version "1.0.4"
resolved "https://registry.yarnpkg.com/object.values/-/object.values-1.0.4.tgz#e524da09b4f66ff05df457546ec72ac99f13069a"
dependencies:
@ -7949,6 +8155,10 @@ optipng-bin@^3.0.0:
bin-wrapper "^3.0.0"
logalot "^2.0.0"
optjs@latest:
version "3.2.2"
resolved "https://registry.yarnpkg.com/optjs/-/optjs-3.2.2.tgz#69a6ce89c442a44403141ad2f9b370bd5bb6f4ee"
ora@^0.2.3:
version "0.2.3"
resolved "https://registry.yarnpkg.com/ora/-/ora-0.2.3.tgz#37527d220adcd53c39b73571d754156d5db657a4"
@ -7958,6 +8168,15 @@ ora@^0.2.3:
cli-spinners "^0.1.2"
object-assign "^4.0.1"
ora@^1.2.0:
version "1.4.0"
resolved "https://registry.yarnpkg.com/ora/-/ora-1.4.0.tgz#884458215b3a5d4097592285f93321bb7a79e2e5"
dependencies:
chalk "^2.1.0"
cli-cursor "^2.1.0"
cli-spinners "^1.0.1"
log-symbols "^2.1.0"
ordered-read-streams@^0.3.0:
version "0.3.0"
resolved "https://registry.yarnpkg.com/ordered-read-streams/-/ordered-read-streams-0.3.0.tgz#7137e69b3298bb342247a1bbee3881c80e2fd78b"
@ -8757,6 +8976,13 @@ proto-list@~1.2.1:
version "1.2.4"
resolved "https://registry.yarnpkg.com/proto-list/-/proto-list-1.2.4.tgz#212d5bfe1318306a420f6402b8e26ff39647a849"
protobufjs-old-fixed-webpack@3.8.5:
version "3.8.5"
resolved "https://registry.yarnpkg.com/protobufjs-old-fixed-webpack/-/protobufjs-old-fixed-webpack-3.8.5.tgz#5813c1af9f1d136bbf39f4f9f2e6f3e43c389d06"
dependencies:
ascli "~0.3"
bytebuffer-old-fixed-webpack "3.5.6"
proxy-addr@~2.0.2:
version "2.0.3"
resolved "https://registry.yarnpkg.com/proxy-addr/-/proxy-addr-2.0.3.tgz#355f262505a621646b3130a728eb647e22055341"
@ -8828,6 +9054,12 @@ punycode@^2.1.0:
version "2.1.0"
resolved "https://registry.yarnpkg.com/punycode/-/punycode-2.1.0.tgz#5f863edc89b96db09074bad7947bf09056ca4e7d"
pushdata-bitcoin@^1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/pushdata-bitcoin/-/pushdata-bitcoin-1.0.1.tgz#15931d3cd967ade52206f523aa7331aef7d43af7"
dependencies:
bitcoin-ops "^1.3.0"
q@^1.1.2:
version "1.5.1"
resolved "https://registry.yarnpkg.com/q/-/q-1.5.1.tgz#7e32f75b41381291d04611f1bf14109ac00651d7"
@ -9616,7 +9848,7 @@ request@2, request@^2.45.0, request@^2.65.0, request@^2.79.0, request@^2.81.0, r
tunnel-agent "^0.6.0"
uuid "^3.1.0"
request@2.81.0:
request@2.81.0, "request@>=2.9.0 <2.82.0":
version "2.81.0"
resolved "https://registry.yarnpkg.com/request/-/request-2.81.0.tgz#c6928946a0e06c5f8d6f8a9333469ffda46298a0"
dependencies:
@ -9824,6 +10056,12 @@ rx@2.3.24:
version "2.3.24"
resolved "https://registry.yarnpkg.com/rx/-/rx-2.3.24.tgz#14f950a4217d7e35daa71bbcbe58eff68ea4b2b7"
rxjs@^5.1.1:
version "5.5.11"
resolved "https://registry.yarnpkg.com/rxjs/-/rxjs-5.5.11.tgz#f733027ca43e3bec6b994473be4ab98ad43ced87"
dependencies:
symbol-observable "1.0.1"
rxjs@^5.4.2, rxjs@^5.5.2:
version "5.5.10"
resolved "https://registry.yarnpkg.com/rxjs/-/rxjs-5.5.10.tgz#fde02d7a614f6c8683d0d1957827f492e09db045"
@ -9979,6 +10217,10 @@ seek-bzip@^1.0.3, seek-bzip@^1.0.5:
dependencies:
commander "~2.8.1"
semver-compare@1.0.0, semver-compare@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/semver-compare/-/semver-compare-1.0.0.tgz#0dee216a1c941ab37e9efb1788f6afc5ff5537fc"
semver-diff@^2.0.0:
version "2.1.0"
resolved "https://registry.yarnpkg.com/semver-diff/-/semver-diff-2.1.0.tgz#4bbb8437c8d37e4b0cf1a68fd726ec6d645d6d36"
@ -10310,6 +10552,14 @@ spawn-command@^0.0.2-1:
version "0.0.2-1"
resolved "https://registry.yarnpkg.com/spawn-command/-/spawn-command-0.0.2-1.tgz#62f5e9466981c1b796dc5929937e11c9c6921bd0"
spawn-rx@^2.0.10:
version "2.0.12"
resolved "https://registry.yarnpkg.com/spawn-rx/-/spawn-rx-2.0.12.tgz#b6285294499426089beea0c3c1ec32d7fc57a376"
dependencies:
debug "^2.5.1"
lodash.assign "^4.2.0"
rxjs "^5.1.1"
spdx-correct@^3.0.0:
version "3.0.0"
resolved "https://registry.yarnpkg.com/spdx-correct/-/spdx-correct-3.0.0.tgz#05a5b4d7153a195bc92c3c425b69f3b2a9524c82"
@ -11170,6 +11420,10 @@ typedarray@^0.0.6:
version "0.0.6"
resolved "https://registry.yarnpkg.com/typedarray/-/typedarray-0.0.6.tgz#867ac74e3864187b1d3d47d996a78ec5c8830777"
typeforce@^1.11.3:
version "1.12.0"
resolved "https://registry.yarnpkg.com/typeforce/-/typeforce-1.12.0.tgz#ca40899919f1466d7819e37be039406beb912a2e"
types-rlp@0.0.1:
version "0.0.1"
resolved "https://registry.yarnpkg.com/types-rlp/-/types-rlp-0.0.1.tgz#cb9a2fe70bbdabda3c2c491b21bc04c8bd917f9b"
@ -11533,6 +11787,12 @@ value-equal@^0.4.0:
version "0.4.0"
resolved "https://registry.yarnpkg.com/value-equal/-/value-equal-0.4.0.tgz#c5bdd2f54ee093c04839d71ce2e4758a6890abc7"
varuint-bitcoin@^1.0.4:
version "1.1.0"
resolved "https://registry.yarnpkg.com/varuint-bitcoin/-/varuint-bitcoin-1.1.0.tgz#7a343f50537607af6a3059312b9782a170894540"
dependencies:
safe-buffer "^5.1.1"
vary@~1.1.2:
version "1.1.2"
resolved "https://registry.yarnpkg.com/vary/-/vary-1.1.2.tgz#2299f02c6ded30d4a5961b0b9f74524a18f634fc"
@ -11843,6 +12103,10 @@ whatwg-encoding@^1.0.1, whatwg-encoding@^1.0.3:
dependencies:
iconv-lite "0.4.19"
whatwg-fetch@0.11.0:
version "0.11.0"
resolved "https://registry.yarnpkg.com/whatwg-fetch/-/whatwg-fetch-0.11.0.tgz#46b1d18d0aa99955971ef1a2f5aac506add28815"
whatwg-fetch@2.0.3:
version "2.0.3"
resolved "https://registry.yarnpkg.com/whatwg-fetch/-/whatwg-fetch-2.0.3.tgz#9c84ec2dcf68187ff00bc64e1274b442176e1c84"
@ -11897,6 +12161,12 @@ widest-line@^2.0.0:
dependencies:
string-width "^2.1.1"
wif@^2.0.1:
version "2.0.6"
resolved "https://registry.yarnpkg.com/wif/-/wif-2.0.6.tgz#08d3f52056c66679299726fade0d432ae74b4704"
dependencies:
bs58check "<3.0.0"
window-or-global@^1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/window-or-global/-/window-or-global-1.0.1.tgz#dbe45ba2a291aabc56d62cf66c45b7fa322946de"
@ -12132,7 +12402,7 @@ yargs@^3.31.0:
window-size "^0.1.4"
y18n "^3.2.0"
yargs@^7.0.0:
yargs@^7.0.0, yargs@^7.0.2:
version "7.1.0"
resolved "https://registry.yarnpkg.com/yargs/-/yargs-7.1.0.tgz#6ba318eb16961727f5d284f8ea003e8d6154d0c8"
dependencies: