Merge pull request #430 from MyEtherWallet/develop

Alpha 0.0.4
This commit is contained in:
Daniel Ternyak 2017-11-18 15:09:43 -06:00 committed by GitHub
commit 4d508bc081
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
263 changed files with 22526 additions and 38531 deletions

View File

@ -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'

1
.gitignore vendored
View File

@ -55,3 +55,4 @@ webpack_config/server.csr
v8-compile-cache-0/ v8-compile-cache-0/
package-lock.json

1
.npmrc Normal file
View File

@ -0,0 +1 @@
package-lock=false

View File

@ -1,8 +0,0 @@
FROM node:8.1.4
WORKDIR /usr/app
COPY package.json .
RUN npm install --quiet
COPY . .

215
README.md
View File

@ -1,5 +1,7 @@
# MyEtherWallet V4+ (ALPHA - VISIT [V3](https://github.com/kvhnuke/etherwallet) for the production site) # 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: #### Run:
```bash ```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 2. [dternyak/eth-priv-to-addr](https://hub.docker.com/r/dternyak/eth-priv-to-addr/) pulled from DockerHub
##### Docker setup instructions: ##### 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` 2. `docker pull dternyak/eth-priv-to-addr`
##### Run Derivation Checker ##### Run Derivation Checker
@ -48,40 +50,32 @@ npm run derivation-checker
``` ```
├── common - Your App ├── common
│ ├── actions - application actions │ ├── 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" │ ├── components - components according to "Redux philosophy"
│ ├── config - frontend config depending on REACT_WEBPACK_ENV │ ├── config - frontend config depending on REACT_WEBPACK_ENV
│ ├── containers - containers according to "Redux philosophy" │ ├── containers - containers according to "Redux philosophy"
│ ├── reducers - application reducers │ ├── reducers - application reducers
│ ├── routing - application routing │ ├── routing - application routing
│ ├── index.jsx - entry │ ├── index.tsx - entry
│ ├── index.html │ ├── index.html
├── static ├── static
├── webpack_config - Webpack configuration ├── webpack_config - Webpack configuration
├── jest_config - Jest 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 ## Style Guides and Philosophies
The following are guides for developers to follow for writing compliant code. The following are guides for developers to follow for writing compliant code.
### Redux and Actions ### Redux and Actions
Each reducer has one file in `reducers/[namespace].js` that contains the reducer Each reducer has one file in `reducers/[namespace].ts` that contains the reducer
and initial state, one file in `actions/[namespace].js` that contains the action and initial state, one file in `actions/[namespace].ts` that contains the action
creators and their return types, and optionally one file in 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). [`redux-saga`](https://github.com/redux-saga/redux-saga).
The files should be laid out as follows: The files should be laid out as follows:
@ -89,75 +83,192 @@ The files should be laid out as follows:
#### Reducer #### Reducer
* State should be explicitly defined and exported * State should be explicitly defined and exported
* Initial state should match state flow typing, define every key * Initial state should match state 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.
```js ```ts
// @flow import { NamespaceAction } from "actions/[namespace]";
import type { 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 const INITIAL_STATE: State = { /* Initial state shape */ };
export function namespace( export function [namespace](
state: State = INITIAL_STATE, state: State = INITIAL_STATE,
action: NamespaceAction action: NamespaceAction
): State { ): State {
switch (action.type) { switch (action.type) {
case 'NAMESPACE_NAME_OF_ACTION': case TypeKeys.NAMESPACE_NAME_OF_ACTION:
return { return {
...state, ...state,
// Alterations to state // Alterations to state
}; };
case 'NAMESPACE_NAME_OF_SAGA_ACTION':
default: default:
// Ensures every action was handled in reducer
// Unhandled actions should just fall into default
(action: empty);
return state; return state;
} }
} }
``` ```
#### Actions #### 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 ├── common
├── actions - application actions
```js ├── [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 ***/ /*** Name of action ***/
export type NameOfActionAction = { export interface NameOfActionAction {
type: 'NAMESPACE_NAME_OF_ACTION', type: TypeKeys.NAMESPACE_NAME_OF_ACTION,
/* Rest of the action object shape */ /* Rest of the action object shape */
}; };
export function nameOfAction(): NameOfActionAction {
return {
type: 'NAMESPACE_NAME_OF_ACTION',
/* Rest of the action object */
};
};
/*** Action Union ***/ /*** Action Union ***/
export type NamespaceAction = export type NamespaceAction =
| ActionOneAction | ActionOneAction
| ActionTwoAction | ActionTwoAction
| ActionThreeAction; | 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 ### Typing Redux-Connected Components
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.
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 (
<MyComponent
name="foo"
countryCode="CA"
// Error: 'routerLocation' is missing!
/>
);
}
```
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<MyComponentProps, {}> {
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<HTMLInputElement>) => {
if (this.props.onChange) {
this.props.onChange(
e.currentTarget.value,
this.props.unit
);
}
};
```
Where you type the event as a `React.FormEvent` of type `HTML<TYPE>Element`.
## 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 ### 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 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: the same name in the same folder, and import it like so:
```js ```ts
import React from "react"; import React from "react";
import "./MyComponent.scss"; import "./MyComponent.scss";
export default class MyComponent extends React.component { export default class MyComponent extends React.component<{}, {}> {
render() { render() {
return ( return (
<div className="MyComponent"> <div className="MyComponent">

87
common/Root.tsx Normal file
View File

@ -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<Props, {}> {
public render() {
const { store, history } = this.props;
// key={Math.random()} = hack for HMR from https://github.com/webpack/webpack-dev-server/issues/395
return (
<Provider store={store} key={Math.random()}>
<Router history={history} key={Math.random()}>
<div>
<Route exact={true} path="/" component={GenerateWallet} />
<Route path="/view-wallet" component={ViewWallet} />
<Route path="/help" component={Help} />
<Route path="/swap" component={Swap} />
<Route path="/send-transaction" component={SendTransaction} />
<Route path="/contracts" component={Contracts} />
<Route path="/ens" component={ENS} />
<Route
path="/sign-and-verify-message"
component={SignAndVerifyMessage}
/>
<Route path="/pushTx" component={BroadcastTx} />
<LegacyRoutes />
</div>
</Router>
</Provider>
);
}
}
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 (
<Switch>
<Redirect from="/signmsg.html" to="/sign-and-verify-message" />
<Redirect from="/helpers.html" to="/helpers" />
</Switch>
);
});

View File

@ -1,5 +1,6 @@
import * as interfaces from './actionTypes'; import * as interfaces from './actionTypes';
import { TypeKeys } from './constants'; import { TypeKeys } from './constants';
import { NodeConfig, CustomNodeConfig } from 'config/data';
export type TForceOfflineConfig = typeof forceOfflineConfig; export type TForceOfflineConfig = typeof forceOfflineConfig;
export function forceOfflineConfig(): interfaces.ForceOfflineAction { export function forceOfflineConfig(): interfaces.ForceOfflineAction {
@ -24,10 +25,13 @@ export function changeLanguage(sign: string): interfaces.ChangeLanguageAction {
} }
export type TChangeNode = typeof changeNode; export type TChangeNode = typeof changeNode;
export function changeNode(value: string): interfaces.ChangeNodeAction { export function changeNode(
nodeSelection: string,
node: NodeConfig
): interfaces.ChangeNodeAction {
return { return {
type: TypeKeys.CONFIG_NODE_CHANGE, type: TypeKeys.CONFIG_NODE_CHANGE,
payload: value payload: { nodeSelection, node }
}; };
} }
@ -55,3 +59,40 @@ export function changeNodeIntent(
payload 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
};
}

View File

@ -1,4 +1,5 @@
import { TypeKeys } from './constants'; import { TypeKeys } from './constants';
import { CustomNodeConfig, NodeConfig } from 'config/data';
/*** Toggle Offline ***/ /*** Toggle Offline ***/
export interface ToggleOfflineAction { export interface ToggleOfflineAction {
@ -20,7 +21,10 @@ export interface ChangeLanguageAction {
export interface ChangeNodeAction { export interface ChangeNodeAction {
type: TypeKeys.CONFIG_NODE_CHANGE; type: TypeKeys.CONFIG_NODE_CHANGE;
// FIXME $keyof? // FIXME $keyof?
payload: string; payload: {
nodeSelection: string;
node: NodeConfig;
};
} }
/*** Change gas price ***/ /*** Change gas price ***/
@ -40,6 +44,29 @@ export interface ChangeNodeIntentAction {
payload: string; 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 ***/ /*** Union Type ***/
export type ConfigAction = export type ConfigAction =
| ChangeNodeAction | ChangeNodeAction
@ -48,4 +75,8 @@ export type ConfigAction =
| ToggleOfflineAction | ToggleOfflineAction
| PollOfflineStatus | PollOfflineStatus
| ForceOfflineAction | ForceOfflineAction
| ChangeNodeIntentAction; | ChangeNodeIntentAction
| AddCustomNodeAction
| RemoveCustomNodeAction
| SetLatestBlockAction
| Web3UnsetNodeAction;

View File

@ -5,5 +5,9 @@ export enum TypeKeys {
CONFIG_GAS_PRICE = 'CONFIG_GAS_PRICE', CONFIG_GAS_PRICE = 'CONFIG_GAS_PRICE',
CONFIG_TOGGLE_OFFLINE = 'CONFIG_TOGGLE_OFFLINE', CONFIG_TOGGLE_OFFLINE = 'CONFIG_TOGGLE_OFFLINE',
CONFIG_FORCE_OFFLINE = 'CONFIG_FORCE_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'
} }

View File

@ -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
};
}

View File

@ -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;

View File

@ -1,4 +0,0 @@
export enum TypeKeys {
ACCESS_CONTRACT = 'ACCESS_CONTRACT',
SET_INTERACTIVE_CONTRACT = 'SET_INTERACTIVE_CONTRACT'
}

View File

@ -1,3 +0,0 @@
export * from './constants';
export * from './actionTypes';
export * from './actionCreators';

View File

@ -1,14 +1,19 @@
import { BigNumber } from 'bignumber.js'; import { TokenValue, Wei } from 'libs/units';
export interface TokenValues { export interface ITokenData {
[key: string]: BigNumber; value: TokenValue;
decimal: number;
}
export interface ITokenValues {
[key: string]: ITokenData | null;
} }
export interface DeterministicWalletData { export interface DeterministicWalletData {
index: number; index: number;
address: string; address: string;
value?: BigNumber; value?: TokenValue;
tokenValues: TokenValues; tokenValues: ITokenValues;
} }
/*** Get determinstic wallets ***/ /*** Get determinstic wallets ***/
@ -39,8 +44,8 @@ export interface SetDesiredTokenAction {
/*** Set wallet values ***/ /*** Set wallet values ***/
export interface UpdateDeterministicWalletArgs { export interface UpdateDeterministicWalletArgs {
address: string; address: string;
value?: BigNumber; value?: Wei;
tokenValues?: TokenValues; tokenValues?: ITokenValues;
index?: any; index?: any;
} }

View File

@ -1,5 +1,3 @@
import * as constants from './constants';
/*** Resolve ENS name ***/ /*** Resolve ENS name ***/
export interface ResolveEnsNameAction { export interface ResolveEnsNameAction {
type: 'ENS_RESOLVE'; type: 'ENS_RESOLVE';

View File

@ -1,4 +1,4 @@
import { PrivKeyWallet } from 'libs/wallet'; import { generate } from 'ethereumjs-wallet';
import * as interfaces from './actionTypes'; import * as interfaces from './actionTypes';
import { TypeKeys } from './constants'; import { TypeKeys } from './constants';
@ -8,7 +8,7 @@ export function generateNewWallet(
): interfaces.GenerateNewWalletAction { ): interfaces.GenerateNewWalletAction {
return { return {
type: TypeKeys.GENERATE_WALLET_GENERATE_WALLET, type: TypeKeys.GENERATE_WALLET_GENERATE_WALLET,
wallet: PrivKeyWallet.generate(), wallet: generate(),
password password
}; };
} }

View File

@ -1,10 +1,10 @@
import { PrivKeyWallet } from 'libs/wallet'; import { IFullWallet } from 'ethereumjs-wallet';
import { TypeKeys } from './constants'; import { TypeKeys } from './constants';
/*** Generate Wallet File ***/ /*** Generate Wallet File ***/
export interface GenerateNewWalletAction { export interface GenerateNewWalletAction {
type: TypeKeys.GENERATE_WALLET_GENERATE_WALLET; type: TypeKeys.GENERATE_WALLET_GENERATE_WALLET;
wallet: PrivKeyWallet; wallet: IFullWallet;
password: string; password: string;
} }

View File

@ -13,7 +13,8 @@ export function showNotification(
payload: { payload: {
level, level,
msg, msg,
duration duration,
id: Math.random()
} }
}; };
} }

View File

@ -7,6 +7,7 @@ export type INFINITY = 'infinity';
export interface Notification { export interface Notification {
level: NOTIFICATION_LEVEL; level: NOTIFICATION_LEVEL;
msg: ReactElement<any> | string; msg: ReactElement<any> | string;
id: number;
duration?: number | INFINITY; duration?: number | INFINITY;
} }

View File

@ -3,9 +3,26 @@ import { TypeKeys } from './constants';
import { fetchRates, CCResponse } from './actionPayloads'; import { fetchRates, CCResponse } from './actionPayloads';
export type TFetchCCRates = typeof fetchCCRates; export type TFetchCCRates = typeof fetchCCRates;
export function fetchCCRates(): interfaces.FetchCCRates { export function fetchCCRates(symbols: string[] = []): interfaces.FetchCCRates {
return { return {
type: TypeKeys.RATES_FETCH_CC, 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
}; };
} }

View File

@ -1,22 +1,52 @@
import { handleJSONResponse } from 'api/utils'; import { handleJSONResponse } from 'api/utils';
export const symbols = ['USD', 'EUR', 'GBP', 'BTC', 'CHF', 'REP']; export const rateSymbols = ['USD', 'EUR', 'GBP', 'BTC', 'CHF', 'REP', 'ETH'];
const symbolsURL = symbols.join(',');
// TODO - internationalize // TODO - internationalize
const ERROR_MESSAGE = 'Could not fetch rate data.'; const ERROR_MESSAGE = 'Could not fetch rate data.';
const CCApi = 'https://min-api.cryptocompare.com'; 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 { export interface CCResponse {
BTC: number; [symbol: string]: {
EUR: number; USD: number;
GBP: number; EUR: number;
CHF: number; GBP: number;
REP: number; BTC: number;
CHF: number;
REP: number;
ETH: number;
};
} }
export const fetchRates = (): Promise<CCResponse> => export const fetchRates = (symbols: string[] = []): Promise<CCResponse> =>
fetch(CCRates(symbolsURL)).then(response => fetch(CCRates(symbols))
handleJSONResponse(response, ERROR_MESSAGE) .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
}
}
);
});

View File

@ -18,6 +18,6 @@ export interface FetchCCRatesFailed {
/*** Union Type ***/ /*** Union Type ***/
export type RatesAction = export type RatesAction =
| FetchCCRatesSucceeded
| FetchCCRates | FetchCCRates
| FetchCCRatesSucceeded
| FetchCCRatesFailed; | FetchCCRatesFailed;

View File

@ -1,15 +1,13 @@
import { BigNumber } from 'bignumber.js'; import { Wei, TokenValue } from 'libs/units';
import { Wei } from 'libs/units';
import { IWallet } from 'libs/wallet/IWallet'; import { IWallet } from 'libs/wallet/IWallet';
import * as types from './actionTypes'; import * as types from './actionTypes';
import * as constants from './constants'; import { TypeKeys } from './constants';
export type TUnlockPrivateKey = typeof unlockPrivateKey; export type TUnlockPrivateKey = typeof unlockPrivateKey;
export function unlockPrivateKey( export function unlockPrivateKey(
value: types.PrivateKeyUnlockParams value: types.PrivateKeyUnlockParams
): types.UnlockPrivateKeyAction { ): types.UnlockPrivateKeyAction {
return { return {
type: constants.WALLET_UNLOCK_PRIVATE_KEY, type: TypeKeys.WALLET_UNLOCK_PRIVATE_KEY,
payload: value payload: value
}; };
} }
@ -19,7 +17,7 @@ export function unlockKeystore(
value: types.KeystoreUnlockParams value: types.KeystoreUnlockParams
): types.UnlockKeystoreAction { ): types.UnlockKeystoreAction {
return { return {
type: constants.WALLET_UNLOCK_KEYSTORE, type: TypeKeys.WALLET_UNLOCK_KEYSTORE,
payload: value payload: value
}; };
} }
@ -29,33 +27,54 @@ export function unlockMnemonic(
value: types.MnemonicUnlockParams value: types.MnemonicUnlockParams
): types.UnlockMnemonicAction { ): types.UnlockMnemonicAction {
return { return {
type: constants.WALLET_UNLOCK_MNEMONIC, type: TypeKeys.WALLET_UNLOCK_MNEMONIC,
payload: value payload: value
}; };
} }
export type TUnlockWeb3 = typeof unlockWeb3;
export function unlockWeb3(): types.UnlockWeb3Action {
return {
type: TypeKeys.WALLET_UNLOCK_WEB3
};
}
export type TSetWallet = typeof setWallet; export type TSetWallet = typeof setWallet;
export function setWallet(value: IWallet): types.SetWalletAction { export function setWallet(value: IWallet): types.SetWalletAction {
return { return {
type: constants.WALLET_SET, type: TypeKeys.WALLET_SET,
payload: value payload: value
}; };
} }
export type TSetBalance = typeof setBalance; export function setBalancePending(): types.SetBalancePendingAction {
export function setBalance(value: Wei): types.SetBalanceAction {
return { 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 payload: value
}; };
} }
export function setBalanceRejected(): types.SetBalanceRejectedAction {
return {
type: TypeKeys.WALLET_SET_BALANCE_REJECTED
};
}
export type TSetTokenBalances = typeof setTokenBalances; export type TSetTokenBalances = typeof setTokenBalances;
export function setTokenBalances(payload: { export function setTokenBalances(payload: {
[key: string]: BigNumber; [key: string]: TokenValue;
}): types.SetTokenBalancesAction { }): types.SetTokenBalancesAction {
return { return {
type: constants.WALLET_SET_TOKEN_BALANCES, type: TypeKeys.WALLET_SET_TOKEN_BALANCES,
payload payload
}; };
} }
@ -65,7 +84,7 @@ export function broadcastTx(
signedTx: string signedTx: string
): types.BroadcastTxRequestedAction { ): types.BroadcastTxRequestedAction {
return { return {
type: constants.WALLET_BROADCAST_TX_REQUESTED, type: TypeKeys.WALLET_BROADCAST_TX_REQUESTED,
payload: { payload: {
signedTx signedTx
} }
@ -78,7 +97,7 @@ export function broadcastTxSucceded(
signedTx: string signedTx: string
): types.BroadcastTxSuccededAction { ): types.BroadcastTxSuccededAction {
return { return {
type: constants.WALLET_BROADCAST_TX_SUCCEEDED, type: TypeKeys.WALLET_BROADCAST_TX_SUCCEEDED,
payload: { payload: {
txHash, txHash,
signedTx signedTx
@ -92,7 +111,7 @@ export function broadCastTxFailed(
errorMsg: string errorMsg: string
): types.BroadcastTxFailedAction { ): types.BroadcastTxFailedAction {
return { return {
type: constants.WALLET_BROADCAST_TX_FAILED, type: TypeKeys.WALLET_BROADCAST_TX_FAILED,
payload: { payload: {
signedTx, signedTx,
error: errorMsg error: errorMsg
@ -101,8 +120,8 @@ export function broadCastTxFailed(
} }
export type TResetWallet = typeof resetWallet; export type TResetWallet = typeof resetWallet;
export function resetWallet() { export function resetWallet(): types.ResetWalletAction {
return { return {
type: constants.WALLET_RESET type: TypeKeys.WALLET_RESET
}; };
} }

View File

@ -1,6 +1,6 @@
import { BigNumber } from 'bignumber.js'; import { Wei, TokenValue } from 'libs/units';
import { Wei } from 'libs/units';
import { IWallet } from 'libs/wallet/IWallet'; import { IWallet } from 'libs/wallet/IWallet';
import { TypeKeys } from './constants';
/*** Unlock Private Key ***/ /*** Unlock Private Key ***/
export interface PrivateKeyUnlockParams { export interface PrivateKeyUnlockParams {
@ -9,42 +9,52 @@ export interface PrivateKeyUnlockParams {
} }
export interface UnlockPrivateKeyAction { export interface UnlockPrivateKeyAction {
type: 'WALLET_UNLOCK_PRIVATE_KEY'; type: TypeKeys.WALLET_UNLOCK_PRIVATE_KEY;
payload: PrivateKeyUnlockParams; payload: PrivateKeyUnlockParams;
} }
export interface UnlockMnemonicAction { export interface UnlockMnemonicAction {
type: 'WALLET_UNLOCK_MNEMONIC'; type: TypeKeys.WALLET_UNLOCK_MNEMONIC;
payload: MnemonicUnlockParams; payload: MnemonicUnlockParams;
} }
export interface UnlockWeb3Action {
type: TypeKeys.WALLET_UNLOCK_WEB3;
}
/*** Set Wallet ***/ /*** Set Wallet ***/
export interface SetWalletAction { export interface SetWalletAction {
type: 'WALLET_SET'; type: TypeKeys.WALLET_SET;
payload: IWallet; payload: IWallet;
} }
/*** Reset Wallet ***/ /*** Reset Wallet ***/
export interface ResetWalletAction { export interface ResetWalletAction {
type: 'WALLET_RESET'; type: TypeKeys.WALLET_RESET;
} }
/*** Set Balance ***/ /*** Set Balance ***/
export interface SetBalanceAction { export interface SetBalancePendingAction {
type: 'WALLET_SET_BALANCE'; type: TypeKeys.WALLET_SET_BALANCE_PENDING;
}
export interface SetBalanceFullfilledAction {
type: TypeKeys.WALLET_SET_BALANCE_FULFILLED;
payload: Wei; payload: Wei;
} }
export interface SetBalanceRejectedAction {
type: TypeKeys.WALLET_SET_BALANCE_REJECTED;
}
/*** Set Token Balance ***/ /*** Set Token Balance ***/
export interface SetTokenBalancesAction { export interface SetTokenBalancesAction {
type: 'WALLET_SET_TOKEN_BALANCES'; type: TypeKeys.WALLET_SET_TOKEN_BALANCES;
payload: { payload: {
[key: string]: BigNumber; [key: string]: TokenValue;
}; };
} }
/*** Broadcast Tx ***/ /*** Broadcast Tx ***/
export interface BroadcastTxRequestedAction { export interface BroadcastTxRequestedAction {
type: 'WALLET_BROADCAST_TX_REQUESTED'; type: TypeKeys.WALLET_BROADCAST_TX_REQUESTED;
payload: { payload: {
signedTx: string; signedTx: string;
}; };
@ -65,12 +75,12 @@ export interface KeystoreUnlockParams {
} }
export interface UnlockKeystoreAction { export interface UnlockKeystoreAction {
type: 'WALLET_UNLOCK_KEYSTORE'; type: TypeKeys.WALLET_UNLOCK_KEYSTORE;
payload: KeystoreUnlockParams; payload: KeystoreUnlockParams;
} }
export interface BroadcastTxSuccededAction { export interface BroadcastTxSuccededAction {
type: 'WALLET_BROADCAST_TX_SUCCEEDED'; type: TypeKeys.WALLET_BROADCAST_TX_SUCCEEDED;
payload: { payload: {
txHash: string; txHash: string;
signedTx: string; signedTx: string;
@ -78,7 +88,7 @@ export interface BroadcastTxSuccededAction {
} }
export interface BroadcastTxFailedAction { export interface BroadcastTxFailedAction {
type: 'WALLET_BROADCAST_TX_FAILED'; type: TypeKeys.WALLET_BROADCAST_TX_FAILED;
payload: { payload: {
signedTx: string; signedTx: string;
error: string; error: string;
@ -90,7 +100,9 @@ export type WalletAction =
| UnlockPrivateKeyAction | UnlockPrivateKeyAction
| SetWalletAction | SetWalletAction
| ResetWalletAction | ResetWalletAction
| SetBalanceAction | SetBalancePendingAction
| SetBalanceFullfilledAction
| SetBalanceRejectedAction
| SetTokenBalancesAction | SetTokenBalancesAction
| BroadcastTxRequestedAction | BroadcastTxRequestedAction
| BroadcastTxFailedAction | BroadcastTxFailedAction

View File

@ -1,10 +1,15 @@
export const WALLET_UNLOCK_PRIVATE_KEY = 'WALLET_UNLOCK_PRIVATE_KEY'; export enum TypeKeys {
export const WALLET_UNLOCK_KEYSTORE = 'WALLET_UNLOCK_KEYSTORE'; WALLET_UNLOCK_PRIVATE_KEY = 'WALLET_UNLOCK_PRIVATE_KEY',
export const WALLET_UNLOCK_MNEMONIC = 'WALLET_UNLOCK_MNEMONIC'; WALLET_UNLOCK_KEYSTORE = 'WALLET_UNLOCK_KEYSTORE',
export const WALLET_SET = 'WALLET_SET'; WALLET_UNLOCK_MNEMONIC = 'WALLET_UNLOCK_MNEMONIC',
export const WALLET_SET_BALANCE = 'WALLET_SET_BALANCE'; WALLET_UNLOCK_WEB3 = 'WALLET_UNLOCK_WEB3',
export const WALLET_SET_TOKEN_BALANCES = 'WALLET_SET_TOKEN_BALANCES'; WALLET_SET = 'WALLET_SET',
export const WALLET_BROADCAST_TX_REQUESTED = 'WALLET_BROADCAST_TX_REQUESTED'; WALLET_SET_BALANCE_PENDING = 'WALLET_SET_BALANCE_PENDING',
export const WALLET_BROADCAST_TX_FAILED = 'WALLET_BROADCAST_TX_FAILED'; WALLET_SET_BALANCE_FULFILLED = 'WALLET_SET_BALANCE_FULFILLED',
export const WALLET_BROADCAST_TX_SUCCEEDED = 'WALLET_BROADCAST_TX_SUCCEEDED'; WALLET_SET_BALANCE_REJECTED = 'WALLET_SET_BALANCE_REJECTED',
export const WALLET_RESET = 'WALLET_RESET'; 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'
}

View File

@ -1,18 +1,15 @@
import { TFetchCCRates } from 'actions/rates'; import { Identicon, UnitDisplay } from 'components/ui';
import { Identicon } from 'components/ui';
import { NetworkConfig } from 'config/data'; import { NetworkConfig } from 'config/data';
import { Ether } from 'libs/units'; import { IWallet, Balance } from 'libs/wallet';
import { IWallet } from 'libs/wallet';
import React from 'react'; import React from 'react';
import translate from 'translations'; import translate from 'translations';
import { formatNumber } from 'utils/formatters';
import './AccountInfo.scss'; import './AccountInfo.scss';
import Spinner from 'components/ui/Spinner';
interface Props { interface Props {
balance: Ether; balance: Balance;
wallet: IWallet; wallet: IWallet;
network: NetworkConfig; network: NetworkConfig;
fetchCCRates: TFetchCCRates;
} }
interface State { interface State {
@ -26,14 +23,13 @@ export default class AccountInfo extends React.Component<Props, State> {
}; };
public async setAddressFromWallet() { public async setAddressFromWallet() {
const address = await this.props.wallet.getAddress(); const address = await this.props.wallet.getAddressString();
if (address !== this.state.address) { if (address !== this.state.address) {
this.setState({ address }); this.setState({ address });
} }
} }
public componentDidMount() { public componentDidMount() {
this.props.fetchCCRates();
this.setAddressFromWallet(); this.setAddressFromWallet();
} }
@ -54,7 +50,7 @@ export default class AccountInfo extends React.Component<Props, State> {
public render() { public render() {
const { network, balance } = this.props; const { network, balance } = this.props;
const { blockExplorer, tokenExplorer } = network; const { blockExplorer, tokenExplorer } = network;
const { address } = this.state; const { address, showLongBalance } = this.state;
return ( return (
<div className="AccountInfo"> <div className="AccountInfo">
@ -80,38 +76,48 @@ export default class AccountInfo extends React.Component<Props, State> {
className="AccountInfo-list-item-clickable mono wrap" className="AccountInfo-list-item-clickable mono wrap"
onClick={this.toggleShowLongBalance} onClick={this.toggleShowLongBalance}
> >
{this.state.showLongBalance {balance.isPending ? (
? balance ? balance.toString() : '???' <Spinner />
: balance ? formatNumber(balance.amount) : '???'} ) : (
<UnitDisplay
value={balance.wei}
unit={'ether'}
displayShortBalance={!showLongBalance}
/>
)}
</span> </span>
{` ${network.name}`} {!balance.isPending ? (
balance.wei ? (
<span> {network.name}</span>
) : null
) : null}
</li> </li>
</ul> </ul>
</div> </div>
{(!!blockExplorer || !!tokenExplorer) && ( {(!!blockExplorer || !!tokenExplorer) && (
<div className="AccountInfo-section"> <div className="AccountInfo-section">
<h5 className="AccountInfo-section-header"> <h5 className="AccountInfo-section-header">
{translate('sidebar_TransHistory')} {translate('sidebar_TransHistory')}
</h5> </h5>
<ul className="AccountInfo-list"> <ul className="AccountInfo-list">
{!!blockExplorer && ( {!!blockExplorer && (
<li className="AccountInfo-list-item"> <li className="AccountInfo-list-item">
<a href={blockExplorer.address(address)} target="_blank"> <a href={blockExplorer.address(address)} target="_blank">
{`${network.name} (${blockExplorer.name})`} {`${network.name} (${blockExplorer.name})`}
</a> </a>
</li> </li>
)} )}
{!!tokenExplorer && ( {!!tokenExplorer && (
<li className="AccountInfo-list-item"> <li className="AccountInfo-list-item">
<a href={tokenExplorer.address(address)} target="_blank"> <a href={tokenExplorer.address(address)} target="_blank">
{`Tokens (${tokenExplorer.name})`} {`Tokens (${tokenExplorer.name})`}
</a> </a>
</li> </li>
)} )}
</ul> </ul>
</div> </div>
)} )}
</div> </div>
); );
} }

View File

@ -1,5 +1,5 @@
@import "common/sass/variables"; @import 'common/sass/variables';
@import "common/sass/mixins"; @import 'common/sass/mixins';
.EquivalentValues { .EquivalentValues {
&-title { &-title {
@ -25,6 +25,7 @@
} }
&-label { &-label {
white-space: pre-wrap;
display: inline-block; display: inline-block;
min-width: 36px; min-width: 36px;
} }
@ -33,5 +34,10 @@
@include mono; @include mono;
} }
} }
&-loader {
padding: 25px 0;
text-align: center;
}
} }
} }

View File

@ -1,48 +1,203 @@
import { Ether } from 'libs/units'; import * as React from 'react';
import React from 'react'; import BN from 'bn.js';
import translate from 'translations'; import translate from 'translations';
import { formatNumber } from 'utils/formatters';
import './EquivalentValues.scss';
import { State } from 'reducers/rates'; 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 { interface Props {
balance?: Ether; balance?: Balance;
rates?: State['rates']; tokenBalances?: TokenBalance[];
rates: State['rates'];
ratesError?: State['ratesError']; ratesError?: State['ratesError'];
fetchCCRates: TFetchCCRates;
} }
export default class EquivalentValues extends React.Component<Props, {}> { interface CmpState {
currency: string;
}
export default class EquivalentValues extends React.Component<Props, CmpState> {
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() { 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 (
<li className="EquivalentValues-values-currency" key={key}>
<span className="EquivalentValues-values-currency-label">
{key}:
</span>{' '}
<span className="EquivalentValues-values-currency-value">
<UnitDisplay
unit={'ether'}
value={values[key]}
displayShortBalance={3}
/>
</span>
</li>
);
});
} else if (ratesError) {
valuesEl = <h5>{ratesError}</h5>;
} else {
valuesEl = (
<div className="EquivalentValues-values-loader">
<Spinner size="x3" />
</div>
);
}
return ( return (
<div className="EquivalentValues"> <div className="EquivalentValues">
<h5 className="EquivalentValues-title">{translate('sidebar_Equiv')}</h5> <h5 className="EquivalentValues-title">
{translate('sidebar_Equiv')} for{' '}
<ul className="EquivalentValues-values"> <select
{rates className="EquivalentValues-title-symbol"
? symbols.map(key => { onChange={this.changeCurrency}
if (!rates[key]) { value={currency}
return null; >
<option value={ALL_OPTION}>All Tokens</option>
<option value="ETH">ETH</option>
{tokenBalances &&
tokenBalances.map(tk => {
if (!tk.balance || tk.balance.isZero()) {
return;
} }
const sym = tk.symbol;
return ( return (
<li className="EquivalentValues-values-currency" key={key}> <option key={sym} value={sym}>
<span className="EquivalentValues-values-currency-label"> {sym}
{key}: </option>
</span>
<span className="EquivalentValues-values-currency-value">
{' '}
{balance
? formatNumber(balance.amount.times(rates[key]))
: '???'}
</span>
</li>
); );
}) })}
: ratesError && <h5>{ratesError}</h5>} </select>
</ul> </h5>
<ul className="EquivalentValues-values">{valuesEl}</ul>
</div> </div>
); );
} }
private changeCurrency = (ev: React.FormEvent<HTMLSelectElement>) => {
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;
}, {});
}
} }

View File

@ -1,13 +1,14 @@
import removeIcon from 'assets/images/icon-remove.svg'; import removeIcon from 'assets/images/icon-remove.svg';
import { BigNumber } from 'bignumber.js';
import React from 'react'; import React from 'react';
import { formatNumber } from 'utils/formatters'; import { TokenValue } from 'libs/units';
import { UnitDisplay } from 'components/ui';
import './TokenRow.scss'; import './TokenRow.scss';
interface Props { interface Props {
balance: BigNumber; balance: TokenValue;
symbol: string; symbol: string;
custom?: boolean; custom?: boolean;
decimal: number;
onRemove(symbol: string): void; onRemove(symbol: string): void;
} }
interface State { interface State {
@ -18,9 +19,11 @@ export default class TokenRow extends React.Component<Props, State> {
public state = { public state = {
showLongBalance: false showLongBalance: false
}; };
public render() { public render() {
const { balance, symbol, custom } = this.props; const { balance, symbol, custom, decimal } = this.props;
const { showLongBalance } = this.state; const { showLongBalance } = this.state;
return ( return (
<tr className="TokenRow"> <tr className="TokenRow">
<td <td
@ -28,21 +31,24 @@ export default class TokenRow extends React.Component<Props, State> {
title={`${balance.toString()} (Double-Click)`} title={`${balance.toString()} (Double-Click)`}
onDoubleClick={this.toggleShowLongBalance} onDoubleClick={this.toggleShowLongBalance}
> >
{!!custom && {!!custom && (
<img <img
src={removeIcon} src={removeIcon}
className="TokenRow-balance-remove" className="TokenRow-balance-remove"
title="Remove Token" title="Remove Token"
onClick={this.onRemove} onClick={this.onRemove}
tabIndex={0} tabIndex={0}
/>} />
)}
<span> <span>
{showLongBalance ? balance.toString() : formatNumber(balance)} <UnitDisplay
value={balance}
decimal={decimal}
displayShortBalance={!showLongBalance}
/>
</span> </span>
</td> </td>
<td className="TokenRow-symbol"> <td className="TokenRow-symbol">{symbol}</td>
{symbol}
</td>
</tr> </tr>
); );
} }

View File

@ -25,25 +25,24 @@ export default class TokenBalances extends React.Component<Props, State> {
public render() { public render() {
const { tokens } = this.props; const { tokens } = this.props;
const shownTokens = tokens.filter( 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 ( return (
<section className="TokenBalances"> <section className="TokenBalances">
<h5 className="TokenBalances-title"> <h5 className="TokenBalances-title">{translate('sidebar_TokenBal')}</h5>
{translate('sidebar_TokenBal')}
</h5>
<table className="TokenBalances-rows"> <table className="TokenBalances-rows">
<tbody> <tbody>
{shownTokens.map(token => {shownTokens.map(token => (
<TokenRow <TokenRow
key={token.symbol} key={token.symbol}
balance={token.balance} balance={token.balance}
symbol={token.symbol} symbol={token.symbol}
custom={token.custom} custom={token.custom}
decimal={token.decimal}
onRemove={this.props.onRemoveCustomToken} onRemove={this.props.onRemoveCustomToken}
/> />
)} ))}
</tbody> </tbody>
</table> </table>
@ -58,16 +57,15 @@ export default class TokenBalances extends React.Component<Props, State> {
className="btn btn-default btn-xs" className="btn btn-default btn-xs"
onClick={this.toggleShowCustomTokenForm} onClick={this.toggleShowCustomTokenForm}
> >
<span> <span>{translate('SEND_custom')}</span>
{translate('SEND_custom')}
</span>
</button> </button>
</div> </div>
{this.state.showCustomTokenForm && {this.state.showCustomTokenForm && (
<div className="TokenBalances-form"> <div className="TokenBalances-form">
<AddCustomTokenForm onSave={this.addCustomToken} /> <AddCustomTokenForm onSave={this.addCustomToken} />
</div>} </div>
)}
</section> </section>
); );
} }

View File

@ -7,8 +7,7 @@ import {
import { showNotification, TShowNotification } from 'actions/notifications'; import { showNotification, TShowNotification } from 'actions/notifications';
import { fetchCCRates as dFetchCCRates, TFetchCCRates } from 'actions/rates'; import { fetchCCRates as dFetchCCRates, TFetchCCRates } from 'actions/rates';
import { NetworkConfig } from 'config/data'; import { NetworkConfig } from 'config/data';
import { Ether } from 'libs/units'; import { IWallet, Balance } from 'libs/wallet';
import { IWallet } from 'libs/wallet/IWallet';
import React from 'react'; import React from 'react';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import { AppState } from 'reducers'; import { AppState } from 'reducers';
@ -22,16 +21,15 @@ import AccountInfo from './AccountInfo';
import EquivalentValues from './EquivalentValues'; import EquivalentValues from './EquivalentValues';
import Promos from './Promos'; import Promos from './Promos';
import TokenBalances from './TokenBalances'; import TokenBalances from './TokenBalances';
import { State } from 'reducers/rates';
import OfflineToggle from './OfflineToggle'; import OfflineToggle from './OfflineToggle';
interface Props { interface Props {
wallet: IWallet; wallet: IWallet;
balance: Ether; balance: Balance;
network: NetworkConfig; network: NetworkConfig;
tokenBalances: TokenBalance[]; tokenBalances: TokenBalance[];
rates: State['rates']; rates: AppState['rates']['rates'];
ratesError: State['ratesError']; ratesError: AppState['rates']['ratesError'];
showNotification: TShowNotification; showNotification: TShowNotification;
addCustomToken: TAddCustomToken; addCustomToken: TAddCustomToken;
removeCustomToken: TRemoveCustomToken; removeCustomToken: TRemoveCustomToken;
@ -67,12 +65,7 @@ export class BalanceSidebar extends React.Component<Props, {}> {
{ {
name: 'Account Info', name: 'Account Info',
content: ( content: (
<AccountInfo <AccountInfo wallet={wallet} balance={balance} network={network} />
wallet={wallet}
balance={balance}
network={network}
fetchCCRates={fetchCCRates}
/>
) )
}, },
{ {
@ -95,8 +88,10 @@ export class BalanceSidebar extends React.Component<Props, {}> {
content: ( content: (
<EquivalentValues <EquivalentValues
balance={balance} balance={balance}
tokenBalances={tokenBalances}
rates={rates} rates={rates}
ratesError={ratesError} ratesError={ratesError}
fetchCCRates={fetchCCRates}
/> />
) )
} }

View File

@ -1,6 +1,6 @@
import logo from 'assets/images/logo-myetherwallet.svg'; import logo from 'assets/images/logo-myetherwallet.svg';
import { bityReferralURL, donationAddressMap } from 'config/data'; import { bityReferralURL, donationAddressMap } from 'config/data';
import React, { Component } from 'react'; import React from 'react';
import translate from 'translations'; import translate from 'translations';
import './index.scss'; import './index.scss';
import PreFooter from './PreFooter'; import PreFooter from './PreFooter';
@ -92,11 +92,15 @@ const LINKS_SOCIAL = [
} }
]; ];
interface ComponentState { interface Props {
latestBlock: string;
};
interface State {
isOpen: boolean; isOpen: boolean;
} }
export default class Footer extends React.Component<{}, ComponentState> { export default class Footer extends React.Component<Props, State> {
constructor(props) { constructor(props) {
super(props); super(props);
this.state = { isOpen: false }; this.state = { isOpen: false };
@ -276,9 +280,7 @@ export default class Footer extends React.Component<{}, ComponentState> {
); );
})} })}
</p> </p>
<p>Latest Block#: {this.props.latestBlock}</p>
{/* TODO: Fix me */}
<p>Latest Block#: ?????</p>
</div> </div>
</footer> </footer>
</div> </div>

View File

@ -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<Props, State> {
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 (
<Modal
title={translate('NODE_Title')}
isOpen={true}
buttons={buttons}
handleClose={handleClose}
>
<div>
{isHttps &&
<div className="alert alert-danger small">
{translate('NODE_Warning')}
</div>
}
<form>
<div className="row">
<div className="col-sm-7">
<label>{translate('NODE_Name')}</label>
{this.renderInput({
name: 'name',
placeholder: 'My Node',
}, invalids)}
</div>
<div className="col-sm-5">
<label>Network</label>
<select
className="form-control"
name="network"
value={this.state.network}
onChange={this.handleChange}
>
{NETWORK_KEYS.map((net) =>
<option key={net} value={net}>{net}</option>
)}
</select>
</div>
</div>
<div className="row">
<div className="col-sm-9">
<label>URL</label>
{this.renderInput({
name: 'url',
placeholder: 'http://127.0.0.1/',
}, invalids)}
</div>
<div className="col-sm-3">
<label>{translate('NODE_Port')}</label>
{this.renderInput({
name: 'port',
placeholder: '8545',
type: 'number',
}, invalids)}
</div>
</div>
<div className="row">
<div className="col-sm-12">
<label>
<input
type="checkbox"
name="hasAuth"
checked={this.state.hasAuth}
onChange={this.handleCheckbox}
/>
{' '}
<span>HTTP Basic Authentication</span>
</label>
</div>
</div>
{this.state.hasAuth &&
<div className="row">
<div className="col-sm-6">
<label>Username</label>
{this.renderInput({ name: 'username' }, invalids)}
</div>
<div className="col-sm-6">
<label>Password</label>
{this.renderInput({
name: 'password',
type: 'password',
}, invalids)}
</div>
</div>
}
</form>
</div>
</Modal>
);
}
private renderInput(input: Input, invalids: { [key: string]: boolean }) {
return <input
className={classnames({
'form-control': true,
'is-invalid': this.state[input.name] && invalids[input.name],
})}
value={this.state[name]}
onChange={this.handleChange}
{...input}
/>;
}
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<HTMLInputElement>) => {
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);
};
}

View File

@ -1,4 +1,3 @@
import PropTypes from 'prop-types';
import React, { Component } from 'react'; import React, { Component } from 'react';
import NavigationLink from './NavigationLink'; import NavigationLink from './NavigationLink';
@ -21,10 +20,22 @@ const tabs = [
name: 'NAV_ViewWallet' name: 'NAV_ViewWallet'
// to: 'view-wallet' // to: 'view-wallet'
}, },
{
name: 'NAV_Contracts',
to: 'contracts'
},
{ {
name: 'NAV_ENS', name: 'NAV_ENS',
to: 'ens' to: 'ens'
}, },
{
name: 'Sign & Verify Message',
to: 'sign-and-verify-message'
},
{
name: 'Broadcast Transaction',
to: 'pushTx'
},
{ {
name: 'NAV_Help', name: 'NAV_Help',
to: 'https://myetherwallet.groovehq.com/help_center', to: 'https://myetherwallet.groovehq.com/help_center',
@ -54,7 +65,7 @@ export default class Navigation extends Component<Props, State> {
/* /*
* public scrollLeft() {} * public scrollLeft() {}
public scrollRight() {} public scrollRight() {}
* *
*/ */
public render() { public render() {

View File

@ -15,6 +15,15 @@ $small-size: 900px;
} }
} }
@keyframes dropdown-is-flashing {
0%, 100% {
opacity: 0.8;
}
50% {
opacity: 0.7;
}
}
// Header // Header
.Header { .Header {
margin-bottom: 2rem; margin-bottom: 2rem;
@ -124,6 +133,11 @@ $small-size: 900px;
padding-top: $space-sm !important; padding-top: $space-sm !important;
padding-bottom: $space-sm !important; padding-bottom: $space-sm !important;
} }
&.is-flashing {
pointer-events: none;
animation: dropdown-is-flashing 800ms ease infinite;
}
} }
} }
} }

View File

@ -1,11 +1,14 @@
import { import {
TChangeGasPrice, TChangeGasPrice,
TChangeLanguage, TChangeLanguage,
TChangeNodeIntent TChangeNodeIntent,
TAddCustomNode,
TRemoveCustomNode
} from 'actions/config'; } from 'actions/config';
import logo from 'assets/images/logo-myetherwallet.svg'; import logo from 'assets/images/logo-myetherwallet.svg';
import { Dropdown, ColorDropdown } from 'components/ui'; import { Dropdown, ColorDropdown } from 'components/ui';
import React, { Component } from 'react'; import React, { Component } from 'react';
import classnames from 'classnames';
import { Link } from 'react-router-dom'; import { Link } from 'react-router-dom';
import { import {
ANNOUNCEMENT_MESSAGE, ANNOUNCEMENT_MESSAGE,
@ -13,43 +16,85 @@ import {
languages, languages,
NETWORKS, NETWORKS,
NODES, NODES,
VERSION VERSION,
NodeConfig,
CustomNodeConfig
} from '../../config/data'; } from '../../config/data';
import GasPriceDropdown from './components/GasPriceDropdown'; import GasPriceDropdown from './components/GasPriceDropdown';
import Navigation from './components/Navigation'; import Navigation from './components/Navigation';
import CustomNodeModal from './components/CustomNodeModal';
import { getKeyByValue } from 'utils/helpers'; import { getKeyByValue } from 'utils/helpers';
import { makeCustomNodeId } from 'utils/node';
import './index.scss'; import './index.scss';
interface Props { interface Props {
languageSelection: string; languageSelection: string;
node: NodeConfig;
nodeSelection: string; nodeSelection: string;
isChangingNode: boolean;
gasPriceGwei: number; gasPriceGwei: number;
customNodes: CustomNodeConfig[];
changeLanguage: TChangeLanguage; changeLanguage: TChangeLanguage;
changeNodeIntent: TChangeNodeIntent; changeNodeIntent: TChangeNodeIntent;
changeGasPrice: TChangeGasPrice; changeGasPrice: TChangeGasPrice;
addCustomNode: TAddCustomNode;
removeCustomNode: TRemoveCustomNode;
} }
export default class Header extends Component<Props, {}> { interface State {
isAddingCustomNode: boolean;
}
export default class Header extends Component<Props, State> {
public state = {
isAddingCustomNode: false
};
public render() { 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 selectedLanguage = languageSelection;
const selectedNode = NODES[nodeSelection]; const selectedNetwork = NETWORKS[node.network];
const selectedNetwork = NETWORKS[selectedNode.network];
const LanguageDropDown = Dropdown as new () => Dropdown< const LanguageDropDown = Dropdown as new () => Dropdown<
typeof selectedLanguage typeof selectedLanguage
>; >;
const nodeOptions = Object.keys(NODES).map(key => {
return { const nodeOptions = Object.keys(NODES)
value: key, .map(key => {
name: ( return {
<span> value: key,
{NODES[key].network} <small>({NODES[key].service})</small> name: (
</span> <span>
), {NODES[key].network} <small>({NODES[key].service})</small>
color: NETWORKS[NODES[key].network].color </span>
}; ),
}); color: NETWORKS[NODES[key].network].color,
hidden: NODES[key].hidden
};
})
.concat(
customNodes.map(customNode => {
return {
value: makeCustomNodeId(customNode),
name: (
<span>
{customNode.network} - {customNode.name} <small>(custom)</small>
</span>
),
color: '#000',
hidden: false,
onRemove: () => this.props.removeCustomNode(customNode)
};
})
);
return ( return (
<div className="Header"> <div className="Header">
@ -65,7 +110,7 @@ export default class Header extends Component<Props, {}> {
<section className="Header-branding"> <section className="Header-branding">
<section className="Header-branding-inner container"> <section className="Header-branding-inner container">
<Link <Link
to={'/'} to="/"
className="Header-branding-title" className="Header-branding-title"
aria-label="Go to homepage" aria-label="Go to homepage"
> >
@ -90,9 +135,9 @@ export default class Header extends Component<Props, {}> {
<div className="Header-branding-right-dropdown"> <div className="Header-branding-right-dropdown">
<LanguageDropDown <LanguageDropDown
ariaLabel={`change language. current language ${languages[ ariaLabel={`change language. current language ${
selectedLanguage languages[selectedLanguage]
]}`} }`}
options={Object.values(languages)} options={Object.values(languages)}
value={languages[selectedLanguage]} value={languages[selectedLanguage]}
extra={ extra={
@ -108,19 +153,29 @@ export default class Header extends Component<Props, {}> {
/> />
</div> </div>
<div className="Header-branding-right-dropdown"> <div
className={classnames({
'Header-branding-right-dropdown': true,
'is-flashing': isChangingNode
})}
>
<ColorDropdown <ColorDropdown
ariaLabel={`change node. current node ${selectedNode.network} node by ${selectedNode.service}`} ariaLabel={`
change node. current node ${node.network}
node by ${node.service}
`}
options={nodeOptions} options={nodeOptions}
value={nodeSelection} value={nodeSelection}
extra={ extra={
<li> <li>
<a>Add Custom Node</a> <a onClick={this.openCustomNodeModal}>Add Custom Node</a>
</li> </li>
} }
disabled={nodeSelection === 'web3'}
onChange={changeNodeIntent} onChange={changeNodeIntent}
size="smr" size="smr"
color="white" color="white"
menuAlign="right"
/> />
</div> </div>
</div> </div>
@ -128,6 +183,13 @@ export default class Header extends Component<Props, {}> {
</section> </section>
<Navigation color={selectedNetwork.color} /> <Navigation color={selectedNetwork.color} />
{isAddingCustomNode && (
<CustomNodeModal
handleAddCustomNode={this.addCustomNode}
handleClose={this.closeCustomNodeModal}
/>
)}
</div> </div>
); );
} }
@ -138,4 +200,17 @@ export default class Header extends Component<Props, {}> {
this.props.changeLanguage(key); 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);
};
} }

View File

@ -1,5 +1,4 @@
import { Identicon, QRCode } from 'components/ui'; import { Identicon, QRCode } from 'components/ui';
import PrivKeyWallet from 'libs/wallet/privkey';
import React from 'react'; import React from 'react';
import ethLogo from 'assets/images/logo-ethereum-1.png'; import ethLogo from 'assets/images/logo-ethereum-1.png';
@ -91,26 +90,13 @@ const styles: any = {
}; };
interface Props { interface Props {
wallet: PrivKeyWallet;
}
interface State {
address: string; address: string;
privateKey: string;
} }
export default class PaperWallet extends React.Component<Props, State> {
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<Props, {}> {
public render() { public render() {
const privateKey = this.props.wallet.getPrivateKey(); const { privateKey, address } = this.props;
return ( return (
<div style={styles.container}> <div style={styles.container}>
@ -119,7 +105,7 @@ export default class PaperWallet extends React.Component<Props, State> {
<div style={styles.block}> <div style={styles.block}>
<div style={styles.box}> <div style={styles.box}>
<QRCode data={this.state.address} /> <QRCode data={address} />
</div> </div>
<p style={styles.blockText}>YOUR ADDRESS</p> <p style={styles.blockText}>YOUR ADDRESS</p>
</div> </div>
@ -140,7 +126,7 @@ export default class PaperWallet extends React.Component<Props, State> {
<p style={styles.infoText}> <p style={styles.infoText}>
<strong style={styles.infoLabel}>Your Address:</strong> <strong style={styles.infoLabel}>Your Address:</strong>
<br /> <br />
{this.state.address} {address}
</p> </p>
<p style={styles.infoText}> <p style={styles.infoText}>
<strong style={styles.infoLabel}>Your Private Key:</strong> <strong style={styles.infoLabel}>Your Private Key:</strong>
@ -151,7 +137,7 @@ export default class PaperWallet extends React.Component<Props, State> {
<div style={styles.identiconContainer}> <div style={styles.identiconContainer}>
<div style={{ float: 'left' }}> <div style={{ float: 'left' }}>
<Identicon address={this.state.address} size={'42px'} /> <Identicon address={address} size={'42px'} />
</div> </div>
<p style={styles.identiconText}> <p style={styles.identiconText}>
Always look for this icon when sending to this wallet Always look for this icon when sending to this wallet

View File

@ -1,49 +1,53 @@
import { PaperWallet } from 'components'; import { PaperWallet } from 'components';
import PrivKeyWallet from 'libs/wallet/privkey'; import { IFullWallet } from 'ethereumjs-wallet';
import React, { Component } from 'react'; import React from 'react';
import translate from 'translations'; import translate from 'translations';
import printElement from 'utils/printElement'; import printElement from 'utils/printElement';
interface Props { const print = (address: string, privateKey: string) => () =>
wallet: PrivKeyWallet; address &&
} privateKey &&
printElement(<PaperWallet address={address} privateKey={privateKey} />, {
popupFeatures: {
scrollbars: 'no'
},
styles: `
* {
box-sizing: border-box;
}
export default class PrintableWallet extends Component<Props, {}> { body {
public print = () => { font-family: Lato, sans-serif;
printElement(<PaperWallet wallet={this.props.wallet} />, { font-size: 1rem;
popupFeatures: { line-height: 1.4;
scrollbars: 'no' margin: 0;
}, }
styles: ` `
* { });
box-sizing: border-box;
}
body { const PrintableWallet: React.SFC<{ wallet: IFullWallet }> = ({ wallet }) => {
font-family: Lato, sans-serif; const address = wallet.getAddressString();
font-size: 1rem; const privateKey = wallet.getPrivateKeyString();
line-height: 1.4;
margin: 0;
}
`
});
};
public render() { if (!address || !privateKey) {
return ( return null;
<div>
<PaperWallet wallet={this.props.wallet} />
<a
role="button"
aria-label={translate('x_Print')}
aria-describedby="x_PrintDesc"
className={'btn btn-lg btn-primary'}
onClick={this.print}
style={{ marginTop: 10 }}
>
{translate('x_Print')}
</a>
</div>
);
} }
}
return (
<div>
<PaperWallet address={address} privateKey={privateKey} />
<a
role="button"
aria-label={translate('x_Print')}
aria-describedby="x_PrintDesc"
className={'btn btn-lg btn-primary'}
onClick={print(address, privateKey)}
style={{ marginTop: 10 }}
>
{translate('x_Print')}
</a>
</div>
);
};
export default PrintableWallet;

View File

@ -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<Props, {}> {
public render() {
const { store, history } = this.props;
// key={Math.random()} = hack for HMR from https://github.com/webpack/webpack-dev-server/issues/395
return (
<Provider store={store} key={Math.random()}>
<Router history={history} key={Math.random()}>
<div>
<Route exact={true} path="/" component={GenerateWallet} />
<Route path="/view-wallet" component={ViewWallet} />
<Route path="/help" component={Help} />
<Route path="/swap" component={Swap} />
<Route path="/send-transaction" component={SendTransaction} />
<Route path="/ens" component={ENS} />
</div>
</Router>
</Provider>
);
}
}

View File

@ -23,6 +23,7 @@
&-table { &-table {
width: 100%; width: 100%;
text-align: center; text-align: center;
margin-bottom: 10px;
&-token { &-token {
width: 82px; width: 82px;
@ -32,6 +33,10 @@
font-size: 13px; font-size: 13px;
text-align: left; text-align: left;
font-family: $font-family-monospace; font-family: $font-family-monospace;
input {
margin-right: 6px;
}
} }
&-more { &-more {

View File

@ -7,12 +7,14 @@ import {
SetDesiredTokenAction SetDesiredTokenAction
} from 'actions/deterministicWallets'; } from 'actions/deterministicWallets';
import Modal, { IButton } from 'components/ui/Modal'; 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 { isValidPath } from 'libs/validators';
import React from 'react'; import React from 'react';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import { getNetworkConfig } from 'selectors/config'; import { getNetworkConfig } from 'selectors/config';
import { getTokens, MergedToken } from 'selectors/wallet'; import { getTokens, MergedToken } from 'selectors/wallet';
import { UnitDisplay } from 'components/ui';
import './DeterministicWalletsModal.scss'; import './DeterministicWalletsModal.scss';
const WALLETS_PER_PAGE = 5; const WALLETS_PER_PAGE = 5;
@ -123,20 +125,21 @@ class DeterministicWalletsModal extends React.Component<Props, State> {
onChange={this.handleChangePath} onChange={this.handleChangePath}
value={isCustomPath ? 'custom' : dPath} value={isCustomPath ? 'custom' : dPath}
> >
{dPaths.map(dp => {dPaths.map(dp => (
<option key={dp.value} value={dp.value}> <option key={dp.value} value={dp.value}>
{dp.label} {dp.label}
</option> </option>
)} ))}
<option value="custom">Custom path...</option> <option value="custom">Custom path...</option>
</select> </select>
{isCustomPath && {isCustomPath && (
<input <input
className={`form-control ${validPathClass}`} className={`form-control ${validPathClass}`}
value={customPath} value={customPath}
placeholder="m/44'/60'/0'/0" placeholder="m/44'/60'/0'/0"
onChange={this.handleChangeCustomPath} onChange={this.handleChangeCustomPath}
/>} />
)}
</form> </form>
<div className="DWModal-addresses"> <div className="DWModal-addresses">
@ -145,9 +148,7 @@ class DeterministicWalletsModal extends React.Component<Props, State> {
<tr> <tr>
<td>#</td> <td>#</td>
<td>Address</td> <td>Address</td>
<td> <td>{network.unit}</td>
{network.unit}
</td>
<td> <td>
<select <select
className="DWModal-addresses-table-token" className="DWModal-addresses-table-token"
@ -155,11 +156,11 @@ class DeterministicWalletsModal extends React.Component<Props, State> {
onChange={this.handleChangeToken} onChange={this.handleChangeToken}
> >
<option value="">-Token-</option> <option value="">-Token-</option>
{tokens.map(t => {tokens.map(t => (
<option key={t.symbol} value={t.symbol}> <option key={t.symbol} value={t.symbol}>
{t.symbol} {t.symbol}
</option> </option>
)} ))}
</select> </select>
</td> </td>
<td>More</td> <td>More</td>
@ -265,24 +266,19 @@ class DeterministicWalletsModal extends React.Component<Props, State> {
); );
}; };
private renderWalletRow(wallet) { private renderWalletRow(wallet: DeterministicWalletData) {
const { desiredToken, network } = this.props; const { desiredToken, network } = this.props;
const { selectedAddress } = this.state; const { selectedAddress } = this.state;
// Get renderable values, but keep 'em short // Get renderable values, but keep 'em short
const value = wallet.value ? wallet.value.toEther().toPrecision(4) : ''; const token = wallet.tokenValues[desiredToken];
const tokenValue = wallet.tokenValues[desiredToken]
? wallet.tokenValues[desiredToken].toPrecision(4)
: '';
return ( return (
<tr <tr
key={wallet.address} key={wallet.address}
onClick={this.selectAddress.bind(this, wallet.address, wallet.index)} onClick={this.selectAddress.bind(this, wallet.address, wallet.index)}
> >
<td> <td>{wallet.index + 1}</td>
{wallet.index + 1}
</td>
<td className="DWModal-addresses-table-address"> <td className="DWModal-addresses-table-address">
<input <input
type="radio" type="radio"
@ -293,10 +289,24 @@ class DeterministicWalletsModal extends React.Component<Props, State> {
{wallet.address} {wallet.address}
</td> </td>
<td> <td>
{value} {network.unit} <UnitDisplay
unit={'ether'}
value={wallet.value}
symbol={network.unit}
displayShortBalance={true}
/>
</td> </td>
<td> <td>
{tokenValue} {desiredToken} {token ? (
<UnitDisplay
decimal={token.decimal}
value={token.value}
symbol={desiredToken}
displayShortBalance={true}
/>
) : (
'???'
)}
</td> </td>
<td> <td>
<a <a
@ -311,7 +321,7 @@ class DeterministicWalletsModal extends React.Component<Props, State> {
} }
} }
function mapStateToProps(state) { function mapStateToProps(state: AppState) {
return { return {
wallets: state.deterministicWallets.wallets, wallets: state.deterministicWallets.wallets,
desiredToken: state.deterministicWallets.desiredToken, desiredToken: state.deterministicWallets.desiredToken,

View File

@ -1,4 +1,4 @@
import { isKeystorePassRequired } from 'libs/keystore'; import { isKeystorePassRequired } from 'libs/wallet';
import React, { Component } from 'react'; import React, { Component } from 'react';
import translate, { translateRaw } from 'translations'; import translate, { translateRaw } from 'translations';
@ -32,9 +32,7 @@ export default class KeystoreDecrypt extends Component {
return ( return (
<section className="col-md-4 col-sm-6"> <section className="col-md-4 col-sm-6">
<div id="selectedUploadKey"> <div id="selectedUploadKey">
<h4> <h4>{translate('ADD_Radio_2_alt')}</h4>
{translate('ADD_Radio_2_alt')}
</h4>
<div className="form-group"> <div className="form-group">
<input <input
@ -54,13 +52,11 @@ export default class KeystoreDecrypt extends Component {
</a> </a>
</label> </label>
<div className={file.length && passReq ? '' : 'hidden'}> <div className={file.length && passReq ? '' : 'hidden'}>
<p> <p>{translate('ADD_Label_3')}</p>
{translate('ADD_Label_3')}
</p>
<input <input
className={`form-control ${password.length > 0 className={`form-control ${
? 'is-valid' password.length > 0 ? 'is-valid' : 'is-invalid'
: 'is-invalid'}`} }`}
value={password} value={password}
onChange={this.onPasswordChange} onChange={this.onPasswordChange}
onKeyDown={this.onKeyDown} onKeyDown={this.onKeyDown}

View File

@ -2,7 +2,7 @@ import './LedgerNano.scss';
import React, { Component } from 'react'; import React, { Component } from 'react';
import translate, { translateRaw } from 'translations'; import translate, { translateRaw } from 'translations';
import DeterministicWalletsModal from './DeterministicWalletsModal'; import DeterministicWalletsModal from './DeterministicWalletsModal';
import LedgerWallet from 'libs/wallet/ledger'; import { LedgerWallet } from 'libs/wallet';
import Ledger3 from 'vendor/ledger3'; import Ledger3 from 'vendor/ledger3';
import LedgerEth from 'vendor/ledger-eth'; import LedgerEth from 'vendor/ledger-eth';
import DPATHS from 'config/dpaths'; import DPATHS from 'config/dpaths';

View File

@ -31,9 +31,7 @@ export default class MnemonicDecrypt extends Component<Props, State> {
return ( return (
<section className="col-md-4 col-sm-6"> <section className="col-md-4 col-sm-6">
<div id="selectedTypeKey"> <div id="selectedTypeKey">
<h4> <h4>{translate('ADD_Radio_5')}</h4>
{translate('ADD_Radio_5')}
</h4>
<div className="form-group"> <div className="form-group">
<textarea <textarea
id="aria-private-key" id="aria-private-key"
@ -56,7 +54,7 @@ export default class MnemonicDecrypt extends Component<Props, State> {
type="password" type="password"
/> />
</div> </div>
{isValidMnemonic && {isValidMnemonic && (
<div className="form-group"> <div className="form-group">
<button <button
style={{ width: '100%' }} style={{ width: '100%' }}
@ -65,7 +63,8 @@ export default class MnemonicDecrypt extends Component<Props, State> {
> >
{translate('Choose Address')} {translate('Choose Address')}
</button> </button>
</div>} </div>
)}
</div> </div>
<DeterministicWalletsModal <DeterministicWalletsModal
@ -90,7 +89,7 @@ export default class MnemonicDecrypt extends Component<Props, State> {
this.setState({ phrase: (e.target as HTMLTextAreaElement).value }); this.setState({ phrase: (e.target as HTMLTextAreaElement).value });
}; };
public onDWModalOpen = (e: React.SyntheticEvent<HTMLButtonElement>) => { public onDWModalOpen = () => {
const { phrase, pass } = this.state; const { phrase, pass } = this.state;
if (!validateMnemonic(phrase)) { if (!validateMnemonic(phrase)) {

View File

@ -1,5 +1,5 @@
import DPATHS from 'config/dpaths'; import DPATHS from 'config/dpaths';
import TrezorWallet from 'libs/wallet/trezor'; import { TrezorWallet } from 'libs/wallet';
import React, { Component } from 'react'; import React, { Component } from 'react';
import translate, { translateRaw } from 'translations'; import translate, { translateRaw } from 'translations';
import TrezorConnect from 'vendor/trezor-connect'; import TrezorConnect from 'vendor/trezor-connect';
@ -125,7 +125,5 @@ export default class TrezorDecrypt extends Component<Props, State> {
this.props.onUnlock(new TrezorWallet(address, this.state.dPath, index)); this.props.onUnlock(new TrezorWallet(address, this.state.dPath, index));
}; };
private handleNullConnect(): void { private handleNullConnect = (): void => this.handleConnect();
return this.handleConnect();
}
} }

View File

@ -0,0 +1,26 @@
.Web3Decrypt {
text-align: center;
padding-top: 30px;
&-decrypt {
width: 100%;
}
&-help {
margin-top: 10px;
font-size: 13px;
}
&-error {
opacity: 0;
transition: none;
&.is-showing {
opacity: 1;
}
}
&-install {
margin-top: 10px;
}
}

View File

@ -0,0 +1,33 @@
import React, { Component } from 'react';
import translate from 'translations';
import { NewTabLink } from 'components/ui';
import './Web3.scss';
interface Props {
onUnlock(): void;
}
export default class Web3Decrypt extends Component<Props> {
public render() {
return (
<section className="Web3Decrypt col-md-4 col-sm-6">
<div>
<button
className="Web3Decrypt btn btn-primary btn-lg"
onClick={this.props.onUnlock}
>
{translate('ADD_MetaMask')}
</button>
</div>
<div>
<NewTabLink
className="Web3Decrypt-install btn btn-sm btn-default"
content={translate('Download MetaMask')}
href="https://chrome.google.com/webstore/detail/metamask/nkbihfbeogaeaoehlefnkodbefgpgknn?hl=en"
/>
</div>
</section>
);
}
}

View File

@ -5,7 +5,8 @@ import {
unlockMnemonic, unlockMnemonic,
UnlockMnemonicAction, UnlockMnemonicAction,
unlockPrivateKey, unlockPrivateKey,
UnlockPrivateKeyAction UnlockPrivateKeyAction,
unlockWeb3
} from 'actions/wallet'; } from 'actions/wallet';
import isEmpty from 'lodash/isEmpty'; import isEmpty from 'lodash/isEmpty';
import map from 'lodash/map'; import map from 'lodash/map';
@ -20,6 +21,7 @@ import PrivateKeyDecrypt, { PrivateKeyValue } from './PrivateKey';
import TrezorDecrypt from './Trezor'; import TrezorDecrypt from './Trezor';
import ViewOnlyDecrypt from './ViewOnly'; import ViewOnlyDecrypt from './ViewOnly';
import { AppState } from 'reducers'; import { AppState } from 'reducers';
import Web3Decrypt from './Web3';
const WALLETS = { const WALLETS = {
'keystore-file': { 'keystore-file': {
@ -63,6 +65,13 @@ const WALLETS = {
unlock: setWallet, unlock: setWallet,
disabled: false disabled: false
}, },
web3: {
lid: 'x_MetaMask',
component: Web3Decrypt,
initialParams: {},
unlock: unlockWeb3,
disabled: false
},
'view-only': { 'view-only': {
lid: 'View with Address Only', lid: 'View with Address Only',
component: ViewOnlyDecrypt, component: ViewOnlyDecrypt,

View File

@ -1,6 +1,5 @@
export { default as Header } from './Header'; export { default as Header } from './Header';
export { default as Footer } from './Footer'; export { default as Footer } from './Footer';
export { default as Root } from './Root';
export { default as BalanceSidebar } from './BalanceSidebar'; export { default as BalanceSidebar } from './BalanceSidebar';
export { default as PaperWallet } from './PaperWallet'; export { default as PaperWallet } from './PaperWallet';
export { default as AlphaAgreement } from './AlphaAgreement'; export { default as AlphaAgreement } from './AlphaAgreement';

View File

@ -0,0 +1,60 @@
import { toTokenBase } from 'libs/units';
import React, { Component } from 'react';
interface IChildren {
onUserInput: UnitConverter['onUserInput'];
convertedUnit: string;
}
interface IFakeEvent {
currentTarget: {
value: string;
};
}
export interface Props {
decimal: number;
children({ onUserInput, convertedUnit }: IChildren): React.ReactElement<any>;
onChange(baseUnit: IFakeEvent);
}
interface State {
userInput: string;
}
const initialState = { userInput: '' };
export class UnitConverter extends Component<Props, State> {
public state: State = initialState;
public componentWillReceiveProps(nextProps: Props) {
const { userInput } = this.state;
if (this.props.decimal !== nextProps.decimal) {
this.baseUnitCb(userInput, nextProps.decimal);
}
}
public onUserInput = (e: React.FormEvent<HTMLInputElement>) => {
const { value } = e.currentTarget;
const { decimal } = this.props;
this.baseUnitCb(value, decimal);
this.setState({ userInput: value });
};
public render() {
return this.props.children({
onUserInput: this.onUserInput,
convertedUnit: this.state.userInput
});
}
private baseUnitCb = (value: string, decimal: number) => {
const baseUnit = toTokenBase(value, decimal).toString();
const fakeEvent = {
currentTarget: {
value: baseUnit
}
};
this.props.onChange(fakeEvent);
};
}

View File

@ -0,0 +1 @@
export * from './UnitConverter';

View File

@ -0,0 +1,14 @@
pre {
color: #333;
background-color: #fafafa;
border: 1px solid #ececec;
border-radius: 0px;
padding: 8px;
code {
font-size: 14px;
line-height: 20px;
word-break: break-all;
word-wrap: break-word;
white-space: pre;
}
}

View File

@ -0,0 +1,10 @@
import React from 'react';
import './Code.scss';
const Code = ({ children }) => (
<pre>
<code>{children}</code>
</pre>
);
export default Code;

View File

@ -0,0 +1,23 @@
.ColorDropdown {
&-item {
position: relative;
padding-right: 10px;
border-left: 2px solid;
&-remove {
position: absolute;
top: 50%;
right: 5px;
width: 15px;
height: 15px;
opacity: 0.5;
cursor: pointer;
// Z fixes clipping issue
transform: translateY(-50%) translateZ(0);
&:hover {
opacity: 1;
}
}
}
}

View File

@ -1,11 +1,15 @@
import React, { Component } from 'react'; import React, { Component } from 'react';
import classnames from 'classnames'; import classnames from 'classnames';
import DropdownShell from './DropdownShell'; import DropdownShell from './DropdownShell';
import removeIcon from 'assets/images/icon-remove.svg';
import './ColorDropdown.scss';
interface Option<T> { interface Option<T> {
name: any; name: any;
value: T; value: T;
color?: string; color?: string;
hidden: boolean | undefined;
onRemove?(): void;
} }
interface Props<T> { interface Props<T> {
@ -17,6 +21,7 @@ interface Props<T> {
size?: string; size?: string;
color?: string; color?: string;
menuAlign?: string; menuAlign?: string;
disabled?: boolean;
onChange(value: T): void; onChange(value: T): void;
} }
@ -24,7 +29,7 @@ export default class ColorDropdown<T> extends Component<Props<T>, {}> {
private dropdownShell: DropdownShell | null; private dropdownShell: DropdownShell | null;
public render() { public render() {
const { ariaLabel, color, size } = this.props; const { ariaLabel, disabled, color, size } = this.props;
return ( return (
<DropdownShell <DropdownShell
@ -34,6 +39,7 @@ export default class ColorDropdown<T> extends Component<Props<T>, {}> {
color={color} color={color}
ariaLabel={ariaLabel} ariaLabel={ariaLabel}
ref={el => (this.dropdownShell = el)} ref={el => (this.dropdownShell = el)}
disabled={disabled}
/> />
); );
} }
@ -52,18 +58,19 @@ export default class ColorDropdown<T> extends Component<Props<T>, {}> {
private renderOptions = () => { private renderOptions = () => {
const { options, value, menuAlign, extra } = this.props; const { options, value, menuAlign, extra } = this.props;
const activeOption = this.getActiveOption(); const listItems = options
.filter(opt => !opt.hidden)
const listItems = options.reduce((prev: any[], opt) => { .reduce((prev: any[], opt) => {
const prevOpt = prev.length ? prev[prev.length - 1] : null; const prevOpt = prev.length ? prev[prev.length - 1] : null;
if (prevOpt && !prevOpt.divider && prevOpt.color !== opt.color) { if (prevOpt && !prevOpt.divider && prevOpt.color !== opt.color) {
prev.push({ divider: true }); prev.push({ divider: true });
} }
prev.push(opt); prev.push(opt);
return prev; return prev;
}, []); }, []);
const menuClass = classnames({ const menuClass = classnames({
ColorDropdown: true,
'dropdown-menu': true, 'dropdown-menu': true,
[`dropdown-menu-${menuAlign || ''}`]: !!menuAlign [`dropdown-menu-${menuAlign || ''}`]: !!menuAlign
}); });
@ -75,12 +82,24 @@ export default class ColorDropdown<T> extends Component<Props<T>, {}> {
return <li key={i} role="separator" className="divider" />; return <li key={i} role="separator" className="divider" />;
} else { } else {
return ( return (
<li key={i} style={{ borderLeft: `2px solid ${option.color}` }}> <li
key={i}
className="ColorDropdown-item"
style={{ borderColor: option.color }}
>
<a <a
className={option.value === value ? 'active' : ''} className={option.value === value ? 'active' : ''}
onClick={this.onChange.bind(null, option.value)} onClick={this.onChange.bind(null, option.value)}
> >
{option.name} {option.name}
{option.onRemove && (
<img
className="ColorDropdown-item-remove"
onClick={this.onRemove.bind(null, option.onRemove)}
src={removeIcon}
/>
)}
</a> </a>
</li> </li>
); );
@ -99,6 +118,17 @@ export default class ColorDropdown<T> extends Component<Props<T>, {}> {
} }
}; };
private onRemove(
onRemove: () => void,
ev?: React.SyntheticEvent<HTMLButtonElement>
) {
if (ev) {
ev.preventDefault();
ev.stopPropagation();
}
onRemove();
}
private getActiveOption() { private getActiveOption() {
return this.props.options.find(opt => opt.value === this.props.value); return this.props.options.find(opt => opt.value === this.props.value);
} }

View File

@ -34,7 +34,7 @@ export default class DropdownComponent<T> extends Component<Props<T>, {}> {
} }
private renderLabel = () => { private renderLabel = () => {
const { label, value } = this.props; const { value } = this.props;
const labelStr = this.props.label ? `${this.props.label}:` : ''; const labelStr = this.props.label ? `${this.props.label}:` : '';
return ( return (
<span> <span>

View File

@ -3,6 +3,7 @@ import classnames from 'classnames';
interface Props { interface Props {
ariaLabel: string; ariaLabel: string;
disabled?: boolean;
size?: string; size?: string;
color?: string; color?: string;
renderLabel(): any; renderLabel(): any;
@ -34,7 +35,14 @@ export default class DropdownComponent extends Component<Props, State> {
} }
public render() { public render() {
const { ariaLabel, color, size, renderOptions, renderLabel } = this.props; const {
ariaLabel,
color,
disabled,
size,
renderOptions,
renderLabel
} = this.props;
const { expanded } = this.state; const { expanded } = this.state;
const toggleClasses = classnames([ const toggleClasses = classnames([
'dropdown-toggle', 'dropdown-toggle',
@ -45,7 +53,7 @@ export default class DropdownComponent extends Component<Props, State> {
return ( return (
<span <span
className={`dropdown ${expanded ? 'open' : ''}`} className={`dropdown ${expanded || disabled ? 'open' : ''}`}
ref={el => (this.dropdown = el)} ref={el => (this.dropdown = el)}
> >
<a <a
@ -57,9 +65,9 @@ export default class DropdownComponent extends Component<Props, State> {
onClick={this.toggle} onClick={this.toggle}
> >
{renderLabel()} {renderLabel()}
<i className="caret" /> {!disabled && <i className="caret" />}
</a> </a>
{expanded && renderOptions()} {expanded && !disabled && renderOptions()}
</span> </span>
); );
} }

View File

@ -1,6 +1,5 @@
import React from 'react'; import React from 'react';
import helpIcon from 'assets/images/icon-help.svg'; import helpIcon from 'assets/images/icon-help.svg';
import translate, { translateRaw } from 'translations';
type sizeType = 'small' | 'medium' | 'large'; type sizeType = 'small' | 'medium' | 'large';

View File

@ -3,7 +3,7 @@ import React, { Component } from 'react';
import './Modal.scss'; import './Modal.scss';
export interface IButton { export interface IButton {
text: string; text: string | React.ReactElement<string>;
type?: type?:
| 'default' | 'default'
| 'primary' | 'primary'
@ -17,7 +17,7 @@ export interface IButton {
} }
interface Props { interface Props {
isOpen?: boolean; isOpen?: boolean;
title: string; title: string | React.ReactElement<any>;
disableButtons?: boolean; disableButtons?: boolean;
children: any; children: any;
buttons: IButton[]; buttons: IButton[];

View File

@ -2,6 +2,7 @@ import React from 'react';
interface AAttributes { interface AAttributes {
charset?: string; charset?: string;
className?: string;
coords?: string; coords?: string;
download?: string; download?: string;
href: string; href: string;
@ -28,14 +29,15 @@ interface AAttributes {
type?: string; type?: string;
} }
interface NewTabLinkProps extends AAttributes { interface NewTabLinkProps extends AAttributes {
content?: React.ReactElement<any> | string; content?: React.ReactElement<any> | string;
children?: React.ReactElement<any> | string; children?: React.ReactElement<any> | string;
} }
const NewTabLink = ({ content, children, ...rest }: NewTabLinkProps) => const NewTabLink = ({ content, children, ...rest }: NewTabLinkProps) => (
<a target="_blank" rel="noopener" {...rest}> <a target="_blank" rel="noopener" {...rest}>
{content || children} {/* Keep content for short-hand text insertion */} {content || children} {/* Keep content for short-hand text insertion */}
</a>; </a>
);
export default NewTabLink; export default NewTabLink;

View File

@ -0,0 +1,66 @@
.Spinner {
animation: rotate 2s linear infinite;
&-x1 {
height: 1em;
width: 1em;
}
&-x2 {
height: 2em;
width: 2em;
}
&-x3 {
height: 3em;
width: 3em;
}
&-x4 {
height: 4em;
width: 4em;
}
&-x5 {
height: 5em;
width: 5em;
}
& .path {
stroke-linecap: round;
animation: dash 1.5s ease-in-out infinite;
}
&-light {
& .path {
stroke: white;
}
}
&-dark {
& .path {
stroke: #163151;
}
}
}
@keyframes rotate {
100% {
transform: rotate(360deg);
}
}
@keyframes dash {
0% {
stroke-dasharray: 1, 150;
stroke-dashoffset: 0;
}
50% {
stroke-dasharray: 90, 150;
stroke-dashoffset: -35;
}
100% {
stroke-dasharray: 90, 150;
stroke-dashoffset: -124;
}
}

View File

@ -1,13 +1,27 @@
import React from 'react'; import React from 'react';
import './Spinner.scss';
type Size = 'lg' | '2x' | '3x' | '4x' | '5x'; type Size = 'x1' | 'x2' | 'x3' | 'x4' | 'x5';
interface SpinnerProps { interface SpinnerProps {
size?: Size; size?: Size;
light?: boolean;
} }
const Spinner = ({ size = 'fa-' }: SpinnerProps) => { const Spinner = ({ size = 'x1', light = false }: SpinnerProps) => {
return <i className={`fa fa-spinner fa-spin fa-${size ? size : 'fw'}`} />; const color = light ? 'Spinner-light' : 'Spinner-dark';
return (
<svg className={`Spinner Spinner-${size} ${color}`} viewBox="0 0 50 50">
<circle
className="path"
cx="25"
cy="25"
r="20"
fill="none"
strokeWidth="5"
/>
</svg>
);
}; };
export default Spinner; export default Spinner;

View File

@ -0,0 +1,73 @@
import React from 'react';
import {
fromTokenBase,
getDecimal,
UnitKey,
Wei,
TokenValue
} from 'libs/units';
import { formatNumber as format } from 'utils/formatters';
interface Props {
/**
* @description base value of the token / ether, incase of waiting for API calls, we can return '???'
* @type {TokenValue | Wei}
* @memberof Props
*/
value?: TokenValue | Wei | null;
/**
* @description Symbol to display to the right of the value, such as 'ETH'
* @type {string}
* @memberof Props
*/
symbol?: string;
/**
* @description display the long balance, if false, trims it to 3 decimal places, if a number is specified then that number is the number of digits to be displayed.
* @type {boolean}
* @memberof Props
*/
displayShortBalance?: boolean | number;
}
interface EthProps extends Props {
unit: UnitKey;
}
interface TokenProps extends Props {
decimal: number;
}
const isEthereumUnit = (param: EthProps | TokenProps): param is EthProps =>
!!(param as EthProps).unit;
const UnitDisplay: React.SFC<EthProps | TokenProps> = params => {
const { value, symbol, displayShortBalance } = params;
if (!value) {
return <span>Balance isn't available offline</span>;
}
const convertedValue = isEthereumUnit(params)
? fromTokenBase(value, getDecimal(params.unit))
: fromTokenBase(value, params.decimal);
let formattedValue;
if (displayShortBalance) {
const digits =
typeof displayShortBalance === 'number' && displayShortBalance;
formattedValue = digits
? format(convertedValue, digits)
: format(convertedValue);
} else {
formattedValue = convertedValue;
}
return (
<span>
{formattedValue}
{symbol ? ` ${symbol}` : ''}
</span>
);
};
export default UnitDisplay;

View File

@ -6,3 +6,4 @@ export { default as Modal } from './Modal';
export { default as UnlockHeader } from './UnlockHeader'; export { default as UnlockHeader } from './UnlockHeader';
export { default as QRCode } from './QRCode'; export { default as QRCode } from './QRCode';
export { default as NewTabLink } from './NewTabLink'; export { default as NewTabLink } from './NewTabLink';
export { default as UnitDisplay } from './UnitDisplay';

View File

@ -1,7 +1,8 @@
import { EtherscanNode, InfuraNode, RPCNode } from 'libs/nodes'; import { EtherscanNode, InfuraNode, RPCNode, Web3Node } from 'libs/nodes';
import { networkIdToName } from 'libs/values';
export const languages = require('./languages.json'); export const languages = require('./languages.json');
// Displays in the header // Displays in the header
export const VERSION = '4.0.0 (Alpha 0.0.3)'; export const VERSION = '4.0.0 (Alpha 0.0.4)';
// Displays at the top of the site, make message empty string to remove. // Displays at the top of the site, make message empty string to remove.
// Type can be primary, warning, danger, success, or info. // Type can be primary, warning, danger, success, or info.
@ -74,9 +75,21 @@ export interface NetworkConfig {
export interface NodeConfig { export interface NodeConfig {
network: string; network: string;
lib: RPCNode; lib: RPCNode | Web3Node;
service: string; service: string;
estimateGas?: boolean; estimateGas?: boolean;
hidden?: boolean;
}
export interface CustomNodeConfig {
name: string;
url: string;
port: number;
network: string;
auth?: {
username: string;
password: string;
};
} }
// Must be a website that follows the ethplorer convention of /tx/[hash] and // Must be a website that follows the ethplorer convention of /tx/[hash] and
@ -242,3 +255,44 @@ export const NODES: { [key: string]: NodeConfig } = {
estimateGas: true estimateGas: true
} }
}; };
export function initWeb3Node(): Promise<void> {
return new Promise((resolve, reject) => {
const { web3 } = window as any;
if (!web3) {
return reject(
new Error(
'Web3 not found. Please check that MetaMask is installed, or that MyEtherWallet is open in Mist.'
)
);
}
if (web3.version.network === 'loading') {
return reject(
new Error(
'MetaMask / Mist is still loading. Please refresh the page and try again.'
)
);
}
web3.version.getNetwork((err, networkId) => {
if (err) {
return reject(err);
}
try {
NODES.web3 = {
network: networkIdToName(networkId),
service: 'MetaMask / Mist',
lib: new Web3Node(web3),
estimateGas: false,
hidden: true
};
resolve();
} catch (err) {
reject(err);
}
});
});
}

View File

@ -142,3 +142,25 @@
} }
} }
} }
.NotificationAnimation{
&-enter {
opacity: 0.01;
&-active {
opacity: 1;
transition: opacity 500ms;
}
}
}
.NotificationAnimation{
&-exit {
opacity: 1;
transform: none;
&-active {
opacity: 0.01;
transform: translateY(100%);
transition: opacity 500ms, transform 500ms;
}
}
}

View File

@ -5,6 +5,7 @@ import {
} from 'actions/notifications'; } from 'actions/notifications';
import React from 'react'; import React from 'react';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import { TransitionGroup, CSSTransition } from 'react-transition-group';
import NotificationRow from './NotificationRow'; import NotificationRow from './NotificationRow';
import './Notifications.scss'; import './Notifications.scss';
@ -12,21 +13,30 @@ interface Props {
notifications: Notification[]; notifications: Notification[];
closeNotification: TCloseNotification; closeNotification: TCloseNotification;
} }
const Transition = props => (
<CSSTransition
{...props}
classNames="NotificationAnimation"
timeout={{ enter: 500, exit: 500 }}
/>
);
export class Notifications extends React.Component<Props, {}> { export class Notifications extends React.Component<Props, {}> {
public render() { public render() {
if (!this.props.notifications.length) {
return null;
}
return ( return (
<div className="Notifications"> <TransitionGroup className="Notifications">
{this.props.notifications.map((n, i) => ( {this.props.notifications.map(n => {
<NotificationRow return (
key={`${n.level}-${i}`} <Transition key={n.id}>
notification={n} <NotificationRow
onClose={this.props.closeNotification} notification={n}
/> onClose={this.props.closeNotification}
))} />
</div> </Transition>
);
})}
</TransitionGroup>
); );
} }
} }

View File

@ -2,50 +2,72 @@ import {
changeGasPrice as dChangeGasPrice, changeGasPrice as dChangeGasPrice,
changeLanguage as dChangeLanguage, changeLanguage as dChangeLanguage,
changeNodeIntent as dChangeNodeIntent, changeNodeIntent as dChangeNodeIntent,
addCustomNode as dAddCustomNode,
removeCustomNode as dRemoveCustomNode,
TChangeGasPrice, TChangeGasPrice,
TChangeLanguage, TChangeLanguage,
TChangeNodeIntent TChangeNodeIntent,
TAddCustomNode,
TRemoveCustomNode,
} from 'actions/config'; } from 'actions/config';
import { AlphaAgreement, Footer, Header } from 'components'; import { AlphaAgreement, Footer, Header } from 'components';
import React, { Component } from 'react'; import React, { Component } from 'react';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import { AppState } from 'reducers'; import { AppState } from 'reducers';
import Notifications from './Notifications'; import Notifications from './Notifications';
import { NodeConfig, CustomNodeConfig } from 'config/data';
interface Props { interface Props {
// FIXME // FIXME
children: any; children: any;
languageSelection: string; languageSelection: string;
node: NodeConfig;
nodeSelection: string; nodeSelection: string;
isChangingNode: boolean;
gasPriceGwei: number; gasPriceGwei: number;
customNodes: CustomNodeConfig[];
latestBlock: string;
changeLanguage: TChangeLanguage; changeLanguage: TChangeLanguage;
changeNodeIntent: TChangeNodeIntent; changeNodeIntent: TChangeNodeIntent;
changeGasPrice: TChangeGasPrice; changeGasPrice: TChangeGasPrice;
addCustomNode: TAddCustomNode;
removeCustomNode: TRemoveCustomNode;
} }
class TabSection extends Component<Props, {}> { class TabSection extends Component<Props, {}> {
public render() { public render() {
const { const {
children, children,
// APP // APP
node,
nodeSelection, nodeSelection,
isChangingNode,
languageSelection, languageSelection,
gasPriceGwei, gasPriceGwei,
customNodes,
latestBlock,
changeLanguage, changeLanguage,
changeNodeIntent, changeNodeIntent,
changeGasPrice changeGasPrice,
addCustomNode,
removeCustomNode,
} = this.props; } = this.props;
const headerProps = { const headerProps = {
languageSelection, languageSelection,
node,
nodeSelection, nodeSelection,
isChangingNode,
gasPriceGwei, gasPriceGwei,
customNodes,
changeLanguage, changeLanguage,
changeNodeIntent, changeNodeIntent,
changeGasPrice changeGasPrice,
addCustomNode,
removeCustomNode,
}; };
return ( return (
@ -53,7 +75,7 @@ class TabSection extends Component<Props, {}> {
<main> <main>
<Header {...headerProps} /> <Header {...headerProps} />
<div className="Tab container">{children}</div> <div className="Tab container">{children}</div>
<Footer /> <Footer latestBlock={latestBlock} />
</main> </main>
<Notifications /> <Notifications />
<AlphaAgreement /> <AlphaAgreement />
@ -64,14 +86,20 @@ class TabSection extends Component<Props, {}> {
function mapStateToProps(state: AppState) { function mapStateToProps(state: AppState) {
return { return {
node: state.config.node,
nodeSelection: state.config.nodeSelection, nodeSelection: state.config.nodeSelection,
isChangingNode: state.config.isChangingNode,
languageSelection: state.config.languageSelection, languageSelection: state.config.languageSelection,
gasPriceGwei: state.config.gasPriceGwei gasPriceGwei: state.config.gasPriceGwei,
customNodes: state.config.customNodes,
latestBlock: state.config.latestBlock,
}; };
} }
export default connect(mapStateToProps, { export default connect(mapStateToProps, {
changeGasPrice: dChangeGasPrice, changeGasPrice: dChangeGasPrice,
changeLanguage: dChangeLanguage, changeLanguage: dChangeLanguage,
changeNodeIntent: dChangeNodeIntent changeNodeIntent: dChangeNodeIntent,
addCustomNode: dAddCustomNode,
removeCustomNode: dRemoveCustomNode,
})(TabSection); })(TabSection);

View File

@ -0,0 +1,7 @@
@import "common/sass/variables";
.BroadcastTx {
&-title {
margin: $space auto $space * 2.5;
}
}

View File

@ -0,0 +1,140 @@
import React, { Component } from 'react';
import { connect } from 'react-redux';
import { AppState } from 'reducers';
import TabSection from 'containers/TabSection';
import { translateRaw } from 'translations';
import { broadcastTx as dBroadcastTx, TBroadcastTx } from 'actions/wallet';
import { QRCode } from 'components/ui';
import './index.scss';
import {
BroadcastTransactionStatus,
getTransactionFields
} from 'libs/transaction';
import EthTx from 'ethereumjs-tx';
import { ConfirmationModal } from 'containers/Tabs/SendTransaction/components';
import classnames from 'classnames';
interface Props {
broadcastTx: TBroadcastTx;
transactions: BroadcastTransactionStatus[];
}
interface State {
signedTx: string;
showConfirmationModal: boolean;
disabled: boolean;
}
const initialState: State = {
showConfirmationModal: false,
signedTx: '',
disabled: true
};
class BroadcastTx extends Component<Props, State> {
public state = initialState;
public ensureValidSignedTxInputOnUpdate() {
try {
const tx = new EthTx(this.state.signedTx);
getTransactionFields(tx);
if (this.state.disabled) {
this.setState({ disabled: false });
}
} catch (e) {
if (!this.state.disabled) {
this.setState({ disabled: true });
}
}
}
public componentDidUpdate() {
this.ensureValidSignedTxInputOnUpdate();
}
public render() {
const { signedTx, disabled, showConfirmationModal } = this.state;
const inputClasses = classnames({
'form-control': true,
'is-valid': !disabled,
'is-invalid': disabled
});
return (
<TabSection>
<div className="Tab-content-pane row block text-center">
<div className="col-md-6">
<div className="col-md-12 BroadcastTx-title">
<h2>Broadcast Signed Transaction</h2>
</div>
<p>
Paste a signed transaction and press the "SEND TRANSACTION"
button.
</p>
<label>{translateRaw('SEND_signed')}</label>
<textarea
className={inputClasses}
rows={7}
value={signedTx}
onChange={this.handleChange}
/>
<button
className="btn btn-primary"
disabled={disabled || signedTx === ''}
onClick={this.handleBroadcastTx}
>
{translateRaw('SEND_trans')}
</button>
</div>
<div className="col-md-6" style={{ marginTop: '70px' }}>
<div
className="qr-code text-center"
style={{
maxWidth: '15rem',
margin: '1rem auto',
width: '100%'
}}
>
{signedTx && <QRCode data={signedTx} />}
</div>
</div>
</div>
{showConfirmationModal && (
<ConfirmationModal
signedTx={signedTx}
onClose={this.handleClose}
onConfirm={this.handleConfirm}
/>
)}
</TabSection>
);
}
public handleClose = () => {
this.setState({ showConfirmationModal: false });
};
public handleBroadcastTx = () => {
this.setState({ showConfirmationModal: true });
};
public handleConfirm = () => {
this.props.broadcastTx(this.state.signedTx);
};
protected handleChange = event => {
this.setState({ signedTx: event.target.value });
};
}
function mapStateToProps(state: AppState) {
return {
transactions: state.wallet.transactions
};
}
export default connect(mapStateToProps, { broadcastTx: dBroadcastTx })(
BroadcastTx
);

View File

@ -0,0 +1,150 @@
import BN from 'bn.js';
import { Wei } from 'libs/units';
import React, { Component } from 'react';
import {
generateCompleteTransaction as makeAndSignTx,
TransactionInput
} from 'libs/transaction';
import { Props, State, initialState } from './types';
import {
TxModal,
Props as DMProps,
TTxModal
} from 'containers/Tabs/Contracts/components/TxModal';
import {
TxCompare,
TTxCompare
} from 'containers/Tabs/Contracts/components/TxCompare';
import { withTx } from 'containers/Tabs/Contracts/components//withTx';
import { Props as DProps } from '../../';
export const deployHOC = PassedComponent => {
class WrappedComponent extends Component<Props, State> {
public state: State = initialState;
public asyncSetState = value =>
new Promise(resolve => this.setState(value, resolve));
public resetState = () => this.setState(initialState);
public handleSignTx = async () => {
const { props, state } = this;
if (state.data === '') {
return;
}
try {
await this.getAddressAndNonce();
await this.makeSignedTxFromState();
} catch (e) {
props.showNotification(
'danger',
e.message || 'Error during contract tx generation',
5000
);
return this.resetState();
}
};
public handleInput = inputName => (
ev: React.FormEvent<HTMLTextAreaElement | HTMLInputElement>
): void => {
if (this.state.signedTx) {
this.resetState();
}
this.setState({
[inputName]: ev.currentTarget.value
});
};
public handleDeploy = () => this.setState({ displayModal: true });
public render() {
const { data: byteCode, gasLimit, signedTx, displayModal } = this.state;
const props: DProps = {
handleInput: this.handleInput,
handleSignTx: this.handleSignTx,
handleDeploy: this.handleDeploy,
byteCode,
gasLimit,
displayModal,
walletExists: !!this.props.wallet,
txCompare: signedTx ? this.displayCompareTx() : null,
deployModal: signedTx ? this.displayDeployModal() : null
};
return <PassedComponent {...props} />;
}
private displayCompareTx = (): React.ReactElement<TTxCompare> => {
const { signedTx, nonce } = this.state;
if (!nonce || !signedTx) {
throw Error('Can not display raw tx, nonce empty or no signed tx');
}
return <TxCompare signedTx={signedTx} />;
};
private displayDeployModal = (): React.ReactElement<TTxModal> => {
const { networkName, node: { network, service } } = this.props;
const { signedTx } = this.state;
if (!signedTx) {
throw Error('Can not deploy contract, no signed tx');
}
const props: DMProps = {
action: 'deploy a contract',
networkName,
network,
service,
handleBroadcastTx: this.handleBroadcastTx,
onClose: this.resetState
};
return <TxModal {...props} />;
};
private handleBroadcastTx = () => {
if (!this.state.signedTx) {
throw Error('Can not broadcast tx, signed tx does not exist');
}
this.props.broadcastTx(this.state.signedTx);
this.resetState();
};
private makeSignedTxFromState = () => {
const { props, state: { data, gasLimit, value, to } } = this;
const transactionInput: TransactionInput = {
unit: 'ether',
to,
data,
value
};
return makeAndSignTx(
props.wallet,
props.nodeLib,
props.gasPrice,
Wei(gasLimit),
props.chainId,
transactionInput,
true
).then(({ signedTx }) => this.asyncSetState({ signedTx }));
};
private getAddressAndNonce = async () => {
const address = await this.props.wallet.getAddressString();
const nonce = await this.props.nodeLib
.getTransactionCount(address)
.then(n => new BN(n).toString());
return this.asyncSetState({ nonce, address });
};
}
return withTx(WrappedComponent);
};

View File

@ -0,0 +1,42 @@
import { Wei } from 'libs/units';
import { IWallet, Balance } from 'libs/wallet';
import { RPCNode } from 'libs/nodes';
import { NodeConfig, NetworkConfig } from 'config/data';
import { TBroadcastTx } from 'actions/wallet';
import { TShowNotification } from 'actions/notifications';
export interface Props {
wallet: IWallet;
balance: Balance;
node: NodeConfig;
nodeLib: RPCNode;
chainId: NetworkConfig['chainId'];
networkName: NetworkConfig['name'];
gasPrice: Wei;
broadcastTx: TBroadcastTx;
showNotification: TShowNotification;
}
export interface State {
data: string;
gasLimit: string;
determinedContractAddress: string;
signedTx: null | string;
nonce: null | string;
address: null | string;
value: string;
to: string;
displayModal: boolean;
}
export const initialState: State = {
data: '',
gasLimit: '300000',
determinedContractAddress: '',
signedTx: null,
nonce: null,
address: null,
to: '0x',
value: '0x0',
displayModal: false
};

View File

@ -0,0 +1,100 @@
import React from 'react';
import translate from 'translations';
import WalletDecrypt from 'components/WalletDecrypt';
import { deployHOC } from './components/DeployHoc';
import { TTxCompare } from '../TxCompare';
import { TTxModal } from '../TxModal';
import classnames from 'classnames';
import { isValidGasPrice, isValidByteCode } from 'libs/validators';
export interface Props {
byteCode: string;
gasLimit: string;
walletExists: boolean;
txCompare: React.ReactElement<TTxCompare> | null;
displayModal: boolean;
deployModal: React.ReactElement<TTxModal> | null;
handleInput(
input: string
): (ev: React.FormEvent<HTMLTextAreaElement | HTMLInputElement>) => void;
handleSignTx(): Promise<void>;
handleDeploy(): void;
}
const Deploy = (props: Props) => {
const {
handleSignTx,
handleInput,
handleDeploy,
byteCode,
gasLimit,
walletExists,
deployModal,
displayModal,
txCompare
} = props;
const validByteCode = isValidByteCode(byteCode);
const validGasLimit = isValidGasPrice(gasLimit);
const showSignTxButton = validByteCode && validGasLimit;
return (
<div className="Deploy">
<section>
<label className="Deploy-field form-group">
<h4 className="Deploy-field-label">
{translate('CONTRACT_ByteCode')}
</h4>
<textarea
name="byteCode"
placeholder="0x8f87a973e..."
rows={6}
onChange={handleInput('data')}
className={classnames('Deploy-field-input', 'form-control', {
'is-invalid': !validByteCode
})}
value={byteCode || ''}
/>
</label>
<label className="Deploy-field form-group">
<h4 className="Deploy-field-label">Gas Limit</h4>
<input
name="gasLimit"
value={gasLimit || ''}
onChange={handleInput('gasLimit')}
className={classnames('Deploy-field-input', 'form-control', {
'is-invalid': !validGasLimit
})}
/>
</label>
{walletExists ? (
<button
className="Sign-submit btn btn-primary"
disabled={!showSignTxButton}
onClick={handleSignTx}
>
{translate('DEP_signtx')}
</button>
) : (
<WalletDecrypt />
)}
{txCompare ? (
<section>
{txCompare}
<button
className="Deploy-submit btn btn-primary"
onClick={handleDeploy}
>
{translate('NAV_DeployContract')}
</button>
</section>
) : null}
{displayModal && deployModal}
</section>
</div>
);
};
export default deployHOC(Deploy);

View File

@ -0,0 +1,32 @@
@import 'common/sass/variables';
.InteractExplorer {
&-title {
&-address {
margin-left: 6px;
font-weight: 300;
opacity: 0.6;
}
}
&-func {
&-in,
&-out {
&-label {
&-type {
margin-left: 5px;
font-weight: 300;
opacity: 0.6;
}
}
}
&-in {
margin-right: 2rem;
}
&-out {
margin-left: 2rem;
}
}
}

View File

@ -0,0 +1,287 @@
import React, { Component } from 'react';
import translate from 'translations';
import './InteractExplorer.scss';
import Contract from 'libs/contracts';
import { TTxModal } from 'containers/Tabs/Contracts/components/TxModal';
import { TTxCompare } from 'containers/Tabs/Contracts/components/TxCompare';
import WalletDecrypt from 'components/WalletDecrypt';
import { TShowNotification } from 'actions/notifications';
import classnames from 'classnames';
import { isValidGasPrice, isValidValue } from 'libs/validators';
import { UnitConverter } from 'components/renderCbs';
import { getDecimal } from 'libs/units';
export interface Props {
contractFunctions: any;
walletDecrypted: boolean;
address: Contract['address'];
gasLimit: string;
value: string;
txGenerated: boolean;
txModal: React.ReactElement<TTxModal> | null;
txCompare: React.ReactElement<TTxCompare> | null;
displayModal: boolean;
showNotification: TShowNotification;
toggleModal(): void;
handleInput(name: string): (ev) => void;
handleFunctionSend(selectedFunction, inputs): () => void;
}
interface State {
inputs: {
[key: string]: { rawData: string; parsedData: string[] | string };
};
outputs;
selectedFunction: null | any;
selectedFunctionName: string;
}
export default class InteractExplorer extends Component<Props, State> {
public static defaultProps: Partial<Props> = {
contractFunctions: {}
};
public state: State = {
selectedFunction: null,
selectedFunctionName: '',
inputs: {},
outputs: {}
};
public render() {
const {
inputs,
outputs,
selectedFunction,
selectedFunctionName
} = this.state;
const {
address,
displayModal,
handleInput,
handleFunctionSend,
gasLimit,
txGenerated,
txCompare,
txModal,
toggleModal,
value,
walletDecrypted
} = this.props;
const validValue = isValidValue(value);
const validGasLimit = isValidGasPrice(gasLimit);
const showContractWrite = validValue && validGasLimit;
return (
<div className="InteractExplorer">
<h3 className="InteractExplorer-title">
{translate('CONTRACT_Interact_Title')}
<span className="InteractExplorer-title-address">{address}</span>
</h3>
<select
value={selectedFunction ? selectedFunction.name : ''}
className="InteractExplorer-fnselect form-control"
onChange={this.handleFunctionSelect}
>
<option>{translate('CONTRACT_Interact_CTA', true)}</option>
{this.contractOptions()}
</select>
{selectedFunction && (
<div key={selectedFunctionName} className="InteractExplorer-func">
{/* TODO: Use reusable components with validation */}
{selectedFunction.inputs.map(input => {
const { type, name } = input;
return (
<label
key={name}
className="InteractExplorer-func-in form-group"
>
<h4 className="InteractExplorer-func-in-label">
{name}
<span className="InteractExplorer-func-in-label-type">
{type}
</span>
</h4>
<input
className="InteractExplorer-func-in-input form-control"
name={name}
value={(inputs[name] && inputs[name].rawData) || ''}
onChange={this.handleInputChange}
/>
</label>
);
})}
{selectedFunction.outputs.map((output, index) => {
const { type, name } = output;
const parsedName = name === '' ? index : name;
return (
<label
key={parsedName}
className="InteractExplorer-func-out form-group"
>
<h4 className="InteractExplorer-func-out-label">
{name}
<span className="InteractExplorer-func-out-label-type">
{type}
</span>
</h4>
<input
className="InteractExplorer-func-out-input form-control"
value={outputs[parsedName] || ''}
disabled={true}
/>
</label>
);
})}
{selectedFunction.constant ? (
<button
className="InteractExplorer-func-submit btn btn-primary"
onClick={this.handleFunctionCall}
>
{translate('CONTRACT_Read')}
</button>
) : walletDecrypted ? (
!txGenerated ? (
<Aux>
<label className="InteractExplorer-field form-group">
<h4 className="InteractExplorer-field-label">Gas Limit</h4>
<input
name="gasLimit"
value={gasLimit}
onChange={handleInput('gasLimit')}
className={classnames(
'InteractExplorer-field-input',
'form-control',
{
'is-invalid': !validGasLimit
}
)}
/>
</label>
<label className="InteractExplorer-field form-group">
<h4 className="InteractExplorer-field-label">Value</h4>
<UnitConverter
decimal={getDecimal('ether')}
onChange={handleInput('value')}
>
{({ convertedUnit, onUserInput }) => (
<input
name="value"
value={convertedUnit}
onChange={onUserInput}
placeholder="0"
className={classnames(
'InteractExplorer-field-input',
'form-control',
{
'is-invalid': !validValue
}
)}
/>
)}
</UnitConverter>
</label>
<button
className="InteractExplorer-func-submit btn btn-primary"
disabled={!showContractWrite}
onClick={handleFunctionSend(selectedFunction, inputs)}
>
{translate('CONTRACT_Write')}
</button>
</Aux>
) : (
<Aux>
{txCompare}
<button
className="Deploy-submit btn btn-primary"
onClick={toggleModal}
>
{translate('SEND_trans')}
</button>
</Aux>
)
) : (
<WalletDecrypt />
)}
</div>
)}
{displayModal && txModal}
</div>
);
}
private contractOptions = () => {
const { contractFunctions } = this.props;
return Object.keys(contractFunctions).map(name => {
return (
<option key={name} value={name}>
{name}
</option>
);
});
};
private handleFunctionCall = async (_: any) => {
try {
const { selectedFunction, inputs } = this.state;
const parsedInputs = Object.keys(inputs).reduce(
(accu, key) => ({ ...accu, [key]: inputs[key].parsedData }),
{}
);
const results = await selectedFunction.call(parsedInputs);
this.setState({ outputs: results });
} catch (e) {
this.props.showNotification(
'warning',
`Function call error: ${(e as Error).message}` ||
'Invalid input parameters',
5000
);
}
};
private handleFunctionSelect = (ev: any) => {
const { contractFunctions } = this.props;
const selectedFunctionName = ev.target.value;
const selectedFunction = contractFunctions[selectedFunctionName];
this.setState({
selectedFunction,
selectedFunctionName,
outputs: {},
inputs: {}
});
};
private tryParseJSON(input: string) {
try {
return JSON.parse(input);
} catch {
return input;
}
}
private handleInputChange = (ev: any) => {
const rawValue: string = ev.target.value;
const isArr = rawValue.startsWith('[') && rawValue.endsWith(']');
const value = {
rawData: rawValue,
parsedData: isArr ? this.tryParseJSON(rawValue) : rawValue
};
this.setState({
inputs: {
...this.state.inputs,
[ev.target.name]: value
}
});
};
}
const Aux = ({ children }) => children;

View File

@ -0,0 +1,14 @@
.InteractForm {
&-address {
display: flex;
> * {
flex: 1;
margin-right: 10px;
&:last-child {
margin-right: 0;
}
}
}
}

View File

@ -0,0 +1,159 @@
import React, { Component } from 'react';
import translate from 'translations';
import './InteractForm.scss';
import { NetworkContract } from 'config/data';
import { getNetworkContracts } from 'selectors/config';
import { connect } from 'react-redux';
import { AppState } from 'reducers';
import { isValidETHAddress, isValidAbiJson } from 'libs/validators';
import classnames from 'classnames';
interface Props {
contracts: NetworkContract[];
accessContract(abiJson: string, address: string): (ev) => void;
resetState(): void;
}
interface State {
address: string;
abiJson: string;
}
class InteractForm extends Component<Props, State> {
public state = {
address: '',
abiJson: ''
};
private abiJsonPlaceholder = '[{ "type":"contructor", "inputs":\
[{ "name":"param1","type":"uint256", "indexed":true }],\
"name":"Event" }, { "type":"function", "inputs": [{"nam\
e":"a", "type":"uint256"}], "name":"foo", "outputs": [] }]';
public render() {
const { contracts, accessContract } = this.props;
const { address, abiJson } = this.state;
const validEthAddress = isValidETHAddress(address);
const validAbiJson = isValidAbiJson(abiJson);
const showContractAccessButton = validEthAddress && validAbiJson;
let contractOptions;
if (contracts && contracts.length) {
contractOptions = [
{
name: 'Select a contract...',
value: null
}
];
contractOptions = contractOptions.concat(
contracts.map(contract => {
return {
name: `${contract.name} (${contract.address.substr(0, 10)}...)`,
value: contract.address
};
})
);
} else {
contractOptions = [
{
name: 'No contracts available',
value: null
}
];
}
// TODO: Use common components for address, abi json
return (
<div className="InteractForm">
<div className="InteractForm-address">
<label className="InteractForm-address-field form-group">
<h4>{translate('CONTRACT_Title')}</h4>
<input
placeholder="mewtopia.eth or 0x7cB57B5A97eAbe94205C07890BE4c1aD31E486A8"
name="contract_address"
autoComplete="off"
value={address}
className={classnames(
'InteractForm-address-field-input',
'form-control',
{
'is-invalid': !validEthAddress
}
)}
onChange={this.handleInput('address')}
/>
</label>
<label className="InteractForm-address-contract form-group">
<h4>{translate('CONTRACT_Title_2')}</h4>
<select
className="InteractForm-address-field-input form-control"
onChange={this.handleSelectContract}
disabled={!contracts || !contracts.length}
>
{contractOptions.map(opt => (
<option key={opt.value} value={opt.value}>
{opt.name}
</option>
))}
</select>
</label>
</div>
<div className="InteractForm-interface">
<label className="InteractForm-interface-field form-group">
<h4 className="InteractForm-interface-field-label">
{translate('CONTRACT_Json')}
</h4>
<textarea
placeholder={this.abiJsonPlaceholder}
name="abiJson"
className={classnames(
'InteractForm-interface-field-input',
'form-control',
{
'is-invalid': !validAbiJson
}
)}
onChange={this.handleInput('abiJson')}
value={abiJson}
rows={6}
/>
</label>
</div>
<button
className="InteractForm-submit btn btn-primary"
disabled={!showContractAccessButton}
onClick={accessContract(abiJson, address)}
>
{translate('x_Access')}
</button>
</div>
);
}
private handleInput = name => (ev: any) => {
this.props.resetState();
this.setState({ [name]: ev.target.value });
};
private handleSelectContract = (ev: any) => {
this.props.resetState();
const addr = ev.target.value;
const contract = this.props.contracts.reduce((prev, currContract) => {
return currContract.address === addr ? currContract : prev;
});
this.setState({
address: contract.address,
abiJson: contract.abi
});
};
}
const mapStateToProps = (state: AppState) => ({
contracts: getNetworkContracts(state)
});
export default connect(mapStateToProps)(InteractForm);

View File

@ -0,0 +1,192 @@
import React, { Component } from 'react';
import InteractForm from './components/InteractForm';
import InteractExplorer from './components//InteractExplorer';
import Contract from 'libs/contracts';
import { withTx, IWithTx } from '../withTx';
import {
TxModal,
Props as DMProps,
TTxModal
} from 'containers/Tabs/Contracts/components/TxModal';
import { IUserSendParams } from 'libs/contracts/ABIFunction';
import BN from 'bn.js';
import {
TxCompare,
TTxCompare
} from 'containers/Tabs/Contracts/components/TxCompare';
interface State {
currentContract: Contract | null;
showExplorer: boolean;
address: string | null;
signedTx: string | null;
rawTx: any | null;
gasLimit: string;
value: string;
displayModal: boolean;
}
class Interact extends Component<IWithTx, State> {
public initialState: State = {
currentContract: null,
showExplorer: false,
address: null,
signedTx: null,
rawTx: null,
gasLimit: '30000',
value: '0',
displayModal: false
};
public state: State = this.initialState;
public componentWillReceiveProps(nextProps: IWithTx) {
if (nextProps.wallet && this.state.currentContract) {
Contract.setConfigForTx(this.state.currentContract, nextProps);
}
}
public accessContract = (contractAbi: string, address: string) => () => {
try {
const parsedAbi = JSON.parse(contractAbi);
const contractInstance = new Contract(parsedAbi);
contractInstance.at(address);
contractInstance.setNode(this.props.nodeLib);
this.setState({
currentContract: contractInstance,
showExplorer: true,
address
});
} catch (e) {
this.props.showNotification(
'danger',
`Contract Access Error: ${(e as Error).message ||
'Can not parse contract'}`
);
this.resetState();
}
};
public render() {
const {
showExplorer,
currentContract,
gasLimit,
value,
signedTx,
displayModal
} = this.state;
const { wallet, showNotification } = this.props;
const txGenerated = !!signedTx;
return (
<div className="Interact">
<InteractForm
accessContract={this.accessContract}
resetState={this.resetState}
/>
<hr />
{showExplorer &&
currentContract && (
<InteractExplorer
{...{
address: currentContract.address,
walletDecrypted: !!wallet,
handleInput: this.handleInput,
contractFunctions: Contract.getFunctions(currentContract),
gasLimit,
value,
handleFunctionSend: this.handleFunctionSend,
txGenerated,
txModal: txGenerated ? this.makeModal() : null,
txCompare: txGenerated ? this.makeCompareTx() : null,
toggleModal: this.toggleModal,
displayModal,
showNotification
}}
/>
)}
</div>
);
}
private makeCompareTx = (): React.ReactElement<TTxCompare> => {
const { nonce } = this.state.rawTx;
const { signedTx } = this.state;
if (!nonce || !signedTx) {
throw Error('Can not display raw tx, nonce empty or no signed tx');
}
return <TxCompare signedTx={signedTx} />;
};
private makeModal = (): React.ReactElement<TTxModal> => {
const { networkName, node: { network, service } } = this.props;
const { signedTx } = this.state;
if (!signedTx) {
throw Error('Can not deploy contract, no signed tx');
}
const props: DMProps = {
action: 'send a contract state modifying transaction',
networkName,
network,
service,
handleBroadcastTx: this.handleBroadcastTx,
onClose: this.resetState
};
return <TxModal {...props} />;
};
private toggleModal = () => this.setState({ displayModal: true });
private resetState = () => this.setState(this.initialState);
private handleBroadcastTx = () => {
const { signedTx } = this.state;
if (!signedTx) {
return null;
}
this.props.broadcastTx(signedTx);
this.resetState();
};
private handleFunctionSend = (selectedFunction, inputs) => async () => {
try {
const { address, gasLimit, value } = this.state;
if (!address) {
return null;
}
const parsedInputs = Object.keys(inputs).reduce(
(accu, key) => ({ ...accu, [key]: inputs[key].parsedData }),
{}
);
const userInputs: IUserSendParams = {
input: parsedInputs,
to: address,
gasLimit: new BN(gasLimit),
value
};
const { signedTx, rawTx } = await selectedFunction.send(userInputs);
this.setState({ signedTx, rawTx });
} catch (e) {
this.props.showNotification(
'danger',
`Function send error: ${(e as Error).message}` ||
'Invalid input parameters',
5000
);
}
};
private handleInput = name => (ev: React.FormEvent<any>) =>
this.setState({ [name]: ev.currentTarget.value });
}
export default withTx(Interact);

View File

@ -0,0 +1,36 @@
import React from 'react';
import translate from 'translations';
import { decodeTransaction } from 'libs/transaction';
import EthTx from 'ethereumjs-tx';
import Code from 'components/ui/Code';
export interface Props {
signedTx: string;
}
export const TxCompare = (props: Props) => {
if (!props.signedTx) {
return null;
}
const rawTx = decodeTransaction(new EthTx(props.signedTx), false);
const Left = () => (
<div className="form-group">
<h4>{translate('SEND_raw')}</h4>
<Code>{JSON.stringify(rawTx, null, 2)}</Code>
</div>
);
const Right = () => (
<div className="form-group">
<h4> {translate('SEND_signed')} </h4>
<Code>{props.signedTx}</Code>
</div>
);
return (
<section>
<Left />
<Right />
</section>
);
};
export type TTxCompare = typeof TxCompare;

View File

@ -0,0 +1,65 @@
import React from 'react';
import translate from 'translations';
import Modal, { IButton } from 'components/ui/Modal';
export interface Props {
networkName: string;
network: string;
service: string;
action: string;
handleBroadcastTx(): void;
onClose(): void;
}
export type TTxModal = typeof TxModal;
export const TxModal = (props: Props) => {
const {
networkName,
network,
service,
handleBroadcastTx,
onClose,
action
} = props;
const buttons: IButton[] = [
{
text: translate('SENDModal_Yes', true) as string,
type: 'primary',
onClick: handleBroadcastTx
},
{
text: translate('SENDModal_No', true) as string,
type: 'default',
onClick: onClose
}
];
return (
<Modal
title="Confirm Your Transaction"
buttons={buttons}
handleClose={onClose}
isOpen={true}
>
<div className="modal-body">
<h2 className="modal-title text-danger">
{translate('SENDModal_Title')}
</h2>
<p>
You are about to <strong>{action}</strong> on the{' '}
<strong>{networkName}</strong> chain.
</p>
<p>
The <strong>{network}</strong> node you are sending through is
provided by <strong>{service}</strong>.
</p>
<h4>{translate('SENDModal_Content_3')}</h4>
</div>
</Modal>
);
};

View File

@ -0,0 +1,40 @@
import * as configSelectors from 'selectors/config';
import { AppState } from 'reducers';
import { toWei, Wei, getDecimal } from 'libs/units';
import { connect } from 'react-redux';
import { showNotification, TShowNotification } from 'actions/notifications';
import { broadcastTx, TBroadcastTx } from 'actions/wallet';
import { IWallet, Balance } from 'libs/wallet';
import { RPCNode } from 'libs/nodes';
import { NodeConfig, NetworkConfig } from 'config/data';
export interface IWithTx {
wallet: IWallet;
balance: Balance;
node: NodeConfig;
nodeLib: RPCNode;
chainId: NetworkConfig['chainId'];
networkName: NetworkConfig['name'];
gasPrice: Wei;
broadcastTx: TBroadcastTx;
showNotification: TShowNotification;
}
const mapStateToProps = (state: AppState) => ({
wallet: state.wallet.inst,
balance: state.wallet.balance,
node: configSelectors.getNodeConfig(state),
nodeLib: configSelectors.getNodeLib(state),
chainId: configSelectors.getNetworkConfig(state).chainId,
networkName: configSelectors.getNetworkConfig(state).name,
gasPrice: toWei(
`${configSelectors.getGasPriceGwei(state)}`,
getDecimal('gwei')
)
});
export const withTx = passedComponent =>
connect(mapStateToProps, {
showNotification,
broadcastTx
})(passedComponent);

View File

@ -0,0 +1,30 @@
@import 'common/sass/variables';
@import 'common/sass/mixins';
.Contracts {
&-header {
margin: 0;
text-align: center;
&-tab {
@include reset-button;
color: $ether-blue;
&:hover,
&:active {
opacity: 0.8;
}
&.is-active {
&,
&:hover,
&:active {
color: $text-color;
cursor: default;
opacity: 1;
font-weight: 500;
}
}
}
}
}

View File

@ -0,0 +1,61 @@
import React, { Component } from 'react';
import translate from 'translations';
import Interact from './components/Interact';
import Deploy from './components/Deploy';
import './index.scss';
import TabSection from 'containers/TabSection';
interface State {
activeTab: string;
}
export default class Contracts extends Component<{}, State> {
public state: State = {
activeTab: 'interact'
};
public changeTab = activeTab => () => this.setState({ activeTab });
public render() {
const { activeTab } = this.state;
let content;
let interactActive = '';
let deployActive = '';
if (activeTab === 'interact') {
content = <Interact />;
interactActive = 'is-active';
} else {
content = <Deploy />;
deployActive = 'is-active';
}
return (
<TabSection>
<section className="Tab-content Contracts">
<div className="Tab-content-pane">
<h1 className="Contracts-header">
<button
className={`Contracts-header-tab ${interactActive}`}
onClick={this.changeTab('interact')}
>
{translate('NAV_InteractContract')}
</button>{' '}
<span>or</span>{' '}
<button
className={`Contracts-header-tab ${deployActive}`}
onClick={this.changeTab('deploy')}
>
{translate('NAV_DeployContract')}
</button>
</h1>
</div>
<main className="Tab-content-pane" role="main">
<div className="Contracts-content">{content}</div>
</main>
</section>
</TabSection>
);
}
}

View File

@ -1,6 +1,6 @@
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import ENS from './components/ENS'; import ENS from './components/ENS';
const mapStateToProps = state => ({}); const mapStateToProps = _ => ({});
export default connect(mapStateToProps)(ENS); export default connect(mapStateToProps)(ENS);

View File

@ -0,0 +1,41 @@
@import "common/sass/variables";
.CryptoWarning {
max-width: 740px;
margin: 0 auto;
text-align: center;
&-title {
margin-bottom: 15px;
}
&-text {
margin-bottom: 30px;
}
&-browsers {
&-browser {
display: inline-block;
width: 86px;
margin: 0 25px;
color: $text-color;
opacity: 0.8;
transition: opacity 100ms ease;
&:hover {
opacity: 1;
}
&-icon {
width: 100%;
height: auto;
margin-bottom: 10px;
}
&-name {
font-size: 18px;
font-weight: 300;
}
}
}
}

View File

@ -0,0 +1,73 @@
import * as React from 'react';
import NewTabLink from 'components/ui/NewTabLink';
import isMobile from 'utils/isMobile';
import firefoxIcon from 'assets/images/browsers/firefox.svg';
import chromeIcon from 'assets/images/browsers/chrome.svg';
import operaIcon from 'assets/images/browsers/opera.svg';
import './CryptoWarning.scss';
const BROWSERS = [
{
name: 'Firefox',
href: 'https://www.mozilla.org/en-US/firefox/new/',
icon: firefoxIcon
},
{
name: 'Chrome',
href: 'https://www.google.com/chrome/browser/desktop/index.html',
icon: chromeIcon
},
{
name: 'Opera',
href: 'http://www.opera.com/',
icon: operaIcon
}
];
const CryptoWarning: React.SFC<{}> = () => (
<div className="Tab-content-pane">
<div className="CryptoWarning">
<h2 className="CryptoWarning-title">
Your Browser Cannot Generate a Wallet
</h2>
<p className="CryptoWarning-text">
{isMobile
? `
MyEtherWallet requires certain features for secure wallet generation
that your browser doesn't offer. You can still securely use the site
otherwise. To generate a wallet, please use your device's default
browser, or switch to a laptop or desktop computer.
`
: `
MyEtherWallet requires certain features for secure wallet generation
that your browser doesn't offer. You can still securely use the site
otherwise. To generate a wallet, upgrade to one of the following
browsers:
`}
</p>
<div className="CryptoWarning-browsers">
{BROWSERS.map(browser => (
<NewTabLink
key={browser.href}
href={browser.href}
className="CryptoWarning-browsers-browser"
>
<div>
<img
className="CryptoWarning-browsers-browser-icon"
src={browser.icon}
/>
<div className="CryptoWarning-browsers-browser-name">
{browser.name}
</div>
</div>
</NewTabLink>
))}
</div>
</div>
</div>
);
export default CryptoWarning;

View File

@ -1,6 +1,7 @@
import { ContinueToPaperAction } from 'actions/generateWallet'; import { ContinueToPaperAction } from 'actions/generateWallet';
import { getV3Filename, UtcKeystore } from 'libs/keystore'; import { IFullWallet, IV3Wallet } from 'ethereumjs-wallet';
import PrivKeyWallet from 'libs/wallet/privkey'; import { toChecksumAddress } from 'ethereumjs-util';
import { NewTabLink } from 'components/ui';
import React, { Component } from 'react'; import React, { Component } from 'react';
import translate from 'translations'; import translate from 'translations';
import { makeBlob } from 'utils/blob'; import { makeBlob } from 'utils/blob';
@ -8,46 +9,35 @@ import './DownloadWallet.scss';
import Template from './Template'; import Template from './Template';
interface Props { interface Props {
wallet: PrivKeyWallet; wallet: IFullWallet;
password: string; password: string;
continueToPaper(): ContinueToPaperAction; continueToPaper(): ContinueToPaperAction;
} }
interface State { interface State {
hasDownloadedWallet: boolean; hasDownloadedWallet: boolean;
address: string; keystore: IV3Wallet | null;
keystore: UtcKeystore | null;
} }
export default class DownloadWallet extends Component<Props, State> { export default class DownloadWallet extends Component<Props, State> {
public state: State = { public state: State = {
hasDownloadedWallet: false, hasDownloadedWallet: false,
address: '',
keystore: null keystore: null
}; };
public componentDidMount() { public componentWillMount() {
this.props.wallet.getAddress().then(address => { this.setWallet(this.props.wallet, this.props.password);
this.setState({ address });
});
} }
public componentWillMount() {
this.props.wallet.toKeystore(this.props.password).then(utcKeystore => {
this.setState({ keystore: utcKeystore });
});
}
public componentWillUpdate(nextProps: Props) { public componentWillUpdate(nextProps: Props) {
if (this.props.wallet !== nextProps.wallet) { if (this.props.wallet !== nextProps.wallet) {
nextProps.wallet.toKeystore(nextProps.password).then(utcKeystore => { this.setWallet(nextProps.wallet, nextProps.password);
this.setState({ keystore: utcKeystore });
});
} }
} }
public render() { public render() {
const { hasDownloadedWallet } = this.state; const { hasDownloadedWallet } = this.state;
const filename = this.getFilename(); const filename = this.props.wallet.getV3Filename();
const content = ( const content = (
<div className="DlWallet"> <div className="DlWallet">
@ -112,22 +102,14 @@ export default class DownloadWallet extends Component<Props, State> {
<h4>{translate('GEN_Help_4')}</h4> <h4>{translate('GEN_Help_4')}</h4>
<ul> <ul>
<li> <li>
<a <NewTabLink href="https://myetherwallet.groovehq.com/knowledge_base/topics/how-do-i-save-slash-backup-my-wallet">
href="https://myetherwallet.groovehq.com/knowledge_base/topics/how-do-i-save-slash-backup-my-wallet"
target="_blank"
rel="noopener"
>
<strong>{translate('GEN_Help_13')}</strong> <strong>{translate('GEN_Help_13')}</strong>
</a> </NewTabLink>
</li> </li>
<li> <li>
<a <NewTabLink href="https://myetherwallet.groovehq.com/knowledge_base/topics/what-are-the-different-formats-of-a-private-key">
href="https://myetherwallet.groovehq.com/knowledge_base/topics/what-are-the-different-formats-of-a-private-key"
target="_blank"
rel="noopener"
>
<strong>{translate('GEN_Help_14')}</strong> <strong>{translate('GEN_Help_14')}</strong>
</a> </NewTabLink>
</li> </li>
</ul> </ul>
</div> </div>
@ -136,28 +118,23 @@ export default class DownloadWallet extends Component<Props, State> {
return <Template content={content} help={help} />; return <Template content={content} help={help} />;
} }
public getBlob() { public getBlob = () =>
if (this.state.keystore) { (this.state.keystore &&
return makeBlob('text/json;charset=UTF-8', this.state.keystore); makeBlob('text/json;charset=UTF-8', this.state.keystore)) ||
} undefined;
private markDownloaded = () =>
this.state.keystore && this.setState({ hasDownloadedWallet: true });
private handleContinue = () =>
this.state.hasDownloadedWallet && this.props.continueToPaper();
private setWallet(wallet: IFullWallet, password: string) {
const keystore = wallet.toV3(password, { n: 1024 });
keystore.address = toChecksumAddress(keystore.address);
this.setState({ keystore });
} }
public getFilename() { private handleDownloadKeystore = e =>
return getV3Filename(this.state.address);
}
private markDownloaded = () => {
if (this.state.keystore) {
this.setState({ hasDownloadedWallet: true });
}
};
private handleContinue = () => {
if (this.state.hasDownloadedWallet) {
this.props.continueToPaper();
}
};
private handleDownloadKeystore = (e): void => {
this.state.keystore ? this.markDownloaded() : e.preventDefault(); this.state.keystore ? this.markDownloaded() : e.preventDefault();
};
} }

View File

@ -1,113 +1,91 @@
import PrintableWallet from 'components/PrintableWallet'; import PrintableWallet from 'components/PrintableWallet';
import PrivKeyWallet from 'libs/wallet/privkey'; import { IFullWallet } from 'ethereumjs-wallet';
import React, { Component } from 'react'; import { NewTabLink } from 'components/ui';
import React from 'react';
import { Link } from 'react-router-dom'; import { Link } from 'react-router-dom';
import translate from 'translations'; import translate from 'translations';
import './PaperWallet.scss'; import './PaperWallet.scss';
import Template from './Template'; import Template from './Template';
interface Props { const content = (wallet: IFullWallet) => (
wallet: PrivKeyWallet; <div className="GenPaper">
} {/* Private Key */}
<h1 className="GenPaper-title">{translate('GEN_Label_5')}</h1>
<input
className="GenPaper-private form-control"
value={wallet.getPrivateKeyString()}
aria-label={translate('x_PrivKey')}
aria-describedby="x_PrivKeyDesc"
type="text"
readOnly={true}
/>
export default class PaperWallet extends Component<Props, {}> { {/* Download Paper Wallet */}
public render() { <h1 className="GenPaper-title">{translate('x_Print')}</h1>
const { wallet } = this.props; <div className="GenPaper-paper">
<PrintableWallet wallet={wallet} />
</div>
const content = ( {/* Warning */}
<div className="GenPaper"> <div className="GenPaper-warning">
{/* Private Key */} <p>
<h1 className="GenPaper-title">{translate('GEN_Label_5')}</h1> <strong>Do not lose it!</strong> It cannot be recovered if you lose it.
<input </p>
className="GenPaper-private form-control" <p>
value={wallet.getPrivateKey()} <strong>Do not share it!</strong> Your funds will be stolen if you use
aria-label={translate('x_PrivKey')} this file on a malicious/phishing site.
aria-describedby="x_PrivKeyDesc" </p>
type="text" <p>
readOnly={true} <strong>Make a backup!</strong> Secure it like the millions of dollars
/> it may one day be worth.
</p>
</div>
{/* Download Paper Wallet */} {/* Continue button */}
<h1 className="GenPaper-title">{translate('x_Print')}</h1> <Link className="GenPaper-continue btn btn-default" to="/view-wallet">
<div className="GenPaper-paper"> {translate('NAV_ViewWallet')}
<PrintableWallet wallet={wallet} /> </Link>
</div> </div>
);
{/* Warning */} const help = (
<div className="GenPaper-warning"> <div>
<p> <h4>{translate('GEN_Help_4')}</h4>
<strong>Do not lose it!</strong> It cannot be recovered if you lose <ul>
it. <li>
</p> <NewTabLink href="https://myetherwallet.groovehq.com/knowledge_base/topics/how-do-i-save-slash-backup-my-wallet">
<p> <strong>{translate('HELP_2a_Title')}</strong>
<strong>Do not share it!</strong> Your funds will be stolen if you </NewTabLink>
use this file on a malicious/phishing site. </li>
</p> <li>
<p> <NewTabLink href="https://myetherwallet.groovehq.com/knowledge_base/topics/protecting-yourself-and-your-funds">
<strong>Make a backup!</strong> Secure it like the millions of <strong>{translate('GEN_Help_15')}</strong>
dollars it may one day be worth. </NewTabLink>
</p> </li>
</div> <li>
<NewTabLink href="https://myetherwallet.groovehq.com/knowledge_base/topics/what-are-the-different-formats-of-a-private-key">
<strong>{translate('GEN_Help_16')}</strong>
</NewTabLink>
</li>
</ul>
{/* Continue button */} <h4>{translate('GEN_Help_17')}</h4>
<Link className="GenPaper-continue btn btn-default" to="/view-wallet"> <ul>
{translate('NAV_ViewWallet')} <li>{translate('GEN_Help_18')}</li>
</Link> <li>{translate('GEN_Help_19')}</li>
</div> <li>
); <NewTabLink href="https://myetherwallet.groovehq.com/knowledge_base/topics/how-do-i-safely-slash-offline-slash-cold-storage-with-myetherwallet">
{translate('GEN_Help_20')}
</NewTabLink>
</li>
</ul>
const help = ( <h4>{translate('x_PrintDesc')}</h4>
<div> </div>
<h4>{translate('GEN_Help_4')}</h4> );
<ul>
<li>
<a
href="https://myetherwallet.groovehq.com/knowledge_base/topics/how-do-i-save-slash-backup-my-wallet"
target="_blank"
rel="noopener"
>
<strong>{translate('HELP_2a_Title')}</strong>
</a>
</li>
<li>
<a
href="https://myetherwallet.groovehq.com/knowledge_base/topics/protecting-yourself-and-your-funds"
target="_blank"
rel="noopener"
>
<strong>{translate('GEN_Help_15')}</strong>
</a>
</li>
<li>
<a
href="https://myetherwallet.groovehq.com/knowledge_base/topics/what-are-the-different-formats-of-a-private-key"
target="_blank"
rel="noopener"
>
<strong>{translate('GEN_Help_16')}</strong>
</a>
</li>
</ul>
<h4>{translate('GEN_Help_17')}</h4> const PaperWallet: React.SFC<{
<ul> wallet: IFullWallet;
<li>{translate('GEN_Help_18')}</li> }> = ({ wallet }) => <Template content={content(wallet)} help={help} />;
<li>{translate('GEN_Help_19')}</li>
<li>
<a
href="https://myetherwallet.groovehq.com/knowledge_base/topics/how-do-i-safely-slash-offline-slash-cold-storage-with-myetherwallet"
target="_blank"
rel="noopener"
>
{translate('GEN_Help_20')}
</a>
</li>
</ul>
<h4>{translate('x_PrintDesc')}</h4> export default PaperWallet;
</div>
);
return <Template content={content} help={help} />;
}
}

View File

@ -6,20 +6,21 @@ import {
TGenerateNewWallet, TGenerateNewWallet,
TResetGenerateWallet TResetGenerateWallet
} from 'actions/generateWallet'; } from 'actions/generateWallet';
import PrivKeyWallet from 'libs/wallet/privkey'; import { IFullWallet } from 'ethereumjs-wallet';
import React, { Component } from 'react'; import React, { Component } from 'react';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import { AppState } from 'reducers'; import { AppState } from 'reducers';
import DownloadWallet from './components/DownloadWallet'; import DownloadWallet from './components/DownloadWallet';
import EnterPassword from './components/EnterPassword'; import EnterPassword from './components/EnterPassword';
import PaperWallet from './components/PaperWallet'; import PaperWallet from './components/PaperWallet';
import CryptoWarning from './components/CryptoWarning';
import TabSection from 'containers/TabSection'; import TabSection from 'containers/TabSection';
interface Props { interface Props {
// Redux state // Redux state
activeStep: string; // FIXME union actual steps activeStep: string; // FIXME union actual steps
password: string; password: string;
wallet: PrivKeyWallet | null | undefined; wallet: IFullWallet | null | undefined;
walletPasswordForm: any; walletPasswordForm: any;
// Actions // Actions
generateNewWallet: TGenerateNewWallet; generateNewWallet: TGenerateNewWallet;
@ -38,38 +39,42 @@ class GenerateWallet extends Component<Props, {}> {
const AnyEnterPassword = EnterPassword as new () => any; const AnyEnterPassword = EnterPassword as new () => any;
switch (activeStep) { if (window.crypto) {
case 'password': switch (activeStep) {
content = ( case 'password':
<AnyEnterPassword
walletPasswordForm={this.props.walletPasswordForm}
generateNewWallet={this.props.generateNewWallet}
/>
);
break;
case 'download':
if (wallet) {
content = ( content = (
<DownloadWallet <AnyEnterPassword
wallet={wallet} walletPasswordForm={this.props.walletPasswordForm}
password={password} generateNewWallet={this.props.generateNewWallet}
continueToPaper={this.props.continueToPaper}
/> />
); );
} break;
break;
case 'paper': case 'download':
if (wallet) { if (wallet) {
content = <PaperWallet wallet={wallet} />; content = (
} else { <DownloadWallet
wallet={wallet}
password={password}
continueToPaper={this.props.continueToPaper}
/>
);
}
break;
case 'paper':
if (wallet) {
content = <PaperWallet wallet={wallet} />;
} else {
content = <h1>Uh oh. Not sure how you got here.</h1>;
}
break;
default:
content = <h1>Uh oh. Not sure how you got here.</h1>; content = <h1>Uh oh. Not sure how you got here.</h1>;
} }
break; } else {
content = <CryptoWarning />;
default:
content = <h1>Uh oh. Not sure how you got here.</h1>;
} }
return ( return (

View File

@ -1,45 +1,55 @@
import React from 'react'; import React from 'react';
import translate, { translateRaw } from 'translations'; import translate, { translateRaw } from 'translations';
import UnitDropdown from './UnitDropdown'; import UnitDropdown from './UnitDropdown';
import { Ether } from 'libs/units'; import { Balance } from 'libs/wallet';
import { UnitConverter } from 'components/renderCbs';
interface Props { interface Props {
value: string; decimal: number;
unit: string; unit: string;
tokens: string[]; tokens: string[];
balance: number | null | Ether; balance: number | null | Balance;
onChange?(value: string, unit: string): void; isReadOnly: boolean;
onAmountChange(value: string, unit: string): void;
onUnitChange(unit: string): void;
} }
export default class AmountField extends React.Component { export default class AmountField extends React.Component {
public props: Props; public props: Props;
get active() {
return !this.props.isReadOnly;
}
public render() { public render() {
const { value, unit, onChange, balance } = this.props; const { unit, balance, decimal, isReadOnly } = this.props;
const isReadonly = !onChange;
return ( return (
<div className="row form-group"> <div className="row form-group">
<div className="col-xs-11"> <div className="col-xs-11">
<label>{translate('SEND_amount')}</label> <label>{translate('SEND_amount')}</label>
<div className="input-group"> <div className="input-group">
<input <UnitConverter decimal={decimal} onChange={this.callWithBaseUnit}>
className={`form-control ${isFinite(Number(value)) && {({ onUserInput, convertedUnit }) => (
Number(value) > 0 <input
? 'is-valid' className={`form-control ${
: 'is-invalid'}`} isFinite(Number(convertedUnit)) && Number(convertedUnit) > 0
type="text" ? 'is-valid'
placeholder={translateRaw('SEND_amount_short')} : 'is-invalid'
value={value} }`}
disabled={isReadonly} type="text"
onChange={isReadonly ? void 0 : this.onValueChange} placeholder={translateRaw('SEND_amount_short')}
/> value={convertedUnit}
disabled={isReadOnly}
onChange={onUserInput}
/>
)}
</UnitConverter>
<UnitDropdown <UnitDropdown
value={unit} value={unit}
options={['ether'].concat(this.props.tokens)} options={['ether'].concat(this.props.tokens)}
onChange={isReadonly ? void 0 : this.onUnitChange} onChange={isReadOnly ? void 0 : this.onUnitChange}
/> />
</div> </div>
{!isReadonly && {!isReadOnly &&
balance && ( balance && (
<span className="help-block"> <span className="help-block">
<a onClick={this.onSendEverything}> <a onClick={this.onSendEverything}>
@ -54,24 +64,12 @@ export default class AmountField extends React.Component {
); );
} }
public onUnitChange = (unit: string) => { public onUnitChange = (unit: string) =>
if (this.props.onChange) { this.active && this.props.onUnitChange(unit); // thsi needs to be converted unit
this.props.onChange(this.props.value, unit);
}
};
public onValueChange = (e: React.SyntheticEvent<HTMLInputElement>) => { public callWithBaseUnit = ({ currentTarget: { value } }) =>
if (this.props.onChange) { this.active && this.props.onAmountChange(value, this.props.unit);
this.props.onChange(
(e.target as HTMLInputElement).value,
this.props.unit
);
}
};
public onSendEverything = () => { public onSendEverything = () =>
if (this.props.onChange) { this.active && this.props.onAmountChange('everything', this.props.unit);
this.props.onChange('everything', this.props.unit);
}
};
} }

View File

@ -1,8 +1,15 @@
@import "common/sass/variables"; @import "common/sass/variables";
$summary-height: 54px; $summary-height: 54px;
$button-break: 'max-width: 620px';
.ConfModal { .ConfModal {
min-width: 580px;
@media (#{$button-break}) {
min-width: 0;
}
&-summary { &-summary {
display: flex; display: flex;
margin-bottom: 30px; margin-bottom: 30px;
@ -51,3 +58,19 @@ $summary-height: 54px;
} }
} }
// Modal overrides for extra long buttons
@media (#{$button-break}) {
.ConfModalWrap {
.Modal-footer-btn {
display: block;
float: none;
width: 100%;
margin: 0 0 5px;
&:last-child {
margin: 0;
}
}
}
}

View File

@ -1,61 +1,55 @@
import Big from 'bignumber.js';
import Identicon from 'components/ui/Identicon'; import Identicon from 'components/ui/Identicon';
import Modal, { IButton } from 'components/ui/Modal'; import Modal, { IButton } from 'components/ui/Modal';
import Spinner from 'components/ui/Spinner'; import Spinner from 'components/ui/Spinner';
import { NetworkConfig, NodeConfig } from 'config/data'; import { NetworkConfig, NodeConfig } from 'config/data';
import EthTx from 'ethereumjs-tx'; import EthTx from 'ethereumjs-tx';
import ERC20 from 'libs/erc20';
import { import {
BroadcastTransactionStatus, BroadcastTransactionStatus,
getTransactionFields getTransactionFields,
decodeTransaction
} from 'libs/transaction'; } from 'libs/transaction';
import { toTokenDisplay, toUnit } from 'libs/units';
import { IWallet } from 'libs/wallet/IWallet';
import React from 'react'; import React from 'react';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import { getLanguageSelection, getNetworkConfig } from 'selectors/config'; import {
getLanguageSelection,
getNetworkConfig,
getNodeConfig
} from 'selectors/config';
import { getTokens, getTxFromState, MergedToken } from 'selectors/wallet'; import { getTokens, getTxFromState, MergedToken } from 'selectors/wallet';
import translate, { translateRaw } from 'translations'; import translate, { translateRaw } from 'translations';
import { UnitDisplay } from 'components/ui';
import './ConfirmationModal.scss'; import './ConfirmationModal.scss';
interface Props { interface Props {
signedTx: string; signedTx: string;
transaction: EthTx; transaction: EthTx;
wallet: IWallet;
node: NodeConfig; node: NodeConfig;
token: MergedToken | undefined; token: MergedToken;
network: NetworkConfig; network: NetworkConfig;
lang: string; lang: string;
broadCastTxStatus: BroadcastTransactionStatus; broadCastTxStatus: BroadcastTransactionStatus;
decimal: number;
onConfirm(signedTx: string): void; onConfirm(signedTx: string): void;
onClose(): void; onClose(): void;
} }
interface State { interface State {
fromAddress: string;
timeToRead: number; timeToRead: number;
hasBroadCasted: boolean; hasBroadCasted: boolean;
} }
class ConfirmationModal extends React.Component<Props, State> { class ConfirmationModal extends React.Component<Props, State> {
public state = { public state = {
fromAddress: '',
timeToRead: 5, timeToRead: 5,
hasBroadCasted: false hasBroadCasted: false
}; };
private readTimer = 0; private readTimer = 0;
public componentWillReceiveProps(newProps: Props) {
// Reload address if the wallet changes
if (newProps.wallet !== this.props.wallet) {
this.setWalletAddress(this.props.wallet);
}
}
public componentDidUpdate() { public componentDidUpdate() {
if ( if (
this.state.hasBroadCasted && this.state.hasBroadCasted &&
this.props.broadCastTxStatus &&
!this.props.broadCastTxStatus.isBroadcasting !this.props.broadCastTxStatus.isBroadcasting
) { ) {
this.props.onClose(); this.props.onClose();
@ -71,20 +65,23 @@ class ConfirmationModal extends React.Component<Props, State> {
window.clearInterval(this.readTimer); window.clearInterval(this.readTimer);
} }
}, 1000); }, 1000);
this.setWalletAddress(this.props.wallet);
} }
public render() { public render() {
const { node, token, network, onClose, broadCastTxStatus } = this.props;
const { fromAddress, timeToRead } = this.state;
const { const {
toAddress, node,
value, token,
gasPrice, network,
data, onClose,
nonce broadCastTxStatus,
} = this.decodeTransaction(); transaction,
decimal
} = this.props;
const { timeToRead } = this.state;
const { toAddress, value, gasPrice, data, from, nonce } = decodeTransaction(
transaction,
token
);
const buttonPrefix = timeToRead > 0 ? `(${timeToRead}) ` : ''; const buttonPrefix = timeToRead > 0 ? `(${timeToRead}) ` : '';
const buttons: IButton[] = [ const buttons: IButton[] = [
@ -107,39 +104,42 @@ class ConfirmationModal extends React.Component<Props, State> {
broadCastTxStatus && broadCastTxStatus.isBroadcasting; broadCastTxStatus && broadCastTxStatus.isBroadcasting;
return ( return (
<Modal <div className="ConfModalWrap">
title="Confirm Your Transaction" <Modal
buttons={buttons} title="Confirm Your Transaction"
handleClose={onClose} buttons={buttons}
disableButtons={isBroadcasting} handleClose={onClose}
isOpen={true} disableButtons={isBroadcasting}
> isOpen={true}
{ >
<div className="ConfModal"> <div className="ConfModal">
{isBroadcasting ? ( {isBroadcasting ? (
<div className="ConfModal-loading"> <div className="ConfModal-loading">
<Spinner size="5x" /> <Spinner size="x5" />
</div> </div>
) : ( ) : (
<div> <div>
<div className="ConfModal-summary"> <div className="ConfModal-summary">
<div className="ConfModal-summary-icon ConfModal-summary-icon--from"> <div className="ConfModal-summary-icon ConfModal-summary-icon--from">
<Identicon size="100%" address={fromAddress} /> <Identicon size="100%" address={from} />
</div> </div>
<div className="ConfModal-summary-amount"> <div className="ConfModal-summary-amount">
<div className="ConfModal-summary-amount-arrow" /> <div className="ConfModal-summary-amount-arrow" />
<div className="ConfModal-summary-amount-currency"> <div className="ConfModal-summary-amount-currency">
{value} {symbol} <UnitDisplay
decimal={decimal}
value={value}
symbol={symbol}
/>
</div> </div>
</div> </div>
<div className="ConfModal-summary-icon ConfModal-summary-icon--to"> <div className="ConfModal-summary-icon ConfModal-summary-icon--to">
<Identicon size="100%" address={toAddress} /> <Identicon size="100%" address={toAddress} />
</div> </div>
</div> </div>
<ul className="ConfModal-details"> <ul className="ConfModal-details">
<li className="ConfModal-details-detail"> <li className="ConfModal-details-detail">
You are sending from <code>{fromAddress}</code> You are sending from <code>{from}</code>
</li> </li>
<li className="ConfModal-details-detail"> <li className="ConfModal-details-detail">
You are sending to <code>{toAddress}</code> You are sending to <code>{toAddress}</code>
@ -150,9 +150,20 @@ class ConfirmationModal extends React.Component<Props, State> {
<li className="ConfModal-details-detail"> <li className="ConfModal-details-detail">
You are sending{' '} You are sending{' '}
<strong> <strong>
{value} {symbol} <UnitDisplay
decimal={decimal}
value={value}
symbol={symbol}
/>
</strong>{' '} </strong>{' '}
with a gas price of <strong>{gasPrice} gwei</strong> with a gas price of{' '}
<strong>
<UnitDisplay
unit={'gwei'}
value={gasPrice}
symbol={'gwei'}
/>
</strong>
</li> </li>
<li className="ConfModal-details-detail"> <li className="ConfModal-details-detail">
You are interacting with the <strong>{node.network}</strong>{' '} You are interacting with the <strong>{node.network}</strong>{' '}
@ -183,8 +194,8 @@ class ConfirmationModal extends React.Component<Props, State> {
</div> </div>
)} )}
</div> </div>
} </Modal>
</Modal> </div>
); );
} }
@ -192,38 +203,6 @@ class ConfirmationModal extends React.Component<Props, State> {
window.clearInterval(this.readTimer); window.clearInterval(this.readTimer);
} }
private async setWalletAddress(wallet: IWallet) {
// TODO move getAddress to saga
const fromAddress = await wallet.getAddress();
this.setState({ fromAddress });
}
private decodeTransaction() {
const { transaction, token } = this.props;
const { to, value, data, gasPrice, nonce } = getTransactionFields(
transaction
);
let fixedValue;
let toAddress;
if (token) {
const tokenData = ERC20.$transfer(data);
fixedValue = toTokenDisplay(new Big(tokenData.value), token).toString();
toAddress = tokenData.to;
} else {
fixedValue = toUnit(new Big(value, 16), 'wei', 'ether').toString();
toAddress = to;
}
return {
value: fixedValue,
gasPrice: toUnit(new Big(gasPrice, 16), 'wei', 'gwei').toString(),
data,
toAddress,
nonce
};
}
private confirm = () => { private confirm = () => {
if (this.state.timeToRead < 1) { if (this.state.timeToRead < 1) {
this.props.onConfirm(this.props.signedTx); this.props.onConfirm(this.props.signedTx);
@ -239,6 +218,8 @@ function mapStateToProps(state, props) {
// Network config for defaults // Network config for defaults
const network = getNetworkConfig(state); const network = getNetworkConfig(state);
const node = getNodeConfig(state);
const lang = getLanguageSelection(state); const lang = getLanguageSelection(state);
const broadCastTxStatus = getTxFromState(state, props.signedTx); const broadCastTxStatus = getTxFromState(state, props.signedTx);
@ -249,6 +230,7 @@ function mapStateToProps(state, props) {
const token = data && tokens.find(t => t.address === to); const token = data && tokens.find(t => t.address === to);
return { return {
node,
broadCastTxStatus, broadCastTxStatus,
transaction, transaction,
token, token,

View File

@ -1,4 +1,3 @@
import Big from 'bignumber.js';
// COMPONENTS // COMPONENTS
import Spinner from 'components/ui/Spinner'; import Spinner from 'components/ui/Spinner';
import TabSection from 'containers/TabSection'; import TabSection from 'containers/TabSection';
@ -13,9 +12,10 @@ import {
DataField, DataField,
GasField GasField
} from './components'; } from './components';
import TransactionSucceeded from 'components/ExtendedNotifications/TransactionSucceeded';
import NavigationPrompt from './components/NavigationPrompt'; import NavigationPrompt from './components/NavigationPrompt';
// CONFIG // CONFIG
import { donationAddressMap, NetworkConfig, NodeConfig } from 'config/data'; import { donationAddressMap, NetworkConfig } from 'config/data';
// LIBS // LIBS
import { stripHexPrefix } from 'libs/values'; import { stripHexPrefix } from 'libs/values';
import { TransactionWithoutGas } from 'libs/messages'; import { TransactionWithoutGas } from 'libs/messages';
@ -23,14 +23,16 @@ import { RPCNode } from 'libs/nodes';
import { import {
BroadcastTransactionStatus, BroadcastTransactionStatus,
CompleteTransaction, CompleteTransaction,
confirmAndSendWeb3Transaction,
formatTxInput, formatTxInput,
generateCompleteTransaction, generateCompleteTransaction,
getBalanceMinusGasCosts, getBalanceMinusGasCosts,
TransactionInput TransactionInput
} from 'libs/transaction'; } from 'libs/transaction';
import { Ether, GWei, UnitKey, Wei } from 'libs/units'; import { UnitKey, Wei, getDecimal, toWei } from 'libs/units';
import { isValidETHAddress } from 'libs/validators'; import { isValidETHAddress } from 'libs/validators';
import { IWallet } from 'libs/wallet/IWallet'; // LIBS
import { IWallet, Balance, Web3Wallet } from 'libs/wallet';
import pickBy from 'lodash/pickBy'; import pickBy from 'lodash/pickBy';
import React from 'react'; import React from 'react';
// REDUX // REDUX
@ -51,7 +53,6 @@ import {
import { import {
getGasPriceGwei, getGasPriceGwei,
getNetworkConfig, getNetworkConfig,
getNodeConfig,
getNodeLib getNodeLib
} from 'selectors/config'; } from 'selectors/config';
import { import {
@ -86,12 +87,12 @@ interface State {
nonce: number | null | undefined; nonce: number | null | undefined;
hasSetDefaultNonce: boolean; hasSetDefaultNonce: boolean;
generateTxProcessing: boolean; generateTxProcessing: boolean;
walletAddress: string | null;
} }
interface Props { interface Props {
wallet: IWallet; wallet: IWallet;
balance: Ether; balance: Balance;
node: NodeConfig;
nodeLib: RPCNode; nodeLib: RPCNode;
network: NetworkConfig; network: NetworkConfig;
tokens: MergedToken[]; tokens: MergedToken[];
@ -122,7 +123,8 @@ const initialState: State = {
generateDisabled: true, generateDisabled: true,
nonce: null, nonce: null,
hasSetDefaultNonce: false, hasSetDefaultNonce: false,
generateTxProcessing: false generateTxProcessing: false,
walletAddress: null
}; };
export class SendTransaction extends React.Component<Props, State> { export class SendTransaction extends React.Component<Props, State> {
@ -155,8 +157,8 @@ export class SendTransaction extends React.Component<Props, State> {
// TODO listen to gas price changes here // TODO listen to gas price changes here
// TODO debounce the call // TODO debounce the call
// handle gas estimation // handle gas estimation
// if any relevant fields changed
return ( return (
// if any relevant fields changed
this.haveFieldsChanged(prevState) && this.haveFieldsChanged(prevState) &&
// if gas has not changed // if gas has not changed
!this.state.gasChanged && !this.state.gasChanged &&
@ -201,7 +203,7 @@ export class SendTransaction extends React.Component<Props, State> {
const { hasSetDefaultNonce, nonce } = this.state; const { hasSetDefaultNonce, nonce } = this.state;
const unlocked = !!wallet; const unlocked = !!wallet;
if (unlocked) { if (unlocked) {
const from = await wallet.getAddress(); const from = await wallet.getAddressString();
if (forceOffline && !offline && !hasSetDefaultNonce) { if (forceOffline && !offline && !hasSetDefaultNonce) {
const nonceHex = await nodeLib.getTransactionCount(from); const nonceHex = await nodeLib.getTransactionCount(from);
const newNonce = parseInt(stripHexPrefix(nonceHex), 10); const newNonce = parseInt(stripHexPrefix(nonceHex), 10);
@ -215,17 +217,27 @@ export class SendTransaction extends React.Component<Props, State> {
} }
public handleWalletStateOnUpdate(prevProps) { public handleWalletStateOnUpdate(prevProps) {
if (this.props.wallet !== prevProps.wallet) { if (this.props.wallet !== prevProps.wallet && !!prevProps.wallet) {
this.setState(initialState); this.setState(initialState);
} }
} }
public async setWalletAddressOnUpdate() {
if (this.props.wallet) {
const walletAddress = await this.props.wallet.getAddressString();
if (walletAddress !== this.state.walletAddress) {
this.setState({ walletAddress });
}
}
}
public componentDidUpdate(prevProps: Props, prevState: State) { public componentDidUpdate(prevProps: Props, prevState: State) {
this.handleGasEstimationOnUpdate(prevState); this.handleGasEstimationOnUpdate(prevState);
this.handleGenerateDisabledOnUpdate(); this.handleGenerateDisabledOnUpdate();
this.handleBroadcastTransactionOnUpdate(); this.handleBroadcastTransactionOnUpdate();
this.handleSetNonceWhenOfflineOnUpdate(); this.handleSetNonceWhenOfflineOnUpdate();
this.handleWalletStateOnUpdate(prevProps); this.handleWalletStateOnUpdate(prevProps);
this.setWalletAddressOnUpdate();
} }
public onNonceChange = (value: number) => { public onNonceChange = (value: number) => {
@ -236,7 +248,6 @@ export class SendTransaction extends React.Component<Props, State> {
const unlocked = !!this.props.wallet; const unlocked = !!this.props.wallet;
const { const {
to, to,
value,
unit, unit,
gasLimit, gasLimit,
data, data,
@ -249,6 +260,11 @@ export class SendTransaction extends React.Component<Props, State> {
} = this.state; } = this.state;
const { offline, forceOffline, balance } = this.props; const { offline, forceOffline, balance } = this.props;
const customMessage = customMessages.find(m => m.to === to); const customMessage = customMessages.find(m => m.to === to);
const decimal =
unit === 'ether'
? getDecimal('ether')
: (this.state.token && this.state.token.decimal) || 0;
const isWeb3Wallet = this.props.wallet instanceof Web3Wallet;
return ( return (
<TabSection> <TabSection>
<section className="Tab-content"> <section className="Tab-content">
@ -268,35 +284,38 @@ export class SendTransaction extends React.Component<Props, State> {
/> />
<div className="row"> <div className="row">
{/* Send Form */} {/* Send Form */}
{unlocked && ( {unlocked &&
<main className="col-sm-8"> !(offline || (forceOffline && isWeb3Wallet)) && (
<div className="Tab-content-pane"> <main className="col-sm-8">
{hasQueryString && ( <div className="Tab-content-pane">
<div className="alert alert-info"> {hasQueryString && (
<p>{translate('WARN_Send_Link')}</p> <div className="alert alert-info">
</div> <p>{translate('WARN_Send_Link')}</p>
)} </div>
)}
<AddressField <AddressField
placeholder={donationAddressMap.ETH} placeholder={donationAddressMap.ETH}
value={this.state.to} value={this.state.to}
onChange={readOnly ? null : this.onAddressChange} onChange={readOnly ? null : this.onAddressChange}
/> />
<AmountField <AmountField
value={value} unit={unit}
unit={unit} decimal={decimal}
balance={balance} balance={balance}
tokens={this.props.tokenBalances tokens={this.props.tokenBalances
.filter(token => !token.balance.eq(0)) .filter(token => !token.balance.eqn(0))
.map(token => token.symbol) .map(token => token.symbol)
.sort()} .sort()}
onChange={readOnly ? void 0 : this.onAmountChange} onAmountChange={this.onAmountChange}
/> isReadOnly={readOnly}
<GasField onUnitChange={this.onUnitChange}
value={gasLimit} />
onChange={readOnly ? void 0 : this.onGasChange} <GasField
/> value={gasLimit}
{(offline || forceOffline) && ( onChange={readOnly ? void 0 : this.onGasChange}
/>
{(offline || forceOffline) && (
<div> <div>
<NonceField <NonceField
value={nonce} value={nonce}
@ -305,88 +324,108 @@ export class SendTransaction extends React.Component<Props, State> {
/> />
</div> </div>
)} )}
{unit === 'ether' && ( {unit === 'ether' && (
<DataField <DataField
value={data} value={data}
onChange={readOnly ? void 0 : this.onDataChange} onChange={readOnly ? void 0 : this.onDataChange}
/> />
)} )}
<CustomMessage message={customMessage} /> <CustomMessage message={customMessage} />
<div className="row form-group"> <div className="row form-group">
<div className="col-xs-12 clearfix"> <div className="col-xs-12 clearfix">
<button <button
disabled={this.state.generateDisabled} disabled={this.state.generateDisabled}
className="btn btn-info btn-block" className="btn btn-info btn-block"
onClick={this.generateTxFromState} onClick={
> isWeb3Wallet
{translate('SEND_generate')} ? this.generateWeb3TxFromState
</button> : this.generateTxFromState
}
>
{isWeb3Wallet
? translate('Send to MetaMask / Mist')
: translate('SEND_generate')}
</button>
</div>
</div> </div>
{generateTxProcessing && (
<div className="container">
<div className="row form-group text-center">
<Spinner size="x5" />
</div>
</div>
)}
{transaction && (
<div>
<div className="row form-group">
<div className="col-sm-6">
<label>{translate('SEND_raw')}</label>
<textarea
className="form-control"
value={transaction.rawTx}
rows={4}
readOnly={true}
/>
</div>
<div className="col-sm-6">
<label>{translate('SEND_signed')}</label>
<textarea
className="form-control"
value={transaction.signedTx}
rows={4}
readOnly={true}
/>
{offline && (
<p>
To broadcast this transaction, paste the above
into{' '}
<a href="https://myetherwallet.com/pushTx">
{' '}
myetherwallet.com/pushTx
</a>{' '}
or{' '}
<a href="https://etherscan.io/pushTx">
{' '}
etherscan.io/pushTx
</a>
</p>
)}
</div>
</div>
{!offline && (
<div className="row form-group">
<div className="col-xs-12">
<button
className="btn btn-primary btn-block"
disabled={!this.state.transaction}
onClick={this.openTxModal}
>
{translate('SEND_trans')}
</button>
</div>
</div>
)}
</div>
)}
</div> </div>
</main>
)}
{generateTxProcessing && ( {unlocked &&
<div className="container"> (offline || (forceOffline && isWeb3Wallet)) && (
<div className="row form-group text-center"> <main className="col-sm-8">
<Spinner size="5x" /> <div className="Tab-content-pane">
</div> <h4>Sorry...</h4>
</div> <p>
)} MetaMask / Mist wallets are not available in offline mode.
</p>
{transaction && ( </div>
<div> </main>
<div className="row form-group"> )}
<div className="col-sm-6">
<label>{translate('SEND_raw')}</label>
<textarea
className="form-control"
value={transaction.rawTx}
rows={4}
readOnly={true}
/>
</div>
<div className="col-sm-6">
<label>{translate('SEND_signed')}</label>
<textarea
className="form-control"
value={transaction.signedTx}
rows={4}
readOnly={true}
/>
{offline && (
<p>
To broadcast this transaction, paste the above
into{' '}
<a href="https://myetherwallet.com/pushTx">
{' '}
myetherwallet.com/pushTx
</a>{' '}
or{' '}
<a href="https://etherscan.io/pushTx">
{' '}
etherscan.io/pushTx
</a>
</p>
)}
</div>
</div>
{!offline && (
<div className="form-group">
<button
className="btn btn-primary btn-block col-sm-11"
disabled={!this.state.transaction}
onClick={this.openTxModal}
>
{translate('SEND_trans')}
</button>
</div>
)}
</div>
)}
</div>
</main>
)}
{/* Sidebar */} {/* Sidebar */}
{unlocked && ( {unlocked && (
@ -398,8 +437,8 @@ export class SendTransaction extends React.Component<Props, State> {
{transaction && {transaction &&
showTxConfirm && ( showTxConfirm && (
<ConfirmationModal <ConfirmationModal
wallet={this.props.wallet} decimal={decimal}
node={this.props.node} fromAddress={this.state.walletAddress}
signedTx={transaction.signedTx} signedTx={transaction.signedTx}
onClose={this.hideConfirmTx} onClose={this.hideConfirmTx}
onConfirm={this.confirmTx} onConfirm={this.confirmTx}
@ -415,15 +454,15 @@ export class SendTransaction extends React.Component<Props, State> {
const query = queryString.parse(searchStr); const query = queryString.parse(searchStr);
const to = getParam(query, 'to'); const to = getParam(query, 'to');
const data = getParam(query, 'data'); const data = getParam(query, 'data');
// FIXME validate token against presets
const unit = getParam(query, 'tokenSymbol'); const unit = getParam(query, 'tokenSymbol');
const token = this.props.tokens.find(x => x.symbol === unit);
const value = getParam(query, 'value'); const value = getParam(query, 'value');
let gasLimit = getParam(query, 'gas'); let gasLimit = getParam(query, 'gaslimit');
if (gasLimit === null) { if (gasLimit === null) {
gasLimit = getParam(query, 'limit'); gasLimit = getParam(query, 'limit');
} }
const readOnly = getParam(query, 'readOnly') != null; const readOnly = getParam(query, 'readOnly') != null;
return { to, data, value, unit, gasLimit, readOnly }; return { to, token, data, value, unit, gasLimit, readOnly };
} }
public isValidNonce() { public isValidNonce() {
@ -488,21 +527,23 @@ export class SendTransaction extends React.Component<Props, State> {
return; return;
} }
try { if (this.props.wallet) {
const cachedFormattedTx = await this.getFormattedTxFromState(); try {
// Grab a reference to state. If it has changed by the time the estimateGas const cachedFormattedTx = await this.getFormattedTxFromState();
// call comes back, we don't want to replace the gasLimit in state. // Grab a reference to state. If it has changed by the time the estimateGas
const state = this.state; // call comes back, we don't want to replace the gasLimit in state.
gasLimit = await nodeLib.estimateGas(cachedFormattedTx); const state = this.state;
if (this.state === state) { gasLimit = await nodeLib.estimateGas(cachedFormattedTx);
this.setState({ gasLimit: formatGasLimit(gasLimit, state.unit) }); if (this.state === state) {
} else { this.setState({ gasLimit: formatGasLimit(gasLimit, state.unit) });
// state has changed, so try again from the start (with the hope that state won't change by the next time) } else {
this.estimateGas(); // state has changed, so try again from the start (with the hope that state won't change by the next time)
this.estimateGas();
}
} catch (error) {
this.setState({ generateDisabled: true });
this.props.showNotification('danger', error.message, 5000);
} }
} catch (error) {
this.setState({ generateDisabled: true });
this.props.showNotification('danger', error.message, 5000);
} }
} }
@ -529,12 +570,11 @@ export class SendTransaction extends React.Component<Props, State> {
if (unit === 'ether') { if (unit === 'ether') {
const { balance, gasPrice } = this.props; const { balance, gasPrice } = this.props;
const { gasLimit } = this.state; const { gasLimit } = this.state;
const weiBalance = balance.toWei(); const bigGasLimit = Wei(gasLimit);
const bigGasLimit = new Big(gasLimit);
value = getBalanceMinusGasCosts( value = getBalanceMinusGasCosts(
bigGasLimit, bigGasLimit,
gasPrice, gasPrice,
weiBalance balance.wei
).toString(); ).toString();
} else { } else {
const tokenBalance = this.props.tokenBalances.find( const tokenBalance = this.props.tokenBalances.find(
@ -552,23 +592,29 @@ export class SendTransaction extends React.Component<Props, State> {
if (value === 'everything') { if (value === 'everything') {
value = this.handleEverythingAmountChange(value, unit); value = this.handleEverythingAmountChange(value, unit);
} }
let transaction = this.state.transaction;
let generateDisabled = this.state.generateDisabled;
if (unit && unit !== this.state.unit) {
value = '';
transaction = null;
generateDisabled = true;
}
const token = this.props.tokens.find(x => x.symbol === unit);
this.setState({ this.setState({
value, value,
unit, unit
token,
transaction,
generateDisabled
}); });
}; };
public onUnitChange = (unit: UnitKey) => {
const token = this.props.tokens.find(x => x.symbol === unit);
let stateToSet: any = { token };
if (unit !== this.state.unit) {
stateToSet = {
...stateToSet,
transaction: null,
generateDisabled: true,
unit
};
}
this.setState(stateToSet);
};
public resetJustTx = async (): Promise<any> => public resetJustTx = async (): Promise<any> =>
new Promise(resolve => new Promise(resolve =>
this.setState( this.setState(
@ -579,6 +625,51 @@ export class SendTransaction extends React.Component<Props, State> {
) )
); );
public generateWeb3TxFromState = async () => {
await this.resetJustTx();
const { nodeLib, wallet, gasPrice, network } = this.props;
const { token, unit, value, to, data, gasLimit } = this.state;
const chainId = network.chainId;
const transactionInput = {
token,
unit,
value,
to,
data
};
const bigGasLimit = Wei(gasLimit);
if (!(wallet instanceof Web3Wallet)) {
return;
}
try {
const txHash = await confirmAndSendWeb3Transaction(
wallet,
nodeLib,
gasPrice,
bigGasLimit,
chainId,
transactionInput
);
if (network.blockExplorer !== undefined) {
this.props.showNotification(
'success',
<TransactionSucceeded
txHash={txHash}
blockExplorer={network.blockExplorer}
/>,
0
);
}
} catch (err) {
//show an error
this.props.showNotification('danger', err.message, 5000);
}
};
public generateTxFromState = async () => { public generateTxFromState = async () => {
this.setState({ generateTxProcessing: true }); this.setState({ generateTxProcessing: true });
await this.resetJustTx(); await this.resetJustTx();
@ -592,7 +683,7 @@ export class SendTransaction extends React.Component<Props, State> {
to, to,
data data
}; };
const bigGasLimit = new Big(gasLimit); const bigGasLimit = Wei(gasLimit);
try { try {
const signedTx = await generateCompleteTransaction( const signedTx = await generateCompleteTransaction(
wallet, wallet,
@ -601,6 +692,7 @@ export class SendTransaction extends React.Component<Props, State> {
bigGasLimit, bigGasLimit,
chainId, chainId,
transactionInput, transactionInput,
false,
nonce, nonce,
offline offline
); );
@ -637,11 +729,10 @@ function mapStateToProps(state: AppState) {
wallet: state.wallet.inst, wallet: state.wallet.inst,
balance: state.wallet.balance, balance: state.wallet.balance,
tokenBalances: getTokenBalances(state), tokenBalances: getTokenBalances(state),
node: getNodeConfig(state),
nodeLib: getNodeLib(state), nodeLib: getNodeLib(state),
network: getNetworkConfig(state), network: getNetworkConfig(state),
tokens: getTokens(state), tokens: getTokens(state),
gasPrice: new GWei(getGasPriceGwei(state)).toWei(), gasPrice: toWei(`${getGasPriceGwei(state)}`, getDecimal('gwei')),
transactions: state.wallet.transactions, transactions: state.wallet.transactions,
offline: state.config.offline, offline: state.config.offline,
forceOffline: state.config.forceOffline forceOffline: state.config.forceOffline

View File

@ -0,0 +1,31 @@
.SignMessage {
text-align: center;
padding-top: 30px;
&-sign {
width: 100%;
}
&-help {
margin-top: 10px;
font-size: 13px;
font-style: italic;
}
&-inputBox {
min-height: 180px;
}
&-error {
opacity: 0;
transition: none;
&.is-showing {
opacity: 1;
}
}
&-buy {
margin-top: 10px;
}
}

View File

@ -0,0 +1,131 @@
import React, { Component } from 'react';
import { connect } from 'react-redux';
import classnames from 'classnames';
import { IWallet } from 'libs/wallet/IWallet';
import WalletDecrypt from 'components/WalletDecrypt';
import translate from 'translations';
import { showNotification, TShowNotification } from 'actions/notifications';
import { ISignedMessage } from 'libs/signing';
import { AppState } from 'reducers';
import './index.scss';
interface Props {
wallet: IWallet;
showNotification: TShowNotification;
}
interface State {
message: string;
signMessageError: string;
signedMessage: ISignedMessage | null;
}
const initialState: State = {
message: '',
signMessageError: '',
signedMessage: null
};
const messagePlaceholder =
'This is a sweet message that you are signing to prove that you own the address you say you own.';
export class SignMessage extends Component<Props, State> {
public state: State = initialState;
public render() {
const { wallet } = this.props;
const { message, signedMessage } = this.state;
const messageBoxClass = classnames([
'SignMessage-inputBox',
'form-control',
message ? 'is-valid' : 'is-invalid'
]);
return (
<div>
<div className="Tab-content-pane">
<h4>{translate('MSG_message')}</h4>
<div className="form-group">
<textarea
className={messageBoxClass}
placeholder={messagePlaceholder}
value={message}
onChange={this.handleMessageChange}
/>
<div className="SignMessage-help">{translate('MSG_info2')}</div>
</div>
{!!wallet && (
<button
className="SignMessage-sign btn btn-primary btn-lg"
onClick={this.handleSignMessage}
>
{translate('NAV_SignMsg')}
</button>
)}
{!!signedMessage && (
<div>
<h4>{translate('MSG_signature')}</h4>
<div className="form-group">
<textarea
className="SignMessage-inputBox form-control"
value={JSON.stringify(signedMessage, null, 2)}
disabled={true}
onChange={this.handleMessageChange}
/>
</div>
</div>
)}
</div>
{!wallet && <WalletDecrypt />}
</div>
);
}
private handleSignMessage = async () => {
const { wallet } = this.props;
const { message } = this.state;
if (!wallet) {
return;
}
try {
const signedMessage: ISignedMessage = {
address: await wallet.getAddressString(),
message,
signature: await wallet.signMessage(message),
version: '2'
};
this.setState({ signedMessage });
this.props.showNotification(
'success',
`Successfully signed message with address ${signedMessage.address}.`
);
} catch (err) {
this.props.showNotification(
'danger',
`Error signing message: ${err.message}`
);
}
};
private handleMessageChange = (e: React.FormEvent<HTMLTextAreaElement>) => {
const message = e.currentTarget.value;
this.setState({ message });
};
}
function mapStateToProps(state: AppState) {
return {
wallet: state.wallet.inst
};
}
export default connect(mapStateToProps, {
showNotification
})(SignMessage);

View File

@ -0,0 +1,28 @@
.VerifyMessage {
text-align: center;
padding-top: 30px;
&-sign {
width: 100%;
}
&-help {
margin-top: 10px;
font-size: 13px;
font-style: italic;
}
&-inputBox {
min-height: 180px;
}
&-success {
opacity: 1;
transition: none;
margin-top: 10px;
}
&-buy {
margin-top: 10px;
}
}

View File

@ -0,0 +1,105 @@
import React, { Component } from 'react';
import { connect } from 'react-redux';
import classnames from 'classnames';
import translate from 'translations';
import { showNotification, TShowNotification } from 'actions/notifications';
import { verifySignedMessage, ISignedMessage } from 'libs/signing';
import './index.scss';
interface Props {
showNotification: TShowNotification;
}
interface State {
signature: string;
verifiedAddress?: string;
verifiedMessage?: string;
}
const initialState: State = {
signature: ''
};
const signaturePlaceholder =
'{"address":"0x7cB57B5A97eAbe94205C07890BE4c1aD31E486A8","message":"asdfasdfasdf","signature":"0x4771d78f13ba8abf608457f12471f427ca8f2fb046c1acb3f5969eefdfe452a10c9154136449f595a654b44b3b0163e86dd099beaca83bfd52d64c21da2221bb1c","version":"2"}';
export class VerifyMessage extends Component<Props, State> {
public state: State = initialState;
public render() {
const { verifiedAddress, verifiedMessage, signature } = this.state;
const signatureBoxClass = classnames([
'VerifyMessage-inputBox',
'form-control',
signature ? 'is-valid' : 'is-invalid'
]);
return (
<div>
<div className="Tab-content-pane">
<h4>{translate('MSG_signature')}</h4>
<div className="form-group">
<textarea
className={signatureBoxClass}
placeholder={signaturePlaceholder}
value={signature}
onChange={this.handleSignatureChange}
/>
</div>
<button
className="VerifyMessage-sign btn btn-primary btn-lg"
onClick={this.handleVerifySignedMessage}
disabled={false}
>
{translate('MSG_verify')}
</button>
{!!verifiedAddress &&
!!verifiedMessage && (
<div className="VerifyMessage-success alert alert-success">
<strong>{verifiedAddress}</strong> did sign the message{' '}
<strong>{verifiedMessage}</strong>.
</div>
)}
</div>
</div>
);
}
private clearVerifiedData = () =>
this.setState({
verifiedAddress: '',
verifiedMessage: ''
});
private handleVerifySignedMessage = () => {
try {
const parsedSignature: ISignedMessage = JSON.parse(this.state.signature);
if (!verifySignedMessage(parsedSignature)) {
throw Error();
}
const { address, message } = parsedSignature;
this.setState({
verifiedAddress: address,
verifiedMessage: message
});
this.props.showNotification('success', translate('SUCCESS_7'));
} catch (err) {
this.clearVerifiedData();
this.props.showNotification('danger', translate('ERROR_12'));
}
};
private handleSignatureChange = (e: React.FormEvent<HTMLTextAreaElement>) => {
const signature = e.currentTarget.value;
this.setState({ signature });
};
}
export default connect(null, {
showNotification
})(VerifyMessage);

View File

@ -0,0 +1,30 @@
@import 'common/sass/variables';
@import 'common/sass/mixins';
.SignAndVerifyMsg {
&-header {
margin: 0;
text-align: center;
&-tab {
@include reset-button;
color: $ether-blue;
&:hover,
&:active {
opacity: 0.8;
}
&.is-active {
&,
&:hover,
&:active {
color: $text-color;
cursor: default;
opacity: 1;
font-weight: 500;
}
}
}
}
}

View File

@ -0,0 +1,59 @@
import React, { Component } from 'react';
import translate from 'translations';
import SignMessage from './components/SignMessage';
import VerifyMessage from './components/VerifyMessage';
import TabSection from 'containers/TabSection';
import './index.scss';
interface State {
activeTab: string;
}
export default class SignAndVerifyMessage extends Component<{}, State> {
public state: State = {
activeTab: 'sign'
};
public changeTab = activeTab => () => this.setState({ activeTab });
public render() {
const { activeTab } = this.state;
let content;
let signActive = '';
let verifyActive = '';
if (activeTab === 'sign') {
content = <SignMessage />;
signActive = 'is-active';
} else {
content = <VerifyMessage />;
verifyActive = 'is-active';
}
return (
<TabSection>
<section className="Tab-content SignAndVerifyMsg">
<div className="Tab-content-pane">
<h1 className="SignAndVerifyMsg-header">
<button
className={`SignAndVerifyMsg-header-tab ${signActive}`}
onClick={this.changeTab('sign')}
>
{translate('Sign Message')}
</button>{' '}
<span>or</span>{' '}
<button
className={`SignAndVerifyMsg-header-tab ${verifyActive}`}
onClick={this.changeTab('verify')}
>
{translate('Verify Message')}
</button>
</h1>
</div>
<main role="main">{content}</main>
</section>
</TabSection>
);
}
}

View File

@ -1,4 +1,4 @@
@import "common/sass/variables"; @import 'common/sass/variables';
.CurrencySwap { .CurrencySwap {
text-align: center; text-align: center;
@ -13,6 +13,30 @@
display: inline-block; display: inline-block;
vertical-align: top; vertical-align: top;
} }
&-input-group {
display: inline-block;
}
&-error-message {
display: block;
min-height: 25px;
color: $brand-danger;
text-align: left;
}
&-inner-wrap {
display: block;
}
@media (min-width: $screen-xs-min) {
&-inner-wrap {
display: flex;
align-items: center;
justify-content: center;
}
}
&-dropdown {
display: inline-block;
margin-top: 0.6rem;
margin-bottom: 0.6rem;
}
&-input { &-input {
width: 100%; width: 100%;

Some files were not shown because too many files have changed in this diff Show More