diff --git a/.flowconfig b/.flowconfig deleted file mode 100644 index 1a77fd5f..00000000 --- a/.flowconfig +++ /dev/null @@ -1,15 +0,0 @@ -[ignore] - -[include] - -[libs] - -[options] -module.file_ext=.js -module.file_ext=.json -module.file_ext=.jsx -module.file_ext=.scss -module.file_ext=.less -module.system.node.resolve_dirname=node_modules -module.system.node.resolve_dirname=common -module.name_mapper='.*\.(css|less)$' -> 'empty/object' diff --git a/.gitignore b/.gitignore index 48b0195e..a540c898 100644 --- a/.gitignore +++ b/.gitignore @@ -55,3 +55,4 @@ webpack_config/server.csr v8-compile-cache-0/ +package-lock.json diff --git a/.npmrc b/.npmrc new file mode 100644 index 00000000..43c97e71 --- /dev/null +++ b/.npmrc @@ -0,0 +1 @@ +package-lock=false diff --git a/Dockerfile b/Dockerfile deleted file mode 100644 index cff9f393..00000000 --- a/Dockerfile +++ /dev/null @@ -1,8 +0,0 @@ -FROM node:8.1.4 - -WORKDIR /usr/app - -COPY package.json . -RUN npm install --quiet - -COPY . . diff --git a/README.md b/README.md index f454b5a7..d0fe51a7 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,7 @@ # MyEtherWallet V4+ (ALPHA - VISIT [V3](https://github.com/kvhnuke/etherwallet) for the production site) +[![Greenkeeper badge](https://badges.greenkeeper.io/MyEtherWallet/MyEtherWallet.svg)](https://greenkeeper.io/) + #### Run: ```bash @@ -36,7 +38,7 @@ npm run dev:https 2. [dternyak/eth-priv-to-addr](https://hub.docker.com/r/dternyak/eth-priv-to-addr/) pulled from DockerHub ##### Docker setup instructions: -1. Install docker (on macOS, I suggest [Docker for Mac](https://docs.docker.com/docker-for-mac/)) +1. Install docker (on macOS, [Docker for Mac](https://docs.docker.com/docker-for-mac/) is suggested) 2. `docker pull dternyak/eth-priv-to-addr` ##### Run Derivation Checker @@ -48,40 +50,32 @@ npm run derivation-checker ``` │ -├── common - Your App +├── common │ ├── actions - application actions -│ ├── api - Services and XHR utils(also custom form validation, see InputComponent from components/common) +│ ├── api - Services and XHR utils │ ├── components - components according to "Redux philosophy" │ ├── config - frontend config depending on REACT_WEBPACK_ENV │ ├── containers - containers according to "Redux philosophy" │ ├── reducers - application reducers │ ├── routing - application routing -│ ├── index.jsx - entry +│ ├── index.tsx - entry │ ├── index.html ├── static ├── webpack_config - Webpack configuration ├── jest_config - Jest configuration ``` -## Docker setup -You should already have docker and docker-compose setup for your platform as a pre-req. - -```bash -docker-compose up -``` - ## Style Guides and Philosophies The following are guides for developers to follow for writing compliant code. - ### Redux and Actions -Each reducer has one file in `reducers/[namespace].js` that contains the reducer -and initial state, one file in `actions/[namespace].js` that contains the action +Each reducer has one file in `reducers/[namespace].ts` that contains the reducer +and initial state, one file in `actions/[namespace].ts` that contains the action creators and their return types, and optionally one file in -`sagas/[namespace].js` that handles action side effects using +`sagas/[namespace].ts` that handles action side effects using [`redux-saga`](https://github.com/redux-saga/redux-saga). The files should be laid out as follows: @@ -89,75 +83,192 @@ The files should be laid out as follows: #### Reducer * State should be explicitly defined and exported -* Initial state should match state flow typing, define every key -* Reducer function should handle all cases for actions. If state does not change -as a result of an action (Because it merely kicks off side-effects in saga) then -define the case above default, and have it fall through. +* Initial state should match state typing, define every key -```js -// @flow -import type { NamespaceAction } from "actions/namespace"; +```ts +import { NamespaceAction } from "actions/[namespace]"; +import { TypeKeys } from 'actions/[namespace]/constants'; -export type State = { /* Flowtype definition for state object */ }; +export interface State { /* definition for state object */ }; export const INITIAL_STATE: State = { /* Initial state shape */ }; -export function namespace( +export function [namespace]( state: State = INITIAL_STATE, action: NamespaceAction ): State { switch (action.type) { - case 'NAMESPACE_NAME_OF_ACTION': + case TypeKeys.NAMESPACE_NAME_OF_ACTION: return { ...state, // Alterations to state - }; - - case 'NAMESPACE_NAME_OF_SAGA_ACTION': + }; default: - // Ensures every action was handled in reducer - // Unhandled actions should just fall into default - (action: empty); return state; } } ``` #### Actions +* Define each action creator in `actionCreator.ts` +* Define each action object type in `actionTypes.ts` + * Export a union of all of the action types for use by the reducer +* Define each action type as a string enum in `constants.ts` +* Export `actionCreators` and `actionTypes` from module file `index.ts` -* Define each action object type beside the action creator -* Export a union of all of the action types for use by the reducer - -```js +``` +├── common + ├── actions - application actions + ├── [namespace] - action namespace + ├── actionCreators.ts - action creators + ├── actionTypes.ts - action interfaces / types + ├── constants.ts - string enum + ├── index.ts - exports all action creators and action object types +``` +##### constants.ts +```ts +export enum TypeKeys { + NAMESPACE_NAME_OF_ACTION = 'NAMESPACE_NAME_OF_ACTION' +} +``` +##### actionTypes.ts +```ts /*** Name of action ***/ -export type NameOfActionAction = { - type: 'NAMESPACE_NAME_OF_ACTION', +export interface NameOfActionAction { + type: TypeKeys.NAMESPACE_NAME_OF_ACTION, /* Rest of the action object shape */ }; -export function nameOfAction(): NameOfActionAction { - return { - type: 'NAMESPACE_NAME_OF_ACTION', - /* Rest of the action object */ - }; -}; - /*** Action Union ***/ export type NamespaceAction = | ActionOneAction | ActionTwoAction | ActionThreeAction; ``` +##### actionCreators.ts +```ts +import * as interfaces from './actionTypes'; +import { TypeKeys } from './constants'; -#### Action Constants +export interface TNameOfAction = typeof nameOfAction; +export function nameOfAction(): interfaces.NameOfActionAction { + return { + type: TypeKeys.NAMESPACE_NAME_OF_ACTION, + payload: {} + }; +}; +``` +##### index.ts +```ts +export * from './actionCreators'; +export * from './actionTypes'; +``` -Action constants are not used thanks to flow type checking. To avoid typos, we -use `(action: empty)` in the default case which assures every case is accounted -for. If you need to use another reducer's action, import that action type into -your reducer, and create a new action union of your actions, and the other -action types used. +### Typing Redux-Connected Components +Components that receive props directly from redux as a result of the `connect` +function should use AppState for typing, rather than manually defining types. +This makes refactoring reducers easier by catching mismatches or changes of +types in components, and reduces the chance for inconsistency. It's also less +code overall. +``` +// Do this +import { AppState } from 'reducers'; +interface Props { + wallet: AppState['wallet']['inst']; + rates: AppState['rates']['rates']; + // ... +} + +// Not this +import { IWallet } from 'libs/wallet'; +import { Rates } from 'libs/rates'; + +interface Props { + wallet: IWallet; + rates: Rates; + // ... +} +``` + +However, if you have a sub-component that takes in props from a connected +component, it's OK to manually specify the type. Especially if you go from +being type-or-null to guaranteeing the prop will be passed (because of a +conditional render.) + +### Higher Order Components + +#### Typing Injected Props +Props made available through higher order components can be tricky to type. Normally, if a component requires a prop, you add it to the component's interface and it just works. However, working with injected props from [higher order components](https://medium.com/@DanHomola/react-higher-order-components-in-typescript-made-simple-6f9b55691af1), you will be forced to supply all required props whenever you compose the component. + +```ts +interface MyComponentProps { + name: string; + countryCode?: string; + routerLocation: { pathname: string }; +} + +... + +class OtherComponent extends React.Component<{}, {}> { + render() { + return ( + + ); + } +``` + +Instead of tacking the injected props on the MyComponentProps interface, put them in another interface called `InjectedProps`: + +```ts +interface MyComponentProps { + name: string; + countryCode?: string; +} + +interface InjectedProps { + routerLocation: { pathname: string }; +} +``` + +Now add a [getter](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Functions/get) to cast `this.props` as the original props - `MyComponentProps` and the injected props - `InjectedProps`: + +```ts +class MyComponent extends React.Component { + get injected() { + return this.props as MyComponentProps & InjectedProps; + } + + render() { + const { name, countryCode, routerLocation } = this.props; + ... + } +} +``` + +## Event Handlers + +Event handlers such as `onChange` and `onClick`, should be properly typed. For example, if you have an event listener on an input element inside a form: +```ts +public onValueChange = (e: React.FormEvent) => { + if (this.props.onChange) { + this.props.onChange( + e.currentTarget.value, + this.props.unit + ); + } + }; +``` +Where you type the event as a `React.FormEvent` of type `HTMLElement`. + +## Class names + +Dynamic class names should use the `classnames` module to simplify how they are created instead of using string template literals with expressions inside. ### Styling @@ -165,12 +276,12 @@ Legacy styles are housed under `common/assets/styles` and written with LESS. However, going forward, each styled component should create a a `.scss` file of the same name in the same folder, and import it like so: -```js +```ts import React from "react"; import "./MyComponent.scss"; -export default class MyComponent extends React.component { +export default class MyComponent extends React.component<{}, {}> { render() { return (
diff --git a/common/Root.tsx b/common/Root.tsx new file mode 100644 index 00000000..c0c139c8 --- /dev/null +++ b/common/Root.tsx @@ -0,0 +1,87 @@ +import React, { Component } from 'react'; +import { Provider } from 'react-redux'; +import { withRouter, Switch, Redirect, Router, Route } from 'react-router-dom'; +// Components +import Contracts from 'containers/Tabs/Contracts'; +import ENS from 'containers/Tabs/ENS'; +import GenerateWallet from 'containers/Tabs/GenerateWallet'; +import Help from 'containers/Tabs/Help'; +import SendTransaction from 'containers/Tabs/SendTransaction'; +import Swap from 'containers/Tabs/Swap'; +import ViewWallet from 'containers/Tabs/ViewWallet'; +import SignAndVerifyMessage from 'containers/Tabs/SignAndVerifyMessage'; +import BroadcastTx from 'containers/Tabs/BroadcastTx'; + +// TODO: fix this +interface Props { + store: any; + history: any; +} + +export default class Root extends Component { + public render() { + const { store, history } = this.props; + // key={Math.random()} = hack for HMR from https://github.com/webpack/webpack-dev-server/issues/395 + return ( + + +
+ + + + + + + + + + + +
+
+
+ ); + } +} + +const LegacyRoutes = withRouter(props => { + const { history } = props; + const { pathname, hash } = props.location; + + if (pathname === '/') { + switch (hash) { + case '#send-transaction': + case '#offline-transaction': + history.push('/send-transaction'); + break; + case '#generate-wallet': + history.push('/'); + break; + case '#swap': + history.push('/swap'); + break; + case '#contracts': + history.push('/contracts'); + break; + case '#ens': + history.push('/ens'); + break; + case '#view-wallet-info': + history.push('/view-wallet'); + break; + case '#check-tx-status': + history.push('/check-tx-status'); + break; + } + } + + return ( + + + + + ); +}); diff --git a/common/actions/config/actionCreators.ts b/common/actions/config/actionCreators.ts index e40e800f..8b189127 100644 --- a/common/actions/config/actionCreators.ts +++ b/common/actions/config/actionCreators.ts @@ -1,5 +1,6 @@ import * as interfaces from './actionTypes'; import { TypeKeys } from './constants'; +import { NodeConfig, CustomNodeConfig } from 'config/data'; export type TForceOfflineConfig = typeof forceOfflineConfig; export function forceOfflineConfig(): interfaces.ForceOfflineAction { @@ -24,10 +25,13 @@ export function changeLanguage(sign: string): interfaces.ChangeLanguageAction { } export type TChangeNode = typeof changeNode; -export function changeNode(value: string): interfaces.ChangeNodeAction { +export function changeNode( + nodeSelection: string, + node: NodeConfig +): interfaces.ChangeNodeAction { return { type: TypeKeys.CONFIG_NODE_CHANGE, - payload: value + payload: { nodeSelection, node } }; } @@ -55,3 +59,40 @@ export function changeNodeIntent( payload }; } + +export type TAddCustomNode = typeof addCustomNode; +export function addCustomNode( + payload: CustomNodeConfig +): interfaces.AddCustomNodeAction { + return { + type: TypeKeys.CONFIG_ADD_CUSTOM_NODE, + payload + }; +} + +export type TRemoveCustomNode = typeof removeCustomNode; +export function removeCustomNode( + payload: CustomNodeConfig +): interfaces.RemoveCustomNodeAction { + return { + type: TypeKeys.CONFIG_REMOVE_CUSTOM_NODE, + payload + }; +} + +export type TSetLatestBlock = typeof setLatestBlock; +export function setLatestBlock( + payload: string +): interfaces.SetLatestBlockAction { + return { + type: TypeKeys.CONFIG_SET_LATEST_BLOCK, + payload + }; +} + +export type TWeb3UnsetNode = typeof web3UnsetNode; +export function web3UnsetNode(): interfaces.Web3UnsetNodeAction { + return { + type: TypeKeys.CONFIG_NODE_WEB3_UNSET + }; +} diff --git a/common/actions/config/actionTypes.ts b/common/actions/config/actionTypes.ts index 010d4608..e8b27135 100644 --- a/common/actions/config/actionTypes.ts +++ b/common/actions/config/actionTypes.ts @@ -1,4 +1,5 @@ import { TypeKeys } from './constants'; +import { CustomNodeConfig, NodeConfig } from 'config/data'; /*** Toggle Offline ***/ export interface ToggleOfflineAction { @@ -20,7 +21,10 @@ export interface ChangeLanguageAction { export interface ChangeNodeAction { type: TypeKeys.CONFIG_NODE_CHANGE; // FIXME $keyof? - payload: string; + payload: { + nodeSelection: string; + node: NodeConfig; + }; } /*** Change gas price ***/ @@ -40,6 +44,29 @@ export interface ChangeNodeIntentAction { payload: string; } +/*** Add Custom Node ***/ +export interface AddCustomNodeAction { + type: TypeKeys.CONFIG_ADD_CUSTOM_NODE; + payload: CustomNodeConfig; +} + +/*** Remove Custom Node ***/ +export interface RemoveCustomNodeAction { + type: TypeKeys.CONFIG_REMOVE_CUSTOM_NODE; + payload: CustomNodeConfig; +} + +/*** Set Latest Block ***/ +export interface SetLatestBlockAction { + type: TypeKeys.CONFIG_SET_LATEST_BLOCK; + payload: string; +} + +/*** Unset Web3 as a Node ***/ +export interface Web3UnsetNodeAction { + type: TypeKeys.CONFIG_NODE_WEB3_UNSET; +} + /*** Union Type ***/ export type ConfigAction = | ChangeNodeAction @@ -48,4 +75,8 @@ export type ConfigAction = | ToggleOfflineAction | PollOfflineStatus | ForceOfflineAction - | ChangeNodeIntentAction; + | ChangeNodeIntentAction + | AddCustomNodeAction + | RemoveCustomNodeAction + | SetLatestBlockAction + | Web3UnsetNodeAction; diff --git a/common/actions/config/constants.ts b/common/actions/config/constants.ts index 55e3d43d..d11471ac 100644 --- a/common/actions/config/constants.ts +++ b/common/actions/config/constants.ts @@ -5,5 +5,9 @@ export enum TypeKeys { CONFIG_GAS_PRICE = 'CONFIG_GAS_PRICE', CONFIG_TOGGLE_OFFLINE = 'CONFIG_TOGGLE_OFFLINE', CONFIG_FORCE_OFFLINE = 'CONFIG_FORCE_OFFLINE', - CONFIG_POLL_OFFLINE_STATUS = 'CONFIG_POLL_OFFLINE_STATUS' + CONFIG_POLL_OFFLINE_STATUS = 'CONFIG_POLL_OFFLINE_STATUS', + CONFIG_ADD_CUSTOM_NODE = 'CONFIG_ADD_CUSTOM_NODE', + CONFIG_REMOVE_CUSTOM_NODE = 'CONFIG_REMOVE_CUSTOM_NODE', + CONFIG_SET_LATEST_BLOCK = 'CONFIG_SET_LATEST_BLOCK', + CONFIG_NODE_WEB3_UNSET = 'CONFIG_NODE_WEB3_UNSET' } diff --git a/common/actions/contracts/actionCreators.ts b/common/actions/contracts/actionCreators.ts deleted file mode 100644 index 3ef77e59..00000000 --- a/common/actions/contracts/actionCreators.ts +++ /dev/null @@ -1,22 +0,0 @@ -import * as interfaces from './actionTypes'; -import { TypeKeys } from './constants'; - -export function accessContract( - address: string, - abiJson: string -): interfaces.AccessContractAction { - return { - type: TypeKeys.ACCESS_CONTRACT, - address, - abiJson - }; -} - -export function setInteractiveContract( - functions: interfaces.ABIFunction[] -): interfaces.SetInteractiveContractAction { - return { - type: TypeKeys.SET_INTERACTIVE_CONTRACT, - functions - }; -} diff --git a/common/actions/contracts/actionTypes.ts b/common/actions/contracts/actionTypes.ts deleted file mode 100644 index 7ae50608..00000000 --- a/common/actions/contracts/actionTypes.ts +++ /dev/null @@ -1,31 +0,0 @@ -import { TypeKeys } from './constants'; -/***** Set Interactive Contract *****/ -export interface ABIFunctionField { - name: string; - type: string; -} - -export interface ABIFunction { - name: string; - type: string; - constant: boolean; - inputs: ABIFunctionField[]; - outputs: ABIFunctionField[]; -} - -export interface SetInteractiveContractAction { - type: TypeKeys.SET_INTERACTIVE_CONTRACT; - functions: ABIFunction[]; -} - -/***** Access Contract *****/ -export interface AccessContractAction { - type: TypeKeys.ACCESS_CONTRACT; - address: string; - abiJson: string; -} - -/*** Union Type ***/ -export type ContractsAction = - | SetInteractiveContractAction - | AccessContractAction; diff --git a/common/actions/contracts/constants.ts b/common/actions/contracts/constants.ts deleted file mode 100644 index 3a23e1ef..00000000 --- a/common/actions/contracts/constants.ts +++ /dev/null @@ -1,4 +0,0 @@ -export enum TypeKeys { - ACCESS_CONTRACT = 'ACCESS_CONTRACT', - SET_INTERACTIVE_CONTRACT = 'SET_INTERACTIVE_CONTRACT' -} diff --git a/common/actions/contracts/index.ts b/common/actions/contracts/index.ts deleted file mode 100644 index fee14683..00000000 --- a/common/actions/contracts/index.ts +++ /dev/null @@ -1,3 +0,0 @@ -export * from './constants'; -export * from './actionTypes'; -export * from './actionCreators'; diff --git a/common/actions/deterministicWallets/actionTypes.ts b/common/actions/deterministicWallets/actionTypes.ts index bc601b50..bf20e9fd 100644 --- a/common/actions/deterministicWallets/actionTypes.ts +++ b/common/actions/deterministicWallets/actionTypes.ts @@ -1,14 +1,19 @@ -import { BigNumber } from 'bignumber.js'; +import { TokenValue, Wei } from 'libs/units'; -export interface TokenValues { - [key: string]: BigNumber; +export interface ITokenData { + value: TokenValue; + decimal: number; +} + +export interface ITokenValues { + [key: string]: ITokenData | null; } export interface DeterministicWalletData { index: number; address: string; - value?: BigNumber; - tokenValues: TokenValues; + value?: TokenValue; + tokenValues: ITokenValues; } /*** Get determinstic wallets ***/ @@ -39,8 +44,8 @@ export interface SetDesiredTokenAction { /*** Set wallet values ***/ export interface UpdateDeterministicWalletArgs { address: string; - value?: BigNumber; - tokenValues?: TokenValues; + value?: Wei; + tokenValues?: ITokenValues; index?: any; } diff --git a/common/actions/ens/actionTypes.ts b/common/actions/ens/actionTypes.ts index 6489772a..d87a9445 100644 --- a/common/actions/ens/actionTypes.ts +++ b/common/actions/ens/actionTypes.ts @@ -1,5 +1,3 @@ -import * as constants from './constants'; - /*** Resolve ENS name ***/ export interface ResolveEnsNameAction { type: 'ENS_RESOLVE'; diff --git a/common/actions/generateWallet/actionCreators.ts b/common/actions/generateWallet/actionCreators.ts index 896d6ffc..2ae69f21 100644 --- a/common/actions/generateWallet/actionCreators.ts +++ b/common/actions/generateWallet/actionCreators.ts @@ -1,4 +1,4 @@ -import { PrivKeyWallet } from 'libs/wallet'; +import { generate } from 'ethereumjs-wallet'; import * as interfaces from './actionTypes'; import { TypeKeys } from './constants'; @@ -8,7 +8,7 @@ export function generateNewWallet( ): interfaces.GenerateNewWalletAction { return { type: TypeKeys.GENERATE_WALLET_GENERATE_WALLET, - wallet: PrivKeyWallet.generate(), + wallet: generate(), password }; } diff --git a/common/actions/generateWallet/actionTypes.ts b/common/actions/generateWallet/actionTypes.ts index 9bbecb70..bfb811fd 100644 --- a/common/actions/generateWallet/actionTypes.ts +++ b/common/actions/generateWallet/actionTypes.ts @@ -1,10 +1,10 @@ -import { PrivKeyWallet } from 'libs/wallet'; +import { IFullWallet } from 'ethereumjs-wallet'; import { TypeKeys } from './constants'; /*** Generate Wallet File ***/ export interface GenerateNewWalletAction { type: TypeKeys.GENERATE_WALLET_GENERATE_WALLET; - wallet: PrivKeyWallet; + wallet: IFullWallet; password: string; } diff --git a/common/actions/notifications/actionCreators.ts b/common/actions/notifications/actionCreators.ts index c439c65d..75c0a735 100644 --- a/common/actions/notifications/actionCreators.ts +++ b/common/actions/notifications/actionCreators.ts @@ -13,7 +13,8 @@ export function showNotification( payload: { level, msg, - duration + duration, + id: Math.random() } }; } diff --git a/common/actions/notifications/actionTypes.ts b/common/actions/notifications/actionTypes.ts index 939cc1af..16de73a2 100644 --- a/common/actions/notifications/actionTypes.ts +++ b/common/actions/notifications/actionTypes.ts @@ -7,6 +7,7 @@ export type INFINITY = 'infinity'; export interface Notification { level: NOTIFICATION_LEVEL; msg: ReactElement | string; + id: number; duration?: number | INFINITY; } diff --git a/common/actions/rates/actionCreators.ts b/common/actions/rates/actionCreators.ts index 91ce5118..ef5705c1 100644 --- a/common/actions/rates/actionCreators.ts +++ b/common/actions/rates/actionCreators.ts @@ -3,9 +3,26 @@ import { TypeKeys } from './constants'; import { fetchRates, CCResponse } from './actionPayloads'; export type TFetchCCRates = typeof fetchCCRates; -export function fetchCCRates(): interfaces.FetchCCRates { +export function fetchCCRates(symbols: string[] = []): interfaces.FetchCCRates { return { type: TypeKeys.RATES_FETCH_CC, - payload: fetchRates() + payload: fetchRates(symbols) + }; +} + +export type TFetchCCRatesSucceeded = typeof fetchCCRatesSucceeded; +export function fetchCCRatesSucceeded( + payload: CCResponse +): interfaces.FetchCCRatesSucceeded { + return { + type: TypeKeys.RATES_FETCH_CC_SUCCEEDED, + payload + }; +} + +export type TFetchCCRatesFailed = typeof fetchCCRatesFailed; +export function fetchCCRatesFailed(): interfaces.FetchCCRatesFailed { + return { + type: TypeKeys.RATES_FETCH_CC_FAILED }; } diff --git a/common/actions/rates/actionPayloads.ts b/common/actions/rates/actionPayloads.ts index 715db807..9256049a 100644 --- a/common/actions/rates/actionPayloads.ts +++ b/common/actions/rates/actionPayloads.ts @@ -1,22 +1,52 @@ import { handleJSONResponse } from 'api/utils'; -export const symbols = ['USD', 'EUR', 'GBP', 'BTC', 'CHF', 'REP']; -const symbolsURL = symbols.join(','); +export const rateSymbols = ['USD', 'EUR', 'GBP', 'BTC', 'CHF', 'REP', 'ETH']; // TODO - internationalize const ERROR_MESSAGE = 'Could not fetch rate data.'; const CCApi = 'https://min-api.cryptocompare.com'; -const CCRates = CCSymbols => `${CCApi}/data/price?fsym=ETH&tsyms=${CCSymbols}`; +const CCRates = (symbols: string[]) => { + const tsyms = rateSymbols.concat(symbols).join(','); + return `${CCApi}/data/price?fsym=ETH&tsyms=${tsyms}`; +}; export interface CCResponse { - BTC: number; - EUR: number; - GBP: number; - CHF: number; - REP: number; + [symbol: string]: { + USD: number; + EUR: number; + GBP: number; + BTC: number; + CHF: number; + REP: number; + ETH: number; + }; } -export const fetchRates = (): Promise => - fetch(CCRates(symbolsURL)).then(response => - handleJSONResponse(response, ERROR_MESSAGE) - ); +export const fetchRates = (symbols: string[] = []): Promise => + fetch(CCRates(symbols)) + .then(response => handleJSONResponse(response, ERROR_MESSAGE)) + .then(rates => { + // All currencies are in ETH right now. We'll do token -> eth -> value to + // do it all in one request + // to their respective rates via ETH. + return symbols.reduce( + (eqRates, sym) => { + eqRates[sym] = rateSymbols.reduce((symRates, rateSym) => { + symRates[rateSym] = 1 / rates[sym] * rates[rateSym]; + return symRates; + }, {}); + return eqRates; + }, + { + ETH: { + USD: rates.USD, + EUR: rates.EUR, + GBP: rates.GBP, + BTC: rates.BTC, + CHF: rates.CHF, + REP: rates.REP, + ETH: 1 + } + } + ); + }); diff --git a/common/actions/rates/actionTypes.ts b/common/actions/rates/actionTypes.ts index 53810686..0609c318 100644 --- a/common/actions/rates/actionTypes.ts +++ b/common/actions/rates/actionTypes.ts @@ -18,6 +18,6 @@ export interface FetchCCRatesFailed { /*** Union Type ***/ export type RatesAction = - | FetchCCRatesSucceeded | FetchCCRates + | FetchCCRatesSucceeded | FetchCCRatesFailed; diff --git a/common/actions/wallet/actionCreators.ts b/common/actions/wallet/actionCreators.ts index 21a423e1..8acf32dc 100644 --- a/common/actions/wallet/actionCreators.ts +++ b/common/actions/wallet/actionCreators.ts @@ -1,15 +1,13 @@ -import { BigNumber } from 'bignumber.js'; -import { Wei } from 'libs/units'; +import { Wei, TokenValue } from 'libs/units'; import { IWallet } from 'libs/wallet/IWallet'; import * as types from './actionTypes'; -import * as constants from './constants'; - +import { TypeKeys } from './constants'; export type TUnlockPrivateKey = typeof unlockPrivateKey; export function unlockPrivateKey( value: types.PrivateKeyUnlockParams ): types.UnlockPrivateKeyAction { return { - type: constants.WALLET_UNLOCK_PRIVATE_KEY, + type: TypeKeys.WALLET_UNLOCK_PRIVATE_KEY, payload: value }; } @@ -19,7 +17,7 @@ export function unlockKeystore( value: types.KeystoreUnlockParams ): types.UnlockKeystoreAction { return { - type: constants.WALLET_UNLOCK_KEYSTORE, + type: TypeKeys.WALLET_UNLOCK_KEYSTORE, payload: value }; } @@ -29,33 +27,54 @@ export function unlockMnemonic( value: types.MnemonicUnlockParams ): types.UnlockMnemonicAction { return { - type: constants.WALLET_UNLOCK_MNEMONIC, + type: TypeKeys.WALLET_UNLOCK_MNEMONIC, payload: value }; } +export type TUnlockWeb3 = typeof unlockWeb3; +export function unlockWeb3(): types.UnlockWeb3Action { + return { + type: TypeKeys.WALLET_UNLOCK_WEB3 + }; +} + export type TSetWallet = typeof setWallet; export function setWallet(value: IWallet): types.SetWalletAction { return { - type: constants.WALLET_SET, + type: TypeKeys.WALLET_SET, payload: value }; } -export type TSetBalance = typeof setBalance; -export function setBalance(value: Wei): types.SetBalanceAction { +export function setBalancePending(): types.SetBalancePendingAction { return { - type: constants.WALLET_SET_BALANCE, + type: TypeKeys.WALLET_SET_BALANCE_PENDING + }; +} + +export type TSetBalance = typeof setBalanceFullfilled; +export function setBalanceFullfilled( + value: Wei +): types.SetBalanceFullfilledAction { + return { + type: TypeKeys.WALLET_SET_BALANCE_FULFILLED, payload: value }; } +export function setBalanceRejected(): types.SetBalanceRejectedAction { + return { + type: TypeKeys.WALLET_SET_BALANCE_REJECTED + }; +} + export type TSetTokenBalances = typeof setTokenBalances; export function setTokenBalances(payload: { - [key: string]: BigNumber; + [key: string]: TokenValue; }): types.SetTokenBalancesAction { return { - type: constants.WALLET_SET_TOKEN_BALANCES, + type: TypeKeys.WALLET_SET_TOKEN_BALANCES, payload }; } @@ -65,7 +84,7 @@ export function broadcastTx( signedTx: string ): types.BroadcastTxRequestedAction { return { - type: constants.WALLET_BROADCAST_TX_REQUESTED, + type: TypeKeys.WALLET_BROADCAST_TX_REQUESTED, payload: { signedTx } @@ -78,7 +97,7 @@ export function broadcastTxSucceded( signedTx: string ): types.BroadcastTxSuccededAction { return { - type: constants.WALLET_BROADCAST_TX_SUCCEEDED, + type: TypeKeys.WALLET_BROADCAST_TX_SUCCEEDED, payload: { txHash, signedTx @@ -92,7 +111,7 @@ export function broadCastTxFailed( errorMsg: string ): types.BroadcastTxFailedAction { return { - type: constants.WALLET_BROADCAST_TX_FAILED, + type: TypeKeys.WALLET_BROADCAST_TX_FAILED, payload: { signedTx, error: errorMsg @@ -101,8 +120,8 @@ export function broadCastTxFailed( } export type TResetWallet = typeof resetWallet; -export function resetWallet() { +export function resetWallet(): types.ResetWalletAction { return { - type: constants.WALLET_RESET + type: TypeKeys.WALLET_RESET }; } diff --git a/common/actions/wallet/actionTypes.ts b/common/actions/wallet/actionTypes.ts index aec7819b..1eb64b50 100644 --- a/common/actions/wallet/actionTypes.ts +++ b/common/actions/wallet/actionTypes.ts @@ -1,6 +1,6 @@ -import { BigNumber } from 'bignumber.js'; -import { Wei } from 'libs/units'; +import { Wei, TokenValue } from 'libs/units'; import { IWallet } from 'libs/wallet/IWallet'; +import { TypeKeys } from './constants'; /*** Unlock Private Key ***/ export interface PrivateKeyUnlockParams { @@ -9,42 +9,52 @@ export interface PrivateKeyUnlockParams { } export interface UnlockPrivateKeyAction { - type: 'WALLET_UNLOCK_PRIVATE_KEY'; + type: TypeKeys.WALLET_UNLOCK_PRIVATE_KEY; payload: PrivateKeyUnlockParams; } export interface UnlockMnemonicAction { - type: 'WALLET_UNLOCK_MNEMONIC'; + type: TypeKeys.WALLET_UNLOCK_MNEMONIC; payload: MnemonicUnlockParams; } +export interface UnlockWeb3Action { + type: TypeKeys.WALLET_UNLOCK_WEB3; +} + /*** Set Wallet ***/ export interface SetWalletAction { - type: 'WALLET_SET'; + type: TypeKeys.WALLET_SET; payload: IWallet; } /*** Reset Wallet ***/ export interface ResetWalletAction { - type: 'WALLET_RESET'; + type: TypeKeys.WALLET_RESET; } /*** Set Balance ***/ -export interface SetBalanceAction { - type: 'WALLET_SET_BALANCE'; +export interface SetBalancePendingAction { + type: TypeKeys.WALLET_SET_BALANCE_PENDING; +} +export interface SetBalanceFullfilledAction { + type: TypeKeys.WALLET_SET_BALANCE_FULFILLED; payload: Wei; } +export interface SetBalanceRejectedAction { + type: TypeKeys.WALLET_SET_BALANCE_REJECTED; +} /*** Set Token Balance ***/ export interface SetTokenBalancesAction { - type: 'WALLET_SET_TOKEN_BALANCES'; + type: TypeKeys.WALLET_SET_TOKEN_BALANCES; payload: { - [key: string]: BigNumber; + [key: string]: TokenValue; }; } /*** Broadcast Tx ***/ export interface BroadcastTxRequestedAction { - type: 'WALLET_BROADCAST_TX_REQUESTED'; + type: TypeKeys.WALLET_BROADCAST_TX_REQUESTED; payload: { signedTx: string; }; @@ -65,12 +75,12 @@ export interface KeystoreUnlockParams { } export interface UnlockKeystoreAction { - type: 'WALLET_UNLOCK_KEYSTORE'; + type: TypeKeys.WALLET_UNLOCK_KEYSTORE; payload: KeystoreUnlockParams; } export interface BroadcastTxSuccededAction { - type: 'WALLET_BROADCAST_TX_SUCCEEDED'; + type: TypeKeys.WALLET_BROADCAST_TX_SUCCEEDED; payload: { txHash: string; signedTx: string; @@ -78,7 +88,7 @@ export interface BroadcastTxSuccededAction { } export interface BroadcastTxFailedAction { - type: 'WALLET_BROADCAST_TX_FAILED'; + type: TypeKeys.WALLET_BROADCAST_TX_FAILED; payload: { signedTx: string; error: string; @@ -90,7 +100,9 @@ export type WalletAction = | UnlockPrivateKeyAction | SetWalletAction | ResetWalletAction - | SetBalanceAction + | SetBalancePendingAction + | SetBalanceFullfilledAction + | SetBalanceRejectedAction | SetTokenBalancesAction | BroadcastTxRequestedAction | BroadcastTxFailedAction diff --git a/common/actions/wallet/constants.ts b/common/actions/wallet/constants.ts index 2178f68a..8ccb8e5d 100644 --- a/common/actions/wallet/constants.ts +++ b/common/actions/wallet/constants.ts @@ -1,10 +1,15 @@ -export const WALLET_UNLOCK_PRIVATE_KEY = 'WALLET_UNLOCK_PRIVATE_KEY'; -export const WALLET_UNLOCK_KEYSTORE = 'WALLET_UNLOCK_KEYSTORE'; -export const WALLET_UNLOCK_MNEMONIC = 'WALLET_UNLOCK_MNEMONIC'; -export const WALLET_SET = 'WALLET_SET'; -export const WALLET_SET_BALANCE = 'WALLET_SET_BALANCE'; -export const WALLET_SET_TOKEN_BALANCES = 'WALLET_SET_TOKEN_BALANCES'; -export const WALLET_BROADCAST_TX_REQUESTED = 'WALLET_BROADCAST_TX_REQUESTED'; -export const WALLET_BROADCAST_TX_FAILED = 'WALLET_BROADCAST_TX_FAILED'; -export const WALLET_BROADCAST_TX_SUCCEEDED = 'WALLET_BROADCAST_TX_SUCCEEDED'; -export const WALLET_RESET = 'WALLET_RESET'; +export enum TypeKeys { + WALLET_UNLOCK_PRIVATE_KEY = 'WALLET_UNLOCK_PRIVATE_KEY', + WALLET_UNLOCK_KEYSTORE = 'WALLET_UNLOCK_KEYSTORE', + WALLET_UNLOCK_MNEMONIC = 'WALLET_UNLOCK_MNEMONIC', + WALLET_UNLOCK_WEB3 = 'WALLET_UNLOCK_WEB3', + WALLET_SET = 'WALLET_SET', + WALLET_SET_BALANCE_PENDING = 'WALLET_SET_BALANCE_PENDING', + WALLET_SET_BALANCE_FULFILLED = 'WALLET_SET_BALANCE_FULFILLED', + WALLET_SET_BALANCE_REJECTED = 'WALLET_SET_BALANCE_REJECTED', + WALLET_SET_TOKEN_BALANCES = 'WALLET_SET_TOKEN_BALANCES', + WALLET_BROADCAST_TX_REQUESTED = 'WALLET_BROADCAST_TX_REQUESTED', + WALLET_BROADCAST_TX_FAILED = 'WALLET_BROADCAST_TX_FAILED', + WALLET_BROADCAST_TX_SUCCEEDED = 'WALLET_BROADCAST_TX_SUCCEEDED', + WALLET_RESET = 'WALLET_RESET' +} diff --git a/common/components/BalanceSidebar/AccountInfo.tsx b/common/components/BalanceSidebar/AccountInfo.tsx index 3ff85d0d..16edfc2c 100644 --- a/common/components/BalanceSidebar/AccountInfo.tsx +++ b/common/components/BalanceSidebar/AccountInfo.tsx @@ -1,18 +1,15 @@ -import { TFetchCCRates } from 'actions/rates'; -import { Identicon } from 'components/ui'; +import { Identicon, UnitDisplay } from 'components/ui'; import { NetworkConfig } from 'config/data'; -import { Ether } from 'libs/units'; -import { IWallet } from 'libs/wallet'; +import { IWallet, Balance } from 'libs/wallet'; import React from 'react'; import translate from 'translations'; -import { formatNumber } from 'utils/formatters'; import './AccountInfo.scss'; +import Spinner from 'components/ui/Spinner'; interface Props { - balance: Ether; + balance: Balance; wallet: IWallet; network: NetworkConfig; - fetchCCRates: TFetchCCRates; } interface State { @@ -26,14 +23,13 @@ export default class AccountInfo extends React.Component { }; public async setAddressFromWallet() { - const address = await this.props.wallet.getAddress(); + const address = await this.props.wallet.getAddressString(); if (address !== this.state.address) { this.setState({ address }); } } public componentDidMount() { - this.props.fetchCCRates(); this.setAddressFromWallet(); } @@ -54,7 +50,7 @@ export default class AccountInfo extends React.Component { public render() { const { network, balance } = this.props; const { blockExplorer, tokenExplorer } = network; - const { address } = this.state; + const { address, showLongBalance } = this.state; return (
@@ -80,38 +76,48 @@ export default class AccountInfo extends React.Component { className="AccountInfo-list-item-clickable mono wrap" onClick={this.toggleShowLongBalance} > - {this.state.showLongBalance - ? balance ? balance.toString() : '???' - : balance ? formatNumber(balance.amount) : '???'} + {balance.isPending ? ( + + ) : ( + + )} - {` ${network.name}`} + {!balance.isPending ? ( + balance.wei ? ( + {network.name} + ) : null + ) : null}
{(!!blockExplorer || !!tokenExplorer) && ( -
-
- {translate('sidebar_TransHistory')} -
- -
- )} +
+
+ {translate('sidebar_TransHistory')} +
+ +
+ )}
); } diff --git a/common/components/BalanceSidebar/EquivalentValues.scss b/common/components/BalanceSidebar/EquivalentValues.scss index 6b54c226..095651cf 100644 --- a/common/components/BalanceSidebar/EquivalentValues.scss +++ b/common/components/BalanceSidebar/EquivalentValues.scss @@ -1,5 +1,5 @@ -@import "common/sass/variables"; -@import "common/sass/mixins"; +@import 'common/sass/variables'; +@import 'common/sass/mixins'; .EquivalentValues { &-title { @@ -25,6 +25,7 @@ } &-label { + white-space: pre-wrap; display: inline-block; min-width: 36px; } @@ -33,5 +34,10 @@ @include mono; } } + + &-loader { + padding: 25px 0; + text-align: center; + } } } diff --git a/common/components/BalanceSidebar/EquivalentValues.tsx b/common/components/BalanceSidebar/EquivalentValues.tsx index 3b30bb35..7fda3f81 100644 --- a/common/components/BalanceSidebar/EquivalentValues.tsx +++ b/common/components/BalanceSidebar/EquivalentValues.tsx @@ -1,48 +1,203 @@ -import { Ether } from 'libs/units'; -import React from 'react'; +import * as React from 'react'; +import BN from 'bn.js'; import translate from 'translations'; -import { formatNumber } from 'utils/formatters'; -import './EquivalentValues.scss'; import { State } from 'reducers/rates'; -import { symbols } from 'actions/rates'; +import { rateSymbols, TFetchCCRates } from 'actions/rates'; +import { TokenBalance } from 'selectors/wallet'; +import { Balance } from 'libs/wallet'; +import Spinner from 'components/ui/Spinner'; +import UnitDisplay from 'components/ui/UnitDisplay'; +import './EquivalentValues.scss'; + +const ALL_OPTION = 'All'; interface Props { - balance?: Ether; - rates?: State['rates']; + balance?: Balance; + tokenBalances?: TokenBalance[]; + rates: State['rates']; ratesError?: State['ratesError']; + fetchCCRates: TFetchCCRates; } -export default class EquivalentValues extends React.Component { +interface CmpState { + currency: string; +} + +export default class EquivalentValues extends React.Component { + public state = { + currency: ALL_OPTION + }; + private balanceLookup: { [key: string]: Balance['wei'] | undefined } = {}; + private requestedCurrencies: string[] = []; + + public constructor(props) { + super(props); + this.makeBalanceLookup(props); + + if (props.balance && props.tokenBalances) { + this.fetchRates(props); + } + } + + public componentWillReceiveProps(nextProps) { + const { balance, tokenBalances } = this.props; + if ( + nextProps.balance !== balance || + nextProps.tokenBalances !== tokenBalances + ) { + this.makeBalanceLookup(nextProps); + this.fetchRates(nextProps); + } + } + public render() { - const { balance, rates, ratesError } = this.props; + const { balance, tokenBalances, rates, ratesError } = this.props; + const { currency } = this.state; + + // There are a bunch of reasons why the incorrect balances might be rendered + // while we have incomplete data that's being fetched. + const isFetching = + !balance || + balance.isPending || + !tokenBalances || + Object.keys(rates).length === 0; + + let valuesEl; + if (!isFetching && (rates[currency] || currency === ALL_OPTION)) { + const values = this.getEquivalentValues(currency); + valuesEl = rateSymbols.map(key => { + if (!values[key] || key === currency) { + return null; + } + + return ( +
  • + + {key}: + {' '} + + + +
  • + ); + }); + } else if (ratesError) { + valuesEl =
    {ratesError}
    ; + } else { + valuesEl = ( +
    + +
    + ); + } return (
    -
    {translate('sidebar_Equiv')}
    - -
      - {rates - ? symbols.map(key => { - if (!rates[key]) { - return null; +
      + {translate('sidebar_Equiv')} for{' '} + +
      + +
        {valuesEl}
    ); } + + private changeCurrency = (ev: React.FormEvent) => { + const currency = ev.currentTarget.value; + this.setState({ currency }); + }; + + private makeBalanceLookup(props: Props) { + const tokenBalances = props.tokenBalances || []; + this.balanceLookup = tokenBalances.reduce( + (prev, tk) => { + return { + ...prev, + [tk.symbol]: tk.balance + }; + }, + { ETH: props.balance && props.balance.wei } + ); + } + + private fetchRates(props: Props) { + // Duck out if we haven't gotten balances yet + if (!props.balance || !props.tokenBalances) { + return; + } + + // First determine which currencies we're asking for + const currencies = props.tokenBalances + .filter(tk => !tk.balance.isZero()) + .map(tk => tk.symbol) + .sort(); + + // If it's the same currencies as we have, skip it + if (currencies.join() === this.requestedCurrencies.join()) { + return; + } + + // Fire off the request and save the currencies requested + this.props.fetchCCRates(currencies); + this.requestedCurrencies = currencies; + } + + private getEquivalentValues( + currency: string + ): { + [key: string]: BN | undefined; + } { + // Recursively call on all currencies + if (currency === ALL_OPTION) { + return ['ETH'].concat(this.requestedCurrencies).reduce( + (prev, curr) => { + const currValues = this.getEquivalentValues(curr); + rateSymbols.forEach( + sym => (prev[sym] = prev[sym].add(currValues[sym] || new BN(0))) + ); + return prev; + }, + rateSymbols.reduce((prev, sym) => { + prev[sym] = new BN(0); + return prev; + }, {}) + ); + } + + // Calculate rates for a single currency + const { rates } = this.props; + const balance = this.balanceLookup[currency]; + if (!balance || !rates[currency]) { + return {}; + } + + return rateSymbols.reduce((prev, sym) => { + prev[sym] = balance ? balance.muln(rates[currency][sym]) : null; + return prev; + }, {}); + } } diff --git a/common/components/BalanceSidebar/TokenBalances/TokenRow.tsx b/common/components/BalanceSidebar/TokenBalances/TokenRow.tsx index b8165dd1..d7740b62 100644 --- a/common/components/BalanceSidebar/TokenBalances/TokenRow.tsx +++ b/common/components/BalanceSidebar/TokenBalances/TokenRow.tsx @@ -1,13 +1,14 @@ import removeIcon from 'assets/images/icon-remove.svg'; -import { BigNumber } from 'bignumber.js'; import React from 'react'; -import { formatNumber } from 'utils/formatters'; +import { TokenValue } from 'libs/units'; +import { UnitDisplay } from 'components/ui'; import './TokenRow.scss'; interface Props { - balance: BigNumber; + balance: TokenValue; symbol: string; custom?: boolean; + decimal: number; onRemove(symbol: string): void; } interface State { @@ -18,9 +19,11 @@ export default class TokenRow extends React.Component { public state = { showLongBalance: false }; + public render() { - const { balance, symbol, custom } = this.props; + const { balance, symbol, custom, decimal } = this.props; const { showLongBalance } = this.state; + return ( { title={`${balance.toString()} (Double-Click)`} onDoubleClick={this.toggleShowLongBalance} > - {!!custom && + {!!custom && ( } + /> + )} - {showLongBalance ? balance.toString() : formatNumber(balance)} + - - {symbol} - + {symbol} ); } diff --git a/common/components/BalanceSidebar/TokenBalances/index.tsx b/common/components/BalanceSidebar/TokenBalances/index.tsx index 93bd4b8a..a8920dbd 100644 --- a/common/components/BalanceSidebar/TokenBalances/index.tsx +++ b/common/components/BalanceSidebar/TokenBalances/index.tsx @@ -25,25 +25,24 @@ export default class TokenBalances extends React.Component { public render() { const { tokens } = this.props; const shownTokens = tokens.filter( - token => !token.balance.eq(0) || token.custom || this.state.showAllTokens + token => !token.balance.eqn(0) || token.custom || this.state.showAllTokens ); return (
    -
    - {translate('sidebar_TokenBal')} -
    +
    {translate('sidebar_TokenBal')}
    - {shownTokens.map(token => + {shownTokens.map(token => ( - )} + ))}
    @@ -58,16 +57,15 @@ export default class TokenBalances extends React.Component { className="btn btn-default btn-xs" onClick={this.toggleShowCustomTokenForm} > - - {translate('SEND_custom')} - + {translate('SEND_custom')} - {this.state.showCustomTokenForm && + {this.state.showCustomTokenForm && (
    -
    } + + )}
    ); } diff --git a/common/components/BalanceSidebar/index.tsx b/common/components/BalanceSidebar/index.tsx index 41dcf5a9..7fab773d 100644 --- a/common/components/BalanceSidebar/index.tsx +++ b/common/components/BalanceSidebar/index.tsx @@ -7,8 +7,7 @@ import { import { showNotification, TShowNotification } from 'actions/notifications'; import { fetchCCRates as dFetchCCRates, TFetchCCRates } from 'actions/rates'; import { NetworkConfig } from 'config/data'; -import { Ether } from 'libs/units'; -import { IWallet } from 'libs/wallet/IWallet'; +import { IWallet, Balance } from 'libs/wallet'; import React from 'react'; import { connect } from 'react-redux'; import { AppState } from 'reducers'; @@ -22,16 +21,15 @@ import AccountInfo from './AccountInfo'; import EquivalentValues from './EquivalentValues'; import Promos from './Promos'; import TokenBalances from './TokenBalances'; -import { State } from 'reducers/rates'; import OfflineToggle from './OfflineToggle'; interface Props { wallet: IWallet; - balance: Ether; + balance: Balance; network: NetworkConfig; tokenBalances: TokenBalance[]; - rates: State['rates']; - ratesError: State['ratesError']; + rates: AppState['rates']['rates']; + ratesError: AppState['rates']['ratesError']; showNotification: TShowNotification; addCustomToken: TAddCustomToken; removeCustomToken: TRemoveCustomToken; @@ -67,12 +65,7 @@ export class BalanceSidebar extends React.Component { { name: 'Account Info', content: ( - + ) }, { @@ -95,8 +88,10 @@ export class BalanceSidebar extends React.Component { content: ( ) } diff --git a/common/components/Footer/index.tsx b/common/components/Footer/index.tsx index 4101dbbc..e8eb76c4 100644 --- a/common/components/Footer/index.tsx +++ b/common/components/Footer/index.tsx @@ -1,6 +1,6 @@ import logo from 'assets/images/logo-myetherwallet.svg'; import { bityReferralURL, donationAddressMap } from 'config/data'; -import React, { Component } from 'react'; +import React from 'react'; import translate from 'translations'; import './index.scss'; import PreFooter from './PreFooter'; @@ -92,11 +92,15 @@ const LINKS_SOCIAL = [ } ]; -interface ComponentState { +interface Props { + latestBlock: string; +}; + +interface State { isOpen: boolean; } -export default class Footer extends React.Component<{}, ComponentState> { +export default class Footer extends React.Component { constructor(props) { super(props); this.state = { isOpen: false }; @@ -276,9 +280,7 @@ export default class Footer extends React.Component<{}, ComponentState> { ); })}

    - - {/* TODO: Fix me */} -

    Latest Block#: ?????

    +

    Latest Block#: {this.props.latestBlock}

    diff --git a/common/components/Header/components/CustomNodeModal.tsx b/common/components/Header/components/CustomNodeModal.tsx new file mode 100644 index 00000000..d4b63800 --- /dev/null +++ b/common/components/Header/components/CustomNodeModal.tsx @@ -0,0 +1,230 @@ +import React from 'react'; +import classnames from 'classnames'; +import Modal, { IButton } from 'components/ui/Modal'; +import translate from 'translations'; +import { NETWORKS, CustomNodeConfig } from 'config/data'; + +const NETWORK_KEYS = Object.keys(NETWORKS); + +interface Input { + name: string; + placeholder?: string; + type?: string; +} + +interface Props { + handleAddCustomNode(node: CustomNodeConfig): void; + handleClose(): void; +} + +interface State { + name: string; + url: string; + port: string; + network: string; + hasAuth: boolean; + username: string; + password: string; +} + +export default class CustomNodeModal extends React.Component { + public state: State = { + name: '', + url: '', + port: '', + network: NETWORK_KEYS[0], + hasAuth: false, + username: '', + password: '', + }; + + public render() { + const { handleClose } = this.props; + const isHttps = window.location.protocol.includes('https'); + const invalids = this.getInvalids(); + + const buttons: IButton[] = [{ + type: 'primary', + text: translate('NODE_CTA'), + onClick: this.saveAndAdd, + disabled: !!Object.keys(invalids).length, + }, { + text: translate('x_Cancel'), + onClick: handleClose + }]; + + return ( + +
    + {isHttps && +
    + {translate('NODE_Warning')} +
    + } + +
    +
    +
    + + {this.renderInput({ + name: 'name', + placeholder: 'My Node', + }, invalids)} +
    +
    + + +
    +
    + +
    +
    + + {this.renderInput({ + name: 'url', + placeholder: 'http://127.0.0.1/', + }, invalids)} +
    + +
    + + {this.renderInput({ + name: 'port', + placeholder: '8545', + type: 'number', + }, invalids)} +
    +
    +
    +
    + +
    +
    + {this.state.hasAuth && +
    +
    + + {this.renderInput({ name: 'username' }, invalids)} +
    +
    + + {this.renderInput({ + name: 'password', + type: 'password', + }, invalids)} +
    +
    + } +
    +
    +
    + ); + } + + private renderInput(input: Input, invalids: { [key: string]: boolean }) { + return ; + } + + private getInvalids(): { [key: string]: boolean } { + const { + url, + port, + hasAuth, + username, + password, + } = this.state; + const required = ["name", "url", "port", "network"]; + const invalids: { [key: string]: boolean } = {}; + + // Required fields + required.forEach((field) => { + if (!this.state[field]) { + invalids[field] = true; + } + }); + + // Somewhat valid URL, not 100% fool-proof + if (!/https?\:\/\/\w+/i.test(url)) { + invalids.url = true; + } + + // Numeric port within range + const iport = parseInt(port, 10); + if (!iport || iport < 1 || iport > 65535) { + invalids.port = true; + } + + // If they have auth, make sure it's provided + if (hasAuth) { + if (!username) { + invalids.username = true; + } + if (!password) { + invalids.password = true; + } + } + + return invalids; + } + + private handleChange = (ev: React.FormEvent< + HTMLInputElement | HTMLSelectElement + >) => { + const { name, value } = ev.currentTarget; + this.setState({ [name as any]: value }); + }; + + private handleCheckbox = (ev: React.FormEvent) => { + const { name } = ev.currentTarget; + this.setState({ [name as any]: !this.state[name] }); + }; + + private saveAndAdd = () => { + const node: CustomNodeConfig = { + name: this.state.name.trim(), + url: this.state.url.trim(), + port: parseInt(this.state.port, 10), + network: this.state.network, + }; + + if (this.state.hasAuth) { + node.auth = { + username: this.state.username, + password: this.state.password, + }; + } + + this.props.handleAddCustomNode(node); + }; +} diff --git a/common/components/Header/components/Navigation.tsx b/common/components/Header/components/Navigation.tsx index 15cda5cd..facdd691 100644 --- a/common/components/Header/components/Navigation.tsx +++ b/common/components/Header/components/Navigation.tsx @@ -1,4 +1,3 @@ -import PropTypes from 'prop-types'; import React, { Component } from 'react'; import NavigationLink from './NavigationLink'; @@ -21,10 +20,22 @@ const tabs = [ name: 'NAV_ViewWallet' // to: 'view-wallet' }, + { + name: 'NAV_Contracts', + to: 'contracts' + }, { name: 'NAV_ENS', to: 'ens' }, + { + name: 'Sign & Verify Message', + to: 'sign-and-verify-message' + }, + { + name: 'Broadcast Transaction', + to: 'pushTx' + }, { name: 'NAV_Help', to: 'https://myetherwallet.groovehq.com/help_center', @@ -54,7 +65,7 @@ export default class Navigation extends Component { /* * public scrollLeft() {} public scrollRight() {} - * + * */ public render() { diff --git a/common/components/Header/index.scss b/common/components/Header/index.scss index b063d3af..7f8aae6e 100644 --- a/common/components/Header/index.scss +++ b/common/components/Header/index.scss @@ -15,6 +15,15 @@ $small-size: 900px; } } +@keyframes dropdown-is-flashing { + 0%, 100% { + opacity: 0.8; + } + 50% { + opacity: 0.7; + } +} + // Header .Header { margin-bottom: 2rem; @@ -124,6 +133,11 @@ $small-size: 900px; padding-top: $space-sm !important; padding-bottom: $space-sm !important; } + + &.is-flashing { + pointer-events: none; + animation: dropdown-is-flashing 800ms ease infinite; + } } } } diff --git a/common/components/Header/index.tsx b/common/components/Header/index.tsx index 9d2ed562..6e381db4 100644 --- a/common/components/Header/index.tsx +++ b/common/components/Header/index.tsx @@ -1,11 +1,14 @@ import { TChangeGasPrice, TChangeLanguage, - TChangeNodeIntent + TChangeNodeIntent, + TAddCustomNode, + TRemoveCustomNode } from 'actions/config'; import logo from 'assets/images/logo-myetherwallet.svg'; import { Dropdown, ColorDropdown } from 'components/ui'; import React, { Component } from 'react'; +import classnames from 'classnames'; import { Link } from 'react-router-dom'; import { ANNOUNCEMENT_MESSAGE, @@ -13,43 +16,85 @@ import { languages, NETWORKS, NODES, - VERSION + VERSION, + NodeConfig, + CustomNodeConfig } from '../../config/data'; import GasPriceDropdown from './components/GasPriceDropdown'; import Navigation from './components/Navigation'; +import CustomNodeModal from './components/CustomNodeModal'; import { getKeyByValue } from 'utils/helpers'; +import { makeCustomNodeId } from 'utils/node'; import './index.scss'; interface Props { languageSelection: string; + node: NodeConfig; nodeSelection: string; + isChangingNode: boolean; gasPriceGwei: number; + customNodes: CustomNodeConfig[]; changeLanguage: TChangeLanguage; changeNodeIntent: TChangeNodeIntent; changeGasPrice: TChangeGasPrice; + addCustomNode: TAddCustomNode; + removeCustomNode: TRemoveCustomNode; } -export default class Header extends Component { +interface State { + isAddingCustomNode: boolean; +} + +export default class Header extends Component { + public state = { + isAddingCustomNode: false + }; + public render() { - const { languageSelection, changeNodeIntent, nodeSelection } = this.props; + const { + languageSelection, + changeNodeIntent, + node, + nodeSelection, + isChangingNode, + customNodes + } = this.props; + const { isAddingCustomNode } = this.state; const selectedLanguage = languageSelection; - const selectedNode = NODES[nodeSelection]; - const selectedNetwork = NETWORKS[selectedNode.network]; + const selectedNetwork = NETWORKS[node.network]; const LanguageDropDown = Dropdown as new () => Dropdown< typeof selectedLanguage >; - const nodeOptions = Object.keys(NODES).map(key => { - return { - value: key, - name: ( - - {NODES[key].network} ({NODES[key].service}) - - ), - color: NETWORKS[NODES[key].network].color - }; - }); + + const nodeOptions = Object.keys(NODES) + .map(key => { + return { + value: key, + name: ( + + {NODES[key].network} ({NODES[key].service}) + + ), + color: NETWORKS[NODES[key].network].color, + hidden: NODES[key].hidden + }; + }) + .concat( + customNodes.map(customNode => { + return { + value: makeCustomNodeId(customNode), + name: ( + + {customNode.network} - {customNode.name} (custom) + + ), + color: '#000', + hidden: false, + onRemove: () => this.props.removeCustomNode(customNode) + }; + }) + ); return (
    @@ -65,7 +110,7 @@ export default class Header extends Component {
    @@ -90,9 +135,9 @@ export default class Header extends Component {
    { />
    -
    +
    - Add Custom Node + Add Custom Node } + disabled={nodeSelection === 'web3'} onChange={changeNodeIntent} size="smr" color="white" + menuAlign="right" />
    @@ -128,6 +183,13 @@ export default class Header extends Component {
    + + {isAddingCustomNode && ( + + )}
    ); } @@ -138,4 +200,17 @@ export default class Header extends Component { this.props.changeLanguage(key); } }; + + private openCustomNodeModal = () => { + this.setState({ isAddingCustomNode: true }); + }; + + private closeCustomNodeModal = () => { + this.setState({ isAddingCustomNode: false }); + }; + + private addCustomNode = (node: CustomNodeConfig) => { + this.setState({ isAddingCustomNode: false }); + this.props.addCustomNode(node); + }; } diff --git a/common/components/PaperWallet/index.tsx b/common/components/PaperWallet/index.tsx index 1d34327c..a8f6baf9 100644 --- a/common/components/PaperWallet/index.tsx +++ b/common/components/PaperWallet/index.tsx @@ -1,5 +1,4 @@ import { Identicon, QRCode } from 'components/ui'; -import PrivKeyWallet from 'libs/wallet/privkey'; import React from 'react'; import ethLogo from 'assets/images/logo-ethereum-1.png'; @@ -91,26 +90,13 @@ const styles: any = { }; interface Props { - wallet: PrivKeyWallet; -} - -interface State { address: string; + privateKey: string; } -export default class PaperWallet extends React.Component { - public state = { address: '' }; - - public componentDidMount() { - if (!this.props.wallet) { - return; - } - this.props.wallet.getAddress().then(address => { - this.setState({ address }); - }); - } +export default class PaperWallet extends React.Component { public render() { - const privateKey = this.props.wallet.getPrivateKey(); + const { privateKey, address } = this.props; return (
    @@ -119,7 +105,7 @@ export default class PaperWallet extends React.Component {
    - +

    YOUR ADDRESS

    @@ -140,7 +126,7 @@ export default class PaperWallet extends React.Component {

    Your Address:
    - {this.state.address} + {address}

    Your Private Key: @@ -151,7 +137,7 @@ export default class PaperWallet extends React.Component {

    - +

    Always look for this icon when sending to this wallet diff --git a/common/components/PrintableWallet/index.tsx b/common/components/PrintableWallet/index.tsx index 205d56b1..bbabe064 100644 --- a/common/components/PrintableWallet/index.tsx +++ b/common/components/PrintableWallet/index.tsx @@ -1,49 +1,53 @@ import { PaperWallet } from 'components'; -import PrivKeyWallet from 'libs/wallet/privkey'; -import React, { Component } from 'react'; +import { IFullWallet } from 'ethereumjs-wallet'; +import React from 'react'; import translate from 'translations'; import printElement from 'utils/printElement'; -interface Props { - wallet: PrivKeyWallet; -} +const print = (address: string, privateKey: string) => () => + address && + privateKey && + printElement(, { + popupFeatures: { + scrollbars: 'no' + }, + styles: ` + * { + box-sizing: border-box; + } -export default class PrintableWallet extends Component { - public print = () => { - printElement(, { - popupFeatures: { - scrollbars: 'no' - }, - styles: ` - * { - box-sizing: border-box; - } + body { + font-family: Lato, sans-serif; + font-size: 1rem; + line-height: 1.4; + margin: 0; + } + ` + }); - body { - font-family: Lato, sans-serif; - font-size: 1rem; - line-height: 1.4; - margin: 0; - } - ` - }); - }; +const PrintableWallet: React.SFC<{ wallet: IFullWallet }> = ({ wallet }) => { + const address = wallet.getAddressString(); + const privateKey = wallet.getPrivateKeyString(); - public render() { - return ( -

    - ); + if (!address || !privateKey) { + return null; } -} + + return ( + + ); +}; + +export default PrintableWallet; diff --git a/common/components/Root/index.tsx b/common/components/Root/index.tsx deleted file mode 100644 index 54916a36..00000000 --- a/common/components/Root/index.tsx +++ /dev/null @@ -1,37 +0,0 @@ -import React, { Component } from 'react'; -import { Provider } from 'react-redux'; -import { Router, Route } from 'react-router-dom'; -// Components -import ENS from 'containers/Tabs/ENS'; -import GenerateWallet from 'containers/Tabs/GenerateWallet'; -import Help from 'containers/Tabs/Help'; -import SendTransaction from 'containers/Tabs/SendTransaction'; -import Swap from 'containers/Tabs/Swap'; -import ViewWallet from 'containers/Tabs/ViewWallet'; - -// TODO: fix this -interface Props { - store: any; - history: any; -} - -export default class Root extends Component { - public render() { - const { store, history } = this.props; - // key={Math.random()} = hack for HMR from https://github.com/webpack/webpack-dev-server/issues/395 - return ( - - -
    - - - - - - -
    -
    -
    - ); - } -} diff --git a/common/components/WalletDecrypt/DeterministicWalletsModal.scss b/common/components/WalletDecrypt/DeterministicWalletsModal.scss index 997e6c7f..b6bf5f2d 100644 --- a/common/components/WalletDecrypt/DeterministicWalletsModal.scss +++ b/common/components/WalletDecrypt/DeterministicWalletsModal.scss @@ -23,6 +23,7 @@ &-table { width: 100%; text-align: center; + margin-bottom: 10px; &-token { width: 82px; @@ -32,6 +33,10 @@ font-size: 13px; text-align: left; font-family: $font-family-monospace; + + input { + margin-right: 6px; + } } &-more { diff --git a/common/components/WalletDecrypt/DeterministicWalletsModal.tsx b/common/components/WalletDecrypt/DeterministicWalletsModal.tsx index 40e953d1..420eddb4 100644 --- a/common/components/WalletDecrypt/DeterministicWalletsModal.tsx +++ b/common/components/WalletDecrypt/DeterministicWalletsModal.tsx @@ -7,12 +7,14 @@ import { SetDesiredTokenAction } from 'actions/deterministicWallets'; import Modal, { IButton } from 'components/ui/Modal'; -import { NetworkConfig, Token } from 'config/data'; +import { AppState } from 'reducers'; +import { NetworkConfig } from 'config/data'; import { isValidPath } from 'libs/validators'; import React from 'react'; import { connect } from 'react-redux'; import { getNetworkConfig } from 'selectors/config'; import { getTokens, MergedToken } from 'selectors/wallet'; +import { UnitDisplay } from 'components/ui'; import './DeterministicWalletsModal.scss'; const WALLETS_PER_PAGE = 5; @@ -123,20 +125,21 @@ class DeterministicWalletsModal extends React.Component { onChange={this.handleChangePath} value={isCustomPath ? 'custom' : dPath} > - {dPaths.map(dp => + {dPaths.map(dp => ( - )} + ))} - {isCustomPath && + {isCustomPath && ( } + /> + )}
    @@ -145,9 +148,7 @@ class DeterministicWalletsModal extends React.Component { # Address - - {network.unit} - + {network.unit} More @@ -265,24 +266,19 @@ class DeterministicWalletsModal extends React.Component { ); }; - private renderWalletRow(wallet) { + private renderWalletRow(wallet: DeterministicWalletData) { const { desiredToken, network } = this.props; const { selectedAddress } = this.state; // Get renderable values, but keep 'em short - const value = wallet.value ? wallet.value.toEther().toPrecision(4) : ''; - const tokenValue = wallet.tokenValues[desiredToken] - ? wallet.tokenValues[desiredToken].toPrecision(4) - : ''; + const token = wallet.tokenValues[desiredToken]; return ( - - {wallet.index + 1} - + {wallet.index + 1} { {wallet.address} - {value} {network.unit} + - {tokenValue} {desiredToken} + {token ? ( + + ) : ( + '???' + )} { } } -function mapStateToProps(state) { +function mapStateToProps(state: AppState) { return { wallets: state.deterministicWallets.wallets, desiredToken: state.deterministicWallets.desiredToken, diff --git a/common/components/WalletDecrypt/Keystore.tsx b/common/components/WalletDecrypt/Keystore.tsx index aaa07112..efc5210c 100644 --- a/common/components/WalletDecrypt/Keystore.tsx +++ b/common/components/WalletDecrypt/Keystore.tsx @@ -1,4 +1,4 @@ -import { isKeystorePassRequired } from 'libs/keystore'; +import { isKeystorePassRequired } from 'libs/wallet'; import React, { Component } from 'react'; import translate, { translateRaw } from 'translations'; @@ -32,9 +32,7 @@ export default class KeystoreDecrypt extends Component { return (
    -

    - {translate('ADD_Radio_2_alt')} -

    +

    {translate('ADD_Radio_2_alt')}

    -

    - {translate('ADD_Label_3')} -

    +

    {translate('ADD_Label_3')}

    0 - ? 'is-valid' - : 'is-invalid'}`} + className={`form-control ${ + password.length > 0 ? 'is-valid' : 'is-invalid' + }`} value={password} onChange={this.onPasswordChange} onKeyDown={this.onKeyDown} diff --git a/common/components/WalletDecrypt/LedgerNano.tsx b/common/components/WalletDecrypt/LedgerNano.tsx index 02d4b195..c1f1e5c7 100644 --- a/common/components/WalletDecrypt/LedgerNano.tsx +++ b/common/components/WalletDecrypt/LedgerNano.tsx @@ -2,7 +2,7 @@ import './LedgerNano.scss'; import React, { Component } from 'react'; import translate, { translateRaw } from 'translations'; import DeterministicWalletsModal from './DeterministicWalletsModal'; -import LedgerWallet from 'libs/wallet/ledger'; +import { LedgerWallet } from 'libs/wallet'; import Ledger3 from 'vendor/ledger3'; import LedgerEth from 'vendor/ledger-eth'; import DPATHS from 'config/dpaths'; diff --git a/common/components/WalletDecrypt/Mnemonic.tsx b/common/components/WalletDecrypt/Mnemonic.tsx index c170d0dd..060e09fc 100644 --- a/common/components/WalletDecrypt/Mnemonic.tsx +++ b/common/components/WalletDecrypt/Mnemonic.tsx @@ -31,9 +31,7 @@ export default class MnemonicDecrypt extends Component { return (
    -

    - {translate('ADD_Radio_5')} -

    +

    {translate('ADD_Radio_5')}