Merge pull request #430 from MyEtherWallet/develop

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

View File

@ -1,15 +0,0 @@
[ignore]
[include]
[libs]
[options]
module.file_ext=.js
module.file_ext=.json
module.file_ext=.jsx
module.file_ext=.scss
module.file_ext=.less
module.system.node.resolve_dirname=node_modules
module.system.node.resolve_dirname=common
module.name_mapper='.*\.(css|less)$' -> 'empty/object'

1
.gitignore vendored
View File

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

1
.npmrc Normal file
View File

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

View File

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

215
README.md
View File

@ -1,5 +1,7 @@
# MyEtherWallet V4+ (ALPHA - VISIT [V3](https://github.com/kvhnuke/etherwallet) for the production site)
[![Greenkeeper badge](https://badges.greenkeeper.io/MyEtherWallet/MyEtherWallet.svg)](https://greenkeeper.io/)
#### Run:
```bash
@ -36,7 +38,7 @@ npm run dev:https
2. [dternyak/eth-priv-to-addr](https://hub.docker.com/r/dternyak/eth-priv-to-addr/) pulled from DockerHub
##### Docker setup instructions:
1. Install docker (on macOS, I suggest [Docker for Mac](https://docs.docker.com/docker-for-mac/))
1. Install docker (on macOS, [Docker for Mac](https://docs.docker.com/docker-for-mac/) is suggested)
2. `docker pull dternyak/eth-priv-to-addr`
##### Run Derivation Checker
@ -48,40 +50,32 @@ npm run derivation-checker
```
├── common - Your App
├── common
│ ├── actions - application actions
│ ├── api - Services and XHR utils(also custom form validation, see InputComponent from components/common)
│ ├── api - Services and XHR utils
│ ├── components - components according to "Redux philosophy"
│ ├── config - frontend config depending on REACT_WEBPACK_ENV
│ ├── containers - containers according to "Redux philosophy"
│ ├── reducers - application reducers
│ ├── routing - application routing
│ ├── index.jsx - entry
│ ├── index.tsx - entry
│ ├── index.html
├── static
├── webpack_config - Webpack configuration
├── jest_config - Jest configuration
```
## Docker setup
You should already have docker and docker-compose setup for your platform as a pre-req.
```bash
docker-compose up
```
## Style Guides and Philosophies
The following are guides for developers to follow for writing compliant code.
### Redux and Actions
Each reducer has one file in `reducers/[namespace].js` that contains the reducer
and initial state, one file in `actions/[namespace].js` that contains the action
Each reducer has one file in `reducers/[namespace].ts` that contains the reducer
and initial state, one file in `actions/[namespace].ts` that contains the action
creators and their return types, and optionally one file in
`sagas/[namespace].js` that handles action side effects using
`sagas/[namespace].ts` that handles action side effects using
[`redux-saga`](https://github.com/redux-saga/redux-saga).
The files should be laid out as follows:
@ -89,75 +83,192 @@ The files should be laid out as follows:
#### Reducer
* State should be explicitly defined and exported
* Initial state should match state flow typing, define every key
* Reducer function should handle all cases for actions. If state does not change
as a result of an action (Because it merely kicks off side-effects in saga) then
define the case above default, and have it fall through.
* Initial state should match state typing, define every key
```js
// @flow
import type { NamespaceAction } from "actions/namespace";
```ts
import { NamespaceAction } from "actions/[namespace]";
import { TypeKeys } from 'actions/[namespace]/constants';
export type State = { /* Flowtype definition for state object */ };
export interface State { /* definition for state object */ };
export const INITIAL_STATE: State = { /* Initial state shape */ };
export function namespace(
export function [namespace](
state: State = INITIAL_STATE,
action: NamespaceAction
): State {
switch (action.type) {
case 'NAMESPACE_NAME_OF_ACTION':
case TypeKeys.NAMESPACE_NAME_OF_ACTION:
return {
...state,
// Alterations to state
};
case 'NAMESPACE_NAME_OF_SAGA_ACTION':
};
default:
// Ensures every action was handled in reducer
// Unhandled actions should just fall into default
(action: empty);
return state;
}
}
```
#### Actions
* Define each action creator in `actionCreator.ts`
* Define each action object type in `actionTypes.ts`
* Export a union of all of the action types for use by the reducer
* Define each action type as a string enum in `constants.ts`
* Export `actionCreators` and `actionTypes` from module file `index.ts`
* Define each action object type beside the action creator
* Export a union of all of the action types for use by the reducer
```js
```
├── common
├── actions - application actions
├── [namespace] - action namespace
├── actionCreators.ts - action creators
├── actionTypes.ts - action interfaces / types
├── constants.ts - string enum
├── index.ts - exports all action creators and action object types
```
##### constants.ts
```ts
export enum TypeKeys {
NAMESPACE_NAME_OF_ACTION = 'NAMESPACE_NAME_OF_ACTION'
}
```
##### actionTypes.ts
```ts
/*** Name of action ***/
export type NameOfActionAction = {
type: 'NAMESPACE_NAME_OF_ACTION',
export interface NameOfActionAction {
type: TypeKeys.NAMESPACE_NAME_OF_ACTION,
/* Rest of the action object shape */
};
export function nameOfAction(): NameOfActionAction {
return {
type: 'NAMESPACE_NAME_OF_ACTION',
/* Rest of the action object */
};
};
/*** Action Union ***/
export type NamespaceAction =
| ActionOneAction
| ActionTwoAction
| ActionThreeAction;
```
##### actionCreators.ts
```ts
import * as interfaces from './actionTypes';
import { TypeKeys } from './constants';
#### Action Constants
export interface TNameOfAction = typeof nameOfAction;
export function nameOfAction(): interfaces.NameOfActionAction {
return {
type: TypeKeys.NAMESPACE_NAME_OF_ACTION,
payload: {}
};
};
```
##### index.ts
```ts
export * from './actionCreators';
export * from './actionTypes';
```
Action constants are not used thanks to flow type checking. To avoid typos, we
use `(action: empty)` in the default case which assures every case is accounted
for. If you need to use another reducer's action, import that action type into
your reducer, and create a new action union of your actions, and the other
action types used.
### Typing Redux-Connected Components
Components that receive props directly from redux as a result of the `connect`
function should use AppState for typing, rather than manually defining types.
This makes refactoring reducers easier by catching mismatches or changes of
types in components, and reduces the chance for inconsistency. It's also less
code overall.
```
// Do this
import { AppState } from 'reducers';
interface Props {
wallet: AppState['wallet']['inst'];
rates: AppState['rates']['rates'];
// ...
}
// Not this
import { IWallet } from 'libs/wallet';
import { Rates } from 'libs/rates';
interface Props {
wallet: IWallet;
rates: Rates;
// ...
}
```
However, if you have a sub-component that takes in props from a connected
component, it's OK to manually specify the type. Especially if you go from
being type-or-null to guaranteeing the prop will be passed (because of a
conditional render.)
### Higher Order Components
#### Typing Injected Props
Props made available through higher order components can be tricky to type. Normally, if a component requires a prop, you add it to the component's interface and it just works. However, working with injected props from [higher order components](https://medium.com/@DanHomola/react-higher-order-components-in-typescript-made-simple-6f9b55691af1), you will be forced to supply all required props whenever you compose the component.
```ts
interface MyComponentProps {
name: string;
countryCode?: string;
routerLocation: { pathname: string };
}
...
class OtherComponent extends React.Component<{}, {}> {
render() {
return (
<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
View File

@ -0,0 +1,87 @@
import React, { Component } from 'react';
import { Provider } from 'react-redux';
import { withRouter, Switch, Redirect, Router, Route } from 'react-router-dom';
// Components
import Contracts from 'containers/Tabs/Contracts';
import ENS from 'containers/Tabs/ENS';
import GenerateWallet from 'containers/Tabs/GenerateWallet';
import Help from 'containers/Tabs/Help';
import SendTransaction from 'containers/Tabs/SendTransaction';
import Swap from 'containers/Tabs/Swap';
import ViewWallet from 'containers/Tabs/ViewWallet';
import SignAndVerifyMessage from 'containers/Tabs/SignAndVerifyMessage';
import BroadcastTx from 'containers/Tabs/BroadcastTx';
// TODO: fix this
interface Props {
store: any;
history: any;
}
export default class Root extends Component<Props, {}> {
public render() {
const { store, history } = this.props;
// key={Math.random()} = hack for HMR from https://github.com/webpack/webpack-dev-server/issues/395
return (
<Provider store={store} key={Math.random()}>
<Router history={history} key={Math.random()}>
<div>
<Route exact={true} path="/" component={GenerateWallet} />
<Route path="/view-wallet" component={ViewWallet} />
<Route path="/help" component={Help} />
<Route path="/swap" component={Swap} />
<Route path="/send-transaction" component={SendTransaction} />
<Route path="/contracts" component={Contracts} />
<Route path="/ens" component={ENS} />
<Route
path="/sign-and-verify-message"
component={SignAndVerifyMessage}
/>
<Route path="/pushTx" component={BroadcastTx} />
<LegacyRoutes />
</div>
</Router>
</Provider>
);
}
}
const LegacyRoutes = withRouter(props => {
const { history } = props;
const { pathname, hash } = props.location;
if (pathname === '/') {
switch (hash) {
case '#send-transaction':
case '#offline-transaction':
history.push('/send-transaction');
break;
case '#generate-wallet':
history.push('/');
break;
case '#swap':
history.push('/swap');
break;
case '#contracts':
history.push('/contracts');
break;
case '#ens':
history.push('/ens');
break;
case '#view-wallet-info':
history.push('/view-wallet');
break;
case '#check-tx-status':
history.push('/check-tx-status');
break;
}
}
return (
<Switch>
<Redirect from="/signmsg.html" to="/sign-and-verify-message" />
<Redirect from="/helpers.html" to="/helpers" />
</Switch>
);
});

View File

@ -1,5 +1,6 @@
import * as interfaces from './actionTypes';
import { 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
};
}

View File

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

View File

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

View File

@ -1,22 +0,0 @@
import * as interfaces from './actionTypes';
import { TypeKeys } from './constants';
export function accessContract(
address: string,
abiJson: string
): interfaces.AccessContractAction {
return {
type: TypeKeys.ACCESS_CONTRACT,
address,
abiJson
};
}
export function setInteractiveContract(
functions: interfaces.ABIFunction[]
): interfaces.SetInteractiveContractAction {
return {
type: TypeKeys.SET_INTERACTIVE_CONTRACT,
functions
};
}

View File

@ -1,31 +0,0 @@
import { TypeKeys } from './constants';
/***** Set Interactive Contract *****/
export interface ABIFunctionField {
name: string;
type: string;
}
export interface ABIFunction {
name: string;
type: string;
constant: boolean;
inputs: ABIFunctionField[];
outputs: ABIFunctionField[];
}
export interface SetInteractiveContractAction {
type: TypeKeys.SET_INTERACTIVE_CONTRACT;
functions: ABIFunction[];
}
/***** Access Contract *****/
export interface AccessContractAction {
type: TypeKeys.ACCESS_CONTRACT;
address: string;
abiJson: string;
}
/*** Union Type ***/
export type ContractsAction =
| SetInteractiveContractAction
| AccessContractAction;

View File

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

View File

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

View File

@ -1,14 +1,19 @@
import { BigNumber } from 'bignumber.js';
import { TokenValue, Wei } from 'libs/units';
export interface TokenValues {
[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;
}

View File

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

View File

@ -1,4 +1,4 @@
import { PrivKeyWallet } from 'libs/wallet';
import { generate } from 'ethereumjs-wallet';
import * as interfaces from './actionTypes';
import { 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
};
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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;
}, {});
}
}

View File

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

View File

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

View File

@ -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}
/>
)
}

View File

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

View File

@ -0,0 +1,230 @@
import React from 'react';
import classnames from 'classnames';
import Modal, { IButton } from 'components/ui/Modal';
import translate from 'translations';
import { NETWORKS, CustomNodeConfig } from 'config/data';
const NETWORK_KEYS = Object.keys(NETWORKS);
interface Input {
name: string;
placeholder?: string;
type?: string;
}
interface Props {
handleAddCustomNode(node: CustomNodeConfig): void;
handleClose(): void;
}
interface State {
name: string;
url: string;
port: string;
network: string;
hasAuth: boolean;
username: string;
password: string;
}
export default class CustomNodeModal extends React.Component<Props, State> {
public state: State = {
name: '',
url: '',
port: '',
network: NETWORK_KEYS[0],
hasAuth: false,
username: '',
password: '',
};
public render() {
const { handleClose } = this.props;
const isHttps = window.location.protocol.includes('https');
const invalids = this.getInvalids();
const buttons: IButton[] = [{
type: 'primary',
text: translate('NODE_CTA'),
onClick: this.saveAndAdd,
disabled: !!Object.keys(invalids).length,
}, {
text: translate('x_Cancel'),
onClick: handleClose
}];
return (
<Modal
title={translate('NODE_Title')}
isOpen={true}
buttons={buttons}
handleClose={handleClose}
>
<div>
{isHttps &&
<div className="alert alert-danger small">
{translate('NODE_Warning')}
</div>
}
<form>
<div className="row">
<div className="col-sm-7">
<label>{translate('NODE_Name')}</label>
{this.renderInput({
name: 'name',
placeholder: 'My Node',
}, invalids)}
</div>
<div className="col-sm-5">
<label>Network</label>
<select
className="form-control"
name="network"
value={this.state.network}
onChange={this.handleChange}
>
{NETWORK_KEYS.map((net) =>
<option key={net} value={net}>{net}</option>
)}
</select>
</div>
</div>
<div className="row">
<div className="col-sm-9">
<label>URL</label>
{this.renderInput({
name: 'url',
placeholder: 'http://127.0.0.1/',
}, invalids)}
</div>
<div className="col-sm-3">
<label>{translate('NODE_Port')}</label>
{this.renderInput({
name: 'port',
placeholder: '8545',
type: 'number',
}, invalids)}
</div>
</div>
<div className="row">
<div className="col-sm-12">
<label>
<input
type="checkbox"
name="hasAuth"
checked={this.state.hasAuth}
onChange={this.handleCheckbox}
/>
{' '}
<span>HTTP Basic Authentication</span>
</label>
</div>
</div>
{this.state.hasAuth &&
<div className="row">
<div className="col-sm-6">
<label>Username</label>
{this.renderInput({ name: 'username' }, invalids)}
</div>
<div className="col-sm-6">
<label>Password</label>
{this.renderInput({
name: 'password',
type: 'password',
}, invalids)}
</div>
</div>
}
</form>
</div>
</Modal>
);
}
private renderInput(input: Input, invalids: { [key: string]: boolean }) {
return <input
className={classnames({
'form-control': true,
'is-invalid': this.state[input.name] && invalids[input.name],
})}
value={this.state[name]}
onChange={this.handleChange}
{...input}
/>;
}
private getInvalids(): { [key: string]: boolean } {
const {
url,
port,
hasAuth,
username,
password,
} = this.state;
const required = ["name", "url", "port", "network"];
const invalids: { [key: string]: boolean } = {};
// Required fields
required.forEach((field) => {
if (!this.state[field]) {
invalids[field] = true;
}
});
// Somewhat valid URL, not 100% fool-proof
if (!/https?\:\/\/\w+/i.test(url)) {
invalids.url = true;
}
// Numeric port within range
const iport = parseInt(port, 10);
if (!iport || iport < 1 || iport > 65535) {
invalids.port = true;
}
// If they have auth, make sure it's provided
if (hasAuth) {
if (!username) {
invalids.username = true;
}
if (!password) {
invalids.password = true;
}
}
return invalids;
}
private handleChange = (ev: React.FormEvent<
HTMLInputElement | HTMLSelectElement
>) => {
const { name, value } = ev.currentTarget;
this.setState({ [name as any]: value });
};
private handleCheckbox = (ev: React.FormEvent<HTMLInputElement>) => {
const { name } = ev.currentTarget;
this.setState({ [name as any]: !this.state[name] });
};
private saveAndAdd = () => {
const node: CustomNodeConfig = {
name: this.state.name.trim(),
url: this.state.url.trim(),
port: parseInt(this.state.port, 10),
network: this.state.network,
};
if (this.state.hasAuth) {
node.auth = {
username: this.state.username,
password: this.state.password,
};
}
this.props.handleAddCustomNode(node);
};
}

View File

@ -1,4 +1,3 @@
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import 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() {

View File

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

View File

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

View File

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

View File

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

View File

@ -1,37 +0,0 @@
import React, { Component } from 'react';
import { Provider } from 'react-redux';
import { Router, Route } from 'react-router-dom';
// Components
import ENS from 'containers/Tabs/ENS';
import GenerateWallet from 'containers/Tabs/GenerateWallet';
import Help from 'containers/Tabs/Help';
import SendTransaction from 'containers/Tabs/SendTransaction';
import Swap from 'containers/Tabs/Swap';
import ViewWallet from 'containers/Tabs/ViewWallet';
// TODO: fix this
interface Props {
store: any;
history: any;
}
export default class Root extends Component<Props, {}> {
public render() {
const { store, history } = this.props;
// key={Math.random()} = hack for HMR from https://github.com/webpack/webpack-dev-server/issues/395
return (
<Provider store={store} key={Math.random()}>
<Router history={history} key={Math.random()}>
<div>
<Route exact={true} path="/" component={GenerateWallet} />
<Route path="/view-wallet" component={ViewWallet} />
<Route path="/help" component={Help} />
<Route path="/swap" component={Swap} />
<Route path="/send-transaction" component={SendTransaction} />
<Route path="/ens" component={ENS} />
</div>
</Router>
</Provider>
);
}
}

View File

@ -23,6 +23,7 @@
&-table {
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 {

View File

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

View File

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

View File

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

View File

@ -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)) {

View File

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

View File

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

View File

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

View File

@ -5,7 +5,8 @@ import {
unlockMnemonic,
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,

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,11 +1,15 @@
import React, { Component } from 'react';
import 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);
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,13 +1,27 @@
import React from 'react';
import './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;

View File

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

View File

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

View File

@ -1,7 +1,8 @@
import { EtherscanNode, InfuraNode, RPCNode } from 'libs/nodes';
import { EtherscanNode, InfuraNode, RPCNode, Web3Node } from 'libs/nodes';
import { networkIdToName } from 'libs/values';
export const languages = require('./languages.json');
// 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);
}
});
});
}

View File

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

View File

@ -5,6 +5,7 @@ import {
} from 'actions/notifications';
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>
);
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,6 +1,7 @@
import { ContinueToPaperAction } from 'actions/generateWallet';
import { 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();
};
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,4 +1,4 @@
@import "common/sass/variables";
@import 'common/sass/variables';
.CurrencySwap {
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