mirror of
https://github.com/status-im/MyCrypto.git
synced 2025-02-28 18:50:59 +00:00
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'
|
1
.gitignore
vendored
1
.gitignore
vendored
@ -55,3 +55,4 @@ webpack_config/server.csr
|
||||
|
||||
|
||||
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)
|
||||
|
||||
[](https://greenkeeper.io/)
|
||||
|
||||
#### Run:
|
||||
|
||||
```bash
|
||||
@ -36,7 +38,7 @@ npm run dev:https
|
||||
2. [dternyak/eth-priv-to-addr](https://hub.docker.com/r/dternyak/eth-priv-to-addr/) pulled from DockerHub
|
||||
|
||||
##### Docker setup instructions:
|
||||
1. Install docker (on macOS, I suggest [Docker for Mac](https://docs.docker.com/docker-for-mac/))
|
||||
1. Install docker (on macOS, [Docker for Mac](https://docs.docker.com/docker-for-mac/) is suggested)
|
||||
2. `docker pull dternyak/eth-priv-to-addr`
|
||||
|
||||
##### Run Derivation Checker
|
||||
@ -48,40 +50,32 @@ npm run derivation-checker
|
||||
|
||||
```
|
||||
│
|
||||
├── common - Your App
|
||||
├── common
|
||||
│ ├── actions - application actions
|
||||
│ ├── api - Services and XHR utils(also custom form validation, see InputComponent from components/common)
|
||||
│ ├── api - Services and XHR utils
|
||||
│ ├── components - components according to "Redux philosophy"
|
||||
│ ├── config - frontend config depending on REACT_WEBPACK_ENV
|
||||
│ ├── containers - containers according to "Redux philosophy"
|
||||
│ ├── reducers - application reducers
|
||||
│ ├── routing - application routing
|
||||
│ ├── index.jsx - entry
|
||||
│ ├── index.tsx - entry
|
||||
│ ├── index.html
|
||||
├── static
|
||||
├── webpack_config - Webpack configuration
|
||||
├── jest_config - Jest configuration
|
||||
```
|
||||
|
||||
## Docker setup
|
||||
You should already have docker and docker-compose setup for your platform as a pre-req.
|
||||
|
||||
```bash
|
||||
docker-compose up
|
||||
```
|
||||
|
||||
## Style Guides and Philosophies
|
||||
|
||||
The following are guides for developers to follow for writing compliant code.
|
||||
|
||||
|
||||
|
||||
### Redux and Actions
|
||||
|
||||
Each reducer has one file in `reducers/[namespace].js` that contains the reducer
|
||||
and initial state, one file in `actions/[namespace].js` that contains the action
|
||||
Each reducer has one file in `reducers/[namespace].ts` that contains the reducer
|
||||
and initial state, one file in `actions/[namespace].ts` that contains the action
|
||||
creators and their return types, and optionally one file in
|
||||
`sagas/[namespace].js` that handles action side effects using
|
||||
`sagas/[namespace].ts` that handles action side effects using
|
||||
[`redux-saga`](https://github.com/redux-saga/redux-saga).
|
||||
|
||||
The files should be laid out as follows:
|
||||
@ -89,75 +83,192 @@ The files should be laid out as follows:
|
||||
#### Reducer
|
||||
|
||||
* State should be explicitly defined and exported
|
||||
* Initial state should match state flow typing, define every key
|
||||
* Reducer function should handle all cases for actions. If state does not change
|
||||
as a result of an action (Because it merely kicks off side-effects in saga) then
|
||||
define the case above default, and have it fall through.
|
||||
* Initial state should match state typing, define every key
|
||||
|
||||
```js
|
||||
// @flow
|
||||
import type { NamespaceAction } from "actions/namespace";
|
||||
```ts
|
||||
import { NamespaceAction } from "actions/[namespace]";
|
||||
import { TypeKeys } from 'actions/[namespace]/constants';
|
||||
|
||||
export type State = { /* Flowtype definition for state object */ };
|
||||
export interface State { /* definition for state object */ };
|
||||
export const INITIAL_STATE: State = { /* Initial state shape */ };
|
||||
|
||||
export function namespace(
|
||||
export function [namespace](
|
||||
state: State = INITIAL_STATE,
|
||||
action: NamespaceAction
|
||||
): State {
|
||||
switch (action.type) {
|
||||
case 'NAMESPACE_NAME_OF_ACTION':
|
||||
case TypeKeys.NAMESPACE_NAME_OF_ACTION:
|
||||
return {
|
||||
...state,
|
||||
// Alterations to state
|
||||
};
|
||||
|
||||
case 'NAMESPACE_NAME_OF_SAGA_ACTION':
|
||||
};
|
||||
default:
|
||||
// Ensures every action was handled in reducer
|
||||
// Unhandled actions should just fall into default
|
||||
(action: empty);
|
||||
return state;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### Actions
|
||||
* Define each action creator in `actionCreator.ts`
|
||||
* Define each action object type in `actionTypes.ts`
|
||||
* Export a union of all of the action types for use by the reducer
|
||||
* Define each action type as a string enum in `constants.ts`
|
||||
* Export `actionCreators` and `actionTypes` from module file `index.ts`
|
||||
|
||||
* Define each action object type beside the action creator
|
||||
* Export a union of all of the action types for use by the reducer
|
||||
|
||||
```js
|
||||
```
|
||||
├── common
|
||||
├── actions - application actions
|
||||
├── [namespace] - action namespace
|
||||
├── actionCreators.ts - action creators
|
||||
├── actionTypes.ts - action interfaces / types
|
||||
├── constants.ts - string enum
|
||||
├── index.ts - exports all action creators and action object types
|
||||
```
|
||||
##### constants.ts
|
||||
```ts
|
||||
export enum TypeKeys {
|
||||
NAMESPACE_NAME_OF_ACTION = 'NAMESPACE_NAME_OF_ACTION'
|
||||
}
|
||||
```
|
||||
##### actionTypes.ts
|
||||
```ts
|
||||
/*** Name of action ***/
|
||||
export type NameOfActionAction = {
|
||||
type: 'NAMESPACE_NAME_OF_ACTION',
|
||||
export interface NameOfActionAction {
|
||||
type: TypeKeys.NAMESPACE_NAME_OF_ACTION,
|
||||
/* Rest of the action object shape */
|
||||
};
|
||||
|
||||
export function nameOfAction(): NameOfActionAction {
|
||||
return {
|
||||
type: 'NAMESPACE_NAME_OF_ACTION',
|
||||
/* Rest of the action object */
|
||||
};
|
||||
};
|
||||
|
||||
/*** Action Union ***/
|
||||
export type NamespaceAction =
|
||||
| ActionOneAction
|
||||
| ActionTwoAction
|
||||
| ActionThreeAction;
|
||||
```
|
||||
##### actionCreators.ts
|
||||
```ts
|
||||
import * as interfaces from './actionTypes';
|
||||
import { TypeKeys } from './constants';
|
||||
|
||||
#### Action Constants
|
||||
export interface TNameOfAction = typeof nameOfAction;
|
||||
export function nameOfAction(): interfaces.NameOfActionAction {
|
||||
return {
|
||||
type: TypeKeys.NAMESPACE_NAME_OF_ACTION,
|
||||
payload: {}
|
||||
};
|
||||
};
|
||||
```
|
||||
##### index.ts
|
||||
```ts
|
||||
export * from './actionCreators';
|
||||
export * from './actionTypes';
|
||||
```
|
||||
|
||||
Action constants are not used thanks to flow type checking. To avoid typos, we
|
||||
use `(action: empty)` in the default case which assures every case is accounted
|
||||
for. If you need to use another reducer's action, import that action type into
|
||||
your reducer, and create a new action union of your actions, and the other
|
||||
action types used.
|
||||
### Typing Redux-Connected Components
|
||||
|
||||
Components that receive props directly from redux as a result of the `connect`
|
||||
function should use AppState for typing, rather than manually defining types.
|
||||
This makes refactoring reducers easier by catching mismatches or changes of
|
||||
types in components, and reduces the chance for inconsistency. It's also less
|
||||
code overall.
|
||||
|
||||
```
|
||||
// Do this
|
||||
import { AppState } from 'reducers';
|
||||
|
||||
interface Props {
|
||||
wallet: AppState['wallet']['inst'];
|
||||
rates: AppState['rates']['rates'];
|
||||
// ...
|
||||
}
|
||||
|
||||
// Not this
|
||||
import { IWallet } from 'libs/wallet';
|
||||
import { Rates } from 'libs/rates';
|
||||
|
||||
interface Props {
|
||||
wallet: IWallet;
|
||||
rates: Rates;
|
||||
// ...
|
||||
}
|
||||
```
|
||||
|
||||
However, if you have a sub-component that takes in props from a connected
|
||||
component, it's OK to manually specify the type. Especially if you go from
|
||||
being type-or-null to guaranteeing the prop will be passed (because of a
|
||||
conditional render.)
|
||||
|
||||
### Higher Order Components
|
||||
|
||||
#### Typing Injected Props
|
||||
Props made available through higher order components can be tricky to type. Normally, if a component requires a prop, you add it to the component's interface and it just works. However, working with injected props from [higher order components](https://medium.com/@DanHomola/react-higher-order-components-in-typescript-made-simple-6f9b55691af1), you will be forced to supply all required props whenever you compose the component.
|
||||
|
||||
```ts
|
||||
interface MyComponentProps {
|
||||
name: string;
|
||||
countryCode?: string;
|
||||
routerLocation: { pathname: string };
|
||||
}
|
||||
|
||||
...
|
||||
|
||||
class OtherComponent extends React.Component<{}, {}> {
|
||||
render() {
|
||||
return (
|
||||
<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
|
||||
|
||||
@ -165,12 +276,12 @@ Legacy styles are housed under `common/assets/styles` and written with LESS.
|
||||
However, going forward, each styled component should create a a `.scss` file of
|
||||
the same name in the same folder, and import it like so:
|
||||
|
||||
```js
|
||||
```ts
|
||||
import React from "react";
|
||||
|
||||
import "./MyComponent.scss";
|
||||
|
||||
export default class MyComponent extends React.component {
|
||||
export default class MyComponent extends React.component<{}, {}> {
|
||||
render() {
|
||||
return (
|
||||
<div className="MyComponent">
|
||||
|
87
common/Root.tsx
Normal file
87
common/Root.tsx
Normal file
@ -0,0 +1,87 @@
|
||||
import React, { Component } from 'react';
|
||||
import { Provider } from 'react-redux';
|
||||
import { withRouter, Switch, Redirect, Router, Route } from 'react-router-dom';
|
||||
// Components
|
||||
import Contracts from 'containers/Tabs/Contracts';
|
||||
import ENS from 'containers/Tabs/ENS';
|
||||
import GenerateWallet from 'containers/Tabs/GenerateWallet';
|
||||
import Help from 'containers/Tabs/Help';
|
||||
import SendTransaction from 'containers/Tabs/SendTransaction';
|
||||
import Swap from 'containers/Tabs/Swap';
|
||||
import ViewWallet from 'containers/Tabs/ViewWallet';
|
||||
import SignAndVerifyMessage from 'containers/Tabs/SignAndVerifyMessage';
|
||||
import BroadcastTx from 'containers/Tabs/BroadcastTx';
|
||||
|
||||
// TODO: fix this
|
||||
interface Props {
|
||||
store: any;
|
||||
history: any;
|
||||
}
|
||||
|
||||
export default class Root extends Component<Props, {}> {
|
||||
public render() {
|
||||
const { store, history } = this.props;
|
||||
// key={Math.random()} = hack for HMR from https://github.com/webpack/webpack-dev-server/issues/395
|
||||
return (
|
||||
<Provider store={store} key={Math.random()}>
|
||||
<Router history={history} key={Math.random()}>
|
||||
<div>
|
||||
<Route exact={true} path="/" component={GenerateWallet} />
|
||||
<Route path="/view-wallet" component={ViewWallet} />
|
||||
<Route path="/help" component={Help} />
|
||||
<Route path="/swap" component={Swap} />
|
||||
<Route path="/send-transaction" component={SendTransaction} />
|
||||
<Route path="/contracts" component={Contracts} />
|
||||
<Route path="/ens" component={ENS} />
|
||||
<Route
|
||||
path="/sign-and-verify-message"
|
||||
component={SignAndVerifyMessage}
|
||||
/>
|
||||
<Route path="/pushTx" component={BroadcastTx} />
|
||||
|
||||
<LegacyRoutes />
|
||||
</div>
|
||||
</Router>
|
||||
</Provider>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const LegacyRoutes = withRouter(props => {
|
||||
const { history } = props;
|
||||
const { pathname, hash } = props.location;
|
||||
|
||||
if (pathname === '/') {
|
||||
switch (hash) {
|
||||
case '#send-transaction':
|
||||
case '#offline-transaction':
|
||||
history.push('/send-transaction');
|
||||
break;
|
||||
case '#generate-wallet':
|
||||
history.push('/');
|
||||
break;
|
||||
case '#swap':
|
||||
history.push('/swap');
|
||||
break;
|
||||
case '#contracts':
|
||||
history.push('/contracts');
|
||||
break;
|
||||
case '#ens':
|
||||
history.push('/ens');
|
||||
break;
|
||||
case '#view-wallet-info':
|
||||
history.push('/view-wallet');
|
||||
break;
|
||||
case '#check-tx-status':
|
||||
history.push('/check-tx-status');
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<Switch>
|
||||
<Redirect from="/signmsg.html" to="/sign-and-verify-message" />
|
||||
<Redirect from="/helpers.html" to="/helpers" />
|
||||
</Switch>
|
||||
);
|
||||
});
|
@ -1,5 +1,6 @@
|
||||
import * as interfaces from './actionTypes';
|
||||
import { TypeKeys } from './constants';
|
||||
import { NodeConfig, CustomNodeConfig } from 'config/data';
|
||||
|
||||
export type TForceOfflineConfig = typeof forceOfflineConfig;
|
||||
export function forceOfflineConfig(): interfaces.ForceOfflineAction {
|
||||
@ -24,10 +25,13 @@ export function changeLanguage(sign: string): interfaces.ChangeLanguageAction {
|
||||
}
|
||||
|
||||
export type TChangeNode = typeof changeNode;
|
||||
export function changeNode(value: string): interfaces.ChangeNodeAction {
|
||||
export function changeNode(
|
||||
nodeSelection: string,
|
||||
node: NodeConfig
|
||||
): interfaces.ChangeNodeAction {
|
||||
return {
|
||||
type: TypeKeys.CONFIG_NODE_CHANGE,
|
||||
payload: value
|
||||
payload: { nodeSelection, node }
|
||||
};
|
||||
}
|
||||
|
||||
@ -55,3 +59,40 @@ export function changeNodeIntent(
|
||||
payload
|
||||
};
|
||||
}
|
||||
|
||||
export type TAddCustomNode = typeof addCustomNode;
|
||||
export function addCustomNode(
|
||||
payload: CustomNodeConfig
|
||||
): interfaces.AddCustomNodeAction {
|
||||
return {
|
||||
type: TypeKeys.CONFIG_ADD_CUSTOM_NODE,
|
||||
payload
|
||||
};
|
||||
}
|
||||
|
||||
export type TRemoveCustomNode = typeof removeCustomNode;
|
||||
export function removeCustomNode(
|
||||
payload: CustomNodeConfig
|
||||
): interfaces.RemoveCustomNodeAction {
|
||||
return {
|
||||
type: TypeKeys.CONFIG_REMOVE_CUSTOM_NODE,
|
||||
payload
|
||||
};
|
||||
}
|
||||
|
||||
export type TSetLatestBlock = typeof setLatestBlock;
|
||||
export function setLatestBlock(
|
||||
payload: string
|
||||
): interfaces.SetLatestBlockAction {
|
||||
return {
|
||||
type: TypeKeys.CONFIG_SET_LATEST_BLOCK,
|
||||
payload
|
||||
};
|
||||
}
|
||||
|
||||
export type TWeb3UnsetNode = typeof web3UnsetNode;
|
||||
export function web3UnsetNode(): interfaces.Web3UnsetNodeAction {
|
||||
return {
|
||||
type: TypeKeys.CONFIG_NODE_WEB3_UNSET
|
||||
};
|
||||
}
|
||||
|
@ -1,4 +1,5 @@
|
||||
import { TypeKeys } from './constants';
|
||||
import { CustomNodeConfig, NodeConfig } from 'config/data';
|
||||
|
||||
/*** Toggle Offline ***/
|
||||
export interface ToggleOfflineAction {
|
||||
@ -20,7 +21,10 @@ export interface ChangeLanguageAction {
|
||||
export interface ChangeNodeAction {
|
||||
type: TypeKeys.CONFIG_NODE_CHANGE;
|
||||
// FIXME $keyof?
|
||||
payload: string;
|
||||
payload: {
|
||||
nodeSelection: string;
|
||||
node: NodeConfig;
|
||||
};
|
||||
}
|
||||
|
||||
/*** Change gas price ***/
|
||||
@ -40,6 +44,29 @@ export interface ChangeNodeIntentAction {
|
||||
payload: string;
|
||||
}
|
||||
|
||||
/*** Add Custom Node ***/
|
||||
export interface AddCustomNodeAction {
|
||||
type: TypeKeys.CONFIG_ADD_CUSTOM_NODE;
|
||||
payload: CustomNodeConfig;
|
||||
}
|
||||
|
||||
/*** Remove Custom Node ***/
|
||||
export interface RemoveCustomNodeAction {
|
||||
type: TypeKeys.CONFIG_REMOVE_CUSTOM_NODE;
|
||||
payload: CustomNodeConfig;
|
||||
}
|
||||
|
||||
/*** Set Latest Block ***/
|
||||
export interface SetLatestBlockAction {
|
||||
type: TypeKeys.CONFIG_SET_LATEST_BLOCK;
|
||||
payload: string;
|
||||
}
|
||||
|
||||
/*** Unset Web3 as a Node ***/
|
||||
export interface Web3UnsetNodeAction {
|
||||
type: TypeKeys.CONFIG_NODE_WEB3_UNSET;
|
||||
}
|
||||
|
||||
/*** Union Type ***/
|
||||
export type ConfigAction =
|
||||
| ChangeNodeAction
|
||||
@ -48,4 +75,8 @@ export type ConfigAction =
|
||||
| ToggleOfflineAction
|
||||
| PollOfflineStatus
|
||||
| ForceOfflineAction
|
||||
| ChangeNodeIntentAction;
|
||||
| ChangeNodeIntentAction
|
||||
| AddCustomNodeAction
|
||||
| RemoveCustomNodeAction
|
||||
| SetLatestBlockAction
|
||||
| Web3UnsetNodeAction;
|
||||
|
@ -5,5 +5,9 @@ export enum TypeKeys {
|
||||
CONFIG_GAS_PRICE = 'CONFIG_GAS_PRICE',
|
||||
CONFIG_TOGGLE_OFFLINE = 'CONFIG_TOGGLE_OFFLINE',
|
||||
CONFIG_FORCE_OFFLINE = 'CONFIG_FORCE_OFFLINE',
|
||||
CONFIG_POLL_OFFLINE_STATUS = 'CONFIG_POLL_OFFLINE_STATUS'
|
||||
CONFIG_POLL_OFFLINE_STATUS = 'CONFIG_POLL_OFFLINE_STATUS',
|
||||
CONFIG_ADD_CUSTOM_NODE = 'CONFIG_ADD_CUSTOM_NODE',
|
||||
CONFIG_REMOVE_CUSTOM_NODE = 'CONFIG_REMOVE_CUSTOM_NODE',
|
||||
CONFIG_SET_LATEST_BLOCK = 'CONFIG_SET_LATEST_BLOCK',
|
||||
CONFIG_NODE_WEB3_UNSET = 'CONFIG_NODE_WEB3_UNSET'
|
||||
}
|
||||
|
@ -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 {
|
||||
[key: string]: BigNumber;
|
||||
export interface ITokenData {
|
||||
value: TokenValue;
|
||||
decimal: number;
|
||||
}
|
||||
|
||||
export interface ITokenValues {
|
||||
[key: string]: ITokenData | null;
|
||||
}
|
||||
|
||||
export interface DeterministicWalletData {
|
||||
index: number;
|
||||
address: string;
|
||||
value?: BigNumber;
|
||||
tokenValues: TokenValues;
|
||||
value?: TokenValue;
|
||||
tokenValues: ITokenValues;
|
||||
}
|
||||
|
||||
/*** Get determinstic wallets ***/
|
||||
@ -39,8 +44,8 @@ export interface SetDesiredTokenAction {
|
||||
/*** Set wallet values ***/
|
||||
export interface UpdateDeterministicWalletArgs {
|
||||
address: string;
|
||||
value?: BigNumber;
|
||||
tokenValues?: TokenValues;
|
||||
value?: Wei;
|
||||
tokenValues?: ITokenValues;
|
||||
index?: any;
|
||||
}
|
||||
|
||||
|
@ -1,5 +1,3 @@
|
||||
import * as constants from './constants';
|
||||
|
||||
/*** Resolve ENS name ***/
|
||||
export interface ResolveEnsNameAction {
|
||||
type: 'ENS_RESOLVE';
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { PrivKeyWallet } from 'libs/wallet';
|
||||
import { generate } from 'ethereumjs-wallet';
|
||||
import * as interfaces from './actionTypes';
|
||||
import { TypeKeys } from './constants';
|
||||
|
||||
@ -8,7 +8,7 @@ export function generateNewWallet(
|
||||
): interfaces.GenerateNewWalletAction {
|
||||
return {
|
||||
type: TypeKeys.GENERATE_WALLET_GENERATE_WALLET,
|
||||
wallet: PrivKeyWallet.generate(),
|
||||
wallet: generate(),
|
||||
password
|
||||
};
|
||||
}
|
||||
|
@ -1,10 +1,10 @@
|
||||
import { PrivKeyWallet } from 'libs/wallet';
|
||||
import { IFullWallet } from 'ethereumjs-wallet';
|
||||
import { TypeKeys } from './constants';
|
||||
|
||||
/*** Generate Wallet File ***/
|
||||
export interface GenerateNewWalletAction {
|
||||
type: TypeKeys.GENERATE_WALLET_GENERATE_WALLET;
|
||||
wallet: PrivKeyWallet;
|
||||
wallet: IFullWallet;
|
||||
password: string;
|
||||
}
|
||||
|
||||
|
@ -13,7 +13,8 @@ export function showNotification(
|
||||
payload: {
|
||||
level,
|
||||
msg,
|
||||
duration
|
||||
duration,
|
||||
id: Math.random()
|
||||
}
|
||||
};
|
||||
}
|
||||
|
@ -7,6 +7,7 @@ export type INFINITY = 'infinity';
|
||||
export interface Notification {
|
||||
level: NOTIFICATION_LEVEL;
|
||||
msg: ReactElement<any> | string;
|
||||
id: number;
|
||||
duration?: number | INFINITY;
|
||||
}
|
||||
|
||||
|
@ -3,9 +3,26 @@ import { TypeKeys } from './constants';
|
||||
import { fetchRates, CCResponse } from './actionPayloads';
|
||||
|
||||
export type TFetchCCRates = typeof fetchCCRates;
|
||||
export function fetchCCRates(): interfaces.FetchCCRates {
|
||||
export function fetchCCRates(symbols: string[] = []): interfaces.FetchCCRates {
|
||||
return {
|
||||
type: TypeKeys.RATES_FETCH_CC,
|
||||
payload: fetchRates()
|
||||
payload: fetchRates(symbols)
|
||||
};
|
||||
}
|
||||
|
||||
export type TFetchCCRatesSucceeded = typeof fetchCCRatesSucceeded;
|
||||
export function fetchCCRatesSucceeded(
|
||||
payload: CCResponse
|
||||
): interfaces.FetchCCRatesSucceeded {
|
||||
return {
|
||||
type: TypeKeys.RATES_FETCH_CC_SUCCEEDED,
|
||||
payload
|
||||
};
|
||||
}
|
||||
|
||||
export type TFetchCCRatesFailed = typeof fetchCCRatesFailed;
|
||||
export function fetchCCRatesFailed(): interfaces.FetchCCRatesFailed {
|
||||
return {
|
||||
type: TypeKeys.RATES_FETCH_CC_FAILED
|
||||
};
|
||||
}
|
||||
|
@ -1,22 +1,52 @@
|
||||
import { handleJSONResponse } from 'api/utils';
|
||||
|
||||
export const symbols = ['USD', 'EUR', 'GBP', 'BTC', 'CHF', 'REP'];
|
||||
const symbolsURL = symbols.join(',');
|
||||
export const rateSymbols = ['USD', 'EUR', 'GBP', 'BTC', 'CHF', 'REP', 'ETH'];
|
||||
// TODO - internationalize
|
||||
const ERROR_MESSAGE = 'Could not fetch rate data.';
|
||||
const CCApi = 'https://min-api.cryptocompare.com';
|
||||
|
||||
const CCRates = CCSymbols => `${CCApi}/data/price?fsym=ETH&tsyms=${CCSymbols}`;
|
||||
const CCRates = (symbols: string[]) => {
|
||||
const tsyms = rateSymbols.concat(symbols).join(',');
|
||||
return `${CCApi}/data/price?fsym=ETH&tsyms=${tsyms}`;
|
||||
};
|
||||
|
||||
export interface CCResponse {
|
||||
BTC: number;
|
||||
EUR: number;
|
||||
GBP: number;
|
||||
CHF: number;
|
||||
REP: number;
|
||||
[symbol: string]: {
|
||||
USD: number;
|
||||
EUR: number;
|
||||
GBP: number;
|
||||
BTC: number;
|
||||
CHF: number;
|
||||
REP: number;
|
||||
ETH: number;
|
||||
};
|
||||
}
|
||||
|
||||
export const fetchRates = (): Promise<CCResponse> =>
|
||||
fetch(CCRates(symbolsURL)).then(response =>
|
||||
handleJSONResponse(response, ERROR_MESSAGE)
|
||||
);
|
||||
export const fetchRates = (symbols: string[] = []): Promise<CCResponse> =>
|
||||
fetch(CCRates(symbols))
|
||||
.then(response => handleJSONResponse(response, ERROR_MESSAGE))
|
||||
.then(rates => {
|
||||
// All currencies are in ETH right now. We'll do token -> eth -> value to
|
||||
// do it all in one request
|
||||
// to their respective rates via ETH.
|
||||
return symbols.reduce(
|
||||
(eqRates, sym) => {
|
||||
eqRates[sym] = rateSymbols.reduce((symRates, rateSym) => {
|
||||
symRates[rateSym] = 1 / rates[sym] * rates[rateSym];
|
||||
return symRates;
|
||||
}, {});
|
||||
return eqRates;
|
||||
},
|
||||
{
|
||||
ETH: {
|
||||
USD: rates.USD,
|
||||
EUR: rates.EUR,
|
||||
GBP: rates.GBP,
|
||||
BTC: rates.BTC,
|
||||
CHF: rates.CHF,
|
||||
REP: rates.REP,
|
||||
ETH: 1
|
||||
}
|
||||
}
|
||||
);
|
||||
});
|
||||
|
@ -18,6 +18,6 @@ export interface FetchCCRatesFailed {
|
||||
|
||||
/*** Union Type ***/
|
||||
export type RatesAction =
|
||||
| FetchCCRatesSucceeded
|
||||
| FetchCCRates
|
||||
| FetchCCRatesSucceeded
|
||||
| FetchCCRatesFailed;
|
||||
|
@ -1,15 +1,13 @@
|
||||
import { BigNumber } from 'bignumber.js';
|
||||
import { Wei } from 'libs/units';
|
||||
import { Wei, TokenValue } from 'libs/units';
|
||||
import { IWallet } from 'libs/wallet/IWallet';
|
||||
import * as types from './actionTypes';
|
||||
import * as constants from './constants';
|
||||
|
||||
import { TypeKeys } from './constants';
|
||||
export type TUnlockPrivateKey = typeof unlockPrivateKey;
|
||||
export function unlockPrivateKey(
|
||||
value: types.PrivateKeyUnlockParams
|
||||
): types.UnlockPrivateKeyAction {
|
||||
return {
|
||||
type: constants.WALLET_UNLOCK_PRIVATE_KEY,
|
||||
type: TypeKeys.WALLET_UNLOCK_PRIVATE_KEY,
|
||||
payload: value
|
||||
};
|
||||
}
|
||||
@ -19,7 +17,7 @@ export function unlockKeystore(
|
||||
value: types.KeystoreUnlockParams
|
||||
): types.UnlockKeystoreAction {
|
||||
return {
|
||||
type: constants.WALLET_UNLOCK_KEYSTORE,
|
||||
type: TypeKeys.WALLET_UNLOCK_KEYSTORE,
|
||||
payload: value
|
||||
};
|
||||
}
|
||||
@ -29,33 +27,54 @@ export function unlockMnemonic(
|
||||
value: types.MnemonicUnlockParams
|
||||
): types.UnlockMnemonicAction {
|
||||
return {
|
||||
type: constants.WALLET_UNLOCK_MNEMONIC,
|
||||
type: TypeKeys.WALLET_UNLOCK_MNEMONIC,
|
||||
payload: value
|
||||
};
|
||||
}
|
||||
|
||||
export type TUnlockWeb3 = typeof unlockWeb3;
|
||||
export function unlockWeb3(): types.UnlockWeb3Action {
|
||||
return {
|
||||
type: TypeKeys.WALLET_UNLOCK_WEB3
|
||||
};
|
||||
}
|
||||
|
||||
export type TSetWallet = typeof setWallet;
|
||||
export function setWallet(value: IWallet): types.SetWalletAction {
|
||||
return {
|
||||
type: constants.WALLET_SET,
|
||||
type: TypeKeys.WALLET_SET,
|
||||
payload: value
|
||||
};
|
||||
}
|
||||
|
||||
export type TSetBalance = typeof setBalance;
|
||||
export function setBalance(value: Wei): types.SetBalanceAction {
|
||||
export function setBalancePending(): types.SetBalancePendingAction {
|
||||
return {
|
||||
type: constants.WALLET_SET_BALANCE,
|
||||
type: TypeKeys.WALLET_SET_BALANCE_PENDING
|
||||
};
|
||||
}
|
||||
|
||||
export type TSetBalance = typeof setBalanceFullfilled;
|
||||
export function setBalanceFullfilled(
|
||||
value: Wei
|
||||
): types.SetBalanceFullfilledAction {
|
||||
return {
|
||||
type: TypeKeys.WALLET_SET_BALANCE_FULFILLED,
|
||||
payload: value
|
||||
};
|
||||
}
|
||||
|
||||
export function setBalanceRejected(): types.SetBalanceRejectedAction {
|
||||
return {
|
||||
type: TypeKeys.WALLET_SET_BALANCE_REJECTED
|
||||
};
|
||||
}
|
||||
|
||||
export type TSetTokenBalances = typeof setTokenBalances;
|
||||
export function setTokenBalances(payload: {
|
||||
[key: string]: BigNumber;
|
||||
[key: string]: TokenValue;
|
||||
}): types.SetTokenBalancesAction {
|
||||
return {
|
||||
type: constants.WALLET_SET_TOKEN_BALANCES,
|
||||
type: TypeKeys.WALLET_SET_TOKEN_BALANCES,
|
||||
payload
|
||||
};
|
||||
}
|
||||
@ -65,7 +84,7 @@ export function broadcastTx(
|
||||
signedTx: string
|
||||
): types.BroadcastTxRequestedAction {
|
||||
return {
|
||||
type: constants.WALLET_BROADCAST_TX_REQUESTED,
|
||||
type: TypeKeys.WALLET_BROADCAST_TX_REQUESTED,
|
||||
payload: {
|
||||
signedTx
|
||||
}
|
||||
@ -78,7 +97,7 @@ export function broadcastTxSucceded(
|
||||
signedTx: string
|
||||
): types.BroadcastTxSuccededAction {
|
||||
return {
|
||||
type: constants.WALLET_BROADCAST_TX_SUCCEEDED,
|
||||
type: TypeKeys.WALLET_BROADCAST_TX_SUCCEEDED,
|
||||
payload: {
|
||||
txHash,
|
||||
signedTx
|
||||
@ -92,7 +111,7 @@ export function broadCastTxFailed(
|
||||
errorMsg: string
|
||||
): types.BroadcastTxFailedAction {
|
||||
return {
|
||||
type: constants.WALLET_BROADCAST_TX_FAILED,
|
||||
type: TypeKeys.WALLET_BROADCAST_TX_FAILED,
|
||||
payload: {
|
||||
signedTx,
|
||||
error: errorMsg
|
||||
@ -101,8 +120,8 @@ export function broadCastTxFailed(
|
||||
}
|
||||
|
||||
export type TResetWallet = typeof resetWallet;
|
||||
export function resetWallet() {
|
||||
export function resetWallet(): types.ResetWalletAction {
|
||||
return {
|
||||
type: constants.WALLET_RESET
|
||||
type: TypeKeys.WALLET_RESET
|
||||
};
|
||||
}
|
||||
|
@ -1,6 +1,6 @@
|
||||
import { BigNumber } from 'bignumber.js';
|
||||
import { Wei } from 'libs/units';
|
||||
import { Wei, TokenValue } from 'libs/units';
|
||||
import { IWallet } from 'libs/wallet/IWallet';
|
||||
import { TypeKeys } from './constants';
|
||||
|
||||
/*** Unlock Private Key ***/
|
||||
export interface PrivateKeyUnlockParams {
|
||||
@ -9,42 +9,52 @@ export interface PrivateKeyUnlockParams {
|
||||
}
|
||||
|
||||
export interface UnlockPrivateKeyAction {
|
||||
type: 'WALLET_UNLOCK_PRIVATE_KEY';
|
||||
type: TypeKeys.WALLET_UNLOCK_PRIVATE_KEY;
|
||||
payload: PrivateKeyUnlockParams;
|
||||
}
|
||||
export interface UnlockMnemonicAction {
|
||||
type: 'WALLET_UNLOCK_MNEMONIC';
|
||||
type: TypeKeys.WALLET_UNLOCK_MNEMONIC;
|
||||
payload: MnemonicUnlockParams;
|
||||
}
|
||||
|
||||
export interface UnlockWeb3Action {
|
||||
type: TypeKeys.WALLET_UNLOCK_WEB3;
|
||||
}
|
||||
|
||||
/*** Set Wallet ***/
|
||||
export interface SetWalletAction {
|
||||
type: 'WALLET_SET';
|
||||
type: TypeKeys.WALLET_SET;
|
||||
payload: IWallet;
|
||||
}
|
||||
|
||||
/*** Reset Wallet ***/
|
||||
export interface ResetWalletAction {
|
||||
type: 'WALLET_RESET';
|
||||
type: TypeKeys.WALLET_RESET;
|
||||
}
|
||||
|
||||
/*** Set Balance ***/
|
||||
export interface SetBalanceAction {
|
||||
type: 'WALLET_SET_BALANCE';
|
||||
export interface SetBalancePendingAction {
|
||||
type: TypeKeys.WALLET_SET_BALANCE_PENDING;
|
||||
}
|
||||
export interface SetBalanceFullfilledAction {
|
||||
type: TypeKeys.WALLET_SET_BALANCE_FULFILLED;
|
||||
payload: Wei;
|
||||
}
|
||||
export interface SetBalanceRejectedAction {
|
||||
type: TypeKeys.WALLET_SET_BALANCE_REJECTED;
|
||||
}
|
||||
|
||||
/*** Set Token Balance ***/
|
||||
export interface SetTokenBalancesAction {
|
||||
type: 'WALLET_SET_TOKEN_BALANCES';
|
||||
type: TypeKeys.WALLET_SET_TOKEN_BALANCES;
|
||||
payload: {
|
||||
[key: string]: BigNumber;
|
||||
[key: string]: TokenValue;
|
||||
};
|
||||
}
|
||||
|
||||
/*** Broadcast Tx ***/
|
||||
export interface BroadcastTxRequestedAction {
|
||||
type: 'WALLET_BROADCAST_TX_REQUESTED';
|
||||
type: TypeKeys.WALLET_BROADCAST_TX_REQUESTED;
|
||||
payload: {
|
||||
signedTx: string;
|
||||
};
|
||||
@ -65,12 +75,12 @@ export interface KeystoreUnlockParams {
|
||||
}
|
||||
|
||||
export interface UnlockKeystoreAction {
|
||||
type: 'WALLET_UNLOCK_KEYSTORE';
|
||||
type: TypeKeys.WALLET_UNLOCK_KEYSTORE;
|
||||
payload: KeystoreUnlockParams;
|
||||
}
|
||||
|
||||
export interface BroadcastTxSuccededAction {
|
||||
type: 'WALLET_BROADCAST_TX_SUCCEEDED';
|
||||
type: TypeKeys.WALLET_BROADCAST_TX_SUCCEEDED;
|
||||
payload: {
|
||||
txHash: string;
|
||||
signedTx: string;
|
||||
@ -78,7 +88,7 @@ export interface BroadcastTxSuccededAction {
|
||||
}
|
||||
|
||||
export interface BroadcastTxFailedAction {
|
||||
type: 'WALLET_BROADCAST_TX_FAILED';
|
||||
type: TypeKeys.WALLET_BROADCAST_TX_FAILED;
|
||||
payload: {
|
||||
signedTx: string;
|
||||
error: string;
|
||||
@ -90,7 +100,9 @@ export type WalletAction =
|
||||
| UnlockPrivateKeyAction
|
||||
| SetWalletAction
|
||||
| ResetWalletAction
|
||||
| SetBalanceAction
|
||||
| SetBalancePendingAction
|
||||
| SetBalanceFullfilledAction
|
||||
| SetBalanceRejectedAction
|
||||
| SetTokenBalancesAction
|
||||
| BroadcastTxRequestedAction
|
||||
| BroadcastTxFailedAction
|
||||
|
@ -1,10 +1,15 @@
|
||||
export const WALLET_UNLOCK_PRIVATE_KEY = 'WALLET_UNLOCK_PRIVATE_KEY';
|
||||
export const WALLET_UNLOCK_KEYSTORE = 'WALLET_UNLOCK_KEYSTORE';
|
||||
export const WALLET_UNLOCK_MNEMONIC = 'WALLET_UNLOCK_MNEMONIC';
|
||||
export const WALLET_SET = 'WALLET_SET';
|
||||
export const WALLET_SET_BALANCE = 'WALLET_SET_BALANCE';
|
||||
export const WALLET_SET_TOKEN_BALANCES = 'WALLET_SET_TOKEN_BALANCES';
|
||||
export const WALLET_BROADCAST_TX_REQUESTED = 'WALLET_BROADCAST_TX_REQUESTED';
|
||||
export const WALLET_BROADCAST_TX_FAILED = 'WALLET_BROADCAST_TX_FAILED';
|
||||
export const WALLET_BROADCAST_TX_SUCCEEDED = 'WALLET_BROADCAST_TX_SUCCEEDED';
|
||||
export const WALLET_RESET = 'WALLET_RESET';
|
||||
export enum TypeKeys {
|
||||
WALLET_UNLOCK_PRIVATE_KEY = 'WALLET_UNLOCK_PRIVATE_KEY',
|
||||
WALLET_UNLOCK_KEYSTORE = 'WALLET_UNLOCK_KEYSTORE',
|
||||
WALLET_UNLOCK_MNEMONIC = 'WALLET_UNLOCK_MNEMONIC',
|
||||
WALLET_UNLOCK_WEB3 = 'WALLET_UNLOCK_WEB3',
|
||||
WALLET_SET = 'WALLET_SET',
|
||||
WALLET_SET_BALANCE_PENDING = 'WALLET_SET_BALANCE_PENDING',
|
||||
WALLET_SET_BALANCE_FULFILLED = 'WALLET_SET_BALANCE_FULFILLED',
|
||||
WALLET_SET_BALANCE_REJECTED = 'WALLET_SET_BALANCE_REJECTED',
|
||||
WALLET_SET_TOKEN_BALANCES = 'WALLET_SET_TOKEN_BALANCES',
|
||||
WALLET_BROADCAST_TX_REQUESTED = 'WALLET_BROADCAST_TX_REQUESTED',
|
||||
WALLET_BROADCAST_TX_FAILED = 'WALLET_BROADCAST_TX_FAILED',
|
||||
WALLET_BROADCAST_TX_SUCCEEDED = 'WALLET_BROADCAST_TX_SUCCEEDED',
|
||||
WALLET_RESET = 'WALLET_RESET'
|
||||
}
|
||||
|
@ -1,18 +1,15 @@
|
||||
import { TFetchCCRates } from 'actions/rates';
|
||||
import { Identicon } from 'components/ui';
|
||||
import { Identicon, UnitDisplay } from 'components/ui';
|
||||
import { NetworkConfig } from 'config/data';
|
||||
import { Ether } from 'libs/units';
|
||||
import { IWallet } from 'libs/wallet';
|
||||
import { IWallet, Balance } from 'libs/wallet';
|
||||
import React from 'react';
|
||||
import translate from 'translations';
|
||||
import { formatNumber } from 'utils/formatters';
|
||||
import './AccountInfo.scss';
|
||||
import Spinner from 'components/ui/Spinner';
|
||||
|
||||
interface Props {
|
||||
balance: Ether;
|
||||
balance: Balance;
|
||||
wallet: IWallet;
|
||||
network: NetworkConfig;
|
||||
fetchCCRates: TFetchCCRates;
|
||||
}
|
||||
|
||||
interface State {
|
||||
@ -26,14 +23,13 @@ export default class AccountInfo extends React.Component<Props, State> {
|
||||
};
|
||||
|
||||
public async setAddressFromWallet() {
|
||||
const address = await this.props.wallet.getAddress();
|
||||
const address = await this.props.wallet.getAddressString();
|
||||
if (address !== this.state.address) {
|
||||
this.setState({ address });
|
||||
}
|
||||
}
|
||||
|
||||
public componentDidMount() {
|
||||
this.props.fetchCCRates();
|
||||
this.setAddressFromWallet();
|
||||
}
|
||||
|
||||
@ -54,7 +50,7 @@ export default class AccountInfo extends React.Component<Props, State> {
|
||||
public render() {
|
||||
const { network, balance } = this.props;
|
||||
const { blockExplorer, tokenExplorer } = network;
|
||||
const { address } = this.state;
|
||||
const { address, showLongBalance } = this.state;
|
||||
|
||||
return (
|
||||
<div className="AccountInfo">
|
||||
@ -80,38 +76,48 @@ export default class AccountInfo extends React.Component<Props, State> {
|
||||
className="AccountInfo-list-item-clickable mono wrap"
|
||||
onClick={this.toggleShowLongBalance}
|
||||
>
|
||||
{this.state.showLongBalance
|
||||
? balance ? balance.toString() : '???'
|
||||
: balance ? formatNumber(balance.amount) : '???'}
|
||||
{balance.isPending ? (
|
||||
<Spinner />
|
||||
) : (
|
||||
<UnitDisplay
|
||||
value={balance.wei}
|
||||
unit={'ether'}
|
||||
displayShortBalance={!showLongBalance}
|
||||
/>
|
||||
)}
|
||||
</span>
|
||||
{` ${network.name}`}
|
||||
{!balance.isPending ? (
|
||||
balance.wei ? (
|
||||
<span> {network.name}</span>
|
||||
) : null
|
||||
) : null}
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
{(!!blockExplorer || !!tokenExplorer) && (
|
||||
<div className="AccountInfo-section">
|
||||
<h5 className="AccountInfo-section-header">
|
||||
{translate('sidebar_TransHistory')}
|
||||
</h5>
|
||||
<ul className="AccountInfo-list">
|
||||
{!!blockExplorer && (
|
||||
<li className="AccountInfo-list-item">
|
||||
<a href={blockExplorer.address(address)} target="_blank">
|
||||
{`${network.name} (${blockExplorer.name})`}
|
||||
</a>
|
||||
</li>
|
||||
)}
|
||||
{!!tokenExplorer && (
|
||||
<li className="AccountInfo-list-item">
|
||||
<a href={tokenExplorer.address(address)} target="_blank">
|
||||
{`Tokens (${tokenExplorer.name})`}
|
||||
</a>
|
||||
</li>
|
||||
)}
|
||||
</ul>
|
||||
</div>
|
||||
)}
|
||||
<div className="AccountInfo-section">
|
||||
<h5 className="AccountInfo-section-header">
|
||||
{translate('sidebar_TransHistory')}
|
||||
</h5>
|
||||
<ul className="AccountInfo-list">
|
||||
{!!blockExplorer && (
|
||||
<li className="AccountInfo-list-item">
|
||||
<a href={blockExplorer.address(address)} target="_blank">
|
||||
{`${network.name} (${blockExplorer.name})`}
|
||||
</a>
|
||||
</li>
|
||||
)}
|
||||
{!!tokenExplorer && (
|
||||
<li className="AccountInfo-list-item">
|
||||
<a href={tokenExplorer.address(address)} target="_blank">
|
||||
{`Tokens (${tokenExplorer.name})`}
|
||||
</a>
|
||||
</li>
|
||||
)}
|
||||
</ul>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
@ -1,5 +1,5 @@
|
||||
@import "common/sass/variables";
|
||||
@import "common/sass/mixins";
|
||||
@import 'common/sass/variables';
|
||||
@import 'common/sass/mixins';
|
||||
|
||||
.EquivalentValues {
|
||||
&-title {
|
||||
@ -25,6 +25,7 @@
|
||||
}
|
||||
|
||||
&-label {
|
||||
white-space: pre-wrap;
|
||||
display: inline-block;
|
||||
min-width: 36px;
|
||||
}
|
||||
@ -33,5 +34,10 @@
|
||||
@include mono;
|
||||
}
|
||||
}
|
||||
|
||||
&-loader {
|
||||
padding: 25px 0;
|
||||
text-align: center;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,48 +1,203 @@
|
||||
import { Ether } from 'libs/units';
|
||||
import React from 'react';
|
||||
import * as React from 'react';
|
||||
import BN from 'bn.js';
|
||||
import translate from 'translations';
|
||||
import { formatNumber } from 'utils/formatters';
|
||||
import './EquivalentValues.scss';
|
||||
import { State } from 'reducers/rates';
|
||||
import { symbols } from 'actions/rates';
|
||||
import { rateSymbols, TFetchCCRates } from 'actions/rates';
|
||||
import { TokenBalance } from 'selectors/wallet';
|
||||
import { Balance } from 'libs/wallet';
|
||||
import Spinner from 'components/ui/Spinner';
|
||||
import UnitDisplay from 'components/ui/UnitDisplay';
|
||||
import './EquivalentValues.scss';
|
||||
|
||||
const ALL_OPTION = 'All';
|
||||
|
||||
interface Props {
|
||||
balance?: Ether;
|
||||
rates?: State['rates'];
|
||||
balance?: Balance;
|
||||
tokenBalances?: TokenBalance[];
|
||||
rates: State['rates'];
|
||||
ratesError?: State['ratesError'];
|
||||
fetchCCRates: TFetchCCRates;
|
||||
}
|
||||
|
||||
export default class EquivalentValues extends React.Component<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() {
|
||||
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 (
|
||||
<div className="EquivalentValues">
|
||||
<h5 className="EquivalentValues-title">{translate('sidebar_Equiv')}</h5>
|
||||
|
||||
<ul className="EquivalentValues-values">
|
||||
{rates
|
||||
? symbols.map(key => {
|
||||
if (!rates[key]) {
|
||||
return null;
|
||||
<h5 className="EquivalentValues-title">
|
||||
{translate('sidebar_Equiv')} for{' '}
|
||||
<select
|
||||
className="EquivalentValues-title-symbol"
|
||||
onChange={this.changeCurrency}
|
||||
value={currency}
|
||||
>
|
||||
<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 (
|
||||
<li className="EquivalentValues-values-currency" key={key}>
|
||||
<span className="EquivalentValues-values-currency-label">
|
||||
{key}:
|
||||
</span>
|
||||
<span className="EquivalentValues-values-currency-value">
|
||||
{' '}
|
||||
{balance
|
||||
? formatNumber(balance.amount.times(rates[key]))
|
||||
: '???'}
|
||||
</span>
|
||||
</li>
|
||||
<option key={sym} value={sym}>
|
||||
{sym}
|
||||
</option>
|
||||
);
|
||||
})
|
||||
: ratesError && <h5>{ratesError}</h5>}
|
||||
</ul>
|
||||
})}
|
||||
</select>
|
||||
</h5>
|
||||
|
||||
<ul className="EquivalentValues-values">{valuesEl}</ul>
|
||||
</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 { BigNumber } from 'bignumber.js';
|
||||
import React from 'react';
|
||||
import { formatNumber } from 'utils/formatters';
|
||||
import { TokenValue } from 'libs/units';
|
||||
import { UnitDisplay } from 'components/ui';
|
||||
import './TokenRow.scss';
|
||||
|
||||
interface Props {
|
||||
balance: BigNumber;
|
||||
balance: TokenValue;
|
||||
symbol: string;
|
||||
custom?: boolean;
|
||||
decimal: number;
|
||||
onRemove(symbol: string): void;
|
||||
}
|
||||
interface State {
|
||||
@ -18,9 +19,11 @@ export default class TokenRow extends React.Component<Props, State> {
|
||||
public state = {
|
||||
showLongBalance: false
|
||||
};
|
||||
|
||||
public render() {
|
||||
const { balance, symbol, custom } = this.props;
|
||||
const { balance, symbol, custom, decimal } = this.props;
|
||||
const { showLongBalance } = this.state;
|
||||
|
||||
return (
|
||||
<tr className="TokenRow">
|
||||
<td
|
||||
@ -28,21 +31,24 @@ export default class TokenRow extends React.Component<Props, State> {
|
||||
title={`${balance.toString()} (Double-Click)`}
|
||||
onDoubleClick={this.toggleShowLongBalance}
|
||||
>
|
||||
{!!custom &&
|
||||
{!!custom && (
|
||||
<img
|
||||
src={removeIcon}
|
||||
className="TokenRow-balance-remove"
|
||||
title="Remove Token"
|
||||
onClick={this.onRemove}
|
||||
tabIndex={0}
|
||||
/>}
|
||||
/>
|
||||
)}
|
||||
<span>
|
||||
{showLongBalance ? balance.toString() : formatNumber(balance)}
|
||||
<UnitDisplay
|
||||
value={balance}
|
||||
decimal={decimal}
|
||||
displayShortBalance={!showLongBalance}
|
||||
/>
|
||||
</span>
|
||||
</td>
|
||||
<td className="TokenRow-symbol">
|
||||
{symbol}
|
||||
</td>
|
||||
<td className="TokenRow-symbol">{symbol}</td>
|
||||
</tr>
|
||||
);
|
||||
}
|
||||
|
@ -25,25 +25,24 @@ export default class TokenBalances extends React.Component<Props, State> {
|
||||
public render() {
|
||||
const { tokens } = this.props;
|
||||
const shownTokens = tokens.filter(
|
||||
token => !token.balance.eq(0) || token.custom || this.state.showAllTokens
|
||||
token => !token.balance.eqn(0) || token.custom || this.state.showAllTokens
|
||||
);
|
||||
|
||||
return (
|
||||
<section className="TokenBalances">
|
||||
<h5 className="TokenBalances-title">
|
||||
{translate('sidebar_TokenBal')}
|
||||
</h5>
|
||||
<h5 className="TokenBalances-title">{translate('sidebar_TokenBal')}</h5>
|
||||
<table className="TokenBalances-rows">
|
||||
<tbody>
|
||||
{shownTokens.map(token =>
|
||||
{shownTokens.map(token => (
|
||||
<TokenRow
|
||||
key={token.symbol}
|
||||
balance={token.balance}
|
||||
symbol={token.symbol}
|
||||
custom={token.custom}
|
||||
decimal={token.decimal}
|
||||
onRemove={this.props.onRemoveCustomToken}
|
||||
/>
|
||||
)}
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
@ -58,16 +57,15 @@ export default class TokenBalances extends React.Component<Props, State> {
|
||||
className="btn btn-default btn-xs"
|
||||
onClick={this.toggleShowCustomTokenForm}
|
||||
>
|
||||
<span>
|
||||
{translate('SEND_custom')}
|
||||
</span>
|
||||
<span>{translate('SEND_custom')}</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{this.state.showCustomTokenForm &&
|
||||
{this.state.showCustomTokenForm && (
|
||||
<div className="TokenBalances-form">
|
||||
<AddCustomTokenForm onSave={this.addCustomToken} />
|
||||
</div>}
|
||||
</div>
|
||||
)}
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
@ -7,8 +7,7 @@ import {
|
||||
import { showNotification, TShowNotification } from 'actions/notifications';
|
||||
import { fetchCCRates as dFetchCCRates, TFetchCCRates } from 'actions/rates';
|
||||
import { NetworkConfig } from 'config/data';
|
||||
import { Ether } from 'libs/units';
|
||||
import { IWallet } from 'libs/wallet/IWallet';
|
||||
import { IWallet, Balance } from 'libs/wallet';
|
||||
import React from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import { AppState } from 'reducers';
|
||||
@ -22,16 +21,15 @@ import AccountInfo from './AccountInfo';
|
||||
import EquivalentValues from './EquivalentValues';
|
||||
import Promos from './Promos';
|
||||
import TokenBalances from './TokenBalances';
|
||||
import { State } from 'reducers/rates';
|
||||
import OfflineToggle from './OfflineToggle';
|
||||
|
||||
interface Props {
|
||||
wallet: IWallet;
|
||||
balance: Ether;
|
||||
balance: Balance;
|
||||
network: NetworkConfig;
|
||||
tokenBalances: TokenBalance[];
|
||||
rates: State['rates'];
|
||||
ratesError: State['ratesError'];
|
||||
rates: AppState['rates']['rates'];
|
||||
ratesError: AppState['rates']['ratesError'];
|
||||
showNotification: TShowNotification;
|
||||
addCustomToken: TAddCustomToken;
|
||||
removeCustomToken: TRemoveCustomToken;
|
||||
@ -67,12 +65,7 @@ export class BalanceSidebar extends React.Component<Props, {}> {
|
||||
{
|
||||
name: 'Account Info',
|
||||
content: (
|
||||
<AccountInfo
|
||||
wallet={wallet}
|
||||
balance={balance}
|
||||
network={network}
|
||||
fetchCCRates={fetchCCRates}
|
||||
/>
|
||||
<AccountInfo wallet={wallet} balance={balance} network={network} />
|
||||
)
|
||||
},
|
||||
{
|
||||
@ -95,8 +88,10 @@ export class BalanceSidebar extends React.Component<Props, {}> {
|
||||
content: (
|
||||
<EquivalentValues
|
||||
balance={balance}
|
||||
tokenBalances={tokenBalances}
|
||||
rates={rates}
|
||||
ratesError={ratesError}
|
||||
fetchCCRates={fetchCCRates}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
@ -1,6 +1,6 @@
|
||||
import logo from 'assets/images/logo-myetherwallet.svg';
|
||||
import { bityReferralURL, donationAddressMap } from 'config/data';
|
||||
import React, { Component } from 'react';
|
||||
import React from 'react';
|
||||
import translate from 'translations';
|
||||
import './index.scss';
|
||||
import PreFooter from './PreFooter';
|
||||
@ -92,11 +92,15 @@ const LINKS_SOCIAL = [
|
||||
}
|
||||
];
|
||||
|
||||
interface ComponentState {
|
||||
interface Props {
|
||||
latestBlock: string;
|
||||
};
|
||||
|
||||
interface State {
|
||||
isOpen: boolean;
|
||||
}
|
||||
|
||||
export default class Footer extends React.Component<{}, ComponentState> {
|
||||
export default class Footer extends React.Component<Props, State> {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = { isOpen: false };
|
||||
@ -276,9 +280,7 @@ export default class Footer extends React.Component<{}, ComponentState> {
|
||||
);
|
||||
})}
|
||||
</p>
|
||||
|
||||
{/* TODO: Fix me */}
|
||||
<p>Latest Block#: ?????</p>
|
||||
<p>Latest Block#: {this.props.latestBlock}</p>
|
||||
</div>
|
||||
</footer>
|
||||
</div>
|
||||
|
230
common/components/Header/components/CustomNodeModal.tsx
Normal file
230
common/components/Header/components/CustomNodeModal.tsx
Normal file
@ -0,0 +1,230 @@
|
||||
import React from 'react';
|
||||
import classnames from 'classnames';
|
||||
import Modal, { IButton } from 'components/ui/Modal';
|
||||
import translate from 'translations';
|
||||
import { NETWORKS, CustomNodeConfig } from 'config/data';
|
||||
|
||||
const NETWORK_KEYS = Object.keys(NETWORKS);
|
||||
|
||||
interface Input {
|
||||
name: string;
|
||||
placeholder?: string;
|
||||
type?: string;
|
||||
}
|
||||
|
||||
interface Props {
|
||||
handleAddCustomNode(node: CustomNodeConfig): void;
|
||||
handleClose(): void;
|
||||
}
|
||||
|
||||
interface State {
|
||||
name: string;
|
||||
url: string;
|
||||
port: string;
|
||||
network: string;
|
||||
hasAuth: boolean;
|
||||
username: string;
|
||||
password: string;
|
||||
}
|
||||
|
||||
export default class CustomNodeModal extends React.Component<Props, State> {
|
||||
public state: State = {
|
||||
name: '',
|
||||
url: '',
|
||||
port: '',
|
||||
network: NETWORK_KEYS[0],
|
||||
hasAuth: false,
|
||||
username: '',
|
||||
password: '',
|
||||
};
|
||||
|
||||
public render() {
|
||||
const { handleClose } = this.props;
|
||||
const isHttps = window.location.protocol.includes('https');
|
||||
const invalids = this.getInvalids();
|
||||
|
||||
const buttons: IButton[] = [{
|
||||
type: 'primary',
|
||||
text: translate('NODE_CTA'),
|
||||
onClick: this.saveAndAdd,
|
||||
disabled: !!Object.keys(invalids).length,
|
||||
}, {
|
||||
text: translate('x_Cancel'),
|
||||
onClick: handleClose
|
||||
}];
|
||||
|
||||
return (
|
||||
<Modal
|
||||
title={translate('NODE_Title')}
|
||||
isOpen={true}
|
||||
buttons={buttons}
|
||||
handleClose={handleClose}
|
||||
>
|
||||
<div>
|
||||
{isHttps &&
|
||||
<div className="alert alert-danger small">
|
||||
{translate('NODE_Warning')}
|
||||
</div>
|
||||
}
|
||||
|
||||
<form>
|
||||
<div className="row">
|
||||
<div className="col-sm-7">
|
||||
<label>{translate('NODE_Name')}</label>
|
||||
{this.renderInput({
|
||||
name: 'name',
|
||||
placeholder: 'My Node',
|
||||
}, invalids)}
|
||||
</div>
|
||||
<div className="col-sm-5">
|
||||
<label>Network</label>
|
||||
<select
|
||||
className="form-control"
|
||||
name="network"
|
||||
value={this.state.network}
|
||||
onChange={this.handleChange}
|
||||
>
|
||||
{NETWORK_KEYS.map((net) =>
|
||||
<option key={net} value={net}>{net}</option>
|
||||
)}
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="row">
|
||||
<div className="col-sm-9">
|
||||
<label>URL</label>
|
||||
{this.renderInput({
|
||||
name: 'url',
|
||||
placeholder: 'http://127.0.0.1/',
|
||||
}, invalids)}
|
||||
</div>
|
||||
|
||||
<div className="col-sm-3">
|
||||
<label>{translate('NODE_Port')}</label>
|
||||
{this.renderInput({
|
||||
name: 'port',
|
||||
placeholder: '8545',
|
||||
type: 'number',
|
||||
}, invalids)}
|
||||
</div>
|
||||
</div>
|
||||
<div className="row">
|
||||
<div className="col-sm-12">
|
||||
<label>
|
||||
<input
|
||||
type="checkbox"
|
||||
name="hasAuth"
|
||||
checked={this.state.hasAuth}
|
||||
onChange={this.handleCheckbox}
|
||||
/>
|
||||
{' '}
|
||||
<span>HTTP Basic Authentication</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
{this.state.hasAuth &&
|
||||
<div className="row">
|
||||
<div className="col-sm-6">
|
||||
<label>Username</label>
|
||||
{this.renderInput({ name: 'username' }, invalids)}
|
||||
</div>
|
||||
<div className="col-sm-6">
|
||||
<label>Password</label>
|
||||
{this.renderInput({
|
||||
name: 'password',
|
||||
type: 'password',
|
||||
}, invalids)}
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
</form>
|
||||
</div>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
|
||||
private renderInput(input: Input, invalids: { [key: string]: boolean }) {
|
||||
return <input
|
||||
className={classnames({
|
||||
'form-control': true,
|
||||
'is-invalid': this.state[input.name] && invalids[input.name],
|
||||
})}
|
||||
value={this.state[name]}
|
||||
onChange={this.handleChange}
|
||||
{...input}
|
||||
/>;
|
||||
}
|
||||
|
||||
private getInvalids(): { [key: string]: boolean } {
|
||||
const {
|
||||
url,
|
||||
port,
|
||||
hasAuth,
|
||||
username,
|
||||
password,
|
||||
} = this.state;
|
||||
const required = ["name", "url", "port", "network"];
|
||||
const invalids: { [key: string]: boolean } = {};
|
||||
|
||||
// Required fields
|
||||
required.forEach((field) => {
|
||||
if (!this.state[field]) {
|
||||
invalids[field] = true;
|
||||
}
|
||||
});
|
||||
|
||||
// Somewhat valid URL, not 100% fool-proof
|
||||
if (!/https?\:\/\/\w+/i.test(url)) {
|
||||
invalids.url = true;
|
||||
}
|
||||
|
||||
// Numeric port within range
|
||||
const iport = parseInt(port, 10);
|
||||
if (!iport || iport < 1 || iport > 65535) {
|
||||
invalids.port = true;
|
||||
}
|
||||
|
||||
// If they have auth, make sure it's provided
|
||||
if (hasAuth) {
|
||||
if (!username) {
|
||||
invalids.username = true;
|
||||
}
|
||||
if (!password) {
|
||||
invalids.password = true;
|
||||
}
|
||||
}
|
||||
|
||||
return invalids;
|
||||
}
|
||||
|
||||
private handleChange = (ev: React.FormEvent<
|
||||
HTMLInputElement | HTMLSelectElement
|
||||
>) => {
|
||||
const { name, value } = ev.currentTarget;
|
||||
this.setState({ [name as any]: value });
|
||||
};
|
||||
|
||||
private handleCheckbox = (ev: React.FormEvent<HTMLInputElement>) => {
|
||||
const { name } = ev.currentTarget;
|
||||
this.setState({ [name as any]: !this.state[name] });
|
||||
};
|
||||
|
||||
private saveAndAdd = () => {
|
||||
const node: CustomNodeConfig = {
|
||||
name: this.state.name.trim(),
|
||||
url: this.state.url.trim(),
|
||||
port: parseInt(this.state.port, 10),
|
||||
network: this.state.network,
|
||||
};
|
||||
|
||||
if (this.state.hasAuth) {
|
||||
node.auth = {
|
||||
username: this.state.username,
|
||||
password: this.state.password,
|
||||
};
|
||||
}
|
||||
|
||||
this.props.handleAddCustomNode(node);
|
||||
};
|
||||
}
|
@ -1,4 +1,3 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import NavigationLink from './NavigationLink';
|
||||
|
||||
@ -21,10 +20,22 @@ const tabs = [
|
||||
name: 'NAV_ViewWallet'
|
||||
// to: 'view-wallet'
|
||||
},
|
||||
{
|
||||
name: 'NAV_Contracts',
|
||||
to: 'contracts'
|
||||
},
|
||||
{
|
||||
name: 'NAV_ENS',
|
||||
to: 'ens'
|
||||
},
|
||||
{
|
||||
name: 'Sign & Verify Message',
|
||||
to: 'sign-and-verify-message'
|
||||
},
|
||||
{
|
||||
name: 'Broadcast Transaction',
|
||||
to: 'pushTx'
|
||||
},
|
||||
{
|
||||
name: 'NAV_Help',
|
||||
to: 'https://myetherwallet.groovehq.com/help_center',
|
||||
@ -54,7 +65,7 @@ export default class Navigation extends Component<Props, State> {
|
||||
/*
|
||||
* public scrollLeft() {}
|
||||
public scrollRight() {}
|
||||
*
|
||||
*
|
||||
*/
|
||||
|
||||
public render() {
|
||||
|
@ -15,6 +15,15 @@ $small-size: 900px;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes dropdown-is-flashing {
|
||||
0%, 100% {
|
||||
opacity: 0.8;
|
||||
}
|
||||
50% {
|
||||
opacity: 0.7;
|
||||
}
|
||||
}
|
||||
|
||||
// Header
|
||||
.Header {
|
||||
margin-bottom: 2rem;
|
||||
@ -124,6 +133,11 @@ $small-size: 900px;
|
||||
padding-top: $space-sm !important;
|
||||
padding-bottom: $space-sm !important;
|
||||
}
|
||||
|
||||
&.is-flashing {
|
||||
pointer-events: none;
|
||||
animation: dropdown-is-flashing 800ms ease infinite;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,11 +1,14 @@
|
||||
import {
|
||||
TChangeGasPrice,
|
||||
TChangeLanguage,
|
||||
TChangeNodeIntent
|
||||
TChangeNodeIntent,
|
||||
TAddCustomNode,
|
||||
TRemoveCustomNode
|
||||
} from 'actions/config';
|
||||
import logo from 'assets/images/logo-myetherwallet.svg';
|
||||
import { Dropdown, ColorDropdown } from 'components/ui';
|
||||
import React, { Component } from 'react';
|
||||
import classnames from 'classnames';
|
||||
import { Link } from 'react-router-dom';
|
||||
import {
|
||||
ANNOUNCEMENT_MESSAGE,
|
||||
@ -13,43 +16,85 @@ import {
|
||||
languages,
|
||||
NETWORKS,
|
||||
NODES,
|
||||
VERSION
|
||||
VERSION,
|
||||
NodeConfig,
|
||||
CustomNodeConfig
|
||||
} from '../../config/data';
|
||||
import GasPriceDropdown from './components/GasPriceDropdown';
|
||||
import Navigation from './components/Navigation';
|
||||
import CustomNodeModal from './components/CustomNodeModal';
|
||||
import { getKeyByValue } from 'utils/helpers';
|
||||
import { makeCustomNodeId } from 'utils/node';
|
||||
import './index.scss';
|
||||
|
||||
interface Props {
|
||||
languageSelection: string;
|
||||
node: NodeConfig;
|
||||
nodeSelection: string;
|
||||
isChangingNode: boolean;
|
||||
gasPriceGwei: number;
|
||||
customNodes: CustomNodeConfig[];
|
||||
|
||||
changeLanguage: TChangeLanguage;
|
||||
changeNodeIntent: TChangeNodeIntent;
|
||||
changeGasPrice: TChangeGasPrice;
|
||||
addCustomNode: TAddCustomNode;
|
||||
removeCustomNode: TRemoveCustomNode;
|
||||
}
|
||||
|
||||
export default class Header extends Component<Props, {}> {
|
||||
interface State {
|
||||
isAddingCustomNode: boolean;
|
||||
}
|
||||
|
||||
export default class Header extends Component<Props, State> {
|
||||
public state = {
|
||||
isAddingCustomNode: false
|
||||
};
|
||||
|
||||
public render() {
|
||||
const { languageSelection, changeNodeIntent, nodeSelection } = this.props;
|
||||
const {
|
||||
languageSelection,
|
||||
changeNodeIntent,
|
||||
node,
|
||||
nodeSelection,
|
||||
isChangingNode,
|
||||
customNodes
|
||||
} = this.props;
|
||||
const { isAddingCustomNode } = this.state;
|
||||
const selectedLanguage = languageSelection;
|
||||
const selectedNode = NODES[nodeSelection];
|
||||
const selectedNetwork = NETWORKS[selectedNode.network];
|
||||
const selectedNetwork = NETWORKS[node.network];
|
||||
const LanguageDropDown = Dropdown as new () => Dropdown<
|
||||
typeof selectedLanguage
|
||||
>;
|
||||
const nodeOptions = Object.keys(NODES).map(key => {
|
||||
return {
|
||||
value: key,
|
||||
name: (
|
||||
<span>
|
||||
{NODES[key].network} <small>({NODES[key].service})</small>
|
||||
</span>
|
||||
),
|
||||
color: NETWORKS[NODES[key].network].color
|
||||
};
|
||||
});
|
||||
|
||||
const nodeOptions = Object.keys(NODES)
|
||||
.map(key => {
|
||||
return {
|
||||
value: key,
|
||||
name: (
|
||||
<span>
|
||||
{NODES[key].network} <small>({NODES[key].service})</small>
|
||||
</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 (
|
||||
<div className="Header">
|
||||
@ -65,7 +110,7 @@ export default class Header extends Component<Props, {}> {
|
||||
<section className="Header-branding">
|
||||
<section className="Header-branding-inner container">
|
||||
<Link
|
||||
to={'/'}
|
||||
to="/"
|
||||
className="Header-branding-title"
|
||||
aria-label="Go to homepage"
|
||||
>
|
||||
@ -90,9 +135,9 @@ export default class Header extends Component<Props, {}> {
|
||||
|
||||
<div className="Header-branding-right-dropdown">
|
||||
<LanguageDropDown
|
||||
ariaLabel={`change language. current language ${languages[
|
||||
selectedLanguage
|
||||
]}`}
|
||||
ariaLabel={`change language. current language ${
|
||||
languages[selectedLanguage]
|
||||
}`}
|
||||
options={Object.values(languages)}
|
||||
value={languages[selectedLanguage]}
|
||||
extra={
|
||||
@ -108,19 +153,29 @@ export default class Header extends Component<Props, {}> {
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="Header-branding-right-dropdown">
|
||||
<div
|
||||
className={classnames({
|
||||
'Header-branding-right-dropdown': true,
|
||||
'is-flashing': isChangingNode
|
||||
})}
|
||||
>
|
||||
<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}
|
||||
value={nodeSelection}
|
||||
extra={
|
||||
<li>
|
||||
<a>Add Custom Node</a>
|
||||
<a onClick={this.openCustomNodeModal}>Add Custom Node</a>
|
||||
</li>
|
||||
}
|
||||
disabled={nodeSelection === 'web3'}
|
||||
onChange={changeNodeIntent}
|
||||
size="smr"
|
||||
color="white"
|
||||
menuAlign="right"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
@ -128,6 +183,13 @@ export default class Header extends Component<Props, {}> {
|
||||
</section>
|
||||
|
||||
<Navigation color={selectedNetwork.color} />
|
||||
|
||||
{isAddingCustomNode && (
|
||||
<CustomNodeModal
|
||||
handleAddCustomNode={this.addCustomNode}
|
||||
handleClose={this.closeCustomNodeModal}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@ -138,4 +200,17 @@ export default class Header extends Component<Props, {}> {
|
||||
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 PrivKeyWallet from 'libs/wallet/privkey';
|
||||
import React from 'react';
|
||||
|
||||
import ethLogo from 'assets/images/logo-ethereum-1.png';
|
||||
@ -91,26 +90,13 @@ const styles: any = {
|
||||
};
|
||||
|
||||
interface Props {
|
||||
wallet: PrivKeyWallet;
|
||||
}
|
||||
|
||||
interface State {
|
||||
address: string;
|
||||
privateKey: string;
|
||||
}
|
||||
export default class PaperWallet extends React.Component<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() {
|
||||
const privateKey = this.props.wallet.getPrivateKey();
|
||||
const { privateKey, address } = this.props;
|
||||
|
||||
return (
|
||||
<div style={styles.container}>
|
||||
@ -119,7 +105,7 @@ export default class PaperWallet extends React.Component<Props, State> {
|
||||
|
||||
<div style={styles.block}>
|
||||
<div style={styles.box}>
|
||||
<QRCode data={this.state.address} />
|
||||
<QRCode data={address} />
|
||||
</div>
|
||||
<p style={styles.blockText}>YOUR ADDRESS</p>
|
||||
</div>
|
||||
@ -140,7 +126,7 @@ export default class PaperWallet extends React.Component<Props, State> {
|
||||
<p style={styles.infoText}>
|
||||
<strong style={styles.infoLabel}>Your Address:</strong>
|
||||
<br />
|
||||
{this.state.address}
|
||||
{address}
|
||||
</p>
|
||||
<p style={styles.infoText}>
|
||||
<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={{ float: 'left' }}>
|
||||
<Identicon address={this.state.address} size={'42px'} />
|
||||
<Identicon address={address} size={'42px'} />
|
||||
</div>
|
||||
<p style={styles.identiconText}>
|
||||
Always look for this icon when sending to this wallet
|
||||
|
@ -1,49 +1,53 @@
|
||||
import { PaperWallet } from 'components';
|
||||
import PrivKeyWallet from 'libs/wallet/privkey';
|
||||
import React, { Component } from 'react';
|
||||
import { IFullWallet } from 'ethereumjs-wallet';
|
||||
import React from 'react';
|
||||
import translate from 'translations';
|
||||
import printElement from 'utils/printElement';
|
||||
|
||||
interface Props {
|
||||
wallet: PrivKeyWallet;
|
||||
}
|
||||
const print = (address: string, privateKey: string) => () =>
|
||||
address &&
|
||||
privateKey &&
|
||||
printElement(<PaperWallet address={address} privateKey={privateKey} />, {
|
||||
popupFeatures: {
|
||||
scrollbars: 'no'
|
||||
},
|
||||
styles: `
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
export default class PrintableWallet extends Component<Props, {}> {
|
||||
public print = () => {
|
||||
printElement(<PaperWallet wallet={this.props.wallet} />, {
|
||||
popupFeatures: {
|
||||
scrollbars: 'no'
|
||||
},
|
||||
styles: `
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
body {
|
||||
font-family: Lato, sans-serif;
|
||||
font-size: 1rem;
|
||||
line-height: 1.4;
|
||||
margin: 0;
|
||||
}
|
||||
`
|
||||
});
|
||||
|
||||
body {
|
||||
font-family: Lato, sans-serif;
|
||||
font-size: 1rem;
|
||||
line-height: 1.4;
|
||||
margin: 0;
|
||||
}
|
||||
`
|
||||
});
|
||||
};
|
||||
const PrintableWallet: React.SFC<{ wallet: IFullWallet }> = ({ wallet }) => {
|
||||
const address = wallet.getAddressString();
|
||||
const privateKey = wallet.getPrivateKeyString();
|
||||
|
||||
public render() {
|
||||
return (
|
||||
<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>
|
||||
);
|
||||
if (!address || !privateKey) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
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 {
|
||||
width: 100%;
|
||||
text-align: center;
|
||||
margin-bottom: 10px;
|
||||
|
||||
&-token {
|
||||
width: 82px;
|
||||
@ -32,6 +33,10 @@
|
||||
font-size: 13px;
|
||||
text-align: left;
|
||||
font-family: $font-family-monospace;
|
||||
|
||||
input {
|
||||
margin-right: 6px;
|
||||
}
|
||||
}
|
||||
|
||||
&-more {
|
||||
|
@ -7,12 +7,14 @@ import {
|
||||
SetDesiredTokenAction
|
||||
} from 'actions/deterministicWallets';
|
||||
import Modal, { IButton } from 'components/ui/Modal';
|
||||
import { NetworkConfig, Token } from 'config/data';
|
||||
import { AppState } from 'reducers';
|
||||
import { NetworkConfig } from 'config/data';
|
||||
import { isValidPath } from 'libs/validators';
|
||||
import React from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import { getNetworkConfig } from 'selectors/config';
|
||||
import { getTokens, MergedToken } from 'selectors/wallet';
|
||||
import { UnitDisplay } from 'components/ui';
|
||||
import './DeterministicWalletsModal.scss';
|
||||
|
||||
const WALLETS_PER_PAGE = 5;
|
||||
@ -123,20 +125,21 @@ class DeterministicWalletsModal extends React.Component<Props, State> {
|
||||
onChange={this.handleChangePath}
|
||||
value={isCustomPath ? 'custom' : dPath}
|
||||
>
|
||||
{dPaths.map(dp =>
|
||||
{dPaths.map(dp => (
|
||||
<option key={dp.value} value={dp.value}>
|
||||
{dp.label}
|
||||
</option>
|
||||
)}
|
||||
))}
|
||||
<option value="custom">Custom path...</option>
|
||||
</select>
|
||||
{isCustomPath &&
|
||||
{isCustomPath && (
|
||||
<input
|
||||
className={`form-control ${validPathClass}`}
|
||||
value={customPath}
|
||||
placeholder="m/44'/60'/0'/0"
|
||||
onChange={this.handleChangeCustomPath}
|
||||
/>}
|
||||
/>
|
||||
)}
|
||||
</form>
|
||||
|
||||
<div className="DWModal-addresses">
|
||||
@ -145,9 +148,7 @@ class DeterministicWalletsModal extends React.Component<Props, State> {
|
||||
<tr>
|
||||
<td>#</td>
|
||||
<td>Address</td>
|
||||
<td>
|
||||
{network.unit}
|
||||
</td>
|
||||
<td>{network.unit}</td>
|
||||
<td>
|
||||
<select
|
||||
className="DWModal-addresses-table-token"
|
||||
@ -155,11 +156,11 @@ class DeterministicWalletsModal extends React.Component<Props, State> {
|
||||
onChange={this.handleChangeToken}
|
||||
>
|
||||
<option value="">-Token-</option>
|
||||
{tokens.map(t =>
|
||||
{tokens.map(t => (
|
||||
<option key={t.symbol} value={t.symbol}>
|
||||
{t.symbol}
|
||||
</option>
|
||||
)}
|
||||
))}
|
||||
</select>
|
||||
</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 { selectedAddress } = this.state;
|
||||
|
||||
// Get renderable values, but keep 'em short
|
||||
const value = wallet.value ? wallet.value.toEther().toPrecision(4) : '';
|
||||
const tokenValue = wallet.tokenValues[desiredToken]
|
||||
? wallet.tokenValues[desiredToken].toPrecision(4)
|
||||
: '';
|
||||
const token = wallet.tokenValues[desiredToken];
|
||||
|
||||
return (
|
||||
<tr
|
||||
key={wallet.address}
|
||||
onClick={this.selectAddress.bind(this, wallet.address, wallet.index)}
|
||||
>
|
||||
<td>
|
||||
{wallet.index + 1}
|
||||
</td>
|
||||
<td>{wallet.index + 1}</td>
|
||||
<td className="DWModal-addresses-table-address">
|
||||
<input
|
||||
type="radio"
|
||||
@ -293,10 +289,24 @@ class DeterministicWalletsModal extends React.Component<Props, State> {
|
||||
{wallet.address}
|
||||
</td>
|
||||
<td>
|
||||
{value} {network.unit}
|
||||
<UnitDisplay
|
||||
unit={'ether'}
|
||||
value={wallet.value}
|
||||
symbol={network.unit}
|
||||
displayShortBalance={true}
|
||||
/>
|
||||
</td>
|
||||
<td>
|
||||
{tokenValue} {desiredToken}
|
||||
{token ? (
|
||||
<UnitDisplay
|
||||
decimal={token.decimal}
|
||||
value={token.value}
|
||||
symbol={desiredToken}
|
||||
displayShortBalance={true}
|
||||
/>
|
||||
) : (
|
||||
'???'
|
||||
)}
|
||||
</td>
|
||||
<td>
|
||||
<a
|
||||
@ -311,7 +321,7 @@ class DeterministicWalletsModal extends React.Component<Props, State> {
|
||||
}
|
||||
}
|
||||
|
||||
function mapStateToProps(state) {
|
||||
function mapStateToProps(state: AppState) {
|
||||
return {
|
||||
wallets: state.deterministicWallets.wallets,
|
||||
desiredToken: state.deterministicWallets.desiredToken,
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { isKeystorePassRequired } from 'libs/keystore';
|
||||
import { isKeystorePassRequired } from 'libs/wallet';
|
||||
import React, { Component } from 'react';
|
||||
import translate, { translateRaw } from 'translations';
|
||||
|
||||
@ -32,9 +32,7 @@ export default class KeystoreDecrypt extends Component {
|
||||
return (
|
||||
<section className="col-md-4 col-sm-6">
|
||||
<div id="selectedUploadKey">
|
||||
<h4>
|
||||
{translate('ADD_Radio_2_alt')}
|
||||
</h4>
|
||||
<h4>{translate('ADD_Radio_2_alt')}</h4>
|
||||
|
||||
<div className="form-group">
|
||||
<input
|
||||
@ -54,13 +52,11 @@ export default class KeystoreDecrypt extends Component {
|
||||
</a>
|
||||
</label>
|
||||
<div className={file.length && passReq ? '' : 'hidden'}>
|
||||
<p>
|
||||
{translate('ADD_Label_3')}
|
||||
</p>
|
||||
<p>{translate('ADD_Label_3')}</p>
|
||||
<input
|
||||
className={`form-control ${password.length > 0
|
||||
? 'is-valid'
|
||||
: 'is-invalid'}`}
|
||||
className={`form-control ${
|
||||
password.length > 0 ? 'is-valid' : 'is-invalid'
|
||||
}`}
|
||||
value={password}
|
||||
onChange={this.onPasswordChange}
|
||||
onKeyDown={this.onKeyDown}
|
||||
|
@ -2,7 +2,7 @@ import './LedgerNano.scss';
|
||||
import React, { Component } from 'react';
|
||||
import translate, { translateRaw } from 'translations';
|
||||
import DeterministicWalletsModal from './DeterministicWalletsModal';
|
||||
import LedgerWallet from 'libs/wallet/ledger';
|
||||
import { LedgerWallet } from 'libs/wallet';
|
||||
import Ledger3 from 'vendor/ledger3';
|
||||
import LedgerEth from 'vendor/ledger-eth';
|
||||
import DPATHS from 'config/dpaths';
|
||||
|
@ -31,9 +31,7 @@ export default class MnemonicDecrypt extends Component<Props, State> {
|
||||
return (
|
||||
<section className="col-md-4 col-sm-6">
|
||||
<div id="selectedTypeKey">
|
||||
<h4>
|
||||
{translate('ADD_Radio_5')}
|
||||
</h4>
|
||||
<h4>{translate('ADD_Radio_5')}</h4>
|
||||
<div className="form-group">
|
||||
<textarea
|
||||
id="aria-private-key"
|
||||
@ -56,7 +54,7 @@ export default class MnemonicDecrypt extends Component<Props, State> {
|
||||
type="password"
|
||||
/>
|
||||
</div>
|
||||
{isValidMnemonic &&
|
||||
{isValidMnemonic && (
|
||||
<div className="form-group">
|
||||
<button
|
||||
style={{ width: '100%' }}
|
||||
@ -65,7 +63,8 @@ export default class MnemonicDecrypt extends Component<Props, State> {
|
||||
>
|
||||
{translate('Choose Address')}
|
||||
</button>
|
||||
</div>}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<DeterministicWalletsModal
|
||||
@ -90,7 +89,7 @@ export default class MnemonicDecrypt extends Component<Props, State> {
|
||||
this.setState({ phrase: (e.target as HTMLTextAreaElement).value });
|
||||
};
|
||||
|
||||
public onDWModalOpen = (e: React.SyntheticEvent<HTMLButtonElement>) => {
|
||||
public onDWModalOpen = () => {
|
||||
const { phrase, pass } = this.state;
|
||||
|
||||
if (!validateMnemonic(phrase)) {
|
||||
|
@ -1,5 +1,5 @@
|
||||
import DPATHS from 'config/dpaths';
|
||||
import TrezorWallet from 'libs/wallet/trezor';
|
||||
import { TrezorWallet } from 'libs/wallet';
|
||||
import React, { Component } from 'react';
|
||||
import translate, { translateRaw } from 'translations';
|
||||
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));
|
||||
};
|
||||
|
||||
private handleNullConnect(): void {
|
||||
return this.handleConnect();
|
||||
}
|
||||
private handleNullConnect = (): void => this.handleConnect();
|
||||
}
|
||||
|
26
common/components/WalletDecrypt/Web3.scss
Normal file
26
common/components/WalletDecrypt/Web3.scss
Normal file
@ -0,0 +1,26 @@
|
||||
.Web3Decrypt {
|
||||
text-align: center;
|
||||
padding-top: 30px;
|
||||
|
||||
&-decrypt {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
&-help {
|
||||
margin-top: 10px;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
&-error {
|
||||
opacity: 0;
|
||||
transition: none;
|
||||
|
||||
&.is-showing {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
&-install {
|
||||
margin-top: 10px;
|
||||
}
|
||||
}
|
33
common/components/WalletDecrypt/Web3.tsx
Normal file
33
common/components/WalletDecrypt/Web3.tsx
Normal file
@ -0,0 +1,33 @@
|
||||
import React, { Component } from 'react';
|
||||
import translate from 'translations';
|
||||
import { NewTabLink } from 'components/ui';
|
||||
import './Web3.scss';
|
||||
|
||||
interface Props {
|
||||
onUnlock(): void;
|
||||
}
|
||||
|
||||
export default class Web3Decrypt extends Component<Props> {
|
||||
public render() {
|
||||
return (
|
||||
<section className="Web3Decrypt col-md-4 col-sm-6">
|
||||
<div>
|
||||
<button
|
||||
className="Web3Decrypt btn btn-primary btn-lg"
|
||||
onClick={this.props.onUnlock}
|
||||
>
|
||||
{translate('ADD_MetaMask')}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<NewTabLink
|
||||
className="Web3Decrypt-install btn btn-sm btn-default"
|
||||
content={translate('Download MetaMask')}
|
||||
href="https://chrome.google.com/webstore/detail/metamask/nkbihfbeogaeaoehlefnkodbefgpgknn?hl=en"
|
||||
/>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
}
|
@ -5,7 +5,8 @@ import {
|
||||
unlockMnemonic,
|
||||
UnlockMnemonicAction,
|
||||
unlockPrivateKey,
|
||||
UnlockPrivateKeyAction
|
||||
UnlockPrivateKeyAction,
|
||||
unlockWeb3
|
||||
} from 'actions/wallet';
|
||||
import isEmpty from 'lodash/isEmpty';
|
||||
import map from 'lodash/map';
|
||||
@ -20,6 +21,7 @@ import PrivateKeyDecrypt, { PrivateKeyValue } from './PrivateKey';
|
||||
import TrezorDecrypt from './Trezor';
|
||||
import ViewOnlyDecrypt from './ViewOnly';
|
||||
import { AppState } from 'reducers';
|
||||
import Web3Decrypt from './Web3';
|
||||
|
||||
const WALLETS = {
|
||||
'keystore-file': {
|
||||
@ -63,6 +65,13 @@ const WALLETS = {
|
||||
unlock: setWallet,
|
||||
disabled: false
|
||||
},
|
||||
web3: {
|
||||
lid: 'x_MetaMask',
|
||||
component: Web3Decrypt,
|
||||
initialParams: {},
|
||||
unlock: unlockWeb3,
|
||||
disabled: false
|
||||
},
|
||||
'view-only': {
|
||||
lid: 'View with Address Only',
|
||||
component: ViewOnlyDecrypt,
|
||||
|
@ -1,6 +1,5 @@
|
||||
export { default as Header } from './Header';
|
||||
export { default as Footer } from './Footer';
|
||||
export { default as Root } from './Root';
|
||||
export { default as BalanceSidebar } from './BalanceSidebar';
|
||||
export { default as PaperWallet } from './PaperWallet';
|
||||
export { default as AlphaAgreement } from './AlphaAgreement';
|
||||
|
60
common/components/renderCbs/UnitConverter.tsx
Normal file
60
common/components/renderCbs/UnitConverter.tsx
Normal file
@ -0,0 +1,60 @@
|
||||
import { toTokenBase } from 'libs/units';
|
||||
|
||||
import React, { Component } from 'react';
|
||||
|
||||
interface IChildren {
|
||||
onUserInput: UnitConverter['onUserInput'];
|
||||
convertedUnit: string;
|
||||
}
|
||||
interface IFakeEvent {
|
||||
currentTarget: {
|
||||
value: string;
|
||||
};
|
||||
}
|
||||
|
||||
export interface Props {
|
||||
decimal: number;
|
||||
children({ onUserInput, convertedUnit }: IChildren): React.ReactElement<any>;
|
||||
onChange(baseUnit: IFakeEvent);
|
||||
}
|
||||
|
||||
interface State {
|
||||
userInput: string;
|
||||
}
|
||||
|
||||
const initialState = { userInput: '' };
|
||||
|
||||
export class UnitConverter extends Component<Props, State> {
|
||||
public state: State = initialState;
|
||||
|
||||
public componentWillReceiveProps(nextProps: Props) {
|
||||
const { userInput } = this.state;
|
||||
|
||||
if (this.props.decimal !== nextProps.decimal) {
|
||||
this.baseUnitCb(userInput, nextProps.decimal);
|
||||
}
|
||||
}
|
||||
|
||||
public onUserInput = (e: React.FormEvent<HTMLInputElement>) => {
|
||||
const { value } = e.currentTarget;
|
||||
const { decimal } = this.props;
|
||||
this.baseUnitCb(value, decimal);
|
||||
this.setState({ userInput: value });
|
||||
};
|
||||
|
||||
public render() {
|
||||
return this.props.children({
|
||||
onUserInput: this.onUserInput,
|
||||
convertedUnit: this.state.userInput
|
||||
});
|
||||
}
|
||||
private baseUnitCb = (value: string, decimal: number) => {
|
||||
const baseUnit = toTokenBase(value, decimal).toString();
|
||||
const fakeEvent = {
|
||||
currentTarget: {
|
||||
value: baseUnit
|
||||
}
|
||||
};
|
||||
this.props.onChange(fakeEvent);
|
||||
};
|
||||
}
|
1
common/components/renderCbs/index.ts
Normal file
1
common/components/renderCbs/index.ts
Normal file
@ -0,0 +1 @@
|
||||
export * from './UnitConverter';
|
14
common/components/ui/Code.scss
Normal file
14
common/components/ui/Code.scss
Normal file
@ -0,0 +1,14 @@
|
||||
pre {
|
||||
color: #333;
|
||||
background-color: #fafafa;
|
||||
border: 1px solid #ececec;
|
||||
border-radius: 0px;
|
||||
padding: 8px;
|
||||
code {
|
||||
font-size: 14px;
|
||||
line-height: 20px;
|
||||
word-break: break-all;
|
||||
word-wrap: break-word;
|
||||
white-space: pre;
|
||||
}
|
||||
}
|
10
common/components/ui/Code.tsx
Normal file
10
common/components/ui/Code.tsx
Normal file
@ -0,0 +1,10 @@
|
||||
import React from 'react';
|
||||
import './Code.scss';
|
||||
|
||||
const Code = ({ children }) => (
|
||||
<pre>
|
||||
<code>{children}</code>
|
||||
</pre>
|
||||
);
|
||||
|
||||
export default Code;
|
23
common/components/ui/ColorDropdown.scss
Normal file
23
common/components/ui/ColorDropdown.scss
Normal file
@ -0,0 +1,23 @@
|
||||
.ColorDropdown {
|
||||
&-item {
|
||||
position: relative;
|
||||
padding-right: 10px;
|
||||
border-left: 2px solid;
|
||||
|
||||
&-remove {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
right: 5px;
|
||||
width: 15px;
|
||||
height: 15px;
|
||||
opacity: 0.5;
|
||||
cursor: pointer;
|
||||
// Z fixes clipping issue
|
||||
transform: translateY(-50%) translateZ(0);
|
||||
|
||||
&:hover {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -1,11 +1,15 @@
|
||||
import React, { Component } from 'react';
|
||||
import classnames from 'classnames';
|
||||
import DropdownShell from './DropdownShell';
|
||||
import removeIcon from 'assets/images/icon-remove.svg';
|
||||
import './ColorDropdown.scss';
|
||||
|
||||
interface Option<T> {
|
||||
name: any;
|
||||
value: T;
|
||||
color?: string;
|
||||
hidden: boolean | undefined;
|
||||
onRemove?(): void;
|
||||
}
|
||||
|
||||
interface Props<T> {
|
||||
@ -17,6 +21,7 @@ interface Props<T> {
|
||||
size?: string;
|
||||
color?: string;
|
||||
menuAlign?: string;
|
||||
disabled?: boolean;
|
||||
onChange(value: T): void;
|
||||
}
|
||||
|
||||
@ -24,7 +29,7 @@ export default class ColorDropdown<T> extends Component<Props<T>, {}> {
|
||||
private dropdownShell: DropdownShell | null;
|
||||
|
||||
public render() {
|
||||
const { ariaLabel, color, size } = this.props;
|
||||
const { ariaLabel, disabled, color, size } = this.props;
|
||||
|
||||
return (
|
||||
<DropdownShell
|
||||
@ -34,6 +39,7 @@ export default class ColorDropdown<T> extends Component<Props<T>, {}> {
|
||||
color={color}
|
||||
ariaLabel={ariaLabel}
|
||||
ref={el => (this.dropdownShell = el)}
|
||||
disabled={disabled}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@ -52,18 +58,19 @@ export default class ColorDropdown<T> extends Component<Props<T>, {}> {
|
||||
private renderOptions = () => {
|
||||
const { options, value, menuAlign, extra } = this.props;
|
||||
|
||||
const activeOption = this.getActiveOption();
|
||||
|
||||
const listItems = options.reduce((prev: any[], opt) => {
|
||||
const prevOpt = prev.length ? prev[prev.length - 1] : null;
|
||||
if (prevOpt && !prevOpt.divider && prevOpt.color !== opt.color) {
|
||||
prev.push({ divider: true });
|
||||
}
|
||||
prev.push(opt);
|
||||
return prev;
|
||||
}, []);
|
||||
const listItems = options
|
||||
.filter(opt => !opt.hidden)
|
||||
.reduce((prev: any[], opt) => {
|
||||
const prevOpt = prev.length ? prev[prev.length - 1] : null;
|
||||
if (prevOpt && !prevOpt.divider && prevOpt.color !== opt.color) {
|
||||
prev.push({ divider: true });
|
||||
}
|
||||
prev.push(opt);
|
||||
return prev;
|
||||
}, []);
|
||||
|
||||
const menuClass = classnames({
|
||||
ColorDropdown: true,
|
||||
'dropdown-menu': true,
|
||||
[`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" />;
|
||||
} else {
|
||||
return (
|
||||
<li key={i} style={{ borderLeft: `2px solid ${option.color}` }}>
|
||||
<li
|
||||
key={i}
|
||||
className="ColorDropdown-item"
|
||||
style={{ borderColor: option.color }}
|
||||
>
|
||||
<a
|
||||
className={option.value === value ? 'active' : ''}
|
||||
onClick={this.onChange.bind(null, option.value)}
|
||||
>
|
||||
{option.name}
|
||||
|
||||
{option.onRemove && (
|
||||
<img
|
||||
className="ColorDropdown-item-remove"
|
||||
onClick={this.onRemove.bind(null, option.onRemove)}
|
||||
src={removeIcon}
|
||||
/>
|
||||
)}
|
||||
</a>
|
||||
</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() {
|
||||
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 = () => {
|
||||
const { label, value } = this.props;
|
||||
const { value } = this.props;
|
||||
const labelStr = this.props.label ? `${this.props.label}:` : '';
|
||||
return (
|
||||
<span>
|
||||
|
@ -3,6 +3,7 @@ import classnames from 'classnames';
|
||||
|
||||
interface Props {
|
||||
ariaLabel: string;
|
||||
disabled?: boolean;
|
||||
size?: string;
|
||||
color?: string;
|
||||
renderLabel(): any;
|
||||
@ -34,7 +35,14 @@ export default class DropdownComponent extends Component<Props, State> {
|
||||
}
|
||||
|
||||
public render() {
|
||||
const { ariaLabel, color, size, renderOptions, renderLabel } = this.props;
|
||||
const {
|
||||
ariaLabel,
|
||||
color,
|
||||
disabled,
|
||||
size,
|
||||
renderOptions,
|
||||
renderLabel
|
||||
} = this.props;
|
||||
const { expanded } = this.state;
|
||||
const toggleClasses = classnames([
|
||||
'dropdown-toggle',
|
||||
@ -45,7 +53,7 @@ export default class DropdownComponent extends Component<Props, State> {
|
||||
|
||||
return (
|
||||
<span
|
||||
className={`dropdown ${expanded ? 'open' : ''}`}
|
||||
className={`dropdown ${expanded || disabled ? 'open' : ''}`}
|
||||
ref={el => (this.dropdown = el)}
|
||||
>
|
||||
<a
|
||||
@ -57,9 +65,9 @@ export default class DropdownComponent extends Component<Props, State> {
|
||||
onClick={this.toggle}
|
||||
>
|
||||
{renderLabel()}
|
||||
<i className="caret" />
|
||||
{!disabled && <i className="caret" />}
|
||||
</a>
|
||||
{expanded && renderOptions()}
|
||||
{expanded && !disabled && renderOptions()}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
@ -1,6 +1,5 @@
|
||||
import React from 'react';
|
||||
import helpIcon from 'assets/images/icon-help.svg';
|
||||
import translate, { translateRaw } from 'translations';
|
||||
|
||||
type sizeType = 'small' | 'medium' | 'large';
|
||||
|
||||
|
@ -3,7 +3,7 @@ import React, { Component } from 'react';
|
||||
import './Modal.scss';
|
||||
|
||||
export interface IButton {
|
||||
text: string;
|
||||
text: string | React.ReactElement<string>;
|
||||
type?:
|
||||
| 'default'
|
||||
| 'primary'
|
||||
@ -17,7 +17,7 @@ export interface IButton {
|
||||
}
|
||||
interface Props {
|
||||
isOpen?: boolean;
|
||||
title: string;
|
||||
title: string | React.ReactElement<any>;
|
||||
disableButtons?: boolean;
|
||||
children: any;
|
||||
buttons: IButton[];
|
||||
|
@ -2,6 +2,7 @@ import React from 'react';
|
||||
|
||||
interface AAttributes {
|
||||
charset?: string;
|
||||
className?: string;
|
||||
coords?: string;
|
||||
download?: string;
|
||||
href: string;
|
||||
@ -28,14 +29,15 @@ interface AAttributes {
|
||||
type?: string;
|
||||
}
|
||||
|
||||
interface NewTabLinkProps extends AAttributes {
|
||||
interface NewTabLinkProps extends AAttributes {
|
||||
content?: 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}>
|
||||
{content || children} {/* Keep content for short-hand text insertion */}
|
||||
</a>;
|
||||
</a>
|
||||
);
|
||||
|
||||
export default NewTabLink;
|
||||
|
66
common/components/ui/Spinner.scss
Normal file
66
common/components/ui/Spinner.scss
Normal file
@ -0,0 +1,66 @@
|
||||
.Spinner {
|
||||
animation: rotate 2s linear infinite;
|
||||
|
||||
&-x1 {
|
||||
height: 1em;
|
||||
width: 1em;
|
||||
}
|
||||
|
||||
&-x2 {
|
||||
height: 2em;
|
||||
width: 2em;
|
||||
}
|
||||
|
||||
&-x3 {
|
||||
height: 3em;
|
||||
width: 3em;
|
||||
}
|
||||
|
||||
&-x4 {
|
||||
height: 4em;
|
||||
width: 4em;
|
||||
}
|
||||
|
||||
&-x5 {
|
||||
height: 5em;
|
||||
width: 5em;
|
||||
}
|
||||
|
||||
& .path {
|
||||
stroke-linecap: round;
|
||||
animation: dash 1.5s ease-in-out infinite;
|
||||
}
|
||||
|
||||
&-light {
|
||||
& .path {
|
||||
stroke: white;
|
||||
}
|
||||
}
|
||||
|
||||
&-dark {
|
||||
& .path {
|
||||
stroke: #163151;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes rotate {
|
||||
100% {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes dash {
|
||||
0% {
|
||||
stroke-dasharray: 1, 150;
|
||||
stroke-dashoffset: 0;
|
||||
}
|
||||
50% {
|
||||
stroke-dasharray: 90, 150;
|
||||
stroke-dashoffset: -35;
|
||||
}
|
||||
100% {
|
||||
stroke-dasharray: 90, 150;
|
||||
stroke-dashoffset: -124;
|
||||
}
|
||||
}
|
@ -1,13 +1,27 @@
|
||||
import React from 'react';
|
||||
import './Spinner.scss';
|
||||
|
||||
type Size = 'lg' | '2x' | '3x' | '4x' | '5x';
|
||||
type Size = 'x1' | 'x2' | 'x3' | 'x4' | 'x5';
|
||||
|
||||
interface SpinnerProps {
|
||||
size?: Size;
|
||||
light?: boolean;
|
||||
}
|
||||
|
||||
const Spinner = ({ size = 'fa-' }: SpinnerProps) => {
|
||||
return <i className={`fa fa-spinner fa-spin fa-${size ? size : 'fw'}`} />;
|
||||
const Spinner = ({ size = 'x1', light = false }: SpinnerProps) => {
|
||||
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;
|
||||
|
73
common/components/ui/UnitDisplay.tsx
Normal file
73
common/components/ui/UnitDisplay.tsx
Normal file
@ -0,0 +1,73 @@
|
||||
import React from 'react';
|
||||
import {
|
||||
fromTokenBase,
|
||||
getDecimal,
|
||||
UnitKey,
|
||||
Wei,
|
||||
TokenValue
|
||||
} from 'libs/units';
|
||||
import { formatNumber as format } from 'utils/formatters';
|
||||
|
||||
interface Props {
|
||||
/**
|
||||
* @description base value of the token / ether, incase of waiting for API calls, we can return '???'
|
||||
* @type {TokenValue | Wei}
|
||||
* @memberof Props
|
||||
*/
|
||||
value?: TokenValue | Wei | null;
|
||||
/**
|
||||
* @description Symbol to display to the right of the value, such as 'ETH'
|
||||
* @type {string}
|
||||
* @memberof Props
|
||||
*/
|
||||
symbol?: string;
|
||||
/**
|
||||
* @description display the long balance, if false, trims it to 3 decimal places, if a number is specified then that number is the number of digits to be displayed.
|
||||
* @type {boolean}
|
||||
* @memberof Props
|
||||
*/
|
||||
displayShortBalance?: boolean | number;
|
||||
}
|
||||
|
||||
interface EthProps extends Props {
|
||||
unit: UnitKey;
|
||||
}
|
||||
interface TokenProps extends Props {
|
||||
decimal: number;
|
||||
}
|
||||
|
||||
const isEthereumUnit = (param: EthProps | TokenProps): param is EthProps =>
|
||||
!!(param as EthProps).unit;
|
||||
|
||||
const UnitDisplay: React.SFC<EthProps | TokenProps> = params => {
|
||||
const { value, symbol, displayShortBalance } = params;
|
||||
|
||||
if (!value) {
|
||||
return <span>Balance isn't available offline</span>;
|
||||
}
|
||||
|
||||
const convertedValue = isEthereumUnit(params)
|
||||
? fromTokenBase(value, getDecimal(params.unit))
|
||||
: fromTokenBase(value, params.decimal);
|
||||
|
||||
let formattedValue;
|
||||
|
||||
if (displayShortBalance) {
|
||||
const digits =
|
||||
typeof displayShortBalance === 'number' && displayShortBalance;
|
||||
formattedValue = digits
|
||||
? format(convertedValue, digits)
|
||||
: format(convertedValue);
|
||||
} else {
|
||||
formattedValue = convertedValue;
|
||||
}
|
||||
|
||||
return (
|
||||
<span>
|
||||
{formattedValue}
|
||||
{symbol ? ` ${symbol}` : ''}
|
||||
</span>
|
||||
);
|
||||
};
|
||||
|
||||
export default UnitDisplay;
|
@ -6,3 +6,4 @@ export { default as Modal } from './Modal';
|
||||
export { default as UnlockHeader } from './UnlockHeader';
|
||||
export { default as QRCode } from './QRCode';
|
||||
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');
|
||||
// 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.
|
||||
// Type can be primary, warning, danger, success, or info.
|
||||
@ -74,9 +75,21 @@ export interface NetworkConfig {
|
||||
|
||||
export interface NodeConfig {
|
||||
network: string;
|
||||
lib: RPCNode;
|
||||
lib: RPCNode | Web3Node;
|
||||
service: string;
|
||||
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
|
||||
@ -242,3 +255,44 @@ export const NODES: { [key: string]: NodeConfig } = {
|
||||
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';
|
||||
import React from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import { TransitionGroup, CSSTransition } from 'react-transition-group';
|
||||
import NotificationRow from './NotificationRow';
|
||||
import './Notifications.scss';
|
||||
|
||||
@ -12,21 +13,30 @@ interface Props {
|
||||
notifications: Notification[];
|
||||
closeNotification: TCloseNotification;
|
||||
}
|
||||
|
||||
const Transition = props => (
|
||||
<CSSTransition
|
||||
{...props}
|
||||
classNames="NotificationAnimation"
|
||||
timeout={{ enter: 500, exit: 500 }}
|
||||
/>
|
||||
);
|
||||
|
||||
export class Notifications extends React.Component<Props, {}> {
|
||||
public render() {
|
||||
if (!this.props.notifications.length) {
|
||||
return null;
|
||||
}
|
||||
return (
|
||||
<div className="Notifications">
|
||||
{this.props.notifications.map((n, i) => (
|
||||
<NotificationRow
|
||||
key={`${n.level}-${i}`}
|
||||
notification={n}
|
||||
onClose={this.props.closeNotification}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
<TransitionGroup className="Notifications">
|
||||
{this.props.notifications.map(n => {
|
||||
return (
|
||||
<Transition key={n.id}>
|
||||
<NotificationRow
|
||||
notification={n}
|
||||
onClose={this.props.closeNotification}
|
||||
/>
|
||||
</Transition>
|
||||
);
|
||||
})}
|
||||
</TransitionGroup>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -2,50 +2,72 @@ import {
|
||||
changeGasPrice as dChangeGasPrice,
|
||||
changeLanguage as dChangeLanguage,
|
||||
changeNodeIntent as dChangeNodeIntent,
|
||||
addCustomNode as dAddCustomNode,
|
||||
removeCustomNode as dRemoveCustomNode,
|
||||
TChangeGasPrice,
|
||||
TChangeLanguage,
|
||||
TChangeNodeIntent
|
||||
TChangeNodeIntent,
|
||||
TAddCustomNode,
|
||||
TRemoveCustomNode,
|
||||
} from 'actions/config';
|
||||
import { AlphaAgreement, Footer, Header } from 'components';
|
||||
import React, { Component } from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import { AppState } from 'reducers';
|
||||
import Notifications from './Notifications';
|
||||
import { NodeConfig, CustomNodeConfig } from 'config/data';
|
||||
|
||||
interface Props {
|
||||
// FIXME
|
||||
children: any;
|
||||
|
||||
languageSelection: string;
|
||||
node: NodeConfig;
|
||||
nodeSelection: string;
|
||||
|
||||
isChangingNode: boolean;
|
||||
gasPriceGwei: number;
|
||||
customNodes: CustomNodeConfig[];
|
||||
latestBlock: string;
|
||||
|
||||
changeLanguage: TChangeLanguage;
|
||||
changeNodeIntent: TChangeNodeIntent;
|
||||
changeGasPrice: TChangeGasPrice;
|
||||
addCustomNode: TAddCustomNode;
|
||||
removeCustomNode: TRemoveCustomNode;
|
||||
}
|
||||
class TabSection extends Component<Props, {}> {
|
||||
public render() {
|
||||
const {
|
||||
children,
|
||||
// APP
|
||||
node,
|
||||
nodeSelection,
|
||||
isChangingNode,
|
||||
languageSelection,
|
||||
gasPriceGwei,
|
||||
customNodes,
|
||||
latestBlock,
|
||||
|
||||
changeLanguage,
|
||||
changeNodeIntent,
|
||||
changeGasPrice
|
||||
changeGasPrice,
|
||||
addCustomNode,
|
||||
removeCustomNode,
|
||||
} = this.props;
|
||||
|
||||
const headerProps = {
|
||||
languageSelection,
|
||||
node,
|
||||
nodeSelection,
|
||||
isChangingNode,
|
||||
gasPriceGwei,
|
||||
customNodes,
|
||||
|
||||
changeLanguage,
|
||||
changeNodeIntent,
|
||||
changeGasPrice
|
||||
changeGasPrice,
|
||||
addCustomNode,
|
||||
removeCustomNode,
|
||||
};
|
||||
|
||||
return (
|
||||
@ -53,7 +75,7 @@ class TabSection extends Component<Props, {}> {
|
||||
<main>
|
||||
<Header {...headerProps} />
|
||||
<div className="Tab container">{children}</div>
|
||||
<Footer />
|
||||
<Footer latestBlock={latestBlock} />
|
||||
</main>
|
||||
<Notifications />
|
||||
<AlphaAgreement />
|
||||
@ -64,14 +86,20 @@ class TabSection extends Component<Props, {}> {
|
||||
|
||||
function mapStateToProps(state: AppState) {
|
||||
return {
|
||||
node: state.config.node,
|
||||
nodeSelection: state.config.nodeSelection,
|
||||
isChangingNode: state.config.isChangingNode,
|
||||
languageSelection: state.config.languageSelection,
|
||||
gasPriceGwei: state.config.gasPriceGwei
|
||||
gasPriceGwei: state.config.gasPriceGwei,
|
||||
customNodes: state.config.customNodes,
|
||||
latestBlock: state.config.latestBlock,
|
||||
};
|
||||
}
|
||||
|
||||
export default connect(mapStateToProps, {
|
||||
changeGasPrice: dChangeGasPrice,
|
||||
changeLanguage: dChangeLanguage,
|
||||
changeNodeIntent: dChangeNodeIntent
|
||||
changeNodeIntent: dChangeNodeIntent,
|
||||
addCustomNode: dAddCustomNode,
|
||||
removeCustomNode: dRemoveCustomNode,
|
||||
})(TabSection);
|
||||
|
7
common/containers/Tabs/BroadcastTx/index.scss
Normal file
7
common/containers/Tabs/BroadcastTx/index.scss
Normal file
@ -0,0 +1,7 @@
|
||||
@import "common/sass/variables";
|
||||
|
||||
.BroadcastTx {
|
||||
&-title {
|
||||
margin: $space auto $space * 2.5;
|
||||
}
|
||||
}
|
140
common/containers/Tabs/BroadcastTx/index.tsx
Normal file
140
common/containers/Tabs/BroadcastTx/index.tsx
Normal file
@ -0,0 +1,140 @@
|
||||
import React, { Component } from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import { AppState } from 'reducers';
|
||||
import TabSection from 'containers/TabSection';
|
||||
import { translateRaw } from 'translations';
|
||||
import { broadcastTx as dBroadcastTx, TBroadcastTx } from 'actions/wallet';
|
||||
import { QRCode } from 'components/ui';
|
||||
import './index.scss';
|
||||
import {
|
||||
BroadcastTransactionStatus,
|
||||
getTransactionFields
|
||||
} from 'libs/transaction';
|
||||
import EthTx from 'ethereumjs-tx';
|
||||
import { ConfirmationModal } from 'containers/Tabs/SendTransaction/components';
|
||||
import classnames from 'classnames';
|
||||
|
||||
interface Props {
|
||||
broadcastTx: TBroadcastTx;
|
||||
transactions: BroadcastTransactionStatus[];
|
||||
}
|
||||
|
||||
interface State {
|
||||
signedTx: string;
|
||||
showConfirmationModal: boolean;
|
||||
disabled: boolean;
|
||||
}
|
||||
|
||||
const initialState: State = {
|
||||
showConfirmationModal: false,
|
||||
signedTx: '',
|
||||
disabled: true
|
||||
};
|
||||
|
||||
class BroadcastTx extends Component<Props, State> {
|
||||
public state = initialState;
|
||||
|
||||
public ensureValidSignedTxInputOnUpdate() {
|
||||
try {
|
||||
const tx = new EthTx(this.state.signedTx);
|
||||
getTransactionFields(tx);
|
||||
if (this.state.disabled) {
|
||||
this.setState({ disabled: false });
|
||||
}
|
||||
} catch (e) {
|
||||
if (!this.state.disabled) {
|
||||
this.setState({ disabled: true });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public componentDidUpdate() {
|
||||
this.ensureValidSignedTxInputOnUpdate();
|
||||
}
|
||||
|
||||
public render() {
|
||||
const { signedTx, disabled, showConfirmationModal } = this.state;
|
||||
|
||||
const inputClasses = classnames({
|
||||
'form-control': true,
|
||||
'is-valid': !disabled,
|
||||
'is-invalid': disabled
|
||||
});
|
||||
|
||||
return (
|
||||
<TabSection>
|
||||
<div className="Tab-content-pane row block text-center">
|
||||
<div className="col-md-6">
|
||||
<div className="col-md-12 BroadcastTx-title">
|
||||
<h2>Broadcast Signed Transaction</h2>
|
||||
</div>
|
||||
<p>
|
||||
Paste a signed transaction and press the "SEND TRANSACTION"
|
||||
button.
|
||||
</p>
|
||||
<label>{translateRaw('SEND_signed')}</label>
|
||||
<textarea
|
||||
className={inputClasses}
|
||||
rows={7}
|
||||
value={signedTx}
|
||||
onChange={this.handleChange}
|
||||
/>
|
||||
<button
|
||||
className="btn btn-primary"
|
||||
disabled={disabled || signedTx === ''}
|
||||
onClick={this.handleBroadcastTx}
|
||||
>
|
||||
{translateRaw('SEND_trans')}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="col-md-6" style={{ marginTop: '70px' }}>
|
||||
<div
|
||||
className="qr-code text-center"
|
||||
style={{
|
||||
maxWidth: '15rem',
|
||||
margin: '1rem auto',
|
||||
width: '100%'
|
||||
}}
|
||||
>
|
||||
{signedTx && <QRCode data={signedTx} />}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{showConfirmationModal && (
|
||||
<ConfirmationModal
|
||||
signedTx={signedTx}
|
||||
onClose={this.handleClose}
|
||||
onConfirm={this.handleConfirm}
|
||||
/>
|
||||
)}
|
||||
</TabSection>
|
||||
);
|
||||
}
|
||||
|
||||
public handleClose = () => {
|
||||
this.setState({ showConfirmationModal: false });
|
||||
};
|
||||
|
||||
public handleBroadcastTx = () => {
|
||||
this.setState({ showConfirmationModal: true });
|
||||
};
|
||||
|
||||
public handleConfirm = () => {
|
||||
this.props.broadcastTx(this.state.signedTx);
|
||||
};
|
||||
|
||||
protected handleChange = event => {
|
||||
this.setState({ signedTx: event.target.value });
|
||||
};
|
||||
}
|
||||
|
||||
function mapStateToProps(state: AppState) {
|
||||
return {
|
||||
transactions: state.wallet.transactions
|
||||
};
|
||||
}
|
||||
|
||||
export default connect(mapStateToProps, { broadcastTx: dBroadcastTx })(
|
||||
BroadcastTx
|
||||
);
|
@ -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
|
||||
};
|
100
common/containers/Tabs/Contracts/components/Deploy/index.tsx
Normal file
100
common/containers/Tabs/Contracts/components/Deploy/index.tsx
Normal file
@ -0,0 +1,100 @@
|
||||
import React from 'react';
|
||||
import translate from 'translations';
|
||||
import WalletDecrypt from 'components/WalletDecrypt';
|
||||
import { deployHOC } from './components/DeployHoc';
|
||||
import { TTxCompare } from '../TxCompare';
|
||||
import { TTxModal } from '../TxModal';
|
||||
import classnames from 'classnames';
|
||||
import { isValidGasPrice, isValidByteCode } from 'libs/validators';
|
||||
|
||||
export interface Props {
|
||||
byteCode: string;
|
||||
gasLimit: string;
|
||||
walletExists: boolean;
|
||||
txCompare: React.ReactElement<TTxCompare> | null;
|
||||
displayModal: boolean;
|
||||
deployModal: React.ReactElement<TTxModal> | null;
|
||||
handleInput(
|
||||
input: string
|
||||
): (ev: React.FormEvent<HTMLTextAreaElement | HTMLInputElement>) => void;
|
||||
handleSignTx(): Promise<void>;
|
||||
handleDeploy(): void;
|
||||
}
|
||||
|
||||
const Deploy = (props: Props) => {
|
||||
const {
|
||||
handleSignTx,
|
||||
handleInput,
|
||||
handleDeploy,
|
||||
byteCode,
|
||||
gasLimit,
|
||||
walletExists,
|
||||
deployModal,
|
||||
displayModal,
|
||||
txCompare
|
||||
} = props;
|
||||
const validByteCode = isValidByteCode(byteCode);
|
||||
const validGasLimit = isValidGasPrice(gasLimit);
|
||||
const showSignTxButton = validByteCode && validGasLimit;
|
||||
return (
|
||||
<div className="Deploy">
|
||||
<section>
|
||||
<label className="Deploy-field form-group">
|
||||
<h4 className="Deploy-field-label">
|
||||
{translate('CONTRACT_ByteCode')}
|
||||
</h4>
|
||||
<textarea
|
||||
name="byteCode"
|
||||
placeholder="0x8f87a973e..."
|
||||
rows={6}
|
||||
onChange={handleInput('data')}
|
||||
className={classnames('Deploy-field-input', 'form-control', {
|
||||
'is-invalid': !validByteCode
|
||||
})}
|
||||
value={byteCode || ''}
|
||||
/>
|
||||
</label>
|
||||
|
||||
<label className="Deploy-field form-group">
|
||||
<h4 className="Deploy-field-label">Gas Limit</h4>
|
||||
<input
|
||||
name="gasLimit"
|
||||
value={gasLimit || ''}
|
||||
onChange={handleInput('gasLimit')}
|
||||
className={classnames('Deploy-field-input', 'form-control', {
|
||||
'is-invalid': !validGasLimit
|
||||
})}
|
||||
/>
|
||||
</label>
|
||||
|
||||
{walletExists ? (
|
||||
<button
|
||||
className="Sign-submit btn btn-primary"
|
||||
disabled={!showSignTxButton}
|
||||
onClick={handleSignTx}
|
||||
>
|
||||
{translate('DEP_signtx')}
|
||||
</button>
|
||||
) : (
|
||||
<WalletDecrypt />
|
||||
)}
|
||||
|
||||
{txCompare ? (
|
||||
<section>
|
||||
{txCompare}
|
||||
<button
|
||||
className="Deploy-submit btn btn-primary"
|
||||
onClick={handleDeploy}
|
||||
>
|
||||
{translate('NAV_DeployContract')}
|
||||
</button>
|
||||
</section>
|
||||
) : null}
|
||||
|
||||
{displayModal && deployModal}
|
||||
</section>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default deployHOC(Deploy);
|
@ -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);
|
192
common/containers/Tabs/Contracts/components/Interact/index.tsx
Normal file
192
common/containers/Tabs/Contracts/components/Interact/index.tsx
Normal file
@ -0,0 +1,192 @@
|
||||
import React, { Component } from 'react';
|
||||
import InteractForm from './components/InteractForm';
|
||||
import InteractExplorer from './components//InteractExplorer';
|
||||
import Contract from 'libs/contracts';
|
||||
import { withTx, IWithTx } from '../withTx';
|
||||
import {
|
||||
TxModal,
|
||||
Props as DMProps,
|
||||
TTxModal
|
||||
} from 'containers/Tabs/Contracts/components/TxModal';
|
||||
import { IUserSendParams } from 'libs/contracts/ABIFunction';
|
||||
import BN from 'bn.js';
|
||||
import {
|
||||
TxCompare,
|
||||
TTxCompare
|
||||
} from 'containers/Tabs/Contracts/components/TxCompare';
|
||||
|
||||
interface State {
|
||||
currentContract: Contract | null;
|
||||
showExplorer: boolean;
|
||||
address: string | null;
|
||||
signedTx: string | null;
|
||||
rawTx: any | null;
|
||||
gasLimit: string;
|
||||
value: string;
|
||||
displayModal: boolean;
|
||||
}
|
||||
|
||||
class Interact extends Component<IWithTx, State> {
|
||||
public initialState: State = {
|
||||
currentContract: null,
|
||||
showExplorer: false,
|
||||
address: null,
|
||||
signedTx: null,
|
||||
rawTx: null,
|
||||
gasLimit: '30000',
|
||||
value: '0',
|
||||
displayModal: false
|
||||
};
|
||||
public state: State = this.initialState;
|
||||
|
||||
public componentWillReceiveProps(nextProps: IWithTx) {
|
||||
if (nextProps.wallet && this.state.currentContract) {
|
||||
Contract.setConfigForTx(this.state.currentContract, nextProps);
|
||||
}
|
||||
}
|
||||
|
||||
public accessContract = (contractAbi: string, address: string) => () => {
|
||||
try {
|
||||
const parsedAbi = JSON.parse(contractAbi);
|
||||
const contractInstance = new Contract(parsedAbi);
|
||||
contractInstance.at(address);
|
||||
contractInstance.setNode(this.props.nodeLib);
|
||||
this.setState({
|
||||
currentContract: contractInstance,
|
||||
showExplorer: true,
|
||||
address
|
||||
});
|
||||
} catch (e) {
|
||||
this.props.showNotification(
|
||||
'danger',
|
||||
`Contract Access Error: ${(e as Error).message ||
|
||||
'Can not parse contract'}`
|
||||
);
|
||||
this.resetState();
|
||||
}
|
||||
};
|
||||
|
||||
public render() {
|
||||
const {
|
||||
showExplorer,
|
||||
currentContract,
|
||||
gasLimit,
|
||||
value,
|
||||
signedTx,
|
||||
displayModal
|
||||
} = this.state;
|
||||
const { wallet, showNotification } = this.props;
|
||||
const txGenerated = !!signedTx;
|
||||
|
||||
return (
|
||||
<div className="Interact">
|
||||
<InteractForm
|
||||
accessContract={this.accessContract}
|
||||
resetState={this.resetState}
|
||||
/>
|
||||
<hr />
|
||||
{showExplorer &&
|
||||
currentContract && (
|
||||
<InteractExplorer
|
||||
{...{
|
||||
address: currentContract.address,
|
||||
walletDecrypted: !!wallet,
|
||||
handleInput: this.handleInput,
|
||||
contractFunctions: Contract.getFunctions(currentContract),
|
||||
gasLimit,
|
||||
value,
|
||||
handleFunctionSend: this.handleFunctionSend,
|
||||
txGenerated,
|
||||
txModal: txGenerated ? this.makeModal() : null,
|
||||
txCompare: txGenerated ? this.makeCompareTx() : null,
|
||||
toggleModal: this.toggleModal,
|
||||
displayModal,
|
||||
showNotification
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
private makeCompareTx = (): React.ReactElement<TTxCompare> => {
|
||||
const { nonce } = this.state.rawTx;
|
||||
const { signedTx } = this.state;
|
||||
|
||||
if (!nonce || !signedTx) {
|
||||
throw Error('Can not display raw tx, nonce empty or no signed tx');
|
||||
}
|
||||
|
||||
return <TxCompare signedTx={signedTx} />;
|
||||
};
|
||||
|
||||
private makeModal = (): React.ReactElement<TTxModal> => {
|
||||
const { networkName, node: { network, service } } = this.props;
|
||||
const { signedTx } = this.state;
|
||||
|
||||
if (!signedTx) {
|
||||
throw Error('Can not deploy contract, no signed tx');
|
||||
}
|
||||
|
||||
const props: DMProps = {
|
||||
action: 'send a contract state modifying transaction',
|
||||
networkName,
|
||||
network,
|
||||
service,
|
||||
handleBroadcastTx: this.handleBroadcastTx,
|
||||
onClose: this.resetState
|
||||
};
|
||||
|
||||
return <TxModal {...props} />;
|
||||
};
|
||||
|
||||
private toggleModal = () => this.setState({ displayModal: true });
|
||||
|
||||
private resetState = () => this.setState(this.initialState);
|
||||
|
||||
private handleBroadcastTx = () => {
|
||||
const { signedTx } = this.state;
|
||||
if (!signedTx) {
|
||||
return null;
|
||||
}
|
||||
|
||||
this.props.broadcastTx(signedTx);
|
||||
this.resetState();
|
||||
};
|
||||
|
||||
private handleFunctionSend = (selectedFunction, inputs) => async () => {
|
||||
try {
|
||||
const { address, gasLimit, value } = this.state;
|
||||
if (!address) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const parsedInputs = Object.keys(inputs).reduce(
|
||||
(accu, key) => ({ ...accu, [key]: inputs[key].parsedData }),
|
||||
{}
|
||||
);
|
||||
|
||||
const userInputs: IUserSendParams = {
|
||||
input: parsedInputs,
|
||||
to: address,
|
||||
gasLimit: new BN(gasLimit),
|
||||
value
|
||||
};
|
||||
|
||||
const { signedTx, rawTx } = await selectedFunction.send(userInputs);
|
||||
this.setState({ signedTx, rawTx });
|
||||
} catch (e) {
|
||||
this.props.showNotification(
|
||||
'danger',
|
||||
`Function send error: ${(e as Error).message}` ||
|
||||
'Invalid input parameters',
|
||||
5000
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
private handleInput = name => (ev: React.FormEvent<any>) =>
|
||||
this.setState({ [name]: ev.currentTarget.value });
|
||||
}
|
||||
|
||||
export default withTx(Interact);
|
36
common/containers/Tabs/Contracts/components/TxCompare.tsx
Normal file
36
common/containers/Tabs/Contracts/components/TxCompare.tsx
Normal file
@ -0,0 +1,36 @@
|
||||
import React from 'react';
|
||||
import translate from 'translations';
|
||||
import { decodeTransaction } from 'libs/transaction';
|
||||
import EthTx from 'ethereumjs-tx';
|
||||
import Code from 'components/ui/Code';
|
||||
export interface Props {
|
||||
signedTx: string;
|
||||
}
|
||||
|
||||
export const TxCompare = (props: Props) => {
|
||||
if (!props.signedTx) {
|
||||
return null;
|
||||
}
|
||||
const rawTx = decodeTransaction(new EthTx(props.signedTx), false);
|
||||
|
||||
const Left = () => (
|
||||
<div className="form-group">
|
||||
<h4>{translate('SEND_raw')}</h4>
|
||||
<Code>{JSON.stringify(rawTx, null, 2)}</Code>
|
||||
</div>
|
||||
);
|
||||
const Right = () => (
|
||||
<div className="form-group">
|
||||
<h4> {translate('SEND_signed')} </h4>
|
||||
<Code>{props.signedTx}</Code>
|
||||
</div>
|
||||
);
|
||||
return (
|
||||
<section>
|
||||
<Left />
|
||||
<Right />
|
||||
</section>
|
||||
);
|
||||
};
|
||||
|
||||
export type TTxCompare = typeof TxCompare;
|
65
common/containers/Tabs/Contracts/components/TxModal.tsx
Normal file
65
common/containers/Tabs/Contracts/components/TxModal.tsx
Normal file
@ -0,0 +1,65 @@
|
||||
import React from 'react';
|
||||
import translate from 'translations';
|
||||
import Modal, { IButton } from 'components/ui/Modal';
|
||||
|
||||
export interface Props {
|
||||
networkName: string;
|
||||
network: string;
|
||||
service: string;
|
||||
action: string;
|
||||
handleBroadcastTx(): void;
|
||||
onClose(): void;
|
||||
}
|
||||
|
||||
export type TTxModal = typeof TxModal;
|
||||
|
||||
export const TxModal = (props: Props) => {
|
||||
const {
|
||||
networkName,
|
||||
network,
|
||||
service,
|
||||
handleBroadcastTx,
|
||||
onClose,
|
||||
action
|
||||
} = props;
|
||||
|
||||
const buttons: IButton[] = [
|
||||
{
|
||||
text: translate('SENDModal_Yes', true) as string,
|
||||
type: 'primary',
|
||||
onClick: handleBroadcastTx
|
||||
},
|
||||
{
|
||||
text: translate('SENDModal_No', true) as string,
|
||||
type: 'default',
|
||||
onClick: onClose
|
||||
}
|
||||
];
|
||||
|
||||
return (
|
||||
<Modal
|
||||
title="Confirm Your Transaction"
|
||||
buttons={buttons}
|
||||
handleClose={onClose}
|
||||
isOpen={true}
|
||||
>
|
||||
<div className="modal-body">
|
||||
<h2 className="modal-title text-danger">
|
||||
{translate('SENDModal_Title')}
|
||||
</h2>
|
||||
|
||||
<p>
|
||||
You are about to <strong>{action}</strong> on the{' '}
|
||||
<strong>{networkName}</strong> chain.
|
||||
</p>
|
||||
|
||||
<p>
|
||||
The <strong>{network}</strong> node you are sending through is
|
||||
provided by <strong>{service}</strong>.
|
||||
</p>
|
||||
|
||||
<h4>{translate('SENDModal_Content_3')}</h4>
|
||||
</div>
|
||||
</Modal>
|
||||
);
|
||||
};
|
40
common/containers/Tabs/Contracts/components/withTx.tsx
Normal file
40
common/containers/Tabs/Contracts/components/withTx.tsx
Normal file
@ -0,0 +1,40 @@
|
||||
import * as configSelectors from 'selectors/config';
|
||||
import { AppState } from 'reducers';
|
||||
import { toWei, Wei, getDecimal } from 'libs/units';
|
||||
import { connect } from 'react-redux';
|
||||
import { showNotification, TShowNotification } from 'actions/notifications';
|
||||
import { broadcastTx, TBroadcastTx } from 'actions/wallet';
|
||||
import { IWallet, Balance } from 'libs/wallet';
|
||||
import { RPCNode } from 'libs/nodes';
|
||||
import { NodeConfig, NetworkConfig } from 'config/data';
|
||||
|
||||
export interface IWithTx {
|
||||
wallet: IWallet;
|
||||
balance: Balance;
|
||||
node: NodeConfig;
|
||||
nodeLib: RPCNode;
|
||||
chainId: NetworkConfig['chainId'];
|
||||
networkName: NetworkConfig['name'];
|
||||
gasPrice: Wei;
|
||||
broadcastTx: TBroadcastTx;
|
||||
showNotification: TShowNotification;
|
||||
}
|
||||
|
||||
const mapStateToProps = (state: AppState) => ({
|
||||
wallet: state.wallet.inst,
|
||||
balance: state.wallet.balance,
|
||||
node: configSelectors.getNodeConfig(state),
|
||||
nodeLib: configSelectors.getNodeLib(state),
|
||||
chainId: configSelectors.getNetworkConfig(state).chainId,
|
||||
networkName: configSelectors.getNetworkConfig(state).name,
|
||||
gasPrice: toWei(
|
||||
`${configSelectors.getGasPriceGwei(state)}`,
|
||||
getDecimal('gwei')
|
||||
)
|
||||
});
|
||||
|
||||
export const withTx = passedComponent =>
|
||||
connect(mapStateToProps, {
|
||||
showNotification,
|
||||
broadcastTx
|
||||
})(passedComponent);
|
30
common/containers/Tabs/Contracts/index.scss
Normal file
30
common/containers/Tabs/Contracts/index.scss
Normal file
@ -0,0 +1,30 @@
|
||||
@import 'common/sass/variables';
|
||||
@import 'common/sass/mixins';
|
||||
|
||||
.Contracts {
|
||||
&-header {
|
||||
margin: 0;
|
||||
text-align: center;
|
||||
|
||||
&-tab {
|
||||
@include reset-button;
|
||||
color: $ether-blue;
|
||||
|
||||
&:hover,
|
||||
&:active {
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
||||
&.is-active {
|
||||
&,
|
||||
&:hover,
|
||||
&:active {
|
||||
color: $text-color;
|
||||
cursor: default;
|
||||
opacity: 1;
|
||||
font-weight: 500;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
61
common/containers/Tabs/Contracts/index.tsx
Normal file
61
common/containers/Tabs/Contracts/index.tsx
Normal file
@ -0,0 +1,61 @@
|
||||
import React, { Component } from 'react';
|
||||
import translate from 'translations';
|
||||
import Interact from './components/Interact';
|
||||
import Deploy from './components/Deploy';
|
||||
import './index.scss';
|
||||
import TabSection from 'containers/TabSection';
|
||||
|
||||
interface State {
|
||||
activeTab: string;
|
||||
}
|
||||
|
||||
export default class Contracts extends Component<{}, State> {
|
||||
public state: State = {
|
||||
activeTab: 'interact'
|
||||
};
|
||||
|
||||
public changeTab = activeTab => () => this.setState({ activeTab });
|
||||
|
||||
public render() {
|
||||
const { activeTab } = this.state;
|
||||
let content;
|
||||
let interactActive = '';
|
||||
let deployActive = '';
|
||||
|
||||
if (activeTab === 'interact') {
|
||||
content = <Interact />;
|
||||
interactActive = 'is-active';
|
||||
} else {
|
||||
content = <Deploy />;
|
||||
deployActive = 'is-active';
|
||||
}
|
||||
|
||||
return (
|
||||
<TabSection>
|
||||
<section className="Tab-content Contracts">
|
||||
<div className="Tab-content-pane">
|
||||
<h1 className="Contracts-header">
|
||||
<button
|
||||
className={`Contracts-header-tab ${interactActive}`}
|
||||
onClick={this.changeTab('interact')}
|
||||
>
|
||||
{translate('NAV_InteractContract')}
|
||||
</button>{' '}
|
||||
<span>or</span>{' '}
|
||||
<button
|
||||
className={`Contracts-header-tab ${deployActive}`}
|
||||
onClick={this.changeTab('deploy')}
|
||||
>
|
||||
{translate('NAV_DeployContract')}
|
||||
</button>
|
||||
</h1>
|
||||
</div>
|
||||
|
||||
<main className="Tab-content-pane" role="main">
|
||||
<div className="Contracts-content">{content}</div>
|
||||
</main>
|
||||
</section>
|
||||
</TabSection>
|
||||
);
|
||||
}
|
||||
}
|
@ -1,6 +1,6 @@
|
||||
import { connect } from 'react-redux';
|
||||
import ENS from './components/ENS';
|
||||
|
||||
const mapStateToProps = state => ({});
|
||||
const mapStateToProps = _ => ({});
|
||||
|
||||
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 { getV3Filename, UtcKeystore } from 'libs/keystore';
|
||||
import PrivKeyWallet from 'libs/wallet/privkey';
|
||||
import { IFullWallet, IV3Wallet } from 'ethereumjs-wallet';
|
||||
import { toChecksumAddress } from 'ethereumjs-util';
|
||||
import { NewTabLink } from 'components/ui';
|
||||
import React, { Component } from 'react';
|
||||
import translate from 'translations';
|
||||
import { makeBlob } from 'utils/blob';
|
||||
@ -8,46 +9,35 @@ import './DownloadWallet.scss';
|
||||
import Template from './Template';
|
||||
|
||||
interface Props {
|
||||
wallet: PrivKeyWallet;
|
||||
wallet: IFullWallet;
|
||||
password: string;
|
||||
continueToPaper(): ContinueToPaperAction;
|
||||
}
|
||||
|
||||
interface State {
|
||||
hasDownloadedWallet: boolean;
|
||||
address: string;
|
||||
keystore: UtcKeystore | null;
|
||||
keystore: IV3Wallet | null;
|
||||
}
|
||||
|
||||
export default class DownloadWallet extends Component<Props, State> {
|
||||
public state: State = {
|
||||
hasDownloadedWallet: false,
|
||||
address: '',
|
||||
keystore: null
|
||||
};
|
||||
|
||||
public componentDidMount() {
|
||||
this.props.wallet.getAddress().then(address => {
|
||||
this.setState({ address });
|
||||
});
|
||||
public componentWillMount() {
|
||||
this.setWallet(this.props.wallet, this.props.password);
|
||||
}
|
||||
|
||||
public componentWillMount() {
|
||||
this.props.wallet.toKeystore(this.props.password).then(utcKeystore => {
|
||||
this.setState({ keystore: utcKeystore });
|
||||
});
|
||||
}
|
||||
public componentWillUpdate(nextProps: Props) {
|
||||
if (this.props.wallet !== nextProps.wallet) {
|
||||
nextProps.wallet.toKeystore(nextProps.password).then(utcKeystore => {
|
||||
this.setState({ keystore: utcKeystore });
|
||||
});
|
||||
this.setWallet(nextProps.wallet, nextProps.password);
|
||||
}
|
||||
}
|
||||
|
||||
public render() {
|
||||
const { hasDownloadedWallet } = this.state;
|
||||
const filename = this.getFilename();
|
||||
const filename = this.props.wallet.getV3Filename();
|
||||
|
||||
const content = (
|
||||
<div className="DlWallet">
|
||||
@ -112,22 +102,14 @@ export default class DownloadWallet extends Component<Props, State> {
|
||||
<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"
|
||||
>
|
||||
<NewTabLink href="https://myetherwallet.groovehq.com/knowledge_base/topics/how-do-i-save-slash-backup-my-wallet">
|
||||
<strong>{translate('GEN_Help_13')}</strong>
|
||||
</a>
|
||||
</NewTabLink>
|
||||
</li>
|
||||
<li>
|
||||
<a
|
||||
href="https://myetherwallet.groovehq.com/knowledge_base/topics/what-are-the-different-formats-of-a-private-key"
|
||||
target="_blank"
|
||||
rel="noopener"
|
||||
>
|
||||
<NewTabLink href="https://myetherwallet.groovehq.com/knowledge_base/topics/what-are-the-different-formats-of-a-private-key">
|
||||
<strong>{translate('GEN_Help_14')}</strong>
|
||||
</a>
|
||||
</NewTabLink>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
@ -136,28 +118,23 @@ export default class DownloadWallet extends Component<Props, State> {
|
||||
return <Template content={content} help={help} />;
|
||||
}
|
||||
|
||||
public getBlob() {
|
||||
if (this.state.keystore) {
|
||||
return makeBlob('text/json;charset=UTF-8', this.state.keystore);
|
||||
}
|
||||
public getBlob = () =>
|
||||
(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() {
|
||||
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 => {
|
||||
private handleDownloadKeystore = e =>
|
||||
this.state.keystore ? this.markDownloaded() : e.preventDefault();
|
||||
};
|
||||
}
|
||||
|
@ -1,113 +1,91 @@
|
||||
import PrintableWallet from 'components/PrintableWallet';
|
||||
import PrivKeyWallet from 'libs/wallet/privkey';
|
||||
import React, { Component } from 'react';
|
||||
import { IFullWallet } from 'ethereumjs-wallet';
|
||||
import { NewTabLink } from 'components/ui';
|
||||
import React from 'react';
|
||||
import { Link } from 'react-router-dom';
|
||||
import translate from 'translations';
|
||||
import './PaperWallet.scss';
|
||||
import Template from './Template';
|
||||
|
||||
interface Props {
|
||||
wallet: PrivKeyWallet;
|
||||
}
|
||||
const content = (wallet: IFullWallet) => (
|
||||
<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, {}> {
|
||||
public render() {
|
||||
const { wallet } = this.props;
|
||||
{/* Download Paper Wallet */}
|
||||
<h1 className="GenPaper-title">{translate('x_Print')}</h1>
|
||||
<div className="GenPaper-paper">
|
||||
<PrintableWallet wallet={wallet} />
|
||||
</div>
|
||||
|
||||
const content = (
|
||||
<div className="GenPaper">
|
||||
{/* Private Key */}
|
||||
<h1 className="GenPaper-title">{translate('GEN_Label_5')}</h1>
|
||||
<input
|
||||
className="GenPaper-private form-control"
|
||||
value={wallet.getPrivateKey()}
|
||||
aria-label={translate('x_PrivKey')}
|
||||
aria-describedby="x_PrivKeyDesc"
|
||||
type="text"
|
||||
readOnly={true}
|
||||
/>
|
||||
{/* Warning */}
|
||||
<div className="GenPaper-warning">
|
||||
<p>
|
||||
<strong>Do not lose it!</strong> It cannot be recovered if you lose it.
|
||||
</p>
|
||||
<p>
|
||||
<strong>Do not share it!</strong> Your funds will be stolen if you use
|
||||
this file on a malicious/phishing site.
|
||||
</p>
|
||||
<p>
|
||||
<strong>Make a backup!</strong> Secure it like the millions of dollars
|
||||
it may one day be worth.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Download Paper Wallet */}
|
||||
<h1 className="GenPaper-title">{translate('x_Print')}</h1>
|
||||
<div className="GenPaper-paper">
|
||||
<PrintableWallet wallet={wallet} />
|
||||
</div>
|
||||
{/* Continue button */}
|
||||
<Link className="GenPaper-continue btn btn-default" to="/view-wallet">
|
||||
{translate('NAV_ViewWallet')} →
|
||||
</Link>
|
||||
</div>
|
||||
);
|
||||
|
||||
{/* Warning */}
|
||||
<div className="GenPaper-warning">
|
||||
<p>
|
||||
<strong>Do not lose it!</strong> It cannot be recovered if you lose
|
||||
it.
|
||||
</p>
|
||||
<p>
|
||||
<strong>Do not share it!</strong> Your funds will be stolen if you
|
||||
use this file on a malicious/phishing site.
|
||||
</p>
|
||||
<p>
|
||||
<strong>Make a backup!</strong> Secure it like the millions of
|
||||
dollars it may one day be worth.
|
||||
</p>
|
||||
</div>
|
||||
const help = (
|
||||
<div>
|
||||
<h4>{translate('GEN_Help_4')}</h4>
|
||||
<ul>
|
||||
<li>
|
||||
<NewTabLink href="https://myetherwallet.groovehq.com/knowledge_base/topics/how-do-i-save-slash-backup-my-wallet">
|
||||
<strong>{translate('HELP_2a_Title')}</strong>
|
||||
</NewTabLink>
|
||||
</li>
|
||||
<li>
|
||||
<NewTabLink href="https://myetherwallet.groovehq.com/knowledge_base/topics/protecting-yourself-and-your-funds">
|
||||
<strong>{translate('GEN_Help_15')}</strong>
|
||||
</NewTabLink>
|
||||
</li>
|
||||
<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 */}
|
||||
<Link className="GenPaper-continue btn btn-default" to="/view-wallet">
|
||||
{translate('NAV_ViewWallet')} →
|
||||
</Link>
|
||||
</div>
|
||||
);
|
||||
<h4>{translate('GEN_Help_17')}</h4>
|
||||
<ul>
|
||||
<li>{translate('GEN_Help_18')}</li>
|
||||
<li>{translate('GEN_Help_19')}</li>
|
||||
<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 = (
|
||||
<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('x_PrintDesc')}</h4>
|
||||
</div>
|
||||
);
|
||||
|
||||
<h4>{translate('GEN_Help_17')}</h4>
|
||||
<ul>
|
||||
<li>{translate('GEN_Help_18')}</li>
|
||||
<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>
|
||||
const PaperWallet: React.SFC<{
|
||||
wallet: IFullWallet;
|
||||
}> = ({ wallet }) => <Template content={content(wallet)} help={help} />;
|
||||
|
||||
<h4>{translate('x_PrintDesc')}</h4>
|
||||
</div>
|
||||
);
|
||||
|
||||
return <Template content={content} help={help} />;
|
||||
}
|
||||
}
|
||||
export default PaperWallet;
|
||||
|
@ -6,20 +6,21 @@ import {
|
||||
TGenerateNewWallet,
|
||||
TResetGenerateWallet
|
||||
} from 'actions/generateWallet';
|
||||
import PrivKeyWallet from 'libs/wallet/privkey';
|
||||
import { IFullWallet } from 'ethereumjs-wallet';
|
||||
import React, { Component } from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import { AppState } from 'reducers';
|
||||
import DownloadWallet from './components/DownloadWallet';
|
||||
import EnterPassword from './components/EnterPassword';
|
||||
import PaperWallet from './components/PaperWallet';
|
||||
import CryptoWarning from './components/CryptoWarning';
|
||||
import TabSection from 'containers/TabSection';
|
||||
|
||||
interface Props {
|
||||
// Redux state
|
||||
activeStep: string; // FIXME union actual steps
|
||||
password: string;
|
||||
wallet: PrivKeyWallet | null | undefined;
|
||||
wallet: IFullWallet | null | undefined;
|
||||
walletPasswordForm: any;
|
||||
// Actions
|
||||
generateNewWallet: TGenerateNewWallet;
|
||||
@ -38,38 +39,42 @@ class GenerateWallet extends Component<Props, {}> {
|
||||
|
||||
const AnyEnterPassword = EnterPassword as new () => any;
|
||||
|
||||
switch (activeStep) {
|
||||
case 'password':
|
||||
content = (
|
||||
<AnyEnterPassword
|
||||
walletPasswordForm={this.props.walletPasswordForm}
|
||||
generateNewWallet={this.props.generateNewWallet}
|
||||
/>
|
||||
);
|
||||
break;
|
||||
|
||||
case 'download':
|
||||
if (wallet) {
|
||||
if (window.crypto) {
|
||||
switch (activeStep) {
|
||||
case 'password':
|
||||
content = (
|
||||
<DownloadWallet
|
||||
wallet={wallet}
|
||||
password={password}
|
||||
continueToPaper={this.props.continueToPaper}
|
||||
<AnyEnterPassword
|
||||
walletPasswordForm={this.props.walletPasswordForm}
|
||||
generateNewWallet={this.props.generateNewWallet}
|
||||
/>
|
||||
);
|
||||
}
|
||||
break;
|
||||
break;
|
||||
|
||||
case 'paper':
|
||||
if (wallet) {
|
||||
content = <PaperWallet wallet={wallet} />;
|
||||
} else {
|
||||
case 'download':
|
||||
if (wallet) {
|
||||
content = (
|
||||
<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>;
|
||||
}
|
||||
break;
|
||||
|
||||
default:
|
||||
content = <h1>Uh oh. Not sure how you got here.</h1>;
|
||||
}
|
||||
} else {
|
||||
content = <CryptoWarning />;
|
||||
}
|
||||
|
||||
return (
|
||||
|
@ -1,45 +1,55 @@
|
||||
import React from 'react';
|
||||
import translate, { translateRaw } from 'translations';
|
||||
import UnitDropdown from './UnitDropdown';
|
||||
import { Ether } from 'libs/units';
|
||||
|
||||
import { Balance } from 'libs/wallet';
|
||||
import { UnitConverter } from 'components/renderCbs';
|
||||
interface Props {
|
||||
value: string;
|
||||
decimal: number;
|
||||
unit: string;
|
||||
tokens: string[];
|
||||
balance: number | null | Ether;
|
||||
onChange?(value: string, unit: string): void;
|
||||
balance: number | null | Balance;
|
||||
isReadOnly: boolean;
|
||||
onAmountChange(value: string, unit: string): void;
|
||||
onUnitChange(unit: string): void;
|
||||
}
|
||||
|
||||
export default class AmountField extends React.Component {
|
||||
public props: Props;
|
||||
|
||||
get active() {
|
||||
return !this.props.isReadOnly;
|
||||
}
|
||||
|
||||
public render() {
|
||||
const { value, unit, onChange, balance } = this.props;
|
||||
const isReadonly = !onChange;
|
||||
const { unit, balance, decimal, isReadOnly } = this.props;
|
||||
return (
|
||||
<div className="row form-group">
|
||||
<div className="col-xs-11">
|
||||
<label>{translate('SEND_amount')}</label>
|
||||
<div className="input-group">
|
||||
<input
|
||||
className={`form-control ${isFinite(Number(value)) &&
|
||||
Number(value) > 0
|
||||
? 'is-valid'
|
||||
: 'is-invalid'}`}
|
||||
type="text"
|
||||
placeholder={translateRaw('SEND_amount_short')}
|
||||
value={value}
|
||||
disabled={isReadonly}
|
||||
onChange={isReadonly ? void 0 : this.onValueChange}
|
||||
/>
|
||||
<UnitConverter decimal={decimal} onChange={this.callWithBaseUnit}>
|
||||
{({ onUserInput, convertedUnit }) => (
|
||||
<input
|
||||
className={`form-control ${
|
||||
isFinite(Number(convertedUnit)) && Number(convertedUnit) > 0
|
||||
? 'is-valid'
|
||||
: 'is-invalid'
|
||||
}`}
|
||||
type="text"
|
||||
placeholder={translateRaw('SEND_amount_short')}
|
||||
value={convertedUnit}
|
||||
disabled={isReadOnly}
|
||||
onChange={onUserInput}
|
||||
/>
|
||||
)}
|
||||
</UnitConverter>
|
||||
<UnitDropdown
|
||||
value={unit}
|
||||
options={['ether'].concat(this.props.tokens)}
|
||||
onChange={isReadonly ? void 0 : this.onUnitChange}
|
||||
onChange={isReadOnly ? void 0 : this.onUnitChange}
|
||||
/>
|
||||
</div>
|
||||
{!isReadonly &&
|
||||
{!isReadOnly &&
|
||||
balance && (
|
||||
<span className="help-block">
|
||||
<a onClick={this.onSendEverything}>
|
||||
@ -54,24 +64,12 @@ export default class AmountField extends React.Component {
|
||||
);
|
||||
}
|
||||
|
||||
public onUnitChange = (unit: string) => {
|
||||
if (this.props.onChange) {
|
||||
this.props.onChange(this.props.value, unit);
|
||||
}
|
||||
};
|
||||
public onUnitChange = (unit: string) =>
|
||||
this.active && this.props.onUnitChange(unit); // thsi needs to be converted unit
|
||||
|
||||
public onValueChange = (e: React.SyntheticEvent<HTMLInputElement>) => {
|
||||
if (this.props.onChange) {
|
||||
this.props.onChange(
|
||||
(e.target as HTMLInputElement).value,
|
||||
this.props.unit
|
||||
);
|
||||
}
|
||||
};
|
||||
public callWithBaseUnit = ({ currentTarget: { value } }) =>
|
||||
this.active && this.props.onAmountChange(value, this.props.unit);
|
||||
|
||||
public onSendEverything = () => {
|
||||
if (this.props.onChange) {
|
||||
this.props.onChange('everything', this.props.unit);
|
||||
}
|
||||
};
|
||||
public onSendEverything = () =>
|
||||
this.active && this.props.onAmountChange('everything', this.props.unit);
|
||||
}
|
||||
|
@ -1,8 +1,15 @@
|
||||
@import "common/sass/variables";
|
||||
|
||||
$summary-height: 54px;
|
||||
$button-break: 'max-width: 620px';
|
||||
|
||||
.ConfModal {
|
||||
min-width: 580px;
|
||||
|
||||
@media (#{$button-break}) {
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
&-summary {
|
||||
display: flex;
|
||||
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 Modal, { IButton } from 'components/ui/Modal';
|
||||
import Spinner from 'components/ui/Spinner';
|
||||
import { NetworkConfig, NodeConfig } from 'config/data';
|
||||
import EthTx from 'ethereumjs-tx';
|
||||
import ERC20 from 'libs/erc20';
|
||||
import {
|
||||
BroadcastTransactionStatus,
|
||||
getTransactionFields
|
||||
getTransactionFields,
|
||||
decodeTransaction
|
||||
} from 'libs/transaction';
|
||||
import { toTokenDisplay, toUnit } from 'libs/units';
|
||||
import { IWallet } from 'libs/wallet/IWallet';
|
||||
import React from 'react';
|
||||
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 translate, { translateRaw } from 'translations';
|
||||
import { UnitDisplay } from 'components/ui';
|
||||
import './ConfirmationModal.scss';
|
||||
|
||||
interface Props {
|
||||
signedTx: string;
|
||||
transaction: EthTx;
|
||||
wallet: IWallet;
|
||||
node: NodeConfig;
|
||||
token: MergedToken | undefined;
|
||||
token: MergedToken;
|
||||
network: NetworkConfig;
|
||||
lang: string;
|
||||
broadCastTxStatus: BroadcastTransactionStatus;
|
||||
decimal: number;
|
||||
onConfirm(signedTx: string): void;
|
||||
onClose(): void;
|
||||
}
|
||||
|
||||
interface State {
|
||||
fromAddress: string;
|
||||
timeToRead: number;
|
||||
hasBroadCasted: boolean;
|
||||
}
|
||||
|
||||
class ConfirmationModal extends React.Component<Props, State> {
|
||||
public state = {
|
||||
fromAddress: '',
|
||||
timeToRead: 5,
|
||||
hasBroadCasted: false
|
||||
};
|
||||
|
||||
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() {
|
||||
if (
|
||||
this.state.hasBroadCasted &&
|
||||
this.props.broadCastTxStatus &&
|
||||
!this.props.broadCastTxStatus.isBroadcasting
|
||||
) {
|
||||
this.props.onClose();
|
||||
@ -71,20 +65,23 @@ class ConfirmationModal extends React.Component<Props, State> {
|
||||
window.clearInterval(this.readTimer);
|
||||
}
|
||||
}, 1000);
|
||||
|
||||
this.setWalletAddress(this.props.wallet);
|
||||
}
|
||||
|
||||
public render() {
|
||||
const { node, token, network, onClose, broadCastTxStatus } = this.props;
|
||||
const { fromAddress, timeToRead } = this.state;
|
||||
const {
|
||||
toAddress,
|
||||
value,
|
||||
gasPrice,
|
||||
data,
|
||||
nonce
|
||||
} = this.decodeTransaction();
|
||||
node,
|
||||
token,
|
||||
network,
|
||||
onClose,
|
||||
broadCastTxStatus,
|
||||
transaction,
|
||||
decimal
|
||||
} = this.props;
|
||||
const { timeToRead } = this.state;
|
||||
const { toAddress, value, gasPrice, data, from, nonce } = decodeTransaction(
|
||||
transaction,
|
||||
token
|
||||
);
|
||||
|
||||
const buttonPrefix = timeToRead > 0 ? `(${timeToRead}) ` : '';
|
||||
const buttons: IButton[] = [
|
||||
@ -107,39 +104,42 @@ class ConfirmationModal extends React.Component<Props, State> {
|
||||
broadCastTxStatus && broadCastTxStatus.isBroadcasting;
|
||||
|
||||
return (
|
||||
<Modal
|
||||
title="Confirm Your Transaction"
|
||||
buttons={buttons}
|
||||
handleClose={onClose}
|
||||
disableButtons={isBroadcasting}
|
||||
isOpen={true}
|
||||
>
|
||||
{
|
||||
<div className="ConfModalWrap">
|
||||
<Modal
|
||||
title="Confirm Your Transaction"
|
||||
buttons={buttons}
|
||||
handleClose={onClose}
|
||||
disableButtons={isBroadcasting}
|
||||
isOpen={true}
|
||||
>
|
||||
<div className="ConfModal">
|
||||
{isBroadcasting ? (
|
||||
<div className="ConfModal-loading">
|
||||
<Spinner size="5x" />
|
||||
<Spinner size="x5" />
|
||||
</div>
|
||||
) : (
|
||||
<div>
|
||||
<div className="ConfModal-summary">
|
||||
<div className="ConfModal-summary-icon ConfModal-summary-icon--from">
|
||||
<Identicon size="100%" address={fromAddress} />
|
||||
<Identicon size="100%" address={from} />
|
||||
</div>
|
||||
<div className="ConfModal-summary-amount">
|
||||
<div className="ConfModal-summary-amount-arrow" />
|
||||
<div className="ConfModal-summary-amount-currency">
|
||||
{value} {symbol}
|
||||
<UnitDisplay
|
||||
decimal={decimal}
|
||||
value={value}
|
||||
symbol={symbol}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="ConfModal-summary-icon ConfModal-summary-icon--to">
|
||||
<Identicon size="100%" address={toAddress} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<ul className="ConfModal-details">
|
||||
<li className="ConfModal-details-detail">
|
||||
You are sending from <code>{fromAddress}</code>
|
||||
You are sending from <code>{from}</code>
|
||||
</li>
|
||||
<li className="ConfModal-details-detail">
|
||||
You are sending to <code>{toAddress}</code>
|
||||
@ -150,9 +150,20 @@ class ConfirmationModal extends React.Component<Props, State> {
|
||||
<li className="ConfModal-details-detail">
|
||||
You are sending{' '}
|
||||
<strong>
|
||||
{value} {symbol}
|
||||
<UnitDisplay
|
||||
decimal={decimal}
|
||||
value={value}
|
||||
symbol={symbol}
|
||||
/>
|
||||
</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 className="ConfModal-details-detail">
|
||||
You are interacting with the <strong>{node.network}</strong>{' '}
|
||||
@ -183,8 +194,8 @@ class ConfirmationModal extends React.Component<Props, State> {
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
}
|
||||
</Modal>
|
||||
</Modal>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@ -192,38 +203,6 @@ class ConfirmationModal extends React.Component<Props, State> {
|
||||
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 = () => {
|
||||
if (this.state.timeToRead < 1) {
|
||||
this.props.onConfirm(this.props.signedTx);
|
||||
@ -239,6 +218,8 @@ function mapStateToProps(state, props) {
|
||||
// Network config for defaults
|
||||
const network = getNetworkConfig(state);
|
||||
|
||||
const node = getNodeConfig(state);
|
||||
|
||||
const lang = getLanguageSelection(state);
|
||||
|
||||
const broadCastTxStatus = getTxFromState(state, props.signedTx);
|
||||
@ -249,6 +230,7 @@ function mapStateToProps(state, props) {
|
||||
const token = data && tokens.find(t => t.address === to);
|
||||
|
||||
return {
|
||||
node,
|
||||
broadCastTxStatus,
|
||||
transaction,
|
||||
token,
|
||||
|
@ -1,4 +1,3 @@
|
||||
import Big from 'bignumber.js';
|
||||
// COMPONENTS
|
||||
import Spinner from 'components/ui/Spinner';
|
||||
import TabSection from 'containers/TabSection';
|
||||
@ -13,9 +12,10 @@ import {
|
||||
DataField,
|
||||
GasField
|
||||
} from './components';
|
||||
import TransactionSucceeded from 'components/ExtendedNotifications/TransactionSucceeded';
|
||||
import NavigationPrompt from './components/NavigationPrompt';
|
||||
// CONFIG
|
||||
import { donationAddressMap, NetworkConfig, NodeConfig } from 'config/data';
|
||||
import { donationAddressMap, NetworkConfig } from 'config/data';
|
||||
// LIBS
|
||||
import { stripHexPrefix } from 'libs/values';
|
||||
import { TransactionWithoutGas } from 'libs/messages';
|
||||
@ -23,14 +23,16 @@ import { RPCNode } from 'libs/nodes';
|
||||
import {
|
||||
BroadcastTransactionStatus,
|
||||
CompleteTransaction,
|
||||
confirmAndSendWeb3Transaction,
|
||||
formatTxInput,
|
||||
generateCompleteTransaction,
|
||||
getBalanceMinusGasCosts,
|
||||
TransactionInput
|
||||
} 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 { IWallet } from 'libs/wallet/IWallet';
|
||||
// LIBS
|
||||
import { IWallet, Balance, Web3Wallet } from 'libs/wallet';
|
||||
import pickBy from 'lodash/pickBy';
|
||||
import React from 'react';
|
||||
// REDUX
|
||||
@ -51,7 +53,6 @@ import {
|
||||
import {
|
||||
getGasPriceGwei,
|
||||
getNetworkConfig,
|
||||
getNodeConfig,
|
||||
getNodeLib
|
||||
} from 'selectors/config';
|
||||
import {
|
||||
@ -86,12 +87,12 @@ interface State {
|
||||
nonce: number | null | undefined;
|
||||
hasSetDefaultNonce: boolean;
|
||||
generateTxProcessing: boolean;
|
||||
walletAddress: string | null;
|
||||
}
|
||||
|
||||
interface Props {
|
||||
wallet: IWallet;
|
||||
balance: Ether;
|
||||
node: NodeConfig;
|
||||
balance: Balance;
|
||||
nodeLib: RPCNode;
|
||||
network: NetworkConfig;
|
||||
tokens: MergedToken[];
|
||||
@ -122,7 +123,8 @@ const initialState: State = {
|
||||
generateDisabled: true,
|
||||
nonce: null,
|
||||
hasSetDefaultNonce: false,
|
||||
generateTxProcessing: false
|
||||
generateTxProcessing: false,
|
||||
walletAddress: null
|
||||
};
|
||||
|
||||
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 debounce the call
|
||||
// handle gas estimation
|
||||
// if any relevant fields changed
|
||||
return (
|
||||
// if any relevant fields changed
|
||||
this.haveFieldsChanged(prevState) &&
|
||||
// if gas has not changed
|
||||
!this.state.gasChanged &&
|
||||
@ -201,7 +203,7 @@ export class SendTransaction extends React.Component<Props, State> {
|
||||
const { hasSetDefaultNonce, nonce } = this.state;
|
||||
const unlocked = !!wallet;
|
||||
if (unlocked) {
|
||||
const from = await wallet.getAddress();
|
||||
const from = await wallet.getAddressString();
|
||||
if (forceOffline && !offline && !hasSetDefaultNonce) {
|
||||
const nonceHex = await nodeLib.getTransactionCount(from);
|
||||
const newNonce = parseInt(stripHexPrefix(nonceHex), 10);
|
||||
@ -215,17 +217,27 @@ export class SendTransaction extends React.Component<Props, State> {
|
||||
}
|
||||
|
||||
public handleWalletStateOnUpdate(prevProps) {
|
||||
if (this.props.wallet !== prevProps.wallet) {
|
||||
if (this.props.wallet !== prevProps.wallet && !!prevProps.wallet) {
|
||||
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) {
|
||||
this.handleGasEstimationOnUpdate(prevState);
|
||||
this.handleGenerateDisabledOnUpdate();
|
||||
this.handleBroadcastTransactionOnUpdate();
|
||||
this.handleSetNonceWhenOfflineOnUpdate();
|
||||
this.handleWalletStateOnUpdate(prevProps);
|
||||
this.setWalletAddressOnUpdate();
|
||||
}
|
||||
|
||||
public onNonceChange = (value: number) => {
|
||||
@ -236,7 +248,6 @@ export class SendTransaction extends React.Component<Props, State> {
|
||||
const unlocked = !!this.props.wallet;
|
||||
const {
|
||||
to,
|
||||
value,
|
||||
unit,
|
||||
gasLimit,
|
||||
data,
|
||||
@ -249,6 +260,11 @@ export class SendTransaction extends React.Component<Props, State> {
|
||||
} = this.state;
|
||||
const { offline, forceOffline, balance } = this.props;
|
||||
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 (
|
||||
<TabSection>
|
||||
<section className="Tab-content">
|
||||
@ -268,35 +284,38 @@ export class SendTransaction extends React.Component<Props, State> {
|
||||
/>
|
||||
<div className="row">
|
||||
{/* Send Form */}
|
||||
{unlocked && (
|
||||
<main className="col-sm-8">
|
||||
<div className="Tab-content-pane">
|
||||
{hasQueryString && (
|
||||
<div className="alert alert-info">
|
||||
<p>{translate('WARN_Send_Link')}</p>
|
||||
</div>
|
||||
)}
|
||||
{unlocked &&
|
||||
!(offline || (forceOffline && isWeb3Wallet)) && (
|
||||
<main className="col-sm-8">
|
||||
<div className="Tab-content-pane">
|
||||
{hasQueryString && (
|
||||
<div className="alert alert-info">
|
||||
<p>{translate('WARN_Send_Link')}</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<AddressField
|
||||
placeholder={donationAddressMap.ETH}
|
||||
value={this.state.to}
|
||||
onChange={readOnly ? null : this.onAddressChange}
|
||||
/>
|
||||
<AmountField
|
||||
value={value}
|
||||
unit={unit}
|
||||
balance={balance}
|
||||
tokens={this.props.tokenBalances
|
||||
.filter(token => !token.balance.eq(0))
|
||||
.map(token => token.symbol)
|
||||
.sort()}
|
||||
onChange={readOnly ? void 0 : this.onAmountChange}
|
||||
/>
|
||||
<GasField
|
||||
value={gasLimit}
|
||||
onChange={readOnly ? void 0 : this.onGasChange}
|
||||
/>
|
||||
{(offline || forceOffline) && (
|
||||
<AddressField
|
||||
placeholder={donationAddressMap.ETH}
|
||||
value={this.state.to}
|
||||
onChange={readOnly ? null : this.onAddressChange}
|
||||
/>
|
||||
<AmountField
|
||||
unit={unit}
|
||||
decimal={decimal}
|
||||
balance={balance}
|
||||
tokens={this.props.tokenBalances
|
||||
.filter(token => !token.balance.eqn(0))
|
||||
.map(token => token.symbol)
|
||||
.sort()}
|
||||
onAmountChange={this.onAmountChange}
|
||||
isReadOnly={readOnly}
|
||||
onUnitChange={this.onUnitChange}
|
||||
/>
|
||||
<GasField
|
||||
value={gasLimit}
|
||||
onChange={readOnly ? void 0 : this.onGasChange}
|
||||
/>
|
||||
{(offline || forceOffline) && (
|
||||
<div>
|
||||
<NonceField
|
||||
value={nonce}
|
||||
@ -305,88 +324,108 @@ export class SendTransaction extends React.Component<Props, State> {
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
{unit === 'ether' && (
|
||||
<DataField
|
||||
value={data}
|
||||
onChange={readOnly ? void 0 : this.onDataChange}
|
||||
/>
|
||||
)}
|
||||
<CustomMessage message={customMessage} />
|
||||
{unit === 'ether' && (
|
||||
<DataField
|
||||
value={data}
|
||||
onChange={readOnly ? void 0 : this.onDataChange}
|
||||
/>
|
||||
)}
|
||||
<CustomMessage message={customMessage} />
|
||||
|
||||
<div className="row form-group">
|
||||
<div className="col-xs-12 clearfix">
|
||||
<button
|
||||
disabled={this.state.generateDisabled}
|
||||
className="btn btn-info btn-block"
|
||||
onClick={this.generateTxFromState}
|
||||
>
|
||||
{translate('SEND_generate')}
|
||||
</button>
|
||||
<div className="row form-group">
|
||||
<div className="col-xs-12 clearfix">
|
||||
<button
|
||||
disabled={this.state.generateDisabled}
|
||||
className="btn btn-info btn-block"
|
||||
onClick={
|
||||
isWeb3Wallet
|
||||
? this.generateWeb3TxFromState
|
||||
: this.generateTxFromState
|
||||
}
|
||||
>
|
||||
{isWeb3Wallet
|
||||
? translate('Send to MetaMask / Mist')
|
||||
: translate('SEND_generate')}
|
||||
</button>
|
||||
</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>
|
||||
</main>
|
||||
)}
|
||||
|
||||
{generateTxProcessing && (
|
||||
<div className="container">
|
||||
<div className="row form-group text-center">
|
||||
<Spinner size="5x" />
|
||||
</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="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>
|
||||
)}
|
||||
{unlocked &&
|
||||
(offline || (forceOffline && isWeb3Wallet)) && (
|
||||
<main className="col-sm-8">
|
||||
<div className="Tab-content-pane">
|
||||
<h4>Sorry...</h4>
|
||||
<p>
|
||||
MetaMask / Mist wallets are not available in offline mode.
|
||||
</p>
|
||||
</div>
|
||||
</main>
|
||||
)}
|
||||
|
||||
{/* Sidebar */}
|
||||
{unlocked && (
|
||||
@ -398,8 +437,8 @@ export class SendTransaction extends React.Component<Props, State> {
|
||||
{transaction &&
|
||||
showTxConfirm && (
|
||||
<ConfirmationModal
|
||||
wallet={this.props.wallet}
|
||||
node={this.props.node}
|
||||
decimal={decimal}
|
||||
fromAddress={this.state.walletAddress}
|
||||
signedTx={transaction.signedTx}
|
||||
onClose={this.hideConfirmTx}
|
||||
onConfirm={this.confirmTx}
|
||||
@ -415,15 +454,15 @@ export class SendTransaction extends React.Component<Props, State> {
|
||||
const query = queryString.parse(searchStr);
|
||||
const to = getParam(query, 'to');
|
||||
const data = getParam(query, 'data');
|
||||
// FIXME validate token against presets
|
||||
const unit = getParam(query, 'tokenSymbol');
|
||||
const token = this.props.tokens.find(x => x.symbol === unit);
|
||||
const value = getParam(query, 'value');
|
||||
let gasLimit = getParam(query, 'gas');
|
||||
let gasLimit = getParam(query, 'gaslimit');
|
||||
if (gasLimit === null) {
|
||||
gasLimit = getParam(query, 'limit');
|
||||
}
|
||||
const readOnly = getParam(query, 'readOnly') != null;
|
||||
return { to, data, value, unit, gasLimit, readOnly };
|
||||
return { to, token, data, value, unit, gasLimit, readOnly };
|
||||
}
|
||||
|
||||
public isValidNonce() {
|
||||
@ -488,21 +527,23 @@ export class SendTransaction extends React.Component<Props, State> {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const cachedFormattedTx = await this.getFormattedTxFromState();
|
||||
// Grab a reference to state. If it has changed by the time the estimateGas
|
||||
// call comes back, we don't want to replace the gasLimit in state.
|
||||
const state = this.state;
|
||||
gasLimit = await nodeLib.estimateGas(cachedFormattedTx);
|
||||
if (this.state === state) {
|
||||
this.setState({ gasLimit: formatGasLimit(gasLimit, state.unit) });
|
||||
} else {
|
||||
// state has changed, so try again from the start (with the hope that state won't change by the next time)
|
||||
this.estimateGas();
|
||||
if (this.props.wallet) {
|
||||
try {
|
||||
const cachedFormattedTx = await this.getFormattedTxFromState();
|
||||
// Grab a reference to state. If it has changed by the time the estimateGas
|
||||
// call comes back, we don't want to replace the gasLimit in state.
|
||||
const state = this.state;
|
||||
gasLimit = await nodeLib.estimateGas(cachedFormattedTx);
|
||||
if (this.state === state) {
|
||||
this.setState({ gasLimit: formatGasLimit(gasLimit, state.unit) });
|
||||
} else {
|
||||
// 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') {
|
||||
const { balance, gasPrice } = this.props;
|
||||
const { gasLimit } = this.state;
|
||||
const weiBalance = balance.toWei();
|
||||
const bigGasLimit = new Big(gasLimit);
|
||||
const bigGasLimit = Wei(gasLimit);
|
||||
value = getBalanceMinusGasCosts(
|
||||
bigGasLimit,
|
||||
gasPrice,
|
||||
weiBalance
|
||||
balance.wei
|
||||
).toString();
|
||||
} else {
|
||||
const tokenBalance = this.props.tokenBalances.find(
|
||||
@ -552,23 +592,29 @@ export class SendTransaction extends React.Component<Props, State> {
|
||||
if (value === 'everything') {
|
||||
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({
|
||||
value,
|
||||
unit,
|
||||
token,
|
||||
transaction,
|
||||
generateDisabled
|
||||
unit
|
||||
});
|
||||
};
|
||||
|
||||
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> =>
|
||||
new Promise(resolve =>
|
||||
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 () => {
|
||||
this.setState({ generateTxProcessing: true });
|
||||
await this.resetJustTx();
|
||||
@ -592,7 +683,7 @@ export class SendTransaction extends React.Component<Props, State> {
|
||||
to,
|
||||
data
|
||||
};
|
||||
const bigGasLimit = new Big(gasLimit);
|
||||
const bigGasLimit = Wei(gasLimit);
|
||||
try {
|
||||
const signedTx = await generateCompleteTransaction(
|
||||
wallet,
|
||||
@ -601,6 +692,7 @@ export class SendTransaction extends React.Component<Props, State> {
|
||||
bigGasLimit,
|
||||
chainId,
|
||||
transactionInput,
|
||||
false,
|
||||
nonce,
|
||||
offline
|
||||
);
|
||||
@ -637,11 +729,10 @@ function mapStateToProps(state: AppState) {
|
||||
wallet: state.wallet.inst,
|
||||
balance: state.wallet.balance,
|
||||
tokenBalances: getTokenBalances(state),
|
||||
node: getNodeConfig(state),
|
||||
nodeLib: getNodeLib(state),
|
||||
network: getNetworkConfig(state),
|
||||
tokens: getTokens(state),
|
||||
gasPrice: new GWei(getGasPriceGwei(state)).toWei(),
|
||||
gasPrice: toWei(`${getGasPriceGwei(state)}`, getDecimal('gwei')),
|
||||
transactions: state.wallet.transactions,
|
||||
offline: state.config.offline,
|
||||
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);
|
30
common/containers/Tabs/SignAndVerifyMessage/index.scss
Normal file
30
common/containers/Tabs/SignAndVerifyMessage/index.scss
Normal file
@ -0,0 +1,30 @@
|
||||
@import 'common/sass/variables';
|
||||
@import 'common/sass/mixins';
|
||||
|
||||
.SignAndVerifyMsg {
|
||||
&-header {
|
||||
margin: 0;
|
||||
text-align: center;
|
||||
|
||||
&-tab {
|
||||
@include reset-button;
|
||||
color: $ether-blue;
|
||||
|
||||
&:hover,
|
||||
&:active {
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
||||
&.is-active {
|
||||
&,
|
||||
&:hover,
|
||||
&:active {
|
||||
color: $text-color;
|
||||
cursor: default;
|
||||
opacity: 1;
|
||||
font-weight: 500;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
59
common/containers/Tabs/SignAndVerifyMessage/index.tsx
Normal file
59
common/containers/Tabs/SignAndVerifyMessage/index.tsx
Normal file
@ -0,0 +1,59 @@
|
||||
import React, { Component } from 'react';
|
||||
import translate from 'translations';
|
||||
import SignMessage from './components/SignMessage';
|
||||
import VerifyMessage from './components/VerifyMessage';
|
||||
import TabSection from 'containers/TabSection';
|
||||
import './index.scss';
|
||||
|
||||
interface State {
|
||||
activeTab: string;
|
||||
}
|
||||
|
||||
export default class SignAndVerifyMessage extends Component<{}, State> {
|
||||
public state: State = {
|
||||
activeTab: 'sign'
|
||||
};
|
||||
|
||||
public changeTab = activeTab => () => this.setState({ activeTab });
|
||||
|
||||
public render() {
|
||||
const { activeTab } = this.state;
|
||||
let content;
|
||||
let signActive = '';
|
||||
let verifyActive = '';
|
||||
|
||||
if (activeTab === 'sign') {
|
||||
content = <SignMessage />;
|
||||
signActive = 'is-active';
|
||||
} else {
|
||||
content = <VerifyMessage />;
|
||||
verifyActive = 'is-active';
|
||||
}
|
||||
|
||||
return (
|
||||
<TabSection>
|
||||
<section className="Tab-content SignAndVerifyMsg">
|
||||
<div className="Tab-content-pane">
|
||||
<h1 className="SignAndVerifyMsg-header">
|
||||
<button
|
||||
className={`SignAndVerifyMsg-header-tab ${signActive}`}
|
||||
onClick={this.changeTab('sign')}
|
||||
>
|
||||
{translate('Sign Message')}
|
||||
</button>{' '}
|
||||
<span>or</span>{' '}
|
||||
<button
|
||||
className={`SignAndVerifyMsg-header-tab ${verifyActive}`}
|
||||
onClick={this.changeTab('verify')}
|
||||
>
|
||||
{translate('Verify Message')}
|
||||
</button>
|
||||
</h1>
|
||||
</div>
|
||||
|
||||
<main role="main">{content}</main>
|
||||
</section>
|
||||
</TabSection>
|
||||
);
|
||||
}
|
||||
}
|
@ -1,4 +1,4 @@
|
||||
@import "common/sass/variables";
|
||||
@import 'common/sass/variables';
|
||||
|
||||
.CurrencySwap {
|
||||
text-align: center;
|
||||
@ -13,6 +13,30 @@
|
||||
display: inline-block;
|
||||
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 {
|
||||
width: 100%;
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user