commit
4d508bc081
15
.flowconfig
15
.flowconfig
|
@ -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'
|
|
|
@ -55,3 +55,4 @@ webpack_config/server.csr
|
||||||
|
|
||||||
|
|
||||||
v8-compile-cache-0/
|
v8-compile-cache-0/
|
||||||
|
package-lock.json
|
||||||
|
|
|
@ -1,8 +0,0 @@
|
||||||
FROM node:8.1.4
|
|
||||||
|
|
||||||
WORKDIR /usr/app
|
|
||||||
|
|
||||||
COPY package.json .
|
|
||||||
RUN npm install --quiet
|
|
||||||
|
|
||||||
COPY . .
|
|
215
README.md
215
README.md
|
@ -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">
|
||||||
|
|
|
@ -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>
|
||||||
|
);
|
||||||
|
});
|
|
@ -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
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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'
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
|
||||||
};
|
|
||||||
}
|
|
|
@ -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;
|
|
|
@ -1,4 +0,0 @@
|
||||||
export enum TypeKeys {
|
|
||||||
ACCESS_CONTRACT = 'ACCESS_CONTRACT',
|
|
||||||
SET_INTERACTIVE_CONTRACT = 'SET_INTERACTIVE_CONTRACT'
|
|
||||||
}
|
|
|
@ -1,3 +0,0 @@
|
||||||
export * from './constants';
|
|
||||||
export * from './actionTypes';
|
|
||||||
export * from './actionCreators';
|
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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';
|
||||||
|
|
|
@ -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
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -13,7 +13,8 @@ export function showNotification(
|
||||||
payload: {
|
payload: {
|
||||||
level,
|
level,
|
||||||
msg,
|
msg,
|
||||||
duration
|
duration,
|
||||||
|
id: Math.random()
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
|
@ -18,6 +18,6 @@ export interface FetchCCRatesFailed {
|
||||||
|
|
||||||
/*** Union Type ***/
|
/*** Union Type ***/
|
||||||
export type RatesAction =
|
export type RatesAction =
|
||||||
| FetchCCRatesSucceeded
|
|
||||||
| FetchCCRates
|
| FetchCCRates
|
||||||
|
| FetchCCRatesSucceeded
|
||||||
| FetchCCRatesFailed;
|
| FetchCCRatesFailed;
|
||||||
|
|
|
@ -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
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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'
|
||||||
|
}
|
||||||
|
|
|
@ -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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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;
|
||||||
|
}, {});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -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}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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);
|
||||||
|
};
|
||||||
|
}
|
|
@ -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() {
|
||||||
|
|
|
@ -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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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);
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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}
|
||||||
|
|
|
@ -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';
|
||||||
|
|
|
@ -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)) {
|
||||||
|
|
|
@ -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();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
|
@ -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>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
|
@ -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,
|
||||||
|
|
|
@ -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';
|
||||||
|
|
|
@ -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);
|
||||||
|
};
|
||||||
|
}
|
|
@ -0,0 +1 @@
|
||||||
|
export * from './UnitConverter';
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,10 @@
|
||||||
|
import React from 'react';
|
||||||
|
import './Code.scss';
|
||||||
|
|
||||||
|
const Code = ({ children }) => (
|
||||||
|
<pre>
|
||||||
|
<code>{children}</code>
|
||||||
|
</pre>
|
||||||
|
);
|
||||||
|
|
||||||
|
export default Code;
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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);
|
||||||
}
|
}
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -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';
|
||||||
|
|
||||||
|
|
|
@ -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[];
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
|
@ -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;
|
||||||
|
|
|
@ -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;
|
|
@ -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';
|
||||||
|
|
|
@ -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);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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);
|
||||||
|
|
|
@ -0,0 +1,7 @@
|
||||||
|
@import "common/sass/variables";
|
||||||
|
|
||||||
|
.BroadcastTx {
|
||||||
|
&-title {
|
||||||
|
margin: $space auto $space * 2.5;
|
||||||
|
}
|
||||||
|
}
|
|
@ -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
|
||||||
|
);
|
|
@ -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);
|
||||||
|
};
|
|
@ -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
|
||||||
|
};
|
|
@ -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);
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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;
|
|
@ -0,0 +1,14 @@
|
||||||
|
.InteractForm {
|
||||||
|
&-address {
|
||||||
|
display: flex;
|
||||||
|
|
||||||
|
> * {
|
||||||
|
flex: 1;
|
||||||
|
margin-right: 10px;
|
||||||
|
|
||||||
|
&:last-child {
|
||||||
|
margin-right: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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);
|
|
@ -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);
|
|
@ -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;
|
|
@ -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>
|
||||||
|
);
|
||||||
|
};
|
|
@ -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);
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
|
@ -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);
|
||||||
|
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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;
|
|
@ -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();
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -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} />;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
|
@ -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 (
|
||||||
|
|
|
@ -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);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
|
@ -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);
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
|
@ -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);
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
|
@ -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
Loading…
Reference in New Issue